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 <script> in the output, including inside a code block and inside link text.
A link the page must not follow
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.