aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--filters/syntax-highlighting.js/.gitignore3
-rw-r--r--filters/syntax-highlighting.js/README14
-rwxr-xr-xfilters/syntax-highlighting.js/generate.py187
-rw-r--r--filters/syntax-highlighting.js/loader.js57
-rw-r--r--filters/syntax-highlighting.js/style.css229
5 files changed, 490 insertions, 0 deletions
diff --git a/filters/syntax-highlighting.js/.gitignore b/filters/syntax-highlighting.js/.gitignore
new file mode 100644
index 0000000..e351b17
--- /dev/null
+++ b/filters/syntax-highlighting.js/.gitignore
@@ -0,0 +1,3 @@
+core.min.js
+filenames.js
+languages/
diff --git a/filters/syntax-highlighting.js/README b/filters/syntax-highlighting.js/README
new file mode 100644
index 0000000..9ea3a7f
--- /dev/null
+++ b/filters/syntax-highlighting.js/README
@@ -0,0 +1,14 @@
+Serve this directory at /syntax-highlighting.js/ (by symlinking it from
+filters/, for example) and configure cgit:
+
+ js=/cgit.js
+ js=/syntax-highlighting.js/loader.js
+
+Remove source-filter and repo.source-filter settings. Styles and language
+modules load automatically from the same directory as loader.js.
+Pastie and Pastie Dark follow the browser's color preference.
+
+To generate the required files, install Python 3, Pygments, and Node.js,
+then run from this directory:
+
+ ./generate.py
diff --git a/filters/syntax-highlighting.js/generate.py b/filters/syntax-highlighting.js/generate.py
new file mode 100755
index 0000000..61476f6
--- /dev/null
+++ b/filters/syntax-highlighting.js/generate.py
@@ -0,0 +1,187 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+
+import fnmatch
+import io
+import json
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tarfile
+import tempfile
+from urllib.request import urlopen
+
+import pygments
+from pygments.lexers import _iter_lexerclasses
+
+OUTPUT = Path(__file__).resolve().parent
+
+TRANSLATIONS = {
+ "actionscript3": ["actionscript"],
+ "bdd": ["gherkin"],
+ "batch": ["dos"],
+ "capnp": ["capnproto"],
+ "clojurescript": ["clojure"],
+ "cuda": ["cpp"],
+ "console": ["shell"],
+ "desktop": ["freedesktop"],
+ "erl": ["erlang-repl"],
+ "fortranfixed": ["fortran"],
+ "gas": ["x86asm", "armasm", "avrasm"],
+ "html+django": ["django"],
+ "html+handlebars": ["handlebars"],
+ "html+php": ["php-template"],
+ "html+twig": ["twig"],
+ "html": ["xml"],
+ "json5": ["json"],
+ "jsp": ["java"],
+ "jsx": ["javascript"],
+ "k": ["q"],
+ "mips": ["mipsasm"],
+ "nasm": ["x86asm"],
+ "objective-c++": ["objectivec"],
+ "octave": ["matlab"],
+ "rhtml": ["erb"],
+ "roboconf-graph": ["roboconf"],
+ "roboconf-instances": ["roboconf"],
+ "slurm": ["bash"],
+ "systemd": ["freedesktop"],
+ "systemverilog": ["verilog"],
+ "tasm": ["x86asm"],
+ "text": ["plaintext"],
+ "toml": ["ini"],
+ "tsx": ["typescript"],
+ "wast": ["wasm"],
+ "xpp": ["axapta"],
+ "xslt": ["xml"],
+ "zone": ["dns"],
+}
+
+INTERPRETERS = {
+ "bash": r"^(?:ba|da|k|z)?sh$",
+ "python": r"^(?:python|pypy)[\d.]*$",
+ "javascript": r"^node(?:js)?$",
+ "perl": r"^perl$",
+ "ruby": r"^ruby$",
+ "lua": r"^lua[\d.]*$",
+ "php": r"^php$",
+}
+
+EXTRA_FILENAMES = {
+ "asciidoc": ["*.adoc", "*.asciidoc"],
+ "bash": ["*.sh.in"],
+ "cmake": ["*.cmake.in"],
+ "coffeescript": ["*.cson", "*.iced"],
+ "django": ["*.jinja"],
+ "erb": ["*.erb"],
+ "fortran": ["*.f95"],
+ "graphql": ["*.gql"],
+ "json": ["*.jsonc"],
+ "makefile": ["Kbuild", "BSDmakefile"],
+ "markdown": ["*.mkd", "*.mkdown"],
+ "typescript": ["*.cts", "*.mts"],
+}
+
+INSPECT = r"""
+import fs from 'node:fs';
+import { pathToFileURL } from 'node:url';
+const base = process.argv[1];
+const { default: hljs } = await import(pathToFileURL(base + '/core.min.js'));
+for (const file of fs.readdirSync(base + '/languages').sort()) {
+ const { default: grammar } = await import(pathToFileURL(base + '/languages/' + file));
+ hljs.registerLanguage(file.replace('.min.js', ''), grammar);
+}
+const names = new Map(hljs.listLanguages().map(name => [hljs.getLanguage(name), name]));
+const info = {};
+for (const [language, name] of names) {
+ const seen = new Set(), dependencies = new Set();
+ function walk(value) {
+ if (!value || typeof value !== 'object' || seen.has(value)) return;
+ seen.add(value);
+ for (const alias of [value.subLanguage || []].flat()) {
+ const grammar = hljs.getLanguage(alias);
+ if (grammar && grammar !== language) {
+ dependencies.add(names.get(grammar));
+ walk(grammar);
+ }
+ }
+ Object.values(value).forEach(walk);
+ }
+ walk(language);
+ info[name] = [...dependencies].sort();
+}
+console.log(JSON.stringify(info));
+"""
+
+
+def pattern_regex(pattern):
+ regex = fnmatch.translate(pattern)
+ assert regex.startswith("(?s:") and regex.endswith((")\\Z", ")\\z"))
+ return regex[4:-3].replace("(?>", "(?:").replace("/", "\\/")
+
+
+def generate():
+ for name in ("core.min.js", "filenames.js"):
+ (OUTPUT / name).unlink(missing_ok=True)
+ if (OUTPUT / "languages").exists():
+ shutil.rmtree(OUTPUT / "languages")
+
+ highlightjs = json.load(urlopen("https://registry.npmjs.org/@highlightjs/cdn-assets/latest"))
+ archive = urlopen(highlightjs["dist"]["tarball"]).read()
+
+ with tempfile.TemporaryDirectory() as temporary:
+ source = Path(temporary)
+ print(f"Using highlight.js {highlightjs['version']} and Pygments {pygments.__version__}")
+ with tarfile.open(fileobj=io.BytesIO(archive), mode="r:gz") as tar:
+ for member in tar.getmembers():
+ if re.fullmatch(r"package/es/(core\.min\.js|languages/[\w-]+\.min\.js)", member.name):
+ target = source / member.name.removeprefix("package/es/")
+ target.parent.mkdir(parents=True, exist_ok=True)
+ target.write_bytes(tar.extractfile(member).read())
+ (source / "package.json").write_text('{"type":"module"}\n')
+ info = json.loads(subprocess.check_output(
+ ["node", "--input-type=module", "-e", INSPECT, str(source)], text=True))
+ groups = {}
+ matched = 0
+
+ def add(names, patterns, priority=0, primary=True):
+ for name in names:
+ if name not in info:
+ raise ValueError(f"No highlight.js grammar: {name}")
+ groups.setdefault((not primary, -priority, name), set()).update(patterns)
+
+ for lexer in _iter_lexerclasses(plugins=False):
+ names = next((TRANSLATIONS[a] for a in lexer.aliases if a in TRANSLATIONS), None)
+ if names is None:
+ name = next((a for a in lexer.aliases if a in info), None)
+ names = [name] if name else []
+ if names:
+ matched += 1
+ add(names, lexer.filenames, lexer.priority)
+ add(names, lexer.alias_filenames, lexer.priority, primary=False)
+
+ add(["cpp"], ["*.h"], 0.1)
+ for name, patterns in EXTRA_FILENAMES.items():
+ add([name], patterns)
+
+ dependencies = {name: deps for name, deps in info.items() if deps}
+ output = ["export const rules = ["]
+ for (_, _, name), patterns in sorted(groups.items()):
+ if patterns:
+ regex = "|".join(pattern_regex(p) for p in sorted(patterns))
+ output.append(f"[{json.dumps(name)},/^(?:{regex})(?!.)/s],")
+ output.extend(["];", "export const interpreters = ["])
+ output.extend(f"[{json.dumps(name)},/{regex}/]," for name, regex in INTERPRETERS.items())
+ output.extend(["];", "export const dependencies = " + json.dumps(dependencies, separators=(",", ":")) + ";", ""])
+
+ (OUTPUT / "languages").mkdir()
+ for asset in [source / "core.min.js", *sorted((source / "languages").glob("*.min.js"))]:
+ (OUTPUT / asset.relative_to(source)).write_bytes(asset.read_bytes())
+ (OUTPUT / "filenames.js").write_text("\n".join(output))
+ print(f"Mapped {matched} Pygments lexers; vendored {len(info)} grammars")
+ print(f"Filename rules and dependency data: {(OUTPUT / 'filenames.js').stat().st_size} bytes")
+
+
+if __name__ == "__main__":
+ generate()
diff --git a/filters/syntax-highlighting.js/loader.js b/filters/syntax-highlighting.js/loader.js
new file mode 100644
index 0000000..40cd71c
--- /dev/null
+++ b/filters/syntax-highlighting.js/loader.js
@@ -0,0 +1,57 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+(function () {
+ const base = new URL(".", document.currentScript.src);
+
+ document.addEventListener("DOMContentLoaded", async function () {
+ const code = document.querySelector("#cgit table.blob td.lines pre > code");
+ if (!code || code.children.length)
+ return;
+ const style = document.createElement("link");
+ style.rel = "stylesheet";
+ style.href = new URL("style.css", base).href;
+ document.head.appendChild(style);
+ const crumb = document.querySelector("#cgit .path a:last-child");
+ const filename = crumb?.textContent.split("/").pop() || "";
+ const text = code.textContent;
+ try {
+ const { rules, interpreters, dependencies } = await import(new URL("filenames.js", base).href);
+ let names = rules.filter(([, pattern]) => pattern.test(filename)).map(([name]) => name);
+ const shebang = !names.length && /^#![ \t]*(\S+)([^\r\n]*)/.exec(text);
+ if (shebang) {
+ let command = shebang[1];
+ if (command.split("/").pop() === "env") {
+ const args = shebang[2].trim();
+ const split = /^(?:-S[ \t]*|--split-string(?:=|[ \t]+))/.exec(args);
+ const word = split
+ ? /^(?:"([\w./+-]+)"|'([\w./+-]+)'|([\w./+-]+))(?:[ \t]|$)/.exec(args.slice(split[0].length))
+ : /^([\w./+-]+)$/.exec(args);
+ command = word?.slice(1).find(Boolean) || "";
+ }
+ const interpreter = command.split("/").pop();
+ names = interpreters.filter(([, pattern]) => pattern.test(interpreter)).map(([name]) => name);
+ }
+ names = [...new Set(names)].filter(name => name !== "plaintext");
+ if (!names.length)
+ return;
+
+ const { default: hljs } = await import(new URL("core.min.js", base).href);
+ const needed = new Set(names.flatMap(name => [name, ...(dependencies[name] || [])]));
+ await Promise.all([...needed].map(async name => {
+ const { default: grammar } = await import(new URL(`languages/${name}.min.js`, base).href);
+ hljs.registerLanguage(name, grammar);
+ }));
+ let best;
+ for (const language of names) {
+ const result = hljs.highlight(text, { language, ignoreIllegals: names.length === 1 });
+ if (!result.illegal && (!best || result.relevance > best.relevance))
+ best = result;
+ }
+ if (best) {
+ code.innerHTML = best.value;
+ code.classList.add("hljs");
+ }
+ } catch (error) {
+ console.warn("Syntax highlighting failed:", error);
+ }
+ });
+})();
diff --git a/filters/syntax-highlighting.js/style.css b/filters/syntax-highlighting.js/style.css
new file mode 100644
index 0000000..26c0f36
--- /dev/null
+++ b/filters/syntax-highlighting.js/style.css
@@ -0,0 +1,229 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#cgit table.blob {
+ --hl-text: currentColor;
+ --hl-comment: #888888;
+ --hl-meta: #cc0000;
+ --hl-keyword: #008800;
+ --hl-string: #dd2200;
+ --hl-symbol: #aa6600;
+ --hl-subst: #3333bb;
+ --hl-escape: #0044dd;
+ --hl-function: #0066bb;
+ --hl-class: #bb0066;
+ --hl-builtin: #003388;
+ --hl-variable: #336699;
+ --hl-constant: #003366;
+ --hl-number: #0000dd;
+ --hl-heading: #333333;
+ --hl-change-text: #000000;
+ --hl-string-bg: #fff0f0;
+ --hl-regexp-bg: #fff0ff;
+ --hl-addition-bg: #ddffdd;
+ --hl-deletion-bg: #ffdddd;
+}
+
+@media (prefers-color-scheme: dark) {
+ #cgit table.blob {
+ --hl-text: #e6e6e6;
+ --hl-comment: #999999;
+ --hl-meta: #ff7070;
+ --hl-keyword: #8fd46a;
+ --hl-string: #ff9777;
+ --hl-symbol: #e3b66e;
+ --hl-subst: #b6a2ff;
+ --hl-escape: #82bcff;
+ --hl-function: #79bbff;
+ --hl-class: #f28fc6;
+ --hl-builtin: #99aaff;
+ --hl-variable: #8fc7de;
+ --hl-constant: #a8d7ed;
+ --hl-number: #879dff;
+ --hl-heading: #e8e8e8;
+ --hl-change-text: #e6e6e6;
+ --hl-string-bg: #2c201f;
+ --hl-regexp-bg: #2b2132;
+ --hl-addition-bg: #203626;
+ --hl-deletion-bg: #3a2024;
+ }
+}
+
+#cgit table.blob pre > code {
+ font: inherit;
+}
+
+#cgit table.blob .hljs {
+ color: var(--hl-text);
+ font-weight: normal;
+ font-style: normal;
+}
+
+#cgit table.blob .hljs-comment,
+#cgit table.blob .hljs-quote,
+#cgit table.blob .hljs-code,
+#cgit table.blob .hljs-formula {
+ color: var(--hl-comment);
+ font-weight: normal;
+ font-style: normal;
+}
+
+#cgit table.blob .hljs-meta,
+#cgit table.blob .hljs-meta .hljs-keyword {
+ color: var(--hl-meta);
+ font-weight: bold;
+ font-style: normal;
+}
+
+#cgit table.blob .hljs-doctag {
+ color: var(--hl-meta);
+ font-weight: bold;
+ font-style: normal;
+ background-color: var(--hl-string-bg);
+}
+
+#cgit table.blob .hljs-keyword,
+#cgit table.blob .hljs-template-tag,
+#cgit table.blob .hljs-selector-tag {
+ color: var(--hl-keyword);
+ font-weight: bold;
+ font-style: normal;
+}
+
+#cgit table.blob .hljs-type {
+ color: var(--hl-comment);
+ font-weight: bold;
+ font-style: normal;
+}
+
+#cgit table.blob .hljs-string,
+#cgit table.blob .hljs-meta .hljs-string {
+ color: var(--hl-string);
+ font-weight: normal;
+ font-style: normal;
+ background-color: var(--hl-string-bg);
+}
+
+#cgit table.blob .hljs-regexp {
+ color: var(--hl-keyword);
+ font-weight: normal;
+ font-style: normal;
+ background-color: var(--hl-regexp-bg);
+}
+
+#cgit table.blob .hljs-symbol {
+ color: var(--hl-symbol);
+ font-weight: normal;
+ font-style: normal;
+ background-color: var(--hl-string-bg);
+}
+
+#cgit table.blob .hljs-subst,
+#cgit table.blob .hljs-template-variable {
+ color: var(--hl-subst);
+ font-weight: normal;
+ font-style: normal;
+ background-color: var(--hl-string-bg);
+}
+
+#cgit table.blob .hljs-char.escape_ {
+ color: var(--hl-escape);
+ font-weight: normal;
+ font-style: normal;
+ background-color: var(--hl-string-bg);
+}
+
+#cgit table.blob .hljs-title {
+ color: var(--hl-function);
+ font-weight: bold;
+ font-style: normal;
+}
+
+#cgit table.blob .hljs-title.class_,
+#cgit table.blob .hljs-class .hljs-title,
+#cgit table.blob .hljs-name,
+#cgit table.blob .hljs-selector-class,
+#cgit table.blob .hljs-selector-id,
+#cgit table.blob .hljs-selector-pseudo {
+ color: var(--hl-class);
+ font-weight: bold;
+ font-style: normal;
+}
+
+#cgit table.blob .hljs-built_in {
+ color: var(--hl-builtin);
+ font-weight: normal;
+ font-style: normal;
+}
+
+#cgit table.blob .hljs-variable,
+#cgit table.blob .hljs-attr,
+#cgit table.blob .hljs-attribute,
+#cgit table.blob .hljs-selector-attr {
+ color: var(--hl-variable);
+ font-weight: normal;
+ font-style: normal;
+}
+
+#cgit table.blob .hljs-variable.language_ {
+ color: var(--hl-subst);
+ font-weight: normal;
+ font-style: normal;
+}
+
+#cgit table.blob .hljs-literal,
+#cgit table.blob .hljs-variable.constant_ {
+ color: var(--hl-constant);
+ font-weight: bold;
+ font-style: normal;
+}
+
+#cgit table.blob .hljs-property {
+ color: var(--hl-variable);
+ font-weight: bold;
+ font-style: normal;
+}
+
+#cgit table.blob .hljs-number {
+ color: var(--hl-number);
+ font-weight: bold;
+ font-style: normal;
+}
+
+#cgit table.blob .hljs-section {
+ color: var(--hl-heading);
+ font-weight: normal;
+ font-style: normal;
+}
+
+#cgit table.blob .hljs-bullet,
+#cgit table.blob .hljs-link {
+ color: var(--hl-variable);
+ font-weight: normal;
+ font-style: italic;
+}
+
+#cgit table.blob .hljs-addition {
+ color: var(--hl-change-text);
+ font-weight: normal;
+ font-style: normal;
+ background-color: var(--hl-addition-bg);
+}
+
+#cgit table.blob .hljs-deletion {
+ color: var(--hl-change-text);
+ font-weight: normal;
+ font-style: normal;
+ background-color: var(--hl-deletion-bg);
+}
+
+#cgit table.blob .hljs-emphasis {
+ color: var(--hl-text);
+ font-weight: normal;
+ font-style: italic;
+}
+
+#cgit table.blob .hljs-strong {
+ color: var(--hl-text);
+ font-weight: bold;
+ font-style: normal;
+}