Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e1e5f85
docs: design spec for mdq standalone package
Sep 14, 2026
ad7c8f4
docs: resolve open questions in mdq spec
Sep 14, 2026
a0dc4a7
docs: implementation plan for mdq package
Sep 15, 2026
791348c
docs: correct whitespace rules in mdq plan from token spikes
Sep 15, 2026
1f7ca9a
feat(mdq): frontmatter-aware token index
Sep 15, 2026
bcfd076
refactor(mdq): move markdown-query into src/utils/mdq behind a shim
Sep 15, 2026
2dbae8a
feat(mdq): writes return MarkdownDoc so edits chain
Sep 15, 2026
8488e6e
feat(mdq): comment and html selectors, honest regex flags, loud selec…
Sep 15, 2026
3aa1de8
feat(mdq): value matchers, sugar layer, at/slice
Sep 15, 2026
3aba5b7
feat(mdq): remove and insert verbs with whitespace normalization
Sep 15, 2026
6347848
feat(mdq): addRow and addItem structural inserts
Sep 15, 2026
0036d7b
feat(mdq): frontmatter read/write via yaml Document API, setEntry
Sep 15, 2026
812b875
feat(mdq): jq-like CLI
Sep 15, 2026
c076c99
docs(mdq): package README
Sep 15, 2026
ab2495e
docs: changelog for mdq package and CLI
Sep 15, 2026
6b22aa6
test(mdq): pin how a Selection stringifies, and document it
Sep 15, 2026
b379e43
refactor(mdq): finish the query/edit boundary the spec draws
Sep 15, 2026
07a0e44
feat(mdq): publish as a standalone npm package runnable with npx
Sep 16, 2026
3238bad
refactor(mdq): collapse back to one class
Sep 16, 2026
4cf2256
docs(mdq): state where the selector grammar comes from
Sep 17, 2026
c679d48
Merge remote-tracking branch 'origin/main' into feat/mdq-package
Sep 17, 2026
4bf74d7
refactor(mdq): one editor class, consolidated constants
Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/workflows/publish-mdq.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Publish mdq to npm

on:
release:
types: [published]

jobs:
publish:
if: startsWith(github.event.release.tag_name, 'mdq-')
runs-on: ubuntu-latest

permissions:
contents: read
id-token: write

steps:
- name: Checkout the released commit
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
registry-url: https://registry.npmjs.org

- name: Upgrade npm for tokenless (OIDC) publishing
run: npm install -g npm@latest

- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest

- name: Install dependencies
run: bun install

- name: Run mdq tests
run: bun test tests/unit/mdq/

- name: Set version from release
id: release
env:
TAG: ${{ github.event.release.tag_name }}
PRERELEASE: ${{ github.event.release.prerelease }}
run: |
VERSION="${TAG#mdq-}"
VERSION="${VERSION#v}"
echo "Publishing mdq $VERSION from release $TAG"
npm version "$VERSION" --no-git-tag-version
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if [[ "$PRERELEASE" == "true" || "$VERSION" == *beta* || "$VERSION" == *pre* || "$VERSION" == *alpha* || "$VERSION" == *rc* ]]; then
echo "tag=beta" >> "$GITHUB_OUTPUT"
else
echo "tag=latest" >> "$GITHUB_OUTPUT"
fi

- name: Stage mdq package
run: bun run build:mdq

- name: Verify mdq tarball on Node.js
run: |
npm pack ./dist-mdq --pack-destination /tmp
mkdir -p /tmp/mdq-smoke && cd /tmp/mdq-smoke
npm init -y > /dev/null
npm install --ignore-scripts --no-audit --no-fund /tmp/mdq-*.tgz
printf '# Doc\n\n## API\n\n| Method | Path |\n|---|---|\n| GET | /users |\n' > sample.md

echo "CLI, file argument"
./node_modules/.bin/mdq 'h2' sample.md

echo "CLI, stdin"
cat sample.md | ./node_modules/.bin/mdq 'section("API") table' --json

echo "CLI, file argument while stdin is an open pipe (must not EAGAIN)"
( sleep 3 ) | ./node_modules/.bin/mdq 'h2' sample.md

echo "CLI, exit code 1 when nothing matches"
./node_modules/.bin/mdq 'h5' sample.md && exit 1 || test $? -eq 1

echo "Library import"
node --input-type=module -e "
import { mdq } from 'mdq';
const doc = mdq('---\nurl: /x\n---\n\n## A\n\n- one\n');
if (doc.frontmatter().url !== '/x') throw new Error('frontmatter');
if (doc.query('h2').count() !== 1) throw new Error('headings');
if (!doc.list().addItem('two').toString().includes('- two')) throw new Error('addItem');
"

- name: Publish mdq to npm
run: |
if npm view "mdq@${{ steps.release.outputs.version }}" version > /dev/null 2>&1; then
echo "mdq@${{ steps.release.outputs.version }} is already published, skipping"
exit 0
fi
npm publish ./dist-mdq --access public --tag ${{ steps.release.outputs.tag }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ session.json
# Build outputs
dist/
dist-prima/
dist-mdq/
build/
out/
.types-build/
Expand Down
57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,63 @@

## 2026-09-15

### New CLI Options

- **`mdq`** — A new command for reading and editing markdown from the shell, the way `jq` reads JSON.
It ships as its own npm package, so it runs without installing anything: `npx mdq 'h2' README.md`.
It also works as a library — `import { mdq } from 'mdq'` — on Node 18 or newer.
The first argument is a selector, the second an optional file (stdin is used when it is omitted).
Matched markdown is printed by default. Exit codes compose like `grep`: `0` when something matched,
`1` when nothing did, `2` for a bad selector or bad usage.

```bash
mdq 'h2' README.md # print every h2
cat plan.md | mdq 'section("API") table' # read from stdin
mdq 'comment(~"test")' plan.md --count # how many test comments
```

- **`--json`, `--count`, `--text`, `--frontmatter`** — Change what is printed: table rows as JSON,
the number of matches, the text with its markdown stripped, or the file's frontmatter as JSON.

```bash
mdq 'section("API") table' --json README.md
mdq 'h2' --count README.md
mdq --frontmatter knowledge/login.md
```

- **`--remove`, `--replace`, `--insert-before`, `--insert-after`, `--prepend`, `--append`, `--add-row`, `--add-item`, `--set`** —
Edit the file instead of reading it. The whole document is printed with the edit applied; one edit at a
time. `--add-row` takes a JSON object and lines the table's columns back up; `--add-item` copies the
list's existing bullet or numbering; `--set` takes `Key=value` and drops the entry when the value is empty.

```bash
mdq 'section("FAQ")' doc.md --remove
mdq 'table[0]' api.md --add-row '{"Method":"GET","Path":"/users"}'
mdq 'list' plan.md --add-item 'check the dashboard'
```

- **`-i`, `--in-place`** — Write the edit back to the file instead of printing it.

```bash
mdq 'section("Draft")' notes.md --remove -i
```

### Changes

- Knowledge and experience files are now read correctly when they start with a `---` block. The
frontmatter used to be read as a heading titled `url: /login`, so the first fact in every such file
could be mistaken for a section title.
- Blank lines are no longer lost or doubled when a block is deleted or inserted, and blank lines inside
fenced code blocks are left exactly as they were.
- A mistyped selector now fails with a message naming the unknown word, instead of quietly matching
nothing and looking like an empty file.
- A search written as `/pattern/` is now case-sensitive unless it ends with `i`, matching how quoted
searches already behaved. Previously every `/pattern/` ignored case whether it said so or not.
- Searching a table now looks at its cells, not only its column titles.
- Reading a file no longer waits on standard input. Passing a filename while standard input was an
open pipe used to abort with an `EAGAIN` error on Node; input is now read only when no filename
is given.

### Fixes

- [Pilot] A control the page has disabled is no longer read as one missing required field. Pilot had a
Expand Down
18 changes: 18 additions & 0 deletions bin/mdq.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env node
import { runMdq } from '../src/utils/mdq/cli.ts';

async function readStdin(): Promise<string> {
if (process.stdin.isTTY) return '';
process.stdin.setEncoding('utf8');
let text = '';
for await (const chunk of process.stdin) text += chunk;
return text;
}

const result = await runMdq(process.argv.slice(2), readStdin);
if (result.output) {
let text = result.output;
if (!text.endsWith('\n')) text = `${text}\n`;
process.stdout.write(text);
}
process.exit(result.code);
3 changes: 2 additions & 1 deletion boat/prima/src/prima.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ function dropVolatileColumns(markdown: string): string {
const divider = `|${columns.map(() => '------').join('|')}|`;
const body = rows.map((row) => `| ${columns.map((name) => row[name] || '-').join(' | ')} |`);
return [header, divider, ...body, ''].join('\n');
});
})
.toString();
}

function cap(text: string, max: number): string {
Expand Down
Loading
Loading