1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-08 13:06:15 +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.d.ts
packages/renderer/MdToHtml/validateLinks.js packages/renderer/MdToHtml/validateLinks.js
packages/renderer/MdToHtml/validateLinks.js.map 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.d.ts
packages/renderer/htmlUtils.js packages/renderer/htmlUtils.js
packages/renderer/htmlUtils.js.map 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.d.ts
packages/renderer/MdToHtml/validateLinks.js packages/renderer/MdToHtml/validateLinks.js
packages/renderer/MdToHtml/validateLinks.js.map 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.d.ts
packages/renderer/htmlUtils.js packages/renderer/htmlUtils.js
packages/renderer/htmlUtils.js.map 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 utils from './utils';
import setupLinkify from './MdToHtml/setupLinkify'; import setupLinkify from './MdToHtml/setupLinkify';
import validateLinks from './MdToHtml/validateLinks'; import validateLinks from './MdToHtml/validateLinks';
import headerAnchor from './headerAnchor';
const assetsToHeaders = require('./assetsToHeaders'); const assetsToHeaders = require('./assetsToHeaders');
export { export {
@ -13,6 +14,7 @@ export {
HtmlToHtml, HtmlToHtml,
setupLinkify, setupLinkify,
validateLinks, validateLinks,
headerAnchor,
assetsToHeaders, assetsToHeaders,
utils, utils,
}; };

View File

@ -63,4 +63,24 @@ ul.pagination-list li {
.readable-block { .readable-block {
max-width: 740px; 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_ = { config_ = {
appVersion: packageJson.version, appVersion: packageJson.version,
appName, appName,
isJoplinCloud: apiBaseUrl.includes('.joplincloud.com'), isJoplinCloud: apiBaseUrl.includes('.joplincloud.com') || apiBaseUrl.includes('.joplincloud.local'),
env: envType, env: envType,
rootDir: rootDir, rootDir: rootDir,
viewDir: viewDir, 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 indexTerms from './index/terms';
import indexPrivacy from './index/privacy'; import indexPrivacy from './index/privacy';
import indexUpgrade from './index/upgrade'; import indexUpgrade from './index/upgrade';
import indexHelp from './index/help';
import defaultRoute from './default'; import defaultRoute from './default';
@ -54,6 +55,7 @@ const routes: Routers = {
'terms': indexTerms, 'terms': indexTerms,
'privacy': indexPrivacy, 'privacy': indexPrivacy,
'upgrade': indexUpgrade, 'upgrade': indexUpgrade,
'help': indexHelp,
'': defaultRoute, '': defaultRoute,
}; };

View File

@ -1,10 +1,13 @@
import * as Mustache from 'mustache'; import * as Mustache from 'mustache';
import * as fs from 'fs-extra'; import * as fs from 'fs-extra';
import { extname } from 'path';
import config from '../config'; import config from '../config';
import { filename } from '@joplin/lib/path-utils'; import { filename } from '@joplin/lib/path-utils';
import { NotificationView } from '../utils/types'; import { NotificationView } from '../utils/types';
import { User } from '../services/database/types'; import { User } from '../services/database/types';
import { makeUrl, UrlType } from '../utils/routeUtils'; import { makeUrl, UrlType } from '../utils/routeUtils';
import MarkdownIt = require('markdown-it');
import { headerAnchor } from '@joplin/renderer';
export interface RenderOptions { export interface RenderOptions {
partials?: any; partials?: any;
@ -37,6 +40,7 @@ interface GlobalParams {
showErrorStackTraces?: boolean; showErrorStackTraces?: boolean;
userDisplayName?: string; userDisplayName?: string;
supportEmail?: string; supportEmail?: string;
isJoplinCloud?: boolean;
} }
export function isView(o: any): boolean { export function isView(o: any): boolean {
@ -50,6 +54,7 @@ export default class MustacheService {
private baseAssetUrl_: string; private baseAssetUrl_: string;
private prefersDarkEnabled_: boolean = true; private prefersDarkEnabled_: boolean = true;
private partials_: Record<string, string> = {}; private partials_: Record<string, string> = {};
private fileContentCache_: Record<string, string> = {};
public constructor(viewDir: string, baseAssetUrl: string) { public constructor(viewDir: string, baseAssetUrl: string) {
this.viewDir_ = viewDir; this.viewDir_ = viewDir;
@ -92,11 +97,35 @@ export default class MustacheService {
termsUrl: config().termsEnabled ? makeUrl(UrlType.Terms) : '', termsUrl: config().termsEnabled ? makeUrl(UrlType.Terms) : '',
privacyUrl: config().termsEnabled ? makeUrl(UrlType.Privacy) : '', privacyUrl: config().termsEnabled ? makeUrl(UrlType.Privacy) : '',
showErrorStackTraces: config().showErrorStackTraces, 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> { 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[] { private resolvesFilePaths(type: string, paths: string[]): string[] {
@ -114,10 +143,38 @@ export default class MustacheService {
return ''; 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> { public async renderView(view: View, globalParams: GlobalParams = null): Promise<string> {
const cssFiles = this.resolvesFilePaths('css', view.cssFiles || []); const cssFiles = this.resolvesFilePaths('css', view.cssFiles || []);
const jsFiles = this.resolvesFilePaths('js', view.jsFiles || []); const jsFiles = this.resolvesFilePaths('js', view.jsFiles || []);
const filePath = `${this.viewDir_}/${view.path}.mustache`; const filePath = await this.viewFilePath(view.path);
globalParams = { globalParams = {
...this.defaultLayoutOptions, ...this.defaultLayoutOptions,
@ -125,14 +182,7 @@ export default class MustacheService {
userDisplayName: this.userDisplayName(globalParams ? globalParams.owner : null), userDisplayName: this.userDisplayName(globalParams ? globalParams.owner : null),
}; };
const contentHtml = Mustache.render( const contentHtml = await this.renderFileContent(filePath, view, globalParams);
await this.loadTemplateContent(filePath),
{
...view.content,
global: globalParams,
},
this.partials_
);
const layoutView: any = { const layoutView: any = {
global: globalParams, 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> <a class="navbar-item" href="{{{global.baseUrl}}}/changes">Log</a>
</div> </div>
<div class="navbar-end"> <div class="navbar-end">
{{#global.isJoplinCloud}}
<a class="navbar-item" href="{{{global.baseUrl}}}/help">Help</a>
{{/global.isJoplinCloud}}
<div class="navbar-item"> <div class="navbar-item">
<form method="post" action="{{{global.baseUrl}}}/logout"> <form method="post" action="{{{global.baseUrl}}}/logout">
<button class="button is-dark">Logout</button> <button class="button is-dark">Logout</button>

View File

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

View File

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