@matrixreq/htmldiff 1.6.0 → 2.0.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/README.md +76 -8
- package/js/constants.d.ts +3 -0
- package/js/constants.js +6 -0
- package/js/core/atomicTags.d.ts +10 -0
- package/js/core/atomicTags.js +28 -0
- package/js/core/diff.d.ts +2 -0
- package/js/core/diff.js +15 -0
- package/js/core/matching.d.ts +27 -0
- package/js/core/matching.js +207 -0
- package/js/core/operations.d.ts +10 -0
- package/js/core/operations.js +76 -0
- package/js/core/rendering.d.ts +25 -0
- package/js/core/rendering.js +212 -0
- package/js/core/tokens.d.ts +11 -0
- package/js/core/tokens.js +256 -0
- package/js/htmldiff.d.ts +0 -15
- package/js/htmldiff.js +14 -1351
- package/js/tables/Cell.d.ts +24 -0
- package/js/tables/Cell.js +115 -0
- package/js/tables/ColumnAligner.d.ts +11 -0
- package/js/tables/ColumnAligner.js +83 -0
- package/js/tables/MergedCells.d.ts +11 -0
- package/js/tables/MergedCells.js +171 -0
- package/js/tables/Row.d.ts +22 -0
- package/js/tables/Row.js +49 -0
- package/js/tables/RowAligner.d.ts +12 -0
- package/js/tables/RowAligner.js +107 -0
- package/js/tables/SequenceAligner.d.ts +47 -0
- package/js/tables/SequenceAligner.js +125 -0
- package/js/tables/Table.d.ts +27 -0
- package/js/tables/Table.js +166 -0
- package/js/tables/TableAligner.d.ts +9 -0
- package/js/tables/TableAligner.js +69 -0
- package/js/tables/TableMerger.d.ts +16 -0
- package/js/tables/TableMerger.js +216 -0
- package/js/tables/TableRedlining.d.ts +10 -0
- package/js/tables/TableRedlining.js +65 -0
- package/js/tables/TableVersion.d.ts +32 -0
- package/js/tables/TableVersion.js +128 -0
- package/js/tables/constants.d.ts +6 -0
- package/js/tables/constants.js +8 -0
- package/js/tables/helpers.d.ts +4 -0
- package/js/tables/helpers.js +23 -0
- package/js/tables/html.d.ts +32 -0
- package/js/tables/html.js +107 -0
- package/js/tables/index.d.ts +2 -0
- package/js/tables/index.js +5 -0
- package/js/tables/similarity.d.ts +10 -0
- package/js/tables/similarity.js +107 -0
- package/package.json +21 -14
- package/test/calculate_operations.spec.js +0 -278
- package/test/config.js +0 -2
- package/test/diff.spec.js +0 -318
- package/test/edge_cases.spec.js +0 -41
- package/test/find_matching_blocks.spec.js +0 -168
- package/test/from_port_source.spec.js +0 -30
- package/test/html_to_tokens.spec.js +0 -249
- package/test/inner_diff.spec.js +0 -333
- package/test/mocha.opts +0 -3
- package/test/module.spec.js +0 -11
- package/test/pain_games.spec.js +0 -18
- package/test/render_operations.spec.js +0 -178
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SequenceAligner = void 0;
|
|
4
|
+
const helpers_1 = require("./helpers");
|
|
5
|
+
class SequenceAligner {
|
|
6
|
+
sequence;
|
|
7
|
+
constructor(sequence) {
|
|
8
|
+
this.sequence = sequence;
|
|
9
|
+
}
|
|
10
|
+
align() {
|
|
11
|
+
const sequence = this.sequence;
|
|
12
|
+
const exact = sequence.isExact ?? ((oldIndex, newIndex) => sequence.similarity(oldIndex, newIndex) === 1);
|
|
13
|
+
const similar = (oldIndex, newIndex) => sequence.similarity(oldIndex, newIndex) >= 0.5;
|
|
14
|
+
const whole = { oldStart: 0, oldEnd: sequence.oldCount, newStart: 0, newEnd: sequence.newCount };
|
|
15
|
+
let pairs = SequenceAligner.matchInOrder(whole, exact);
|
|
16
|
+
pairs = this.fillUnmatchedRanges(pairs, (unmatched) => SequenceAligner.matchInOrder(unmatched, similar));
|
|
17
|
+
pairs = this.fillUnmatchedRanges(pairs, (unmatched) => SequenceAligner.pairByPosition(sequence.byPosition, unmatched));
|
|
18
|
+
return SequenceAligner.toAlignments(pairs, sequence.oldCount, sequence.newCount);
|
|
19
|
+
}
|
|
20
|
+
fillUnmatchedRanges(pairs, pairUnmatched) {
|
|
21
|
+
const sequence = this.sequence;
|
|
22
|
+
const result = [];
|
|
23
|
+
let oldStart = 0;
|
|
24
|
+
let newStart = 0;
|
|
25
|
+
pairs.concat([{ oldIndex: sequence.oldCount, newIndex: sequence.newCount }]).forEach((pair) => {
|
|
26
|
+
if (pair.oldIndex > oldStart && pair.newIndex > newStart) {
|
|
27
|
+
pairUnmatched({ oldStart, oldEnd: pair.oldIndex, newStart, newEnd: pair.newIndex }).forEach((unmatchedPair) => result.push(unmatchedPair));
|
|
28
|
+
}
|
|
29
|
+
if (pair.oldIndex < sequence.oldCount) {
|
|
30
|
+
result.push(pair);
|
|
31
|
+
}
|
|
32
|
+
oldStart = pair.oldIndex + 1;
|
|
33
|
+
newStart = pair.newIndex + 1;
|
|
34
|
+
});
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
static pairByPosition(byPosition, unmatched) {
|
|
38
|
+
const oldIndexes = (0, helpers_1.range)(unmatched.oldStart, unmatched.oldEnd).filter((oldIndex) => !(byPosition.hasOldIdentity && byPosition.hasOldIdentity(oldIndex)));
|
|
39
|
+
const newIndexes = (0, helpers_1.range)(unmatched.newStart, unmatched.newEnd).filter((newIndex) => !(byPosition.hasNewIdentity && byPosition.hasNewIdentity(newIndex)));
|
|
40
|
+
const pairsInPlace = byPosition.pairsInPlace;
|
|
41
|
+
if (pairsInPlace && oldIndexes.length === newIndexes.length) {
|
|
42
|
+
return oldIndexes
|
|
43
|
+
.map((oldIndex, offset) => ({ oldIndex, newIndex: newIndexes[offset] }))
|
|
44
|
+
.filter((pair) => pairsInPlace(pair.oldIndex, pair.newIndex));
|
|
45
|
+
}
|
|
46
|
+
const blankOld = oldIndexes.filter(byPosition.isOldBlank);
|
|
47
|
+
const blankNew = newIndexes.filter(byPosition.isNewBlank);
|
|
48
|
+
const blankPairs = (0, helpers_1.range)(0, Math.min(blankOld.length, blankNew.length)).map((offset) => ({
|
|
49
|
+
oldIndex: blankOld[offset],
|
|
50
|
+
newIndex: blankNew[offset],
|
|
51
|
+
}));
|
|
52
|
+
const pairs = [];
|
|
53
|
+
let oldPosition = 0;
|
|
54
|
+
let newPosition = 0;
|
|
55
|
+
blankPairs.concat([null]).forEach((blankPair) => {
|
|
56
|
+
const oldEnd = blankPair ? oldIndexes.indexOf(blankPair.oldIndex) : oldIndexes.length;
|
|
57
|
+
const newEnd = blankPair ? newIndexes.indexOf(blankPair.newIndex) : newIndexes.length;
|
|
58
|
+
(0, helpers_1.range)(0, Math.min(oldEnd - oldPosition, newEnd - newPosition)).forEach((offset) => {
|
|
59
|
+
const oldIndex = oldIndexes[oldPosition + offset];
|
|
60
|
+
const newIndex = newIndexes[newPosition + offset];
|
|
61
|
+
if (byPosition.isOldBlank(oldIndex) || byPosition.isNewBlank(newIndex)) {
|
|
62
|
+
pairs.push({ oldIndex, newIndex });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
if (blankPair) {
|
|
66
|
+
pairs.push(blankPair);
|
|
67
|
+
}
|
|
68
|
+
oldPosition = oldEnd + 1;
|
|
69
|
+
newPosition = newEnd + 1;
|
|
70
|
+
});
|
|
71
|
+
return pairs;
|
|
72
|
+
}
|
|
73
|
+
static matchInOrder(unmatched, matches) {
|
|
74
|
+
const oldStart = unmatched.oldStart;
|
|
75
|
+
const newStart = unmatched.newStart;
|
|
76
|
+
const oldLength = unmatched.oldEnd - oldStart;
|
|
77
|
+
const newLength = unmatched.newEnd - newStart;
|
|
78
|
+
const width = newLength + 1;
|
|
79
|
+
const lengths = new Int32Array((oldLength + 1) * width);
|
|
80
|
+
for (let i = oldLength - 1; i >= 0; i--) {
|
|
81
|
+
for (let j = newLength - 1; j >= 0; j--) {
|
|
82
|
+
lengths[i * width + j] = matches(oldStart + i, newStart + j)
|
|
83
|
+
? lengths[(i + 1) * width + j + 1] + 1
|
|
84
|
+
: Math.max(lengths[(i + 1) * width + j], lengths[i * width + j + 1]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const pairs = [];
|
|
88
|
+
let oldOffset = 0;
|
|
89
|
+
let newOffset = 0;
|
|
90
|
+
while (oldOffset < oldLength && newOffset < newLength) {
|
|
91
|
+
if (matches(oldStart + oldOffset, newStart + newOffset)) {
|
|
92
|
+
pairs.push({ oldIndex: oldStart + oldOffset, newIndex: newStart + newOffset });
|
|
93
|
+
oldOffset++;
|
|
94
|
+
newOffset++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (lengths[(oldOffset + 1) * width + newOffset] >= lengths[oldOffset * width + newOffset + 1]) {
|
|
98
|
+
oldOffset++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
newOffset++;
|
|
102
|
+
}
|
|
103
|
+
return pairs;
|
|
104
|
+
}
|
|
105
|
+
static toAlignments(pairs, oldCount, newCount) {
|
|
106
|
+
const alignments = [];
|
|
107
|
+
let oldIndex = 0;
|
|
108
|
+
let newIndex = 0;
|
|
109
|
+
pairs.concat([{ oldIndex: oldCount, newIndex: newCount }]).forEach((pair) => {
|
|
110
|
+
for (; oldIndex < pair.oldIndex; oldIndex++) {
|
|
111
|
+
alignments.push({ kind: "deleted", oldIndex });
|
|
112
|
+
}
|
|
113
|
+
for (; newIndex < pair.newIndex; newIndex++) {
|
|
114
|
+
alignments.push({ kind: "added", newIndex });
|
|
115
|
+
}
|
|
116
|
+
if (pair.oldIndex < oldCount) {
|
|
117
|
+
alignments.push({ kind: "same", oldIndex: pair.oldIndex, newIndex: pair.newIndex });
|
|
118
|
+
oldIndex++;
|
|
119
|
+
newIndex++;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return alignments;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
exports.SequenceAligner = SequenceAligner;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Element } from "./html";
|
|
2
|
+
import { Row } from "./Row";
|
|
3
|
+
export interface AppendChunk {
|
|
4
|
+
appended: Row[];
|
|
5
|
+
container: string | null;
|
|
6
|
+
}
|
|
7
|
+
export declare type Chunk = string | Row | AppendChunk;
|
|
8
|
+
export declare class Table {
|
|
9
|
+
openTag: string;
|
|
10
|
+
readonly closeTag: string;
|
|
11
|
+
readonly start: number;
|
|
12
|
+
readonly end: number;
|
|
13
|
+
readonly inner: string;
|
|
14
|
+
readonly chunks: Chunk[];
|
|
15
|
+
readonly appendChunk: AppendChunk;
|
|
16
|
+
private constructor();
|
|
17
|
+
static read(element: Element): Table;
|
|
18
|
+
static findTopLevel(html: string): Table[];
|
|
19
|
+
ownId(): string | undefined;
|
|
20
|
+
wantsInnerDiff(): boolean;
|
|
21
|
+
ensureId(fallbackId: string): string;
|
|
22
|
+
documentRows(): Row[];
|
|
23
|
+
rows(): Row[];
|
|
24
|
+
nextSibling(row: Row): Row | null;
|
|
25
|
+
appendRow(row: Row): void;
|
|
26
|
+
renderInner(): string;
|
|
27
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Table = void 0;
|
|
4
|
+
const Cell_1 = require("./Cell");
|
|
5
|
+
const constants_1 = require("../constants");
|
|
6
|
+
const html_1 = require("./html");
|
|
7
|
+
const Row_1 = require("./Row");
|
|
8
|
+
class Table {
|
|
9
|
+
openTag;
|
|
10
|
+
closeTag;
|
|
11
|
+
start;
|
|
12
|
+
end;
|
|
13
|
+
inner;
|
|
14
|
+
chunks;
|
|
15
|
+
appendChunk;
|
|
16
|
+
constructor(element, chunks, appendChunk) {
|
|
17
|
+
this.openTag = element.openTag;
|
|
18
|
+
this.closeTag = element.closeTag;
|
|
19
|
+
this.start = element.start;
|
|
20
|
+
this.end = element.end;
|
|
21
|
+
this.inner = element.inner;
|
|
22
|
+
this.chunks = chunks;
|
|
23
|
+
this.appendChunk = appendChunk;
|
|
24
|
+
}
|
|
25
|
+
static read(element) {
|
|
26
|
+
const inner = element.inner;
|
|
27
|
+
const rowElements = (0, html_1.findElements)(inner, ["tr"]);
|
|
28
|
+
const containers = {};
|
|
29
|
+
let tableDepth = 0;
|
|
30
|
+
let container = null;
|
|
31
|
+
let appendAt = inner.length;
|
|
32
|
+
let firstTbodyEnd = -1;
|
|
33
|
+
(0, html_1.scanTags)(inner, (tag) => {
|
|
34
|
+
if (tag.name === "table") {
|
|
35
|
+
tableDepth += tag.isClosing ? -1 : 1;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (tableDepth !== 0) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (tag.name === "thead" || tag.name === "tbody" || tag.name === "tfoot") {
|
|
42
|
+
if (tag.isClosing && tag.name === "tbody" && firstTbodyEnd === -1) {
|
|
43
|
+
firstTbodyEnd = tag.start;
|
|
44
|
+
}
|
|
45
|
+
container = tag.isClosing ? null : tag.name;
|
|
46
|
+
}
|
|
47
|
+
if (tag.name === "tr" && !tag.isClosing) {
|
|
48
|
+
containers[tag.start] = container;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
if (firstTbodyEnd !== -1) {
|
|
52
|
+
appendAt = firstTbodyEnd;
|
|
53
|
+
}
|
|
54
|
+
const chunks = [];
|
|
55
|
+
const appendChunk = { appended: [], container: firstTbodyEnd !== -1 ? "tbody" : null };
|
|
56
|
+
let position = 0;
|
|
57
|
+
let appendInserted = false;
|
|
58
|
+
const pushText = (textStart, textEnd) => {
|
|
59
|
+
if (!appendInserted && appendAt >= textStart && appendAt <= textEnd) {
|
|
60
|
+
if (textStart < appendAt) {
|
|
61
|
+
chunks.push(inner.slice(textStart, appendAt));
|
|
62
|
+
}
|
|
63
|
+
chunks.push(appendChunk);
|
|
64
|
+
appendInserted = true;
|
|
65
|
+
if (appendAt < textEnd) {
|
|
66
|
+
chunks.push(inner.slice(appendAt, textEnd));
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (textStart < textEnd) {
|
|
71
|
+
chunks.push(inner.slice(textStart, textEnd));
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
rowElements.forEach((rowElement) => {
|
|
75
|
+
pushText(position, rowElement.start);
|
|
76
|
+
chunks.push(new Row_1.Row(rowElement.openTag, Cell_1.Cell.readRow(rowElement.inner), containers[rowElement.start] || null));
|
|
77
|
+
position = rowElement.end;
|
|
78
|
+
});
|
|
79
|
+
pushText(position, inner.length);
|
|
80
|
+
if (!appendInserted) {
|
|
81
|
+
chunks.push(appendChunk);
|
|
82
|
+
}
|
|
83
|
+
return new Table(element, chunks, appendChunk);
|
|
84
|
+
}
|
|
85
|
+
static findTopLevel(html) {
|
|
86
|
+
const elements = (0, html_1.findElements)(html, ["table"]);
|
|
87
|
+
const sealed = {};
|
|
88
|
+
const openTags = [];
|
|
89
|
+
let tableIndex = 0;
|
|
90
|
+
(0, html_1.scanTags)(html, (tag) => {
|
|
91
|
+
if (tag.isComment) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (tag.isClosing) {
|
|
95
|
+
for (let depth = openTags.length - 1; depth >= 0; depth--) {
|
|
96
|
+
if (openTags[depth].name === tag.name) {
|
|
97
|
+
openTags.length = depth;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (elements[tableIndex] && elements[tableIndex].start === tag.start) {
|
|
104
|
+
sealed[tag.start] = openTags.some((open) => open.sealed);
|
|
105
|
+
tableIndex++;
|
|
106
|
+
}
|
|
107
|
+
if (!tag.isSelfClosing) {
|
|
108
|
+
const id = (0, html_1.getTagAttribute)(tag.text, constants_1.HTMLDIFF_ID_ATTRIBUTE);
|
|
109
|
+
const flag = (0, html_1.getTagAttribute)(tag.text, constants_1.INNER_DIFF_ATTRIBUTE);
|
|
110
|
+
openTags.push({ name: tag.name, sealed: id !== null && (flag === null || flag === "false") });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
return elements.filter((element) => !sealed[element.start]).map((element) => Table.read(element));
|
|
114
|
+
}
|
|
115
|
+
ownId() {
|
|
116
|
+
return (0, html_1.getTagAttribute)(this.openTag, constants_1.HTMLDIFF_ID_ATTRIBUTE) || undefined;
|
|
117
|
+
}
|
|
118
|
+
wantsInnerDiff() {
|
|
119
|
+
const flag = (0, html_1.getTagAttribute)(this.openTag, constants_1.INNER_DIFF_ATTRIBUTE);
|
|
120
|
+
return flag !== null && flag !== "false";
|
|
121
|
+
}
|
|
122
|
+
ensureId(fallbackId) {
|
|
123
|
+
const id = this.ownId() ?? fallbackId;
|
|
124
|
+
this.openTag = (0, html_1.setTagAttribute)(this.openTag, constants_1.HTMLDIFF_ID_ATTRIBUTE, id);
|
|
125
|
+
return id;
|
|
126
|
+
}
|
|
127
|
+
documentRows() {
|
|
128
|
+
const rows = [];
|
|
129
|
+
this.chunks.forEach((chunk) => {
|
|
130
|
+
if (chunk === this.appendChunk) {
|
|
131
|
+
chunk.appended.forEach((row) => row.collect(rows));
|
|
132
|
+
}
|
|
133
|
+
else if (chunk instanceof Row_1.Row && !chunk.detached) {
|
|
134
|
+
chunk.collect(rows);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return rows;
|
|
138
|
+
}
|
|
139
|
+
rows() {
|
|
140
|
+
const rows = this.documentRows();
|
|
141
|
+
return rows.filter((row) => row.container !== null).concat(rows.filter((row) => row.container === null));
|
|
142
|
+
}
|
|
143
|
+
nextSibling(row) {
|
|
144
|
+
const rows = this.documentRows();
|
|
145
|
+
const next = rows[rows.indexOf(row) + 1];
|
|
146
|
+
return next && next.container === row.container ? next : null;
|
|
147
|
+
}
|
|
148
|
+
appendRow(row) {
|
|
149
|
+
row.container = this.appendChunk.container;
|
|
150
|
+
this.appendChunk.appended.push(row);
|
|
151
|
+
}
|
|
152
|
+
renderInner() {
|
|
153
|
+
return this.chunks
|
|
154
|
+
.map((chunk) => {
|
|
155
|
+
if (typeof chunk === "string") {
|
|
156
|
+
return chunk;
|
|
157
|
+
}
|
|
158
|
+
if (chunk === this.appendChunk) {
|
|
159
|
+
return Row_1.Row.renderAll(chunk.appended);
|
|
160
|
+
}
|
|
161
|
+
return chunk.detached ? "" : chunk.render();
|
|
162
|
+
})
|
|
163
|
+
.join("");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
exports.Table = Table;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Alignment } from "./SequenceAligner";
|
|
2
|
+
import { Table } from "./Table";
|
|
3
|
+
export declare class TableAligner {
|
|
4
|
+
private readonly oldTables;
|
|
5
|
+
private readonly newTables;
|
|
6
|
+
constructor(oldTables: Table[], newTables: Table[]);
|
|
7
|
+
align(): Alignment[];
|
|
8
|
+
private static headerSignature;
|
|
9
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TableAligner = void 0;
|
|
4
|
+
const SequenceAligner_1 = require("./SequenceAligner");
|
|
5
|
+
const similarity_1 = require("./similarity");
|
|
6
|
+
const TableVersion_1 = require("./TableVersion");
|
|
7
|
+
class TableAligner {
|
|
8
|
+
oldTables;
|
|
9
|
+
newTables;
|
|
10
|
+
constructor(oldTables, newTables) {
|
|
11
|
+
this.oldTables = oldTables;
|
|
12
|
+
this.newTables = newTables;
|
|
13
|
+
}
|
|
14
|
+
align() {
|
|
15
|
+
const oldTables = this.oldTables;
|
|
16
|
+
const newTables = this.newTables;
|
|
17
|
+
const oldIds = oldTables.map((table) => table.ownId());
|
|
18
|
+
const newIds = newTables.map((table) => table.ownId());
|
|
19
|
+
const oldVersions = oldTables.map((table) => TableVersion_1.TableVersion.read(table));
|
|
20
|
+
const newVersions = newTables.map((table) => TableVersion_1.TableVersion.read(table));
|
|
21
|
+
const oldValueCounts = oldVersions.map((version) => version.valueCounts());
|
|
22
|
+
const newValueCounts = newVersions.map((version) => version.valueCounts());
|
|
23
|
+
const oldHeaders = oldVersions.map((version) => TableAligner.headerSignature(version));
|
|
24
|
+
const newHeaders = newVersions.map((version) => TableAligner.headerSignature(version));
|
|
25
|
+
const keyed = (oldIndex, newIndex) => oldVersions[oldIndex].hasRowKeys && newVersions[newIndex].hasRowKeys;
|
|
26
|
+
const headersApart = (oldIndex, newIndex) => keyed(oldIndex, newIndex) &&
|
|
27
|
+
oldHeaders[oldIndex] !== null &&
|
|
28
|
+
newHeaders[newIndex] !== null &&
|
|
29
|
+
oldHeaders[oldIndex] !== newHeaders[newIndex];
|
|
30
|
+
const spansApart = (oldIndex, newIndex) => !keyed(oldIndex, newIndex) && !oldVersions[oldIndex].hasSameSpanLayoutAs(newVersions[newIndex]);
|
|
31
|
+
return new SequenceAligner_1.SequenceAligner({
|
|
32
|
+
oldCount: oldTables.length,
|
|
33
|
+
newCount: newTables.length,
|
|
34
|
+
similarity: (oldIndex, newIndex) => {
|
|
35
|
+
if (oldIds[oldIndex] !== undefined || newIds[newIndex] !== undefined) {
|
|
36
|
+
return oldIds[oldIndex] === newIds[newIndex] ? 1 : 0;
|
|
37
|
+
}
|
|
38
|
+
if (headersApart(oldIndex, newIndex) || spansApart(oldIndex, newIndex)) {
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
return (0, similarity_1.valueOverlap)(oldValueCounts[oldIndex], newValueCounts[newIndex]);
|
|
42
|
+
},
|
|
43
|
+
byPosition: {
|
|
44
|
+
pairsInPlace: (oldIndex, newIndex) => {
|
|
45
|
+
if (headersApart(oldIndex, newIndex) || spansApart(oldIndex, newIndex)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return ((0, similarity_1.distinctSharedShare)(oldValueCounts[oldIndex], newValueCounts[newIndex]) >= 0.5 ||
|
|
49
|
+
(keyed(oldIndex, newIndex) && oldVersions[oldIndex].columnCount === newVersions[newIndex].columnCount));
|
|
50
|
+
},
|
|
51
|
+
isOldBlank: (oldIndex) => (0, similarity_1.countsSize)(oldValueCounts[oldIndex]) === 0,
|
|
52
|
+
isNewBlank: (newIndex) => (0, similarity_1.countsSize)(newValueCounts[newIndex]) === 0,
|
|
53
|
+
hasOldIdentity: (oldIndex) => oldIds[oldIndex] !== undefined,
|
|
54
|
+
hasNewIdentity: (newIndex) => newIds[newIndex] !== undefined,
|
|
55
|
+
},
|
|
56
|
+
}).align();
|
|
57
|
+
}
|
|
58
|
+
static headerSignature(version) {
|
|
59
|
+
const headerRowCount = version.headerRowCount();
|
|
60
|
+
if (headerRowCount === 0) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return version
|
|
64
|
+
.slice(0, headerRowCount)
|
|
65
|
+
.signatures.map((row) => row.join("|"))
|
|
66
|
+
.join("||");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.TableAligner = TableAligner;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CellDiff } from "./constants";
|
|
2
|
+
import { Table } from "./Table";
|
|
3
|
+
export declare class TableMerger {
|
|
4
|
+
private readonly oldTable;
|
|
5
|
+
private readonly newTable;
|
|
6
|
+
private readonly diffContent;
|
|
7
|
+
constructor(oldTable: Table, newTable: Table, diffContent: CellDiff);
|
|
8
|
+
merge(): string;
|
|
9
|
+
private mergeUnderSameHeader;
|
|
10
|
+
private diffCellContent;
|
|
11
|
+
private diffCellsByPosition;
|
|
12
|
+
private static oldCellToCompare;
|
|
13
|
+
private buildKeptRowCell;
|
|
14
|
+
private static markChangedCells;
|
|
15
|
+
private emitRows;
|
|
16
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TableMerger = void 0;
|
|
4
|
+
const Cell_1 = require("./Cell");
|
|
5
|
+
const ColumnAligner_1 = require("./ColumnAligner");
|
|
6
|
+
const constants_1 = require("./constants");
|
|
7
|
+
const helpers_1 = require("./helpers");
|
|
8
|
+
const html_1 = require("./html");
|
|
9
|
+
const MergedCells_1 = require("./MergedCells");
|
|
10
|
+
const Row_1 = require("./Row");
|
|
11
|
+
const RowAligner_1 = require("./RowAligner");
|
|
12
|
+
const TableVersion_1 = require("./TableVersion");
|
|
13
|
+
class TableMerger {
|
|
14
|
+
oldTable;
|
|
15
|
+
newTable;
|
|
16
|
+
diffContent;
|
|
17
|
+
constructor(oldTable, newTable, diffContent) {
|
|
18
|
+
this.oldTable = oldTable;
|
|
19
|
+
this.newTable = newTable;
|
|
20
|
+
this.diffContent = diffContent;
|
|
21
|
+
}
|
|
22
|
+
merge() {
|
|
23
|
+
let oldVersion = TableVersion_1.TableVersion.read(this.oldTable);
|
|
24
|
+
let newVersion = TableVersion_1.TableVersion.read(this.newTable);
|
|
25
|
+
if (oldVersion.hasSpans || newVersion.hasSpans) {
|
|
26
|
+
if (oldVersion.hasSameShapeAs(newVersion) && !(oldVersion.hasRowKeys && newVersion.hasRowKeys)) {
|
|
27
|
+
this.diffCellsByPosition(oldVersion, newVersion);
|
|
28
|
+
return this.newTable.renderInner();
|
|
29
|
+
}
|
|
30
|
+
if (this.mergeUnderSameHeader(oldVersion, newVersion)) {
|
|
31
|
+
return this.newTable.renderInner();
|
|
32
|
+
}
|
|
33
|
+
MergedCells_1.MergedCells.expand(this.oldTable, "old");
|
|
34
|
+
MergedCells_1.MergedCells.expand(this.newTable, "new");
|
|
35
|
+
oldVersion = TableVersion_1.TableVersion.read(this.oldTable);
|
|
36
|
+
newVersion = TableVersion_1.TableVersion.read(this.newTable);
|
|
37
|
+
}
|
|
38
|
+
const columnAligner = new ColumnAligner_1.ColumnAligner(oldVersion, newVersion);
|
|
39
|
+
let columns = columnAligner.align();
|
|
40
|
+
columns = columnAligner.splitReplaced(columns, new RowAligner_1.RowAligner(oldVersion, newVersion, columns).align());
|
|
41
|
+
const rowAligner = new RowAligner_1.RowAligner(oldVersion, newVersion, columns);
|
|
42
|
+
const rows = rowAligner.alternateReplaced(rowAligner.align());
|
|
43
|
+
const mergedCells = new MergedCells_1.MergedCells(oldVersion, newVersion);
|
|
44
|
+
this.emitRows(oldVersion, newVersion, columns, rows, mergedCells);
|
|
45
|
+
mergedCells.fold();
|
|
46
|
+
return columnAligner.rebuildColgroup(columns, this.newTable.renderInner());
|
|
47
|
+
}
|
|
48
|
+
mergeUnderSameHeader(oldVersion, newVersion) {
|
|
49
|
+
const headerRowCount = oldVersion.headerRowCount();
|
|
50
|
+
const oldHeader = oldVersion.slice(0, headerRowCount);
|
|
51
|
+
const newHeader = newVersion.slice(0, newVersion.headerRowCount());
|
|
52
|
+
const oldBody = oldVersion.slice(headerRowCount);
|
|
53
|
+
const newBody = newVersion.slice(newVersion.headerRowCount());
|
|
54
|
+
if (!oldHeader.hasSameShapeAs(newHeader) || oldBody.hasSpans || newBody.hasSpans) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (oldBody.columnCount !== newBody.columnCount) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
this.diffCellsByPosition(oldHeader, newHeader);
|
|
61
|
+
const columns = (0, helpers_1.range)(0, newBody.columnCount).map((index) => ({ kind: "same", oldIndex: index, newIndex: index }));
|
|
62
|
+
const rowAligner = new RowAligner_1.RowAligner(oldBody, newBody, columns);
|
|
63
|
+
this.emitRows(oldBody, newBody, columns, rowAligner.alternateReplaced(rowAligner.align()), new MergedCells_1.MergedCells(oldBody, newBody));
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
diffCellContent(oldCell, newCell) {
|
|
67
|
+
if (oldCell && oldCell.inner !== newCell.inner) {
|
|
68
|
+
newCell.inner = this.diffContent(oldCell.inner, newCell.inner);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
diffCellsByPosition(oldVersion, newVersion) {
|
|
72
|
+
newVersion.cells.forEach((rowCells, rowIndex) => {
|
|
73
|
+
rowCells.forEach((newCell, cellIndex) => {
|
|
74
|
+
const oldCells = oldVersion.cells[rowIndex];
|
|
75
|
+
this.diffCellContent(oldCells && oldCells[cellIndex], newCell);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
static oldCellToCompare(oldVersion, oldCell, newCell) {
|
|
80
|
+
if (!oldCell || oldCell.partOf === null) {
|
|
81
|
+
return oldCell;
|
|
82
|
+
}
|
|
83
|
+
const mergedCell = oldVersion.ownerOf(oldCell);
|
|
84
|
+
return mergedCell && mergedCell.signature() === newCell.signature() ? mergedCell : oldCell;
|
|
85
|
+
}
|
|
86
|
+
buildKeptRowCell(oldVersion, newVersion, row, column) {
|
|
87
|
+
const newCells = newVersion.cells[row.newIndex];
|
|
88
|
+
const oldCells = oldVersion.cells[row.oldIndex];
|
|
89
|
+
if (column.kind === "deleted") {
|
|
90
|
+
const cell = oldCells[column.oldIndex] ? oldCells[column.oldIndex].clone() : Cell_1.Cell.placeholder(newCells);
|
|
91
|
+
cell.addClass(constants_1.CELL_DELETED_CLASS);
|
|
92
|
+
return cell;
|
|
93
|
+
}
|
|
94
|
+
const newCell = newCells[column.newIndex] || Cell_1.Cell.placeholder(newCells);
|
|
95
|
+
if (column.kind === "added") {
|
|
96
|
+
newCell.addClass(constants_1.CELL_ADDED_CLASS);
|
|
97
|
+
return newCell;
|
|
98
|
+
}
|
|
99
|
+
if (newCell.partOf !== null) {
|
|
100
|
+
return newCell;
|
|
101
|
+
}
|
|
102
|
+
this.diffCellContent(TableMerger.oldCellToCompare(oldVersion, oldCells[column.oldIndex], newCell), newCell);
|
|
103
|
+
return newCell;
|
|
104
|
+
}
|
|
105
|
+
static markChangedCells(cells, className) {
|
|
106
|
+
cells.forEach((cell) => {
|
|
107
|
+
if (cell.partOf === null && !cell.hasChangeClass()) {
|
|
108
|
+
cell.addClass(className);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
emitRows(oldVersion, newVersion, columns, rows, mergedCells) {
|
|
113
|
+
const table = newVersion.table;
|
|
114
|
+
mergedCells.moveOwnersToKeptRows(columns, rows);
|
|
115
|
+
const keptRows = rows.filter((row) => row.kind === "same");
|
|
116
|
+
const keptGroupSignatures = keptRows.map((row) => newVersion.groupSignatures(row.newIndex).concat(oldVersion.groupSignatures(row.oldIndex)));
|
|
117
|
+
const lastKeptRowOfGroup = (signatures) => {
|
|
118
|
+
if (signatures.length === 0) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
let found = null;
|
|
122
|
+
keptRows.forEach((row, index) => {
|
|
123
|
+
if (keptGroupSignatures[index].some((signature) => signatures.indexOf(signature) !== -1)) {
|
|
124
|
+
found = newVersion.rows[row.newIndex];
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return found;
|
|
128
|
+
};
|
|
129
|
+
const insertAfterGroup = (groupTail, tr) => {
|
|
130
|
+
let anchor = groupTail;
|
|
131
|
+
for (let sibling = table.nextSibling(anchor); sibling && sibling.changedInGroup; sibling = table.nextSibling(anchor)) {
|
|
132
|
+
anchor = sibling;
|
|
133
|
+
}
|
|
134
|
+
anchor.insertAfter(tr);
|
|
135
|
+
};
|
|
136
|
+
const placeDeletedRow = (tr, oldIndex, following) => {
|
|
137
|
+
const group = oldVersion.groupSignatures(oldIndex);
|
|
138
|
+
const next = following.find((row) => row.kind !== "deleted" &&
|
|
139
|
+
(!newVersion.isContinuationRow(row.newIndex) ||
|
|
140
|
+
newVersion.groupSignatures(row.newIndex).some((signature) => group.indexOf(signature) !== -1)));
|
|
141
|
+
if (next) {
|
|
142
|
+
newVersion.rows[next.newIndex].insertBefore(tr);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const tail = (0, helpers_1.last)(table.rows());
|
|
146
|
+
if (tail && tail.container !== "thead") {
|
|
147
|
+
tail.insertAfter(tr);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
table.appendRow(tr);
|
|
151
|
+
};
|
|
152
|
+
rows.forEach((row, index) => {
|
|
153
|
+
if (row.kind === "same") {
|
|
154
|
+
if (newVersion.cells[row.newIndex].length > 0) {
|
|
155
|
+
newVersion.rows[row.newIndex].cells = columns.map((column) => this.buildKeptRowCell(oldVersion, newVersion, row, column));
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (row.kind === "added") {
|
|
160
|
+
const addedRow = newVersion.rows[row.newIndex];
|
|
161
|
+
const addedCells = newVersion.cells[row.newIndex];
|
|
162
|
+
const addedGroupTail = lastKeptRowOfGroup(newVersion.groupSignatures(row.newIndex));
|
|
163
|
+
const addedInGroup = !!addedGroupTail && addedCells.length > 0;
|
|
164
|
+
addedRow.added = !addedInGroup;
|
|
165
|
+
if (!addedInGroup) {
|
|
166
|
+
addedRow.openTag = (0, html_1.addTagClass)(addedRow.openTag, constants_1.ROW_ADDED_CLASS);
|
|
167
|
+
}
|
|
168
|
+
if (addedCells.length > 0) {
|
|
169
|
+
addedRow.cells = columns.map((column) => {
|
|
170
|
+
if (column.kind === "deleted") {
|
|
171
|
+
return Cell_1.Cell.placeholder(addedCells, constants_1.CELL_DELETED_CLASS);
|
|
172
|
+
}
|
|
173
|
+
return addedCells[column.newIndex] || Cell_1.Cell.placeholder(addedCells);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (addedInGroup) {
|
|
177
|
+
addedRow.changedInGroup = true;
|
|
178
|
+
addedRow.changeClass = constants_1.CELL_ADDED_CLASS;
|
|
179
|
+
TableMerger.markChangedCells(addedRow.cells, constants_1.CELL_ADDED_CLASS);
|
|
180
|
+
if (addedGroupTail && newVersion.rows.indexOf(addedGroupTail) > row.newIndex) {
|
|
181
|
+
addedRow.detach();
|
|
182
|
+
insertAfterGroup(addedGroupTail, addedRow);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const oldRow = oldVersion.rows[row.oldIndex];
|
|
188
|
+
const oldCells = oldVersion.cells[row.oldIndex];
|
|
189
|
+
const deletedGroupTail = lastKeptRowOfGroup(oldVersion.groupSignatures(row.oldIndex));
|
|
190
|
+
const deletedInGroup = !!deletedGroupTail && oldCells.length > 0;
|
|
191
|
+
const tr = new Row_1.Row(deletedInGroup ? oldRow.openTag : (0, html_1.addTagClass)(oldRow.openTag, constants_1.ROW_DELETED_CLASS), oldCells.map((cell) => cell.clone()), null);
|
|
192
|
+
tr.deleted = !deletedInGroup;
|
|
193
|
+
if (oldCells.length > 0) {
|
|
194
|
+
tr.cells = columns.map((column) => {
|
|
195
|
+
if (column.kind === "added") {
|
|
196
|
+
return Cell_1.Cell.placeholder(oldCells, constants_1.CELL_ADDED_CLASS);
|
|
197
|
+
}
|
|
198
|
+
return oldCells[column.oldIndex] ? oldCells[column.oldIndex].clone() : Cell_1.Cell.placeholder(oldCells);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (deletedInGroup) {
|
|
202
|
+
tr.changedInGroup = true;
|
|
203
|
+
tr.changeClass = constants_1.CELL_DELETED_CLASS;
|
|
204
|
+
TableMerger.markChangedCells(tr.cells, constants_1.CELL_DELETED_CLASS);
|
|
205
|
+
insertAfterGroup(deletedGroupTail, tr);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (deletedGroupTail) {
|
|
209
|
+
insertAfterGroup(deletedGroupTail, tr);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
placeDeletedRow(tr, row.oldIndex, rows.slice(index + 1));
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
exports.TableMerger = TableMerger;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { CellDiff } from "./constants";
|
|
2
|
+
export interface PreparedHtml {
|
|
3
|
+
before: string;
|
|
4
|
+
after: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class TableRedlining {
|
|
7
|
+
private readonly diffContent;
|
|
8
|
+
constructor(diffContent: CellDiff);
|
|
9
|
+
redline(before: string, after: string): PreparedHtml;
|
|
10
|
+
}
|