explorbot 0.4.10 → 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.
- package/README.md +4 -1
- package/bin/mdq.ts +18 -0
- package/boat/api-tester/src/ai/chief.ts +72 -0
- package/boat/api-tester/src/api-client.ts +37 -0
- package/boat/prima/src/prima.ts +41 -2
- package/dist/bin/mdq.js +19 -0
- package/dist/boat/api-tester/src/ai/chief.js +69 -0
- package/dist/boat/api-tester/src/api-client.js +26 -0
- package/dist/boat/prima/src/prima.js +42 -2
- package/dist/package.json +3 -2
- package/dist/src/ai/agent.d.ts +3 -1
- package/dist/src/ai/judge-provider.d.ts +17 -0
- package/dist/src/ai/judge-provider.js +56 -0
- package/dist/src/ai/judge-tool.d.ts +2 -0
- package/dist/src/ai/judge-tool.js +33 -0
- package/dist/src/ai/judge.d.ts +28 -0
- package/dist/src/ai/judge.js +71 -0
- package/dist/src/ai/navigator.d.ts +3 -0
- package/dist/src/ai/navigator.js +13 -4
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +57 -5
- package/dist/src/ai/planner.js +9 -6
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +52 -7
- package/dist/src/ai/researcher/deep-analysis.js +2 -2
- package/dist/src/ai/researcher/locators.js +2 -2
- package/dist/src/ai/researcher/pagination.js +1 -1
- package/dist/src/ai/researcher/research-result.js +2 -2
- package/dist/src/ai/researcher.js +1 -1
- package/dist/src/ai/task-agent.d.ts +2 -0
- package/dist/src/ai/task-agent.js +3 -1
- package/dist/src/ai/tester.js +19 -15
- package/dist/src/ai/tools.d.ts +4 -3
- package/dist/src/ai/tools.js +35 -7
- package/dist/src/api/request-result.js +2 -1
- package/dist/src/command-handler.d.ts +1 -0
- package/dist/src/command-handler.js +24 -3
- package/dist/src/commands/base-command.d.ts +5 -0
- package/dist/src/commands/base-command.js +3 -0
- package/dist/src/commands/explore-command.d.ts +2 -1
- package/dist/src/commands/explore-command.js +12 -1
- package/dist/src/commands/freesail-command.js +8 -2
- package/dist/src/commands/init-command.js +1 -1
- package/dist/src/commands/navigate-command.d.ts +2 -1
- package/dist/src/commands/navigate-command.js +6 -0
- package/dist/src/commands/plan-load-command.d.ts +2 -1
- package/dist/src/commands/plan-load-command.js +4 -0
- package/dist/src/commands/plans-command.d.ts +3 -9
- package/dist/src/commands/plans-command.js +11 -21
- package/dist/src/commands/rerun-command.d.ts +2 -1
- package/dist/src/commands/rerun-command.js +5 -1
- package/dist/src/commands/research-command.d.ts +2 -1
- package/dist/src/commands/research-command.js +6 -0
- package/dist/src/commands/test-command.d.ts +2 -1
- package/dist/src/commands/test-command.js +4 -1
- package/dist/src/components/Autocomplete.js +26 -12
- package/dist/src/components/InputReadline.js +10 -1
- package/dist/src/config.d.ts +6 -0
- package/dist/src/experience-tracker.js +4 -3
- package/dist/src/explorbot.d.ts +3 -0
- package/dist/src/explorbot.js +8 -0
- package/dist/src/knowledge-tracker.js +1 -1
- package/dist/src/state-manager.d.ts +2 -0
- package/dist/src/state-manager.js +16 -0
- package/dist/src/test-plan.d.ts +11 -0
- package/dist/src/test-plan.js +54 -2
- package/dist/src/utils/aria-ref.js +1 -1
- package/dist/src/utils/logger.js +8 -2
- package/dist/src/utils/markdown-query.d.ts +1 -48
- package/dist/src/utils/markdown-query.js +1 -444
- package/dist/src/utils/mdq/cli.d.ts +6 -0
- package/dist/src/utils/mdq/cli.js +122 -0
- package/dist/src/utils/mdq/edit.d.ts +24 -0
- package/dist/src/utils/mdq/edit.js +147 -0
- package/dist/src/utils/mdq/query.d.ts +118 -0
- package/dist/src/utils/mdq/query.js +451 -0
- package/dist/src/utils/strings.d.ts +1 -0
- package/dist/src/utils/strings.js +7 -0
- package/dist/src/utils/test-files.d.ts +1 -0
- package/dist/src/utils/test-files.js +5 -2
- package/docs/api-testing/planning.md +1 -1
- package/docs/superpowers/plans/2026-09-15-mdq-package.md +130 -94
- package/docs/superpowers/specs/2026-09-18-judge-decision-model-design.md +79 -0
- package/package.json +3 -2
- package/src/ai/agent.ts +3 -1
- package/src/ai/judge-provider.ts +62 -0
- package/src/ai/judge-tool.ts +35 -0
- package/src/ai/judge.ts +75 -0
- package/src/ai/navigator.ts +15 -4
- package/src/ai/pilot.ts +58 -5
- package/src/ai/planner.ts +9 -6
- package/src/ai/provider.ts +51 -7
- package/src/ai/researcher/deep-analysis.ts +2 -2
- package/src/ai/researcher/locators.ts +2 -2
- package/src/ai/researcher/pagination.ts +1 -1
- package/src/ai/researcher/research-result.ts +2 -2
- package/src/ai/researcher.ts +1 -1
- package/src/ai/task-agent.ts +4 -1
- package/src/ai/tester.ts +19 -16
- package/src/ai/tools.ts +42 -7
- package/src/api/request-result.ts +2 -1
- package/src/command-handler.ts +28 -3
- package/src/commands/base-command.ts +9 -0
- package/src/commands/explore-command.ts +15 -2
- package/src/commands/freesail-command.ts +9 -2
- package/src/commands/init-command.ts +1 -1
- package/src/commands/navigate-command.ts +8 -1
- package/src/commands/plan-load-command.ts +6 -1
- package/src/commands/plans-command.ts +13 -29
- package/src/commands/rerun-command.ts +7 -2
- package/src/commands/research-command.ts +8 -1
- package/src/commands/test-command.ts +6 -2
- package/src/components/Autocomplete.tsx +39 -10
- package/src/components/InputReadline.tsx +10 -1
- package/src/config.ts +1 -0
- package/src/experience-tracker.ts +4 -3
- package/src/explorbot.ts +8 -0
- package/src/knowledge-tracker.ts +1 -1
- package/src/state-manager.ts +16 -0
- package/src/test-plan.ts +67 -2
- package/src/utils/aria-ref.ts +1 -1
- package/src/utils/logger.ts +6 -1
- package/src/utils/markdown-query.ts +1 -519
- package/src/utils/mdq/cli.ts +118 -0
- package/src/utils/mdq/edit.ts +158 -0
- package/src/utils/mdq/query.ts +556 -0
- package/src/utils/strings.ts +7 -0
- package/src/utils/test-files.ts +5 -2
|
@@ -1,444 +1 @@
|
|
|
1
|
-
|
|
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,122 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { MdqError, mdq } from "./query.js";
|
|
4
|
+
const EDIT_FLAGS = ['remove', 'replace', 'insertBefore', 'insertAfter', 'prepend', 'append', 'addRow', 'addItem', 'set'];
|
|
5
|
+
export async function runMdq(argv, readStdin) {
|
|
6
|
+
const program = new Command();
|
|
7
|
+
program
|
|
8
|
+
.name('mdq')
|
|
9
|
+
.description('query and edit markdown')
|
|
10
|
+
.argument('[selector]', 'markdown selector')
|
|
11
|
+
.argument('[file]', 'file to read; stdin when omitted')
|
|
12
|
+
.option('-j, --json', 'output rows as JSON')
|
|
13
|
+
.option('-c, --count', 'print the number of matches')
|
|
14
|
+
.option('-t, --text', 'print unwrapped text')
|
|
15
|
+
.option('--frontmatter', 'print frontmatter as JSON')
|
|
16
|
+
.option('-i, --in-place', 'write the result back to the file')
|
|
17
|
+
.option('--remove', 'delete matched blocks')
|
|
18
|
+
.option('--replace <markdown>', 'replace matched blocks')
|
|
19
|
+
.option('--insert-before <markdown>', 'insert before each match')
|
|
20
|
+
.option('--insert-after <markdown>', 'insert after each match')
|
|
21
|
+
.option('--prepend <markdown>', 'insert at the start of each match')
|
|
22
|
+
.option('--append <markdown>', 'insert at the end of each match')
|
|
23
|
+
.option('--add-row <json>', 'append a table row')
|
|
24
|
+
.option('--add-item <text>', 'append a list item')
|
|
25
|
+
.option('--set <key=value>', 'set an entry; omit the value to delete it')
|
|
26
|
+
.exitOverride()
|
|
27
|
+
.configureOutput({ writeOut: () => { }, writeErr: () => { } });
|
|
28
|
+
try {
|
|
29
|
+
program.parse(argv, { from: 'user' });
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
return { output: String(error.message), code: 2 };
|
|
33
|
+
}
|
|
34
|
+
const options = program.opts();
|
|
35
|
+
let [selector, file] = program.args;
|
|
36
|
+
if (options.frontmatter && selector && !file) {
|
|
37
|
+
file = selector;
|
|
38
|
+
selector = '';
|
|
39
|
+
}
|
|
40
|
+
let source = '';
|
|
41
|
+
if (!file)
|
|
42
|
+
source = await readStdin();
|
|
43
|
+
if (file) {
|
|
44
|
+
try {
|
|
45
|
+
source = readFileSync(file, 'utf8');
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { output: `Cannot read ${file}`, code: 2 };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
return await apply(mdq(source), selector, options, file);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof MdqError)
|
|
56
|
+
return { output: error.message, code: 2 };
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function apply(doc, selector, options, file) {
|
|
61
|
+
if (options.frontmatter)
|
|
62
|
+
return { output: `${JSON.stringify(doc.frontmatter(), null, 2)}\n`, code: 0 };
|
|
63
|
+
if (!selector)
|
|
64
|
+
return { output: 'A selector is required', code: 2 };
|
|
65
|
+
const chosen = EDIT_FLAGS.filter((flag) => options[flag] !== undefined);
|
|
66
|
+
if (chosen.length > 1)
|
|
67
|
+
return { output: 'Only one edit at a time', code: 2 };
|
|
68
|
+
const selection = doc.query(selector);
|
|
69
|
+
if (chosen.length === 0)
|
|
70
|
+
return read(selection, options);
|
|
71
|
+
if (!selection.exists())
|
|
72
|
+
return finish(String(doc), 1, options, file);
|
|
73
|
+
return finish(String(edit(selection, chosen[0], options)), 0, options, file);
|
|
74
|
+
}
|
|
75
|
+
function read(selection, options) {
|
|
76
|
+
let code = 1;
|
|
77
|
+
if (selection.exists())
|
|
78
|
+
code = 0;
|
|
79
|
+
if (options.count)
|
|
80
|
+
return { output: `${selection.count()}\n`, code: 0 };
|
|
81
|
+
if (options.json)
|
|
82
|
+
return { output: `${JSON.stringify(selection.rows(), null, 2)}\n`, code };
|
|
83
|
+
if (options.text)
|
|
84
|
+
return {
|
|
85
|
+
output: selection
|
|
86
|
+
.nodes()
|
|
87
|
+
.map((node) => node.text)
|
|
88
|
+
.join('\n\n'),
|
|
89
|
+
code,
|
|
90
|
+
};
|
|
91
|
+
return { output: selection.text(), code };
|
|
92
|
+
}
|
|
93
|
+
function edit(selection, flag, options) {
|
|
94
|
+
if (flag === 'remove')
|
|
95
|
+
return selection.remove();
|
|
96
|
+
if (flag === 'replace')
|
|
97
|
+
return selection.replace(options.replace);
|
|
98
|
+
if (flag === 'insertBefore')
|
|
99
|
+
return selection.insertBefore(options.insertBefore);
|
|
100
|
+
if (flag === 'insertAfter')
|
|
101
|
+
return selection.insertAfter(options.insertAfter);
|
|
102
|
+
if (flag === 'prepend')
|
|
103
|
+
return selection.prepend(options.prepend);
|
|
104
|
+
if (flag === 'append')
|
|
105
|
+
return selection.append(options.append);
|
|
106
|
+
if (flag === 'addItem')
|
|
107
|
+
return selection.addItem(options.addItem);
|
|
108
|
+
if (flag === 'addRow')
|
|
109
|
+
return selection.addRow(JSON.parse(options.addRow));
|
|
110
|
+
const separator = options.set.indexOf('=');
|
|
111
|
+
if (separator < 0)
|
|
112
|
+
return selection.setEntry(options.set, null);
|
|
113
|
+
return selection.setEntry(options.set.slice(0, separator), options.set.slice(separator + 1) || null);
|
|
114
|
+
}
|
|
115
|
+
function finish(output, code, options, file) {
|
|
116
|
+
if (!options.inPlace)
|
|
117
|
+
return { output, code };
|
|
118
|
+
if (!file)
|
|
119
|
+
return { output: '--in-place needs a file', code: 2 };
|
|
120
|
+
writeFileSync(file, output);
|
|
121
|
+
return { output: '', code };
|
|
122
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { MatchedRange } from './query.js';
|
|
2
|
+
export declare class MarkdownEditor {
|
|
3
|
+
source: string;
|
|
4
|
+
constructor(source: string);
|
|
5
|
+
static splitFrontmatter(source: string): FrontmatterSplit;
|
|
6
|
+
static blockEnd(range: MatchedRange): number;
|
|
7
|
+
static table(headers: string[], rows: string[][], align: (string | null)[]): string;
|
|
8
|
+
static item(listRaw: string, text: string): string;
|
|
9
|
+
static entries(text: string, key: string, value: string | null, isBlockquote: boolean): string;
|
|
10
|
+
remove(ranges: MatchedRange[]): string;
|
|
11
|
+
replace(ranges: MatchedRange[], render: (range: MatchedRange, index: number) => string): string;
|
|
12
|
+
insert(offsets: number[], markdown: string): string;
|
|
13
|
+
frontmatter(): Record<string, unknown>;
|
|
14
|
+
setFrontmatter(key: string, value: unknown): string;
|
|
15
|
+
static spliceIn(source: string, offset: number, markdown: string): string;
|
|
16
|
+
static dashes(alignment: string | null, width: number): string;
|
|
17
|
+
static entryKey(line: string): string | null;
|
|
18
|
+
dedupe(ranges: MatchedRange[]): MatchedRange[];
|
|
19
|
+
}
|
|
20
|
+
export interface FrontmatterSplit {
|
|
21
|
+
raw: string;
|
|
22
|
+
body: string;
|
|
23
|
+
offset: number;
|
|
24
|
+
}
|