use bytes::Bytes;
use nom::bytes::streaming as streaming_bytes;
use nom::character::streaming as streaming_char;
use nom::bytes::complete::{tag, tag_no_case, take_until, take_while, take_while1};
use nom::character::complete::{multispace0, multispace1};
use nom::branch::alt;
use nom::combinator::{not, opt, peek, recognize};
use nom::error::Error;
use nom::multi::separated_list0;
use nom::sequence::{delimited, preceded, terminated};
use nom::IResult;
use nom::Parser;
use crate::literals::*;
use crate::parser_types::{DcaMode, Element, Expr, IncludeAttributes, Operator, Tag, WhenBranch};
type Attrs<'a> = Vec<(&'a str, &'a str)>;
fn attrs_remove<'a>(attrs: &mut Attrs<'a>, name: &str) -> Option<&'a str> {
attrs
.iter()
.position(|(k, _)| *k == name)
.map(|i| attrs.remove(i).1)
}
fn attrs_get<'a>(attrs: &'a Attrs<'_>, name: &str) -> Option<&'a str> {
attrs.iter().find(|(k, _)| *k == name).map(|(_, v)| *v)
}
#[inline]
fn slice_as_bytes(original: &Bytes, slice: &[u8]) -> Bytes {
let original_ptr = original.as_ptr() as usize;
let slice_ptr = slice.as_ptr() as usize;
debug_assert!(
slice_ptr >= original_ptr && slice_ptr + slice.len() <= original_ptr + original.len(),
"slice must be within original Bytes range"
);
let offset = slice_ptr - original_ptr;
let len = slice.len();
original.slice(offset..offset + len)
}
enum ParsingMode {
Streaming,
Complete,
Eof,
}
enum ParseResult {
Single(Element),
Multiple(Vec<Element>),
Empty,
}
impl ParseResult {
#[inline]
fn append_to(self, acc: &mut Vec<Element>) {
match self {
Self::Single(e) => acc.push(e),
Self::Multiple(mut v) => acc.append(&mut v),
Self::Empty => {}
}
}
}
fn parse_loop<'a, F>(
original: &'a Bytes,
mut parser: F,
incomplete_strategy: &ParsingMode,
) -> IResult<&'a [u8], Vec<Element>, Error<&'a [u8]>>
where
F: FnMut(&Bytes, &'a [u8]) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>>,
{
let mut result = Vec::with_capacity(8);
let mut remaining = original.as_ref();
loop {
match parser(original, remaining) {
Ok((rest, parse_result)) => {
parse_result.append_to(&mut result);
if rest.len() == remaining.len() {
return Ok((rest, result));
}
remaining = rest;
if remaining.is_empty() {
return Ok((remaining, result));
}
}
Err(nom::Err::Incomplete(needed)) => {
return match incomplete_strategy {
ParsingMode::Streaming => {
if result.is_empty() {
Err(nom::Err::Incomplete(needed))
} else {
Ok((remaining, result))
}
}
ParsingMode::Complete => {
if !remaining.is_empty() {
result.push(Element::Content(slice_as_bytes(original, remaining)));
}
Ok((&remaining[remaining.len()..], result))
}
ParsingMode::Eof => {
Err(nom::Err::Failure(Error::new(
remaining,
nom::error::ErrorKind::Eof,
)))
}
};
}
Err(e) => {
if result.is_empty() {
return Err(e);
}
return Ok((remaining, result));
}
}
}
}
pub fn parse(input: &Bytes) -> IResult<&[u8], Vec<Element>, Error<&[u8]>> {
parse_loop(input, element, &ParsingMode::Streaming)
}
pub fn parse_complete(input: &Bytes) -> IResult<&[u8], Vec<Element>, Error<&[u8]>> {
parse_loop(input, element, &ParsingMode::Complete)
}
pub fn parse_eof(input: &Bytes) -> IResult<&[u8], Vec<Element>, Error<&[u8]>> {
if input.is_empty() {
return Ok((input.as_ref(), vec![]));
}
parse_loop(input, element_eof, &ParsingMode::Eof)
}
#[inline]
fn bytes_to_string(bytes: &[u8]) -> String {
unsafe { std::str::from_utf8_unchecked(bytes) }.to_owned()
}
pub fn parse_expression(input: &str) -> IResult<&str, Expr, Error<&str>> {
let bytes = input.as_bytes();
match expr(bytes) {
Ok((remaining_bytes, expr)) => {
let consumed = bytes.len() - remaining_bytes.len();
Ok((&input[consumed..], expr))
}
Err(nom::Err::Error(e)) => Err(nom::Err::Error(Error::new(input, e.code))),
Err(nom::Err::Failure(e)) => Err(nom::Err::Failure(Error::new(input, e.code))),
Err(nom::Err::Incomplete(_)) => {
unreachable!("complete parsers don't return Incomplete")
}
}
}
fn interpolated_text<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
streaming_bytes::take_while1(|c| !is_open_bracket(c) && !is_dollar(c) && c != BACKSLASH)
.map(|s: &[u8]| ParseResult::Single(Element::Content(slice_as_bytes(original, s))))
.parse(input)
}
fn interpolated_text_complete<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
take_while1(|c| !is_open_bracket(c) && !is_dollar(c) && c != BACKSLASH)
.map(|s: &[u8]| ParseResult::Single(Element::Content(slice_as_bytes(original, s))))
.parse(input)
}
pub fn interpolated_content(input: &Bytes) -> IResult<&[u8], Vec<Element>, Error<&[u8]>> {
let mut acc = Vec::with_capacity(4);
let mut rest = input.as_ref();
loop {
if let Ok((r, item)) = interpolated_expression(rest) {
item.append_to(&mut acc);
rest = r;
} else if let Ok((r, item)) = esi_escape_complete(input, rest) {
item.append_to(&mut acc);
rest = r;
} else if let Ok((r, item)) = interpolated_text_complete(input, rest) {
item.append_to(&mut acc);
rest = r;
} else {
break;
}
}
Ok((rest, acc))
}
fn element<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
alt((|i| parse_text(original, i), |i| tag_handler(original, i))).parse(input)
}
fn parse_text<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
streaming_bytes::take_while1(|c| !is_open_bracket(c))
.map(|s: &[u8]| ParseResult::Single(Element::Content(slice_as_bytes(original, s))))
.parse(input)
}
fn parse_text_complete<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
take_while1(|c: u8| !is_open_bracket(c))
.map(|s: &[u8]| ParseResult::Single(Element::Content(slice_as_bytes(original, s))))
.parse(input)
}
fn element_eof<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
alt((
|i| parse_text_complete(original, i),
|i| tag_handler(original, i),
))
.parse(input)
}
fn interpolated_element<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
match input.first() {
Some(&OPEN_BRACKET) => tag_handler(original, input),
Some(&BACKSLASH) => esi_escape(original, input),
Some(&DOLLAR) => alt((interpolated_expression, |i| tag_handler(original, i))).parse(input),
_ => alt((
|i| interpolated_text(original, i),
interpolated_expression,
|i| tag_handler(original, i),
))
.parse(input),
}
}
fn tag_content<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], Vec<Element>, Error<&'a [u8]>> {
let mut acc = Vec::with_capacity(10);
let mut rest = input;
loop {
match interpolated_element(original, rest) {
Ok((r, item)) => {
item.append_to(&mut acc);
if r.len() == rest.len() {
break;
}
rest = r;
}
Err(nom::Err::Incomplete(needed)) => return Err(nom::Err::Incomplete(needed)),
Err(_) => break,
}
}
Ok((rest, acc))
}
fn is_valid_variable_name(name: &str) -> bool {
if name.is_empty() || name.len() > 256 {
return false;
}
if let Some(brace_pos) = name.find('{') {
let base_name = &name[..brace_pos];
if !is_valid_base_variable_name(base_name) {
return false;
}
if !name.ends_with('}') {
return false;
}
true
} else {
is_valid_base_variable_name(name)
}
}
fn is_valid_base_variable_name(name: &str) -> bool {
let bytes = name.as_bytes();
match bytes.first() {
Some(b) if b.is_ascii_alphabetic() => {}
_ => return false,
}
bytes[1..]
.iter()
.all(|b| b.is_ascii_alphanumeric() || *b == UNDERSCORE)
}
fn parse_variable_name_with_subscript(name: &str) -> (String, Option<Expr>) {
if let Some(brace_pos) = name.find('{') {
if name.ends_with('}') {
let var_name = &name[..brace_pos];
let subscript_str = &name[brace_pos + 1..name.len() - 1];
let subscript_expr = subscript_str.parse::<i32>().map_or_else(
|_| {
if subscript_str
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == UNDERSCORE)
{
Some(Expr::String(Some(Bytes::copy_from_slice(
subscript_str.as_bytes(),
))))
} else if let Ok((_, expr)) = parse_expression(subscript_str) {
Some(expr)
} else {
None
}
},
|num| Some(Expr::Integer(num)),
);
if let Some(expr) = subscript_expr {
return (var_name.to_string(), Some(expr));
}
}
}
(name.to_string(), None)
}
fn esi_assign<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
alt((esi_assign_short, |i| esi_assign_long(original, i))).parse(input)
}
fn assign_attributes_short(mut attrs: Attrs<'_>) -> ParseResult {
let name = attrs_remove(&mut attrs, "name").unwrap_or_default();
if !is_valid_variable_name(name) {
return ParseResult::Empty;
}
let (var_name, subscript) = parse_variable_name_with_subscript(name);
let value_str = attrs_remove(&mut attrs, "value").unwrap_or_default();
let value = match parse_expression(value_str) {
Ok((_, expr)) => expr,
Err(_) => {
Expr::String(Some(Bytes::copy_from_slice(value_str.as_bytes())))
}
};
ParseResult::Single(Element::Esi(Tag::Assign {
name: var_name,
subscript,
value,
}))
}
fn parse_attr_as_expr(value_str: &str) -> Expr {
if value_str.is_empty() {
return Expr::String(Some(Bytes::new()));
}
if let Ok((remaining, expr)) = parse_expression(value_str) {
if remaining.is_empty() {
return expr;
}
}
let bytes = Bytes::copy_from_slice(value_str.as_bytes());
match interpolated_content(&bytes) {
Ok(([], elements)) => {
if elements.len() == 1 {
match elements.into_iter().next().unwrap() {
Element::Expr(expr) => expr,
Element::Content(text) => Expr::String(Some(text)),
_ => Expr::String(Some(bytes.clone())),
}
} else if !elements.is_empty() {
Expr::Interpolated(elements)
} else {
Expr::String(Some(Bytes::new()))
}
}
_ => Expr::String(Some(bytes.clone())),
}
}
fn assign_long(attrs: &Attrs<'_>, mut content: Vec<Element>) -> ParseResult {
let name = attrs_get(attrs, "name").unwrap_or_default();
if !is_valid_variable_name(name) {
return ParseResult::Empty;
}
let (var_name, subscript) = parse_variable_name_with_subscript(name);
let value = if content.is_empty() {
Expr::String(Some(Bytes::new()))
} else if content.len() == 1 {
match content.pop().expect("checked len == 1") {
Element::Expr(expr) => expr,
Element::Content(text) => {
match std::str::from_utf8(text.as_ref()) {
Ok(text_str) => match parse_expression(text_str) {
Ok((_, expr)) => expr,
Err(_) => Expr::String(Some(text)),
},
Err(_) => Expr::String(Some(text)),
}
}
_ => {
Expr::String(Some(Bytes::new()))
}
}
} else {
Expr::Interpolated(content)
};
ParseResult::Single(Element::Esi(Tag::Assign {
name: var_name,
subscript,
value,
}))
}
fn esi_assign_short(input: &[u8]) -> IResult<&[u8], ParseResult, Error<&[u8]>> {
delimited(
tag(TAG_ESI_ASSIGN_OPEN),
attributes,
preceded(multispace0, self_closing),
)
.map(assign_attributes_short)
.parse(input)
}
fn esi_assign_long<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
(
delimited(
tag(TAG_ESI_ASSIGN_OPEN),
attributes,
preceded(multispace0, close_bracket),
),
streaming_bytes::take_until(TAG_ESI_ASSIGN_CLOSE),
streaming_bytes::tag(TAG_ESI_ASSIGN_CLOSE),
)
.map(|(attrs, content, _)| {
let elements = parse_content_complete(original, content);
assign_long(&attrs, elements)
})
.parse(input)
}
fn parse_container_tag<'a>(
original: &Bytes,
input: &'a [u8],
opening_tag: &'static [u8],
closing_tag: &'static [u8],
constructor: impl FnOnce(Vec<Element>) -> Tag,
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
let (input, content) = delimited(
tag(opening_tag), |i| tag_content(original, i),
streaming_bytes::tag(closing_tag), )
.parse(input)?;
Ok((
input,
ParseResult::Single(Element::Esi(constructor(content))),
))
}
fn esi_except<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
parse_container_tag(
original,
input,
TAG_ESI_EXCEPT_OPEN,
TAG_ESI_EXCEPT_CLOSE,
Tag::Except,
)
}
fn esi_attempt<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
parse_container_tag(
original,
input,
TAG_ESI_ATTEMPT_OPEN,
TAG_ESI_ATTEMPT_CLOSE,
Tag::Attempt,
)
}
fn esi_try<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
let (input, _) = tag(TAG_ESI_TRY_OPEN).parse(input)?;
let (input, v) = tag_content(original, input)?;
let (input, _) = streaming_bytes::tag(TAG_ESI_TRY_CLOSE).parse(input)?;
let mut attempts = Vec::with_capacity(v.len());
let mut except = None;
for element in v {
match element {
Element::Esi(Tag::Attempt(cs)) => attempts.push(cs),
Element::Esi(Tag::Except(cs)) => {
except = Some(cs);
}
_ => {} }
}
Ok((
input,
ParseResult::Single(Element::Esi(Tag::Try {
attempt_events: attempts,
except_events: except.unwrap_or_default(),
})),
))
}
fn esi_otherwise<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
delimited(
tag(TAG_ESI_OTHERWISE_OPEN),
|i| tag_content(original, i),
streaming_bytes::tag(TAG_ESI_OTHERWISE_CLOSE),
)
.map(|mut content| {
content.insert(0, Element::Esi(Tag::Otherwise));
ParseResult::Multiple(content)
})
.parse(input)
}
fn esi_when<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
(
delimited(
tag(TAG_ESI_WHEN_OPEN),
attributes,
preceded(multispace0, alt((close_bracket, self_closing))),
),
|i| tag_content(original, i),
streaming_bytes::tag(TAG_ESI_WHEN_CLOSE),
)
.map(|(mut attrs, content, _)| {
let test = attrs_remove(&mut attrs, "test")
.unwrap_or_default()
.to_owned();
let match_name = attrs_remove(&mut attrs, "matchname").map(ToOwned::to_owned);
let mut result = content;
result.insert(0, Element::Esi(Tag::When { test, match_name }));
ParseResult::Multiple(result)
})
.parse(input)
}
fn esi_foreach<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
(
delimited(
tag(TAG_ESI_FOREACH_OPEN),
attributes,
preceded(multispace0, close_bracket),
),
|i| tag_content(original, i),
streaming_bytes::tag(TAG_ESI_FOREACH_CLOSE),
)
.map(|(mut attrs, content, _)| {
let collection_str = attrs_remove(&mut attrs, "collection").unwrap_or_default();
let collection = parse_attr_as_expr(collection_str);
let item = attrs_remove(&mut attrs, "item").map(ToOwned::to_owned);
ParseResult::Single(Element::Esi(Tag::Foreach {
collection,
item,
content,
}))
})
.parse(input)
}
fn esi_break(input: &[u8]) -> IResult<&[u8], ParseResult, Error<&[u8]>> {
delimited(tag(TAG_ESI_BREAK_OPEN), multispace0, self_closing)
.map(|_| ParseResult::Single(Element::Esi(Tag::Break)))
.parse(input)
}
fn esi_function_tag<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
(
delimited(
tag(TAG_ESI_FUNCTION_OPEN),
attributes,
preceded(multispace0, close_bracket),
),
|i| tag_content(original, i),
streaming_bytes::tag(TAG_ESI_FUNCTION_CLOSE),
)
.map(|(mut attrs, body, _)| {
let name = attrs_remove(&mut attrs, "name")
.unwrap_or_default()
.to_owned();
ParseResult::Single(Element::Esi(Tag::Function { name, body }))
})
.parse(input)
}
fn esi_return(input: &[u8]) -> IResult<&[u8], ParseResult, Error<&[u8]>> {
delimited(
tag(TAG_ESI_RETURN_OPEN),
attributes,
preceded(multispace0, self_closing),
)
.map(|mut attrs| {
let value_str = attrs_remove(&mut attrs, "value").unwrap_or_default();
let value = parse_attr_as_expr(value_str);
ParseResult::Single(Element::Esi(Tag::Return { value }))
})
.parse(input)
}
fn esi_choose<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
let (input, _) = tag(TAG_ESI_CHOOSE_OPEN).parse(input)?;
let (input, v) = tag_content(original, input)?;
let (input, _) = streaming_bytes::tag(TAG_ESI_CHOOSE_CLOSE).parse(input)?;
let mut when_branches = Vec::with_capacity(v.len());
let mut otherwise_events = Vec::new();
let mut current_when: Option<WhenBranch> = None;
let mut in_otherwise = false;
for element in v {
match element {
Element::Esi(Tag::When { test, match_name }) => {
if let Some(when_branch) = current_when.take() {
when_branches.push(when_branch);
}
in_otherwise = false;
let test_expr = match parse_expression(&test) {
Ok((_, expr)) => expr,
Err(_) => {
Expr::Integer(0)
}
};
current_when = Some(WhenBranch {
test: test_expr,
match_name,
content: Vec::new(),
});
}
Element::Esi(Tag::Otherwise) => {
if let Some(when_branch) = current_when.take() {
when_branches.push(when_branch);
}
in_otherwise = true;
}
_ => {
if in_otherwise {
otherwise_events.push(element);
} else if let Some(ref mut when_branch) = current_when {
when_branch.content.push(element);
}
}
}
}
if let Some(when_branch) = current_when {
when_branches.push(when_branch);
}
Ok((
input,
ParseResult::Single(Element::Esi(Tag::Choose {
when_branches,
otherwise_events,
})),
))
}
fn esi_vars<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
alt((esi_vars_short, |i| esi_vars_long(original, i))).parse(input)
}
fn parse_vars_attributes(mut attrs: Attrs<'_>) -> Result<ParseResult, &'static str> {
attrs_remove(&mut attrs, "name").map_or_else(
|| Err("no name field in short form vars"),
|name_val| {
if let Ok((_, expr)) = parse_expression(name_val) {
Ok(ParseResult::Single(Element::Expr(expr)))
} else {
Err("failed to parse expression")
}
},
)
}
fn esi_vars_short(input: &[u8]) -> IResult<&[u8], ParseResult, Error<&[u8]>> {
delimited(
tag(TAG_ESI_VARS_OPEN),
attributes,
preceded(multispace0, self_closing), )
.map_res(parse_vars_attributes)
.parse(input)
}
fn parse_content_complete(original: &Bytes, content: &[u8]) -> Vec<Element> {
let mut elements = Vec::new();
let mut remaining = content;
while !remaining.is_empty() {
if let Ok((rest, result)) = esi_escape_complete(original, remaining) {
result.append_to(&mut elements);
remaining = rest;
continue;
}
if let Ok((rest, result)) = interpolated_expression(remaining) {
result.append_to(&mut elements);
remaining = rest;
continue;
}
if let Ok((rest, result)) = interpolated_text_complete(original, remaining) {
result.append_to(&mut elements);
remaining = rest;
continue;
}
elements.push(Element::Content(slice_as_bytes(original, &remaining[..1])));
remaining = &remaining[1..];
}
elements
}
fn esi_vars_long<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
let (input, _) = tag(TAG_ESI_VARS_OPEN_COMPLETE).parse(input)?;
let (input, elements) = tag_content(original, input)?;
let (input, _) = streaming_bytes::tag(TAG_ESI_VARS_CLOSE).parse(input)?;
Ok((input, ParseResult::Multiple(elements)))
}
fn esi_comment(input: &[u8]) -> IResult<&[u8], ParseResult, Error<&[u8]>> {
delimited(
tag(TAG_ESI_COMMENT_OPEN),
attributes,
preceded(multispace0, self_closing), )
.map(|_| ParseResult::Empty)
.parse(input)
}
fn esi_remove(input: &[u8]) -> IResult<&[u8], ParseResult, Error<&[u8]>> {
let (input, _) = tag(TAG_ESI_REMOVE_OPEN).parse(input)?;
let (input, _) = streaming_bytes::take_until(TAG_ESI_REMOVE_CLOSE).parse(input)?;
let (input, _) = streaming_bytes::tag(TAG_ESI_REMOVE_CLOSE).parse(input)?;
Ok((input, ParseResult::Empty))
}
fn esi_text<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
delimited(
tag(TAG_ESI_TEXT_OPEN),
streaming_bytes::take_until(TAG_ESI_TEXT_CLOSE),
streaming_bytes::tag(TAG_ESI_TEXT_CLOSE),
)
.map(|v| ParseResult::Single(Element::Content(slice_as_bytes(original, v))))
.parse(input)
}
fn esi_include(input: &[u8]) -> IResult<&[u8], ParseResult, Error<&[u8]>> {
alt((esi_include_self_closing, esi_include_with_params)).parse(input)
}
fn extract_include_attrs(mut attrs: Attrs<'_>, params: Vec<(String, Expr)>) -> IncludeAttributes {
let src = parse_attr_as_expr(attrs_remove(&mut attrs, "src").unwrap_or_default());
let alt = attrs_remove(&mut attrs, "alt").map(parse_attr_as_expr);
let continue_on_error = attrs_get(&attrs, "onerror").is_some_and(|v| v == "continue");
let dca = if attrs_get(&attrs, "dca").is_some_and(|v| v.eq_ignore_ascii_case("esi")) {
DcaMode::Esi
} else {
DcaMode::None
};
let ttl = attrs_remove(&mut attrs, "ttl").map(ToOwned::to_owned);
let maxwait = attrs_remove(&mut attrs, "maxwait").and_then(|s| s.parse::<u32>().ok());
let no_store = attrs_get(&attrs, "no-store").is_some_and(|v| v.eq_ignore_ascii_case("on"));
let method = attrs_remove(&mut attrs, "method").map(parse_attr_as_expr);
let entity = attrs_remove(&mut attrs, "entity").map(parse_attr_as_expr);
let mut appendheaders = Vec::new();
let mut setheaders = Vec::new();
let mut removeheaders = Vec::new();
for (key, value) in &attrs {
if key.starts_with("appendheader") {
appendheaders.push(parse_attr_as_expr(value));
} else if key.starts_with("setheader") {
setheaders.push(parse_attr_as_expr(value));
} else if key.starts_with("removeheader") {
removeheaders.push(parse_attr_as_expr(value));
}
}
IncludeAttributes {
src,
alt,
continue_on_error,
dca,
ttl,
maxwait,
no_store,
method,
entity,
appendheaders,
removeheaders,
setheaders,
params,
}
}
fn esi_include_self_closing(input: &[u8]) -> IResult<&[u8], ParseResult, Error<&[u8]>> {
delimited(
tag(TAG_ESI_INCLUDE_OPEN),
attributes,
preceded(multispace0, self_closing),
)
.map(|attrs| {
let attrs = extract_include_attrs(attrs, Vec::new());
ParseResult::Single(Element::Esi(Tag::Include { attrs }))
})
.parse(input)
}
fn esi_include_with_params(input: &[u8]) -> IResult<&[u8], ParseResult, Error<&[u8]>> {
let (rest, attrs) = delimited(
tag(TAG_ESI_INCLUDE_OPEN),
attributes,
preceded(multispace0, close_bracket),
)
.parse(input)?;
let mut params = Vec::new();
let mut rest = rest;
loop {
match streaming_char::multispace0::<_, Error<&[u8]>>(rest) {
Err(nom::Err::Incomplete(needed)) => return Err(nom::Err::Incomplete(needed)),
Err(_) => break,
Ok((r, _)) => match esi_param(r) {
Ok((r, param)) => {
params.push(param);
rest = r;
}
Err(nom::Err::Incomplete(needed)) => return Err(nom::Err::Incomplete(needed)),
Err(_) => break,
},
}
}
let (rest, _) = preceded(
streaming_char::multispace0,
streaming_bytes::tag(TAG_ESI_INCLUDE_CLOSE),
)
.parse(rest)?;
let attrs = extract_include_attrs(attrs, params);
Ok((
rest,
ParseResult::Single(Element::Esi(Tag::Include { attrs })),
))
}
fn esi_eval(input: &[u8]) -> IResult<&[u8], ParseResult, Error<&[u8]>> {
alt((esi_eval_self_closing, esi_eval_with_params)).parse(input)
}
fn esi_eval_self_closing(input: &[u8]) -> IResult<&[u8], ParseResult, Error<&[u8]>> {
delimited(
tag(TAG_ESI_EVAL_OPEN),
attributes,
preceded(multispace0, self_closing),
)
.map(|attrs| {
let mut attrs = extract_include_attrs(attrs, Vec::new());
attrs.alt = None;
ParseResult::Single(Element::Esi(Tag::Eval { attrs }))
})
.parse(input)
}
fn esi_eval_with_params(input: &[u8]) -> IResult<&[u8], ParseResult, Error<&[u8]>> {
let (rest, attrs) = delimited(
tag(TAG_ESI_EVAL_OPEN),
attributes,
preceded(multispace0, close_bracket),
)
.parse(input)?;
let mut params = Vec::new();
let mut rest = rest;
loop {
match streaming_char::multispace0::<_, Error<&[u8]>>(rest) {
Err(nom::Err::Incomplete(needed)) => return Err(nom::Err::Incomplete(needed)),
Err(_) => break,
Ok((r, _)) => match esi_param(r) {
Ok((r, param)) => {
params.push(param);
rest = r;
}
Err(nom::Err::Incomplete(needed)) => return Err(nom::Err::Incomplete(needed)),
Err(_) => break,
},
}
}
let (rest, _) = preceded(
streaming_char::multispace0,
streaming_bytes::tag(TAG_ESI_EVAL_CLOSE),
)
.parse(rest)?;
let mut attrs = extract_include_attrs(attrs, params);
attrs.alt = None;
Ok((rest, ParseResult::Single(Element::Esi(Tag::Eval { attrs }))))
}
fn esi_param(input: &[u8]) -> IResult<&[u8], (String, Expr), Error<&[u8]>> {
let (after, _) = esi_opening_tag(input)?;
let tag_slice = &input[..input.len() - after.len()];
let (_, mut attrs) = delimited(
tag(TAG_ESI_PARAM_OPEN),
attributes,
preceded(
multispace0,
alt((tag(TAG_SELF_CLOSE), tag(&[CLOSE_BRACKET] as &[u8]))),
),
)
.parse(tag_slice)?;
let name = attrs_remove(&mut attrs, "name")
.unwrap_or_default()
.to_owned();
let value = parse_attr_as_expr(attrs_remove(&mut attrs, "value").unwrap_or_default());
Ok((after, (name, value)))
}
fn attributes(input: &[u8]) -> IResult<&[u8], Attrs<'_>, Error<&[u8]>> {
let mut acc = Vec::new();
let mut rest = input;
loop {
let Ok((r, _)) = multispace1::<_, Error<&[u8]>>(rest) else {
break;
};
let Ok((r, k)): Result<_, nom::Err<Error<&[u8]>>> =
take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'-').parse(r)
else {
break;
};
let Ok((r, _)): Result<_, nom::Err<Error<&[u8]>>> = tag(EQUALS).parse(r) else {
break;
};
let Ok((r, v)) = htmlstring(r) else { break };
let key = unsafe { std::str::from_utf8_unchecked(k) };
if let Ok(val) = std::str::from_utf8(v) {
acc.push((key, val));
}
rest = r;
}
Ok((rest, acc))
}
fn htmlstring(input: &[u8]) -> IResult<&[u8], &[u8], Error<&[u8]>> {
alt((
delimited(
tag(&[DOUBLE_QUOTE] as &[u8]),
take_while(|c: u8| !is_double_quote(c)),
tag(&[DOUBLE_QUOTE] as &[u8]),
),
delimited(
tag(&[SINGLE_QUOTE] as &[u8]),
take_while(|c: u8| !is_single_quote(c)),
tag(&[SINGLE_QUOTE] as &[u8]),
),
))
.parse(input)
}
#[inline]
fn close_bracket(input: &[u8]) -> IResult<&[u8], &[u8], Error<&[u8]>> {
tag(&[CLOSE_BRACKET] as &[u8]).parse(input)
}
#[inline]
fn self_closing(input: &[u8]) -> IResult<&[u8], &[u8], Error<&[u8]>> {
tag(TAG_SELF_CLOSE).parse(input)
}
#[inline]
fn streaming_close_bracket(input: &[u8]) -> IResult<&[u8], &[u8], Error<&[u8]>> {
streaming_bytes::tag(&[CLOSE_BRACKET] as &[u8]).parse(input)
}
#[inline]
fn streaming_open_bracket(input: &[u8]) -> IResult<&[u8], &[u8], Error<&[u8]>> {
streaming_bytes::tag(&[OPEN_BRACKET] as &[u8]).parse(input)
}
#[inline]
const fn is_close_bracket(b: u8) -> bool {
b == CLOSE_BRACKET
}
#[inline]
const fn is_double_quote(b: u8) -> bool {
b == DOUBLE_QUOTE
}
#[inline]
const fn is_single_quote(b: u8) -> bool {
b == SINGLE_QUOTE
}
#[inline]
const fn is_tag_start(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == EXCLAMATION
}
#[inline]
const fn is_tag_cont(b: u8) -> bool {
b.is_ascii_alphanumeric() || matches!(b, HYPHEN | UNDERSCORE | COLON)
}
#[inline]
fn tag_name(input: &[u8]) -> IResult<&[u8], &[u8], Error<&[u8]>> {
recognize((
streaming_bytes::take_while_m_n(1, 1, is_tag_start), streaming_bytes::take_while(is_tag_cont), ))
.parse(input)
}
fn skip_tag_attrs(input: &[u8]) -> IResult<&[u8], &[u8], Error<&[u8]>> {
let mut i = 0;
while i < input.len() {
match input[i] {
CLOSE_BRACKET => return Ok((&input[i..], &input[..i])),
DOUBLE_QUOTE | SINGLE_QUOTE => {
let quote = input[i];
i += 1;
while i < input.len() && input[i] != quote {
i += 1;
}
if i >= input.len() {
return Err(nom::Err::Incomplete(nom::Needed::Unknown));
}
i += 1; }
_ => i += 1,
}
}
Err(nom::Err::Incomplete(nom::Needed::Unknown))
}
#[allow(clippy::type_complexity)]
fn esi_opening_tag(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8]), Error<&[u8]>> {
let start = input;
let (rest, _) = streaming_open_bracket(input)?;
let (rest, name) = tag_name(rest)?;
let (rest, _) = skip_tag_attrs(rest)?;
let (rest, _) = streaming_close_bracket(rest)?;
Ok((rest, (name, start)))
}
fn tag_handler<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
alt((
|i| html_comment_content(original, i),
|i| closing_tag(original, i),
|i| {
let (rest, (name, start)) = esi_opening_tag(i)?;
match name {
TAG_NAME_ESI_ASSIGN => esi_assign(original, start),
TAG_NAME_ESI_INCLUDE => esi_include(start),
TAG_NAME_ESI_EVAL => esi_eval(start),
TAG_NAME_ESI_VARS => esi_vars(original, start),
TAG_NAME_ESI_COMMENT => esi_comment(start),
TAG_NAME_ESI_REMOVE => esi_remove(start),
TAG_NAME_ESI_TEXT => esi_text(original, start),
TAG_NAME_ESI_CHOOSE => esi_choose(original, start),
TAG_NAME_ESI_TRY => esi_try(original, start),
TAG_NAME_ESI_WHEN => esi_when(original, start),
TAG_NAME_ESI_OTHERWISE => esi_otherwise(original, start),
TAG_NAME_ESI_ATTEMPT => esi_attempt(original, start),
TAG_NAME_ESI_EXCEPT => esi_except(original, start),
TAG_NAME_ESI_FOREACH => esi_foreach(original, start),
TAG_NAME_ESI_BREAK => esi_break(start),
TAG_NAME_ESI_FUNCTION => esi_function_tag(original, start),
TAG_NAME_ESI_RETURN => esi_return(start),
_ if name.eq_ignore_ascii_case(TAG_NAME_SCRIPT) => html_script_tag(original, start),
_ => {
let full_tag = &start[..start.len() - rest.len()];
Ok((
rest,
ParseResult::Single(Element::Html(slice_as_bytes(original, full_tag))),
))
}
}
},
))
.parse(input)
}
fn html_comment_content<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
let start = input;
let (rest, _) = delimited(
streaming_bytes::tag(HTML_COMMENT_OPEN),
streaming_bytes::take_until(HTML_COMMENT_CLOSE),
streaming_bytes::tag(HTML_COMMENT_CLOSE),
)
.parse(input)?;
let full_comment = &start[..start.len() - rest.len()];
Ok((
rest,
ParseResult::Single(Element::Html(slice_as_bytes(original, full_comment))),
))
}
fn script_content(input: &[u8]) -> IResult<&[u8], &[u8], Error<&[u8]>> {
const CLOSING_SCRIPT: &[u8] = TAG_SCRIPT_CLOSE;
for i in 0..input.len() {
if i + CLOSING_SCRIPT.len() <= input.len() {
let window = &input[i..i + CLOSING_SCRIPT.len()];
if window.eq_ignore_ascii_case(CLOSING_SCRIPT) {
return Ok((&input[i..], &input[..i]));
}
}
}
Err(nom::Err::Incomplete(nom::Needed::Unknown))
}
fn html_script_tag<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
let start = input;
let (input, _) = recognize(delimited(
tag_no_case(TAG_SCRIPT_OPEN),
take_while(|c: u8| !is_close_bracket(c)),
close_bracket,
))
.parse(input)?;
let (input, _) = opt((
script_content,
recognize(delimited(
streaming_bytes::tag_no_case(TAG_SCRIPT_CLOSE),
streaming_char::multispace0,
streaming_close_bracket,
)),
))
.parse(input)?;
let full_script = &start[..start.len() - input.len()];
Ok((
input,
ParseResult::Single(Element::Html(slice_as_bytes(original, full_script))),
))
}
fn closing_tag<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
let (_, _) = peek(not(streaming_bytes::tag(ESI_CLOSE_PREFIX))).parse(input)?;
recognize((
streaming_bytes::tag(TAG_OPEN_CLOSE),
tag_name,
streaming_char::multispace0,
streaming_close_bracket,
))
.map(|s: &[u8]| ParseResult::Single(Element::Html(slice_as_bytes(original, s))))
.parse(input)
}
#[inline]
const fn is_open_bracket(b: u8) -> bool {
b == OPEN_BRACKET
}
#[inline]
const fn is_dollar(b: u8) -> bool {
b == DOLLAR
}
#[inline]
const fn is_alphanumeric_or_underscore(c: u8) -> bool {
c.is_ascii_alphanumeric() || c == UNDERSCORE
}
#[inline]
const fn is_lower_alphanumeric_or_underscore(c: u8) -> bool {
c.is_ascii_lowercase() || c.is_ascii_digit() || c == UNDERSCORE
}
fn esi_fn_name(input: &[u8]) -> IResult<&[u8], String, Error<&[u8]>> {
preceded(
tag(&[DOLLAR] as &[u8]),
take_while1(is_lower_alphanumeric_or_underscore),
)
.map(bytes_to_string)
.parse(input)
}
fn esi_var_name(input: &[u8]) -> IResult<&[u8], Expr, Error<&[u8]>> {
(
take_while1(is_alphanumeric_or_underscore),
opt(delimited(
tag(&[OPEN_BRACE] as &[u8]),
esi_var_key_expr,
tag(&[CLOSE_BRACE] as &[u8]),
)),
opt(preceded(tag(PIPE), fn_nested_argument)),
)
.map(|(name, key, default): (&[u8], _, _)| {
Expr::Variable(
bytes_to_string(name),
key.map(Box::new),
default.map(Box::new),
)
})
.parse(input)
}
fn not_dollar_or_curlies(input: &[u8]) -> IResult<&[u8], Bytes, Error<&[u8]>> {
take_while(|c| {
!is_dollar(c) && c != OPEN_BRACE && c != CLOSE_BRACE && c != COMMA && c != DOUBLE_QUOTE
})
.map(Bytes::copy_from_slice)
.parse(input)
}
fn escaped_string_content(input: &[u8]) -> IResult<&[u8], Vec<u8>, Error<&[u8]>> {
let mut result = Vec::new();
let mut remaining = input;
loop {
let (rest, chunk) =
take_while(|c: u8| c != SINGLE_QUOTE && c != BACKSLASH).parse(remaining)?;
result.extend_from_slice(chunk);
if rest.is_empty() || rest[0] == SINGLE_QUOTE {
return Ok((rest, result));
}
if rest.len() < 2 {
result.push(BACKSLASH);
return Ok((&rest[1..], result));
}
result.push(rest[1]);
remaining = &rest[2..];
}
}
fn esi_escape<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
use nom::error::ErrorKind;
if input.is_empty() {
return Err(nom::Err::Incomplete(nom::Needed::new(2)));
}
if input[0] != BACKSLASH {
return Err(nom::Err::Error(Error::new(input, ErrorKind::Tag)));
}
if input.len() < 2 {
return Err(nom::Err::Incomplete(nom::Needed::new(1)));
}
let escaped_byte = &input[1..2];
Ok((
&input[2..],
ParseResult::Single(Element::Content(slice_as_bytes(original, escaped_byte))),
))
}
fn esi_escape_complete<'a>(
original: &Bytes,
input: &'a [u8],
) -> IResult<&'a [u8], ParseResult, Error<&'a [u8]>> {
use nom::error::ErrorKind;
if input.len() < 2 || input[0] != BACKSLASH {
return Err(nom::Err::Error(Error::new(input, ErrorKind::Tag)));
}
let escaped_byte = &input[1..2];
Ok((
&input[2..],
ParseResult::Single(Element::Content(slice_as_bytes(original, escaped_byte))),
))
}
fn single_quoted_string(input: &[u8]) -> IResult<&[u8], Bytes, Error<&[u8]>> {
let (input, _) = tag(&[SINGLE_QUOTE] as &[u8]).parse(input)?;
let (input, content) = escaped_string_content(input)?;
let (input, _) = tag(&[SINGLE_QUOTE] as &[u8]).parse(input)?;
Ok((input, Bytes::from(content)))
}
fn triple_quoted_string(input: &[u8]) -> IResult<&[u8], Bytes, Error<&[u8]>> {
delimited(
tag(QUOTE_TRIPLE),
take_until(QUOTE_TRIPLE),
tag(QUOTE_TRIPLE),
)
.map(Bytes::copy_from_slice)
.parse(input)
}
fn string(input: &[u8]) -> IResult<&[u8], Expr, Error<&[u8]>> {
alt((triple_quoted_string, single_quoted_string))
.map(|bytes: Bytes| {
if bytes.is_empty() {
Expr::String(None)
} else {
Expr::String(Some(bytes))
}
})
.parse(input)
}
fn var_key(input: &[u8]) -> IResult<&[u8], Bytes, Error<&[u8]>> {
alt((
triple_quoted_string,
single_quoted_string,
not_dollar_or_curlies,
))
.parse(input)
}
fn esi_var_key_expr(input: &[u8]) -> IResult<&[u8], Expr, Error<&[u8]>> {
alt((
esi_variable,
var_key.map(|b: Bytes| Expr::String(Some(b))),
))
.parse(input)
}
fn fn_argument(input: &[u8]) -> IResult<&[u8], Vec<Expr>, Error<&[u8]>> {
let (input, mut parsed) = separated_list0(
(multispace0, tag(&[COMMA] as &[u8]), multispace0),
fn_nested_argument,
)
.parse(input)?;
if parsed.len() == 1 && parsed[0] == Expr::String(None) {
parsed = vec![];
}
Ok((input, parsed))
}
fn fn_nested_argument(input: &[u8]) -> IResult<&[u8], Expr, Error<&[u8]>> {
alt((expr, bareword)).parse(input)
}
fn integer(input: &[u8]) -> IResult<&[u8], Expr, Error<&[u8]>> {
recognize((
opt(tag(&[HYPHEN] as &[u8])),
take_while1(|c: u8| c.is_ascii_digit()),
))
.map_res(|s: &[u8]| {
unsafe { std::str::from_utf8_unchecked(s) }
.parse::<i32>()
.map(Expr::Integer)
})
.parse(input)
}
fn bareword(input: &[u8]) -> IResult<&[u8], Expr, Error<&[u8]>> {
take_while1(is_alphanumeric_or_underscore)
.map(|name: &[u8]| Expr::Variable(bytes_to_string(name), None, None))
.parse(input)
}
fn esi_function(input: &[u8]) -> IResult<&[u8], Expr, Error<&[u8]>> {
let (input, parsed) = (
esi_fn_name,
delimited(
terminated(tag(&[OPEN_PAREN] as &[u8]), multispace0),
fn_argument,
preceded(multispace0, tag(&[CLOSE_PAREN] as &[u8])),
),
)
.parse(input)?;
let (name, args) = parsed;
Ok((input, Expr::Call(name, args)))
}
fn esi_variable(input: &[u8]) -> IResult<&[u8], Expr, Error<&[u8]>> {
delimited(tag(VAR_OPEN), esi_var_name, tag(&[CLOSE_PAREN] as &[u8])).parse(input)
}
fn operator(input: &[u8]) -> IResult<&[u8], Operator, Error<&[u8]>> {
alt((
tag(OP_MATCHES_I).map(|_| Operator::MatchesInsensitive),
tag(OP_MATCHES).map(|_| Operator::Matches),
tag(OP_HAS_I).map(|_| Operator::HasInsensitive),
tag(OP_HAS).map(|_| Operator::Has),
tag(OP_EQUALS_COMP).map(|_| Operator::Equals),
tag(OP_NOT_EQUALS).map(|_| Operator::NotEquals),
tag(OP_LESS_EQUAL).map(|_| Operator::LessThanOrEqual),
tag(OP_GREATER_EQUAL).map(|_| Operator::GreaterThanOrEqual),
tag(&[OPEN_BRACKET] as &[u8]).map(|_| Operator::LessThan),
tag(&[CLOSE_BRACKET] as &[u8]).map(|_| Operator::GreaterThan),
tag(OP_AND).map(|_| Operator::And),
tag(OP_OR).map(|_| Operator::Or),
tag(OP_ADD).map(|_| Operator::Add),
tag(&[HYPHEN] as &[u8]).map(|_| Operator::Subtract),
tag(OP_MULTIPLY).map(|_| Operator::Multiply),
tag(OP_DIVIDE).map(|_| Operator::Divide),
tag(OP_MODULO).map(|_| Operator::Modulo),
))
.parse(input)
}
fn interpolated_expression(input: &[u8]) -> IResult<&[u8], ParseResult, Error<&[u8]>> {
let expr = match input.first() {
Some(&OPEN_BRACE) => dict_literal(input),
Some(&OPEN_SQ_BRACKET) => list_literal(input),
Some(&DOLLAR) => alt((esi_function, esi_variable)).parse(input),
Some(b'0'..=b'9') => integer(input),
Some(&SINGLE_QUOTE) => string(input),
_ => {
return Err(nom::Err::Error(Error::new(
input,
nom::error::ErrorKind::Alt,
)))
}
}?;
Ok((expr.0, ParseResult::Single(Element::Expr(expr.1))))
}
fn dict_literal(input: &[u8]) -> IResult<&[u8], Expr, Error<&[u8]>> {
delimited(
tag(&[OPEN_BRACE] as &[u8]),
separated_list0(
(multispace0, tag(&[COMMA] as &[u8]), multispace0),
(
delimited(multispace0, primary_expr, multispace0),
preceded(
tag(&[COLON] as &[u8]),
delimited(multispace0, primary_expr, multispace0),
),
),
),
preceded(multispace0, tag(&[CLOSE_BRACE] as &[u8])),
)
.map(Expr::DictLiteral)
.parse(input)
}
fn list_literal(input: &[u8]) -> IResult<&[u8], Expr, Error<&[u8]>> {
delimited(
tag(&[OPEN_SQ_BRACKET] as &[u8]),
alt((
(
delimited(multispace0, primary_expr, multispace0),
tag(OP_RANGE),
delimited(multispace0, primary_expr, multispace0),
)
.map(|(start, _, end)| {
Expr::Comparison {
left: Box::new(start),
operator: Operator::Range,
right: Box::new(end),
}
}),
separated_list0(
(multispace0, tag(&[COMMA] as &[u8]), multispace0),
delimited(multispace0, primary_expr, multispace0),
)
.map(Expr::ListLiteral),
)),
preceded(multispace0, tag(CLOSE_SQ_BRACKET)),
)
.parse(input)
}
fn primary_expr(input: &[u8]) -> IResult<&[u8], Expr, Error<&[u8]>> {
match input.first() {
Some(&OPEN_PAREN) => delimited(
tag(&[OPEN_PAREN] as &[u8]),
delimited(multispace0, expr, multispace0),
tag(&[CLOSE_PAREN] as &[u8]),
)
.parse(input),
Some(&HYPHEN) => integer(input),
_ => {
let (rest, result) = interpolated_expression(input)?;
match result {
ParseResult::Single(Element::Expr(expr)) => Ok((rest, expr)),
_ => unreachable!("interpolated_expression always returns Single(Expr)"),
}
}
}
}
fn expr(input: &[u8]) -> IResult<&[u8], Expr, Error<&[u8]>> {
let (mut rest, mut left) = unary_expr(input)?;
loop {
let Ok((r, _)) = multispace0::<_, Error<&[u8]>>(rest) else {
break;
};
let Ok((r, op)) = operator(r) else { break };
let Ok((r, _)) = multispace0::<_, Error<&[u8]>>(r) else {
break;
};
let Ok((r, right)) = unary_expr(r) else { break };
left = Expr::Comparison {
left: Box::new(left),
operator: op,
right: Box::new(right),
};
rest = r;
}
Ok((rest, left))
}
fn unary_expr(input: &[u8]) -> IResult<&[u8], Expr, Error<&[u8]>> {
alt((
preceded(
tag(&[EXCLAMATION] as &[u8]),
preceded(multispace0, unary_expr),
)
.map(|expr| Expr::Not(Box::new(expr))),
primary_expr,
))
.parse(input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_choose() {
let input = b"<esi:choose></esi:choose>";
let bytes = Bytes::from_static(input);
let result = parse_complete(&bytes);
match result {
Ok((rest, _)) => {
assert_eq!(rest.len(), 0, "Should parse completely");
}
Err(e) => {
panic!("Parse failed with error: {:?}", e);
}
}
}
#[test]
fn test_choose_with_when() {
let input = b"<esi:choose><esi:when test=\"1\">hi</esi:when></esi:choose>";
let bytes = Bytes::from_static(input);
let result = parse_complete(&bytes);
match result {
Ok((rest, result)) => {
if rest.is_empty() {
println!("Success! Result: {:?}", result);
} else {
panic!(
"Did not parse completely. Remaining: {:?}",
String::from_utf8_lossy(rest)
);
}
}
Err(e) => {
panic!("Parse failed with error: {:?}", e);
}
}
}
#[test]
fn test_greater_than_in_quoted_attribute() {
let input = b"<esi:choose><esi:when test=\"$(x) > 5\">big</esi:when></esi:choose>";
let bytes = Bytes::from_static(input);
let result = parse_complete(&bytes);
match result {
Ok((rest, _)) => {
assert!(
rest.is_empty(),
"Should parse completely, remaining: {:?}",
String::from_utf8_lossy(rest)
);
}
Err(e) => panic!("Parse failed: {:?}", e),
}
let input = b"<esi:choose><esi:when test='$(x) > 5'>big</esi:when></esi:choose>";
let bytes = Bytes::from_static(input);
let result = parse_complete(&bytes);
match result {
Ok((rest, _)) => {
assert!(
rest.is_empty(),
"Should parse completely, remaining: {:?}",
String::from_utf8_lossy(rest)
);
}
Err(e) => panic!("Parse failed: {:?}", e),
}
}
#[test]
fn test_parse() {
let input = br#"
<a>foo</a>
<bar />
baz
<esi:vars name="$(hello)"/>
<esi:vars>
hello <br>
</esi:vars>
<sCripT src="whatever">
<baz>
<script> less </fuckery more </script>
<esi:remove>should not appear</esi:remove>
<esi:comment text="also should not appear" />
<esi:text> this <esi:vars>$(should)</esi> appear unchanged</esi:text>
<esi:include src="whatever" />
<esi:choose>
should not appear
</esi:choose>
<esi:choose>
should not appear
<esi:when test="whatever">hi</esi:when>
<esi:otherwise>goodbye</esi:otherwise>
should not appear
</esi:choose>
<esi:try>
should not appear
<esi:attempt>
attempt 1
</esi:attempt>
should not appear
<esi:attempt>
attempt 2
</esi:attempt>
should not appear
<esi:except>
exception!
</esi:except>
</esi:try>"#;
let bytes = Bytes::from_static(input);
let result = parse_complete(&bytes);
match result {
Ok((rest, _)) => {
if !rest.is_empty() {
panic!(
"Failed to parse completely. Remaining: {:?}",
String::from_utf8_lossy(rest)
);
}
}
Err(e) => {
panic!("Parse failed with error: {:?}", e);
}
}
}
#[test]
fn test_parse_script() {
let input = b"<sCripT> less < more </scRIpt>";
let bytes = Bytes::from_static(input);
let (rest, x) = html_script_tag(&bytes, input).unwrap();
assert_eq!(rest.len(), 0);
assert!(matches!(
x,
ParseResult::Single(Element::Html(ref h)) if h.as_ref() == b"<sCripT> less < more </scRIpt>"
));
}
#[test]
fn test_parse_script_with_src() {
let input = b"<sCripT src=\"whatever\"></sCripT>";
let bytes = Bytes::from_static(input);
let (rest, x) = html_script_tag(&bytes, input).unwrap();
assert_eq!(rest.len(), 0);
assert!(matches!(
x,
ParseResult::Single(Element::Html(ref h)) if h.as_ref() == b"<sCripT src=\"whatever\"></sCripT>"
));
}
#[test]
fn test_parse_esi_vars_short() {
let input = br#"<esi:vars name="$(hello)"/>"#;
let bytes = Bytes::from_static(input);
let (rest, x) = esi_vars(&bytes, input).unwrap();
assert_eq!(rest.len(), 0);
match x {
ParseResult::Single(Element::Expr(Expr::Variable(name, None, None))) => {
assert_eq!(name, "hello");
}
ParseResult::Single(e) => {
panic!("Expected Variable expression, got {:?}", e);
}
ParseResult::Multiple(_) => {
panic!("Expected ParseResult::Single, got Multiple");
}
ParseResult::Empty => {
panic!("Expected ParseResult::Single, got Empty");
}
}
}
#[test]
fn test_parse_esi_vars_long() {
let input = br#"<esi:vars>hello<br></esi:vars>"#;
let bytes = Bytes::from_static(input);
let (rest, x) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0);
assert_eq!(
x,
[
Element::Content(Bytes::from_static(b"hello")),
Element::Html(Bytes::from_static(b"<br>")),
]
);
}
#[test]
fn test_nested_vars() {
let input = br#"<esi:vars>outer<esi:vars>inner</esi:vars></esi:vars>"#;
let bytes = Bytes::from_static(input);
let (rest, elements) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0, "Should parse completely");
assert_eq!(
elements,
[
Element::Content(Bytes::from_static(b"outer")),
Element::Content(Bytes::from_static(b"inner")),
]
);
}
#[test]
fn test_vars_with_expressions() {
let input = br#"<esi:vars>Hello $(name), welcome!</esi:vars>"#;
let bytes = Bytes::from_static(input);
let (rest, elements) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0, "Should parse completely");
assert_eq!(elements.len(), 3);
assert!(matches!(&elements[0], Element::Content(t) if t.as_ref() == b"Hello "));
assert!(matches!(&elements[1], Element::Expr(_)));
assert!(matches!(&elements[2], Element::Content(t) if t.as_ref() == b", welcome!"));
}
#[test]
fn test_assign_inside_vars() {
let input = br#"
<esi:vars>
<esi:assign name="xyz" value="'test'" />
Result: $(xyz)
</esi:vars>"#;
let bytes = Bytes::from_static(input);
let (rest, elements) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0, "Should parse completely");
assert!(
elements.len() >= 3,
"Should have at least assign tag, text, and expression"
);
let has_assign = elements
.iter()
.any(|e| matches!(e, Element::Esi(Tag::Assign { name, .. }) if name == "xyz"));
assert!(has_assign, "Should contain esi:assign tag with name='xyz'");
let has_expr = elements
.iter()
.any(|e| matches!(e, Element::Expr(Expr::Variable(name, None, None)) if name == "xyz"));
assert!(has_expr, "Should contain expression $(xyz)");
}
#[test]
fn test_parse_complex_expr() {
let input = br#"<esi:vars name="$call('hello') matches $(var{'key'})"/>"#;
let bytes = Bytes::from_static(input);
let (rest, x) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0);
assert_eq!(
x,
[Element::Expr(Expr::Comparison {
left: Box::new(Expr::Call(
"call".to_string(),
vec![Expr::String(Some(Bytes::from("hello")))]
)),
operator: Operator::Matches,
right: Box::new(Expr::Variable(
"var".to_string(),
Some(Box::new(Expr::String(Some(Bytes::from("key"))))),
None
))
})]
);
}
#[test]
fn test_vars_with_content() {
let input = br#"<esi:vars>
$(QUERY_STRING{param})
</esi:vars>"#;
let bytes = Bytes::from_static(input);
let result = esi_vars_long(&bytes, input);
assert!(
result.is_ok(),
"esi_vars_long should parse successfully: {:?}",
result.err()
);
let (rest, _elements) = result.unwrap();
assert_eq!(
rest.len(),
0,
"Parser should consume all input. Remaining: '{:?}'",
String::from_utf8_lossy(rest)
);
}
#[test]
fn test_exact_failing_input() {
let input = br#"
<esi:assign name="keyVar" value="'param'" />
<esi:vars>
$(QUERY_STRING{param})
$(QUERY_STRING{$(keyVar)})
</esi:vars>
"#;
let bytes = Bytes::from_static(input);
let (rest, elements) = parse_complete(&bytes).unwrap();
eprintln!("Chunks: {:?}", elements);
eprintln!("Remaining: {:?}", String::from_utf8_lossy(rest));
assert_eq!(
rest.len(),
0,
"Parser should consume all input. Remaining: '{:?}'",
String::from_utf8_lossy(rest)
);
}
#[test]
fn test_esi_vars_directly() {
let input = br#"<esi:vars>
$(QUERY_STRING{param})
$(QUERY_STRING{$(keyVar)})
</esi:vars>"#;
let bytes = Bytes::from_static(input);
let result = esi_vars(&bytes, input);
assert!(result.is_ok(), "esi_vars should parse: {:?}", result.err());
let (rest, _) = result.unwrap();
assert_eq!(rest.len(), 0, "Should consume all input");
}
#[test]
fn test_esi_tag_on_vars() {
let input = br#"<esi:vars>
$(QUERY_STRING{param})
</esi:vars>"#;
let bytes = Bytes::from_static(input);
let (rest, _result) = esi_vars(&bytes, input).unwrap();
assert_eq!(rest.len(), 0, "Parser should consume all input");
}
#[test]
fn test_assign_then_vars() {
let input =
br#"<esi:assign name="key" value="'val'" /><esi:vars>$(QUERY_STRING{param})</esi:vars>"#;
let bytes = Bytes::from_static(input);
let (rest, _elements) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0);
}
#[test]
fn test_parse_plain_text() {
let input = b"hello\nthere";
let bytes = Bytes::from_static(input);
let (rest, x) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0);
assert_eq!(x, [Element::Content(Bytes::from_static(b"hello\nthere"))]);
}
#[test]
fn test_parse_interpolated() {
let input = b"hello $(foo)<esi:vars>goodbye $(foo)</esi:vars>";
let bytes = Bytes::from_static(input);
let (rest, x) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0);
assert_eq!(
x,
[
Element::Content(Bytes::from_static(b"hello $(foo)")),
Element::Content(Bytes::from_static(b"goodbye ")),
Element::Expr(Expr::Variable("foo".to_string(), None, None)),
]
);
}
#[test]
fn test_parse_examples() {
let input = include_bytes!("../../examples/esi_vars_example/src/index.html");
let bytes = Bytes::from_static(input);
let (rest, _) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0);
}
#[test]
fn test_parse_equality_operators() {
let input = b"$(foo) == 'bar'";
let (rest, result) = expr(input).unwrap();
assert_eq!(rest.len(), 0);
assert!(matches!(
result,
Expr::Comparison {
operator: Operator::Equals,
..
}
));
let input2 = b"$(foo) != 'bar'";
let (rest, result) = expr(input2).unwrap();
assert_eq!(rest.len(), 0);
assert!(matches!(
result,
Expr::Comparison {
operator: Operator::NotEquals,
..
}
));
}
#[test]
fn test_parse_comparison_operators() {
let input1 = b"<esi:choose><esi:when test=\"$(count) < 10\">yes</esi:when></esi:choose>";
let bytes1 = Bytes::from_static(input1);
let result1 = parse_complete(&bytes1);
assert!(
result1.is_ok(),
"Should parse < operator: {:?}",
result1.err()
);
let input2 = b"<esi:choose><esi:when test=\"$(count) >= 5\">yes</esi:when></esi:choose>";
let bytes2 = Bytes::from_static(input2);
let result2 = parse_complete(&bytes2);
assert!(
result2.is_ok(),
"Should parse >= operator: {:?}",
result2.err()
);
let input3 = b"<esi:choose><esi:when test=\"$(USER_AGENT) has 'Mobile'\">yes</esi:when></esi:choose>";
let bytes3 = Bytes::from_static(input3);
let result3 = parse_complete(&bytes3);
assert!(
result3.is_ok(),
"Should parse 'has' operator: {:?}",
result3.err()
);
let input4 =
b"<esi:choose><esi:when test=\"$(COOKIE) has_i 'sam'\">yes</esi:when></esi:choose>";
let bytes4 = Bytes::from_static(input4);
let result4 = parse_complete(&bytes4);
assert!(
result4.is_ok(),
"Should parse 'has_i' operator: {:?}",
result4.err()
);
}
#[test]
fn test_parse_logical_operators() {
let input = b"($(foo) == 'bar') & ($(baz) == 'qux')";
let (rest, result) = expr(input).unwrap();
assert_eq!(rest.len(), 0);
assert!(matches!(
result,
Expr::Comparison {
operator: Operator::And,
..
}
));
let input2 = b"($(foo) == 'bar') | ($(baz) == 'qux')";
let (rest, result) = expr(input2).unwrap();
assert_eq!(rest.len(), 0);
assert!(matches!(
result,
Expr::Comparison {
operator: Operator::Or,
..
}
));
}
#[test]
fn test_parse_negation() {
let input = b"!$(flag)";
let (rest, result) = expr(input).unwrap();
assert_eq!(rest.len(), 0);
assert!(matches!(result, Expr::Not(_)));
let input2 = b"!($(foo) == 'bar')";
let (rest, result) = expr(input2).unwrap();
assert_eq!(rest.len(), 0);
assert!(matches!(result, Expr::Not(_)));
}
#[test]
fn test_parse_grouped_expressions() {
let input = b"($(foo) == 'bar')";
let (rest, result) = expr(input).unwrap();
assert_eq!(rest.len(), 0);
assert!(matches!(
result,
Expr::Comparison {
operator: Operator::Equals,
..
}
));
}
#[test]
fn test_single_quoted_attributes() {
let input = b"<esi:include src='http://example.com/fragment' />";
let bytes = Bytes::from_static(input);
let (rest, elements) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0, "Should parse completely");
assert_eq!(elements.len(), 1);
if let Element::Esi(Tag::Include { attrs, .. }) = &elements[0] {
assert!(
matches!(&attrs.src, Expr::String(Some(s)) if s == &Bytes::from("http://example.com/fragment"))
);
} else {
panic!("Expected Include tag");
}
let input2 = b"<esi:assign name='foo' value=\"bar\" />";
let bytes2 = Bytes::from_static(input2);
let (rest, elements) = parse_complete(&bytes2).unwrap();
assert_eq!(rest.len(), 0, "Should parse completely");
assert_eq!(elements.len(), 1);
if let Element::Esi(Tag::Assign {
name,
subscript: _,
value,
}) = &elements[0]
{
assert_eq!(name, "foo");
assert_eq!(value, &Expr::String(Some(Bytes::from("bar"))));
} else {
panic!("Expected Assign tag");
}
}
#[test]
fn test_assign_valid_variable_names() {
let valid_cases: Vec<&[u8]> = vec![
b"<esi:assign name=\"valid_name\" value=\"test\"/>",
b"<esi:assign name=\"a\" value=\"test\"/>",
b"<esi:assign name=\"Z\" value=\"test\"/>",
b"<esi:assign name=\"var123\" value=\"test\"/>",
b"<esi:assign name=\"my_var_123\" value=\"test\"/>",
b"<esi:assign name=\"CamelCase\" value=\"test\"/>",
];
for input in valid_cases {
let bytes = Bytes::copy_from_slice(input);
let result = parse_complete(&bytes);
assert!(
result.is_ok(),
"Should parse valid name: {:?}",
std::str::from_utf8(input)
);
let (_, elements) = result.unwrap();
let has_assign = elements
.iter()
.any(|e| matches!(e, Element::Esi(Tag::Assign { .. })));
assert!(
has_assign,
"Should have Assign tag for: {:?}",
std::str::from_utf8(input)
);
}
}
#[test]
fn test_assign_invalid_variable_names() {
let invalid_cases: Vec<&[u8]> = vec![
b"<esi:assign name=\"$invalid\" value=\"test\"/>", b"<esi:assign name=\"123invalid\" value=\"test\"/>", b"<esi:assign name=\"_invalid\" value=\"test\"/>", b"<esi:assign name=\"invalid-name\" value=\"test\"/>", b"<esi:assign name=\"invalid.name\" value=\"test\"/>", b"<esi:assign name=\"invalid name\" value=\"test\"/>", b"<esi:assign name=\"\" value=\"test\"/>", ];
for input in invalid_cases {
let bytes = Bytes::copy_from_slice(input);
let result = parse_complete(&bytes);
assert!(
result.is_ok(),
"Should parse (but skip invalid): {:?}",
std::str::from_utf8(input)
);
let (_, elements) = result.unwrap();
let has_assign = elements
.iter()
.any(|e| matches!(e, Element::Esi(Tag::Assign { .. })));
assert!(
!has_assign,
"Should NOT have Assign tag for invalid name: {:?}",
std::str::from_utf8(input)
);
}
}
#[test]
fn test_assign_name_length_limit() {
let valid_256 = format!(r#"<esi:assign name="a{}" value="test"/>"#, "b".repeat(255));
let bytes = Bytes::from(valid_256.clone());
let result = parse_complete(&bytes);
assert!(result.is_ok(), "Should parse 256 char name");
let (_, elements) = result.unwrap();
let has_assign = elements
.iter()
.any(|e| matches!(e, Element::Esi(Tag::Assign { .. })));
assert!(has_assign, "Should have Assign tag for 256 char name");
let invalid_257 = format!(r#"<esi:assign name="a{}" value="test"/>"#, "b".repeat(256));
let bytes = Bytes::from(invalid_257);
let result = parse_complete(&bytes);
assert!(result.is_ok(), "Should parse (but skip)");
let (_, elements) = result.unwrap();
let has_assign = elements
.iter()
.any(|e| matches!(e, Element::Esi(Tag::Assign { .. })));
assert!(!has_assign, "Should NOT have Assign tag for 257 char name");
}
#[test]
fn test_assign_long_form_invalid_name() {
let input = b"<esi:assign name=\"$invalid\">test value</esi:assign>";
let bytes = Bytes::copy_from_slice(input);
let result = parse_complete(&bytes);
assert!(result.is_ok(), "Should parse");
let (_, elements) = result.unwrap();
let has_assign = elements
.iter()
.any(|e| matches!(e, Element::Esi(Tag::Assign { .. })));
assert!(
!has_assign,
"Should NOT have Assign tag for invalid name in long form"
);
}
#[test]
fn test_assign_with_subscript() {
let input = b"<esi:assign name=\"ages{joan}\" value=\"28\"/>";
let bytes = Bytes::copy_from_slice(input);
let result = parse_complete(&bytes);
assert!(result.is_ok(), "Should parse");
let (_, elements) = result.unwrap();
assert_eq!(elements.len(), 1);
match &elements[0] {
Element::Esi(Tag::Assign {
name,
subscript,
value,
}) => {
assert_eq!(name, "ages");
assert!(subscript.is_some(), "Should have subscript");
if let Some(sub) = subscript {
assert!(matches!(sub, Expr::String(Some(s)) if s == &Bytes::from("joan")));
}
assert!(matches!(value, Expr::Integer(28)));
}
_ => panic!("Expected Assign tag"),
}
let input2 = b"<esi:assign name=\"ages{bob}\" value=\"34\"/>";
let bytes2 = Bytes::copy_from_slice(input2);
let result2 = parse_complete(&bytes2);
assert!(result2.is_ok(), "Should parse");
let (_, elements2) = result2.unwrap();
assert_eq!(elements2.len(), 1);
match &elements2[0] {
Element::Esi(Tag::Assign {
name,
subscript,
value,
}) => {
assert_eq!(name, "ages");
assert!(subscript.is_some(), "Should have subscript");
if let Some(sub) = subscript {
assert!(
matches!(sub, Expr::String(Some(s)) if s == &Bytes::from("bob")),
"Subscript should be 'bob', got {:?}",
sub
);
}
assert!(matches!(value, Expr::Integer(34)));
}
_ => panic!("Expected Assign tag"),
}
}
#[test]
fn test_assign_with_quoted_subscript() {
let input = b"<esi:assign name=\"ages{'joan'}\" value=\"28\"/>";
let bytes = Bytes::copy_from_slice(input);
let result = parse_complete(&bytes);
assert!(
result.is_ok(),
"Should parse spec-compliant quoted subscript"
);
let (_, elements) = result.unwrap();
assert_eq!(elements.len(), 1, "Should have exactly 1 element");
match &elements[0] {
Element::Esi(Tag::Assign {
name,
subscript,
value,
}) => {
assert_eq!(name, "ages");
assert!(subscript.is_some(), "Should have subscript");
if let Some(sub) = subscript {
assert!(
matches!(sub, Expr::String(Some(s)) if s == "joan"),
"Subscript should be 'joan', got {:?}",
sub
);
}
assert!(matches!(value, Expr::Integer(28)));
}
other => panic!("Expected Assign tag, got {:?}", other),
}
let input2 = b"<esi:assign name=\"data{'key1'}\" value=\"${'value1'}\"/>";
let bytes2 = Bytes::copy_from_slice(input2);
let result2 = parse_complete(&bytes2);
assert!(
result2.is_ok(),
"Should parse assignment with quoted subscript and quoted value"
);
}
#[test]
fn test_unclosed_script_tag() {
let input = b"<script>content without closing";
let bytes = Bytes::from_static(input);
let (rest, elements) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0, "Should consume all input");
assert_eq!(elements.len(), 1);
assert!(matches!(&elements[0], Element::Content(_)));
}
#[test]
fn test_partial_esi_tag() {
let input = b"<esi:inclu";
let bytes = Bytes::from_static(input);
let result = parse(&bytes);
assert!(matches!(result, Err(nom::Err::Incomplete(_))));
}
#[test]
fn test_partial_esi_tag_with_prefix() {
let input = b"hello <esi:inclu";
let bytes = Bytes::from_static(input);
let result = parse(&bytes);
match result {
Ok((rest, elements)) => {
assert_eq!(elements.len(), 1);
assert!(matches!(&elements[0], Element::Content(t) if t.as_ref() == b"hello "));
assert_eq!(rest, b"<esi:inclu");
}
Err(nom::Err::Incomplete(_)) => {
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[test]
fn test_html_comment() {
let input = b"<!-- this is a comment -->";
let bytes = Bytes::from_static(input);
let (rest, elements) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0);
assert_eq!(elements.len(), 1);
assert!(matches!(
&elements[0],
Element::Html(h) if h.as_ref() == b"<!-- this is a comment -->"
));
}
#[test]
fn test_parse_foreach() {
let input = b"<esi:foreach collection=\"$(items)\" item=\"x\">Item: $(x)</esi:foreach>";
let bytes = Bytes::from_static(input);
let (rest, elements) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0);
assert_eq!(elements.len(), 1);
match &elements[0] {
Element::Esi(Tag::Foreach {
collection,
item,
content,
}) => {
assert!(matches!(collection, Expr::Variable(name, None, None) if name == "items"));
assert_eq!(item.as_deref(), Some("x"));
assert!(!content.is_empty());
}
other => panic!("Expected Foreach tag, got {:?}", other),
}
}
#[test]
fn test_parse_foreach_no_item() {
let input = b"<esi:foreach collection=\"$(mylist)\">Value: $(item)</esi:foreach>";
let bytes = Bytes::from_static(input);
let (rest, elements) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0);
assert_eq!(elements.len(), 1);
match &elements[0] {
Element::Esi(Tag::Foreach {
collection,
item,
content,
}) => {
assert!(matches!(collection, Expr::Variable(name, None, None) if name == "mylist"));
assert_eq!(item, &None);
assert!(!content.is_empty());
}
other => panic!("Expected Foreach tag, got {:?}", other),
}
}
#[test]
fn test_parse_break() {
let input = b"<esi:break />";
let bytes = Bytes::from_static(input);
let (rest, elements) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0);
assert_eq!(elements.len(), 1);
assert!(matches!(&elements[0], Element::Esi(Tag::Break)));
}
#[test]
fn test_parse_foreach_with_break() {
let input = b"<esi:foreach collection=\"$(items)\"><esi:break /></esi:foreach>";
let bytes = Bytes::from_static(input);
let (rest, elements) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0);
assert_eq!(elements.len(), 1);
match &elements[0] {
Element::Esi(Tag::Foreach {
collection,
content,
..
}) => {
assert!(matches!(collection, Expr::Variable(name, None, None) if name == "items"));
assert_eq!(content.len(), 1);
assert!(matches!(&content[0], Element::Esi(Tag::Break)));
}
other => panic!("Expected Foreach tag, got {:?}", other),
}
}
#[test]
fn test_parse_function() {
let input = b"<esi:function name=\"greet\">Hello $(name)</esi:function>";
let bytes = Bytes::from_static(input);
let (rest, elements) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0);
assert_eq!(elements.len(), 1);
match &elements[0] {
Element::Esi(Tag::Function { name, body }) => {
assert_eq!(name, "greet");
assert!(!body.is_empty());
}
other => panic!("Expected Function tag, got {:?}", other),
}
}
#[test]
fn test_parse_function_with_return() {
let input =
b"<esi:function name=\"add\"><esi:return value=\"$(a) + $(b)\" /></esi:function>";
let bytes = Bytes::from_static(input);
let (rest, elements) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0);
assert_eq!(elements.len(), 1);
match &elements[0] {
Element::Esi(Tag::Function { name, body }) => {
assert_eq!(name, "add");
assert_eq!(body.len(), 1);
match &body[0] {
Element::Esi(Tag::Return { value }) => {
assert!(matches!(value, Expr::Comparison { .. }));
}
other => panic!("Expected Return tag in function body, got {:?}", other),
}
}
other => panic!("Expected Function tag, got {:?}", other),
}
}
#[test]
fn test_parse_return() {
let input = b"<esi:return value=\"42\" />";
let bytes = Bytes::from_static(input);
let (rest, elements) = parse_complete(&bytes).unwrap();
assert_eq!(rest.len(), 0);
assert_eq!(elements.len(), 1);
match &elements[0] {
Element::Esi(Tag::Return { value }) => {
assert!(matches!(value, Expr::Integer(42)));
}
other => panic!("Expected Return tag, got {:?}", other),
}
}
#[test]
fn test_parse_dict_literal() {
let input = b"{1:'apple',2:'orange'}";
let result = dict_literal(input);
assert!(result.is_ok(), "Dict literal should parse: {:?}", result);
let (rest, expr) = result.unwrap();
assert_eq!(rest, b"");
assert!(matches!(expr, Expr::DictLiteral(_)));
}
#[test]
fn test_left_to_right_evaluation() {
let input = b"$(a) & $(b) | $(c)";
let result = expr(input);
assert!(
result.is_ok(),
"Failed to parse '$(a) & $(b) | $(c)': {:?}",
result
);
let (rest, parsed) = result.unwrap();
assert_eq!(rest, b"");
match parsed {
Expr::Comparison {
operator: Operator::Or,
left,
right,
} => {
match *left {
Expr::Comparison {
operator: Operator::And,
..
} => {}
_ => panic!("Expected AND on left side, got {:?}", left),
}
match *right {
Expr::Variable(name, None, None) if name == "c" => {}
_ => panic!("Expected variable 'c' on right side, got {:?}", right),
}
}
_ => panic!("Expected OR at top level, got {:?}", parsed),
}
let input = b"$(a) | $(b) & $(c)";
let result = expr(input);
assert!(result.is_ok(), "Failed to parse '$(a) | $(b) & $(c)'");
let (rest, parsed) = result.unwrap();
assert_eq!(rest, b"");
match parsed {
Expr::Comparison {
operator: Operator::And,
left,
right,
} => {
match *left {
Expr::Comparison {
operator: Operator::Or,
..
} => {}
_ => panic!("Expected OR on left side, got {:?}", left),
}
match *right {
Expr::Variable(name, None, None) if name == "c" => {}
_ => panic!("Expected variable 'c' on right side, got {:?}", right),
}
}
_ => panic!("Expected AND at top level, got {:?}", parsed),
}
let input = b"!$(a) & $(b)";
let result = expr(input);
assert!(result.is_ok(), "Failed to parse '!$(a) & $(b)'");
let (rest, parsed) = result.unwrap();
assert_eq!(rest, b"");
match parsed {
Expr::Comparison {
operator: Operator::And,
left,
right,
} => {
match *left {
Expr::Not(_) => {}
_ => panic!("Expected NOT on left side, got {:?}", left),
}
match *right {
Expr::Variable(name, None, None) if name == "b" => {}
_ => panic!("Expected variable 'b' on right side, got {:?}", right),
}
}
_ => panic!("Expected AND at top level, got {:?}", parsed),
}
let input = b"$(a) == $(b) | $(c)";
let result = expr(input);
assert!(result.is_ok(), "Failed to parse '$(a) == $(b) | $(c)'");
let (rest, parsed) = result.unwrap();
assert_eq!(rest, b"");
match parsed {
Expr::Comparison {
operator: Operator::Or,
left,
right,
} => {
match *left {
Expr::Comparison {
operator: Operator::Equals,
..
} => {}
_ => panic!("Expected EQUALS on left side, got {:?}", left),
}
match *right {
Expr::Variable(name, None, None) if name == "c" => {}
_ => panic!("Expected variable 'c' on right side, got {:?}", right),
}
}
_ => panic!("Expected OR at top level, got {:?}", parsed),
}
let input = b"$(a) & ($(b) | $(c))";
let result = expr(input);
assert!(result.is_ok(), "Failed to parse '$(a) & ($(b) | $(c))'");
let (rest, parsed) = result.unwrap();
assert_eq!(rest, b"");
match parsed {
Expr::Comparison {
operator: Operator::And,
left,
right,
} => {
match *left {
Expr::Variable(name, None, None) if name == "a" => {}
_ => panic!("Expected variable 'a' on left side, got {:?}", left),
}
match *right {
Expr::Comparison {
operator: Operator::Or,
..
} => {}
_ => panic!("Expected OR on right side, got {:?}", right),
}
}
_ => panic!("Expected AND at top level, got {:?}", parsed),
}
}
#[test]
fn test_arithmetic_left_to_right() {
let input = b"2 + 3 * 4";
let result = expr(input);
assert!(result.is_ok(), "Failed to parse '2 + 3 * 4': {:?}", result);
let (rest, parsed) = result.unwrap();
assert_eq!(rest, b"");
match parsed {
Expr::Comparison {
operator: Operator::Multiply,
left,
right,
} => {
match *left {
Expr::Comparison {
operator: Operator::Add,
..
} => {}
_ => panic!("Expected ADD on left side, got {:?}", left),
}
match *right {
Expr::Integer(4) => {}
_ => panic!("Expected integer 4 on right side, got {:?}", right),
}
}
_ => panic!("Expected MULTIPLY at top level, got {:?}", parsed),
}
let input = b"10 - 2 / 2";
let result = expr(input);
assert!(result.is_ok(), "Failed to parse '10 - 2 / 2'");
let (rest, parsed) = result.unwrap();
assert_eq!(rest, b"");
match parsed {
Expr::Comparison {
operator: Operator::Divide,
left,
right,
} => {
match *left {
Expr::Comparison {
operator: Operator::Subtract,
..
} => {}
_ => panic!("Expected SUBTRACT on left side, got {:?}", left),
}
match *right {
Expr::Integer(2) => {}
_ => panic!("Expected integer 2 on right side, got {:?}", right),
}
}
_ => panic!("Expected DIVIDE at top level, got {:?}", parsed),
}
let input = b"7 + 3 % 2";
let result = expr(input);
assert!(result.is_ok(), "Failed to parse '7 + 3 % 2'");
let (rest, parsed) = result.unwrap();
assert_eq!(rest, b"");
match parsed {
Expr::Comparison {
operator: Operator::Modulo,
..
} => {}
_ => panic!("Expected MODULO at top level, got {:?}", parsed),
}
let input = b"2 + (3 * 4)";
let result = expr(input);
assert!(result.is_ok(), "Failed to parse '2 + (3 * 4)'");
let (rest, parsed) = result.unwrap();
assert_eq!(rest, b"");
match parsed {
Expr::Comparison {
operator: Operator::Add,
left,
right,
} => {
match *left {
Expr::Integer(2) => {}
_ => panic!("Expected integer 2 on left side, got {:?}", left),
}
match *right {
Expr::Comparison {
operator: Operator::Multiply,
..
} => {}
_ => panic!("Expected MULTIPLY on right side, got {:?}", right),
}
}
_ => panic!("Expected ADD at top level, got {:?}", parsed),
}
let input = b"2 + (3 * 4)";
let result = expr(input);
assert!(result.is_ok(), "Failed to parse '2 + (3 * 4)'");
let (rest, parsed) = result.unwrap();
assert_eq!(rest, b"");
match parsed {
Expr::Comparison {
operator: Operator::Add,
left,
right,
} => {
match *left {
Expr::Integer(2) => {}
_ => panic!("Expected integer 2 on left side, got {:?}", left),
}
match *right {
Expr::Comparison {
operator: Operator::Multiply,
..
} => {}
_ => panic!("Expected MULTIPLY on right side, got {:?}", right),
}
}
_ => panic!("Expected ADD at top level, got {:?}", parsed),
}
let input = b"5 + 3 > 7";
let result = expr(input);
assert!(result.is_ok(), "Failed to parse '5 + 3 > 7'");
let (rest, parsed) = result.unwrap();
assert_eq!(rest, b"");
match parsed {
Expr::Comparison {
operator: Operator::GreaterThan,
left,
right,
} => {
match *left {
Expr::Comparison {
operator: Operator::Add,
..
} => {}
_ => panic!("Expected ADD on left side, got {:?}", left),
}
match *right {
Expr::Integer(7) => {}
_ => panic!("Expected integer 7 on right side, got {:?}", right),
}
}
_ => panic!("Expected GREATER_THAN at top level, got {:?}", parsed),
}
}
#[test]
fn test_single_quoted_string_escape_quote() {
let input = br"'it\'s'";
let (rest, result) = single_quoted_string(input).unwrap();
assert!(rest.is_empty());
assert_eq!(result.as_ref(), b"it's");
}
#[test]
fn test_single_quoted_string_escape_backslash() {
let input = br"'a\\b'";
let (rest, result) = single_quoted_string(input).unwrap();
assert!(rest.is_empty());
assert_eq!(result.as_ref(), b"a\\b");
}
#[test]
fn test_single_quoted_string_escape_arbitrary() {
let input = br"'a\nb'";
let (rest, result) = single_quoted_string(input).unwrap();
assert!(rest.is_empty());
assert_eq!(result.as_ref(), b"anb");
}
#[test]
fn test_single_quoted_string_no_escapes() {
let input = b"'hello'";
let (rest, result) = single_quoted_string(input).unwrap();
assert!(rest.is_empty());
assert_eq!(result.as_ref(), b"hello");
}
#[test]
fn test_interpolated_content_escape() {
let input_bytes = Bytes::from_static(br"hello\<world");
let (rest, elements) = interpolated_content(&input_bytes).unwrap();
assert!(
rest.is_empty(),
"remaining: {:?}",
String::from_utf8_lossy(rest)
);
let text: Vec<u8> = elements
.iter()
.filter_map(|e| match e {
Element::Content(b) => Some(b.as_ref().to_vec()),
_ => None,
})
.flatten()
.collect();
assert_eq!(text, b"hello<world");
}
#[test]
fn test_interpolated_content_escape_backslash() {
let input_bytes = Bytes::from_static(br"a\\b");
let (rest, elements) = interpolated_content(&input_bytes).unwrap();
assert!(rest.is_empty());
let text: Vec<u8> = elements
.iter()
.filter_map(|e| match e {
Element::Content(b) => Some(b.as_ref().to_vec()),
_ => None,
})
.flatten()
.collect();
assert_eq!(text, b"a\\b");
}
#[test]
fn test_interpolated_content_escape_dollar() {
let input_bytes = Bytes::from_static(br"\$notavar");
let (rest, elements) = interpolated_content(&input_bytes).unwrap();
assert!(
rest.is_empty(),
"remaining: {:?}",
String::from_utf8_lossy(rest)
);
let text: Vec<u8> = elements
.iter()
.filter_map(|e| match e {
Element::Content(b) => Some(b.as_ref().to_vec()),
_ => None,
})
.flatten()
.collect();
assert_eq!(text, b"$notavar");
}
#[test]
fn test_parse_content_complete_backslash_escape() {
let input_bytes = Bytes::from_static(br"hello\$world");
let elements = parse_content_complete(&input_bytes, input_bytes.as_ref());
let text: Vec<u8> = elements
.iter()
.filter_map(|e| match e {
Element::Content(b) => Some(b.as_ref().to_vec()),
_ => None,
})
.flatten()
.collect();
assert_eq!(text, b"hello$world");
}
}