diff options
| -rw-r--r-- | filters/syntax-highlighting.js/.gitignore | 3 | ||||
| -rw-r--r-- | filters/syntax-highlighting.js/README | 14 | ||||
| -rwxr-xr-x | filters/syntax-highlighting.js/generate.py | 187 | ||||
| -rw-r--r-- | filters/syntax-highlighting.js/loader.js | 57 | ||||
| -rw-r--r-- | filters/syntax-highlighting.js/style.css | 229 |
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; +} |
