jmd-format 0.2.1 → 0.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jmd-format",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "JMD (JSON Markdown) — structured data format for LLM-driven infrastructure. JavaScript reference implementation.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/parser.js CHANGED
@@ -127,10 +127,12 @@ export function createParser() {
127
127
  return drain()
128
128
  }
129
129
 
130
- // Thematic break: `---` (or more) at column 0, only meaningful inside
131
- // an array scope, where it terminates the current item.
130
+ // Thematic break `---` is pure decoration (§8.6): the level-pop (an
131
+ // anonymous heading) is the structural mechanism, so a `---` line is
132
+ // skipped. It consumes any pending blank, so a following `- ` item
133
+ // still joins the current array instead of triggering a root reset.
132
134
  if (/^-{3,}$/.test(line)) {
133
- onThematicBreak()
135
+ blankPending = false
134
136
  return drain()
135
137
  }
136
138
 
@@ -153,34 +155,6 @@ export function createParser() {
153
155
  if (stack[0] && stack[0].kind === 'array') closeItem(stack[0])
154
156
  }
155
157
 
156
- function onThematicBreak() {
157
- blankPending = false
158
- // A thematic break closes any sub-scope opened by the most-recent
159
- // item, then signals the next item of the enclosing array (spec §8.6).
160
- // We search outward for the innermost array whose last item opened a
161
- // sub-structure and close down to it. If none qualifies, the break is
162
- // a no-op — and that is correct, not lossy: a flat item opens no
163
- // sub-scope, so the enclosing array is still current and the next
164
- // `- ` item continues it. This is why a `---` after a flat item in a
165
- // mixed array (canonical per the v0.3.4 §8.6 clarification) parses
166
- // without dropping the following item.
167
- let targetIdx = -1
168
- for (let i = stack.length - 1; i >= 0; i--) {
169
- const s = stack[i]
170
- if (s.kind !== 'array') continue
171
- const last = s.container[s.container.length - 1]
172
- if (last && typeof last === 'object' && !Array.isArray(last)
173
- && Object.values(last).some(
174
- v => v !== null && typeof v === 'object')) {
175
- targetIdx = i
176
- break
177
- }
178
- }
179
- if (targetIdx === -1) return
180
- while (stack.length - 1 > targetIdx) popScope()
181
- closeItem(stack[targetIdx])
182
- }
183
-
184
158
  // --- Blockquote ----------------------------------------------------------
185
159
 
186
160
  function startBlockquote(container, key) {
@@ -374,13 +348,25 @@ export function createParser() {
374
348
  return
375
349
  }
376
350
 
377
- popToDepth(depth)
378
-
351
+ // Anonymous heading at depth D. If an array scope is open at exactly
352
+ // this depth, the heading is a level-pop (§8.6): return to that array
353
+ // — drop any deeper sub-scopes and close the record that opened them,
354
+ // so the next `- ` item joins THIS array. One marker pops arbitrary
355
+ // nesting in a single step. Otherwise it is a §3.2a anonymous object.
379
356
  if (text === '' || text === undefined) {
357
+ const arr = arrayScopeAtDepth(depth)
358
+ if (arr) {
359
+ popToDepth(depth + 1)
360
+ closeItem(arr)
361
+ return
362
+ }
363
+ popToDepth(depth)
380
364
  openObjectScope(depth, '')
381
365
  return
382
366
  }
383
367
 
368
+ popToDepth(depth)
369
+
384
370
  // Anonymous sub-array: `### []` — handled below with the other array forms.
385
371
  if (text === '[]') {
386
372
  openSubArray(depth)
@@ -574,6 +560,20 @@ export function createParser() {
574
560
  )
575
561
  }
576
562
 
563
+ // Find an open array scope at exactly this depth, scanning from the top.
564
+ // Used to disambiguate an anonymous heading: an array at the heading's
565
+ // own depth means the heading is a level-pop (§8.6), not a §3.2a
566
+ // anonymous object. Returns null if the first scope at depth ≤ D is not
567
+ // such an array.
568
+ function arrayScopeAtDepth(depth) {
569
+ for (let i = stack.length - 1; i >= 0; i--) {
570
+ const s = stack[i]
571
+ if (s.depth > depth) continue
572
+ return (s.depth === depth && s.kind === 'array') ? s : null
573
+ }
574
+ return null
575
+ }
576
+
577
577
  function parentContainerAndSeen(scope) {
578
578
  if (scope.kind === 'object') {
579
579
  return { container: scope.container, seen: scope.seen }
package/src/serializer.js CHANGED
@@ -152,10 +152,8 @@ function writeArrayItems(lst, lines, depth) {
152
152
  }
153
153
 
154
154
  if (allDicts) {
155
- const hasNested = lst.some(item =>
156
- Object.values(item).some(isNested))
157
155
  for (let i = 0; i < lst.length; i++) {
158
- writeDictItem(lst[i], lines, depth, i > 0 && hasNested)
156
+ writeDictItem(lst[i], lines, depth, i < lst.length - 1)
159
157
  }
160
158
  return
161
159
  }
@@ -196,7 +194,7 @@ function writeArrayItems(lst, lines, depth) {
196
194
  }
197
195
  }
198
196
 
199
- function writeDictItem(item, lines, depth, separatorNeeded, qualifierPrefix = '') {
197
+ function writeDictItem(item, lines, depth, moreFollow, qualifierPrefix = '') {
200
198
  const scalarFields = []
201
199
  const nestedFields = []
202
200
  for (const [k, v] of Object.entries(item)) {
@@ -204,14 +202,6 @@ function writeDictItem(item, lines, depth, separatorNeeded, qualifierPrefix = ''
204
202
  else scalarFields.push([k, v])
205
203
  }
206
204
 
207
- if (separatorNeeded) {
208
- // Match the C-accelerated Python serializer (the default in jmd-format):
209
- // blank line before the `---`, but the next `- ` follows immediately on
210
- // the next line — no blank after the thematic break.
211
- lines.push('')
212
- lines.push('---')
213
- }
214
-
215
205
  if (scalarFields.length === 0) {
216
206
  lines.push(qualifierPrefix + '-')
217
207
  } else {
@@ -230,6 +220,13 @@ function writeDictItem(item, lines, depth, separatorNeeded, qualifierPrefix = ''
230
220
 
231
221
  if (nestedFields.length > 0) {
232
222
  writeObjectFields(Object.fromEntries(nestedFields), lines, depth)
223
+ // Level-pop (§8.6): this record opened a sub-structure (heading at
224
+ // depth+1). If more records follow, emit an anonymous heading at the
225
+ // array's own depth to pop back, so the next bare `-` item is read into
226
+ // THIS array. The last record needs no pop — end-of-scope closes it.
227
+ if (moreFollow) {
228
+ lines.push('#'.repeat(depth))
229
+ }
233
230
  }
234
231
  }
235
232