struct Shape {
len_mask: [u32; 26],
min_len: usize,
max_len: usize,
}
const fn shape_of(names: &[&'static str]) -> Shape {
let mut len_mask = [0u32; 26];
let mut min_len = usize::MAX;
let mut max_len = 0;
let mut i = 0;
while i < names.len() {
let bytes = names[i].as_bytes();
let first = bytes[0];
assert!(
first.is_ascii_lowercase(),
"element name must start with an ASCII lowercase letter"
);
assert!(bytes.len() < 32, "element name must be under 32 bytes");
len_mask[(first - b'a') as usize] |= 1 << bytes.len();
if bytes.len() < min_len {
min_len = bytes.len();
}
if bytes.len() > max_len {
max_len = bytes.len();
}
i += 1;
}
Shape {
len_mask,
min_len,
max_len,
}
}
#[inline]
fn shape_admits(shape: &Shape, tag_name: &str) -> bool {
let bytes = tag_name.as_bytes();
let len = bytes.len();
if len < shape.min_len || len > shape.max_len {
return false;
}
let first = bytes[0];
if !first.is_ascii_lowercase() {
return false;
}
shape.len_mask[(first - b'a') as usize] & (1 << len) != 0
}
macro_rules! element_set {
($contains:ident, $shape:ident, $names:ident, $doc:literal, [$($name:literal),* $(,)?]) => {
#[doc = $doc]
#[inline]
fn $contains(tag_name: &str) -> bool {
matches!(tag_name, $($name)|*)
}
const $names: &[&'static str] = &[$($name),*];
const $shape: Shape = shape_of($names);
};
}
element_set! {
block_set_contains,
BLOCK_SHAPE,
BLOCK_NAMES,
"Block elements for formatting purposes.\n\n\
Elements NOT in this list are treated as inline for formatting (including table cells), \
matching prettier's logic: `isInlineElement = !isBlockElement`. Intentionally absent: \
`<center>`, `<select>`, `<svg>`, `<math>` — prettier-plugin-svelte omits these, and \
`<svg>`/`<math>` are handled separately as foreign elements. Two members are here for spec \
compliance where prettier-plugin-svelte omits them: `<menu>`, which the HTML spec treats \
identically to `<ul>`, and `<summary>`, whose every UA rule is block-level — plain \
`display: block`, and `display: list-item` (`li`'s own display, and `li` is a member) when \
it is a `details`' first summary. Note this list tracks UA *display*, not content model: \
`<p>` and `<summary>` are both phrasing-content-only and both block. See \
`tests/fixtures/svelte/elements/menu_block_prettier_divergence/README.md` and \
`tests/fixtures/svelte/elements/summary_block_prettier_divergence/README.md`.",
[
"address", "article", "aside", "blockquote", "details", "dialog", "dd", "div", "dl", "dt",
"fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6",
"header", "hgroup", "hr", "li", "main", "menu", "nav", "ol", "p", "pre", "section",
"summary", "table", "ul",
]
}
element_set! {
void_set_contains,
VOID_SHAPE,
VOID_NAMES,
"Void (self-closing) elements — Svelte's `VOID_ELEMENT_NAMES`.\n\n\
`command` and `keygen` are obsolete (removed from the HTML spec) but included for Svelte \
parity — the parser (tsv_svelte) also includes them. Svelte's `is_void` also answers \
`true` for a `!doctype` declaration; that is no element, so it is not here — a caller \
that reads Svelte's answer adds it (`tsv_svelte`'s `is_svelte_void`).",
[
"area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link",
"meta", "param", "source", "track", "wbr",
]
}
element_set! {
svg_set_contains,
SVG_SHAPE,
SVG_NAMES,
"SVG elements — synced with Svelte's `utils.js` `SVG_ELEMENTS`.",
[
"altGlyph", "altGlyphDef", "altGlyphItem", "animate", "animateColor", "animateMotion",
"animateTransform", "circle", "clipPath", "color-profile", "cursor", "defs", "desc",
"discard", "ellipse", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite",
"feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight",
"feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur",
"feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight",
"feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "filter", "font",
"font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri",
"foreignObject", "g", "glyph", "glyphRef", "hatch", "hatchpath", "hkern", "image", "line",
"linearGradient", "marker", "mask", "mesh", "meshgradient", "meshpatch", "meshrow",
"metadata", "missing-glyph", "mpath", "path", "pattern", "polygon", "polyline",
"radialGradient", "rect", "set", "solidcolor", "stop", "svg", "switch", "symbol", "text",
"textPath", "title", "tref", "tspan", "unknown", "use", "view", "vkern",
]
}
element_set! {
mathml_set_contains,
MATHML_SHAPE,
MATHML_NAMES,
"MathML elements — synced with Svelte's `utils.js` (MathML Core).",
[
"annotation", "annotation-xml", "maction", "math", "merror", "mfrac", "mi",
"mmultiscripts", "mn", "mo", "mover", "mpadded", "mphantom", "mprescripts", "mroot",
"mrow", "ms", "mspace", "msqrt", "mstyle", "msub", "msubsup", "msup", "mtable", "mtd",
"mtext", "mtr", "munder", "munderover", "semantics",
]
}
#[inline]
pub(crate) fn is_block_element(tag_name: &str) -> bool {
shape_admits(&BLOCK_SHAPE, tag_name) && block_set_contains(tag_name)
}
#[inline]
pub(crate) fn is_void_element(tag_name: &str) -> bool {
shape_admits(&VOID_SHAPE, tag_name) && void_set_contains(tag_name)
}
#[inline]
pub(crate) fn is_line_break_element(tag_name: &str) -> bool {
tag_name == "br"
}
#[inline]
pub(crate) fn collapses_child_whitespace(tag_name: &str) -> bool {
matches!(
tag_name,
"select" | "table" | "tbody" | "thead" | "tfoot" | "tr" | "colgroup" | "datalist"
)
}
#[inline]
pub fn is_svg_element(tag_name: &str) -> bool {
shape_admits(&SVG_SHAPE, tag_name) && svg_set_contains(tag_name)
}
#[inline]
pub fn is_mathml_element(tag_name: &str) -> bool {
shape_admits(&MATHML_SHAPE, tag_name) && mathml_set_contains(tag_name)
}
#[inline]
pub(crate) fn is_foreign_element(tag_name: &str) -> bool {
is_svg_element(tag_name) || is_mathml_element(tag_name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shape_filter_agrees_with_a_plain_scan() {
let probes: Vec<String> = BLOCK_NAMES
.iter()
.chain(VOID_NAMES)
.chain(SVG_NAMES)
.chain(MATHML_NAMES)
.flat_map(|name| {
[
(*name).to_string(),
name.to_uppercase(), format!("{}{}", name[..1].to_uppercase(), &name[1..]),
format!("{name}x"), name[..name.len() - 1].to_string(), format!("x{name}"), ]
})
.chain(
[
"",
"div",
"Div",
"DIV",
"span",
"Button",
"MyComponent",
"svg",
"Svg",
"!doctype",
"!DOCTYPE",
"!",
"!doctypex",
"!DOCTYPEX",
"h1",
"h7",
"p",
"é",
"ünknown",
"annotation-xml",
"foreignObject",
"a",
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
]
.iter()
.map(|s| (*s).to_string()),
)
.collect();
for tag in &probes {
assert_eq!(
is_block_element(tag),
BLOCK_NAMES.contains(&tag.as_str()),
"is_block_element disagrees with a plain scan on {tag:?}"
);
assert_eq!(
is_void_element(tag),
VOID_NAMES.contains(&tag.as_str()),
"is_void_element disagrees with a plain scan on {tag:?}"
);
assert_eq!(
is_svg_element(tag),
SVG_NAMES.contains(&tag.as_str()),
"is_svg_element disagrees with a plain scan on {tag:?}"
);
assert_eq!(
is_mathml_element(tag),
MATHML_NAMES.contains(&tag.as_str()),
"is_mathml_element disagrees with a plain scan on {tag:?}"
);
}
}
#[test]
fn shape_admits_every_member_of_its_own_set() {
for (shape, names) in [
(&BLOCK_SHAPE, BLOCK_NAMES),
(&VOID_SHAPE, VOID_NAMES),
(&SVG_SHAPE, SVG_NAMES),
(&MATHML_SHAPE, MATHML_NAMES),
] {
for name in names {
assert!(
shape_admits(shape, name),
"shape rejects its own member {name:?}"
);
}
}
}
#[test]
fn test_is_block_element() {
assert!(is_block_element("div"));
assert!(is_block_element("p"));
assert!(is_block_element("menu"));
assert!(!is_block_element("span"));
assert!(!is_block_element("td"));
assert!(!is_block_element("svg"));
assert!(!is_block_element("DIV"));
}
#[test]
fn test_is_void_element() {
assert!(is_void_element("br"));
assert!(is_void_element("img"));
assert!(is_void_element("input"));
assert!(is_void_element("command"));
assert!(is_void_element("keygen"));
assert!(!is_void_element("BR"));
assert!(!is_void_element("div"));
assert!(!is_void_element("!doctype"));
assert!(!is_void_element("!DOCTYPE"));
assert!(!is_void_element("doctype"));
}
#[test]
fn test_is_line_break_element() {
assert!(is_line_break_element("br"));
assert!(!is_line_break_element("img"));
assert!(!is_line_break_element("input"));
assert!(!is_line_break_element("hr"));
assert!(!is_line_break_element("wbr"));
assert!(!is_line_break_element("BR"));
assert!(!is_line_break_element("div"));
}
#[test]
fn test_collapses_child_whitespace() {
for tag in [
"select", "table", "tbody", "thead", "tfoot", "tr", "colgroup", "datalist",
] {
assert!(collapses_child_whitespace(tag), "member: {tag:?}");
}
for tag in [
"optgroup", "ul", "ol", "menu", "dl", "fieldset", "td", "th", "option", "div", "span",
"svg",
] {
assert!(!collapses_child_whitespace(tag), "non-member: {tag:?}");
}
assert!(!collapses_child_whitespace("TABLE"));
}
#[test]
fn test_foreign_element_classification() {
assert!(is_svg_element("circle"));
assert!(is_svg_element("foreignObject"));
assert!(is_svg_element("color-profile"));
assert!(is_mathml_element("math"));
assert!(is_mathml_element("annotation-xml"));
assert!(is_mathml_element("mfrac"));
assert!(!is_svg_element("math"));
assert!(!is_mathml_element("svg"));
assert!(!is_svg_element("div"));
assert!(is_foreign_element("circle"));
assert!(is_foreign_element("math"));
assert!(!is_foreign_element("div"));
assert!(is_svg_element("title"));
for shared in ["a", "script", "style"] {
assert!(!is_svg_element(shared), "{shared}");
assert!(!is_foreign_element(shared), "{shared}");
}
}
}