mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-04 00:47:01 +01:00
markdown: math set by KaTeX, fetched only when a page has some
$x^2$ inline, $$…$$ for display, and a ```math block, the way GitHub writes them, in documents, shared pages and .md files alike. KaTeX loads on first use; until then the math shows as its source. Only the woff2 fonts are bundled.
This commit is contained in:
1 parent
889f99f8d6
commit
fc593e6452
9 files changed
+111
-6
No files matched your search
@@ -141,7 +141,7 @@
|
||||
<footer>
|
||||
inside the editor the full vim keymap is live — hjkl, w/b, dd, ciw, visual mode, macros, and
|
||||
<span class="key">:w</span> to write. drop or paste an image, video or audio file into the
|
||||
editor to attach it. a file opens in place — pdf, text and code, markdown, csv, and
|
||||
editor to attach it. <span class="key">$x^2$</span> and <span class="key">$$…$$</span> are set as math. a file opens in place — pdf, text and code, markdown, csv, and
|
||||
certificates and keystores, which ask for their password. <span class="key">:12</span> jumps straight to entry 12. yanks and
|
||||
deletes are copied to the system clipboard —
|
||||
<span class="key">:set noclipboard</span> keeps them in vim's registers.
|
||||
|
||||
+62
-3
@@ -1,8 +1,66 @@
|
||||
import DOMPurify from 'dompurify'
|
||||
import { marked } from 'marked'
|
||||
import { marked, type TokenizerAndRendererExtension } from 'marked'
|
||||
import { mathEngine } from './math.svelte'
|
||||
|
||||
marked.setOptions({ gfm: true, breaks: true })
|
||||
|
||||
/*
|
||||
* Math the way GitHub writes it: `$x^2$` inline, `$$…$$` on lines of its own
|
||||
* or inline for display, and a ```math block. A `$` opening on a space or
|
||||
* closing before a digit is a price, not math — "$5 and $10" stays text — and
|
||||
* `\$` is a dollar sign as ever.
|
||||
*/
|
||||
const DISPLAY = /^\$\$((?:\\.|[^\\$])+?)\$\$/
|
||||
const INLINE = /^\$(?!\s)((?:\\.|[^\\$\n])+?)(?<!\s)\$(?!\d)/
|
||||
|
||||
const mathBlock: TokenizerAndRendererExtension = {
|
||||
name: 'mathBlock',
|
||||
level: 'block',
|
||||
start: (src) => src.match(/^\$\$/m)?.index,
|
||||
tokenizer(src) {
|
||||
const match = /^\$\$((?:\\.|[^\\$])+?)\$\$[^\S\n]*(?:\n+|$)/.exec(src)
|
||||
if (match) return { type: 'mathBlock', raw: match[0], text: match[1].trim() }
|
||||
},
|
||||
renderer: (token) => tex(token.text, true),
|
||||
}
|
||||
|
||||
const mathInline: TokenizerAndRendererExtension = {
|
||||
name: 'mathInline',
|
||||
level: 'inline',
|
||||
start: (src) => src.indexOf('$'),
|
||||
tokenizer(src) {
|
||||
const display = DISPLAY.exec(src)
|
||||
if (display) return { type: 'mathInline', raw: display[0], text: display[1].trim(), display: true }
|
||||
const inline = INLINE.exec(src)
|
||||
if (inline) return { type: 'mathInline', raw: inline[0], text: inline[1], display: false }
|
||||
},
|
||||
renderer: (token) => tex(token.text, token.display),
|
||||
}
|
||||
|
||||
marked.use({
|
||||
extensions: [mathBlock, mathInline],
|
||||
renderer: {
|
||||
code(token) {
|
||||
return token.lang === 'math' ? tex(token.text, true) : false
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
/** TeX as KaTeX sets it, or as its source while KaTeX is still loading. */
|
||||
function tex(source: string, display: boolean): string {
|
||||
const katex = mathEngine()
|
||||
if (!katex) {
|
||||
const text = escapeHtml(source)
|
||||
return display ? `<pre class="math">${text}</pre>` : `<code class="math">${text}</code>`
|
||||
}
|
||||
// An error shows in place, in red, rather than throwing away the page.
|
||||
return katex.renderToString(source, { displayMode: display, throwOnError: false })
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
return text.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"')
|
||||
}
|
||||
|
||||
/**
|
||||
* Render entry markdown to sanitised HTML. When share credentials are given,
|
||||
* media URLs are rewritten to the public route — token and key both, since that
|
||||
@@ -20,8 +78,9 @@ export function renderMarkdown(source: string, share?: { token: string; key: str
|
||||
const html = marked.parse(md, { async: false }) as string
|
||||
|
||||
return DOMPurify.sanitize(html, {
|
||||
ADD_TAGS: ['video', 'audio', 'source'],
|
||||
ADD_ATTR: ['controls', 'target', 'loading', 'playsinline', 'preload'],
|
||||
// `semantics` and `annotation` carry KaTeX's MathML, for screen readers.
|
||||
ADD_TAGS: ['video', 'audio', 'source', 'semantics', 'annotation'],
|
||||
ADD_ATTR: ['controls', 'target', 'loading', 'playsinline', 'preload', 'encoding'],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* KaTeX, fetched the first time something on the page has math in it rather
|
||||
* than with the app: it is larger than the rest of the app put together.
|
||||
* Until it arrives the math shows as its source. The engine is state, so
|
||||
* markdown rendered without it renders again once it is here.
|
||||
*/
|
||||
import type Katex from 'katex'
|
||||
|
||||
let engine = $state.raw<typeof Katex | null>(null)
|
||||
let loading = false
|
||||
|
||||
export function mathEngine(): typeof Katex | null {
|
||||
if (!engine && !loading) {
|
||||
loading = true
|
||||
Promise.all([import('katex'), import('katex/dist/katex.min.css')])
|
||||
.then(([katex]) => (engine = katex.default))
|
||||
.catch((e) => console.error('math: could not load KaTeX', e))
|
||||
}
|
||||
return engine
|
||||
}
|
||||
Reference in new issue
Block a user