@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.
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 -1343
  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 -238
  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,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TableRedlining = void 0;
4
+ const constants_1 = require("../constants");
5
+ const constants_2 = require("./constants");
6
+ const html_1 = require("./html");
7
+ const Table_1 = require("./Table");
8
+ const TableAligner_1 = require("./TableAligner");
9
+ const TableMerger_1 = require("./TableMerger");
10
+ class TableRedlining {
11
+ diffContent;
12
+ constructor(diffContent) {
13
+ this.diffContent = diffContent;
14
+ }
15
+ redline(before, after) {
16
+ const oldTables = Table_1.Table.findTopLevel(before);
17
+ const newTables = Table_1.Table.findTopLevel(after);
18
+ if (oldTables.length === 0 && newTables.length === 0) {
19
+ return { before, after };
20
+ }
21
+ const beforeReplacements = [];
22
+ const afterReplacements = [];
23
+ const oldOpenTags = oldTables.map((table) => table.openTag);
24
+ const newOpenTags = newTables.map((table) => table.openTag);
25
+ const openTagReplacement = (table, original) => ({
26
+ start: table.start,
27
+ end: table.start + original.length,
28
+ html: table.openTag,
29
+ });
30
+ new TableAligner_1.TableAligner(oldTables, newTables).align().forEach((alignment, index) => {
31
+ if (alignment.kind === "added") {
32
+ const addedTable = newTables[alignment.newIndex];
33
+ addedTable.ensureId(`${constants_2.TABLE_ID_PREFIX}added-${index}`);
34
+ afterReplacements.push(openTagReplacement(addedTable, newOpenTags[alignment.newIndex]));
35
+ return;
36
+ }
37
+ if (alignment.kind === "deleted") {
38
+ const deletedTable = oldTables[alignment.oldIndex];
39
+ deletedTable.ensureId(`${constants_2.TABLE_ID_PREFIX}deleted-${index}`);
40
+ beforeReplacements.push(openTagReplacement(deletedTable, oldOpenTags[alignment.oldIndex]));
41
+ return;
42
+ }
43
+ const oldTable = oldTables[alignment.oldIndex];
44
+ const newTable = newTables[alignment.newIndex];
45
+ if (newTable.ownId() !== undefined && !newTable.wantsInnerDiff()) {
46
+ return;
47
+ }
48
+ const mergedInner = new TableMerger_1.TableMerger(oldTable, newTable, this.diffContent).merge();
49
+ const id = newTable.ensureId(`${constants_2.TABLE_ID_PREFIX}${index}`);
50
+ oldTable.openTag = (0, html_1.setTagAttribute)(oldTable.openTag, constants_1.HTMLDIFF_ID_ATTRIBUTE, id);
51
+ newTable.openTag = (0, html_1.removeTagAttribute)(newTable.openTag, constants_1.INNER_DIFF_ATTRIBUTE);
52
+ beforeReplacements.push(openTagReplacement(oldTable, oldOpenTags[alignment.oldIndex]));
53
+ afterReplacements.push({
54
+ start: newTable.start,
55
+ end: newTable.end,
56
+ html: newTable.openTag + mergedInner + newTable.closeTag,
57
+ });
58
+ });
59
+ return {
60
+ before: (0, html_1.replaceRanges)(before, beforeReplacements),
61
+ after: (0, html_1.replaceRanges)(after, afterReplacements),
62
+ };
63
+ }
64
+ }
65
+ exports.TableRedlining = TableRedlining;
@@ -0,0 +1,32 @@
1
+ import { Cell } from "./Cell";
2
+ import { Row } from "./Row";
3
+ import { ValueCounts } from "./similarity";
4
+ import { Table } from "./Table";
5
+ export declare class TableVersion {
6
+ readonly table: Table;
7
+ readonly rows: Row[];
8
+ readonly cells: Cell[][];
9
+ readonly signatures: string[][];
10
+ readonly shapes: string[];
11
+ readonly columnCount: number;
12
+ readonly hasSpans: boolean;
13
+ readonly hasRowKeys: boolean;
14
+ readonly mergedCells: Record<string, string>;
15
+ constructor(table: Table, rows: Row[], cells: Cell[][]);
16
+ static read(table: Table): TableVersion;
17
+ slice(start: number, end?: number): TableVersion;
18
+ signatureAt(rowIndex: number, columnIndex: number): string;
19
+ hasSameShapeAs(other: TableVersion): boolean;
20
+ hasSameSpanLayoutAs(other: TableVersion): boolean;
21
+ static isHeaderRow(cells: Cell[]): boolean;
22
+ headerRowCount(): number;
23
+ isSequenceColumn(columnIndex: number): boolean;
24
+ columnValueCounts(): ValueCounts[];
25
+ valueCounts(): ValueCounts;
26
+ ownerOf(cell: Cell): Cell | undefined;
27
+ rowKeys(rowIndex: number): string[];
28
+ rowKeysByRow(): string[][];
29
+ private static keysOf;
30
+ groupSignatures(rowIndex: number): string[];
31
+ isContinuationRow(rowIndex: number): boolean;
32
+ }
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TableVersion = void 0;
4
+ const helpers_1 = require("./helpers");
5
+ const similarity_1 = require("./similarity");
6
+ class TableVersion {
7
+ table;
8
+ rows;
9
+ cells;
10
+ signatures;
11
+ shapes;
12
+ columnCount;
13
+ hasSpans;
14
+ hasRowKeys;
15
+ mergedCells;
16
+ constructor(table, rows, cells) {
17
+ this.table = table;
18
+ this.rows = rows;
19
+ this.cells = cells;
20
+ this.signatures = cells.map((rowCells) => rowCells.map((cell) => cell.signature()));
21
+ this.shapes = cells.map((rowCells) => rowCells.map((cell) => cell.shape()).join("|"));
22
+ this.columnCount = cells.reduce((max, rowCells) => Math.max(max, rowCells.length), 0);
23
+ this.hasSpans = (0, helpers_1.flatten)(cells).some((cell) => cell.span("colspan") > 1 || cell.span("rowspan") > 1);
24
+ this.hasRowKeys = (0, helpers_1.flatten)(cells).some((cell) => cell.ownId() !== null);
25
+ this.mergedCells = Object.create(null);
26
+ cells.forEach((rowCells, rowIndex) => {
27
+ rowCells.forEach((cell, cellIndex) => {
28
+ if (cell.mergedKey !== null) {
29
+ this.mergedCells[cell.mergedKey] = this.signatures[rowIndex][cellIndex];
30
+ }
31
+ });
32
+ });
33
+ }
34
+ static read(table) {
35
+ const rows = table.rows();
36
+ return new TableVersion(table, rows, rows.map((row) => row.cells));
37
+ }
38
+ slice(start, end = this.rows.length) {
39
+ return new TableVersion(this.table, this.rows.slice(start, end), this.cells.slice(start, end));
40
+ }
41
+ signatureAt(rowIndex, columnIndex) {
42
+ const rowSignatures = this.signatures[rowIndex];
43
+ return rowSignatures && rowSignatures[columnIndex] !== undefined ? rowSignatures[columnIndex] : "";
44
+ }
45
+ hasSameShapeAs(other) {
46
+ return this.shapes.length === other.shapes.length && this.shapes.every((shape, index) => shape === other.shapes[index]);
47
+ }
48
+ hasSameSpanLayoutAs(other) {
49
+ const layout = (version) => {
50
+ const spans = [];
51
+ version.cells.forEach((rowCells) => {
52
+ rowCells.forEach((cell, cellIndex) => {
53
+ if (cell.span("colspan") > 1 || cell.span("rowspan") > 1) {
54
+ spans.push(`${cellIndex}:${cell.shape()}`);
55
+ }
56
+ });
57
+ });
58
+ return spans.sort().join(",");
59
+ };
60
+ return layout(this) === layout(other);
61
+ }
62
+ static isHeaderRow(cells) {
63
+ return cells.length > 0 && cells.every((cell) => cell.tag === "th");
64
+ }
65
+ headerRowCount() {
66
+ for (let rowIndex = 0; rowIndex < this.rows.length; rowIndex++) {
67
+ if (this.rows[rowIndex].container !== "thead" && !TableVersion.isHeaderRow(this.cells[rowIndex])) {
68
+ return rowIndex;
69
+ }
70
+ }
71
+ return this.rows.length;
72
+ }
73
+ isSequenceColumn(columnIndex) {
74
+ const bodyRows = (0, helpers_1.range)(0, this.rows.length).filter((rowIndex) => !TableVersion.isHeaderRow(this.cells[rowIndex]));
75
+ return bodyRows.length > 0 && bodyRows.every((rowIndex, position) => this.signatureAt(rowIndex, columnIndex) === String(position + 1));
76
+ }
77
+ columnValueCounts() {
78
+ return (0, helpers_1.range)(0, this.columnCount).map((columnIndex) => (0, similarity_1.countValues)(this.signatures.map((rowSignatures) => (rowSignatures[columnIndex] === undefined ? "" : rowSignatures[columnIndex]))));
79
+ }
80
+ valueCounts() {
81
+ return (0, similarity_1.countValues)((0, helpers_1.flatten)(this.signatures));
82
+ }
83
+ ownerOf(cell) {
84
+ if (cell.partOf === null) {
85
+ return cell;
86
+ }
87
+ const partOf = cell.partOf;
88
+ return (0, helpers_1.flatten)(this.cells).filter((other) => other.mergedKey === partOf)[0];
89
+ }
90
+ rowKeys(rowIndex) {
91
+ return TableVersion.keysOf(this.cells[rowIndex], (cell) => this.ownerOf(cell));
92
+ }
93
+ rowKeysByRow() {
94
+ if (!this.hasRowKeys) {
95
+ return this.rows.map(() => []);
96
+ }
97
+ const owners = new Map();
98
+ (0, helpers_1.flatten)(this.cells).forEach((cell) => {
99
+ if (cell.mergedKey !== null && !owners.has(cell.mergedKey)) {
100
+ owners.set(cell.mergedKey, cell);
101
+ }
102
+ });
103
+ return this.cells.map((rowCells) => TableVersion.keysOf(rowCells, (cell) => (cell.partOf === null ? cell : owners.get(cell.partOf))));
104
+ }
105
+ static keysOf(rowCells, ownerOf) {
106
+ const keys = [];
107
+ (rowCells || []).forEach((cell) => {
108
+ const owner = ownerOf(cell);
109
+ const key = owner ? owner.ownId() : null;
110
+ if (key !== null) {
111
+ keys.push(key);
112
+ }
113
+ });
114
+ return keys;
115
+ }
116
+ groupSignatures(rowIndex) {
117
+ return (0, helpers_1.flatten)(this.cells[rowIndex].map((cell) => {
118
+ const key = cell.mergedKey !== null ? cell.mergedKey : cell.partOf;
119
+ const signature = key === null ? undefined : this.mergedCells[key];
120
+ return signature === undefined || signature === "" ? [] : [signature];
121
+ }));
122
+ }
123
+ isContinuationRow(rowIndex) {
124
+ const rowCells = this.cells[rowIndex];
125
+ return rowCells.some((cell) => cell.partOf !== null && !rowCells.some((other) => other.mergedKey === cell.partOf));
126
+ }
127
+ }
128
+ exports.TableVersion = TableVersion;
@@ -0,0 +1,6 @@
1
+ export declare const TABLE_ID_PREFIX = "redline-table-";
2
+ export declare const ROW_ADDED_CLASS = "table-row-added";
3
+ export declare const ROW_DELETED_CLASS = "table-row-deleted";
4
+ export declare const CELL_ADDED_CLASS = "table-cell-added";
5
+ export declare const CELL_DELETED_CLASS = "table-cell-deleted";
6
+ export declare type CellDiff = (before: string, after: string) => string;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CELL_DELETED_CLASS = exports.CELL_ADDED_CLASS = exports.ROW_DELETED_CLASS = exports.ROW_ADDED_CLASS = exports.TABLE_ID_PREFIX = void 0;
4
+ exports.TABLE_ID_PREFIX = "redline-table-";
5
+ exports.ROW_ADDED_CLASS = "table-row-added";
6
+ exports.ROW_DELETED_CLASS = "table-row-deleted";
7
+ exports.CELL_ADDED_CLASS = "table-cell-added";
8
+ exports.CELL_DELETED_CLASS = "table-cell-deleted";
@@ -0,0 +1,4 @@
1
+ export declare function range(start: number, end: number): number[];
2
+ export declare function flatten<T>(arrays: T[][]): T[];
3
+ export declare function last<T>(array: T[]): T | undefined;
4
+ export declare function uniqueValues<T>(values: T[]): T[];
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.uniqueValues = exports.last = exports.flatten = exports.range = void 0;
4
+ function range(start, end) {
5
+ const result = [];
6
+ for (let i = start; i < end; i++) {
7
+ result.push(i);
8
+ }
9
+ return result;
10
+ }
11
+ exports.range = range;
12
+ function flatten(arrays) {
13
+ return [].concat(...arrays);
14
+ }
15
+ exports.flatten = flatten;
16
+ function last(array) {
17
+ return array[array.length - 1];
18
+ }
19
+ exports.last = last;
20
+ function uniqueValues(values) {
21
+ return Array.from(new Set(values));
22
+ }
23
+ exports.uniqueValues = uniqueValues;
@@ -0,0 +1,32 @@
1
+ export interface Tag {
2
+ name: string;
3
+ isComment: boolean;
4
+ isClosing: boolean;
5
+ isSelfClosing: boolean;
6
+ attributes: string;
7
+ text: string;
8
+ start: number;
9
+ end: number;
10
+ }
11
+ export interface Element {
12
+ name: string;
13
+ start: number;
14
+ openTag: string;
15
+ innerStart: number;
16
+ inner: string;
17
+ closeTag: string;
18
+ end: number;
19
+ }
20
+ export interface Replacement {
21
+ start: number;
22
+ end: number;
23
+ html: string;
24
+ }
25
+ export declare function scanTags(html: string, callback: (tag: Tag) => void): void;
26
+ export declare function findElements(html: string, tagNames: string[]): Element[];
27
+ export declare function getTagAttribute(tag: string, name: string): string | null;
28
+ export declare function setTagAttribute(tag: string, name: string, value: string | number): string;
29
+ export declare function removeTagAttribute(tag: string, name: string): string;
30
+ export declare function addTagClass(tag: string, className: string): string;
31
+ export declare function decodeEntities(text: string): string;
32
+ export declare function replaceRanges(html: string, replacements: Replacement[]): string;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.replaceRanges = exports.decodeEntities = exports.addTagClass = exports.removeTagAttribute = exports.setTagAttribute = exports.getTagAttribute = exports.findElements = exports.scanTags = void 0;
4
+ const TAG_REGEXP = /<!--[\s\S]*?-->|<(\/?)([a-zA-Z][\w-]*)((?:[^>"']|"[^"]*"|'[^']*')*)>/g;
5
+ const VOID_TAG_NAMES = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"];
6
+ function scanTags(html, callback) {
7
+ const regExp = new RegExp(TAG_REGEXP.source, "g");
8
+ let match;
9
+ while ((match = regExp.exec(html))) {
10
+ const name = match[2] ? match[2].toLowerCase() : "";
11
+ callback({
12
+ name,
13
+ isComment: !match[2],
14
+ isClosing: match[1] === "/",
15
+ isSelfClosing: !match[2] || /\/\s*>$/.test(match[0]) || VOID_TAG_NAMES.indexOf(name) !== -1,
16
+ attributes: match[3],
17
+ text: match[0],
18
+ start: match.index,
19
+ end: match.index + match[0].length,
20
+ });
21
+ }
22
+ }
23
+ exports.scanTags = scanTags;
24
+ function findElements(html, tagNames) {
25
+ const elements = [];
26
+ let tableDepth = 0;
27
+ let current = null;
28
+ scanTags(html, (tag) => {
29
+ if (!tag.isClosing && tableDepth === 0 && !current && tagNames.indexOf(tag.name) !== -1) {
30
+ current = { name: tag.name, start: tag.start, openTag: tag.text, innerStart: tag.end };
31
+ }
32
+ if (tag.name === "table") {
33
+ tableDepth += tag.isClosing ? -1 : 1;
34
+ }
35
+ if (tag.isClosing && tableDepth === 0 && current && tag.name === current.name) {
36
+ current.inner = html.slice(current.innerStart, tag.start);
37
+ current.closeTag = tag.text;
38
+ current.end = tag.end;
39
+ elements.push(current);
40
+ current = null;
41
+ }
42
+ });
43
+ return elements;
44
+ }
45
+ exports.findElements = findElements;
46
+ const attributeRegExps = new Map();
47
+ function attributeRegExp(name) {
48
+ let regExp = attributeRegExps.get(name);
49
+ if (!regExp) {
50
+ regExp = new RegExp("(\\s" + name + ")(\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s\"'>]+))?(?=[\\s/>])", "i");
51
+ attributeRegExps.set(name, regExp);
52
+ }
53
+ return regExp;
54
+ }
55
+ function unquote(value) {
56
+ return /^["']/.test(value) ? value.slice(1, -1) : value;
57
+ }
58
+ function getTagAttribute(tag, name) {
59
+ const match = attributeRegExp(name).exec(tag);
60
+ if (!match) {
61
+ return null;
62
+ }
63
+ return match[3] === undefined ? "" : unquote(match[3]);
64
+ }
65
+ exports.getTagAttribute = getTagAttribute;
66
+ function setTagAttribute(tag, name, value) {
67
+ const match = attributeRegExp(name).exec(tag);
68
+ const attribute = " " + name + '="' + String(value).replace(/"/g, "&quot;") + '"';
69
+ if (match) {
70
+ return tag.slice(0, match.index) + attribute + tag.slice(match.index + match[0].length);
71
+ }
72
+ return tag.replace(/\s*\/?>$/, attribute + "$&");
73
+ }
74
+ exports.setTagAttribute = setTagAttribute;
75
+ function removeTagAttribute(tag, name) {
76
+ const match = attributeRegExp(name).exec(tag);
77
+ return match ? tag.slice(0, match.index) + tag.slice(match.index + match[0].length) : tag;
78
+ }
79
+ exports.removeTagAttribute = removeTagAttribute;
80
+ function addTagClass(tag, className) {
81
+ const current = getTagAttribute(tag, "class");
82
+ const tokens = current === null ? [] : current.split(/\s+/).filter((token) => token !== "");
83
+ if (tokens.indexOf(className) !== -1) {
84
+ return tag;
85
+ }
86
+ return setTagAttribute(tag, "class", tokens.concat(className).join(" "));
87
+ }
88
+ exports.addTagClass = addTagClass;
89
+ const NAMED_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: "\u00A0" };
90
+ function decodeEntities(text) {
91
+ return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (entity, body) => {
92
+ const lower = body.toLowerCase();
93
+ if (lower.charAt(0) === "#") {
94
+ const code = lower.charAt(1) === "x" ? parseInt(lower.slice(2), 16) : parseInt(lower.slice(1), 10);
95
+ return isNaN(code) ? entity : String.fromCharCode(code);
96
+ }
97
+ return Object.prototype.hasOwnProperty.call(NAMED_ENTITIES, lower) ? NAMED_ENTITIES[lower] : entity;
98
+ });
99
+ }
100
+ exports.decodeEntities = decodeEntities;
101
+ function replaceRanges(html, replacements) {
102
+ return replacements
103
+ .slice()
104
+ .sort((first, second) => second.start - first.start)
105
+ .reduce((result, replacement) => result.slice(0, replacement.start) + replacement.html + result.slice(replacement.end), html);
106
+ }
107
+ exports.replaceRanges = replaceRanges;
@@ -0,0 +1,2 @@
1
+ export { TableRedlining, PreparedHtml } from "./TableRedlining";
2
+ export { CellDiff } from "./constants";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TableRedlining = void 0;
4
+ var TableRedlining_1 = require("./TableRedlining");
5
+ Object.defineProperty(exports, "TableRedlining", { enumerable: true, get: function () { return TableRedlining_1.TableRedlining; } });
@@ -0,0 +1,10 @@
1
+ export declare type ValueCounts = Record<string, number>;
2
+ export declare function countValues(values: string[]): ValueCounts;
3
+ export declare function countsSize(counts: ValueCounts): number;
4
+ export declare function retainedShare(oldCounts: ValueCounts, newCounts: ValueCounts): number;
5
+ export declare function valueOverlap(oldCounts: ValueCounts, newCounts: ValueCounts): number;
6
+ export declare function sharedShare(oldCounts: ValueCounts, newCounts: ValueCounts): number;
7
+ export declare function distinctSharedShare(oldCounts: ValueCounts, newCounts: ValueCounts): number;
8
+ export declare function cellSimilarity(oldSignature: string, newSignature: string): number;
9
+ export declare function signatureWords(signature: string): string[];
10
+ export declare function wordSimilarity(oldWords: string[], newWords: string[]): number;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.wordSimilarity = exports.signatureWords = exports.cellSimilarity = exports.distinctSharedShare = exports.sharedShare = exports.valueOverlap = exports.retainedShare = exports.countsSize = exports.countValues = void 0;
4
+ const helpers_1 = require("./helpers");
5
+ function countValues(values) {
6
+ const counts = Object.create(null);
7
+ values.forEach((value) => {
8
+ if (value !== "") {
9
+ counts[value] = (counts[value] || 0) + 1;
10
+ }
11
+ });
12
+ return counts;
13
+ }
14
+ exports.countValues = countValues;
15
+ function countsSize(counts) {
16
+ return Object.keys(counts).length;
17
+ }
18
+ exports.countsSize = countsSize;
19
+ function retainedShare(oldCounts, newCounts) {
20
+ let shared = 0;
21
+ let total = 0;
22
+ Object.keys(oldCounts).forEach((value) => {
23
+ shared += Math.min(oldCounts[value], newCounts[value] || 0);
24
+ total += oldCounts[value];
25
+ });
26
+ return total === 0 ? 0 : shared / total;
27
+ }
28
+ exports.retainedShare = retainedShare;
29
+ function valueOverlap(oldCounts, newCounts) {
30
+ if (countsSize(oldCounts) === 0 && countsSize(newCounts) === 0) {
31
+ return NaN;
32
+ }
33
+ let shared = 0;
34
+ let total = 0;
35
+ (0, helpers_1.uniqueValues)(Object.keys(oldCounts).concat(Object.keys(newCounts))).forEach((value) => {
36
+ const oldCount = oldCounts[value] || 0;
37
+ const newCount = newCounts[value] || 0;
38
+ shared += Math.min(oldCount, newCount);
39
+ total += Math.max(oldCount, newCount);
40
+ });
41
+ return shared / total;
42
+ }
43
+ exports.valueOverlap = valueOverlap;
44
+ function sharedShare(oldCounts, newCounts) {
45
+ let oldTotal = 0;
46
+ let newTotal = 0;
47
+ let shared = 0;
48
+ Object.keys(oldCounts).forEach((value) => {
49
+ oldTotal += oldCounts[value];
50
+ });
51
+ Object.keys(newCounts).forEach((value) => {
52
+ newTotal += newCounts[value];
53
+ });
54
+ if (oldTotal === 0 && newTotal === 0) {
55
+ return NaN;
56
+ }
57
+ if (oldTotal === 0 || newTotal === 0) {
58
+ return 0;
59
+ }
60
+ Object.keys(oldCounts).forEach((value) => {
61
+ shared += Math.min(oldCounts[value], newCounts[value] || 0);
62
+ });
63
+ return shared / Math.min(oldTotal, newTotal);
64
+ }
65
+ exports.sharedShare = sharedShare;
66
+ function distinctSharedShare(oldCounts, newCounts) {
67
+ let oldTotal = 0;
68
+ let newTotal = 0;
69
+ Object.keys(oldCounts).forEach((value) => {
70
+ oldTotal += oldCounts[value];
71
+ });
72
+ Object.keys(newCounts).forEach((value) => {
73
+ newTotal += newCounts[value];
74
+ });
75
+ if (oldTotal === 0 && newTotal === 0) {
76
+ return NaN;
77
+ }
78
+ if (oldTotal === 0 || newTotal === 0) {
79
+ return 0;
80
+ }
81
+ const shared = Object.keys(oldCounts).filter((value) => newCounts[value] !== undefined).length;
82
+ return shared / Math.min(oldTotal, newTotal);
83
+ }
84
+ exports.distinctSharedShare = distinctSharedShare;
85
+ function cellSimilarity(oldSignature, newSignature) {
86
+ if (oldSignature === newSignature) {
87
+ return 1;
88
+ }
89
+ return wordSimilarity(signatureWords(oldSignature), signatureWords(newSignature));
90
+ }
91
+ exports.cellSimilarity = cellSimilarity;
92
+ function signatureWords(signature) {
93
+ return (0, helpers_1.uniqueValues)(signature.split(/[\s|]+/).filter((word) => word !== ""));
94
+ }
95
+ exports.signatureWords = signatureWords;
96
+ function wordSimilarity(oldWords, newWords) {
97
+ const newSet = new Set(newWords);
98
+ let shared = 0;
99
+ oldWords.forEach((word) => {
100
+ if (newSet.has(word)) {
101
+ shared++;
102
+ }
103
+ });
104
+ const total = oldWords.length + newWords.length - shared;
105
+ return total === 0 ? 1 : shared / total;
106
+ }
107
+ exports.wordSimilarity = wordSimilarity;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@matrixreq/htmldiff",
3
3
  "description": "Diff and markup HTML with <ins> and <del> tags",
4
4
  "companyname": "Matrix Requirements",
5
- "version": "1.5.0",
5
+ "version": "2.0.0",
6
6
  "keywords": [
7
7
  "diff",
8
8
  "html",
@@ -13,7 +13,7 @@
13
13
  "homepage": "https://github.com/MatrixRequirements/htmldiff.js",
14
14
  "repository": {
15
15
  "type": "git",
16
- "url": "git+https://github.com/MatrixRequirements/htmldiff.js"
16
+ "url": "git+https://github.com/MatrixRequirements/htmldiff.js.git"
17
17
  },
18
18
  "readme": "README.md",
19
19
  "bugs": {
@@ -22,34 +22,41 @@
22
22
  "license": "MIT",
23
23
  "main": "js/htmldiff.js",
24
24
  "bin": {
25
- "htmldiff": "./htmldiff-cli.js"
25
+ "htmldiff": "htmldiff-cli.js"
26
26
  },
27
27
  "files": [
28
- "js/htmldiff.js",
29
- "js/htmldiff.d.ts",
28
+ "js/**/*.js",
29
+ "js/**/*.d.ts",
30
30
  "sample/*",
31
- "test/*",
32
31
  "htmldiff-cli.js"
33
32
  ],
34
33
  "types": "js/htmldiff.d.ts",
35
- "dependencies": {},
36
34
  "devDependencies": {
35
+ "@types/chai": "^4.3.20",
36
+ "@types/mocha": "^10.0.10",
37
37
  "@types/node": "18.7.11",
38
38
  "@typescript-eslint/eslint-plugin": "5.34.0",
39
39
  "@typescript-eslint/parser": "5.34.0",
40
40
  "chai": "*",
41
41
  "eslint": "8.22.0",
42
42
  "eslint-plugin-jsdoc": "39.3.6",
43
+ "mocha": "*",
43
44
  "pkg": "5.8.0",
44
- "typescript": "4.7.4",
45
- "mocha": "*"
45
+ "ts-node": "^10.9.2",
46
+ "typescript": "4.7.4"
46
47
  },
47
48
  "scripts": {
48
- "test": "mocha -R min",
49
- "testsample": "node htmldiff-cli.js sample/before.html sample/after.html - -c myDiffClass -p myPrefix",
50
- "lint": "eslint -c eslintrc.json --parser-options=project:./tsconfig.json --ext .ts ./",
49
+ "prebuild": "node -e \"require('fs').rmSync('js',{recursive:true,force:true})\"",
50
+ "build": "tsc -p tsconfig.build.json",
51
+ "build:cli": "tsc -p tsconfig.cli.json",
52
+ "prepare": "npm run build",
53
+ "test": "npm run build && tsc -p tsconfig.json && mocha -R min",
54
+ "lint": "npm run build && eslint -c eslintrc.json --ext .ts ./",
55
+ "verify": "npm run build && tsc -p tsconfig.json && mocha -R min && eslint -c eslintrc.json --ext .ts ./ && npm run build:cli",
56
+ "prepublishOnly": "npm run verify",
51
57
  "premake": "npm run lint",
52
- "make": "tsc -p tsconfig.json && node -e \"require('fs').unlinkSync('./htmldiff-cli.d.ts')\"",
58
+ "make": "npm run build && npm run build:cli",
59
+ "testsample": "node htmldiff-cli.js sample/before.html sample/after.html - -c myDiffClass -p myPrefix",
53
60
  "pack-macos-aarch64": "pkg -t node18-macos-arm64 -C GZip --output ./bin/macos-aarch64/htmldiff-cli htmldiff-cli.js -c ./pkg.json",
54
61
  "pack-macos-x64": " pkg -t node18-macos-x64 -C GZip --output ./bin/macos-x64/htmldiff-cli htmldiff-cli.js -c ./pkg.json",
55
62
  "pack-win32-x64": " pkg -t node18-win32-x64 -C GZip --output ./bin/win32-x64/htmldiff-cli htmldiff-cli.js -c ./pkg.json",
@@ -57,4 +64,4 @@
57
64
  "pack-linux-aarch64": "pkg -t node18-linuxstatic-arm64 -C GZip --output ./bin/linux-aarch64/htmldiff-cli htmldiff-cli.js -c ./pkg.json",
58
65
  "pack-all": "npm run pack-macos-aarch64 && npm run pack-macos-x64 && npm run pack-win32-x64 && npm run pack-linux-x64 && npm run pack-linux-aarch64"
59
66
  }
60
- }
67
+ }