@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.
Files changed (62) hide show
  1. package/README.md +76 -8
  2. package/js/constants.d.ts +3 -0
  3. package/js/constants.js +6 -0
  4. package/js/core/atomicTags.d.ts +10 -0
  5. package/js/core/atomicTags.js +28 -0
  6. package/js/core/diff.d.ts +2 -0
  7. package/js/core/diff.js +15 -0
  8. package/js/core/matching.d.ts +27 -0
  9. package/js/core/matching.js +207 -0
  10. package/js/core/operations.d.ts +10 -0
  11. package/js/core/operations.js +76 -0
  12. package/js/core/rendering.d.ts +25 -0
  13. package/js/core/rendering.js +212 -0
  14. package/js/core/tokens.d.ts +11 -0
  15. package/js/core/tokens.js +256 -0
  16. package/js/htmldiff.d.ts +0 -15
  17. package/js/htmldiff.js +14 -1351
  18. package/js/tables/Cell.d.ts +24 -0
  19. package/js/tables/Cell.js +115 -0
  20. package/js/tables/ColumnAligner.d.ts +11 -0
  21. package/js/tables/ColumnAligner.js +83 -0
  22. package/js/tables/MergedCells.d.ts +11 -0
  23. package/js/tables/MergedCells.js +171 -0
  24. package/js/tables/Row.d.ts +22 -0
  25. package/js/tables/Row.js +49 -0
  26. package/js/tables/RowAligner.d.ts +12 -0
  27. package/js/tables/RowAligner.js +107 -0
  28. package/js/tables/SequenceAligner.d.ts +47 -0
  29. package/js/tables/SequenceAligner.js +125 -0
  30. package/js/tables/Table.d.ts +27 -0
  31. package/js/tables/Table.js +166 -0
  32. package/js/tables/TableAligner.d.ts +9 -0
  33. package/js/tables/TableAligner.js +69 -0
  34. package/js/tables/TableMerger.d.ts +16 -0
  35. package/js/tables/TableMerger.js +216 -0
  36. package/js/tables/TableRedlining.d.ts +10 -0
  37. package/js/tables/TableRedlining.js +65 -0
  38. package/js/tables/TableVersion.d.ts +32 -0
  39. package/js/tables/TableVersion.js +128 -0
  40. package/js/tables/constants.d.ts +6 -0
  41. package/js/tables/constants.js +8 -0
  42. package/js/tables/helpers.d.ts +4 -0
  43. package/js/tables/helpers.js +23 -0
  44. package/js/tables/html.d.ts +32 -0
  45. package/js/tables/html.js +107 -0
  46. package/js/tables/index.d.ts +2 -0
  47. package/js/tables/index.js +5 -0
  48. package/js/tables/similarity.d.ts +10 -0
  49. package/js/tables/similarity.js +107 -0
  50. package/package.json +21 -14
  51. package/test/calculate_operations.spec.js +0 -278
  52. package/test/config.js +0 -2
  53. package/test/diff.spec.js +0 -318
  54. package/test/edge_cases.spec.js +0 -41
  55. package/test/find_matching_blocks.spec.js +0 -168
  56. package/test/from_port_source.spec.js +0 -30
  57. package/test/html_to_tokens.spec.js +0 -249
  58. package/test/inner_diff.spec.js +0 -333
  59. package/test/mocha.opts +0 -3
  60. package/test/module.spec.js +0 -11
  61. package/test/pain_games.spec.js +0 -18
  62. 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 i` or `npm install` to install the necessary
202
- dependencies. A run of `npm run make` creates the JavaScript output file.
203
- `npm run lint` checks the TypeScript sources with TSLint. `npm test` runs all the
204
- tests from the `test` directory. `npm run testsample` diffs the HTML sample files
205
- from the directory `sample` and logs the result to the console.
206
-
207
- The command line interface of htmldiff is developed in TypeScript so you have to run
208
- `npm run make` once to create the JavaScript output file.
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
@@ -0,0 +1,3 @@
1
+ export declare const HTMLDIFF_ID_ATTRIBUTE = "data-htmldiff-id";
2
+ export declare const INNER_DIFF_ATTRIBUTE = "data-htmldiff-inner-diff";
3
+ export declare const INNER_DIFF_ATOMIC_TAGS_ATTRIBUTE = "data-htmldiff-inner-diff-atomic-tags";
@@ -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;
@@ -0,0 +1,2 @@
1
+ import { ContentDiff } from "./rendering";
2
+ export declare const diffCore: ContentDiff;
@@ -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 {};