use std::borrow::Cow;
pub(crate) const OUTPUT_TAG: &str = "__archival_out";
pub(crate) struct Rewrite<'a> {
pub text: Cow<'a, str>,
#[cfg(feature = "lsp")]
pub anchors: Anchors,
}
impl<'a> Rewrite<'a> {
fn new(text: Cow<'a, str>, _anchors: Anchors) -> Self {
Self {
text,
#[cfg(feature = "lsp")]
anchors: _anchors,
}
}
}
#[derive(Default)]
pub(crate) struct Anchors {
#[cfg(feature = "lsp")]
map: Vec<(u32, u32)>,
#[cfg(feature = "lsp")]
source_len: u32,
}
impl Anchors {
fn new(_source_len: usize) -> Self {
Self {
#[cfg(feature = "lsp")]
map: Vec::new(),
#[cfg(feature = "lsp")]
source_len: _source_len as u32,
}
}
#[inline]
fn record(&mut self, _rewritten: usize, _source: usize) {
#[cfg(feature = "lsp")]
self.map.push((_rewritten as u32, _source as u32));
}
}
#[cfg(feature = "lsp")]
impl Anchors {
pub fn to_source(&self, offset: usize) -> usize {
let source_len = self.source_len as usize;
let Some(governing) = self
.map
.partition_point(|(at, _)| *at as usize <= offset)
.checked_sub(1)
else {
return self
.map
.first()
.map_or(offset, |(_, at)| *at as usize)
.min(source_len);
};
let (from, to) = self.map[governing];
let limit = self
.map
.get(governing + 1)
.map_or(source_len, |(_, at)| *at as usize);
(to as usize + (offset - from as usize)).min(limit)
}
fn is_empty(&self) -> bool {
self.map.is_empty()
}
}
pub(crate) fn rewrite_template(source: &str) -> Cow<'_, str> {
rewrite_template_mapped(source).text
}
pub(crate) fn rewrite_template_mapped(source: &str) -> Rewrite<'_> {
let borrowed = |anchors| Rewrite::new(Cow::Borrowed(source), anchors);
if !source.contains("{{") && !source.contains("{%") {
return borrowed(Anchors::new(source.len()));
}
let mut anchors = Anchors::new(source.len());
let bytes = source.as_bytes();
let len = bytes.len();
let mut out = String::with_capacity(len + 16);
let mut copied = 0;
let mut i = 0;
while i + 1 < len {
if bytes[i] != b'{' {
i += 1;
continue;
}
match bytes[i + 1] {
b'%' => {
let name = tag_name(bytes, i + 2);
let expandable = name == b"liquid" || name == b"echo";
if name == b"raw" {
i = skip_raw_block(bytes, i + 2);
} else if is_inline_comment(bytes, i + 2) || expandable {
let body_start = tag_body_start(bytes, i + 2) + name.len();
let close = match name {
b"liquid" => find_liquid_tag_end(bytes, body_start),
b"echo" => scan_to(bytes, body_start, *b"%}").map(|(at, _)| at),
_ => find_tag_end(bytes, i + 2),
};
let Some(close_at) = close else {
i += 2;
continue;
};
let dash_open = bytes.get(i + 2) == Some(&b'-');
let dash_close = bytes[close_at - 1] == b'-';
let before = &source[copied..i];
anchors.record(out.len(), copied);
out.push_str(if dash_open {
before.trim_end_matches(is_liquid_whitespace)
} else {
before
});
if expandable {
let body_end = if dash_close { close_at - 1 } else { close_at };
let body = &source[body_start..body_end];
if name == b"liquid" {
expand_statements(body, body_start, &mut out, &mut anchors);
} else {
let at = body_start + leading_whitespace(body);
expand_echo(body.trim(), at, &mut out, &mut anchors);
}
}
let mut next = close_at + 2;
if dash_close {
let rest = &source[next..];
next += rest.len() - rest.trim_start_matches(is_liquid_whitespace).len();
}
copied = next;
i = next;
} else {
i = scan_to(bytes, i + 2, *b"%}").map_or(i + 2, |(_, end)| end);
}
}
b'{' => {
let dash_open = bytes.get(i + 2) == Some(&b'-');
let inner_start = i + 2 + usize::from(dash_open);
let Some((close_at, close_end)) = scan_to(bytes, inner_start, *b"}}") else {
break;
};
let dash_close = close_at > inner_start && bytes[close_at - 1] == b'-';
let inner_end = if dash_close { close_at - 1 } else { close_at };
let raw_inner = &source[inner_start..inner_end];
let inner = raw_inner.trim_matches(is_liquid_whitespace);
if inner.is_empty() {
i = close_end;
continue;
}
anchors.record(out.len(), copied);
out.push_str(&source[copied..i]);
out.push_str(if dash_open { "{%- " } else { "{% " });
out.push_str(OUTPUT_TAG);
out.push(' ');
let inner_at = inner_start
+ (raw_inner.len() - raw_inner.trim_start_matches(is_liquid_whitespace).len());
anchors.record(out.len(), inner_at);
out.push_str(inner);
out.push_str(if dash_close { " -%}" } else { " %}" });
copied = close_end;
i = close_end;
}
_ => i += 1,
}
}
if copied == 0 {
#[cfg(feature = "lsp")]
debug_assert!(
anchors.is_empty(),
"nothing was rewritten, so nothing to map"
);
return borrowed(anchors);
}
anchors.record(out.len(), copied);
out.push_str(&source[copied..]);
Rewrite::new(Cow::Owned(out), anchors)
}
fn leading_whitespace(s: &str) -> usize {
s.len() - s.trim_start().len()
}
pub(crate) fn scan_to(bytes: &[u8], from: usize, close: [u8; 2]) -> Option<(usize, usize)> {
let mut i = from;
while i < bytes.len() {
match bytes[i] {
quote @ (b'\'' | b'"') => {
i += 1;
loop {
if i >= bytes.len() {
return None;
}
let closed = bytes[i] == quote;
i += 1;
if closed {
break;
}
}
}
b if b == close[0] && bytes.get(i + 1) == Some(&close[1]) => return Some((i, i + 2)),
_ => i += 1,
}
}
None
}
pub(crate) fn is_liquid_whitespace(c: char) -> bool {
c == ' ' || c == '\n' || c == '\r'
}
fn expand_statements(body: &str, body_start: usize, out: &mut String, anchors: &mut Anchors) {
let mut at = body_start;
for line in body.split_inclusive('\n') {
expand_statement(line, at, out, anchors);
at += line.len();
}
}
fn expand_statement(line: &str, line_start: usize, out: &mut String, anchors: &mut Anchors) {
let statement = line.trim();
if statement.is_empty() || statement.starts_with('#') {
return;
}
let at = line_start + leading_whitespace(line);
match echo_argument(statement) {
Some(expression) => expand_echo(
expression,
at + (statement.len() - expression.len()),
out,
anchors,
),
None => {
out.push_str("{% ");
anchors.record(out.len(), at);
out.push_str(statement);
out.push_str(" %}");
}
}
}
fn echo_argument(statement: &str) -> Option<&str> {
let rest = statement.strip_prefix("echo")?;
(rest.is_empty() || rest.starts_with(|c: char| c.is_ascii_whitespace())).then(|| rest.trim())
}
fn expand_echo(expression: &str, at: usize, out: &mut String, anchors: &mut Anchors) {
if expression.is_empty() {
return;
}
out.push_str("{% ");
out.push_str(OUTPUT_TAG);
out.push(' ');
anchors.record(out.len(), at);
out.push_str(expression);
out.push_str(" %}");
}
pub(crate) fn find_liquid_tag_end(bytes: &[u8], from: usize) -> Option<usize> {
let mut i = from;
while i < bytes.len() {
while matches!(bytes.get(i), Some(b' ' | b'\t' | b'\r')) {
i += 1;
}
let comment = bytes.get(i) == Some(&b'#');
while i < bytes.len() {
match bytes[i] {
b'\n' => {
i += 1;
break;
}
quote @ (b'\'' | b'"') if !comment => {
i += 1;
while i < bytes.len() && bytes[i] != quote {
i += 1;
}
if i >= bytes.len() {
return None;
}
i += 1;
}
b'%' if bytes.get(i + 1) == Some(&b'}') => return Some(i),
_ => i += 1,
}
}
}
None
}
pub(crate) fn find_tag_end(bytes: &[u8], from: usize) -> Option<usize> {
(from..bytes.len().saturating_sub(1)).find(|&i| bytes[i] == b'%' && bytes[i + 1] == b'}')
}
pub(crate) fn tag_body_start(bytes: &[u8], from: usize) -> usize {
let mut i = from;
if bytes.get(i) == Some(&b'-') {
i += 1;
}
while matches!(bytes.get(i), Some(b' ' | b'\n' | b'\r')) {
i += 1;
}
i
}
pub(crate) fn is_inline_comment(bytes: &[u8], from: usize) -> bool {
bytes.get(tag_body_start(bytes, from)) == Some(&b'#')
}
pub(crate) fn tag_name(bytes: &[u8], from: usize) -> &[u8] {
let mut i = tag_body_start(bytes, from);
let start = i;
while matches!(bytes.get(i), Some(b) if b.is_ascii_alphanumeric() || *b == b'_' || *b == b'-') {
i += 1;
}
&bytes[start..i]
}
pub(crate) fn skip_raw_block(bytes: &[u8], from: usize) -> usize {
let mut i = scan_to(bytes, from, *b"%}").map_or(from, |(_, end)| end);
while i + 1 < bytes.len() {
if bytes[i] != b'{' || bytes[i + 1] != b'%' {
i += 1;
continue;
}
let name = tag_name(bytes, i + 2);
match scan_to(bytes, i + 2, *b"%}") {
Some((_, end)) => {
i = end;
if name == b"endraw" {
return i;
}
}
None => i += 2,
}
}
bytes.len()
}
#[cfg(test)]
mod tests {
use super::*;
fn rewrite(source: &str) -> String {
rewrite_template(source).into_owned()
}
fn unchanged(source: &str) {
assert!(
matches!(rewrite_template(source), Cow::Borrowed(_)),
"expected {source:?} to be left alone, got {:?}",
rewrite(source)
);
}
#[test]
fn rewrites_output_statements() {
assert_eq!(rewrite("{{x}}"), "{% __archival_out x %}");
assert_eq!(rewrite("{{ x }}"), "{% __archival_out x %}");
assert_eq!(rewrite("a{{ x }}b"), "a{% __archival_out x %}b");
assert_eq!(
rewrite("{{a}}{{b}}{{c}}"),
"{% __archival_out a %}{% __archival_out b %}{% __archival_out c %}"
);
assert_eq!(
rewrite("{{ x | append: 'y' }}"),
"{% __archival_out x | append: 'y' %}"
);
assert_eq!(rewrite("{{ x }}}"), "{% __archival_out x %}}");
}
#[test]
fn preserves_whitespace_control() {
assert_eq!(rewrite("{{- x -}}"), "{%- __archival_out x -%}");
assert_eq!(rewrite("{{-x-}}"), "{%- __archival_out x -%}");
assert_eq!(rewrite("{{- x }}"), "{%- __archival_out x %}");
assert_eq!(rewrite("{{ x -}}"), "{% __archival_out x -%}");
assert_eq!(rewrite("a\n {{- x }}b"), "a\n {%- __archival_out x %}b");
}
#[test]
fn respects_string_literals() {
assert_eq!(
rewrite(r#"{{ x | append: "}}" }}"#),
r#"{% __archival_out x | append: "}}" %}"#
);
assert_eq!(
rewrite("{{ x | append: '{%' }}"),
"{% __archival_out x | append: '{%' %}"
);
assert_eq!(
rewrite(r#"{% assign a = "{{" %}{{ a }}"#),
r#"{% assign a = "{{" %}{% __archival_out a %}"#
);
assert_eq!(
rewrite(r#"{% assign a = "%}" %}{{ a }}"#),
r#"{% assign a = "%}" %}{% __archival_out a %}"#
);
}
#[test]
fn strips_inline_comments() {
assert_eq!(rewrite("{% # comment %}"), "");
assert_eq!(rewrite("{%# comment %}"), "");
assert_eq!(rewrite("{% #comment %}"), "");
assert_eq!(rewrite("{% # %}"), "");
assert_eq!(rewrite("a{% # c %}b"), "ab");
assert_eq!(rewrite("{% # a %}{% # b %}"), "");
assert_eq!(rewrite("{%\n # multi\n # line\n%}"), "");
assert_eq!(
rewrite("{% # prettier-ignore %}{{ x }}"),
"{% __archival_out x %}"
);
assert_eq!(rewrite("a{% # it's fine %}b"), "ab");
assert_eq!(rewrite(r#"a{% # "unclosed %}b"#), "ab");
assert_eq!(rewrite("a{% # {{ x }} %}b"), "ab");
}
#[test]
fn inline_comments_preserve_whitespace_control() {
assert_eq!(rewrite("a\n {%- # c %}\nb"), "a\nb");
assert_eq!(rewrite("a\n{% # c -%}\n b"), "a\nb");
assert_eq!(rewrite("a {%- # c -%} b"), "ab");
assert_eq!(rewrite("a {%-# c-%} b"), "ab");
assert_eq!(rewrite("a {% # c %} b"), "a b");
}
#[test]
fn leaves_raw_blocks_alone() {
unchanged("{% raw %}{% # c %}{% endraw %}");
unchanged("{% raw %}{{ x }}{% endraw %}");
unchanged("{%- raw -%}{{ x }}{%- endraw -%}");
unchanged("{%-raw%}{{x}}{%-endraw%}");
unchanged("{% raw %}{{ x }}");
assert_eq!(
rewrite("{% raw %}{{ x }}{% endraw %}{{ y }}"),
"{% raw %}{{ x }}{% endraw %}{% __archival_out y %}"
);
assert_eq!(
rewrite("{% raw %}{{ unclosed{% endraw %}{{ y }}"),
"{% raw %}{{ unclosed{% endraw %}{% __archival_out y %}"
);
}
#[test]
fn rewrites_inside_comments() {
assert_eq!(
rewrite("{% comment %}{% # c %}{% endcomment %}ok"),
"{% comment %}{% endcomment %}ok"
);
assert_eq!(
rewrite("{% comment %}{{ x }}{% endcomment %}"),
"{% comment %}{% __archival_out x %}{% endcomment %}"
);
}
#[test]
fn leaves_malformed_input_alone() {
unchanged("hello");
unchanged("{{ x");
unchanged(r#"{{ "abc }}"#);
unchanged("{{}}");
unchanged("{{ }}");
unchanged("{{-}}");
unchanged("{% assign x = 1 %}");
unchanged("{% # c");
}
#[test]
fn rewrites_inside_markdown_code_fences() {
assert_eq!(
rewrite("```\n{{ x }}\n```"),
"```\n{% __archival_out x %}\n```"
);
}
#[test]
fn expands_the_liquid_tag() {
assert_eq!(rewrite("{% liquid assign a = 1 %}"), "{% assign a = 1 %}");
assert_eq!(
rewrite("{% liquid\n assign a = 1\n echo a\n%}"),
"{% assign a = 1 %}{% __archival_out a %}"
);
assert_eq!(
rewrite("{% liquid\n\n # note\n echo 'x'\n%}"),
"{% __archival_out 'x' %}"
);
assert_eq!(rewrite("{% liquid\n\techo a\n%}"), "{% __archival_out a %}");
assert_eq!(
rewrite("{% echo a | upcase %}"),
"{% __archival_out a | upcase %}"
);
assert_eq!(rewrite("{% liquid %}"), "");
assert_eq!(rewrite("{% echo %}"), "");
}
#[test]
fn liquid_tag_preserves_whitespace_control() {
assert_eq!(
rewrite("a {%- liquid echo b -%} c"),
"a{% __archival_out b %}c"
);
assert_eq!(
rewrite("a {%- liquid\n echo b\n-%} c"),
"a{% __archival_out b %}c"
);
}
#[test]
fn liquid_tag_comments_are_text() {
assert_eq!(
rewrite("{% liquid # it's a note\n echo a\n%}"),
"{% __archival_out a %}"
);
assert_eq!(
rewrite("{% liquid\n # don't\n echo a\n%}"),
"{% __archival_out a %}"
);
}
#[test]
fn liquid_tag_respects_string_literals() {
assert_eq!(
rewrite("{% liquid assign a = '%}' %}"),
"{% assign a = '%}' %}"
);
}
#[test]
fn is_idempotent() {
let sources = CORPUS
.iter()
.copied()
.chain(COMMENT_CORPUS.iter().map(|(source, _)| *source))
.chain(LIQUID_TAG_CORPUS.iter().map(|(source, _)| *source));
for source in sources {
let once = rewrite(source);
assert_eq!(rewrite(&once), once, "not idempotent: {source:?}");
}
}
#[cfg(feature = "lsp")]
fn map_back<'a>(source: &'a str, needle: &str) -> &'a str {
let rewritten = rewrite_template_mapped(source);
let at = rewritten
.text
.find(needle)
.unwrap_or_else(|| panic!("{needle:?} is not in {:?}", rewritten.text));
&source[rewritten.anchors.to_source(at)..]
}
#[cfg(feature = "lsp")]
fn line_of(source: &str, needle: &str) -> usize {
let rewritten = rewrite_template_mapped(source);
let at = rewritten
.anchors
.to_source(rewritten.text.find(needle).unwrap());
source[..at].matches('\n').count() + 1
}
#[cfg(feature = "lsp")]
#[test]
fn maps_rewritten_offsets_back_to_source() {
assert!(map_back("a {{ x }} b", "x %}").starts_with("x }} b"));
assert!(map_back("{{- x -}}", "x -%}").starts_with("x -}}"));
assert!(map_back("{{a}}{{b}}{{c}}", "c %}").starts_with("c}}"));
assert!(map_back("{{ x }}tail", "tail").starts_with("tail"));
assert!(map_back("plain text", "text").starts_with("text"));
}
#[cfg(feature = "lsp")]
#[test]
fn maps_liquid_statements_back_to_their_own_line() {
let source = "{% liquid\n assign one = 1\n echo two\n%}\n{{ three }}\n{{ four }}";
assert_eq!(line_of(source, "assign one = 1"), 2);
assert_eq!(line_of(source, "two %}"), 3);
assert_eq!(line_of(source, "three %}"), 5);
assert_eq!(line_of(source, "four %}"), 6);
}
#[cfg(feature = "lsp")]
#[test]
fn maps_around_removed_and_expanded_regions() {
assert_eq!(line_of("{% # note %}\n{{ x }}", "x %}"), 2);
assert_eq!(line_of("{% echo a %}\n{{ b }}", "b %}"), 2);
assert!(map_back("{% echo a | upcase %}", "a | upcase").starts_with("a | upcase %}"));
}
#[cfg(feature = "lsp")]
#[test]
fn maps_every_offset_into_the_source() {
let sources = CORPUS
.iter()
.copied()
.chain(COMMENT_CORPUS.iter().map(|(source, _)| *source))
.chain(LIQUID_TAG_CORPUS.iter().map(|(source, _)| *source))
.chain(["héllo {{ wörld }} 🎉", "{% liquid\n echo é\n%}ü"]);
for source in sources {
let rewritten = rewrite_template_mapped(source);
let mut last = 0;
for offset in 0..=rewritten.text.len() {
if !rewritten.text.is_char_boundary(offset) {
continue;
}
let at = rewritten.anchors.to_source(offset);
assert!(
at <= source.len() && source.is_char_boundary(at),
"{source:?}: offset {offset} mapped to {at}, not a boundary"
);
assert!(at >= last, "{source:?}: mapping went backwards at {offset}");
last = at;
}
}
}
const COMMENT_CORPUS: &[(&str, &str)] = &[
("{% # c %}", ""),
("{%# c %}", ""),
("{% # %}", ""),
("a{% # c %}b", "ab"),
("a {%- # c -%} b", "ab"),
("{% # it's a comment %}", ""),
(r#"{% # a "quoted" comment %}"#, ""),
("{% # {{ name }} %}", ""),
("{% # if name %}", ""),
("{%\n # multi\n # line\n%}", ""),
("{% # prettier-ignore %}{{ name }}", "Archival"),
("{% if name %}{% # c %}{{ name }}{% endif %}", "Archival"),
("{% for i in list %}{% # c %}{{ i }}{% endfor %}", "onetwo"),
];
const LIQUID_TAG_CORPUS: &[(&str, &str)] = &[
("{% liquid echo name %}", "Archival"),
("{% liquid\n echo name\n%}", "Archival"),
("{% liquid\n assign a = name\n echo a\n%}", "Archival"),
("{% liquid\n echo name | upcase\n%}", "ARCHIVAL"),
("{% echo name %}", "Archival"),
("{% echo name | upcase %}", "ARCHIVAL"),
("{% echo %}", ""),
("{% liquid %}", ""),
("{% liquid\n # only a comment\n%}", ""),
(
"{% liquid\n if name\n echo name\n else\n echo 'none'\n endif\n%}",
"Archival",
),
(
"{% liquid\n for i in list\n echo i\n endfor\n%}",
"onetwo",
),
(
"{% liquid\n case name\n when 'Archival'\n echo 'yes'\n else\n echo 'no'\n endcase\n%}",
"yes",
),
(
"{% liquid\n capture c\n endcapture\n assign a = 'x'\n echo a\n%}",
"x",
),
("{% liquid if name %}{{ name }}{% liquid endif %}", "Archival"),
("{% if name %}{% liquid echo name %}{% endif %}", "Archival"),
("a{% liquid echo name %}b", "aArchivalb"),
("{% liquid assign a = '%}' \n echo a %}", "%}"),
("{% liquid # it's a note\n echo name %}", "Archival"),
];
#[test]
fn liquid_tags_render_like_their_expansion() {
let parser = crate::liquid_parser::build_with_partials(Default::default()).unwrap();
let globals = liquid::object!({
"name": "Archival",
"list": ["one", "two"],
});
for (source, expected) in LIQUID_TAG_CORPUS {
assert!(
parser.parse(source).is_err(),
"{source:?} parses as stock liquid; it no longer exercises the expansion"
);
let rendered = crate::liquid_parser::parse(&parser, source)
.unwrap_or_else(|e| panic!("failed parsing {source:?}: {e}"))
.render(&globals)
.unwrap_or_else(|e| panic!("failed rendering {source:?}: {e}"));
assert_eq!(&rendered, expected, "output mismatch for {source:?}");
}
}
#[test]
fn inline_comments_render_as_nothing() {
let parser = crate::liquid_parser::build_with_partials(Default::default()).unwrap();
let globals = liquid::object!({
"name": "Archival",
"list": ["one", "two"],
});
for (source, expected) in COMMENT_CORPUS {
assert!(
parser.parse(source).is_err(),
"{source:?} parses as stock liquid; it no longer exercises the strip"
);
let rendered = crate::liquid_parser::parse(&parser, source)
.unwrap_or_else(|e| panic!("failed parsing {source:?}: {e}"))
.render(&globals)
.unwrap_or_else(|e| panic!("failed rendering {source:?}: {e}"));
assert_eq!(&rendered, expected, "output mismatch for {source:?}");
}
}
const CORPUS: &[&str] = &[
"",
"hello",
"{{ name }}",
"{{name}}",
"{{ name | upcase }}",
"{{ name | append: '!' }}",
r#"{{ name | append: "}}" }}"#,
"{{ name | append: '{%' }}",
"{{ nested.a }}",
"{{ list[0] }}",
"{{ 'literal' }}",
"{{ 42 }}",
"{{ missing }}",
"{{- name -}}",
" {{- name -}} ",
"a\n{{- name }}\nb",
"{{ name }}{{ name }}",
"x{{ name }}y{{ nested.a }}z",
"{% assign a = name %}{{ a }}",
"{% assign a = 'x' %}{{ a }}{{ name }}",
r#"{% assign a = "{{" %}{{ a }}"#,
r#"{% assign a = "%}" %}{{ a }}"#,
"{% if name %}{{ name }}{% endif %}",
"{% unless name %}no{% else %}{{ name }}{% endunless %}",
"{% for i in list %}{{ i }}{{ forloop.index }}{% endfor %}",
"{% for i in list %}{%- if i %}{{- i -}}{% endif -%}{% endfor %}",
"{% capture c %}{{ name }}{% endcapture %}{{ c }}",
"{% case name %}{% when 'a' %}{{ name }}{% else %}x{% endcase %}",
"{% comment %}{{ name }}{% endcomment %}ok",
"{% raw %}{{ name }}{% endraw %}",
"{% raw %}{{ name }}{% endraw %}{{ name }}",
"{%- raw -%}{{ name }}{%- endraw -%}",
"{% raw %}{% if %}{% endraw %}{{ name }}",
"{{ name }}}",
"}}{{ name }}",
"{{{ name }}}",
"{{}}",
"{{ x",
r#"{{ "abc }}"#,
"{% tablerow i in list %}{{ i }}{% endtablerow %}",
"<script>var a = 1;</script>{{ name }}",
"```\n{{ name }}\n```",
];
#[test]
fn rewriting_preserves_parse_and_render() {
let parser = crate::liquid_parser::build_with_partials(Default::default()).unwrap();
let globals = liquid::object!({
"name": "Archival",
"nested": { "a": "A" },
"list": ["one", "two"],
});
for source in CORPUS {
let rewritten = rewrite_template(source);
let before = parser.parse(source);
let after = parser.parse(&rewritten);
assert_eq!(
before.is_err(),
after.is_err(),
"parse mismatch for {source:?} (rewritten: {rewritten:?}): {:?} vs {:?}",
before.err().map(|e| e.to_string()),
after.err().map(|e| e.to_string()),
);
let (Ok(before), Ok(after)) = (before, after) else {
continue;
};
let before = before.render(&globals);
if before
.as_ref()
.is_ok_and(|r| r.contains("{{") || r.contains("{%"))
{
continue;
}
let after = after.render(&globals);
assert_eq!(
before.is_err(),
after.is_err(),
"render mismatch for {source:?}: {:?} vs {:?}",
before.as_ref().err().map(|e| e.to_string()),
after.as_ref().err().map(|e| e.to_string()),
);
if let (Ok(before), Ok(after)) = (before, after) {
assert_eq!(before, after, "output mismatch for {source:?}");
}
}
}
}