0 branches
Tree
Top files
Clone with HTTPS:
// 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 Renderers
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.
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')
text := markdown.to_plaintext('# Hello\n\nWorld')
println(html)
println(text)
}
With Extensions
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
Fine-Grained Configuration
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)
}
Parse to AST and Walk
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
})
}
API Overview
Top-Level Functions
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 only
Main Structs
Markdown
The 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 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
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 })
Tables (GFM)
import x.markdown
src := '| Left | Center | Right |\n|:--|:--:|--:|\n| A | B | C |'
html := markdown.to_html(src, markdown.Options{ tables: true })
Footnotes
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
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 thisStatus: 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.