@fuzdev/fuz_util 0.48.2 → 0.48.4
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/dist/async.d.ts +11 -0
- package/dist/async.d.ts.map +1 -1
- package/dist/async.js +30 -0
- package/dist/dag.d.ts +80 -0
- package/dist/dag.d.ts.map +1 -0
- package/dist/dag.js +156 -0
- package/dist/diff.d.ts +61 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +185 -0
- package/dist/hash.d.ts +27 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +66 -0
- package/dist/sort.d.ts +38 -0
- package/dist/sort.d.ts.map +1 -0
- package/dist/sort.js +123 -0
- package/dist/source_json.d.ts +4 -4
- package/dist/string.d.ts +9 -0
- package/dist/string.d.ts.map +1 -1
- package/dist/string.js +12 -0
- package/package.json +7 -7
- package/src/lib/async.ts +33 -0
- package/src/lib/dag.ts +240 -0
- package/src/lib/diff.ts +234 -0
- package/src/lib/hash.ts +73 -0
- package/src/lib/sort.ts +160 -0
- package/src/lib/string.ts +13 -0
package/src/lib/diff.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-based diff utilities using LCS (Longest Common Subsequence).
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {is_binary} from './string.js';
|
|
8
|
+
|
|
9
|
+
/** Line diff result */
|
|
10
|
+
export interface DiffLine {
|
|
11
|
+
type: 'same' | 'add' | 'remove';
|
|
12
|
+
line: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate a line-based diff between two strings using LCS algorithm.
|
|
17
|
+
*
|
|
18
|
+
* @param a - The original/current content.
|
|
19
|
+
* @param b - The new/desired content.
|
|
20
|
+
* @returns Array of diff lines with type annotations.
|
|
21
|
+
*/
|
|
22
|
+
export const diff_lines = (a: string, b: string): Array<DiffLine> => {
|
|
23
|
+
const a_lines = a.split('\n');
|
|
24
|
+
const b_lines = b.split('\n');
|
|
25
|
+
const result: Array<DiffLine> = [];
|
|
26
|
+
|
|
27
|
+
const lcs = compute_lcs(a_lines, b_lines);
|
|
28
|
+
let ai = 0;
|
|
29
|
+
let bi = 0;
|
|
30
|
+
let li = 0;
|
|
31
|
+
|
|
32
|
+
while (ai < a_lines.length || bi < b_lines.length) {
|
|
33
|
+
if (li < lcs.length && ai < a_lines.length && a_lines[ai] === lcs[li]) {
|
|
34
|
+
if (bi < b_lines.length && b_lines[bi] === lcs[li]) {
|
|
35
|
+
result.push({type: 'same', line: a_lines[ai]!});
|
|
36
|
+
ai++;
|
|
37
|
+
bi++;
|
|
38
|
+
li++;
|
|
39
|
+
} else {
|
|
40
|
+
result.push({type: 'add', line: b_lines[bi]!});
|
|
41
|
+
bi++;
|
|
42
|
+
}
|
|
43
|
+
} else if (ai < a_lines.length && (li >= lcs.length || a_lines[ai] !== lcs[li])) {
|
|
44
|
+
result.push({type: 'remove', line: a_lines[ai]!});
|
|
45
|
+
ai++;
|
|
46
|
+
} else if (bi < b_lines.length) {
|
|
47
|
+
result.push({type: 'add', line: b_lines[bi]!});
|
|
48
|
+
bi++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Compute longest common subsequence of two string arrays.
|
|
57
|
+
*
|
|
58
|
+
* Uses dynamic programming with O(m*n) time and space complexity.
|
|
59
|
+
*/
|
|
60
|
+
const compute_lcs = (a: Array<string>, b: Array<string>): Array<string> => {
|
|
61
|
+
const m = a.length;
|
|
62
|
+
const n = b.length;
|
|
63
|
+
const dp: Array<Array<number>> = Array.from({length: m + 1}, () => Array(n + 1).fill(0));
|
|
64
|
+
|
|
65
|
+
for (let i = 1; i <= m; i++) {
|
|
66
|
+
for (let j = 1; j <= n; j++) {
|
|
67
|
+
if (a[i - 1] === b[j - 1]) {
|
|
68
|
+
dp[i]![j] = dp[i - 1]![j - 1]! + 1;
|
|
69
|
+
} else {
|
|
70
|
+
dp[i]![j] = Math.max(dp[i - 1]![j]!, dp[i]![j - 1]!);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Backtrack to find LCS
|
|
76
|
+
const lcs: Array<string> = [];
|
|
77
|
+
let i = m;
|
|
78
|
+
let j = n;
|
|
79
|
+
while (i > 0 && j > 0) {
|
|
80
|
+
if (a[i - 1] === b[j - 1]) {
|
|
81
|
+
lcs.unshift(a[i - 1]!);
|
|
82
|
+
i--;
|
|
83
|
+
j--;
|
|
84
|
+
} else if (dp[i - 1]![j]! > dp[i]![j - 1]!) {
|
|
85
|
+
i--;
|
|
86
|
+
} else {
|
|
87
|
+
j--;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return lcs;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Filter diff to only include lines within N lines of context around changes.
|
|
96
|
+
*
|
|
97
|
+
* @param diff - The full diff lines.
|
|
98
|
+
* @param context_lines - Number of context lines to show around changes (default: 3).
|
|
99
|
+
* @returns Filtered diff with ellipsis markers for skipped regions.
|
|
100
|
+
*/
|
|
101
|
+
export const filter_diff_context = (diff: Array<DiffLine>, context_lines = 3): Array<DiffLine> => {
|
|
102
|
+
if (diff.length === 0) return [];
|
|
103
|
+
|
|
104
|
+
// Find indices of all changed lines
|
|
105
|
+
const changed_indices: Array<number> = [];
|
|
106
|
+
for (let i = 0; i < diff.length; i++) {
|
|
107
|
+
if (diff[i]!.type !== 'same') {
|
|
108
|
+
changed_indices.push(i);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (changed_indices.length === 0) return [];
|
|
113
|
+
|
|
114
|
+
// Build set of indices to include (changed lines + context)
|
|
115
|
+
const include_indices: Set<number> = new Set();
|
|
116
|
+
for (const idx of changed_indices) {
|
|
117
|
+
for (
|
|
118
|
+
let i = Math.max(0, idx - context_lines);
|
|
119
|
+
i <= Math.min(diff.length - 1, idx + context_lines);
|
|
120
|
+
i++
|
|
121
|
+
) {
|
|
122
|
+
include_indices.add(i);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Build result with ellipsis markers for gaps
|
|
127
|
+
const result: Array<DiffLine> = [];
|
|
128
|
+
let last_included = -1;
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < diff.length; i++) {
|
|
131
|
+
if (include_indices.has(i)) {
|
|
132
|
+
// Add ellipsis if there's a gap
|
|
133
|
+
if (last_included >= 0 && i > last_included + 1) {
|
|
134
|
+
result.push({type: 'same', line: '...'});
|
|
135
|
+
}
|
|
136
|
+
result.push(diff[i]!);
|
|
137
|
+
last_included = i;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return result;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** ANSI color codes */
|
|
145
|
+
const colors = {
|
|
146
|
+
red: '\x1b[31m',
|
|
147
|
+
green: '\x1b[32m',
|
|
148
|
+
reset: '\x1b[0m',
|
|
149
|
+
} as const;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Format options for diff output.
|
|
153
|
+
*/
|
|
154
|
+
export interface FormatDiffOptions {
|
|
155
|
+
/** Prefix for each line (for indentation in plan output). */
|
|
156
|
+
prefix?: string;
|
|
157
|
+
/** Whether to use ANSI colors. */
|
|
158
|
+
use_color?: boolean;
|
|
159
|
+
/** Maximum number of diff lines to show (0 = unlimited). */
|
|
160
|
+
max_lines?: number;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Format a diff for display.
|
|
165
|
+
*
|
|
166
|
+
* @param diff - The diff lines to format.
|
|
167
|
+
* @param current_path - Path label for "current" content.
|
|
168
|
+
* @param desired_path - Path label for "desired" content.
|
|
169
|
+
* @param options - Formatting options.
|
|
170
|
+
* @returns Formatted diff string.
|
|
171
|
+
*/
|
|
172
|
+
export const format_diff = (
|
|
173
|
+
diff: Array<DiffLine>,
|
|
174
|
+
current_path: string,
|
|
175
|
+
desired_path: string,
|
|
176
|
+
options: FormatDiffOptions = {},
|
|
177
|
+
): string => {
|
|
178
|
+
const {prefix = '', use_color = true, max_lines = 50} = options;
|
|
179
|
+
|
|
180
|
+
const lines: Array<string> = [
|
|
181
|
+
`${prefix}--- ${current_path} (current)`,
|
|
182
|
+
`${prefix}+++ ${desired_path} (desired)`,
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
let count = 0;
|
|
186
|
+
for (const d of diff) {
|
|
187
|
+
if (max_lines > 0 && count >= max_lines) {
|
|
188
|
+
const remaining = diff.length - count;
|
|
189
|
+
lines.push(`${prefix}... (${remaining} more lines)`);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const line_prefix = d.type === 'add' ? '+' : d.type === 'remove' ? '-' : ' ';
|
|
194
|
+
|
|
195
|
+
if (use_color && d.type !== 'same') {
|
|
196
|
+
const color = d.type === 'add' ? colors.green : colors.red;
|
|
197
|
+
lines.push(`${prefix}${color}${line_prefix}${d.line}${colors.reset}`);
|
|
198
|
+
} else {
|
|
199
|
+
lines.push(`${prefix}${line_prefix}${d.line}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
count++;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return lines.join('\n');
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Generate a formatted diff between two strings.
|
|
210
|
+
*
|
|
211
|
+
* Combines diff_lines, filter_diff_context, and format_diff for convenience.
|
|
212
|
+
* Returns null if content is binary.
|
|
213
|
+
*
|
|
214
|
+
* @param current - Current content.
|
|
215
|
+
* @param desired - Desired content.
|
|
216
|
+
* @param path - File path for labels.
|
|
217
|
+
* @param options - Formatting options.
|
|
218
|
+
* @returns Formatted diff string, or null if binary.
|
|
219
|
+
*/
|
|
220
|
+
export const generate_diff = (
|
|
221
|
+
current: string,
|
|
222
|
+
desired: string,
|
|
223
|
+
path: string,
|
|
224
|
+
options: FormatDiffOptions = {},
|
|
225
|
+
): string | null => {
|
|
226
|
+
// Skip binary files
|
|
227
|
+
if (is_binary(current) || is_binary(desired)) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const diff = diff_lines(current, desired);
|
|
232
|
+
const filtered = filter_diff_context(diff);
|
|
233
|
+
return format_diff(filtered, path, path, options);
|
|
234
|
+
};
|
package/src/lib/hash.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash utilities for content comparison and cache invalidation.
|
|
3
|
+
*
|
|
4
|
+
* Provides both secure (cryptographic) and insecure (fast) hash functions.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
|
|
11
|
+
// Lazily computed lookup table for byte to hex conversion
|
|
12
|
+
let byte_to_hex: Array<string> | undefined;
|
|
13
|
+
const get_byte_to_hex = (): Array<string> => {
|
|
14
|
+
if (byte_to_hex === undefined) {
|
|
15
|
+
byte_to_hex = new Array(256); // 256 possible byte values (0x00-0xff)
|
|
16
|
+
for (let i = 0; i < 256; i++) {
|
|
17
|
+
byte_to_hex[i] = i.toString(16).padStart(2, '0');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return byte_to_hex;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Computes a cryptographic hash using Web Crypto API.
|
|
25
|
+
*
|
|
26
|
+
* @param data - String or binary data to hash. Strings are UTF-8 encoded.
|
|
27
|
+
* @param algorithm - Hash algorithm. Defaults to SHA-256.
|
|
28
|
+
* @returns Hexadecimal hash string.
|
|
29
|
+
*/
|
|
30
|
+
export const hash_secure = async (
|
|
31
|
+
data: BufferSource | string,
|
|
32
|
+
algorithm: 'SHA-256' | 'SHA-384' | 'SHA-512' = 'SHA-256',
|
|
33
|
+
): Promise<string> => {
|
|
34
|
+
const buffer = typeof data === 'string' ? encoder.encode(data) : data;
|
|
35
|
+
const digested = await crypto.subtle.digest(algorithm, buffer);
|
|
36
|
+
const bytes = new Uint8Array(digested);
|
|
37
|
+
const lookup = get_byte_to_hex();
|
|
38
|
+
let hex = '';
|
|
39
|
+
for (const byte of bytes) {
|
|
40
|
+
hex += lookup[byte];
|
|
41
|
+
}
|
|
42
|
+
return hex;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Computes a fast non-cryptographic hash using DJB2 algorithm.
|
|
47
|
+
* Use for content comparison and cache keys, not security.
|
|
48
|
+
*
|
|
49
|
+
* Note: Strings use UTF-16 code units, buffers use raw bytes.
|
|
50
|
+
* For non-ASCII, `hash_insecure(str) !== hash_insecure(encoder.encode(str))`.
|
|
51
|
+
*
|
|
52
|
+
* @param data - String or binary data to hash.
|
|
53
|
+
* @returns 8-character hex-encoded unsigned 32-bit hash.
|
|
54
|
+
*/
|
|
55
|
+
export const hash_insecure = (data: BufferSource | string): string => {
|
|
56
|
+
let hash = 5381; // DJB2 initial value, chosen empirically for good distribution
|
|
57
|
+
if (typeof data === 'string') {
|
|
58
|
+
for (let i = 0; i < data.length; i++) {
|
|
59
|
+
hash = (hash << 5) - hash + data.charCodeAt(i);
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
const bytes: Uint8Array =
|
|
63
|
+
data instanceof Uint8Array
|
|
64
|
+
? data
|
|
65
|
+
: data instanceof ArrayBuffer
|
|
66
|
+
? new Uint8Array(data)
|
|
67
|
+
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
68
|
+
for (const byte of bytes) {
|
|
69
|
+
hash = (hash << 5) - hash + byte;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
73
|
+
};
|
package/src/lib/sort.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic topological sort using Kahn's algorithm.
|
|
3
|
+
*
|
|
4
|
+
* Orders items so that dependencies come before dependents.
|
|
5
|
+
* Works with any item type that has `id` and optional `depends_on`.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Minimum shape required for topological sorting.
|
|
12
|
+
*/
|
|
13
|
+
export interface Sortable {
|
|
14
|
+
id: string;
|
|
15
|
+
depends_on?: Array<string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Result of topological sort.
|
|
20
|
+
*/
|
|
21
|
+
export type TopologicalSortResult<T extends Sortable> =
|
|
22
|
+
| {ok: true; sorted: Array<T>}
|
|
23
|
+
| {ok: false; error: string; cycle?: Array<string>};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sort items by their dependencies using Kahn's algorithm.
|
|
27
|
+
*
|
|
28
|
+
* Returns items ordered so that dependencies come before dependents.
|
|
29
|
+
* If a cycle is detected, returns an error with the cycle path.
|
|
30
|
+
*
|
|
31
|
+
* @param items - Array of items to sort.
|
|
32
|
+
* @param label - Label for error messages (e.g. "resource", "step").
|
|
33
|
+
* @returns Sorted items or error if cycle detected.
|
|
34
|
+
*/
|
|
35
|
+
export const topological_sort = <T extends Sortable>(
|
|
36
|
+
items: Array<T>,
|
|
37
|
+
label: string = 'item',
|
|
38
|
+
): TopologicalSortResult<T> => {
|
|
39
|
+
// Build id -> item map
|
|
40
|
+
const item_map: Map<string, T> = new Map();
|
|
41
|
+
for (const item of items) {
|
|
42
|
+
if (item_map.has(item.id)) {
|
|
43
|
+
return {ok: false, error: `duplicate ${label} id: ${item.id}`};
|
|
44
|
+
}
|
|
45
|
+
item_map.set(item.id, item);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Validate all dependencies exist and count dependents per item
|
|
49
|
+
// dependents_count[X] = how many items list X in their depends_on
|
|
50
|
+
const dependents_count: Map<string, number> = new Map();
|
|
51
|
+
for (const item of items) {
|
|
52
|
+
dependents_count.set(item.id, 0);
|
|
53
|
+
}
|
|
54
|
+
for (const item of items) {
|
|
55
|
+
const deps = item.depends_on ?? [];
|
|
56
|
+
for (const dep of deps) {
|
|
57
|
+
if (!item_map.has(dep)) {
|
|
58
|
+
return {ok: false, error: `${label} "${item.id}" depends on unknown ${label} "${dep}"`};
|
|
59
|
+
}
|
|
60
|
+
dependents_count.set(dep, dependents_count.get(dep)! + 1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Start from leaf items (nothing depends on them), work toward roots
|
|
65
|
+
const queue: Array<string> = [];
|
|
66
|
+
for (const [id, count] of dependents_count) {
|
|
67
|
+
if (count === 0) {
|
|
68
|
+
queue.push(id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Process leaves first, then items whose dependents are all processed
|
|
73
|
+
const sorted: Array<T> = [];
|
|
74
|
+
const visited: Set<string> = new Set();
|
|
75
|
+
|
|
76
|
+
while (queue.length > 0) {
|
|
77
|
+
const id = queue.shift()!;
|
|
78
|
+
if (visited.has(id)) continue;
|
|
79
|
+
visited.add(id);
|
|
80
|
+
|
|
81
|
+
sorted.push(item_map.get(id)!);
|
|
82
|
+
|
|
83
|
+
// This item is processed — decrement its dependencies' dependent counts
|
|
84
|
+
const deps = item_map.get(id)!.depends_on ?? [];
|
|
85
|
+
for (const dep of deps) {
|
|
86
|
+
const new_count = dependents_count.get(dep)! - 1;
|
|
87
|
+
dependents_count.set(dep, new_count);
|
|
88
|
+
if (new_count === 0) {
|
|
89
|
+
queue.push(dep);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check for cycle
|
|
95
|
+
if (sorted.length !== items.length) {
|
|
96
|
+
const unvisited = items.filter((item) => !visited.has(item.id)).map((item) => item.id);
|
|
97
|
+
const cycle = find_cycle(item_map, unvisited);
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
error: `dependency cycle detected: ${cycle.join(' -> ')}`,
|
|
101
|
+
cycle,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Reverse: leaves were processed first, but dependencies must come first in output
|
|
106
|
+
sorted.reverse();
|
|
107
|
+
|
|
108
|
+
return {ok: true, sorted};
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Find a cycle in the dependency graph starting from unvisited nodes.
|
|
113
|
+
* Used for error reporting when a cycle is detected.
|
|
114
|
+
*/
|
|
115
|
+
const find_cycle = <T extends Sortable>(
|
|
116
|
+
item_map: Map<string, T>,
|
|
117
|
+
unvisited: Array<string>,
|
|
118
|
+
): Array<string> => {
|
|
119
|
+
const unvisited_set = new Set(unvisited);
|
|
120
|
+
|
|
121
|
+
// DFS to find cycle
|
|
122
|
+
const path: Array<string> = [];
|
|
123
|
+
const in_path: Set<string> = new Set();
|
|
124
|
+
|
|
125
|
+
const dfs = (id: string): boolean => {
|
|
126
|
+
if (in_path.has(id)) {
|
|
127
|
+
path.push(id);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
if (!unvisited_set.has(id)) return false;
|
|
131
|
+
|
|
132
|
+
in_path.add(id);
|
|
133
|
+
path.push(id);
|
|
134
|
+
|
|
135
|
+
const item = item_map.get(id);
|
|
136
|
+
const deps = item?.depends_on ?? [];
|
|
137
|
+
for (const dep of deps) {
|
|
138
|
+
if (dfs(dep)) return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
in_path.delete(id);
|
|
142
|
+
path.pop();
|
|
143
|
+
return false;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (unvisited.length > 0) {
|
|
147
|
+
dfs(unvisited[0]!);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Extract just the cycle portion
|
|
151
|
+
if (path.length > 0) {
|
|
152
|
+
const last = path[path.length - 1]!;
|
|
153
|
+
const cycle_start = path.indexOf(last);
|
|
154
|
+
if (cycle_start < path.length - 1) {
|
|
155
|
+
return path.slice(cycle_start);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return unvisited;
|
|
160
|
+
};
|
package/src/lib/string.ts
CHANGED
|
@@ -208,3 +208,16 @@ export const levenshtein_distance = (a: string, b: string): number => {
|
|
|
208
208
|
|
|
209
209
|
return prev[short_len]!;
|
|
210
210
|
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Check if content appears to be binary.
|
|
214
|
+
*
|
|
215
|
+
* Checks for null bytes in the first 8KB of content.
|
|
216
|
+
*
|
|
217
|
+
* @param content - Content to check.
|
|
218
|
+
* @returns True if content appears to be binary.
|
|
219
|
+
*/
|
|
220
|
+
export const is_binary = (content: string): boolean => {
|
|
221
|
+
const sample = content.slice(0, 8192);
|
|
222
|
+
return sample.includes('\0');
|
|
223
|
+
};
|