snowkit 0.0.0-stage → 0.1.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.
@@ -0,0 +1,623 @@
1
+ export const READ_WPM = 238;
2
+ export const SPEAK_WPM = 140;
3
+
4
+ export const SMALL_WORDS = new Set('a an the and but or nor for so yet as at by in of off on per to up via vs'.split(' '));
5
+ export const STOP_WORDS = new Set("a about above after again against all also am an and any are aren't as at be because been before being below between both but by can can't could did didn't do does doesn't doing don't down during each few for from further get got had has have having he her here hers herself him himself his how i i'm if in into is isn't it it's its itself just let me more most my myself no nor not now of off on once one only or other our ours ourselves out over own same she should so some such than that that's the their theirs them themselves then there there's these they this those through to too under until up us very was we were what when where which while who whom why will with won't would you you're your yours yourself yourselves".split(' '));
6
+ export const SLUG_STOP = new Set('a an the and or but of to in on at for with by from as is are'.split(' '));
7
+ export const ABBREVIATIONS = /\b(Mr|Mrs|Ms|Dr|Prof|Sr|Jr|St|vs|etc|e\.g|i\.e|approx|Inc|Ltd|Co|Corp|Mt|Ave|No|Fig|Vol|a\.m|p\.m|U\.S)\./gi;
8
+ export const LATIN = { '\u00DF': 'ss', '\u00E6': 'ae', '\u00F8': 'o', '\u0153': 'oe', '\u0111': 'd', '\u0142': 'l', '\u00FE': 'th', '\u00F0': 'd', '\u0131': 'i', '\u0127': 'h', '\u0167': 't' };
9
+ export const MAP = Object.assign({}, LATIN);
10
+ for (const pair of '\u0430-a \u0431-b \u0432-v \u0433-g \u0434-d \u0435-e \u0451-yo \u0436-zh \u0437-z \u0438-i \u0439-y \u043A-k \u043B-l \u043C-m \u043D-n \u043E-o \u043F-p \u0440-r \u0441-s \u0442-t \u0443-u \u0444-f \u0445-kh \u0446-ts \u0447-ch \u0448-sh \u0449-shch \u044A- \u044B-y \u044C- \u044D-e \u044E-yu \u044F-ya \u0456-i \u0457-yi \u0454-ye \u0491-g \u03B1-a \u03B2-v \u03B3-g \u03B4-d \u03B5-e \u03B6-z \u03B7-i \u03B8-th \u03B9-i \u03BA-k \u03BB-l \u03BC-m \u03BD-n \u03BE-x \u03BF-o \u03C0-p \u03C1-r \u03C3-s \u03C2-s \u03C4-t \u03C5-y \u03C6-f \u03C7-ch \u03C8-ps \u03C9-o'.split(' ')) {
11
+ const [from, to] = pair.split('-');
12
+ MAP[from] = to;
13
+ }
14
+ export const SENTENCES = [
15
+ 'The morning train was quieter than usual, so most people read or looked out at the fields.',
16
+ 'A good recipe tells you what to buy, how long it takes and what the dish should look like.',
17
+ 'Our office moved to the third floor, next to the meeting rooms and the small kitchen.',
18
+ 'Please bring a pen, a notebook and any questions you have about the new schedule.',
19
+ 'The park opens at seven, and the cafe by the pond starts serving coffee an hour later.',
20
+ 'Every chapter ends with a short summary and a few exercises to try on your own.',
21
+ 'We tested the bike lights in rain, fog and full darkness before choosing the brightest pair.',
22
+ 'The library keeps a shelf of local maps, old newspapers and photos of the town square.',
23
+ 'If the parcel does not arrive by Friday, write to us and we will send a replacement.',
24
+ 'She painted the fence a pale green that matched the leaves of the old apple tree.',
25
+ 'The workshop covers basic tools, safe habits and how to plan a small project from start to finish.',
26
+ 'Most visitors come in spring, when the hills are bright and the paths are dry.',
27
+ 'A clear label on each box saves time when you unpack in a new home.',
28
+ 'The team meets every Tuesday to review progress and agree on the next steps.',
29
+ 'Fresh bread tastes best on the day it is baked, but it also freezes well.',
30
+ 'The guide explains how to set up the device, connect it and keep it up to date.',
31
+ 'Our garden club swaps seeds every autumn and shares tips on growing vegetables in small spaces.',
32
+ 'The museum added a quiet room where visitors can sit, rest and read about the collection.',
33
+ 'Water the plants early in the day so the soil stays cool and damp for longer.',
34
+ 'The report shows steady growth, with the biggest change in the last three months.',
35
+ 'A short walk after lunch helps many people stay focused for the rest of the afternoon.',
36
+ 'The new menu lists every allergen next to each dish, in plain words.',
37
+ 'Our support team answers most messages within one working day.',
38
+ 'The bridge was repainted last summer and now looks almost new.',
39
+ 'Choose a password that is long, unique and easy for you to remember.',
40
+ 'The festival brings music, food stalls and craft tables to the main street each July.',
41
+ 'Each room has a desk, a reading lamp and a window that faces the courtyard.',
42
+ 'The class starts with ten minutes of stretching and ends with a slow cool-down.',
43
+ 'Keep your receipt, since you will need it for any return or exchange.',
44
+ 'The river rose after a week of rain, but the path along the bank stayed open.',
45
+ 'Good notes record what was decided, who will do it and when it is due.',
46
+ 'The bakery on the corner sells out of rye loaves before noon on Saturdays.',
47
+ 'Our newsletter goes out once a month with news, events and a short interview.',
48
+ 'The hiking trail is well marked, with benches at the steepest points.',
49
+ 'A simple layout with clear headings makes a long page much easier to read.',
50
+ 'The shop repairs shoes, bags and belts, and most jobs are ready the next day.',
51
+ 'Volunteers planted forty trees along the school fence over two weekends.',
52
+ 'The tour lasts about an hour and ends at the top of the old clock tower.',
53
+ 'Store the jars in a cool, dark cupboard and use them within a year.',
54
+ 'The town hall clock was fixed in May and has kept good time ever since.',
55
+ 'Every invoice shows the date, the amount due and how to pay.',
56
+ 'The quiet carriage is at the front of the train, next to the luggage racks.',
57
+ 'Learning a few words of the local language goes a long way when you travel.',
58
+ 'The new playground has swings, a climbing frame and soft ground under every piece.',
59
+ 'The printer on the second floor now prints on both sides unless you change it.'
60
+ ];
61
+
62
+ export const plural = (n, one, many) => `${n.toLocaleString()} ${n === 1 ? one : many || `${one}s`}`;
63
+
64
+ export const graphemes = typeof Intl === 'object' && typeof Intl.Segmenter === 'function' ? new Intl.Segmenter(undefined, { granularity: 'grapheme' }) : null;
65
+
66
+ export function charCount(text) {
67
+ if (!text) return 0;
68
+ if (!graphemes) return Array.from(text).length;
69
+ let n = 0;
70
+ const it = graphemes.segment(text)[Symbol.iterator]();
71
+ while (!it.next().done) n++;
72
+ return n;
73
+ }
74
+
75
+ export const CJK = /[\u3040-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]/g;
76
+
77
+ export function wordCount(text) {
78
+ const cjk = (text.match(CJK) || []).length;
79
+ let n = 0;
80
+ for (const token of text.replace(CJK, ' ').split(/\s+/)) {
81
+ if (token && /[\p{L}\p{N}]/u.test(token)) n++;
82
+ }
83
+ return n + cjk;
84
+ }
85
+
86
+ export function sentenceCount(text) {
87
+ const marked = text.replace(ABBREVIATIONS, match => match.replace(/\./g, '\u0001'));
88
+ let n = 0;
89
+ for (const part of marked.split(/[.!?\u2026\u3002\uFF01\uFF1F]+["'\u201D\u2019)\]]*(?=\s|$)|\n+/)) {
90
+ if (/[\p{L}\p{N}]/u.test(part)) n++;
91
+ }
92
+ return n;
93
+ }
94
+
95
+ export function duration(words, wpm) {
96
+ if (!words) return '0 s';
97
+ const s = Math.max(1, Math.round((words / wpm) * 60));
98
+ if (s < 60) return `${s} s`;
99
+ const m = Math.floor(s / 60);
100
+ if (m >= 60) return `${Math.floor(m / 60)} h ${m % 60} min`;
101
+ return s % 60 ? `${m} min ${s % 60} s` : `${m} min`;
102
+ }
103
+
104
+ export function stats(text) {
105
+ const t = text.replace(/\r\n?/g, '\n');
106
+ const lines = t ? t.split('\n') : [];
107
+ const words = wordCount(t);
108
+ return {
109
+ words,
110
+ chars: charCount(t),
111
+ nospace: charCount(t.replace(/\s+/g, '')),
112
+ sentences: sentenceCount(t),
113
+ paragraphs: lines.filter(line => line.trim()).length,
114
+ lines: lines.length,
115
+ read: duration(words, READ_WPM),
116
+ speak: duration(words, SPEAK_WPM)
117
+ };
118
+ }
119
+
120
+ export function density(text, phrases, common) {
121
+ const tokens = text.toLowerCase().match(/[\p{L}\p{N}]+(?:['\u2019][\p{L}\p{N}]+)*/gu) || [];
122
+ const words = tokens.map(t => t.replace(/\u2019/g, "'"));
123
+ const skip = w => w.length < 2 || /^\d+$/.test(w) || (common && STOP_WORDS.has(w));
124
+ const counts = new Map();
125
+ let total = 0;
126
+ if (phrases) {
127
+ for (let i = 0; i + 1 < words.length; i++) {
128
+ total++;
129
+ if (skip(words[i]) || skip(words[i + 1])) continue;
130
+ const key = `${words[i]} ${words[i + 1]}`;
131
+ counts.set(key, (counts.get(key) || 0) + 1);
132
+ }
133
+ } else {
134
+ for (const w of words) {
135
+ total++;
136
+ if (skip(w)) continue;
137
+ counts.set(w, (counts.get(w) || 0) + 1);
138
+ }
139
+ }
140
+ const top = [...counts].filter(([, n]) => !phrases || n > 1).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 12);
141
+ return { top, total };
142
+ }
143
+
144
+ export function hasInnerCaps(word) {
145
+ return /\p{Lu}/u.test(word.slice(1)) && /\p{L}/u.test(word);
146
+ }
147
+
148
+ export function capitalize(word) {
149
+ const first = word.search(/\p{L}|\p{N}/u);
150
+ if (first < 0) return word;
151
+ return word.slice(0, first) + word.charAt(first).toUpperCase() + word.slice(first + 1);
152
+ }
153
+
154
+ export function titleCase(text) {
155
+ return text.split('\n').map(line => {
156
+ const source = /\p{Ll}/u.test(line) ? line : line.toLowerCase();
157
+ const parts = source.split(/(\s+)/);
158
+ const wordIdx = parts.map((p, i) => (/[\p{L}\p{N}]/u.test(p) ? i : -1)).filter(i => i >= 0);
159
+ const lastWord = wordIdx[wordIdx.length - 1];
160
+ let newPhrase = true;
161
+ return parts.map((part, i) => {
162
+ if (!/[\p{L}\p{N}]/u.test(part)) {
163
+ if (/[:.!?\u2014\u2013]$/.test(part.trim())) newPhrase = true;
164
+ return part;
165
+ }
166
+ const starts = newPhrase;
167
+ newPhrase = /[:.!?\u2014\u2013]["'\u201D\u2019)]*$/.test(part);
168
+ if (/@|:\/\/|^www\.|\.[a-z]{2,}$/i.test(part) || hasInnerCaps(part.replace(/^[^\p{L}\p{N}]+/u, ''))) return part;
169
+ const pieces = part.split('-');
170
+ return pieces.map((piece, j) => {
171
+ const bare = piece.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, '').toLowerCase();
172
+ const edge = (starts && j === 0) || (i === lastWord && j === pieces.length - 1);
173
+ if (SMALL_WORDS.has(bare) && !edge) return piece.toLowerCase();
174
+ return capitalize(piece.toLowerCase());
175
+ }).join('-');
176
+ }).join('');
177
+ }).join('\n');
178
+ }
179
+
180
+ export function sentenceCase(text) {
181
+ const lowered = text.split(/(\s+)/).map(part => (/^[^\p{L}]*\p{Ll}+\p{Lu}/u.test(part) || /\p{Lu}\p{Ll}+\p{Lu}/u.test(part) ? part : part.toLowerCase())).join('');
182
+ let out = lowered.replace(/(^|[.!?\u2026]["'\u201D\u2019)]*\s+|\n\s*)([^\p{L}\p{N}\s]*)(\p{L})/gu, (m, lead, punct, letter) => lead + punct + letter.toUpperCase());
183
+ out = out.replace(/(^|[^\p{L}\p{N}'])i(?=$|[^\p{L}\p{N}])/gu, (m, lead) => `${lead}I`);
184
+ return out;
185
+ }
186
+
187
+ export function codeWords(line) {
188
+ return line
189
+ .replace(/['\u2019]/g, '')
190
+ .replace(/([\p{Ll}\p{N}])(\p{Lu})/gu, '$1 $2')
191
+ .replace(/(\p{Lu})(\p{Lu}\p{Ll})/gu, '$1 $2')
192
+ .split(/[^\p{L}\p{N}]+/u)
193
+ .filter(Boolean);
194
+ }
195
+
196
+ export function convertCase(text, type) {
197
+ if (type === 'upper') return text.toUpperCase();
198
+ if (type === 'lower') return text.toLowerCase();
199
+ if (type === 'title') return titleCase(text);
200
+ if (type === 'sentence') return sentenceCase(text);
201
+ return text.split('\n').map(line => {
202
+ const words = codeWords(line).map(w => w.toLowerCase());
203
+ if (!words.length) return '';
204
+ if (type === 'camel') return words[0] + words.slice(1).map(capitalize).join('');
205
+ if (type === 'pascal') return words.map(capitalize).join('');
206
+ if (type === 'snake') return words.join('_');
207
+ if (type === 'kebab') return words.join('-');
208
+ return words.join('_').toUpperCase();
209
+ }).join('\n');
210
+ }
211
+
212
+ export function splitLines(text) {
213
+ return text.replace(/\r\n?/g, '\n').split('\n');
214
+ }
215
+
216
+ export function randomInt(n) {
217
+ const buf = new Uint32Array(1);
218
+ const limit = Math.floor(0x100000000 / n) * n;
219
+ let v;
220
+ do {
221
+ crypto.getRandomValues(buf);
222
+ v = buf[0];
223
+ } while (v >= limit);
224
+ return v % n;
225
+ }
226
+
227
+ export function cleanText(text, o) {
228
+ let t = text.replace(/\r\n?/g, '\n');
229
+ if (o.invisible) t = t.replace(/[\u00A0\u2007\u202F\u2000-\u200A\u3000]/g, ' ').replace(/[\u200B-\u200D\u2060\uFEFF\u00AD]/g, '');
230
+ if (o.tabs) t = t.replace(/\t/g, ' ');
231
+ if (o.spaces) t = t.replace(/[ \t]{2,}/g, match => (match.includes('\t') && !o.tabs ? '\t' : ' '));
232
+ if (o.trim) t = t.split('\n').map(line => line.replace(/^[ \t]+|[ \t]+$/g, '')).join('\n');
233
+ if (o.breaks === 'collapse') t = t.replace(/\n[ \t]*\n(?:[ \t]*\n)+/g, '\n\n');
234
+ else if (o.breaks === 'remove') t = t.split('\n').filter(line => line.trim()).join('\n');
235
+ else if (o.breaks === 'join') {
236
+ t = t.split(/\n[ \t]*\n+/).map(block => block.split('\n').map(line => line.trim()).filter(Boolean).join(' ').replace(/(\p{L})- (\p{Ll})/gu, '$1$2')).filter(Boolean).join('\n\n');
237
+ if (o.spaces) t = t.replace(/ {2,}/g, ' ');
238
+ } else if (o.breaks === 'single') {
239
+ t = t.split('\n').map(line => line.trim()).filter(Boolean).join(' ');
240
+ if (o.spaces) t = t.replace(/ {2,}/g, ' ');
241
+ }
242
+ if (o.breaks !== 'keep') t = t.replace(/^\n+|\n+$/g, '');
243
+ return t;
244
+ }
245
+
246
+ export function escapeRegex(text) {
247
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
248
+ }
249
+
250
+ export function regexError(error) {
251
+ const message = String((error && error.message) || error);
252
+ const reason = message.includes(': ') ? message.slice(message.lastIndexOf(': ') + 2) : message;
253
+ return `That pattern is not a valid regular expression: ${reason.replace(/\.$/, '')}.`;
254
+ }
255
+
256
+ export function dedupeLines(text, o) {
257
+ const seen = new Set();
258
+ const out = [];
259
+ let dupes = 0;
260
+ let empties = 0;
261
+ for (const line of splitLines(text)) {
262
+ if (!line.trim()) {
263
+ if (o.dropEmpty) {
264
+ empties++;
265
+ continue;
266
+ }
267
+ out.push(line);
268
+ continue;
269
+ }
270
+ let key = o.trim ? line.trim() : line;
271
+ if (o.ignoreCase) key = key.toLocaleLowerCase();
272
+ if (seen.has(key)) {
273
+ dupes++;
274
+ continue;
275
+ }
276
+ seen.add(key);
277
+ out.push(line);
278
+ }
279
+ return { out, dupes, empties };
280
+ }
281
+
282
+ export function sortLines(text, o, shuffle) {
283
+ let lines = splitLines(text);
284
+ if (o.dropEmpty) lines = lines.filter(line => line.trim());
285
+ if (o.unique) {
286
+ const seen = new Set();
287
+ lines = lines.filter(line => {
288
+ const key = o.ignoreCase ? line.trim().toLocaleLowerCase() : line.trim();
289
+ if (seen.has(key)) return false;
290
+ seen.add(key);
291
+ return true;
292
+ });
293
+ }
294
+ const plain = new Intl.Collator(undefined, { sensitivity: o.ignoreCase ? 'base' : 'variant', caseFirst: 'upper' });
295
+ const natural = new Intl.Collator(undefined, { sensitivity: o.ignoreCase ? 'base' : 'variant', numeric: true, caseFirst: 'upper' });
296
+ const tie = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
297
+ switch (o.order) {
298
+ case 'az': lines.sort((a, b) => plain.compare(a.trim(), b.trim()) || tie(a, b)); break;
299
+ case 'za': lines.sort((a, b) => plain.compare(b.trim(), a.trim()) || tie(b, a)); break;
300
+ case 'natural': lines.sort((a, b) => natural.compare(a.trim(), b.trim()) || tie(a, b)); break;
301
+ case 'short': lines.sort((a, b) => charCount(a) - charCount(b) || plain.compare(a, b)); break;
302
+ case 'long': lines.sort((a, b) => charCount(b) - charCount(a) || plain.compare(a, b)); break;
303
+ case 'reverse': lines.reverse(); break;
304
+ default: lines = shuffle(lines);
305
+ }
306
+ return lines;
307
+ }
308
+
309
+ export function shuffleLines(lines, pick = randomInt) {
310
+ for (let i = lines.length - 1; i > 0; i--) {
311
+ const j = pick(i + 1);
312
+ [lines[i], lines[j]] = [lines[j], lines[i]];
313
+ }
314
+ return lines;
315
+ }
316
+
317
+ export function replaceFlags(o) {
318
+ return `g${o.matchCase ? '' : 'i'}${o.regex ? 'm' : ''}`;
319
+ }
320
+
321
+ export function plainReplace(text, o) {
322
+ const flags = replaceFlags(o);
323
+ const core = escapeRegex(o.find);
324
+ let re;
325
+ try {
326
+ re = o.whole ? new RegExp(`(?<![\\p{L}\\p{N}_])${core}(?![\\p{L}\\p{N}_])`, `${flags}u`) : new RegExp(core, flags);
327
+ } catch {
328
+ re = new RegExp(o.whole ? `\\b${core}\\b` : core, flags);
329
+ }
330
+ const found = text.match(re);
331
+ return { result: text.replace(re, () => o.with), count: found ? found.length : 0 };
332
+ }
333
+
334
+ export function regexPattern(o) {
335
+ return o.whole ? `\\b(?:${o.find})\\b` : o.find;
336
+ }
337
+
338
+ export function expandReplacement(text) {
339
+ return text.replace(/\\(n|t|\\)/g, (m, c) => (c === 'n' ? '\n' : c === 't' ? '\t' : '\\'));
340
+ }
341
+
342
+ export function regexReplace(text, pattern, flags, replacement) {
343
+ const re = new RegExp(pattern, flags);
344
+ const found = text.match(re);
345
+ return { result: text.replace(re, replacement), count: found ? found.length : 0 };
346
+ }
347
+
348
+ export function diffSeq(a, b) {
349
+ const ops = [];
350
+ let start = 0;
351
+ while (start < a.length && start < b.length && a[start] === b[start]) {
352
+ ops.push(['=', start, start]);
353
+ start++;
354
+ }
355
+ let endA = a.length;
356
+ let endB = b.length;
357
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
358
+ endA--;
359
+ endB--;
360
+ }
361
+ const A = a.slice(start, endA);
362
+ const B = b.slice(start, endB);
363
+ const middle = myers(A, B);
364
+ for (const [type, i, j] of middle) ops.push([type, i < 0 ? -1 : i + start, j < 0 ? -1 : j + start]);
365
+ for (let k = 0; endA + k < a.length; k++) ops.push(['=', endA + k, endB + k]);
366
+ return ops;
367
+ }
368
+
369
+ export function myers(A, B) {
370
+ const N = A.length;
371
+ const M = B.length;
372
+ if (!N) return B.map((_, j) => ['+', -1, j]);
373
+ if (!M) return A.map((_, i) => ['-', i, -1]);
374
+ const MAX = N + M;
375
+ const off = MAX + 1;
376
+ const V = new Int32Array(2 * MAX + 3);
377
+ const trace = [];
378
+ for (let d = 0; d <= MAX; d++) {
379
+ if (d > 2500) return A.map((_, i) => ['-', i, -1]).concat(B.map((_, j) => ['+', -1, j]));
380
+ trace.push({ lo: -d - 1, arr: V.slice(off - d - 1, off + d + 2) });
381
+ for (let k = -d; k <= d; k += 2) {
382
+ let x = k === -d || (k !== d && V[off + k - 1] < V[off + k + 1]) ? V[off + k + 1] : V[off + k - 1] + 1;
383
+ let y = x - k;
384
+ while (x < N && y < M && A[x] === B[y]) {
385
+ x++;
386
+ y++;
387
+ }
388
+ V[off + k] = x;
389
+ if (x >= N && y >= M) return backtrack(trace, d, N, M);
390
+ }
391
+ }
392
+ return [];
393
+ }
394
+
395
+ export function backtrack(trace, d, N, M) {
396
+ const ops = [];
397
+ let x = N;
398
+ let y = M;
399
+ for (let dd = d; dd > 0; dd--) {
400
+ const snap = trace[dd];
401
+ const get = k => snap.arr[k - snap.lo];
402
+ const k = x - y;
403
+ const prevK = k === -dd || (k !== dd && get(k - 1) < get(k + 1)) ? k + 1 : k - 1;
404
+ const prevX = get(prevK);
405
+ const prevY = prevX - prevK;
406
+ while (x > prevX && y > prevY) {
407
+ ops.push(['=', x - 1, y - 1]);
408
+ x--;
409
+ y--;
410
+ }
411
+ if (x === prevX) ops.push(['+', -1, y - 1]);
412
+ else ops.push(['-', x - 1, -1]);
413
+ x = prevX;
414
+ y = prevY;
415
+ }
416
+ while (x > 0 && y > 0) {
417
+ ops.push(['=', x - 1, y - 1]);
418
+ x--;
419
+ y--;
420
+ }
421
+ return ops.reverse();
422
+ }
423
+
424
+ export function tokens(text) {
425
+ return text.match(/\s+|[\p{L}\p{N}_]+|[^\s\p{L}\p{N}_]/gu) || [];
426
+ }
427
+
428
+ export function keyOf(text, o) {
429
+ let t = o.ignoreCase ? text.toLocaleLowerCase() : text;
430
+ if (o.ignoreSpace) t = t.replace(/\s+/g, ' ').trim();
431
+ return t;
432
+ }
433
+
434
+ export function wordDiff(a, b, o) {
435
+ const ta = tokens(a);
436
+ const tb = tokens(b);
437
+ const ka = ta.map(t => (o.ignoreSpace && /^\s+$/.test(t) ? ' ' : keyOf(t, o)));
438
+ const kb = tb.map(t => (o.ignoreSpace && /^\s+$/.test(t) ? ' ' : keyOf(t, o)));
439
+ return { ops: diffSeq(ka, kb), ta, tb };
440
+ }
441
+
442
+ export function coalesce(parts) {
443
+ const kinds = parts.map(p => p[0]);
444
+ for (let i = 1; i + 1 < parts.length; i++) {
445
+ if (kinds[i] === '=' && /^\s+$/.test(parts[i][1]) && kinds[i - 1] !== '=' && kinds[i - 1] === kinds[i + 1]) kinds[i] = kinds[i - 1];
446
+ }
447
+ const out = [];
448
+ parts.forEach((p, i) => {
449
+ const last = out[out.length - 1];
450
+ if (last && last[0] === kinds[i]) last[1] += p[1];
451
+ else out.push([kinds[i], p[1]]);
452
+ });
453
+ return out;
454
+ }
455
+
456
+ export function lineParts(a, b, o) {
457
+ const { ops, ta, tb } = wordDiff(a, b, o);
458
+ const same = ops.filter(op => op[0] === '=' && /\S/.test(ta[op[1]])).length;
459
+ const words = Math.max(1, ta.filter(t => /\S/.test(t)).length, tb.filter(t => /\S/.test(t)).length);
460
+ if (same / words < 0.3) return null;
461
+ const left = [];
462
+ const right = [];
463
+ for (const [type, i, j] of ops) {
464
+ if (type === '=') {
465
+ left.push(['=', ta[i]]);
466
+ right.push(['=', tb[j]]);
467
+ } else if (type === '-') left.push(['-', ta[i]]);
468
+ else right.push(['+', tb[j]]);
469
+ }
470
+ return { left, right };
471
+ }
472
+
473
+ export const CONTEXT = 3;
474
+
475
+ export function buildRows(ops, a, b, o) {
476
+ const rows = [];
477
+ let i = 0;
478
+ while (i < ops.length) {
479
+ if (ops[i][0] === '=') {
480
+ const run = [];
481
+ while (i < ops.length && ops[i][0] === '=') run.push(ops[i++]);
482
+ rows.push({ type: 'same', run });
483
+ } else {
484
+ const dels = [];
485
+ const adds = [];
486
+ while (i < ops.length && ops[i][0] !== '=') {
487
+ if (ops[i][0] === '-') dels.push(ops[i][1]);
488
+ else adds.push(ops[i][2]);
489
+ i++;
490
+ }
491
+ rows.push({ type: 'change', dels, adds });
492
+ }
493
+ }
494
+ return rows;
495
+ }
496
+
497
+ export function unified(diff) {
498
+ const { ops, a, b } = diff;
499
+ const changes = [];
500
+ ops.forEach((op, k) => {
501
+ if (op[0] !== '=') changes.push(k);
502
+ });
503
+ if (!changes.length) return '';
504
+ const hunks = [];
505
+ let from = changes[0];
506
+ let to = changes[0];
507
+ for (const k of changes.slice(1)) {
508
+ if (k - to <= CONTEXT * 2) to = k;
509
+ else {
510
+ hunks.push([from, to]);
511
+ from = to = k;
512
+ }
513
+ }
514
+ hunks.push([from, to]);
515
+ const out = ['--- Original', '+++ Changed'];
516
+ for (const [s, e] of hunks) {
517
+ const lo = Math.max(0, s - CONTEXT);
518
+ const hi = Math.min(ops.length - 1, e + CONTEXT);
519
+ let aLine = 0;
520
+ let bLine = 0;
521
+ for (let k = 0; k < lo; k++) {
522
+ if (ops[k][0] !== '+') aLine++;
523
+ if (ops[k][0] !== '-') bLine++;
524
+ }
525
+ let aCount = 0;
526
+ let bCount = 0;
527
+ const body = [];
528
+ for (let k = lo; k <= hi; k++) {
529
+ const [type, i, j] = ops[k];
530
+ if (type === '=') {
531
+ body.push(` ${a[i]}`);
532
+ aCount++;
533
+ bCount++;
534
+ } else if (type === '-') {
535
+ body.push(`-${a[i]}`);
536
+ aCount++;
537
+ } else {
538
+ body.push(`+${b[j]}`);
539
+ bCount++;
540
+ }
541
+ }
542
+ out.push(`@@ -${aCount ? aLine + 1 : aLine},${aCount} +${bCount ? bLine + 1 : bLine},${bCount} @@`, ...body);
543
+ }
544
+ return `${out.join('\n')}\n`;
545
+ }
546
+
547
+ export function seeded(seed) {
548
+ let s = seed >>> 0 || 1;
549
+ return () => {
550
+ s ^= s << 13;
551
+ s ^= s >>> 17;
552
+ s ^= s << 5;
553
+ return (s >>> 0) / 0x100000000;
554
+ };
555
+ }
556
+
557
+ export function sampleText(o) {
558
+ const rand = seeded(o.seed * 2654435761);
559
+ const order = SENTENCES.map((s, i) => i);
560
+ for (let i = order.length - 1; i > 0; i--) {
561
+ const j = Math.floor(rand() * (i + 1));
562
+ [order[i], order[j]] = [order[j], order[i]];
563
+ }
564
+ if (o.seed === 1) order.sort((x, y) => x - y);
565
+ let at = 0;
566
+ const next = () => SENTENCES[order[at++ % order.length]];
567
+ if (o.unit === 'sentences') {
568
+ const list = [];
569
+ for (let i = 0; i < o.amount; i++) list.push(next());
570
+ return o.html ? `<p>${list.join(' ')}</p>` : list.join(' ');
571
+ }
572
+ if (o.unit === 'words') {
573
+ const words = [];
574
+ while (words.length < o.amount) words.push(...next().replace(/[.,]/g, '').split(' '));
575
+ let text = words.slice(0, o.amount).join(' ');
576
+ text = `${text.charAt(0).toUpperCase()}${text.slice(1)}.`;
577
+ return o.html ? `<p>${text}</p>` : text;
578
+ }
579
+ const paragraphs = [];
580
+ for (let p = 0; p < o.amount; p++) {
581
+ const size = 3 + Math.floor(rand() * 3);
582
+ const list = [];
583
+ for (let i = 0; i < size; i++) list.push(next());
584
+ paragraphs.push(list.join(' '));
585
+ }
586
+ return o.html ? paragraphs.map(p => `<p>${p}</p>`).join('\n') : paragraphs.join('\n\n');
587
+ }
588
+
589
+ export function translit(text, keepCase) {
590
+ let out = '';
591
+ for (const ch of text) {
592
+ const lower = ch.toLowerCase();
593
+ const mapped = MAP[lower];
594
+ if (mapped === undefined) out += ch;
595
+ else out += keepCase && ch !== lower ? mapped.charAt(0).toUpperCase() + mapped.slice(1) : mapped;
596
+ }
597
+ return out;
598
+ }
599
+
600
+ export function slugLine(line, o) {
601
+ let s = line.normalize('NFC');
602
+ if (o.ascii) {
603
+ s = translit(s, !o.lower).normalize('NFKD').replace(/[\u0300-\u036F]/g, '');
604
+ s = translit(s, !o.lower);
605
+ }
606
+ s = s.replace(/&/g, ' and ').replace(/['\u2019`]/g, '');
607
+ if (o.lower) s = s.toLowerCase();
608
+ let words = s.split(o.ascii ? /[^A-Za-z0-9]+/ : /[^\p{L}\p{N}]+/u).filter(Boolean);
609
+ if (o.stop) {
610
+ const kept = words.filter(w => !SLUG_STOP.has(w.toLowerCase()));
611
+ if (kept.length) words = kept;
612
+ }
613
+ let slug = words.join(o.sep);
614
+ if (o.max && slug.length > o.max) {
615
+ let cut = slug.slice(0, o.max);
616
+ if (slug.charAt(o.max) !== o.sep) {
617
+ const at = cut.lastIndexOf(o.sep);
618
+ if (at > 0) cut = cut.slice(0, at);
619
+ }
620
+ slug = cut.split(o.sep).filter(Boolean).join(o.sep);
621
+ }
622
+ return slug;
623
+ }