markedin-parser 0.1.3 → 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 +1 -0
- package/package.json +1 -1
- package/parse.js +71 -6
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ First note: {{notes[0]}}
|
|
|
62
62
|
| `{{#each items}}...{{/each}}` | Iterate an array |
|
|
63
63
|
| `{{#if key}}...{{else}}...{{/if}}` | Conditional block |
|
|
64
64
|
| `{{> key}}` | Inline a frontmatter string as raw text |
|
|
65
|
+
| `\{{key}}` | Render `{{key}}` literally (escape) |
|
|
65
66
|
|
|
66
67
|
## Parsing and Rendering a Markedin File
|
|
67
68
|
|
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 };
|