Arcology Engine

Arcology HTML Renderer

Contents

Introduction

This document is the source of truth for the Arcology HTML renderer — a string-builder-based converter that turns a parsed org-mode AST (OrgSection OrgChunk OrgInlineElem from orgmode-kmp) into semantic HTML strings.

The renderer mirrors the traversal structure of the Compose renderer but outputs HTML instead of Compose composables. It uses no template engine — Phase 7 will add Mustache for templating and layout inheritance. For Phase 1, we produce clean semantic HTML: headings, paragraphs, lists, source blocks, quotes, inline markup, and links.

Design

  • No dependencies: pure Kotlin string building, works in commonMain

  • Skips metadata: OrgKeywordLine, OrgCommentLine, property/logbook drawers are not rendered

  • Link rewriting: =...= links are resolved to route paths via a LinkResolver interface. Unresolvable links render as =/404?node=<id>= with a unpublished CSS class.

  • HTML escaping: all text content is HTML-escaped to prevent injection

  • Raw HTML passthrough: #+begin_html blocks are emitted as-is

  • Rich source blocks: #+begin_src blocks render as =<figure class="src-block">= with a <figcaption> listing the language, #+name:, and every :header-arg for human readers, plus matching =data-= attributes for machine consumption. The =<pre><code class="language-X">= child is preserved so the self-hosted highlight.js (see templates.org) can highlight it client-side. - Noweb anchors*: a source block carrying a #+name: (from the preceding #+NAME: keyword) or a :noweb-ref header arg gets a unique =id="src-<ref>"= anchor. The renderer accumulates a ref → [anchors] map across the render pass and emits it as an embedded JSON =<script class="noweb-ref-map">= at the end of each public render call. A highlight.js plugin (see arcology-src-blocks.js) reads this map and rewrites <<ref>> directives in code bodies into links to those anchors — multiple matches render as <<ref[1][2]…>>.

Link Resolver

The renderer needs to resolve =...= links to URLs. This interface decouples rendering from the route table.

Attachments extend the contract: a file: or attachment: link target that matches an indexed attachment resolves to an AttachmentRef — the URL of the small (or verbatim) variant for the =src=/=href=, plus the HTMX fragment URL of the large variant for progressive enhancement. Resolvers that don't support attachments keep the default null.

HTML Escaping

kotlin#+name: html-escape
private fun htmlEscape(text: String): String {
    return text
        .replace("&", "&amp;")
        .replace("<", "&lt;")
        .replace(">", "&gt;")
        .replace("\"", "&quot;")
        .replace("'", "&#39;")
}

Inline Markup Post-Processing

The orgmode-kmp parser does not produce AST nodes for inline markup like =bold=, /italic/, =\=verbatim\==, \~code\~, or =...= inline HTML — the relevant parsers in OrgInlineElem.kt are commented out, so these constructs fall through to OrgInlineElem.Text as literal strings. The web renderer post-processes Text nodes with regex to emit the corresponding HTML tags, mirroring the approach in the Android app's InlineMarkupPatterns.kt.

=...= inline HTML passes through raw (no escaping), matching org-mode semantics and the existing OrgInlineElem.InlineQuote.HTML renderer behavior. All other text segments are HTML-escaped; only the markup delimiters and their captured content are transformed.

kotlin#+name: inline-markup
private val HTML_INLINE_PATTERN = Regex("""@@html:([^@]+)@@""")
private val BOLD_PATTERN = Regex("""(?<=^|[\s({"'])\*([^*\n]+)\*(?=$|[\s)}.,:;!?"'])""")
private val ITALIC_PATTERN = Regex("""(?<=^|[\s({"'])/([^/\n]+)/(?=$|[\s)}.,:;!?"'])""")
private val VERBATIM_PATTERN = Regex("""(?<=^|[\s({"'])=([^=\n]+)=(?=$|[\s)}.,:;!?"'])""")
private val CODE_PATTERN = Regex("""(?<=^|[\s({"'])~([^~\n]+)~(?=$|[\s)}.,:;!?"'])""")

/**
 * Apply inline markup transformations to a raw text string.
 *
 * Order matters: @@html:...@@ is applied first so that HTML it emits is not
 * misinterpreted by the later markup patterns (e.g. a literal '*' inside an
 * HTML fragment would be wrongly bolded). The markup patterns operate on
 * non-HTML segments only.
 *
 * Returns a string that may contain raw HTML (from @@html:) and HTML tags
 * (from markup), with all other text HTML-escaped. Callers must NOT re-escape
 * the result.
 */
private fun applyInlineMarkup(text: String): String {
    // 1. Extract @@html:...@@ segments and replace with sentinels so the
    //    markup regexes don't touch their contents. The sentinel is wrapped in
    //    spaces so the markup lookbehind/ahead boundaries (which accept \s)
    //    still fire for org text adjacent to an @@html: bookend.
    val htmlSegments = mutableListOf<String>()
    var working = HTML_INLINE_PATTERN.replace(text) { mr ->
        htmlSegments.add(mr.groupValues[1])
        " \u0000HTML${htmlSegments.size - 1}\u0000 "
    }

    // 2. HTML-escape the rest, then apply markup patterns to the escaped text.
    //    The markup delimiters (*, /, =, ~) are not HTML-special so they survive
    //    escaping intact. The captured content is escaped along with everything
    //    else, which is correct — *foo & bar* should yield <strong>foo &amp; bar</strong>.
    working = htmlEscape(working)
    working = BOLD_PATTERN.replace(working) { "<strong>${it.groupValues[1]}</strong>" }
    working = ITALIC_PATTERN.replace(working) { "<em>${it.groupValues[1]}</em>" }
    working = VERBATIM_PATTERN.replace(working) { "<code>${it.groupValues[1]}</code>" }
    working = CODE_PATTERN.replace(working) { "<code>${it.groupValues[1]}</code>" }

    // 3. Restore the @@html:...@@ segments raw (un-escaped). Collapse the
    //    sentinel's surrounding spaces only when they were inserted by us
    //    (i.e. they're directly adjacent to a sentinel marker).
    for ((i, html) in htmlSegments.withIndex()) {
        working = working.replace(" \u0000HTML$i\u0000 ", html)
    }
    return working
}

Tests: inline markup regex

kotlin#+name: renderer-test-prelude
package computer.whatthefuck.arcology.publishing

import xyz.lepisma.orgmode.OrgDocument
import xyz.lepisma.orgmode.OrgChunk
import xyz.lepisma.orgmode.OrgBlock
import xyz.lepisma.orgmode.OrgHeading
import xyz.lepisma.orgmode.OrgHeadingLevel
import xyz.lepisma.orgmode.OrgInlineElem
import xyz.lepisma.orgmode.OrgLine
import xyz.lepisma.orgmode.OrgList
import xyz.lepisma.orgmode.OrgPreface
import xyz.lepisma.orgmode.OrgProperties
import xyz.lepisma.orgmode.OrgSection
import xyz.lepisma.orgmode.OrgTags
import xyz.lepisma.orgmode.lexer.Token
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertFalse

class OrgHtmlRendererTest {
    private val renderer = OrgHtmlRenderer()

    private fun text(s: String): OrgInlineElem.Text {
        return OrgInlineElem.Text(text = s, tokens = emptyList())
    }

    private fun paragraph(s: String): OrgChunk.OrgParagraph {
        return OrgChunk.OrgParagraph(items = listOf(text(s)), tokens = emptyList())
    }

    private fun section(level: Int, title: String, body: List<OrgChunk>): OrgSection {
        return OrgSection(
            heading = OrgHeading(
                title = OrgLine(items = listOf(text(title)), tokens = emptyList()),
                level = OrgHeadingLevel(level = level, tokens = emptyList()),
                tags = null, todoState = null, priority = null,
                planningInfo = null, properties = null,
                tokens = emptyList()
            ),
            body = body,
            tokens = emptyList()
        )
    }

    private fun sectionWithTags(level: Int, title: String, body: List<OrgChunk>, tags: List<String>): OrgSection {
        return OrgSection(
            heading = OrgHeading(
                title = OrgLine(items = listOf(text(title)), tokens = emptyList()),
                level = OrgHeadingLevel(level = level, tokens = emptyList()),
                tags = OrgTags(tags = tags, tokens = emptyList()),
                todoState = null, priority = null,
                planningInfo = null, properties = null,
                tokens = emptyList()
            ),
            body = body,
            tokens = emptyList()
        )
    }

    private fun orgDocument(preface: OrgPreface, sections: List<OrgSection>): OrgDocument {
        return OrgDocument(
            preamble = xyz.lepisma.orgmode.OrgPreamble(
                title = OrgLine(emptyList(), tokens = emptyList()),
                tokens = emptyList()
            ),
            preface = preface,
            content = sections,
            tokens = emptyList()
        )
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `regex applies bold markup to Text nodes`() {
        val section = section(level = 1, title = "Test", body = listOf(
            paragraph("say *hello world* loudly")
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<strong>hello world</strong>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `merges adjacent Text nodes before applying bold markup`() {
        // Mirrors real lexer output: *bold* becomes three Text tokens.
        val section = section(level = 1, title = "Test", body = listOf(
            OrgChunk.OrgParagraph(
                items = listOf(
                    text("say "),
                    text("*"),
                    text("hello"),
                    text("*"),
                    text(" loudly")
                ),
                tokens = emptyList()
            )
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<strong>hello</strong>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `regex applies italic markup to Text nodes`() {
        val section = section(level = 1, title = "Test", body = listOf(
            paragraph("an /important/ note")
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<em>important</em>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `regex applies verbatim markup to Text nodes`() {
        val section = section(level = 1, title = "Test", body = listOf(
            paragraph("the =literal= thing")
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<code>literal</code>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `regex applies code markup to Text nodes`() {
        val section = section(level = 1, title = "Test", body = listOf(
            paragraph("use ~fmt~ module")
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<code>fmt</code>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `regex markup escapes ampersand inside bold content`() {
        val section = section(level = 1, title = "Test", body = listOf(
            paragraph("see *foo & bar* there")
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<strong>foo &amp; bar</strong>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `regex passes @@html inline HTML through raw`() {
        val section = section(level = 1, title = "Test", body = listOf(
            paragraph("@@html:<b>@@bold text@@html:</b>@@")
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<b>bold text</b>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `merges adjacent Text nodes for @@html inline HTML`() {
        // Mirrors real lexer output: @ chars are single-char Text tokens.
        val section = section(level = 1, title = "Test", body = listOf(
            OrgChunk.OrgParagraph(
                items = listOf(
                    text("@"),
                    text("@"),
                    text("html:"),
                    text("<b>"),
                    text("@"),
                    text("@"),
                    text("bold text"),
                    text("@"),
                    text("@"),
                    text("html:"),
                    text("</b>"),
                    text("@"),
                    text("@")
                ),
                tokens = emptyList()
            )
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<b>bold text</b>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `regex applies markup to text between @@html bookends`() {
        // In org-mode, @@html:<b>@@ and @@html:</b>@@ are bookends; the text
        // between is normal org text and DOES get inline markup applied.
        val section = section(level = 1, title = "Test", body = listOf(
            paragraph("@@html:<b>@@*bold* text@@html:</b>@@")
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<b><strong>bold</strong> text</b>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `escapes text with no markup intact`() {
        val section = section(level = 1, title = "Test", body = listOf(
            paragraph("plain text & <stuff>")
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("plain text &amp; &lt;stuff&gt;"))
    }

Inline Element Renderer

Converts OrgInlineElem subtypes to HTML strings. Recursively renders nested content inside markup elements (Bold, Italic, etc.). Footnotes are accumulated on the renderer instance as they are encountered inline; each footnote ref emits a numbered <sup> link, and the accumulated definitions are emitted as =<aside class="sidenote">= blocks by renderSidenotes() at the end of the page.

Text-node merging

kotlin#+name: inline-renderer-merge
private fun OrgHtmlRenderer.renderInlineElems(elems: List<OrgInlineElem>): String {
    // Merge consecutive Text nodes into one before rendering. The orgmode-kmp
    // lexer emits emphasis delimiters (*, /, =, ~) and @@html: characters as
    // separate single-char Text tokens, so a paragraph like "*bold*" becomes
    // [Text("*"), Text("bold"), Text("*")]. Without merging, the inline markup
    // regex (which matches *content* within a single string) would never fire.
    // Merging reconstructs the contiguous text so the regex can see the full
    // markup pattern. Non-Text elements (links, footnotes, clozes) break the
    // merge — they are rendered to HTML and spliced in at their position.
    val sb = StringBuilder()
    val textBuffer = StringBuilder()
    for (elem in elems) {
        if (elem is OrgInlineElem.Text) {
            textBuffer.append(elem.text)
        } else {
            if (textBuffer.isNotEmpty()) {
                sb.append(applyInlineMarkup(textBuffer.toString()))
                textBuffer.clear()
            }
            sb.append(renderInlineElem(elem))
        }
    }
    if (textBuffer.isNotEmpty()) {
        sb.append(applyInlineMarkup(textBuffer.toString()))
    }
    return sb.toString()
}

Element dispatch

kotlin#+name: inline-renderer-dispatch
private fun OrgHtmlRenderer.renderInlineElem(elem: OrgInlineElem): String {
    return when (elem) {
        is OrgInlineElem.Text -> applyInlineMarkup(elem.text)
        is OrgInlineElem.Bold -> "<strong>${renderInlineElems(elem.content)}</strong>"
        is OrgInlineElem.Italic -> "<em>${renderInlineElems(elem.content)}</em>"
        is OrgInlineElem.Underline -> "<u>${renderInlineElems(elem.content)}</u>"
        is OrgInlineElem.StrikeThrough -> "<del>${renderInlineElems(elem.content)}</del>"
        is OrgInlineElem.Verbatim -> "<code>${renderInlineElems(elem.content)}</code>"
        is OrgInlineElem.Code -> "<code>${renderInlineElems(elem.content)}</code>"
        is OrgInlineElem.Link -> renderLink(elem)
        is OrgInlineElem.DTStamp -> "<time>${htmlEscape(elem.date.toString())}</time>"
        is OrgInlineElem.DTRange -> "<time>${htmlEscape(elem.start.date.toString())}–${htmlEscape(elem.end.date.toString())}</time>"
        is OrgInlineElem.HashTag -> "<span class=\"hashtag\">#${htmlEscape(elem.text)}</span>"
        is OrgInlineElem.HashMetric -> "<span class=\"metric\">#${htmlEscape(elem.metric)}(${htmlEscape(elem.value)})</span>"
        is OrgInlineElem.InlineMath -> "<span class=\"math\">${htmlEscape(elem.text)}</span>"
        is OrgInlineElem.InlineQuote -> when (elem.type) {
            OrgInlineElem.InlineQuoteType.HTML -> elem.text
            OrgInlineElem.InlineQuoteType.LATEX -> "<span class=\"latex\">${htmlEscape(elem.text)}</span>"
        }
        is OrgInlineElem.Citation -> "<cite>${htmlEscape(elem.citeString)}</cite>"
        is OrgInlineElem.Footnote -> renderFootnoteRef(elem)
        is OrgInlineElem.Cloze -> {
            val hintText = elem.hint ?: "…"
            "<span class=\"cloze\" data-cloze-id=\"${elem.id}\">[${htmlEscape(hintText)}]</span>"
        }
    }
}

file: and attachment: links first try the attachment resolver: image attachments render as <img> with the small crushed variant as src and an HTMX hx-get that swaps in the large variant on load; non-image attachments render as plain links to the verbatim copy. Unresolved file: links fall through to resolveFilePath (pass-through), preserving the pre-attachment behavior.

Footnote reference rendering

kotlin#+name: inline-renderer-footnote
private fun OrgHtmlRenderer.renderFootnoteRef(fn: OrgInlineElem.Footnote): String {
    val n = nextFootnoteNumber()
    footnotes.add(FootnoteDef(n, fn.key, renderInlineElems(fn.text.items)))
    val id = "fn-$n"
    return """<label for="$id" class="margin-toggle sidenote-number">⊕</label>""" +
        """<input type="checkbox" id="$id" class="margin-toggle"/>""" +
        """<sup class="sidenote-ref"><a href="#$id" id="${id}ref">$n</a></sup>"""
}

Tests: inline element rendering

kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `renders paragraph as p tag`() {
        val section = section(level = 1, title = "Test", body = listOf(
            paragraph("Hello world")
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<p>Hello world</p>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `escapes HTML in text`() {
        val section = section(level = 1, title = "Test", body = listOf(
            paragraph("<script>alert('xss')</script>")
        ))
        val html = renderer.renderSection(section)
        assertFalse(html.contains("<script>"))
        assertTrue(html.contains("&lt;script&gt;"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `renders bold text`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgChunk.OrgParagraph(
                items = listOf(
                    text("Hello "),
                    OrgInlineElem.Bold(content = listOf(text("world")), tokens = emptyList())
                ),
                tokens = emptyList()
            )
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<strong>world</strong>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `renders italic text`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgChunk.OrgParagraph(
                items = listOf(
                    OrgInlineElem.Italic(content = listOf(text("emphasized")), tokens = emptyList())
                ),
                tokens = emptyList()
            )
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<em>emphasized</em>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `renders code and verbatim`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgChunk.OrgParagraph(
                items = listOf(
                    OrgInlineElem.Code(content = listOf(text("code")), tokens = emptyList()),
                    OrgInlineElem.Verbatim(content = listOf(text("verb")), tokens = emptyList())
                ),
                tokens = emptyList()
            )
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<code>code</code>"))
        assertTrue(html.contains("<code>verb</code>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `resolves id links via LinkResolver`() {
        val resolver = object : LinkResolver {
            override fun resolveNodeId(nodeId: String): String? = "/resolved/path"
            override fun resolveFilePath(filePath: String): String? = null
        }
        val r = OrgHtmlRenderer(resolver)
        val section = section(level = 1, title = "Test", body = listOf(
            OrgChunk.OrgParagraph(
                items = listOf(
                    OrgInlineElem.Link(
                        type = "id",
                        target = "abc123",
                        title = listOf(text("link text")),
                        tokens = emptyList()
                    )
                ),
                tokens = emptyList()
            )
        ))
        val html = r.renderSection(section)
        assertTrue(html.contains("href=\"/resolved/path\""))
        assertTrue(html.contains("link text"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `renders http links as-is`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgChunk.OrgParagraph(
                items = listOf(
                    OrgInlineElem.Link(
                        type = "https",
                        target = "https://example.com",
                        title = listOf(text("example")),
                        tokens = emptyList()
                    )
                ),
                tokens = emptyList()
            )
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("href=\"https://example.com\""))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `image attachment link renders img with htmx swap`() {
        val resolver = object : LinkResolver {
            override fun resolveNodeId(nodeId: String): String? = null
            override fun resolveFilePath(filePath: String): String? = null
            override fun resolveAttachment(filePath: String): AttachmentRef? =
                AttachmentRef(
                    url = "/attachment/abc123-512.jpg",
                    largeHtmlUrl = "/arcology/attachment/abc123-2048.jpg/html",
                    isImage = true
                )
        }
        val r = OrgHtmlRenderer(resolver)
        val section = section(level = 1, title = "Test", body = listOf(
            OrgChunk.OrgParagraph(
                items = listOf(
                    OrgInlineElem.Link(
                        type = "file",
                        target = "./data/xx/photo.jpg",
                        title = listOf(text("A photo")),
                        tokens = emptyList()
                    )
                ),
                tokens = emptyList()
            )
        ))
        val html = r.renderSection(section)
        assertTrue(html.contains("<img src=\"/attachment/abc123-512.jpg\""))
        assertTrue(html.contains("alt=\"A photo\""))
        assertTrue(html.contains("loading=\"lazy\""))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `attachment link type renders img`() {
        val resolver = object : LinkResolver {
            override fun resolveNodeId(nodeId: String): String? = null
            override fun resolveFilePath(filePath: String): String? = null
            override fun resolveAttachment(filePath: String): AttachmentRef? =
                AttachmentRef(url = "/attachment/abc123-0.png", largeHtmlUrl = null, isImage = true)
        }
        val r = OrgHtmlRenderer(resolver)
        val section = section(level = 1, title = "Test", body = listOf(
            OrgChunk.OrgParagraph(
                items = listOf(
                    OrgInlineElem.Link(
                        type = "attachment",
                        target = "drawing.png",
                        title = null,
                        tokens = emptyList()
                    )
                ),
                tokens = emptyList()
            )
        ))
        val html = r.renderSection(section)
        // No link title: alt falls back to the link target's basename
        assertTrue(html.contains("<img src=\"/attachment/abc123-0.png\" alt=\"drawing.png\""))
        // Single variant: no htmx attributes
        assertFalse(html.contains("hx-get"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `non-image attachment renders plain link`() {
        val resolver = object : LinkResolver {
            override fun resolveNodeId(nodeId: String): String? = null
            override fun resolveFilePath(filePath: String): String? = null
            override fun resolveAttachment(filePath: String): AttachmentRef? =
                AttachmentRef(url = "/attachment/abc123-0.pdf", largeHtmlUrl = null, isImage = false)
        }
        val r = OrgHtmlRenderer(resolver)
        val section = section(level = 1, title = "Test", body = listOf(
            OrgChunk.OrgParagraph(
                items = listOf(
                    OrgInlineElem.Link(
                        type = "file",
                        target = "./report.pdf",
                        title = listOf(text("the report")),
                        tokens = emptyList()
                    )
                ),
                tokens = emptyList()
            )
        ))
        val html = r.renderSection(section)
        assertTrue(html.contains("<a href=\"/attachment/abc123-0.pdf\">the report</a>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `unresolved file link falls back to resolveFilePath`() {
        val resolver = object : LinkResolver {
            override fun resolveNodeId(nodeId: String): String? = null
            override fun resolveFilePath(filePath: String): String? = "/plain/${filePath}"
            override fun resolveAttachment(filePath: String): AttachmentRef? = null
        }
        val r = OrgHtmlRenderer(resolver)
        val section = section(level = 1, title = "Test", body = listOf(
            OrgChunk.OrgParagraph(
                items = listOf(
                    OrgInlineElem.Link(
                        type = "file",
                        target = "notes.txt",
                        title = null,
                        tokens = emptyList()
                    )
                ),
                tokens = emptyList()
            )
        ))
        val html = r.renderSection(section)
        assertTrue(html.contains("href=\"/plain/notes.txt\""))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `footnote ref emits numbered sup link and accumulates definition`() {
        val footnote = OrgInlineElem.Footnote(
            key = "1",
            text = OrgLine(items = listOf(text("note body")), tokens = emptyList()),
            tokens = emptyList()
        )
        val section = section(level = 1, title = "Doc", body = listOf(
            OrgChunk.OrgParagraph(items = listOf(text("see "), footnote), tokens = emptyList())
        ))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertTrue(html.contains("<sup class=\"sidenote-ref\">"))
        assertTrue(html.contains("<a href=\"#fn-1\" id=\"fn-1ref\">1</a>"))
        val sidenotes = r.renderSidenotes()
        assertTrue(sidenotes.contains("<aside class=\"sidenote\" id=\"fn-1ref\">"))
        assertTrue(sidenotes.contains("note body"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `multiple footnotes get incremental numbers`() {
        val fn1 = OrgInlineElem.Footnote(
            key = "1",
            text = OrgLine(items = listOf(text("first")), tokens = emptyList()),
            tokens = emptyList()
        )
        val fn2 = OrgInlineElem.Footnote(
            key = "2",
            text = OrgLine(items = listOf(text("second")), tokens = emptyList()),
            tokens = emptyList()
        )
        val section = section(level = 1, title = "Doc", body = listOf(
            OrgChunk.OrgParagraph(items = listOf(fn1, text(" "), fn2), tokens = emptyList())
        ))
        val r = OrgHtmlRenderer()
        r.renderSection(section)
        val sidenotes = r.renderSidenotes()
        assertTrue(sidenotes.contains("id=\"fn-1ref\">"))
        assertTrue(sidenotes.contains("id=\"fn-2ref\">"))
        assertTrue(sidenotes.contains("first"))
        assertTrue(sidenotes.contains("second"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `consumeFootnotes resets accumulator`() {
        val fn = OrgInlineElem.Footnote(
            key = "1",
            text = OrgLine(items = listOf(text("only")), tokens = emptyList()),
            tokens = emptyList()
        )
        val section = section(level = 1, title = "Doc", body = listOf(
            OrgChunk.OrgParagraph(items = listOf(fn), tokens = emptyList())
        ))
        val r = OrgHtmlRenderer()
        r.renderSection(section)
        assertEquals(1, r.consumeFootnotes().size)
        assertEquals(0, r.consumeFootnotes().size)
    }

Block Renderer

Converts OrgChunk subtypes to HTML. Dispatches paragraphs, blocks, lists, tables, and sections. While walking a chunk list, renderChunks remembers the most recent #+NAME: keyword line (OrgChunk.OrgKeywordLine with =keyword == "NAME"=) so the following OrgSourceBlock can carry its #+name: as both a visible caption entry and a unique HTML anchor. The NAME keyword itself is still suppressed from output (it is org metadata, not prose). Any other chunk type between the NAME and the source block clears the pending name so it is not mis-attributed.

Chunk dispatch

kotlin#+name: block-renderer-chunks
private fun OrgHtmlRenderer.renderChunks(chunks: List<OrgChunk>): String {
    val sb = StringBuilder()
    var pendingName: String? = null
    for (chunk in chunks) {
        if (chunk is OrgChunk.OrgKeywordLine) {
            // A NAME keyword arms the pending name for the next source block;
            // any other keyword clears it so a stale NAME doesn't leak past an
            // intervening non-NAME keyword.
            pendingName = if (chunk.keyword == "NAME" && chunk.value.isNotEmpty()) chunk.value else null
            sb.append(renderChunk(chunk, pendingName))
            continue
        }
        sb.append(renderChunk(chunk, pendingName))
        // The pending name is consumed by the first non-keyword chunk after it.
        pendingName = null
    }
    return sb.toString()
}

private fun OrgHtmlRenderer.renderChunk(chunk: OrgChunk, pendingName: String? = null): String {
    return when (chunk) {
        is OrgChunk.OrgParagraph -> "<p>${renderInlineElems(chunk.items)}</p>"
        is OrgChunk.OrgKeywordLine -> ""
        is OrgChunk.OrgCommentLine -> ""
        is OrgChunk.OrgHorizontalLine -> "<hr/>"
        is OrgSection -> renderSectionInternal(chunk)
        is OrgBlock.OrgSourceBlock -> renderSourceBlock(chunk, pendingName)
        is OrgBlock.OrgExampleBlock -> "<pre><code>${htmlEscape(chunk.text)}</code></pre>"
        is OrgBlock.OrgQuoteBlock -> "<blockquote>${renderChunks(chunk.body)}</blockquote>"
        is OrgBlock.OrgCenterBlock -> "<div class=\"center\">${renderChunks(chunk.body)}</div>"
        is OrgBlock.OrgHTMLBlock -> chunk.body
        is OrgBlock.OrgVerseBlock -> "<pre class=\"verse\">${htmlEscape(chunk.body)}</pre>"
        is OrgBlock.OrgLaTeXBlock -> "<div class=\"latex\">${htmlEscape(chunk.body)}</div>"
        is OrgBlock.OrgPageIntroBlock -> "<div class=\"page-intro\">${renderChunks(chunk.body)}</div>"
        is OrgBlock.OrgEditsBlock -> "<div class=\"edits\">${renderChunks(chunk.body)}</div>"
        is OrgBlock.OrgAsideBlock -> "<aside>${renderChunks(chunk.body)}</aside>"
        is OrgBlock.OrgVideoBlock -> "<div class=\"video\">${renderChunks(chunk.body)}</div>"
        is OrgBlock.OrgCommentBlock -> ""
        is OrgChunk.OrgTable -> renderTable(chunk)
        is OrgChunk.OrgLogbookDrawer -> ""
        is OrgChunk.OrgReviewDataDrawer -> ""
        is OrgList.OrgUnorderedList -> renderUnorderedList(chunk)
        is OrgList.OrgOrderedList -> renderOrderedList(chunk)
        else -> ""
    }
}

Source block rendering

kotlin#+name: block-renderer-source
/**
 * Regex matching org noweb-ref directives inside source block bodies. Must
 * match the same pattern used by the browser-side plugin in
 * [[file:templates.org][arcology-src-blocks.js]]: double angle brackets around
 * an identifier starting with a letter. Digits-only refs are not matched.
 */
private val NOWEB_REF_REGEX = Regex("""<<([A-Za-z][A-Za-z0-9_-]*)>>""")

/**
 * Render a source block as a <figure class="src-block"> with a visible <figcaption>
 * listing the language, #+name, and every :header-arg, plus matching data-*
 * attributes for machine consumption. The <pre><code class="language-X"> child is
 * preserved so highlight.js can highlight it.
 *
 * Anchor allocation: a block gets an id when it has a #+name (from the preceding
 * NAME keyword) OR a :noweb-ref header arg. The first occurrence of a ref uses
 * `src-<ref>`; later occurrences append `-2`, `-3`, ... All refs (name and
 * noweb-ref) are registered in [nowebRefs] so the page-level ref-map script can
 * link noweb-ref directives (the double-angle-bracket syntax) to these anchors.
 *
 * Backlink collection: if this block has an anchor (i.e. it is itself named),
 * its body is scanned for noweb-ref directives. Each unique ref found is
 * recorded in [nowebBacklinks] as `ref -> NowebBacklink(thisAnchor, thisName)`,
 * so the block defining `ref` can link back to this block as a consumer.
 */
private fun OrgHtmlRenderer.renderSourceBlock(block: OrgBlock.OrgSourceBlock, pendingName: String?): String {
    val name = pendingName ?: block.headerArgs["noweb-ref"]?.takeIf { it.isNotEmpty() }
    val nowebRef = block.headerArgs["noweb-ref"]?.takeIf { it.isNotEmpty() }
    val anchor = if (name != null) allocateAnchor(name) else null

    // Register every ref that should resolve to this block's anchor, and scan
    // this block's body for noweb-ref directives to collect backlinks.
    if (anchor != null && name != null) {
        if (nowebRef != null && nowebRef != name) registerNowebRef(nowebRef, anchor)
        registerNowebRef(name, anchor)

        // Collect backlinks: scan this block's body for noweb-ref directives.
        // For each unique ref found, record that *this* block consumes it, so
        // the defining block can link back here. Only named blocks with anchors
        // can be backlink targets (anonymous consumers have nothing to link to).
        val seenRefs = mutableSetOf<String>()
        for (match in NOWEB_REF_REGEX.findAll(block.body)) {
            val consumedRef = match.groupValues[1]
            if (seenRefs.add(consumedRef)) {
                nowebBacklinks.getOrPut(consumedRef) { mutableListOf() }
                    .add(NowebBacklink(anchor = anchor, label = name))
            }
        }
    }

    val sb = StringBuilder()
    sb.append("<figure class=\"src-block\"")
    if (anchor != null) sb.append(" id=\"${htmlEscape(anchor)}\"")
    if (block.language.isNotEmpty()) sb.append(" data-language=\"${htmlEscape(block.language)}\"")
    if (name != null) sb.append(" data-name=\"${htmlEscape(name)}\"")
    for ((k, v) in block.headerArgs) {
        sb.append(" data-${htmlEscape(dataAttrKey(k))}=\"${htmlEscape(v)}\"")
    }
    if (block.switches.isNotEmpty()) sb.append(" data-switches=\"${htmlEscape(block.switches.joinToString(" "))}\"")
    sb.append(">")

    sb.append(renderSourceBlockCaption(block, name))
    val langClass = if (block.language.isNotEmpty()) " class=\"language-${htmlEscape(block.language)}\"" else ""
    sb.append("<pre><code$langClass>${htmlEscape(block.body)}</code></pre>")
    sb.append("</figure>")
    return sb.toString()
}

private fun renderSourceBlockCaption(block: OrgBlock.OrgSourceBlock, name: String?): String {
    val sb = StringBuilder()
    sb.append("<figcaption class=\"src-block-meta\">")
    if (block.language.isNotEmpty()) {
        sb.append("<span class=\"src-lang\">${htmlEscape(block.language)}</span>")
    }
    if (name != null) {
        sb.append("<span class=\"src-arg src-name\">#+name: ${htmlEscape(name)}</span>")
    }
    for ((k, v) in block.headerArgs) {
        val display = if (v.isEmpty()) ":$k" else ":$k $v"
        sb.append("<span class=\"src-arg src-${htmlEscape(dataAttrKey(k))}\">${htmlEscape(display)}</span>")
    }
    if (block.switches.isNotEmpty()) {
        sb.append("<span class=\"src-arg src-switches\">${htmlEscape(block.switches.joinToString(" "))}</span>")
    }
    sb.append("</figcaption>")
    return sb.toString()
}

/**
 * Map an org header-arg key to a CSS/data-attribute-safe slug. Org keys are
 * already lower-ASCII (e.g. "tangle", "noweb-ref", "exports"); we only need to
 * replace non-[a-z0-9-] chars with dashes.
 */
private fun dataAttrKey(key: String): String {
    return key.lowercase().replace(Regex("[^a-z0-9-]+"), "-")
}
kotlin#+name: block-renderer-noweb
/**
 * Allocate a unique anchor id for a noweb ref. First use is `src-<ref>`;
 * subsequent uses are `src-<ref>-2`, `src-<ref>-3`, ... The renderer's
 * [nowebRefCounts] map tracks the running count per ref.
 */
private fun OrgHtmlRenderer.allocateAnchor(ref: String): String {
    val n = (nowebRefCounts[ref] ?: 0) + 1
    nowebRefCounts[ref] = n
    return if (n == 1) "src-${slugify(ref)}" else "src-${slugify(ref)}-$n"
}

private fun OrgHtmlRenderer.registerNowebRef(ref: String, anchor: String) {
    nowebRefs.getOrPut(ref) { mutableListOf() }.add(anchor)
}

/**
 * Emit the accumulated noweb ref → anchor map as a JSON <script> tag and clear
 * the accumulators. The script is appended to the rendered body so it survives
 * the HtmlCache disk cache (which stores htmlContent only) — the browser
 * always has the map available when it runs the highlight.js plugin.
 */
private fun OrgHtmlRenderer.renderNowebRefMap(): String {
    if (nowebRefs.isEmpty()) {
        nowebRefs.clear()
        nowebRefCounts.clear()
        return ""
    }
    val sb = StringBuilder()
    sb.append("<script type=\"application/json\" class=\"noweb-ref-map\">")
    sb.append("{")
    val entries = nowebRefs.entries.map { (ref, anchors) ->
        val anchorList = anchors.joinToString(",") { "\"${htmlEscape(it)}\"" }
        "\"${htmlEscape(ref)}\":[$anchorList]"
    }
    sb.append(entries.joinToString(","))
    sb.append("}</script>")
    nowebRefs.clear()
    nowebRefCounts.clear()
    return sb.toString()
}

/**
 * Emit the accumulated noweb backlink map as a JSON <script> tag and clear the
 * accumulator. The backlink map is `ref -> [{anchor, label}, ...]` where each
 * entry is a block that consumes `ref`. The browser-side plugin reads this to
 * append "embedded in" footers to the blocks that define each ref.
 *
 * Like the ref-map, this is embedded in the rendered body so it survives the
 * HtmlCache disk cache. Forward references work because the map is emitted at
 * the end of the render pass after all blocks have been scanned.
 */
private fun OrgHtmlRenderer.renderNowebBacklinkMap(): String {
    if (nowebBacklinks.isEmpty()) {
        nowebBacklinks.clear()
        return ""
    }
    val sb = StringBuilder()
    sb.append("<script type=\"application/json\" class=\"noweb-backlink-map\">")
    sb.append("{")
    val entries = nowebBacklinks.entries.map { (ref, backlinks) ->
        val linkList = backlinks.joinToString(",") { bl ->
            "{\"anchor\":\"${htmlEscape(bl.anchor)}\",\"label\":\"${htmlEscape(bl.label)}\"}"
        }
        "\"${htmlEscape(ref)}\":[$linkList]"
    }
    sb.append(entries.joinToString(","))
    sb.append("}</script>")
    nowebBacklinks.clear()
    return sb.toString()
}

Tables and lists

kotlin#+name: block-renderer-tables-lists
private fun OrgHtmlRenderer.renderTable(table: OrgChunk.OrgTable): String {
    val sb = StringBuilder()
    sb.append("<table>")
    table.header?.let { headerRow ->
        sb.append("<thead><tr>")
        for (cell in headerRow.cells) {
            sb.append("<th>${renderInlineElems(cell.items)}</th>")
        }
        sb.append("</tr></thead>")
    }
    sb.append("<tbody>")
    for (subtable in table.subtables) {
        for (row in subtable) {
            sb.append("<tr>")
            for (cell in row.cells) {
                sb.append("<td>${renderInlineElems(cell.items)}</td>")
            }
            sb.append("</tr>")
        }
    }
    sb.append("</tbody></table>")
    return sb.toString()
}

private fun OrgHtmlRenderer.renderUnorderedList(list: OrgList.OrgUnorderedList): String {
    val sb = StringBuilder()
    sb.append("<ul>")
    for (item in list.items) {
        sb.append("<li>${renderChunks(item.content)}</li>")
    }
    sb.append("</ul>")
    return sb.toString()
}

private fun OrgHtmlRenderer.renderOrderedList(list: OrgList.OrgOrderedList): String {
    val sb = StringBuilder()
    sb.append("<ol>")
    for (item in list.items) {
        sb.append("<li>${renderChunks(item.content)}</li>")
    }
    sb.append("</ol>")
    return sb.toString()
}

Tests: source block rendering

kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `renders source block with language class`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgBlock.OrgSourceBlock(
                language = "kotlin",
                switches = emptyList(),
                headerArgs = emptyMap(),
                body = "val x = 1",
                name = null,
                tokens = emptyList()
            )
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<pre><code class=\"language-kotlin\">val x = 1</code></pre>"))
        assertTrue(html.contains("<figure class=\"src-block\""))
        assertTrue(html.contains("data-language=\"kotlin\""))
        assertTrue(html.contains("<figcaption class=\"src-block-meta\">"))
        assertTrue(html.contains("<span class=\"src-lang\">kotlin</span>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `escapes source block content`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgBlock.OrgSourceBlock(
                language = "",
                switches = emptyList(),
                headerArgs = emptyMap(),
                body = "<b>not bold</b>",
                name = null,
                tokens = emptyList()
            )
        ))
        val html = renderer.renderSection(section)
        assertFalse(html.contains("<b>not bold</b>"))
        assertTrue(html.contains("&lt;b&gt;not bold&lt;/b&gt;"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `source block caption lists header args`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgBlock.OrgSourceBlock(
                language = "kotlin",
                switches = emptyList(),
                headerArgs = mapOf(
                    "tangle" to "../src/Foo.kt",
                    "noweb" to "yes",
                    "exports" to "code"
                ),
                body = "val x = 1",
                name = null,
                tokens = emptyList()
            )
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("data-tangle=\"../src/Foo.kt\""))
        assertTrue(html.contains("data-noweb=\"yes\""))
        assertTrue(html.contains("data-exports=\"code\""))
        assertTrue(html.contains("<span class=\"src-arg src-tangle\">:tangle ../src/Foo.kt</span>"))
        assertTrue(html.contains("<span class=\"src-arg src-noweb\">:noweb yes</span>"))
        assertTrue(html.contains("<span class=\"src-arg src-exports\">:exports code</span>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `source block with empty-value header arg renders bare key`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgBlock.OrgSourceBlock(
                language = "nix",
                switches = emptyList(),
                headerArgs = mapOf("tangle" to "", "noweb" to ""),
                body = "{ }",
                name = null,
                tokens = emptyList()
            )
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("data-tangle=\"\""))
        assertTrue(html.contains("<span class=\"src-arg src-tangle\">:tangle</span>"))
        assertTrue(html.contains("<span class=\"src-arg src-noweb\">:noweb</span>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `source block with switches emits data-switches`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgBlock.OrgSourceBlock(
                language = "python",
                switches = listOf("-n", "-r"),
                headerArgs = emptyMap(),
                body = "pass",
                name = null,
                tokens = emptyList()
            )
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("data-switches=\"-n -r\""))
        assertTrue(html.contains("<span class=\"src-arg src-switches\">-n -r</span>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `named source block gets anchor id`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgChunk.OrgKeywordLine(keyword = "NAME", value = "my-block", tokens = emptyList()),
            OrgBlock.OrgSourceBlock(
                language = "kotlin",
                switches = emptyList(),
                headerArgs = emptyMap(),
                body = "val x = 1",
                name = null,
                tokens = emptyList()
            )
        ))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertTrue(html.contains("id=\"src-my-block\""))
        assertTrue(html.contains("data-name=\"my-block\""))
        assertTrue(html.contains("<span class=\"src-arg src-name\">#+name: my-block</span>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `noweb-ref source block gets anchor id`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgBlock.OrgSourceBlock(
                language = "kotlin",
                switches = emptyList(),
                headerArgs = mapOf("noweb-ref" to "foo-bar"),
                body = "val x = 1",
                name = "foo-bar",
                tokens = emptyList()
            )
        ))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertTrue(html.contains("id=\"src-foo-bar\""))
        assertTrue(html.contains("data-name=\"foo-bar\""))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `duplicate noweb-refs get distinct anchors`() {
        val block = { ->
            OrgBlock.OrgSourceBlock(
                language = "kotlin",
                switches = emptyList(),
                headerArgs = mapOf("noweb-ref" to "shared"),
                body = "val x = 1",
                name = "shared",
                tokens = emptyList()
            )
        }
        val section = section(level = 1, title = "Test", body = listOf(block(), block(), block()))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertTrue(html.contains("id=\"src-shared\""))
        assertTrue(html.contains("id=\"src-shared-2\""))
        assertTrue(html.contains("id=\"src-shared-3\""))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `unnamed source block has no id`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgBlock.OrgSourceBlock(
                language = "kotlin",
                switches = emptyList(),
                headerArgs = emptyMap(),
                body = "val x = 1",
                name = null,
                tokens = emptyList()
            )
        ))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertFalse(html.contains("id=\"src-"))
        assertFalse(html.contains("data-name="))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `emits noweb ref map as json script`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgBlock.OrgSourceBlock(
                language = "kotlin",
                switches = emptyList(),
                headerArgs = mapOf("noweb-ref" to "foo"),
                body = "<" + "<foo>>",
                name = "foo",
                tokens = emptyList()
            )
        ))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertTrue(html.contains("<script type=\"application/json\" class=\"noweb-ref-map\">"))
        assertTrue(html.contains("\"foo\":[\"src-foo\"]"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `ref map includes multiple anchors for duplicate refs`() {
        val block = { ->
            OrgBlock.OrgSourceBlock(
                language = "kotlin",
                switches = emptyList(),
                headerArgs = mapOf("noweb-ref" to "dup"),
                body = "x",
                name = "dup",
                tokens = emptyList()
            )
        }
        val section = section(level = 1, title = "Test", body = listOf(block(), block()))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertTrue(html.contains("\"dup\":[\"src-dup\",\"src-dup-2\"]"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `ref map omitted when no named blocks`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgBlock.OrgSourceBlock(
                language = "kotlin",
                switches = emptyList(),
                headerArgs = emptyMap(),
                body = "val x = 1",
                name = null,
                tokens = emptyList()
            )
        ))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertFalse(html.contains("noweb-ref-map"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `ref map registers both name and noweb-ref when they differ`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgChunk.OrgKeywordLine(keyword = "NAME", value = "alpha", tokens = emptyList()),
            OrgBlock.OrgSourceBlock(
                language = "kotlin",
                switches = emptyList(),
                headerArgs = mapOf("noweb-ref" to "beta"),
                body = "val x = 1",
                name = "beta",
                tokens = emptyList()
            )
        ))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        // The anchor is derived from the #+name (alpha), and both alpha and beta
        // should map to it so <alpha> and <beta> both link here.
        assertTrue(html.contains("id=\"src-alpha\""))
        assertTrue(html.contains("\"alpha\":[\"src-alpha\"]"))
        assertTrue(html.contains("\"beta\":[\"src-alpha\"]"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `NAME keyword not consumed by intervening paragraph`() {
        // A NAME keyword followed by a paragraph (not a source block) should not
        // mis-attribute the name to a later source block.
        val section = section(level = 1, title = "Test", body = listOf(
            OrgChunk.OrgKeywordLine(keyword = "NAME", value = "orphan", tokens = emptyList()),
            paragraph("some prose"),
            OrgBlock.OrgSourceBlock(
                language = "kotlin",
                switches = emptyList(),
                headerArgs = emptyMap(),
                body = "val x = 1",
                name = null,
                tokens = emptyList()
            )
        ))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertFalse(html.contains("data-name=\"orphan\""))
        assertFalse(html.contains("id=\"src-orphan\""))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `backlink map records consumer of a noweb ref`() {
        // Block A defines `foo`; block B (named `bar`) consumes the foo ref.
        // The backlink map should map `foo -> [{anchor: "src-bar", label: "bar"}]`.
        val fooBlock = OrgBlock.OrgSourceBlock(
            language = "kotlin", switches = emptyList(),
            headerArgs = mapOf("noweb-ref" to "foo"),
            body = "val x = 1", name = "foo", tokens = emptyList()
        )
        val barBlock = OrgBlock.OrgSourceBlock(
            language = "kotlin", switches = emptyList(),
            headerArgs = mapOf("noweb-ref" to "bar"),
            body = "<" + "<foo>>", name = "bar", tokens = emptyList()
        )
        val section = section(level = 1, title = "Test", body = listOf(fooBlock, barBlock))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertTrue(html.contains("<script type=\"application/json\" class=\"noweb-backlink-map\">"))
        assertTrue(html.contains("\"foo\":[{\"anchor\":\"src-bar\",\"label\":\"bar\"}]"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `backlink map omitted when no named blocks consume refs`() {
        // An anonymous block (no name/noweb-ref) consuming the foo ref should not
        // produce a backlink entry (there's no anchor to link back to).
        val fooBlock = OrgBlock.OrgSourceBlock(
            language = "kotlin", switches = emptyList(),
            headerArgs = mapOf("noweb-ref" to "foo"),
            body = "val x = 1", name = "foo", tokens = emptyList()
        )
        val anonBlock = OrgBlock.OrgSourceBlock(
            language = "kotlin", switches = emptyList(),
            headerArgs = emptyMap(),
            body = "<" + "<foo>>", name = null, tokens = emptyList()
        )
        val section = section(level = 1, title = "Test", body = listOf(fooBlock, anonBlock))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertFalse(html.contains("noweb-backlink-map"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `backlink map deduplicates refs within a single block`() {
        // A block that references the foo ref three times should produce only one
        // backlink entry for `foo`.
        val fooBlock = OrgBlock.OrgSourceBlock(
            language = "kotlin", switches = emptyList(),
            headerArgs = mapOf("noweb-ref" to "foo"),
            body = "val x = 1", name = "foo", tokens = emptyList()
        )
        val consumerBlock = OrgBlock.OrgSourceBlock(
            language = "kotlin", switches = emptyList(),
            headerArgs = mapOf("noweb-ref" to "consumer"),
                body = "<" + "<foo>>\n<" + "<foo>>\n<" + "<foo>>", name = "consumer", tokens = emptyList()
        )
        val section = section(level = 1, title = "Test", body = listOf(fooBlock, consumerBlock))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        // Should contain exactly one backlink entry for foo
        assertTrue(html.contains("\"foo\":[{\"anchor\":\"src-consumer\",\"label\":\"consumer\"}]"))
        // Count occurrences of the anchor in the backlink map — should be 1
        val backlinkSection = html.substringAfter("noweb-backlink-map").substringBefore("</script>")
        assertEquals(1, backlinkSection.split("src-consumer").size - 1)
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `backlink map records multiple consumers of same ref`() {
        val fooBlock = OrgBlock.OrgSourceBlock(
            language = "kotlin", switches = emptyList(),
            headerArgs = mapOf("noweb-ref" to "foo"),
            body = "val x = 1", name = "foo", tokens = emptyList()
        )
        val consumerA = OrgBlock.OrgSourceBlock(
            language = "kotlin", switches = emptyList(),
            headerArgs = mapOf("noweb-ref" to "alpha"),
            body = "<" + "<foo>>", name = "alpha", tokens = emptyList()
        )
        val consumerB = OrgBlock.OrgSourceBlock(
            language = "kotlin", switches = emptyList(),
            headerArgs = mapOf("noweb-ref" to "beta"),
            body = "<" + "<foo>>", name = "beta", tokens = emptyList()
        )
        val section = section(level = 1, title = "Test", body = listOf(fooBlock, consumerA, consumerB))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertTrue(html.contains("\"foo\":[{\"anchor\":\"src-alpha\",\"label\":\"alpha\"},{\"anchor\":\"src-beta\",\"label\":\"beta\"}]"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `backlink map supports forward references`() {
        // Consumer appears BEFORE the defining block in document order.
        // The backlink map is emitted at end of render, so forward refs work.
        val consumer = OrgBlock.OrgSourceBlock(
            language = "kotlin", switches = emptyList(),
            headerArgs = mapOf("noweb-ref" to "consumer"),
            body = "<" + "<def>>", name = "consumer", tokens = emptyList()
        )
        val defBlock = OrgBlock.OrgSourceBlock(
            language = "kotlin", switches = emptyList(),
            headerArgs = mapOf("noweb-ref" to "def"),
            body = "val x = 1", name = "def", tokens = emptyList()
        )
        val section = section(level = 1, title = "Test", body = listOf(consumer, defBlock))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertTrue(html.contains("\"def\":[{\"anchor\":\"src-consumer\",\"label\":\"consumer\"}]"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `backlink map cleared after render`() {
        val fooBlock = OrgBlock.OrgSourceBlock(
            language = "kotlin", switches = emptyList(),
            headerArgs = mapOf("noweb-ref" to "foo"),
            body = "val x = 1", name = "foo", tokens = emptyList()
        )
        val consumer = OrgBlock.OrgSourceBlock(
            language = "kotlin", switches = emptyList(),
            headerArgs = mapOf("noweb-ref" to "consumer"),
            body = "<" + "<foo>>", name = "consumer", tokens = emptyList()
        )
        val section = section(level = 1, title = "Test", body = listOf(fooBlock, consumer))
        val r = OrgHtmlRenderer()
        r.renderSection(section)
        // After rendering, the backlink map should be cleared
        assertEquals(0, r.nowebBacklinks.size)
    }

Tests: other block types

kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `renders quote block`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgBlock.OrgQuoteBlock(
                body = listOf(paragraph("Quoted text")),
                tokens = emptyList()
            )
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<blockquote>"))
        assertTrue(html.contains("Quoted text"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `renders HTML block as raw passthrough`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgBlock.OrgHTMLBlock(
                body = "<div class=\"custom\">Raw HTML</div>",
                name = null,
                tokens = emptyList()
            )
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<div class=\"custom\">Raw HTML</div>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `skips keyword lines`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgChunk.OrgKeywordLine(keyword = "ARCOLOGY_KEY", value = "site/path", tokens = emptyList()),
            paragraph("Content")
        ))
        val html = renderer.renderSection(section)
        assertFalse(html.contains("ARCOLOGY_KEY"))
        assertTrue(html.contains("<p>Content</p>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `skips comment lines`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgChunk.OrgCommentLine(text = "This is a comment", tokens = emptyList()),
            paragraph("Content")
        ))
        val html = renderer.renderSection(section)
        assertFalse(html.contains("This is a comment"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `renders unordered list`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgList.OrgUnorderedList(
                markerStyle = xyz.lepisma.orgmode.OrgUnorderedListMarker.DASH,
                items = listOf(
                    OrgList.OrgListItem(content = listOf(paragraph("Item 1")), checkbox = null, tokens = emptyList()),
                    OrgList.OrgListItem(content = listOf(paragraph("Item 2")), checkbox = null, tokens = emptyList())
                ),
                tokens = emptyList()
            )
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<ul>"))
        assertTrue(html.contains("<li>"))
        assertTrue(html.contains("Item 1"))
        assertTrue(html.contains("Item 2"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `renders ordered list`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgList.OrgOrderedList(
                markerStyle = xyz.lepisma.orgmode.OrgOrderedListMarker.PERIOD,
                items = listOf(
                    OrgList.OrgListItem(content = listOf(paragraph("First")), checkbox = null, tokens = emptyList())
                ),
                tokens = emptyList()
            )
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<ol>"))
        assertTrue(html.contains("First"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `renders horizontal rule`() {
        val section = section(level = 1, title = "Test", body = listOf(
            OrgChunk.OrgHorizontalLine(tokens = emptyList())
        ))
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<hr/>"))
    }

Section Renderer

Renders a heading and its body. Heading levels map to =<h1>=–=<h6>=. Each rendered heading is also recorded on the renderer instance as a HeadingLink (level, title, anchor) for the sidebar TOC panel; the anchor is the heading's ID property if present, otherwise a slug derived from the heading text. Anchors are emitted as id attributes on the <hN> element so in-page =#anchor= links resolve.

When the renderer is constructed with a non-empty publishedNodeIds set, any sub-heading whose ID property is in that set gets =class="published-node"= on its <hN> element. This lets the CSS call out sub-headings that are themselves published nodes (each a separate page in the route table). The class is currently unstyled in arcology.css — the hook is available for future visual treatment.

Tag-Based Elision

A heading carrying any tag in the shared EXCLUDE_TAGS set (noexport NOEXPORT ARCHIVE) is elided from the rendered HTML entirely: no <hN> element, no body, no sub-tree recursion, and no entry in consumeHeadings(). This mirrors =ArcologyPublishingPlugin='s route-table filtering but applies at render time to every heading the renderer walks — including non-node sub-headings that bear no ARCOLOGY_KEY and so were never considered by the route-table filter. Without this, a published node's body would still emit the contents of its =:noexport:=-tagged children.

The elision is case-sensitive (:Archive:, the personal organization tag, is preserved) and is applied uniformly to renderSection, renderSections, and renderDocument since they all funnel through renderSectionInternal. The /arcology/node/{nodeId} localhost preview endpoint uses the same renderer, so previewing an elided node yields empty content — the preview shows what production would serve.

kotlin#+name: section-renderer
private fun OrgHtmlRenderer.renderSectionInternal(section: OrgSection): String {
    val tags = section.heading.tags?.tags ?: emptyList()
    if (tags.any { it in EXCLUDE_TAGS }) {
        return ""
    }
    val sb = StringBuilder()
    val heading = section.heading
    val level = heading.level.level.coerceAtMost(6)
    val todoState = heading.todoState
    val taskSpan = if (todoState != null) "<span class=\"task task-${todoState.text}\">${todoState.text}</span> " else ""
    val headingText = renderInlineElems(heading.title.items)
    val id = headingId(section)
    val anchor = if (id != null) id else slugify(headingText)
    headings.add(HeadingLink(level = level, title = headingText, anchor = anchor))
    val classAttr = if (id != null && id in publishedNodeIds) " class=\"published-node\"" else ""
    val idAttr = if (anchor.isNotEmpty()) " id=\"${htmlEscape(anchor)}\"" else ""
    sb.append("<h${level}${idAttr}${classAttr}>${taskSpan}${headingText}</h${level}>")
    // Inline backlinks and references cluster with the heading rather than the
    // sidebar. Only headings with an ID property (published nodes) get these
    // sections; the data is supplied by [[file:server.org][SidebarService]]
    // via the headingBacklinks and headingRefs constructor params.
    if (id != null) {
        sb.append(renderHeadingBacklinks(id))
        sb.append(renderHeadingRefs(id))
    }
    sb.append(renderChunks(section.body))
    return sb.toString()
}

/** Emit a <section class="heading-backlinks"> after a heading if it has incoming links. */
private fun OrgHtmlRenderer.renderHeadingBacklinks(nodeId: String): String {
    val links = headingBacklinks[nodeId] ?: return ""
    if (links.isEmpty()) return ""
    val sb = StringBuilder()
    sb.append("<section class=\"heading-backlinks\" data-node-id=\"${htmlEscape(nodeId)}\">")
    sb.append("<h4>Linked from</h4>")
    sb.append("<ul>")
    for (bl in links) {
        sb.append("<li><a href=\"${htmlEscape(bl.url)}\">${htmlEscape(bl.title)}</a></li>")
    }
    sb.append("</ul></section>")
    return sb.toString()
}

/** Emit a <section class="heading-refs"> after a heading if it has external references. */
private fun OrgHtmlRenderer.renderHeadingRefs(nodeId: String): String {
    val refs = headingRefs[nodeId] ?: return ""
    if (refs.isEmpty()) return ""
    val sb = StringBuilder()
    sb.append("<section class=\"heading-refs\" data-node-id=\"${htmlEscape(nodeId)}\">")
    sb.append("<h4>References</h4>")
    sb.append("<ul>")
    for (ref in refs) {
        sb.append("<li><a target=\"_blank\" href=\"${htmlEscape(ref)}\">${htmlEscape(ref)}</a></li>")
    }
    sb.append("</ul></section>")
    return sb.toString()
}

/** Extract the raw ID property of a heading, or null if absent. */
private fun OrgHtmlRenderer.headingId(section: OrgSection): String? {
    val idProp = section.heading.properties?.map?.get("ID")?.let { orgLine ->
        orgLine.items.filterIsInstance<OrgInlineElem.Text>()
            .joinToString("") { it.text }.trim()
    }
    return idProp?.takeIf { it.isNotEmpty() }
}

/** Derive a URL-safe anchor slug from heading text (HTML stripped, lowercased, non-alphanumerics → dashes). */
private fun OrgHtmlRenderer.slugify(headingText: String): String {
    val slug = headingText
        .replace(Regex("<[^>]+>"), "")
        .lowercase()
        .replace(Regex("[^a-z0-9]+"), "-")
        .trim('-')
    return slug.ifEmpty { "heading-${headings.size + 1}" }
}

Tests: heading rendering and anchors

kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `renders heading with correct level`() {
        val section = section(level = 3, title = "Subheading", body = emptyList())
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<h3 id=\"subheading\">Subheading</h3>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `clamps heading level to 6`() {
        val section = section(level = 10, title = "Deep", body = emptyList())
        val html = renderer.renderSection(section)
        assertTrue(html.contains("<h6 id=\"deep\">Deep</h6>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `heading anchor uses ID property when present`() {
        val section = OrgSection(
            heading = OrgHeading(
                title = OrgLine(items = listOf(text("Titled")), tokens = emptyList()),
                level = OrgHeadingLevel(level = 1, tokens = emptyList()),
                tags = null, todoState = null, priority = null,
                planningInfo = null,
                properties = OrgProperties(
                    map = mapOf("ID" to OrgLine(items = listOf(text("ABC-123")), tokens = emptyList())),
                    tokens = emptyList()
                ),
                tokens = emptyList()
            ),
            body = emptyList(),
            tokens = emptyList()
        )
        val r = OrgHtmlRenderer()
        r.renderSection(section)
        val headings = r.consumeHeadings()
        assertEquals("ABC-123", headings[0].anchor)
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `consumeHeadings returns TOC entries`() {
        val section = section(level = 2, title = "My Heading", body = emptyList())
        val r = OrgHtmlRenderer()
        r.renderSection(section)
        val headings = r.consumeHeadings()
        assertEquals(1, headings.size)
        assertEquals(2, headings[0].level)
        assertEquals("My Heading", headings[0].title)
        assertEquals("my-heading", headings[0].anchor)
    }

Tests: published-node class

kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `published sub-heading gets published-node class`() {
        val section = OrgSection(
            heading = OrgHeading(
                title = OrgLine(items = listOf(text("Published Child")), tokens = emptyList()),
                level = OrgHeadingLevel(level = 2, tokens = emptyList()),
                tags = null, todoState = null, priority = null,
                planningInfo = null,
                properties = OrgProperties(
                    map = mapOf("ID" to OrgLine(items = listOf(text("child-node-id")), tokens = emptyList())),
                    tokens = emptyList()
                ),
                tokens = emptyList()
            ),
            body = emptyList(),
            tokens = emptyList()
        )
        val r = OrgHtmlRenderer(publishedNodeIds = setOf("child-node-id"))
        val html = r.renderSection(section)
        assertTrue(html.contains("class=\"published-node\""))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `unpublished sub-heading has no published-node class`() {
        val section = OrgSection(
            heading = OrgHeading(
                title = OrgLine(items = listOf(text("Unpublished Child")), tokens = emptyList()),
                level = OrgHeadingLevel(level = 2, tokens = emptyList()),
                tags = null, todoState = null, priority = null,
                planningInfo = null,
                properties = OrgProperties(
                    map = mapOf("ID" to OrgLine(items = listOf(text("child-node-id")), tokens = emptyList())),
                    tokens = emptyList()
                ),
                tokens = emptyList()
            ),
            body = emptyList(),
            tokens = emptyList()
        )
        val r = OrgHtmlRenderer(publishedNodeIds = setOf("some-other-id"))
        val html = r.renderSection(section)
        assertFalse(html.contains("published-node"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `heading with ID and backlinks emits inline heading-backlinks section`() {
        val section = OrgSection(
            heading = OrgHeading(
                title = OrgLine(items = listOf(text("Topic")), tokens = emptyList()),
                level = OrgHeadingLevel(level = 2, tokens = emptyList()),
                tags = null, todoState = null, priority = null,
                planningInfo = null,
                properties = OrgProperties(
                    map = mapOf("ID" to OrgLine(items = listOf(text("node-abc")), tokens = emptyList())),
                    tokens = emptyList()
                ),
                tokens = emptyList()
            ),
            body = listOf(paragraph("body text")),
            tokens = emptyList()
        )
        val r = OrgHtmlRenderer(headingBacklinks = mapOf(
            "node-abc" to listOf(BacklinkSnippet("src-1", "Source Page", "/source"))
        ))
        val html = r.renderSection(section)
        assertTrue(html.contains("<section class=\"heading-backlinks\" data-node-id=\"node-abc\">"))
        assertTrue(html.contains("<h4>Linked from</h4>"))
        assertTrue(html.contains("<a href=\"/source\">Source Page</a>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `heading with ID and no backlinks emits no heading-backlinks section`() {
        val section = OrgSection(
            heading = OrgHeading(
                title = OrgLine(items = listOf(text("Topic")), tokens = emptyList()),
                level = OrgHeadingLevel(level = 2, tokens = emptyList()),
                tags = null, todoState = null, priority = null,
                planningInfo = null,
                properties = OrgProperties(
                    map = mapOf("ID" to OrgLine(items = listOf(text("node-abc")), tokens = emptyList())),
                    tokens = emptyList()
                ),
                tokens = emptyList()
            ),
            body = listOf(paragraph("body text")),
            tokens = emptyList()
        )
        val r = OrgHtmlRenderer(headingBacklinks = mapOf("node-abc" to emptyList()))
        val html = r.renderSection(section)
        assertFalse(html.contains("heading-backlinks"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `heading with ID and refs emits inline heading-refs section`() {
        val section = OrgSection(
            heading = OrgHeading(
                title = OrgLine(items = listOf(text("Topic")), tokens = emptyList()),
                level = OrgHeadingLevel(level = 2, tokens = emptyList()),
                tags = null, todoState = null, priority = null,
                planningInfo = null,
                properties = OrgProperties(
                    map = mapOf("ID" to OrgLine(items = listOf(text("node-abc")), tokens = emptyList())),
                    tokens = emptyList()
                ),
                tokens = emptyList()
            ),
            body = listOf(paragraph("body text")),
            tokens = emptyList()
        )
        val r = OrgHtmlRenderer(headingRefs = mapOf(
            "node-abc" to listOf("https://example.com", "https://other.com")
        ))
        val html = r.renderSection(section)
        assertTrue(html.contains("<section class=\"heading-refs\" data-node-id=\"node-abc\">"))
        assertTrue(html.contains("<h4>References</h4>"))
        assertTrue(html.contains("<a target=\"_blank\" href=\"https://example.com\">https://example.com</a>"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `heading without ID emits no inline sections even with data`() {
        val section = section(level = 2, title = "No ID", body = listOf(paragraph("text")))
        val r = OrgHtmlRenderer(
            headingBacklinks = mapOf("irrelevant" to listOf(BacklinkSnippet("s", "t", "/u"))),
            headingRefs = mapOf("irrelevant" to listOf("https://x.com"))
        )
        val html = r.renderSection(section)
        assertFalse(html.contains("heading-backlinks"))
        assertFalse(html.contains("heading-refs"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `empty default maps produce no inline sections`() {
        val section = section(level = 2, title = "Plain", body = listOf(paragraph("text")))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertFalse(html.contains("heading-backlinks"))
        assertFalse(html.contains("heading-refs"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `heading with no ID property never gets published-node class`() {
        val section = section(level = 2, title = "No ID Here", body = emptyList())
        val r = OrgHtmlRenderer(publishedNodeIds = setOf("anything"))
        val html = r.renderSection(section)
        assertFalse(html.contains("published-node"))
    }

Tests: tag-based elision

kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `elides heading tagged noexport`() {
        val section = sectionWithTags(level = 1, title = "Hidden", body = listOf(paragraph("secret")), tags = listOf("noexport"))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertFalse(html.contains("Hidden"))
        assertFalse(html.contains("secret"))
        assertFalse(html.contains("<h1"))
        assertEquals(0, r.consumeHeadings().size)
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `elides heading tagged ARCHIVE`() {
        val section = sectionWithTags(level = 2, title = "Archived", body = listOf(paragraph("old")), tags = listOf("ARCHIVE"))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertFalse(html.contains("Archived"))
        assertFalse(html.contains("old"))
        assertEquals(0, r.consumeHeadings().size)
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `elides heading tagged uppercase NOEXPORT`() {
        val section = sectionWithTags(level = 1, title = "Also Hidden", body = listOf(paragraph("hush")), tags = listOf("NOEXPORT"))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertFalse(html.contains("Also Hidden"))
        assertFalse(html.contains("hush"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `preserves heading tagged Archive mixed case`() {
        // :Archive: is a personal organization tag, NOT org-mode's archive tag.
        // It must still be rendered.
        val section = sectionWithTags(level = 1, title = "My Archive", body = listOf(paragraph("kept")), tags = listOf("Archive"))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertTrue(html.contains("My Archive"))
        assertTrue(html.contains("kept"))
        assertEquals(1, r.consumeHeadings().size)
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `elides noexport sub-section but renders parent body`() {
        // Parent is published; its child carries :noexport:. The child and its
        // sub-tree must vanish, but the parent and its non-excluded content remain.
        val child = sectionWithTags(level = 2, title = "Secret Child", body = listOf(paragraph("leaked")), tags = listOf("noexport"))
        val parent = section(level = 1, title = "Parent", body = listOf(
            paragraph("parent intro"),
            child,
            paragraph("parent outro")
        ))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(parent)
        assertTrue(html.contains("Parent"))
        assertTrue(html.contains("parent intro"))
        assertTrue(html.contains("parent outro"))
        assertFalse(html.contains("Secret Child"))
        assertFalse(html.contains("leaked"))
        // Only the parent heading is recorded for the TOC.
        val headings = r.consumeHeadings()
        assertEquals(1, headings.size)
        assertEquals("Parent", headings[0].title)
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `elides sub-tree under noexport heading`() {
        // A noexport heading's entire body — including nested sub-headings —
        // must be pruned, not just the heading line.
        val grandchild = section(level = 3, title = "Grandchild", body = listOf(paragraph("deep secret")))
        val hiddenParent = sectionWithTags(level = 2, title = "Hidden Branch", body = listOf(grandchild), tags = listOf("noexport"))
        val root = section(level = 1, title = "Root", body = listOf(hiddenParent))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(root)
        assertTrue(html.contains("Root"))
        assertFalse(html.contains("Hidden Branch"))
        assertFalse(html.contains("Grandchild"))
        assertFalse(html.contains("deep secret"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `renderDocument elides noexport top-level section`() {
        val preface = OrgPreface(body = listOf(paragraph("Intro")), tokens = emptyList())
        val visible = section(level = 1, title = "Visible", body = listOf(paragraph("shown")))
        val hidden = sectionWithTags(level = 1, title = "Classified", body = listOf(paragraph("redacted")), tags = listOf("noexport"))
        val doc = orgDocument(preface, listOf(visible, hidden))
        val r = OrgHtmlRenderer()
        val html = r.renderDocument(doc)
        assertTrue(html.contains("Visible"))
        assertTrue(html.contains("shown"))
        assertFalse(html.contains("Classified"))
        assertFalse(html.contains("redacted"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `elision checks all tags not just first`() {
        // A heading with several tags where an exclude tag is not first must still elide.
        val section = sectionWithTags(level = 1, title = "Multi", body = listOf(paragraph("x")), tags = listOf("project", "noexport", "research"))
        val r = OrgHtmlRenderer()
        val html = r.renderSection(section)
        assertFalse(html.contains("Multi"))
        assertFalse(html.contains("<h1"))
    }

OrgHtmlRenderer — Entry Point

The public API. renderSection renders a single section (heading + body). renderSections renders multiple sections (for list pages). renderPreface renders preface body chunks (for file-level content). The renderer accumulates footnotes and headings during a render pass; callers retrieve them via consumeFootnotes() / consumeHeadings() and reset the renderer for a new page by constructing a fresh instance (the standard pattern — ArcologyServer.renderNode creates one OrgHtmlRenderer per request).

renderSidenotes() emits the accumulated footnote definitions as =<aside class="sidenote">= blocks. Callers typically append this after the body content, inside <main>, before the sidebar.

Data classes

HeadingLink is @Serializable so the HtmlCache can store a JSON envelope of (html, sidenotes, headings) per node — otherwise a cache hit would lose the TOC and sidenotes, since consumeHeadings() only returns data after a fresh render pass.

kotlin#+name: renderer-entry-data
data class FootnoteDef(val number: Int, val key: String?, val html: String)

@Serializable
data class HeadingLink(val level: Int, val title: String, val anchor: String)

data class NowebBacklink(val anchor: String, val label: String)

/**
 * A backlink to a heading-level node, grouped by target node ID.
 *
 * The [[file:server.org][SidebarService]] collects these per-node and passes
 * the grouping to [OrgHtmlRenderer.headingBacklinks] so the renderer can embed
 * a backlinks section after each heading that has incoming links. The flat list
 * (for the sidebar panel) is derived from the same grouped map.
 *
 * `nodeId` is the *source* node (the one linking here); the map key (supplied
 * by the caller, not stored here) is the *target* node (the heading being
 * linked to).
 */
data class BacklinkSnippet(val nodeId: String, val title: String, val url: String)

Class declaration and state

kotlin#+name: renderer-entry-class
class OrgHtmlRenderer(
    internal val linkResolver: LinkResolver = LinkResolver.Noop,
    internal val publishedNodeIds: Set<String> = emptySet(),
    internal val headingBacklinks: Map<String, List<BacklinkSnippet>> = emptyMap(),
    internal val headingRefs: Map<String, List<String>> = emptyMap()
) {
    internal val footnotes: MutableList<FootnoteDef> = mutableListOf()
    internal val headings: MutableList<HeadingLink> = mutableListOf()
    internal val nowebRefs: MutableMap<String, MutableList<String>> = mutableMapOf()
    internal val nowebRefCounts: MutableMap<String, Int> = mutableMapOf()
    internal val nowebBacklinks: MutableMap<String, MutableList<NowebBacklink>> = mutableMapOf()
    private var footnoteCounter: Int = 0

    internal fun nextFootnoteNumber(): Int {
        footnoteCounter += 1
        return footnoteCounter
    }

Render methods

kotlin#+name: renderer-entry-render
    fun renderSection(section: OrgSection): String {
        val sb = StringBuilder()
        sb.append("<article>")
        sb.append(renderSectionInternal(section))
        sb.append(renderNowebRefMap())
        sb.append(renderNowebBacklinkMap())
        sb.append("</article>")
        return sb.toString()
    }

    fun renderSections(sections: List<OrgSection>): String {
        val sb = StringBuilder()
        for (section in sections) {
            sb.append("<article>")
            sb.append(renderSectionInternal(section))
            sb.append("</article>")
        }
        sb.append(renderNowebRefMap())
        sb.append(renderNowebBacklinkMap())
        return sb.toString()
    }

    fun renderPreface(preface: OrgPreface): String {
        val sb = StringBuilder()
        sb.append(renderChunks(preface.body))
        sb.append(renderNowebRefMap())
        sb.append(renderNowebBacklinkMap())
        return sb.toString()
    }

    fun renderDocument(document: OrgDocument): String {
        val sb = StringBuilder()
        sb.append("<article>")
        if (document.preface.body.isNotEmpty()) {
            sb.append(renderChunks(document.preface.body))
        }
        for (section in document.content) {
            sb.append(renderSectionInternal(section))
        }
        sb.append(renderNowebRefMap())
        sb.append(renderNowebBacklinkMap())
        sb.append("</article>")
        return sb.toString()
    }

Consume methods

kotlin#+name: renderer-entry-consume
    fun renderSidenotes(): String {
        if (footnotes.isEmpty()) return ""
        val sb = StringBuilder()
        for (fn in footnotes) {
            val id = "fn-${fn.number}"
            sb.append("""<aside class="sidenote" id="${id}ref"><sup>${fn.number}</sup> ${fn.html}</aside>""")
        }
        return sb.toString()
    }

    fun consumeFootnotes(): List<FootnoteDef> {
        val snapshot = footnotes.toList()
        footnotes.clear()
        footnoteCounter = 0
        return snapshot
    }

    fun consumeHeadings(): List<HeadingLink> {
        val snapshot = headings.toList()
        headings.clear()
        return snapshot
    }
}

Tests: renderDocument and renderSections

kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `renderDocument includes preface and sections`() {
        val doc = orgDocument(
            preface = OrgPreface(body = listOf(paragraph("Preface content")), tokens = emptyList()),
            sections = listOf(section(1, "Section 1", listOf(paragraph("Section body"))))
        )
        val html = renderer.renderDocument(doc)
        assertTrue(html.contains("Preface content"))
        assertTrue(html.contains("Section 1"))
        assertTrue(html.contains("Section body"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `renderSections wraps each in article tag`() {
        val sections = listOf(
            section(1, "First", listOf(paragraph("Body 1"))),
            section(1, "Second", listOf(paragraph("Body 2")))
        )
        val html = renderer.renderSections(sections)
        assertTrue(html.contains("<article>"))
        assertTrue(html.contains("First"))
        assertTrue(html.contains("Second"))
    }
kotlin#+name: renderer-test:noweb-ref renderer-test
    @Test
    fun `renderDocument renders preface and all top-level sections`() {
        // Simulates a file-level node: its ID is the preface's, so the server
        // falls through to renderDocument. All top-level sections must appear.
        val preface = OrgPreface(body = listOf(paragraph("Intro text")), tokens = emptyList())
        val s1 = section(level = 1, title = "First", body = listOf(paragraph("Body one")))
        val s2 = section(level = 1, title = "Second", body = listOf(paragraph("Body two")))
        val doc = orgDocument(preface, listOf(s1, s2))
        val r = OrgHtmlRenderer()
        val html = r.renderDocument(doc)
        assertTrue(html.contains("Intro text"))
        assertTrue(html.contains("<h1 id=\"first\">First</h1>"))
        assertTrue(html.contains("Body one"))
        assertTrue(html.contains("<h1 id=\"second\">Second</h1>"))
        assertTrue(html.contains("Body two"))
    }
kotlin#+name: renderer-test-end
}

Tangle Targets

LinkResolver.kt

OrgHtmlRenderer.kt

kotlin#+name: renderer-assembly:tangle ../src/commonMain/kotlin/computer/whatthefuck/arcology/publishing/OrgHtmlRenderer.kt:noweb yes
package computer.whatthefuck.arcology.publishing

import kotlinx.serialization.Serializable
import xyz.lepisma.orgmode.OrgChunk
import xyz.lepisma.orgmode.OrgBlock
import xyz.lepisma.orgmode.OrgDocument
import xyz.lepisma.orgmode.OrgInlineElem
import xyz.lepisma.orgmode.OrgList
import xyz.lepisma.orgmode.OrgPreface
import xyz.lepisma.orgmode.OrgSection

<<html-escape>>

<<inline-markup>>

<<inline-renderer-merge>>
<<inline-renderer-dispatch>>
<<inline-renderer-link>>
<<inline-renderer-footnote>>

<<block-renderer-chunks>>
<<block-renderer-source>>
<<block-renderer-noweb>>
<<block-renderer-tables-lists>>

<<section-renderer>>

<<renderer-entry-data>>
<<renderer-entry-class>>
<<renderer-entry-render>>
<<renderer-entry-consume>>

OrgHtmlRendererTest.kt

kotlin#+name: renderer-test-assembly:tangle ../src/commonTest/kotlin/computer/whatthefuck/arcology/publishing/OrgHtmlRendererTest.kt:noweb yes
<<renderer-test-prelude>>
<<renderer-test>>
<<renderer-test-end>>

Related Modules