@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.
Files changed (89) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/types/acp/connection.d.ts +118 -0
  3. package/dist/types/acp/protocol.d.ts +526 -0
  4. package/dist/types/acp/schema.d.ts +41 -0
  5. package/dist/types/acp/stream.d.ts +8 -0
  6. package/dist/types/acp/transport.d.ts +81 -0
  7. package/dist/types/acp.d.ts +6 -0
  8. package/dist/types/browsers.d.ts +68 -0
  9. package/dist/types/chalk.d.ts +125 -0
  10. package/dist/types/dates.d.ts +7 -0
  11. package/dist/types/docx/converter.d.ts +46 -0
  12. package/dist/types/docx/xml.d.ts +26 -0
  13. package/dist/types/docx/zip.d.ts +6 -0
  14. package/dist/types/docx.d.ts +11 -0
  15. package/dist/types/dom/core.d.ts +431 -0
  16. package/dist/types/dom/parser.d.ts +7 -0
  17. package/dist/types/dom/selector.d.ts +5 -0
  18. package/dist/types/dom.d.ts +5 -0
  19. package/dist/types/headers.d.ts +34 -0
  20. package/dist/types/index.d.ts +1 -0
  21. package/dist/types/logger/rotating-file.d.ts +18 -0
  22. package/dist/types/lru.d.ts +46 -0
  23. package/dist/types/marked/core.d.ts +445 -0
  24. package/dist/types/marked.d.ts +2 -0
  25. package/dist/types/postmortem.d.ts +14 -0
  26. package/dist/types/procmgr.d.ts +16 -0
  27. package/dist/types/prompt.d.ts +2 -2
  28. package/dist/types/readability/readability.d.ts +9 -0
  29. package/dist/types/readability/readerable.d.ts +10 -0
  30. package/dist/types/readability/types.d.ts +70 -0
  31. package/dist/types/readability.d.ts +4 -0
  32. package/dist/types/template.d.ts +62 -0
  33. package/dist/types/turndown/gfm.d.ts +11 -0
  34. package/dist/types/turndown/html.d.ts +5 -0
  35. package/dist/types/turndown/service.d.ts +21 -0
  36. package/dist/types/turndown/types.d.ts +70 -0
  37. package/dist/types/turndown.d.ts +4 -0
  38. package/dist/types/version.d.ts +18 -0
  39. package/dist/types/vterm/buffer.d.ts +99 -0
  40. package/dist/types/vterm/terminal.d.ts +44 -0
  41. package/dist/types/vterm.d.ts +8 -0
  42. package/dist/types/xml.d.ts +31 -0
  43. package/package.json +2 -5
  44. package/src/acp/connection.ts +344 -0
  45. package/src/acp/protocol.ts +466 -0
  46. package/src/acp/schema.ts +160 -0
  47. package/src/acp/stream.ts +82 -0
  48. package/src/acp/transport.ts +213 -0
  49. package/src/acp.ts +6 -0
  50. package/src/browsers.ts +501 -0
  51. package/src/chalk.ts +312 -0
  52. package/src/dates.ts +194 -0
  53. package/src/docx/converter.ts +681 -0
  54. package/src/docx/xml.ts +166 -0
  55. package/src/docx/zip.ts +87 -0
  56. package/src/docx.ts +20 -0
  57. package/src/dom/core.ts +1254 -0
  58. package/src/dom/parser.ts +370 -0
  59. package/src/dom/selector.ts +290 -0
  60. package/src/dom.ts +33 -0
  61. package/src/fetch-retry.ts +7 -1
  62. package/src/frontmatter.ts +1 -1
  63. package/src/headers.ts +167 -0
  64. package/src/index.ts +1 -0
  65. package/src/logger/rotating-file.ts +149 -0
  66. package/src/logger.ts +12 -28
  67. package/src/lru.ts +185 -0
  68. package/src/marked/core.ts +1576 -0
  69. package/src/marked.ts +2 -0
  70. package/src/postmortem.ts +44 -1
  71. package/src/procmgr.ts +21 -4
  72. package/src/prompt.ts +5 -22
  73. package/src/readability/readability.ts +533 -0
  74. package/src/readability/readerable.ts +51 -0
  75. package/src/readability/types.ts +72 -0
  76. package/src/readability.ts +11 -0
  77. package/src/template.ts +586 -0
  78. package/src/turndown/gfm.ts +106 -0
  79. package/src/turndown/html.ts +257 -0
  80. package/src/turndown/service.ts +334 -0
  81. package/src/turndown/types.ts +81 -0
  82. package/src/turndown.ts +5 -0
  83. package/src/version.ts +99 -0
  84. package/src/vterm/buffer.ts +218 -0
  85. package/src/vterm/terminal.ts +773 -0
  86. package/src/vterm.ts +8 -0
  87. package/src/which.ts +6 -4
  88. package/src/xml.ts +313 -0
  89. package/src/winston-daily-rotate-file.d.ts +0 -6
@@ -0,0 +1,257 @@
1
+ import type { TurndownNode } from "./types";
2
+
3
+ const VOID_ELEMENTS: Readonly<Record<string, true>> = {
4
+ AREA: true,
5
+ BASE: true,
6
+ BR: true,
7
+ COL: true,
8
+ EMBED: true,
9
+ HR: true,
10
+ IMG: true,
11
+ INPUT: true,
12
+ LINK: true,
13
+ META: true,
14
+ PARAM: true,
15
+ SOURCE: true,
16
+ TRACK: true,
17
+ WBR: true,
18
+ };
19
+
20
+ const NAMED_ENTITIES: Readonly<Record<string, string>> = {
21
+ amp: "&",
22
+ apos: "'",
23
+ copy: "©",
24
+ gt: ">",
25
+ hellip: "…",
26
+ laquo: "«",
27
+ lt: "<",
28
+ mdash: "—",
29
+ nbsp: " ",
30
+ ndash: "–",
31
+ quot: '"',
32
+ raquo: "»",
33
+ reg: "®",
34
+ };
35
+
36
+ function decodeEntities(value: string): string {
37
+ return value.replace(/&(#(?:x[\da-f]+|\d+)|[a-z][\da-z]+);?/gi, (entity, name: string) => {
38
+ if (name.charAt(0) === "#") {
39
+ const hexadecimal = name.charAt(1).toLowerCase() === "x";
40
+ const number = Number.parseInt(name.slice(hexadecimal ? 2 : 1), hexadecimal ? 16 : 10);
41
+ if (!Number.isFinite(number) || number <= 0 || number > 0x10ffff) return entity;
42
+ try {
43
+ return String.fromCodePoint(number);
44
+ } catch {
45
+ return "�";
46
+ }
47
+ }
48
+ return NAMED_ENTITIES[name.toLowerCase()] ?? entity;
49
+ });
50
+ }
51
+
52
+ function escapeAttribute(value: string): string {
53
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
54
+ }
55
+
56
+ abstract class HtmlNode implements TurndownNode {
57
+ parentNode: HtmlNode | null = null;
58
+ readonly childNodes: HtmlNode[] = [];
59
+ abstract readonly nodeType: number;
60
+ abstract readonly nodeName: string;
61
+
62
+ get children(): HtmlNode[] {
63
+ return this.childNodes.filter(child => child.nodeType === 1);
64
+ }
65
+
66
+ get firstChild(): HtmlNode | null {
67
+ return this.childNodes[0] ?? null;
68
+ }
69
+
70
+ get lastChild(): HtmlNode | null {
71
+ return this.childNodes[this.childNodes.length - 1] ?? null;
72
+ }
73
+
74
+ get previousSibling(): HtmlNode | null {
75
+ if (!this.parentNode) return null;
76
+ const index = this.parentNode.childNodes.indexOf(this);
77
+ return index > 0 ? (this.parentNode.childNodes[index - 1] ?? null) : null;
78
+ }
79
+
80
+ get nextSibling(): HtmlNode | null {
81
+ if (!this.parentNode) return null;
82
+ const index = this.parentNode.childNodes.indexOf(this);
83
+ return index >= 0 ? (this.parentNode.childNodes[index + 1] ?? null) : null;
84
+ }
85
+
86
+ get textContent(): string {
87
+ return this.childNodes.map(child => child.textContent).join("");
88
+ }
89
+
90
+ abstract get outerHTML(): string;
91
+
92
+ getAttribute(_name: string): string | null {
93
+ return null;
94
+ }
95
+
96
+ hasAttribute(_name: string): boolean {
97
+ return false;
98
+ }
99
+
100
+ append(child: HtmlNode): void {
101
+ child.parentNode = this;
102
+ this.childNodes.push(child);
103
+ }
104
+ }
105
+
106
+ class HtmlText extends HtmlNode {
107
+ readonly nodeType = 3;
108
+ readonly nodeName = "#text";
109
+ readonly value: string;
110
+
111
+ constructor(value: string) {
112
+ super();
113
+ this.value = value;
114
+ }
115
+
116
+ override get textContent(): string {
117
+ return this.value;
118
+ }
119
+
120
+ get outerHTML(): string {
121
+ return this.value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
122
+ }
123
+ }
124
+
125
+ class HtmlElement extends HtmlNode {
126
+ readonly nodeType = 1;
127
+ readonly nodeName: string;
128
+ readonly #attributes: Map<string, string>;
129
+
130
+ constructor(name: string, attributes: Map<string, string>) {
131
+ super();
132
+ this.nodeName = name.toUpperCase();
133
+ this.#attributes = attributes;
134
+ }
135
+
136
+ override getAttribute(name: string): string | null {
137
+ return this.#attributes.get(name.toLowerCase()) ?? null;
138
+ }
139
+
140
+ override hasAttribute(name: string): boolean {
141
+ return this.#attributes.has(name.toLowerCase());
142
+ }
143
+
144
+ get outerHTML(): string {
145
+ const tag = this.nodeName.toLowerCase();
146
+ const attributes = [...this.#attributes]
147
+ .map(([name, value]) => (value === "" ? name : `${name}="${escapeAttribute(value)}"`))
148
+ .join(" ");
149
+ const opening = `<${tag}${attributes ? ` ${attributes}` : ""}>`;
150
+ if (VOID_ELEMENTS[this.nodeName]) return opening;
151
+ return `${opening}${this.childNodes.map(child => child.outerHTML).join("")}</${tag}>`;
152
+ }
153
+ }
154
+
155
+ class HtmlFragment extends HtmlNode {
156
+ readonly nodeType = 11;
157
+ readonly nodeName = "#document-fragment";
158
+
159
+ get outerHTML(): string {
160
+ return this.childNodes.map(child => child.outerHTML).join("");
161
+ }
162
+ }
163
+
164
+ function parseAttributes(source: string): Map<string, string> {
165
+ const attributes = new Map<string, string>();
166
+ const pattern = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
167
+ for (const match of source.matchAll(pattern)) {
168
+ const name = match[1]?.toLowerCase();
169
+ if (!name) continue;
170
+ attributes.set(name, decodeEntities(match[2] ?? match[3] ?? match[4] ?? ""));
171
+ }
172
+ return attributes;
173
+ }
174
+
175
+ function* htmlTokens(html: string): Generator<string> {
176
+ let cursor = 0;
177
+ while (cursor < html.length) {
178
+ if (html.charAt(cursor) !== "<") {
179
+ const nextTag = html.indexOf("<", cursor);
180
+ const end = nextTag < 0 ? html.length : nextTag;
181
+ yield html.slice(cursor, end);
182
+ cursor = end;
183
+ continue;
184
+ }
185
+ if (!/[/!a-z]/i.test(html.charAt(cursor + 1))) {
186
+ yield "<";
187
+ cursor++;
188
+ continue;
189
+ }
190
+ if (html.startsWith("<!--", cursor)) {
191
+ const commentEnd = html.indexOf("-->", cursor + 4);
192
+ const end = commentEnd < 0 ? html.length : commentEnd + 3;
193
+ yield html.slice(cursor, end);
194
+ cursor = end;
195
+ continue;
196
+ }
197
+ let quote: string | undefined;
198
+ let tagEnd = cursor + 1;
199
+ for (; tagEnd < html.length; tagEnd++) {
200
+ const character = html.charAt(tagEnd);
201
+ if (quote) {
202
+ if (character === quote) quote = undefined;
203
+ } else if (character === '"' || character === "'") {
204
+ quote = character;
205
+ } else if (character === ">") {
206
+ break;
207
+ }
208
+ }
209
+ if (tagEnd >= html.length) {
210
+ yield "<";
211
+ cursor++;
212
+ continue;
213
+ }
214
+ yield html.slice(cursor, tagEnd + 1);
215
+ cursor = tagEnd + 1;
216
+ }
217
+ }
218
+
219
+ /** Parse an HTML fragment into the small DOM subset needed by Turndown. */
220
+ export function parseHtmlFragment(html: string): TurndownNode {
221
+ const root = new HtmlFragment();
222
+ const stack: HtmlNode[] = [root];
223
+ for (const token of htmlTokens(html)) {
224
+ const parent = stack[stack.length - 1] ?? root;
225
+ if (token.startsWith("<!--") || /^<!/i.test(token)) continue;
226
+ if (!token.startsWith("<")) {
227
+ parent.append(new HtmlText(decodeEntities(token)));
228
+ continue;
229
+ }
230
+ const closing = /^<\/\s*([a-z][\w:-]*)[^>]*>$/i.exec(token);
231
+ if (closing) {
232
+ const name = closing[1]?.toUpperCase();
233
+ for (let index = stack.length - 1; index > 0; index--) {
234
+ if (stack[index]?.nodeName !== name) continue;
235
+ stack.length = index;
236
+ break;
237
+ }
238
+ continue;
239
+ }
240
+ const opening = /^<\s*([a-z][\w:-]*)([\s\S]*?)\/?\s*>$/i.exec(token);
241
+ if (!opening?.[1]) {
242
+ parent.append(new HtmlText("<"));
243
+ continue;
244
+ }
245
+ const element = new HtmlElement(opening[1], parseAttributes(opening[2] ?? ""));
246
+ parent.append(element);
247
+ if (!VOID_ELEMENTS[element.nodeName] && !/\/\s*>$/.test(token)) stack.push(element);
248
+ }
249
+ return root;
250
+ }
251
+
252
+ /** Serialize a standards-shaped or internal HTML node. */
253
+ export function serializeNode(node: TurndownNode): string {
254
+ if (typeof node.outerHTML === "string") return node.outerHTML;
255
+ if (node.nodeType === 3) return node.textContent ?? "";
256
+ return Array.from(node.childNodes).map(serializeNode).join("");
257
+ }
@@ -0,0 +1,334 @@
1
+ import { parseHtmlFragment, serializeNode } from "./html";
2
+ import type {
3
+ ResolvedTurndownOptions,
4
+ RuleFilter,
5
+ TurndownNode,
6
+ TurndownOptions,
7
+ TurndownPlugin,
8
+ TurndownRule,
9
+ } from "./types";
10
+
11
+ const BLOCK_ELEMENTS: Readonly<Record<string, true>> = {
12
+ ADDRESS: true,
13
+ ARTICLE: true,
14
+ ASIDE: true,
15
+ BLOCKQUOTE: true,
16
+ BODY: true,
17
+ CANVAS: true,
18
+ CENTER: true,
19
+ DD: true,
20
+ DETAILS: true,
21
+ DIR: true,
22
+ DIV: true,
23
+ DL: true,
24
+ DT: true,
25
+ FIELDSET: true,
26
+ FIGCAPTION: true,
27
+ FIGURE: true,
28
+ FOOTER: true,
29
+ FORM: true,
30
+ FRAMESET: true,
31
+ H1: true,
32
+ H2: true,
33
+ H3: true,
34
+ H4: true,
35
+ H5: true,
36
+ H6: true,
37
+ HEADER: true,
38
+ HGROUP: true,
39
+ HR: true,
40
+ HTML: true,
41
+ ISINDEX: true,
42
+ LI: true,
43
+ MAIN: true,
44
+ MENU: true,
45
+ NAV: true,
46
+ NOFRAMES: true,
47
+ NOSCRIPT: true,
48
+ OL: true,
49
+ OUTPUT: true,
50
+ P: true,
51
+ PRE: true,
52
+ SECTION: true,
53
+ SUMMARY: true,
54
+ TABLE: true,
55
+ TBODY: true,
56
+ TD: true,
57
+ TFOOT: true,
58
+ TH: true,
59
+ THEAD: true,
60
+ TR: true,
61
+ UL: true,
62
+ };
63
+ const NONBLANK_EMPTY_ELEMENTS: Readonly<Record<string, true>> = {
64
+ A: true,
65
+ AUDIO: true,
66
+ BR: true,
67
+ HR: true,
68
+ IFRAME: true,
69
+ IMG: true,
70
+ INPUT: true,
71
+ SCRIPT: true,
72
+ SOURCE: true,
73
+ TD: true,
74
+ TH: true,
75
+ VIDEO: true,
76
+ };
77
+
78
+ type RuleEntry = { key: string; rule: TurndownRule };
79
+ type Reference = { destination: string };
80
+
81
+ const DEFAULT_OPTIONS: ResolvedTurndownOptions = {
82
+ headingStyle: "setext",
83
+ hr: "* * *",
84
+ bulletListMarker: "*",
85
+ codeBlockStyle: "indented",
86
+ fence: "```",
87
+ emDelimiter: "_",
88
+ strongDelimiter: "**",
89
+ linkStyle: "inlined",
90
+ linkReferenceStyle: "full",
91
+ preformattedCode: false,
92
+ };
93
+
94
+ function matchesFilter(filter: RuleFilter, node: TurndownNode, options: ResolvedTurndownOptions): boolean {
95
+ if (typeof filter === "function") return filter(node, options);
96
+ const name = node.nodeName.toLowerCase();
97
+ if (typeof filter === "string") return name === filter.toLowerCase();
98
+ return filter.some(tag => name === tag.toLowerCase());
99
+ }
100
+
101
+ function contentWithFlankingWhitespace(content: string, delimiter: string): string {
102
+ const leading = content.match(/^\s+/)?.[0] ?? "";
103
+ const trailing = content.match(/\s+$/)?.[0] ?? "";
104
+ const body = content.slice(leading.length, trailing ? -trailing.length : undefined);
105
+ return body ? `${leading}${delimiter}${body}${delimiter}${trailing}` : "";
106
+ }
107
+
108
+ function languageFromCode(node: TurndownNode): string {
109
+ const code = Array.from(node.children).find(child => child.nodeName === "CODE");
110
+ const className = code?.getAttribute("class") ?? "";
111
+ return /(?:^|\s)language-([^\s]+)/.exec(className)?.[1] ?? "";
112
+ }
113
+
114
+ function listItemPrefix(node: TurndownNode, options: ResolvedTurndownOptions): string {
115
+ const parent = node.parentNode;
116
+ if (parent?.nodeName !== "OL") return `${options.bulletListMarker} `;
117
+ const start = Number(parent.getAttribute("start") ?? "1");
118
+ const siblings = Array.from(parent.children);
119
+ return `${(Number.isFinite(start) ? start : 1) + siblings.indexOf(node)}. `;
120
+ }
121
+ function hasNonblankDescendant(node: TurndownNode): boolean {
122
+ for (const child of Array.from(node.children)) {
123
+ if (NONBLANK_EMPTY_ELEMENTS[child.nodeName] || hasNonblankDescendant(child)) return true;
124
+ }
125
+ return false;
126
+ }
127
+
128
+ /** Convert HTML fragments to Markdown with extensible Turndown-compatible rules. */
129
+ export default class TurndownService {
130
+ readonly options: ResolvedTurndownOptions;
131
+ readonly #rules: RuleEntry[] = [];
132
+ readonly #keepFilters: RuleFilter[] = [];
133
+ readonly #removeFilters: RuleFilter[] = [];
134
+ #references: Reference[] = [];
135
+ #previousSourceWhitespace = false;
136
+
137
+ constructor(options: TurndownOptions = {}) {
138
+ this.options = { ...DEFAULT_OPTIONS, ...options };
139
+ }
140
+
141
+ /** Install a highest-priority named conversion rule. */
142
+ addRule(key: string, rule: TurndownRule): this {
143
+ const previous = this.#rules.findIndex(entry => entry.key === key);
144
+ if (previous >= 0) this.#rules.splice(previous, 1);
145
+ this.#rules.unshift({ key, rule });
146
+ return this;
147
+ }
148
+
149
+ /** Preserve matching elements as HTML when no custom rule handles them. */
150
+ keep(filter: RuleFilter): this {
151
+ this.#keepFilters.unshift(filter);
152
+ return this;
153
+ }
154
+
155
+ /** Drop matching elements and all of their converted content. */
156
+ remove(filter: RuleFilter): this {
157
+ this.#removeFilters.unshift(filter);
158
+ return this;
159
+ }
160
+
161
+ /** Install one plugin or an ordered list of plugins. */
162
+ use(plugin: TurndownPlugin | readonly TurndownPlugin[]): this {
163
+ if (typeof plugin === "function") {
164
+ plugin(this);
165
+ } else {
166
+ for (const install of plugin) install(this);
167
+ }
168
+ return this;
169
+ }
170
+
171
+ /** Escape Markdown punctuation using Turndown's public escaping rules. */
172
+ escape(text: string): string {
173
+ return text
174
+ .replace(/\\/g, "\\\\")
175
+ .replace(/([*_[\]])/g, "\\$1")
176
+ .replace(/^(\s*)(#{1,6}|[+>])(?=\s)/gm, "$1\\$2")
177
+ .replace(/^(\s*)-(?=\s)/gm, "$1\\-")
178
+ .replace(/^(\s*\d+)\.(?=\s)/gm, "$1\\.");
179
+ }
180
+
181
+ /** Convert an HTML string or standards-shaped DOM node to Markdown. */
182
+ turndown(input: string | TurndownNode): string {
183
+ const root = typeof input === "string" ? parseHtmlFragment(input) : input;
184
+ this.#references = [];
185
+ this.#previousSourceWhitespace = false;
186
+ let markdown =
187
+ root.nodeType === 9 || root.nodeType === 11 ? this.#convertChildren(root) : this.#convertNode(root);
188
+ markdown = markdown
189
+ .replace(/^[\t\r\n]+/, "")
190
+ .replace(/[\t\r\n ]+$/, "")
191
+ .replace(/\n{3,}/g, "\n\n");
192
+ if (this.#references.length > 0) {
193
+ markdown += `\n\n${this.#references.map(reference => reference.destination).join("\n")}`;
194
+ }
195
+ return markdown;
196
+ }
197
+
198
+ /** Convert only a node's children within the active conversion. */
199
+ convertChildren(node: TurndownNode): string {
200
+ return this.#convertChildren(node);
201
+ }
202
+
203
+ #convertChildren(node: TurndownNode): string {
204
+ let content = "";
205
+ for (const child of Array.from(node.childNodes)) content += this.#convertNode(child);
206
+ return content;
207
+ }
208
+
209
+ #convertNode(node: TurndownNode): string {
210
+ if (node.nodeType === 3) {
211
+ const text = node.textContent ?? "";
212
+ if (node.parentNode?.nodeName === "PRE") return text;
213
+ let collapsed = text.replace(/[\t\r\n\f ]+/g, " ");
214
+ if (this.#previousSourceWhitespace && collapsed.startsWith(" ")) collapsed = collapsed.slice(1);
215
+ this.#previousSourceWhitespace = collapsed.endsWith(" ");
216
+ return node.parentNode?.nodeName === "CODE" ? collapsed : this.escape(collapsed);
217
+ }
218
+ if (node.nodeType !== 1) return this.#convertChildren(node);
219
+ if (!(node.textContent ?? "").trim() && !NONBLANK_EMPTY_ELEMENTS[node.nodeName] && !hasNonblankDescendant(node)) {
220
+ return (
221
+ this.options.blankReplacement?.("", node, this.options) ?? (BLOCK_ELEMENTS[node.nodeName] ? "\n\n" : "")
222
+ );
223
+ }
224
+
225
+ const block = Boolean(BLOCK_ELEMENTS[node.nodeName]);
226
+ if (block) this.#previousSourceWhitespace = false;
227
+ const content = this.#convertChildren(node);
228
+ if (block) this.#previousSourceWhitespace = false;
229
+ if (node.nodeName === "CODE") this.#previousSourceWhitespace = false;
230
+ const custom = this.#rules.find(entry => matchesFilter(entry.rule.filter, node, this.options));
231
+ if (custom) return custom.rule.replacement(content, node, this.options);
232
+ if (this.#keepFilters.some(filter => matchesFilter(filter, node, this.options))) {
233
+ return this.options.keepReplacement?.(content, node, this.options) ?? serializeNode(node);
234
+ }
235
+ if (this.#removeFilters.some(filter => matchesFilter(filter, node, this.options))) return "";
236
+ return this.#defaultReplacement(content, node);
237
+ }
238
+
239
+ #defaultReplacement(content: string, node: TurndownNode): string {
240
+ const name = node.nodeName;
241
+ if (name === "P") return content.trim() ? `\n\n${content.trim()}\n\n` : "";
242
+ if (/^H[1-6]$/.test(name)) {
243
+ const level = Number(name.charAt(1));
244
+ const body = content.trim();
245
+ if (this.options.headingStyle === "setext" && level < 3) {
246
+ return `\n\n${body}\n${(level === 1 ? "=" : "-").repeat(body.length)}\n\n`;
247
+ }
248
+ return `\n\n${"#".repeat(level)} ${body}\n\n`;
249
+ }
250
+ if (name === "BR") return " \n";
251
+ if (name === "HR") return `\n\n${this.options.hr}\n\n`;
252
+ if (name === "BLOCKQUOTE") {
253
+ const body = content
254
+ .trim()
255
+ .replace(/\n{3,}/g, "\n\n")
256
+ .replace(/^/gm, "> ");
257
+ return body ? `\n\n${body}\n\n` : "";
258
+ }
259
+ if (name === "UL" || name === "OL") {
260
+ const body = content.replace(/^\n+|\n+$/g, "");
261
+ return node.parentNode?.nodeName === "LI" ? `\n${body}` : `\n\n${body}\n\n`;
262
+ }
263
+ if (name === "LI") {
264
+ const body = content.replace(/^\n+/, "").replace(/\n+$/, "\n").replace(/\n/gm, "\n ");
265
+ return `${listItemPrefix(node, this.options)}${body}${node.nextSibling ? "\n" : ""}`;
266
+ }
267
+ if (name === "PRE" && Array.from(node.children).some(child => child.nodeName === "CODE")) {
268
+ return this.#replacePre(node);
269
+ }
270
+ if (name === "EM" || name === "I") return contentWithFlankingWhitespace(content, this.options.emDelimiter);
271
+ if (name === "STRONG" || name === "B") {
272
+ return contentWithFlankingWhitespace(content, this.options.strongDelimiter);
273
+ }
274
+ if (name === "CODE") return this.#replaceInlineCode(content);
275
+ if (name === "A") return this.#replaceLink(content, node);
276
+ if (name === "IMG") return this.#replaceImage(node);
277
+ if (this.options.defaultReplacement) return this.options.defaultReplacement(content, node, this.options);
278
+ if (BLOCK_ELEMENTS[name]) return content.trim() ? `\n\n${content.trim()}\n\n` : "";
279
+ return content;
280
+ }
281
+
282
+ #replacePre(node: TurndownNode): string {
283
+ const text = node.textContent ?? "";
284
+ if (this.options.codeBlockStyle === "indented") {
285
+ return `\n\n${text.replace(/^/gm, " ")}\n\n`;
286
+ }
287
+ const fenceCharacter = this.options.fence.charAt(0);
288
+ let longestFence = this.options.fence.length;
289
+ let run = 0;
290
+ for (const character of text) {
291
+ run = character === fenceCharacter ? run + 1 : 0;
292
+ longestFence = Math.max(longestFence, run + 1);
293
+ }
294
+ const fence = fenceCharacter.repeat(longestFence);
295
+ const body = text.replace(/\n$/, "");
296
+ return `\n\n${fence}${languageFromCode(node)}\n${body}\n${fence}\n\n`;
297
+ }
298
+
299
+ #replaceInlineCode(text: string): string {
300
+ const body = text.trim();
301
+ if (!body) return "";
302
+ let longestRun = 0;
303
+ let run = 0;
304
+ for (const character of body) {
305
+ run = character === "`" ? run + 1 : 0;
306
+ longestRun = Math.max(longestRun, run);
307
+ }
308
+ const delimiter = "`".repeat(longestRun + 1);
309
+ const padding = /^`|`$/.test(body) ? " " : "";
310
+ return `${delimiter}${padding}${body}${padding}${delimiter}`;
311
+ }
312
+
313
+ #replaceLink(content: string, node: TurndownNode): string {
314
+ const href = (node.getAttribute("href") ?? "").replace(/([()<>])/g, "\\$1");
315
+ const title = node.getAttribute("title");
316
+ const destination = `${href}${title ? ` "${title.replace(/"/g, '\\"')}"` : ""}`;
317
+ if (this.options.linkStyle !== "referenced") return `[${content}](${destination})`;
318
+ const referenceNumber = this.#references.length + 1;
319
+ let label = String(referenceNumber);
320
+ if (this.options.linkReferenceStyle === "collapsed") label = "";
321
+ if (this.options.linkReferenceStyle === "shortcut") label = content;
322
+ const marker = this.options.linkReferenceStyle === "shortcut" ? `[${content}]` : `[${content}][${label}]`;
323
+ this.#references.push({ destination: `[${label || content}]: ${destination}` });
324
+ return marker;
325
+ }
326
+
327
+ #replaceImage(node: TurndownNode): string {
328
+ const source = (node.getAttribute("src") ?? "").replace(/([()<>])/g, "\\$1");
329
+ if (!source) return "";
330
+ const alternative = this.escape(node.getAttribute("alt") ?? "");
331
+ const title = node.getAttribute("title");
332
+ return `![${alternative}](${source}${title ? ` "${title.replace(/"/g, '\\"')}"` : ""})`;
333
+ }
334
+ }
@@ -0,0 +1,81 @@
1
+ /** Behavior-compatible reimplementation of turndown's used surface. */
2
+
3
+ /** A DOM-shaped node accepted by conversion rules. */
4
+ export interface TurndownNode {
5
+ readonly nodeType: number;
6
+ readonly nodeName: string;
7
+ readonly parentNode: TurndownNode | null;
8
+ readonly childNodes: ArrayLike<TurndownNode>;
9
+ readonly children: ArrayLike<TurndownNode>;
10
+ readonly firstChild: TurndownNode | null;
11
+ readonly lastChild: TurndownNode | null;
12
+ readonly nextSibling: TurndownNode | null;
13
+ readonly previousSibling: TurndownNode | null;
14
+ readonly textContent: string | null;
15
+ readonly outerHTML?: string;
16
+ getAttribute(name: string): string | null;
17
+ hasAttribute(name: string): boolean;
18
+ }
19
+
20
+ /** Options supported by the HTML-to-Markdown converter. */
21
+ export interface TurndownOptions {
22
+ headingStyle?: "setext" | "atx";
23
+ hr?: string;
24
+ bulletListMarker?: "*" | "-" | "+";
25
+ codeBlockStyle?: "indented" | "fenced";
26
+ fence?: string;
27
+ emDelimiter?: "_" | "*";
28
+ strongDelimiter?: "**" | "__";
29
+ linkStyle?: "inlined" | "referenced";
30
+ linkReferenceStyle?: "full" | "collapsed" | "shortcut";
31
+ preformattedCode?: boolean;
32
+ blankReplacement?: ReplacementFunction;
33
+ keepReplacement?: ReplacementFunction;
34
+ defaultReplacement?: ReplacementFunction;
35
+ }
36
+
37
+ /** Fully resolved options passed to replacement callbacks. */
38
+ export interface ResolvedTurndownOptions {
39
+ headingStyle: "setext" | "atx";
40
+ hr: string;
41
+ bulletListMarker: "*" | "-" | "+";
42
+ codeBlockStyle: "indented" | "fenced";
43
+ fence: string;
44
+ emDelimiter: "_" | "*";
45
+ strongDelimiter: "**" | "__";
46
+ linkStyle: "inlined" | "referenced";
47
+ linkReferenceStyle: "full" | "collapsed" | "shortcut";
48
+ preformattedCode: boolean;
49
+ blankReplacement?: ReplacementFunction;
50
+ keepReplacement?: ReplacementFunction;
51
+ defaultReplacement?: ReplacementFunction;
52
+ }
53
+
54
+ /** A tag name, list of tag names, or predicate selecting nodes for a rule. */
55
+ export type RuleFilter =
56
+ | string
57
+ | readonly string[]
58
+ | ((node: TurndownNode, options: ResolvedTurndownOptions) => boolean);
59
+
60
+ /** Produces Markdown for a matched node and its already-converted children. */
61
+ export type ReplacementFunction = (content: string, node: TurndownNode, options: ResolvedTurndownOptions) => string;
62
+
63
+ /** A named conversion rule accepted by `addRule`. */
64
+ export interface TurndownRule {
65
+ filter: RuleFilter;
66
+ replacement: ReplacementFunction;
67
+ }
68
+
69
+ /** A plugin that installs one or more conversion rules. */
70
+ export type TurndownPlugin = (service: TurndownServiceLike) => void;
71
+
72
+ /** Structural service surface available to plugins. */
73
+ export interface TurndownServiceLike {
74
+ readonly options: ResolvedTurndownOptions;
75
+ addRule(key: string, rule: TurndownRule): this;
76
+ convertChildren(node: TurndownNode): string;
77
+ keep(filter: RuleFilter): this;
78
+ remove(filter: RuleFilter): this;
79
+ turndown(input: string | TurndownNode): string;
80
+ escape(text: string): string;
81
+ }
@@ -0,0 +1,5 @@
1
+ /** Behavior-compatible reimplementation of turndown's used surface. */
2
+
3
+ export * from "./turndown/gfm";
4
+ export { default, default as TurndownService } from "./turndown/service";
5
+ export * from "./turndown/types";