Skip to main content

pmcp_widget_utils/
lib.rs

1//! Shared widget utilities for PMCP SDK.
2//!
3//! Provides common HTML manipulation functions used by both the core `pmcp` crate
4//! and the `mcp-preview` crate, eliminating code duplication.
5
6/// Inject a bridge script tag into widget HTML.
7///
8/// Inserts `<script type="module" src="{bridge_url}"></script>` at the best
9/// location in the HTML document:
10///
11/// 1. Before `</head>` if present
12/// 2. After `<body>` opening tag if present
13/// 3. Prepended to the document otherwise
14///
15/// # Example
16///
17/// ```
18/// use pmcp_widget_utils::inject_bridge_script;
19///
20/// let html = r#"<html><head><title>Widget</title></head><body>Hello</body></html>"#;
21/// let result = inject_bridge_script(html, "/assets/widget-runtime.mjs");
22/// assert!(result.contains(r#"<script type="module" src="/assets/widget-runtime.mjs"></script>"#));
23/// ```
24pub fn inject_bridge_script(html: &str, bridge_url: &str) -> String {
25    let script_tag = format!(r#"<script type="module" src="{}"></script>"#, bridge_url);
26
27    if let Some(pos) = html.find("</head>") {
28        // Insert before </head>
29        let mut result = String::with_capacity(html.len() + script_tag.len() + 1);
30        result.push_str(&html[..pos]);
31        result.push_str(&script_tag);
32        result.push('\n');
33        result.push_str(&html[pos..]);
34        result
35    } else if let Some(pos) = html.find("<body") {
36        // Find the closing '>' of the <body> tag
37        if let Some(close) = html[pos..].find('>') {
38            let insert_at = pos + close + 1;
39            let mut result = String::with_capacity(html.len() + script_tag.len() + 1);
40            result.push_str(&html[..insert_at]);
41            result.push('\n');
42            result.push_str(&script_tag);
43            result.push_str(&html[insert_at..]);
44            result
45        } else {
46            format!("{}\n{}", script_tag, html)
47        }
48    } else {
49        // No </head> or <body> — prepend
50        format!("{}\n{}", script_tag, html)
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn injects_before_head_close() {
60        let html = "<html><head><title>T</title></head><body></body></html>";
61        let result = inject_bridge_script(html, "/bridge.mjs");
62        assert!(result.contains(r#"<script type="module" src="/bridge.mjs"></script>"#));
63        assert!(result.find("bridge.mjs").unwrap() < result.find("</head>").unwrap());
64    }
65
66    #[test]
67    fn injects_after_body_open_when_no_head() {
68        let html = "<body><div>Content</div></body>";
69        let result = inject_bridge_script(html, "/bridge.mjs");
70        assert!(result.contains(r#"<script type="module" src="/bridge.mjs"></script>"#));
71        assert!(result.find("bridge.mjs").unwrap() > result.find("<body>").unwrap());
72    }
73
74    #[test]
75    fn prepends_when_no_head_or_body() {
76        let html = "<div>Simple widget</div>";
77        let result = inject_bridge_script(html, "/bridge.mjs");
78        assert!(result.starts_with(r#"<script type="module" src="/bridge.mjs"></script>"#));
79    }
80}