rtf-codec 2.0.0 → 3.0.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 +90 -45
- package/dist/base64.d.cts +1 -1
- package/dist/base64.d.ts +1 -1
- package/dist/codepage.d.cts +1 -1
- package/dist/codepage.d.ts +1 -1
- package/dist/constructs.cjs +35 -0
- package/dist/constructs.d.cts +13 -2
- package/dist/constructs.d.ts +13 -2
- package/dist/constructs.js +34 -1
- package/dist/{diagnostics-BgG_KAiN.d.cts → diagnostics-CjMoO7x1.d.cts} +3 -1
- package/dist/{diagnostics-BgG_KAiN.d.ts → diagnostics-CjMoO7x1.d.ts} +3 -1
- package/dist/diagnostics.cjs +3 -1
- package/dist/diagnostics.d.cts +1 -1
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.js +3 -1
- package/dist/embedded-object.cjs +215 -0
- package/dist/embedded-object.d.cts +6 -0
- package/dist/embedded-object.d.ts +6 -0
- package/dist/embedded-object.js +213 -0
- package/dist/header.d.cts +1 -1
- package/dist/header.d.ts +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/options.d.cts +1 -1
- package/dist/options.d.ts +1 -1
- package/dist/read.cjs +367 -13
- package/dist/read.d.cts +1 -1
- package/dist/read.d.ts +1 -1
- package/dist/read.js +368 -14
- package/dist/write.cjs +241 -17
- package/dist/write.js +241 -17
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**Status: under active development.** The read and write paths described below are implemented and tested, but this package is new and has not yet been exercised against a real-world corpus. [Scope](#scope) states exactly what is handled and what is not; nothing in this README describes work that is planned rather than done.
|
|
8
8
|
|
|
9
|
-
Every construct that remains unhandled is either a gap in `document-schema.js` rather than in this codec (superscript/subscript and text direction have no field to land in), or something RTF itself does not specify (
|
|
9
|
+
Every construct that remains unhandled is either a gap in `document-schema.js` rather than in this codec (superscript/subscript and text direction have no field to land in), or something RTF itself does not specify at all beyond its own form-field vocabulary (a docx-style rich-text SDT has no RTF spelling of any kind) — see [Deliberately not handled](#deliberately-not-handled), which says which of the two each row is.
|
|
10
10
|
|
|
11
11
|
RTF is the cleanest structural fit of any format this family did not already handle. It is a wordprocessing format through and through — paragraphs, runs, character properties, paragraph properties, tables, lists and pictures all have direct `ContentDocument` equivalents — and it can express more of the wordprocessing variant than markdown can, carrying colour, font family, font size and alignment natively. No `document-schema.js` model change was needed for it.
|
|
12
12
|
|
|
@@ -14,18 +14,21 @@ What it is _not_ is another XML format. RTF is tokenised plain text with a brace
|
|
|
14
14
|
|
|
15
15
|
```mermaid
|
|
16
16
|
graph TD
|
|
17
|
+
archive("archive-codec")
|
|
17
18
|
schema("document-schema.js")
|
|
18
19
|
rtfcodec("rtf-codec")
|
|
19
20
|
|
|
21
|
+
archive --> rtfcodec
|
|
20
22
|
schema --> rtfcodec
|
|
21
23
|
|
|
24
|
+
click archive "https://github.com/ExaDev/documents.js/tree/main/packages/archive-codec" "archive-codec"
|
|
22
25
|
click schema "https://github.com/ExaDev/documents.js/tree/main/packages/document-schema.js" "document-schema.js"
|
|
23
26
|
click rtfcodec "https://github.com/ExaDev/documents.js/tree/main/packages/rtf-codec" "rtf-codec"
|
|
24
27
|
|
|
25
28
|
style rtfcodec fill:#f9a825,stroke:#333,stroke-width:3px
|
|
26
29
|
```
|
|
27
30
|
|
|
28
|
-
`rtf-codec` depends on
|
|
31
|
+
`rtf-codec` depends on `document-schema.js` for the content pivot and `archive-codec` for the [MS-CFB] container an embedded object's `\objdata` carries — see [Embedded objects](#embedded-objects) and [Dependency choices](#dependency-choices). It is reachable from [`documents.js`](../documents.js/README.md)'s conversion engine, and so from `document-cli`, `document-mcp`, and the web UI, as an ordinary source and target format.
|
|
29
32
|
|
|
30
33
|
## Getting started
|
|
31
34
|
|
|
@@ -81,7 +84,7 @@ Five stages, each its own module, each testable on its own:
|
|
|
81
84
|
| Read | `src/read.ts` | The destination/group state machine that turns the token stream into a `ContentDocument`. |
|
|
82
85
|
| Write | `src/write.ts` | The inverse: mints the header tables from what the document actually uses, then emits a body that references them by index. |
|
|
83
86
|
|
|
84
|
-
Supporting modules: `src/codepage.ts` (byte-to-character tables and the `\ansicpgN`/`\fcharsetN`/`\cpgN` precedence), `src/base64.ts` (hex and base64 conversion for picture payloads), `src/units.ts` (twips, half-points, pixels), `src/list-id.ts` (the opaque `numId` grammar), `src/constructs.ts` (the fidelity-construct descriptor shapes and the DTTM bit field), `src/cell-format.ts` (the `<celldef>` border, shading, and merge production), `src/diagnostics.ts` (the three-tier diagnostic policy).
|
|
87
|
+
Supporting modules: `src/codepage.ts` (byte-to-character tables and the `\ansicpgN`/`\fcharsetN`/`\cpgN` precedence), `src/base64.ts` (hex and base64 conversion for picture and object payloads), `src/units.ts` (twips, half-points, pixels), `src/list-id.ts` (the opaque `numId` grammar), `src/constructs.ts` (the fidelity-construct descriptor shapes and the DTTM bit field), `src/cell-format.ts` (the `<celldef>` border, shading, and merge production), `src/embedded-object.ts` (the `\object`/`\objdata` payload -- JSON in, real `[MS-CFB]` compound file out, via `archive-codec`; see [Embedded objects](#embedded-objects)), `src/diagnostics.ts` (the three-tier diagnostic policy).
|
|
85
88
|
|
|
86
89
|
### The reader is the specification's own model, literally
|
|
87
90
|
|
|
@@ -103,59 +106,101 @@ On the way out, **every non-ASCII character leaves as `\uN`** with a one-charact
|
|
|
103
106
|
|
|
104
107
|
### Read: RTF → `ContentDocument`
|
|
105
108
|
|
|
106
|
-
| Construct | Handled
|
|
107
|
-
| -------------------------------------------------------- |
|
|
108
|
-
| Groups, destinations, `{\*` ignorable destinations | Yes — per the spec's own reader conventions
|
|
109
|
-
| Control words, control symbols, `\'hh`, `\binN` | Yes
|
|
110
|
-
| `\uN` / `\ucN` with ANSI fallback skipping, `\upr`/`\ud` | Yes
|
|
111
|
-
| Code pages | `\ansi`/`\mac`/`\pc`/`\pca`, `\ansicpgN`, per-font `\cpgN`/`\fcharsetN`; the Windows, OEM and Macintosh single-byte pages, plus UTF-8
|
|
112
|
-
| `\fonttbl` | Face name, family keyword, per-font code page
|
|
113
|
-
| `\colortbl` | RGB, including a theme colour's own literal RGB; index 0 is the auto colour
|
|
114
|
-
| `\stylesheet` | Paragraph style names and heading levels (`\outlinelevelN` or a built-in `heading N` name)
|
|
115
|
-
| `\listtable` / `\listoverridetable` | `\lsN` → `\listidN` → the level's `\levelnfcN` and `\levelstartatN`, with each `\lfolevel`'s own start-at or whole-level override applied
|
|
116
|
-
| `\*\revtbl` | The revision authors `\revauthN` and its siblings index into
|
|
117
|
-
| Sections | `\sect`, `\sectd`, the `\pgwsxnN`/`\marg*sxnN` geometry family, and the `\sbk*` break vocabulary
|
|
118
|
-
| Paragraphs | `\par`, `\pard`, alignment, indents, spacing, `\slN`/`\slmultN`, `\pagebb`
|
|
119
|
-
| Runs | `\b`, `\i`, `\ul` (every variant), `\strike`, `\fN`, `\fsN`, `\cfN`, `\v` (dropped as hidden)
|
|
120
|
-
| Tables | `\trowd`, `\cellxN`, `\trleftN`, `\cell`, `\row`, multi-paragraph cells
|
|
121
|
-
| Table cells | `\clbrdrt`/`l`/`b`/`r` with the whole `<brdr>` production, `\clcbpatN` shading, and both merge families (`\clvmgf`/`\clvmrg`, `\clmgf`/`\clmrg`)
|
|
122
|
-
| Bookmarks | `\*\bkmkstart`/`\*\bkmkend` as `anchor` constructs, with `\bkmkcolfN`/`\bkmkcollN` quarantined as residue
|
|
123
|
-
| Revision marks | The whole `<chrev>` production as `provenance` constructs: `\revised`, `\deleted`, `\mvf`/`\mvt`, `\crauthN`, with authors and `\revdttmN` dates
|
|
124
|
-
|
|
|
125
|
-
|
|
|
126
|
-
|
|
|
127
|
-
|
|
|
128
|
-
|
|
|
129
|
-
| `\
|
|
109
|
+
| Construct | Handled |
|
|
110
|
+
| -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
111
|
+
| Groups, destinations, `{\*` ignorable destinations | Yes — per the spec's own reader conventions |
|
|
112
|
+
| Control words, control symbols, `\'hh`, `\binN` | Yes |
|
|
113
|
+
| `\uN` / `\ucN` with ANSI fallback skipping, `\upr`/`\ud` | Yes |
|
|
114
|
+
| Code pages | `\ansi`/`\mac`/`\pc`/`\pca`, `\ansicpgN`, per-font `\cpgN`/`\fcharsetN`; the Windows, OEM and Macintosh single-byte pages, plus UTF-8 |
|
|
115
|
+
| `\fonttbl` | Face name, family keyword, per-font code page |
|
|
116
|
+
| `\colortbl` | RGB, including a theme colour's own literal RGB; index 0 is the auto colour |
|
|
117
|
+
| `\stylesheet` | Paragraph style names and heading levels (`\outlinelevelN` or a built-in `heading N` name) |
|
|
118
|
+
| `\listtable` / `\listoverridetable` | `\lsN` → `\listidN` → the level's `\levelnfcN` and `\levelstartatN`, with each `\lfolevel`'s own start-at or whole-level override applied |
|
|
119
|
+
| `\*\revtbl` | The revision authors `\revauthN` and its siblings index into |
|
|
120
|
+
| Sections | `\sect`, `\sectd`, the `\pgwsxnN`/`\marg*sxnN` geometry family, and the `\sbk*` break vocabulary |
|
|
121
|
+
| Paragraphs | `\par`, `\pard`, alignment, indents, spacing, `\slN`/`\slmultN`, `\pagebb` |
|
|
122
|
+
| Runs | `\b`, `\i`, `\ul` (every variant), `\strike`, `\fN`, `\fsN`, `\cfN`, `\v` (dropped as hidden) |
|
|
123
|
+
| Tables | `\trowd`, `\cellxN`, `\trleftN`, `\cell`, `\row`, multi-paragraph cells |
|
|
124
|
+
| Table cells | `\clbrdrt`/`l`/`b`/`r` with the whole `<brdr>` production, `\clcbpatN` shading, and both merge families (`\clvmgf`/`\clvmrg`, `\clmgf`/`\clmrg`) |
|
|
125
|
+
| Bookmarks | `\*\bkmkstart`/`\*\bkmkend` as `anchor` constructs, with `\bkmkcolfN`/`\bkmkcollN` quarantined as residue |
|
|
126
|
+
| Revision marks | The whole `<chrev>` production as `provenance` constructs: `\revised`, `\deleted`, `\mvf`/`\mvt`, `\crauthN`, with authors and `\revdttmN` dates |
|
|
127
|
+
| Form fields | A `\field` whose `\*\fldinst` names FORMTEXT/FORMCHECKBOX/FORMDROPDOWN, plus whatever `\*\formfield` data it carries (`\*\ffname` as its tag, `\*\ffhelptext` as its alias when `\ffownhelp` says it is author-set rather than auto-generated, `\ffprot` as its lock, a checkbox's own `\ffres`/`\ffdefres` as its checked state, a dropdown's own `\ffres`/`\ffdefres` as its selected entry alongside its `\*\ffl` option list), as a `contentControl` construct (plainText/checkbox/dropDown). A plainText field's own `\*\ffdeftext` group is recognised but its content is skipped whole rather than captured, since `value` names the control's CURRENT value, which for a text field is the wrapped-run text already carried in the extent's own children, not `\ffdeftext`'s default/reset text (see Write below for the one direction `\ffdeftext` does feed `value`); the same applies to the four other `\*\formfield` destination strings RTF's own Form Fields table names alongside it (`\*\ffformat`, `\*\ffstattext`, `\*\ffentrymcr`, `\*\ffexitmcr`), each skipped whole for the identical reason -- no `ContentControlDescriptor` field exists to carry any of them |
|
|
128
|
+
| Lists | `\lsN`, `\ilvlN`, with the marker type carried through the `numId` grammar |
|
|
129
|
+
| Pictures | `\pngblip` and `\jpegblip`, hex or `\binN` payload, `\picwgoalN`/`\pichgoalN` or `\picwN`/`\pichN`, `\picscalexN`/`\picscaleyN` |
|
|
130
|
+
| Hyperlinks | The `HYPERLINK` field production, including its `\l` anchor switch |
|
|
131
|
+
| Special characters | `\tab`, `\line`, `\emdash`, `\endash`, `\bullet`, the quotation marks, `\~`, `\-`, `\_`, `\\`, `\{`, `\}`, and the zero-width and directional marks |
|
|
132
|
+
| Page breaks | `\page` |
|
|
133
|
+
| `\info` | Title, author, subject, keywords |
|
|
134
|
+
| Embedded objects | `\object\objemb` -> `ContentEmbeddedObjectBlock` when `\objdata` is this package's own payload (see [Embedded objects](#embedded-objects)); a real, foreign OLE object degrades with a diagnostic and its own `\result` fallback paragraphs, when present |
|
|
130
135
|
|
|
131
136
|
### Write: `ContentDocument` → RTF
|
|
132
137
|
|
|
133
|
-
Everything in the read table above has a write path, with the header tables minted from what the document actually uses: a font table entry per distinct family, a colour table per distinct colour (runs' and cells' alike), a `heading N` style per distinct heading level, a `\listtable`/`\listoverridetable` pair per distinct list, and a `\*\revtbl` per distinct revision author. Output is deterministic (the same document produces byte-identical bytes) and pure 7-bit ASCII.
|
|
138
|
+
Everything in the read table above has a write path, with the header tables minted from what the document actually uses: a font table entry per distinct family, a colour table per distinct colour (runs' and cells' alike), a `heading N` style per distinct heading level, a `\listtable`/`\listoverridetable` pair per distinct list, and a `\*\revtbl` per distinct revision author. Output is deterministic (the same document produces byte-identical bytes) and pure 7-bit ASCII. An `embeddedObject` block writes unconditionally too now (see [Embedded objects](#embedded-objects)), and that holds inside a table cell as well as outside one: `writeCellBlocks` writes a cell's own content as a run of `\intbl` `<pict>`/`<obj>`/paragraph groups, plus any `constructStart`/`constructEnd` bracket a bookmark spans across them with (see [Deliberately not handled](#deliberately-not-handled)), so only a `table` or `pageBreak` block placed directly in a cell's own content is degraded with a diagnostic rather than embedded.
|
|
134
139
|
|
|
135
140
|
Two places where the two models genuinely differ in shape, rather than merely in spelling:
|
|
136
141
|
|
|
137
142
|
- **Page geometry is stated twice.** The document-level `\paperwN` family is written once in the header from the first section's own geometry, and the section-level `\pgwsxnN` family per section — so a reader that understands neither multiple sections nor the section family still lays the document out on the right paper.
|
|
138
143
|
- **A horizontally merged cell is one cell here and several there.** `ContentTableCell` states a `colSpan` on one cell, while RTF states the same merge as several cells, the first carrying `\clmgf` and each continuation `\clmrg`. The writer expands one into the other, and the reader collapses it back. A _vertical_ merge is the opposite: RTF and the content model both keep a cell in each covered row, so `\clvmrg` reads as a cell with no blocks — the convention `ooxml.js` already follows for `w:vMerge`.
|
|
139
144
|
|
|
145
|
+
A `contentControl` construct mints a real `\*\formfield` only for the three controlTypes RTF's own vocabulary actually spells (plainText/checkbox/dropDown); any other controlType (richText, comboBox, date, and the rest) degrades through the same construct-gap diagnostic every other unrepresentable construct uses. Two contentControl extents that cross within the same paragraph -- neither nests inside nor around the other, so one starts before the other ends but also ends after it does -- have no valid `\*\formfield` brace sequence at all, since RTF's own destination is a bracket, not a range; the later-opening extent is dropped with its own diagnostic rather than emit output where each extent's closing braces close the other's groups instead of its own. The fields inside a minted `\*\formfield` are emitted in a fixed order this writer itself chooses -- RTF 1.9.1's own "Form Fields" section gives a real Formal Syntax production for `<formfield>`, and further productions for `<formparams>`/`<formstrings>` that mandate a fixed order for their own members (via the spec's plain-juxtaposition operator, not its `&` "any order" operator) -- this writer's own order is a genuine subsequence of that spec-mandated order, not a free house convention; see write.ts's own top-of-file comment on formFieldPayload for the full citation and the exact productions. This writer's own convention puts every numeric flag/index control word (`\fftype`, `\ffownhelp`, `\ffprot`, `\ffhaslistbox`, `\ffdefres`/`\ffres`) before every destination string (`\*\ffname`, `\*\ffdeftext`, `\*\ffhelptext`, then the `\*\ffl` entries), and `\*\ffname` itself before `\*\ffhelptext` within that second group. A dropDown always mints the explicit `\ffhaslistbox1`, never a bare `\ffhaslistbox`, whether or not it carries any options: [MS-DOC] 2.9.79 FFDataBits.fHasListBox must be set for a list-type field regardless of how many entries the list holds, and RTF 1.9.1's own Form Fields table states `\ffhaslistboxN` as a genuine N-parameterised control word ("1 if this field has list box attached to it, 0 otherwise"), so a conformant reader applying RTF's own general Value-word default would read a bare occurrence as 0/false, the opposite of what a dropdown actually has -- this codec's own reader never has to apply that default here at all, since it has no `\ffhaslistbox` case anywhere and simply does not consult the control word on the way in. `\ffres`/`\ffdefres` are minted alongside it only when the field's own recorded selection genuinely names one of `options`, and omitted together in both remaining cases — no selection was ever recorded, or the recorded `value` names none of `options` — rather than guess an index that would silently point at the wrong entry. The unmatched-value case is reported through the same diagnostic sink every other unrepresentable construct in this writer uses, since it is real, signalable data loss rather than an absence. The never-selected case is different: a real producer (PHPRtfLite, per this package's own read.test.ts fixtures) spells "no current selection" as `\ffres25` (FFDataBits' own undefined-selection sentinel) plus a genuine `\ffdefres0`, not by omitting both — but this writer cannot emit that exact form without reintroducing the ambiguity an earlier round of it removed, because this codec's own reader deliberately falls a sentinel `\ffres25` through to `\ffdefres` (to recover a real PHPRtfLite checkbox's meaningful reset default rather than reading it as unchecked), and that same fallback would read a written `\ffdefres0` back as "option 0 is selected" rather than "nothing is selected". Omitting both fields instead sidesteps that: this reader tolerates the omission cleanly and decodes it as an unset value with no ambiguity, at the cost of not matching the form a real producer would actually write for the identical case. [MS-DOC] 2.9.78 FFData.wDef "MUST exist if and only if" the field is a checkbox or dropdown is a real MS-DOC production rule that this omission does not satisfy: a producer omitting wDef is spec-noncompliant but demonstrably tolerated in practice, since this reader is built to survive real-world RTF, not just conformant RTF. Options beyond [MS-DOC] 2.9.78 FFData.hsttbDropList's own 25-entry limit are truncated, also with a diagnostic — not an arbitrary cutoff, since FFDataBits' own `iRes` field reserves index 25 as its "undefined selection" sentinel, so a 26th real entry would collide with it. When the recorded `value` genuinely matched one of the truncated-away entries, the resulting unmatched-value diagnostic names truncation as the reason rather than reusing the generic "does not match any of the field's own options" wording — the value did match something, until the cap removed it, and the two causes are distinguished so the message states the one that actually happened. A plainText control's own `value` is minted as `{\*\ffdeftext ...}` (FFData.xstzTextDef, "MUST exist if and only if" the field is a text field), distinct from the field's DISPLAYED text carried by its wrapped runs — a real, reachable case: documents.js's own PDF AcroForm-to-contentControl reconstruction hands a text field exactly this `{controlType:'plainText', value, ...}` shape for a real `/V` string. `value` names the control's CURRENT scalar value and `\ffdeftext` names its DEFAULT/reset text — a genuinely different fact, not merely a different spelling of the same one — so this mis-slot is reported through the diagnostic sink like every other cross-field case this writer names, even though (unlike those) the string itself is not dropped: it lands in the RTF byte stream, just under a field that reads back as something else (see the round-trip note at the end of this paragraph). `\ffprot1` ([MS-DOC] 2.9.79 FFDataBits.fProt), always written with its explicit parameter rather than bare, is minted whenever a control's `lock` is `content` or `both`, since both lock the control's own value; `lock: container` protects only the control's own removal, a fact RTF's form-field vocabulary has no bit for at all, so a `container` lock is dropped in full (nothing is written for it) and a `both` lock's own removal half is dropped alongside the `\ffprot1` its content half still writes — each reported through the diagnostic sink with a message naming which of the two actually happened, rather than one message describing both. `\ffownhelp1{\*\ffhelptext ...}` carries a control's `alias`, RTF's own closest analogue to docx `w:alias`/PDF AcroForm's `/TU` alternate description; the read side honours an explicit `\ffownhelp0` too, since FFDataBits.fOwnHelp being 0 means `xstzHelpText` "contains an empty or auto-generated string" rather than an author-set label, and promoting that text to `alias` regardless would misrepresent it -- but a bare, unparameterised `\ffownhelp` reads as true rather than following that same 0-default, since LibreOffice's own RTF exporter emits exactly that bare form whenever the control model exposes a HelpText property at all, alongside genuine author-set help text, and the literal spec default would otherwise silently discard it (see read.ts's own comment on applyFormFieldControlWord's "ffownhelp" case for the full citation). A checkbox's own `value` -- distinct from `checked` -- has no RTF spelling at all: a real, reachable case (pdf-codec's own AcroForm reading spreads a checkbox widget's `/V` export-value name, e.g. `'Yes'`, onto `value` alongside the boolean `checked` derived from that same `/V`) is reported through the diagnostic sink rather than silently dropped, since unlike a dropDown's `value` it can never match anything RTF's `\ffres`/`\ffdefres` can name. `value`/`checked`/`options` recorded on a controlType that has no concept of them at all -- a plainText field's `checked` or `options`, a checkbox's `options`, a dropDown's `checked` -- are each reported through the identical sink rather than the writer silently reading past a field it has no branch for. The plainText `value`->`\ffdeftext` minting described above is write-only: this codec's own reader does not restore `\ffdeftext` back onto `value` on the way in, since `value` names the control's CURRENT value and `\ffdeftext` is the field's DEFAULT/reset text -- for a text field the current value is whatever the wrapped runs actually carry, so a document built from a `value`-carrying plainText descriptor does not read back with that `value` on a round trip, a mismatch reported through the diagnostic sink at write time (see above) rather than left for a caller to discover only by round-tripping the document themselves.
|
|
146
|
+
|
|
140
147
|
### Deliberately not handled
|
|
141
148
|
|
|
142
149
|
Each of these is reported through a diagnostic rather than dropped silently — see [Diagnostics](#diagnostics).
|
|
143
150
|
|
|
144
|
-
| Construct
|
|
145
|
-
|
|
|
146
|
-
| Headers, footers, footnotes, endnotes, annotations
|
|
147
|
-
| Content controls
|
|
148
|
-
|
|
|
149
|
-
|
|
|
150
|
-
|
|
|
151
|
-
|
|
|
152
|
-
|
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
| Superscript/subscript (`\super`, `\sub`, `\upN`, `\dnN`), character scaling, kerning, background colour
|
|
156
|
-
| Right-to-left text (`\rtlch`, `\ltrch`, `\rtlpar`, `\rtlrow`, `\rtldoc`)
|
|
157
|
-
| Cell vertical alignment (`\clvertalt`/`\clvertalc`/`\clvertalb`), diagonal cell borders (`\cldglu`/`\cldgll`), `\clshdngN`
|
|
158
|
-
| A bookmark whose two halves straddle a table cell wall
|
|
151
|
+
| Construct | Why |
|
|
152
|
+
| ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
153
|
+
| Headers, footers, footnotes, endnotes, annotations | `ContentDocument`'s flat form has no page-furniture or note position for them. A footnote's real home is `document-schema.js`'s tree-only `definitions` table, which a codec producing the flat form cannot reach. |
|
|
154
|
+
| Content controls beyond RTF's own form-field vocabulary (richText, comboBox, date, picture, repeatingSection, button, index, group) | **RTF 1.9.1 specifies nothing for these.** It predates OOXML's `w:sdt`: its "Custom XML Tags" (`\xmlopen`/`\xmlclose`) are a bare namespace/name tag with no type, lock, alias or value, and `\*\datastore` is an opaque blob whose "format ... is unknown to RTF" by the spec's own words. `\*\formfield` (see the Scope table above) is the one real analogue RTF has, and covers plainText/checkbox/dropDown only. |
|
|
155
|
+
| East Asian DBCS code pages (932, 936, 949, 950, 1361) | Each needs a ~20k-entry table and its own lead-byte state machine. A document declaring one decodes through cp1252 and says so. |
|
|
156
|
+
| Code page 42 (`SYMBOL_CHARSET`) | Not an encoding: its bytes are glyph indices into whichever symbol font the run names, so there is no correct Unicode for them without that font's own cmap. |
|
|
157
|
+
| Metafile and bitmap pictures (`\wmetafileN`, `\emfblip`, `\dibitmapN`, `\wbitmapN`, `\macpict`) | `ContentImageBlock` carries PNG and JPEG only. |
|
|
158
|
+
| A picture with no stated size | `ContentImageBlock` requires a positive width and height, and deriving them from the payload would need an image decoder this package deliberately does not carry. |
|
|
159
|
+
| Nested tables (`\nestcell`/`\nestrow`) | Read as ordinary cell content; the inner table's own structure is not reconstructed. |
|
|
160
|
+
| Drawing objects (`\do`, `\shp`) | **A schema gap, not a container one.** These are Word's own native in-document vector-drawing layer, not an OLE embed -- there is no raw drawing-shape `ContentBlock` for a wordprocessing section's block flow to land in (only `ContentEmbeddedObjectBlock`, which names a whole embedded document, not a shape), so a `\do`/`\shp` construct is dropped regardless of the container work [Embedded objects](#embedded-objects) below did for `\object`. |
|
|
161
|
+
| A real, foreign `\object`'s OLE data | This package's own `\objdata` payload round-trips fully (see [Embedded objects](#embedded-objects)); a real Word-authored OLESaveToStream structure (an actual embedded `.xls`/`.doc`/OLE-control payload) has no decoder here and degrades with a diagnostic, recovering `\result`'s own fallback paragraphs when present instead of the real object. |
|
|
162
|
+
| Superscript/subscript (`\super`, `\sub`, `\upN`, `\dnN`), character scaling, kerning, background colour | **A schema gap, not an RTF one.** `ContentRun` carries no vertical-alignment field at all -- `epub-codec` reports the identical gap for its own `<sub>`/`<sup>`, and `ooxml.js`'s docx reader has no `w:vertAlign` handling either. Closing it is a change to `document-schema.js` and every codec that would then carry it, not to this one. |
|
|
163
|
+
| Right-to-left text (`\rtlch`, `\ltrch`, `\rtlpar`, `\rtlrow`, `\rtldoc`) | The same shape of gap: no `ContentDocument` field carries text direction, at any of the four scopes RTF states it at. |
|
|
164
|
+
| Cell vertical alignment (`\clvertalt`/`\clvertalc`/`\clvertalb`), diagonal cell borders (`\cldglu`/`\cldgll`), `\clshdngN` | `ContentTableCell.borders` has no diagonal member, so a diagonal rule is not a side; `\clshdngN`'s own shading percentage names a genuine two-colour pattern fill, which `document-schema.js`'s `ContentCellFill` can now express (ExaDev/documents.js#951) but this package does not yet resolve either `\clcbpatN`'s own foreground colour or `\clshdngN`'s percentage into one -- `\clcbpatN`'s background colour alone still reads and writes as a `'solid'` fill. |
|
|
165
|
+
| A bookmark whose two halves straddle a table cell wall | `document-schema.js` ratifies this as a drop rather than a shape to repair: each block list is its own bracket scope, and pairing across two of them would need the marker ids its contract deliberately refuses. |
|
|
166
|
+
| A contentControl extent that crosses another contentControl extent in the same paragraph | RTF's `\*\formfield` destination can only nest, never cross: a `{\field...}` group nests cleanly inside another one's own `\fldrslt`, but two extents that start-before/end-after each other have no valid brace sequence, since whichever closes second would close the other's own braces instead of its own. |
|
|
167
|
+
| A `table` or `pageBreak` block placed directly inside a table cell | `writeCellBlocks` writes a cell's own content as a run of `\intbl` `<pict>`/`<obj>`/paragraph groups, plus any bracketing `constructStart`/`constructEnd` markers spliced in inline exactly as `writeBlock` does at the top level; an `image`/`embeddedObject` block borrows the identical `\intbl` shell a paragraph gets (a `\pict`/`\object` group inside an ordinary `\intbl` paragraph is a real, already-round-trippable shape -- see the tests around it), but a nested `table` needs its own `\itapN` row grammar this writer does not build, and a mid-row `\page` would `\pard`-reset the row's own `\intbl` state, so those two kinds are still dropped. |
|
|
168
|
+
|
|
169
|
+
## Embedded objects
|
|
170
|
+
|
|
171
|
+
RTF 1.9.1's own "Objects" section states an `\object`'s grammar precisely: `'{' \object (<objtype> & ...) <objdata> <result> '}'`, where `\objdata` is `'{\*' \objdata (<objalias>? & <objsect>?) <data> '}'` and `<data>` is the identical `(\binN #BDATA) | #SDATA` production `\pict`'s own payload uses. The spec is direct about what that data actually is: "When the object is an OLE embedded or linked object, the data part of the object is the structure produced by the OLESaveToStream function." [MS-OLEDS] 2.2.5 names that structure precisely, and it has four fields, not two: an `ObjectHeader` (OLEVersion, a FormatID of `0x00000002`, then ClassName/TopicName/ItemName as length-prefixed ANSI strings), a `NativeDataSize` and the `NativeData` that size names (the real OLE compound file is _NativeData_, one field inside the structure, not the whole of `\objdata`), and — mandatory, not optional — `Presentation`: "This MUST be a MetaFilePresentationObject, a BitmapPresentationObject, a DIBPresentationObject, a StandardClipboardFormatPresentationObject, or a RegisteredClipboardFormatPresentationObject." A reader that stops once `NativeData` ends is reading a truncated prefix of an `EmbeddedObject`, not a real one — a genuine OLE1.0 consumer keeps looking for `Presentation` and hits EOF instead. This package used to drop `\object` in both directions because it had no way to build or read a compound file at all; [`archive-codec`](../archive-codec/README.md) now ships exactly that (`writeCompoundFile`/`readCompoundFile`, [MS-CFB]), plus the `Package` stream wrapper (`writeOlePackage`/`readOlePackage`) real Word/PowerPoint embeds use inside it — the same pair `doc-codec`/`xls-codec`/`ppt-codec` already depend on `archive-codec` for. The `ObjectHeader`/`NativeDataSize`/`Presentation` envelope those bytes ride inside is this module's own responsibility (`src/embedded-object.ts`'s `writeObjectHeader`/`readObjectHeader` and `writePresentationObject`/`skipPresentationObject`), since archive-codec's own charter is container structure below OLE's own object-embedding framing, never that framing itself. `Presentation` is written as the smallest of the five legitimate shapes — a `StandardClipboardFormatPresentationObject` (2.2.3.2) wrapping a minimal 1x1 monochrome `CF_DIB` image — since the other four each need a full metafile/bitmap-record writer this module has no other use for; a real OLE1.0 consumer can genuinely decode and display it as the object's placeholder preview, not merely treat it as size-matching filler.
|
|
172
|
+
|
|
173
|
+
**What rides inside the compound file is this codec's own JSON, not a foreign format's bytes.** `rtf-codec` cannot depend on `ooxml.js`/`odf.js` — format codecs are peers in this family, never one another's dependency — so a `wordprocessing`/`presentation`/`spreadsheet`/`drawing`/`formula` `ContentEmbeddedObjectBlock`'s own nested `ContentDocument` cannot be re-serialised into a real docx/pptx/xlsx/odf/MathML byte stream the way a genuine OLE server would. What this codec can write and read back losslessly is its own `ContentDocument` (a plain, Zod-validated, JSON-serialisable value), so `src/embedded-object.ts` packages that JSON as the `Package` stream's own "file" — the identical slot a real embed's actual docx/xlsx bytes would occupy — wraps it in a real `[MS-CFB]` compound file, wraps that as an `ObjectHeader`+`NativeDataSize`+`NativeData`+`Presentation` `EmbeddedObject` structure, and hex-encodes the whole thing as `\objdata`. `objectKind`, `frame`, and the anchor fields all ride the same envelope, so nothing about the embed's position or kind depends on `\object`'s own `\objw`/`\objh`/`\objclass` control words — those are still written, purely as the size hint and class label a reader that cannot decode `\objdata` at all would fall back to, matching the spec's own advice that a producer supply them "to maintain backward compatibility."
|
|
174
|
+
|
|
175
|
+
**Reading is honest about what it can and cannot decode.** A real Word-authored `\object` — an actual embedded `.xls` range, a Windows Media Player control, an Equation Editor formula — carries a real OLESaveToStream `NativeData` with no JSON envelope inside it, and decoding that would need this package to understand every OLE server's own on-disk format, which is out of scope for the same reason a metafile picture is: no image/OLE decoder lives here. `readEmbeddedObjectData` tries the one path it can (`ObjectHeader` parse -> `NativeData` slice -> compound file -> `Package` stream -> JSON -> `ContentEmbeddedObjectSchema` -> confirm `Presentation` is genuinely present and well-formed) and returns `undefined` for anything else, degrading with `RtfDiagnosticCodes.EMBEDDED_OBJECT_UNREADABLE` rather than throwing — one unreadable `\object` must not fail the whole document. A payload whose `NativeData` is genuinely this package's own JSON but whose mandatory `Presentation` field is missing or malformed is rejected too, at the same tier: it is not a shape this writer ever produced, so it is not treated as a truncated match. On that degrade path, `\object`'s own `\result` fallback — the rendered preview a non-`\object`-aware reader would show instead, per the spec's own advice that this "allows RTF readers that do not understand objects ... to use the current result, in place of the object, to maintain appearance" — is exactly what this reader needs too, so `\result`'s own paragraphs (RTF 1.9.1's `<result> = '{' \result <para>+ '}'`) are read as ordinary body content in the object's place; the `\objw`/`\objh` size hint that has nowhere left to land is folded into the `EMBEDDED_OBJECT_UNREADABLE` diagnostic's own message instead of being silently discarded, reporting whichever of the two is actually present when only one was stated. `\result`'s own content is rendered into a totally isolated scratch accumulator the moment its group is seen, sharing no paragraph, block list, or table state with whatever the surrounding document was already accumulating around `\object`, and is spliced into `\object`'s own place — in the open table cell's own blocks or the section's, whichever `\object` itself sits in — only once `\object`'s group closes and `\objdata` is confirmed never to have decoded; if `\objdata` does decode, the scratch content is discarded outright rather than spliced in. That isolation is what makes the outcome correct regardless of which of `<objdata>`/`<result>` the producer wrote first, without predicting it via a lookahead that would have to independently re-derive every rule the real, group-aware parse already applies: earlier revisions tried a flat token scan ahead of `\objdata`'s own group (which disagreed with the real parse on a payload nesting a spec-legal `{\*\objalias ...}`/`{\*\objsect ...}` sub-group, since RTF 1.9.1's own `<objdata>` production allows both directly inside `\objdata`'s braces and the flat scan folded their bytes into the payload it predicted from, where the real read correctly skips them), then a block-index retraction scheme keyed to the section's or cell's own block count at the moment `\result` opened and closed (which mistook any text the surrounding paragraph was still accumulating, unclosed, when `\object` began for `\result`'s own content, and left a bare-inline `\result` with no trailing `\par` — legal per that same `<result>` production, since the group's own closing brace can stand in for the final paragraph's `\par` — entirely unretracted, since it had closed no block for the index range to name). The isolated scratch accumulator has neither failure mode: it force-closes whatever paragraph `\result`'s own content was still accumulating exactly as a table cell's `\cell` or the document's own end already do, and its finished blocks are identified by construction rather than by an index range into a list something else was concurrently writing to. A second `\result` sibling (RTF's own grammar allows only one, but a malformed producer can still write two) is recognised as a duplicate — mirroring how a second `\objdata` sibling is already handled — and discarded with its own diagnostic rather than silently overwriting the first's recovered content. An `\object` with no `\objdata` destination at all is a distinct case from one whose `\objdata` merely fails to decode: `\result`'s content still recovers, but is reported with its own diagnostic, since a substitution a caller cannot see is indistinguishable from a reader that dropped the construct outright.
|
|
176
|
+
|
|
177
|
+
`writeCompoundFile` cannot yet set a root-storage CLSID ([MS-CFB] 2.6.1's `Root Directory Entry`), so even a correctly-framed `\objdata` payload will not self-identify its object class to a real OLE consumer the way a genuine Windows Packager-authored compound file does — a gap in `archive-codec`, not in this module's own framing.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { readRtfContent, writeRtfContent } from "rtf-codec";
|
|
181
|
+
|
|
182
|
+
const written = writeRtfContent({
|
|
183
|
+
kind: "wordprocessing",
|
|
184
|
+
metadata: {},
|
|
185
|
+
sections: [
|
|
186
|
+
{
|
|
187
|
+
pageSize: { widthPt: 612, heightPt: 792 },
|
|
188
|
+
margins: { topPt: 72, rightPt: 72, bottomPt: 72, leftPt: 72 },
|
|
189
|
+
blocks: [
|
|
190
|
+
{
|
|
191
|
+
kind: "embeddedObject",
|
|
192
|
+
objectKind: "spreadsheet",
|
|
193
|
+
frame: { xPt: 0, yPt: 0, widthPt: 200, heightPt: 100 },
|
|
194
|
+
document: { kind: "spreadsheet", metadata: {}, sheets: [] },
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// \objdata's own NativeData field (inside the ObjectHeader/NativeDataSize/NativeData/Presentation envelope this writer produces) is a real [MS-CFB] compound file -- readRtfContent decodes the whole envelope back into the identical objectKind/frame/document.
|
|
202
|
+
const { document } = readRtfContent(written);
|
|
203
|
+
```
|
|
159
204
|
|
|
160
205
|
## Diagnostics
|
|
161
206
|
|
|
@@ -179,7 +224,7 @@ The throw tier is `RtfNotAnRtfDocumentError` (no `{\rtf` header), `RtfInputTooLa
|
|
|
179
224
|
|
|
180
225
|
## Dependency choices
|
|
181
226
|
|
|
182
|
-
`document-schema.js`
|
|
227
|
+
`document-schema.js` for the content pivot, `archive-codec` for the `[MS-CFB]` container [Embedded objects](#embedded-objects) needs, and `zod`. Every third-party RTF library is banned **by name** in this package's own `eslint.config.ts` — `rtf-parser`, `rtf.js`, `rtf-stream-parser`, `node-rtf`, `jsrtf`, `@shelf/rtf-to-html` — the same bet `markdown-codec` makes against micromark/remark/marked and `pdf-codec` makes against pdf-lib/pdfjs-dist. Depending on one would defeat the reason this package exists. `archive-codec` is not such a library: it is zero document-format knowledge, the same sibling `doc-codec`/`xls-codec`/`ppt-codec` already depend on for their own `[MS-CFB]` container, not an RTF-aware dependency this package's own bet is against.
|
|
183
228
|
|
|
184
229
|
`iconv-lite` is banned for a second reason on top of that: it is Node-only (it is built on `Buffer`), so depending on it would break this package's Worker isomorphism. The code-page tables in `src/codepage.ts` exist instead.
|
|
185
230
|
|
|
@@ -195,7 +240,7 @@ Two places would have been tempting to write with a Node-only shortcut, and the
|
|
|
195
240
|
|
|
196
241
|
Both of the channels `document-schema.js` defines are live here.
|
|
197
242
|
|
|
198
|
-
**Channel 1, the harmonised construct vocabulary.** Bookmarks read and write as `anchor` descriptors, and the whole `<chrev>` revision-mark family as `provenance` descriptors. Which of the two flat encodings a construct takes is decided by what it actually spans, exactly as the schema requires: a bookmark opening and closing inside one paragraph is a `RunConstructExtent` on that paragraph, one spanning whole paragraphs is a `constructStart`/`constructEnd` marker pair, and a revision mark — being a character property — is always the former. Content controls are absent because RTF has
|
|
243
|
+
**Channel 1, the harmonised construct vocabulary.** Bookmarks read and write as `anchor` descriptors, and the whole `<chrev>` revision-mark family as `provenance` descriptors. Which of the two flat encodings a construct takes is decided by what it actually spans, exactly as the schema requires: a bookmark opening and closing inside one paragraph is a `RunConstructExtent` on that paragraph, one spanning whole paragraphs is a `constructStart`/`constructEnd` marker pair, and a revision mark — being a character property — is always the former. A form field (`\*\formfield`, gated on FORMTEXT/FORMCHECKBOX/FORMDROPDOWN) reads and writes as a `contentControl` construct the same way: always a `RunConstructExtent`, since one `{\field ...}` group is always inline and never spans a paragraph boundary. Content controls beyond that (a docx-style rich-text SDT and the rest) are still absent because RTF has no spelling for them at all; see the gap table above.
|
|
199
244
|
|
|
200
245
|
**Channel 2, the residue channel.** `SourceFormatSchema` gained its `rtf` member, so this codec can now quarantine what no semantic field carries. `\bkmkcolfN`/`\bkmkcollN` — a bookmark's table-column range — ride the anchor descriptor's own `source`, and the writer restores them verbatim inside its `{\*\bkmkstart …}` when the residue names `rtf` as its format, leaving another format's residue untouched. That decidability is the whole point of the `format` field.
|
|
201
246
|
|
package/dist/base64.d.cts
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
declare function bytesToBase64(input: Uint8Array): string;
|
|
3
3
|
declare function base64ToBytes(input: string): Uint8Array | undefined;
|
|
4
4
|
declare function bytesToHex(input: Uint8Array): string;
|
|
5
|
-
declare function hexToBytes(input: string): Uint8Array
|
|
5
|
+
declare function hexToBytes(input: string): Uint8Array<ArrayBuffer>;
|
|
6
6
|
//#endregion
|
|
7
7
|
export { base64ToBytes, bytesToBase64, bytesToHex, hexToBytes };
|
package/dist/base64.d.ts
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
declare function bytesToBase64(input: Uint8Array): string;
|
|
3
3
|
declare function base64ToBytes(input: string): Uint8Array | undefined;
|
|
4
4
|
declare function bytesToHex(input: Uint8Array): string;
|
|
5
|
-
declare function hexToBytes(input: string): Uint8Array
|
|
5
|
+
declare function hexToBytes(input: string): Uint8Array<ArrayBuffer>;
|
|
6
6
|
//#endregion
|
|
7
7
|
export { base64ToBytes, bytesToBase64, bytesToHex, hexToBytes };
|
package/dist/codepage.d.cts
CHANGED
package/dist/codepage.d.ts
CHANGED
package/dist/constructs.cjs
CHANGED
|
@@ -94,6 +94,39 @@ function dttmFromIso(dateIso) {
|
|
|
94
94
|
function pad(value, width) {
|
|
95
95
|
return String(value).padStart(width, "0");
|
|
96
96
|
}
|
|
97
|
+
const FORM_FIELD_RESULT_UNDEFINED = 25;
|
|
98
|
+
const FORM_FIELD_CHECKBOX_INSTRUCTION = /^\s*FORMCHECKBOX\b/i;
|
|
99
|
+
const FORM_FIELD_DROPDOWN_INSTRUCTION = /^\s*FORMDROPDOWN\b/i;
|
|
100
|
+
const FORM_FIELD_TEXT_INSTRUCTION = /^\s*FORMTEXT\b/i;
|
|
101
|
+
function formFieldControlType(instruction) {
|
|
102
|
+
if (FORM_FIELD_CHECKBOX_INSTRUCTION.test(instruction)) return "checkbox";
|
|
103
|
+
if (FORM_FIELD_DROPDOWN_INSTRUCTION.test(instruction)) return "dropDown";
|
|
104
|
+
if (FORM_FIELD_TEXT_INSTRUCTION.test(instruction)) return "plainText";
|
|
105
|
+
}
|
|
106
|
+
function formFieldContentControl(instruction, formField) {
|
|
107
|
+
const controlType = formFieldControlType(instruction);
|
|
108
|
+
if (controlType === void 0) return;
|
|
109
|
+
const descriptor = {
|
|
110
|
+
kind: "contentControl",
|
|
111
|
+
controlType
|
|
112
|
+
};
|
|
113
|
+
if (formField === void 0) return descriptor;
|
|
114
|
+
const name = formField.name.trim();
|
|
115
|
+
if (name.length > 0) descriptor.tag = name;
|
|
116
|
+
const helpText = formField.helpText.trim();
|
|
117
|
+
if (formField.ownHelp && helpText.length > 0) descriptor.alias = helpText;
|
|
118
|
+
if (formField.protectedField) descriptor.lock = "content";
|
|
119
|
+
if (controlType === "checkbox") descriptor.checked = ((formField.resultIndex === FORM_FIELD_RESULT_UNDEFINED ? void 0 : formField.resultIndex) ?? formField.defaultResultIndex ?? 0) !== 0;
|
|
120
|
+
else if (controlType === "dropDown" && formField.listItems.length > 0) {
|
|
121
|
+
descriptor.options = [...formField.listItems];
|
|
122
|
+
const selectedIndex = (formField.resultIndex === FORM_FIELD_RESULT_UNDEFINED ? void 0 : formField.resultIndex) ?? formField.defaultResultIndex;
|
|
123
|
+
if (selectedIndex !== void 0 && selectedIndex >= 0 && selectedIndex < formField.listItems.length) {
|
|
124
|
+
const selected = formField.listItems[selectedIndex];
|
|
125
|
+
if (selected !== void 0) descriptor.value = selected;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return descriptor;
|
|
129
|
+
}
|
|
97
130
|
function coalesceRunConstructs(perRun) {
|
|
98
131
|
const open = /* @__PURE__ */ new Map();
|
|
99
132
|
const out = [];
|
|
@@ -125,6 +158,8 @@ exports.bookmarkAnchorDescriptor = bookmarkAnchorDescriptor;
|
|
|
125
158
|
exports.bookmarkResidueControlWords = bookmarkResidueControlWords;
|
|
126
159
|
exports.coalesceRunConstructs = coalesceRunConstructs;
|
|
127
160
|
exports.dttmFromIso = dttmFromIso;
|
|
161
|
+
exports.formFieldContentControl = formFieldContentControl;
|
|
162
|
+
exports.formFieldControlType = formFieldControlType;
|
|
128
163
|
exports.hasRevision = hasRevision;
|
|
129
164
|
exports.isBookmarkAnchor = isBookmarkAnchor;
|
|
130
165
|
exports.isoFromDttm = isoFromDttm;
|
package/dist/constructs.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnchorDescriptor, ConstructDescriptor, ProvenanceDescriptor, RunConstructExtent } from "document-schema.js";
|
|
1
|
+
import { AnchorDescriptor, ConstructDescriptor, ContentControlDescriptor, ContentControlType, ProvenanceDescriptor, RunConstructExtent } from "document-schema.js";
|
|
2
2
|
//#region src/constructs.d.ts
|
|
3
3
|
declare const RTF_SOURCE_FORMAT: "rtf";
|
|
4
4
|
interface BookmarkColumnRange {
|
|
@@ -26,6 +26,17 @@ declare function hasRevision(state: RevisionState): boolean;
|
|
|
26
26
|
declare function provenanceDescriptors(state: RevisionState, authors: readonly string[]): ProvenanceDescriptor[];
|
|
27
27
|
declare function isoFromDttm(value: number): string | undefined;
|
|
28
28
|
declare function dttmFromIso(dateIso: string): number | undefined;
|
|
29
|
+
interface RtfFormFieldData {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly helpText: string;
|
|
32
|
+
readonly ownHelp: boolean;
|
|
33
|
+
readonly listItems: readonly string[];
|
|
34
|
+
readonly resultIndex: number | undefined;
|
|
35
|
+
readonly defaultResultIndex: number | undefined;
|
|
36
|
+
readonly protectedField: boolean;
|
|
37
|
+
}
|
|
38
|
+
declare function formFieldControlType(instruction: string): ContentControlType | undefined;
|
|
39
|
+
declare function formFieldContentControl(instruction: string, formField: RtfFormFieldData | undefined): ContentControlDescriptor | undefined;
|
|
29
40
|
declare function coalesceRunConstructs(perRun: readonly (readonly ConstructDescriptor[])[]): RunConstructExtent[];
|
|
30
41
|
//#endregion
|
|
31
|
-
export { BookmarkColumnRange, NO_REVISION, RTF_SOURCE_FORMAT, RevisionState, bookmarkAnchorDescriptor, bookmarkResidueControlWords, coalesceRunConstructs, dttmFromIso, hasRevision, isBookmarkAnchor, isoFromDttm, provenanceDescriptors };
|
|
42
|
+
export { BookmarkColumnRange, NO_REVISION, RTF_SOURCE_FORMAT, RevisionState, RtfFormFieldData, bookmarkAnchorDescriptor, bookmarkResidueControlWords, coalesceRunConstructs, dttmFromIso, formFieldContentControl, formFieldControlType, hasRevision, isBookmarkAnchor, isoFromDttm, provenanceDescriptors };
|
package/dist/constructs.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnchorDescriptor, ConstructDescriptor, ProvenanceDescriptor, RunConstructExtent } from "document-schema.js";
|
|
1
|
+
import { AnchorDescriptor, ConstructDescriptor, ContentControlDescriptor, ContentControlType, ProvenanceDescriptor, RunConstructExtent } from "document-schema.js";
|
|
2
2
|
//#region src/constructs.d.ts
|
|
3
3
|
declare const RTF_SOURCE_FORMAT: "rtf";
|
|
4
4
|
interface BookmarkColumnRange {
|
|
@@ -26,6 +26,17 @@ declare function hasRevision(state: RevisionState): boolean;
|
|
|
26
26
|
declare function provenanceDescriptors(state: RevisionState, authors: readonly string[]): ProvenanceDescriptor[];
|
|
27
27
|
declare function isoFromDttm(value: number): string | undefined;
|
|
28
28
|
declare function dttmFromIso(dateIso: string): number | undefined;
|
|
29
|
+
interface RtfFormFieldData {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly helpText: string;
|
|
32
|
+
readonly ownHelp: boolean;
|
|
33
|
+
readonly listItems: readonly string[];
|
|
34
|
+
readonly resultIndex: number | undefined;
|
|
35
|
+
readonly defaultResultIndex: number | undefined;
|
|
36
|
+
readonly protectedField: boolean;
|
|
37
|
+
}
|
|
38
|
+
declare function formFieldControlType(instruction: string): ContentControlType | undefined;
|
|
39
|
+
declare function formFieldContentControl(instruction: string, formField: RtfFormFieldData | undefined): ContentControlDescriptor | undefined;
|
|
29
40
|
declare function coalesceRunConstructs(perRun: readonly (readonly ConstructDescriptor[])[]): RunConstructExtent[];
|
|
30
41
|
//#endregion
|
|
31
|
-
export { BookmarkColumnRange, NO_REVISION, RTF_SOURCE_FORMAT, RevisionState, bookmarkAnchorDescriptor, bookmarkResidueControlWords, coalesceRunConstructs, dttmFromIso, hasRevision, isBookmarkAnchor, isoFromDttm, provenanceDescriptors };
|
|
42
|
+
export { BookmarkColumnRange, NO_REVISION, RTF_SOURCE_FORMAT, RevisionState, RtfFormFieldData, bookmarkAnchorDescriptor, bookmarkResidueControlWords, coalesceRunConstructs, dttmFromIso, formFieldContentControl, formFieldControlType, hasRevision, isBookmarkAnchor, isoFromDttm, provenanceDescriptors };
|
package/dist/constructs.js
CHANGED
|
@@ -93,6 +93,39 @@ function dttmFromIso(dateIso) {
|
|
|
93
93
|
function pad(value, width) {
|
|
94
94
|
return String(value).padStart(width, "0");
|
|
95
95
|
}
|
|
96
|
+
const FORM_FIELD_RESULT_UNDEFINED = 25;
|
|
97
|
+
const FORM_FIELD_CHECKBOX_INSTRUCTION = /^\s*FORMCHECKBOX\b/i;
|
|
98
|
+
const FORM_FIELD_DROPDOWN_INSTRUCTION = /^\s*FORMDROPDOWN\b/i;
|
|
99
|
+
const FORM_FIELD_TEXT_INSTRUCTION = /^\s*FORMTEXT\b/i;
|
|
100
|
+
function formFieldControlType(instruction) {
|
|
101
|
+
if (FORM_FIELD_CHECKBOX_INSTRUCTION.test(instruction)) return "checkbox";
|
|
102
|
+
if (FORM_FIELD_DROPDOWN_INSTRUCTION.test(instruction)) return "dropDown";
|
|
103
|
+
if (FORM_FIELD_TEXT_INSTRUCTION.test(instruction)) return "plainText";
|
|
104
|
+
}
|
|
105
|
+
function formFieldContentControl(instruction, formField) {
|
|
106
|
+
const controlType = formFieldControlType(instruction);
|
|
107
|
+
if (controlType === void 0) return;
|
|
108
|
+
const descriptor = {
|
|
109
|
+
kind: "contentControl",
|
|
110
|
+
controlType
|
|
111
|
+
};
|
|
112
|
+
if (formField === void 0) return descriptor;
|
|
113
|
+
const name = formField.name.trim();
|
|
114
|
+
if (name.length > 0) descriptor.tag = name;
|
|
115
|
+
const helpText = formField.helpText.trim();
|
|
116
|
+
if (formField.ownHelp && helpText.length > 0) descriptor.alias = helpText;
|
|
117
|
+
if (formField.protectedField) descriptor.lock = "content";
|
|
118
|
+
if (controlType === "checkbox") descriptor.checked = ((formField.resultIndex === FORM_FIELD_RESULT_UNDEFINED ? void 0 : formField.resultIndex) ?? formField.defaultResultIndex ?? 0) !== 0;
|
|
119
|
+
else if (controlType === "dropDown" && formField.listItems.length > 0) {
|
|
120
|
+
descriptor.options = [...formField.listItems];
|
|
121
|
+
const selectedIndex = (formField.resultIndex === FORM_FIELD_RESULT_UNDEFINED ? void 0 : formField.resultIndex) ?? formField.defaultResultIndex;
|
|
122
|
+
if (selectedIndex !== void 0 && selectedIndex >= 0 && selectedIndex < formField.listItems.length) {
|
|
123
|
+
const selected = formField.listItems[selectedIndex];
|
|
124
|
+
if (selected !== void 0) descriptor.value = selected;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return descriptor;
|
|
128
|
+
}
|
|
96
129
|
function coalesceRunConstructs(perRun) {
|
|
97
130
|
const open = /* @__PURE__ */ new Map();
|
|
98
131
|
const out = [];
|
|
@@ -118,4 +151,4 @@ function coalesceRunConstructs(perRun) {
|
|
|
118
151
|
return out.sort((left, right) => left.startRun - right.startRun || left.endRun - right.endRun);
|
|
119
152
|
}
|
|
120
153
|
//#endregion
|
|
121
|
-
export { NO_REVISION, RTF_SOURCE_FORMAT, bookmarkAnchorDescriptor, bookmarkResidueControlWords, coalesceRunConstructs, dttmFromIso, hasRevision, isBookmarkAnchor, isoFromDttm, provenanceDescriptors };
|
|
154
|
+
export { NO_REVISION, RTF_SOURCE_FORMAT, bookmarkAnchorDescriptor, bookmarkResidueControlWords, coalesceRunConstructs, dttmFromIso, formFieldContentControl, formFieldControlType, hasRevision, isBookmarkAnchor, isoFromDttm, provenanceDescriptors };
|
|
@@ -15,13 +15,15 @@ declare const RtfDiagnosticCodes: {
|
|
|
15
15
|
readonly UNSUPPORTED_CODEPAGE: "rtf/unsupported-codepage";
|
|
16
16
|
readonly UNSUPPORTED_PICTURE_FORMAT: "rtf/unsupported-picture-format";
|
|
17
17
|
readonly PICTURE_SIZE_UNSTATED: "rtf/picture-size-unstated";
|
|
18
|
+
readonly EMBEDDED_OBJECT_UNREADABLE: "rtf/embedded-object-unreadable";
|
|
18
19
|
readonly TABLE_ROW_WITHOUT_DEFINITION: "rtf/table-row-without-definition";
|
|
19
20
|
readonly TABLE_COLUMN_WIDTH_INVALID: "rtf/table-column-width-invalid";
|
|
20
21
|
readonly NESTED_TABLE_FLATTENED: "rtf/nested-table-flattened";
|
|
21
22
|
readonly SECTION_BREAK_UNREPRESENTED: "rtf/section-break-unrepresented";
|
|
22
23
|
readonly BOOKMARK_UNPAIRED: "rtf/bookmark-unpaired";
|
|
24
|
+
readonly FORM_FIELD_SPAN_DROPPED: "rtf/form-field-span-dropped";
|
|
25
|
+
readonly FORM_FIELD_KEYWORD_LOST: "rtf/form-field-keyword-lost";
|
|
23
26
|
readonly CONSTRUCT_UNREPRESENTED: "rtf/construct-unrepresented";
|
|
24
|
-
readonly EMBEDDED_OBJECT_DROPPED: "rtf/embedded-object-dropped";
|
|
25
27
|
readonly PACKAGE_TABLE_DROPPED: "rtf/package-table-dropped";
|
|
26
28
|
};
|
|
27
29
|
declare class RtfParseError extends Error {
|
|
@@ -15,13 +15,15 @@ declare const RtfDiagnosticCodes: {
|
|
|
15
15
|
readonly UNSUPPORTED_CODEPAGE: "rtf/unsupported-codepage";
|
|
16
16
|
readonly UNSUPPORTED_PICTURE_FORMAT: "rtf/unsupported-picture-format";
|
|
17
17
|
readonly PICTURE_SIZE_UNSTATED: "rtf/picture-size-unstated";
|
|
18
|
+
readonly EMBEDDED_OBJECT_UNREADABLE: "rtf/embedded-object-unreadable";
|
|
18
19
|
readonly TABLE_ROW_WITHOUT_DEFINITION: "rtf/table-row-without-definition";
|
|
19
20
|
readonly TABLE_COLUMN_WIDTH_INVALID: "rtf/table-column-width-invalid";
|
|
20
21
|
readonly NESTED_TABLE_FLATTENED: "rtf/nested-table-flattened";
|
|
21
22
|
readonly SECTION_BREAK_UNREPRESENTED: "rtf/section-break-unrepresented";
|
|
22
23
|
readonly BOOKMARK_UNPAIRED: "rtf/bookmark-unpaired";
|
|
24
|
+
readonly FORM_FIELD_SPAN_DROPPED: "rtf/form-field-span-dropped";
|
|
25
|
+
readonly FORM_FIELD_KEYWORD_LOST: "rtf/form-field-keyword-lost";
|
|
23
26
|
readonly CONSTRUCT_UNREPRESENTED: "rtf/construct-unrepresented";
|
|
24
|
-
readonly EMBEDDED_OBJECT_DROPPED: "rtf/embedded-object-dropped";
|
|
25
27
|
readonly PACKAGE_TABLE_DROPPED: "rtf/package-table-dropped";
|
|
26
28
|
};
|
|
27
29
|
declare class RtfParseError extends Error {
|
package/dist/diagnostics.cjs
CHANGED
|
@@ -8,13 +8,15 @@ const RtfDiagnosticCodes = {
|
|
|
8
8
|
UNSUPPORTED_CODEPAGE: "rtf/unsupported-codepage",
|
|
9
9
|
UNSUPPORTED_PICTURE_FORMAT: "rtf/unsupported-picture-format",
|
|
10
10
|
PICTURE_SIZE_UNSTATED: "rtf/picture-size-unstated",
|
|
11
|
+
EMBEDDED_OBJECT_UNREADABLE: "rtf/embedded-object-unreadable",
|
|
11
12
|
TABLE_ROW_WITHOUT_DEFINITION: "rtf/table-row-without-definition",
|
|
12
13
|
TABLE_COLUMN_WIDTH_INVALID: "rtf/table-column-width-invalid",
|
|
13
14
|
NESTED_TABLE_FLATTENED: "rtf/nested-table-flattened",
|
|
14
15
|
SECTION_BREAK_UNREPRESENTED: "rtf/section-break-unrepresented",
|
|
15
16
|
BOOKMARK_UNPAIRED: "rtf/bookmark-unpaired",
|
|
17
|
+
FORM_FIELD_SPAN_DROPPED: "rtf/form-field-span-dropped",
|
|
18
|
+
FORM_FIELD_KEYWORD_LOST: "rtf/form-field-keyword-lost",
|
|
16
19
|
CONSTRUCT_UNREPRESENTED: "rtf/construct-unrepresented",
|
|
17
|
-
EMBEDDED_OBJECT_DROPPED: "rtf/embedded-object-dropped",
|
|
18
20
|
PACKAGE_TABLE_DROPPED: "rtf/package-table-dropped"
|
|
19
21
|
};
|
|
20
22
|
var RtfParseError = class extends Error {
|