Skip to content

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: - [ ] todo and - [x] done
  • Linkify: bare URLs become links

Additional Extensions

  • Footnotes: [^1] references and [^1]: footnote text definitions
  • Typographer: smart punctuation (-- → en-dash, --- → em-dash, ... → ellipsis, smart quotes)
  • Auto-heading IDs: automatic id attributes 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 settings
  • to_html_opts(src: string, opts: Options) string - Convert with custom options
  • parse_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 call
  • parse(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 descendants
  • walk(f: fn(&Node) bool) bool - Traverse AST pre-order; return false from callback to stop

Extensions

Available as functions returning extension structs:

  • table() - GFM tables
  • strikethrough() - GFM strikethrough
  • linkify() - Bare URL autolinks
  • task_list() - GFM task lists
  • footnote() - Footnote references and definitions
  • typographer() - Smart punctuation
  • definition_list() - Pandoc-style definition lists
  • gfm() - 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>')
// 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('![alt](image.png "title")')

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 on NodeKind
  • Inline nodes parsed on-demand during rendering
  • Link references cached in Markdown for 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 -w on 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 #

@[heap]
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 #

@[params]
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 #

@[params]
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 #

@[params]
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.