1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-06-21 23:17:42 +02:00

Desktop: Added support for checkboxes and fixed various issues with WYSIWYG editor

This commit is contained in:
Laurent Cozic
2020-03-23 00:47:25 +00:00
parent c607444c23
commit 41acdce165
79 changed files with 12340 additions and 877 deletions

View File

@ -40,6 +40,7 @@ const BaseService = require('lib/services/BaseService');
const SearchEngine = require('lib/services/SearchEngine');
const KvStore = require('lib/services/KvStore');
const MigrationService = require('lib/services/MigrationService');
const { toSystemSlashes } = require('lib/path-utils.js');
class BaseApplication {
constructor() {
@ -580,7 +581,7 @@ class BaseApplication {
if (process && process.env && process.env.PORTABLE_EXECUTABLE_DIR) return `${process.env.PORTABLE_EXECUTABLE_DIR}/JoplinProfile`;
return `${os.homedir()}/.config/${Setting.value('appName')}`;
return toSystemSlashes(`${os.homedir()}/.config/${Setting.value('appName')}`, 'linux');
}
async start(argv) {

View File

@ -9,6 +9,9 @@ class HtmlToMd {
anchorNames: options.anchorNames ? options.anchorNames.map(n => n.trim().toLowerCase()) : [],
codeBlockStyle: 'fenced',
preserveImageTagsWithSize: !!options.preserveImageTagsWithSize,
bulletListMarker: '-',
emDelimiter: '*',
strongDelimiter: '**',
});
turndown.use(turndownPluginGfm);
turndown.remove('script');

View File

@ -55,14 +55,21 @@ class NoteBodyViewer extends Component {
this.forceUpdate();
}, 100);
},
paddingBottom: '3.8em', // Extra bottom padding to make it possible to scroll past the action button (so that it doesn't overlap the text)
highlightedKeywords: this.props.highlightedKeywords,
resources: this.props.noteResources, // await shared.attachedResources(bodyToRender),
codeTheme: theme.codeThemeCss,
postMessageSyntax: 'window.ReactNativeWebView.postMessage',
};
const result = await this.markupToHtml_.render(note.markup_language, bodyToRender, this.props.webViewStyle, mdOptions);
const result = await this.markupToHtml_.render(
note.markup_language,
bodyToRender,
{
bodyPaddingBottom: '3.8em', // Extra bottom padding to make it possible to scroll past the action button (so that it doesn't overlap the text)
...this.props.webViewStyle,
},
mdOptions
);
let html = result.html;
const resourceDownloadMode = Setting.value('sync.resourceDownloadMode');

View File

@ -73,7 +73,7 @@ class FsDriverBase {
// TODO: move out of here and make it part of joplin-renderer
// or assign to option using .bind(fsDriver())
async cacheCssToFile(cssStrings) {
const cssString = cssStrings.join('\n');
const cssString = Array.isArray(cssStrings) ? cssStrings.join('\n') : cssStrings;
const cssFilePath = `${Setting.value('tempDir')}/${md5(escape(cssString))}.css`;
if (!(await this.exists(cssFilePath))) {
await this.writeFile(cssFilePath, cssString, 'utf8');

View File

@ -40,6 +40,10 @@ class HtmlToHtml {
};
}
async allAssets(/* theme*/) {
return []; // TODO
}
async render(markup, theme, options) {
options = Object.assign({}, {
splitted: false,
@ -80,7 +84,7 @@ class HtmlToHtml {
};
}
let cssStrings = noteStyle(theme, options);
let cssStrings = noteStyle(theme);
if (options.splitted) {
const splitted = this.splitHtml(html);

View File

@ -36,6 +36,10 @@ class MarkupToHtml {
async render(markupLanguage, markup, theme, options) {
return this.renderer(markupLanguage).render(markup, theme, options);
}
async allAssets(markupLanguage, theme) {
return this.renderer(markupLanguage).allAssets(theme);
}
}
MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN = 1;

View File

@ -3,19 +3,22 @@ const md5 = require('md5');
const noteStyle = require('./noteStyle');
const { fileExtension } = require('./pathUtils');
const memoryCache = require('memory-cache');
// /!\/!\ Note: the order of rules is important!! /!\/!\
const rules = {
fence: require('./MdToHtml/rules/fence').default,
sanitize_html: require('./MdToHtml/rules/sanitize_html').default,
image: require('./MdToHtml/rules/image'),
checkbox: require('./MdToHtml/rules/checkbox'),
checkbox: require('./MdToHtml/rules/checkbox').default,
katex: require('./MdToHtml/rules/katex'),
link_open: require('./MdToHtml/rules/link_open'),
html_image: require('./MdToHtml/rules/html_image'),
highlight_keywords: require('./MdToHtml/rules/highlight_keywords'),
code_inline: require('./MdToHtml/rules/code_inline'),
fence: require('./MdToHtml/rules/fence').default,
fountain: require('./MdToHtml/rules/fountain'),
mermaid: require('./MdToHtml/rules/mermaid').default,
sanitize_html: require('./MdToHtml/rules/sanitize_html').default,
};
const setupLinkify = require('./MdToHtml/setupLinkify');
const hljs = require('highlight.js');
const uslug = require('uslug');
@ -77,6 +80,13 @@ class MdToHtml {
return this.tempDir_;
}
static pluginNames() {
const output = [];
for (const n in rules) output.push(n);
for (const n in plugins) output.push(n);
return output;
}
pluginOptions(name) {
let o = this.pluginOptions_[name] ? this.pluginOptions_[name] : {};
o = Object.assign({
@ -115,8 +125,10 @@ class MdToHtml {
throw new Error(`Unsupported inline mime type: ${mime}`);
}
} else {
const name = `${pluginName}/${asset.name}`;
files.push(Object.assign({}, asset, {
name: `${pluginName}/${asset.name}`,
name: name,
path: `pluginAssets/${name}`,
mime: mime,
}));
}
@ -124,21 +136,52 @@ class MdToHtml {
}
return {
files: files,
pluginAssets: files,
cssStrings: cssStrings,
};
}
async render(body, style = null, options = null) {
async allAssets(theme) {
const assets = {};
for (const key in rules) {
if (!this.pluginEnabled(key)) continue;
const rule = rules[key];
if (rule.style) {
assets[key] = rule.style(theme);
}
}
const processedAssets = this.processPluginAssets(assets);
processedAssets.cssStrings.splice(0, 0, noteStyle(theme));
const output = await this.outputAssetsToExternalAssets_(processedAssets);
return output.pluginAssets;
}
async outputAssetsToExternalAssets_(output) {
for (const cssString of output.cssStrings) {
output.pluginAssets.push(await this.fsDriver().cacheCssToFile(cssString));
}
delete output.cssStrings;
return output;
}
// "style" here is really the theme, as returned by themeStyle()
async render(body, theme = null, options = null) {
options = Object.assign({}, {
// In bodyOnly mode, the rendered Markdown is returned without the wrapper DIV
bodyOnly: false,
// In splitted mode, the CSS and HTML will be returned in separate properties.
// In non-splitted mode, CSS and HTML will be merged in the same document.
splitted: false,
// When this is true, all assets such as CSS or JS are returned as external
// files. Otherwise some of them might be in the cssStrings property.
externalAssetsOnly: false,
postMessageSyntax: 'postMessage',
paddingBottom: '0',
highlightedKeywords: [],
codeTheme: 'atom-one-light.css',
style: Object.assign({}, defaultNoteStyle),
theme: Object.assign({}, defaultNoteStyle, theme),
plugins: {},
}, options);
// The "codeHighlightCacheKey" option indicates what set of cached object should be
@ -150,7 +193,7 @@ class MdToHtml {
this.lastCodeHighlightCacheKey_ = options.codeHighlightCacheKey;
}
const cacheKey = md5(escape(body + JSON.stringify(options) + JSON.stringify(style)));
const cacheKey = md5(escape(body + JSON.stringify(options) + JSON.stringify(options.theme)));
const cachedOutput = this.cachedOutputs_[cacheKey];
if (cachedOutput) return cachedOutput;
@ -237,19 +280,13 @@ class MdToHtml {
// Using the `context` object, a plugin can define what additional assets they need (css, fonts, etc.) using context.pluginAssets.
// The calling application will need to handle loading these assets.
// /!\/!\ Note: the order of rules is important!! /!\/!\
for (const key in rules) {
if (!this.pluginEnabled(key)) continue;
const rule = rules[key];
const ruleInstall = rule.install ? rule.install : rule;
markdownIt.use(ruleInstall(context, { ...ruleOptions }));
}
markdownIt.use(rules.fence(context, ruleOptions));
markdownIt.use(rules.sanitize_html(context, ruleOptions));
markdownIt.use(rules.image(context, ruleOptions));
markdownIt.use(rules.checkbox(context, ruleOptions));
markdownIt.use(rules.link_open(context, ruleOptions));
markdownIt.use(rules.html_image(context, ruleOptions));
if (this.pluginEnabled('katex')) markdownIt.use(rules.katex(context, ruleOptions));
if (this.pluginEnabled('fountain')) markdownIt.use(rules.fountain(context, ruleOptions));
if (this.pluginEnabled('mermaid')) markdownIt.use(rules.mermaid(context, ruleOptions));
markdownIt.use(rules.highlight_keywords(context, ruleOptions));
markdownIt.use(rules.code_inline(context, ruleOptions));
markdownIt.use(markdownItAnchor, { slugify: uslugify });
for (const key in plugins) {
@ -260,41 +297,28 @@ class MdToHtml {
const renderedBody = markdownIt.render(body);
let cssStrings = noteStyle(style, options);
let cssStrings = noteStyle(options.theme);
const pluginAssets = this.processPluginAssets(context.pluginAssets);
cssStrings = cssStrings.concat(pluginAssets.cssStrings);
const output = {
pluginAssets: pluginAssets.files.map(f => {
return Object.assign({}, f, {
path: `pluginAssets/${f.name}`,
});
}),
};
if (options.bodyOnly) {
output.html = renderedBody;
return output;
}
let output = this.processPluginAssets(context.pluginAssets);
cssStrings = cssStrings.concat(output.cssStrings);
if (options.userCss) cssStrings.push(options.userCss);
const styleHtml = `<style>${cssStrings.join('\n')}</style>`;
const html = `${styleHtml}<div id="rendered-md">${renderedBody}</div>`;
output.html = html;
if (options.splitted) {
if (options.bodyOnly) {
output.html = renderedBody;
output.cssStrings = cssStrings;
output.html = `<div id="rendered-md">${renderedBody}</div>`;
} else {
const styleHtml = `<style>${cssStrings.join('\n')}</style>`;
output.html = `${styleHtml}<div id="rendered-md">${renderedBody}</div>`;
if (options.externalAssetsOnly) {
output.pluginAssets.push(await this.fsDriver().cacheCssToFile(cssStrings));
if (options.splitted) {
output.cssStrings = cssStrings;
output.html = `<div id="rendered-md">${renderedBody}</div>`;
}
}
if (options.externalAssetsOnly) output = await this.outputAssetsToExternalAssets_(output);
// Fow now, we keep only the last entry in the cache
this.cachedOutputs_ = {};
this.cachedOutputs_[cacheKey] = output;

View File

@ -1,153 +0,0 @@
let checkboxIndex_ = -1;
const checkboxStyle = `
/* Remove the indentation from the checkboxes at the root of the document
(otherwise they are too far right), but keep it for their children to allow
nested lists. Make sure this value matches the UL margin. */
.md-checkbox .checkbox-wrapper {
display: flex;
align-items: center;
}
li.md-checkbox {
list-style-type: none;
}
li.md-checkbox input[type=checkbox] {
margin-left: -1.71em;
margin-right: 0.7em;
}
`;
function createPrefixTokens(Token, id, checked, label, postMessageSyntax, sourceToken) {
let token = null;
const tokens = [];
// A bit hard to handle errors here and it's unlikely that the token won't have a valid
// map parameter, but if it does set it to a very high value, which will be more easy to notice
// in calling code.
const lineIndex = sourceToken.map && sourceToken.map.length ? sourceToken.map[0] : 99999999;
const checkedString = checked ? 'checked' : 'unchecked';
const labelId = `cb-label-${id}`;
const js = `
try {
if (this.checked) {
this.setAttribute('checked', 'checked');
} else {
this.removeAttribute('checked');
}
${postMessageSyntax}('checkboxclick:${checkedString}:${lineIndex}');
const label = document.getElementById("${labelId}");
label.classList.remove(this.checked ? 'checkbox-label-unchecked' : 'checkbox-label-checked');
label.classList.add(this.checked ? 'checkbox-label-checked' : 'checkbox-label-unchecked');
} catch (error) {
console.warn('Checkbox ${checkedString}:${lineIndex} error', error);
}
return true;
`;
token = new Token('checkbox_wrapper_open', 'div', 1);
token.attrs = [['class', 'checkbox-wrapper']];
tokens.push(token);
token = new Token('checkbox_input', 'input', 0);
token.attrs = [['type', 'checkbox'], ['id', id], ['onclick', js]];
if (checked) token.attrs.push(['checked', 'checked']);
tokens.push(token);
token = new Token('label_open', 'label', 1);
token.attrs = [['id', labelId], ['for', id], ['class', `checkbox-label-${checkedString}`]];
tokens.push(token);
if (label) {
token = new Token('text', '', 0);
token.content = label;
tokens.push(token);
}
return tokens;
}
function createSuffixTokens(Token) {
return [
new Token('label_close', 'label', -1),
new Token('checkbox_wrapper_close', 'div', -1),
];
}
function installRule(markdownIt, mdOptions, ruleOptions, context) {
markdownIt.core.ruler.push('checkbox', state => {
const tokens = state.tokens;
const Token = state.Token;
const checkboxPattern = /^\[([x|X| ])\] (.*)$/;
let currentListItem = null;
let processedFirstInline = false;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type === 'list_item_open') {
currentListItem = token;
processedFirstInline = false;
continue;
}
if (token.type === 'list_item_close') {
currentListItem = null;
processedFirstInline = false;
continue;
}
// Note that we only support list items that start with "-" (not with "*")
if (currentListItem && currentListItem.markup === '-' && !processedFirstInline && token.type === 'inline') {
processedFirstInline = true;
const firstChild = token.children && token.children.length ? token.children[0] : null;
if (!firstChild) continue;
const matches = checkboxPattern.exec(firstChild.content);
if (!matches || matches.length < 2) continue;
checkboxIndex_++;
const checked = matches[1] !== ' ';
const id = `md-checkbox-${checkboxIndex_}`;
const label = matches.length >= 3 ? matches[2] : '';
// Prepend the text content with the checkbox markup and the opening <label> tag
// then append the </label> tag at the end of the text content.
const prefix = createPrefixTokens(Token, id, checked, label, ruleOptions.postMessageSyntax, token);
const suffix = createSuffixTokens(Token);
token.children = markdownIt.utils.arrayReplaceAt(token.children, 0, prefix);
token.children = token.children.concat(suffix);
// Add a class to the <li> container so that it can be targetted with CSS.
let itemClass = currentListItem.attrGet('class');
if (!itemClass) itemClass = '';
itemClass += ' md-checkbox joplin-checkbox';
currentListItem.attrSet('class', itemClass.trim());
if (!('checkbox' in context.pluginAssets)) {
context.pluginAssets['checkbox'] = [
{
inline: true,
text: checkboxStyle,
mime: 'text/css',
},
];
}
}
}
});
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions, context);
};
};

View File

@ -0,0 +1,228 @@
let checkboxIndex_ = -1;
const pluginAssets:Function[] = [];
pluginAssets[1] = function() {
return [
{
inline: true,
mime: 'text/css',
text: `
/* Remove the indentation from the checkboxes at the root of the document
(otherwise they are too far right), but keep it for their children to allow
nested lists. Make sure this value matches the UL margin. */
.md-checkbox .checkbox-wrapper {
display: flex;
align-items: center;
}
li.md-checkbox {
list-style-type: none;
}
li.md-checkbox input[type=checkbox] {
margin-left: -1.71em;
margin-right: 0.7em;
}`,
},
];
};
pluginAssets[2] = function(theme:any) {
return [
{
inline: true,
mime: 'text/css',
text: `
/* https://stackoverflow.com/questions/7478336/only-detect-click-event-on-pseudo-element#comment39751366_7478344 */
ul.joplin-checklist li {
pointer-events: none;
}
ul.joplin-checklist {
list-style:none;
}
ul.joplin-checklist li::before {
content:"\\f14a";
font-family:ForkAwesome;
background-size: 16px 16px;
pointer-events: all;
cursor: pointer;
width: 1em;
height: 1em;
margin-left: -1.3em;
position: absolute;
color: ${theme.htmlColor};
}
.joplin-checklist li:not(.checked)::before {
content:"\\f0c8";
}`,
},
];
};
function createPrefixTokens(Token:any, id:string, checked:boolean, label:string, postMessageSyntax:string, sourceToken:any):any[] {
let token = null;
const tokens = [];
// A bit hard to handle errors here and it's unlikely that the token won't have a valid
// map parameter, but if it does set it to a very high value, which will be more easy to notice
// in calling code.
const lineIndex = sourceToken.map && sourceToken.map.length ? sourceToken.map[0] : 99999999;
const checkedString = checked ? 'checked' : 'unchecked';
const labelId = `cb-label-${id}`;
const js = `
try {
if (this.checked) {
this.setAttribute('checked', 'checked');
} else {
this.removeAttribute('checked');
}
${postMessageSyntax}('checkboxclick:${checkedString}:${lineIndex}');
const label = document.getElementById("${labelId}");
label.classList.remove(this.checked ? 'checkbox-label-unchecked' : 'checkbox-label-checked');
label.classList.add(this.checked ? 'checkbox-label-checked' : 'checkbox-label-unchecked');
} catch (error) {
console.warn('Checkbox ${checkedString}:${lineIndex} error', error);
}
return true;
`;
token = new Token('checkbox_wrapper_open', 'div', 1);
token.attrs = [['class', 'checkbox-wrapper']];
tokens.push(token);
token = new Token('checkbox_input', 'input', 0);
token.attrs = [['type', 'checkbox'], ['id', id], ['onclick', js]];
if (checked) token.attrs.push(['checked', 'checked']);
tokens.push(token);
token = new Token('label_open', 'label', 1);
token.attrs = [['id', labelId], ['for', id], ['class', `checkbox-label-${checkedString}`]];
tokens.push(token);
if (label) {
token = new Token('text', '', 0);
token.content = label;
tokens.push(token);
}
return tokens;
}
function createSuffixTokens(Token:any):any[] {
return [
new Token('label_close', 'label', -1),
new Token('checkbox_wrapper_close', 'div', -1),
];
}
// @ts-ignore: Keep the function signature as-is despite unusued arguments
function installRule(markdownIt:any, mdOptions:any, ruleOptions:any, context:any) {
const pluginOptions = { renderingType: 1, ...ruleOptions.plugins['checkbox'] };
markdownIt.core.ruler.push('checkbox', (state:any) => {
const tokens = state.tokens;
const Token = state.Token;
const checkboxPattern = /^\[([x|X| ])\] (.*)$/;
let currentListItem = null;
let processedFirstInline = false;
const lists = [];
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type === 'bullet_list_open') {
lists.push(token);
continue;
}
if (token.type === 'bullet_list_close') {
lists.pop();
continue;
}
if (token.type === 'list_item_open') {
currentListItem = token;
processedFirstInline = false;
continue;
}
if (token.type === 'list_item_close') {
currentListItem = null;
processedFirstInline = false;
continue;
}
// Note that we only support list items that start with "-" (not with "*")
if (currentListItem && currentListItem.markup === '-' && !processedFirstInline && token.type === 'inline') {
processedFirstInline = true;
const firstChild = token.children && token.children.length ? token.children[0] : null;
if (!firstChild) continue;
const matches = checkboxPattern.exec(firstChild.content);
if (!matches || matches.length < 2) continue;
const checked = matches[1] !== ' ';
const label = matches.length >= 3 ? matches[2] : '';
const currentList = lists[lists.length - 1];
if (pluginOptions.renderingType === 1) {
checkboxIndex_++;
const id = `md-checkbox-${checkboxIndex_}`;
// Prepend the text content with the checkbox markup and the opening <label> tag
// then append the </label> tag at the end of the text content.
const prefix = createPrefixTokens(Token, id, checked, label, ruleOptions.postMessageSyntax, token);
const suffix = createSuffixTokens(Token);
token.children = markdownIt.utils.arrayReplaceAt(token.children, 0, prefix);
token.children = token.children.concat(suffix);
// Add a class to the <li> container so that it can be targetted with CSS.
let itemClass = currentListItem.attrGet('class');
if (!itemClass) itemClass = '';
itemClass += ' md-checkbox joplin-checkbox';
currentListItem.attrSet('class', itemClass.trim());
} else {
const textToken = new Token('text', '', 0);
textToken.content = label;
const tokens = [];
tokens.push(textToken);
token.children = markdownIt.utils.arrayReplaceAt(token.children, 0, tokens);
const listClass = currentList.attrGet('class') || '';
if (listClass.indexOf('joplin-') < 0) currentList.attrSet('class', (`${listClass} joplin-checklist`).trim());
if (checked) {
currentListItem.attrSet('class', (`${currentListItem.attrGet('class') || ''} checked`).trim());
}
}
if (!('checkbox' in context.pluginAssets)) {
context.pluginAssets['checkbox'] = pluginAssets[pluginOptions.renderingType](ruleOptions.theme);
}
}
}
});
}
export default {
install: function(context:any, ruleOptions:any) {
return function(md:any, mdOptions:any) {
installRule(md, mdOptions, ruleOptions, context);
};
},
style: pluginAssets[2],
};

View File

@ -1,99 +1,106 @@
const fountain = require('../../vendor/fountain.min.js');
const fountainCss = `
.fountain {
font-family: monospace;
line-height: 107%;
max-width: 1000px;
margin-left: auto;
margin-right: auto;
}
const fountainCss = function() {
return [
{
inline: true,
mime: 'text/css',
text: `
.fountain {
font-family: monospace;
line-height: 107%;
max-width: 1000px;
margin-left: auto;
margin-right: auto;
}
.fountain .title-page,
.fountain .page {
box-shadow: 0 0 5px rgba(0,0,0,0.1);
border: 1px solid #d2d2d2;
padding: 10%;
margin-bottom: 2em;
}
.fountain .title-page,
.fountain .page {
box-shadow: 0 0 5px rgba(0,0,0,0.1);
border: 1px solid #d2d2d2;
padding: 10%;
margin-bottom: 2em;
}
.fountain h1,
.fountain h2,
.fountain h3,
.fountain h4,
.fountain p {
font-weight: normal;
line-height: 107%;
margin: 1em 0;
border: none;
font-size: 1em;
}
.fountain h1,
.fountain h2,
.fountain h3,
.fountain h4,
.fountain p {
font-weight: normal;
line-height: 107%;
margin: 1em 0;
border: none;
font-size: 1em;
}
.fountain .bold {
font-weight: bold;
}
.fountain .bold {
font-weight: bold;
}
.fountain .underline {
text-decoration: underline;
}
.fountain .underline {
text-decoration: underline;
}
.fountain .centered {
text-align: center;
}
.fountain .centered {
text-align: center;
}
.fountain h2 {
text-align: right;
}
.fountain h2 {
text-align: right;
}
.fountain .dialogue p.parenthetical {
margin-left: 11%;
}
.fountain .dialogue p.parenthetical {
margin-left: 11%;
}
.fountain .title-page .credit,
.fountain .title-page .authors,
.fountain .title-page .source {
text-align: center;
}
.fountain .title-page .credit,
.fountain .title-page .authors,
.fountain .title-page .source {
text-align: center;
}
.fountain .title-page h1 {
margin-bottom: 1.5em;
text-align: center;
}
.fountain .title-page h1 {
margin-bottom: 1.5em;
text-align: center;
}
.fountain .title-page .source {
margin-top: 1.5em;
}
.fountain .title-page .source {
margin-top: 1.5em;
}
.fountain .title-page .notes {
text-align: right;
margin: 3em 0;
}
.fountain .title-page .notes {
text-align: right;
margin: 3em 0;
}
.fountain .title-page h1 {
margin-bottom: 1.5em;
text-align: center;
}
.fountain .title-page h1 {
margin-bottom: 1.5em;
text-align: center;
}
.fountain .dialogue {
margin-left: 3em;
margin-right: 3em;
}
.fountain .dialogue {
margin-left: 3em;
margin-right: 3em;
}
.fountain .dialogue p,
.fountain .dialogue h1,
.fountain .dialogue h2,
.fountain .dialogue h3,
.fountain .dialogue h4 {
margin: 0;
}
.fountain .dialogue p,
.fountain .dialogue h1,
.fountain .dialogue h2,
.fountain .dialogue h3,
.fountain .dialogue h4 {
margin: 0;
}
.fountain .dialogue h1,
.fountain .dialogue h2,
.fountain .dialogue h3,
.fountain .dialogue h4 {
text-align: center;
}
`;
.fountain .dialogue h1,
.fountain .dialogue h2,
.fountain .dialogue h3,
.fountain .dialogue h4 {
text-align: center;
}`,
},
];
};
function renderFountainScript(markdownIt, content) {
const result = fountain.parse(content);
@ -114,13 +121,7 @@ function renderFountainScript(markdownIt, content) {
function addContextAssets(context) {
if ('fountain' in context.pluginAssets) return;
context.pluginAssets['fountain'] = [
{
inline: true,
text: fountainCss,
mime: 'text/css',
},
];
context.pluginAssets['fountain'] = fountainCss();
}
function installRule(markdownIt, mdOptions, ruleOptions, context) {
@ -136,8 +137,11 @@ function installRule(markdownIt, mdOptions, ruleOptions, context) {
};
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions, context);
};
module.exports = {
install: function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions, context);
};
},
style: fountainCss,
};

View File

@ -14,6 +14,14 @@ const stringifySafe = require('json-stringify-safe');
katex = mhchemModule(katex);
function katexStyle() {
return [
{ name: 'katex.css' },
// Note: Katex also requires a number of fonts but they don't need to be specified here
// since they will be loaded as needed from the CSS.
];
}
// Test if potential opening or closing delimieter
// Assumes that there is a "$" at state.src[pos]
function isValidDelim(state, pos) {
@ -184,97 +192,78 @@ function math_block(state, start, end, silent) {
const cache_ = {};
module.exports = function(context) {
// Keep macros that persist across Katex blocks to allow defining a macro
// in one block and re-using it later in other blocks.
// https://github.com/laurent22/joplin/issues/1105
context.__katex = { macros: {} };
module.exports = {
install: function(context) {
// Keep macros that persist across Katex blocks to allow defining a macro
// in one block and re-using it later in other blocks.
// https://github.com/laurent22/joplin/issues/1105
context.__katex = { macros: {} };
const addContextAssets = () => {
context.pluginAssets['katex'] = [
{ name: 'katex.css' },
{ name: 'fonts/KaTeX_Main-Regular.woff2' },
{ name: 'fonts/KaTeX_Main-Bold.woff2' },
{ name: 'fonts/KaTeX_Main-BoldItalic.woff2' },
{ name: 'fonts/KaTeX_Main-Italic.woff2' },
{ name: 'fonts/KaTeX_Math-Italic.woff2' },
{ name: 'fonts/KaTeX_Math-BoldItalic.woff2' },
{ name: 'fonts/KaTeX_Size1-Regular.woff2' },
{ name: 'fonts/KaTeX_Size2-Regular.woff2' },
{ name: 'fonts/KaTeX_Size3-Regular.woff2' },
{ name: 'fonts/KaTeX_Size4-Regular.woff2' },
{ name: 'fonts/KaTeX_AMS-Regular.woff2' },
{ name: 'fonts/KaTeX_Caligraphic-Bold.woff2' },
{ name: 'fonts/KaTeX_Caligraphic-Regular.woff2' },
{ name: 'fonts/KaTeX_Fraktur-Bold.woff2' },
{ name: 'fonts/KaTeX_Fraktur-Regular.woff2' },
{ name: 'fonts/KaTeX_SansSerif-Bold.woff2' },
{ name: 'fonts/KaTeX_SansSerif-Italic.woff2' },
{ name: 'fonts/KaTeX_SansSerif-Regular.woff2' },
{ name: 'fonts/KaTeX_Script-Regular.woff2' },
{ name: 'fonts/KaTeX_Typewriter-Regular.woff2' },
];
};
const addContextAssets = () => {
context.pluginAssets['katex'] = katexStyle();
};
function renderToStringWithCache(latex, options) {
const cacheKey = md5(escape(latex) + escape(stringifySafe(options)));
if (cacheKey in cache_) {
return cache_[cacheKey];
} else {
const beforeMacros = stringifySafe(options.macros);
const output = katex.renderToString(latex, options);
const afterMacros = stringifySafe(options.macros);
function renderToStringWithCache(latex, options) {
const cacheKey = md5(escape(latex) + escape(stringifySafe(options)));
if (cacheKey in cache_) {
return cache_[cacheKey];
} else {
const beforeMacros = stringifySafe(options.macros);
const output = katex.renderToString(latex, options);
const afterMacros = stringifySafe(options.macros);
// Don't cache the formulas that add macros, otherwise
// they won't be added on second run.
if (beforeMacros === afterMacros) cache_[cacheKey] = output;
return output;
// Don't cache the formulas that add macros, otherwise
// they won't be added on second run.
if (beforeMacros === afterMacros) cache_[cacheKey] = output;
return output;
}
}
}
return function(md, options) {
// Default options
return function(md, options) {
// Default options
options = options || {};
options.macros = context.__katex.macros;
options.trust = true;
options = options || {};
options.macros = context.__katex.macros;
options.trust = true;
// set KaTeX as the renderer for markdown-it-simplemath
const katexInline = function(latex) {
options.displayMode = false;
try {
return `<span class="joplin-editable"><pre class="joplin-source" data-joplin-source-open="$" data-joplin-source-close="$">${latex}</pre>${renderToStringWithCache(latex, options)}</span>`;
} catch (error) {
console.error('Katex error for:', latex, error);
return latex;
}
// set KaTeX as the renderer for markdown-it-simplemath
const katexInline = function(latex) {
options.displayMode = false;
try {
return `<span class="joplin-editable"><span class="joplin-source" data-joplin-source-open="$" data-joplin-source-close="$">${latex}</span>${renderToStringWithCache(latex, options)}</span>`;
} catch (error) {
console.error('Katex error for:', latex, error);
return latex;
}
};
const inlineRenderer = function(tokens, idx) {
addContextAssets();
return katexInline(tokens[idx].content);
};
const katexBlock = function(latex) {
options.displayMode = true;
try {
return `<div class="joplin-editable"><pre class="joplin-source" data-joplin-source-open="$$&#10;" data-joplin-source-close="&#10;$$&#10;">${latex}</pre>${renderToStringWithCache(latex, options)}</div>`;
} catch (error) {
console.error('Katex error for:', latex, error);
return latex;
}
};
const blockRenderer = function(tokens, idx) {
addContextAssets();
return `${katexBlock(tokens[idx].content)}\n`;
};
md.inline.ruler.after('escape', 'math_inline', math_inline);
md.block.ruler.after('blockquote', 'math_block', math_block, {
alt: ['paragraph', 'reference', 'blockquote', 'list'],
});
md.renderer.rules.math_inline = inlineRenderer;
md.renderer.rules.math_block = blockRenderer;
};
const inlineRenderer = function(tokens, idx) {
addContextAssets();
return katexInline(tokens[idx].content);
};
const katexBlock = function(latex) {
options.displayMode = true;
try {
return `<div class="joplin-editable"><pre class="joplin-source" data-joplin-source-open="$$&#10;" data-joplin-source-close="&#10;$$&#10;">${latex}</pre>${renderToStringWithCache(latex, options)}</div>`;
} catch (error) {
console.error('Katex error for:', latex, error);
return latex;
}
};
const blockRenderer = function(tokens, idx) {
addContextAssets();
return `${katexBlock(tokens[idx].content)}\n`;
};
md.inline.ruler.after('escape', 'math_inline', math_inline);
md.block.ruler.after('blockquote', 'math_block', math_block, {
alt: ['paragraph', 'reference', 'blockquote', 'list'],
});
md.renderer.rules.math_inline = inlineRenderer;
md.renderer.rules.math_block = blockRenderer;
};
},
style: katexStyle,
};

View File

@ -1,7 +1,5 @@
function addContextAssets(context:any) {
if ('mermaid' in context.pluginAssets) return;
context.pluginAssets['mermaid'] = [
function style() {
return [
{ name: 'mermaid.min.js' },
{ name: 'mermaid_render.js' },
{
@ -15,6 +13,12 @@ function addContextAssets(context:any) {
];
}
function addContextAssets(context:any) {
if ('mermaid' in context.pluginAssets) return;
context.pluginAssets['mermaid'] = style();
}
// @ts-ignore: Keep the function signature as-is despite unusued arguments
function installRule(markdownIt:any, mdOptions:any, ruleOptions:any, context:any) {
const defaultRender:Function = markdownIt.renderer.rules.fence || function(tokens:any[], idx:number, options:any, env:any, self:any) {
@ -35,8 +39,11 @@ function installRule(markdownIt:any, mdOptions:any, ruleOptions:any, context:any
};
}
export default function(context:any, ruleOptions:any) {
return function(md:any, mdOptions:any) {
installRule(md, mdOptions, ruleOptions, context);
};
}
export default {
install: function(context:any, ruleOptions:any) {
return function(md:any, mdOptions:any) {
installRule(md, mdOptions, ruleOptions, context);
};
},
style: style,
};

View File

@ -13,6 +13,7 @@ module.exports = {
raisedBackgroundColor: '#e5e5e5',
htmlCodeColor: 'rgb(0,0,0)',
htmlCodeFontSize: '.9em',
bodyPaddingBottom: '0',
editorTheme: 'chrome',
codeThemeCss: 'atom-one-light.css',

View File

@ -1,36 +1,34 @@
module.exports = function(style, options) {
style = style ? style : {};
// https://necolas.github.io/normalize.css/
const normalizeCss = `
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}
pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}
b,strong{font-weight:bolder}small{font-size:80%}img{border-style:none}
`;
module.exports = function(theme) {
theme = theme ? theme : {};
const fontFamily = '\'Avenir\', \'Arial\', sans-serif';
const css =
`
/* https://necolas.github.io/normalize.css/ */
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}
pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}
b,strong{font-weight:bolder}small{font-size:80%}img{border-style:none}
body {
font-size: ${style.htmlFontSize};
color: ${style.htmlColor};
font-size: ${theme.htmlFontSize};
color: ${theme.htmlColor};
word-wrap: break-word;
line-height: ${style.htmlLineHeight};
background-color: ${style.htmlBackgroundColor};
line-height: ${theme.htmlLineHeight};
background-color: ${theme.htmlBackgroundColor};
font-family: ${fontFamily};
padding-bottom: ${options.paddingBottom};
padding-bottom: ${theme.bodyPaddingBottom};
}
strong {
color: ${style.colorBright};
color: ${theme.colorBright};
}
kbd {
border: 1px solid ${style.htmlCodeBorderColor};
box-shadow: inset 0 -1px 0 ${style.htmlCodeBorderColor};
border: 1px solid ${theme.htmlCodeBorderColor};
box-shadow: inset 0 -1px 0 ${theme.htmlCodeBorderColor};
padding: 2px 4px;
border-radius: 3px;
background-color: ${style.htmlCodeBackgroundColor};
background-color: ${theme.htmlCodeBackgroundColor};
}
::-webkit-scrollbar {
width: 7px;
@ -79,7 +77,7 @@ module.exports = function(style, options) {
h1 {
font-size: 1.5em;
font-weight: bold;
border-bottom: 1px solid ${style.htmlDividerColor};
border-bottom: 1px solid ${theme.htmlDividerColor};
padding-bottom: .3em;
}
h2 {
@ -95,7 +93,7 @@ module.exports = function(style, options) {
font-weight: bold;
}
a {
color: ${style.htmlLinkColor};
color: ${theme.htmlLinkColor};
}
ul, ol {
padding-left: 0;
@ -116,7 +114,7 @@ module.exports = function(style, options) {
width: 1.2em;
height: 1.4em;
margin-right: 0.4em;
background-color: ${style.htmlLinkColor};
background-color: ${theme.htmlLinkColor};
}
/* These icons are obtained from the wonderful ForkAwesome project by copying the src svgs
* into the css classes below.
@ -177,7 +175,7 @@ module.exports = function(style, options) {
-webkit-mask-repeat: no-repeat;
}
blockquote {
border-left: 4px solid ${style.htmlCodeBorderColor};
border-left: 4px solid ${theme.htmlCodeBorderColor};
padding-left: 1.2em;
margin-left: 0;
opacity: .7;
@ -185,45 +183,45 @@ module.exports = function(style, options) {
table {
text-align: left-align;
border-collapse: collapse;
border: 1px solid ${style.htmlCodeBorderColor};
background-color: ${style.htmlBackgroundColor};
border: 1px solid ${theme.htmlCodeBorderColor};
background-color: ${theme.htmlBackgroundColor};
}
td, th {
padding: .5em 1em .5em 1em;
font-size: ${style.htmlFontSize};
color: ${style.htmlColor};
font-size: ${theme.htmlFontSize};
color: ${theme.htmlColor};
font-family: ${fontFamily};
}
td {
border: 1px solid ${style.htmlCodeBorderColor};
border: 1px solid ${theme.htmlCodeBorderColor};
}
th {
border: 1px solid ${style.htmlCodeBorderColor};
border-bottom: 2px solid ${style.htmlCodeBorderColor};
background-color: ${style.htmlTableBackgroundColor};
border: 1px solid ${theme.htmlCodeBorderColor};
border-bottom: 2px solid ${theme.htmlCodeBorderColor};
background-color: ${theme.htmlTableBackgroundColor};
}
tr:nth-child(even) {
background-color: ${style.htmlTableBackgroundColor};
background-color: ${theme.htmlTableBackgroundColor};
}
tr:hover {
background-color: ${style.raisedBackgroundColor};
background-color: ${theme.raisedBackgroundColor};
}
hr {
border: none;
border-bottom: 2px solid ${style.htmlDividerColor};
border-bottom: 2px solid ${theme.htmlDividerColor};
}
img {
max-width: 100%;
height: auto;
}
.inline-code {
border: 1px solid ${style.htmlCodeBorderColor};
background-color: ${style.htmlCodeBackgroundColor};
border: 1px solid ${theme.htmlCodeBorderColor};
background-color: ${theme.htmlCodeBackgroundColor};
padding-right: .2em;
padding-left: .2em;
border-radius: .25em;
color: ${style.htmlCodeColor};
font-size: ${style.htmlCodeFontSize};
color: ${theme.htmlCodeColor};
font-size: ${theme.htmlCodeFontSize};
}
.highlighted-keyword {
@ -316,5 +314,5 @@ module.exports = function(style, options) {
}
`;
return [normalizeCss, css];
return [css];
};