use std::any::TypeId;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use wasm_bindgen::JsValue;
use web_sys::Element;
use crate::reactive::ScopeId;
use crate::scope::Scope;
use crate::templates_plan::StaticTemplatePlan;
pub type ComponentCtor = fn() -> Scope;
pub type ComponentMountFn = fn(&Element, ScopeId, &JsValue);
pub struct ComponentEntry {
pub name: &'static str,
pub ctor: ComponentCtor,
pub mount_template: Option<ComponentMountFn>,
}
#[derive(Clone, Copy)]
pub struct RegisteredComponent {
pub canonical: &'static str,
pub owner: &'static str,
pub ctor: ComponentCtor,
pub mount_template: Option<ComponentMountFn>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RegistryErrorKind {
DuplicateCanonicalTag,
DuplicateAlias,
AliasConflictsWithCanonical,
CanonicalConflictsWithAlias,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RegistryError {
pub kind: RegistryErrorKind,
pub tag: &'static str,
pub first_owner: &'static str,
pub second_owner: &'static str,
}
impl std::fmt::Display for RegistryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let what = match self.kind {
RegistryErrorKind::DuplicateCanonicalTag => "duplicate component tag",
RegistryErrorKind::DuplicateAlias => "duplicate component alias",
RegistryErrorKind::AliasConflictsWithCanonical => {
"alias collides with an existing canonical tag"
}
RegistryErrorKind::CanonicalConflictsWithAlias => {
"canonical tag collides with an existing alias"
}
};
write!(
f,
"pocopine registry: {what} `{}` (owners: `{}` and `{}`)",
self.tag, self.first_owner, self.second_owner,
)
}
}
#[derive(Default)]
struct Registry {
canonical: HashMap<&'static str, RegisteredComponent>,
aliases: HashMap<&'static str, (&'static str, &'static str)>,
errors: Vec<RegistryError>,
}
thread_local! {
static REGISTRY: RefCell<Registry> = RefCell::new(Registry::default());
static ACTIVE_PHF_REGISTRY: RefCell<Option<&'static phf::Map<&'static str, &'static ComponentVTable>>> =
const { RefCell::new(None) };
static REGISTERED: RefCell<HashSet<TypeId>> = RefCell::new(HashSet::new());
}
pub struct ComponentVTable {
pub name: &'static str,
pub register: fn(),
pub template_html: Option<&'static str>,
pub plan: Option<&'static StaticTemplatePlan>,
pub mount_template: Option<ComponentMountFn>,
}
pub fn set_active_phf_registry(
registry: &'static phf::Map<&'static str, &'static ComponentVTable>,
) {
ACTIVE_PHF_REGISTRY.with(|active| {
*active.borrow_mut() = Some(registry);
});
}
#[doc(hidden)]
pub fn clear_active_phf_registry_for_test() {
ACTIVE_PHF_REGISTRY.with(|active| {
*active.borrow_mut() = None;
});
crate::templates::clear_template_element_cache_for_test();
}
pub fn active_component_vtable(name: &str) -> Option<&'static ComponentVTable> {
ACTIVE_PHF_REGISTRY.with(|active| {
active
.borrow()
.and_then(|registry| registry.get(name).copied())
})
}
pub fn active_has_template(name: &str) -> bool {
ACTIVE_PHF_REGISTRY.with(|active| {
active
.borrow()
.and_then(|registry| registry.get(name))
.and_then(|v| v.template_html)
.is_some()
})
}
pub fn active_component_names() -> Vec<&'static str> {
ACTIVE_PHF_REGISTRY.with(|active| {
active
.borrow()
.map(|registry| registry.keys().copied().collect())
.unwrap_or_default()
})
}
pub fn mark_registered<T: 'static>() -> bool {
REGISTERED.with(|r| r.borrow_mut().insert(TypeId::of::<T>()))
}
pub fn register_component(canonical: &'static str, owner: &'static str, ctor: ComponentCtor) {
register_component_with_mount(canonical, owner, ctor, None);
}
pub fn register_component_with_mount(
canonical: &'static str,
owner: &'static str,
ctor: ComponentCtor,
mount_template: Option<ComponentMountFn>,
) {
REGISTRY.with(|r| {
let mut reg = r.borrow_mut();
if let Some(&(_canon, alias_owner)) = reg.aliases.get(canonical) {
if alias_owner != owner {
reg.errors.push(RegistryError {
kind: RegistryErrorKind::CanonicalConflictsWithAlias,
tag: canonical,
first_owner: alias_owner,
second_owner: owner,
});
return;
}
}
if let Some(existing_owner) = reg.canonical.get(canonical).map(|e| e.owner) {
if existing_owner == owner {
return;
}
reg.errors.push(RegistryError {
kind: RegistryErrorKind::DuplicateCanonicalTag,
tag: canonical,
first_owner: existing_owner,
second_owner: owner,
});
return;
}
reg.canonical.insert(
canonical,
RegisteredComponent {
canonical,
owner,
ctor,
mount_template,
},
);
});
}
pub fn register_component_as(
alias: &'static str,
canonical: &'static str,
owner: &'static str,
ctor: ComponentCtor,
) {
register_component(canonical, owner, ctor);
REGISTRY.with(|r| {
let mut reg = r.borrow_mut();
if let Some(existing_owner) = reg.canonical.get(alias).map(|e| e.owner) {
if existing_owner == owner {
return;
}
reg.errors.push(RegistryError {
kind: RegistryErrorKind::AliasConflictsWithCanonical,
tag: alias,
first_owner: existing_owner,
second_owner: owner,
});
return;
}
if let Some(&(_canon, existing_owner)) = reg.aliases.get(alias) {
if existing_owner == owner {
return;
}
reg.errors.push(RegistryError {
kind: RegistryErrorKind::DuplicateAlias,
tag: alias,
first_owner: existing_owner,
second_owner: owner,
});
return;
}
reg.aliases.insert(alias, (canonical, owner));
});
}
pub fn register_component_prefixed(
prefix: &'static str,
short: &'static str,
owner: &'static str,
ctor: ComponentCtor,
) {
let combined: &'static str = Box::leak(format!("{prefix}-{short}").into_boxed_str());
register_component(combined, owner, ctor);
}
pub static COMPONENT_ENTRIES: &[ComponentEntry] = &[];
pub fn instantiate(name: &str) -> Option<Scope> {
REGISTRY.with(|r| {
let reg = r.borrow();
if let Some(entry) = reg.canonical.get(name) {
return Some((entry.ctor)());
}
if let Some(&(canon, _)) = reg.aliases.get(name) {
if let Some(entry) = reg.canonical.get(canon) {
return Some((entry.ctor)());
}
}
None
})
}
pub fn mount_template_for(name: &str) -> Option<ComponentMountFn> {
REGISTRY.with(|r| {
let reg = r.borrow();
if let Some(entry) = reg.canonical.get(name) {
return entry.mount_template;
}
if let Some(&(canon, _)) = reg.aliases.get(name) {
if let Some(entry) = reg.canonical.get(canon) {
return entry.mount_template;
}
}
None
})
}
pub fn canonical_component_name(name: &str) -> Option<&'static str> {
REGISTRY.with(|r| {
let reg = r.borrow();
if let Some(entry) = reg.canonical.get(name) {
return Some(entry.canonical);
}
if let Some(&(canon, _)) = reg.aliases.get(name) {
if let Some(entry) = reg.canonical.get(canon) {
return Some(entry.canonical);
}
}
None
})
}
pub fn registry_errors() -> Vec<RegistryError> {
REGISTRY.with(|r| r.borrow().errors.clone())
}
pub fn registered_component_names() -> Vec<String> {
REGISTRY.with(|r| {
let reg = r.borrow();
let mut names: Vec<String> = reg.canonical.keys().map(|s| s.to_string()).collect();
names.extend(reg.aliases.keys().map(|s| s.to_string()));
names.sort();
names
})
}
pub fn verify_registry() -> Result<(), Vec<RegistryError>> {
let errors = registry_errors();
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
#[track_caller]
pub fn assert_registry_clean() {
if let Err(errors) = verify_registry() {
let mut msg = String::from("pocopine registry has unresolved collisions:\n");
for err in &errors {
msg.push_str(" - ");
msg.push_str(&err.to_string());
msg.push('\n');
}
panic!("{msg}");
}
}
#[doc(hidden)]
pub fn __reset_for_test() {
REGISTRY.with(|r| {
let mut reg = r.borrow_mut();
reg.canonical.clear();
reg.aliases.clear();
reg.errors.clear();
});
REGISTERED.with(|r| r.borrow_mut().clear());
}
pub fn render_boot_error(errors: &[RegistryError]) {
let Some(win) = web_sys::window() else { return };
let Some(doc) = win.document() else { return };
let Some(body) = doc.body() else { return };
body.set_inner_html("");
let Ok(banner) = doc.create_element("div") else {
return;
};
let _ = banner.set_attribute("data-pocopine-boot-error", "registry");
let _ = banner.set_attribute(
"style",
"position:fixed;inset:0;background:#1b1b1f;color:#f5f5f7;\
font-family:ui-monospace,monospace;padding:24px;overflow:auto;\
z-index:2147483647;",
);
let mut html = String::from(
"<h2 style=\"margin:0 0 12px 0;color:#ff6b6b;\">pocopine: \
component registry has unresolved collisions</h2>\
<p style=\"margin:0 0 16px 0;\">The runtime refused to mount \
because two owners registered the same component tag. Resolve \
the conflicts and reload.</p><ul style=\"margin:0;padding-left:20px;\">",
);
for err in errors {
html.push_str("<li style=\"margin-bottom:8px;\"><code>");
html.push_str(&html_escape(err.tag));
html.push_str("</code> — ");
html.push_str(match err.kind {
RegistryErrorKind::DuplicateCanonicalTag => "duplicate canonical tag",
RegistryErrorKind::DuplicateAlias => "duplicate alias",
RegistryErrorKind::AliasConflictsWithCanonical => {
"alias collides with an existing canonical tag"
}
RegistryErrorKind::CanonicalConflictsWithAlias => {
"canonical tag collides with an existing alias"
}
});
html.push_str(" (owners: <code>");
html.push_str(&html_escape(err.first_owner));
html.push_str("</code> and <code>");
html.push_str(&html_escape(err.second_owner));
html.push_str("</code>)</li>");
}
html.push_str("</ul>");
banner.set_inner_html(&html);
let _ = body.append_child(&banner);
web_sys::console::error_1(
&format!(
"pocopine: component registry has {} unresolved collision(s); refusing to mount",
errors.len()
)
.into(),
);
for err in errors {
web_sys::console::error_1(&err.to_string().into());
}
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}