Compare commits

..
4 changed files with 36 additions and 18 deletions
+1 -12
View File
@@ -390,7 +390,6 @@ pub fn pdf_options(p: &Value, count: usize) -> Result<deckcraft_pdf::PdfOptions>
pub fn parse_slide_range(text: &str, count: usize) -> Result<Vec<usize>> {
let invalid = || bad("file.export", format!("“{text}” isn't a valid slide range: use numbers and ranges such as 1-3, 5"));
let mut out: Vec<usize> = vec![];
let mut seen: std::collections::HashSet<usize> = std::collections::HashSet::new();
for part in text.split([',', ';']).map(str::trim).filter(|p| !p.is_empty()) {
let (a, b) = match part.split_once('-') {
Some((a, b)) => (a.trim(), b.trim()),
@@ -406,7 +405,7 @@ pub fn parse_slide_range(text: &str, count: usize) -> Result<Vec<usize>> {
return Err(bad("file.export", format!("the presentation has {count} slides; “{part}” is outside them")));
}
for i in lo - 1..hi {
if seen.insert(i) {
if !out.contains(&i) {
out.push(i);
}
}
@@ -770,16 +769,6 @@ mod tests {
use super::{parse_slide_range, write_file};
#[test]
fn broad_and_repeated_slide_ranges_expand_once() {
// 200 copies of a 100,000-slide range: each index once, in order, without a scan per index.
let text = vec!["1-100000"; 200].join(",");
let v = parse_slide_range(&text, 100_000).unwrap();
assert_eq!(v.len(), 100_000);
assert!(v.windows(2).all(|w| w[0] < w[1]));
assert_eq!(parse_slide_range("3, 1-3, 2", 3).unwrap(), vec![2, 0, 1]);
}
#[test]
fn slide_range_rejects_plus_sign() {
assert!(parse_slide_range("+2", 3).is_err());
+1 -1
View File
@@ -367,7 +367,7 @@ fn spelling(s: &mut Session, p: &Value) -> Result<Value> {
}
out.push(v);
}
let rep: std::collections::HashSet<usize> = crate::spell::repeated(para, &default).iter().map(|w| w.start).collect();
let rep: Vec<usize> = crate::spell::repeated(para, &default).iter().map(|w| w.start).collect();
for w in crate::spell::paragraph_words(para, &default) {
if ignore.contains(&w.text.to_lowercase()) || rep.contains(&w.start) {
continue;
+1 -2
View File
@@ -405,9 +405,8 @@ fn select_slides(s: &mut Session, p: &Value) -> Result<Value> {
};
s.select(|doc, sel| {
if add {
let mut have: std::collections::HashSet<_> = sel.slides.iter().copied().collect();
for i in &ids {
if have.insert(*i) {
if !sel.slides.contains(i) {
sel.slides.push(*i);
}
}
+33 -3
View File
@@ -171,6 +171,7 @@ fn rels_of(doc: &xml::Doc, part: &str) -> Rels {
let external = r.attr("TargetMode").is_some_and(|m| m.eq_ignore_ascii_case("external"));
let target_raw = target.to_string();
let target = if external { target_raw.clone() } else { resolve(&base, target) };
out.by_id.entry(id.to_string()).or_insert(out.items.len());
out.items.push(Rel {
id: id.to_string(),
kind: ty.rsplit('/').next().unwrap_or(ty).to_string(),
@@ -211,11 +212,13 @@ pub struct Rel {
#[derive(Clone, Debug, Default)]
pub struct Rels {
pub items: Vec<Rel>,
/// Id → index of its first relationship in `items`.
by_id: HashMap<String, usize>,
}
impl Rels {
pub fn get(&self, id: &str) -> Option<&Rel> {
self.items.iter().find(|r| r.id == id)
self.by_id.get(id).and_then(|&i| self.items.get(i))
}
pub fn target(&self, id: &str) -> Option<&str> {
self.get(id).map(|r| r.target.as_str())
@@ -339,23 +342,29 @@ pub fn relative(from: &str, to: &str) -> String {
#[derive(Default)]
pub struct RelsOut {
pub items: Vec<(String, String, String, bool)>,
/// (type, target, external) → id of the relationship [`RelsOut::add`] made for it.
added: HashMap<(String, String, bool), String>,
}
impl RelsOut {
/// Add (or reuse) a relationship; `kind` is a full type URI or a suffix of the standard base.
pub fn add(&mut self, kind: &str, target: &str, external: bool) -> String {
let ty = if kind.starts_with("http") { kind.to_string() } else { format!("{RT_BASE}{kind}") };
if let Some((id, ..)) = self.items.iter().find(|(_, t, tg, e)| *t == ty && tg == target && *e == external) {
let key = (ty, target.to_string(), external);
if let Some(id) = self.added.get(&key) {
return id.clone();
}
let id = format!("rId{}", self.items.len() + 1);
self.items.push((id.clone(), ty, target.to_string(), external));
self.items.push((id.clone(), key.0.clone(), key.1.clone(), external));
self.added.insert(key, id.clone());
id
}
/// Always add a new relationship even if an identical one exists.
pub fn add_new(&mut self, kind: &str, target: &str, external: bool) -> String {
let ty = if kind.starts_with("http") { kind.to_string() } else { format!("{RT_BASE}{kind}") };
let id = format!("rId{}", self.items.len() + 1);
// `add` reuses the first relationship for a target, whichever call made it.
self.added.entry((ty.clone(), target.to_string(), external)).or_insert_with(|| id.clone());
self.items.push((id.clone(), ty, target.to_string(), external));
id
}
@@ -500,6 +509,27 @@ mod tests {
assert!(p.get("e").is_some());
}
#[test]
fn many_relationships_are_found_by_index_and_first_id_wins() {
let n = 50_000;
let mut x = String::from(r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">"#);
for i in 0..n {
x.push_str(&format!(r#"<Relationship Id="rId{i}" Type="{RT_BASE}image" Target="../media/image{i}.png"/>"#));
}
x.push_str(&format!(r#"<Relationship Id="rId0" Type="{RT_BASE}image" Target="../media/dup.png"/></Relationships>"#));
let rels = rels_from_bytes(x.as_bytes(), "ppt/slides/slide1.xml");
assert_eq!(rels.target("rId0"), Some("ppt/media/image0.png"), "the first of a repeated id wins");
assert!((0..n).all(|i| rels.target(&format!("rId{i}")) == Some(&*format!("ppt/media/image{i}.png"))));
let mut out = RelsOut::default();
let ids: Vec<String> = (0..n).map(|i| out.add("hyperlink", &format!("https://example.com/{i}"), true)).collect();
assert_eq!(out.add("hyperlink", "https://example.com/7", true), ids[7], "an identical target reuses its id");
assert_eq!(out.add("hyperlink", "https://example.com/7", false), format!("rId{}", n + 1), "internal is distinct");
let fresh = out.add_new("image", "../media/a.png", false);
assert_ne!(out.add_new("image", "../media/a.png", false), fresh);
assert_eq!(out.add("image", "../media/a.png", false), fresh, "add reuses the first add_new made");
}
#[test]
fn backslash_zip_entry_names_are_found() {
let z = zip_of(&[("PPT\\Slides\\Slide1.xml", 3)]);