// 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.
//
// Module markdown provides CommonMark-compliant markdown parsing and HTML
// rendering with support for GitHub Flavored Markdown and additional
// extensions. It is designed for feature parity with github.com/yuin/goldmark.
//
// Basic usage:
//
// import x.markdown
//
// html := markdown.to_html('# Hello\n\nWorld')
//
// With GFM extensions:
//
// md := markdown.new(extensions: markdown.gfm())
// html := md.convert('| a | b |\n|---|---|\n| 1 | 2 |')
//
// With fine-grained options:
//
// md := markdown.new(
// extensions: [markdown.Extension(markdown.footnote()), markdown.typographer()],
// parser_opts: markdown.ParserOptions{ auto_heading_id: true },
// renderer_opts: markdown.RendererOptions{ unsafe_: true, xhtml: true },
// )
// html := md.convert(source)
//
// Parse to AST and walk:
//
// doc := md.parse(source)
// doc.walk(fn (node &markdown.Node) bool {
// println(node.kind)
// return true
// })
module markdown
// ParserOptions configures parser behaviour.
@[params]
pub 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
}
// RendererOptions configures HTML renderer behaviour.
@[params]
pub 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
tag.
hard_wraps bool
// xhtml outputs XHTML-style self-closing tags (e.g.
).
xhtml 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.
@[params]
pub 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
}
// LinkRef holds a collected link reference definition (url + optional title).
struct LinkRef {
dest string
title string
}
// Markdown is the main markdown processor. Create one with new() and reuse it
// across multiple convert/parse calls; link reference definitions are cached.
pub struct Markdown {
pub mut:
opts Options
ref_map map[string]LinkRef
}
// Markdown.new creates a Markdown processor with the given options.
// All extensions in opts.extensions are applied immediately.
pub fn Markdown.new(opts Options) Markdown {
mut m := Markdown{
opts: opts
ref_map: map[string]LinkRef{}
}
for ext in opts.extensions {
ext.extend(mut m)
}
return m
}
// to_html converts the markdown source to HTML with the given options.
pub fn to_html(src string, opts Options) string {
mut md := Markdown.new(opts)
return md.convert(src)
}
// to_plaintext converts the markdown source to UTF-8 plain text with the given options.
pub fn to_plaintext(src string, opts Options) string {
mut md := Markdown.new(opts)
return md.convert_plaintext(src)
}
// convert parses the markdown source and renders it to an HTML string.
pub fn (mut m Markdown) convert(src string) string {
doc := m.parse(src)
mut r := HTMLRenderer{
opts: m.opts
ref_map: m.ref_map // Use the updated ref_map after parse()
}
return r.render(doc)
}
// convert_plaintext parses the markdown source and renders it to UTF-8 plain text.
pub fn (mut m Markdown) convert_plaintext(src string) string {
doc := m.parse(src)
mut r := PlainTextRenderer{
opts: m.opts
ref_map: m.ref_map
}
return r.render(doc)
}
// 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.
pub fn (mut m Markdown) parse(src string) &Node {
mut p := BlockParser.new(src, m.opts, m.ref_map)
doc := p.parse()
for k, v in p.ref_map {
m.ref_map[k] = v
}
return doc
}