mdeval 1.4.0 → 1.6.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 CHANGED
@@ -31,7 +31,12 @@ const pkg = JSON.parse(await fs.readFile('package.json', 'utf8'));
31
31
  -->
32
32
  ```
33
33
 
34
- Full Node.js, ESM imports, top-level `await`, and shell commands via [`$`](https://google.github.io/zx/) are supported.
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/)
35
40
 
36
41
  A **value marker** is where the result appears. It starts with `<!--mdeval ` (with a space) followed by a JavaScript expression:
37
42
 
@@ -112,11 +117,73 @@ const list = ul(events.map(({ date, description }) => `${bold(date)} — ${descr
112
117
 
113
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.
114
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
+
115
168
  ## Notes
116
169
 
117
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.
118
171
 
119
- - Markers can be **self-contained** — no script block needed:
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:
120
187
 
121
188
  ```markdown
122
189
  <!--mdeval (() => {
@@ -126,18 +193,14 @@ The source of truth for contacts live in `CONTACTS.md`. If contact info changes,
126
193
  })()-->odd<!--/mdeval-->
127
194
  ```
128
195
 
129
- In large documents, this IIFE pattern lets you keep logic next to the marker it serves instead of in a distant 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.
130
197
 
131
- - 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.
132
-
133
- - **`$`** from [zx](https://google.github.io/zx/) is available as a global — run shell commands directly in markers:
198
+ - Marker expressions are **auto-awaited** — promises resolve automatically. Async APIs work directly in a marker without a script block:
134
199
 
135
200
  ```markdown
136
- <!--mdeval $`git branch --show-current`-->main<!--/mdeval-->
201
+ Status: <!--mdeval (await fetch('https://api.github.com/repos/privatenumber/mdeval')).status-->200<!--/mdeval-->
137
202
  ```
138
203
 
139
- - 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.
140
-
141
204
  - You can **import from other `.md` files**. Only the script blocks are executed — no markers are processed:
142
205
 
143
206
  ```markdown
@@ -148,6 +211,15 @@ The source of truth for contacts live in `CONTACTS.md`. If contact info changes,
148
211
 
149
212
  This lets you share constants and logic across multiple markdown files.
150
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
+
151
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()`.
152
224
 
153
225
  ## Caveats
@@ -181,6 +253,29 @@ const depsTable = table(deps.map(([name, version]) => [
181
253
  <!--/mdeval-->
182
254
  ```
183
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
+ [ -z "$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
+
184
279
  ## Agent Skills
185
280
 
186
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,8 @@
1
1
  #!/usr/bin/env node
2
- var M=Object.defineProperty;var p=(o,r)=>M(o,"name",{value:r,configurable:!0});import m from"node:fs/promises";import $ from"node:path";import{cli as b}from"cleye";import{glob as x}from"tinyglobby";import{$ as S}from"zx";import{p as d,b as O,E as _,M as f,C as k,a as g,c as C,i as P}from"./parse-markdown-B18EMsYy.mjs";import{register as R}from"node:module";import{pathToFileURL as T}from"node:url";import"micromark";const N=p(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");R("#md-loader",import.meta.url);const v=p(async(o,r)=>{const{markers:t}=d(o);if(t.length===0)return o;const i=await import(T(r).href),n=O(t),w=await Promise.all(Array.from(n).map(async([e,l])=>{const s=N(await i[`${_}${l}`]);if(s.includes(f)||s.includes(`${k}
3
- `)||s.includes(g))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 l=f.length+e.expression.length+C.length,s=e.start+l,E=e.end-g.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");globalThis.block=o=>`
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 C=Object.defineProperty;var a=(t,e)=>C(t,"name",{value:e,configurable:!0});import h from"node:fs/promises";import E from"node:path";import{cli as M}from"cleye";import{glob as N}from"tinyglobby";import{yellow as $}from"ansis";import"./loader.mjs";import{p as g,b as R,E as T,M as k,C as m,a as d,c as A,i as F}from"./parse-markdown-wnFVEAED.mjs";import{pathToFileURL as P}from"node:url";import{fromMarkdown as S}from"mdast-util-from-markdown";import{gfmFootnote as _}from"micromark-extension-gfm-footnote";import{gfmFootnoteFromMarkdown as I}from"mdast-util-gfm-footnote";import{gfmTable as W}from"micromark-extension-gfm-table";import{gfmTableFromMarkdown as V}from"mdast-util-gfm-table";import{toHast as H}from"mdast-util-to-hast";import{visitParents as X}from"unist-util-visit-parents";import{parseFragment as j}from"parse5";import"node:module";import"./runtime.mjs";import"zx";import"micromark";const G=a(t=>{if(t==null)throw new Error(`Cannot interpolate ${t} \u2014 this is likely a bug in your script`);return typeof t=="string"?t:typeof t=="object"?Symbol.toPrimitive in t?String(t):JSON.stringify(t):String(t)},"coerceValue"),K=a(async(t,e)=>{const{markers:o}=g(t);if(o.length===0)return t;const n=await import(P(e).href),s=R(o),r=await Promise.all(Array.from(s).map(async([l,f])=>{const u=G(await n[`${T}${f}`]);if(u.includes(k)||u.includes(`${m}
3
+ `)||u.includes(d))throw new Error(`Expression "${l}" produced a value containing mdeval comment syntax, which would corrupt the document on re-parse`);return u})),i=[];let c=0;for(const l of o){const f=k.length+l.expression.length+A.length,u=l.start+f,x=l.end-d.length,b=s.get(l.expression);i.push(t.slice(c,u),r[b]),c=x}return i.push(t.slice(c)),i.join("")},"processSource"),B=a(t=>{const e=[];for(let o=0;o<t.length;o+=1)t[o]===`
4
+ `&&e.push(o);return e},"buildLineIndex"),D=a((t,e)=>{let o=0,n=t.length;for(;o<n;){const r=Math.floor((o+n)/2);t[r]<e?o=r+1:n=r}const s=o===0?-1:t[o-1];return{line:o+1,column:e-s}},"offsetToLineColumn"),w=a((t,e,o,n)=>{let s=t,r=e;for(let i=0;i<n;i+=1)o[i]===`
5
+ `?(s+=1,r=1):r+=1;return{line:s,column:r}},"advanceByValue"),J=a((t,e)=>{if(!t.startsWith(m,e))return!1;const o=t[e+m.length];return o===" "||o===`
6
+ `?!0:o==="\r"&&t[e+m.length+1]===`
7
+ `},"isMarkerOpening"),p=a((t,e=0,o=t.length)=>{const n=[];let s=0,r=e;for(;r<o;){const i=t.indexOf("<!--",r);if(i===-1||i>=o)break;J(t,i)?(n.push(i),s+=1,r=i+m.length):t.startsWith(d,i)?(s>0?s-=1:n.push(i),r=i+d.length):r=i+4}return n},"findMarkerLeaks"),U=a((t,e,o)=>{if(!t.attrs||!t.sourceCodeLocation?.attrs)return[];const n=[];for(const s of t.attrs){const r=t.sourceCodeLocation.attrs[s.name];if(!r)continue;const i=e+r.startOffset,{line:c,column:l}=o(i);for(const f of p(s.value))n.push({line:c,column:l})}return n},"attributeLeaks"),q=a((t,e,o)=>{const{value:n}=t;if(t.nodeName!=="#text"||typeof n!="string"||t.sourceCodeLocation?.startOffset===void 0)return[];const s=e+t.sourceCodeLocation.startOffset,r=o(s);return p(n).map(i=>{const{line:c,column:l}=w(r.line,r.column,n,i);return{line:c,column:l}})},"textLeaks"),z=a((t,e,o,n)=>{const s=t.slice(e,o),r=j(s,{sourceCodeLocationInfo:!0}),i=[],c=a(l=>{i.push(...U(l,e,n),...q(l,e,n));for(const f of l.childNodes??[])c(f)},"visit");return c(r),i},"findRawHtmlLeaks"),Q=a(t=>{let e=!1;for(const o of t){if(o.tagName==="pre")return"code block";o.tagName==="code"&&(e=!0)}return e?"inline code":"text"},"classifyTextLeak"),Y=a((t,e)=>e==="alt"?"image alt":t.tagName==="a"?"link title":"image title","classifyAttributeLeak"),v=a(t=>{const e=t.position?.start.offset,o=t.position?.end.offset;if(!(e===void 0||o===void 0))return{startOffset:e,endOffset:o,startLine:t.position?.start.line,startColumn:t.position?.start.column}},"rangeOf"),Z=a((t,e)=>{const o=v(t);if(o)return o;for(let n=e.length-1;n>=0;n-=1){const s=v(e[n]);if(s)return s}return{}},"positionRange"),tt=a((t,e,o)=>{if(e.startOffset!==void 0){if(o==="code block"){const n=t[e.startOffset];if(n===" "||n===" "){let r=e.startOffset;for(;t[r]===" "||t[r]===" ";)r+=1;return r}const s=t.indexOf(`
8
+ `,e.startOffset);return s===-1?e.startOffset:s+1}if(o==="inline code"){let n=e.startOffset;for(;t[n]==="`";)n+=1;return n}return e.startOffset}},"findContentStart"),et=a((t,e,o)=>t.map(n=>{const{line:s,column:r}=e(n);return{kind:o,line:s,column:r}}),"mapOffsetsToLeaks"),ot=a((t,e,o)=>e===void 0?{line:t.startLine??1,column:t.startColumn??1}:o(e),"resolveContentStart"),nt=a((t,e,o,n)=>e.map(s=>{const{line:r,column:i}=w(o.line,o.column,t,s);return{kind:n,line:r,column:i}}),"valueWalkLeaks"),rt=a((t,e,o,n,s)=>{const r=o.value??"";if(!r.includes("mdeval"))return[];const i=p(r);if(i.length===0)return[];const c=Z(o,n),l=tt(t,c,s);if(l!==void 0&&c.endOffset!==void 0){const u=p(t,l,c.endOffset);if(u.length===i.length)return et(u,e,s)}const f=ot(c,l,e);return nt(r,i,f,s)},"collectTextNodeLeaks"),st=a((t,e,o,n)=>{if(!o.includes("mdeval"))return[];const s=p(o);if(s.length===0)return[];const r=e.position?.start.offset??-1,{line:i,column:c}=r===-1?{line:e.position?.start.line??1,column:e.position?.start.column??1}:t(r);return s.map(()=>({kind:n,line:i,column:c}))},"collectAttributeLeaks"),it=a((t,e,o,n)=>o.type!=="text"||typeof o.value!="string"?[]:rt(t,e,o,n,Q(n)),"textNodeLeaks"),at=a((t,e,o)=>{if(o.type!=="raw"||typeof o.value!="string")return[];const n=o.position?.start.offset,s=o.position?.end.offset;return n===void 0||s===void 0?[]:z(t,n,s,e).map(r=>({kind:"raw html",...r}))},"rawNodeLeaks"),ct=a((t,e)=>{if(e.type!=="element"||!e.properties)return[];const o=[];for(const n of["alt","title"]){const s=e.properties[n];if(typeof s=="string"){const r=Y(e,n);o.push(...st(t,e,s,r))}}return o},"elementAttributeLeaks"),lt=a(t=>{if(!t.includes("mdeval"))return[];const e=t.codePointAt(0)===65279?t.slice(1):t,o=B(e),n=a(c=>D(o,c),"point"),s=S(e,{extensions:[_(),W()],mdastExtensions:[I(),V()]}),r=[],i=H(s,{allowDangerousHtml:!0});return X(i,(c,l)=>{const f=c;r.push(...it(e,n,f,l),...at(e,n,f),...ct(n,f))}),r},"findRenderedLeaks"),L=$("Warning:"),ft=M({name:"mdeval",parameters:["<files...>"]}),O=ft._.files,ut=O.some(t=>t.includes("node_modules")),y=await N(O,{ignore:ut?[]:["**/node_modules/**"]});y.length===0&&(console.error("No files matched the given patterns"),process.exit(1)),await Promise.all(y.map(async t=>{try{const e=E.resolve(t),o=await h.readFile(e,"utf8"),n=g(o);F(o,n)&&console.warn(`${L} ${t} has no markdown content outside of mdeval blocks`);const s=await K(o,e);s!==o&&(await h.writeFile(e,s,"utf8"),console.log(t));for(const r of lt(s))console.warn(`${L} ${t}:${r.line}:${r.column} mdeval marker leaks into rendered ${r.kind}`)}catch(e){console.error(`Error processing ${t}:`,e instanceof Error?e.message:e),process.exitCode=1}}));
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{register as r}from"node:module";import{block as o}from"./runtime.mjs";import{$ as e}from"zx";Object.assign(globalThis,{block:o,$:e}),process.setSourceMapsEnabled(!0),r("#md-loader",import.meta.url);
@@ -1,5 +1,12 @@
1
1
  #!/usr/bin/env node
2
- var p=Object.defineProperty;var i=(r,o)=>p(r,"name",{value:o,configurable:!0});import c from"node:fs/promises";import{fileURLToPath as m}from"node:url";import{p as f,b as u,E as d}from"./parse-markdown-B18EMsYy.mjs";import"micromark";const l=i((r,o)=>{const e=r.map(t=>t.content).join(`
3
- `),s=u(o);if(s.size===0)return e;const n=[...s].map(([t,a])=>`export const ${d}${a} = ${t};`);return`${e}
4
- ${n.join(`
5
- `)}`},"generateModule"),$=i(async(r,o,e)=>{if(!r.endsWith(".md"))return e(r,o);const s=await c.readFile(m(r),"utf8"),{scriptBlocks:n,markers:t}=f(s);return n.length===0&&t.length===0?e(r,o):{format:"module",source:l(n,t),shortCircuit:!0}},"load");export{$ as load};
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};
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ var c=Object.defineProperty;var r=(o,t)=>c(o,"name",{value:t,configurable:!0});import{$}from"zx";const e=r(o=>`
3
+ ${String(o)}
4
+ `,"block");export{$,e as block};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdeval",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Evaluate JavaScript in markdown HTML comments and interpolate results in-place",
5
5
  "keywords": [
6
6
  "markdown",
@@ -21,16 +21,30 @@
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
  },
27
31
  "engines": {
28
- "node": ">=20"
32
+ "node": ">=20.19"
29
33
  },
30
34
  "dependencies": {
35
+ "@jridgewell/gen-mapping": "^0.3.13",
36
+ "ansis": "^4.3.0",
31
37
  "cleye": "^2.3.0",
38
+ "mdast-util-from-markdown": "^2.0.3",
39
+ "mdast-util-gfm-footnote": "^2.1.0",
40
+ "mdast-util-gfm-table": "^2.0.0",
41
+ "mdast-util-to-hast": "^13.2.1",
32
42
  "micromark": "^4.0.2",
43
+ "micromark-extension-gfm-footnote": "^2.1.0",
44
+ "micromark-extension-gfm-table": "^2.1.1",
45
+ "parse5": "^8.0.1",
33
46
  "tinyglobby": "^0.2.15",
47
+ "unist-util-visit-parents": "^6.0.2",
34
48
  "zx": "8.8.5-lite"
35
49
  }
36
50
  }
@@ -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
- ## Globals
50
+ ## Helpers
51
51
 
52
- | Global | Description |
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,21 @@ 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
- **Use IIFEs to co-locate logic.** In large docs, keep marker-specific computation inline instead of in a distant script block: `<!--mdeval (() => { ... })()-->`.
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.
152
204
 
153
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.
154
206
 
@@ -181,6 +233,44 @@ Markers inside headings are always inline — markdown in the value always rende
181
233
  ![<!-- comment -->](image.png)
182
234
  ````
183
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
+ [ -z "$files" ] || git add $files
270
+ ```
271
+
272
+ `git add $files` re-stages only the files mdeval actually rewrote. Assumes `.md` paths without spaces.
273
+
184
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.
185
275
 
186
276
  **Never create a .md file with only a script block and no real content.** Markdown files must contain actual prose/documentation. For shared logic or utilities, create a `.js` or `.ts` file and import it from your markdown instead.
@@ -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};