@ansonlai/docx-redline-js 0.8.4 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +221 -12
- package/CHANGELOG.md +176 -0
- package/README.md +125 -12
- package/core/current-properties.js +136 -0
- package/core/paragraph-text.js +211 -209
- package/core/types.js +9 -3
- package/core/word-revision-semantics.js +256 -0
- package/dist/docx-redline-js.esm.js +4189 -767
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +111 -108
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/dist/docx-redline.bundle.cjs +4292 -870
- package/dist/docx-redline.bundle.cjs.map +4 -4
- package/dist/docx-redline.bundle.js +4292 -870
- package/dist/docx-redline.bundle.js.map +4 -4
- package/docs/AGENT_FAST_START.md +2 -0
- package/docs/AGENT_KNOWLEDGE_BASE.md +19 -3
- package/docs/SKILL_AUTHORING.md +165 -150
- package/docs/TESTING.md +1142 -1059
- package/docs/schemas/document-operations.schema.json +54 -1
- package/document/docx-document.js +1022 -1010
- package/engine/oxml-engine.js +736 -749
- package/engine/paragraph-mark-revisions.js +292 -0
- package/engine/reconstruction-mapper.js +312 -311
- package/engine/reconstruction-writer.js +67 -10
- package/engine/rpr-helpers.js +42 -0
- package/engine/run-builders.js +0 -73
- package/engine/surgical-diff-application.js +604 -605
- package/engine/table-mode.js +63 -63
- package/index.d.ts +47 -1
- package/index.js +11 -2
- package/node/cli-help.js +1 -0
- package/node/cli.js +992 -975
- package/orchestration/list-structural-fallback.js +67 -9
- package/package.json +1 -1
- package/pipeline/diff-engine.js +130 -1
- package/pipeline/ingestion-export.js +227 -243
- package/pipeline/ingestion-paragraph.js +93 -98
- package/pipeline/list-generation.js +415 -419
- package/pipeline/pipeline.js +206 -204
- package/scripts/export-validation-fixtures.mjs +19 -6
- package/scripts/generate-word-revision-semantics-matrix.mjs +134 -0
- package/scripts/lib/word-coverage-catalogue.mjs +7 -2
- package/scripts/render-multilevel-bullet-visual.ps1 +2 -2
- package/scripts/word-com-corpus-visual-suite.ps1 +2 -2
- package/scripts/word-com-differential.ps1 +443 -335
- package/scripts/word-com-visual-suite.ps1 +2 -2
- package/scripts/word-revision-semantics-matrix.ps1 +108 -0
- package/services/batch-operation-orchestrator.js +23 -3
- package/services/capture-engine.js +228 -226
- package/services/document-operation-applier.js +213 -0
- package/services/document-operation-contract.js +75 -0
- package/services/document-operation-mutations.js +3346 -2721
- package/services/error-recovery.js +17 -0
- package/services/list-conversion-diagnostics.js +296 -0
- package/services/list-format-change.js +586 -0
- package/services/list-format-mutation.js +416 -0
- package/services/list-format.js +305 -0
- package/services/numbering-allocation.js +642 -0
- package/services/numbering-helpers.js +354 -524
- package/services/operation-batch-compiler.js +183 -11
- package/services/operation-preflight.js +1 -0
- package/services/receipt-collector.js +288 -288
- package/services/revision-comment-management.js +38 -5
- package/services/revision-fidelity-verifier.js +498 -0
- package/services/standalone-docx-plumbing.js +627 -631
- package/services/standalone-operation-runner.d.ts +182 -5
- package/services/table-reconciliation.js +59 -7
package/README.md
CHANGED
|
@@ -15,14 +15,19 @@ Converts AI-generated or programmatic text/markdown edits into valid Office Open
|
|
|
15
15
|
- Comment management: delete comments by author or for all authors
|
|
16
16
|
- Highlights: apply highlight colors to runs
|
|
17
17
|
- Markdown and OOXML conversion in both directions
|
|
18
|
+
- Reject All verification: every tracked operation is refused unless Word's Reject All of the output reproduces the source
|
|
18
19
|
- Status/error result fields for parse, targeting, and existing-revision failures
|
|
19
20
|
- Localized exact replacements against inspected targets, plus optional fail-closed speculative CLI edits
|
|
20
21
|
- Package plumbing helpers for numbering.xml, comments.xml, content types, and relationships
|
|
21
22
|
- Zero host dependencies: works in Node.js, browsers, Deno, and similar JS runtimes with DOM parsing support
|
|
22
23
|
|
|
23
|
-
## What's New in 0.8.x
|
|
24
|
+
## What's New in 0.9.0 and 0.8.x
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
0.9.0 moves the CLI contract to version 9 and adds the `list-format` operation (`convert-plain`, `change-format`, `join`),
|
|
27
|
+
`paragraph-format` numbering binding, list-conversion diagnostics, a single numbering allocator and merge, and a Reject All
|
|
28
|
+
revision fidelity verifier that refuses by default (`REVISION_FIDELITY_VIOLATION`). See the
|
|
29
|
+
[0.9.0 release notes](./docs/releases/0.9.0.md) and the [CHANGELOG](./CHANGELOG.md). The CLI contract was version 8
|
|
30
|
+
throughout the 0.8 line.
|
|
26
31
|
|
|
27
32
|
- **0.8.0, universal document facade:** `openDocx` and `DocxDocument` are exported from the primary entry point and work
|
|
28
33
|
on `Uint8Array` input in any JavaScript runtime. ZIP handling uses `fflate` and SHA-256 is pure JavaScript, so there are no
|
|
@@ -311,7 +316,9 @@ marker. Machine `index` and `ref` fields are for targeting and pagination, not
|
|
|
311
316
|
user-facing Word locations; use `humanReference`, `provision`, or
|
|
312
317
|
`nearestHeading` in reports.
|
|
313
318
|
|
|
314
|
-
`docx-redline version` reports contract version
|
|
319
|
+
`docx-redline version` reports contract version 9 (adds the `list-format` operation type). Contract 9 adds the
|
|
320
|
+
capabilities `revision-fidelity-v1`, `list-conversion-diagnostics-v1`, `paragraph-numbering-binding-v1`,
|
|
321
|
+
`list-format-v1` (`convert-plain`) and `list-format-change-v1` (`change-format` and `join`), alongside the additive
|
|
315
322
|
`command-help-v1`, `inspection-context-v1`, `bounded-inspection-v1`,
|
|
316
323
|
`human-document-references-v1`, `batch-start-source-binding`,
|
|
317
324
|
`recovery-envelope-v1`, `require-complete-exit`, `operations-stdin`,
|
|
@@ -320,7 +327,9 @@ user-facing Word locations; use `humanReference`, `provision`, or
|
|
|
320
327
|
`speculative-search-apply-v1`, `localized-change-summary-v1`, and
|
|
321
328
|
`restore-shortcuts-v1`, plus `localized-space-equivalence-v1` capabilities. Header/footer
|
|
322
329
|
`part` targeting, the `comment_resolve` operation and `delete-comments --comment-id` are advertised as
|
|
323
|
-
`header-footer-parts-v1`, `comment-resolve-operation-v1` and `delete-comments-by-id-v1`.
|
|
330
|
+
`header-footer-parts-v1`, `comment-resolve-operation-v1` and `delete-comments-by-id-v1`. The remaining advertised
|
|
331
|
+
capabilities are `atomic-batch-results-on-package-failure`, `baseline-aware-validation`, `compact-mutation-results`,
|
|
332
|
+
`cross-author-revision-slicing`, `document-scoped-list-revision-ids` and `format-text-occurrence-v1`.
|
|
324
333
|
Wrappers should negotiate only the
|
|
325
334
|
capabilities they use. Run `docx-redline <command> --help` for that command's
|
|
326
335
|
machine-readable options, behavior, exit codes, canonical GitHub documentation links, and compact examples.
|
|
@@ -371,6 +380,9 @@ See the [compact agent fast start](./docs/AGENT_FAST_START.md), the
|
|
|
371
380
|
| `strictTargets` | `boolean` | `true` (CLI/facade) | Requires exact target descriptors (`exactText`, `paragraphId`, `index`, `occurrence`, `fingerprint`) and forbids ambiguous matching. Defaults to `false` in low-level runner for backwards compatibility. |
|
|
372
381
|
| `existingRevisions` | `string` | `'merge-same-author'` | How to handle paragraphs with existing tracked changes. `'merge-same-author'` merges revisions from the same author and protects other authors with `EXISTING_REVISIONS`. `'slice-cross-author'` keeps that same-author merge behavior while allowing Word-native edits inside another author's pending insertion. Pass `'accept-all-first'` to normalize prior revisions or `'reject-input'` to refuse editing revised paragraphs. |
|
|
373
382
|
| `removeFormatting` | `boolean` | `false` | When `true` and the text is unchanged with no Markdown hints, strips existing bold/italic/underline/strikethrough formatting. |
|
|
383
|
+
| `verifyRevisionFidelity` | `'enforce' \| 'warn' \| 'off'` | `'enforce'` | Post-apply Reject All check described under [Reject All verification](#reject-all-verification). CLI: `--revision-fidelity`; the agent profile rejects `off`. |
|
|
384
|
+
| `listMarkers` | `'literal'` | unset | Per operation. Accepts list-marker text as literal text and suppresses `LIST_MARKER_NOT_BOUND`, `UNSUPPORTED_LIST_CONVERSION`, `LIST_FORMAT_NOT_APPLIED` and `UNSUPPORTED_LIST_FORMAT_CHANGE` for that operation. |
|
|
385
|
+
| `strictListConversion` | `boolean` | `atomic` | Makes `UNSUPPORTED_LIST_CONVERSION` and `UNSUPPORTED_LIST_FORMAT_CHANGE` errors instead of warnings (capability `list-conversion-diagnostics-v1`). The CLI agent profile and `--atomic` set it; an operation's `listMarkers: 'literal'` wins. |
|
|
374
386
|
| `sanitizeInput` | `boolean` | `false` | Opt-in removal of standalone leading assistant-preface lines. Literal dollar signs and `\n` sequences are always preserved. |
|
|
375
387
|
|
|
376
388
|
Same-author revision merging refuses paragraphs containing comment anchors with
|
|
@@ -437,6 +449,58 @@ for it. For a complete `word/document.xml`, prefer
|
|
|
437
449
|
`applyOperationToDocumentXml(...)` so replacement nodes and numbering artifacts
|
|
438
450
|
are imported at the correct scope.
|
|
439
451
|
|
|
452
|
+
### List operations (`list-format`)
|
|
453
|
+
|
|
454
|
+
Text edits never create or restyle list structure. Typed markers such as `1.` or `A.` stay literal text, and a
|
|
455
|
+
`list-change` that retypes markers keeps the old numbering (it warns `LIST_FORMAT_NOT_APPLIED`, or fails
|
|
456
|
+
`UNSUPPORTED_LIST_FORMAT_CHANGE` under the strict policy, with a `list-format` request in `recovery.suggestedOperation`).
|
|
457
|
+
Structure changes use the `list-format` operation. Every mode records each paragraph's previous `w:numPr` in a tracked
|
|
458
|
+
`w:pPrChange`, so Reject All restores the original labels exactly and Accept All shows the new ones. The shared numbering
|
|
459
|
+
definitions in `numbering.xml` are never edited in place: new definitions come from one allocator and are merged by one numbering
|
|
460
|
+
merge, and an id that already exists with different content fails with `NUMBERING_ID_CONFLICT`. `paragraph-format`
|
|
461
|
+
`properties.numbering` binds or removes a list on a paragraph the same way (tracked `w:pPrChange`). Unknown
|
|
462
|
+
`paragraph-format.properties` keys are rejected with `INVALID_OPERATION`. The CLI advertises `list-format-v1` (contract version 9) for
|
|
463
|
+
`convert-plain` and `list-format-change-v1` for `change-format` and `join`.
|
|
464
|
+
|
|
465
|
+
```js
|
|
466
|
+
const doc = openDocx(bytes);
|
|
467
|
+
const result = await doc.applyOperations([
|
|
468
|
+
// Typed headers "A. Scope" .. "C. Fees" become a real A./B./C. list. Each marker is a tracked deletion, and body
|
|
469
|
+
// paragraphs in the range stay plain. A typed start value ("C.") is kept.
|
|
470
|
+
{ type: 'list-format', target: { exactText: 'A. Scope' }, targetEnd: { exactText: 'C. Fees' },
|
|
471
|
+
list: { mode: 'convert-plain' } },
|
|
472
|
+
// Bullets become 1., 2., 3. A new definition copies the list's levels and indentation.
|
|
473
|
+
{ type: 'list-format', target: { exactText: 'Photo identification' },
|
|
474
|
+
list: { mode: 'change-format', format: 'decimal' } },
|
|
475
|
+
// A later list continues an earlier one (1. 2. / 1. 2. becomes 1. 2. / 3. 4.).
|
|
476
|
+
{ type: 'list-format', target: { exactText: 'Confidentiality' },
|
|
477
|
+
list: { mode: 'join', continueFrom: { exactText: 'Termination' } } }
|
|
478
|
+
], { author: 'Editor' });
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
A changed-text header (`1. Old Terms` -> `1. New Terms`) takes two operations on the same paragraph in one batch: a
|
|
482
|
+
`redline` of the text that keeps the typed marker exactly, and a `list-format` that converts the paragraph. The batch
|
|
483
|
+
compiler allows this pair and always runs the redline first, whatever the batch order. The list-format then strips the
|
|
484
|
+
unchanged marker. Accept All shows label `1.` with `New Terms`, and Reject All shows the literal `1. Old Terms`:
|
|
485
|
+
|
|
486
|
+
```js
|
|
487
|
+
await doc.applyOperations([
|
|
488
|
+
{ type: 'redline', target: { exactText: '1. Old Terms' }, replacements: [{ find: 'Old', replace: 'New' }] },
|
|
489
|
+
{ type: 'list-format', target: { exactText: '1. Old Terms' }, list: { mode: 'convert-plain' } }
|
|
490
|
+
], { author: 'Editor' });
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
A redline in that pair that adds, removes or changes the marker is refused before anything is written, with
|
|
494
|
+
`LIST_FORMAT_COMPOSITION_CONFLICT` (`reason: 'marker-changed'`). So is a redline that spans paragraphs or leaves the text
|
|
495
|
+
unchanged. To give a paragraph without a typed marker a label, use `strip: 'none'` with `format` (and `start`). A
|
|
496
|
+
`change-format` or `join` with `scope: 'list'` (the default) also writes every other item of the list. In the same batch,
|
|
497
|
+
each of those items accepts only a single-paragraph `redline`; any other write to them fails with
|
|
498
|
+
`LIST_FORMAT_COMPOSITION_CONFLICT` (`reason: 'list-rebind'`).
|
|
499
|
+
Style-linked lists (heading numbering), mid-list splits, and markers inside hyperlinks or fields are refused with a
|
|
500
|
+
recovery envelope. They are not approximated. Removing a list binding is `paragraph-format` with `properties.numbering:
|
|
501
|
+
null`. A `list-format` `detach` mode is not available yet. See the
|
|
502
|
+
[knowledge base](docs/AGENT_KNOWLEDGE_BASE.md#operation-model-choose-by-output-shape) for every mode, field and refusal.
|
|
503
|
+
|
|
440
504
|
### Planning large mixed-content insertions
|
|
441
505
|
|
|
442
506
|
For an attachment or schedule containing several kinds of content, use
|
|
@@ -509,7 +573,7 @@ an earlier block has already replaced.
|
|
|
509
573
|
| `deleteCommentsByAuthorInOoxml(oxml, { author?, allAuthors? })` | Delete matching comment definitions and anchors present in the supplied OOXML payload. Real `.docx` packages require updating both `word/comments.xml` and `word/document.xml`. |
|
|
510
574
|
| `generateTableOoxml(headers, rows, options)` | Generate a `w:tbl` from tabular data. |
|
|
511
575
|
| `createDynamicNumberingIdState(numberingXml)` | Allocate numbering IDs without collisions. |
|
|
512
|
-
| `ensureNumberingArtifactsInZip(zip, numberingXml, options)` | Add numbering artifacts to a `.docx` package.
|
|
576
|
+
| `ensureNumberingArtifactsInZip(zip, numberingXml, options)` | Add numbering artifacts to a `.docx` package. Always merges into an existing `word/numbering.xml` through the package's single numbering merge (`mergeNumberingParts`); an incoming id that already exists with different content fails with `NUMBERING_ID_CONFLICT`. The former `mergeNumberingXml` option is ignored. |
|
|
513
577
|
| `ensureCommentsArtifactsInZip(zip, commentsXml)` | Merge comments artifacts into a `.docx` package. |
|
|
514
578
|
| `ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXml)` | Add or replace modern Word comment-thread metadata in a `.docx` package. |
|
|
515
579
|
| `validateDocxPackage(zip)` | Validate `.docx` structural consistency. |
|
|
@@ -556,8 +620,9 @@ identities. Independent edits therefore do not need to be manually sorted when
|
|
|
556
620
|
an earlier structural rewrite changes later paragraph indexes or fingerprints.
|
|
557
621
|
Targets that deliberately refer to uniquely created paragraph text are compiled
|
|
558
622
|
into an internal capture dependency. True overlap is not guessed: incompatible
|
|
559
|
-
writes to one source fail before mutation with `OVERLAPPING_SOURCE_TARGETS
|
|
560
|
-
`REVISION_ORDER_CONFLICT
|
|
623
|
+
writes to one source fail before mutation with `OVERLAPPING_SOURCE_TARGETS`,
|
|
624
|
+
`REVISION_ORDER_CONFLICT` or `LIST_FORMAT_COMPOSITION_CONFLICT` (the one composed pair, a
|
|
625
|
+
marker-preserving `redline` plus a `list-format`, runs redline first), and mutating capture fan-out without distinct
|
|
561
626
|
selectors fails with `CAPTURE_FANOUT_CONFLICT`.
|
|
562
627
|
|
|
563
628
|
Threaded replies use a comment operation with no body target:
|
|
@@ -667,6 +732,57 @@ chooses among multiple candidate paragraphs heuristically, it emits an
|
|
|
667
732
|
guidance. Callers should pass `{ strictTargets: true }` and use strict descriptors
|
|
668
733
|
(`paragraphId`, `index`, `occurrence`, or `fingerprint`) to prepare for v1.0.0.
|
|
669
734
|
|
|
735
|
+
### Reject All verification
|
|
736
|
+
|
|
737
|
+
A tracked edit only adds revisions, so rejecting every revision in the output
|
|
738
|
+
must give back the source document. After each operation the library projects
|
|
739
|
+
Word's Reject All and Accept All of the output (`core/word-revision-semantics.js`,
|
|
740
|
+
validated against recorded desktop Word results) and checks these invariants:
|
|
741
|
+
|
|
742
|
+
- `REJECT_PARAGRAPH_COUNT`, `REJECT_PARAGRAPH_TEXT` (including tabs and breaks),
|
|
743
|
+
`REJECT_PARAGRAPH_PROPS`, `REJECT_NUMBERING_REFS` (list membership `numId`/`ilvl`)
|
|
744
|
+
and `REJECT_WRAPPERS` (hyperlink, field and content-control membership):
|
|
745
|
+
Reject All of the output equals Reject All of the source (under accept-first
|
|
746
|
+
`existingRevisions` policies the target range is compared with the source's
|
|
747
|
+
accepted view). `REJECT_NUMBERING_REFS` also refuses a `numId` that the output
|
|
748
|
+
binds, or Reject All restores (including inside `w:pPrChange`), when the source
|
|
749
|
+
did not already use it and no `w:num` -> `w:abstractNum` -> `w:lvl` in the
|
|
750
|
+
available numbering defines it.
|
|
751
|
+
- `ACCEPT_INTENT`: a tracked plain-text `redline` with complete `modified` (no
|
|
752
|
+
Markdown, list-marker or table syntax; a single paragraph or a `targetEnd`
|
|
753
|
+
range) is refused when Accept All of its target would not read as `modified`.
|
|
754
|
+
- `ACCEPT_NEIGHBORS`: an untracked edit (`generateRedlines: false`) may not
|
|
755
|
+
change a paragraph outside its target in the Accept All or Reject All view.
|
|
756
|
+
- `UNTRACKED_CREATED_REVISION`: an untracked edit may not create a tracked
|
|
757
|
+
revision that was not in the source (fragments of a sliced source revision
|
|
758
|
+
keep their element, author and date and are allowed).
|
|
759
|
+
|
|
760
|
+
A localized replacement is also checked before it is committed: the target's
|
|
761
|
+
accepted text must equal the compiled replacement text, otherwise the operation
|
|
762
|
+
is refused with `PATCH_ROUNDTRIP_MISMATCH` rather than dropping content.
|
|
763
|
+
|
|
764
|
+
By default a violation is refused with `REVISION_FIDELITY_VIOLATION`; the
|
|
765
|
+
document is unchanged, `status` is `error`, and the receipt is `refused`. The
|
|
766
|
+
error payload contains:
|
|
767
|
+
|
|
768
|
+
- `violations[]`, each with `invariant` (`REJECT_PARAGRAPH_COUNT`,
|
|
769
|
+
`REJECT_PARAGRAPH_TEXT`, `REJECT_PARAGRAPH_PROPS`, `REJECT_NUMBERING_REFS`,
|
|
770
|
+
`REJECT_WRAPPERS`, `ACCEPT_INTENT`, `ACCEPT_NEIGHBORS` or
|
|
771
|
+
`UNTRACKED_CREATED_REVISION`), a plain-language `explanation`,
|
|
772
|
+
`expected`/`actual` excerpts, and `source.sourceParagraphIndex` (the 1-based
|
|
773
|
+
paragraph index from extraction) with a text excerpt;
|
|
774
|
+
- `recovery.retryGuidance`, one retry instruction per reported invariant, for
|
|
775
|
+
example use `replacements` for a literal edit, keep breaks and hyperlink
|
|
776
|
+
boundaries, or move formatting and list changes into their own operation.
|
|
777
|
+
|
|
778
|
+
Control it with the `verifyRevisionFidelity` option (`'enforce'` default,
|
|
779
|
+
`'warn'`, `'off'`) or `--revision-fidelity enforce|warn|off` on the CLI. `warn`
|
|
780
|
+
applies the edit and appends the same text to `warnings`. The `--profile agent`
|
|
781
|
+
profile rejects `off`. A narrower retry that is also refused indicates an
|
|
782
|
+
unsupported shape; report it rather than disabling verification. Recovery
|
|
783
|
+
details are in the `REVISION_FIDELITY_VIOLATION` row of
|
|
784
|
+
[docs/AGENT_KNOWLEDGE_BASE.md](./docs/AGENT_KNOWLEDGE_BASE.md).
|
|
785
|
+
|
|
670
786
|
### Mutation Receipts
|
|
671
787
|
|
|
672
788
|
Both single-operation (`applyOperationToDocumentXml`) and batch
|
|
@@ -722,7 +838,7 @@ Different APIs return different OOXML shapes. Use this as a packaging safety che
|
|
|
722
838
|
ID. Generated bullet and numbered paragraphs must reference a positive ID
|
|
723
839
|
whose definition is merged into `word/numbering.xml`.
|
|
724
840
|
- Do use `extractReplacementNodesFromOoxml(...)` when you are consuming `result.oxml` from paragraph/range/table APIs.
|
|
725
|
-
- Do merge numbering/comments artifacts with `ensureNumberingArtifactsInZip(...)` and `ensureCommentsArtifactsInZip(...)` when those parts are present.
|
|
841
|
+
- Do merge numbering/comments artifacts with `ensureNumberingArtifactsInZip(...)` and `ensureCommentsArtifactsInZip(...)` when those parts are present. Numbering is always merged, never replaced; pass `numberingXml` or a shared `numberingIdState` to every call so generated ids cannot collide.
|
|
726
842
|
- Don't write payloads that start with `<pkg:package` directly into `word/document.xml`.
|
|
727
843
|
- Don't assume every `result.oxml` payload is a raw paragraph fragment.
|
|
728
844
|
|
|
@@ -744,7 +860,6 @@ import {
|
|
|
744
860
|
applyRedlineToOxml,
|
|
745
861
|
extractReplacementNodesFromOoxml,
|
|
746
862
|
ensureNumberingArtifactsInZip,
|
|
747
|
-
mergeNumberingXmlBySchemaOrder,
|
|
748
863
|
validateDocxPackage
|
|
749
864
|
} from '@ansonlai/docx-redline-js';
|
|
750
865
|
import { applyOperationToDocumentXml } from '@ansonlai/docx-redline-js/standalone-runner';
|
|
@@ -807,9 +922,7 @@ const normalized = extractReplacementNodesFromOoxml(fragmentResult.oxml);
|
|
|
807
922
|
// If sourceType === 'package', merge extracted content/artifacts instead of
|
|
808
923
|
// writing the raw pkg:package payload into word/document.xml.
|
|
809
924
|
if (normalized.numberingXml) {
|
|
810
|
-
await ensureNumberingArtifactsInZip(zip, normalized.numberingXml
|
|
811
|
-
mergeNumberingXml: mergeNumberingXmlBySchemaOrder
|
|
812
|
-
});
|
|
925
|
+
await ensureNumberingArtifactsInZip(zip, normalized.numberingXml);
|
|
813
926
|
}
|
|
814
927
|
|
|
815
928
|
await validateDocxPackage(zip);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single accessor for CURRENT paragraph/run properties.
|
|
3
|
+
*
|
|
4
|
+
* Descendant lookups (`getElementsByTagNameNS`, `getWordDescendants`, ...) cross `w:pPrChange` and
|
|
5
|
+
* `w:rPrChange`, so a historical `numPr`/`pStyle`/`b` snapshot looks like a current value. Everything in
|
|
6
|
+
* this module reads direct children of `w:pPr` / `w:rPr` only. The `*PrChange` snapshot is reachable
|
|
7
|
+
* only through the explicit `getHistorical*` functions (reject simulation, restore).
|
|
8
|
+
*
|
|
9
|
+
* Depends only on `core/types.js`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { NS_W } from './types.js';
|
|
13
|
+
|
|
14
|
+
function isWordElement(node, localName) {
|
|
15
|
+
if (!node || node.nodeType !== 1) return false;
|
|
16
|
+
const name = String(node.localName || node.nodeName || '').replace(/^.*:/, '');
|
|
17
|
+
if (localName != null && name !== localName) return false;
|
|
18
|
+
return !node.namespaceURI || node.namespaceURI === NS_W;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** First direct child element with the given WordprocessingML local name. */
|
|
22
|
+
export function getDirectWordChild(node, localName) {
|
|
23
|
+
if (!node) return null;
|
|
24
|
+
for (const child of Array.from(node.childNodes || [])) {
|
|
25
|
+
if (isWordElement(child, localName)) return child;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Direct child elements with the given local name (all direct Word elements when omitted). */
|
|
31
|
+
export function getDirectWordChildren(node, localName) {
|
|
32
|
+
if (!node) return [];
|
|
33
|
+
return Array.from(node.childNodes || []).filter(child => isWordElement(child, localName));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Direct child of a properties element; never descends into `*PrChange` snapshots. */
|
|
37
|
+
export function getCurrentChild(propsEl, localName) {
|
|
38
|
+
return getDirectWordChild(propsEl, localName);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readVal(el) {
|
|
42
|
+
if (!el) return null;
|
|
43
|
+
const value = (typeof el.getAttributeNS === 'function' && el.getAttributeNS(NS_W, 'val'))
|
|
44
|
+
|| el.getAttribute?.('w:val')
|
|
45
|
+
|| el.getAttribute?.('val')
|
|
46
|
+
|| null;
|
|
47
|
+
return value === '' ? null : value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readNumPr(propsEl) {
|
|
51
|
+
const numPr = getDirectWordChild(propsEl, 'numPr');
|
|
52
|
+
if (!numPr) return null;
|
|
53
|
+
return {
|
|
54
|
+
element: numPr,
|
|
55
|
+
numId: readVal(getDirectWordChild(numPr, 'numId')),
|
|
56
|
+
ilvl: readVal(getDirectWordChild(numPr, 'ilvl')),
|
|
57
|
+
numIdElement: getDirectWordChild(numPr, 'numId'),
|
|
58
|
+
ilvlElement: getDirectWordChild(numPr, 'ilvl')
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readParagraphProps(pPr) {
|
|
63
|
+
const pStyleEl = getDirectWordChild(pPr, 'pStyle');
|
|
64
|
+
const outlineEl = getDirectWordChild(pPr, 'outlineLvl');
|
|
65
|
+
return {
|
|
66
|
+
pPr: pPr || null,
|
|
67
|
+
numPr: readNumPr(pPr),
|
|
68
|
+
pStyle: readVal(pStyleEl),
|
|
69
|
+
pStyleElement: pStyleEl,
|
|
70
|
+
outlineLvl: readVal(outlineEl),
|
|
71
|
+
outlineLvlElement: outlineEl
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Current paragraph properties: the paragraph's direct `w:pPr` only. */
|
|
76
|
+
export function getCurrentParagraphProperties(p) {
|
|
77
|
+
return readParagraphProps(getDirectWordChild(p, 'pPr'));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Current run properties: the run's direct `w:rPr` (null when absent). */
|
|
81
|
+
export function getCurrentRunProperties(r) {
|
|
82
|
+
return getDirectWordChild(r, 'rPr');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Paragraph-mark run properties (`w:pPr/w:rPr`), distinct from any run's `rPr`. */
|
|
86
|
+
export function getParagraphMarkProperties(p) {
|
|
87
|
+
return getDirectWordChild(getDirectWordChild(p, 'pPr'), 'rPr');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Historical paragraph properties: the `w:pPrChange/w:pPr` snapshot. Explicit opt-in; shape matches
|
|
92
|
+
* `getCurrentParagraphProperties`. Returns null when the paragraph has no tracked property change.
|
|
93
|
+
*/
|
|
94
|
+
export function getHistoricalParagraphProperties(p) {
|
|
95
|
+
const change = getDirectWordChild(getDirectWordChild(p, 'pPr'), 'pPrChange');
|
|
96
|
+
const snapshot = getDirectWordChild(change, 'pPr');
|
|
97
|
+
return snapshot ? readParagraphProps(snapshot) : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Historical run properties: the `w:rPrChange/w:rPr` snapshot, or null. */
|
|
101
|
+
export function getHistoricalRunProperties(r) {
|
|
102
|
+
const change = getDirectWordChild(getDirectWordChild(r, 'rPr'), 'rPrChange');
|
|
103
|
+
return getDirectWordChild(change, 'rPr');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** All `w:p` elements at or under `root` (the paragraph itself when `root` is one). */
|
|
107
|
+
function paragraphsUnder(root) {
|
|
108
|
+
if (!root || root.nodeType !== 1) return [];
|
|
109
|
+
const out = isWordElement(root, 'p') ? [root] : [];
|
|
110
|
+
if (typeof root.getElementsByTagNameNS === 'function') {
|
|
111
|
+
out.push(...Array.from(root.getElementsByTagNameNS(NS_W, 'p')));
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Current `w:numId` elements (direct `pPr/numPr/numId`) of every paragraph at or under `root`.
|
|
118
|
+
* Excludes `pPrChange` snapshots, so Reject still restores the snapshot numId.
|
|
119
|
+
*/
|
|
120
|
+
export function getCurrentNumIdElements(root) {
|
|
121
|
+
const out = [];
|
|
122
|
+
for (const p of paragraphsUnder(root)) {
|
|
123
|
+
const el = getCurrentParagraphProperties(p).numPr?.numIdElement;
|
|
124
|
+
if (el) out.push(el);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Every `w:numId` element referenced anywhere under `root`, INCLUDING `pPrChange` snapshots. Explicitly
|
|
131
|
+
* historical-inclusive: used when a numbering part must define every id the content can ever reference.
|
|
132
|
+
*/
|
|
133
|
+
export function getAllReferencedNumIdElements(root) {
|
|
134
|
+
if (!root || typeof root.getElementsByTagNameNS !== 'function') return [];
|
|
135
|
+
return Array.from(root.getElementsByTagNameNS(NS_W, 'numId'));
|
|
136
|
+
}
|