use std::path::{Path, PathBuf};
use headless_chrome::{Browser, LaunchOptions};
const STRIP_HIDDEN_JS: &str = r#"
(() => {
const drop = [];
for (const el of document.querySelectorAll('html *')) {
if (!el.isConnected) continue;
if (getComputedStyle(el).display === 'none') drop.push(el);
}
for (const el of drop) if (el.isConnected) el.remove();
return '<!DOCTYPE html>\n' + document.documentElement.outerHTML;
})()
"#;
pub fn render_visible_html(html: &str) -> Result<String, String> {
let path = temp_html_path();
std::fs::write(&path, html).map_err(|e| format!("browser: writing temp page: {e}"))?;
let result = render_file(&path);
let _ = std::fs::remove_file(&path);
result
}
fn render_file(path: &Path) -> Result<String, String> {
let options = LaunchOptions::default_builder()
.headless(true)
.sandbox(false)
.path(locate_chrome())
.build()
.map_err(|e| format!("browser: launch options: {e}"))?;
let browser = Browser::new(options)
.map_err(|e| format!("browser: launch failed ({e}); is Chromium installed?"))?;
let tab = browser
.new_tab()
.map_err(|e| format!("browser: new tab: {e}"))?;
let url = format!("file://{}", path.display());
tab.navigate_to(&url)
.map_err(|e| format!("browser: navigate: {e}"))?;
tab.wait_until_navigated()
.map_err(|e| format!("browser: page load: {e}"))?;
let result = tab
.evaluate(STRIP_HIDDEN_JS, true)
.map_err(|e| format!("browser: evaluate: {e}"))?;
match result.value {
Some(serde_json::Value::String(s)) => Ok(s),
_ => Err("browser: page returned no HTML".into()),
}
}
fn locate_chrome() -> Option<PathBuf> {
for var in ["DOCLING_RS_CHROME", "CHROME"] {
if let Ok(p) = std::env::var(var) {
if !p.is_empty() {
return Some(PathBuf::from(p));
}
}
}
if let Ok(dir) = std::env::var("PLAYWRIGHT_BROWSERS_PATH") {
let p = PathBuf::from(dir).join("chromium");
if p.exists() {
return Some(p);
}
}
None
}
fn temp_html_path() -> PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let n = SEQ.fetch_add(1, Ordering::Relaxed);
let mut path = std::env::temp_dir();
path.push(format!(
"docling.rs-render-{}-{}.html",
std::process::id(),
n
));
path
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strips_stylesheet_hidden_elements() {
let html = "<html><head><style>.gone{display:none}</style></head><body>\
<div>SHOWN</div><div class=\"gone\">HIDDEN</div></body></html>";
let rendered = match render_visible_html(html) {
Ok(r) => r,
Err(_) => return, };
assert!(rendered.contains("SHOWN"));
assert!(
!rendered.contains("HIDDEN"),
"stylesheet-hidden element should be stripped: {rendered}"
);
}
}