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/dist/write.cjs CHANGED
@@ -2,6 +2,8 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_base64 = require("./base64.cjs");
3
3
  const require_diagnostics = require("./diagnostics.cjs");
4
4
  const require_units = require("./units.cjs");
5
+ const require_cell_format = require("./cell-format.cjs");
6
+ const require_constructs = require("./constructs.cjs");
5
7
  const require_list_id = require("./list-id.cjs");
6
8
  let document_schema_js = require("document-schema.js");
7
9
  //#region src/write.ts
@@ -12,28 +14,47 @@ const LIST_MARKER_HANG_TWIPS = 360;
12
14
  const BULLET_LEVEL_TEXT = "\\'01\\u183 ?";
13
15
  const ARABIC_LEVEL_TEXT = "\\'02\\'00.";
14
16
  const OUTPUT_CODEPAGE = 1252;
17
+ const SECTION_BREAK_CONTROL_WORDS = /* @__PURE__ */ new Map([
18
+ ["continuous", "\\sbknone"],
19
+ ["evenPage", "\\sbkeven"],
20
+ ["oddPage", "\\sbkodd"]
21
+ ]);
15
22
  const ALIGNMENT_CONTROL_WORDS = /* @__PURE__ */ new Map([
16
23
  ["left", "\\ql"],
17
24
  ["center", "\\qc"],
18
25
  ["right", "\\qr"],
19
26
  ["justify", "\\qj"]
20
27
  ]);
28
+ const UNKNOWN_REVISION_AUTHOR = "Unknown";
21
29
  const DEFAULT_FONT_NAME = "Times New Roman";
22
30
  function collectTables(document) {
23
31
  const fonts = /* @__PURE__ */ new Map([[DEFAULT_FONT_NAME, 0]]);
24
32
  const colors = /* @__PURE__ */ new Map();
25
33
  const headingStyles = /* @__PURE__ */ new Map();
26
34
  const lists = /* @__PURE__ */ new Map();
35
+ const revisionAuthors = /* @__PURE__ */ new Map([[UNKNOWN_REVISION_AUTHOR, 0]]);
36
+ const noteDescriptor = (descriptor) => {
37
+ if (descriptor.kind !== "provenance") return;
38
+ const author = descriptor.author;
39
+ if (author !== void 0 && !revisionAuthors.has(author)) revisionAuthors.set(author, revisionAuthors.size);
40
+ };
41
+ const noteColor = (color) => {
42
+ if (color === void 0) return;
43
+ const hex = (0, document_schema_js.colorToRgbHex)(color);
44
+ if (!colors.has(hex)) colors.set(hex, colors.size + 1);
45
+ };
27
46
  const noteRun = (run) => {
28
47
  if (run.fontFamily !== void 0 && !fonts.has(run.fontFamily)) fonts.set(run.fontFamily, fonts.size);
29
- if (run.color !== void 0) {
30
- const hex = (0, document_schema_js.colorToRgbHex)(run.color);
31
- if (!colors.has(hex)) colors.set(hex, colors.size + 1);
32
- }
48
+ noteColor(run.color);
33
49
  };
34
50
  const noteBlock = (block) => {
51
+ if (block.kind === "constructStart") {
52
+ noteDescriptor(block.descriptor);
53
+ return;
54
+ }
35
55
  if (block.kind === "paragraph") {
36
56
  for (const run of block.runs) noteRun(run);
57
+ for (const extent of block.constructs ?? []) noteDescriptor(extent.descriptor);
37
58
  if (block.headingLevel !== void 0) {
38
59
  const level = (0, document_schema_js.clampHeadingLevel)(block.headingLevel);
39
60
  if (!headingStyles.has(level)) headingStyles.set(level, level);
@@ -51,14 +72,19 @@ function collectTables(document) {
51
72
  }
52
73
  return;
53
74
  }
54
- if (block.kind === "table") for (const row of block.rows) for (const cell of row.cells) for (const inner of cell.blocks) noteBlock(inner);
75
+ if (block.kind === "table") for (const row of block.rows) for (const cell of row.cells) {
76
+ noteColor(cell.background);
77
+ for (const side of CELL_BORDER_ORDER) noteColor(cell.borders?.[side]?.color);
78
+ for (const inner of cell.blocks) noteBlock(inner);
79
+ }
55
80
  };
56
81
  if (document.kind === "wordprocessing") for (const section of document.sections) for (const block of section.blocks) noteBlock(block);
57
82
  return {
58
83
  fonts,
59
84
  colors,
60
85
  headingStyles,
61
- lists
86
+ lists,
87
+ revisionAuthors
62
88
  };
63
89
  }
64
90
  function escapeText(text) {
@@ -95,11 +121,76 @@ function escapeText(text) {
95
121
  }
96
122
  return out;
97
123
  }
124
+ function bookmarkStartGroup(descriptor) {
125
+ if (!require_constructs.isBookmarkAnchor(descriptor)) return "";
126
+ return `{\\*\\bkmkstart${require_constructs.bookmarkResidueControlWords(descriptor)} ${escapeText(descriptor.name)}}`;
127
+ }
128
+ const CHREV_CONTROL_WORDS = {
129
+ insertion: {
130
+ flag: "\\revised",
131
+ author: "revauth",
132
+ date: "revdttm"
133
+ },
134
+ deletion: {
135
+ flag: "\\deleted",
136
+ author: "revauthdel",
137
+ date: "revdttmdel"
138
+ },
139
+ moveFrom: {
140
+ flag: "\\mvf",
141
+ author: "mvauth",
142
+ date: "mvdate"
143
+ },
144
+ moveTo: {
145
+ flag: "\\mvt",
146
+ author: "mvauth",
147
+ date: "mvdate"
148
+ },
149
+ formatChange: {
150
+ flag: "",
151
+ author: "crauth",
152
+ date: "crdate"
153
+ }
154
+ };
155
+ function revisionsCovering(extents, index) {
156
+ return extents.filter((extent) => extent.descriptor.kind === "provenance" && extent.startRun <= index && index < extent.endRun).map((extent) => extent.descriptor).filter((descriptor) => descriptor.kind === "provenance");
157
+ }
158
+ const CELL_BORDER_ORDER = [
159
+ "top",
160
+ "left",
161
+ "bottom",
162
+ "right"
163
+ ];
164
+ function verticalMergeCoverage(table) {
165
+ const covered = table.rows.map((row) => row.cells.map(() => false));
166
+ for (const [rowIndex, row] of table.rows.entries()) for (const [cellIndex, cell] of row.cells.entries()) {
167
+ const rowSpan = cell.rowSpan ?? 1;
168
+ for (let next = 1; next < rowSpan; next += 1) {
169
+ const target = covered[rowIndex + next];
170
+ if (target !== void 0 && cellIndex < target.length) target[cellIndex] = true;
171
+ }
172
+ }
173
+ return covered;
174
+ }
175
+ function nameOf(descriptor) {
176
+ return require_constructs.isBookmarkAnchor(descriptor) ? descriptor.name : "";
177
+ }
178
+ function describeConstructGap(descriptor) {
179
+ switch (descriptor.kind) {
180
+ case "contentControl": return "structured-document-tag equivalent; its own \\*\\formfield production is a narrower construct this writer does not yet mint";
181
+ 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";
182
+ case "anchor": return `spelling for a '${descriptor.anchorType}' anchor, whose body would need the note or annotation destination this reader does not place`;
183
+ 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";
184
+ case "link": return "block-scoped link; an external target rides ContentRun.hyperlink instead";
185
+ default: return "equivalent construct";
186
+ }
187
+ }
98
188
  var RtfWriter = class {
99
189
  tables;
100
190
  sink;
101
191
  lineEnding;
102
192
  out = "";
193
+ openConstructs = [];
103
194
  constructor(tables, sink, lineEnding) {
104
195
  this.tables = tables;
105
196
  this.sink = sink;
@@ -116,10 +207,12 @@ var RtfWriter = class {
116
207
  }
117
208
  writeHeader(document) {
118
209
  this.raw(`{\\rtf1\\ansi\\ansicpg${String(OUTPUT_CODEPAGE)}\\deff0\\uc1`);
210
+ this.writeDocumentGeometry(document);
119
211
  this.writeFontTable();
120
212
  this.writeColorTable();
121
213
  this.writeStyleSheet();
122
214
  this.writeListTables();
215
+ this.writeRevisionTable();
123
216
  this.writeInfoGroup(document);
124
217
  this.line("");
125
218
  }
@@ -165,6 +258,12 @@ var RtfWriter = class {
165
258
  for (const entry of entries) this.raw(`{\\listoverride\\listid${String(1e3 + entry.index)}\\listoverridecount0\\ls${String(entry.index)}}`);
166
259
  this.raw("}");
167
260
  }
261
+ writeRevisionTable() {
262
+ if (this.tables.revisionAuthors.size <= 1) return;
263
+ this.raw("{\\*\\revtbl");
264
+ for (const [author] of [...this.tables.revisionAuthors].sort((left, right) => left[1] - right[1])) this.raw(`{${escapeText(author)};}`);
265
+ this.raw("}");
266
+ }
168
267
  writeInfoGroup(document) {
169
268
  const { title, author, subject, keywords } = document.metadata;
170
269
  const fields = [];
@@ -174,9 +273,14 @@ var RtfWriter = class {
174
273
  if (keywords !== void 0 && keywords.length > 0) fields.push(`{\\keywords ${escapeText(keywords.join("; "))}}`);
175
274
  if (fields.length > 0) this.raw(`{\\info${fields.join("")}}`);
176
275
  }
276
+ writeDocumentGeometry(document) {
277
+ const first = document.sections[0];
278
+ if (first === void 0) return;
279
+ this.raw(`\\paperw${String(require_units.pointsToTwips(first.pageSize.widthPt))}\\paperh${String(require_units.pointsToTwips(first.pageSize.heightPt))}\\margl${String(require_units.pointsToTwips(first.margins.leftPt))}\\margr${String(require_units.pointsToTwips(first.margins.rightPt))}\\margt${String(require_units.pointsToTwips(first.margins.topPt))}\\margb${String(require_units.pointsToTwips(first.margins.bottomPt))}`);
280
+ }
177
281
  writeSection(section, isFirst) {
178
282
  if (!isFirst) this.line("\\sect");
179
- this.line(`\\sectd\\paperw${String(require_units.pointsToTwips(section.pageSize.widthPt))}\\paperh${String(require_units.pointsToTwips(section.pageSize.heightPt))}\\margl${String(require_units.pointsToTwips(section.margins.leftPt))}\\margr${String(require_units.pointsToTwips(section.margins.rightPt))}\\margt${String(require_units.pointsToTwips(section.margins.topPt))}\\margb${String(require_units.pointsToTwips(section.margins.bottomPt))}`);
283
+ this.line(`\\sectd${SECTION_BREAK_CONTROL_WORDS.get(section.breakType ?? "") ?? ""}\\pgwsxn${String(require_units.pointsToTwips(section.pageSize.widthPt))}\\pghsxn${String(require_units.pointsToTwips(section.pageSize.heightPt))}\\marglsxn${String(require_units.pointsToTwips(section.margins.leftPt))}\\margrsxn${String(require_units.pointsToTwips(section.margins.rightPt))}\\margtsxn${String(require_units.pointsToTwips(section.margins.topPt))}\\margbsxn${String(require_units.pointsToTwips(section.margins.bottomPt))}`);
180
284
  this.writeBlocks(section.blocks);
181
285
  }
182
286
  writeBlocks(blocks) {
@@ -204,24 +308,52 @@ var RtfWriter = class {
204
308
  });
205
309
  return;
206
310
  case "constructStart":
311
+ this.openConstruct(block.descriptor);
312
+ return;
207
313
  case "constructEnd":
208
- this.sink({
209
- code: require_diagnostics.RtfDiagnosticCodes.CONSTRUCT_UNREPRESENTED,
210
- severity: "warning",
211
- message: `a ${block.kind} boundary marker is dropped; this writer emits no RTF construct for the fidelity-construct vocabulary`
212
- });
314
+ this.closeConstruct();
213
315
  return;
214
316
  default: return;
215
317
  }
216
318
  }
319
+ openConstruct(descriptor) {
320
+ if (!require_constructs.isBookmarkAnchor(descriptor)) {
321
+ this.sink({
322
+ code: require_diagnostics.RtfDiagnosticCodes.CONSTRUCT_UNREPRESENTED,
323
+ severity: "warning",
324
+ message: `a ${descriptor.kind} construct is dropped: RTF has no ${describeConstructGap(descriptor)}`
325
+ });
326
+ this.openConstructs.push(void 0);
327
+ return;
328
+ }
329
+ this.openConstructs.push(descriptor.name);
330
+ this.line(bookmarkStartGroup(descriptor));
331
+ }
332
+ closeConstruct() {
333
+ const name = this.openConstructs.pop();
334
+ if (name !== void 0) this.line(`{\\*\\bkmkend ${escapeText(name)}}`);
335
+ }
217
336
  writeParagraph(paragraph, inTable) {
218
337
  this.raw("\\pard\\plain");
219
338
  if (inTable) this.raw("\\intbl");
220
339
  this.raw(this.paragraphProperties(paragraph));
221
340
  this.raw(" ");
222
- for (const run of paragraph.runs) this.writeRun(run);
341
+ const bookmarks = (paragraph.constructs ?? []).filter((extent) => require_constructs.isBookmarkAnchor(extent.descriptor));
342
+ const revisions = (paragraph.constructs ?? []).filter((extent) => extent.descriptor.kind === "provenance");
343
+ for (const [index, run] of paragraph.runs.entries()) {
344
+ this.writeRunBoundaries(bookmarks, index);
345
+ this.writeRun(run, revisionsCovering(revisions, index));
346
+ }
347
+ this.writeRunBoundaries(bookmarks, paragraph.runs.length);
223
348
  if (!inTable) this.line("\\par");
224
349
  }
350
+ writeRunBoundaries(extents, position) {
351
+ for (const extent of extents) if (extent.endRun === position && extent.startRun !== position) this.raw(`{\\*\\bkmkend ${escapeText(nameOf(extent.descriptor))}}`);
352
+ for (const extent of extents) if (extent.startRun === position) {
353
+ this.raw(bookmarkStartGroup(extent.descriptor));
354
+ if (extent.endRun === position) this.raw(`{\\*\\bkmkend ${escapeText(nameOf(extent.descriptor))}}`);
355
+ }
356
+ }
225
357
  paragraphProperties(paragraph) {
226
358
  let out = "";
227
359
  const level = paragraph.headingLevel === void 0 ? void 0 : (0, document_schema_js.clampHeadingLevel)(paragraph.headingLevel);
@@ -251,8 +383,8 @@ var RtfWriter = class {
251
383
  if (paragraph.pageBreakBefore === true) out += "\\pagebb";
252
384
  return out;
253
385
  }
254
- writeRun(run) {
255
- const properties = this.runProperties(run);
386
+ writeRun(run, revisions = []) {
387
+ const properties = this.runProperties(run) + this.revisionProperties(revisions);
256
388
  const body = `${properties}${properties.length > 0 ? " " : ""}${escapeText(run.text)}`;
257
389
  if (run.hyperlink === void 0) {
258
390
  this.raw(`{${body}}`);
@@ -260,6 +392,19 @@ var RtfWriter = class {
260
392
  }
261
393
  this.raw(`{\\field{\\*\\fldinst{HYPERLINK "${escapeText(run.hyperlink)}"}}{\\fldrslt{${body}}}}`);
262
394
  }
395
+ revisionProperties(revisions) {
396
+ let out = "";
397
+ for (const descriptor of revisions) {
398
+ const author = descriptor.author;
399
+ const authorIndex = author === void 0 ? void 0 : this.tables.revisionAuthors.get(author);
400
+ const dttm = descriptor.dateIso === void 0 ? void 0 : require_constructs.dttmFromIso(descriptor.dateIso);
401
+ const words = CHREV_CONTROL_WORDS[descriptor.change];
402
+ out += words.flag;
403
+ if (authorIndex !== void 0) out += `\\${words.author}${String(authorIndex)}`;
404
+ if (dttm !== void 0) out += `\\${words.date}${String(dttm)}`;
405
+ }
406
+ return out;
407
+ }
263
408
  runProperties(run) {
264
409
  let out = "";
265
410
  const fontIndex = run.fontFamily === void 0 ? void 0 : this.tables.fonts.get(run.fontFamily);
@@ -275,28 +420,51 @@ var RtfWriter = class {
275
420
  return out;
276
421
  }
277
422
  writeTable(table) {
278
- for (const row of table.rows) {
423
+ const covered = verticalMergeCoverage(table);
424
+ for (const [rowIndex, row] of table.rows.entries()) {
279
425
  let right = 0;
280
- const boundaries = [];
281
- for (let column = 0; column < row.cells.length; column += 1) {
282
- right += require_units.pointsToTwips(table.columnWidthsPt[column] ?? 0);
283
- boundaries.push(right);
426
+ let column = 0;
427
+ const definitions = [];
428
+ const marks = [];
429
+ for (const [cellIndex, cell] of row.cells.entries()) {
430
+ const colSpan = cell.colSpan ?? 1;
431
+ for (let offset = 0; offset < colSpan; offset += 1) {
432
+ right += require_units.pointsToTwips(table.columnWidthsPt[column] ?? 0);
433
+ column += 1;
434
+ definitions.push(this.cellDefinition(cell, covered[rowIndex]?.[cellIndex] === true, colSpan > 1 ? offset === 0 ? "mergeFirst" : "mergeContinuation" : "single", right));
435
+ marks.push({
436
+ cell,
437
+ empty: offset > 0
438
+ });
439
+ }
284
440
  }
285
- const rowDefinition = `\\trowd\\trgaph108\\trleft0${boundaries.map((boundary) => `\\cellx${String(boundary)}`).join("")}`;
441
+ const rowDefinition = `\\trowd\\trgaph108\\trleft0${definitions.join("")}`;
286
442
  this.line(rowDefinition);
287
- for (const cell of row.cells) {
288
- if (cell.borders !== void 0 || cell.background !== void 0) this.sink({
289
- code: require_diagnostics.RtfDiagnosticCodes.CELL_BORDER_DROPPED,
290
- severity: "info",
291
- message: "a table cell's borders or background are dropped; this writer emits cell boundaries only"
292
- });
293
- this.writeCellBlocks(cell.blocks);
443
+ for (const mark of marks) {
444
+ this.writeCellBlocks(mark.empty ? [] : mark.cell.blocks);
294
445
  this.raw("\\cell");
295
446
  }
296
447
  this.line(`${rowDefinition}\\row`);
297
448
  }
298
449
  this.line("\\pard");
299
450
  }
451
+ cellDefinition(cell, isVerticalContinuation, horizontal, rightTwips) {
452
+ let out = "";
453
+ if (isVerticalContinuation) out += "\\clvmrg";
454
+ else if ((cell.rowSpan ?? 1) > 1) out += "\\clvmgf";
455
+ if (horizontal === "mergeFirst") out += "\\clmgf";
456
+ else if (horizontal === "mergeContinuation") return `${out}\\clmrg\\cellx${String(rightTwips)}`;
457
+ const borders = cell.borders;
458
+ if (borders !== void 0) for (const side of CELL_BORDER_ORDER) {
459
+ const border = borders[side];
460
+ if (border !== void 0) out += require_cell_format.borderControlWords(side, border, colorIndexOf(border.color, this.tables.colors));
461
+ }
462
+ if (cell.background !== void 0) {
463
+ const index = colorIndexOf(cell.background, this.tables.colors);
464
+ if (index !== void 0) out += `\\clcbpat${String(index)}`;
465
+ }
466
+ return `${out}\\cellx${String(rightTwips)}`;
467
+ }
300
468
  writeCellBlocks(blocks) {
301
469
  const paragraphs = blocks.filter((block) => block.kind === "paragraph");
302
470
  if (paragraphs.length === 0) {