use std::iter::Peekable;
use crate::ast::{command_name, environment_name};
use crate::parser::{LatexFlavor, parse_with_flavor};
use crate::semantic::{ArgKind, ArgSpec, Signatures, scan_definitions};
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken};
use super::context::FormatContext;
use super::ir::Ir;
use super::printer::Printer;
use super::style::{FormatStyle, WrapMode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FormatError {
ParseErrors { count: usize },
UnsupportedConstruct { kind: SyntaxKind, snippet: String },
}
impl std::fmt::Display for FormatError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ParseErrors { count } => write!(
f,
"input contains {count} parser diagnostic(s); formatter only supports parseable input"
),
Self::UnsupportedConstruct { kind, snippet } => {
write!(
f,
"unsupported construct for formatter: {kind:?} near {snippet:?}"
)
}
}
}
}
impl std::error::Error for FormatError {}
pub fn format(input: &str) -> Result<String, FormatError> {
format_with_style(input, FormatStyle::default())
}
pub fn format_with_style(input: &str, style: FormatStyle) -> Result<String, FormatError> {
format_with_style_flavored(input, style, LatexFlavor::Document)
}
pub fn format_with_style_flavored(
input: &str,
style: FormatStyle,
flavor: LatexFlavor,
) -> Result<String, FormatError> {
let parsed = parse_with_flavor(input, flavor);
if !parsed.errors.is_empty() {
return Err(FormatError::ParseErrors {
count: parsed.errors.len(),
});
}
format_node(&parsed.syntax(), style)
}
pub fn format_node(root: &SyntaxNode, style: FormatStyle) -> Result<String, FormatError> {
validate_supported_tokens(root)?;
let ctx = FormatContext::new(style);
let mut formatted = format_root(root, ctx);
let trimmed_len = formatted.trim_end_matches([' ', '\t', '\n', '\r']).len();
formatted.truncate(trimmed_len);
if !formatted.is_empty() {
formatted.push('\n');
}
Ok(formatted)
}
fn validate_supported_tokens(root: &SyntaxNode) -> Result<(), FormatError> {
for element in root.descendants_with_tokens() {
let Some(token) = element.into_token() else {
continue;
};
if token.kind() == SyntaxKind::ERROR {
return Err(FormatError::UnsupportedConstruct {
kind: token.kind(),
snippet: token.text().to_string(),
});
}
}
Ok(())
}
fn format_root(root: &SyntaxNode, ctx: FormatContext) -> String {
let user = scan_definitions(root);
let cx = LowerCtx {
wrap: ctx.style().wrap,
signatures: Signatures::new(&user),
};
let ir = lower_node(root, cx);
Printer::new(ctx.style()).print(&ir)
}
#[derive(Clone, Copy)]
struct LowerCtx<'a> {
wrap: WrapMode,
signatures: Signatures<'a>,
}
fn lower_node(node: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
match node.kind() {
SyntaxKind::PARAGRAPH if cx.wrap == WrapMode::Reflow => {
return lower_paragraph_reflow(node, cx);
}
SyntaxKind::ENVIRONMENT if !has_verbatim_body(node) && is_alignment_env(node, cx) => {
return lower_aligned_environment(node, cx);
}
SyntaxKind::ENVIRONMENT
if cx.wrap == WrapMode::Reflow && !has_verbatim_body(node) && is_list_env(node, cx) =>
{
return lower_list_environment(node, cx);
}
SyntaxKind::ENVIRONMENT if !has_verbatim_body(node) => {
return lower_environment(node, cx);
}
SyntaxKind::COMMAND if cx.wrap == WrapMode::Reflow && command_has_managed_arg(node, cx) => {
return lower_command(node, cx);
}
SyntaxKind::INLINE_MATH => {
return lower_math(node, cx);
}
SyntaxKind::DISPLAY_MATH => {
return lower_display_math(node, cx);
}
SyntaxKind::MATH => {
return lower_math_body(node, cx);
}
SyntaxKind::GROUP if spans_multiple_lines(node) => {
return lower_bracketed(node, SyntaxKind::L_BRACE, SyntaxKind::R_BRACE, cx);
}
SyntaxKind::OPTIONAL if spans_multiple_lines(node) => {
return lower_bracketed(node, SyntaxKind::L_BRACKET, SyntaxKind::R_BRACKET, cx);
}
_ => {}
}
Ir::concat(lower_element_stream(node.children_with_tokens(), cx))
}
fn lower_paragraph_reflow(node: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
reflow_elements(node.children_with_tokens(), cx)
}
fn reflow_elements(elements: impl Iterator<Item = SyntaxElement>, cx: LowerCtx<'_>) -> Ir {
let elements: Vec<SyntaxElement> = flatten_inline_prose(elements.collect(), cx);
let mut atom: Vec<Ir> = Vec::new();
let mut run: Vec<Ir> = Vec::new();
let mut lines: Vec<Ir> = Vec::new();
let mut seps: Vec<Ir> = Vec::new();
let mut pending_sep: Ir = Ir::hard_line();
let mut line_all_commands = true;
let mut line_has_content = false;
fn flush_atom(atom: &mut Vec<Ir>, run: &mut Vec<Ir>) {
if !atom.is_empty() {
run.push(Ir::concat(atom.drain(..)));
}
}
fn push_segment(content: Ir, lines: &mut Vec<Ir>, seps: &mut Vec<Ir>, pending_sep: &mut Ir) {
seps.push(std::mem::replace(pending_sep, Ir::hard_line()));
lines.push(content);
}
fn end_line(
atom: &mut Vec<Ir>,
run: &mut Vec<Ir>,
lines: &mut Vec<Ir>,
seps: &mut Vec<Ir>,
pending_sep: &mut Ir,
) {
flush_atom(atom, run);
if !run.is_empty() {
push_segment(Ir::fill(run.drain(..)), lines, seps, pending_sep);
}
}
let mut idx = 0;
while idx < elements.len() {
match &elements[idx] {
SyntaxElement::Token(token) if is_collapsible_trivia(token.kind()) => {
let newlines = consume_trivia_run_slice(&elements, &mut idx);
if newlines >= 2 {
end_line(&mut atom, &mut run, &mut lines, &mut seps, &mut pending_sep);
pending_sep = Ir::empty_line();
line_all_commands = true;
line_has_content = false;
} else if newlines == 1 {
let prev_is_command = line_has_content && line_all_commands;
let next_is_command = line_is_command_only(&elements, idx, cx);
if prev_is_command || next_is_command {
end_line(&mut atom, &mut run, &mut lines, &mut seps, &mut pending_sep);
} else {
flush_atom(&mut atom, &mut run);
}
line_all_commands = true;
line_has_content = false;
} else {
flush_atom(&mut atom, &mut run);
}
continue;
}
SyntaxElement::Token(token) if token.kind() == SyntaxKind::COMMENT => {
if !line_has_content {
end_line(&mut atom, &mut run, &mut lines, &mut seps, &mut pending_sep);
}
atom.push(Ir::verbatim(token.text()));
end_line(&mut atom, &mut run, &mut lines, &mut seps, &mut pending_sep);
line_all_commands = true;
line_has_content = false;
}
SyntaxElement::Token(token)
if token.kind() == SyntaxKind::CONTROL_SYMBOL && token.text().contains('\n') =>
{
let before = token.text().split_once('\n').map(|(b, _)| b).unwrap_or("");
if !before.is_empty() {
atom.push(Ir::verbatim(before));
}
end_line(&mut atom, &mut run, &mut lines, &mut seps, &mut pending_sep);
line_all_commands = true;
line_has_content = false;
}
SyntaxElement::Token(token) => {
atom.push(Ir::verbatim(token.text()));
line_has_content = true;
line_all_commands = false;
}
SyntaxElement::Node(child) if child.kind() == SyntaxKind::LINE_BREAK => {
atom.push(lower_node(child, cx));
end_line(&mut atom, &mut run, &mut lines, &mut seps, &mut pending_sep);
line_all_commands = true;
line_has_content = false;
}
SyntaxElement::Node(child) => {
let ir = lower_node(child, cx);
if ir.contains_forced_break() {
end_line(&mut atom, &mut run, &mut lines, &mut seps, &mut pending_sep);
push_segment(ir, &mut lines, &mut seps, &mut pending_sep);
line_all_commands = true;
line_has_content = false;
} else {
atom.push(ir);
line_has_content = true;
line_all_commands &=
child.kind() == SyntaxKind::COMMAND && !command_is_inline(child, cx);
}
}
}
idx += 1;
}
end_line(&mut atom, &mut run, &mut lines, &mut seps, &mut pending_sep);
let mut result: Vec<Ir> = Vec::with_capacity(lines.len().saturating_mul(2));
for (i, line) in lines.into_iter().enumerate() {
if i > 0 {
result.push(seps[i].clone());
}
result.push(line);
}
Ir::concat(result)
}
fn lower_element_stream(
elements: impl Iterator<Item = SyntaxElement>,
cx: LowerCtx<'_>,
) -> Vec<Ir> {
let mut out = Vec::new();
let mut iter = elements.peekable();
while let Some(element) = iter.next() {
match element {
SyntaxElement::Node(child) => out.push(lower_node(&child, cx)),
SyntaxElement::Token(token) if is_collapsible_trivia(token.kind()) => {
let (newlines, trailing_ws) = consume_trivia_run(&token, &mut iter);
out.push(classify_trivia(newlines, trailing_ws));
}
SyntaxElement::Token(token) => out.push(Ir::verbatim(token.text())),
}
}
out
}
fn lower_environment_leading(node: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
let mut leading: Vec<SyntaxElement> = Vec::new();
for element in node.children_with_tokens() {
if matches!(&element, SyntaxElement::Node(c) if c.kind() == SyntaxKind::BEGIN) {
break;
}
leading.push(element);
}
if leading.is_empty() {
Ir::Nil
} else {
Ir::concat(lower_element_stream(leading.into_iter(), cx))
}
}
fn lower_environment(node: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
let leading = lower_environment_leading(node, cx);
let mut begin = Ir::Nil;
let mut end = Ir::Nil;
let mut body_elements: Vec<SyntaxElement> = Vec::new();
let mut seen_begin = false;
for element in node.children_with_tokens() {
match &element {
SyntaxElement::Node(child) if child.kind() == SyntaxKind::BEGIN => {
seen_begin = true;
begin = lower_begin(child, cx);
}
SyntaxElement::Node(child) if child.kind() == SyntaxKind::END => {
end = lower_node(child, cx);
}
_ if !seen_begin => {}
_ => body_elements.push(element),
}
}
let (begin, body) = match leading_inline_comment(&body_elements) {
Some(comment) => {
let begin = Ir::concat([begin, Ir::verbatim(comment.text())]);
(
begin,
lower_body_dropping_leading_comment(body_elements, cx),
)
}
None => (
begin,
Ir::concat(lower_element_stream(body_elements.into_iter(), cx)),
),
};
let (lead_blank, body) = peel_leading_break(body);
let (trail_blank, body) = peel_trailing_break(body);
let lead = if lead_blank {
Ir::empty_line()
} else {
Ir::hard_line()
};
let trail = if trail_blank {
Ir::empty_line()
} else {
Ir::hard_line()
};
let env = if matches!(body, Ir::Nil) {
Ir::concat([begin, Ir::hard_line(), end])
} else if environment_no_indent(node, cx) {
Ir::concat([begin, lead, body, trail, end])
} else {
Ir::concat([begin, Ir::indent(Ir::concat([lead, body])), trail, end])
};
Ir::concat([leading, env])
}
fn leading_inline_comment(body_elements: &[SyntaxElement]) -> Option<SyntaxToken> {
for element in body_elements {
match element {
SyntaxElement::Token(token) => match token.kind() {
SyntaxKind::WHITESPACE => continue,
SyntaxKind::COMMENT => return Some(token.clone()),
_ => return None,
},
SyntaxElement::Node(node) => {
for token in node
.descendants_with_tokens()
.filter_map(|e| e.into_token())
{
match token.kind() {
SyntaxKind::WHITESPACE => continue,
SyntaxKind::COMMENT => return Some(token),
_ => return None,
}
}
}
}
}
None
}
fn lower_body_dropping_leading_comment(body_elements: Vec<SyntaxElement>, cx: LowerCtx<'_>) -> Ir {
let mut out: Vec<Ir> = Vec::new();
let mut iter = body_elements.into_iter();
for element in iter.by_ref() {
match element {
SyntaxElement::Token(token) if token.kind() == SyntaxKind::WHITESPACE => continue,
SyntaxElement::Token(token) if token.kind() == SyntaxKind::COMMENT => break,
SyntaxElement::Node(node) => {
out.push(lower_node_dropping_leading_comment(&node, cx));
break;
}
SyntaxElement::Token(token) => {
out.push(Ir::verbatim(token.text()));
break;
}
}
}
out.extend(lower_element_stream(iter, cx));
Ir::concat(out)
}
fn lower_node_dropping_leading_comment(node: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
let mut children: Vec<SyntaxElement> = node.children_with_tokens().collect();
let mut i = 0;
while matches!(
children.get(i).and_then(|c| c.as_token()).map(|t| t.kind()),
Some(SyntaxKind::WHITESPACE)
) {
i += 1;
}
if matches!(
children.get(i).and_then(|c| c.as_token()).map(|t| t.kind()),
Some(SyntaxKind::COMMENT)
) {
children.drain(..=i);
}
if node.kind() == SyntaxKind::PARAGRAPH && cx.wrap == WrapMode::Reflow {
reflow_elements(children.into_iter(), cx)
} else {
Ir::concat(lower_element_stream(children.into_iter(), cx))
}
}
fn environment_no_indent(node: &SyntaxNode, cx: LowerCtx<'_>) -> bool {
node.children()
.find(|child| child.kind() == SyntaxKind::BEGIN)
.and_then(|begin| environment_name(&begin))
.and_then(|name| cx.signatures.environment(&name))
.is_some_and(|sig| sig.no_indent)
}
fn lower_begin(begin: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
let arity = environment_name(begin)
.and_then(|name| cx.signatures.environment(&name))
.map(|sig| sig.args.len())
.unwrap_or(0);
let has_comment = begin
.children_with_tokens()
.filter_map(|element| element.into_token())
.any(|token| token.kind() == SyntaxKind::COMMENT);
if arity == 0 || has_comment {
return lower_node(begin, cx);
}
let mut head: Vec<Ir> = Vec::new();
let mut tail: Vec<SyntaxElement> = Vec::new();
let mut args_seen = 0;
let mut in_tail = false;
for element in begin.children_with_tokens() {
if in_tail {
tail.push(element);
continue;
}
match &element {
SyntaxElement::Node(child)
if matches!(child.kind(), SyntaxKind::GROUP | SyntaxKind::OPTIONAL) =>
{
head.push(lower_node(child, cx));
args_seen += 1;
if args_seen == arity {
in_tail = true;
}
}
SyntaxElement::Node(child) => head.push(lower_node(child, cx)),
SyntaxElement::Token(token) if is_collapsible_trivia(token.kind()) => {}
SyntaxElement::Token(token) => head.push(Ir::verbatim(token.text())),
}
}
if !tail.is_empty() {
head.extend(lower_element_stream(tail.into_iter(), cx));
}
Ir::concat(head)
}
fn is_list_env(node: &SyntaxNode, cx: LowerCtx<'_>) -> bool {
node.children()
.find(|child| child.kind() == SyntaxKind::BEGIN)
.and_then(|begin| environment_name(&begin))
.and_then(|name| cx.signatures.environment(&name))
.is_some_and(|sig| sig.list)
}
struct ListItem {
marker: String,
hang: usize,
chunks: Vec<Vec<SyntaxElement>>,
blank_before: bool,
}
enum FlatItem {
El(SyntaxElement),
Blank,
}
fn lower_list_environment(node: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
let leading = lower_environment_leading(node, cx);
let mut begin = Ir::Nil;
let mut end = Ir::Nil;
let mut body_elements: Vec<SyntaxElement> = Vec::new();
let mut seen_begin = false;
for element in node.children_with_tokens() {
match &element {
SyntaxElement::Node(child) if child.kind() == SyntaxKind::BEGIN => {
seen_begin = true;
begin = lower_begin(child, cx);
}
SyntaxElement::Node(child) if child.kind() == SyntaxKind::END => {
end = lower_node(child, cx);
}
_ if !seen_begin => {}
_ => body_elements.push(element),
}
}
let Some(body) = lower_list_body(&body_elements, cx) else {
return lower_environment(node, cx);
};
Ir::concat([
leading,
begin,
Ir::indent(Ir::concat([Ir::hard_line(), body])),
Ir::hard_line(),
end,
])
}
fn lower_list_body(body_elements: &[SyntaxElement], cx: LowerCtx<'_>) -> Option<Ir> {
let flat = flatten_list_body(body_elements);
let mut preamble: Vec<Vec<SyntaxElement>> = vec![Vec::new()];
let mut items: Vec<ListItem> = Vec::new();
let mut blank_pending = false;
for fi in flat {
match fi {
FlatItem::Blank => {
blank_pending = true;
match items.last_mut() {
Some(item) => item.chunks.push(Vec::new()),
None => preamble.push(Vec::new()),
}
}
FlatItem::El(el) if is_item_command(&el) => {
let (marker, hang, leading) = split_item_marker(&el, cx);
items.push(ListItem {
marker,
hang,
chunks: vec![leading],
blank_before: blank_pending,
});
blank_pending = false;
}
FlatItem::El(el) => {
match items.last_mut() {
Some(item) => item.chunks.last_mut().unwrap().push(el),
None => preamble.last_mut().unwrap().push(el),
}
blank_pending = false;
}
}
}
if items.is_empty() {
return None;
}
let mut segments: Vec<Ir> = Vec::new();
let mut seps: Vec<Ir> = Vec::new();
let preamble_ir = reflow_chunks(&preamble, cx);
if !matches!(preamble_ir, Ir::Nil) {
seps.push(Ir::hard_line()); segments.push(preamble_ir);
}
for item in &items {
seps.push(if item.blank_before {
Ir::empty_line()
} else {
Ir::hard_line()
});
segments.push(render_list_item(item, cx));
}
let mut result: Vec<Ir> = Vec::with_capacity(segments.len().saturating_mul(2));
for (i, segment) in segments.into_iter().enumerate() {
if i > 0 {
result.push(seps[i].clone());
}
result.push(segment);
}
Some(Ir::concat(result))
}
fn render_list_item(item: &ListItem, cx: LowerCtx<'_>) -> Ir {
let content = reflow_chunks(&item.chunks, cx);
let marker = Ir::verbatim(item.marker.clone());
if matches!(content, Ir::Nil) {
return marker;
}
Ir::concat([marker, Ir::verbatim(" "), Ir::align(item.hang, content)])
}
fn reflow_chunks(chunks: &[Vec<SyntaxElement>], cx: LowerCtx<'_>) -> Ir {
let parts = chunks
.iter()
.map(|chunk| reflow_elements(chunk.iter().cloned(), cx))
.filter(|ir| !matches!(ir, Ir::Nil));
Ir::join(Ir::empty_line(), parts)
}
fn flatten_list_body(body_elements: &[SyntaxElement]) -> Vec<FlatItem> {
let mut out: Vec<FlatItem> = Vec::new();
let mut started = false;
for element in body_elements {
match element {
SyntaxElement::Node(p) if p.kind() == SyntaxKind::PARAGRAPH => {
if started {
out.push(FlatItem::Blank);
}
out.extend(p.children_with_tokens().map(FlatItem::El));
started = true;
}
SyntaxElement::Token(t) if is_collapsible_trivia(t.kind()) => {}
other => {
out.push(FlatItem::El(other.clone()));
started = true;
}
}
}
out
}
fn is_item_command(el: &SyntaxElement) -> bool {
el.as_node().is_some_and(|node| {
node.kind() == SyntaxKind::COMMAND && command_name(node).as_deref() == Some("item")
})
}
fn split_item_marker(el: &SyntaxElement, cx: LowerCtx<'_>) -> (String, usize, Vec<SyntaxElement>) {
let node = el.as_node().expect("item command is a node");
let mut marker_parts: Vec<Ir> = Vec::new();
let mut content: Vec<SyntaxElement> = Vec::new();
let mut hang = 1; let mut in_content = false;
for child in node.children_with_tokens() {
if in_content {
content.push(child);
continue;
}
match &child {
SyntaxElement::Token(t) if t.kind() == SyntaxKind::CONTROL_WORD => {
hang += t.text().chars().count();
marker_parts.push(Ir::verbatim(t.text()));
}
SyntaxElement::Token(t) if is_collapsible_trivia(t.kind()) => {}
SyntaxElement::Node(n) if n.kind() == SyntaxKind::OPTIONAL => {
marker_parts.push(lower_node(n, cx));
}
other => {
in_content = true;
content.push(other.clone());
}
}
}
let marker = Printer::new(FormatStyle::default()).print_flat(&Ir::concat(marker_parts));
(marker, hang, content)
}
fn is_alignment_env(node: &SyntaxNode, cx: LowerCtx<'_>) -> bool {
node.children()
.find(|child| child.kind() == SyntaxKind::BEGIN)
.and_then(|begin| environment_name(&begin))
.and_then(|name| cx.signatures.environment(&name))
.is_some_and(|sig| sig.align)
}
struct AlignRow {
cells: Vec<String>,
line_break: Option<String>,
trailing_comment: Option<String>,
}
enum GridItem {
Row(AlignRow),
Passthrough(String),
}
fn lower_aligned_environment(node: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
let leading = lower_environment_leading(node, cx);
let mut begin = Ir::Nil;
let mut end = Ir::Nil;
let mut body_elements: Vec<SyntaxElement> = Vec::new();
let mut seen_begin = false;
for element in node.children_with_tokens() {
match &element {
SyntaxElement::Node(child) if child.kind() == SyntaxKind::BEGIN => {
seen_begin = true;
begin = lower_begin(child, cx);
}
SyntaxElement::Node(child) if child.kind() == SyntaxKind::END => {
end = lower_node(child, cx);
}
_ if !seen_begin => {}
_ => body_elements.push(element),
}
}
let Some(items) = build_alignment_grid(&body_elements, cx) else {
return lower_environment(node, cx);
};
if !items.iter().any(|item| matches!(item, GridItem::Row(_))) {
return lower_environment(node, cx);
}
let body = render_alignment_rows(&items);
Ir::concat([
leading,
begin,
Ir::indent(Ir::concat([Ir::hard_line(), body])),
Ir::hard_line(),
end,
])
}
fn build_alignment_grid(
body_elements: &[SyntaxElement],
cx: LowerCtx<'_>,
) -> Option<Vec<GridItem>> {
let inline = flatten_alignment_body(body_elements)?;
let printer = Printer::new(FormatStyle::default());
fn finish_cell(
cell: &mut Vec<SyntaxElement>,
cells: &mut Vec<String>,
printer: &Printer,
cx: LowerCtx<'_>,
) -> Option<()> {
let is_edge_trivia = |e: &SyntaxElement| {
e.as_token()
.is_some_and(|t| is_collapsible_trivia(t.kind()))
};
while cell.first().is_some_and(&is_edge_trivia) {
cell.remove(0);
}
while cell.last().is_some_and(&is_edge_trivia) {
cell.pop();
}
if cell.iter().any(|e| {
e.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::COMMENT)
}) {
return None;
}
let ir = Ir::concat(lower_element_stream(cell.drain(..), cx));
if ir.contains_forced_break() {
return None;
}
cells.push(printer.print_flat(&ir).trim().to_string());
Some(())
}
let mut items: Vec<GridItem> = Vec::new();
let mut cells: Vec<String> = Vec::new();
let mut cell: Vec<SyntaxElement> = Vec::new();
let mut final_pushed = false;
let mut idx = 0;
while idx < inline.len() {
let at_boundary = cells.is_empty() && cell_is_blank(&cell);
if at_boundary
&& is_comment_or_rule_start(&inline[idx], cx)
&& let Some(line) = non_row_line(&inline, idx, &printer, cx)
{
let own_line = cell_has_newline(&cell);
let prev_is_row = matches!(items.last(), Some(GridItem::Row(_)));
if own_line || !prev_is_row {
items.push(GridItem::Passthrough(line.text));
cell.clear();
idx = line.next;
continue;
}
if !line.has_rule {
if let Some(GridItem::Row(row)) = items.last_mut() {
row.trailing_comment = Some(line.text);
}
cell.clear();
idx = line.next;
continue;
}
}
match &inline[idx] {
SyntaxElement::Token(token) if token.kind() == SyntaxKind::AMPERSAND => {
finish_cell(&mut cell, &mut cells, &printer, cx)?;
}
SyntaxElement::Node(child) if child.kind() == SyntaxKind::LINE_BREAK => {
finish_cell(&mut cell, &mut cells, &printer, cx)?;
let line_break = printer
.print_flat(&lower_node(child, cx))
.trim()
.to_string();
items.push(GridItem::Row(AlignRow {
cells: std::mem::take(&mut cells),
line_break: Some(line_break),
trailing_comment: None,
}));
}
SyntaxElement::Token(token) if token.kind() == SyntaxKind::COMMENT => {
if !rest_is_only_trivia(&inline, idx + 1) {
return None;
}
let text = token.text().trim_end().to_string();
finish_cell(&mut cell, &mut cells, &printer, cx)?;
items.push(GridItem::Row(AlignRow {
cells: std::mem::take(&mut cells),
line_break: None,
trailing_comment: Some(text),
}));
final_pushed = true;
break;
}
_ => cell.push(inline[idx].clone()),
}
idx += 1;
}
if !final_pushed {
finish_cell(&mut cell, &mut cells, &printer, cx)?;
let final_is_empty = cells.len() == 1 && cells[0].is_empty();
if !final_is_empty {
items.push(GridItem::Row(AlignRow {
cells,
line_break: None,
trailing_comment: None,
}));
}
}
Some(items)
}
struct NonRowLine {
text: String,
next: usize,
has_rule: bool,
}
fn non_row_line(
inline: &[SyntaxElement],
start: usize,
printer: &Printer,
cx: LowerCtx<'_>,
) -> Option<NonRowLine> {
let mut i = start;
let mut content_end = start;
let mut has_rule = false;
let mut has_comment = false;
while i < inline.len() {
match &inline[i] {
SyntaxElement::Token(t) if t.kind() == SyntaxKind::NEWLINE => break,
SyntaxElement::Token(t) if t.kind() == SyntaxKind::WHITESPACE => {}
SyntaxElement::Token(t) if t.kind() == SyntaxKind::COMMENT => {
has_comment = true;
i += 1;
content_end = i;
break;
}
SyntaxElement::Node(n) if n.kind() == SyntaxKind::COMMAND && is_rule_command(n, cx) => {
has_rule = true;
i += 1;
content_end = i;
continue;
}
_ => return None,
}
i += 1;
}
if !(has_rule || has_comment) {
return None;
}
let mut next = content_end;
while next < inline.len() {
match &inline[next] {
SyntaxElement::Token(t) if t.kind() == SyntaxKind::WHITESPACE => next += 1,
SyntaxElement::Token(t) if t.kind() == SyntaxKind::NEWLINE => {
next += 1;
break;
}
_ => break,
}
}
let ir = Ir::concat(lower_element_stream(
inline[start..content_end].iter().cloned(),
cx,
));
let text = printer.print_flat(&ir).trim().to_string();
Some(NonRowLine {
text,
next,
has_rule,
})
}
fn is_comment_or_rule_start(element: &SyntaxElement, cx: LowerCtx<'_>) -> bool {
match element {
SyntaxElement::Token(t) => t.kind() == SyntaxKind::COMMENT,
SyntaxElement::Node(n) => n.kind() == SyntaxKind::COMMAND && is_rule_command(n, cx),
}
}
fn is_rule_command(node: &SyntaxNode, cx: LowerCtx<'_>) -> bool {
command_name(node)
.and_then(|name| cx.signatures.command(&name))
.is_some_and(|sig| sig.rule)
}
fn cell_is_blank(cell: &[SyntaxElement]) -> bool {
cell.iter().all(|e| {
e.as_token()
.is_some_and(|t| is_collapsible_trivia(t.kind()))
})
}
fn cell_has_newline(cell: &[SyntaxElement]) -> bool {
cell.iter().any(|e| {
e.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::NEWLINE)
})
}
fn rest_is_only_trivia(inline: &[SyntaxElement], from: usize) -> bool {
inline[from..].iter().all(|e| {
e.as_token()
.is_some_and(|t| is_collapsible_trivia(t.kind()))
})
}
fn flatten_alignment_body(body_elements: &[SyntaxElement]) -> Option<Vec<SyntaxElement>> {
let mut inline: Vec<SyntaxElement> = Vec::new();
let mut paragraphs = 0;
for element in body_elements {
match element {
SyntaxElement::Node(child) if child.kind() == SyntaxKind::PARAGRAPH => {
paragraphs += 1;
if paragraphs > 1 {
return None;
}
inline.extend(child.children_with_tokens());
}
SyntaxElement::Token(token) if is_collapsible_trivia(token.kind()) => {}
other => inline.push(other.clone()),
}
}
Some(inline)
}
fn render_alignment_rows(items: &[GridItem]) -> Ir {
let mut col_widths: Vec<usize> = Vec::new();
for item in items {
let GridItem::Row(row) = item else { continue };
for (c, cell) in row.cells.iter().enumerate() {
let width = cell.chars().count();
if c == col_widths.len() {
col_widths.push(width);
} else if width > col_widths[c] {
col_widths[c] = width;
}
}
}
let lines = items.iter().map(|item| {
let row = match item {
GridItem::Passthrough(text) => return Ir::text(text.clone()),
GridItem::Row(row) => row,
};
let mut line = String::new();
let last = row.cells.len().saturating_sub(1);
for (c, cell) in row.cells.iter().enumerate() {
if c > 0 {
line.push_str(" & ");
}
line.push_str(cell);
if c < last {
let pad = col_widths[c].saturating_sub(cell.chars().count());
line.push_str(&" ".repeat(pad));
}
}
if let Some(line_break) = &row.line_break {
line.push(' ');
line.push_str(line_break);
}
if let Some(comment) = &row.trailing_comment {
line.push(' ');
line.push_str(comment);
}
Ir::text(line)
});
Ir::join(Ir::hard_line(), lines)
}
fn lower_bracketed(node: &SyntaxNode, open: SyntaxKind, close: SyntaxKind, cx: LowerCtx<'_>) -> Ir {
let mut open_ir = Ir::Nil;
let mut close_ir = Ir::Nil;
let mut body_elements: Vec<SyntaxElement> = Vec::new();
for element in node.children_with_tokens() {
match &element {
SyntaxElement::Token(t) if t.kind() == open && matches!(open_ir, Ir::Nil) => {
open_ir = Ir::verbatim(t.text());
}
SyntaxElement::Token(t) if t.kind() == close => {
close_ir = Ir::verbatim(t.text());
}
_ => body_elements.push(element),
}
}
let has_leading_comment = body_elements
.first()
.and_then(SyntaxElement::as_token)
.is_some_and(|t| t.kind() == SyntaxKind::COMMENT);
let open_ir = if has_leading_comment {
let comment = body_elements.remove(0);
Ir::concat([open_ir, Ir::verbatim(comment.as_token().unwrap().text())])
} else {
open_ir
};
let body = Ir::concat(lower_element_stream(body_elements.into_iter(), cx));
let body = trim_trailing_break(trim_leading_break(body));
if matches!(body, Ir::Nil) {
if has_leading_comment {
Ir::concat([open_ir, Ir::hard_line(), close_ir])
} else {
Ir::concat([open_ir, close_ir])
}
} else {
Ir::concat([
open_ir,
Ir::indent(Ir::concat([Ir::hard_line(), body])),
Ir::hard_line(),
close_ir,
])
}
}
fn command_has_managed_arg(command: &SyntaxNode, cx: LowerCtx<'_>) -> bool {
command_name(command)
.and_then(|name| cx.signatures.command(&name))
.is_some_and(|sig| sig.args.iter().any(|spec| spec.prose || spec.collapse))
}
fn command_is_inline_prose(command: &SyntaxNode, cx: LowerCtx<'_>) -> bool {
command_name(command)
.and_then(|name| cx.signatures.command(&name))
.is_some_and(|sig| sig.inline && sig.args.iter().any(|spec| spec.prose))
}
fn command_is_inline(command: &SyntaxNode, cx: LowerCtx<'_>) -> bool {
command_name(command)
.and_then(|name| cx.signatures.command(&name))
.is_some_and(|sig| sig.inline)
}
fn flatten_inline_prose(elements: Vec<SyntaxElement>, cx: LowerCtx<'_>) -> Vec<SyntaxElement> {
let mut out = Vec::new();
for element in elements {
match &element {
SyntaxElement::Node(node)
if node.kind() == SyntaxKind::COMMAND && command_is_inline_prose(node, cx) =>
{
expand_inline_prose(node, cx, &mut out);
}
_ => out.push(element),
}
}
out
}
fn expand_inline_prose(node: &SyntaxNode, cx: LowerCtx<'_>, out: &mut Vec<SyntaxElement>) {
let Some(sig) = command_name(node).and_then(|name| cx.signatures.command(&name)) else {
out.push(SyntaxElement::Node(node.clone()));
return;
};
let mut slot = 0usize;
for child in node.children_with_tokens() {
match child {
SyntaxElement::Node(group)
if matches!(group.kind(), SyntaxKind::GROUP | SyntaxKind::OPTIONAL) =>
{
let is_bracket = group.kind() == SyntaxKind::OPTIONAL;
let prose =
match_arg_slot(&sig.args, &mut slot, is_bracket).is_some_and(|spec| spec.prose);
if prose {
splice_prose_group(&group, cx, out);
} else {
out.push(SyntaxElement::Node(group));
}
}
other => out.push(other),
}
}
}
fn splice_prose_group(group: &SyntaxNode, cx: LowerCtx<'_>, out: &mut Vec<SyntaxElement>) {
let mut open: Option<SyntaxElement> = None;
let mut close: Option<SyntaxElement> = None;
let mut body: Vec<SyntaxElement> = Vec::new();
for element in group.children_with_tokens() {
match &element {
SyntaxElement::Token(t)
if matches!(t.kind(), SyntaxKind::L_BRACE | SyntaxKind::L_BRACKET)
&& open.is_none() =>
{
open = Some(element);
}
SyntaxElement::Token(t)
if matches!(t.kind(), SyntaxKind::R_BRACE | SyntaxKind::R_BRACKET) =>
{
close = Some(element);
}
_ => body.push(element),
}
}
while body.first().is_some_and(is_collapsible_trivia_element) {
body.remove(0);
}
while body.last().is_some_and(is_collapsible_trivia_element) {
body.pop();
}
if let Some(open) = open {
out.push(open);
}
out.extend(flatten_inline_prose(body, cx));
if let Some(close) = close {
out.push(close);
}
}
fn is_collapsible_trivia_element(element: &SyntaxElement) -> bool {
matches!(element, SyntaxElement::Token(t) if is_collapsible_trivia(t.kind()))
}
fn lower_command(node: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
let Some(sig) = command_name(node).and_then(|name| cx.signatures.command(&name)) else {
return Ir::concat(lower_element_stream(node.children_with_tokens(), cx));
};
let mut out: Vec<Ir> = Vec::new();
let mut slot = 0usize;
let mut iter = node.children_with_tokens().peekable();
while let Some(element) = iter.next() {
match element {
SyntaxElement::Node(child)
if matches!(child.kind(), SyntaxKind::GROUP | SyntaxKind::OPTIONAL) =>
{
let is_bracket = child.kind() == SyntaxKind::OPTIONAL;
let (open, close) = if is_bracket {
(SyntaxKind::L_BRACKET, SyntaxKind::R_BRACKET)
} else {
(SyntaxKind::L_BRACE, SyntaxKind::R_BRACE)
};
let spec = match_arg_slot(&sig.args, &mut slot, is_bracket);
if spec.is_some_and(|s| s.prose) {
out.push(lower_prose_group(&child, open, close, cx));
} else if spec.is_some_and(|s| s.collapse) {
out.push(
collapse_arg_group(&child, open, close, cx)
.unwrap_or_else(|| lower_node(&child, cx)),
);
} else {
out.push(lower_node(&child, cx));
}
}
SyntaxElement::Node(child) => out.push(lower_node(&child, cx)),
SyntaxElement::Token(token) if is_collapsible_trivia(token.kind()) => {
let (newlines, trailing_ws) = consume_trivia_run(&token, &mut iter);
out.push(classify_trivia(newlines, trailing_ws));
}
SyntaxElement::Token(token) => out.push(Ir::verbatim(token.text())),
}
}
Ir::concat(out)
}
fn match_arg_slot(args: &[ArgSpec], slot: &mut usize, is_bracket: bool) -> Option<ArgSpec> {
while *slot < args.len() {
let spec = args[*slot];
let spec_bracket = matches!(spec.kind, ArgKind::Bracket);
if spec_bracket == is_bracket {
*slot += 1;
return Some(spec);
}
if spec_bracket {
*slot += 1;
continue;
}
return None;
}
None
}
fn lower_prose_group(
node: &SyntaxNode,
open: SyntaxKind,
close: SyntaxKind,
cx: LowerCtx<'_>,
) -> Ir {
let mut open_ir = Ir::Nil;
let mut close_ir = Ir::Nil;
let mut body_elements: Vec<SyntaxElement> = Vec::new();
for element in node.children_with_tokens() {
match &element {
SyntaxElement::Token(t) if t.kind() == open && matches!(open_ir, Ir::Nil) => {
open_ir = Ir::verbatim(t.text());
}
SyntaxElement::Token(t) if t.kind() == close => {
close_ir = Ir::verbatim(t.text());
}
_ => body_elements.push(element),
}
}
let body = reflow_elements(body_elements.into_iter(), cx);
if matches!(body, Ir::Nil) {
Ir::concat([open_ir, close_ir])
} else {
Ir::group(Ir::concat([
open_ir,
Ir::indent(Ir::concat([Ir::soft_line(), body])),
Ir::soft_line(),
close_ir,
]))
}
}
fn collapse_arg_group(
node: &SyntaxNode,
open: SyntaxKind,
close: SyntaxKind,
cx: LowerCtx<'_>,
) -> Option<Ir> {
let mut open_ir = Ir::Nil;
let mut close_ir = Ir::Nil;
let mut body: Vec<Ir> = Vec::new();
let mut iter = node.children_with_tokens().peekable();
while let Some(element) = iter.next() {
match element {
SyntaxElement::Token(t) if t.kind() == open && matches!(open_ir, Ir::Nil) => {
open_ir = Ir::verbatim(t.text());
}
SyntaxElement::Token(t) if t.kind() == close => {
close_ir = Ir::verbatim(t.text());
}
SyntaxElement::Token(t) if is_collapsible_trivia(t.kind()) => {
let (newlines, trailing_ws) = consume_trivia_run(&t, &mut iter);
if newlines >= 2 {
return None; }
body.push(if newlines == 1 {
Ir::verbatim(" ")
} else {
Ir::verbatim(trailing_ws)
});
}
SyntaxElement::Token(t) if t.kind() == SyntaxKind::COMMENT => return None,
SyntaxElement::Token(t) => body.push(Ir::verbatim(t.text())),
SyntaxElement::Node(child) => {
let ir = lower_node(&child, cx);
if ir.contains_forced_break() {
return None; }
body.push(ir);
}
}
}
let body = trim_trailing_break(trim_leading_break(Ir::concat(body)));
Some(Ir::concat([open_ir, body, close_ir]))
}
fn lower_math(node: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
Ir::concat(node.children_with_tokens().map(|el| match el {
SyntaxElement::Node(n) if n.kind() == SyntaxKind::MATH => lower_math_body(&n, cx),
SyntaxElement::Node(n) => lower_node(&n, cx),
SyntaxElement::Token(t) => Ir::verbatim(t.text()),
}))
}
fn lower_display_math(node: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
let mut open = String::new();
let mut close = String::new();
let mut body = Ir::Nil;
let mut body_empty = true;
let mut seen_body = false;
for element in node.children_with_tokens() {
match element {
SyntaxElement::Node(n) if n.kind() == SyntaxKind::MATH => {
body_empty = math_body_is_empty(&n);
body = trim_trailing_break(lower_display_math_body(&n, cx));
seen_body = true;
}
SyntaxElement::Token(t) if is_collapsible_trivia(t.kind()) => {}
SyntaxElement::Token(t) if seen_body => close.push_str(t.text()),
SyntaxElement::Token(t) => open.push_str(t.text()),
SyntaxElement::Node(n) => {
body = lower_node(&n, cx);
body_empty = false;
seen_body = true;
}
}
}
if body_empty {
Ir::concat([Ir::verbatim(open), Ir::verbatim(close)])
} else {
Ir::concat([
Ir::verbatim(open),
Ir::indent(Ir::concat([Ir::hard_line(), body])),
Ir::hard_line(),
Ir::verbatim(close),
])
}
}
fn lower_math_body(node: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
lower_math_seq(node.children_with_tokens(), cx)
}
#[derive(Clone, Copy, PartialEq)]
enum MathRole {
Operand,
Binary,
Relation,
}
struct MathPiece {
ir: Ir,
role: MathRole,
}
const MATH_RELATION_COMMANDS: &[&str] = &[
"le",
"leq",
"ge",
"geq",
"ne",
"neq",
"equiv",
"approx",
"approxeq",
"sim",
"simeq",
"cong",
"propto",
"asymp",
"doteq",
"models",
"vdash",
"dashv",
"perp",
"parallel",
"mid",
"in",
"ni",
"notin",
"subset",
"subseteq",
"subsetneq",
"supset",
"supseteq",
"supsetneq",
"sqsubseteq",
"sqsupseteq",
"prec",
"preceq",
"succ",
"succeq",
"ll",
"gg",
"lll",
"ggg",
"to",
"rightarrow",
"longrightarrow",
"Rightarrow",
"Longrightarrow",
"implies",
"impliedby",
"iff",
"mapsto",
"longmapsto",
"leftarrow",
"Leftarrow",
"gets",
"leftrightarrow",
"Leftrightarrow",
"Longleftrightarrow",
"hookrightarrow",
"hookleftarrow",
"triangleq",
"coloneqq",
"eqqcolon",
"lesssim",
"gtrsim",
];
const MATH_BINARY_COMMANDS: &[&str] = &[
"pm",
"mp",
"times",
"div",
"cdot",
"ast",
"star",
"circ",
"bullet",
"cup",
"cap",
"uplus",
"sqcup",
"sqcap",
"vee",
"wedge",
"lor",
"land",
"oplus",
"ominus",
"otimes",
"oslash",
"odot",
"setminus",
"amalg",
"diamond",
"wr",
"dagger",
"ddagger",
"bigtriangleup",
"bigtriangledown",
"triangleleft",
"triangleright",
];
fn classify_math_op_text(text: &str) -> MathRole {
match text {
"=" | "<" | ">" => MathRole::Relation,
"+" | "-" => MathRole::Binary,
_ => MathRole::Operand,
}
}
fn math_atom_role(el: &SyntaxElement, prev: MathRole) -> MathRole {
let raw = match el {
SyntaxElement::Token(t) => classify_math_op_text(t.text()),
SyntaxElement::Node(n) if n.kind() == SyntaxKind::COMMAND => crate::ast::command_name(n)
.map_or(MathRole::Operand, |name| {
if MATH_RELATION_COMMANDS.contains(&name.as_str()) {
MathRole::Relation
} else if MATH_BINARY_COMMANDS.contains(&name.as_str()) {
MathRole::Binary
} else {
MathRole::Operand
}
}),
_ => MathRole::Operand,
};
if raw == MathRole::Binary && prev != MathRole::Operand {
MathRole::Operand
} else {
raw
}
}
fn collect_math_pieces(node: &SyntaxNode, cx: LowerCtx<'_>) -> Option<Vec<MathPiece>> {
let mut pieces: Vec<MathPiece> = Vec::new();
let mut prev_role = MathRole::Operand;
let mut iter = node.children_with_tokens().peekable();
while let Some(el) = iter.next() {
match el {
SyntaxElement::Token(t) if is_collapsible_trivia(t.kind()) => {
consume_trivia_run(&t, &mut iter);
}
SyntaxElement::Token(t) if t.kind() == SyntaxKind::COMMENT => return None,
other => {
let role = math_atom_role(&other, prev_role);
prev_role = role;
pieces.push(MathPiece {
ir: lower_math_element(other, cx),
role,
});
}
}
}
(pieces.len() >= 2).then_some(pieces)
}
fn lower_display_math_body(node: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
let Some(pieces) = collect_math_pieces(node, cx) else {
return lower_math_body(node, cx);
};
let anchor = pieces.iter().position(|p| p.role == MathRole::Relation);
let offset = match anchor {
Some(a) => {
let head = Ir::join(Ir::text(" "), pieces[..=a].iter().map(|p| p.ir.clone()));
Printer::new(FormatStyle::default())
.print_flat(&head)
.chars()
.count()
+ 1
}
None => 0,
};
let mut parts: Vec<Ir> = Vec::with_capacity(pieces.len() * 2);
for (i, piece) in pieces.iter().enumerate() {
if i > 0 {
let after_anchor = anchor.is_none_or(|a| i > a);
let break_here = after_anchor
&& match piece.role {
MathRole::Binary => pieces[i - 1].role == MathRole::Operand,
MathRole::Relation => true,
MathRole::Operand => false,
};
parts.push(if break_here {
Ir::line()
} else {
Ir::text(" ")
});
}
parts.push(piece.ir.clone());
}
Ir::group(Ir::align(offset, Ir::concat(parts)))
}
#[derive(PartialEq)]
enum MathSep {
None,
Space,
Break,
}
fn lower_math_seq(elements: impl Iterator<Item = SyntaxElement>, cx: LowerCtx<'_>) -> Ir {
let mut out: Vec<Ir> = Vec::new();
let mut sep = MathSep::None;
let mut started = false;
let mut iter = elements.peekable();
while let Some(el) = iter.next() {
match el {
SyntaxElement::Token(t) if is_collapsible_trivia(t.kind()) => {
consume_trivia_run(&t, &mut iter);
if started && sep == MathSep::None {
sep = MathSep::Space;
}
}
SyntaxElement::Token(t) if t.kind() == SyntaxKind::COMMENT => {
if sep == MathSep::Space {
out.push(Ir::verbatim(" "));
}
out.push(Ir::verbatim(t.text()));
started = true;
sep = MathSep::Break;
}
other => {
match sep {
MathSep::Space => out.push(Ir::verbatim(" ")),
MathSep::Break => out.push(Ir::hard_line()),
MathSep::None => {}
}
out.push(lower_math_element(other, cx));
started = true;
sep = MathSep::None;
}
}
}
if sep == MathSep::Break {
out.push(Ir::hard_line());
}
Ir::concat(out)
}
fn lower_math_element(el: SyntaxElement, cx: LowerCtx<'_>) -> Ir {
match el {
SyntaxElement::Node(n) => match n.kind() {
SyntaxKind::SCRIPTED => lower_scripted(&n, cx),
SyntaxKind::SUBSCRIPT | SyntaxKind::SUPERSCRIPT => lower_script(&n, cx),
SyntaxKind::GROUP => lower_math_group(&n, cx),
SyntaxKind::LEFT_RIGHT => lower_left_right(&n, cx),
SyntaxKind::COMMAND => Ir::verbatim(n.text().to_string()),
_ => lower_node(&n, cx),
},
SyntaxElement::Token(t) => Ir::verbatim(t.text()),
}
}
fn lower_math_group(node: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
let inner = node
.children_with_tokens()
.filter(|el| !matches!(el.kind(), SyntaxKind::L_BRACE | SyntaxKind::R_BRACE));
Ir::concat([
Ir::verbatim("{"),
lower_math_seq(inner, cx),
Ir::verbatim("}"),
])
}
fn lower_left_right(node: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
Ir::concat(node.children_with_tokens().filter_map(|el| match el {
SyntaxElement::Token(t) if is_collapsible_trivia(t.kind()) => None,
SyntaxElement::Node(n) if n.kind() == SyntaxKind::MATH => {
if math_body_is_empty(&n) {
None
} else {
Some(Ir::concat([
Ir::verbatim(" "),
lower_math_body(&n, cx),
Ir::verbatim(" "),
]))
}
}
SyntaxElement::Token(t) => Some(Ir::verbatim(t.text())),
SyntaxElement::Node(n) => Some(lower_node(&n, cx)),
}))
}
fn math_body_is_empty(node: &SyntaxNode) -> bool {
node.text().to_string().trim().is_empty()
}
fn lower_scripted(node: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
Ir::concat(node.children_with_tokens().filter_map(|el| match el {
SyntaxElement::Token(t) if is_collapsible_trivia(t.kind()) => None,
SyntaxElement::Node(n)
if matches!(n.kind(), SyntaxKind::SUBSCRIPT | SyntaxKind::SUPERSCRIPT) =>
{
Some(lower_script(&n, cx))
}
other => Some(lower_math_element(other, cx)),
}))
}
fn lower_script(node: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
Ir::concat(node.children_with_tokens().filter_map(|el| match el {
SyntaxElement::Token(t) if is_collapsible_trivia(t.kind()) => None,
SyntaxElement::Token(t)
if matches!(t.kind(), SyntaxKind::CARET | SyntaxKind::UNDERSCORE) =>
{
Some(Ir::verbatim(t.text()))
}
SyntaxElement::Node(n) if n.kind() == SyntaxKind::GROUP && strippable_script_arg(&n) => {
Some(lower_stripped_group(&n, cx))
}
other => Some(lower_math_element(other, cx)),
}))
}
fn strippable_script_arg(group: &SyntaxNode) -> bool {
let mut inner = group.children_with_tokens().filter(|el| {
!matches!(
el.kind(),
SyntaxKind::L_BRACE
| SyntaxKind::R_BRACE
| SyntaxKind::WHITESPACE
| SyntaxKind::NEWLINE
)
});
let Some(only) = inner.next() else {
return false; };
if inner.next().is_some() {
return false; }
match only {
SyntaxElement::Token(t)
if t.kind() == SyntaxKind::WORD && t.text().chars().count() == 1 =>
{
next_token_safe_after(group, false)
}
SyntaxElement::Node(n) if is_lone_control_word(&n) => next_token_safe_after(group, true),
_ => false,
}
}
fn is_lone_control_word(node: &SyntaxNode) -> bool {
if node.kind() != SyntaxKind::COMMAND {
return false;
}
let mut children = node.children_with_tokens();
let first_is_control_word = matches!(
children.next(),
Some(SyntaxElement::Token(t)) if t.kind() == SyntaxKind::CONTROL_WORD
);
first_is_control_word && children.next().is_none()
}
fn next_token_safe_after(group: &SyntaxNode, letter_only: bool) -> bool {
let next = group.last_token().and_then(|t| t.next_token());
match next.as_ref().and_then(|t| t.text().chars().next()) {
None => true,
Some(c) if letter_only => !c.is_ascii_alphabetic(),
Some(c) => !crate::parser::lexer::is_word_char(c),
}
}
fn lower_stripped_group(group: &SyntaxNode, cx: LowerCtx<'_>) -> Ir {
match group.children_with_tokens().find(|el| {
!matches!(
el.kind(),
SyntaxKind::L_BRACE
| SyntaxKind::R_BRACE
| SyntaxKind::WHITESPACE
| SyntaxKind::NEWLINE
)
}) {
Some(el) => lower_math_element(el, cx),
None => Ir::nil(),
}
}
fn spans_multiple_lines(node: &SyntaxNode) -> bool {
node.children_with_tokens()
.filter_map(|e| e.into_token())
.any(|t| t.kind() == SyntaxKind::NEWLINE)
}
fn has_verbatim_body(node: &SyntaxNode) -> bool {
node.children_with_tokens()
.filter_map(|e| e.into_token())
.any(|t| t.kind() == SyntaxKind::VERBATIM_BODY)
}
fn is_collapsible_trivia(kind: SyntaxKind) -> bool {
matches!(kind, SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE)
}
fn consume_trivia_run(
first: &SyntaxToken,
iter: &mut Peekable<impl Iterator<Item = SyntaxElement>>,
) -> (usize, String) {
let mut newlines = 0;
let mut trailing_ws = String::new();
absorb(first, &mut newlines, &mut trailing_ws);
loop {
match iter.peek() {
Some(SyntaxElement::Token(tok)) if is_collapsible_trivia(tok.kind()) => {}
_ => break,
}
let token = match iter.next() {
Some(SyntaxElement::Token(tok)) => tok,
_ => unreachable!("peeked a collapsible trivia token"),
};
absorb(&token, &mut newlines, &mut trailing_ws);
}
(newlines, trailing_ws)
}
fn consume_trivia_run_slice(elements: &[SyntaxElement], i: &mut usize) -> usize {
let mut newlines = 0;
while let Some(SyntaxElement::Token(tok)) = elements.get(*i) {
if !is_collapsible_trivia(tok.kind()) {
break;
}
if tok.kind() == SyntaxKind::NEWLINE {
newlines += 1;
}
*i += 1;
}
newlines
}
fn line_is_command_only(elements: &[SyntaxElement], start: usize, cx: LowerCtx<'_>) -> bool {
let mut saw_command = false;
for element in &elements[start..] {
match element {
SyntaxElement::Token(t) if t.kind() == SyntaxKind::NEWLINE => break,
SyntaxElement::Token(t) if t.kind() == SyntaxKind::COMMENT => break,
SyntaxElement::Token(t) if t.kind() == SyntaxKind::WHITESPACE => continue,
SyntaxElement::Node(n)
if n.kind() == SyntaxKind::COMMAND && !command_is_inline(n, cx) =>
{
saw_command = true
}
_ => return false,
}
}
saw_command
}
fn absorb(tok: &SyntaxToken, newlines: &mut usize, trailing_ws: &mut String) {
if tok.kind() == SyntaxKind::NEWLINE {
*newlines += 1;
trailing_ws.clear();
} else {
trailing_ws.push_str(tok.text());
}
}
fn classify_trivia(newlines: usize, trailing_ws: String) -> Ir {
match newlines {
0 => Ir::verbatim(trailing_ws),
1 => Ir::hard_line(),
_ => Ir::empty_line(),
}
}
fn is_trimmable_break(ir: &Ir) -> bool {
match ir {
Ir::HardLine | Ir::EmptyLine | Ir::Nil => true,
Ir::Verbatim { text, force_break } => {
!force_break && text.chars().all(|c| c == ' ' || c == '\t')
}
_ => false,
}
}
fn peel_leading_break(ir: Ir) -> (bool, Ir) {
if is_trimmable_break(&ir) {
return (matches!(ir, Ir::EmptyLine), Ir::Nil);
}
match ir {
Ir::Concat(items) => {
let mut v: Vec<Ir> = items.iter().cloned().collect();
let mut blank = false;
while !v.is_empty() {
let (b, head) = peel_leading_break(v.remove(0));
blank |= b;
if matches!(head, Ir::Nil) {
continue;
}
v.insert(0, head);
break;
}
(blank, Ir::concat(v))
}
other => (false, other),
}
}
fn peel_trailing_break(ir: Ir) -> (bool, Ir) {
if is_trimmable_break(&ir) {
return (matches!(ir, Ir::EmptyLine), Ir::Nil);
}
match ir {
Ir::Concat(items) => {
let mut v: Vec<Ir> = items.iter().cloned().collect();
let mut blank = false;
while let Some(last) = v.pop() {
let (b, tail) = peel_trailing_break(last);
blank |= b;
if matches!(tail, Ir::Nil) {
continue;
}
v.push(tail);
break;
}
(blank, Ir::concat(v))
}
other => (false, other),
}
}
fn trim_leading_break(ir: Ir) -> Ir {
peel_leading_break(ir).1
}
fn trim_trailing_break(ir: Ir) -> Ir {
peel_trailing_break(ir).1
}