@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
|
@@ -6,17 +6,19 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { OOXML, W } from './namespaces.js';
|
|
8
8
|
import { parseXml, serializeXml } from './xml.js';
|
|
9
|
-
import {
|
|
9
|
+
import { buildParagraphIndex } from './paragraph-index.js';
|
|
10
10
|
import { getParagraphBookmarkId } from './bookmarks.js';
|
|
11
11
|
import { findUniqueSubstringMatch } from './matching.js';
|
|
12
12
|
import { childElements, isW } from './dom-helpers.js';
|
|
13
13
|
import { getFirstChild } from './xml-helpers.js';
|
|
14
|
-
import { extractEffectiveRunFormatting, parseStylesXml } from './styles.js';
|
|
14
|
+
import { extractEffectiveRunFormatting, parseStylesXml, parseThemeXml, } from './styles.js';
|
|
15
15
|
import { emitFormattingTags, mergeAdjacentTags } from './formatting_tags.js';
|
|
16
|
+
import { ensureExternalHyperlinkRelationships } from './relationships.js';
|
|
16
17
|
import { createRevisionContainer, prepareElementForDeletion, } from './track-changes-emitter.js';
|
|
17
18
|
// ── Relationship & content types ────────────────────────────────────────
|
|
18
19
|
const REL_TYPE_FOOTNOTES = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes';
|
|
19
20
|
const CT_FOOTNOTES = 'application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml';
|
|
21
|
+
const XMLNS_NS = 'http://www.w3.org/2000/xmlns/';
|
|
20
22
|
// ── Minimal XML template ────────────────────────────────────────────────
|
|
21
23
|
const FOOTNOTES_XML_TEMPLATE = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
22
24
|
`<w:footnotes xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"` +
|
|
@@ -178,8 +180,10 @@ function buildDisplayNumberMap(documentXml, footnotesDoc) {
|
|
|
178
180
|
* When omitted, formatting is read from direct `w:rPr` only — the flattened
|
|
179
181
|
* `text` and the plural anchor map are unaffected either way, so existing
|
|
180
182
|
* callers that pass `(zip, documentXml)` keep their exact behavior.
|
|
183
|
+
*
|
|
184
|
+
* @conformance ECMA-376 edition 5, Part 1 § 17.11.14
|
|
181
185
|
*/
|
|
182
|
-
export async function getFootnotes(zip, documentXml, styles) {
|
|
186
|
+
export async function getFootnotes(zip, documentXml, styles, theme) {
|
|
183
187
|
const footnotesText = await zip.readTextOrNull('word/footnotes.xml');
|
|
184
188
|
if (!footnotesText)
|
|
185
189
|
return [];
|
|
@@ -189,6 +193,8 @@ export async function getFootnotes(zip, documentXml, styles) {
|
|
|
189
193
|
return [];
|
|
190
194
|
const displayMap = buildDisplayNumberMap(documentXml, footnotesDoc);
|
|
191
195
|
const stylesModel = styles ?? parseStylesXml(null);
|
|
196
|
+
const themeText = theme ? null : await zip.readTextOrNull('word/theme/theme1.xml');
|
|
197
|
+
const themeModel = theme ?? parseThemeXml(themeText ? parseXml(themeText) : null);
|
|
192
198
|
// Build map of footnoteReference id → every anchored paragraph bookmark id, in
|
|
193
199
|
// document order (deduplicated). The FIRST entry feeds the legacy
|
|
194
200
|
// `anchoredParagraphId`; the whole ordered list feeds `refParagraphIds`.
|
|
@@ -196,6 +202,7 @@ export async function getFootnotes(zip, documentXml, styles) {
|
|
|
196
202
|
// malformed one has been observed reusing an id across several — so we keep
|
|
197
203
|
// them all rather than silently dropping the extras.
|
|
198
204
|
const anchorMap = new Map();
|
|
205
|
+
const referencePointMap = new Map();
|
|
199
206
|
const refs = documentXml.getElementsByTagNameNS(OOXML.W_NS, W.footnoteReference);
|
|
200
207
|
for (let i = 0; i < refs.length; i++) {
|
|
201
208
|
const ref = refs.item(i);
|
|
@@ -214,6 +221,14 @@ export async function getFootnotes(zip, documentXml, styles) {
|
|
|
214
221
|
if (!existing.includes(bookmarkId))
|
|
215
222
|
existing.push(bookmarkId);
|
|
216
223
|
anchorMap.set(id, existing);
|
|
224
|
+
const index = buildParagraphIndex(pel);
|
|
225
|
+
const indexedReference = index.nodes.find((node) => node.element === ref);
|
|
226
|
+
if (indexedReference) {
|
|
227
|
+
referencePointMap.set(id, [
|
|
228
|
+
...(referencePointMap.get(id) ?? []),
|
|
229
|
+
{ paragraphId: bookmarkId, textOffset: indexedReference.visibleStart },
|
|
230
|
+
]);
|
|
231
|
+
}
|
|
217
232
|
}
|
|
218
233
|
break;
|
|
219
234
|
}
|
|
@@ -229,19 +244,20 @@ export async function getFootnotes(zip, documentXml, styles) {
|
|
|
229
244
|
if (!idStr)
|
|
230
245
|
continue;
|
|
231
246
|
const id = parseInt(idStr, 10);
|
|
232
|
-
const paragraphs = extractFootnoteParagraphs(el, stylesModel);
|
|
247
|
+
const paragraphs = extractFootnoteParagraphs(el, stylesModel, themeModel);
|
|
233
248
|
const text = paragraphs.map((p) => p.text).join('\n');
|
|
234
249
|
const displayNumber = displayMap.get(id) ?? 0;
|
|
235
250
|
const refParagraphIds = anchorMap.get(id) ?? [];
|
|
251
|
+
const referencePoints = referencePointMap.get(id) ?? [];
|
|
236
252
|
const anchoredParagraphId = refParagraphIds[0] ?? null;
|
|
237
|
-
footnotes.push({ id, displayNumber, text, anchoredParagraphId, refParagraphIds, paragraphs });
|
|
253
|
+
footnotes.push({ id, displayNumber, text, anchoredParagraphId, refParagraphIds, referencePoints, paragraphs });
|
|
238
254
|
}
|
|
239
255
|
// Sort by display number (document order)
|
|
240
256
|
footnotes.sort((a, b) => a.displayNumber - b.displayNumber);
|
|
241
257
|
return footnotes;
|
|
242
258
|
}
|
|
243
|
-
export async function getFootnote(zip, documentXml, noteId, styles) {
|
|
244
|
-
const all = await getFootnotes(zip, documentXml, styles);
|
|
259
|
+
export async function getFootnote(zip, documentXml, noteId, styles, theme) {
|
|
260
|
+
const all = await getFootnotes(zip, documentXml, styles, theme);
|
|
245
261
|
return all.find((f) => f.id === noteId) ?? null;
|
|
246
262
|
}
|
|
247
263
|
function extractFootnoteText(footnoteEl) {
|
|
@@ -259,7 +275,7 @@ function extractFootnoteText(footnoteEl) {
|
|
|
259
275
|
* skipped so the footnote number never leaks into the body text. The reserved
|
|
260
276
|
* separator paragraphs are filtered out by the caller (`isReservedFootnote`).
|
|
261
277
|
*/
|
|
262
|
-
function extractFootnoteParagraphs(footnoteEl, styles) {
|
|
278
|
+
function extractFootnoteParagraphs(footnoteEl, styles, theme) {
|
|
263
279
|
const paragraphs = footnoteEl.getElementsByTagNameNS(OOXML.W_NS, W.p);
|
|
264
280
|
const out = [];
|
|
265
281
|
for (let pi = 0; pi < paragraphs.length; pi++) {
|
|
@@ -288,12 +304,18 @@ function extractFootnoteParagraphs(footnoteEl, styles) {
|
|
|
288
304
|
paragraphPPr: paraPPr ?? null,
|
|
289
305
|
paragraphStyleId: style,
|
|
290
306
|
styles,
|
|
307
|
+
theme,
|
|
291
308
|
});
|
|
292
309
|
annotated.push({ text: runText, formatting, hyperlinkUrl: null, charCount: runText.length, isHeaderRun: false });
|
|
293
310
|
}
|
|
294
311
|
// `full` mode: no baseline suppression, so every run's bold/italic/etc.
|
|
295
312
|
// survives into tagged_text at node-level fidelity.
|
|
296
|
-
const tagged = mergeAdjacentTags(emitFormattingTags({
|
|
313
|
+
const tagged = mergeAdjacentTags(emitFormattingTags({
|
|
314
|
+
runs: annotated,
|
|
315
|
+
baseline: FOOTNOTE_TAG_BASELINE,
|
|
316
|
+
fontBaseline: { modalColor: null, colorSuppressed: false, modalFontSizePt: 0, fontSizeSuppressed: true, modalFontName: '', fontNameSuppressed: true },
|
|
317
|
+
formattingMode: 'full',
|
|
318
|
+
}));
|
|
297
319
|
out.push({ text: textParts.join(''), tagged_text: tagged, style });
|
|
298
320
|
}
|
|
299
321
|
return out;
|
|
@@ -311,30 +333,50 @@ function getFootnoteParagraphStyle(p) {
|
|
|
311
333
|
return getWAttr(pStyle, 'val');
|
|
312
334
|
}
|
|
313
335
|
// ── Insertion ───────────────────────────────────────────────────────────
|
|
336
|
+
/**
|
|
337
|
+
* Add one footnote definition and its exact point reference in the main story.
|
|
338
|
+
*
|
|
339
|
+
* @conformance ECMA-376 edition 5, Part 1 § 17.11.14
|
|
340
|
+
*/
|
|
314
341
|
export async function addFootnote(documentXml, zip, params, ctx) {
|
|
315
|
-
const { paragraphEl, afterText, text } = params;
|
|
342
|
+
const { paragraphEl, afterText, visibleOffset, text, presentation } = params;
|
|
343
|
+
if (afterText !== undefined && visibleOffset !== undefined) {
|
|
344
|
+
throw new Error('afterText and visibleOffset are mutually exclusive footnote anchors');
|
|
345
|
+
}
|
|
316
346
|
// Load or bootstrap footnotes.xml
|
|
317
347
|
const footnotesXml = await zip.readText('word/footnotes.xml');
|
|
318
348
|
const footnotesDoc = parseXml(footnotesXml);
|
|
319
349
|
// Allocate next ID
|
|
320
350
|
const noteId = allocateNextFootnoteId(footnotesDoc);
|
|
321
351
|
// Insert footnoteReference run in document body
|
|
322
|
-
insertFootnoteReference(documentXml, paragraphEl, noteId, afterText, ctx);
|
|
352
|
+
insertFootnoteReference(documentXml, paragraphEl, noteId, afterText, visibleOffset, ctx);
|
|
323
353
|
// Add footnote body to footnotes.xml
|
|
324
|
-
const
|
|
354
|
+
const destinations = [
|
|
355
|
+
...(presentation?.prefixRuns ?? []),
|
|
356
|
+
...(presentation?.separatorRuns ?? []),
|
|
357
|
+
...(presentation?.body?.flatMap((paragraph) => paragraph.runs) ?? []),
|
|
358
|
+
].flatMap((run) => run.hyperlink ? [run.hyperlink.destination] : []);
|
|
359
|
+
const hyperlinkRelationshipIds = await ensureExternalHyperlinkRelationships(zip, 'word/footnotes.xml', destinations);
|
|
360
|
+
const footnoteEl = addFootnoteElement(footnotesDoc, noteId, text, presentation, hyperlinkRelationshipIds);
|
|
325
361
|
if (ctx) {
|
|
326
362
|
wrapFootnoteParagraphTextRuns(getFirstFootnoteParagraph(footnoteEl), 'ins', ctx);
|
|
327
363
|
}
|
|
328
364
|
zip.writeText('word/footnotes.xml', serializeXml(footnotesDoc));
|
|
329
365
|
return { noteId };
|
|
330
366
|
}
|
|
331
|
-
function insertFootnoteReference(documentXml, paragraphEl, noteId, afterText, ctx) {
|
|
367
|
+
function insertFootnoteReference(documentXml, paragraphEl, noteId, afterText, requestedVisibleOffset, ctx) {
|
|
332
368
|
// Create the reference run
|
|
333
369
|
const refRun = documentXml.createElementNS(OOXML.W_NS, 'w:r');
|
|
334
370
|
const rPr = documentXml.createElementNS(OOXML.W_NS, 'w:rPr');
|
|
335
371
|
const rStyle = documentXml.createElementNS(OOXML.W_NS, 'w:rStyle');
|
|
336
372
|
rStyle.setAttributeNS(OOXML.W_NS, 'w:val', 'FootnoteReference');
|
|
337
373
|
rPr.appendChild(rStyle);
|
|
374
|
+
// Some source documents omit or redefine the FootnoteReference character
|
|
375
|
+
// style. Keep the semantic style and make the required visual elevation
|
|
376
|
+
// explicit so the marker remains superscript across those documents.
|
|
377
|
+
const vertAlign = documentXml.createElementNS(OOXML.W_NS, 'w:vertAlign');
|
|
378
|
+
vertAlign.setAttributeNS(OOXML.W_NS, 'w:val', 'superscript');
|
|
379
|
+
rPr.appendChild(vertAlign);
|
|
338
380
|
refRun.appendChild(rPr);
|
|
339
381
|
const fnRef = documentXml.createElementNS(OOXML.W_NS, 'w:footnoteReference');
|
|
340
382
|
fnRef.setAttributeNS(OOXML.W_NS, 'w:id', String(noteId));
|
|
@@ -343,44 +385,49 @@ function insertFootnoteReference(documentXml, paragraphEl, noteId, afterText, ct
|
|
|
343
385
|
if (ctx) {
|
|
344
386
|
refAnchor.appendChild(refRun);
|
|
345
387
|
}
|
|
346
|
-
if (
|
|
388
|
+
if (afterText === undefined && requestedVisibleOffset === undefined) {
|
|
347
389
|
// Default: append at end of paragraph
|
|
348
390
|
paragraphEl.appendChild(refAnchor);
|
|
349
391
|
return;
|
|
350
392
|
}
|
|
351
|
-
|
|
352
|
-
const runs =
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
393
|
+
const index = buildParagraphIndex(paragraphEl);
|
|
394
|
+
const runs = index.runs.filter((run) => run.visibleText.length > 0);
|
|
395
|
+
let insertOffset;
|
|
396
|
+
if (requestedVisibleOffset !== undefined) {
|
|
397
|
+
if (!Number.isInteger(requestedVisibleOffset) || requestedVisibleOffset < 0 || requestedVisibleOffset > index.text.length) {
|
|
398
|
+
throw new Error(`visibleOffset ${requestedVisibleOffset} is outside paragraph visible text [0, ${index.text.length}]`);
|
|
399
|
+
}
|
|
400
|
+
insertOffset = requestedVisibleOffset;
|
|
357
401
|
}
|
|
358
|
-
|
|
359
|
-
|
|
402
|
+
else {
|
|
403
|
+
const match = findUniqueSubstringMatch(index.text, afterText);
|
|
404
|
+
if (match.status === 'not_found')
|
|
405
|
+
throw new Error(`after_text '${afterText}' not found in paragraph`);
|
|
406
|
+
if (match.status === 'multiple')
|
|
407
|
+
throw new Error(`after_text '${afterText}' found ${match.matchCount} times in paragraph`);
|
|
408
|
+
insertOffset = match.end;
|
|
360
409
|
}
|
|
361
|
-
// We need to insert after the end of the matched text
|
|
362
|
-
const insertOffset = match.end;
|
|
363
410
|
// Map offset to run position
|
|
364
411
|
let pos = 0;
|
|
365
412
|
for (let i = 0; i < runs.length; i++) {
|
|
366
413
|
const run = runs[i];
|
|
367
|
-
const runEnd = pos + run.
|
|
414
|
+
const runEnd = pos + run.visibleText.length;
|
|
368
415
|
if (insertOffset <= pos) {
|
|
369
416
|
// Insert before this run
|
|
370
|
-
const parent = run.
|
|
371
|
-
parent.insertBefore(refAnchor, run.
|
|
417
|
+
const parent = run.element.parentNode;
|
|
418
|
+
parent.insertBefore(refAnchor, run.element);
|
|
372
419
|
return;
|
|
373
420
|
}
|
|
374
421
|
if (insertOffset > pos && insertOffset < runEnd) {
|
|
375
422
|
// Need to split this run at the offset
|
|
376
423
|
const splitOffset = insertOffset - pos;
|
|
377
|
-
splitRunAndInsertReference(run.
|
|
424
|
+
splitRunAndInsertReference(run.element, splitOffset, refAnchor);
|
|
378
425
|
return;
|
|
379
426
|
}
|
|
380
427
|
if (insertOffset === runEnd) {
|
|
381
428
|
// Insert after this run
|
|
382
|
-
const parent = run.
|
|
383
|
-
parent.insertBefore(refAnchor, run.
|
|
429
|
+
const parent = run.element.parentNode;
|
|
430
|
+
parent.insertBefore(refAnchor, run.element.nextSibling);
|
|
384
431
|
return;
|
|
385
432
|
}
|
|
386
433
|
pos = runEnd;
|
|
@@ -493,8 +540,11 @@ function setXmlSpacePreserve(t, text) {
|
|
|
493
540
|
t.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
|
494
541
|
}
|
|
495
542
|
}
|
|
496
|
-
function addFootnoteElement(footnotesDoc, noteId, text) {
|
|
543
|
+
function addFootnoteElement(footnotesDoc, noteId, text, presentation, hyperlinkRelationshipIds) {
|
|
497
544
|
const root = footnotesDoc.documentElement;
|
|
545
|
+
if (hyperlinkRelationshipIds?.size && root.lookupNamespaceURI('r') !== OOXML.R_NS) {
|
|
546
|
+
root.setAttributeNS(XMLNS_NS, 'xmlns:r', OOXML.R_NS);
|
|
547
|
+
}
|
|
498
548
|
const footnoteEl = footnotesDoc.createElementNS(OOXML.W_NS, 'w:footnote');
|
|
499
549
|
footnoteEl.setAttributeNS(OOXML.W_NS, 'w:id', String(noteId));
|
|
500
550
|
// Word-compatible body skeleton
|
|
@@ -511,6 +561,9 @@ function addFootnoteElement(footnotesDoc, noteId, text) {
|
|
|
511
561
|
const refRStyle = footnotesDoc.createElementNS(OOXML.W_NS, 'w:rStyle');
|
|
512
562
|
refRStyle.setAttributeNS(OOXML.W_NS, 'w:val', 'FootnoteReference');
|
|
513
563
|
refRPr.appendChild(refRStyle);
|
|
564
|
+
const refVertAlign = footnotesDoc.createElementNS(OOXML.W_NS, 'w:vertAlign');
|
|
565
|
+
refVertAlign.setAttributeNS(OOXML.W_NS, 'w:val', 'superscript');
|
|
566
|
+
refRPr.appendChild(refVertAlign);
|
|
514
567
|
refRun.appendChild(refRPr);
|
|
515
568
|
const fnRefEl = footnotesDoc.createElementNS(OOXML.W_NS, 'w:footnoteRef');
|
|
516
569
|
refRun.appendChild(fnRefEl);
|
|
@@ -522,19 +575,99 @@ function addFootnoteElement(footnotesDoc, noteId, text) {
|
|
|
522
575
|
spaceT.appendChild(footnotesDoc.createTextNode(' '));
|
|
523
576
|
spaceRun.appendChild(spaceT);
|
|
524
577
|
p.appendChild(spaceRun);
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
p.appendChild(textRun);
|
|
578
|
+
const prefixRuns = presentation?.prefixRuns
|
|
579
|
+
?? (presentation?.prefix ? [{ text: presentation.prefix, style: presentation.prefixStyle }] : []);
|
|
580
|
+
const separatorRuns = presentation?.separatorRuns
|
|
581
|
+
?? (presentation?.prefixSeparator ? [{ text: presentation.prefixSeparator }] : []);
|
|
582
|
+
appendFootnoteRuns(p, prefixRuns, footnotesDoc, hyperlinkRelationshipIds);
|
|
583
|
+
appendFootnoteRuns(p, separatorRuns, footnotesDoc, hyperlinkRelationshipIds);
|
|
584
|
+
const body = presentation?.body ?? [{ runs: [{ text, style: presentation?.bodyStyle }] }];
|
|
585
|
+
appendFootnoteRuns(p, body[0]?.runs ?? [], footnotesDoc, hyperlinkRelationshipIds);
|
|
534
586
|
footnoteEl.appendChild(p);
|
|
587
|
+
for (const paragraph of body.slice(1)) {
|
|
588
|
+
const bodyParagraph = footnotesDoc.createElementNS(OOXML.W_NS, 'w:p');
|
|
589
|
+
appendFootnoteRuns(bodyParagraph, paragraph.runs, footnotesDoc, hyperlinkRelationshipIds);
|
|
590
|
+
footnoteEl.appendChild(bodyParagraph);
|
|
591
|
+
}
|
|
535
592
|
root.appendChild(footnoteEl);
|
|
536
593
|
return footnoteEl;
|
|
537
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* Emit footnote runs under destination-grouped external hyperlink wrappers.
|
|
597
|
+
*
|
|
598
|
+
* @conformance ECMA-376 edition 5, Part 1 § 17.16.22
|
|
599
|
+
* @see #956
|
|
600
|
+
*/
|
|
601
|
+
function appendFootnoteRuns(paragraph, runs, doc, relationshipIds) {
|
|
602
|
+
let activeDestination;
|
|
603
|
+
let activeHyperlink;
|
|
604
|
+
for (const styledRun of runs) {
|
|
605
|
+
const destination = styledRun.hyperlink?.destination;
|
|
606
|
+
if (!destination) {
|
|
607
|
+
activeDestination = undefined;
|
|
608
|
+
activeHyperlink = undefined;
|
|
609
|
+
paragraph.appendChild(buildStyledTextRun(doc, styledRun.text, styledRun.style));
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const relationshipId = relationshipIds?.get(destination);
|
|
613
|
+
if (!relationshipId)
|
|
614
|
+
throw new Error(`Missing footnote hyperlink relationship for ${destination}`);
|
|
615
|
+
if (activeDestination !== destination || !activeHyperlink) {
|
|
616
|
+
activeDestination = destination;
|
|
617
|
+
activeHyperlink = doc.createElementNS(OOXML.W_NS, 'w:hyperlink');
|
|
618
|
+
activeHyperlink.setAttributeNS(OOXML.R_NS, 'r:id', relationshipId);
|
|
619
|
+
paragraph.appendChild(activeHyperlink);
|
|
620
|
+
}
|
|
621
|
+
activeHyperlink.appendChild(buildStyledTextRun(doc, styledRun.text, styledRun.style));
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function buildStyledTextRun(doc, text, style) {
|
|
625
|
+
const run = doc.createElementNS(OOXML.W_NS, 'w:r');
|
|
626
|
+
if (style && Object.values(style).some((value) => value !== undefined && value !== false)) {
|
|
627
|
+
const rPr = doc.createElementNS(OOXML.W_NS, 'w:rPr');
|
|
628
|
+
const onOff = (name) => {
|
|
629
|
+
const el = doc.createElementNS(OOXML.W_NS, `w:${name}`);
|
|
630
|
+
rPr.appendChild(el);
|
|
631
|
+
};
|
|
632
|
+
if (style.styleId) {
|
|
633
|
+
const rStyle = doc.createElementNS(OOXML.W_NS, 'w:rStyle');
|
|
634
|
+
rStyle.setAttributeNS(OOXML.W_NS, 'w:val', style.styleId);
|
|
635
|
+
rPr.appendChild(rStyle);
|
|
636
|
+
}
|
|
637
|
+
if (style.fontSizeHalfPoints !== undefined) {
|
|
638
|
+
const size = doc.createElementNS(OOXML.W_NS, 'w:sz');
|
|
639
|
+
size.setAttributeNS(OOXML.W_NS, 'w:val', String(style.fontSizeHalfPoints));
|
|
640
|
+
rPr.appendChild(size);
|
|
641
|
+
}
|
|
642
|
+
if (style.bold)
|
|
643
|
+
onOff('b');
|
|
644
|
+
if (style.italic)
|
|
645
|
+
onOff('i');
|
|
646
|
+
if (style.underline) {
|
|
647
|
+
const u = doc.createElementNS(OOXML.W_NS, 'w:u');
|
|
648
|
+
u.setAttributeNS(OOXML.W_NS, 'w:val', 'single');
|
|
649
|
+
rPr.appendChild(u);
|
|
650
|
+
}
|
|
651
|
+
if (style.color) {
|
|
652
|
+
const color = doc.createElementNS(OOXML.W_NS, 'w:color');
|
|
653
|
+
color.setAttributeNS(OOXML.W_NS, 'w:val', style.color);
|
|
654
|
+
rPr.appendChild(color);
|
|
655
|
+
}
|
|
656
|
+
if (style.highlight && style.highlight !== 'none') {
|
|
657
|
+
const highlight = doc.createElementNS(OOXML.W_NS, 'w:highlight');
|
|
658
|
+
highlight.setAttributeNS(OOXML.W_NS, 'w:val', style.highlight);
|
|
659
|
+
rPr.appendChild(highlight);
|
|
660
|
+
}
|
|
661
|
+
run.appendChild(rPr);
|
|
662
|
+
}
|
|
663
|
+
const t = doc.createElementNS(OOXML.W_NS, 'w:t');
|
|
664
|
+
if (text.startsWith(' ') || text.endsWith(' ')) {
|
|
665
|
+
t.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
|
666
|
+
}
|
|
667
|
+
t.appendChild(doc.createTextNode(text));
|
|
668
|
+
run.appendChild(t);
|
|
669
|
+
return run;
|
|
670
|
+
}
|
|
538
671
|
// ── Update ──────────────────────────────────────────────────────────────
|
|
539
672
|
export async function updateFootnoteText(zip, params, ctx) {
|
|
540
673
|
const { noteId, newText } = params;
|
|
@@ -661,78 +794,90 @@ function buildFootnoteTextRuns(doc, text) {
|
|
|
661
794
|
return [spaceRun, textRun];
|
|
662
795
|
}
|
|
663
796
|
function removeFootnoteParagraphTextRuns(paragraph) {
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
for (const run of collected.runs) {
|
|
668
|
-
run.parentNode?.removeChild(run);
|
|
797
|
+
for (const group of collectFootnoteTextRunGroups(paragraph)) {
|
|
798
|
+
for (const run of group.runs)
|
|
799
|
+
group.parent.removeChild(run);
|
|
669
800
|
}
|
|
670
|
-
|
|
801
|
+
cleanupEmptyRunContainers(paragraph);
|
|
671
802
|
}
|
|
803
|
+
/**
|
|
804
|
+
* Wrap every text run of a footnote paragraph in a `w:ins` / `w:del`
|
|
805
|
+
* revision container. Runs are wrapped per parent container, so text that
|
|
806
|
+
* already sits inside a revision wrapper or inside a `w:hyperlink` stays
|
|
807
|
+
* where it is: the new container is created inside that parent, which keeps
|
|
808
|
+
* document order and link identity intact instead of hoisting linked runs
|
|
809
|
+
* out of their hyperlink.
|
|
810
|
+
*
|
|
811
|
+
* @conformance ECMA-376 edition 5, Part 1 § 17.16.22
|
|
812
|
+
* @see #956
|
|
813
|
+
*/
|
|
672
814
|
function wrapFootnoteParagraphTextRuns(paragraph, kind, ctx) {
|
|
673
|
-
const
|
|
674
|
-
if (
|
|
815
|
+
const groups = collectFootnoteTextRunGroups(paragraph);
|
|
816
|
+
if (groups.length === 0)
|
|
675
817
|
return null;
|
|
676
818
|
const doc = paragraph.ownerDocument;
|
|
677
819
|
if (!doc)
|
|
678
820
|
throw new Error('Paragraph has no ownerDocument');
|
|
679
|
-
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
run.
|
|
684
|
-
|
|
821
|
+
let first = null;
|
|
822
|
+
for (const group of groups) {
|
|
823
|
+
const wrapper = createRevisionContainer(doc, kind, ctx);
|
|
824
|
+
group.parent.insertBefore(wrapper, group.runs[0]);
|
|
825
|
+
for (const run of group.runs) {
|
|
826
|
+
group.parent.removeChild(run);
|
|
827
|
+
wrapper.appendChild(kind === 'del' ? prepareElementForDeletion(run) : run);
|
|
828
|
+
}
|
|
829
|
+
first ??= wrapper;
|
|
685
830
|
}
|
|
686
|
-
|
|
687
|
-
return
|
|
831
|
+
cleanupEmptyRunContainers(paragraph);
|
|
832
|
+
return first;
|
|
688
833
|
}
|
|
689
834
|
/**
|
|
690
835
|
* Collect every `<w:r>` descendant of the paragraph that does NOT contain a
|
|
691
|
-
* `footnoteRef` marker
|
|
692
|
-
*
|
|
693
|
-
* third-party documents or prior
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
836
|
+
* `footnoteRef` marker, grouped by immediate parent in document order. The
|
|
837
|
+
* traversal intentionally crosses `<w:ins>` / `<w:del>` wrappers so footnote
|
|
838
|
+
* text already carrying revision history (third-party documents or prior
|
|
839
|
+
* tracked edits in the same session) is still captured by
|
|
840
|
+
* `updateFootnoteText` and `deleteFootnote`, and crosses `<w:hyperlink>`
|
|
841
|
+
* containers so linked text is captured without being detached from its link.
|
|
697
842
|
*/
|
|
698
|
-
function
|
|
699
|
-
const
|
|
843
|
+
function collectFootnoteTextRunGroups(paragraph) {
|
|
844
|
+
const groups = [];
|
|
700
845
|
function visit(parent) {
|
|
701
846
|
for (const child of childElements(parent)) {
|
|
702
847
|
if (isW(child, W.r)) {
|
|
703
|
-
if (
|
|
704
|
-
|
|
705
|
-
|
|
848
|
+
if (runContainsFootnoteRef(child))
|
|
849
|
+
continue;
|
|
850
|
+
const last = groups[groups.length - 1];
|
|
851
|
+
if (last && last.parent === parent)
|
|
852
|
+
last.runs.push(child);
|
|
853
|
+
else
|
|
854
|
+
groups.push({ parent, runs: [child] });
|
|
706
855
|
continue;
|
|
707
856
|
}
|
|
708
|
-
if (isW(child, 'ins') || isW(child, 'del')) {
|
|
857
|
+
if (isW(child, 'ins') || isW(child, 'del') || isW(child, W.hyperlink)) {
|
|
709
858
|
visit(child);
|
|
710
859
|
}
|
|
711
860
|
}
|
|
712
861
|
}
|
|
713
862
|
visit(paragraph);
|
|
714
|
-
|
|
715
|
-
return null;
|
|
716
|
-
const first = collected[0];
|
|
717
|
-
return {
|
|
718
|
-
insertionAnchor: { parent: first.parent, before: first.run },
|
|
719
|
-
runs: collected.map((entry) => entry.run),
|
|
720
|
-
};
|
|
863
|
+
return groups;
|
|
721
864
|
}
|
|
722
865
|
function runContainsFootnoteRef(run) {
|
|
723
866
|
return run.getElementsByTagNameNS(OOXML.W_NS, W.footnoteRef).length > 0;
|
|
724
867
|
}
|
|
725
868
|
/**
|
|
726
869
|
* After detaching runs from their parents, sweep up any now-empty
|
|
727
|
-
* `<w:ins
|
|
728
|
-
*
|
|
729
|
-
* cross-
|
|
870
|
+
* `<w:ins>` / `<w:del>` wrappers and `<w:hyperlink>` containers at any depth
|
|
871
|
+
* so we do not leave orphan containers behind. This pairs with
|
|
872
|
+
* `collectFootnoteTextRunGroups`'s cross-container traversal.
|
|
730
873
|
*/
|
|
731
|
-
function
|
|
732
|
-
for (const child of Array.from(childElements(
|
|
733
|
-
if ((isW(child, 'ins') || isW(child, 'del')
|
|
734
|
-
|
|
735
|
-
|
|
874
|
+
function cleanupEmptyRunContainers(element) {
|
|
875
|
+
for (const child of Array.from(childElements(element))) {
|
|
876
|
+
if (!(isW(child, 'ins') || isW(child, 'del') || isW(child, W.hyperlink)))
|
|
877
|
+
continue;
|
|
878
|
+
cleanupEmptyRunContainers(child);
|
|
879
|
+
if (childElements(child).length === 0)
|
|
880
|
+
element.removeChild(child);
|
|
736
881
|
}
|
|
737
882
|
}
|
|
738
883
|
function isolateReferenceRun(run, ref) {
|