}
#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;
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 `<` 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 `"`.
+ s = s.replace(/"([A-Z][\w']*(?:\.[A-Z][\w']*)*)"/g,
+ '<code>$1</code>');
+
+ // 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 + '<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