mdeval 1.3.1 → 1.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 +170 -9
- package/dist/cli.mjs +2 -4
- package/dist/loader.mjs +2 -0
- package/dist/md-loader.mjs +11 -4
- package/dist/parse-markdown-wnFVEAED.mjs +6 -0
- package/dist/runtime.mjs +4 -0
- package/package.json +6 -1
- package/skills/mdeval/SKILL.md +124 -3
- package/dist/parse-markdown-B18EMsYy.mjs +0 -4
package/README.md
CHANGED
|
@@ -9,6 +9,9 @@ mdeval embeds JavaScript directly in your Markdown using HTML comments. The logi
|
|
|
9
9
|
|
|
10
10
|
Any value that matters — a dependency count, a version string, a benchmark result — is computed, not typed. Whether written by a person or an AI agent, the expression shows the work and anyone can verify it. Re-run `mdeval` and every value updates.
|
|
11
11
|
|
|
12
|
+
> [!TIP]
|
|
13
|
+
> Particularly useful for **knowledge bases** — contact lists, project trackers, changelogs, or any Markdown that functions as a source of truth. Values stay accurate on every run, so nothing drifts. AI agents reading the files also benefit: pre-computed values mean they don't need to re-process raw data across multiple documents, cutting token usage significantly.
|
|
14
|
+
|
|
12
15
|
## Install
|
|
13
16
|
|
|
14
17
|
```bash
|
|
@@ -28,7 +31,12 @@ const pkg = JSON.parse(await fs.readFile('package.json', 'utf8'));
|
|
|
28
31
|
-->
|
|
29
32
|
```
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
Script blocks support:
|
|
35
|
+
|
|
36
|
+
- Full Node.js — any built-in module or installed package
|
|
37
|
+
- ESM `import` / `export`
|
|
38
|
+
- Top-level `await`
|
|
39
|
+
- Shell commands via [`$`](https://google.github.io/zx/)
|
|
32
40
|
|
|
33
41
|
A **value marker** is where the result appears. It starts with `<!--mdeval ` (with a space) followed by a JavaScript expression:
|
|
34
42
|
|
|
@@ -46,11 +54,136 @@ The file is updated in-place. You can pass multiple files or glob patterns: `mde
|
|
|
46
54
|
|
|
47
55
|
`node_modules` and hidden directories are automatically excluded from glob expansion.
|
|
48
56
|
|
|
57
|
+
## Example
|
|
58
|
+
|
|
59
|
+
**`CONTACTS.md`** — a contact list stored as data, rendered as a table when viewed:
|
|
60
|
+
|
|
61
|
+
````markdown
|
|
62
|
+
<!--mdeval
|
|
63
|
+
import { table, link } from 'md-pen';
|
|
64
|
+
|
|
65
|
+
export const contacts = [
|
|
66
|
+
{
|
|
67
|
+
name: 'Alice Martin',
|
|
68
|
+
linkedin: 'https://linkedin.com/in/alice-martin',
|
|
69
|
+
notes: 'Met at JSConf 2023',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'Bob Tanaka',
|
|
73
|
+
linkedin: 'https://linkedin.com/in/bob-tanaka',
|
|
74
|
+
notes: 'Former colleague at Acme Co.',
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
export const findContact = name => contacts.find(c => c.name === name);
|
|
79
|
+
|
|
80
|
+
const contactsTable = table(
|
|
81
|
+
contacts.map(({ name, linkedin, notes }) => ({ Name: link(linkedin, name), Notes: notes })),
|
|
82
|
+
);
|
|
83
|
+
-->
|
|
84
|
+
|
|
85
|
+
<!--mdeval block(contactsTable)-->
|
|
86
|
+
| Name | Notes |
|
|
87
|
+
| - | - |
|
|
88
|
+
| [Alice Martin](https://linkedin.com/in/alice-martin) | Met at JSConf 2023 |
|
|
89
|
+
| [Bob Tanaka](https://linkedin.com/in/bob-tanaka) | Former colleague at Acme Co. |
|
|
90
|
+
<!--/mdeval-->
|
|
91
|
+
````
|
|
92
|
+
|
|
93
|
+
**`TIMELINE.md`** — imports contacts and links them by LinkedIn:
|
|
94
|
+
|
|
95
|
+
````markdown
|
|
96
|
+
<!--mdeval
|
|
97
|
+
import { link, ul, bold } from 'md-pen';
|
|
98
|
+
|
|
99
|
+
// Imports from the mdeval block!
|
|
100
|
+
import { findContact } from './CONTACTS.md';
|
|
101
|
+
|
|
102
|
+
const events = [
|
|
103
|
+
{ date: '2023-10-15', description: 'Attended JSConf 2023' },
|
|
104
|
+
{ date: '2024-03-01', description: `Coffee with ${link(findContact('Alice Martin').linkedin, 'Alice Martin')}` },
|
|
105
|
+
{ date: '2024-11-08', description: 'Started new open-source project' },
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
const list = ul(events.map(({ date, description }) => `${bold(date)} — ${description}`));
|
|
109
|
+
-->
|
|
110
|
+
|
|
111
|
+
<!--mdeval block(list)-->
|
|
112
|
+
- __2023-10-15__ — Attended JSConf 2023
|
|
113
|
+
- __2024-03-01__ — Coffee with [Alice Martin](https://linkedin.com/in/alice-martin)
|
|
114
|
+
- __2024-11-08__ — Started new open-source project
|
|
115
|
+
<!--/mdeval-->
|
|
116
|
+
````
|
|
117
|
+
|
|
118
|
+
The source of truth for contacts live in `CONTACTS.md`. If contact info changes, run `mdeval **/*.md` and all files get re-rendered with the latest data.
|
|
119
|
+
|
|
120
|
+
## Helpers
|
|
121
|
+
|
|
122
|
+
mdeval ships two helpers used in markers:
|
|
123
|
+
|
|
124
|
+
| Helper | Description |
|
|
125
|
+
|--------|-------------|
|
|
126
|
+
| `block(value)` | Wraps the value with surrounding newlines so block-level Markdown (headings, lists, tables) renders correctly |
|
|
127
|
+
| `$` | [zx](https://google.github.io/zx/) shell — run commands via tagged templates |
|
|
128
|
+
|
|
129
|
+
How you access them depends on the file extension:
|
|
130
|
+
|
|
131
|
+
| File | Access |
|
|
132
|
+
|------|--------|
|
|
133
|
+
| `.md` | Available as globals — call directly: `block(x)` or `` $`cmd` `` |
|
|
134
|
+
| `.js`, `.ts`, anything else | Import explicitly: `import { block, $ } from 'mdeval'` |
|
|
135
|
+
|
|
136
|
+
The `.md` case works because the CLI (and `--import mdeval/loader`) seeds the helpers on `globalThis` at startup. Modules imported transitively from a `.md` see them too, but in non-`.md` files prefer the explicit import for clarity and portability. The plain `import { block, $ } from 'mdeval'` is side-effect-free — no globals are touched, no loader is registered.
|
|
137
|
+
|
|
138
|
+
## Using `.md` exports from a script
|
|
139
|
+
|
|
140
|
+
`.md` files with mdeval script blocks are real ESM modules — anything they `export` can be imported from a Node script. Useful for validation tools, migration scripts, or any code that needs to read structured data out of a Markdown knowledge base.
|
|
141
|
+
|
|
142
|
+
Run your script with `--import mdeval/loader`:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
node --import mdeval/loader ./consumer.js
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`consumer.js` can then use ordinary static imports against `.md` files:
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
import { todos } from './TODOS.md'
|
|
152
|
+
|
|
153
|
+
console.log(todos)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`--import mdeval/loader` is a side-effect-only entry. Before any of `consumer.js`'s imports are linked, it seeds `block` and `$` on `globalThis` and registers the Node ESM loader so `.md` files resolve as modules. Without it, the static `import` of a `.md` fails — Node has no idea how to load `.md` yet.
|
|
157
|
+
|
|
158
|
+
The plain `mdeval` import stays pure — `import { block, $ } from 'mdeval'` just gives you the helpers without touching `globalThis` or registering any loader. The split keeps "I want the helpers in my script" separate from "I want to load `.md` files".
|
|
159
|
+
|
|
160
|
+
You can also point `--import mdeval/loader` at a `.md` file directly:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
node --import mdeval/loader ./TODOS.md
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Runtime stack traces from a `.md` point at original lines and columns — the loader sets up source maps automatically.
|
|
167
|
+
|
|
49
168
|
## Notes
|
|
50
169
|
|
|
51
170
|
- A file can have **multiple script blocks** — they're merged into one module, so variables and imports are shared. We recommend placing them at the top to signal the file has generated content.
|
|
52
171
|
|
|
53
|
-
- Markers can
|
|
172
|
+
- **Markers can span multiple lines.** Don't cram everything into one line — give the expression room to breathe. This is what makes inlining marker-specific logic next to its output practical:
|
|
173
|
+
|
|
174
|
+
```markdown
|
|
175
|
+
<!--mdeval block(table([
|
|
176
|
+
{ name: 'cleye', version: '^2.3.0' },
|
|
177
|
+
{ name: 'md-pen', version: '^0.0.2' },
|
|
178
|
+
]))-->
|
|
179
|
+
| Package | Version |
|
|
180
|
+
| - | - |
|
|
181
|
+
| cleye | ^2.3.0 |
|
|
182
|
+
| md-pen | ^0.0.2 |
|
|
183
|
+
<!--/mdeval-->
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
When the expression needs statements or control flow, wrap it in an IIFE:
|
|
54
187
|
|
|
55
188
|
```markdown
|
|
56
189
|
<!--mdeval (() => {
|
|
@@ -60,18 +193,14 @@ The file is updated in-place. You can pass multiple files or glob patterns: `mde
|
|
|
60
193
|
})()-->odd<!--/mdeval-->
|
|
61
194
|
```
|
|
62
195
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
- Marker expressions are **auto-awaited** — promises resolve automatically, so you can use `fetch()` or any async API directly in a marker without wrapping it in a script block.
|
|
196
|
+
Coupling the expression to its rendered value keeps related code together and reads better than pushing every variable into a distant script block. Reserve script blocks for shared imports or values referenced by multiple markers.
|
|
66
197
|
|
|
67
|
-
-
|
|
198
|
+
- Marker expressions are **auto-awaited** — promises resolve automatically. Async APIs work directly in a marker without a script block:
|
|
68
199
|
|
|
69
200
|
```markdown
|
|
70
|
-
<!--mdeval
|
|
201
|
+
Status: <!--mdeval (await fetch('https://api.github.com/repos/privatenumber/mdeval')).status-->200<!--/mdeval-->
|
|
71
202
|
```
|
|
72
203
|
|
|
73
|
-
- If your value starts with a heading, list, or other block element, wrap it with **`block()`** so it renders on its own line. `block()` is a global helper that adds newlines before and after the value.
|
|
74
|
-
|
|
75
204
|
- You can **import from other `.md` files**. Only the script blocks are executed — no markers are processed:
|
|
76
205
|
|
|
77
206
|
```markdown
|
|
@@ -82,6 +211,15 @@ The file is updated in-place. You can pass multiple files or glob patterns: `mde
|
|
|
82
211
|
|
|
83
212
|
This lets you share constants and logic across multiple markdown files.
|
|
84
213
|
|
|
214
|
+
If the imported file might not yet have mdeval content (e.g. a stub being filled in over time), use a **namespace import** so missing exports resolve to `undefined` instead of crashing Node's ESM linker:
|
|
215
|
+
|
|
216
|
+
```markdown
|
|
217
|
+
<!--mdeval
|
|
218
|
+
import * as data from './stub.md';
|
|
219
|
+
const version = data.version ?? 'tbd';
|
|
220
|
+
-->
|
|
221
|
+
```
|
|
222
|
+
|
|
85
223
|
- When processing multiple files (`mdeval a.md b.md`), all files **share the same Node.js runtime** — including the module cache, `globalThis`, and `process.env`. This is the same behavior as any Node.js program using `import()`.
|
|
86
224
|
|
|
87
225
|
## Caveats
|
|
@@ -115,6 +253,29 @@ const depsTable = table(deps.map(([name, version]) => [
|
|
|
115
253
|
<!--/mdeval-->
|
|
116
254
|
```
|
|
117
255
|
|
|
256
|
+
## Git hook
|
|
257
|
+
|
|
258
|
+
Run mdeval automatically before every commit so values never go stale. [Lefthook](https://github.com/evilmartians/lefthook) makes this easy:
|
|
259
|
+
|
|
260
|
+
```sh
|
|
261
|
+
npm install lefthook --save-dev
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
```yaml
|
|
265
|
+
# lefthook.yml
|
|
266
|
+
pre-commit:
|
|
267
|
+
jobs:
|
|
268
|
+
- name: mdeval
|
|
269
|
+
# lefthook's default `gobwas` matcher requires `**` to span 1+ dirs,
|
|
270
|
+
# so `**/*.md` alone misses root-level files like README.md
|
|
271
|
+
glob: ["*.md", "**/*.md"]
|
|
272
|
+
run: |
|
|
273
|
+
files=$(npx mdeval "**/*.md")
|
|
274
|
+
[ -n "$files" ] && git add $files
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`glob` makes the hook a no-op when no Markdown files are staged. mdeval prints the path of each file it rewrites to stdout, so `git add $files` re-stages only the files it actually updated. Note: `$files` relies on shell word-splitting, so this assumes `.md` paths without spaces.
|
|
278
|
+
|
|
118
279
|
## Agent Skills
|
|
119
280
|
|
|
120
281
|
This package ships with a built-in [agent skill](./skills/mdeval/SKILL.md) for AI coding assistants. Set up [`skills-npm`](https://github.com/antfu/skills-npm) to automatically discover it.
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var M=Object.defineProperty;var
|
|
3
|
-
`)||s.includes(
|
|
4
|
-
${String(o)}
|
|
5
|
-
`,globalThis.$=S;const A=b({name:"mdeval",parameters:["<files...>"]}),u=A._.files,F=u.some(o=>o.includes("node_modules")),h=await x(u,{ignore:F?[]:["**/node_modules/**"]});h.length===0&&(console.error("No files matched the given patterns"),process.exit(1)),await Promise.all(h.map(async o=>{try{const r=$.resolve(o),t=await m.readFile(r,"utf8"),i=d(t);P(t,i)&&console.warn(`Warning: ${o} has no markdown content outside of mdeval blocks`);const n=await v(t,r);n!==t&&(await m.writeFile(r,n,"utf8"),console.log(`Updated: ${o}`))}catch(r){console.error(`Error processing ${o}:`,r instanceof Error?r.message:r),process.exitCode=1}}));
|
|
2
|
+
var M=Object.defineProperty;var l=(o,r)=>M(o,"name",{value:r,configurable:!0});import m from"node:fs/promises";import x from"node:path";import{cli as O}from"cleye";import{glob as S}from"tinyglobby";import"./loader.mjs";import{p as d,b as _,E as b,M as f,C,a as u,c as P,i as R}from"./parse-markdown-wnFVEAED.mjs";import{pathToFileURL as $}from"node:url";import"node:module";import"./runtime.mjs";import"zx";import"micromark";const k=l(o=>{if(o==null)throw new Error(`Cannot interpolate ${o} \u2014 this is likely a bug in your script`);return typeof o=="string"?o:typeof o=="object"?Symbol.toPrimitive in o?String(o):JSON.stringify(o):String(o)},"coerceValue"),N=l(async(o,r)=>{const{markers:t}=d(o);if(t.length===0)return o;const i=await import($(r).href),n=_(t),w=await Promise.all(Array.from(n).map(async([e,p])=>{const s=k(await i[`${b}${p}`]);if(s.includes(f)||s.includes(`${C}
|
|
3
|
+
`)||s.includes(u))throw new Error(`Expression "${e}" produced a value containing mdeval comment syntax, which would corrupt the document on re-parse`);return s})),a=[];let c=0;for(const e of t){const p=f.length+e.expression.length+P.length,s=e.start+p,E=e.end-u.length,y=n.get(e.expression);a.push(o.slice(c,s),w[y]),c=E}return a.push(o.slice(c)),a.join("")},"processSource"),T=O({name:"mdeval",parameters:["<files...>"]}),g=T._.files,v=g.some(o=>o.includes("node_modules")),h=await S(g,{ignore:v?[]:["**/node_modules/**"]});h.length===0&&(console.error("No files matched the given patterns"),process.exit(1)),await Promise.all(h.map(async o=>{try{const r=x.resolve(o),t=await m.readFile(r,"utf8"),i=d(t);R(t,i)&&console.warn(`Warning: ${o} has no markdown content outside of mdeval blocks`);const n=await N(t,r);n!==t&&(await m.writeFile(r,n,"utf8"),console.log(o))}catch(r){console.error(`Error processing ${o}:`,r instanceof Error?r.message:r),process.exitCode=1}}));
|
package/dist/loader.mjs
ADDED
package/dist/md-loader.mjs
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
3
|
-
`)
|
|
4
|
-
|
|
5
|
-
`)
|
|
2
|
+
var x=Object.defineProperty;var c=(e,a)=>x(e,"name",{value:a,configurable:!0});import b from"node:fs/promises";import{pathToFileURL as E,fileURLToPath as T}from"node:url";import{p as $,E as B,M as F}from"./parse-markdown-wnFVEAED.mjs";import{GenMapping as P,toEncodedMap as W,addSegment as U}from"@jridgewell/gen-mapping";import"micromark";const M=10,v=13,N=c(e=>e===9||e===M||e===11||e===12||e===v||e===32,"isWhitespaceCode"),j=c(e=>e>=48&&e<=57||e>=65&&e<=90||e===95||e>=97&&e<=122||e===36,"isIdentifierCode"),A=c((e,a)=>{const h=E(e).href,p=new P({file:e}),s=[];let i="";const o=[];for(let n=a.indexOf(`
|
|
3
|
+
`);n!==-1;n=a.indexOf(`
|
|
4
|
+
`,n+1))o.push(n);const y=c(n=>{let t=0,r=o.length;for(;t<r;){const f=Math.floor((t+r)/2);o[f]<n?t=f+1:r=f}const d=t>0?o[t-1]:-1;return{line:t,column:n-d-1}},"positionFromOffset"),u=c(n=>{i+=n},"appendLineChunk"),S=c(()=>{s.push(i),i=""},"flushLine"),O=c((n,t)=>{const r=y(t);let d=r.line,f=r.column,L=!1,R=!1,m=0;const w=c(l=>i.length+l-m,"generatedColumn");for(let l=0;l<n.length;l+=1){const g=n.codePointAt(l),C=N(g),I=j(g);!C&&(l===0||R||!L||!I)&&U(p,s.length,w(l),h,d,f),g===M?(d+=1,f=0):f+=1,L=I,R=C,g===M&&(u(n.slice(m,l)),S(),m=l+1)}m<n.length&&u(n.slice(m))},"appendSourceText"),k=c(n=>{let t=0;for(let r=n.indexOf(`
|
|
5
|
+
`);r!==-1;r=n.indexOf(`
|
|
6
|
+
`,t))u(n.slice(t,r)),S(),t=r+1;t<n.length&&u(n.slice(t))},"appendSyntheticText");return{appendSource(n,t){O(n,t)},appendSynthetic(n){k(n)},toModuleSource:c(()=>{const t=`${(i===""?s:[...s,i]).join(`
|
|
7
|
+
`)}
|
|
8
|
+
`,r=W(p),d=`//# sourceMappingURL=data:application/json;base64,${Buffer.from(JSON.stringify(r)).toString("base64")}`;return`${t}${d}
|
|
9
|
+
`},"toModuleSource")}},"createSourceBuilder"),X=c((e,a,h,p)=>{const s=A(a,e);for(const o of h)s.appendSource(o.content,o.contentStart),o.content.endsWith(`
|
|
10
|
+
`)||s.appendSynthetic(`
|
|
11
|
+
`);const i=new Map;for(const o of p)i.has(o.expression)||i.set(o.expression,{index:i.size,marker:o});for(const[o,{index:y,marker:u}]of i){const S=`export const ${B}${y} = `;s.appendSynthetic(S),s.appendSource(o,u.start+F.length),s.appendSynthetic(`;
|
|
12
|
+
`)}return s.toModuleSource()},"generateModule"),_=c(async(e,a,h)=>{if(!e.endsWith(".md"))return h(e,a);const p=T(e),s=await b.readFile(p,"utf8"),{scriptBlocks:i,markers:o}=$(s);return i.length===0&&o.length===0?{format:"module",source:"",shortCircuit:!0}:{format:"module",source:X(s,p,i,o),shortCircuit:!0}},"load");export{_ as load};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
var M=Object.defineProperty;var l=(n,e)=>M(n,"name",{value:e,configurable:!0});import{postprocess as v,parse as w,preprocess as k}from"micromark";const x="mdeval",c=`<!--${x}`,C=`${c} `,E=`<!--/${x}-->`,b=`__${x}_`,g="-->",_=l(n=>{const e=new Map;for(const{expression:s}of n)e.has(s)||e.set(s,e.size);return e},"buildExpressionMap"),$=l(n=>{const e=n.codePointAt(0)===65279?1:0,s=v(w().document().write(k()(n,void 0,!0))),r=[];for(const[o,t]of s)o==="enter"&&(t.type==="codeFenced"||t.type==="codeIndented"||t.type==="codeText")&&r.push([t.start.offset+e,t.end.offset+e]);return r},"findCodeRanges"),R=l(n=>{let e=null,s=0;return r=>{for(e===null&&(e=$(n));s<e.length&&e[s][1]<=r;)s+=1;return s<e.length&&r>=e[s][0]}},"createIsInCode"),u=l((n,e)=>{let s=e;for(;s<n.length;){const r=n.indexOf(c,s);if(r===-1)return-1;const o=n[r+c.length];if(o===" "||o===`
|
|
2
|
+
`||o==="\r"&&n[r+c.length+1]===`
|
|
3
|
+
`)return r;s=r+c.length}return-1},"findNextMdevalOpening"),S=l(n=>{if(!n.includes(c))return{scriptBlocks:[],markers:[]};const e=R(n),s=[],r=[];let o=0;for(;o<n.length;){const t=n.indexOf(c,o);if(t===-1)break;const i=t+c.length,m=n[i];if(m===`
|
|
4
|
+
`||m==="\r"&&n[i+1]===`
|
|
5
|
+
`){const f=m===`
|
|
6
|
+
`?i+1:i+2,a=n.indexOf(g,f),d=u(n,f),p=d!==-1&&(a===-1||d<a);if(a===-1||p){if(!e(t))throw new Error(p?`mdeval: unclosed script block at offset ${t} (another mdeval opening found before \`-->\`)`:`mdeval: unclosed script block at offset ${t} (missing \`-->\`)`);if(a===-1)break;o=f;continue}const h=a+g.length;e(t)||s.push({content:n.slice(f,a),contentStart:f,start:t,end:h}),o=h;continue}if(m===" "){const f=i+1,a=n.indexOf(g,f);if(a===-1){if(e(t))break;throw new Error(`mdeval: unclosed marker open at offset ${t} (missing \`-->\` on the marker open)`)}const d=a+g.length,p=n.indexOf(E,d),h=u(n,d);if(p===-1||h!==-1&&h<p){if(e(t)){o=d;continue}throw new Error(`mdeval: unclosed marker open at offset ${t} (missing \`<!--/mdeval-->\`)`)}const O=p+E.length;e(t)||r.push({expression:n.slice(f,a),start:t,end:O}),o=O;continue}o=i}return{scriptBlocks:s,markers:r}},"parseMarkdown"),I=l((n,{scriptBlocks:e,markers:s})=>{if(e.length===0&&s.length===0)return!1;const r=[...e.map(t=>[t.start,t.end]),...s.map(t=>[t.start,t.end])].sort((t,i)=>t[0]-i[0]);let o=0;for(const[t,i]of r){if(n.slice(o,t).trim()!=="")return!1;o=i}return n.slice(o).trim()===""},"isOnlyMdeval");export{c as C,b as E,C as M,E as a,_ as b,g as c,I as i,S as p};
|
package/dist/runtime.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mdeval",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Evaluate JavaScript in markdown HTML comments and interpolate results in-place",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
],
|
|
22
22
|
"type": "module",
|
|
23
23
|
"bin": "./dist/cli.mjs",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./dist/runtime.mjs",
|
|
26
|
+
"./loader": "./dist/loader.mjs"
|
|
27
|
+
},
|
|
24
28
|
"imports": {
|
|
25
29
|
"#md-loader": "./dist/md-loader.mjs"
|
|
26
30
|
},
|
|
@@ -28,6 +32,7 @@
|
|
|
28
32
|
"node": ">=20"
|
|
29
33
|
},
|
|
30
34
|
"dependencies": {
|
|
35
|
+
"@jridgewell/gen-mapping": "^0.3.13",
|
|
31
36
|
"cleye": "^2.3.0",
|
|
32
37
|
"micromark": "^4.0.2",
|
|
33
38
|
"tinyglobby": "^0.2.15",
|
package/skills/mdeval/SKILL.md
CHANGED
|
@@ -47,13 +47,42 @@ Duplicate expressions across markers are evaluated once and reused.
|
|
|
47
47
|
| `Promise` | Auto-awaited, then coerced |
|
|
48
48
|
| `undefined`, `null` | Error |
|
|
49
49
|
|
|
50
|
-
##
|
|
50
|
+
## Helpers
|
|
51
51
|
|
|
52
|
-
|
|
|
52
|
+
| Helper | Description |
|
|
53
53
|
|--------|-------------|
|
|
54
54
|
| `block(value)` | Wraps value with newlines for block-level rendering |
|
|
55
55
|
| `$` | [zx](https://google.github.io/zx/) shell — run commands via tagged templates: `` $`git branch` `` |
|
|
56
56
|
|
|
57
|
+
| File | Access |
|
|
58
|
+
|------|--------|
|
|
59
|
+
| `.md` | Globals — call directly: `block(x)`, `` $`cmd` `` |
|
|
60
|
+
| `.js`, `.ts`, anything else | `import { block, $ } from 'mdeval'` |
|
|
61
|
+
|
|
62
|
+
CLI (and `--import mdeval/loader`) seeds the helpers on `globalThis` at startup. Modules imported transitively from a `.md` see them too, but in non-`.md` files prefer the explicit import. `import { block, $ } from 'mdeval'` is side-effect-free.
|
|
63
|
+
|
|
64
|
+
## Importing `.md` exports from a script
|
|
65
|
+
|
|
66
|
+
Use when a Node script (validation, migration, agent tooling) needs to read exports from a `.md`. Run the script with `--import mdeval/loader`:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
node --import mdeval/loader ./consumer.js
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`consumer.js` can use ordinary static imports against `.md` files:
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
import { todos } from './TODOS.md';
|
|
76
|
+
|
|
77
|
+
console.log(todos);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- `--import mdeval/loader` is a side-effect-only entry — seeds `block`/`$` on `globalThis` and registers the Node ESM loader before any of the script's imports link.
|
|
81
|
+
- Without it, static `import` of a `.md` fails — Node can't resolve `.md` until the loader is registered.
|
|
82
|
+
- The plain `mdeval` import (`import { block, $ } from 'mdeval'`) stays pure — no globals, no loader. Use it when you only want the helpers.
|
|
83
|
+
- Works on `.md` directly too: `node --import mdeval/loader ./TODOS.md`.
|
|
84
|
+
- Runtime stack traces from a `.md` point at original lines and columns automatically.
|
|
85
|
+
|
|
57
86
|
## CLI
|
|
58
87
|
|
|
59
88
|
```bash
|
|
@@ -98,6 +127,15 @@ import { version } from './data.md';
|
|
|
98
127
|
<!--mdeval version-->1.0.0<!--/mdeval-->
|
|
99
128
|
````
|
|
100
129
|
|
|
130
|
+
If the imported `.md` may not yet have mdeval content (stubs filled in over time), use a **namespace import** — named imports against an empty module are rejected by Node's ESM linker, but missing properties on a namespace resolve to `undefined`:
|
|
131
|
+
|
|
132
|
+
````markdown
|
|
133
|
+
<!--mdeval
|
|
134
|
+
import * as data from './stub.md';
|
|
135
|
+
const version = data.version ?? 'tbd';
|
|
136
|
+
-->
|
|
137
|
+
````
|
|
138
|
+
|
|
101
139
|
### Generate Markdown with md-pen
|
|
102
140
|
|
|
103
141
|
Use [md-pen](https://github.com/privatenumber/md-pen) for formatted output (tables, lists, headings):
|
|
@@ -148,7 +186,90 @@ const x = String.fromCharCode(45, 45, 62);
|
|
|
148
186
|
|
|
149
187
|
**Place scripts at the top.** Order doesn't affect execution — scripts can appear after the markers that reference them — but top placement signals the file contains generated content.
|
|
150
188
|
|
|
151
|
-
**
|
|
189
|
+
**Markers can span multiple lines — use them to inline logic.** Don't cram expressions into one line. Co-locate marker-specific computation with its output:
|
|
190
|
+
|
|
191
|
+
````markdown
|
|
192
|
+
<!--mdeval block(table([
|
|
193
|
+
{ name: 'cleye', version: '^2.3.0' },
|
|
194
|
+
{ name: 'md-pen', version: '^0.0.2' },
|
|
195
|
+
]))-->
|
|
196
|
+
| Package | Version |
|
|
197
|
+
| - | - |
|
|
198
|
+
| cleye | ^2.3.0 |
|
|
199
|
+
| md-pen | ^0.0.2 |
|
|
200
|
+
<!--/mdeval-->
|
|
201
|
+
````
|
|
202
|
+
|
|
203
|
+
For statements or control flow, wrap in an IIFE: `<!--mdeval (() => { ... })()-->`. Reserve script blocks for shared imports or values referenced by multiple markers.
|
|
204
|
+
|
|
205
|
+
**Standalone markers suppress inline markdown in the value.** When a marker opens a line — standalone paragraph, list item, or blockquote — GitHub treats the entire line as a raw HTML block. Inline markdown in the value (`` `code` ``, `**bold**`, `[link](url)`) is not processed and appears literally.
|
|
206
|
+
|
|
207
|
+
````markdown
|
|
208
|
+
<!-- ❌ backtick shows literally — line is an HTML block, not inline markdown -->
|
|
209
|
+
<!--mdeval expr-->`value`<!--/mdeval-->
|
|
210
|
+
````
|
|
211
|
+
|
|
212
|
+
Use a multi-line marker so the value occupies its own lines, which are processed as normal markdown. Use [md-pen](https://github.com/privatenumber/md-pen) to generate formatted values:
|
|
213
|
+
|
|
214
|
+
````markdown
|
|
215
|
+
<!--mdeval
|
|
216
|
+
import { code, bold, link } from 'md-pen';
|
|
217
|
+
-->
|
|
218
|
+
|
|
219
|
+
<!--mdeval code('asdf')-->
|
|
220
|
+
`asdf`
|
|
221
|
+
<!--/mdeval-->
|
|
222
|
+
````
|
|
223
|
+
|
|
224
|
+
Markers inside headings are always inline — markdown in the value always renders there.
|
|
225
|
+
|
|
226
|
+
**Comments don't work in link URLs or image alt text.** GitHub escapes comment syntax in these positions:
|
|
227
|
+
|
|
228
|
+
````markdown
|
|
229
|
+
<!-- ❌ comment is URL-encoded as href -->
|
|
230
|
+
[text](<!-- comment -->)
|
|
231
|
+
|
|
232
|
+
<!-- ❌ comment appears as visible alt text -->
|
|
233
|
+

|
|
234
|
+
````
|
|
235
|
+
|
|
236
|
+
**`<details>` requires blank lines around the `</summary>` close.** Block-level Markdown inside a `<details>` only renders when blank lines surround the closing `</summary>` tag — otherwise lists, code fences, tables, and other block elements render as literal text. md-pen's `details(summary, content)` emits the correct shape:
|
|
237
|
+
|
|
238
|
+
````markdown
|
|
239
|
+
<!--mdeval
|
|
240
|
+
import { details, ul } from 'md-pen';
|
|
241
|
+
-->
|
|
242
|
+
|
|
243
|
+
<!--mdeval block(details(
|
|
244
|
+
'Click me',
|
|
245
|
+
ul(['item one', 'item two']),
|
|
246
|
+
))-->
|
|
247
|
+
<details>
|
|
248
|
+
<summary>Click me</summary>
|
|
249
|
+
|
|
250
|
+
- item one
|
|
251
|
+
- item two
|
|
252
|
+
|
|
253
|
+
</details>
|
|
254
|
+
<!--/mdeval-->
|
|
255
|
+
````
|
|
256
|
+
|
|
257
|
+
**Set up a git hook so values never go stale.** Use [Lefthook](https://github.com/evilmartians/lefthook) with this config:
|
|
258
|
+
|
|
259
|
+
```yaml
|
|
260
|
+
# lefthook.yml
|
|
261
|
+
pre-commit:
|
|
262
|
+
jobs:
|
|
263
|
+
- name: mdeval
|
|
264
|
+
# lefthook's default `gobwas` matcher requires `**` to span 1+ dirs,
|
|
265
|
+
# so `**/*.md` alone misses root-level files like README.md
|
|
266
|
+
glob: ["*.md", "**/*.md"]
|
|
267
|
+
run: |
|
|
268
|
+
files=$(npx mdeval "**/*.md")
|
|
269
|
+
[ -n "$files" ] && git add $files
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`git add $files` re-stages only the files mdeval actually rewrote. Assumes `.md` paths without spaces.
|
|
152
273
|
|
|
153
274
|
**Markers in code blocks are safe.** Fenced, indented, and inline code won't be touched — safe to document mdeval syntax in your own README.
|
|
154
275
|
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
var M=Object.defineProperty;var i=(t,n)=>M(t,"name",{value:n,configurable:!0});import{postprocess as O,parse as u,preprocess as C}from"micromark";const g="mdeval",d=`<!--${g}`,_=`${d} `,E=`<!--/${g}-->`,R=`__${g}_`,p="-->",S=i(t=>{const n=new Map;for(const{expression:s}of t)n.has(s)||n.set(s,n.size);return n},"buildExpressionMap"),k=i(t=>{const n=t.codePointAt(0)===65279?1:0,s=O(u().document().write(C()(t,void 0,!0))),r=[];for(const[o,e]of s)o==="enter"&&(e.type==="codeFenced"||e.type==="codeIndented"||e.type==="codeText")&&r.push([e.start.offset+n,e.end.offset+n]);return r},"findCodeRanges"),I=i(t=>{let n=null,s=0;return r=>{for(n===null&&(n=k(t));s<n.length&&n[s][1]<=r;)s+=1;return s<n.length&&r>=n[s][0]}},"createIsInCode"),T=i(t=>{if(!t.includes(d))return{scriptBlocks:[],markers:[]};const n=I(t),s=[],r=[];let o=0;for(;o<t.length;){const e=t.indexOf(d,o);if(e===-1)break;const c=e+d.length,h=t[c];if(h===`
|
|
2
|
-
`||h==="\r"&&t[c+1]===`
|
|
3
|
-
`){const f=h===`
|
|
4
|
-
`?c+1:c+2,a=t.indexOf(p,f);if(a===-1)break;const l=a+p.length;n(e)||s.push({content:t.slice(f,a),start:e,end:l}),o=l;continue}if(h===" "){const f=c+1,a=t.indexOf(p,f);if(a===-1)break;const l=a+p.length,x=t.indexOf(E,l);if(x===-1){o=l;continue}const m=x+E.length;n(e)||r.push({expression:t.slice(f,a),start:e,end:m}),o=m;continue}o=c}return{scriptBlocks:s,markers:r}},"parseMarkdown"),b=i((t,{scriptBlocks:n,markers:s})=>{if(n.length===0&&s.length===0)return!1;const r=[...n.map(e=>[e.start,e.end]),...s.map(e=>[e.start,e.end])].sort((e,c)=>e[0]-c[0]);let o=0;for(const[e,c]of r){if(t.slice(o,e).trim()!=="")return!1;o=c}return t.slice(o).trim()===""},"isOnlyMdeval");export{d as C,R as E,_ as M,E as a,S as b,p as c,b as i,T as p};
|