///|
/// Everything the rendering functions need to read.
priv struct DocCtx {
pal : Palette
color : Bool
defs : Map[String, @cmark.LabelDef]
}
///|
/// Columns left for content once `indent` columns are taken by decoration.
fn remaining(width : Int32, indent : Int32) -> Int32 {
if width > indent + 4 {
width - indent
} else {
4
}
}
///|
fn hyperlink(url : String, text : String) -> String {
"\u001b]8;;\{url}\u001b\\\{text}\u001b]8;;\u001b\\"
}
///|
fn render_blocks(
ctx : DocCtx,
blocks : Array[@cmark.Block],
width : Int32,
tight : Bool,
) -> String {
let parts : Array[String] = []
for b in blocks {
let text = render_block(ctx, b, width)
if text != "" {
parts.push(text)
}
}
parts.join(if tight { "\n" } else { "\n\n" })
}
///|
/// Render a block that is really a container: `Blocks` is unwrapped so that the
/// separator between children follows `tight`.
fn render_block_seq(
ctx : DocCtx,
b : @cmark.Block,
width : Int32,
tight : Bool,
) -> String {
match b {
@cmark.Blocks(bs) => render_blocks(ctx, bs.v.to_array(), width, tight)
_ => render_block(ctx, b, width)
}
}
///|
fn render_block(ctx : DocCtx, b : @cmark.Block, width : Int32) -> String {
match b {
@cmark.Blocks(bs) => render_blocks(ctx, bs.v.to_array(), width, false)
@cmark.Heading(h) => render_heading(ctx, h.v, width)
@cmark.Paragraph(p) => render_paragraph(ctx, p.v, width)
@cmark.BlockQuote(q) => render_quote(ctx, q.v, width)
@cmark.List(l) => render_list(ctx, l.v, width)
@cmark.CodeBlock(cb) => render_code_block(ctx, cb.v)
@cmark.ExtMathBlock(cb) => render_code_block(ctx, cb.v)
@cmark.ThematicBreak(_) => render_rule(ctx, width)
@cmark.ExtTable(t) => render_table(ctx, t.v, width)
@cmark.HtmlBlock(lines) => {
let parts : Array[String] = []
for line in lines.v.0 {
parts.push(paint(line.v, ctx.pal.dim))
}
parts.join("\n")
}
@cmark.ExtFootnoteDefinition(f) => render_footnote(ctx, f.v, width)
@cmark.BlankLine(_) => ""
@cmark.LinkRefDefinition(_) => ""
}
}
///|
fn render_heading(
ctx : DocCtx,
h : @cmark.BlockHeading,
width : Int32,
) -> String {
let level = if h.level < 1 { 1 } else if h.level > 6 { 6 } else { h.level }
let indent = level + 1
let w = Wrap::new(remaining(width, indent))
render_inline(ctx, h.inline, w, ctx.pal.heading[level - 1])
let marker = paint(repeat("#", level) + " ", ctx.pal.heading_marker)
indent_lines(w.finish(), marker, repeat(" ", indent))
}
///|
fn render_paragraph(
ctx : DocCtx,
p : @cmark.BlockParagraph,
width : Int32,
) -> String {
let w = Wrap::new(width)
render_inline(ctx, p.inline, w, plain_style())
w.finish()
}
///|
fn render_quote(ctx : DocCtx, q : @cmark.BlockQuote, width : Int32) -> String {
let bar = paint("│ ", ctx.pal.quote_bar)
indent_lines(
render_block_seq(ctx, q.block, remaining(width, 2), false),
bar,
bar,
)
}
///|
fn render_list(ctx : DocCtx, l : @cmark.BlockList, width : Int32) -> String {
let parts : Array[String] = []
let mut index = match l.ty {
@cmark_base.Ordered(start, _) => start
@cmark_base.Unordered(_) => 1
}
for item in l.items {
let marker = match l.ty {
@cmark_base.Ordered(_, _) => "\{index}. "
@cmark_base.Unordered(delim) => "\{delim} "
}
let task = match item.v.ext_task_marker {
None => ""
Some(mark) =>
match @cmark.ListTaskStatus::from_marker(mark.v) {
@cmark.Checked => "[x] "
@cmark.Cancelled => "[-] "
@cmark.Unchecked | @cmark.Other(_) => "[ ] "
}
}
let head_width = display_width(marker) + display_width(task)
let prefix = paint(marker, ctx.pal.list_marker) + paint(task, ctx.pal.dim)
let body = render_block_seq(
ctx,
item.v.block,
remaining(width, head_width),
l.tight,
)
parts.push(indent_lines(body, prefix, repeat(" ", head_width)))
index = index + 1
}
parts.join(if l.tight { "\n" } else { "\n\n" })
}
///|
/// Code is emitted verbatim: long lines are left for the terminal to fold.
fn render_code_block(ctx : DocCtx, cb : @cmark.CodeBlock) -> String {
let parts : Array[String] = []
let lang = match cb.info_string {
None => ""
Some(info) =>
match @cmark.CodeBlock::language_of_info_string(info.v) {
Some((lang, _)) => lang
None => ""
}
}
if lang != "" {
parts.push(paint(lang, ctx.pal.dim))
}
let bar = paint("│ ", ctx.pal.code_bar)
for line in cb.code {
parts.push(bar + paint(line.v, ctx.pal.code_text))
}
parts.join("\n")
}
///|
fn render_rule(ctx : DocCtx, width : Int32) -> String {
let n = if width > 0 { width } else { 72 }
paint(repeat("─", n), ctx.pal.rule)
}
///|
fn render_footnote(ctx : DocCtx, f : @cmark.Footnote, width : Int32) -> String {
let label = paint("[\{f.label.key}]", ctx.pal.dim)
let body = render_block_seq(ctx, f.block, remaining(width, 4), false)
label + "\n" + indent_lines(body, " ", " ")
}
///|
fn render_inline(
ctx : DocCtx,
i : @cmark.Inline,
w : Wrap,
base : Style,
) -> Unit {
match i {
@cmark.Text(t) => w.text(t.v, base)
@cmark.Inlines(inlines) =>
for x in inlines.v {
render_inline(ctx, x, w, base)
}
@cmark.CodeSpan(cs) => w.text(cs.v.code(), base.then(ctx.pal.code_span))
@cmark.Emphasis(e) =>
render_inline(ctx, e.v.inline, w, base.then(ctx.pal.emphasis))
@cmark.StrongEmphasis(e) =>
render_inline(ctx, e.v.inline, w, base.then(ctx.pal.strong))
@cmark.ExtStrikethrough(s) =>
render_inline(ctx, s.v.0, w, base.then(ctx.pal.strike))
@cmark.Link(l) => render_link(ctx, l.v, w, base)
@cmark.Image(l) => render_image(ctx, l.v, w, base)
@cmark.Autolink(a) => render_autolink(ctx, a.v, w, base)
@cmark.Break(b) =>
match b.v.ty {
@cmark.Hard => w.hard_break()
@cmark.Soft => w.space()
}
@cmark.ExtMathSpan(ms) =>
w.text("$\{ms.v.tex()}$", base.then(ctx.pal.code_span))
@cmark.RawHtml(_) => ()
}
}
///|
/// Destination of a link or image, resolving reference-style links against the
/// document's definitions. Unsafe destinations are dropped.
fn link_dest(ctx : DocCtx, l : @cmark.InlineLink) -> String? {
let def = match l.reference {
@cmark.Inline(def) => Some(def)
@cmark.Ref(_, _, _) =>
match l.reference_definition(ctx.defs) {
Some(@cmark.LinkDef(def)) => Some(def)
_ => None
}
}
match def {
None => None
Some(def) =>
match def.v.dest {
None => None
Some(dest) =>
if @cmark.InlineLink::is_unsafe(dest.v) {
None
} else {
Some(dest.v)
}
}
}
}
///|
fn link_text(ctx : DocCtx, text : String, dest : String?) -> String {
match dest {
Some(url) => if ctx.color { hyperlink(url, text) } else { text }
None => text
}
}
///|
/// Key of the footnote a link refers to, when it is a footnote reference.
///
/// Footnote references are labels rather than prose, so they print as `[^key]`,
/// which is also how their definition is labelled.
fn footnote_key(ctx : DocCtx, l : @cmark.InlineLink) -> String? {
match l.referenced_label() {
None => None
Some(label) =>
if !label.key.has_prefix("^") {
None
} else {
match l.reference_definition(ctx.defs) {
Some(@cmark.LinkDef(_)) => None
_ => Some(label.key)
}
}
}
}
///|
fn render_link(
ctx : DocCtx,
l : @cmark.InlineLink,
w : Wrap,
base : Style,
) -> Unit {
match footnote_key(ctx, l) {
Some(key) => {
let text = paint("[\{key}]", base.then(ctx.pal.dim))
w.push(text, display_width(text))
}
None => {
let inner = Wrap::new(0)
render_inline(ctx, l.text, inner, base.then(ctx.pal.link))
let text = inner.finish()
let width = display_width(text)
w.push(link_text(ctx, text, link_dest(ctx, l)), width)
}
}
}
///|
fn render_image(
ctx : DocCtx,
l : @cmark.InlineLink,
w : Wrap,
base : Style,
) -> Unit {
let inner = Wrap::new(0)
render_inline(ctx, l.text, inner, base.then(ctx.pal.image))
let alt = inner.finish()
let label = if display_width(alt) == 0 {
paint("image", ctx.pal.image)
} else {
alt
}
let shown = match link_dest(ctx, l) {
Some(url) => label + " " + paint("(\{url})", ctx.pal.dim)
None => label
}
w.push(shown, display_width(shown))
}
///|
fn render_autolink(
ctx : DocCtx,
a : @cmark.InlineAutolink,
w : Wrap,
base : Style,
) -> Unit {
let url = a.link.v
let text = paint(url, base.then(ctx.pal.link))
let target = if a.is_email { "mailto:\{url}" } else { url }
w.push(link_text(ctx, text, Some(target)), display_width(url))
}
///|
fn pad_to(s : String, width : Int32, align : @cmark.TableAlign?) -> String {
let w = display_width(s)
if w >= width {
return s
}
let pad = width - w
match align {
Some(@cmark.Right) => repeat(" ", pad) + s
Some(@cmark.Center) => repeat(" ", pad / 2) + s + repeat(" ", pad - pad / 2)
_ => s + repeat(" ", pad)
}
}
///|
fn cell_texts(
ctx : DocCtx,
cells : Array[(@cmark.Inline, @cmark.TableCellLayout)],
ncols : Int32,
) -> Array[String] {
let out : Array[String] = []
for cell in cells {
let w = Wrap::new(0)
render_inline(ctx, cell.0, w, plain_style())
out.push(w.finish().replace_all(old="\n", new=" "))
}
while out.length() < ncols {
out.push("")
}
out
}
///|
fn table_rule(
widths : Array[Int32],
left : String,
mid : String,
right : String,
) -> String {
let parts : Array[String] = []
for w in widths {
parts.push(repeat("─", w + 2))
}
left + parts.join(mid) + right
}
///|
fn table_row(
ctx : DocCtx,
cells : Array[String],
widths : Array[Int32],
aligns : Array[@cmark.TableAlign?],
) -> String {
let parts : Array[String] = []
for i, w in widths {
let cell = if i < cells.length() {
truncate_visible(cells[i], w)
} else {
""
}
let align = if i < aligns.length() { aligns[i] } else { None }
parts.push(pad_to(cell, w, align))
}
let bar = paint("│", ctx.pal.border)
bar + " " + parts.join(" " + bar + " ") + " " + bar
}
///|
/// Tables are laid out in a box. Columns keep their natural width until the
/// table no longer fits `width`, at which point every column is capped and
/// cells are truncated with an ellipsis.
fn render_table(ctx : DocCtx, t : @cmark.Table, width : Int32) -> String {
let ncols = t.col_count
if ncols <= 0 {
return ""
}
let rows : Array[(Array[String], Bool)] = []
let aligns : Array[@cmark.TableAlign?] = []
let mut col = 0
while col < ncols {
aligns.push(None)
col = col + 1
}
for entry in t.rows {
match entry.0.v {
@cmark.Header(cells) =>
rows.push((cell_texts(ctx, cells.to_array(), ncols), true))
@cmark.Data(cells) =>
rows.push((cell_texts(ctx, cells.to_array(), ncols), false))
@cmark.Sep(seps) => {
let mut i = 0
for sep in seps {
if i < ncols {
aligns[i] = sep.v.0.0
}
i = i + 1
}
}
}
}
if rows.is_empty() {
return ""
}
let widths : Array[Int32] = []
let mut col = 0
while col < ncols {
widths.push(0)
col = col + 1
}
for row in rows {
for i, w in widths {
if i < row.0.length() {
let cell = display_width(row.0[i])
if cell > w {
widths[i] = cell
}
}
}
}
let budget = if width > ncols * 4 + ncols + 1 {
width - (ncols + 1)
} else {
ncols * 4
}
let cap = budget / ncols
let mut col = 0
while col < ncols {
if widths[col] > cap {
widths[col] = cap
}
col = col + 1
}
let parts : Array[String] = []
parts.push(paint(table_rule(widths, "┌", "┬", "┐"), ctx.pal.border))
for i, row in rows {
parts.push(table_row(ctx, row.0, widths, aligns))
if row.1 && i + 1 < rows.length() {
parts.push(paint(table_rule(widths, "├", "┼", "┤"), ctx.pal.border))
}
}
parts.push(paint(table_rule(widths, "└", "┴", "┘"), ctx.pal.border))
parts.join("\n")
}