use std::{fmt, str::FromStr};
pub const VERSION: u32 = 2;
pub const VERSION_ATTRIBUTE: &str = "data-rf-version";
pub const COMPONENT_ATTRIBUTE: &str = "data-rf-component";
pub const ELEMENT_ATTRIBUTE: &str = "data-rf-node";
pub const TEXT_ELEMENT_ATTRIBUTE: &str = "data-rf-text";
pub const MANAGED_ATTRIBUTE: &str = "data-rf-managed";
pub const EXTERNAL_ATTRIBUTE: &str = "data-rf-external";
pub const INSTANCE_ATTRIBUTE: &str = "data-rf-instance";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InvalidId;
impl fmt::Display for InvalidId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("expected a canonical unsigned template identifier")
}
}
impl std::error::Error for InvalidId {}
macro_rules! identifier {
($name:ident) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct $name(usize);
impl $name {
pub const fn new(index: usize) -> Self {
Self(index)
}
pub const fn index(self) -> usize {
self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl FromStr for $name {
type Err = InvalidId;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if value.is_empty()
|| (value.len() > 1 && value.starts_with('0'))
|| !value.bytes().all(|byte| byte.is_ascii_digit())
{
return Err(InvalidId);
}
value.parse().map(Self).map_err(|_| InvalidId)
}
}
};
}
identifier!(ComponentId);
identifier!(ElementId);
identifier!(TextId);
identifier!(MountId);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MountMarker {
Start(MountId),
End(MountId),
}
impl MountMarker {
pub fn parse(value: &str) -> Result<Option<Self>, InvalidId> {
if let Some(id) = value.strip_prefix("rf:mount:") {
id.parse().map(|id| Some(Self::Start(id)))
} else if let Some(id) = value.strip_prefix("/rf:mount:") {
id.parse().map(|id| Some(Self::End(id)))
} else {
Ok(None)
}
}
}
impl fmt::Display for MountMarker {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Start(id) => write!(f, "rf:mount:{id}"),
Self::End(id) => write!(f, "/rf:mount:{id}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextMarker {
Start(TextId),
End(TextId),
}
impl TextMarker {
pub fn parse(value: &str) -> Result<Option<Self>, InvalidId> {
if let Some(id) = value.strip_prefix("rf:") {
id.parse().map(|id| Some(Self::Start(id)))
} else if let Some(id) = value.strip_prefix("/rf:") {
id.parse().map(|id| Some(Self::End(id)))
} else {
Ok(None)
}
}
}
impl fmt::Display for TextMarker {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Start(id) => write!(f, "rf:{id}"),
Self::End(id) => write!(f, "/rf:{id}"),
}
}
}
pub fn reserved_attribute(name: &str) -> bool {
name.starts_with("data-rf-")
}
pub fn reserved_comment(value: &str) -> bool {
value.starts_with("rf:") || value.starts_with("/rf:")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RootKind {
Existing,
Template,
}
#[derive(Debug, Clone, Copy)]
pub struct ElementDescriptor {
pub id: ElementId,
pub tag: &'static str,
pub children: ChildPolicy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChildPolicy {
Static,
Managed,
}
#[derive(Debug, Clone, Copy)]
pub struct TextElementDescriptor {
pub id: TextId,
pub host: Option<ElementId>,
pub tag: &'static str,
}
#[derive(Debug)]
pub struct TemplateDescriptor {
pub version: u32,
pub component: ComponentId,
pub kind: RootKind,
pub elements: &'static [ElementDescriptor],
pub texts: &'static [TextId],
pub text_elements: &'static [TextElementDescriptor],
}
pub fn escape_into(output: &mut String, value: &str, attribute: bool) {
for ch in value.chars() {
match ch {
'&' => output.push_str("&"),
'<' => output.push_str("<"),
'>' => output.push_str(">"),
'"' if attribute => output.push_str("""),
'\'' if attribute => output.push_str("'"),
ch => output.push(ch),
}
}
}