Fix render engine: CSS grid, cascade correctness, SVG, inline flow
The render layer had no CSS Grid support, so Vector 2022's page layout
(three nested grids) collapsed to a vertical stack with the article
landing tens of thousands of pixels down the page. Wikipedia rendered
as a near-blank page and Hacker News only worked through per-site
hardcoded CSS overrides.
Removed the HN-specific hardcoding and fixed the general engine instead:
- CSS Grid layout (grid-template-areas/columns/rows via taffy's grid mode)
- Indexed, specificity-correct cascade in a new css.rs, replacing the
old test-every-selector-against-every-node scan. Cascade time on
Wikipedia went from 9.5s to under 200ms.
- Ancestor bloom-filter fast-rejection for selector matching in
obscura-dom, needed to make the indexed cascade actually fast on
descendant combinators.
- :link/:visited pseudo-classes were previously unparseable, so any
rule using them (a:link{color:...}, extremely common) silently
dropped the whole rule from the cascade.
- overflow:hidden clipping, fixing the pervasive visually-hidden-but-
accessible pattern used for skip links and screen-reader-only labels.
- @media parser robustness: it did a literal substring search for
max-width: and silently dropped the constraint on max-width : 750px
(space before colon) or max-width:calc(750px - 1px), treating
mobile-only or narrow-viewport rules as always active. The calc()
case was hiding Wikipedia's TOC sidebar via a rule meant only for
narrow screens.
- Table auto-layout approximation: the trailing auto-width td in a row
absorbs leftover space, so a width:100% table's unsized last column
reaches the right edge instead of shrinking to content.
- Inline formatting context approximation: a block holding inline-level
children (a paragraph or list item with links) is promoted to a
wrapping flex row so text and links read as continuous prose instead
of one fragment per line.
- Whitespace-only text nodes no longer get a full line-height box. They
used to stack up as blank rows, turning Wikipedia's collapsed TOC
subsections and the page header into hundreds of pixels of dead space.
- SVG rendering via resvg/usvg, plus resolving relative image URLs
against the page's base URL.
- background-image support including background-size/background-position
for correctly sized and positioned icons.
- Per-screenshot image fetch cache.
- Fixed the layout-only (no paint feature) build, which didn't compile
due to an unconditional call into paint-gated code.
This commit is contained in:
1 parent
cd1ee2dcc5
commit
920977776c
12 files changed
+1949
-225
No files matched your search
@@ -385,6 +385,78 @@ impl Page {
|
||||
None => Some(doc_url.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_stylesheets(&mut self) {
|
||||
let all_links = match &self.js {
|
||||
Some(js) => {
|
||||
js.with_dom(|dom| {
|
||||
let link_ids = dom.query_selector_all("link[rel=\"stylesheet\"]").unwrap_or_default();
|
||||
let mut links = Vec::new();
|
||||
for lid in link_ids {
|
||||
if let Some(node) = dom.get_node(lid) {
|
||||
if let Some(href) = node.get_attribute("href") {
|
||||
links.push(href.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
links
|
||||
}).unwrap_or_default()
|
||||
}
|
||||
None => {
|
||||
tracing::info!("fetch_stylesheets: no js runtime");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
tracing::info!("fetch_stylesheets: found {} stylesheet links", all_links.len());
|
||||
|
||||
let document_base = self.resolve_base_url();
|
||||
|
||||
let mut fetch_tasks = Vec::new();
|
||||
for href in all_links {
|
||||
let full_url = if href.starts_with("http://") || href.starts_with("https://") {
|
||||
href.clone()
|
||||
} else if let Some(base) = &document_base {
|
||||
base.join(&href).map(|u| u.to_string()).unwrap_or_else(|_| href.clone())
|
||||
} else {
|
||||
href.clone()
|
||||
};
|
||||
tracing::info!("fetch_stylesheets: fetching {}", full_url);
|
||||
|
||||
let client = self.http_client.clone();
|
||||
fetch_tasks.push(tokio::spawn(async move {
|
||||
if let Ok(url) = url::Url::parse(&full_url) {
|
||||
if let Ok(Ok(resp)) = tokio::time::timeout(tokio::time::Duration::from_secs(5), client.fetch(&url)).await {
|
||||
if let Ok(css) = String::from_utf8(resp.body) {
|
||||
return Some(css);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}));
|
||||
}
|
||||
|
||||
let mut all_css = String::new();
|
||||
for task in fetch_tasks {
|
||||
if let Ok(Some(css)) = task.await {
|
||||
tracing::info!("fetch_stylesheets: fetched {} bytes of CSS", css.len());
|
||||
all_css.push_str(&css);
|
||||
all_css.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
if !all_css.is_empty() {
|
||||
tracing::info!("fetch_stylesheets: injecting {} bytes of CSS", all_css.len());
|
||||
if let Some(js) = &mut self.js {
|
||||
let escaped_css = all_css.replace('\\', "\\\\").replace('`', "\\`").replace('$', "\\$");
|
||||
let code = format!(
|
||||
"var style = document.createElement('style'); style.textContent = `{}`; document.head.appendChild(style);",
|
||||
escaped_css
|
||||
);
|
||||
let _ = js.execute_script("<fetch_stylesheets>", &code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_scripts(&mut self) {
|
||||
tracing::info!("execute_scripts called, js runtime exists: {}", self.js.is_some());
|
||||
@@ -1180,6 +1252,7 @@ impl Page {
|
||||
// listeners never registered, frameworks never bootstrapped,
|
||||
// page.click() handlers were no-ops. Now scripts run regardless
|
||||
// of waitUntil and DCL means "DOM parsed AND scripts executed".
|
||||
self.fetch_stylesheets().await;
|
||||
self.execute_scripts().await;
|
||||
|
||||
self.lifecycle = LifecycleState::DomContentLoaded;
|
||||
@@ -1286,7 +1359,11 @@ impl Page {
|
||||
/// viewport is zero-sized.
|
||||
#[cfg(feature = "render")]
|
||||
pub fn screenshot(&self, viewport: (f32, f32)) -> Option<Vec<u8>> {
|
||||
self.with_dom(|dom| obscura_js::screenshot_png(dom, viewport)).flatten()
|
||||
// Needed to resolve the relative image URLs ("logo.svg") that make up
|
||||
// the overwhelming majority of real markup.
|
||||
let base_url = self.resolve_base_url();
|
||||
let base_url = base_url.as_ref().map(|u| u.as_str());
|
||||
self.with_dom(|dom| obscura_js::screenshot_png(dom, viewport, base_url)).flatten()
|
||||
}
|
||||
|
||||
/// Absolute URLs the page pulled in via fetch()/XHR (issue #301). Empty
|
||||
|
||||
Reference in new issue
Block a user