use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{console, Element, Node};
use crate::directives::for_plan::{
BindingKind, StaticBinding, StaticChildMount, StaticForPlan, StaticIfPlan, StaticInterp,
StaticListener, StaticNativeModel, StaticOpaqueDirective, StaticRef, StaticSlotOutlet,
StaticTeleportPlan,
};
use crate::directives::interp::PlannedSegment;
use crate::directives::{self};
use crate::expr;
use crate::expr::StaticExpr;
use crate::reactive::ScopeId;
use crate::slot_fragment::SlotSet;
#[doc(hidden)]
pub struct StaticTemplatePlan {
pub bindings: &'static [StaticBinding],
pub listeners: &'static [StaticListener],
pub refs: &'static [StaticRef],
pub child_mounts: &'static [StaticChildMount],
pub if_plans: &'static [StaticIfPlan],
pub for_plans: &'static [StaticForPlan],
pub teleport_plans: &'static [StaticTeleportPlan],
pub slot_outlets: &'static [StaticSlotOutlet],
pub opaque_directives: &'static [StaticOpaqueDirective],
pub interps: &'static [StaticInterp],
pub native_models: &'static [StaticNativeModel],
}
#[doc(hidden)]
pub struct StaticInterpTarget {
pub parent: Element,
pub text: web_sys::Text,
pub segments: &'static [PlannedSegment],
}
thread_local! {
static TEMPLATE_PLANS: RefCell<HashMap<&'static str, &'static StaticTemplatePlan>> =
RefCell::new(HashMap::new());
static PLAN_FAILURES: Cell<u32> = const { Cell::new(0) };
}
pub fn register_template_plan(tag: &'static str, plan: &'static StaticTemplatePlan) {
TEMPLATE_PLANS.with(|registry| {
registry.borrow_mut().insert(tag, plan);
});
}
pub fn template_plan_for(tag: &str) -> Option<&'static StaticTemplatePlan> {
if let Some(plan) = crate::registry::active_component_vtable(tag).and_then(|v| v.plan) {
return Some(plan);
}
TEMPLATE_PLANS.with(|registry| registry.borrow().get(tag).copied())
}
pub fn registered_template_tags() -> Vec<String> {
let mut tags: Vec<String> = crate::registry::active_component_names()
.into_iter()
.filter(|name| {
crate::registry::active_component_vtable(name)
.and_then(|v| v.plan)
.is_some()
})
.map(str::to_string)
.collect();
let mut seen: HashSet<String> = tags.iter().cloned().collect();
TEMPLATE_PLANS.with(|registry| {
for tag in registry.borrow().keys() {
if seen.insert((*tag).to_string()) {
tags.push((*tag).to_string());
}
}
});
tags
}
pub fn plan_failure_count() -> u32 {
PLAN_FAILURES.with(|c| c.get())
}
#[doc(hidden)]
pub fn reset_plan_failure_count() {
PLAN_FAILURES.with(|c| c.set(0));
}
#[doc(hidden)]
pub fn record_plan_failure() {
PLAN_FAILURES.with(|c| c.set(c.get().saturating_add(1)));
}
#[doc(hidden)]
pub fn install_static_ref(
el: &Element,
scope_id: ScopeId,
entry: &'static StaticRef,
_template_name: &str,
) {
crate::refs::register(scope_id, entry.name, el);
}
#[doc(hidden)]
pub fn install_static_binding(
el: &Element,
proxy: &JsValue,
entry: &'static StaticBinding,
template_name: &str,
) {
let Some(evaluator) = static_evaluator(entry.compiled, entry.expr_src) else {
fail(
"binding-parse",
template_name,
entry.node_path,
Some(entry.expr_src),
);
return;
};
match entry.kind {
BindingKind::Text => directives::text::install_eval(el, proxy, evaluator),
BindingKind::Html => directives::html::install_eval(el, proxy, evaluator),
BindingKind::Show => directives::show::install_eval(el, proxy, evaluator),
BindingKind::Bind { arg } => directives::bind::install_eval(el, proxy, arg, evaluator),
BindingKind::Class => fail(
"binding-kind",
template_name,
entry.node_path,
Some(entry.expr_src),
),
}
}
#[doc(hidden)]
pub fn install_static_listener(
el: &Element,
scope_id: ScopeId,
proxy: &JsValue,
entry: &'static StaticListener,
template_name: &str,
) {
let ast = match expr::parse_cached(entry.expr_src) {
Ok(a) => a,
Err(_) => {
fail(
"listener-parse",
template_name,
entry.node_path,
Some(entry.expr_src),
);
return;
}
};
directives::on::install(
el,
scope_id,
proxy,
entry.event,
entry.modifiers,
Rc::new(directives::on::backfill_legacy_call(ast)),
);
}
#[doc(hidden)]
pub fn install_static_child_mount(
el: &Element,
scope_id: ScopeId,
proxy: &JsValue,
entry: &'static StaticChildMount,
template_name: &str,
) {
if entry.slots.is_empty() {
crate::mount::mount_child_component(el, entry.tag);
} else {
let mut set = SlotSet::new();
for s in entry.slots {
set = match s.scoped_let {
Some(let_ident) => set.scoped(s.name, s.fragment, let_ident),
None => set.named(s.name, s.fragment),
};
}
crate::mount::mount_child_component_with_slots(el, entry.tag, set, scope_id, proxy);
}
install_child_host_directives(el, scope_id, proxy, entry, template_name);
}
#[doc(hidden)]
pub fn install_static_for_plan(
el: &Element,
scope_id: ScopeId,
proxy: &JsValue,
entry: &'static StaticForPlan,
template_name: &str,
) {
let template = match directives::for_::ForTemplate::from_element(el.clone()) {
Some(t) => t,
None => {
fail(
"for-plan-template",
template_name,
entry.template_node_path,
Some(entry.items_expr),
);
return;
}
};
directives::for_::install(
template,
proxy.clone(),
scope_id,
entry.item_name,
entry.items_expr,
entry.key_expr,
entry.stagger_ms,
entry.body,
);
}
#[doc(hidden)]
pub fn install_static_teleport_plan(
el: &Element,
entry: &'static StaticTeleportPlan,
template_name: &str,
) {
let template = match el.clone().dyn_into::<web_sys::HtmlTemplateElement>() {
Ok(t) => t,
Err(_) => {
fail(
"teleport-plan-template",
template_name,
entry.template_node_path,
Some(entry.selector),
);
return;
}
};
directives::teleport::install(template, entry.selector, entry.body);
}
#[doc(hidden)]
pub fn install_static_if_plan(
el: &Element,
proxy: &JsValue,
entry: &'static StaticIfPlan,
template_name: &str,
) {
let template = match el.clone().dyn_into::<web_sys::HtmlTemplateElement>() {
Ok(t) => t,
Err(_) => {
fail(
"if-plan-template",
template_name,
entry.template_node_path,
Some(entry.expr_src),
);
return;
}
};
let Some(evaluator) = static_evaluator(entry.compiled, entry.expr_src) else {
fail(
"if-plan-parse",
template_name,
entry.template_node_path,
Some(entry.expr_src),
);
return;
};
directives::if_::install_eval(
template,
proxy.clone(),
evaluator,
entry.body,
entry.teleport_selector,
);
}
#[doc(hidden)]
pub fn capture_static_slot_outlet(
el: &Element,
entry: &'static StaticSlotOutlet,
template_name: &str,
) -> Option<Element> {
if el.local_name() != "slot" {
fail(
"slot-outlet-tag",
template_name,
entry.node_path,
Some(entry.name),
);
return None;
}
Some(el.clone())
}
#[doc(hidden)]
pub fn materialize_static_slot_outlet(el: &Element) {
crate::mount::materialize_compiled_slot_outlet(el);
}
#[doc(hidden)]
pub fn install_static_opaque_directive(
el: &Element,
scope_id: ScopeId,
proxy: &JsValue,
entry: &'static StaticOpaqueDirective,
template_name: &str,
) {
if !dispatch_opaque(
entry.name,
el,
entry.arg,
entry.modifiers,
entry.value,
scope_id,
proxy,
) {
fail(
"opaque-directive-lookup",
template_name,
entry.node_path,
Some(entry.name),
);
}
}
#[doc(hidden)]
pub fn capture_static_interp_target(
el: &Element,
entry: &'static StaticInterp,
_template_name: &str,
) -> Option<StaticInterpTarget> {
let target = directives::interp::resolve_text_target(el, entry.text_index as usize)?;
Some(StaticInterpTarget {
parent: el.clone(),
text: target,
segments: entry.segments,
})
}
#[doc(hidden)]
pub fn install_static_interp_target(target: &StaticInterpTarget, proxy: &JsValue) {
directives::interp::install_planned_target(
&target.parent,
proxy,
&target.text,
target.segments,
);
}
#[doc(hidden)]
pub fn install_static_native_model(
el: &Element,
proxy: &JsValue,
entry: &'static StaticNativeModel,
) {
directives::model::install_native(
el,
proxy,
entry.expr_src.to_string(),
entry.number,
entry.lazy,
);
}
fn dispatch_opaque(
name: &str,
el: &Element,
arg: Option<&str>,
modifiers: &'static [&'static str],
value: &str,
scope_id: ScopeId,
proxy: &JsValue,
) -> bool {
match name {
"resize" => directives::resize::install_opaque(el, arg, modifiers, value, scope_id, proxy),
"intersect" => {
directives::intersect::install_opaque(el, arg, modifiers, value, scope_id, proxy)
}
"anchor" => directives::anchor::install_opaque(el, arg, modifiers, value, scope_id, proxy),
"roving" => directives::roving::install_opaque(el, arg, modifiers, value, scope_id, proxy),
"flip" => directives::flip::install_opaque(el, arg, modifiers, value, scope_id, proxy),
_ => return false,
}
true
}
#[doc(hidden)]
pub fn apply_static_pp_as_plan(
root: &Element,
scope_id: ScopeId,
proxy: &JsValue,
plan: &'static StaticTemplatePlan,
template_name: &str,
) {
for r in plan.refs.iter().filter(|r| r.node_path.is_empty()) {
crate::refs::register(scope_id, r.name, root);
}
for b in plan.bindings.iter().filter(|b| b.node_path.is_empty()) {
let Some(evaluator) = static_evaluator(b.compiled, b.expr_src) else {
fail(
"pp-as-binding-parse",
template_name,
b.node_path,
Some(b.expr_src),
);
continue;
};
match b.kind {
BindingKind::Text => directives::text::install_eval(root, proxy, evaluator),
BindingKind::Html => directives::html::install_eval(root, proxy, evaluator),
BindingKind::Show => directives::show::install_eval(root, proxy, evaluator),
BindingKind::Bind { arg } => {
directives::bind::install_eval(root, proxy, arg, evaluator)
}
BindingKind::Class => {
fail(
"pp-as-binding-kind",
template_name,
b.node_path,
Some(b.expr_src),
);
}
}
}
for l in plan.listeners.iter().filter(|l| l.node_path.is_empty()) {
let ast = match expr::parse_cached(l.expr_src) {
Ok(a) => a,
Err(_) => {
fail(
"pp-as-listener-parse",
template_name,
l.node_path,
Some(l.expr_src),
);
continue;
}
};
directives::on::install(
root,
scope_id,
proxy,
l.event,
l.modifiers,
Rc::new(directives::on::backfill_legacy_call(ast)),
);
}
for d in plan
.opaque_directives
.iter()
.filter(|d| d.node_path.is_empty())
{
if !dispatch_opaque(d.name, root, d.arg, d.modifiers, d.value, scope_id, proxy) {
fail(
"opaque-directive-lookup",
template_name,
d.node_path,
Some(d.name),
);
}
}
}
fn install_child_host_directives(
el: &Element,
scope_id: ScopeId,
proxy: &JsValue,
child: &StaticChildMount,
template_name: &str,
) {
for b in child.bindings {
let Some(evaluator) = static_evaluator(b.compiled, b.expr_src) else {
fail(
"child-host-binding-parse",
template_name,
child.node_path,
Some(b.expr_src),
);
continue;
};
directives::bind::install_eval(el, proxy, b.arg, evaluator);
}
for l in child.listeners {
let ast = match expr::parse_cached(l.expr_src) {
Ok(a) => a,
Err(_) => {
fail(
"child-host-listener-parse",
template_name,
child.node_path,
Some(l.expr_src),
);
continue;
}
};
directives::on::install(
el,
scope_id,
proxy,
l.event,
l.modifiers,
Rc::new(directives::on::backfill_legacy_call(ast)),
);
}
for m in child.models {
let _ = scope_id;
directives::model::install_compiled(el, proxy, m.arg, m.modifiers, m.expr_src);
}
}
pub fn stamp_if_body_with(
html: &str,
scope_id: ScopeId,
proxy: &JsValue,
ctx_parent_id: ScopeId,
install_plan: impl FnOnce(&Element, ScopeId, &JsValue),
) -> Option<Element> {
let doc = web_sys::window().and_then(|w| w.document())?;
let (root, content_parent) = parse_body_fragment_root(&doc, html)?;
crate::mount::bind_borrowed_scope_to(&root, scope_id, proxy);
let ctx_key = JsValue::from_str(crate::mount::CTX_PARENT_KEY);
let ctx_val = JsValue::from_f64(ctx_parent_id.0 as f64);
let _ = js_sys::Reflect::set(root.as_ref(), &ctx_key, &ctx_val);
install_plan(&root, scope_id, proxy);
if root.parent_node().is_some() {
return Some(root);
}
let kids = content_parent.child_nodes();
for i in 0..kids.length() {
if let Some(n) = kids.item(i) {
if let Ok(el) = n.dyn_into::<Element>() {
return Some(el);
}
}
}
None
}
const SVG_NS: &str = "http://www.w3.org/2000/svg";
fn parse_body_fragment_root(doc: &web_sys::Document, html: &str) -> Option<(Element, Node)> {
if first_fragment_tag(html).is_some_and(is_svg_fragment_root_tag) {
let wrapper = doc.create_element_ns(Some(SVG_NS), "svg").ok()?;
wrapper.set_inner_html(html);
let root = first_element_child(wrapper.as_ref())?;
return Some((root, wrapper.into()));
}
let template_el = doc.create_element("template").ok()?;
template_el.set_inner_html(html);
let template_el = template_el
.dyn_into::<web_sys::HtmlTemplateElement>()
.ok()?;
let content = template_el.content();
let root = first_element_child(content.as_ref())?;
Some((root, content.into()))
}
fn first_element_child(parent: &Node) -> Option<Element> {
let kids = parent.child_nodes();
for i in 0..kids.length() {
if let Some(n) = kids.item(i) {
if let Ok(el) = n.dyn_into::<Element>() {
return Some(el);
}
}
}
None
}
fn first_fragment_tag(html: &str) -> Option<&str> {
let rest = html.trim_start().strip_prefix('<')?;
if rest.starts_with('!') || rest.starts_with('?') || rest.starts_with('/') {
return None;
}
let end = rest
.find(|c: char| c.is_ascii_whitespace() || c == '>' || c == '/')
.unwrap_or(rest.len());
let tag = &rest[..end];
(!tag.is_empty()).then_some(tag)
}
fn is_svg_fragment_root_tag(tag: &str) -> bool {
matches!(
tag.to_ascii_lowercase().as_str(),
"animate"
| "animatemotion"
| "animatetransform"
| "circle"
| "clippath"
| "defs"
| "desc"
| "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"
| "foreignobject"
| "g"
| "image"
| "line"
| "lineargradient"
| "marker"
| "mask"
| "metadata"
| "mpath"
| "path"
| "pattern"
| "polygon"
| "polyline"
| "radialgradient"
| "rect"
| "script"
| "set"
| "stop"
| "style"
| "svg"
| "switch"
| "symbol"
| "text"
| "textpath"
| "title"
| "tspan"
| "use"
| "view"
)
}
type StaticEvaluator = Rc<dyn Fn(&JsValue) -> JsValue>;
fn static_evaluator(
compiled: Option<&'static StaticExpr>,
expr_src: &'static str,
) -> Option<StaticEvaluator> {
if let Some(compiled) = compiled {
return Some(Rc::new(move |scope| compiled.evaluate(scope)));
}
runtime_evaluator(expr_src)
}
#[cfg(feature = "runtime-expr-fallback")]
fn runtime_evaluator(expr_src: &'static str) -> Option<StaticEvaluator> {
let ast = expr::parse_cached(expr_src).ok()?;
Some(Rc::new(move |scope| expr::evaluate(&ast, scope)))
}
#[cfg(not(feature = "runtime-expr-fallback"))]
fn runtime_evaluator(_expr_src: &'static str) -> Option<StaticEvaluator> {
None
}
fn fail(kind: &str, template_name: &str, node_path: &[u16], expr_src: Option<&str>) {
record_plan_failure();
let msg = match expr_src {
Some(src) => format!(
"pocopine: template plan {kind} install failed for `{template_name}` at \
node_path {node_path:?} (expr: {src:?}). This is a framework bug — the \
macro stripped a directive whose plan entry cannot deliver."
),
None => format!(
"pocopine: template plan {kind} install failed for `{template_name}` at \
node_path {node_path:?}. This is a framework bug — the macro stripped a \
directive whose plan entry cannot deliver."
),
};
if cfg!(debug_assertions) {
panic!("{msg}");
}
console::error_1(&JsValue::from_str(&msg));
}