From: Javier Sagredo Date: Thu, 7 May 2026 01:16:37 +0000 (+0200) Subject: Render Haddock markup in the side panel X-Git-Url: https://git.sagredo.dev/?a=commitdiff_plain;h=e844a180d639d865d8bf15624c7fb85dbe1aaabb;p=classgraph.git Render Haddock markup in the side panel Replace the plain-text rendering of class / instance / family / fam-instance docstrings with a small markup-aware renderer that covers the common Haddock constructs: * @code@ → inline * 'Foo' / 'Foo.bar' / 'Mod.Foo.bar' → Foo[…] (only when the content has identifier shape; English contractions like "don't" are left alone) * "Module.Name" → Module.Name * __bold__ → * /italic/ → (with surrounding-punct check so URLs and Haskell operators like /= aren't mangled) * [text](url) → * `> code` bird-foot blocks →

No new dependencies — six regex passes in a couple of dozen lines
of JS. Anything we don't recognise renders as plain text (visible
@ / quotes), which is mildly ugly but never wrong. CSS gives
inline code an amber tag styling, code blocks a soft-amber box.

Co-Authored-By: Claude Opus 4.7 (1M context) 
---

diff --git a/data/viewer.css b/data/viewer.css
index b10c21e..953de09 100644
--- a/data/viewer.css
+++ b/data/viewer.css
@@ -567,6 +567,32 @@ ol.placeholder-reasons li {
 }
 #selected dd.haddock p { margin: 0 0 8px; }
 #selected dd.haddock p:last-child { margin-bottom: 0; }
+#selected dd.haddock code {
+  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
+  font-size: 12px;
+  background: #fef3c7;
+  padding: 0 4px;
+  border-radius: 3px;
+  color: #78350f;
+}
+#selected dd.haddock pre {
+  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
+  font-size: 12px;
+  background: #fef3c7;
+  border: 1px solid #fde68a;
+  padding: 6px 8px;
+  border-radius: 4px;
+  margin: 4px 0 8px;
+  overflow-x: auto;
+  color: #1f2937;
+}
+#selected dd.haddock pre:last-child { margin-bottom: 0; }
+#selected dd.haddock a {
+  color: #1d4ed8;
+  text-decoration: underline;
+}
+#selected dd.haddock strong { font-weight: 600; color: #1f2937; }
+#selected dd.haddock em { font-style: italic; }
 #selected a.src-link {
   color: #1d4ed8;
   text-decoration: none;
diff --git a/data/viewer.js b/data/viewer.js
index 5e74342..d745020 100644
--- a/data/viewer.js
+++ b/data/viewer.js
@@ -1734,18 +1734,86 @@
     return `
${escape(text)}
`; } - // Render a Haddock comment as a simple block of paragraphs. - // Empty strings, plain whitespace, and missing values all collapse to "". - // We do NOT try to interpret Haddock markup (links, code blocks, etc.) — - // we just preserve paragraph breaks and escape HTML. + // Render a Haddock comment with light markup support. We don't run + // Haddock or Pandoc — just a few regex passes that catch the four or + // five constructs that show up in the wild. Anything we don't + // recognise renders as the original text (visible quotes / @-signs + // and all), which is mildly ugly but never wrong. + // + // Order matters: code blocks first (their content is verbatim), then + // markdown links (URLs may contain @), then inline @code@, then the + // various emphasis forms. Identifier and module quotes are last so + // their patterns don't trigger inside other constructs. function renderDocSection(doc) { if (!doc) return ''; const text = String(doc).trim(); if (text === '') return ''; - const paras = text.split(/\n\s*\n/).map(p => `

${escape(p.trim())}

`).join(''); + const paras = text.split(/\n\s*\n/).map(p => { + const trimmed = p.replace(/\s+$/, ''); + if (isHaddockCodeBlock(trimmed)) return renderHaddockCodeBlock(trimmed); + return '

' + renderHaddockInline(trimmed.trim()) + '

'; + }).join(''); return `
Documentation
${paras}
`; } + // Detect a code-block paragraph: every non-blank line starts with `>` + // (Haddock's bird-foot syntax). Indentation-based code blocks would + // need lookahead across paragraphs and aren't worth the complexity + // for how rarely they appear. + function isHaddockCodeBlock(p) { + const lines = p.split('\n').filter(l => l.trim() !== ''); + if (lines.length === 0) return false; + return lines.every(l => /^\s*>/.test(l)); + } + + function renderHaddockCodeBlock(p) { + const code = p.split('\n') + .map(l => l.replace(/^\s*>\s?/, '')) + .join('\n'); + return '
' + escape(code) + '
'; + } + + // Apply Haddock inline markup transformations to a single paragraph. + // Operates on raw (unescaped) text and produces escaped HTML. + function renderHaddockInline(text) { + // Escape HTML first; markup passes operate on the escaped string, + // which is safe because none of our regexes match `<` etc. + let s = escape(text); + + // Markdown link: [text](url). Run before @code@ so @ inside URLs + // doesn't get swallowed. + s = s.replace(/\[([^\]\n]+?)\]\(([^)\n]+?)\)/g, + (_m, label, url) => `${label}`); + + // Inline code: @text@. Non-greedy, no newlines or @ inside. + s = s.replace(/@([^@\n]+?)@/g, '$1'); + + // Module reference: "Module.Name" — only when the content looks + // like a module path (capitalised dotted segments). Plain quoted + // English isn't touched. Note `escape` turned " into `"`. + s = s.replace(/"([A-Z][\w']*(?:\.[A-Z][\w']*)*)"/g, + '$1'); + + // Identifier link: 'Foo' / 'Foo.bar' / 'Foo.Bar.baz'. We escaped + // single-quotes to `'` already, so match that form. The + // leading apostrophe must not be inside a word, otherwise + // English contractions ("don't") would be mangled. + s = s.replace( + /(^|[^A-Za-z0-9])'([A-Z][\w]*(?:\.[A-Za-z][\w]*)*)'(?=[^A-Za-z0-9]|$)/g, + (_m, pre, ident) => pre + '' + ident + ''); + + // Bold: __text__. + s = s.replace(/__([^_\n]+?)__/g, '$1'); + + // Italic: /text/. Surrounded by whitespace/punctuation so URLs + // (`https://…`) and Haskell operators (`/=`) aren't touched. + s = s.replace( + /(^|[\s(\[])\/([^\/\n]+?)\/(?=[\s.,;:!?)\]]|$)/g, + (_m, pre, body) => pre + '' + body + ''); + + return s; + } + function renderClassPanel(c) { const cid = qid(c.ciName); const tvs = c.ciTyVars