markdown-schema 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/dist/index.js ADDED
@@ -0,0 +1,1039 @@
1
+ // src/index/parse-template/infer-section/freetext.ts
2
+ import { toMarkdown } from "mdast-util-to-markdown";
3
+ import { gfmToMarkdown } from "mdast-util-gfm";
4
+
5
+ // src/index/parse-template/_shared/extract-directive-body.ts
6
+ var TEMPLATE_ONLY_RE = /<!--\s*TEMPLATE-ONLY:\s*([\s\S]*?)\s*-->/;
7
+ function extractDirectiveBody(html) {
8
+ const m = html.match(TEMPLATE_ONLY_RE);
9
+ return m ? m[1].trim() : null;
10
+ }
11
+
12
+ // src/index/parse-template/infer-section/freetext.ts
13
+ var freetext = (nodes, { section }) => {
14
+ const hasNestedDirective = (n) => {
15
+ if (typeof n !== "object" || n === null) return false;
16
+ const node = n;
17
+ if (node.type === "html" && typeof node.value === "string") {
18
+ if (extractDirectiveBody(node.value) !== null) return true;
19
+ }
20
+ if (Array.isArray(node.children)) {
21
+ return node.children.some(hasNestedDirective);
22
+ }
23
+ return false;
24
+ };
25
+ for (const n of nodes) {
26
+ if (hasNestedDirective(n)) {
27
+ throw new Error(
28
+ `Section "${section}": free-text bodies cannot contain TEMPLATE-ONLY directives`
29
+ );
30
+ }
31
+ }
32
+ const root = { type: "root", children: nodes };
33
+ return toMarkdown(root, { extensions: [gfmToMarkdown()] }).trim();
34
+ };
35
+
36
+ // src/index/parse-template/infer-section/table.ts
37
+ import { toString } from "mdast-util-to-string";
38
+ var table = (nodes, { section }) => {
39
+ const t = nodes.find((n) => n.type === "table");
40
+ if (!t) throw new Error(`Section "${section}" must contain a table`);
41
+ const [head, ...body] = t.children;
42
+ const headers = head.children.map((c) => toString(c).trim());
43
+ return body.map((row) => {
44
+ const obj = /* @__PURE__ */ Object.create(null);
45
+ row.children.forEach(
46
+ (cell, i) => obj[headers[i]] = toString(cell).trim()
47
+ );
48
+ return obj;
49
+ });
50
+ };
51
+
52
+ // src/index/parse-template/infer-section/bullet-list.ts
53
+ import { toString as toString2 } from "mdast-util-to-string";
54
+ var bulletList = (nodes, { section }) => {
55
+ const l = nodes.find((n) => n.type === "list");
56
+ if (!l) throw new Error(`Section "${section}" must contain a list`);
57
+ return l.children.map((item) => toString2(item).trim());
58
+ };
59
+
60
+ // src/index/parse-template/infer-section/task-list.ts
61
+ import { toString as toString3 } from "mdast-util-to-string";
62
+ var taskList = (nodes, { section }) => {
63
+ const list = nodes.find((n) => n.type === "list");
64
+ if (!list) throw new Error(`Section "${section}" must contain a list`);
65
+ return list.children.map((item) => ({
66
+ checked: item.checked ?? false,
67
+ text: toString3(item).trim()
68
+ }));
69
+ };
70
+
71
+ // src/index/code-blocks.ts
72
+ var codeBlocks = (nodes) => nodes.filter((n) => n.type === "code").map((n) => ({ lang: n.lang ?? null, value: n.value }));
73
+
74
+ // src/index/raw-nodes.ts
75
+ var rawNodes = (nodes) => nodes;
76
+
77
+ // src/index/parse-template/infer-section/optional.ts
78
+ var optional = (ex) => (nodes, ctx) => {
79
+ try {
80
+ return ex(nodes, ctx);
81
+ } catch {
82
+ return void 0;
83
+ }
84
+ };
85
+
86
+ // src/index/fenced-code-with-marker.ts
87
+ function fencedCodeWithMarker(opts) {
88
+ return (nodes, { section }) => {
89
+ const idx = nodes.findIndex(
90
+ (n) => n.type === "html" && opts.marker.test(n.value)
91
+ );
92
+ if (idx === -1) {
93
+ throw new Error(
94
+ `Section "${section}" must contain ${opts.markerLabel ?? "a marker comment"}`
95
+ );
96
+ }
97
+ const next = nodes[idx + 1];
98
+ if (!next || next.type !== "code") {
99
+ throw new Error(
100
+ `Section "${section}": code block must follow ${opts.markerLabel ?? "the marker"}`
101
+ );
102
+ }
103
+ return { lang: next.lang ?? null, value: next.value };
104
+ };
105
+ }
106
+
107
+ // src/index/parse-template/infer-section/repeated/repeated-where.ts
108
+ import { toString as toString4 } from "mdast-util-to-string";
109
+ function repeatedWhere(opts) {
110
+ return (nodes, ctx) => {
111
+ const groups = [];
112
+ let current = null;
113
+ for (const node of nodes) {
114
+ if (opts.startsAt(node)) {
115
+ if (current) groups.push(current);
116
+ current = {
117
+ heading: opts.headingFrom?.(node) ?? toString4(node).trim(),
118
+ nodes: opts.includeBoundary ? [node] : []
119
+ };
120
+ } else if (current) {
121
+ current.nodes.push(node);
122
+ }
123
+ }
124
+ if (current) groups.push(current);
125
+ return groups.map((g) => {
126
+ const out = { heading: g.heading };
127
+ for (const [key, ex] of Object.entries(opts.shape)) {
128
+ out[key] = ex(g.nodes, { section: `${ctx.section} > ${g.heading}` });
129
+ }
130
+ return out;
131
+ });
132
+ };
133
+ }
134
+
135
+ // src/index/parse-template/infer-section/repeated.ts
136
+ import { toString as toString5 } from "mdast-util-to-string";
137
+ function repeated(opts) {
138
+ return (nodes, ctx) => {
139
+ let depth = opts.by;
140
+ return repeatedWhere({
141
+ startsAt: (n) => {
142
+ if (n.type !== "heading") return false;
143
+ if (depth === void 0) depth = n.depth;
144
+ return n.depth === depth;
145
+ },
146
+ headingFrom: (n) => toString5(n).trim(),
147
+ shape: opts.shape,
148
+ includeBoundary: false
149
+ })(nodes, ctx);
150
+ };
151
+ }
152
+
153
+ // src/index/parse-template/define-doc-schema.ts
154
+ import { unified } from "unified";
155
+ import remarkParse from "remark-parse";
156
+ import remarkFrontmatter from "remark-frontmatter";
157
+ import remarkGfm from "remark-gfm";
158
+ import { z } from "zod";
159
+
160
+ // src/index/parse-template/define-doc-schema/split-doc.ts
161
+ import { toString as toString6 } from "mdast-util-to-string";
162
+
163
+ // src/index/parse-template/define-doc-schema/split-doc/extract-frontmatter.ts
164
+ import { parse as parseYaml } from "yaml";
165
+ function extractFrontmatter(tree) {
166
+ const node = tree.children.find((n) => n.type === "yaml");
167
+ if (!node) return void 0;
168
+ const raw = node.value;
169
+ const parsed = parseYaml(raw);
170
+ return parsed ?? {};
171
+ }
172
+
173
+ // src/index/parse-template/define-doc-schema/split-doc.ts
174
+ function splitDoc(tree, opts) {
175
+ const frontmatter = extractFrontmatter(tree);
176
+ let title = "";
177
+ const sections = /* @__PURE__ */ new Map();
178
+ let current = null;
179
+ for (const node of tree.children) {
180
+ if (opts.titleDepth !== void 0 && !title && node.type === "heading" && node.depth === opts.titleDepth) {
181
+ title = toString6(node).trim();
182
+ continue;
183
+ }
184
+ if (node.type === "heading" && node.depth === opts.sectionDepth) {
185
+ current = toString6(node).trim();
186
+ sections.set(current, []);
187
+ continue;
188
+ }
189
+ if (current) sections.get(current).push(node);
190
+ }
191
+ return { frontmatter, title, sections };
192
+ }
193
+
194
+ // src/index/parse-template/define-doc-schema.ts
195
+ function defineDocSchema(spec) {
196
+ const shape = {};
197
+ shape.frontmatter = z.unknown().optional();
198
+ if (spec.title) shape.title = spec.title.schema;
199
+ for (const [key, sec] of Object.entries(spec.sections)) {
200
+ shape[key] = sec.optional ? sec.schema.optional() : sec.schema;
201
+ }
202
+ let composite = z.object(shape).strict();
203
+ if (spec.refine) {
204
+ composite = composite.superRefine(
205
+ (doc, ctx) => spec.refine(doc, ctx)
206
+ );
207
+ }
208
+ const sectionDepth = spec.sectionDepth ?? 2;
209
+ const titleDepth = spec.title ? spec.titleDepth ?? 1 : void 0;
210
+ return {
211
+ /**
212
+ * Parses and validates a raw markdown string against this document schema.
213
+ *
214
+ * @param raw - The raw markdown content to parse.
215
+ * @returns A fully-typed document object.
216
+ */
217
+ parse(raw) {
218
+ const tree = unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm).parse(raw);
219
+ const { frontmatter, title, sections } = splitDoc(tree, {
220
+ titleDepth,
221
+ sectionDepth
222
+ });
223
+ const lifted = {};
224
+ if (frontmatter !== void 0) lifted.frontmatter = frontmatter;
225
+ if (spec.title) lifted.title = title;
226
+ for (const [key, sec] of Object.entries(spec.sections)) {
227
+ const heading = sec.heading ?? key;
228
+ const nodes = sections.get(heading);
229
+ if (!nodes) {
230
+ if (sec.optional) continue;
231
+ throw new Error(`Missing required section: "${heading}"`);
232
+ }
233
+ if (nodes.length === 0 && sec.optional) continue;
234
+ lifted[key] = sec.extract(nodes, { section: heading });
235
+ }
236
+ return composite.parse(lifted);
237
+ }
238
+ };
239
+ }
240
+
241
+ // src/index/parse-template.ts
242
+ import { unified as unified2 } from "unified";
243
+ import remarkParse2 from "remark-parse";
244
+ import remarkFrontmatter2 from "remark-frontmatter";
245
+ import remarkGfm2 from "remark-gfm";
246
+
247
+ // src/index/parse-template/infer-section.ts
248
+ import { z as z6 } from "zod";
249
+
250
+ // src/index/parse-template/_shared/parse-directive/_shared/split-escaped-choices.ts
251
+ function splitEscapedChoices(input) {
252
+ const choices = [];
253
+ let current = "";
254
+ let i = 0;
255
+ while (i < input.length) {
256
+ const ch = input[i];
257
+ if (ch === "\\") {
258
+ const next = input[i + 1];
259
+ if (next === "\\") {
260
+ current += "\\";
261
+ i += 2;
262
+ } else if (next === "|") {
263
+ current += "|";
264
+ i += 2;
265
+ } else {
266
+ current += "\\" + (next ?? "");
267
+ i += 2;
268
+ }
269
+ } else if (ch === "|") {
270
+ choices.push(current.trim());
271
+ current = "";
272
+ i++;
273
+ } else {
274
+ current += ch;
275
+ i++;
276
+ }
277
+ }
278
+ choices.push(current.trim());
279
+ return choices.filter(Boolean);
280
+ }
281
+
282
+ // src/index/parse-template/_shared/parse-directive/parse-enum.ts
283
+ function parseEnum(body) {
284
+ const parts = body.split(";").map((s) => s.trim());
285
+ const choicesPart = parts[0] ?? body;
286
+ const after = choicesPart.slice("enum:".length).trim();
287
+ const choices = splitEscapedChoices(after);
288
+ const required = !parts.slice(1).includes("optional");
289
+ return { kind: "enum", choices, required };
290
+ }
291
+
292
+ // src/index/parse-template/_shared/parse-directive/parse-field.ts
293
+ function parseField(body) {
294
+ const parts = body.split(";").map((s) => s.trim());
295
+ let required = true;
296
+ let regex;
297
+ let defaultValue;
298
+ let onlyIf;
299
+ for (const part of parts) {
300
+ if (part === "string") {
301
+ } else if (part === "optional") {
302
+ required = false;
303
+ } else if (part === "required") {
304
+ required = true;
305
+ } else if (part.startsWith("regex")) {
306
+ const m = part.match(/`([^`]+)`/);
307
+ if (m) regex = m[1];
308
+ } else if (part.startsWith("default=")) {
309
+ defaultValue = part.slice("default=".length).replace(/^["']/, "").replace(/["']$/, "");
310
+ } else if (part.startsWith("only-if ")) {
311
+ const rest = part.slice("only-if ".length);
312
+ const eqIdx = rest.indexOf("=");
313
+ if (eqIdx > -1) {
314
+ onlyIf = {
315
+ key: rest.slice(0, eqIdx).trim(),
316
+ value: rest.slice(eqIdx + 1).trim()
317
+ };
318
+ }
319
+ }
320
+ }
321
+ return { kind: "field", required, regex, defaultValue, onlyIf };
322
+ }
323
+
324
+ // src/index/parse-template/_shared/directive-error.ts
325
+ var DirectiveError = class extends Error {
326
+ constructor(message, options) {
327
+ super(message);
328
+ this.name = "DirectiveError";
329
+ this.body = options?.body;
330
+ this.position = options?.position;
331
+ this.file = options?.file;
332
+ }
333
+ /** The raw directive body that triggered the error, if available. */
334
+ body;
335
+ /** Source position of the directive opener, if available. */
336
+ position;
337
+ /** Source file path, if available. */
338
+ file;
339
+ };
340
+
341
+ // src/index/parse-template/_shared/parse-directive/parse-freetext-directive.ts
342
+ function parseFreetextDirective(body) {
343
+ const allLines = body.split("\n");
344
+ const openerLine = allLines[0] ?? "";
345
+ const bodyLines = allLines.slice(1);
346
+ for (const line of bodyLines) {
347
+ const trimmed = line.trim();
348
+ if (trimmed.length === 0) continue;
349
+ if (trimmed.startsWith("//")) continue;
350
+ throw new DirectiveError(
351
+ `freetext modifiers must be on the directive opener, not in the body; found: "${trimmed}"`,
352
+ { body }
353
+ );
354
+ }
355
+ const parts = openerLine.slice("freetext".length).split(";").map((s) => s.trim()).filter(Boolean);
356
+ let optional2 = false;
357
+ for (const part of parts) {
358
+ if (part === "optional") {
359
+ optional2 = true;
360
+ } else if (part === "required") {
361
+ } else {
362
+ throw new DirectiveError(`unknown freetext modifier: '${part}'`, {
363
+ body
364
+ });
365
+ }
366
+ }
367
+ return { kind: "freetext", optional: optional2 };
368
+ }
369
+
370
+ // src/index/parse-template/_shared/parse-directive/parse-guide.ts
371
+ function parseGuide(body) {
372
+ const lines = body.split("\n").slice(1);
373
+ for (const line of lines) {
374
+ const trimmed = line.trim();
375
+ if (trimmed.length === 0) continue;
376
+ if (trimmed.startsWith("//")) continue;
377
+ throw new DirectiveError(
378
+ `guide block: line must start with '//' or be blank; got: "${trimmed}"`,
379
+ { body }
380
+ );
381
+ }
382
+ return { kind: "guide" };
383
+ }
384
+
385
+ // src/index/parse-template/_shared/parse-directive/parse-row-directive/parse-column-spec.ts
386
+ function parseColumnSpec(spec) {
387
+ const parts = spec.split(";").map((s) => s.trim());
388
+ let required = true;
389
+ let regex;
390
+ let enumChoices;
391
+ let defaultValue;
392
+ for (const part of parts) {
393
+ if (part === "string") {
394
+ } else if (part === "optional") {
395
+ required = false;
396
+ } else if (part === "required") {
397
+ required = true;
398
+ } else if (part.startsWith("regex")) {
399
+ const m = part.match(/`([^`]+)`/);
400
+ if (m) regex = m[1];
401
+ } else if (part.startsWith("enum:")) {
402
+ enumChoices = splitEscapedChoices(part.slice("enum:".length).trim());
403
+ } else if (part.startsWith("default=")) {
404
+ defaultValue = part.slice("default=".length).replace(/^["']/, "").replace(/["']$/, "");
405
+ }
406
+ }
407
+ return { required, regex, enumChoices, defaultValue };
408
+ }
409
+
410
+ // src/index/parse-template/_shared/parse-directive/parse-row-directive.ts
411
+ function parseRowDirective(body) {
412
+ const allLines = body.split("\n");
413
+ const openerLine = allLines[0] ?? "";
414
+ const bodyLines = allLines.slice(1);
415
+ const openerParts = openerLine.slice("row".length).split(";").map((s) => s.trim()).filter(Boolean);
416
+ const columns = {};
417
+ let minRows;
418
+ let maxRows;
419
+ let sectionOptional = false;
420
+ for (const part of openerParts) {
421
+ if (part.startsWith("min-rows:")) {
422
+ minRows = parseInt(part.slice("min-rows:".length).trim(), 10);
423
+ } else if (part.startsWith("max-rows:")) {
424
+ maxRows = parseInt(part.slice("max-rows:".length).trim(), 10);
425
+ } else if (part === "section-optional") {
426
+ sectionOptional = true;
427
+ }
428
+ }
429
+ for (const line of bodyLines) {
430
+ if (line.trim().length === 0) continue;
431
+ if (line.trim().startsWith("//")) continue;
432
+ if (line.trim().startsWith("min-rows:") || line.trim().startsWith("max-rows:") || line.trim() === "section-optional") {
433
+ throw new DirectiveError(
434
+ `min-rows / max-rows / section-optional must be on the directive opener, not in the body`,
435
+ { body }
436
+ );
437
+ }
438
+ const colonIdx = line.indexOf(":");
439
+ if (colonIdx > -1) {
440
+ let colName = line.slice(0, colonIdx).trim();
441
+ if (colName.startsWith('"') && colName.endsWith('"') || colName.startsWith("'") && colName.endsWith("'")) {
442
+ colName = colName.slice(1, -1);
443
+ }
444
+ const spec = line.slice(colonIdx + 1).trim();
445
+ if (colName) columns[colName] = parseColumnSpec(spec);
446
+ }
447
+ }
448
+ return { kind: "row", columns, minRows, maxRows, sectionOptional };
449
+ }
450
+
451
+ // src/index/parse-template/_shared/parse-directive/parse-section-directive.ts
452
+ function parseSectionDirective(body) {
453
+ const allLines = body.split("\n");
454
+ const openerLine = allLines[0] ?? "";
455
+ const bodyLines = allLines.slice(1);
456
+ for (const line of bodyLines) {
457
+ const trimmed = line.trim();
458
+ if (trimmed.length === 0) continue;
459
+ if (trimmed.startsWith("//")) continue;
460
+ throw new DirectiveError(
461
+ `section modifiers must be on the directive opener, not in the body; found: "${trimmed}"`,
462
+ { body }
463
+ );
464
+ }
465
+ const parts = openerLine.slice("section".length).split(";").map((s) => s.trim()).filter(Boolean);
466
+ let optional2 = false;
467
+ let removeIf;
468
+ let minGroups;
469
+ let maxGroups;
470
+ for (const part of parts) {
471
+ if (part === "optional") {
472
+ optional2 = true;
473
+ } else if (part.startsWith("remove-if ")) {
474
+ const rest = part.slice("remove-if ".length);
475
+ const eqIdx = rest.indexOf("=");
476
+ if (eqIdx > -1) {
477
+ removeIf = {
478
+ key: rest.slice(0, eqIdx).trim(),
479
+ value: rest.slice(eqIdx + 1).trim()
480
+ };
481
+ }
482
+ } else if (part.startsWith("min-groups:")) {
483
+ minGroups = parseInt(part.slice("min-groups:".length).trim(), 10);
484
+ } else if (part.startsWith("max-groups:")) {
485
+ maxGroups = parseInt(part.slice("max-groups:".length).trim(), 10);
486
+ }
487
+ }
488
+ return { kind: "section", optional: optional2, removeIf, minGroups, maxGroups };
489
+ }
490
+
491
+ // src/index/parse-template/_shared/parse-directive.ts
492
+ function parseDirective(body, opts) {
493
+ if (body === "guide" || body.startsWith("guide\n") || body.startsWith("guide;"))
494
+ return parseGuide(body);
495
+ if (body === "row" || body.startsWith("row\n") || body.startsWith("row;"))
496
+ return parseRowDirective(body);
497
+ if (body === "section" || body.startsWith("section\n") || body.startsWith("section;"))
498
+ return parseSectionDirective(body);
499
+ if (body === "freetext" || body.startsWith("freetext\n") || body.startsWith("freetext;"))
500
+ return parseFreetextDirective(body);
501
+ const stripped = body.split("\n").filter((l) => !l.trim().startsWith("//")).join("\n").trim();
502
+ const firstToken = stripped.split(/[\s;]/)[0] ?? "";
503
+ if (stripped.startsWith("enum:")) return parseEnum(stripped);
504
+ if (firstToken === "string") return parseField(stripped);
505
+ const token = firstToken || stripped.slice(0, 20);
506
+ throw new DirectiveError(`unknown TEMPLATE-ONLY directive kind: '${token}'`, {
507
+ body,
508
+ position: opts?.position,
509
+ file: opts?.file
510
+ });
511
+ }
512
+
513
+ // src/index/parse-template/infer-section/build-row-schema.ts
514
+ import { z as z2 } from "zod";
515
+
516
+ // src/index/parse-template/_shared/compile-user-regex.ts
517
+ import { checkSync } from "recheck";
518
+ var MAX_REGEX_LENGTH = 200;
519
+ var MAX_POLYNOMIAL_DEGREE = 2;
520
+ function compileUserRegex(src, label) {
521
+ const ctx = label ? ` for ${label}` : "";
522
+ if (src.length > MAX_REGEX_LENGTH) {
523
+ throw new DirectiveError(
524
+ `regex${ctx} is too long (${src.length} > ${MAX_REGEX_LENGTH} characters)`
525
+ );
526
+ }
527
+ const diagnostics = checkSync(src, "");
528
+ if (diagnostics.status === "vulnerable") {
529
+ const c = diagnostics.complexity;
530
+ const tolerable = c?.type === "polynomial" && c.degree <= MAX_POLYNOMIAL_DEGREE;
531
+ if (!tolerable) {
532
+ const detail = c?.type === "polynomial" ? `is super-linear (degree ${c.degree})` : "is vulnerable to catastrophic backtracking (ReDoS)";
533
+ throw new DirectiveError(`regex${ctx} ${detail}: \`${src}\``);
534
+ }
535
+ } else if (diagnostics.status !== "safe") {
536
+ throw new DirectiveError(
537
+ `regex${ctx} could not be proven safe (${diagnostics.status}): \`${src}\``
538
+ );
539
+ }
540
+ try {
541
+ return new RegExp(src);
542
+ } catch (err) {
543
+ throw new DirectiveError(
544
+ `regex${ctx} is invalid: ${err instanceof Error ? err.message : String(err)}`
545
+ );
546
+ }
547
+ }
548
+
549
+ // src/index/parse-template/infer-section/build-row-schema.ts
550
+ function buildRowSchema(columns) {
551
+ const shape = {};
552
+ for (const [col, spec] of Object.entries(columns)) {
553
+ const re = spec.regex ? compileUserRegex(spec.regex, col) : void 0;
554
+ let field;
555
+ if (spec.enumChoices && spec.enumChoices.length > 0) {
556
+ field = z2.enum(spec.enumChoices);
557
+ } else if (re) {
558
+ field = z2.string().regex(re);
559
+ } else {
560
+ field = z2.string();
561
+ }
562
+ if (spec.required) {
563
+ if (spec.enumChoices && spec.enumChoices.length > 0) {
564
+ field = z2.enum(spec.enumChoices);
565
+ } else if (re) {
566
+ field = z2.string().min(1).regex(re);
567
+ } else {
568
+ field = z2.string();
569
+ }
570
+ } else {
571
+ field = field.optional();
572
+ }
573
+ shape[col] = field;
574
+ }
575
+ return z2.object(shape).strict();
576
+ }
577
+
578
+ // src/index/parse-template/infer-section/parse-block-directives/block-html-nodes.ts
579
+ function blockHtmlNodes(nodes) {
580
+ return nodes.filter((n) => n.type === "html");
581
+ }
582
+
583
+ // src/index/parse-template/infer-section/parse-block-directives.ts
584
+ function parseBlockDirectives(nodes) {
585
+ const result = [];
586
+ for (const n of blockHtmlNodes(nodes)) {
587
+ const body = extractDirectiveBody(n.value);
588
+ if (!body) continue;
589
+ const d = parseDirective(body);
590
+ if (d) result.push(d);
591
+ }
592
+ return result;
593
+ }
594
+
595
+ // src/index/parse-template/infer-section/find-directive.ts
596
+ function findDirective(directives, kind) {
597
+ return directives.find(
598
+ (d) => d.kind === kind
599
+ );
600
+ }
601
+
602
+ // src/index/parse-template/_shared/collect-item-directives.ts
603
+ function collectItemDirectives(list) {
604
+ const result = {};
605
+ for (const item of list.children) {
606
+ const para = item.children.find(
607
+ (c) => c.type === "paragraph"
608
+ );
609
+ if (!para) continue;
610
+ let label = "";
611
+ let directive = null;
612
+ for (const child of para.children) {
613
+ if (child.type === "text") {
614
+ label += child.value;
615
+ } else if (child.type === "html") {
616
+ const body = extractDirectiveBody(child.value);
617
+ if (body) {
618
+ directive = parseDirective(body);
619
+ }
620
+ } else if (child.type === "inlineCode") {
621
+ }
622
+ }
623
+ const colonIdx = label.indexOf(":");
624
+ if (colonIdx > -1) {
625
+ const key = label.slice(0, colonIdx).trim();
626
+ if (key) result[key] = directive;
627
+ }
628
+ }
629
+ return result;
630
+ }
631
+
632
+ // src/index/parse-template/infer-section/is-labeled-list.ts
633
+ function isLabeledList(list) {
634
+ const directives = collectItemDirectives(list);
635
+ return Object.keys(directives).length > 0 && Object.values(directives).some((d) => d !== null);
636
+ }
637
+
638
+ // src/index/parse-template/infer-section/is-task-list.ts
639
+ function isTaskList(list) {
640
+ return list.children.some(
641
+ (item) => item.checked !== null && item.checked !== void 0
642
+ );
643
+ }
644
+
645
+ // src/index/parse-template/infer-section/build-labeled-list-schema.ts
646
+ import { z as z3 } from "zod";
647
+ function buildLabeledListSchema(itemDirectives) {
648
+ const specs = {};
649
+ for (const [key, d] of Object.entries(itemDirectives)) {
650
+ if (!d) {
651
+ specs[key] = { required: false, validate: () => null };
652
+ continue;
653
+ }
654
+ if (d.kind === "enum") {
655
+ specs[key] = {
656
+ required: d.required,
657
+ validate: (val) => {
658
+ if (d.required && !val) return `${key} is required`;
659
+ if (val && !d.choices.includes(val))
660
+ return `Invalid option: expected one of ${d.choices.map((c) => `"${c}"`).join("|")}`;
661
+ return null;
662
+ }
663
+ };
664
+ } else if (d.kind === "field") {
665
+ const spec = d;
666
+ const re = spec.regex ? compileUserRegex(spec.regex, key) : void 0;
667
+ specs[key] = {
668
+ required: spec.required,
669
+ validate: (val) => {
670
+ if (spec.required && !val) return `${key} is required`;
671
+ if (val && re) {
672
+ if (!re.test(val)) return `${key} does not match required format`;
673
+ }
674
+ return null;
675
+ },
676
+ onlyIf: spec.onlyIf
677
+ };
678
+ }
679
+ }
680
+ return z3.array(z3.string()).transform((lines) => {
681
+ const obj = /* @__PURE__ */ Object.create(null);
682
+ for (const line of lines) {
683
+ const idx = line.indexOf(":");
684
+ if (idx >= 0) {
685
+ const k = line.slice(0, idx).trim();
686
+ const v = line.slice(idx + 1).trim();
687
+ if (k) obj[k] = v || void 0;
688
+ }
689
+ }
690
+ return obj;
691
+ }).superRefine((obj, ctx) => {
692
+ for (const [key, spec] of Object.entries(specs)) {
693
+ const val = obj[key];
694
+ const err = spec.validate(val);
695
+ if (err) {
696
+ ctx.addIssue({ code: "custom", path: [key], message: err });
697
+ }
698
+ if (spec.onlyIf && val) {
699
+ const guardVal = obj[spec.onlyIf.key];
700
+ if (guardVal !== spec.onlyIf.value) {
701
+ ctx.addIssue({
702
+ code: "custom",
703
+ path: [key],
704
+ message: `${key} is only allowed when ${spec.onlyIf.key}=${spec.onlyIf.value}`
705
+ });
706
+ }
707
+ }
708
+ }
709
+ });
710
+ }
711
+
712
+ // src/index/parse-template/infer-section/build-repeated-schema.ts
713
+ import { z as z4 } from "zod";
714
+ function buildRepeatedSchema(nodes, minGroups, maxGroups, headingSchema) {
715
+ const groupItemZod = z4.object({
716
+ heading: headingSchema ?? z4.string().min(1),
717
+ items: z4.array(z4.string())
718
+ });
719
+ let schema = z4.array(groupItemZod);
720
+ if (minGroups) schema = schema.min(minGroups);
721
+ if (maxGroups) schema = schema.max(maxGroups);
722
+ return schema;
723
+ }
724
+
725
+ // src/index/parse-template/infer-section/directive-to-field-schema.ts
726
+ import { z as z5 } from "zod";
727
+ function directiveToFieldSchema(directive) {
728
+ if (directive.kind === "enum") {
729
+ const base = z5.enum(directive.choices);
730
+ return directive.required ? base : base.optional();
731
+ }
732
+ if (directive.kind === "field") {
733
+ const base = directive.regex ? z5.string().min(1).regex(compileUserRegex(directive.regex)) : z5.string().min(1);
734
+ return directive.required ? base : base.optional();
735
+ }
736
+ return z5.string().min(1);
737
+ }
738
+
739
+ // src/index/parse-template/infer-section.ts
740
+ function inferSection(nodes, _heading) {
741
+ const blockDirectives = parseBlockDirectives(nodes);
742
+ const sectionDir = findDirective(blockDirectives, "section");
743
+ const rowDir = findDirective(blockDirectives, "row");
744
+ const freetextDir = findDirective(blockDirectives, "freetext");
745
+ const isOptional = (sectionDir?.optional ?? rowDir?.sectionOptional ?? false) || (freetextDir?.optional ?? false);
746
+ const contentNodes = nodes.filter((n) => {
747
+ if (n.type !== "html") return true;
748
+ const body = extractDirectiveBody(n.value);
749
+ return body === null;
750
+ });
751
+ if (freetextDir) {
752
+ if (rowDir) {
753
+ throw new Error(
754
+ `Section "${_heading}" cannot use both 'freetext' and 'row' directives`
755
+ );
756
+ }
757
+ let extract = freetext;
758
+ let schema = z6.string().min(1);
759
+ if (isOptional) {
760
+ extract = optional(freetext);
761
+ schema = schema.optional();
762
+ }
763
+ return { extract, schema, optional: isOptional };
764
+ }
765
+ const hasSubHeadings = contentNodes.some(
766
+ (n) => n.type === "heading"
767
+ );
768
+ if (hasSubHeadings) {
769
+ const minGroups = sectionDir?.minGroups;
770
+ const maxGroups = sectionDir?.maxGroups;
771
+ let headingSchema;
772
+ const firstHeading = contentNodes.find(
773
+ (n) => n.type === "heading"
774
+ );
775
+ if (firstHeading) {
776
+ const inlineHtml = firstHeading.children.find(
777
+ (c) => c.type === "html"
778
+ );
779
+ if (inlineHtml) {
780
+ const body = extractDirectiveBody(inlineHtml.value);
781
+ if (body) {
782
+ const dir = parseDirective(body);
783
+ if (dir.kind === "field" || dir.kind === "enum") {
784
+ headingSchema = directiveToFieldSchema(dir);
785
+ } else {
786
+ throw new DirectiveError(
787
+ `H3 sub-headings only support inline field or enum directives, got: ${dir.kind}`
788
+ );
789
+ }
790
+ }
791
+ }
792
+ }
793
+ const schema = buildRepeatedSchema(
794
+ contentNodes,
795
+ minGroups,
796
+ maxGroups,
797
+ headingSchema
798
+ );
799
+ return {
800
+ extract: repeated({ shape: { items: bulletList } }),
801
+ schema,
802
+ optional: isOptional
803
+ };
804
+ }
805
+ const hasTable = contentNodes.some((n) => n.type === "table");
806
+ if (rowDir || hasTable) {
807
+ let rowSchema = z6.record(z6.string(), z6.string());
808
+ if (rowDir) rowSchema = buildRowSchema(rowDir.columns);
809
+ let arraySchema = z6.array(rowSchema);
810
+ if (rowDir?.minRows)
811
+ arraySchema = arraySchema.min(
812
+ rowDir.minRows
813
+ );
814
+ if (rowDir?.maxRows)
815
+ arraySchema = arraySchema.max(
816
+ rowDir.maxRows
817
+ );
818
+ let extract = table;
819
+ if (isOptional) {
820
+ extract = optional(table);
821
+ arraySchema = arraySchema.optional();
822
+ }
823
+ return { extract, schema: arraySchema, optional: isOptional };
824
+ }
825
+ const listNode = contentNodes.find((n) => n.type === "list");
826
+ if (listNode) {
827
+ if (isTaskList(listNode)) {
828
+ return {
829
+ extract: taskList,
830
+ schema: z6.array(z6.object({ checked: z6.boolean(), text: z6.string() })),
831
+ optional: isOptional
832
+ };
833
+ }
834
+ const itemDirs = collectItemDirectives(listNode);
835
+ if (isLabeledList(listNode)) {
836
+ const schema = buildLabeledListSchema(itemDirs);
837
+ return { extract: bulletList, schema, optional: isOptional };
838
+ }
839
+ return {
840
+ extract: bulletList,
841
+ schema: z6.array(z6.string()),
842
+ optional: isOptional
843
+ };
844
+ }
845
+ throw new Error(
846
+ `Section "${_heading}" has no recognized shape; add a 'freetext' directive if free-form content is intended`
847
+ );
848
+ }
849
+
850
+ // src/index/parse-template/split-sections.ts
851
+ import { toString as toString7 } from "mdast-util-to-string";
852
+ function splitSections(tree, titleDepth, sectionDepth) {
853
+ const sections = /* @__PURE__ */ new Map();
854
+ let current = null;
855
+ let titleSeen = false;
856
+ for (const node of tree.children) {
857
+ if (node.type === "heading" && node.depth === titleDepth && !titleSeen) {
858
+ titleSeen = true;
859
+ continue;
860
+ }
861
+ if (node.type === "heading" && node.depth === sectionDepth) {
862
+ for (const child of node.children) {
863
+ if (child.type === "html") {
864
+ const body = extractDirectiveBody(child.value);
865
+ if (body !== null) {
866
+ const pos = node.position?.start;
867
+ throw new DirectiveError(
868
+ `H2 headings cannot carry inline TEMPLATE-ONLY directives \u2014 only H1 (title) and H3 (per-group heading inside repeated sections) support inline directives`,
869
+ {
870
+ body,
871
+ position: pos ? { line: pos.line, column: pos.column } : void 0
872
+ }
873
+ );
874
+ }
875
+ }
876
+ }
877
+ current = toString7(node).trim();
878
+ sections.set(current, []);
879
+ continue;
880
+ }
881
+ if (current !== null) {
882
+ sections.get(current).push(node);
883
+ }
884
+ }
885
+ return sections;
886
+ }
887
+
888
+ // src/index/parse-template/build-title-schema.ts
889
+ import { z as z7 } from "zod";
890
+ function buildTitleSchema(tree, depth) {
891
+ const h = tree.children.find(
892
+ (n) => n.type === "heading" && n.depth === depth
893
+ );
894
+ if (!h) return void 0;
895
+ let prefix = "";
896
+ let hasPlaceholder = false;
897
+ let directiveHtml;
898
+ for (const child of h.children) {
899
+ if (child.type === "text") prefix += child.value;
900
+ else if (child.type === "html") {
901
+ hasPlaceholder = true;
902
+ directiveHtml = child.value;
903
+ break;
904
+ }
905
+ }
906
+ prefix = prefix.replace(/^\s+/, "");
907
+ const hadTrailingSpace = /\s$/.test(prefix);
908
+ prefix = prefix.replace(/\s+$/, "");
909
+ if (prefix && hasPlaceholder) {
910
+ const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
911
+ const sep = hadTrailingSpace ? " " : "";
912
+ const body = directiveHtml ? extractDirectiveBody(directiveHtml) : null;
913
+ let directive = null;
914
+ if (body) {
915
+ try {
916
+ directive = parseDirective(body);
917
+ } catch (err) {
918
+ if (!(err instanceof DirectiveError)) throw err;
919
+ }
920
+ }
921
+ if (directive && directive.kind === "field") {
922
+ const innerSource = directive.regex ? compileUserRegex(directive.regex, "title").source.replace(
923
+ /^\^|\$$/g,
924
+ ""
925
+ ) : void 0;
926
+ const suffix = innerSource ? `(?:${innerSource})` : `.+`;
927
+ const label = directive.regex ? directive.regex : "value";
928
+ return z7.string().regex(
929
+ new RegExp(`^${escaped}${sep}${suffix}$`),
930
+ `title must match '${prefix}${sep}<${label}>'`
931
+ );
932
+ }
933
+ if (directive && directive.kind === "enum") {
934
+ const choicePattern = directive.choices.map((c) => c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
935
+ return z7.string().regex(
936
+ new RegExp(`^${escaped}${sep}(?:${choicePattern})$`),
937
+ `title must be '${prefix}${sep}<${directive.choices.join(" | ")}>'`
938
+ );
939
+ }
940
+ return z7.string().regex(
941
+ new RegExp(`^${escaped}${sep}[A-Z][A-Za-z0-9]*$`),
942
+ `title must be '${prefix}${sep}PascalCaseName'`
943
+ );
944
+ }
945
+ if (prefix) {
946
+ const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
947
+ return z7.string().regex(new RegExp(`^${escaped}$`), `title must be '${prefix}'`);
948
+ }
949
+ return z7.string().min(1);
950
+ }
951
+
952
+ // src/index/parse-template/heading-to-key.ts
953
+ function headingToKey(heading) {
954
+ const withoutNumber = heading.replace(/^\d+(\.\d+)*\.\s*/, "");
955
+ const cleaned = withoutNumber.replace(/\s*\(.*\)\s*$/, "").replace(/\s*\/.*$/, "").trim();
956
+ const words = cleaned.split(/[\s-]+/).filter(Boolean);
957
+ if (words.length === 0) return heading.toLowerCase();
958
+ const [first, ...rest] = words;
959
+ return first.toLowerCase() + rest.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
960
+ }
961
+
962
+ // src/index/parse-template/validate-directive-shape.ts
963
+ function validateDirectiveShape(raw, file) {
964
+ const lines = raw.split("\n");
965
+ for (let i = 0; i < lines.length; i++) {
966
+ const line = lines[i] ?? "";
967
+ const openerIdx = line.indexOf("<!-- TEMPLATE-ONLY:");
968
+ if (openerIdx < 0) continue;
969
+ const beforeOpener = line.slice(0, openerIdx).trim();
970
+ const isInline = beforeOpener.length > 0;
971
+ const closesOnSameLine = line.indexOf("-->", openerIdx) > -1;
972
+ const lineNum = i + 1;
973
+ const col = openerIdx + 1;
974
+ const pos = { line: lineNum, column: col };
975
+ if (isInline && !closesOnSameLine) {
976
+ throw new DirectiveError(
977
+ `${file ?? "template.md"}:${lineNum}:${col}: inline TEMPLATE-ONLY directive must close on the same line; "-->" not found before line end`,
978
+ { position: pos, file }
979
+ );
980
+ }
981
+ if (!isInline && !closesOnSameLine) {
982
+ let found = false;
983
+ for (let j = i + 1; j < lines.length; j++) {
984
+ if ((lines[j] ?? "").includes("-->")) {
985
+ found = true;
986
+ break;
987
+ }
988
+ }
989
+ if (!found) {
990
+ throw new DirectiveError(
991
+ `${file ?? "template.md"}:${lineNum}:${col}: block TEMPLATE-ONLY directive is unclosed; "-->" not found`,
992
+ { position: pos, file }
993
+ );
994
+ }
995
+ }
996
+ }
997
+ }
998
+
999
+ // src/index/parse-template.ts
1000
+ function parseTemplate(templateRaw, opts) {
1001
+ validateDirectiveShape(templateRaw, opts?.file);
1002
+ const tree = unified2().use(remarkParse2).use(remarkFrontmatter2, ["yaml"]).use(remarkGfm2).parse(templateRaw);
1003
+ const titleDepth = 1;
1004
+ const sectionDepth = 2;
1005
+ const titleValidation = buildTitleSchema(tree, titleDepth);
1006
+ const sections = splitSections(tree, titleDepth, sectionDepth);
1007
+ const sectionSpecs = {};
1008
+ for (const [heading, nodes] of sections) {
1009
+ const inference = inferSection(nodes, heading);
1010
+ const key = headingToKey(heading);
1011
+ sectionSpecs[key] = {
1012
+ heading,
1013
+ extract: inference.extract,
1014
+ schema: inference.schema,
1015
+ optional: inference.optional
1016
+ };
1017
+ }
1018
+ return defineDocSchema({
1019
+ title: titleValidation ? { schema: titleValidation } : void 0,
1020
+ sections: sectionSpecs,
1021
+ refine: opts?.refine
1022
+ });
1023
+ }
1024
+ export {
1025
+ bulletList,
1026
+ codeBlocks,
1027
+ defineDocSchema,
1028
+ fencedCodeWithMarker,
1029
+ freetext,
1030
+ headingToKey,
1031
+ optional,
1032
+ parseTemplate,
1033
+ rawNodes,
1034
+ repeated,
1035
+ repeatedWhere,
1036
+ table,
1037
+ taskList
1038
+ };
1039
+ //# sourceMappingURL=index.js.map