From 4ea5790fd33e3c47dd3bfe49fff85fec7fc55549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Sat, 11 Nov 2023 01:53:53 +0100 Subject: [PATCH 1/3] first stab at inline SSR TeX --- docs/tex.md | 10 ++++++++++ package.json | 1 + public/client.js | 1 + src/markdown.ts | 39 +++++++++++++++++++++++++++++++-------- yarn.lock | 12 ++++++++++++ 5 files changed, 55 insertions(+), 8 deletions(-) diff --git a/docs/tex.md b/docs/tex.md index d5bde354f..f885ab7e7 100644 --- a/docs/tex.md +++ b/docs/tex.md @@ -48,3 +48,13 @@ c = \pm\sqrt{a^2 + b^2} \f\hat\xi\,e^{2 \pi i \xi x} \,d\xi ``` + +When possible, a ${tex`\TeX`} expression is compiled server-side, for faster rendering; however, when it uses variables, it needs to load the katex library, and to be compiled client-side: + +```js +const a = view(Inputs.range([0, 5], {step: 1})); +``` + +```tex show +\int_0^1 x^${a}dx = \frac1${a + 1} +``` diff --git a/package.json b/package.json index 5bded0d60..a25263a04 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "fast-deep-equal": "^3.1.3", "gray-matter": "^4.0.3", "highlight.js": "^11.8.0", + "katex": "^0.16.9", "linkedom": "^0.15.6", "markdown-it": "^13.0.2", "markdown-it-anchor": "^8.6.7", diff --git a/public/client.js b/public/client.js index cf7eeffab..bcf021019 100644 --- a/public/client.js +++ b/public/client.js @@ -161,6 +161,7 @@ async function mermaid() { export function define(cell) { const {id, inline, inputs = [], outputs = [], files = [], databases = [], body} = cell; + if (body === undefined) return; const variables = []; cellsById.get(id)?.variables.forEach((v) => v.delete()); cellsById.set(id, {cell, variables}); diff --git a/src/markdown.ts b/src/markdown.ts index 60067d1c8..13e115015 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -4,6 +4,7 @@ import {type Patch, type PatchItem, getPatch} from "fast-array-diff"; import equal from "fast-deep-equal"; import matter from "gray-matter"; import hljs from "highlight.js"; +import katex from "katex"; import {parseHTML} from "linkedom"; import MarkdownIt from "markdown-it"; import {type RuleCore} from "markdown-it/lib/parser_core.js"; @@ -79,18 +80,32 @@ function uniqueCodeId(context: ParseContext, content: string): string { return id; } -function getLiveSource(content, language, option) { +function getLiveSource(content, language, option): {source?: string; html?: string} { return option === "no-run" - ? undefined + ? {} : language === "js" - ? content + ? {source: content} : language === "tex" - ? transpileTag(content, "tex.block", true) + ? maybeStaticTeX(content, true) : language === "dot" - ? transpileTag(content, "dot", false) + ? {source: transpileTag(content, "dot", false)} : language === "mermaid" - ? transpileTag(content, "await mermaid", false) - : undefined; + ? {source: transpileTag(content, "await mermaid", false)} + : {}; +} + +function maybeStaticTeX(content, displayMode) { + try { + // TODO smarter detection of ${} contents + // TODO smarter insertion of the TeX stylesheet + return { + html: + katex.renderToString(content, {displayMode}) + + `` + }; + } catch { + return {source: transpileTag(content, displayMode ? "tex.block" : "tex", true)}; + } } function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: string): RenderRule { @@ -99,7 +114,7 @@ function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: s const [language, option] = token.info.split(" "); let result = ""; let count = 0; - const source = getLiveSource(token.content, language, option); + const {source, html} = getLiveSource(token.content, language, option); if (source != null) { const id = uniqueCodeId(context, token.content); const sourceLine = context.startLine + context.currentLine; @@ -115,6 +130,7 @@ function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: s result += `
\n`; count++; } + if (html !== undefined) result += html; if (source == null || option === "show") { result += baseRenderer(tokens, idx, options, context, self); count++; @@ -258,6 +274,13 @@ function makePlaceholderRenderer(root: string, sourcePath: string): RenderRule { return (tokens, idx, options, context: ParseContext) => { const id = uniqueCodeId(context, tokens[idx].content); const token = tokens[idx]; + + // inline TeX? + if (token.content.match(/^tex[`]/)) { + const {html} = maybeStaticTeX(token.content.slice(4, -1), false); + if (html !== undefined) return `${html}`; + } + const transpile = transpileJavaScript(token.content, { id, root, diff --git a/yarn.lock b/yarn.lock index 5d8ffb4d4..7cc7c7cfb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -654,6 +654,11 @@ commander@7: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== +commander@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -1794,6 +1799,13 @@ json5@^1.0.2: dependencies: minimist "^1.2.0" +katex@^0.16.9: + version "0.16.9" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.9.tgz#bc62d8f7abfea6e181250f85a56e4ef292dcb1fa" + integrity sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ== + dependencies: + commander "^8.3.0" + keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" From 9369125c5552e26bdb78d481f058fc83ff13292a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Sun, 12 Nov 2023 18:18:31 +0100 Subject: [PATCH 2/3] =?UTF-8?q?clarify=20how=20we=20test=20for=20static=20?= =?UTF-8?q?contents;=20it's=20a=20bit=20of=20a=20hack,=20maybe=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/client.js | 2 +- src/markdown.ts | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/public/client.js b/public/client.js index bcf021019..fa708955b 100644 --- a/public/client.js +++ b/public/client.js @@ -161,7 +161,7 @@ async function mermaid() { export function define(cell) { const {id, inline, inputs = [], outputs = [], files = [], databases = [], body} = cell; - if (body === undefined) return; + if (body == null) return; const variables = []; cellsById.get(id)?.variables.forEach((v) => v.delete()); cellsById.set(id, {cell, variables}); diff --git a/src/markdown.ts b/src/markdown.ts index 13e115015..da7036288 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -95,9 +95,13 @@ function getLiveSource(content, language, option): {source?: string; html?: stri } function maybeStaticTeX(content, displayMode) { + // We try SSR first. katex.renderToString errors when the expression contains + // some ${interpolation}, so this guarantees that interpolations will be + // handled in the browser. By way of consequence, TeX errors stemming from + // static text (e.g., ParseError on tex`\left{x}`) are handled in the browser, + // and don't stop the build process. try { - // TODO smarter detection of ${} contents - // TODO smarter insertion of the TeX stylesheet + // TODO: unique insertion of the TeX stylesheet? return { html: katex.renderToString(content, {displayMode}) + @@ -130,7 +134,7 @@ function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: s result += `
\n`; count++; } - if (html !== undefined) result += html; + if (html != null) result += html; if (source == null || option === "show") { result += baseRenderer(tokens, idx, options, context, self); count++; @@ -276,9 +280,9 @@ function makePlaceholderRenderer(root: string, sourcePath: string): RenderRule { const token = tokens[idx]; // inline TeX? - if (token.content.match(/^tex[`]/)) { + if (token.content.startsWith("tex`") && token.content.endsWith("`")) { const {html} = maybeStaticTeX(token.content.slice(4, -1), false); - if (html !== undefined) return `${html}`; + if (html != null) return `${html}`; } const transpile = transpileJavaScript(token.content, { From 2efbcf7164dc6161f8d023191b0bc3a38903c277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Sun, 12 Nov 2023 18:47:56 +0100 Subject: [PATCH 3/3] options --- src/markdown.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/markdown.ts b/src/markdown.ts index da7036288..b0382afed 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -86,7 +86,7 @@ function getLiveSource(content, language, option): {source?: string; html?: stri : language === "js" ? {source: content} : language === "tex" - ? maybeStaticTeX(content, true) + ? maybeStaticTeX(content, {displayMode: true}) : language === "dot" ? {source: transpileTag(content, "dot", false)} : language === "mermaid" @@ -94,7 +94,7 @@ function getLiveSource(content, language, option): {source?: string; html?: stri : {}; } -function maybeStaticTeX(content, displayMode) { +function maybeStaticTeX(content, {displayMode = false} = {}) { // We try SSR first. katex.renderToString errors when the expression contains // some ${interpolation}, so this guarantees that interpolations will be // handled in the browser. By way of consequence, TeX errors stemming from @@ -281,7 +281,7 @@ function makePlaceholderRenderer(root: string, sourcePath: string): RenderRule { // inline TeX? if (token.content.startsWith("tex`") && token.content.endsWith("`")) { - const {html} = maybeStaticTeX(token.content.slice(4, -1), false); + const {html} = maybeStaticTeX(token.content.slice(4, -1)); if (html != null) return `${html}`; }