@stll/folio-core 0.33.0 → 0.33.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/dist/ai-edits/apply.d.ts +15 -0
- package/dist/ai-edits/apply.js +135 -24
- package/dist/ai-edits/headless.d.ts +47 -3
- package/dist/ai-edits/headless.js +53 -3
- package/dist/ai-edits/index.d.ts +1 -1
- package/dist/ai-edits/snapshot.d.ts +19 -1
- package/dist/ai-edits/snapshot.js +25 -3
- package/dist/ai-edits/table-geometry.d.ts +56 -0
- package/dist/ai-edits/table-geometry.js +217 -0
- package/dist/ai-edits/table-row-column-mutations.d.ts +34 -16
- package/dist/ai-edits/table-row-column-mutations.js +12 -4
- package/dist/ai-edits/table-template.d.ts +60 -0
- package/dist/ai-edits/table-template.js +176 -0
- package/dist/ai-edits/types.d.ts +7 -0
- package/dist/compare/__fixtures__/body-sequence.d.ts +86 -4
- package/dist/compare/__fixtures__/body-sequence.js +169 -12
- package/dist/compare/compare.d.ts +15 -8
- package/dist/compare/compare.js +138 -34
- package/dist/compare/plan.d.ts +31 -1
- package/dist/compare/plan.js +277 -28
- package/dist/compare/reproducible-package.d.ts +7 -3
- package/dist/compare/reproducible-package.js +20 -3
- package/dist/compare/types.d.ts +20 -3
- package/dist/compare/types.js +13 -1
- package/dist/compare/verification.d.ts +49 -2
- package/dist/compare/verification.js +115 -1
- package/dist/compat/eigenpal.d.ts +5 -5
- package/dist/compat/eigenpal.js +3 -3
- package/dist/document-operations.d.ts +10 -1
- package/dist/document-operations.js +3 -2
- package/dist/docx/appVersionNormalization.d.ts +49 -0
- package/dist/docx/appVersionNormalization.js +74 -0
- package/dist/docx/blockContentParser.js +3 -2
- package/dist/docx/drawingUtils.js +4 -3
- package/dist/docx/hyperlinkParser.d.ts +9 -1
- package/dist/docx/hyperlinkParser.js +19 -13
- package/dist/docx/paraIdRangeNormalization.d.ts +40 -0
- package/dist/docx/paraIdRangeNormalization.js +64 -0
- package/dist/docx/paragraphParser.js +82 -6
- package/dist/docx/revisionIdNormalization.d.ts +15 -1
- package/dist/docx/revisionIdNormalization.js +22 -4
- package/dist/docx/rezip.d.ts +13 -1
- package/dist/docx/rezip.js +80 -21
- package/dist/docx/runParser.js +9 -8
- package/dist/docx/sdtProperties.js +4 -3
- package/dist/docx/selectiveSave.js +7 -6
- package/dist/docx/serializer/commentSerializer.d.ts +1 -1
- package/dist/docx/serializer/commentSerializer.js +43 -19
- package/dist/docx/serializer/documentSerializer.d.ts +3 -1
- package/dist/docx/serializer/documentSerializer.js +42 -78
- package/dist/docx/serializer/fontTableSerializer.js +8 -5
- package/dist/docx/serializer/headerFooterSerializer.d.ts +4 -1
- package/dist/docx/serializer/headerFooterSerializer.js +34 -29
- package/dist/docx/serializer/noteSerializer.js +34 -30
- package/dist/docx/serializer/numberingSerializer.js +8 -4
- package/dist/docx/serializer/paragraphSerializer.js +45 -21
- package/dist/docx/serializer/partNamespaces.d.ts +78 -0
- package/dist/docx/serializer/partNamespaces.js +324 -0
- package/dist/docx/serializer/runSerializer.js +1 -1
- package/dist/docx/serializer/settingsSerializer.js +8 -2
- package/dist/docx/serializer/stylesSerializer.js +8 -5
- package/dist/docx/serializer/tableSerializer.js +107 -27
- package/dist/docx/serializer/themeSerializer.js +10 -2
- package/dist/docx/server/build.d.ts +1 -1
- package/dist/docx/strictValueEncodings.gen.d.ts +15 -0
- package/dist/docx/strictValueEncodings.gen.js +275 -0
- package/dist/docx/tableParser.d.ts +0 -6
- package/dist/docx/tableParser.js +52 -7
- package/dist/docx/transitionalSpelling.d.ts +23 -0
- package/dist/docx/transitionalSpelling.js +36 -0
- package/dist/docx/universalMeasure.d.ts +20 -0
- package/dist/docx/universalMeasure.js +32 -0
- package/dist/docx/verbatimCapture.d.ts +20 -0
- package/dist/docx/verbatimCapture.js +131 -0
- package/dist/docx/vmlImageParser.js +4 -3
- package/dist/docx/watermarkParser.js +4 -3
- package/dist/docx/xmlParser.d.ts +23 -2
- package/dist/docx/xmlParser.js +43 -11
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/model.d.ts +3 -3
- package/dist/model.js +2 -2
- package/dist/prosemirror/commands/comments.js +8 -2
- package/dist/prosemirror/containerFinalParagraph.d.ts +47 -0
- package/dist/prosemirror/containerFinalParagraph.js +92 -0
- package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +15 -4
- package/dist/prosemirror/conversion/toProseDoc.js +12 -3
- package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -0
- package/dist/prosemirror/schema/marks.d.ts +1 -1
- package/dist/prosemirror/schema/nodes.d.ts +22 -0
- package/dist/server.d.ts +1 -1
- package/dist/types/block-id.d.ts +12 -1
- package/dist/types/block-id.js +17 -1
- package/dist/utils/canonicalJson.d.ts +14 -0
- package/dist/utils/canonicalJson.js +19 -0
- package/dist/utils/formatToStyle.d.ts +1 -1
- package/package.json +2 -2
package/dist/compare/plan.d.ts
CHANGED
|
@@ -1,10 +1,40 @@
|
|
|
1
1
|
import { FolioAIEditOperation, FolioAIEditSnapshot } from "../ai-edits/types.js";
|
|
2
|
+
import { TableGeometryPairing } from "../ai-edits/table-geometry.js";
|
|
2
3
|
import { FolioDocumentStoryHandle } from "../ai-edits/headless.js";
|
|
3
4
|
import { CompareChange } from "./types.js";
|
|
4
5
|
//#region src/compare/plan.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* A table the target document already holds, which the operation naming it
|
|
8
|
+
* should place verbatim instead of rebuilding from its cell texts.
|
|
9
|
+
*
|
|
10
|
+
* The plan is pure and sees only block snapshots, so it names the table by the
|
|
11
|
+
* index the snapshot numbers tables with; the caller, which has both
|
|
12
|
+
* documents, resolves the index to the node.
|
|
13
|
+
*/
|
|
14
|
+
type CompareTableTemplateRequest = {
|
|
15
|
+
/** The `insertTable` or `insertTableRow` operation this table belongs to. */
|
|
16
|
+
operationId: string;
|
|
17
|
+
/** Index of the table in the TARGET story. */
|
|
18
|
+
targetTableIndex: number;
|
|
19
|
+
/** Set for a row insertion: which of that table's rows to place. */
|
|
20
|
+
targetRowIndex?: number;
|
|
21
|
+
};
|
|
5
22
|
type CompareStoryPlan = {
|
|
6
23
|
changes: CompareChange[];
|
|
7
24
|
operations: FolioAIEditOperation[];
|
|
25
|
+
/**
|
|
26
|
+
* Where an operation's table comes from. Kept beside the operations rather
|
|
27
|
+
* than inside them because a table node is not JSON, and the serialized
|
|
28
|
+
* operation contract describes a table by its cell texts.
|
|
29
|
+
*/
|
|
30
|
+
tableTemplates: CompareTableTemplateRequest[];
|
|
31
|
+
/**
|
|
32
|
+
* Base cells the alignment put opposite a target cell. Their tables, rows
|
|
33
|
+
* and cells are the ones whose `w:tblPr` / `w:trPr` / `w:tcPr` the caller
|
|
34
|
+
* matches: a table that stayed in place while its widths, shading or header
|
|
35
|
+
* row changed moves no block, so no operation carries the difference.
|
|
36
|
+
*/
|
|
37
|
+
tableGeometryPairings: TableGeometryPairing[];
|
|
8
38
|
};
|
|
9
39
|
type PlanStoryCompareOptions = {
|
|
10
40
|
story: FolioDocumentStoryHandle;
|
|
@@ -19,4 +49,4 @@ type PlanStoryCompareOptions = {
|
|
|
19
49
|
*/
|
|
20
50
|
declare const planStoryCompare: ({ story, baseSnapshot, targetSnapshot, maxOperations }: PlanStoryCompareOptions) => CompareStoryPlan | null;
|
|
21
51
|
//#endregion
|
|
22
|
-
export { CompareStoryPlan, PlanStoryCompareOptions, planStoryCompare };
|
|
52
|
+
export { CompareStoryPlan, CompareTableTemplateRequest, PlanStoryCompareOptions, planStoryCompare };
|
package/dist/compare/plan.js
CHANGED
|
@@ -457,16 +457,8 @@ const isEmptyParagraphNode = (block, snapshot) => {
|
|
|
457
457
|
const anchor = snapshot.anchors[block.id] ?? panic("A comparison snapshot block has no matching anchor", { blockId: block.id });
|
|
458
458
|
return block.text === "" && block.table === void 0 && anchor.to - anchor.from === 2;
|
|
459
459
|
};
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const targetBlocks = targetSnapshot.blocks;
|
|
463
|
-
const baseLast = baseBlocks.at(-1);
|
|
464
|
-
const targetLast = targetBlocks.at(-1);
|
|
465
|
-
const terminalCarrierPair = story.type === "main" && baseLast !== void 0 && isEmptyParagraphNode(baseLast, baseSnapshot) && targetLast !== void 0 && isEmptyParagraphNode(targetLast, targetSnapshot) ? {
|
|
466
|
-
type: "pair",
|
|
467
|
-
baseBlock: baseLast,
|
|
468
|
-
targetBlock: targetLast
|
|
469
|
-
} : null;
|
|
460
|
+
/** Align both stories, optionally holding the two last paragraphs out as a pair. */
|
|
461
|
+
const alignedSteps = (baseBlocks, targetBlocks, terminalCarrierPair) => {
|
|
470
462
|
const alignedBaseBlocks = terminalCarrierPair ? baseBlocks.slice(0, -1) : baseBlocks;
|
|
471
463
|
const alignedTargetBlocks = terminalCarrierPair ? targetBlocks.slice(0, -1) : targetBlocks;
|
|
472
464
|
const steps = [];
|
|
@@ -485,6 +477,65 @@ const buildSteps = ({ story, baseSnapshot, targetSnapshot }) => {
|
|
|
485
477
|
return steps;
|
|
486
478
|
};
|
|
487
479
|
/**
|
|
480
|
+
* Whether an alignment left the story's last paragraph removed with no
|
|
481
|
+
* paragraph of its own container surviving in front of it.
|
|
482
|
+
*
|
|
483
|
+
* That paragraph's mark is the one a story cannot lose, and a deleted mark
|
|
484
|
+
* joins two paragraphs of ONE container, so with nothing of that container in
|
|
485
|
+
* front of it there is no chain a removal could run down. The paragraph then
|
|
486
|
+
* stays whatever the plan says, and the story ends on a blank line the target
|
|
487
|
+
* does not have — unless the words the target ends with are written into it,
|
|
488
|
+
* which is what reserving it for the target's last paragraph does.
|
|
489
|
+
*/
|
|
490
|
+
const terminalCarrierIsStranded = (steps, baseBlocks) => {
|
|
491
|
+
const carrier = baseBlocks.at(-1);
|
|
492
|
+
if (carrier === void 0) return false;
|
|
493
|
+
const removed = new Set(steps.flatMap((step) => step.type === "baseOnly" ? [step.block.id] : []));
|
|
494
|
+
if (!removed.has(carrier.id)) return false;
|
|
495
|
+
const container = containerKeyOf(carrier);
|
|
496
|
+
for (let index = baseBlocks.length - 2; index >= 0; index--) {
|
|
497
|
+
const block = baseBlocks[index];
|
|
498
|
+
if (block === void 0 || containerKeyOf(block) !== container) return true;
|
|
499
|
+
if (!removed.has(block.id)) return false;
|
|
500
|
+
}
|
|
501
|
+
return true;
|
|
502
|
+
};
|
|
503
|
+
/**
|
|
504
|
+
* Whether an alignment ADDED the story's last paragraph with something other
|
|
505
|
+
* than paragraphs between it and the base's.
|
|
506
|
+
*
|
|
507
|
+
* An inserted mark at a container's end has the same problem its deletion
|
|
508
|
+
* does: nothing follows it, so rejecting the addition cannot close the break
|
|
509
|
+
* back over the next paragraph. The applier rotates instead — the paragraph
|
|
510
|
+
* the run was appended after takes the inserted mark, and the last one takes
|
|
511
|
+
* the free mark — and that rotation reaches only across paragraphs. A table
|
|
512
|
+
* among them stops it, and the words the target ends with have to be written
|
|
513
|
+
* into the base's own last paragraph instead.
|
|
514
|
+
*/
|
|
515
|
+
const addedTerminalCarrierIsStranded = (steps, baseBlocks, targetBlocks) => {
|
|
516
|
+
const baseLast = baseBlocks.at(-1);
|
|
517
|
+
const targetLast = targetBlocks.at(-1);
|
|
518
|
+
if (baseLast === void 0 || targetLast === void 0) return false;
|
|
519
|
+
if (!steps.some((step) => step.type === "targetOnly" && step.block.id === targetLast.id)) return false;
|
|
520
|
+
const baseLastStep = steps.findIndex((step) => step.type === "pair" && step.baseBlock.id === baseLast.id || step.type === "baseOnly" && step.block.id === baseLast.id);
|
|
521
|
+
return baseLastStep === -1 || steps.slice(baseLastStep + 1).some((step) => step.type !== "targetOnly");
|
|
522
|
+
};
|
|
523
|
+
const buildSteps = ({ baseSnapshot, targetSnapshot }) => {
|
|
524
|
+
const baseBlocks = baseSnapshot.blocks;
|
|
525
|
+
const targetBlocks = targetSnapshot.blocks;
|
|
526
|
+
const baseLast = baseBlocks.at(-1);
|
|
527
|
+
const targetLast = targetBlocks.at(-1);
|
|
528
|
+
const carrierPair = baseLast !== void 0 && targetLast !== void 0 ? {
|
|
529
|
+
type: "pair",
|
|
530
|
+
baseBlock: baseLast,
|
|
531
|
+
targetBlock: targetLast
|
|
532
|
+
} : null;
|
|
533
|
+
const bothStoriesEndBlank = carrierPair !== null && isEmptyParagraphNode(carrierPair.baseBlock, baseSnapshot) && isEmptyParagraphNode(carrierPair.targetBlock, targetSnapshot);
|
|
534
|
+
const steps = alignedSteps(baseBlocks, targetBlocks, bothStoriesEndBlank ? carrierPair : null);
|
|
535
|
+
if (carrierPair === null || bothStoriesEndBlank || !shareAContainer(carrierPair.baseBlock, carrierPair.targetBlock) || !(terminalCarrierIsStranded(steps, baseBlocks) || addedTerminalCarrierIsStranded(steps, baseBlocks, targetBlocks))) return steps;
|
|
536
|
+
return alignedSteps(baseBlocks, targetBlocks, carrierPair);
|
|
537
|
+
};
|
|
538
|
+
/**
|
|
488
539
|
* Two blocks are in the same container when a paragraph mark between them
|
|
489
540
|
* exists at all: two body paragraphs, or two paragraphs of one table cell. A
|
|
490
541
|
* mark cannot span a cell boundary, so a split or a merge across one is not a
|
|
@@ -701,12 +752,198 @@ const locationOf = (story, block) => block.table ? {
|
|
|
701
752
|
cell: block.table
|
|
702
753
|
} : { story };
|
|
703
754
|
/**
|
|
755
|
+
* The container a paragraph mark ends: the story's body, or one table cell. A
|
|
756
|
+
* mark joins the paragraph it ends with the next paragraph of the SAME
|
|
757
|
+
* container, so two blocks on either side of a boundary are not neighbours for
|
|
758
|
+
* this purpose however adjacent they read.
|
|
759
|
+
*/
|
|
760
|
+
const containerKeyOf = ({ table }) => table ? `t${String(table.tableIndex)}r${String(table.rowIndex)}c${String(table.cellIndex)}` : "body";
|
|
761
|
+
/** Each container's last block, in the order the story holds them. */
|
|
762
|
+
const lastBlockByContainer = (blocks) => {
|
|
763
|
+
const last = /* @__PURE__ */ new Map();
|
|
764
|
+
for (const block of blocks) last.set(containerKeyOf(block), block);
|
|
765
|
+
return last;
|
|
766
|
+
};
|
|
767
|
+
/**
|
|
768
|
+
* Rewrite the plan so a container's final paragraph mark survives the removal
|
|
769
|
+
* of the paragraphs that end it.
|
|
770
|
+
*
|
|
771
|
+
* A deleted paragraph mark means "merge this paragraph into the following
|
|
772
|
+
* one". A container's final paragraph has no following one, so the mark cannot
|
|
773
|
+
* say it: the applier leaves that mark alone, and the removal has to be
|
|
774
|
+
* expressed one paragraph earlier. The carrier is that final paragraph, and
|
|
775
|
+
* every trailing removal resolves onto it.
|
|
776
|
+
*
|
|
777
|
+
* Nothing is inserted where the run was: the chain runs from the last
|
|
778
|
+
* SURVIVING paragraph forward. Its mark goes, each removed paragraph's mark
|
|
779
|
+
* goes with it, and the carrier loses its words and keeps its mark as the
|
|
780
|
+
* paragraph the merged text lands in. A carrier that has no words to lose is
|
|
781
|
+
* not deleted at all: its removal is entirely the marks in front of it, and an
|
|
782
|
+
* operation that would write no revision belongs nowhere in the plan.
|
|
783
|
+
*
|
|
784
|
+
* Something IS inserted there: the inserted paragraphs stand where the removed
|
|
785
|
+
* ones did, and the last of them lands INSIDE the carrier as inserted runs
|
|
786
|
+
* before its kept mark, so the carrier is the target's last paragraph rather
|
|
787
|
+
* than a blank line after it. The other inserted paragraphs carry inserted
|
|
788
|
+
* marks of their own and sit before the carrier. The mark count then works out
|
|
789
|
+
* on its own — one mark deleted for every removed paragraph but the carrier,
|
|
790
|
+
* one inserted for every added paragraph but the last — so the chain is not
|
|
791
|
+
* rotated as well.
|
|
792
|
+
*
|
|
793
|
+
* The carrier keeps its own mark either way, and a paragraph's properties live
|
|
794
|
+
* on its mark, so the merged paragraph ends up with the carrier's. Those
|
|
795
|
+
* properties therefore become the target's, written as `w:pPrChange` so
|
|
796
|
+
* rejecting restores what the carrier had. That is bookkeeping for the merge
|
|
797
|
+
* rather than an edit of its own, so it adds no entry to the change list: what
|
|
798
|
+
* the reader is told is that paragraphs were removed and paragraphs added.
|
|
799
|
+
*/
|
|
800
|
+
const withTrailingDeletionRules = ({ baseSnapshot, targetSnapshot, operations, nextOperationId }) => {
|
|
801
|
+
const plan = [...operations];
|
|
802
|
+
const deletionIndexByBlockId = /* @__PURE__ */ new Map();
|
|
803
|
+
const insertIndexesByAnchor = /* @__PURE__ */ new Map();
|
|
804
|
+
const tableAnchorIds = /* @__PURE__ */ new Set();
|
|
805
|
+
const markedBlockIds = /* @__PURE__ */ new Set();
|
|
806
|
+
for (const [index, operation] of plan.entries()) switch (operation.type) {
|
|
807
|
+
case "deleteBlock":
|
|
808
|
+
deletionIndexByBlockId.set(operation.blockId, index);
|
|
809
|
+
break;
|
|
810
|
+
case "insertBeforeBlock":
|
|
811
|
+
case "insertAfterBlock": {
|
|
812
|
+
const placed = insertIndexesByAnchor.get(operation.blockId) ?? [];
|
|
813
|
+
placed.push(index);
|
|
814
|
+
insertIndexesByAnchor.set(operation.blockId, placed);
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
case "insertTable":
|
|
818
|
+
tableAnchorIds.add(operation.blockId);
|
|
819
|
+
break;
|
|
820
|
+
case "splitBlock":
|
|
821
|
+
case "mergeBlockWithNext":
|
|
822
|
+
markedBlockIds.add(operation.blockId);
|
|
823
|
+
break;
|
|
824
|
+
default: break;
|
|
825
|
+
}
|
|
826
|
+
if (deletionIndexByBlockId.size === 0) return plan;
|
|
827
|
+
const blocks = baseSnapshot.blocks;
|
|
828
|
+
const indexById = new Map(blocks.map((block, index) => [block.id, index]));
|
|
829
|
+
/**
|
|
830
|
+
* Walk back from a container's last block over the run of deleted
|
|
831
|
+
* paragraphs it ends with. A block of another container in between — a
|
|
832
|
+
* table between two body paragraphs, a table nested in a cell — ends the
|
|
833
|
+
* walk: no mark joins across it.
|
|
834
|
+
*/
|
|
835
|
+
const trailingRunOf = (container, carrier) => {
|
|
836
|
+
const blockIds = [];
|
|
837
|
+
const insertIndexes = [];
|
|
838
|
+
let chainStart = null;
|
|
839
|
+
let holdsAnInsertedTable = false;
|
|
840
|
+
let index = indexById.get(carrier.id) ?? panic("A container's last block is not in the snapshot it came from", { blockId: carrier.id });
|
|
841
|
+
for (; index >= 0; index--) {
|
|
842
|
+
const block = blocks[index];
|
|
843
|
+
if (block === void 0 || containerKeyOf(block) !== container) break;
|
|
844
|
+
if (!deletionIndexByBlockId.has(block.id)) {
|
|
845
|
+
chainStart = markedBlockIds.has(block.id) ? null : block;
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
blockIds.push(block.id);
|
|
849
|
+
insertIndexes.push(...insertIndexesByAnchor.get(block.id) ?? []);
|
|
850
|
+
holdsAnInsertedTable ||= tableAnchorIds.has(block.id);
|
|
851
|
+
}
|
|
852
|
+
return {
|
|
853
|
+
blockIds,
|
|
854
|
+
chainStart,
|
|
855
|
+
insertIndexes: insertIndexes.toSorted((left, right) => left - right),
|
|
856
|
+
holdsAnInsertedTable
|
|
857
|
+
};
|
|
858
|
+
};
|
|
859
|
+
const targetLastByContainer = lastBlockByContainer(targetSnapshot.blocks);
|
|
860
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
861
|
+
const appended = [];
|
|
862
|
+
for (const [container, carrier] of lastBlockByContainer(blocks)) {
|
|
863
|
+
const carrierDeletionIndex = deletionIndexByBlockId.get(carrier.id);
|
|
864
|
+
if (carrierDeletionIndex === void 0) continue;
|
|
865
|
+
const run = trailingRunOf(container, carrier);
|
|
866
|
+
if (run.holdsAnInsertedTable) continue;
|
|
867
|
+
const lastInsertIndex = run.insertIndexes.at(-1);
|
|
868
|
+
if (lastInsertIndex === void 0) {
|
|
869
|
+
if (run.chainStart) appended.push({
|
|
870
|
+
id: nextOperationId(),
|
|
871
|
+
type: "mergeBlockWithNext",
|
|
872
|
+
blockId: run.chainStart.id
|
|
873
|
+
});
|
|
874
|
+
if (isEmptyParagraphNode(carrier, baseSnapshot)) dropped.add(carrierDeletionIndex);
|
|
875
|
+
} else {
|
|
876
|
+
const lastInsert = plan[lastInsertIndex];
|
|
877
|
+
if (lastInsert === void 0 || lastInsert.type !== "insertBeforeBlock" && lastInsert.type !== "insertAfterBlock" || lastInsert.moveId !== void 0) continue;
|
|
878
|
+
const carrierDeletion = plan[carrierDeletionIndex];
|
|
879
|
+
if (carrierDeletion?.type !== "deleteBlock" || carrierDeletion.moveId !== void 0) continue;
|
|
880
|
+
dropped.add(lastInsertIndex);
|
|
881
|
+
if (carrier.text === lastInsert.text) dropped.add(carrierDeletionIndex);
|
|
882
|
+
else plan[carrierDeletionIndex] = {
|
|
883
|
+
id: carrierDeletion.id,
|
|
884
|
+
type: "replaceBlock",
|
|
885
|
+
blockId: carrier.id,
|
|
886
|
+
text: lastInsert.text
|
|
887
|
+
};
|
|
888
|
+
for (const index of run.insertIndexes.slice(0, -1)) {
|
|
889
|
+
const insert = plan[index];
|
|
890
|
+
if (insert?.type !== "insertBeforeBlock" && insert?.type !== "insertAfterBlock") continue;
|
|
891
|
+
plan[index] = {
|
|
892
|
+
id: insert.id,
|
|
893
|
+
type: "insertBeforeBlock",
|
|
894
|
+
blockId: carrier.id,
|
|
895
|
+
text: insert.text,
|
|
896
|
+
...insert.moveId !== void 0 && { moveId: insert.moveId },
|
|
897
|
+
styleId: insert.styleId ?? null,
|
|
898
|
+
listLevel: insert.listLevel ?? null
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const targetCarrier = targetLastByContainer.get(container);
|
|
903
|
+
const properties = targetCarrier && changedParagraphProperties(carrier, targetCarrier);
|
|
904
|
+
if (properties) appended.push({
|
|
905
|
+
id: nextOperationId(),
|
|
906
|
+
type: "setBlockParagraphProperties",
|
|
907
|
+
blockId: carrier.id,
|
|
908
|
+
properties
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
return [...plan.filter((_, index) => !dropped.has(index)), ...appended];
|
|
912
|
+
};
|
|
913
|
+
const cellCoordinate = ({ tableIndex, rowIndex, cellIndex }) => ({
|
|
914
|
+
tableIndex,
|
|
915
|
+
rowIndex,
|
|
916
|
+
cellIndex
|
|
917
|
+
});
|
|
918
|
+
/**
|
|
919
|
+
* The cells the alignment paired, one entry each. A cell holds several
|
|
920
|
+
* paragraphs and each pairs on its own, so the first pairing of a cell is the
|
|
921
|
+
* one kept: later ones would name the same two cells.
|
|
922
|
+
*/
|
|
923
|
+
const tableGeometryPairingsOf = (steps) => {
|
|
924
|
+
const pairings = [];
|
|
925
|
+
const seen = /* @__PURE__ */ new Set();
|
|
926
|
+
for (const step of steps) {
|
|
927
|
+
if (step.type !== "pair") continue;
|
|
928
|
+
const base = step.baseBlock.table;
|
|
929
|
+
const target = step.targetBlock.table;
|
|
930
|
+
if (!base || !target) continue;
|
|
931
|
+
const key = `${String(base.tableIndex)}:${String(base.rowIndex)}:${String(base.cellIndex)}`;
|
|
932
|
+
if (seen.has(key)) continue;
|
|
933
|
+
seen.add(key);
|
|
934
|
+
pairings.push({
|
|
935
|
+
base: cellCoordinate(base),
|
|
936
|
+
target: cellCoordinate(target)
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
return pairings;
|
|
940
|
+
};
|
|
941
|
+
/**
|
|
704
942
|
* Plan one story's comparison, or `null` when it needs more operations than
|
|
705
943
|
* `maxOperations`.
|
|
706
944
|
*/
|
|
707
945
|
const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations }) => {
|
|
708
946
|
const steps = buildSteps({
|
|
709
|
-
story,
|
|
710
947
|
baseSnapshot,
|
|
711
948
|
targetSnapshot
|
|
712
949
|
});
|
|
@@ -718,6 +955,7 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations }
|
|
|
718
955
|
const anchorIds = nextBaseBlockIdByStep(steps);
|
|
719
956
|
const changes = [];
|
|
720
957
|
const operations = [];
|
|
958
|
+
const tableTemplates = [];
|
|
721
959
|
/**
|
|
722
960
|
* The anchor everything past the base document's content hangs from: its
|
|
723
961
|
* last BODY-LEVEL paragraph, which the format guarantees exists because a
|
|
@@ -980,23 +1218,20 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations }
|
|
|
980
1218
|
targetBlockIds: step.blocks.map(({ id }) => id)
|
|
981
1219
|
});
|
|
982
1220
|
const before = anchorIds[stepIndex] ?? null;
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
position: "before",
|
|
989
|
-
rows
|
|
990
|
-
});
|
|
991
|
-
break;
|
|
992
|
-
}
|
|
993
|
-
if (tailAnchorId !== null) operations.push({
|
|
994
|
-
id: nextOperationId(),
|
|
1221
|
+
const anchorBlockId = before ?? tailAnchorId;
|
|
1222
|
+
if (anchorBlockId === null) break;
|
|
1223
|
+
const operationId = nextOperationId();
|
|
1224
|
+
operations.push({
|
|
1225
|
+
id: operationId,
|
|
995
1226
|
type: "insertTable",
|
|
996
|
-
blockId:
|
|
997
|
-
position: "after",
|
|
1227
|
+
blockId: anchorBlockId,
|
|
1228
|
+
position: before === null ? "after" : "before",
|
|
998
1229
|
rows
|
|
999
1230
|
});
|
|
1231
|
+
tableTemplates.push({
|
|
1232
|
+
operationId,
|
|
1233
|
+
targetTableIndex: step.location.tableIndex
|
|
1234
|
+
});
|
|
1000
1235
|
break;
|
|
1001
1236
|
}
|
|
1002
1237
|
case "targetRow": {
|
|
@@ -1017,13 +1252,19 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations }
|
|
|
1017
1252
|
for (const block of step.blocks) pushInsertOperation(block, anchorIds[stepIndex] ?? null);
|
|
1018
1253
|
break;
|
|
1019
1254
|
}
|
|
1255
|
+
const rowOperationId = nextOperationId();
|
|
1020
1256
|
operations.push({
|
|
1021
|
-
id:
|
|
1257
|
+
id: rowOperationId,
|
|
1022
1258
|
type: "insertTableRow",
|
|
1023
1259
|
blockId: anchor.blockId,
|
|
1024
1260
|
position: anchor.position,
|
|
1025
1261
|
cellTexts: cells
|
|
1026
1262
|
});
|
|
1263
|
+
tableTemplates.push({
|
|
1264
|
+
operationId: rowOperationId,
|
|
1265
|
+
targetTableIndex: step.location.tableIndex,
|
|
1266
|
+
targetRowIndex: step.location.rowIndex
|
|
1267
|
+
});
|
|
1027
1268
|
break;
|
|
1028
1269
|
}
|
|
1029
1270
|
case "targetColumn":
|
|
@@ -1050,9 +1291,17 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations }
|
|
|
1050
1291
|
}
|
|
1051
1292
|
if (operations.length > maxOperations) return null;
|
|
1052
1293
|
}
|
|
1053
|
-
|
|
1294
|
+
const planned = withTrailingDeletionRules({
|
|
1295
|
+
baseSnapshot,
|
|
1296
|
+
targetSnapshot,
|
|
1297
|
+
operations,
|
|
1298
|
+
nextOperationId
|
|
1299
|
+
});
|
|
1300
|
+
return planned.length > maxOperations ? null : {
|
|
1054
1301
|
changes,
|
|
1055
|
-
operations
|
|
1302
|
+
operations: planned,
|
|
1303
|
+
tableTemplates,
|
|
1304
|
+
tableGeometryPairings: tableGeometryPairingsOf(steps)
|
|
1056
1305
|
};
|
|
1057
1306
|
};
|
|
1058
1307
|
//#endregion
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/compare/reproducible-package.d.ts
|
|
2
2
|
/**
|
|
3
|
-
* The last
|
|
3
|
+
* The last two clocks in the compare path are outside the document body.
|
|
4
4
|
*
|
|
5
5
|
* Every part the serializer rewrites is stored with JSZip's default entry
|
|
6
6
|
* date, which is `new Date()`. The XML is identical between two runs, but the
|
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
* both runs land in the same two-second bucket — so the packages match most of
|
|
9
9
|
* the time and differ occasionally, which is worse than differing always.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* The save also stamps `dcterms:modified` in `docProps/core.xml` from the wall
|
|
12
|
+
* clock, which is the same failure one part deeper: two runs over identical
|
|
13
|
+
* inputs differ in that part alone.
|
|
14
|
+
*
|
|
15
|
+
* Restamping both from the comparison's own timestamp removes them. It also
|
|
16
|
+
* states the truth about the package: a generated redline is dated by the
|
|
13
17
|
* comparison that produced it, not by the second it happened to be written.
|
|
14
18
|
*/
|
|
15
19
|
declare const withFixedPackageDates: (buffer: ArrayBuffer, date: Date) => Promise<ArrayBuffer>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import JSZip from "jszip";
|
|
2
2
|
//#region src/compare/reproducible-package.ts
|
|
3
3
|
/**
|
|
4
|
-
* The last
|
|
4
|
+
* The last two clocks in the compare path are outside the document body.
|
|
5
5
|
*
|
|
6
6
|
* Every part the serializer rewrites is stored with JSZip's default entry
|
|
7
7
|
* date, which is `new Date()`. The XML is identical between two runs, but the
|
|
@@ -9,14 +9,31 @@ import JSZip from "jszip";
|
|
|
9
9
|
* both runs land in the same two-second bucket — so the packages match most of
|
|
10
10
|
* the time and differ occasionally, which is worse than differing always.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* The save also stamps `dcterms:modified` in `docProps/core.xml` from the wall
|
|
13
|
+
* clock, which is the same failure one part deeper: two runs over identical
|
|
14
|
+
* inputs differ in that part alone.
|
|
15
|
+
*
|
|
16
|
+
* Restamping both from the comparison's own timestamp removes them. It also
|
|
17
|
+
* states the truth about the package: a generated redline is dated by the
|
|
14
18
|
* comparison that produced it, not by the second it happened to be written.
|
|
15
19
|
*/
|
|
16
20
|
/** JSZip deflate level `repackDocx` writes DOCX parts at. */
|
|
17
21
|
const DOCX_COMPRESSION_LEVEL = 6;
|
|
22
|
+
const CORE_PROPERTIES_PATH = "docProps/core.xml";
|
|
23
|
+
const MODIFIED_ELEMENT = /<dcterms:modified[^<>]*>[^<]*<\/dcterms:modified>/u;
|
|
24
|
+
/**
|
|
25
|
+
* Rewrite `dcterms:modified` where the save already wrote one. An absent
|
|
26
|
+
* element stays absent: the comparison edits the document it was handed, and
|
|
27
|
+
* synthesizing metadata the input never carried is a different decision.
|
|
28
|
+
*/
|
|
29
|
+
const withFixedModifiedDate = (corePropsXml, date) => corePropsXml.replace(MODIFIED_ELEMENT, `<dcterms:modified xsi:type="dcterms:W3CDTF">${date.toISOString()}</dcterms:modified>`);
|
|
18
30
|
const withFixedPackageDates = async (buffer, date) => {
|
|
19
31
|
const zip = await JSZip.loadAsync(buffer);
|
|
32
|
+
const coreProps = zip.file(CORE_PROPERTIES_PATH);
|
|
33
|
+
if (coreProps) zip.file(CORE_PROPERTIES_PATH, withFixedModifiedDate(await coreProps.async("text"), date), {
|
|
34
|
+
compression: "DEFLATE",
|
|
35
|
+
compressionOptions: { level: DOCX_COMPRESSION_LEVEL }
|
|
36
|
+
});
|
|
20
37
|
zip.forEach((_path, file) => {
|
|
21
38
|
file.date = date;
|
|
22
39
|
});
|
package/dist/compare/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FolioAIBlockParagraphProperties, FolioAIBlockTableLocation, FolioAIEditSkippedOperation, FolioAIInlineFormatting } from "../ai-edits/types.js";
|
|
2
2
|
import { WordDiffGranularity } from "../ai-edits/word-diff.js";
|
|
3
3
|
import { FolioDocumentStoryHandle, FolioNumberingLevel } from "../ai-edits/headless.js";
|
|
4
|
-
import { CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant } from "./verification.js";
|
|
4
|
+
import { CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant, FinalParagraphMarkRevision } from "./verification.js";
|
|
5
5
|
//#region src/compare/types.d.ts
|
|
6
6
|
/** Everything {@link compareDocx} needs; nothing it reads from the ambient clock. */
|
|
7
7
|
type CompareDocxOptions = {
|
|
@@ -277,6 +277,23 @@ declare class CompareDocxSerializeError extends CompareDocxSerializeError_base<{
|
|
|
277
277
|
message: string;
|
|
278
278
|
cause: unknown;
|
|
279
279
|
}> {}
|
|
280
|
-
|
|
280
|
+
declare const CompareDocxFinalParagraphMarkError_base: import("better-result").TaggedErrorClass<"CompareDocxFinalParagraphMarkError">;
|
|
281
|
+
/**
|
|
282
|
+
* A container's final paragraph mark carries a revision, so the package would
|
|
283
|
+
* not open, or would open carrying one no reader can resolve.
|
|
284
|
+
*
|
|
285
|
+
* A deleted paragraph mark means "merge this paragraph into the following
|
|
286
|
+
* one", an inserted one means the break was added and rejecting it closes the
|
|
287
|
+
* paragraph back over the next one, and a container's last paragraph has no
|
|
288
|
+
* following one either way. Checked before the package is written, and fatal
|
|
289
|
+
* under either `onUnverified` setting: there is no redline to emit when a
|
|
290
|
+
* consumer refuses the file.
|
|
291
|
+
*/
|
|
292
|
+
declare class CompareDocxFinalParagraphMarkError extends CompareDocxFinalParagraphMarkError_base<{
|
|
293
|
+
message: string;
|
|
294
|
+
/** Every container that carries one, each named structurally. */
|
|
295
|
+
revisions: readonly FinalParagraphMarkRevision[];
|
|
296
|
+
}> {}
|
|
297
|
+
type CompareDocxError = CompareDocxApplyError | CompareDocxFinalParagraphMarkError | CompareDocxOperationLimitError | CompareDocxParseError | CompareDocxRoundTripError | CompareDocxSerializeError | InvalidCompareDocxOptionsError;
|
|
281
298
|
//#endregion
|
|
282
|
-
export { COMPARE_UNSUPPORTED_REASONS, CompareChange, CompareChangeLocation, CompareDocxApplyError, CompareDocxError, CompareDocxOperationLimitError, CompareDocxOptions, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, CompareFormatRange, CompareResult, CompareUnsupportedPart, CompareUnsupportedReason, InvalidCompareDocxOptionsError };
|
|
299
|
+
export { COMPARE_UNSUPPORTED_REASONS, CompareChange, CompareChangeLocation, CompareDocxApplyError, CompareDocxError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxOptions, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, CompareFormatRange, CompareResult, CompareUnsupportedPart, CompareUnsupportedReason, InvalidCompareDocxOptionsError };
|
package/dist/compare/types.js
CHANGED
|
@@ -28,5 +28,17 @@ var CompareDocxRoundTripError = class extends TaggedError("CompareDocxRoundTripE
|
|
|
28
28
|
/** The difference needs more operations than the engine will generate. */
|
|
29
29
|
var CompareDocxOperationLimitError = class extends TaggedError("CompareDocxOperationLimitError") {};
|
|
30
30
|
var CompareDocxSerializeError = class extends TaggedError("CompareDocxSerializeError") {};
|
|
31
|
+
/**
|
|
32
|
+
* A container's final paragraph mark carries a revision, so the package would
|
|
33
|
+
* not open, or would open carrying one no reader can resolve.
|
|
34
|
+
*
|
|
35
|
+
* A deleted paragraph mark means "merge this paragraph into the following
|
|
36
|
+
* one", an inserted one means the break was added and rejecting it closes the
|
|
37
|
+
* paragraph back over the next one, and a container's last paragraph has no
|
|
38
|
+
* following one either way. Checked before the package is written, and fatal
|
|
39
|
+
* under either `onUnverified` setting: there is no redline to emit when a
|
|
40
|
+
* consumer refuses the file.
|
|
41
|
+
*/
|
|
42
|
+
var CompareDocxFinalParagraphMarkError = class extends TaggedError("CompareDocxFinalParagraphMarkError") {};
|
|
31
43
|
//#endregion
|
|
32
|
-
export { COMPARE_UNSUPPORTED_REASONS, CompareDocxApplyError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, InvalidCompareDocxOptionsError };
|
|
44
|
+
export { COMPARE_UNSUPPORTED_REASONS, CompareDocxApplyError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, InvalidCompareDocxOptionsError };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FolioAIBlock } from "../ai-edits/types.js";
|
|
2
2
|
import { FolioDocumentStoryHandle } from "../ai-edits/headless.js";
|
|
3
|
+
import { ParagraphMarkChangeKind } from "@stll/docx-core/model";
|
|
3
4
|
//#region src/compare/verification.d.ts
|
|
4
5
|
/** The two directions of the round trip, each an invariant of its own. */
|
|
5
6
|
declare const COMPARE_VERIFICATION_INVARIANTS: readonly ["accept-reproduces-target", "reject-reproduces-base"];
|
|
@@ -9,7 +10,7 @@ type CompareVerificationInvariant = (typeof COMPARE_VERIFICATION_INVARIANTS)[num
|
|
|
9
10
|
* that fits is the one reported, because a block in the wrong container is a
|
|
10
11
|
* different finding from a block whose words are wrong even when both are true.
|
|
11
12
|
*/
|
|
12
|
-
declare const COMPARE_VERIFICATION_CAUSES: readonly ["invisible-structure", "block-count", "container", "style", "list-level", "inline-formatting", "whitespace", "text"];
|
|
13
|
+
declare const COMPARE_VERIFICATION_CAUSES: readonly ["invisible-structure", "block-count", "container", "table-geometry", "style", "list-level", "inline-formatting", "whitespace", "text"];
|
|
13
14
|
type CompareVerificationCause = (typeof COMPARE_VERIFICATION_CAUSES)[number];
|
|
14
15
|
/** One invariant that did not hold, in one story. */
|
|
15
16
|
type CompareVerificationFailure = {
|
|
@@ -44,6 +45,13 @@ type ClassifyOptions = {
|
|
|
44
45
|
/** What the invariant says it should leave. */
|
|
45
46
|
expected: readonly string[];
|
|
46
47
|
};
|
|
48
|
+
/**
|
|
49
|
+
* The failure two table-geometry projections describe, or `null` when they
|
|
50
|
+
* agree. One line per table, so the detail names which table diverged and
|
|
51
|
+
* whether the count itself did — never a property value, which could carry a
|
|
52
|
+
* style name either document chose.
|
|
53
|
+
*/
|
|
54
|
+
declare const classifyGeometryMismatch: ({ invariant, story, actual, expected }: ClassifyOptions) => CompareVerificationFailure | null;
|
|
47
55
|
/**
|
|
48
56
|
* The failure two projections describe, or `null` when they agree.
|
|
49
57
|
*
|
|
@@ -51,5 +59,44 @@ type ClassifyOptions = {
|
|
|
51
59
|
* a divergence always produces a failure rather than being dropped.
|
|
52
60
|
*/
|
|
53
61
|
declare const classifyProjectionMismatch: ({ invariant, story, actual, expected }: ClassifyOptions) => CompareVerificationFailure | null;
|
|
62
|
+
/**
|
|
63
|
+
* A container whose last paragraph carries a revision on its mark.
|
|
64
|
+
*
|
|
65
|
+
* Structural facts only — a path through the package model and an index — so
|
|
66
|
+
* the finding is safe to log, report or quote.
|
|
67
|
+
*/
|
|
68
|
+
type FinalParagraphMarkRevision = {
|
|
69
|
+
/** Where the container sits in the package model, e.g. `package.document.content`. */
|
|
70
|
+
container: string;
|
|
71
|
+
/** The paragraph's index among its container's children. */
|
|
72
|
+
paragraphIndex: number;
|
|
73
|
+
/** The mark kind found there, `moveFrom` / `moveTo` for a relocation's ends. */
|
|
74
|
+
kind: ParagraphMarkChangeKind;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Every container in a package whose final paragraph mark carries a revision.
|
|
78
|
+
*
|
|
79
|
+
* A deleted paragraph mark says "join this paragraph with the one after it",
|
|
80
|
+
* and an inserted one says that break was ADDED, so rejecting it closes the
|
|
81
|
+
* paragraph back over the next one. The last paragraph of a body, a table
|
|
82
|
+
* cell, a header or footer, a note or a text box has no paragraph after it, so
|
|
83
|
+
* neither direction states an edit that can be carried out: a consumer refuses
|
|
84
|
+
* the package, or opens it and leaves a revision standing that neither
|
|
85
|
+
* accepting nor rejecting everything can clear. The exception is a cell of a
|
|
86
|
+
* row the package is DELETING: there the mark leaves with its row.
|
|
87
|
+
*
|
|
88
|
+
* The walk is over the package model rather than over a list of the containers
|
|
89
|
+
* known today: a container is any sequence that ends in a paragraph, so a part
|
|
90
|
+
* the model grows later is covered the day it arrives instead of the day
|
|
91
|
+
* someone remembers this function.
|
|
92
|
+
*
|
|
93
|
+
* `since` scopes it to the revisions a comparison MINTED: a base may arrive
|
|
94
|
+
* carrying one of these on a paragraph in a part no story mounts, which folio
|
|
95
|
+
* preserves the way it preserves everything else it parses. What this proves
|
|
96
|
+
* is that the comparison writes none of its own.
|
|
97
|
+
*/
|
|
98
|
+
declare const revisedFinalParagraphMarks: (packageModel: unknown, { since }?: {
|
|
99
|
+
since?: number;
|
|
100
|
+
}) => FinalParagraphMarkRevision[];
|
|
54
101
|
//#endregion
|
|
55
|
-
export { COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant, classifyProjectionMismatch, projectSupportedInlineFormatting, sameProjection };
|
|
102
|
+
export { COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant, FinalParagraphMarkRevision, classifyGeometryMismatch, classifyProjectionMismatch, projectSupportedInlineFormatting, revisedFinalParagraphMarks, sameProjection };
|