mod support;
use support::parser::*;
use texform_core::parse::{
AllowedMode, CommandKind, ContextItem, DelimiterControlItem, ParseConfig,
};
use texform_interface::syntax_node::{
ArgumentKind, ArgumentValue, ContentMode, Delimiter, GroupKind, SyntaxNode,
};
#[test]
fn test_qty_supports_multiple_delimiter_pairs() {
let cases = [
(r"\qty(x)", Delimiter::Char('('), Delimiter::Char(')')),
(r"\qty[x]", Delimiter::Char('['), Delimiter::Char(']')),
(r"\qty{x}", Delimiter::Char('{'), Delimiter::Char('}')),
(r"\qty|x|", Delimiter::Char('|'), Delimiter::Char('|')),
];
for (src, open, close) in cases {
let (result, _) = parse(src, false).unwrap();
let (name, args) = extract_first_command(result);
assert_eq!(name, "qty");
assert_eq!(args.len(), 1);
let arg = expect_arg(&args[0]);
assert_eq!(arg.value, ArgumentValue::MathContent(SyntaxNode::Char('x')));
match arg.kind {
ArgumentKind::Paired {
open: matched_open,
close: matched_close,
} => {
assert_eq!(matched_open, open);
assert_eq!(matched_close, close);
}
other => panic!("Expected paired argument kind, got {:?}", other),
}
}
}
#[test]
fn test_qty_optional_slot_can_be_missing() {
let (result, _) = parse(r"\qty", false).unwrap();
let (name, args) = extract_first_command(result);
assert_eq!(name, "qty");
assert_eq!(args.len(), 1);
assert!(args[0].is_none());
}
#[test]
fn test_arg_true_quantity_commands_require_braces() {
let (pqty_ok, _) = parse(r"\pqty{x}", false).unwrap();
let (name, args) = extract_first_command(pqty_ok);
assert_eq!(name, "pqty");
assert_eq!(args.len(), 2);
assert_eq!(expect_arg(&args[0]).value, ArgumentValue::Boolean(false));
let content = expect_arg(&args[1]);
assert_eq!(
content.value,
ArgumentValue::MathContent(SyntaxNode::Char('x'))
);
match content.kind {
ArgumentKind::Delimited { open, close } => {
assert_eq!(open, Delimiter::Char('{'));
assert_eq!(close, Delimiter::Char('}'));
}
other => panic!("Expected brace-delimited argument, got {:?}", other),
}
let (abs_ok, _) = parse(r"\abs{x}", false).unwrap();
let (name, args) = extract_first_command(abs_ok);
assert_eq!(name, "abs");
assert_eq!(
expect_arg(&args[1]).value,
ArgumentValue::MathContent(SyntaxNode::Char('x'))
);
assert!(parse(r"\pqty(x)", false).is_err());
assert!(parse(r"\abs|x|", false).is_err());
}
#[test]
fn test_eval_uses_nonsymmetric_paired_delimiter() {
let (result, _) = parse(r"\eval(x|", false).unwrap();
let (name, args) = extract_first_command(result);
assert_eq!(name, "eval");
assert_eq!(args.len(), 2);
let star = expect_arg(&args[0]);
assert_eq!(star.kind, ArgumentKind::Star);
assert_eq!(star.value, ArgumentValue::Boolean(false));
let paired = expect_arg(&args[1]);
assert_eq!(
paired.value,
ArgumentValue::MathContent(SyntaxNode::Char('x'))
);
match paired.kind {
ArgumentKind::Paired { open, close } => {
assert_eq!(open, Delimiter::Char('('));
assert_eq!(close, Delimiter::Char('|'));
}
other => panic!("Expected paired argument kind, got {:?}", other),
}
}
#[test]
fn test_dv_and_pdv_group_slots_are_stable() {
let (dv_result, _) = parse(r"\dv{f}", false).unwrap();
let (name, args) = extract_first_command(dv_result);
assert_eq!(name, "dv");
assert_eq!(args.len(), 4);
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::Boolean(false),
"star slot should always exist",
);
assert!(args[1].is_none(), "optional bracket slot should be None");
assert_eq!(
expect_arg(&args[2]).value,
ArgumentValue::MathContent(SyntaxNode::Char('f'))
);
assert!(args[3].is_none(), "group slot should be None when absent");
let (pdv_result, _) = parse(r"\pdv{f}{x}{y}", false).unwrap();
let (name, args) = extract_first_command(pdv_result);
assert_eq!(name, "pdv");
assert_eq!(args.len(), 5);
assert_eq!(expect_arg(&args[0]).value, ArgumentValue::Boolean(false));
assert!(args[1].is_none());
assert_eq!(
expect_arg(&args[2]).value,
ArgumentValue::MathContent(SyntaxNode::Char('f'))
);
assert_eq!(expect_arg(&args[3]).kind, ArgumentKind::Group);
assert_eq!(expect_arg(&args[4]).kind, ArgumentKind::Group);
}
#[test]
fn test_braket_optional_group_slot() {
let (result_full, _) = parse(r"\braket{a}{b}", false).unwrap();
let (_, args_full) = extract_first_command(result_full);
assert_eq!(args_full.len(), 3);
assert_eq!(
expect_arg(&args_full[0]).value,
ArgumentValue::Boolean(false)
);
assert_eq!(
expect_arg(&args_full[1]).value,
ArgumentValue::MathContent(SyntaxNode::Char('a'))
);
assert_eq!(expect_arg(&args_full[2]).kind, ArgumentKind::Group);
let (result_short, _) = parse(r"\braket{a}", false).unwrap();
let (_, args_short) = extract_first_command(result_short);
assert_eq!(args_short.len(), 3);
assert!(args_short[2].is_none());
}
#[test]
fn test_exp_does_not_consume_star_without_s_slot() {
let (result, _) = parse(r"\exp*", false).unwrap();
match result {
SyntaxNode::Root { children, .. } => {
assert_eq!(children.len(), 2);
match &children[0] {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "exp");
assert!(args.is_empty());
}
other => panic!("Expected exp command, got {:?}", other),
}
assert_eq!(children[1], SyntaxNode::Char('*'));
}
_ => panic!("Expected root Group"),
}
}
#[test]
fn test_exp_does_not_consume_brackets_without_optional_slot() {
let (result, _) = parse(r"\exp[x]", false).unwrap();
match result {
SyntaxNode::Root { children, .. } => {
assert_eq!(children.len(), 4);
match &children[0] {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "exp");
assert!(args.is_empty());
}
other => panic!("Expected exp command, got {:?}", other),
}
assert_eq!(children[1], SyntaxNode::Char('['));
assert_eq!(children[2], SyntaxNode::Char('x'));
assert_eq!(children[3], SyntaxNode::Char(']'));
}
_ => panic!("Expected root Group"),
}
}
#[test]
fn test_bare_brackets_parse_as_regular_characters() {
let (result, _) = parse("[a]", false).unwrap();
match result {
SyntaxNode::Root { children, .. } => {
assert_eq!(children.len(), 3);
assert_eq!(children[0], SyntaxNode::Char('['));
assert_eq!(children[1], SyntaxNode::Char('a'));
assert_eq!(children[2], SyntaxNode::Char(']'));
}
other => panic!("Expected root node, got {:?}", other),
}
}
#[test]
fn test_no_leading_space_after_single_token_m_for_optional_brackets() {
let ctx = test_context_with_items([command_item(
"probe",
CommandKind::Prefix,
AllowedMode::Math,
"m !o",
)]);
let spaced = ctx.parse(r"\probe a [b]", &ParseConfig::STRICT);
assert!(
spaced.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
spaced.diagnostics
);
let spaced_node = spaced
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone();
match spaced_node {
SyntaxNode::Root { children, .. } => {
assert_eq!(children.len(), 4);
match &children[0] {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "probe");
assert_eq!(args.len(), 2);
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::MathContent(SyntaxNode::Char('a'))
);
assert!(args[1].is_none(), "spaced !o slot should not match");
}
other => panic!("Expected probe command, got {:?}", other),
}
assert_eq!(children[1], SyntaxNode::Char('['));
assert_eq!(children[2], SyntaxNode::Char('b'));
assert_eq!(children[3], SyntaxNode::Char(']'));
}
other => panic!("Expected root node, got {:?}", other),
}
let tight = ctx.parse(r"\probe a[b]", &ParseConfig::STRICT);
assert!(
tight.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
tight.diagnostics
);
let tight_node = tight
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone();
match tight_node {
SyntaxNode::Root { children, .. } => {
assert_eq!(children.len(), 1);
match &children[0] {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "probe");
assert_eq!(args.len(), 2);
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::MathContent(SyntaxNode::Char('a'))
);
assert_eq!(
expect_arg(&args[1]).value,
ArgumentValue::MathContent(SyntaxNode::Char('b'))
);
}
other => panic!("Expected probe command, got {:?}", other),
}
}
other => panic!("Expected root node, got {:?}", other),
}
}
#[test]
fn test_no_leading_space_after_single_token_m_for_group_slot() {
let ctx = test_context_with_items([command_item(
"probe",
CommandKind::Prefix,
AllowedMode::Math,
"s o m !g",
)]);
let spaced = ctx.parse(r"\probe*[n]f {x}", &ParseConfig::STRICT);
assert!(
spaced.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
spaced.diagnostics
);
let spaced_node = spaced
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone();
match spaced_node {
SyntaxNode::Root { children, .. } => {
assert_eq!(children.len(), 2);
match &children[0] {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "probe");
assert_eq!(args.len(), 4);
assert_eq!(expect_arg(&args[0]).value, ArgumentValue::Boolean(true));
assert_eq!(
expect_arg(&args[1]).value,
ArgumentValue::MathContent(SyntaxNode::Char('n'))
);
assert_eq!(
expect_arg(&args[2]).value,
ArgumentValue::MathContent(SyntaxNode::Char('f'))
);
assert!(args[3].is_none(), "spaced !g slot should not match");
}
other => panic!("Expected probe command, got {:?}", other),
}
match &children[1] {
SyntaxNode::Group {
kind: GroupKind::Explicit,
children: group_children,
..
} => {
assert_eq!(group_children.len(), 1);
assert_eq!(group_children[0], SyntaxNode::Char('x'));
}
other => panic!("Expected trailing explicit group, got {:?}", other),
}
}
other => panic!("Expected root node, got {:?}", other),
}
let tight = ctx.parse(r"\probe*[n]f{x}", &ParseConfig::STRICT);
assert!(
tight.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
tight.diagnostics
);
let tight_node = tight
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone();
match tight_node {
SyntaxNode::Root { children, .. } => {
assert_eq!(children.len(), 1);
match &children[0] {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "probe");
assert_eq!(args.len(), 4);
assert_eq!(expect_arg(&args[0]).value, ArgumentValue::Boolean(true));
assert_eq!(
expect_arg(&args[1]).value,
ArgumentValue::MathContent(SyntaxNode::Char('n'))
);
assert_eq!(
expect_arg(&args[2]).value,
ArgumentValue::MathContent(SyntaxNode::Char('f'))
);
assert_eq!(expect_arg(&args[3]).kind, ArgumentKind::Group);
assert_eq!(
expect_arg(&args[3]).value,
ArgumentValue::MathContent(SyntaxNode::Char('x'))
);
}
other => panic!("Expected probe command, got {:?}", other),
}
}
other => panic!("Expected root node, got {:?}", other),
}
}
#[test]
fn test_required_group_form_enforces_braces() {
let ctx = test_context_with_items([command_item(
"reqgrp",
CommandKind::Prefix,
AllowedMode::Math,
"m{}",
)]);
let present = ctx.parse(r"\reqgrp{x}", &ParseConfig::STRICT);
assert!(
present.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
present.diagnostics
);
let present_node = present
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone();
match present_node {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "reqgrp");
assert_eq!(args.len(), 1);
assert_eq!(expect_arg(&args[0]).kind, ArgumentKind::Group);
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::MathContent(SyntaxNode::Char('x'))
);
}
other => panic!("Expected reqgrp command, got {:?}", other),
},
other => panic!("Expected root node, got {:?}", other),
}
let missing = ctx.parse(r"\reqgrp x", &ParseConfig::STRICT);
assert!(
missing.document().is_none(),
"missing required group should fail"
);
assert!(
!missing.diagnostics.is_empty(),
"missing required group should report diagnostics, got {:?}",
missing.diagnostics
);
let wrong_form = ctx.parse(r"\reqgrp|x|", &ParseConfig::STRICT);
assert!(
wrong_form.document().is_none(),
"non-braced required group should fail"
);
assert!(
!wrong_form.diagnostics.is_empty(),
"non-braced required group should report diagnostics, got {:?}",
wrong_form.diagnostics
);
}
#[test]
fn test_group_form_supports_dimension_kind() {
let ctx = test_context_with_items([command_item(
"gdim",
CommandKind::Prefix,
AllowedMode::Math,
"g:L",
)]);
let missing = ctx.parse(r"\gdim", &ParseConfig::STRICT);
assert!(
missing.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
missing.diagnostics
);
let missing_node = missing
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone();
match missing_node {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "gdim");
assert_eq!(args.len(), 1);
assert!(args[0].is_none(), "group slot should be None when absent");
}
other => panic!("Expected gdim command, got {:?}", other),
},
other => panic!("Expected root node, got {:?}", other),
}
let present = ctx.parse(r"\gdim{1.5em}", &ParseConfig::STRICT);
assert!(
present.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
present.diagnostics
);
let present_node = present
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone();
match present_node {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "gdim");
assert_eq!(args.len(), 1);
assert_eq!(expect_arg(&args[0]).kind, ArgumentKind::Group);
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::Dimension("1.5em".to_string())
);
}
other => panic!("Expected gdim command, got {:?}", other),
},
other => panic!("Expected root node, got {:?}", other),
}
}
#[test]
fn test_required_group_form_composes_with_star_and_standard_slots() {
let ctx = test_context_with_items([command_item(
"probe",
CommandKind::Prefix,
AllowedMode::Math,
"s m{} m",
)]);
let basic = ctx.parse(r"\probe{A}B", &ParseConfig::STRICT);
assert!(
basic.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
basic.diagnostics
);
let (name, args) = extract_first_command(
basic
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone(),
);
assert_eq!(name, "probe");
assert_eq!(args.len(), 3);
assert_eq!(expect_arg(&args[0]).kind, ArgumentKind::Star);
assert_eq!(expect_arg(&args[0]).value, ArgumentValue::Boolean(false));
assert_eq!(expect_arg(&args[1]).kind, ArgumentKind::Group);
assert_eq!(
expect_arg(&args[1]).value,
ArgumentValue::MathContent(SyntaxNode::Char('A'))
);
assert_eq!(expect_arg(&args[2]).kind, ArgumentKind::Mandatory);
assert_eq!(
expect_arg(&args[2]).value,
ArgumentValue::MathContent(SyntaxNode::Char('B'))
);
let starred = ctx.parse(r"\probe*{A}B", &ParseConfig::STRICT);
assert!(
starred.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
starred.diagnostics
);
let (_, starred_args) = extract_first_command(
starred
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone(),
);
assert_eq!(
expect_arg(&starred_args[0]).value,
ArgumentValue::Boolean(true)
);
assert_eq!(expect_arg(&starred_args[1]).kind, ArgumentKind::Group);
assert_eq!(expect_arg(&starred_args[2]).kind, ArgumentKind::Mandatory);
let missing = ctx.parse(r"\probe B", &ParseConfig::STRICT);
assert!(
missing.document().is_none(),
"missing required group slot should fail"
);
assert!(
!missing.diagnostics.is_empty(),
"missing required group slot should report diagnostics, got {:?}",
missing.diagnostics
);
}
#[test]
fn test_group_form_supports_delimiter_kind() {
let ctx = test_context_with_items([command_item(
"gdelim",
CommandKind::Prefix,
AllowedMode::Math,
"g:D",
)]);
let output = ctx.parse(r"\gdelim{|}", &ParseConfig::STRICT);
assert!(
output.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
output.diagnostics
);
let node = output
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone();
match node {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "gdelim");
assert_eq!(args.len(), 1);
assert_eq!(expect_arg(&args[0]).kind, ArgumentKind::Group);
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::Delimiter(Delimiter::Char('|'))
);
}
other => panic!("Expected gdelim command, got {:?}", other),
},
other => panic!("Expected root node, got {:?}", other),
}
}
#[test]
fn test_nullable_delimiter_argument_accepts_empty_required_group() {
let ctx = test_context_with_items(vec![
ContextItem::from(command_item(
"ndelim",
CommandKind::Prefix,
AllowedMode::Math,
"m:D?",
)),
ContextItem::from(command_item(
"strictdelim",
CommandKind::Prefix,
AllowedMode::Math,
"m:D",
)),
ContextItem::from(DelimiterControlItem::new("langle")),
]);
let empty = ctx.parse(r"\ndelim{}", &ParseConfig::STRICT);
assert!(
empty.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
empty.diagnostics
);
let (_, empty_args) = extract_first_command(
empty
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone(),
);
assert_eq!(
expect_arg(&empty_args[0]).value,
ArgumentValue::Delimiter(Delimiter::None)
);
let explicit = ctx.parse(r"\ndelim\langle", &ParseConfig::STRICT);
assert!(
explicit.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
explicit.diagnostics
);
let (_, explicit_args) = extract_first_command(
explicit
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone(),
);
assert_eq!(
expect_arg(&explicit_args[0]).value,
ArgumentValue::Delimiter(Delimiter::Control("langle"))
);
let strict_empty = ctx.parse(r"\strictdelim{}", &ParseConfig::STRICT);
assert!(
strict_empty.document().is_none(),
"non-nullable delimiter should reject empty braces"
);
assert!(
!strict_empty.diagnostics.is_empty(),
"non-nullable delimiter should report diagnostics"
);
}
#[test]
fn test_nullable_delimiter_group_accepts_empty_group() {
let ctx = test_context_with_items([command_item(
"gdelimnull",
CommandKind::Prefix,
AllowedMode::Math,
"m{}:D?",
)]);
let output = ctx.parse(r"\gdelimnull{}", &ParseConfig::STRICT);
assert!(
output.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
output.diagnostics
);
let (_, args) = extract_first_command(
output
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone(),
);
assert_eq!(expect_arg(&args[0]).kind, ArgumentKind::Group);
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::Delimiter(Delimiter::None)
);
}
#[test]
fn test_required_group_and_delimited_forms_have_distinct_ast_kinds() {
let ctx = test_context_with_items([
command_item("reqgrp", CommandKind::Prefix, AllowedMode::Math, "m{}"),
command_item("reqdelim", CommandKind::Prefix, AllowedMode::Math, "r{}"),
]);
let group = ctx.parse(r"\reqgrp{x}", &ParseConfig::STRICT);
assert!(
group.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
group.diagnostics
);
let (_, group_args) = extract_first_command(
group
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone(),
);
assert_eq!(expect_arg(&group_args[0]).kind, ArgumentKind::Group);
let delimited = ctx.parse(r"\reqdelim{x}", &ParseConfig::STRICT);
assert!(
delimited.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
delimited.diagnostics
);
let (_, delimited_args) = extract_first_command(
delimited
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone(),
);
match expect_arg(&delimited_args[0]).kind {
ArgumentKind::Delimited { open, close } => {
assert_eq!(open, Delimiter::Char('{'));
assert_eq!(close, Delimiter::Char('}'));
}
other => panic!("Expected delimited argument kind, got {:?}", other),
}
}
#[test]
fn test_mqty_supports_star_plus_optional_paired_slot() {
let (starred, _) = parse(r"\mqty*|x|", false).unwrap();
let (name, args) = extract_first_command(starred);
assert_eq!(name, "mqty");
assert_eq!(args.len(), 2);
assert_eq!(expect_arg(&args[0]).kind, ArgumentKind::Star);
assert_eq!(expect_arg(&args[0]).value, ArgumentValue::Boolean(true));
assert_eq!(
expect_arg(&args[1]).value,
ArgumentValue::MathContent(SyntaxNode::Char('x'))
);
match expect_arg(&args[1]).kind {
ArgumentKind::Paired { open, close } => {
assert_eq!(open, Delimiter::Char('|'));
assert_eq!(close, Delimiter::Char('|'));
}
other => panic!("Expected paired argument kind, got {:?}", other),
}
let (missing, _) = parse(r"\mqty*", false).unwrap();
let (_, missing_args) = extract_first_command(missing);
assert_eq!(missing_args.len(), 2);
assert!(missing_args[1].is_none(), "paired slot should be optional");
}
#[test]
fn test_dd_supports_optional_then_paired_slots() {
let (basic, _) = parse(r"\dd{x}", false).unwrap();
let (name, args) = extract_first_command(basic);
assert_eq!(name, "dd");
assert_eq!(args.len(), 2);
assert!(args[0].is_none(), "optional bracket slot should be None");
assert_eq!(
expect_arg(&args[1]).value,
ArgumentValue::MathContent(SyntaxNode::Char('x'))
);
match expect_arg(&args[1]).kind {
ArgumentKind::Paired { open, close } => {
assert_eq!(open, Delimiter::Char('{'));
assert_eq!(close, Delimiter::Char('}'));
}
other => panic!("Expected paired argument kind, got {:?}", other),
}
let (with_opt, _) = parse(r"\dd[y](x)", false).unwrap();
let (_, args_with_opt) = extract_first_command(with_opt);
assert_eq!(
expect_arg(&args_with_opt[0]).value,
ArgumentValue::MathContent(SyntaxNode::Char('y'))
);
match expect_arg(&args_with_opt[1]).kind {
ArgumentKind::Paired { open, close } => {
assert_eq!(open, Delimiter::Char('('));
assert_eq!(close, Delimiter::Char(')'));
}
other => panic!("Expected paired argument kind, got {:?}", other),
}
let (unmatched, _) = parse(r"\dd[y]|x|", false).unwrap();
match unmatched {
SyntaxNode::Root { children, .. } => {
assert_eq!(children.len(), 4);
match &children[0] {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "dd");
assert_eq!(args.len(), 2);
assert!(
args[1].is_none(),
"non-candidate delimiter should not be consumed"
);
}
other => panic!("Expected dd command, got {:?}", other),
}
assert_eq!(children[1], SyntaxNode::Char('|'));
assert_eq!(children[2], SyntaxNode::Char('x'));
assert_eq!(children[3], SyntaxNode::Char('|'));
}
other => panic!("Expected root node, got {:?}", other),
}
}
#[test]
fn test_paired_form_required_vs_optional_semantics() {
let ctx = test_context_with_items([
command_item("mustpair", CommandKind::Prefix, AllowedMode::Math, "r<(,)>"),
command_item(
"maybepair",
CommandKind::Prefix,
AllowedMode::Math,
"d<(,)>",
),
]);
let required_ok = ctx.parse(r"\mustpair(x)", &ParseConfig::STRICT);
assert!(
required_ok.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
required_ok.diagnostics
);
assert!(
required_ok.document().is_some(),
"required paired arg should parse"
);
let required_missing = ctx.parse(r"\mustpair", &ParseConfig::STRICT);
assert!(
required_missing.document().is_none(),
"missing required paired arg should fail"
);
assert!(
!required_missing.diagnostics.is_empty(),
"missing required paired arg should report diagnostics, got {:?}",
required_missing.diagnostics
);
let optional_unmatched = ctx.parse(r"\maybepair[x]", &ParseConfig::STRICT);
assert!(
optional_unmatched.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
optional_unmatched.diagnostics
);
let node = optional_unmatched
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone();
match node {
SyntaxNode::Root { children, .. } => {
assert_eq!(children.len(), 4);
match &children[0] {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "maybepair");
assert_eq!(args.len(), 1);
assert!(args[0].is_none(), "optional paired slot should stay empty");
}
other => panic!("Expected maybepair command, got {:?}", other),
}
assert_eq!(children[1], SyntaxNode::Char('['));
assert_eq!(children[2], SyntaxNode::Char('x'));
assert_eq!(children[3], SyntaxNode::Char(']'));
}
other => panic!("Expected root node, got {:?}", other),
}
}
#[test]
fn test_environment_star_in_name_is_independent_from_s_arg_slot() {
let ctx = test_context_with_items([
environment_item("probenv", AllowedMode::Math, ContentMode::Math, "s"),
environment_item("probenv*", AllowedMode::Math, ContentMode::Math, "s"),
]);
let starred_name = ctx.parse(r"\begin{probenv*}x\end{probenv*}", &ParseConfig::STRICT);
assert!(
starred_name.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
starred_name.diagnostics
);
let starred_name_node = starred_name
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone();
assert!(
matches!(starred_name_node, SyntaxNode::Root { .. }),
"expected root node"
);
match starred_name_node {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Environment { name, args, .. } => {
assert_eq!(name, "probenv*");
assert_eq!(args.len(), 1);
assert_eq!(expect_arg(&args[0]).value, ArgumentValue::Boolean(false));
}
other => panic!("Expected environment node, got {:?}", other),
},
other => panic!("Expected root node, got {:?}", other),
}
let star_arg = ctx.parse(r"\begin{probenv}*x\end{probenv}", &ParseConfig::STRICT);
assert!(
star_arg.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
star_arg.diagnostics
);
let node = star_arg
.document()
.unwrap_or_else(|| panic!("expected parse result"))
.to_syntax()
.clone();
match node {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Environment {
name, args, body, ..
} => {
assert_eq!(name, "probenv");
assert_eq!(args.len(), 1);
assert_eq!(expect_arg(&args[0]).kind, ArgumentKind::Star);
assert_eq!(expect_arg(&args[0]).value, ArgumentValue::Boolean(true));
match &**body {
SyntaxNode::Group { children, .. } => {
assert_eq!(children.len(), 1);
assert_eq!(children[0], SyntaxNode::Char('x'));
}
other => panic!("Expected environment body group, got {:?}", other),
}
}
other => panic!("Expected environment node, got {:?}", other),
},
other => panic!("Expected root node, got {:?}", other),
}
}