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 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
- 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/)
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 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:
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
- In large documents, this IIFE pattern lets you keep logic next to the marker it serves instead of in a distant script block.
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
- - **`$`** 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:
68
199
 
69
200
  ```markdown
70
- <!--mdeval $`git branch --show-current`-->main<!--/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 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 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}}));
@@ -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.3.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",
@@ -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,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
- **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.
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
+ ![<!-- comment -->](image.png)
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};