Replace linear scan in CSS class dedup with a HashMap lookup
Export::class() deduplicated declaration blocks with classes.iter().position(), an O(n) scan per style, making exports with many distinct styles (e.g. large palettes) quadratic overall. Track the 1-based class number per declaration in a HashMap alongside the first-use-ordered Vec, so each lookup is O(1) while output stays byte-identical. Also adds a regression test covering 256 distinct declarations with recurrence of early styles, for both InternalCss and StyleEntities styling.
This commit is contained in:
parent
2c01a42b35
commit
3af08505f8
|
|
@ -50,6 +50,7 @@ fn write(doc: &Document, opts: &ExportOptions, native: Option<&[u8]>, id_prefix:
|
|||
body: String::new(),
|
||||
defs: String::new(),
|
||||
classes: Vec::new(),
|
||||
class_ids: HashMap::new(),
|
||||
used_ids: HashSet::new(),
|
||||
names: HashMap::new(),
|
||||
depth: 1,
|
||||
|
|
@ -259,8 +260,10 @@ struct Writer<'a> {
|
|||
xf: Affine,
|
||||
body: String,
|
||||
defs: String,
|
||||
/// Unique CSS declaration blocks; class name is `cls-{index + 1}`.
|
||||
/// Unique CSS declaration blocks, in first-use order (used for deterministic output).
|
||||
classes: Vec<String>,
|
||||
/// 1-based class/entity number by declaration; avoids a linear scan for each style.
|
||||
class_ids: HashMap<String, usize>,
|
||||
used_ids: HashSet<String>,
|
||||
names: HashMap<NodeId, String>,
|
||||
depth: usize,
|
||||
|
|
@ -657,13 +660,13 @@ impl Writer<'_> {
|
|||
|
||||
/// The 1-based number of a distinct declaration block (a CSS class or a style entity).
|
||||
fn class(&mut self, decl: String) -> usize {
|
||||
match self.classes.iter().position(|c| *c == decl) {
|
||||
Some(i) => i + 1,
|
||||
None => {
|
||||
self.classes.push(decl);
|
||||
self.classes.len()
|
||||
}
|
||||
if let Some(&i) = self.class_ids.get(decl.as_str()) {
|
||||
return i;
|
||||
}
|
||||
let i = self.classes.len() + 1;
|
||||
self.class_ids.insert(decl.clone(), i);
|
||||
self.classes.push(decl);
|
||||
i
|
||||
}
|
||||
|
||||
/// `<metadata>`: the Dublin Core terms of File Info ([`ExportOptions::metadata`]) and the
|
||||
|
|
|
|||
|
|
@ -29,6 +29,33 @@ fn doc() -> Document {
|
|||
d
|
||||
}
|
||||
|
||||
/// A large palette should create one class per distinct declaration, preserving first-use
|
||||
/// numbers even when earlier styles recur after hundreds of others.
|
||||
#[test]
|
||||
fn many_css_styles_preserve_first_use_numbers() {
|
||||
let mut d = Document::new(300.0, 10.0);
|
||||
let layer = d.layers[0].id;
|
||||
let colors = (0u16..256).chain([0, 128, 255]);
|
||||
for (x, color) in colors.enumerate() {
|
||||
let x = x as f64;
|
||||
let appearance = Appearance::basic(Paint::solid(Color::rgb8(color as u8, 31, 63)), Paint::None, 0.0);
|
||||
let n = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(x, 0.0, x + 0.5, 1.0)), appearance);
|
||||
d.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
}
|
||||
|
||||
for (styling, definition_marker) in [(Styling::InternalCss, ".cls-"), (Styling::StyleEntities, "<!ENTITY st")] {
|
||||
let svg = export(&d, &ExportOptions { styling, ..Default::default() });
|
||||
let paths: Vec<_> = svg.lines().filter(|line| line.contains("<path ")).collect();
|
||||
assert_eq!(paths.len(), 259, "{styling:?}");
|
||||
for (i, path) in paths.iter().enumerate() {
|
||||
let number = if i < 256 { i + 1 } else { [1, 129, 256][i - 256] };
|
||||
let expected = if styling == Styling::InternalCss { format!("class=\"cls-{number}\"") } else { format!("style=\"&st{number};\"") };
|
||||
assert!(path.contains(&expected), "path {i} should reference {expected}: {path}");
|
||||
}
|
||||
assert_eq!(svg.matches(definition_marker).count(), 256, "{styling:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_combination_reads_back() {
|
||||
let d = doc();
|
||||
|
|
|
|||
Loading…
Reference in New Issue