on master help sign in
Verbatim text bodies, one way
75da6a7iko
DESIGN.md+9−4
@@ -76,12 +76,16 @@ gets the indented children.
76
76
77### Raw content
77### Raw content
78
78
79−`@raw """ ... """` is pasted into the output verbatim.
79+`@raw` pastes HTML into the output verbatim: the rest of its line, or its
80+indented block. The bodies of `@style`, `@head`, `@script` and `@markdown` are
81+verbatim too (a `--` line in them is not a comment).
80
82
81```
83```
82−@raw """
84+@raw <hr class="fancy">
85+@raw
83−<div class="custom-widget"></div>
86+ <div class="custom-widget">
84−"""
87+ <span>Hand-written HTML</span>
88+ </div>
85```
89```
86
90
87## Elements
91## Elements
@@ -484,6 +488,7 @@ removed form with its replacement.
484| `$a ~ " " ~ $b` | `"$a $b"` |
488| `$a ~ " " ~ $b` | `"$a $b"` |
485| `@each $x in LIST [page N]` | Split the list, or filter it with `@if` |
489| `@each $x in LIST [page N]` | Split the list, or filter it with `@if` |
486| `...$bundle` | `$bundle` |
490| `...$bundle` | `$bundle` |
491+| `@raw """ ... """` | `@raw` with an indented body |
487| `@keyframes NAME` with `from [opacity 0]` | The `@keyframes` rule in `@style` |
492| `@keyframes NAME` with `from [opacity 0]` | The `@keyframes` rule in `@style` |
488| `@include lib.hl as ui` | `@include lib.hl`, without the `ui.` prefix |
493| `@include lib.hl as ui` | `@include lib.hl`, without the `ui.` prefix |
489| `@data file.json` (keys as `$key`) | `@data $name file.json` (keys as `$name.key`) |
494| `@data file.json` (keys as `$key`) | `@data $name file.json` (keys as `$name.key`) |
crates/htmlang-core/src/parser.rs+29−14
@@ -748,6 +748,24 @@ impl Evaluator {
748
748
749 // --- @each loop ---
749 // --- @each loop ---
750
750
751+ // @raw: its indented body, or the rest of the line, verbatim
752+ if content == "@raw" {
753+ let text = block_text(children);
754+ let text = text.trim_end_matches('\n');
755+ return Ok((!text.is_empty()).then(|| vec![Node::Raw(text.to_string())]));
756+ }
757+ if let Some(rest) = content.strip_prefix("@raw ") {
758+ if rest.starts_with("\"\"\"") {
759+ return Err(ParseError {
760+ line: line_num,
761+ message: "`@raw \"\"\"` was removed: put the content in an indented block \
762+ under `@raw` (run `htmlang upgrade`)"
763+ .to_string(),
764+ });
765+ }
766+ return Ok(Some(vec![Node::Raw(rest.to_string())]));
767+ }
768+
751 // --- Function call ---
769 // --- Function call ---
752
770
753 if content.starts_with('@') {
771 if content.starts_with('@') {
@@ -993,20 +1011,17 @@ impl Evaluator {
993 .iter()
1011 .iter()
994 .partition(|node| matches!(node, Syntax::Line { text, .. } if text == "@style"));
1012 .partition(|node| matches!(node, Syntax::Line { text, .. } if text == "@style"));
995 if !style.is_empty() {
1013 if !style.is_empty() {
996− let mut scoped_css = String::new();
1014+ let css: String = style
997− for node in style {
1015+ .iter()
1016+ .filter_map(|node| match node {
998− if let Syntax::Line { children, .. } = node {
1017+ Syntax::Line { children, .. } => Some(block_text(children)),
999− for_each_line(children, &mut |node| {
1018+ _ => None,
1000− if let Syntax::Line { text, .. } = node
1019+ })
1020+ .collect();
1021+ // Nested under the scope class, so any CSS works (multi-line
1022+ // rules, at-rules)
1001− && !text.trim().is_empty()
1023+ if !css.trim().is_empty() {
1002− {
1024+ ctx.custom_css.push(format!(".hl-{} {{\n{}}}", name, css));
1003− scoped_css.push_str(&format!(".hl-{} {}\n", name, text.trim()));
1004− }
1005− });
1006− }
1007− }
1008− if !scoped_css.is_empty() {
1009− ctx.custom_css.push(scoped_css);
1010 }
1025 }
1011 ctx.scoped_functions.insert(name.to_string());
1026 ctx.scoped_functions.insert(name.to_string());
1012 }
1027 }
crates/htmlang-core/src/syntax.rs+41−69
@@ -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.
2//!
2//!
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)
105}
105}
106
106
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) ->
125 };
123 };
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) ->
134 });
132 });
135 continue;
133 continue;
136 }
134 }
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);
135+ let children = build(lines, pos, Some(line.indent));
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− }
151
136
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)),
172 });
157 });
173 if last {
158 if last {
174 break;
159 break;
@@ -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));
186 }
171 }
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> {
274
259
275 let indent = line.len() - line.trim_start().len();
260 let indent = line.len() - line.trim_start().len();
276
261
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 {
287 indent,
273 indent,
@@ -325,46 +311,30 @@ pub(crate) fn preprocess(input: &str) -> Vec<Line> {
325 continue;
311 continue;
326 }
312 }
327
313
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;
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() {
319+ if !(after_open.len() >= 3 && after_open.ends_with("\"\"\"")) {
347− raw_content.push_str(after_open);
348− raw_content.push('\n');
349− }
350 i += 1;
320 i += 1;
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]);
353− i += 1;
354− break;
355− }
356− raw_content.push_str(raw_lines[i]);
357− raw_content.push('\n');
358 i += 1;
323 i += 1;
359 }
324 }
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;
367 }
325 }
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;
368 }
338 }
369
339
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 {
501 }
471 }
502
472
503 #[test]
473 #[test]
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"]);
508 }
480 }
509
481
510 #[test]
482 #[test]
editors/vscode/README.md+1−1
@@ -6,7 +6,7 @@ Syntax highlighting, snippets, and LSP-backed intelligence for [htmlang](https:/
6## Features
6## Features
7
7
8- **Syntax highlighting** — `@` directives, `$variables`, `[attribute]` brackets,
8- **Syntax highlighting** — `@` directives, `$variables`, `[attribute]` brackets,
9− `-- comments`, `@raw """..."""` blocks.
9+ `-- comments`, verbatim `@raw` / `@style` / `@script` bodies.
10- **Diagnostics** — parse errors, missing variables/functions, unused bindings,
10- **Diagnostics** — parse errors, missing variables/functions, unused bindings,
11 CSS / a11y warnings, and typo suggestions.
11 CSS / a11y warnings, and typo suggestions.
12- **Completion** — directives, attribute keys and values, variable names, and
12- **Completion** — directives, attribute keys and values, variable names, and
examples/demo.hl+6−8
@@ -44,9 +44,8 @@
44 }
44 }
45
45
46-- Raw HTML escape hatch
46-- Raw HTML escape hatch
47−@raw """
47+@raw
48−<noscript><style>.needs-js{display:none}</style></noscript>
48+ <noscript><style>.needs-js{display:none}</style></noscript>
49−"""
50
49
51-- Component with default parameter + @children slot
50-- Component with default parameter + @children slot
52@let card $title $icon=◆
51@let card $title $icon=◆
@@ -128,11 +127,10 @@
128 padding 16, rounded 8, font monospace, size 13,
127 padding 16, rounded 8, font monospace, size 13,
129 overflow-x auto
128 overflow-x auto
130 ]
129 ]
131− @raw """
130+ @raw
132−@let button $label
131+ @let button $label
133− @el [padding 12, background blue]
132+ @el [padding 12, background blue]
134− @text $label
133+ @text $label
135−"""
136
134
137 -- Control flow: @each, @else, @match, if()
135 -- Control flow: @each, @else, @match, if()
138 @titled [title Control flow]
136 @titled [title Control flow]
src/bin/htmlang_lsp/docs.rs+1−1
@@ -95,7 +95,7 @@ pub(crate) const DIRECTIVES: &[Doc] = &[
95 "@let primary #3b82f6\n@let gap = 8 * 2\n@let card [padding 20]\n@let button $label\n @el [$card] $label",
95 "@let primary #3b82f6\n@let gap = 8 * 2\n@let card [padding 20]\n@let button $label\n @el [$card] $label",
96 ),
96 ),
97 doc("include", "Inserts another `.hl` file here, with its definitions.", "@include header.hl"),
97 doc("include", "Inserts another `.hl` file here, with its definitions.", "@include header.hl"),
98− doc("raw", "Pastes HTML into the output verbatim.", "@raw \"\"\"<div></div>\"\"\""),
98+ doc("raw", "Pastes HTML into the output verbatim: the rest of the line, or an indented block.", "@raw\n <div class=\"widget\"></div>"),
99 doc("markdown", "Markdown (indented body or file), converted to HTML.", "@markdown\n # Title"),
99 doc("markdown", "Markdown (indented body or file), converted to HTML.", "@markdown\n # Title"),
100 doc("style", "Raw CSS; overrides generated styles.", "@style\n .note { color: gray; }"),
100 doc("style", "Raw CSS; overrides generated styles.", "@style\n .note { color: gray; }"),
101 doc("head", "Raw HTML added to `<head>`.", "@head\n <link rel=\"icon\" href=\"f.ico\">"),
101 doc("head", "Raw HTML added to `<head>`.", "@head\n <link rel=\"icon\" href=\"f.ico\">"),
src/fmt.rs+2−2
@@ -91,8 +91,8 @@ fn is_raw_body_header(code: &str) -> bool {
91 "@style",
91 "@style",
92 "@script",
92 "@script",
93 "@markdown",
93 "@markdown",
94− "@json-ld",
95 "@head",
94 "@head",
95+ "@raw",
96 ]
96 ]
97 .iter()
97 .iter()
98 .any(|d| code == *d || code.strip_prefix(d).is_some_and(|r| r.starts_with(' ')))
98 .any(|d| code == *d || code.strip_prefix(d).is_some_and(|r| r.starts_with(' ')))
@@ -528,7 +528,7 @@ mod tests {
528
528
529 #[test]
529 #[test]
530 fn raw_blocks_are_verbatim() {
530 fn raw_blocks_are_verbatim() {
531− let src = "@raw \"\"\"\n<pre>\n deeply indented\n</pre>\n\"\"\"\n";
531+ let src = "@raw\n <pre>\n deeply indented\n </pre>\n";
532 assert_eq!(format(src), src);
532 assert_eq!(format(src), src);
533 }
533 }
534
534
src/upgrade.rs+40−3
@@ -45,7 +45,7 @@ const FILTER_ALIASES: &[(&str, &str)] = &[
45
45
46/// Directives whose indented bodies are foreign text (CSS, JS, Markdown,
46/// Directives whose indented bodies are foreign text (CSS, JS, Markdown,
47/// HTML, JSON) and must not be rewritten.
47/// HTML, JSON) and must not be rewritten.
48−const VERBATIM_BODIES: &[&str] = &["@style", "@script", "@markdown", "@head"];
48+const VERBATIM_BODIES: &[&str] = &["@style", "@script", "@markdown", "@head", "@raw"];
49
49
50/// The function name a layout file becomes: `layout.hl` → `layout`,
50/// The function name a layout file becomes: `layout.hl` → `layout`,
51/// `base.hl` → `base-layout` (so it can't shadow an element like `@main`).
51/// `base.hl` → `base-layout` (so it can't shadow an element like `@main`).
@@ -174,6 +174,34 @@ pub fn upgrade(input: &str) -> Upgrade {
174 let trimmed = line.trim();
174 let trimmed = line.trim();
175 let indent = indent_of(line);
175 let indent = indent_of(line);
176
176
177+ // `@raw """…"""` → `@raw` with the content as its indented body
178+ if let Some(after_open) = trimmed.strip_prefix("@raw \"\"\"") {
179+ let pad = " ".repeat(indent);
180+ match triple_quote_end(&lines, i) {
181+ None => out.push(format!(
182+ "{pad}@raw {}",
183+ after_open.strip_suffix("\"\"\"").unwrap_or(after_open)
184+ )),
185+ Some(end) => {
186+ out.push(format!("{pad}@raw"));
187+ let closed = end <= lines.len() && lines[end - 1].trim() == "\"\"\"";
188+ let body_end = if closed { end - 1 } else { end };
189+ let content = std::iter::once(after_open)
190+ .filter(|l| !l.is_empty())
191+ .chain(lines[i + 1..body_end].iter().copied());
192+ out.extend(content.map(|l| {
193+ if l.trim().is_empty() { String::new() } else { format!("{pad} {l}") }
194+ }));
195+ i = end;
196+ changes += 1;
197+ continue;
198+ }
199+ }
200+ changes += 1;
201+ i += 1;
202+ continue;
203+ }
204+
177 // Copy verbatim regions unchanged.
205 // Copy verbatim regions unchanged.
178 if let Some(end) = triple_quote_end(&lines, i) {
206 if let Some(end) = triple_quote_end(&lines, i) {
179 out.extend(lines[i..end].iter().map(|l| l.to_string()));
207 out.extend(lines[i..end].iter().map(|l| l.to_string()));
@@ -1566,7 +1594,7 @@ mod tests {
1566 );
1594 );
1567 assert_eq!(
1595 assert_eq!(
1568 up("@raw \"\"\"\n@let button $x\n\"\"\"\n@button [type submit] Go"),
1596 up("@raw \"\"\"\n@let button $x\n\"\"\"\n@button [type submit] Go"),
1569− "@raw \"\"\"\n@let button $x\n\"\"\"\n@button [type=submit] Go"
1597+ "@raw\n @let button $x\n@button [type=submit] Go"
1570 );
1598 );
1571 // Multi-line lists keep their element context.
1599 // Multi-line lists keep their element context.
1572 assert_eq!(up("@input [\n type email,\n padding 8\n]"), "@input [\n type=email,\n padding 8\n]");
1600 assert_eq!(up("@input [\n type email,\n padding 8\n]"), "@input [\n type=email,\n padding 8\n]");
@@ -1648,6 +1676,15 @@ mod tests {
1648 );
1676 );
1649 }
1677 }
1650
1678
1679+ #[test]
1680+ fn raw_strings_become_indented_bodies() {
1681+ assert_eq!(up("@raw \"\"\"<br>\"\"\"\n"), "@raw <br>\n");
1682+ assert_eq!(
1683+ up("@el\n @raw \"\"\"\n<div>\n\n <p>x</p>\n</div>\n\"\"\"\n@text y\n"),
1684+ "@el\n @raw\n <div>\n\n <p>x</p>\n </div>\n@text y\n"
1685+ );
1686+ }
1687+
1651 #[test]
1688 #[test]
1652 fn keyframes_move_into_style() {
1689 fn keyframes_move_into_style() {
1653 assert_eq!(
1690 assert_eq!(
@@ -1702,7 +1739,7 @@ mod tests {
1702
1739
1703 #[test]
1740 #[test]
1704 fn verbatim_regions_are_untouched() {
1741 fn verbatim_regions_are_untouched() {
1705− let src = "@script\n for (x of y) {}\n@raw \"\"\"\n@unless\n\"\"\"\n@style\n .p { }";
1742+ let src = "@script\n for (x of y) {}\n@raw\n @unless\n@style\n .p { }\n@let s \"\"\"\n@unless\n\"\"\"";
1706 assert_eq!(up(src), src);
1743 assert_eq!(up(src), src);
1707 assert_eq!(up("@script [src app.js, defer]"), "@script [src=app.js, defer]");
1744 assert_eq!(up("@script [src app.js, defer]"), "@script [src=app.js, defer]");
1708 }
1745 }
tests/regressions.rs+18
@@ -410,3 +410,21 @@ fn lines_under_text_are_its_siblings() {
410 );
410 );
411 assert!(html.contains("<span>Some text</span><span>more text</span>"), "{}", html);
411 assert!(html.contains("<span>Some text</span><span>more text</span>"), "{}", html);
412}
412}
413+
414+#[test]
415+fn style_keeps_css_custom_properties() {
416+ let out = compile("@style\n :root {\n --brand: red;\n }\n@el [color var(--brand)] x\n");
417+ assert!(out.contains("--brand: red;"), "{}", out);
418+}
419+
420+#[test]
421+fn raw_takes_an_indented_body() {
422+ let out = compile("@raw\n <div>\n\n <p>x</p>\n </div>\n@raw <hr>\n");
423+ assert!(out.contains("<div>\n\n <p>x</p>\n</div><hr>"), "{}", out);
424+ let result = parser::parse("@raw \"\"\"<br>\"\"\"\n");
425+ assert!(
426+ result.diagnostics.iter().any(|d| d.message.contains("`@raw \"\"\"` was removed")),
427+ "{:?}",
428+ result.diagnostics
429+ );
430+}
tests/snapshots.rs+1−1
@@ -3786,7 +3786,7 @@ fn function_with_style_is_scoped() {
3786 "@let card $title\n @style\n .t { color: red; }\n @text [class=t] $title\n@card [title Hello]\n",
3786 "@let card $title\n @style\n .t { color: red; }\n @text [class=t] $title\n@card [title Hello]\n",
3787 );
3787 );
3788 assert!(html.contains("hl-card\"><span class=\"t\">Hello"), "{}", html);
3788 assert!(html.contains("hl-card\"><span class=\"t\">Hello"), "{}", html);
3789− assert!(html.contains(".hl-card .t { color: red; }"), "{}", html);
3789+ assert!(html.contains(".hl-card {.t { color: red; }}"), "{}", html);
3790}
3790}
3791
3791
3792#[test]
3792#[test]
tests/snapshots/new_directives.html+5−5
@@ -1,7 +1,7 @@
1<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>My Site</title><base href="https://example.com/"><link rel="canonical" href="https://example.com/page"><link rel="preload" href="fonts/inter.woff2" as="font" crossorigin><link rel="preload" href="fonts/mono.ttf" as="font" crossorigin><script type="application/ld+json">
1<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>My Site</title><base href="https://example.com/"><link rel="canonical" href="https://example.com/page"><link rel="preload" href="fonts/inter.woff2" as="font" crossorigin><link rel="preload" href="fonts/mono.ttf" as="font" crossorigin><script type="application/ld+json">
2−{
2+ {
3−"@context": "https://schema.org",
3+ "@context": "https://schema.org",
4−"@type": "WebPage",
4+ "@type": "WebPage",
5−"name": "My Site"
5+ "name": "My Site"
6−}
6+ }
7</script><style>@layer hl-reset,htmlang;@layer hl-reset{*,*::before,*::after{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,sans-serif}img{display:block}a{text-decoration:none;color:inherit}}@layer htmlang{.a{display:flex;flex-direction:column;gap:10px;}.b{font-family:Inter;}}@font-face { font-family: 'Inter'; src: url('fonts/inter.woff2') format('woff2'); font-display: swap; }@font-face { font-family: 'Mono'; src: url('fonts/mono.ttf') format('truetype'); font-display: swap; }</style></head><body><div class="a"><span class="b">Hello with custom fonts</span></div></body></html>
7</script><style>@layer hl-reset,htmlang;@layer hl-reset{*,*::before,*::after{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,sans-serif}img{display:block}a{text-decoration:none;color:inherit}}@layer htmlang{.a{display:flex;flex-direction:column;gap:10px;}.b{font-family:Inter;}}@font-face { font-family: 'Inter'; src: url('fonts/inter.woff2') format('woff2'); font-display: swap; }@font-face { font-family: 'Mono'; src: url('fonts/mono.ttf') format('truetype'); font-display: swap; }</style></head><body><div class="a"><span class="b">Hello with custom fonts</span></div></body></html>
tests/snapshots/raw_html.hl+1−1
@@ -1,5 +1,5 @@
1@page Raw HTML
1@page Raw HTML
2@column [spacing 10]
2@column [spacing 10]
3 @text Before raw
3 @text Before raw
4− @raw """<div class="custom">Raw HTML content</div>"""
4+ @raw <div class="custom">Raw HTML content</div>
5 @text After raw
5 @text After raw