@usejunior/docx-core 0.20.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.
- package/README.md +12 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli/conformance-adapter.d.ts.map +1 -1
- package/dist/cli/conformance-adapter.js +0 -27
- package/dist/cli/conformance-adapter.js.map +1 -1
- package/dist/generation/compile.d.ts.map +1 -1
- package/dist/generation/compile.js +1 -3
- package/dist/generation/compile.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/integration/libreoffice-oracle.d.ts +3 -1
- package/dist/integration/libreoffice-oracle.d.ts.map +1 -1
- package/dist/integration/libreoffice-oracle.js +9 -3
- package/dist/integration/libreoffice-oracle.js.map +1 -1
- package/dist/primitives/accept_changes.d.ts +3 -4
- package/dist/primitives/accept_changes.d.ts.map +1 -1
- package/dist/primitives/accept_changes.js +165 -79
- package/dist/primitives/accept_changes.js.map +1 -1
- package/dist/primitives/bookmarks.d.ts +7 -2
- package/dist/primitives/bookmarks.d.ts.map +1 -1
- package/dist/primitives/bookmarks.js +30 -6
- package/dist/primitives/bookmarks.js.map +1 -1
- package/dist/primitives/comments.d.ts +73 -4
- package/dist/primitives/comments.d.ts.map +1 -1
- package/dist/primitives/comments.js +266 -165
- package/dist/primitives/comments.js.map +1 -1
- package/dist/primitives/conformance.d.ts +42 -0
- package/dist/primitives/conformance.d.ts.map +1 -0
- package/dist/primitives/conformance.js +61 -0
- package/dist/primitives/conformance.js.map +1 -0
- package/dist/primitives/document.d.ts +98 -1
- package/dist/primitives/document.d.ts.map +1 -1
- package/dist/primitives/document.js +315 -22
- package/dist/primitives/document.js.map +1 -1
- package/dist/primitives/errors.d.ts +3 -2
- package/dist/primitives/errors.d.ts.map +1 -1
- package/dist/primitives/errors.js +3 -1
- package/dist/primitives/errors.js.map +1 -1
- package/dist/primitives/extract_revisions.d.ts +23 -6
- package/dist/primitives/extract_revisions.d.ts.map +1 -1
- package/dist/primitives/extract_revisions.js +208 -38
- package/dist/primitives/extract_revisions.js.map +1 -1
- package/dist/primitives/footnotes.d.ts +36 -0
- package/dist/primitives/footnotes.d.ts.map +1 -1
- package/dist/primitives/footnotes.js +173 -75
- package/dist/primitives/footnotes.js.map +1 -1
- package/dist/primitives/index.d.ts +7 -1
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/index.js +7 -1
- package/dist/primitives/index.js.map +1 -1
- package/dist/primitives/paragraph-index.d.ts +31 -0
- package/dist/primitives/paragraph-index.d.ts.map +1 -0
- package/dist/primitives/paragraph-index.js +140 -0
- package/dist/primitives/paragraph-index.js.map +1 -0
- package/dist/primitives/paragraph_merge_formatting.d.ts +24 -0
- package/dist/primitives/paragraph_merge_formatting.d.ts.map +1 -0
- package/dist/primitives/paragraph_merge_formatting.js +86 -0
- package/dist/primitives/paragraph_merge_formatting.js.map +1 -0
- package/dist/primitives/paragraph_structure.d.ts +19 -0
- package/dist/primitives/paragraph_structure.d.ts.map +1 -0
- package/dist/primitives/paragraph_structure.js +66 -0
- package/dist/primitives/paragraph_structure.js.map +1 -0
- package/dist/primitives/reject_changes.d.ts +3 -4
- package/dist/primitives/reject_changes.d.ts.map +1 -1
- package/dist/primitives/reject_changes.js +166 -79
- package/dist/primitives/reject_changes.js.map +1 -1
- package/dist/primitives/relationships.d.ts +22 -0
- package/dist/primitives/relationships.d.ts.map +1 -1
- package/dist/primitives/relationships.js +87 -0
- package/dist/primitives/relationships.js.map +1 -1
- package/dist/primitives/revision-parts.d.ts +15 -0
- package/dist/primitives/revision-parts.d.ts.map +1 -1
- package/dist/primitives/revision-parts.js +33 -1
- package/dist/primitives/revision-parts.js.map +1 -1
- package/dist/primitives/sections.d.ts +2 -1
- package/dist/primitives/sections.d.ts.map +1 -1
- package/dist/primitives/sections.js +2 -2
- package/dist/primitives/sections.js.map +1 -1
- package/dist/primitives/styles.d.ts +1 -0
- package/dist/primitives/styles.d.ts.map +1 -1
- package/dist/primitives/styles.js +1 -0
- package/dist/primitives/styles.js.map +1 -1
- package/dist/primitives/table_cells.d.ts +49 -0
- package/dist/primitives/table_cells.d.ts.map +1 -0
- package/dist/primitives/table_cells.js +149 -0
- package/dist/primitives/table_cells.js.map +1 -0
- package/dist/primitives/table_columns.d.ts +50 -0
- package/dist/primitives/table_columns.d.ts.map +1 -0
- package/dist/primitives/table_columns.js +138 -0
- package/dist/primitives/table_columns.js.map +1 -0
- package/dist/primitives/table_edit_common.d.ts +39 -0
- package/dist/primitives/table_edit_common.d.ts.map +1 -0
- package/dist/primitives/table_edit_common.js +163 -0
- package/dist/primitives/table_edit_common.js.map +1 -0
- package/dist/primitives/table_occupancy.d.ts +39 -0
- package/dist/primitives/table_occupancy.d.ts.map +1 -0
- package/dist/primitives/table_occupancy.js +145 -0
- package/dist/primitives/table_occupancy.js.map +1 -0
- package/dist/primitives/table_rows.d.ts +56 -0
- package/dist/primitives/table_rows.d.ts.map +1 -0
- package/dist/primitives/table_rows.js +390 -0
- package/dist/primitives/table_rows.js.map +1 -0
- package/dist/primitives/text.d.ts +40 -1
- package/dist/primitives/text.d.ts.map +1 -1
- package/dist/primitives/text.js +309 -129
- package/dist/primitives/text.js.map +1 -1
- package/dist/primitives/track-changes-emitter.d.ts +16 -2
- package/dist/primitives/track-changes-emitter.d.ts.map +1 -1
- package/dist/primitives/track-changes-emitter.js +38 -13
- package/dist/primitives/track-changes-emitter.js.map +1 -1
- package/dist/primitives/zip.d.ts +22 -1
- package/dist/primitives/zip.d.ts.map +1 -1
- package/dist/primitives/zip.js +47 -8
- package/dist/primitives/zip.js.map +1 -1
- package/dist/shared/docx/DocxArchive.d.ts +6 -2
- package/dist/shared/docx/DocxArchive.d.ts.map +1 -1
- package/dist/shared/docx/DocxArchive.js +10 -4
- package/dist/shared/docx/DocxArchive.js.map +1 -1
- package/dist/shared/field-structure.d.ts +3 -0
- package/dist/shared/field-structure.d.ts.map +1 -1
- package/dist/shared/field-structure.js +7 -5
- package/dist/shared/field-structure.js.map +1 -1
- package/package.json +3 -4
|
@@ -10,8 +10,14 @@ import { parseXml, serializeXml } from './xml.js';
|
|
|
10
10
|
import { DocxZip } from './zip.js';
|
|
11
11
|
import { getParagraphRuns, getParagraphText, splitRunAtVisibleOffset } from './text.js';
|
|
12
12
|
import { getParagraphBookmarkId } from './bookmarks.js';
|
|
13
|
-
import {
|
|
13
|
+
import { isW } from './dom-helpers.js';
|
|
14
|
+
import { buildParagraphIndex } from './paragraph-index.js';
|
|
14
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';
|
|
15
21
|
import { createRevisionContainer, prepareElementForDeletion, } from './track-changes-emitter.js';
|
|
16
22
|
// ── Relationship types ──────────────────────────────────────────────────
|
|
17
23
|
const REL_TYPE_COMMENTS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments';
|
|
@@ -148,6 +154,9 @@ async function ensureRelationships(zip, newParts) {
|
|
|
148
154
|
}
|
|
149
155
|
zip.writeText(relsPath, serializeXml(relsDoc));
|
|
150
156
|
}
|
|
157
|
+
function commentBodyDestinations(body) {
|
|
158
|
+
return body?.flatMap((paragraph) => paragraph.runs.flatMap((run) => run.hyperlink ? [run.hyperlink.destination] : [])) ?? [];
|
|
159
|
+
}
|
|
151
160
|
function deterministicParaId(params, commentId) {
|
|
152
161
|
return createHash('sha256')
|
|
153
162
|
.update(`${commentId}\0${params.author}\0${params.initials}\0${params.date}\0${params.text}`)
|
|
@@ -198,6 +207,11 @@ export async function addTrackedRangeComments(buffer, comments) {
|
|
|
198
207
|
const endParent = endRevision.parentNode;
|
|
199
208
|
if (!startParent || !endParent)
|
|
200
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
|
+
}
|
|
201
215
|
const commentId = allocateNextCommentId(commentsDoc);
|
|
202
216
|
const rangeStart = documentXml.createElementNS(OOXML.W_NS, 'w:commentRangeStart');
|
|
203
217
|
rangeStart.setAttribute('w:id', String(commentId));
|
|
@@ -228,6 +242,10 @@ export async function addTrackedRangeComments(buffer, comments) {
|
|
|
228
242
|
* - Inserts commentReference run after range end
|
|
229
243
|
* - Adds comment entry to comments.xml
|
|
230
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
|
|
231
249
|
*/
|
|
232
250
|
export async function addComment(documentXml, zip, params, ctx) {
|
|
233
251
|
const { paragraphEl, author, text, initials } = params;
|
|
@@ -249,19 +267,31 @@ export async function addComment(documentXml, zip, params, ctx) {
|
|
|
249
267
|
insertCommentMarkers(documentXml, paragraphEl, commentId, start, end, ctx);
|
|
250
268
|
// Add comment element to comments.xml
|
|
251
269
|
const paraId = generateParaId();
|
|
270
|
+
const hyperlinkRelationshipIds = await ensureExternalHyperlinkRelationships(zip, 'word/comments.xml', commentBodyDestinations(params.body));
|
|
252
271
|
addCommentElement(commentsDoc, {
|
|
253
272
|
id: commentId,
|
|
254
273
|
author,
|
|
255
274
|
initials: initials ?? author.charAt(0).toUpperCase(),
|
|
256
275
|
text,
|
|
257
276
|
paraId,
|
|
258
|
-
date:
|
|
277
|
+
date: resolveDefinitionDate(params.date, ctx),
|
|
278
|
+
body: params.body,
|
|
279
|
+
hyperlinkRelationshipIds,
|
|
259
280
|
});
|
|
260
281
|
zip.writeText('word/comments.xml', serializeXml(commentsDoc));
|
|
261
282
|
// Add author to people.xml
|
|
262
283
|
await ensureAuthorInPeople(zip, author);
|
|
263
284
|
return { commentId };
|
|
264
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
|
+
}
|
|
265
295
|
/**
|
|
266
296
|
* Add a threaded reply to an existing comment.
|
|
267
297
|
*
|
|
@@ -269,9 +299,10 @@ export async function addComment(documentXml, zip, params, ctx) {
|
|
|
269
299
|
* Thread linkage is stored in commentsExtended.xml via paraIdParent.
|
|
270
300
|
* Replies emit no body revision markup (there is nothing to anchor), but the
|
|
271
301
|
* reply's comment definition still claims creation metadata — so a
|
|
272
|
-
* caller-supplied `
|
|
273
|
-
*
|
|
274
|
-
* operation. Author and initials
|
|
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`.
|
|
275
306
|
*/
|
|
276
307
|
export async function addCommentReply(_documentXml, zip, params, ctx) {
|
|
277
308
|
const { parentCommentId, author, text, initials } = params;
|
|
@@ -286,13 +317,16 @@ export async function addCommentReply(_documentXml, zip, params, ctx) {
|
|
|
286
317
|
// Allocate ID and add reply comment
|
|
287
318
|
const commentId = allocateNextCommentId(commentsDoc);
|
|
288
319
|
const replyParaId = generateParaId();
|
|
320
|
+
const hyperlinkRelationshipIds = await ensureExternalHyperlinkRelationships(zip, 'word/comments.xml', commentBodyDestinations(params.body));
|
|
289
321
|
addCommentElement(commentsDoc, {
|
|
290
322
|
id: commentId,
|
|
291
323
|
author,
|
|
292
324
|
initials: initials ?? author.charAt(0).toUpperCase(),
|
|
293
325
|
text,
|
|
294
326
|
paraId: replyParaId,
|
|
295
|
-
date:
|
|
327
|
+
date: resolveDefinitionDate(params.date, ctx),
|
|
328
|
+
body: params.body,
|
|
329
|
+
hyperlinkRelationshipIds,
|
|
296
330
|
});
|
|
297
331
|
zip.writeText('word/comments.xml', serializeXml(commentsDoc));
|
|
298
332
|
// Link reply in commentsExtended.xml
|
|
@@ -303,6 +337,66 @@ export async function addCommentReply(_documentXml, zip, params, ctx) {
|
|
|
303
337
|
await ensureAuthorInPeople(zip, author);
|
|
304
338
|
return { commentId, parentCommentId };
|
|
305
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
|
+
}
|
|
306
400
|
// ── Internal helpers ────────────────────────────────────────────────────
|
|
307
401
|
function allocateNextCommentId(commentsDoc) {
|
|
308
402
|
const commentEls = commentsDoc.getElementsByTagNameNS(OOXML.W_NS, W.comment);
|
|
@@ -360,23 +454,35 @@ function ensureCommentPartNamespaceAliases(commentsDoc) {
|
|
|
360
454
|
* Append a `w:comment` definition to the comments part.
|
|
361
455
|
*
|
|
362
456
|
* @conformance ECMA-376 edition 5, Part 1 § 17.13.4.2
|
|
363
|
-
* The `w:date` creation stamp uses the caller-supplied
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
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.
|
|
371
471
|
* @see #859
|
|
472
|
+
* @see #961
|
|
473
|
+
* @see #1103
|
|
372
474
|
*/
|
|
373
475
|
function addCommentElement(commentsDoc, params) {
|
|
374
476
|
ensureCommentPartNamespaceAliases(commentsDoc);
|
|
375
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
|
+
}
|
|
376
481
|
const commentEl = commentsDoc.createElementNS(OOXML.W_NS, 'w:comment');
|
|
377
482
|
commentEl.setAttribute('w:id', String(params.id));
|
|
378
483
|
commentEl.setAttribute('w:author', params.author);
|
|
379
|
-
|
|
484
|
+
if (params.date !== null)
|
|
485
|
+
commentEl.setAttribute('w:date', params.date ?? isoNow());
|
|
380
486
|
commentEl.setAttribute('w:initials', params.initials);
|
|
381
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>
|
|
382
488
|
const p = commentsDoc.createElementNS(OOXML.W_NS, 'w:p');
|
|
@@ -388,18 +494,88 @@ function addCommentElement(commentsDoc, params) {
|
|
|
388
494
|
const annotRef = commentsDoc.createElementNS(OOXML.W_NS, 'w:annotationRef');
|
|
389
495
|
refRun.appendChild(annotRef);
|
|
390
496
|
p.appendChild(refRun);
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const t = commentsDoc.createElementNS(OOXML.W_NS, 'w:t');
|
|
394
|
-
if (params.text.startsWith(' ') || params.text.endsWith(' ')) {
|
|
395
|
-
t.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
|
396
|
-
}
|
|
397
|
-
t.appendChild(commentsDoc.createTextNode(params.text));
|
|
398
|
-
textRun.appendChild(t);
|
|
399
|
-
p.appendChild(textRun);
|
|
497
|
+
const body = params.body ?? [{ runs: [{ text: params.text }] }];
|
|
498
|
+
appendCommentBodyRuns(p, body[0]?.runs ?? [], commentsDoc, params.hyperlinkRelationshipIds);
|
|
400
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
|
+
}
|
|
401
505
|
root.appendChild(commentEl);
|
|
402
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
|
+
}
|
|
403
579
|
function insertCommentMarkers(documentXml, paragraphEl, commentId, start, end, ctx) {
|
|
404
580
|
const runs = getParagraphRuns(paragraphEl);
|
|
405
581
|
const rangeStart = documentXml.createElementNS(OOXML.W_NS, 'w:commentRangeStart');
|
|
@@ -583,12 +759,6 @@ async function ensureAuthorInPeople(zip, author) {
|
|
|
583
759
|
root.appendChild(personEl);
|
|
584
760
|
zip.writeText('word/people.xml', serializeXml(peopleDoc));
|
|
585
761
|
}
|
|
586
|
-
var FieldState;
|
|
587
|
-
(function (FieldState) {
|
|
588
|
-
FieldState[FieldState["OUTSIDE_FIELD"] = 0] = "OUTSIDE_FIELD";
|
|
589
|
-
FieldState[FieldState["IN_FIELD_CODE"] = 1] = "IN_FIELD_CODE";
|
|
590
|
-
FieldState[FieldState["IN_FIELD_RESULT"] = 2] = "IN_FIELD_RESULT";
|
|
591
|
-
})(FieldState || (FieldState = {}));
|
|
592
762
|
/**
|
|
593
763
|
* Read all comments from a document, building a threaded tree.
|
|
594
764
|
*
|
|
@@ -596,7 +766,7 @@ var FieldState;
|
|
|
596
766
|
* their parent's `replies` array. Thread linkage is resolved via
|
|
597
767
|
* commentsExtended.xml paraIdParent relationships.
|
|
598
768
|
*/
|
|
599
|
-
export async function getComments(zip, documentXml) {
|
|
769
|
+
export async function getComments(zip, documentXml, styles = parseStylesXml(null), theme = parseThemeXml(null)) {
|
|
600
770
|
const commentsText = await zip.readTextOrNull('word/comments.xml');
|
|
601
771
|
if (!commentsText)
|
|
602
772
|
return [];
|
|
@@ -618,7 +788,8 @@ export async function getComments(zip, documentXml) {
|
|
|
618
788
|
const date = getAttributeSafe(el, OOXML.W_NS, 'date', 'w', { bareFallback: false }) ?? '';
|
|
619
789
|
const initials = getAttributeSafe(el, OOXML.W_NS, 'initials', 'w', { bareFallback: false }) ?? '';
|
|
620
790
|
// Extract text from <w:t> elements, skipping annotationRef runs
|
|
621
|
-
const
|
|
791
|
+
const paragraphs = extractCommentParagraphs(el, styles, theme);
|
|
792
|
+
const text = paragraphs.map((paragraph) => paragraph.text).join('\n');
|
|
622
793
|
// Get paraId from first <w:p> child (namespace-aware to handle non-`w` prefixes)
|
|
623
794
|
const paras = el.getElementsByTagNameNS(OOXML.W_NS, W.p);
|
|
624
795
|
let paragraphId = null;
|
|
@@ -634,6 +805,7 @@ export async function getComments(zip, documentXml) {
|
|
|
634
805
|
date,
|
|
635
806
|
initials,
|
|
636
807
|
text,
|
|
808
|
+
paragraphs,
|
|
637
809
|
paragraphId,
|
|
638
810
|
anchoredParagraphId: startPoint?.paragraphId ?? null,
|
|
639
811
|
endParagraphId: endPoint?.paragraphId ?? startPoint?.paragraphId ?? null,
|
|
@@ -702,103 +874,29 @@ function resolveCommentRangeMetadata(documentXml) {
|
|
|
702
874
|
return { startById, endById };
|
|
703
875
|
}
|
|
704
876
|
function resolveCommentRangeMetadataInParagraph(paragraph, startById, endById) {
|
|
705
|
-
const
|
|
706
|
-
charPos: 0,
|
|
707
|
-
fieldState: FieldState.OUTSIDE_FIELD,
|
|
708
|
-
allRuns: [],
|
|
709
|
-
currentRunIndex: 0,
|
|
710
|
-
startMarkers: [],
|
|
711
|
-
endMarkers: [],
|
|
712
|
-
};
|
|
713
|
-
walkParagraphForCommentMarkers(paragraph, paragraph, walkState);
|
|
877
|
+
const index = buildParagraphIndex(paragraph);
|
|
714
878
|
const paragraphId = getParagraphBookmarkId(paragraph);
|
|
715
|
-
for (const marker of
|
|
716
|
-
|
|
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))
|
|
717
882
|
continue;
|
|
718
|
-
startById.set(
|
|
883
|
+
startById.set(id, {
|
|
719
884
|
paragraphId,
|
|
720
|
-
textOffset: marker.
|
|
721
|
-
...
|
|
885
|
+
textOffset: marker.visibleStart,
|
|
886
|
+
...resolveIndexedMarkerBoundary(index, marker, 'start'),
|
|
722
887
|
});
|
|
723
888
|
}
|
|
724
|
-
for (const marker of
|
|
725
|
-
|
|
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))
|
|
726
892
|
continue;
|
|
727
|
-
endById.set(
|
|
893
|
+
endById.set(id, {
|
|
728
894
|
paragraphId,
|
|
729
|
-
textOffset: marker.
|
|
730
|
-
...
|
|
895
|
+
textOffset: marker.visibleStart,
|
|
896
|
+
...resolveIndexedMarkerBoundary(index, marker, 'end'),
|
|
731
897
|
});
|
|
732
898
|
}
|
|
733
899
|
}
|
|
734
|
-
function walkParagraphForCommentMarkers(rootParagraph, node, state) {
|
|
735
|
-
for (const child of childElements(node)) {
|
|
736
|
-
if (child !== rootParagraph && isW(child, W.p))
|
|
737
|
-
continue;
|
|
738
|
-
if (isW(child, W.commentRangeStart)) {
|
|
739
|
-
recordParagraphLevelMarker(child, state.startMarkers, state);
|
|
740
|
-
continue;
|
|
741
|
-
}
|
|
742
|
-
if (isW(child, W.commentRangeEnd)) {
|
|
743
|
-
recordParagraphLevelMarker(child, state.endMarkers, state);
|
|
744
|
-
continue;
|
|
745
|
-
}
|
|
746
|
-
if (isW(child, W.r)) {
|
|
747
|
-
walkRunForCommentMarkers(child, state);
|
|
748
|
-
continue;
|
|
749
|
-
}
|
|
750
|
-
walkParagraphForCommentMarkers(rootParagraph, child, state);
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
function walkRunForCommentMarkers(run, state) {
|
|
754
|
-
const runIndex = state.currentRunIndex;
|
|
755
|
-
state.currentRunIndex += 1;
|
|
756
|
-
const runVisibleStart = state.charPos;
|
|
757
|
-
for (const child of childElements(run)) {
|
|
758
|
-
if (isW(child, W.commentRangeStart)) {
|
|
759
|
-
recordInRunMarker(child, state.startMarkers, runIndex, state.charPos - runVisibleStart, state.charPos);
|
|
760
|
-
continue;
|
|
761
|
-
}
|
|
762
|
-
if (isW(child, W.commentRangeEnd)) {
|
|
763
|
-
recordInRunMarker(child, state.endMarkers, runIndex, state.charPos - runVisibleStart, state.charPos);
|
|
764
|
-
continue;
|
|
765
|
-
}
|
|
766
|
-
if (!child.namespaceURI || child.namespaceURI !== OOXML.W_NS)
|
|
767
|
-
continue;
|
|
768
|
-
if (child.localName === W.fldChar) {
|
|
769
|
-
const type = getWordAttribute(child, 'fldCharType') ?? '';
|
|
770
|
-
if (type === 'begin')
|
|
771
|
-
state.fieldState = FieldState.IN_FIELD_CODE;
|
|
772
|
-
else if (type === 'separate')
|
|
773
|
-
state.fieldState = FieldState.IN_FIELD_RESULT;
|
|
774
|
-
else if (type === 'end')
|
|
775
|
-
state.fieldState = FieldState.OUTSIDE_FIELD;
|
|
776
|
-
continue;
|
|
777
|
-
}
|
|
778
|
-
if (state.fieldState === FieldState.IN_FIELD_CODE)
|
|
779
|
-
continue;
|
|
780
|
-
if (child.localName === W.t) {
|
|
781
|
-
state.charPos += (getLeafText(child) ?? '').length;
|
|
782
|
-
continue;
|
|
783
|
-
}
|
|
784
|
-
if (child.localName === W.tab || child.localName === W.br) {
|
|
785
|
-
state.charPos += 1;
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
state.allRuns.push({ visibleLength: state.charPos - runVisibleStart });
|
|
789
|
-
}
|
|
790
|
-
function recordInRunMarker(markerEl, bucket, runIndex, charOffset, textOffset) {
|
|
791
|
-
const id = getCommentMarkerId(markerEl);
|
|
792
|
-
if (id == null)
|
|
793
|
-
return;
|
|
794
|
-
bucket.push({ id, textOffset, inside: { runIndex, charOffset } });
|
|
795
|
-
}
|
|
796
|
-
function recordParagraphLevelMarker(markerEl, bucket, state) {
|
|
797
|
-
const id = getCommentMarkerId(markerEl);
|
|
798
|
-
if (id == null)
|
|
799
|
-
return;
|
|
800
|
-
bucket.push({ id, textOffset: state.charPos, between: { afterRunIndex: state.currentRunIndex - 1 } });
|
|
801
|
-
}
|
|
802
900
|
function getCommentMarkerId(markerEl) {
|
|
803
901
|
const idStr = getAttributeSafe(markerEl, OOXML.W_NS, 'id', 'w', { bareFallback: false });
|
|
804
902
|
if (!idStr)
|
|
@@ -806,39 +904,20 @@ function getCommentMarkerId(markerEl) {
|
|
|
806
904
|
const id = parseInt(idStr, 10);
|
|
807
905
|
return Number.isNaN(id) ? null : id;
|
|
808
906
|
}
|
|
809
|
-
function
|
|
810
|
-
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// Marker seen INSIDE a run already carries the exact runIndex + charOffset.
|
|
814
|
-
if (marker.inside) {
|
|
815
|
-
return { runIndex: marker.inside.runIndex, charOffset: marker.inside.charOffset };
|
|
816
|
-
}
|
|
817
|
-
if (!marker.between)
|
|
818
|
-
return {};
|
|
819
|
-
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)
|
|
820
911
|
return {};
|
|
821
|
-
const
|
|
822
|
-
const
|
|
823
|
-
if (boundary === 'start')
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
// Marker is past the last run — clamp to end of last run.
|
|
831
|
-
return { runIndex: lastIndex, charOffset: allRuns[lastIndex].visibleLength };
|
|
832
|
-
}
|
|
833
|
-
// boundary === 'end': range closes at the end of the previous run if one exists.
|
|
834
|
-
if (afterRunIndex < 0) {
|
|
835
|
-
// Marker is before the first run — empty range starting at run 0.
|
|
836
|
-
return { runIndex: 0, charOffset: 0 };
|
|
837
|
-
}
|
|
838
|
-
if (afterRunIndex <= lastIndex) {
|
|
839
|
-
return { runIndex: afterRunIndex, charOffset: allRuns[afterRunIndex].visibleLength };
|
|
840
|
-
}
|
|
841
|
-
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 };
|
|
842
921
|
}
|
|
843
922
|
/**
|
|
844
923
|
* Get a single comment by ID, searching the full tree including replies.
|
|
@@ -1064,21 +1143,43 @@ function hasVisibleRunContent(run) {
|
|
|
1064
1143
|
function hasElementChildren(element) {
|
|
1065
1144
|
return Array.from(element.childNodes).some((child) => child.nodeType === 1);
|
|
1066
1145
|
}
|
|
1067
|
-
function
|
|
1068
|
-
const
|
|
1069
|
-
const
|
|
1070
|
-
for (let
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
const
|
|
1077
|
-
for (let
|
|
1078
|
-
const
|
|
1079
|
-
|
|
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 });
|
|
1080
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 });
|
|
1081
1182
|
}
|
|
1082
|
-
return
|
|
1183
|
+
return result;
|
|
1083
1184
|
}
|
|
1084
1185
|
//# sourceMappingURL=comments.js.map
|