Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,287 @@
|
||||
// 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')
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user