fix(viewer): pretty print fallback tokenizer quand highlight.js ne produit aucun token (txt/log/plaintext) (#89)
This commit is contained in:
+3
-1
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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("<script>"), "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("<img"), "img escaped");
|
||||
|
||||
console.log("✅ All pretty-print tests passed successfully!");
|
||||
|
||||
Reference in New Issue
Block a user