@linxiraos/pi-utils 1.1.26 → 1.2.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 +6 -4
- package/README.md +1 -1
- package/dist/types/ar/index.d.ts +1 -0
- package/dist/types/ar/zip-package.d.ts +21 -0
- package/dist/types/color.d.ts +2 -0
- package/dist/types/dirs.d.ts +37 -12
- package/dist/types/docx/converter.d.ts +2 -0
- package/dist/types/dom/core.d.ts +15 -4
- package/dist/types/dom/parser.d.ts +5 -3
- package/dist/types/dom/selector.d.ts +7 -1
- package/dist/types/logger/rotating-file.d.ts +4 -2
- package/dist/types/loop-phase.d.ts +12 -7
- package/dist/types/marked/core.d.ts +7 -6
- package/dist/types/marked-list.d.ts +38 -0
- package/dist/types/math-delimiters.d.ts +48 -0
- package/dist/types/procmgr.d.ts +14 -20
- package/dist/types/readability/types.d.ts +2 -0
- package/dist/types/temp.d.ts +7 -0
- package/dist/types/turndown/types.d.ts +7 -0
- package/dist/types/vterm/buffer.d.ts +16 -5
- package/dist/types/vterm/query-responder.d.ts +15 -0
- package/package.json +2 -2
- package/src/ar/index.ts +1 -0
- package/src/ar/open.ts +29 -8
- package/src/ar/zip-package.ts +57 -0
- package/src/chalk.ts +66 -23
- package/src/color.ts +6 -0
- package/src/dirs.ts +93 -24
- package/src/docx/converter.ts +26 -12
- package/src/dom/core.ts +354 -111
- package/src/dom/parser.ts +42 -19
- package/src/dom/selector.ts +328 -95
- package/src/fetch-retry.ts +2 -0
- package/src/format.ts +5 -1
- package/src/json-lexer.ts +54 -0
- package/src/json-parse.ts +21 -6
- package/src/logger/rotating-file.ts +5 -3
- package/src/logger.ts +39 -23
- package/src/loop-phase.ts +66 -21
- package/src/marked/core.ts +45 -19
- package/src/marked-list.ts +73 -0
- package/src/math-delimiters.ts +125 -5
- package/src/module-timer.ts +5 -5
- package/src/postmortem.ts +1 -1
- package/src/process-name.ts +2 -2
- package/src/procmgr.ts +58 -4
- package/src/prompt.ts +3 -1
- package/src/ptree.ts +28 -35
- package/src/readability/readability.ts +349 -39
- package/src/readability/types.ts +2 -0
- package/src/runtime-install.ts +2 -2
- package/src/sqlite.ts +58 -1
- package/src/stream.ts +34 -13
- package/src/temp.ts +13 -0
- package/src/template.ts +7 -4
- package/src/timing-buffer.ts +1 -1
- package/src/tls-fetch.ts +1 -1
- package/src/turndown/gfm.ts +16 -5
- package/src/turndown/html.ts +28 -8
- package/src/turndown/service.ts +43 -15
- package/src/turndown/types.ts +7 -0
- package/src/vterm/buffer.ts +31 -11
- package/src/vterm/query-responder.ts +31 -2
- package/src/vterm/terminal.ts +213 -100
package/src/stream.ts
CHANGED
|
@@ -6,6 +6,10 @@ import { parseStreamingJson } from "./json-parse";
|
|
|
6
6
|
const LF = 0x0a;
|
|
7
7
|
const CR = 0x0d;
|
|
8
8
|
|
|
9
|
+
// Capacity above this is released once the buffered bytes no longer need it,
|
|
10
|
+
// so one oversized frame does not pin its peak buffer for the stream's life.
|
|
11
|
+
const CONCAT_SINK_RETAIN_BYTES = 1024 * 1024;
|
|
12
|
+
|
|
9
13
|
/**
|
|
10
14
|
* Split a byte stream on LF boundaries.
|
|
11
15
|
*
|
|
@@ -89,6 +93,21 @@ export class ConcatSink {
|
|
|
89
93
|
return next;
|
|
90
94
|
}
|
|
91
95
|
|
|
96
|
+
/** Drop or right-size an oversized buffer after the buffered length shrank. */
|
|
97
|
+
#releaseCapacity(): void {
|
|
98
|
+
const space = this.#space;
|
|
99
|
+
if (!space || space.length <= CONCAT_SINK_RETAIN_BYTES) return;
|
|
100
|
+
const length = this.#length;
|
|
101
|
+
if (length === 0) {
|
|
102
|
+
this.#space = undefined;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (length * 4 > space.length) return;
|
|
106
|
+
const next = Buffer.allocUnsafe(length);
|
|
107
|
+
space.copy(next, 0, 0, length);
|
|
108
|
+
this.#space = next;
|
|
109
|
+
}
|
|
110
|
+
|
|
92
111
|
append(chunk: Uint8Array) {
|
|
93
112
|
const n = chunk.length;
|
|
94
113
|
if (!n) return;
|
|
@@ -99,11 +118,10 @@ export class ConcatSink {
|
|
|
99
118
|
}
|
|
100
119
|
|
|
101
120
|
reset(chunk: Uint8Array) {
|
|
121
|
+
this.#length = 0;
|
|
122
|
+
this.#releaseCapacity();
|
|
102
123
|
const n = chunk.length;
|
|
103
|
-
if (!n)
|
|
104
|
-
this.#length = 0;
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
124
|
+
if (!n) return;
|
|
107
125
|
const space = this.#ensureCapacity(n);
|
|
108
126
|
space.set(chunk, 0);
|
|
109
127
|
this.#length = n;
|
|
@@ -127,14 +145,16 @@ export class ConcatSink {
|
|
|
127
145
|
if (count <= 0) return;
|
|
128
146
|
if (count >= this.#length) {
|
|
129
147
|
this.#length = 0;
|
|
130
|
-
|
|
148
|
+
} else {
|
|
149
|
+
this.#space!.copyWithin(0, count, this.#length);
|
|
150
|
+
this.#length -= count;
|
|
131
151
|
}
|
|
132
|
-
this.#
|
|
133
|
-
this.#length -= count;
|
|
152
|
+
this.#releaseCapacity();
|
|
134
153
|
}
|
|
135
154
|
|
|
136
155
|
clear() {
|
|
137
156
|
this.#length = 0;
|
|
157
|
+
this.#releaseCapacity();
|
|
138
158
|
}
|
|
139
159
|
|
|
140
160
|
/**
|
|
@@ -229,13 +249,14 @@ export class ConcatSink {
|
|
|
229
249
|
if (error) throw error;
|
|
230
250
|
if (done) {
|
|
231
251
|
this.#length = 0;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
252
|
+
} else {
|
|
253
|
+
const rem = total - read;
|
|
254
|
+
if (rem < total) {
|
|
255
|
+
space.copyWithin(0, read, total);
|
|
256
|
+
}
|
|
257
|
+
this.#length = rem;
|
|
237
258
|
}
|
|
238
|
-
this.#
|
|
259
|
+
this.#releaseCapacity();
|
|
239
260
|
}
|
|
240
261
|
}
|
|
241
262
|
|
package/src/temp.ts
CHANGED
|
@@ -3,6 +3,13 @@ import * as fsPromises from "node:fs/promises";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Owned temporary directory, removed via `remove()` or `using`/`await using`.
|
|
8
|
+
*
|
|
9
|
+
* Prefixes follow `mkdtemp`: `"@name-"` creates under the OS temp dir, any other
|
|
10
|
+
* prefix is a path (a bare `"name-"` lands in the process cwd), and no prefix
|
|
11
|
+
* means `"@pi-temp-"`.
|
|
12
|
+
*/
|
|
6
13
|
export class TempDir {
|
|
7
14
|
#path: string;
|
|
8
15
|
private constructor(path: string) {
|
|
@@ -96,6 +103,11 @@ export async function removeWithRetries(target: string): Promise<void> {
|
|
|
96
103
|
return;
|
|
97
104
|
} catch (err) {
|
|
98
105
|
if (!shouldRetryRemove(err, attempt)) throw err;
|
|
106
|
+
// bun on Windows finalizes SQLite's db/-wal/-shm file and directory
|
|
107
|
+
// handles on GC, so a closed database can still block deletion for
|
|
108
|
+
// seconds. One forced major collection on the first retry releases
|
|
109
|
+
// them immediately instead of burning the retry window.
|
|
110
|
+
if (attempt === 0) Bun.gc(true);
|
|
99
111
|
await Bun.sleep(kRemoveRetryDelayMs);
|
|
100
112
|
}
|
|
101
113
|
}
|
|
@@ -108,6 +120,7 @@ export function removeSyncWithRetries(target: string): void {
|
|
|
108
120
|
return;
|
|
109
121
|
} catch (err) {
|
|
110
122
|
if (!shouldRetryRemove(err, attempt)) throw err;
|
|
123
|
+
if (attempt === 0) Bun.gc(true);
|
|
111
124
|
sleepSync(kRemoveRetryDelayMs);
|
|
112
125
|
}
|
|
113
126
|
}
|
package/src/template.ts
CHANGED
|
@@ -226,7 +226,9 @@ function findTagEnd(source: string, start: number, triple: boolean): number {
|
|
|
226
226
|
|
|
227
227
|
function parseTemplate(source: string): Node[] {
|
|
228
228
|
const root: Node[] = [];
|
|
229
|
-
|
|
229
|
+
// `chained` marks a block opened by `{{else name …}}`: it has no closing tag of
|
|
230
|
+
// its own and closes with the block it chains from, as in Handlebars.
|
|
231
|
+
const stack: { node: BlockNode; target: Node[]; inverted: boolean; chained: boolean }[] = [];
|
|
230
232
|
let target = root;
|
|
231
233
|
let cursor = 0;
|
|
232
234
|
while (cursor < source.length) {
|
|
@@ -263,7 +265,7 @@ function parseTemplate(source: string): Node[] {
|
|
|
263
265
|
};
|
|
264
266
|
target.push(nested);
|
|
265
267
|
target = nested.body;
|
|
266
|
-
stack.push({ node: nested, target: parentTarget, inverted: false });
|
|
268
|
+
stack.push({ node: nested, target: parentTarget, inverted: false, chained: true });
|
|
267
269
|
}
|
|
268
270
|
continue;
|
|
269
271
|
}
|
|
@@ -277,12 +279,13 @@ function parseTemplate(source: string): Node[] {
|
|
|
277
279
|
inverted,
|
|
278
280
|
};
|
|
279
281
|
target.push(node);
|
|
280
|
-
stack.push({ node, target, inverted });
|
|
282
|
+
stack.push({ node, target, inverted, chained: false });
|
|
281
283
|
target = node.body;
|
|
282
284
|
continue;
|
|
283
285
|
}
|
|
284
286
|
if (raw.startsWith("/")) {
|
|
285
|
-
|
|
287
|
+
let current = stack.pop();
|
|
288
|
+
while (current?.chained) current = stack.pop();
|
|
286
289
|
if (!current || current.node.expression.name !== raw.slice(1).trim())
|
|
287
290
|
throw new Error(`Parse error: mismatched ${raw}`);
|
|
288
291
|
if (current.inverted) [current.node.body, current.node.inverse] = [current.node.inverse, current.node.body];
|
package/src/timing-buffer.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface ModuleLoadEvent {
|
|
|
22
22
|
* Registry-global key under which the preload accumulates module-load events.
|
|
23
23
|
* `Symbol.for` so both modules resolve the same symbol independently.
|
|
24
24
|
*/
|
|
25
|
-
const KEY: symbol = Symbol.for("
|
|
25
|
+
const KEY: symbol = Symbol.for("zeta.moduleLoadBuffer");
|
|
26
26
|
|
|
27
27
|
type Store = Record<symbol, ModuleLoadEvent[] | undefined>;
|
|
28
28
|
|
package/src/tls-fetch.ts
CHANGED
|
@@ -52,7 +52,7 @@ type BunTlsOptions = {
|
|
|
52
52
|
|
|
53
53
|
type BunTlsRequestInit = RequestInit & { tls?: BunTlsOptions };
|
|
54
54
|
|
|
55
|
-
const EXTRA_CA_FETCH_MARKER = Symbol("
|
|
55
|
+
const EXTRA_CA_FETCH_MARKER = Symbol("zeta.extraCaFetch");
|
|
56
56
|
type ExtraCaFetch = FetchImpl & { [EXTRA_CA_FETCH_MARKER]?: true };
|
|
57
57
|
|
|
58
58
|
/**
|
package/src/turndown/gfm.ts
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import type { TurndownNode, TurndownPlugin, TurndownServiceLike } from "./types";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/** Elements named `name` below `node` in document order, not descending into nested tables. */
|
|
4
|
+
function descendantElements(node: TurndownNode, name: string, limit = Number.POSITIVE_INFINITY): TurndownNode[] {
|
|
4
5
|
const matches: TurndownNode[] = [];
|
|
5
|
-
|
|
6
|
+
const stack: TurndownNode[] = [];
|
|
7
|
+
const pushChildren = (parent: TurndownNode): void => {
|
|
8
|
+
const children = parent.children;
|
|
9
|
+
for (let index = children.length - 1; index >= 0; index--) stack.push(children[index]!);
|
|
10
|
+
};
|
|
11
|
+
pushChildren(node);
|
|
12
|
+
while (stack.length > 0 && matches.length < limit) {
|
|
13
|
+
const child = stack.pop()!;
|
|
6
14
|
if (child.nodeName === name) matches.push(child);
|
|
7
|
-
if (child.nodeName !== "TABLE")
|
|
15
|
+
if (child.nodeName !== "TABLE") pushChildren(child);
|
|
8
16
|
}
|
|
9
17
|
return matches;
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
function isHeadingTable(node: TurndownNode): boolean {
|
|
13
21
|
if (node.nodeName !== "TABLE") return false;
|
|
14
|
-
const firstRow = descendantElements(node, "TR")[0];
|
|
22
|
+
const firstRow = descendantElements(node, "TR", 1)[0];
|
|
15
23
|
if (!firstRow) return false;
|
|
16
24
|
const cells = Array.from(firstRow.children);
|
|
17
25
|
return cells.length > 0 && cells.every(cell => cell.nodeName === "TH");
|
|
@@ -50,12 +58,13 @@ function renderTable(service: TurndownServiceLike, table: TurndownNode): string
|
|
|
50
58
|
/** Install GitHub fenced code blocks selected by `highlight-source-*` wrappers. */
|
|
51
59
|
export const highlightedCodeBlock: TurndownPlugin = service => {
|
|
52
60
|
service.addRule("highlightedCodeBlock", {
|
|
61
|
+
skipContent: true,
|
|
53
62
|
filter(node) {
|
|
54
63
|
return node.nodeName === "DIV" && /(?:^|\s)highlight-source-([^\s]+)/.test(node.getAttribute("class") ?? "");
|
|
55
64
|
},
|
|
56
65
|
replacement(_content, node, options) {
|
|
57
66
|
const language = /(?:^|\s)highlight-source-([^\s]+)/.exec(node.getAttribute("class") ?? "")?.[1] ?? "";
|
|
58
|
-
const text = descendantElements(node, "PRE")[0]?.textContent ?? "";
|
|
67
|
+
const text = descendantElements(node, "PRE", 1)[0]?.textContent ?? "";
|
|
59
68
|
return `\n\n${options.fence}${language}\n${text}\n${options.fence}\n\n`;
|
|
60
69
|
},
|
|
61
70
|
});
|
|
@@ -74,6 +83,7 @@ export const strikethrough: TurndownPlugin = service => {
|
|
|
74
83
|
/** Install GFM task-list checkbox conversion. */
|
|
75
84
|
export const taskListItems: TurndownPlugin = service => {
|
|
76
85
|
service.addRule("taskListItems", {
|
|
86
|
+
skipContent: true,
|
|
77
87
|
filter(node) {
|
|
78
88
|
return (
|
|
79
89
|
node.nodeName === "INPUT" &&
|
|
@@ -90,6 +100,7 @@ export const taskListItems: TurndownPlugin = service => {
|
|
|
90
100
|
/** Install GFM table conversion for tables with a heading row. */
|
|
91
101
|
export const tables: TurndownPlugin = service => {
|
|
92
102
|
service.addRule("table", {
|
|
103
|
+
skipContent: true,
|
|
93
104
|
filter: isHeadingTable,
|
|
94
105
|
replacement(_content, node) {
|
|
95
106
|
return renderTable(service, node);
|
package/src/turndown/html.ts
CHANGED
|
@@ -56,11 +56,16 @@ function escapeAttribute(value: string): string {
|
|
|
56
56
|
abstract class HtmlNode implements TurndownNode {
|
|
57
57
|
parentNode: HtmlNode | null = null;
|
|
58
58
|
readonly childNodes: HtmlNode[] = [];
|
|
59
|
+
/** Position in the parent's `childNodes`; assigned on append (the tree is immutable after parse). */
|
|
60
|
+
index = -1;
|
|
61
|
+
/** Position in the parent's `children`; -1 for non-element nodes. */
|
|
62
|
+
elementIndex = -1;
|
|
63
|
+
readonly #elements: HtmlNode[] = [];
|
|
59
64
|
abstract readonly nodeType: number;
|
|
60
65
|
abstract readonly nodeName: string;
|
|
61
66
|
|
|
62
|
-
get children(): HtmlNode[] {
|
|
63
|
-
return this
|
|
67
|
+
get children(): readonly HtmlNode[] {
|
|
68
|
+
return this.#elements;
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
get firstChild(): HtmlNode | null {
|
|
@@ -72,19 +77,29 @@ abstract class HtmlNode implements TurndownNode {
|
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
get previousSibling(): HtmlNode | null {
|
|
75
|
-
if (!this.parentNode) return null;
|
|
76
|
-
|
|
77
|
-
return index > 0 ? (this.parentNode.childNodes[index - 1] ?? null) : null;
|
|
80
|
+
if (!this.parentNode || this.index <= 0) return null;
|
|
81
|
+
return this.parentNode.childNodes[this.index - 1] ?? null;
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
get nextSibling(): HtmlNode | null {
|
|
81
85
|
if (!this.parentNode) return null;
|
|
82
|
-
|
|
83
|
-
return index >= 0 ? (this.parentNode.childNodes[index + 1] ?? null) : null;
|
|
86
|
+
return this.parentNode.childNodes[this.index + 1] ?? null;
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
get textContent(): string {
|
|
87
|
-
|
|
90
|
+
const parts: string[] = [];
|
|
91
|
+
const stack: HtmlNode[] = [];
|
|
92
|
+
for (let index = this.childNodes.length - 1; index >= 0; index--) stack.push(this.childNodes[index]!);
|
|
93
|
+
while (stack.length > 0) {
|
|
94
|
+
const node = stack.pop()!;
|
|
95
|
+
if (node instanceof HtmlText) {
|
|
96
|
+
parts.push(node.value);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const childNodes = node.childNodes;
|
|
100
|
+
for (let index = childNodes.length - 1; index >= 0; index--) stack.push(childNodes[index]!);
|
|
101
|
+
}
|
|
102
|
+
return parts.join("");
|
|
88
103
|
}
|
|
89
104
|
|
|
90
105
|
abstract get outerHTML(): string;
|
|
@@ -99,7 +114,12 @@ abstract class HtmlNode implements TurndownNode {
|
|
|
99
114
|
|
|
100
115
|
append(child: HtmlNode): void {
|
|
101
116
|
child.parentNode = this;
|
|
117
|
+
child.index = this.childNodes.length;
|
|
102
118
|
this.childNodes.push(child);
|
|
119
|
+
if (child.nodeType === 1) {
|
|
120
|
+
child.elementIndex = this.#elements.length;
|
|
121
|
+
this.#elements.push(child);
|
|
122
|
+
}
|
|
103
123
|
}
|
|
104
124
|
}
|
|
105
125
|
|
package/src/turndown/service.ts
CHANGED
|
@@ -75,7 +75,8 @@ const NONBLANK_EMPTY_ELEMENTS: Readonly<Record<string, true>> = {
|
|
|
75
75
|
VIDEO: true,
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
/** `filter` is `rule.filter` with string tag names lowercased once at registration. */
|
|
79
|
+
type RuleEntry = { key: string; rule: TurndownRule; filter: RuleFilter };
|
|
79
80
|
type Reference = { destination: string };
|
|
80
81
|
|
|
81
82
|
const DEFAULT_OPTIONS: ResolvedTurndownOptions = {
|
|
@@ -91,11 +92,37 @@ const DEFAULT_OPTIONS: ResolvedTurndownOptions = {
|
|
|
91
92
|
preformattedCode: false,
|
|
92
93
|
};
|
|
93
94
|
|
|
94
|
-
function
|
|
95
|
+
function normalizeFilter(filter: RuleFilter): RuleFilter {
|
|
96
|
+
if (typeof filter === "function") return filter;
|
|
97
|
+
if (typeof filter === "string") return filter.toLowerCase();
|
|
98
|
+
return filter.map(tag => tag.toLowerCase());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function matchesFilter(
|
|
102
|
+
filter: RuleFilter,
|
|
103
|
+
node: TurndownNode,
|
|
104
|
+
name: string,
|
|
105
|
+
options: ResolvedTurndownOptions,
|
|
106
|
+
): boolean {
|
|
95
107
|
if (typeof filter === "function") return filter(node, options);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
108
|
+
if (typeof filter === "string") return name === filter;
|
|
109
|
+
return filter.includes(name);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Equivalent to `node.textContent.trim() !== ""` without building the subtree text. */
|
|
113
|
+
function hasNonWhitespaceText(node: TurndownNode): boolean {
|
|
114
|
+
const stack: TurndownNode[] = [node];
|
|
115
|
+
while (stack.length > 0) {
|
|
116
|
+
const current = stack.pop()!;
|
|
117
|
+
const type = current.nodeType;
|
|
118
|
+
if (type === 3 || type === 4) {
|
|
119
|
+
if (/\S/.test(current.textContent ?? "")) return true;
|
|
120
|
+
} else if (type === 1 || type === 9 || type === 11) {
|
|
121
|
+
const childNodes = current.childNodes;
|
|
122
|
+
for (let index = 0; index < childNodes.length; index++) stack.push(childNodes[index]!);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
99
126
|
}
|
|
100
127
|
|
|
101
128
|
function contentWithFlankingWhitespace(content: string, delimiter: string): string {
|
|
@@ -115,8 +142,8 @@ function listItemPrefix(node: TurndownNode, options: ResolvedTurndownOptions): s
|
|
|
115
142
|
const parent = node.parentNode;
|
|
116
143
|
if (parent?.nodeName !== "OL") return `${options.bulletListMarker} `;
|
|
117
144
|
const start = Number(parent.getAttribute("start") ?? "1");
|
|
118
|
-
const
|
|
119
|
-
return `${(Number.isFinite(start) ? start : 1) +
|
|
145
|
+
const index = node.elementIndex ?? Array.prototype.indexOf.call(parent.children, node);
|
|
146
|
+
return `${(Number.isFinite(start) ? start : 1) + index}. `;
|
|
120
147
|
}
|
|
121
148
|
function hasNonblankDescendant(node: TurndownNode): boolean {
|
|
122
149
|
for (const child of Array.from(node.children)) {
|
|
@@ -142,19 +169,19 @@ export default class TurndownService {
|
|
|
142
169
|
addRule(key: string, rule: TurndownRule): this {
|
|
143
170
|
const previous = this.#rules.findIndex(entry => entry.key === key);
|
|
144
171
|
if (previous >= 0) this.#rules.splice(previous, 1);
|
|
145
|
-
this.#rules.unshift({ key, rule });
|
|
172
|
+
this.#rules.unshift({ key, rule, filter: normalizeFilter(rule.filter) });
|
|
146
173
|
return this;
|
|
147
174
|
}
|
|
148
175
|
|
|
149
176
|
/** Preserve matching elements as HTML when no custom rule handles them. */
|
|
150
177
|
keep(filter: RuleFilter): this {
|
|
151
|
-
this.#keepFilters.unshift(filter);
|
|
178
|
+
this.#keepFilters.unshift(normalizeFilter(filter));
|
|
152
179
|
return this;
|
|
153
180
|
}
|
|
154
181
|
|
|
155
182
|
/** Drop matching elements and all of their converted content. */
|
|
156
183
|
remove(filter: RuleFilter): this {
|
|
157
|
-
this.#removeFilters.unshift(filter);
|
|
184
|
+
this.#removeFilters.unshift(normalizeFilter(filter));
|
|
158
185
|
return this;
|
|
159
186
|
}
|
|
160
187
|
|
|
@@ -216,23 +243,24 @@ export default class TurndownService {
|
|
|
216
243
|
return node.parentNode?.nodeName === "CODE" ? collapsed : this.escape(collapsed);
|
|
217
244
|
}
|
|
218
245
|
if (node.nodeType !== 1) return this.#convertChildren(node);
|
|
219
|
-
if (!(node
|
|
246
|
+
if (!hasNonWhitespaceText(node) && !NONBLANK_EMPTY_ELEMENTS[node.nodeName] && !hasNonblankDescendant(node)) {
|
|
220
247
|
return (
|
|
221
248
|
this.options.blankReplacement?.("", node, this.options) ?? (BLOCK_ELEMENTS[node.nodeName] ? "\n\n" : "")
|
|
222
249
|
);
|
|
223
250
|
}
|
|
224
251
|
|
|
252
|
+
const name = node.nodeName.toLowerCase();
|
|
253
|
+
const custom = this.#rules.find(entry => matchesFilter(entry.filter, node, name, this.options));
|
|
225
254
|
const block = Boolean(BLOCK_ELEMENTS[node.nodeName]);
|
|
226
255
|
if (block) this.#previousSourceWhitespace = false;
|
|
227
|
-
const content = this.#convertChildren(node);
|
|
256
|
+
const content = custom?.rule.skipContent ? "" : this.#convertChildren(node);
|
|
228
257
|
if (block) this.#previousSourceWhitespace = false;
|
|
229
258
|
if (node.nodeName === "CODE") this.#previousSourceWhitespace = false;
|
|
230
|
-
const custom = this.#rules.find(entry => matchesFilter(entry.rule.filter, node, this.options));
|
|
231
259
|
if (custom) return custom.rule.replacement(content, node, this.options);
|
|
232
|
-
if (this.#keepFilters.some(filter => matchesFilter(filter, node, this.options))) {
|
|
260
|
+
if (this.#keepFilters.some(filter => matchesFilter(filter, node, name, this.options))) {
|
|
233
261
|
return this.options.keepReplacement?.(content, node, this.options) ?? serializeNode(node);
|
|
234
262
|
}
|
|
235
|
-
if (this.#removeFilters.some(filter => matchesFilter(filter, node, this.options))) return "";
|
|
263
|
+
if (this.#removeFilters.some(filter => matchesFilter(filter, node, name, this.options))) return "";
|
|
236
264
|
return this.#defaultReplacement(content, node);
|
|
237
265
|
}
|
|
238
266
|
|
package/src/turndown/types.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface TurndownNode {
|
|
|
13
13
|
readonly previousSibling: TurndownNode | null;
|
|
14
14
|
readonly textContent: string | null;
|
|
15
15
|
readonly outerHTML?: string;
|
|
16
|
+
/** Position among the parent's element children, when the node implementation tracks it. */
|
|
17
|
+
readonly elementIndex?: number;
|
|
16
18
|
getAttribute(name: string): string | null;
|
|
17
19
|
hasAttribute(name: string): boolean;
|
|
18
20
|
}
|
|
@@ -64,6 +66,11 @@ export type ReplacementFunction = (content: string, node: TurndownNode, options:
|
|
|
64
66
|
export interface TurndownRule {
|
|
65
67
|
filter: RuleFilter;
|
|
66
68
|
replacement: ReplacementFunction;
|
|
69
|
+
/**
|
|
70
|
+
* Set when `replacement` ignores its `content` argument; the service then skips
|
|
71
|
+
* converting the node's children before calling it.
|
|
72
|
+
*/
|
|
73
|
+
skipContent?: boolean;
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
/** A plugin that installs one or more conversion rules. */
|
package/src/vterm/buffer.ts
CHANGED
|
@@ -13,11 +13,15 @@ export interface CellAttributes {
|
|
|
13
13
|
bg: number;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* A terminal grid cell. Cells written by the terminal are frozen and may be
|
|
18
|
+
* shared between grid positions (blank cells, repeated ASCII glyphs), so the
|
|
19
|
+
* type is readonly: replace a cell instead of mutating it.
|
|
20
|
+
*/
|
|
17
21
|
export interface CellData {
|
|
18
|
-
chars: string;
|
|
19
|
-
width: number;
|
|
20
|
-
attrs: CellAttributes
|
|
22
|
+
readonly chars: string;
|
|
23
|
+
readonly width: number;
|
|
24
|
+
readonly attrs: Readonly<CellAttributes>;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
/** Creates the default rendition. */
|
|
@@ -37,18 +41,33 @@ export function defaultAttributes(): CellAttributes {
|
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
44
|
+
/** Frozen default rendition shared by every unstyled cell. */
|
|
45
|
+
export const DEFAULT_CELL_ATTRIBUTES: Readonly<CellAttributes> = Object.freeze(defaultAttributes());
|
|
43
46
|
|
|
44
47
|
/** Creates an empty cell with the supplied rendition. */
|
|
45
48
|
export function blankCell(attrs: CellAttributes = defaultAttributes()): CellData {
|
|
46
|
-
return { chars: "", width: 1, attrs:
|
|
49
|
+
return { chars: "", width: 1, attrs: { ...attrs } };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const sharedBlankCells = new WeakMap<Readonly<CellAttributes>, CellData>();
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns the frozen empty cell shared by every blank position with `attrs`.
|
|
56
|
+
* Frozen renditions are interned; a mutable one is snapshotted first.
|
|
57
|
+
*/
|
|
58
|
+
export function sharedBlankCell(attrs: Readonly<CellAttributes> = DEFAULT_CELL_ATTRIBUTES): CellData {
|
|
59
|
+
if (!Object.isFrozen(attrs)) return Object.freeze({ chars: "", width: 1, attrs: Object.freeze({ ...attrs }) });
|
|
60
|
+
let cell = sharedBlankCells.get(attrs);
|
|
61
|
+
if (!cell) {
|
|
62
|
+
cell = Object.freeze({ chars: "", width: 1, attrs });
|
|
63
|
+
sharedBlankCells.set(attrs, cell);
|
|
64
|
+
}
|
|
65
|
+
return cell;
|
|
47
66
|
}
|
|
48
67
|
|
|
49
68
|
/** Proposed xterm-compatible cell readback object. */
|
|
50
69
|
export class BufferCell {
|
|
51
|
-
#cell: CellData =
|
|
70
|
+
#cell: CellData = sharedBlankCell();
|
|
52
71
|
|
|
53
72
|
/** Reuses this object for another grid cell. */
|
|
54
73
|
setFrom(cell: CellData): this {
|
|
@@ -137,8 +156,9 @@ export class BufferLine {
|
|
|
137
156
|
cells: CellData[];
|
|
138
157
|
isWrapped = false;
|
|
139
158
|
|
|
140
|
-
constructor(columns: number, attrs: CellAttributes =
|
|
141
|
-
|
|
159
|
+
constructor(columns: number, attrs: Readonly<CellAttributes> = DEFAULT_CELL_ATTRIBUTES) {
|
|
160
|
+
const blank = sharedBlankCell(attrs);
|
|
161
|
+
this.cells = Array.from({ length: columns }, () => blank);
|
|
142
162
|
}
|
|
143
163
|
|
|
144
164
|
/** Number of grid columns in the line. */
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for {@link TerminalQueryResponder}.
|
|
3
|
+
*/
|
|
4
|
+
export interface TerminalQueryResponderOptions {
|
|
5
|
+
/**
|
|
6
|
+
* The PTY host opens the session with its own cursor-position query
|
|
7
|
+
* (`CSI 6 n`), which the PTY layer has already answered — ConPTY's
|
|
8
|
+
* `PSEUDOCONSOLE_INHERIT_CURSOR` handshake. Leave that first cursor query
|
|
9
|
+
* unanswered: the host consumes exactly one report, so a second one would
|
|
10
|
+
* reach the program as input it never asked for. Every later cursor query
|
|
11
|
+
* is answered. Defaults to `false`.
|
|
12
|
+
*/
|
|
13
|
+
hostCursorHandshake?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
1
16
|
/**
|
|
2
17
|
* Answers terminal capability queries emitted by programs on a headless PTY.
|
|
3
18
|
*
|
|
@@ -17,6 +32,12 @@
|
|
|
17
32
|
export class TerminalQueryResponder {
|
|
18
33
|
/** Trailing bytes that may be the start of an unfinished query escape. */
|
|
19
34
|
#residual = "";
|
|
35
|
+
/** Whether the next cursor query is the host's already-answered session-start handshake. */
|
|
36
|
+
#hostCursorHandshakePending: boolean;
|
|
37
|
+
|
|
38
|
+
constructor(options: TerminalQueryResponderOptions = {}) {
|
|
39
|
+
this.#hostCursorHandshakePending = options.hostCursorHandshake ?? false;
|
|
40
|
+
}
|
|
20
41
|
|
|
21
42
|
/**
|
|
22
43
|
* Feed one raw PTY output chunk. Returns the reply bytes to write back into
|
|
@@ -29,7 +50,12 @@ export class TerminalQueryResponder {
|
|
|
29
50
|
QUERY.lastIndex = 0;
|
|
30
51
|
for (let match = QUERY.exec(buffer); match !== null; match = QUERY.exec(buffer)) {
|
|
31
52
|
lastEnd = match.index + match[0].length;
|
|
32
|
-
|
|
53
|
+
const reply = replyFor(match);
|
|
54
|
+
if (reply === CURSOR_POSITION_REPORT && this.#hostCursorHandshakePending) {
|
|
55
|
+
this.#hostCursorHandshakePending = false;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
replies += reply;
|
|
33
59
|
}
|
|
34
60
|
// Keep only a short unmatched trailing escape: a query split across
|
|
35
61
|
// chunks completes on the next feed, while a long tail is ordinary output
|
|
@@ -41,6 +67,9 @@ export class TerminalQueryResponder {
|
|
|
41
67
|
}
|
|
42
68
|
}
|
|
43
69
|
|
|
70
|
+
/** Cursor position report for the home position: there is no screen to locate the cursor on. */
|
|
71
|
+
const CURSOR_POSITION_REPORT = "\x1b[1;1R";
|
|
72
|
+
|
|
44
73
|
/** Longest query escape we answer, bounding the cross-chunk residual. */
|
|
45
74
|
const MAX_PARTIAL_QUERY = 32;
|
|
46
75
|
|
|
@@ -63,7 +92,7 @@ function replyFor(match: RegExpExecArray): string {
|
|
|
63
92
|
}
|
|
64
93
|
if (intermediate !== "") return ""; // private DSR forms (DECXCPR, appearance) stay unanswered
|
|
65
94
|
const selector = params.split(";", 1)[0];
|
|
66
|
-
if (selector === "6") return
|
|
95
|
+
if (selector === "6") return CURSOR_POSITION_REPORT;
|
|
67
96
|
if (selector === "5") return "\x1b[0n"; // device status: OK
|
|
68
97
|
return "";
|
|
69
98
|
}
|