288 lines
7.0 KiB
V
288 lines
7.0 KiB
V
// 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
|
|
|
|
import strings
|
|
|
|
// PlainTextRenderer renders a parsed markdown AST to UTF-8 console-friendly text.
|
|
struct PlainTextRenderer {
|
|
opts Options
|
|
ref_map map[string]LinkRef
|
|
mut:
|
|
sb strings.Builder
|
|
// footnote tracking
|
|
fn_order []string
|
|
fn_nodes map[string]&Node
|
|
// list tracking
|
|
list_depth int
|
|
list_nums []int
|
|
}
|
|
|
|
// render renders the document node to plain text.
|
|
pub fn (mut r PlainTextRenderer) render(doc &Node) string {
|
|
r.sb = strings.new_builder(1024)
|
|
if r.opts.footnotes {
|
|
for child in doc.children {
|
|
if child.kind == .footnote_def {
|
|
r.fn_nodes[child.fn_label] = child
|
|
}
|
|
}
|
|
}
|
|
r.render_children(doc)
|
|
if r.opts.footnotes && r.fn_order.len > 0 {
|
|
r.render_footnotes_section()
|
|
}
|
|
return r.sb.str().trim_right('\n') + '\n'
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_node(node &Node) {
|
|
match node.kind {
|
|
.document { r.render_children(node) }
|
|
.heading { r.render_heading(node) }
|
|
.paragraph { r.render_paragraph(node) }
|
|
.blockquote { r.render_blockquote(node) }
|
|
.list { r.render_list(node) }
|
|
.list_item { r.render_list_item(node) }
|
|
.code_block, .fenced_code { r.render_code_block(node) }
|
|
.thematic_break { r.sb.write_string('---\n') }
|
|
.html_block { r.render_html_block(node) }
|
|
.link_ref_def, .footnote_def {}
|
|
.table { r.render_table(node) }
|
|
.table_head, .table_body { r.render_children(node) }
|
|
.table_row { r.render_table_row(node) }
|
|
.table_cell { r.render_table_cell(node) }
|
|
.definition_list { r.render_children(node) }
|
|
.definition_term { r.render_definition_term(node) }
|
|
.definition_desc { r.render_definition_desc(node) }
|
|
.text { r.render_text(node) }
|
|
.emphasis { r.render_wrapped(node, '*') }
|
|
.strong { r.render_wrapped(node, '**') }
|
|
.code_span { r.sb.write_string('`' + node.literal + '`') }
|
|
.link { r.render_link(node) }
|
|
.image { r.render_image(node) }
|
|
.autolink { r.sb.write_string(node.literal) }
|
|
.raw_html { r.render_raw_html(node) }
|
|
.hard_break, .soft_break { r.sb.write_string('\n') }
|
|
.strikethrough { r.render_wrapped(node, '~~') }
|
|
.task_checkbox { r.render_task_checkbox(node) }
|
|
.footnote_ref { r.render_footnote_ref(node) }
|
|
}
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_children(node &Node) {
|
|
for child in node.children {
|
|
r.render_node(child)
|
|
}
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_inline(src string) {
|
|
nodes := parse_inline(src, r.opts, r.ref_map)
|
|
for node in nodes {
|
|
r.render_node(node)
|
|
}
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_heading(node &Node) {
|
|
r.sb.write_string('${'#'.repeat(node.level)} ')
|
|
if node.children.len > 0 {
|
|
r.render_children(node)
|
|
} else {
|
|
r.render_inline(node.literal)
|
|
}
|
|
r.sb.write_string('\n\n')
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_paragraph(node &Node) {
|
|
if node.children.len > 0 {
|
|
r.render_children(node)
|
|
} else {
|
|
r.render_inline(node.literal)
|
|
}
|
|
r.sb.write_string('\n')
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_blockquote(node &Node) {
|
|
mut inner := strings.new_builder(128)
|
|
mut rr := PlainTextRenderer{
|
|
opts: r.opts
|
|
ref_map: r.ref_map
|
|
fn_order: r.fn_order.clone()
|
|
fn_nodes: r.fn_nodes
|
|
}
|
|
rr.sb = inner
|
|
rr.render_children(node)
|
|
for line in rr.sb.str().trim_right('\n').split('\n') {
|
|
r.sb.write_string('> ${line}\n')
|
|
}
|
|
// Keep footnote reference order discovered inside the blockquote.
|
|
r.fn_order = rr.fn_order
|
|
r.sb.write_string('\n')
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_list(node &Node) {
|
|
r.list_depth++
|
|
if node.is_ordered {
|
|
r.list_nums << node.list_start
|
|
} else {
|
|
r.list_nums << 0
|
|
}
|
|
r.render_children(node)
|
|
r.list_nums.delete_last()
|
|
r.list_depth--
|
|
if r.list_depth == 0 {
|
|
r.sb.write_string('\n')
|
|
}
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_list_item(node &Node) {
|
|
indent := ' '.repeat(if r.list_depth > 0 { r.list_depth - 1 } else { 0 })
|
|
idx := r.list_nums.len - 1
|
|
marker := if idx >= 0 && r.list_nums[idx] > 0 {
|
|
m := '${r.list_nums[idx]}. '
|
|
r.list_nums[idx]++
|
|
m
|
|
} else {
|
|
'- '
|
|
}
|
|
r.sb.write_string(indent + marker)
|
|
for i, child in node.children {
|
|
if i > 0 {
|
|
r.sb.write_string(' ')
|
|
}
|
|
if child.kind == .paragraph {
|
|
if child.children.len > 0 {
|
|
r.render_children(child)
|
|
} else {
|
|
r.render_inline(child.literal)
|
|
}
|
|
} else if child.kind == .list {
|
|
r.sb.write_string('\n')
|
|
r.render_node(child)
|
|
} else {
|
|
r.render_node(child)
|
|
}
|
|
}
|
|
r.sb.write_string('\n')
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_code_block(node &Node) {
|
|
r.sb.write_string('```\n')
|
|
r.sb.write_string(node.literal.trim_right('\n'))
|
|
r.sb.write_string('\n```\n\n')
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_html_block(node &Node) {
|
|
if r.opts.renderer_opts.unsafe_ {
|
|
r.sb.write_string(node.literal)
|
|
} else {
|
|
r.sb.write_string('[raw HTML omitted]\n')
|
|
}
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_table(node &Node) {
|
|
r.render_children(node)
|
|
r.sb.write_string('\n')
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_table_row(node &Node) {
|
|
r.render_children(node)
|
|
r.sb.write_string('\n')
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_table_cell(node &Node) {
|
|
r.sb.write_string(node.literal.trim_space())
|
|
r.sb.write_string(' | ')
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_definition_term(node &Node) {
|
|
r.render_inline(node.literal)
|
|
r.sb.write_string('\n')
|
|
for child in node.children {
|
|
r.render_node(child)
|
|
}
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_definition_desc(node &Node) {
|
|
r.sb.write_string(' - ')
|
|
r.render_inline(node.literal)
|
|
r.sb.write_string('\n')
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_text(node &Node) {
|
|
content := if r.opts.typographer {
|
|
smart_punctuate(node.literal)
|
|
} else {
|
|
node.literal
|
|
}
|
|
r.sb.write_string(content)
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_wrapped(node &Node, marker string) {
|
|
r.sb.write_string(marker)
|
|
r.render_children(node)
|
|
r.sb.write_string(marker)
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_link(node &Node) {
|
|
r.render_children(node)
|
|
if node.dest.len > 0 {
|
|
r.sb.write_string(' (${node.dest})')
|
|
}
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_image(node &Node) {
|
|
alt := node.text_content().trim_space()
|
|
if alt.len == 0 {
|
|
r.sb.write_string('[image]')
|
|
} else {
|
|
r.sb.write_string('[image: ${alt}]')
|
|
}
|
|
if node.dest.len > 0 {
|
|
r.sb.write_string(' (${node.dest})')
|
|
}
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_raw_html(node &Node) {
|
|
if r.opts.renderer_opts.unsafe_ {
|
|
r.sb.write_string(node.literal)
|
|
}
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_task_checkbox(node &Node) {
|
|
r.sb.write_string(if node.checked { '☑' } else { '☐' })
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_footnote_ref(node &Node) {
|
|
label := node.fn_label
|
|
mut idx := 0
|
|
for i, l in r.fn_order {
|
|
if l == label {
|
|
idx = i + 1
|
|
break
|
|
}
|
|
}
|
|
if idx == 0 {
|
|
r.fn_order << label
|
|
idx = r.fn_order.len
|
|
}
|
|
r.sb.write_string('[${idx}]')
|
|
}
|
|
|
|
fn (mut r PlainTextRenderer) render_footnotes_section() {
|
|
r.sb.write_string('\nFootnotes:\n')
|
|
for label in r.fn_order {
|
|
fn_node := r.fn_nodes[label] or { continue }
|
|
mut idx := 0
|
|
for i, l in r.fn_order {
|
|
if l == label {
|
|
idx = i + 1
|
|
break
|
|
}
|
|
}
|
|
r.sb.write_string('[${idx}] ')
|
|
r.render_inline(fn_node.literal)
|
|
r.sb.write_string('\n')
|
|
}
|
|
}
|