pi-hashline-edit-pro 0.11.4 → 0.11.5
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 +1 -1
- package/src/file-kind.ts +134 -134
- package/src/replace-normalize.ts +18 -2
package/package.json
CHANGED
package/src/file-kind.ts
CHANGED
|
@@ -3,158 +3,158 @@ import { fileTypeFromBuffer } from "file-type";
|
|
|
3
3
|
import { SNIFF_BYTES, MAX_BYTES } from "./constants";
|
|
4
4
|
|
|
5
5
|
const IMG_TYPES = new Set<string>([
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
"image/jpeg",
|
|
7
|
+
"image/png",
|
|
8
|
+
"image/gif",
|
|
9
|
+
"image/webp",
|
|
10
10
|
]);
|
|
11
11
|
|
|
12
12
|
const TEXT_TYPES = new Set<string>([
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
"application/rtf",
|
|
14
|
+
"application/xml",
|
|
15
|
+
"application/x-ms-regedit",
|
|
16
16
|
]);
|
|
17
17
|
|
|
18
18
|
function isTextType(mimeType: string): boolean {
|
|
19
|
-
|
|
19
|
+
return mimeType.startsWith("text/") || TEXT_TYPES.has(mimeType);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export type FKind =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
| { kind: "directory" }
|
|
24
|
+
| { kind: "image"; mimeType: string }
|
|
25
|
+
| { kind: "text" }
|
|
26
|
+
| { kind: "binary"; description: string };
|
|
27
27
|
|
|
28
28
|
export type LFile =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
| { kind: "directory" }
|
|
30
|
+
| { kind: "image"; mimeType: string }
|
|
31
|
+
| { kind: "text"; text: string; hadUtf8DecodeErrors?: true }
|
|
32
|
+
| { kind: "binary"; description: string };
|
|
33
33
|
|
|
34
34
|
function hasNull(buffer: Uint8Array): boolean {
|
|
35
|
-
|
|
35
|
+
return buffer.includes(0);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export async function loadFileKindAndText(
|
|
39
|
-
|
|
39
|
+
filePath: string,
|
|
40
40
|
): Promise<LFile> {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
41
|
+
const pathStat = await fsStat(filePath);
|
|
42
|
+
if (pathStat.isDirectory()) {
|
|
43
|
+
return { kind: "directory" };
|
|
44
|
+
}
|
|
45
|
+
if (!pathStat.isFile()) {
|
|
46
|
+
return {
|
|
47
|
+
kind: "binary",
|
|
48
|
+
description: "unsupported file type",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (pathStat.size > MAX_BYTES) {
|
|
52
|
+
return {
|
|
53
|
+
kind: "binary",
|
|
54
|
+
description: `file exceeds ${MAX_BYTES} byte limit`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const fileHandle = await fsOpen(filePath, "r");
|
|
59
|
+
try {
|
|
60
|
+
const buffer = Buffer.alloc(SNIFF_BYTES);
|
|
61
|
+
const { bytesRead } = await fileHandle.read(
|
|
62
|
+
buffer,
|
|
63
|
+
0,
|
|
64
|
+
SNIFF_BYTES,
|
|
65
|
+
0,
|
|
66
|
+
);
|
|
67
|
+
if (bytesRead === 0) {
|
|
68
|
+
return { kind: "text", text: "" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sample = buffer.subarray(0, bytesRead);
|
|
72
|
+
const detectedMimeType = (await fileTypeFromBuffer(sample))?.mime;
|
|
73
|
+
if (
|
|
74
|
+
detectedMimeType !== undefined &&
|
|
75
|
+
!isTextType(detectedMimeType)
|
|
76
|
+
) {
|
|
77
|
+
if (IMG_TYPES.has(detectedMimeType)) {
|
|
78
|
+
return { kind: "image", mimeType: detectedMimeType };
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
kind: "binary",
|
|
82
|
+
description: detectedMimeType,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (hasNull(sample)) {
|
|
86
|
+
return {
|
|
87
|
+
kind: "binary",
|
|
88
|
+
description: "null bytes detected",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const decoder = new TextDecoder("utf-8");
|
|
93
|
+
const fatalDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
94
|
+
let hadUtf8DecodeErrors = false;
|
|
95
|
+
const noteUtf8Err = (chunk?: Uint8Array): void => {
|
|
96
|
+
if (hadUtf8DecodeErrors) return;
|
|
97
|
+
try {
|
|
98
|
+
fatalDecoder.decode(chunk, { stream: chunk !== undefined });
|
|
99
|
+
} catch (error: unknown) {
|
|
100
|
+
if (error instanceof TypeError) {
|
|
101
|
+
hadUtf8DecodeErrors = true;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
noteUtf8Err(sample);
|
|
109
|
+
const parts: string[] = [decoder.decode(sample, { stream: true })];
|
|
110
|
+
|
|
111
|
+
let position = bytesRead;
|
|
112
|
+
while (true) {
|
|
113
|
+
const { bytesRead: chunkBytesRead } = await fileHandle.read(
|
|
114
|
+
buffer,
|
|
115
|
+
0,
|
|
116
|
+
SNIFF_BYTES,
|
|
117
|
+
position,
|
|
118
|
+
);
|
|
119
|
+
if (chunkBytesRead === 0) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const chunk = buffer.subarray(0, chunkBytesRead);
|
|
124
|
+
if (hasNull(chunk)) {
|
|
125
|
+
return {
|
|
126
|
+
kind: "binary",
|
|
127
|
+
description: "null bytes detected",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
noteUtf8Err(chunk);
|
|
131
|
+
parts.push(decoder.decode(chunk, { stream: true }));
|
|
132
|
+
position += chunkBytesRead;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
noteUtf8Err();
|
|
136
|
+
parts.push(decoder.decode());
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
kind: "text",
|
|
140
|
+
text: parts.join(""),
|
|
141
|
+
...(hadUtf8DecodeErrors ? { hadUtf8DecodeErrors: true as const } : {}),
|
|
142
|
+
};
|
|
143
|
+
} finally {
|
|
144
|
+
await fileHandle.close();
|
|
145
|
+
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
export async function classifyFileKind(filePath: string): Promise<FKind> {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
149
|
+
const loaded = await loadFileKindAndText(filePath);
|
|
150
|
+
switch (loaded.kind) {
|
|
151
|
+
case "directory":
|
|
152
|
+
return loaded;
|
|
153
|
+
case "image":
|
|
154
|
+
return loaded;
|
|
155
|
+
case "binary":
|
|
156
|
+
return loaded;
|
|
157
|
+
case "text":
|
|
158
|
+
return { kind: "text" };
|
|
159
|
+
}
|
|
160
160
|
}
|
package/src/replace-normalize.ts
CHANGED
|
@@ -12,6 +12,22 @@ function coerceEditsArray(edits: unknown): unknown {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function coerceNewLines(edits: unknown): unknown {
|
|
16
|
+
if (!Array.isArray(edits)) return edits;
|
|
17
|
+
return edits.map((edit: unknown) => {
|
|
18
|
+
if (!isRec(edit)) return edit;
|
|
19
|
+
if (typeof edit.new_lines !== "string") return edit;
|
|
20
|
+
try {
|
|
21
|
+
const parsed: unknown = JSON.parse(edit.new_lines);
|
|
22
|
+
if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
|
|
23
|
+
return { ...edit, new_lines: parsed };
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// not valid JSON, leave as-is for downstream validation
|
|
27
|
+
}
|
|
28
|
+
return edit;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
15
31
|
|
|
16
32
|
export function normReq(input: unknown): unknown {
|
|
17
33
|
if (!isRec(input)) {
|
|
@@ -29,8 +45,8 @@ export function normReq(input: unknown): unknown {
|
|
|
29
45
|
|
|
30
46
|
if (hasEditsField) {
|
|
31
47
|
record.edits = coerceEditsArray(record.edits);
|
|
48
|
+
record.edits = coerceNewLines(record.edits);
|
|
32
49
|
}
|
|
33
50
|
|
|
34
|
-
|
|
35
51
|
return record;
|
|
36
|
-
}
|
|
52
|
+
}
|