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 `
${gutter}
${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("