@blackwell-systems/gcf 0.6.1 → 1.0.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/LICENSE +1 -1
- package/README.md +11 -1
- package/dist/decode.d.ts.map +1 -1
- package/dist/decode.js +15 -6
- package/dist/decode.js.map +1 -1
- package/dist/decode_generic.d.ts +1 -5
- package/dist/decode_generic.d.ts.map +1 -1
- package/dist/decode_generic.js +392 -142
- package/dist/decode_generic.js.map +1 -1
- package/dist/delta.js +1 -1
- package/dist/delta.js.map +1 -1
- package/dist/encode.d.ts.map +1 -1
- package/dist/encode.js +22 -7
- package/dist/encode.js.map +1 -1
- package/dist/generic.d.ts +0 -6
- package/dist/generic.d.ts.map +1 -1
- package/dist/generic.js +146 -114
- package/dist/generic.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/scalar.d.ts +27 -0
- package/dist/scalar.d.ts.map +1 -0
- package/dist/scalar.js +315 -0
- package/dist/scalar.js.map +1 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +9 -1
- package/dist/session.js.map +1 -1
- package/dist/stream.d.ts +1 -1
- package/dist/stream.js +7 -7
- package/dist/stream.js.map +1 -1
- package/dist/stream_generic.d.ts +1 -1
- package/dist/stream_generic.d.ts.map +1 -1
- package/dist/stream_generic.js +5 -27
- package/dist/stream_generic.js.map +1 -1
- package/package.json +2 -2
- package/src/decode.ts +18 -6
- package/src/decode_generic.ts +352 -137
- package/src/delta.ts +1 -1
- package/src/encode.ts +19 -7
- package/src/generic.ts +127 -128
- package/src/index.ts +1 -0
- package/src/scalar.ts +249 -0
- package/src/session.ts +6 -1
- package/src/stream.ts +7 -7
- package/src/stream_generic.ts +5 -27
package/dist/decode_generic.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { decode } from './decode.js';
|
|
2
|
+
import { parseScalar, parseQuotedString, splitRespectingQuotes, splitFieldDecl, MISSING, ATTACHMENT, } from './scalar.js';
|
|
2
3
|
/**
|
|
3
|
-
* Decode
|
|
4
|
-
* Returns objects, arrays, and primitives matching the original structure.
|
|
5
|
-
*
|
|
6
|
-
* If the input starts with "GCF " (graph profile), falls back to decode()
|
|
7
|
-
* and returns the Payload as a plain object.
|
|
4
|
+
* Decode GCF v2.0 generic or graph profile text into a JS value.
|
|
8
5
|
*/
|
|
9
6
|
export function decodeGeneric(input) {
|
|
10
7
|
input = input.trimEnd();
|
|
11
8
|
if (!input)
|
|
12
|
-
|
|
9
|
+
throw new Error('missing_header: empty input');
|
|
13
10
|
const lines = input.split('\n');
|
|
14
|
-
|
|
15
|
-
if (
|
|
11
|
+
const header = lines[0].replace(/\r$/, '');
|
|
12
|
+
if (!header.startsWith('GCF '))
|
|
13
|
+
throw new Error('missing_header: first line does not begin with GCF');
|
|
14
|
+
const profile = parseHeaderProfile(header);
|
|
15
|
+
if (profile === 'graph') {
|
|
16
16
|
const p = decode(input);
|
|
17
17
|
return {
|
|
18
18
|
tool: p.tool,
|
|
@@ -30,99 +30,139 @@ export function decodeGeneric(input) {
|
|
|
30
30
|
source: e.source,
|
|
31
31
|
target: e.target,
|
|
32
32
|
edgeType: e.edgeType,
|
|
33
|
-
|
|
33
|
+
status: e.status ?? '',
|
|
34
34
|
})),
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
+
if (profile !== 'generic')
|
|
38
|
+
throw new Error(`unknown_profile: ${profile}`);
|
|
39
|
+
// Filter body.
|
|
40
|
+
const contentLines = [];
|
|
41
|
+
let summaryLine = '';
|
|
42
|
+
let deferredSectionCount = 0;
|
|
43
|
+
for (let i = 1; i < lines.length; i++) {
|
|
44
|
+
const l = lines[i].replace(/\r$/, '');
|
|
45
|
+
if (l === '')
|
|
46
|
+
continue;
|
|
47
|
+
// Tab check.
|
|
48
|
+
for (let j = 0; j < l.length; j++) {
|
|
49
|
+
if (l[j] === '\t')
|
|
50
|
+
throw new Error('tab_indentation: tabs in leading whitespace');
|
|
51
|
+
if (l[j] !== ' ')
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
const trimmed = l.trimStart();
|
|
55
|
+
if (trimmed.startsWith('# '))
|
|
56
|
+
continue;
|
|
57
|
+
if (trimmed.startsWith('##! ')) {
|
|
58
|
+
summaryLine = trimmed;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (trimmed.startsWith('## ') && trimmed.includes('[?]'))
|
|
62
|
+
deferredSectionCount++;
|
|
63
|
+
contentLines.push(l);
|
|
64
|
+
}
|
|
65
|
+
// Validate ##! summary counts.
|
|
66
|
+
if (summaryLine && deferredSectionCount > 0) {
|
|
67
|
+
validateSummaryCounts(summaryLine, deferredSectionCount, contentLines);
|
|
68
|
+
}
|
|
69
|
+
if (contentLines.length === 0)
|
|
70
|
+
return {};
|
|
71
|
+
const first = contentLines[0].trimStart();
|
|
72
|
+
// Root scalar.
|
|
73
|
+
if (first.startsWith('=')) {
|
|
74
|
+
if (contentLines.length > 1)
|
|
75
|
+
throw new Error('trailing_characters: extra lines after root scalar');
|
|
76
|
+
return parseScalar(first.slice(1), false);
|
|
77
|
+
}
|
|
78
|
+
// Root array.
|
|
79
|
+
if (first.startsWith('## [')) {
|
|
80
|
+
const [arr] = parseArrayFromHeader(contentLines, 0, 0, first.slice(3));
|
|
81
|
+
return arr;
|
|
82
|
+
}
|
|
83
|
+
// Root object.
|
|
37
84
|
const result = {};
|
|
38
|
-
|
|
85
|
+
parseObjectBody(contentLines, 0, 0, result);
|
|
39
86
|
return result;
|
|
40
87
|
}
|
|
41
|
-
function
|
|
42
|
-
const
|
|
88
|
+
function parseHeaderProfile(header) {
|
|
89
|
+
const parts = header.split(/\s+/);
|
|
90
|
+
if (parts.length < 2)
|
|
91
|
+
throw new Error('missing_profile');
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
let profile = '';
|
|
94
|
+
for (let i = 1; i < parts.length; i++) {
|
|
95
|
+
const eq = parts[i].indexOf('=');
|
|
96
|
+
if (eq < 0)
|
|
97
|
+
throw new Error(`malformed_header_field: ${parts[i]}`);
|
|
98
|
+
const key = parts[i].slice(0, eq);
|
|
99
|
+
if (seen.has(key))
|
|
100
|
+
throw new Error(`duplicate_header_field: ${key}`);
|
|
101
|
+
seen.add(key);
|
|
102
|
+
if (key === 'profile')
|
|
103
|
+
profile = parts[i].slice(eq + 1);
|
|
104
|
+
}
|
|
105
|
+
if (!profile)
|
|
106
|
+
throw new Error('missing_profile');
|
|
107
|
+
return profile;
|
|
108
|
+
}
|
|
109
|
+
function parseObjectBody(lines, start, depth, out) {
|
|
110
|
+
const ind = ' '.repeat(depth);
|
|
43
111
|
let i = start;
|
|
44
112
|
while (i < lines.length) {
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
i++;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (depth > 0 && !raw.startsWith(indent))
|
|
113
|
+
const line = lines[i];
|
|
114
|
+
if (depth > 0 && !line.startsWith(ind))
|
|
51
115
|
break;
|
|
52
|
-
const content = depth > 0 ?
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
i++;
|
|
56
|
-
continue;
|
|
116
|
+
const content = depth > 0 ? line.slice(ind.length) : line;
|
|
117
|
+
if (content.length > 0 && content[0] === ' ') {
|
|
118
|
+
throw new Error('invalid_indent: indentation increases by more than one level');
|
|
57
119
|
}
|
|
58
|
-
//
|
|
120
|
+
// Array section.
|
|
59
121
|
if (content.startsWith('## ')) {
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
const name =
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Tabular with fields.
|
|
70
|
-
const fieldEnd = afterBracket.indexOf('}');
|
|
71
|
-
if (fieldEnd >= 0) {
|
|
72
|
-
const fields = afterBracket.slice(1, fieldEnd).split(',');
|
|
73
|
-
i++;
|
|
74
|
-
const [rows, consumed] = parseTabularRows(lines, i, depth, fields);
|
|
75
|
-
out[name] = rows;
|
|
76
|
-
i += consumed;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
const countStr = rest.slice(0, closeBracket);
|
|
82
|
-
if (countStr === '0') {
|
|
83
|
-
out[name] = [];
|
|
84
|
-
i++;
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
// Non-uniform array.
|
|
88
|
-
i++;
|
|
89
|
-
const [items, consumed] = parseNonUniformArray(lines, i, depth);
|
|
90
|
-
out[name] = items;
|
|
91
|
-
i += consumed;
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
122
|
+
const hdr = content.slice(3);
|
|
123
|
+
const bi = hdr.indexOf(' [');
|
|
124
|
+
if (bi >= 0) {
|
|
125
|
+
const name = parseKeyFromHeader(hdr.slice(0, bi));
|
|
126
|
+
checkDup(out, name);
|
|
127
|
+
const [arr, consumed] = parseArrayFromHeader(lines, i, depth, hdr.slice(bi));
|
|
128
|
+
out[name] = arr;
|
|
129
|
+
i += consumed;
|
|
130
|
+
continue;
|
|
95
131
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const bi = name.indexOf(' [');
|
|
99
|
-
if (bi >= 0)
|
|
100
|
-
name = name.slice(0, bi);
|
|
132
|
+
const name = parseKeyFromHeader(hdr);
|
|
133
|
+
checkDup(out, name);
|
|
101
134
|
i++;
|
|
102
135
|
const nested = {};
|
|
103
|
-
const consumed =
|
|
136
|
+
const consumed = parseObjectBody(lines, i, depth + 1, nested);
|
|
104
137
|
out[name] = nested;
|
|
105
138
|
i += consumed;
|
|
106
139
|
continue;
|
|
107
140
|
}
|
|
108
|
-
// Inline
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
141
|
+
// Inline array.
|
|
142
|
+
if (!content.startsWith('@') && !content.startsWith('##')) {
|
|
143
|
+
const bracketIdx = content.indexOf('[');
|
|
144
|
+
if (bracketIdx > 0) {
|
|
145
|
+
const rest = content.slice(bracketIdx);
|
|
146
|
+
const closeIdx = rest.indexOf(']');
|
|
147
|
+
if (closeIdx >= 0) {
|
|
148
|
+
const after = rest.slice(closeIdx + 1);
|
|
149
|
+
if (after.startsWith(': ') || after === ':') {
|
|
150
|
+
const name = parseKeyFromHeader(content.slice(0, bracketIdx));
|
|
151
|
+
checkDup(out, name);
|
|
152
|
+
const [arr] = parseArrayFromHeader(lines, i, depth, rest);
|
|
153
|
+
out[name] = arr;
|
|
154
|
+
i++;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
118
158
|
}
|
|
119
159
|
}
|
|
120
160
|
// Key=value.
|
|
121
|
-
const eqIdx = content
|
|
161
|
+
const eqIdx = findKeyValueSplit(content);
|
|
122
162
|
if (eqIdx > 0) {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
out[
|
|
163
|
+
const name = parseKeyFromHeader(content.slice(0, eqIdx));
|
|
164
|
+
checkDup(out, name);
|
|
165
|
+
out[name] = parseScalar(content.slice(eqIdx + 1), false);
|
|
126
166
|
i++;
|
|
127
167
|
continue;
|
|
128
168
|
}
|
|
@@ -130,107 +170,317 @@ function parseObject(lines, start, depth, out) {
|
|
|
130
170
|
}
|
|
131
171
|
return i - start;
|
|
132
172
|
}
|
|
133
|
-
function
|
|
134
|
-
|
|
173
|
+
function findKeyValueSplit(s) {
|
|
174
|
+
if (!s.length)
|
|
175
|
+
return -1;
|
|
176
|
+
if (s[0] === '"') {
|
|
177
|
+
for (let i = 1; i < s.length; i++) {
|
|
178
|
+
if (s[i] === '\\') {
|
|
179
|
+
i++;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (s[i] === '"')
|
|
183
|
+
return (i + 1 < s.length && s[i + 1] === '=') ? i + 1 : -1;
|
|
184
|
+
}
|
|
185
|
+
return -1;
|
|
186
|
+
}
|
|
187
|
+
return s.indexOf('=');
|
|
188
|
+
}
|
|
189
|
+
function parseKeyFromHeader(s) {
|
|
190
|
+
s = s.trim();
|
|
191
|
+
if (s.length >= 2 && s[0] === '"')
|
|
192
|
+
return parseQuotedString(s);
|
|
193
|
+
return s;
|
|
194
|
+
}
|
|
195
|
+
function checkDup(obj, key) {
|
|
196
|
+
if (key in obj)
|
|
197
|
+
throw new Error(`duplicate_key: ${key}`);
|
|
198
|
+
}
|
|
199
|
+
function parseArrayFromHeader(lines, headerLine, depth, bracketPart) {
|
|
200
|
+
const bp = bracketPart.trimStart();
|
|
201
|
+
if (!bp.startsWith('['))
|
|
202
|
+
throw new Error('invalid_count');
|
|
203
|
+
const closeIdx = bp.indexOf(']');
|
|
204
|
+
if (closeIdx < 0)
|
|
205
|
+
throw new Error('invalid_count');
|
|
206
|
+
const countStr = bp.slice(1, closeIdx);
|
|
207
|
+
const afterBracket = bp.slice(closeIdx + 1);
|
|
208
|
+
let count = -1;
|
|
209
|
+
if (countStr !== '?')
|
|
210
|
+
count = parseCount(countStr);
|
|
211
|
+
if (count === 0 && !afterBracket.startsWith('{') && !afterBracket.startsWith(':')) {
|
|
212
|
+
return [[], 1];
|
|
213
|
+
}
|
|
214
|
+
// Inline.
|
|
215
|
+
if (afterBracket.startsWith(': ') || afterBracket === ':') {
|
|
216
|
+
const valsStr = afterBracket.startsWith(': ') ? afterBracket.slice(2) : '';
|
|
217
|
+
if (!valsStr) {
|
|
218
|
+
if (count >= 0 && count !== 0)
|
|
219
|
+
throw new Error(`count_mismatch: declared ${count}, got 0`);
|
|
220
|
+
return [[], 1];
|
|
221
|
+
}
|
|
222
|
+
const vals = splitRespectingQuotes(valsStr, ',');
|
|
223
|
+
if (count >= 0 && vals.length !== count)
|
|
224
|
+
throw new Error(`count_mismatch: declared ${count}, got ${vals.length}`);
|
|
225
|
+
return [vals.map(v => parseScalar(v.trim(), false)), 1];
|
|
226
|
+
}
|
|
227
|
+
// Tabular.
|
|
228
|
+
if (afterBracket.startsWith('{')) {
|
|
229
|
+
const braceEnd = findClosingBrace(afterBracket);
|
|
230
|
+
if (braceEnd < 0)
|
|
231
|
+
throw new Error('invalid field declaration');
|
|
232
|
+
const fields = splitFieldDecl(afterBracket.slice(0, braceEnd + 1));
|
|
233
|
+
const [rows, consumed] = parseTabularBody(lines, headerLine + 1, depth, fields, count);
|
|
234
|
+
if (count >= 0 && rows.length !== count)
|
|
235
|
+
throw new Error(`count_mismatch: declared ${count}, got ${rows.length}`);
|
|
236
|
+
return [rows, consumed + 1];
|
|
237
|
+
}
|
|
238
|
+
// Expanded.
|
|
239
|
+
const [items, consumed] = parseExpandedBody(lines, headerLine + 1, depth);
|
|
240
|
+
if (count >= 0 && items.length !== count)
|
|
241
|
+
throw new Error(`count_mismatch: declared ${count}, got ${items.length}`);
|
|
242
|
+
return [items, consumed + 1];
|
|
243
|
+
}
|
|
244
|
+
function findClosingBrace(s) {
|
|
245
|
+
let inQuote = false, escaped = false;
|
|
246
|
+
for (let i = 0; i < s.length; i++) {
|
|
247
|
+
if (escaped) {
|
|
248
|
+
escaped = false;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (s[i] === '\\' && inQuote) {
|
|
252
|
+
escaped = true;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (s[i] === '"') {
|
|
256
|
+
inQuote = !inQuote;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (s[i] === '}' && !inQuote)
|
|
260
|
+
return i;
|
|
261
|
+
}
|
|
262
|
+
return -1;
|
|
263
|
+
}
|
|
264
|
+
function parseTabularBody(lines, start, depth, fields, expectedCount) {
|
|
265
|
+
const ind = ' '.repeat(depth);
|
|
135
266
|
const rows = [];
|
|
136
267
|
let i = start;
|
|
137
268
|
while (i < lines.length) {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
i++;
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
const content = depth > 0 ? (raw.startsWith(indent) ? raw.slice(indent.length) : null) : raw;
|
|
269
|
+
const line = lines[i];
|
|
270
|
+
const content = depth > 0 ? (line.startsWith(ind) ? line.slice(ind.length) : null) : line;
|
|
144
271
|
if (content === null)
|
|
145
272
|
break;
|
|
146
|
-
if (content.startsWith('## '))
|
|
273
|
+
if (content.startsWith('## ') || content.startsWith('##!'))
|
|
274
|
+
break;
|
|
275
|
+
if (content.length > 0 && content[0] === ' ') {
|
|
276
|
+
const trimmed = content.trimStart();
|
|
277
|
+
if (trimmed.startsWith('.'))
|
|
278
|
+
throw new Error(`orphan_attachment: ${trimmed}`);
|
|
147
279
|
break;
|
|
148
|
-
if (content.startsWith('# ')) {
|
|
149
|
-
i++;
|
|
150
|
-
continue;
|
|
151
280
|
}
|
|
152
281
|
let rowData = content;
|
|
153
|
-
let
|
|
282
|
+
let rowHasID = false;
|
|
154
283
|
if (rowData.startsWith('@')) {
|
|
155
284
|
const sp = rowData.indexOf(' ');
|
|
156
285
|
if (sp > 0) {
|
|
157
286
|
rowData = rowData.slice(sp + 1);
|
|
158
|
-
|
|
287
|
+
rowHasID = true;
|
|
159
288
|
}
|
|
160
289
|
}
|
|
161
|
-
const vals = rowData
|
|
290
|
+
const vals = splitRespectingQuotes(rowData, '|');
|
|
291
|
+
if (vals.length !== fields.length)
|
|
292
|
+
throw new Error(`row_width_mismatch: expected ${fields.length}, got ${vals.length}`);
|
|
162
293
|
const row = {};
|
|
294
|
+
const attachmentFields = [];
|
|
163
295
|
for (let j = 0; j < fields.length; j++) {
|
|
164
|
-
|
|
296
|
+
const parsed = parseScalar(vals[j], true);
|
|
297
|
+
if (parsed === MISSING)
|
|
298
|
+
continue;
|
|
299
|
+
if (parsed === ATTACHMENT) {
|
|
300
|
+
attachmentFields.push(fields[j]);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
row[fields[j]] = parsed;
|
|
165
304
|
}
|
|
166
305
|
i++;
|
|
167
|
-
if (
|
|
168
|
-
const
|
|
306
|
+
if (rowHasID && attachmentFields.length > 0) {
|
|
307
|
+
const attIndent = ind + ' ';
|
|
308
|
+
const resolved = new Set();
|
|
169
309
|
while (i < lines.length) {
|
|
170
|
-
const
|
|
171
|
-
if (!
|
|
310
|
+
const al = lines[i];
|
|
311
|
+
if (!al.startsWith(attIndent))
|
|
172
312
|
break;
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
175
|
-
const fieldName = nc.slice(1);
|
|
176
|
-
i++;
|
|
177
|
-
const nested = {};
|
|
178
|
-
const consumed = parseObject(lines, i, depth + 2, nested);
|
|
179
|
-
row[fieldName] = nested;
|
|
180
|
-
i += consumed;
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
313
|
+
const ac = al.slice(attIndent.length);
|
|
314
|
+
if (!ac.startsWith('.'))
|
|
183
315
|
break;
|
|
184
|
-
|
|
316
|
+
const [name, val, consumed] = parseAttachment(lines, i, ac.slice(1), depth + 2);
|
|
317
|
+
if (resolved.has(name))
|
|
318
|
+
throw new Error(`duplicate_attachment: ${name}`);
|
|
319
|
+
resolved.add(name);
|
|
320
|
+
row[name] = val;
|
|
321
|
+
i += consumed;
|
|
322
|
+
}
|
|
323
|
+
for (const f of attachmentFields) {
|
|
324
|
+
if (!resolved.has(f))
|
|
325
|
+
throw new Error(`missing_attachment: ${f}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (!rowHasID || attachmentFields.length === 0) {
|
|
329
|
+
const attIndent = ind + ' ';
|
|
330
|
+
if (i < lines.length && lines[i].startsWith(attIndent)) {
|
|
331
|
+
const peek = lines[i].slice(attIndent.length);
|
|
332
|
+
if (peek.startsWith('.'))
|
|
333
|
+
throw new Error(`orphan_attachment: ${peek}`);
|
|
185
334
|
}
|
|
186
335
|
}
|
|
187
336
|
rows.push(row);
|
|
337
|
+
if (expectedCount >= 0 && rows.length >= expectedCount)
|
|
338
|
+
break;
|
|
188
339
|
}
|
|
189
340
|
return [rows, i - start];
|
|
190
341
|
}
|
|
191
|
-
function
|
|
192
|
-
|
|
342
|
+
function parseAttachment(lines, lineIdx, rest, depth) {
|
|
343
|
+
let name;
|
|
344
|
+
let afterName;
|
|
345
|
+
if (rest[0] === '"') {
|
|
346
|
+
let closeIdx = -1;
|
|
347
|
+
for (let j = 1; j < rest.length; j++) {
|
|
348
|
+
if (rest[j] === '\\') {
|
|
349
|
+
j++;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (rest[j] === '"') {
|
|
353
|
+
closeIdx = j;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (closeIdx < 0)
|
|
358
|
+
throw new Error('unterminated_quote');
|
|
359
|
+
name = parseQuotedString(rest.slice(0, closeIdx + 1));
|
|
360
|
+
afterName = rest.slice(closeIdx + 1).trimStart();
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
const sp = rest.indexOf(' ');
|
|
364
|
+
if (sp < 0)
|
|
365
|
+
throw new Error(`invalid attachment: ${rest}`);
|
|
366
|
+
name = rest.slice(0, sp);
|
|
367
|
+
afterName = rest.slice(sp).trimStart();
|
|
368
|
+
}
|
|
369
|
+
if (afterName.startsWith('{}')) {
|
|
370
|
+
const nested = {};
|
|
371
|
+
const consumed = parseObjectBody(lines, lineIdx + 1, depth, nested);
|
|
372
|
+
return [name, nested, consumed + 1];
|
|
373
|
+
}
|
|
374
|
+
if (afterName.startsWith('[')) {
|
|
375
|
+
const [arr, consumed] = parseArrayFromHeader(lines, lineIdx, depth, afterName);
|
|
376
|
+
return [name, arr, consumed];
|
|
377
|
+
}
|
|
378
|
+
throw new Error(`invalid attachment form: ${afterName}`);
|
|
379
|
+
}
|
|
380
|
+
function parseExpandedBody(lines, start, depth) {
|
|
381
|
+
const ind = ' '.repeat(depth);
|
|
193
382
|
const items = [];
|
|
194
383
|
let i = start;
|
|
195
384
|
while (i < lines.length) {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
i++;
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
const content = depth > 0 ? (raw.startsWith(indent) ? raw.slice(indent.length) : null) : raw;
|
|
385
|
+
const line = lines[i];
|
|
386
|
+
const content = depth > 0 ? (line.startsWith(ind) ? line.slice(ind.length) : null) : line;
|
|
202
387
|
if (content === null)
|
|
203
388
|
break;
|
|
204
|
-
if (content.startsWith('## '))
|
|
389
|
+
if (content.startsWith('## ') || content.startsWith('##!'))
|
|
205
390
|
break;
|
|
206
|
-
if (content.startsWith('@'))
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
391
|
+
if (!content.startsWith('@'))
|
|
392
|
+
break;
|
|
393
|
+
const sp = content.indexOf(' ');
|
|
394
|
+
if (sp < 0)
|
|
395
|
+
break;
|
|
396
|
+
const idStr = content.slice(1, sp);
|
|
397
|
+
const id = parseInt(idStr, 10);
|
|
398
|
+
if (!isNaN(id) && id !== items.length) {
|
|
399
|
+
throw new Error(`invalid_item_id: expected @${items.length}, got @${idStr}`);
|
|
400
|
+
}
|
|
401
|
+
const marker = content.slice(sp + 1);
|
|
402
|
+
if (marker.startsWith('=')) {
|
|
403
|
+
items.push(parseScalar(marker.slice(1), false));
|
|
211
404
|
i++;
|
|
405
|
+
continue;
|
|
212
406
|
}
|
|
213
|
-
|
|
214
|
-
|
|
407
|
+
if (marker.startsWith('{}')) {
|
|
408
|
+
const nested = {};
|
|
409
|
+
i++;
|
|
410
|
+
const consumed = parseObjectBody(lines, i, depth + 1, nested);
|
|
411
|
+
items.push(nested);
|
|
412
|
+
i += consumed;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (marker.startsWith('[')) {
|
|
416
|
+
const [arr, consumed] = parseArrayFromHeader(lines, i, depth + 1, marker);
|
|
417
|
+
items.push(arr);
|
|
418
|
+
i += consumed;
|
|
419
|
+
continue;
|
|
215
420
|
}
|
|
421
|
+
break;
|
|
216
422
|
}
|
|
217
423
|
return [items, i - start];
|
|
218
424
|
}
|
|
219
|
-
function
|
|
220
|
-
if (s === '
|
|
221
|
-
return
|
|
222
|
-
if (s === '
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
425
|
+
function parseCount(s) {
|
|
426
|
+
if (s === '0')
|
|
427
|
+
return 0;
|
|
428
|
+
if (!s.length || s[0] === '0')
|
|
429
|
+
throw new Error(`invalid_count: ${s}`);
|
|
430
|
+
const n = parseInt(s, 10);
|
|
431
|
+
if (isNaN(n) || String(n) !== s)
|
|
432
|
+
throw new Error(`invalid_count: ${s}`);
|
|
433
|
+
return n;
|
|
434
|
+
}
|
|
435
|
+
function validateSummaryCounts(summaryLine, deferredCount, contentLines) {
|
|
436
|
+
// Parse counts from "##! summary counts=N,M,..."
|
|
437
|
+
const parts = summaryLine.split(/\s+/);
|
|
438
|
+
let countsStr = '';
|
|
439
|
+
for (const p of parts) {
|
|
440
|
+
if (p.startsWith('counts=')) {
|
|
441
|
+
countsStr = p.slice(7);
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (!countsStr)
|
|
446
|
+
return;
|
|
447
|
+
const countVals = countsStr.split(',');
|
|
448
|
+
if (countVals.length !== deferredCount) {
|
|
449
|
+
throw new Error(`count_mismatch: summary has ${countVals.length} count entries but ${deferredCount} deferred sections`);
|
|
450
|
+
}
|
|
451
|
+
// Count actual items per deferred section.
|
|
452
|
+
const actualCounts = [];
|
|
453
|
+
let inDeferred = false;
|
|
454
|
+
let currentCount = 0;
|
|
455
|
+
for (const l of contentLines) {
|
|
456
|
+
const trimmed = l.trimStart();
|
|
457
|
+
if (trimmed.startsWith('## ') && trimmed.includes('[?]')) {
|
|
458
|
+
if (inDeferred)
|
|
459
|
+
actualCounts.push(currentCount);
|
|
460
|
+
inDeferred = true;
|
|
461
|
+
currentCount = 0;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (trimmed.startsWith('## ')) {
|
|
465
|
+
if (inDeferred) {
|
|
466
|
+
actualCounts.push(currentCount);
|
|
467
|
+
inDeferred = false;
|
|
468
|
+
}
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (inDeferred && !trimmed.startsWith(' ') && !trimmed.startsWith('.')) {
|
|
472
|
+
currentCount++;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (inDeferred)
|
|
476
|
+
actualCounts.push(currentCount);
|
|
477
|
+
for (let i = 0; i < countVals.length; i++) {
|
|
478
|
+
const declared = parseInt(countVals[i], 10);
|
|
479
|
+
if (isNaN(declared))
|
|
480
|
+
throw new Error(`count_mismatch: invalid count value "${countVals[i]}"`);
|
|
481
|
+
if (i < actualCounts.length && declared !== actualCounts[i]) {
|
|
482
|
+
throw new Error(`count_mismatch: section ${i} declared ${declared} in summary, actual ${actualCounts[i]}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
235
485
|
}
|
|
236
486
|
//# sourceMappingURL=decode_generic.js.map
|