mod support;
use std::time::{Duration, Instant};
use support::{
assert_first_diagnostic_span_eq, collect_messages, command_item, contains_command_named,
contains_error_node, parse_with_items,
};
use texform_core::parse::{
AllowedMode, CommandKind, ContextItem, ParseConfig, ParseContext, ParseDiagnosticKind,
ParseResult,
};
use texform_interface::syntax_node::{ArgumentValue, SyntaxNode};
fn parse_shared(src: &str, config: &ParseConfig) -> ParseResult {
ParseContext::shared().parse(src, config)
}
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 assert_first_diagnostic_expected_found(
output: &ParseResult,
expected: &[&str],
found: Option<&str>,
) {
let diagnostic = output
.diagnostics
.first()
.expect("expected at least one diagnostic");
assert_eq!(diagnostic.expected, expected);
assert_eq!(diagnostic.found.as_deref(), found);
}
#[test]
fn content_argument_partial_result_keeps_outer_text_command() {
let output = parse_with_items(
&[text_command_item(), frac_command_item()],
r"\text{\frac{a}{b}}",
false,
);
assert_eq!(
collect_messages(&output),
vec![r"Command \frac is not allowed in text mode"]
);
let result = output.document().expect("should produce a partial result");
let syntax = result.to_syntax();
let root_children = match &syntax {
SyntaxNode::Root { children, .. } => children,
other => panic!("expected root node, got {:?}", other),
};
let text_args = match root_children.first() {
Some(SyntaxNode::Command { name, args, .. }) => {
assert_eq!(name, "text");
args
}
other => panic!("expected first child to be \\text, got {:?}", other),
};
let text_arg = text_args[0]
.as_ref()
.expect("text should keep its argument slot");
let content = match &text_arg.value {
ArgumentValue::TextContent(node) => node,
other => panic!("expected text content argument, got {:?}", other),
};
assert!(
contains_error_node(content),
"recoverable content subparse should keep an Error placeholder"
);
}
#[test]
fn nested_recoverable_content_keeps_inner_diagnostics() {
let output = parse_with_items(
&[text_command_item(), frac_command_item()],
r"\frac{\text{\frac{a}{b}}}{c}",
false,
);
assert!(
output.document().is_some(),
"should produce a partial result"
);
assert!(
collect_messages(&output).contains(&r"Command \frac is not allowed in text mode"),
"nested recoverable content should keep the inner mode diagnostic"
);
}
#[test]
fn text_scripted_content_reports_only_direct_error() {
let output = parse_with_items(
&[
text_command_item(),
command_item("underline", CommandKind::Prefix, AllowedMode::Math, "m"),
command_item("underline", CommandKind::Prefix, AllowedMode::Text, "m:T"),
],
r"\text{\underline{a^2}}",
false,
);
assert_eq!(
collect_messages(&output),
vec!["Scripted syntax is not allowed in Text mode"]
);
let result = output
.document()
.expect("scripted text should still keep a partial result");
let syntax = result.to_syntax();
let root_children = match &syntax {
SyntaxNode::Root { children, .. } => children,
other => panic!("expected root node, got {:?}", other),
};
let text_args = match root_children.first() {
Some(SyntaxNode::Command { name, args, .. }) => {
assert_eq!(name, "text");
args
}
other => panic!("expected first child to be \\text, got {:?}", other),
};
let text_arg = text_args[0]
.as_ref()
.expect("text should keep its argument slot");
let underline_args = match &text_arg.value {
ArgumentValue::TextContent(SyntaxNode::Command { name, args, .. }) => {
assert_eq!(name, "underline");
args
}
other => panic!("expected recoverable underline command, got {:?}", other),
};
let underline_arg = underline_args[0]
.as_ref()
.expect("underline should keep its text argument");
let underline_content = match &underline_arg.value {
ArgumentValue::TextContent(node) => node,
other => panic!("expected underline text content, got {:?}", other),
};
assert!(contains_error_node(underline_content));
}
#[test]
fn nonstrict_direct_error_survives_trailing_outer_generic() {
let output = parse_with_items(
&[
text_command_item(),
command_item("underline", CommandKind::Prefix, AllowedMode::Math, "m"),
command_item("underline", CommandKind::Prefix, AllowedMode::Text, "m:T"),
],
r"\text{\underline{a^2}$}",
false,
);
assert_eq!(
collect_messages(&output),
vec!["Scripted syntax is not allowed in Text mode"]
);
assert!(output.document().is_some(), "should keep a partial result");
let result = output
.document()
.expect("mixed direct/generic error should keep a partial result");
assert!(contains_command_named(&result.to_syntax(), "underline"));
assert!(contains_error_node(&result.to_syntax()));
}
#[test]
fn nonstrict_command_direct_error_survives_trailing_outer_generic() {
let output = parse_with_items(
&[text_command_item(), frac_command_item()],
r"\text{\frac{a}{b}$}",
false,
);
assert_eq!(
collect_messages(&output),
vec![r"Command \frac is not allowed in text mode"]
);
assert!(output.document().is_some(), "should keep a partial result");
let result = output
.document()
.expect("command mixed error should keep a partial result");
assert!(contains_command_named(&result.to_syntax(), "text"));
let syntax = result.to_syntax();
let root_children = match &syntax {
SyntaxNode::Root { children, .. } => children,
other => panic!("expected root node, got {:?}", other),
};
let text_args = match root_children.first() {
Some(SyntaxNode::Command { name, args, .. }) => {
assert_eq!(name, "text");
args
}
other => panic!("expected first child to be \\text, got {:?}", other),
};
let text_arg = text_args[0]
.as_ref()
.expect("text should keep its argument slot");
let text_content = match &text_arg.value {
ArgumentValue::TextContent(node) => node,
other => panic!("expected text content, got {:?}", other),
};
assert!(contains_error_node(text_content));
}
#[test]
fn generic_only_content_error_is_not_filtered_out() {
let output = parse_with_items(&[text_command_item()], r"\text{$x}", false);
assert_eq!(
collect_messages(&output),
vec!["found '$' expected something else, or end of input"]
);
assert!(
output.document().is_some(),
"generic-only content error should still keep outer text shell"
);
let result = output
.document()
.expect("generic-only content error should keep a partial result");
let syntax = result.to_syntax();
let root_children = match &syntax {
SyntaxNode::Root { children, .. } => children,
other => panic!("expected root node, got {:?}", other),
};
let text_args = match root_children.first() {
Some(SyntaxNode::Command { name, args, .. }) => {
assert_eq!(name, "text");
args
}
other => panic!("expected first child to be \\text, got {:?}", other),
};
let text_arg = text_args[0]
.as_ref()
.expect("text should keep its argument slot");
let text_content = match &text_arg.value {
ArgumentValue::TextContent(node) => node,
other => panic!("expected text content argument, got {:?}", other),
};
match text_content {
SyntaxNode::Error { message, .. } => {
assert_eq!(
message,
"found '$' expected something else, or end of input"
);
}
other => panic!("expected recovered Error node, got {:?}", other),
}
}
#[test]
fn recover_false_keeps_nonstrict_unknowns_without_partial_recovery() {
let config = ParseConfig {
abort_on_error: true,
..Default::default()
};
let output = parse_shared(r"\unknowncmd {", &config);
assert!(
output.document().is_none(),
"recover=false should not keep a partial tree for malformed input"
);
assert_eq!(collect_messages(&output), vec!["not a command"]);
}
#[test]
fn nonstrict_recover_handles_unclosed_nested_groups_without_exponential_retry() {
let src = format!("{}x", "{".repeat(18));
let started = Instant::now();
let output = parse_shared(&src, &ParseConfig::LENIENT);
let elapsed = started.elapsed();
assert!(
output.document().is_some(),
"recovery should keep a partial tree"
);
assert!(
!output.diagnostics.is_empty(),
"unclosed groups should report diagnostics"
);
assert!(
elapsed < Duration::from_millis(250),
"recover=true should not retry exponentially for unclosed nested groups; elapsed={elapsed:?}"
);
}
#[test]
fn max_group_depth_exceeded_reports_public_kind() {
let config = ParseConfig {
max_group_depth: 1,
..ParseConfig::LENIENT
};
let output = parse_shared("{{x}}", &config);
assert!(
output
.diagnostics
.iter()
.any(|diagnostic| diagnostic.kind == Some(ParseDiagnosticKind::MaxGroupDepthExceeded)),
"expected max depth diagnostic, got {:?}",
output.diagnostics
);
let result = output
.document()
.expect("max depth should keep an error node");
assert!(
contains_error_node(&result.to_syntax()),
"max depth should produce an Error node"
);
}
#[test]
fn max_group_depth_restored_between_sibling_groups() {
let config = ParseConfig {
max_group_depth: 2,
..ParseConfig::LENIENT
};
let output = parse_shared("{a}{b}{c}", &config);
assert!(
output.diagnostics.is_empty(),
"depth must be restored between sibling groups, got {:?}",
output.diagnostics
);
}
#[test]
fn nested_content_arguments_merge_inner_direct_error_once() {
let output = parse_with_items(
&[
text_command_item(),
command_item("fbox", CommandKind::Prefix, AllowedMode::Text, "m:T"),
command_item("underline", CommandKind::Prefix, AllowedMode::Math, "m"),
command_item("underline", CommandKind::Prefix, AllowedMode::Text, "m:T"),
],
r"\text{\fbox{\underline{a^2}}}",
false,
);
assert_eq!(
collect_messages(&output),
vec!["Scripted syntax is not allowed in Text mode"]
);
let result = output
.document()
.expect("nested content arguments should still keep a partial result");
assert!(contains_command_named(&result.to_syntax(), "text"));
assert!(contains_command_named(&result.to_syntax(), "fbox"));
assert!(contains_error_node(&result.to_syntax()));
}
#[test]
fn empty_text_content_argument_stays_on_success_path() {
let output = parse_with_items(&[text_command_item()], r"\text{}", false);
assert!(output.diagnostics.is_empty());
let result = output
.document()
.expect("empty text content should stay on the success path");
let syntax = result.to_syntax();
let root_children = match &syntax {
SyntaxNode::Root { children, .. } => children,
other => panic!("expected root node, got {:?}", other),
};
let text_args = match root_children.first() {
Some(SyntaxNode::Command { name, args, .. }) => {
assert_eq!(name, "text");
args
}
other => panic!("expected first child to be \\text, got {:?}", other),
};
let text_arg = text_args[0]
.as_ref()
.expect("text should keep its argument slot");
match &text_arg.value {
ArgumentValue::TextContent(SyntaxNode::Group { children, .. }) => {
assert!(children.is_empty(), "expected an empty content group");
}
other => panic!("expected empty text content group, got {:?}", other),
}
}
#[test]
fn strict_content_scripted_error_keeps_inner_span() {
let output = parse_with_items(
&[
text_command_item(),
command_item("underline", CommandKind::Prefix, AllowedMode::Math, "m"),
command_item("underline", CommandKind::Prefix, AllowedMode::Text, "m:T"),
],
r"\text{\underline{a^2}}",
true,
);
assert_first_diagnostic_span_eq(&output, r"\text{\underline{a^2}}", "^");
}
#[test]
fn nonstrict_content_command_error_keeps_original_inner_span() {
let src = r"\text{\frac{\frac}{b}}";
let output = parse_with_items(&[text_command_item(), frac_command_item()], src, false);
assert_eq!(
collect_messages(&output),
vec![r"Command \frac is not allowed in text mode"]
);
assert_first_diagnostic_span_eq(&output, src, r"\frac");
let outer_frac_start = src.find(r"\frac{").expect("should find outer frac");
assert_eq!(output.diagnostics[0].span.start, outer_frac_start);
}
#[test]
fn diagnostics_serialize_includes_contexts_field() {
let output = parse_shared(r"\unknowncmd", &texform_core::parse::ParseConfig::STRICT);
let json = serde_json::to_value(output.diagnostics()).unwrap();
let diagnostics = json.as_array().unwrap();
assert!(!diagnostics.is_empty());
let diagnostic = &diagnostics[0];
assert!(diagnostic.get("message").is_some());
assert!(diagnostic.get("span").is_some());
assert!(diagnostic.get("expected").is_some());
assert!(diagnostic.get("contexts").is_some());
assert!(diagnostic.get("kind").is_some());
}
#[test]
fn invalid_left_delimiter_reports_root_cause_and_contexts() {
let output = parse_shared(
r"\begin{aligned}\left\foo x \right)\end{aligned}",
&texform_core::parse::ParseConfig::default(),
);
assert!(!output.diagnostics.is_empty(), "should have diagnostics");
let diagnostic = &output.diagnostics[0];
assert_eq!(diagnostic.message, "invalid \\left delimiter");
let labels: Vec<&str> = diagnostic
.contexts
.iter()
.map(|context| context.label.as_str())
.collect();
assert!(labels.contains(&"left-delimited group"));
assert!(labels.contains(&"environment body"));
}
#[test]
fn invalid_left_delimiter_reports_bare_left_context_only() {
let output = parse_shared(
r"\left\foo x \right)",
&texform_core::parse::ParseConfig::default(),
);
assert!(!output.diagnostics.is_empty(), "should have diagnostics");
let diagnostic = &output.diagnostics[0];
assert_eq!(diagnostic.message, "invalid \\left delimiter");
let labels: Vec<&str> = diagnostic
.contexts
.iter()
.map(|context| context.label.as_str())
.collect();
assert!(labels.contains(&"left-delimited group"));
assert!(!labels.contains(&"environment body"));
}
#[test]
fn partial_result_keeps_outer_delimited_group_and_following_siblings() {
let output = parse_shared(
r"\left( \begin{matrix} a \end{align} \right) + z",
&texform_core::parse::ParseConfig::default(),
);
assert!(!output.diagnostics.is_empty(), "should have diagnostics");
let result = output.document().expect("should produce a partial result");
let syntax = result.to_syntax();
let root_children = match &syntax {
SyntaxNode::Root { children, .. } => children,
other => panic!("expected root node, got {:?}", other),
};
let delimited_children = match root_children.first() {
Some(SyntaxNode::Group {
kind: texform_interface::syntax_node::GroupKind::Delimited { .. },
children,
..
}) => children,
other => panic!(
"expected first child to be a delimited group, got {:?}",
other
),
};
assert!(
delimited_children
.iter()
.any(|child| matches!(child, SyntaxNode::Error { .. })),
"delimited group should keep an error placeholder"
);
assert!(
root_children
.iter()
.any(|child| matches!(child, SyntaxNode::Char('+'))),
"following siblings should still contain '+'"
);
assert!(
root_children
.iter()
.any(|child| matches!(child, SyntaxNode::Char('z'))),
"following siblings should still contain 'z'"
);
}
#[test]
fn partial_result_json_contains_error_node() {
let output = parse_shared(
r"\left( \begin{matrix} a \end{align} \right) + z",
&texform_core::parse::ParseConfig::default(),
);
assert!(!output.diagnostics.is_empty(), "should have diagnostics");
let node_json = serde_json::to_value(
output
.document()
.expect("partial result should contain a document")
.to_syntax(),
)
.expect("partial document should serialize to JSON");
let node_text =
serde_json::to_string(&node_json).expect("result.to_syntax() JSON should stringify");
assert!(
node_text.contains("\"Error\""),
"result.to_syntax() JSON should expose the recovered Error node: {node_text}"
);
assert!(
node_text.contains("Environment name mismatch")
&& node_text.contains("\\end{matrix}")
&& node_text.contains("\\end{align}"),
"result.to_syntax() JSON should preserve the normalized recovered environment mismatch message: {node_text}"
);
}
#[test]
fn partial_result_keeps_outer_environment_on_inner_environment_error() {
let src = r"\begin{matrix} \begin{align} x \end{matrix}";
let output = parse_shared(src, &texform_core::parse::ParseConfig::default());
assert!(!output.diagnostics.is_empty(), "should have diagnostics");
assert_eq!(
output.diagnostics[0].message,
"Environment name mismatch: expected \\end{align}, found \\end{matrix}",
"inner environment mismatch should stay more specific than an outer missing-end error"
);
assert_first_diagnostic_span_eq(&output, src, r"\end{matrix}");
assert_first_diagnostic_expected_found(&output, &[r"\end{align}"], Some(r"\end{matrix}"));
let result = output.document().expect("should produce a partial result");
let syntax = result.to_syntax();
let root_children = match &syntax {
SyntaxNode::Root { children, .. } => children,
other => panic!("expected root node, got {:?}", other),
};
let body_children = match root_children.first() {
Some(SyntaxNode::Environment { name, body, .. }) => {
assert_eq!(name, "matrix");
match body.as_ref() {
SyntaxNode::Group { children, .. } => children,
other => panic!("expected matrix body group, got {:?}", other),
}
}
other => panic!("expected first child to be an environment, got {:?}", other),
};
assert!(
body_children
.iter()
.any(|child| matches!(child, SyntaxNode::Error { .. })),
"matrix body should keep an error placeholder for the broken inner environment"
);
}
#[test]
fn partial_result_keeps_following_siblings_after_environment_mismatch() {
let src = r"\begin{matrix} x \end{align} + z";
let output = parse_shared(src, &texform_core::parse::ParseConfig::default());
assert!(!output.diagnostics.is_empty(), "should have diagnostics");
assert_eq!(
output.diagnostics[0].message,
"Environment name mismatch: expected \\end{matrix}, found \\end{align}"
);
assert_first_diagnostic_span_eq(&output, src, r"\end{align}");
assert_first_diagnostic_expected_found(&output, &[r"\end{matrix}"], Some(r"\end{align}"));
let result = output.document().expect("should produce a partial result");
let syntax = result.to_syntax();
let root_children = match &syntax {
SyntaxNode::Root { children, .. } => children,
other => panic!("expected root node, got {:?}", other),
};
assert!(
root_children
.iter()
.any(|child| matches!(child, SyntaxNode::Error { .. })),
"root should keep an error placeholder for the broken environment"
);
assert!(
root_children
.iter()
.any(|child| matches!(child, SyntaxNode::Char('+'))),
"following siblings should still contain '+'"
);
assert!(
root_children
.iter()
.any(|child| matches!(child, SyntaxNode::Char('z'))),
"following siblings should still contain 'z'"
);
}
#[test]
fn environment_mismatch_rewrite_does_not_capture_later_generic_errors() {
let src = r"\begin{matrix} x \end{align}}";
let output = parse_shared(src, &texform_core::parse::ParseConfig::default());
assert_eq!(
output.diagnostics.len(),
2,
"should keep two distinct diagnostics"
);
let mismatch = &output.diagnostics[0];
assert_eq!(
mismatch.message,
"Environment name mismatch: expected \\end{matrix}, found \\end{align}"
);
assert_eq!(&src[mismatch.span.start..mismatch.span.end], r"\end{align}");
let trailing_brace = &output.diagnostics[1];
assert_eq!(
trailing_brace.message,
"found '}' expected something else, or end of input"
);
assert_eq!(
&src[trailing_brace.span.start..trailing_brace.span.end],
"}"
);
assert_eq!(trailing_brace.expected, ["something else", "end of input"]);
assert_eq!(trailing_brace.found.as_deref(), Some("}"));
}
mod parser_diagnostic_regressions {
use super::support::parser::*;
use chumsky::error::RichReason;
use texform_core::parse::{
AllowedMode, CommandKind, ContextItem, ParseConfig, ParseDiagnosticKind,
};
use texform_interface::syntax_node::{ContentMode, GroupKind, SyntaxNode};
#[test]
fn test_bare_left_reports_invalid_left_delimiter() {
let diagnostics = parse(r"\left\foo x \right)", false).unwrap_err();
assert_eq!(diagnostics[0], "invalid \\left delimiter");
}
#[test]
fn invalid_left_delimiter_reports_root_cause_and_contexts() {
let src = r"\begin{aligned}\probe[\left\foo x]\end{aligned}";
let output = test_context_with_items([
ContextItem::from(environment_item(
"aligned",
AllowedMode::Math,
ContentMode::Math,
"",
)),
ContextItem::from(command_item(
"probe",
CommandKind::Prefix,
AllowedMode::Math,
"o",
)),
])
.parse(src, &ParseConfig::LENIENT);
assert!(
output.document().is_some(),
"recoverable subparse should keep a partial result"
);
let diagnostic = output
.diagnostics
.iter()
.find(|diagnostic| diagnostic.message == "invalid \\left delimiter")
.unwrap_or_else(|| panic!("diagnostics: {:?}", output.diagnostics));
assert_eq!(diagnostic.contexts.len(), 3);
let environment_context = diagnostic
.contexts
.iter()
.find(|context| context.label == "environment body")
.expect("missing environment body context");
let argument_context = diagnostic
.contexts
.iter()
.find(|context| context.label == "command argument")
.expect("missing command argument context");
let left_context = diagnostic
.contexts
.iter()
.find(|context| context.label == "left-delimited group")
.expect("missing left-delimited group context");
assert_eq!(
&src[argument_context.span.start..argument_context.span.end],
r"[\left\foo x]"
);
assert_eq!(
&src[environment_context.span.start..environment_context.span.end],
r"\probe[\left\foo"
);
assert_eq!(
&src[left_context.span.start..left_context.span.end],
r"\left\foo"
);
}
#[test]
fn test_later_left_item_reports_invalid_left_delimiter() {
let diagnostics = parse(r"\left( x \right) + \left\foo y \right)", false).unwrap_err();
assert_eq!(diagnostics[0], "invalid \\left delimiter");
}
#[test]
fn test_comment_truncated_argument_reports_specific_kind() {
let output = test_context().parse(
r"\frac{%\ change \ in \ x}{%\ change \ in \ y}",
&ParseConfig::LENIENT,
);
let diagnostic = output
.diagnostics
.first()
.unwrap_or_else(|| panic!("expected diagnostic, got output: {:?}", output.document()));
assert_eq!(
diagnostic.kind,
Some(ParseDiagnosticKind::CommentTruncatedArgument)
);
assert_eq!(
diagnostic.message,
"Unescaped % starts a comment inside this argument"
);
}
#[test]
fn test_math_shift_inside_formula_reports_specific_kind() {
let output = test_context_with_items([environment_item(
"cases",
AllowedMode::Math,
ContentMode::Math,
"",
)])
.parse(
r"f_X(x) = \begin{cases}$1000 & 0.01\\$0 & 0.99\end{cases}",
&ParseConfig::LENIENT,
);
let diagnostic = output
.diagnostics
.iter()
.find(|diagnostic| diagnostic.kind == Some(ParseDiagnosticKind::UnexpectedMathShift))
.unwrap_or_else(|| panic!("diagnostics: {:?}", output.diagnostics));
assert_eq!(
diagnostic.message,
"Unexpected $ inside a math formula; it looks like a currency marker"
);
}
#[test]
fn test_unknown_command_strict() {
let result = parse(r"\unknown{x}", true);
assert!(result.is_err());
}
#[test]
fn test_unknown_environment_strict() {
let result = parse(r"\begin{foo}a\end{foo}", true);
assert_eq!(result.unwrap_err(), vec!["Unknown environment: foo"]);
}
#[test]
fn test_ambiguous_over_reports_diagnostic() {
let errors = texform_core::parse::grammar::parse(r"a \over b + 1 \over c", true)
.expect_err("ambiguous repeated top-level infix should fail");
let messages: Vec<_> = errors
.iter()
.map(|error| match error.reason() {
RichReason::Custom(message) => message.clone(),
RichReason::ExpectedFound { .. } => format!("{error}"),
})
.collect();
assert!(
messages
.iter()
.any(|message| message.ends_with("Ambiguous use of \\over")),
"{:?}",
messages
);
}
#[test]
fn test_repeated_over_still_reports_ambiguous_infix_kind() {
let output = test_context().parse(r"a \over b + 1 \over c", &ParseConfig::LENIENT);
let diagnostic = output
.diagnostics
.iter()
.find(|diagnostic| diagnostic.message == "Ambiguous use of \\over")
.unwrap_or_else(|| panic!("diagnostics: {:?}", output.diagnostics));
assert_eq!(diagnostic.kind, Some(ParseDiagnosticKind::AmbiguousInfix));
}
#[test]
fn test_environment_name_mismatch() {
let result = parse(r"\begin{matrix}a\end{align}", false);
assert!(result.is_err());
}
#[test]
fn test_environment_missing_end_errors() {
let errors = parse(r"\begin{matrix}", false).expect_err("expected missing end error");
let debug = format!("{errors:?}");
assert!(
debug.contains("\\end{matrix}"),
"Unexpected errors: {debug}"
);
}
#[test]
fn test_bare_prime_superscript_parses_as_prime_node() {
let (result, _) = parse(r"f^'", false).expect("expected prime superscript");
match result {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Scripted { superscript, .. } => {
assert_eq!(
superscript.as_deref(),
Some(&SyntaxNode::Prime { count: 1 })
);
}
other => panic!("expected scripted node, got {other:?}"),
},
other => panic!("expected root node, got {other:?}"),
}
}
#[test]
fn test_double_superscript_error() {
assert!(parse(r"x^2^3", false).is_err());
}
#[test]
fn test_double_subscript_error() {
assert!(parse(r"x_2_3", false).is_err());
}
#[test]
fn test_prime_after_superscript_error() {
assert!(parse(r"x^a'", false).is_err());
}
#[test]
fn test_prime_then_explicit_prime_superscript_stays_in_single_slot() {
let (result, _) = parse(r"x'^'", false).expect("expected combined prime superscript");
match result {
SyntaxNode::Root { children, .. } => match &children[0] {
SyntaxNode::Scripted { superscript, .. } => {
assert_eq!(
superscript.as_deref(),
Some(&SyntaxNode::Group {
mode: ContentMode::Math,
kind: GroupKind::Implicit,
children: vec![
SyntaxNode::Prime { count: 1 },
SyntaxNode::Prime { count: 1 },
],
})
);
}
other => panic!("expected scripted node, got {other:?}"),
},
other => panic!("expected root node, got {other:?}"),
}
}
}