From bb0c8e773103536fdd25d21bc356235195203d8b Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Mon, 14 Sep 2026 19:43:50 -0400 Subject: [PATCH] fix(viewer): pretty print fallback tokenizer quand highlight.js ne produit aucun token (txt/log/plaintext) (#89) --- CHANGELOG.md | 4 ++- frontend/js/viewer.js | 55 +++++++++++++++++++++++++++++++++- frontend/style.css | 25 ++++++++++++++++ tests/frontend/pretty.test.mjs | 20 +++++++++++-- 4 files changed, 100 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb2f9e0..5dd8a1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,9 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Fiche : [docs/features/drag-and-drop-ai.md](./docs/features/drag-and-drop-ai.md). - **#89 Pretty print fichiers texte** — bouton « Pretty » dans le mode lecture des fichiers non-markdown (code, JSON, configs, logs...) : formatage JSON, numéros de ligne avec gouttière - et coloration syntaxique highlight.js alignée sur le thème clair/sombre. + et coloration syntaxique highlight.js alignée sur le thème clair/sombre, avec colorateur + générique de secours (chaînes, commentaires, nombres, niveaux de log) quand highlight.js + ne reconnaît pas le langage (txt, log, plaintext). - **#88 Assistant IA : contexte applicatif** — l'assistant en mode **Général** reçoit l'état vivant de l'interface (`app_context`) et connaît désormais les documents ouverts, le répertoire et le vault courants, la recherche en cours diff --git a/frontend/js/viewer.js b/frontend/js/viewer.js index e05833c..197334e 100644 --- a/frontend/js/viewer.js +++ b/frontend/js/viewer.js @@ -467,6 +467,58 @@ function buildPrettyHtml(data, raw) { return `
${escapeHtml(text)}
`; } +// Colorateur générique de secours: utilisé quand highlight.js ne produit +// aucun token (langage plaintext — .txt/.log —, langue absente du bundle, +// ou CDN hljs indisponible). Réutilise les classes hljs existantes (le +// thème CDN colore string/comment/number/attr) + classes .pp-* maison +// pour les niveaux de log qui n'ont pas d'équivalent sémantique hljs. +const PP_TOKEN_RE = /("[^"\n]*"?|'[^'\n]*'?|`[^`\n]*`?)|(^\s*(?:#|\/\/|;)\s.*$|\s+(?:#|\/\/)\s.*$)|\b(\d+(?:\.\d+)?)\b|\b(ERROR|FATAL|CRITICAL|SEVERE|WARN(?:ING)?|INFO|DEBUG|TRACE|NOTICE)\b|^(\s*[\w.\-\/ ]+)=(?=\S)|\b(true|false|null|None|TRUE|FALSE|NULL)\b/gm; + +function ppClassFor(m) { + if (m[1] !== undefined) return "hljs-string"; + if (m[2] !== undefined) return "hljs-comment"; + if (m[3] !== undefined) return "hljs-number"; + if (m[4] !== undefined) { + const lvl = m[4]; + if (/ERROR|FATAL|CRITICAL|SEVERE/.test(lvl)) return "pp-level pp-err"; + if (/WARN/.test(lvl)) return "pp-level pp-warn"; + if (/INFO|NOTICE/.test(lvl)) return "pp-level pp-info"; + return "pp-level pp-debug"; + } + if (m[5] !== undefined) return "hljs-attr"; + if (m[6] !== undefined) return "hljs-literal"; + return null; +} + +function fallbackTokenize(text) { + let out = ""; + let last = 0; + PP_TOKEN_RE.lastIndex = 0; + let m; + while ((m = PP_TOKEN_RE.exec(text)) !== null) { + if (m.index > last) out += escapeHtml(text.slice(last, m.index)); + const cls = ppClassFor(m); + out += cls ? `${escapeHtml(m[0])}` : escapeHtml(m[0]); + last = m.index + (m[0].length || 1); + if (m[0].length === 0) PP_TOKEN_RE.lastIndex++; // garde anti-boucle infinie + } + out += escapeHtml(text.slice(last)); + return out; +} + +// Applique la coloration: hljs si le langage est connu et que des tokens +// sortent, sinon fallback regex générique. +function applyPrettyHighlight(codeEl, lang, text) { + if (typeof hljs !== "undefined" && lang !== "plaintext") { + try { hljs.highlightElement(codeEl); } catch (_) { /* CDN pas charge */ } + } + const tokens = codeEl.querySelectorAll('[class^="hljs-"]').length; + if (!tokens && text.trim()) { + codeEl.innerHTML = fallbackTokenize(text); + codeEl.classList.add("hljs"); + } +} + export function renderFile(data) { const area = getContentArea(); @@ -814,7 +866,8 @@ export function renderFile(data) { state.cachedRawSource = mdDiv.textContent || ""; } mdDiv.innerHTML = buildPrettyHtml(data, state.cachedRawSource); - safeHighlight(mdDiv.querySelector("code")); + const codeEl = mdDiv.querySelector("code"); + applyPrettyHighlight(codeEl, detectPrettyLang(data), state.cachedRawSource); prettyBtn.classList.add("active"); } else { mdDiv.innerHTML = data.html; diff --git a/frontend/style.css b/frontend/style.css index fbeae7c..a88ac45 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -2092,6 +2092,31 @@ select { line-height: 1.55; white-space: pre; } +/* Niveaux de log colorises par le fallback pretty-print (pas d'equivalent hljs) */ +.pretty-view .pp-level { + font-weight: 700; +} +.pretty-view .pp-err { + color: #f87171; +} +.pretty-view .pp-warn { + color: #fbbf24; +} +.pretty-view .pp-info { + color: #38bdf8; +} +.pretty-view .pp-debug { + color: var(--text-muted); +} +[data-theme="light"] .pretty-view .pp-err { + color: #dc2626; +} +[data-theme="light"] .pretty-view .pp-warn { + color: #b45309; +} +[data-theme="light"] .pretty-view .pp-info { + color: #0369a1; +} /* --- Markdown Rendered Content --- */ .md-content h1, diff --git a/tests/frontend/pretty.test.mjs b/tests/frontend/pretty.test.mjs index b58a97c..9abdbb9 100644 --- a/tests/frontend/pretty.test.mjs +++ b/tests/frontend/pretty.test.mjs @@ -40,8 +40,11 @@ function escapeHtml(s) { const sandbox = { escapeHtml }; vm.createContext(sandbox); -vm.runInContext(`${extractFn("detectPrettyLang")}\n${extractFn("buildPrettyHtml")}\nglobalThis.detectPrettyLang = detectPrettyLang; globalThis.buildPrettyHtml = buildPrettyHtml;`, sandbox); -const { detectPrettyLang, buildPrettyHtml } = sandbox; +vm.runInContext( + `${extractFn("detectPrettyLang")}\n${extractFn("buildPrettyHtml")}\n${src.slice(src.indexOf("const PP_TOKEN_RE"), src.indexOf("function applyPrettyHighlight"))}\nglobalThis.fallbackTokenize = fallbackTokenize;`, + sandbox, +); +const { detectPrettyLang, buildPrettyHtml, fallbackTokenize } = sandbox; console.log("Testing detectPrettyLang..."); assert.equal(detectPrettyLang({ is_json: true }), "json"); @@ -68,4 +71,17 @@ const xss = buildPrettyHtml({ extension: ".py", html: "" }, 'print("