Skip to content

Commit b825df6

Browse files
uwnilaurmaedje
andauthored
Allow custom element names in HTML tag syntax (typst#6676)
Co-authored-by: Laurenz <[email protected]>
1 parent cd1a786 commit b825df6

File tree

3 files changed

+77
-2
lines changed

3 files changed

+77
-2
lines changed

crates/typst-html/src/dom.rs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,53 @@ impl HtmlTag {
105105
bail!("tag name must not be empty");
106106
}
107107

108-
if let Some(c) = string.chars().find(|&c| !charsets::is_valid_in_tag_name(c)) {
109-
bail!("the character {} is not valid in a tag name", c.repr());
108+
let mut has_hyphen = false;
109+
let mut has_uppercase = false;
110+
111+
for c in string.chars() {
112+
if c == '-' {
113+
has_hyphen = true;
114+
} else if !charsets::is_valid_in_tag_name(c) {
115+
bail!("the character {} is not valid in a tag name", c.repr());
116+
} else {
117+
has_uppercase |= c.is_ascii_uppercase();
118+
}
119+
}
120+
121+
// If we encounter a hyphen, we are dealing with a custom element rather
122+
// than a standard HTML element.
123+
//
124+
// A valid custom element name must:
125+
// - Contain at least one hyphen (U+002D)
126+
// - Start with an ASCII lowercase letter (a-z)
127+
// - Not contain any ASCII uppercase letters (A-Z)
128+
// - Not be one of the reserved names
129+
// - Only contain valid characters (ASCII alphanumeric and hyphens)
130+
//
131+
// See https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
132+
if has_hyphen {
133+
if !string.starts_with(|c: char| c.is_ascii_lowercase()) {
134+
bail!("custom element name must start with a lowercase letter");
135+
}
136+
if has_uppercase {
137+
bail!("custom element name must not contain uppercase letters");
138+
}
139+
140+
// These names are used in SVG and MathML. Since `html.elem` only
141+
// supports creation of _HTML_ elements, they are forbidden.
142+
if matches!(
143+
string,
144+
"annotation-xml"
145+
| "color-profile"
146+
| "font-face"
147+
| "font-face-src"
148+
| "font-face-uri"
149+
| "font-face-format"
150+
| "font-face-name"
151+
| "missing-glyph"
152+
) {
153+
bail!("name is reserved and not valid for a custom element");
154+
}
110155
}
111156

112157
Ok(Self(PicoStr::intern(string)))
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
</head>
7+
<body><my-element>Hi</my-element><custom-button>Hi</custom-button><multi-word-component>Hi</multi-word-component><element->Hi</element-></body>
8+
</html>

tests/suite/html/elem.typ

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,25 @@ Text
1313
val
1414
})
1515
#metadata("Hi") <l>
16+
17+
--- html-elem-custom html ---
18+
#html.elem("my-element")[Hi]
19+
#html.elem("custom-button")[Hi]
20+
#html.elem("multi-word-component")[Hi]
21+
#html.elem("element-")[Hi]
22+
23+
--- html-elem-invalid ---
24+
// Error: 12-24 the character "@" is not valid in a tag name
25+
#html.elem("my@element")
26+
27+
--- html-elem-custom-bad-start html ---
28+
// Error: 12-22 custom element name must start with a lowercase letter
29+
#html.elem("1-custom")
30+
31+
--- html-elem-custom-uppercase html ---
32+
// Error: 12-21 custom element name must not contain uppercase letters
33+
#html.elem("my-ELEM")
34+
35+
--- html-elem-custom-reserved html ---
36+
// Error: 12-28 name is reserved and not valid for a custom element
37+
#html.elem("annotation-xml")

0 commit comments

Comments
 (0)