From b825df6bbc81856b979848361c4f11ce5ed61810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=9A=E9=B4=89?= Date: Fri, 1 Aug 2025 21:57:07 +0900 Subject: [PATCH] Allow custom element names in HTML tag syntax (#6676) Co-authored-by: Laurenz --- crates/typst-html/src/dom.rs | 49 ++++++++++++++++++++++++++-- tests/ref/html/html-elem-custom.html | 8 +++++ tests/suite/html/elem.typ | 22 +++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 tests/ref/html/html-elem-custom.html diff --git a/crates/typst-html/src/dom.rs b/crates/typst-html/src/dom.rs index 2c9a2e64..48b433fd 100644 --- a/crates/typst-html/src/dom.rs +++ b/crates/typst-html/src/dom.rs @@ -105,8 +105,53 @@ impl HtmlTag { bail!("tag name must not be empty"); } - if let Some(c) = string.chars().find(|&c| !charsets::is_valid_in_tag_name(c)) { - bail!("the character {} is not valid in a tag name", c.repr()); + let mut has_hyphen = false; + let mut has_uppercase = false; + + for c in string.chars() { + if c == '-' { + has_hyphen = true; + } else if !charsets::is_valid_in_tag_name(c) { + bail!("the character {} is not valid in a tag name", c.repr()); + } else { + has_uppercase |= c.is_ascii_uppercase(); + } + } + + // If we encounter a hyphen, we are dealing with a custom element rather + // than a standard HTML element. + // + // A valid custom element name must: + // - Contain at least one hyphen (U+002D) + // - Start with an ASCII lowercase letter (a-z) + // - Not contain any ASCII uppercase letters (A-Z) + // - Not be one of the reserved names + // - Only contain valid characters (ASCII alphanumeric and hyphens) + // + // See https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name + if has_hyphen { + if !string.starts_with(|c: char| c.is_ascii_lowercase()) { + bail!("custom element name must start with a lowercase letter"); + } + if has_uppercase { + bail!("custom element name must not contain uppercase letters"); + } + + // These names are used in SVG and MathML. Since `html.elem` only + // supports creation of _HTML_ elements, they are forbidden. + if matches!( + string, + "annotation-xml" + | "color-profile" + | "font-face" + | "font-face-src" + | "font-face-uri" + | "font-face-format" + | "font-face-name" + | "missing-glyph" + ) { + bail!("name is reserved and not valid for a custom element"); + } } Ok(Self(PicoStr::intern(string))) diff --git a/tests/ref/html/html-elem-custom.html b/tests/ref/html/html-elem-custom.html new file mode 100644 index 00000000..bd917117 --- /dev/null +++ b/tests/ref/html/html-elem-custom.html @@ -0,0 +1,8 @@ + + + + + + + HiHiHiHi + diff --git a/tests/suite/html/elem.typ b/tests/suite/html/elem.typ index b416fdf9..54c82f7f 100644 --- a/tests/suite/html/elem.typ +++ b/tests/suite/html/elem.typ @@ -13,3 +13,25 @@ Text val }) #metadata("Hi") + +--- html-elem-custom html --- +#html.elem("my-element")[Hi] +#html.elem("custom-button")[Hi] +#html.elem("multi-word-component")[Hi] +#html.elem("element-")[Hi] + +--- html-elem-invalid --- +// Error: 12-24 the character "@" is not valid in a tag name +#html.elem("my@element") + +--- html-elem-custom-bad-start html --- +// Error: 12-22 custom element name must start with a lowercase letter +#html.elem("1-custom") + +--- html-elem-custom-uppercase html --- +// Error: 12-21 custom element name must not contain uppercase letters +#html.elem("my-ELEM") + +--- html-elem-custom-reserved html --- +// Error: 12-28 name is reserved and not valid for a custom element +#html.elem("annotation-xml")