@createiq/htmldiff 1.0.1 → 1.0.2

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/.editorconfig ADDED
@@ -0,0 +1,9 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
package/.gitlab-ci.yml CHANGED
@@ -1,15 +1,17 @@
1
1
  stages:
2
2
  - test
3
3
  - deploy
4
+ - generate_artifacts
4
5
 
5
6
  default:
6
7
  interruptible: true
7
8
 
8
9
  "Run unit tests":
9
10
  stage: test
10
- image: ${CI_REGISTRY}/infra/baseimages/node:22-alpine
11
+ image: ${ACR_HOSTNAME}/infra/baseimages/node:22-alpine
12
+ before_script:
13
+ - npm ci --prefer-offline --no-audit
11
14
  script:
12
- - npm i
13
15
  - VITEST_MAX_THREADS=4 npm run test:ci -- --coverage
14
16
  allow_failure: false
15
17
  needs: []
@@ -26,12 +28,14 @@ default:
26
28
  - if: $CI_PIPELINE_SOURCE == "merge_request_event"
27
29
  - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_TRIGGERED != "true"
28
30
  - if: $CI_COMMIT_BRANCH == "prod" && $CI_PIPELINE_TRIGGERED != "true"
31
+ tags: ["kluster:aks-ciqi-eun"]
29
32
 
30
33
  "Lint":
31
34
  stage: test
32
- image: ${CI_REGISTRY}/infra/baseimages/node:22-alpine
35
+ image: ${ACR_HOSTNAME}/infra/baseimages/node:22-alpine
36
+ before_script:
37
+ - npm ci --prefer-offline --no-audit
33
38
  script:
34
- - npm i
35
39
  - npm run lint
36
40
  - npm run format
37
41
  allow_failure: false
@@ -40,32 +44,101 @@ default:
40
44
  - if: $CI_PIPELINE_SOURCE == "merge_request_event"
41
45
  - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_TRIGGERED != "true"
42
46
  - if: $CI_COMMIT_BRANCH == "prod" && $CI_PIPELINE_TRIGGERED != "true"
47
+ tags: ["kluster:aks-ciqi-eun"]
43
48
 
49
+ # TODO this runs on gke01-london for firewall reasons to be able to talk to registry.npmjs.org
44
50
  "Publish to npm - dry-run":
45
51
  stage: deploy
46
52
  image: ${CI_REGISTRY}/infra/baseimages/node:22-alpine
53
+ before_script:
54
+ - npm ci --prefer-offline --no-audit
47
55
  script:
48
- - npm i
49
56
  - npm run build
50
57
  # Use the default registry for publishing
51
58
  - npm --verbose config --global delete registry
52
- - NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt HTTPS_PROXY=$PROJENV_HTTPS_PROXY NO_PROXY=$PROJENV_NO_PROXY npm publish --access public --dry-run
59
+ - npm publish --access public --dry-run
53
60
  allow_failure: false
54
61
  # No needs, only run if the test stage passes
55
62
  rules:
56
63
  - if: $CI_PIPELINE_SOURCE == "merge_request_event"
57
64
  - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_TRIGGERED != "true"
65
+ tags: ["kluster:gke01-london"]
58
66
 
67
+ # TODO this runs on gke01-london for firewall reasons to be able to talk to registry.npmjs.org
59
68
  "Publish to npm":
60
69
  stage: deploy
61
70
  image: ${CI_REGISTRY}/infra/baseimages/node:22-alpine
71
+ before_script:
72
+ - npm ci --prefer-offline --no-audit
62
73
  script:
63
- - npm i
64
74
  - npm run build
65
75
  # Use the default registry for publishing
66
76
  - npm --verbose config --global delete registry
67
- - NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt HTTPS_PROXY=$PROJENV_HTTPS_PROXY NO_PROXY=$PROJENV_NO_PROXY npm publish --access public
77
+ - npm publish --access public
68
78
  allow_failure: false
69
79
  # No needs, only run if the test stage passes
70
80
  rules:
71
- - if: $CI_COMMIT_BRANCH == "prod" && $CI_PIPELINE_TRIGGERED != "true"
81
+ - if: $CI_COMMIT_BRANCH == "prod" && $CI_PIPELINE_TRIGGERED != "true"
82
+ tags: ["kluster:gke01-london"]
83
+
84
+ .generate_sbom: &generate_sbom
85
+ stage: generate_artifacts
86
+ image: ${ACR_HOSTNAME}/infra/baseimages/node:22-alpine
87
+ before_script:
88
+ - npm ci --prefer-offline --no-audit
89
+ script:
90
+ - mkdir -p cyclonedx
91
+ - npm exec -- @cyclonedx/cyclonedx-npm --package-lock-only --omit dev --spec-version 1.6 --mc-type library --output-file cyclonedx/bom_${CI_COMMIT_SHORT_SHA}.json --validate
92
+ artifacts:
93
+ name: "sbom_htmldiff_$CI_COMMIT_SHORT_SHA"
94
+ paths:
95
+ - cyclonedx/*
96
+ expire_in: 1 month
97
+ tags: ["kluster:aks-ciqi-eun"]
98
+
99
+ .push_sbom_deptrack: &push_sbom_deptrack
100
+ stage: generate_artifacts
101
+ variables:
102
+ DTRACK_SERVER: https://dependencytrack.stag.a.nakhoda.ai
103
+ DTRACK_API_KEY: odt_DKxpr0Sx_TmAc11wSPPl1YphFxEuiwDPb1VmSRY7d
104
+ DTRACK_PROJECT: 2f456575-183e-4985-99f6-ad8767b86476
105
+ script:
106
+ - |-
107
+ cat <<EOF >payload.json
108
+ {
109
+ "project": "${DTRACK_PROJECT}",
110
+ "bom": "$(base64 --wrap=0 cyclonedx/bom_${CI_COMMIT_SHORT_SHA}.json)"
111
+ }
112
+ EOF
113
+ # Upload using a limited permission API key that can only upload SBOMs
114
+ - |
115
+ curl --fail-with-body -X "PUT" "${DTRACK_SERVER}/api/v1/bom" \
116
+ -H 'Content-Type: application/json' \
117
+ -H "X-API-Key: ${DTRACK_API_KEY}" \
118
+ -d @payload.json
119
+ tags: ["kluster:aks-ciqi-eun"]
120
+
121
+ "Generate SBOM":
122
+ <<: *generate_sbom
123
+ rules:
124
+ - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_TRIGGERED != "true"
125
+
126
+ "Push SBOM to Dependency-Track":
127
+ <<: *push_sbom_deptrack
128
+ needs:
129
+ - "Generate SBOM"
130
+ rules:
131
+ - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_TRIGGERED != "true"
132
+
133
+ # Don't need this on every build but useful for testing changes to the above
134
+ #"MR: Generate SBOM":
135
+ # <<: *generate_sbom
136
+ # rules:
137
+ # - if: $CI_PIPELINE_SOURCE == "merge_request_event"
138
+ #
139
+ #"MR: Push SBOM to Dependency-Track":
140
+ # <<: *push_sbom_deptrack
141
+ # needs:
142
+ # - "MR: Generate SBOM"
143
+ # rules:
144
+ # - if: $CI_PIPELINE_SOURCE == "merge_request_event"
@@ -0,0 +1 @@
1
+ npm exec -- lint-staged
package/README.md CHANGED
@@ -1,5 +1,140 @@
1
- ## Project Description
1
+ ## @createiq/htmldiff
2
2
 
3
- A library for comparing two HTML files/snippets and highlighting the differences using simple HTML.
3
+ A library for comparing two HTML snippets and highlighting the differences using simple HTML.
4
4
 
5
- This HTML Diff implementation is a TypeScript port of the C# port found [here](https://github.com/Rohland/htmldiff.net), which is a port of the ruby implementation found [here](https://github.com/myobie/htmldiff).
5
+ This HTML Diff implementation is a TypeScript port of the C# port found [here](https://github.com/Rohland/htmldiff.net),
6
+ which is a port of the ruby implementation found [here](https://github.com/myobie/htmldiff).
7
+
8
+ ## Installation
9
+
10
+ `npm install @createiq/htmldiff`
11
+
12
+ ## Usage
13
+
14
+ This is intended to be a drop-in replacement for the `htmldiff` npm library (https://github.com/dfoverdx/htmldiff-js),
15
+ and intentionally mirrors the structure and classes of the [HtmlDiff.NET](https://github.com/Rohland/htmldiff.net)
16
+ library that was initially ported, hence usage differing slightly from many JavaScript-ecosystem libraries.
17
+
18
+ ```ts
19
+ import HtmlDiff from '@createiq/htmldiff'
20
+
21
+ const oldHtml = `<p>Some <em>old</em> html here</p>`
22
+ const newHtml = `<p>Some <b>new</b> html goes here</p>`
23
+
24
+ const diffHtml = HtmlDiff.execute(oldHtml, newHtml)
25
+
26
+ // Output: <p>Some <ins class='mod em'><del class='diffmod'>old</del></ins><b><ins class='mod b'><ins class='diffmod'>new</ins></ins></b> html <ins class='diffins'>goes </ins>here</p>
27
+ ```
28
+
29
+ ### Configuring the diff output
30
+
31
+ There are a couple of options that can be set directly on the `HtmlDiff` instance, `HtmlDiff.execute` will always create
32
+ a new instance and use defaults, but for custom output you can use `new HtmlDiff(oldHtml, newHtml)` and set the options
33
+ directly:
34
+
35
+ ```ts
36
+ import HtmlDiff from '@createiq/htmldiff'
37
+
38
+ const oldHtml = `<p>Some <em>old</em> html here</p>`
39
+ const newHtml = `<p>Some <b>new</b> html goes here</p>`
40
+
41
+ const diff = new HtmlDiff(oldHtml, newHtml)
42
+ diff.repeatingWordsAccuracy = 0.5
43
+ diff.ignoreWhitespaceDifferences = true
44
+ diff.orphanMatchThreshold = 0.2
45
+
46
+ const diffHtml = diff.build()
47
+ ```
48
+
49
+ ## API
50
+
51
+ ### HtmlDiff.execute(oldHtml, newHtml)
52
+
53
+ Returns a HTML diff from `oldHtml` to `newHtml` with the default options. This is exactly the same as calling
54
+ `new HtmlDiff(oldHtml, newHtml).build()`
55
+
56
+ ### new HtmlDiff(oldHtml, newHtml)
57
+
58
+ Returns an HtmlDiff instance.
59
+
60
+ #### addBlockExpression(expression: RegExp)
61
+
62
+ Uses `expression` to group text together so that any change detected within the group is treated as a single block, for
63
+ example to keep dates together.
64
+
65
+ This **MUST** have the global flag (`/g`) enabled on the RegExp and must not overlap with other block expressions.
66
+
67
+ Example:
68
+
69
+ ```ts
70
+ const oldHtml = 'This is a date 1 Jan 2016 that will change'
71
+ const newHtml = 'This is a date 22 Feb 2017 that did change'
72
+
73
+ const diff = new HtmlDiff(oldHtml, newHtml)
74
+ diff.addBlockExpression(/\d{1,2}\s*(Jan|Feb)\s*\d{4}/g)
75
+
76
+ const diffHtml = diff.build()
77
+
78
+ // Output: This is a date<del class='diffmod'> 1 Jan 2016</del><ins class='diffmod'> 22 Feb 2017</ins> that <del class='diffmod'>will</del><ins class='diffmod'>did</ins> change
79
+ ```
80
+
81
+ #### .repeatingWordsAccuracy <sup>get/set</sup>
82
+
83
+ Type: `number`\
84
+ Default: `1.0`
85
+
86
+ Defines how to compare repeating words. Valid values are from 0 to 1. This value allows to exclude some words from
87
+ comparison that eventually reduces the total time of the diff algorithm.
88
+
89
+ - `0` means that all words are excluded so the diff will not find any matching words at all.
90
+ - `1` (default value) means that all words participate in comparison so this is the most accurate case.
91
+ - `0.5` means that any word that occurs more than 50% times may be excluded from comparison. This doesn't
92
+ mean that such words will definitely be excluded but only gives a permission to exclude them if necessary.
93
+
94
+ #### .ignoreWhitespaceDifferences <sup>get/set</sup>
95
+
96
+ Type: `boolean`\
97
+ Default: `false`
98
+
99
+ If true all whitespaces are considered as equal
100
+
101
+ #### .orphanMatchThreshold <sup>get/set</sup>
102
+
103
+ Type: `number`\
104
+ Default: `0.0`
105
+
106
+ If some match is too small and located far from its neighbors then it is considered as orphan and removed. For example:
107
+
108
+ ```
109
+ aaaaa bb ccccccccc dddddd ee
110
+ 11111 bb 222222222 dddddd ee
111
+ ```
112
+
113
+ will find two matches `bb` and `dddddd ee` but the first will be considered as orphan and ignored, as result it will
114
+ consider texts `aaaaa bb ccccccccc` and `11111 bb 222222222` as single replacement:
115
+
116
+ ```html
117
+ <del>aaaaa bb ccccccccc</del><ins>11111 bb 222222222</ins> dddddd ee
118
+ ```
119
+
120
+ This property defines relative size of the match to be considered as orphan, from 0 to 1.
121
+
122
+ - `1` means that all matches will be considered as orphans.
123
+ - `0` (default) means that no match will be considered as orphan.
124
+ - `0.2` means that if match length is less than 20% of distance between its neighbors it is considered as orphan.
125
+
126
+ #### build()
127
+
128
+ Returns the diff from an HtmlDiff instance.
129
+
130
+ ## Contributing
131
+
132
+ The library uses [Biome](https://biomejs.dev/) for linting and formatting, and [Vitest](https://vitest.dev/) for unit
133
+ tests and benchmarking. It's worth ensuring that you have appropriate plugins for your development environment,
134
+ particularly for Biome to avoid having to fix formatting issues late.
135
+
136
+ ### Releasing
137
+
138
+ Merge requests to the `main` branch should be reviewed by the team as normal but will not release a new version of the
139
+ library to npm. This happens when merge requests are made to the `prod` branch, this should be an MR directly from
140
+ `main` to `prod` and **MUST** include a bump to the version in package.json satisfying semver.
package/biome.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
2
+ "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
3
3
  "formatter": {
4
4
  "enabled": true,
5
5
  "formatWithErrors": false,
@@ -9,13 +9,25 @@
9
9
  "lineWidth": 120,
10
10
  "attributePosition": "auto"
11
11
  },
12
- "organizeImports": { "enabled": true },
12
+ "assist": { "actions": { "source": { "organizeImports": "on" } } },
13
13
  "linter": {
14
14
  "enabled": true,
15
15
  "rules": {
16
16
  "recommended": true,
17
17
  "suspicious": {
18
18
  "noConfusingVoidType": "off"
19
+ },
20
+ "style": {
21
+ "noParameterAssign": "error",
22
+ "useAsConstAssertion": "error",
23
+ "useDefaultParameterLast": "error",
24
+ "useEnumInitializers": "error",
25
+ "useSelfClosingElements": "error",
26
+ "useSingleVarDeclarator": "error",
27
+ "noUnusedTemplateLiteral": "error",
28
+ "useNumberNamespace": "error",
29
+ "noInferrableTypes": "error",
30
+ "noUselessElse": "error"
19
31
  }
20
32
  }
21
33
  },
package/dist/HtmlDiff.cjs CHANGED
@@ -68,7 +68,7 @@ var closingTagTexRegex = /^\s*<\/[^>]+>\s*$/;
68
68
  var tagWordRegex = /<[^\s>]+/;
69
69
  var whitespaceRegex = /^(\s|&nbsp;)+$/;
70
70
  var wordRegex = /[\w#@]+/;
71
- var tagRegex = /<\/?(?<name>[^\s\/>]+)[^>]*>/;
71
+ var tagRegex = /<\/?(?<name>[^\s/>]+)[^>]*>/;
72
72
  var SpecialCaseWordTags = ["<img"];
73
73
  function isTag(item) {
74
74
  if (SpecialCaseWordTags.some((re) => item?.startsWith(re))) {
@@ -168,10 +168,10 @@ var MatchFinder = class _MatchFinder {
168
168
  if (key === null) {
169
169
  continue;
170
170
  }
171
- this.wordIndices = {
172
- ...this.wordIndices,
173
- [key]: [...this.wordIndices[key] ?? [], i]
174
- };
171
+ if (!this.wordIndices[key]) {
172
+ this.wordIndices[key] = [];
173
+ }
174
+ this.wordIndices[key].push(i);
175
175
  }
176
176
  }
177
177
  static putNewWord(block, word, blockSize) {
@@ -194,7 +194,12 @@ var MatchFinder = class _MatchFinder {
194
194
  findMatch() {
195
195
  this.indexNewWords();
196
196
  this.removeRepeatingWords();
197
- if (Object.keys(this.wordIndices).length === 0) {
197
+ let hasIndices = false;
198
+ for (const _key in this.wordIndices) {
199
+ hasIndices = true;
200
+ break;
201
+ }
202
+ if (!hasIndices) {
198
203
  return null;
199
204
  }
200
205
  let bestMatchInOld = this.startInOld;
@@ -209,7 +214,7 @@ var MatchFinder = class _MatchFinder {
209
214
  continue;
210
215
  }
211
216
  const newMatchLengthAt = /* @__PURE__ */ new Map();
212
- if (!Object.keys(this.wordIndices).includes(index)) {
217
+ if (!this.wordIndices[index]) {
213
218
  matchLengthAt = newMatchLengthAt;
214
219
  continue;
215
220
  }
@@ -471,7 +476,7 @@ var BlockFinder = class {
471
476
  const from = match.index;
472
477
  const to = match.index + match[0].length;
473
478
  result.addBlock(from, to);
474
- } catch (e) {
479
+ } catch {
475
480
  throw new ArgumentError(
476
481
  `One or more block expressions result in a text sequence that overlaps. Current expression: ${exp}`
477
482
  );
@@ -503,6 +508,20 @@ var HtmlDiff = class _HtmlDiff {
503
508
  "</s>",
504
509
  "</span>"
505
510
  ];
511
+ static SpecialCaseClosingTagsSet = /* @__PURE__ */ new Set([
512
+ "</strong>",
513
+ "</em>",
514
+ "</b>",
515
+ "</i>",
516
+ "</big>",
517
+ "</small>",
518
+ "</u>",
519
+ "</sub>",
520
+ "</sup>",
521
+ "</strike>",
522
+ "</s>",
523
+ "</span>"
524
+ ]);
506
525
  static SpecialCaseOpeningTagRegex = /<((strong)|(b)|(i)|(em)|(big)|(small)|(u)|(sub)|(sup)|(strike)|(s)|(span))[>\s]+/i;
507
526
  content = [];
508
527
  newText;
@@ -612,15 +631,15 @@ var HtmlDiff = class _HtmlDiff {
612
631
  this.processInsertOperation(operation, "diffmod");
613
632
  }
614
633
  processInsertOperation(operation, cssClass) {
615
- const text = this.newWords.filter((_, pos) => pos >= operation.startInNew && pos < operation.endInNew);
634
+ const text = this.newWords.slice(operation.startInNew, operation.endInNew);
616
635
  this.insertTag(_HtmlDiff.InsTag, cssClass, text);
617
636
  }
618
637
  processDeleteOperation(operation, cssClass) {
619
- const text = this.oldWords.filter((_, pos) => pos >= operation.startInOld && pos < operation.endInOld);
638
+ const text = this.oldWords.slice(operation.startInOld, operation.endInOld);
620
639
  this.insertTag(_HtmlDiff.DelTag, cssClass, text);
621
640
  }
622
641
  processEqualOperation(operation) {
623
- const result = this.newWords.filter((_, pos) => pos >= operation.startInNew && pos < operation.endInNew);
642
+ const result = this.newWords.slice(operation.startInNew, operation.endInNew);
624
643
  this.content.push(result.join(""));
625
644
  }
626
645
  /**
@@ -663,7 +682,13 @@ var HtmlDiff = class _HtmlDiff {
663
682
  let specialCaseTagInjection = "";
664
683
  let specialCaseTagInjectionIsBefore = false;
665
684
  if (_HtmlDiff.SpecialCaseOpeningTagRegex.test(words[0])) {
666
- const styledTagNames = words.filter((word) => Utils_default.isTag(word)).map((style) => Utils_default.getTagName(style)).join(" ");
685
+ const tagNames = /* @__PURE__ */ new Set();
686
+ for (const word of words) {
687
+ if (Utils_default.isTag(word)) {
688
+ tagNames.add(Utils_default.getTagName(word));
689
+ }
690
+ }
691
+ const styledTagNames = Array.from(tagNames).join(" ");
667
692
  this.specialTagDiffStack.push(words[0]);
668
693
  specialCaseTagInjection = `<ins class='mod ${styledTagNames}'>`;
669
694
  if (tag === _HtmlDiff.DelTag) {
@@ -672,7 +697,7 @@ var HtmlDiff = class _HtmlDiff {
672
697
  words.shift();
673
698
  }
674
699
  }
675
- } else if (_HtmlDiff.SpecialCaseClosingTags.includes(words[0].toLowerCase())) {
700
+ } else if (_HtmlDiff.SpecialCaseClosingTagsSet.has(words[0].toLowerCase())) {
676
701
  const openingTag = this.specialTagDiffStack.length === 0 ? null : this.specialTagDiffStack.pop();
677
702
  const openingAndClosingTagsMatch = !!openingTag && Utils_default.getTagName(openingTag) === Utils_default.getTagName(words[indexLastTagInFirstTagBlock]);
678
703
  if (!!openingTag && openingAndClosingTagsMatch) {
@@ -683,7 +708,7 @@ var HtmlDiff = class _HtmlDiff {
683
708
  }
684
709
  if (tag === _HtmlDiff.DelTag) {
685
710
  words.shift();
686
- while (words.length > 0 && _HtmlDiff.SpecialCaseClosingTags.includes(words[0].toLowerCase())) {
711
+ while (words.length > 0 && _HtmlDiff.SpecialCaseClosingTagsSet.has(words[0].toLowerCase())) {
687
712
  words.shift();
688
713
  }
689
714
  }
@@ -714,13 +739,13 @@ var HtmlDiff = class _HtmlDiff {
714
739
  }
715
740
  }
716
741
  if (indexOfFirstTag !== null) {
717
- const items2 = words.filter((s, pos) => pos >= 0 && pos < indexOfFirstTag);
742
+ const items2 = words.slice(0, indexOfFirstTag);
718
743
  if (indexOfFirstTag > 0) {
719
744
  words.splice(0, indexOfFirstTag);
720
745
  }
721
746
  return items2;
722
747
  }
723
- const items = words.filter((s, pos) => pos >= 0 && pos <= words.length);
748
+ const items = words.slice(0);
724
749
  words.splice(0, words.length);
725
750
  return items;
726
751
  }
@@ -769,9 +794,18 @@ var HtmlDiff = class _HtmlDiff {
769
794
  curr = next;
770
795
  continue;
771
796
  }
772
- const oldDistanceInChars = this.oldWords.slice(prev.endInOld, next.startInOld).reduce((acc, word) => acc + word.length, 0);
773
- const newDistanceInChars = this.newWords.slice(prev.endInNew, next.startInNew).reduce((acc, word) => acc + word.length, 0);
774
- const currMatchLengthInChars = this.newWords.slice(curr.startInNew, curr.endInNew).reduce((acc, word) => acc + word.length, 0);
797
+ let oldDistanceInChars = 0;
798
+ for (let i = prev.endInOld; i < next.startInOld; i++) {
799
+ oldDistanceInChars += this.oldWords[i].length;
800
+ }
801
+ let newDistanceInChars = 0;
802
+ for (let i = prev.endInNew; i < next.startInNew; i++) {
803
+ newDistanceInChars += this.newWords[i].length;
804
+ }
805
+ let currMatchLengthInChars = 0;
806
+ for (let i = curr.startInNew; i < curr.endInNew; i++) {
807
+ currMatchLengthInChars += this.newWords[i].length;
808
+ }
775
809
  if (currMatchLengthInChars > Math.max(oldDistanceInChars, newDistanceInChars) * this.orphanMatchThreshold) {
776
810
  yield curr;
777
811
  }