Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 7 additions & 18 deletions src/formats/pptx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ const MASTER_REL: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster";
const NOTES_REL: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide";
const SLIDE_REL: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";

/// Namespaces whose markup this frontend understands; `mc:Choice` branches
/// requiring anything else fall back to `mc:Fallback`.
Expand Down Expand Up @@ -94,9 +93,10 @@ pub fn parse(bytes: &[u8]) -> Result<Document, ConvertError> {
let mut blocks: Vec<Block> = Vec::new();
let mut failed = 0usize;
let instance_counter = StdCell::new(0u64);
// Every slide has a start anchor id so internal slide-to-slide links
// resolve after concatenation; the anchor node is emitted only on
// slides some link actually targets.
// Every slide carries a start anchor id: the node marks where the slide
// begins (downstream chunkers key on it) and lets internal slide-to-slide
// links resolve after concatenation. An anchor nothing links to renders
// as nothing in Markdown, so emitting one per slide is free.
let slide_anchors: HashMap<String, String> = slide_paths
.iter()
.enumerate()
Expand All @@ -106,16 +106,6 @@ pub fn parse(bytes: &[u8]) -> Result<Document, ConvertError> {
for p in &slide_paths {
all_rels.push(read_rels(&mut pkg.borrow_mut(), &rels_part_for(p))?);
}
let targeted: std::collections::HashSet<String> = slide_paths
.iter()
.zip(&all_rels)
.flat_map(|(p, rels)| {
rels.iter()
.filter(|(_, r)| r.rel_type == SLIDE_REL && r.mode == TargetMode::Internal)
.filter_map(move |(_, r)| path::resolve(p, &r.target).ok().map(|t| t.path))
})
.filter(|t| slide_anchors.contains_key(t))
.collect();

for (slide_index, slide_path) in slide_paths.iter().enumerate() {
let tree = match pkg.borrow_mut().optional_xml_part(slide_path)? {
Expand Down Expand Up @@ -164,9 +154,7 @@ pub fn parse(bytes: &[u8]) -> Result<Document, ConvertError> {
instance_counter: &instance_counter,
slide_anchors: &slide_anchors,
};
if targeted.contains(slide_path)
&& let Some(anchor) = slide_anchors.get(slide_path)
{
if let Some(anchor) = slide_anchors.get(slide_path) {
blocks.push(Block::Paragraph(vec![Inline::Anchor(anchor.clone())]));
}
parse_shapes(sp_tree, &ctx, &mut blocks)?;
Expand Down Expand Up @@ -267,7 +255,8 @@ struct SlideCtx<'a, 'b> {
master: Option<&'b MasterInfo>,
/// Per-text-body list instance ids, unique document-wide.
instance_counter: &'b StdCell<u64>,
/// Slide part path -> the slide's start anchor id, for internal
/// Slide part path -> the slide's start anchor id: the boundary marker
/// emitted before each slide's content and the target of internal
/// slide-to-slide links.
slide_anchors: &'b HashMap<String, String>,
}
Expand Down
26 changes: 26 additions & 0 deletions tests/snapshots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,32 @@ fn embedded_ole_payload_is_retained() {
assert_eq!(ole.bytes, b"OLE-PAYLOAD-STAND-IN".repeat(4));
}

/// Every slide carries a start anchor in the document model, so downstream
/// chunkers can find where one slide ends and the next begins (issue #94).
/// An anchor nothing links to renders as nothing in Markdown, so the flat
/// `to_markdown` output is unchanged; the boundary signal is only visible
/// through `to_document`.
#[test]
fn pptx_slide_boundaries_are_exposed() {
let path = fixture_root().join("pptx").join("handmade-links.pptx");
let bytes = std::fs::read(&path).unwrap();
let doc = anydoc::to_document(&bytes, anydoc::Format::Pptx).unwrap();

let anchors: Vec<&str> = doc
.blocks
.iter()
.filter_map(|b| match b {
anydoc::model::Block::Paragraph(inlines) => match inlines.as_slice() {
[anydoc::model::Inline::Anchor(id)] => Some(id.as_str()),
_ => None,
},
_ => None,
})
.collect();

assert_eq!(anchors, ["slide-1", "slide-2"]);
}

/// Standard Word OLE markup places a VML preview image next to the
/// `o:OLEObject`; the object payload (not the preview) must be retained.
#[test]
Expand Down