use std::collections::HashMap;
use std::sync::Arc;
type ValidatorFn = Arc<dyn Fn(&serde_json::Value) -> bool + Send + Sync>;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ComponentId(pub String);
impl ComponentId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
}
impl std::fmt::Display for ComponentId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<&str> for ComponentId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
#[derive(Default)]
pub struct ComponentRegistry {
templates: HashMap<ComponentId, String>,
insertion_order: Vec<ComponentId>,
validators: HashMap<ComponentId, ValidatorFn>,
}
impl ComponentRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn with_standard_components() -> Self {
let mut registry = Self::new();
registry.register_standard_components();
registry
}
pub fn register_standard_components(&mut self) {
use crate::components::catalog::ComponentCatalog;
let descs: Vec<&'static crate::components::catalog::ComponentDescriptor> =
ComponentCatalog::all().collect();
let ids: Vec<&'static str> = descs.iter().map(|d| d.id).collect();
let references = |template: &str, candidate: &str| -> bool {
let bytes = template.as_bytes();
let needle = format!("{candidate}(");
let mut from = 0;
while let Some(pos) = template[from..].find(&needle) {
let abs = from + pos;
let ok_prefix = abs == 0
|| !matches!(bytes[abs - 1], b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_');
if ok_prefix {
return true;
}
from = abs + needle.len();
}
false
};
let mut indegree: HashMap<&'static str, usize> = ids.iter().map(|id| (*id, 0)).collect();
let mut dependents: HashMap<&'static str, Vec<&'static str>> = HashMap::new();
for d in &descs {
for dep in &ids {
if *dep != d.id && references(d.template, dep) {
dependents.entry(dep).or_default().push(d.id);
*indegree.get_mut(d.id).unwrap() += 1;
}
}
}
let desc_by_id: HashMap<&'static str, &'static crate::components::catalog::ComponentDescriptor> =
descs.iter().map(|d| (d.id, *d)).collect();
let mut ready: Vec<&'static str> = indegree
.iter()
.filter(|(_, °)| deg == 0)
.map(|(id, _)| *id)
.collect();
ready.sort_unstable_by(|a, b| b.cmp(a));
let mut emitted = 0usize;
while let Some(id) = ready.pop() {
if let Some(desc) = desc_by_id.get(id) {
self.register(ComponentId::new(desc.id), desc.template.to_string());
emitted += 1;
}
if let Some(children) = dependents.get(id) {
let mut newly_ready = Vec::new();
for child in children {
let deg = indegree.get_mut(child).unwrap();
*deg -= 1;
if *deg == 0 {
newly_ready.push(*child);
}
}
for c in newly_ready {
ready.push(c);
}
ready.sort_unstable_by(|a, b| b.cmp(a));
}
}
if emitted < descs.len() {
let mut remaining: Vec<&'static str> = descs
.iter()
.map(|d| d.id)
.filter(|id| !self.templates.contains_key(&ComponentId::new(*id)))
.collect();
remaining.sort_unstable();
for id in remaining {
if let Some(desc) = desc_by_id.get(id) {
self.register(ComponentId::new(desc.id), desc.template.to_string());
}
}
}
}
#[allow(dead_code)]
fn _register_standard_components_legacy(&mut self) {
self.register(
ComponentId::new("score-card"),
include_str!("../../templates/components/score_card.typ").to_string(),
);
self.register(
ComponentId::new("finding"),
include_str!("../../templates/components/finding.typ").to_string(),
);
self.register(
ComponentId::new("audit-table"),
include_str!("../../templates/components/audit_table.typ").to_string(),
);
self.register(
ComponentId::new("section"),
include_str!("../../templates/components/section.typ").to_string(),
);
self.register(
ComponentId::new("image"),
include_str!("../../templates/components/image.typ").to_string(),
);
self.register(
ComponentId::new("callout"),
include_str!("../../templates/components/callout.typ").to_string(),
);
self.register(
ComponentId::new("summary-box"),
include_str!("../../templates/components/summary_box.typ").to_string(),
);
self.register(
ComponentId::new("list"),
include_str!("../../templates/components/list.typ").to_string(),
);
self.register(
ComponentId::new("divider"),
include_str!("../../templates/components/divider.typ").to_string(),
);
self.register(
ComponentId::new("progress-bar"),
include_str!("../../templates/components/progress_bar.typ").to_string(),
);
self.register(
ComponentId::new("key-value-list"),
include_str!("../../templates/components/key_value_list.typ").to_string(),
);
self.register(
ComponentId::new("watermark"),
include_str!("../../templates/components/watermark.typ").to_string(),
);
self.register(
ComponentId::new("page-break"),
include_str!("../../templates/components/page_break.typ").to_string(),
);
self.register(
ComponentId::new("chart"),
include_str!("../../templates/components/chart.typ").to_string(),
);
self.register(
ComponentId::new("sparkline"),
include_str!("../../templates/components/sparkline.typ").to_string(),
);
self.register(
ComponentId::new("gauge"),
include_str!("../../templates/components/gauge.typ").to_string(),
);
self.register(
ComponentId::new("crosstab"),
include_str!("../../templates/components/crosstab.typ").to_string(),
);
self.register(
ComponentId::new("pivot-table"),
include_str!("../../templates/components/pivot_table.typ").to_string(),
);
self.register(
ComponentId::new("barcode"),
include_str!("../../templates/components/barcode.typ").to_string(),
);
self.register(
ComponentId::new("label"),
include_str!("../../templates/components/label.typ").to_string(),
);
self.register(
ComponentId::new("textblock"),
include_str!("../../templates/components/text.typ").to_string(),
);
self.register(
ComponentId::new("number-field"),
include_str!("../../templates/components/number_field.typ").to_string(),
);
self.register(
ComponentId::new("date-field"),
include_str!("../../templates/components/date_field.typ").to_string(),
);
self.register(
ComponentId::new("resource-field"),
include_str!("../../templates/components/resource_field.typ").to_string(),
);
self.register(
ComponentId::new("side-label"),
include_str!("../../templates/components/side_label.typ").to_string(),
);
self.register(
ComponentId::new("table-of-contents"),
include_str!("../../templates/components/table_of_contents.typ").to_string(),
);
self.register(
ComponentId::new("metric-card"),
include_str!("../../templates/components/metric_card.typ").to_string(),
);
self.register(
ComponentId::new("hero-summary"),
include_str!("../../templates/components/hero_summary.typ").to_string(),
);
self.register(
ComponentId::new("product-hero"),
include_str!("../../templates/components/product_hero.typ").to_string(),
);
self.register(
ComponentId::new("card-dashboard"),
include_str!("../../templates/components/card_dashboard.typ").to_string(),
);
self.register(
ComponentId::new("roadmap-block"),
include_str!("../../templates/components/roadmap_block.typ").to_string(),
);
self.register(
ComponentId::new("severity-overview"),
include_str!("../../templates/components/severity_overview.typ").to_string(),
);
self.register(
ComponentId::new("cover-page"),
include_str!("../../templates/components/cover_page.typ").to_string(),
);
self.register(
ComponentId::new("module-comparison"),
include_str!("../../templates/components/module_comparison.typ").to_string(),
);
self.register(
ComponentId::new("portfolio-summary"),
include_str!("../../templates/components/portfolio_summary.typ").to_string(),
);
self.register(
ComponentId::new("benchmark-table"),
include_str!("../../templates/components/benchmark_table.typ").to_string(),
);
self.register(
ComponentId::new("eyebrow"),
include_str!("../../templates/components/eyebrow.typ").to_string(),
);
self.register(
ComponentId::new("status-pill"),
include_str!("../../templates/components/status_pill.typ").to_string(),
);
self.register(
ComponentId::new("tag-cloud"),
include_str!("../../templates/components/tag_cloud.typ").to_string(),
);
self.register(
ComponentId::new("stat"),
include_str!("../../templates/components/stat.typ").to_string(),
);
self.register(
ComponentId::new("stat-pair"),
include_str!("../../templates/components/stat_pair.typ").to_string(),
);
self.register(
ComponentId::new("score-band"),
include_str!("../../templates/components/score_band.typ").to_string(),
);
self.register(
ComponentId::new("trend-tile"),
include_str!("../../templates/components/trend_tile.typ").to_string(),
);
self.register(
ComponentId::new("section-header-split"),
include_str!("../../templates/components/section_header_split.typ").to_string(),
);
self.register(
ComponentId::new("phase-block"),
include_str!("../../templates/components/phase_block.typ").to_string(),
);
self.register(
ComponentId::new("checklist-panel"),
include_str!("../../templates/components/checklist_panel.typ").to_string(),
);
self.register(
ComponentId::new("metric-strip"),
include_str!("../../templates/components/metric_strip.typ").to_string(),
);
self.register(
ComponentId::new("impact-grid"),
include_str!("../../templates/components/impact_grid.typ").to_string(),
);
self.register(
ComponentId::new("spotlight-card"),
include_str!("../../templates/components/spotlight_card.typ").to_string(),
);
self.register(
ComponentId::new("comparison-block"),
include_str!("../../templates/components/comparison_block.typ").to_string(),
);
self.register(
ComponentId::new("comparison-cluster"),
include_str!("../../templates/components/comparison_cluster.typ").to_string(),
);
self.register(
ComponentId::new("feature-grid"),
include_str!("../../templates/components/feature_grid.typ").to_string(),
);
self.register(
ComponentId::new("cta-box"),
include_str!("../../templates/components/cta_box.typ").to_string(),
);
self.register(
ComponentId::new("testimonial"),
include_str!("../../templates/components/testimonial.typ").to_string(),
);
self.register(
ComponentId::new("process-flow"),
include_str!("../../templates/components/process_flow.typ").to_string(),
);
self.register(
ComponentId::new("timeline"),
include_str!("../../templates/components/timeline.typ").to_string(),
);
self.register(
ComponentId::new("funnel"),
include_str!("../../templates/components/funnel.typ").to_string(),
);
self.register(
ComponentId::new("problem-solution"),
include_str!("../../templates/components/problem_solution.typ").to_string(),
);
self.register(
ComponentId::new("before-after"),
include_str!("../../templates/components/before_after.typ").to_string(),
);
self.register(
ComponentId::new("why-it-matters"),
include_str!("../../templates/components/why_it_matters.typ").to_string(),
);
self.register(
ComponentId::new("fact-box"),
include_str!("../../templates/components/fact_box.typ").to_string(),
);
self.register(
ComponentId::new("quote-block"),
include_str!("../../templates/components/quote_block.typ").to_string(),
);
self.register(
ComponentId::new("benefit-strip"),
include_str!("../../templates/components/benefit_strip.typ").to_string(),
);
self.register(
ComponentId::new("pricing-card"),
include_str!("../../templates/components/pricing_card.typ").to_string(),
);
self.register(
ComponentId::new("recommendation-card"),
include_str!("../../templates/components/recommendation_card.typ").to_string(),
);
self.register(
ComponentId::new("step-card-row"),
include_str!("../../templates/components/step_card_row.typ").to_string(),
);
self.register(
ComponentId::new("columns"),
include_str!("../../templates/components/columns.typ").to_string(),
);
self.register(
ComponentId::new("device-preview"),
include_str!("../../templates/components/device_preview.typ").to_string(),
);
self.register(
ComponentId::new("faq-list"),
include_str!("../../templates/components/faq_list.typ").to_string(),
);
self.register(
ComponentId::new("use-case-card"),
include_str!("../../templates/components/use_case_card.typ").to_string(),
);
self.register(
ComponentId::new("logo-strip"),
include_str!("../../templates/components/logo_strip.typ").to_string(),
);
self.register(
ComponentId::new("pull-quote"),
include_str!("../../templates/components/pull_quote.typ").to_string(),
);
self.register(
ComponentId::new("big-number"),
include_str!("../../templates/components/big_number.typ").to_string(),
);
self.register(
ComponentId::new("glossary-list"),
include_str!("../../templates/components/glossary_list.typ").to_string(),
);
self.register(
ComponentId::new("diagnosis-panel"),
include_str!("../../templates/components/diagnosis_panel.typ").to_string(),
);
self.register(
ComponentId::new("dominant-issue-spotlight"),
include_str!("../../templates/components/dominant_issue_spotlight.typ").to_string(),
);
self.register(
ComponentId::new("wrong-right-block"),
include_str!("../../templates/components/wrong_right_block.typ").to_string(),
);
self.register(
ComponentId::new("grid-component"),
include_str!("../../templates/components/grid.typ").to_string(),
);
self.register(
ComponentId::new("flow-group"),
include_str!("../../templates/components/flow_group.typ").to_string(),
);
}
pub fn register(&mut self, id: ComponentId, template: String) {
if !self.templates.contains_key(&id) {
self.insertion_order.push(id.clone());
}
self.templates.insert(id, template);
}
pub fn register_with_validator(
&mut self,
id: ComponentId,
template: String,
validator: impl Fn(&serde_json::Value) -> bool + Send + Sync + 'static,
) {
self.templates.insert(id.clone(), template);
self.validators.insert(id, Arc::new(validator));
}
pub fn get_template(&self, id: &ComponentId) -> Option<&String> {
self.templates.get(id)
}
pub fn has_component(&self, id: &ComponentId) -> bool {
self.templates.contains_key(id)
}
pub fn validate(&self, id: &ComponentId, data: &serde_json::Value) -> bool {
self.validators.get(id).map(|v| v(data)).unwrap_or(true)
}
pub fn list_components(&self) -> Vec<&ComponentId> {
self.insertion_order.iter().collect()
}
}
impl std::fmt::Debug for ComponentRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ComponentRegistry")
.field("templates", &self.templates.keys().collect::<Vec<_>>())
.field("validators_count", &self.validators.len())
.finish()
}
}