@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.
- 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/core-types.d.ts +1 -139
- package/dist/core-types.d.ts.map +1 -1
- package/dist/core-types.js +1 -3
- package/dist/core-types.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 +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/integration/generation-probes.d.ts +36 -2
- package/dist/integration/generation-probes.d.ts.map +1 -1
- package/dist/integration/generation-probes.js +137 -15
- package/dist/integration/generation-probes.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 +10 -4
- package/dist/integration/libreoffice-oracle.js.map +1 -1
- package/dist/integration/synthetic-docx-fixture.d.ts.map +1 -1
- package/dist/integration/synthetic-docx-fixture.js +8 -0
- package/dist/integration/synthetic-docx-fixture.js.map +1 -1
- package/dist/primitives/accept_ai_edits.js +14 -2
- package/dist/primitives/accept_ai_edits.js.map +1 -1
- package/dist/primitives/accept_changes.d.ts +6 -0
- package/dist/primitives/accept_changes.d.ts.map +1 -1
- package/dist/primitives/accept_changes.js +236 -62
- package/dist/primitives/accept_changes.js.map +1 -1
- package/dist/primitives/bookmarks.d.ts +8 -2
- package/dist/primitives/bookmarks.d.ts.map +1 -1
- package/dist/primitives/bookmarks.js +31 -7
- package/dist/primitives/bookmarks.js.map +1 -1
- package/dist/primitives/comments.d.ts +101 -5
- package/dist/primitives/comments.d.ts.map +1 -1
- package/dist/primitives/comments.js +356 -156
- 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 +145 -2
- package/dist/primitives/document.d.ts.map +1 -1
- package/dist/primitives/document.js +505 -27
- package/dist/primitives/document.js.map +1 -1
- package/dist/primitives/document_view-headings.d.ts +3 -1
- package/dist/primitives/document_view-headings.d.ts.map +1 -1
- package/dist/primitives/document_view-headings.js +5 -5
- package/dist/primitives/document_view-headings.js.map +1 -1
- package/dist/primitives/document_view.d.ts +2 -0
- package/dist/primitives/document_view.d.ts.map +1 -1
- package/dist/primitives/document_view.js +18 -4
- package/dist/primitives/document_view.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/field_evaluation.d.ts +70 -0
- package/dist/primitives/field_evaluation.d.ts.map +1 -0
- package/dist/primitives/field_evaluation.js +462 -0
- package/dist/primitives/field_evaluation.js.map +1 -0
- package/dist/primitives/footnotes.d.ts +53 -3
- package/dist/primitives/footnotes.d.ts.map +1 -1
- package/dist/primitives/footnotes.js +228 -83
- package/dist/primitives/footnotes.js.map +1 -1
- package/dist/primitives/index.d.ts +12 -1
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/index.js +12 -1
- package/dist/primitives/index.js.map +1 -1
- package/dist/primitives/merge_runs.d.ts +3 -1
- package/dist/primitives/merge_runs.d.ts.map +1 -1
- package/dist/primitives/merge_runs.js +12 -2
- package/dist/primitives/merge_runs.js.map +1 -1
- package/dist/primitives/namespaces.d.ts +10 -2
- package/dist/primitives/namespaces.d.ts.map +1 -1
- package/dist/primitives/namespaces.js +11 -2
- package/dist/primitives/namespaces.js.map +1 -1
- package/dist/primitives/note_conversion.d.ts +11 -0
- package/dist/primitives/note_conversion.d.ts.map +1 -0
- package/dist/primitives/note_conversion.js +12 -0
- package/dist/primitives/note_conversion.js.map +1 -0
- 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_numbering.d.ts +40 -0
- package/dist/primitives/paragraph_numbering.d.ts.map +1 -0
- package/dist/primitives/paragraph_numbering.js +198 -0
- package/dist/primitives/paragraph_numbering.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 +6 -0
- package/dist/primitives/reject_changes.d.ts.map +1 -1
- package/dist/primitives/reject_changes.js +278 -68
- 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/sectPrAudit.d.ts.map +1 -1
- package/dist/primitives/sectPrAudit.js +10 -1
- package/dist/primitives/sectPrAudit.js.map +1 -1
- package/dist/primitives/sections.d.ts +135 -0
- package/dist/primitives/sections.d.ts.map +1 -0
- package/dist/primitives/sections.js +644 -0
- package/dist/primitives/sections.js.map +1 -0
- package/dist/primitives/styles.d.ts +61 -0
- package/dist/primitives/styles.d.ts.map +1 -1
- package/dist/primitives/styles.js +207 -25
- package/dist/primitives/styles.js.map +1 -1
- package/dist/primitives/symbol_run_content.d.ts +42 -0
- package/dist/primitives/symbol_run_content.d.ts.map +1 -0
- package/dist/primitives/symbol_run_content.js +79 -0
- package/dist/primitives/symbol_run_content.js.map +1 -0
- 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 +69 -1
- package/dist/primitives/text.d.ts.map +1 -1
- package/dist/primitives/text.js +592 -102
- package/dist/primitives/text.js.map +1 -1
- package/dist/primitives/track-changes-emitter.d.ts +23 -0
- package/dist/primitives/track-changes-emitter.d.ts.map +1 -1
- package/dist/primitives/track-changes-emitter.js +64 -1
- package/dist/primitives/track-changes-emitter.js.map +1 -1
- package/dist/primitives/validate_document.d.ts.map +1 -1
- package/dist/primitives/validate_document.js +22 -1
- package/dist/primitives/validate_document.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 +17 -4
- package/dist/shared/docx/DocxArchive.js.map +1 -1
- package/dist/shared/field-semantics.d.ts +26 -0
- package/dist/shared/field-semantics.d.ts.map +1 -0
- package/dist/shared/field-semantics.js +232 -0
- package/dist/shared/field-semantics.js.map +1 -0
- package/dist/shared/field-structure.d.ts +10 -1
- package/dist/shared/field-structure.d.ts.map +1 -1
- package/dist/shared/field-structure.js +31 -12
- package/dist/shared/field-structure.js.map +1 -1
- 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 {
|
|
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
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
|
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
|
|
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
|
|
622
|
-
|
|
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(
|
|
883
|
+
startById.set(id, {
|
|
625
884
|
paragraphId,
|
|
626
|
-
|
|
885
|
+
textOffset: marker.visibleStart,
|
|
886
|
+
...resolveIndexedMarkerBoundary(index, marker, 'start'),
|
|
627
887
|
});
|
|
628
888
|
}
|
|
629
|
-
for (const marker of
|
|
630
|
-
|
|
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(
|
|
893
|
+
endById.set(id, {
|
|
633
894
|
paragraphId,
|
|
634
|
-
|
|
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
|
|
714
|
-
|
|
715
|
-
}
|
|
716
|
-
|
|
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
|
|
726
|
-
const
|
|
727
|
-
if (boundary === 'start')
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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
|
|
1183
|
+
return result;
|
|
984
1184
|
}
|
|
985
1185
|
//# sourceMappingURL=comments.js.map
|