v2 / vlib / x / markdown / markdown.v
151 lines · 140 sloc · 4.42 KB · 46c3d7f13d605a08603985fe4e6f82f2a8771775
Raw
1// Copyright 2026 The V Language. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4//
5// Module markdown provides CommonMark-compliant markdown parsing and HTML
6// rendering with support for GitHub Flavored Markdown and additional
7// extensions. It is designed for feature parity with github.com/yuin/goldmark.
8//
9// Basic usage:
10//
11// import x.markdown
12//
13// html := markdown.to_html('# Hello\n\nWorld')
14//
15// With GFM extensions:
16//
17// md := markdown.new(extensions: markdown.gfm())
18// html := md.convert('| a | b |\n|---|---|\n| 1 | 2 |')
19//
20// With fine-grained options:
21//
22// md := markdown.new(
23// extensions: [markdown.Extension(markdown.footnote()), markdown.typographer()],
24// parser_opts: markdown.ParserOptions{ auto_heading_id: true },
25// renderer_opts: markdown.RendererOptions{ unsafe_: true, xhtml: true },
26// )
27// html := md.convert(source)
28//
29// Parse to AST and walk:
30//
31// doc := md.parse(source)
32// doc.walk(fn (node &markdown.Node) bool {
33// println(node.kind)
34// return true
35// })
36module markdown
37
38// ParserOptions configures parser behaviour.
39@[params]
40pub struct ParserOptions {
41pub mut:
42 // auto_heading_id generates an id attribute for every heading node
43 // derived from the heading text content (goldmark WithAutoHeadingID).
44 auto_heading_id bool
45}
46
47// RendererOptions configures HTML renderer behaviour.
48@[params]
49pub struct RendererOptions {
50pub mut:
51 // unsafe_ allows raw HTML from the source to be included in the output.
52 // When false (the default) raw HTML is replaced with an HTML comment.
53 unsafe_ bool
54 // hard_wraps converts every newline inside a paragraph to a <br> tag.
55 hard_wraps bool
56 // xhtml outputs XHTML-style self-closing tags (e.g. <br />).
57 xhtml bool
58}
59
60// Options configures a Markdown processor.
61// Extension flags in the mut section are normally set by calling new() with
62// an extensions slice; they can also be set directly.
63@[params]
64pub struct Options {
65pub mut:
66 // extensions is the list of extensions applied when new() is called.
67 extensions []Extension
68 // parser_opts configures the parser.
69 parser_opts ParserOptions
70 // renderer_opts configures the renderer.
71 renderer_opts RendererOptions
72 // --- feature flags set by extensions ---
73 tables bool
74 strikethrough bool
75 linkify bool
76 task_list bool
77 footnotes bool
78 typographer bool
79 definition_list bool
80}
81
82// LinkRef holds a collected link reference definition (url + optional title).
83struct LinkRef {
84 dest string
85 title string
86}
87
88// Markdown is the main markdown processor. Create one with new() and reuse it
89// across multiple convert/parse calls; link reference definitions are cached.
90pub struct Markdown {
91pub mut:
92 opts Options
93 ref_map map[string]LinkRef
94}
95
96// Markdown.new creates a Markdown processor with the given options.
97// All extensions in opts.extensions are applied immediately.
98pub fn Markdown.new(opts Options) Markdown {
99 mut m := Markdown{
100 opts: opts
101 ref_map: map[string]LinkRef{}
102 }
103 for ext in opts.extensions {
104 ext.extend(mut m)
105 }
106 return m
107}
108
109// to_html converts the markdown source to HTML with the given options.
110pub fn to_html(src string, opts Options) string {
111 mut md := Markdown.new(opts)
112 return md.convert(src)
113}
114
115// to_plaintext converts the markdown source to UTF-8 plain text with the given options.
116pub fn to_plaintext(src string, opts Options) string {
117 mut md := Markdown.new(opts)
118 return md.convert_plaintext(src)
119}
120
121// convert parses the markdown source and renders it to an HTML string.
122pub fn (mut m Markdown) convert(src string) string {
123 doc := m.parse(src)
124 mut r := HTMLRenderer{
125 opts: m.opts
126 ref_map: m.ref_map // Use the updated ref_map after parse()
127 }
128 return r.render(doc)
129}
130
131// convert_plaintext parses the markdown source and renders it to UTF-8 plain text.
132pub fn (mut m Markdown) convert_plaintext(src string) string {
133 doc := m.parse(src)
134 mut r := PlainTextRenderer{
135 opts: m.opts
136 ref_map: m.ref_map
137 }
138 return r.render(doc)
139}
140
141// parse parses the markdown source into an AST and returns the document root.
142// Link reference definitions collected during parsing are cached so that
143// subsequent parse/convert calls on the same Markdown instance share them.
144pub fn (mut m Markdown) parse(src string) &Node {
145 mut p := BlockParser.new(src, m.opts, m.ref_map)
146 doc := p.parse()
147 for k, v in p.ref_map {
148 m.ref_map[k] = v
149 }
150 return doc
151}
152