glossarist 0.4.15 → 0.4.16
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/package.json +6 -1
- package/src/concept-diff.js +19 -0
- package/src/diff/change.js +111 -0
- package/src/diff/collection-diff.js +164 -0
- package/src/diff/concept-diff.js +609 -0
- package/src/diff/diff-patch.js +234 -0
- package/src/diff/diff-renderer.js +220 -0
- package/src/diff/index.d.ts +179 -0
- package/src/diff/index.js +48 -0
- package/src/diff/list-diff.js +227 -0
- package/src/diff/text-diff.js +128 -0
- package/src/index.d.ts +21 -0
- package/src/index.js +19 -0
- package/src/models/concept.js +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glossarist",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.16",
|
|
4
4
|
"description": "JavaScript SDK for Glossarist GCR packages — read, write, validate, and manage terminology concepts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -37,6 +37,11 @@
|
|
|
37
37
|
"import": "./src/validators/index.js",
|
|
38
38
|
"default": "./src/validators/index.js"
|
|
39
39
|
},
|
|
40
|
+
"./diff": {
|
|
41
|
+
"types": "./src/diff/index.d.ts",
|
|
42
|
+
"import": "./src/diff/index.js",
|
|
43
|
+
"default": "./src/diff/index.js"
|
|
44
|
+
},
|
|
40
45
|
"./rdf": {
|
|
41
46
|
"types": "./src/rdf/index.d.ts",
|
|
42
47
|
"import": "./src/rdf/index.js",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export {
|
|
2
|
+
diffConcepts,
|
|
3
|
+
diffLocalizedConcepts,
|
|
4
|
+
diffText,
|
|
5
|
+
diffList,
|
|
6
|
+
diffSet,
|
|
7
|
+
ConceptDiff,
|
|
8
|
+
ConceptLevelDiff,
|
|
9
|
+
LocalizedConceptDiff,
|
|
10
|
+
MetadataDiff,
|
|
11
|
+
DiffStats,
|
|
12
|
+
ListDiff,
|
|
13
|
+
TextDiff,
|
|
14
|
+
TextHunk,
|
|
15
|
+
Change,
|
|
16
|
+
Added,
|
|
17
|
+
Removed,
|
|
18
|
+
Changed,
|
|
19
|
+
} from './diff/index.js';
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { GlossaristModel } from '../models/base.js';
|
|
2
|
+
import { TextDiff } from './text-diff.js';
|
|
3
|
+
|
|
4
|
+
export const CHANGE_ADDED = 'added';
|
|
5
|
+
export const CHANGE_REMOVED = 'removed';
|
|
6
|
+
export const CHANGE_CHANGED = 'changed';
|
|
7
|
+
|
|
8
|
+
function serializeValue(v) {
|
|
9
|
+
if (v == null) return null;
|
|
10
|
+
if (v instanceof GlossaristModel) return v.toJSON();
|
|
11
|
+
if (Array.isArray(v)) return v.map(serializeValue);
|
|
12
|
+
if (typeof v === 'object') {
|
|
13
|
+
const out = {};
|
|
14
|
+
for (const [k, val] of Object.entries(v)) out[k] = serializeValue(val);
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
return v;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class Change extends GlossaristModel {
|
|
21
|
+
constructor(data = {}) {
|
|
22
|
+
super();
|
|
23
|
+
this.path = data.path ?? null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get type() {
|
|
27
|
+
throw new Error(`${this.constructor.name} must override type getter`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static fromJSON(data) {
|
|
31
|
+
return deserializeChange(data);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class Added extends Change {
|
|
36
|
+
constructor(data = {}) {
|
|
37
|
+
super(data);
|
|
38
|
+
this.value = data.value ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get type() { return CHANGE_ADDED; }
|
|
42
|
+
|
|
43
|
+
toJSON() {
|
|
44
|
+
const obj = { type: CHANGE_ADDED };
|
|
45
|
+
if (this.path != null) obj.path = this.path;
|
|
46
|
+
obj.value = serializeValue(this.value);
|
|
47
|
+
return obj;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static fromJSON(data) {
|
|
51
|
+
return new Added(data);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class Removed extends Change {
|
|
56
|
+
constructor(data = {}) {
|
|
57
|
+
super(data);
|
|
58
|
+
this.value = data.value ?? null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get type() { return CHANGE_REMOVED; }
|
|
62
|
+
|
|
63
|
+
toJSON() {
|
|
64
|
+
const obj = { type: CHANGE_REMOVED };
|
|
65
|
+
if (this.path != null) obj.path = this.path;
|
|
66
|
+
obj.value = serializeValue(this.value);
|
|
67
|
+
return obj;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static fromJSON(data) {
|
|
71
|
+
return new Removed(data);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class Changed extends Change {
|
|
76
|
+
constructor(data = {}) {
|
|
77
|
+
super(data);
|
|
78
|
+
this.oldValue = data.oldValue ?? data.old_value ?? null;
|
|
79
|
+
this.newValue = data.newValue ?? data.new_value ?? null;
|
|
80
|
+
this._textDiff = data.textDiff ?? (data.text_diff ? TextDiff.fromJSON(data.text_diff) : null);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get type() { return CHANGE_CHANGED; }
|
|
84
|
+
|
|
85
|
+
get textDiff() {
|
|
86
|
+
return this._textDiff;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
toJSON() {
|
|
90
|
+
const obj = { type: CHANGE_CHANGED };
|
|
91
|
+
if (this.path != null) obj.path = this.path;
|
|
92
|
+
obj.old_value = serializeValue(this.oldValue);
|
|
93
|
+
obj.new_value = serializeValue(this.newValue);
|
|
94
|
+
if (this._textDiff) obj.text_diff = this._textDiff.toJSON();
|
|
95
|
+
return obj;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static fromJSON(data) {
|
|
99
|
+
return new Changed(data);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function deserializeChange(data) {
|
|
104
|
+
switch (data?.type) {
|
|
105
|
+
case CHANGE_ADDED: return Added.fromJSON(data);
|
|
106
|
+
case CHANGE_REMOVED: return Removed.fromJSON(data);
|
|
107
|
+
case CHANGE_CHANGED: return Changed.fromJSON(data);
|
|
108
|
+
default:
|
|
109
|
+
throw new Error(`Unknown change type: ${data?.type}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { GlossaristModel } from '../models/base.js';
|
|
2
|
+
import { Added, Removed } from './change.js';
|
|
3
|
+
import { ConceptDiff, DiffStats, diffConcepts } from './concept-diff.js';
|
|
4
|
+
|
|
5
|
+
export class ConceptCollectionDiff extends GlossaristModel {
|
|
6
|
+
constructor(data = {}) {
|
|
7
|
+
super();
|
|
8
|
+
this.oldCount = data.oldCount ?? data.old_count ?? 0;
|
|
9
|
+
this.newCount = data.newCount ?? data.new_count ?? 0;
|
|
10
|
+
|
|
11
|
+
this._matched = wrapIdList(data.matched);
|
|
12
|
+
this._added = wrapIdList(data.added);
|
|
13
|
+
this._removed = wrapIdList(data.removed);
|
|
14
|
+
|
|
15
|
+
this._conceptDiffs = {};
|
|
16
|
+
const raw = data.conceptDiffs ?? data.concept_diffs ?? {};
|
|
17
|
+
for (const [id, diff] of Object.entries(raw)) {
|
|
18
|
+
this._conceptDiffs[id] = diff instanceof ConceptDiff
|
|
19
|
+
? diff
|
|
20
|
+
: ConceptDiff.fromJSON(diff);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get matched() { return this._matched; }
|
|
25
|
+
get added() { return this._added; }
|
|
26
|
+
get removed() { return this._removed; }
|
|
27
|
+
get conceptDiffs() { return this._conceptDiffs; }
|
|
28
|
+
|
|
29
|
+
get changedIds() {
|
|
30
|
+
return Object.keys(this._conceptDiffs);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get hasChanges() {
|
|
34
|
+
return this._added.length > 0
|
|
35
|
+
|| this._removed.length > 0
|
|
36
|
+
|| Object.values(this._conceptDiffs).some(d => d.hasChanges);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
conceptDiff(id) {
|
|
40
|
+
return this._conceptDiffs[id] ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get stats() {
|
|
44
|
+
let added = this._added.length;
|
|
45
|
+
let removed = this._removed.length;
|
|
46
|
+
let changed = 0;
|
|
47
|
+
for (const diff of Object.values(this._conceptDiffs)) {
|
|
48
|
+
const s = diff.stats;
|
|
49
|
+
added += s.added;
|
|
50
|
+
removed += s.removed;
|
|
51
|
+
changed += s.changed;
|
|
52
|
+
}
|
|
53
|
+
return new DiffStats({ added, removed, changed });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get similarity() {
|
|
57
|
+
if (this._matched.length === 0) return 0;
|
|
58
|
+
let sum = 0;
|
|
59
|
+
let n = 0;
|
|
60
|
+
for (const entry of this._matched) {
|
|
61
|
+
const id = entry.value;
|
|
62
|
+
const diff = this._conceptDiffs[id];
|
|
63
|
+
sum += diff ? diff.similarity : 1.0;
|
|
64
|
+
n++;
|
|
65
|
+
}
|
|
66
|
+
return n > 0 ? Math.round((sum / n) * 10000) / 10000 : 1.0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
*walk() {
|
|
70
|
+
for (let i = 0; i < this._added.length; i++) {
|
|
71
|
+
yield { path: `added[${i}]`, change: this._added[i] };
|
|
72
|
+
}
|
|
73
|
+
for (let i = 0; i < this._removed.length; i++) {
|
|
74
|
+
yield { path: `removed[${i}]`, change: this._removed[i] };
|
|
75
|
+
}
|
|
76
|
+
for (const [id, diff] of Object.entries(this._conceptDiffs)) {
|
|
77
|
+
for (const entry of diff.walk()) {
|
|
78
|
+
yield { path: `concepts.${id}.${entry.path}`, change: entry.change, conceptId: id, language: entry.language };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
toJSON() {
|
|
84
|
+
const obj = {
|
|
85
|
+
old_count: this.oldCount,
|
|
86
|
+
new_count: this.newCount,
|
|
87
|
+
matched: this._matched.map(c => c.toJSON()),
|
|
88
|
+
added: this._added.map(c => c.toJSON()),
|
|
89
|
+
removed: this._removed.map(c => c.toJSON()),
|
|
90
|
+
concept_diffs: {},
|
|
91
|
+
};
|
|
92
|
+
for (const [id, diff] of Object.entries(this._conceptDiffs)) {
|
|
93
|
+
obj.concept_diffs[id] = diff.toJSON();
|
|
94
|
+
}
|
|
95
|
+
return obj;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static fromJSON(data) {
|
|
99
|
+
return new ConceptCollectionDiff(data);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function diffConceptCollections(oldCollection, newCollection, options = {}) {
|
|
104
|
+
const language = options.language ?? 'eng';
|
|
105
|
+
|
|
106
|
+
const oldConcepts = extractConcepts(oldCollection);
|
|
107
|
+
const newConcepts = extractConcepts(newCollection);
|
|
108
|
+
|
|
109
|
+
const oldMap = new Map(oldConcepts.map(c => [c.id, c]));
|
|
110
|
+
const newMap = new Map(newConcepts.map(c => [c.id, c]));
|
|
111
|
+
|
|
112
|
+
const oldIds = new Set(oldMap.keys());
|
|
113
|
+
const newIds = new Set(newMap.keys());
|
|
114
|
+
|
|
115
|
+
const matchedIds = [...newIds].filter(id => oldIds.has(id)).sort();
|
|
116
|
+
const addedIds = [...newIds].filter(id => !oldIds.has(id)).sort();
|
|
117
|
+
const removedIds = [...oldIds].filter(id => !newIds.has(id)).sort();
|
|
118
|
+
|
|
119
|
+
const conceptDiffs = {};
|
|
120
|
+
for (const id of matchedIds) {
|
|
121
|
+
const oldConcept = oldMap.get(id);
|
|
122
|
+
const newConcept = newMap.get(id);
|
|
123
|
+
|
|
124
|
+
if (options.skipUnchanged && conceptsEqual(oldConcept, newConcept)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const diff = diffConcepts(oldConcept, newConcept, language);
|
|
129
|
+
if (diff.hasChanges || !options.skipUnchanged) {
|
|
130
|
+
conceptDiffs[id] = diff;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return new ConceptCollectionDiff({
|
|
135
|
+
oldCount: oldConcepts.length,
|
|
136
|
+
newCount: newConcepts.length,
|
|
137
|
+
matched: matchedIds.map(id => new Added({ value: id })),
|
|
138
|
+
added: addedIds.map(id => new Added({ value: id })),
|
|
139
|
+
removed: removedIds.map(id => new Removed({ value: id })),
|
|
140
|
+
conceptDiffs,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function extractConcepts(collection) {
|
|
145
|
+
if (collection == null) return [];
|
|
146
|
+
if (Array.isArray(collection)) return collection;
|
|
147
|
+
if (typeof collection.toArray === 'function') return collection.toArray();
|
|
148
|
+
if (Symbol.iterator in Object(collection)) return [...collection];
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function conceptsEqual(a, b) {
|
|
153
|
+
if (a === b) return true;
|
|
154
|
+
if (!a || !b) return false;
|
|
155
|
+
if (typeof a.equals === 'function' && typeof b.equals === 'function') {
|
|
156
|
+
return a.equals(b);
|
|
157
|
+
}
|
|
158
|
+
return JSON.stringify(a?.toJSON?.()) === JSON.stringify(b?.toJSON?.());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function wrapIdList(data) {
|
|
162
|
+
if (!data) return [];
|
|
163
|
+
return data.map(c => c instanceof Added || c instanceof Removed ? c : Added.fromJSON(c));
|
|
164
|
+
}
|