Files
flowdeck/static/js/code-highlight.js
T
bruno 0218d8f5e5
FlowDeck CI / lint (push) Successful in 1m37s
FlowDeck CI / test (push) Failing after 24m19s
FlowDeck CI / docker (push) Skipped
fix: /local-workspace — chemin du header + clic sur un dossier du sidebar (v7.69.9)
Corrige deux régressions de /local-workspace :

- le chemin du header restait bloqué sur « Home / <workspace> » quel que
  soit le dossier affiché : la route rend désormais breadcrumb_items
  (Home / <workspace> / <dossier> / <sous-dossier>, niveaux cliquables,
  collapse « … » au-delà de 4) et la navigation sans rechargement recalcule
  le chemin via l'event flowdeck:breadcrumb-changed ;
- le clic sur un dossier du sidebar affichait TOUS les composants à la
  fois : Alpine.data('wsInitData') retournait le même objet singleton, le
  2e montage (navigation partielle) levait « Cannot redefine property:
  \ » et initTree abandonnait, laissant tout le contenu au state
  brut. La factory retourne désormais une enveloppe fraîche par montage
  qui délègue à l'état réactif partagé. #lw-config est aussi relu à chaque
  exécution (le 2e montage gardait le folder_id du 1er chargement).

Inclus également le travail en cours de l'arbre : Library (colonnes Last
visited/Source, ordre d'en-tête, favoris à icônes Workspace), Meeting
Notes (bloc, CSS, routes, docs), coloration de code hljs, badges
favori/publié dans l'arbre local-workspace, docs (DATA_MODEL,
architectures) et tests associés.
2026-10-09 16:58:04 -04:00

349 lines
15 KiB
JavaScript

/* ═══════════════════════════════════════════════════════════════════
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
/** 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 <code> 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,
};
})();