]> Repositorios git - classgraph.git/commitdiff
Render Haddock markup in the side panel
authorJavier Sagredo <[email protected]>
Thu, 7 May 2026 01:16:37 +0000 (03:16 +0200)
committerJavier Sagredo <[email protected]>
Thu, 7 May 2026 01:16:37 +0000 (03:16 +0200)
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 <code>
* 'Foo' / 'Foo.bar' / 'Mod.Foo.bar' → <code>Foo[…]</code>
  (only when the content has identifier shape; English contractions
  like "don't" are left alone)
* "Module.Name" → <code>Module.Name</code>
* __bold__ → <strong>
* /italic/ → <em> (with surrounding-punct check so URLs and Haskell
  operators like /= aren't mangled)
* [text](url) → <a href>
* `> code` bird-foot blocks → <pre>

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) <[email protected]>
data/viewer.css
data/viewer.js

index b10c21ee469cd197f2c0a8dbc0677f8281627890..953de09d35b0d6995e9c28a42cd0d2a58c2048c3 100644 (file)
@@ -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;
index 5e74342012de7188744b4dbea126ddce364c52ab..d745020669083aa3ba5cbd7aeb7b11109f9cc9df 100644 (file)
     return `<dd><a class="src-link" href="${escapeAttr(url)}" title="Open in your editor">${escape(text)}</a></dd>`;
   }
 
-  // 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 => `<p>${escape(p.trim())}</p>`).join('');
+    const paras = text.split(/\n\s*\n/).map(p => {
+      const trimmed = p.replace(/\s+$/, '');
+      if (isHaddockCodeBlock(trimmed)) return renderHaddockCodeBlock(trimmed);
+      return '<p>' + renderHaddockInline(trimmed.trim()) + '</p>';
+    }).join('');
     return `<dt>Documentation</dt><dd class="haddock">${paras}</dd>`;
   }
 
+  // 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 '<pre>' + escape(code) + '</pre>';
+  }
+
+  // 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 `&lt;` 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) => `<a href="${url}">${label}</a>`);
+
+    // Inline code: @text@. Non-greedy, no newlines or @ inside.
+    s = s.replace(/@([^@\n]+?)@/g, '<code>$1</code>');
+
+    // 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 `&quot;`.
+    s = s.replace(/&quot;([A-Z][\w']*(?:\.[A-Z][\w']*)*)&quot;/g,
+                  '<code>$1</code>');
+
+    // Identifier link: 'Foo' / 'Foo.bar' / 'Foo.Bar.baz'. We escaped
+    // single-quotes to `&#39;` 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])&#39;([A-Z][\w]*(?:\.[A-Za-z][\w]*)*)&#39;(?=[^A-Za-z0-9]|$)/g,
+      (_m, pre, ident) => pre + '<code>' + ident + '</code>');
+
+    // Bold: __text__.
+    s = s.replace(/__([^_\n]+?)__/g, '<strong>$1</strong>');
+
+    // 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 + '<em>' + body + '</em>');
+
+    return s;
+  }
+
   function renderClassPanel(c) {
     const cid = qid(c.ciName);
     const tvs = c.ciTyVars