use bumpalo::collections::Vec as BumpVec;
use crate::ast::*;
use crate::lexer::TokenKind;
use crate::whitespace::{char_at, is_svelte_ws, name_run_end};
use tsv_lang::{Error, Span};
use super::expression_tag::SequenceLocation;
use super::parser_impl::SvelteParser;
use super::{find_exact_tag_close, rcdata_close_at};
const fn is_tag_name_terminator(c: char) -> bool {
is_svelte_ws(c) || matches!(c, '/' | '>')
}
pub(super) fn tag_name_end(source: &str, start: usize) -> usize {
name_run_end(source, start, is_tag_name_terminator)
}
fn is_valid_tag_name(name: &str) -> bool {
is_valid_element_name(name) || is_component_name(name)
}
fn is_reserved_namespace_miss(name: &str) -> bool {
name.starts_with("svelte:") && !SpecialElementTag::is_meta_tag_name(name)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ElementParent {
Root,
Nested,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RootOnlyMetaTag {
Head,
Options,
Window,
Document,
Body,
}
impl RootOnlyMetaTag {
pub(crate) fn from_tag_name(name: &str) -> Option<Self> {
Some(match name.strip_prefix("svelte:")? {
"head" => Self::Head,
"options" => Self::Options,
"window" => Self::Window,
"document" => Self::Document,
"body" => Self::Body,
_ => return None,
})
}
const fn bit(self) -> u8 {
1 << self as u8
}
}
fn is_root_slot_meta_tag(name: &str) -> bool {
name.starts_with("svelte:")
&& SpecialElementTag::is_meta_tag_name(name)
&& SpecialElementTag::from_tag_name(name, false, false).is_none()
}
fn is_valid_element_name(name: &str) -> bool {
is_regex_valid_tag_name(name) || is_doctype_name(name) || is_namespaced_name(name)
}
fn is_doctype_name(name: &str) -> bool {
name.strip_prefix('!')
.is_some_and(|rest| !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_alphabetic()))
}
fn is_namespaced_name(name: &str) -> bool {
let bytes = name.as_bytes();
let Some(colon) = bytes.iter().position(|&b| b == b':') else {
return false;
};
let (prefix, local) = (&bytes[..colon], &bytes[colon + 1..]);
if !prefix.first().is_some_and(u8::is_ascii_alphabetic)
|| !prefix[1..].iter().all(u8::is_ascii_alphanumeric)
{
return false;
}
local.len() >= 2
&& local[0].is_ascii_alphabetic()
&& local[local.len() - 1].is_ascii_alphanumeric()
&& local[1..local.len() - 1]
.iter()
.all(|&b| b.is_ascii_alphanumeric() || b == b'-')
}
fn is_regex_valid_tag_name(name: &str) -> bool {
let bytes = name.as_bytes();
if !bytes.first().is_some_and(u8::is_ascii_alphabetic) {
return false;
}
let mut i = 1;
while i < bytes.len() && bytes[i].is_ascii_alphanumeric() {
i += 1;
}
match bytes.get(i) {
None => true,
Some(b'-') => is_tag_name_tail_run(&name[i + 1..]),
Some(_) => false,
}
}
#[cold]
#[inline(never)]
fn is_tag_name_tail_run(tail: &str) -> bool {
tail.chars().all(crate::html::is_svelte_tag_name_tail_char)
}
pub(crate) enum ParsedElement<'arena> {
Element(Element<'arena>),
SpecialElement(SpecialElement<'arena>),
}
pub(super) struct OpeningTagEnd {
pub(super) self_closing: bool,
pub(super) gt: u32,
pub(super) after_gt: usize,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ImplicitClose {
Never,
AtAncestorClose,
AlsoAtNextTag,
}
impl ImplicitClose {
fn for_html_element(tag_name: &str) -> Self {
if crate::html::svelte_autoclosing_row(tag_name).is_some() {
Self::AlsoAtNextTag
} else {
Self::AtAncestorClose
}
}
}
enum SpecialThisBinding<'arena> {
Absent,
Element(SpecialThis<'arena>),
Component(ExpressionTag<'arena>),
}
type SpecialElementAttrs<'arena> = (
BumpVec<'arena, AttributeNode<'arena>>,
SpecialThisBinding<'arena>,
);
impl<'a, 'arena> SvelteParser<'a, 'arena> {
fn attrs_have_shadowrootmode(&self, attributes: &[AttributeNode<'arena>]) -> bool {
attributes.iter().any(|attr| {
matches!(attr, AttributeNode::Attribute(a) if a.name(self.source) == "shadowrootmode")
})
}
pub(crate) fn admit_root_only_meta_tag(
&mut self,
tag: RootOnlyMetaTag,
tag_name: &str,
start: usize,
parent: ElementParent,
) -> Result<(), Error> {
if self.root_only_meta_tags_seen & tag.bit() != 0 {
return Err(self.error_msg_at(
&format!("A component can only have one `<{tag_name}>` element"),
start,
));
}
if parent != ElementParent::Root {
return Err(self.error_msg_at(
&format!("`<{tag_name}>` tags cannot be inside elements or blocks"),
start,
));
}
self.root_only_meta_tags_seen |= tag.bit();
Ok(())
}
pub(crate) fn parse_element_or_special(
&mut self,
parent: ElementParent,
) -> Result<ParsedElement<'arena>, Error> {
self.nested(2, |p| p.parse_element_or_special_body(parent))
}
fn parse_element_or_special_body(
&mut self,
parent: ElementParent,
) -> Result<ParsedElement<'arena>, Error> {
let start = self.current_start();
self.expect(TokenKind::LeftAngle)?;
if !self.check(TokenKind::Identifier) {
return Err(self.error_expected_found("tag name"));
}
let name_start = self.current_start();
let name_end = tag_name_end(self.source, self.current_end());
let tag_name = &self.source[name_start..name_end];
let name_span = Span {
start: name_start as u32,
end: name_end as u32,
};
if name_start != start + 1 {
return Err(self.error_msg_at("Expected a valid element or component name", start + 1));
}
if is_reserved_namespace_miss(tag_name) {
return Err(self.error_msg_at("Invalid `<svelte:...>` tag name", start + 1));
}
if !is_valid_tag_name(tag_name) {
return Err(self.error_msg_at("Expected a valid element or component name", start + 1));
}
if let Some(tag) = RootOnlyMetaTag::from_tag_name(tag_name) {
let parent = if is_root_slot_meta_tag(tag_name) {
ElementParent::Nested
} else {
parent
};
self.admit_root_only_meta_tag(tag, tag_name, start, parent)?;
}
self.advance_past_name(name_end)?;
if let Some(special_tag) = SpecialElementTag::from_tag_name(
tag_name,
self.in_svelte_head,
self.in_shadowroot_template,
) {
return self.parse_special_element_body(start, name_span, special_tag);
}
let facts = TagFacts::compute(tag_name);
self.parse_regular_element_body(start, tag_name, facts, name_span)
}
pub(super) fn finish_opening_tag(&mut self) -> Result<OpeningTagEnd, Error> {
let opening = self.read_opening_tag_end()?;
self.advance()?; Ok(opening)
}
fn read_opening_tag_end(&mut self) -> Result<OpeningTagEnd, Error> {
let self_closing = self.check(TokenKind::Slash);
if self_closing {
self.advance()?; }
if !self.check(TokenKind::RightAngle) {
return Err(self.error_expected_kind(TokenKind::RightAngle));
}
Ok(OpeningTagEnd {
self_closing,
gt: self.current_start() as u32,
after_gt: self.current_end(),
})
}
fn parse_regular_element_body(
&mut self,
start: usize,
tag_name: &'a str,
facts: TagFacts,
name_span: Span,
) -> Result<ParsedElement<'arena>, Error> {
let attributes = self.parse_attributes()?;
let opening = self.read_opening_tag_end()?;
let (nodes, end): (&'arena [FragmentNode<'arena>], u32) = if facts.is_void()
|| opening.self_closing
{
self.advance()?; (&[], opening.after_gt as u32)
} else if facts.is_raw_text() {
self.holds_nested_raw_text = true;
self.nested_raw_text_nesting = self.nested_raw_text_nesting.max(self.island_nesting());
let child_nodes = self.parse_raw_text_content(tag_name, opening.after_gt, start)?;
let end = self.parse_closing_tag(tag_name)?;
(child_nodes.into_bump_slice(), end)
} else if tag_name == "textarea" {
let (child_nodes, end) = self.parse_rcdata_content(opening.after_gt, start)?;
(child_nodes.into_bump_slice(), end)
} else {
self.advance()?; let is_html = facts.element_kind() == ElementKind::Html;
let in_shadow = self.in_shadowroot_template
|| (is_html && self.attrs_have_shadowrootmode(&attributes));
let implicit_close = if is_html {
ImplicitClose::for_html_element(tag_name)
} else {
ImplicitClose::Never
};
let (child_nodes, end) = self.parse_children_in_context(
false,
in_shadow,
tag_name,
opening.after_gt,
start,
implicit_close,
)?;
(child_nodes.into_bump_slice(), end)
};
Ok(ParsedElement::Element(Element {
kind: facts.element_kind(),
facts,
attributes: attributes.into_bump_slice(),
fragment: Fragment { nodes },
span: Span {
start: start as u32,
end,
},
name_span,
open_tag_end: opening.gt,
}))
}
fn parse_special_element_body(
&mut self,
start: usize,
name_span: Span,
tag: SpecialElementTag,
) -> Result<ParsedElement<'arena>, Error> {
let (attributes, this) = self.parse_special_element_attributes(tag)?;
let kind = self.build_special_element_kind(tag, this, name_span)?;
let opening = self.finish_opening_tag()?;
let (nodes, end): (&'arena [FragmentNode<'arena>], u32) = if opening.self_closing {
(&[], opening.after_gt as u32)
} else {
let in_head = tag == SpecialElementTag::SvelteHead || self.in_svelte_head;
let (child_nodes, end) = self.parse_children_in_context(
in_head,
self.in_shadowroot_template,
tag.tag_name(),
opening.after_gt,
start,
ImplicitClose::Never,
)?;
(child_nodes.into_bump_slice(), end)
};
Ok(ParsedElement::SpecialElement(SpecialElement {
kind,
attributes: attributes.into_bump_slice(),
fragment: Fragment { nodes },
span: Span {
start: start as u32,
end,
},
name_span,
open_tag_end: opening.gt,
}))
}
fn build_special_element_kind(
&self,
tag: SpecialElementTag,
this: SpecialThisBinding<'arena>,
name_span: Span,
) -> Result<SpecialElementKind<'arena>, Error> {
Ok(match tag {
SpecialElementTag::SvelteElement => {
let SpecialThisBinding::Element(tag) = this else {
return Err(self.error_msg_at(
"`<svelte:element>` must have a 'this' attribute with a value",
name_span.start as usize,
));
};
SpecialElementKind::SvelteElement { tag }
}
SpecialElementTag::SvelteComponent => {
let SpecialThisBinding::Component(expression) = this else {
return Err(self.error_msg_at(
"`<svelte:component>` must have a 'this' attribute",
name_span.start as usize,
));
};
SpecialElementKind::SvelteComponent { expression }
}
SpecialElementTag::SvelteHead => SpecialElementKind::SvelteHead,
SpecialElementTag::SvelteWindow => SpecialElementKind::SvelteWindow,
SpecialElementTag::SvelteBody => SpecialElementKind::SvelteBody,
SpecialElementTag::SvelteDocument => SpecialElementKind::SvelteDocument,
SpecialElementTag::SvelteSelf => SpecialElementKind::SvelteSelf,
SpecialElementTag::SlotElement => SpecialElementKind::SlotElement,
SpecialElementTag::SvelteFragment => SpecialElementKind::SvelteFragment,
SpecialElementTag::SvelteBoundary => SpecialElementKind::SvelteBoundary,
SpecialElementTag::TitleElement => SpecialElementKind::TitleElement,
})
}
fn parse_special_element_attributes(
&mut self,
tag: SpecialElementTag,
) -> Result<SpecialElementAttrs<'arena>, Error> {
let mut attributes = self.parse_attributes()?;
if !tag.takes_this() {
return Ok((attributes, SpecialThisBinding::Absent));
}
let source = self.source;
let found = attributes
.iter()
.enumerate()
.find_map(|(i, node)| match node {
AttributeNode::Attribute(a) if a.name(source) == "this" => Some((i, a)),
_ => None,
});
let Some((index, a)) = found else {
return Ok((attributes, SpecialThisBinding::Absent));
};
let this = if tag == SpecialElementTag::SvelteElement {
match a.value.and_then(<[_]>::first) {
Some(AttributeValue::ExpressionTag(et)) => {
SpecialThisBinding::Element(SpecialThis::Braced(et.clone()))
}
Some(AttributeValue::Text(t)) => SpecialThisBinding::Element(SpecialThis::Plain {
content: self.alloc_str_in(&t.data(source)),
span: t.span,
}),
None => SpecialThisBinding::Absent,
}
} else {
let Some([AttributeValue::ExpressionTag(et)]) = a.value else {
return Err(self.error_msg_at(
"Invalid component definition — must be an `{expression}`",
a.span.start as usize,
));
};
SpecialThisBinding::Component(et.clone())
};
attributes.remove(index);
Ok((attributes, this))
}
fn parse_children_in_context(
&mut self,
in_svelte_head: bool,
in_shadowroot_template: bool,
tag_name: &str,
opening_tag_end: usize,
start: usize,
implicit_close: ImplicitClose,
) -> Result<(BumpVec<'arena, FragmentNode<'arena>>, u32), Error> {
let saved_head = self.in_svelte_head;
let saved_shadow = self.in_shadowroot_template;
self.in_svelte_head = in_svelte_head;
self.in_shadowroot_template = in_shadowroot_template;
let result = self.parse_children(tag_name, opening_tag_end, start, implicit_close);
self.in_svelte_head = saved_head;
self.in_shadowroot_template = saved_shadow;
result
}
fn parse_children(
&mut self,
tag_name: &str,
opening_tag_end: usize,
start: usize,
implicit_close: ImplicitClose,
) -> Result<(BumpVec<'arena, FragmentNode<'arena>>, u32), Error> {
let mut child_nodes = self.bvec();
let mut last_end = opening_tag_end;
loop {
self.capture_text_if_gap(last_end, &mut child_nodes)?;
if self.check(TokenKind::Comment) {
let comment = self.parse_comment()?;
last_end = comment.span.end_usize();
child_nodes.push(FragmentNode::Comment(comment));
} else if self.check(TokenKind::LeftBrace) {
let tag = self.parse_brace_tag()?;
last_end = tag.span().end_usize();
child_nodes.push(tag);
} else if self.check(TokenKind::LeftAngle) {
if self.is_next_token(TokenKind::Slash)? {
let end = if implicit_close != ImplicitClose::Never
&& !self.is_closing_tag_for(tag_name)
{
self.current_start() as u32
} else {
self.parse_closing_tag(tag_name)?
};
return Ok((child_nodes, end));
}
if implicit_close == ImplicitClose::AlsoAtNextTag
&& self.next_open_tag_closes(tag_name)?
{
return Ok((child_nodes, self.current_start() as u32));
}
let child = self.parse_element_or_special(ElementParent::Nested)?;
match child {
ParsedElement::Element(elem) => {
last_end = elem.span.end_usize();
child_nodes.push(FragmentNode::Element(elem));
}
ParsedElement::SpecialElement(elem) => {
last_end = elem.span.end_usize();
child_nodes.push(FragmentNode::SpecialElement(elem));
}
}
} else if self.check(TokenKind::BlockOpen) {
let block = self.parse_block()?;
last_end = block.span().end_usize();
child_nodes.push(block);
} else if self.check(TokenKind::TagOpen) {
let tag = self.parse_template_tag()?;
last_end = tag.span().end_usize();
child_nodes.push(tag);
} else if self.check(TokenKind::Eof) {
return Err(self.error_unclosed_element(tag_name, start));
} else {
return Err(self.error_expected_found(
"element, expression tag, comment, block, or closing tag",
));
}
}
}
#[inline(never)]
fn next_open_tag_closes(&mut self, tag_name: &str) -> Result<bool, Error> {
let Some(next_name) = self.peek_open_tag_name()? else {
return Ok(false);
};
Ok(crate::html::svelte_autoclosing_row(tag_name)
.is_some_and(|row| row.is_closed_by(next_name)))
}
#[cold]
#[inline(never)]
fn error_unclosed_element(&self, tag_name: &str, start: usize) -> Error {
self.error_unclosed_at(&format!("element: <{tag_name}>"), start)
}
fn is_closing_tag_for(&self, tag_name: &str) -> bool {
let name_start = self.current_start() + 2; if !self
.source
.get(name_start..)
.is_some_and(|rest| rest.starts_with(tag_name))
{
return false;
}
match char_at(self.source, name_start + tag_name.len()) {
None => true,
Some((c, _)) => is_tag_name_terminator(c),
}
}
fn peek_open_tag_name(&mut self) -> Result<Option<&'a str>, Error> {
self.fill_peek()?;
Ok(self.peek.as_ref().and_then(|p| {
(p.kind == TokenKind::Identifier).then(|| {
let name_start = p.start as usize;
let name_end = tag_name_end(self.source, p.end as usize);
&self.source[name_start..name_end]
})
}))
}
pub(super) fn parse_closing_tag(&mut self, expected_name: &str) -> Result<u32, Error> {
self.expect(TokenKind::LeftAngle)?;
self.expect(TokenKind::Slash)?;
if !self.check(TokenKind::Identifier) {
return Err(self.error_expected_found("tag name"));
}
let closing_tag_name = self.current_value();
if closing_tag_name != expected_name {
return Err(self.error_msg(&format!(
"Mismatched tags: expected closing tag for '{expected_name}' but found '{closing_tag_name}'"
)));
}
self.advance()?;
let end = self.current_end();
self.expect(TokenKind::RightAngle)?;
Ok(end as u32)
}
fn parse_raw_text_content(
&mut self,
tag_name: &str,
content_start: usize,
element_start: usize,
) -> Result<BumpVec<'arena, FragmentNode<'arena>>, Error> {
let content_end = find_exact_tag_close(self.source, content_start, tag_name.as_bytes())
.ok_or_else(|| {
self.error_msg_at(&format!("Unterminated <{tag_name}> element"), element_start)
})?;
self.advance_to_position(content_end)?;
let span = Span {
start: content_start as u32,
end: content_end as u32,
};
let mut nodes = self.bvec();
nodes.push(FragmentNode::Text(Text::new(
span,
TextDecoding::Raw,
span,
self.source,
)));
Ok(nodes)
}
fn parse_rcdata_content(
&mut self,
content_start: usize,
element_start: usize,
) -> Result<(BumpVec<'arena, FragmentNode<'arena>>, u32), Error> {
let bytes = self.source.as_bytes();
let mut nodes = self.bvec();
let mut chunk_start = content_start;
let mut i = content_start;
let close_gt = loop {
if let Some((close_lt, close_gt)) = rcdata_close_at(self.source, i, b"textarea") {
self.push_rcdata_text(&mut nodes, chunk_start, close_lt);
break close_gt;
}
match bytes.get(i) {
None => {
return Err(self.error_unclosed_at("<textarea> element", element_start));
}
Some(b'{') => {
self.push_rcdata_text(&mut nodes, chunk_start, i);
let tag =
self.parse_sequence_expression_tag_at(i, SequenceLocation::InsideTextarea)?;
i = tag.span.end as usize;
chunk_start = i;
nodes.push(FragmentNode::ExpressionTag(tag));
}
Some(_) => i += 1,
}
};
let end = close_gt + 1;
self.lexer.inside_tag = false;
self.advance_to_position(end)?;
Ok((nodes, end as u32))
}
fn push_rcdata_text(
&self,
nodes: &mut BumpVec<'arena, FragmentNode<'arena>>,
start: usize,
end: usize,
) {
if end > start {
let span = Span {
start: start as u32,
end: end as u32,
};
nodes.push(FragmentNode::Text(Text::new(
span,
TextDecoding::AttributeValue,
span,
self.source,
)));
}
}
}
#[cfg(test)]
mod tests {
use super::{
RootOnlyMetaTag, is_doctype_name, is_namespaced_name, is_regex_valid_tag_name,
is_reserved_namespace_miss, is_root_slot_meta_tag, is_valid_element_name,
is_valid_tag_name, tag_name_end,
};
use crate::ast::is_component_name;
#[test]
fn tag_name_run_ends_only_at_whitespace_slash_or_gt() {
for (name, tail) in [
("div", ">"),
("div", "/>"),
("div", " x"),
("div", ""),
("div", "\u{a0}>"), ("div", "\u{feff}>"), ("div\u{85}x", ">"),
("div%x", ">"),
("div{x}", ">"),
("div=x", ">"),
("div\"x", ">"),
("div'x", ">"),
("my-elem", ">"),
("my-caf\u{e9}", ">"),
("svelte:head", ">"),
("Foo.Bar", ">"),
("!DOCTYPE", " html>"),
] {
let source = format!("{name}{tail}");
assert_eq!(tag_name_end(&source, 0), name.len(), "{source:?}");
}
}
#[test]
fn component_name_matches_its_reference() {
const PIECES: [&str; 8] = [":", ".", "A", "a", "\u{394}", "\u{e9}", "-", "\u{1f600}"];
crate::test_support::for_every_arrangement(&PIECES, 5, |name| {
let reference = !name.contains(':')
&& (name.contains('.') || name.chars().next().is_some_and(char::is_uppercase));
assert_eq!(is_component_name(name), reference, "{name:?}");
});
}
#[test]
fn component_classification() {
assert!(is_component_name("Comp"));
assert!(is_component_name("ns.Comp"));
assert!(is_component_name("deep.nested.Comp"));
assert!(is_component_name("Object.component"));
assert!(is_component_name("object.property"));
assert!(is_component_name("ns.lower"));
assert!(!is_component_name("div"));
assert!(!is_component_name(""));
assert!(is_component_name("Über"));
assert!(!is_component_name("élan"));
assert!(!is_component_name("foo:bar"));
assert!(!is_component_name("Foo:bar"));
assert!(!is_component_name("svelte:head"));
}
#[test]
fn accepts_valid_element_and_component_names() {
for name in [
"div",
"h1",
"a",
"my-elem",
"foreignObject",
"annotation-xml",
] {
assert!(is_valid_tag_name(name), "should accept <{name}>");
}
for name in ["my-café", "x-\u{00B7}", "a-b.c", "a-b_c"] {
assert!(is_valid_tag_name(name), "should accept <{name}>");
}
for name in ["Foo", "MyComp", "Δfoo", "ns.Comp", "deep.nested.Comp"] {
assert!(is_valid_tag_name(name), "should accept <{name}>");
}
for name in [
"!DOCTYPE",
"!doctype",
"svelte:head",
"svelte:component",
"foo:bar",
] {
assert!(is_valid_tag_name(name), "should accept <{name}>");
}
}
#[test]
fn rejects_invalid_tag_names() {
for name in ["élan", "δfoo", "你好", "Džfoo"] {
assert!(!is_valid_tag_name(name), "should reject <{name}>");
}
assert!(!is_valid_tag_name("divä"));
assert!(!is_valid_tag_name("a|b"));
for name in ["foo:b", "foo:bar-", "foo:bär"] {
assert!(!is_namespaced_name(name), "namespaced should reject {name}");
assert!(
!is_valid_element_name(name),
"element name should reject {name}"
);
}
assert!(!is_valid_element_name("!-"));
assert!(!is_valid_element_name("!doc7"));
}
fn namespaced_name_reference(name: &str) -> bool {
let Some((prefix, local)) = name.split_once(':') else {
return false;
};
let (prefix, local) = (prefix.as_bytes(), local.as_bytes());
prefix.first().is_some_and(u8::is_ascii_alphabetic)
&& prefix[1..].iter().all(u8::is_ascii_alphanumeric)
&& local.len() >= 2
&& local[0].is_ascii_alphabetic()
&& local[local.len() - 1].is_ascii_alphanumeric()
&& local[1..local.len() - 1]
.iter()
.all(|&b| b.is_ascii_alphanumeric() || b == b'-')
}
fn regex_valid_tag_name_reference(name: &str) -> bool {
let mut chars = name.chars();
if !chars.next().is_some_and(|c| c.is_ascii_alphabetic()) {
return false;
}
let mut after_hyphen = false;
for c in chars {
if after_hyphen {
if !crate::html::is_svelte_tag_name_tail_char(c) {
return false;
}
} else if c.is_ascii_alphanumeric() {
} else if c == '-' {
after_hyphen = true;
} else {
return false;
}
}
true
}
#[test]
fn element_local_name_matches_the_reference_for_every_scalar() {
let mut checked = 0usize;
for cp in 0..=0x10_ffff_u32 {
let Some(c) = char::from_u32(cp) else {
continue;
};
for name in [
format!("{c}"),
format!("{c}b"),
format!("a{c}"),
format!("a1{c}b"),
format!("a{c}-b"),
format!("a-{c}"),
format!("a-b{c}c"),
format!("a-{c}{c}"),
format!("a--{c}"),
] {
assert_eq!(
is_regex_valid_tag_name(&name),
regex_valid_tag_name_reference(&name),
"U+{cp:04X} in {name:?}"
);
checked += 1;
}
}
assert!(checked > 9_000_000, "the scalar walk ran ({checked} cases)");
}
#[test]
fn element_name_predicates_match_their_references() {
const PIECES: [&str; 12] = [
"a",
"Z",
"0",
"-",
":",
"!",
".",
"_",
"\u{e9}",
"\u{b7}",
"\u{1f600}",
"|",
];
crate::test_support::for_every_arrangement(&PIECES, 5, |name| {
let namespaced = namespaced_name_reference(name);
assert_eq!(is_namespaced_name(name), namespaced, "{name:?}");
assert_eq!(
is_regex_valid_tag_name(name),
regex_valid_tag_name_reference(name),
"{name:?}"
);
assert_eq!(
is_valid_element_name(name),
is_doctype_name(name) || namespaced || is_regex_valid_tag_name(name),
"{name:?}"
);
});
for (name, valid) in [
("svelte:head", true),
("a:bc", true),
("a:b-c", true),
("a1:b2", true),
("a:b", false),
("a:b-", false),
(":ab", false),
("1a:bc", false),
("a::bc", false),
("a:b:c", false),
("a:bc:", false),
("!DOCTYPE", true),
("!", false),
("my-caf\u{e9}", true),
("x-\u{1f600}", true),
("x\u{e9}", false),
] {
assert_eq!(is_valid_element_name(name), valid, "{name:?}");
}
}
#[test]
fn reserved_namespace_admits_only_the_meta_tags() {
for name in [
"svelte:head",
"svelte:options",
"svelte:window",
"svelte:document",
"svelte:body",
"svelte:element",
"svelte:component",
"svelte:self",
"svelte:fragment",
"svelte:boundary",
] {
assert!(is_valid_tag_name(name), "should accept <{name}>");
assert!(
!is_reserved_namespace_miss(name),
"meta tag <{name}> should not be a miss"
);
}
for name in [
"svelte:foo",
"svelte:headx",
"svelte:optionsx",
"svelte:Head",
"svelte:HEAD",
] {
assert!(
is_reserved_namespace_miss(name),
"<{name}> should be a miss"
);
}
for name in [
"sveltex:foo",
"foo:bar",
"SVELTE:head",
"Svelte:head",
"div",
"svelte",
] {
assert!(
!is_reserved_namespace_miss(name),
"<{name}> is outside the reserved namespace"
);
}
}
#[test]
fn root_slot_meta_tag_is_only_svelte_options() {
assert!(is_root_slot_meta_tag("svelte:options"));
for name in [
"svelte:head",
"svelte:window",
"svelte:body",
"svelte:document",
] {
assert!(
!is_root_slot_meta_tag(name),
"<{name}> has a node type, so it is not a root-slot tag"
);
}
for name in [
"svelte:element",
"svelte:component",
"svelte:self",
"svelte:fragment",
"svelte:boundary",
"svelte:foo",
"div",
"title",
"Comp",
] {
assert!(
!is_root_slot_meta_tag(name),
"<{name}> is not a root-slot tag"
);
}
assert!(!is_root_slot_meta_tag("slot"));
}
#[test]
fn root_only_meta_tags_are_svelte_s_five() {
let names = [
"svelte:head",
"svelte:options",
"svelte:window",
"svelte:document",
"svelte:body",
];
let mut bits = 0u8;
for name in names {
let tag = RootOnlyMetaTag::from_tag_name(name)
.unwrap_or_else(|| panic!("<{name}> is root-only"));
assert_eq!(bits & tag.bit(), 0, "<{name}> shares a bit");
bits |= tag.bit();
}
for name in [
"svelte:element",
"svelte:component",
"svelte:self",
"svelte:fragment",
"svelte:boundary",
"svelte:headx",
"svelte:Head",
"head",
"window",
"title",
"slot",
"div",
] {
assert_eq!(RootOnlyMetaTag::from_tag_name(name), None, "<{name}>");
assert!(!is_root_slot_meta_tag(name), "<{name}>");
}
assert!(RootOnlyMetaTag::from_tag_name("svelte:options").is_some());
}
}