///|
/// A markdown block.
pub(all) enum Block {
BlankLine(Node[BlockBlankLine])
BlockQuote(Node[BlockQuote])
Blocks(Node[Seq[Block]])
CodeBlock(Node[CodeBlock])
Heading(Node[BlockHeading])
HtmlBlock(Node[HtmlBlock])
LinkRefDefinition(Node[LinkDefinition])
List(Node[BlockList])
Paragraph(Node[BlockParagraph])
ThematicBreak(Node[BlockThematicBreak])
ExtMathBlock(Node[CodeBlock])
ExtTable(Node[Table])
ExtFootnoteDefinition(Node[Footnote])
} derive(Debug)
///|
#deprecated
#doc(hidden)
pub extend Block with @debug.Debug::{to_repr}
///|
pub fn Block::empty() -> Block {
Blocks({ v: [], meta: Meta::none(), })
}
///|
pub fn Block::meta(self : Block) -> Meta {
match self {
BlankLine(bl) => bl.meta
BlockQuote(bl) => bl.meta
Blocks(bl) => bl.meta
CodeBlock(bl) => bl.meta
Heading(bl) => bl.meta
HtmlBlock(bl) => bl.meta
LinkRefDefinition(bl) => bl.meta
List(bl) => bl.meta
Paragraph(bl) => bl.meta
ThematicBreak(bl) => bl.meta
ExtFootnoteDefinition(bl) => bl.meta
ExtTable(bl) => bl.meta
ExtMathBlock(bl) => bl.meta
}
}
///|
/// Normalize a block.
///
/// `b.normalize()` has the same content as `b` but is such that for any
/// occurrence of `Blocks` in `b` the list of blocks `bs`:
/// - `bs` is not a singleton list.
/// - Has no `Blocks` case. The meta is dropped and the nested
/// blocks are spliced in `bs` where the case occurs.
pub fn Block::normalize(self : Block) -> Block {
match self {
BlockQuote(bl) => BlockQuote(bl.map(BlockQuote::normalize_block))
List(bl) => List(bl.map(BlockList::normalize_items))
Blocks({ v: Seq([.., _] as bs), meta, }) => {
let mut bs = bs
let acc = [bs.pop().unwrap().normalize()]
for ;; {
match bs.pop() {
Some(Blocks({ v: bs1, .. })) => bs += bs1.0
Some(b) => acc.push(b.normalize())
None => break
}
}
match acc {
[b] => b
_ => Blocks({ v: bs, meta, })
}
}
ExtFootnoteDefinition(bl) =>
ExtFootnoteDefinition(bl.map(Footnote::normalize_block))
_ => self
}
}
///|
/// Collects the given `Block`'s link definitions (e.g. `LinkRefDefinition`
/// and `ExtFootnoteDefinition`) and adds them to `init` (defaults to an empty map).
pub fn Block::defs(self : Block, init~ : LabelDefs = Map([])) -> LabelDefs {
match self {
BlockQuote(bl) => bl.v.block.defs(init~)
Blocks(bl) => bl.v.fold(init~, fn(init, b) { b.defs(init~) })
List(bl) => bl.v.items.fold(init~, fn(init, it) { it.v.block.defs(init~) })
LinkRefDefinition(bl) =>
bl.v.defined_label.map_or(init, fn(it) {
init.set(it.key, LinkDef(bl))
init
})
ExtFootnoteDefinition(footnote) =>
footnote.v.defined_label.map_or(init, fn(it) {
init.set(it.key, FootnoteDef(footnote))
init
})
_ => init
}
}
///|
/// The type for [blank lines](https://spec.commonmark.org/0.30/#blank-lines).
/// These can be ignored during rendering, they are kept for layout.
pub type BlockBlankLine = Blanks
///|
/// The type for [block quotes](https://spec.commonmark.org/0.30/#block-quotes).
pub(all) struct BlockQuote {
/// The indentation to the block quote marker found on the first line.
indent : Indent
/// The quoted block.
block : Block
} derive(Debug)
///|
#deprecated
#doc(hidden)
pub extend BlockQuote with @debug.Debug::{to_repr}
///|
pub fn BlockQuote::new(indent~ : Int32 = 0, block : Block) -> BlockQuote {
{ indent, block, }
}
///|
pub fn BlockQuote::map_block(
self : BlockQuote,
f : (Block) -> Block,
) -> BlockQuote {
{ ..self, block: f(self.block), }
}
///|
pub fn BlockQuote::normalize_block(self : BlockQuote) -> BlockQuote {
self.map_block(Block::normalize)
}
///|
/// The type for [indented](https://spec.commonmark.org/0.30/#indented-code-block) and
/// [fenced](:https://spec.commonmark.org/0.30/#fenced-code-blocks) code blocks.
pub(all) struct CodeBlock {
layout : CodeBlockLayout
info_string : StringNode?
code : Seq[StringNode]
} derive(Debug)
///|
#deprecated
#doc(hidden)
pub extend CodeBlock with @debug.Debug::{to_repr}
///|
pub(all) enum CodeBlockLayout {
Indented
Fenced(CodeBlockFencedLayout)
} derive(Debug)
///|
#deprecated
#doc(hidden)
pub extend CodeBlockLayout with @debug.Debug::{to_repr}
///|
pub(all) struct CodeBlockFencedLayout {
indent : Indent
opening_fence : StringNode
closing_fence : StringNode?
} derive(Debug)
///|
#deprecated
#doc(hidden)
pub extend CodeBlockFencedLayout with @debug.Debug::{to_repr}
///|
pub fn CodeBlockFencedLayout::default() -> CodeBlockFencedLayout {
{ indent: 0, opening_fence: layout_empty, closing_fence: Some(layout_empty), }
}
///|
pub fn CodeBlock::new(
layout~ : CodeBlockLayout = Fenced(CodeBlockFencedLayout::default()),
info_string~ : StringNode? = None,
code : Seq[StringNode],
) -> CodeBlock {
let layout = match (info_string, layout) {
(Some(_), Indented) =>
CodeBlockLayout::Fenced(CodeBlockFencedLayout::default())
(_, layout) => layout
}
{ layout, info_string, code, }
}
///|
/// [make_fence cb] is a fence character and count suitable for [cb].
pub fn CodeBlock::make_fence(self : CodeBlock) -> (Char, Count) {
let ch = if self.info_string is Some(info) && info.v.contains_char('`') {
'~'
} else {
'`'
}
let counts = @sorted_set.SortedSet([])
for n in self.code {
let c = n.v
let mut k = 0
while k < c.length() && c[k] == ch.to_int().to_uint16() {
// TODO(upstream): provide UInt16 ~ Char comparison
k += 1
}
if k != 0 {
counts.add(k)
}
}
(ch, InlineCodeSpan::min_backticks(min=3, counts))
}
///|
/// [language_of_info_string s] extracts a (non-empty) language, the first word of [s] and a trimmed remainder.
/// Assumes [s] is {!String.trim}ed which is what {!info_string} gives you.
pub fn CodeBlock::language_of_info_string(s : String) -> (String, String)? {
guard !s.is_empty() else { None }
let max = s.length() - 1
// ASCII whitespace is a single code unit, so scan code units: indexing
// chars with `get_char` aborts on the trailing half of a surrogate pair.
let white = for i = 0; ; i = i + 1 {
if i > max || (s[i].to_char() is Some(c) && @char.is_ascii_whitespace(c)) {
break i
}
}
let rem_first = @cmark_base.first_non_blank(s, last=max, start=white)
let buf = StringBuilder()
let lang = @char.utf_16_clean_raw(buf, s, first=0, last=white - 1)
guard !lang.is_empty() else { None }
Some((lang, @char.utf_16_clean_raw(buf, s, first=rem_first, last=max)))
}
///|
fn CodeBlock::is_math_block(s : String?) -> Bool {
guard s is Some(s) else { return false }
CodeBlock::language_of_info_string(s).map(fn(i) { i.0 }) == Some("math")
}
///|
/// The type for [ATX](https://spec.commonmark.org/0.30/#atx-headings)
/// and [Setext](https://spec.commonmark.org/0.30/#setext-headings) headings.
pub(all) struct BlockHeading {
layout : BlockHeadingLayout
level : Int32
inline : Inline
id : BlockHeadingId?
} derive(Debug)
///|
#deprecated
#doc(hidden)
pub extend BlockHeading with @debug.Debug::{to_repr}
///|
pub(all) enum BlockHeadingLayout {
Atx(BlockHeadingAtxLayout)
Setext(BlockHeadingSetextLayout)
} derive(Debug)
///|
#deprecated
#doc(hidden)
pub extend BlockHeadingLayout with @debug.Debug::{to_repr}
///|
pub(all) struct BlockHeadingAtxLayout {
indent : Indent
after_opening : Blanks
closing : String
} derive(Debug, FromJson, ToJson)
///|
#deprecated
#doc(hidden)
pub extend BlockHeadingAtxLayout with @debug.Debug::{to_repr}
///|
#deprecated
#doc(hidden)
pub extend BlockHeadingAtxLayout with @json.FromJson::{from_json}
///|
#deprecated
#doc(hidden)
pub extend BlockHeadingAtxLayout with ToJson::{to_json}
///|
pub fn BlockHeadingAtxLayout::default() -> BlockHeadingAtxLayout {
{ indent: 0, after_opening: "", closing: "", }
}
///|
pub(all) struct BlockHeadingSetextLayout {
leading_indent : Indent
trailing_blanks : Blanks
underline_indent : Indent
underline_count : Node[Count]
underline_blanks : Blanks
} derive(Debug)
///|
#deprecated
#doc(hidden)
pub extend BlockHeadingSetextLayout with @debug.Debug::{to_repr}
///|
pub(all) enum BlockHeadingId {
Auto(String)
Id(String)
} derive(Debug, FromJson, ToJson)
///|
#deprecated
#doc(hidden)
pub extend BlockHeadingId with @debug.Debug::{to_repr}
///|
#deprecated
#doc(hidden)
pub extend BlockHeadingId with @json.FromJson::{from_json}
///|
#deprecated
#doc(hidden)
pub extend BlockHeadingId with ToJson::{to_json}
///|
pub fn BlockHeading::new(
id~ : BlockHeadingId? = None,
layout~ : BlockHeadingLayout = Atx(BlockHeadingAtxLayout::default()),
level~ : Int32,
inline : Inline,
) -> BlockHeading {
{ layout, level, inline, id, }
}
///|
/// The type for [HTML blocks](https://spec.commonmark.org/0.30/#html-blocks).
pub(all) struct HtmlBlock(Seq[StringNode]) derive(Debug)
///|
#deprecated
#doc(hidden)
pub extend HtmlBlock with @debug.Debug::{to_repr}
///|
pub(all) struct ListItem {
before_marker : Indent
marker : StringNode
after_marker : Indent
block : Block
ext_task_marker : Node[Char]?
} derive(Debug)
///|
#deprecated
#doc(hidden)
pub extend ListItem with @debug.Debug::{to_repr}
///|
pub type ListItemBlock = ListItem
///|
pub fn ListItem::new(
before_marker~ : Indent = 0,
marker~ : StringNode = layout_empty,
after_marker~ : Indent = 1,
ext_task_marker~ : Node[Char]?,
block : Block,
) -> ListItem {
{ before_marker, marker, after_marker, block, ext_task_marker, }
}
///|
pub fn ListItem::map_block(self : ListItem, f : (Block) -> Block) -> ListItem {
{ ..self, block: f(self.block), }
}
///|
pub fn ListItem::normalize_block(self : ListItem) -> ListItem {
self.map_block(Block::normalize)
}
///|
pub(all) enum ListTaskStatus {
Cancelled
Checked
Unchecked
Other(Char)
}
///|
pub fn ListTaskStatus::from_marker(marker : Char) -> ListTaskStatus {
match marker {
' ' => Unchecked
'x' | 'X' | '✓' | '✔' | '𐄂' | '🗸' => Checked
'~' => Cancelled
_ => Other(marker)
}
}
///|
/// The type for [lists](https://spec.commonmark.org/0.30/#lists).
pub(all) struct BlockList {
ty : ListType
tight : Bool
items : Seq[Node[ListItem]]
} derive(Debug)
///|
#deprecated
#doc(hidden)
pub extend BlockList with @debug.Debug::{to_repr}
///|
pub fn BlockList::map_items(
self : BlockList,
f : (ListItem) -> ListItem,
) -> BlockList {
{ ..self, items: self.items.map(fn(it) { it.map(f) }), }
}
///|
pub fn BlockList::normalize_items(self : BlockList) -> BlockList {
self.map_items(ListItem::normalize_block)
}
///|
/// The type for [paragraphs](https://spec.commonmark.org/0.30/#paragraphs).
pub(all) struct BlockParagraph {
leading_indent : Indent
inline : Inline
trailing_blanks : Blanks
} derive(Debug)
///|
#deprecated
#doc(hidden)
pub extend BlockParagraph with @debug.Debug::{to_repr}
///|
pub fn BlockParagraph::new(
leading_indent~ : Indent = 0,
trailing_blanks~ : Blanks = "",
inline : Inline,
) -> BlockParagraph {
{ leading_indent, inline, trailing_blanks, }
}
///|
/// The type for [thematic breaks](https://spec.commonmark.org/0.30/#thematic-break).
pub(all) struct BlockThematicBreak {
indent : Indent
layout : String
} derive(Debug, FromJson, ToJson)
///|
#deprecated
#doc(hidden)
pub extend BlockThematicBreak with @debug.Debug::{to_repr}
///|
#deprecated
#doc(hidden)
pub extend BlockThematicBreak with @json.FromJson::{from_json}
///|
#deprecated
#doc(hidden)
pub extend BlockThematicBreak with ToJson::{to_json}
///|
pub fn BlockThematicBreak::new(
indent~ : Indent = 0,
layout~ : String = "---",
) -> BlockThematicBreak {
{ indent, layout, }
}
// Extensions
///|
pub(all) struct Footnote {
indent : Indent
label : Label
defined_label : Label?
block : Block
} derive(Debug)
///|
#deprecated
#doc(hidden)
pub extend Footnote with @debug.Debug::{to_repr}
///|
pub fn Footnote::new(
indent~ : Indent = 0,
defined_label? : Label?,
label : Label,
block : Block,
) -> Footnote {
let defined_label = defined_label.unwrap_or(Some(label))
{ indent, label, defined_label, block, }
}
///|
pub fn Footnote::map_block(self : Footnote, f : (Block) -> Block) -> Footnote {
{ ..self, block: f(self.block), }
}
///|
pub fn Footnote::normalize_block(self : Footnote) -> Footnote {
self.map_block(Block::normalize)
}
///|
fn Label::stub(label : Label, defined_label : Label?) -> LabelDef {
FootnoteDef(
Node::new({ indent: 0, label, defined_label, block: Block::empty(), }),
)
}
///|
pub(all) struct Table {
indent : Indent
col_count : Count
rows : Seq[(Node[TableRow], Blanks)]
} derive(Debug)
///|
#deprecated
#doc(hidden)
pub extend Table with @debug.Debug::{to_repr}
///|
pub(all) enum TableAlign {
Left
Center
Right
} derive(Debug, FromJson, ToJson)
///|
#deprecated
#doc(hidden)
pub extend TableAlign with @debug.Debug::{to_repr}
///|
#deprecated
#doc(hidden)
pub extend TableAlign with @json.FromJson::{from_json}
///|
#deprecated
#doc(hidden)
pub extend TableAlign with ToJson::{to_json}
///|
pub(all) enum TableRow {
Header(Seq[(Inline, TableCellLayout)])
Sep(Seq[Node[TableSep]])
Data(Seq[(Inline, TableCellLayout)])
} derive(Debug)
///|
#deprecated
#doc(hidden)
pub extend TableRow with @debug.Debug::{to_repr}
///|
pub(all) struct TableSep((TableAlign?, Count)) derive(Debug)
///|
#deprecated
#doc(hidden)
pub extend TableSep with @debug.Debug::{to_repr}
///|
pub(all) struct TableCellLayout((Blanks, Blanks)) derive(Debug, Eq, Compare)
///|
#deprecated
#doc(hidden)
pub extend TableCellLayout with @debug.Debug::{to_repr}
///|
#deprecated
#doc(hidden)
pub extend TableCellLayout with Eq::{not_equal, equal}
///|
#deprecated
#doc(hidden)
pub extend TableCellLayout with Compare::{op_lt, op_le, op_ge, compare, op_gt}
///|
pub fn Table::new(
indent~ : Indent = 0,
rows : Seq[(Node[TableRow], Blanks)],
) -> Table {
let mut col_count = 0
for row in rows {
match row.0.v {
Header(cols) | Data(cols) =>
col_count = @cmp.maximum(col_count, cols.length())
Sep(cols) => col_count = @cmp.maximum(col_count, cols.length())
}
}
{ indent, rows, col_count, }
}
///|
fn Table::parse_sep_row(
cs : ArrayView[(Inline, TableCellLayout)],
) -> Array[Node[TableSep]]? {
let acc : Array[_] = []
for cs = cs {
match cs {
[] => break Some(acc)
[(Text({ v, meta, }), TableCellLayout(("", ""))), .. cs] => {
guard !v.is_empty() else { break None }
let max = v.length() - 1
let first_colon = v[0] == ':'
let last_colon = v[max] == ':'
let first = if first_colon { 1 } else { 0 }
let last = if last_colon { max - 1 } else { max }
let all_dashes = for i in first..<=last {
guard v[i] == '-' else { break false }
} nobreak {
true
}
guard all_dashes else { break None }
let count = last - first + 1
let sep = match (first_colon, last_colon) {
(false, false) => None
(true, true) => Some(Center)
(true, false) => Some(Left)
(false, true) => Some(Right)
}
acc.push({ v: (sep, count), meta, })
continue cs
}
_ => break None
}
}
}