on master help sign in
Remove @assert and @warn

A layout needs no compile-time checks or messages; mistakes already get diagnostics. htmlang upgrade drops the lines (and those of the older @debug, @log and @deprecated).

commitef700b02cc665cc03cce0d0d46beae1ec52b27db authoriko <mail@iko.soy> dateSat 26 Sep 2026, 02:58 (2 weeks ago) parent35ae18a
9 files changed+22−184
DESIGN.md+1−6 crates/htmlang-core/src/parser.rs+4−35 crates/htmlang-core/src/syntax.rs+1−1 examples/demo.hl+1−2 src/bin/htmlang_lsp/docs.rs−2 src/upgrade.rs+15−17 tests/snapshots.rs−110 tests/snapshots/assert_directive.hl−10 tests/snapshots/assert_directive.html−1
DESIGN.md+1−6
@@ -331,11 +331,6 @@ as `var(--name)` in the generated CSS:
331331@el [background $--brand] Themed
332332```
333333
334−### Checks
335−
336−`@assert CONDITION` fails the build when false. `@warn MESSAGE` emits a
337−warning.
338−
339334## Expressions
340335
341336Conditions and computed values (`@let x = ...`) are expressions:
@@ -482,7 +477,7 @@ removed form with its replacement.
482477| `@scope`, `@starting-style`, `@css-property` | The CSS rule in `@style` |
483478| `@lang`, `@favicon`, `@canonical`, `@base` | `@page [lang ..., favicon ..., canonical ..., base ...] Title` |
484479| `@og KEY VALUE` | `@meta og:KEY VALUE` |
485−| `@debug`, `@log` | `@warn` |
480+| `@assert`, `@warn`, `@debug`, `@log` | Nothing (the lines are removed) |
486481| `@defer` | Its content, directly |
487482| `@col`, `@p`, `@img`, `@li`, `@btn`, `@ul`, `@divider`, `@opt` | `@column`, `@paragraph`, `@image`, `@item`, `@button`, `@list`, `@hr`, `@option` |
488483| `type email`, `id main` (HTML attributes written as styles) | `type=email`, `id=main` |
crates/htmlang-core/src/parser.rs+4−35
@@ -497,22 +497,6 @@ impl Evaluator {
497497
498498
499499
500− // --- @assert directive (compile-time assertions) ---
501−
502− if let Some(rest) = content.strip_prefix("@assert ") {
503− let rest = rest.trim();
504− if !ctx.condition(rest, line_num) {
505− ctx.diagnostics.push(Diagnostic {
506− line: line_num,
507− column: None,
508− message: format!("assertion failed: {}", rest),
509− severity: Severity::Error,
510− source_line: Some(content.clone()),
511− });
512− }
513− return Ok(None);
514− }
515−
516500 if let Some(rest) = content.strip_prefix("@include ") {
517501 let rest = rest.trim();
518502 let (filename, alias) = if let Some((file_part, alias_part)) = rest.rsplit_once(" as ")
@@ -871,21 +855,6 @@ impl Evaluator {
871855
872856 // --- @each loop ---
873857
874− // --- @warn / @debug ---
875−
876− if let Some(rest) = content.strip_prefix("@warn ") {
877− let msg = substitute_vars(rest.trim(), &ctx.variables);
878− ctx.diagnostics.push(Diagnostic {
879− line: line_num,
880− column: None,
881− message: msg,
882− severity: Severity::Warning,
883− source_line: Some(content.clone()),
884− });
885− return Ok(None);
886− }
887−
888−
889858 // --- @keyframes directive ---
890859
891860 if let Some(rest) = content.strip_prefix("@keyframes ") {
@@ -1546,8 +1515,6 @@ const KNOWN_DIRECTIVES: &[&str] = &[
15461515 "meta",
15471516 "head",
15481517 "style",
1549− "warn",
1550− "assert",
15511518 "markdown",
15521519 "data",
15531520];
@@ -1614,7 +1581,7 @@ const REMOVED_SYNTAX: &[(&str, &str)] = &[
16141581 ("@canonical", "use `@page [canonical ...] Title`"),
16151582 ("@base", "use `@page [base ...] Title`"),
16161583 ("@og", "use `@meta og:NAME VALUE`"),
1617− ("@debug", "use `@warn`"),
1584+ ("@debug", "removed: a layout needs no compile-time messages"),
16181585 ("@svg", "use `@image [inline] file.svg`"),
16191586 ("@match", "use `@if $x == a` / `@else if $x == b` / `@else`"),
16201587 ("@case", "use `@if $x == a` / `@else if $x == b` / `@else`"),
@@ -1645,7 +1612,9 @@ const REMOVED_SYNTAX: &[(&str, &str)] = &[
16451612 "put each locale's strings in a JSON file and use `@data $t locales/$lang.json`",
16461613 ),
16471614 ("@defer", "remove it: the content is already in the page"),
1648− ("@log", "use `@warn`"),
1615+ ("@log", "removed: a layout needs no compile-time messages"),
1616+ ("@warn", "removed: a layout needs no compile-time messages"),
1617+ ("@assert", "removed: a layout needs no compile-time checks"),
16491618 (
16501619 "@component",
16511620 "use `@let`: an `@style` block in a function body is scoped to it",
crates/htmlang-core/src/syntax.rs+1−1
@@ -232,7 +232,7 @@ fn build(lines: &[Line], pos: &mut usize, parent: Option<usize>, plain: bool) ->
232232
233233/// Whether the lines indented under `text` belong to it.
234234fn takes_body(text: &str) -> bool {
235− const ONE_LINE: &[&str] = &["@page", "@meta", "@warn", "@assert", "@data", "@include"];
235+ const ONE_LINE: &[&str] = &["@page", "@meta", "@data", "@include"];
236236 let name = text.split([' ', '[']).next().unwrap_or("");
237237 (text.starts_with('@') || text.starts_with('[')) && !ONE_LINE.contains(&name)
238238}
examples/demo.hl+1−2
@@ -162,9 +162,8 @@
162162 ]
163163 $i
164164
165− -- @if / @else if / @else (with @assert to verify at compile time)
165+ -- @if / @else if / @else
166166 @let count 3
167− @assert $count == 3
168167 @row [spacing 6, align-items center]
169168 @text [bold, min-width 64] Count:
170169 @if $count == 0
src/bin/htmlang_lsp/docs.rs−2
@@ -108,8 +108,6 @@ pub(crate) const DIRECTIVES: &[Doc] = &[
108108 "Repeats its body for each item of a list or range; `$_index` counts from 0.",
109109 "@each $item in apple, banana\n @text $item\n@each $i in 1..10 step 2\n @text $i",
110110 ),
111− doc("assert", "Fails the build when the condition is false.", "@assert $spacing == 16"),
112− doc("warn", "Emits a compile-time warning.", "@warn Remember to set a title"),
113111 doc("data", "Loads a JSON file as variables.", "@data site.json"),
114112];
115113
src/upgrade.rs+15−17
@@ -204,6 +204,17 @@ pub fn upgrade(input: &str) -> Upgrade {
204204 continue;
205205 }
206206
207+ // Directives removed without a replacement: drop the line
208+ if attr_depth == 0
209+ && ["@assert ", "@warn ", "@debug ", "@log ", "@deprecated "]
210+ .iter()
211+ .any(|d| trimmed.starts_with(d) && !user_defined.contains(&&d[1..d.len() - 1]))
212+ {
213+ changes += 1;
214+ i += 1;
215+ continue;
216+ }
217+
207218 // --- Line rewrites ---
208219 let mut new_line = line.to_string();
209220 if attr_depth == 0 {
@@ -688,16 +699,6 @@ fn rewrite_directive_line(
688699 .unwrap_or(value);
689700 return format!("{pad}@meta og:{} {}", key, value);
690701 }
691− if let Some(rest) = trimmed.strip_prefix("@debug ") {
692− return format!("{pad}@warn {}", rest);
693− }
694− if let Some(rest) = trimmed.strip_prefix("@log ") {
695− let shown: Vec<String> = rest
696− .split_whitespace()
697− .map(|v| format!("{} = ${}", v.trim_start_matches('$'), v.trim_start_matches('$')))
698− .collect();
699− return format!("{pad}@warn {}", shown.join(", "));
700− }
701702 // `@let x = $a ~ " " ~ $b` → `@let x "$a $b"` (`~` was removed:
702703 // interpolated strings do the same)
703704 if let Some(rest) = trimmed.strip_prefix("@let ")
@@ -805,9 +806,6 @@ fn rewrite_directive_line(
805806 {pad}@head\n{pad} <link rel=\"preload\" href=\"{url}\" as=\"font\" crossorigin>"
806807 );
807808 }
808− if trimmed.starts_with("@deprecated ") {
809− return String::new();
810− }
811809 if trimmed.starts_with("@breakpoint ") {
812810 manual.push((
813811 idx + 1,
@@ -1209,7 +1207,7 @@ fn rename_filters(line: &str) -> String {
12091207}
12101208
12111209/// Turn `$name|filter:arg|...` chains into function calls: bare in
1212−/// expressions (`@if`, `@else if`, `@assert`, `@let x = ...`), and as
1210+/// expressions (`@if`, `@else if`, `@let x = ...`), and as
12131211/// `${...}` interpolation everywhere else.
12141212fn convert_filters(line: &str) -> String {
12151213 let line = rename_filters(line);
@@ -1217,7 +1215,7 @@ fn convert_filters(line: &str) -> String {
12171215 return line;
12181216 }
12191217 let trimmed = line.trim_start();
1220− let expression_line = ["@if ", "@else if ", "@assert "]
1218+ let expression_line = ["@if ", "@else if "]
12211219 .iter()
12221220 .any(|p| trimmed.starts_with(p))
12231221 || trimmed
@@ -1469,7 +1467,7 @@ mod tests {
14691467 "@page [lang en, favicon /f.png] Home\n@text hi\n"
14701468 );
14711469 assert_eq!(up("@og title \"My Page\""), "@meta og:title My Page");
1472− assert_eq!(up("@debug hi $x\n@log $a $b"), "@warn hi $x\n@warn a = $a, b = $b");
1470+ assert_eq!(up("@debug hi $x\n@log $a $b\n@assert $x == 1\n@warn hi\n@text x"), "@text x");
14731471 assert_eq!(up("@component card $t\n @text $t"), "@let card $t\n @text $t");
14741472 assert_eq!(up("@el [color $on ? green : gray]"), "@el [color if($on, green, gray)]");
14751473 let r = upgrade("@lang en\n@text hi");
@@ -1554,7 +1552,7 @@ mod tests {
15541552 up("@font-face Inter fonts/inter.woff2"),
15551553 "@style\n @font-face { font-family: 'Inter'; src: url('fonts/inter.woff2') format('woff2'); font-display: swap; }\n@head\n <link rel=\"preload\" href=\"fonts/inter.woff2\" as=\"font\" crossorigin>"
15561554 );
1557− assert_eq!(up("@deprecated old\n@let x 1"), "\n@let x 1");
1555+ assert_eq!(up("@deprecated old\n@let x 1"), "@let x 1");
15581556 assert_eq!(upgrade("@breakpoint tablet 600").manual.len(), 1);
15591557 }
15601558
tests/snapshots.rs−110
@@ -920,33 +920,6 @@ fn css_scroll_snap_align() {
920920 assert!(output.contains("scroll-snap-align:center"));
921921}
922922
923−// ---------------------------------------------------------------------------
924−// Feature tests: @warn / @debug
925−// ---------------------------------------------------------------------------
926−
927−#[test]
928−fn warn_produces_diagnostic() {
929− let diags = parse_diagnostics("@warn Something is wrong");
930− assert!(
931− diags.iter().any(|d| d.message == "Something is wrong"
932− && d.severity == htmlang::parser::Severity::Warning),
933− "expected @warn diagnostic, got: {:?}",
934− diags
935− );
936−}
937−
938−#[test]
939−fn warn_substitutes_variables() {
940− let diags = parse_diagnostics("@let name test\n@warn Missing $name value");
941− assert!(
942− diags
943− .iter()
944− .any(|d| d.message.contains("Missing test value")),
945− "expected substituted @warn, got: {:?}",
946− diags
947− );
948−}
949−
950923// ---------------------------------------------------------------------------
951924// Feature tests: image optimization hints
952925// ---------------------------------------------------------------------------
@@ -3694,64 +3667,6 @@ fn no_warning_used_mixin() {
36943667 );
36953668}
36963669
3697−// ---------------------------------------------------------------------------
3698−// @assert directive
3699−// ---------------------------------------------------------------------------
3700−
3701−#[test]
3702−fn snapshot_assert_directive() {
3703− snapshot_test("assert_directive");
3704−}
3705−
3706−#[test]
3707−fn assert_passes_no_error() {
3708− let diags = parse_diagnostics("@let x hello\n@assert $x == hello\n@el [padding 10]");
3709− assert!(
3710− !diags
3711− .iter()
3712− .any(|d| d.severity == htmlang::parser::Severity::Error),
3713− "passing assertion should produce no error, got: {:?}",
3714− diags
3715− );
3716−}
3717−
3718−#[test]
3719−fn assert_fails_produces_error() {
3720− let diags = parse_diagnostics("@let x hello\n@assert $x == world");
3721− assert!(
3722− diags
3723− .iter()
3724− .any(|d| d.severity == htmlang::parser::Severity::Error
3725− && d.message.contains("assertion failed")),
3726− "failing assertion should produce error, got: {:?}",
3727− diags
3728− );
3729−}
3730−
3731−#[test]
3732−fn assert_not_equal() {
3733− let diags = parse_diagnostics("@let x hello\n@assert $x != world\n@el");
3734− assert!(
3735− !diags
3736− .iter()
3737− .any(|d| d.severity == htmlang::parser::Severity::Error),
3738− "!= assertion should pass when values differ, got: {:?}",
3739− diags
3740− );
3741−}
3742−
3743−#[test]
3744−fn assert_truthy() {
3745− let diags = parse_diagnostics("@let x true\n@assert $x\n@el");
3746− assert!(
3747− !diags
3748− .iter()
3749− .any(|d| d.severity == htmlang::parser::Severity::Error),
3750− "truthy assertion should pass, got: {:?}",
3751− diags
3752− );
3753−}
3754−
37553670// ---------------------------------------------------------------------------
37563671// clamp() / min() / max() CSS functions
37573672// ---------------------------------------------------------------------------
@@ -4173,16 +4088,6 @@ fn test_error_circular_include() {
41734088 );
41744089}
41754090
4176−#[test]
4177−fn test_error_assert_failure() {
4178− let diags = parse_diagnostics("@let x 5\n@assert $x == 10");
4179− assert!(
4180− diags.iter().any(|d| d.message.contains("assertion failed")
4181− && d.severity == htmlang::parser::Severity::Error),
4182− "should report assertion failure as error"
4183− );
4184−}
4185−
41864091#[test]
41874092fn test_error_duplicate_attribute() {
41884093 let diags = parse_diagnostics("@el [padding 10, padding 20]\n Hello");
@@ -4250,21 +4155,6 @@ fn test_spread_define() {
42504155 );
42514156}
42524157
4253−#[test]
4254−fn test_log_directive() {
4255− // @log should not produce errors and should be consumed without output nodes
4256− let result = htmlang::parser::parse("@let x hello\n@warn x = $x\n@text $x");
4257− assert!(
4258− result
4259− .diagnostics
4260− .iter()
4261− .all(|d| d.severity != htmlang::parser::Severity::Error),
4262− "@warn should = $should, not = $not, produce = $produce, errors = $errors"
4263− );
4264− let html = htmlang::codegen::generate(&result.document);
4265− assert!(!html.contains("@log"), "@warn should = $should, not = $not, appear = $appear, in = $in, output = $output");
4266−}
4267−
42684158#[test]
42694159fn test_short_class_names() {
42704160 let html = compile("@el [padding 10]\n @el [padding 20]\n Hello");
tests/snapshots/assert_directive.hldeleted−10
@@ -1,10 +0,0 @@
1−@page Assert Test
2−
3−@let spacing 16
4−@let primary #3b82f6
5−
6−@assert $spacing == 16
7−@assert $primary != red
8−
9−@el [padding $spacing, color $primary]
10− Assertions passed
tests/snapshots/assert_directive.htmldeleted−1
@@ -1 +0,0 @@
1−<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Assert Test</title><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;padding:16px;color:#3b82f6;}}</style></head><body><div class="a"><span>Assertions passed</span></div></body></html>