@aaroncql/pim-agent 0.0.1

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 (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +212 -0
  3. package/bin/pim.ts +109 -0
  4. package/package.json +49 -0
  5. package/src/extensions/_init/index.ts +109 -0
  6. package/src/extensions/bash/capture.test.ts +126 -0
  7. package/src/extensions/bash/capture.ts +80 -0
  8. package/src/extensions/bash/format.test.ts +240 -0
  9. package/src/extensions/bash/format.ts +76 -0
  10. package/src/extensions/bash/index.ts +86 -0
  11. package/src/extensions/bash/run.test.ts +262 -0
  12. package/src/extensions/bash/run.ts +207 -0
  13. package/src/extensions/bash/schema.ts +54 -0
  14. package/src/extensions/command-picker/index.ts +52 -0
  15. package/src/extensions/command-picker/ranker.test.ts +46 -0
  16. package/src/extensions/command-picker/ranker.ts +17 -0
  17. package/src/extensions/edit/edit.test.ts +285 -0
  18. package/src/extensions/edit/edit.ts +382 -0
  19. package/src/extensions/edit/index.ts +54 -0
  20. package/src/extensions/edit/schema.ts +37 -0
  21. package/src/extensions/file-picker/catalog.test.ts +263 -0
  22. package/src/extensions/file-picker/catalog.ts +219 -0
  23. package/src/extensions/file-picker/index.test.ts +168 -0
  24. package/src/extensions/file-picker/index.ts +119 -0
  25. package/src/extensions/file-picker/ranker.test.ts +94 -0
  26. package/src/extensions/file-picker/ranker.ts +76 -0
  27. package/src/extensions/footer/git.test.ts +76 -0
  28. package/src/extensions/footer/git.ts +87 -0
  29. package/src/extensions/footer/index.test.ts +161 -0
  30. package/src/extensions/footer/index.ts +148 -0
  31. package/src/extensions/footer/powerline.ts +87 -0
  32. package/src/extensions/footer/segments.test.ts +164 -0
  33. package/src/extensions/footer/segments.ts +234 -0
  34. package/src/extensions/glob/glob.test.ts +171 -0
  35. package/src/extensions/glob/glob.ts +34 -0
  36. package/src/extensions/glob/index.test.ts +68 -0
  37. package/src/extensions/glob/index.ts +136 -0
  38. package/src/extensions/glob/render.test.ts +126 -0
  39. package/src/extensions/glob/render.ts +74 -0
  40. package/src/extensions/glob/schema.ts +52 -0
  41. package/src/extensions/grep/grep.test.ts +387 -0
  42. package/src/extensions/grep/grep.ts +215 -0
  43. package/src/extensions/grep/index.test.ts +68 -0
  44. package/src/extensions/grep/index.ts +158 -0
  45. package/src/extensions/grep/render.test.ts +269 -0
  46. package/src/extensions/grep/render.ts +243 -0
  47. package/src/extensions/grep/schema.ts +92 -0
  48. package/src/extensions/read/index.ts +84 -0
  49. package/src/extensions/read/read.test.ts +177 -0
  50. package/src/extensions/read/read.ts +206 -0
  51. package/src/extensions/read/render.test.ts +61 -0
  52. package/src/extensions/read/render.ts +33 -0
  53. package/src/extensions/read/schema.ts +27 -0
  54. package/src/extensions/subagent/index.test.ts +44 -0
  55. package/src/extensions/subagent/index.ts +30 -0
  56. package/src/extensions/subagent/render.test.ts +292 -0
  57. package/src/extensions/subagent/render.ts +359 -0
  58. package/src/extensions/subagent/schema.ts +9 -0
  59. package/src/extensions/subagent/subagent.test.ts +315 -0
  60. package/src/extensions/subagent/subagent.ts +418 -0
  61. package/src/extensions/system-prompt/index.ts +28 -0
  62. package/src/extensions/system-prompt/prompt.test.ts +64 -0
  63. package/src/extensions/system-prompt/prompt.ts +213 -0
  64. package/src/extensions/todo/index.test.ts +244 -0
  65. package/src/extensions/todo/index.ts +122 -0
  66. package/src/extensions/todo/render.test.ts +180 -0
  67. package/src/extensions/todo/render.ts +172 -0
  68. package/src/extensions/todo/schema.ts +24 -0
  69. package/src/extensions/todo/todo.test.ts +222 -0
  70. package/src/extensions/todo/todo.ts +188 -0
  71. package/src/extensions/tps/index.test.ts +254 -0
  72. package/src/extensions/tps/index.ts +136 -0
  73. package/src/extensions/web-fetch/JinaReaderClient.ts +230 -0
  74. package/src/extensions/web-fetch/WebViewFetchClient.ts +186 -0
  75. package/src/extensions/web-fetch/WebViewMarkdownSnapshot.test.ts +119 -0
  76. package/src/extensions/web-fetch/WebViewMarkdownSnapshot.ts +511 -0
  77. package/src/extensions/web-fetch/fetch.test.ts +244 -0
  78. package/src/extensions/web-fetch/fetch.ts +249 -0
  79. package/src/extensions/web-fetch/index.ts +107 -0
  80. package/src/extensions/web-fetch/render.test.ts +56 -0
  81. package/src/extensions/web-fetch/render.ts +39 -0
  82. package/src/extensions/web-fetch/schema.ts +23 -0
  83. package/src/extensions/web-search/ExaMcpClient.test.ts +143 -0
  84. package/src/extensions/web-search/ExaMcpClient.ts +258 -0
  85. package/src/extensions/web-search/index.ts +118 -0
  86. package/src/extensions/web-search/render.test.ts +21 -0
  87. package/src/extensions/web-search/render.ts +9 -0
  88. package/src/extensions/web-search/schema.ts +21 -0
  89. package/src/extensions/web-search/search.test.ts +53 -0
  90. package/src/extensions/web-search/search.ts +23 -0
  91. package/src/extensions/working-indicator/index.test.ts +21 -0
  92. package/src/extensions/working-indicator/index.ts +77 -0
  93. package/src/extensions/write/index.ts +76 -0
  94. package/src/extensions/write/render.test.ts +64 -0
  95. package/src/extensions/write/schema.ts +14 -0
  96. package/src/extensions/write/write.test.ts +108 -0
  97. package/src/extensions/write/write.ts +104 -0
  98. package/src/shared/DiffLines.test.ts +193 -0
  99. package/src/shared/DiffLines.ts +307 -0
  100. package/src/shared/DiffRenderer.test.ts +206 -0
  101. package/src/shared/DiffRenderer.ts +396 -0
  102. package/src/shared/DiffView.ts +199 -0
  103. package/src/shared/EditMatcher.test.ts +123 -0
  104. package/src/shared/EditMatcher.ts +826 -0
  105. package/src/shared/FileScanner.test.ts +158 -0
  106. package/src/shared/FileScanner.ts +41 -0
  107. package/src/shared/Fs.ts +46 -0
  108. package/src/shared/FsErrors.ts +72 -0
  109. package/src/shared/FuzzyMatcher.test.ts +114 -0
  110. package/src/shared/FuzzyMatcher.ts +73 -0
  111. package/src/shared/GitignoreFilter.test.ts +64 -0
  112. package/src/shared/GitignoreFilter.ts +142 -0
  113. package/src/shared/GlobExclusions.ts +23 -0
  114. package/src/shared/Levenshtein.ts +33 -0
  115. package/src/shared/Lines.test.ts +25 -0
  116. package/src/shared/Lines.ts +77 -0
  117. package/src/shared/McpClient.test.ts +235 -0
  118. package/src/shared/McpClient.ts +406 -0
  119. package/src/shared/OutputBudget.test.ts +99 -0
  120. package/src/shared/OutputBudget.ts +79 -0
  121. package/src/shared/Paths.test.ts +51 -0
  122. package/src/shared/Paths.ts +52 -0
  123. package/src/shared/PimSettings.test.ts +90 -0
  124. package/src/shared/PimSettings.ts +124 -0
  125. package/src/shared/Renderer.test.ts +190 -0
  126. package/src/shared/Renderer.ts +256 -0
  127. package/src/shared/SpillCache.test.ts +94 -0
  128. package/src/shared/SpillCache.ts +89 -0
  129. package/src/shared/Tools.test.ts +392 -0
  130. package/src/shared/Tools.ts +636 -0
  131. package/src/telegram/Bot.ts +198 -0
  132. package/src/telegram/Commands.ts +721 -0
  133. package/src/telegram/Config.test.ts +275 -0
  134. package/src/telegram/Config.ts +162 -0
  135. package/src/telegram/Markdown.test.ts +143 -0
  136. package/src/telegram/Markdown.ts +177 -0
  137. package/src/telegram/Message.ts +211 -0
  138. package/src/telegram/Renderer.test.ts +216 -0
  139. package/src/telegram/Renderer.ts +713 -0
  140. package/src/telegram/SendFileSchema.ts +19 -0
  141. package/src/telegram/SendFileTool.ts +94 -0
  142. package/src/telegram/Session.ts +579 -0
  143. package/src/telegram/SessionRegistry.test.ts +89 -0
  144. package/src/telegram/SessionRegistry.ts +170 -0
  145. package/src/telegram/Supervisor.ts +357 -0
  146. package/src/telegram/TaskScheduler.test.ts +278 -0
  147. package/src/telegram/TaskScheduler.ts +293 -0
  148. package/src/telegram/TaskSchema.ts +88 -0
  149. package/src/telegram/TaskStore.ts +73 -0
  150. package/src/telegram/TaskTool.test.ts +179 -0
  151. package/src/telegram/TaskTool.ts +159 -0
  152. package/src/telegram/TypingIndicator.ts +43 -0
  153. package/src/telegram/index.ts +32 -0
  154. package/src/themes/pim-dark.json +84 -0
  155. package/src/themes/pim-light.json +84 -0
@@ -0,0 +1,511 @@
1
+ export type MarkdownSnapshotTextNode = {
2
+ readonly type: "text";
3
+ readonly text: string;
4
+ };
5
+
6
+ export type MarkdownSnapshotElementNode = {
7
+ readonly type: "element";
8
+ readonly tagName: string;
9
+ readonly hidden?: boolean;
10
+ readonly ariaHidden?: string | null;
11
+ readonly attributes?: readonly {
12
+ readonly name: string;
13
+ readonly value: string;
14
+ }[];
15
+ readonly children?: readonly MarkdownSnapshotNode[];
16
+ readonly textContent?: string;
17
+ };
18
+
19
+ export type MarkdownSnapshotNode =
20
+ | MarkdownSnapshotTextNode
21
+ | MarkdownSnapshotElementNode;
22
+
23
+ type BrowserMarkdownSnapshotRenderer = (
24
+ root: MarkdownSnapshotNode,
25
+ baseUrl: string
26
+ ) => string;
27
+
28
+ type BrowserMarkdownSnapshot = {
29
+ readonly title: string;
30
+ readonly url: string;
31
+ readonly content: string;
32
+ };
33
+
34
+ export function createMarkdownSnapshotScript(): string {
35
+ return `(${captureMarkdownSnapshot.toString()})(${renderMarkdownSnapshotTree.toString()})`;
36
+ }
37
+
38
+ function captureMarkdownSnapshot(
39
+ renderMarkdownSnapshotTree: BrowserMarkdownSnapshotRenderer
40
+ ): BrowserMarkdownSnapshot {
41
+ function textFallback(): BrowserMarkdownSnapshot {
42
+ const content = (document.body?.innerText || "")
43
+ .replace(/[ \t]+/g, " ")
44
+ .replace(/\n{3,}/g, "\n\n")
45
+ .trim();
46
+
47
+ return {
48
+ title: document.title,
49
+ url: location.href,
50
+ content,
51
+ };
52
+ }
53
+
54
+ function serializeNode(node: Node): MarkdownSnapshotNode | undefined {
55
+ if (node.nodeType === Node.TEXT_NODE) {
56
+ return { type: "text", text: node.textContent || "" };
57
+ }
58
+
59
+ if (!(node instanceof HTMLElement)) {
60
+ return undefined;
61
+ }
62
+
63
+ const tagName = node.tagName.toLowerCase();
64
+
65
+ if (
66
+ [
67
+ "script",
68
+ "style",
69
+ "noscript",
70
+ "svg",
71
+ "canvas",
72
+ "template",
73
+ "iframe",
74
+ ].includes(tagName)
75
+ ) {
76
+ return undefined;
77
+ }
78
+
79
+ return {
80
+ type: "element",
81
+ tagName,
82
+ hidden: Boolean(node.hidden) || node.hasAttribute("hidden"),
83
+ ariaHidden: node.getAttribute("aria-hidden"),
84
+ attributes: ["href", "alt"]
85
+ .map((name) => ({ name, value: node.getAttribute(name) }))
86
+ .filter(
87
+ (
88
+ attribute
89
+ ): attribute is { readonly name: string; readonly value: string } =>
90
+ attribute.value !== null
91
+ ),
92
+ children: Array.from(node.childNodes)
93
+ .map(serializeNode)
94
+ .filter((child): child is MarkdownSnapshotNode => child !== undefined),
95
+ textContent: node.textContent || "",
96
+ };
97
+ }
98
+
99
+ try {
100
+ const sourceRoot = document.body;
101
+
102
+ if (!(sourceRoot instanceof HTMLElement)) {
103
+ return textFallback();
104
+ }
105
+
106
+ const root = serializeNode(sourceRoot);
107
+
108
+ if (root === undefined) {
109
+ return textFallback();
110
+ }
111
+
112
+ return {
113
+ title: document.title,
114
+ url: location.href,
115
+ content: renderMarkdownSnapshotTree(root, location.href),
116
+ };
117
+ } catch {
118
+ return textFallback();
119
+ }
120
+ }
121
+
122
+ export function renderMarkdownSnapshotTree(
123
+ root: MarkdownSnapshotNode,
124
+ baseUrl: string
125
+ ): string {
126
+ const blockTags = new Set([
127
+ "address",
128
+ "article",
129
+ "aside",
130
+ "blockquote",
131
+ "dd",
132
+ "details",
133
+ "dialog",
134
+ "div",
135
+ "dl",
136
+ "dt",
137
+ "fieldset",
138
+ "figcaption",
139
+ "figure",
140
+ "footer",
141
+ "form",
142
+ "h1",
143
+ "h2",
144
+ "h3",
145
+ "h4",
146
+ "h5",
147
+ "h6",
148
+ "header",
149
+ "hr",
150
+ "li",
151
+ "main",
152
+ "nav",
153
+ "ol",
154
+ "p",
155
+ "pre",
156
+ "section",
157
+ "table",
158
+ "ul",
159
+ ]);
160
+
161
+ function renderNode(node: MarkdownSnapshotNode, depth: number): string {
162
+ if (node.type === "text") {
163
+ return normalizeInline(node.text || "");
164
+ }
165
+
166
+ if (node.type !== "element") {
167
+ return "";
168
+ }
169
+
170
+ const tag = String(node.tagName || "").toLowerCase();
171
+
172
+ if (node.hidden === true || node.ariaHidden === "true") {
173
+ return "";
174
+ }
175
+
176
+ switch (tag) {
177
+ case "h1":
178
+ return block("# " + renderInlineChildren(node));
179
+ case "h2":
180
+ return block("## " + renderInlineChildren(node));
181
+ case "h3":
182
+ return block("### " + renderInlineChildren(node));
183
+ case "h4":
184
+ return block("#### " + renderInlineChildren(node));
185
+ case "h5":
186
+ return block("##### " + renderInlineChildren(node));
187
+ case "h6":
188
+ return block("###### " + renderInlineChildren(node));
189
+ case "p":
190
+ return block(renderInlineChildren(node));
191
+ case "br":
192
+ return "\n";
193
+ case "hr":
194
+ return "\n---\n\n";
195
+ case "strong":
196
+ case "b":
197
+ return wrapInline("**", renderInlineChildren(node));
198
+ case "em":
199
+ case "i":
200
+ return wrapInline("*", renderInlineChildren(node));
201
+ case "code":
202
+ return renderCode(node);
203
+ case "pre":
204
+ return renderPre(node);
205
+ case "a":
206
+ return renderLink(node);
207
+ case "img":
208
+ return renderImage(node);
209
+ case "ul":
210
+ return renderList(node, false, depth);
211
+ case "ol":
212
+ return renderList(node, true, depth);
213
+ case "blockquote":
214
+ return renderBlockquote(node, depth);
215
+ case "table":
216
+ return renderTable(node);
217
+ case "thead":
218
+ case "tbody":
219
+ case "tfoot":
220
+ case "tr":
221
+ case "th":
222
+ case "td":
223
+ return "";
224
+ default: {
225
+ const rendered = renderChildren(node, depth);
226
+ return blockTags.has(tag) ? block(rendered) : rendered;
227
+ }
228
+ }
229
+ }
230
+
231
+ function renderChildren(
232
+ node: MarkdownSnapshotElementNode,
233
+ depth: number
234
+ ): string {
235
+ return joinMarkdown(
236
+ getChildren(node).map((child) => renderNode(child, depth))
237
+ );
238
+ }
239
+
240
+ function renderInlineChildren(node: MarkdownSnapshotElementNode): string {
241
+ return normalizeInline(
242
+ getChildren(node)
243
+ .map((child) => renderInlineNode(child))
244
+ .filter((part) => part.trim().length > 0)
245
+ .join(" ")
246
+ );
247
+ }
248
+
249
+ function renderInlineNode(node: MarkdownSnapshotNode): string {
250
+ if (node.type === "text") {
251
+ return normalizeInline(node.text || "");
252
+ }
253
+
254
+ if (node.type !== "element") {
255
+ return "";
256
+ }
257
+
258
+ const tag = String(node.tagName || "").toLowerCase();
259
+
260
+ if (node.hidden === true || node.ariaHidden === "true") {
261
+ return "";
262
+ }
263
+
264
+ switch (tag) {
265
+ case "br":
266
+ return "\n";
267
+ case "strong":
268
+ case "b":
269
+ return wrapInline("**", renderInlineChildren(node));
270
+ case "em":
271
+ case "i":
272
+ return wrapInline("*", renderInlineChildren(node));
273
+ case "code":
274
+ return renderCode(node);
275
+ case "a":
276
+ return renderLink(node);
277
+ case "img":
278
+ return renderImage(node);
279
+ default:
280
+ return renderInlineChildren(node);
281
+ }
282
+ }
283
+
284
+ function renderList(
285
+ list: MarkdownSnapshotElementNode,
286
+ ordered: boolean,
287
+ depth: number
288
+ ): string {
289
+ const items = getChildren(list).filter(
290
+ (child): child is MarkdownSnapshotElementNode =>
291
+ child.type === "element" && child.tagName.toLowerCase() === "li"
292
+ );
293
+ const indent = " ".repeat(depth);
294
+ const lines: string[] = [];
295
+
296
+ items.forEach((item, index) => {
297
+ const marker = ordered ? String(index + 1) + ". " : "- ";
298
+ const body = renderChildren(item, depth + 1).trim();
299
+
300
+ if (body.length === 0) {
301
+ return;
302
+ }
303
+
304
+ const bodyLines = body.split("\n");
305
+ lines.push(indent + marker + bodyLines[0]);
306
+
307
+ for (const line of bodyLines.slice(1)) {
308
+ lines.push(line.trim().length === 0 ? "" : indent + " " + line);
309
+ }
310
+ });
311
+
312
+ return lines.length === 0 ? "" : "\n" + lines.join("\n") + "\n\n";
313
+ }
314
+
315
+ function renderBlockquote(
316
+ node: MarkdownSnapshotElementNode,
317
+ depth: number
318
+ ): string {
319
+ const rendered = renderChildren(node, depth).trim();
320
+
321
+ if (rendered.length === 0) {
322
+ return "";
323
+ }
324
+
325
+ return (
326
+ rendered
327
+ .split("\n")
328
+ .map((line) => (line.trim().length === 0 ? ">" : "> " + line))
329
+ .join("\n") + "\n\n"
330
+ );
331
+ }
332
+
333
+ function renderTable(table: MarkdownSnapshotElementNode): string {
334
+ const rows = descendants(table, ["tr"])
335
+ .map((row) =>
336
+ descendants(row, ["th", "td"]).map((cell) =>
337
+ renderInlineChildren(cell).replace(/\|/g, "\\|")
338
+ )
339
+ )
340
+ .filter((row) => row.length > 0);
341
+
342
+ if (rows.length === 0) {
343
+ return "";
344
+ }
345
+
346
+ const width = Math.max(...rows.map((row) => row.length));
347
+ const normalizedRows = rows.map((row) => padRow(row, width));
348
+ const header = normalizedRows[0] ?? [];
349
+ const body = normalizedRows.slice(1);
350
+ const lines = [
351
+ "| " + header.join(" | ") + " |",
352
+ "| " + header.map(() => "---").join(" | ") + " |",
353
+ ...body.map((row) => "| " + row.join(" | ") + " |"),
354
+ ];
355
+
356
+ return lines.join("\n") + "\n\n";
357
+ }
358
+
359
+ function padRow(row: readonly string[], width: number): string[] {
360
+ return Array.from({ length: width }, (_, index) => row[index] || "");
361
+ }
362
+
363
+ function renderPre(node: MarkdownSnapshotElementNode): string {
364
+ const text = (node.textContent || "").replace(/^\n+|\n+$/g, "");
365
+
366
+ if (text.length === 0) {
367
+ return "";
368
+ }
369
+
370
+ const fence = String.fromCharCode(96).repeat(3);
371
+ return fence + "\n" + text + "\n" + fence + "\n\n";
372
+ }
373
+
374
+ function renderCode(node: MarkdownSnapshotElementNode): string {
375
+ const backtick = String.fromCharCode(96);
376
+ const text = normalizeInline(node.textContent || textContent(node)).replace(
377
+ new RegExp(backtick, "g"),
378
+ "\\" + backtick
379
+ );
380
+ return text.length === 0 ? "" : backtick + text + backtick;
381
+ }
382
+
383
+ function renderLink(node: MarkdownSnapshotElementNode): string {
384
+ const text = renderInlineChildren(node);
385
+ const href = getAttribute(node, "href");
386
+
387
+ if (text.length === 0) {
388
+ return "";
389
+ }
390
+
391
+ if (
392
+ href === undefined ||
393
+ href.trim().length === 0 ||
394
+ href.trim().toLowerCase().startsWith("javascript:")
395
+ ) {
396
+ return text;
397
+ }
398
+
399
+ try {
400
+ return (
401
+ "[" +
402
+ text.replace(/[[\]]/g, "") +
403
+ "](" +
404
+ new URL(href, baseUrl).href +
405
+ ")"
406
+ );
407
+ } catch {
408
+ return text;
409
+ }
410
+ }
411
+
412
+ function renderImage(node: MarkdownSnapshotElementNode): string {
413
+ const alt = normalizeInline(getAttribute(node, "alt") || "");
414
+
415
+ if (alt.length === 0) {
416
+ return "";
417
+ }
418
+
419
+ return "![" + alt.replace(/[[\]]/g, "") + "]";
420
+ }
421
+
422
+ function block(text: string): string {
423
+ const trimmed = text.trim();
424
+ return trimmed.length === 0 ? "" : trimmed + "\n\n";
425
+ }
426
+
427
+ function wrapInline(marker: string, text: string): string {
428
+ return text.length === 0 ? "" : marker + text + marker;
429
+ }
430
+
431
+ function normalizeInline(text: string): string {
432
+ return String(text).replace(/\s+/g, " ");
433
+ }
434
+
435
+ function joinMarkdown(parts: readonly string[]): string {
436
+ let output = "";
437
+
438
+ for (const part of parts) {
439
+ if (part.length === 0) {
440
+ continue;
441
+ }
442
+
443
+ if (
444
+ output.length > 0 &&
445
+ !output.endsWith("\n") &&
446
+ !part.startsWith("\n")
447
+ ) {
448
+ output += " ";
449
+ }
450
+
451
+ output += part;
452
+ }
453
+
454
+ return output;
455
+ }
456
+
457
+ function getChildren(
458
+ node: MarkdownSnapshotElementNode
459
+ ): readonly MarkdownSnapshotNode[] {
460
+ return Array.isArray(node.children) ? node.children : [];
461
+ }
462
+
463
+ function getAttribute(
464
+ node: MarkdownSnapshotElementNode,
465
+ name: string
466
+ ): string | undefined {
467
+ const attributes = Array.isArray(node.attributes) ? node.attributes : [];
468
+ const attribute = attributes.find((item) => item.name === name);
469
+ return typeof attribute?.value === "string" ? attribute.value : undefined;
470
+ }
471
+
472
+ function textContent(node: MarkdownSnapshotNode): string {
473
+ if (node.type === "text") {
474
+ return node.text || "";
475
+ }
476
+
477
+ return getChildren(node)
478
+ .map((child) => textContent(child))
479
+ .join("");
480
+ }
481
+
482
+ function descendants(
483
+ node: MarkdownSnapshotElementNode,
484
+ tags: readonly string[]
485
+ ): MarkdownSnapshotElementNode[] {
486
+ const matches: MarkdownSnapshotElementNode[] = [];
487
+ const tagSet = new Set(tags);
488
+
489
+ function visit(current: MarkdownSnapshotElementNode): void {
490
+ for (const child of getChildren(current)) {
491
+ if (child.type !== "element") {
492
+ continue;
493
+ }
494
+
495
+ if (tagSet.has(String(child.tagName || "").toLowerCase())) {
496
+ matches.push(child);
497
+ }
498
+
499
+ visit(child);
500
+ }
501
+ }
502
+
503
+ visit(node);
504
+ return matches;
505
+ }
506
+
507
+ return renderNode(root, 0)
508
+ .replace(/[ \t]+\n/g, "\n")
509
+ .replace(/\n{3,}/g, "\n\n")
510
+ .trim();
511
+ }