on master help sign in
Add h1–h6 heading elements
c3d40c1iko
crates/htmlang-core/src/ast.rs+7
@@ -138,6 +138,13 @@ pub enum ElementKind {
138 Iframe,
138 Iframe,
139 Output,
139 Output,
140 Canvas,
140 Canvas,
141+ // Heading elements
142+ H1,
143+ H2,
144+ H3,
145+ H4,
146+ H5,
147+ H6,
141 // New convenience elements
148 // New convenience elements
142 Grid,
149 Grid,
143 Stack,
150 Stack,
crates/htmlang-core/src/codegen.rs+36−2
@@ -1768,6 +1768,13 @@ fn generate_element(
1768 ElementKind::Noscript => "noscript",
1768 ElementKind::Noscript => "noscript",
1769 ElementKind::Address => "address",
1769 ElementKind::Address => "address",
1770 ElementKind::Search => "search",
1770 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",
1771 ElementKind::Image
1778 ElementKind::Image
1772 | ElementKind::Input
1779 | ElementKind::Input
1773 | ElementKind::HorizontalRule
1780 | ElementKind::HorizontalRule
@@ -1845,6 +1852,12 @@ fn generate_element(
1845 ElementKind::Noscript => "noscript",
1852 ElementKind::Noscript => "noscript",
1846 ElementKind::Address => "address",
1853 ElementKind::Address => "address",
1847 ElementKind::Search => "search",
1854 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",
1848 _ => "",
1861 _ => "",
1849 };
1862 };
1850
1863
@@ -2000,6 +2013,12 @@ fn generate_element(
2000 | ElementKind::Tooltip
2013 | ElementKind::Tooltip
2001 | ElementKind::Chip
2014 | ElementKind::Chip
2002 | ElementKind::Tag
2015 | ElementKind::Tag
2016+ | ElementKind::H1
2017+ | ElementKind::H2
2018+ | ElementKind::H3
2019+ | ElementKind::H4
2020+ | ElementKind::H5
2021+ | ElementKind::H6
2003 ) && let Some(text) = &elem.argument
2022 ) && let Some(text) = &elem.argument
2004 {
2023 {
2005 out.push_str(&html_escape(text));
2024 out.push_str(&html_escape(text));
@@ -2007,7 +2026,16 @@ fn generate_element(
2007
2026
2008 // Children
2027 // Children
2009 ctx.depth += 1;
2028 ctx.depth += 1;
2029+ let is_paragraph = matches!(
2030+ elem.kind,
2010− let is_paragraph = elem.kind == ElementKind::Paragraph;
2031+ ElementKind::Paragraph
2032+ | ElementKind::H1
2033+ | ElementKind::H2
2034+ | ElementKind::H3
2035+ | ElementKind::H4
2036+ | ElementKind::H5
2037+ | ElementKind::H6
2038+ );
2011 for (i, child) in elem.children.iter().enumerate() {
2039 for (i, child) in elem.children.iter().enumerate() {
2012 generate_node(child, Some(&elem.kind), out, styles, ctx);
2040 generate_node(child, Some(&elem.kind), out, styles, ctx);
2013 if is_paragraph && i < elem.children.len() - 1 {
2041 if is_paragraph && i < elem.children.len() - 1 {
@@ -2537,7 +2565,13 @@ fn attrs_to_css(
2537 | ElementKind::Form
2565 | ElementKind::Form
2538 | ElementKind::Details
2566 | ElementKind::Details
2539 | ElementKind::Dialog => css.push_str(FLEX_COLUMN),
2567 | 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;"),
2541 // Lists: reset browser defaults
2575 // Lists: reset browser defaults
2542 ElementKind::List => css.push_str("margin:0;padding-left:0;list-style:none;"),
2576 ElementKind::List => css.push_str("margin:0;padding-left:0;list-style:none;"),
2543 // Figure / Blockquote: flex column with browser margin reset
2577 // Figure / Blockquote: flex column with browser margin reset
crates/htmlang-core/src/parser.rs+18
@@ -3337,6 +3337,12 @@ const KNOWN_ELEMENTS: &[&str] = &[
3337 "iframe",
3337 "iframe",
3338 "output",
3338 "output",
3339 "canvas",
3339 "canvas",
3340+ "h1",
3341+ "h2",
3342+ "h3",
3343+ "h4",
3344+ "h5",
3345+ "h6",
3340 "grid",
3346 "grid",
3341 "stack",
3347 "stack",
3342 "spacer",
3348 "spacer",
@@ -3462,6 +3468,12 @@ fn parse_element_kind(s: &str, line_num: usize) -> Result<ElementKind, ParseErro
3462 "mark" => Ok(ElementKind::Mark),
3468 "mark" => Ok(ElementKind::Mark),
3463 "kbd" => Ok(ElementKind::Kbd),
3469 "kbd" => Ok(ElementKind::Kbd),
3464 "abbr" => Ok(ElementKind::Abbr),
3470 "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),
3465 "datalist" => Ok(ElementKind::Datalist),
3477 "datalist" => Ok(ElementKind::Datalist),
3466 "iframe" => Ok(ElementKind::Iframe),
3478 "iframe" => Ok(ElementKind::Iframe),
3467 "output" => Ok(ElementKind::Output),
3479 "output" => Ok(ElementKind::Output),
@@ -5037,6 +5049,12 @@ fn element_kind_name(kind: &ElementKind) -> &'static str {
5037 ElementKind::Mark => "@mark",
5049 ElementKind::Mark => "@mark",
5038 ElementKind::Kbd => "@kbd",
5050 ElementKind::Kbd => "@kbd",
5039 ElementKind::Abbr => "@abbr",
5051 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",
5040 ElementKind::Datalist => "@datalist",
5058 ElementKind::Datalist => "@datalist",
5041 ElementKind::Iframe => "@iframe",
5059 ElementKind::Iframe => "@iframe",
5042 ElementKind::Output => "@output",
5060 ElementKind::Output => "@output",
src/bin/htmlang_lsp/completion.rs+7
@@ -210,6 +210,13 @@ fn element_completions(range: Range) -> Vec<CompletionItem> {
210 "@source",
210 "@source",
211 "Media source for @picture/@video/@audio (source)",
211 "Media source for @picture/@video/@audio (source)",
212 ),
212 ),
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)"),
213 // Inline semantics
220 // Inline semantics
214 ("@time", "Date/time element (time)"),
221 ("@time", "Date/time element (time)"),
215 ("@mark", "Highlighted/marked text (mark)"),
222 ("@mark", "Highlighted/marked text (mark)"),
src/bin/htmlang_lsp/hover.rs+6
@@ -228,6 +228,12 @@ fn hover_builtin(word: &str) -> Option<String> {
228 "@paragraph" | "@p" => {
228 "@paragraph" | "@p" => {
229 "**@paragraph** \u{2014} Text block\n\nRenders as `<p>`.\n\nSupports inline elements: `{@text [bold] word}`"
229 "**@paragraph** \u{2014} Text block\n\nRenders as `<p>`.\n\nSupports inline elements: `{@text [bold] word}`"
230 }
230 }
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>`.",
231 "@image" | "@img" => {
237 "@image" | "@img" => {
232 "**@image** \u{2014} Image\n\nRenders as `<img>`.\n\nUsage: `@image [width 200] https://example.com/photo.jpg`"
238 "**@image** \u{2014} Image\n\nRenders as `<img>`.\n\nUsage: `@image [width 200] https://example.com/photo.jpg`"
233 }
239 }
src/convert.rs+9−15
@@ -863,7 +863,12 @@ fn emit_element(
863 "time" => "@time",
863 "time" => "@time",
864 "progress" => "@progress",
864 "progress" => "@progress",
865 "meter" => "@meter",
865 "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",
867 "b" | "strong" => "@text",
872 "b" | "strong" => "@text",
868 "i" | "em" => "@text",
873 "i" | "em" => "@text",
869 "u" => "@text",
874 "u" => "@text",
@@ -898,17 +903,6 @@ fn emit_element(
898 hl_attrs.insert(0, ("id".into(), Some(id.clone())));
903 hl_attrs.insert(0, ("id".into(), Some(id.clone())));
899 }
904 }
900
905
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−
912 // Bold/italic for strong/em/b/i/u
906 // Bold/italic for strong/em/b/i/u
913 match tag {
907 match tag {
914 "b" | "strong" => {
908 "b" | "strong" => {
@@ -1226,11 +1220,11 @@ mod tests {
1226 }
1220 }
1227
1221
1228 #[test]
1222 #[test]
1229− fn heading_sizes() {
1223+ fn heading_elements() {
1230 let h1 = convert("<h1>Title</h1>");
1224 let h1 = convert("<h1>Title</h1>");
1231− assert!(h1.contains("@paragraph [size 32]"));
1225+ assert!(h1.contains("@h1"));
1232 let h3 = convert("<h3>Sub</h3>");
1226 let h3 = convert("<h3>Sub</h3>");
1233− assert!(h3.contains("@paragraph [size 20]"));
1227+ assert!(h3.contains("@h3"));
1234 }
1228 }
1235
1229
1236 #[test]
1230 #[test]