@usejunior/docx-core 0.9.1 → 0.11.0
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/LICENSE +202 -21
- package/NOTICE +2 -0
- package/README.md +2 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/atomizer.d.ts +28 -8
- package/dist/atomizer.d.ts.map +1 -1
- package/dist/atomizer.js +96 -25
- package/dist/atomizer.js.map +1 -1
- package/dist/baselines/atomizer/auxiliaryIdCollision.d.ts +99 -0
- package/dist/baselines/atomizer/auxiliaryIdCollision.d.ts.map +1 -0
- package/dist/baselines/atomizer/auxiliaryIdCollision.js +415 -0
- package/dist/baselines/atomizer/auxiliaryIdCollision.js.map +1 -0
- package/dist/baselines/atomizer/documentReconstructor.d.ts.map +1 -1
- package/dist/baselines/atomizer/documentReconstructor.js +333 -112
- package/dist/baselines/atomizer/documentReconstructor.js.map +1 -1
- package/dist/baselines/atomizer/formattingFidelity.d.ts +99 -0
- package/dist/baselines/atomizer/formattingFidelity.d.ts.map +1 -0
- package/dist/baselines/atomizer/formattingFidelity.js +449 -0
- package/dist/baselines/atomizer/formattingFidelity.js.map +1 -0
- package/dist/baselines/atomizer/inPlaceModifier-bookmarks.d.ts +37 -0
- package/dist/baselines/atomizer/inPlaceModifier-bookmarks.d.ts.map +1 -0
- package/dist/baselines/atomizer/inPlaceModifier-bookmarks.js +189 -0
- package/dist/baselines/atomizer/inPlaceModifier-bookmarks.js.map +1 -0
- package/dist/baselines/atomizer/inPlaceModifier-containers.d.ts +74 -0
- package/dist/baselines/atomizer/inPlaceModifier-containers.d.ts.map +1 -0
- package/dist/baselines/atomizer/inPlaceModifier-containers.js +171 -0
- package/dist/baselines/atomizer/inPlaceModifier-containers.js.map +1 -0
- package/dist/baselines/atomizer/inPlaceModifier-deletion.d.ts +88 -0
- package/dist/baselines/atomizer/inPlaceModifier-deletion.d.ts.map +1 -0
- package/dist/baselines/atomizer/inPlaceModifier-deletion.js +326 -0
- package/dist/baselines/atomizer/inPlaceModifier-deletion.js.map +1 -0
- package/dist/baselines/atomizer/inPlaceModifier-postprocess.d.ts +85 -0
- package/dist/baselines/atomizer/inPlaceModifier-postprocess.d.ts.map +1 -0
- package/dist/baselines/atomizer/inPlaceModifier-postprocess.js +402 -0
- package/dist/baselines/atomizer/inPlaceModifier-postprocess.js.map +1 -0
- package/dist/baselines/atomizer/inPlaceModifier-presplit.d.ts +39 -0
- package/dist/baselines/atomizer/inPlaceModifier-presplit.d.ts.map +1 -0
- package/dist/baselines/atomizer/inPlaceModifier-presplit.js +265 -0
- package/dist/baselines/atomizer/inPlaceModifier-presplit.js.map +1 -0
- package/dist/baselines/atomizer/inPlaceModifier-shared.d.ts +62 -0
- package/dist/baselines/atomizer/inPlaceModifier-shared.d.ts.map +1 -0
- package/dist/baselines/atomizer/inPlaceModifier-shared.js +139 -0
- package/dist/baselines/atomizer/inPlaceModifier-shared.js.map +1 -0
- package/dist/baselines/atomizer/inPlaceModifier-wrappers.d.ts +198 -0
- package/dist/baselines/atomizer/inPlaceModifier-wrappers.d.ts.map +1 -0
- package/dist/baselines/atomizer/inPlaceModifier-wrappers.js +475 -0
- package/dist/baselines/atomizer/inPlaceModifier-wrappers.js.map +1 -0
- package/dist/baselines/atomizer/inPlaceModifier.d.ts +6 -290
- package/dist/baselines/atomizer/inPlaceModifier.d.ts.map +1 -1
- package/dist/baselines/atomizer/inPlaceModifier.js +23 -1828
- package/dist/baselines/atomizer/inPlaceModifier.js.map +1 -1
- package/dist/baselines/atomizer/pipeline.d.ts +36 -2
- package/dist/baselines/atomizer/pipeline.d.ts.map +1 -1
- package/dist/baselines/atomizer/pipeline.js +216 -144
- package/dist/baselines/atomizer/pipeline.js.map +1 -1
- package/dist/baselines/atomizer/trackChangesAcceptorAst.d.ts.map +1 -1
- package/dist/baselines/atomizer/trackChangesAcceptorAst.js +199 -173
- package/dist/baselines/atomizer/trackChangesAcceptorAst.js.map +1 -1
- package/dist/baselines/wmlcomparer/DotnetCli.d.ts.map +1 -1
- package/dist/baselines/wmlcomparer/DotnetCli.js +7 -0
- package/dist/baselines/wmlcomparer/DotnetCli.js.map +1 -1
- package/dist/cli/compare-two.d.ts.map +1 -1
- package/dist/cli/compare-two.js +3 -1
- package/dist/cli/compare-two.js.map +1 -1
- package/dist/cli/conformance-adapter.d.ts +3 -0
- package/dist/cli/conformance-adapter.d.ts.map +1 -0
- package/dist/cli/conformance-adapter.js +93 -0
- package/dist/cli/conformance-adapter.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +5 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/compare-types.d.ts +197 -0
- package/dist/compare-types.d.ts.map +1 -0
- package/dist/compare-types.js +2 -0
- package/dist/compare-types.js.map +1 -0
- package/dist/core-types.d.ts +5 -1
- package/dist/core-types.d.ts.map +1 -1
- package/dist/core-types.js +5 -1
- package/dist/core-types.js.map +1 -1
- package/dist/footnotes.d.ts +8 -3
- package/dist/footnotes.d.ts.map +1 -1
- package/dist/footnotes.js +8 -3
- package/dist/footnotes.js.map +1 -1
- package/dist/generation/compile.d.ts +21 -0
- package/dist/generation/compile.d.ts.map +1 -0
- package/dist/generation/compile.js +46 -0
- package/dist/generation/compile.js.map +1 -0
- package/dist/generation/context.d.ts +42 -0
- package/dist/generation/context.d.ts.map +1 -0
- package/dist/generation/context.js +65 -0
- package/dist/generation/context.js.map +1 -0
- package/dist/generation/emit/comments-part.d.ts +36 -0
- package/dist/generation/emit/comments-part.d.ts.map +1 -0
- package/dist/generation/emit/comments-part.js +116 -0
- package/dist/generation/emit/comments-part.js.map +1 -0
- package/dist/generation/emit/document-part.d.ts +24 -0
- package/dist/generation/emit/document-part.d.ts.map +1 -0
- package/dist/generation/emit/document-part.js +60 -0
- package/dist/generation/emit/document-part.js.map +1 -0
- package/dist/generation/emit/emit-context.d.ts +26 -0
- package/dist/generation/emit/emit-context.d.ts.map +1 -0
- package/dist/generation/emit/emit-context.js +19 -0
- package/dist/generation/emit/emit-context.js.map +1 -0
- package/dist/generation/emit/header-footer-part.d.ts +23 -0
- package/dist/generation/emit/header-footer-part.d.ts.map +1 -0
- package/dist/generation/emit/header-footer-part.js +57 -0
- package/dist/generation/emit/header-footer-part.js.map +1 -0
- package/dist/generation/emit/numbering-part.d.ts +29 -0
- package/dist/generation/emit/numbering-part.d.ts.map +1 -0
- package/dist/generation/emit/numbering-part.js +100 -0
- package/dist/generation/emit/numbering-part.js.map +1 -0
- package/dist/generation/emit/package-parts.d.ts +24 -0
- package/dist/generation/emit/package-parts.d.ts.map +1 -0
- package/dist/generation/emit/package-parts.js +121 -0
- package/dist/generation/emit/package-parts.js.map +1 -0
- package/dist/generation/emit/paragraph.d.ts +24 -0
- package/dist/generation/emit/paragraph.d.ts.map +1 -0
- package/dist/generation/emit/paragraph.js +63 -0
- package/dist/generation/emit/paragraph.js.map +1 -0
- package/dist/generation/emit/properties.d.ts +34 -0
- package/dist/generation/emit/properties.d.ts.map +1 -0
- package/dist/generation/emit/properties.js +138 -0
- package/dist/generation/emit/properties.js.map +1 -0
- package/dist/generation/emit/run.d.ts +15 -0
- package/dist/generation/emit/run.d.ts.map +1 -0
- package/dist/generation/emit/run.js +71 -0
- package/dist/generation/emit/run.js.map +1 -0
- package/dist/generation/emit/section.d.ts +29 -0
- package/dist/generation/emit/section.d.ts.map +1 -0
- package/dist/generation/emit/section.js +117 -0
- package/dist/generation/emit/section.js.map +1 -0
- package/dist/generation/emit/settings-part.d.ts +13 -0
- package/dist/generation/emit/settings-part.d.ts.map +1 -0
- package/dist/generation/emit/settings-part.js +24 -0
- package/dist/generation/emit/settings-part.js.map +1 -0
- package/dist/generation/emit/styles-part.d.ts +16 -0
- package/dist/generation/emit/styles-part.d.ts.map +1 -0
- package/dist/generation/emit/styles-part.js +80 -0
- package/dist/generation/emit/styles-part.js.map +1 -0
- package/dist/generation/emit/table.d.ts +26 -0
- package/dist/generation/emit/table.d.ts.map +1 -0
- package/dist/generation/emit/table.js +196 -0
- package/dist/generation/emit/table.js.map +1 -0
- package/dist/generation/errors.d.ts +22 -0
- package/dist/generation/errors.d.ts.map +1 -0
- package/dist/generation/errors.js +29 -0
- package/dist/generation/errors.js.map +1 -0
- package/dist/generation/index.d.ts +13 -0
- package/dist/generation/index.d.ts.map +1 -0
- package/dist/generation/index.js +12 -0
- package/dist/generation/index.js.map +1 -0
- package/dist/generation/ordering.d.ts +46 -0
- package/dist/generation/ordering.d.ts.map +1 -0
- package/dist/generation/ordering.js +119 -0
- package/dist/generation/ordering.js.map +1 -0
- package/dist/generation/recipes.d.ts +47 -0
- package/dist/generation/recipes.d.ts.map +1 -0
- package/dist/generation/recipes.js +84 -0
- package/dist/generation/recipes.js.map +1 -0
- package/dist/generation/structural-checks.d.ts +24 -0
- package/dist/generation/structural-checks.d.ts.map +1 -0
- package/dist/generation/structural-checks.js +318 -0
- package/dist/generation/structural-checks.js.map +1 -0
- package/dist/generation/types.d.ts +217 -0
- package/dist/generation/types.d.ts.map +1 -0
- package/dist/generation/types.js +16 -0
- package/dist/generation/types.js.map +1 -0
- package/dist/generation/validate-spec.d.ts +27 -0
- package/dist/generation/validate-spec.d.ts.map +1 -0
- package/dist/generation/validate-spec.js +307 -0
- package/dist/generation/validate-spec.js.map +1 -0
- package/dist/index.d.ts +9 -150
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/integration/generation-probes.d.ts +15 -0
- package/dist/integration/generation-probes.d.ts.map +1 -0
- package/dist/integration/generation-probes.js +84 -0
- package/dist/integration/generation-probes.js.map +1 -0
- package/dist/integration/libreoffice-oracle.d.ts +49 -0
- package/dist/integration/libreoffice-oracle.d.ts.map +1 -0
- package/dist/integration/libreoffice-oracle.js +290 -0
- package/dist/integration/libreoffice-oracle.js.map +1 -0
- package/dist/integration/synthetic-docx-fixture.d.ts +72 -0
- package/dist/integration/synthetic-docx-fixture.d.ts.map +1 -1
- package/dist/integration/synthetic-docx-fixture.js +131 -4
- package/dist/integration/synthetic-docx-fixture.js.map +1 -1
- package/dist/primitives/accept_changes.d.ts +4 -3
- package/dist/primitives/accept_changes.d.ts.map +1 -1
- package/dist/primitives/accept_changes.js +163 -77
- package/dist/primitives/accept_changes.js.map +1 -1
- package/dist/primitives/comments.d.ts +12 -3
- package/dist/primitives/comments.d.ts.map +1 -1
- package/dist/primitives/comments.js +374 -97
- package/dist/primitives/comments.js.map +1 -1
- package/dist/primitives/content_fingerprint.d.ts +29 -0
- package/dist/primitives/content_fingerprint.d.ts.map +1 -0
- package/dist/primitives/content_fingerprint.js +63 -0
- package/dist/primitives/content_fingerprint.js.map +1 -0
- package/dist/primitives/document.d.ts +94 -15
- package/dist/primitives/document.d.ts.map +1 -1
- package/dist/primitives/document.js +373 -36
- package/dist/primitives/document.js.map +1 -1
- package/dist/primitives/document_view-comments.d.ts +18 -0
- package/dist/primitives/document_view-comments.d.ts.map +1 -0
- package/dist/primitives/document_view-comments.js +160 -0
- package/dist/primitives/document_view-comments.js.map +1 -0
- package/dist/primitives/document_view-headings.d.ts +45 -0
- package/dist/primitives/document_view-headings.d.ts.map +1 -0
- package/dist/primitives/document_view-headings.js +247 -0
- package/dist/primitives/document_view-headings.js.map +1 -0
- package/dist/primitives/document_view-styles.d.ts +11 -0
- package/dist/primitives/document_view-styles.d.ts.map +1 -0
- package/dist/primitives/document_view-styles.js +104 -0
- package/dist/primitives/document_view-styles.js.map +1 -0
- package/dist/primitives/document_view-toon.d.ts +37 -0
- package/dist/primitives/document_view-toon.d.ts.map +1 -0
- package/dist/primitives/document_view-toon.js +199 -0
- package/dist/primitives/document_view-toon.js.map +1 -0
- package/dist/primitives/document_view-types.d.ts +152 -0
- package/dist/primitives/document_view-types.d.ts.map +1 -0
- package/dist/primitives/document_view-types.js +2 -0
- package/dist/primitives/document_view-types.js.map +1 -0
- package/dist/primitives/document_view.d.ts +8 -106
- package/dist/primitives/document_view.d.ts.map +1 -1
- package/dist/primitives/document_view.js +153 -312
- package/dist/primitives/document_view.js.map +1 -1
- package/dist/primitives/dom-helpers.d.ts +9 -0
- package/dist/primitives/dom-helpers.d.ts.map +1 -1
- package/dist/primitives/dom-helpers.js +10 -1
- package/dist/primitives/dom-helpers.js.map +1 -1
- package/dist/primitives/footnotes.d.ts +4 -3
- package/dist/primitives/footnotes.d.ts.map +1 -1
- package/dist/primitives/footnotes.js +232 -44
- package/dist/primitives/footnotes.js.map +1 -1
- package/dist/primitives/formatting_tags.d.ts +7 -0
- package/dist/primitives/formatting_tags.d.ts.map +1 -1
- package/dist/primitives/formatting_tags.js +22 -11
- package/dist/primitives/formatting_tags.js.map +1 -1
- package/dist/primitives/index.d.ts +10 -0
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/index.js +9 -0
- package/dist/primitives/index.js.map +1 -1
- package/dist/primitives/layout.d.ts +4 -3
- package/dist/primitives/layout.d.ts.map +1 -1
- package/dist/primitives/layout.js +45 -3
- package/dist/primitives/layout.js.map +1 -1
- package/dist/primitives/merge_runs.d.ts +21 -3
- package/dist/primitives/merge_runs.d.ts.map +1 -1
- package/dist/primitives/merge_runs.js +32 -10
- package/dist/primitives/merge_runs.js.map +1 -1
- package/dist/primitives/minimal_save.d.ts +38 -0
- package/dist/primitives/minimal_save.d.ts.map +1 -0
- package/dist/primitives/minimal_save.js +323 -0
- package/dist/primitives/minimal_save.js.map +1 -0
- package/dist/primitives/namespaces.d.ts +47 -0
- package/dist/primitives/namespaces.d.ts.map +1 -1
- package/dist/primitives/namespaces.js +52 -0
- package/dist/primitives/namespaces.js.map +1 -1
- package/dist/primitives/reject_changes.d.ts +6 -4
- package/dist/primitives/reject_changes.d.ts.map +1 -1
- package/dist/primitives/reject_changes.js +187 -91
- package/dist/primitives/reject_changes.js.map +1 -1
- package/dist/primitives/revision-parts.d.ts +7 -0
- package/dist/primitives/revision-parts.d.ts.map +1 -0
- package/dist/primitives/revision-parts.js +27 -0
- package/dist/primitives/revision-parts.js.map +1 -0
- package/dist/primitives/revision-vocabulary.d.ts +7 -0
- package/dist/primitives/revision-vocabulary.d.ts.map +1 -0
- package/dist/primitives/revision-vocabulary.js +39 -0
- package/dist/primitives/revision-vocabulary.js.map +1 -0
- package/dist/primitives/schema-corpus-capture.d.ts +19 -0
- package/dist/primitives/schema-corpus-capture.d.ts.map +1 -0
- package/dist/primitives/schema-corpus-capture.js +29 -0
- package/dist/primitives/schema-corpus-capture.js.map +1 -0
- package/dist/primitives/sectPrAudit.d.ts +19 -0
- package/dist/primitives/sectPrAudit.d.ts.map +1 -0
- package/dist/primitives/sectPrAudit.js +165 -0
- package/dist/primitives/sectPrAudit.js.map +1 -0
- package/dist/primitives/semantic_tags.d.ts +7 -0
- package/dist/primitives/semantic_tags.d.ts.map +1 -1
- package/dist/primitives/semantic_tags.js +23 -4
- package/dist/primitives/semantic_tags.js.map +1 -1
- package/dist/primitives/serialize_html.d.ts +37 -0
- package/dist/primitives/serialize_html.d.ts.map +1 -0
- package/dist/primitives/serialize_html.js +395 -0
- package/dist/primitives/serialize_html.js.map +1 -0
- package/dist/primitives/serialize_markdown.d.ts +16 -0
- package/dist/primitives/serialize_markdown.d.ts.map +1 -0
- package/dist/primitives/serialize_markdown.js +300 -0
- package/dist/primitives/serialize_markdown.js.map +1 -0
- package/dist/primitives/serialize_plaintext.d.ts +15 -0
- package/dist/primitives/serialize_plaintext.d.ts.map +1 -0
- package/dist/primitives/serialize_plaintext.js +154 -0
- package/dist/primitives/serialize_plaintext.js.map +1 -0
- package/dist/primitives/styles.d.ts +15 -0
- package/dist/primitives/styles.d.ts.map +1 -1
- package/dist/primitives/styles.js +33 -22
- package/dist/primitives/styles.js.map +1 -1
- package/dist/primitives/tables.d.ts.map +1 -1
- package/dist/primitives/tables.js +13 -3
- package/dist/primitives/tables.js.map +1 -1
- package/dist/primitives/text.d.ts +2 -1
- package/dist/primitives/text.d.ts.map +1 -1
- package/dist/primitives/text.js +116 -12
- package/dist/primitives/text.js.map +1 -1
- package/dist/primitives/track-changes-emitter.d.ts +148 -0
- package/dist/primitives/track-changes-emitter.d.ts.map +1 -0
- package/dist/primitives/track-changes-emitter.js +291 -0
- package/dist/primitives/track-changes-emitter.js.map +1 -0
- package/dist/primitives/validate_ai_revisions.d.ts +35 -0
- package/dist/primitives/validate_ai_revisions.d.ts.map +1 -0
- package/dist/primitives/validate_ai_revisions.js +323 -0
- package/dist/primitives/validate_ai_revisions.js.map +1 -0
- package/dist/primitives/xml-helpers.d.ts +29 -0
- package/dist/primitives/xml-helpers.d.ts.map +1 -0
- package/dist/primitives/xml-helpers.js +35 -0
- package/dist/primitives/xml-helpers.js.map +1 -0
- package/dist/primitives/xml.d.ts +5 -0
- package/dist/primitives/xml.d.ts.map +1 -1
- package/dist/primitives/xml.js +5 -0
- package/dist/primitives/xml.js.map +1 -1
- package/dist/primitives/zip.d.ts +1 -0
- package/dist/primitives/zip.d.ts.map +1 -1
- package/dist/primitives/zip.js +21 -3
- package/dist/primitives/zip.js.map +1 -1
- package/dist/shared/field-structure.d.ts +14 -0
- package/dist/shared/field-structure.d.ts.map +1 -0
- package/dist/shared/field-structure.js +166 -0
- package/dist/shared/field-structure.js.map +1 -0
- package/dist/shared/ooxml/namespaces.d.ts +4 -1
- package/dist/shared/ooxml/namespaces.d.ts.map +1 -1
- package/dist/shared/ooxml/namespaces.js +4 -1
- package/dist/shared/ooxml/namespaces.js.map +1 -1
- package/package.json +13 -9
|
@@ -8,9 +8,11 @@ import { XMLSerializer } from '@xmldom/xmldom';
|
|
|
8
8
|
import { parseXml } from '../../primitives/xml.js';
|
|
9
9
|
import { CorrelationStatus } from '../../core-types.js';
|
|
10
10
|
import { getLeafText, childElements, findChildByTagName } from '../../primitives/index.js';
|
|
11
|
+
import { allocateRevisionId, buildPPrChangeElement, convertSerializedDeletionContent, createRevisionContext, createRevisionIdState, escapeXmlAttr, formatDate, wrapSerializedContentWithDel, wrapSerializedContentWithIns, } from '../../primitives/track-changes-emitter.js';
|
|
11
12
|
import { serializeToXml, cloneElement } from './xmlToWmlElement.js';
|
|
12
|
-
import { EMPTY_PARAGRAPH_TAG, isParagraphLevelLeaf } from '../../atomizer.js';
|
|
13
|
+
import { EMPTY_PARAGRAPH_TAG, isParagraphLevelLeaf, nearestHyperlinkAncestor } from '../../atomizer.js';
|
|
13
14
|
import { enforceConsumerCompatibility } from './consumerCompatibility.js';
|
|
15
|
+
import { placeParagraphMarkRevisionMarker } from './inPlaceModifier-wrappers.js';
|
|
14
16
|
import { areRunPropertiesEqual } from '../../format-detection.js';
|
|
15
17
|
import { debug } from './debug.js';
|
|
16
18
|
const SYNTHETIC_DOC = parseXml('<root xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/>');
|
|
@@ -22,21 +24,6 @@ function createEl(tag, attrs) {
|
|
|
22
24
|
el.setAttribute(k, v);
|
|
23
25
|
return el;
|
|
24
26
|
}
|
|
25
|
-
/**
|
|
26
|
-
* Create initial revision ID state.
|
|
27
|
-
*/
|
|
28
|
-
function createRevisionIdState() {
|
|
29
|
-
return {
|
|
30
|
-
nextId: 1,
|
|
31
|
-
moveRangeIds: new Map(),
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Allocate a new revision ID.
|
|
36
|
-
*/
|
|
37
|
-
function allocateRevisionId(state) {
|
|
38
|
-
return state.nextId++;
|
|
39
|
-
}
|
|
40
27
|
/**
|
|
41
28
|
* Get or allocate move range IDs for a move name.
|
|
42
29
|
*/
|
|
@@ -51,12 +38,6 @@ function getMoveRangeIds(state, moveName) {
|
|
|
51
38
|
}
|
|
52
39
|
return ids;
|
|
53
40
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Format date for OOXML (ISO 8601).
|
|
56
|
-
*/
|
|
57
|
-
function formatDate(date) {
|
|
58
|
-
return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
59
|
-
}
|
|
60
41
|
/**
|
|
61
42
|
* Reconstruct document.xml from merged atoms with track changes.
|
|
62
43
|
*
|
|
@@ -343,8 +324,14 @@ function shouldStartNewRunGroup(currentGroup, atom) {
|
|
|
343
324
|
if (currentGroup.moveName !== atom.moveName) {
|
|
344
325
|
return true;
|
|
345
326
|
}
|
|
346
|
-
// Skip rPr splitting for MovedSource/MovedDestination
|
|
347
|
-
//
|
|
327
|
+
// Skip rPr splitting for MovedSource/MovedDestination: every moved run
|
|
328
|
+
// group is wrapped by wrapWithMoveFrom/wrapWithMoveTo, so splitting one
|
|
329
|
+
// move into several groups would emit moveFromRangeStart/End (resp.
|
|
330
|
+
// moveToRangeStart/End) once per slice with the same w:name and range ids.
|
|
331
|
+
// This stays required now that explicit move-range markers atomize: the
|
|
332
|
+
// synthetic-range suppression keyed off those markers is per paragraph, so
|
|
333
|
+
// a detected move in a marker-free paragraph still synthesizes one range
|
|
334
|
+
// pair per moved run group.
|
|
348
335
|
if (currentGroup.status === CorrelationStatus.MovedSource ||
|
|
349
336
|
currentGroup.status === CorrelationStatus.MovedDestination) {
|
|
350
337
|
return false;
|
|
@@ -420,10 +407,28 @@ function isEmptyParagraphGroup(group) {
|
|
|
420
407
|
}
|
|
421
408
|
return group.runGroups.length > 0;
|
|
422
409
|
}
|
|
410
|
+
const NO_EXPLICIT_MOVE_MARKERS = { moveFrom: false, moveTo: false };
|
|
411
|
+
function collectExplicitMoveMarkers(group) {
|
|
412
|
+
let moveFrom = false;
|
|
413
|
+
let moveTo = false;
|
|
414
|
+
for (const runGroup of group.runGroups) {
|
|
415
|
+
for (const atom of runGroup.atoms) {
|
|
416
|
+
const tag = atom.contentElement.tagName;
|
|
417
|
+
if (tag === 'w:moveFromRangeStart' || tag === 'w:moveFromRangeEnd') {
|
|
418
|
+
moveFrom = true;
|
|
419
|
+
}
|
|
420
|
+
else if (tag === 'w:moveToRangeStart' || tag === 'w:moveToRangeEnd') {
|
|
421
|
+
moveTo = true;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return { moveFrom, moveTo };
|
|
426
|
+
}
|
|
423
427
|
/**
|
|
424
428
|
* Build XML for a single paragraph with track changes.
|
|
425
429
|
*/
|
|
426
430
|
function buildParagraphXml(group, author, dateStr, revState) {
|
|
431
|
+
const revisionCtx = createRevisionContext({ author, date: dateStr, idState: revState });
|
|
427
432
|
// Track empty paragraph statuses for debugging
|
|
428
433
|
if (isEmptyParagraphGroup(group)) {
|
|
429
434
|
const status = group.runGroups[0]?.atoms[0]?.correlationStatus;
|
|
@@ -461,33 +466,25 @@ function buildParagraphXml(group, author, dateStr, revState) {
|
|
|
461
466
|
// entirely (instead of leaving behind a stub <w:p> break).
|
|
462
467
|
if (isEntireParagraphWithStatus(group, CorrelationStatus.Inserted)) {
|
|
463
468
|
const paraId = allocateRevisionId(revState);
|
|
464
|
-
const
|
|
465
|
-
|
|
469
|
+
const insertedRunXml = paragraphHasHyperlinkAtoms(group)
|
|
470
|
+
? buildWholeParagraphRevisionContent(group, (runs) => wrapSerializedContentWithIns(runs, revisionCtx))
|
|
471
|
+
: wrapSerializedContentWithIns(group.runGroups.map((runGroup) => buildRunContentAsPlainRun(runGroup)).join(''), revisionCtx);
|
|
472
|
+
const pPrChangeEl = buildPPrChangeElement(group.pPr, revisionCtx);
|
|
466
473
|
const parts = [];
|
|
467
474
|
parts.push('<w:p>');
|
|
468
475
|
parts.push(serializePPrWithParaRevisionMarker(group.pPr, 'w:ins', paraId, author, dateStr, pPrChangeEl));
|
|
469
|
-
parts.push(
|
|
470
|
-
for (const runGroup of group.runGroups) {
|
|
471
|
-
parts.push(buildRunContentAsPlainRun(runGroup));
|
|
472
|
-
}
|
|
473
|
-
parts.push('</w:ins>');
|
|
476
|
+
parts.push(insertedRunXml);
|
|
474
477
|
parts.push('</w:p>');
|
|
475
478
|
return parts.join('');
|
|
476
479
|
}
|
|
477
480
|
if (isEntireParagraphWithStatus(group, CorrelationStatus.Deleted)) {
|
|
478
481
|
const paraId = allocateRevisionId(revState);
|
|
479
|
-
const runId = allocateRevisionId(revState);
|
|
480
482
|
const parts = [];
|
|
481
483
|
parts.push('<w:p>');
|
|
482
484
|
parts.push(serializePPrWithParaRevisionMarker(group.pPr, 'w:del', paraId, author, dateStr));
|
|
483
|
-
parts.push(
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
parts.push(plainRun
|
|
487
|
-
.replace(/<w:t([^>]*)>([^<]*)<\/w:t>/g, '<w:delText$1>$2</w:delText>')
|
|
488
|
-
.replace(/<w:instrText([^>]*)>([^<]*)<\/w:instrText>/g, '<w:delInstrText$1>$2</w:delInstrText>'));
|
|
489
|
-
}
|
|
490
|
-
parts.push('</w:del>');
|
|
485
|
+
parts.push(paragraphHasHyperlinkAtoms(group)
|
|
486
|
+
? buildWholeParagraphRevisionContent(group, (runs) => wrapSerializedContentWithDel(runs, revisionCtx))
|
|
487
|
+
: wrapSerializedContentWithDel(group.runGroups.map((runGroup) => buildRunContentAsPlainRun(runGroup)).join(''), revisionCtx));
|
|
491
488
|
parts.push('</w:p>');
|
|
492
489
|
return parts.join('');
|
|
493
490
|
}
|
|
@@ -496,7 +493,7 @@ function buildParagraphXml(group, author, dateStr, revState) {
|
|
|
496
493
|
// marker inside w:pPr > w:rPr.
|
|
497
494
|
if (isEmptyParagraphWithStatus(group, CorrelationStatus.Inserted)) {
|
|
498
495
|
const paraId = allocateRevisionId(revState);
|
|
499
|
-
const pPrChangeEl = buildPPrChangeElement(group.pPr,
|
|
496
|
+
const pPrChangeEl = buildPPrChangeElement(group.pPr, revisionCtx);
|
|
500
497
|
const pPrXml = serializePPrWithParaRevisionMarker(group.pPr, 'w:ins', paraId, author, dateStr, pPrChangeEl);
|
|
501
498
|
return `<w:p>${pPrXml}</w:p>`;
|
|
502
499
|
}
|
|
@@ -512,10 +509,18 @@ function buildParagraphXml(group, author, dateStr, revState) {
|
|
|
512
509
|
if (group.pPr) {
|
|
513
510
|
parts.push(serializeToXml(group.pPr));
|
|
514
511
|
}
|
|
515
|
-
// Add run groups with track changes
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
512
|
+
// Add run groups with track changes, restoring w:hyperlink wrappers when
|
|
513
|
+
// the paragraph contains hyperlink atoms (issue #368). Hyperlink-free
|
|
514
|
+
// paragraphs keep the legacy per-group emission byte-identical.
|
|
515
|
+
const explicitMoveMarkers = collectExplicitMoveMarkers(group);
|
|
516
|
+
if (paragraphHasHyperlinkAtoms(group)) {
|
|
517
|
+
parts.push(buildRunGroupsWithHyperlinks(group.runGroups, author, dateStr, revState, explicitMoveMarkers));
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
for (const runGroup of group.runGroups) {
|
|
521
|
+
const runXml = buildRunGroupXml(runGroup, author, dateStr, revState, explicitMoveMarkers);
|
|
522
|
+
parts.push(runXml);
|
|
523
|
+
}
|
|
519
524
|
}
|
|
520
525
|
parts.push('</w:p>');
|
|
521
526
|
return parts.join('');
|
|
@@ -543,43 +548,25 @@ function serializePPrWithParaRevisionMarker(pPr, markerTag, id, author, dateStr,
|
|
|
543
548
|
effectivePPr.appendChild(rPr);
|
|
544
549
|
}
|
|
545
550
|
}
|
|
546
|
-
//
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
551
|
+
// Reuse a pre-existing paragraph-mark marker of the same kind cloned from the
|
|
552
|
+
// source pPr (issue #452): CT_ParaRPr allows at most one of each tracked-change
|
|
553
|
+
// child, and the source revision's metadata (author/date/id) outranks a
|
|
554
|
+
// synthetic duplicate. Either way the marker is placed in its schema-correct
|
|
555
|
+
// slot ahead of formatting children.
|
|
556
|
+
const existingMarker = findChildByTagName(rPr, markerTag);
|
|
557
|
+
const marker = existingMarker ??
|
|
558
|
+
createEl(markerTag, {
|
|
559
|
+
'w:id': String(id),
|
|
560
|
+
'w:author': author,
|
|
561
|
+
'w:date': dateStr,
|
|
562
|
+
});
|
|
563
|
+
placeParagraphMarkRevisionMarker(rPr, marker, markerTag);
|
|
553
564
|
// Append pPrChange at end if provided.
|
|
554
565
|
if (pPrChangeEl) {
|
|
555
566
|
effectivePPr.appendChild(pPrChangeEl);
|
|
556
567
|
}
|
|
557
568
|
return serializeToXml(effectivePPr);
|
|
558
569
|
}
|
|
559
|
-
/**
|
|
560
|
-
* Build a `<w:pPrChange>` Element from a pPr DOM element.
|
|
561
|
-
*
|
|
562
|
-
* The child `<w:pPr>` conforms to CT_PPrBase — it excludes w:rPr, w:sectPr,
|
|
563
|
-
* w:rPrChange, and w:pPrChange.
|
|
564
|
-
*/
|
|
565
|
-
function buildPPrChangeElement(pPr, author, dateStr, revState) {
|
|
566
|
-
const id = allocateRevisionId(revState);
|
|
567
|
-
const EXCLUDED = new Set(['w:rPr', 'w:rPrChange', 'w:pPrChange', 'w:sectPr']);
|
|
568
|
-
const pPrChange = createEl('w:pPrChange', {
|
|
569
|
-
'w:id': String(id),
|
|
570
|
-
'w:author': author,
|
|
571
|
-
'w:date': dateStr,
|
|
572
|
-
});
|
|
573
|
-
const oldPPr = createEl('w:pPr');
|
|
574
|
-
if (pPr) {
|
|
575
|
-
for (const child of childElements(pPr)) {
|
|
576
|
-
if (!EXCLUDED.has(child.tagName))
|
|
577
|
-
oldPPr.appendChild(child.cloneNode(true));
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
pPrChange.appendChild(oldPPr);
|
|
581
|
-
return pPrChange;
|
|
582
|
-
}
|
|
583
570
|
/**
|
|
584
571
|
* Returns true if every atom in the paragraph is of the specified status
|
|
585
572
|
* (ignoring EMPTY_PARAGRAPH_TAG markers).
|
|
@@ -638,8 +625,12 @@ function buildRunContentAsPlainRun(group) {
|
|
|
638
625
|
}
|
|
639
626
|
/**
|
|
640
627
|
* Build XML for a run group with appropriate track changes wrapper.
|
|
628
|
+
*
|
|
629
|
+
* `explicitMoveMarkers` reports whether the surrounding paragraph's atom
|
|
630
|
+
* stream already carries explicit moveFromRange / moveToRange markers; moved
|
|
631
|
+
* groups then skip synthetic range emission (see ExplicitMoveMarkers).
|
|
641
632
|
*/
|
|
642
|
-
function buildRunGroupXml(group, author, dateStr, revState) {
|
|
633
|
+
function buildRunGroupXml(group, author, dateStr, revState, explicitMoveMarkers = NO_EXPLICIT_MOVE_MARKERS) {
|
|
643
634
|
const runContent = buildRunContent(group);
|
|
644
635
|
// If run content is empty (e.g., only empty paragraph atoms), return empty string
|
|
645
636
|
// This avoids generating empty track changes wrappers
|
|
@@ -655,9 +646,9 @@ function buildRunGroupXml(group, author, dateStr, revState) {
|
|
|
655
646
|
case CorrelationStatus.Deleted:
|
|
656
647
|
return wrapWithDel(runContent, author, dateStr, revState);
|
|
657
648
|
case CorrelationStatus.MovedSource:
|
|
658
|
-
return wrapWithMoveFrom(runContent, author, dateStr, group.moveName || 'move1', revState);
|
|
649
|
+
return wrapWithMoveFrom(runContent, author, dateStr, group.moveName || 'move1', revState, explicitMoveMarkers.moveFrom);
|
|
659
650
|
case CorrelationStatus.MovedDestination:
|
|
660
|
-
return wrapWithMoveTo(runContent, author, dateStr, group.moveName || 'move1', revState);
|
|
651
|
+
return wrapWithMoveTo(runContent, author, dateStr, group.moveName || 'move1', revState, explicitMoveMarkers.moveTo);
|
|
661
652
|
case CorrelationStatus.FormatChanged:
|
|
662
653
|
// For format changes, we include the run with rPrChange
|
|
663
654
|
return buildFormatChangeRun(group, author, dateStr, revState);
|
|
@@ -713,9 +704,195 @@ function subGroupByRPr(atoms) {
|
|
|
713
704
|
result.push({ rPr: currentRPr, atoms: currentAtoms });
|
|
714
705
|
return result;
|
|
715
706
|
}
|
|
707
|
+
/**
|
|
708
|
+
* Attribute fingerprint of a w:hyperlink element, used to recognize "the
|
|
709
|
+
* same" hyperlink across the original and revised trees (equal/deleted atoms
|
|
710
|
+
* reference the original tree's element, inserted atoms the revised tree's).
|
|
711
|
+
*/
|
|
712
|
+
function hyperlinkKey(el) {
|
|
713
|
+
const parts = [];
|
|
714
|
+
for (let i = 0; i < el.attributes.length; i++) {
|
|
715
|
+
const attr = el.attributes.item(i);
|
|
716
|
+
if (attr.name.startsWith('xmlns'))
|
|
717
|
+
continue;
|
|
718
|
+
parts.push(`${attr.name}=${attr.value}`);
|
|
719
|
+
}
|
|
720
|
+
return parts.sort().join('\u0000');
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Resolve the hyperlink wrapper an atom belongs to, preferring the
|
|
724
|
+
* original-tree element so the re-emitted r:id resolves against the
|
|
725
|
+
* original-based rebuild package: deleted atoms carry original ancestry
|
|
726
|
+
* directly; equal atoms (revised tree) reach it via comparisonUnitAtomBefore.
|
|
727
|
+
*
|
|
728
|
+
* @conformance ECMA-376 edition 5, Part 1 § 17.16.22
|
|
729
|
+
* @see https://github.com/UseJunior/safe-docx/issues/368
|
|
730
|
+
*/
|
|
731
|
+
function resolveHyperlinkForAtom(atom) {
|
|
732
|
+
const own = nearestHyperlinkAncestor(atom);
|
|
733
|
+
if (!own)
|
|
734
|
+
return null;
|
|
735
|
+
if (atom.sourceDocument === 'original') {
|
|
736
|
+
return { element: own, key: hyperlinkKey(own), fromOriginal: true };
|
|
737
|
+
}
|
|
738
|
+
const before = atom.comparisonUnitAtomBefore;
|
|
739
|
+
const beforeHyperlink = before ? nearestHyperlinkAncestor(before) : null;
|
|
740
|
+
// Attribute to the original wrapper only when both trees agree on the
|
|
741
|
+
// hyperlink's attributes. When they differ (e.g. the revision retargeted
|
|
742
|
+
// the link to a new r:id), emitting the original wrapper would pin the
|
|
743
|
+
// still-equal link text to the STALE target in the accepted document —
|
|
744
|
+
// worse than dropping the wrapper. Such atoms fall through to the
|
|
745
|
+
// revised-only policy below instead.
|
|
746
|
+
// TODO(#376): the faithful tracked representation of a retargeted link
|
|
747
|
+
// is delete-old-link + insert-new-link (what Word emits), which needs the
|
|
748
|
+
// hyperlink fingerprint in atom identity so the LCS stops matching text
|
|
749
|
+
// across different link targets.
|
|
750
|
+
if (beforeHyperlink && hyperlinkKey(beforeHyperlink) === hyperlinkKey(own)) {
|
|
751
|
+
return { element: beforeHyperlink, key: hyperlinkKey(beforeHyperlink), fromOriginal: true };
|
|
752
|
+
}
|
|
753
|
+
// Revised-only attribution (purely inserted hyperlink). Emitting its r:id
|
|
754
|
+
// would dangle against the original-based package, so the caller only
|
|
755
|
+
// wraps when the hyperlink carries no relationship reference (anchor-only).
|
|
756
|
+
return { element: own, key: hyperlinkKey(own), fromOriginal: false };
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Whether a resolved hyperlink is safe to re-emit. Original-attributed
|
|
760
|
+
* wrappers always are; revised-only wrappers are safe only without an r:id
|
|
761
|
+
* (internal anchor links), because the rebuild package ships the ORIGINAL
|
|
762
|
+
* document.xml.rels and a revised-only r:id would be a dangling reference
|
|
763
|
+
* (Word treats those as a corrupt package). Revised-only r:id hyperlinks
|
|
764
|
+
* keep today's behavior — content emitted unwrapped.
|
|
765
|
+
*/
|
|
766
|
+
function isEmittableHyperlink(resolved) {
|
|
767
|
+
return resolved.fromOriginal || resolved.element.getAttribute('r:id') === null;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* True when any atom in the paragraph sits inside a w:hyperlink. Gates the
|
|
771
|
+
* hyperlink-aware emission paths so hyperlink-free paragraphs keep the
|
|
772
|
+
* byte-identical legacy output.
|
|
773
|
+
*/
|
|
774
|
+
function paragraphHasHyperlinkAtoms(group) {
|
|
775
|
+
return group.runGroups.some((rg) => rg.atoms.some((atom) => nearestHyperlinkAncestor(atom) !== null));
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Split a RunGroup into contiguous hyperlink-pure sub-groups.
|
|
779
|
+
*
|
|
780
|
+
* Moved groups are returned whole: splitting them would emit
|
|
781
|
+
* moveFromRangeStart/End once per slice, corrupting the move ranges. A move
|
|
782
|
+
* spanning a hyperlink keeps today's unwrapped emission.
|
|
783
|
+
*/
|
|
784
|
+
function splitRunGroupByHyperlink(group) {
|
|
785
|
+
if (group.status === CorrelationStatus.MovedSource ||
|
|
786
|
+
group.status === CorrelationStatus.MovedDestination) {
|
|
787
|
+
return [{ group, hyperlink: null }];
|
|
788
|
+
}
|
|
789
|
+
const segments = [];
|
|
790
|
+
let current = null;
|
|
791
|
+
for (const atom of group.atoms) {
|
|
792
|
+
// Emit-ability is decided per merged bucket, not per atom: an inserted
|
|
793
|
+
// atom inside an otherwise-original hyperlink folds into the adjacent
|
|
794
|
+
// original-attributed bucket via the shared key.
|
|
795
|
+
const resolved = resolveHyperlinkForAtom(atom);
|
|
796
|
+
const key = resolved?.key ?? null;
|
|
797
|
+
if (current && (current.hyperlink?.key ?? null) === key) {
|
|
798
|
+
current.group.atoms.push(atom);
|
|
799
|
+
// Prefer an original-attributed representative within the segment.
|
|
800
|
+
if (resolved?.fromOriginal && current.hyperlink && !current.hyperlink.fromOriginal) {
|
|
801
|
+
current.hyperlink = resolved;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
current = {
|
|
806
|
+
group: { ...group, atoms: [atom] },
|
|
807
|
+
hyperlink: resolved,
|
|
808
|
+
};
|
|
809
|
+
segments.push(current);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return segments;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Serialize the opening tag of a re-emitted w:hyperlink wrapper, copying the
|
|
816
|
+
* source element's attributes verbatim (r:id, w:anchor, w:history, ...).
|
|
817
|
+
*/
|
|
818
|
+
function serializeHyperlinkOpenTag(el) {
|
|
819
|
+
const attrs = [];
|
|
820
|
+
for (let i = 0; i < el.attributes.length; i++) {
|
|
821
|
+
const attr = el.attributes.item(i);
|
|
822
|
+
if (attr.name.startsWith('xmlns'))
|
|
823
|
+
continue;
|
|
824
|
+
attrs.push(` ${attr.name}="${escapeXmlAttr(attr.value)}"`);
|
|
825
|
+
}
|
|
826
|
+
return `<w:hyperlink${attrs.join('')}>`;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Merge adjacent segments that resolve to the same hyperlink fingerprint, so
|
|
830
|
+
* an equal/deleted/inserted sequence inside one link shares one wrapper.
|
|
831
|
+
*/
|
|
832
|
+
function mergeAdjacentHyperlinkSegments(segments) {
|
|
833
|
+
const buckets = [];
|
|
834
|
+
for (const segment of segments) {
|
|
835
|
+
const last = buckets[buckets.length - 1];
|
|
836
|
+
if (last && (last.hyperlink?.key ?? null) === (segment.hyperlink?.key ?? null)) {
|
|
837
|
+
last.groups.push(segment.group);
|
|
838
|
+
if (segment.hyperlink?.fromOriginal && last.hyperlink && !last.hyperlink.fromOriginal) {
|
|
839
|
+
last.hyperlink = segment.hyperlink;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
buckets.push({ hyperlink: segment.hyperlink, groups: [segment.group] });
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return buckets;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Emit a paragraph's run groups with w:hyperlink wrappers restored around the
|
|
850
|
+
* runs whose atoms came from inside a hyperlink. Track-change wrappers nest
|
|
851
|
+
* INSIDE the hyperlink (`<w:hyperlink><w:ins>…`): CT_Hyperlink admits
|
|
852
|
+
* EG_RunLevelElts (w:ins / w:del / range markers), while CT_RunTrackChange
|
|
853
|
+
* does not admit w:hyperlink.
|
|
854
|
+
*
|
|
855
|
+
* @conformance ECMA-376 edition 5, Part 1 § 17.16.22
|
|
856
|
+
* @see https://github.com/UseJunior/safe-docx/issues/368
|
|
857
|
+
*/
|
|
858
|
+
function buildRunGroupsWithHyperlinks(runGroups, author, dateStr, revState, explicitMoveMarkers = NO_EXPLICIT_MOVE_MARKERS) {
|
|
859
|
+
const buckets = mergeAdjacentHyperlinkSegments(runGroups.flatMap(splitRunGroupByHyperlink));
|
|
860
|
+
const parts = [];
|
|
861
|
+
for (const bucket of buckets) {
|
|
862
|
+
const content = bucket.groups
|
|
863
|
+
.map((g) => buildRunGroupXml(g, author, dateStr, revState, explicitMoveMarkers))
|
|
864
|
+
.join('');
|
|
865
|
+
if (!content)
|
|
866
|
+
continue;
|
|
867
|
+
parts.push(bucket.hyperlink && isEmittableHyperlink(bucket.hyperlink)
|
|
868
|
+
? `${serializeHyperlinkOpenTag(bucket.hyperlink.element)}${content}</w:hyperlink>`
|
|
869
|
+
: content);
|
|
870
|
+
}
|
|
871
|
+
return parts.join('');
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Whole-paragraph insert/delete emission with hyperlink wrappers restored.
|
|
875
|
+
* Each bucket gets its own revision wrapper so the hyperlink can stay
|
|
876
|
+
* OUTSIDE the w:ins / w:del (see buildRunGroupsWithHyperlinks).
|
|
877
|
+
*/
|
|
878
|
+
function buildWholeParagraphRevisionContent(group, wrap) {
|
|
879
|
+
const buckets = mergeAdjacentHyperlinkSegments(group.runGroups.flatMap(splitRunGroupByHyperlink));
|
|
880
|
+
const parts = [];
|
|
881
|
+
for (const bucket of buckets) {
|
|
882
|
+
const runs = bucket.groups.map((g) => buildRunContentAsPlainRun(g)).join('');
|
|
883
|
+
if (!runs)
|
|
884
|
+
continue;
|
|
885
|
+
const wrapped = wrap(runs);
|
|
886
|
+
parts.push(bucket.hyperlink && isEmittableHyperlink(bucket.hyperlink)
|
|
887
|
+
? `${serializeHyperlinkOpenTag(bucket.hyperlink.element)}${wrapped}</w:hyperlink>`
|
|
888
|
+
: wrapped);
|
|
889
|
+
}
|
|
890
|
+
return parts.join('');
|
|
891
|
+
}
|
|
716
892
|
/**
|
|
717
893
|
* Returns true when any atom in the group is a paragraph-level marker
|
|
718
|
-
* (commentRange / bookmark
|
|
894
|
+
* (commentRange / bookmark / moveFromRange / moveToRange / perm) that must
|
|
895
|
+
* be emitted outside <w:r>.
|
|
719
896
|
*/
|
|
720
897
|
function groupHasParagraphLevelAtoms(group) {
|
|
721
898
|
for (const atom of group.atoms) {
|
|
@@ -876,38 +1053,49 @@ function buildRunContent(group) {
|
|
|
876
1053
|
* Wrap content with w:ins element.
|
|
877
1054
|
*/
|
|
878
1055
|
function wrapWithIns(content, author, dateStr, revState) {
|
|
879
|
-
|
|
880
|
-
return `<w:ins w:id="${id}" w:author="${escapeXmlAttr(author)}" w:date="${dateStr}">${content}</w:ins>`;
|
|
1056
|
+
return wrapSerializedContentWithIns(content, createRevisionContext({ author, date: dateStr, idState: revState }));
|
|
881
1057
|
}
|
|
882
1058
|
/**
|
|
883
1059
|
* Wrap content with w:del element.
|
|
884
1060
|
*/
|
|
885
1061
|
function wrapWithDel(content, author, dateStr, revState) {
|
|
886
|
-
|
|
887
|
-
// For deletions, convert w:t to w:delText and w:instrText to w:delInstrText
|
|
888
|
-
const delContent = content
|
|
889
|
-
.replace(/<w:t([^>]*)>([^<]*)<\/w:t>/g, '<w:delText$1>$2</w:delText>')
|
|
890
|
-
.replace(/<w:instrText([^>]*)>([^<]*)<\/w:instrText>/g, '<w:delInstrText$1>$2</w:delInstrText>');
|
|
891
|
-
return `<w:del w:id="${id}" w:author="${escapeXmlAttr(author)}" w:date="${dateStr}">${delContent}</w:del>`;
|
|
1062
|
+
return wrapSerializedContentWithDel(content, createRevisionContext({ author, date: dateStr, idState: revState }));
|
|
892
1063
|
}
|
|
893
1064
|
/**
|
|
894
1065
|
* Wrap content with w:moveFrom elements.
|
|
1066
|
+
*
|
|
1067
|
+
* When `suppressRangeMarkers` is true the paragraph's atom stream already
|
|
1068
|
+
* carries explicit w:moveFromRangeStart/End markers (re-emitted by
|
|
1069
|
+
* buildRunContentWithParagraphMarkers), so only the w:moveFrom wrapper is
|
|
1070
|
+
* synthesized — emitting a second range pair would corrupt the move ranges.
|
|
1071
|
+
*
|
|
1072
|
+
* @see https://github.com/UseJunior/safe-docx/issues/110
|
|
895
1073
|
*/
|
|
896
|
-
function wrapWithMoveFrom(content, author, dateStr, moveName, revState) {
|
|
1074
|
+
function wrapWithMoveFrom(content, author, dateStr, moveName, revState, suppressRangeMarkers = false) {
|
|
1075
|
+
if (suppressRangeMarkers) {
|
|
1076
|
+
const moveId = allocateRevisionId(revState);
|
|
1077
|
+
const delContent = convertSerializedDeletionContent(content);
|
|
1078
|
+
return `<w:moveFrom w:id="${moveId}" w:author="${escapeXmlAttr(author)}" w:date="${dateStr}">${delContent}</w:moveFrom>`;
|
|
1079
|
+
}
|
|
897
1080
|
const ids = getMoveRangeIds(revState, moveName);
|
|
898
1081
|
const moveId = allocateRevisionId(revState);
|
|
899
|
-
|
|
900
|
-
const delContent = content
|
|
901
|
-
.replace(/<w:t([^>]*)>([^<]*)<\/w:t>/g, '<w:delText$1>$2</w:delText>')
|
|
902
|
-
.replace(/<w:instrText([^>]*)>([^<]*)<\/w:instrText>/g, '<w:delInstrText$1>$2</w:delInstrText>');
|
|
1082
|
+
const delContent = convertSerializedDeletionContent(content);
|
|
903
1083
|
return (`<w:moveFromRangeStart w:id="${ids.sourceRangeId}" w:name="${moveName}" w:author="${escapeXmlAttr(author)}" w:date="${dateStr}"/>` +
|
|
904
1084
|
`<w:moveFrom w:id="${moveId}" w:author="${escapeXmlAttr(author)}" w:date="${dateStr}">${delContent}</w:moveFrom>` +
|
|
905
1085
|
`<w:moveFromRangeEnd w:id="${ids.sourceRangeId}"/>`);
|
|
906
1086
|
}
|
|
907
1087
|
/**
|
|
908
1088
|
* Wrap content with w:moveTo elements.
|
|
1089
|
+
*
|
|
1090
|
+
* When `suppressRangeMarkers` is true the paragraph's atom stream already
|
|
1091
|
+
* carries explicit w:moveToRangeStart/End markers, so only the w:moveTo
|
|
1092
|
+
* wrapper is synthesized (see wrapWithMoveFrom).
|
|
909
1093
|
*/
|
|
910
|
-
function wrapWithMoveTo(content, author, dateStr, moveName, revState) {
|
|
1094
|
+
function wrapWithMoveTo(content, author, dateStr, moveName, revState, suppressRangeMarkers = false) {
|
|
1095
|
+
if (suppressRangeMarkers) {
|
|
1096
|
+
const moveId = allocateRevisionId(revState);
|
|
1097
|
+
return `<w:moveTo w:id="${moveId}" w:author="${escapeXmlAttr(author)}" w:date="${dateStr}">${content}</w:moveTo>`;
|
|
1098
|
+
}
|
|
911
1099
|
const ids = getMoveRangeIds(revState, moveName);
|
|
912
1100
|
const moveId = allocateRevisionId(revState);
|
|
913
1101
|
return (`<w:moveToRangeStart w:id="${ids.destRangeId}" w:name="${moveName}" w:author="${escapeXmlAttr(author)}" w:date="${dateStr}"/>` +
|
|
@@ -932,7 +1120,13 @@ function buildFormatChangeRun(group, author, dateStr, revState) {
|
|
|
932
1120
|
}
|
|
933
1121
|
}
|
|
934
1122
|
}
|
|
935
|
-
// Add rPrChange with old properties (wrapped in w:rPr per OOXML spec)
|
|
1123
|
+
// Add rPrChange with old properties (wrapped in w:rPr per OOXML spec).
|
|
1124
|
+
// Kept as the original per-child serialization (NOT delegated to
|
|
1125
|
+
// buildRPrChangeElement) to preserve byte-identical output: xmldom emits
|
|
1126
|
+
// inline `xmlns:w="..."` declarations when serializing detached children,
|
|
1127
|
+
// and downstream consumers may pin on that exact serialized form. The
|
|
1128
|
+
// DOM-aware buildRPrChangeElement helper exists for new primitive code
|
|
1129
|
+
// paths (#136 onward).
|
|
936
1130
|
const formatChange = group.atoms[0]?.formatChange;
|
|
937
1131
|
if (formatChange?.oldRunProperties) {
|
|
938
1132
|
const id = allocateRevisionId(revState);
|
|
@@ -1083,15 +1277,44 @@ function buildDocumentPreservingStructure(originalXml, paragraphXmls, paragraphG
|
|
|
1083
1277
|
const slot = slots[i];
|
|
1084
1278
|
slot.parent.removeChild(slot.element);
|
|
1085
1279
|
}
|
|
1086
|
-
// Strip inter-paragraph bookmark/comment
|
|
1087
|
-
// These are bookmarkStart/End, commentRangeStart/End
|
|
1088
|
-
//
|
|
1089
|
-
//
|
|
1090
|
-
//
|
|
1280
|
+
// Strip inter-paragraph bookmark/comment/move-range/permission markers from
|
|
1281
|
+
// the scaffold. These are bookmarkStart/End, commentRangeStart/End,
|
|
1282
|
+
// moveFromRange*/moveToRange*, and permStart/End elements that were siblings
|
|
1283
|
+
// of <w:p> in the original body. The paragraph rebuilder handles its own
|
|
1284
|
+
// bookmark logic, so keeping these orphaned markers causes unmatched
|
|
1285
|
+
// bookmark IDs. Body-level move-range markers are likewise scaffold
|
|
1286
|
+
// remnants: in-paragraph markers travel through the atom stream, and
|
|
1287
|
+
// detected moves synthesize fresh range pairs inside the reconstructed
|
|
1288
|
+
// paragraphs, so a leftover body-level pair would either dangle or double an
|
|
1289
|
+
// emitted range.
|
|
1290
|
+
//
|
|
1291
|
+
// Comment range markers are treated differently: a sibling-level
|
|
1292
|
+
// commentRangeStart/End is the legitimate shape for a comment range that
|
|
1293
|
+
// spans whole paragraphs, and such markers never enter the atom stream
|
|
1294
|
+
// (see isParagraphLevelLeaf in atomizer.ts), so nothing re-emits them.
|
|
1295
|
+
// Stripping them unconditionally destroys multi-paragraph comment ranges
|
|
1296
|
+
// (issue #103). Instead, strip a sibling-level comment range marker only
|
|
1297
|
+
// when its counterpart (same w:id) is absent from the rebuilt body —
|
|
1298
|
+
// i.e., it is a genuinely orphaned scaffold remnant.
|
|
1091
1299
|
const SCAFFOLD_STRIP_TAGS = new Set([
|
|
1092
1300
|
'w:bookmarkStart', 'w:bookmarkEnd',
|
|
1093
1301
|
'w:commentRangeStart', 'w:commentRangeEnd',
|
|
1302
|
+
'w:moveFromRangeStart', 'w:moveFromRangeEnd',
|
|
1303
|
+
'w:moveToRangeStart', 'w:moveToRangeEnd',
|
|
1304
|
+
'w:permStart', 'w:permEnd',
|
|
1094
1305
|
]);
|
|
1306
|
+
const COMMENT_RANGE_TAGS = new Set(['w:commentRangeStart', 'w:commentRangeEnd']);
|
|
1307
|
+
const commentRangeStartIds = new Set();
|
|
1308
|
+
const commentRangeEndIds = new Set();
|
|
1309
|
+
for (const el of Array.from(body.getElementsByTagName('*'))) {
|
|
1310
|
+
const id = el.getAttribute('w:id');
|
|
1311
|
+
if (id == null)
|
|
1312
|
+
continue;
|
|
1313
|
+
if (el.tagName === 'w:commentRangeStart')
|
|
1314
|
+
commentRangeStartIds.add(id);
|
|
1315
|
+
else if (el.tagName === 'w:commentRangeEnd')
|
|
1316
|
+
commentRangeEndIds.add(id);
|
|
1317
|
+
}
|
|
1095
1318
|
const toRemove = [];
|
|
1096
1319
|
for (const el of Array.from(body.getElementsByTagName('*'))) {
|
|
1097
1320
|
if (SCAFFOLD_STRIP_TAGS.has(el.tagName) && el.parentNode) {
|
|
@@ -1105,9 +1328,17 @@ function buildDocumentPreservingStructure(originalXml, paragraphXmls, paragraphG
|
|
|
1105
1328
|
}
|
|
1106
1329
|
ancestor = ancestor.parentNode;
|
|
1107
1330
|
}
|
|
1108
|
-
if (
|
|
1109
|
-
|
|
1331
|
+
if (insideParagraph)
|
|
1332
|
+
continue;
|
|
1333
|
+
if (COMMENT_RANGE_TAGS.has(el.tagName)) {
|
|
1334
|
+
const id = el.getAttribute('w:id');
|
|
1335
|
+
const counterpartIds = el.tagName === 'w:commentRangeStart'
|
|
1336
|
+
? commentRangeEndIds
|
|
1337
|
+
: commentRangeStartIds;
|
|
1338
|
+
if (id != null && counterpartIds.has(id))
|
|
1339
|
+
continue;
|
|
1110
1340
|
}
|
|
1341
|
+
toRemove.push(el);
|
|
1111
1342
|
}
|
|
1112
1343
|
}
|
|
1113
1344
|
for (const el of toRemove) {
|
|
@@ -1175,16 +1406,6 @@ function escapeXmlText(text) {
|
|
|
1175
1406
|
.replace(/</g, '<')
|
|
1176
1407
|
.replace(/>/g, '>');
|
|
1177
1408
|
}
|
|
1178
|
-
/**
|
|
1179
|
-
* Escape XML attribute value.
|
|
1180
|
-
*/
|
|
1181
|
-
function escapeXmlAttr(text) {
|
|
1182
|
-
return text
|
|
1183
|
-
.replace(/&/g, '&')
|
|
1184
|
-
.replace(/</g, '<')
|
|
1185
|
-
.replace(/>/g, '>')
|
|
1186
|
-
.replace(/"/g, '"');
|
|
1187
|
-
}
|
|
1188
1409
|
/**
|
|
1189
1410
|
* Count statistics from merged atoms.
|
|
1190
1411
|
*/
|