@x-oasis/map-diff-range 0.1.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.
- package/.turbo/turbo-build.log +18 -0
- package/CHANGELOG.md +14 -0
- package/README.md +275 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +8 -0
- package/dist/map-diff-range.cjs.development.js +220 -0
- package/dist/map-diff-range.cjs.development.js.map +1 -0
- package/dist/map-diff-range.cjs.production.min.js +2 -0
- package/dist/map-diff-range.cjs.production.min.js.map +1 -0
- package/dist/map-diff-range.esm.js +213 -0
- package/dist/map-diff-range.esm.js.map +1 -0
- package/examples/.turbo/turbo-build.log +12 -0
- package/examples/README.md +111 -0
- package/examples/index.html +13 -0
- package/examples/package.json +28 -0
- package/examples/src/App.tsx +257 -0
- package/examples/src/index.css +253 -0
- package/examples/src/main.tsx +10 -0
- package/examples/tsconfig.json +25 -0
- package/examples/tsconfig.node.json +10 -0
- package/examples/vite.config.ts +30 -0
- package/package.json +25 -0
- package/src/index.ts +352 -0
- package/test/index.test.ts +234 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +6 -0
- package/vitest.config.ts +21 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { diff_match_patch } from 'diff-match-patch';
|
|
2
|
+
|
|
3
|
+
var DIFF_DELETE = -1;
|
|
4
|
+
var DIFF_INSERT = 1;
|
|
5
|
+
var DIFF_EQUAL = 0;
|
|
6
|
+
function mapNewerRangeToOlder(olderContent, newerContent, startOffset, endOffset) {
|
|
7
|
+
var dmp = new diff_match_patch();
|
|
8
|
+
var diffs = dmp.diff_main(olderContent, newerContent);
|
|
9
|
+
dmp.diff_cleanupSemantic(diffs);
|
|
10
|
+
var newerOffset = 0;
|
|
11
|
+
var olderOffset = 0;
|
|
12
|
+
var olderStart = null;
|
|
13
|
+
var olderEnd = null;
|
|
14
|
+
for (var i = 0; i < diffs.length; i++) {
|
|
15
|
+
var _diffs$i = diffs[i],
|
|
16
|
+
op = _diffs$i[0],
|
|
17
|
+
text = _diffs$i[1];
|
|
18
|
+
var len = text.length;
|
|
19
|
+
var nextDiff = i < diffs.length - 1 ? diffs[i + 1] : null;
|
|
20
|
+
if (op === DIFF_EQUAL) {
|
|
21
|
+
var rangeStart = newerOffset;
|
|
22
|
+
var rangeEnd = newerOffset + len;
|
|
23
|
+
if (olderStart === null && startOffset >= rangeStart && startOffset < rangeEnd) {
|
|
24
|
+
olderStart = olderOffset + (startOffset - rangeStart);
|
|
25
|
+
}
|
|
26
|
+
if (olderEnd === null && endOffset > rangeStart && endOffset <= rangeEnd) {
|
|
27
|
+
var offsetInRange = endOffset - rangeStart;
|
|
28
|
+
olderEnd = olderOffset + offsetInRange;
|
|
29
|
+
if (endOffset === rangeEnd && nextDiff && nextDiff[0] === DIFF_DELETE) {
|
|
30
|
+
olderEnd = olderOffset + len + nextDiff[1].length;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
newerOffset += len;
|
|
34
|
+
olderOffset += len;
|
|
35
|
+
} else if (op === DIFF_INSERT) {
|
|
36
|
+
var _rangeStart = newerOffset;
|
|
37
|
+
var _rangeEnd = newerOffset + len;
|
|
38
|
+
if (olderStart === null && startOffset >= _rangeStart && startOffset < _rangeEnd) {
|
|
39
|
+
olderStart = olderOffset;
|
|
40
|
+
}
|
|
41
|
+
if (olderEnd === null && endOffset > _rangeStart && endOffset <= _rangeEnd) {
|
|
42
|
+
olderEnd = olderOffset;
|
|
43
|
+
}
|
|
44
|
+
newerOffset += len;
|
|
45
|
+
} else if (op === DIFF_DELETE) {
|
|
46
|
+
if (olderStart === null && startOffset <= newerOffset) {
|
|
47
|
+
olderStart = olderOffset;
|
|
48
|
+
}
|
|
49
|
+
if (olderEnd === null && endOffset <= newerOffset) {
|
|
50
|
+
olderEnd = endOffset === newerOffset ? olderOffset + len : olderOffset;
|
|
51
|
+
} else if (olderEnd === null && endOffset === newerOffset) {
|
|
52
|
+
olderEnd = olderOffset + len;
|
|
53
|
+
}
|
|
54
|
+
olderOffset += len;
|
|
55
|
+
}
|
|
56
|
+
if (olderStart !== null && olderEnd !== null) break;
|
|
57
|
+
}
|
|
58
|
+
if (olderStart === null) olderStart = olderOffset;
|
|
59
|
+
if (olderEnd === null) olderEnd = olderOffset;
|
|
60
|
+
return {
|
|
61
|
+
start: olderStart,
|
|
62
|
+
end: olderEnd
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function mapOlderRangeToNewer(olderContent, newerContent, startOffset, endOffset) {
|
|
66
|
+
var dmp = new diff_match_patch();
|
|
67
|
+
var diffs = dmp.diff_main(olderContent, newerContent);
|
|
68
|
+
dmp.diff_cleanupSemantic(diffs);
|
|
69
|
+
var olderOffset = 0;
|
|
70
|
+
var newerOffset = 0;
|
|
71
|
+
var newerStart = null;
|
|
72
|
+
var newerEnd = null;
|
|
73
|
+
for (var i = 0; i < diffs.length; i++) {
|
|
74
|
+
var _diffs$i2 = diffs[i],
|
|
75
|
+
op = _diffs$i2[0],
|
|
76
|
+
text = _diffs$i2[1];
|
|
77
|
+
var len = text.length;
|
|
78
|
+
var nextDiff = i < diffs.length - 1 ? diffs[i + 1] : null;
|
|
79
|
+
if (op === DIFF_EQUAL) {
|
|
80
|
+
var oldRangeStart = olderOffset;
|
|
81
|
+
var oldRangeEnd = olderOffset + len;
|
|
82
|
+
if (newerStart === null && startOffset >= oldRangeStart && startOffset < oldRangeEnd) {
|
|
83
|
+
newerStart = newerOffset + (startOffset - oldRangeStart);
|
|
84
|
+
}
|
|
85
|
+
if (newerEnd === null && endOffset > oldRangeStart && endOffset <= oldRangeEnd) {
|
|
86
|
+
var offsetInRange = endOffset - oldRangeStart;
|
|
87
|
+
newerEnd = newerOffset + offsetInRange;
|
|
88
|
+
if (endOffset === oldRangeEnd && nextDiff && nextDiff[0] === DIFF_INSERT) {
|
|
89
|
+
newerEnd = newerOffset + len + nextDiff[1].length;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
olderOffset += len;
|
|
93
|
+
newerOffset += len;
|
|
94
|
+
} else if (op === DIFF_INSERT) {
|
|
95
|
+
newerOffset += len;
|
|
96
|
+
} else if (op === DIFF_DELETE) {
|
|
97
|
+
var _oldRangeStart = olderOffset;
|
|
98
|
+
var _oldRangeEnd = olderOffset + len;
|
|
99
|
+
if (newerStart === null && startOffset >= _oldRangeStart && startOffset < _oldRangeEnd) {
|
|
100
|
+
newerStart = newerOffset;
|
|
101
|
+
}
|
|
102
|
+
if (newerEnd === null && endOffset > _oldRangeStart && endOffset <= _oldRangeEnd) {
|
|
103
|
+
newerEnd = newerOffset;
|
|
104
|
+
} else if (newerEnd === null && endOffset <= olderOffset) {
|
|
105
|
+
newerEnd = newerOffset;
|
|
106
|
+
}
|
|
107
|
+
olderOffset += len;
|
|
108
|
+
}
|
|
109
|
+
if (newerStart !== null && newerEnd !== null) break;
|
|
110
|
+
}
|
|
111
|
+
if (newerStart === null) newerStart = newerOffset;
|
|
112
|
+
if (newerEnd === null) newerEnd = newerOffset;
|
|
113
|
+
return {
|
|
114
|
+
start: newerStart,
|
|
115
|
+
end: newerEnd
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function analyzeFragmentChange(originalFragment, finalFragment) {
|
|
119
|
+
var dmp = new diff_match_patch();
|
|
120
|
+
var diffs = dmp.diff_main(originalFragment, finalFragment);
|
|
121
|
+
dmp.diff_cleanupSemantic(diffs);
|
|
122
|
+
var hasDelete = diffs.some(function (_ref) {
|
|
123
|
+
var op = _ref[0];
|
|
124
|
+
return op === DIFF_DELETE;
|
|
125
|
+
});
|
|
126
|
+
var hasInsert = diffs.some(function (_ref2) {
|
|
127
|
+
var op = _ref2[0];
|
|
128
|
+
return op === DIFF_INSERT;
|
|
129
|
+
});
|
|
130
|
+
var equal = !hasDelete && !hasInsert;
|
|
131
|
+
var onlyDeletion = hasDelete && !hasInsert;
|
|
132
|
+
var onlyInsertion = !hasDelete && hasInsert;
|
|
133
|
+
var replacement = hasDelete && hasInsert;
|
|
134
|
+
var summary;
|
|
135
|
+
if (equal) {
|
|
136
|
+
summary = '无变更';
|
|
137
|
+
} else if (onlyDeletion) {
|
|
138
|
+
var deleted = diffs.filter(function (_ref3) {
|
|
139
|
+
var op = _ref3[0];
|
|
140
|
+
return op === DIFF_DELETE;
|
|
141
|
+
}).map(function (_ref4) {
|
|
142
|
+
var text = _ref4[1];
|
|
143
|
+
return text;
|
|
144
|
+
}).join('');
|
|
145
|
+
summary = "\u5220\u9664: " + formatSnippet(deleted);
|
|
146
|
+
} else if (onlyInsertion) {
|
|
147
|
+
var inserted = diffs.filter(function (_ref5) {
|
|
148
|
+
var op = _ref5[0];
|
|
149
|
+
return op === DIFF_INSERT;
|
|
150
|
+
}).map(function (_ref6) {
|
|
151
|
+
var text = _ref6[1];
|
|
152
|
+
return text;
|
|
153
|
+
}).join('');
|
|
154
|
+
summary = "\u65B0\u589E: " + formatSnippet(inserted);
|
|
155
|
+
} else {
|
|
156
|
+
var _deleted = diffs.filter(function (_ref7) {
|
|
157
|
+
var op = _ref7[0];
|
|
158
|
+
return op === DIFF_DELETE;
|
|
159
|
+
}).map(function (_ref8) {
|
|
160
|
+
var text = _ref8[1];
|
|
161
|
+
return text;
|
|
162
|
+
}).join('');
|
|
163
|
+
var _inserted = diffs.filter(function (_ref9) {
|
|
164
|
+
var op = _ref9[0];
|
|
165
|
+
return op === DIFF_INSERT;
|
|
166
|
+
}).map(function (_ref10) {
|
|
167
|
+
var text = _ref10[1];
|
|
168
|
+
return text;
|
|
169
|
+
}).join('');
|
|
170
|
+
summary = "\u66FF\u6362: " + formatSnippet(_deleted) + " \u2192 " + formatSnippet(_inserted);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
originalFragment: originalFragment,
|
|
174
|
+
finalFragment: finalFragment,
|
|
175
|
+
equal: equal,
|
|
176
|
+
onlyDeletion: onlyDeletion,
|
|
177
|
+
onlyInsertion: onlyInsertion,
|
|
178
|
+
replacement: replacement,
|
|
179
|
+
summary: summary,
|
|
180
|
+
diffs: diffs
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function formatSnippet(s, maxLen) {
|
|
184
|
+
if (maxLen === void 0) {
|
|
185
|
+
maxLen = 40;
|
|
186
|
+
}
|
|
187
|
+
var t = s.replace(/\s+/g, ' ').trim();
|
|
188
|
+
if (t.length <= maxLen) return t;
|
|
189
|
+
return t.slice(0, maxLen) + "\u2026";
|
|
190
|
+
}
|
|
191
|
+
function resolveGroupChangeFragments(options) {
|
|
192
|
+
var currentContent = options.currentContent,
|
|
193
|
+
nextContent = options.nextContent,
|
|
194
|
+
currentRange = options.currentRange;
|
|
195
|
+
var startOffset = currentRange.start,
|
|
196
|
+
endOffset = currentRange.end;
|
|
197
|
+
if (startOffset < 0 || endOffset < 0 || startOffset > endOffset || endOffset > currentContent.length) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
var nextRange = mapOlderRangeToNewer(currentContent, nextContent, startOffset, endOffset);
|
|
201
|
+
var currentFragment = currentContent.substring(startOffset, endOffset);
|
|
202
|
+
var nextFragment = nextContent.substring(nextRange.start, nextRange.end);
|
|
203
|
+
var changeAnalysis = analyzeFragmentChange(currentFragment, nextFragment);
|
|
204
|
+
return {
|
|
205
|
+
nextRange: nextRange,
|
|
206
|
+
currentFragment: currentFragment,
|
|
207
|
+
nextFragment: nextFragment,
|
|
208
|
+
changeAnalysis: changeAnalysis
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export { analyzeFragmentChange, mapNewerRangeToOlder, mapOlderRangeToNewer, resolveGroupChangeFragments };
|
|
213
|
+
//# sourceMappingURL=map-diff-range.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map-diff-range.esm.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * 基于 diff-match-patch 的 range 映射与变更分析工具\n * 用于在 originalContent / currentContent / finalContent 之间映射 range,并分析片段级变更\n *\n * 参考: https://github.com/red-armor/x-oasis/blob/main/packages/diff/diff-match-patch/src/index.ts#L123\n */\n\nimport { diff_match_patch } from 'diff-match-patch';\n\n/** diff 操作常量,与 diff-match-patch 一致 */\nconst DIFF_DELETE = -1;\nconst DIFF_INSERT = 1;\nconst DIFF_EQUAL = 0;\n\n/** 单端 range,start 含头,end 含尾(substring 语义) */\nexport interface Range {\n start: number;\n end: number;\n}\n\n/** 片段级变更分析结果 */\nexport interface FragmentChangeAnalysis {\n /** 原始片段内容 */\n originalFragment: string;\n /** 最终片段内容 */\n finalFragment: string;\n /** 是否完全相同 */\n equal: boolean;\n /** 仅删除(无新增) */\n onlyDeletion: boolean;\n /** 仅新增(无删除) */\n onlyInsertion: boolean;\n /** 既有删除又有新增(替换) */\n replacement: boolean;\n /** 语义化描述(简短) */\n summary: string;\n /** 详细 diff 条目,便于上层做展示或进一步处理 */\n diffs: Array<[number, string]>;\n}\n\n/**\n * 将「新内容」中的 offset range 映射到「旧内容」中的 offset range\n * 等价于 x-oasis 的 mapCurrentRangeToOriginal:diffs = diff_main(older, newer),给定 newer 上的 [start,end],返回 older 上的 [start,end]\n *\n * @param olderContent 旧内容(diff 的 text1)\n * @param newerContent 新内容(diff 的 text2)\n * @param startOffset 新内容中的 range 起始 offset\n * @param endOffset 新内容中的 range 结束 offset(不含尾,即 [startOffset, endOffset) 或含尾由调用方约定,此处按 substring 含头含尾)\n * @returns 旧内容中对应的 range;若越界或无效则返回 { start: 0, end: 0 }\n */\nexport function mapNewerRangeToOlder(\n olderContent: string,\n newerContent: string,\n startOffset: number,\n endOffset: number\n): Range {\n const dmp = new diff_match_patch();\n const diffs = dmp.diff_main(olderContent, newerContent);\n dmp.diff_cleanupSemantic(diffs);\n\n let newerOffset = 0;\n let olderOffset = 0;\n let olderStart: number | null = null;\n let olderEnd: number | null = null;\n\n for (let i = 0; i < diffs.length; i++) {\n const [op, text] = diffs[i];\n const len = text.length;\n const nextDiff = i < diffs.length - 1 ? diffs[i + 1] : null;\n\n if (op === DIFF_EQUAL) {\n const rangeStart = newerOffset;\n const rangeEnd = newerOffset + len;\n\n if (\n olderStart === null &&\n startOffset >= rangeStart &&\n startOffset < rangeEnd\n ) {\n olderStart = olderOffset + (startOffset - rangeStart);\n }\n if (\n olderEnd === null &&\n endOffset > rangeStart &&\n endOffset <= rangeEnd\n ) {\n const offsetInRange = endOffset - rangeStart;\n olderEnd = olderOffset + offsetInRange;\n if (endOffset === rangeEnd && nextDiff && nextDiff[0] === DIFF_DELETE) {\n olderEnd = olderOffset + len + nextDiff[1].length;\n }\n }\n\n newerOffset += len;\n olderOffset += len;\n } else if (op === DIFF_INSERT) {\n const rangeStart = newerOffset;\n const rangeEnd = newerOffset + len;\n\n if (\n olderStart === null &&\n startOffset >= rangeStart &&\n startOffset < rangeEnd\n ) {\n olderStart = olderOffset;\n }\n if (\n olderEnd === null &&\n endOffset > rangeStart &&\n endOffset <= rangeEnd\n ) {\n olderEnd = olderOffset;\n }\n\n newerOffset += len;\n } else if (op === DIFF_DELETE) {\n if (olderStart === null && startOffset <= newerOffset) {\n olderStart = olderOffset;\n }\n if (olderEnd === null && endOffset <= newerOffset) {\n olderEnd = endOffset === newerOffset ? olderOffset + len : olderOffset;\n } else if (olderEnd === null && endOffset === newerOffset) {\n olderEnd = olderOffset + len;\n }\n\n olderOffset += len;\n }\n\n if (olderStart !== null && olderEnd !== null) break;\n }\n\n if (olderStart === null) olderStart = olderOffset;\n if (olderEnd === null) olderEnd = olderOffset;\n\n return { start: olderStart, end: olderEnd };\n}\n\n/**\n * 将「旧内容」中的 offset range 映射到「新内容」中的 offset range\n * 与 mapNewerRangeToOlder 对称:diffs = diff_main(older, newer),给定 older 上的 [start,end],返回 newer 上的 [start,end]\n *\n * @param olderContent 旧内容(diff 的 text1)\n * @param newerContent 新内容(diff 的 text2)\n * @param startOffset 旧内容中的 range 起始 offset\n * @param endOffset 旧内容中的 range 结束 offset\n * @returns 新内容中对应的 range\n */\nexport function mapOlderRangeToNewer(\n olderContent: string,\n newerContent: string,\n startOffset: number,\n endOffset: number\n): Range {\n const dmp = new diff_match_patch();\n const diffs = dmp.diff_main(olderContent, newerContent);\n dmp.diff_cleanupSemantic(diffs);\n\n let olderOffset = 0;\n let newerOffset = 0;\n let newerStart: number | null = null;\n let newerEnd: number | null = null;\n\n for (let i = 0; i < diffs.length; i++) {\n const [op, text] = diffs[i];\n const len = text.length;\n const nextDiff = i < diffs.length - 1 ? diffs[i + 1] : null;\n\n if (op === DIFF_EQUAL) {\n const oldRangeStart = olderOffset;\n const oldRangeEnd = olderOffset + len;\n\n if (\n newerStart === null &&\n startOffset >= oldRangeStart &&\n startOffset < oldRangeEnd\n ) {\n newerStart = newerOffset + (startOffset - oldRangeStart);\n }\n if (\n newerEnd === null &&\n endOffset > oldRangeStart &&\n endOffset <= oldRangeEnd\n ) {\n const offsetInRange = endOffset - oldRangeStart;\n newerEnd = newerOffset + offsetInRange;\n if (\n endOffset === oldRangeEnd &&\n nextDiff &&\n nextDiff[0] === DIFF_INSERT\n ) {\n newerEnd = newerOffset + len + nextDiff[1].length;\n }\n }\n\n olderOffset += len;\n newerOffset += len;\n } else if (op === DIFF_INSERT) {\n newerOffset += len;\n } else if (op === DIFF_DELETE) {\n const oldRangeStart = olderOffset;\n const oldRangeEnd = olderOffset + len;\n\n if (\n newerStart === null &&\n startOffset >= oldRangeStart &&\n startOffset < oldRangeEnd\n ) {\n newerStart = newerOffset;\n }\n if (\n newerEnd === null &&\n endOffset > oldRangeStart &&\n endOffset <= oldRangeEnd\n ) {\n newerEnd = newerOffset;\n } else if (newerEnd === null && endOffset <= olderOffset) {\n newerEnd = newerOffset;\n }\n\n olderOffset += len;\n }\n\n if (newerStart !== null && newerEnd !== null) break;\n }\n\n if (newerStart === null) newerStart = newerOffset;\n if (newerEnd === null) newerEnd = newerOffset;\n\n return { start: newerStart, end: newerEnd };\n}\n\n/**\n * 对比两段片段内容,分析发生的变更类型并生成简短描述\n *\n * @param originalFragment 原始片段\n * @param finalFragment 变更后片段\n * @returns 变更分析结果\n */\nexport function analyzeFragmentChange(\n originalFragment: string,\n finalFragment: string\n): FragmentChangeAnalysis {\n const dmp = new diff_match_patch();\n const diffs = dmp.diff_main(originalFragment, finalFragment);\n dmp.diff_cleanupSemantic(diffs);\n\n const hasDelete = diffs.some(([op]) => op === DIFF_DELETE);\n const hasInsert = diffs.some(([op]) => op === DIFF_INSERT);\n const equal = !hasDelete && !hasInsert;\n const onlyDeletion = hasDelete && !hasInsert;\n const onlyInsertion = !hasDelete && hasInsert;\n const replacement = hasDelete && hasInsert;\n\n let summary: string;\n if (equal) {\n summary = '无变更';\n } else if (onlyDeletion) {\n const deleted = diffs\n .filter(([op]) => op === DIFF_DELETE)\n .map(([, text]) => text)\n .join('');\n summary = `删除: ${formatSnippet(deleted)}`;\n } else if (onlyInsertion) {\n const inserted = diffs\n .filter(([op]) => op === DIFF_INSERT)\n .map(([, text]) => text)\n .join('');\n summary = `新增: ${formatSnippet(inserted)}`;\n } else {\n const deleted = diffs\n .filter(([op]) => op === DIFF_DELETE)\n .map(([, text]) => text)\n .join('');\n const inserted = diffs\n .filter(([op]) => op === DIFF_INSERT)\n .map(([, text]) => text)\n .join('');\n summary = `替换: ${formatSnippet(deleted)} → ${formatSnippet(inserted)}`;\n }\n\n return {\n originalFragment,\n finalFragment,\n equal,\n onlyDeletion,\n onlyInsertion,\n replacement,\n summary,\n diffs,\n };\n}\n\n/** 片段截断展示,避免过长 */\nfunction formatSnippet(s: string, maxLen = 40): string {\n const t = s.replace(/\\s+/g, ' ').trim();\n if (t.length <= maxLen) return t;\n return `${t.slice(0, maxLen)}…`;\n}\n\n/**\n * 根据当前内容和 range,找到下一个内容对应的 range\n * 用于将 currentContent 中的 range 映射到 nextContent 中对应的 range\n *\n * @param options.currentContent 当前文件内容\n * @param options.nextContent 下一个文件内容\n * @param options.currentRange 当前文件中需要映射的 range\n * @returns 映射得到的 nextRange、对应片段、以及片段级变更分析;若无法解析则返回 undefined\n */\nexport function resolveGroupChangeFragments(options: {\n currentContent: string;\n nextContent: string;\n currentRange: Range;\n}):\n | {\n nextRange: Range;\n currentFragment: string;\n nextFragment: string;\n changeAnalysis: FragmentChangeAnalysis;\n }\n | undefined {\n const { currentContent, nextContent, currentRange } = options;\n\n const { start: startOffset, end: endOffset } = currentRange;\n\n if (\n startOffset < 0 ||\n endOffset < 0 ||\n startOffset > endOffset ||\n endOffset > currentContent.length\n ) {\n return undefined;\n }\n\n const nextRange = mapOlderRangeToNewer(\n currentContent,\n nextContent,\n startOffset,\n endOffset\n );\n\n const currentFragment = currentContent.substring(startOffset, endOffset);\n const nextFragment = nextContent.substring(nextRange.start, nextRange.end);\n\n const changeAnalysis = analyzeFragmentChange(currentFragment, nextFragment);\n\n return {\n nextRange,\n currentFragment,\n nextFragment,\n changeAnalysis,\n };\n}\n"],"names":["DIFF_DELETE","DIFF_INSERT","DIFF_EQUAL","mapNewerRangeToOlder","olderContent","newerContent","startOffset","endOffset","dmp","diff_match_patch","diffs","diff_main","diff_cleanupSemantic","newerOffset","olderOffset","olderStart","olderEnd","i","length","_diffs$i","op","text","len","nextDiff","rangeStart","rangeEnd","offsetInRange","start","end","mapOlderRangeToNewer","newerStart","newerEnd","_diffs$i2","oldRangeStart","oldRangeEnd","analyzeFragmentChange","originalFragment","finalFragment","hasDelete","some","_ref","hasInsert","_ref2","equal","onlyDeletion","onlyInsertion","replacement","summary","deleted","filter","_ref3","map","_ref4","join","formatSnippet","inserted","_ref5","_ref6","_ref7","_ref8","_ref9","_ref10","s","maxLen","t","replace","trim","slice","resolveGroupChangeFragments","options","currentContent","nextContent","currentRange","undefined","nextRange","currentFragment","substring","nextFragment","changeAnalysis"],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
|
|
2
|
+
> map-diff-range-example@0.1.0 build /home/runner/work/x-oasis/x-oasis/packages/diff/map-diff-range/examples
|
|
3
|
+
> vite build
|
|
4
|
+
|
|
5
|
+
[36mvite v4.1.4 [32mbuilding for production...[36m[39m
|
|
6
|
+
transforming...
|
|
7
|
+
[32m✓[39m 34 modules transformed.
|
|
8
|
+
rendering chunks...
|
|
9
|
+
computing gzip size...
|
|
10
|
+
[2mdist/[22m[32mindex.html [39m[1m[2m 0.54 kB[22m[1m[22m
|
|
11
|
+
[2mdist/[22m[2massets/[22m[35mindex-a9dac6a7.css [39m[1m[2m 3.25 kB[22m[1m[22m[2m │ gzip: 1.06 kB[22m
|
|
12
|
+
[2mdist/[22m[2massets/[22m[36mindex-07c5d0de.js [39m[1m[2m168.84 kB[22m[1m[22m[2m │ gzip: 54.30 kB[22m
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
这个目录包含 `@x-oasis/map-diff-range` 的交互式示例。
|
|
4
|
+
|
|
5
|
+
## 运行示例
|
|
6
|
+
|
|
7
|
+
这是一个基于 Vite + React 的项目。
|
|
8
|
+
|
|
9
|
+
### 安装依赖
|
|
10
|
+
|
|
11
|
+
在项目根目录运行:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### 启动开发服务器
|
|
18
|
+
|
|
19
|
+
在 `examples` 目录下运行:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd packages/diff/map-diff-range/examples
|
|
23
|
+
pnpm install
|
|
24
|
+
pnpm dev
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
或者从项目根目录运行:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm install
|
|
31
|
+
cd packages/diff/map-diff-range/examples
|
|
32
|
+
pnpm dev
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
开发服务器将在 `http://localhost:3000` 启动,并自动在浏览器中打开。
|
|
36
|
+
|
|
37
|
+
### 构建生产版本
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pnpm build
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 预览生产构建
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pnpm preview
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 功能说明
|
|
50
|
+
|
|
51
|
+
示例页面包含以下功能:
|
|
52
|
+
|
|
53
|
+
1. **三路文件输入**: 并排显示原始文件、当前文件和最终文件内容
|
|
54
|
+
2. **Range 输入**: 输入当前文件中发生变更的 offset range(startOffset/endOffset)
|
|
55
|
+
3. **Range 映射**: 自动计算并显示:
|
|
56
|
+
- Original Range:原始文件中对应的 range
|
|
57
|
+
- Final Range:最终文件中对应的 range
|
|
58
|
+
4. **片段提取**: 显示从原始文件和最终文件中提取的对应片段
|
|
59
|
+
5. **变更分析**: 显示详细的变更分析:
|
|
60
|
+
- 变更类型(equal、onlyDeletion、onlyInsertion、replacement)
|
|
61
|
+
- 变更摘要(语义化描述)
|
|
62
|
+
- 详细 diff 条目(带颜色编码)
|
|
63
|
+
6. **快速示例**: 提供预设的测试场景按钮,快速测试不同场景
|
|
64
|
+
|
|
65
|
+
## 示例场景
|
|
66
|
+
|
|
67
|
+
### 场景 1: Class 属性变更
|
|
68
|
+
|
|
69
|
+
- Original: `<h1 class="title">Name</h1>`
|
|
70
|
+
- Current: `<h1 class="title text-xl">Name</h1>`
|
|
71
|
+
- Final: `<h1 class="text-xl font-bold">Name</h1>`
|
|
72
|
+
- Range: startOffset: 4, endOffset: 30
|
|
73
|
+
- 说明: 演示 class 属性的变更,从 "title" 变为 "text-xl font-bold"
|
|
74
|
+
|
|
75
|
+
### 场景 2: 文本替换
|
|
76
|
+
|
|
77
|
+
- Original: `Hello World`
|
|
78
|
+
- Current: `Hello Beautiful World`
|
|
79
|
+
- Final: `Hello Amazing World`
|
|
80
|
+
- Range: startOffset: 6, endOffset: 15
|
|
81
|
+
- 说明: 演示文本的替换,从 "Beautiful" 变为 "Amazing"
|
|
82
|
+
|
|
83
|
+
### 场景 3: 单词替换
|
|
84
|
+
|
|
85
|
+
- Original: `The quick brown fox`
|
|
86
|
+
- Current: `The fast brown fox`
|
|
87
|
+
- Final: `The slow brown fox`
|
|
88
|
+
- Range: startOffset: 4, endOffset: 8
|
|
89
|
+
- 说明: 演示单词的替换,从 "quick" → "fast" → "slow"
|
|
90
|
+
|
|
91
|
+
## 技术实现
|
|
92
|
+
|
|
93
|
+
示例使用:
|
|
94
|
+
- **Vite**: 快速的前端构建工具
|
|
95
|
+
- **React**: UI 框架
|
|
96
|
+
- **TypeScript**: 类型安全
|
|
97
|
+
- **@x-oasis/map-diff-range**: 核心库,提供 range 映射和变更分析功能
|
|
98
|
+
- **diff-match-patch**: 差异计算库
|
|
99
|
+
|
|
100
|
+
## GitHub Pages 部署
|
|
101
|
+
|
|
102
|
+
示例已配置支持 GitHub Pages 部署。在 `vite.config.ts` 中:
|
|
103
|
+
|
|
104
|
+
- 支持 `GITHUB_PAGES` 环境变量
|
|
105
|
+
- 自动配置 base 路径
|
|
106
|
+
- 支持子路径部署
|
|
107
|
+
|
|
108
|
+
部署时设置环境变量:
|
|
109
|
+
- `GITHUB_PAGES=true`
|
|
110
|
+
- `GITHUB_REPOSITORY=owner/repo-name`
|
|
111
|
+
- `GITHUB_PAGES_PATH=map-diff-range`
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Map Diff Range - Range Mapping and Change Analysis Example</title>
|
|
7
|
+
<link rel="stylesheet" href="https://esm.sh/@git-diff-view/react@latest/styles/diff-view.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "map-diff-range-example",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"react": "^18.2.0",
|
|
13
|
+
"react-dom": "^18.2.0",
|
|
14
|
+
"@git-diff-view/react": "latest",
|
|
15
|
+
"@git-diff-view/file": "latest",
|
|
16
|
+
"diff-match-patch": "^1.0.5"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/react": "^18.2.0",
|
|
20
|
+
"@types/react-dom": "^18.2.0",
|
|
21
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
22
|
+
"vite": "^4.1.4",
|
|
23
|
+
"typescript": "^4.8.3"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "private"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { resolveGroupChangeFragments } from '@x-oasis/map-diff-range';
|
|
3
|
+
import './index.css';
|
|
4
|
+
|
|
5
|
+
const App: React.FC = () => {
|
|
6
|
+
const [currentContent, setCurrentContent] = useState(
|
|
7
|
+
'<h1 class="text-[--color-text-title] text-2xl font-bold">姓名3333444</h1>'
|
|
8
|
+
);
|
|
9
|
+
const [nextContent, setNextContent] = useState(
|
|
10
|
+
'<h1 class="text-2xl font-bold text-[--color-primary-pressing]">姓名3</h1>'
|
|
11
|
+
);
|
|
12
|
+
const [startOffset, setStartOffset] = useState<number>(244);
|
|
13
|
+
const [endOffset, setEndOffset] = useState<number>(315);
|
|
14
|
+
const [result, setResult] = useState<any>(null);
|
|
15
|
+
const [error, setError] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
const handleAnalyze = () => {
|
|
18
|
+
setError(null);
|
|
19
|
+
try {
|
|
20
|
+
const analysis = resolveGroupChangeFragments({
|
|
21
|
+
currentContent,
|
|
22
|
+
nextContent,
|
|
23
|
+
currentRange: { start: startOffset, end: endOffset },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (analysis) {
|
|
27
|
+
setResult(analysis);
|
|
28
|
+
} else {
|
|
29
|
+
setError('无法解析变更:offset range 无效或超出范围');
|
|
30
|
+
}
|
|
31
|
+
} catch (err: any) {
|
|
32
|
+
setError(err.message || '分析失败');
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const getDiffEntryClass = (op: number) => {
|
|
37
|
+
if (op === 1) return 'insert';
|
|
38
|
+
if (op === -1) return 'delete';
|
|
39
|
+
return 'equal';
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const getDiffEntryLabel = (op: number) => {
|
|
43
|
+
if (op === 1) return '+';
|
|
44
|
+
if (op === -1) return '-';
|
|
45
|
+
return '=';
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="container">
|
|
50
|
+
<h1>Map Diff Range - Range Mapping and Change Analysis</h1>
|
|
51
|
+
<p className="subtitle">
|
|
52
|
+
在 currentContent 和 nextContent 之间映射 range,并分析片段级变更
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
<div className="info-box">
|
|
56
|
+
<strong>使用说明:</strong>
|
|
57
|
+
<ul style={{ marginTop: '8px', marginLeft: '20px' }}>
|
|
58
|
+
<li>输入两个文件内容:当前内容、下一个内容</li>
|
|
59
|
+
<li>输入当前文件中需要映射的 range(startOffset 和 endOffset)</li>
|
|
60
|
+
<li>点击"分析变更"查看映射结果和变更分析</li>
|
|
61
|
+
<li>系统会自动计算 nextRange,并分析片段间的变更</li>
|
|
62
|
+
</ul>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div className="section">
|
|
66
|
+
<div className="section-title">文件内容输入</div>
|
|
67
|
+
<div className="file-comparison">
|
|
68
|
+
<div className="file-panel">
|
|
69
|
+
<div className="file-header">当前内容 (currentContent)</div>
|
|
70
|
+
<textarea
|
|
71
|
+
className="file-content"
|
|
72
|
+
value={currentContent}
|
|
73
|
+
onChange={(e) => setCurrentContent(e.target.value)}
|
|
74
|
+
spellCheck={false}
|
|
75
|
+
placeholder="输入当前文件内容..."
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
<div className="file-panel">
|
|
79
|
+
<div className="file-header">下一个内容 (nextContent)</div>
|
|
80
|
+
<textarea
|
|
81
|
+
className="file-content"
|
|
82
|
+
value={nextContent}
|
|
83
|
+
onChange={(e) => setNextContent(e.target.value)}
|
|
84
|
+
spellCheck={false}
|
|
85
|
+
placeholder="输入下一个文件内容..."
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="section">
|
|
92
|
+
<div className="section-title">变更 Range 输入</div>
|
|
93
|
+
<div className="controls">
|
|
94
|
+
<div className="control-group">
|
|
95
|
+
<label>Start Offset (基于 currentContent)</label>
|
|
96
|
+
<input
|
|
97
|
+
type="number"
|
|
98
|
+
value={startOffset}
|
|
99
|
+
onChange={(e) => setStartOffset(Number(e.target.value))}
|
|
100
|
+
min="0"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
<div className="control-group">
|
|
104
|
+
<label>End Offset (基于 currentContent)</label>
|
|
105
|
+
<input
|
|
106
|
+
type="number"
|
|
107
|
+
value={endOffset}
|
|
108
|
+
onChange={(e) => setEndOffset(Number(e.target.value))}
|
|
109
|
+
min="0"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
<button onClick={handleAnalyze}>分析变更</button>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{error && (
|
|
117
|
+
<div className="error-message">
|
|
118
|
+
<strong>错误:</strong> {error}
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{result && (
|
|
123
|
+
<div className="section">
|
|
124
|
+
<div className="section-title">分析结果</div>
|
|
125
|
+
|
|
126
|
+
<div className="analysis-panel">
|
|
127
|
+
<div className="analysis-item">
|
|
128
|
+
<strong>Range 映射</strong>
|
|
129
|
+
<div className="analysis-value">
|
|
130
|
+
<div>
|
|
131
|
+
Next Range:{' '}
|
|
132
|
+
<span className="range-info">
|
|
133
|
+
[{result.nextRange.start}, {result.nextRange.end}]
|
|
134
|
+
</span>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div className="analysis-item">
|
|
140
|
+
<strong>当前片段 (currentFragment)</strong>
|
|
141
|
+
<div className="analysis-value">{result.currentFragment}</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div className="analysis-item">
|
|
145
|
+
<strong>下一个片段 (nextFragment)</strong>
|
|
146
|
+
<div className="analysis-value">{result.nextFragment}</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div className="analysis-item">
|
|
150
|
+
<strong>变更类型</strong>
|
|
151
|
+
<div className="analysis-value">
|
|
152
|
+
<div>
|
|
153
|
+
Equal:{' '}
|
|
154
|
+
<span className="range-info">
|
|
155
|
+
{String(result.changeAnalysis.equal)}
|
|
156
|
+
</span>
|
|
157
|
+
</div>
|
|
158
|
+
<div>
|
|
159
|
+
Only Deletion:{' '}
|
|
160
|
+
<span className="range-info">
|
|
161
|
+
{String(result.changeAnalysis.onlyDeletion)}
|
|
162
|
+
</span>
|
|
163
|
+
</div>
|
|
164
|
+
<div>
|
|
165
|
+
Only Insertion:{' '}
|
|
166
|
+
<span className="range-info">
|
|
167
|
+
{String(result.changeAnalysis.onlyInsertion)}
|
|
168
|
+
</span>
|
|
169
|
+
</div>
|
|
170
|
+
<div>
|
|
171
|
+
Replacement:{' '}
|
|
172
|
+
<span className="range-info">
|
|
173
|
+
{String(result.changeAnalysis.replacement)}
|
|
174
|
+
</span>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div className="analysis-item">
|
|
180
|
+
<strong>变更摘要 (summary)</strong>
|
|
181
|
+
<div className="analysis-value">
|
|
182
|
+
{result.changeAnalysis.summary}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div className="analysis-item">
|
|
187
|
+
<strong>详细 Diff 条目</strong>
|
|
188
|
+
<div className="analysis-value">
|
|
189
|
+
{result.changeAnalysis.diffs.map(
|
|
190
|
+
(diff: [number, string], index: number) => (
|
|
191
|
+
<div
|
|
192
|
+
key={index}
|
|
193
|
+
className={`diff-entry ${getDiffEntryClass(diff[0])}`}
|
|
194
|
+
>
|
|
195
|
+
<span style={{ fontWeight: 'bold', marginRight: '5px' }}>
|
|
196
|
+
{getDiffEntryLabel(diff[0])}
|
|
197
|
+
</span>
|
|
198
|
+
{diff[1]}
|
|
199
|
+
</div>
|
|
200
|
+
)
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<div className="result-panel">
|
|
207
|
+
<div className="result-title">完整结果 (JSON)</div>
|
|
208
|
+
<div className="result-content">
|
|
209
|
+
{JSON.stringify(result, null, 2)}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
<div className="section">
|
|
216
|
+
<div className="section-title">快速测试场景</div>
|
|
217
|
+
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
|
|
218
|
+
<button
|
|
219
|
+
className="example-btn"
|
|
220
|
+
onClick={() => {
|
|
221
|
+
setCurrentContent('<h1 class="title text-xl">Name</h1>');
|
|
222
|
+
setNextContent('<h1 class="text-xl font-bold">Name</h1>');
|
|
223
|
+
setStartOffset(4);
|
|
224
|
+
setEndOffset(30);
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
场景 1: Class 变更
|
|
228
|
+
</button>
|
|
229
|
+
<button
|
|
230
|
+
className="example-btn"
|
|
231
|
+
onClick={() => {
|
|
232
|
+
setCurrentContent('Hello Beautiful World');
|
|
233
|
+
setNextContent('Hello Amazing World');
|
|
234
|
+
setStartOffset(6);
|
|
235
|
+
setEndOffset(15);
|
|
236
|
+
}}
|
|
237
|
+
>
|
|
238
|
+
场景 2: 文本替换
|
|
239
|
+
</button>
|
|
240
|
+
<button
|
|
241
|
+
className="example-btn"
|
|
242
|
+
onClick={() => {
|
|
243
|
+
setCurrentContent('The fast brown fox');
|
|
244
|
+
setNextContent('The slow brown fox');
|
|
245
|
+
setStartOffset(4);
|
|
246
|
+
setEndOffset(8);
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
场景 3: 单词替换
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export default App;
|