mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-03 17:12:52 -06:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
866899d9de |
+3
-33
@@ -171,7 +171,6 @@ 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(),
|
||||
@@ -212,13 +211,11 @@ 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.by_id.get(id).and_then(|&i| self.items.get(i))
|
||||
self.items.iter().find(|r| r.id == id)
|
||||
}
|
||||
pub fn target(&self, id: &str) -> Option<&str> {
|
||||
self.get(id).map(|r| r.target.as_str())
|
||||
@@ -342,29 +339,23 @@ 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}") };
|
||||
let key = (ty, target.to_string(), external);
|
||||
if let Some(id) = self.added.get(&key) {
|
||||
if let Some((id, ..)) = self.items.iter().find(|(_, t, tg, e)| *t == ty && tg == target && *e == external) {
|
||||
return id.clone();
|
||||
}
|
||||
let id = format!("rId{}", self.items.len() + 1);
|
||||
self.items.push((id.clone(), key.0.clone(), key.1.clone(), external));
|
||||
self.added.insert(key, id.clone());
|
||||
self.items.push((id.clone(), ty, target.to_string(), external));
|
||||
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
|
||||
}
|
||||
@@ -509,27 +500,6 @@ 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)]);
|
||||
|
||||
@@ -46,6 +46,9 @@ pub struct Imp<'a> {
|
||||
pub tag_bytes: usize,
|
||||
/// The tag budget ran out and the warning was given.
|
||||
pub tags_capped: bool,
|
||||
/// Whether a part has relationships of its own, by normalized part name: looked up once
|
||||
/// however many embedded objects point at the part.
|
||||
pub has_rels: HashMap<String, bool>,
|
||||
}
|
||||
|
||||
impl Imp<'_> {
|
||||
@@ -446,6 +449,7 @@ pub fn import_theme(bytes: &[u8]) -> Result<(deckcraft_model::theme::Theme, Vec<
|
||||
tags_by_part: HashMap::new(),
|
||||
tag_bytes: 0,
|
||||
tags_capped: false,
|
||||
has_rels: HashMap::new(),
|
||||
};
|
||||
let part = Part::new(&pkg, &theme_path, &doc);
|
||||
let theme = theme::read_theme(&mut imp, &part, &doc.root);
|
||||
@@ -487,6 +491,7 @@ pub fn import_checked(bytes: &[u8]) -> Result<(Presentation, Vec<String>), PptxE
|
||||
tags_by_part: HashMap::new(),
|
||||
tag_bytes: 0,
|
||||
tags_capped: false,
|
||||
has_rels: HashMap::new(),
|
||||
};
|
||||
// Slide ids first so hyperlinks anywhere can point at slides.
|
||||
let slide_ids = register_slides(&mut imp, &docs, &slide_entries, &mut lost_slides);
|
||||
|
||||
@@ -627,12 +627,22 @@ fn push_text_runs(para: &mut Paragraph, props: RunProps, text: &str) {
|
||||
/// Appends an Office Math (a14:m) run: its plain text and its OMML.
|
||||
fn push_math_run(para: &mut Paragraph, part: &Part, e: &El) {
|
||||
if para.runs.len() < MAX_PARAGRAPH_RUNS {
|
||||
let mut text = String::new();
|
||||
let mut ts = vec![];
|
||||
e.find_all("t", &mut ts);
|
||||
for t in ts {
|
||||
text.push_str(&t.text());
|
||||
// The text of the outermost `t` elements: a `t` nested in another is already part of
|
||||
// the outer one's text, and taking it again would copy it once per nesting level.
|
||||
fn outer_t(e: &El, depth: usize, text: &mut String) {
|
||||
if depth > crate::xml::MAX_DEPTH {
|
||||
return;
|
||||
}
|
||||
for c in e.elements() {
|
||||
if c.is("t") {
|
||||
text.push_str(&c.text());
|
||||
} else {
|
||||
outer_t(c, depth + 1, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut text = String::new();
|
||||
outer_t(e, 0, &mut text);
|
||||
let omml = e.elements().map(|c| part.keep_inner(c)).collect::<String>();
|
||||
para.runs.push(Run { text, props: RunProps::default(), kind: RunKind::Math { omml } });
|
||||
}
|
||||
@@ -677,6 +687,17 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::xml::parse;
|
||||
|
||||
#[test]
|
||||
fn nested_math_text_is_taken_once() {
|
||||
// 200 `m:t` nested in each other around one word: the word, not 200 copies of it.
|
||||
let x = format!(r#"<a14:m xmlns:a14="a" xmlns:m="m"><m:oMath>{}word{}</m:oMath></a14:m>"#, "<m:t>".repeat(200), "</m:t>".repeat(200));
|
||||
let d = parse(x.as_bytes()).unwrap();
|
||||
let part = Part { name: "ppt/slides/slide1.xml".into(), rels: Default::default(), ns: vec![], inner_ns: vec![] };
|
||||
let mut para = Paragraph::default();
|
||||
push_math_run(&mut para, &part, &d.root);
|
||||
assert_eq!(para.runs[0].text, "word");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colors_and_transforms() {
|
||||
let d = parse(
|
||||
|
||||
@@ -475,7 +475,8 @@ fn frame(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El, depth: usize) -> S
|
||||
.into_iter()
|
||||
.map(|id| {
|
||||
let rel = part.rels.get(&id).filter(|r| !r.external)?;
|
||||
if !imp.pkg.rels(&rel.target).items.is_empty() {
|
||||
let pkg = imp.pkg;
|
||||
if *imp.has_rels.entry(crate::opc::norm(&rel.target)).or_insert_with(|| !pkg.rels(&rel.target).items.is_empty()) {
|
||||
return None;
|
||||
}
|
||||
let (kind, target) = (rel.ty.clone(), rel.target.clone());
|
||||
|
||||
Reference in New Issue
Block a user