explorbot 0.4.9 → 0.5.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.
Files changed (144) hide show
  1. package/README.md +4 -1
  2. package/bin/mdq.ts +18 -0
  3. package/boat/api-tester/src/ai/chief.ts +72 -0
  4. package/boat/api-tester/src/api-client.ts +37 -0
  5. package/boat/prima/src/prima.ts +41 -2
  6. package/dist/bin/mdq.js +19 -0
  7. package/dist/boat/api-tester/src/ai/chief.js +69 -0
  8. package/dist/boat/api-tester/src/api-client.js +26 -0
  9. package/dist/boat/prima/src/prima.js +42 -2
  10. package/dist/package.json +3 -2
  11. package/dist/src/action.js +4 -2
  12. package/dist/src/ai/agent.d.ts +3 -1
  13. package/dist/src/ai/judge-provider.d.ts +17 -0
  14. package/dist/src/ai/judge-provider.js +56 -0
  15. package/dist/src/ai/judge-tool.d.ts +2 -0
  16. package/dist/src/ai/judge-tool.js +33 -0
  17. package/dist/src/ai/judge.d.ts +28 -0
  18. package/dist/src/ai/judge.js +71 -0
  19. package/dist/src/ai/navigator.d.ts +3 -1
  20. package/dist/src/ai/navigator.js +24 -28
  21. package/dist/src/ai/pilot.d.ts +4 -0
  22. package/dist/src/ai/pilot.js +67 -9
  23. package/dist/src/ai/planner.js +9 -6
  24. package/dist/src/ai/provider.d.ts +4 -1
  25. package/dist/src/ai/provider.js +52 -7
  26. package/dist/src/ai/rerunner.js +7 -0
  27. package/dist/src/ai/researcher/deep-analysis.js +2 -2
  28. package/dist/src/ai/researcher/locators.js +2 -2
  29. package/dist/src/ai/researcher/pagination.js +1 -1
  30. package/dist/src/ai/researcher/research-result.js +2 -2
  31. package/dist/src/ai/researcher.js +1 -1
  32. package/dist/src/ai/task-agent.d.ts +2 -0
  33. package/dist/src/ai/task-agent.js +3 -1
  34. package/dist/src/ai/tester.js +19 -15
  35. package/dist/src/ai/tools.d.ts +4 -3
  36. package/dist/src/ai/tools.js +35 -7
  37. package/dist/src/api/request-result.js +2 -1
  38. package/dist/src/api/xhr-capture.js +2 -1
  39. package/dist/src/command-handler.d.ts +1 -0
  40. package/dist/src/command-handler.js +24 -3
  41. package/dist/src/commands/base-command.d.ts +5 -0
  42. package/dist/src/commands/base-command.js +3 -0
  43. package/dist/src/commands/explore-command.d.ts +2 -1
  44. package/dist/src/commands/explore-command.js +12 -1
  45. package/dist/src/commands/freesail-command.js +8 -2
  46. package/dist/src/commands/init-command.js +1 -1
  47. package/dist/src/commands/navigate-command.d.ts +2 -1
  48. package/dist/src/commands/navigate-command.js +6 -0
  49. package/dist/src/commands/plan-load-command.d.ts +2 -1
  50. package/dist/src/commands/plan-load-command.js +4 -0
  51. package/dist/src/commands/plans-command.d.ts +3 -9
  52. package/dist/src/commands/plans-command.js +11 -21
  53. package/dist/src/commands/rerun-command.d.ts +2 -1
  54. package/dist/src/commands/rerun-command.js +5 -1
  55. package/dist/src/commands/research-command.d.ts +2 -1
  56. package/dist/src/commands/research-command.js +6 -0
  57. package/dist/src/commands/test-command.d.ts +2 -1
  58. package/dist/src/commands/test-command.js +4 -1
  59. package/dist/src/components/Autocomplete.js +26 -12
  60. package/dist/src/components/InputReadline.js +10 -1
  61. package/dist/src/config.d.ts +6 -0
  62. package/dist/src/experience-tracker.js +4 -3
  63. package/dist/src/explorbot.d.ts +3 -0
  64. package/dist/src/explorbot.js +8 -0
  65. package/dist/src/explorer.js +2 -3
  66. package/dist/src/knowledge-tracker.js +1 -1
  67. package/dist/src/reporter.js +8 -4
  68. package/dist/src/state-manager.d.ts +2 -0
  69. package/dist/src/state-manager.js +16 -0
  70. package/dist/src/test-plan.d.ts +11 -0
  71. package/dist/src/test-plan.js +54 -2
  72. package/dist/src/utils/aria-ref.js +1 -1
  73. package/dist/src/utils/logger.js +9 -3
  74. package/dist/src/utils/markdown-query.d.ts +1 -48
  75. package/dist/src/utils/markdown-query.js +1 -444
  76. package/dist/src/utils/mdq/cli.d.ts +6 -0
  77. package/dist/src/utils/mdq/cli.js +122 -0
  78. package/dist/src/utils/mdq/edit.d.ts +24 -0
  79. package/dist/src/utils/mdq/edit.js +147 -0
  80. package/dist/src/utils/mdq/query.d.ts +118 -0
  81. package/dist/src/utils/mdq/query.js +451 -0
  82. package/dist/src/utils/step-analyzer.d.ts +3 -0
  83. package/dist/src/utils/step-analyzer.js +7 -0
  84. package/dist/src/utils/strings.d.ts +1 -0
  85. package/dist/src/utils/strings.js +7 -0
  86. package/dist/src/utils/test-files.d.ts +1 -0
  87. package/dist/src/utils/test-files.js +5 -2
  88. package/dist/src/utils/url-matcher.d.ts +1 -0
  89. package/dist/src/utils/url-matcher.js +7 -0
  90. package/docs/api-testing/planning.md +1 -1
  91. package/docs/superpowers/plans/2026-09-15-mdq-package.md +130 -94
  92. package/docs/superpowers/specs/2026-09-18-judge-decision-model-design.md +79 -0
  93. package/package.json +3 -2
  94. package/src/action.ts +4 -2
  95. package/src/ai/agent.ts +3 -1
  96. package/src/ai/judge-provider.ts +62 -0
  97. package/src/ai/judge-tool.ts +35 -0
  98. package/src/ai/judge.ts +75 -0
  99. package/src/ai/navigator.ts +24 -27
  100. package/src/ai/pilot.ts +68 -9
  101. package/src/ai/planner.ts +9 -6
  102. package/src/ai/provider.ts +51 -7
  103. package/src/ai/rerunner.ts +4 -0
  104. package/src/ai/researcher/deep-analysis.ts +2 -2
  105. package/src/ai/researcher/locators.ts +2 -2
  106. package/src/ai/researcher/pagination.ts +1 -1
  107. package/src/ai/researcher/research-result.ts +2 -2
  108. package/src/ai/researcher.ts +1 -1
  109. package/src/ai/task-agent.ts +4 -1
  110. package/src/ai/tester.ts +19 -16
  111. package/src/ai/tools.ts +42 -7
  112. package/src/api/request-result.ts +2 -1
  113. package/src/api/xhr-capture.ts +2 -1
  114. package/src/command-handler.ts +28 -3
  115. package/src/commands/base-command.ts +9 -0
  116. package/src/commands/explore-command.ts +15 -2
  117. package/src/commands/freesail-command.ts +9 -2
  118. package/src/commands/init-command.ts +1 -1
  119. package/src/commands/navigate-command.ts +8 -1
  120. package/src/commands/plan-load-command.ts +6 -1
  121. package/src/commands/plans-command.ts +13 -29
  122. package/src/commands/rerun-command.ts +7 -2
  123. package/src/commands/research-command.ts +8 -1
  124. package/src/commands/test-command.ts +6 -2
  125. package/src/components/Autocomplete.tsx +39 -10
  126. package/src/components/InputReadline.tsx +10 -1
  127. package/src/config.ts +1 -0
  128. package/src/experience-tracker.ts +4 -3
  129. package/src/explorbot.ts +8 -0
  130. package/src/explorer.ts +2 -2
  131. package/src/knowledge-tracker.ts +1 -1
  132. package/src/reporter.ts +8 -4
  133. package/src/state-manager.ts +16 -0
  134. package/src/test-plan.ts +67 -2
  135. package/src/utils/aria-ref.ts +1 -1
  136. package/src/utils/logger.ts +7 -2
  137. package/src/utils/markdown-query.ts +1 -519
  138. package/src/utils/mdq/cli.ts +118 -0
  139. package/src/utils/mdq/edit.ts +158 -0
  140. package/src/utils/mdq/query.ts +556 -0
  141. package/src/utils/step-analyzer.ts +8 -0
  142. package/src/utils/strings.ts +7 -0
  143. package/src/utils/test-files.ts +5 -2
  144. package/src/utils/url-matcher.ts +7 -0
@@ -96,6 +96,7 @@ export declare class Test extends Task {
96
96
  endTime?: number;
97
97
  resetCount: number;
98
98
  appliedExperience: AppliedExperience[];
99
+ preparedData: string[];
99
100
  constructor(scenario: string, priority: 'critical' | 'important' | 'high' | 'normal' | 'low', expectedOutcome: string | string[], startUrl: string, plannedSteps?: string[]);
100
101
  getVisitedUrls({ localOnly }?: {
101
102
  localOnly?: boolean;
@@ -147,6 +148,7 @@ export declare class Plan {
147
148
  get allSuccessful(): boolean;
148
149
  get allFailed(): boolean;
149
150
  updateStatus(): void;
151
+ static listFiles(plansDir: string): PlanFile[];
150
152
  static resolveFile(file: string, plansDir?: string): string | null;
151
153
  static loadFromFile(file: string, plansDir?: string): Plan | null;
152
154
  static fromMarkdown(filePath: string): Plan;
@@ -178,4 +180,13 @@ interface AppliedExperience {
178
180
  url: string;
179
181
  content: string;
180
182
  }
183
+ export interface PlanFile {
184
+ name: string;
185
+ path: string;
186
+ modifiedAt: number;
187
+ title: string;
188
+ url: string;
189
+ testCount: number;
190
+ label: string;
191
+ }
181
192
  export {};
@@ -1,10 +1,12 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { existsSync } from 'node:fs';
2
+ import { existsSync, readdirSync, statSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { listSitePlanDirs } from "./global-config.js";
5
- import { tag } from "./utils/logger.js";
5
+ import { createDebug, pluralize, tag } from "./utils/logger.js";
6
+ import { truncateMiddle } from "./utils/strings.js";
6
7
  import { parsePlanFromMarkdown, planToAiContext, savePlanToMarkdown, savePlansToMarkdown } from "./utils/test-plan-markdown.js";
7
8
  import { uniqSessionName } from "./utils/unique-names.js";
9
+ import { extractStatePath } from "./utils/url-matcher.js";
8
10
  export const TestResult = {
9
11
  PASSED: 'passed',
10
12
  FAILED: 'failed',
@@ -194,6 +196,7 @@ export class Test extends Task {
194
196
  endTime;
195
197
  resetCount = 0;
196
198
  appliedExperience = [];
199
+ preparedData = [];
197
200
  constructor(scenario, priority, expectedOutcome, startUrl, plannedSteps = []) {
198
201
  super(scenario, startUrl);
199
202
  this.scenario = scenario;
@@ -397,6 +400,37 @@ export class Plan {
397
400
  return this.tests.length > 0 && this.tests.every((test) => test.hasFailed);
398
401
  }
399
402
  updateStatus() { }
403
+ static listFiles(plansDir) {
404
+ if (!existsSync(plansDir))
405
+ return [];
406
+ const files = readdirSync(plansDir)
407
+ .filter((file) => file.endsWith('.md'))
408
+ .map((file) => {
409
+ const filePath = path.join(plansDir, file);
410
+ const modifiedAt = statSync(filePath).mtimeMs;
411
+ const plan = readPlanFile(filePath, modifiedAt);
412
+ return {
413
+ name: file,
414
+ path: filePath,
415
+ modifiedAt,
416
+ title: plan.title,
417
+ url: extractStatePath(plan.startUrl || ''),
418
+ testCount: plan.tests.length,
419
+ label: '',
420
+ };
421
+ })
422
+ .sort((left, right) => right.modifiedAt - left.modifiedAt);
423
+ if (files.length === 0)
424
+ return files;
425
+ const nameWidth = Math.min(MAX_PLAN_NAME_WIDTH, Math.max(...files.map((file) => file.name.length)));
426
+ const urlWidth = Math.min(MAX_PLAN_URL_WIDTH, Math.max(...files.map((file) => file.url.length)));
427
+ for (const file of files) {
428
+ const name = truncateMiddle(file.name, nameWidth).padEnd(nameWidth);
429
+ const url = truncateMiddle(file.url, urlWidth).padEnd(urlWidth);
430
+ file.label = `${name} ${url} ${file.testCount} ${pluralize(file.testCount, 'test')}`;
431
+ }
432
+ return files;
433
+ }
400
434
  static resolveFile(file, plansDir) {
401
435
  const names = [file];
402
436
  if (!file.endsWith('.md'))
@@ -464,3 +498,21 @@ export class Plan {
464
498
  return planToAiContext(this, options);
465
499
  }
466
500
  }
501
+ const debugLog = createDebug('explorbot:test-plan');
502
+ const MAX_PLAN_NAME_WIDTH = 38;
503
+ const MAX_PLAN_URL_WIDTH = 26;
504
+ const planFileCache = new Map();
505
+ function readPlanFile(filePath, modifiedAt) {
506
+ const cached = planFileCache.get(filePath);
507
+ if (cached?.modifiedAt === modifiedAt)
508
+ return cached.plan;
509
+ let plan = new Plan(path.basename(filePath));
510
+ try {
511
+ plan = parsePlanFromMarkdown(filePath);
512
+ }
513
+ catch (error) {
514
+ debugLog(`Failed to read plan file ${filePath}:`, error);
515
+ }
516
+ planFileCache.set(filePath, { modifiedAt, plan });
517
+ return plan;
518
+ }
@@ -1,5 +1,5 @@
1
1
  import { WebElement } from "./web-element.js";
2
- const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
2
+ const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=((?:f\d+)?e\d+)\]/;
3
3
  const ARIA_REF_PATTERN = /^(f\d+)?e\d+$/i;
4
4
  const REF_ROLES = { a: 'link', button: 'button', select: 'combobox', textarea: 'textbox' };
5
5
  export function ariaRefSelector(ref) {
@@ -75,8 +75,14 @@ class ConsoleDestination {
75
75
  return;
76
76
  if (entry.type === 'operation' && !this.verboseMode)
77
77
  return;
78
- if (entry.type === 'step' && !this.verboseMode && this.recentSteps.shouldSuppress(entry.content))
79
- return;
78
+ if (entry.type === 'step' && !this.verboseMode) {
79
+ const step = entry.originalArgs?.[0];
80
+ const error = entry.originalArgs?.[1];
81
+ if (error || step?.failed === true || step?.status === 'failed')
82
+ return;
83
+ if (this.recentSteps.shouldSuppress(entry.content))
84
+ return;
85
+ }
80
86
  let content = entry.content;
81
87
  if (entry.type === 'multiline' || entry.type === 'details') {
82
88
  const cleaned = stripAnsi(dedent(entry.content));
@@ -189,7 +195,7 @@ class SpanDestination {
189
195
  if (!step?.toCode) {
190
196
  return;
191
197
  }
192
- const stepName = step?.name ? `I.${step.name}` : 'I.step';
198
+ const stepName = step?.title ? `I.${step.title}` : 'I.step';
193
199
  const stepInput = typeof step?.toCode === 'function' ? step.toCode() : entry.content;
194
200
  const errorFromStep = step?.error;
195
201
  const errorMessage = stepError && typeof stepError === 'object' && 'message' in stepError && typeof stepError.message === 'string'
@@ -1,48 +1 @@
1
- import { type Token } from 'marked';
2
- export type SelectorType = 'section' | 'section1' | 'section2' | 'section3' | 'section4' | 'section5' | 'section6' | 'table' | 'heading' | 'paragraph' | 'list' | 'item' | 'code' | 'blockquote' | 'hr' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
3
- export interface TextMatcher {
4
- mode: 'exact' | 'contains' | 'regex';
5
- value: string;
6
- negated: boolean;
7
- }
8
- export interface QuerySegment {
9
- selector: SelectorType;
10
- textMatch?: TextMatcher;
11
- index: number | null;
12
- slice: {
13
- from?: number;
14
- to?: number;
15
- } | null;
16
- }
17
- export interface MatchedRange {
18
- token: Token;
19
- start: number;
20
- length: number;
21
- innerTokens?: MatchedRange[];
22
- }
23
- export declare function parseQuery(input: string): QuerySegment[];
24
- export declare class MarkdownQuery {
25
- source: string;
26
- matches: MatchedRange[];
27
- constructor(source: string, matches?: MatchedRange[]);
28
- query(selector: string): MarkdownQuery;
29
- text(): string;
30
- get(): string;
31
- toJson(): Record<string, string>[];
32
- keyValue(): Record<string, string>;
33
- setKeyValue(key: string, value: string | null): string;
34
- replace(content: string): string;
35
- replaceEach(replacer: (match: MarkdownQuery, index: number) => string): string;
36
- count(): number;
37
- first(): MarkdownQuery;
38
- last(): MarkdownQuery;
39
- before(): MarkdownQuery;
40
- after(): MarkdownQuery;
41
- each(): MarkdownQuery[];
42
- meta(): Array<{
43
- type: string;
44
- depth: number | null;
45
- text: string;
46
- }>;
47
- }
48
- export declare function mdq(source: string): MarkdownQuery;
1
+ export * from './mdq/query.js';
@@ -1,444 +1 @@
1
- import { marked } from 'marked';
2
- export function parseQuery(input) {
3
- const segments = [];
4
- let pos = 0;
5
- function peek() {
6
- return pos < input.length ? input[pos] : '';
7
- }
8
- function advance() {
9
- return input[pos++] || '';
10
- }
11
- function skipWhitespace() {
12
- while (pos < input.length && /\s/.test(input[pos]))
13
- pos++;
14
- }
15
- function readIdentifier() {
16
- const start = pos;
17
- while (pos < input.length && /[a-zA-Z_\d]/.test(input[pos]))
18
- pos++;
19
- return input.slice(start, pos);
20
- }
21
- function readQuotedString() {
22
- const quote = advance();
23
- let result = '';
24
- while (pos < input.length && input[pos] !== quote) {
25
- if (input[pos] === '\\') {
26
- pos++;
27
- result += input[pos] || '';
28
- }
29
- else {
30
- result += input[pos];
31
- }
32
- pos++;
33
- }
34
- if (pos < input.length)
35
- pos++;
36
- return result;
37
- }
38
- function readUntilAny(chars) {
39
- const start = pos;
40
- while (pos < input.length && !chars.includes(input[pos]))
41
- pos++;
42
- return input.slice(start, pos);
43
- }
44
- function parseTextMatcher() {
45
- let negated = false;
46
- if (peek() === '!') {
47
- negated = true;
48
- advance();
49
- }
50
- if (peek() === '~') {
51
- advance();
52
- const value = readQuotedString();
53
- return { mode: 'contains', value, negated };
54
- }
55
- if (peek() === '/') {
56
- advance();
57
- let value = '';
58
- while (pos < input.length && input[pos] !== '/') {
59
- value += input[pos];
60
- pos++;
61
- }
62
- if (pos < input.length)
63
- pos++;
64
- while (pos < input.length && /[gimsuy]/.test(input[pos]))
65
- pos++;
66
- return { mode: 'regex', value, negated };
67
- }
68
- const value = readQuotedString();
69
- return { mode: 'exact', value, negated };
70
- }
71
- while (pos < input.length) {
72
- skipWhitespace();
73
- if (pos >= input.length)
74
- break;
75
- const selector = readIdentifier();
76
- if (!selector) {
77
- pos++;
78
- continue;
79
- }
80
- const segment = {
81
- selector: selector,
82
- index: null,
83
- slice: null,
84
- };
85
- if (peek() === '(') {
86
- advance();
87
- segment.textMatch = parseTextMatcher();
88
- if (peek() === ')')
89
- advance();
90
- }
91
- while (peek() === '[') {
92
- advance();
93
- const content = readUntilAny(']');
94
- if (/^-?\d*(:-?\d*)?$/.test(content) && content !== '') {
95
- if (content.includes(':')) {
96
- const colonIdx = content.indexOf(':');
97
- const fromStr = content.slice(0, colonIdx);
98
- const toStr = content.slice(colonIdx + 1);
99
- segment.slice = {
100
- from: fromStr ? Number.parseInt(fromStr, 10) : undefined,
101
- to: toStr ? Number.parseInt(toStr, 10) : undefined,
102
- };
103
- }
104
- else {
105
- segment.index = Number.parseInt(content, 10);
106
- }
107
- }
108
- if (peek() === ']')
109
- advance();
110
- }
111
- segments.push(segment);
112
- }
113
- return segments;
114
- }
115
- function matchText(text, matcher) {
116
- let result;
117
- switch (matcher.mode) {
118
- case 'exact':
119
- result = text === matcher.value;
120
- break;
121
- case 'contains':
122
- result = text.includes(matcher.value);
123
- break;
124
- case 'regex':
125
- result = new RegExp(matcher.value, 'i').test(text);
126
- break;
127
- default:
128
- result = false;
129
- }
130
- return matcher.negated ? !result : result;
131
- }
132
- function entryKey(line) {
133
- const separator = line.indexOf(':');
134
- if (separator < 1)
135
- return null;
136
- return line.slice(0, separator).trim().toLowerCase();
137
- }
138
- function getTokenText(token) {
139
- const t = token;
140
- switch (token.type) {
141
- case 'heading':
142
- case 'paragraph':
143
- case 'code':
144
- case 'blockquote':
145
- case 'list_item':
146
- return t.text || '';
147
- case 'table':
148
- return (t.header || []).map((h) => h.text).join(', ');
149
- default:
150
- return '';
151
- }
152
- }
153
- function getHeadingDepth(selector) {
154
- const match = selector.match(/^h([1-6])$/);
155
- return match ? Number.parseInt(match[1], 10) : null;
156
- }
157
- function isSectionSelector(selector) {
158
- return /^section\d?$/.test(selector);
159
- }
160
- function getSectionDepth(selector) {
161
- const match = selector.match(/^section([1-6])$/);
162
- return match ? Number.parseInt(match[1], 10) : null;
163
- }
164
- function selectorToTokenType(selector) {
165
- if (/^h[1-6]$/.test(selector))
166
- return 'heading';
167
- const map = {
168
- heading: 'heading',
169
- paragraph: 'paragraph',
170
- table: 'table',
171
- code: 'code',
172
- list: 'list',
173
- blockquote: 'blockquote',
174
- hr: 'hr',
175
- item: 'list_item',
176
- };
177
- return map[selector] || null;
178
- }
179
- function buildTokenIndex(source) {
180
- const tokens = marked.lexer(source);
181
- const ranges = [];
182
- let offset = 0;
183
- for (const token of tokens) {
184
- const raw = token.raw || '';
185
- ranges.push({ token, start: offset, length: raw.length });
186
- offset += raw.length;
187
- }
188
- return ranges;
189
- }
190
- function computeSections(candidates, segment) {
191
- const sectionDepth = getSectionDepth(segment.selector);
192
- const sections = [];
193
- for (let i = 0; i < candidates.length; i++) {
194
- const range = candidates[i];
195
- if (range.token.type !== 'heading')
196
- continue;
197
- const heading = range.token;
198
- if (sectionDepth !== null && heading.depth !== sectionDepth)
199
- continue;
200
- if (segment.textMatch && !matchText(heading.text, segment.textMatch))
201
- continue;
202
- const depth = heading.depth;
203
- const innerTokens = [];
204
- let endOffset = range.start + range.length;
205
- for (let j = i + 1; j < candidates.length; j++) {
206
- const nextRange = candidates[j];
207
- if (nextRange.token.type === 'heading' && nextRange.token.depth <= depth)
208
- break;
209
- innerTokens.push(nextRange);
210
- endOffset = nextRange.start + nextRange.length;
211
- }
212
- sections.push({
213
- token: range.token,
214
- start: range.start,
215
- length: endOffset - range.start,
216
- innerTokens,
217
- });
218
- }
219
- return sections;
220
- }
221
- function extractListItems(candidates) {
222
- const items = [];
223
- for (const range of candidates) {
224
- if (range.token.type !== 'list')
225
- continue;
226
- const list = range.token;
227
- const listRaw = range.token.raw;
228
- let searchFrom = 0;
229
- for (const item of list.items) {
230
- const itemRaw = item.raw;
231
- const idx = listRaw.indexOf(itemRaw, searchFrom);
232
- if (idx === -1)
233
- continue;
234
- items.push({
235
- token: item,
236
- start: range.start + idx,
237
- length: itemRaw.length,
238
- });
239
- searchFrom = idx + itemRaw.length;
240
- }
241
- }
242
- return items;
243
- }
244
- function applyIndexSlice(matches, segment) {
245
- if (segment.index !== null) {
246
- const idx = segment.index < 0 ? matches.length + segment.index : segment.index;
247
- return idx >= 0 && idx < matches.length ? [matches[idx]] : [];
248
- }
249
- if (segment.slice) {
250
- const { from, to } = segment.slice;
251
- return matches.slice(from, to);
252
- }
253
- return matches;
254
- }
255
- function expandSectionRanges(matches) {
256
- let hasSection = false;
257
- for (const m of matches) {
258
- if (m.innerTokens) {
259
- hasSection = true;
260
- break;
261
- }
262
- }
263
- if (!hasSection)
264
- return matches;
265
- const expanded = [];
266
- for (const m of matches) {
267
- if (m.innerTokens) {
268
- expanded.push({ token: m.token, start: m.start, length: (m.token.raw || '').length });
269
- expanded.push(...m.innerTokens);
270
- }
271
- else {
272
- expanded.push(m);
273
- }
274
- }
275
- return expanded;
276
- }
277
- function executeSegments(candidates, segments) {
278
- if (segments.length === 0)
279
- return candidates;
280
- const segment = segments[0];
281
- const remaining = segments.slice(1);
282
- if (isSectionSelector(segment.selector)) {
283
- const sections = computeSections(candidates, segment);
284
- const indexed = applyIndexSlice(sections, segment);
285
- if (remaining.length === 0)
286
- return indexed;
287
- const results = [];
288
- for (const section of indexed) {
289
- results.push(...executeSegments(section.innerTokens || [], remaining));
290
- }
291
- return results;
292
- }
293
- if (segment.selector === 'item') {
294
- let items = extractListItems(candidates);
295
- if (segment.textMatch) {
296
- items = items.filter((r) => matchText(getTokenText(r.token), segment.textMatch));
297
- }
298
- return executeSegments(applyIndexSlice(items, segment), remaining);
299
- }
300
- const tokenType = selectorToTokenType(segment.selector);
301
- if (!tokenType)
302
- return [];
303
- let matches = candidates.filter((r) => r.token.type === tokenType);
304
- const depth = getHeadingDepth(segment.selector);
305
- if (depth !== null) {
306
- matches = matches.filter((r) => r.token.depth === depth);
307
- }
308
- if (segment.textMatch) {
309
- matches = matches.filter((r) => matchText(getTokenText(r.token), segment.textMatch));
310
- }
311
- return executeSegments(applyIndexSlice(matches, segment), remaining);
312
- }
313
- export class MarkdownQuery {
314
- source;
315
- matches;
316
- constructor(source, matches) {
317
- this.source = source;
318
- this.matches = matches || buildTokenIndex(source);
319
- }
320
- query(selector) {
321
- const segments = parseQuery(selector);
322
- const candidates = expandSectionRanges(this.matches);
323
- const results = executeSegments(candidates, segments);
324
- return new MarkdownQuery(this.source, results);
325
- }
326
- text() {
327
- return this.matches.map((r) => this.source.slice(r.start, r.start + r.length)).join('');
328
- }
329
- get() {
330
- return this.text();
331
- }
332
- toJson() {
333
- const results = [];
334
- for (const range of this.matches) {
335
- if (range.token.type !== 'table')
336
- continue;
337
- const table = range.token;
338
- const headers = table.header.map((h) => h.text);
339
- for (const row of table.rows) {
340
- const obj = {};
341
- for (let i = 0; i < headers.length; i++) {
342
- obj[headers[i]] = row[i]?.text ?? '';
343
- }
344
- results.push(obj);
345
- }
346
- }
347
- return results;
348
- }
349
- keyValue() {
350
- const entries = {};
351
- for (const range of this.matches) {
352
- for (const line of getTokenText(range.token).split('\n')) {
353
- const key = entryKey(line);
354
- if (!key)
355
- continue;
356
- const value = line.slice(line.indexOf(':') + 1).trim();
357
- if (value)
358
- entries[key] = value;
359
- }
360
- }
361
- return entries;
362
- }
363
- setKeyValue(key, value) {
364
- return this.replaceEach((match) => {
365
- const token = match.matches[0].token;
366
- const lines = getTokenText(token)
367
- .split('\n')
368
- .map((line) => line.trim())
369
- .filter(Boolean);
370
- const index = lines.findIndex((line) => entryKey(line) === key.toLowerCase());
371
- if (index < 0 && value)
372
- lines.push(`${key}: ${value}`);
373
- if (index >= 0 && value)
374
- lines[index] = `${key}: ${value}`;
375
- if (index >= 0 && !value)
376
- lines.splice(index, 1);
377
- if (token.type !== 'blockquote')
378
- return lines.join('\n');
379
- return lines.map((line) => `> ${line}`).join('\n');
380
- });
381
- }
382
- replace(content) {
383
- return this.replaceEach(() => content);
384
- }
385
- replaceEach(replacer) {
386
- const sorted = [...this.matches].sort((a, b) => a.start - b.start);
387
- const kept = [];
388
- let lastEnd = -1;
389
- for (const range of sorted) {
390
- if (range.start < lastEnd)
391
- continue;
392
- kept.push(range);
393
- lastEnd = range.start + range.length;
394
- }
395
- const replacements = kept.map((range, index) => replacer(new MarkdownQuery(this.source, [range]), index));
396
- let result = this.source;
397
- for (let i = kept.length - 1; i >= 0; i--) {
398
- const range = kept[i];
399
- result = result.slice(0, range.start) + replacements[i] + result.slice(range.start + range.length);
400
- }
401
- return result;
402
- }
403
- count() {
404
- return this.matches.length;
405
- }
406
- first() {
407
- return new MarkdownQuery(this.source, this.matches.slice(0, 1));
408
- }
409
- last() {
410
- return new MarkdownQuery(this.source, this.matches.slice(-1));
411
- }
412
- before() {
413
- if (this.matches.length === 0)
414
- return new MarkdownQuery(this.source, []);
415
- const cutoff = this.matches[0].start;
416
- const allTokens = buildTokenIndex(this.source);
417
- const beforeTokens = allTokens.filter((r) => r.start + r.length <= cutoff);
418
- return new MarkdownQuery(this.source, beforeTokens);
419
- }
420
- after() {
421
- if (this.matches.length === 0)
422
- return new MarkdownQuery(this.source, []);
423
- const lastMatch = this.matches[this.matches.length - 1];
424
- const cutoff = lastMatch.start + lastMatch.length;
425
- const allTokens = buildTokenIndex(this.source);
426
- const afterTokens = allTokens.filter((r) => r.start >= cutoff);
427
- return new MarkdownQuery(this.source, afterTokens);
428
- }
429
- each() {
430
- return this.matches.map((m) => new MarkdownQuery(this.source, [m]));
431
- }
432
- meta() {
433
- return this.matches.map((range) => {
434
- const token = range.token;
435
- let depth = null;
436
- if (token.type === 'heading')
437
- depth = token.depth;
438
- return { type: token.type, depth, text: getTokenText(range.token) };
439
- });
440
- }
441
- }
442
- export function mdq(source) {
443
- return new MarkdownQuery(source);
444
- }
1
+ export * from "./mdq/query.js";
@@ -0,0 +1,6 @@
1
+ export declare function runMdq(argv: string[], readStdin: StdinReader): Promise<CliResult>;
2
+ export type StdinReader = () => Promise<string>;
3
+ export interface CliResult {
4
+ output: string;
5
+ code: number;
6
+ }