diff --git a/package.json b/package.json index a5ff86c01..d5beb5f7e 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@types/react-bootstrap-typeahead": "3.4.6", "@types/react-dom": "16.9.8", "@types/react-html-parser": "2.0.1", + "@types/react-mathjax": "^1.0.0", "@types/react-redux": "7.1.9", "@types/react-router": "5.1.7", "@types/react-router-bootstrap": "0.24.5", @@ -50,11 +51,13 @@ "markdown-it-footnote": "3.0.2", "markdown-it-ins": "3.0.0", "markdown-it-mark": "3.0.0", + "markdown-it-mathjax": "^2.0.0", "markdown-it-regex": "0.2.0", "markdown-it-sub": "1.0.0", "markdown-it-sup": "1.0.0", "markdown-it-table-of-contents": "0.4.4", "markdown-it-task-lists": "2.1.1", + "mathjax": "^3.0.5", "moment": "2.27.0", "node-sass": "4.14.1", "react": "16.13.1", @@ -64,6 +67,7 @@ "react-dom": "16.13.1", "react-html-parser": "2.0.2", "react-i18next": "11.7.0", + "react-mathjax": "^1.0.1", "react-redux": "7.2.0", "react-router": "5.2.0", "react-router-bootstrap": "0.25.0", diff --git a/src/components/editor/editor.tsx b/src/components/editor/editor.tsx index 5047197ad..41ca964d1 100644 --- a/src/components/editor/editor.tsx +++ b/src/components/editor/editor.tsx @@ -15,6 +15,21 @@ const Editor: React.FC = () => { const [markdownContent, setMarkdownContent] = useState(`# Embedding demo [TOC] +## MathJax +You can render *LaTeX* mathematical expressions using **MathJax**, as on [math.stackexchange.com](https://math.stackexchange.com/): + +The *Gamma function* satisfying $\\Gamma(n) = (n-1)!\\quad\\forall n\\in\\mathbb N$ is via the Euler integral + +$$ +x = {-b \\pm \\sqrt{b^2-4ac} \\over 2a}. +$$ + +$$ +\\Gamma(z) = \\int_0^\\infty t^{z-1}e^{-t}dt\\,. +$$ + +> More information about **LaTeX** mathematical expressions [here](https://meta.math.stackexchange.com/questions/5020/mathjax-basic-tutorial-and-quick-reference). + ## Blockquote > Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. > Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. diff --git a/src/components/editor/markdown-renderer/markdown-renderer.tsx b/src/components/editor/markdown-renderer/markdown-renderer.tsx index 9bc21fd1a..333dae2d9 100644 --- a/src/components/editor/markdown-renderer/markdown-renderer.tsx +++ b/src/components/editor/markdown-renderer/markdown-renderer.tsx @@ -13,7 +13,9 @@ import subscript from 'markdown-it-sub' import superscript from 'markdown-it-sup' import toc from 'markdown-it-table-of-contents' import taskList from 'markdown-it-task-lists' +import mathJax from 'markdown-it-mathjax' import React, { ReactElement, useMemo } from 'react' +import MathJaxReact from 'react-mathjax' import ReactHtmlParser, { convertNodeToElement, Transform } from 'react-html-parser' import { createRenderContainer, validAlertLevels } from './container-plugins/alert' import { highlightedCode } from './markdown-it-plugins/highlighted-code' @@ -32,6 +34,7 @@ import { replaceQuoteExtraTime } from './regex-plugins/replace-quote-extra-time' import { replaceVimeoLink } from './regex-plugins/replace-vimeo-link' import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link' import { getGistReplacement } from './replace-components/gist/gist-frame' +import { getMathJaxReplacement } from './replace-components/mathjax/mathjax-replacer' import { getHighlightedCodeBlock } from './replace-components/highlighted-code/highlighted-code' import { getPDFReplacement } from './replace-components/pdf/pdf-frame' import { getTOCReplacement } from './replace-components/toc/toc-replacer' @@ -47,7 +50,7 @@ export type SubNodeConverter = (node: DomElement, index: number) => ReactElement export type ComponentReplacer = (node: DomElement, index: number, counterMap: Map, nodeConverter: SubNodeConverter) => (ReactElement | undefined); type ComponentReplacer2Identifier2CounterMap = Map> -const allComponentReplacers: ComponentReplacer[] = [getYouTubeReplacement, getVimeoReplacement, getGistReplacement, getPDFReplacement, getTOCReplacement, getHighlightedCodeBlock, getQuoteOptionsReplacement] +const allComponentReplacers: ComponentReplacer[] = [getYouTubeReplacement, getVimeoReplacement, getGistReplacement, getPDFReplacement, getTOCReplacement, getHighlightedCodeBlock, getQuoteOptionsReplacement, getMathJaxReplacement] const tryToReplaceNode = (node: DomElement, index:number, componentReplacer2Identifier2CounterMap: ComponentReplacer2Identifier2CounterMap, nodeConverter: SubNodeConverter) => { return allComponentReplacers @@ -75,6 +78,7 @@ const MarkdownRenderer: React.FC = ({ content }) => { md.use(inserted) md.use(marked) md.use(footnote) + // noinspection CheckTagEmptyBody md.use(anchor, { permalink: true, permalinkBefore: true, @@ -84,6 +88,14 @@ const MarkdownRenderer: React.FC = ({ content }) => { md.use(toc, { markerPattern: /^\[TOC]$/i }) + md.use(mathJax({ + beforeMath: '', + afterMath: '', + beforeInlineMath: '', + afterInlineMath: '', + beforeDisplayMath: '', + afterDisplayMath: '' + })) md.use(markdownItRegex, replaceLegacyYoutubeShortCode) md.use(markdownItRegex, replaceLegacyVimeoShortCode) md.use(markdownItRegex, replaceLegacyGistShortCode) @@ -119,7 +131,11 @@ const MarkdownRenderer: React.FC = ({ content }) => { return (
-
{result}
+
+ + {result} + +
) } diff --git a/src/components/editor/markdown-renderer/replace-components/mathjax/mathjax-replacer.tsx b/src/components/editor/markdown-renderer/replace-components/mathjax/mathjax-replacer.tsx new file mode 100644 index 000000000..6c466df99 --- /dev/null +++ b/src/components/editor/markdown-renderer/replace-components/mathjax/mathjax-replacer.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { DomElement } from 'domhandler' +import { ComponentReplacer } from '../../markdown-renderer' +import MathJax from 'react-mathjax' + +const getNodeIfMathJaxBlock = (node: DomElement): (DomElement|undefined) => { + if (node.name !== 'p' || !node.children || node.children.length !== 1) { + return + } + const mathJaxNode = node.children[0] + return (mathJaxNode.name === 'codimd-mathjax' && mathJaxNode.attribs?.inline === undefined) ? mathJaxNode : undefined +} + +const getNodeIfInlineMathJax = (node: DomElement): (DomElement|undefined) => { + return (node.name === 'codimd-mathjax' && node.attribs?.inline !== undefined) ? node : undefined +} + +const getElementReplacement: ComponentReplacer = (node, index: number, counterMap) => { + const mathJax = getNodeIfMathJaxBlock(node) || getNodeIfInlineMathJax(node) + if (mathJax?.children && mathJax.children[0]) { + const mathJaxContent = mathJax.children[0]?.data as string + const isInline = (mathJax.attribs?.inline) !== undefined + return + } +} + +export { getElementReplacement as getMathJaxReplacement } diff --git a/src/external-types/markdown-it-mathjax/index.d.ts b/src/external-types/markdown-it-mathjax/index.d.ts new file mode 100644 index 000000000..6948845d1 --- /dev/null +++ b/src/external-types/markdown-it-mathjax/index.d.ts @@ -0,0 +1,6 @@ +declare module 'markdown-it-mathjax' { + import MarkdownIt from 'markdown-it/lib' + import { MathJaxOptions } from './interface' + const markdownItMathJax: (MathJaxOptions) => MarkdownIt.PluginSimple + export = markdownItMathJax +} diff --git a/src/external-types/markdown-it-mathjax/interface.ts b/src/external-types/markdown-it-mathjax/interface.ts new file mode 100644 index 000000000..9a8f01900 --- /dev/null +++ b/src/external-types/markdown-it-mathjax/interface.ts @@ -0,0 +1,8 @@ +export interface MathJaxOptions { + beforeMath: string, + afterMath: string, + beforeInlineMath: string, + afterInlineMath: string, + beforeDisplayMath: string, + afterDisplayMath: string +} diff --git a/yarn.lock b/yarn.lock index 225e1e394..6be76390d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1683,6 +1683,13 @@ "@types/htmlparser2" "*" "@types/react" "*" +"@types/react-mathjax@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/react-mathjax/-/react-mathjax-1.0.0.tgz#5e337ddc0fbee4d01513d960f81f85963a9ee482" + integrity sha512-c3beW/LhRx/7LkwYGj7mIDphatRdih80Cpe4l2u3LFdyOgBloPaXq3H9QROC6GnhbGxVql7ByHLA70s+Q6ol1Q== + dependencies: + "@types/react" "*" + "@types/react-redux@7.1.9": version "7.1.9" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.9.tgz#280c13565c9f13ceb727ec21e767abe0e9b4aec3" @@ -7097,6 +7104,11 @@ load-json-file@^4.0.0: pify "^3.0.0" strip-bom "^3.0.0" +load-script@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4" + integrity sha1-BJGTngvuVkPuSUp+PaPSuscMbKQ= + loader-fs-cache@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/loader-fs-cache/-/loader-fs-cache-1.0.3.tgz#f08657646d607078be2f0a032f8bd69dd6f277d9" @@ -7334,6 +7346,11 @@ markdown-it-mark@3.0.0: resolved "https://registry.yarnpkg.com/markdown-it-mark/-/markdown-it-mark-3.0.0.tgz#27c3e39ef3cc310b2dde5375082c9fa912983cda" integrity sha512-HqMWeKfMMOu4zBO0emmxsoMWmbf2cPKZY1wP6FsTbKmicFfp5y4L3KXAsNeO1rM6NTJVOrNlLKMPjWzriBGspw== +markdown-it-mathjax@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/markdown-it-mathjax/-/markdown-it-mathjax-2.0.0.tgz#ae2b4f4c5c719a03f9e475c664f7b2685231d9e9" + integrity sha1-ritPTFxxmgP55HXGZPeyaFIx2ek= + markdown-it-regex@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/markdown-it-regex/-/markdown-it-regex-0.2.0.tgz#e09ad2d75209720d591d3949e1142c75c0fbecf6" @@ -7370,6 +7387,11 @@ markdown-it@11.0.0: mdurl "^1.0.1" uc.micro "^1.0.5" +mathjax@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/mathjax/-/mathjax-3.0.5.tgz#707e703a9c1d95f0790bbd404b895566f459d514" + integrity sha512-9M7VulhltkD8sIebWutK/VfAD+m+6BIFqfpjDh9Pz/etoKUtjO6UMnOhUcDmNl6iApE8C9xrUmaMyNZkZAlrMw== + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -9592,6 +9614,13 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== +react-mathjax@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/react-mathjax/-/react-mathjax-1.0.1.tgz#a8c282e75d277a201632dfd07edf41edda372b4b" + integrity sha512-+mjFcciZY3GQoqiQm3aRTyDjgBKuoaXpY+SCONX00jScuPpTKwnASeFMS5+pbTIzDf5zPT2Y9ZZfQ9U/d4CKtQ== + dependencies: + load-script "^1.0.0" + react-overlays@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-2.1.1.tgz#ffe2090c4a10da6b8947a1c7b1a67d0457648a0d"