1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

Server: Added Help page for Joplin Cloud

This commit is contained in:
Laurent Cozic 2021-08-31 13:46:46 +01:00
parent 5805a41249
commit 6520a481ca
13 changed files with 305 additions and 85 deletions

View File

@ -1725,6 +1725,9 @@ packages/renderer/MdToHtml/setupLinkify.js.map
packages/renderer/MdToHtml/validateLinks.d.ts
packages/renderer/MdToHtml/validateLinks.js
packages/renderer/MdToHtml/validateLinks.js.map
packages/renderer/headerAnchor.d.ts
packages/renderer/headerAnchor.js
packages/renderer/headerAnchor.js.map
packages/renderer/htmlUtils.d.ts
packages/renderer/htmlUtils.js
packages/renderer/htmlUtils.js.map

3
.gitignore vendored
View File

@ -1710,6 +1710,9 @@ packages/renderer/MdToHtml/setupLinkify.js.map
packages/renderer/MdToHtml/validateLinks.d.ts
packages/renderer/MdToHtml/validateLinks.js
packages/renderer/MdToHtml/validateLinks.js.map
packages/renderer/headerAnchor.d.ts
packages/renderer/headerAnchor.js
packages/renderer/headerAnchor.js.map
packages/renderer/htmlUtils.d.ts
packages/renderer/htmlUtils.js
packages/renderer/htmlUtils.js.map

View File

@ -0,0 +1,92 @@
export default function(markdownIt: any) {
markdownIt.core.ruler.push('anchorHeader', (state: any): boolean => {
const tokens = state.tokens;
const Token = state.Token;
const doneNames = [];
const headingTextToAnchorName = (text: string, doneNames: string[]) => {
const allowed = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let lastWasDash = true;
let output = '';
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (allowed.indexOf(c) < 0) {
if (lastWasDash) continue;
lastWasDash = true;
output += '-';
} else {
lastWasDash = false;
output += c;
}
}
output = output.toLowerCase();
while (output.length && output[output.length - 1] === '-') {
output = output.substr(0, output.length - 1);
}
let temp = output;
let index = 1;
while (doneNames.indexOf(temp) >= 0) {
temp = `${output}-${index}`;
index++;
}
output = temp;
return output;
};
const createAnchorTokens = (anchorName: string) => {
const output = [];
{
const token = new Token('heading_anchor_open', 'a', 1);
token.attrs = [
['name', anchorName],
['href', `#${anchorName}`],
['class', 'heading-anchor'],
];
output.push(token);
}
{
const token = new Token('text', '', 0);
token.content = '🔗';
output.push(token);
}
{
const token = new Token('heading_anchor_close', 'a', -1);
output.push(token);
}
return output;
};
let insideHeading = false;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type === 'heading_open') {
insideHeading = true;
continue;
}
if (token.type === 'heading_close') {
insideHeading = false;
continue;
}
if (insideHeading && token.type === 'inline') {
const anchorName = headingTextToAnchorName(token.content, doneNames);
doneNames.push(anchorName);
const anchorTokens = createAnchorTokens(anchorName);
// token.children = anchorTokens.concat(token.children);
token.children = token.children.concat(anchorTokens);
}
}
return true;
});
}

View File

@ -4,6 +4,7 @@ import HtmlToHtml from './HtmlToHtml';
import utils from './utils';
import setupLinkify from './MdToHtml/setupLinkify';
import validateLinks from './MdToHtml/validateLinks';
import headerAnchor from './headerAnchor';
const assetsToHeaders = require('./assetsToHeaders');
export {
@ -13,6 +14,7 @@ export {
HtmlToHtml,
setupLinkify,
validateLinks,
headerAnchor,
assetsToHeaders,
utils,
};

View File

@ -63,4 +63,24 @@ ul.pagination-list li {
.readable-block {
max-width: 740px;
}
a.heading-anchor {
display: inline-block;
opacity: 0;
width: 1.3em;
font-size: 0.7em;
margin-left: 0.4em;
line-height: 1em;
text-decoration: none;
transition: opacity 0.3s;
}
a.heading-anchor:hover,
h1:hover a.heading-anchor,
h2:hover a.heading-anchor,
h3:hover a.heading-anchor,
h4:hover a.heading-anchor,
h5:hover a.heading-anchor,
h6:hover a.heading-anchor {
opacity: 1;
}

View File

@ -149,7 +149,7 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any
config_ = {
appVersion: packageJson.version,
appName,
isJoplinCloud: apiBaseUrl.includes('.joplincloud.com'),
isJoplinCloud: apiBaseUrl.includes('.joplincloud.com') || apiBaseUrl.includes('.joplincloud.local'),
env: envType,
rootDir: rootDir,
viewDir: viewDir,

View File

@ -0,0 +1,21 @@
import { SubPath } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import { ErrorMethodNotAllowed } from '../../utils/errors';
import defaultView from '../../utils/defaultView';
const router: Router = new Router(RouteType.Web);
router.public = true;
router.get('help', async (_path: SubPath, ctx: AppContext) => {
if (ctx.method === 'GET') {
const view = defaultView('help', 'Help');
return view;
}
throw new ErrorMethodNotAllowed();
});
export default router;

View File

@ -25,6 +25,7 @@ import indexStripe from './index/stripe';
import indexTerms from './index/terms';
import indexPrivacy from './index/privacy';
import indexUpgrade from './index/upgrade';
import indexHelp from './index/help';
import defaultRoute from './default';
@ -54,6 +55,7 @@ const routes: Routers = {
'terms': indexTerms,
'privacy': indexPrivacy,
'upgrade': indexUpgrade,
'help': indexHelp,
'': defaultRoute,
};

View File

@ -1,10 +1,13 @@
import * as Mustache from 'mustache';
import * as fs from 'fs-extra';
import { extname } from 'path';
import config from '../config';
import { filename } from '@joplin/lib/path-utils';
import { NotificationView } from '../utils/types';
import { User } from '../services/database/types';
import { makeUrl, UrlType } from '../utils/routeUtils';
import MarkdownIt = require('markdown-it');
import { headerAnchor } from '@joplin/renderer';
export interface RenderOptions {
partials?: any;
@ -37,6 +40,7 @@ interface GlobalParams {
showErrorStackTraces?: boolean;
userDisplayName?: string;
supportEmail?: string;
isJoplinCloud?: boolean;
}
export function isView(o: any): boolean {
@ -50,6 +54,7 @@ export default class MustacheService {
private baseAssetUrl_: string;
private prefersDarkEnabled_: boolean = true;
private partials_: Record<string, string> = {};
private fileContentCache_: Record<string, string> = {};
public constructor(viewDir: string, baseAssetUrl: string) {
this.viewDir_ = viewDir;
@ -92,11 +97,35 @@ export default class MustacheService {
termsUrl: config().termsEnabled ? makeUrl(UrlType.Terms) : '',
privacyUrl: config().termsEnabled ? makeUrl(UrlType.Privacy) : '',
showErrorStackTraces: config().showErrorStackTraces,
isJoplinCloud: config().isJoplinCloud,
};
}
private async viewFilePath(name: string): Promise<string> {
const pathsToTry = [
`${this.viewDir_}/${name}.mustache`,
`${this.viewDir_}/${name}.md`,
];
for (const p of pathsToTry) {
if (await fs.pathExists(p)) return p;
}
throw new Error(`Cannot find view file: ${name}`);
}
private async loadTemplateContent(path: string): Promise<string> {
return fs.readFile(path, 'utf8');
if (this.fileContentCache_[path]) return this.fileContentCache_[path];
try {
const output = await fs.readFile(path, 'utf8');
this.fileContentCache_[path] = output;
return output;
} catch (error) {
// Shouldn't have to do this but node.fs error messages are useless
// so throw a new error to get a proper stack trace.
throw new Error(`Cannot load view ${path}: ${error.message}`);
}
}
private resolvesFilePaths(type: string, paths: string[]): string[] {
@ -114,10 +143,38 @@ export default class MustacheService {
return '';
}
private async renderFileContent(filePath: string, view: View, globalParams: GlobalParams = null): Promise<string> {
const ext = extname(filePath);
if (ext === '.mustache') {
return Mustache.render(
await this.loadTemplateContent(filePath),
{
...view.content,
global: globalParams,
},
this.partials_
);
} else if (ext === '.md') {
const markdownIt = new MarkdownIt({
linkify: true,
});
markdownIt.use(headerAnchor);
// Need to wrap in a `content` element so that default styles are
// applied to it.
// https://github.com/jgthms/bulma/issues/3232#issuecomment-909176563
return `<div class="content">${markdownIt.render(await this.loadTemplateContent(filePath))}</div>`;
}
throw new Error(`Unsupported view extension: ${ext}`);
}
public async renderView(view: View, globalParams: GlobalParams = null): Promise<string> {
const cssFiles = this.resolvesFilePaths('css', view.cssFiles || []);
const jsFiles = this.resolvesFilePaths('js', view.jsFiles || []);
const filePath = `${this.viewDir_}/${view.path}.mustache`;
const filePath = await this.viewFilePath(view.path);
globalParams = {
...this.defaultLayoutOptions,
@ -125,14 +182,7 @@ export default class MustacheService {
userDisplayName: this.userDisplayName(globalParams ? globalParams.owner : null),
};
const contentHtml = Mustache.render(
await this.loadTemplateContent(filePath),
{
...view.content,
global: globalParams,
},
this.partials_
);
const contentHtml = await this.renderFileContent(filePath, view, globalParams);
const layoutView: any = {
global: globalParams,

View File

@ -0,0 +1,20 @@
# Joplin Cloud Help
## How can I change my details?
Most of your details can be found in your Profile page. To open it, click on the Profile button - this is the button in the top right corner, with your name or email on it.
## How can I cancel my account?
Click on the [Profile button](#how-can-i-change-my-details), then scroll down and click on "Cancel subscription".
## How can I get more space?
If you are on a Basic account, you may upgrade to a Pro account to get more space. Click on the [Profile button](#how-can-i-change-my-details), then scroll down and select "Upgrade account".
If you are already on a Pro account, and you need more space for specific reasons, please contact us as we may increase the cap in some cases.
## Further information
- [Joplin Cloud Privacy Policy](/privacy)
- [Joplin Cloud Terms & Conditions](/terms)

View File

@ -18,6 +18,9 @@
<a class="navbar-item" href="{{{global.baseUrl}}}/changes">Log</a>
</div>
<div class="navbar-end">
{{#global.isJoplinCloud}}
<a class="navbar-item" href="{{{global.baseUrl}}}/help">Help</a>
{{/global.isJoplinCloud}}
<div class="navbar-item">
<form method="post" action="{{{global.baseUrl}}}/logout">
<button class="button is-dark">Logout</button>

View File

@ -19,6 +19,7 @@
"license": "MIT",
"dependencies": {
"@joplin/lib": "~2.4",
"@joplin/renderer": "~2.4",
"execa": "^4.1.0",
"fs-extra": "^4.0.3",
"gettext-parser": "^1.3.0",

View File

@ -2,6 +2,7 @@ import * as Mustache from 'mustache';
import { filename } from '@joplin/lib/path-utils';
import * as fs from 'fs-extra';
import { TemplateParams } from './types';
import { headerAnchor } from '@joplin/renderer';
const MarkdownIt = require('markdown-it');
export async function loadMustachePartials(partialDir: string) {
@ -46,94 +47,96 @@ export function markdownToPageHtml(md: string, templateParams: TemplateParams):
}
});
markdownIt.core.ruler.push('checkbox', (state: any) => {
const tokens = state.tokens;
const Token = state.Token;
const doneNames = [];
markdownIt.use(headerAnchor);
const headingTextToAnchorName = (text: string, doneNames: string[]) => {
const allowed = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let lastWasDash = true;
let output = '';
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (allowed.indexOf(c) < 0) {
if (lastWasDash) continue;
lastWasDash = true;
output += '-';
} else {
lastWasDash = false;
output += c;
}
}
// markdownIt.core.ruler.push('checkbox', (state: any) => {
// const tokens = state.tokens;
// const Token = state.Token;
// const doneNames = [];
output = output.toLowerCase();
// const headingTextToAnchorName = (text: string, doneNames: string[]) => {
// const allowed = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
// let lastWasDash = true;
// let output = '';
// for (let i = 0; i < text.length; i++) {
// const c = text[i];
// if (allowed.indexOf(c) < 0) {
// if (lastWasDash) continue;
// lastWasDash = true;
// output += '-';
// } else {
// lastWasDash = false;
// output += c;
// }
// }
while (output.length && output[output.length - 1] === '-') {
output = output.substr(0, output.length - 1);
}
// output = output.toLowerCase();
let temp = output;
let index = 1;
while (doneNames.indexOf(temp) >= 0) {
temp = `${output}-${index}`;
index++;
}
output = temp;
// while (output.length && output[output.length - 1] === '-') {
// output = output.substr(0, output.length - 1);
// }
return output;
};
// let temp = output;
// let index = 1;
// while (doneNames.indexOf(temp) >= 0) {
// temp = `${output}-${index}`;
// index++;
// }
// output = temp;
const createAnchorTokens = (anchorName: string) => {
const output = [];
// return output;
// };
{
const token = new Token('heading_anchor_open', 'a', 1);
token.attrs = [
['name', anchorName],
['href', `#${anchorName}`],
['class', 'heading-anchor'],
];
output.push(token);
}
// const createAnchorTokens = (anchorName: string) => {
// const output = [];
{
const token = new Token('text', '', 0);
token.content = '🔗';
output.push(token);
}
// {
// const token = new Token('heading_anchor_open', 'a', 1);
// token.attrs = [
// ['name', anchorName],
// ['href', `#${anchorName}`],
// ['class', 'heading-anchor'],
// ];
// output.push(token);
// }
{
const token = new Token('heading_anchor_close', 'a', -1);
output.push(token);
}
// {
// const token = new Token('text', '', 0);
// token.content = '🔗';
// output.push(token);
// }
return output;
};
// {
// const token = new Token('heading_anchor_close', 'a', -1);
// output.push(token);
// }
let insideHeading = false;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
// return output;
// };
if (token.type === 'heading_open') {
insideHeading = true;
continue;
}
// let insideHeading = false;
// for (let i = 0; i < tokens.length; i++) {
// const token = tokens[i];
if (token.type === 'heading_close') {
insideHeading = false;
continue;
}
// if (token.type === 'heading_open') {
// insideHeading = true;
// continue;
// }
if (insideHeading && token.type === 'inline') {
const anchorName = headingTextToAnchorName(token.content, doneNames);
doneNames.push(anchorName);
const anchorTokens = createAnchorTokens(anchorName);
// token.children = anchorTokens.concat(token.children);
token.children = token.children.concat(anchorTokens);
}
}
});
// if (token.type === 'heading_close') {
// insideHeading = false;
// continue;
// }
// if (insideHeading && token.type === 'inline') {
// const anchorName = headingTextToAnchorName(token.content, doneNames);
// doneNames.push(anchorName);
// const anchorTokens = createAnchorTokens(anchorName);
// // token.children = anchorTokens.concat(token.children);
// token.children = token.children.concat(anchorTokens);
// }
// }
// });
return renderMustache(markdownIt.render(md), templateParams);
}