on master help sign in
A smaller prelude: @spacer and $truncate
2f8138aiko
DESIGN.md+7−21
@@ -160,31 +160,17 @@ wins.
160
160
161### Standard library
161### Standard library
162
162
163−These components are written in htmlang (`std.hl`) and available in every file.
163+Two definitions are written in htmlang (`std.hl`) and available in every
164−Use them like elements; your own `@let` with the same name takes precedence.
164+file; your own `@let` with the same name takes precedence. `@spacer` takes up
165−
166−| Component | Purpose |
167−|---|---|
168−| `@badge` | Small pill for counts and statuses |
169−| `@tag` | Label with slightly rounded corners |
170−| `@chip` | Outlined, fully rounded label |
171−| `@avatar` | Circular frame for an image or initials |
172−| `@spacer` | Takes up the remaining space in a row or column |
165+the remaining space in a row or column, and the `$truncate` bundle cuts text
173−| `@tooltip [tip TEXT]` | Text that shows `TEXT` when hovered |
166+off at one line with an ellipsis. Components beyond that are yours to define
174−| `@carousel` | Horizontally scrolling row that snaps to each child |
167+with `@let`.
175−| `@breadcrumb` | Breadcrumb trail of `@li`s |
176−| `$skeleton`, `$truncate`, `$no-scrollbar` | Attribute bundles: loading placeholder, one-line ellipsis, hidden scrollbars |
177
168
178```
169```
179@row [spacing 8, align-items center]
170@row [spacing 8, align-items center]
180− @badge [background #ef4444, color white] 3
171+ @text Logo
181− @chip Rust
182 @spacer
172 @spacer
183− @tooltip [tip Opens in a new tab] Help
173+ @text [$truncate, max-width 200] A long title that gets cut off
184−@breadcrumb
185− @li > @link / Home
186− @li Docs
187−@el [$skeleton, height 20, width fill]
188```
174```
189
175
190## Attributes
176## Attributes
README.md+1−2
@@ -107,8 +107,7 @@ Prefixes apply a style conditionally:
107```
107```
108
108
109Functions are used like elements: extra attributes (here `padding 12`) style
109Functions are used like elements: extra attributes (here `padding 12`) style
110−their root element. A small standard library (`@badge`, `@chip`, `@tooltip`,
110+their root element.
111−`@carousel`, `@breadcrumb`, ...) is written the same way.
112
111
113Control flow runs at compile time:
112Control flow runs at compile time:
114
113
crates/htmlang-core/src/codegen.rs−10
@@ -837,16 +837,6 @@ fn build_element_css(doc: &Document, styles: &StyleCollector, dev: bool) -> Stri
837
837
838 element_css.push_str(&styles_css);
838 element_css.push_str(&styles_css);
839
839
840−
841− // Keyframes for the standard library's `$skeleton` bundle, when used
842− if element_css.contains("hl-skeleton") {
843− if dev {
844− element_css.push_str("@keyframes hl-skeleton {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n}\n");
845− } else {
846− element_css.push_str("@keyframes hl-skeleton{0%{background-position:200% 0}100%{background-position:-200% 0}}");
847− }
848− }
849−
850 // @style blocks (custom CSS)
840 // @style blocks (custom CSS)
851 for block in &doc.custom_css {
841 for block in &doc.custom_css {
852 if dev {
842 if dev {
crates/htmlang-core/src/parser.rs+4−4
@@ -1051,7 +1051,7 @@ impl Evaluator {
1051 }
1051 }
1052 caller_children.push(child);
1052 caller_children.push(child);
1053 }
1053 }
1054− // Text after the call, as in `@badge [color red] New`, is content:
1054+ // Text after the call, as in `@card [color red] New`, is content:
1055 // it goes first among the caller's children.
1055 // it goes first among the caller's children.
1056 let trailing_text = trailing_text.trim();
1056 let trailing_text = trailing_text.trim();
1057 if !trailing_text.is_empty() {
1057 if !trailing_text.is_empty() {
@@ -1085,7 +1085,7 @@ impl Evaluator {
1085 }
1085 }
1086 // Arguments that aren't parameters are attributes for the
1086 // Arguments that aren't parameters are attributes for the
1087 // function's root element, so a function can be styled like an
1087 // function's root element, so a function can be styled like an
1088− // element: `@badge [background red] New`.
1088+ // element: `@card [background red] New`.
1089 let forwarded: Vec<Attribute> = args
1089 let forwarded: Vec<Attribute> = args
1090 .iter()
1090 .iter()
1091 .zip(&consumed)
1091 .zip(&consumed)
@@ -1500,8 +1500,8 @@ const REMOVED_SYNTAX: &[(&str, &str)] = &[
1500/// Attributes that became standard-library bundles or plain CSS.
1500/// Attributes that became standard-library bundles or plain CSS.
1501fn removed_attribute_hint(name: &str) -> Option<&'static str> {
1501fn removed_attribute_hint(name: &str) -> Option<&'static str> {
1502 match name {
1502 match name {
1503− "skeleton" => Some("use the `$skeleton` bundle"),
1503+ "skeleton" => Some("write the animation in `@style`"),
1504− "no-scrollbar" => Some("use the `$no-scrollbar` bundle"),
1504+ "no-scrollbar" => Some("use `scrollbar-width none`"),
1505 "gradient" => Some("use `background linear-gradient(...)`"),
1505 "gradient" => Some("use `background linear-gradient(...)`"),
1506 "animate" => Some("use `animation`"),
1506 "animate" => Some("use `animation`"),
1507 "inset-area" => Some("use `position-area`"),
1507 "inset-area" => Some("use `position-area`"),
crates/htmlang-core/src/std.hl+1−45
@@ -1,53 +1,9 @@
1-- The htmlang standard library, available in every file. It is ordinary
1-- The htmlang standard library, available in every file. It is ordinary
2−-- htmlang: a file's own @let with the same name takes precedence, and
2+-- htmlang: a file's own @let with the same name takes precedence.
3−-- attributes on a call style the component's root element, so
4−-- `@badge [background red] New` works like a built-in element.
5−
6−-- Small pill for counts and statuses.
7−@let badge
8− @text [display inline-flex, align-items center, padding 2px 8px, font-size 0.75rem, font-weight 600, justify-content center, border-radius 9999px, line-height 1]
9− @children
10−
11−-- Label with slightly rounded corners.
12−@let tag
13− @text [display inline-flex, align-items center, padding 2px 8px, font-size 0.75rem, font-weight 600, border-radius 4px]
14− @children
15−
16−-- Outlined, fully rounded label.
17−@let chip
18− @text [display inline-flex, align-items center, column-gap 4px, padding 4px 12px, border-radius 9999px, font-size 0.875rem, border 1 solid currentColor]
19− @children
20−
21−-- Circular frame for an image or initials.
22−@let avatar
23− @el [display inline-flex, flex-direction row, align-items center, justify-content center, border-radius 9999px, overflow hidden, flex-shrink 0]
24− @children
25
3
26-- Takes up the remaining space in a row or column.
4-- Takes up the remaining space in a row or column.
27@let spacer
5@let spacer
28 @el [flex 1]
6 @el [flex 1]
29
7
30−-- Text that shows `tip` when hovered.
31−@let tooltip $tip
32− @text [position relative, cursor help, title=$tip]
33− @children
34−
35−-- Horizontally scrolling row that snaps to each child.
36−@let carousel
37− @row [overflow-x auto, scroll-snap-type x mandatory, scrollbar-width none, children:scroll-snap-align start, children:flex-shrink 0]
38− @children
39−
40−-- Breadcrumb trail: each crumb is an @li.
41−@let breadcrumb
42− @nav [aria-label=breadcrumb]
43− @ol [display flex, flex-direction row, flex-wrap wrap, gap 8px]
44− @children
45−
46−-- Loading placeholder: [$skeleton]
47−@let skeleton [background linear-gradient(90deg,#e5e7eb 25%,#f3f4f6 50%,#e5e7eb 75%), background-size 200% 100%, animation hl-skeleton 1.5s ease-in-out infinite]
48−
49-- Cut text off at one line with an ellipsis: [$truncate]
8-- Cut text off at one line with an ellipsis: [$truncate]
50@let truncate [overflow hidden, text-overflow ellipsis, white-space nowrap]
9@let truncate [overflow hidden, text-overflow ellipsis, white-space nowrap]
51−
52−-- Hide scrollbars but keep scrolling: [$no-scrollbar]
53−@let no-scrollbar [scrollbar-width none]
editors/vscode/snippets/htmlang.json−11
@@ -222,17 +222,6 @@
222 ],
222 ],
223 "description": "Fallback content when JavaScript is disabled"
223 "description": "Fallback content when JavaScript is disabled"
224 },
224 },
225− "Breadcrumb navigation": {
226− "prefix": "@breadcrumb",
227− "body": [
228− "@breadcrumb",
229− " @li > @link ${1:/} ${2:Home}",
230− " @li > @link ${3:/docs} ${4:Docs}",
231− " @li ${5:Current Page}",
232− " $0"
233− ],
234− "description": "Breadcrumb navigation with links"
235− },
236 "Address block": {
225 "Address block": {
237 "prefix": "@address",
226 "prefix": "@address",
238 "body": [
227 "body": [
src/bin/htmlang_lsp/completion.rs+1−1
@@ -948,7 +948,7 @@ mod tests {
948 let label = format!("@{}", name);
948 let label = format!("@{}", name);
949 assert!(elements.iter().any(|i| i.label == label), "missing {}", label);
949 assert!(elements.iter().any(|i| i.label == label), "missing {}", label);
950 }
950 }
951− assert!(elements.iter().any(|i| i.label == "@badge"));
951+ assert!(elements.iter().any(|i| i.label == "@spacer"));
952 let directives = directive_completions(range);
952 let directives = directive_completions(range);
953 for name in htmlang::parser::known_directives() {
953 for name in htmlang::parser::known_directives() {
954 assert!(directives.iter().any(|i| i.label == format!("@{}", name)));
954 assert!(directives.iter().any(|i| i.label == format!("@{}", name)));
src/bin/htmlang_lsp/docs.rs+4−12
@@ -127,26 +127,18 @@ pub(crate) const ATTRIBUTES: &[Doc] = &[
127 doc("grid-rows", "Number of equal grid rows.", "grid-rows 2"),
127 doc("grid-rows", "Number of equal grid rows.", "grid-rows 2"),
128 doc("col-span", "Columns a grid child spans.", "col-span 2"),
128 doc("col-span", "Columns a grid child spans.", "col-span 2"),
129 doc("row-span", "Rows a grid child spans.", "row-span 2"),
129 doc("row-span", "Rows a grid child spans.", "row-span 2"),
130− doc("line-height", "Line height; integers of 2 or more are px.", "line-height 1.5"),
130+ doc("line-height", "Line height; a bare number is a multiplier of the font size.", "line-height 1.5"),
131 doc("inline", "On `@image`: embed the file (SVG markup, or other images as base64) in the page.", "inline"),
131 doc("inline", "On `@image`: embed the file (SVG markup, or other images as base64) in the page.", "inline"),
132];
132];
133
133
134/// Standard-library components (`std.hl`), used like elements.
134/// Standard-library components (`std.hl`), used like elements.
135pub(crate) const COMPONENTS: &[Doc] = &[
135pub(crate) const COMPONENTS: &[Doc] = &[
136− doc("badge", "Small pill for counts and statuses.", "@badge [background red, color white] 3"),
137− doc("tag", "Label with slightly rounded corners.", "@tag v2.0"),
138− doc("chip", "Outlined, fully rounded label.", "@chip Rust"),
139− doc("avatar", "Circular frame for an image or initials.", "@avatar [width 48, height 48]\n AB"),
140 doc("spacer", "Takes up the remaining space in a row or column.", "@spacer"),
136 doc("spacer", "Takes up the remaining space in a row or column.", "@spacer"),
141− doc("tooltip", "Text that shows `tip` when hovered.", "@tooltip [tip More info] Hover me"),
142− doc("carousel", "Horizontally scrolling row that snaps to each child.", "@carousel [spacing 16]\n @el Slide 1"),
143− doc("breadcrumb", "Breadcrumb trail of `@li`s.", "@breadcrumb\n @li > @link / Home\n @li Docs"),
144];
137];
145
138
146/// Standard-library attribute bundles, used as `[$name]`.
139/// Standard-library attribute bundles, used as `[$name]`.
147pub(crate) const BUNDLES: &[Doc] = &[
140pub(crate) const BUNDLES: &[Doc] = &[
148− doc("skeleton", "Loading placeholder with a shimmer animation.", "@el [$skeleton, height 20]"),
141+ doc("truncate", "Cuts text off at one line with an ellipsis.", "@text [$truncate] A long title"),
149− doc("no-scrollbar", "Hides scrollbars while keeping scrolling.", "@el [$no-scrollbar, overflow-x auto]"),
150];
142];
151
143
152fn find(table: &'static [Doc], name: &str) -> Option<&'static Doc> {
144fn find(table: &'static [Doc], name: &str) -> Option<&'static Doc> {
@@ -351,8 +343,8 @@ mod tests {
351 assert!(hover("required").unwrap().contains("Boolean"));
343 assert!(hover("required").unwrap().contains("Boolean"));
352 assert!(hover("hover:color").unwrap().contains(":hover"));
344 assert!(hover("hover:color").unwrap().contains(":hover"));
353 assert!(hover("md:").unwrap().contains("viewport"));
345 assert!(hover("md:").unwrap().contains("viewport"));
354− assert!(hover("@badge").unwrap().contains("standard library"));
346+ assert!(hover("@spacer").unwrap().contains("standard library"));
355− assert!(hover("$skeleton").unwrap().contains("bundle"));
347+ assert!(hover("$truncate").unwrap().contains("bundle"));
356 assert!(hover("@nav").unwrap().contains("<nav>"));
348 assert!(hover("@nav").unwrap().contains("<nav>"));
357 }
349 }
358}
350}
src/upgrade.rs+11−42
@@ -492,7 +492,7 @@ fn rewrite_block(
492 }
492 }
493
493
494 // @tooltip TEXT (text shown and used as the hover tip) →
494 // @tooltip TEXT (text shown and used as the hover tip) →
495− // @tooltip [tip TEXT] TEXT
495+ // @text [title=TEXT] TEXT
496 if let Some(rest) = trimmed.strip_prefix("@tooltip ")
496 if let Some(rest) = trimmed.strip_prefix("@tooltip ")
497 && !user_defined.contains(&"tooltip")
497 && !user_defined.contains(&"tooltip")
498 && !has_attr_key(rest, "tip")
498 && !has_attr_key(rest, "tip")
@@ -508,42 +508,14 @@ fn rewrite_block(
508 return None;
508 return None;
509 }
509 }
510 let attrs = match attrs {
510 let attrs = match attrs {
511− Some(a) if !a.trim().is_empty() => format!("[tip {}, {}]", text, a.trim()),
511+ Some(a) if !a.trim().is_empty() => format!("[title={}, {}]", text, a.trim()),
512− _ => format!("[tip {}]", text),
512+ _ => format!("[title={}]", text),
513 };
513 };
514− let mut out = vec![format!("{pad}@tooltip {} {}", attrs, text)];
514+ let mut out = vec![format!("{pad}@text {} {}", attrs, text)];
515 out.extend(body.iter().map(|l| l.to_string()));
515 out.extend(body.iter().map(|l| l.to_string()));
516 return Some(Block { lines: out, end });
516 return Some(Block { lines: out, end });
517 }
517 }
518
518
519− // @breadcrumb: each crumb is now an explicit @li
520− if (trimmed == "@breadcrumb" || trimmed.starts_with("@breadcrumb "))
521− && !user_defined.contains(&"breadcrumb")
522− {
523− let child_indent = body.iter().find(|l| !l.trim().is_empty()).map(|l| indent_of(l));
524− let is_item = |t: &str| t == "@li" || t.starts_with("@li ") || t.starts_with("@li[");
525− if body.iter().all(|l| {
526− l.trim().is_empty() || Some(indent_of(l)) != child_indent || is_item(l.trim())
527− }) {
528− return None;
529− }
530− let mut out = vec![line.to_string()];
531− for l in body {
532− let t = l.trim();
533− if Some(indent_of(l)) == child_indent && !t.is_empty() && !is_item(t) {
534− let wrapped = if t.starts_with('@') {
535− format!("@li > {}", t)
536− } else {
537− format!("@li {}", t)
538− };
539− out.push(format!("{}{}", " ".repeat(indent_of(l)), wrapped));
540− } else {
541− out.push(l.to_string());
542− }
543− }
544− return Some(Block { lines: out, end });
545− }
546−
547 // @switch $v / @case x [attrs] → @match $v / @case x / @let __switch [attrs]
519 // @switch $v / @case x [attrs] → @match $v / @case x / @let __switch [attrs]
548 if let Some(rest) = trimmed.strip_prefix("@switch ") {
520 if let Some(rest) = trimmed.strip_prefix("@switch ") {
549 let mut out = vec![format!("{pad}@match {}", rest.trim())];
521 let mut out = vec![format!("{pad}@match {}", rest.trim())];
@@ -1190,7 +1162,8 @@ const HTML_ATTRIBUTES: &[&str] = &[
1190 "step", "tabindex", "target", "title", "translate", "type", "value",
1162 "step", "tabindex", "target", "title", "translate", "type", "value",
1191];
1163];
1192
1164
1193−/// Standard-library components, which forward attributes to an element.
1165+/// Components the standard library had, which forwarded attributes to an
1166+/// element.
1194const STD_COMPONENTS: &[&str] = &[
1167const STD_COMPONENTS: &[&str] = &[
1195 "badge", "tag", "chip", "avatar", "spacer", "tooltip", "carousel", "breadcrumb",
1168 "badge", "tag", "chip", "avatar", "spacer", "tooltip", "carousel", "breadcrumb",
1196];
1169];
@@ -1322,7 +1295,8 @@ fn rewrite_attr_list(list: &str, element_attrs: bool) -> String {
1322 let body = body.strip_prefix("...$").map_or(body.to_string(), |b| format!("${b}"));
1295 let body = body.strip_prefix("...$").map_or(body.to_string(), |b| format!("${b}"));
1323 // Built-in style attributes that became standard-library bundles
1296 // Built-in style attributes that became standard-library bundles
1324 let body = match body.trim_end() {
1297 let body = match body.trim_end() {
1325− "skeleton" | "no-scrollbar" | "truncate" => format!("${}", body),
1298+ "truncate" => format!("${}", body),
1299+ "no-scrollbar" => "scrollbar-width none".to_string(),
1326 "grid" => "display grid".to_string(),
1300 "grid" => "display grid".to_string(),
1327 _ => body,
1301 _ => body,
1328 };
1302 };
@@ -1764,20 +1738,15 @@ mod tests {
1764 #[test]
1738 #[test]
1765 fn standard_library_migrations() {
1739 fn standard_library_migrations() {
1766 assert_eq!(
1740 assert_eq!(
1767− up("@el [skeleton, height 20]\n@el [no-scrollbar]"),
1741+ up("@el [truncate, height 20]\n@el [no-scrollbar]"),
1768− "@el [$skeleton, height 20]\n@el [$no-scrollbar]"
1742+ "@el [$truncate, height 20]\n@el [scrollbar-width none]"
1769 );
1743 );
1770 assert_eq!(
1744 assert_eq!(
1771 up("@el [gradient #f00 #00f 45deg, padding 4]"),
1745 up("@el [gradient #f00 #00f 45deg, padding 4]"),
1772 "@el [background linear-gradient(45deg,#f00,#00f), padding 4]"
1746 "@el [background linear-gradient(45deg,#f00,#00f), padding 4]"
1773 );
1747 );
1774− assert_eq!(up("@tooltip Hover me"), "@tooltip [tip Hover me] Hover me");
1748+ assert_eq!(up("@tooltip Hover me"), "@text [title=Hover me] Hover me");
1775− assert_eq!(up("@tooltip A tooltip text"), "@tooltip [tip A tooltip text] A tooltip text");
1776 assert_eq!(up("@tooltip [tip X] Y"), "@tooltip [tip X] Y");
1749 assert_eq!(up("@tooltip [tip X] Y"), "@tooltip [tip X] Y");
1777− assert_eq!(
1778− up("@breadcrumb\n @link / Home\n Current"),
1779− "@breadcrumb\n @li > @link / Home\n @li Current"
1780− );
1781 }
1750 }
1782
1751
1783 #[test]
1752 #[test]
tests/snapshots.rs+4−145
@@ -2488,39 +2488,8 @@ fn element_spacer() {
2488
2488
2489// --- Badge element ---
2489// --- Badge element ---
2490
2490
2491−#[test]
2492−fn element_badge() {
2493− let output = compile("@page T\n@badge [background red, color white] 3");
2494− assert!(
2495− output.contains("<span"),
2496− "badge renders as span: {}",
2497− output
2498− );
2499− assert!(
2500− output.contains("border-radius:9999px"),
2501− "badge should be pill-shaped: {}",
2502− output
2503− );
2504− assert!(output.contains("3"), "badge content: {}", output);
2505−}
2506−
2507// --- Tooltip element ---
2491// --- Tooltip element ---
2508
2492
2509−#[test]
2510−fn element_tooltip() {
2511− let output = compile("@page T\n@tooltip [tip Hover for info]\n @text Help");
2512− assert!(
2513− output.contains("title=\"Hover for info\""),
2514− "tooltip should have title attr: {}",
2515− output
2516− );
2517− assert!(
2518− output.contains("cursor:help"),
2519− "tooltip should have cursor:help: {}",
2520− output
2521− );
2522−}
2523−
2524// --- @each step ---
2493// --- @each step ---
2525
2494
2526#[test]
2495#[test]
@@ -2695,7 +2664,7 @@ fn no_warning_direction_attr() {
2695
2664
2696#[test]
2665#[test]
2697fn no_warning_new_elements() {
2666fn no_warning_new_elements() {
2698− // Grid, stack, spacer, badge, tooltip should all parse without errors
2667+ // Grid and spacer parse without errors
2699 let diags = parse_diagnostics("@grid\n @text A");
2668 let diags = parse_diagnostics("@grid\n @text A");
2700 assert!(
2669 assert!(
2701 !diags
2670 !diags
@@ -2712,22 +2681,6 @@ fn no_warning_new_elements() {
2712 "spacer should parse, got: {:?}",
2681 "spacer should parse, got: {:?}",
2713 diags
2682 diags
2714 );
2683 );
2715− let diags = parse_diagnostics("@badge [background red] 5");
2716− assert!(
2717− !diags
2718− .iter()
2719− .any(|d| d.severity == htmlang::parser::Severity::Error),
2720− "badge should parse, got: {:?}",
2721− diags
2722− );
2723− let diags = parse_diagnostics("@tooltip Help text\n @text Hover");
2724− assert!(
2725− !diags
2726− .iter()
2727− .any(|d| d.severity == htmlang::parser::Severity::Error),
2728− "tooltip should parse, got: {:?}",
2729− diags
2730− );
2731}
2684}
2732
2685
2733// --- New feature tests ---
2686// --- New feature tests ---
@@ -2737,11 +2690,6 @@ fn snapshot_variable_filters() {
2737 snapshot_test("variable_filters");
2690 snapshot_test("variable_filters");
2738}
2691}
2739
2692
2740−#[test]
2741−fn snapshot_new_elements_5() {
2742− snapshot_test("new_elements_5");
2743−}
2744−
2745#[test]
2693#[test]
2746fn snapshot_css_shorthands() {
2694fn snapshot_css_shorthands() {
2747 snapshot_test("css_shorthands");
2695 snapshot_test("css_shorthands");
@@ -2804,49 +2752,6 @@ fn test_variable_filters() {
2804 );
2752 );
2805}
2753}
2806
2754
2807−#[test]
2808−fn test_new_elements_parse() {
2809− // Avatar
2810− let diags = parse_diagnostics("@avatar [width 48, height 48]\n @text AB");
2811− assert!(
2812− !diags
2813− .iter()
2814− .any(|d| d.severity == htmlang::parser::Severity::Error),
2815− "avatar: {:?}",
2816− diags
2817− );
2818−
2819− // Carousel
2820− let diags = parse_diagnostics("@carousel [gap 16]\n @el Slide 1");
2821− assert!(
2822− !diags
2823− .iter()
2824− .any(|d| d.severity == htmlang::parser::Severity::Error),
2825− "carousel: {:?}",
2826− diags
2827− );
2828−
2829− // Chip
2830− let diags = parse_diagnostics("@chip [background #eee] Tag");
2831− assert!(
2832− !diags
2833− .iter()
2834− .any(|d| d.severity == htmlang::parser::Severity::Error),
2835− "chip: {:?}",
2836− diags
2837− );
2838−
2839− // Tag
2840− let diags = parse_diagnostics("@tag [color blue] v1.0");
2841− assert!(
2842− !diags
2843− .iter()
2844− .any(|d| d.severity == htmlang::parser::Severity::Error),
2845− "tag: {:?}",
2846− diags
2847− );
2848−}
2849−
2850#[test]
2755#[test]
2851fn test_css_shorthands_output() {
2756fn test_css_shorthands_output() {
2852 let result = htmlang::parser::parse("@text [$truncate] Hello");
2757 let result = htmlang::parser::parse("@text [$truncate] Hello");
@@ -2885,27 +2790,6 @@ fn test_css_shorthands_output() {
2885 html
2790 html
2886 );
2791 );
2887
2792
2888− let result = htmlang::parser::parse("@el [$no-scrollbar] Content");
2889− let html = htmlang::codegen::generate(&result.document);
2890− assert!(
2891− html.contains("scrollbar-width:none"),
2892− "no-scrollbar should work, got: {}",
2893− html
2894− );
2895−
2896− let result = htmlang::parser::parse("@el [$skeleton, width 100, height 20] Content");
2897− let html = htmlang::codegen::generate(&result.document);
2898− assert!(
2899− html.contains("hl-skeleton"),
2900− "skeleton should add animation, got: {}",
2901− html
2902− );
2903− assert!(
2904− html.contains("@keyframes hl-skeleton"),
2905− "skeleton should add keyframes, got: {}",
2906− html
2907− );
2908−
2909 // `gradient` became plain CSS: the compiler points to the replacement.
2793 // `gradient` became plain CSS: the compiler points to the replacement.
2910 let result = htmlang::parser::parse("@el [gradient #fff #000] Content");
2794 let result = htmlang::parser::parse("@el [gradient #fff #000] Content");
2911 assert!(
2795 assert!(
@@ -2918,17 +2802,6 @@ fn test_css_shorthands_output() {
2918 );
2802 );
2919}
2803}
2920
2804
2921−#[test]
2922−fn test_carousel_children_snap() {
2923− let result = htmlang::parser::parse("@carousel\n @el A\n @el B");
2924− let html = htmlang::codegen::generate(&result.document);
2925− assert!(
2926− html.contains("scroll-snap-align:start"),
2927− "carousel children should have snap-align, got: {}",
2928− html
2929− );
2930−}
2931−
2932#[test]
2805#[test]
2933fn test_import_missing_file() {
2806fn test_import_missing_file() {
2934 // We can't test @use with actual files in unit tests easily, but we can verify
2807 // We can't test @use with actual files in unit tests easily, but we can verify
@@ -3306,11 +3179,6 @@ fn snapshot_script_element() {
3306 snapshot_test("script_element");
3179 snapshot_test("script_element");
3307}
3180}
3308
3181
3309−#[test]
3310−fn snapshot_breadcrumb() {
3311− snapshot_test("breadcrumb");
3312−}
3313−
3314#[test]
3182#[test]
3315fn snapshot_new_directives() {
3183fn snapshot_new_directives() {
3316 snapshot_test("new_directives");
3184 snapshot_test("new_directives");
@@ -3368,14 +3236,6 @@ fn search_element() {
3368 assert!(output.contains("<search>"), "search: {}", output);
3236 assert!(output.contains("<search>"), "search: {}", output);
3369}
3237}
3370
3238
3371−#[test]
3372−fn breadcrumb_generates_nav_ol() {
3373− let output = compile("@breadcrumb\n @li > @text Home\n @li About");
3374− assert!(output.contains("aria-label=\"breadcrumb\""), "nav: {}", output);
3375− assert!(output.contains("<ol class="), "ol: {}", output);
3376− assert!(output.contains("<li class="), "li: {}", output);
3377−}
3378−
3379#[test]
3239#[test]
3380fn canonical_directive() {
3240fn canonical_directive() {
3381 let output = compile("@page [canonical https://example.com/page] T\n@text Hello");
3241 let output = compile("@page [canonical https://example.com/page] T\n@text Hello");
@@ -4819,13 +4679,12 @@ fn children_prefix_styles_direct_children() {
4819
4679
4820#[test]
4680#[test]
4821fn standard_library_components_compile_cleanly() {
4681fn standard_library_components_compile_cleanly() {
4822− let src = "@badge [background red] 3\n@tag v2\n@chip Rust\n@avatar\n AB\n@spacer\n@tooltip [tip More] Hover\n@carousel\n @el A\n@breadcrumb\n @li Home\n@el [$skeleton]\n@el [$no-scrollbar]";
4682+ let src = "@row\n @text A\n @spacer\n @text [$truncate] B";
4823 let diags = parse_diagnostics(src);
4683 let diags = parse_diagnostics(src);
4824 assert!(diags.is_empty(), "standard library produced diagnostics: {:?}", diags);
4684 assert!(diags.is_empty(), "standard library produced diagnostics: {:?}", diags);
4825 let output = compile(src);
4685 let output = compile(src);
4826− assert!(output.contains("<span class=\"a\">3</span>"), "{}", output);
4686+ assert!(output.contains("flex:1"), "{}", output);
4827− assert!(output.contains("title=\"More\""), "{}", output);
4828− assert!(output.contains("@keyframes hl-skeleton"), "{}", output);
4687+ assert!(output.contains("text-overflow:ellipsis"), "{}", output);
4829}
4688}
4830
4689
4831#[test]
4690#[test]
tests/snapshots/breadcrumb.hldeleted−4
@@ -1,4 +0,0 @@
1−@breadcrumb [spacing 10]
2− @li > @link / Home
3− @li > @link /docs Docs
4− @li > @text Current Page
tests/snapshots/breadcrumb.htmldeleted−1
@@ -1 +0,0 @@
1−<style>@layer htmlang{.a{display:flex;flex-direction:column;gap:10px;}.b{margin:0;padding-left:0;list-style:none;display:flex;flex-direction:row;flex-wrap:wrap;gap:8px;}.c{display:flex;flex-direction:column;}}</style><nav class="a" aria-label="breadcrumb"><ol class="b"><li class="c"><a href="/">Home</a></li><li class="c"><a href="/docs">Docs</a></li><li class="c"><span>Current Page</span></li></ol></nav>
tests/snapshots/css_shorthands.hl+1−4
@@ -17,15 +17,12 @@
17 Frosted glass effect
17 Frosted glass effect
18
18
19 -- No scrollbar
19 -- No scrollbar
20− @el [overflow-x auto, $no-scrollbar, max-width 300]
20+ @el [overflow-x auto, scrollbar-width none, max-width 300]
21 @row [spacing 20]
21 @row [spacing 20]
22 @el [width 200, padding 20] Item 1
22 @el [width 200, padding 20] Item 1
23 @el [width 200, padding 20] Item 2
23 @el [width 200, padding 20] Item 2
24 @el [width 200, padding 20] Item 3
24 @el [width 200, padding 20] Item 3
25
25
26− -- Skeleton
27− @el [width fill, height 20, border-radius 4, $skeleton]
28−
29 -- Gradient
26 -- Gradient
30 @el [background linear-gradient(#3b82f6,#8b5cf6), padding 20, border-radius 8]
27 @el [background linear-gradient(#3b82f6,#8b5cf6), padding 20, border-radius 8]
31 @text [color white] Gradient background
28 @text [color white] Gradient background
tests/snapshots/css_shorthands.html+1−1
@@ -1 +1 @@
1−<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>CSS Shorthands</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;gap:20px;}.b{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}.c{margin:0;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;}.d{display:flex;flex-direction:column;filter:blur(4px);padding:20px;}.e{display:flex;flex-direction:column;backdrop-filter:blur(10px);padding:20px;background:rgba(255,255,255,0.5);}.f{display:flex;flex-direction:column;overflow-x:auto;scrollbar-width:none;max-width:300px;}.g{display:flex;flex-direction:row;gap:20px;}.h{display:flex;flex-direction:column;width:200px;padding:20px;}.i{display:flex;flex-direction:column;width:100%;height:20px;border-radius:4px;background:linear-gradient(90deg,#e5e7eb 25%,#f3f4f6 50%,#e5e7eb 75%);background-size:200% 100%;animation:hl-skeleton 1.5s ease-in-out infinite;}.j{display:flex;flex-direction:column;background:linear-gradient(#3b82f6,#8b5cf6);padding:20px;border-radius:8px;}.k{color:white;}.l{display:flex;flex-direction:column;background:linear-gradient(45deg,#ff6b6b,#feca57);padding:20px;border-radius:8px;}}@keyframes hl-skeleton{0%{background-position:200% 0}100%{background-position:-200% 0}}</style></head><body><div class="a"><span class="b">This text will be truncated with an ellipsis</span><p class="c">This is a long paragraph that spans multiple lines and should be clamped at three lines.</p><div class="d"><span>Blurred content</span></div><div class="e"><span>Frosted glass effect</span></div><div class="f"><div class="g"><div class="h"><span>Item 1</span></div><div class="h"><span>Item 2</span></div><div class="h"><span>Item 3</span></div></div></div><div class="i"></div><div class="j"><span class="k">Gradient background</span></div><div class="l"><span class="k">Angled gradient</span></div></div></body></html>
1+<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>CSS Shorthands</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;gap:20px;}.b{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}.c{margin:0;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;}.d{display:flex;flex-direction:column;filter:blur(4px);padding:20px;}.e{display:flex;flex-direction:column;backdrop-filter:blur(10px);padding:20px;background:rgba(255,255,255,0.5);}.f{display:flex;flex-direction:column;overflow-x:auto;scrollbar-width:none;max-width:300px;}.g{display:flex;flex-direction:row;gap:20px;}.h{display:flex;flex-direction:column;width:200px;padding:20px;}.i{display:flex;flex-direction:column;background:linear-gradient(#3b82f6,#8b5cf6);padding:20px;border-radius:8px;}.j{color:white;}.k{display:flex;flex-direction:column;background:linear-gradient(45deg,#ff6b6b,#feca57);padding:20px;border-radius:8px;}}</style></head><body><div class="a"><span class="b">This text will be truncated with an ellipsis</span><p class="c">This is a long paragraph that spans multiple lines and should be clamped at three lines.</p><div class="d"><span>Blurred content</span></div><div class="e"><span>Frosted glass effect</span></div><div class="f"><div class="g"><div class="h"><span>Item 1</span></div><div class="h"><span>Item 2</span></div><div class="h"><span>Item 3</span></div></div></div><div class="i"><span class="j">Gradient background</span></div><div class="k"><span class="j">Angled gradient</span></div></div></body></html>
tests/snapshots/new_elements_4.hl+2−11
@@ -21,15 +21,6 @@
21 @spacer
21 @spacer
22 @text Right
22 @text Right
23
23
24−-- Badge element
24+-- A hover tip is the title attribute
25−@row [spacing 8]
26− @badge [background #ef4444, color white] 3
27− @badge [background #22c55e, color white] New
28− @badge [background #3b82f6, color white] Pro
29−
30−-- Tooltip element
31@row [spacing 16]
25@row [spacing 16]
32− @tooltip [tip Hover me for info] Hover me for info
33− @text [text-decoration underline] Helpful text
26+ @text [title=Hover me for info, text-decoration underline] Helpful text
34− @tooltip [tip This is a tooltip message] This is a tooltip message
35− @text [font-weight bold] Important
tests/snapshots/new_elements_4.html+1−1
@@ -1 +1 @@
1−<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>New Elements 4</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:grid;grid-template-columns:repeat(3,1fr);gap:16px;}.b{display:flex;flex-direction:column;background:#f0f0f0;padding:16px;}.c{display:flex;flex-direction:column;background:#e0e0e0;padding:16px;}.d{display:flex;flex-direction:column;background:#d0d0d0;padding:16px;}.e{display:flex;flex-direction:column;position:relative;width:200px;height:200px;}.f{display:flex;flex-direction:column;position:absolute;inset:0;background:#3b82f6;}.g{display:flex;flex-direction:column;position:absolute;bottom:10px;right:10px;}.h{color:white;}.i{display:flex;flex-direction:row;height:100px;}.j{display:flex;flex-direction:column;flex:1;}.k{display:flex;flex-direction:row;gap:8px;}.l{display:inline-flex;align-items:center;padding:2px 8px;font-size:0.75rem;font-weight:600;justify-content:center;border-radius:9999px;line-height:1;background:#ef4444;color:white;}.m{display:inline-flex;align-items:center;padding:2px 8px;font-size:0.75rem;font-weight:600;justify-content:center;border-radius:9999px;line-height:1;background:#22c55e;color:white;}.n{display:inline-flex;align-items:center;padding:2px 8px;font-size:0.75rem;font-weight:600;justify-content:center;border-radius:9999px;line-height:1;background:#3b82f6;color:white;}.o{display:flex;flex-direction:row;gap:16px;}.p{position:relative;cursor:help;}.q{text-decoration:underline;}.r{font-weight:bold;}}</style></head><body><div class="a"><div class="b"><span>First</span></div><div class="c"><span>Second</span></div><div class="d"><span>Third</span></div></div><div class="e"><div class="f"></div><div class="g"><span class="h">Overlay</span></div></div><div class="i"><span>Left</span><div class="j"></div><span>Right</span></div><div class="k"><span class="l">3</span><span class="m">New</span><span class="n">Pro</span></div><div class="o"><span class="p" title="Hover me for info">Hover me for info<span class="q">Helpful text</span></span><span class="p" title="This is a tooltip message">This is a tooltip message<span class="r">Important</span></span></div></body></html>
1+<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>New Elements 4</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:grid;grid-template-columns:repeat(3,1fr);gap:16px;}.b{display:flex;flex-direction:column;background:#f0f0f0;padding:16px;}.c{display:flex;flex-direction:column;background:#e0e0e0;padding:16px;}.d{display:flex;flex-direction:column;background:#d0d0d0;padding:16px;}.e{display:flex;flex-direction:column;position:relative;width:200px;height:200px;}.f{display:flex;flex-direction:column;position:absolute;inset:0;background:#3b82f6;}.g{display:flex;flex-direction:column;position:absolute;bottom:10px;right:10px;}.h{color:white;}.i{display:flex;flex-direction:row;height:100px;}.j{display:flex;flex-direction:column;flex:1;}.k{display:flex;flex-direction:row;gap:16px;}.l{text-decoration:underline;}}</style></head><body><div class="a"><div class="b"><span>First</span></div><div class="c"><span>Second</span></div><div class="d"><span>Third</span></div></div><div class="e"><div class="f"></div><div class="g"><span class="h">Overlay</span></div></div><div class="i"><span>Left</span><div class="j"></div><span>Right</span></div><div class="k"><span class="l" title="Hover me for info">Helpful text</span></div></body></html>
tests/snapshots/new_elements_5.hldeleted−19
@@ -1,19 +0,0 @@
1−@page New Elements 5
2−
3−@el [spacing 20]
4− -- Avatar
5− @avatar [width 48, height 48, background #e5e7eb]
6− @text AB
7−
8− -- Carousel
9− @carousel [gap 16]
10− @el [width 200, padding 20, background #f3f4f6, border-radius 8]
11− Slide 1
12− @el [width 200, padding 20, background #f3f4f6, border-radius 8]
13− Slide 2
14−
15− -- Chip
16− @chip [background #e5e7eb] Category
17−
18− -- Tag
19− @tag [background #dbeafe, color #1e40af] v2.0
tests/snapshots/new_elements_5.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>New Elements 5</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;gap:20px;}.b{display:inline-flex;flex-direction:row;align-items:center;justify-content:center;border-radius:9999px;overflow:hidden;flex-shrink:0;width:48px;height:48px;background:#e5e7eb;}.c{display:flex;flex-direction:row;overflow-x:auto;scroll-snap-type:x mandatory;scrollbar-width:none;gap:16px;}.d{display:flex;flex-direction:column;width:200px;padding:20px;background:#f3f4f6;border-radius:8px;}.e{display:inline-flex;align-items:center;column-gap:4px;padding:4px 12px;border-radius:9999px;font-size:0.875rem;border:1px solid currentColor;background:#e5e7eb;}.f{display:inline-flex;align-items:center;padding:2px 8px;font-size:0.75rem;font-weight:600;border-radius:4px;background:#dbeafe;color:#1e40af;}.c > *{scroll-snap-align:start;flex-shrink:0;}}</style></head><body><div class="a"><div class="b"><span>AB</span></div><div class="c"><div class="d"><span>Slide 1</span></div><div class="d"><span>Slide 2</span></div></div><span class="e">Category</span><span class="f">v2.0</span></div></body></html>