#[cfg(feature = "json")]
pub(crate) mod convert;
use std::borrow::Cow;
use tsv_css::ast::CssStyleSheet;
pub use tsv_lang::{Comment, Span};
use tsv_ts::ast::{Expression, Program, TSTypeParameterDeclaration, VariableDeclaration};
#[derive(Debug, Clone)]
pub struct Root<'arena> {
pub fragment: Fragment<'arena>,
pub instance: Option<&'arena Script<'arena>>,
pub module: Option<&'arena Script<'arena>>,
pub css: Option<&'arena Style<'arena>>,
pub options: Option<SvelteOptions<'arena>>,
pub comments: Vec<Comment>,
pub snippet_wire_parameters: &'arena [SnippetWireParameters<'arena>],
pub(crate) holds_nested_raw_text: bool,
pub(crate) nested_raw_text_nesting: u32,
}
#[derive(Debug, Clone, Copy)]
pub struct SnippetWireParameters<'arena> {
pub snippet_start: u32,
pub parameters: &'arena [Expression<'arena>],
}
#[derive(Debug, Clone)]
pub struct Fragment<'arena> {
pub nodes: &'arena [FragmentNode<'arena>],
}
#[derive(Debug, Clone)]
pub enum FragmentNode<'arena> {
Element(Element<'arena>),
SpecialElement(SpecialElement<'arena>),
ExpressionTag(ExpressionTag<'arena>),
Text(Text),
Comment(HtmlComment),
IfBlock(IfBlock<'arena>),
EachBlock(EachBlock<'arena>),
AwaitBlock(AwaitBlock<'arena>),
KeyBlock(KeyBlock<'arena>),
SnippetBlock(SnippetBlock<'arena>),
HtmlTag(HtmlTag<'arena>),
ConstTag(ConstTag<'arena>),
DeclarationTag(DeclarationTag<'arena>),
DebugTag(DebugTag<'arena>),
RenderTag(RenderTag<'arena>),
}
#[derive(Debug, Clone)]
pub struct HtmlComment {
pub content_span: Span,
pub span: Span,
}
impl HtmlComment {
pub fn content<'s>(&self, source: &'s str) -> &'s str {
self.content_span.extract(source)
}
}
#[derive(Debug, Clone)]
pub struct IfBlock<'arena> {
pub elseif: bool,
pub test: &'arena Expression<'arena>,
pub consequent: Fragment<'arena>,
pub alternate: Option<Fragment<'arena>>,
pub span: Span,
pub opening_tag_span: Span,
}
#[derive(Debug, Clone)]
pub struct EachBlock<'arena> {
pub expression: &'arena Expression<'arena>,
pub context: Option<&'arena Expression<'arena>>, pub index: Option<&'arena str>,
pub key: Option<EachKey<'arena>>,
pub body: Fragment<'arena>,
pub fallback: Option<Fragment<'arena>>,
pub span: Span,
pub opening_tag_span: Span,
}
#[derive(Debug, Clone)]
pub struct EachKey<'arena> {
pub expression: &'arena Expression<'arena>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct AwaitBlock<'arena> {
pub expression: &'arena Expression<'arena>,
pub value: Option<&'arena Expression<'arena>>, pub error: Option<&'arena Expression<'arena>>, pub pending: Option<Fragment<'arena>>,
pub pending_block: bool,
pub then: Option<Fragment<'arena>>,
pub catch: Option<Fragment<'arena>>,
pub span: Span,
pub opening_tag_span: Span,
}
#[derive(Debug, Clone)]
pub struct KeyBlock<'arena> {
pub expression: &'arena Expression<'arena>,
pub fragment: Fragment<'arena>,
pub span: Span,
pub opening_tag_span: Span,
}
#[derive(Debug, Clone)]
pub struct SnippetBlock<'arena> {
pub expression: &'arena Expression<'arena>, pub type_parameters: Option<TSTypeParameterDeclaration<'arena>>,
pub type_params_raw: Option<&'arena str>,
pub parameters: &'arena [Expression<'arena>],
pub params_paren: Option<Span>,
pub body: Fragment<'arena>,
pub span: Span,
pub opening_tag_span: Span,
}
#[derive(Debug, Clone)]
pub struct HtmlTag<'arena> {
pub expression: &'arena Expression<'arena>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct ConstTag<'arena> {
pub id: &'arena Expression<'arena>, pub init: &'arena Expression<'arena>, pub span: Span,
}
#[derive(Debug, Clone)]
pub struct DeclarationTag<'arena> {
pub declaration: VariableDeclaration<'arena>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct DebugTag<'arena> {
pub identifiers: &'arena [Expression<'arena>], pub span: Span,
}
#[derive(Debug, Clone)]
pub struct RenderTag<'arena> {
pub expression: &'arena Expression<'arena>, pub span: Span,
}
#[derive(Debug, Clone)]
pub struct AttachTag<'arena> {
pub expression: &'arena Expression<'arena>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct OnDirective<'arena> {
pub name_span: Span,
pub expression: Option<&'arena Expression<'arena>>, pub modifiers: &'arena [&'arena str], pub span: Span,
pub head_span: Span,
pub expression_tag_span: Option<Span>,
}
#[derive(Debug, Clone)]
pub struct BindDirective<'arena> {
pub name_span: Span,
pub expression: &'arena Expression<'arena>, pub modifiers: &'arena [&'arena str], pub span: Span,
pub head_span: Span,
pub expression_tag_span: Option<Span>,
}
#[derive(Debug, Clone)]
pub struct ClassDirective<'arena> {
pub name_span: Span,
pub expression: &'arena Expression<'arena>, pub modifiers: &'arena [&'arena str], pub span: Span,
pub head_span: Span,
pub expression_tag_span: Option<Span>,
}
#[derive(Debug, Clone)]
pub struct StyleDirective<'arena> {
pub name_span: Span,
pub value: StyleDirectiveValue<'arena>, pub modifiers: &'arena [&'arena str], pub span: Span,
pub head_span: Span,
}
#[derive(Debug, Clone)]
pub enum StyleDirectiveValue<'arena> {
True,
ExpressionTag(ExpressionTag<'arena>),
Parts(&'arena [AttributeValue<'arena>]),
}
#[derive(Debug, Clone)]
pub struct UseDirective<'arena> {
pub name_span: Span,
pub expression: Option<&'arena Expression<'arena>>, pub modifiers: &'arena [&'arena str], pub span: Span,
pub head_span: Span,
pub expression_tag_span: Option<Span>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransitionDirection {
Both,
In,
Out,
}
impl TransitionDirection {
pub const fn prefix(self) -> &'static str {
match self {
Self::Both => "transition",
Self::In => "in",
Self::Out => "out",
}
}
pub const fn prefix_with_colon(self) -> &'static str {
match self {
Self::Both => "transition:",
Self::In => "in:",
Self::Out => "out:",
}
}
pub const fn has_intro(self) -> bool {
matches!(self, Self::Both | Self::In)
}
pub const fn has_outro(self) -> bool {
matches!(self, Self::Both | Self::Out)
}
}
#[derive(Debug, Clone)]
pub struct TransitionDirective<'arena> {
pub name_span: Span,
pub expression: Option<&'arena Expression<'arena>>, pub modifiers: &'arena [&'arena str], pub direction: TransitionDirection, pub span: Span,
pub head_span: Span,
pub expression_tag_span: Option<Span>,
}
#[derive(Debug, Clone)]
pub struct AnimateDirective<'arena> {
pub name_span: Span,
pub expression: Option<&'arena Expression<'arena>>, pub modifiers: &'arena [&'arena str], pub span: Span,
pub head_span: Span,
pub expression_tag_span: Option<Span>,
}
#[derive(Debug, Clone)]
pub struct LetDirective<'arena> {
pub name_span: Span,
pub expression: Option<&'arena Expression<'arena>>, pub modifiers: &'arena [&'arena str], pub span: Span,
pub head_span: Span,
pub expression_tag_span: Option<Span>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpecialElementTag {
SvelteHead,
SvelteWindow,
SvelteBody,
SvelteDocument,
SvelteElement,
SvelteComponent,
SvelteSelf,
SlotElement,
SvelteFragment,
SvelteBoundary,
TitleElement,
}
impl SpecialElementTag {
pub fn from_tag_name(
name: &str,
in_svelte_head: bool,
in_shadowroot_template: bool,
) -> Option<Self> {
match name {
"svelte:head" => Some(Self::SvelteHead),
"svelte:window" => Some(Self::SvelteWindow),
"svelte:body" => Some(Self::SvelteBody),
"svelte:document" => Some(Self::SvelteDocument),
"svelte:element" => Some(Self::SvelteElement),
"svelte:component" => Some(Self::SvelteComponent),
"svelte:self" => Some(Self::SvelteSelf),
"slot" if !in_shadowroot_template => Some(Self::SlotElement),
"svelte:fragment" => Some(Self::SvelteFragment),
"svelte:boundary" => Some(Self::SvelteBoundary),
"title" if in_svelte_head => Some(Self::TitleElement),
_ => None,
}
}
pub fn is_meta_tag_name(name: &str) -> bool {
name == "svelte:options" || Self::from_tag_name(name, false, false).is_some()
}
#[inline]
pub const fn tag_name(self) -> &'static str {
match self {
Self::SvelteHead => "svelte:head",
Self::SvelteWindow => "svelte:window",
Self::SvelteBody => "svelte:body",
Self::SvelteDocument => "svelte:document",
Self::SvelteElement => "svelte:element",
Self::SvelteComponent => "svelte:component",
Self::SvelteSelf => "svelte:self",
Self::SlotElement => "slot",
Self::SvelteFragment => "svelte:fragment",
Self::SvelteBoundary => "svelte:boundary",
Self::TitleElement => "title",
}
}
#[inline]
pub const fn takes_this(self) -> bool {
matches!(self, Self::SvelteElement | Self::SvelteComponent)
}
}
#[derive(Debug, Clone)]
pub enum SpecialThis<'arena> {
Braced(ExpressionTag<'arena>),
Plain {
content: &'arena str,
span: Span,
},
}
impl<'arena> SpecialThis<'arena> {
pub fn span(&self) -> Span {
match self {
Self::Braced(tag) => tag.expression.span(),
Self::Plain { span, .. } => *span,
}
}
pub fn braces(&self) -> Option<Span> {
match self {
Self::Braced(tag) => Some(tag.span),
Self::Plain { .. } => None,
}
}
}
#[derive(Debug, Clone)]
pub enum SpecialElementKind<'arena> {
SvelteHead,
SvelteWindow,
SvelteBody,
SvelteDocument,
SvelteElement { tag: SpecialThis<'arena> },
SvelteComponent { expression: ExpressionTag<'arena> },
SvelteSelf,
SlotElement,
SvelteFragment,
SvelteBoundary,
TitleElement,
}
impl<'arena> SpecialElementKind<'arena> {
#[inline]
pub const fn is_block(&self) -> bool {
self.is_global()
}
#[inline]
pub const fn is_global(&self) -> bool {
matches!(
self,
Self::SvelteHead | Self::SvelteWindow | Self::SvelteBody | Self::SvelteDocument
)
}
#[inline]
pub const fn preserves_content_whitespace(&self) -> bool {
matches!(self, Self::TitleElement)
}
#[inline]
pub const fn tag_name(&self) -> &'static str {
match self {
Self::SvelteHead => "svelte:head",
Self::SvelteWindow => "svelte:window",
Self::SvelteBody => "svelte:body",
Self::SvelteDocument => "svelte:document",
Self::SvelteElement { .. } => "svelte:element",
Self::SvelteComponent { .. } => "svelte:component",
Self::SvelteSelf => "svelte:self",
Self::SlotElement => "slot",
Self::SvelteFragment => "svelte:fragment",
Self::SvelteBoundary => "svelte:boundary",
Self::TitleElement => "title",
}
}
#[inline]
pub const fn node_type(&self) -> &'static str {
match self {
Self::SvelteHead => "SvelteHead",
Self::SvelteWindow => "SvelteWindow",
Self::SvelteBody => "SvelteBody",
Self::SvelteDocument => "SvelteDocument",
Self::SvelteElement { .. } => "SvelteElement",
Self::SvelteComponent { .. } => "SvelteComponent",
Self::SvelteSelf => "SvelteSelf",
Self::SlotElement => "SlotElement",
Self::SvelteFragment => "SvelteFragment",
Self::SvelteBoundary => "SvelteBoundary",
Self::TitleElement => "TitleElement",
}
}
pub fn tag(&self) -> Option<&SpecialThis<'arena>> {
match self {
Self::SvelteElement { tag } => Some(tag),
_ => None,
}
}
pub fn expression(&self) -> Option<&ExpressionTag<'arena>> {
match self {
Self::SvelteComponent { expression } => Some(expression),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct SpecialElement<'arena> {
pub kind: SpecialElementKind<'arena>,
pub attributes: &'arena [AttributeNode<'arena>],
pub fragment: Fragment<'arena>,
pub span: Span,
pub name_span: Span,
pub open_tag_end: u32,
}
#[derive(Debug, Clone)]
pub struct SvelteOptions<'arena> {
pub attributes: &'arena [AttributeNode<'arena>],
pub span: Span,
pub name_end: u32,
pub open_tag_end: u32,
}
impl<'arena> FragmentNode<'arena> {
pub fn span(&self) -> Span {
match self {
FragmentNode::Element(elem) => elem.span,
FragmentNode::SpecialElement(elem) => elem.span,
FragmentNode::ExpressionTag(tag) => tag.span,
FragmentNode::Text(text) => text.span,
FragmentNode::Comment(comment) => comment.span,
FragmentNode::IfBlock(block) => block.span,
FragmentNode::EachBlock(block) => block.span,
FragmentNode::AwaitBlock(block) => block.span,
FragmentNode::KeyBlock(block) => block.span,
FragmentNode::SnippetBlock(block) => block.span,
FragmentNode::HtmlTag(tag) => tag.span,
FragmentNode::ConstTag(tag) => tag.span,
FragmentNode::DeclarationTag(tag) => tag.span,
FragmentNode::DebugTag(tag) => tag.span,
FragmentNode::RenderTag(tag) => tag.span,
}
}
#[inline]
pub fn is_whitespace_only_text(&self) -> bool {
matches!(self, FragmentNode::Text(t) if t.is_collapsible_ws_only)
}
#[inline]
pub fn is_boundary_break(&self) -> bool {
matches!(self, FragmentNode::Text(t) if t.is_collapsible_ws_only && t.has_newline())
}
#[inline]
pub fn is_declaration(&self) -> bool {
matches!(
self,
FragmentNode::ConstTag(_)
| FragmentNode::DeclarationTag(_)
| FragmentNode::SnippetBlock(_)
)
}
#[inline]
pub fn is_hoisted_from_fragment(&self) -> bool {
match self {
FragmentNode::ConstTag(_)
| FragmentNode::DeclarationTag(_)
| FragmentNode::DebugTag(_)
| FragmentNode::SnippetBlock(_) => true,
FragmentNode::SpecialElement(se) => {
matches!(se.kind, SpecialElementKind::TitleElement)
}
_ => false,
}
}
#[inline]
pub fn is_compiler_hoisted(&self) -> bool {
self.is_hoisted_from_fragment()
|| matches!(self, FragmentNode::SpecialElement(se) if se.kind.is_global())
}
pub(crate) fn content_bounds(nodes: &[FragmentNode<'_>]) -> Option<(usize, usize)> {
let first = nodes.iter().position(|n| !n.is_hoisted_from_fragment())?;
let last = nodes.iter().rposition(|n| !n.is_hoisted_from_fragment())?;
Some((first, last))
}
#[inline]
pub(crate) fn at_content_start(i: usize, bounds: (usize, usize)) -> bool {
i <= bounds.0
}
#[inline]
pub(crate) fn at_content_end(i: usize, bounds: (usize, usize)) -> bool {
i >= bounds.1
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ElementKind {
Html,
Component,
}
pub(crate) fn is_component_name(name: &str) -> bool {
name_shape(name) == NameShape::Component
}
pub fn is_svelte_void(name: &str) -> bool {
crate::html::is_void_element(name)
|| (name.starts_with('!') && name.eq_ignore_ascii_case("!doctype"))
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum NameShape {
Namespaced,
Component,
Plain,
}
fn name_shape(name: &str) -> NameShape {
let mut dotted = false;
for &b in name.as_bytes() {
match b {
b':' => return NameShape::Namespaced,
b'.' => dotted = true,
_ => {}
}
}
if dotted || name.chars().next().is_some_and(char::is_uppercase) {
NameShape::Component
} else {
NameShape::Plain
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) struct TagFacts(u16);
impl TagFacts {
const BLOCK: u16 = 1 << 0;
const VOID: u16 = 1 << 1;
const FOREIGN: u16 = 1 << 2;
const COMPONENT_NAME: u16 = 1 << 3;
const STYLE: u16 = 1 << 4;
const SCRIPT: u16 = 1 << 5;
const TEMPLATE: u16 = 1 << 6;
const WS_SENSITIVE: u16 = 1 << 7;
const DECLARATION: u16 = 1 << 8;
const NAMESPACED: u16 = 1 << 9;
const WS_COLLAPSING: u16 = 1 << 10;
const LINE_BREAK: u16 = 1 << 11;
#[inline(never)]
pub(crate) fn compute(tag_name: &str) -> Self {
let mut bits: u16 = 0;
if crate::html::is_block_element(tag_name) {
bits |= Self::BLOCK;
}
if is_svelte_void(tag_name) {
bits |= Self::VOID;
}
if crate::html::is_foreign_element(tag_name) {
bits |= Self::FOREIGN;
}
match name_shape(tag_name) {
NameShape::Namespaced => bits |= Self::NAMESPACED,
NameShape::Component => bits |= Self::COMPONENT_NAME,
NameShape::Plain => {}
}
if tag_name == "style" {
bits |= Self::STYLE;
}
if tag_name == "script" {
bits |= Self::SCRIPT;
}
if tag_name == "template" {
bits |= Self::TEMPLATE;
}
if crate::html::preserves_whitespace(tag_name) {
bits |= Self::WS_SENSITIVE;
}
if crate::html::collapses_child_whitespace(tag_name) {
bits |= Self::WS_COLLAPSING;
}
if tag_name.starts_with('!') {
bits |= Self::DECLARATION;
}
if crate::html::is_line_break_element(tag_name) {
bits |= Self::LINE_BREAK;
}
Self(bits)
}
pub(crate) fn is_block(self) -> bool {
self.0 & Self::BLOCK != 0
}
pub(crate) fn element_kind(self) -> ElementKind {
if self.is_component_name() {
ElementKind::Component
} else {
ElementKind::Html
}
}
pub(crate) fn is_void(self) -> bool {
self.0 & Self::VOID != 0
}
pub(crate) fn is_foreign(self) -> bool {
self.0 & Self::FOREIGN != 0
}
pub(crate) fn is_component_name(self) -> bool {
self.0 & Self::COMPONENT_NAME != 0
}
pub(crate) fn is_namespaced(self) -> bool {
self.0 & Self::NAMESPACED != 0
}
pub(crate) fn is_style(self) -> bool {
self.0 & Self::STYLE != 0
}
pub(crate) fn is_raw_text(self) -> bool {
self.0 & (Self::SCRIPT | Self::STYLE) != 0
}
pub(crate) fn is_template(self) -> bool {
self.0 & Self::TEMPLATE != 0
}
pub(crate) fn is_ws_sensitive(self) -> bool {
self.0 & Self::WS_SENSITIVE != 0
}
pub(crate) fn collapses_child_whitespace(self) -> bool {
self.0 & Self::WS_COLLAPSING != 0
}
pub(crate) fn is_declaration(self) -> bool {
self.0 & Self::DECLARATION != 0
}
pub(crate) fn is_line_break(self) -> bool {
self.0 & Self::LINE_BREAK != 0
}
}
#[derive(Debug, Clone)]
pub struct Element<'arena> {
pub kind: ElementKind,
pub(crate) facts: TagFacts,
pub attributes: &'arena [AttributeNode<'arena>],
pub fragment: Fragment<'arena>,
pub span: Span,
pub name_span: Span,
pub open_tag_end: u32,
}
impl Element<'_> {
#[inline]
pub fn name<'s>(&self, source: &'s str) -> &'s str {
&source[self.name_span.range()]
}
pub fn is_frozen_template(&self, source: &str) -> bool {
self.facts.is_template() && EmbeddedLang::Template.is_frozen(self.attributes, source)
}
}
fn lang_attribute<'s>(attributes: &[AttributeNode<'_>], source: &'s str) -> Option<Cow<'s, str>> {
let mut type_value = None;
for attr_node in attributes {
if let AttributeNode::Attribute(attr) = attr_node {
let name = attr.name(source);
let is_lang = name == "lang";
if (is_lang || name == "type")
&& let Some(value_parts) = attr.value
{
for part in value_parts {
if let AttributeValue::Text(text) = part {
let value = narrow_lang_value(text.data(source));
if !value.is_empty() {
if is_lang {
return Some(value);
}
if type_value.is_none() {
type_value = Some(value);
}
}
break;
}
}
}
}
}
type_value
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EmbeddedLang {
Script,
Style,
Template,
}
impl EmbeddedLang {
fn formattable_langs(self) -> &'static [&'static str] {
match self {
Self::Script => &[
"ts",
"typescript",
"js",
"javascript",
"ecmascript",
"application/javascript",
"application/ecmascript",
"module",
],
Self::Style => &["css"],
Self::Template => &["html"],
}
}
pub fn is_frozen(self, attributes: &[AttributeNode<'_>], source: &str) -> bool {
lang_attribute(attributes, source)
.is_some_and(|lang| !self.formattable_langs().contains(&lang.as_ref()))
}
}
fn narrow_lang_value(value: Cow<'_, str>) -> Cow<'_, str> {
fn is_lang_pad(c: char) -> bool {
crate::whitespace::is_svelte_ws(c) || c.is_whitespace()
}
fn narrow(raw: &str) -> &str {
let lang = raw.trim_matches(is_lang_pad);
lang.strip_prefix("text/").unwrap_or(lang)
}
match value {
Cow::Borrowed(raw) => Cow::Borrowed(narrow(raw)),
Cow::Owned(raw) => Cow::Owned(narrow(&raw).to_owned()),
}
}
#[cfg(all(test, target_pointer_width = "64"))]
const _: () = assert!(size_of::<Element<'static>>() == 56);
#[derive(Debug, Clone)]
pub struct Attribute<'arena> {
pub value: Option<&'arena [AttributeValue<'arena>]>,
pub span: Span,
pub name_span: Span,
}
impl Attribute<'_> {
#[inline]
pub fn name<'s>(&self, source: &'s str) -> &'s str {
&source[self.name_span.range()]
}
}
#[derive(Debug, Clone)]
pub struct SpreadAttribute<'arena> {
pub expression: &'arena Expression<'arena>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub enum AttributeNode<'arena> {
Attribute(Attribute<'arena>),
SpreadAttribute(SpreadAttribute<'arena>),
AttachTag(AttachTag<'arena>),
OnDirective(OnDirective<'arena>),
BindDirective(BindDirective<'arena>),
ClassDirective(ClassDirective<'arena>),
StyleDirective(StyleDirective<'arena>),
UseDirective(UseDirective<'arena>),
TransitionDirective(TransitionDirective<'arena>),
AnimateDirective(AnimateDirective<'arena>),
LetDirective(LetDirective<'arena>),
}
impl<'arena> AttributeNode<'arena> {
pub fn span(&self) -> Span {
match self {
AttributeNode::Attribute(a) => a.span,
AttributeNode::SpreadAttribute(s) => s.span,
AttributeNode::AttachTag(t) => t.span,
AttributeNode::OnDirective(d) => d.span,
AttributeNode::BindDirective(d) => d.span,
AttributeNode::ClassDirective(d) => d.span,
AttributeNode::StyleDirective(d) => d.span,
AttributeNode::UseDirective(d) => d.span,
AttributeNode::TransitionDirective(d) => d.span,
AttributeNode::AnimateDirective(d) => d.span,
AttributeNode::LetDirective(d) => d.span,
}
}
}
#[derive(Debug, Clone)]
pub enum AttributeValue<'arena> {
Text(Text),
ExpressionTag(ExpressionTag<'arena>),
}
#[inline]
pub(crate) const fn is_collapsible_ws(b: u8) -> bool {
matches!(b, b' ' | b'\t' | b'\n' | b'\r')
}
#[inline]
pub fn is_collapsible_ws_char(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '\r')
}
#[inline]
pub(crate) fn collapsible_ws_prefix_len(s: &str) -> usize {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() && is_collapsible_ws(bytes[i]) {
i += 1;
}
i
}
#[inline]
pub(crate) fn collapsible_ws_suffix_len(s: &str) -> usize {
let bytes = s.as_bytes();
let mut i = bytes.len();
while i > 0 && is_collapsible_ws(bytes[i - 1]) {
i -= 1;
}
bytes.len() - i
}
#[inline]
pub(crate) fn is_collapsible_ws_str(s: &str) -> bool {
collapsible_ws_prefix_len(s) == s.len()
}
#[inline]
pub(crate) fn trim_start_collapsible_ws(s: &str) -> &str {
&s[collapsible_ws_prefix_len(s)..]
}
#[inline]
pub(crate) fn trim_end_collapsible_ws(s: &str) -> &str {
&s[..s.len() - collapsible_ws_suffix_len(s)]
}
#[inline]
pub(crate) fn split_collapsible_ws(s: &str) -> impl Iterator<Item = &str> {
CollapsibleWords { s, pos: 0 }
}
#[inline]
pub(crate) fn collapsible_word_ranges(s: &str) -> impl Iterator<Item = CollapsibleWord> {
CollapsibleWordRanges {
bytes: s.as_bytes(),
pos: 0,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct CollapsibleWord {
pub(crate) start: u32,
pub(crate) end: u32,
pub(crate) is_ascii: bool,
}
impl CollapsibleWord {
#[inline]
pub(crate) fn extract<'a>(&self, s: &'a str) -> &'a str {
&s[self.start as usize..self.end as usize]
}
}
#[inline]
fn next_collapsible_word(bytes: &[u8], from: usize) -> Option<(usize, usize, u8)> {
let mut start = from;
while start < bytes.len() && is_collapsible_ws(bytes[start]) {
start += 1;
}
if start == bytes.len() {
return None;
}
let mut bits = bytes[start];
let mut end = start + 1;
while end < bytes.len() && !is_collapsible_ws(bytes[end]) {
bits |= bytes[end];
end += 1;
}
Some((start, end, bits))
}
struct CollapsibleWords<'a> {
s: &'a str,
pos: usize,
}
impl<'a> Iterator for CollapsibleWords<'a> {
type Item = &'a str;
#[inline]
fn next(&mut self) -> Option<&'a str> {
let Some((start, end, _)) = next_collapsible_word(self.s.as_bytes(), self.pos) else {
self.pos = self.s.len();
return None;
};
self.pos = end;
Some(&self.s[start..end])
}
}
struct CollapsibleWordRanges<'a> {
bytes: &'a [u8],
pos: usize,
}
impl Iterator for CollapsibleWordRanges<'_> {
type Item = CollapsibleWord;
#[inline]
fn next(&mut self) -> Option<CollapsibleWord> {
let Some((start, end, bits)) = next_collapsible_word(self.bytes, self.pos) else {
self.pos = self.bytes.len();
return None;
};
self.pos = end;
Some(CollapsibleWord {
start: start as u32,
end: end as u32,
is_ascii: bits < 0x80,
})
}
}
#[inline]
pub fn text_edge_ws(raw: &str, leading: bool) -> &str {
if leading {
&raw[..collapsible_ws_prefix_len(raw)]
} else {
&raw[raw.len() - collapsible_ws_suffix_len(raw)..]
}
}
#[inline]
pub(crate) fn text_edge_newlines(raw: &str, leading: bool) -> usize {
let bytes = raw.as_bytes();
let mut newlines = 0;
if leading {
for &b in bytes {
if !is_collapsible_ws(b) {
break;
}
newlines += usize::from(b == b'\n');
}
} else {
for &b in bytes.iter().rev() {
if !is_collapsible_ws(b) {
break;
}
newlines += usize::from(b == b'\n');
}
}
newlines
}
#[derive(Debug, Clone)]
pub struct Text {
pub raw_span: Span,
pub decoding: TextDecoding,
pub span: Span,
pub is_collapsible_ws_only: bool,
pub newline_count: u8,
}
#[inline]
fn text_ws_scalars(raw: &[u8]) -> (bool, u8) {
let mut weight = 0usize;
let mut i = 0;
while i < raw.len() {
let w = TEXT_WS_WEIGHT[usize::from(raw[i])];
if w == 0 {
return (false, capped_newline_count(&raw[i + 1..], weight - i));
}
weight += usize::from(w);
i += 1;
}
(true, (weight - raw.len()).min(2) as u8)
}
const TEXT_WS_WEIGHT: [u8; 256] = {
let mut t = [0; 256];
let mut i = 0;
while i < 256 {
let b = i as u8;
t[i] = if b == b'\n' {
2
} else if is_collapsible_ws(b) {
1
} else {
0
};
i += 1;
}
t
};
#[inline(never)]
fn capped_newline_count(rest: &[u8], newlines: usize) -> u8 {
if newlines >= 2 {
return 2;
}
(newlines + tsv_lang::swar::count_byte_up_to(rest, b'\n', 2 - newlines)) as u8
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextDecoding {
Fragment,
AttributeValue,
Raw,
}
impl TextDecoding {
pub(crate) fn reference_context(self) -> crate::html::ReferenceContext {
match self {
Self::AttributeValue => crate::html::ReferenceContext::AttributeValue,
Self::Fragment | Self::Raw => crate::html::ReferenceContext::Text,
}
}
}
impl Text {
pub fn new(raw_span: Span, decoding: TextDecoding, span: Span, source: &str) -> Self {
let (is_collapsible_ws_only, newline_count) =
text_ws_scalars(&source.as_bytes()[raw_span.range()]);
Text {
raw_span,
decoding,
span,
is_collapsible_ws_only,
newline_count,
}
}
#[inline]
pub fn has_newline(&self) -> bool {
self.newline_count >= 1
}
pub fn raw<'s>(&self, source: &'s str) -> &'s str {
self.raw_span.extract(source)
}
#[inline(never)]
pub fn data<'s>(&self, source: &'s str) -> Cow<'s, str> {
let raw = self.raw(source);
if self.data_is_raw(raw) {
return Cow::Borrowed(raw);
}
Cow::Owned(decode_text_data(raw, self.decoding.reference_context()))
}
#[inline]
pub(crate) fn data_is_raw(&self, raw: &str) -> bool {
debug_assert!(!self.is_collapsible_ws_only || !raw.contains('&'));
self.is_collapsible_ws_only || self.decoding == TextDecoding::Raw || !raw.contains('&')
}
}
#[cold]
#[inline(never)]
fn decode_text_data(raw: &str, context: crate::html::ReferenceContext) -> String {
crate::html::decode_character_references(raw, context).into_owned()
}
#[derive(Debug, Clone)]
pub struct ExpressionTag<'arena> {
pub expression: &'arena Expression<'arena>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct Script<'arena> {
pub content: Program<'arena>,
pub attributes: &'arena [AttributeNode<'arena>],
pub context: ScriptContext,
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ScriptContext {
Default = 0, Module = 1, }
impl ScriptContext {
#[inline]
pub const fn as_str(self) -> &'static str {
match self {
ScriptContext::Default => "default",
ScriptContext::Module => "module",
}
}
}
#[derive(Debug, Clone)]
pub struct Style<'arena> {
pub span: Span, pub content_span: Span, pub attributes: &'arena [AttributeNode<'arena>],
pub css_stylesheet: CssStyleSheet<'arena>,
}
#[cfg(all(test, target_pointer_width = "64"))]
const _: () = assert!(size_of::<FragmentNode<'static>>() == 120);
#[cfg(all(test, target_pointer_width = "64"))]
const _: () = assert!(size_of::<AttributeNode<'static>>() == 72);
#[cfg(all(test, target_pointer_width = "64"))]
const _: () = assert!(size_of::<ExpressionTag<'static>>() == 16);
#[cfg(test)]
mod tests {
use super::{SpecialElementKind, SpecialElementTag};
#[test]
fn special_element_tag_from_name() {
use SpecialElementTag::*;
assert_eq!(
SpecialElementTag::from_tag_name("svelte:head", false, false),
Some(SvelteHead)
);
assert_eq!(
SpecialElementTag::from_tag_name("svelte:boundary", false, false),
Some(SvelteBoundary)
);
assert_eq!(
SpecialElementTag::from_tag_name("title", true, false),
Some(TitleElement)
);
assert_eq!(
SpecialElementTag::from_tag_name("title", false, false),
None
);
assert_eq!(
SpecialElementTag::from_tag_name("slot", false, false),
Some(SlotElement)
);
assert_eq!(SpecialElementTag::from_tag_name("slot", false, true), None);
assert_eq!(SpecialElementTag::from_tag_name("div", true, false), None);
assert_eq!(
SpecialElementTag::from_tag_name("svelte:unknown", false, false),
None
);
}
#[test]
fn special_element_kind_is_block() {
assert!(SpecialElementKind::SvelteHead.is_block());
assert!(SpecialElementKind::SvelteWindow.is_block());
assert!(SpecialElementKind::SvelteBody.is_block());
assert!(SpecialElementKind::SvelteDocument.is_block());
assert!(!SpecialElementKind::SlotElement.is_block());
assert!(!SpecialElementKind::SvelteSelf.is_block());
assert!(!SpecialElementKind::SvelteFragment.is_block());
assert!(!SpecialElementKind::SvelteBoundary.is_block());
assert!(!SpecialElementKind::TitleElement.is_block());
}
#[test]
fn text_new_precomputes_whitespace_flags() {
use super::{Span, Text, TextDecoding};
let mk = |raw: &str| {
let span = Span {
start: 0,
end: raw.len() as u32,
};
Text::new(span, TextDecoding::Fragment, span, raw)
};
assert!(mk(" \t\n ").is_collapsible_ws_only);
assert!(mk("").is_collapsible_ws_only);
assert!(!mk("\u{00A0}").is_collapsible_ws_only);
assert!(!mk("a").is_collapsible_ws_only);
assert_eq!(mk("a b").newline_count, 0);
assert!(!mk("a b").has_newline());
assert_eq!(mk("a\nb").newline_count, 1);
assert!(mk("a\nb").has_newline());
assert_eq!(mk("a\n\nb").newline_count, 2);
assert_eq!(mk("\n\n\n\n").newline_count, 2);
}
fn text_scalars_model(raw: &str) -> (bool, u8) {
(
raw.bytes().all(super::is_collapsible_ws),
raw.bytes().filter(|&b| b == b'\n').take(2).count() as u8,
)
}
fn assert_text_scalars(raw: &str) {
use super::{Span, Text, TextDecoding};
let expected = text_scalars_model(raw);
let prefix = "<p>a\n\n";
for source in [raw.to_owned(), format!("{prefix}{raw}\n\nb</p>")] {
let start = if source.len() == raw.len() {
0
} else {
prefix.len()
};
let span = Span {
start: start as u32,
end: (start + raw.len()) as u32,
};
let text = Text::new(span, TextDecoding::Fragment, span, &source);
assert_eq!(
(text.is_collapsible_ws_only, text.newline_count),
expected,
"raw {raw:?} at {start}"
);
}
}
#[test]
fn text_new_scalars_match_the_two_pass_model_exhaustively() {
let alphabet: Vec<String> = [' ', '\t', '\n', '\r', '&', 'a', '\u{c}']
.into_iter()
.chain([0xA0, 0x202F, 0xE9].map(|c| char::from_u32(c).unwrap()))
.map(String::from)
.collect();
let head = &alphabet[..6];
let mut cases = 0usize;
for (symbols, max_len) in [(&alphabet[..], 5), (head, 6)] {
let mut digits: Vec<usize> = Vec::new();
loop {
let raw: String = digits.iter().map(|&d| symbols[d].as_str()).collect();
assert_text_scalars(&raw);
cases += 1;
let mut k = 0;
loop {
if k == digits.len() {
digits.push(0);
break;
}
digits[k] += 1;
if digits[k] < symbols.len() {
break;
}
digits[k] = 0;
k += 1;
}
if digits.len() > max_len {
break;
}
}
}
let expected: usize = (0..=5).map(|n| 10usize.pow(n)).sum::<usize>()
+ (0..=6).map(|n| 6usize.pow(n)).sum::<usize>();
assert_eq!(cases, expected);
}
#[test]
fn text_new_scalars_match_the_two_pass_model_on_long_runs() {
for len in [7, 8, 9, 15, 16, 17, 23, 24, 25, 31, 32, 33, 64, 65] {
for filler in [b' ', b'\t', b'a'] {
let base = vec![filler; len];
assert_text_scalars(std::str::from_utf8(&base).unwrap());
for i in 0..len {
for decider in [b'a', b'&', b'\n', b'\r', 0x0C] {
let mut bytes = base.clone();
bytes[i] = decider;
assert_text_scalars(std::str::from_utf8(&bytes).unwrap());
}
for j in i + 1..len {
let mut bytes = base.clone();
bytes[i] = b'\n';
bytes[j] = b'\n';
assert_text_scalars(std::str::from_utf8(&bytes).unwrap());
for k in [0, usize::midpoint(i, j), len - 1] {
if k != i && k != j {
let mut mixed = bytes.clone();
mixed[k] = b'x';
assert_text_scalars(std::str::from_utf8(&mixed).unwrap());
}
}
}
}
}
}
}
#[test]
fn text_new_scalars_match_the_two_pass_model_on_multibyte_runs() {
for filler in [0xE9, 0xA0, 0x202F].map(|c| char::from_u32(c).unwrap().to_string()) {
for len in [4, 8, 12, 17, 33] {
for head in ["", "a", "\n", " \t"] {
for i in 0..len {
for j in i..len {
let raw: String = std::iter::once(head)
.chain((0..len).map(|k| {
if k == i || k == j {
"\n"
} else {
filler.as_str()
}
}))
.collect();
assert_text_scalars(&raw);
}
}
}
}
}
}
#[test]
fn text_data_matches_the_decode_model() {
use super::{Span, Text, TextDecoding};
use std::borrow::Cow;
let nbsp = char::from_u32(0xA0).unwrap();
let raws = [
String::new(),
" ".into(),
"\n\t\t".into(),
" \r\n \t".into(),
"a".into(),
"&".into(),
"&".into(),
" < ".into(),
"	".into(),
"\n \n".into(),
"a&b".into(),
"¬it;".into(),
format!("{nbsp}"),
format!(" {nbsp}&"),
];
for decoding in [
TextDecoding::Fragment,
TextDecoding::AttributeValue,
TextDecoding::Raw,
] {
for raw in &raws {
let source = format!("<p>{raw}</p>");
let span = Span {
start: 3,
end: (3 + raw.len()) as u32,
};
let text = Text::new(span, decoding, span, &source);
let expected: Cow<'_, str> = match decoding {
TextDecoding::Raw => Cow::Borrowed(raw),
_ if raw.contains('&') => Cow::Owned(
crate::html::decode_character_references(raw, decoding.reference_context())
.into_owned(),
),
_ => Cow::Borrowed(raw),
};
let data = text.data(&source);
assert_eq!(data, expected, "{decoding:?} {raw:?}");
assert_eq!(
text.data_is_raw(text.raw(&source)),
matches!(expected, Cow::Borrowed(_)),
"{decoding:?} {raw:?}"
);
assert_eq!(
matches!(data, Cow::Borrowed(_)),
matches!(expected, Cow::Borrowed(_)),
"{decoding:?} {raw:?}"
);
}
}
}
#[test]
fn svelte_void_adds_the_doctype_declaration() {
use super::is_svelte_void;
for void in ["br", "img", "!doctype", "!DOCTYPE", "!DocType"] {
assert!(is_svelte_void(void), "{void:?}");
}
for not_void in ["div", "BR", "doctype", "!", "!doctypex", "!DOCTYPEX"] {
assert!(!is_svelte_void(not_void), "{not_void:?}");
}
assert!(!crate::html::is_void_element("!doctype"));
}
#[test]
fn tag_facts_bits_agree_with_the_pure_predicates() {
use super::{ElementKind, TagFacts, is_component_name, is_svelte_void};
let probes = [
"div",
"p",
"h1",
"menu",
"table",
"select",
"tr",
"tbody",
"thead",
"tfoot",
"colgroup",
"datalist",
"optgroup",
"ul",
"li",
"pre",
"hr",
"blockquote",
"br",
"img",
"input",
"wbr",
"command",
"keygen",
"!doctype",
"!DOCTYPE",
"!DocType",
"!",
"!doctypex",
"!DOCTYPEX",
"svg",
"circle",
"foreignObject",
"color-profile",
"math",
"annotation-xml",
"mi",
"script",
"style",
"template",
"textarea",
"Button",
"MyComponent",
"svelte:head",
"svelte:component",
"foo:bar",
"Foo:bar",
"foo.bar",
"foo.bar:baz",
"Foo.bar:",
":",
".",
"Div",
"DIV",
"Δcomp",
"Écomp",
"Яcomp",
"étoile",
"span",
"td",
"divx",
"di",
"xdiv",
"doctype",
"é",
"ünknown",
"",
];
for tag in probes {
let facts = TagFacts::compute(tag);
assert_eq!(
facts.is_block(),
crate::html::is_block_element(tag),
"block: {tag:?}"
);
assert_eq!(facts.is_void(), is_svelte_void(tag), "void: {tag:?}");
assert_eq!(
facts.is_foreign(),
crate::html::is_foreign_element(tag),
"foreign: {tag:?}"
);
assert_eq!(
facts.is_component_name(),
is_component_name(tag),
"component name: {tag:?}"
);
assert_eq!(
facts.element_kind(),
if is_component_name(tag) {
ElementKind::Component
} else {
ElementKind::Html
},
"element kind: {tag:?}"
);
assert_eq!(
facts.is_namespaced(),
tag.contains(':'),
"namespaced: {tag:?}"
);
assert_eq!(facts.is_style(), tag == "style", "style: {tag:?}");
assert_eq!(
facts.is_raw_text(),
tag == "script" || tag == "style",
"raw text: {tag:?}"
);
assert_eq!(facts.is_template(), tag == "template", "template: {tag:?}");
assert_eq!(
facts.is_ws_sensitive(),
crate::html::preserves_whitespace(tag),
"ws-sensitive: {tag:?}"
);
assert_eq!(
facts.collapses_child_whitespace(),
crate::html::collapses_child_whitespace(tag),
"ws-collapsing: {tag:?}"
);
assert_eq!(
facts.is_declaration(),
tag.starts_with('!'),
"declaration: {tag:?}"
);
assert_eq!(
facts.is_line_break(),
crate::html::is_line_break_element(tag),
"line break: {tag:?}"
);
}
}
}
#[cfg(test)]
mod lang_attribute_tests {
use super::EmbeddedLang;
fn langs(open_tag: &str) -> (bool, bool) {
let tag = if open_tag.starts_with("<style") {
"style"
} else {
"script"
};
let src = format!("<div>{open_tag}x</{tag}></div>");
let arena = bumpalo::Bump::new();
let ast = crate::__private::parse(&src, &arena).expect("parses");
let attrs =
raw_text_attributes(ast.fragment.nodes, tag, &src).expect("the raw-text element");
(
EmbeddedLang::Style.is_frozen(attrs, &src),
EmbeddedLang::Script.is_frozen(attrs, &src),
)
}
fn raw_text_attributes<'a>(
nodes: &'a [super::FragmentNode<'a>],
tag: &str,
source: &str,
) -> Option<&'a [super::AttributeNode<'a>]> {
nodes.iter().find_map(|node| match node {
super::FragmentNode::Element(el) if el.name(source) == tag => Some(el.attributes),
super::FragmentNode::Element(el) => raw_text_attributes(el.fragment.nodes, tag, source),
_ => None,
})
}
#[test]
fn an_absent_or_matching_lang_is_native() {
assert!(!langs("<style>").0, "<style>");
assert!(!langs("<style lang=\"css\">").0, "<style lang=\"css\">");
assert!(!langs("<script>").1, "<script>");
assert!(!langs("<script lang=\"ts\">").1, "<script lang=\"ts\">");
}
#[test]
fn the_value_is_trimmed_and_text_stripped() {
assert!(!langs("<style lang=\" css \">").0, "<style lang=\" css \">");
assert!(
!langs("<style type=\"text/css\">").0,
"<style type=\"text/css\">"
);
assert!(
!langs("<script type=\"text/ts\">").1,
"<script type=\"text/ts\">"
);
}
#[test]
fn the_value_is_decoded() {
assert!(
!langs("<style lang=\"css\">").0,
"<style lang=\"css\">"
);
assert!(
!langs("<script lang=\"ts\">").1,
"<script lang=\"ts\">"
);
}
#[test]
fn lang_outranks_type_in_either_order() {
assert!(
langs("<style type=\"text/css\" lang=\"scss\">").0,
"<style type=\"text/css\" lang=\"scss\">"
);
assert!(
langs("<style lang=\"scss\" type=\"text/css\">").0,
"<style lang=\"scss\" type=\"text/css\">"
);
assert!(
!langs("<script type=\"text/coffeescript\" lang=\"ts\">").1,
"<script type=\"text/coffeescript\" lang=\"ts\">"
);
assert!(
!langs("<script lang=\"ts\" type=\"text/coffeescript\">").1,
"<script lang=\"ts\" type=\"text/coffeescript\">"
);
}
#[test]
fn an_empty_value_names_nothing() {
assert!(!langs("<style lang=\"\">").0, "<style lang=\"\">");
assert!(!langs("<script lang=\"\">").1, "<script lang=\"\">");
assert!(
langs("<style lang=\"\" type=\"text/less\">").0,
"<style lang=\"\" type=\"text/less\">"
);
assert!(
langs("<style type=\"text/less\" lang=\"\">").0,
"<style type=\"text/less\" lang=\"\">"
);
}
#[test]
fn a_boolean_attribute_names_nothing() {
assert!(!langs("<style lang>").0, "<style lang>");
assert!(!langs("<script lang>").1, "<script lang>");
}
#[test]
fn the_script_set_is_the_js_ts_family() {
for tag in [
"<script lang=\"js\">",
"<script lang=\"javascript\">",
"<script lang=\"typescript\">",
"<script type=\"module\">",
"<script type=\"text/javascript\">",
"<script type=\"text/ecmascript\">",
"<script type=\"application/javascript\">",
"<script type=\"application/ecmascript\">",
] {
assert!(!langs(tag).1, "{tag}");
}
}
#[test]
fn any_other_name_is_foreign() {
assert!(langs("<style lang=\"less\">").0, "<style lang=\"less\">");
assert!(langs("<style lang=\"scss\">").0, "<style lang=\"scss\">");
assert!(langs("<style lang=\"sass\">").0, "<style lang=\"sass\">");
for tag in [
"<script lang=\"coffee\">",
"<script type=\"text/coffeescript\">",
"<script type=\"application/json\">",
"<script type=\"application/ld+json\">",
"<script type=\"importmap\">",
"<script type=\"text/jscript\">",
"<script type=\"text/livescript\">",
"<script type=\"application/x-javascript\">",
"<script type=\"text/javascript1.5\">",
] {
assert!(langs(tag).1, "{tag}");
}
}
}
#[cfg(test)]
mod collapsible_ws_tests {
use super::*;
fn for_every_arrangement(check: impl FnMut(&str)) {
let nul = char::from(0_u8).to_string();
let del = char::from(0x7f_u8).to_string();
let pieces = [
" ", "\t", "\n", "\r", "\u{c}", "\u{a0}", "x", "é", &nul, &del,
];
crate::test_support::for_every_arrangement(&pieces, 4, check);
}
#[test]
fn byte_trims_match_the_searchers() {
for_every_arrangement(|s| {
assert_eq!(
trim_start_collapsible_ws(s),
s.trim_start_matches(is_collapsible_ws_char),
"{s:?}"
);
assert_eq!(
trim_end_collapsible_ws(s),
s.trim_end_matches(is_collapsible_ws_char),
"{s:?}"
);
assert_eq!(
text_edge_ws(s, true).len(),
collapsible_ws_prefix_len(s),
"{s:?}"
);
assert_eq!(
text_edge_ws(s, false).len(),
collapsible_ws_suffix_len(s),
"{s:?}"
);
});
}
#[test]
fn edge_newlines_match_the_edge_run() {
for_every_arrangement(|s| {
for leading in [true, false] {
assert_eq!(
text_edge_newlines(s, leading),
text_edge_ws(s, leading).matches('\n').count(),
"{s:?} leading={leading}"
);
}
});
assert_eq!(text_edge_newlines("\r\n\r\n\tx", true), 2);
assert_eq!(text_edge_newlines("x\n\t\r\n ", false), 2);
assert_eq!(text_edge_newlines("\n\u{c}\nx", true), 1);
assert_eq!(text_edge_newlines("x\n\u{a0}\n", false), 1);
assert_eq!(text_edge_newlines("\n\n", true), 2);
assert_eq!(text_edge_newlines("", false), 0);
}
#[test]
fn byte_split_matches_the_searcher() {
for_every_arrangement(|s| {
let words: Vec<&str> = split_collapsible_ws(s).collect();
let reference: Vec<&str> = s
.split(is_collapsible_ws_char)
.filter(|w| !w.is_empty())
.collect();
assert_eq!(words, reference, "{s:?}");
});
}
#[test]
fn word_ranges_match_the_slice_split() {
for_every_arrangement(|s| {
let words: Vec<&str> = split_collapsible_ws(s).collect();
let ranged: Vec<&str> = collapsible_word_ranges(s)
.map(|word| {
let text = word.extract(s);
assert_eq!(word.is_ascii, text.is_ascii(), "{s:?} word {text:?}");
assert_eq!(
word.is_ascii,
!text.bytes().any(tsv_lang::printing::is_width_relevant),
"{s:?} word {text:?}"
);
text
})
.collect();
assert_eq!(ranged, words, "{s:?}");
});
}
}