///| Inline rendering for the literal (source-preserving) renderer.
///|
///| Split from renderer_literal.mbt to keep that file focused on the
///| document / block-level dispatch. Helpers (`LiteralOpts`,
///| `write_marker`, `write_image_preview_slot`, `repeat_char`,
///| `escape_html` …) live alongside in the same package.
///|
fn render_inlines_literal(
inlines : Array[Inline],
buf : StringBuilder,
opts : LiteralOpts,
) -> Unit {
for inline in inlines {
render_inline_literal(inline, buf, opts)
}
}
///|
/// Paragraph renderer variant that can attach a block preview after an
/// image URL that occupies a whole source line.
fn render_paragraph_inlines_literal(
inlines : Array[Inline],
buf : StringBuilder,
opts : LiteralOpts,
) -> Unit {
let len = inlines.length()
let mut at_line_start = true
for i = 0; i < len; i = i + 1 {
let inline = inlines[i]
match inline {
Inline::SoftBreak(..) | Inline::HardBreak(..) => {
render_inline_literal(inline, buf, opts)
at_line_start = true
}
Inline::Image(alt~, url~, title~, ..) =>
if opts.image_preview &&
at_line_start &&
is_line_end_after(inlines, i) &&
is_previewable_image_url(url) {
render_inline_literal(inline, buf, opts.without_image_preview())
write_image_preview_block_slot(buf, url, alt, title)
at_line_start = false
} else {
render_inline_literal(inline, buf, opts)
at_line_start = false
}
_ => {
render_inline_literal(inline, buf, opts)
at_line_start = false
}
}
}
}
///|
fn is_line_end_after(inlines : Array[Inline], index : Int) -> Bool {
if index + 1 >= inlines.length() {
return true
}
match inlines[index + 1] {
Inline::SoftBreak(..) | Inline::HardBreak(..) => true
_ => false
}
}
///|
fn render_inline_literal(
inline : Inline,
buf : StringBuilder,
opts : LiteralOpts,
) -> Unit {
// Inline spans come from the inline parser and are relative to the
// surrounding block's content, not the document. They are intentionally
// omitted from `data-src-*` annotations — the block-level ancestor's
// span is enough to recover precise source offsets via the visible-text
// invariant.
match inline {
Inline::Text(content~, ..) => escape_html_into(buf, content)
Inline::SoftBreak(..) => buf.write_char('\n')
Inline::HardBreak(..) => {
write_marker(buf, "\\")
buf.write_char('\n')
}
Inline::Emphasis(children~, ..) => {
buf.write_string("")
write_marker(buf, "*")
render_inlines_literal(children, buf, opts)
write_marker(buf, "*")
buf.write_string("")
}
Inline::Strong(children~, ..) => {
buf.write_string("")
write_marker(buf, "**")
render_inlines_literal(children, buf, opts)
write_marker(buf, "**")
buf.write_string("")
}
Inline::Strikethrough(children~, ..) => {
buf.write_string("")
write_marker(buf, "~~")
render_inlines_literal(children, buf, opts)
write_marker(buf, "~~")
buf.write_string("")
}
Inline::Code(content~, ..) => {
let backticks = calc_code_span_backticks(content)
let needs_padding = content.length() > 0 &&
({
let first = content.get_char(0)
let last = content.get_char(content.length() - 1)
first == Some('`') || last == Some('`')
})
buf.write_string("")
write_marker_repeat(buf, '`', backticks)
if needs_padding {
buf.write_char(' ')
}
escape_html_into(buf, content)
if needs_padding {
buf.write_char(' ')
}
write_marker_repeat(buf, '`', backticks)
buf.write_string("")
}
Inline::WikiLink(target~, label~, fragment~, ..) => {
let href = if fragment.is_empty() {
target
} else {
target + "#" + fragment
}
buf.write_string("")
write_marker(buf, "[[")
escape_html_into(buf, target)
if !fragment.is_empty() {
escape_html_into(buf, "#" + fragment)
}
if !label.is_empty() {
write_marker(buf, "|")
escape_html_into(buf, label)
}
write_marker(buf, "]]")
buf.write_string("")
}
Inline::Link(children~, url~, title~, ..) => {
buf.write_string("')
write_marker(buf, "[")
render_inlines_literal(children, buf, opts)
let trailing = if title.is_empty() {
"](" + url + ")"
} else {
"](" + url + " \"" + title + "\")"
}
write_marker(buf, trailing)
buf.write_string("")
}
Inline::RefLink(children~, label~, ..) => {
buf.write_string("")
write_marker(buf, "[")
render_inlines_literal(children, buf, opts)
write_marker(buf, "][" + label + "]")
buf.write_string("")
}
Inline::Autolink(url~, is_email~, ..) => {
let href = if is_email { "mailto:" + url } else { url }
buf.write_string("")
write_marker(buf, "<")
escape_html_into(buf, url)
write_marker(buf, ">")
buf.write_string("")
}
Inline::Image(alt~, url~, title~, ..) => {
let meta = parse_image_preview_alt(alt)
buf.write_string("")
write_marker(buf, ""
} else {
"](" + url + " \"" + title + "\")"
}
write_marker(buf, trailing)
if opts.image_preview {
write_image_preview_slot(buf, url, alt, title)
}
buf.write_string("")
}
Inline::RefImage(alt~, label~, ..) => {
// Reference images don't carry an URL inline; the slot stays empty.
// The consumer can resolve `data-md-image-ref` against their own
// definition map and assign a `src` later.
let meta = parse_image_preview_alt(alt)
buf.write_string("")
write_marker(buf, "![")
escape_html_into(buf, alt)
write_marker(buf, "][" + label + "]")
if opts.image_preview {
write_image_preview_slot_open(buf, meta)
buf.write_string("
")
buf.write_string("")
}
buf.write_string("")
}
Inline::HtmlInline(html~, ..) => {
buf.write_string("")
escape_html_into(buf, html)
buf.write_string("")
}
Inline::FootnoteReference(label~, ..) => {
// The `[^label]` source characters are the only content, but they
// sit inside an `aria-hidden` marker span — without an explicit
// `aria-label` the `` would be focusable yet nameless.
buf.write_string("")
}
}
}