@matrixreq/htmldiff 1.5.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 -1343
- 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 -238
- 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
package/README.md
CHANGED
|
@@ -145,6 +145,48 @@ Limitations:
|
|
|
145
145
|
- The recursion depth is capped at 10 levels as a backstop against deep
|
|
146
146
|
nesting. Opted-in elements beyond the cap are rendered as their after version.
|
|
147
147
|
|
|
148
|
+
### Tables
|
|
149
|
+
|
|
150
|
+
Tables are compared as structures before the flat diff runs. A table with its own
|
|
151
|
+
`data-htmldiff-id` takes part only when it also carries `data-htmldiff-inner-diff`, like every
|
|
152
|
+
other element with an identity: without the inner-id it is one unit, shown as it is or replaced
|
|
153
|
+
whole. The same goes for a table inside such an element. The two documents' top level
|
|
154
|
+
tables are paired (by their own `data-htmldiff-id` when they have one, otherwise by the values
|
|
155
|
+
they hold; a table in the other's place is the same table only when the two still share half of
|
|
156
|
+
what the smaller one holds, or when both are generated tables, see below), each pair is aligned
|
|
157
|
+
column by column and row by row, and a merged table takes the place of the after version's
|
|
158
|
+
table. A table without a partner is kept whole, so the flat diff wraps it as added or deleted:
|
|
159
|
+
|
|
160
|
+
- an added or deleted row is a whole row with the class `table-row-added` or
|
|
161
|
+
`table-row-deleted`,
|
|
162
|
+
- an added or deleted column marks every one of its cells (and its `<col>`, when the table has
|
|
163
|
+
a `<colgroup>`) with `table-cell-added` or `table-cell-deleted`,
|
|
164
|
+
- a kept cell holds the diff of its content, with the usual `<ins>`/`<del>` tags,
|
|
165
|
+
- a row that keeps less than half of its content, or a column no kept row agrees with, is
|
|
166
|
+
deleted and added instead of diffed; so is a moved row or column,
|
|
167
|
+
- merged cells (`rowspan`, `colspan`) are kept. A row that a kept group (a merged cell and the
|
|
168
|
+
rows it spans) lost or gained goes under the group's kept rows and carries the change on its
|
|
169
|
+
cells (`table-cell-deleted` / `table-cell-added`), not on the row: the group's cell, sitting on
|
|
170
|
+
the group's first kept row, spans it like any other row of the group. A view that hides the
|
|
171
|
+
changed cells then hides nothing the span counts on, so the layout holds. A whole group that
|
|
172
|
+
one version has is added or deleted row by row.
|
|
173
|
+
- in a table without such a statement, a change of its merged cells (a cell merged, split, a
|
|
174
|
+
span grown or shrunk) makes it another table: both versions are kept whole.
|
|
175
|
+
|
|
176
|
+
The diff reads nothing else from the content. A producer that knows what a row is about says
|
|
177
|
+
so by giving the cells an identity with `data-htmldiff-id`.
|
|
178
|
+
When both versions carry such cells, those alone pair the rows: the same
|
|
179
|
+
identities are the same row, whatever its other cells say, and they get cell diffs; other
|
|
180
|
+
identities are another row, deleted and added whole. The identity sits on the cell, not on
|
|
181
|
+
the row, because such cells often span several rows: the rows under a spanning cell never
|
|
182
|
+
contain it, yet they belong to it, and an id on the cell is inherited by every row it spans,
|
|
183
|
+
where an id on each `<tr>` would have to be composed and repeated by the producer. A cell id
|
|
184
|
+
also says which cells name the row and which are content.
|
|
185
|
+
|
|
186
|
+
Both versions of a pair get the same `data-htmldiff-id` (`redline-table-<n>` unless the table
|
|
187
|
+
had one), a table only one version has gets one of its own, so the flat diff keeps every table
|
|
188
|
+
whole and emits the merged table as it is. The styling of the classes is up to the consumer.
|
|
189
|
+
|
|
148
190
|
### Example
|
|
149
191
|
|
|
150
192
|
JavaScript:
|
|
@@ -198,14 +240,40 @@ description please see API documentation above.
|
|
|
198
240
|
|
|
199
241
|
## Development
|
|
200
242
|
|
|
201
|
-
After cloning the repository run `npm
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
243
|
+
After cloning the repository run `npm install` to install the dependencies.
|
|
244
|
+
|
|
245
|
+
Everything is TypeScript. The library lives in `src/` and is compiled to CommonJS in `js/`
|
|
246
|
+
(`js/htmldiff.js` is the entry point, `js/htmldiff.d.ts` the typings); `js/` is what gets
|
|
247
|
+
published.
|
|
248
|
+
|
|
249
|
+
- `src/htmldiff.ts` is the facade: it runs the table pass, then the flat diff.
|
|
250
|
+
- `src/constants.ts` names the attributes the diff reads, `data-htmldiff-id` and
|
|
251
|
+
`data-htmldiff-inner-diff`: the contract between a producer of HTML and the diff.
|
|
252
|
+
- `src/core/` is the flat diff, one module per stage: `atomicTags` (which elements are one
|
|
253
|
+
token), `tokens` (tokenizing and token keys), `matching` (matching blocks), `operations`
|
|
254
|
+
(insert, delete, replace, equal), `rendering` (ins/del markup and the recursive inner
|
|
255
|
+
diff) and `diff` (the pipeline).
|
|
256
|
+
- `src/tables/` is the structural table pass, one class per concern: `Cell`, `Row` and
|
|
257
|
+
`Table` are the model, `TableVersion` a table as the alignment reads it, `SequenceAligner`,
|
|
258
|
+
`ColumnAligner`, `RowAligner` and `TableAligner` decide what is the same, `MergedCells`
|
|
259
|
+
handles spans, `TableMerger` writes the merged table and `TableRedlining` is the pass
|
|
260
|
+
itself. `html.ts`, `similarity.ts` and `helpers.ts` are plain helper functions.
|
|
261
|
+
|
|
262
|
+
Tests are TypeScript too, run by mocha through `ts-node`. Every module and class has a spec
|
|
263
|
+
next to it (`src/**/*.spec.ts`, left out of the build); the end to end specs that go through
|
|
264
|
+
`diff()` itself live in `test/`.
|
|
265
|
+
|
|
266
|
+
Scripts:
|
|
267
|
+
|
|
268
|
+
- `npm run build` compiles `src/` to `js/`.
|
|
269
|
+
- `npm test` builds, type-checks sources and specs, then runs the specs.
|
|
270
|
+
- `npm run lint` checks sources and specs with ESLint.
|
|
271
|
+
- `npm run verify` does all of that in one go and compiles the CLI; `npm publish` runs it first
|
|
272
|
+
(`prepublishOnly`), so nothing unbuilt or untested can be published. `npm install` in a clone
|
|
273
|
+
builds `js/` as well (`prepare`).
|
|
274
|
+
- `npm run make` builds the library and the command line interface, `htmldiff-cli.ts`.
|
|
275
|
+
- `npm run testsample` diffs the HTML sample files from the directory `sample` and logs the
|
|
276
|
+
result to the console.
|
|
209
277
|
|
|
210
278
|
|
|
211
279
|
## Credits
|
package/js/constants.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.INNER_DIFF_ATOMIC_TAGS_ATTRIBUTE = exports.INNER_DIFF_ATTRIBUTE = exports.HTMLDIFF_ID_ATTRIBUTE = void 0;
|
|
4
|
+
exports.HTMLDIFF_ID_ATTRIBUTE = "data-htmldiff-id";
|
|
5
|
+
exports.INNER_DIFF_ATTRIBUTE = "data-htmldiff-inner-diff";
|
|
6
|
+
exports.INNER_DIFF_ATOMIC_TAGS_ATTRIBUTE = "data-htmldiff-inner-diff-atomic-tags";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const defaultAtomicTagsRegExp: RegExp;
|
|
2
|
+
export declare const defaultInnerDiffAtomicTagsRegExp: RegExp;
|
|
3
|
+
export declare const noAtomicTagsRegExp: RegExp;
|
|
4
|
+
export declare const dataHtmlDiffIdRegExp: RegExp;
|
|
5
|
+
export declare const dataHtmlDiffInnerDiffRegExp: RegExp;
|
|
6
|
+
export declare const dataHtmlDiffInnerDiffAtomicTagsRegExp: RegExp;
|
|
7
|
+
export declare function getAtomicTagsRegExp(): RegExp;
|
|
8
|
+
export declare function setAtomicTagsRegExp(regExp: RegExp): void;
|
|
9
|
+
export declare function buildAtomicTagsRegExp(atomicTags: string): RegExp;
|
|
10
|
+
export declare function isStartOfAtomicTag(word: string): string | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isStartOfAtomicTag = exports.buildAtomicTagsRegExp = exports.setAtomicTagsRegExp = exports.getAtomicTagsRegExp = exports.dataHtmlDiffInnerDiffAtomicTagsRegExp = exports.dataHtmlDiffInnerDiffRegExp = exports.dataHtmlDiffIdRegExp = exports.noAtomicTagsRegExp = exports.defaultInnerDiffAtomicTagsRegExp = exports.defaultAtomicTagsRegExp = void 0;
|
|
4
|
+
const constants_1 = require("../constants");
|
|
5
|
+
exports.defaultAtomicTagsRegExp = new RegExp("^<(iframe|object|math|svg|script|video|head|style|a)[\\s/>]");
|
|
6
|
+
exports.defaultInnerDiffAtomicTagsRegExp = new RegExp("^<(iframe|object|math|svg|script|video|head|style)[\\s/>]");
|
|
7
|
+
exports.noAtomicTagsRegExp = /^<(?!)/;
|
|
8
|
+
exports.dataHtmlDiffIdRegExp = new RegExp(`^<([a-z-]+)(?:[^>"']|"[^"]*"|'[^']*')*\\s${constants_1.HTMLDIFF_ID_ATTRIBUTE}=["']?((?:.(?!["']?\\s+(?:\\S+)=|\\s*\\/?[>"']))*.)["']?`);
|
|
9
|
+
exports.dataHtmlDiffInnerDiffRegExp = new RegExp(`^<[^>]*\\s${constants_1.INNER_DIFF_ATTRIBUTE}(?:\\s*=\\s*["']?([^"'\\s/>]*)|(?=[\\s/>]))`);
|
|
10
|
+
exports.dataHtmlDiffInnerDiffAtomicTagsRegExp = new RegExp(`^<[^>]*\\s${constants_1.INNER_DIFF_ATOMIC_TAGS_ATTRIBUTE}\\s*=\\s*["']([^"']*)["']`);
|
|
11
|
+
let atomicTagsRegExp = exports.defaultAtomicTagsRegExp;
|
|
12
|
+
function getAtomicTagsRegExp() {
|
|
13
|
+
return atomicTagsRegExp;
|
|
14
|
+
}
|
|
15
|
+
exports.getAtomicTagsRegExp = getAtomicTagsRegExp;
|
|
16
|
+
function setAtomicTagsRegExp(regExp) {
|
|
17
|
+
atomicTagsRegExp = regExp;
|
|
18
|
+
}
|
|
19
|
+
exports.setAtomicTagsRegExp = setAtomicTagsRegExp;
|
|
20
|
+
function buildAtomicTagsRegExp(atomicTags) {
|
|
21
|
+
return new RegExp("^<(" + atomicTags.replace(/\s*/g, "").replace(/,/g, "|") + ")[\\s/>]");
|
|
22
|
+
}
|
|
23
|
+
exports.buildAtomicTagsRegExp = buildAtomicTagsRegExp;
|
|
24
|
+
function isStartOfAtomicTag(word) {
|
|
25
|
+
const result = atomicTagsRegExp.exec(word) || exports.dataHtmlDiffIdRegExp.exec(word);
|
|
26
|
+
return result ? result[1] : null;
|
|
27
|
+
}
|
|
28
|
+
exports.isStartOfAtomicTag = isStartOfAtomicTag;
|
package/js/core/diff.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.diffCore = void 0;
|
|
4
|
+
const operations_1 = require("./operations");
|
|
5
|
+
const rendering_1 = require("./rendering");
|
|
6
|
+
const tokens_1 = require("./tokens");
|
|
7
|
+
const diffCore = (before, after, className, dataPrefix) => {
|
|
8
|
+
if (before === after)
|
|
9
|
+
return before;
|
|
10
|
+
const beforeTokens = (0, tokens_1.htmlToTokens)(before);
|
|
11
|
+
const afterTokens = (0, tokens_1.htmlToTokens)(after);
|
|
12
|
+
const ops = (0, operations_1.calculateOperations)(beforeTokens, afterTokens);
|
|
13
|
+
return (0, rendering_1.renderOperations)(beforeTokens, afterTokens, ops, exports.diffCore, dataPrefix, className);
|
|
14
|
+
};
|
|
15
|
+
exports.diffCore = diffCore;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Token } from "./tokens";
|
|
2
|
+
export interface Segment {
|
|
3
|
+
beforeTokens: Token[];
|
|
4
|
+
afterTokens: Token[];
|
|
5
|
+
beforeMap: TokenMap;
|
|
6
|
+
afterMap: TokenMap;
|
|
7
|
+
beforeIndex: number;
|
|
8
|
+
afterIndex: number;
|
|
9
|
+
}
|
|
10
|
+
export declare type TokenMap = Record<string, number[]>;
|
|
11
|
+
export declare class Match {
|
|
12
|
+
segment: Segment;
|
|
13
|
+
length: number;
|
|
14
|
+
startInBefore: number;
|
|
15
|
+
startInAfter: number;
|
|
16
|
+
endInBefore: number;
|
|
17
|
+
endInAfter: number;
|
|
18
|
+
segmentStartInBefore: number;
|
|
19
|
+
segmentStartInAfter: number;
|
|
20
|
+
segmentEndInBefore: number;
|
|
21
|
+
segmentEndInAfter: number;
|
|
22
|
+
constructor(startInBefore: number, startInAfter: number, length: number, segment: Segment);
|
|
23
|
+
}
|
|
24
|
+
export declare function createMap(tokens: Token[]): TokenMap;
|
|
25
|
+
export declare function findBestMatch(segment: Segment): Match | null;
|
|
26
|
+
export declare function createSegment(beforeTokens: Token[], afterTokens: Token[], beforeIndex: number, afterIndex: number): Segment;
|
|
27
|
+
export declare function findMatchingBlocks(segment: Segment): Match[];
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findMatchingBlocks = exports.createSegment = exports.findBestMatch = exports.createMap = exports.Match = void 0;
|
|
4
|
+
class Match {
|
|
5
|
+
segment;
|
|
6
|
+
length;
|
|
7
|
+
startInBefore;
|
|
8
|
+
startInAfter;
|
|
9
|
+
endInBefore;
|
|
10
|
+
endInAfter;
|
|
11
|
+
segmentStartInBefore;
|
|
12
|
+
segmentStartInAfter;
|
|
13
|
+
segmentEndInBefore;
|
|
14
|
+
segmentEndInAfter;
|
|
15
|
+
constructor(startInBefore, startInAfter, length, segment) {
|
|
16
|
+
this.segment = segment;
|
|
17
|
+
this.length = length;
|
|
18
|
+
this.startInBefore = startInBefore + segment.beforeIndex;
|
|
19
|
+
this.startInAfter = startInAfter + segment.afterIndex;
|
|
20
|
+
this.endInBefore = this.startInBefore + this.length - 1;
|
|
21
|
+
this.endInAfter = this.startInAfter + this.length - 1;
|
|
22
|
+
this.segmentStartInBefore = startInBefore;
|
|
23
|
+
this.segmentStartInAfter = startInAfter;
|
|
24
|
+
this.segmentEndInBefore = this.segmentStartInBefore + this.length - 1;
|
|
25
|
+
this.segmentEndInAfter = this.segmentStartInAfter + this.length - 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.Match = Match;
|
|
29
|
+
function createMap(tokens) {
|
|
30
|
+
return tokens.reduce((map, token, index) => {
|
|
31
|
+
if (map[token.key]) {
|
|
32
|
+
map[token.key].push(index);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
map[token.key] = [index];
|
|
36
|
+
}
|
|
37
|
+
return map;
|
|
38
|
+
}, Object.create(null));
|
|
39
|
+
}
|
|
40
|
+
exports.createMap = createMap;
|
|
41
|
+
function compareMatches(m1, m2) {
|
|
42
|
+
if (m2.endInBefore < m1.startInBefore && m2.endInAfter < m1.startInAfter) {
|
|
43
|
+
return -1;
|
|
44
|
+
}
|
|
45
|
+
if (m2.startInBefore > m1.endInBefore && m2.startInAfter > m1.endInAfter) {
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
class MatchBinarySearchTree {
|
|
51
|
+
root = null;
|
|
52
|
+
add(value) {
|
|
53
|
+
const node = { value, left: null, right: null };
|
|
54
|
+
let current = this.root;
|
|
55
|
+
if (!current) {
|
|
56
|
+
this.root = node;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
for (;;) {
|
|
60
|
+
const position = compareMatches(current.value, value);
|
|
61
|
+
if (position === -1) {
|
|
62
|
+
if (current.left) {
|
|
63
|
+
current = current.left;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
current.left = node;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (position === 1) {
|
|
71
|
+
if (current.right) {
|
|
72
|
+
current = current.right;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
current.right = node;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
toArray() {
|
|
85
|
+
function inOrder(node, nodes) {
|
|
86
|
+
if (node) {
|
|
87
|
+
inOrder(node.left, nodes);
|
|
88
|
+
nodes.push(node.value);
|
|
89
|
+
inOrder(node.right, nodes);
|
|
90
|
+
}
|
|
91
|
+
return nodes;
|
|
92
|
+
}
|
|
93
|
+
return inOrder(this.root, []);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function findBestMatch(segment) {
|
|
97
|
+
const beforeTokens = segment.beforeTokens;
|
|
98
|
+
const afterMap = segment.afterMap;
|
|
99
|
+
let lastSpace = null;
|
|
100
|
+
let bestMatch = null;
|
|
101
|
+
for (let beforeIndex = 0; beforeIndex < beforeTokens.length; beforeIndex++) {
|
|
102
|
+
let lookBehind = false;
|
|
103
|
+
const remainingTokens = beforeTokens.length - beforeIndex;
|
|
104
|
+
if (bestMatch && remainingTokens < bestMatch.length) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
const beforeToken = beforeTokens[beforeIndex];
|
|
108
|
+
if (beforeToken.key === " ") {
|
|
109
|
+
lastSpace = beforeIndex;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (lastSpace === beforeIndex - 1) {
|
|
113
|
+
lookBehind = true;
|
|
114
|
+
}
|
|
115
|
+
const afterTokenLocations = afterMap[beforeToken.key];
|
|
116
|
+
if (!afterTokenLocations) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
for (const afterIndex of afterTokenLocations) {
|
|
120
|
+
const bestMatchLength = bestMatch ? bestMatch.length : 0;
|
|
121
|
+
const match = getFullMatch(segment, beforeIndex, afterIndex, bestMatchLength, lookBehind);
|
|
122
|
+
if (match && match.length > bestMatchLength) {
|
|
123
|
+
bestMatch = match;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return bestMatch;
|
|
128
|
+
}
|
|
129
|
+
exports.findBestMatch = findBestMatch;
|
|
130
|
+
function getFullMatch(segment, beforeStart, afterStart, minLength, lookBehind) {
|
|
131
|
+
const beforeTokens = segment.beforeTokens;
|
|
132
|
+
const afterTokens = segment.afterTokens;
|
|
133
|
+
const minBeforeIndex = beforeStart + minLength;
|
|
134
|
+
const minAfterIndex = afterStart + minLength;
|
|
135
|
+
if (minBeforeIndex >= beforeTokens.length || minAfterIndex >= afterTokens.length) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
if (minLength) {
|
|
139
|
+
const nextBeforeWord = beforeTokens[minBeforeIndex].key;
|
|
140
|
+
const nextAfterWord = afterTokens[minAfterIndex].key;
|
|
141
|
+
if (nextBeforeWord !== nextAfterWord) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
let searching = true;
|
|
146
|
+
let currentLength = 1;
|
|
147
|
+
let beforeIndex = beforeStart + currentLength;
|
|
148
|
+
let afterIndex = afterStart + currentLength;
|
|
149
|
+
while (searching && beforeIndex < beforeTokens.length && afterIndex < afterTokens.length) {
|
|
150
|
+
const beforeWord = beforeTokens[beforeIndex].key;
|
|
151
|
+
const afterWord = afterTokens[afterIndex].key;
|
|
152
|
+
if (beforeWord === afterWord) {
|
|
153
|
+
currentLength++;
|
|
154
|
+
beforeIndex = beforeStart + currentLength;
|
|
155
|
+
afterIndex = afterStart + currentLength;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
searching = false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (lookBehind && beforeStart > 0 && afterStart > 0) {
|
|
162
|
+
const prevBeforeKey = beforeTokens[beforeStart - 1].key;
|
|
163
|
+
const prevAfterKey = afterTokens[afterStart - 1].key;
|
|
164
|
+
if (prevBeforeKey === " " && prevAfterKey === " ") {
|
|
165
|
+
beforeStart--;
|
|
166
|
+
afterStart--;
|
|
167
|
+
currentLength++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return new Match(beforeStart, afterStart, currentLength, segment);
|
|
171
|
+
}
|
|
172
|
+
function createSegment(beforeTokens, afterTokens, beforeIndex, afterIndex) {
|
|
173
|
+
return {
|
|
174
|
+
beforeTokens,
|
|
175
|
+
afterTokens,
|
|
176
|
+
beforeMap: createMap(beforeTokens),
|
|
177
|
+
afterMap: createMap(afterTokens),
|
|
178
|
+
beforeIndex,
|
|
179
|
+
afterIndex,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
exports.createSegment = createSegment;
|
|
183
|
+
function findMatchingBlocks(segment) {
|
|
184
|
+
const matches = new MatchBinarySearchTree();
|
|
185
|
+
const segments = [segment];
|
|
186
|
+
while (segments.length) {
|
|
187
|
+
const current = segments.pop();
|
|
188
|
+
const match = findBestMatch(current);
|
|
189
|
+
if (match && match.length) {
|
|
190
|
+
if (match.segmentStartInBefore > 0 && match.segmentStartInAfter > 0) {
|
|
191
|
+
const leftBeforeTokens = current.beforeTokens.slice(0, match.segmentStartInBefore);
|
|
192
|
+
const leftAfterTokens = current.afterTokens.slice(0, match.segmentStartInAfter);
|
|
193
|
+
segments.push(createSegment(leftBeforeTokens, leftAfterTokens, current.beforeIndex, current.afterIndex));
|
|
194
|
+
}
|
|
195
|
+
const rightBeforeTokens = current.beforeTokens.slice(match.segmentEndInBefore + 1);
|
|
196
|
+
const rightAfterTokens = current.afterTokens.slice(match.segmentEndInAfter + 1);
|
|
197
|
+
const rightBeforeIndex = current.beforeIndex + match.segmentEndInBefore + 1;
|
|
198
|
+
const rightAfterIndex = current.afterIndex + match.segmentEndInAfter + 1;
|
|
199
|
+
if (rightBeforeTokens.length && rightAfterTokens.length) {
|
|
200
|
+
segments.push(createSegment(rightBeforeTokens, rightAfterTokens, rightBeforeIndex, rightAfterIndex));
|
|
201
|
+
}
|
|
202
|
+
matches.add(match);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return matches.toArray();
|
|
206
|
+
}
|
|
207
|
+
exports.findMatchingBlocks = findMatchingBlocks;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Token } from "./tokens";
|
|
2
|
+
export declare type OperationAction = "equal" | "insert" | "delete" | "replace";
|
|
3
|
+
export interface Operation {
|
|
4
|
+
action: OperationAction;
|
|
5
|
+
startInBefore: number;
|
|
6
|
+
endInBefore: number | null;
|
|
7
|
+
startInAfter: number;
|
|
8
|
+
endInAfter: number | null;
|
|
9
|
+
}
|
|
10
|
+
export declare function calculateOperations(beforeTokens: Token[], afterTokens: Token[]): Operation[];
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.calculateOperations = void 0;
|
|
4
|
+
const matching_1 = require("./matching");
|
|
5
|
+
function calculateOperations(beforeTokens, afterTokens) {
|
|
6
|
+
if (!beforeTokens)
|
|
7
|
+
throw new Error("Missing beforeTokens");
|
|
8
|
+
if (!afterTokens)
|
|
9
|
+
throw new Error("Missing afterTokens");
|
|
10
|
+
let positionInBefore = 0;
|
|
11
|
+
let positionInAfter = 0;
|
|
12
|
+
const operations = [];
|
|
13
|
+
const segment = (0, matching_1.createSegment)(beforeTokens, afterTokens, 0, 0);
|
|
14
|
+
const matches = (0, matching_1.findMatchingBlocks)(segment);
|
|
15
|
+
matches.push(new matching_1.Match(beforeTokens.length, afterTokens.length, 0, segment));
|
|
16
|
+
for (let index = 0; index < matches.length; index++) {
|
|
17
|
+
const match = matches[index];
|
|
18
|
+
let actionUpToMatchPositions = "none";
|
|
19
|
+
if (positionInBefore === match.startInBefore) {
|
|
20
|
+
if (positionInAfter !== match.startInAfter) {
|
|
21
|
+
actionUpToMatchPositions = "insert";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
actionUpToMatchPositions = "delete";
|
|
26
|
+
if (positionInAfter !== match.startInAfter) {
|
|
27
|
+
actionUpToMatchPositions = "replace";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (actionUpToMatchPositions !== "none") {
|
|
31
|
+
operations.push({
|
|
32
|
+
action: actionUpToMatchPositions,
|
|
33
|
+
startInBefore: positionInBefore,
|
|
34
|
+
endInBefore: actionUpToMatchPositions !== "insert" ? match.startInBefore - 1 : null,
|
|
35
|
+
startInAfter: positionInAfter,
|
|
36
|
+
endInAfter: actionUpToMatchPositions !== "delete" ? match.startInAfter - 1 : null,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (match.length !== 0) {
|
|
40
|
+
operations.push({
|
|
41
|
+
action: "equal",
|
|
42
|
+
startInBefore: match.startInBefore,
|
|
43
|
+
endInBefore: match.endInBefore,
|
|
44
|
+
startInAfter: match.startInAfter,
|
|
45
|
+
endInAfter: match.endInAfter,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
positionInBefore = match.endInBefore + 1;
|
|
49
|
+
positionInAfter = match.endInAfter + 1;
|
|
50
|
+
}
|
|
51
|
+
const postProcessed = [];
|
|
52
|
+
let lastOp = { action: "none" };
|
|
53
|
+
function isSingleWhitespace(op) {
|
|
54
|
+
if (op.action !== "equal") {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (op.endInBefore - op.startInBefore !== 0) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return /^\s$/.test(String(beforeTokens.slice(op.startInBefore, op.endInBefore + 1)));
|
|
61
|
+
}
|
|
62
|
+
for (let i = 0; i < operations.length; i++) {
|
|
63
|
+
const op = operations[i];
|
|
64
|
+
if (lastOp.action === "replace" &&
|
|
65
|
+
(isSingleWhitespace(op) || op.action === "replace")) {
|
|
66
|
+
lastOp.endInBefore = op.endInBefore;
|
|
67
|
+
lastOp.endInAfter = op.endInAfter;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
postProcessed.push(op);
|
|
71
|
+
lastOp = op;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return postProcessed;
|
|
75
|
+
}
|
|
76
|
+
exports.calculateOperations = calculateOperations;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Operation } from "./operations";
|
|
2
|
+
import { Token } from "./tokens";
|
|
3
|
+
export declare type ContentDiff = (before: string, after: string, className?: string | null, dataPrefix?: string | null) => string;
|
|
4
|
+
interface TokenSegment {
|
|
5
|
+
isWrappable: boolean;
|
|
6
|
+
tokens: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare class TokenWrapper {
|
|
9
|
+
private readonly tokens;
|
|
10
|
+
private readonly notes;
|
|
11
|
+
constructor(tokens: string[]);
|
|
12
|
+
combine(mapFn: (segment: TokenSegment) => string, tagFn: (openingTag: string) => string): string;
|
|
13
|
+
}
|
|
14
|
+
export declare function wrap(tag: string, content: string[], opIndex: number, dataPrefix?: string | null, className?: string | null): string;
|
|
15
|
+
export declare function isInnerDiffToken(tokenString: string): boolean;
|
|
16
|
+
export declare function findOpeningTagEnd(tokenString: string): number;
|
|
17
|
+
export interface SplitToken {
|
|
18
|
+
openingTag: string;
|
|
19
|
+
innerHtml: string;
|
|
20
|
+
closingTag: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function splitAtomicTokenString(tokenString: string): SplitToken | null;
|
|
23
|
+
export declare function renderInnerDiff(beforeString: string, afterString: string, diffContent: ContentDiff, dataPrefix?: string | null, className?: string | null): string;
|
|
24
|
+
export declare function renderOperations(beforeTokens: Token[], afterTokens: Token[], operations: Operation[], diffContent: ContentDiff, dataPrefix?: string | null, className?: string | null): string;
|
|
25
|
+
export {};
|