///|
fn Parser::consume_array_hlines(
self : Parser,
) -> Array[Bool] raise ParseFailure {
let lines : Array[Bool] = []
self.consume_spaces()
while self.fetch().text == "\\hline" || self.fetch().text == "\\hdashline" {
let dashed = self.fetch().text == "\\hdashline"
self.consume()
lines.push(dashed)
self.consume_spaces()
}
lines
}
///|
fn Parser::take_array_tag(
self : Parser,
auto_tag : Bool?,
) -> (Array[ParseNode]?, Bool) raise ParseFailure {
guard auto_tag is Some(automatic) else { (None, false) }
guard self.gullet.macros.get("\\df@tag") is Some(_) else { (None, automatic) }
let tag = self.subparse([Token::make("\\df@tag")])
self.gullet.macros.set("\\df@tag", None, global=true)
(Some(tag), false)
}
///|
/// Records the tag of the finished row when the environment collects tags:
/// pushes the tag (or `None` when the row has no `\tag`) and whether the tag
/// was automatic.
fn Parser::push_array_tag(
self : Parser,
tags : Array[Array[ParseNode]?],
auto_tags : Array[Bool],
auto_tag : Bool?,
) -> Unit raise ParseFailure {
let (tag, automatic) = self.take_array_tag(auto_tag)
if auto_tag is Some(_) {
tags.push(tag)
auto_tags.push(automatic)
}
}
///|
/// Parses the optional `[height]` argument that may follow `\\` in an array
/// row. A space before the bracket means the gap is absent.
fn Parser::parse_array_row_gap(
self : Parser,
) -> Measurement? raise ParseFailure {
if self.gullet.future().text == " " {
None
} else {
match self.parse_size_group(true) {
Some(Size(value~, ..)) => Some(value)
Some(_) => raise InternalInvariant(message="Expected array row gap")
None => None
}
}
}
///|
/// True when the current row already holds `max_columns` cells, so the next
/// `&` would exceed the declared column limit.
fn array_row_at_max(body : Array[Array[ParseNode]], max_columns : Int?) -> Bool {
max_columns.map_or(false, maximum => {
body.last() is Some(row) && row.length() >= maximum
})
}
///|
/// Unwraps an array parse whose cleanup closes a cell group and an array
/// group, re-raising the first captured error.
fn unwrap_array_parse_result(
result : Result[ParseNode, ParseFailure],
close_cell : Result[Unit, ParseFailure],
close_array : Result[Unit, ParseFailure],
) -> ParseNode raise ParseFailure {
match (result, close_cell, close_array) {
(Err(err), _, _) => raise err
(_, Err(err), _) => raise err
(_, _, Err(err)) => raise err
(Ok(node), Ok(_), Ok(_)) => node
}
}
///|
/// Parses an array environment body: cells of the current row accumulate
/// until `&`, `\\`, or `\end`; `\\` takes an optional `[gap]` and the
/// `\hline`/`\hdashline` rules that open the next row; `\end` closes the
/// array, collecting the row's `\tag` when `auto_tag` is set. Row gaps, tags,
/// and the per-row hline list are collected alongside the body, and the
/// hline list is padded so it has one entry per row boundary.
fn Parser::parse_array_environment(
self : Parser,
options : ArrayEnvironmentOptions,
) -> 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 body : Array[Array[ParseNode]] = [[]]
let row_gaps : Array[Measurement?] = []
let hlines_before_row : Array[Array[Bool]] = [self.consume_array_hlines()]
let tags : Array[Array[ParseNode]?] = []
let auto_tags : Array[Bool] = []
for ;; {
let cell_body = self.parse_expression(false, Some("\\\\"))
let cell : ParseNode = Styling(
mode=self.mode,
body=[
OrdGroup(mode=self.mode, loc=None, body=cell_body, semisimple=false),
],
style=options.cell_style,
reset_font=true,
)
self.gullet.end_group()
self.gullet.begin_group()
guard body.last() is Some(row) else {
raise InternalInvariant(message="Missing array row")
}
row.push(cell)
match self.fetch().text {
"&" => {
guard !array_row_at_max(body, options.max_columns) else {
raise InvalidArgument(
message="Too many tab characters: &",
loc=None,
)
}
self.consume()
}
"\\end" => {
self.push_array_tag(tags, auto_tags, options.auto_tag)
break
}
"\\\\" => {
guard !options.single_row else {
raise InvalidArgument(message="Expected \\end", loc=None)
}
self.consume()
row_gaps.push(self.parse_array_row_gap())
self.push_array_tag(tags, auto_tags, options.auto_tag)
hlines_before_row.push(self.consume_array_hlines())
body.push([])
}
token =>
raise InvalidArgument(
message="Expected & or \\\\ or \\end, got \{token}",
loc=None,
)
}
}
if hlines_before_row.length() < body.length() + 1 {
hlines_before_row.push([])
}
Array(
mode=self.mode,
body~,
add_jot=options.add_jot,
array_stretch=options.array_stretch,
columns=options.columns,
row_gaps~,
hskip_before_and_after=options.hskip_before_and_after,
hlines_before_row~,
column_separation_type=options.column_separation_type,
tags=if options.auto_tag is Some(_) { Some(tags) } else { None },
auto_tags=if options.auto_tag is Some(_) { Some(auto_tags) } else { None },
leqno=options.leqno,
)
})
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)
}