v2 / vlib / x / markdown / plaintext.v
287 lines · 261 sloc · 7.0 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.
4module markdown
5
6import strings
7
8// PlainTextRenderer renders a parsed markdown AST to UTF-8 console-friendly text.
9struct PlainTextRenderer {
10 opts Options
11 ref_map map[string]LinkRef
12mut:
13 sb strings.Builder
14 // footnote tracking
15 fn_order []string
16 fn_nodes map[string]&Node
17 // list tracking
18 list_depth int
19 list_nums []int
20}
21
22// render renders the document node to plain text.
23pub fn (mut r PlainTextRenderer) render(doc &Node) string {
24 r.sb = strings.new_builder(1024)
25 if r.opts.footnotes {
26 for child in doc.children {
27 if child.kind == .footnote_def {
28 r.fn_nodes[child.fn_label] = child
29 }
30 }
31 }
32 r.render_children(doc)
33 if r.opts.footnotes && r.fn_order.len > 0 {
34 r.render_footnotes_section()
35 }
36 return r.sb.str().trim_right('\n') + '\n'
37}
38
39fn (mut r PlainTextRenderer) render_node(node &Node) {
40 match node.kind {
41 .document { r.render_children(node) }
42 .heading { r.render_heading(node) }
43 .paragraph { r.render_paragraph(node) }
44 .blockquote { r.render_blockquote(node) }
45 .list { r.render_list(node) }
46 .list_item { r.render_list_item(node) }
47 .code_block, .fenced_code { r.render_code_block(node) }
48 .thematic_break { r.sb.write_string('---\n') }
49 .html_block { r.render_html_block(node) }
50 .link_ref_def, .footnote_def {}
51 .table { r.render_table(node) }
52 .table_head, .table_body { r.render_children(node) }
53 .table_row { r.render_table_row(node) }
54 .table_cell { r.render_table_cell(node) }
55 .definition_list { r.render_children(node) }
56 .definition_term { r.render_definition_term(node) }
57 .definition_desc { r.render_definition_desc(node) }
58 .text { r.render_text(node) }
59 .emphasis { r.render_wrapped(node, '*') }
60 .strong { r.render_wrapped(node, '**') }
61 .code_span { r.sb.write_string('`' + node.literal + '`') }
62 .link { r.render_link(node) }
63 .image { r.render_image(node) }
64 .autolink { r.sb.write_string(node.literal) }
65 .raw_html { r.render_raw_html(node) }
66 .hard_break, .soft_break { r.sb.write_string('\n') }
67 .strikethrough { r.render_wrapped(node, '~~') }
68 .task_checkbox { r.render_task_checkbox(node) }
69 .footnote_ref { r.render_footnote_ref(node) }
70 }
71}
72
73fn (mut r PlainTextRenderer) render_children(node &Node) {
74 for child in node.children {
75 r.render_node(child)
76 }
77}
78
79fn (mut r PlainTextRenderer) render_inline(src string) {
80 nodes := parse_inline(src, r.opts, r.ref_map)
81 for node in nodes {
82 r.render_node(node)
83 }
84}
85
86fn (mut r PlainTextRenderer) render_heading(node &Node) {
87 r.sb.write_string('${'#'.repeat(node.level)} ')
88 if node.children.len > 0 {
89 r.render_children(node)
90 } else {
91 r.render_inline(node.literal)
92 }
93 r.sb.write_string('\n\n')
94}
95
96fn (mut r PlainTextRenderer) render_paragraph(node &Node) {
97 if node.children.len > 0 {
98 r.render_children(node)
99 } else {
100 r.render_inline(node.literal)
101 }
102 r.sb.write_string('\n')
103}
104
105fn (mut r PlainTextRenderer) render_blockquote(node &Node) {
106 mut inner := strings.new_builder(128)
107 mut rr := PlainTextRenderer{
108 opts: r.opts
109 ref_map: r.ref_map
110 fn_order: r.fn_order.clone()
111 fn_nodes: r.fn_nodes
112 }
113 rr.sb = inner
114 rr.render_children(node)
115 for line in rr.sb.str().trim_right('\n').split('\n') {
116 r.sb.write_string('> ${line}\n')
117 }
118 // Keep footnote reference order discovered inside the blockquote.
119 r.fn_order = rr.fn_order
120 r.sb.write_string('\n')
121}
122
123fn (mut r PlainTextRenderer) render_list(node &Node) {
124 r.list_depth++
125 if node.is_ordered {
126 r.list_nums << node.list_start
127 } else {
128 r.list_nums << 0
129 }
130 r.render_children(node)
131 r.list_nums.delete_last()
132 r.list_depth--
133 if r.list_depth == 0 {
134 r.sb.write_string('\n')
135 }
136}
137
138fn (mut r PlainTextRenderer) render_list_item(node &Node) {
139 indent := ' '.repeat(if r.list_depth > 0 { r.list_depth - 1 } else { 0 })
140 idx := r.list_nums.len - 1
141 marker := if idx >= 0 && r.list_nums[idx] > 0 {
142 m := '${r.list_nums[idx]}. '
143 r.list_nums[idx]++
144 m
145 } else {
146 '- '
147 }
148 r.sb.write_string(indent + marker)
149 for i, child in node.children {
150 if i > 0 {
151 r.sb.write_string(' ')
152 }
153 if child.kind == .paragraph {
154 if child.children.len > 0 {
155 r.render_children(child)
156 } else {
157 r.render_inline(child.literal)
158 }
159 } else if child.kind == .list {
160 r.sb.write_string('\n')
161 r.render_node(child)
162 } else {
163 r.render_node(child)
164 }
165 }
166 r.sb.write_string('\n')
167}
168
169fn (mut r PlainTextRenderer) render_code_block(node &Node) {
170 r.sb.write_string('```\n')
171 r.sb.write_string(node.literal.trim_right('\n'))
172 r.sb.write_string('\n```\n\n')
173}
174
175fn (mut r PlainTextRenderer) render_html_block(node &Node) {
176 if r.opts.renderer_opts.unsafe_ {
177 r.sb.write_string(node.literal)
178 } else {
179 r.sb.write_string('[raw HTML omitted]\n')
180 }
181}
182
183fn (mut r PlainTextRenderer) render_table(node &Node) {
184 r.render_children(node)
185 r.sb.write_string('\n')
186}
187
188fn (mut r PlainTextRenderer) render_table_row(node &Node) {
189 r.render_children(node)
190 r.sb.write_string('\n')
191}
192
193fn (mut r PlainTextRenderer) render_table_cell(node &Node) {
194 r.sb.write_string(node.literal.trim_space())
195 r.sb.write_string(' | ')
196}
197
198fn (mut r PlainTextRenderer) render_definition_term(node &Node) {
199 r.render_inline(node.literal)
200 r.sb.write_string('\n')
201 for child in node.children {
202 r.render_node(child)
203 }
204}
205
206fn (mut r PlainTextRenderer) render_definition_desc(node &Node) {
207 r.sb.write_string(' - ')
208 r.render_inline(node.literal)
209 r.sb.write_string('\n')
210}
211
212fn (mut r PlainTextRenderer) render_text(node &Node) {
213 content := if r.opts.typographer {
214 smart_punctuate(node.literal)
215 } else {
216 node.literal
217 }
218 r.sb.write_string(content)
219}
220
221fn (mut r PlainTextRenderer) render_wrapped(node &Node, marker string) {
222 r.sb.write_string(marker)
223 r.render_children(node)
224 r.sb.write_string(marker)
225}
226
227fn (mut r PlainTextRenderer) render_link(node &Node) {
228 r.render_children(node)
229 if node.dest.len > 0 {
230 r.sb.write_string(' (${node.dest})')
231 }
232}
233
234fn (mut r PlainTextRenderer) render_image(node &Node) {
235 alt := node.text_content().trim_space()
236 if alt.len == 0 {
237 r.sb.write_string('[image]')
238 } else {
239 r.sb.write_string('[image: ${alt}]')
240 }
241 if node.dest.len > 0 {
242 r.sb.write_string(' (${node.dest})')
243 }
244}
245
246fn (mut r PlainTextRenderer) render_raw_html(node &Node) {
247 if r.opts.renderer_opts.unsafe_ {
248 r.sb.write_string(node.literal)
249 }
250}
251
252fn (mut r PlainTextRenderer) render_task_checkbox(node &Node) {
253 r.sb.write_string(if node.checked { '☑' } else { '☐' })
254}
255
256fn (mut r PlainTextRenderer) render_footnote_ref(node &Node) {
257 label := node.fn_label
258 mut idx := 0
259 for i, l in r.fn_order {
260 if l == label {
261 idx = i + 1
262 break
263 }
264 }
265 if idx == 0 {
266 r.fn_order << label
267 idx = r.fn_order.len
268 }
269 r.sb.write_string('[${idx}]')
270}
271
272fn (mut r PlainTextRenderer) render_footnotes_section() {
273 r.sb.write_string('\nFootnotes:\n')
274 for label in r.fn_order {
275 fn_node := r.fn_nodes[label] or { continue }
276 mut idx := 0
277 for i, l in r.fn_order {
278 if l == label {
279 idx = i + 1
280 break
281 }
282 }
283 r.sb.write_string('[${idx}] ')
284 r.render_inline(fn_node.literal)
285 r.sb.write_string('\n')
286 }
287}
288