markedin-parser 0.1.2 → 0.2.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 +13 -6
- package/package.json +1 -1
- package/parse.js +71 -6
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# markedin-parser
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Markedin (`.mi`) is a file format for both machines and humans. YAML frontmatter + templated Markdown.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`markedin-parser` parses and renders `.mi` (markedin) files. No framework required. Perfect for file-based agentic systems.
|
|
6
6
|
|
|
7
7
|
Full documentation at [markedin.dev](https://markedin.dev)
|
|
8
8
|
|
|
@@ -17,6 +17,7 @@ npm install markedin-parser
|
|
|
17
17
|
A `.mi` file has YAML frontmatter between `---` delimiters and a Markdown body that renders from it.
|
|
18
18
|
|
|
19
19
|
`task.mi`:
|
|
20
|
+
|
|
20
21
|
```
|
|
21
22
|
---
|
|
22
23
|
task: Implement rate limiting
|
|
@@ -61,12 +62,18 @@ First note: {{notes[0]}}
|
|
|
61
62
|
| `{{#each items}}...{{/each}}` | Iterate an array |
|
|
62
63
|
| `{{#if key}}...{{else}}...{{/if}}` | Conditional block |
|
|
63
64
|
| `{{> key}}` | Inline a frontmatter string as raw text |
|
|
65
|
+
| `\{{key}}` | Render `{{key}}` literally (escape) |
|
|
64
66
|
|
|
65
67
|
## Parsing and Rendering a Markedin File
|
|
66
68
|
|
|
67
69
|
```javascript
|
|
68
70
|
const fs = require("fs");
|
|
69
|
-
const {
|
|
71
|
+
const {
|
|
72
|
+
parse,
|
|
73
|
+
render,
|
|
74
|
+
renderHtmlFrag,
|
|
75
|
+
renderHtml,
|
|
76
|
+
} = require("markedin-parser");
|
|
70
77
|
|
|
71
78
|
const source = fs.readFileSync("task.mi", "utf8");
|
|
72
79
|
|
|
@@ -84,8 +91,8 @@ renderHtmlFrag(source);
|
|
|
84
91
|
renderHtml(source);
|
|
85
92
|
|
|
86
93
|
// Embed frontmatter in output
|
|
87
|
-
render(source, { embed: true });
|
|
88
|
-
renderHtml(source, { embed: true });
|
|
94
|
+
render(source, { embed: true }); // appends as HTML comment
|
|
95
|
+
renderHtml(source, { embed: true }); // adds <script> tag in <head>
|
|
89
96
|
```
|
|
90
97
|
|
|
91
98
|
## Markedin Parser API
|
|
@@ -116,7 +123,7 @@ Resolve a dotted/bracketed path against a data object.
|
|
|
116
123
|
|
|
117
124
|
```javascript
|
|
118
125
|
resolvePath(data, "owner.name"); // → 'Dana'
|
|
119
|
-
resolvePath(data, "notes[0]");
|
|
126
|
+
resolvePath(data, "notes[0]"); // → 'Token bucket algorithm chosen over leaky bucket'
|
|
120
127
|
```
|
|
121
128
|
|
|
122
129
|
## License
|
package/package.json
CHANGED
package/parse.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* {{> partial_key}} — inline another frontmatter string as markdown
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
const SPEC_VERSION = '0.4.0';
|
|
16
|
+
|
|
15
17
|
const yaml = require('js-yaml');
|
|
16
18
|
const { marked } = require('marked');
|
|
17
19
|
|
|
@@ -69,6 +71,32 @@ function formatValue(val) {
|
|
|
69
71
|
return String(val);
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
// ─── Standalone tag detection ────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
function isStandalone(str, tagStart, tagEnd) {
|
|
77
|
+
// Find start of line containing the tag
|
|
78
|
+
let lineStart = tagStart;
|
|
79
|
+
while (lineStart > 0 && str[lineStart - 1] !== '\n') lineStart--;
|
|
80
|
+
|
|
81
|
+
// Only whitespace allowed before tag on this line
|
|
82
|
+
for (let i = lineStart; i < tagStart; i++) {
|
|
83
|
+
if (str[i] !== ' ' && str[i] !== '\t') return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Only whitespace allowed after tag until end of line
|
|
87
|
+
let pos = tagEnd;
|
|
88
|
+
while (pos < str.length && str[pos] !== '\n' && str[pos] !== '\r') {
|
|
89
|
+
if (str[pos] !== ' ' && str[pos] !== '\t') return null;
|
|
90
|
+
pos++;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Consume \r\n or \n
|
|
94
|
+
if (pos < str.length && str[pos] === '\r') pos++;
|
|
95
|
+
if (pos < str.length && str[pos] === '\n') pos++;
|
|
96
|
+
|
|
97
|
+
return { lineStart, lineEnd: pos };
|
|
98
|
+
}
|
|
99
|
+
|
|
72
100
|
// ─── Block processing ────────────────────────────────────────────────────────
|
|
73
101
|
|
|
74
102
|
function findClose(str, nestedOpenRe, closeTag, from) {
|
|
@@ -104,13 +132,34 @@ function processBlocks(str, openRe, nestedOpenRe, closeTag, fn) {
|
|
|
104
132
|
let lastEnd = 0;
|
|
105
133
|
let m;
|
|
106
134
|
while ((m = openRe.exec(str)) !== null) {
|
|
135
|
+
const openStart = m.index;
|
|
107
136
|
const openEnd = m.index + m[0].length;
|
|
108
137
|
const closeIdx = findClose(str, nestedOpenRe, closeTag, openEnd);
|
|
109
138
|
if (closeIdx === -1) continue;
|
|
110
|
-
const
|
|
111
|
-
|
|
139
|
+
const closeEnd = closeIdx + closeTag.length;
|
|
140
|
+
|
|
141
|
+
let consumeFrom = openStart;
|
|
142
|
+
let consumeTo = closeEnd;
|
|
143
|
+
let innerStart = openEnd;
|
|
144
|
+
let innerEnd = closeIdx;
|
|
145
|
+
|
|
146
|
+
// Standalone open tag: consume the entire line
|
|
147
|
+
const openSA = isStandalone(str, openStart, openEnd);
|
|
148
|
+
if (openSA) {
|
|
149
|
+
consumeFrom = openSA.lineStart;
|
|
150
|
+
innerStart = openSA.lineEnd;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Standalone close tag: consume the entire line
|
|
154
|
+
const closeSA = isStandalone(str, closeIdx, closeEnd);
|
|
155
|
+
if (closeSA) {
|
|
156
|
+
consumeTo = closeSA.lineEnd;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const inner = str.slice(innerStart, innerEnd);
|
|
160
|
+
result += str.slice(lastEnd, consumeFrom);
|
|
112
161
|
result += fn(m[1], inner);
|
|
113
|
-
lastEnd =
|
|
162
|
+
lastEnd = consumeTo;
|
|
114
163
|
openRe.lastIndex = lastEnd;
|
|
115
164
|
}
|
|
116
165
|
return result + str.slice(lastEnd);
|
|
@@ -191,6 +240,9 @@ function renderTemplate(template, ctx) {
|
|
|
191
240
|
|
|
192
241
|
let out = template;
|
|
193
242
|
|
|
243
|
+
// 0. \{{ → protect as literal {{
|
|
244
|
+
out = out.replace(/\\\{\{/g, () => protect('{{'));
|
|
245
|
+
|
|
194
246
|
// 1. {{#each key}} ... {{/each}}
|
|
195
247
|
out = processBlocks(out,
|
|
196
248
|
/\{\{#each ([\w.[\]]+)\}\}/g,
|
|
@@ -215,8 +267,21 @@ function renderTemplate(template, ctx) {
|
|
|
215
267
|
(key, inner) => {
|
|
216
268
|
const val = resolvePath(ctx, key);
|
|
217
269
|
const elseIdx = findTopLevelElse(inner);
|
|
218
|
-
|
|
219
|
-
|
|
270
|
+
let truthy, falsy;
|
|
271
|
+
if (elseIdx === -1) {
|
|
272
|
+
truthy = inner;
|
|
273
|
+
falsy = '';
|
|
274
|
+
} else {
|
|
275
|
+
const elseEnd = elseIdx + '{{else}}'.length;
|
|
276
|
+
const elseSA = isStandalone(inner, elseIdx, elseEnd);
|
|
277
|
+
if (elseSA) {
|
|
278
|
+
truthy = inner.slice(0, elseSA.lineStart);
|
|
279
|
+
falsy = inner.slice(elseSA.lineEnd);
|
|
280
|
+
} else {
|
|
281
|
+
truthy = inner.slice(0, elseIdx);
|
|
282
|
+
falsy = inner.slice(elseEnd);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
220
285
|
return protect(renderTemplate(isTruthy(val) ? truthy : falsy, ctx));
|
|
221
286
|
}
|
|
222
287
|
);
|
|
@@ -237,4 +302,4 @@ function renderTemplate(template, ctx) {
|
|
|
237
302
|
return restore(out);
|
|
238
303
|
}
|
|
239
304
|
|
|
240
|
-
module.exports = { parse, render, renderHtmlFrag, renderHtml, renderTemplate, resolvePath };
|
|
305
|
+
module.exports = { SPEC_VERSION, parse, render, renderHtmlFrag, renderHtml, renderTemplate, resolvePath };
|