flex-md 1.1.0 → 3.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.
Files changed (84) hide show
  1. package/README.md +75 -29
  2. package/SPEC.md +559 -0
  3. package/dist/__tests__/validate.test.d.ts +1 -0
  4. package/dist/__tests__/validate.test.js +108 -0
  5. package/dist/detect/json/detectIntent.d.ts +2 -0
  6. package/dist/detect/json/detectIntent.js +79 -0
  7. package/dist/detect/json/detectPresence.d.ts +6 -0
  8. package/dist/detect/json/detectPresence.js +191 -0
  9. package/dist/detect/json/index.d.ts +7 -0
  10. package/dist/detect/json/index.js +12 -0
  11. package/dist/detect/json/types.d.ts +43 -0
  12. package/dist/detect/json/types.js +1 -0
  13. package/dist/detection/detector.d.ts +6 -0
  14. package/dist/detection/detector.js +104 -0
  15. package/dist/detection/extractor.d.ts +10 -0
  16. package/dist/detection/extractor.js +54 -0
  17. package/dist/extract/extract.d.ts +5 -0
  18. package/dist/extract/extract.js +50 -0
  19. package/dist/extract/types.d.ts +11 -0
  20. package/dist/extract/types.js +1 -0
  21. package/dist/index.d.ts +13 -3
  22. package/dist/index.js +20 -3
  23. package/dist/issues/build.d.ts +26 -0
  24. package/dist/issues/build.js +62 -0
  25. package/dist/md/lists.d.ts +14 -0
  26. package/dist/md/lists.js +33 -0
  27. package/dist/md/match.d.ts +12 -0
  28. package/dist/md/match.js +44 -0
  29. package/dist/md/outline.d.ts +6 -0
  30. package/dist/md/outline.js +67 -0
  31. package/dist/md/parse.d.ts +29 -0
  32. package/dist/md/parse.js +105 -0
  33. package/dist/md/tables.d.ts +25 -0
  34. package/dist/md/tables.js +72 -0
  35. package/dist/ofs/enricher.d.ts +16 -0
  36. package/dist/ofs/enricher.js +77 -0
  37. package/dist/ofs/extractor.d.ts +9 -0
  38. package/dist/ofs/extractor.js +75 -0
  39. package/dist/ofs/issues.d.ts +14 -0
  40. package/dist/ofs/issues.js +92 -0
  41. package/dist/ofs/issuesEnvelope.d.ts +15 -0
  42. package/dist/ofs/issuesEnvelope.js +71 -0
  43. package/dist/ofs/parser.d.ts +9 -0
  44. package/dist/ofs/parser.js +133 -0
  45. package/dist/ofs/stringify.d.ts +5 -0
  46. package/dist/ofs/stringify.js +32 -0
  47. package/dist/ofs/validator.d.ts +10 -0
  48. package/dist/ofs/validator.js +91 -0
  49. package/dist/outline/builder.d.ts +10 -0
  50. package/dist/outline/builder.js +85 -0
  51. package/dist/outline/renderer.d.ts +6 -0
  52. package/dist/outline/renderer.js +23 -0
  53. package/dist/parser.js +58 -10
  54. package/dist/parsers/lists.d.ts +6 -0
  55. package/dist/parsers/lists.js +36 -0
  56. package/dist/parsers/tables.d.ts +10 -0
  57. package/dist/parsers/tables.js +58 -0
  58. package/dist/pipeline/enforce.d.ts +10 -0
  59. package/dist/pipeline/enforce.js +46 -0
  60. package/dist/pipeline/kind.d.ts +16 -0
  61. package/dist/pipeline/kind.js +24 -0
  62. package/dist/pipeline/repair.d.ts +14 -0
  63. package/dist/pipeline/repair.js +112 -0
  64. package/dist/strictness/container.d.ts +14 -0
  65. package/dist/strictness/container.js +46 -0
  66. package/dist/strictness/processor.d.ts +5 -0
  67. package/dist/strictness/processor.js +29 -0
  68. package/dist/strictness/types.d.ts +77 -0
  69. package/dist/strictness/types.js +106 -0
  70. package/dist/test-pipeline.d.ts +1 -0
  71. package/dist/test-pipeline.js +53 -0
  72. package/dist/test-runner.d.ts +1 -0
  73. package/dist/test-runner.js +331 -0
  74. package/dist/test-strictness.d.ts +1 -0
  75. package/dist/test-strictness.js +213 -0
  76. package/dist/types.d.ts +140 -22
  77. package/dist/validate/policy.d.ts +10 -0
  78. package/dist/validate/policy.js +17 -0
  79. package/dist/validate/types.d.ts +11 -0
  80. package/dist/validate/types.js +1 -0
  81. package/dist/validate/validate.d.ts +2 -0
  82. package/dist/validate/validate.js +308 -0
  83. package/docs/mdflex-compliance.md +216 -0
  84. package/package.json +15 -6
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { validateMarkdownAgainstOfs } from "../validate/validate.js";
3
+ import { parseIssuesEnvelope } from "../ofs/issuesEnvelope.js";
4
+ const SPEC = {
5
+ emptySectionValue: "None",
6
+ sections: [
7
+ { name: "Short answer", kind: "text", required: true },
8
+ { name: "Long answer", kind: "text", required: true },
9
+ { name: "Reasoning", kind: "ordered_list", required: true },
10
+ { name: "Assumptions", kind: "list", required: true },
11
+ { name: "Unknowns", kind: "list", required: true },
12
+ ],
13
+ };
14
+ function mdL1Good() {
15
+ return [
16
+ "## Short answer",
17
+ "Yes.",
18
+ "",
19
+ "## Long answer",
20
+ "More details.",
21
+ "",
22
+ "## Reasoning",
23
+ "1. First",
24
+ "2. Second",
25
+ "",
26
+ "## Assumptions",
27
+ "- A",
28
+ "",
29
+ "## Unknowns",
30
+ "- U",
31
+ ].join("\n");
32
+ }
33
+ describe("parseIssuesEnvelope()", () => {
34
+ it("detects envelope and extracts bullets", () => {
35
+ const env = [
36
+ "## Status",
37
+ "Non-compliant output (cannot be repaired to the required format).",
38
+ "",
39
+ "## Issues",
40
+ "- Missing required section: \"Short answer\"",
41
+ "",
42
+ "## Expected",
43
+ "- Headings ...",
44
+ "",
45
+ "## Found",
46
+ "- Something ...",
47
+ "",
48
+ "## How to fix",
49
+ "- Do X",
50
+ ].join("\n");
51
+ const parsed = parseIssuesEnvelope(env);
52
+ expect(parsed.isIssuesEnvelope).toBe(true);
53
+ expect(parsed.sections["issues"].bullets[0]).toContain("Missing required section");
54
+ });
55
+ });
56
+ describe("validateMarkdownAgainstOfs()", () => {
57
+ it("L0 accepts anything", () => {
58
+ const r = validateMarkdownAgainstOfs("whatever", SPEC, 0);
59
+ expect(r.ok).toBe(true);
60
+ });
61
+ it("L1 fails when required section missing", () => {
62
+ const md = [
63
+ "## Short answer",
64
+ "Yes.",
65
+ "",
66
+ "## Reasoning",
67
+ "1. A",
68
+ ].join("\n");
69
+ const r = validateMarkdownAgainstOfs(md, SPEC, 1);
70
+ expect(r.ok).toBe(false);
71
+ expect(r.issues.some(i => i.code === "MISSING_SECTION")).toBe(true);
72
+ });
73
+ it("L2 requires a single fenced markdown block", () => {
74
+ const r = validateMarkdownAgainstOfs(mdL1Good(), SPEC, 2);
75
+ expect(r.ok).toBe(false);
76
+ expect(r.issues.some(i => i.code === "CONTAINER_MISSING")).toBe(true);
77
+ });
78
+ it("L2 passes with one fence containing valid L1", () => {
79
+ const md = ["```markdown", mdL1Good(), "```"].join("\n");
80
+ const r = validateMarkdownAgainstOfs(md, SPEC, 2);
81
+ expect(r.ok).toBe(true);
82
+ });
83
+ it("L3 enforces section kinds (Reasoning must be ordered list)", () => {
84
+ const bad = [
85
+ "```markdown",
86
+ [
87
+ "## Short answer",
88
+ "Yes",
89
+ "",
90
+ "## Long answer",
91
+ "Details",
92
+ "",
93
+ "## Reasoning",
94
+ "- bullet but should be ordered",
95
+ "",
96
+ "## Assumptions",
97
+ "- A",
98
+ "",
99
+ "## Unknowns",
100
+ "- U",
101
+ ].join("\n"),
102
+ "```",
103
+ ].join("\n");
104
+ const r = validateMarkdownAgainstOfs(bad, SPEC, 3);
105
+ expect(r.ok).toBe(false);
106
+ expect(r.issues.some(i => i.code === "WRONG_SECTION_KIND")).toBe(true);
107
+ });
108
+ });
@@ -0,0 +1,2 @@
1
+ import { DetectJsonIntentResult } from "./types.js";
2
+ export declare function detectJsonIntent(text: string): DetectJsonIntentResult;
@@ -0,0 +1,79 @@
1
+ const HARD_PATTERNS = [
2
+ /\breturn\s+only\s+json\b/i,
3
+ /\boutput\s+only\s+json\b/i,
4
+ /\bvalid\s+json\s+(object|array)\b/i,
5
+ /\bno\s+(prose|text|markdown)\b/i,
6
+ /\bdo\s+not\s+output\s+anything\s+else\b/i,
7
+ ];
8
+ const SOFT_PATTERNS = [
9
+ /\binclude\s+json\b/i,
10
+ /\bjson\s+preferred\b/i,
11
+ /\badd\s+a\s+json\b/i,
12
+ /\bjson\s+format\b/i,
13
+ ];
14
+ const SCHEMA_PATTERNS = [
15
+ /\bjson\s+schema\b/i,
16
+ /\bschema\b.*\bjson\b/i,
17
+ /\bajv\b/i,
18
+ /\bzod\b/i,
19
+ /\bopenapi\b/i,
20
+ ];
21
+ const TOOLING_PATTERNS = [
22
+ /\bfunction\s+calling\b/i,
23
+ /\btools?\b/i,
24
+ /\bresponse_format\b/i,
25
+ /\btool\s+call\b/i,
26
+ /\barguments\b.*\bjson\b/i,
27
+ ];
28
+ export function detectJsonIntent(text) {
29
+ const signals = [];
30
+ const add = (rx, strength, idx, len) => {
31
+ signals.push({
32
+ type: "pattern",
33
+ value: rx.source,
34
+ strength,
35
+ start: idx,
36
+ end: idx != null && len != null ? idx + len : undefined,
37
+ });
38
+ };
39
+ const scan = (patterns, strength) => {
40
+ for (const base of patterns) {
41
+ const rx = new RegExp(base.source, base.flags.includes("g") ? base.flags : base.flags + "g");
42
+ rx.lastIndex = 0;
43
+ let m;
44
+ while ((m = rx.exec(text)) !== null) {
45
+ add(base, strength, m.index, m[0]?.length ?? 0);
46
+ if (m[0]?.length === 0)
47
+ rx.lastIndex++;
48
+ }
49
+ }
50
+ };
51
+ scan(HARD_PATTERNS, "hard");
52
+ scan(TOOLING_PATTERNS, "soft");
53
+ scan(SCHEMA_PATTERNS, "soft");
54
+ scan(SOFT_PATTERNS, "soft");
55
+ const hasHard = signals.some(s => s.strength === "hard");
56
+ const hasSchema = signals.some(s => SCHEMA_PATTERNS.some(rx => rx.source === s.value));
57
+ const hasTooling = signals.some(s => TOOLING_PATTERNS.some(rx => rx.source === s.value));
58
+ const hasSoft = signals.some(s => s.strength === "soft");
59
+ let intent = "none";
60
+ if (hasHard)
61
+ intent = "hard";
62
+ else if (hasSchema)
63
+ intent = "schema";
64
+ else if (hasTooling)
65
+ intent = "tooling";
66
+ else if (hasSoft)
67
+ intent = "soft";
68
+ const confidence = scoreConfidence(intent, signals);
69
+ return { intent, signals, confidence };
70
+ }
71
+ function scoreConfidence(intent, signals) {
72
+ if (intent === "none")
73
+ return signals.length ? 0.2 : 0.0;
74
+ const hard = signals.filter(s => s.strength === "hard").length;
75
+ const soft = signals.filter(s => s.strength === "soft").length;
76
+ if (intent === "hard")
77
+ return Math.min(1, 0.7 + hard * 0.15);
78
+ return Math.min(1, 0.4 + soft * 0.1);
79
+ }
@@ -0,0 +1,6 @@
1
+ import { DetectJsonPresenceResult, DetectJsonContainersResult } from "./types.js";
2
+ export declare function detectJsonContainers(md: string): DetectJsonContainersResult;
3
+ export declare function detectJsonPresence(text: string, opts?: {
4
+ parse?: boolean;
5
+ maxParses?: number;
6
+ }): DetectJsonPresenceResult;
@@ -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,6 @@
1
+ import type { DetectedObject } from "../types.js";
2
+ /**
3
+ * Detect FlexMD and other structured objects in arbitrary text.
4
+ * Returns all detected objects with confidence scores and byte ranges.
5
+ */
6
+ export declare function detectObjects(text: string): DetectedObject[];
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Detect FlexMD and other structured objects in arbitrary text.
3
+ * Returns all detected objects with confidence scores and byte ranges.
4
+ */
5
+ export function detectObjects(text) {
6
+ const detected = [];
7
+ // Tier A: Detect ```flexmd fenced blocks (highest confidence)
8
+ detected.push(...detectFlexMdFences(text));
9
+ // Tier B: Detect ```json blocks with FlexDocument shape
10
+ detected.push(...detectFlexDocJsonFences(text));
11
+ // Tier C: Detect raw FlexMD markers (best effort)
12
+ detected.push(...detectRawFlexMd(text));
13
+ // Sort by start position
14
+ detected.sort((a, b) => a.start - b.start);
15
+ return detected;
16
+ }
17
+ /**
18
+ * Tier A: Detect ```flexmd fenced blocks.
19
+ */
20
+ function detectFlexMdFences(text) {
21
+ const detected = [];
22
+ const regex = /```flexmd\n([\s\S]*?)```/g;
23
+ let match;
24
+ while ((match = regex.exec(text)) !== null) {
25
+ detected.push({
26
+ kind: "flexmd_fence",
27
+ confidence: 1.0,
28
+ start: match.index,
29
+ end: match.index + match[0].length,
30
+ raw: match[0],
31
+ inner: match[1]
32
+ });
33
+ }
34
+ return detected;
35
+ }
36
+ /**
37
+ * Tier B: Detect ```json blocks that match FlexDocument shape.
38
+ */
39
+ function detectFlexDocJsonFences(text) {
40
+ const detected = [];
41
+ const regex = /```json\n([\s\S]*?)```/g;
42
+ let match;
43
+ while ((match = regex.exec(text)) !== null) {
44
+ const inner = match[1];
45
+ // Try to parse as JSON
46
+ try {
47
+ const parsed = JSON.parse(inner);
48
+ // Check if it has the FlexDocument shape (has "frames" array)
49
+ if (parsed && typeof parsed === "object" && Array.isArray(parsed.frames)) {
50
+ detected.push({
51
+ kind: "flexdoc_json_fence",
52
+ confidence: 0.9,
53
+ start: match.index,
54
+ end: match.index + match[0].length,
55
+ raw: match[0],
56
+ inner
57
+ });
58
+ }
59
+ }
60
+ catch {
61
+ // Not valid JSON, skip
62
+ }
63
+ }
64
+ return detected;
65
+ }
66
+ /**
67
+ * Tier C: Detect raw FlexMD markers (best effort).
68
+ * Looks for at least 2 strong markers within first 500 chars:
69
+ * - [[...]]
70
+ * - @key:
71
+ * - @payload:name:
72
+ */
73
+ function detectRawFlexMd(text) {
74
+ const detected = [];
75
+ // Look for frame markers
76
+ const frameRegex = /\[\[([^\]]+)\]\]/g;
77
+ const metaRegex = /@[a-zA-Z_][a-zA-Z0-9_]*:/g;
78
+ const payloadRegex = /@payload:[a-zA-Z_][a-zA-Z0-9_]*:/g;
79
+ let match;
80
+ const markers = [];
81
+ // Collect all marker positions
82
+ while ((match = frameRegex.exec(text)) !== null) {
83
+ markers.push(match.index);
84
+ }
85
+ while ((match = metaRegex.exec(text)) !== null) {
86
+ markers.push(match.index);
87
+ }
88
+ while ((match = payloadRegex.exec(text)) !== null) {
89
+ markers.push(match.index);
90
+ }
91
+ // If we have at least 2 markers, consider it raw FlexMD
92
+ if (markers.length >= 2) {
93
+ const start = Math.min(...markers);
94
+ const end = text.length;
95
+ detected.push({
96
+ kind: "raw_flexmd",
97
+ confidence: 0.7,
98
+ start,
99
+ end,
100
+ raw: text.substring(start, end)
101
+ });
102
+ }
103
+ return detected;
104
+ }
@@ -0,0 +1,10 @@
1
+ import type { FlexDocument } from "../types.js";
2
+ export interface ParseAnyResult {
3
+ flexDocs: FlexDocument[];
4
+ markdownSnippets: string[];
5
+ remainder: string;
6
+ }
7
+ /**
8
+ * Parse any text and extract all FlexMD documents and Markdown snippets.
9
+ */
10
+ export declare function parseAny(text: string): ParseAnyResult;
@@ -0,0 +1,54 @@
1
+ import { detectObjects } from "./detector.js";
2
+ import { parseFlexMd } from "../parser.js";
3
+ /**
4
+ * Parse any text and extract all FlexMD documents and Markdown snippets.
5
+ */
6
+ export function parseAny(text) {
7
+ const detected = detectObjects(text);
8
+ const flexDocs = [];
9
+ const markdownSnippets = [];
10
+ for (const obj of detected) {
11
+ if (obj.kind === "flexmd_fence" && obj.inner) {
12
+ // Parse fenced FlexMD
13
+ try {
14
+ const doc = parseFlexMd(obj.inner);
15
+ flexDocs.push(doc);
16
+ }
17
+ catch {
18
+ // Parse failed, treat as markdown snippet
19
+ markdownSnippets.push(obj.inner);
20
+ }
21
+ }
22
+ else if (obj.kind === "flexdoc_json_fence" && obj.inner) {
23
+ // Parse JSON FlexDocument
24
+ try {
25
+ const doc = JSON.parse(obj.inner);
26
+ flexDocs.push(doc);
27
+ }
28
+ catch {
29
+ // Parse failed, skip
30
+ }
31
+ }
32
+ else if (obj.kind === "raw_flexmd") {
33
+ // Parse raw FlexMD
34
+ try {
35
+ const doc = parseFlexMd(obj.raw);
36
+ flexDocs.push(doc);
37
+ }
38
+ catch {
39
+ // Parse failed, treat as markdown snippet
40
+ markdownSnippets.push(obj.raw);
41
+ }
42
+ }
43
+ }
44
+ // Calculate remainder (text not covered by detected objects)
45
+ let remainder = text;
46
+ for (const obj of detected) {
47
+ remainder = remainder.replace(obj.raw, "");
48
+ }
49
+ return {
50
+ flexDocs,
51
+ markdownSnippets,
52
+ remainder: remainder.trim()
53
+ };
54
+ }
@@ -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;