flex-md 2.0.0 → 3.1.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 (66) hide show
  1. package/README.md +57 -309
  2. package/dist/__tests__/ofs.test.d.ts +1 -0
  3. package/dist/__tests__/ofs.test.js +51 -0
  4. package/dist/__tests__/validate.test.d.ts +1 -0
  5. package/dist/__tests__/validate.test.js +108 -0
  6. package/dist/detect/json/detectIntent.d.ts +2 -0
  7. package/dist/detect/json/detectIntent.js +79 -0
  8. package/dist/detect/json/detectPresence.d.ts +6 -0
  9. package/dist/detect/json/detectPresence.js +191 -0
  10. package/dist/detect/json/index.d.ts +7 -0
  11. package/dist/detect/json/index.js +12 -0
  12. package/dist/detect/json/types.d.ts +43 -0
  13. package/dist/detect/json/types.js +1 -0
  14. package/dist/extract/extract.d.ts +5 -0
  15. package/dist/extract/extract.js +50 -0
  16. package/dist/extract/types.d.ts +11 -0
  17. package/dist/extract/types.js +1 -0
  18. package/dist/index.d.ts +11 -15
  19. package/dist/index.js +18 -17
  20. package/dist/issues/build.d.ts +26 -0
  21. package/dist/issues/build.js +62 -0
  22. package/dist/md/lists.d.ts +14 -0
  23. package/dist/md/lists.js +33 -0
  24. package/dist/md/match.d.ts +12 -0
  25. package/dist/md/match.js +44 -0
  26. package/dist/md/outline.d.ts +6 -0
  27. package/dist/md/outline.js +67 -0
  28. package/dist/md/parse.d.ts +29 -0
  29. package/dist/md/parse.js +105 -0
  30. package/dist/md/tables.d.ts +25 -0
  31. package/dist/md/tables.js +72 -0
  32. package/dist/ofs/enricher.d.ts +14 -4
  33. package/dist/ofs/enricher.js +76 -20
  34. package/dist/ofs/issues.d.ts +14 -0
  35. package/dist/ofs/issues.js +92 -0
  36. package/dist/ofs/issuesEnvelope.d.ts +15 -0
  37. package/dist/ofs/issuesEnvelope.js +71 -0
  38. package/dist/ofs/parser.d.ts +5 -17
  39. package/dist/ofs/parser.js +114 -45
  40. package/dist/ofs/stringify.js +33 -21
  41. package/dist/pipeline/enforce.d.ts +10 -0
  42. package/dist/pipeline/enforce.js +46 -0
  43. package/dist/pipeline/kind.d.ts +16 -0
  44. package/dist/pipeline/kind.js +24 -0
  45. package/dist/pipeline/repair.d.ts +14 -0
  46. package/dist/pipeline/repair.js +112 -0
  47. package/dist/strictness/container.d.ts +14 -0
  48. package/dist/strictness/container.js +46 -0
  49. package/dist/strictness/processor.d.ts +5 -0
  50. package/dist/strictness/processor.js +29 -0
  51. package/dist/strictness/types.d.ts +77 -0
  52. package/dist/strictness/types.js +106 -0
  53. package/dist/test-pipeline.d.ts +1 -0
  54. package/dist/test-pipeline.js +53 -0
  55. package/dist/test-runner.js +10 -7
  56. package/dist/test-strictness.d.ts +1 -0
  57. package/dist/test-strictness.js +213 -0
  58. package/dist/types.d.ts +82 -43
  59. package/dist/validate/policy.d.ts +10 -0
  60. package/dist/validate/policy.js +17 -0
  61. package/dist/validate/types.d.ts +11 -0
  62. package/dist/validate/types.js +1 -0
  63. package/dist/validate/validate.d.ts +2 -0
  64. package/dist/validate/validate.js +308 -0
  65. package/docs/mdflex-compliance.md +216 -0
  66. package/package.json +7 -3
@@ -0,0 +1,191 @@
1
+ const RX_JSON_FENCE = /```json\s*\n([\s\S]*?)\n```/gi;
2
+ const RX_JSON_MARKER = /(BEGIN_JSON|BEGIN\s+JSON)\s*\n([\s\S]*?)\n(END_JSON|END\s+JSON)/gi;
3
+ // Very conservative raw JSON: must be only whitespace + object/array + whitespace
4
+ const RX_RAW_JSON = /^\s*(\{[\s\S]*\}|\[[\s\S]*\])\s*$/;
5
+ // Inline candidate finder
6
+ const RX_INLINE_CANDIDATE = /(\{|\[)/g;
7
+ export function detectJsonContainers(md) {
8
+ const fences = [];
9
+ const markers = [];
10
+ RX_JSON_FENCE.lastIndex = 0;
11
+ RX_JSON_MARKER.lastIndex = 0;
12
+ let m;
13
+ while ((m = RX_JSON_FENCE.exec(md)) !== null) {
14
+ fences.push({
15
+ start: m.index,
16
+ end: m.index + m[0].length,
17
+ kind: "fence",
18
+ snippet: preview(m[0]),
19
+ });
20
+ }
21
+ while ((m = RX_JSON_MARKER.exec(md)) !== null) {
22
+ markers.push({
23
+ start: m.index,
24
+ end: m.index + m[0].length,
25
+ kind: "marker",
26
+ snippet: preview(m[0]),
27
+ });
28
+ }
29
+ return {
30
+ hasJsonFence: fences.length > 0,
31
+ hasJsonMarkers: markers.length > 0,
32
+ fences,
33
+ markers,
34
+ };
35
+ }
36
+ export function detectJsonPresence(text, opts) {
37
+ const spans = [];
38
+ const kinds = new Set();
39
+ // 1) fenced json blocks
40
+ let m;
41
+ RX_JSON_FENCE.lastIndex = 0;
42
+ while ((m = RX_JSON_FENCE.exec(text)) !== null) {
43
+ kinds.add("fenced_json");
44
+ spans.push({
45
+ start: m.index,
46
+ end: m.index + m[0].length,
47
+ kind: "fence",
48
+ snippet: preview(m[0]),
49
+ });
50
+ }
51
+ // 2) BEGIN_JSON / END_JSON
52
+ RX_JSON_MARKER.lastIndex = 0;
53
+ while ((m = RX_JSON_MARKER.exec(text)) !== null) {
54
+ kinds.add("fenced_json");
55
+ spans.push({
56
+ start: m.index,
57
+ end: m.index + m[0].length,
58
+ kind: "marker",
59
+ snippet: preview(m[0]),
60
+ });
61
+ }
62
+ // 3) raw json (standalone)
63
+ if (RX_RAW_JSON.test(text) && looksLikeJson(text)) {
64
+ kinds.add("raw_json");
65
+ spans.push({ start: 0, end: text.length, kind: "raw", snippet: preview(text) });
66
+ }
67
+ else {
68
+ // 4) inline json-ish occurrences (best effort)
69
+ const inlineSpans = findInlineJsonishSpans(text);
70
+ if (inlineSpans.length) {
71
+ kinds.add("inline_json");
72
+ for (const s of inlineSpans)
73
+ spans.push(s);
74
+ }
75
+ }
76
+ const present = kinds.size > 0;
77
+ // Optional parse
78
+ const parse = !!opts?.parse;
79
+ if (!parse || !present) {
80
+ return { present, kinds: Array.from(kinds), spans };
81
+ }
82
+ const maxParses = opts?.maxParses ?? 3;
83
+ const parsedValues = [];
84
+ const errors = [];
85
+ const parseCandidates = spans
86
+ .map((s, i) => ({ s, i }))
87
+ .filter(x => x.s.kind === "fence" || x.s.kind === "raw" || x.s.kind === "inline")
88
+ .slice(0, maxParses);
89
+ for (const { s, i } of parseCandidates) {
90
+ const content = extractJsonText(text, s);
91
+ try {
92
+ parsedValues.push(JSON.parse(content));
93
+ }
94
+ catch (e) {
95
+ errors.push({ spanIndex: i, message: String(e?.message ?? e) });
96
+ if (looksLikeJson(content))
97
+ kinds.add("json_like");
98
+ }
99
+ }
100
+ return {
101
+ present,
102
+ kinds: Array.from(kinds),
103
+ spans,
104
+ parsed: {
105
+ ok: errors.length === 0 && parsedValues.length > 0,
106
+ values: parsedValues,
107
+ errors: errors.length ? errors : undefined,
108
+ },
109
+ };
110
+ }
111
+ function extractJsonText(full, span) {
112
+ const slice = full.slice(span.start, span.end);
113
+ if (span.kind === "fence") {
114
+ const m = slice.match(/^```json\s*\n([\s\S]*?)\n```$/i);
115
+ if (m)
116
+ return m[1] ?? "";
117
+ }
118
+ if (span.kind === "marker") {
119
+ const m = slice.match(/(BEGIN_JSON|BEGIN\s+JSON)\s*\n([\s\S]*?)\n(END_JSON|END\s+JSON)/i);
120
+ if (m)
121
+ return m[2] ?? "";
122
+ }
123
+ return slice;
124
+ }
125
+ function looksLikeJson(s) {
126
+ const t = s.trim();
127
+ if (!t)
128
+ return false;
129
+ if ((t.startsWith("{") && t.endsWith("}")) || (t.startsWith("[") && t.endsWith("]")))
130
+ return true;
131
+ return false;
132
+ }
133
+ function findInlineJsonishSpans(text) {
134
+ const spans = [];
135
+ const maxLen = 5000;
136
+ const masked = maskNonJsonCodeFences(text);
137
+ let m;
138
+ RX_INLINE_CANDIDATE.lastIndex = 0;
139
+ while ((m = RX_INLINE_CANDIDATE.exec(masked)) !== null) {
140
+ const start = m.index;
141
+ const open = masked[start];
142
+ const close = open === "{" ? "}" : "]";
143
+ let depth = 0;
144
+ let end = -1;
145
+ for (let i = start; i < Math.min(masked.length, start + maxLen); i++) {
146
+ const ch = masked[i];
147
+ if (ch === open)
148
+ depth++;
149
+ else if (ch === close) {
150
+ depth--;
151
+ if (depth === 0) {
152
+ end = i + 1;
153
+ break;
154
+ }
155
+ }
156
+ }
157
+ if (end > start) {
158
+ const snippet = text.slice(start, Math.min(end, start + 300));
159
+ if (snippet.trim().length >= 5) {
160
+ spans.push({ start, end, kind: "inline", snippet: preview(text.slice(start, end)) });
161
+ }
162
+ RX_INLINE_CANDIDATE.lastIndex = end;
163
+ }
164
+ }
165
+ return squashOverlaps(spans);
166
+ }
167
+ function maskNonJsonCodeFences(text) {
168
+ const rx = /```(\w+)?\s*\n([\s\S]*?)\n```/g;
169
+ return text.replace(rx, (full, lang) => {
170
+ const l = String(lang ?? "").toLowerCase();
171
+ if (l === "json")
172
+ return full;
173
+ return " ".repeat(full.length);
174
+ });
175
+ }
176
+ function squashOverlaps(spans) {
177
+ spans.sort((a, b) => a.start - b.start || (b.end - b.start) - (a.end - a.start));
178
+ const out = [];
179
+ let last = null;
180
+ for (const s of spans) {
181
+ if (!last || s.start >= last.end) {
182
+ out.push(s);
183
+ last = s;
184
+ }
185
+ }
186
+ return out;
187
+ }
188
+ function preview(s, n = 160) {
189
+ const t = s.replace(/\s+/g, " ").trim();
190
+ return t.length > n ? t.slice(0, n - 1) + "…" : t;
191
+ }
@@ -0,0 +1,7 @@
1
+ import { DetectJsonAllResult } from "./types.js";
2
+ export * from "./types.js";
3
+ export { detectJsonIntent } from "./detectIntent.js";
4
+ export { detectJsonContainers, detectJsonPresence } from "./detectPresence.js";
5
+ export declare function detectJsonAll(textOrMd: string, opts?: {
6
+ parseJson?: boolean;
7
+ }): DetectJsonAllResult;
@@ -0,0 +1,12 @@
1
+ import { detectJsonIntent } from "./detectIntent.js";
2
+ import { detectJsonContainers, detectJsonPresence } from "./detectPresence.js";
3
+ export * from "./types.js";
4
+ export { detectJsonIntent } from "./detectIntent.js";
5
+ export { detectJsonContainers, detectJsonPresence } from "./detectPresence.js";
6
+ export function detectJsonAll(textOrMd, opts) {
7
+ return {
8
+ intent: detectJsonIntent(textOrMd),
9
+ containers: detectJsonContainers(textOrMd),
10
+ presence: detectJsonPresence(textOrMd, { parse: !!opts?.parseJson }),
11
+ };
12
+ }
@@ -0,0 +1,43 @@
1
+ export type JsonIntent = "none" | "soft" | "hard" | "schema" | "tooling";
2
+ export type JsonPresenceKind = "none" | "fenced_json" | "raw_json" | "inline_json" | "json_like";
3
+ export interface JsonSpan {
4
+ start: number;
5
+ end: number;
6
+ kind: "fence" | "raw" | "inline" | "marker";
7
+ snippet: string;
8
+ }
9
+ export interface DetectJsonPresenceResult {
10
+ present: boolean;
11
+ kinds: JsonPresenceKind[];
12
+ spans: JsonSpan[];
13
+ parsed?: {
14
+ ok: boolean;
15
+ values: unknown[];
16
+ errors?: Array<{
17
+ spanIndex: number;
18
+ message: string;
19
+ }>;
20
+ };
21
+ }
22
+ export interface DetectJsonContainersResult {
23
+ hasJsonFence: boolean;
24
+ hasJsonMarkers: boolean;
25
+ fences: JsonSpan[];
26
+ markers: JsonSpan[];
27
+ }
28
+ export interface DetectJsonIntentResult {
29
+ intent: JsonIntent;
30
+ signals: Array<{
31
+ type: "pattern";
32
+ value: string;
33
+ strength: "soft" | "hard";
34
+ start?: number;
35
+ end?: number;
36
+ }>;
37
+ confidence: number;
38
+ }
39
+ export interface DetectJsonAllResult {
40
+ intent: DetectJsonIntentResult;
41
+ containers: DetectJsonContainersResult;
42
+ presence: DetectJsonPresenceResult;
43
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import { OutputFormatSpec, ExtractedResult } from "../types.js";
2
+ /**
3
+ * Extracts sections, lists, and tables from Markdown based on the OFS.
4
+ */
5
+ export declare function extractFromMarkdown(md: string, spec: OutputFormatSpec): ExtractedResult;
@@ -0,0 +1,50 @@
1
+ import { parseHeadingsAndSections, extractBullets, normalizeName } from "../md/parse.js";
2
+ /**
3
+ * Extracts sections, lists, and tables from Markdown based on the OFS.
4
+ */
5
+ export function extractFromMarkdown(md, spec) {
6
+ const parsed = parseHeadingsAndSections(md);
7
+ const sectionsByName = {};
8
+ const tables = [];
9
+ const specMap = new Map(spec.sections.map(s => [normalizeName(s.name), s]));
10
+ for (const p of parsed) {
11
+ const norm = normalizeName(p.heading.name);
12
+ const sSpec = specMap.get(norm);
13
+ if (sSpec) {
14
+ const body = p.body.trim();
15
+ let list;
16
+ if (sSpec.kind === "list" || sSpec.kind === "ordered_list") {
17
+ const bullets = extractBullets(body);
18
+ if (bullets.length > 0) {
19
+ list = {
20
+ kind: "list",
21
+ ordered: sSpec.kind === "ordered_list",
22
+ items: bullets.map(b => ({ text: b, children: [] }))
23
+ };
24
+ }
25
+ }
26
+ // Merge if duplicate heading (standard policy for extraction)
27
+ if (sectionsByName[sSpec.name]) {
28
+ sectionsByName[sSpec.name].md += "\n\n" + body;
29
+ if (list && sectionsByName[sSpec.name].list) {
30
+ sectionsByName[sSpec.name].list.items.push(...list.items);
31
+ }
32
+ }
33
+ else {
34
+ sectionsByName[sSpec.name] = {
35
+ nodeKey: norm,
36
+ nodeLevel: p.heading.level,
37
+ md: body,
38
+ list
39
+ };
40
+ }
41
+ }
42
+ }
43
+ // Table extraction logic (simplified for now)
44
+ // In a full impl, we'd search section bodies for tables.
45
+ return {
46
+ outline: { type: "md_outline", nodes: [] }, // Simplified outline
47
+ sectionsByName,
48
+ tables
49
+ };
50
+ }
@@ -0,0 +1,11 @@
1
+ import type { MdOutline, ParsedList, ParsedTable } from "../types.js";
2
+ export interface ExtractedResult {
3
+ outline: MdOutline;
4
+ sectionsByName: Record<string, {
5
+ nodeKey: string;
6
+ nodeLevel: number;
7
+ md: string;
8
+ list?: ParsedList;
9
+ }>;
10
+ tables: ParsedTable[];
11
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,18 +1,14 @@
1
1
  export * from "./types.js";
2
- export { parseFlexMd } from "./parser.js";
3
- export { stringifyFlexMd } from "./stringify.js";
4
- export { validateFlexMd } from "./validator.js";
2
+ export * from "./strictness/types.js";
3
+ export * from "./md/parse.js";
5
4
  export { parseOutputFormatSpec } from "./ofs/parser.js";
6
5
  export { stringifyOutputFormatSpec } from "./ofs/stringify.js";
7
- export { enrichInstructions } from "./ofs/enricher.js";
8
- export { validateOutput } from "./ofs/validator.js";
9
- export type { OfsValidationResult } from "./ofs/validator.js";
10
- export { extractOutput } from "./ofs/extractor.js";
11
- export type { ExtractOptions } from "./ofs/extractor.js";
12
- export { buildOutline, slugify } from "./outline/builder.js";
13
- export { renderOutline } from "./outline/renderer.js";
14
- export { parseList } from "./parsers/lists.js";
15
- export { parsePipeTable, extractAllTables } from "./parsers/tables.js";
16
- export { detectObjects } from "./detection/detector.js";
17
- export { parseAny } from "./detection/extractor.js";
18
- export type { ParseAnyResult } from "./detection/extractor.js";
6
+ export { buildMarkdownGuidance, enrichInstructions } from "./ofs/enricher.js";
7
+ export { validateMarkdownAgainstOfs } from "./validate/validate.js";
8
+ export { extractFromMarkdown } from "./extract/extract.js";
9
+ export { processResponseMarkdown } from "./strictness/processor.js";
10
+ export { parseIssuesEnvelope, buildIssuesEnvelope, buildIssuesEnvelopeAuto } from "./ofs/issuesEnvelope.js";
11
+ export { detectResponseKind } from "./pipeline/kind.js";
12
+ export { repairToMarkdownLevel } from "./pipeline/repair.js";
13
+ export { enforceFlexMd } from "./pipeline/enforce.js";
14
+ export * from "./detect/json/index.js";
package/dist/index.js CHANGED
@@ -1,20 +1,21 @@
1
- // Layer A - FlexMD Frames
1
+ // Core SFMD Types
2
2
  export * from "./types.js";
3
- export { parseFlexMd } from "./parser.js";
4
- export { stringifyFlexMd } from "./stringify.js";
5
- export { validateFlexMd } from "./validator.js";
6
- // Layer B - Output Format Spec (OFS)
3
+ export * from "./strictness/types.js";
4
+ // Shared MD Parsing
5
+ export * from "./md/parse.js";
6
+ // Output Format Spec (OFS)
7
7
  export { parseOutputFormatSpec } from "./ofs/parser.js";
8
8
  export { stringifyOutputFormatSpec } from "./ofs/stringify.js";
9
- export { enrichInstructions } from "./ofs/enricher.js";
10
- export { validateOutput } from "./ofs/validator.js";
11
- export { extractOutput } from "./ofs/extractor.js";
12
- // Outline & Tree
13
- export { buildOutline, slugify } from "./outline/builder.js";
14
- export { renderOutline } from "./outline/renderer.js";
15
- // Parsers
16
- export { parseList } from "./parsers/lists.js";
17
- export { parsePipeTable, extractAllTables } from "./parsers/tables.js";
18
- // Layer C - Detection & Extraction
19
- export { detectObjects } from "./detection/detector.js";
20
- export { parseAny } from "./detection/extractor.js";
9
+ export { buildMarkdownGuidance, enrichInstructions } from "./ofs/enricher.js";
10
+ // Validation & Extraction
11
+ export { validateMarkdownAgainstOfs } from "./validate/validate.js";
12
+ export { extractFromMarkdown } from "./extract/extract.js";
13
+ // Processor & Fallback
14
+ export { processResponseMarkdown } from "./strictness/processor.js";
15
+ export { parseIssuesEnvelope, buildIssuesEnvelope, buildIssuesEnvelopeAuto } from "./ofs/issuesEnvelope.js";
16
+ // Pipeline
17
+ export { detectResponseKind } from "./pipeline/kind.js";
18
+ export { repairToMarkdownLevel } from "./pipeline/repair.js";
19
+ export { enforceFlexMd } from "./pipeline/enforce.js";
20
+ // JSON Detection
21
+ export * from "./detect/json/index.js";
@@ -0,0 +1,26 @@
1
+ import type { StrictnessOptions } from "../strictness/types.js";
2
+ export type IssuesStatusCode = "missing_input" | "unclear_instructions" | "invalid_format" | "unsupported" | (string & {});
3
+ export interface IssueItem {
4
+ issue: string;
5
+ path?: string;
6
+ expected?: string;
7
+ got?: string;
8
+ hint?: string;
9
+ [k: string]: string | undefined;
10
+ }
11
+ export interface BuildIssuesEnvelopeInput {
12
+ status: "error" | "partial";
13
+ code: IssuesStatusCode;
14
+ message: string;
15
+ issues: IssueItem[];
16
+ missingInputs?: string[];
17
+ clarificationsNeeded?: string[];
18
+ /** If provided, wraps output in a single container fence when strictness requires it. */
19
+ strictness?: StrictnessOptions;
20
+ /** Override fence language: "markdown" | "flexmd" (defaults from strictness.container) */
21
+ fence?: "markdown" | "flexmd";
22
+ }
23
+ /**
24
+ * Build a Markdown-only issues envelope.
25
+ */
26
+ export declare function buildIssuesEnvelope(input: BuildIssuesEnvelopeInput): string;
@@ -0,0 +1,62 @@
1
+ import { strictnessDefaults } from "../strictness/types.js";
2
+ /**
3
+ * Build a Markdown-only issues envelope.
4
+ */
5
+ export function buildIssuesEnvelope(input) {
6
+ const strict = input.strictness ? { ...strictnessDefaults(input.strictness.level), ...input.strictness } : undefined;
7
+ const fence = input.fence ?? resolveFence(strict);
8
+ const lines = [];
9
+ lines.push(`## Status`);
10
+ lines.push(`- status: ${input.status}`);
11
+ lines.push(`- code: ${input.code}`);
12
+ lines.push(`- message: ${input.message}`);
13
+ lines.push(``);
14
+ if (input.missingInputs && input.missingInputs.length) {
15
+ lines.push(`## Missing inputs`);
16
+ for (const mi of input.missingInputs)
17
+ lines.push(`- ${mi}`);
18
+ lines.push(``);
19
+ }
20
+ if (input.clarificationsNeeded && input.clarificationsNeeded.length) {
21
+ lines.push(`## Clarifications needed`);
22
+ for (const q of input.clarificationsNeeded)
23
+ lines.push(`- ${q}`);
24
+ lines.push(``);
25
+ }
26
+ lines.push(`## Issues`);
27
+ if (!input.issues.length) {
28
+ lines.push(`- issue: none`);
29
+ }
30
+ else {
31
+ for (const it of input.issues) {
32
+ lines.push(`- issue: ${it.issue}`);
33
+ emitField(lines, "path", it.path);
34
+ emitField(lines, "expected", it.expected);
35
+ emitField(lines, "got", it.got);
36
+ emitField(lines, "hint", it.hint);
37
+ for (const [k, v] of Object.entries(it)) {
38
+ if (k === "issue" || k === "path" || k === "expected" || k === "got" || k === "hint")
39
+ continue;
40
+ emitField(lines, k, v);
41
+ }
42
+ }
43
+ }
44
+ let md = lines.join("\n").trimEnd() + "\n";
45
+ if (strict && strict.level >= 2) {
46
+ const lang = (fence === "flexmd") ? "flexmd" : "markdown";
47
+ md = `\`\`\`${lang}\n${md}\`\`\`\n`;
48
+ }
49
+ return md;
50
+ }
51
+ function emitField(lines, key, value) {
52
+ if (value == null || value === "")
53
+ return;
54
+ lines.push(` ${key}: ${value}`);
55
+ }
56
+ function resolveFence(strict) {
57
+ if (!strict)
58
+ return "markdown";
59
+ if (strict.container === "flexmd_fence")
60
+ return "flexmd";
61
+ return "markdown";
62
+ }
@@ -0,0 +1,14 @@
1
+ export interface ListItem {
2
+ text: string;
3
+ index?: number;
4
+ children: ListItem[];
5
+ }
6
+ export interface ParsedList {
7
+ kind: "list";
8
+ ordered: boolean;
9
+ items: ListItem[];
10
+ }
11
+ /**
12
+ * Parses a flat list segment from Markdown into a nested structure.
13
+ */
14
+ export declare function parseNestedList(sectionMd: string): ParsedList | null;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Parses a flat list segment from Markdown into a nested structure.
3
+ */
4
+ export function parseNestedList(sectionMd) {
5
+ const lines = sectionMd.split("\n");
6
+ const listLines = lines
7
+ .map(l => ({ raw: l, indent: (l.match(/^\s*/)?.[0].length ?? 0) }))
8
+ .filter(x => /^\s*(-\s+|\d+\.\s+)/.test(x.raw));
9
+ if (!listLines.length)
10
+ return null;
11
+ const ordered = listLines.some(x => /^\s*\d+\.\s+/.test(x.raw));
12
+ const root = [];
13
+ const stack = [];
14
+ for (const { raw, indent } of listLines) {
15
+ const mO = raw.match(/^\s*(\d+)\.\s+(.*)$/);
16
+ const mU = raw.match(/^\s*-\s+(.*)$/);
17
+ const text = (mO?.[2] ?? mU?.[1] ?? "").trim();
18
+ const item = { text, children: [] };
19
+ if (mO)
20
+ item.index = Number(mO[1]);
21
+ while (stack.length && stack[stack.length - 1].indent >= indent) {
22
+ stack.pop();
23
+ }
24
+ if (!stack.length) {
25
+ root.push(item);
26
+ }
27
+ else {
28
+ stack[stack.length - 1].item.children.push(item);
29
+ }
30
+ stack.push({ indent, item });
31
+ }
32
+ return { kind: "list", ordered, items: root };
33
+ }
@@ -0,0 +1,12 @@
1
+ import type { MdNode, MdOutline } from "../types.js";
2
+ export interface MatchedSection {
3
+ name: string;
4
+ nodes: MdNode[];
5
+ chosen: MdNode[];
6
+ mergedContent: string;
7
+ }
8
+ /**
9
+ * Matches sections from the outline against required section names.
10
+ * Supports merging multiple nodes of the same name and selecting highest-level matches.
11
+ */
12
+ export declare function matchSections(outline: MdOutline, sectionNames: string[], strategy?: "merge" | "first"): Map<string, MatchedSection>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Matches sections from the outline against required section names.
3
+ * Supports merging multiple nodes of the same name and selecting highest-level matches.
4
+ */
5
+ export function matchSections(outline, sectionNames, strategy = "merge") {
6
+ const normalizedTargets = new Map(sectionNames.map(n => [norm(n), n]));
7
+ const found = new Map();
8
+ walk(outline.nodes, (node) => {
9
+ const k = norm(node.title);
10
+ if (normalizedTargets.has(k)) {
11
+ if (!found.has(k))
12
+ found.set(k, []);
13
+ found.get(k).push(node);
14
+ }
15
+ });
16
+ const res = new Map();
17
+ for (const [nk, originalName] of normalizedTargets.entries()) {
18
+ const nodes = found.get(nk) ?? [];
19
+ if (!nodes.length)
20
+ continue;
21
+ // choose highest-level (smallest level number)
22
+ const minLevel = Math.min(...nodes.map(n => n.level));
23
+ const top = nodes.filter(n => n.level === minLevel);
24
+ let chosen;
25
+ if (strategy === "first") {
26
+ chosen = [top[0]];
27
+ }
28
+ else {
29
+ chosen = top; // merge same-level matches
30
+ }
31
+ const merged = chosen.map(n => n.content_md).join("\n").trimEnd() + "\n";
32
+ res.set(originalName, { name: originalName, nodes, chosen, mergedContent: merged });
33
+ }
34
+ return res;
35
+ }
36
+ function walk(nodes, fn) {
37
+ for (const n of nodes) {
38
+ fn(n);
39
+ walk(n.children, fn);
40
+ }
41
+ }
42
+ function norm(s) {
43
+ return s.trim().replace(/[:\-–—]\s*$/, "").trim().toLowerCase();
44
+ }
@@ -0,0 +1,6 @@
1
+ import type { MdOutline } from "../types.js";
2
+ /**
3
+ * Builds a nested outline tree from Markdown headings.
4
+ * Supports any heading level and captures content between headings.
5
+ */
6
+ export declare function buildOutline(md: string): MdOutline;