jmd-format 0.1.0 → 0.1.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/README.md +9 -7
- package/package.json +1 -1
- package/src/parser.js +34 -5
- package/src/serializer.js +18 -2
package/README.md
CHANGED
|
@@ -94,6 +94,11 @@ string chunks into an async iterable of complete lines.
|
|
|
94
94
|
- Thematic breaks (`---`) as array-item separators (§8.6). Consumed by
|
|
95
95
|
the innermost enclosing array whose most-recent item is a dict with
|
|
96
96
|
nested structures.
|
|
97
|
+
- Depth-qualified array items (`##N -`, §8.6a) and depth+1 items
|
|
98
|
+
(`##N - key: val`, §8.6b). Parser-tolerance forms: the canonical
|
|
99
|
+
serializer emits bare `- ` items with thematic-break separators, but
|
|
100
|
+
the parser accepts the depth-annotated forms that LLMs tend to
|
|
101
|
+
produce when items sit one heading level below the array heading.
|
|
97
102
|
- **Streaming parser** via async generator: events match the sequence
|
|
98
103
|
defined in §18.2 (document_start, field, field_start, field_content,
|
|
99
104
|
object_start/end, array_start/end, item_start/value/end, scope_reset,
|
|
@@ -101,14 +106,11 @@ string chunks into an async iterable of complete lines.
|
|
|
101
106
|
- **Streaming serializer** via sync generator (`serializeLines`).
|
|
102
107
|
- Line adapter (`toLines`) to convert chunked input to lines.
|
|
103
108
|
|
|
104
|
-
## Not yet
|
|
109
|
+
## Not yet structured
|
|
105
110
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
- Depth+1 items (items one heading level deeper than the array heading).
|
|
110
|
-
- Schema-specific type expressions, QBE filter conditions — parsed as raw
|
|
111
|
-
strings today; structured interpretation will follow.
|
|
111
|
+
Schema-specific type expressions (§14) and QBE filter conditions (§13)
|
|
112
|
+
are parsed as raw strings on both this implementation and the Python
|
|
113
|
+
reference. Structured interpretation will follow in a later release.
|
|
112
114
|
|
|
113
115
|
## Design
|
|
114
116
|
|
package/package.json
CHANGED
package/src/parser.js
CHANGED
|
@@ -206,6 +206,15 @@ export function createParser() {
|
|
|
206
206
|
throw parseError('Mode markers (!, ?, -) are only valid on the root heading')
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
// Depth-qualified array item (§8.6a) or depth+1 item (§8.6b):
|
|
210
|
+
// `##N -` or `##N - key: val` starts a new item in an enclosing array.
|
|
211
|
+
// Resolution prefers the innermost array at depth N; failing that, an
|
|
212
|
+
// array at depth N-1 (the LLM-natural "items under the heading" form).
|
|
213
|
+
if (text === '-' || text.startsWith('- ')) {
|
|
214
|
+
onDepthQualifiedItem(depth, text)
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
|
|
209
218
|
popToDepth(depth)
|
|
210
219
|
|
|
211
220
|
if (text === '' || text === undefined) {
|
|
@@ -213,11 +222,6 @@ export function createParser() {
|
|
|
213
222
|
return
|
|
214
223
|
}
|
|
215
224
|
|
|
216
|
-
// Depth-qualified array item: `## -` or `## - key: val` — deferred.
|
|
217
|
-
if (text === '-' || text.startsWith('- ')) {
|
|
218
|
-
throw parseError('Depth-qualified array items (## -) are not yet supported')
|
|
219
|
-
}
|
|
220
|
-
|
|
221
225
|
// Anonymous sub-array: `### []` — handled below with the other array forms.
|
|
222
226
|
if (text === '[]') {
|
|
223
227
|
openSubArray(depth)
|
|
@@ -356,6 +360,31 @@ export function createParser() {
|
|
|
356
360
|
|
|
357
361
|
// --- Array items ---------------------------------------------------------
|
|
358
362
|
|
|
363
|
+
function onDepthQualifiedItem(headingDepth, text) {
|
|
364
|
+
// Find target: innermost array at depth == headingDepth wins (§8.6a).
|
|
365
|
+
// Else fall back to array at depth == headingDepth - 1 (§8.6b — the
|
|
366
|
+
// LLM-natural pattern of writing items one heading-level under the
|
|
367
|
+
// array heading).
|
|
368
|
+
let sameDepthIdx = -1
|
|
369
|
+
let parentDepthIdx = -1
|
|
370
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
371
|
+
const s = stack[i]
|
|
372
|
+
if (s.kind !== 'array') continue
|
|
373
|
+
if (sameDepthIdx === -1 && s.depth === headingDepth) sameDepthIdx = i
|
|
374
|
+
if (parentDepthIdx === -1 && s.depth === headingDepth - 1) {
|
|
375
|
+
parentDepthIdx = i
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const targetIdx = sameDepthIdx !== -1 ? sameDepthIdx : parentDepthIdx
|
|
379
|
+
if (targetIdx === -1) {
|
|
380
|
+
throw parseError('Depth-qualified item has no matching array scope')
|
|
381
|
+
}
|
|
382
|
+
// Close any scopes nested inside the target array.
|
|
383
|
+
while (stack.length - 1 > targetIdx) popScope()
|
|
384
|
+
// Reuse the regular item handler with the target array on top.
|
|
385
|
+
onItem(text)
|
|
386
|
+
}
|
|
387
|
+
|
|
359
388
|
function onItem(line) {
|
|
360
389
|
const top = stack[stack.length - 1]
|
|
361
390
|
if (top.kind !== 'array') throw parseError('Array item outside array scope')
|
package/src/serializer.js
CHANGED
|
@@ -29,6 +29,19 @@ export function* serializeLines(value, label = 'Document', frontmatter = null) {
|
|
|
29
29
|
for (const ln of lines) yield ln + '\n'
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// Mode markers attach directly to `#` in the root heading: `#- Order`,
|
|
33
|
+
// `#? Order`, `#! Order`. Callers pass the mark as a `- `, `? ` or `! `
|
|
34
|
+
// prefix on `label`; the serializer attaches it to `#` without a space
|
|
35
|
+
// between them. Plain data documents (no prefix) emit `# Label`.
|
|
36
|
+
function splitLabel(label) {
|
|
37
|
+
if (label.length >= 2
|
|
38
|
+
&& (label[0] === '-' || label[0] === '?' || label[0] === '!')
|
|
39
|
+
&& label[1] === ' ') {
|
|
40
|
+
return { mark: label[0], rest: label.slice(2) }
|
|
41
|
+
}
|
|
42
|
+
return { mark: '', rest: label }
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
function emitDocument(value, label, frontmatter, lines) {
|
|
33
46
|
if (frontmatter && Object.keys(frontmatter).length > 0) {
|
|
34
47
|
for (const [k, v] of Object.entries(frontmatter)) {
|
|
@@ -38,12 +51,15 @@ function emitDocument(value, label, frontmatter, lines) {
|
|
|
38
51
|
lines.push('')
|
|
39
52
|
}
|
|
40
53
|
|
|
54
|
+
const { mark, rest } = splitLabel(label)
|
|
55
|
+
const prefix = '#' + mark + ' '
|
|
56
|
+
|
|
41
57
|
if (Array.isArray(value)) {
|
|
42
|
-
const head =
|
|
58
|
+
const head = rest === '[]' ? prefix + '[]' : prefix + rest + '[]'
|
|
43
59
|
lines.push(head)
|
|
44
60
|
writeArrayItems(value, lines, 1)
|
|
45
61
|
} else if (value !== null && typeof value === 'object') {
|
|
46
|
-
lines.push(
|
|
62
|
+
lines.push(prefix + rest)
|
|
47
63
|
writeObjectFields(value, lines, 1)
|
|
48
64
|
} else {
|
|
49
65
|
throw new TypeError('Root value must be an object or array')
|