///|
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,
)
}
}
///|
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] = [cd_empty_label(), cd_empty_label()]
if arrow == ">" || arrow == "<" || arrow == "A" || arrow == "V" {
for label_index in 0..<2 {
let label_body : Array[ParseNode] = []
let mut complete = false
while index < nodes.length() {
let current = nodes[index]
if cd_label_end(current, arrow) {
complete = true
index = index + 1
break
}
if current is TextOrd(text="@", ..) {
raise InvalidArgument(
message="Missing a " +
arrow +
" character to complete a CD arrow.",
loc=None,
)
}
label_body.push(current)
index = index + 1
}
guard complete else {
raise InvalidArgument(
message="Missing a " +
arrow +
" character to complete a CD arrow.",
loc=None,
)
}
labels[label_index] = 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,
)
}
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()
})
match (result, close_cell, close_array) {
(Err(err), _, _) => raise err
(_, Err(err), _) => raise err
(_, _, Err(err)) => raise err
(Ok(node), Ok(_), Ok(_)) => node
}
}
///|
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)()
}