// Copyright 2026 The V Language. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file.
A CommonMark-compliant Markdown parser for V, with HTML and console-friendly plain-text renderers, plus support for GitHub Flavored Markdown (GFM) extensions. Designed for feature parity with github.com/yuin/goldmark.
.gfm() helper or individual extensions)| col | col | with alignment (:--, :--:, --:)~~text~~- [ ] todo and - [x] done[^1] references and [^1]: footnote text definitions-- → en-dash, --- → em-dash,
... → ellipsis, smart quotes)id attributes on headings from text contentimport x.markdown
fn main() {
html := markdown.to_html('# Hello\n\nWorld')
text := markdown.to_plaintext('# Hello\n\nWorld')
println(html)
println(text)
}
import x.markdown
mut md := markdown.Markdown.new(markdown.Options{
extensions: markdown.gfm()
})
html := md.convert('| Name |\n|------|\n| Alice |')
println(html) // Renders as HTML table
import x.markdown
fn main() {
mut md := markdown.Markdown.new(markdown.Options{
extensions: [markdown.Extension(markdown.footnote()), markdown.typographer()]
parser_opts: markdown.ParserOptions{
auto_heading_id: true
}
renderer_opts: markdown.RendererOptions{
unsafe_: true
xhtml: true
}
})
source := '# Title'
html := md.convert(source)
println(html)
}
import x.markdown
fn main() {
mut md := markdown.Markdown.new(markdown.Options{})
source := '# Hello\n\n`x`'
doc := md.parse(source)
doc.walk(fn (node &markdown.Node) bool {
match node.kind {
.heading {
println('Heading level ${node.level}')
}
.code_span {
println('Code: ${node.literal}')
}
else {}
}
return true
})
}
to_html(src: string) string - Convert Markdown to HTML with default settingsto_html(src: string, opts: Options) string - Convert with custom optionsto_plaintext(src: string) string - Convert Markdown to UTF-8 plain textto_plaintext(src: string, opts: Options) string - Convert plain text with optionsparse_inline(src: string, opts: Options, ref_map: map) []&Node - Parse inline content onlyMarkdownThe main processor. Create with Markdown.new(), reuse across multiple calls
to share link references.
Methods:
convert(src: string) string - Parse and render to HTML in one callconvert_plaintext(src: string) string - Parse and render to plain textparse(src: string) &Node - Parse to AST onlyOptions (@[params])pub struct Options {
pub mut:
extensions []Extension
parser_opts ParserOptions
renderer_opts RendererOptions
// Extension feature flags (set by extensions)
tables bool
strikethrough bool
linkify bool
task_list bool
footnotes bool
typographer bool
definition_list bool
}
ParserOptions (@[params])pub struct ParserOptions {
pub mut:
auto_heading_id bool // Generate id from heading text
}
RendererOptions (@[params])pub struct RendererOptions {
pub mut:
unsafe_ bool // Allow raw HTML (default: false)
hard_wraps bool // Convert all \n to <br> (default: false)
xhtml bool // Output XHTML self-closing tags (default: false)
}
NodeAn AST node. Navigate with .children, inspect with .kind, .literal, .level, etc.
Methods:
text_content() string - Extract plain text from this node and descendantswalk(f: fn(&Node) bool) bool - Traverse AST pre-order; return false from callback to stopAvailable as functions returning extension structs:
table() - GFM tablesstrikethrough() - GFM strikethroughlinkify() - Bare URL autolinkstask_list() - GFM task listsfootnote() - Footnote references and definitionstypographer() - Smart punctuationdefinition_list() - Pandoc-style definition listsgfm() - Convenience helper returning [table(), strikethrough(), linkify(), task_list()]assert markdown.to_html('*em*').contains('<em>em</em>')
assert markdown.to_html('**strong**').contains('<strong>strong</strong>')
// Inline link
html := markdown.to_html('[click](https://example.com)')
// Reference link
html = markdown.to_html('[click][ref]\n\n[ref]: https://example.com')
// Image
html = markdown.to_html('')
// Indented code
html := markdown.to_html(' code')
// Fenced code
html = markdown.to_html('```v\nfn main() {}\n```')
import x.markdown
// Bullet list
html := markdown.to_html('- item 1\n- item 2')
// Ordered list
html = markdown.to_html('1. first\n2. second')
// Task list (enable via extension or task_list option)
html = markdown.to_html('- [x] done', markdown.Options{ task_list: true })
import x.markdown
src := '| Left | Center | Right |\n|:--|:--:|--:|\n| A | B | C |'
html := markdown.to_html(src, markdown.Options{ tables: true })
import x.markdown
src := 'Text[^1]\n\n[^1]: Footnote body.'
html := markdown.to_html(src, markdown.Options{ footnotes: true })
// Renders with <sup> reference and footnote section at bottom
render_node() dispatch on NodeKindMarkdown for reuse across multiple convert callsRun the test suite:
v -silent test vlib/x/markdown/markdown_test.v
Or write your own:
import x.markdown
fn test_my_markdown() {
html := markdown.to_html('# Test')
assert html == '<h1>Test</h1>\n'
}
v fmt -w on edits)MIT, same as V.