@@ -1,8 +1,8 @@
1//! Parsing: source text to a syntax tree, before anything is evaluated.
1//! Parsing: source text to a syntax tree, before anything is evaluated.
3//! This step knows the shape of the language: indentation, continuation
3//! This step knows the shape of the language: indentation, continuation
4−//! lines of attribute lists, verbatim blocks (`@raw """…"""`, the bodies of
4+//! lines of attribute lists, verbatim bodies (of `@raw`, `@style`, `@head`,
5−//! `@markdown` and `@script`), multi-line strings, and the directives whose
5+//! `@script` and `@markdown`), multi-line strings, and the directives whose
6//! structure spans several lines (`@if` / `@else` chains, `@each` with an
6//! structure spans several lines (`@if` / `@else` chains, `@each` with an
7//! `@else`, function definitions). It reads no files and substitutes no
7//! `@else`, function definitions). It reads no files and substitutes no
8//! variables; `parser.rs` evaluates the tree.
8//! variables; `parser.rs` evaluates the tree.
@@ -101,13 +101,11 @@ impl Syntax {
101pub(crate) fn parse(input: &str) -> Vec<Syntax> {
101pub(crate) fn parse(input: &str) -> Vec<Syntax> {
102 let lines = preprocess(input);
102 let lines = preprocess(input);
103 let mut pos = 0;
103 let mut pos = 0;
104− build(&lines, &mut pos, None, false)
104+ build(&lines, &mut pos, None)
107/// Build the nodes for the lines from `pos` indented deeper than `parent`.
107/// 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> {
111 let mut nodes = Vec::new();
109 let mut nodes = Vec::new();
112 let deeper = |line: &Line| parent.is_none_or(|p| line.indent > p);
110 let deeper = |line: &Line| parent.is_none_or(|p| line.indent > p);
113 while *pos < lines.len() && deeper(&lines[*pos]) {
111 while *pos < lines.len() && deeper(&lines[*pos]) {
@@ -125,7 +123,7 @@ fn build(lines: &[Line], pos: &mut usize, parent: Option<usize>, plain: bool) ->
126 // Text and one-line directives take no body: lines indented under
124 // Text and one-line directives take no body: lines indented under
127 // them are their siblings.
125 // them are their siblings.
128− if !plain && !takes_body(&text) {
126+ if !takes_body(&text) {
129 nodes.push(Syntax::Line {
127 nodes.push(Syntax::Line {
130 line: line.line_num,
128 line: line.line_num,
131 indent: line.indent,
129 indent: line.indent,
@@ -134,20 +132,7 @@ fn build(lines: &[Line], pos: &mut usize, parent: Option<usize>, plain: bool) ->
135 continue;
133 continue;
137− let text_body = plain
140− || text.starts_with("@head ");
141− let children = build(lines, pos, Some(line.indent), text_body);
135+ let children = build(lines, pos, Some(line.indent));
143− nodes.push(Syntax::Line {
144− line: line.line_num,
145− indent: line.indent,
152 if let Some(condition) = text.strip_prefix("@if ") {
137 if let Some(condition) = text.strip_prefix("@if ") {
153 let mut branches = vec![Branch {
138 let mut branches = vec![Branch {
@@ -168,7 +153,7 @@ fn build(lines: &[Line], pos: &mut usize, parent: Option<usize>, plain: bool) ->
168 branches.push(Branch {
153 branches.push(Branch {
169 line: next_line,
154 line: next_line,
170 condition,
155 condition,
171− body: build(lines, pos, Some(line.indent), false),
156+ body: build(lines, pos, Some(line.indent)),
173 if last {
158 if last {
@@ -182,7 +167,7 @@ fn build(lines: &[Line], pos: &mut usize, parent: Option<usize>, plain: bool) ->
182 let mut empty = Vec::new();
167 let mut empty = Vec::new();
183 if let Some((_, "@else")) = sibling(lines, *pos, line.indent) {
168 if let Some((_, "@else")) = sibling(lines, *pos, line.indent) {
184 *pos += 1;
169 *pos += 1;
185− empty = build(lines, pos, Some(line.indent), false);
170+ empty = build(lines, pos, Some(line.indent));
187 nodes.push(Syntax::Each {
172 nodes.push(Syntax::Each {
188 line: line.line_num,
173 line: line.line_num,
@@ -274,14 +259,15 @@ pub(crate) fn preprocess(input: &str) -> Vec<Line> {
275 let indent = line.len() - line.trim_start().len();
260 let indent = line.len() - line.trim_start().len();
277− // Inline `@markdown` and `@script` blocks keep their bodies verbatim:
262+ // The bodies of `@markdown`, `@script`, `@style`, `@head` and `@raw`
278− // in Markdown, blank lines separate paragraphs, `---` is a rule (not
263+ // are foreign text, kept verbatim: in Markdown, blank lines separate
264+ // paragraphs and `---` is a rule (not a comment); in CSS, `--name` is
279− // a comment) and code indentation matters; in JavaScript, newlines,
265+ // a custom property; in JavaScript, newlines, `{...}` and `$` must
280− // `{...}` and `$` must reach the output untouched.
266+ // reach the output untouched.
281− let verbatim_body = trimmed == "@markdown"
267+ let verbatim_body = ["@markdown", "@script", "@style", "@head", "@raw"].contains(&trimmed)
282− || trimmed == "@script"
283 || trimmed.starts_with("@script ")
268 || trimmed.starts_with("@script ")
284− || trimmed.starts_with("@script[");
269+ || trimmed.starts_with("@script[")
270+ || trimmed.starts_with("@head ");
285 if verbatim_body {
271 if verbatim_body {
286 lines.push(Line {
272 lines.push(Line {
@@ -325,46 +311,30 @@ pub(crate) fn preprocess(input: &str) -> Vec<Line> {
325 continue;
311 continue;
328− // Handle @raw """..."""
314+ // The removed `@raw """…"""` form: its content is kept whole (and
315+ // not evaluated) so the evaluator can point at the indented form.
329− if let Some(raw_rest) = trimmed.strip_prefix("@raw") {
316+ if let Some(after_open) = 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];
317+ let mut content = vec![after_open.trim_end_matches("\"\"\"")];
335− lines.push(Line {
318+ let first_line_num = i + 1;
337− content: LineContent::Raw(content.to_string()),
344− // Multiline: collect until closing """
345− let mut raw_content = String::new();
346− if !after_open.is_empty() {
319+ if !(after_open.len() >= 3 && after_open.ends_with("\"\"\"")) {
347− raw_content.push_str(after_open);
348− raw_content.push('\n');
351− while i < raw_lines.len() {
321+ while i < raw_lines.len() && raw_lines[i].trim() != "\"\"\"" {
352− if raw_lines[i].trim() == "\"\"\"" {
322+ content.push(raw_lines[i]);
356− raw_content.push_str(raw_lines[i]);
357− raw_content.push('\n');
363− content: LineContent::Raw(raw_content.trim_end_matches('\n').to_string()),
329+ content: LineContent::Normal("@raw \"\"\"".to_string()),
330+ line_num: first_line_num,
334+ content: LineContent::Raw(content.join("\n")),
335+ line_num: first_line_num,
370 // `@let name """` opens a multi-line string running to a `"""` line;
340 // `@let name """` opens a multi-line string running to a `"""` line;
@@ -501,10 +471,12 @@ mod tests {
504− fn text_bodies_stay_plain() {
474+ fn text_bodies_are_verbatim() {
505− let tree = parse("@style\n @if x {\n a\n }\n");
475+ let tree = parse("@style\n :root {\n --brand: red;\n }\n@text x\n");
506 let Syntax::Line { children, .. } = &tree[0] else { unreachable!() };
476 let Syntax::Line { children, .. } = &tree[0] else { unreachable!() };
477+ let [Syntax::Raw { text, .. }] = children.as_slice() else { panic!("{:?}", children) };
478+ assert_eq!(text, ":root {\n --brand: red;\n}");
507− assert_eq!(kinds(children), ["line", "line"]);
479+ assert_eq!(kinds(&tree), ["line", "line"]);