diff --git a/crates/common/src/html_processor.rs b/crates/common/src/html_processor.rs index e3d827f..b354e8b 100644 --- a/crates/common/src/html_processor.rs +++ b/crates/common/src/html_processor.rs @@ -1,4 +1,4 @@ -//! Simplified HTML processor that combines URL replacement and Prebid injection +//! Simplified HTML processor that combines URL replacement and integration injection //! //! This module provides a `StreamProcessor` implementation for HTML content. use std::cell::Cell; @@ -191,10 +191,26 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso // Inject unified tsjs bundle once at the start of element!("head", { let injected_tsjs = injected_tsjs.clone(); + let integrations = integration_registry.clone(); + let patterns = patterns.clone(); + let document_state = document_state.clone(); move |el| { if !injected_tsjs.get() { - let loader = tsjs::unified_script_tag(); - el.prepend(&loader, ContentType::Html); + let mut snippet = String::new(); + let ctx = IntegrationHtmlContext { + request_host: &patterns.request_host, + request_scheme: &patterns.request_scheme, + origin_host: &patterns.origin_host, + document_state: &document_state, + }; + // First inject the unified TSJS bundle (defines tsjs.setConfig, etc.) + snippet.push_str(&tsjs::unified_script_tag()); + // Then add any integration-specific head inserts (e.g., mode config) + // These run after the bundle so tsjs API is available + for insert in integrations.head_inserts(&ctx) { + snippet.push_str(&insert); + } + el.prepend(&snippet, ContentType::Html); injected_tsjs.set(true); } Ok(()) diff --git a/crates/common/src/integrations/registry.rs b/crates/common/src/integrations/registry.rs index 7fe9e77..426b557 100644 --- a/crates/common/src/integrations/registry.rs +++ b/crates/common/src/integrations/registry.rs @@ -376,6 +376,14 @@ pub trait IntegrationHtmlPostProcessor: Send + Sync { fn post_process(&self, html: &mut String, ctx: &IntegrationHtmlContext<'_>) -> bool; } +/// Trait for integration-provided HTML head injections. +pub trait IntegrationHeadInjector: Send + Sync { + /// Identifier for logging/diagnostics. + fn integration_id(&self) -> &'static str; + /// Return HTML snippets to insert at the start of ``. + fn head_inserts(&self, ctx: &IntegrationHtmlContext<'_>) -> Vec; +} + /// Registration payload returned by integration builders. pub struct IntegrationRegistration { pub integration_id: &'static str, @@ -383,6 +391,7 @@ pub struct IntegrationRegistration { pub attribute_rewriters: Vec>, pub script_rewriters: Vec>, pub html_post_processors: Vec>, + pub head_injectors: Vec>, } impl IntegrationRegistration { @@ -405,6 +414,7 @@ impl IntegrationRegistrationBuilder { attribute_rewriters: Vec::new(), script_rewriters: Vec::new(), html_post_processors: Vec::new(), + head_injectors: Vec::new(), }, } } @@ -439,6 +449,12 @@ impl IntegrationRegistrationBuilder { self } + #[must_use] + pub fn with_head_injector(mut self, injector: Arc) -> Self { + self.registration.head_injectors.push(injector); + self + } + #[must_use] pub fn build(self) -> IntegrationRegistration { self.registration @@ -460,6 +476,7 @@ struct IntegrationRegistryInner { html_rewriters: Vec>, script_rewriters: Vec>, html_post_processors: Vec>, + head_injectors: Vec>, } impl Default for IntegrationRegistryInner { @@ -474,6 +491,7 @@ impl Default for IntegrationRegistryInner { html_rewriters: Vec::new(), script_rewriters: Vec::new(), html_post_processors: Vec::new(), + head_injectors: Vec::new(), } } } @@ -574,6 +592,9 @@ impl IntegrationRegistry { inner .html_post_processors .extend(registration.html_post_processors.into_iter()); + inner + .head_injectors + .extend(registration.head_injectors.into_iter()); } } @@ -662,6 +683,19 @@ impl IntegrationRegistry { self.inner.html_post_processors.clone() } + /// Collect HTML snippets for insertion at the start of ``. + #[must_use] + pub fn head_inserts(&self, ctx: &IntegrationHtmlContext<'_>) -> Vec { + let mut inserts = Vec::new(); + for injector in &self.inner.head_injectors { + let mut next = injector.head_inserts(ctx); + if !next.is_empty() { + inserts.append(&mut next); + } + } + inserts + } + /// Provide a snapshot of registered integrations and their hooks. #[must_use] pub fn registered_integrations(&self) -> Vec { @@ -711,6 +745,29 @@ impl IntegrationRegistry { html_rewriters: attribute_rewriters, script_rewriters, html_post_processors: Vec::new(), + head_injectors: Vec::new(), + }), + } + } + + #[cfg(test)] + pub fn from_rewriters_with_head_injectors( + attribute_rewriters: Vec>, + script_rewriters: Vec>, + head_injectors: Vec>, + ) -> Self { + Self { + inner: Arc::new(IntegrationRegistryInner { + get_router: Router::new(), + post_router: Router::new(), + put_router: Router::new(), + delete_router: Router::new(), + patch_router: Router::new(), + routes: Vec::new(), + html_rewriters: attribute_rewriters, + script_rewriters, + html_post_processors: Vec::new(), + head_injectors, }), } } @@ -765,6 +822,7 @@ impl IntegrationRegistry { html_rewriters: Vec::new(), script_rewriters: Vec::new(), html_post_processors: Vec::new(), + head_injectors: Vec::new(), }), } }