diff options
| author | Bryce Kwon <bryce@brycekwon.com> | |
|---|---|---|
| committer | Bryce Kwon <bryce@brycekwon.com> | |
| commit | ||
| parent | ||
| tree | ||
| download | ||
Add built-in syntax highlighting
Highlighting previously required a source filter shelling out to
something like Pygments. A configured source filter still takes
precedence, every character is preserved so the line gutter stays
aligned, and large files are skipped.
Diffstat (limited to 'assets')
| -rw-r--r-- | assets/cgit.css | 7 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -rw-r--r-- | assets/cgit.js | 226 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
2 files changed, 233 insertions, 0 deletions
diff --git a/assets/cgit.css b/assets/cgit.css index 4504702..96192e4 100644 --- a/assets/cgit.css +++ b/assets/cgit.css @@ -601,6 +601,13 @@ div#cgit table.blob td.linenumbers a:target:before { content: "\2BA9"; } +/* Tokens produced by the client-side highlighter in cgit.js. */ +div#cgit .hl-comment { color: light-dark(#6a737d, #8b949e); font-style: italic; } +div#cgit .hl-keyword { color: light-dark(#cf222e, #ff7b72); } +div#cgit .hl-string { color: light-dark(#0a7d24, #7ee787); } +div#cgit .hl-number { color: light-dark(#0550ae, #79c0ff); } + + /* ---- Blame ------------------------------------------------------------ */ div#cgit table.blame { diff --git a/assets/cgit.js b/assets/cgit.js index 85826b4..cda167e 100644 --- a/assets/cgit.js +++ b/assets/cgit.js @@ -122,3 +122,229 @@ document.addEventListener("DOMContentLoaded", function () { }, false); })(); + +/* Built-in syntax highlighting for the blob view. When no server-side + * source filter is configured, cgit tags the <code> element with + * data-lang set to the file's extension (or bare name, so Makefile and + * Dockerfile work too). We tokenise the plain text into comments, + * strings, numbers and keywords. Every character is preserved so the + * line-number gutter stays aligned, and any failure leaves the original + * text untouched, so a blob is always readable with or without JS. + * + * To support another language: reuse or add a lexical profile in P, add + * one LANGS entry with its keyword list, then map its extensions in EXT. */ + +(function () { + +var MAX_BYTES = 400000; + +/* Lexical profiles shared across language families. Fields: + * line - line-comment prefix, or an array of prefixes + * block - [open, close] block-comment delimiters + * str - characters that open a single-line quoted string + * tick - subset of str whose strings may also span newlines + * tstr - triple-quoted string delimiters (Python-style) + * ci - keywords match case-insensitively (SQL) */ +var P = { + c: { line: "//", block: [ "/*", "*/" ], str: "\"'" }, + tick: { line: "//", block: [ "/*", "*/" ], str: "\"'`", tick: "`" }, + hash: { line: "#", str: "\"'" }, + sql: { line: "--", block: [ "/*", "*/" ], str: "\"'", ci: true }, + lua: { line: "--", block: [ "--[[", "]]" ], str: "\"'" }, + haskell: { line: "--", block: [ "{-", "-}" ], str: "\"" }, + lisp: { line: ";", str: "\"" }, + ini: { line: [ ";", "#" ], str: "\"'" }, + css: { block: [ "/*", "*/" ], str: "\"'" }, + scss: { line: "//", block: [ "/*", "*/" ], str: "\"'" }, + json: { str: "\"" }, + ps: { line: "#", block: [ "<#", "#>" ], str: "\"'" }, + py: { line: "#", str: "\"'", tstr: [ "\"\"\"", "'''" ] } +}; + +var KW = { + c: "auto break case char const continue default do double else enum extern float for goto if inline int long register restrict return short signed sizeof static struct switch typedef union unsigned void volatile while _Bool bool complex size_t ssize_t ptrdiff_t intptr_t uintptr_t int8_t int16_t int32_t int64_t uint8_t uint16_t uint32_t uint64_t true false NULL", + cpp: "auto break case catch char class const consteval constexpr constinit continue co_await co_return co_yield decltype default delete do double dynamic_cast else enum explicit export extern false final float for friend goto if inline int long mutable namespace new noexcept nullptr operator override private protected public register reinterpret_cast return short signed sizeof static static_assert static_cast struct switch template this thread_local throw true try typedef typeid typename union unsigned using virtual void volatile wchar_t while and or not xor concept requires bool size_t std string vector map set", + cs: "abstract as base bool break byte case catch char checked class const continue decimal default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock long namespace new null object operator out override params private protected public readonly ref return sbyte sealed short sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unsafe ushort using virtual void volatile while var async await dynamic yield get set value record nameof when where", + java: "abstract assert boolean break byte case catch char class const continue default do double else enum extends final finally float for goto if implements import instanceof int interface long native new package private protected public return short static strictfp super switch synchronized this throw throws transient try void volatile while true false null var record sealed permits yield String Integer Boolean Object List Map", + js: "async await break case catch class const continue debugger default delete do else export extends finally for function if import in instanceof let new return static super switch this throw try typeof var void while with yield null true false undefined of get set from as NaN Infinity console", + ts: "async await break case catch class const continue debugger default delete do else export extends finally for function if import in instanceof let new return static super switch this throw try typeof var void while with yield null true false undefined of get set from as interface type enum namespace declare abstract implements private public protected readonly any unknown never string number boolean object symbol bigint keyof infer satisfies is", + go: "break case chan const continue default defer else fallthrough for func go goto if import interface map package range return select struct switch type var nil true false iota append cap close complex copy delete imag len make new panic print println real recover string bool byte rune error int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex64 complex128 any", + rust: "as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while union String str Vec Option Result Some None Ok Err Box Rc Arc bool char i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64", + swift: "associatedtype actor async await class deinit enum extension fileprivate func import init inout internal let open operator private protocol public rethrows static struct subscript typealias var break case continue default defer do else fallthrough for guard if in repeat return switch where while as catch is super self Self throw throws try false true nil some Any Int Double String Bool Array Dictionary Optional", + kotlin: "abstract actual annotation as break by catch class companion const constructor continue crossinline data delegate do dynamic else enum external false final finally for fun get if import in infix init inline inner interface internal is lateinit noinline null object open operator out override package private protected public reified return sealed set super suspend tailrec this throw true try typealias typeof val var vararg when where while Int Long Double Float String Boolean List Map", + scala: "abstract case catch class def do else extends false final finally for forSome given if implicit import lazy match new null object override package private protected return sealed super this throw trait try true type using val var while with yield enum export then Int Long Double String Boolean List Map Option Some None", + php: "abstract and array as break callable case catch class clone const continue declare default do echo else elseif empty enddeclare endfor endforeach endif endswitch endwhile enum extends final finally fn for foreach function global goto if implements include include_once instanceof insteadof interface isset list match namespace new or print private protected public readonly require require_once return static switch throw trait try unset use var while xor yield true false null self parent int float string bool void mixed object", + dart: "abstract as assert async await break case catch class const continue covariant default deferred do dynamic else enum export extends extension external factory false final finally for get hide if implements import in interface is late library mixin new null on operator part required rethrow return set show static super switch sync this throw true try typedef var void while with yield int double String bool List Map Set Future Stream", + objc: "auto break case char const continue default do double else enum extern float for goto if inline int long register restrict return short signed sizeof static struct switch typedef union unsigned void volatile while bool true false NULL id Class SEL IMP BOOL YES NO nil Nil self super instancetype nonatomic atomic strong weak copy assign retain readonly readwrite nonnull nullable NSString NSObject NSArray NSInteger NSUInteger", + d: "abstract alias align asm assert auto body bool break byte case cast catch cdouble cent cfloat char class const continue creal dchar debug default delegate delete deprecated do double else enum export extern false final finally float for foreach function goto idouble if ifloat immutable import in inout int interface invariant ireal is lazy long macro mixin module new nothrow null out override package pragma private protected public pure real ref return scope shared short static struct super switch synchronized template this throw true try typeid typeof ubyte uint ulong union unittest ushort version void wchar while with string", + zig: "addrspace align allowzero and anyframe anytype asm async await break callconv catch comptime const continue defer else enum errdefer error export extern fn for if inline noalias noinline nosuspend opaque or orelse packed pub resume return struct suspend switch test threadlocal try union unreachable usingnamespace var volatile while bool void type u8 u16 u32 u64 usize i8 i16 i32 i64 isize f16 f32 f64 f128 comptime_int comptime_float true false null undefined", + py: "False None True and as assert async await break class continue def del elif else except finally for from global if import in is lambda match nonlocal not or pass raise return try while with yield self cls print range len int str float bool list dict set tuple bytes object Exception", + sh: "if then elif else fi for while until do done case esac in function select return local export readonly declare typeset unset shift eval exec source break continue trap set getopts read echo printf test cd pushd popd", + ruby: "alias and begin break case class def defined do else elsif end ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield require require_relative attr_accessor attr_reader attr_writer include extend private public protected new puts print lambda proc", + perl: "if elsif else unless while until for foreach do sub return last next redo goto my our local use require package no and or not eq ne lt gt le ge cmp print printf say chomp chop split join map grep keys values each shift unshift push pop defined undef ref bless wantarray qw", + r: "if else repeat while function for in next break TRUE FALSE NULL Inf NaN NA return library require", + yaml: "true false null yes no on off True False Null Yes No On Off", + toml: "true false", + ini: "true false yes no on off", + make: "ifeq ifneq ifdef ifndef else endif define endef include export unexport override vpath and or foreach filter wildcard patsubst subst shell call eval error warning info notdir dir basename suffix addprefix addsuffix", + docker: "FROM RUN CMD LABEL MAINTAINER EXPOSE ENV ADD COPY ENTRYPOINT VOLUME USER WORKDIR ARG ONBUILD STOPSIGNAL HEALTHCHECK SHELL AS", + lua: "and break do else elseif end false for function goto if in local nil not or repeat return then true until while self require print pairs ipairs tostring tonumber type setmetatable getmetatable pcall error assert table string math io os", + sql: "select from where insert into values update set delete create table drop alter add column index view join inner outer left right full on as distinct group by order having limit offset union all and or not null is in like between exists case when then else end asc desc primary key foreign references default unique check constraint int integer varchar char text date datetime timestamp boolean float double decimal begin commit rollback transaction grant revoke with returning true false", + haskell: "case class data default deriving do else foreign if import in infix infixl infixr instance let module newtype of then type where as qualified hiding forall family Int Integer Float Double Char String Bool Maybe Just Nothing Either Left Right IO map filter foldr foldl", + lisp: "defun defvar defparameter defmacro defconstant let let* lambda if cond case when unless progn setq setf quote function list cons car cdr append mapcar reduce and or not nil t define set begin delay force define-syntax syntax-rules require provide ns defn def fn loop recur do var true false", + scss: "if else for each while function return extend use import forward media content mixin include null true false and or not in from through to default important", + json: "true false null", + ps: "if else elseif switch foreach for while do until break continue function return param begin process end try catch finally throw class enum filter workflow in trap exit true false null" +}; + +var LANGS = { + c: def(KW.c, P.c), cpp: def(KW.cpp, P.c), cs: def(KW.cs, P.c), + java: def(KW.java, P.c), js: def(KW.js, P.tick), ts: def(KW.ts, P.tick), + go: def(KW.go, P.c), rust: def(KW.rust, P.c), swift: def(KW.swift, P.c), + kotlin: def(KW.kotlin, P.c), scala: def(KW.scala, P.c), + php: def(KW.php, { line: [ "//", "#" ], block: [ "/*", "*/" ], str: "\"'" }), + dart: def(KW.dart, P.c), objc: def(KW.objc, P.c), d: def(KW.d, P.c), + zig: def(KW.zig, { line: "//", str: "\"" }), + py: def(KW.py, P.py), sh: def(KW.sh, P.hash), ruby: def(KW.ruby, P.hash), + perl: def(KW.perl, P.hash), r: def(KW.r, P.hash), yaml: def(KW.yaml, P.hash), + toml: def(KW.toml, P.hash), ini: def(KW.ini, P.ini), + make: def(KW.make, P.hash), docker: def(KW.docker, P.hash), + lua: def(KW.lua, P.lua), sql: def(KW.sql, P.sql), + haskell: def(KW.haskell, P.haskell), lisp: def(KW.lisp, P.lisp), + css: def("", P.css), scss: def(KW.scss, P.scss), json: def(KW.json, P.json), + ps: def(KW.ps, P.ps) +}; + +/* file extension (or bare filename), lowercased, -> language id */ +var EXT = { + c: "c", h: "c", cc: "cpp", cpp: "cpp", cxx: "cpp", hpp: "cpp", hh: "cpp", + hxx: "cpp", ipp: "cpp", cs: "cs", java: "java", + js: "js", mjs: "js", cjs: "js", jsx: "js", + ts: "ts", tsx: "ts", mts: "ts", cts: "ts", + go: "go", rs: "rust", swift: "swift", kt: "kotlin", kts: "kotlin", + scala: "scala", sc: "scala", php: "php", phtml: "php", + dart: "dart", m: "objc", mm: "objc", d: "d", di: "d", zig: "zig", + py: "py", pyw: "py", pyi: "py", + sh: "sh", bash: "sh", zsh: "sh", ksh: "sh", bashrc: "sh", zshrc: "sh", + rb: "ruby", gemspec: "ruby", rake: "ruby", + pl: "perl", pm: "perl", r: "r", + yaml: "yaml", yml: "yaml", toml: "toml", + ini: "ini", cfg: "ini", conf: "ini", + makefile: "make", mk: "make", mak: "make", dockerfile: "docker", + lua: "lua", sql: "sql", hs: "haskell", + lisp: "lisp", cl: "lisp", el: "lisp", clj: "lisp", cljs: "lisp", + cljc: "lisp", scm: "lisp", rkt: "lisp", + css: "css", scss: "scss", sass: "scss", less: "scss", + json: "json", jsonc: "json", + ps1: "ps", psm1: "ps", psd1: "ps", + /* language names as they appear in markdown code fences */ + python: "py", ruby: "ruby", rust: "rust", golang: "go", cpp: "cpp", + "c++": "cpp", csharp: "cs", "c#": "cs", javascript: "js", + typescript: "ts", kotlin: "kotlin", haskell: "haskell", shell: "sh" +}; + +function words(s, ci) { + var d = Object.create(null), a = s.split(/\s+/), i, w; + for (i = 0; i < a.length; i++) { + w = a[i]; + if (w) + d[ci ? w.toLowerCase() : w] = true; + } + return d; +} + +function norm(v) { + return v == null ? [] : (typeof v === "string" ? [ v ] : v); +} + +function def(kw, p) { + return { + kw: words(kw || "", p.ci), ci: !!p.ci, + line: norm(p.line), bs: p.block ? p.block[0] : null, + be: p.block ? p.block[1] : null, str: p.str || "", + tick: p.tick || "", tstr: norm(p.tstr) + }; +} + +var reWord = /[A-Za-z_$][\w$]*/y; +var reNum = /(?:0[xX][0-9a-fA-F]+|0[bB][01]+|\d[\d_]*(?:\.\d+)?(?:[eE][+-]?\d+)?)[fFlLuU]*/y; + +function esc(s) { + return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); +} + +function span(cls, s) { + return "<span class='hl-" + cls + "'>" + esc(s) + "</span>"; +} + +function tokenize(text, cfg) { + var out = "", i = 0, n = text.length, c, j, k, m, t, multi; + while (i < n) { + c = text.charAt(i); + if (cfg.bs && text.startsWith(cfg.bs, i)) { + j = text.indexOf(cfg.be, i + cfg.bs.length); + j = j < 0 ? n : j + cfg.be.length; + out += span("comment", text.slice(i, j)); i = j; continue; + } + for (k = 0; k < cfg.line.length; k++) + if (text.startsWith(cfg.line[k], i)) { + j = text.indexOf("\n", i); if (j < 0) j = n; + out += span("comment", text.slice(i, j)); i = j; break; + } + if (k < cfg.line.length) continue; + for (k = 0; k < cfg.tstr.length; k++) { + t = cfg.tstr[k]; + if (text.startsWith(t, i)) { + j = text.indexOf(t, i + t.length); + j = j < 0 ? n : j + t.length; + out += span("string", text.slice(i, j)); i = j; break; + } + } + if (k < cfg.tstr.length) continue; + if (cfg.str.indexOf(c) >= 0) { + multi = cfg.tick.indexOf(c) >= 0; + j = i + 1; + while (j < n) { + if (text.charAt(j) === "\\") { j += 2; continue; } + if (text.charAt(j) === c) { j++; break; } + if (text.charAt(j) === "\n" && !multi) break; + j++; + } + out += span("string", text.slice(i, j)); i = j; continue; + } + if (c >= "0" && c <= "9") { + reNum.lastIndex = i; + m = reNum.exec(text); + if (m) { out += span("number", m[0]); i += m[0].length; continue; } + } + reWord.lastIndex = i; + m = reWord.exec(text); + if (m) { + t = cfg.ci ? m[0].toLowerCase() : m[0]; + out += cfg.kw[t] ? span("keyword", m[0]) : esc(m[0]); + i += m[0].length; continue; + } + out += esc(c); i++; + } + return out; +} + +document.addEventListener("DOMContentLoaded", function () { + var nodes = document.querySelectorAll("div#cgit code[data-lang]"), i, el, cfg, text; + for (i = 0; i < nodes.length; i++) { + el = nodes[i]; + cfg = LANGS[EXT[(el.getAttribute("data-lang") || "").toLowerCase()]]; + if (!cfg) + continue; + text = el.textContent; + if (!text || text.length > MAX_BYTES) + continue; + try { + el.innerHTML = tokenize(text, cfg); + } catch (e) { + /* leave the plain text untouched on any failure */ + } + } +}, false); + +})(); |
