Skip to content

fix(docx): continue counters across numIds that share an abstract (#96) - #129

Open
Mr-Neutr0n wants to merge 2 commits into
firecrawl:mainfrom
Mr-Neutr0n:fix/docx-shared-abstract-numbering
Open

fix(docx): continue counters across numIds that share an abstract (#96)#129
Mr-Neutr0n wants to merge 2 commits into
firecrawl:mainfrom
Mr-Neutr0n:fix/docx-shared-abstract-numbering

Conversation

@Mr-Neutr0n

@Mr-Neutr0n Mr-Neutr0n commented Aug 23, 2026

Copy link
Copy Markdown

Fixes #96

Summary

Heading and list numbering now continues across w:num instances that share one w:abstractNum, instead of restarting at every instance switch.

Root cause

Counters were keyed per numId. Word documents (and converter output) routinely split one logical outline across several numIds of the same abstract, so every direct-numPr vs style-inherited switch reset the sequence: the reported file renders "Section 4 → 4.1 → 1.1 → 4.2" with multiple "Section 1"s. Reapplying the style "fixes" it only because it removes the direct numPr, collapsing everything to a single numId.

Reproduced with a minimal OOXML fixture: two numIds sharing abstract 0 produce Section 1, Section 2, Section 1, 1.1 on this branch's base commit.

What changed

Counters keys state by the resolved abstract definition (after numStyleLink indirection). Entering through an instance carrying lvlOverride restarts exactly its overridden levels; plain instances continue the shared sequence. startOverride starts are honored as before.

Evidence this matches Word: the real-world text.docx fixture has consecutive items on numId 5 then numId 4 whose source text labels them "Roman starting at four", "Roman five" - previously rendered IV then I; now IV then V.

Snapshot updates (intentional, flagged)

  • handmade-numbering: "Two-one independent counter" now continues (5.) rather than restarting (1.), and the style-referenced paragraph follows the merged count (8.). Override cases (10., 7)) unchanged. If bare shared instances were meant to stay independent despite Word behavior, that contract can be restored behind an option -- but parity with Word is what DOCM to MD Conversion - Headings to not match expected numbering from document. #96 reports as missing.
  • text.docx / malformed variants: "I." becomes "V.", matching the document's own labels.

Verification

  • New regression test instances_sharing_an_abstract_continue_one_sequence (continues across n1→n2, restarts at startOverride=10).
  • Full suite: 287 passed incl. snapshots; clippy clean; fmt clean.

This change was prepared with AI assistance under human direction and review.


Summary by cubic

Continue DOCX list and heading counters across w:num instances that resolve to the same w:abstractNum, matching Word. Previously counters keyed by numId restarted on each instance switch; now they key by the resolved abstract and restart only levels overridden by w:lvlOverride/w:startOverride.

  • Key counters by abstract identity (after w:numStyleLink resolution). Track per-instance overridden levels and restart them on first use under that instance; plain instances continue the shared sequence.
  • Store the last active instance as Option<u64> to avoid sentinel collisions with legal max numId values.
  • Add regression tests instances_sharing_an_abstract_continue_one_sequence and override_restart_does_not_leak_into_the_next_instance. Update snapshots where shared instances now continue (e.g., "Two-one independent counter" -> 5., "I." -> "V."); override-driven restarts are unchanged.

Written for commit 52dfa5e. Summary will update on new commits.

Review in cubic

…recrawl#96)

Word treats every w:num referencing one w:abstractNum as a single logical
list; startOverride exists precisely to break away from it. Counters were
keyed per numId instead, so documents whose outline is split across
several instances of one abstract restarted at every switch: headings
came out as Section 1, 2, 1(!), 1.1(!), 3 ... exactly the reported
multiple-section-1 / 4.x-to-1.1 symptom in firecrawl#96. Reapplying the paragraph
style clears it because it drops the direct numPr pointing at the second
instance, collapsing the document back to one numId.

Counters are now keyed by the resolved abstract definition. Entering a
list through an instance that carries lvlOverride restarts exactly its
overridden levels (nested w:lvl or startOverride); everything else keeps
counting. The real-world text.docx fixture shows why this direction is
right: consecutive items use numId 5 then numId 4 and the source itself
labels them four, five -- previously rendered as IV, I.

Snapshot updates: handmade-numbering's "Two-one independent counter"
now continues the shared sequence (5.) rather than restarting (1.), and
the style-referenced paragraph follows the merged sequence (8.). The
override-driven cases (10., 7)) still restart as before. If the intent
was that bare shared instances stay independent in anydoc despite Word,
this is easy to flip behind a flag -- but Word parity is what firecrawl#96 asks
for.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread src/formats/docx/numbering.rs Outdated
Comment thread src/formats/docx/numbering.rs Outdated
Mr-Neutr0n added a commit to Mr-Neutr0n/anydoc that referenced this pull request Aug 23, 2026
Cubic review follow-ups on firecrawl#129, both real:

- last_num is now Option<u64>: a document may legally carry
  numId=18446744073709551615 (xsd:int clamps were only applied to start
  values), and a u64::MAX sentinel would have swallowed the instance
  switch that triggers an override restart.
- Overridden levels restart on their first USE under the entering
  instance (fresh_overrides) instead of eagerly marking pending bits at
  the switch. Eager marking let a following plain instance consume a
  restart it never earned: n1(1), n2[startOverride](unused at L0),
  n3 plain restarted to 1 instead of continuing at 2.

Regression test walks nested lists and asserts the continuation.
Mr-Neutr0n added a commit to Mr-Neutr0n/anydoc that referenced this pull request Aug 23, 2026
Cubic review round on firecrawl#130, all addressed:

- CLI: a path conversion with a password but no --format dropped the
  password entirely; such conversions now go through toMarkdownBytes with
  content detection. -p/--password without a value is a usage error
  instead of silently falling back to the environment variable.
- CLI: the asset pass re-read the still-encrypted container after a
  successful password-protected Markdown conversion and died with
  Encrypted. The container is now decrypted once, up front, so Markdown
  and assets see the same plaintext.
- Library: to_markdown_with_password no longer fails with Unsupported
  before decrypting when an encrypted file has no recognizable extension;
  unresolved formats flow through to the byte-level entry point.
- crypto: the decrypted payload is bounded by MAX_TOTAL_BYTES before any
  part is read, and is validated by actually opening it as an archive —
  a wrong password whose noise happens to start with PK now still ends
  in Encrypted instead of Malformed.
- is_encrypted_ooxml / decrypt_ooxml are re-exported for embedders.
- Python binding doc: removed the duplicated summary line.
- Carries the firecrawl#129 per-instance override-restart fix so the two branches
  do not conflict.
Cubic review follow-ups on firecrawl#129, both real:

- last_num is now Option<u64>: a document may legally carry
  numId=18446744073709551615 (xsd:int clamps were only applied to start
  values), and a u64::MAX sentinel would have swallowed the instance
  switch that triggers an override restart.
- Overridden levels restart on their first USE under the entering
  instance (fresh_overrides) instead of eagerly marking pending bits at
  the switch. Eager marking let a following plain instance consume a
  restart it never earned: n1(1), n2[startOverride](unused at L0),
  n3 plain restarted to 1 instead of continuing at 2.

Regression test walks nested lists and asserts the continuation.
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.

DOCM to MD Conversion - Headings to not match expected numbering from document.

1 participant