feat(viewer): mode pretty print avec coloration syntaxique et numeros de ligne pour fichiers texte (#89)
CI / lint (push) Successful in 1m17s
CI / security (push) Successful in 56s
CI / test (push) Successful in 2m25s
CI / build (push) Successful in 48s
CI / e2e (push) Successful in 10m35s

This commit is contained in:
2026-09-14 19:12:43 -04:00
parent 96bb1cbcae
commit d3f7a04411
6 changed files with 170 additions and 0 deletions
+51
View File
@@ -446,6 +446,27 @@ async function renderBacklinksPanel(vault, path, container) {
}
}
// ── Pretty print (mode lecture enrichi pour fichiers texte non-markdown) ──
// Detecte le langage depuis le HTML deja rendu par le backend (classe
// language-xxx), formate le JSON, puis genere un bloc avec gouttiere de
// numeros de ligne. La coloration est appliquee ensuite via highlight.js.
function detectPrettyLang(data) {
if (data.is_json || data.extension === ".json") return "json";
const m = /class="language-([\w-]+)/.exec(data.html || "");
return m ? m[1] : "plaintext";
}
function buildPrettyHtml(data, raw) {
let text = raw;
const lang = detectPrettyLang(data);
if (lang === "json") {
try { text = JSON.stringify(JSON.parse(raw), null, 2); } catch (_) { /* JSON invalide: on affiche brut */ }
}
const lines = text.split("\n");
const gutter = lines.map((_, i) => `<span>${i + 1}</span>`).join("");
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>`;
}
export function renderFile(data) {
const area = getContentArea();
@@ -774,10 +795,40 @@ export function renderFile(data) {
}
});
// Pretty print — mode lecture enrichi (numeros de ligne + coloration)
// pour les fichiers texte non-markdown (code, JSON, configs, logs...)
let prettyBtn = null;
const isTextViewerFile = !data.is_markdown && !data.is_csv && !data.unsupported && !data.is_pdf && !data.is_image && !data.is_excalidraw;
if (isTextViewerFile) {
prettyBtn = el("button", { class: "btn-action", title: t("viewer.pretty_title") || "Mode formaté avec couleurs et numéros de ligne" }, [icon("sparkles", 14), document.createTextNode(t("viewer.pretty") || "Pretty")]);
prettyBtn.addEventListener("click", async () => {
const on = !prettyBtn.classList.contains("active");
if (on) {
try {
if (!state.cachedRawSource) {
const rawUrl = `/api/file/${encodeURIComponent(data.vault)}/raw?path=${encodeURIComponent(data.path)}`;
const rawData = await api(rawUrl);
state.cachedRawSource = rawData.raw;
}
} catch (_) {
state.cachedRawSource = mdDiv.textContent || "";
}
mdDiv.innerHTML = buildPrettyHtml(data, state.cachedRawSource);
safeHighlight(mdDiv.querySelector("code"));
prettyBtn.classList.add("active");
} else {
mdDiv.innerHTML = data.html;
mdDiv.querySelectorAll("pre code").forEach((block) => safeHighlight(block));
prettyBtn.classList.remove("active");
}
});
}
const fileActions = [copyBtn, sourceBtn];
if (!data.is_image) fileActions.push(mdBtn);
if (data.is_markdown) fileActions.push(pdfBtn, exportWrap, forgeBtn, tocBtn);
if (!data.unsupported) fileActions.push(editBtn);
if (prettyBtn) fileActions.push(prettyBtn);
fileActions.push(openNewWindowBtn, shareBtn, bookmarkBtn);
// Assemble
+2
View File
@@ -1687,6 +1687,8 @@
"viewer.download": "Download",
"viewer.download_md": "Download as .md",
"viewer.download_file": "Download file",
"viewer.pretty": "Pretty",
"viewer.pretty_title": "Formatted mode: colors and line numbers",
"viewer.download_pdf": "Download as PDF",
"viewer.edit": "Edit",
"viewer.error": "Loading error",
+2
View File
@@ -1687,6 +1687,8 @@
"viewer.download": "Télécharger",
"viewer.download_md": "Télécharger en .md",
"viewer.download_file": "Télécharger le fichier",
"viewer.pretty": "Pretty",
"viewer.pretty_title": "Mode formaté : couleurs et numéros de ligne",
"viewer.download_pdf": "Télécharger en PDF",
"viewer.edit": "Éditer",
"viewer.error": "Erreur de chargement",
+41
View File
@@ -2052,6 +2052,47 @@ select {
overflow-y: auto;
}
/* --- Pretty print (mode lecture fichiers texte non-markdown) --- */
.pretty-view {
display: flex;
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
max-height: 78vh;
}
.pretty-view .pp-gutter {
flex: 0 0 auto;
display: flex;
flex-direction: column;
padding: 16px 10px 16px 14px;
text-align: right;
font-family: "JetBrains Mono", monospace;
font-size: 0.8rem;
line-height: 1.55;
color: var(--text-muted);
opacity: 0.65;
user-select: none;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
overflow: hidden;
}
.pretty-view .pp-pre {
flex: 1 1 auto;
margin: 0 !important;
border: none !important;
border-radius: 0 !important;
background: transparent !important;
padding: 16px !important;
overflow: auto;
}
.pretty-view .pp-pre code {
font-family: "JetBrains Mono", monospace;
font-size: 0.85rem;
line-height: 1.55;
white-space: pre;
}
/* --- Markdown Rendered Content --- */
.md-content h1,
.md-content h2,