@oh-my-pi/pi-utils 18.11.2 → 18.12.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/CHANGELOG.md +17 -0
- package/dist/types/sanitize-text.d.ts +11 -0
- package/package.json +2 -2
- package/src/ar/open.ts +2 -1
- package/src/dom/parser.ts +6 -1
- package/src/fetch-retry.ts +5 -1
- package/src/marked/core.ts +4 -1
- package/src/sanitize-text.ts +70 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.12.0] - 2026-10-17
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `sanitizeTextDeep()`, which applies `sanitizeText()` to every string in a JSON-shaped value without mutating it ([#15963](https://github.com/can1357/oh-my-pi/issues/15963))
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed Windows archive extraction overwriting existing files or following existing symlinks when copying a symlink entry without symlink privileges ([#15931](https://github.com/can1357/oh-my-pi/pull/15931) by [@jchanghong023](https://github.com/jchanghong023)).
|
|
14
|
+
|
|
15
|
+
## [18.11.3] - 2026-10-16
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Fixed Markdown fence info-string parsing to keep backtick-containing backtick openers as paragraph text while accepting backticks in tilde info strings and Unicode line separators in either fence's info string ([#15928](https://github.com/can1357/oh-my-pi/pull/15928) by [@jchanghong023](https://github.com/jchanghong023)).
|
|
20
|
+
- Fixed `parseHTML` treating `<link>` in RSS/Atom documents as an HTML void element, which dropped the URL text of RSS `<link>…</link>` ([#15954](https://github.com/can1357/oh-my-pi/issues/15954))
|
|
21
|
+
|
|
5
22
|
## [18.11.2] - 2026-10-16
|
|
6
23
|
|
|
7
24
|
### Added
|
|
@@ -12,6 +12,17 @@
|
|
|
12
12
|
* string after the control probe.
|
|
13
13
|
*/
|
|
14
14
|
export declare function sanitizeText(text: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Apply {@link sanitizeText} to every string value inside a JSON-shaped value.
|
|
17
|
+
* Only plain objects and arrays are walked; class instances, typed arrays, and
|
|
18
|
+
* cyclic back-references pass through untouched. Object keys are kept as-is.
|
|
19
|
+
*
|
|
20
|
+
* Returns the input reference when nothing changed, so reference-keyed caches
|
|
21
|
+
* stay warm; changed containers are copied and the input is never mutated.
|
|
22
|
+
* The iterative walk also handles arbitrarily deep tool-result `details`
|
|
23
|
+
* without exhausting the TUI's call stack.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sanitizeTextDeep(value: unknown): unknown;
|
|
15
26
|
/**
|
|
16
27
|
* Escape the three XML-significant characters (`&`, `<`, `>`) in text destined
|
|
17
28
|
* for an XML/markup element body. Allocation-conscious: returns the input
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-utils",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.12.0",
|
|
4
4
|
"description": "Shared utilities for pi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@oh-my-pi/pi-natives": "18.
|
|
60
|
+
"@oh-my-pi/pi-natives": "18.12.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/bun": "^1.3.14"
|
package/src/ar/open.ts
CHANGED
|
@@ -234,7 +234,8 @@ export async function extractArchive(
|
|
|
234
234
|
// privilege; skip the entry so the rest still extracts.
|
|
235
235
|
const extracted = await archive.readFile(link.path).catch(() => undefined);
|
|
236
236
|
if (!extracted) continue;
|
|
237
|
-
|
|
237
|
+
// Keep symlink creation's exclusive destination semantics.
|
|
238
|
+
await fs.writeFile(outputPath, extracted.bytes, { flag: "wx" });
|
|
238
239
|
const permissions = (extracted.mode ?? 0) & 0o777;
|
|
239
240
|
if (permissions) await fs.chmod(outputPath, permissions);
|
|
240
241
|
}
|
package/src/dom/parser.ts
CHANGED
|
@@ -366,7 +366,12 @@ export function parseInto(html: string, document: Document, root: Document | Doc
|
|
|
366
366
|
index = match ? closePattern.lastIndex : html.length;
|
|
367
367
|
continue;
|
|
368
368
|
}
|
|
369
|
-
|
|
369
|
+
// Feed documents follow XML rules: only an explicit `/>` closes immediately, so RSS `<link>url</link>`
|
|
370
|
+
// keeps its text instead of being treated as an HTML void element.
|
|
371
|
+
const isVoid = xmlLike
|
|
372
|
+
? parsed.selfClosing
|
|
373
|
+
: VOID_ELEMENTS[parsed.name] || (namespace === SVG_NAMESPACE && parsed.selfClosing);
|
|
374
|
+
if (!isVoid) {
|
|
370
375
|
stack.push(element);
|
|
371
376
|
parent = element;
|
|
372
377
|
}
|
package/src/fetch-retry.ts
CHANGED
|
@@ -416,7 +416,11 @@ function wrapNetworkError(error: unknown): Error {
|
|
|
416
416
|
return new Error("Request was aborted");
|
|
417
417
|
}
|
|
418
418
|
if (error.message === "fetch failed" && error.cause instanceof Error) {
|
|
419
|
-
|
|
419
|
+
// Keep the original rejection as `cause`: undici nests the errno under
|
|
420
|
+
// `fetch failed` (often through an AggregateError), and
|
|
421
|
+
// classifyNetworkCause (packages/ai/src/error/network-cause.ts) walks
|
|
422
|
+
// `cause`/`errors` off this error. Flattening to text alone loses it.
|
|
423
|
+
return new Error(`Network error: ${error.cause.message}`, { cause: error });
|
|
420
424
|
}
|
|
421
425
|
return error;
|
|
422
426
|
}
|
package/src/marked/core.ts
CHANGED
|
@@ -775,7 +775,10 @@ function splitTableRow(line: string): string[] {
|
|
|
775
775
|
}
|
|
776
776
|
|
|
777
777
|
function isFence(line: string): RegExpExecArray | null {
|
|
778
|
-
|
|
778
|
+
// Unicode line separators may occur in the info string.
|
|
779
|
+
const match = /^ {0,3}(`{3,}|~{3,})([^\n]*?)(?:\n|$)$/.exec(line);
|
|
780
|
+
// Backtick fences cannot contain backticks in their info string.
|
|
781
|
+
return match && match[1]!.startsWith("`") && match[2]!.includes("`") ? null : match;
|
|
779
782
|
}
|
|
780
783
|
function isHeading(line: string): boolean {
|
|
781
784
|
return /^ {0,3}#{1,6}(?:\s|$)/.test(line);
|
package/src/sanitize-text.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* string after the control probe.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { isRecord } from "./type-guards";
|
|
16
|
+
|
|
15
17
|
const ESC_CHAR = "\x1b";
|
|
16
18
|
|
|
17
19
|
// Well-formed strings only need control/ANSI detection: C0 (excl. \t \n),
|
|
@@ -37,6 +39,74 @@ function sanitizeWellFormedText(text: string): string {
|
|
|
37
39
|
return stripped.replace(CONTROL_RE, "");
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Apply {@link sanitizeText} to every string value inside a JSON-shaped value.
|
|
44
|
+
* Only plain objects and arrays are walked; class instances, typed arrays, and
|
|
45
|
+
* cyclic back-references pass through untouched. Object keys are kept as-is.
|
|
46
|
+
*
|
|
47
|
+
* Returns the input reference when nothing changed, so reference-keyed caches
|
|
48
|
+
* stay warm; changed containers are copied and the input is never mutated.
|
|
49
|
+
* The iterative walk also handles arbitrarily deep tool-result `details`
|
|
50
|
+
* without exhausting the TUI's call stack.
|
|
51
|
+
*/
|
|
52
|
+
export function sanitizeTextDeep(value: unknown): unknown {
|
|
53
|
+
if (typeof value === "string") return sanitizeText(value);
|
|
54
|
+
const root = sanitizerFrame(value);
|
|
55
|
+
if (!root) return value;
|
|
56
|
+
const stack: SanitizerFrame[] = [root];
|
|
57
|
+
const ancestors = new Set<object>([root.value]);
|
|
58
|
+
while (true) {
|
|
59
|
+
const frame = stack[stack.length - 1]!;
|
|
60
|
+
const length = frame.kind === "array" ? frame.value.length : frame.keys.length;
|
|
61
|
+
if (frame.index === length) {
|
|
62
|
+
const clean = frame.copy ?? frame.value;
|
|
63
|
+
ancestors.delete(frame.value);
|
|
64
|
+
stack.pop();
|
|
65
|
+
if (stack.length === 0) return clean;
|
|
66
|
+
acceptSanitizedValue(stack[stack.length - 1]!, clean);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const child = frame.kind === "array" ? frame.value[frame.index++] : frame.value[frame.keys[frame.index++]!];
|
|
70
|
+
if (typeof child === "string") {
|
|
71
|
+
acceptSanitizedValue(frame, sanitizeText(child));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (child === null || typeof child !== "object" || ancestors.has(child)) continue;
|
|
75
|
+
const next = sanitizerFrame(child);
|
|
76
|
+
if (!next) continue;
|
|
77
|
+
ancestors.add(child);
|
|
78
|
+
stack.push(next);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type SanitizerFrame =
|
|
83
|
+
| { kind: "array"; value: unknown[]; index: number; copy?: unknown[] }
|
|
84
|
+
| { kind: "record"; value: Record<string, unknown>; keys: string[]; index: number; copy?: Record<string, unknown> };
|
|
85
|
+
|
|
86
|
+
function sanitizerFrame(value: unknown): SanitizerFrame | undefined {
|
|
87
|
+
if (Array.isArray(value)) return { kind: "array", value, index: 0 };
|
|
88
|
+
if (!isRecord(value)) return undefined;
|
|
89
|
+
const proto: unknown = Object.getPrototypeOf(value);
|
|
90
|
+
if (proto !== Object.prototype && proto !== null) return undefined;
|
|
91
|
+
return { kind: "record", value, keys: Object.keys(value), index: 0 };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function acceptSanitizedValue(frame: SanitizerFrame, clean: unknown): void {
|
|
95
|
+
if (frame.kind === "array") {
|
|
96
|
+
const index = frame.index - 1;
|
|
97
|
+
if (clean !== frame.value[index]) (frame.copy ??= frame.value.slice())[index] = clean;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const key = frame.keys[frame.index - 1]!;
|
|
101
|
+
if (clean === frame.value[key]) return;
|
|
102
|
+
const copy = (frame.copy ??= { ...frame.value });
|
|
103
|
+
if (key === "__proto__") {
|
|
104
|
+
Object.defineProperty(copy, key, { value: clean, writable: true, enumerable: true, configurable: true });
|
|
105
|
+
} else {
|
|
106
|
+
copy[key] = clean;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
40
110
|
/**
|
|
41
111
|
* Escape the three XML-significant characters (`&`, `<`, `>`) in text destined
|
|
42
112
|
* for an XML/markup element body. Allocation-conscious: returns the input
|