Skip to content

fix(presentations): separate slides with a thematic break - #32

Open
Ready22Race wants to merge 1 commit into
firecrawl:mainfrom
Ready22Race:fix/presentation-slide-boundaries
Open

fix(presentations): separate slides with a thematic break#32
Ready22Race wants to merge 1 commit into
firecrawl:mainfrom
Ready22Race:fix/presentation-slide-boundaries

Conversation

@Ready22Race

@Ready22Race Ready22Race commented Aug 6, 2026

Copy link
Copy Markdown

Fixes #31.

What

Emits Block::Rule between slides in the three presentation parsers (pptx, ppt, odp), which previously concatenated every slide's blocks with nothing in between.

A slide's title becomes a Heading, so decks that title every slide read correctly by accident. A slide with no title placeholder contributes no structural block, so its content is indistinguishable from a continuation of the previous slide — two untitled slides in a row produce two adjacent paragraphs, and bullet lists from different slides merge into one list.

Why a thematic break

No new concept and no page number: Block::Rule already exists, is already emitted for <hr> (src/shared/html.rs:432), and already renders as --- (src/render/markdown/mod.rs:201).

This is deliberately not the pagination #26 declined. That issue settled that a w:type="page" break is layout and dropping it is correct. A slide is a container in the source model rather than a layout artifact, so its boundary is document structure.

Shape of the change

Each parser now accumulates one slide into a local Vec<Block> and appends it only if non-empty, pushing a Rule first when output already exists. That gives two guarantees worth stating:

  • never leading — no --- before the first slide
  • never doubled — a slide that produces no blocks (empty layout, chrome-only placeholders) cannot leave a dangling or repeated break

ppt already accumulated per slide, so it only needed the guard.

Scope: core only, no binding changes

Block::Rule already exists and every binding already handles it, so nothing outside src/formats/ needs to move:

binding maps model::Block::Rule declares the rule kind
node node/src/document.rs:88 node/index.d.ts:60
python python/src/document.rs:83 python/anydoc/_anydoc.pyi:84
wasm wasm/src/document.rs:96 wasm/src/typescript.rs:47

No new Block variant, no public API change, no type-surface change. Confirmed end to end through the Python binding on the deck from the issue:

>>> anydoc.to_document(data).blocks
['heading', 'rule', 'paragraph', 'rule', 'paragraph']   # kinds

Tests

Three unit tests in src/formats/pptx/mod.rs, building minimal packages in memory in the style of the sheet module's tests — they pin the behaviour rather than just the output:

  • untitled_slides_are_separated — the defect itself: two title-less slides must not read as one
  • separator_never_leads — a deck must not open with ---, and a single slide yields only its own content
  • empty_slides_leave_no_dangling_separator — a slide contributing no blocks must not add a break, neither doubled between two real slides nor trailing

Verification

cargo fmt --all --check                                          clean
cargo clippy --workspace --all-targets --all-features -D warnings clean
cargo clippy -p anydoc-wasm --target wasm32-unknown-unknown       clean
cargo test --locked                                              194 passed, 0 failed
python -m unittest discover -s tests                             9 passed

Snapshot delta is 6 added --- lines and 6 added blank lines across 6 snapshots, with zero removals — no content changed, only separators appeared:

tests/snapshots/snapshots__odp__pres.odp.snap                  | 2 ++
tests/snapshots/snapshots__ppt__handmade-multimaster.ppt.snap  | 2 ++
tests/snapshots/snapshots__ppt__handmade-sparsenotes.ppt.snap  | 2 ++
tests/snapshots/snapshots__ppt__pres.ppt.snap                  | 2 ++
tests/snapshots/snapshots__pptx__handmade-links.pptx.snap      | 2 ++
tests/snapshots/snapshots__pptx__pres.pptx.snap                | 2 ++

Also checked by hand against a deck whose slides 2 and 3 have no title placeholder (the case in the issue) and against tests/fixtures/ppt/pres.ppt.

Note

Happy to take this in a different direction if you would rather the boundary be expressed some other way, or scope it to a subset of the three parsers. The behaviour change is visible in output, so it is your call whether it belongs in a minor bump.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

The three presentation parsers concatenate every slide's blocks into one
list with nothing between them. A slide's title becomes a heading, so decks
that title every slide read correctly by accident — but a slide with no
title placeholder contributes no structural block at all, and its content
becomes indistinguishable from the previous slide's.

Two title-less slides in a row produce two adjacent paragraphs, exactly as
if they were two paragraphs of one slide. Bullet lists from different slides
merge into one list.

This is not the pagination that "Markdown has no pages" rightly refuses. A
slide is a container in the source model, not a layout artifact, and losing
its boundary loses document structure. Block::Rule is the separator the
model already has (emitted for <hr>, rendered as ---), so no new concept is
introduced and no page number is implied.

Emitted BETWEEN slides only: never leading, and a slide that produces no
blocks never yields a doubled or dangling break. Applies to pptx, ppt and
odp, which all had the same shape.

Core-only: every binding already maps model::Block::Rule (node/src/
document.rs:88, python/src/document.rs:83, wasm/src/document.rs:96) and
already declares the `rule` kind (node/index.d.ts:60,
python/anydoc/_anydoc.pyi:84, wasm/src/typescript.rs:47), so no binding
source or type surface changes.
@H0rowitz

H0rowitz commented Aug 6, 2026

Copy link
Copy Markdown

Independent verification, for whatever it's worth — we hit #31 on a Pandoc-authored deck while evaluating anydoc as the conversion engine for our ingestion pipeline, before finding the issue already filed.

Checked out 4b0edae and ran it against that deck (3 slides; slide 2 has no title placeholder — exactly the pathological case from the issue):

  • cargo test --locked: 194 passed, 0 failed, matching the PR's own verification block.
  • The two separators appear at exactly the two slide boundaries, none leading, none doubled.
  • A diff of the full converted output against 8eecca5 is 4 added lines (two ---, two blanks), zero removals — and this is a producer (Pandoc) the fixture corpus doesn't currently cover, so it complements the PR's LibreOffice/handmade snapshot evidence rather than repeating it.

One note for anyone reading along, orthogonal to this PR: on that same deck the remaining structure loss is bullet formatting inherited from the layout (<p:ph idx="1"/> content placeholders lose their list markers) — the separate, bigger inheritance topic. The boundary fix makes such decks navigable regardless, and untitled slides stop merging into their predecessor.

H0rowitz added a commit to H0rowitz/anydoc that referenced this pull request Aug 6, 2026
…(independently verified)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@blueagledev

Copy link
Copy Markdown

Independent verification. We're evaluating anydoc as the extraction layer for a documentation-ingestion pipeline that consumes the document model rather than the Markdown serializer, so slide boundaries are load-bearing for us. In fairness: we tested this PR and #95 against the same deck, and we've posted parallel notes there. Both work; the differences are below.

Setup: checked out from refs/pull/32/head, cargo/rustc 1.95.0. Test deck is Pandoc-authored: 2 slides, slide 1 titled with an image and speaker notes, slide 2 has no title placeholder — the pathological case from #31.

cargo test --locked: 194 passed, 0 failed, 1 ignored. (The lower count vs. main's 217 is staleness, not skipped tests — this branch is based on 8eecca5, roughly 23 commits behind e754e1d/v0.1.9. It still applies cleanly to current main.)

Document model. Baseline v0.1.9 gives 4 top-level blocks with no marker of any kind — Heading -> Paragraph(Image) -> BlockQuote -> Table. With this PR, 5 blocks:

Heading -> Paragraph(Image) -> BlockQuote -> Rule -> Table

The untitled slide 2 separates correctly, exactly as the PR describes, and the never-leading / never-doubled guarantees hold on our fixture.

Where it left us short, and why we'd bet on #95. Our output format numbers slides (## Slide N), so we need an ordinal, and it can't be recovered here by counting rules. This PR's own passing test empty_slides_leave_no_dangling_separator asserts that a 3-slide deck with an empty middle slide yields exactly one rule — correct behaviour for a separator, but it means a consumer counting rules labels the third slide "2". #95 carries the ordinal in the marker itself (Inline::Anchor("slide-N"), derived from slide_paths.enumerate()), so it survives empty and unparseable slides.

The secondary point is that Block::Rule is a content construct — indistinguishable from a genuine <hr> in source content — where an anchor is a dedicated identity. Your write-up argues a slide boundary is document structure rather than a layout artifact, and we agree; that argument seems to us to point at a structural marker rather than a content one.

One thing this PR has that #95 does not: ODF coverage. On tests/fixtures/odp/pres.odp this separates slides (11 blocks, one rule) while #95's output is byte-identical to base (10 blocks, no marker). If #95 is the one that lands, .odp needs the same treatment as a follow-up — that gap is real and it's yours.

The two conflict: this applies cleanly to main, after which #95 conflicts in 6 files (both edit the same statement), so whichever lands first forces the other to rebase.

None of this is a knock on the change — it does what it says and the reasoning in the description is sound. Posting the comparison because a maintainer choosing between two competing PRs is better served by it than by a thumbs-up on each.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Presentation slides are concatenated with no boundary, so untitled slides merge into the previous one

3 participants