on master help sign in
Add h1–h6 heading elements
commitc3d40c1ed396b38be3f3735902ce33495af90393 authoriko <mail@iko.soy> dateSat 25 Apr 2026, 15:49 (5 months ago) parent127f12a
6 files changed+83−17
crates/htmlang-core/src/ast.rs+7 crates/htmlang-core/src/codegen.rs+36−2 crates/htmlang-core/src/parser.rs+18 src/bin/htmlang_lsp/completion.rs+7 src/bin/htmlang_lsp/hover.rs+6 src/convert.rs+9−15
crates/htmlang-core/src/ast.rs+7
@@ -138,6 +138,13 @@ pub enum ElementKind {
138138 Iframe,
139139 Output,
140140 Canvas,
141+ // Heading elements
142+ H1,
143+ H2,
144+ H3,
145+ H4,
146+ H5,
147+ H6,
141148 // New convenience elements
142149 Grid,
143150 Stack,
crates/htmlang-core/src/codegen.rs+36−2
@@ -1768,6 +1768,13 @@ fn generate_element(
17681768 ElementKind::Noscript => "noscript",
17691769 ElementKind::Address => "address",
17701770 ElementKind::Search => "search",
1771+ // Heading elements
1772+ ElementKind::H1 => "h1",
1773+ ElementKind::H2 => "h2",
1774+ ElementKind::H3 => "h3",
1775+ ElementKind::H4 => "h4",
1776+ ElementKind::H5 => "h5",
1777+ ElementKind::H6 => "h6",
17711778 ElementKind::Image
17721779 | ElementKind::Input
17731780 | ElementKind::HorizontalRule
@@ -1845,6 +1852,12 @@ fn generate_element(
18451852 ElementKind::Noscript => "noscript",
18461853 ElementKind::Address => "address",
18471854 ElementKind::Search => "search",
1855+ ElementKind::H1 => "h1",
1856+ ElementKind::H2 => "h2",
1857+ ElementKind::H3 => "h3",
1858+ ElementKind::H4 => "h4",
1859+ ElementKind::H5 => "h5",
1860+ ElementKind::H6 => "h6",
18481861 _ => "",
18491862 };
18501863
@@ -2000,6 +2013,12 @@ fn generate_element(
20002013 | ElementKind::Tooltip
20012014 | ElementKind::Chip
20022015 | ElementKind::Tag
2016+ | ElementKind::H1
2017+ | ElementKind::H2
2018+ | ElementKind::H3
2019+ | ElementKind::H4
2020+ | ElementKind::H5
2021+ | ElementKind::H6
20032022 ) && let Some(text) = &elem.argument
20042023 {
20052024 out.push_str(&html_escape(text));
@@ -2007,7 +2026,16 @@ fn generate_element(
20072026
20082027 // Children
20092028 ctx.depth += 1;
2010− let is_paragraph = elem.kind == ElementKind::Paragraph;
2029+ let is_paragraph = matches!(
2030+ elem.kind,
2031+ ElementKind::Paragraph
2032+ | ElementKind::H1
2033+ | ElementKind::H2
2034+ | ElementKind::H3
2035+ | ElementKind::H4
2036+ | ElementKind::H5
2037+ | ElementKind::H6
2038+ );
20112039 for (i, child) in elem.children.iter().enumerate() {
20122040 generate_node(child, Some(&elem.kind), out, styles, ctx);
20132041 if is_paragraph && i < elem.children.len() - 1 {
@@ -2537,7 +2565,13 @@ fn attrs_to_css(
25372565 | ElementKind::Form
25382566 | ElementKind::Details
25392567 | ElementKind::Dialog => css.push_str(FLEX_COLUMN),
2540− ElementKind::Paragraph => css.push_str("margin:0;"),
2568+ ElementKind::Paragraph
2569+ | ElementKind::H1
2570+ | ElementKind::H2
2571+ | ElementKind::H3
2572+ | ElementKind::H4
2573+ | ElementKind::H5
2574+ | ElementKind::H6 => css.push_str("margin:0;"),
25412575 // Lists: reset browser defaults
25422576 ElementKind::List => css.push_str("margin:0;padding-left:0;list-style:none;"),
25432577 // Figure / Blockquote: flex column with browser margin reset
crates/htmlang-core/src/parser.rs+18
@@ -3337,6 +3337,12 @@ const KNOWN_ELEMENTS: &[&str] = &[
33373337 "iframe",
33383338 "output",
33393339 "canvas",
3340+ "h1",
3341+ "h2",
3342+ "h3",
3343+ "h4",
3344+ "h5",
3345+ "h6",
33403346 "grid",
33413347 "stack",
33423348 "spacer",
@@ -3462,6 +3468,12 @@ fn parse_element_kind(s: &str, line_num: usize) -> Result<ElementKind, ParseErro
34623468 "mark" => Ok(ElementKind::Mark),
34633469 "kbd" => Ok(ElementKind::Kbd),
34643470 "abbr" => Ok(ElementKind::Abbr),
3471+ "h1" => Ok(ElementKind::H1),
3472+ "h2" => Ok(ElementKind::H2),
3473+ "h3" => Ok(ElementKind::H3),
3474+ "h4" => Ok(ElementKind::H4),
3475+ "h5" => Ok(ElementKind::H5),
3476+ "h6" => Ok(ElementKind::H6),
34653477 "datalist" => Ok(ElementKind::Datalist),
34663478 "iframe" => Ok(ElementKind::Iframe),
34673479 "output" => Ok(ElementKind::Output),
@@ -5037,6 +5049,12 @@ fn element_kind_name(kind: &ElementKind) -> &'static str {
50375049 ElementKind::Mark => "@mark",
50385050 ElementKind::Kbd => "@kbd",
50395051 ElementKind::Abbr => "@abbr",
5052+ ElementKind::H1 => "@h1",
5053+ ElementKind::H2 => "@h2",
5054+ ElementKind::H3 => "@h3",
5055+ ElementKind::H4 => "@h4",
5056+ ElementKind::H5 => "@h5",
5057+ ElementKind::H6 => "@h6",
50405058 ElementKind::Datalist => "@datalist",
50415059 ElementKind::Iframe => "@iframe",
50425060 ElementKind::Output => "@output",
src/bin/htmlang_lsp/completion.rs+7
@@ -210,6 +210,13 @@ fn element_completions(range: Range) -> Vec<CompletionItem> {
210210 "@source",
211211 "Media source for @picture/@video/@audio (source)",
212212 ),
213+ // Heading elements
214+ ("@h1", "Heading level 1 (h1)"),
215+ ("@h2", "Heading level 2 (h2)"),
216+ ("@h3", "Heading level 3 (h3)"),
217+ ("@h4", "Heading level 4 (h4)"),
218+ ("@h5", "Heading level 5 (h5)"),
219+ ("@h6", "Heading level 6 (h6)"),
213220 // Inline semantics
214221 ("@time", "Date/time element (time)"),
215222 ("@mark", "Highlighted/marked text (mark)"),
src/bin/htmlang_lsp/hover.rs+6
@@ -228,6 +228,12 @@ fn hover_builtin(word: &str) -> Option<String> {
228228 "@paragraph" | "@p" => {
229229 "**@paragraph** \u{2014} Text block\n\nRenders as `<p>`.\n\nSupports inline elements: `{@text [bold] word}`"
230230 }
231+ "@h1" => "**@h1** \u{2014} Heading level 1\n\nRenders as `<h1>`.\n\nUsage: `@h1 Page Title`",
232+ "@h2" => "**@h2** \u{2014} Heading level 2\n\nRenders as `<h2>`.\n\nUsage: `@h2 Section Title`",
233+ "@h3" => "**@h3** \u{2014} Heading level 3\n\nRenders as `<h3>`.\n\nUsage: `@h3 Subsection`",
234+ "@h4" => "**@h4** \u{2014} Heading level 4\n\nRenders as `<h4>`.",
235+ "@h5" => "**@h5** \u{2014} Heading level 5\n\nRenders as `<h5>`.",
236+ "@h6" => "**@h6** \u{2014} Heading level 6\n\nRenders as `<h6>`.",
231237 "@image" | "@img" => {
232238 "**@image** \u{2014} Image\n\nRenders as `<img>`.\n\nUsage: `@image [width 200] https://example.com/photo.jpg`"
233239 }
src/convert.rs+9−15
@@ -863,7 +863,12 @@ fn emit_element(
863863 "time" => "@time",
864864 "progress" => "@progress",
865865 "meter" => "@meter",
866− "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => "@paragraph",
866+ "h1" => "@h1",
867+ "h2" => "@h2",
868+ "h3" => "@h3",
869+ "h4" => "@h4",
870+ "h5" => "@h5",
871+ "h6" => "@h6",
867872 "b" | "strong" => "@text",
868873 "i" | "em" => "@text",
869874 "u" => "@text",
@@ -898,17 +903,6 @@ fn emit_element(
898903 hl_attrs.insert(0, ("id".into(), Some(id.clone())));
899904 }
900905
901− // Special handling for heading sizes
902− match tag {
903− "h1" => hl_attrs.push(("size".into(), Some("32".into()))),
904− "h2" => hl_attrs.push(("size".into(), Some("24".into()))),
905− "h3" => hl_attrs.push(("size".into(), Some("20".into()))),
906− "h4" => hl_attrs.push(("size".into(), Some("18".into()))),
907− "h5" => hl_attrs.push(("size".into(), Some("16".into()))),
908− "h6" => hl_attrs.push(("size".into(), Some("14".into()))),
909− _ => {}
910− }
911−
912906 // Bold/italic for strong/em/b/i/u
913907 match tag {
914908 "b" | "strong" => {
@@ -1226,11 +1220,11 @@ mod tests {
12261220 }
12271221
12281222 #[test]
1229− fn heading_sizes() {
1223+ fn heading_elements() {
12301224 let h1 = convert("<h1>Title</h1>");
1231− assert!(h1.contains("@paragraph [size 32]"));
1225+ assert!(h1.contains("@h1"));
12321226 let h3 = convert("<h3>Sub</h3>");
1233− assert!(h3.contains("@paragraph [size 20]"));
1227+ assert!(h3.contains("@h3"));
12341228 }
12351229
12361230 #[test]