@preventive/compare 0.0.0-stage → 1.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.
package/src/words.js ADDED
@@ -0,0 +1,57 @@
1
+ // Changed lines read word by word: what changed within a line and the one it
2
+ // pairs with, as one shortest edit script between their words finds it.
3
+
4
+ import { script } from './script.js'
5
+
6
+ // Words as a word diff compares them, so `foo(a)` -> `foo(b)` marks `a` alone.
7
+ const TOKEN = /[\p{L}\p{N}_$]+|\s+|[^\p{L}\p{N}_$\s]/gu
8
+ // Past these a word diff is noise (minified lines) or too slow to be worth it.
9
+ const MAX_CHARS = 2000
10
+ const MAX_TOKENS = 800
11
+ // Lines whose words are mostly replaced are not one line edited.
12
+ export const MAX_CHANGED = 0.6
13
+
14
+ // A line as a word diff reads it, each distinct word numbered in `ids` as the
15
+ // script compares them; null past the size limits.
16
+ export function tokenized(line, ids) {
17
+ if (line.length > MAX_CHARS) return null
18
+ const tokens = line.match(TOKEN) ?? []
19
+ if (tokens.length > MAX_TOKENS) return null
20
+ return { line, tokens, words: tokens.map((token) => {
21
+ if (!ids.has(token)) ids.set(token, ids.size)
22
+ return ids.get(token)
23
+ }) }
24
+ }
25
+
26
+ // A line's words the script changed as ranges `[from, to)`, those split by blanks alone merged.
27
+ function changedRanges({ tokens, line }, kept) {
28
+ const ranges = []
29
+ for (let at = 0, i = 0; i < tokens.length; at += tokens[i++].length) {
30
+ if (kept[i]) continue
31
+ const last = ranges.at(-1)
32
+ if (last && /^\s*$/u.test(line.slice(last[1], at))) last[1] = at + tokens[i].length
33
+ else ranges.push([at, at + tokens[i].length])
34
+ }
35
+ return ranges
36
+ }
37
+
38
+ const part = (ranges, line) => ranges.reduce((sum, [from, to]) => sum + to - from, 0) / Math.max(1, line.length)
39
+
40
+ // What a word view marks in two lines read: the ranges, the smaller share of
41
+ // either they cover (all of it where the script keeps no word), and the work.
42
+ export function wordView(a, b) {
43
+ const kept = { a: a.words.map(() => 0), b: b.words.map(() => 0) }
44
+ const steps = script(a.words, b.words, kept)
45
+ const ranges = { a: changedRanges(a, kept.a), b: changedRanges(b, kept.b) }
46
+ return { ranges, share: kept.a.includes(1) ? Math.min(part(ranges.a, a.line), part(ranges.b, b.line)) : 1, steps }
47
+ }
48
+
49
+ // Lines the same have no ranges, not null: null is a line with no word view.
50
+ export function wordRanges(left, right) {
51
+ if (left === right) return { a: [], b: [] }
52
+ const ids = new Map()
53
+ const a = tokenized(left, ids), b = tokenized(right, ids)
54
+ if (a === null || b === null) return null
55
+ const { ranges, share } = wordView(a, b)
56
+ return share <= MAX_CHANGED ? ranges : null
57
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.