on master help sign in
The output is what you wrote

Links and images no longer gain attributes nobody wrote: no automatic target/rel on external links, no image preloads or fetchpriority, no width/height/aspect-ratio read from image files, and no srcset from a file-naming convention (the responsive attribute; write srcset= and sizes=). An @main no longer adds a 'Skip to content' link. This drops the image-format readers from code generation.

commit0234fb8c1811ac0d7a1069c9f8ec34535c694e1b authoriko <mail@iko.soy> dateSat 26 Sep 2026, 03:52 (2 weeks ago) parent580497e
19 files changed+30−465
crates/htmlang-core/src/ast.rs−8 crates/htmlang-core/src/codegen.rs+5−300 crates/htmlang-core/src/parser.rs−32 crates/htmlang-core/src/vocab.rs+1−1 examples/demo.html+2−2 src/bin/htmlang_lsp/docs.rs−1 tests/regressions.rs+11 tests/snapshots.rs−110 tests/snapshots/accessibility.html+1−1 tests/snapshots/basic_elements.html+1−1 tests/snapshots/chain_operator.html+1−1 tests/snapshots/new_css.html+1−1 tests/snapshots/new_elements.html+1−1 tests/snapshots/new_elements_2.html+1−1 tests/snapshots/new_html_attrs.html+1−1 tests/snapshots/new_pseudos.html+1−1 tests/snapshots/responsive_images.hl+1−1 tests/snapshots/responsive_images.html+1−1 tests/snapshots/semantic_elements.html+1−1
crates/htmlang-core/src/ast.rs−8
@@ -15,17 +15,9 @@ pub struct Document {
1515 pub og_tags: Vec<(String, String)>,
1616 pub canonical: Option<String>,
1717 pub base_url: Option<String>,
18− pub preload_hints: Vec<PreloadHint>,
1918 pub nodes: Vec<Node>,
2019}
2120
22−#[derive(Debug, Clone)]
23−pub struct PreloadHint {
24− pub href: String,
25− pub as_type: String, // "font", "image", "style", "script"
26− pub crossorigin: bool,
27−}
28−
2921#[derive(Debug, Clone)]
3022pub enum Node {
3123 Element(Element),
crates/htmlang-core/src/codegen.rs+5−300
@@ -417,7 +417,6 @@ fn emit_media_block(
417417struct GenContext {
418418 dev: bool,
419419 depth: usize,
420− image_count: usize,
421420 has_interactive: bool,
422421}
423422
@@ -587,24 +586,11 @@ fn generate_full_inner(doc: &Document, dev: bool) -> String {
587586 let mut ctx = GenContext {
588587 dev,
589588 depth: 0,
590− image_count: 0,
591589 has_interactive: false,
592590 };
593591
594− // Check if document has @main for skip-to-content link
595− let has_main = has_tag(&doc.nodes, "main");
596−
597592 let mut body = String::new();
598593
599− // Skip-to-content link for accessibility (only when @main exists)
600− if has_main {
601− if dev {
602− body.push_str("<a href=\"#hl-main\" class=\"hl-skip\">Skip to content</a>\n");
603− } else {
604− body.push_str("<a href=\"#hl-main\" class=\"hl-skip\">Skip to content</a>");
605− }
606− }
607−
608594 for node in &doc.nodes {
609595 generate_node(node, None, &mut body, &mut styles, &mut ctx);
610596 }
@@ -735,27 +721,6 @@ fn generate_full_inner(doc: &Document, dev: bool) -> String {
735721 None => String::new(),
736722 };
737723
738− // Preload hints
739− let mut preload_html = String::new();
740− // Explicit preload hints from the document
741− for hint in &doc.preload_hints {
742− if dev {
743− preload_html.push_str(&format!(
744− "<link rel=\"preload\" href=\"{}\" as=\"{}\"{}>\n",
745− html_escape(&hint.href),
746− hint.as_type,
747− if hint.crossorigin { " crossorigin" } else { "" }
748− ));
749− } else {
750− preload_html.push_str(&format!(
751− "<link rel=\"preload\" href=\"{}\" as=\"{}\"{}>",
752− html_escape(&hint.href),
753− hint.as_type,
754− if hint.crossorigin { " crossorigin" } else { "" }
755− ));
756− }
757− }
758−
759724 // Focus-visible CSS for interactive elements (accessibility)
760725 let focus_visible_css = if ctx.has_interactive {
761726 if dev {
@@ -767,18 +732,7 @@ fn generate_full_inner(doc: &Document, dev: bool) -> String {
767732 ""
768733 };
769734
770− // Skip-to-content CSS (visually hidden but accessible)
771− let skip_link_css = if has_main {
772− if dev {
773− ".hl-skip { position: absolute; left: -9999px; top: auto; width: 1px; height: 1px; overflow: hidden; z-index: 9999; padding: 8px 16px; background: #000; color: #fff; text-decoration: none; font-size: 14px; }\n.hl-skip:focus { left: 8px; top: 8px; width: auto; height: auto; overflow: visible; }\n"
774− } else {
775− ".hl-skip{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;z-index:9999;padding:8px 16px;background:#000;color:#fff;text-decoration:none;font-size:14px}.hl-skip:focus{left:8px;top:8px;width:auto;height:auto;overflow:visible}"
776− }
777− } else {
778− ""
779− };
780−
781− let reset_css = reset_css(dev, focus_visible_css, skip_link_css);
735+ let reset_css = reset_css(dev, focus_visible_css);
782736
783737 match &doc.page_title {
784738 Some(title) => {
@@ -791,7 +745,7 @@ fn generate_full_inner(doc: &Document, dev: bool) -> String {
791745<meta charset=\"utf-8\">
792746<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
793747<title>{title}</title>
794−{base_html}{canonical_html}{preload_html}{meta_html}{og_html}{favicon_html}{head_html}\
748+{base_html}{canonical_html}{meta_html}{og_html}{favicon_html}{head_html}\
795749<style>
796750{reset_css}{element_css}\
797751</style>
@@ -805,7 +759,6 @@ fn generate_full_inner(doc: &Document, dev: bool) -> String {
805759 lang_attr = lang_attr,
806760 base_html = base_html,
807761 canonical_html = canonical_html,
808− preload_html = preload_html,
809762 meta_html = meta_html,
810763 favicon_html = favicon_html,
811764 head_html = head_html,
@@ -816,12 +769,11 @@ fn generate_full_inner(doc: &Document, dev: bool) -> String {
816769 )
817770 } else {
818771 format!(
819− "<!DOCTYPE html><html{lang_attr}><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>{title}</title>{base_html}{canonical_html}{preload_html}{meta_html}{og_html}{favicon_html}{head_html}<style>{reset_css}{element_css}</style></head><body>{body}</body></html>",
772+ "<!DOCTYPE html><html{lang_attr}><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>{title}</title>{base_html}{canonical_html}{meta_html}{og_html}{favicon_html}{head_html}<style>{reset_css}{element_css}</style></head><body>{body}</body></html>",
820773 title = html_escape(title),
821774 lang_attr = lang_attr,
822775 base_html = base_html,
823776 canonical_html = canonical_html,
824− preload_html = preload_html,
825777 meta_html = meta_html,
826778 og_html = og_html,
827779 favicon_html = favicon_html,
@@ -912,13 +864,13 @@ fn build_element_css(doc: &Document, styles: &StyleCollector, dev: bool) -> Stri
912864/// Built-in reset rules, in a layer before `htmlang`: unlayered CSS beats
913865/// every layer, so an unlayered `a{color:inherit}` would override
914866/// `@link [color red]`.
915−fn reset_css(dev: bool, focus_visible_css: &str, skip_link_css: &str) -> String {
867+fn reset_css(dev: bool, focus_visible_css: &str) -> String {
916868 let base = if dev {
917869 "*, *::before, *::after { box-sizing: border-box; }\nbody { margin: 0; font-family: system-ui, -apple-system, sans-serif; }\nimg { display: block; }\na { text-decoration: none; color: inherit; }\n"
918870 } else {
919871 "*,*::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}"
920872 };
921− let rules = format!("{}{}{}", base, focus_visible_css, skip_link_css);
873+ let rules = format!("{}{}", base, focus_visible_css);
922874 if dev {
923875 format!("@layer hl-reset, htmlang;\n@layer hl-reset {{\n{}}}\n", rules)
924876 } else {
@@ -932,7 +884,6 @@ fn generate_partial_inner(doc: &Document, dev: bool) -> String {
932884 let mut ctx = GenContext {
933885 dev,
934886 depth: 0,
935− image_count: 0,
936887 has_interactive: false,
937888 };
938889 let mut body = String::new();
@@ -1178,10 +1129,6 @@ fn generate_element(
11781129 out.push('<');
11791130 out.push_str(tag);
11801131
1181− // @main gets id="hl-main" for skip-to-content link (unless user set an id)
1182− if elem.kind.is_tag("main") && id.is_none() {
1183− out.push_str(" id=\"hl-main\"");
1184− }
11851132
11861133 if elem.kind == ElementKind::Link
11871134 && let Some(url) = &elem.argument
@@ -1189,18 +1136,6 @@ fn generate_element(
11891136 out.push_str(" href=\"");
11901137 out.push_str(&html_escape(url));
11911138 out.push('"');
1192− // Auto rel="noopener noreferrer" and target="_blank" for external links
1193− let is_external = url.starts_with("http://") || url.starts_with("https://");
1194− if is_external {
1195− let has_rel = elem.attrs.iter().any(|a| a.key == "rel");
1196− let has_target = elem.attrs.iter().any(|a| a.key == "target");
1197− if !has_rel {
1198− out.push_str(" rel=\"noopener noreferrer\"");
1199− }
1200− if !has_target {
1201− out.push_str(" target=\"_blank\"");
1202− }
1203− }
12041139 }
12051140
12061141 emit_argument_attr(out, elem);
@@ -1335,80 +1270,6 @@ fn generate_self_closing(
13351270 ));
13361271 }
13371272
1338− // Image optimization: auto-add loading="lazy" and decoding="async"
1339− if elem.kind == ElementKind::Image {
1340− // Responsive srcset: @image photo.jpg [responsive 400 800 1200]
1341− let responsive_attr = elem.attrs.iter().find(|a| a.key == "responsive");
1342− if let Some(resp) = responsive_attr
1343− && let Some(ref sizes_str) = resp.value
1344− {
1345− let widths: Vec<&str> = sizes_str.split_whitespace().collect();
1346− if !widths.is_empty() {
1347− let src = elem.argument.as_deref().unwrap_or("");
1348− if !src.is_empty() {
1349− // Generate srcset with width descriptors
1350− // Convention: file-{width}.ext (e.g., photo-400.jpg)
1351− let dot_pos = src.rfind('.').unwrap_or(src.len());
1352− let base = &src[..dot_pos];
1353− let ext = &src[dot_pos..];
1354− let mut srcset_parts = Vec::new();
1355− for w in &widths {
1356− srcset_parts.push(format!("{}-{}{} {}w", base, w, ext, w));
1357− }
1358− out.push_str(" srcset=\"");
1359− out.push_str(&srcset_parts.join(", "));
1360− out.push('"');
1361− // Generate sizes attribute
1362− let max_width = widths.last().unwrap_or(&"100vw");
1363− out.push_str(&format!(
1364− " sizes=\"(max-width: {}px) 100vw, {}px\"",
1365− max_width, max_width
1366− ));
1367− }
1368− }
1369− }
1370−
1371− // Auto image dimensions: read local image file to inject width/height + aspect-ratio
1372− let has_width = elem.attrs.iter().any(|a| a.key == "width");
1373− let has_height = elem.attrs.iter().any(|a| a.key == "height");
1374− if (!has_width || !has_height)
1375− && let Some(ref src) = elem.argument
1376− && !src.starts_with("http://")
1377− && !src.starts_with("https://")
1378− && !src.starts_with("data:")
1379− && let Some((w, h)) = read_image_dimensions(src)
1380− {
1381− // Intrinsic size attributes only when neither dimension is set in
1382− // CSS; with one CSS dimension, a leftover intrinsic attribute for
1383− // the other would distort the image (200 wide but 1000 tall).
1384− if !has_width && !has_height {
1385− out.push_str(&format!(" width=\"{}\" height=\"{}\"", w, h));
1386− }
1387− // Auto aspect-ratio to prevent CLS
1388− if !elem.attrs.iter().any(|a| a.key == "aspect-ratio") {
1389− out.push_str(&format!(" style=\"aspect-ratio:{}/{}\"", w, h));
1390− }
1391− }
1392−
1393− // Smart image loading: first 3 images get fetchpriority="high" (above the fold),
1394− // subsequent images get loading="lazy" + decoding="async"
1395− ctx.image_count += 1;
1396− if ctx.image_count <= 3 {
1397− // Above-the-fold: eager loading with high priority
1398− if !elem.attrs.iter().any(|a| a.key == "fetchpriority") {
1399− out.push_str(" fetchpriority=\"high\"");
1400− }
1401− } else {
1402− // Below-the-fold: lazy loading
1403− if !elem.attrs.iter().any(|a| a.key == "loading") {
1404− out.push_str(" loading=\"lazy\"");
1405− }
1406− if !elem.attrs.iter().any(|a| a.key == "decoding") {
1407− out.push_str(" decoding=\"async\"");
1408− }
1409− }
1410− }
1411−
14121273 out.push('>');
14131274 out.push_str(ctx.nl());
14141275}
@@ -2251,153 +2112,6 @@ fn extract_id_class(attrs: &[Attribute]) -> (Option<String>, Option<String>) {
22512112 (id, class)
22522113}
22532114
2254−/// Read image dimensions from a local file by parsing the header bytes.
2255−/// Supports PNG, JPEG, GIF, WebP, AVIF, and SVG.
2256−fn read_image_dimensions(path: &str) -> Option<(u32, u32)> {
2257− // SVG: parse as text for viewBox/width/height attributes
2258− if path.ends_with(".svg") || path.ends_with(".SVG") {
2259− let text = std::fs::read_to_string(path).ok()?;
2260− return read_svg_dimensions(&text);
2261− }
2262−
2263− let data = std::fs::read(path).ok()?;
2264− if data.len() < 12 {
2265− return None;
2266− }
2267−
2268− // PNG: 8-byte signature, then IHDR chunk with width/height at bytes 16-23
2269− if data.len() >= 24 && data.starts_with(b"\x89PNG\r\n\x1a\n") {
2270− let w = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
2271− let h = u32::from_be_bytes([data[20], data[21], data[22], data[23]]);
2272− return Some((w, h));
2273− }
2274−
2275− // GIF: "GIF87a" or "GIF89a", width/height at bytes 6-9 (little-endian)
2276− if data.starts_with(b"GIF87a") || data.starts_with(b"GIF89a") {
2277− let w = u16::from_le_bytes([data[6], data[7]]) as u32;
2278− let h = u16::from_le_bytes([data[8], data[9]]) as u32;
2279− return Some((w, h));
2280− }
2281−
2282− // JPEG: scan for SOF0/SOF2 marker (0xFF 0xC0 or 0xFF 0xC2)
2283− if data.starts_with(b"\xff\xd8") {
2284− let mut i = 2;
2285− while i + 9 < data.len() {
2286− if data[i] != 0xFF {
2287− i += 1;
2288− continue;
2289− }
2290− let marker = data[i + 1];
2291− if marker == 0xC0 || marker == 0xC2 {
2292− let h = u16::from_be_bytes([data[i + 5], data[i + 6]]) as u32;
2293− let w = u16::from_be_bytes([data[i + 7], data[i + 8]]) as u32;
2294− return Some((w, h));
2295− }
2296− if i + 3 < data.len() {
2297− let len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
2298− i += 2 + len;
2299− } else {
2300− break;
2301− }
2302− }
2303− }
2304−
2305− // WebP: "RIFF" ... "WEBP", VP8 header at byte 20
2306− if data.len() >= 30 && &data[..4] == b"RIFF" && &data[8..12] == b"WEBP" {
2307− if &data[12..16] == b"VP8 " && data.len() >= 30 {
2308− let w = u16::from_le_bytes([data[26], data[27]]) as u32 & 0x3FFF;
2309− let h = u16::from_le_bytes([data[28], data[29]]) as u32 & 0x3FFF;
2310− return Some((w, h));
2311− }
2312− if &data[12..16] == b"VP8L" && data.len() >= 25 && data[21] == 0x2F {
2313− let bits = u32::from_le_bytes([data[22], data[23], data[24], data[25]]);
2314− let w = (bits & 0x3FFF) + 1;
2315− let h = ((bits >> 14) & 0x3FFF) + 1;
2316− return Some((w, h));
2317− }
2318− }
2319−
2320− // AVIF: ISOBMFF container with "ftyp" box containing "avif"/"avis" brand,
2321− // then "ispe" box with width/height
2322− if data.len() >= 12 && &data[4..8] == b"ftyp" {
2323− let brand = &data[8..12];
2324− if brand == b"avif" || brand == b"avis" || brand == b"mif1" {
2325− return read_avif_dimensions(&data);
2326− }
2327− }
2328−
2329− None
2330−}
2331−
2332−/// Parse SVG viewBox or width/height attributes to get dimensions.
2333−fn read_svg_dimensions(text: &str) -> Option<(u32, u32)> {
2334− // Try viewBox first: viewBox="minX minY width height"
2335− if let Some(vb_start) = text.find("viewBox=\"") {
2336− let rest = &text[vb_start + 9..];
2337− if let Some(end) = rest.find('"') {
2338− let parts: Vec<&str> = rest[..end].split_whitespace().collect();
2339− if parts.len() == 4
2340− && let (Ok(w), Ok(h)) = (parts[2].parse::<f64>(), parts[3].parse::<f64>())
2341− && w > 0.0
2342− && h > 0.0
2343− {
2344− return Some((w.round() as u32, h.round() as u32));
2345− }
2346− }
2347− }
2348− // Fall back to width/height attributes on <svg>
2349− let svg_tag = text.find("<svg")?;
2350− let tag_end = text[svg_tag..].find('>')? + svg_tag;
2351− let tag = &text[svg_tag..tag_end];
2352− let w = extract_svg_attr(tag, "width")?;
2353− let h = extract_svg_attr(tag, "height")?;
2354− Some((w, h))
2355−}
2356−
2357−fn extract_svg_attr(tag: &str, attr: &str) -> Option<u32> {
2358− let needle = format!("{}=\"", attr);
2359− let start = tag.find(&needle)? + needle.len();
2360− let rest = &tag[start..];
2361− let end = rest.find('"')?;
2362− let val = rest[..end].trim_end_matches("px");
2363− val.parse::<f64>().ok().map(|v| v.round() as u32)
2364−}
2365−
2366−/// Parse AVIF (ISOBMFF) container to find ispe box with image dimensions.
2367−fn read_avif_dimensions(data: &[u8]) -> Option<(u32, u32)> {
2368− // Walk ISOBMFF boxes looking for "ispe" (image spatial extents)
2369− let mut i = 0;
2370− while i + 8 <= data.len() {
2371− let box_size =
2372− u32::from_be_bytes([data[i], data[i + 1], data[i + 2], data[i + 3]]) as usize;
2373− let box_type = &data[i + 4..i + 8];
2374− if box_size < 8 {
2375− break;
2376− }
2377− let box_end = (i + box_size).min(data.len());
2378− // ispe box: 4 bytes version/flags + 4 bytes width + 4 bytes height
2379− if box_type == b"ispe" && box_end >= i + 20 {
2380− let w = u32::from_be_bytes([data[i + 12], data[i + 13], data[i + 14], data[i + 15]]);
2381− let h = u32::from_be_bytes([data[i + 16], data[i + 17], data[i + 18], data[i + 19]]);
2382− return Some((w, h));
2383− }
2384− // Recurse into container boxes (meta, iprp, ipco)
2385− if matches!(
2386− box_type,
2387− b"meta" | b"iprp" | b"ipco" | b"moov" | b"trak" | b"mdia"
2388− ) {
2389− let header_size = if box_type == b"meta" { 12 } else { 8 };
2390− if i + header_size < box_end
2391− && let Some(dims) = read_avif_dimensions(&data[i + header_size..box_end])
2392− {
2393− return Some(dims);
2394− }
2395− }
2396− i = box_end;
2397− }
2398− None
2399−}
2400−
24012115fn html_escape(s: &str) -> String {
24022116 s.replace('&', "&amp;")
24032117 .replace('<', "&lt;")
@@ -2514,12 +2228,3 @@ fn vlq_encode(value: i64, out: &mut String) {
25142228}
25152229
25162230
2517−/// Does the tree contain the table element `name` (e.g. `"main"`)?
2518−fn has_tag(nodes: &[Node], name: &str) -> bool {
2519− nodes.iter().any(|node| match node {
2520− Node::Element(elem) => elem.kind.is_tag(name) || has_tag(&elem.children, name),
2521− _ => false,
2522− })
2523−}
2524−
2525−
crates/htmlang-core/src/parser.rs−32
@@ -191,7 +191,6 @@ pub fn parse_with_base(input: &str, base_path: Option<&Path>) -> ParseResult {
191191 og_tags: ctx.og_tags,
192192 canonical: ctx.canonical,
193193 base_url: ctx.base_url,
194− preload_hints: collect_image_preload_hints(&nodes),
195194 nodes,
196195 },
197196 diagnostics: ctx.diagnostics,
@@ -199,37 +198,6 @@ pub fn parse_with_base(input: &str, base_path: Option<&Path>) -> ParseResult {
199198 }
200199}
201200
202−/// Scan nodes for @image elements and generate preload hints for early images.
203−fn collect_image_preload_hints(nodes: &[Node]) -> Vec<crate::ast::PreloadHint> {
204− let mut hints = Vec::new();
205− // Only preload the first few images (above-the-fold heuristic)
206− collect_images_recursive(nodes, &mut hints, 3);
207− hints
208−}
209−
210−fn collect_images_recursive(nodes: &[Node], hints: &mut Vec<crate::ast::PreloadHint>, max: usize) {
211− for node in nodes {
212− if hints.len() >= max {
213− return;
214− }
215− if let Node::Element(elem) = node {
216− if elem.kind == ElementKind::Image
217− && let Some(ref src) = elem.argument
218− && !src.is_empty()
219− && !src.starts_with("data:")
220− && !src.starts_with('#')
221− {
222− hints.push(crate::ast::PreloadHint {
223− href: src.clone(),
224− as_type: "image".to_string(),
225− crossorigin: false,
226− });
227− }
228− collect_images_recursive(&elem.children, hints, max);
229− }
230− }
231−}
232−
233201// ---------------------------------------------------------------------------
234202// Preprocessing: strip comments/blanks, collapse @raw blocks
235203// ---------------------------------------------------------------------------
crates/htmlang-core/src/vocab.rs+1−1
@@ -11,7 +11,7 @@
1111/// HTML attribute).
1212pub const HTMLANG_ATTRIBUTES: &[&str] = &[
1313 "align-bottom", "align-left", "align-right", "align-top", "center-x", "center-y",
14− "col-span", "grid-cols", "grid-rows", "inline", "responsive", "row-span",
14+ "col-span", "grid-cols", "grid-rows", "inline", "row-span",
1515 "spacing", "wrap",
1616];
1717
examples/demo.html+2−2
@@ -1,3 +1,3 @@
1−<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>htmlang Demo</title><link rel="preload" href="https://picsum.photos/72" as="image"><link rel="preload" href="https://picsum.photos/640/200" as="image"><meta name="description" content="A one-page tour of htmlang."><meta name="theme-color" content="#3b82f6"><meta property="og:title" content="htmlang"><meta property="og:type" content="website"><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}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}:root{--primary:#3b82f6;--ink:#0f172a;--muted:#64748b;--subtle:#f8fafc;}@layer htmlang{.a{display:flex;flex-direction:column;max-width:960px;margin-left:auto;margin-right:auto;padding:32px;gap:24px;background:white;}.b{display:flex;flex-direction:column;gap:8px;}.c{display:flex;flex-direction:column;gap:6px;color:#64748b;font-size:13px;}.d{display:flex;flex-direction:row;gap:6px;align-items:center;}.e{color:#64748b;}.f{color:#0f172a;}.g{display:flex;flex-direction:row;gap:16px;align-items:center;}.h{display:flex;flex-direction:column;flex:1;min-width:0;gap:6px;}.i{font-weight:bold;font-size:40px;color:#0f172a;}.j{margin:0;line-height:1.6;color:#64748b;}.k{font-weight:bold;color:#3b82f6;}.l{color:#3b82f6;text-decoration:underline;}.m{width:72px;height:72px;border-radius:36px;}.n{border-top:1px solid #e5e7eb;}.o{display:flex;flex-direction:column;gap:12px;padding-block:8px;}.p{font-weight:bold;font-size:22px;color:#0f172a;}.q{display:flex;flex-direction:row;flex-wrap:wrap;gap:8px;}.r{display:flex;flex-direction:column;padding:16px;background:white;border-radius:8px;border:1px solid #e5e7eb;box-shadow:0 1px 3px rgba(0,0,0,0.05);cursor:pointer;transition:all 0.2s ease;animation:fade-up 0.3s ease;}.s{display:flex;flex-direction:row;gap:10px;align-items:center;}.t{font-size:20px;color:#3b82f6;}.u{font-weight:bold;font-size:16px;color:#0f172a;}.v{margin:0;line-height:1.7;}.w{font-weight:bold;}.x{font-style:italic;}.y{padding:2px 6px;background:#f1f5f9;border-radius:4px;font-family:monospace;font-size:13px;}.z{background:#fef08a;}.aa{display:flex;flex-direction:column;margin:0;padding-inline-start:16px;border-left:3px solid #3b82f6;color:#64748b;font-style:italic;}.ab{margin:0;white-space:pre;background:#0f172a;color:#e2e8f0;padding:16px;border-radius:8px;font-family:monospace;font-size:13px;overflow-x:auto;}.ac{display:flex;flex-direction:column;gap:6px;}.ad{display:flex;flex-direction:row;gap:10px;align-items:baseline;}.ae{font-weight:bold;min-width:80px;}.af{color:#64748b;font-style:italic;}.ag{display:flex;flex-direction:row;gap:6px;}.ah{display:flex;flex-direction:column;width:18px;height:18px;border-radius:9px;background:#3b82f6;opacity:0.4;}.ai{display:flex;flex-direction:row;gap:4px;}.aj{display:flex;flex-direction:column;width:24px;height:24px;border-radius:4px;background:#3b82f6;color:white;text-align:center;font-size:12px;font-weight:bold;line-height:24px;}.ak{font-weight:bold;min-width:64px;}.al{color:#3b82f6;font-weight:bold;}.am{color:#10b981;font-weight:bold;}.an{display:flex;flex-direction:column;padding:12px;border-radius:6px;background:#3b82f6;color:white;}.ao{font-family:ui-monospace,monospace;}.ap{display:flex;flex-direction:column;gap:10px;max-width:420px;}.aq{font-weight:bold;font-size:13px;}.ar{padding:10px;border:1px solid #e5e7eb;border-radius:6px;transition:border 0.15s ease;}.as{padding:10px;border:1px solid #e5e7eb;border-radius:6px;}.at{display:flex;flex-direction:row;gap:8px;}.au{padding:10px 16px;background:#3b82f6;color:white;font-weight:bold;border-radius:6px;border:0;transition:background 0.15s ease;cursor:pointer;}.av{padding:10px 16px;background:white;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;}.aw{display:flex;flex-direction:row;gap:16px;flex-wrap:wrap;}.ax{display:flex;flex-direction:column;flex:1;gap:8px;min-width:240px;}.ay{margin:0;padding-left:0;gap:4px;list-style:disc;padding-inline-start:20px;}.az{flex-direction:column;display:list-item;}.a0{margin:0;padding-left:0;list-style:decimal;padding-inline-start:20px;}.a1{text-align:left;padding:8px;}.a2{text-align:right;padding:8px;}.a3{padding:8px;border-top:1px solid #e5e7eb;}.a4{padding:8px;border-top:1px solid #e5e7eb;text-align:right;}.a5{display:flex;flex-direction:column;margin:0;gap:6px;}.a6{border-radius:8px;}.a7{color:#64748b;font-size:13px;text-align:center;}.a8{display:flex;flex-direction:column;padding:12px;border:1px solid #e5e7eb;border-radius:6px;background:#f8fafc;}.a9{font-weight:bold;cursor:pointer;}.ba{margin:0;color:#64748b;line-height:1.6;padding-block-start:6px;}.bb{flex-direction:column;display:grid;grid-template-columns:1fr 2fr;grid-template-areas:"h h" "s m" "f f";gap:8px;padding:8px;background:#f8fafc;border-radius:8px;}.bc{display:flex;flex-direction:column;grid-area:h;padding:10px;background:#3b82f6;color:white;border-radius:6px;text-align:center;}.bd{display:flex;flex-direction:column;grid-area:s;padding:10px;background:white;border-radius:6px;border:1px solid #e5e7eb;}.be{display:flex;flex-direction:column;grid-area:m;padding:10px;background:white;border-radius:6px;border:1px solid #e5e7eb;}.bf{display:flex;flex-direction:column;grid-area:f;padding:10px;background:#0f172a;color:white;border-radius:6px;text-align:center;}.bg{gap:4px;font-style:normal;}.bh,.bn{color:#3b82f6;}.bi{font-size:13px;color:#64748b;}.bj{font-size:13px;}.bk{display:flex;flex-direction:column;padding-block-start:24px;margin-block-start:24px;border-top:1px solid #e5e7eb;gap:8px;}.bl{display:flex;flex-direction:row;gap:8px;align-items:center;}.bm{display:flex;flex-direction:column;padding:12px 20px;background:#3b82f6;color:white;font-weight:bold;border-radius:8px;transition:all 0.15s ease;cursor:pointer;}.bo{display:flex;flex-direction:column;flex:1;min-width:0;}.bp{color:#64748b;font-size:13px;}.r:hover{box-shadow:0 4px 12px rgba(0,0,0,0.08);}.au:hover,.bm:hover{background:#3575dd;}.bn:hover{text-decoration:underline;}.an::before{content:"→ ";font-weight:bold;}.ar:focus{border:1px solid #3b82f6;}.ar:valid{border:1px solid #10b981;}.ar:invalid{border:1px solid #ef4444;}.au:active,.bm:active{background:#2f68c5;}.bn:visited{color:#8b5cf6;}@media(prefers-color-scheme:dark){.a{background:#0b1220;color:#e2e8f0;}.i{color:white;}}@media print{.bk{display:none;}}}@keyframes fade-up {from { opacity: 0; transform: translateY(8px); }to { opacity: 1; transform: translateY(0); }}</style></head><body><noscript><style>.needs-js{display:none}</style></noscript><div class="a"><header class="b"><nav class="c"><div class="d"><a href="/" class="e">Home</a><span>/</span><a href="/demos" class="e">Demos</a><span>/</span><span class="f">htmlang</span></div></nav><div class="g"><div class="h"><span class="i">Welcome to htmlang</span><p class="j">A minimalist layout language that compiles to <span class="k">static HTML</span> — no JS, no build step. Visit the <a href="https://github.com" rel="noopener noreferrer" target="_blank" class="l">repo</a> for more.</p></div><img src="https://picsum.photos/72" class="m" alt="Avatar" fetchpriority="high"></div></header><hr class="n"><section class="o"><span class="p">Features</span><div class="q"><article class="r"><div class="s"><span class="t">✦</span><span class="u">Simple</span></div><span>Write layouts without CSS classes.</span></article><article class="r"><div class="s"><span class="t">⚡</span><span class="u">Fast</span></div><span>Compiles to a single self-contained HTML file.</span></article><article class="r"><div class="s"><span class="t">○</span><span class="u">Minimal</span></div><span>Zero runtime JavaScript by default.</span></article><article class="r"><div class="s"><span class="t">◆</span><span class="u">Typed</span></div><span>LSP with diagnostics and go-to-definition.</span></article></div></section><section class="o"><span class="p">Typography</span><p class="v">A quick brown fox jumps over <span class="w">the lazy</span> <span class="x">dog</span>. Press <kbd class="y">Ctrl+K</kbd> to search. <mark class="z">Highlighted</mark> uses mark. <abbr title="HyperText Markup Language + language">htmlang</abbr> is an abbreviation.</p><blockquote class="aa"><span>Layout is code. Style is code. Everything should compose.</span><cite>— Anonymous</cite></blockquote><pre class="ab">@let button $label
1+<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>htmlang Demo</title><meta name="description" content="A one-page tour of htmlang."><meta name="theme-color" content="#3b82f6"><meta property="og:title" content="htmlang"><meta property="og:type" content="website"><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}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}:root{--primary:#3b82f6;--ink:#0f172a;--muted:#64748b;--subtle:#f8fafc;}@layer htmlang{.a{display:flex;flex-direction:column;max-width:960px;margin-left:auto;margin-right:auto;padding:32px;gap:24px;background:white;}.b{display:flex;flex-direction:column;gap:8px;}.c{display:flex;flex-direction:column;gap:6px;color:#64748b;font-size:13px;}.d{display:flex;flex-direction:row;gap:6px;align-items:center;}.e{color:#64748b;}.f{color:#0f172a;}.g{display:flex;flex-direction:row;gap:16px;align-items:center;}.h{display:flex;flex-direction:column;flex:1;min-width:0;gap:6px;}.i{font-weight:bold;font-size:40px;color:#0f172a;}.j{margin:0;line-height:1.6;color:#64748b;}.k{font-weight:bold;color:#3b82f6;}.l{color:#3b82f6;text-decoration:underline;}.m{width:72px;height:72px;border-radius:36px;}.n{border-top:1px solid #e5e7eb;}.o{display:flex;flex-direction:column;gap:12px;padding-block:8px;}.p{font-weight:bold;font-size:22px;color:#0f172a;}.q{display:flex;flex-direction:row;flex-wrap:wrap;gap:8px;}.r{display:flex;flex-direction:column;padding:16px;background:white;border-radius:8px;border:1px solid #e5e7eb;box-shadow:0 1px 3px rgba(0,0,0,0.05);cursor:pointer;transition:all 0.2s ease;animation:fade-up 0.3s ease;}.s{display:flex;flex-direction:row;gap:10px;align-items:center;}.t{font-size:20px;color:#3b82f6;}.u{font-weight:bold;font-size:16px;color:#0f172a;}.v{margin:0;line-height:1.7;}.w{font-weight:bold;}.x{font-style:italic;}.y{padding:2px 6px;background:#f1f5f9;border-radius:4px;font-family:monospace;font-size:13px;}.z{background:#fef08a;}.aa{display:flex;flex-direction:column;margin:0;padding-inline-start:16px;border-left:3px solid #3b82f6;color:#64748b;font-style:italic;}.ab{margin:0;white-space:pre;background:#0f172a;color:#e2e8f0;padding:16px;border-radius:8px;font-family:monospace;font-size:13px;overflow-x:auto;}.ac{display:flex;flex-direction:column;gap:6px;}.ad{display:flex;flex-direction:row;gap:10px;align-items:baseline;}.ae{font-weight:bold;min-width:80px;}.af{color:#64748b;font-style:italic;}.ag{display:flex;flex-direction:row;gap:6px;}.ah{display:flex;flex-direction:column;width:18px;height:18px;border-radius:9px;background:#3b82f6;opacity:0.4;}.ai{display:flex;flex-direction:row;gap:4px;}.aj{display:flex;flex-direction:column;width:24px;height:24px;border-radius:4px;background:#3b82f6;color:white;text-align:center;font-size:12px;font-weight:bold;line-height:24px;}.ak{font-weight:bold;min-width:64px;}.al{color:#3b82f6;font-weight:bold;}.am{color:#10b981;font-weight:bold;}.an{display:flex;flex-direction:column;padding:12px;border-radius:6px;background:#3b82f6;color:white;}.ao{font-family:ui-monospace,monospace;}.ap{display:flex;flex-direction:column;gap:10px;max-width:420px;}.aq{font-weight:bold;font-size:13px;}.ar{padding:10px;border:1px solid #e5e7eb;border-radius:6px;transition:border 0.15s ease;}.as{padding:10px;border:1px solid #e5e7eb;border-radius:6px;}.at{display:flex;flex-direction:row;gap:8px;}.au{padding:10px 16px;background:#3b82f6;color:white;font-weight:bold;border-radius:6px;border:0;transition:background 0.15s ease;cursor:pointer;}.av{padding:10px 16px;background:white;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;}.aw{display:flex;flex-direction:row;gap:16px;flex-wrap:wrap;}.ax{display:flex;flex-direction:column;flex:1;gap:8px;min-width:240px;}.ay{margin:0;padding-left:0;gap:4px;list-style:disc;padding-inline-start:20px;}.az{flex-direction:column;display:list-item;}.a0{margin:0;padding-left:0;list-style:decimal;padding-inline-start:20px;}.a1{text-align:left;padding:8px;}.a2{text-align:right;padding:8px;}.a3{padding:8px;border-top:1px solid #e5e7eb;}.a4{padding:8px;border-top:1px solid #e5e7eb;text-align:right;}.a5{display:flex;flex-direction:column;margin:0;gap:6px;}.a6{border-radius:8px;}.a7{color:#64748b;font-size:13px;text-align:center;}.a8{display:flex;flex-direction:column;padding:12px;border:1px solid #e5e7eb;border-radius:6px;background:#f8fafc;}.a9{font-weight:bold;cursor:pointer;}.ba{margin:0;color:#64748b;line-height:1.6;padding-block-start:6px;}.bb{flex-direction:column;display:grid;grid-template-columns:1fr 2fr;grid-template-areas:"h h" "s m" "f f";gap:8px;padding:8px;background:#f8fafc;border-radius:8px;}.bc{display:flex;flex-direction:column;grid-area:h;padding:10px;background:#3b82f6;color:white;border-radius:6px;text-align:center;}.bd{display:flex;flex-direction:column;grid-area:s;padding:10px;background:white;border-radius:6px;border:1px solid #e5e7eb;}.be{display:flex;flex-direction:column;grid-area:m;padding:10px;background:white;border-radius:6px;border:1px solid #e5e7eb;}.bf{display:flex;flex-direction:column;grid-area:f;padding:10px;background:#0f172a;color:white;border-radius:6px;text-align:center;}.bg{gap:4px;font-style:normal;}.bh,.bn{color:#3b82f6;}.bi{font-size:13px;color:#64748b;}.bj{font-size:13px;}.bk{display:flex;flex-direction:column;padding-block-start:24px;margin-block-start:24px;border-top:1px solid #e5e7eb;gap:8px;}.bl{display:flex;flex-direction:row;gap:8px;align-items:center;}.bm{display:flex;flex-direction:column;padding:12px 20px;background:#3b82f6;color:white;font-weight:bold;border-radius:8px;transition:all 0.15s ease;cursor:pointer;}.bo{display:flex;flex-direction:column;flex:1;min-width:0;}.bp{color:#64748b;font-size:13px;}.r:hover{box-shadow:0 4px 12px rgba(0,0,0,0.08);}.au:hover,.bm:hover{background:#3575dd;}.bn:hover{text-decoration:underline;}.an::before{content:"→ ";font-weight:bold;}.ar:focus{border:1px solid #3b82f6;}.ar:valid{border:1px solid #10b981;}.ar:invalid{border:1px solid #ef4444;}.au:active,.bm:active{background:#2f68c5;}.bn:visited{color:#8b5cf6;}@media(prefers-color-scheme:dark){.a{background:#0b1220;color:#e2e8f0;}.i{color:white;}}@media print{.bk{display:none;}}}@keyframes fade-up {from { opacity: 0; transform: translateY(8px); }to { opacity: 1; transform: translateY(0); }}</style></head><body><noscript><style>.needs-js{display:none}</style></noscript><div class="a"><header class="b"><nav class="c"><div class="d"><a href="/" class="e">Home</a><span>/</span><a href="/demos" class="e">Demos</a><span>/</span><span class="f">htmlang</span></div></nav><div class="g"><div class="h"><span class="i">Welcome to htmlang</span><p class="j">A minimalist layout language that compiles to <span class="k">static HTML</span> — no JS, no build step. Visit the <a href="https://github.com" class="l">repo</a> for more.</p></div><img src="https://picsum.photos/72" class="m" alt="Avatar"></div></header><hr class="n"><section class="o"><span class="p">Features</span><div class="q"><article class="r"><div class="s"><span class="t">✦</span><span class="u">Simple</span></div><span>Write layouts without CSS classes.</span></article><article class="r"><div class="s"><span class="t">⚡</span><span class="u">Fast</span></div><span>Compiles to a single self-contained HTML file.</span></article><article class="r"><div class="s"><span class="t">○</span><span class="u">Minimal</span></div><span>Zero runtime JavaScript by default.</span></article><article class="r"><div class="s"><span class="t">◆</span><span class="u">Typed</span></div><span>LSP with diagnostics and go-to-definition.</span></article></div></section><section class="o"><span class="p">Typography</span><p class="v">A quick brown fox jumps over <span class="w">the lazy</span> <span class="x">dog</span>. Press <kbd class="y">Ctrl+K</kbd> to search. <mark class="z">Highlighted</mark> uses mark. <abbr title="HyperText Markup Language + language">htmlang</abbr> is an abbreviation.</p><blockquote class="aa"><span>Layout is code. Style is code. Everything should compose.</span><cite>— Anonymous</cite></blockquote><pre class="ab">@let button $label
22 @el [padding 12, background blue]
3− @text $label</pre></section><section class="o"><span class="p">Control flow</span><div class="ac"><div class="ad"><span class="ae">Alice</span><span class="e">Engineer</span></div><div class="ad"><span class="ae">Bob</span><span class="e">Designer</span></div><div class="ad"><span class="ae">Carol</span><span class="e">PM</span></div></div><span class="af" data-state="">(Nothing — @each ... @else fallback)</span><div class="ag"><div class="ah"></div><div class="ah"></div><div class="ah"></div><div class="ah"></div><div class="ah"></div><div class="ah"></div></div><div class="ai"><div class="aj"><span>1</span></div><div class="aj"><span>2</span></div><div class="aj"><span>3</span></div><div class="aj"><span>4</span></div><div class="aj"><span>5</span></div><div class="aj"><span>6</span></div></div><div class="d"><span class="ak">Count:</span><span class="al">3 items</span></div><span class="am">✓ Ready</span><div class="an"><span>Conditional styling via <code class="ao">if(cond, a, b)</code>.</span></div></section><section class="o"><span class="p">Forms</span><form action="/subscribe" class="ap"><label class="aq" for="email">Email</label><input class="ar" id="email" type="email" name="email" placeholder="you@example.com" required><label class="aq" for="role">Role</label><select class="as" id="role" name="role"><option value="dev">Developer</option><option value="designer">Designer</option><option value="other">Other</option></select><label class="aq" for="msg">Message</label><textarea class="as" id="msg" name="msg" rows="3"></textarea><div class="at"><button class="au" type="submit">Subscribe</button><button class="av" type="reset">Reset</button></div><progress value="60" max="100"></progress></form></section><section class="o"><span class="p">Data</span><div class="aw"><div class="ax"><span class="w">Unordered</span><ul class="ay"><li class="az">Composable with @each</li><li class="az">Indent to nest children</li><li class="az">Bold list items work too</li></ul><span class="w">Ordered</span><ol class="a0"><li class="az">First</li><li class="az">Second</li><li class="az">Third</li></ol></div><div class="ax"><span class="w">Table</span><table><thead><tr><th class="a1">Item</th><th class="a2">Qty</th></tr></thead><tbody><tr><td class="a3">Widgets</td><td class="a4">12</td></tr><tr><td class="a3">Gadgets</td><td class="a4">7</td></tr></tbody></table></div></div><figure class="a5"><img src="https://picsum.photos/640/200" class="a6" alt="A random placeholder" fetchpriority="high"><figcaption class="a7">A placeholder image.</figcaption></figure></section><section class="o"><span class="p">Disclosure + grid areas</span><details class="a8"><summary class="a9">What is htmlang?</summary><p class="ba">htmlang is a layout-first language that compiles to a single, self-contained HTML file with embedded CSS.</p></details><div class="bb"><div class="bc"><span>Header</span></div><div class="bd"><span>Sidebar</span></div><div class="be"><span>Main</span></div><div class="bf"><span>Footer</span></div></div></section><section class="o"><span class="p">Misc</span><address class="bg"><span class="w">htmlang HQ</span><span>123 Flexbox Avenue</span><a href="mailto:hi@htmlang.dev" class="bh">hi@htmlang.dev</a></address><div class="s"><span class="bi">Published</span><time class="bj" datetime="2026-04-16">April 16 2026</time></div><div class="s"><span class="bi">Confidence</span><meter value="8" min="0" max="10" low="3" high="7">8/10</meter></div></section><footer class="bk"><div class="bl"><div class="bm"><a href="https://github.com" rel="noopener noreferrer" target="_blank">Get Started</a></div><a href="https://github.com" rel="noopener noreferrer" target="_blank" class="bn">Browse source</a><div class="bo"></div><span class="bp">© 2026 htmlang</span></div></footer></div></body></html>
3+ @text $label</pre></section><section class="o"><span class="p">Control flow</span><div class="ac"><div class="ad"><span class="ae">Alice</span><span class="e">Engineer</span></div><div class="ad"><span class="ae">Bob</span><span class="e">Designer</span></div><div class="ad"><span class="ae">Carol</span><span class="e">PM</span></div></div><span class="af" data-state="">(Nothing — @each ... @else fallback)</span><div class="ag"><div class="ah"></div><div class="ah"></div><div class="ah"></div><div class="ah"></div><div class="ah"></div><div class="ah"></div></div><div class="ai"><div class="aj"><span>1</span></div><div class="aj"><span>2</span></div><div class="aj"><span>3</span></div><div class="aj"><span>4</span></div><div class="aj"><span>5</span></div><div class="aj"><span>6</span></div></div><div class="d"><span class="ak">Count:</span><span class="al">3 items</span></div><span class="am">✓ Ready</span><div class="an"><span>Conditional styling via <code class="ao">if(cond, a, b)</code>.</span></div></section><section class="o"><span class="p">Forms</span><form action="/subscribe" class="ap"><label class="aq" for="email">Email</label><input class="ar" id="email" type="email" name="email" placeholder="you@example.com" required><label class="aq" for="role">Role</label><select class="as" id="role" name="role"><option value="dev">Developer</option><option value="designer">Designer</option><option value="other">Other</option></select><label class="aq" for="msg">Message</label><textarea class="as" id="msg" name="msg" rows="3"></textarea><div class="at"><button class="au" type="submit">Subscribe</button><button class="av" type="reset">Reset</button></div><progress value="60" max="100"></progress></form></section><section class="o"><span class="p">Data</span><div class="aw"><div class="ax"><span class="w">Unordered</span><ul class="ay"><li class="az">Composable with @each</li><li class="az">Indent to nest children</li><li class="az">Bold list items work too</li></ul><span class="w">Ordered</span><ol class="a0"><li class="az">First</li><li class="az">Second</li><li class="az">Third</li></ol></div><div class="ax"><span class="w">Table</span><table><thead><tr><th class="a1">Item</th><th class="a2">Qty</th></tr></thead><tbody><tr><td class="a3">Widgets</td><td class="a4">12</td></tr><tr><td class="a3">Gadgets</td><td class="a4">7</td></tr></tbody></table></div></div><figure class="a5"><img src="https://picsum.photos/640/200" class="a6" alt="A random placeholder"><figcaption class="a7">A placeholder image.</figcaption></figure></section><section class="o"><span class="p">Disclosure + grid areas</span><details class="a8"><summary class="a9">What is htmlang?</summary><p class="ba">htmlang is a layout-first language that compiles to a single, self-contained HTML file with embedded CSS.</p></details><div class="bb"><div class="bc"><span>Header</span></div><div class="bd"><span>Sidebar</span></div><div class="be"><span>Main</span></div><div class="bf"><span>Footer</span></div></div></section><section class="o"><span class="p">Misc</span><address class="bg"><span class="w">htmlang HQ</span><span>123 Flexbox Avenue</span><a href="mailto:hi@htmlang.dev" class="bh">hi@htmlang.dev</a></address><div class="s"><span class="bi">Published</span><time class="bj" datetime="2026-04-16">April 16 2026</time></div><div class="s"><span class="bi">Confidence</span><meter value="8" min="0" max="10" low="3" high="7">8/10</meter></div></section><footer class="bk"><div class="bl"><div class="bm"><a href="https://github.com">Get Started</a></div><a href="https://github.com" class="bn">Browse source</a><div class="bo"></div><span class="bp">© 2026 htmlang</span></div></footer></div></body></html>
src/bin/htmlang_lsp/docs.rs−1
@@ -129,7 +129,6 @@ pub(crate) const ATTRIBUTES: &[Doc] = &[
129129 doc("row-span", "Rows a grid child spans.", "row-span 2"),
130130 doc("line-height", "Line height; integers of 2 or more are px.", "line-height 1.5"),
131131 doc("inline", "On `@image`: embed the file (SVG markup, or other images as base64) in the page.", "inline"),
132− doc("responsive", "On `@image`: widths for a generated `srcset`.", "responsive 400 800 1200"),
133132];
134133
135134/// Standard-library components (`std.hl`), used like elements.
tests/regressions.rs+11
@@ -444,3 +444,14 @@ fn backslash_escapes_in_text() {
444444 assert!(out.contains("$price is 9, {@b} \\<"), "{}", out);
445445 assert!(out.contains(">$5</span> only"), "{}", out);
446446}
447+
448+#[test]
449+fn links_and_images_are_output_as_written() {
450+ let out = compile("@main\n @link https://example.com Out\n @image [alt=a] a.png\n");
451+ assert!(!out.contains("target="), "{}", out);
452+ assert!(!out.contains("rel="), "{}", out);
453+ assert!(!out.contains("preload"), "{}", out);
454+ assert!(!out.contains("fetchpriority"), "{}", out);
455+ assert!(!out.contains("Skip to content"), "{}", out);
456+ assert!(!out.contains("hl-main"), "{}", out);
457+}
tests/snapshots.rs−110
@@ -918,19 +918,6 @@ fn css_scroll_snap_align() {
918918// Feature tests: image optimization hints
919919// ---------------------------------------------------------------------------
920920
921−#[test]
922−fn image_auto_lazy_loading() {
923− // First 3 images get fetchpriority="high" (above-the-fold), subsequent get loading="lazy"
924− let output = compile(
925− "@page T\n@image https://example.com/1.jpg\n@image https://example.com/2.jpg\n@image https://example.com/3.jpg\n@image https://example.com/4.jpg",
926− );
927− // First 3 images: fetchpriority="high", no lazy loading
928− assert!(output.contains("fetchpriority=\"high\""));
929− // 4th image: loading="lazy" + decoding="async"
930− assert!(output.contains("loading=\"lazy\""));
931− assert!(output.contains("decoding=\"async\""));
932−}
933−
934921#[test]
935922fn image_explicit_loading_not_doubled() {
936923 let output = compile("@page T\n@image [loading=eager] https://example.com/photo.jpg");
@@ -1018,27 +1005,12 @@ fn focus_visible_css_without_interactive() {
10181005 assert!(!output.contains("focus-visible"));
10191006}
10201007
1021−#[test]
1022−fn skip_to_content_with_main() {
1023− let output = compile("@page T\n@main\n content");
1024− assert!(output.contains("hl-skip"));
1025− assert!(output.contains("hl-main"));
1026− assert!(output.contains("Skip to content"));
1027−}
1028−
10291008#[test]
10301009fn no_skip_to_content_without_main() {
10311010 let output = compile("@page T\n@el\n content");
10321011 assert!(!output.contains("hl-skip"));
10331012}
10341013
1035−#[test]
1036−fn external_link_noopener() {
1037− let output = compile("@page T\n@link https://example.com\n External");
1038− assert!(output.contains("rel=\"noopener noreferrer\""));
1039− assert!(output.contains("target=\"_blank\""));
1040−}
1041−
10421014#[test]
10431015fn internal_link_no_noopener() {
10441016 let output = compile("@page T\n@link /about\n About");
@@ -4348,16 +4320,6 @@ fn translations_point_to_data_files() {
43484320 );
43494321}
43504322
4351−#[test]
4352−fn image_auto_preload() {
4353− let output = compile("@page Test\n@image logo.png\n@image hero.jpg");
4354− assert!(
4355− output.contains("rel=\"preload\""),
4356− "should auto-preload images"
4357− );
4358− assert!(output.contains("logo.png"), "should preload logo.png");
4359−}
4360−
43614323#[test]
43624324fn source_map_generation() {
43634325 let input = "@page Test\n@text [font-weight bold] Hello";
@@ -4607,78 +4569,6 @@ fn partial_output_dev() {
46074569 assert!(html.contains("Hello"), "partial dev should have content");
46084570}
46094571
4610−#[test]
4611−fn responsive_srcset_on_image() {
4612− let output = compile("@image [responsive 400 800 1200, alt=Photo] photo.jpg");
4613− assert!(
4614− output.contains("srcset=\""),
4615− "should generate srcset, got: {}",
4616− output
4617− );
4618− assert!(
4619− output.contains("photo-400.jpg 400w"),
4620− "should have 400w source, got: {}",
4621− output
4622− );
4623− assert!(
4624− output.contains("photo-800.jpg 800w"),
4625− "should have 800w source, got: {}",
4626− output
4627− );
4628− assert!(
4629− output.contains("photo-1200.jpg 1200w"),
4630− "should have 1200w source, got: {}",
4631− output
4632− );
4633− assert!(
4634− output.contains("sizes="),
4635− "should generate sizes attribute, got: {}",
4636− output
4637− );
4638−}
4639−
4640−#[test]
4641−fn auto_image_dimensions_png() {
4642− // Create a minimal valid PNG (1x1 pixel)
4643− let dir = std::env::temp_dir().join("htmlang_test_img_dim");
4644− let _ = std::fs::create_dir_all(&dir);
4645− let png_path = dir.join("test.png");
4646− // Minimal 1x1 PNG file
4647− let png_data: Vec<u8> = vec![
4648− 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
4649− 0x00, 0x00, 0x00, 0x0D, // IHDR length
4650− 0x49, 0x48, 0x44, 0x52, // IHDR
4651− 0x00, 0x00, 0x00, 0x01, // width = 1
4652− 0x00, 0x00, 0x00, 0x01, // height = 1
4653− 0x08, 0x02, 0x00, 0x00, 0x00, // bit depth, color type, etc.
4654− 0x90, 0x77, 0x53, 0xDE, // CRC
4655− 0x00, 0x00, 0x00, 0x0C, // IDAT length
4656− 0x49, 0x44, 0x41, 0x54, // IDAT
4657− 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xE2, 0x21, 0xBC,
4658− 0x33, // CRC
4659− 0x00, 0x00, 0x00, 0x00, // IEND length
4660− 0x49, 0x45, 0x4E, 0x44, // IEND
4661− 0xAE, 0x42, 0x60, 0x82, // CRC
4662− ];
4663− std::fs::write(&png_path, &png_data).unwrap();
4664−
4665− let input = format!("@image [alt=test] {}", png_path.display());
4666− let result = htmlang::parser::parse(&input);
4667− let html = htmlang::codegen::generate(&result.document);
4668− assert!(
4669− html.contains("width=\"1\""),
4670− "should auto-detect width=1 from PNG, got: {}",
4671− html
4672− );
4673− assert!(
4674− html.contains("height=\"1\""),
4675− "should auto-detect height=1 from PNG, got: {}",
4676− html
4677− );
4678−
4679− let _ = std::fs::remove_dir_all(&dir);
4680−}
4681−
46824572#[test]
46834573fn auto_image_dimensions_not_for_urls() {
46844574 // Remote URLs should not trigger dimension detection
tests/snapshots/accessibility.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>Accessibility</title><link rel="preload" href="https://example.com/sunset.jpg" as="image"><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}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}@layer htmlang{.a{width:200px;}.b{display:flex;flex-direction:column;}}</style></head><body><img src="https://example.com/sunset.jpg" class="a" alt="A photo of sunset" fetchpriority="high"><div class="b" role="navigation" aria-label="Main menu"><a href="/" tabindex="0" aria-current="page">Home</a><a href="/about" tabindex="0">About</a></div><button aria-pressed="false" data-action="toggle">Toggle</button></body></html>
1+<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Accessibility</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}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}@layer htmlang{.a{width:200px;}.b{display:flex;flex-direction:column;}}</style></head><body><img src="https://example.com/sunset.jpg" class="a" alt="A photo of sunset"><div class="b" role="navigation" aria-label="Main menu"><a href="/" tabindex="0" aria-current="page">Home</a><a href="/about" tabindex="0">About</a></div><button aria-pressed="false" data-action="toggle">Toggle</button></body></html>
tests/snapshots/basic_elements.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>Basic Elements</title><link rel="preload" href="https://example.com/img.png" as="image"><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}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}@layer htmlang{.a{display:flex;flex-direction:row;gap:10px;}.b{font-weight:bold;}.c{display:flex;flex-direction:column;gap:5px;}.d{display:flex;flex-direction:column;padding:10px;}.e{margin:0;}.f{width:100px;}}</style></head><body><div class="a"><span class="b">Hello</span><span>World</span></div><div class="c"><div class="d"><span>Inner</span></div></div><p class="e">Some text here</p><img src="https://example.com/img.png" class="f" fetchpriority="high"><a href="https://example.com" rel="noopener noreferrer" target="_blank">Click me</a></body></html>
1+<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Basic Elements</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}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}@layer htmlang{.a{display:flex;flex-direction:row;gap:10px;}.b{font-weight:bold;}.c{display:flex;flex-direction:column;gap:5px;}.d{display:flex;flex-direction:column;padding:10px;}.e{margin:0;}.f{width:100px;}}</style></head><body><div class="a"><span class="b">Hello</span><span>World</span></div><div class="c"><div class="d"><span>Inner</span></div></div><p class="e">Some text here</p><img src="https://example.com/img.png" class="f"><a href="https://example.com">Click me</a></body></html>
tests/snapshots/chain_operator.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>Chain</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}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}@layer htmlang{.a{display:flex;flex-direction:column;padding:10px;}.b{font-weight:bold;}.c{display:flex;flex-direction:row;gap:5px;}.d{display:flex;flex-direction:column;padding:20px;}.e{color:red;}}</style></head><body><div class="a"><a href="https://example.com" rel="noopener noreferrer" target="_blank"><span class="b">Click</span></a></div><div class="c"><div class="d"><span class="e">Deep chain</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>Chain</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}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}@layer htmlang{.a{display:flex;flex-direction:column;padding:10px;}.b{font-weight:bold;}.c{display:flex;flex-direction:row;gap:5px;}.d{display:flex;flex-direction:column;padding:20px;}.e{color:red;}}</style></head><body><div class="a"><a href="https://example.com"><span class="b">Click</span></a></div><div class="c"><div class="d"><span class="e">Deep chain</span></div></div></body></html>
tests/snapshots/new_css.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 CSS</title><link rel="preload" href="photo.jpg" as="image"><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;margin:10px;margin-inline:20px;margin-block:30px;}.b{width:200px;height:200px;object-fit:cover;object-position:center;}.c{display:flex;flex-direction:column;filter:blur(5px);pointer-events:none;user-select:none;}.d{display:flex;flex-direction:row;justify-content:space-between;align-items:center;}.e{display:flex;flex-direction:column;order:2;}.f{display:flex;flex-direction:column;background:url(bg.jpg);background-size:cover;background-position:center;background-repeat:no-repeat;}.g{text-shadow:1px 1px 2px black;text-overflow:ellipsis;}.h{display:flex;flex-direction:column;word-break:break-all;overflow-wrap:break-word;}}</style></head><body><div class="a"><span>Margins</span></div><img src="photo.jpg" class="b" fetchpriority="high"><div class="c"><span>Filtered</span></div><div class="d"><span>Left</span><span>Right</span></div><div class="e"><span>Second</span></div><div class="f"><span>Background</span></div><span class="g">Shadowed</span><div class="h"><span>Longtext</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 CSS</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;margin:10px;margin-inline:20px;margin-block:30px;}.b{width:200px;height:200px;object-fit:cover;object-position:center;}.c{display:flex;flex-direction:column;filter:blur(5px);pointer-events:none;user-select:none;}.d{display:flex;flex-direction:row;justify-content:space-between;align-items:center;}.e{display:flex;flex-direction:column;order:2;}.f{display:flex;flex-direction:column;background:url(bg.jpg);background-size:cover;background-position:center;background-repeat:no-repeat;}.g{text-shadow:1px 1px 2px black;text-overflow:ellipsis;}.h{display:flex;flex-direction:column;word-break:break-all;overflow-wrap:break-word;}}</style></head><body><div class="a"><span>Margins</span></div><img src="photo.jpg" class="b"><div class="c"><span>Filtered</span></div><div class="d"><span>Left</span><span>Right</span></div><div class="e"><span>Second</span></div><div class="f"><span>Background</span></div><span class="g">Shadowed</span><div class="h"><span>Longtext</span></div></body></html>
tests/snapshots/new_elements.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</title><link rel="preload" href="example.png" as="image"><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}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}@layer htmlang{.a{display:flex;flex-direction:column;}.b{display:flex;flex-direction:column;margin:0;padding:20px;}.c{display:flex;flex-direction:column;margin:0;}.d{width:200px;}.e{margin:0;white-space:pre;font-family:ui-monospace,monospace;}.f{font-family:ui-monospace,monospace;}}</style></head><body><form class="a" method="post" action="/submit"><label for="email">Email</label><input type="email" name="email" required><button type="submit">Submit</button></form><hr><details class="a" open><summary>Click to expand</summary><span>Hidden content here</span></details><blockquote class="b"><span>To be or not to be</span><cite>Shakespeare</cite></blockquote><figure class="c"><img src="example.png" class="d" alt="A code example" fetchpriority="high"><figcaption>A code example</figcaption></figure><pre class="e"><code class="f">console.log(&quot;hello&quot;)</code></pre><progress value="70" max="100"></progress><meter value="6" min="0" max="10" low="3" high="8"></meter></body></html>
1+<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>New Elements</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}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}@layer htmlang{.a{display:flex;flex-direction:column;}.b{display:flex;flex-direction:column;margin:0;padding:20px;}.c{display:flex;flex-direction:column;margin:0;}.d{width:200px;}.e{margin:0;white-space:pre;font-family:ui-monospace,monospace;}.f{font-family:ui-monospace,monospace;}}</style></head><body><form class="a" method="post" action="/submit"><label for="email">Email</label><input type="email" name="email" required><button type="submit">Submit</button></form><hr><details class="a" open><summary>Click to expand</summary><span>Hidden content here</span></details><blockquote class="b"><span>To be or not to be</span><cite>Shakespeare</cite></blockquote><figure class="c"><img src="example.png" class="d" alt="A code example"><figcaption>A code example</figcaption></figure><pre class="e"><code class="f">console.log(&quot;hello&quot;)</code></pre><progress value="70" max="100"></progress><meter value="6" min="0" max="10" low="3" high="8"></meter></body></html>
tests/snapshots/new_elements_2.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 2</title><link rel="preload" href="photo.jpg" as="image"><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}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}@layer htmlang{.a{display:flex;flex-direction:column;}.b{font-weight:bold;}.c{margin:0;}.d{margin:0;display:flex;flex-direction:column;}.e{display:flex;flex-direction:column;border:1px solid currentColor;padding:8px;margin:0;}}</style></head><body><dialog class="a" id="modal" open><span class="b">Dialog Title</span><span>Dialog content here</span><button>Close</button></dialog><dl class="c"><dt>Term One</dt><dd class="d">Definition of term one</dd><dt>Term Two</dt><dd class="d">Definition of term two</dd></dl><fieldset class="e"><legend>Personal Info</legend><label for="name">Name</label><input type="text" name="name"></fieldset><picture><source srcset="photo-wide.jpg" media="(min-width: 800px)"><img src="photo.jpg" alt="A photo" fetchpriority="high"></picture><time datetime="2026-04-15">April 15, 2026</time><span>This is {@mark highlighted} text</span><span>Press {@kbd Ctrl+C} to copy</span><abbr title="HyperText Markup Language">HTML</abbr><input type="text" name="browser" list="browsers"><datalist id="browsers"><option>Chrome</option><option>Firefox</option><option>Safari</option></datalist></body></html>
1+<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>New Elements 2</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}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}@layer htmlang{.a{display:flex;flex-direction:column;}.b{font-weight:bold;}.c{margin:0;}.d{margin:0;display:flex;flex-direction:column;}.e{display:flex;flex-direction:column;border:1px solid currentColor;padding:8px;margin:0;}}</style></head><body><dialog class="a" id="modal" open><span class="b">Dialog Title</span><span>Dialog content here</span><button>Close</button></dialog><dl class="c"><dt>Term One</dt><dd class="d">Definition of term one</dd><dt>Term Two</dt><dd class="d">Definition of term two</dd></dl><fieldset class="e"><legend>Personal Info</legend><label for="name">Name</label><input type="text" name="name"></fieldset><picture><source srcset="photo-wide.jpg" media="(min-width: 800px)"><img src="photo.jpg" alt="A photo"></picture><time datetime="2026-04-15">April 15, 2026</time><span>This is {@mark highlighted} text</span><span>Press {@kbd Ctrl+C} to copy</span><abbr title="HyperText Markup Language">HTML</abbr><input type="text" name="browser" list="browsers"><datalist id="browsers"><option>Chrome</option><option>Firefox</option><option>Safari</option></datalist></body></html>
tests/snapshots/new_html_attrs.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>HTML Attrs</title><link rel="preload" href="hero.jpg" as="image"><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}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}@layer htmlang{.a{display:flex;flex-direction:column;}.b{width:200px;}}</style></head><body><form action="/search" class="a" method="post"><input type="search" inputmode="search" enterkeyhint="search" placeholder="Search..." spellcheck="false"><input type="email" inputmode="email" enterkeyhint="done" translate="no"><img src="hero.jpg" class="b" fetchpriority="high"><button type="submit">Go</button></form></body></html>
1+<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>HTML Attrs</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}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}@layer htmlang{.a{display:flex;flex-direction:column;}.b{width:200px;}}</style></head><body><form action="/search" class="a" method="post"><input type="search" inputmode="search" enterkeyhint="search" placeholder="Search..." spellcheck="false"><input type="email" inputmode="email" enterkeyhint="done" translate="no"><img src="hero.jpg" class="b" fetchpriority="high"><button type="submit">Go</button></form></body></html>
tests/snapshots/new_pseudos.html+1−1
@@ -1 +1 @@
1−<style>@layer htmlang{.b{display:flex;flex-direction:column;}.a:visited{color:purple;}.b:empty{display:none;}.b:target{background:yellow;}.c:valid{border:2px solid green;}.d:invalid{border:2px solid red;}}</style><a href="https://example.com" rel="noopener noreferrer" target="_blank" class="a">Already visited</a><div class="b"><span>Conditional content</span></div><input class="c" type="email"><input class="d" type="text">
1+<style>@layer htmlang{.b{display:flex;flex-direction:column;}.a:visited{color:purple;}.b:empty{display:none;}.b:target{background:yellow;}.c:valid{border:2px solid green;}.d:invalid{border:2px solid red;}}</style><a href="https://example.com" class="a">Already visited</a><div class="b"><span>Conditional content</span></div><input class="c" type="email"><input class="d" type="text">
tests/snapshots/responsive_images.hl+1−1
@@ -1,3 +1,3 @@
11@page Responsive Images
2−@image [responsive 400 800 1200, alt=Hero photo] hero.jpg
2+@image [srcset="hero-400.jpg 400w, hero-800.jpg 800w", sizes=100vw, alt=Hero photo] hero.jpg
33@image [alt=Small icon] icon.png
tests/snapshots/responsive_images.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>Responsive Images</title><link rel="preload" href="hero.jpg" as="image"><link rel="preload" href="icon.png" as="image"><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{}</style></head><body><img src="hero.jpg" alt="Hero photo" srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w" sizes="(max-width: 1200px) 100vw, 1200px" fetchpriority="high"><img src="icon.png" alt="Small icon" fetchpriority="high"></body></html>
1+<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Responsive Images</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{}</style></head><body><img src="hero.jpg" srcset="hero-400.jpg 400w, hero-800.jpg 800w" sizes="100vw" alt="Hero photo"><img src="icon.png" alt="Small icon"></body></html>
tests/snapshots/semantic_elements.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>Semantic Elements</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}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}.hl-skip{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;z-index:9999;padding:8px 16px;background:#000;color:#fff;text-decoration:none;font-size:14px}.hl-skip:focus{left:8px;top:8px;width:auto;height:auto;overflow:visible}}@layer htmlang{.a{display:flex;flex-direction:column;padding:20px;background:#f0f0f0;}.b{font-weight:bold;font-size:24px;}.c{display:flex;flex-direction:column;gap:10px;}.d{display:flex;flex-direction:column;padding:20px;}.e{display:flex;flex-direction:column;padding:16px;border:1px solid #ddd;border-radius:8px;}.f{font-weight:bold;}.g{margin:0;}.h{display:flex;flex-direction:column;padding:16px;}.i{display:flex;flex-direction:column;padding:16px;background:#f8f8f8;}.j{display:flex;flex-direction:column;padding:20px;background:#333;color:white;}}</style></head><body><a href="#hl-main" class="hl-skip">Skip to content</a><header class="a"><span class="b">My Site</span></header><nav class="c"><a href="/">Home</a><a href="/about">About</a><a href="/contact">Contact</a></nav><main id="hl-main" class="d"><article class="e"><span class="f">Article Title</span><p class="g">Some article content here</p></article><section class="h"><span class="f">Section Title</span><p class="g">Section content</p></section></main><aside class="i"><span>Related links</span></aside><footer class="j"><span>Copyright 2024</span></footer></body></html>
1+<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Semantic Elements</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}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}@layer htmlang{.a{display:flex;flex-direction:column;padding:20px;background:#f0f0f0;}.b{font-weight:bold;font-size:24px;}.c{display:flex;flex-direction:column;gap:10px;}.d{display:flex;flex-direction:column;padding:20px;}.e{display:flex;flex-direction:column;padding:16px;border:1px solid #ddd;border-radius:8px;}.f{font-weight:bold;}.g{margin:0;}.h{display:flex;flex-direction:column;padding:16px;}.i{display:flex;flex-direction:column;padding:16px;background:#f8f8f8;}.j{display:flex;flex-direction:column;padding:20px;background:#333;color:white;}}</style></head><body><header class="a"><span class="b">My Site</span></header><nav class="c"><a href="/">Home</a><a href="/about">About</a><a href="/contact">Contact</a></nav><main class="d"><article class="e"><span class="f">Article Title</span><p class="g">Some article content here</p></article><section class="h"><span class="f">Section Title</span><p class="g">Section content</p></section></main><aside class="i"><span>Related links</span></aside><footer class="j"><span>Copyright 2024</span></footer></body></html>