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:
parent
5805a41249
commit
6520a481ca
@ -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
3
.gitignore
vendored
@ -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
|
||||
|
92
packages/renderer/headerAnchor.ts
Normal file
92
packages/renderer/headerAnchor.ts
Normal 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;
|
||||
});
|
||||
}
|
@ -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,
|
||||
};
|
||||
|
@ -64,3 +64,23 @@ 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;
|
||||
}
|
@ -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,
|
||||
|
21
packages/server/src/routes/index/help.ts
Normal file
21
packages/server/src/routes/index/help.ts
Normal 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;
|
@ -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,
|
||||
};
|
||||
|
@ -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,18 +143,11 @@ export default class MustacheService {
|
||||
return '';
|
||||
}
|
||||
|
||||
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`;
|
||||
private async renderFileContent(filePath: string, view: View, globalParams: GlobalParams = null): Promise<string> {
|
||||
const ext = extname(filePath);
|
||||
|
||||
globalParams = {
|
||||
...this.defaultLayoutOptions,
|
||||
...globalParams,
|
||||
userDisplayName: this.userDisplayName(globalParams ? globalParams.owner : null),
|
||||
};
|
||||
|
||||
const contentHtml = Mustache.render(
|
||||
if (ext === '.mustache') {
|
||||
return Mustache.render(
|
||||
await this.loadTemplateContent(filePath),
|
||||
{
|
||||
...view.content,
|
||||
@ -133,6 +155,34 @@ export default class MustacheService {
|
||||
},
|
||||
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 = await this.viewFilePath(view.path);
|
||||
|
||||
globalParams = {
|
||||
...this.defaultLayoutOptions,
|
||||
...globalParams,
|
||||
userDisplayName: this.userDisplayName(globalParams ? globalParams.owner : null),
|
||||
};
|
||||
|
||||
const contentHtml = await this.renderFileContent(filePath, view, globalParams);
|
||||
|
||||
const layoutView: any = {
|
||||
global: globalParams,
|
||||
|
20
packages/server/src/views/index/help.md
Normal file
20
packages/server/src/views/index/help.md
Normal 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)
|
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user