@oh-my-pi/pi-utils 17.2.8 → 17.2.10
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 +32 -0
- package/dist/types/acp/connection.d.ts +118 -0
- package/dist/types/acp/protocol.d.ts +526 -0
- package/dist/types/acp/schema.d.ts +41 -0
- package/dist/types/acp/stream.d.ts +8 -0
- package/dist/types/acp/transport.d.ts +81 -0
- package/dist/types/acp.d.ts +6 -0
- package/dist/types/browsers.d.ts +68 -0
- package/dist/types/chalk.d.ts +125 -0
- package/dist/types/dates.d.ts +7 -0
- package/dist/types/docx/converter.d.ts +46 -0
- package/dist/types/docx/xml.d.ts +26 -0
- package/dist/types/docx/zip.d.ts +6 -0
- package/dist/types/docx.d.ts +11 -0
- package/dist/types/dom/core.d.ts +431 -0
- package/dist/types/dom/parser.d.ts +7 -0
- package/dist/types/dom/selector.d.ts +5 -0
- package/dist/types/dom.d.ts +5 -0
- package/dist/types/headers.d.ts +34 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/logger/rotating-file.d.ts +18 -0
- package/dist/types/lru.d.ts +46 -0
- package/dist/types/marked/core.d.ts +445 -0
- package/dist/types/marked.d.ts +2 -0
- package/dist/types/postmortem.d.ts +14 -0
- package/dist/types/procmgr.d.ts +16 -0
- package/dist/types/prompt.d.ts +2 -2
- package/dist/types/readability/readability.d.ts +9 -0
- package/dist/types/readability/readerable.d.ts +10 -0
- package/dist/types/readability/types.d.ts +70 -0
- package/dist/types/readability.d.ts +4 -0
- package/dist/types/template.d.ts +62 -0
- package/dist/types/turndown/gfm.d.ts +11 -0
- package/dist/types/turndown/html.d.ts +5 -0
- package/dist/types/turndown/service.d.ts +21 -0
- package/dist/types/turndown/types.d.ts +70 -0
- package/dist/types/turndown.d.ts +4 -0
- package/dist/types/version.d.ts +18 -0
- package/dist/types/vterm/buffer.d.ts +99 -0
- package/dist/types/vterm/terminal.d.ts +44 -0
- package/dist/types/vterm.d.ts +8 -0
- package/dist/types/xml.d.ts +31 -0
- package/package.json +2 -5
- package/src/acp/connection.ts +344 -0
- package/src/acp/protocol.ts +466 -0
- package/src/acp/schema.ts +160 -0
- package/src/acp/stream.ts +82 -0
- package/src/acp/transport.ts +213 -0
- package/src/acp.ts +6 -0
- package/src/browsers.ts +501 -0
- package/src/chalk.ts +312 -0
- package/src/dates.ts +194 -0
- package/src/docx/converter.ts +681 -0
- package/src/docx/xml.ts +166 -0
- package/src/docx/zip.ts +87 -0
- package/src/docx.ts +20 -0
- package/src/dom/core.ts +1254 -0
- package/src/dom/parser.ts +370 -0
- package/src/dom/selector.ts +290 -0
- package/src/dom.ts +33 -0
- package/src/fetch-retry.ts +7 -1
- package/src/frontmatter.ts +1 -1
- package/src/headers.ts +167 -0
- package/src/index.ts +1 -0
- package/src/logger/rotating-file.ts +149 -0
- package/src/logger.ts +12 -28
- package/src/lru.ts +185 -0
- package/src/marked/core.ts +1576 -0
- package/src/marked.ts +2 -0
- package/src/postmortem.ts +44 -1
- package/src/procmgr.ts +21 -4
- package/src/prompt.ts +5 -22
- package/src/readability/readability.ts +533 -0
- package/src/readability/readerable.ts +51 -0
- package/src/readability/types.ts +72 -0
- package/src/readability.ts +11 -0
- package/src/template.ts +586 -0
- package/src/turndown/gfm.ts +106 -0
- package/src/turndown/html.ts +257 -0
- package/src/turndown/service.ts +334 -0
- package/src/turndown/types.ts +81 -0
- package/src/turndown.ts +5 -0
- package/src/version.ts +99 -0
- package/src/vterm/buffer.ts +218 -0
- package/src/vterm/terminal.ts +773 -0
- package/src/vterm.ts +8 -0
- package/src/which.ts +6 -4
- package/src/xml.ts +313 -0
- package/src/winston-daily-rotate-file.d.ts +0 -6
package/src/template.ts
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
/** Behavior-compatible reimplementation of handlebars' used surface. */
|
|
2
|
+
|
|
3
|
+
/** Values accepted as registered partial templates. */
|
|
4
|
+
export type Template = string | TemplateDelegate;
|
|
5
|
+
|
|
6
|
+
/** A compiled template function. */
|
|
7
|
+
export type TemplateDelegate<T = unknown> = (context?: T, options?: RuntimeOptions) => string;
|
|
8
|
+
|
|
9
|
+
/** Runtime options accepted by compiled templates. */
|
|
10
|
+
export interface RuntimeOptions {
|
|
11
|
+
data?: Record<string, unknown>;
|
|
12
|
+
helpers?: Record<string, HelperDelegate>;
|
|
13
|
+
partials?: Record<string, Template>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Options passed as the final argument to helpers. */
|
|
17
|
+
export interface HelperOptions {
|
|
18
|
+
name: string;
|
|
19
|
+
hash: Record<string, unknown>;
|
|
20
|
+
data: Record<string, unknown>;
|
|
21
|
+
fn: (context?: unknown, options?: { data?: Record<string, unknown> }) => string;
|
|
22
|
+
inverse: (context?: unknown, options?: { data?: Record<string, unknown> }) => string;
|
|
23
|
+
lookupProperty: (parent: unknown, propertyName: string) => unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A template helper function. */
|
|
27
|
+
export type HelperDelegate = { bivarianceHack(this: unknown, ...args: unknown[]): unknown }["bivarianceHack"];
|
|
28
|
+
|
|
29
|
+
/** Template compilation behavior. */
|
|
30
|
+
export interface CompileOptions {
|
|
31
|
+
noEscape?: boolean;
|
|
32
|
+
strict?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A string that bypasses HTML escaping when interpolated. */
|
|
36
|
+
export class SafeString {
|
|
37
|
+
readonly #value: string;
|
|
38
|
+
|
|
39
|
+
constructor(value: unknown) {
|
|
40
|
+
this.#value = String(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Return the unescaped string value. */
|
|
44
|
+
toString(): string {
|
|
45
|
+
return this.#value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Return the unescaped primitive value. */
|
|
49
|
+
toHTML(): string {
|
|
50
|
+
return this.#value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type PathExpression = { kind: "path"; value: string };
|
|
55
|
+
type LiteralExpression = { kind: "literal"; value: unknown };
|
|
56
|
+
type CallExpression = { kind: "call"; name: string; args: Expression[]; hash: Record<string, Expression> };
|
|
57
|
+
type Expression = PathExpression | LiteralExpression | CallExpression;
|
|
58
|
+
type TextNode = { kind: "text"; value: string };
|
|
59
|
+
type OutputNode = { kind: "output"; expression: CallExpression; escaped: boolean };
|
|
60
|
+
type BlockNode = { kind: "block"; expression: CallExpression; body: Node[]; inverse: Node[]; inverted: boolean };
|
|
61
|
+
type PartialNode = { kind: "partial"; expression: CallExpression };
|
|
62
|
+
type Node = TextNode | OutputNode | BlockNode | PartialNode;
|
|
63
|
+
|
|
64
|
+
type Frame = {
|
|
65
|
+
context: unknown;
|
|
66
|
+
parents: Frame[];
|
|
67
|
+
root: unknown;
|
|
68
|
+
data: Record<string, unknown>;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type Evaluation = {
|
|
72
|
+
helpers: Map<string, HelperDelegate>;
|
|
73
|
+
partials: Map<string, Template>;
|
|
74
|
+
options: CompileOptions;
|
|
75
|
+
runtime: RuntimeOptions;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const helpers = new Map<string, HelperDelegate>();
|
|
79
|
+
const partials = new Map<string, Template>();
|
|
80
|
+
const BLOCK_TAG = /^\s*\{\{(?:#|\/|\^|else\b|!)/;
|
|
81
|
+
|
|
82
|
+
function stripStandalone(source: string): string {
|
|
83
|
+
const lines = source.split(/(?<=\n)/);
|
|
84
|
+
for (let index = 0; index < lines.length; index++) {
|
|
85
|
+
const line = lines[index];
|
|
86
|
+
const body = line.endsWith("\n") ? line.slice(0, -1).replace(/\r$/, "") : line;
|
|
87
|
+
if (/^\s*\{\{!--/.test(body) && !body.includes("--}}")) {
|
|
88
|
+
for (let end = index + 1; end < lines.length; end++) {
|
|
89
|
+
const endLine = lines[end].endsWith("\n") ? lines[end].slice(0, -1).replace(/\r$/, "") : lines[end];
|
|
90
|
+
const close = endLine.indexOf("--}}");
|
|
91
|
+
if (close < 0) continue;
|
|
92
|
+
if (/^\s*$/.test(endLine.slice(close + 4))) {
|
|
93
|
+
for (let commentLine = index; commentLine <= end; commentLine++) lines[commentLine] = "";
|
|
94
|
+
index = end;
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (BLOCK_TAG.test(body) && /^\s*\{\{(?:#|\/|\^|else\b|!)[^{}]*\}\}\s*$/.test(body)) {
|
|
101
|
+
lines[index] = body.trim();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return lines.join("");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function tokenizeExpression(source: string): string[] {
|
|
108
|
+
const tokens: string[] = [];
|
|
109
|
+
let start = -1;
|
|
110
|
+
let quote = "";
|
|
111
|
+
let depth = 0;
|
|
112
|
+
let bracketDepth = 0;
|
|
113
|
+
for (let index = 0; index < source.length; index++) {
|
|
114
|
+
const char = source[index];
|
|
115
|
+
if (quote) {
|
|
116
|
+
if (char === "\\" && index + 1 < source.length) index++;
|
|
117
|
+
else if (char === quote) quote = "";
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (char === '"' || char === "'") {
|
|
121
|
+
if (start < 0) start = index;
|
|
122
|
+
quote = char;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (char === "(") {
|
|
126
|
+
if (start < 0) start = index;
|
|
127
|
+
depth++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (char === ")") {
|
|
131
|
+
depth--;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (char === "[") {
|
|
135
|
+
if (start < 0) start = index;
|
|
136
|
+
bracketDepth++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (char === "]") {
|
|
140
|
+
bracketDepth--;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (/\s/.test(char) && depth === 0 && bracketDepth === 0) {
|
|
144
|
+
if (start >= 0) tokens.push(source.slice(start, index));
|
|
145
|
+
start = -1;
|
|
146
|
+
} else if (start < 0) {
|
|
147
|
+
start = index;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (start >= 0) tokens.push(source.slice(start));
|
|
151
|
+
return tokens;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parseString(token: string): string {
|
|
155
|
+
const quote = token[0];
|
|
156
|
+
let result = "";
|
|
157
|
+
for (let index = 1; index < token.length - 1; index++) {
|
|
158
|
+
const char = token[index];
|
|
159
|
+
if (char === "\\" && index + 1 < token.length - 1) {
|
|
160
|
+
const next = token[++index];
|
|
161
|
+
result += next === quote || next === "\\" ? next : `\\${next}`;
|
|
162
|
+
} else result += char;
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseAtom(token: string): Expression {
|
|
168
|
+
if (token.startsWith("(") && token.endsWith(")")) return parseCall(token.slice(1, -1));
|
|
169
|
+
if ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith("'") && token.endsWith("'"))) {
|
|
170
|
+
return { kind: "literal", value: parseString(token) };
|
|
171
|
+
}
|
|
172
|
+
if (token === "true" || token === "false") return { kind: "literal", value: token === "true" };
|
|
173
|
+
if (token === "null") return { kind: "literal", value: null };
|
|
174
|
+
if (token === "undefined") return { kind: "literal", value: undefined };
|
|
175
|
+
if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(token)) return { kind: "literal", value: Number(token) };
|
|
176
|
+
return { kind: "path", value: token };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function hashSeparator(token: string): number {
|
|
180
|
+
let quote = "";
|
|
181
|
+
let depth = 0;
|
|
182
|
+
for (let index = 0; index < token.length; index++) {
|
|
183
|
+
const char = token[index];
|
|
184
|
+
if (quote) {
|
|
185
|
+
if (char === "\\") index++;
|
|
186
|
+
else if (char === quote) quote = "";
|
|
187
|
+
} else if (char === '"' || char === "'") quote = char;
|
|
188
|
+
else if (char === "(") depth++;
|
|
189
|
+
else if (char === ")") depth--;
|
|
190
|
+
else if (char === "=" && depth === 0) return index;
|
|
191
|
+
}
|
|
192
|
+
return -1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function parseCall(source: string): CallExpression {
|
|
196
|
+
const tokens = tokenizeExpression(source.trim());
|
|
197
|
+
const name = tokens.shift() ?? "";
|
|
198
|
+
const args: Expression[] = [];
|
|
199
|
+
const hash: Record<string, Expression> = {};
|
|
200
|
+
for (const token of tokens) {
|
|
201
|
+
const separator = hashSeparator(token);
|
|
202
|
+
if (separator > 0) hash[token.slice(0, separator)] = parseAtom(token.slice(separator + 1));
|
|
203
|
+
else args.push(parseAtom(token));
|
|
204
|
+
}
|
|
205
|
+
return { kind: "call", name, args, hash };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function findTagEnd(source: string, start: number, triple: boolean): number {
|
|
209
|
+
const close = triple ? "}}}" : "}}";
|
|
210
|
+
let quote = "";
|
|
211
|
+
let depth = 0;
|
|
212
|
+
for (let index = start; index <= source.length - close.length; index++) {
|
|
213
|
+
const char = source[index];
|
|
214
|
+
if (quote) {
|
|
215
|
+
if (char === "\\") index++;
|
|
216
|
+
else if (char === quote) quote = "";
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (char === '"' || char === "'") quote = char;
|
|
220
|
+
else if (char === "(") depth++;
|
|
221
|
+
else if (char === ")") depth--;
|
|
222
|
+
else if (depth === 0 && source.startsWith(close, index)) return index;
|
|
223
|
+
}
|
|
224
|
+
throw new Error("Parse error: unclosed template expression");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function parseTemplate(source: string): Node[] {
|
|
228
|
+
const root: Node[] = [];
|
|
229
|
+
const stack: { node: BlockNode; target: Node[]; inverted: boolean }[] = [];
|
|
230
|
+
let target = root;
|
|
231
|
+
let cursor = 0;
|
|
232
|
+
while (cursor < source.length) {
|
|
233
|
+
const open = source.indexOf("{{", cursor);
|
|
234
|
+
if (open < 0) {
|
|
235
|
+
if (cursor < source.length) target.push({ kind: "text", value: source.slice(cursor) });
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
if (open > cursor) target.push({ kind: "text", value: source.slice(cursor, open) });
|
|
239
|
+
if (source.startsWith("{{!--", open)) {
|
|
240
|
+
const end = source.indexOf("--}}", open + 6);
|
|
241
|
+
if (end < 0) throw new Error("Parse error: unclosed comment");
|
|
242
|
+
cursor = end + 4;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const triple = source.startsWith("{{{", open);
|
|
246
|
+
const contentStart = open + (triple ? 3 : 2);
|
|
247
|
+
const end = findTagEnd(source, contentStart, triple);
|
|
248
|
+
const raw = source.slice(contentStart, end).trim();
|
|
249
|
+
cursor = end + (triple ? 3 : 2);
|
|
250
|
+
if (!raw || raw.startsWith("!")) continue;
|
|
251
|
+
if (raw === "else" || raw.startsWith("else ")) {
|
|
252
|
+
const current = stack[stack.length - 1];
|
|
253
|
+
if (!current) throw new Error("Parse error: unexpected else");
|
|
254
|
+
target = current.node.inverse;
|
|
255
|
+
if (raw.length > 4) {
|
|
256
|
+
const parentTarget = target;
|
|
257
|
+
const nested: BlockNode = {
|
|
258
|
+
kind: "block",
|
|
259
|
+
expression: parseCall(raw.slice(5)),
|
|
260
|
+
body: [],
|
|
261
|
+
inverse: [],
|
|
262
|
+
inverted: false,
|
|
263
|
+
};
|
|
264
|
+
target.push(nested);
|
|
265
|
+
target = nested.body;
|
|
266
|
+
stack.push({ node: nested, target: parentTarget, inverted: false });
|
|
267
|
+
}
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (raw.startsWith("#") || raw.startsWith("^")) {
|
|
271
|
+
const inverted = raw.startsWith("^");
|
|
272
|
+
const node: BlockNode = {
|
|
273
|
+
kind: "block",
|
|
274
|
+
expression: parseCall(raw.slice(1)),
|
|
275
|
+
body: [],
|
|
276
|
+
inverse: [],
|
|
277
|
+
inverted,
|
|
278
|
+
};
|
|
279
|
+
target.push(node);
|
|
280
|
+
stack.push({ node, target, inverted });
|
|
281
|
+
target = node.body;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (raw.startsWith("/")) {
|
|
285
|
+
const current = stack.pop();
|
|
286
|
+
if (!current || current.node.expression.name !== raw.slice(1).trim())
|
|
287
|
+
throw new Error(`Parse error: mismatched ${raw}`);
|
|
288
|
+
if (current.inverted) [current.node.body, current.node.inverse] = [current.node.inverse, current.node.body];
|
|
289
|
+
target = current.target;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (raw.startsWith(">")) target.push({ kind: "partial", expression: parseCall(raw.slice(1)) });
|
|
293
|
+
else
|
|
294
|
+
target.push({
|
|
295
|
+
kind: "output",
|
|
296
|
+
expression: parseCall(raw.startsWith("&") ? raw.slice(1) : raw),
|
|
297
|
+
escaped: !triple && !raw.startsWith("&"),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
if (stack.length) throw new Error(`Parse error: unclosed block ${stack[stack.length - 1].node.expression.name}`);
|
|
301
|
+
return root;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function pathParts(path: string): string[] {
|
|
305
|
+
const parts: string[] = [];
|
|
306
|
+
let start = 0;
|
|
307
|
+
let bracketStart = -1;
|
|
308
|
+
for (let index = 0; index <= path.length; index++) {
|
|
309
|
+
const char = path[index];
|
|
310
|
+
if (char === "[" && bracketStart < 0) {
|
|
311
|
+
if (index > start) parts.push(path.slice(start, index).replace(/[./]+$/, ""));
|
|
312
|
+
bracketStart = index + 1;
|
|
313
|
+
} else if (char === "]" && bracketStart >= 0) {
|
|
314
|
+
parts.push(path.slice(bracketStart, index));
|
|
315
|
+
start = index + 1;
|
|
316
|
+
bracketStart = -1;
|
|
317
|
+
} else if ((char === "." || char === "/" || char === undefined) && bracketStart < 0) {
|
|
318
|
+
if (index > start) parts.push(path.slice(start, index));
|
|
319
|
+
start = index + 1;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return parts.filter(Boolean);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function property(parent: unknown, key: string): unknown {
|
|
326
|
+
if (parent == null) return undefined;
|
|
327
|
+
if (key === "length" && (typeof parent === "string" || Array.isArray(parent))) return parent.length;
|
|
328
|
+
if (typeof parent !== "object" && typeof parent !== "function") return undefined;
|
|
329
|
+
if (key === "__proto__" || key === "prototype" || key === "constructor") return undefined;
|
|
330
|
+
const record = parent as Record<string, unknown>;
|
|
331
|
+
return Object.hasOwn(record, key) ? record[key] : undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function resolvePath(path: string, frame: Frame): unknown {
|
|
335
|
+
if (path === "this" || path === ".") return frame.context;
|
|
336
|
+
let current = frame;
|
|
337
|
+
while (path.startsWith("../")) {
|
|
338
|
+
current = current.parents[0] ?? current;
|
|
339
|
+
path = path.slice(3);
|
|
340
|
+
}
|
|
341
|
+
if (path === "this" || path === ".") return current.context;
|
|
342
|
+
if (path.startsWith("this.")) path = path.slice(5);
|
|
343
|
+
if (path === "@root") return current.root;
|
|
344
|
+
if (path.startsWith("@root.")) {
|
|
345
|
+
let value: unknown = current.root;
|
|
346
|
+
for (const part of pathParts(path.slice(6))) value = property(value, part);
|
|
347
|
+
return value;
|
|
348
|
+
}
|
|
349
|
+
if (path.startsWith("@")) {
|
|
350
|
+
const parts = pathParts(path.slice(1));
|
|
351
|
+
let value: unknown = current.data[parts.shift() ?? ""];
|
|
352
|
+
for (const part of parts) value = property(value, part);
|
|
353
|
+
return value;
|
|
354
|
+
}
|
|
355
|
+
let value: unknown = current.context;
|
|
356
|
+
for (const part of pathParts(path)) value = property(value, part);
|
|
357
|
+
return typeof value === "function" ? value.call(current.context) : value;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function childFrame(frame: Frame, context: unknown, data = frame.data): Frame {
|
|
361
|
+
return { context, parents: [frame, ...frame.parents], root: frame.root, data };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function evaluateExpression(expression: Expression, frame: Frame, evaluation: Evaluation): unknown {
|
|
365
|
+
if (expression.kind === "literal") return expression.value;
|
|
366
|
+
if (expression.kind === "path") return resolvePath(expression.value, frame);
|
|
367
|
+
return evaluateCall(expression, frame, evaluation, false);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function isConditionalTruthy(value: unknown, includeZero = false): boolean {
|
|
371
|
+
return value === 0 ? includeZero : Boolean(value) && !(Array.isArray(value) && value.length === 0);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function renderNodes(nodes: Node[], frame: Frame, evaluation: Evaluation): string {
|
|
375
|
+
let result = "";
|
|
376
|
+
for (const node of nodes) {
|
|
377
|
+
if (node.kind === "text") result += node.value;
|
|
378
|
+
else if (node.kind === "output") {
|
|
379
|
+
const value = evaluateCall(node.expression, frame, evaluation, false);
|
|
380
|
+
result += stringify(value, node.escaped && !evaluation.options.noEscape);
|
|
381
|
+
} else if (node.kind === "partial") result += renderPartial(node, frame, evaluation);
|
|
382
|
+
else result += evaluateBlock(node, frame, evaluation);
|
|
383
|
+
}
|
|
384
|
+
return result;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function helperOptions(
|
|
388
|
+
name: string,
|
|
389
|
+
hash: Record<string, unknown>,
|
|
390
|
+
frame: Frame,
|
|
391
|
+
evaluation: Evaluation,
|
|
392
|
+
body: Node[] = [],
|
|
393
|
+
inverse: Node[] = [],
|
|
394
|
+
): HelperOptions {
|
|
395
|
+
return {
|
|
396
|
+
name,
|
|
397
|
+
hash,
|
|
398
|
+
data: frame.data,
|
|
399
|
+
fn: (context = frame.context, options) =>
|
|
400
|
+
renderNodes(body, childFrame(frame, context, options?.data ?? frame.data), evaluation),
|
|
401
|
+
inverse: (context = frame.context, options) =>
|
|
402
|
+
renderNodes(inverse, childFrame(frame, context, options?.data ?? frame.data), evaluation),
|
|
403
|
+
lookupProperty: property,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function evaluateHash(hash: Record<string, Expression>, frame: Frame, evaluation: Evaluation): Record<string, unknown> {
|
|
408
|
+
const result: Record<string, unknown> = {};
|
|
409
|
+
for (const key in hash) result[key] = evaluateExpression(hash[key], frame, evaluation);
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
function findHelper(name: string, evaluation: Evaluation): HelperDelegate | undefined {
|
|
413
|
+
return evaluation.runtime.helpers?.[name] ?? evaluation.helpers.get(name);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function evaluateCall(
|
|
417
|
+
call: CallExpression,
|
|
418
|
+
frame: Frame,
|
|
419
|
+
evaluation: Evaluation,
|
|
420
|
+
forceHelper: boolean,
|
|
421
|
+
body: Node[] = [],
|
|
422
|
+
inverse: Node[] = [],
|
|
423
|
+
): unknown {
|
|
424
|
+
const helper = findHelper(call.name, evaluation);
|
|
425
|
+
const args = call.args.map(argument => evaluateExpression(argument, frame, evaluation));
|
|
426
|
+
const hash = evaluateHash(call.hash, frame, evaluation);
|
|
427
|
+
if (helper)
|
|
428
|
+
return helper.call(frame.context, ...args, helperOptions(call.name, hash, frame, evaluation, body, inverse));
|
|
429
|
+
// Handlebars built-in: `{{lookup obj key}}` → proto-safe `obj[key]`.
|
|
430
|
+
// Resolved after user helpers so a registered `lookup` override wins.
|
|
431
|
+
if (call.name === "lookup" && args.length >= 2) return property(args[0], String(args[1]));
|
|
432
|
+
if (forceHelper || args.length) throw new Error(`Missing helper: "${call.name}"`);
|
|
433
|
+
for (const _key in hash) throw new Error(`Missing helper: "${call.name}"`);
|
|
434
|
+
return resolvePath(call.name, frame);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function evaluateBlock(node: BlockNode, frame: Frame, evaluation: Evaluation): string {
|
|
438
|
+
const { name } = node.expression;
|
|
439
|
+
const helper = findHelper(name, evaluation);
|
|
440
|
+
if (helper) return stringify(evaluateCall(node.expression, frame, evaluation, true, node.body, node.inverse), false);
|
|
441
|
+
const args = node.expression.args.map(argument => evaluateExpression(argument, frame, evaluation));
|
|
442
|
+
const value = args.length ? args[0] : resolvePath(name, frame);
|
|
443
|
+
const hash = evaluateHash(node.expression.hash, frame, evaluation);
|
|
444
|
+
if (name === "if" || name === "unless") {
|
|
445
|
+
const truthy = isConditionalTruthy(value, hash.includeZero === true);
|
|
446
|
+
const branch = name === "if" ? truthy : !truthy;
|
|
447
|
+
return renderNodes(branch ? node.body : node.inverse, frame, evaluation);
|
|
448
|
+
}
|
|
449
|
+
if (name === "each") {
|
|
450
|
+
if (Array.isArray(value)) {
|
|
451
|
+
if (!value.length) return renderNodes(node.inverse, frame, evaluation);
|
|
452
|
+
return value
|
|
453
|
+
.map((item, index) =>
|
|
454
|
+
renderNodes(
|
|
455
|
+
node.body,
|
|
456
|
+
childFrame(frame, item, {
|
|
457
|
+
...frame.data,
|
|
458
|
+
index,
|
|
459
|
+
key: index,
|
|
460
|
+
first: index === 0,
|
|
461
|
+
last: index === value.length - 1,
|
|
462
|
+
}),
|
|
463
|
+
evaluation,
|
|
464
|
+
),
|
|
465
|
+
)
|
|
466
|
+
.join("");
|
|
467
|
+
}
|
|
468
|
+
if (value && typeof value === "object") {
|
|
469
|
+
const entries = Object.entries(value);
|
|
470
|
+
if (!entries.length) return renderNodes(node.inverse, frame, evaluation);
|
|
471
|
+
return entries
|
|
472
|
+
.map(([key, item], index) =>
|
|
473
|
+
renderNodes(
|
|
474
|
+
node.body,
|
|
475
|
+
childFrame(frame, item, {
|
|
476
|
+
...frame.data,
|
|
477
|
+
index,
|
|
478
|
+
key,
|
|
479
|
+
first: index === 0,
|
|
480
|
+
last: index === entries.length - 1,
|
|
481
|
+
}),
|
|
482
|
+
evaluation,
|
|
483
|
+
),
|
|
484
|
+
)
|
|
485
|
+
.join("");
|
|
486
|
+
}
|
|
487
|
+
return renderNodes(node.inverse, frame, evaluation);
|
|
488
|
+
}
|
|
489
|
+
if (name === "with")
|
|
490
|
+
return isConditionalTruthy(value)
|
|
491
|
+
? renderNodes(node.body, childFrame(frame, value), evaluation)
|
|
492
|
+
: renderNodes(node.inverse, frame, evaluation);
|
|
493
|
+
const resolved = resolvePath(name, frame);
|
|
494
|
+
if (Array.isArray(resolved))
|
|
495
|
+
return (
|
|
496
|
+
resolved.map(item => renderNodes(node.body, childFrame(frame, item), evaluation)).join("") ||
|
|
497
|
+
renderNodes(node.inverse, frame, evaluation)
|
|
498
|
+
);
|
|
499
|
+
return resolved === false || resolved == null
|
|
500
|
+
? renderNodes(node.inverse, frame, evaluation)
|
|
501
|
+
: renderNodes(node.body, childFrame(frame, resolved), evaluation);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function renderPartial(node: PartialNode, frame: Frame, evaluation: Evaluation): string {
|
|
505
|
+
const partial = evaluation.runtime.partials?.[node.expression.name] ?? evaluation.partials.get(node.expression.name);
|
|
506
|
+
if (partial === undefined) throw new Error(`The partial ${node.expression.name} could not be found`);
|
|
507
|
+
const context = node.expression.args.length
|
|
508
|
+
? evaluateExpression(node.expression.args[0], frame, evaluation)
|
|
509
|
+
: frame.context;
|
|
510
|
+
const hash = evaluateHash(node.expression.hash, frame, evaluation);
|
|
511
|
+
const merged =
|
|
512
|
+
hash && context && typeof context === "object" ? { ...(context as Record<string, unknown>), ...hash } : context;
|
|
513
|
+
return typeof partial === "function"
|
|
514
|
+
? partial(merged, evaluation.runtime)
|
|
515
|
+
: compile(partial, evaluation.options)(merged, evaluation.runtime);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function stringify(value: unknown, shouldEscape: boolean): string {
|
|
519
|
+
if (value == null) return "";
|
|
520
|
+
if (value instanceof SafeString) return value.toString();
|
|
521
|
+
const text = String(value);
|
|
522
|
+
return shouldEscape ? escapeExpression(text) : text;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** Escape a value with Handlebars' HTML entity set. */
|
|
526
|
+
export function escapeExpression(value: unknown): string {
|
|
527
|
+
const text = value == null ? "" : String(value);
|
|
528
|
+
return text.replace(
|
|
529
|
+
/[&<>"'`=]/g,
|
|
530
|
+
char =>
|
|
531
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "`": "`", "=": "=" })[
|
|
532
|
+
char
|
|
533
|
+
] ?? char,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Register a helper for subsequently compiled templates. */
|
|
538
|
+
export function registerHelper(name: string, helper: HelperDelegate): void {
|
|
539
|
+
helpers.set(name, helper);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** Register a partial for subsequently rendered templates. */
|
|
543
|
+
export function registerPartial(name: string, partial: Template): void {
|
|
544
|
+
partials.set(name, partial);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Compile a template into a reusable rendering function. */
|
|
548
|
+
export function compile<T = unknown>(source: string, options: CompileOptions = {}): TemplateDelegate<T> {
|
|
549
|
+
const nodes = parseTemplate(stripStandalone(source));
|
|
550
|
+
return (context, runtime = {}) => {
|
|
551
|
+
const root = context ?? {};
|
|
552
|
+
const frame: Frame = { context: root, parents: [], root, data: { root, ...(runtime.data ?? {}) } };
|
|
553
|
+
return renderNodes(nodes, frame, { helpers, partials, options, runtime });
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** Create an isolated template engine with its own helper and partial registries. */
|
|
558
|
+
export function create(): TemplateEngine {
|
|
559
|
+
return new TemplateEngine();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** Isolated template compiler and registry. */
|
|
563
|
+
export class TemplateEngine {
|
|
564
|
+
readonly #helpers = new Map<string, HelperDelegate>();
|
|
565
|
+
readonly #partials = new Map<string, Template>();
|
|
566
|
+
|
|
567
|
+
/** Register a helper in this engine. */
|
|
568
|
+
registerHelper(name: string, helper: HelperDelegate): void {
|
|
569
|
+
this.#helpers.set(name, helper);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** Register a partial in this engine. */
|
|
573
|
+
registerPartial(name: string, partial: Template): void {
|
|
574
|
+
this.#partials.set(name, partial);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Compile a template using this engine's registries. */
|
|
578
|
+
compile<T = unknown>(source: string, options: CompileOptions = {}): TemplateDelegate<T> {
|
|
579
|
+
const nodes = parseTemplate(stripStandalone(source));
|
|
580
|
+
return (context, runtime = {}) => {
|
|
581
|
+
const root = context ?? {};
|
|
582
|
+
const frame: Frame = { context: root, parents: [], root, data: { root, ...(runtime.data ?? {}) } };
|
|
583
|
+
return renderNodes(nodes, frame, { helpers: this.#helpers, partials: this.#partials, options, runtime });
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { TurndownNode, TurndownPlugin, TurndownServiceLike } from "./types";
|
|
2
|
+
|
|
3
|
+
function descendantElements(node: TurndownNode, name: string): TurndownNode[] {
|
|
4
|
+
const matches: TurndownNode[] = [];
|
|
5
|
+
for (const child of Array.from(node.children)) {
|
|
6
|
+
if (child.nodeName === name) matches.push(child);
|
|
7
|
+
if (child.nodeName !== "TABLE") matches.push(...descendantElements(child, name));
|
|
8
|
+
}
|
|
9
|
+
return matches;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isHeadingTable(node: TurndownNode): boolean {
|
|
13
|
+
if (node.nodeName !== "TABLE") return false;
|
|
14
|
+
const firstRow = descendantElements(node, "TR")[0];
|
|
15
|
+
if (!firstRow) return false;
|
|
16
|
+
const cells = Array.from(firstRow.children);
|
|
17
|
+
return cells.length > 0 && cells.every(cell => cell.nodeName === "TH");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function cellAlignment(cell: TurndownNode): "left" | "center" | "right" | undefined {
|
|
21
|
+
const explicit = cell.getAttribute("align")?.toLowerCase();
|
|
22
|
+
if (explicit === "left" || explicit === "center" || explicit === "right") return explicit;
|
|
23
|
+
const style = cell.getAttribute("style") ?? "";
|
|
24
|
+
const styled = /(?:^|;)\s*text-align\s*:\s*(left|center|right)\s*(?:;|$)/i.exec(style)?.[1]?.toLowerCase();
|
|
25
|
+
return styled === "left" || styled === "center" || styled === "right" ? styled : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function alignmentMarker(cell: TurndownNode): string {
|
|
29
|
+
const alignment = cellAlignment(cell);
|
|
30
|
+
if (alignment === "left") return ":--";
|
|
31
|
+
if (alignment === "center") return ":-:";
|
|
32
|
+
if (alignment === "right") return "--:";
|
|
33
|
+
return "---";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function renderTable(service: TurndownServiceLike, table: TurndownNode): string {
|
|
37
|
+
const rows = descendantElements(table, "TR");
|
|
38
|
+
const rendered: string[] = [];
|
|
39
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
40
|
+
const row = rows[rowIndex];
|
|
41
|
+
if (!row) continue;
|
|
42
|
+
const cells = Array.from(row.children).filter(cell => cell.nodeName === "TH" || cell.nodeName === "TD");
|
|
43
|
+
const values = cells.map(cell => service.convertChildren(cell).trim());
|
|
44
|
+
rendered.push(`| ${values.join(" | ")} |`);
|
|
45
|
+
if (rowIndex === 0) rendered.push(`| ${cells.map(alignmentMarker).join(" | ")} |`);
|
|
46
|
+
}
|
|
47
|
+
return `\n\n${rendered.join("\n")}\n\n`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Install GitHub fenced code blocks selected by `highlight-source-*` wrappers. */
|
|
51
|
+
export const highlightedCodeBlock: TurndownPlugin = service => {
|
|
52
|
+
service.addRule("highlightedCodeBlock", {
|
|
53
|
+
filter(node) {
|
|
54
|
+
return node.nodeName === "DIV" && /(?:^|\s)highlight-source-([^\s]+)/.test(node.getAttribute("class") ?? "");
|
|
55
|
+
},
|
|
56
|
+
replacement(_content, node, options) {
|
|
57
|
+
const language = /(?:^|\s)highlight-source-([^\s]+)/.exec(node.getAttribute("class") ?? "")?.[1] ?? "";
|
|
58
|
+
const text = descendantElements(node, "PRE")[0]?.textContent ?? "";
|
|
59
|
+
return `\n\n${options.fence}${language}\n${text}\n${options.fence}\n\n`;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Install GFM strikethrough conversion. */
|
|
65
|
+
export const strikethrough: TurndownPlugin = service => {
|
|
66
|
+
service.addRule("strikethrough", {
|
|
67
|
+
filter: ["del", "s", "strike"],
|
|
68
|
+
replacement(content) {
|
|
69
|
+
return `~${content}~`;
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Install GFM task-list checkbox conversion. */
|
|
75
|
+
export const taskListItems: TurndownPlugin = service => {
|
|
76
|
+
service.addRule("taskListItems", {
|
|
77
|
+
filter(node) {
|
|
78
|
+
return (
|
|
79
|
+
node.nodeName === "INPUT" &&
|
|
80
|
+
node.parentNode?.nodeName === "LI" &&
|
|
81
|
+
(node.getAttribute("type") ?? "").toLowerCase() === "checkbox"
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
replacement(_content, node) {
|
|
85
|
+
return node.hasAttribute("checked") ? "[x] " : "[ ] ";
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Install GFM table conversion for tables with a heading row. */
|
|
91
|
+
export const tables: TurndownPlugin = service => {
|
|
92
|
+
service.addRule("table", {
|
|
93
|
+
filter: isHeadingTable,
|
|
94
|
+
replacement(_content, node) {
|
|
95
|
+
return renderTable(service, node);
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Install every GFM rule used by the coding agent. */
|
|
101
|
+
export const gfm: TurndownPlugin = service => {
|
|
102
|
+
highlightedCodeBlock(service);
|
|
103
|
+
strikethrough(service);
|
|
104
|
+
tables(service);
|
|
105
|
+
taskListItems(service);
|
|
106
|
+
};
|