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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +226 -0
  3. package/dist/base64.cjs +66 -0
  4. package/dist/base64.d.cts +7 -0
  5. package/dist/base64.d.ts +7 -0
  6. package/dist/base64.js +62 -0
  7. package/dist/bytes.cjs +25 -0
  8. package/dist/bytes.d.cts +6 -0
  9. package/dist/bytes.d.ts +6 -0
  10. package/dist/bytes.js +22 -0
  11. package/dist/cell-format.cjs +142 -0
  12. package/dist/cell-format.d.cts +25 -0
  13. package/dist/cell-format.d.ts +25 -0
  14. package/dist/cell-format.js +137 -0
  15. package/dist/codec.cjs +29 -0
  16. package/dist/codec.d.cts +2344 -0
  17. package/dist/codec.d.ts +2344 -0
  18. package/dist/codec.js +26 -0
  19. package/dist/codepage.cjs +98 -0
  20. package/dist/codepage.d.cts +10 -0
  21. package/dist/codepage.d.ts +10 -0
  22. package/dist/codepage.js +92 -0
  23. package/dist/constructs.cjs +131 -0
  24. package/dist/constructs.d.cts +31 -0
  25. package/dist/constructs.d.ts +31 -0
  26. package/dist/constructs.js +121 -0
  27. package/dist/diagnostics-BgG_KAiN.d.cts +52 -0
  28. package/dist/diagnostics-BgG_KAiN.d.ts +52 -0
  29. package/dist/diagnostics.cjs +76 -0
  30. package/dist/diagnostics.d.cts +2 -0
  31. package/dist/diagnostics.d.ts +2 -0
  32. package/dist/diagnostics.js +68 -0
  33. package/dist/group.cjs +40 -0
  34. package/dist/group.d.cts +11 -0
  35. package/dist/group.d.ts +11 -0
  36. package/dist/group.js +38 -0
  37. package/dist/header.cjs +433 -0
  38. package/dist/header.d.cts +44 -0
  39. package/dist/header.d.ts +44 -0
  40. package/dist/header.js +431 -0
  41. package/dist/index.cjs +28 -0
  42. package/dist/index.d.cts +8 -0
  43. package/dist/index.d.ts +8 -0
  44. package/dist/index.js +8 -0
  45. package/dist/list-id.cjs +30 -0
  46. package/dist/list-id.d.cts +16 -0
  47. package/dist/list-id.d.ts +16 -0
  48. package/dist/list-id.js +27 -0
  49. package/dist/options.cjs +7 -0
  50. package/dist/options.d.cts +18 -0
  51. package/dist/options.d.ts +18 -0
  52. package/dist/options.js +5 -0
  53. package/dist/read.cjs +1211 -0
  54. package/dist/read.d.cts +16 -0
  55. package/dist/read.d.ts +16 -0
  56. package/dist/read.js +1209 -0
  57. package/dist/tokenize.cjs +155 -0
  58. package/dist/tokenize.d.cts +25 -0
  59. package/dist/tokenize.d.ts +25 -0
  60. package/dist/tokenize.js +154 -0
  61. package/dist/units.cjs +43 -0
  62. package/dist/units.d.cts +17 -0
  63. package/dist/units.d.ts +17 -0
  64. package/dist/units.js +29 -0
  65. package/dist/write.cjs +532 -0
  66. package/dist/write.d.cts +7 -0
  67. package/dist/write.d.ts +7 -0
  68. package/dist/write.js +530 -0
  69. package/package.json +95 -2
package/dist/write.js ADDED
@@ -0,0 +1,530 @@
1
+ import { base64ToBytes, bytesToHex } from "./base64.js";
2
+ import { RtfDiagnosticCodes, RtfUnsupportedDocumentKindError } from "./diagnostics.js";
3
+ import { pointsToHalfPoints, pointsToTwips } from "./units.js";
4
+ import { borderControlWords } from "./cell-format.js";
5
+ import { bookmarkResidueControlWords, dttmFromIso, isBookmarkAnchor } from "./constructs.js";
6
+ import { parseRtfListNumId } from "./list-id.js";
7
+ import { clampHeadingLevel, colorToRgbHex, flattenTree } from "document-schema.js";
8
+ //#region src/write.ts
9
+ const LEVEL_NUMBER_FORMAT_BULLET = 23;
10
+ const LEVEL_NUMBER_FORMAT_ARABIC = 0;
11
+ const LIST_LEVEL_INDENT_TWIPS = 720;
12
+ const LIST_MARKER_HANG_TWIPS = 360;
13
+ const BULLET_LEVEL_TEXT = "\\'01\\u183 ?";
14
+ const ARABIC_LEVEL_TEXT = "\\'02\\'00.";
15
+ const OUTPUT_CODEPAGE = 1252;
16
+ const SECTION_BREAK_CONTROL_WORDS = /* @__PURE__ */ new Map([
17
+ ["continuous", "\\sbknone"],
18
+ ["evenPage", "\\sbkeven"],
19
+ ["oddPage", "\\sbkodd"]
20
+ ]);
21
+ const ALIGNMENT_CONTROL_WORDS = /* @__PURE__ */ new Map([
22
+ ["left", "\\ql"],
23
+ ["center", "\\qc"],
24
+ ["right", "\\qr"],
25
+ ["justify", "\\qj"]
26
+ ]);
27
+ const UNKNOWN_REVISION_AUTHOR = "Unknown";
28
+ const DEFAULT_FONT_NAME = "Times New Roman";
29
+ function collectTables(document) {
30
+ const fonts = /* @__PURE__ */ new Map([[DEFAULT_FONT_NAME, 0]]);
31
+ const colors = /* @__PURE__ */ new Map();
32
+ const headingStyles = /* @__PURE__ */ new Map();
33
+ const lists = /* @__PURE__ */ new Map();
34
+ const revisionAuthors = /* @__PURE__ */ new Map([[UNKNOWN_REVISION_AUTHOR, 0]]);
35
+ const noteDescriptor = (descriptor) => {
36
+ if (descriptor.kind !== "provenance") return;
37
+ const author = descriptor.author;
38
+ if (author !== void 0 && !revisionAuthors.has(author)) revisionAuthors.set(author, revisionAuthors.size);
39
+ };
40
+ const noteColor = (color) => {
41
+ if (color === void 0) return;
42
+ const hex = colorToRgbHex(color);
43
+ if (!colors.has(hex)) colors.set(hex, colors.size + 1);
44
+ };
45
+ const noteRun = (run) => {
46
+ if (run.fontFamily !== void 0 && !fonts.has(run.fontFamily)) fonts.set(run.fontFamily, fonts.size);
47
+ noteColor(run.color);
48
+ };
49
+ const noteBlock = (block) => {
50
+ if (block.kind === "constructStart") {
51
+ noteDescriptor(block.descriptor);
52
+ return;
53
+ }
54
+ if (block.kind === "paragraph") {
55
+ for (const run of block.runs) noteRun(run);
56
+ for (const extent of block.constructs ?? []) noteDescriptor(extent.descriptor);
57
+ if (block.headingLevel !== void 0) {
58
+ const level = clampHeadingLevel(block.headingLevel);
59
+ if (!headingStyles.has(level)) headingStyles.set(level, level);
60
+ }
61
+ const numId = block.list?.numId;
62
+ if (numId !== void 0 && !lists.has(numId)) {
63
+ const parsed = parseRtfListNumId(numId);
64
+ lists.set(numId, {
65
+ index: lists.size + 1,
66
+ definition: {
67
+ type: parsed?.type ?? "bullet",
68
+ start: parsed?.start ?? 1
69
+ }
70
+ });
71
+ }
72
+ return;
73
+ }
74
+ if (block.kind === "table") for (const row of block.rows) for (const cell of row.cells) {
75
+ noteColor(cell.background);
76
+ for (const side of CELL_BORDER_ORDER) noteColor(cell.borders?.[side]?.color);
77
+ for (const inner of cell.blocks) noteBlock(inner);
78
+ }
79
+ };
80
+ if (document.kind === "wordprocessing") for (const section of document.sections) for (const block of section.blocks) noteBlock(block);
81
+ return {
82
+ fonts,
83
+ colors,
84
+ headingStyles,
85
+ lists,
86
+ revisionAuthors
87
+ };
88
+ }
89
+ function escapeText(text) {
90
+ let out = "";
91
+ for (const character of text) {
92
+ switch (character) {
93
+ case "\\":
94
+ out += "\\\\";
95
+ continue;
96
+ case "{":
97
+ out += "\\{";
98
+ continue;
99
+ case "}":
100
+ out += "\\}";
101
+ continue;
102
+ case " ":
103
+ out += "\\tab ";
104
+ continue;
105
+ case "\n":
106
+ case "\r":
107
+ out += "\\line ";
108
+ continue;
109
+ }
110
+ const code = character.codePointAt(0) ?? 0;
111
+ if (code >= 32 && code < 127) {
112
+ out += character;
113
+ continue;
114
+ }
115
+ for (let unit = 0; unit < character.length; unit += 1) {
116
+ const value = character.charCodeAt(unit);
117
+ const signed = value > 32767 ? value - 65536 : value;
118
+ out += `\\u${String(signed)} ?`;
119
+ }
120
+ }
121
+ return out;
122
+ }
123
+ function bookmarkStartGroup(descriptor) {
124
+ if (!isBookmarkAnchor(descriptor)) return "";
125
+ return `{\\*\\bkmkstart${bookmarkResidueControlWords(descriptor)} ${escapeText(descriptor.name)}}`;
126
+ }
127
+ const CHREV_CONTROL_WORDS = {
128
+ insertion: {
129
+ flag: "\\revised",
130
+ author: "revauth",
131
+ date: "revdttm"
132
+ },
133
+ deletion: {
134
+ flag: "\\deleted",
135
+ author: "revauthdel",
136
+ date: "revdttmdel"
137
+ },
138
+ moveFrom: {
139
+ flag: "\\mvf",
140
+ author: "mvauth",
141
+ date: "mvdate"
142
+ },
143
+ moveTo: {
144
+ flag: "\\mvt",
145
+ author: "mvauth",
146
+ date: "mvdate"
147
+ },
148
+ formatChange: {
149
+ flag: "",
150
+ author: "crauth",
151
+ date: "crdate"
152
+ }
153
+ };
154
+ function revisionsCovering(extents, index) {
155
+ return extents.filter((extent) => extent.descriptor.kind === "provenance" && extent.startRun <= index && index < extent.endRun).map((extent) => extent.descriptor).filter((descriptor) => descriptor.kind === "provenance");
156
+ }
157
+ const CELL_BORDER_ORDER = [
158
+ "top",
159
+ "left",
160
+ "bottom",
161
+ "right"
162
+ ];
163
+ function verticalMergeCoverage(table) {
164
+ const covered = table.rows.map((row) => row.cells.map(() => false));
165
+ for (const [rowIndex, row] of table.rows.entries()) for (const [cellIndex, cell] of row.cells.entries()) {
166
+ const rowSpan = cell.rowSpan ?? 1;
167
+ for (let next = 1; next < rowSpan; next += 1) {
168
+ const target = covered[rowIndex + next];
169
+ if (target !== void 0 && cellIndex < target.length) target[cellIndex] = true;
170
+ }
171
+ }
172
+ return covered;
173
+ }
174
+ function nameOf(descriptor) {
175
+ return isBookmarkAnchor(descriptor) ? descriptor.name : "";
176
+ }
177
+ function describeConstructGap(descriptor) {
178
+ switch (descriptor.kind) {
179
+ case "contentControl": return "structured-document-tag equivalent; its own \\*\\formfield production is a narrower construct this writer does not yet mint";
180
+ case "provenance": return "block-scoped revision mark: its <chrev> production is a character property, so a tracked change reaches RTF only as a run-level extent";
181
+ case "anchor": return `spelling for a '${descriptor.anchorType}' anchor, whose body would need the note or annotation destination this reader does not place`;
182
+ case "field": return "block-scoped field: a field is a character-stream construct, written from a run's own hyperlink rather than from a block marker";
183
+ case "link": return "block-scoped link; an external target rides ContentRun.hyperlink instead";
184
+ default: return "equivalent construct";
185
+ }
186
+ }
187
+ var RtfWriter = class {
188
+ tables;
189
+ sink;
190
+ lineEnding;
191
+ out = "";
192
+ openConstructs = [];
193
+ constructor(tables, sink, lineEnding) {
194
+ this.tables = tables;
195
+ this.sink = sink;
196
+ this.lineEnding = lineEnding;
197
+ }
198
+ raw(text) {
199
+ this.out += text;
200
+ }
201
+ line(text) {
202
+ this.out += text + this.lineEnding;
203
+ }
204
+ get text() {
205
+ return this.out;
206
+ }
207
+ writeHeader(document) {
208
+ this.raw(`{\\rtf1\\ansi\\ansicpg${String(OUTPUT_CODEPAGE)}\\deff0\\uc1`);
209
+ this.writeDocumentGeometry(document);
210
+ this.writeFontTable();
211
+ this.writeColorTable();
212
+ this.writeStyleSheet();
213
+ this.writeListTables();
214
+ this.writeRevisionTable();
215
+ this.writeInfoGroup(document);
216
+ this.line("");
217
+ }
218
+ writeFontTable() {
219
+ this.raw("{\\fonttbl");
220
+ for (const [name, index] of [...this.tables.fonts].sort((left, right) => left[1] - right[1])) this.raw(`{\\f${String(index)}\\fnil\\fcharset0 ${escapeText(name)};}`);
221
+ this.raw("}");
222
+ }
223
+ writeColorTable() {
224
+ if (this.tables.colors.size === 0) return;
225
+ this.raw("{\\colortbl;");
226
+ for (const [hex] of [...this.tables.colors].sort((left, right) => left[1] - right[1])) {
227
+ const red = Number.parseInt(hex.slice(0, 2), 16);
228
+ const green = Number.parseInt(hex.slice(2, 4), 16);
229
+ const blue = Number.parseInt(hex.slice(4, 6), 16);
230
+ this.raw(`\\red${String(red)}\\green${String(green)}\\blue${String(blue)};`);
231
+ }
232
+ this.raw("}");
233
+ }
234
+ writeStyleSheet() {
235
+ if (this.tables.headingStyles.size === 0) return;
236
+ this.raw("{\\stylesheet{\\s0\\snext0 Normal;}");
237
+ for (const [level, handle] of [...this.tables.headingStyles].sort((left, right) => left[0] - right[0])) this.raw(`{\\s${String(handle)}\\sbasedon0\\snext0\\outlinelevel${String(level - 1)} heading ${String(level)};}`);
238
+ this.raw("}");
239
+ }
240
+ writeListTables() {
241
+ if (this.tables.lists.size === 0) return;
242
+ const entries = [...this.tables.lists.values()].sort((left, right) => left.index - right.index);
243
+ this.raw("{\\*\\listtable");
244
+ for (const entry of entries) {
245
+ const bullet = entry.definition.type === "bullet";
246
+ const numberFormat = bullet ? LEVEL_NUMBER_FORMAT_BULLET : LEVEL_NUMBER_FORMAT_ARABIC;
247
+ const levelText = bullet ? BULLET_LEVEL_TEXT : ARABIC_LEVEL_TEXT;
248
+ const levelNumbers = bullet ? "" : "\\'01";
249
+ this.raw(`{\\list\\listtemplateid${String(entry.index)}\\listhybrid`);
250
+ for (let level = 0; level < 9; level += 1) {
251
+ const indent = LIST_LEVEL_INDENT_TWIPS * (level + 1);
252
+ this.raw(`{\\listlevel\\levelnfc${String(numberFormat)}\\levelnfcn${String(numberFormat)}\\leveljc0\\leveljcn0\\levelfollow0\\levelstartat${String(entry.definition.start)}\\levelspace0\\levelindent0{\\leveltext${levelText};}{\\levelnumbers${levelNumbers};}\\fi-${String(LIST_MARKER_HANG_TWIPS)}\\li${String(indent)}\\lin${String(indent)}}`);
253
+ }
254
+ this.raw(`\\listid${String(1e3 + entry.index)}}`);
255
+ }
256
+ this.raw("}{\\*\\listoverridetable");
257
+ for (const entry of entries) this.raw(`{\\listoverride\\listid${String(1e3 + entry.index)}\\listoverridecount0\\ls${String(entry.index)}}`);
258
+ this.raw("}");
259
+ }
260
+ writeRevisionTable() {
261
+ if (this.tables.revisionAuthors.size <= 1) return;
262
+ this.raw("{\\*\\revtbl");
263
+ for (const [author] of [...this.tables.revisionAuthors].sort((left, right) => left[1] - right[1])) this.raw(`{${escapeText(author)};}`);
264
+ this.raw("}");
265
+ }
266
+ writeInfoGroup(document) {
267
+ const { title, author, subject, keywords } = document.metadata;
268
+ const fields = [];
269
+ if (title !== void 0) fields.push(`{\\title ${escapeText(title)}}`);
270
+ if (author !== void 0) fields.push(`{\\author ${escapeText(author)}}`);
271
+ if (subject !== void 0) fields.push(`{\\subject ${escapeText(subject)}}`);
272
+ if (keywords !== void 0 && keywords.length > 0) fields.push(`{\\keywords ${escapeText(keywords.join("; "))}}`);
273
+ if (fields.length > 0) this.raw(`{\\info${fields.join("")}}`);
274
+ }
275
+ writeDocumentGeometry(document) {
276
+ const first = document.sections[0];
277
+ if (first === void 0) return;
278
+ this.raw(`\\paperw${String(pointsToTwips(first.pageSize.widthPt))}\\paperh${String(pointsToTwips(first.pageSize.heightPt))}\\margl${String(pointsToTwips(first.margins.leftPt))}\\margr${String(pointsToTwips(first.margins.rightPt))}\\margt${String(pointsToTwips(first.margins.topPt))}\\margb${String(pointsToTwips(first.margins.bottomPt))}`);
279
+ }
280
+ writeSection(section, isFirst) {
281
+ if (!isFirst) this.line("\\sect");
282
+ this.line(`\\sectd${SECTION_BREAK_CONTROL_WORDS.get(section.breakType ?? "") ?? ""}\\pgwsxn${String(pointsToTwips(section.pageSize.widthPt))}\\pghsxn${String(pointsToTwips(section.pageSize.heightPt))}\\marglsxn${String(pointsToTwips(section.margins.leftPt))}\\margrsxn${String(pointsToTwips(section.margins.rightPt))}\\margtsxn${String(pointsToTwips(section.margins.topPt))}\\margbsxn${String(pointsToTwips(section.margins.bottomPt))}`);
283
+ this.writeBlocks(section.blocks);
284
+ }
285
+ writeBlocks(blocks) {
286
+ for (const block of blocks) this.writeBlock(block);
287
+ }
288
+ writeBlock(block) {
289
+ switch (block.kind) {
290
+ case "paragraph":
291
+ this.writeParagraph(block, false);
292
+ return;
293
+ case "table":
294
+ this.writeTable(block);
295
+ return;
296
+ case "image":
297
+ this.writeImageParagraph(block.base64, block);
298
+ return;
299
+ case "pageBreak":
300
+ this.line("\\page\\pard");
301
+ return;
302
+ case "embeddedObject":
303
+ this.sink({
304
+ code: RtfDiagnosticCodes.EMBEDDED_OBJECT_DROPPED,
305
+ severity: "warning",
306
+ message: `an embedded ${block.objectKind} object is dropped: writing it as an RTF \\object would need the OLE container this package does not build`
307
+ });
308
+ return;
309
+ case "constructStart":
310
+ this.openConstruct(block.descriptor);
311
+ return;
312
+ case "constructEnd":
313
+ this.closeConstruct();
314
+ return;
315
+ default: return;
316
+ }
317
+ }
318
+ openConstruct(descriptor) {
319
+ if (!isBookmarkAnchor(descriptor)) {
320
+ this.sink({
321
+ code: RtfDiagnosticCodes.CONSTRUCT_UNREPRESENTED,
322
+ severity: "warning",
323
+ message: `a ${descriptor.kind} construct is dropped: RTF has no ${describeConstructGap(descriptor)}`
324
+ });
325
+ this.openConstructs.push(void 0);
326
+ return;
327
+ }
328
+ this.openConstructs.push(descriptor.name);
329
+ this.line(bookmarkStartGroup(descriptor));
330
+ }
331
+ closeConstruct() {
332
+ const name = this.openConstructs.pop();
333
+ if (name !== void 0) this.line(`{\\*\\bkmkend ${escapeText(name)}}`);
334
+ }
335
+ writeParagraph(paragraph, inTable) {
336
+ this.raw("\\pard\\plain");
337
+ if (inTable) this.raw("\\intbl");
338
+ this.raw(this.paragraphProperties(paragraph));
339
+ this.raw(" ");
340
+ const bookmarks = (paragraph.constructs ?? []).filter((extent) => isBookmarkAnchor(extent.descriptor));
341
+ const revisions = (paragraph.constructs ?? []).filter((extent) => extent.descriptor.kind === "provenance");
342
+ for (const [index, run] of paragraph.runs.entries()) {
343
+ this.writeRunBoundaries(bookmarks, index);
344
+ this.writeRun(run, revisionsCovering(revisions, index));
345
+ }
346
+ this.writeRunBoundaries(bookmarks, paragraph.runs.length);
347
+ if (!inTable) this.line("\\par");
348
+ }
349
+ writeRunBoundaries(extents, position) {
350
+ for (const extent of extents) if (extent.endRun === position && extent.startRun !== position) this.raw(`{\\*\\bkmkend ${escapeText(nameOf(extent.descriptor))}}`);
351
+ for (const extent of extents) if (extent.startRun === position) {
352
+ this.raw(bookmarkStartGroup(extent.descriptor));
353
+ if (extent.endRun === position) this.raw(`{\\*\\bkmkend ${escapeText(nameOf(extent.descriptor))}}`);
354
+ }
355
+ }
356
+ paragraphProperties(paragraph) {
357
+ let out = "";
358
+ const level = paragraph.headingLevel === void 0 ? void 0 : clampHeadingLevel(paragraph.headingLevel);
359
+ const styleHandle = level === void 0 ? void 0 : this.tables.headingStyles.get(level);
360
+ if (styleHandle !== void 0 && level !== void 0) out += `\\s${String(styleHandle)}\\outlinelevel${String(level - 1)}`;
361
+ const alignment = paragraph.alignment === void 0 ? void 0 : ALIGNMENT_CONTROL_WORDS.get(paragraph.alignment);
362
+ if (alignment !== void 0) out += alignment;
363
+ const list = paragraph.list;
364
+ if (list !== void 0) {
365
+ const numId = list.numId;
366
+ const entry = numId === void 0 ? void 0 : this.tables.lists.get(numId);
367
+ if (entry === void 0) this.sink({
368
+ code: RtfDiagnosticCodes.CONSTRUCT_UNREPRESENTED,
369
+ severity: "info",
370
+ message: "a list membership carries no numId this writer minted a list for; the paragraph keeps its indentation but no list marker"
371
+ });
372
+ else {
373
+ const indent = LIST_LEVEL_INDENT_TWIPS * (list.level + 1);
374
+ out += `\\ls${String(entry.index)}\\ilvl${String(list.level)}\\fi-${String(LIST_MARKER_HANG_TWIPS)}\\li${String(indent)}`;
375
+ }
376
+ }
377
+ if (paragraph.indentLeftPt !== void 0) out += `\\li${String(pointsToTwips(paragraph.indentLeftPt))}`;
378
+ if (paragraph.indentFirstLinePt !== void 0) out += `\\fi${String(pointsToTwips(paragraph.indentFirstLinePt))}`;
379
+ if (paragraph.spacingBeforePt !== void 0) out += `\\sb${String(pointsToTwips(paragraph.spacingBeforePt))}`;
380
+ if (paragraph.spacingAfterPt !== void 0) out += `\\sa${String(pointsToTwips(paragraph.spacingAfterPt))}`;
381
+ if (paragraph.lineSpacing !== void 0) out += `\\sl${String(Math.round(paragraph.lineSpacing * 240))}\\slmult1`;
382
+ if (paragraph.pageBreakBefore === true) out += "\\pagebb";
383
+ return out;
384
+ }
385
+ writeRun(run, revisions = []) {
386
+ const properties = this.runProperties(run) + this.revisionProperties(revisions);
387
+ const body = `${properties}${properties.length > 0 ? " " : ""}${escapeText(run.text)}`;
388
+ if (run.hyperlink === void 0) {
389
+ this.raw(`{${body}}`);
390
+ return;
391
+ }
392
+ this.raw(`{\\field{\\*\\fldinst{HYPERLINK "${escapeText(run.hyperlink)}"}}{\\fldrslt{${body}}}}`);
393
+ }
394
+ revisionProperties(revisions) {
395
+ let out = "";
396
+ for (const descriptor of revisions) {
397
+ const author = descriptor.author;
398
+ const authorIndex = author === void 0 ? void 0 : this.tables.revisionAuthors.get(author);
399
+ const dttm = descriptor.dateIso === void 0 ? void 0 : dttmFromIso(descriptor.dateIso);
400
+ const words = CHREV_CONTROL_WORDS[descriptor.change];
401
+ out += words.flag;
402
+ if (authorIndex !== void 0) out += `\\${words.author}${String(authorIndex)}`;
403
+ if (dttm !== void 0) out += `\\${words.date}${String(dttm)}`;
404
+ }
405
+ return out;
406
+ }
407
+ runProperties(run) {
408
+ let out = "";
409
+ const fontIndex = run.fontFamily === void 0 ? void 0 : this.tables.fonts.get(run.fontFamily);
410
+ if (fontIndex !== void 0 && fontIndex !== 0) out += `\\f${String(fontIndex)}`;
411
+ const halfPoints = run.sizePt === void 0 ? 24 : pointsToHalfPoints(run.sizePt);
412
+ if (halfPoints !== 24) out += `\\fs${String(halfPoints)}`;
413
+ if (run.bold === true) out += "\\b";
414
+ if (run.italic === true) out += "\\i";
415
+ if (run.underline === true) out += "\\ul";
416
+ if (run.strike === true) out += "\\strike";
417
+ const colorIndex = colorIndexOf(run.color, this.tables.colors);
418
+ if (colorIndex !== void 0) out += `\\cf${String(colorIndex)}`;
419
+ return out;
420
+ }
421
+ writeTable(table) {
422
+ const covered = verticalMergeCoverage(table);
423
+ for (const [rowIndex, row] of table.rows.entries()) {
424
+ let right = 0;
425
+ let column = 0;
426
+ const definitions = [];
427
+ const marks = [];
428
+ for (const [cellIndex, cell] of row.cells.entries()) {
429
+ const colSpan = cell.colSpan ?? 1;
430
+ for (let offset = 0; offset < colSpan; offset += 1) {
431
+ right += pointsToTwips(table.columnWidthsPt[column] ?? 0);
432
+ column += 1;
433
+ definitions.push(this.cellDefinition(cell, covered[rowIndex]?.[cellIndex] === true, colSpan > 1 ? offset === 0 ? "mergeFirst" : "mergeContinuation" : "single", right));
434
+ marks.push({
435
+ cell,
436
+ empty: offset > 0
437
+ });
438
+ }
439
+ }
440
+ const rowDefinition = `\\trowd\\trgaph108\\trleft0${definitions.join("")}`;
441
+ this.line(rowDefinition);
442
+ for (const mark of marks) {
443
+ this.writeCellBlocks(mark.empty ? [] : mark.cell.blocks);
444
+ this.raw("\\cell");
445
+ }
446
+ this.line(`${rowDefinition}\\row`);
447
+ }
448
+ this.line("\\pard");
449
+ }
450
+ cellDefinition(cell, isVerticalContinuation, horizontal, rightTwips) {
451
+ let out = "";
452
+ if (isVerticalContinuation) out += "\\clvmrg";
453
+ else if ((cell.rowSpan ?? 1) > 1) out += "\\clvmgf";
454
+ if (horizontal === "mergeFirst") out += "\\clmgf";
455
+ else if (horizontal === "mergeContinuation") return `${out}\\clmrg\\cellx${String(rightTwips)}`;
456
+ const borders = cell.borders;
457
+ if (borders !== void 0) for (const side of CELL_BORDER_ORDER) {
458
+ const border = borders[side];
459
+ if (border !== void 0) out += borderControlWords(side, border, colorIndexOf(border.color, this.tables.colors));
460
+ }
461
+ if (cell.background !== void 0) {
462
+ const index = colorIndexOf(cell.background, this.tables.colors);
463
+ if (index !== void 0) out += `\\clcbpat${String(index)}`;
464
+ }
465
+ return `${out}\\cellx${String(rightTwips)}`;
466
+ }
467
+ writeCellBlocks(blocks) {
468
+ const paragraphs = blocks.filter((block) => block.kind === "paragraph");
469
+ if (paragraphs.length === 0) {
470
+ this.raw("\\pard\\plain\\intbl ");
471
+ return;
472
+ }
473
+ for (const [index, paragraph] of paragraphs.entries()) {
474
+ this.writeParagraph(paragraph, true);
475
+ if (index < paragraphs.length - 1) this.raw("\\par");
476
+ }
477
+ }
478
+ writeImageParagraph(base64, image) {
479
+ const bytes = base64ToBytes(base64);
480
+ if (bytes === void 0 || bytes.length === 0) {
481
+ this.sink({
482
+ code: RtfDiagnosticCodes.UNSUPPORTED_PICTURE_FORMAT,
483
+ severity: "warning",
484
+ message: "an image block's base64 payload could not be decoded, so no \\pict destination is written for it"
485
+ });
486
+ return;
487
+ }
488
+ const widthTwips = pointsToTwips(image.widthPt);
489
+ const heightTwips = pointsToTwips(image.heightPt);
490
+ this.line(`\\pard\\plain {\\*\\shppict{\\pict\\${image.format === "png" ? "pngblip" : "jpegblip"}\\picwgoal${String(widthTwips)}\\pichgoal${String(heightTwips)}${this.lineEnding}${wrapHex(bytesToHex(bytes), this.lineEnding)}}}\\par`);
491
+ }
492
+ };
493
+ function colorIndexOf(color, colors) {
494
+ return color === void 0 ? void 0 : colors.get(colorToRgbHex(color));
495
+ }
496
+ const HEX_LINE_LENGTH = 128;
497
+ function wrapHex(hex, lineEnding) {
498
+ const lines = [];
499
+ for (let index = 0; index < hex.length; index += HEX_LINE_LENGTH) lines.push(hex.slice(index, index + HEX_LINE_LENGTH));
500
+ return lines.join(lineEnding);
501
+ }
502
+ function encodeAscii(text) {
503
+ const out = new Uint8Array(text.length);
504
+ for (let index = 0; index < text.length; index += 1) out[index] = text.charCodeAt(index) & 127;
505
+ return out;
506
+ }
507
+ function writeRtfContent(document, options = {}) {
508
+ options.signal?.throwIfAborted();
509
+ if (document.kind !== "wordprocessing") throw new RtfUnsupportedDocumentKindError(document.kind);
510
+ const sink = options.sink ?? (() => {});
511
+ const writer = new RtfWriter(collectTables(document), sink, options.lineEnding ?? "\n");
512
+ writer.writeHeader(document);
513
+ for (const [index, section] of document.sections.entries()) writer.writeSection(section, index === 0);
514
+ writer.raw("}");
515
+ return encodeAscii(writer.text);
516
+ }
517
+ function writeRtf(documentPackage, options = {}) {
518
+ const sink = options.sink;
519
+ if (sink !== void 0 && hasPackageTables(documentPackage)) sink({
520
+ code: RtfDiagnosticCodes.PACKAGE_TABLE_DROPPED,
521
+ severity: "info",
522
+ message: "the package's definitions/layers/attachments/destinations tables are dropped: flattening resolves style refs, and RTF has no destination for the remaining tenants"
523
+ });
524
+ return writeRtfContent(flattenTree(documentPackage), options);
525
+ }
526
+ function hasPackageTables(documentPackage) {
527
+ return documentPackage.definitions !== void 0 || documentPackage.layers !== void 0 || documentPackage.attachments !== void 0 || documentPackage.destinations !== void 0;
528
+ }
529
+ //#endregion
530
+ export { writeRtf, writeRtfContent };
package/package.json CHANGED
@@ -1,5 +1,98 @@
1
1
  {
2
2
  "name": "rtf-codec",
3
- "version": "0.0.0",
4
- "private": false
3
+ "version": "1.1.0",
4
+ "description": "Hand-written Rich Text Format (RTF 1.9.1) reader and writer against the shared document-schema.js content pivot.",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ExaDev/documents.js.git",
9
+ "directory": "packages/rtf-codec"
10
+ },
11
+ "homepage": "https://github.com/ExaDev/documents.js/tree/main/packages/rtf-codec",
12
+ "bugs": {
13
+ "url": "https://github.com/ExaDev/documents.js/issues"
14
+ },
15
+ "exports": {
16
+ ".": {
17
+ "types": {
18
+ "import": "./dist/index.d.ts",
19
+ "require": "./dist/index.d.cts"
20
+ },
21
+ "import": "./dist/index.js",
22
+ "require": "./dist/index.cjs"
23
+ },
24
+ "./*": {
25
+ "types": {
26
+ "import": "./dist/*.d.ts",
27
+ "require": "./dist/*.d.cts"
28
+ },
29
+ "import": "./dist/*.js",
30
+ "require": "./dist/*.cjs"
31
+ }
32
+ },
33
+ "main": "./dist/index.cjs",
34
+ "module": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "provenance": true,
42
+ "registry": "https://registry.npmjs.org/"
43
+ },
44
+ "sideEffects": false,
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
48
+ "scripts": {
49
+ "build": "turbo run _build",
50
+ "_build": "tsdown",
51
+ "prepublishOnly": "pnpm run lint && pnpm run typecheck && tsdown && publint && attw --pack",
52
+ "lint": "turbo run _lint",
53
+ "_lint": "eslint . --fix --cache --max-warnings 0",
54
+ "typecheck": "turbo run _typecheck _typecheck:node _typecheck:attw",
55
+ "_typecheck": "tsc -p tsconfig.json",
56
+ "_typecheck:node": "tsc -p tsconfig.node.json",
57
+ "_typecheck:attw": "attw --pack",
58
+ "test": "turbo run _test",
59
+ "_test": "vitest run --project unit",
60
+ "test:workers": "turbo run _test:workers",
61
+ "_test:workers": "vitest run --config vitest.workers.config.ts",
62
+ "test:watch": "vitest --project unit",
63
+ "test:coverage": "turbo run _test:coverage",
64
+ "_test:coverage": "vitest run --project unit --coverage",
65
+ "test:smoke": "turbo run _test:smoke",
66
+ "_test:smoke": "vitest run --project smoke",
67
+ "prepare": "husky",
68
+ "release": "semantic-release"
69
+ },
70
+ "keywords": [
71
+ "rtf",
72
+ "rich-text-format",
73
+ "codec",
74
+ "round-trip",
75
+ "zod",
76
+ "wordprocessing"
77
+ ],
78
+ "license": "MIT",
79
+ "packageManager": "pnpm@11.6.0",
80
+ "dependencies": {
81
+ "document-schema.js": "^5.5.0",
82
+ "zod": "^4.4.3"
83
+ },
84
+ "devDependencies": {
85
+ "@arethetypeswrong/cli": "^0.18.5",
86
+ "@cloudflare/vitest-pool-workers": "^0.21.2",
87
+ "@types/node": "^26.1.1",
88
+ "@vitest/coverage-v8": "^4.1.10",
89
+ "eslint": "^10.8.0",
90
+ "husky": "^9.1.7",
91
+ "publint": "^0.3.21",
92
+ "semantic-release": "^25.0.8",
93
+ "tsdown": "^0.22.13",
94
+ "turbo": "^2.10.8",
95
+ "typescript": "^6.0.3",
96
+ "vitest": "^4.1.10"
97
+ }
5
98
  }