@oh-my-pi/pi-utils 18.7.0 → 18.8.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 +16 -0
- package/dist/types/ar/index.d.ts +1 -0
- package/dist/types/ar/zip-package.d.ts +21 -0
- 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/marked/core.d.ts +7 -6
- package/dist/types/marked-list.d.ts +38 -0
- package/dist/types/math-delimiters.d.ts +10 -0
- package/dist/types/readability/types.d.ts +2 -0
- package/dist/types/turndown/types.d.ts +7 -0
- package/dist/types/vterm/buffer.d.ts +16 -5
- package/package.json +2 -2
- package/src/ar/index.ts +1 -0
- package/src/ar/zip-package.ts +57 -0
- package/src/chalk.ts +66 -23
- 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/json-parse.ts +21 -6
- package/src/logger/rotating-file.ts +5 -3
- package/src/logger.ts +39 -23
- package/src/marked/core.ts +42 -18
- package/src/marked-list.ts +73 -0
- package/src/math-delimiters.ts +16 -0
- 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/stream.ts +34 -13
- package/src/temp.ts +1 -10
- package/src/template.ts +7 -4
- 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/terminal.ts +185 -92
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. */
|