rtf-codec 0.0.0 → 1.1.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/LICENSE +21 -0
- package/README.md +226 -0
- package/dist/base64.cjs +66 -0
- package/dist/base64.d.cts +7 -0
- package/dist/base64.d.ts +7 -0
- package/dist/base64.js +62 -0
- package/dist/bytes.cjs +25 -0
- package/dist/bytes.d.cts +6 -0
- package/dist/bytes.d.ts +6 -0
- package/dist/bytes.js +22 -0
- package/dist/cell-format.cjs +142 -0
- package/dist/cell-format.d.cts +25 -0
- package/dist/cell-format.d.ts +25 -0
- package/dist/cell-format.js +137 -0
- package/dist/codec.cjs +29 -0
- package/dist/codec.d.cts +2344 -0
- package/dist/codec.d.ts +2344 -0
- package/dist/codec.js +26 -0
- package/dist/codepage.cjs +98 -0
- package/dist/codepage.d.cts +10 -0
- package/dist/codepage.d.ts +10 -0
- package/dist/codepage.js +92 -0
- package/dist/constructs.cjs +131 -0
- package/dist/constructs.d.cts +31 -0
- package/dist/constructs.d.ts +31 -0
- package/dist/constructs.js +121 -0
- package/dist/diagnostics-BgG_KAiN.d.cts +52 -0
- package/dist/diagnostics-BgG_KAiN.d.ts +52 -0
- package/dist/diagnostics.cjs +76 -0
- package/dist/diagnostics.d.cts +2 -0
- package/dist/diagnostics.d.ts +2 -0
- package/dist/diagnostics.js +68 -0
- package/dist/group.cjs +40 -0
- package/dist/group.d.cts +11 -0
- package/dist/group.d.ts +11 -0
- package/dist/group.js +38 -0
- package/dist/header.cjs +433 -0
- package/dist/header.d.cts +44 -0
- package/dist/header.d.ts +44 -0
- package/dist/header.js +431 -0
- package/dist/index.cjs +28 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/list-id.cjs +30 -0
- package/dist/list-id.d.cts +16 -0
- package/dist/list-id.d.ts +16 -0
- package/dist/list-id.js +27 -0
- package/dist/options.cjs +7 -0
- package/dist/options.d.cts +18 -0
- package/dist/options.d.ts +18 -0
- package/dist/options.js +5 -0
- package/dist/read.cjs +1211 -0
- package/dist/read.d.cts +16 -0
- package/dist/read.d.ts +16 -0
- package/dist/read.js +1209 -0
- package/dist/tokenize.cjs +155 -0
- package/dist/tokenize.d.cts +25 -0
- package/dist/tokenize.d.ts +25 -0
- package/dist/tokenize.js +154 -0
- package/dist/units.cjs +43 -0
- package/dist/units.d.cts +17 -0
- package/dist/units.d.ts +17 -0
- package/dist/units.js +29 -0
- package/dist/write.cjs +532 -0
- package/dist/write.d.cts +7 -0
- package/dist/write.d.ts +7 -0
- package/dist/write.js +530 -0
- package/package.json +95 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joseph Mearman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# rtf-codec
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ExaDev/documents.js/tree/main/packages/rtf-codec) [](https://www.npmjs.com/package/rtf-codec) [](https://www.npmjs.com/package/rtf-codec) [](https://github.com/ExaDev/documents.js/actions)
|
|
4
|
+
|
|
5
|
+
> A hand-written, dependency-minimal Rich Text Format codec: reads RTF into the shared [document-schema.js](../document-schema.js/README.md) content pivot, and writes deterministic, 7-bit-ASCII RTF back out. Built against Microsoft's own [RTF Specification, version 1.9.1](#the-specification) and [Zod 4](https://zod.dev), with no third-party RTF library.
|
|
6
|
+
|
|
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
|
+
|
|
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 (it has no content-control equivalent at all) — see [Deliberately not handled](#deliberately-not-handled), which says which of the two each row is.
|
|
10
|
+
|
|
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
|
+
|
|
13
|
+
What it is _not_ is another XML format. RTF is tokenised plain text with a brace-nested group and destination model, so none of the XML plumbing `ooxml.js` and `odf.js` share applies here: this package carries its own byte lexer, its own destination state machine, its own `\uN`/`\ucN` Unicode handling with code-page fallback, and its own parsers for the five header mini-formats. The closest relative in this workspace is `markdown-codec`, which is likewise a hand-written scanner and parser for a non-XML text format rather than a wrapper around a document library.
|
|
14
|
+
|
|
15
|
+
```mermaid
|
|
16
|
+
graph TD
|
|
17
|
+
schema("document-schema.js")
|
|
18
|
+
rtfcodec("rtf-codec")
|
|
19
|
+
|
|
20
|
+
schema --> rtfcodec
|
|
21
|
+
|
|
22
|
+
click schema "https://github.com/ExaDev/documents.js/tree/main/packages/document-schema.js" "document-schema.js"
|
|
23
|
+
click rtfcodec "https://github.com/ExaDev/documents.js/tree/main/packages/rtf-codec" "rtf-codec"
|
|
24
|
+
|
|
25
|
+
style rtfcodec fill:#f9a825,stroke:#333,stroke-width:3px
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`rtf-codec` depends on nothing else in this family beyond `document-schema.js` — see [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
|
+
|
|
30
|
+
## Getting started
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
pnpm add rtf-codec
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { readRtf, writeRtf, readRtfContent, writeRtfContent } from "rtf-codec";
|
|
38
|
+
|
|
39
|
+
// The tree-form pair, over document-schema.js's DocumentTree -- what to reach for by default.
|
|
40
|
+
const { documentPackage, diagnostics } = readRtf(await file.bytes());
|
|
41
|
+
const bytes = writeRtf(documentPackage);
|
|
42
|
+
|
|
43
|
+
// The flat pair, over its ContentDocument -- the shape the reader itself builds.
|
|
44
|
+
const { document } = readRtfContent(await file.bytes());
|
|
45
|
+
const flatBytes = writeRtfContent(document);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Every entry point takes **bytes**, not a string. RTF is defined over bytes: `\'hh` names a raw byte decoded through whichever code page the document declared, and `\binN` is followed by literally N arbitrary bytes. A caller who has already decoded a `.rtf` file as UTF-8 has destroyed exactly the information the code-page layer needs. For the one string form that genuinely still holds bytes — a file read with a latin-1/binary reader — `rtfBytesFromLatin1` converts it exactly, and throws above U+00FF rather than truncating.
|
|
49
|
+
|
|
50
|
+
Both encodings are also available as [`z.codec()`](https://zod.dev) pairs, matching the convention `markdown-codec` and `pdf-codec` already follow:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { rtfCodec, rtfContentCodec, RtfBytesSchema } from "rtf-codec";
|
|
54
|
+
|
|
55
|
+
const documentPackage = rtfCodec.parse(bytes); // bytes -> DocumentTree
|
|
56
|
+
const roundTripped = rtfCodec.encode(documentPackage); // DocumentTree -> bytes
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`RtfBytesSchema` is a real magic-byte check — the `<File>` production requires an RTF document to begin `{\rtf`, so a caller handing the codec a docx or a PDF is refused at the schema boundary rather than deep inside the tokenizer.
|
|
60
|
+
|
|
61
|
+
## The specification
|
|
62
|
+
|
|
63
|
+
Everything here is implemented against Microsoft's own **Rich Text Format (RTF) Specification, Version 1.9.1** (March 2008, 278 pages) — the final revision, covering Word 2007. Each source module cites the section it implements by name.
|
|
64
|
+
|
|
65
|
+
- Primary source: [`[MSFT-RTF].pdf`](https://officeprotocoldoc.z19.web.core.windows.net/files/Archive_References/%5BMSFT-RTF%5D.pdf), hosted in Microsoft's own Office protocol documentation archive.
|
|
66
|
+
- Microsoft's original download page: <https://www.microsoft.com/en-us/download/details.aspx?id=10725> ([Wayback snapshot](https://web.archive.org/web/2024/https://www.microsoft.com/en-us/download/details.aspx?id=10725)).
|
|
67
|
+
- The version history and the note that 1.9.1 is the final revision: [Rich Text Format on Wikipedia](https://en.wikipedia.org/wiki/Rich_Text_Format) ([Wayback snapshot](https://web.archive.org/web/2025/https://en.wikipedia.org/wiki/Rich_Text_Format)).
|
|
68
|
+
- Format-preservation context: [Library of Congress, Sustainability of Digital Formats — RTF](https://www.loc.gov/preservation/digital/formats/fdd/fdd000473.shtml) ([Wayback snapshot](https://web.archive.org/web/2024/https://www.loc.gov/preservation/digital/formats/fdd/fdd000473.shtml)).
|
|
69
|
+
|
|
70
|
+
The code-page tables in `src/codepage.ts` were **generated, not transcribed**: each is `bytes([b]).decode(codec)` over `0x80..0xFF` from Python's own codec library, verified byte-for-byte against it, because a hand-typed 128-entry table is exactly where one transposed character hides until a real document decodes wrong.
|
|
71
|
+
|
|
72
|
+
## Architecture
|
|
73
|
+
|
|
74
|
+
Five stages, each its own module, each testable on its own:
|
|
75
|
+
|
|
76
|
+
| Stage | Module | What it does |
|
|
77
|
+
| ------ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
78
|
+
| Lex | `src/tokenize.ts` | Bytes to a flat token stream: control words (32-letter name cap, 10-digit signed parameter, one-space delimiter), control symbols (no delimiter at all), the `\'hh` hex byte as its own token kind, `\binN`'s raw byte run, and CR/LF handling. |
|
|
79
|
+
| Group | `src/group.ts` | Brace matching and destination identification — the two structural facts every stage above the lexer needs. |
|
|
80
|
+
| Header | `src/header.ts` | The five header mini-formats: `\fonttbl`, `\colortbl`, `\stylesheet`, `\listtable` and `\listoverridetable`, plus `\info` and the document properties, in one pass ahead of the body. |
|
|
81
|
+
| Read | `src/read.ts` | The destination/group state machine that turns the token stream into a `ContentDocument`. |
|
|
82
|
+
| 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
|
+
|
|
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).
|
|
85
|
+
|
|
86
|
+
### The reader is the specification's own model, literally
|
|
87
|
+
|
|
88
|
+
"Conventions of an RTF Reader" states the model this reader implements exactly: an opening brace stores the current state on a stack, a closing brace retrieves it, a backslash collects a control word or symbol and dispatches on it, and anything else is text written "to the current destination using the current formatting properties". Four kinds of state ride that stack, as the spec enumerates them — destination, character properties, paragraph properties, table properties — plus the `\ucN` skip count, which the spec separately requires be stacked.
|
|
89
|
+
|
|
90
|
+
The destination is not merely a label: it decides what happens to text. Body text becomes runs; a `\pict` destination's text is hex picture payload; a `\fldinst` destination's text is a field instruction to be parsed rather than shown; a `\listtext` destination's text is the flat rendering of a list number that "should be ignored by any reader that understands Word 97 through Word 2007 numbering"; an unrecognised `{\*` destination's text is discarded whole. That mapping is what lets the reader be a single pass with no lookahead beyond a group's own head.
|
|
91
|
+
|
|
92
|
+
### Tables are paragraph properties, not a group
|
|
93
|
+
|
|
94
|
+
"There is no RTF table group; instead, tables are specified as paragraph properties." A row is a run of `\intbl` paragraphs terminated by `\cell` marks and closed by `\row`, with the row's own `\trowd ... \cellxN` definition sitting before it, after it, or — for Word 2002 onward — both. The table builder is therefore driven by the `\cell`/`\row` marks in the text stream rather than by nesting, and a table closes when a non-table paragraph arrives.
|
|
95
|
+
|
|
96
|
+
### Unicode
|
|
97
|
+
|
|
98
|
+
`\uN` carries the character and is followed by an ANSI approximation a Unicode-aware reader must skip: "the reader should ignore the next N' characters, where N' corresponds to the last `\ucN'` value encountered", where "any RTF control word or symbol is considered a single character" and a brace ends the skippable run early. All three of those rules are implemented, including partial consumption of a text run — which is why the main loop carries a byte offset alongside its token index. `{\upr {ansi} {\*\ud unicode}}` pairs take the `\ud` half and discard the ANSI one.
|
|
99
|
+
|
|
100
|
+
On the way out, **every non-ASCII character leaves as `\uN`** with a one-character `?` fallback under a single `\uc1`. The writer deliberately does not hunt for a code page that could carry a character as a `\'hh` byte: the output is then pure 7-bit ASCII whatever the input contained, which is what makes it safe to transmit and trivially diffable, and costs a conforming reader nothing.
|
|
101
|
+
|
|
102
|
+
## Scope
|
|
103
|
+
|
|
104
|
+
### Read: RTF → `ContentDocument`
|
|
105
|
+
|
|
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
|
+
| Lists | `\lsN`, `\ilvlN`, with the marker type carried through the `numId` grammar |
|
|
125
|
+
| Pictures | `\pngblip` and `\jpegblip`, hex or `\binN` payload, `\picwgoalN`/`\pichgoalN` or `\picwN`/`\pichN`, `\picscalexN`/`\picscaleyN` |
|
|
126
|
+
| Hyperlinks | The `HYPERLINK` field production, including its `\l` anchor switch |
|
|
127
|
+
| Special characters | `\tab`, `\line`, `\emdash`, `\endash`, `\bullet`, the quotation marks, `\~`, `\-`, `\_`, `\\`, `\{`, `\}`, and the zero-width and directional marks |
|
|
128
|
+
| Page breaks | `\page` |
|
|
129
|
+
| `\info` | Title, author, subject, keywords |
|
|
130
|
+
|
|
131
|
+
### Write: `ContentDocument` → RTF
|
|
132
|
+
|
|
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.
|
|
134
|
+
|
|
135
|
+
Two places where the two models genuinely differ in shape, rather than merely in spelling:
|
|
136
|
+
|
|
137
|
+
- **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
|
+
- **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
|
+
|
|
140
|
+
### Deliberately not handled
|
|
141
|
+
|
|
142
|
+
Each of these is reported through a diagnostic rather than dropped silently — see [Diagnostics](#diagnostics).
|
|
143
|
+
|
|
144
|
+
| Construct | Why |
|
|
145
|
+
| -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
146
|
+
| 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. |
|
|
147
|
+
| Content controls | **RTF 1.9.1 specifies none.** 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. |
|
|
148
|
+
| Form fields (`\*\formfield`, with a `FORMTEXT`/`FORMCHECKBOX`/`FORMDROPDOWN` instruction) | RTF's own nearest analogue to a content control, and a real `contentControl` mapping -- `ooxml.js` maps docx's legacy `w:ffData` twin onto exactly that kind. Not mapped yet: it needs the field machinery to hand the control its instruction and cached result. |
|
|
149
|
+
| 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. |
|
|
150
|
+
| 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. |
|
|
151
|
+
| Metafile and bitmap pictures (`\wmetafileN`, `\emfblip`, `\dibitmapN`, `\wbitmapN`, `\macpict`) | `ContentImageBlock` carries PNG and JPEG only. |
|
|
152
|
+
| 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. |
|
|
153
|
+
| Nested tables (`\nestcell`/`\nestrow`) | Read as ordinary cell content; the inner table's own structure is not reconstructed. |
|
|
154
|
+
| Embedded objects (`\object`), drawing objects (`\do`, `\shp`) | Dropped. Writing one would need the OLE container this package does not build. |
|
|
155
|
+
| 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. |
|
|
156
|
+
| 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. |
|
|
157
|
+
| Cell vertical alignment (`\clvertalt`/`\clvertalc`/`\clvertalb`), diagonal cell borders (`\cldglu`/`\cldgll`), `\clshdngN` | `ContentTableCell` carries per-side borders and one background colour and nothing else -- a diagonal rule is not a side, and a shading percentage is a pattern rather than a colour. |
|
|
158
|
+
| 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. |
|
|
159
|
+
|
|
160
|
+
## Diagnostics
|
|
161
|
+
|
|
162
|
+
The same three-tier policy `markdown-codec` and `pdf-codec` use: **throw** for input that cannot be processed at all, **recover with a diagnostic** for input that is malformed in a way the spec's own robustness advice says to survive, and **degrade with a diagnostic** for a construct read correctly at the token level whose meaning the `ContentDocument` mapping does not carry.
|
|
163
|
+
|
|
164
|
+
That third tier does more work here than in the XML formats. The spec _requires_ an unknown control word to be ignored and an unknown `{\*` destination to be skipped whole, so "I did not understand this" is the format's normal operating mode rather than an error condition — but a reader that silently drops a construct a caller cared about is indistinguishable from one that never saw it. Every drop this package makes deliberately therefore names itself through a code in `RtfDiagnosticCodes`.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { readRtf, RtfDiagnosticCodes } from "rtf-codec";
|
|
168
|
+
|
|
169
|
+
const { documentPackage, diagnostics } = readRtf(bytes);
|
|
170
|
+
const droppedPictures = diagnostics.filter(
|
|
171
|
+
(diagnostic) =>
|
|
172
|
+
diagnostic.code === RtfDiagnosticCodes.UNSUPPORTED_PICTURE_FORMAT,
|
|
173
|
+
);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
A coverage suite proves every code in `RtfDiagnosticCodes` is reachable by producing each one from a real input, and fails if any has no fixture — so the table cannot grow an entry nothing can emit, and a construct that stops being dropped has its code removed rather than left behind as a promise the package no longer keeps.
|
|
177
|
+
|
|
178
|
+
The throw tier is `RtfNotAnRtfDocumentError` (no `{\rtf` header), `RtfInputTooLargeError` and `RtfNestingLimitExceededError` (the two resource guards, both configurable through `ReadRtfOptions`), and `RtfUnsupportedDocumentKindError` on the write side, since RTF is a wordprocessing format and a presentation, spreadsheet, drawing or formula document has no RTF spelling.
|
|
179
|
+
|
|
180
|
+
## Dependency choices
|
|
181
|
+
|
|
182
|
+
`document-schema.js` and `zod`, and nothing else. 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.
|
|
183
|
+
|
|
184
|
+
`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
|
+
|
|
186
|
+
It deliberately does **not** depend on `byte-codec`, even though that package has the base64 and byte-writing primitives `src/base64.ts` reimplements. RTF's picture payload is hex-encoded ASCII inside a text format, not a binary container, so what is actually needed here is about sixty lines of hex and base64 conversion — considerably less than the coupling a dependency on a sibling's release cadence would cost. `epub-codec` made the same call for the same reason.
|
|
187
|
+
|
|
188
|
+
## Worker isomorphism
|
|
189
|
+
|
|
190
|
+
Like every foundation and format-codec package in this family, `rtf-codec` is Worker-isomorphic: its published `src/` imports no `node:*` module and uses no `Buffer`, so one artifact behaves identically in a Node host, a browser, and a Cloudflare Worker. The ban is enforced by `isomorphic: true` in this package's `eslint.config.ts`, and `pnpm test:workers` proves it at runtime by running the public surface inside workerd.
|
|
191
|
+
|
|
192
|
+
Two places would have been tempting to write with a Node-only shortcut, and the workers suite exercises both: `src/base64.ts`'s hand-written encoders (`Buffer.from(bytes).toString("base64")` is the one-liner they exist instead of) and `src/codepage.ts`'s own tables.
|
|
193
|
+
|
|
194
|
+
## Fidelity constructs and the residue channel
|
|
195
|
+
|
|
196
|
+
Both of the channels `document-schema.js` defines are live here.
|
|
197
|
+
|
|
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 none; see the gap table above.
|
|
199
|
+
|
|
200
|
+
**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
|
+
|
|
202
|
+
## Build, test, and lint
|
|
203
|
+
|
|
204
|
+
```sh
|
|
205
|
+
pnpm install
|
|
206
|
+
pnpm build # tsdown -> ESM + CJS + .d.ts in dist/
|
|
207
|
+
pnpm typecheck # tsc for the web program and the node program, plus attw --pack
|
|
208
|
+
pnpm lint # eslint . --fix --cache --max-warnings 0
|
|
209
|
+
pnpm test # vitest run --project unit
|
|
210
|
+
pnpm test:workers # the same code inside workerd, the real Cloudflare Workers runtime
|
|
211
|
+
pnpm test:smoke # rebuilds dist/ and exercises the built ESM and CJS artifacts
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
To run a single test file: `pnpm vitest run src/read.test.ts`.
|
|
215
|
+
|
|
216
|
+
## Release and publishing
|
|
217
|
+
|
|
218
|
+
Release, CI, and commit-message conventions are workspace-wide, not package-local — see the [monorepo root README](../../README.md#releases) for the mechanism.
|
|
219
|
+
|
|
220
|
+
## Contributing
|
|
221
|
+
|
|
222
|
+
Conventional Commits, enforced workspace-wide by commitlint through a root `commit-msg` hook. Work inside `packages/rtf-codec/`; see [CONTRIBUTING.md](../../CONTRIBUTING.md) for the shared git hooks and history conventions.
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
MIT
|
package/dist/base64.cjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/base64.ts
|
|
3
|
+
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
4
|
+
const HEX_DIGITS = "0123456789abcdef";
|
|
5
|
+
function bytesToBase64(input) {
|
|
6
|
+
let out = "";
|
|
7
|
+
for (let index = 0; index < input.length; index += 3) {
|
|
8
|
+
const first = input[index] ?? 0;
|
|
9
|
+
const second = input[index + 1];
|
|
10
|
+
const third = input[index + 2];
|
|
11
|
+
out += BASE64_ALPHABET.charAt(first >> 2);
|
|
12
|
+
out += BASE64_ALPHABET.charAt((first & 3) << 4 | (second ?? 0) >> 4);
|
|
13
|
+
out += second === void 0 ? "=" : BASE64_ALPHABET.charAt((second & 15) << 2 | (third ?? 0) >> 6);
|
|
14
|
+
out += third === void 0 ? "=" : BASE64_ALPHABET.charAt(third & 63);
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
function base64Value(character) {
|
|
19
|
+
const index = BASE64_ALPHABET.indexOf(character);
|
|
20
|
+
return index === -1 ? void 0 : index;
|
|
21
|
+
}
|
|
22
|
+
function base64ToBytes(input) {
|
|
23
|
+
const out = [];
|
|
24
|
+
let accumulator = 0;
|
|
25
|
+
let bits = 0;
|
|
26
|
+
for (const character of input) {
|
|
27
|
+
if (character === "=" || /\s/.test(character)) continue;
|
|
28
|
+
const value = base64Value(character);
|
|
29
|
+
if (value === void 0) return;
|
|
30
|
+
accumulator = accumulator << 6 | value;
|
|
31
|
+
bits += 6;
|
|
32
|
+
if (bits >= 8) {
|
|
33
|
+
bits -= 8;
|
|
34
|
+
out.push(accumulator >> bits & 255);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return Uint8Array.from(out);
|
|
38
|
+
}
|
|
39
|
+
function bytesToHex(input) {
|
|
40
|
+
let out = "";
|
|
41
|
+
for (const byte of input) {
|
|
42
|
+
out += HEX_DIGITS.charAt(byte >> 4);
|
|
43
|
+
out += HEX_DIGITS.charAt(byte & 15);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function hexToBytes(input) {
|
|
48
|
+
const out = [];
|
|
49
|
+
let high;
|
|
50
|
+
for (const character of input) {
|
|
51
|
+
const value = HEX_DIGITS.indexOf(character.toLowerCase());
|
|
52
|
+
if (value === -1) continue;
|
|
53
|
+
if (high === void 0) {
|
|
54
|
+
high = value;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
out.push(high * 16 + value);
|
|
58
|
+
high = void 0;
|
|
59
|
+
}
|
|
60
|
+
return Uint8Array.from(out);
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
exports.base64ToBytes = base64ToBytes;
|
|
64
|
+
exports.bytesToBase64 = bytesToBase64;
|
|
65
|
+
exports.bytesToHex = bytesToHex;
|
|
66
|
+
exports.hexToBytes = hexToBytes;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/base64.d.ts
|
|
2
|
+
declare function bytesToBase64(input: Uint8Array): string;
|
|
3
|
+
declare function base64ToBytes(input: string): Uint8Array | undefined;
|
|
4
|
+
declare function bytesToHex(input: Uint8Array): string;
|
|
5
|
+
declare function hexToBytes(input: string): Uint8Array;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { base64ToBytes, bytesToBase64, bytesToHex, hexToBytes };
|
package/dist/base64.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/base64.d.ts
|
|
2
|
+
declare function bytesToBase64(input: Uint8Array): string;
|
|
3
|
+
declare function base64ToBytes(input: string): Uint8Array | undefined;
|
|
4
|
+
declare function bytesToHex(input: Uint8Array): string;
|
|
5
|
+
declare function hexToBytes(input: string): Uint8Array;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { base64ToBytes, bytesToBase64, bytesToHex, hexToBytes };
|
package/dist/base64.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//#region src/base64.ts
|
|
2
|
+
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
3
|
+
const HEX_DIGITS = "0123456789abcdef";
|
|
4
|
+
function bytesToBase64(input) {
|
|
5
|
+
let out = "";
|
|
6
|
+
for (let index = 0; index < input.length; index += 3) {
|
|
7
|
+
const first = input[index] ?? 0;
|
|
8
|
+
const second = input[index + 1];
|
|
9
|
+
const third = input[index + 2];
|
|
10
|
+
out += BASE64_ALPHABET.charAt(first >> 2);
|
|
11
|
+
out += BASE64_ALPHABET.charAt((first & 3) << 4 | (second ?? 0) >> 4);
|
|
12
|
+
out += second === void 0 ? "=" : BASE64_ALPHABET.charAt((second & 15) << 2 | (third ?? 0) >> 6);
|
|
13
|
+
out += third === void 0 ? "=" : BASE64_ALPHABET.charAt(third & 63);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
function base64Value(character) {
|
|
18
|
+
const index = BASE64_ALPHABET.indexOf(character);
|
|
19
|
+
return index === -1 ? void 0 : index;
|
|
20
|
+
}
|
|
21
|
+
function base64ToBytes(input) {
|
|
22
|
+
const out = [];
|
|
23
|
+
let accumulator = 0;
|
|
24
|
+
let bits = 0;
|
|
25
|
+
for (const character of input) {
|
|
26
|
+
if (character === "=" || /\s/.test(character)) continue;
|
|
27
|
+
const value = base64Value(character);
|
|
28
|
+
if (value === void 0) return;
|
|
29
|
+
accumulator = accumulator << 6 | value;
|
|
30
|
+
bits += 6;
|
|
31
|
+
if (bits >= 8) {
|
|
32
|
+
bits -= 8;
|
|
33
|
+
out.push(accumulator >> bits & 255);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return Uint8Array.from(out);
|
|
37
|
+
}
|
|
38
|
+
function bytesToHex(input) {
|
|
39
|
+
let out = "";
|
|
40
|
+
for (const byte of input) {
|
|
41
|
+
out += HEX_DIGITS.charAt(byte >> 4);
|
|
42
|
+
out += HEX_DIGITS.charAt(byte & 15);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function hexToBytes(input) {
|
|
47
|
+
const out = [];
|
|
48
|
+
let high;
|
|
49
|
+
for (const character of input) {
|
|
50
|
+
const value = HEX_DIGITS.indexOf(character.toLowerCase());
|
|
51
|
+
if (value === -1) continue;
|
|
52
|
+
if (high === void 0) {
|
|
53
|
+
high = value;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
out.push(high * 16 + value);
|
|
57
|
+
high = void 0;
|
|
58
|
+
}
|
|
59
|
+
return Uint8Array.from(out);
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { base64ToBytes, bytesToBase64, bytesToHex, hexToBytes };
|
package/dist/bytes.cjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_diagnostics = require("./diagnostics.cjs");
|
|
3
|
+
//#region src/bytes.ts
|
|
4
|
+
const ASCII_CHUNK_SIZE = 8192;
|
|
5
|
+
function asciiStringFromBytes(input) {
|
|
6
|
+
let out = "";
|
|
7
|
+
for (let start = 0; start < input.length; start += ASCII_CHUNK_SIZE) out += String.fromCharCode(...input.subarray(start, start + ASCII_CHUNK_SIZE));
|
|
8
|
+
return out;
|
|
9
|
+
}
|
|
10
|
+
function appendBytes(target, input) {
|
|
11
|
+
for (const byte of input) target.push(byte);
|
|
12
|
+
}
|
|
13
|
+
function rtfBytesFromLatin1(source) {
|
|
14
|
+
const out = new Uint8Array(source.length);
|
|
15
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
16
|
+
const code = source.charCodeAt(index);
|
|
17
|
+
if (code > 255) throw new require_diagnostics.RtfParseError("rtf/not-byte-preserving-string", `character at index ${String(index)} is U+${code.toString(16).toUpperCase().padStart(4, "0")}, above U+00FF: this string was decoded through a multi-byte encoding and no longer holds the file's bytes. Read the .rtf file as bytes and pass the Uint8Array directly.`);
|
|
18
|
+
out[index] = code;
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
exports.appendBytes = appendBytes;
|
|
24
|
+
exports.asciiStringFromBytes = asciiStringFromBytes;
|
|
25
|
+
exports.rtfBytesFromLatin1 = rtfBytesFromLatin1;
|
package/dist/bytes.d.cts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/bytes.d.ts
|
|
2
|
+
declare function asciiStringFromBytes(input: Uint8Array): string;
|
|
3
|
+
declare function appendBytes(target: number[], input: Uint8Array): void;
|
|
4
|
+
declare function rtfBytesFromLatin1(source: string): Uint8Array;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { appendBytes, asciiStringFromBytes, rtfBytesFromLatin1 };
|
package/dist/bytes.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/bytes.d.ts
|
|
2
|
+
declare function asciiStringFromBytes(input: Uint8Array): string;
|
|
3
|
+
declare function appendBytes(target: number[], input: Uint8Array): void;
|
|
4
|
+
declare function rtfBytesFromLatin1(source: string): Uint8Array;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { appendBytes, asciiStringFromBytes, rtfBytesFromLatin1 };
|
package/dist/bytes.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { RtfParseError } from "./diagnostics.js";
|
|
2
|
+
//#region src/bytes.ts
|
|
3
|
+
const ASCII_CHUNK_SIZE = 8192;
|
|
4
|
+
function asciiStringFromBytes(input) {
|
|
5
|
+
let out = "";
|
|
6
|
+
for (let start = 0; start < input.length; start += ASCII_CHUNK_SIZE) out += String.fromCharCode(...input.subarray(start, start + ASCII_CHUNK_SIZE));
|
|
7
|
+
return out;
|
|
8
|
+
}
|
|
9
|
+
function appendBytes(target, input) {
|
|
10
|
+
for (const byte of input) target.push(byte);
|
|
11
|
+
}
|
|
12
|
+
function rtfBytesFromLatin1(source) {
|
|
13
|
+
const out = new Uint8Array(source.length);
|
|
14
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
15
|
+
const code = source.charCodeAt(index);
|
|
16
|
+
if (code > 255) throw new RtfParseError("rtf/not-byte-preserving-string", `character at index ${String(index)} is U+${code.toString(16).toUpperCase().padStart(4, "0")}, above U+00FF: this string was decoded through a multi-byte encoding and no longer holds the file's bytes. Read the .rtf file as bytes and pass the Uint8Array directly.`);
|
|
17
|
+
out[index] = code;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { appendBytes, asciiStringFromBytes, rtfBytesFromLatin1 };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_units = require("./units.cjs");
|
|
3
|
+
//#region src/cell-format.ts
|
|
4
|
+
const CELL_BORDER_SIDES = /* @__PURE__ */ new Map([
|
|
5
|
+
["clbrdrt", "top"],
|
|
6
|
+
["clbrdrl", "left"],
|
|
7
|
+
["clbrdrb", "bottom"],
|
|
8
|
+
["clbrdrr", "right"]
|
|
9
|
+
]);
|
|
10
|
+
const BORDER_STYLES = /* @__PURE__ */ new Map([
|
|
11
|
+
["brdrs", "solid"],
|
|
12
|
+
["brdrth", "solid"],
|
|
13
|
+
["brdrsh", "solid"],
|
|
14
|
+
["brdrhair", "solid"],
|
|
15
|
+
["brdrinset", "solid"],
|
|
16
|
+
["brdroutset", "solid"],
|
|
17
|
+
["brdrengrave", "solid"],
|
|
18
|
+
["brdremboss", "solid"],
|
|
19
|
+
["brdrdot", "dotted"],
|
|
20
|
+
["brdrdash", "dashed"],
|
|
21
|
+
["brdrdashsm", "dashed"],
|
|
22
|
+
["brdrdashd", "dashed"],
|
|
23
|
+
["brdrdashdd", "dashed"],
|
|
24
|
+
["brdrdashdot", "dashed"],
|
|
25
|
+
["brdrdashdotdot", "dashed"],
|
|
26
|
+
["brdrdashdotstr", "dashed"],
|
|
27
|
+
["brdrdb", "double"],
|
|
28
|
+
["brdrtriple", "double"],
|
|
29
|
+
["brdrwavydb", "double"],
|
|
30
|
+
["brdrtnthsg", "double"],
|
|
31
|
+
["brdrthtnsg", "double"],
|
|
32
|
+
["brdrtnthtnsg", "double"],
|
|
33
|
+
["brdrtnthmg", "double"],
|
|
34
|
+
["brdrthtnmg", "double"],
|
|
35
|
+
["brdrtnthtnmg", "double"],
|
|
36
|
+
["brdrtnthlg", "double"],
|
|
37
|
+
["brdrthtnlg", "double"],
|
|
38
|
+
["brdrtnthtnlg", "double"],
|
|
39
|
+
["brdrwavy", "solid"]
|
|
40
|
+
]);
|
|
41
|
+
const NO_BORDER_KEYWORDS = /* @__PURE__ */ new Set([
|
|
42
|
+
"brdrnone",
|
|
43
|
+
"brdrnil",
|
|
44
|
+
"brdrtbl"
|
|
45
|
+
]);
|
|
46
|
+
const DEFAULT_BORDER_WIDTH_TWIPS = 15;
|
|
47
|
+
function newPendingCell() {
|
|
48
|
+
return {
|
|
49
|
+
borders: {},
|
|
50
|
+
side: void 0,
|
|
51
|
+
backgroundIndex: void 0,
|
|
52
|
+
verticalMergeFirst: false,
|
|
53
|
+
verticalMergeContinuation: false,
|
|
54
|
+
horizontalMergeFirst: false,
|
|
55
|
+
horizontalMergeContinuation: false
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function applyCellDefinitionControlWord(name, param, cell) {
|
|
59
|
+
const side = CELL_BORDER_SIDES.get(name);
|
|
60
|
+
if (side !== void 0) {
|
|
61
|
+
cell.side = side;
|
|
62
|
+
cell.borders[side] = {
|
|
63
|
+
style: void 0,
|
|
64
|
+
widthTwips: void 0,
|
|
65
|
+
colorIndex: void 0,
|
|
66
|
+
none: false
|
|
67
|
+
};
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
switch (name) {
|
|
71
|
+
case "clvmgf":
|
|
72
|
+
cell.verticalMergeFirst = true;
|
|
73
|
+
return true;
|
|
74
|
+
case "clvmrg":
|
|
75
|
+
cell.verticalMergeContinuation = true;
|
|
76
|
+
return true;
|
|
77
|
+
case "clmgf":
|
|
78
|
+
cell.horizontalMergeFirst = true;
|
|
79
|
+
return true;
|
|
80
|
+
case "clmrg":
|
|
81
|
+
cell.horizontalMergeContinuation = true;
|
|
82
|
+
return true;
|
|
83
|
+
case "clcbpat":
|
|
84
|
+
cell.backgroundIndex = param;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
const pending = cell.side === void 0 ? void 0 : cell.borders[cell.side];
|
|
88
|
+
if (pending === void 0) return false;
|
|
89
|
+
if (NO_BORDER_KEYWORDS.has(name)) {
|
|
90
|
+
pending.none = true;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
const style = BORDER_STYLES.get(name);
|
|
94
|
+
if (style !== void 0) {
|
|
95
|
+
pending.style = style;
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if (name === "brdrw") {
|
|
99
|
+
pending.widthTwips = param;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
if (name === "brdrcf") {
|
|
103
|
+
pending.colorIndex = param;
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return name.startsWith("brdr") || name.startsWith("brsp");
|
|
107
|
+
}
|
|
108
|
+
function resolveBorder(pending, colorAt) {
|
|
109
|
+
if (pending.none) return;
|
|
110
|
+
const widthPt = require_units.twipsToPoints(pending.widthTwips ?? DEFAULT_BORDER_WIDTH_TWIPS);
|
|
111
|
+
if (widthPt <= 0) return;
|
|
112
|
+
const color = pending.colorIndex === void 0 ? void 0 : colorAt(pending.colorIndex);
|
|
113
|
+
const style = pending.style;
|
|
114
|
+
return {
|
|
115
|
+
color: color ?? {
|
|
116
|
+
r: 0,
|
|
117
|
+
g: 0,
|
|
118
|
+
b: 0
|
|
119
|
+
},
|
|
120
|
+
widthPt,
|
|
121
|
+
...style === void 0 || style === "solid" ? {} : { style }
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function borderControlWords(side, border, colorIndex) {
|
|
125
|
+
const sideWord = [...CELL_BORDER_SIDES].find(([, value]) => value === side)?.[0];
|
|
126
|
+
if (sideWord === void 0) return "";
|
|
127
|
+
const style = BORDER_STYLE_CONTROL_WORDS[border.style ?? "solid"];
|
|
128
|
+
const width = Math.max(1, require_units.pointsToTwips(border.widthPt));
|
|
129
|
+
return `\\${sideWord}\\${style}\\brdrw${String(width)}` + (colorIndex === void 0 ? "" : `\\brdrcf${String(colorIndex)}`);
|
|
130
|
+
}
|
|
131
|
+
const BORDER_STYLE_CONTROL_WORDS = {
|
|
132
|
+
solid: "brdrs",
|
|
133
|
+
dashed: "brdrdash",
|
|
134
|
+
dotted: "brdrdot",
|
|
135
|
+
double: "brdrdb"
|
|
136
|
+
};
|
|
137
|
+
//#endregion
|
|
138
|
+
exports.CELL_BORDER_SIDES = CELL_BORDER_SIDES;
|
|
139
|
+
exports.applyCellDefinitionControlWord = applyCellDefinitionControlWord;
|
|
140
|
+
exports.borderControlWords = borderControlWords;
|
|
141
|
+
exports.newPendingCell = newPendingCell;
|
|
142
|
+
exports.resolveBorder = resolveBorder;
|