Repository navigation
Render HTML links and harden the 0.5 renderers against injection - #10
Merged
Merged
Conversation
A links edge with a text_span now makes the text it spans an <a class="cnd-link">, through the inline layer's edge_wrap, which the HTML renderer enables for the links family only; refs, cites and footnotes keep their marker line. A links edge without a span goes to the marker line as a link to its href. Every href goes through one check, cnd.core.urls.is_safe_url: http, https, mailto or no scheme at all, read as a browser reads it (leading controls stripped, every tab and newline removed). An unsafe href is never a link: the spanned text stays plain and a span-less href is written as text. The Markdown link marker uses the same check, escapes its text, and escapes its destination so that it parses back to the href that was checked (no entity or backslash turns into a scheme). A Markdown HTML table renders its cells with links=False: the converter already appends every link to the marker line. Part of CND-36 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
A math block rendered as bare text, so its source was read as prose: "a * b_1" turned into emphasis. It is now a fenced block with the info string "math", its fence longer than any backtick run inside, holding the producer's raw source when there is one, else the text. A numbering value follows on its own line in parentheses (a value that already has them keeps them as they are). The text-only option renders it the same way: a fence is not HTML. Part of CND-37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
Cell.rowspan and Cell.colspan accepted 0 or a negative number, which no table can lay out. Both are now constrained to >= 1; the schema gains "minimum": 1 on the two fields and the spec table says so. The default stays 1, so the hash vectors do not move. Part of CND-37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
_yaml_scalar escaped the backslash, the quote, \n, \r and \t only. A NUL or ESC in a title made the front matter invalid YAML, and NEL, U+2028 or U+2029 broke the scalar across lines. Every other C0 control, DEL, NEL and the two Unicode separators are now written as \uXXXX; PyYAML reads the front matter back to the original strings. Part of CND-37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
The HTML renderer wrote any ImageNode.path as src, javascript: and data:text/html included. A path now becomes src only when it passes the shared URL check (http, https, mailto or no scheme) or is a data:image/ URI (cnd.core.urls.is_safe_image_source); any other path renders the missing-image span with the alt text. Part of CND-37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
… writes A doc.extra key such as "description" or "viewport" produced a second <meta> with that name next to the one the converter writes from the document. A key equal to a name the converter emits (author, description, keywords, date, status, version, viewport; compared without case or surrounding spaces) is now skipped, and the conversion reports it in its warnings. Part of CND-37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
Under the text-only option sup and sub dropped to plain text, so "10<sup>3</sup>" read "103" and "H<sub>2</sub>O" was fine only by luck. sup now renders as ^x over one character and ^(text) over several, sub as _x and _(text); where free text is escaped the "_" is written "\_" so it never opens emphasis (markdown-it reads "H\_2O" and "10^3" as plain text). underline and highlight stay plain; the default mode and HTML are unchanged. Spec section 7 states the rule. Part of CND-37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
An item holding a nested list and then a paragraph rendered tight, so the paragraph's line was a lazy continuation of the nested list's last item. A nested list followed by any other block now counts as several blocks: the Markdown list goes loose and the HTML item wraps its paragraphs in <p>. A nested list at the end of an item still keeps the list tight. Both renderers share the rule (holds_several_blocks). Part of CND-37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
A caller that renders a container's or a footnote's blocks itself needs expand_headings, or a nested heading's section is lost; it was reachable only through the module. It is now in cnd.core.render's new __all__ (with the two renderers and holds_several_blocks) and re-exported from cnd.core and cnd. Part of CND-37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…class A blank or whitespace admonition kind gave the class "cnd-admonition-"; the kind class is now left out when the kind is empty. The renderer module docstring's NodeRenderer line, the HtmlNodeRenderer links paragraph and the HtmlConverter head bullet are rewrapped to the width of the text around them. Part of CND-37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
A math fence is read as LaTeX, so the Typst source a producer kept in raw displayed wrongly there. The fence now holds raw.value only when raw.format is "latex" (any case), and the block's text otherwise. Part of CND-37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
Two wrapped edges whose spans overlap gave an edge inside another, which for links is an <a> inside an <a>. Of overlapping wrapped edges the outer or first one now wins and the other is left out; an edge edge_wrap declines does not block another. wrapped_edges() says which edges a render actually wraps (not one left out for its span, an atom or an overlap), so a caller can render the others elsewhere. Part of CND-36 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
… marker line The HTML converter left every link with a text_span out of the marker line, so a link whose span did not render inline was lost: an empty or clamped span, a span inside a code or math atom, a link dropped as nested inside or across another, an unsafe href. HtmlNodeRenderer now says which links it wraps (inline_links, the same wrapped_edges predicate its render uses), and every other link goes to the marker line; an unsafe href shows there as text. A renderer without inline_links, or with links=False, gets every link in the marker line. Part of CND-36 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
The HTML bullet now says that a spanned link is an inline cnd-link anchor, that anchors never nest, and that every link not rendered inline falls back to the marker line. The Markdown bullet says that a link marker is escaped and URL-checked, and that a math block is a math fence holding LaTeX source or the text. A URL rule bullet states the safety rule both follow: http, https or mailto, or no scheme; anything else stays plain text. Part of CND-36 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…ink marker The marker's text was escape_inline(href), which keeps CR and LF. An href such as "a\n\n[x](ms-msdt:e)" passes the URL check (it has no scheme) and its second line became a link, a heading, a quote or an image of its own. Every line ending in the marker text is now a space, in the link branch and in the plain-text branch (which also goes through escape_block now); the destination already percent-encodes them. Part of CND-36 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
is_safe_url stripped only U+0000-U+0020 before reading the scheme, so " javascript:alert(1)" read as "no scheme", hence safe, while a Markdown parser strips Unicode whitespace from a destination and links to javascript:. Every leading Unicode whitespace (NEL, U+2028, U+3000 and the rest) and a BOM are now stripped too: the scheme is read as the most lenient reader reads it. Spec section 7 says so. Part of CND-36 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
TestFrontMatterScalars held literal U+2028 and U+2029 (in CONTROLS and in the round-trip test), which editors and diff tools show as line breaks. They are now written " " and " ". Part of CND-37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
Text decoded as Latin-1 instead of cp1252 carries C1 controls where its quotes and dashes were, and YAML forbids them unescaped, as it forbids U+FFFE, U+FFFF and lone surrogates. The whole U+0080-U+009F range, the two noncharacters and lone surrogates are now written as \uXXXX; PyYAML reads a cp1252-style title back unchanged. Part of CND-37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
The marker line left out a link whenever its object had rendered inline in any text of the block. A producer may share one LinkRef between two texts, inline in one and inside a code atom in the other, and the second was lost. Inline links are now keyed on (text_index, id(link)), the position ResolvedMarker already carries. Part of CND-36 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
The Markdown image wrote any path as its destination: no scheme check, "&" and "\" left as they were (so "javascript:…" parsed back as javascript:), and a line ending let the rest of the path start a block of its own. The destination now goes through escape_destination; a path that fails is_safe_image_source renders the [[image:id alt="…"]] placeholder instead of ; a line ending in the alt becomes a space, in the image and in the placeholder. Part of CND-37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
A "~~" next to a "~" (in the text, or a nested strike's own delimiter) made a run of three tildes or more. Such a run does not delimit, and at the start of a line it opens a code fence that swallows every block after it. A strike whose delimiter would touch a tilde now falls back to <s>, or to plain text under the text-only option. Part of CND-37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…nert The [[image:id alt="…"]] placeholder wrote its alt raw, so an alt holding "<img … onerror=…>" became live HTML and "x](ms-msdt:e)" a link; since the unsafe-path fallback, every image with an unsafe path took that route. The figure and table placeholders (proposal 0012) and the heading numbering prefix had the same pattern. Under escape, every quoted placeholder value now goes through escape_inline after the placeholder's own quoting (attr_value, made public and now also used for the image alt), and the heading numbering prefix is escaped and kept on one line. Without escape the placeholders stay verbatim, as all free text does. An image alt also escapes a backtick and "<": a code span, an autolink or raw HTML binds tighter than the image brackets and could reach into the destination. Part of CND-37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
A label is free text, and the Markdown converter wrote it raw in the [label], [^label] and [@Label] markers, the [^label]: definitions and the bibliography's **label**: "x](ms-msdt:e)" made a link, raw HTML ran, and a space or a "]" broke a GFM footnote. Every label now goes through one function, markdown_label: letters, digits and "-._:" kept, every other character (and "%") percent-encoded as UTF-8. The mapping is injective (unquote undoes it), so a footnote marker and its definition still match and two labels never meet; ordinary labels are unchanged. Where inline syntax applies the mapped label is also escaped, and a cite supplement is escaped and kept on one line. The HTML converter already escapes labels in ids, fragment hrefs and marker text; a test now holds it. mdit-py-plugins joins the dev group for the footnote syntax. Part of CND-37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Renders hyperlinks in the HTML output and hardens the 0.5 renderers against hostile CND content.
Stacked on #9. This branch builds on
cnd-27-render-cnd-0-5-0. Merge #9 first; this PR will then be retargeted toformat-0-5-0.Hyperlinks (CND-36)
<a class="cnd-link">. A link with no span goes on the marker line. A link is never lost: when it cannot render inline, it falls back to the marker line. The cases are an empty span, a span inside a code or math mark, a link nested in another, and an unsafe address.hrefandsrcand for Markdown link and image destinations. Onlyhttp,httpsandmailto, or a URL with no scheme, are kept (images also acceptdata:image/). Anything else stays plain text. The scheme is read as leniently as any reader of the output does: a browser strips control characters, tabs and newlines; Markdown strips Unicode whitespace.Hardening (CND-37)
mathfence holding LaTeX source, or its text form when the source is not LaTeX, followed by its number.minimumconstraints; content hashes are unchanged.extrakey cannot shadow a meta element the converter writes itself.text_onlykeeps superscripts and subscripts readable:10^3,H\_2O.expand_headingsis exported.Injection fixes from review
The review probed the output with markdown-it and
html.parser. Each of these is fixed and has a regression test:~~next to another tilde could open a code fence.Results
mdit-py-plugins, for the footnote parsing tests.Closes CND-36
Closes CND-37
🤖 Generated with Claude Code
https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS