@@ -1,8 +1,8 @@
11//! Parsing: source text to a syntax tree, before anything is evaluated.
22//!
33//! This step knows the shape of the language: indentation, continuation
4−//! lines of attribute lists, verbatim blocks (`@raw """…"""`, the bodies of
5−//! `@markdown` and `@script`), multi-line strings, and the directives whose
4+//! lines of attribute lists, verbatim bodies (of `@raw`, `@style`, `@head`,
5+//! `@script` and `@markdown`), multi-line strings, and the directives whose
66//! structure spans several lines (`@if` / `@else` chains, `@each` with an
77//! `@else`, function definitions). It reads no files and substitutes no
88//! variables; `parser.rs` evaluates the tree.
@@ -101,13 +101,11 @@ impl Syntax {
101101pub(crate) fn parse(input: &str) -> Vec<Syntax> {
102102 let lines = preprocess(input);
103103 let mut pos = 0;
104− build(&lines, &mut pos, None, false)
104+ build(&lines, &mut pos, None)
105105}
106106
107107/// Build the nodes for the lines from `pos` indented deeper than `parent`.
108−/// A `plain` block is text (the body of `@style` or `@head`),
109−/// so no directive in it is given structure.
110−fn build(lines: &[Line], pos: &mut usize, parent: Option<usize>, plain: bool) -> Vec<Syntax> {
108+fn build(lines: &[Line], pos: &mut usize, parent: Option<usize>) -> Vec<Syntax> {
111109 let mut nodes = Vec::new();
112110 let deeper = |line: &Line| parent.is_none_or(|p| line.indent > p);
113111 while *pos < lines.len() && deeper(&lines[*pos]) {
@@ -125,7 +123,7 @@ fn build(lines: &[Line], pos: &mut usize, parent: Option<usize>, plain: bool) ->
125123 };
126124 // Text and one-line directives take no body: lines indented under
127125 // them are their siblings.
128− if !plain && !takes_body(&text) {
126+ if !takes_body(&text) {
129127 nodes.push(Syntax::Line {
130128 line: line.line_num,
131129 indent: line.indent,
@@ -134,20 +132,7 @@ fn build(lines: &[Line], pos: &mut usize, parent: Option<usize>, plain: bool) ->
134132 });
135133 continue;
136134 }
137− let text_body = plain
138− || text == "@style"
139− || text == "@head"
140− || text.starts_with("@head ");
141− let children = build(lines, pos, Some(line.indent), text_body);
142− if plain {
143− nodes.push(Syntax::Line {
144− line: line.line_num,
145− indent: line.indent,
146− text,
147− children,
148− });
149− continue;
150− }
135+ let children = build(lines, pos, Some(line.indent));
151136
152137 if let Some(condition) = text.strip_prefix("@if ") {
153138 let mut branches = vec![Branch {
@@ -168,7 +153,7 @@ fn build(lines: &[Line], pos: &mut usize, parent: Option<usize>, plain: bool) ->
168153 branches.push(Branch {
169154 line: next_line,
170155 condition,
171− body: build(lines, pos, Some(line.indent), false),
156+ body: build(lines, pos, Some(line.indent)),
172157 });
173158 if last {
174159 break;
@@ -182,7 +167,7 @@ fn build(lines: &[Line], pos: &mut usize, parent: Option<usize>, plain: bool) ->
182167 let mut empty = Vec::new();
183168 if let Some((_, "@else")) = sibling(lines, *pos, line.indent) {
184169 *pos += 1;
185− empty = build(lines, pos, Some(line.indent), false);
170+ empty = build(lines, pos, Some(line.indent));
186171 }
187172 nodes.push(Syntax::Each {
188173 line: line.line_num,
@@ -274,14 +259,15 @@ pub(crate) fn preprocess(input: &str) -> Vec<Line> {
274259
275260 let indent = line.len() - line.trim_start().len();
276261
277− // Inline `@markdown` and `@script` blocks keep their bodies verbatim:
278− // in Markdown, blank lines separate paragraphs, `---` is a rule (not
279− // a comment) and code indentation matters; in JavaScript, newlines,
280− // `{...}` and `$` must reach the output untouched.
281− let verbatim_body = trimmed == "@markdown"
282− || trimmed == "@script"
262+ // The bodies of `@markdown`, `@script`, `@style`, `@head` and `@raw`
263+ // are foreign text, kept verbatim: in Markdown, blank lines separate
264+ // paragraphs and `---` is a rule (not a comment); in CSS, `--name` is
265+ // a custom property; in JavaScript, newlines, `{...}` and `$` must
266+ // reach the output untouched.
267+ let verbatim_body = ["@markdown", "@script", "@style", "@head", "@raw"].contains(&trimmed)
283268 || trimmed.starts_with("@script ")
284− || trimmed.starts_with("@script[");
269+ || trimmed.starts_with("@script[")
270+ || trimmed.starts_with("@head ");
285271 if verbatim_body {
286272 lines.push(Line {
287273 indent,
@@ -325,46 +311,30 @@ pub(crate) fn preprocess(input: &str) -> Vec<Line> {
325311 continue;
326312 }
327313
328− // Handle @raw """..."""
329− if let Some(raw_rest) = trimmed.strip_prefix("@raw") {
330− let after_raw = raw_rest.trim_start();
331− if let Some(after_open) = after_raw.strip_prefix("\"\"\"") {
332− // Single-line: @raw """content"""
333− if after_open.ends_with("\"\"\"") && after_open.len() >= 3 {
334− let content = &after_open[..after_open.len() - 3];
335− lines.push(Line {
336− indent,
337− content: LineContent::Raw(content.to_string()),
338− line_num: i + 1,
339− });
340− i += 1;
341− continue;
342− }
343−
344− // Multiline: collect until closing """
345− let mut raw_content = String::new();
346− if !after_open.is_empty() {
347− raw_content.push_str(after_open);
348− raw_content.push('\n');
349− }
314+ // The removed `@raw """…"""` form: its content is kept whole (and
315+ // not evaluated) so the evaluator can point at the indented form.
316+ if let Some(after_open) = trimmed.strip_prefix("@raw \"\"\"") {
317+ let mut content = vec![after_open.trim_end_matches("\"\"\"")];
318+ let first_line_num = i + 1;
319+ if !(after_open.len() >= 3 && after_open.ends_with("\"\"\"")) {
350320 i += 1;
351− while i < raw_lines.len() {
352− if raw_lines[i].trim() == "\"\"\"" {
353− i += 1;
354− break;
355− }
356− raw_content.push_str(raw_lines[i]);
357− raw_content.push('\n');
321+ while i < raw_lines.len() && raw_lines[i].trim() != "\"\"\"" {
322+ content.push(raw_lines[i]);
358323 i += 1;
359324 }
360−
361− lines.push(Line {
362− indent,
363− content: LineContent::Raw(raw_content.trim_end_matches('\n').to_string()),
364− line_num: i,
365− });
366− continue;
367325 }
326+ i += 1;
327+ lines.push(Line {
328+ indent,
329+ content: LineContent::Normal("@raw \"\"\"".to_string()),
330+ line_num: first_line_num,
331+ });
332+ lines.push(Line {
333+ indent: indent + 1,
334+ content: LineContent::Raw(content.join("\n")),
335+ line_num: first_line_num,
336+ });
337+ continue;
368338 }
369339
370340 // `@let name """` opens a multi-line string running to a `"""` line;
@@ -501,10 +471,12 @@ mod tests {
501471 }
502472
503473 #[test]
504− fn text_bodies_stay_plain() {
505− let tree = parse("@style\n @if x {\n a\n }\n");
474+ fn text_bodies_are_verbatim() {
475+ let tree = parse("@style\n :root {\n --brand: red;\n }\n@text x\n");
506476 let Syntax::Line { children, .. } = &tree[0] else { unreachable!() };
507− assert_eq!(kinds(children), ["line", "line"]);
477+ let [Syntax::Raw { text, .. }] = children.as_slice() else { panic!("{:?}", children) };
478+ assert_eq!(text, ":root {\n --brand: red;\n}");
479+ assert_eq!(kinds(&tree), ["line", "line"]);
508480 }
509481
510482 #[test]