-- Server-side syntax highlighting for the tree and blob views, used with -- the source-filter setting in cgitrc and the lua: prefix so it runs in -- cgit's embedded interpreter with no per-request process. -- -- source-filter=lua:/usr/lib/cgit/extensions/syntax-highlight.lua -- -- Highlighting is deliberately not built into cgit itself. Without this -- filter cgit serves plain escaped text, and any other program can take -- this filter's place. -- -- REQUIREMENTS -- -- Two pieces, and BOTH must be installed. When either is missing the -- filter serves plain escaped text by design, so uncolored code means -- a missing dependency, not an error. Works with Lua 5.1 onward, -- including LuaJIT. -- -- 1. lpeg, the parsing module for the Lua that cgit is linked -- against. Scintillua does NOT bundle it, it must come from the -- system, and forgetting it is the usual reason nothing happens. -- -- Debian and Ubuntu sudo apt install lua-lpeg -- macOS luarocks --lua-version 5.1 install lpeg -- -- 2. Scintillua, the lexer collection from the Textadept editor. -- Roughly 120 languages as plain .lua files, nothing to compile. -- Download a release and unpack it anywhere. -- -- https://orbitalquark.github.io/scintillua/ -- -- The lexers are found by probing, in order -- -- $CGIT_SCINTILLUA_PATH (used alone when set, no fallback) -- /scintillua/lexers -- /usr/local/share/scintillua/lexers -- /usr/share/scintillua/lexers -- /opt/homebrew/share/scintillua/lexers -- -- so either set the variable in the web server environment, or place -- (or symlink) the scintillua directory next to your cgitrc. When -- Scintillua or lpeg is missing the filter passes text through escaped -- and unhighlighted, so it is safe to enable before the dependencies -- are installed. -- -- OUTPUT -- -- Tokens are wrapped in elements carrying the hl- classes that -- assets/cgit.css styles. Every input byte is preserved, so the line -- number gutter stays aligned. -- Files larger than this are passed through without lexing. local max_bytes = 512 * 1024 local function scintillua_path() local env = os.getenv("CGIT_SCINTILLUA_PATH") if env then return env end local candidates = { "/usr/local/share/scintillua/lexers", "/usr/share/scintillua/lexers", "/opt/homebrew/share/scintillua/lexers", } local config = os.getenv("CGIT_CONFIG") if config then local dir = string.match(config, "^(.*)/[^/]+$") if dir then table.insert(candidates, 1, dir .. "/scintillua/lexers") end end for _, dir in ipairs(candidates) do local f = io.open(dir .. "/lexer.lua", "r") if f then f:close() return dir end end return nil end -- Scintillua tag name (first dotted component) to cgit css class. local css = { comment = "hl-comment", string = "hl-string", regex = "hl-string", number = "hl-number", keyword = "hl-keyword", preprocessor = "hl-keyword", type = "hl-type", class = "hl-type", constant = "hl-number", ["function"] = "hl-func", } local lexer_mod = nil local filename = "" local chunks = {} local function escape(s) s = string.gsub(s, "&", "&") s = string.gsub(s, "<", "<") s = string.gsub(s, ">", ">") return s end local function load_scintillua() local dir = scintillua_path() if not dir then return nil end if not string.find(package.path, dir, 1, true) then package.path = dir .. "/?.lua;" .. package.path end local ok, mod = pcall(require, "lexer") -- A real Scintillua exposes load(); anything else on the path -- that happens to be called lexer is not usable. if ok and type(mod) == "table" and type(mod.load) == "function" then return mod end return nil end -- Resolve a lexer for the file, preferring Scintillua's own filename -- detection when this version provides it. local function lexer_for(name) if type(lexer_mod.detect) == "function" then local ok, lang = pcall(lexer_mod.detect, name) if ok and lang then local okl, lex = pcall(lexer_mod.load, lang) if okl then return lex end end return nil end local ext = string.match(name, "%.([^.]+)$") if not ext then return nil end local ok, lex = pcall(lexer_mod.load, string.lower(ext)) if ok then return lex end return nil end local function highlight(text) local lex = lexer_for(filename) if not lex then return nil end local ok, tokens = pcall(lex.lex, lex, text) if not ok or type(tokens) ~= "table" then return nil end local out = {} local pos = 1 for i = 1, #tokens, 2 do local tag = tokens[i] local fin = tokens[i + 1] local part = escape(string.sub(text, pos, fin - 1)) local class = css[string.match(tag, "^[%w_]+")] if class and part ~= "" then part = "" .. part .. "" end out[#out + 1] = part pos = fin end -- Anything the lexer left unconsumed is kept, escaped. if pos <= #text then out[#out + 1] = escape(string.sub(text, pos)) end return table.concat(out) end function filter_open(name) filename = name or "" chunks = {} end function filter_write(str) chunks[#chunks + 1] = str end function filter_close() local text = table.concat(chunks) chunks = {} if #text <= max_bytes then if lexer_mod == nil then lexer_mod = load_scintillua() or false end if lexer_mod then local ok, marked = pcall(highlight, text) if ok and marked then html(marked) return 0 end end end html(escape(text)) return 0 end