@usejunior/docx-core 0.19.1 → 0.21.2

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 (180) hide show
  1. package/README.md +12 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/cli/conformance-adapter.d.ts.map +1 -1
  4. package/dist/cli/conformance-adapter.js +0 -27
  5. package/dist/cli/conformance-adapter.js.map +1 -1
  6. package/dist/core-types.d.ts +1 -139
  7. package/dist/core-types.d.ts.map +1 -1
  8. package/dist/core-types.js +1 -3
  9. package/dist/core-types.js.map +1 -1
  10. package/dist/generation/compile.d.ts.map +1 -1
  11. package/dist/generation/compile.js +1 -3
  12. package/dist/generation/compile.js.map +1 -1
  13. package/dist/index.d.ts +5 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +13 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/integration/generation-probes.d.ts +36 -2
  18. package/dist/integration/generation-probes.d.ts.map +1 -1
  19. package/dist/integration/generation-probes.js +137 -15
  20. package/dist/integration/generation-probes.js.map +1 -1
  21. package/dist/integration/libreoffice-oracle.d.ts +3 -1
  22. package/dist/integration/libreoffice-oracle.d.ts.map +1 -1
  23. package/dist/integration/libreoffice-oracle.js +10 -4
  24. package/dist/integration/libreoffice-oracle.js.map +1 -1
  25. package/dist/integration/synthetic-docx-fixture.d.ts.map +1 -1
  26. package/dist/integration/synthetic-docx-fixture.js +8 -0
  27. package/dist/integration/synthetic-docx-fixture.js.map +1 -1
  28. package/dist/primitives/accept_ai_edits.js +14 -2
  29. package/dist/primitives/accept_ai_edits.js.map +1 -1
  30. package/dist/primitives/accept_changes.d.ts +6 -0
  31. package/dist/primitives/accept_changes.d.ts.map +1 -1
  32. package/dist/primitives/accept_changes.js +236 -62
  33. package/dist/primitives/accept_changes.js.map +1 -1
  34. package/dist/primitives/bookmarks.d.ts +8 -2
  35. package/dist/primitives/bookmarks.d.ts.map +1 -1
  36. package/dist/primitives/bookmarks.js +31 -7
  37. package/dist/primitives/bookmarks.js.map +1 -1
  38. package/dist/primitives/comments.d.ts +101 -5
  39. package/dist/primitives/comments.d.ts.map +1 -1
  40. package/dist/primitives/comments.js +356 -156
  41. package/dist/primitives/comments.js.map +1 -1
  42. package/dist/primitives/conformance.d.ts +42 -0
  43. package/dist/primitives/conformance.d.ts.map +1 -0
  44. package/dist/primitives/conformance.js +61 -0
  45. package/dist/primitives/conformance.js.map +1 -0
  46. package/dist/primitives/document.d.ts +145 -2
  47. package/dist/primitives/document.d.ts.map +1 -1
  48. package/dist/primitives/document.js +505 -27
  49. package/dist/primitives/document.js.map +1 -1
  50. package/dist/primitives/document_view-headings.d.ts +3 -1
  51. package/dist/primitives/document_view-headings.d.ts.map +1 -1
  52. package/dist/primitives/document_view-headings.js +5 -5
  53. package/dist/primitives/document_view-headings.js.map +1 -1
  54. package/dist/primitives/document_view.d.ts +2 -0
  55. package/dist/primitives/document_view.d.ts.map +1 -1
  56. package/dist/primitives/document_view.js +18 -4
  57. package/dist/primitives/document_view.js.map +1 -1
  58. package/dist/primitives/errors.d.ts +3 -2
  59. package/dist/primitives/errors.d.ts.map +1 -1
  60. package/dist/primitives/errors.js +3 -1
  61. package/dist/primitives/errors.js.map +1 -1
  62. package/dist/primitives/extract_revisions.d.ts +23 -6
  63. package/dist/primitives/extract_revisions.d.ts.map +1 -1
  64. package/dist/primitives/extract_revisions.js +208 -38
  65. package/dist/primitives/extract_revisions.js.map +1 -1
  66. package/dist/primitives/field_evaluation.d.ts +70 -0
  67. package/dist/primitives/field_evaluation.d.ts.map +1 -0
  68. package/dist/primitives/field_evaluation.js +462 -0
  69. package/dist/primitives/field_evaluation.js.map +1 -0
  70. package/dist/primitives/footnotes.d.ts +53 -3
  71. package/dist/primitives/footnotes.d.ts.map +1 -1
  72. package/dist/primitives/footnotes.js +228 -83
  73. package/dist/primitives/footnotes.js.map +1 -1
  74. package/dist/primitives/index.d.ts +12 -1
  75. package/dist/primitives/index.d.ts.map +1 -1
  76. package/dist/primitives/index.js +12 -1
  77. package/dist/primitives/index.js.map +1 -1
  78. package/dist/primitives/merge_runs.d.ts +3 -1
  79. package/dist/primitives/merge_runs.d.ts.map +1 -1
  80. package/dist/primitives/merge_runs.js +12 -2
  81. package/dist/primitives/merge_runs.js.map +1 -1
  82. package/dist/primitives/namespaces.d.ts +10 -2
  83. package/dist/primitives/namespaces.d.ts.map +1 -1
  84. package/dist/primitives/namespaces.js +11 -2
  85. package/dist/primitives/namespaces.js.map +1 -1
  86. package/dist/primitives/note_conversion.d.ts +11 -0
  87. package/dist/primitives/note_conversion.d.ts.map +1 -0
  88. package/dist/primitives/note_conversion.js +12 -0
  89. package/dist/primitives/note_conversion.js.map +1 -0
  90. package/dist/primitives/paragraph-index.d.ts +31 -0
  91. package/dist/primitives/paragraph-index.d.ts.map +1 -0
  92. package/dist/primitives/paragraph-index.js +140 -0
  93. package/dist/primitives/paragraph-index.js.map +1 -0
  94. package/dist/primitives/paragraph_merge_formatting.d.ts +24 -0
  95. package/dist/primitives/paragraph_merge_formatting.d.ts.map +1 -0
  96. package/dist/primitives/paragraph_merge_formatting.js +86 -0
  97. package/dist/primitives/paragraph_merge_formatting.js.map +1 -0
  98. package/dist/primitives/paragraph_numbering.d.ts +40 -0
  99. package/dist/primitives/paragraph_numbering.d.ts.map +1 -0
  100. package/dist/primitives/paragraph_numbering.js +198 -0
  101. package/dist/primitives/paragraph_numbering.js.map +1 -0
  102. package/dist/primitives/paragraph_structure.d.ts +19 -0
  103. package/dist/primitives/paragraph_structure.d.ts.map +1 -0
  104. package/dist/primitives/paragraph_structure.js +66 -0
  105. package/dist/primitives/paragraph_structure.js.map +1 -0
  106. package/dist/primitives/reject_changes.d.ts +6 -0
  107. package/dist/primitives/reject_changes.d.ts.map +1 -1
  108. package/dist/primitives/reject_changes.js +278 -68
  109. package/dist/primitives/reject_changes.js.map +1 -1
  110. package/dist/primitives/relationships.d.ts +22 -0
  111. package/dist/primitives/relationships.d.ts.map +1 -1
  112. package/dist/primitives/relationships.js +87 -0
  113. package/dist/primitives/relationships.js.map +1 -1
  114. package/dist/primitives/revision-parts.d.ts +15 -0
  115. package/dist/primitives/revision-parts.d.ts.map +1 -1
  116. package/dist/primitives/revision-parts.js +33 -1
  117. package/dist/primitives/revision-parts.js.map +1 -1
  118. package/dist/primitives/sectPrAudit.d.ts.map +1 -1
  119. package/dist/primitives/sectPrAudit.js +10 -1
  120. package/dist/primitives/sectPrAudit.js.map +1 -1
  121. package/dist/primitives/sections.d.ts +135 -0
  122. package/dist/primitives/sections.d.ts.map +1 -0
  123. package/dist/primitives/sections.js +644 -0
  124. package/dist/primitives/sections.js.map +1 -0
  125. package/dist/primitives/styles.d.ts +61 -0
  126. package/dist/primitives/styles.d.ts.map +1 -1
  127. package/dist/primitives/styles.js +207 -25
  128. package/dist/primitives/styles.js.map +1 -1
  129. package/dist/primitives/symbol_run_content.d.ts +42 -0
  130. package/dist/primitives/symbol_run_content.d.ts.map +1 -0
  131. package/dist/primitives/symbol_run_content.js +79 -0
  132. package/dist/primitives/symbol_run_content.js.map +1 -0
  133. package/dist/primitives/table_cells.d.ts +49 -0
  134. package/dist/primitives/table_cells.d.ts.map +1 -0
  135. package/dist/primitives/table_cells.js +149 -0
  136. package/dist/primitives/table_cells.js.map +1 -0
  137. package/dist/primitives/table_columns.d.ts +50 -0
  138. package/dist/primitives/table_columns.d.ts.map +1 -0
  139. package/dist/primitives/table_columns.js +138 -0
  140. package/dist/primitives/table_columns.js.map +1 -0
  141. package/dist/primitives/table_edit_common.d.ts +39 -0
  142. package/dist/primitives/table_edit_common.d.ts.map +1 -0
  143. package/dist/primitives/table_edit_common.js +163 -0
  144. package/dist/primitives/table_edit_common.js.map +1 -0
  145. package/dist/primitives/table_occupancy.d.ts +39 -0
  146. package/dist/primitives/table_occupancy.d.ts.map +1 -0
  147. package/dist/primitives/table_occupancy.js +145 -0
  148. package/dist/primitives/table_occupancy.js.map +1 -0
  149. package/dist/primitives/table_rows.d.ts +56 -0
  150. package/dist/primitives/table_rows.d.ts.map +1 -0
  151. package/dist/primitives/table_rows.js +390 -0
  152. package/dist/primitives/table_rows.js.map +1 -0
  153. package/dist/primitives/text.d.ts +69 -1
  154. package/dist/primitives/text.d.ts.map +1 -1
  155. package/dist/primitives/text.js +592 -102
  156. package/dist/primitives/text.js.map +1 -1
  157. package/dist/primitives/track-changes-emitter.d.ts +23 -0
  158. package/dist/primitives/track-changes-emitter.d.ts.map +1 -1
  159. package/dist/primitives/track-changes-emitter.js +64 -1
  160. package/dist/primitives/track-changes-emitter.js.map +1 -1
  161. package/dist/primitives/validate_document.d.ts.map +1 -1
  162. package/dist/primitives/validate_document.js +22 -1
  163. package/dist/primitives/validate_document.js.map +1 -1
  164. package/dist/primitives/zip.d.ts +22 -1
  165. package/dist/primitives/zip.d.ts.map +1 -1
  166. package/dist/primitives/zip.js +47 -8
  167. package/dist/primitives/zip.js.map +1 -1
  168. package/dist/shared/docx/DocxArchive.d.ts +6 -2
  169. package/dist/shared/docx/DocxArchive.d.ts.map +1 -1
  170. package/dist/shared/docx/DocxArchive.js +17 -4
  171. package/dist/shared/docx/DocxArchive.js.map +1 -1
  172. package/dist/shared/field-semantics.d.ts +26 -0
  173. package/dist/shared/field-semantics.d.ts.map +1 -0
  174. package/dist/shared/field-semantics.js +232 -0
  175. package/dist/shared/field-semantics.js.map +1 -0
  176. package/dist/shared/field-structure.d.ts +10 -1
  177. package/dist/shared/field-structure.d.ts.map +1 -1
  178. package/dist/shared/field-structure.js +31 -12
  179. package/dist/shared/field-structure.js.map +1 -1
  180. package/package.json +4 -5
@@ -4,12 +4,20 @@
4
4
  * Creates comment XML parts when missing, inserts comment range markers,
5
5
  * and supports threaded replies via commentsExtended.xml.
6
6
  */
7
+ import { createHash } from 'node:crypto';
7
8
  import { OOXML, W } from './namespaces.js';
8
9
  import { parseXml, serializeXml } from './xml.js';
10
+ import { DocxZip } from './zip.js';
9
11
  import { getParagraphRuns, getParagraphText, splitRunAtVisibleOffset } from './text.js';
10
12
  import { getParagraphBookmarkId } from './bookmarks.js';
11
- import { childElements, getLeafText, isW } from './dom-helpers.js';
13
+ import { isW } from './dom-helpers.js';
14
+ import { buildParagraphIndex } from './paragraph-index.js';
12
15
  import { getAttributeSafe } from './xml-helpers.js';
16
+ import { getFirstChild } from './xml-helpers.js';
17
+ import { extractEffectiveRunFormatting, parseStylesXml, parseThemeXml } from './styles.js';
18
+ import { emitFormattingTags, mergeAdjacentTags } from './formatting_tags.js';
19
+ import { ensureExternalHyperlinkRelationships } from './relationships.js';
20
+ import { SafeDocxError } from './errors.js';
13
21
  import { createRevisionContainer, prepareElementForDeletion, } from './track-changes-emitter.js';
14
22
  // ── Relationship types ──────────────────────────────────────────────────
15
23
  const REL_TYPE_COMMENTS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments';
@@ -146,6 +154,86 @@ async function ensureRelationships(zip, newParts) {
146
154
  }
147
155
  zip.writeText(relsPath, serializeXml(relsDoc));
148
156
  }
157
+ function commentBodyDestinations(body) {
158
+ return body?.flatMap((paragraph) => paragraph.runs.flatMap((run) => run.hyperlink ? [run.hyperlink.destination] : [])) ?? [];
159
+ }
160
+ function deterministicParaId(params, commentId) {
161
+ return createHash('sha256')
162
+ .update(`${commentId}\0${params.author}\0${params.initials}\0${params.date}\0${params.text}`)
163
+ .digest('hex')
164
+ .slice(0, 8)
165
+ .toUpperCase();
166
+ }
167
+ function findUniqueRevision(documentXml, locator) {
168
+ const matches = Array.from(documentXml.getElementsByTagNameNS(OOXML.W_NS, locator.type))
169
+ .filter((element) => getAttributeSafe(element, OOXML.W_NS, 'id', 'w', { bareFallback: false }) === locator.id);
170
+ if (matches.length !== 1) {
171
+ throw new Error(`Tracked revision ${locator.type}#${locator.id} must occur exactly once; found ${matches.length}.`);
172
+ }
173
+ return matches[0];
174
+ }
175
+ function createCommentReference(documentXml, commentId) {
176
+ const refRun = documentXml.createElementNS(OOXML.W_NS, 'w:r');
177
+ const rPr = documentXml.createElementNS(OOXML.W_NS, 'w:rPr');
178
+ const rStyle = documentXml.createElementNS(OOXML.W_NS, 'w:rStyle');
179
+ rStyle.setAttribute('w:val', 'CommentReference');
180
+ rPr.appendChild(rStyle);
181
+ refRun.appendChild(rPr);
182
+ const reference = documentXml.createElementNS(OOXML.W_NS, 'w:commentReference');
183
+ reference.setAttribute('w:id', String(commentId));
184
+ refRun.appendChild(reference);
185
+ return refRun;
186
+ }
187
+ /**
188
+ * Materialize root comments around an OOXML tracked-revision range. Markers
189
+ * are placed immediately outside the identified revision containers so
190
+ * accept/reject keeps a balanced annotation and naturally collapses it when
191
+ * that revision's text is removed.
192
+ *
193
+ * @conformance ECMA-376 edition 5, Part 1 § 17.13.4.2
194
+ * @see https://github.com/UseJunior/safe-docx/issues/860
195
+ */
196
+ export async function addTrackedRangeComments(buffer, comments) {
197
+ if (comments.length === 0)
198
+ return buffer;
199
+ const zip = await DocxZip.load(buffer);
200
+ await bootstrapCommentParts(zip);
201
+ const documentXml = parseXml(await zip.readText('word/document.xml'));
202
+ const commentsDoc = parseXml(await zip.readText('word/comments.xml'));
203
+ for (const params of comments) {
204
+ const startRevision = findUniqueRevision(documentXml, params.startRevision);
205
+ const endRevision = findUniqueRevision(documentXml, params.endRevision);
206
+ const startParent = startRevision.parentNode;
207
+ const endParent = endRevision.parentNode;
208
+ if (!startParent || !endParent)
209
+ throw new Error('Attributed revision container has no parent.');
210
+ for (const parent of [startParent, endParent]) {
211
+ if (parent.nodeType === 1 && ['trPr', 'rPr'].includes(parent.localName)) {
212
+ throw new Error('Tracked range comments cannot be anchored inside a property container.');
213
+ }
214
+ }
215
+ const commentId = allocateNextCommentId(commentsDoc);
216
+ const rangeStart = documentXml.createElementNS(OOXML.W_NS, 'w:commentRangeStart');
217
+ rangeStart.setAttribute('w:id', String(commentId));
218
+ const rangeEnd = documentXml.createElementNS(OOXML.W_NS, 'w:commentRangeEnd');
219
+ rangeEnd.setAttribute('w:id', String(commentId));
220
+ startParent.insertBefore(rangeStart, startRevision);
221
+ endParent.insertBefore(rangeEnd, endRevision.nextSibling);
222
+ endParent.insertBefore(createCommentReference(documentXml, commentId), rangeEnd.nextSibling);
223
+ addCommentElement(commentsDoc, {
224
+ id: commentId,
225
+ author: params.author,
226
+ initials: params.initials,
227
+ text: params.text,
228
+ paraId: deterministicParaId(params, commentId),
229
+ date: params.date,
230
+ });
231
+ await ensureAuthorInPeople(zip, params.author);
232
+ }
233
+ zip.writeText('word/document.xml', serializeXml(documentXml));
234
+ zip.writeText('word/comments.xml', serializeXml(commentsDoc));
235
+ return zip.toBuffer();
236
+ }
149
237
  /**
150
238
  * Insert a root comment anchored to a text range within a paragraph.
151
239
  *
@@ -154,6 +242,10 @@ async function ensureRelationships(zip, newParts) {
154
242
  * - Inserts commentReference run after range end
155
243
  * - Adds comment entry to comments.xml
156
244
  * - Adds author to people.xml if not present
245
+ *
246
+ * @conformance ECMA-376 edition 5, Part 1 § 17.13.4.4
247
+ * @conformance ECMA-376 edition 5, Part 1 § 17.13.4.3
248
+ * @conformance ECMA-376 edition 5, Part 1 § 17.13.4.5
157
249
  */
158
250
  export async function addComment(documentXml, zip, params, ctx) {
159
251
  const { paragraphEl, author, text, initials } = params;
@@ -175,28 +267,44 @@ export async function addComment(documentXml, zip, params, ctx) {
175
267
  insertCommentMarkers(documentXml, paragraphEl, commentId, start, end, ctx);
176
268
  // Add comment element to comments.xml
177
269
  const paraId = generateParaId();
270
+ const hyperlinkRelationshipIds = await ensureExternalHyperlinkRelationships(zip, 'word/comments.xml', commentBodyDestinations(params.body));
178
271
  addCommentElement(commentsDoc, {
179
272
  id: commentId,
180
273
  author,
181
274
  initials: initials ?? author.charAt(0).toUpperCase(),
182
275
  text,
183
276
  paraId,
277
+ date: resolveDefinitionDate(params.date, ctx),
278
+ body: params.body,
279
+ hyperlinkRelationshipIds,
184
280
  });
185
281
  zip.writeText('word/comments.xml', serializeXml(commentsDoc));
186
282
  // Add author to people.xml
187
283
  await ensureAuthorInPeople(zip, author);
188
284
  return { commentId };
189
285
  }
286
+ /**
287
+ * Resolve the `w:date` a new comment definition should carry. An explicit
288
+ * `null` from the caller means "no `w:date`" and wins over `ctx.date`, exactly
289
+ * as an explicit string does; `undefined` defers to the revision context and
290
+ * then to the process clock inside `addCommentElement` (#961, #1103).
291
+ */
292
+ function resolveDefinitionDate(date, ctx) {
293
+ return date === null ? null : (date ?? ctx?.date);
294
+ }
190
295
  /**
191
296
  * Add a threaded reply to an existing comment.
192
297
  *
193
298
  * Replies don't have range markers in the document body.
194
299
  * Thread linkage is stored in commentsExtended.xml via paraIdParent.
195
- * `ctx` is accepted for API consistency with other comment mutations, but this
196
- * primitive only updates comment side parts for replies, so no body revision
197
- * markup is emitted here.
300
+ * Replies emit no body revision markup (there is nothing to anchor), but the
301
+ * reply's comment definition still claims creation metadata — so a
302
+ * caller-supplied `params.date` (or, failing that, `ctx.date`) stamps
303
+ * `w:date` exactly as it does for root comments, keeping reply timestamps
304
+ * deterministic alongside the rest of the operation. Author and initials
305
+ * intentionally stay sourced from `params`.
198
306
  */
199
- export async function addCommentReply(_documentXml, zip, params, _ctx) {
307
+ export async function addCommentReply(_documentXml, zip, params, ctx) {
200
308
  const { parentCommentId, author, text, initials } = params;
201
309
  // Load comments.xml
202
310
  const commentsXml = await zip.readText('word/comments.xml');
@@ -209,12 +317,16 @@ export async function addCommentReply(_documentXml, zip, params, _ctx) {
209
317
  // Allocate ID and add reply comment
210
318
  const commentId = allocateNextCommentId(commentsDoc);
211
319
  const replyParaId = generateParaId();
320
+ const hyperlinkRelationshipIds = await ensureExternalHyperlinkRelationships(zip, 'word/comments.xml', commentBodyDestinations(params.body));
212
321
  addCommentElement(commentsDoc, {
213
322
  id: commentId,
214
323
  author,
215
324
  initials: initials ?? author.charAt(0).toUpperCase(),
216
325
  text,
217
326
  paraId: replyParaId,
327
+ date: resolveDefinitionDate(params.date, ctx),
328
+ body: params.body,
329
+ hyperlinkRelationshipIds,
218
330
  });
219
331
  zip.writeText('word/comments.xml', serializeXml(commentsDoc));
220
332
  // Link reply in commentsExtended.xml
@@ -225,6 +337,66 @@ export async function addCommentReply(_documentXml, zip, params, _ctx) {
225
337
  await ensureAuthorInPeople(zip, author);
226
338
  return { commentId, parentCommentId };
227
339
  }
340
+ /**
341
+ * Replace only the editable body payload of an existing comment definition.
342
+ * Comment identity, authorship metadata, threading metadata, and every anchor
343
+ * in document.xml remain untouched. The admitted topology intentionally
344
+ * matches the comment-body subset exposed by getComments(); unfamiliar direct
345
+ * children fail closed before comments.xml is mutated.
346
+ *
347
+ * @conformance ECMA-376 edition 5, Part 1 § 17.13.4.2
348
+ * @see https://github.com/UseJunior/safe-docx/issues/960
349
+ */
350
+ export async function updateCommentBody(zip, params) {
351
+ const commentsText = await zip.readTextOrNull('word/comments.xml');
352
+ if (!commentsText)
353
+ throw new Error(`Comment ID ${params.commentId} not found`);
354
+ const commentsDoc = parseXml(commentsText);
355
+ const commentEl = findCommentElementById(commentsDoc, params.commentId);
356
+ if (!commentEl)
357
+ throw new Error(`Comment ID ${params.commentId} not found`);
358
+ const paragraphs = Array.from(commentEl.childNodes)
359
+ .filter((node) => node.nodeType === 1);
360
+ if (paragraphs.length === 0 || paragraphs.some((element) => !isW(element, W.p))) {
361
+ throw new SafeDocxError('UNSUPPORTED_EDIT', `Comment ID ${params.commentId} has unsupported body topology`, 'Only direct w:p comment-body children can be updated in place.');
362
+ }
363
+ const first = paragraphs[0];
364
+ const markerRuns = paragraphs.flatMap((paragraph) => Array.from(paragraph.childNodes)
365
+ .filter((node) => node.nodeType === 1 && isW(node, W.r))
366
+ .filter((run) => run.getElementsByTagNameNS(OOXML.W_NS, W.annotationRef).length > 0));
367
+ const annotationReferenceCount = paragraphs.reduce((count, paragraph) => count + paragraph.getElementsByTagNameNS(OOXML.W_NS, W.annotationRef).length, 0);
368
+ if (annotationReferenceCount > 1 || (markerRuns.length === 1 && markerRuns[0]?.parentNode !== first)) {
369
+ throw new SafeDocxError('UNSUPPORTED_EDIT', `Comment ID ${params.commentId} has unsupported annotation-reference topology`, 'At most one annotation-reference run is admitted, and it must be in the first comment paragraph.');
370
+ }
371
+ for (const paragraph of paragraphs) {
372
+ const children = Array.from(paragraph.childNodes).filter((node) => node.nodeType === 1);
373
+ const unsupported = children.find((child) => !isW(child, W.pPr)
374
+ && !isW(child, W.r)
375
+ && !isW(child, W.hyperlink));
376
+ if (unsupported) {
377
+ throw new SafeDocxError('UNSUPPORTED_EDIT', `Comment ID ${params.commentId} contains unsupported ${unsupported.nodeName}`, 'The existing comment body was left unchanged.');
378
+ }
379
+ }
380
+ const body = params.body ?? [{ runs: [{ text: params.text }] }];
381
+ const hyperlinkRelationshipIds = await ensureExternalHyperlinkRelationships(zip, 'word/comments.xml', commentBodyDestinations(body));
382
+ for (let index = 0; index < body.length; index++) {
383
+ const paragraph = paragraphs[index] ?? commentsDoc.createElementNS(OOXML.W_NS, 'w:p');
384
+ if (!paragraph.parentNode)
385
+ commentEl.appendChild(paragraph);
386
+ for (const child of Array.from(paragraph.childNodes)) {
387
+ if (child.nodeType !== 1)
388
+ continue;
389
+ const element = child;
390
+ if (isW(element, W.pPr) || (index === 0 && element === markerRuns[0]))
391
+ continue;
392
+ paragraph.removeChild(element);
393
+ }
394
+ appendCommentBodyRuns(paragraph, body[index].runs, commentsDoc, hyperlinkRelationshipIds);
395
+ }
396
+ for (const paragraph of paragraphs.slice(body.length))
397
+ commentEl.removeChild(paragraph);
398
+ zip.writeText('word/comments.xml', serializeXml(commentsDoc));
399
+ }
228
400
  // ── Internal helpers ────────────────────────────────────────────────────
229
401
  function allocateNextCommentId(commentsDoc) {
230
402
  const commentEls = commentsDoc.getElementsByTagNameNS(OOXML.W_NS, W.comment);
@@ -278,13 +450,39 @@ function ensureCommentPartNamespaceAliases(commentsDoc) {
278
450
  root.setAttributeNS(XMLNS_NS, 'xmlns:w15', OOXML.W15_NS);
279
451
  }
280
452
  }
453
+ /**
454
+ * Append a `w:comment` definition to the comments part.
455
+ *
456
+ * @conformance ECMA-376 edition 5, Part 1 § 17.13.4.2
457
+ * The `w:date` creation stamp uses the caller-supplied date when one is
458
+ * provided — an explicit `AddCommentParams.date`, or else the revision
459
+ * context's date — so the comment definition and any body revision markup
460
+ * emitted for the same operation agree on the calendar date Word displays,
461
+ * even across a UTC/local day boundary. Only when the caller supplies no date
462
+ * does the process clock remain the default. An explicit `null` writes no
463
+ * `w:date` attribute at all, which is how a source comment that never carried
464
+ * one is re-emitted without gaining the compile time (#1103); `w:date` is
465
+ * optional on `w:comment`. Author and initials always come
466
+ * from `AddCommentParams` / `AddCommentReplyParams`, never from
467
+ * `RevisionContext`: the comment's attribution is the commenting author, which
468
+ * is allowed to differ from the tracked-change author wrapping the reference
469
+ * run. A date alone never produces revision markup; only a `RevisionContext`
470
+ * does.
471
+ * @see #859
472
+ * @see #961
473
+ * @see #1103
474
+ */
281
475
  function addCommentElement(commentsDoc, params) {
282
476
  ensureCommentPartNamespaceAliases(commentsDoc);
283
477
  const root = commentsDoc.documentElement;
478
+ if (params.hyperlinkRelationshipIds?.size && root.lookupNamespaceURI('r') !== OOXML.R_NS) {
479
+ root.setAttributeNS(XMLNS_NS, 'xmlns:r', OOXML.R_NS);
480
+ }
284
481
  const commentEl = commentsDoc.createElementNS(OOXML.W_NS, 'w:comment');
285
482
  commentEl.setAttribute('w:id', String(params.id));
286
483
  commentEl.setAttribute('w:author', params.author);
287
- commentEl.setAttribute('w:date', isoNow());
484
+ if (params.date !== null)
485
+ commentEl.setAttribute('w:date', params.date ?? isoNow());
288
486
  commentEl.setAttribute('w:initials', params.initials);
289
487
  // Comment body: <w:p w14:paraId="..."><w:pPr><w:pStyle w:val="CommentText"/></w:pPr><w:r><w:annotationRef/></w:r><w:r><w:t>text</w:t></w:r></w:p>
290
488
  const p = commentsDoc.createElementNS(OOXML.W_NS, 'w:p');
@@ -296,18 +494,88 @@ function addCommentElement(commentsDoc, params) {
296
494
  const annotRef = commentsDoc.createElementNS(OOXML.W_NS, 'w:annotationRef');
297
495
  refRun.appendChild(annotRef);
298
496
  p.appendChild(refRun);
299
- // Text run
300
- const textRun = commentsDoc.createElementNS(OOXML.W_NS, 'w:r');
301
- const t = commentsDoc.createElementNS(OOXML.W_NS, 'w:t');
302
- if (params.text.startsWith(' ') || params.text.endsWith(' ')) {
303
- t.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
304
- }
305
- t.appendChild(commentsDoc.createTextNode(params.text));
306
- textRun.appendChild(t);
307
- p.appendChild(textRun);
497
+ const body = params.body ?? [{ runs: [{ text: params.text }] }];
498
+ appendCommentBodyRuns(p, body[0]?.runs ?? [], commentsDoc, params.hyperlinkRelationshipIds);
308
499
  commentEl.appendChild(p);
500
+ for (const paragraph of body.slice(1)) {
501
+ const bodyParagraph = commentsDoc.createElementNS(OOXML.W_NS, 'w:p');
502
+ appendCommentBodyRuns(bodyParagraph, paragraph.runs, commentsDoc, params.hyperlinkRelationshipIds);
503
+ commentEl.appendChild(bodyParagraph);
504
+ }
309
505
  root.appendChild(commentEl);
310
506
  }
507
+ /**
508
+ * Emit annotation runs while retaining external hyperlink boundaries.
509
+ *
510
+ * @conformance ECMA-376 edition 5, Part 1 § 17.16.22
511
+ * @see #956
512
+ */
513
+ function appendCommentBodyRuns(paragraph, runs, doc, relationshipIds) {
514
+ let activeDestination;
515
+ let activeHyperlink;
516
+ for (const bodyRun of runs) {
517
+ const destination = bodyRun.hyperlink?.destination;
518
+ if (!destination) {
519
+ activeDestination = undefined;
520
+ activeHyperlink = undefined;
521
+ paragraph.appendChild(buildCommentBodyRun(doc, bodyRun));
522
+ continue;
523
+ }
524
+ const relationshipId = relationshipIds?.get(destination);
525
+ if (!relationshipId)
526
+ throw new Error(`Missing comment hyperlink relationship for ${destination}`);
527
+ if (activeDestination !== destination || !activeHyperlink) {
528
+ activeDestination = destination;
529
+ activeHyperlink = doc.createElementNS(OOXML.W_NS, 'w:hyperlink');
530
+ activeHyperlink.setAttributeNS(OOXML.R_NS, 'r:id', relationshipId);
531
+ paragraph.appendChild(activeHyperlink);
532
+ }
533
+ activeHyperlink.appendChild(buildCommentBodyRun(doc, bodyRun));
534
+ }
535
+ }
536
+ function buildCommentBodyRun(doc, bodyRun) {
537
+ const run = doc.createElementNS(OOXML.W_NS, 'w:r');
538
+ const style = bodyRun.style;
539
+ if (style && Object.values(style).some((value) => value !== undefined && value !== false)) {
540
+ const rPr = doc.createElementNS(OOXML.W_NS, 'w:rPr');
541
+ if (style.styleId) {
542
+ const rStyle = doc.createElementNS(OOXML.W_NS, 'w:rStyle');
543
+ rStyle.setAttributeNS(OOXML.W_NS, 'w:val', style.styleId);
544
+ rPr.appendChild(rStyle);
545
+ }
546
+ if (style.fontSizeHalfPoints !== undefined) {
547
+ const size = doc.createElementNS(OOXML.W_NS, 'w:sz');
548
+ size.setAttributeNS(OOXML.W_NS, 'w:val', String(style.fontSizeHalfPoints));
549
+ rPr.appendChild(size);
550
+ }
551
+ if (style.bold)
552
+ rPr.appendChild(doc.createElementNS(OOXML.W_NS, 'w:b'));
553
+ if (style.italic)
554
+ rPr.appendChild(doc.createElementNS(OOXML.W_NS, 'w:i'));
555
+ if (style.underline) {
556
+ const underline = doc.createElementNS(OOXML.W_NS, 'w:u');
557
+ underline.setAttributeNS(OOXML.W_NS, 'w:val', 'single');
558
+ rPr.appendChild(underline);
559
+ }
560
+ if (style.color) {
561
+ const color = doc.createElementNS(OOXML.W_NS, 'w:color');
562
+ color.setAttributeNS(OOXML.W_NS, 'w:val', style.color);
563
+ rPr.appendChild(color);
564
+ }
565
+ if (style.highlight && style.highlight !== 'none') {
566
+ const highlight = doc.createElementNS(OOXML.W_NS, 'w:highlight');
567
+ highlight.setAttributeNS(OOXML.W_NS, 'w:val', style.highlight);
568
+ rPr.appendChild(highlight);
569
+ }
570
+ run.appendChild(rPr);
571
+ }
572
+ const text = doc.createElementNS(OOXML.W_NS, 'w:t');
573
+ if (bodyRun.text.startsWith(' ') || bodyRun.text.endsWith(' '))
574
+ text.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
575
+ text.appendChild(doc.createTextNode(bodyRun.text));
576
+ run.appendChild(text);
577
+ return run;
578
+ }
311
579
  function insertCommentMarkers(documentXml, paragraphEl, commentId, start, end, ctx) {
312
580
  const runs = getParagraphRuns(paragraphEl);
313
581
  const rangeStart = documentXml.createElementNS(OOXML.W_NS, 'w:commentRangeStart');
@@ -491,12 +759,6 @@ async function ensureAuthorInPeople(zip, author) {
491
759
  root.appendChild(personEl);
492
760
  zip.writeText('word/people.xml', serializeXml(peopleDoc));
493
761
  }
494
- var FieldState;
495
- (function (FieldState) {
496
- FieldState[FieldState["OUTSIDE_FIELD"] = 0] = "OUTSIDE_FIELD";
497
- FieldState[FieldState["IN_FIELD_CODE"] = 1] = "IN_FIELD_CODE";
498
- FieldState[FieldState["IN_FIELD_RESULT"] = 2] = "IN_FIELD_RESULT";
499
- })(FieldState || (FieldState = {}));
500
762
  /**
501
763
  * Read all comments from a document, building a threaded tree.
502
764
  *
@@ -504,7 +766,7 @@ var FieldState;
504
766
  * their parent's `replies` array. Thread linkage is resolved via
505
767
  * commentsExtended.xml paraIdParent relationships.
506
768
  */
507
- export async function getComments(zip, documentXml) {
769
+ export async function getComments(zip, documentXml, styles = parseStylesXml(null), theme = parseThemeXml(null)) {
508
770
  const commentsText = await zip.readTextOrNull('word/comments.xml');
509
771
  if (!commentsText)
510
772
  return [];
@@ -526,7 +788,8 @@ export async function getComments(zip, documentXml) {
526
788
  const date = getAttributeSafe(el, OOXML.W_NS, 'date', 'w', { bareFallback: false }) ?? '';
527
789
  const initials = getAttributeSafe(el, OOXML.W_NS, 'initials', 'w', { bareFallback: false }) ?? '';
528
790
  // Extract text from <w:t> elements, skipping annotationRef runs
529
- const text = extractCommentText(el);
791
+ const paragraphs = extractCommentParagraphs(el, styles, theme);
792
+ const text = paragraphs.map((paragraph) => paragraph.text).join('\n');
530
793
  // Get paraId from first <w:p> child (namespace-aware to handle non-`w` prefixes)
531
794
  const paras = el.getElementsByTagNameNS(OOXML.W_NS, W.p);
532
795
  let paragraphId = null;
@@ -542,6 +805,7 @@ export async function getComments(zip, documentXml) {
542
805
  date,
543
806
  initials,
544
807
  text,
808
+ paragraphs,
545
809
  paragraphId,
546
810
  anchoredParagraphId: startPoint?.paragraphId ?? null,
547
811
  endParagraphId: endPoint?.paragraphId ?? startPoint?.paragraphId ?? null,
@@ -549,6 +813,8 @@ export async function getComments(zip, documentXml) {
549
813
  startCharOffset: startPoint?.charOffset,
550
814
  endRunIndex: endPoint?.runIndex,
551
815
  endCharOffset: endPoint?.charOffset,
816
+ startTextOffset: startPoint?.textOffset,
817
+ endTextOffset: endPoint?.textOffset,
552
818
  replies: [],
553
819
  };
554
820
  byId.set(id, comment);
@@ -608,101 +874,29 @@ function resolveCommentRangeMetadata(documentXml) {
608
874
  return { startById, endById };
609
875
  }
610
876
  function resolveCommentRangeMetadataInParagraph(paragraph, startById, endById) {
611
- const walkState = {
612
- charPos: 0,
613
- fieldState: FieldState.OUTSIDE_FIELD,
614
- allRuns: [],
615
- currentRunIndex: 0,
616
- startMarkers: [],
617
- endMarkers: [],
618
- };
619
- walkParagraphForCommentMarkers(paragraph, paragraph, walkState);
877
+ const index = buildParagraphIndex(paragraph);
620
878
  const paragraphId = getParagraphBookmarkId(paragraph);
621
- for (const marker of walkState.startMarkers) {
622
- if (startById.has(marker.id))
879
+ for (const marker of index.nodes.filter((node) => node.kind === 'comment-range-start')) {
880
+ const id = getCommentMarkerId(marker.element);
881
+ if (id == null || startById.has(id))
623
882
  continue;
624
- startById.set(marker.id, {
883
+ startById.set(id, {
625
884
  paragraphId,
626
- ...resolveMarkerToRunBoundary(walkState.allRuns, marker, 'start'),
885
+ textOffset: marker.visibleStart,
886
+ ...resolveIndexedMarkerBoundary(index, marker, 'start'),
627
887
  });
628
888
  }
629
- for (const marker of walkState.endMarkers) {
630
- if (endById.has(marker.id))
889
+ for (const marker of index.nodes.filter((node) => node.kind === 'comment-range-end')) {
890
+ const id = getCommentMarkerId(marker.element);
891
+ if (id == null || endById.has(id))
631
892
  continue;
632
- endById.set(marker.id, {
893
+ endById.set(id, {
633
894
  paragraphId,
634
- ...resolveMarkerToRunBoundary(walkState.allRuns, marker, 'end'),
895
+ textOffset: marker.visibleStart,
896
+ ...resolveIndexedMarkerBoundary(index, marker, 'end'),
635
897
  });
636
898
  }
637
899
  }
638
- function walkParagraphForCommentMarkers(rootParagraph, node, state) {
639
- for (const child of childElements(node)) {
640
- if (child !== rootParagraph && isW(child, W.p))
641
- continue;
642
- if (isW(child, W.commentRangeStart)) {
643
- recordParagraphLevelMarker(child, state.startMarkers, state);
644
- continue;
645
- }
646
- if (isW(child, W.commentRangeEnd)) {
647
- recordParagraphLevelMarker(child, state.endMarkers, state);
648
- continue;
649
- }
650
- if (isW(child, W.r)) {
651
- walkRunForCommentMarkers(child, state);
652
- continue;
653
- }
654
- walkParagraphForCommentMarkers(rootParagraph, child, state);
655
- }
656
- }
657
- function walkRunForCommentMarkers(run, state) {
658
- const runIndex = state.currentRunIndex;
659
- state.currentRunIndex += 1;
660
- const runVisibleStart = state.charPos;
661
- for (const child of childElements(run)) {
662
- if (isW(child, W.commentRangeStart)) {
663
- recordInRunMarker(child, state.startMarkers, runIndex, state.charPos - runVisibleStart);
664
- continue;
665
- }
666
- if (isW(child, W.commentRangeEnd)) {
667
- recordInRunMarker(child, state.endMarkers, runIndex, state.charPos - runVisibleStart);
668
- continue;
669
- }
670
- if (!child.namespaceURI || child.namespaceURI !== OOXML.W_NS)
671
- continue;
672
- if (child.localName === W.fldChar) {
673
- const type = getWordAttribute(child, 'fldCharType') ?? '';
674
- if (type === 'begin')
675
- state.fieldState = FieldState.IN_FIELD_CODE;
676
- else if (type === 'separate')
677
- state.fieldState = FieldState.IN_FIELD_RESULT;
678
- else if (type === 'end')
679
- state.fieldState = FieldState.OUTSIDE_FIELD;
680
- continue;
681
- }
682
- if (state.fieldState === FieldState.IN_FIELD_CODE)
683
- continue;
684
- if (child.localName === W.t) {
685
- state.charPos += (getLeafText(child) ?? '').length;
686
- continue;
687
- }
688
- if (child.localName === W.tab || child.localName === W.br) {
689
- state.charPos += 1;
690
- }
691
- }
692
- state.allRuns.push({ visibleLength: state.charPos - runVisibleStart });
693
- }
694
- function recordInRunMarker(markerEl, bucket, runIndex, charOffset) {
695
- const id = getCommentMarkerId(markerEl);
696
- if (id == null)
697
- return;
698
- bucket.push({ id, inside: { runIndex, charOffset } });
699
- }
700
- function recordParagraphLevelMarker(markerEl, bucket, state) {
701
- const id = getCommentMarkerId(markerEl);
702
- if (id == null)
703
- return;
704
- bucket.push({ id, between: { afterRunIndex: state.currentRunIndex - 1 } });
705
- }
706
900
  function getCommentMarkerId(markerEl) {
707
901
  const idStr = getAttributeSafe(markerEl, OOXML.W_NS, 'id', 'w', { bareFallback: false });
708
902
  if (!idStr)
@@ -710,39 +904,20 @@ function getCommentMarkerId(markerEl) {
710
904
  const id = parseInt(idStr, 10);
711
905
  return Number.isNaN(id) ? null : id;
712
906
  }
713
- function getWordAttribute(el, localName) {
714
- return getAttributeSafe(el, OOXML.W_NS, localName, 'w');
715
- }
716
- function resolveMarkerToRunBoundary(allRuns, marker, boundary) {
717
- // Marker seen INSIDE a run already carries the exact runIndex + charOffset.
718
- if (marker.inside) {
719
- return { runIndex: marker.inside.runIndex, charOffset: marker.inside.charOffset };
720
- }
721
- if (!marker.between)
722
- return {};
723
- if (allRuns.length === 0)
907
+ function resolveIndexedMarkerBoundary(index, marker, boundary) {
908
+ if (marker.runIndex !== null)
909
+ return { runIndex: marker.runIndex, charOffset: marker.runVisibleOffset };
910
+ if (index.runs.length === 0)
724
911
  return {};
725
- const { afterRunIndex } = marker.between;
726
- const lastIndex = allRuns.length - 1;
727
- if (boundary === 'start') {
728
- // Marker sits between run `afterRunIndex` and run `afterRunIndex + 1`.
729
- // For a `start` marker, the range opens at offset 0 of the next run if it exists.
730
- const nextIdx = afterRunIndex + 1;
731
- if (nextIdx <= lastIndex) {
732
- return { runIndex: nextIdx, charOffset: 0 };
733
- }
734
- // Marker is past the last run — clamp to end of last run.
735
- return { runIndex: lastIndex, charOffset: allRuns[lastIndex].visibleLength };
736
- }
737
- // boundary === 'end': range closes at the end of the previous run if one exists.
738
- if (afterRunIndex < 0) {
739
- // Marker is before the first run — empty range starting at run 0.
740
- return { runIndex: 0, charOffset: 0 };
741
- }
742
- if (afterRunIndex <= lastIndex) {
743
- return { runIndex: afterRunIndex, charOffset: allRuns[afterRunIndex].visibleLength };
744
- }
745
- return { runIndex: lastIndex, charOffset: allRuns[lastIndex].visibleLength };
912
+ const before = [...index.runs].reverse().find((run) => run.structuralIndex < marker.structuralIndex);
913
+ const after = index.runs.find((run) => run.structuralIndex > marker.structuralIndex);
914
+ if (boundary === 'start' && after)
915
+ return { runIndex: after.runIndex, charOffset: 0 };
916
+ if (before)
917
+ return { runIndex: before.runIndex, charOffset: before.visibleText.length };
918
+ // At least one run exists, and a marker with no preceding run must sort
919
+ // before the first run, so `after` is necessarily defined here.
920
+ return { runIndex: after.runIndex, charOffset: 0 };
746
921
  }
747
922
  /**
748
923
  * Get a single comment by ID, searching the full tree including replies.
@@ -943,10 +1118,10 @@ function removeCommentMarkersFromDocument(documentXml, commentId, ctx) {
943
1118
  // If the run lived inside a tracked-change wrapper (e.g., the comment
944
1119
  // was added with ctx earlier and is now being deleted without ctx),
945
1120
  // the wrapper is left orphaned with no content. Clean it up.
946
- if (runParent && isW(runParent, 'ins')) {
1121
+ if (runParent && isW(runParent, 'ins') && !hasElementChildren(runParent)) {
947
1122
  runParent.parentNode?.removeChild(runParent);
948
1123
  }
949
- else if (runParent && isW(runParent, 'del')) {
1124
+ else if (runParent && isW(runParent, 'del') && !hasElementChildren(runParent)) {
950
1125
  runParent.parentNode?.removeChild(runParent);
951
1126
  }
952
1127
  }
@@ -965,21 +1140,46 @@ function hasVisibleRunContent(run) {
965
1140
  }
966
1141
  return false;
967
1142
  }
968
- function extractCommentText(commentEl) {
969
- const parts = [];
970
- const runs = commentEl.getElementsByTagNameNS(OOXML.W_NS, W.r);
971
- for (let i = 0; i < runs.length; i++) {
972
- const run = runs.item(i);
973
- // Skip runs that contain annotationRef (they're metadata, not user text)
974
- const annotRefs = run.getElementsByTagNameNS(OOXML.W_NS, W.annotationRef);
975
- if (annotRefs.length > 0)
976
- continue;
977
- const ts = run.getElementsByTagNameNS(OOXML.W_NS, W.t);
978
- for (let j = 0; j < ts.length; j++) {
979
- const t = ts.item(j);
980
- parts.push(t.textContent ?? '');
1143
+ function hasElementChildren(element) {
1144
+ return Array.from(element.childNodes).some((child) => child.nodeType === 1);
1145
+ }
1146
+ function extractCommentParagraphs(commentEl, styles, theme) {
1147
+ const paragraphs = commentEl.getElementsByTagNameNS(OOXML.W_NS, W.p);
1148
+ const result = [];
1149
+ for (let pi = 0; pi < paragraphs.length; pi++) {
1150
+ const paragraph = paragraphs.item(pi);
1151
+ const pPr = getFirstChild(paragraph, OOXML.W_NS, W.pPr);
1152
+ const pStyle = pPr ? getFirstChild(pPr, OOXML.W_NS, W.pStyle) : null;
1153
+ const style = pStyle ? getAttributeSafe(pStyle, OOXML.W_NS, 'val', 'w') : null;
1154
+ const annotated = [];
1155
+ const runs = paragraph.getElementsByTagNameNS(OOXML.W_NS, W.r);
1156
+ for (let ri = 0; ri < runs.length; ri++) {
1157
+ const run = runs.item(ri);
1158
+ if (run.getElementsByTagNameNS(OOXML.W_NS, W.annotationRef).length > 0)
1159
+ continue;
1160
+ let text = '';
1161
+ const ts = run.getElementsByTagNameNS(OOXML.W_NS, W.t);
1162
+ for (let ti = 0; ti < ts.length; ti++)
1163
+ text += ts.item(ti).textContent ?? '';
1164
+ if (!text)
1165
+ continue;
1166
+ const formatting = extractEffectiveRunFormatting({
1167
+ run,
1168
+ paragraphPPr: pPr,
1169
+ paragraphStyleId: style,
1170
+ styles,
1171
+ theme,
1172
+ });
1173
+ annotated.push({ text, formatting, hyperlinkUrl: null, charCount: text.length, isHeaderRun: false });
981
1174
  }
1175
+ const tagged_text = mergeAdjacentTags(emitFormattingTags({
1176
+ runs: annotated,
1177
+ baseline: { bold: false, italic: false, underline: false, suppressed: false },
1178
+ fontBaseline: { modalColor: null, colorSuppressed: false, modalFontSizePt: 0, fontSizeSuppressed: true, modalFontName: '', fontNameSuppressed: true },
1179
+ formattingMode: 'full',
1180
+ }));
1181
+ result.push({ text: annotated.map((run) => run.text).join(''), tagged_text, style });
982
1182
  }
983
- return parts.join('');
1183
+ return result;
984
1184
  }
985
1185
  //# sourceMappingURL=comments.js.map