x.markdown #
// 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.
vlib/x/markdown - Markdown Parser and HTML Renderer
A CommonMark-compliant Markdown parser and HTML renderer for V, with support for GitHub Flavored Markdown (GFM) extensions. Designed for feature parity with github.com/yuin/goldmark.
Features
CommonMark Support
- Block-level elements: headings (ATX and setext), paragraphs, blockquotes, lists (bullet and ordered), code blocks (indented and fenced), HTML blocks, thematic breaks
- Inline elements: emphasis (em and strong), code spans, links (inline and reference), images, autolinks, hard/soft line breaks, HTML entities, raw HTML
- Link reference definitions for DRY Markdown
GFM Extensions (via .gfm() helper or individual extensions)
- Tables:
| col | col |with alignment (:--,:--:,--:) - Strikethrough:
~~text~~ - Task lists:
- [ ] todoand- [x] done - Linkify: bare URLs become links
Additional Extensions
- Footnotes:
[^1]references and[^1]: footnote textdefinitions - Typographer: smart punctuation (
--→ en-dash,---→ em-dash,...→ ellipsis, smart quotes) - Auto-heading IDs: automatic
idattributes on headings from text content - Definition lists: Pandoc-style (requires extension)
Quick Start
Basic Usage
import x.markdown
fn main() {
html := markdown.to_html('# Hello\n\nWorld')
println(html)
// Output: <h1>Hello</h1>\n<p>World</p>\n
}
With Extensions
mut md := markdown.new(Options{
extensions: markdown.gfm()
})
html := md.convert('| Name |\n|------|\n| Alice |')
println(html) // Renders as HTML table
Fine-Grained Configuration
import x.markdown
fn main() {
mut md := 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)
}
Parse to AST and Walk
import x.markdown
fn main() {
mut md := 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
})
}
API Overview
Top-Level Functions
to_html(src: string) string- Convert Markdown to HTML with default settingsto_html_opts(src: string, opts: Options) string- Convert with custom optionsparse_inline(src: string, opts: Options, ref_map: map) []&Node- Parse inline content only
Main Structs
Markdown
The main processor. Create with new(), reuse across multiple calls to share link references.
Methods:
convert(src: string) string- Parse and render to HTML in one callparse(src: string) &Node- Parse to AST only
Options (@[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)
}
Node
An 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 stop
Extensions
Available 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()]
Examples
Simple Emphasis
assert markdown.to_html('*em*').contains('<em>em</em>')
assert markdown.to_html('**strong**').contains('<strong>strong</strong>')
Links and Images
// 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('')
Code Blocks
// Indented code
html := markdown.to_html(' code')
// Fenced code
html = markdown.to_html('```v\nfn main() {}\n```')
Lists
// 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_opts('- [x] done', Options{ task_list: true })
Tables (GFM)
src := '| Left | Center | Right |\n|:--|:--:|--:|\n| A | B | C |'
html := markdown.to_html_opts(src, Options{ tables: true })
Footnotes
src := 'Text[^1]\n\n[^1]: Footnote body.'
html := markdown.to_html_opts(src, Options{ footnotes: true })
// Renders with <sup> reference and footnote section at bottom
Design Notes
Block Parsing
- Reads source line-by-line, building a block-level AST
- Handles lazy continuation lines for blockquotes and lists
- Collects link reference definitions for inline resolution
Inline Parsing
- Parses raw text from paragraph/heading/cell nodes using a simple state machine
- Emphasis/strong uses a delimiter-run resolution pass aligned with CommonMark rules
- Backticks, brackets, and HTML are handled specially
Rendering
- Tree walk via
render_node()dispatch onNodeKind - Inline nodes parsed on-demand during rendering
- Link references cached in
Markdownfor reuse across multiple convert calls
Limitations and Known Issues
- Definition list syntax is Pandoc-style; CommonMark does not define this
Status: All core features (headings, emphasis, links, code, lists, blockquotes, task lists, tables, HTML escaping) work reliably without crashes.
Testing
Run 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'
}
Contributing
- Follow V style guidelines (use
v fmt -won edits) - Add tests for new features
- Update documentation for public API changes
- Keep CommonMark compliance as the baseline
License
MIT, same as V.
References
fn definition_list #
fn definition_list() DefinitionListExt
definition_list returns a DefinitionListExt extension value.
fn footnote #
fn footnote() FootnoteExt
footnote returns a FootnoteExt extension value.
fn gfm #
fn gfm() []Extension
gfm returns the core GitHub Flavored Markdown extensions: TableExt, StrikethroughExt, LinkifyExt, and TaskListExt.
fn linkify #
fn linkify() LinkifyExt
linkify returns a LinkifyExt extension value.
fn new_node #
fn new_node(kind NodeKind) &Node
new_node allocates and returns a new Node of the given kind.
fn parse_inline #
fn parse_inline(src string, opts Options, ref_map map[string]LinkRef) []&Node
parse_inline parses src as inline content and returns a slice of inline nodes.
fn strikethrough #
fn strikethrough() StrikethroughExt
strikethrough returns a StrikethroughExt extension value.
fn table #
fn table() TableExt
table returns a TableExt extension value.
fn task_list #
fn task_list() TaskListExt
task_list returns a TaskListExt extension value.
fn to_html #
fn to_html(src string, opts Options) string
to_html converts the markdown source to HTML with the given options.
fn typographer #
fn typographer() TypographerExt
typographer returns a TypographerExt extension value.
fn Alignment.from #
fn Alignment.from[W](input W) !Alignment
fn Markdown.new #
fn Markdown.new(opts Options) Markdown
Markdown.new creates a Markdown processor with the given options. All extensions in opts.extensions are applied immediately.
fn NodeKind.from #
fn NodeKind.from[W](input W) !NodeKind
interface Extension #
interface Extension {
// extend is called once when the extension is registered with a Markdown processor.
extend(mut m Markdown)
}
Extension is the interface implemented by markdown extensions. An extension configures the Markdown processor by enabling parser and renderer features.
fn (HTMLRenderer) render #
fn (mut r HTMLRenderer) render(doc &Node) string
render renders the document node to an HTML string.
fn (x.markdown.Node) append_child #
fn (mut n Node) append_child(child &Node)
append_child appends child as the last child of n.
fn (x.markdown.Node) text_content #
fn (n &Node) text_content() string
text_content returns the plain-text content of this node and all descendants, concatenated in document order.
fn (x.markdown.Node) walk #
fn (n &Node) walk(f fn (&Node) bool) bool
walk traverses n and all its descendants in pre-order (root before children). The callback f receives each node; return false from f to stop traversal early. walk itself returns false if traversal was stopped, true otherwise.
enum Alignment #
enum Alignment {
none_
left
center
right
}
Alignment is the text alignment of a table cell column.
enum NodeKind #
enum NodeKind {
document
heading
paragraph
blockquote
list
list_item
code_block
fenced_code
thematic_break
html_block
link_ref_def
table
table_head
table_body
table_row
table_cell
definition_list
definition_term
definition_desc
footnote_def
text
emphasis
strong
code_span
link
image
autolink
raw_html
hard_break
soft_break
strikethrough
footnote_ref
task_checkbox
}
NodeKind identifies what kind of AST node a Node represents.
struct DefinitionListExt #
struct DefinitionListExt {}
DefinitionListExt adds Pandoc-style definition list support.
fn (DefinitionListExt) extend #
fn (_ DefinitionListExt) extend(mut m Markdown)
extend implements Extension for DefinitionListExt.
struct FootnoteExt #
struct FootnoteExt {}
FootnoteExt adds footnote support ([^label] references and [^label]: definitions).
fn (FootnoteExt) extend #
fn (_ FootnoteExt) extend(mut m Markdown)
extend implements Extension for FootnoteExt.
struct LinkifyExt #
struct LinkifyExt {}
LinkifyExt adds autolink support for bare URLs and email addresses.
fn (LinkifyExt) extend #
fn (_ LinkifyExt) extend(mut m Markdown)
extend implements Extension for LinkifyExt.
struct Markdown #
struct Markdown {
pub mut:
opts Options
ref_map map[string]LinkRef
}
Markdown is the main markdown processor. Create one with new() and reuse it across multiple convert/parse calls; link reference definitions are cached.
fn (Markdown) convert #
fn (mut m Markdown) convert(src string) string
convert parses the markdown source and renders it to an HTML string.
fn (Markdown) parse #
fn (mut m Markdown) parse(src string) &Node
parse parses the markdown source into an AST and returns the document root. Link reference definitions collected during parsing are cached so that subsequent parse/convert calls on the same Markdown instance share them.
struct Node #
struct Node {
pub mut:
kind NodeKind
level int
is_tight bool
is_ordered bool
list_start int = 1
fence_info string
literal string
dest string
title string
label string
checked bool
align Alignment
id string
fn_label string
fn_index int
children []&Node
}
Node is a node in the parsed markdown AST. A document is a tree of Nodes with .document as the root.
struct Options #
struct Options {
pub mut:
// extensions is the list of extensions applied when new() is called.
extensions []Extension
// parser_opts configures the parser.
parser_opts ParserOptions
// renderer_opts configures the renderer.
renderer_opts RendererOptions
// --- feature flags set by extensions ---
tables bool
strikethrough bool
linkify bool
task_list bool
footnotes bool
typographer bool
definition_list bool
}
Options configures a Markdown processor. Extension flags in the mut section are normally set by calling new() with an extensions slice; they can also be set directly.
struct ParserOptions #
struct ParserOptions {
pub mut:
// auto_heading_id generates an id attribute for every heading node
// derived from the heading text content (goldmark WithAutoHeadingID).
auto_heading_id bool
}
struct RendererOptions #
struct RendererOptions {
pub mut:
// unsafe_ allows raw HTML from the source to be included in the output.
// When false (the default) raw HTML is replaced with an HTML comment.
unsafe_ bool
// hard_wraps converts every newline inside a paragraph to a <br> tag.
hard_wraps bool
// xhtml outputs XHTML-style self-closing tags (e.g. <br />).
xhtml bool
}
RendererOptions configures HTML renderer behaviour.
struct StrikethroughExt #
struct StrikethroughExt {}
StrikethroughExt adds GFM strikethrough support (text).
fn (StrikethroughExt) extend #
fn (_ StrikethroughExt) extend(mut m Markdown)
extend implements Extension for StrikethroughExt.
struct TableExt #
struct TableExt {}
TableExt adds GitHub Flavored Markdown table support (| col | col |).
fn (TableExt) extend #
fn (_ TableExt) extend(mut m Markdown)
extend implements Extension for TableExt.
struct TaskListExt #
struct TaskListExt {}
TaskListExt adds GFM task list item support (- [ ] / - [x]).
fn (TaskListExt) extend #
fn (_ TaskListExt) extend(mut m Markdown)
extend implements Extension for TaskListExt.
struct TypographerExt #
struct TypographerExt {}
TypographerExt replaces ASCII punctuation sequences with Unicode typographic equivalents: -- en dash, --- em dash, ... ellipsis, and smart quotes.
fn (TypographerExt) extend #
fn (_ TypographerExt) extend(mut m Markdown)
extend implements Extension for TypographerExt.
- README
- fn definition_list
- fn footnote
- fn gfm
- fn linkify
- fn new_node
- fn parse_inline
- fn strikethrough
- fn table
- fn task_list
- fn to_html
- fn typographer
- fn Alignment.from
- fn Markdown.new
- fn NodeKind.from
- interface Extension
- type HTMLRenderer
- type x.markdown.Node
- enum Alignment
- enum NodeKind
- struct DefinitionListExt
- struct FootnoteExt
- struct LinkifyExt
- struct Markdown
- struct Node
- struct Options
- struct ParserOptions
- struct RendererOptions
- struct StrikethroughExt
- struct TableExt
- struct TaskListExt
- struct TypographerExt