Skip to content

Commit 4ea5790

Browse files
committed
first stab at inline SSR TeX
1 parent 2078c98 commit 4ea5790

File tree

5 files changed

+55
-8
lines changed

5 files changed

+55
-8
lines changed

docs/tex.md

+10
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,13 @@ c = \pm\sqrt{a^2 + b^2}
4848
\f\hat\xi\,e^{2 \pi i \xi x}
4949
\,d\xi
5050
```
51+
52+
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:
53+
54+
```js
55+
const a = view(Inputs.range([0, 5], {step: 1}));
56+
```
57+
58+
```tex show
59+
\int_0^1 x^${a}dx = \frac1${a + 1}
60+
```

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"fast-deep-equal": "^3.1.3",
3737
"gray-matter": "^4.0.3",
3838
"highlight.js": "^11.8.0",
39+
"katex": "^0.16.9",
3940
"linkedom": "^0.15.6",
4041
"markdown-it": "^13.0.2",
4142
"markdown-it-anchor": "^8.6.7",

public/client.js

+1
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ async function mermaid() {
161161

162162
export function define(cell) {
163163
const {id, inline, inputs = [], outputs = [], files = [], databases = [], body} = cell;
164+
if (body === undefined) return;
164165
const variables = [];
165166
cellsById.get(id)?.variables.forEach((v) => v.delete());
166167
cellsById.set(id, {cell, variables});

src/markdown.ts

+31-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {type Patch, type PatchItem, getPatch} from "fast-array-diff";
44
import equal from "fast-deep-equal";
55
import matter from "gray-matter";
66
import hljs from "highlight.js";
7+
import katex from "katex";
78
import {parseHTML} from "linkedom";
89
import MarkdownIt from "markdown-it";
910
import {type RuleCore} from "markdown-it/lib/parser_core.js";
@@ -79,18 +80,32 @@ function uniqueCodeId(context: ParseContext, content: string): string {
7980
return id;
8081
}
8182

82-
function getLiveSource(content, language, option) {
83+
function getLiveSource(content, language, option): {source?: string; html?: string} {
8384
return option === "no-run"
84-
? undefined
85+
? {}
8586
: language === "js"
86-
? content
87+
? {source: content}
8788
: language === "tex"
88-
? transpileTag(content, "tex.block", true)
89+
? maybeStaticTeX(content, true)
8990
: language === "dot"
90-
? transpileTag(content, "dot", false)
91+
? {source: transpileTag(content, "dot", false)}
9192
: language === "mermaid"
92-
? transpileTag(content, "await mermaid", false)
93-
: undefined;
93+
? {source: transpileTag(content, "await mermaid", false)}
94+
: {};
95+
}
96+
97+
function maybeStaticTeX(content, displayMode) {
98+
try {
99+
// TODO smarter detection of ${} contents
100+
// TODO smarter insertion of the TeX stylesheet
101+
return {
102+
html:
103+
katex.renderToString(content, {displayMode}) +
104+
`<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/katex/dist/katex.min.css">`
105+
};
106+
} catch {
107+
return {source: transpileTag(content, displayMode ? "tex.block" : "tex", true)};
108+
}
94109
}
95110

96111
function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: string): RenderRule {
@@ -99,7 +114,7 @@ function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: s
99114
const [language, option] = token.info.split(" ");
100115
let result = "";
101116
let count = 0;
102-
const source = getLiveSource(token.content, language, option);
117+
const {source, html} = getLiveSource(token.content, language, option);
103118
if (source != null) {
104119
const id = uniqueCodeId(context, token.content);
105120
const sourceLine = context.startLine + context.currentLine;
@@ -115,6 +130,7 @@ function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: s
115130
result += `<div id="cell-${id}" class="observablehq observablehq--block"></div>\n`;
116131
count++;
117132
}
133+
if (html !== undefined) result += html;
118134
if (source == null || option === "show") {
119135
result += baseRenderer(tokens, idx, options, context, self);
120136
count++;
@@ -258,6 +274,13 @@ function makePlaceholderRenderer(root: string, sourcePath: string): RenderRule {
258274
return (tokens, idx, options, context: ParseContext) => {
259275
const id = uniqueCodeId(context, tokens[idx].content);
260276
const token = tokens[idx];
277+
278+
// inline TeX?
279+
if (token.content.match(/^tex[`]/)) {
280+
const {html} = maybeStaticTeX(token.content.slice(4, -1), false);
281+
if (html !== undefined) return `<span id="cell-${id}">${html}</span>`;
282+
}
283+
261284
const transpile = transpileJavaScript(token.content, {
262285
id,
263286
root,

yarn.lock

+12
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,11 @@ commander@7:
654654
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
655655
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
656656

657+
commander@^8.3.0:
658+
version "8.3.0"
659+
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
660+
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
661+
657662
658663
version "0.0.1"
659664
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -1794,6 +1799,13 @@ json5@^1.0.2:
17941799
dependencies:
17951800
minimist "^1.2.0"
17961801

1802+
katex@^0.16.9:
1803+
version "0.16.9"
1804+
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.9.tgz#bc62d8f7abfea6e181250f85a56e4ef292dcb1fa"
1805+
integrity sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==
1806+
dependencies:
1807+
commander "^8.3.0"
1808+
17971809
keyv@^4.5.3:
17981810
version "4.5.4"
17991811
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"

0 commit comments

Comments
 (0)