rtf-codec 1.0.0 → 1.1.1
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 +39 -21
- 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.d.cts +37 -1
- package/dist/codec.d.ts +37 -1
- package/dist/codepage.d.cts +1 -1
- package/dist/codepage.d.ts +1 -1
- 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-DdDFJLNO.d.cts → diagnostics-BgG_KAiN.d.cts} +2 -1
- package/dist/{diagnostics-DdDFJLNO.d.ts → diagnostics-BgG_KAiN.d.ts} +2 -1
- package/dist/diagnostics.cjs +3 -2
- package/dist/diagnostics.d.cts +1 -1
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.js +3 -2
- package/dist/header.cjs +61 -6
- package/dist/header.d.cts +2 -1
- package/dist/header.d.ts +2 -1
- package/dist/header.js +61 -6
- 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 +397 -40
- package/dist/read.d.cts +1 -1
- package/dist/read.d.ts +1 -1
- package/dist/read.js +397 -40
- package/dist/write.cjs +196 -28
- package/dist/write.js +196 -28
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
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 (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
|
+
|
|
9
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.
|
|
10
12
|
|
|
11
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.
|
|
@@ -23,7 +25,7 @@ graph TD
|
|
|
23
25
|
style rtfcodec fill:#f9a825,stroke:#333,stroke-width:3px
|
|
24
26
|
```
|
|
25
27
|
|
|
26
|
-
`rtf-codec` depends on nothing else in this family beyond `document-schema.js` — see [Dependency choices](#dependency-choices).
|
|
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.
|
|
27
29
|
|
|
28
30
|
## Getting started
|
|
29
31
|
|
|
@@ -79,7 +81,7 @@ Five stages, each its own module, each testable on its own:
|
|
|
79
81
|
| Read | `src/read.ts` | The destination/group state machine that turns the token stream into a `ContentDocument`. |
|
|
80
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. |
|
|
81
83
|
|
|
82
|
-
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/diagnostics.ts` (the three-tier diagnostic policy).
|
|
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).
|
|
83
85
|
|
|
84
86
|
### The reader is the specification's own model, literally
|
|
85
87
|
|
|
@@ -110,10 +112,15 @@ On the way out, **every non-ASCII character leaves as `\uN`** with a one-charact
|
|
|
110
112
|
| `\fonttbl` | Face name, family keyword, per-font code page |
|
|
111
113
|
| `\colortbl` | RGB, including a theme colour's own literal RGB; index 0 is the auto colour |
|
|
112
114
|
| `\stylesheet` | Paragraph style names and heading levels (`\outlinelevelN` or a built-in `heading N` name) |
|
|
113
|
-
| `\listtable` / `\listoverridetable` | `\lsN` → `\listidN` → the level's `\levelnfcN` and `\levelstartatN`
|
|
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 |
|
|
114
118
|
| Paragraphs | `\par`, `\pard`, alignment, indents, spacing, `\slN`/`\slmultN`, `\pagebb` |
|
|
115
119
|
| Runs | `\b`, `\i`, `\ul` (every variant), `\strike`, `\fN`, `\fsN`, `\cfN`, `\v` (dropped as hidden) |
|
|
116
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 |
|
|
117
124
|
| Lists | `\lsN`, `\ilvlN`, with the marker type carried through the `numId` grammar |
|
|
118
125
|
| Pictures | `\pngblip` and `\jpegblip`, hex or `\binN` payload, `\picwgoalN`/`\pichgoalN` or `\picwN`/`\pichN`, `\picscalexN`/`\picscaleyN` |
|
|
119
126
|
| Hyperlinks | The `HYPERLINK` field production, including its `\l` anchor switch |
|
|
@@ -123,27 +130,32 @@ On the way out, **every non-ASCII character leaves as `\uN`** with a one-charact
|
|
|
123
130
|
|
|
124
131
|
### Write: `ContentDocument` → RTF
|
|
125
132
|
|
|
126
|
-
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, a `heading N` style per distinct heading level,
|
|
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`.
|
|
127
139
|
|
|
128
140
|
### Deliberately not handled
|
|
129
141
|
|
|
130
142
|
Each of these is reported through a diagnostic rather than dropped silently — see [Diagnostics](#diagnostics).
|
|
131
143
|
|
|
132
|
-
| Construct
|
|
133
|
-
|
|
|
134
|
-
| Headers, footers, footnotes, endnotes, annotations
|
|
135
|
-
|
|
|
136
|
-
|
|
|
137
|
-
| East Asian DBCS code pages (932, 936, 949, 950, 1361)
|
|
138
|
-
| Code page 42 (`SYMBOL_CHARSET`)
|
|
139
|
-
| Metafile and bitmap pictures (`\wmetafileN`, `\emfblip`, `\dibitmapN`, `\wbitmapN`, `\macpict`)
|
|
140
|
-
| A picture with no stated size
|
|
141
|
-
| Nested tables (`\nestcell`/`\nestrow`)
|
|
142
|
-
| Embedded objects (`\object`), drawing objects (`\do`, `\shp`)
|
|
143
|
-
|
|
|
144
|
-
|
|
|
145
|
-
|
|
|
146
|
-
|
|
|
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. |
|
|
147
159
|
|
|
148
160
|
## Diagnostics
|
|
149
161
|
|
|
@@ -161,6 +173,8 @@ const droppedPictures = diagnostics.filter(
|
|
|
161
173
|
);
|
|
162
174
|
```
|
|
163
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
|
+
|
|
164
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.
|
|
165
179
|
|
|
166
180
|
## Dependency choices
|
|
@@ -177,9 +191,13 @@ Like every foundation and format-codec package in this family, `rtf-codec` is Wo
|
|
|
177
191
|
|
|
178
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.
|
|
179
193
|
|
|
180
|
-
##
|
|
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.
|
|
181
199
|
|
|
182
|
-
|
|
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.
|
|
183
201
|
|
|
184
202
|
## Build, test, and lint
|
|
185
203
|
|
|
@@ -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;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Color, ContentBorder, ContentStrokeStyle } from "document-schema.js";
|
|
2
|
+
//#region src/cell-format.d.ts
|
|
3
|
+
type CellBorderSide = "top" | "left" | "bottom" | "right";
|
|
4
|
+
declare const CELL_BORDER_SIDES: ReadonlyMap<string, CellBorderSide>;
|
|
5
|
+
interface PendingBorder {
|
|
6
|
+
style: ContentStrokeStyle | undefined;
|
|
7
|
+
widthTwips: number | undefined;
|
|
8
|
+
colorIndex: number | undefined;
|
|
9
|
+
none: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface PendingCell {
|
|
12
|
+
borders: Partial<Record<CellBorderSide, PendingBorder>>;
|
|
13
|
+
side: CellBorderSide | undefined;
|
|
14
|
+
backgroundIndex: number | undefined;
|
|
15
|
+
verticalMergeFirst: boolean;
|
|
16
|
+
verticalMergeContinuation: boolean;
|
|
17
|
+
horizontalMergeFirst: boolean;
|
|
18
|
+
horizontalMergeContinuation: boolean;
|
|
19
|
+
}
|
|
20
|
+
declare function newPendingCell(): PendingCell;
|
|
21
|
+
declare function applyCellDefinitionControlWord(name: string, param: number | undefined, cell: PendingCell): boolean;
|
|
22
|
+
declare function resolveBorder(pending: PendingBorder, colorAt: (index: number) => Color | undefined): ContentBorder | undefined;
|
|
23
|
+
declare function borderControlWords(side: CellBorderSide, border: ContentBorder, colorIndex: number | undefined): string;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { CELL_BORDER_SIDES, CellBorderSide, PendingBorder, PendingCell, applyCellDefinitionControlWord, borderControlWords, newPendingCell, resolveBorder };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Color, ContentBorder, ContentStrokeStyle } from "document-schema.js";
|
|
2
|
+
//#region src/cell-format.d.ts
|
|
3
|
+
type CellBorderSide = "top" | "left" | "bottom" | "right";
|
|
4
|
+
declare const CELL_BORDER_SIDES: ReadonlyMap<string, CellBorderSide>;
|
|
5
|
+
interface PendingBorder {
|
|
6
|
+
style: ContentStrokeStyle | undefined;
|
|
7
|
+
widthTwips: number | undefined;
|
|
8
|
+
colorIndex: number | undefined;
|
|
9
|
+
none: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface PendingCell {
|
|
12
|
+
borders: Partial<Record<CellBorderSide, PendingBorder>>;
|
|
13
|
+
side: CellBorderSide | undefined;
|
|
14
|
+
backgroundIndex: number | undefined;
|
|
15
|
+
verticalMergeFirst: boolean;
|
|
16
|
+
verticalMergeContinuation: boolean;
|
|
17
|
+
horizontalMergeFirst: boolean;
|
|
18
|
+
horizontalMergeContinuation: boolean;
|
|
19
|
+
}
|
|
20
|
+
declare function newPendingCell(): PendingCell;
|
|
21
|
+
declare function applyCellDefinitionControlWord(name: string, param: number | undefined, cell: PendingCell): boolean;
|
|
22
|
+
declare function resolveBorder(pending: PendingBorder, colorAt: (index: number) => Color | undefined): ContentBorder | undefined;
|
|
23
|
+
declare function borderControlWords(side: CellBorderSide, border: ContentBorder, colorIndex: number | undefined): string;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { CELL_BORDER_SIDES, CellBorderSide, PendingBorder, PendingCell, applyCellDefinitionControlWord, borderControlWords, newPendingCell, resolveBorder };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { pointsToTwips, twipsToPoints } from "./units.js";
|
|
2
|
+
//#region src/cell-format.ts
|
|
3
|
+
const CELL_BORDER_SIDES = /* @__PURE__ */ new Map([
|
|
4
|
+
["clbrdrt", "top"],
|
|
5
|
+
["clbrdrl", "left"],
|
|
6
|
+
["clbrdrb", "bottom"],
|
|
7
|
+
["clbrdrr", "right"]
|
|
8
|
+
]);
|
|
9
|
+
const BORDER_STYLES = /* @__PURE__ */ new Map([
|
|
10
|
+
["brdrs", "solid"],
|
|
11
|
+
["brdrth", "solid"],
|
|
12
|
+
["brdrsh", "solid"],
|
|
13
|
+
["brdrhair", "solid"],
|
|
14
|
+
["brdrinset", "solid"],
|
|
15
|
+
["brdroutset", "solid"],
|
|
16
|
+
["brdrengrave", "solid"],
|
|
17
|
+
["brdremboss", "solid"],
|
|
18
|
+
["brdrdot", "dotted"],
|
|
19
|
+
["brdrdash", "dashed"],
|
|
20
|
+
["brdrdashsm", "dashed"],
|
|
21
|
+
["brdrdashd", "dashed"],
|
|
22
|
+
["brdrdashdd", "dashed"],
|
|
23
|
+
["brdrdashdot", "dashed"],
|
|
24
|
+
["brdrdashdotdot", "dashed"],
|
|
25
|
+
["brdrdashdotstr", "dashed"],
|
|
26
|
+
["brdrdb", "double"],
|
|
27
|
+
["brdrtriple", "double"],
|
|
28
|
+
["brdrwavydb", "double"],
|
|
29
|
+
["brdrtnthsg", "double"],
|
|
30
|
+
["brdrthtnsg", "double"],
|
|
31
|
+
["brdrtnthtnsg", "double"],
|
|
32
|
+
["brdrtnthmg", "double"],
|
|
33
|
+
["brdrthtnmg", "double"],
|
|
34
|
+
["brdrtnthtnmg", "double"],
|
|
35
|
+
["brdrtnthlg", "double"],
|
|
36
|
+
["brdrthtnlg", "double"],
|
|
37
|
+
["brdrtnthtnlg", "double"],
|
|
38
|
+
["brdrwavy", "solid"]
|
|
39
|
+
]);
|
|
40
|
+
const NO_BORDER_KEYWORDS = /* @__PURE__ */ new Set([
|
|
41
|
+
"brdrnone",
|
|
42
|
+
"brdrnil",
|
|
43
|
+
"brdrtbl"
|
|
44
|
+
]);
|
|
45
|
+
const DEFAULT_BORDER_WIDTH_TWIPS = 15;
|
|
46
|
+
function newPendingCell() {
|
|
47
|
+
return {
|
|
48
|
+
borders: {},
|
|
49
|
+
side: void 0,
|
|
50
|
+
backgroundIndex: void 0,
|
|
51
|
+
verticalMergeFirst: false,
|
|
52
|
+
verticalMergeContinuation: false,
|
|
53
|
+
horizontalMergeFirst: false,
|
|
54
|
+
horizontalMergeContinuation: false
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function applyCellDefinitionControlWord(name, param, cell) {
|
|
58
|
+
const side = CELL_BORDER_SIDES.get(name);
|
|
59
|
+
if (side !== void 0) {
|
|
60
|
+
cell.side = side;
|
|
61
|
+
cell.borders[side] = {
|
|
62
|
+
style: void 0,
|
|
63
|
+
widthTwips: void 0,
|
|
64
|
+
colorIndex: void 0,
|
|
65
|
+
none: false
|
|
66
|
+
};
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
switch (name) {
|
|
70
|
+
case "clvmgf":
|
|
71
|
+
cell.verticalMergeFirst = true;
|
|
72
|
+
return true;
|
|
73
|
+
case "clvmrg":
|
|
74
|
+
cell.verticalMergeContinuation = true;
|
|
75
|
+
return true;
|
|
76
|
+
case "clmgf":
|
|
77
|
+
cell.horizontalMergeFirst = true;
|
|
78
|
+
return true;
|
|
79
|
+
case "clmrg":
|
|
80
|
+
cell.horizontalMergeContinuation = true;
|
|
81
|
+
return true;
|
|
82
|
+
case "clcbpat":
|
|
83
|
+
cell.backgroundIndex = param;
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
const pending = cell.side === void 0 ? void 0 : cell.borders[cell.side];
|
|
87
|
+
if (pending === void 0) return false;
|
|
88
|
+
if (NO_BORDER_KEYWORDS.has(name)) {
|
|
89
|
+
pending.none = true;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
const style = BORDER_STYLES.get(name);
|
|
93
|
+
if (style !== void 0) {
|
|
94
|
+
pending.style = style;
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (name === "brdrw") {
|
|
98
|
+
pending.widthTwips = param;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (name === "brdrcf") {
|
|
102
|
+
pending.colorIndex = param;
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return name.startsWith("brdr") || name.startsWith("brsp");
|
|
106
|
+
}
|
|
107
|
+
function resolveBorder(pending, colorAt) {
|
|
108
|
+
if (pending.none) return;
|
|
109
|
+
const widthPt = twipsToPoints(pending.widthTwips ?? DEFAULT_BORDER_WIDTH_TWIPS);
|
|
110
|
+
if (widthPt <= 0) return;
|
|
111
|
+
const color = pending.colorIndex === void 0 ? void 0 : colorAt(pending.colorIndex);
|
|
112
|
+
const style = pending.style;
|
|
113
|
+
return {
|
|
114
|
+
color: color ?? {
|
|
115
|
+
r: 0,
|
|
116
|
+
g: 0,
|
|
117
|
+
b: 0
|
|
118
|
+
},
|
|
119
|
+
widthPt,
|
|
120
|
+
...style === void 0 || style === "solid" ? {} : { style }
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function borderControlWords(side, border, colorIndex) {
|
|
124
|
+
const sideWord = [...CELL_BORDER_SIDES].find(([, value]) => value === side)?.[0];
|
|
125
|
+
if (sideWord === void 0) return "";
|
|
126
|
+
const style = BORDER_STYLE_CONTROL_WORDS[border.style ?? "solid"];
|
|
127
|
+
const width = Math.max(1, pointsToTwips(border.widthPt));
|
|
128
|
+
return `\\${sideWord}\\${style}\\brdrw${String(width)}` + (colorIndex === void 0 ? "" : `\\brdrcf${String(colorIndex)}`);
|
|
129
|
+
}
|
|
130
|
+
const BORDER_STYLE_CONTROL_WORDS = {
|
|
131
|
+
solid: "brdrs",
|
|
132
|
+
dashed: "brdrdash",
|
|
133
|
+
dotted: "brdrdot",
|
|
134
|
+
double: "brdrdb"
|
|
135
|
+
};
|
|
136
|
+
//#endregion
|
|
137
|
+
export { CELL_BORDER_SIDES, applyCellDefinitionControlWord, borderControlWords, newPendingCell, resolveBorder };
|