flex-md 1.0.0 → 2.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/README.md +312 -6
- package/SPEC.md +559 -0
- package/dist/detection/detector.d.ts +6 -0
- package/dist/detection/detector.js +104 -0
- package/dist/detection/extractor.d.ts +10 -0
- package/dist/detection/extractor.js +54 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +17 -0
- package/dist/ofs/enricher.d.ts +6 -0
- package/dist/ofs/enricher.js +29 -0
- package/dist/ofs/extractor.d.ts +9 -0
- package/dist/ofs/extractor.js +75 -0
- package/dist/ofs/parser.d.ts +21 -0
- package/dist/ofs/parser.js +64 -0
- package/dist/ofs/stringify.d.ts +5 -0
- package/dist/ofs/stringify.js +30 -0
- package/dist/ofs/validator.d.ts +10 -0
- package/dist/ofs/validator.js +91 -0
- package/dist/outline/builder.d.ts +10 -0
- package/dist/outline/builder.js +85 -0
- package/dist/outline/renderer.d.ts +6 -0
- package/dist/outline/renderer.js +23 -0
- package/dist/parser.js +58 -10
- package/dist/parsers/lists.d.ts +6 -0
- package/dist/parsers/lists.js +36 -0
- package/dist/parsers/tables.d.ts +10 -0
- package/dist/parsers/tables.js +58 -0
- package/dist/test-runner.d.ts +1 -0
- package/dist/test-runner.js +328 -0
- package/dist/types.d.ts +91 -0
- package/dist/validator.d.ts +2 -0
- package/dist/validator.js +80 -0
- package/package.json +20 -6
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse nested Markdown lists into a tree structure.
|
|
3
|
+
* Supports both unordered (-) and ordered (1.) lists.
|
|
4
|
+
*/
|
|
5
|
+
export function parseList(md) {
|
|
6
|
+
const lines = md.split("\n");
|
|
7
|
+
const isListLine = (s) => /^\s*(-\s+|\d+\.\s+)/.test(s);
|
|
8
|
+
const listLines = lines.filter(isListLine);
|
|
9
|
+
if (!listLines.length)
|
|
10
|
+
return null;
|
|
11
|
+
// Determine if ordered by presence of numbered item
|
|
12
|
+
const ordered = listLines.some(l => /^\s*\d+\.\s+/.test(l));
|
|
13
|
+
const root = [];
|
|
14
|
+
const stack = [];
|
|
15
|
+
for (const line of listLines) {
|
|
16
|
+
const indent = (line.match(/^\s*/)?.[0].length) ?? 0;
|
|
17
|
+
const mOrdered = line.match(/^\s*(\d+)\.\s+(.*)$/);
|
|
18
|
+
const mUn = line.match(/^\s*-\s+(.*)$/);
|
|
19
|
+
const text = (mOrdered?.[2] ?? mUn?.[1] ?? "").trim();
|
|
20
|
+
const item = { text, children: [] };
|
|
21
|
+
if (mOrdered)
|
|
22
|
+
item.index = Number(mOrdered[1]);
|
|
23
|
+
// Pop stack until we find parent (lower indent)
|
|
24
|
+
while (stack.length && stack[stack.length - 1].indent >= indent) {
|
|
25
|
+
stack.pop();
|
|
26
|
+
}
|
|
27
|
+
if (!stack.length) {
|
|
28
|
+
root.push(item);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
stack[stack.length - 1].item.children.push(item);
|
|
32
|
+
}
|
|
33
|
+
stack.push({ indent, item });
|
|
34
|
+
}
|
|
35
|
+
return { kind: "list", ordered, items: root };
|
|
36
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ParsedTable } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parse a single GFM pipe table block.
|
|
4
|
+
* Returns null if the block is not a valid table.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parsePipeTable(block: string): ParsedTable | null;
|
|
7
|
+
/**
|
|
8
|
+
* Extract all pipe tables from a Markdown document.
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractAllTables(md: string): ParsedTable[];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a single GFM pipe table block.
|
|
3
|
+
* Returns null if the block is not a valid table.
|
|
4
|
+
*/
|
|
5
|
+
export function parsePipeTable(block) {
|
|
6
|
+
const lines = block.split("\n").map(l => l.trim()).filter(Boolean);
|
|
7
|
+
if (lines.length < 2)
|
|
8
|
+
return null;
|
|
9
|
+
const header = lines[0];
|
|
10
|
+
const sep = lines[1];
|
|
11
|
+
if (!header || !header.includes("|"))
|
|
12
|
+
return null;
|
|
13
|
+
if (!sep || !sep.match(/^\|?[\s:-]+\|/))
|
|
14
|
+
return null;
|
|
15
|
+
const parseRow = (row) => row.replace(/^\|/, "").replace(/\|$/, "").split("|").map(c => c.trim());
|
|
16
|
+
const columns = parseRow(header);
|
|
17
|
+
const rows = lines.slice(2).map(parseRow);
|
|
18
|
+
// Normalize row lengths
|
|
19
|
+
for (const r of rows) {
|
|
20
|
+
while (r.length < columns.length)
|
|
21
|
+
r.push("");
|
|
22
|
+
}
|
|
23
|
+
// Detect ordered table (first column is "#")
|
|
24
|
+
const isOrdered = columns[0] === "#";
|
|
25
|
+
const kind = isOrdered ? "ordered_table" : "table";
|
|
26
|
+
return { kind, columns, rows };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Extract all pipe tables from a Markdown document.
|
|
30
|
+
*/
|
|
31
|
+
export function extractAllTables(md) {
|
|
32
|
+
const tables = [];
|
|
33
|
+
const lines = md.split("\n");
|
|
34
|
+
let i = 0;
|
|
35
|
+
while (i < lines.length) {
|
|
36
|
+
const line = lines[i];
|
|
37
|
+
// Look for potential table start (contains |)
|
|
38
|
+
if (line && line.includes("|")) {
|
|
39
|
+
// Collect consecutive lines that look like table rows
|
|
40
|
+
const tableLines = [];
|
|
41
|
+
let j = i;
|
|
42
|
+
while (j < lines.length && lines[j] && lines[j].includes("|")) {
|
|
43
|
+
tableLines.push(lines[j]);
|
|
44
|
+
j++;
|
|
45
|
+
}
|
|
46
|
+
if (tableLines.length >= 2) {
|
|
47
|
+
const table = parsePipeTable(tableLines.join("\n"));
|
|
48
|
+
if (table) {
|
|
49
|
+
tables.push(table);
|
|
50
|
+
i = j;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
i++;
|
|
56
|
+
}
|
|
57
|
+
return tables;
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import {
|
|
2
|
+
// Layer A
|
|
3
|
+
parseFlexMd, stringifyFlexMd, validateFlexMd,
|
|
4
|
+
// Layer B
|
|
5
|
+
parseOutputFormatSpec, stringifyOutputFormatSpec, enrichInstructions, validateOutput, extractOutput,
|
|
6
|
+
// Outline
|
|
7
|
+
buildOutline, renderOutline,
|
|
8
|
+
// Parsers
|
|
9
|
+
parseList, parsePipeTable, extractAllTables,
|
|
10
|
+
// Layer C
|
|
11
|
+
detectObjects, parseAny } from "./index.js";
|
|
12
|
+
let passed = 0;
|
|
13
|
+
let failed = 0;
|
|
14
|
+
function test(name, fn) {
|
|
15
|
+
try {
|
|
16
|
+
fn();
|
|
17
|
+
console.log(`✅ ${name}`);
|
|
18
|
+
passed++;
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
console.error(`❌ ${name}`);
|
|
22
|
+
console.error(e);
|
|
23
|
+
failed++;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function assert(condition, message) {
|
|
27
|
+
if (!condition) {
|
|
28
|
+
throw new Error(`Assertion failed: ${message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
console.log("🧪 Running FlexMD v1.1 Tests\n");
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Layer A Tests
|
|
34
|
+
// ============================================================================
|
|
35
|
+
console.log("📦 Layer A - FlexMD Frames\n");
|
|
36
|
+
test("Parse basic FlexMD frame", () => {
|
|
37
|
+
const md = `[[message role=user id=m1]]
|
|
38
|
+
@tags: auth, login
|
|
39
|
+
Hello world
|
|
40
|
+
`;
|
|
41
|
+
const doc = parseFlexMd(md);
|
|
42
|
+
assert(doc.frames.length === 1, "Should have 1 frame");
|
|
43
|
+
assert(doc.frames[0].type === "message", "Type should be message");
|
|
44
|
+
assert(doc.frames[0].role === "user", "Role should be user");
|
|
45
|
+
assert(Array.isArray(doc.frames[0].meta?.tags), "Tags should be array");
|
|
46
|
+
});
|
|
47
|
+
test("Stringify FlexMD frame", () => {
|
|
48
|
+
const doc = {
|
|
49
|
+
frames: [{
|
|
50
|
+
type: "message",
|
|
51
|
+
role: "user",
|
|
52
|
+
id: "m1",
|
|
53
|
+
meta: { tags: ["auth", "login"] },
|
|
54
|
+
body_md: "Hello world\n"
|
|
55
|
+
}]
|
|
56
|
+
};
|
|
57
|
+
const md = stringifyFlexMd(doc);
|
|
58
|
+
assert(md.includes("[[message"), "Should contain frame header");
|
|
59
|
+
assert(md.includes("@tags:"), "Should contain meta");
|
|
60
|
+
});
|
|
61
|
+
test("Parse with type inference", () => {
|
|
62
|
+
const md = `[[message]]
|
|
63
|
+
@priority: 5
|
|
64
|
+
@enabled: true
|
|
65
|
+
@value: null
|
|
66
|
+
Body text
|
|
67
|
+
`;
|
|
68
|
+
const doc = parseFlexMd(md, { metaTypeMode: "infer" });
|
|
69
|
+
assert(doc.frames[0].meta?.priority === 5, "Should infer number");
|
|
70
|
+
assert(doc.frames[0].meta?.enabled === true, "Should infer boolean");
|
|
71
|
+
assert(doc.frames[0].meta?.value === null, "Should infer null");
|
|
72
|
+
});
|
|
73
|
+
test("Validate FlexMD", () => {
|
|
74
|
+
const valid = `[[message role=user]]
|
|
75
|
+
Hello
|
|
76
|
+
`;
|
|
77
|
+
const result = validateFlexMd(valid);
|
|
78
|
+
assert(result.valid === true, "Should be valid");
|
|
79
|
+
});
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Layer B Tests
|
|
82
|
+
// ============================================================================
|
|
83
|
+
console.log("\n📋 Layer B - Output Format Spec\n");
|
|
84
|
+
test("Parse OFS", () => {
|
|
85
|
+
const md = `## Output format (Markdown)
|
|
86
|
+
Include these sections somewhere (order does not matter):
|
|
87
|
+
|
|
88
|
+
- Short answer — prose
|
|
89
|
+
- Reasoning — ordered list
|
|
90
|
+
- Assumptions — list
|
|
91
|
+
`;
|
|
92
|
+
const spec = parseOutputFormatSpec(md);
|
|
93
|
+
assert(spec.sections.length === 3, "Should have 3 sections");
|
|
94
|
+
assert(spec.sections[0].name === "Short answer", "First section name");
|
|
95
|
+
assert(spec.sections[0].kind === "prose", "First section kind");
|
|
96
|
+
assert(spec.sections[1].kind === "ordered_list", "Second section kind");
|
|
97
|
+
});
|
|
98
|
+
test("Stringify OFS", () => {
|
|
99
|
+
const spec = {
|
|
100
|
+
descriptorType: "output_format_spec",
|
|
101
|
+
format: "markdown",
|
|
102
|
+
sectionOrderMatters: false,
|
|
103
|
+
sections: [
|
|
104
|
+
{ name: "Answer", kind: "prose" }
|
|
105
|
+
],
|
|
106
|
+
tablesOptional: true,
|
|
107
|
+
tables: [],
|
|
108
|
+
emptySectionValue: "None"
|
|
109
|
+
};
|
|
110
|
+
const md = stringifyOutputFormatSpec(spec);
|
|
111
|
+
assert(md.includes("Answer — prose"), "Should contain section");
|
|
112
|
+
assert(md.includes("None"), "Should contain empty value");
|
|
113
|
+
});
|
|
114
|
+
test("Enrich instructions", () => {
|
|
115
|
+
const spec = {
|
|
116
|
+
descriptorType: "output_format_spec",
|
|
117
|
+
format: "markdown",
|
|
118
|
+
sectionOrderMatters: false,
|
|
119
|
+
sections: [
|
|
120
|
+
{ name: "Steps", kind: "ordered_list" },
|
|
121
|
+
{ name: "Notes", kind: "list" }
|
|
122
|
+
],
|
|
123
|
+
tablesOptional: true,
|
|
124
|
+
tables: [],
|
|
125
|
+
emptySectionValue: "None"
|
|
126
|
+
};
|
|
127
|
+
const instructions = enrichInstructions(spec);
|
|
128
|
+
assert(instructions.includes("numbered items"), "Should mention ordered lists");
|
|
129
|
+
assert(instructions.includes("bullets"), "Should mention lists");
|
|
130
|
+
assert(instructions.includes("None"), "Should mention empty value");
|
|
131
|
+
});
|
|
132
|
+
test("Validate output against OFS", () => {
|
|
133
|
+
const spec = {
|
|
134
|
+
descriptorType: "output_format_spec",
|
|
135
|
+
format: "markdown",
|
|
136
|
+
sectionOrderMatters: false,
|
|
137
|
+
sections: [
|
|
138
|
+
{ name: "Answer", kind: "prose" }
|
|
139
|
+
],
|
|
140
|
+
tablesOptional: true,
|
|
141
|
+
tables: []
|
|
142
|
+
};
|
|
143
|
+
const md = `## Answer\nThe answer is 42.`;
|
|
144
|
+
const result = validateOutput(md, spec);
|
|
145
|
+
assert(result.ok === true, "Should validate successfully");
|
|
146
|
+
});
|
|
147
|
+
test("Extract output from OFS", () => {
|
|
148
|
+
const spec = {
|
|
149
|
+
descriptorType: "output_format_spec",
|
|
150
|
+
format: "markdown",
|
|
151
|
+
sectionOrderMatters: false,
|
|
152
|
+
sections: [
|
|
153
|
+
{ name: "Answer", kind: "prose" },
|
|
154
|
+
{ name: "Steps", kind: "ordered_list" }
|
|
155
|
+
],
|
|
156
|
+
tablesOptional: true,
|
|
157
|
+
tables: []
|
|
158
|
+
};
|
|
159
|
+
const md = `## Answer
|
|
160
|
+
The answer is 42.
|
|
161
|
+
|
|
162
|
+
## Steps
|
|
163
|
+
1. First step
|
|
164
|
+
2. Second step
|
|
165
|
+
`;
|
|
166
|
+
const extracted = extractOutput(md, spec);
|
|
167
|
+
assert(extracted.sectionsByName["Answer"].md.includes("42"), "Should extract answer");
|
|
168
|
+
assert(extracted.sectionsByName["Steps"].list?.ordered === true, "Should parse ordered list");
|
|
169
|
+
assert(extracted.sectionsByName["Steps"].list?.items.length === 2, "Should have 2 items");
|
|
170
|
+
});
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// Outline Tests
|
|
173
|
+
// ============================================================================
|
|
174
|
+
console.log("\n🌳 Outline & Tree Building\n");
|
|
175
|
+
test("Build outline from headings", () => {
|
|
176
|
+
const md = `# Title
|
|
177
|
+
Content
|
|
178
|
+
|
|
179
|
+
## Section 1
|
|
180
|
+
More content
|
|
181
|
+
|
|
182
|
+
### Subsection 1.1
|
|
183
|
+
Details
|
|
184
|
+
|
|
185
|
+
## Section 2
|
|
186
|
+
Other content
|
|
187
|
+
`;
|
|
188
|
+
const outline = buildOutline(md);
|
|
189
|
+
assert(outline.nodes.length === 1, "Should have 1 root node");
|
|
190
|
+
assert(outline.nodes[0].title === "Title", "Root title");
|
|
191
|
+
assert(outline.nodes[0].children.length === 2, "Should have 2 children");
|
|
192
|
+
assert(outline.nodes[0].children[0].children.length === 1, "First child has 1 child");
|
|
193
|
+
});
|
|
194
|
+
test("Render outline back to Markdown", () => {
|
|
195
|
+
const outline = {
|
|
196
|
+
type: "md_outline",
|
|
197
|
+
nodes: [{
|
|
198
|
+
title: "Title",
|
|
199
|
+
level: 1,
|
|
200
|
+
key: "title",
|
|
201
|
+
content_md: "Content\n",
|
|
202
|
+
children: [{
|
|
203
|
+
title: "Section",
|
|
204
|
+
level: 2,
|
|
205
|
+
key: "section",
|
|
206
|
+
content_md: "More\n",
|
|
207
|
+
children: []
|
|
208
|
+
}]
|
|
209
|
+
}]
|
|
210
|
+
};
|
|
211
|
+
const md = renderOutline(outline);
|
|
212
|
+
assert(md.includes("# Title"), "Should have h1");
|
|
213
|
+
assert(md.includes("## Section"), "Should have h2");
|
|
214
|
+
assert(!md.includes("key"), "Should not include internal keys");
|
|
215
|
+
});
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// Parser Tests
|
|
218
|
+
// ============================================================================
|
|
219
|
+
console.log("\n📝 List & Table Parsers\n");
|
|
220
|
+
test("Parse unordered list", () => {
|
|
221
|
+
const md = `- Item 1
|
|
222
|
+
- Nested 1.1
|
|
223
|
+
- Item 2
|
|
224
|
+
`;
|
|
225
|
+
const list = parseList(md);
|
|
226
|
+
assert(list !== null, "Should parse list");
|
|
227
|
+
assert(list.ordered === false, "Should be unordered");
|
|
228
|
+
assert(list.items.length === 2, "Should have 2 items");
|
|
229
|
+
assert(list.items[0].children.length === 1, "First item has 1 child");
|
|
230
|
+
});
|
|
231
|
+
test("Parse ordered list", () => {
|
|
232
|
+
const md = `1. First
|
|
233
|
+
1. Sub-first
|
|
234
|
+
2. Second
|
|
235
|
+
`;
|
|
236
|
+
const list = parseList(md);
|
|
237
|
+
assert(list !== null, "Should parse list");
|
|
238
|
+
assert(list.ordered === true, "Should be ordered");
|
|
239
|
+
assert(list.items.length === 2, "Should have 2 items");
|
|
240
|
+
assert(list.items[0].index === 1, "First item index");
|
|
241
|
+
});
|
|
242
|
+
test("Parse pipe table", () => {
|
|
243
|
+
const md = `| Name | Age |
|
|
244
|
+
|------|-----|
|
|
245
|
+
| Alice | 30 |
|
|
246
|
+
| Bob | 25 |
|
|
247
|
+
`;
|
|
248
|
+
const table = parsePipeTable(md);
|
|
249
|
+
assert(table !== null, "Should parse table");
|
|
250
|
+
assert(table.columns.length === 2, "Should have 2 columns");
|
|
251
|
+
assert(table.rows.length === 2, "Should have 2 rows");
|
|
252
|
+
assert(table.kind === "table", "Should be regular table");
|
|
253
|
+
});
|
|
254
|
+
test("Parse ordered table", () => {
|
|
255
|
+
const md = `| # | Task | Status |
|
|
256
|
+
|---|------|--------|
|
|
257
|
+
| 1 | Do it | Done |
|
|
258
|
+
| 2 | Check | Pending |
|
|
259
|
+
`;
|
|
260
|
+
const table = parsePipeTable(md);
|
|
261
|
+
assert(table !== null, "Should parse table");
|
|
262
|
+
assert(table.kind === "ordered_table", "Should be ordered table");
|
|
263
|
+
assert(table.columns[0] === "#", "First column should be #");
|
|
264
|
+
});
|
|
265
|
+
test("Extract all tables", () => {
|
|
266
|
+
const md = `Some text
|
|
267
|
+
|
|
268
|
+
| A | B |
|
|
269
|
+
|---|---|
|
|
270
|
+
| 1 | 2 |
|
|
271
|
+
|
|
272
|
+
More text
|
|
273
|
+
|
|
274
|
+
| C | D |
|
|
275
|
+
|---|---|
|
|
276
|
+
| 3 | 4 |
|
|
277
|
+
`;
|
|
278
|
+
const tables = extractAllTables(md);
|
|
279
|
+
assert(tables.length === 2, "Should find 2 tables");
|
|
280
|
+
});
|
|
281
|
+
// ============================================================================
|
|
282
|
+
// Layer C Tests
|
|
283
|
+
// ============================================================================
|
|
284
|
+
console.log("\n🔍 Layer C - Detection & Extraction\n");
|
|
285
|
+
test("Detect flexmd fence", () => {
|
|
286
|
+
const text = `Some text
|
|
287
|
+
\`\`\`flexmd
|
|
288
|
+
[[message]]
|
|
289
|
+
Hello
|
|
290
|
+
\`\`\`
|
|
291
|
+
More text
|
|
292
|
+
`;
|
|
293
|
+
const detected = detectObjects(text);
|
|
294
|
+
assert(detected.length > 0, "Should detect object");
|
|
295
|
+
assert(detected[0].kind === "flexmd_fence", "Should be flexmd fence");
|
|
296
|
+
assert(detected[0].confidence === 1.0, "Should have high confidence");
|
|
297
|
+
});
|
|
298
|
+
test("Detect JSON FlexDocument", () => {
|
|
299
|
+
const text = `\`\`\`json
|
|
300
|
+
{"frames": [{"type": "message", "body_md": "Hello"}]}
|
|
301
|
+
\`\`\`
|
|
302
|
+
`;
|
|
303
|
+
const detected = detectObjects(text);
|
|
304
|
+
assert(detected.length > 0, "Should detect object");
|
|
305
|
+
assert(detected[0].kind === "flexdoc_json_fence", "Should be JSON FlexDoc");
|
|
306
|
+
});
|
|
307
|
+
test("Parse any text", () => {
|
|
308
|
+
const text = `Random text
|
|
309
|
+
\`\`\`flexmd
|
|
310
|
+
[[message role=user]]
|
|
311
|
+
Hello
|
|
312
|
+
\`\`\`
|
|
313
|
+
More text
|
|
314
|
+
`;
|
|
315
|
+
const result = parseAny(text);
|
|
316
|
+
assert(result.flexDocs.length === 1, "Should extract 1 FlexDoc");
|
|
317
|
+
assert(result.flexDocs[0].frames.length === 1, "Should have 1 frame");
|
|
318
|
+
});
|
|
319
|
+
// ============================================================================
|
|
320
|
+
// Summary
|
|
321
|
+
// ============================================================================
|
|
322
|
+
console.log("\n" + "=".repeat(50));
|
|
323
|
+
console.log(`✅ Passed: ${passed}`);
|
|
324
|
+
console.log(`❌ Failed: ${failed}`);
|
|
325
|
+
console.log("=" + "=".repeat(50));
|
|
326
|
+
if (failed > 0) {
|
|
327
|
+
throw new Error("Tests failed");
|
|
328
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface FlexFrame {
|
|
|
22
22
|
payloads?: Record<string, FlexPayload>;
|
|
23
23
|
}
|
|
24
24
|
export interface FlexDocument {
|
|
25
|
+
title?: string;
|
|
26
|
+
meta?: Record<string, FlexMetaValue>;
|
|
25
27
|
frames: FlexFrame[];
|
|
26
28
|
}
|
|
27
29
|
export interface ParseOptions {
|
|
@@ -30,6 +32,18 @@ export interface ParseOptions {
|
|
|
30
32
|
* Default: ["tags", "refs"]
|
|
31
33
|
*/
|
|
32
34
|
arrayMetaKeys?: string[];
|
|
35
|
+
/**
|
|
36
|
+
* How to parse metadata values:
|
|
37
|
+
* - "strings": all values remain strings (default)
|
|
38
|
+
* - "infer": safely infer boolean, null, number
|
|
39
|
+
* - "schema": use metaSchema to determine types
|
|
40
|
+
*/
|
|
41
|
+
metaTypeMode?: "strings" | "infer" | "schema";
|
|
42
|
+
/**
|
|
43
|
+
* Schema for metadata value types (used when metaTypeMode="schema")
|
|
44
|
+
* Example: { priority: "number", enabled: "boolean" }
|
|
45
|
+
*/
|
|
46
|
+
metaSchema?: Record<string, "string" | "number" | "boolean" | "null">;
|
|
33
47
|
}
|
|
34
48
|
export interface StringifyOptions {
|
|
35
49
|
/**
|
|
@@ -47,3 +61,80 @@ export interface StringifyOptions {
|
|
|
47
61
|
*/
|
|
48
62
|
fence?: "```" | "~~~";
|
|
49
63
|
}
|
|
64
|
+
export interface FlexValidationError {
|
|
65
|
+
line: number;
|
|
66
|
+
message: string;
|
|
67
|
+
severity: "error" | "warning";
|
|
68
|
+
}
|
|
69
|
+
export interface FlexValidationResult {
|
|
70
|
+
valid: boolean;
|
|
71
|
+
errors: FlexValidationError[];
|
|
72
|
+
}
|
|
73
|
+
export type SectionKind = "prose" | "list" | "ordered_list";
|
|
74
|
+
export interface OfsSection {
|
|
75
|
+
name: string;
|
|
76
|
+
kind: SectionKind;
|
|
77
|
+
hint?: string;
|
|
78
|
+
}
|
|
79
|
+
export type TableKind = "table" | "ordered_table";
|
|
80
|
+
export interface OfsTable {
|
|
81
|
+
columns: string[];
|
|
82
|
+
kind: TableKind;
|
|
83
|
+
by?: string;
|
|
84
|
+
}
|
|
85
|
+
export interface OutputFormatSpec {
|
|
86
|
+
descriptorType: "output_format_spec";
|
|
87
|
+
format: "markdown";
|
|
88
|
+
sectionOrderMatters: false;
|
|
89
|
+
sections: OfsSection[];
|
|
90
|
+
tablesOptional: boolean;
|
|
91
|
+
tables: OfsTable[];
|
|
92
|
+
emptySectionValue?: string;
|
|
93
|
+
}
|
|
94
|
+
export interface MdNode {
|
|
95
|
+
title: string;
|
|
96
|
+
level: number;
|
|
97
|
+
key: string;
|
|
98
|
+
id?: string;
|
|
99
|
+
content_md: string;
|
|
100
|
+
children: MdNode[];
|
|
101
|
+
}
|
|
102
|
+
export interface MdOutline {
|
|
103
|
+
type: "md_outline";
|
|
104
|
+
nodes: MdNode[];
|
|
105
|
+
}
|
|
106
|
+
export interface ListItem {
|
|
107
|
+
text: string;
|
|
108
|
+
index?: number;
|
|
109
|
+
children: ListItem[];
|
|
110
|
+
}
|
|
111
|
+
export interface ParsedList {
|
|
112
|
+
kind: "list";
|
|
113
|
+
ordered: boolean;
|
|
114
|
+
items: ListItem[];
|
|
115
|
+
}
|
|
116
|
+
export interface ParsedTable {
|
|
117
|
+
kind: "table" | "ordered_table";
|
|
118
|
+
by?: string;
|
|
119
|
+
columns: string[];
|
|
120
|
+
rows: string[][];
|
|
121
|
+
}
|
|
122
|
+
export interface ExtractedResult {
|
|
123
|
+
outline: MdOutline;
|
|
124
|
+
sectionsByName: Record<string, {
|
|
125
|
+
nodeKey: string;
|
|
126
|
+
nodeLevel: number;
|
|
127
|
+
md: string;
|
|
128
|
+
list?: ParsedList;
|
|
129
|
+
}>;
|
|
130
|
+
tables: ParsedTable[];
|
|
131
|
+
}
|
|
132
|
+
export type DetectedKind = "flexmd_fence" | "flexdoc_json_fence" | "raw_flexmd" | "markdown_snippet" | "none";
|
|
133
|
+
export interface DetectedObject {
|
|
134
|
+
kind: DetectedKind;
|
|
135
|
+
confidence: number;
|
|
136
|
+
start: number;
|
|
137
|
+
end: number;
|
|
138
|
+
raw: string;
|
|
139
|
+
inner?: string;
|
|
140
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const HEADER_START = "[[";
|
|
2
|
+
const HEADER_END = "]]";
|
|
3
|
+
const PAYLOAD_DECL_RE = /^@payload:name:\s*(.+)\s*$/;
|
|
4
|
+
const FENCE_RE = /^(```|~~~)/;
|
|
5
|
+
export function validateFlexMd(input) {
|
|
6
|
+
const lines = input.split("\n");
|
|
7
|
+
const errors = [];
|
|
8
|
+
let inFence = null;
|
|
9
|
+
let pendingPayloadLine = null;
|
|
10
|
+
for (let i = 0; i < lines.length; i++) {
|
|
11
|
+
const line = lines[i];
|
|
12
|
+
const lineNum = i + 1;
|
|
13
|
+
// Check for unterminated headers
|
|
14
|
+
if (line.includes(HEADER_START) && !line.includes(HEADER_END)) {
|
|
15
|
+
// Small check: if it really looks like a header start at beginning of line
|
|
16
|
+
if (line.trim().startsWith(HEADER_START)) {
|
|
17
|
+
errors.push({
|
|
18
|
+
line: lineNum,
|
|
19
|
+
message: "Possible unterminated frame header (missing ']]')",
|
|
20
|
+
severity: "error",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Handle code fences
|
|
25
|
+
const fenceMatch = line.match(FENCE_RE);
|
|
26
|
+
if (fenceMatch) {
|
|
27
|
+
const fence = fenceMatch[1];
|
|
28
|
+
if (inFence) {
|
|
29
|
+
if (line.trimEnd() === inFence) {
|
|
30
|
+
inFence = null;
|
|
31
|
+
pendingPayloadLine = null; // Payload satisfied
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
inFence = fence;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else if (pendingPayloadLine !== null && !inFence && line.trim().length > 0) {
|
|
39
|
+
// We had a @payload:name: but the next non-empty line wasn't a fence
|
|
40
|
+
errors.push({
|
|
41
|
+
line: pendingPayloadLine,
|
|
42
|
+
message: "Payload declaration not followed by a code fence",
|
|
43
|
+
severity: "warning",
|
|
44
|
+
});
|
|
45
|
+
pendingPayloadLine = null;
|
|
46
|
+
}
|
|
47
|
+
// Check for payload declarations
|
|
48
|
+
const payloadMatch = line.match(PAYLOAD_DECL_RE);
|
|
49
|
+
if (payloadMatch) {
|
|
50
|
+
if (inFence) {
|
|
51
|
+
errors.push({
|
|
52
|
+
line: lineNum,
|
|
53
|
+
message: "Payload declaration inside a code fence",
|
|
54
|
+
severity: "warning",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
pendingPayloadLine = lineNum;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (inFence) {
|
|
63
|
+
errors.push({
|
|
64
|
+
line: lines.length,
|
|
65
|
+
message: "Unterminated code fence at end of document",
|
|
66
|
+
severity: "error",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (pendingPayloadLine !== null) {
|
|
70
|
+
errors.push({
|
|
71
|
+
line: pendingPayloadLine,
|
|
72
|
+
message: "Dangling payload declaration at end of document",
|
|
73
|
+
severity: "warning",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
valid: !errors.some((e) => e.severity === "error"),
|
|
78
|
+
errors,
|
|
79
|
+
};
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flex-md",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Parse and stringify FlexMD
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Parse and stringify FlexMD: semi-structured Markdown with three powerful layers - Frames, Output Format Spec (OFS), and Detection/Extraction.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "",
|
|
7
|
-
"keywords": [
|
|
7
|
+
"keywords": [
|
|
8
|
+
"markdown",
|
|
9
|
+
"parser",
|
|
10
|
+
"serialization",
|
|
11
|
+
"llm",
|
|
12
|
+
"prompting",
|
|
13
|
+
"semi-structured",
|
|
14
|
+
"ofs",
|
|
15
|
+
"output-format",
|
|
16
|
+
"detection",
|
|
17
|
+
"extraction"
|
|
18
|
+
],
|
|
8
19
|
"type": "module",
|
|
9
20
|
"main": "./dist/index.cjs",
|
|
10
21
|
"module": "./dist/index.js",
|
|
@@ -16,12 +27,15 @@
|
|
|
16
27
|
"require": "./dist/index.cjs"
|
|
17
28
|
}
|
|
18
29
|
},
|
|
19
|
-
"files": [
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"SPEC.md"
|
|
33
|
+
],
|
|
20
34
|
"scripts": {
|
|
21
35
|
"build": "tsc && node ./scripts/cjs-bridge.mjs",
|
|
22
|
-
"test": "node ./dist/
|
|
36
|
+
"test": "npm run build && node ./dist/test-runner.js"
|
|
23
37
|
},
|
|
24
38
|
"devDependencies": {
|
|
25
39
|
"typescript": "^5.6.3"
|
|
26
40
|
}
|
|
27
|
-
}
|
|
41
|
+
}
|