Add heading IDs, subscript, and superscript to markdown rendering (#21)

markdown-to-jsx has no plugin hook for new inline/block syntax, but it
does correctly parse ordinary links and exposes a slugify callback for
heading anchors -- both get reused the same way this app's own
@mention/#room-reference highlighting already works: ~sub~/^sup^ are
rewritten to a link before compiling (the "URL" is just a carrier for
meaning the parser was never told about), then re-rendered as
<sub>/<sup> instead of an anchor; a heading's {#custom-id} suffix is
stripped from its own text before compiling, and slugify substitutes
the requested id for the auto-generated one.

Applied everywhere markdown renders (chat messages, file previews, the
Help page), not just chat -- MARKDOWN_OPTIONS became a per-render
createMarkdownOptions() since slugify needs each render's own heading
ids.

Definition lists deliberately left unsupported -- no block-level
equivalent to the link-trick exists, and faking one would mean either
reopening the disableParsingRawHTML XSS mitigation or unreliably
misusing blockquote syntax. Documented on the issue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ksmithandClaude Sonnet 5 committed 2026-08-28 18:53:38 -06:00
1 parent 51d0092bd3
commit 89d609f584
3 files changed
+126 -21

No files matched your search

+12 -4
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import Markdown from 'markdown-to-jsx'
import { Link } from 'react-router-dom'
import { MARKDOWN_OPTIONS } from '../components/MessageContent'
import { createMarkdownOptions, preprocessMarkdown } from '../components/MessageContent'
import { TopBar } from '../components/TopBar'
import './HelpPage.css'
@@ -13,6 +13,12 @@ const GUIDE_URL = '/USER_GUIDE.md'
export function HelpPage() {
const [content, setContent] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
// #21: heading-id/sub/superscript support -- the user guide is exactly
// the kind of document that benefits from an explicit anchor override.
const markdownPreview = useMemo(
() => (content !== null ? preprocessMarkdown(content) : null),
[content],
)
useEffect(() => {
let cancelled = false
@@ -44,9 +50,11 @@ export function HelpPage() {
</div>
{error && <p className="admin-error">{error}</p>}
{!error && content === null && <p className="help-loading">Loading…</p>}
{!error && content !== null && (
{!error && markdownPreview && (
<div className="message-text help-content">
<Markdown options={MARKDOWN_OPTIONS}>{content}</Markdown>
<Markdown options={createMarkdownOptions(markdownPreview.headingIds)}>
{markdownPreview.text}
</Markdown>
</div>
)}
</div>