fix(viewer): pretty print fallback tokenizer quand highlight.js ne produit aucun token (txt/log/plaintext) (#89)
CI / lint (push) Successful in 1m18s
CI / security (push) Successful in 56s
CI / test (push) Successful in 2m57s
CI / build (push) Successful in 48s
CI / e2e (push) Successful in 10m47s

This commit is contained in:
2026-09-14 19:43:50 -04:00
parent d3f7a04411
commit bb0c8e7731
4 changed files with 100 additions and 4 deletions
+54 -1
View File
@@ -467,6 +467,58 @@ function buildPrettyHtml(data, raw) {
return `<div class="pretty-view"><div class="pp-gutter" aria-hidden="true">${gutter}</div><pre class="pp-pre"><code class="language-${lang}">${escapeHtml(text)}</code></pre></div>`;
}
// 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 ? `<span class="${cls}">${escapeHtml(m[0])}</span>` : 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;
+25
View File
@@ -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,