mdq-cli 0.1.1 → 0.1.2
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 +335 -114
- package/bin/mdq.js +3 -38
- package/index.js +7 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,187 +1,408 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/testomatio/explorbot/main/assets/logos/mdq/mdq-icon-bg-v2.png" alt="mdq" width="160">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# mdq
|
|
2
6
|
|
|
3
|
-
Query and
|
|
7
|
+
Query, validate, extract, and update structured Markdown with a selector language. Like `jq` for Markdown.
|
|
4
8
|
|
|
5
|
-
##
|
|
9
|
+
## Usage examples
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx mdq-cli 'section("Overview")' generated.md # validate
|
|
13
|
+
npx mdq-cli 'section("API") table' --json README.md # extract
|
|
14
|
+
npx mdq-cli 'section("Tasks") list' --add-item 'Review docs' --in-place plan.md # update
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Validate the structure of LLM-generated Markdown
|
|
18
|
+
|
|
19
|
+
Use a selector as a structural assertion. The command prints the matching Markdown and exits with status `0` when the section exists, `1` when it does not exist, or `2` when the selector or input is invalid:
|
|
6
20
|
|
|
7
21
|
```bash
|
|
8
|
-
npx mdq-cli '
|
|
9
|
-
npm install mdq-cli # as a library
|
|
10
|
-
npm install -g mdq-cli # as a command
|
|
22
|
+
npx mdq-cli 'section("Overview")' generated.md
|
|
11
23
|
```
|
|
12
24
|
|
|
13
|
-
|
|
25
|
+
Validate more specific structure by composing selectors:
|
|
14
26
|
|
|
15
|
-
|
|
27
|
+
```bash
|
|
28
|
+
npx mdq-cli 'section("API") table' generated.md
|
|
29
|
+
npx mdq-cli 'section("Examples") code[0]' generated.md
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
These queries check for an **Overview** section, a table inside **API**, and at least one fenced code block inside **Examples**. An unknown selector raises an error instead of silently matching nothing.
|
|
33
|
+
|
|
34
|
+
The same validation can be scripted in JavaScript:
|
|
16
35
|
|
|
17
36
|
```js
|
|
37
|
+
import { readFile } from 'node:fs/promises';
|
|
18
38
|
import { mdq } from 'mdq-cli';
|
|
19
39
|
|
|
20
|
-
|
|
21
|
-
mdq(
|
|
22
|
-
|
|
40
|
+
const source = await readFile('generated.md', 'utf8');
|
|
41
|
+
const doc = mdq(source);
|
|
42
|
+
|
|
43
|
+
const valid =
|
|
44
|
+
doc.section('Overview').exists() &&
|
|
45
|
+
doc.query('section("API") table').exists() &&
|
|
46
|
+
doc.query('section("Examples") code[0]').exists();
|
|
47
|
+
|
|
48
|
+
if (!valid) throw new Error('Generated Markdown does not match the required structure');
|
|
23
49
|
```
|
|
24
50
|
|
|
25
|
-
|
|
51
|
+
### Extract data from structured Markdown
|
|
52
|
+
|
|
53
|
+
Read a table as JSON from the CLI:
|
|
26
54
|
|
|
27
55
|
```bash
|
|
28
56
|
npx mdq-cli 'section("API") table' --json README.md
|
|
29
57
|
```
|
|
30
58
|
|
|
31
|
-
|
|
59
|
+
Or extract typed structures through the JavaScript API:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import { mdq } from 'mdq-cli';
|
|
63
|
+
|
|
64
|
+
const endpoints = mdq(readme).query('section("API") table').rows();
|
|
65
|
+
const metadata = mdq(plan).section('Metadata').entries();
|
|
66
|
+
const testComments = mdq(doc).comment(/^test/).nodes();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Update data in Markdown
|
|
70
|
+
|
|
71
|
+
Append a row to a table and save the file in place:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx mdq-cli 'section("API") table' \
|
|
75
|
+
--add-row '{"Method":"POST","Path":"/sessions"}' \
|
|
76
|
+
--in-place README.md
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Or compose several edits in JavaScript:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
import { mdq } from 'mdq-cli';
|
|
83
|
+
|
|
84
|
+
const updated = mdq(source)
|
|
85
|
+
.query('section("API") table')
|
|
86
|
+
.addRow({ Method: 'POST', Path: '/sessions' })
|
|
87
|
+
.query('section("Notes")')
|
|
88
|
+
.append('- Document the new endpoint\n')
|
|
89
|
+
.toString();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Install
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx mdq-cli 'h2' README.md # run without installing
|
|
96
|
+
npm install mdq-cli # use as a library
|
|
97
|
+
npm install -g mdq-cli # install the mdq-cli command globally
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
mdq requires Node.js 18 or newer. Its implementation libraries are described in [Implementation](#implementation).
|
|
101
|
+
|
|
102
|
+
## Use mdq with coding agents
|
|
103
|
+
|
|
104
|
+
Add this instruction to your project's `AGENTS.md`:
|
|
105
|
+
|
|
106
|
+
```md
|
|
107
|
+
When you need to inspect, validate, or edit structured Markdown, use `npx mdq-cli` selectors instead of parsing or rewriting the document manually; run `npx mdq-cli --help` for the available query and edit options.
|
|
108
|
+
```
|
|
32
109
|
|
|
33
|
-
|
|
110
|
+
This gives an agent a deterministic CLI for checking generated documents, extracting structured content, and making targeted edits without rewriting unrelated Markdown.
|
|
34
111
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
112
|
+
## Core model
|
|
113
|
+
|
|
114
|
+
**Reads narrow the selection; writes return the document.**
|
|
115
|
+
|
|
116
|
+
`mdq(source)` returns a `MarkdownQuery`. It holds the complete document and the blocks currently selected from it. `query()` and the convenience methods narrow that selection. Every write returns a fresh `MarkdownQuery` over the edited document, so edits can be chained and finished with `toString()`:
|
|
39
117
|
|
|
40
118
|
```js
|
|
41
119
|
mdq(source)
|
|
42
|
-
.query('section("API")')
|
|
43
|
-
.
|
|
120
|
+
.query('section("API")')
|
|
121
|
+
.append('## Notes\n')
|
|
122
|
+
.query('blockquote[0]')
|
|
123
|
+
.remove()
|
|
44
124
|
.toString();
|
|
45
125
|
```
|
|
46
126
|
|
|
47
|
-
`toString()`
|
|
48
|
-
selection.
|
|
127
|
+
`toString()` always returns the whole document. `text()` returns the raw Markdown for only the current selection.
|
|
49
128
|
|
|
50
|
-
##
|
|
129
|
+
## CLI reference
|
|
51
130
|
|
|
52
|
-
|
|
131
|
+
```text
|
|
132
|
+
mdq-cli [options] [selector] [file]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Square brackets in `mdq-cli [options] [selector] [file]` mean that an argument is optional; they are documentation notation, not characters to type.
|
|
136
|
+
|
|
137
|
+
- `[selector]` is the query that chooses Markdown blocks.
|
|
138
|
+
- `[file]` is the input file. When it is omitted, mdq reads the document from standard input.
|
|
139
|
+
- `--in-place` is an output option for edits, not a replacement for `[file]`. It tells mdq to overwrite the input file instead of printing the edited document.
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npx mdq-cli 'h2' README.md # read from a file
|
|
143
|
+
cat README.md | npx mdq-cli 'h2' # read from standard input
|
|
144
|
+
npx mdq-cli 'comment(~"draft")' --remove README.md
|
|
145
|
+
# print the edited document
|
|
146
|
+
npx mdq-cli 'comment(~"draft")' --remove --in-place README.md
|
|
147
|
+
# overwrite README.md
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Only one edit option can be used per invocation. `--in-place` requires a file because there is no input file to overwrite when mdq reads from standard input.
|
|
151
|
+
|
|
152
|
+
### Read options
|
|
153
|
+
|
|
154
|
+
| Option | Result |
|
|
155
|
+
| --- | --- |
|
|
156
|
+
| `-j, --json` | Output selected table rows as JSON |
|
|
157
|
+
| `-c, --count` | Print the number of matches |
|
|
158
|
+
| `-t, --text` | Print unwrapped node text |
|
|
159
|
+
| `--frontmatter` | Print YAML frontmatter as JSON; no selector is required |
|
|
160
|
+
|
|
161
|
+
Without a read option, mdq prints the raw Markdown for the selection.
|
|
162
|
+
|
|
163
|
+
### Edit options
|
|
164
|
+
|
|
165
|
+
| Option | Effect |
|
|
53
166
|
| --- | --- |
|
|
54
|
-
| `
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
| `
|
|
64
|
-
|
|
65
|
-
|
|
167
|
+
| `--remove` | Delete matched blocks |
|
|
168
|
+
| `--replace <markdown>` | Replace matched blocks |
|
|
169
|
+
| `--insert-before <markdown>` | Insert a sibling block before each match |
|
|
170
|
+
| `--insert-after <markdown>` | Insert a sibling block after each match |
|
|
171
|
+
| `--prepend <markdown>` | Insert at the start of each matched section or list |
|
|
172
|
+
| `--append <markdown>` | Insert at the end of each matched section or list |
|
|
173
|
+
| `--add-row <json>` | Append a row to each matched table |
|
|
174
|
+
| `--add-item <text>` | Append an item to each matched list |
|
|
175
|
+
| `--set <key=value>` | Set a `Key: value` entry; omit `=value` to delete the key |
|
|
176
|
+
| `-i, --in-place` | Write the edited document back to the input file |
|
|
177
|
+
|
|
178
|
+
Without `--in-place`, an edit prints the complete edited document to standard output. `--in-place` requires a file.
|
|
179
|
+
|
|
180
|
+
### Use mdq in pipelines
|
|
181
|
+
|
|
182
|
+
Because mdq reads from standard input when `[file]` is omitted, it can query generated Markdown without creating a temporary file:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
generate-markdown | npx mdq-cli 'section("Summary")'
|
|
186
|
+
```
|
|
66
187
|
|
|
67
|
-
|
|
188
|
+
Pipe selected table rows as JSON into another data tool:
|
|
68
189
|
|
|
190
|
+
```bash
|
|
191
|
+
npx mdq-cli 'section("API") table' --json README.md |
|
|
192
|
+
jq -r '.[].Path'
|
|
69
193
|
```
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
194
|
+
|
|
195
|
+
Pipe a selected Markdown block to another command:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
cat README.md |
|
|
199
|
+
npx mdq-cli 'section("Install")' |
|
|
200
|
+
markdownlint
|
|
74
201
|
```
|
|
75
202
|
|
|
76
|
-
|
|
77
|
-
another:
|
|
203
|
+
Edits without `--in-place` print the complete edited document, so multiple mdq edits can be chained:
|
|
78
204
|
|
|
205
|
+
```bash
|
|
206
|
+
npx mdq-cli 'section("Deprecated")' --remove README.md |
|
|
207
|
+
npx mdq-cli 'section("Notes") list[0]' --add-item 'Reviewed' \
|
|
208
|
+
> README.updated.md
|
|
79
209
|
```
|
|
80
|
-
|
|
81
|
-
|
|
210
|
+
|
|
211
|
+
To replace the original file, prefer `--in-place`. Do not redirect output back to the same file being read, such as `... README.md > README.md`, because the shell truncates the file before mdq can read it.
|
|
212
|
+
|
|
213
|
+
### Exit status
|
|
214
|
+
|
|
215
|
+
| Status | Meaning |
|
|
216
|
+
| --- | --- |
|
|
217
|
+
| `0` | The query matched, the count completed, or the edit succeeded |
|
|
218
|
+
| `1` | The selector matched nothing |
|
|
219
|
+
| `2` | The command, selector, input file, or operation was invalid |
|
|
220
|
+
|
|
221
|
+
`--count` exits with status `0`, including when the count is zero.
|
|
222
|
+
|
|
223
|
+
## Selector reference
|
|
224
|
+
|
|
225
|
+
| Selector | Matches |
|
|
226
|
+
| --- | --- |
|
|
227
|
+
| `section` | A heading and everything below it, up to the next heading of the same or shallower depth |
|
|
228
|
+
| `section1` … `section6` | The same section range, restricted to one heading depth |
|
|
229
|
+
| `heading`, `h1` … `h6` | The heading line only |
|
|
230
|
+
| `paragraph` | A paragraph |
|
|
231
|
+
| `table` | A GFM table |
|
|
232
|
+
| `list` | A bullet or ordered list |
|
|
233
|
+
| `item` | One list item |
|
|
234
|
+
| `code` | A fenced code block |
|
|
235
|
+
| `blockquote` | A `>` block |
|
|
236
|
+
| `hr` | A thematic break |
|
|
237
|
+
| `html` | An HTML block, matched against its raw text |
|
|
238
|
+
| `comment` | An HTML comment, matched against its **inner** body |
|
|
239
|
+
|
|
240
|
+
### Match text
|
|
241
|
+
|
|
242
|
+
Put a text matcher in parentheses. Prefix any matcher with `!` to negate it.
|
|
243
|
+
|
|
244
|
+
```text
|
|
245
|
+
section("Install") exact match
|
|
246
|
+
section(~"Inst") contains text
|
|
247
|
+
section(/^inst/i) regular expression, with its own flags
|
|
248
|
+
section(!~"Draft") negated match
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Select by index or slice
|
|
252
|
+
|
|
253
|
+
Use brackets to select by position. Negative indexes count from the end, and slices follow Python-style `from:to` bounds.
|
|
254
|
+
|
|
255
|
+
```text
|
|
256
|
+
heading[0] first heading
|
|
257
|
+
heading[-1] last heading
|
|
82
258
|
blockquote[2:5] a slice
|
|
83
|
-
section("API") table every table inside that section
|
|
84
259
|
```
|
|
85
260
|
|
|
86
|
-
|
|
261
|
+
### Scope selectors
|
|
87
262
|
|
|
88
|
-
|
|
89
|
-
character. It never silently matches nothing.
|
|
263
|
+
Separate selectors with spaces to scope each selector inside the previous one:
|
|
90
264
|
|
|
91
|
-
|
|
265
|
+
```text
|
|
266
|
+
section("API") table every table inside the API section
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
A leading `.` is accepted and ignored, so `.h2` works if that is your habit from `jq`.
|
|
270
|
+
|
|
271
|
+
An unknown selector throws `MdqSelectorError`. The error carries the `index` of the offending character; mdq never silently treats an unknown selector as an empty result.
|
|
272
|
+
|
|
273
|
+
## Matchers as JavaScript values
|
|
92
274
|
|
|
93
|
-
|
|
275
|
+
Pass a JavaScript matcher value when building a selector string would require escaping dynamic input:
|
|
94
276
|
|
|
95
277
|
```js
|
|
96
|
-
mdq(doc).query('section2', section.name); // exact
|
|
97
|
-
mdq(doc).heading(/^summary/i); //
|
|
278
|
+
mdq(doc).query('section2', section.name); // exact string
|
|
279
|
+
mdq(doc).heading(/^summary/i); // RegExp with its own flags
|
|
98
280
|
mdq(doc).item((text) => text.length > 80); // predicate
|
|
99
281
|
```
|
|
100
282
|
|
|
101
|
-
A `string` matches exactly, a `RegExp` honors its own flags, and a function is a predicate
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
283
|
+
A `string` matches exactly, a `RegExp` honors its own flags, and a function is called as a predicate over the node's text.
|
|
284
|
+
|
|
285
|
+
These convenience selector methods accept a matcher:
|
|
286
|
+
|
|
287
|
+
- `section`
|
|
288
|
+
- `heading`
|
|
289
|
+
- `paragraph`
|
|
290
|
+
- `table`
|
|
291
|
+
- `list`
|
|
292
|
+
- `item`
|
|
293
|
+
- `code`
|
|
294
|
+
- `blockquote`
|
|
295
|
+
- `comment`
|
|
296
|
+
- `html`
|
|
297
|
+
|
|
298
|
+
Each is equivalent to `query(selector, matcher)`. The `hr()` convenience method takes no matcher. `section` and `heading` also accept `{ depth }`:
|
|
105
299
|
|
|
106
|
-
|
|
300
|
+
```js
|
|
301
|
+
mdq(doc).section('Install', { depth: 2 });
|
|
302
|
+
mdq(doc).heading(/^summary/i, { depth: 3 });
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Reading with the JavaScript API
|
|
107
306
|
|
|
108
307
|
| Method | Returns |
|
|
109
308
|
| --- | --- |
|
|
110
|
-
| `text()` |
|
|
111
|
-
| `nodes()` | `{ type, depth, text }` per match |
|
|
112
|
-
| `rows()` |
|
|
113
|
-
| `entries()` | `Key: value` lines
|
|
114
|
-
| `count()` / `exists()` |
|
|
115
|
-
| `first()` / `last()` / `at(n)` / `slice(from, to)` |
|
|
116
|
-
| `each()` |
|
|
117
|
-
| `preceding()` / `following()` |
|
|
309
|
+
| `text()` | Raw Markdown from every match, joined together |
|
|
310
|
+
| `nodes()` | One `{ type, depth, text }` object per match |
|
|
311
|
+
| `rows()` | Table rows as objects keyed by table header |
|
|
312
|
+
| `entries()` | `Key: value` lines from a block, with lowercase keys |
|
|
313
|
+
| `count()` / `exists()` | The number of matches / whether any match exists |
|
|
314
|
+
| `first()` / `last()` / `at(n)` / `slice(from, to)` | A narrowed selection |
|
|
315
|
+
| `each()` | One single-match `MarkdownQuery` per match |
|
|
316
|
+
| `preceding()` / `following()` | Everything before the first / after the last match |
|
|
118
317
|
|
|
119
|
-
## Writing
|
|
318
|
+
## Writing with the JavaScript API
|
|
120
319
|
|
|
121
|
-
Every
|
|
320
|
+
Every write returns a new `MarkdownQuery` over the complete edited document.
|
|
122
321
|
|
|
123
322
|
| Method | Effect |
|
|
124
323
|
| --- | --- |
|
|
125
|
-
| `replace(md)` |
|
|
126
|
-
| `replaceEach(fn)` |
|
|
127
|
-
| `remove()` |
|
|
128
|
-
| `insertBefore(md)` / `insertAfter(md)` |
|
|
129
|
-
| `prepend(md)` / `append(md)` |
|
|
130
|
-
| `addRow(obj)` |
|
|
131
|
-
| `addItem(text)` |
|
|
132
|
-
| `setEntry(key, value)` |
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
`
|
|
136
|
-
|
|
137
|
-
|
|
324
|
+
| `replace(md)` | Replace every match |
|
|
325
|
+
| `replaceEach(fn)` | Replace each match with `fn(selection, index)` |
|
|
326
|
+
| `remove()` | Delete every match and its blank line |
|
|
327
|
+
| `insertBefore(md)` / `insertAfter(md)` | Add a sibling block |
|
|
328
|
+
| `prepend(md)` / `append(md)` | Add a block inside a section or list |
|
|
329
|
+
| `addRow(obj)` | Append a table row and realign the columns |
|
|
330
|
+
| `addItem(text)` | Append a list item using the existing list marker |
|
|
331
|
+
| `setEntry(key, value)` | Set a `Key: value` line; pass `null` to delete it |
|
|
332
|
+
| `setFrontmatter(key, value)` | Set a YAML frontmatter value |
|
|
333
|
+
|
|
334
|
+
`prepend` and `append` require a section or list. Using either method on another block raises `MdqOperationError`.
|
|
335
|
+
|
|
336
|
+
Any method that accepts Markdown also accepts another `MarkdownQuery`. Writes normalize spacing to exactly one blank line between blocks. Content inside fenced code blocks is preserved exactly.
|
|
138
337
|
|
|
139
338
|
## Frontmatter
|
|
140
339
|
|
|
141
|
-
A leading `---` block is parsed as YAML,
|
|
142
|
-
Without this, `marked` reads `url: /login` as a setext heading.
|
|
340
|
+
A leading `---` block is parsed as YAML, excluded from the token index, and exposed as data. This prevents `marked` from interpreting a line such as `url: /login` as a setext heading.
|
|
143
341
|
|
|
144
342
|
```js
|
|
145
343
|
const doc = mdq(page);
|
|
146
|
-
|
|
147
|
-
doc.
|
|
148
|
-
doc.
|
|
344
|
+
|
|
345
|
+
doc.frontmatter(); // { url: '/login', wait: 1000, tags: ['auth'] }
|
|
346
|
+
doc.query('h2').count(); // 0 — the --- block is not a heading
|
|
347
|
+
|
|
348
|
+
const updated = doc
|
|
349
|
+
.setFrontmatter('wait', 2000) // comments and formatting survive
|
|
350
|
+
.toString();
|
|
149
351
|
```
|
|
150
352
|
|
|
151
|
-
|
|
353
|
+
From the CLI:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
npx mdq-cli --frontmatter page.md
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## Implementation
|
|
360
|
+
|
|
361
|
+
Under the hood, mdq uses `marked` to parse Markdown into a block-token AST before it queries or edits anything. Headings, paragraphs, tables, lists, list items, fenced code blocks, blockquotes, HTML, and thematic breaks all come from that parsed structure.
|
|
362
|
+
|
|
363
|
+
### Processing model
|
|
152
364
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
365
|
+
1. A leading YAML frontmatter block is separated from the Markdown body.
|
|
366
|
+
2. `marked.lexer()` parses the body into block tokens, including nested list items and GFM table data.
|
|
367
|
+
3. mdq indexes each token with its range in the original source.
|
|
368
|
+
4. The selector parser converts the query into validated selector segments.
|
|
369
|
+
5. The evaluator matches those segments against parsed tokens. It derives sections from heading depth and descendant scope from the selected token ranges.
|
|
370
|
+
6. Edits splice only the selected source ranges. Untouched text remains unchanged, and the result is parsed again into a fresh `MarkdownQuery`.
|
|
156
371
|
|
|
157
|
-
|
|
158
|
-
[mdast](https://github.com/syntax-tree/mdast), then select with
|
|
159
|
-
[`unist-util-select`](https://github.com/syntax-tree/unist-util-select), which implements
|
|
160
|
-
real CSS selectors on top of `css-selector-parser`. If you want a standards-based tool,
|
|
161
|
-
use that.
|
|
372
|
+
This model provides consistent queries and targeted edits while preserving content outside the selection.
|
|
162
373
|
|
|
163
|
-
|
|
374
|
+
### Selector grammar
|
|
164
375
|
|
|
165
|
-
|
|
166
|
-
descendant combinator cannot express "this heading and everything under it until the next
|
|
167
|
-
heading of the same depth". [`remark-sectionize`](https://github.com/jake-low/remark-sectionize)
|
|
168
|
-
adds the nesting, but its synthetic `section` nodes carry no `position`, so their source
|
|
169
|
-
range has to be derived from their children before anything can be edited in place.
|
|
170
|
-
- **Editing by byte range.** mdq records each block's offset in the original source and
|
|
171
|
-
splices text, so anything it does not touch stays byte-identical. mdast nodes do carry
|
|
172
|
-
offsets, so this is achievable there too — it is a thing to build, not a thing you get.
|
|
173
|
-
- **Text and pattern matching.** mdast headings have no flat text field (the text is a child
|
|
174
|
-
node), CSS dropped `:contains()`, and `unist-util-select` parses the attribute `i` flag
|
|
175
|
-
but does not apply it — so `/^summary/i` has no selector form at all.
|
|
376
|
+
The selector language has a small, explicit grammar:
|
|
176
377
|
|
|
177
|
-
|
|
378
|
+
```text
|
|
379
|
+
query := segment (whitespace segment)*
|
|
380
|
+
segment := ["." ] name [matcher] position*
|
|
381
|
+
name := section | section1..section6
|
|
382
|
+
| heading | h1..h6
|
|
383
|
+
| paragraph | table | list | item | code
|
|
384
|
+
| blockquote | hr | html | comment
|
|
385
|
+
matcher := "(" ["!"] (quoted | "~" quoted | regex) ")"
|
|
386
|
+
quoted := '"' text '"'
|
|
387
|
+
regex := "/" pattern "/" flags
|
|
388
|
+
position := "[" integer "]"
|
|
389
|
+
| "[" [integer] ":" [integer] "]"
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Whitespace between segments means “select inside the previous match.” A leading `.` is accepted as optional syntax. Indexes may be negative, and slices use `from:to` bounds. The complete selector must match this grammar: unknown names and unexpected characters raise `MdqSelectorError` with the failing character position.
|
|
393
|
+
|
|
394
|
+
Markdown structure is not parsed with regular expressions. Regex is limited to selector tokenization, explicit `/pattern/flags` matchers, and small local formatting operations.
|
|
395
|
+
|
|
396
|
+
### Libraries
|
|
397
|
+
|
|
398
|
+
| Library | Purpose |
|
|
399
|
+
| --- | --- |
|
|
400
|
+
| [`marked`](https://marked.js.org/) | Parse Markdown into block tokens and provide GFM table and nested-list structure |
|
|
401
|
+
| [`yaml`](https://eemeli.org/yaml/) | Parse and update YAML frontmatter through a document model that preserves comments and formatting |
|
|
402
|
+
| [`commander`](https://github.com/tj/commander.js) | Parse CLI arguments and options, and generate command help |
|
|
178
403
|
|
|
404
|
+
File input and in-place writes use Node.js built-in filesystem APIs.
|
|
179
405
|
|
|
180
|
-
##
|
|
406
|
+
## License
|
|
181
407
|
|
|
182
|
-
|
|
183
|
-
part of that paragraph's token and is not reachable as a `comment`.
|
|
184
|
-
- **No row or item selectors.** `addRow` and `addItem` append; there is no `removeRow`,
|
|
185
|
-
because there is nothing to select.
|
|
186
|
-
- **YAML frontmatter only.** TOML (`+++`) and JSON blocks are skipped from the token index
|
|
187
|
-
but not parsed.
|
|
408
|
+
MIT
|
package/bin/mdq.js
CHANGED
|
@@ -3,35 +3,6 @@
|
|
|
3
3
|
// src/utils/mdq/cli.ts
|
|
4
4
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
-
// src/utils/mdq/package.json
|
|
7
|
-
var package_default = {
|
|
8
|
-
name: "mdq-cli",
|
|
9
|
-
version: "0.1.1",
|
|
10
|
-
description: "Query and edit markdown with a selector language - jq, for markdown",
|
|
11
|
-
license: "MIT",
|
|
12
|
-
type: "module",
|
|
13
|
-
main: "./index.js",
|
|
14
|
-
types: "./types/query.d.ts",
|
|
15
|
-
exports: {
|
|
16
|
-
".": {
|
|
17
|
-
types: "./types/query.d.ts",
|
|
18
|
-
import: "./index.js"
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
bin: {
|
|
22
|
-
mdq: "./bin/mdq.js"
|
|
23
|
-
},
|
|
24
|
-
files: ["index.js", "types/", "bin/", "README.md"],
|
|
25
|
-
engines: {
|
|
26
|
-
node: ">=18"
|
|
27
|
-
},
|
|
28
|
-
keywords: ["markdown", "query", "selector", "cli", "jq", "frontmatter", "marked", "edit"],
|
|
29
|
-
repository: {
|
|
30
|
-
type: "git",
|
|
31
|
-
url: "https://github.com/testomatio/explorbot",
|
|
32
|
-
directory: "src/utils/mdq"
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
6
|
|
|
36
7
|
// src/utils/mdq/query.ts
|
|
37
8
|
import { marked } from "marked";
|
|
@@ -662,19 +633,13 @@ var SEGMENT = /(\s*\.?)([A-Za-z]\w*)(?:\((!?)(~?)(?:"((?:[^"\\]|\\.)*)"|\/((?:[^
|
|
|
662
633
|
// src/utils/mdq/cli.ts
|
|
663
634
|
var EDIT_FLAGS = ["remove", "replace", "insertBefore", "insertAfter", "prepend", "append", "addRow", "addItem", "set"];
|
|
664
635
|
async function runMdq(argv, readStdin) {
|
|
665
|
-
let printed = "";
|
|
666
636
|
const program = new Command;
|
|
667
|
-
program.name("mdq").description("query and edit markdown").
|
|
668
|
-
writeOut: (text) => {
|
|
669
|
-
printed += text;
|
|
670
|
-
},
|
|
671
|
-
writeErr: () => {}
|
|
672
|
-
});
|
|
637
|
+
program.name("mdq-cli").description("query and edit markdown").argument("[selector]", "markdown selector").argument("[file]", "file to read; stdin when omitted").option("-j, --json", "output rows as JSON").option("-c, --count", "print the number of matches").option("-t, --text", "print unwrapped text").option("--frontmatter", "print frontmatter as JSON").option("-i, --in-place", "write the result back to the file").option("--remove", "delete matched blocks").option("--replace <markdown>", "replace matched blocks").option("--insert-before <markdown>", "insert before each match").option("--insert-after <markdown>", "insert after each match").option("--prepend <markdown>", "insert at the start of each match").option("--append <markdown>", "insert at the end of each match").option("--add-row <json>", "append a table row").option("--add-item <text>", "append a list item").option("--set <key=value>", "set an entry; omit the value to delete it").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} });
|
|
673
638
|
try {
|
|
674
639
|
program.parse(argv, { from: "user" });
|
|
675
640
|
} catch (error) {
|
|
676
|
-
if (
|
|
677
|
-
return { output:
|
|
641
|
+
if (error.code === "commander.helpDisplayed")
|
|
642
|
+
return { output: program.helpInformation(), code: 0 };
|
|
678
643
|
return { output: String(error.message), code: 2 };
|
|
679
644
|
}
|
|
680
645
|
const options = program.opts();
|
package/index.js
CHANGED
|
@@ -624,12 +624,12 @@ var SELECTOR_NAMES = new Set(["section", "heading", "paragraph", "table", "list"
|
|
|
624
624
|
var TOKEN_ALIASES = { item: "list_item" };
|
|
625
625
|
var SEGMENT = /(\s*\.?)([A-Za-z]\w*)(?:\((!?)(~?)(?:"((?:[^"\\]|\\.)*)"|\/((?:[^/\\]|\\.)*)\/([a-z]*))\))?((?:\[[^\]]*\])*)\s*/y;
|
|
626
626
|
export {
|
|
627
|
-
|
|
628
|
-
mdq,
|
|
629
|
-
buildTokenIndex,
|
|
630
|
-
MdqSelectorError,
|
|
631
|
-
MdqOperationError,
|
|
632
|
-
MdqError,
|
|
627
|
+
MarkdownEditor,
|
|
633
628
|
MarkdownQuery,
|
|
634
|
-
|
|
629
|
+
MdqError,
|
|
630
|
+
MdqOperationError,
|
|
631
|
+
MdqSelectorError,
|
|
632
|
+
buildTokenIndex,
|
|
633
|
+
mdq,
|
|
634
|
+
parseQuery
|
|
635
635
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mdq-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Query and edit markdown with a selector language - jq, for markdown",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
|
16
|
-
"mdq": "./bin/mdq.js"
|
|
16
|
+
"mdq-cli": "./bin/mdq.js"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"index.js",
|