diff --git a/ElectronClient/app/package-lock.json b/ElectronClient/app/package-lock.json index 0f93fee35c..5e2a11fa24 100644 --- a/ElectronClient/app/package-lock.json +++ b/ElectronClient/app/package-lock.json @@ -2852,6 +2852,24 @@ "uc.micro": "1.0.3" } }, + "markdown-it-katex": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/markdown-it-katex/-/markdown-it-katex-2.0.3.tgz", + "integrity": "sha1-17hqGuoLnWSW+rTnkZoY/e9YnDk=", + "requires": { + "katex": "0.6.0" + }, + "dependencies": { + "katex": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.6.0.tgz", + "integrity": "sha1-EkGOCRIcBckgQbazuftrqyE8tvM=", + "requires": { + "match-at": "0.1.1" + } + } + } + }, "match-at": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/match-at/-/match-at-0.1.1.tgz", diff --git a/ElectronClient/app/package.json b/ElectronClient/app/package.json index 72d41515a9..1ea37e4b24 100644 --- a/ElectronClient/app/package.json +++ b/ElectronClient/app/package.json @@ -70,6 +70,7 @@ "levenshtein": "^1.0.5", "lodash": "^4.17.4", "markdown-it": "^8.4.0", + "markdown-it-katex": "^2.0.3", "md5": "^2.2.1", "mime": "^2.0.3", "moment": "^2.19.1", diff --git a/README.md b/README.md index 48e2d88638..6350a5b57d 100644 --- a/README.md +++ b/README.md @@ -150,19 +150,19 @@ Joplin uses and renders [Github-flavoured Markdown](https://github.com/adam-p/ma ## Math notation -Math expressions can be added using the [Katex notation](https://khan.github.io/KaTeX/). To add an inline equation, wrap the expression in `` `{.katex}EXPRESSION` ``, eg. `` `{.katex}\sqrt{3x-1}+(1+x)^2` ``. To create an expression block, wrap it as follow: +Math expressions can be added using the [Katex notation](https://khan.github.io/KaTeX/). To add an inline equation, wrap the expression in `$EXPRESSION$`, eg. `$\sqrt{3x-1}+(1+x)^2$`. To create an expression block, wrap it as follow: - ```katex + $$ EXPRESSION - ``` + $$ For example: - ```katex + $$ f(x) = \int_{-\infty}^\infty \hat f(\xi)\,e^{2 \pi i \xi x} \,d\xi - ``` + $$ Here is an example with the Markdown and rendered result side by side: diff --git a/ReactNativeClient/lib/MdToHtml.js b/ReactNativeClient/lib/MdToHtml.js index dc21551fb6..9c4f11bf0f 100644 --- a/ReactNativeClient/lib/MdToHtml.js +++ b/ReactNativeClient/lib/MdToHtml.js @@ -157,7 +157,7 @@ class MdToHtml { } } - customCodeHandler_(language) { + rendererPlugin_(language) { if (!language) return null; const handlers = {}; @@ -196,9 +196,10 @@ class MdToHtml { const isCodeBlock = tag === 'code' && t.block; const isInlineCode = t.type === 'code_inline'; const codeBlockLanguage = t && t.info ? t.info : null; - let codeBlockHandler = null; + let rendererPlugin = null; + let rendererPluginOptions = { tagType: 'inline' }; - if (isCodeBlock) codeBlockHandler = this.customCodeHandler_(codeBlockLanguage); + if (isCodeBlock) rendererPlugin = this.rendererPlugin_(codeBlockLanguage); if (previousToken && previousToken.tag === 'li' && tag === 'p') { // Markdown-it render list items as
  • Text

  • which makes it @@ -216,7 +217,7 @@ class MdToHtml { } else if (t.type === 'link_open') { openTag = 'a'; } else if (isCodeBlock) { - if (codeBlockHandler) { + if (rendererPlugin) { openTag = null; } else { openTag = 'pre'; @@ -235,25 +236,30 @@ class MdToHtml { if (isCodeBlock) { const codeAttrs = ['code']; - if (!codeBlockHandler) { + if (!rendererPlugin) { if (codeBlockLanguage) codeAttrs.push(t.info); // t.info contains the language when the token is a codeblock output.push(''); } } else if (isInlineCode) { const result = this.parseInlineCodeLanguage_(tokenContent); if (result) { - codeBlockHandler = this.customCodeHandler_(result.language); + rendererPlugin = this.rendererPlugin_(result.language); tokenContent = result.newContent; } - if (!codeBlockHandler) { + if (!rendererPlugin) { output.push(''); } } - if (codeBlockHandler) { - codeBlockHandler.loadAssets().catch((error) => { - console.warn('MdToHtml: Error loading assets for ' + codeBlockHandler.name() + ': ', error.message); + if (t.type === 'math_inline' || t.type === 'math_block') { + rendererPlugin = this.rendererPlugin_('katex'); + rendererPluginOptions = { tagType: t.type === 'math_block' ? 'block' : 'inline' }; + } + + if (rendererPlugin) { + rendererPlugin.loadAssets().catch((error) => { + console.warn('MdToHtml: Error loading assets for ' + rendererPlugin.name() + ': ', error.message); }); } @@ -270,8 +276,10 @@ class MdToHtml { output = output.concat(parsedChildren); } else { if (tokenContent) { - if ((isCodeBlock || isInlineCode) && codeBlockHandler) { - output = codeBlockHandler.processContent(output, tokenContent, isCodeBlock ? 'block' : 'inline'); + if ((isCodeBlock || isInlineCode) && rendererPlugin) { + output = rendererPlugin.processContent(output, tokenContent, isCodeBlock ? 'block' : 'inline'); + } else if (rendererPlugin) { + output = rendererPlugin.processContent(output, tokenContent, rendererPluginOptions.tagType); } else { output.push(htmlentities(tokenContent)); } @@ -286,15 +294,15 @@ class MdToHtml { } else if (tag && t.type.indexOf('inline') >= 0) { closeTag = openTag; } else if (isCodeBlock) { - if (!codeBlockHandler) closeTag = openTag; + if (!rendererPlugin) closeTag = openTag; } if (isCodeBlock) { - if (!codeBlockHandler) { + if (!rendererPlugin) { output.push(''); } } else if (isInlineCode) { - if (!codeBlockHandler) { + if (!rendererPlugin) { output.push(''); } } @@ -307,9 +315,9 @@ class MdToHtml { } } - if (codeBlockHandler) { - const extraCss = codeBlockHandler.extraCss(); - const name = codeBlockHandler.name(); + if (rendererPlugin) { + const extraCss = rendererPlugin.extraCss(); + const name = rendererPlugin.name(); if (extraCss && !(name in extraCssBlocks)) { extraCssBlocks[name] = extraCss; } @@ -345,6 +353,13 @@ class MdToHtml { linkify: true, }); + // This is currently used only so that the $expression$ and $$\nexpression\n$$ blocks are translated + // to math_inline and math_block blocks. These blocks are then processed directly with the Katex + // library. It is better this way as then it is possible to conditionally load the CSS required by + // Katex and use an up-to-date version of Katex (as of 2018, the plugin is still using 0.6, which is + // buggy instead of 0.9). + md.use(require('markdown-it-katex')); + // Hack to make checkboxes clickable. Ideally, checkboxes should be parsed properly in // renderTokens_(), but for now this hack works. Marking it with HORRIBLE_HACK so // that it can be removed and replaced later on. diff --git a/ReactNativeClient/package-lock.json b/ReactNativeClient/package-lock.json index 97605d2af4..14dac9fe1e 100644 --- a/ReactNativeClient/package-lock.json +++ b/ReactNativeClient/package-lock.json @@ -3666,6 +3666,24 @@ "uc.micro": "1.0.3" } }, + "markdown-it-katex": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/markdown-it-katex/-/markdown-it-katex-2.0.3.tgz", + "integrity": "sha1-17hqGuoLnWSW+rTnkZoY/e9YnDk=", + "requires": { + "katex": "0.6.0" + }, + "dependencies": { + "katex": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.6.0.tgz", + "integrity": "sha1-EkGOCRIcBckgQbazuftrqyE8tvM=", + "requires": { + "match-at": "0.1.1" + } + } + } + }, "match-at": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/match-at/-/match-at-0.1.1.tgz", diff --git a/ReactNativeClient/package.json b/ReactNativeClient/package.json index f73d96f931..690fd9bbbb 100644 --- a/ReactNativeClient/package.json +++ b/ReactNativeClient/package.json @@ -17,6 +17,7 @@ "html-entities": "^1.2.1", "katex": "^0.9.0-beta1", "markdown-it": "^8.4.0", + "markdown-it-katex": "^2.0.3", "md5": "^2.2.1", "moment": "^2.18.1", "prop-types": "^15.6.0", diff --git a/docs/images/Katex.png b/docs/images/Katex.png index bc761595d0..95b029e8a4 100644 Binary files a/docs/images/Katex.png and b/docs/images/Katex.png differ diff --git a/docs/index.html b/docs/index.html index e431440d09..dddf60618a 100644 --- a/docs/index.html +++ b/docs/index.html @@ -331,16 +331,16 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin

    Markdown

    Joplin uses and renders Github-flavoured Markdown with a few variations and additions. In particular:

    Math notation

    -

    Math expressions can be added using the Katex notation. To add an inline equation, wrap the expression in `{.katex}EXPRESSION` , eg. `{.katex}\sqrt{3x-1}+(1+x)^2` . To create an expression block, wrap it as follow:

    -
    ```katex
    +

    Math expressions can be added using the Katex notation. To add an inline equation, wrap the expression in $EXPRESSION$, eg. $\sqrt{3x-1}+(1+x)^2$. To create an expression block, wrap it as follow:

    +
    $$
     EXPRESSION
    -```
    +$$
     

    For example:

    -
    ```katex
    +
    $$
     f(x) = \int_{-\infty}^\infty
         \hat f(\xi)\,e^{2 \pi i \xi x}
         \,d\xi
    -```
    +$$
     

    Here is an example with the Markdown and rendered result side by side:

    Checkboxes

    @@ -378,14 +378,14 @@ f(x) = \int_{-\infty}^\infty Croatian hr_HR -Hrvoje Mandić trbuhom@net.hr +Hrvoje Mandić trbuhom@net.hr 72% Deutsch de_DE -Tobias Strobel git@strobeltobias.de +Tobias Strobel git@strobeltobias.de 92% @@ -441,14 +441,14 @@ f(x) = \int_{-\infty}^\infty Русский ru_RU -Artyom Karlov artyom.karlov@gmail.com +Artyom Karlov artyom.karlov@gmail.com 96% 中文 (简体) zh_CN -RCJacH RCJacH@outlook.com +RCJacH RCJacH@outlook.com 76%