From d68c271aaeeab646c9122cb9c11dc1933065ba2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Thu, 8 Apr 2021 15:31:46 +0200 Subject: [PATCH] Add support for math formulas (#151) * Add support for LaTeX equations * Mention math support in the introductory notebook --- assets/css/markdown.css | 4 ++++ assets/js/app.js | 1 + assets/js/cell/markdown.js | 28 ++++++++++++++++++++++++- assets/package-lock.json | 35 ++++++++++++++++++++++++++++++++ assets/package.json | 1 + lib/livebook/notebook/welcome.ex | 10 +++++++++ 6 files changed, 78 insertions(+), 1 deletion(-) diff --git a/assets/css/markdown.css b/assets/css/markdown.css index 4918ef81d..541aeea80 100644 --- a/assets/css/markdown.css +++ b/assets/css/markdown.css @@ -124,6 +124,10 @@ @apply mb-0; } +.markdown .katex-display { + @apply my-8; +} + /* Overrides for user-entered markdown */ [data-element="cell"] .markdown h1, diff --git a/assets/js/app.js b/assets/js/app.js index c0bb68efd..90d967344 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1,5 +1,6 @@ import "../css/app.css"; import "remixicon/fonts/remixicon.css"; +import "katex/dist/katex.min.css"; import "@fontsource/inter"; import "@fontsource/inter/500.css"; diff --git a/assets/js/cell/markdown.js b/assets/js/cell/markdown.js index e6aea2d2e..f49f217cd 100644 --- a/assets/js/cell/markdown.js +++ b/assets/js/cell/markdown.js @@ -1,6 +1,7 @@ import marked from "marked"; import morphdom from "morphdom"; import DOMPurify from "dompurify"; +import katex from "katex"; import monaco from "./live_editor/monaco"; // Reuse Monaco highlighter for Markdown code blocks @@ -51,7 +52,16 @@ class Markdown { // Marked requires a trailing slash in the base URL const opts = { baseUrl: this.baseUrl + "/" }; - marked(this.content, opts, (error, html) => { + // Render math formulas using KaTeX. + // The resulting tags will pass through + // marked.js and sanitization unchanged. + // + // We render math before anything else, because passing + // TeX through markdown renderer may have undesired + // effects like rendering \\ as \. + const contentWithRenderedMath = this.__renderMathInString(this.content); + + marked(contentWithRenderedMath, opts, (error, html) => { const sanitizedHtml = DOMPurify.sanitize(html); if (sanitizedHtml) { @@ -66,6 +76,22 @@ class Markdown { }); }); } + + // Replaces TeX formulas in string with rendered HTML using KaTeX. + __renderMathInString(string) { + const options = { + throwOnError: false, + errorColor: "inherit", + }; + + return string + .replace(/\$\$([\s\S]*?)\$\$/g, (match, math) => { + return katex.renderToString(math, { ...options, displayMode: true }); + }) + .replace(/\$([\s\S]*?)\$/g, (match, math) => { + return katex.renderToString(math, options); + }); + } } export default Markdown; diff --git a/assets/package-lock.json b/assets/package-lock.json index f6b63c888..e0ff3bd81 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -10,6 +10,7 @@ "@fontsource/jetbrains-mono": "^4.2.2", "dompurify": "^2.2.6", "hyperlist": "^1.0.0", + "katex": "^0.13.2", "marked": "^1.2.8", "monaco-editor": "^0.23.0", "morphdom": "^2.6.1", @@ -9296,6 +9297,25 @@ "verror": "1.10.0" } }, + "node_modules/katex": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.13.2.tgz", + "integrity": "sha512-u/KhjFDhyPr+70aiBn9SL/9w/QlLagIXBi2NZSbNnBUp2tR8dCjQplyEMkEzniem5gOeSCBjlBUg4VaiWs1JJg==", + "dependencies": { + "commander": "^6.0.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "engines": { + "node": ">= 6" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -23464,6 +23484,21 @@ "verror": "1.10.0" } }, + "katex": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.13.2.tgz", + "integrity": "sha512-u/KhjFDhyPr+70aiBn9SL/9w/QlLagIXBi2NZSbNnBUp2tR8dCjQplyEMkEzniem5gOeSCBjlBUg4VaiWs1JJg==", + "requires": { + "commander": "^6.0.0" + }, + "dependencies": { + "commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" + } + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", diff --git a/assets/package.json b/assets/package.json index 90d059113..fb0d31e18 100644 --- a/assets/package.json +++ b/assets/package.json @@ -15,6 +15,7 @@ "@fontsource/jetbrains-mono": "^4.2.2", "dompurify": "^2.2.6", "hyperlist": "^1.0.0", + "katex": "^0.13.2", "marked": "^1.2.8", "monaco-editor": "^0.23.0", "morphdom": "^2.6.1", diff --git a/lib/livebook/notebook/welcome.ex b/lib/livebook/notebook/welcome.ex index 1a6140208..700986df3 100644 --- a/lib/livebook/notebook/welcome.ex +++ b/lib/livebook/notebook/welcome.ex @@ -164,6 +164,16 @@ defmodule Livebook.Notebook.Welcome do per runtime, so if you need to modify the dependencies, you should go to the notebook runtime configuration and **reconnect** the current runtime. + ## Math + + Livebook supports both inline formulas like $e^{\\pi i} + 1 = 0$, as well as block formulas: + + $$ + S(x) = \\frac{1}{1 + e^{-x}} = \\frac{e^{x}}{e^{x} + 1} + $$ + + You can explore all supported expressions [here](https://katex.org/docs/supported.html). + ## Stepping up your workflow Once you start using notebooks more, it's gonna be beneficial