@oh-my-pi/pi-utils 18.6.3 → 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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.8.0] - 2026-10-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `ZipPackage` to `@oh-my-pi/pi-utils/ar` for lazily reading ZIP-based document packages with a configurable total-inflation limit, plus `DocxImage.readBytes()` for accessing raw DOCX image data.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Improved DOCX conversion to inflate only the package contents it needs, reducing unnecessary work and memory use.
|
|
14
|
+
- Improved performance across HTML-to-Markdown conversion, Readability extraction, Markdown lexing, terminal emulation, terminal styling, streaming tool-argument parsing, and log writing. Large-page processing and terminal workloads now use substantially less time and memory.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Fixed memory growth in long-lived child processes, streaming readers, prompt template compilation, and retried HTTP requests by releasing buffers, cache entries, and discarded response bodies promptly.
|
|
19
|
+
- Fixed prompt templates rejecting `{{else if …}}` chains as unclosed blocks; a chain now closes with its opening block's single closing tag, as in Handlebars.
|
|
20
|
+
|
|
5
21
|
## [18.6.3] - 2026-10-06
|
|
6
22
|
|
|
7
23
|
### Breaking Changes
|
package/dist/types/ar/index.d.ts
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ArchiveReader } from "./reader.js";
|
|
2
|
+
/**
|
|
3
|
+
* A ZIP-based document package (OOXML, EPUB) whose members are inflated only
|
|
4
|
+
* when read. Lookups use exact member paths, so a converter sees the same
|
|
5
|
+
* members a fully materialized `path → bytes` map would expose. Each member's
|
|
6
|
+
* size counts once toward a cap on total inflated bytes, like a fully
|
|
7
|
+
* materialized archive, so a crafted package cannot amplify past the in-memory
|
|
8
|
+
* limit one member at a time.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ZipPackage {
|
|
11
|
+
#private;
|
|
12
|
+
constructor(archive: ArchiveReader, maxTotalBytes?: number);
|
|
13
|
+
/** Index a ZIP buffer without inflating any member. */
|
|
14
|
+
static open(bytes: Uint8Array, maxTotalBytes?: number): Promise<ZipPackage>;
|
|
15
|
+
/** Exact paths of every file member. */
|
|
16
|
+
get members(): ReadonlySet<string>;
|
|
17
|
+
/** Inflate one member, or `undefined` when absent. Throws once distinct inflated bytes exceed the cap. */
|
|
18
|
+
readBytes(memberPath: string): Promise<Uint8Array | undefined>;
|
|
19
|
+
/** Inflate one member as UTF-8 text, or `undefined` when absent. */
|
|
20
|
+
readText(memberPath: string): Promise<string | undefined>;
|
|
21
|
+
}
|
|
@@ -22,6 +22,8 @@ export interface DocxImage {
|
|
|
22
22
|
readonly altText: string;
|
|
23
23
|
/** Read the image payload using mammoth's used encoding surface. */
|
|
24
24
|
read(encoding: "base64"): Promise<string>;
|
|
25
|
+
/** Read the raw image bytes without a base64 round trip. */
|
|
26
|
+
readBytes(): Promise<Uint8Array>;
|
|
25
27
|
}
|
|
26
28
|
/** HTML attributes returned by a custom image converter. */
|
|
27
29
|
export type ImageAttributes = Readonly<Record<string, string>>;
|
package/dist/types/dom/core.d.ts
CHANGED
|
@@ -59,6 +59,7 @@ export declare class EventTarget {
|
|
|
59
59
|
}
|
|
60
60
|
/** Base class for the implemented DOM tree. */
|
|
61
61
|
export declare class Node extends EventTarget {
|
|
62
|
+
#private;
|
|
62
63
|
static readonly ELEMENT_NODE = NodeType.ELEMENT;
|
|
63
64
|
static readonly ATTRIBUTE_NODE = NodeType.ATTRIBUTE;
|
|
64
65
|
static readonly TEXT_NODE = NodeType.TEXT;
|
|
@@ -69,8 +70,12 @@ export declare class Node extends EventTarget {
|
|
|
69
70
|
readonly nodeName: string;
|
|
70
71
|
parentNode: Node | null;
|
|
71
72
|
ownerDocument: Document | null;
|
|
72
|
-
childNodes: Node[];
|
|
73
73
|
constructor(nodeType: number, nodeName: string, ownerDocument?: Document | null);
|
|
74
|
+
/** Child nodes in tree order. */
|
|
75
|
+
get childNodes(): Node[];
|
|
76
|
+
set childNodes(value: Node[]);
|
|
77
|
+
/** Whether this node has any children (without allocating a child list). */
|
|
78
|
+
hasChildNodes(): boolean;
|
|
74
79
|
/** First child node, if present. */
|
|
75
80
|
get firstChild(): Node | null;
|
|
76
81
|
/** Last child node, if present. */
|
|
@@ -196,14 +201,20 @@ export declare class CSSStyleDeclaration {
|
|
|
196
201
|
}
|
|
197
202
|
/** DOM element with attributes, selectors, and HTML serialization. */
|
|
198
203
|
export declare class Element extends Node {
|
|
204
|
+
#private;
|
|
199
205
|
readonly localName: string;
|
|
200
206
|
readonly tagName: string;
|
|
201
207
|
readonly namespaceURI: string | null;
|
|
202
208
|
readonly qualifiedName: string;
|
|
203
|
-
readonly attributes: NamedNodeMap;
|
|
204
|
-
readonly classList: DOMTokenList;
|
|
205
|
-
readonly style: CSSStyleDeclaration;
|
|
206
209
|
constructor(tagName: string, ownerDocument?: Document | null, namespaceURI?: string | null);
|
|
210
|
+
/** Attribute collection. */
|
|
211
|
+
get attributes(): NamedNodeMap;
|
|
212
|
+
/** Class token list over the `class` attribute. */
|
|
213
|
+
get classList(): DOMTokenList;
|
|
214
|
+
/** Inline style declaration. */
|
|
215
|
+
get style(): CSSStyleDeclaration;
|
|
216
|
+
/** Whether the element has any attribute objects (without allocating the collection). */
|
|
217
|
+
hasAttributes(): boolean;
|
|
207
218
|
/** Element children. */
|
|
208
219
|
get children(): Element[];
|
|
209
220
|
/** First element child. */
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { Document, type DocumentFragment } from "./core.js";
|
|
1
|
+
import { Document, type DocumentFragment, Element } from "./core.js";
|
|
2
2
|
/** Decode common named entities and all valid numeric character references. */
|
|
3
3
|
export declare function decodeEntities(value: string): string;
|
|
4
|
-
/** Parse markup
|
|
5
|
-
export declare function
|
|
4
|
+
/** Parse markup, appending the resulting nodes to `root` (a document, fragment, or element). */
|
|
5
|
+
export declare function parseInto(html: string, document: Document, root: Document | DocumentFragment | Element): void;
|
|
6
|
+
/** Parse markup into a document fragment. `_contextTag` is accepted for compatibility and ignored. */
|
|
7
|
+
export declare function parseFragment(html: string, document: Document, _contextTag?: string): DocumentFragment;
|
|
6
8
|
/** Parse markup directly into a document. */
|
|
7
9
|
export declare function parseDocument(html: string): Document;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { type Document, type DocumentFragment, Element } from "./core.js";
|
|
2
|
+
/** Whether a whitespace-separated class string contains a token, without splitting it. */
|
|
3
|
+
export declare function hasClassToken(className: string, token: string): boolean;
|
|
2
4
|
/** Whether an element matches a CSS selector list. */
|
|
3
5
|
export declare function matchesSelector(element: Element, selector: string): boolean;
|
|
6
|
+
/** Nearest inclusive ancestor of an element matching a CSS selector list. */
|
|
7
|
+
export declare function closestMatching(element: Element, selector: string): Element | null;
|
|
4
8
|
/** Query descendants of a node in document order. */
|
|
5
|
-
export declare function querySelectorAllFrom(root: Document | DocumentFragment | Element, selector: string,
|
|
9
|
+
export declare function querySelectorAllFrom(root: Document | DocumentFragment | Element, selector: string, _includeRoot: boolean): Element[];
|
|
10
|
+
/** First descendant of a node in document order matching a selector, stopping at the first match. */
|
|
11
|
+
export declare function querySelectorFrom(root: Document | DocumentFragment | Element, selector: string): Element | null;
|
|
@@ -47,9 +47,11 @@ export declare class RotatingFileSink {
|
|
|
47
47
|
/**
|
|
48
48
|
* Append one already-formatted log record. A batching sink buffers it unless
|
|
49
49
|
* `flushNow` is set or the buffer reached its size threshold; either way the
|
|
50
|
-
* whole buffer, this record included, is then written in order.
|
|
50
|
+
* whole buffer, this record included, is then written in order. `day` is the
|
|
51
|
+
* record's `localDay` key, so callers that already took the record's `Date`
|
|
52
|
+
* reuse it.
|
|
51
53
|
*/
|
|
52
|
-
write(line: string, flushNow?: boolean): void;
|
|
54
|
+
write(line: string, flushNow?: boolean, day?: string): void;
|
|
53
55
|
/** Write every buffered record to the active file now. */
|
|
54
56
|
flush(): void;
|
|
55
57
|
/** Write buffered records, then stop accepting records. */
|
|
@@ -185,14 +185,15 @@ export type TokensList = Token[] & {
|
|
|
185
185
|
export interface TokenizerThis {
|
|
186
186
|
lexer: Lexer;
|
|
187
187
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
188
|
+
* The whole source the lexer works through. The `src` a tokenizer receives is the part of it before `end` that
|
|
189
|
+
* starts at `end - src.length`. For a block tokenizer, `source` is the text passed to one `blockTokens` call and
|
|
190
|
+
* `end` its length. For an inline tokenizer, it is the inline source, such as a paragraph's text: inside a link
|
|
191
|
+
* label or the text of emphasis, `end` is where that text ends, right before its closer, and a token there must
|
|
192
|
+
* not reach past it. Tokenizers get one context object per source, the same for every call while the lexer works
|
|
193
|
+
* through it (and, inline, the labels and emphasis inside it), so a tokenizer can keep state for it in a `WeakMap`.
|
|
193
194
|
*/
|
|
194
195
|
source?: string;
|
|
195
|
-
/**
|
|
196
|
+
/** Where the text the lexer works through ends in `source` (see `source`). */
|
|
196
197
|
end?: number;
|
|
197
198
|
}
|
|
198
199
|
/** A tokenizer extension callback. */
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List-continuation grammar of `@oh-my-pi/pi-utils/marked`, for streaming
|
|
3
|
+
* renderers that freeze a lexed prefix at blank-line boundaries (TUI and
|
|
4
|
+
* collab web).
|
|
5
|
+
*
|
|
6
|
+
* marked's list tokenizer (Tokenizer.list, marked v18) continues a list across
|
|
7
|
+
* blank lines only when the remaining source matches
|
|
8
|
+
* `listItemRegex(marker)` = `^( {0,3}${marker})((?:[\t ][^\n]*)?(?:\n|$))`,
|
|
9
|
+
* where `marker` is the exact bullet char for unordered lists (`\${char}`) or
|
|
10
|
+
* 1-9 digits plus the exact delimiter for ordered lists (`\d{1,9}\${delim}`).
|
|
11
|
+
* The marker is derived from the list's FIRST item (`n = t[1].trim()`), which
|
|
12
|
+
* sits at the start of a top-level list token's raw.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Whether a same-marker item could still continue the list `listRaw` at
|
|
16
|
+
* `tailStart` (directly after the blank line ending the list), now or after
|
|
17
|
+
* any append to `text`.
|
|
18
|
+
*
|
|
19
|
+
* Streaming-freeze equivalence invariant: lex(prefix) ++ lex(tail) must equal
|
|
20
|
+
* lex(full text) for the current text and every append-only extension of it,
|
|
21
|
+
* because a frozen prefix is sticky. At a blank-line cut directly after a
|
|
22
|
+
* top-level `list` token, the only construct that can straddle the cut is a
|
|
23
|
+
* continuation item of that list: marked consumed the blank line into the last
|
|
24
|
+
* item's raw and re-ran `listItemRegex` at exactly `tailStart`, merging a
|
|
25
|
+
* same-marker item into one renumbered loose list. The cut is safe only when
|
|
26
|
+
* that regex can NEVER match at `tailStart`, no matter what is appended later.
|
|
27
|
+
*
|
|
28
|
+
* Append-only growth means "closed" may only be concluded from a present
|
|
29
|
+
* character that contradicts every possible continuation (tail "1x" can never
|
|
30
|
+
* grow into an ordered item, but tail "1" can become "1. c"). Running out of
|
|
31
|
+
* text mid-marker therefore answers "may continue".
|
|
32
|
+
*
|
|
33
|
+
* Returns true when the tail could still continue the list (or the list's
|
|
34
|
+
* marker is unrecognizable) — the conservative "don't freeze" answer. marked
|
|
35
|
+
* may break the list anyway when the matching line is also an hr (`- - -`);
|
|
36
|
+
* treating that as "may continue" merely skips a freeze, never corrupts one.
|
|
37
|
+
*/
|
|
38
|
+
export declare function listMayContinueAt(text: string, tailStart: number, listRaw: string): boolean;
|
|
@@ -80,6 +80,16 @@ export declare class MathBlockScan {
|
|
|
80
80
|
/** The own-line display block starting at `from`, or `undefined`. */
|
|
81
81
|
at(from: number): MathBlock | undefined;
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* The own-line display block at the start of `src`, for a marked block tokenizer: `src` is the part of
|
|
85
|
+
* `context.source` that starts at `context.end - src.length`. One {@link MathBlockScan} is kept per context, which
|
|
86
|
+
* marked keeps for one `blockTokens` source, so asking at every block start takes linear time even when an opener
|
|
87
|
+
* never closes. Without `context.source` it is {@link mathBlockAt}.
|
|
88
|
+
*/
|
|
89
|
+
export declare function mathBlockInContext(context: {
|
|
90
|
+
source?: string;
|
|
91
|
+
end?: number;
|
|
92
|
+
}, src: string): MathBlock | undefined;
|
|
83
93
|
/**
|
|
84
94
|
* The display opener alone on the line at `from` (up to 3 leading spaces,
|
|
85
95
|
* trailing spaces or tabs; the source's last line may be unterminated), or
|
|
@@ -13,6 +13,8 @@ export interface ReadabilityNode {
|
|
|
13
13
|
appendChild(node: ReadabilityNode): ReadabilityNode;
|
|
14
14
|
cloneNode(deep?: boolean): ReadabilityNode;
|
|
15
15
|
remove(): void;
|
|
16
|
+
/** Detach all children; used when present to move or reset children in one step. */
|
|
17
|
+
replaceChildren?(): void;
|
|
16
18
|
}
|
|
17
19
|
/** Minimal standards-shaped DOM element accepted by the readability extractor. */
|
|
18
20
|
export interface ReadabilityElement extends ReadabilityNode {
|
|
@@ -12,6 +12,8 @@ export interface TurndownNode {
|
|
|
12
12
|
readonly previousSibling: TurndownNode | null;
|
|
13
13
|
readonly textContent: string | null;
|
|
14
14
|
readonly outerHTML?: string;
|
|
15
|
+
/** Position among the parent's element children, when the node implementation tracks it. */
|
|
16
|
+
readonly elementIndex?: number;
|
|
15
17
|
getAttribute(name: string): string | null;
|
|
16
18
|
hasAttribute(name: string): boolean;
|
|
17
19
|
}
|
|
@@ -55,6 +57,11 @@ export type ReplacementFunction = (content: string, node: TurndownNode, options:
|
|
|
55
57
|
export interface TurndownRule {
|
|
56
58
|
filter: RuleFilter;
|
|
57
59
|
replacement: ReplacementFunction;
|
|
60
|
+
/**
|
|
61
|
+
* Set when `replacement` ignores its `content` argument; the service then skips
|
|
62
|
+
* converting the node's children before calling it.
|
|
63
|
+
*/
|
|
64
|
+
skipContent?: boolean;
|
|
58
65
|
}
|
|
59
66
|
/** A plugin that installs one or more conversion rules. */
|
|
60
67
|
export type TurndownPlugin = (service: TurndownServiceLike) => void;
|
|
@@ -12,16 +12,27 @@ export interface CellAttributes {
|
|
|
12
12
|
bgMode: 0 | 1 | 2;
|
|
13
13
|
bg: number;
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* A terminal grid cell. Cells written by the terminal are frozen and may be
|
|
17
|
+
* shared between grid positions (blank cells, repeated ASCII glyphs), so the
|
|
18
|
+
* type is readonly: replace a cell instead of mutating it.
|
|
19
|
+
*/
|
|
16
20
|
export interface CellData {
|
|
17
|
-
chars: string;
|
|
18
|
-
width: number;
|
|
19
|
-
attrs: CellAttributes
|
|
21
|
+
readonly chars: string;
|
|
22
|
+
readonly width: number;
|
|
23
|
+
readonly attrs: Readonly<CellAttributes>;
|
|
20
24
|
}
|
|
21
25
|
/** Creates the default rendition. */
|
|
22
26
|
export declare function defaultAttributes(): CellAttributes;
|
|
27
|
+
/** Frozen default rendition shared by every unstyled cell. */
|
|
28
|
+
export declare const DEFAULT_CELL_ATTRIBUTES: Readonly<CellAttributes>;
|
|
23
29
|
/** Creates an empty cell with the supplied rendition. */
|
|
24
30
|
export declare function blankCell(attrs?: CellAttributes): CellData;
|
|
31
|
+
/**
|
|
32
|
+
* Returns the frozen empty cell shared by every blank position with `attrs`.
|
|
33
|
+
* Frozen renditions are interned; a mutable one is snapshotted first.
|
|
34
|
+
*/
|
|
35
|
+
export declare function sharedBlankCell(attrs?: Readonly<CellAttributes>): CellData;
|
|
25
36
|
/** Proposed xterm-compatible cell readback object. */
|
|
26
37
|
export declare class BufferCell {
|
|
27
38
|
#private;
|
|
@@ -62,7 +73,7 @@ export declare class BufferCell {
|
|
|
62
73
|
export declare class BufferLine {
|
|
63
74
|
cells: CellData[];
|
|
64
75
|
isWrapped: boolean;
|
|
65
|
-
constructor(columns: number, attrs?: CellAttributes);
|
|
76
|
+
constructor(columns: number, attrs?: Readonly<CellAttributes>);
|
|
66
77
|
/** Number of grid columns in the line. */
|
|
67
78
|
get length(): number;
|
|
68
79
|
/** Reads a cell, optionally reusing the caller's object. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-utils",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.8.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.8.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/bun": "^1.3.14"
|
package/src/ar/index.ts
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { UTF8_DECODER } from "./bytes";
|
|
2
|
+
import { type ArchiveLimits, assertInMemorySize, DEFAULT_ARCHIVE_LIMITS } from "./limits";
|
|
3
|
+
import { openArchive } from "./open";
|
|
4
|
+
import type { ArchiveReader } from "./reader";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A ZIP-based document package (OOXML, EPUB) whose members are inflated only
|
|
8
|
+
* when read. Lookups use exact member paths, so a converter sees the same
|
|
9
|
+
* members a fully materialized `path → bytes` map would expose. Each member's
|
|
10
|
+
* size counts once toward a cap on total inflated bytes, like a fully
|
|
11
|
+
* materialized archive, so a crafted package cannot amplify past the in-memory
|
|
12
|
+
* limit one member at a time.
|
|
13
|
+
*/
|
|
14
|
+
export class ZipPackage {
|
|
15
|
+
#archive: ArchiveReader;
|
|
16
|
+
#members = new Set<string>();
|
|
17
|
+
#limits: ArchiveLimits;
|
|
18
|
+
/** Members already counted toward {@link #inflated}; re-reading one is free against the cap. */
|
|
19
|
+
#counted = new Set<string>();
|
|
20
|
+
#inflated = 0;
|
|
21
|
+
|
|
22
|
+
constructor(archive: ArchiveReader, maxTotalBytes = DEFAULT_ARCHIVE_LIMITS.maxInMemorySize) {
|
|
23
|
+
this.#archive = archive;
|
|
24
|
+
this.#limits = { ...DEFAULT_ARCHIVE_LIMITS, maxInMemorySize: maxTotalBytes };
|
|
25
|
+
for (const entry of archive.indexEntries()) {
|
|
26
|
+
if (!entry.isDirectory) this.#members.add(entry.path);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Index a ZIP buffer without inflating any member. */
|
|
31
|
+
static async open(bytes: Uint8Array, maxTotalBytes?: number): Promise<ZipPackage> {
|
|
32
|
+
return new ZipPackage(await openArchive({ bytes, format: "zip" }), maxTotalBytes);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Exact paths of every file member. */
|
|
36
|
+
get members(): ReadonlySet<string> {
|
|
37
|
+
return this.#members;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Inflate one member, or `undefined` when absent. Throws once distinct inflated bytes exceed the cap. */
|
|
41
|
+
async readBytes(memberPath: string): Promise<Uint8Array | undefined> {
|
|
42
|
+
if (!this.#members.has(memberPath)) return undefined;
|
|
43
|
+
const { bytes } = await this.#archive.readFile(memberPath);
|
|
44
|
+
if (!this.#counted.has(memberPath)) {
|
|
45
|
+
this.#counted.add(memberPath);
|
|
46
|
+
this.#inflated += bytes.length;
|
|
47
|
+
assertInMemorySize(this.#inflated, this.#limits);
|
|
48
|
+
}
|
|
49
|
+
return bytes;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Inflate one member as UTF-8 text, or `undefined` when absent. */
|
|
53
|
+
async readText(memberPath: string): Promise<string | undefined> {
|
|
54
|
+
const bytes = await this.readBytes(memberPath);
|
|
55
|
+
return bytes ? UTF8_DECODER.decode(bytes) : undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/chalk.ts
CHANGED
|
@@ -257,14 +257,74 @@ function applyStyles(input: string, styles: readonly Style[]): string {
|
|
|
257
257
|
for (let index = styles.length - 1; index >= 0; index--) {
|
|
258
258
|
const style = styles[index];
|
|
259
259
|
if (!style) continue;
|
|
260
|
-
output = output.split(style.close).join(style.close + style.open);
|
|
260
|
+
if (output.includes(style.close)) output = output.split(style.close).join(style.close + style.open);
|
|
261
261
|
opening = style.open + opening;
|
|
262
262
|
closing += style.close;
|
|
263
263
|
}
|
|
264
|
-
output = output.replace(/\r?\n/g, match => `${closing}${match}${opening}`);
|
|
264
|
+
if (output.includes("\n")) output = output.replace(/\r?\n/g, match => `${closing}${match}${opening}`);
|
|
265
265
|
return opening + output + closing;
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
const BUILDER_CONTEXT = Symbol("chalk.context");
|
|
269
|
+
const BUILDER_STYLES = Symbol("chalk.styles");
|
|
270
|
+
const BUILDER_HEX_CACHE = Symbol("chalk.hexCache");
|
|
271
|
+
// Bounds per-builder hex children when callers derive colors dynamically.
|
|
272
|
+
const MAX_CACHED_HEX_BUILDERS = 256;
|
|
273
|
+
|
|
274
|
+
interface BuilderState {
|
|
275
|
+
[BUILDER_CONTEXT]: ChalkContext;
|
|
276
|
+
[BUILDER_STYLES]: readonly Style[];
|
|
277
|
+
[BUILDER_HEX_CACHE]?: Map<string, ChalkInstance>;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
type Builder = ChalkInstance & BuilderState;
|
|
281
|
+
|
|
282
|
+
/** Child builders are created on first access and cached on the parent builder. */
|
|
283
|
+
function defineStyleGetter(proto: object, name: string, style: Style): void {
|
|
284
|
+
Object.defineProperty(proto, name, {
|
|
285
|
+
get(this: Builder) {
|
|
286
|
+
const child = createBuilder(this[BUILDER_CONTEXT], [...this[BUILDER_STYLES], style]);
|
|
287
|
+
Object.defineProperty(this, name, { value: child });
|
|
288
|
+
return child;
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const builderPrototype: object = Object.create(Function.prototype);
|
|
294
|
+
Object.defineProperty(builderPrototype, "level", {
|
|
295
|
+
get(this: Builder) {
|
|
296
|
+
return this[BUILDER_CONTEXT].level;
|
|
297
|
+
},
|
|
298
|
+
set(this: Builder, level: number) {
|
|
299
|
+
this[BUILDER_CONTEXT].level = level;
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
for (const name in STYLES) {
|
|
303
|
+
const style = STYLES[name];
|
|
304
|
+
if (style) defineStyleGetter(builderPrototype, name, style);
|
|
305
|
+
}
|
|
306
|
+
defineStyleGetter(builderPrototype, "grey", STYLES.gray!);
|
|
307
|
+
defineStyleGetter(builderPrototype, "bgGray", STYLES.bgBlackBright!);
|
|
308
|
+
defineStyleGetter(builderPrototype, "bgGrey", STYLES.bgBlackBright!);
|
|
309
|
+
Object.defineProperty(builderPrototype, "hex", {
|
|
310
|
+
value(this: Builder, color: string): ChalkInstance {
|
|
311
|
+
const context = this[BUILDER_CONTEXT];
|
|
312
|
+
const key = `${context.level}:${color}`;
|
|
313
|
+
let cache = this[BUILDER_HEX_CACHE];
|
|
314
|
+
const cached = cache?.get(key);
|
|
315
|
+
if (cached) return cached;
|
|
316
|
+
const child = createBuilder(context, [...this[BUILDER_STYLES], hexStyle(color, context.level)]);
|
|
317
|
+
if (!cache) {
|
|
318
|
+
cache = new Map();
|
|
319
|
+
this[BUILDER_HEX_CACHE] = cache;
|
|
320
|
+
} else if (cache.size >= MAX_CACHED_HEX_BUILDERS) {
|
|
321
|
+
cache.clear();
|
|
322
|
+
}
|
|
323
|
+
cache.set(key, child);
|
|
324
|
+
return child;
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
|
|
268
328
|
function createBuilder(context: ChalkContext, styles: readonly Style[]): ChalkInstance {
|
|
269
329
|
const builder = ((...values: unknown[]) => {
|
|
270
330
|
let text = "";
|
|
@@ -274,27 +334,10 @@ function createBuilder(context: ChalkContext, styles: readonly Style[]): ChalkIn
|
|
|
274
334
|
}
|
|
275
335
|
if (text.length === 0 || context.level === 0) return text;
|
|
276
336
|
return applyStyles(text, styles);
|
|
277
|
-
}) as
|
|
278
|
-
Object.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
context.level = level;
|
|
282
|
-
},
|
|
283
|
-
});
|
|
284
|
-
for (const name in STYLES) {
|
|
285
|
-
const style = STYLES[name];
|
|
286
|
-
if (style) Object.defineProperty(builder, name, { get: () => createBuilder(context, [...styles, style]) });
|
|
287
|
-
}
|
|
288
|
-
Object.defineProperty(builder, "grey", { get: () => createBuilder(context, [...styles, STYLES.gray!]) });
|
|
289
|
-
Object.defineProperty(builder, "bgGray", {
|
|
290
|
-
get: () => createBuilder(context, [...styles, STYLES.bgBlackBright!]),
|
|
291
|
-
});
|
|
292
|
-
Object.defineProperty(builder, "bgGrey", {
|
|
293
|
-
get: () => createBuilder(context, [...styles, STYLES.bgBlackBright!]),
|
|
294
|
-
});
|
|
295
|
-
Object.defineProperty(builder, "hex", {
|
|
296
|
-
value: (color: string) => createBuilder(context, [...styles, hexStyle(color, context.level)]),
|
|
297
|
-
});
|
|
337
|
+
}) as Builder;
|
|
338
|
+
Object.setPrototypeOf(builder, builderPrototype);
|
|
339
|
+
builder[BUILDER_CONTEXT] = context;
|
|
340
|
+
builder[BUILDER_STYLES] = styles;
|
|
298
341
|
return builder;
|
|
299
342
|
}
|
|
300
343
|
|
package/src/docx/converter.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { ZipPackage } from "../ar";
|
|
4
4
|
import { attribute, childElements, descendants, firstChild, localName, parseXml, type XmlElement } from "./xml";
|
|
5
5
|
|
|
6
6
|
/** A mammoth-compatible diagnostic emitted while converting a document. */
|
|
@@ -26,6 +26,8 @@ export interface DocxImage {
|
|
|
26
26
|
readonly altText: string;
|
|
27
27
|
/** Read the image payload using mammoth's used encoding surface. */
|
|
28
28
|
read(encoding: "base64"): Promise<string>;
|
|
29
|
+
/** Read the raw image bytes without a base64 round trip. */
|
|
30
|
+
readBytes(): Promise<Uint8Array>;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/** HTML attributes returned by a custom image converter. */
|
|
@@ -119,7 +121,8 @@ interface NumberingLevel {
|
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
interface ConversionContext {
|
|
122
|
-
|
|
124
|
+
/** Lazily inflated DOCX members, capped on total inflated bytes. */
|
|
125
|
+
readonly package: ZipPackage;
|
|
123
126
|
readonly relationships: ReadonlyMap<string, Relationship>;
|
|
124
127
|
readonly contentTypes: ReadonlyMap<string, string>;
|
|
125
128
|
readonly styles: ReadonlyMap<string, Style>;
|
|
@@ -373,8 +376,8 @@ async function renderImage(element: XmlElement, context: ConversionContext): Pro
|
|
|
373
376
|
const relationship = relationshipId ? context.relationships.get(relationshipId) : undefined;
|
|
374
377
|
if (!relationship || relationship.external) return "";
|
|
375
378
|
const memberPath = relationshipPath(relationship.target);
|
|
376
|
-
const
|
|
377
|
-
if (!
|
|
379
|
+
const docx = context.package;
|
|
380
|
+
if (!docx.members.has(memberPath)) {
|
|
378
381
|
context.messages.push({ type: "warning", message: `Could not find image ${memberPath}` });
|
|
379
382
|
return "";
|
|
380
383
|
}
|
|
@@ -391,8 +394,12 @@ async function renderImage(element: XmlElement, context: ConversionContext): Pro
|
|
|
391
394
|
altText,
|
|
392
395
|
async read(encoding: "base64"): Promise<string> {
|
|
393
396
|
if (encoding !== "base64") throw new Error(`Unsupported image encoding: ${encoding}`);
|
|
397
|
+
const bytes = await readImageMember(docx, memberPath);
|
|
394
398
|
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
|
|
395
399
|
},
|
|
400
|
+
readBytes(): Promise<Uint8Array> {
|
|
401
|
+
return readImageMember(docx, memberPath);
|
|
402
|
+
},
|
|
396
403
|
};
|
|
397
404
|
const converted = await context.convertImage.convert(image);
|
|
398
405
|
const attributes = Object.entries(converted)
|
|
@@ -403,6 +410,13 @@ async function renderImage(element: XmlElement, context: ConversionContext): Pro
|
|
|
403
410
|
return attributes ? `<img ${attributes} />` : "<img />";
|
|
404
411
|
}
|
|
405
412
|
|
|
413
|
+
/** Bytes of an image member already known to exist. */
|
|
414
|
+
async function readImageMember(docx: ZipPackage, memberPath: string): Promise<Uint8Array> {
|
|
415
|
+
const bytes = await docx.readBytes(memberPath);
|
|
416
|
+
if (!bytes) throw new Error(`Could not find image ${memberPath}`);
|
|
417
|
+
return bytes;
|
|
418
|
+
}
|
|
419
|
+
|
|
406
420
|
function renderFootnoteReference(element: XmlElement, context: ConversionContext): string {
|
|
407
421
|
const id = attribute(element, "w:id");
|
|
408
422
|
if (!id || !context.footnotes.has(id)) return "";
|
|
@@ -655,21 +669,21 @@ function defaultImageConverter(): ImageConverter {
|
|
|
655
669
|
/** Convert a DOCX buffer or path to mammoth-compatible HTML. */
|
|
656
670
|
export async function convertToHtml(input: DocxInput, options: ConvertToHtmlOptions = {}): Promise<DocxResult> {
|
|
657
671
|
const bytes = "buffer" in input && input.buffer ? input.buffer : await fs.readFile(input.path);
|
|
658
|
-
const
|
|
659
|
-
const documentXml =
|
|
672
|
+
const docx = await ZipPackage.open(bytes);
|
|
673
|
+
const documentXml = await docx.readText("word/document.xml");
|
|
660
674
|
if (!documentXml) throw new Error("Invalid DOCX: missing word/document.xml");
|
|
661
675
|
const context: ConversionContext = {
|
|
662
|
-
|
|
663
|
-
relationships: parseRelationships(
|
|
664
|
-
contentTypes: parseContentTypes(
|
|
665
|
-
styles: parseStyles(
|
|
666
|
-
numbering: parseNumbering(
|
|
676
|
+
package: docx,
|
|
677
|
+
relationships: parseRelationships(await docx.readText("word/_rels/document.xml.rels")),
|
|
678
|
+
contentTypes: parseContentTypes(await docx.readText("[Content_Types].xml")),
|
|
679
|
+
styles: parseStyles(await docx.readText("word/styles.xml")),
|
|
680
|
+
numbering: parseNumbering(await docx.readText("word/numbering.xml")),
|
|
667
681
|
messages: [],
|
|
668
682
|
warnedStyles: new Set(),
|
|
669
683
|
customStyles: parseCustomStyles(options.styleMap),
|
|
670
684
|
includeDefaultStyleMap: options.includeDefaultStyleMap !== false,
|
|
671
685
|
convertImage: options.convertImage ?? defaultImageConverter(),
|
|
672
|
-
footnotes: parseFootnotes(
|
|
686
|
+
footnotes: parseFootnotes(await docx.readText("word/footnotes.xml")),
|
|
673
687
|
usedFootnotes: [],
|
|
674
688
|
footnoteOrdinals: new Map(),
|
|
675
689
|
};
|