@matrixreq/htmldiff 1.6.0 → 2.0.1

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 +114 -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
@@ -0,0 +1,24 @@
1
+ import { Element } from "./html";
2
+ export declare class Cell {
3
+ tag: string;
4
+ openTag: string;
5
+ inner: string;
6
+ closeTag: string;
7
+ mergedKey: string | null;
8
+ partOf: string | null;
9
+ removed: boolean;
10
+ constructor(tag: string, openTag: string, inner: string, closeTag: string);
11
+ static read(element: Element): Cell;
12
+ static readRow(rowInner: string): Cell[];
13
+ static placeholder(siblingCells: Cell[], className?: string): Cell;
14
+ static part(mergedCellKey: string, siblingCells: Cell[]): Cell;
15
+ clone(): Cell;
16
+ addClass(className: string): void;
17
+ hasChangeClass(): boolean;
18
+ render(): string;
19
+ signature(): string;
20
+ ownId(): string | null;
21
+ span(attribute: "colspan" | "rowspan"): number;
22
+ shape(): string;
23
+ private scanContent;
24
+ }
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Cell = void 0;
4
+ const constants_1 = require("../constants");
5
+ const constants_2 = require("./constants");
6
+ const html_1 = require("./html");
7
+ class Cell {
8
+ tag;
9
+ openTag;
10
+ inner;
11
+ closeTag;
12
+ mergedKey = null;
13
+ partOf = null;
14
+ removed = false;
15
+ constructor(tag, openTag, inner, closeTag) {
16
+ this.tag = tag;
17
+ this.openTag = openTag;
18
+ this.inner = inner;
19
+ this.closeTag = closeTag;
20
+ }
21
+ static read(element) {
22
+ return new Cell(element.name, element.openTag, element.inner, element.closeTag);
23
+ }
24
+ static readRow(rowInner) {
25
+ return (0, html_1.findElements)(rowInner, ["td", "th"]).map((element) => Cell.read(element));
26
+ }
27
+ static placeholder(siblingCells, className) {
28
+ const tag = siblingCells[0] ? siblingCells[0].tag : "td";
29
+ return new Cell(tag, `<${tag}${className ? ` class="${className}"` : ""}>`, "", `</${tag}>`);
30
+ }
31
+ static part(mergedCellKey, siblingCells) {
32
+ const part = Cell.placeholder(siblingCells);
33
+ part.partOf = mergedCellKey;
34
+ return part;
35
+ }
36
+ clone() {
37
+ const copy = new Cell(this.tag, this.openTag, this.inner, this.closeTag);
38
+ copy.mergedKey = this.mergedKey;
39
+ copy.partOf = this.partOf;
40
+ return copy;
41
+ }
42
+ addClass(className) {
43
+ this.openTag = (0, html_1.addTagClass)(this.openTag, className);
44
+ }
45
+ hasChangeClass() {
46
+ const classes = ((0, html_1.getTagAttribute)(this.openTag, "class") || "").split(/\s+/);
47
+ return classes.indexOf(constants_2.CELL_ADDED_CLASS) !== -1 || classes.indexOf(constants_2.CELL_DELETED_CLASS) !== -1;
48
+ }
49
+ render() {
50
+ return this.removed ? "" : this.openTag + this.inner + this.closeTag;
51
+ }
52
+ signature() {
53
+ const content = this.scanContent();
54
+ const text = content.text.replace(/\s+/g, " ").trim();
55
+ return [text]
56
+ .concat(content.ids, content.images)
57
+ .filter((part) => part !== "")
58
+ .join("|");
59
+ }
60
+ ownId() {
61
+ return (0, html_1.getTagAttribute)(this.openTag, constants_1.HTMLDIFF_ID_ATTRIBUTE);
62
+ }
63
+ span(attribute) {
64
+ const span = parseInt((0, html_1.getTagAttribute)(this.openTag, attribute) || "", 10);
65
+ return isNaN(span) || span < 1 ? 1 : span;
66
+ }
67
+ shape() {
68
+ return `${this.tag}:${this.span("colspan")}x${this.span("rowspan")}`;
69
+ }
70
+ scanContent() {
71
+ const inner = this.inner;
72
+ const openTags = [];
73
+ let text = "";
74
+ const ids = [];
75
+ const images = [];
76
+ let position = 0;
77
+ const isHidden = () => openTags.some((tag) => tag.hidden);
78
+ (0, html_1.scanTags)(inner, (tag) => {
79
+ if (!isHidden()) {
80
+ text += inner.slice(position, tag.start);
81
+ }
82
+ position = tag.end;
83
+ if (tag.isComment) {
84
+ return;
85
+ }
86
+ if (tag.isClosing) {
87
+ for (let depth = openTags.length - 1; depth >= 0; depth--) {
88
+ if (openTags[depth].name === tag.name) {
89
+ openTags.length = depth;
90
+ break;
91
+ }
92
+ }
93
+ return;
94
+ }
95
+ const hidden = (0, html_1.getTagAttribute)(tag.text, "aria-hidden") === "true";
96
+ if (!hidden && !isHidden()) {
97
+ const id = (0, html_1.getTagAttribute)(tag.text, constants_1.HTMLDIFF_ID_ATTRIBUTE);
98
+ if (id !== null) {
99
+ ids.push(id);
100
+ }
101
+ if (tag.name === "img") {
102
+ images.push((0, html_1.getTagAttribute)(tag.text, "src") || "");
103
+ }
104
+ }
105
+ if (!tag.isSelfClosing) {
106
+ openTags.push({ name: tag.name, hidden });
107
+ }
108
+ });
109
+ if (!isHidden()) {
110
+ text += inner.slice(position);
111
+ }
112
+ return { text: (0, html_1.decodeEntities)(text), ids, images };
113
+ }
114
+ }
115
+ exports.Cell = Cell;
@@ -0,0 +1,11 @@
1
+ import { Alignment } from "./SequenceAligner";
2
+ import { TableVersion } from "./TableVersion";
3
+ export declare class ColumnAligner {
4
+ private readonly oldVersion;
5
+ private readonly newVersion;
6
+ constructor(oldVersion: TableVersion, newVersion: TableVersion);
7
+ align(): Alignment[];
8
+ splitReplaced(columns: Alignment[], rows: Alignment[]): Alignment[];
9
+ rebuildColgroup(columns: Alignment[], mergedInner: string): string;
10
+ private static colTags;
11
+ }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ColumnAligner = void 0;
4
+ const constants_1 = require("./constants");
5
+ const helpers_1 = require("./helpers");
6
+ const html_1 = require("./html");
7
+ const SequenceAligner_1 = require("./SequenceAligner");
8
+ const similarity_1 = require("./similarity");
9
+ class ColumnAligner {
10
+ oldVersion;
11
+ newVersion;
12
+ constructor(oldVersion, newVersion) {
13
+ this.oldVersion = oldVersion;
14
+ this.newVersion = newVersion;
15
+ }
16
+ align() {
17
+ const oldValueCounts = this.oldVersion.columnValueCounts();
18
+ const newValueCounts = this.newVersion.columnValueCounts();
19
+ return new SequenceAligner_1.SequenceAligner({
20
+ oldCount: this.oldVersion.columnCount,
21
+ newCount: this.newVersion.columnCount,
22
+ similarity: (oldIndex, newIndex) => (0, similarity_1.sharedShare)(oldValueCounts[oldIndex], newValueCounts[newIndex]),
23
+ byPosition: {
24
+ pairsInPlace: () => true,
25
+ isOldBlank: (oldIndex) => (0, similarity_1.countsSize)(oldValueCounts[oldIndex]) === 0,
26
+ isNewBlank: (newIndex) => (0, similarity_1.countsSize)(newValueCounts[newIndex]) === 0,
27
+ },
28
+ }).align();
29
+ }
30
+ splitReplaced(columns, rows) {
31
+ const keptRows = rows.filter((row) => row.kind === "same");
32
+ return (0, helpers_1.flatten)(columns.map((column) => {
33
+ if (column.kind !== "same" ||
34
+ (this.oldVersion.isSequenceColumn(column.oldIndex) && this.newVersion.isSequenceColumn(column.newIndex))) {
35
+ return [column];
36
+ }
37
+ const compared = keptRows
38
+ .map((row) => [this.oldVersion.signatureAt(row.oldIndex, column.oldIndex), this.newVersion.signatureAt(row.newIndex, column.newIndex)])
39
+ .filter((pair) => pair[0] !== "" && pair[1] !== "");
40
+ const isReplaced = compared.length >= 2 && compared.every((pair) => (0, similarity_1.cellSimilarity)(pair[0], pair[1]) < 0.5);
41
+ return isReplaced
42
+ ? [
43
+ { kind: "deleted", oldIndex: column.oldIndex },
44
+ { kind: "added", newIndex: column.newIndex },
45
+ ]
46
+ : [column];
47
+ }));
48
+ }
49
+ rebuildColgroup(columns, mergedInner) {
50
+ const oldCols = ColumnAligner.colTags(this.oldVersion.table.inner);
51
+ const newCols = ColumnAligner.colTags(this.newVersion.table.inner);
52
+ if (oldCols.length === 0 && newCols.length === 0) {
53
+ return mergedInner;
54
+ }
55
+ const inner = (0, html_1.replaceRanges)(mergedInner, (0, html_1.findElements)(mergedInner, ["colgroup"]).map((colgroup) => ({ start: colgroup.start, end: colgroup.end, html: "" })));
56
+ const colgroup = "<colgroup>" +
57
+ columns
58
+ .map((column) => {
59
+ let col = column.kind === "deleted" ? oldCols[column.oldIndex] || "<col />" : newCols[column.newIndex] || "<col />";
60
+ if (column.kind !== "same") {
61
+ col = (0, html_1.addTagClass)(col, column.kind === "added" ? constants_1.CELL_ADDED_CLASS : constants_1.CELL_DELETED_CLASS);
62
+ }
63
+ return col;
64
+ })
65
+ .join("") +
66
+ "</colgroup>";
67
+ const caption = (0, html_1.findElements)(inner, ["caption"])[0];
68
+ const at = caption ? caption.end : 0;
69
+ return inner.slice(0, at) + colgroup + inner.slice(at);
70
+ }
71
+ static colTags(tableInner) {
72
+ const cols = [];
73
+ (0, html_1.findElements)(tableInner, ["colgroup"]).forEach((colgroup) => {
74
+ (0, html_1.scanTags)(colgroup.inner, (tag) => {
75
+ if (tag.name === "col" && !tag.isClosing) {
76
+ cols.push(tag.text);
77
+ }
78
+ });
79
+ });
80
+ return cols;
81
+ }
82
+ }
83
+ exports.ColumnAligner = ColumnAligner;
@@ -0,0 +1,11 @@
1
+ import { Alignment } from "./SequenceAligner";
2
+ import { Table } from "./Table";
3
+ import { TableVersion } from "./TableVersion";
4
+ export declare class MergedCells {
5
+ private readonly oldVersion;
6
+ private readonly newVersion;
7
+ constructor(oldVersion: TableVersion, newVersion: TableVersion);
8
+ static expand(table: Table, versionKey: string): void;
9
+ fold(): void;
10
+ moveOwnersToKeptRows(columns: Alignment[], rows: Alignment[]): void;
11
+ }
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MergedCells = void 0;
4
+ const Cell_1 = require("./Cell");
5
+ const helpers_1 = require("./helpers");
6
+ const html_1 = require("./html");
7
+ class MergedCells {
8
+ oldVersion;
9
+ newVersion;
10
+ constructor(oldVersion, newVersion) {
11
+ this.oldVersion = oldVersion;
12
+ this.newVersion = newVersion;
13
+ }
14
+ static expand(table, versionKey) {
15
+ const coveredByRow = {};
16
+ const coveredIn = (rowIndex) => {
17
+ coveredByRow[rowIndex] = coveredByRow[rowIndex] || {};
18
+ return coveredByRow[rowIndex];
19
+ };
20
+ table.rows().forEach((row, rowIndex) => {
21
+ const covered = coveredIn(rowIndex);
22
+ const cells = row.cells;
23
+ const expanded = [];
24
+ let column = 0;
25
+ cells.forEach((cell, cellIndex) => {
26
+ while (covered[column] !== undefined) {
27
+ expanded.push(Cell_1.Cell.part(covered[column], [cell]));
28
+ column++;
29
+ }
30
+ const columnSpan = cell.span("colspan");
31
+ const rowSpan = cell.span("rowspan");
32
+ expanded.push(cell);
33
+ if (columnSpan === 1 && rowSpan === 1) {
34
+ column++;
35
+ return;
36
+ }
37
+ const mergedCellKey = `${versionKey}-${rowIndex}-${cellIndex}`;
38
+ cell.mergedKey = mergedCellKey;
39
+ cell.openTag = (0, html_1.removeTagAttribute)((0, html_1.removeTagAttribute)(cell.openTag, "colspan"), "rowspan");
40
+ for (let offset = 1; offset < columnSpan; offset++) {
41
+ expanded.push(Cell_1.Cell.part(mergedCellKey, [cell]));
42
+ }
43
+ for (let rowOffset = 1; rowOffset < rowSpan; rowOffset++) {
44
+ (0, helpers_1.range)(column, column + columnSpan).forEach((index) => {
45
+ coveredIn(rowIndex + rowOffset)[index] = mergedCellKey;
46
+ });
47
+ }
48
+ column += columnSpan;
49
+ });
50
+ while (covered[column] !== undefined) {
51
+ expanded.push(Cell_1.Cell.part(covered[column], cells));
52
+ column++;
53
+ }
54
+ row.cells = expanded;
55
+ });
56
+ }
57
+ fold() {
58
+ const rows = this.newVersion.table.rows();
59
+ const cells = rows.map((row) => row.cells);
60
+ const mergedCells = Object.create(null);
61
+ Object.keys(this.oldVersion.mergedCells).forEach((key) => {
62
+ mergedCells[key] = this.oldVersion.mergedCells[key];
63
+ });
64
+ Object.keys(this.newVersion.mergedCells).forEach((key) => {
65
+ mergedCells[key] = this.newVersion.mergedCells[key];
66
+ });
67
+ const isSameMergedCell = (key, otherKey) => key === otherKey || (mergedCells[key] !== "" && mergedCells[key] === mergedCells[otherKey]);
68
+ const isEmptyPartOf = (mergedCellKey, rowIndex, columnIndex, rowKind) => {
69
+ const cell = cells[rowIndex] && cells[rowIndex][columnIndex];
70
+ return (!!cell &&
71
+ cell.partOf !== null &&
72
+ cell.inner === "" &&
73
+ isSameMergedCell(mergedCellKey, cell.partOf) &&
74
+ rows[rowIndex].kind() === rowKind);
75
+ };
76
+ cells.forEach((rowCells, rowIndex) => {
77
+ rowCells.forEach((cell, columnIndex) => {
78
+ const mergedCellKey = cell.mergedKey;
79
+ if (mergedCellKey === null) {
80
+ return;
81
+ }
82
+ const rowKind = rows[rowIndex].kind();
83
+ const spansRows = !(rows[rowIndex].changedInGroup && cell.hasChangeClass());
84
+ let columnSpan = 1;
85
+ while (isEmptyPartOf(mergedCellKey, rowIndex, columnIndex + columnSpan, rowKind)) {
86
+ columnSpan++;
87
+ }
88
+ let rowSpan = 1;
89
+ while (spansRows &&
90
+ (0, helpers_1.range)(columnIndex, columnIndex + columnSpan).every((index) => isEmptyPartOf(mergedCellKey, rowIndex + rowSpan, index, rowKind))) {
91
+ rowSpan++;
92
+ }
93
+ (0, helpers_1.range)(rowIndex, rowIndex + rowSpan).forEach((spannedRow) => {
94
+ (0, helpers_1.range)(columnIndex, columnIndex + columnSpan).forEach((spannedColumn) => {
95
+ if (spannedRow !== rowIndex || spannedColumn !== columnIndex) {
96
+ cells[spannedRow][spannedColumn].removed = true;
97
+ }
98
+ });
99
+ });
100
+ if (columnSpan > 1) {
101
+ cell.openTag = (0, html_1.setTagAttribute)(cell.openTag, "colspan", columnSpan);
102
+ }
103
+ if (rowSpan > 1) {
104
+ cell.openTag = (0, html_1.setTagAttribute)(cell.openTag, "rowspan", rowSpan);
105
+ }
106
+ });
107
+ });
108
+ rows.forEach((row) => {
109
+ if (!row.changedInGroup || !row.changeClass) {
110
+ return;
111
+ }
112
+ const changeClass = row.changeClass;
113
+ row.cells.forEach((cell) => {
114
+ if (!cell.removed && cell.partOf !== null && !cell.hasChangeClass()) {
115
+ cell.addClass(changeClass);
116
+ }
117
+ });
118
+ });
119
+ }
120
+ moveOwnersToKeptRows(columns, rows) {
121
+ const moveOwners = (version, keptIndexes) => {
122
+ version.cells.forEach((rowCells, rowIndex) => {
123
+ if (keptIndexes.indexOf(rowIndex) !== -1) {
124
+ return;
125
+ }
126
+ rowCells.forEach((cell, columnIndex) => {
127
+ if (cell.mergedKey === null) {
128
+ return;
129
+ }
130
+ const target = keptIndexes
131
+ .filter((keptIndex) => {
132
+ const keptCell = version.cells[keptIndex][columnIndex];
133
+ return keptIndex > rowIndex && !!keptCell && keptCell.partOf === cell.mergedKey;
134
+ })
135
+ .sort((first, second) => first - second)[0];
136
+ if (target === undefined) {
137
+ return;
138
+ }
139
+ rowCells[columnIndex] = version.cells[target][columnIndex];
140
+ version.cells[target][columnIndex] = cell;
141
+ const signature = version.signatures[rowIndex][columnIndex];
142
+ version.signatures[rowIndex][columnIndex] = version.signatures[target][columnIndex];
143
+ version.signatures[target][columnIndex] = signature;
144
+ });
145
+ });
146
+ };
147
+ const keptRows = rows.filter((row) => row.kind === "same");
148
+ moveOwners(this.oldVersion, keptRows.map((row) => row.oldIndex));
149
+ moveOwners(this.newVersion, keptRows.map((row) => row.newIndex));
150
+ keptRows.forEach((row) => {
151
+ columns.forEach((column) => {
152
+ if (column.kind !== "same") {
153
+ return;
154
+ }
155
+ const oldCell = this.oldVersion.cells[row.oldIndex][column.oldIndex];
156
+ const newCell = this.newVersion.cells[row.newIndex][column.newIndex];
157
+ if (!oldCell ||
158
+ !newCell ||
159
+ oldCell.mergedKey === null ||
160
+ newCell.mergedKey !== null ||
161
+ newCell.partOf !== null ||
162
+ oldCell.signature() !== newCell.signature()) {
163
+ return;
164
+ }
165
+ newCell.mergedKey = oldCell.mergedKey;
166
+ this.newVersion.mergedCells[oldCell.mergedKey] = this.oldVersion.mergedCells[oldCell.mergedKey];
167
+ });
168
+ });
169
+ }
170
+ }
171
+ exports.MergedCells = MergedCells;
@@ -0,0 +1,22 @@
1
+ import { Cell } from "./Cell";
2
+ export declare type RowKind = "kept" | "added" | "deleted";
3
+ export declare class Row {
4
+ openTag: string;
5
+ cells: Cell[];
6
+ container: string | null;
7
+ before: Row[];
8
+ after: Row[];
9
+ added: boolean;
10
+ deleted: boolean;
11
+ detached: boolean;
12
+ changedInGroup: boolean;
13
+ changeClass: string | null;
14
+ constructor(openTag: string, cells: Cell[], container: string | null);
15
+ kind(): RowKind;
16
+ render(): string;
17
+ static renderAll(rows: Row[]): string;
18
+ insertBefore(row: Row): void;
19
+ insertAfter(row: Row): void;
20
+ detach(): void;
21
+ collect(rows: Row[]): void;
22
+ }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Row = void 0;
4
+ class Row {
5
+ openTag;
6
+ cells;
7
+ container;
8
+ before = [];
9
+ after = [];
10
+ added = false;
11
+ deleted = false;
12
+ detached = false;
13
+ changedInGroup = false;
14
+ changeClass = null;
15
+ constructor(openTag, cells, container) {
16
+ this.openTag = openTag;
17
+ this.cells = cells;
18
+ this.container = container;
19
+ }
20
+ kind() {
21
+ if (this.deleted) {
22
+ return "deleted";
23
+ }
24
+ return this.added ? "added" : "kept";
25
+ }
26
+ render() {
27
+ return Row.renderAll(this.before) + this.openTag + this.cells.map((cell) => cell.render()).join("") + "</tr>" + Row.renderAll(this.after);
28
+ }
29
+ static renderAll(rows) {
30
+ return rows.map((row) => row.render()).join("");
31
+ }
32
+ insertBefore(row) {
33
+ row.container = this.container;
34
+ this.before.push(row);
35
+ }
36
+ insertAfter(row) {
37
+ row.container = this.container;
38
+ this.after.unshift(row);
39
+ }
40
+ detach() {
41
+ this.detached = true;
42
+ }
43
+ collect(rows) {
44
+ this.before.forEach((row) => row.collect(rows));
45
+ rows.push(this);
46
+ this.after.forEach((row) => row.collect(rows));
47
+ }
48
+ }
49
+ exports.Row = Row;
@@ -0,0 +1,12 @@
1
+ import { Alignment } from "./SequenceAligner";
2
+ import { TableVersion } from "./TableVersion";
3
+ export declare class RowAligner {
4
+ private readonly oldVersion;
5
+ private readonly newVersion;
6
+ private readonly comparedColumns;
7
+ constructor(oldVersion: TableVersion, newVersion: TableVersion, columns: Alignment[]);
8
+ align(): Alignment[];
9
+ private static readRows;
10
+ alternateReplaced(rows: Alignment[]): Alignment[];
11
+ private static groupIntoUnits;
12
+ }
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RowAligner = void 0;
4
+ const helpers_1 = require("./helpers");
5
+ const SequenceAligner_1 = require("./SequenceAligner");
6
+ const similarity_1 = require("./similarity");
7
+ class RowAligner {
8
+ oldVersion;
9
+ newVersion;
10
+ comparedColumns;
11
+ constructor(oldVersion, newVersion, columns) {
12
+ this.oldVersion = oldVersion;
13
+ this.newVersion = newVersion;
14
+ this.comparedColumns = columns.filter((column) => column.kind === "same" && (!oldVersion.isSequenceColumn(column.oldIndex) || !newVersion.isSequenceColumn(column.newIndex)));
15
+ }
16
+ align() {
17
+ const keyed = this.oldVersion.hasRowKeys && this.newVersion.hasRowKeys;
18
+ const exactKeys = new Map();
19
+ const oldRows = RowAligner.readRows(this.oldVersion, this.comparedColumns.map((column) => column.oldIndex), keyed, exactKeys);
20
+ const newRows = RowAligner.readRows(this.newVersion, this.comparedColumns.map((column) => column.newIndex), keyed, exactKeys);
21
+ const similarity = (oldIndex, newIndex) => {
22
+ const oldRow = oldRows[oldIndex];
23
+ const newRow = newRows[newIndex];
24
+ if (keyed) {
25
+ return oldRow.exactKey === newRow.exactKey ? 1 : 0;
26
+ }
27
+ let compared = 0;
28
+ let kept = 0;
29
+ oldRow.signatures.forEach((oldSignature, column) => {
30
+ const newSignature = newRow.signatures[column];
31
+ if (oldSignature === "" && newSignature === "") {
32
+ return;
33
+ }
34
+ compared++;
35
+ kept += oldSignature === newSignature ? 1 : (0, similarity_1.wordSimilarity)(oldRow.words[column], newRow.words[column]);
36
+ });
37
+ return compared === 0 ? NaN : kept / compared;
38
+ };
39
+ const isExact = (oldIndex, newIndex) => {
40
+ const oldRow = oldRows[oldIndex];
41
+ const newRow = newRows[newIndex];
42
+ return oldRow.exactKey === newRow.exactKey && (keyed || !(oldRow.blank && newRow.blank));
43
+ };
44
+ return new SequenceAligner_1.SequenceAligner({
45
+ oldCount: oldRows.length,
46
+ newCount: newRows.length,
47
+ similarity,
48
+ isExact,
49
+ byPosition: {
50
+ isOldBlank: (oldIndex) => oldRows[oldIndex].blank,
51
+ isNewBlank: (newIndex) => newRows[newIndex].blank,
52
+ hasOldIdentity: (oldIndex) => keyed && oldRows[oldIndex].keys.length > 0,
53
+ hasNewIdentity: (newIndex) => keyed && newRows[newIndex].keys.length > 0,
54
+ },
55
+ }).align();
56
+ }
57
+ static readRows(version, columnIndexes, keyed, exactKeys) {
58
+ const keysByRow = keyed ? version.rowKeysByRow() : [];
59
+ return (0, helpers_1.range)(0, version.rows.length).map((rowIndex) => {
60
+ const signatures = columnIndexes.map((columnIndex) => version.signatureAt(rowIndex, columnIndex));
61
+ const words = signatures.map((signature) => (0, similarity_1.signatureWords)(signature));
62
+ const keys = keyed ? keysByRow[rowIndex] : [];
63
+ const exactKey = keyed ? JSON.stringify(keys) : words.map((cellWords) => cellWords.slice().sort().join(" ")).join("|");
64
+ let interned = exactKeys.get(exactKey);
65
+ if (interned === undefined) {
66
+ interned = exactKeys.size;
67
+ exactKeys.set(exactKey, interned);
68
+ }
69
+ return { signatures, words, blank: signatures.every((signature) => signature === ""), keys, exactKey: interned };
70
+ });
71
+ }
72
+ alternateReplaced(rows) {
73
+ let result = [];
74
+ let run = [];
75
+ const flushRun = () => {
76
+ const deletedUnits = RowAligner.groupIntoUnits(run.filter((row) => row.kind === "deleted"), (row) => row.kind === "deleted" && this.oldVersion.isContinuationRow(row.oldIndex));
77
+ const addedUnits = RowAligner.groupIntoUnits(run.filter((row) => row.kind === "added"), (row) => row.kind === "added" && this.newVersion.isContinuationRow(row.newIndex));
78
+ (0, helpers_1.range)(0, Math.max(deletedUnits.length, addedUnits.length)).forEach((index) => {
79
+ result = result.concat(deletedUnits[index] || [], addedUnits[index] || []);
80
+ });
81
+ run = [];
82
+ };
83
+ rows.forEach((row) => {
84
+ if (row.kind === "same") {
85
+ flushRun();
86
+ result.push(row);
87
+ return;
88
+ }
89
+ run.push(row);
90
+ });
91
+ flushRun();
92
+ return result;
93
+ }
94
+ static groupIntoUnits(rows, continuesUnit) {
95
+ const units = [];
96
+ rows.forEach((row) => {
97
+ const unit = (0, helpers_1.last)(units);
98
+ if (unit && continuesUnit(row)) {
99
+ unit.push(row);
100
+ return;
101
+ }
102
+ units.push([row]);
103
+ });
104
+ return units;
105
+ }
106
+ }
107
+ exports.RowAligner = RowAligner;
@@ -0,0 +1,47 @@
1
+ export declare type Alignment = {
2
+ kind: "same";
3
+ oldIndex: number;
4
+ newIndex: number;
5
+ } | {
6
+ kind: "added";
7
+ newIndex: number;
8
+ } | {
9
+ kind: "deleted";
10
+ oldIndex: number;
11
+ };
12
+ export declare type SameAlignment = Extract<Alignment, {
13
+ kind: "same";
14
+ }>;
15
+ export interface IndexPair {
16
+ oldIndex: number;
17
+ newIndex: number;
18
+ }
19
+ export interface UnmatchedRange {
20
+ oldStart: number;
21
+ oldEnd: number;
22
+ newStart: number;
23
+ newEnd: number;
24
+ }
25
+ export interface PositionalPairing {
26
+ pairsInPlace?: (oldIndex: number, newIndex: number) => boolean;
27
+ isOldBlank: (oldIndex: number) => boolean;
28
+ isNewBlank: (newIndex: number) => boolean;
29
+ hasOldIdentity?: (oldIndex: number) => boolean;
30
+ hasNewIdentity?: (newIndex: number) => boolean;
31
+ }
32
+ export interface Sequence {
33
+ oldCount: number;
34
+ newCount: number;
35
+ similarity: (oldIndex: number, newIndex: number) => number;
36
+ isExact?: (oldIndex: number, newIndex: number) => boolean;
37
+ byPosition: PositionalPairing;
38
+ }
39
+ export declare class SequenceAligner {
40
+ private readonly sequence;
41
+ constructor(sequence: Sequence);
42
+ align(): Alignment[];
43
+ private fillUnmatchedRanges;
44
+ static pairByPosition(byPosition: PositionalPairing, unmatched: UnmatchedRange): IndexPair[];
45
+ static matchInOrder(unmatched: UnmatchedRange, matches: (oldIndex: number, newIndex: number) => boolean): IndexPair[];
46
+ static toAlignments(pairs: IndexPair[], oldCount: number, newCount: number): Alignment[];
47
+ }