mod support;
use support::{
assert_first_diagnostic_span_eq, collect_messages, command_item, contains_command_named,
contains_error_node, parse_many_with_items, parse_single_with_items, parse_with_items,
};
use texform_core::parse::{
AllowedMode, CommandKind, ContextItem, ParseContext, ParseContextBuilder, ParseResult,
};
use texform_interface::syntax_node::{
Argument, ArgumentKind, ArgumentValue, ContentMode, Delimiter, GroupKind, SyntaxNode,
};
fn text_command_item() -> ContextItem {
command_item("text", CommandKind::Prefix, AllowedMode::Math, "m:T")
}
fn frac_command_item() -> ContextItem {
command_item("frac", CommandKind::Prefix, AllowedMode::Math, "m m")
}
fn underline_math_item() -> ContextItem {
command_item("underline", CommandKind::Prefix, AllowedMode::Math, "m")
}
fn underline_text_item() -> ContextItem {
command_item("underline", CommandKind::Prefix, AllowedMode::Text, "m:T")
}
fn content_test_context() -> ParseContext {
ParseContextBuilder::empty()
.insert_item(text_command_item())
.insert_item(frac_command_item())
.insert_item(underline_math_item())
.insert_item(underline_text_item())
.build()
.expect("content test context should build")
}
fn expect_arg(slot: &Option<Argument>) -> &Argument {
slot.as_ref()
.unwrap_or_else(|| panic!("expected argument slot to be present"))
}
fn first_command(output: &ParseResult) -> (String, Vec<Option<Argument>>) {
let result = output
.document()
.unwrap_or_else(|| panic!("expected parse result"));
match result.to_syntax() {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Command { name, args, .. } => (name.clone(), args.clone()),
other => panic!("expected command node, got {:?}", other),
},
other => panic!("expected root node, got {:?}", other),
}
}
fn single_root_child(output: &ParseResult) -> SyntaxNode {
let result = output
.document()
.unwrap_or_else(|| panic!("expected parse result"));
match result.to_syntax() {
SyntaxNode::Root { children, .. } => {
assert_eq!(children.len(), 1, "expected a single root child");
children[0].clone()
}
other => panic!("expected root node, got {:?}", other),
}
}
fn expect_command_with_math_arg(
node: &SyntaxNode,
expected_name: &str,
arg_index: usize,
) -> SyntaxNode {
match node {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, expected_name);
match &expect_arg(&args[arg_index]).value {
ArgumentValue::MathContent(value) => value.clone(),
other => panic!("expected math content argument, got {:?}", other),
}
}
other => panic!("expected command node, got {:?}", other),
}
}
fn known_command(name: &str) -> SyntaxNode {
SyntaxNode::Command {
name: name.to_string(),
args: Vec::new(),
known: true,
}
}
fn assert_prefix_shorthand_keeps_script_outside(
src: &str,
command_name: &str,
expected_arg: SyntaxNode,
expected_subscript: Option<SyntaxNode>,
expected_superscript: Option<SyntaxNode>,
) {
let output = ParseContext::shared().parse(src, &texform_core::parse::ParseConfig::STRICT);
assert!(
output.diagnostics.is_empty(),
"unexpected diagnostics for {src}: {:?}",
output.diagnostics
);
match single_root_child(&output) {
SyntaxNode::Scripted {
base,
subscript,
superscript,
} => {
assert_eq!(
expect_command_with_math_arg(&base, command_name, 0),
expected_arg
);
assert_eq!(subscript.map(|node| *node), expected_subscript);
assert_eq!(superscript.map(|node| *node), expected_superscript);
}
other => panic!("expected outer scripted command for {src}, got {:?}", other),
}
}
#[test]
fn integer_argument_is_verified_by_parser() {
let items = [command_item(
"romannumeral",
CommandKind::Prefix,
AllowedMode::Both,
"m:I",
)];
let valid = parse_single_with_items(&items, r"\romannumeral+42", true);
assert!(
valid.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
valid.diagnostics
);
let (name, args) = first_command(&valid);
assert_eq!(name, "romannumeral");
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::Integer("+42".to_string())
);
let invalid = parse_single_with_items(&items, r"\romannumeral+", true);
assert!(invalid.document().is_none(), "invalid integer should fail");
assert!(
!invalid.diagnostics.is_empty(),
"expected integer diagnostics"
);
}
#[test]
fn non_nullable_integer_argument_rejects_empty_group() {
let items = [command_item(
"romannumeral",
CommandKind::Prefix,
AllowedMode::Both,
"m:I",
)];
let output = parse_single_with_items(&items, r"\romannumeral{}", true);
assert!(output.document().is_none(), "empty integer should fail");
assert!(
!output.diagnostics.is_empty(),
"expected integer diagnostics"
);
}
#[test]
fn genfrac_accepts_empty_and_integer_style_arguments() {
let outputs = parse_many_with_items(
&[],
&[
r"\genfrac{}{}{0.0pt}{}{a}{b}",
r"\genfrac{}{}{0.0pt}{1}{a}{b}",
],
Some(&["base", "ams"]),
&texform_core::parse::ParseConfig::STRICT,
);
assert_eq!(outputs.len(), 2);
for item in outputs {
assert!(
item.output.diagnostics.is_empty(),
"unexpected diagnostics for {}: {:?}",
item.input,
item.output.diagnostics
);
assert!(
item.output.document().is_some(),
"expected parse result for {}",
item.input
);
}
}
#[test]
fn dimension_argument_is_verified_by_parser() {
let items = [command_item(
"hspace",
CommandKind::Prefix,
AllowedMode::Both,
"m:L",
)];
let valid = parse_single_with_items(&items, r"\hspace{1,5 em}", true);
assert!(
valid.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
valid.diagnostics
);
let (name, args) = first_command(&valid);
assert_eq!(name, "hspace");
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::Dimension("1.5em".to_string())
);
let invalid = parse_single_with_items(&items, r"\hspaceabc", true);
assert!(
invalid.document().is_none(),
"invalid dimension should fail"
);
assert!(
!invalid.diagnostics.is_empty(),
"expected dimension diagnostics"
);
}
#[test]
fn dimension_argument_accepts_shared_unit_set_and_rejects_unknown_units() {
let items = [command_item(
"hspace",
CommandKind::Prefix,
AllowedMode::Both,
"m:L",
)];
for unit in ["em", "ex", "pt", "pc", "px", "in", "cm", "mm", "mu"] {
let src = format!(r"\hspace{{1{unit}}}");
let output = parse_single_with_items(&items, &src, true);
assert!(
output.diagnostics.is_empty(),
"unexpected diagnostics for {src}: {:?}",
output.diagnostics
);
let (_name, args) = first_command(&output);
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::Dimension(format!("1{unit}"))
);
}
let invalid = parse_single_with_items(&items, r"\hspace{1zz}", true);
assert!(invalid.document().is_none(), "unsupported unit should fail");
assert!(
!invalid.diagnostics.is_empty(),
"expected diagnostics for unsupported unit, got {:?}",
invalid.diagnostics
);
}
#[test]
fn keyval_argument_accepts_nested_and_escaped_shapes() {
let items = [command_item(
"includegraphics",
CommandKind::Prefix,
AllowedMode::Both,
"o:K m:T",
)];
let outputs = parse_many_with_items(
&items,
&[
r"\includegraphics[key=val]{file}",
r"\includegraphics[key={a,b},other=c]{file}",
r"\includegraphics[key=\{,other=c]{file}",
],
None,
&texform_core::parse::ParseConfig::STRICT,
);
let expected = ["key=val", "key={a,b},other=c", r"key=\{,other=c"];
for (item, expected_keyval) in outputs.iter().zip(expected) {
assert!(
item.output.diagnostics.is_empty(),
"unexpected diagnostics for {}: {:?}",
item.input,
item.output.diagnostics
);
let (_, args) = first_command(&item.output);
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::KeyVal(expected_keyval.to_string())
);
}
}
#[test]
fn keyval_argument_rejects_invalid_shapes() {
let items = [command_item(
"includegraphics",
CommandKind::Prefix,
AllowedMode::Both,
"o:K m:T",
)];
let outputs = parse_many_with_items(
&items,
&[
r"\includegraphics[key=]{file}",
r"\includegraphics[=value]{file}",
r"\includegraphics[key={a]{file}",
],
None,
&texform_core::parse::ParseConfig::STRICT,
);
for item in &outputs {
assert!(
item.output.document().is_none(),
"{} should fail",
item.input
);
assert!(
!item.output.diagnostics.is_empty(),
"expected diagnostics for {}",
item.input
);
}
}
#[test]
fn keyval_argument_diagnostic_span_covers_bracket_argument() {
let items = [command_item(
"includegraphics",
CommandKind::Prefix,
AllowedMode::Both,
"o:K m:T",
)];
let src = r"\includegraphics[key=]{file}";
let output = parse_with_items(&items, src, true);
assert!(output.document().is_none(), "invalid keyval should fail");
let diagnostic = output
.diagnostics
.first()
.expect("expected keyval diagnostic");
assert_eq!(diagnostic.message, "keyval missing value");
assert_eq!(&src[diagnostic.span.start..diagnostic.span.end], "[key=]");
}
#[test]
fn optional_bracket_content_stops_at_top_level_closer() {
let items = [command_item(
"includegraphics",
CommandKind::Prefix,
AllowedMode::Both,
"o:K m:T",
)];
let output = parse_with_items(&items, r"\includegraphics[key={[[},width=1em]{file}", true);
assert!(
output.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
output.diagnostics
);
let (_, args) = first_command(&output);
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::KeyVal("key={[[},width=1em".to_string())
);
}
#[test]
fn delimited_argument_collects_nested_content() {
let items = [command_item(
"reqdelim",
CommandKind::Prefix,
AllowedMode::Math,
"r{}",
)];
let output = parse_with_items(&items, r"\reqdelim{a{b}c}", true);
assert!(
output.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
output.diagnostics
);
let (_, args) = first_command(&output);
match expect_arg(&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 mandatory_argument_normalizes_single_explicit_group() {
let items = [command_item(
"probe",
CommandKind::Prefix,
AllowedMode::Math,
"m",
)];
let output = parse_with_items(&items, r"\probe{x}", true);
assert!(
output.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
output.diagnostics
);
let (_, args) = first_command(&output);
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::MathContent(SyntaxNode::Char('x'))
);
}
#[test]
fn non_braced_mandatory_math_argument_leaves_subscript_outside_prefix_command() {
assert_prefix_shorthand_keeps_script_outside(
r"\vec A_\mu",
"vec",
SyntaxNode::Char('A'),
Some(known_command("mu")),
None,
);
}
#[test]
fn non_braced_mandatory_math_argument_leaves_superscript_outside_prefix_command() {
assert_prefix_shorthand_keeps_script_outside(
r"\bar C^\mu",
"bar",
SyntaxNode::Char('C'),
None,
Some(known_command("mu")),
);
assert_prefix_shorthand_keeps_script_outside(
r"\widehat \lambda^k",
"widehat",
known_command("lambda"),
None,
Some(SyntaxNode::Char('k')),
);
}
#[test]
fn non_braced_mandatory_math_argument_leaves_prime_outside_prefix_command() {
let output =
ParseContext::shared().parse(r"\vec A'", &texform_core::parse::ParseConfig::STRICT);
assert!(
output.diagnostics.is_empty(),
"unexpected diagnostics for \\vec A': {:?}",
output.diagnostics
);
match single_root_child(&output) {
SyntaxNode::Scripted {
base,
subscript,
superscript,
} => {
assert_eq!(
expect_command_with_math_arg(&base, "vec", 0),
SyntaxNode::Char('A')
);
assert!(subscript.is_none());
assert_eq!(
superscript.as_deref(),
Some(&SyntaxNode::Prime { count: 1 }),
"prime should stay outside \\vec"
);
}
other => panic!("expected outer scripted command, got {:?}", other),
}
let output =
ParseContext::shared().parse(r"\bar C'", &texform_core::parse::ParseConfig::STRICT);
assert!(
output.diagnostics.is_empty(),
"unexpected diagnostics for \\bar C': {:?}",
output.diagnostics
);
match single_root_child(&output) {
SyntaxNode::Scripted {
base,
subscript,
superscript,
} => {
assert_eq!(
expect_command_with_math_arg(&base, "bar", 0),
SyntaxNode::Char('C')
);
assert!(subscript.is_none());
assert_eq!(
superscript.as_deref(),
Some(&SyntaxNode::Prime { count: 1 }),
"prime should stay outside \\bar"
);
}
other => panic!("expected outer scripted command, got {:?}", other),
}
}
#[test]
fn non_braced_sqrt_and_frac_arguments_leave_following_scripts_outside() {
let sqrt =
ParseContext::shared().parse(r"\sqrt x^2", &texform_core::parse::ParseConfig::STRICT);
assert!(
sqrt.diagnostics.is_empty(),
"unexpected diagnostics for \\sqrt x^2: {:?}",
sqrt.diagnostics
);
match single_root_child(&sqrt) {
SyntaxNode::Scripted {
base,
subscript,
superscript,
} => {
assert_eq!(
expect_command_with_math_arg(&base, "sqrt", 1),
SyntaxNode::Char('x')
);
assert!(subscript.is_none());
assert_eq!(superscript.map(|node| *node), Some(SyntaxNode::Char('2')));
}
other => panic!("expected outer scripted sqrt command, got {:?}", other),
}
let frac =
ParseContext::shared().parse(r"\frac a b_c", &texform_core::parse::ParseConfig::STRICT);
assert!(
frac.diagnostics.is_empty(),
"unexpected diagnostics for \\frac a b_c: {:?}",
frac.diagnostics
);
match single_root_child(&frac) {
SyntaxNode::Scripted {
base,
subscript,
superscript,
} => {
assert_eq!(
expect_command_with_math_arg(&base, "frac", 0),
SyntaxNode::Char('a')
);
assert_eq!(
expect_command_with_math_arg(&base, "frac", 1),
SyntaxNode::Char('b')
);
assert_eq!(subscript.map(|node| *node), Some(SyntaxNode::Char('c')));
assert!(superscript.is_none());
}
other => panic!("expected outer scripted frac command, got {:?}", other),
}
}
#[test]
fn non_braced_mandatory_math_argument_accepts_leading_subscript_argument() {
let output = ParseContext::shared().parse(
r"\mod _ { 2 \pi }",
&texform_core::parse::ParseConfig::STRICT,
);
assert!(
output.diagnostics.is_empty(),
"unexpected diagnostics for \\mod _ {{ 2 \\pi }}: {:?}",
output.diagnostics
);
match single_root_child(&output) {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "mod");
match &expect_arg(&args[0]).value {
ArgumentValue::MathContent(SyntaxNode::Scripted {
base,
subscript,
superscript,
}) => {
assert_eq!(
**base,
SyntaxNode::Group {
mode: ContentMode::Math,
kind: GroupKind::Implicit,
children: Vec::new(),
}
);
assert_eq!(
subscript.as_deref(),
Some(&SyntaxNode::Group {
mode: ContentMode::Math,
kind: GroupKind::Explicit,
children: vec![SyntaxNode::Char('2'), known_command("pi")],
})
);
assert!(superscript.is_none());
}
other => panic!("expected leading subscript math argument, got {:?}", other),
}
}
other => panic!("expected command node, got {:?}", other),
}
}
#[test]
fn non_braced_skew_argument_accepts_leading_subscript_argument() {
let output = ParseContext::shared().parse(
r"\skew 5 \hat { \bar { \psi } } _ { - }",
&texform_core::parse::ParseConfig::STRICT,
);
assert!(
output.diagnostics.is_empty(),
"unexpected diagnostics for skew argument shorthand: {:?}",
output.diagnostics
);
}
#[test]
fn runtime_math_shorthand_argument_leaves_scripts_outside_custom_command() {
let items = [command_item(
"myvec",
CommandKind::Prefix,
AllowedMode::Math,
"m",
)];
let subscript = parse_with_items(&items, r"\myvec A_c", true);
assert!(
subscript.diagnostics.is_empty(),
"unexpected diagnostics for custom shorthand subscript: {:?}",
subscript.diagnostics
);
match single_root_child(&subscript) {
SyntaxNode::Scripted {
base,
subscript,
superscript,
} => {
assert_eq!(
expect_command_with_math_arg(&base, "myvec", 0),
SyntaxNode::Char('A')
);
assert_eq!(subscript.as_deref(), Some(&SyntaxNode::Char('c')));
assert!(superscript.is_none());
}
other => panic!("expected outer scripted custom command, got {:?}", other),
}
let superscript = parse_with_items(&items, r"\myvec A^d", true);
assert!(
superscript.diagnostics.is_empty(),
"unexpected diagnostics for custom shorthand superscript: {:?}",
superscript.diagnostics
);
match single_root_child(&superscript) {
SyntaxNode::Scripted {
base,
subscript,
superscript,
} => {
assert_eq!(
expect_command_with_math_arg(&base, "myvec", 0),
SyntaxNode::Char('A')
);
assert!(subscript.is_none());
assert_eq!(superscript.as_deref(), Some(&SyntaxNode::Char('d')));
}
other => panic!("expected outer scripted custom command, got {:?}", other),
}
let prime = parse_with_items(&items, r"\myvec A'", true);
assert!(
prime.diagnostics.is_empty(),
"unexpected diagnostics for custom shorthand prime: {:?}",
prime.diagnostics
);
match single_root_child(&prime) {
SyntaxNode::Scripted {
base,
subscript,
superscript,
} => {
assert_eq!(
expect_command_with_math_arg(&base, "myvec", 0),
SyntaxNode::Char('A')
);
assert!(subscript.is_none());
assert_eq!(
superscript.as_deref(),
Some(&SyntaxNode::Prime { count: 1 })
);
}
other => panic!("expected outer scripted custom command, got {:?}", other),
}
}
#[test]
fn braced_mandatory_math_argument_keeps_scripts_inside_command_argument() {
let output =
ParseContext::shared().parse(r"\vec{A_\mu}", &texform_core::parse::ParseConfig::STRICT);
assert!(
output.diagnostics.is_empty(),
"unexpected diagnostics for \\vec{{A_\\mu}}: {:?}",
output.diagnostics
);
match single_root_child(&output) {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "vec");
match &expect_arg(&args[0]).value {
ArgumentValue::MathContent(SyntaxNode::Scripted {
base,
subscript,
superscript,
}) => {
assert_eq!(**base, SyntaxNode::Char('A'));
assert_eq!(subscript.as_deref(), Some(&known_command("mu")));
assert!(superscript.is_none());
}
other => panic!("expected scripted braced argument, got {:?}", other),
}
}
other => panic!("expected command node, got {:?}", other),
}
}
#[test]
fn star_argument_uses_boolean_value() {
let items = [command_item(
"sqrt",
CommandKind::Prefix,
AllowedMode::Math,
"s m",
)];
let with_star = parse_with_items(&items, r"\sqrt*{x}", true);
assert!(
with_star.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
with_star.diagnostics
);
let (_, args) = first_command(&with_star);
assert_eq!(
expect_arg(&args[0]),
&Argument {
kind: ArgumentKind::Star,
value: ArgumentValue::Boolean(true),
}
);
let without_star = parse_with_items(&items, r"\sqrt{x}", true);
assert!(
without_star.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
without_star.diagnostics
);
let (_, args) = first_command(&without_star);
assert_eq!(
expect_arg(&args[0]),
&Argument {
kind: ArgumentKind::Star,
value: ArgumentValue::Boolean(false),
}
);
}
#[test]
fn text_content_generic_only_error_keeps_expected_found_diagnostic() {
let items = [text_command_item()];
let output = parse_with_items(&items, r"\text{$x}", true);
assert!(
output.document().is_none(),
"strict mode should not keep a partial result"
);
assert_eq!(
collect_messages(&output),
vec!["found '$' expected something else, or end of input"]
);
assert!(
!output.diagnostics[0].expected.is_empty(),
"expected/found details should stay available"
);
assert_eq!(
output.diagnostics[0].expected,
["something else", "end of input"]
);
}
#[test]
fn strict_text_content_command_error_points_to_inner_command() {
let src = r"\text{\frac{a}{b}}";
let items = [text_command_item(), frac_command_item()];
let output = parse_with_items(&items, src, true);
assert_eq!(
collect_messages(&output),
vec![r"Command \frac is not allowed in text mode"]
);
assert_first_diagnostic_span_eq(&output, src, r"\frac");
}
#[test]
fn strict_text_content_command_error_has_no_partial_result() {
let items = [text_command_item(), frac_command_item()];
let output = parse_with_items(&items, r"\text{\frac{a}{b}}", true);
assert!(
output.document().is_none(),
"strict content argument errors should not keep a partial result"
);
assert_eq!(
collect_messages(&output),
vec![r"Command \frac is not allowed in text mode"]
);
}
#[test]
fn nonstrict_text_content_direct_error_survives_trailing_generic() {
let ctx = content_test_context();
let src = r"\text{\underline{a^2}$}";
let output = ctx.parse(src, &texform_core::parse::ParseConfig::default());
assert_eq!(
collect_messages(&output),
vec!["Scripted syntax is not allowed in Text mode"]
);
assert_first_diagnostic_span_eq(&output, src, "^");
let result = output
.document()
.expect("non-strict direct error should keep a partial result");
assert!(contains_command_named(&result.to_syntax(), "text"));
assert!(contains_command_named(&result.to_syntax(), "underline"));
assert!(contains_error_node(&result.to_syntax()));
}
mod migrated_argument_regressions {
use super::support::parser::{
expect_arg, label_command_item, parse, test_context_with_items, unwrap_content,
};
use texform_core::parse::ParseConfig;
use texform_interface::syntax_node::{ArgumentKind, ArgumentValue, SyntaxNode};
#[test]
fn test_dimension_argument() {
let (result, _) = parse(r"\hspace1em", false).unwrap();
match result {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "hspace");
assert_eq!(args.len(), 1);
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::Dimension("1em".to_string())
);
}
_ => panic!("Expected Command node"),
},
_ => panic!("Expected root Group"),
}
}
#[test]
fn test_integer_argument() {
let (result, _) = parse(r"\romannumeral12", false).unwrap();
match result {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "romannumeral");
assert_eq!(args.len(), 1);
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::Integer("12".to_string())
);
}
_ => panic!("Expected Command node"),
},
_ => panic!("Expected root Group"),
}
}
#[test]
fn test_keyval_argument() {
let (result, _) = parse(r"\includegraphics[width=1em,height=2pt]{file}", false).unwrap();
match result {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "includegraphics");
assert_eq!(args.len(), 2);
assert_eq!(expect_arg(&args[0]).kind, ArgumentKind::Optional);
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::KeyVal("width=1em,height=2pt".to_string())
);
assert_eq!(
unwrap_content(&args[1]),
&SyntaxNode::Text("file".to_string())
);
}
_ => panic!("Expected Command node"),
},
_ => panic!("Expected root Group"),
}
}
#[test]
fn test_csname_argument() {
let (result, _) = parse(r"\label{sec:intro}", false).unwrap();
match result {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "label");
assert_eq!(args.len(), 1);
assert_eq!(expect_arg(&args[0]).kind, ArgumentKind::Mandatory);
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::CSName("sec:intro".to_string())
);
}
_ => panic!("Expected Command node"),
},
_ => panic!("Expected root Group"),
}
}
#[test]
fn test_csname_argument_rejects_escape_sequence() {
let ctx = test_context_with_items([label_command_item()]);
let command = ctx.parse(r"\label{\alpha}", &ParseConfig::STRICT);
assert!(
command.document().is_none(),
"control sequence inside CSName should fail"
);
assert!(
!command.diagnostics.is_empty(),
"expected CSName diagnostics, got {:?}",
command.diagnostics
);
let escaped_symbol = ctx.parse(r"\label{sec\_a}", &ParseConfig::STRICT);
assert!(
escaped_symbol.document().is_none(),
"escaped symbol inside CSName should fail"
);
assert!(
!escaped_symbol.diagnostics.is_empty(),
"expected CSName diagnostics, got {:?}",
escaped_symbol.diagnostics
);
}
#[test]
fn test_delimiter_argument_braced_matches_inline() {
let (inline, _) = parse(r"\delim\langle", false).unwrap();
let (braced, _) = parse(r"\delim{\langle}", false).unwrap();
let inline_value = match inline {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Command { args, .. } => expect_arg(&args[0]).value.clone(),
_ => panic!("Expected Command node"),
},
_ => panic!("Expected root Group"),
};
let braced_value = match braced {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Command { args, .. } => expect_arg(&args[0]).value.clone(),
_ => panic!("Expected Command node"),
},
_ => panic!("Expected root Group"),
};
assert_eq!(inline_value, braced_value);
}
#[test]
fn test_integer_argument_braced_matches_inline() {
let (inline, _) = parse(r"\romannumeral12", false).unwrap();
let (braced, _) = parse(r"\romannumeral{ 12 }", false).unwrap();
let inline_value = match inline {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Command { args, .. } => expect_arg(&args[0]).value.clone(),
_ => panic!("Expected Command node"),
},
_ => panic!("Expected root Group"),
};
let braced_value = match braced {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Command { args, .. } => expect_arg(&args[0]).value.clone(),
_ => panic!("Expected Command node"),
},
_ => panic!("Expected root Group"),
};
assert_eq!(inline_value, braced_value);
}
#[test]
fn test_dimension_argument_braced_matches_inline() {
let (inline, _) = parse(r"\hspace1.5em", false).unwrap();
let (braced, _) = parse(r"\hspace{ 1,5 em }", false).unwrap();
let inline_value = match inline {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Command { args, .. } => expect_arg(&args[0]).value.clone(),
_ => panic!("Expected Command node"),
},
_ => panic!("Expected root Group"),
};
let braced_value = match braced {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Command { args, .. } => expect_arg(&args[0]).value.clone(),
_ => panic!("Expected Command node"),
},
_ => panic!("Expected root Group"),
};
assert_eq!(inline_value, braced_value);
assert_eq!(inline_value, ArgumentValue::Dimension("1.5em".to_string()));
}
#[test]
fn test_optional_bracket_closes_at_top_level() {
let (result, _) = parse(r"\includegraphics[key={[[},width=1em]{file}", false).unwrap();
match result {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Command { args, .. } => {
assert_eq!(args.len(), 2);
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::KeyVal("key={[[},width=1em".to_string())
);
}
_ => panic!("Expected Command node"),
},
_ => panic!("Expected root Group"),
}
}
#[test]
fn test_dimension_with_spaces() {
let (result, _) = parse(r"\hspace{1.5 cm}", false).unwrap();
match result {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Command { name, args, .. } => {
assert_eq!(name, "hspace");
assert_eq!(args.len(), 1);
assert_eq!(
expect_arg(&args[0]).value,
ArgumentValue::Dimension("1.5cm".to_string())
);
}
_ => panic!("Expected Command node"),
},
_ => panic!("Expected root Group"),
}
}
#[test]
fn test_keyval_empty() {
let result = parse(r"\includegraphics[{}]{file}", false);
assert!(result.is_err(), "Expected error for empty keyval");
}
#[test]
fn test_keyval_empty_brackets() {
let result = parse(r"\includegraphics[]{file}", false);
assert!(
result.is_err(),
"Expected error for empty optional keyval brackets"
);
}
#[test]
fn test_optional_bracket_missing_closer_errors() {
let result = parse(r"\includegraphics[width=1em{file}", false);
assert!(result.is_err());
}
#[test]
fn test_invalid_integer_argument_errors() {
let result = parse(r"\romannumeral{12.5}", false);
assert!(result.is_err());
}
#[test]
fn test_invalid_dimension_argument_errors() {
let result = parse(r"\hspace{abc}", false);
assert!(result.is_err());
}
#[test]
fn test_unclosed_brace_argument_errors() {
let result = parse(r"\frac{a", false);
assert!(result.is_err());
}
#[test]
fn test_dimension_missing_number() {
let result = parse(r"\hspace{cm}", false);
assert!(
result.is_err(),
"Expected error for dimension missing number"
);
}
#[test]
fn test_dimension_missing_unit() {
let result = parse(r"\hspace{1.5}", false);
assert!(result.is_err(), "Expected error for dimension missing unit");
}
}