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. | |