/* ═══════════════════════════════════════════════════════════════════ FlowDeck — central syntax-highlighting helper (highlight.js, vendored). Single source of truth mapping file extensions / well-known filenames to highlight.js language ids. Covers ~230 file types (code, config, IaC, docs). Every code viewer (Gitea file view, page-editor code blocks & file viewer, workspace previews, public pages) must go through this helper so a new language is added in ONE place. Requires (load order): highlight.min.js, highlight-extra.min.js. All functions are defensive: without hljs they degrade to escaped plain text — never throw, never use eval (CSP-safe). ═══════════════════════════════════════════════════════════════════ */ (function () { 'use strict'; /* Extension (lowercase, no dot) → highlight.js language id. Ids must exist in static/js/highlight.min.js (common bundle) or static/js/highlight-extra.min.js — anything else falls back to the alias table / plaintext via normalizeLang(). */ var EXT_TO_LANG = { // ── Python ── py: 'python', pyw: 'python', pyi: 'python', gyp: 'python', gypi: 'python', // ── JavaScript / TypeScript ── js: 'javascript', mjs: 'javascript', cjs: 'javascript', jsx: 'javascript', ts: 'typescript', mts: 'typescript', cts: 'typescript', tsx: 'typescript', // ── Web markup / styles ── html: 'xml', htm: 'xml', xhtml: 'xml', xht: 'xml', xml: 'xml', xsd: 'xml', xsl: 'xml', xslt: 'xml', rss: 'xml', atom: 'xml', plist: 'xml', wsf: 'xml', svg: 'xml', vue: 'xml', svelte: 'xml', astro: 'xml', axml: 'xml', css: 'css', scss: 'scss', sass: 'scss', less: 'less', // ── Data / config ── json: 'json', json5: 'json', jsonc: 'json', geojson: 'json', webmanifest: 'json', avsc: 'json', har: 'json', yaml: 'yaml', yml: 'yaml', toml: 'ini', ini: 'ini', cfg: 'ini', conf: 'ini', env: 'ini', properties: 'ini', prop: 'ini', gitconfig: 'ini', gitmodules: 'ini', editorconfig: 'ini', inf: 'ini', url: 'ini', reg: 'ini', // ── Markdown / docs ── md: 'markdown', markdown: 'markdown', mkd: 'markdown', mdwn: 'markdown', mdown: 'markdown', rmd: 'markdown', qmd: 'markdown', tex: 'latex', latex: 'latex', sty: 'latex', cls: 'latex', dtx: 'latex', // ── SQL ── sql: 'sql', pgsql: 'sql', psql: 'sql', mysql: 'sql', mssql: 'sql', // ── Shells ── // NOTE: no `.shell` extension key (unused); the `shell` grammar stays // available via `fish`, aliases and the language picker. sh: 'bash', bash: 'bash', zsh: 'bash', ksh: 'bash', mksh: 'bash', fish: 'shell', yash: 'bash', ps1: 'powershell', psm1: 'powershell', psd1: 'powershell', bat: 'dos', cmd: 'dos', // ── Systems ── c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', cxx: 'cpp', 'c++': 'cpp', hh: 'cpp', hpp: 'cpp', hxx: 'cpp', hcc: 'cpp', ino: 'cpp', pde: 'cpp', cs: 'csharp', csx: 'csharp', cake: 'csharp', rs: 'rust', go: 'go', swift: 'swift', kt: 'kotlin', kts: 'kotlin', java: 'java', jsp: 'java', scala: 'scala', sc: 'scala', groovy: 'groovy', gvy: 'groovy', gradle: 'groovy', dart: 'dart', lua: 'lua', // NOTE: no `.perl` / `.t` / `.mat` / `.app` / `.gpg` / `.wasm` keys on // purpose (unused, ambiguous or binary extensions). pl: 'perl', pm: 'perl', pod: 'perl', php: 'php', phtml: 'php-template', r: 'r', jl: 'julia', rb: 'ruby', gemspec: 'ruby', rake: 'ruby', rbw: 'ruby', // ── Functional / misc ── hs: 'haskell', lhs: 'haskell', ex: 'elixir', exs: 'elixir', eex: 'elixir', heex: 'elixir', leex: 'elixir', erl: 'erlang', hrl: 'erlang', escript: 'erlang', clj: 'clojure', cljs: 'clojure', cljc: 'clojure', edn: 'clojure', ml: 'ocaml', mli: 'ocaml', fs: 'fsharp', fsi: 'fsharp', fsx: 'fsharp', fsscript: 'fsharp', nim: 'nim', nims: 'nim', d: 'd', di: 'd', m: 'objectivec', mm: 'objectivec', vb: 'vbnet', vbs: 'vbnet', bas: 'vbnet', pas: 'delphi', pp: 'delphi', dpr: 'delphi', // ── HDL / asm / legacy ── vhd: 'vhdl', vhdl: 'vhdl', v: 'verilog', vh: 'verilog', sv: 'verilog', svh: 'verilog', asm: 'x86asm', s: 'x86asm', nasm: 'x86asm', inc: 'x86asm', ada: 'ada', ads: 'ada', adb: 'ada', f: 'fortran', for: 'fortran', f90: 'fortran', f95: 'fortran', f03: 'fortran', f08: 'fortran', cob: 'plaintext', cbl: 'plaintext', cpy: 'plaintext', // ── Build / IaC / containers ── dockerfile: 'dockerfile', tf: 'plaintext', tfvars: 'plaintext', hcl: 'plaintext', cmake: 'cmake', makefile: 'makefile', mk: 'makefile', mak: 'makefile', ninja: 'ini', gql: 'graphql', graphql: 'graphql', graphqls: 'graphql', proto: 'protobuf', thrift: 'thrift', vim: 'vim', vimrc: 'vim', gvimrc: 'vim', awk: 'awk', gcode: 'gcode', gco: 'gcode', nc: 'gcode', ngc: 'gcode', // ── Web servers / protocols ── http: 'http', rest: 'http', nginx: 'nginx', apache: 'apache', htaccess: 'apache', wat: 'wasm', // ── Diffs / misc text ── diff: 'diff', patch: 'diff', rej: 'diff', log: 'plaintext', txt: 'plaintext', text: 'plaintext', csv: 'plaintext', tsv: 'plaintext', tab: 'plaintext', lock: 'plaintext', gitignore: 'plaintext', gitattributes: 'plaintext', gitkeep: 'plaintext', dockerignore: 'plaintext', npmignore: 'plaintext', bib: 'plaintext', bbl: 'plaintext', nfo: 'plaintext', asc: 'plaintext', }; /* Exact basenames (lowercase) → language, checked BEFORE the extension (Dockerfile / Makefile / go.mod … often carry no usable extension). */ var FILE_TO_LANG = { 'dockerfile': 'dockerfile', 'containerfile': 'dockerfile', 'docker-compose.yml': 'yaml', 'docker-compose.yaml': 'yaml', 'compose.yml': 'yaml', 'compose.yaml': 'yaml', 'makefile': 'makefile', 'gnumakefile': 'makefile', 'cmakelists.txt': 'cmake', 'go.mod': 'go', 'go.sum': 'plaintext', 'go.work': 'go', 'cargo.toml': 'ini', 'cargo.lock': 'plaintext', 'package.json': 'json', 'package-lock.json': 'json', 'composer.json': 'json', 'tsconfig.json': 'json', 'nginx.conf': 'nginx', '.bashrc': 'bash', '.bash_profile': 'bash', '.bash_login': 'bash', '.bash_logout': 'bash', '.zshrc': 'bash', '.zshenv': 'bash', '.profile': 'bash', '.shrc': 'bash', '.kshrc': 'bash', '.vimrc': 'vim', '.gvimrc': 'vim', 'rakefile': 'ruby', 'gemfile': 'ruby', 'vagrantfile': 'ruby', 'podfile': 'ruby', 'fastfile': 'ruby', 'dangerfile': 'ruby', 'brewfile': 'ruby', 'berksfile': 'ruby', 'build.gradle': 'groovy', 'settings.gradle': 'groovy', 'build.gradle.kts': 'kotlin', 'settings.gradle.kts': 'kotlin', '.gitconfig': 'ini', '.gitmodules': 'ini', '.editorconfig': 'ini', '.htaccess': 'apache', '.viminfo': 'vim', }; /* Aliases users may type (markdown fences, old block labels, Prism ids…) → canonical highlight.js id. */ var ALIAS_TO_LANG = { 'py': 'python', 'python3': 'python', 'gyp': 'python', 'js': 'javascript', 'jsx': 'javascript', 'mjs': 'javascript', 'cjs': 'javascript', 'node': 'javascript', 'ts': 'typescript', 'tsx': 'typescript', 'html': 'xml', 'htm': 'xml', 'xhtml': 'xml', 'svg': 'xml', 'markup': 'xml', 'vue': 'xml', 'svelte': 'xml', 'yml': 'yaml', 'yaml': 'yaml', 'md': 'markdown', 'mkd': 'markdown', 'sh': 'bash', 'shell': 'shell', 'zsh': 'bash', 'console': 'shell', 'ps1': 'powershell', 'ps': 'powershell', 'powershell': 'powershell', 'bat': 'dos', 'cmd': 'dos', 'batch': 'dos', 'shell-session': 'shell', 'shellsession': 'shell', 'c++': 'cpp', 'cc': 'cpp', 'hpp': 'cpp', 'h': 'c', 'c#': 'csharp', 'csharp': 'csharp', 'cs': 'csharp', 'kt': 'kotlin', 'kts': 'kotlin', 'rb': 'ruby', 'rs': 'rust', 'golang': 'go', 'pl': 'perl', 'pm': 'perl', 'm': 'objectivec', 'mm': 'objectivec', 'objc': 'objectivec', 'r': 'r', 'jl': 'julia', 'toml': 'ini', 'ini': 'ini', 'cfg': 'ini', 'conf': 'ini', 'env': 'ini', 'properties': 'ini', 'tex': 'latex', 'latex': 'latex', 'docker': 'dockerfile', 'make': 'makefile', 'patch': 'diff', 'text': 'plaintext', 'plain': 'plaintext', 'plain text': 'plaintext', 'plaintext': 'plaintext', 'txt': 'plaintext', 'log': 'plaintext', 'none': 'plaintext', '': 'plaintext', }; /* Language id → short human label for the code-block badge / selector. */ var LANG_LABELS = { python: 'Python', javascript: 'JavaScript', typescript: 'TypeScript', xml: 'HTML/XML', css: 'CSS', scss: 'SCSS', less: 'Less', json: 'JSON', yaml: 'YAML', ini: 'INI/Config', markdown: 'Markdown', latex: 'LaTeX', sql: 'SQL', bash: 'Bash', shell: 'Shell', powershell: 'PowerShell', dos: 'Batch', c: 'C', cpp: 'C++', csharp: 'C#', rust: 'Rust', go: 'Go', swift: 'Swift', kotlin: 'Kotlin', java: 'Java', scala: 'Scala', groovy: 'Groovy', dart: 'Dart', lua: 'Lua', perl: 'Perl', php: 'PHP', 'php-template': 'PHP', r: 'R', julia: 'Julia', ruby: 'Ruby', haskell: 'Haskell', elixir: 'Elixir', erlang: 'Erlang', clojure: 'Clojure', ocaml: 'OCaml', fsharp: 'F#', nim: 'Nim', d: 'D', objectivec: 'Objective-C', matlab: 'MATLAB', vbnet: 'VB.NET', delphi: 'Delphi', vhdl: 'VHDL', verilog: 'Verilog', x86asm: 'ASM', armasm: 'ARM ASM', ada: 'Ada', fortran: 'Fortran', dockerfile: 'Docker', cmake: 'CMake', makefile: 'Makefile', graphql: 'GraphQL', protobuf: 'Protobuf', thrift: 'Thrift', vim: 'Vim', awk: 'AWK', gcode: 'G-code', http: 'HTTP', nginx: 'Nginx', apache: 'Apache', wasm: 'WASM', diff: 'Diff', plaintext: 'Plain Text', }; /** Languages offered in the code-block language picker (id order). */ var PICKER_LANGS = [ 'plaintext', 'python', 'javascript', 'typescript', 'xml', 'css', 'scss', 'json', 'yaml', 'ini', 'markdown', 'sql', 'bash', 'shell', 'powershell', 'dos', 'c', 'cpp', 'csharp', 'rust', 'go', 'swift', 'kotlin', 'java', 'scala', 'groovy', 'dart', 'lua', 'perl', 'php', 'r', 'julia', 'ruby', 'haskell', 'elixir', 'erlang', 'clojure', 'ocaml', 'fsharp', 'nim', 'd', 'objectivec', 'dockerfile', 'cmake', 'makefile', 'graphql', 'protobuf', 'latex', 'vim', 'http', 'nginx', 'diff', ]; function hasHljs() { return typeof window.hljs !== 'undefined' && !!window.hljs.getLanguage; } function escHtml(s) { return String(s == null ? '' : s) .replace(/&/g, '&') .replace(//g, '>'); } /** Normalize any user label / extension / Prism id to a bundled hljs id. */ function normalizeLang(lang) { var l = String(lang == null ? '' : lang).trim().toLowerCase(); if (!l || l === 'plain text' || l === 'none') return 'plaintext'; if (ALIAS_TO_LANG[l]) return ALIAS_TO_LANG[l]; if (!hasHljs()) return l || 'plaintext'; try { if (window.hljs.getLanguage(l)) return l; } catch (e) { /* ignore → plaintext */ } return 'plaintext'; } /** Best highlight.js id for a file path (basename map, then extension). */ function langForFile(path) { var name = String(path || '').replace(/\\/g, '/').split('/').pop().toLowerCase(); if (!name) return 'plaintext'; if (FILE_TO_LANG[name]) return FILE_TO_LANG[name]; // dotfiles like ".bashrc" without another dot are covered above; a // trailing ".xyz" is the extension otherwise. var dot = name.lastIndexOf('.'); if (dot < 0) return 'plaintext'; var ext = name.slice(dot + 1); if (FILE_TO_LANG['.' + name]) return FILE_TO_LANG['.' + name]; if (EXT_TO_LANG[ext]) return EXT_TO_LANG[ext]; return 'plaintext'; } function labelFor(lang) { var n = normalizeLang(lang); return LANG_LABELS[n] || n; } /** Highlight `text` → HTML string (escaped when hljs/lang missing). */ function highlightText(text, lang) { var src = String(text == null ? '' : text); var n = normalizeLang(lang); if (n === 'plaintext' || !hasHljs()) return escHtml(src); try { return window.hljs.highlight(src, { language: n }).value; } catch (e) { return escHtml(src); } } /** * Highlight a element in place. The element's TEXT is preserved * (highlight.js reads textContent), so this is safe for contenteditable * code blocks: sync() re-reads text, spans are transparent. * Re-highlighting is skipped when neither language nor text changed * (cheap length+prefix fingerprint) — e.g. repeated editor renders. */ function highlightBlock(el, lang) { if (!el) return; var n = normalizeLang(lang || el.getAttribute('data-hljs-lang') || langClassOf(el)); el.setAttribute('data-hljs-lang', n); if (n !== 'plaintext') { el.classList.add('hljs'); el.classList.add('language-' + n); } if (!hasHljs() || n === 'plaintext') return; try { var fp = fingerprint(el); if (el.dataset && el.dataset.hljsFp === fp) return; // unchanged window.hljs.highlightElement(el); if (el.dataset) el.dataset.hljsFp = fp; } catch (e) { /* keep plain text */ } } function langClassOf(el) { if (!el || !el.className) return ''; var m = /(?:^|\s)language-([\w-]+)/.exec(el.className); return m ? m[1] : ''; } function fingerprint(el) { var t = el.textContent || ''; return (el.getAttribute('data-hljs-lang') || '') + '|' + t.length + ':' + t.slice(0, 64); } /** * Highlight every `pre code` viewer block under `root`: * `[data-hljs]` (explicit, e.g. editor code blocks) and * `code[class*=language-]` (Gitea viewer, exports, public pages). * Editable code blocks are included — their text round-trips losslessly. */ function highlightAllIn(root) { var scope = root || document; if (!hasHljs() || !scope.querySelectorAll) return 0; var count = 0; try { var nodes = scope.querySelectorAll( 'pre code[data-hljs], pre code[data-hljs-lang], pre code[class*="language-"]' ); for (var i = 0; i < nodes.length; i++) { var el = nodes[i]; var lang = el.getAttribute('data-hljs-lang') || langClassOf(el) || 'plaintext'; var n = normalizeLang(lang); if (n === 'plaintext') continue; el.setAttribute('data-hljs-lang', n); el.classList.add('hljs'); var fp = n + '|' + (el.textContent || '').length + ':' + (el.textContent || '').slice(0, 64); if (el.dataset && el.dataset.hljsFp === fp) continue; window.hljs.highlightElement(el); if (el.dataset) el.dataset.hljsFp = fp; count++; } } catch (e) { /* never break rendering */ } return count; } window.FDHighlight = { EXT_TO_LANG: EXT_TO_LANG, FILE_TO_LANG: FILE_TO_LANG, PICKER_LANGS: PICKER_LANGS, normalizeLang: normalizeLang, langForFile: langForFile, labelFor: labelFor, highlightText: highlightText, highlightBlock: highlightBlock, highlightAllIn: highlightAllIn, hasHljs: hasHljs, }; })();