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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markedin-parser",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Parse and render .mi (markedin) files — YAML frontmatter + templated Markdown",
5
5
  "main": "parse.js",
6
6
  "files": [
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 inner = str.slice(openEnd, closeIdx);
111
- result += str.slice(lastEnd, m.index);
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 = closeIdx + closeTag.length;
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
- const truthy = elseIdx === -1 ? inner : inner.slice(0, elseIdx);
219
- const falsy = elseIdx === -1 ? '' : inner.slice(elseIdx + '{{else}}'.length);
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 };