Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Load a Prism grammar only when a blob names it

1b8675b19e70346d6d02d5fc0c10538f50bf94b2 · Keenan Nemetz · 9d ago · parent 11720250

Load a Prism grammar only when a blob names it

The worker imported prism-languages.min.js — every grammar Prism ships — on
every code page. That is 546 KB to evaluate before the first token, and a page
showing one Go file paid it for all 293 components.

Each component is now vendored as its own file, and prism-manifest.js maps a
language name to the files that define it in dependency order. The worker
starts with Prism's core and the manifest and imports a language's files the
first time a job names it, remembering what it has run so a file is never
imported twice. A Go blob loads 30 KB instead of 564 KB; a .tsx blob, the
widest common entry at 8 files, loads 49 KB.

The manifest also carries the components that register no language of their
own but change one: css-extras, js-extras, js-templates, php-extras, xml-doc.
Each is placed after the grammar it rewrites and included in the entries that
inherit it, so a language that extends javascript now gets js-extras' arrow
and maybe-class-name tokens too. The old bundle's order handed those to some
inheriting languages and not others; nothing loses a token.

scripts/vendor-prism.py writes the grammars and the manifest, and --check
compares them byte for byte with cdnjs. TestPrismLanguagesAreVendored,
TestPrismGrammarsMatchTheManifest and TestPrismWorkerLoadsGrammarsOnDemand
hold the server's extension table, the vendored set and the worker's lazy
import together.

server/web/static/prism-worker.js +44−2

@@ -6,12 +6,54 @@
6 6 * swaps in the contents of each line cell, so line numbers and anchors are
7 7 * present before this file runs and stay put afterwards.
8 8 *
9 * The worker starts with Prism's core — markup, css, clike, javascript — and a
10 * manifest of every other language, not with every grammar Prism ships. A job
11 * that names a language this worker has not loaded imports that language's files
12 * from static/prism-languages/ first, once: a page pays for the grammars it
13 * shows, and the code browser rarely shows more than a few. The manifest entry
14 * also carries the components that only tweak another grammar (js-extras,
15 * php-extras, xml-doc), which is why a language already present can still have
16 * files to import.
17 *
9 18 * Prism's core detects a WorkerGlobalScope, but `manual` keeps it from trying to
10 19 * walk a document that does not exist and `disableWorkerMessageHandler` keeps it
11 20 * from installing its own JSON-string listener on top of ours. */
12 21
13 22 self.Prism = { manual: true, disableWorkerMessageHandler: true };
14 importScripts('prism.min.js', 'prism-languages.min.js');
23 importScripts('prism.min.js', 'prism-manifest.js');
24
25 const has = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
26
27 // imported is what this worker has already run. importScripts re-executes a
28 // script it is given twice, and an extras component run twice would insert its
29 // grammar into the same language again, so each file is imported at most once.
30 const imported = new Set();
31
32 // load imports a language's files in manifest order, dependencies first. A file
33 // that will not load is not fatal and not remembered, so a later job tries it
34 // again rather than inheriting this job's failure.
35 function load(files) {
36 const missing = files.filter((name) => !imported.has(name));
37 if (!missing.length) return true;
38 try {
39 // importScripts takes URLs as arguments; an array would stringify to one
40 // comma-joined URL and fetch nothing.
41 importScripts(...missing.map((name) => 'prism-languages/' + name + '.js'));
42 } catch (err) {
43 return false;
44 }
45 missing.forEach((name) => imported.add(name));
46 return true;
47 }
48
49 // grammarFor returns the grammar for a language, loading it first if needed. An
50 // unknown name and a grammar that will not load both answer nothing, which the
51 // caller turns into the plain text the page was served with.
52 function grammarFor(lang) {
53 const files = has(self.PRISM_LANGUAGES, lang) ? self.PRISM_LANGUAGES[lang] : null;
54 if (files && !load(files)) return null;
55 return has(self.Prism.languages, lang) ? self.Prism.languages[lang] : null;
56 }
15 57
16 58 // splitLines closes the spans that are open at a newline and reopens them on the
17 59 // next line, so a multi-line comment or string keeps its class across lines
@@ -47,7 +89,7 @@ function splitLines(html) {
47 89
48 90 self.onmessage = (event) => {
49 91 const job = event.data || {};
50 const grammar = self.Prism.languages[job.lang];
92 const grammar = typeof job.lang === 'string' ? grammarFor(job.lang) : null;
51 93 if (!grammar || typeof job.code !== 'string') {
52 94 // An unknown language is not an error: the page keeps the plain text it was
53 95 // served with.