Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions crates/common/src/html_processor.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -191,10 +191,26 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso
// Inject unified tsjs bundle once at the start of <head>
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(())
Expand Down
58 changes: 58 additions & 0 deletions crates/common/src/integrations/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,13 +376,22 @@ 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 `<head>`.
fn head_inserts(&self, ctx: &IntegrationHtmlContext<'_>) -> Vec<String>;
}

/// Registration payload returned by integration builders.
pub struct IntegrationRegistration {
pub integration_id: &'static str,
pub proxies: Vec<Arc<dyn IntegrationProxy>>,
pub attribute_rewriters: Vec<Arc<dyn IntegrationAttributeRewriter>>,
pub script_rewriters: Vec<Arc<dyn IntegrationScriptRewriter>>,
pub html_post_processors: Vec<Arc<dyn IntegrationHtmlPostProcessor>>,
pub head_injectors: Vec<Arc<dyn IntegrationHeadInjector>>,
}

impl IntegrationRegistration {
Expand All @@ -405,6 +414,7 @@ impl IntegrationRegistrationBuilder {
attribute_rewriters: Vec::new(),
script_rewriters: Vec::new(),
html_post_processors: Vec::new(),
head_injectors: Vec::new(),
},
}
}
Expand Down Expand Up @@ -439,6 +449,12 @@ impl IntegrationRegistrationBuilder {
self
}

#[must_use]
pub fn with_head_injector(mut self, injector: Arc<dyn IntegrationHeadInjector>) -> Self {
self.registration.head_injectors.push(injector);
self
}

#[must_use]
pub fn build(self) -> IntegrationRegistration {
self.registration
Expand All @@ -460,6 +476,7 @@ struct IntegrationRegistryInner {
html_rewriters: Vec<Arc<dyn IntegrationAttributeRewriter>>,
script_rewriters: Vec<Arc<dyn IntegrationScriptRewriter>>,
html_post_processors: Vec<Arc<dyn IntegrationHtmlPostProcessor>>,
head_injectors: Vec<Arc<dyn IntegrationHeadInjector>>,
}

impl Default for IntegrationRegistryInner {
Expand All @@ -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(),
}
}
}
Expand Down Expand Up @@ -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());
}
}

Expand Down Expand Up @@ -662,6 +683,19 @@ impl IntegrationRegistry {
self.inner.html_post_processors.clone()
}

/// Collect HTML snippets for insertion at the start of `<head>`.
#[must_use]
pub fn head_inserts(&self, ctx: &IntegrationHtmlContext<'_>) -> Vec<String> {
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<IntegrationMetadata> {
Expand Down Expand Up @@ -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<Arc<dyn IntegrationAttributeRewriter>>,
script_rewriters: Vec<Arc<dyn IntegrationScriptRewriter>>,
head_injectors: Vec<Arc<dyn IntegrationHeadInjector>>,
) -> 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,
}),
}
}
Expand Down Expand Up @@ -765,6 +822,7 @@ impl IntegrationRegistry {
html_rewriters: Vec::new(),
script_rewriters: Vec::new(),
html_post_processors: Vec::new(),
head_injectors: Vec::new(),
}),
}
}
Expand Down