use nom::{
branch::alt,
bytes::complete::{tag, take, take_until, take_while, take_while1},
character::complete::{
alpha1, alphanumeric1, char, digit1, multispace1, one_of, space0, space1,
},
combinator::{opt, recognize, value},
multi::{many0, many1, many_till, separated_list0},
sequence::{delimited, pair, preceded},
IResult, Parser,
};
use super::node::*;
use super::span::{Span, SpanInfo};
use crate::format;
type ParseResult<'a, T> = IResult<Span<'a>, T>;
pub fn parse_tolerant(name: &str, input: &str) -> CstRoot {
let span = Span::new(input);
let start_info = SpanInfo::from_span(span);
let mut nodes = Vec::new();
let mut remaining = span;
while !remaining.fragment().is_empty() {
if let Ok((rest, trivia)) = parse_trivia(remaining) {
nodes.push(CstNode::Trivia(trivia));
remaining = rest;
continue;
}
if let Ok((rest, para)) = parse_paragraph(remaining) {
nodes.push(CstNode::Paragraph(para));
remaining = rest;
continue;
}
if let Ok((rest, cmd)) = parse_command(remaining) {
nodes.push(CstNode::Command(cmd));
remaining = rest;
continue;
}
if let Ok((rest, sc)) = parse_systemcall(remaining) {
nodes.push(CstNode::SystemCall(sc));
remaining = rest;
continue;
}
if let Ok((rest, _)) = take::<usize, Span, nom::error::Error<Span>>(1usize)(remaining) {
remaining = rest;
} else {
break;
}
}
let _end_span = remaining;
let end_info = if !remaining.fragment().is_empty() {
SpanInfo::from_span(remaining)
} else {
start_info
};
CstRoot {
name: name.to_string(),
nodes,
span: SpanInfo {
start: start_info.start,
end: end_info.end,
start_line: start_info.start_line,
start_column: start_info.start_column,
end_line: end_info.end_line,
end_column: end_info.end_column,
},
}
}
fn parse_trivia(input: Span) -> ParseResult<CstTrivia> {
alt((parse_line_comment, parse_block_comment, parse_whitespace)).parse(input)
}
fn parse_whitespace(input: Span) -> ParseResult<CstTrivia> {
let start_span = input;
let (input, ws) = multispace1(input)?;
let end_span = input;
Ok((
input,
CstTrivia::Whitespace {
content: ws.fragment().to_string(),
span: SpanInfo::from_range(start_span, end_span),
},
))
}
fn parse_line_comment(input: Span) -> ParseResult<CstTrivia> {
let start_span = input;
let (input, _) = tag("//")(input)?;
let (input, content) = take_while(|c| c != '\n' && c != '\r')(input)?;
let end_span = input;
Ok((
input,
CstTrivia::LineComment {
content: content.fragment().to_string(),
span: SpanInfo::from_range(start_span, end_span),
},
))
}
fn parse_block_comment(input: Span) -> ParseResult<CstTrivia> {
let start_span = input;
let (input, _) = tag("/*")(input)?;
let (input, content) = take_until("*/")(input)?;
let (input, _) = tag("*/")(input)?;
let end_span = input;
Ok((
input,
CstTrivia::BlockComment {
content: content.fragment().to_string(),
span: SpanInfo::from_range(start_span, end_span),
},
))
}
fn parse_identifier(input: Span) -> ParseResult<(String, SpanInfo)> {
let start_span = input;
let (input, name) = recognize(pair(
alt((alpha1, tag("_"))),
many0(alt((alphanumeric1, tag("_")))),
))
.parse(input)?;
let end_span = input;
Ok((
input,
(
name.fragment().to_string(),
SpanInfo::from_range(start_span, end_span),
),
))
}
pub fn parse_command(input: Span) -> ParseResult<CstCommand> {
let start_span = input;
let (input, leading_trivia) = many0(parse_trivia).parse(input)?;
let at_start = input;
let (input, _) = tag("@")(input)?;
let at_token = SpanInfo::from_span_and_len(at_start, 1);
let (input, (command, name_span)) = parse_identifier(input)?;
let (input, (arguments, syntax)) = alt((
parse_arguments_parenthesized,
parse_arguments_space_separated,
))
.parse(input)?;
let end_span = input;
Ok((
input,
CstCommand {
command,
at_token,
name_span,
arguments,
syntax,
span: SpanInfo::from_range(start_span, end_span),
leading_trivia,
},
))
}
pub fn parse_systemcall(input: Span) -> ParseResult<CstSystemCall> {
let start_span = input;
let (input, leading_trivia) = many0(parse_trivia).parse(input)?;
let hash_start = input;
let (input, _) = tag("#")(input)?;
let hash_token = SpanInfo::from_span_and_len(hash_start, 1);
let (input, (command, name_span)) = parse_identifier(input)?;
let (input, (arguments, syntax)) = alt((
parse_arguments_parenthesized,
parse_arguments_space_separated,
))
.parse(input)?;
let end_span = input;
Ok((
input,
CstSystemCall {
command,
hash_token,
name_span,
arguments,
syntax,
span: SpanInfo::from_range(start_span, end_span),
leading_trivia,
},
))
}
fn parse_arguments_parenthesized(input: Span) -> ParseResult<(Vec<CstArgument>, CommandSyntax)> {
let (input, _) = space0(input)?;
let open_start = input;
let (input, _) = tag("(")(input)?;
let open_paren = SpanInfo::from_span_and_len(open_start, 1);
let (input, _) = space0(input)?;
let (input, arguments) =
separated_list0(delimited(space0, tag(","), space0), parse_argument).parse(input)?;
let (input, _) = space0(input)?;
let close_start = input;
let (input, _) = tag(")")(input)?;
let close_paren = SpanInfo::from_span_and_len(close_start, 1);
Ok((
input,
(
arguments,
CommandSyntax::Parenthesized {
open_paren,
close_paren,
},
),
))
}
fn parse_arguments_space_separated(input: Span) -> ParseResult<(Vec<CstArgument>, CommandSyntax)> {
let (input, arguments) = many0(preceded(space1, parse_argument)).parse(input)?;
Ok((input, (arguments, CommandSyntax::SpaceSeparated)))
}
fn parse_argument(input: Span) -> ParseResult<CstArgument> {
let start_span = input;
let (input, leading_trivia) = many0(parse_trivia).parse(input)?;
let (input, (name, name_span)) = parse_identifier(input)?;
let (input, equals_and_value) =
opt((preceded(space0, tag("=")), preceded(space0, parse_value))).parse(input)?;
let (equals_token, value) = if let Some((eq, val)) = equals_and_value {
let eq_span = SpanInfo::from_span_and_len(Span::new(eq.fragment()), 1);
(Some(eq_span), Some(val))
} else {
(None, None)
};
let end_span = input;
Ok((
input,
CstArgument {
name,
name_span,
equals_token,
value,
span: SpanInfo::from_range(start_span, end_span),
leading_trivia,
trailing_trivia: vec![],
},
))
}
fn parse_value(input: Span) -> ParseResult<CstValue> {
alt((
parse_string_value,
parse_template_string_value,
parse_number_value,
parse_boolean_value,
parse_array_value,
parse_variable_value,
))
.parse(input)
}
fn parse_string_value(input: Span) -> ParseResult<CstValue> {
let start_span = input;
let (input, quote_char) = alt((char('"'), char('\''))).parse(input)?;
let quote_style = if quote_char == '"' {
QuoteStyle::Double
} else {
QuoteStyle::Single
};
let (input, content) = take_while(move |c| c != quote_char)(input)?;
let (input, _) = char(quote_char)(input)?;
let end_span = input;
let raw = format!("{}{}{}", quote_char, content.fragment(), quote_char);
Ok((
input,
CstValue {
kind: CstValueKind::String { quote: quote_style },
raw: raw.clone(),
parsed: format::RValue::Literal(format::Literal::String(
content.fragment().to_string(),
)),
span: SpanInfo::from_range(start_span, end_span),
},
))
}
fn parse_template_string_value(input: Span) -> ParseResult<CstValue> {
let start_span = input;
let (input, _) = char('`')(input)?;
let (input, content) = take_while(|c| c != '`')(input)?;
let (input, _) = char('`')(input)?;
let end_span = input;
let raw = format!("`{}`", content.fragment());
Ok((
input,
CstValue {
kind: CstValueKind::TemplateString,
raw: raw.clone(),
parsed: format::RValue::Literal(format::Literal::String(
content.fragment().to_string(),
)),
span: SpanInfo::from_range(start_span, end_span),
},
))
}
fn parse_number_value(input: Span) -> ParseResult<CstValue> {
let start_span = input;
let (input, number_str) =
recognize((opt(char('-')), digit1, opt((char('.'), digit1)))).parse(input)?;
let end_span = input;
let raw = number_str.fragment().to_string();
let parsed = if raw.contains('.') {
format::RValue::Literal(format::Literal::Float(raw.parse::<f64>().unwrap_or(0.0)))
} else {
format::RValue::Literal(format::Literal::Integer(raw.parse::<i64>().unwrap_or(0)))
};
let kind = if raw.contains('.') {
CstValueKind::Float
} else {
CstValueKind::Integer
};
Ok((
input,
CstValue {
kind,
raw,
parsed,
span: SpanInfo::from_range(start_span, end_span),
},
))
}
fn parse_boolean_value(input: Span) -> ParseResult<CstValue> {
let start_span = input;
let (input, bool_str) = alt((tag("true"), tag("false"))).parse(input)?;
let end_span = input;
let raw = bool_str.fragment().to_string();
let value = raw == "true";
Ok((
input,
CstValue {
kind: CstValueKind::Boolean,
raw,
parsed: format::RValue::Literal(format::Literal::Boolean(value)),
span: SpanInfo::from_range(start_span, end_span),
},
))
}
fn parse_variable_value(input: Span) -> ParseResult<CstValue> {
let start_span = input;
let (input, var_str) =
recognize(many1(alt((alphanumeric1, tag("."), tag("_"))))).parse(input)?;
let end_span = input;
let raw = var_str.fragment().to_string();
let chain: Vec<String> = raw.split('.').map(|s| s.to_string()).collect();
Ok((
input,
CstValue {
kind: CstValueKind::Variable,
raw,
parsed: format::RValue::Variable(format::Variable { chain }),
span: SpanInfo::from_range(start_span, end_span),
},
))
}
fn parse_array_value(input: Span) -> ParseResult<CstValue> {
let start_span = input;
let fragment = input.fragment();
if !fragment.starts_with('[') {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
}
let mut depth = 0usize;
let mut end = None;
for (i, ch) in fragment.char_indices() {
match ch {
'[' => depth += 1,
']' => {
depth -= 1;
if depth == 0 {
end = Some(i + 1);
break;
}
}
_ => {}
}
}
let end = end.ok_or_else(|| {
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag))
})?;
let raw = fragment[..end].to_string();
let (input, _) = take(end)(input)?;
let end_span = input;
let parsed = crate::parser::primitive::array(&raw)
.map_err(|_| {
nom::Err::Error(nom::error::Error::new(start_span, nom::error::ErrorKind::Tag))
})
.map(|(_, lit)| format::RValue::Literal(lit))?;
Ok((
input,
CstValue {
kind: CstValueKind::Array,
raw,
parsed,
span: SpanInfo::from_range(start_span, end_span),
},
))
}
pub fn parse_paragraph(input: Span) -> ParseResult<CstParagraph> {
let start_span = input;
let (input, leading_trivia) = many0(parse_trivia).parse(input)?;
let colon_start = input;
let (input, _) = tag("::").parse(input)?;
let colon_span = SpanInfo::from_span_and_len(colon_start, 2);
let name_start = input;
let (input, (name, _)) = parse_identifier(input)?;
let name_end = input;
let name_span = SpanInfo::from_range(name_start, name_end);
let (input, params_opt) = opt(parse_parameters).parse(input)?;
let (open_paren, parameters, close_paren) = match params_opt {
Some((op, ps, cp)) => (Some(op), ps, Some(cp)),
None => (None, vec![], None),
};
let (input, _) = space0(input)?;
let (input, block) = parse_block(input)?;
let end_span = input;
let span = SpanInfo::from_range(start_span, end_span);
Ok((
input,
CstParagraph {
name: name.clone(),
colon_token: colon_span,
name_span,
parameters,
open_paren,
close_paren,
block,
span,
leading_trivia,
},
))
}
fn parse_parameters(input: Span) -> ParseResult<(SpanInfo, Vec<CstParameter>, SpanInfo)> {
let (input, _) = space0(input)?;
let open_paren_start = input;
let (input, _) = char('(').parse(input)?;
let open_paren_span = SpanInfo::from_span_and_len(open_paren_start, 1);
let (input, _) = space0(input)?;
let (input, parameters) =
separated_list0(delimited(space0, char(','), space0), parse_parameter).parse(input)?;
let (input, _) = space0(input)?;
let close_paren_start = input;
let (input, _) = char(')').parse(input)?;
let close_paren_span = SpanInfo::from_span_and_len(close_paren_start, 1);
Ok((input, (open_paren_span, parameters, close_paren_span)))
}
fn parse_parameter(input: Span) -> ParseResult<CstParameter> {
let start_span = input;
let (input, leading_trivia) = many0(parse_trivia).parse(input)?;
let name_start = input;
let (input, (name, _)) = parse_identifier(input)?;
let name_end = input;
let name_span = SpanInfo::from_range(name_start, name_end);
let (input, opt_default) = opt(preceded(
space0,
pair(parse_equals_token, preceded(space0, parse_value)),
))
.parse(input)?;
let (equals_token, default_value) = match opt_default {
Some((eq, val)) => (Some(eq), Some(val)),
None => (None, None),
};
let (input, trailing_trivia) = many0(parse_trivia).parse(input)?;
let end_span = input;
let span = SpanInfo::from_range(start_span, end_span);
Ok((
input,
CstParameter {
name: name.clone(),
name_span,
equals_token,
default_value,
span,
leading_trivia,
trailing_trivia,
},
))
}
fn parse_equals_token(input: Span) -> ParseResult<SpanInfo> {
let eq_start = input;
let (input, _) = char('=').parse(input)?;
Ok((input, SpanInfo::from_span_and_len(eq_start, 1)))
}
fn parse_cst_attribute(input: Span) -> ParseResult<CstAttribute> {
let start_span = input;
let (input, leading_trivia) = many0(parse_trivia).parse(input)?;
let open_start = input;
let (input, _) = tag("#[").parse(input)?;
let open_token = SpanInfo::from_span_and_len(open_start, 2);
let (input, _) = space0(input)?;
let keyword_start = input;
let (input, keyword) = recognize(pair(
alt((alpha1, tag("_"))),
many0(alt((alphanumeric1, tag("_")))),
))
.parse(input)?;
let keyword_str = keyword.fragment().to_string();
let keyword_span = SpanInfo::from_range(keyword_start, input);
let (input, _) = space0(input)?;
let (input, condition, condition_span) = if input.fragment().starts_with('(') {
let (input, _) = char('(').parse(input)?;
let (input, _) = space0(input)?;
let cond_start = input;
let quote_char = input.fragment().chars().next().ok_or_else(|| {
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Char))
})?;
if quote_char != '"' && quote_char != '\'' {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Char,
)));
}
let (input, _) = char(quote_char).parse(input)?;
let (input, condition_content) = take_while(move |c| c != quote_char)(input)?;
let condition_str = condition_content.fragment().to_string();
let (input, _) = char(quote_char).parse(input)?;
let cond_span = SpanInfo::from_range(cond_start, input);
let (input, _) = space0(input)?;
let (input, _) = char(')').parse(input)?;
(input, Some(condition_str), Some(cond_span))
} else {
(input, None, None)
};
let (input, _) = space0(input)?;
let close_start = input;
let (input, _) = char(']').parse(input)?;
let close_token = SpanInfo::from_span_and_len(close_start, 1);
let end_span = input;
let span = SpanInfo::from_range(start_span, end_span);
Ok((
input,
CstAttribute {
keyword: keyword_str,
keyword_span,
condition,
condition_span,
open_token,
close_token,
span,
leading_trivia,
},
))
}
pub fn parse_block(input: Span) -> ParseResult<CstBlock> {
let start_span = input;
let open_brace_start = input;
let (input, _) = char('{').parse(input)?;
let open_brace_span = SpanInfo::from_span_and_len(open_brace_start, 1);
let (input, children) = parse_block_children(input)?;
let close_brace_start = input;
let (input, _) = char('}').parse(input)?;
let close_brace_span = SpanInfo::from_span_and_len(close_brace_start, 1);
let end_span = input;
let span = SpanInfo::from_range(start_span, end_span);
Ok((
input,
CstBlock {
open_brace: open_brace_span,
children,
close_brace: close_brace_span,
span,
},
))
}
fn parse_block_children(input: Span) -> ParseResult<Vec<CstNode>> {
let mut nodes = Vec::new();
let mut remaining = input;
while !remaining.fragment().is_empty() {
let trimmed = remaining.fragment().trim_start();
if trimmed.starts_with('}') || trimmed.is_empty() {
while let Ok((rest, trivia)) = parse_trivia(remaining) {
nodes.push(CstNode::Trivia(trivia));
remaining = rest;
}
break;
}
if let Ok((rest, trivia)) = parse_trivia(remaining) {
nodes.push(CstNode::Trivia(trivia));
remaining = rest;
continue;
}
if let Ok((rest, code)) = parse_embedded_code(remaining) {
nodes.push(CstNode::EmbeddedCode(code));
remaining = rest;
continue;
}
if let Ok((rest, block)) = parse_block(remaining) {
nodes.push(CstNode::Block(block));
remaining = rest;
continue;
}
let trimmed = remaining.fragment().trim_start();
let looks_like_command = trimmed.starts_with('@') && !trimmed.starts_with("@{");
let looks_like_attribute = trimmed.starts_with("#[");
let looks_like_systemcall = trimmed.starts_with('#') && !looks_like_attribute;
if looks_like_attribute {
match parse_cst_attribute(remaining) {
Ok((rest, attr)) => {
nodes.push(CstNode::Attribute(attr));
remaining = rest;
continue;
}
Err(_) => {
}
}
}
if looks_like_command {
match parse_command(remaining) {
Ok((rest, cmd)) => {
nodes.push(CstNode::Command(cmd));
remaining = rest;
continue;
}
Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => {
let start_span = remaining;
let content = remaining.fragment();
let line_end = content.find('\n').unwrap_or(content.len());
let line_content = &content[..line_end];
let bytes_to_skip = line_end + if line_end < content.len() { 1 } else { 0 };
let (rest, _) =
take::<usize, Span, nom::error::Error<Span>>(bytes_to_skip)(remaining)
.unwrap_or((remaining, remaining));
nodes.push(CstNode::Error {
content: line_content.to_string(),
span: SpanInfo::from_range(start_span, rest),
message: format!("Invalid command syntax: {:?}", e.code),
});
remaining = rest;
continue;
}
_ => {}
}
}
if looks_like_systemcall {
match parse_systemcall(remaining) {
Ok((rest, sc)) => {
nodes.push(CstNode::SystemCall(sc));
remaining = rest;
continue;
}
Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => {
let start_span = remaining;
let content = remaining.fragment();
let line_end = content.find('\n').unwrap_or(content.len());
let line_content = &content[..line_end];
let bytes_to_skip = line_end + if line_end < content.len() { 1 } else { 0 };
let (rest, _) =
take::<usize, Span, nom::error::Error<Span>>(bytes_to_skip)(remaining)
.unwrap_or((remaining, remaining));
nodes.push(CstNode::Error {
content: line_content.to_string(),
span: SpanInfo::from_range(start_span, rest),
message: format!("Invalid system call syntax: {:?}", e.code),
});
remaining = rest;
continue;
}
_ => {}
}
}
if let Ok((rest, text_line)) = parse_text_line(remaining) {
nodes.push(CstNode::TextLine(text_line));
remaining = rest;
continue;
}
if let Ok((rest, _)) = take::<usize, Span, nom::error::Error<Span>>(1usize)(remaining) {
remaining = rest;
} else {
break;
}
}
Ok((remaining, nodes))
}
pub fn parse_embedded_code(input: Span) -> ParseResult<CstEmbeddedCode> {
alt((parse_embedded_code_brace, parse_embedded_code_hash)).parse(input)
}
fn parse_embedded_code_brace(input: Span) -> ParseResult<CstEmbeddedCode> {
let start_span = input;
let (input, _) = tag("@{").parse(input)?;
let mut depth = 1;
let mut pos = 0;
let content = input.fragment();
let chars: Vec<char> = content.chars().collect();
while pos < chars.len() && depth > 0 {
match chars[pos] {
'{' => depth += 1,
'}' => depth -= 1,
_ => {}
}
pos += 1;
}
if depth != 0 {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Char,
)));
}
let code_end = pos - 1; let code = chars[..code_end].iter().collect::<String>();
let (input, _) = take(pos).parse(input)?;
let end_span = input;
Ok((
input,
CstEmbeddedCode {
syntax: EmbeddedCodeSyntax::Brace,
code,
span: SpanInfo::from_range(start_span, end_span),
},
))
}
fn parse_embedded_code_hash(input: Span) -> ParseResult<CstEmbeddedCode> {
use nom::character::complete::anychar;
let start_span = input;
let (input, _) = tag("##").parse(input)?;
let (input, _) = parse_whitespace_inline.parse(input)?;
let (input, _) = opt(parse_line_ending).parse(input)?;
let (input, content_chars) = many_till(
anychar,
(tag("##"), parse_whitespace_inline, parse_line_ending),
)
.parse(input)?;
let code: String = content_chars.0.into_iter().collect();
let end_span = input;
Ok((
input,
CstEmbeddedCode {
syntax: EmbeddedCodeSyntax::Hash,
code,
span: SpanInfo::from_range(start_span, end_span),
},
))
}
fn parse_whitespace_inline(input: Span) -> ParseResult<()> {
value((), many0(one_of(" \t"))).parse(input)
}
fn parse_line_ending(input: Span) -> ParseResult<()> {
value((), alt((tag("\r\n"), tag("\n")))).parse(input)
}
pub fn parse_text_line(input: Span) -> ParseResult<CstTextLine> {
let start_span = input;
let (input, leading_trivia) = many0(parse_trivia).parse(input)?;
if input.fragment().trim_start().starts_with('@')
|| input.fragment().trim_start().starts_with('#')
|| input.fragment().trim_start().starts_with('{')
|| input.fragment().trim_start().starts_with('}')
|| input.fragment().trim_start().starts_with(':')
{
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
}
let (input, leading) = opt(parse_leading_text).parse(input)?;
let (input, _) = space0(input)?;
let (input, text) = opt(parse_text).parse(input)?;
let (input, _) = space0(input)?;
let (input, tailing) = opt(parse_tailing_text).parse(input)?;
let end_span = input;
let span = SpanInfo::from_range(start_span, end_span);
Ok((
input,
CstTextLine {
leading,
text,
tailing,
span,
leading_trivia,
},
))
}
fn parse_leading_template(i: Span) -> ParseResult<CstLeadingTextContent> {
let (i, tpl) = parse_template_literal(i)?;
Ok((i, CstLeadingTextContent::Template(tpl)))
}
fn parse_leading_quoted(i: Span) -> ParseResult<CstLeadingTextContent> {
let (i, text) = parse_quoted_string(i)?;
Ok((i, CstLeadingTextContent::Text(text)))
}
fn parse_leading_bare(i: Span) -> ParseResult<CstLeadingTextContent> {
let (i, text) = take_while(|c| c != ']' && c != '\n').parse(i)?;
Ok((
i,
CstLeadingTextContent::Text(text.fragment().trim().to_string()),
))
}
fn parse_leading_text(input: Span) -> ParseResult<CstLeadingText> {
let start_span = input;
let open_start = input;
let (input, _) = char('[').parse(input)?;
let open_bracket = SpanInfo::from_span_and_len(open_start, 1);
let (input, _) = space0(input)?;
let (input, content) = alt((
parse_leading_template,
parse_leading_quoted,
parse_leading_bare,
))
.parse(input)?;
let (input, _) = space0(input)?;
let close_start = input;
let (input, _) = char(']').parse(input)?;
let close_bracket = SpanInfo::from_span_and_len(close_start, 1);
let end_span = input;
let span = SpanInfo::from_range(start_span, end_span);
Ok((
input,
CstLeadingText {
open_bracket,
content,
close_bracket,
span,
},
))
}
fn parse_text(input: Span) -> ParseResult<CstText> {
let start_span = input;
if let Ok((i, tpl)) = parse_template_literal(input) {
let span = SpanInfo::from_range(start_span, i);
return Ok((
i,
CstText {
kind: CstTextKind::Template(tpl.clone()),
raw: start_span.fragment()[..span.len()].to_string(),
parsed: String::new(), span,
},
));
}
if let Some(quote_char) = input.fragment().chars().next() {
if quote_char == '"' || quote_char == '\'' {
if let Ok((i, text)) = parse_quoted_string(input) {
let quote_style = if quote_char == '"' {
QuoteStyle::Double
} else {
QuoteStyle::Single
};
let span = SpanInfo::from_range(start_span, i);
return Ok((
i,
CstText {
kind: CstTextKind::Quoted(quote_style),
raw: start_span.fragment()[..span.len()].to_string(),
parsed: text,
span,
},
));
}
}
}
let (i, text) =
take_while1(|c: char| c != '\n' && c != '\r' && c != '@' && c != '{').parse(input)?;
let span = SpanInfo::from_range(start_span, i);
let text_str = text.fragment().trim_end().to_string();
Ok((
i,
CstText {
kind: CstTextKind::Bare,
raw: text_str.clone(),
parsed: text_str,
span,
},
))
}
fn parse_tailing_text(input: Span) -> ParseResult<CstTailingText> {
let start_span = input;
let hash_start = input;
let (input, _) = char('#').parse(input)?;
let hash_token = SpanInfo::from_span_and_len(hash_start, 1);
let marker_start = input;
let (input, marker) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
let marker_end = input;
let marker_span = SpanInfo::from_range(marker_start, marker_end);
let end_span = input;
let span = SpanInfo::from_range(start_span, end_span);
Ok((
input,
CstTailingText {
hash_token,
marker: marker.fragment().to_string(),
marker_span,
span,
},
))
}
fn parse_template_literal(input: Span) -> ParseResult<CstTemplateLiteral> {
let start_span = input;
let (input, _) = char('`').parse(input)?;
let mut parts = Vec::new();
let mut remaining = input;
loop {
if remaining.fragment().starts_with('`') {
break;
}
if remaining.fragment().is_empty() {
return Err(nom::Err::Error(nom::error::Error::new(
remaining,
nom::error::ErrorKind::Tag,
)));
}
if remaining.fragment().starts_with("${") {
let value_start = remaining;
let (rest, _) = tag("${").parse(remaining)?;
let open_token = SpanInfo::from_span_and_len(value_start, 2);
let var_start = rest;
let (rest, (var_name, _)) = parse_identifier(rest)?;
let var_end = rest;
let variable_span = SpanInfo::from_range(var_start, var_end);
let close_start = rest;
let (rest, _) = char('}').parse(rest)?;
let close_token = SpanInfo::from_span_and_len(close_start, 1);
let part_span = SpanInfo::from_range(value_start, rest);
parts.push(CstTemplatePart::Value {
open_token,
variable: format::Variable {
chain: vec![var_name.clone()],
},
variable_span,
close_token,
span: part_span,
});
remaining = rest;
} else {
let text_start = remaining;
let mut text = String::new();
while !remaining.fragment().is_empty()
&& !remaining.fragment().starts_with('`')
&& !remaining.fragment().starts_with("${")
{
let ch = remaining.fragment().chars().next().unwrap();
if ch == '\\' && remaining.fragment().len() > 1 {
let (rest, _) = char('\\').parse(remaining)?;
let next_ch = rest.fragment().chars().next().unwrap();
let escaped = match next_ch {
'n' => '\n',
't' => '\t',
'r' => '\r',
'\\' => '\\',
'`' => '`',
'$' => '$',
_ => next_ch,
};
text.push(escaped);
let (rest, _) = take(1usize)(rest)?;
remaining = rest;
} else {
text.push(ch);
let (rest, _) = take(1usize)(remaining)?;
remaining = rest;
}
}
if !text.is_empty() {
let text_end = remaining;
let text_span = SpanInfo::from_range(text_start, text_end);
parts.push(CstTemplatePart::Text {
content: text,
span: text_span,
});
}
}
}
let (input, _) = char('`').parse(remaining)?;
let end_span = input;
let span = SpanInfo::from_range(start_span, end_span);
Ok((input, CstTemplateLiteral { parts, span }))
}
fn parse_quoted_string(input: Span) -> ParseResult<String> {
let quote_char = input.fragment().chars().next().ok_or_else(|| {
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag))
})?;
if quote_char != '"' && quote_char != '\'' {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
}
let (input, _) = char(quote_char).parse(input)?;
let mut result = String::new();
let mut remaining = input;
while !remaining.fragment().is_empty() {
let ch = remaining.fragment().chars().next().unwrap();
if ch == quote_char {
let (rest, _) = char(quote_char).parse(remaining)?;
return Ok((rest, result));
}
if ch == '\\' && remaining.fragment().len() > 1 {
let (rest, _) = char('\\').parse(remaining)?;
let next_ch = rest.fragment().chars().next().unwrap();
match next_ch {
'n' => {
result.push('\n');
let (rest, _) = take(1usize)(rest)?;
remaining = rest;
}
't' => {
result.push('\t');
let (rest, _) = take(1usize)(rest)?;
remaining = rest;
}
'r' => {
result.push('\r');
let (rest, _) = take(1usize)(rest)?;
remaining = rest;
}
'\\' => {
result.push('\\');
let (rest, _) = take(1usize)(rest)?;
remaining = rest;
}
'"' => {
result.push('"');
let (rest, _) = take(1usize)(rest)?;
remaining = rest;
}
'\'' => {
result.push('\'');
let (rest, _) = take(1usize)(rest)?;
remaining = rest;
}
'u' => {
let (rest, _) = take(1usize)(rest)?;
if rest.fragment().starts_with('{') {
let (rest, _) = char('{').parse(rest)?;
let mut hex_len = 0;
let chars: Vec<char> = rest.fragment().chars().collect();
while hex_len < chars.len() && chars[hex_len] != '}' {
if !chars[hex_len].is_ascii_hexdigit() {
return Err(nom::Err::Error(nom::error::Error::new(
rest,
nom::error::ErrorKind::HexDigit,
)));
}
hex_len += 1;
}
if hex_len == 0 || hex_len >= chars.len() {
return Err(nom::Err::Error(nom::error::Error::new(
rest,
nom::error::ErrorKind::HexDigit,
)));
}
let hex_str: String = chars[..hex_len].iter().collect();
let (rest, _) = take(hex_len)(rest)?;
let (rest, _) = char('}').parse(rest)?;
if let Ok(code_point) = u32::from_str_radix(&hex_str, 16) {
if let Some(unicode_char) = char::from_u32(code_point) {
result.push(unicode_char);
} else {
return Err(nom::Err::Error(nom::error::Error::new(
rest,
nom::error::ErrorKind::HexDigit,
)));
}
} else {
return Err(nom::Err::Error(nom::error::Error::new(
rest,
nom::error::ErrorKind::HexDigit,
)));
}
remaining = rest;
} else {
let (rest, hex_digits) = take(4usize)(rest)?;
let hex_str = hex_digits.fragment();
if !hex_str.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(nom::Err::Error(nom::error::Error::new(
rest,
nom::error::ErrorKind::HexDigit,
)));
}
if let Ok(code_point) = u16::from_str_radix(hex_str, 16) {
if let Some(unicode_char) = char::from_u32(code_point as u32) {
result.push(unicode_char);
} else {
return Err(nom::Err::Error(nom::error::Error::new(
rest,
nom::error::ErrorKind::HexDigit,
)));
}
} else {
return Err(nom::Err::Error(nom::error::Error::new(
rest,
nom::error::ErrorKind::HexDigit,
)));
}
remaining = rest;
}
}
_ => {
result.push(next_ch);
let (rest, _) = take(1usize)(rest)?;
remaining = rest;
}
}
} else {
result.push(ch);
let (rest, _) = take(1usize)(remaining)?;
remaining = rest;
}
}
Err(nom::Err::Error(nom::error::Error::new(
remaining,
nom::error::ErrorKind::Tag,
)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_command_parenthesized() {
let input = r#"@changebg(src="test.jpg", fadeTime=600)"#;
let result = parse_command(Span::new(input));
assert!(result.is_ok());
let (_, cmd) = result.unwrap();
assert_eq!(cmd.command, "changebg");
assert_eq!(cmd.arguments.len(), 2);
assert_eq!(cmd.arguments[0].name, "src");
assert_eq!(cmd.arguments[1].name, "fadeTime");
assert!(matches!(cmd.syntax, CommandSyntax::Parenthesized { .. }));
}
#[test]
fn test_parse_command_space_separated() {
let input = r#"@changebg src="test.jpg" fadeTime=600"#;
let result = parse_command(Span::new(input));
assert!(result.is_ok());
let (_, cmd) = result.unwrap();
assert_eq!(cmd.command, "changebg");
assert_eq!(cmd.arguments.len(), 2);
assert!(matches!(cmd.syntax, CommandSyntax::SpaceSeparated));
}
#[test]
fn test_parse_command_boolean_flag() {
let input = r#"@command flag"#;
let result = parse_command(Span::new(input));
assert!(result.is_ok());
let (_, cmd) = result.unwrap();
assert_eq!(cmd.arguments.len(), 1);
assert_eq!(cmd.arguments[0].name, "flag");
assert!(cmd.arguments[0].value.is_none());
}
#[test]
fn test_parse_systemcall() {
let input = r#"#goto paragraph="main""#;
let result = parse_systemcall(Span::new(input));
assert!(result.is_ok());
let (_, sc) = result.unwrap();
assert_eq!(sc.command, "goto");
assert_eq!(sc.arguments.len(), 1);
assert_eq!(sc.arguments[0].name, "paragraph");
}
#[test]
fn test_parse_tolerant() {
let input = r#"
@command1 arg=1
// 注释
@command2 arg=2
"#;
let cst = parse_tolerant("test", input);
assert!(cst.nodes.len() > 0);
let cmd_count = cst
.nodes
.iter()
.filter(|n| matches!(n, CstNode::Command(_)))
.count();
assert_eq!(cmd_count, 2);
}
#[test]
fn test_parse_number_values() {
let tests = vec![
("123", CstValueKind::Integer),
("-456", CstValueKind::Integer),
("3.14", CstValueKind::Float),
("-2.5", CstValueKind::Float),
];
for (input, expected_kind) in tests {
let result = parse_number_value(Span::new(input));
assert!(result.is_ok(), "Failed to parse: {}", input);
let (_, value) = result.unwrap();
assert_eq!(value.kind, expected_kind);
}
}
#[test]
fn test_parse_array_value() {
let (_, v) = parse_array_value(Span::new("[0,0]")).unwrap();
assert!(matches!(v.kind, CstValueKind::Array));
assert_eq!(v.raw, "[0,0]");
let (_, v) = parse_array_value(Span::new("[]")).unwrap();
assert!(matches!(v.kind, CstValueKind::Array));
assert_eq!(v.raw, "[]");
let (_, v) = parse_array_value(Span::new("[[1,2],[3,4]]")).unwrap();
assert!(matches!(v.kind, CstValueKind::Array));
assert_eq!(v.raw, "[[1,2],[3,4]]");
let (_, v) = parse_array_value(Span::new(r#"[1, "hello", true]"#)).unwrap();
assert!(matches!(v.kind, CstValueKind::Array));
let cst = parse_tolerant("test", "@cmd x=[0,0] y=false\n");
let formatter = crate::cst::formatter::CstFormatter::new();
let result = formatter.format(&cst);
assert!(result.contains("@cmd x=[0,0] y=false"), "got: {}", result);
let cst = parse_tolerant("test", "@cmd x=[ 0, 0 ] y=false\n");
let result = formatter.format(&cst);
assert!(result.contains("@cmd x=[0,0] y=false"), "got: {}", result);
let cst = parse_tolerant("test", "@cmd pts=[[1, 2], [3, 4]]\n");
let result = formatter.format(&cst);
assert!(result.contains("@cmd pts=[[1,2],[3,4]]"), "got: {}", result);
}
#[test]
fn test_to_ast() {
let input = r#"@changebg src="test.jpg" fadeTime=600"#;
let (_, cmd) = parse_command(Span::new(input)).unwrap();
let ast_cmd = cmd.to_ast();
assert_eq!(ast_cmd.command, "changebg");
assert_eq!(ast_cmd.arguments.len(), 2);
}
#[test]
fn test_parse_parameter_without_default() {
let input = "param1";
let result = parse_parameter(Span::new(input));
assert!(result.is_ok());
let (_, param) = result.unwrap();
assert_eq!(param.name, "param1");
assert!(param.default_value.is_none());
assert!(param.equals_token.is_none());
}
#[test]
fn test_parse_parameter_with_default() {
let input = r#"param2="default""#;
let result = parse_parameter(Span::new(input));
assert!(result.is_ok());
let (_, param) = result.unwrap();
assert_eq!(param.name, "param2");
assert!(param.default_value.is_some());
assert!(param.equals_token.is_some());
}
#[test]
fn test_parse_parameters() {
let input = r#"(param1, param2="default", param3=123)"#;
let result = parse_parameters(Span::new(input));
assert!(result.is_ok());
let (_, (_, params, _)) = result.unwrap();
assert_eq!(params.len(), 3);
assert_eq!(params[0].name, "param1");
assert_eq!(params[1].name, "param2");
assert_eq!(params[2].name, "param3");
}
#[test]
fn test_parse_block_empty() {
let input = "{}";
let result = parse_block(Span::new(input));
assert!(result.is_ok());
let (_, block) = result.unwrap();
assert_eq!(block.children.len(), 0);
}
#[test]
fn test_parse_block_with_commands() {
let input = r#"{
@command1 arg=1
@command2 arg=2
}"#;
let result = parse_block(Span::new(input));
assert!(result.is_ok());
let (_, block) = result.unwrap();
let cmd_count = block
.children
.iter()
.filter(|n| matches!(n, CstNode::Command(_)))
.count();
assert_eq!(cmd_count, 2);
}
#[test]
fn test_parse_block_nested() {
let input = r#"{
@command1
{
@command2
}
}"#;
let result = parse_block(Span::new(input));
assert!(result.is_ok());
let (_, block) = result.unwrap();
let has_nested_block = block
.children
.iter()
.any(|n| matches!(n, CstNode::Block(_)));
assert!(has_nested_block);
}
#[test]
fn test_parse_paragraph_simple() {
let input = r#"::main {
@command arg=1
}"#;
let result = parse_paragraph(Span::new(input));
assert!(result.is_ok());
let (_, para) = result.unwrap();
assert_eq!(para.name, "main");
assert_eq!(para.parameters.len(), 0);
assert!(para.open_paren.is_none());
assert!(para.close_paren.is_none());
}
#[test]
fn test_parse_paragraph_with_params() {
let input = r#"::scene(location, time="morning") {
@changebg src="bg.jpg"
}"#;
let result = parse_paragraph(Span::new(input));
assert!(result.is_ok());
let (_, para) = result.unwrap();
assert_eq!(para.name, "scene");
assert_eq!(para.parameters.len(), 2);
assert!(para.open_paren.is_some());
assert!(para.close_paren.is_some());
assert_eq!(para.parameters[0].name, "location");
assert!(para.parameters[0].default_value.is_none());
assert_eq!(para.parameters[1].name, "time");
assert!(para.parameters[1].default_value.is_some());
}
#[test]
fn test_paragraph_to_ast() {
let input = r#"::test(param1="value") {
@command arg=1
}"#;
let (_, para) = parse_paragraph(Span::new(input)).unwrap();
let ast_para = para.to_ast().unwrap();
assert_eq!(ast_para.name, "test");
assert_eq!(ast_para.parameters.len(), 1);
assert_eq!(ast_para.parameters[0].name, "param1");
}
#[test]
fn test_parse_file_with_paragraphs() {
let input = r#"
::start {
@changebg src="bg.jpg"
#goto next
}
::next {
@wait time=1000
}
"#;
let cst = parse_tolerant("test", input);
let para_count = cst
.nodes
.iter()
.filter(|n| matches!(n, CstNode::Paragraph(_)))
.count();
assert_eq!(para_count, 2);
let ast = cst.to_ast().unwrap();
assert_eq!(ast.name, "test");
assert_eq!(ast.paragraphs.len(), 2);
assert_eq!(ast.paragraphs[0].name, "start");
assert_eq!(ast.paragraphs[1].name, "next");
}
#[test]
fn test_cst_to_ast_equivalence() {
let input = r#"
::main(location="classroom") {
@changebg src="bg.jpg" fadeTime=600
@addchar name="hero" src="hero.png"
#goto ending
}
::ending {
@wait time=500
}
"#;
let cst = parse_tolerant("test", input);
let cst_ast = cst.to_ast().unwrap();
let ast_result = crate::parser::parse("test", input);
if let Ok((_, ast)) = ast_result {
assert_eq!(cst_ast.paragraphs.len(), ast.paragraphs.len());
assert_eq!(cst_ast.paragraphs[0].name, ast.paragraphs[0].name);
assert_eq!(cst_ast.paragraphs[1].name, ast.paragraphs[1].name);
}
}
#[test]
fn test_parse_quoted_string_double() {
let input = r#""hello world""#;
let result = parse_quoted_string(Span::new(input));
assert!(result.is_ok());
let (_, text) = result.unwrap();
assert_eq!(text, "hello world");
}
#[test]
fn test_parse_quoted_string_empty_double() {
let input = r#""""#;
let result = parse_quoted_string(Span::new(input));
assert!(result.is_ok());
let (_, text) = result.unwrap();
assert_eq!(text, "");
}
#[test]
fn test_parse_quoted_string_single() {
let input = r#"'hello world'"#;
let result = parse_quoted_string(Span::new(input));
assert!(result.is_ok());
let (_, text) = result.unwrap();
assert_eq!(text, "hello world");
}
#[test]
fn test_parse_quoted_string_empty_single() {
let input = "''";
let result = parse_quoted_string(Span::new(input));
assert!(result.is_ok());
let (_, text) = result.unwrap();
assert_eq!(text, "");
}
#[test]
fn test_parse_quoted_string_with_escapes() {
let input = r#""hello\nworld\t!""#;
let result = parse_quoted_string(Span::new(input));
assert!(result.is_ok());
let (_, text) = result.unwrap();
assert_eq!(text, "hello\nworld\t!");
}
#[test]
fn test_parse_template_literal_simple() {
let input = "`hello world`";
let result = parse_template_literal(Span::new(input));
assert!(result.is_ok());
let (_, tpl) = result.unwrap();
assert_eq!(tpl.parts.len(), 1);
if let CstTemplatePart::Text { content, .. } = &tpl.parts[0] {
assert_eq!(content, "hello world");
} else {
panic!("Expected text part");
}
}
#[test]
fn test_parse_template_literal_with_variable() {
let input = "`hello ${name}!`";
let result = parse_template_literal(Span::new(input));
assert!(result.is_ok());
let (_, tpl) = result.unwrap();
assert_eq!(tpl.parts.len(), 3);
if let CstTemplatePart::Text { content, .. } = &tpl.parts[0] {
assert_eq!(content, "hello ");
} else {
panic!("Expected text part");
}
if let CstTemplatePart::Value { variable, .. } = &tpl.parts[1] {
assert_eq!(variable.chain, vec!["name".to_string()]);
} else {
panic!("Expected value part");
}
}
#[test]
fn test_parse_leading_text_simple() {
let input = "[角色名]";
let result = parse_leading_text(Span::new(input));
assert!(result.is_ok());
let (_, leading) = result.unwrap();
if let CstLeadingTextContent::Text(text) = &leading.content {
assert_eq!(text, "角色名");
} else {
panic!("Expected text content");
}
}
#[test]
fn test_parse_leading_text_quoted() {
let input = r#"["角色名"]"#;
let result = parse_leading_text(Span::new(input));
assert!(result.is_ok());
let (_, leading) = result.unwrap();
if let CstLeadingTextContent::Text(text) = &leading.content {
assert_eq!(text, "角色名");
} else {
panic!("Expected text content");
}
}
#[test]
fn test_parse_tailing_text() {
let input = "#wait";
let result = parse_tailing_text(Span::new(input));
assert!(result.is_ok());
let (_, tailing) = result.unwrap();
assert_eq!(tailing.marker, "wait");
}
#[test]
fn test_parse_text_bare() {
let input = "这是一段文本";
let result = parse_text(Span::new(input));
assert!(result.is_ok());
let (_, text) = result.unwrap();
assert!(matches!(text.kind, CstTextKind::Bare));
assert_eq!(text.parsed, "这是一段文本");
}
#[test]
fn test_parse_text_quoted() {
let input = r#""这是一段文本""#;
let result = parse_text(Span::new(input));
assert!(result.is_ok());
let (_, text) = result.unwrap();
assert!(matches!(text.kind, CstTextKind::Quoted(_)));
assert_eq!(text.parsed, "这是一段文本");
}
#[test]
fn test_parse_text_line_simple() {
let input = "这是一行文本\n";
let result = parse_text_line(Span::new(input));
assert!(result.is_ok());
let (_, line) = result.unwrap();
assert!(line.leading.is_none());
assert!(line.text.is_some());
assert!(line.tailing.is_none());
}
#[test]
fn test_parse_text_line_with_leading() {
let input = "[角色名] \"对话内容\"\n";
let result = parse_text_line(Span::new(input));
assert!(result.is_ok());
let (_, line) = result.unwrap();
assert!(line.leading.is_some());
assert!(line.text.is_some());
}
#[test]
fn test_parse_text_line_with_tailing() {
let input = "\"对话内容\" #wait\n";
let result = parse_text_line(Span::new(input));
assert!(result.is_ok());
let (_, line) = result.unwrap();
assert!(line.text.is_some());
assert!(line.tailing.is_some());
}
#[test]
fn test_parse_text_line_plain_no_tailing() {
let input = "对话内容 #wait\n";
let result = parse_text_line(Span::new(input));
assert!(result.is_ok());
let (_, line) = result.unwrap();
assert!(line.text.is_some());
assert!(line.tailing.is_none());
}
#[test]
fn test_text_line_to_ast() {
let input = "[角色] \"对话\"\n";
let (_, line) = parse_text_line(Span::new(input)).unwrap();
let ast_child = line.to_ast().unwrap();
if let format::ChildContent::TextLine(leading, text, _) = ast_child.content {
assert!(matches!(leading, format::LeadingText::Text(_)));
assert!(matches!(text, format::Text::Text(_)));
} else {
panic!("Expected TextLine");
}
}
}