mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-23 08:32:20 +02:00
perf(static): vendor KaTeX and Mermaid, and load them on first use (#5994)
* fix(static): vendor KaTeX and Mermaid instead of loading them from a CDN
index.html pulled katex.min.{js,css} and mermaid.min.js from cdn.jsdelivr.net on
every page load. For self-hosted software that is three problems at once: an
air-gapped or offline install renders no math and no diagrams at all, every
session announces its IP, User-Agent and Referer to a third party, and the "runs
on your own hardware" promise quietly isn't true.
static/lib/ already vendors highlight.js, docx, xlsx, mammoth, html2pdf and
qrcode, so the CDN usage was an inconsistency rather than a policy. Vendoring
also pins Mermaid, which was floating on the `11` tag, to 11.16.1.
Behaviour is unchanged: both libraries still load eagerly from <head>, just from
this machine.
- KaTeX goes in its own directory because its stylesheet resolves fonts with a
relative url(fonts/...), so the vendored CSS needs no rewrite. Only the .woff2
variants ship, matching static/fonts/, since a browser that supports woff2
never requests the .woff/.ttf alternatives the stylesheet also lists.
- The service worker precaches KaTeX and its fonts so offline math is typeset
rather than falling back to system glyphs, and CACHE_NAME is bumped. Mermaid
is left to the existing cache-first rule: at 3.5 MB, precaching it would mean
re-downloading it on every cache bump for a library most sessions never touch.
- Licence texts travel with the bundles in licenses/, following the convention
the repo already uses for OpenDyslexic and DeepResearch.
- .gitattributes turns the whitespace check off for static/lib/ so `git diff
--check` passes without stripping bytes from the published npm artifacts,
which would desync them from upstream.
* perf(markdown): load KaTeX and Mermaid on first use, not on every page load
Both libraries loaded eagerly from <head>, costing every session ~985 KB on the
wire (929 KB of that Mermaid) even though most chats contain neither a formula
nor a diagram. Measured on a cold profile via the Resource Timing API: JS bytes
per page load drop from 3,102,141 to 2,098,634, a saving of 1,003,507 bytes, and
third-party requests per load go from 3 to 0.
markdown.js now fetches each library the first time one is actually needed:
- renderMermaid() checks for an unprocessed mermaid fence before touching the
network, and re-queries the DOM after the load so a diagram replaced mid-stream
still renders.
- mdToHtml() is synchronous, so when KaTeX is not in yet it banks the math source
in an inert placeholder and schedules a flush that loads the library and swaps
the placeholders in. Once KaTeX is loaded it typesets inline exactly as before,
so callers that never call a render helper still get their math.
Both loaders memoise the promise rather than the module, so concurrent callers
share one fetch and a double trigger cannot start two loads; a failed load clears
the memo so the next formula retries instead of being poisoned for the session.
The flush is scheduled with setTimeout rather than requestAnimationFrame, which
is throttled to a stop in a background tab and never fires at all in a headless
browser, so math would have sat as plain source text until the tab was focused.
If neither library ever loads, math degrades to readable source text and diagrams
to their fence contents, rather than to nothing.
* fix(markdown): unescape & last so math entities survive intact
The math pass unescaped & before < and >. mdToHtml escapes the source
first, so a literal "<" typed inside a formula arrives here as "&lt;",
turns back into "<" on the ampersand pass, and is then eaten by the very next
one. Typing $a < b$ rendered as "a < b" instead of the literal text.
The code-block pass in the same function already unescapes & last; only the
math paths were the outlier, in all four of the copies this branch consolidated
into pushMath(). Reordering to match makes them consistent and clears the
js/double-escaping alert CodeQL raised on this PR.
Math containing a genuinely typed "<" is unaffected, which is why this went
unnoticed for so long. Covered by a regression test asserting both cases.
* fix(markdown): decode entity-spelled math in one pass
mdToHtml escapes the source before the math pass, so a typed "<" reaches
the delimiters as "<" and a typed "<" reaches them as "&lt;".
KaTeX has no entity syntax and reads the leftover "&" as an alignment
marker, so "$a < b$" rendered as a red .katex-error instead of a
formula, on both the inline and the deferred path.
Chained replaces cannot fix it in either order: unescaping "&" first
lets the next pass eat the "<" it just wrote, and unescaping it last
leaves the entity spelling for KaTeX to choke on. One alternation,
longest form first, decodes every spelling and never rescans its own
output.
The tests now drive the vendored KaTeX build rather than a renderer that
echoes its input, which is why the old assertion looked correct.
* fix(document): typeset deferred math before the PDF export
exportAsPdf() renders the document into a detached container and hands
it straight to html2pdf. On a page where KaTeX has not loaded yet,
mdToHtml() returns pending placeholders and schedules a flush scoped to
document, which never reaches a node that was never attached, so the
PDF printed raw formula source.
Render the container's own math first. renderMath() returns immediately
without fetching anything when there is nothing pending, so a document
with no formulas still exports without pulling KaTeX.
This commit is contained in:
+5
-17
@@ -231,23 +231,11 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- KaTeX CSS is loaded with media="print" so it doesn't block render,
|
||||
then flipped to "all" via JS after load. Mermaid init runs once the
|
||||
library finishes loading. Both hooks are wired via addEventListener
|
||||
below (inline onload= attrs are blocked by CSP script-src-attr). -->
|
||||
<link id="katex-css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css" media="print">
|
||||
<script async src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js"></script>
|
||||
<script id="mermaid-script" async src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
||||
<script nonce="{{CSP_NONCE}}">
|
||||
(function(){
|
||||
var k = document.getElementById('katex-css');
|
||||
if (k) k.addEventListener('load', function(){ k.media = 'all'; }, { once: true });
|
||||
var m = document.getElementById('mermaid-script');
|
||||
if (m) m.addEventListener('load', function(){
|
||||
if (window.odysseusInitMermaid) window.odysseusInitMermaid();
|
||||
}, { once: true });
|
||||
})();
|
||||
</script>
|
||||
<!-- KaTeX and Mermaid are vendored in /static/lib and pulled in by
|
||||
static/js/markdown.js the first time a page actually renders math or a
|
||||
```mermaid fence. They used to load here from cdn.jsdelivr.net on every
|
||||
page load, which cost ~985 KB on the wire, broke offline installs, and
|
||||
announced every session to a third party. -->
|
||||
<!-- Preload the two faces first paint actually uses: Fira Code 400 and 600,
|
||||
the app font and the weight the sidebar and header text render at. They
|
||||
are declared in style.css, so without a hint they are only discovered
|
||||
|
||||
@@ -9738,6 +9738,11 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
|
||||
const container = document.createElement('div');
|
||||
container.style.cssText = 'padding:20px;font-family:sans-serif;font-size:12px;color:#000;background:#fff;line-height:1.6;';
|
||||
container.innerHTML = html;
|
||||
// This container is detached, so the document-scoped flush mdToHtml
|
||||
// schedules never sees it. Typeset the deferred math before html2pdf
|
||||
// rasterises, or the PDF gets raw formula source. renderMath() returns
|
||||
// immediately, without loading KaTeX, when there is nothing pending.
|
||||
await markdownModule.renderMath(container);
|
||||
const baseName = _getExportBaseName();
|
||||
window.html2pdf().set({
|
||||
margin: 10,
|
||||
|
||||
+202
-63
@@ -10,6 +10,127 @@ import { replaceEmojiShortcodes, hasEmojiShortcode } from './emojiShortcodes.js'
|
||||
|
||||
var escapeHtml = uiModule.esc;
|
||||
|
||||
// Mermaid and KaTeX are vendored under /static/lib and fetched on first use.
|
||||
// Loading them from <head> cost every session ~985 KB on the wire even though
|
||||
// most chats never contain a diagram or a formula. Both loaders memoise the
|
||||
// *promise* rather than the resolved library, so concurrent callers share one
|
||||
// fetch and a double trigger cannot start two loads. A failed load clears the
|
||||
// memo so the next diagram/formula retries instead of being poisoned forever.
|
||||
const MERMAID_SRC = '/static/lib/mermaid.min.js';
|
||||
const KATEX_SRC = '/static/lib/katex/katex.min.js';
|
||||
const KATEX_CSS = '/static/lib/katex/katex.min.css';
|
||||
// Marks math emitted before KaTeX finished loading; renderMath() swaps these
|
||||
// for typeset output. The source stays as readable text inside the span, so a
|
||||
// load that never completes degrades to plain text rather than to nothing.
|
||||
const MATH_PENDING_CLASS = 'ody-math-pending';
|
||||
|
||||
// KaTeX has no entity syntax: it reads a bare "&" as an alignment marker and
|
||||
// errors out on anything that is not a valid column break, so "a < b" comes
|
||||
// back as a red .katex-error instead of a formula. mdToHtml escapes the whole
|
||||
// string before the math pass, which leaves two spellings of the same
|
||||
// character at the delimiters — a typed "<" arrives as "<", while a typed
|
||||
// "<" arrives as "&lt;" — and both have to reach KaTeX as "<".
|
||||
//
|
||||
// One alternation, longest form first, so nothing this writes is scanned
|
||||
// again. Chained .replace() calls cannot do it: unescaping "&" first lets
|
||||
// the next pass eat the "<" it just produced (the double-unescape CodeQL
|
||||
// flags), and unescaping it last leaves the entity spelling intact and breaks
|
||||
// the render. The code-block pass upstream keeps its chained order on purpose
|
||||
// — Markdown does not decode entities inside code, so "<" there is meant to
|
||||
// stay visible.
|
||||
const MATH_SOURCE_ENTITY_RE = /&(?:lt|gt|amp|quot|#39);|<|>|&/g;
|
||||
const MATH_SOURCE_ENTITIES = {
|
||||
'&lt;': '<',
|
||||
'&gt;': '>',
|
||||
'&amp;': '&',
|
||||
'&quot;': '"',
|
||||
'&#39;': "'",
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'&': '&',
|
||||
};
|
||||
|
||||
function decodeMathSource(text) {
|
||||
return String(text).replace(MATH_SOURCE_ENTITY_RE, (entity) => MATH_SOURCE_ENTITIES[entity]);
|
||||
}
|
||||
|
||||
let _mermaidPromise = null;
|
||||
let _katexPromise = null;
|
||||
let _mathFlushScheduled = false;
|
||||
|
||||
function _loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.addEventListener('load', () => resolve(), { once: true });
|
||||
script.addEventListener('error', () => reject(new Error('Failed to load ' + src)), { once: true });
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
function _loadStylesheet(href) {
|
||||
// Resolves either way: without the stylesheet KaTeX still produces correct
|
||||
// markup, just unstyled, which beats failing the whole math render.
|
||||
return new Promise((resolve) => {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = href;
|
||||
link.addEventListener('load', () => resolve(), { once: true });
|
||||
link.addEventListener('error', () => resolve(), { once: true });
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Mermaid on first use and initialize it once.
|
||||
*/
|
||||
export function ensureMermaid() {
|
||||
return (_mermaidPromise ??= _loadScript(MERMAID_SRC)
|
||||
.then(() => {
|
||||
if (!window.mermaid) throw new Error('mermaid global missing after load');
|
||||
window.mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose' });
|
||||
return window.mermaid;
|
||||
})
|
||||
.catch((err) => {
|
||||
_mermaidPromise = null;
|
||||
throw err;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load KaTeX (script + stylesheet) on first use.
|
||||
*/
|
||||
export function ensureKatex() {
|
||||
return (_katexPromise ??= Promise.all([_loadScript(KATEX_SRC), _loadStylesheet(KATEX_CSS)])
|
||||
.then(() => {
|
||||
if (!window.katex) throw new Error('katex global missing after load');
|
||||
return window.katex;
|
||||
})
|
||||
.catch((err) => {
|
||||
_katexPromise = null;
|
||||
throw err;
|
||||
}));
|
||||
}
|
||||
|
||||
// mdToHtml() is synchronous and its callers insert the returned string into the
|
||||
// DOM themselves, so the placeholders are usually not attached yet when this
|
||||
// fires. Loading first and scanning afterwards covers that gap: by the time
|
||||
// KaTeX is in, the caller's innerHTML assignment has long since happened.
|
||||
//
|
||||
// setTimeout, not requestAnimationFrame: this has nothing to do with paint, and
|
||||
// rAF is throttled to a stop in a background tab (and never fires at all in a
|
||||
// headless browser), which would leave math untypeset until the tab is focused.
|
||||
function _scheduleMathFlush() {
|
||||
if (_mathFlushScheduled) return;
|
||||
_mathFlushScheduled = true;
|
||||
setTimeout(() => {
|
||||
_mathFlushScheduled = false;
|
||||
ensureKatex()
|
||||
.then(() => renderMath(document))
|
||||
.catch((e) => console.warn('KaTeX load error:', e));
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function safeLinkUrl(rawUrl) {
|
||||
const url = String(rawUrl || '').trim();
|
||||
if (url.startsWith('#')) {
|
||||
@@ -631,49 +752,45 @@ export function mdToHtml(src, opts) {
|
||||
|
||||
// KaTeX math rendering (after code blocks are extracted, so math in code is safe)
|
||||
const mathBlocks = [];
|
||||
if (window.katex) {
|
||||
// Display math: \[ ... \] — GPT-style delimiter (gpt-5.x, Claude, etc.).
|
||||
// Handle before $$/$ so all common delimiters render.
|
||||
s = s.replace(/\\\[([\s\S]*?)\\\]/g, (match, math) => {
|
||||
try {
|
||||
const raw = math.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
const placeholder = `___MATH_BLOCK_${mathBlocks.length}___`;
|
||||
mathBlocks.push(katex.renderToString(raw.trim(), { displayMode: true, throwOnError: false }));
|
||||
return placeholder;
|
||||
} catch (e) { return match; }
|
||||
});
|
||||
// Inline math: \( ... \) — GPT-style inline delimiter. Single-line only
|
||||
// ([^\n]) so a stray escaped paren in prose can't swallow across lines.
|
||||
s = s.replace(/\\\(([^\n]*?)\\\)/g, (match, math) => {
|
||||
try {
|
||||
const raw = math.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
const placeholder = `___MATH_BLOCK_${mathBlocks.length}___`;
|
||||
mathBlocks.push(katex.renderToString(raw.trim(), { displayMode: false, throwOnError: false }));
|
||||
return placeholder;
|
||||
} catch (e) { return match; }
|
||||
});
|
||||
// Display math: $$...$$
|
||||
s = s.replace(/\$\$([\s\S]*?)\$\$/g, (match, math) => {
|
||||
try {
|
||||
const raw = math.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
const placeholder = `___MATH_BLOCK_${mathBlocks.length}___`;
|
||||
mathBlocks.push(katex.renderToString(raw.trim(), { displayMode: true, throwOnError: false }));
|
||||
return placeholder;
|
||||
} catch (e) { return match; }
|
||||
});
|
||||
// Inline math: $...$ — single line only, and Pandoc-style delimiter rules so
|
||||
// currency doesn't render as math ("$5 to $10"): the opening $ must be
|
||||
// immediately followed by a non-space, the closing $ must be immediately
|
||||
// preceded by a non-space and not followed by a digit.
|
||||
s = s.replace(/(?<![\$\d])\$(?!\$)(?=\S)([^\$\n]+?)(?<=\S)\$(?!\$|\d)/g, (match, math) => {
|
||||
try {
|
||||
const raw = math.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
const placeholder = `___MATH_BLOCK_${mathBlocks.length}___`;
|
||||
mathBlocks.push(katex.renderToString(raw.trim(), { displayMode: false, throwOnError: false }));
|
||||
return placeholder;
|
||||
} catch (e) { return match; }
|
||||
});
|
||||
}
|
||||
let sawPendingMath = false;
|
||||
|
||||
// Typeset straight away when KaTeX is already in, otherwise bank the source in
|
||||
// an inert placeholder for renderMath() to swap once the library lands.
|
||||
const pushMath = (math, displayMode) => {
|
||||
const raw = decodeMathSource(math).trim();
|
||||
const placeholder = `___MATH_BLOCK_${mathBlocks.length}___`;
|
||||
if (window.katex) {
|
||||
mathBlocks.push(katex.renderToString(raw, { displayMode, throwOnError: false }));
|
||||
} else {
|
||||
sawPendingMath = true;
|
||||
mathBlocks.push(`<span class="${MATH_PENDING_CLASS}" data-display="${displayMode}">${escapeHtml(raw)}</span>`);
|
||||
}
|
||||
return placeholder;
|
||||
};
|
||||
|
||||
// Display math: \[ ... \] — GPT-style delimiter (gpt-5.x, Claude, etc.).
|
||||
// Handle before $$/$ so all common delimiters render.
|
||||
s = s.replace(/\\\[([\s\S]*?)\\\]/g, (match, math) => {
|
||||
try { return pushMath(math, true); } catch (e) { return match; }
|
||||
});
|
||||
// Inline math: \( ... \) — GPT-style inline delimiter. Single-line only
|
||||
// ([^\n]) so a stray escaped paren in prose can't swallow across lines.
|
||||
s = s.replace(/\\\(([^\n]*?)\\\)/g, (match, math) => {
|
||||
try { return pushMath(math, false); } catch (e) { return match; }
|
||||
});
|
||||
// Display math: $$...$$
|
||||
s = s.replace(/\$\$([\s\S]*?)\$\$/g, (match, math) => {
|
||||
try { return pushMath(math, true); } catch (e) { return match; }
|
||||
});
|
||||
// Inline math: $...$ — single line only, and Pandoc-style delimiter rules so
|
||||
// currency doesn't render as math ("$5 to $10"): the opening $ must be
|
||||
// immediately followed by a non-space, the closing $ must be immediately
|
||||
// preceded by a non-space and not followed by a digit.
|
||||
s = s.replace(/(?<![\$\d])\$(?!\$)(?=\S)([^\$\n]+?)(?<=\S)\$(?!\$|\d)/g, (match, math) => {
|
||||
try { return pushMath(math, false); } catch (e) { return match; }
|
||||
});
|
||||
|
||||
if (sawPendingMath) _scheduleMathFlush();
|
||||
|
||||
// Handle pipe tables
|
||||
s = s.replace(/(?:^|\n)([^\n]*\|[^\n]*\|[^\n]*)(?:\n([^\n]*\|[^\n]*\|[^\n]*))*/g, (table) => {
|
||||
@@ -826,19 +943,47 @@ export function renderContent(content) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize any unprocessed Mermaid diagrams in a container (or whole document)
|
||||
* Initialize any unprocessed Mermaid diagrams in a container (or whole document).
|
||||
* Returns a promise so callers can await the (lazy) library load if they need to.
|
||||
*/
|
||||
export function renderMermaid(container) {
|
||||
if (!window.mermaid) return;
|
||||
initMermaid();
|
||||
const target = container || document;
|
||||
const pending = target.querySelectorAll('pre.mermaid:not([data-processed])');
|
||||
if (pending.length === 0) return;
|
||||
try {
|
||||
window.mermaid.run({ nodes: pending });
|
||||
} catch (e) {
|
||||
console.warn('Mermaid render error:', e);
|
||||
}
|
||||
if (!target || typeof target.querySelectorAll !== 'function') return Promise.resolve();
|
||||
// Cheap pre-check: no fence on the page means Mermaid is never fetched.
|
||||
if (target.querySelectorAll('pre.mermaid:not([data-processed])').length === 0) return Promise.resolve();
|
||||
return ensureMermaid()
|
||||
.then((mermaid) => {
|
||||
// Re-query after the load: during streaming the renderer replaces the
|
||||
// message body repeatedly, so the nodes seen before the fetch are stale.
|
||||
const nodes = [...target.querySelectorAll('pre.mermaid:not([data-processed])')]
|
||||
.filter((node) => node.isConnected);
|
||||
if (nodes.length === 0) return;
|
||||
return mermaid.run({ nodes });
|
||||
})
|
||||
.catch((e) => { console.warn('Mermaid render error:', e); });
|
||||
}
|
||||
|
||||
/**
|
||||
* Typeset any math that mdToHtml() had to defer because KaTeX was not loaded
|
||||
* yet. Once KaTeX is in, mdToHtml() renders inline and this finds nothing.
|
||||
*/
|
||||
export function renderMath(container) {
|
||||
const target = container || document;
|
||||
if (!target || typeof target.querySelectorAll !== 'function') return Promise.resolve();
|
||||
if (target.querySelectorAll('.' + MATH_PENDING_CLASS).length === 0) return Promise.resolve();
|
||||
return ensureKatex()
|
||||
.then((katex) => {
|
||||
target.querySelectorAll('.' + MATH_PENDING_CLASS).forEach((el) => {
|
||||
const displayMode = el.getAttribute('data-display') === 'true';
|
||||
try {
|
||||
el.outerHTML = katex.renderToString(el.textContent || '', { displayMode, throwOnError: false });
|
||||
} catch (e) {
|
||||
// Leave the source visible — readable, just not typeset.
|
||||
el.classList.remove(MATH_PENDING_CLASS);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((e) => { console.warn('KaTeX render error:', e); });
|
||||
}
|
||||
|
||||
const markdownModule = {
|
||||
@@ -853,20 +998,14 @@ const markdownModule = {
|
||||
extractThinkingBlocks,
|
||||
normalizeThinkingMarkup,
|
||||
startsWithReasoningPrefix,
|
||||
renderMermaid
|
||||
renderMermaid,
|
||||
renderMath,
|
||||
ensureMermaid,
|
||||
ensureKatex
|
||||
};
|
||||
|
||||
export default markdownModule;
|
||||
|
||||
// Mermaid is loaded async so it cannot delay the app shell.
|
||||
function initMermaid() {
|
||||
if (!window.mermaid || window.__odysseusMermaidReady) return;
|
||||
window.mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose' });
|
||||
window.__odysseusMermaidReady = true;
|
||||
}
|
||||
window.odysseusInitMermaid = initMermaid;
|
||||
initMermaid();
|
||||
|
||||
// Persist which thinking sections were expanded across page refreshes.
|
||||
// IDs are render-generated (Date.now-based) so we key by a stable hash of
|
||||
// the inner text content instead — same content reproduces the same hash on
|
||||
|
||||
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+3587
File diff suppressed because one or more lines are too long
+24
-1
@@ -7,7 +7,21 @@
|
||||
// - Other static assets (images/fonts/libs): cache-first with bg refresh.
|
||||
// - API / non-GET: never cached.
|
||||
// Bump CACHE_NAME whenever the precache list or SW logic changes.
|
||||
const CACHE_NAME = 'odysseus-v378-shared-config-image-editor';
|
||||
const CACHE_NAME = 'odysseus-v380-shared-config-image-editor-lazy-katex-mermaid';
|
||||
|
||||
// KaTeX resolves these from its own stylesheet, so caching the CSS without them
|
||||
// gives offline math fallback glyphs instead of proper typesetting.
|
||||
const KATEX_FONTS = [
|
||||
'AMS-Regular', 'Caligraphic-Bold', 'Caligraphic-Regular',
|
||||
'Fraktur-Bold', 'Fraktur-Regular',
|
||||
'Main-Bold', 'Main-BoldItalic', 'Main-Italic', 'Main-Regular',
|
||||
'Math-BoldItalic', 'Math-Italic',
|
||||
'SansSerif-Bold', 'SansSerif-Italic', 'SansSerif-Regular',
|
||||
'Script-Regular',
|
||||
'Size1-Regular', 'Size2-Regular', 'Size3-Regular', 'Size4-Regular',
|
||||
'Typewriter-Regular',
|
||||
].map(name => `/static/lib/katex/fonts/KaTeX_${name}.woff2`);
|
||||
|
||||
|
||||
// Two lists, two jobs — they are no longer the same set and must not be
|
||||
// "resynced" back into one:
|
||||
@@ -73,6 +87,15 @@ const PRECACHE = [
|
||||
'/static/js/sidebar-layout.js',
|
||||
'/static/js/section-management.js',
|
||||
'/static/lib/highlight.min.js',
|
||||
// Math turns up in ordinary answers and KaTeX is small, so precaching it and
|
||||
// its fonts keeps formulas typeset offline. Mermaid is deliberately NOT
|
||||
// precached: at 3.5 MB it would re-download on every CACHE_NAME bump, a poor
|
||||
// trade for a library most sessions never touch. The cache-first rule below
|
||||
// picks it up the first time a diagram renders, which is also when it starts
|
||||
// mattering offline.
|
||||
'/static/lib/katex/katex.min.js',
|
||||
'/static/lib/katex/katex.min.css',
|
||||
...KATEX_FONTS,
|
||||
];
|
||||
|
||||
// Lazily-imported panel modules (js/panels.js). Not in index.html by design;
|
||||
|
||||
Reference in New Issue
Block a user