The markdown package

Parse a document, render HTML, and see which links are left as plain text.

ecko get github.com/ecko-lang/markdown

The package parses a Markdown string into blocks, renders HTML, and pulls out the pieces a docs pipeline or a retrieval index actually asks for. It declares no capabilities, so an import cannot touch the network, the filesystem, or the environment.

import markdown
import std.fs

doc = fs.read("notes.md")

for c in markdown.code_blocks(doc) {
    if c.lang == "sh" { print(c.code) }
}
for s in markdown.sections(doc) {
    print(s.heading)
}
print(markdown.to_html(doc))

code_blocks returns { lang, code } for every fence. lang is the word after the opening fence, or "" when the fence was bare. sections returns { heading, level, body }, one per heading, which is the chunk you embed: the heading stays with the text under it. Text before the first heading comes back with an empty heading and level 0, rather than being dropped.

to_html escapes text before it adds any tag. A <script> in the source is &lt;script&gt; in the output, including inside a code block and inside link text.

html = markdown.to_html("[click](javascript:alert(1))")

html contains the word click. It does not contain href, and it does not contain javascript:. The words survive so a reader can see them. The URL does not become a link. data: and vbscript: are refused the same way. http, https, mailto, and a target with no scheme (/docs, #install) still become an href.

What it will not parse

It is a block-level subset. Three CommonMark forms are left as ordinary text:

  • A setext heading, the kind underlined with === or ---.
  • A list nested inside another list.
  • A reference link, [label][id] with the URL defined elsewhere.

If you were about to feed it a document that depends on one of those, convert that part first or it will render as a paragraph. The block list, the tables, and the plain-text extractor are on the markdown page.