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
+3 -1
View File
@@ -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
+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,
+18 -2
View File
@@ -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("<script>aler
assert.ok(!xss.includes("<script>"), "no raw script tag injected");
assert.ok(xss.includes("&lt;script&gt;"), "script tag escaped");
console.log("Testing fallbackTokenize (log levels, strings, comments)...");
const fb = fallbackTokenize('2026-09-14 ERROR failed to open "/var/db"\n# note simple\nvalue=42');
assert.ok(fb.includes("pp-err"), "ERROR level colored");
assert.ok(fb.includes("hljs-string"), "quoted string colored");
assert.ok(fb.includes("hljs-comment"), "comment colored");
assert.ok(fb.includes("hljs-number"), "number colored");
assert.ok(fb.includes("hljs-attr"), "key= colored");
console.log("Testing fallbackTokenize (no html injection)...");
const fbXss = fallbackTokenize('bad <img src=x onerror="alert(1)">');
assert.ok(!/<img/.test(fbXss), "no raw img tag");
assert.ok(fbXss.includes("&lt;img"), "img escaped");
console.log("✅ All pretty-print tests passed successfully!");