Fix icon rendering, calc()/var() length resolution, and approximate CSS float

Icons were rendering as either overlapping solid squares or not at all,
  text ran under the infobox instead of beside it, and general.css parsing
  had two more silent-corruption bugs on top of the ones fixed earlier:

  - A CSS declaration list was split on every literal semicolon, so any
    value containing one inside a quoted string or url() (a data: URI
    with a MIME parameter, e.g. url(data:image/svg+xml;utf8,<svg...>),
    extremely common for small inline icon SVGs) got torn into two
    malformed halves and silently dropped. This is why mask-image never
    applied to the TOC toggle icons at all.
  - data: URIs were only decoded as base64. Inline SVGs are just as
    commonly written as data:image/svg+xml;utf8,<percent-escaped text>,
    which needs percent-decoding instead; added that path.
  - top/left/right/bottom/width/height only handled bare lengths and a
    flat calc(a +/- b). Real values nest var(--x, fallback), calc(), and
    min()/max() inside each other (Wikipedia's icon offset math is
    literally calc(-1 * 22px / 2), and its icon sizing is
    calc(max(calc(var(--font-size-medium,1rem) + 4px),10px))). Replaced
    the flat evaluator with a small recursive resolver that handles all
    three, nested arbitrarily. This is what was placing the TOC toggle
    button's icon on top of the section text instead of in the left
    margin where it belongs.

  Added actual feature support once the parsing was correct:

  - mask-image/-webkit-mask-image: the pattern behind most scalable
    colored icons on the modern web (an SVG or image used purely as an
    alpha stencil, tinted by background-color/color). Previously every
    masked icon rendered as an unmasked solid-color square.
  - float: left/right, approximated without a full per-line reflow
    engine (which taffy's block/flex/grid modes don't provide): a
    floated element is grouped into a row alongside the flow siblings
    that follow it, up to whichever comes first of the next heading or
    another float, then everything from there on reverts to normal
    full-width flow. This isn't general CSS float, but it directly
    targets the overwhelmingly common real shape (an infobox or image
    floated near the top of an article, with the rest of the content
    starting at the next section heading), and is what makes article
    text actually wrap beside an infobox instead of running underneath
    it.

  Refactored paint.rs's image fetch/decode/cache logic into shared
  helpers (fetch_bytes, raster_to_pixmap) reused by plain <img> painting,
  background-image, and the new mask-image path.
This commit is contained in:
SGavrl committed 2026-07-03 12:34:27 +00:00
1 parent 920977776c
commit ca4a4ef135
4 files changed
+530 -87

No files matched your search

+100 -5
View File
@@ -450,11 +450,13 @@ fn build(
taffy_style.flex_wrap = taffy::FlexWrap::Wrap;
}
let child_ids: Vec<taffy::NodeId> = tree
.children(id)
.into_iter()
.filter_map(|cid| build(tree, cid, taffy_tree, id_map, styles))
.collect();
let dom_children = tree.children(id);
let has_float_child = dom_children.iter().any(|&cid| styles.get(&cid).map(|s| s.float.is_some()).unwrap_or(false));
let child_ids: Vec<taffy::NodeId> = if has_float_child {
build_children_with_float_zone(tree, &dom_children, taffy_tree, id_map, styles)
} else {
dom_children.into_iter().filter_map(|cid| build(tree, cid, taffy_tree, id_map, styles)).collect()
};
let taffy_id = if child_ids.is_empty() {
taffy_tree.new_leaf(taffy_style).ok()?
@@ -465,6 +467,99 @@ fn build(
Some(taffy_id)
}
/// Approximate `float: left|right` without real per-line reflow (which
/// taffy's block/flex/grid modes do not provide): place the float alongside
/// the flow siblings that follow it, up to whichever comes first of the next
/// heading or another floated element, then let everything from there on
/// revert to normal full-width flow.
///
/// This is not a general CSS float implementation (a float taller than its
/// flow zone, or one that should keep affecting content past a heading,
/// won't reflow correctly), but it directly targets the overwhelmingly
/// common real-world shape: a floated image or infobox near the top of an
/// article, sitting beside the intro text, with the rest of the content
/// (starting at the next section heading) running full width beneath it.
fn build_children_with_float_zone(
tree: &DomTree,
dom_children: &[NodeId],
taffy_tree: &mut TaffyTree,
id_map: &mut HashMap<taffy::NodeId, NodeId>,
styles: &HashMap<NodeId, crate::LayoutStyle>,
) -> Vec<taffy::NodeId> {
let is_float = |cid: NodeId| styles.get(&cid).map(|s| s.float.is_some()).unwrap_or(false);
let is_heading = |cid: NodeId| {
tree.get_node(cid)
.and_then(|n| n.as_element().map(|e| e.local.to_string()))
.map(|local| matches!(local.as_str(), "h1" | "h2" | "h3" | "h4" | "h5" | "h6"))
.unwrap_or(false)
};
let Some(float_idx) = dom_children.iter().position(|&cid| is_float(cid)) else {
return dom_children.iter().filter_map(|&cid| build(tree, cid, taffy_tree, id_map, styles)).collect();
};
let mut result: Vec<taffy::NodeId> = dom_children[..float_idx]
.iter()
.filter_map(|&cid| build(tree, cid, taffy_tree, id_map, styles))
.collect();
let float_side = styles.get(&dom_children[float_idx]).and_then(|s| s.float);
let mut zone_end = float_idx + 1;
while zone_end < dom_children.len() && !is_heading(dom_children[zone_end]) && !is_float(dom_children[zone_end]) {
zone_end += 1;
}
let float_taffy = build(tree, dom_children[float_idx], taffy_tree, id_map, styles);
let flow_taffy: Vec<taffy::NodeId> = dom_children[float_idx + 1..zone_end]
.iter()
.filter_map(|&cid| build(tree, cid, taffy_tree, id_map, styles))
.collect();
match float_taffy {
Some(float_id) => {
let flow_column_style = taffy::Style {
display: taffy::style::Display::Flex,
flex_direction: taffy::FlexDirection::Column,
flex_grow: 1.0,
flex_shrink: 1.0,
flex_basis: taffy::Dimension::Length(0.0),
min_size: taffy::Size { width: taffy::Dimension::Length(0.0), height: taffy::Dimension::Auto },
..Default::default()
};
let flow_column = if flow_taffy.is_empty() {
taffy_tree.new_leaf(flow_column_style).ok()
} else {
taffy_tree.new_with_children(flow_column_style, &flow_taffy).ok()
};
let row_style = taffy::Style {
display: taffy::style::Display::Flex,
flex_direction: taffy::FlexDirection::Row,
align_items: Some(taffy::AlignItems::FlexStart),
..Default::default()
};
let row_children: Vec<taffy::NodeId> = match float_side {
Some(crate::Float::Left) => [Some(float_id), flow_column].into_iter().flatten().collect(),
_ => [flow_column, Some(float_id)].into_iter().flatten().collect(),
};
if let Ok(row) = taffy_tree.new_with_children(row_style, &row_children) {
result.push(row);
}
}
// The float itself failed to build (e.g. display:none resolved for
// it specifically); still build its flow siblings so their content
// is not silently lost.
None => result.extend(flow_taffy),
}
result.extend(
dom_children[zone_end..]
.iter()
.filter_map(|&cid| build(tree, cid, taffy_tree, id_map, styles)),
);
result
}
#[cfg(test)]
mod tests {
use super::*;