doc-codec 2.8.6 → 2.10.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/README.md +38 -28
- package/dist/fib/write.cjs +8 -0
- package/dist/fib/write.d.cts +16 -0
- package/dist/fib/write.d.ts +16 -0
- package/dist/fib/write.js +8 -0
- package/dist/headers-footers.cjs +5 -3
- package/dist/headers-footers.d.cts +5 -1
- package/dist/headers-footers.d.ts +5 -1
- package/dist/headers-footers.js +4 -4
- package/dist/index.cjs +7 -0
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/list/numbering.cjs +1 -3
- package/dist/list/numbering.js +1 -3
- package/dist/read.cjs +19 -2
- package/dist/read.js +19 -2
- package/dist/subdocument-write.cjs +108 -0
- package/dist/subdocument-write.d.cts +42 -0
- package/dist/subdocument-write.d.ts +42 -0
- package/dist/subdocument-write.js +104 -0
- package/dist/subdocument.cjs +20 -3
- package/dist/subdocument.js +21 -4
- package/dist/table/read.cjs +10 -7
- package/dist/table/read.d.cts +1 -1
- package/dist/table/read.d.ts +1 -1
- package/dist/table/read.js +10 -7
- package/dist/table/write.cjs +20 -1
- package/dist/table/write.js +20 -1
- package/dist/text/paragraphs.cjs +17 -1
- package/dist/text/paragraphs.js +18 -2
- package/dist/write.cjs +35 -5
- package/dist/write.d.cts +11 -2
- package/dist/write.d.ts +11 -2
- package/dist/write.js +35 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -24,30 +24,31 @@ Built and shipped, on the read side:
|
|
|
24
24
|
- **Document metadata** — `title`/`subject`/`author`/`keywords`/`createdIso`/`modifiedIso` read from a `"\x05SummaryInformation"` stream when one is present (see [Metadata](#metadata)); `comments` and `lastPrintedIso` remain unread, since `LayoutMetadata` has no field for either.
|
|
25
25
|
- **RC4-encrypted and XOR-obfuscated documents, decrypted for real (ExaDev/documents.js#1113, #922)** — `readDocContent`/`readDocStreams` take an optional `password`, verified against the Table stream's own `EncryptionHeader` for RC4, or against `FibBase.lKey` directly for XOR obfuscation (see [Encryption](#encryption)). RC4 CryptoAPI stays refused.
|
|
26
26
|
- **Numbering definitions** — `readDocContent`'s own `numbering` field: every list's glyph/format, level-text template, and start-at value, resolved from `PlfLst`/`PlfLfo` and keyed by the same `listId` a paragraph's `ContentParagraph.list.numId` already carries. See [Numbering definitions](#numbering-definitions), which also covers `writeDocContent`'s own inverse.
|
|
27
|
-
- **Every section's own page size and margins** — `prop/sep.ts`'s `readAllSectionProperties` resolves every `Sed` `PlcfSed` carries, each into its own `ContentSection.pageSize`/`.margins` (`sprmSXaPage`/`sprmSYaPage`/`sprmSDxaLeft`/`sprmSDxaRight`/`sprmSDyaTop`/`sprmSDyaBottom`), falling back field by field to Word's own new-document default (US Letter, one-inch margins) for a document carrying no `PlcfSed` at all, or for any one sprm a section leaves unstated. `read.ts`'s `splitIntoSections` groups the main document's own paragraphs by which section each falls in, per [MS-DOC] 2.8.26's own "each CP specifies the beginning of a range of text ... that constitutes a section" — a genuinely multi-section `.doc` now reads as multiple `ContentSection`s, each with its own real page geometry, rather than the whole document collapsing onto the first section's.
|
|
28
|
-
- **Footnotes, endnotes, and comments, as plain text** — `notes.ts`'s `readNoteBodies` resolves `PlcffndTxt`/`PlcfendTxt`/`PlcfandTxt` (each a CP-only plex dividing its own subdocument into stories, [MS-DOC]'s own "each CP ... specifies the beginning of a story ... the story ends immediately prior to the next CP") into `DocContent.footnotes`/`.endnotes`/`.comments`: one `{id, text}`/`{id, text}` entry per story, `id` a synthetic one-based ordinal, matching the same simplification `ooxml.js`'s own `DocxDocument.footnotes`/`.endnotes`/`.comments` already make for the identical constructs (plain text, no paragraph/run structure). A story's own trailing guard paragraph mark ("not considered part of the story contents", [MS-DOC]'s own words) is excluded from its text; a genuinely empty story reads as `""`.
|
|
29
|
-
- **Headers and footers, as real block flow** — `headers-footers.ts`'s `readHeaderFooterStories` resolves `Plcfhdd` (the identical CP-only-plex shape as the note-text plexes above) into `DocContent.headerFooterStories`: one entry per non-empty story, each carrying which section it belongs to, which of the six fixed slots it is (`evenHeader`/`oddHeader`/`evenFooter`/`oddFooter`/`firstHeader`/`firstFooter`, [MS-DOC]'s own fixed per-section story order), and a real `ContentBlock[]` — paragraphs and tables, read through the identical pipeline the main document uses, rather than plain text. An empty story ([MS-DOC]'s own "the beginning CP has the same value as the next CP") is left out of the array entirely, since it means "reuse the previous section's header/footer of this kind" rather than a genuinely blank one. The six footnote/endnote-separator stories Plcfhdd carries ahead of every section's own six are not read: neither this package's schema nor `ooxml.js`'s `DocxDocument` has anywhere to put a separator story.
|
|
27
|
+
- **Every section's own page size and margins** — `prop/sep.ts`'s `readAllSectionProperties` resolves every `Sed` `PlcfSed` carries, each into its own `ContentSection.pageSize`/`.margins` (`sprmSXaPage`/`sprmSYaPage`/`sprmSDxaLeft`/`sprmSDxaRight`/`sprmSDyaTop`/`sprmSDyaBottom`), falling back field by field to Word's own new-document default (US Letter, one-inch margins) for a document carrying no `PlcfSed` at all, or for any one sprm a section leaves unstated. `read.ts`'s `splitIntoSections` groups the main document's own paragraphs by which section each falls in, per [MS-DOC] 2.8.26's own "each CP specifies the beginning of a range of text ... that constitutes a section" — a genuinely multi-section `.doc` now reads as multiple `ContentSection`s, each with its own real page geometry, rather than the whole document collapsing onto the first section's. A 0x000C where no section ends reads as a real `pageBreak` block attached to the paragraph it terminates (`read.ts`'s `markManualPageBreaks`) — [MS-DOC]'s own PlcfSed.aCP text distinguishes the format's two spellings of that one character outright ("An end-of-section character (0x0C) which occurs at a CP and which is not the last character in a section specifies a manual page break"), and `writeDocContent` writes that exact shape back (see [Writing](#writing)).
|
|
28
|
+
- **Footnotes, endnotes, and comments, as plain text** — `notes.ts`'s `readNoteBodies` resolves `PlcffndTxt`/`PlcfendTxt`/`PlcfandTxt` (each a CP-only plex dividing its own subdocument into stories, [MS-DOC]'s own "each CP ... specifies the beginning of a story ... the story ends immediately prior to the next CP") into `DocContent.footnotes`/`.endnotes`/`.comments`: one `{id, text}`/`{id, text}` entry per story, `id` a synthetic one-based ordinal, matching the same simplification `ooxml.js`'s own `DocxDocument.footnotes`/`.endnotes`/`.comments` already make for the identical constructs (plain text, no paragraph/run structure). A story's own trailing guard paragraph mark ("not considered part of the story contents", [MS-DOC]'s own words) is excluded from its text when the story ends in one — and a story that ends with a content paragraph of its own is kept whole, the spelling a real producer writes for note stories (a LibreOffice-authored single-paragraph footnote ends at its own content mark with no separate guard; an earlier unconditional drop read every such note as `""` and lost a multi-paragraph note's last paragraph). A genuinely empty story reads as `""`.
|
|
29
|
+
- **Headers and footers, as real block flow** — `headers-footers.ts`'s `readHeaderFooterStories` resolves `Plcfhdd` (the identical CP-only-plex shape as the note-text plexes above) into `DocContent.headerFooterStories`: one entry per non-empty story, each carrying which section it belongs to, which of the six fixed slots it is (`evenHeader`/`oddHeader`/`evenFooter`/`oddFooter`/`firstHeader`/`firstFooter`, [MS-DOC]'s own fixed per-section story order), and a real `ContentBlock[]` — paragraphs and tables, read through the identical pipeline the main document uses, rather than plain text. An empty story ([MS-DOC]'s own "the beginning CP has the same value as the next CP") is left out of the array entirely, since it means "reuse the previous section's header/footer of this kind" rather than a genuinely blank one. The six footnote/endnote-separator stories Plcfhdd carries ahead of every section's own six are not read: neither this package's schema nor `ooxml.js`'s `DocxDocument` has anywhere to put a separator story. A genuine Word 97-authored file's own `Plcfhdd` can carry placeholder keys the published specification's own bounds forbid — `-1` entries mid-array and a key past the subdocument's own length — when the document carries next to no header/footer content at all; `subdocument.ts`'s `readStoryPlexKeys` normalises an out-of-range or descending key to its own in-range predecessor (the same "beginning CP equals the next CP" empty-story spelling the format already defines) rather than refusing the whole document, confirmed against a genuine Word 97 file carrying exactly this shape (`HeaderFooterProblematic.doc`) and matching what a real, independent [MS-DOC] implementation (LibreOffice) reads from the identical bytes.
|
|
30
30
|
- **Inline pictures** — `pictures.ts`'s `readInlinePicture` resolves a picture character (`U+0001`, `sprmCFSpec` applied) through its own Chpx's `sprmCPicLocation` operand: an offset into the container's `"Data"` stream where a `PICF` ([MS-DOC] 68-byte type/size/border header) is followed, for every producer this reader has seen, by a real `OfficeArtInlineSpContainer` ([MS-ODRAW] 2.2.15) regardless of `PICF.mfpf.mm`'s own value. The container's own `shape` (an `OfficeArtSpContainer`) is skipped whole by its own record header's `recLen`, and the first blip record in `rgfb` immediately after it is decoded when it is `OfficeArtBlipJPEG`/`OfficeArtBlipPNG` — the two raster formats `document-schema.js`'s `ContentImageBlock` can hold losslessly — into a real inline image, sized from `PICMID`'s own `dxaGoal`/`dyaGoal` scaled by `mx`/`my`. A paragraph containing a picture anchor is split around it into a real `ContentBlock[]` (`text/paragraphs.ts`'s `buildParagraphBlocks`), since an inline image is block-level in `document-schema.js`'s own model rather than a run property — mirroring how `ooxml.js`'s own docx reader splits a paragraph around a mid-run page break, paragraph-level formatting carried unchanged onto every split piece. See [Images](#images) for what this does not read.
|
|
31
|
+
- **`sprmPHugePapx`-indirected paragraph properties** — `text/paragraphs.ts`'s `resolveIndirectPapx` follows a paragraph grpprl that opens with `sprmPHugePapx` (0x6646) to the `PrcData` it names in the Data stream, up to 16 hops per [MS-DOC] 2.6.2's own chain-termination requirement, and reads that `PrcData`'s own `GrpPrl` in its place — the mechanism Word reaches for once a paragraph's direct formatting no longer fits its own 512-byte `PapxFkp` page. Confirmed against two genuine Word-2003-produced files whose wide table rows state their whole TAP this way rather than through a direct `sprmTDefTable` (`Bug61268.doc`'s seven-column history tables, `57603-seven_columns.doc`'s single seven-column row) — since a row's TAP rides its own row-mark paragraph's grpprl, resolving the indirection here resolves it for a table row exactly as it does for an ordinary paragraph, with no separate table-side code path needed. `sprmPTableProps` (the sibling indirection [MS-DOC] 2.4.3 also permits for a row's TAP) is not followed.
|
|
31
32
|
|
|
32
33
|
Built and shipped, on the write side — see [Writing](#writing) for the full scope statement:
|
|
33
34
|
|
|
34
|
-
- **`writeDocContent`** — a `'wordprocessing'` `ContentDocument` (any number of sections, each of paragraphs, tables, and
|
|
35
|
+
- **`writeDocContent`** — a `'wordprocessing'` `ContentDocument` (any number of sections, each of paragraphs, tables, inline images, and page breaks), widened by the optional story fields a `DocContent` read carries (footnotes/endnotes/comments as plain-text bodies, header/footer stories as real block flow — see [Writing](#writing)'s stories paragraph), to genuine [MS-DOC] bytes: a real piece table, real `ChpxFkp`/`PapxFkp` pages (splitting across as many as a document's own formatting needs, not just the common one-page case), a real style sheet minting one entry per distinct `styleId`/`headingLevel` a document's paragraphs actually use (`style/stsh.ts`'s `buildStshForStyles`, [ExaDev/documents.js#1059](https://github.com/ExaDev/documents.js/issues/1059)), a font table when a run names one, real `PlfLst`/`PlfLfo` numbering tables when at least one paragraph carries `list` (see [Numbering definitions](#numbering-definitions)), a real `PlcfSed`/`Sepx` array stating every section's own page size and margins (`ExaDev/documents.js#971`, see [Sections](#sections)), the four story subdocuments and their boundary plexes when the input carries stories (`subdocument-write.ts`, see [Subdocuments](#subdocuments)), a `"Data"` stream carrying every inline picture's own real `PICF`/`OfficeArtInlineSpContainer`/blip bytes (`ExaDev/documents.js#971`, see [Images](#images)), and a `"\x05SummaryInformation"` stream when the input's metadata carries anything that stream can hold (see [Metadata](#metadata)) — wrapped in a real [MS-CFB] compound file via `archive-codec`'s `writeCompoundFile`. A `ContentTable` block is expanded by `table/write.ts`'s `flattenSectionBlocks` into the same flat paragraph sequence every other block already is (see [Tables](#tables)), so table paragraphs flow through the identical `ChpxFkp`/`PapxFkp` paging as every other paragraph rather than a separate table-only path. Each cell's own `background` and `borders` ride along into the row's TAP (see [Cell decoration](#cell-decoration)). A `pageBreak` block is written as the format's own manual-page-break spelling of 0x000C (see [Writing](#writing)).
|
|
35
36
|
- Every property `writeDocContent` writes is verified by reading it back through this package's own `readDocContent` (`src/write.test.ts`), and additionally against a real, independent [MS-DOC] implementation: LibreOffice opened, rendered, and re-exported a `writeDocContent` sample without error or content loss, including bold/italic/underline/strike/size/colour/font-family runs, paragraph alignment and indentation, non-Latin-1 and non-BMP text (accented Latin, CJK, an emoji surrogate pair), a section's own page size and all four margins (a non-default 600x800pt page with a 90/54/45/36pt left/right/top/bottom margin, confirmed against LibreOffice's own `fo:page-width`/`fo:page-height`/`fo:margin-*` export to the exact point), and a table — recognised as a genuine `table:table`, its row/column/cell structure and both horizontal and vertical merges intact, matching real `table:number-columns-spanned`/`table:number-rows-spanned` attributes and `table:covered-table-cell` elements, and each cell's own background fill and per-side borders recovered with their exact colours, exactly as [Tables](#tables) confirms in full.
|
|
36
37
|
|
|
37
38
|
**Not built, and not approximated, on either side.** Each of these is a genuine layer of [MS-DOC] that this package does not implement; none is silently faked, and a document using one reads (or fails to write) as though it did not:
|
|
38
39
|
|
|
39
|
-
| Absent | Consequence
|
|
40
|
-
| ----------------------------------------------------------------------------------------- |
|
|
41
|
-
| **Nested tables, on write** | `readDocContent` recurses into a table nested inside a table cell to whatever depth the file states (see the read-side Tables bullet above and [Tables](#tables)). `writeDocContent` still refuses a `ContentTable` block found inside a table cell's own blocks: it has no depth-aware sgc-1/sgc-5 grpprl encoding for anything past depth 1, and writing one incorrectly would silently corrupt the nested table's own structure rather than fail loudly.
|
|
42
|
-
| **Floating/anchored drawn objects, text boxes, and every picture format beyond JPEG/PNG** | An inline picture (`U+0001`) round-trips when its own blip is `OfficeArtBlipJPEG`/`OfficeArtBlipPNG` (see the read-side Inline pictures bullet above, `writeDocContent`'s own `Data`-stream bullet, and [Images](#images) — `ExaDev/documents.js#971`). A floating or anchored drawn object (`U+0008`, `PlcfSpa`) is a genuinely different structure — an arbitrary `OfficeArtSpContainer` shape (which may or may not be a picture at all), reached through a document-level drawing tree (`OfficeArtDgContainer`) rather than a single inline blip — and is not resolved on read, nor written: `document-schema.js` has no block kind for a floating/anchored image distinct from an inline `ContentImageBlock` in the first place. A text box rides the identical drawing layer (`PlcfTxbxTxt`, itself anchored via a drawn object), so it is likewise not read or written. `ContentImageBlock.format`'s `svg`/`gif` members, and any blip format other than JPEG/PNG on read (a WMF/EMF/PICT metafile, a raw DIB, TIFF), have no `OfficeArtBlip` type this package's own writer can fabricate or its own reader can decode, so `readInlinePicture` returns `undefined` for one rather than mis-decoding it, and `writeDocContent` throws `DocUnsupportedError` naming the unwritable format rather than fabricating a blip its own reader could not read back.
|
|
43
|
-
| **A style's own formatting on write** | A style's own property sets (`STD.grLPUpxSw`) are resolved and folded on read ([ExaDev/documents.js#1005](https://github.com/ExaDev/documents.js/issues/1005)): a paragraph's formatting is its style's own grpprlPapx/grpprlChpx (walked up the full `istdBase` inheritance chain, most-specific style winning) plus its own direct exceptions on top, and a run's own referenced character style (`sprmCIstd`) folds in the same way. `writeDocContent` mints a real STSH entry for every distinct `styleId`/`headingLevel` a document's paragraphs use ([ExaDev/documents.js#1059](https://github.com/ExaDev/documents.js/issues/1059)), so style IDENTITY round-trips -- but every minted entry carries an empty `grLPUpxSw` (no formatting of its own), since doc-codec's flat `ContentDocument` has no style-vs-direct-formatting split to draw a real one from: every property this writer emits is already, unconditionally, a direct exception. A style's own inherited formatting therefore never round-trips through a write -- only its name and istd position do.
|
|
44
|
-
| **Table and numbering style formatting** | `STD.grLPUpxSw` is resolved only for paragraph and character styles (`stk` 1/2, see the row above) -- a table style's `StkTableGRLPUPX` (its own `TAPX` plus `CNFC`-conditional per-region formatting, [MS-DOC] 2.4.6.6's own table-specific steps 6-9) and a numbering style's `StkListGRLPUPX` are neither read nor written. A table applying a named table style, or a paragraph using a numbering style rather than direct list formatting, reports none of the formatting either would supply.
|
|
45
|
-
| **
|
|
46
|
-
| **Extended and user-defined document properties** | `title`/`subject`/`author`/`keywords`/`createdIso`/`modifiedIso` are read from and written to a `"\x05SummaryInformation"` stream when present (see [Metadata](#metadata)); the sibling `"\x05DocumentSummaryInformation"` stream (company, manager, and custom user-defined properties) is not read or written at all.
|
|
47
|
-
| **Encryption** | RC4-encrypted ([MS-DOC] 2.2.6.2, ExaDev/documents.js#1113) and XOR-obfuscated (2.2.6.1, #922) documents are both decrypted given a password -- `readDocContent`/`readDocStreams` take an optional `password`, verified against the Table stream's own EncryptionHeader for RC4, or against `FibBase.lKey` directly for XOR obfuscation (see [Encryption](#encryption) below). RC4 CryptoAPI (a different, newer EncryptionHeader shape) still refuses with a `DocUnsupportedError` rather than being read as plaintext or misread as the wrong scheme. `writeDocContent` never encrypts.
|
|
48
|
-
| **`sprmPHugePapx`
|
|
49
|
-
| **Hyperlinks and fields** |
|
|
50
|
-
| **Every FIB field beyond what this package's own reader needs** | `writeDocContent` populates only the fc/lcb pairs its own reader consults (the style sheet, the two property bin tables, the Clx, the font table). Roughly 140 other `FibRgFcLcb97` pairs — `SttbfAssoc`, `Dop`, the printer-driver structures among them — are left zero, which is the format's own "undefined, MUST be ignored" contract for most of them, but not a certification that every third-party [MS-DOC] reader accepts the result; see `fib/write.ts`'s own note.
|
|
40
|
+
| Absent | Consequence |
|
|
41
|
+
| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
42
|
+
| **Nested tables, on write** | `readDocContent` recurses into a table nested inside a table cell to whatever depth the file states (see the read-side Tables bullet above and [Tables](#tables)). `writeDocContent` still refuses a `ContentTable` block found inside a table cell's own blocks: it has no depth-aware sgc-1/sgc-5 grpprl encoding for anything past depth 1, and writing one incorrectly would silently corrupt the nested table's own structure rather than fail loudly. |
|
|
43
|
+
| **Floating/anchored drawn objects, text boxes, and every picture format beyond JPEG/PNG** | An inline picture (`U+0001`) round-trips when its own blip is `OfficeArtBlipJPEG`/`OfficeArtBlipPNG` (see the read-side Inline pictures bullet above, `writeDocContent`'s own `Data`-stream bullet, and [Images](#images) — `ExaDev/documents.js#971`). A floating or anchored drawn object (`U+0008`, `PlcfSpa`) is a genuinely different structure — an arbitrary `OfficeArtSpContainer` shape (which may or may not be a picture at all), reached through a document-level drawing tree (`OfficeArtDgContainer`) rather than a single inline blip — and is not resolved on read, nor written: `document-schema.js` has no block kind for a floating/anchored image distinct from an inline `ContentImageBlock` in the first place. A text box rides the identical drawing layer (`PlcfTxbxTxt`, itself anchored via a drawn object), so it is likewise not read or written. `ContentImageBlock.format`'s `svg`/`gif` members, and any blip format other than JPEG/PNG on read (a WMF/EMF/PICT metafile, a raw DIB, TIFF), have no `OfficeArtBlip` type this package's own writer can fabricate or its own reader can decode, so `readInlinePicture` returns `undefined` for one rather than mis-decoding it, and `writeDocContent` throws `DocUnsupportedError` naming the unwritable format rather than fabricating a blip its own reader could not read back. |
|
|
44
|
+
| **A style's own formatting on write** | A style's own property sets (`STD.grLPUpxSw`) are resolved and folded on read ([ExaDev/documents.js#1005](https://github.com/ExaDev/documents.js/issues/1005)): a paragraph's formatting is its style's own grpprlPapx/grpprlChpx (walked up the full `istdBase` inheritance chain, most-specific style winning) plus its own direct exceptions on top, and a run's own referenced character style (`sprmCIstd`) folds in the same way. `writeDocContent` mints a real STSH entry for every distinct `styleId`/`headingLevel` a document's paragraphs use ([ExaDev/documents.js#1059](https://github.com/ExaDev/documents.js/issues/1059)), so style IDENTITY round-trips -- but every minted entry carries an empty `grLPUpxSw` (no formatting of its own), since doc-codec's flat `ContentDocument` has no style-vs-direct-formatting split to draw a real one from: every property this writer emits is already, unconditionally, a direct exception. A style's own inherited formatting therefore never round-trips through a write -- only its name and istd position do. |
|
|
45
|
+
| **Table and numbering style formatting** | `STD.grLPUpxSw` is resolved only for paragraph and character styles (`stk` 1/2, see the row above) -- a table style's `StkTableGRLPUPX` (its own `TAPX` plus `CNFC`-conditional per-region formatting, [MS-DOC] 2.4.6.6's own table-specific steps 6-9) and a numbering style's `StkListGRLPUPX` are neither read nor written. A table applying a named table style, or a paragraph using a numbering style rather than direct list formatting, reports none of the formatting either would supply. |
|
|
46
|
+
| **Note references and separators, on write; text boxes, on either side** | Footnote/endnote/comment BODIES and full header/footer story block flow are now written (see [Writing](#writing)'s stories paragraph and `subdocument-write.ts`) — but the REFERENCE side is not: `PlcffndRef`/`PlcfandRef`/`PlcfendRef` and the `U+0002`/`U+0005` reference characters in the main document have no model to write from (the reader drops the anchors and keys note bodies by document-order ordinals), so a written note story carries no reference and a real producer renders it as unreferenced content rather than a live footnote — LibreOffice opens such a file without error and resolves its header/footer stories fully, while ignoring the unreferenced note stories. The six footnote/endnote-separator stories `Plcfhdd` leads with are written as genuinely empty stories, matching that neither side models one. Text boxes are not read or written in either direction: `PlcfTxbxTxt` divides its own subdocument into stories the identical way the note-text plexes above do, but a text box's own anchor rides the drawn-object mechanism ([Images](#images)'s own scope note), which this package does not resolve. |
|
|
47
|
+
| **Extended and user-defined document properties** | `title`/`subject`/`author`/`keywords`/`createdIso`/`modifiedIso` are read from and written to a `"\x05SummaryInformation"` stream when present (see [Metadata](#metadata)); the sibling `"\x05DocumentSummaryInformation"` stream (company, manager, and custom user-defined properties) is not read or written at all. |
|
|
48
|
+
| **Encryption** | RC4-encrypted ([MS-DOC] 2.2.6.2, ExaDev/documents.js#1113) and XOR-obfuscated (2.2.6.1, #922) documents are both decrypted given a password -- `readDocContent`/`readDocStreams` take an optional `password`, verified against the Table stream's own EncryptionHeader for RC4, or against `FibBase.lKey` directly for XOR obfuscation (see [Encryption](#encryption) below). RC4 CryptoAPI (a different, newer EncryptionHeader shape) still refuses with a `DocUnsupportedError` rather than being read as plaintext or misread as the wrong scheme. `writeDocContent` never encrypts. |
|
|
49
|
+
| **`sprmPTableProps`, and `sprmPHugePapx` on write** | `sprmPHugePapx`-indirected paragraph properties are now read (see the read-side bullet list above); `writeDocContent` never writes an indirect Papx, so a document whose own formatting would need one still throws the ordinary over-large-record `DocFormatError` rather than following the indirect path out. [MS-DOC] 2.4.3's own Overview of Tables text names `sprmPTableProps` as a further, real, legal alternative to `sprmTDefTable` a table row's own TAP may ride instead — a genuine LibreOffice-authored `.doc` table's own row mark was confirmed to prefer direct `sprmTDefTable` (see [ExaDev/documents.js#892](https://github.com/ExaDev/documents.js/issues/892)), matching 2.4.3's own compatibility guidance ("An application SHOULD use sprmTDefTable to define table cells for applications that do not process sprmPTableProps"), so this remains genuinely unread rather than merely unconfirmed. |
|
|
50
|
+
| **Hyperlinks and fields** | Fields are resolved, not modelled: everything between a field-begin (`U+0013`) and a field-separator (`U+0014`) is the instruction and is dropped; the result between the separator and the field-end (`U+0015`) is kept as ordinary run text (see [What is converted](#what-is-converted)) — and that resolved result text is what `writeDocContent` writes, so the field layer genuinely has nothing left to invert: no field structure exists in the model, and a field's displayed content round-trips as the plain runs it already read as. A hyperlink is a field in [MS-DOC] plus two further unimplemented layers — the field-begin character is tracked by a `Plcfld` in the Table stream, and the URL itself lives in a [MS-OSHARED] hyperlink property set whose `dwApp` names an index into that `Plcfld` ([MS-DOC] 2.4.7's own association rule) — none of which this package reads or writes in either direction, so a `ContentRun.hyperlink` a caller hands the writer is dropped (its text preserved, its URL lost) rather than approximated: writing a bare `HYPERLINK` field without the property layer would emit bytes this package's own reader cannot read back — the round-trip bar every other property this writer emits is held to — and even LibreOffice's own `.doc` export drops the URL of an ODT hyperlink entirely (its re-exported text carries no link at all), so there is no producer spelling to mirror that this package's own model could verify. Reference characters (footnote/comment/annotation anchors) are likewise dropped as plain text or structure, per the read-side rule. |
|
|
51
|
+
| **Every FIB field beyond what this package's own reader needs** | `writeDocContent` populates only the fc/lcb pairs its own reader consults (the style sheet, the two property bin tables, the Clx, the font table, the numbering tables, and the four story plexes when the document carries stories). Roughly 140 other `FibRgFcLcb97` pairs — `SttbfAssoc`, `Dop`, the printer-driver structures among them — are left zero, which is the format's own "undefined, MUST be ignored" contract for most of them, but not a certification that every third-party [MS-DOC] reader accepts the result; see `fib/write.ts`'s own note. |
|
|
51
52
|
|
|
52
53
|
One construct is refused rather than mis-read: a `sprmPChgTabs` whose `cb` is the `255` sentinel encodes its own length as a formula over tab-stop counts this package does not parse, and its length is needed to find the next `Prl`. Rather than guess and silently mis-read every property after it, `operandSize` throws.
|
|
53
54
|
|
|
@@ -94,7 +95,7 @@ A table in [MS-DOC] is not a separate container: it is a run of ordinary paragra
|
|
|
94
95
|
|
|
95
96
|
The one-point default is not applied unconditionally, because `MINLAY`'s own guarantee is LibreOffice's alone: this package's own writer widens nothing, so nothing stops a real producer's `rgdxaCenter` from stating a column genuinely narrower than a point, and folding that column's own two boundaries together as "drift" would silently delete it rather than fix a phantom one. The tolerance is therefore clamped, per table, to one twip below the narrowest strictly-positive gap any single row states between two of its _own_ adjacent boundaries (a zero-width gap is a legal adjacent-duplicate boundary, not a column, and is excluded) — two boundaries a row itself distinguishes are never folded together, however close, and the clamp can only ever be as generous as the tightest real column that table actually declares. Beyond that, [MS-DOC]'s own physical-cell model keeps every horizontally- and vertically-merged-away cell present in the text stream with its own cell mark and its own `TC80` entry — never omitted the way OOXML's `w:gridSpan` model omits a horizontally-merged-away `<w:tc>` outright — so a horizontal-continuation cell stated the legacy way (`TCGRF.horzMerge` = 1, still honoured for a genuine third-party producer that uses it) is folded into the preceding real cell's own `colSpan` exactly as before, while a genuinely narrower, wider physical cell (no flag, LibreOffice's own encoding) resolves to a `colSpan` greater than 1 directly from its own boundaries — both mechanisms produce the identical shape downstream. A vertical-continuation cell (`TCGRF.vertMerge` = `fvmMerge`) is kept as its own `{blocks: []}` entry — carrying its own `colSpan` too when it is also part of a horizontal-merge group in that row — with `rowSpan` computed on the anchor by scanning subsequent rows for a cell starting at the same position on the table's own shared grid, never a raw physical-array index, since two rows may genuinely have different physical cell counts and still need their vertical merges to line up correctly. Both conventions mirror `ooxml.js`'s own docx table reader exactly, since `colSpan`/`rowSpan`/`{blocks: []}` are precisely the shape `document-schema.js`'s `ContentTableCell` was designed to hold for either format's own cousin of the same merge model. A column boundary that no row in the table ever states on its own — every row happens to merge across it identically — cannot be recovered from the physical bytes at all; this is a genuine limitation of [MS-DOC]'s own physical model, not an approximation this reader chooses to make. This package's own writer closes that gap for an ordinary merge (see [Writing](#writing) below and [ExaDev/documents.js#992](https://github.com/ExaDev/documents.js/issues/992)): it falls back to a horizontal-merge continuation cell precisely when every row would otherwise merge across a boundary identically. But the writer's own lost-boundary fallback genuinely reopens the same gap when a row's assigned split overflows either the row-ending mark's own byte budget or the format's 63-physical-cell ceiling: `flattenTable` trims the excess boundaries rather than throwing ([ExaDev/documents.js#1013](https://github.com/ExaDev/documents.js/issues/1013); see the [Writing](#writing) section's own note on the trim fallback below), and a boundary it drops is unrecoverable on the next read exactly as if no row had ever stated it. That trim is now the most likely source of this shape; a table hand-built for a test, or one produced by a genuine third-party [MS-DOC] implementation that happens to encode a merge the identical way on every row, are the two remaining, rarer sources.
|
|
96
97
|
|
|
97
|
-
A row whose own TAP cannot be resolved this way — no direct `sprmTDefTable` anywhere in its grpprl (a producer may legally state it indirectly instead
|
|
98
|
+
A row whose own TAP cannot be resolved this way — no direct `sprmTDefTable` anywhere in its grpprl once `sprmPHugePapx` indirection is followed (a producer may legally state it indirectly instead through `sprmPTableProps`, which this reader still does not follow; see the `sprmPTableProps`/`sprmPHugePapx` scope row above), or a cell-mark count that disagrees with what its `TDefTableOperand` declares — degrades the _whole_ contiguous run of table-depth paragraphs back to flat paragraphs, rather than refusing the whole document: this is a legal, real-world construct this reader does not yet implement, not corruption, and paragraphs that would have become a table simply stay paragraphs instead, the identical class of degrade the scope row above already documents for `sprmPTableProps`. A run of table-flagged paragraphs that never closes a single row (a genuine Word 2000-authored title page confirmed to have exactly this shape — no cell or row mark anywhere in its own `sprmPFInTable` run) degrades the same way, provided the wider text stream continues past it; one dangling at the stream's very end is still the truncated-stream case below. A row ending mid-cell with no terminating mark at all is different in kind — the stream itself is truncated, not merely using an unsupported mechanism — and still throws `DocFormatError`.
|
|
98
99
|
|
|
99
100
|
**Writing** (`table/write.ts`'s `flattenSectionBlocks`, called from `write.ts`) is the inverse: a `ContentTable` block expands into its own real physical-cell paragraph stream, one physical cell per `ContentTableCell` — real content or a vertical-merge continuation's own `{blocks: []}` — never expanded into extra synthetic cells for a `colSpan` greater than 1. A `{blocks: []}` cell becomes a single empty paragraph, but is only written as a vertical-merge continuation (`TCGRF.vertMerge` = `fvmMerge`) when a vertical merge is genuinely still in progress at that column — tracked across rows by an `active` map keyed by column position, mirroring `ooxml.js`'s own `buildTable` exactly, since a genuinely blank cell has the identical `{blocks: []}` shape and inferring the merge from emptiness alone would silently mis-merge it with whatever real content sits above it; a continuation's own physical column span, likewise, always comes from the anchor's own recorded `colSpan` rather than the continuation cell's own (typically absent) one, so a cell merged both horizontally and vertically at once writes correctly instead of throwing. Every physical cell's own paragraphs carry `sprmPFInTable`; the row's own trailing mark additionally carries `sprmPFTtp` plus a single `sprmTDefTable` stating the row's own column layout and every cell's `TC80.tcgrf` vertical-merge state (`table/tap-write.ts`), and a `sprmTDyaRowHeight` when the row states a `heightPt`. Column widths are derived once from the table's own `columnWidthsPt`, giving every row a shared full grid of boundary points to draw from, but a row containing a horizontal merge writes its own narrower, wider `rgdxaCenter`: a `colSpan`-anchored cell's own physical boundary is the combined width of however many of the full grid's columns it spans, merged into one cell rather than kept as separate flagged ones. This is a deliberate match for how a real, independent [MS-DOC] implementation (LibreOffice 26.2.5.2) was confirmed to encode a horizontal merge — see the third-party verification paragraph below for the full ground-truth finding and [ExaDev/documents.js#895](https://github.com/ExaDev/documents.js/issues/895) for the issue it fixes. `TCGRF.horzMerge` is 0 for an ordinary merge like this one — no flag or `sprmTMerge` sprm is written for it — with one deliberate exception: before flattening any row, the writer first computes, across every row in the table, which of the table's own internal column boundaries at least one row's ordinary physical layout would state; a boundary none of them would (every row happens to merge across it identically — a single-row table with one merged cell is the simplest case) is kept physically present anyway, by splitting the cell that crosses it into an extra physical cell flagged as a genuine `TCGRF.horzMerge` continuation (contentless, per [MS-DOC] 2.9.317's own TCGRF: `horzMerge` value 1, "the cell is one of a set of horizontally merged cells. It contributes its layout region to the set and its own contents are not rendered") rather than folded into one wider cell. This is the fix for [ExaDev/documents.js#992](https://github.com/ExaDev/documents.js/issues/992): the fallback triggers only for the rows and boundaries that actually need it, so an ordinary table — one with at least one row that does not merge across the same span — writes exactly as before, and only the pathological case gains an extra physical cell purely to keep the boundary recoverable on read. The trade-off is real and worth stating plainly: LibreOffice was confirmed not to read `TCGRF.horzMerge` back as a merge at all (see the top-of-file note above), so a table this fallback applies to shows as unmerged, separate cells there — one of them empty — rather than as the single merged cell this package's own reader now correctly recovers. Given the alternative was `colSpan` coming back `undefined` and `columnWidthsPt` silently narrowing on every reader including this package's own, that trade is the honest one to make.
|
|
100
101
|
|
|
@@ -102,7 +103,7 @@ A row whose own TAP cannot be resolved this way — no direct `sprmTDefTable` an
|
|
|
102
103
|
|
|
103
104
|
The Main Document's own last character MUST be an ordinary paragraph mark ([MS-DOC]'s "Main Document" glossary entry: "The last character in the main document MUST be a paragraph mark (Unicode 0x000D)") — never the row-ending mark's own cell-mark character (0x0007), even though a row mark is a perfectly legal paragraph-boundary terminator everywhere else. `write.ts`'s own top-level `writeDocContent` — not this module — is what guarantees this: whenever `flattenSectionBlocks`' own output ends in anything other than an ordinary paragraph mark (an empty section, or, the case that matters here, a section whose very last block is a table), it appends one trailing empty paragraph so the table's own row mark is never the document's final character. This is the confirmed root cause of, and fix for, [ExaDev/documents.js#892](https://github.com/ExaDev/documents.js/issues/892) — see the third-party verification paragraph immediately below for the full finding.
|
|
104
105
|
|
|
105
|
-
**Third-party verification: passing for a plain table, a vertical merge, a horizontal merge, and a cell merged both ways at once.** [ExaDev/documents.js#892](https://github.com/ExaDev/documents.js/issues/892) tracked a genuine regression an earlier draft of this README had falsely certified as passing: LibreOffice's own `.doc` import filter recognised no table at all in a `writeDocContent` sample, in any configuration — every cell's text came back concatenated into one flat paragraph, with no `table:table` element anywhere in the converted output. The root cause was found by comparing this writer's own bytes against a genuine LibreOffice-authored `.doc`, byte for byte, rather than guessing: a LibreOffice 26.2.5.2-built `.odt` table converted to `.doc` (`soffice --headless --convert-to doc`) and its `WordDocument` stream parsed directly through this package's own `PapxFkp`/grpprl primitives shows LibreOffice's own row mark stating its TAP through the identical direct `sprmTDefTable` this writer already used — ruling out the indirect-Papx hypothesis #892 had raised (see the `sprmPHugePapx
|
|
106
|
+
**Third-party verification: passing for a plain table, a vertical merge, a horizontal merge, and a cell merged both ways at once.** [ExaDev/documents.js#892](https://github.com/ExaDev/documents.js/issues/892) tracked a genuine regression an earlier draft of this README had falsely certified as passing: LibreOffice's own `.doc` import filter recognised no table at all in a `writeDocContent` sample, in any configuration — every cell's text came back concatenated into one flat paragraph, with no `table:table` element anywhere in the converted output. The root cause was found by comparing this writer's own bytes against a genuine LibreOffice-authored `.doc`, byte for byte, rather than guessing: a LibreOffice 26.2.5.2-built `.odt` table converted to `.doc` (`soffice --headless --convert-to doc`) and its `WordDocument` stream parsed directly through this package's own `PapxFkp`/grpprl primitives shows LibreOffice's own row mark stating its TAP through the identical direct `sprmTDefTable` this writer already used — ruling out the indirect-Papx hypothesis #892 had raised (see the `sprmPTableProps`/`sprmPHugePapx` scope row above) — a hypothesis a later genuine Word-produced corpus did in fact confirm for other files, once `sprmPHugePapx` resolution existed to check it against (see the read-side bullet list's own note). The actual difference was the document's own last character: LibreOffice's file ends in a genuine paragraph mark (0x000D) after the table's own row-ending cell mark, while this writer's output ended the whole text stream _at_ the row mark itself (0x0007) — violating [MS-DOC]'s own "Main Document" glossary entry ("The last character in the main document MUST be a paragraph mark") outright. Restoring that trailing paragraph mark (see the note above) with no other change fixed table recognition completely; reverting it (verified by hand) reproduces the original failure exactly.
|
|
106
107
|
|
|
107
108
|
The horizontal-merge gap #892 left open ([ExaDev/documents.js#895](https://github.com/ExaDev/documents.js/issues/895)) was root-caused the same way: round-tripping a LibreOffice-authored horizontal merge through its own `.doc` writer and parsing the result's raw TAP bytes with this package's own primitives shows LibreOffice does not use `TC80.tcgrf.horzMerge` _or_ `sprmTMerge` for a horizontal merge at all — the merged row's own `TDefTableOperand` genuinely has fewer, wider physical cells (`rgdxaCenter = [0, 6425, 9638]`, 2 physical cells, both `TCGRF.horzMerge = 0`) than an unmerged row in the same table (`rgdxaCenter = [0, 3212, 6425, 9638]`, 3 cells), a real per-row column layout [MS-DOC] 2.6.3 permits ("There is no requirement that each row of a table have the same number of cells"). This writer now matches that encoding (see [Writing](#writing) above) and the reader reconstructs `colSpan` from it (see the Reading paragraph above). Verified against LibreOffice 26.2.5.2 (`soffice --headless --convert-to fodt`, checking for real `table:table`/`table:table-row`/`table:table-cell` elements) for four cases: a plain 2x2 unmerged table (**passes** — genuine `table:table` structure, correct cell text and column count); a vertically merged cell (**passes** — `table:number-rows-spanned="2"` on the anchor cell and a real `table:covered-table-cell` on the row below); a horizontally merged cell (**passes** — `table:number-columns-spanned="2"` on the anchor cell and a real `table:covered-table-cell` beside it, with the table's other, unmerged row confirming 3 real columns); and a cell merged both horizontally and vertically at once (**passes** — `table:number-rows-spanned="2" table:number-columns-spanned="2"` together on the anchor, with two `table:covered-table-cell` elements on the row below it). No case regressed against the other: the same writer output that produces the merges above still passes the plain-table and vertical-merge checks unchanged.
|
|
108
109
|
|
|
@@ -144,7 +145,7 @@ One narrow accuracy limit follows from the same missing field. `rgdxaCenter`'s e
|
|
|
144
145
|
|
|
145
146
|
A paragraph's own `list.numId`/`list.level` (`sprmPIlfo`/`sprmPIlvl`, unchanged by this section) say WHICH list a paragraph belongs to and WHAT DEPTH within it -- they say nothing about what that list actually looks like. `readDocContent`'s own `numbering` field is that: keyed by the same `listId` string `numId` already carries, each entry names every level's glyph/format, level-text template, and start-at value, resolved from `PlfLst` (the list definitions, `LSTF` plus each one's appended array of `LVL`s) and `PlfLfo` (which list a paragraph's own `ilfo` actually refers to). `list/numbering.ts`'s `readNumberingDefinitions` is the whole implementation; `read.ts`'s `DocContent` is `ContentDocument` widened by exactly this one field, so every existing caller expecting a plain `ContentDocument` is unaffected.
|
|
146
147
|
|
|
147
|
-
**Deliberately shaped like ooxml.js's own numbering, not document-schema.js's.** `NumberingDefinition`/`NumberingLevel` are doc-codec's own types, not a `document-schema.js` addition: `ContentListMembership` is shared verbatim across every codec in this family, and widening it with a doc-codec-specific numbering-definition payload would leak this package's own model into a schema the sibling packages also depend on -- exactly the reasoning `ooxml.js`'s own `typed/docx/numbering.ts` states for `word/numbering.xml`'s `abstractNum`/`num` tables, which this module deliberately mirrors rather than reinvents. `NumberingLevel.format` is the identical ECMA-376 `ST_NumberFormat` string ooxml.js's own field already carries (`"decimal"`, `"upperRoman"`, `"bullet"`, ...) -- [MS-OSHARED] 2.2.1.3's own `MSONFC` enumeration documents each value as "mapped to the `ST_NumberFormat`... equivalent", so this reader uses that same mapping rather than inventing a second vocabulary. `NumberingLevel.text` is the identical `'%1.'`/`'%2)'`-style placeholder convention: `[MS-DOC]`'s own `Xst`/`rgbxchNums` encoding names a placeholder by which _character position_ in the level's text is a raw, zero-based level index rather than literal content, and `readLevelText` converts that into the one-based `%N` spelling ooxml.js's own `w:lvlText` values already use -- so a consumer that already resolves one already resolves the other.
|
|
148
|
+
**Deliberately shaped like ooxml.js's own numbering, not document-schema.js's.** `NumberingDefinition`/`NumberingLevel` are doc-codec's own types, not a `document-schema.js` addition: `ContentListMembership` is shared verbatim across every codec in this family, and widening it with a doc-codec-specific numbering-definition payload would leak this package's own model into a schema the sibling packages also depend on -- exactly the reasoning `ooxml.js`'s own `typed/docx/numbering.ts` states for `word/numbering.xml`'s `abstractNum`/`num` tables, which this module deliberately mirrors rather than reinvents. `NumberingLevel.format` is the identical ECMA-376 `ST_NumberFormat` string ooxml.js's own field already carries (`"decimal"`, `"upperRoman"`, `"bullet"`, ...) -- [MS-OSHARED] 2.2.1.3's own `MSONFC` enumeration documents each value as "mapped to the `ST_NumberFormat`... equivalent", so this reader uses that same mapping rather than inventing a second vocabulary. An `LVLF.nfc` outside that table's own range reads as `"decimal"` rather than refusing the whole document -- one byte of one level's own number format is a property-level fact, not structural corruption, the identical degrade principle every unconverted sprm elsewhere in this package already follows -- matching the default the one independent [MS-DOC] implementation checked applies (LibreOffice's `WW8ListManager::GetSvxNumTypeFromMSONFC` falls through to `SVX_NUM_ARABIC`), confirmed against a genuine Word-produced `.doc` carrying exactly such a value (`nfc` 0x92, well outside `MSONFC`'s 0x00-0x3B/0xFF range). `NumberingLevel.text` is the identical `'%1.'`/`'%2)'`-style placeholder convention: `[MS-DOC]`'s own `Xst`/`rgbxchNums` encoding names a placeholder by which _character position_ in the level's text is a raw, zero-based level index rather than literal content, and `readLevelText` converts that into the one-based `%N` spelling ooxml.js's own `w:lvlText` values already use -- so a consumer that already resolves one already resolves the other.
|
|
148
149
|
|
|
149
150
|
**Written too, as of this package's own inverse -- `ooxml.js`'s own docx writer is unaffected.** `list/numbering-write.ts` is `list/numbering.ts`'s inverse: `gatherListUsage` walks the document's own flattened paragraph sequence (table cells included, so a list used only inside a cell still resolves) and reconstructs a real `NumberingDefinitions` from each paragraph's `list.numId`/`.level`/`.format` -- `ContentListMembership` carries no full level table of its own, only one paragraph's own membership, so the writer mints one from what the document's paragraphs actually use, assigning a fresh one-based `ilfo` to each distinct `numId` in first-occurrence order (exactly the value `numbering.ts`'s own reader would assign it back on a re-read, since that reader's `numId` IS the `ilfo`, stringified). `buildNumberingTables` then encodes that `NumberingDefinitions` into real `PlfLst`/`PlfLfo` bytes, and `write.ts` places both in the Table stream and populates `FibRgFcLcb97.fcPlfLst`/`lcbPlfLst`/`fcPlfLfo`/`lcbPlfLfo` -- left at 0/0, matching what `readNumberingDefinitions` itself treats as "no lists", for a document with no `list` membership at all. `buildNumberingTables` is independent of `gatherListUsage` -- a hand-built `NumberingDefinitions` round-trips through it too, since every `LVLF` field it writes comes from the definition's own `NumberingLevel` rather than an assumed default -- and validates accordingly: a definition keyed by a string that collides with another key once both are converted to a number (`"1"` and `"01"` both become the lsid `1`, even though `Object.keys` never canonicalises `"01"` the way it does a true integer-index key) throws a `DocFormatError` naming both colliding keys, since [MS-DOC] 2.9.147 states `lsid` "MUST be unique for each LSTF" and `readNumberingDefinitions` resolves an `LFO` to its `LSTF` purely by matching `lsid` -- a silent collision would make the colliding lists indistinguishable on the next read. `gatherListUsage`'s own minted keys (`ilfoByNumId.size + 1`, stringified) can never collide this way, so the check is reachable only through a hand-built `NumberingDefinitions`, the same route every other `buildNumberingTables` refusal below is reachable through. `pap-write.ts`'s `encodeParagraphGrpprl` takes the resulting numId-to-ilfo map as its own `ilfoOf` parameter to write each paragraph's `sprmPIlfo`/`sprmPIlvl`. A round trip through this package alone is stable, but a `numId` minted by a _different_ producer or codec is not preserved verbatim -- [MS-DOC] addresses a list by its one-based `ilfo`, not an opaque identifier, so it is renumbered to whichever `ilfo` this document's own write happens to mint it. `word/numbering.xml` is a separate package's separate decision: it is read into `DocxDocument.numbering` but never written back by `ooxml.js`'s own docx writer (that package's own stated write scope), and nothing here changes that.
|
|
150
151
|
|
|
@@ -170,7 +171,7 @@ Only page size and the four margins are resolved, matching what `document-schema
|
|
|
170
171
|
|
|
171
172
|
## Subdocuments
|
|
172
173
|
|
|
173
|
-
The main document is only the first of several logical text ranges [MS-DOC] 2.4.1 concatenates into one CP space, in this fixed order: the main document (`0` to `ccpText`), the footnote document (`ccpFtn` characters), the header document (`ccpHdd`), the comment/annotation document (`ccpAtn`), the endnote document (`ccpEdn`), and the textbox documents (`ccpTxbx`/`ccpHdrTxbx`, not read at all — see below). `subdocument.ts`'s `readSubdocumentStories` is the one function every subdocument this package reads goes through: it reads a subdocument's own CP range via the identical `readTextRange`/`readParagraphs` pipeline the main document uses, then splits the resulting flat paragraph sequence by a boundary plex's own CPs (`text/paragraphs.ts`'s `splitEntriesByBoundaries`) into one group per story. `PlcffndTxt`/`PlcfandTxt`/`PlcfendTxt`/`Plcfhdd` all share the identical shape and the identical two conventions this one function handles once: "the story is considered empty" when two consecutive CPs are equal (skipped past, never assigned any entry — a run of several empty stories in a row is handled correctly, not just a single one), and a trailing CP the structure's own page states is "undefined and MUST be ignored" (the last group `readSubdocumentStories` produces is always dropped). A non-empty story's own final paragraph
|
|
174
|
+
The main document is only the first of several logical text ranges [MS-DOC] 2.4.1 concatenates into one CP space, in this fixed order: the main document (`0` to `ccpText`), the footnote document (`ccpFtn` characters), the header document (`ccpHdd`), the comment/annotation document (`ccpAtn`), the endnote document (`ccpEdn`), and the textbox documents (`ccpTxbx`/`ccpHdrTxbx`, not read at all — see below). `subdocument.ts`'s `readSubdocumentStories` is the one function every subdocument this package reads goes through: it reads a subdocument's own CP range via the identical `readTextRange`/`readParagraphs` pipeline the main document uses, then splits the resulting flat paragraph sequence by a boundary plex's own CPs (`text/paragraphs.ts`'s `splitEntriesByBoundaries`) into one group per story. `PlcffndTxt`/`PlcfandTxt`/`PlcfendTxt`/`Plcfhdd` all share the identical shape and the identical two conventions this one function handles once: "the story is considered empty" when two consecutive CPs are equal (skipped past, never assigned any entry — a run of several empty stories in a row is handled correctly, not just a single one), and a trailing CP the structure's own page states is "undefined and MUST be ignored" (the last group `readSubdocumentStories` produces is always dropped). A non-empty story's own final paragraph is dropped only when it is a bare paragraph mark with no content of its own — the guard "not considered part of the story contents" — because the two legal spellings differ exactly there: a header story ends with its content's own final mark PLUS that guard ([MS-DOC]'s Headers page: "if the story contents require a paragraph mark themselves, a second paragraph mark MUST be used"), while a real producer's note stories end at their own content mark with no second one (confirmed against a LibreOffice-authored `.doc`, and matching PlcffndTxt's own "The range of text MUST end in character 0x0D immediately before the next CP", which that content mark already satisfies). An earlier unconditional drop of every story's final paragraph read the second spelling as though it were the first, losing a single-paragraph note entirely and a multi-paragraph note's last paragraph.
|
|
174
175
|
|
|
175
176
|
**Footnotes, endnotes, and comments** (`notes.ts`) resolve to `DocContent.footnotes`/`.endnotes`/`.comments`: one `{id, text}` entry per story, `id` a synthetic one-based ordinal in document order (this reader does not resolve `PlcffndRef`/`PlcfandRef`/`PlcfendRef`'s own reference data — the auto-numbering/custom-symbol state a real footnote mark carries — only the order its own text plex already states), `text` the story's own paragraphs joined by `\n`. This is a deliberate simplification matching `ooxml.js`'s own `DocxDocument.footnotes`/`.endnotes`/`.comments`, which carry the identical flat-text shape for the identical constructs rather than real block flow.
|
|
176
177
|
|
|
@@ -178,7 +179,7 @@ The main document is only the first of several logical text ranges [MS-DOC] 2.4.
|
|
|
178
179
|
|
|
179
180
|
**Not read at all: text boxes.** `PlcfTxbxTxt`/`PlcfHdrtxbxTxt` divide their own subdocument into stories the identical way, but a text box's own anchor is a drawn object (`U+0008`, `PlcfSpa`) — the same drawing-layer mechanism [Images](#images) below does not resolve — so there is no way to reach a text box's own story from the main document's text at all without that layer, and this package does not attempt it.
|
|
180
181
|
|
|
181
|
-
|
|
182
|
+
This subdocument side IS written, symmetrically with the read: `writeDocContent`'s input is widened by the same four fields `DocContent` carries (`WritableDocContent`, each optional — a plain `ContentDocument` writes no subdocument at all), and `subdocument-write.ts` appends the footnote, header, comment, and endnote subdocuments after the main document's own text in [MS-DOC]'s own concatenation order, each with its genuine boundary plex and its `ccp` in the FIB. See [Writing](#writing)'s stories paragraph for the layout facts and the one deliberate absence (note references).
|
|
182
183
|
|
|
183
184
|
## Images
|
|
184
185
|
|
|
@@ -227,16 +228,20 @@ XOR obfuscation (Method 2) needs `archive-codec`'s own `createXorObfuscationKey`
|
|
|
227
228
|
|
|
228
229
|
## Writing
|
|
229
230
|
|
|
230
|
-
`writeDocContent` takes a `'wordprocessing'` `ContentDocument` with one or more sections, an optional `WriteDocContentOptions`, and produces real [MS-DOC] bytes wrapped in a real [MS-CFB] compound file, inverting every read-side structure listed above: a real piece table (`text/piece-table-write.ts`, always one uncompressed 16-bit piece — see [Why always uncompressed](#why-the-writer-always-writes-uncompressed-text)), `Sprm`-encoded grpprls for each run's and paragraph's own direct formatting (`prop/chp-write.ts`, `prop/pap-write.ts`), `ChpxFkp`/`PapxFkp` pages packed and split across as many 512-byte pages as the content needs (`prop/fkp-write.ts`), a real style sheet minting one entry per distinct `styleId`/`headingLevel` a document's paragraphs actually use (`style/stsh.ts`'s `buildStshForStyles`, [ExaDev/documents.js#1059](https://github.com/ExaDev/documents.js/issues/1059) — identity only, no formatting of a style's own; `FibRgFcLcb97.lcbStshf` "MUST be a nonzero value", so a document is never written without one, even one carrying zero styles, though this package's own reader tolerates a missing one), a font table when at least one run names a font (`style/fonts.ts`), a real `PlcfSed`/`Sepx` array with one entry per section (`prop/sep-write.ts`'s `buildPlcfSed`, `ExaDev/documents.js#971` — see [Sections](#sections)), and a `"Data"` stream carrying every inline picture's own real bytes when the document has at least one (`pictures-write.ts`'s `buildInlinePicture`, `data-stream.ts`'s `DataStreamBuilder`, `ExaDev/documents.js#971` — see [Images](#images)). `WriteDocContentOptions.onWarning`, when given, is called for a non-fatal write-time degradation this writer chooses over throwing — today, only the lost-boundary fallback's own per-row budget check (see [Tables](#tables)'s own note on [ExaDev/documents.js#1013](https://github.com/ExaDev/documents.js/issues/1013)) — but that is not a guarantee the write itself goes on to succeed: a row whose own assigned lost boundaries cannot be trimmed down to a split that fits at all still reports one warning, describing its boundaries as unrecoverable and its fully-unsplit encoding as the fallback being attempted — and, should that unsplit encoding also overflow the row's own byte budget once `buildPapxPages` actually packs it, `writeDocContent` can still throw further down the same pipeline, after any rows still to come have had their own chance to report a warning: a caller can genuinely see this warning followed by a hard failure, but not necessarily right away, and not necessarily for the row whose warning it just read, since `buildPapxPages` packs paragraphs in document order and can fail first on an earlier row that also overflowed. Every genuine refusal this writer makes outright — the kinds named in the table below — still throws `DocFormatError`/`DocUnsupportedError` directly, with no warning first.
|
|
231
|
+
`writeDocContent` takes a `'wordprocessing'` `ContentDocument` with one or more sections, an optional `WriteDocContentOptions`, and produces real [MS-DOC] bytes wrapped in a real [MS-CFB] compound file, inverting every read-side structure listed above: a real piece table (`text/piece-table-write.ts`, always one uncompressed 16-bit piece — see [Why always uncompressed](#why-the-writer-always-writes-uncompressed-text)), `Sprm`-encoded grpprls for each run's and paragraph's own direct formatting (`prop/chp-write.ts`, `prop/pap-write.ts`), `ChpxFkp`/`PapxFkp` pages packed and split across as many 512-byte pages as the content needs (`prop/fkp-write.ts`), a real style sheet minting one entry per distinct `styleId`/`headingLevel` a document's paragraphs actually use (`style/stsh.ts`'s `buildStshForStyles`, [ExaDev/documents.js#1059](https://github.com/ExaDev/documents.js/issues/1059) — identity only, no formatting of a style's own; `FibRgFcLcb97.lcbStshf` "MUST be a nonzero value", so a document is never written without one, even one carrying zero styles, though this package's own reader tolerates a missing one), a font table when at least one run names a font (`style/fonts.ts`), a real `PlcfSed`/`Sepx` array with one entry per section (`prop/sep-write.ts`'s `buildPlcfSed`, `ExaDev/documents.js#971` — see [Sections](#sections)), the four story subdocuments and their boundary plexes when the input carries stories (`subdocument-write.ts` — see the stories paragraph below and [Subdocuments](#subdocuments)), and a `"Data"` stream carrying every inline picture's own real bytes when the document has at least one (`pictures-write.ts`'s `buildInlinePicture`, `data-stream.ts`'s `DataStreamBuilder`, `ExaDev/documents.js#971` — see [Images](#images)). The input is a `WritableDocContent` — a `ContentDocument` widened by the same optional story fields `DocContent` reads (see the stories paragraph below) — so a plain `ContentDocument` still writes exactly as it always did. `WriteDocContentOptions.onWarning`, when given, is called for a non-fatal write-time degradation this writer chooses over throwing — today, only the lost-boundary fallback's own per-row budget check (see [Tables](#tables)'s own note on [ExaDev/documents.js#1013](https://github.com/ExaDev/documents.js/issues/1013)) — but that is not a guarantee the write itself goes on to succeed: a row whose own assigned lost boundaries cannot be trimmed down to a split that fits at all still reports one warning, describing its boundaries as unrecoverable and its fully-unsplit encoding as the fallback being attempted — and, should that unsplit encoding also overflow the row's own byte budget once `buildPapxPages` actually packs it, `writeDocContent` can still throw further down the same pipeline, after any rows still to come have had their own chance to report a warning: a caller can genuinely see this warning followed by a hard failure, but not necessarily right away, and not necessarily for the row whose warning it just read, since `buildPapxPages` packs paragraphs in document order and can fail first on an earlier row that also overflowed. Every genuine refusal this writer makes outright — the kinds named in the table below — still throws `DocFormatError`/`DocUnsupportedError` directly, with no warning first.
|
|
231
232
|
|
|
232
233
|
Character properties this writer converts, the exact inverse of [What is converted](#what-is-converted)'s character table above: `bold`, `italic`, `strike`, `underline` (as `kulSingle`, the only style a plain boolean can express), `sizePt`, `color` (via `sprmCCv`'s exact `COLORREF`, never the lossy 17-entry `sprmCIco` palette), and `fontFamily`. Paragraph properties: `alignment` (the four `ST_Jc`-aligned values this package's reader itself maps — `left`/`center`/`right`/`justify`), `indentLeftPt`, `indentRightPt`, `indentFirstLinePt`, `spacingBeforePt`, `spacingAfterPt`, `lineSpacing` (only `LSPD`'s multiplier form, matching the reader), `pageBreakBefore`, `styleId`/`headingLevel` (via the minted STSH entry above), and `list` membership (`sprmPIlfo`/`sprmPIlvl` — see [Numbering definitions](#numbering-definitions) for the numbering tables this depends on).
|
|
233
234
|
|
|
235
|
+
**A `pageBreak` block is written as the format's own manual page break** — the end-of-section character (0x000C) placed where no section ends, the exact shape [MS-DOC]'s own PlcfSed.aCP text defines: "An end-of-section character (0x0C) which occurs at a CP and which is not the last character in a section specifies a manual page break." The two spellings of that one character are distinguished purely by PlcfSed's own boundaries, so read.ts's `markManualPageBreaks` decodes a 0x000C-terminated paragraph whose end CP is not a following section's start CP into `[paragraph, pageBreak]`, and `table/write.ts`'s `appendPageBreak` writes the inverse: a page break retargets the preceding paragraph's own terminator from an ordinary paragraph mark to 0x000C, which is why the ordinary case (`[paragraph alpha, pageBreak, paragraph beta]`) round-trips exactly with no stray empty paragraph — the break rides a paragraph terminator the format already requires there. A page break with no ordinary paragraph before it to carry it (a section's first block, directly after a table's row mark, or directly after another page break) becomes its own empty 0x000C-terminated paragraph instead, visible in the round trip as a `[paragraph "", pageBreak]` pair the input did not state: a page break in [MS-DOC] always terminates SOME paragraph, so a modelled break with nothing before it necessarily mints one — a faithful statement of the format's own limit rather than a loss. Verified against a real, independent [MS-DOC] implementation: LibreOffice opens a `writeDocContent` sample carrying a page break between two paragraphs and re-exports it with a genuine `fo:break-before="page"` on the paragraph after the break, both paragraphs' text intact.
|
|
236
|
+
|
|
237
|
+
**Story subdocuments are written, symmetrically with the read** — `writeDocContent`'s input is widened from a bare `ContentDocument` to a `WritableDocContent`: the same four story fields `readDocContent`'s own `DocContent` output carries (`footnotes`/`endnotes`/`comments` as `{id, text}` bodies, `headerFooterStories` as real block flow), each optional, exactly the input shape `ooxml.js`'s own `DocxContent` established for the identical constructs — so a genuine `DocContent` assigns straight across and a plain `ContentDocument` writes no subdocument at all. `subdocument-write.ts` appends the footnote, header, comment, and endnote subdocuments after the main document's own text in [MS-DOC]'s own concatenation order, states each one's `ccp` in the FIB, and writes each one's genuine CP-only boundary plex (`PlcffndTxt`/`PlcfHdd`/`PlcfandTxt`/`PlcfendTxt`). Every layout fact is the one a real producer writes, each confirmed against a LibreOffice-authored `.doc` before being implemented: each non-empty story's span ends with its content's own final mark plus the one guard mark [MS-DOC]'s Headers page mandates, the subdocument carries exactly one further paragraph mark beyond the last story (what makes each plex's own "second-to-last CP MUST be equal to ccp - 1" satisfiable), note text is split into paragraphs at the same newlines the reader joined on (so a note whose text ends in `\n` round-trips exactly, its trailing empty paragraph protected by the writer's separate guard), a header story flattens through the identical `flattenSectionBlocks` the main document uses (tables and inline pictures included, sharing the one `"Data"` stream), an absent slot stays a genuinely empty story — the "reuse the previous section's header/footer of this kind" meaning the reader gives one — while a story the model carries is written as present even when its blocks flatten to nothing, and a story naming a section the document does not have, or a second story for one slot, throws `DocFormatError`. The one deliberate absence is the REFERENCE side: `PlcffndRef`/`PlcfandRef`/`PlcfendRef` and the `U+0002`/`U+0005` reference characters in the main document are not written, because the model carries no reference positions to write them from (the reader drops the anchors; note `id`s are document-order ordinals) — a written note story is honestly unreferenced content rather than a fake live footnote. Verified against a real, independent [MS-DOC] implementation: LibreOffice opens a `writeDocContent` sample carrying footnotes, endnotes, comments, a header, and a footer without error, resolves the header and footer stories in full (their text appears in the re-exported master page's `style:header`/`style:footer`), and ignores the unreferenced note stories; a full read→write→read cycle over a LibreOffice-authored `.doc` reproduces every note body and every (section, slot) story exactly.
|
|
238
|
+
|
|
234
239
|
**Deliberately not handled**, beyond what the read-side scope table above already states applies to both directions:
|
|
235
240
|
|
|
236
|
-
| Absent | Consequence
|
|
237
|
-
| ---------------------------------------------- |
|
|
238
|
-
| **Non-paragraph, non-table, non-image blocks** |
|
|
239
|
-
| **An empty `ContentSection.blocks`** | Written as a single paragraph with no runs — [MS-DOC] 2.4.2 requires the Main Document's own text to end in a paragraph mark, so an otherwise-empty section still needs one to hold it, exactly as a real producer's own blank document has one.
|
|
241
|
+
| Absent | Consequence |
|
|
242
|
+
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
243
|
+
| **Non-paragraph, non-table, non-image blocks** | An `embeddedObject` or construct-boundary marker (`constructStart`/`constructEnd`) block throws `DocUnsupportedError` naming its own `kind` — `ContentTable`, `ContentImageBlock`, and `pageBreak` are now written (see [Tables](#tables), [Images](#images), and the page-break paragraph below); a non-paragraph block found inside one of a table's own cells throws the identical error. An embedded object rides the same `PlcfSpa`/`OfficeArtDgContainer` drawing-tree mechanism this package's reader does not resolve at all (see the drawn-objects row above); a construct-boundary marker has no [MS-DOC] representation of its own to target until the cross-format `document-schema.js` design work tracked on [ExaDev/documents.js#1122](https://github.com/ExaDev/documents.js/issues/1122) lands. |
|
|
244
|
+
| **An empty `ContentSection.blocks`** | Written as a single paragraph with no runs — [MS-DOC] 2.4.2 requires the Main Document's own text to end in a paragraph mark, so an otherwise-empty section still needs one to hold it, exactly as a real producer's own blank document has one. |
|
|
240
245
|
|
|
241
246
|
### Why the writer always writes uncompressed text
|
|
242
247
|
|
|
@@ -279,7 +284,9 @@ The modules layer in the order [MS-DOC]'s own algorithms chain:
|
|
|
279
284
|
| `src/metadata.ts` | Wraps `archive-codec`'s own `SummaryInformationProperties` <-> `LayoutMetadata` mapping with this package's `createdIso`/`modifiedIso` date validation, throwing `DocFormatError` for a malformed one rather than letting an opaque `RangeError` escape the FILETIME conversion (see [Metadata](#metadata)). |
|
|
280
285
|
| `src/table/decoration.ts` | A cell's border and background-shading encodings -- `Brc80`, `Brc`, `Shd`, `Shd80`, and the `BrcType`/`Ipat` vocabularies -- read and written in one place, so neither direction can drift from the other (see [Cell decoration](#cell-decoration)). |
|
|
281
286
|
| `src/table/tap.ts` | Folding a table row's own sgc-5 grpprl into its TAP — column boundaries and every physical cell's merge state from `sprmTDefTable`, folded with a `sprmTMerge` range or `sprmTVertMerge` flag where one is present, regardless of which order they appear in. |
|
|
282
|
-
| `src/table/read.ts` | Grouping a contiguous run of table-depth paragraphs (from `read.ts`'s own flat sequence) into a real `ContentTable`,
|
|
287
|
+
| `src/table/read.ts` | Grouping a contiguous run of table-depth paragraphs (from `read.ts`'s own flat sequence) into a real `ContentTable`, recursing into a nested table at whatever depth the file states. |
|
|
288
|
+
| `src/text/paragraphs.ts` | Splitting a paragraph around an inline picture anchor, and `resolveIndirectPapx`'s own `sprmPHugePapx` indirection chase into the Data stream. |
|
|
289
|
+
| `src/subdocument.ts` | The shared subdocument/story-plex reader every footnote/endnote/comment/header-footer story goes through, including `readStoryPlexKeys`'s own leniency for a genuine Word 97 file's out-of-specification placeholder keys. |
|
|
283
290
|
| `src/read.ts` | The whole read chain, to a `ContentDocument`. |
|
|
284
291
|
| `src/fib/write.ts` | Builds a real FIB for nFib 0x00C1 (Word 97), populated with the fc/lcb pairs this package's own writer needs. |
|
|
285
292
|
| `src/text/piece-table-write.ts` | Builds a `Clx` describing the whole logical text stream as one uncompressed piece. |
|
|
@@ -291,6 +298,7 @@ The modules layer in the order [MS-DOC]'s own algorithms chain:
|
|
|
291
298
|
| `src/pictures-write.ts` | The inverse of `pictures.ts`: a `ContentImageBlock` to its own `PICF`/`OfficeArtInlineSpContainer`/blip bytes and `sprmCPicLocation` grpprl (see [Images](#images)). |
|
|
292
299
|
| `src/data-stream.ts` | Accumulates every inline picture's own bytes across a whole write into one real `"Data"` stream, handing each one back the offset it landed at. |
|
|
293
300
|
| `src/prop/sep-write.ts` | The inverse of `prop/sep.ts`: a document's own sections to a real `PlcfSed`/`Sepx` array, one entry per section (see [Sections](#sections)). |
|
|
301
|
+
| `src/subdocument-write.ts` | The inverse of `subdocument.ts`/`notes.ts`/`headers-footers.ts`: the footnote, header, comment, and endnote subdocuments and their CP-only boundary plexes (see [Subdocuments](#subdocuments)). |
|
|
294
302
|
| `src/write.ts` | The whole write chain, from a `ContentDocument` to real [MS-DOC] bytes in a real [MS-CFB] compound file. |
|
|
295
303
|
|
|
296
304
|
### Why the piece table gets the most attention
|
|
@@ -343,10 +351,12 @@ Every structure is tested against bytes hand-assembled from [MS-DOC]'s own field
|
|
|
343
351
|
|
|
344
352
|
The writer is verified the opposite way: `src/write.test.ts` reads every document `writeDocContent` produces back through this package's own `readDocContent`, including cases that force `ChpxFkp`/`PapxFkp` page-splitting (150 distinctly-formatted runs, 60 distinctly-indented paragraphs) rather than relying only on the common one-page case, a dedicated `describe("writeDocContent numbering")` block covering a multi-level list's own `format`/`text` per level, a bulleted list's glyph, separate-`ilfo` minting for separate `numId`s, a no-lists document writing no numbering tables at all, and a list membership inside a table cell (see [Numbering definitions](#numbering-definitions)), and a dedicated `describe("writeDocContent tables")` block covering row/column/cell round-tripping, a multi-paragraph cell, row height, a horizontally merged cell's `colSpan`, a vertically merged cell's `rowSpan`, the lost-boundary fallback recovering `colSpan`/`columnWidthsPt` for a single-row merge and for a multi-row table that merges the identical boundary in every row ([ExaDev/documents.js#992](https://github.com/ExaDev/documents.js/issues/992)), that same fallback's own per-row budget check degrading gracefully via `onWarning` instead of throwing once a row's assigned split would overflow its row-ending mark -- a two-row table sitting exactly at the 21-physical-cell ceiling versus one column past it (only the over-budget row trims, so the other row's `#992` recovery survives, and the trimmed row itself still recovers all but one of its own assigned boundaries rather than losing all of them), a single-row table one column past that same ceiling recovering all but one of its own boundaries the identical way with no sibling row to share the work with, and a single-row table whose full assigned split would need 64 physical cells -- one past `TDefTableOperand`'s own hard `NumberOfColumns` ceiling ([MS-DOC] 2.9.321's own "MUST NOT exceed 63", not 2.4.3's separate "between 1 and 63 table cells" limit) -- trimming down to the row-ending mark's own byte-budget ceiling instead of throwing the way this writer used to before that check ran ahead of `encodeTableRowGrpprl`'s own unconditional throw past it ([ExaDev/documents.js#1013](https://github.com/ExaDev/documents.js/issues/1013)), the nested-table refusal, and a `describe("cell decoration")` block round-tripping a solid background fill, a genuine two-colour pattern fill (a percentage grey, a crosshatch -- ExaDev/documents.js#951), all four borders at different styles/widths/colours, a partially bordered cell, a cell with no decoration at all (which must emit none), decoration on a merged cell, and a colour the `Ico` palette cannot hold. `src/table/decoration.test.ts` covers the same vocabulary one layer down, against bytes hand-built from the specification's own field tables -- including every encoding this package's own writer never emits, which a round trip therefore cannot reach: both no-border spellings, each `BrcType` family's collapse onto `ContentStrokeStyle`, the art-border and automatic-colour cases, `ipatSolid`, every pattern `IPAT_TO_PATTERN_TYPE` names (percentages and stripe/cross families alike) alongside `ipatNil` and an `ipatPctNew*` value with no `ST_Shd` equivalent, the `DocUnsupportedError` a SpreadsheetML-only pattern name throws on write, `Shd80`, and the second and third shading arrays a row too wide for one `PapxInFkp` record can never exercise end to end. Beyond the committed suite, a `writeDocContent` sample carrying every character and paragraph property this writer supports was opened, rendered, and re-exported by a real, independent [MS-DOC] implementation — LibreOffice — without error or visible content loss, confirming those bytes are genuinely conformant to a reader this package did not write, not merely self-consistent with its own. Table samples were checked the same way and now pass in both directions -- plain, vertically merged, horizontally merged, merged both ways, and decorated with cell fills and per-side borders -- after [ExaDev/documents.js#892](https://github.com/ExaDev/documents.js/issues/892) and [#895](https://github.com/ExaDev/documents.js/issues/895) were each root-caused by comparing this writer's own bytes against a genuine LibreOffice-authored `.doc`; see [Tables](#tables) for the full findings.
|
|
345
353
|
|
|
346
|
-
A `describe("writeDocContent multiple sections")` block (`ExaDev/documents.js#971`) covers two and three sections each round-tripping their own distinct page size and margins independently, and a non-final section whose own last block is a table still closing on a genuine paragraph mark before its end-of-section character, rather than landing on the table's own row-ending mark. A `describe("writeDocContent inline pictures")` block covers a PNG and a JPEG image round-tripping their own raw bytes/format/size, a paragraph's own text splitting around an inline picture into separate blocks, more than one picture landing at distinct offsets in the same `"Data"` stream, an unwritable format (`svg`) throwing `DocUnsupportedError`, and no `"Data"` stream at all being written for a picture-free document.
|
|
354
|
+
A `describe("writeDocContent multiple sections")` block (`ExaDev/documents.js#971`) covers two and three sections each round-tripping their own distinct page size and margins independently, and a non-final section whose own last block is a table still closing on a genuine paragraph mark before its end-of-section character, rather than landing on the table's own row-ending mark. A `describe("writeDocContent page breaks")` block covers the ordinary `[paragraph, pageBreak, paragraph]` round trip (including the terminated paragraph's own formatting surviving), a second consecutive break, a leading break, a break directly after a table (never retargeting the row mark), and a break as a non-final section's last block — alongside `read.test.ts`'s own page-break tests over hand-built bytes, which pin the read-side distinction (a 0x000C at a PlcfSed boundary is a section end and gains no block; one elsewhere is a `pageBreak`) against the specification's own PlcfSed.aCP sentence. The write side is additionally verified against a real, independent [MS-DOC] implementation: LibreOffice opens a written page-break sample and re-exports a genuine `fo:break-before="page"` on the paragraph after the break. A `describe("writeDocContent stories")` block covers footnote/endnote/comment bodies round-tripping as plain text (a note carrying newlines and a trailing empty paragraph included, which is what the writer's separate guard mark exists to protect), a document stating no stories writing no subdocument at all, header/footer stories round-tripping per (section, slot) with absent slots staying absent, a header story carrying a table through the identical table pipeline, a story whose blocks flatten to nothing reading back as present-but-blank, the `DocFormatError` refusals for a story naming a section the document does not have and for two stories sharing a slot, and a full `DocContent` re-writen unchanged — the read side of that last case additionally run over a genuine LibreOffice-authored `.doc` in [Writing](#writing)'s stories paragraph. A `describe("writeDocContent inline pictures")` block covers a PNG and a JPEG image round-tripping their own raw bytes/format/size, a paragraph's own text splitting around an inline picture into separate blocks, more than one picture landing at distinct offsets in the same `"Data"` stream, an unwritable format (`svg`) throwing `DocUnsupportedError`, and no `"Data"` stream at all being written for a picture-free document. The inline-picture block has not itself been checked against a real, independent [MS-DOC] implementation the way [Tables](#tables) and [Numbering definitions](#numbering-definitions) have -- that is the next thing worth doing here, mirroring how each of those was itself first verified by this package's own round trip alone.
|
|
347
355
|
|
|
348
356
|
A real-producer corpus layer exists: `pnpm test:corpus` runs the gitignored `test/corpus/` suite against LibreOffice-produced Word 97 documents generated by `scripts/generate-corpus.mjs` (flat-ODT and packaged-ODT sources spanning paragraphs, runs, headings, lists, tables with merges, images, and sections, converted headlessly through Writer's own export filter). That corpus already paid for itself once: it exposed that the reader's inline-picture locator assumed Word's OfficeArt wrapper nesting, which LibreOffice does not follow, and the locator now validates blip candidates by their payload file signatures instead. What a LibreOffice-produced corpus does not prove remains stated plainly: it is a real application's Word 97 spelling, not Word 1997-2007's own.
|
|
349
357
|
|
|
358
|
+
A second, independent corpus layer closes that gap ([ExaDev/documents.js#1193](https://github.com/ExaDev/documents.js/issues/1193)): `scripts/fetch-word-corpus.mjs` downloads a genuine Word-97-through-2003-produced `.doc` set from Apache POI's own public test-data repository (itself assembled from real bug-report attachments and, for two files, a public web crawl), pinned to one POI commit and verified per file at fetch time — a recorded sha256, the `WordDocument` stream's own 0xA5EC signature, the effective `nFib` [MS-DOC] 2.5.1's own rule computes, and, where the file carries one, the `"\x05SummaryInformation"` stream's own application-name property stating the exact Word version. `node scripts/fetch-word-corpus.mjs` (after a build; requires network) writes the verified files, a `manifest.json` recording each one's producer/origin/provenance, and a generated `corpus.test.ts` under `test/corpus/word/`, beside — never disturbing — `generate-corpus.mjs`'s own output at the `test/corpus/` root. This corpus already found and fixed four genuine defects no LibreOffice-produced fixture had reached: the `sprmPHugePapx` paragraph-property indirection two Word-2003 files state their wide table rows' TAP through; a Word-97 file's own out-of-specification `Plcfhdd` placeholder keys; an `LVLF.nfc` value outside `MSONFC`'s own table; and a table-flagged paragraph run with no row-ending mark anywhere in it, which is not a table at all rather than a malformed one. See each fix's own note above for the file and the finding.
|
|
359
|
+
|
|
350
360
|
## Specification
|
|
351
361
|
|
|
352
362
|
Every structure in this package cites the section of [MS-DOC] it implements. The specification is published by Microsoft under its Open Specifications programme:
|
package/dist/fib/write.cjs
CHANGED
|
@@ -24,6 +24,10 @@ function buildFib(spec) {
|
|
|
24
24
|
};
|
|
25
25
|
lw(require_fib_offsets.LW_OFFSET.cbMac, spec.cbMac);
|
|
26
26
|
lw(require_fib_offsets.LW_OFFSET.ccpText, spec.ccpText);
|
|
27
|
+
lw(require_fib_offsets.LW_OFFSET.ccpFtn, spec.ccpFtn);
|
|
28
|
+
lw(require_fib_offsets.LW_OFFSET.ccpHdd, spec.ccpHdd);
|
|
29
|
+
lw(require_fib_offsets.LW_OFFSET.ccpAtn, spec.ccpAtn);
|
|
30
|
+
lw(require_fib_offsets.LW_OFFSET.ccpEdn, spec.ccpEdn);
|
|
27
31
|
view.setUint16(152, CB_RG_FC_LCB_WORD_97, true);
|
|
28
32
|
const pair = (index, fc, lcb) => {
|
|
29
33
|
const offset = 154 + index * 4;
|
|
@@ -38,6 +42,10 @@ function buildFib(spec) {
|
|
|
38
42
|
pair(require_fib_offsets.FC_LCB_VALUE_INDEX.fcPlcfSed, spec.fcPlcfSed, spec.lcbPlcfSed);
|
|
39
43
|
pair(require_fib_offsets.FC_LCB_VALUE_INDEX.fcPlfLst, spec.fcPlfLst, spec.lcbPlfLst);
|
|
40
44
|
pair(require_fib_offsets.FC_LCB_VALUE_INDEX.fcPlfLfo, spec.fcPlfLfo, spec.lcbPlfLfo);
|
|
45
|
+
pair(require_fib_offsets.FC_LCB_VALUE_INDEX.fcPlcffndTxt, spec.fcPlcffndTxt, spec.lcbPlcffndTxt);
|
|
46
|
+
pair(require_fib_offsets.FC_LCB_VALUE_INDEX.fcPlcfHdd, spec.fcPlcfHdd, spec.lcbPlcfHdd);
|
|
47
|
+
pair(require_fib_offsets.FC_LCB_VALUE_INDEX.fcPlcfandTxt, spec.fcPlcfandTxt, spec.lcbPlcfandTxt);
|
|
48
|
+
pair(require_fib_offsets.FC_LCB_VALUE_INDEX.fcPlcfendTxt, spec.fcPlcfendTxt, spec.lcbPlcfendTxt);
|
|
41
49
|
if (require_bytes.readUint16LE(bytes, 0) !== 42476) throw new require_errors.DocFormatError("buildFib produced a Fib whose own wIdent does not read back as 0xA5EC; this is an internal defect, not an input error");
|
|
42
50
|
return bytes;
|
|
43
51
|
}
|
package/dist/fib/write.d.cts
CHANGED
|
@@ -20,6 +20,22 @@ interface FibWriteSpec {
|
|
|
20
20
|
readonly lcbPlfLst: number;
|
|
21
21
|
readonly fcPlfLfo: number;
|
|
22
22
|
readonly lcbPlfLfo: number;
|
|
23
|
+
/** The footnote subdocument's own character count, and its PlcffndTxt -- 0/0 when the document carries no footnotes (see subdocument-write.ts). */
|
|
24
|
+
readonly ccpFtn: number;
|
|
25
|
+
readonly fcPlcffndTxt: number;
|
|
26
|
+
readonly lcbPlcffndTxt: number;
|
|
27
|
+
/** The header subdocument's own character count, and its Plcfhdd -- 0/0 when the document carries no header/footer stories. */
|
|
28
|
+
readonly ccpHdd: number;
|
|
29
|
+
readonly fcPlcfHdd: number;
|
|
30
|
+
readonly lcbPlcfHdd: number;
|
|
31
|
+
/** The comment subdocument's own character count, and its PlcfandTxt -- 0/0 when the document carries no comments. */
|
|
32
|
+
readonly ccpAtn: number;
|
|
33
|
+
readonly fcPlcfandTxt: number;
|
|
34
|
+
readonly lcbPlcfandTxt: number;
|
|
35
|
+
/** The endnote subdocument's own character count, and its PlcfendTxt -- 0/0 when the document carries no endnotes. */
|
|
36
|
+
readonly ccpEdn: number;
|
|
37
|
+
readonly fcPlcfendTxt: number;
|
|
38
|
+
readonly lcbPlcfendTxt: number;
|
|
23
39
|
}
|
|
24
40
|
declare function buildFib(spec: FibWriteSpec): Uint8Array<ArrayBuffer>;
|
|
25
41
|
//#endregion
|
package/dist/fib/write.d.ts
CHANGED
|
@@ -20,6 +20,22 @@ interface FibWriteSpec {
|
|
|
20
20
|
readonly lcbPlfLst: number;
|
|
21
21
|
readonly fcPlfLfo: number;
|
|
22
22
|
readonly lcbPlfLfo: number;
|
|
23
|
+
/** The footnote subdocument's own character count, and its PlcffndTxt -- 0/0 when the document carries no footnotes (see subdocument-write.ts). */
|
|
24
|
+
readonly ccpFtn: number;
|
|
25
|
+
readonly fcPlcffndTxt: number;
|
|
26
|
+
readonly lcbPlcffndTxt: number;
|
|
27
|
+
/** The header subdocument's own character count, and its Plcfhdd -- 0/0 when the document carries no header/footer stories. */
|
|
28
|
+
readonly ccpHdd: number;
|
|
29
|
+
readonly fcPlcfHdd: number;
|
|
30
|
+
readonly lcbPlcfHdd: number;
|
|
31
|
+
/** The comment subdocument's own character count, and its PlcfandTxt -- 0/0 when the document carries no comments. */
|
|
32
|
+
readonly ccpAtn: number;
|
|
33
|
+
readonly fcPlcfandTxt: number;
|
|
34
|
+
readonly lcbPlcfandTxt: number;
|
|
35
|
+
/** The endnote subdocument's own character count, and its PlcfendTxt -- 0/0 when the document carries no endnotes. */
|
|
36
|
+
readonly ccpEdn: number;
|
|
37
|
+
readonly fcPlcfendTxt: number;
|
|
38
|
+
readonly lcbPlcfendTxt: number;
|
|
23
39
|
}
|
|
24
40
|
declare function buildFib(spec: FibWriteSpec): Uint8Array<ArrayBuffer>;
|
|
25
41
|
//#endregion
|
package/dist/fib/write.js
CHANGED
|
@@ -23,6 +23,10 @@ function buildFib(spec) {
|
|
|
23
23
|
};
|
|
24
24
|
lw(LW_OFFSET.cbMac, spec.cbMac);
|
|
25
25
|
lw(LW_OFFSET.ccpText, spec.ccpText);
|
|
26
|
+
lw(LW_OFFSET.ccpFtn, spec.ccpFtn);
|
|
27
|
+
lw(LW_OFFSET.ccpHdd, spec.ccpHdd);
|
|
28
|
+
lw(LW_OFFSET.ccpAtn, spec.ccpAtn);
|
|
29
|
+
lw(LW_OFFSET.ccpEdn, spec.ccpEdn);
|
|
26
30
|
view.setUint16(152, CB_RG_FC_LCB_WORD_97, true);
|
|
27
31
|
const pair = (index, fc, lcb) => {
|
|
28
32
|
const offset = 154 + index * 4;
|
|
@@ -37,6 +41,10 @@ function buildFib(spec) {
|
|
|
37
41
|
pair(FC_LCB_VALUE_INDEX.fcPlcfSed, spec.fcPlcfSed, spec.lcbPlcfSed);
|
|
38
42
|
pair(FC_LCB_VALUE_INDEX.fcPlfLst, spec.fcPlfLst, spec.lcbPlfLst);
|
|
39
43
|
pair(FC_LCB_VALUE_INDEX.fcPlfLfo, spec.fcPlfLfo, spec.lcbPlfLfo);
|
|
44
|
+
pair(FC_LCB_VALUE_INDEX.fcPlcffndTxt, spec.fcPlcffndTxt, spec.lcbPlcffndTxt);
|
|
45
|
+
pair(FC_LCB_VALUE_INDEX.fcPlcfHdd, spec.fcPlcfHdd, spec.lcbPlcfHdd);
|
|
46
|
+
pair(FC_LCB_VALUE_INDEX.fcPlcfandTxt, spec.fcPlcfandTxt, spec.lcbPlcfandTxt);
|
|
47
|
+
pair(FC_LCB_VALUE_INDEX.fcPlcfendTxt, spec.fcPlcfendTxt, spec.lcbPlcfendTxt);
|
|
40
48
|
if (readUint16LE(bytes, 0) !== 42476) throw new DocFormatError("buildFib produced a Fib whose own wIdent does not read back as 0xA5EC; this is an internal defect, not an input error");
|
|
41
49
|
return bytes;
|
|
42
50
|
}
|
package/dist/headers-footers.cjs
CHANGED
|
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
const require_subdocument = require("./subdocument.cjs");
|
|
3
3
|
const require_table_read = require("./table/read.cjs");
|
|
4
4
|
//#region src/headers-footers.ts
|
|
5
|
-
/** The six per-section slots' own order within each of Plcfhdd's per-section groups, [MS-DOC]'s own Headers page: "The stories within each group MUST appear in the following order." */
|
|
5
|
+
/** The six per-section slots' own order within each of Plcfhdd's per-section groups, [MS-DOC]'s own Headers page: "The stories within each group MUST appear in the following order." Exported because subdocument-write.ts builds Plcfhdd in the identical order -- one source of it, so the two directions cannot drift. */
|
|
6
6
|
const SLOT_ORDER = [
|
|
7
7
|
"evenHeader",
|
|
8
8
|
"oddHeader",
|
|
@@ -11,11 +11,11 @@ const SLOT_ORDER = [
|
|
|
11
11
|
"firstHeader",
|
|
12
12
|
"firstFooter"
|
|
13
13
|
];
|
|
14
|
-
/** The six fixed footnote/endnote-separator stories preceding every section's own six -- footnote separator, footnote continuation separator, footnote continuation notice, endnote separator, endnote continuation separator, endnote continuation notice -- carried by Plcfhdd but not read here, since neither this package's own schema nor ooxml.js's DocxDocument has anywhere to put a separator story. */
|
|
14
|
+
/** The six fixed footnote/endnote-separator stories preceding every section's own six -- footnote separator, footnote continuation separator, footnote continuation notice, endnote separator, endnote continuation separator, endnote continuation notice -- carried by Plcfhdd but not read here, since neither this package's own schema nor ooxml.js's DocxDocument has anywhere to put a separator story. Exported for subdocument-write.ts, which writes the same six slots as genuinely empty stories. */
|
|
15
15
|
const FIXED_SEPARATOR_STORY_COUNT = 6;
|
|
16
16
|
function readHeaderFooterStories(wordDocument, table, pieceTable, context, fib, sectionCount) {
|
|
17
17
|
const headerStartCp = fib.ccpText + fib.ccpFtn;
|
|
18
|
-
const perSectionStories = require_subdocument.readSubdocumentStories(wordDocument, table, pieceTable, context, headerStartCp, fib.ccpHdd, fib.fcPlcfHdd, fib.lcbPlcfHdd, "Plcfhdd").slice(
|
|
18
|
+
const perSectionStories = require_subdocument.readSubdocumentStories(wordDocument, table, pieceTable, context, headerStartCp, fib.ccpHdd, fib.fcPlcfHdd, fib.lcbPlcfHdd, "Plcfhdd").slice(6);
|
|
19
19
|
const stories = [];
|
|
20
20
|
for (let section = 0; section < sectionCount; section += 1) for (let slotIndex = 0; slotIndex < SLOT_ORDER.length; slotIndex += 1) {
|
|
21
21
|
const entries = perSectionStories[section * SLOT_ORDER.length + slotIndex];
|
|
@@ -31,4 +31,6 @@ function readHeaderFooterStories(wordDocument, table, pieceTable, context, fib,
|
|
|
31
31
|
return stories;
|
|
32
32
|
}
|
|
33
33
|
//#endregion
|
|
34
|
+
exports.FIXED_SEPARATOR_STORY_COUNT = FIXED_SEPARATOR_STORY_COUNT;
|
|
35
|
+
exports.SLOT_ORDER = SLOT_ORDER;
|
|
34
36
|
exports.readHeaderFooterStories = readHeaderFooterStories;
|
|
@@ -11,6 +11,10 @@ interface HeaderFooterStory {
|
|
|
11
11
|
readonly blocks: readonly ContentBlock[];
|
|
12
12
|
}
|
|
13
13
|
type HeaderFooterStories = readonly HeaderFooterStory[];
|
|
14
|
+
/** The six per-section slots' own order within each of Plcfhdd's per-section groups, [MS-DOC]'s own Headers page: "The stories within each group MUST appear in the following order." Exported because subdocument-write.ts builds Plcfhdd in the identical order -- one source of it, so the two directions cannot drift. */
|
|
15
|
+
declare const SLOT_ORDER: readonly HeaderFooterSlot[];
|
|
16
|
+
/** The six fixed footnote/endnote-separator stories preceding every section's own six -- footnote separator, footnote continuation separator, footnote continuation notice, endnote separator, endnote continuation separator, endnote continuation notice -- carried by Plcfhdd but not read here, since neither this package's own schema nor ooxml.js's DocxDocument has anywhere to put a separator story. Exported for subdocument-write.ts, which writes the same six slots as genuinely empty stories. */
|
|
17
|
+
declare const FIXED_SEPARATOR_STORY_COUNT = 6;
|
|
14
18
|
declare function readHeaderFooterStories(wordDocument: Uint8Array, table: Uint8Array, pieceTable: PieceTable, context: ReadContext, fib: Fib, sectionCount: number): HeaderFooterStories;
|
|
15
19
|
//#endregion
|
|
16
|
-
export { HeaderFooterSlot, HeaderFooterStories, HeaderFooterStory, readHeaderFooterStories };
|
|
20
|
+
export { FIXED_SEPARATOR_STORY_COUNT, HeaderFooterSlot, HeaderFooterStories, HeaderFooterStory, SLOT_ORDER, readHeaderFooterStories };
|
|
@@ -11,6 +11,10 @@ interface HeaderFooterStory {
|
|
|
11
11
|
readonly blocks: readonly ContentBlock[];
|
|
12
12
|
}
|
|
13
13
|
type HeaderFooterStories = readonly HeaderFooterStory[];
|
|
14
|
+
/** The six per-section slots' own order within each of Plcfhdd's per-section groups, [MS-DOC]'s own Headers page: "The stories within each group MUST appear in the following order." Exported because subdocument-write.ts builds Plcfhdd in the identical order -- one source of it, so the two directions cannot drift. */
|
|
15
|
+
declare const SLOT_ORDER: readonly HeaderFooterSlot[];
|
|
16
|
+
/** The six fixed footnote/endnote-separator stories preceding every section's own six -- footnote separator, footnote continuation separator, footnote continuation notice, endnote separator, endnote continuation separator, endnote continuation notice -- carried by Plcfhdd but not read here, since neither this package's own schema nor ooxml.js's DocxDocument has anywhere to put a separator story. Exported for subdocument-write.ts, which writes the same six slots as genuinely empty stories. */
|
|
17
|
+
declare const FIXED_SEPARATOR_STORY_COUNT = 6;
|
|
14
18
|
declare function readHeaderFooterStories(wordDocument: Uint8Array, table: Uint8Array, pieceTable: PieceTable, context: ReadContext, fib: Fib, sectionCount: number): HeaderFooterStories;
|
|
15
19
|
//#endregion
|
|
16
|
-
export { HeaderFooterSlot, HeaderFooterStories, HeaderFooterStory, readHeaderFooterStories };
|
|
20
|
+
export { FIXED_SEPARATOR_STORY_COUNT, HeaderFooterSlot, HeaderFooterStories, HeaderFooterStory, SLOT_ORDER, readHeaderFooterStories };
|
package/dist/headers-footers.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readSubdocumentStories } from "./subdocument.js";
|
|
2
2
|
import { assembleBlocks } from "./table/read.js";
|
|
3
3
|
//#region src/headers-footers.ts
|
|
4
|
-
/** The six per-section slots' own order within each of Plcfhdd's per-section groups, [MS-DOC]'s own Headers page: "The stories within each group MUST appear in the following order." */
|
|
4
|
+
/** The six per-section slots' own order within each of Plcfhdd's per-section groups, [MS-DOC]'s own Headers page: "The stories within each group MUST appear in the following order." Exported because subdocument-write.ts builds Plcfhdd in the identical order -- one source of it, so the two directions cannot drift. */
|
|
5
5
|
const SLOT_ORDER = [
|
|
6
6
|
"evenHeader",
|
|
7
7
|
"oddHeader",
|
|
@@ -10,11 +10,11 @@ const SLOT_ORDER = [
|
|
|
10
10
|
"firstHeader",
|
|
11
11
|
"firstFooter"
|
|
12
12
|
];
|
|
13
|
-
/** The six fixed footnote/endnote-separator stories preceding every section's own six -- footnote separator, footnote continuation separator, footnote continuation notice, endnote separator, endnote continuation separator, endnote continuation notice -- carried by Plcfhdd but not read here, since neither this package's own schema nor ooxml.js's DocxDocument has anywhere to put a separator story. */
|
|
13
|
+
/** The six fixed footnote/endnote-separator stories preceding every section's own six -- footnote separator, footnote continuation separator, footnote continuation notice, endnote separator, endnote continuation separator, endnote continuation notice -- carried by Plcfhdd but not read here, since neither this package's own schema nor ooxml.js's DocxDocument has anywhere to put a separator story. Exported for subdocument-write.ts, which writes the same six slots as genuinely empty stories. */
|
|
14
14
|
const FIXED_SEPARATOR_STORY_COUNT = 6;
|
|
15
15
|
function readHeaderFooterStories(wordDocument, table, pieceTable, context, fib, sectionCount) {
|
|
16
16
|
const headerStartCp = fib.ccpText + fib.ccpFtn;
|
|
17
|
-
const perSectionStories = readSubdocumentStories(wordDocument, table, pieceTable, context, headerStartCp, fib.ccpHdd, fib.fcPlcfHdd, fib.lcbPlcfHdd, "Plcfhdd").slice(
|
|
17
|
+
const perSectionStories = readSubdocumentStories(wordDocument, table, pieceTable, context, headerStartCp, fib.ccpHdd, fib.fcPlcfHdd, fib.lcbPlcfHdd, "Plcfhdd").slice(6);
|
|
18
18
|
const stories = [];
|
|
19
19
|
for (let section = 0; section < sectionCount; section += 1) for (let slotIndex = 0; slotIndex < SLOT_ORDER.length; slotIndex += 1) {
|
|
20
20
|
const entries = perSectionStories[section * SLOT_ORDER.length + slotIndex];
|
|
@@ -30,4 +30,4 @@ function readHeaderFooterStories(wordDocument, table, pieceTable, context, fib,
|
|
|
30
30
|
return stories;
|
|
31
31
|
}
|
|
32
32
|
//#endregion
|
|
33
|
-
export { readHeaderFooterStories };
|
|
33
|
+
export { FIXED_SEPARATOR_STORY_COUNT, SLOT_ORDER, readHeaderFooterStories };
|