///|
fn cd_cell(body : Array[ParseNode]) -> ParseNode {
Styling(mode=Math, body~, style=DisplayStyle, reset_font=true)
}
///|
fn cd_arrow_character(node : ParseNode) -> String? {
match node {
MathOrd(text~, ..) | TextOrd(text~, ..) | Atom(text~, ..) => Some(text)
_ => None
}
}
///|
fn cd_label_end(node : ParseNode, end : String) -> Bool {
match node {
MathOrd(text~, ..) | Atom(text~, ..) => text == end
_ => false
}
}
///|
fn cd_empty_label() -> ParseNode {
OrdGroup(mode=Math, loc=None, body=[], semisimple=false)
}
///|
fn cd_arrow(
arrow : String,
upper : ParseNode,
lower : ParseNode,
) -> ParseNode raise ParseFailure {
match arrow {
">" =>
XArrow(mode=Math, label="\\\\cdrightarrow", body=upper, below=Some(lower))
"<" =>
XArrow(mode=Math, label="\\\\cdleftarrow", body=upper, below=Some(lower))
"=" =>
XArrow(
mode=Math,
label="\\\\cdlongequal",
body=cd_empty_label(),
below=None,
)
"|" => DelimSizing(mode=Math, size=2, mclass=Mord, delim="\\Vert")
"." => TextOrd(mode=Math, loc=None, text=" ")
"A" | "V" => {
let direction = if arrow == "A" { "\\uparrow" } else { "\\downarrow" }
CdParent(
mode=Math,
fragment=OrdGroup(
mode=Math,
loc=None,
body=[
CdLabel(mode=Math, side="left", label=upper),
DelimSizing(mode=Math, size=2, mclass=Mord, delim=direction),
CdLabel(mode=Math, side="right", label=lower),
],
semisimple=false,
),
)
}
_ =>
raise InvalidArgument(
message="Expected one of \"<>AV=|.\" after @",
loc=None,
)
}
}
///|
/// Scans the label above/below a CD arrow: all nodes until the arrow
/// character (exclusive). Raises when the arrow character never appears or an
/// `@` interrupts the label.
fn scan_cd_label(
nodes : Array[ParseNode],
index : Int,
arrow : String,
) -> (Array[ParseNode], Int) raise ParseFailure {
let label : Array[ParseNode] = []
let mut index = index
while index < nodes.length() {
let current = nodes[index]
if cd_label_end(current, arrow) {
return (label, index + 1)
}
if current is TextOrd(text="@", ..) {
raise InvalidArgument(
message="Missing a \{arrow} character to complete a CD arrow.",
loc=None,
)
}
label.push(current)
index = index + 1
}
raise InvalidArgument(
message="Missing a \{arrow} character to complete a CD arrow.",
loc=None,
)
}
///|
/// Splits one parsed CD row into cells: `@` starts an arrow cell (arrow
/// character, optional labels above/below), everything else accumulates into
/// the current node cell. On even rows the leading blank cell (the empty
/// corner before the first arrow) is dropped; on odd rows the trailing cell
/// is kept to preserve the column grid.
fn cd_row(
nodes : Array[ParseNode],
even : Bool,
) -> Array[ParseNode] raise ParseFailure {
let row : Array[ParseNode] = []
let cell : Array[ParseNode] = []
let mut index = 0
while index < nodes.length() {
let node = nodes[index]
if node is TextOrd(text="@", ..) {
row.push(cd_cell(cell.copy()))
cell.clear()
index = index + 1
guard nodes.get(index) is Some(character_node) else {
raise InvalidArgument(
message="Expected one of \"<>AV=|.\" after @",
loc=None,
)
}
guard cd_arrow_character(character_node) is Some(arrow) else {
raise InvalidArgument(
message="Expected one of \"<>AV=|.\" after @",
loc=None,
)
}
index = index + 1
let labels : Array[ParseNode] = []
if arrow == ">" || arrow == "<" || arrow == "A" || arrow == "V" {
for _ in 0..<2 {
let (label_body, next_index) = scan_cd_label(nodes, index, arrow)
index = next_index
labels.push(
OrdGroup(mode=Math, loc=None, body=label_body, semisimple=false),
)
}
} else if arrow != "=" && arrow != "|" && arrow != "." {
raise InvalidArgument(
message="Expected one of \"<>AV=|.\" after @",
loc=None,
)
} else {
labels.push(cd_empty_label())
labels.push(cd_empty_label())
}
row.push(cd_cell([cd_arrow(arrow, labels[0], labels[1])]))
} else {
cell.push(node)
index = index + 1
}
}
if even {
row.push(cd_cell(cell.copy()))
} else if !row.is_empty() {
let _ = row.remove(0)
}
row
}
///|
fn Parser::parse_cd_environment(self : Parser) -> ParseNode raise ParseFailure {
self.gullet.begin_group()
self.gullet.macros.set("\\cr", Some(MacroDefinition::text("\\\\\\relax")))
self.gullet.begin_group()
let result : Result[ParseNode, ParseFailure] = capture_parse_result(() => {
let parsed_rows : Array[Array[ParseNode]] = [[]]
for ;; {
let part = self.parse_expression(false, Some("\\\\"))
guard parsed_rows.last() is Some(row) else {
raise InternalInvariant(message="Missing CD row")
}
row.append(part)
match self.fetch().text {
"&" => self.consume()
"\\\\" => {
self.consume()
parsed_rows.push([])
}
"\\end" => break
token =>
raise InvalidArgument(
message="Expected \\ or \\end, got \{token}",
loc=None,
)
}
}
if parsed_rows.last() is Some([]) {
let _ = parsed_rows.pop()
}
let body : Array[Array[ParseNode]] = []
for index, row in parsed_rows {
body.push(cd_row(row, index % 2 == 0))
}
let count = body.get(0).map_or(0, row => row.length())
let columns = Array::make(
count,
AlignColumn(alignment="c", pre_gap=0.25, post_gap=0.25),
)
let row_gap_count = body.length() + 1
let hlines_before_row = Array::makei(row_gap_count, _ => [])
Array(
mode=Math,
body~,
add_jot=true,
array_stretch=1.0,
columns=Some(columns),
row_gaps=[None],
hskip_before_and_after=false,
hlines_before_row~,
column_separation_type=Some(CdSeparation),
tags=None,
auto_tags=None,
leqno=false,
)
})
let close_cell : Result[Unit, ParseFailure] = capture_parse_result(() => {
self.gullet.end_group()
})
let close_array : Result[Unit, ParseFailure] = capture_parse_result(() => {
self.gullet.end_group()
})
unwrap_array_parse_result(result, close_cell, close_array)
}
///|
fn cd_environment_handler(
context : EnvironmentContext,
_ : Array[ParseNode],
_ : Array[ParseNode?],
) -> ParseNode raise ParseFailure {
guard context.display_mode else {
raise InvalidArgument(
message="{CD} can be used only in display mode.",
loc=None,
)
}
(context.parse_cd)()
}