flex-md 3.5.0 → 4.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.
- package/README.md +423 -39
- package/dist/index.cjs +62 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/md/parse.d.ts +1 -0
- package/dist/md/parse.js +12 -0
- package/dist/ofs/parser.js +31 -10
- package/dist/tokens/auto-fix.d.ts +10 -0
- package/dist/tokens/auto-fix.js +56 -0
- package/dist/tokens/cognitive-cost.d.ts +10 -0
- package/dist/tokens/cognitive-cost.js +205 -0
- package/dist/tokens/compliance.d.ts +10 -0
- package/dist/tokens/compliance.js +70 -0
- package/dist/tokens/confidence.d.ts +6 -0
- package/dist/tokens/confidence.js +332 -0
- package/dist/tokens/estimator.d.ts +12 -0
- package/dist/tokens/estimator.js +138 -0
- package/dist/tokens/improvements.d.ts +10 -0
- package/dist/tokens/improvements.js +697 -0
- package/dist/tokens/index.d.ts +24 -0
- package/dist/tokens/index.js +31 -0
- package/dist/tokens/parser.d.ts +3 -0
- package/dist/tokens/parser.js +97 -0
- package/dist/tokens/patterns.d.ts +9 -0
- package/dist/tokens/patterns.js +20 -0
- package/dist/tokens/smart-report.d.ts +10 -0
- package/dist/tokens/smart-report.js +187 -0
- package/dist/tokens/spec-estimator.d.ts +7 -0
- package/dist/tokens/spec-estimator.js +68 -0
- package/dist/tokens/types.d.ts +185 -0
- package/dist/tokens/validator.d.ts +16 -0
- package/dist/tokens/validator.js +59 -0
- package/docs/Recommended New Strategies for AI Request Builder.md +691 -0
- package/package.json +7 -3
- package/dist/detection/detector.d.ts +0 -6
- package/dist/detection/detector.js +0 -104
- package/dist/detection/extractor.d.ts +0 -10
- package/dist/detection/extractor.js +0 -54
- package/dist/issues/build.d.ts +0 -26
- package/dist/issues/build.js +0 -62
- package/dist/md/lists.d.ts +0 -14
- package/dist/md/lists.js +0 -33
- package/dist/md/tables.d.ts +0 -25
- package/dist/md/tables.js +0 -72
- package/dist/ofs/extractor.d.ts +0 -9
- package/dist/ofs/extractor.js +0 -75
- package/dist/ofs/issues.d.ts +0 -14
- package/dist/ofs/issues.js +0 -92
- package/dist/ofs/validator.d.ts +0 -10
- package/dist/ofs/validator.js +0 -91
- package/dist/outline/builder.d.ts +0 -10
- package/dist/outline/builder.js +0 -85
- package/dist/outline/renderer.d.ts +0 -6
- package/dist/outline/renderer.js +0 -23
- package/dist/parser.d.ts +0 -2
- package/dist/parser.js +0 -199
- package/dist/parsers/lists.d.ts +0 -6
- package/dist/parsers/lists.js +0 -36
- package/dist/parsers/tables.d.ts +0 -10
- package/dist/parsers/tables.js +0 -58
- package/dist/stringify.d.ts +0 -2
- package/dist/stringify.js +0 -110
- package/dist/test-pipeline.js +0 -53
- package/dist/test-runner.d.ts +0 -1
- package/dist/test-runner.js +0 -331
- package/dist/test-strictness.d.ts +0 -1
- package/dist/test-strictness.js +0 -213
- package/dist/util.d.ts +0 -5
- package/dist/util.js +0 -64
- package/dist/validate/policy.d.ts +0 -10
- package/dist/validate/policy.js +0 -17
- package/dist/validator.d.ts +0 -2
- package/dist/validator.js +0 -80
- /package/dist/{test-pipeline.d.ts → tokens/types.js} +0 -0
package/dist/ofs/validator.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { buildOutline } from "../outline/builder.js";
|
|
2
|
-
/**
|
|
3
|
-
* Validate Markdown output against an OutputFormatSpec.
|
|
4
|
-
*/
|
|
5
|
-
export function validateOutput(md, spec) {
|
|
6
|
-
const outline = buildOutline(md);
|
|
7
|
-
const errors = [];
|
|
8
|
-
const warnings = [];
|
|
9
|
-
// Index nodes by normalized title
|
|
10
|
-
const matches = collectMatches(outline.nodes);
|
|
11
|
-
// Validate each required section
|
|
12
|
-
for (const section of spec.sections) {
|
|
13
|
-
const key = normalizeTitle(section.name);
|
|
14
|
-
const nodes = matches.get(key) ?? [];
|
|
15
|
-
if (nodes.length === 0) {
|
|
16
|
-
errors.push(`missing_section:${section.name}`);
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
// Choose best node (highest level, i.e., smallest level number)
|
|
20
|
-
const chosen = chooseBestNode(nodes);
|
|
21
|
-
const body = chosen.content_md.trim();
|
|
22
|
-
// Check for empty sections
|
|
23
|
-
if (spec.emptySectionValue) {
|
|
24
|
-
if (body === "" && !normalizeNone(body, spec.emptySectionValue)) {
|
|
25
|
-
errors.push(`empty_section_without_none:${section.name}`);
|
|
26
|
-
}
|
|
27
|
-
// If it's "None", skip further validation
|
|
28
|
-
if (normalizeNone(body, spec.emptySectionValue)) {
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
// Validate section kind
|
|
33
|
-
if (section.kind === "list") {
|
|
34
|
-
if (!/^\s*-\s+/m.test(body)) {
|
|
35
|
-
errors.push(`section_not_bullets:${section.name}`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
if (section.kind === "ordered_list") {
|
|
39
|
-
if (!/^\s*\d+\.\s+/m.test(body)) {
|
|
40
|
-
errors.push(`section_not_numbered:${section.name}`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
// Validate tables (if any)
|
|
45
|
-
// Note: Full table validation would require parsing tables from the markdown
|
|
46
|
-
// For now, we just check if tables exist when required
|
|
47
|
-
// This is a simplified version; full implementation would use extractAllTables
|
|
48
|
-
return {
|
|
49
|
-
ok: errors.length === 0,
|
|
50
|
-
errors,
|
|
51
|
-
warnings
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Collect all nodes by normalized title.
|
|
56
|
-
*/
|
|
57
|
-
function collectMatches(nodes) {
|
|
58
|
-
const map = new Map();
|
|
59
|
-
function visit(node) {
|
|
60
|
-
const key = normalizeTitle(node.title);
|
|
61
|
-
const existing = map.get(key) ?? [];
|
|
62
|
-
existing.push(node);
|
|
63
|
-
map.set(key, existing);
|
|
64
|
-
node.children.forEach(visit);
|
|
65
|
-
}
|
|
66
|
-
nodes.forEach(visit);
|
|
67
|
-
return map;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Choose the best node from multiple matches.
|
|
71
|
-
* Prefer highest-level heading (smallest level number).
|
|
72
|
-
*/
|
|
73
|
-
function chooseBestNode(nodes) {
|
|
74
|
-
if (nodes.length === 1)
|
|
75
|
-
return nodes[0];
|
|
76
|
-
// Sort by level (ascending) and take first
|
|
77
|
-
const sorted = [...nodes].sort((a, b) => a.level - b.level);
|
|
78
|
-
return sorted[0];
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Normalize title for comparison: lowercase, remove trailing punctuation.
|
|
82
|
-
*/
|
|
83
|
-
function normalizeTitle(t) {
|
|
84
|
-
return t.trim().replace(/[:\-–—]\s*$/, "").trim().toLowerCase();
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Check if body is the "None" value.
|
|
88
|
-
*/
|
|
89
|
-
function normalizeNone(body, noneValue) {
|
|
90
|
-
return body.trim().toLowerCase() === noneValue.toLowerCase();
|
|
91
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { MdOutline } from "../types.js";
|
|
2
|
-
/**
|
|
3
|
-
* Build a nested outline tree from Markdown headings.
|
|
4
|
-
* Accepts any heading level (#..######) and builds parent/child relationships.
|
|
5
|
-
*/
|
|
6
|
-
export declare function buildOutline(md: string): MdOutline;
|
|
7
|
-
/**
|
|
8
|
-
* Convert title to slug: lowercase, replace spaces with _, remove special chars
|
|
9
|
-
*/
|
|
10
|
-
export declare function slugify(t: string): string;
|
package/dist/outline/builder.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Build a nested outline tree from Markdown headings.
|
|
3
|
-
* Accepts any heading level (#..######) and builds parent/child relationships.
|
|
4
|
-
*/
|
|
5
|
-
export function buildOutline(md) {
|
|
6
|
-
const lines = md.split("\n");
|
|
7
|
-
const nodes = [];
|
|
8
|
-
const stack = [];
|
|
9
|
-
// Collect heading positions
|
|
10
|
-
const headings = [];
|
|
11
|
-
for (let i = 0; i < lines.length; i++) {
|
|
12
|
-
const m = lines[i].match(/^(#{1,6})\s+(.+)\s*$/);
|
|
13
|
-
if (m) {
|
|
14
|
-
headings.push({
|
|
15
|
-
idx: i,
|
|
16
|
-
level: m[1].length,
|
|
17
|
-
title: cleanTitle(m[2])
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
// If no headings, return empty outline
|
|
22
|
-
if (headings.length === 0) {
|
|
23
|
-
return { type: "md_outline", nodes: [] };
|
|
24
|
-
}
|
|
25
|
-
// Build tree using stack-based algorithm
|
|
26
|
-
for (let h = 0; h < headings.length; h++) {
|
|
27
|
-
const cur = headings[h];
|
|
28
|
-
const next = headings[h + 1];
|
|
29
|
-
const contentStart = cur.idx + 1;
|
|
30
|
-
const contentEnd = next ? next.idx : lines.length;
|
|
31
|
-
const content_md = lines.slice(contentStart, contentEnd).join("\n").trimEnd() + "\n";
|
|
32
|
-
const node = {
|
|
33
|
-
title: cur.title,
|
|
34
|
-
level: cur.level,
|
|
35
|
-
key: "", // filled later
|
|
36
|
-
content_md,
|
|
37
|
-
children: []
|
|
38
|
-
};
|
|
39
|
-
// Attach using stack: pop nodes with level >= current level
|
|
40
|
-
while (stack.length && stack[stack.length - 1].level >= node.level) {
|
|
41
|
-
stack.pop();
|
|
42
|
-
}
|
|
43
|
-
if (!stack.length) {
|
|
44
|
-
nodes.push(node);
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
stack[stack.length - 1].children.push(node);
|
|
48
|
-
}
|
|
49
|
-
stack.push(node);
|
|
50
|
-
}
|
|
51
|
-
// Fill keys deterministically (slug + dedup)
|
|
52
|
-
assignKeys(nodes);
|
|
53
|
-
return { type: "md_outline", nodes };
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Clean heading title: remove trailing punctuation like :, -, –, —
|
|
57
|
-
*/
|
|
58
|
-
function cleanTitle(t) {
|
|
59
|
-
return t.trim().replace(/[:\-–—]\s*$/, "").trim();
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Convert title to slug: lowercase, replace spaces with _, remove special chars
|
|
63
|
-
*/
|
|
64
|
-
export function slugify(t) {
|
|
65
|
-
return t.toLowerCase()
|
|
66
|
-
.replace(/[:\-–—]+$/g, "")
|
|
67
|
-
.replace(/\s+/g, "_")
|
|
68
|
-
.replace(/[^a-z0-9_]/g, "")
|
|
69
|
-
.replace(/_+/g, "_")
|
|
70
|
-
.replace(/^_+|_+$/g, "");
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Assign unique keys to all nodes in the tree.
|
|
74
|
-
* Uses slugified titles with deduplication (e.g., "section", "section__2", "section__3")
|
|
75
|
-
*/
|
|
76
|
-
function assignKeys(nodes, seen = new Map()) {
|
|
77
|
-
const visit = (n) => {
|
|
78
|
-
const base = slugify(n.title) || "section";
|
|
79
|
-
const count = (seen.get(base) ?? 0) + 1;
|
|
80
|
-
seen.set(base, count);
|
|
81
|
-
n.key = count === 1 ? base : `${base}__${count}`;
|
|
82
|
-
n.children.forEach(visit);
|
|
83
|
-
};
|
|
84
|
-
nodes.forEach(visit);
|
|
85
|
-
}
|
package/dist/outline/renderer.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Render an outline tree back to Markdown.
|
|
3
|
-
* Never renders internal keys, ids, or dedup suffixes.
|
|
4
|
-
*/
|
|
5
|
-
export function renderOutline(outline) {
|
|
6
|
-
const parts = [];
|
|
7
|
-
function renderNode(node) {
|
|
8
|
-
// Render heading
|
|
9
|
-
const hashes = "#".repeat(node.level);
|
|
10
|
-
parts.push(`${hashes} ${node.title}\n`);
|
|
11
|
-
// Render content
|
|
12
|
-
if (node.content_md && node.content_md.trim()) {
|
|
13
|
-
parts.push(node.content_md);
|
|
14
|
-
if (!node.content_md.endsWith("\n")) {
|
|
15
|
-
parts.push("\n");
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
// Render children recursively
|
|
19
|
-
node.children.forEach(renderNode);
|
|
20
|
-
}
|
|
21
|
-
outline.nodes.forEach(renderNode);
|
|
22
|
-
return parts.join("");
|
|
23
|
-
}
|
package/dist/parser.d.ts
DELETED
package/dist/parser.js
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { splitTokensPreservingQuotes, unquote } from "./util.js";
|
|
2
|
-
const HEADER_RE = /^\[\[(.+)\]\]\s*$/;
|
|
3
|
-
const META_RE = /^@([^:]+):\s*(.*)$/;
|
|
4
|
-
const PAYLOAD_DECL_RE = /^@payload:name:\s*(.+)\s*$/;
|
|
5
|
-
function parseHeader(inner) {
|
|
6
|
-
// Supports:
|
|
7
|
-
// 1) [[message role=user id=m1 ts=...]]
|
|
8
|
-
// 2) shorthand: [[user m1]] => type=message role=user id=m1
|
|
9
|
-
const tokens = splitTokensPreservingQuotes(inner.trim()).map(unquote);
|
|
10
|
-
if (tokens.length === 0)
|
|
11
|
-
return { type: "message" };
|
|
12
|
-
// shorthand: [[user m1]] or [[assistant m2]]
|
|
13
|
-
if (tokens.length === 2 && !tokens[0].includes("=") && !tokens[1].includes("=")) {
|
|
14
|
-
return { type: "message", role: tokens[0], id: tokens[1] };
|
|
15
|
-
}
|
|
16
|
-
const type = tokens[0];
|
|
17
|
-
const out = { type };
|
|
18
|
-
for (const t of tokens.slice(1)) {
|
|
19
|
-
const idx = t.indexOf("=");
|
|
20
|
-
if (idx <= 0)
|
|
21
|
-
continue;
|
|
22
|
-
const key = t.slice(0, idx).trim();
|
|
23
|
-
const val = unquote(t.slice(idx + 1).trim());
|
|
24
|
-
out[key] = val;
|
|
25
|
-
}
|
|
26
|
-
return out;
|
|
27
|
-
}
|
|
28
|
-
function parseMetaValue(key, value, arrayKeys, options) {
|
|
29
|
-
const v = value.trim();
|
|
30
|
-
// Handle array keys first
|
|
31
|
-
if (arrayKeys.has(key)) {
|
|
32
|
-
const parts = v
|
|
33
|
-
.split(",")
|
|
34
|
-
.map((p) => p.trim())
|
|
35
|
-
.filter((p) => p.length > 0);
|
|
36
|
-
return parts;
|
|
37
|
-
}
|
|
38
|
-
// Apply type mode
|
|
39
|
-
const mode = options.metaTypeMode ?? "strings";
|
|
40
|
-
if (mode === "schema" && options.metaSchema?.[key]) {
|
|
41
|
-
return parseWithSchema(v, options.metaSchema[key]);
|
|
42
|
-
}
|
|
43
|
-
if (mode === "infer") {
|
|
44
|
-
return inferType(v);
|
|
45
|
-
}
|
|
46
|
-
// Default: strings mode
|
|
47
|
-
return v;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Parse value according to schema type.
|
|
51
|
-
*/
|
|
52
|
-
function parseWithSchema(value, type) {
|
|
53
|
-
switch (type) {
|
|
54
|
-
case "boolean":
|
|
55
|
-
return value.toLowerCase() === "true";
|
|
56
|
-
case "null":
|
|
57
|
-
return null;
|
|
58
|
-
case "number": {
|
|
59
|
-
const num = Number(value);
|
|
60
|
-
return isNaN(num) ? value : num;
|
|
61
|
-
}
|
|
62
|
-
default:
|
|
63
|
-
return value;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Safely infer type from string value.
|
|
68
|
-
*/
|
|
69
|
-
function inferType(value) {
|
|
70
|
-
const lower = value.toLowerCase();
|
|
71
|
-
// Boolean
|
|
72
|
-
if (lower === "true")
|
|
73
|
-
return true;
|
|
74
|
-
if (lower === "false")
|
|
75
|
-
return false;
|
|
76
|
-
// Null
|
|
77
|
-
if (lower === "null")
|
|
78
|
-
return null;
|
|
79
|
-
// Number (avoid leading zeros like "0012" unless it's just "0" or "0.xxx")
|
|
80
|
-
if (/^-?\d+(\.\d+)?$/.test(value) && !/^0\d/.test(value)) {
|
|
81
|
-
const num = Number(value);
|
|
82
|
-
if (!isNaN(num))
|
|
83
|
-
return num;
|
|
84
|
-
}
|
|
85
|
-
return value;
|
|
86
|
-
}
|
|
87
|
-
function tryParsePayload(lang, raw) {
|
|
88
|
-
const l = (lang ?? "").toLowerCase();
|
|
89
|
-
if (l === "json") {
|
|
90
|
-
try {
|
|
91
|
-
const value = JSON.parse(raw);
|
|
92
|
-
return { lang, value, raw };
|
|
93
|
-
}
|
|
94
|
-
catch (e) {
|
|
95
|
-
return { lang, value: raw, raw, parseError: String(e?.message ?? e) };
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return { lang, value: raw, raw };
|
|
99
|
-
}
|
|
100
|
-
export function parseFlexMd(input, options = {}) {
|
|
101
|
-
const arrayKeys = new Set((options.arrayMetaKeys ?? ["tags", "refs"]).map((s) => s.trim()));
|
|
102
|
-
const lines = input.split("\n");
|
|
103
|
-
const endsWithNewline = input.endsWith("\n");
|
|
104
|
-
const frames = [];
|
|
105
|
-
let cur = null;
|
|
106
|
-
// body accumulator for current frame (excluding meta and payload blocks)
|
|
107
|
-
let bodyLines = [];
|
|
108
|
-
// payload state: if declared, next code fence becomes its payload
|
|
109
|
-
let pendingPayloadName = null;
|
|
110
|
-
function flushCurrent() {
|
|
111
|
-
if (!cur)
|
|
112
|
-
return;
|
|
113
|
-
const body = bodyLines.join("\n");
|
|
114
|
-
// Preserve original trailing newline behavior inside body (best-effort):
|
|
115
|
-
cur.body_md = body.length ? body + "\n" : "";
|
|
116
|
-
// Trim to empty if it's only whitespace/newlines
|
|
117
|
-
if (cur.body_md.trim().length === 0)
|
|
118
|
-
delete cur.body_md;
|
|
119
|
-
frames.push(cur);
|
|
120
|
-
cur = null;
|
|
121
|
-
bodyLines = [];
|
|
122
|
-
pendingPayloadName = null;
|
|
123
|
-
}
|
|
124
|
-
let i = 0;
|
|
125
|
-
while (i < lines.length) {
|
|
126
|
-
const line = lines[i];
|
|
127
|
-
// Frame header
|
|
128
|
-
const hm = line.match(HEADER_RE);
|
|
129
|
-
if (hm) {
|
|
130
|
-
flushCurrent();
|
|
131
|
-
cur = { ...parseHeader(hm[1]) };
|
|
132
|
-
i++;
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
// If we haven't seen a header yet, start an implicit frame.
|
|
136
|
-
cur ??= { type: "message" };
|
|
137
|
-
// Payload declaration
|
|
138
|
-
const pm = line.match(PAYLOAD_DECL_RE);
|
|
139
|
-
if (pm) {
|
|
140
|
-
pendingPayloadName = pm[1].trim();
|
|
141
|
-
i++;
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
// If a payload was declared, capture the next fenced code block
|
|
145
|
-
if (pendingPayloadName) {
|
|
146
|
-
const fenceStart = line.match(/^(```|~~~)\s*([A-Za-z0-9_-]+)?\s*$/);
|
|
147
|
-
if (fenceStart) {
|
|
148
|
-
const fence = fenceStart[1];
|
|
149
|
-
const lang = fenceStart[2]?.trim();
|
|
150
|
-
const rawLines = [];
|
|
151
|
-
i++;
|
|
152
|
-
while (i < lines.length && lines[i].trimEnd() !== fence) {
|
|
153
|
-
rawLines.push(lines[i]);
|
|
154
|
-
i++;
|
|
155
|
-
}
|
|
156
|
-
// consume closing fence if present
|
|
157
|
-
if (i < lines.length && lines[i].trimEnd() === fence)
|
|
158
|
-
i++;
|
|
159
|
-
const raw = rawLines.join("\n");
|
|
160
|
-
cur.payloads ??= {};
|
|
161
|
-
cur.payloads[pendingPayloadName] = tryParsePayload(lang, raw);
|
|
162
|
-
pendingPayloadName = null;
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
// payload declared but no fence; treat as body line
|
|
167
|
-
bodyLines.push(line);
|
|
168
|
-
pendingPayloadName = null;
|
|
169
|
-
i++;
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
// Metadata line
|
|
174
|
-
const mm = line.match(META_RE);
|
|
175
|
-
if (mm) {
|
|
176
|
-
const key = mm[1].trim();
|
|
177
|
-
const value = mm[2] ?? "";
|
|
178
|
-
cur.meta ??= {};
|
|
179
|
-
cur.meta[key] = parseMetaValue(key, value, arrayKeys, options);
|
|
180
|
-
i++;
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
// Default: part of body
|
|
184
|
-
bodyLines.push(line);
|
|
185
|
-
i++;
|
|
186
|
-
}
|
|
187
|
-
flushCurrent();
|
|
188
|
-
// Preserve overall trailing newline more closely: if input had no final newline,
|
|
189
|
-
// avoid forcing one by trimming last frame body newline.
|
|
190
|
-
if (!endsWithNewline && frames.length > 0) {
|
|
191
|
-
const last = frames[frames.length - 1];
|
|
192
|
-
if (typeof last.body_md === "string" && last.body_md.endsWith("\n")) {
|
|
193
|
-
last.body_md = last.body_md.slice(0, -1);
|
|
194
|
-
if (last.body_md.trim().length === 0)
|
|
195
|
-
delete last.body_md;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
return { frames };
|
|
199
|
-
}
|
package/dist/parsers/lists.d.ts
DELETED
package/dist/parsers/lists.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/parsers/tables.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
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[];
|
package/dist/parsers/tables.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/stringify.d.ts
DELETED
package/dist/stringify.js
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { isBlank, isEmptyObject, quoteIfNeeded } from "./util.js";
|
|
2
|
-
function metaValueToString(key, v, arrayKeys) {
|
|
3
|
-
if (v === null)
|
|
4
|
-
return null;
|
|
5
|
-
if (Array.isArray(v)) {
|
|
6
|
-
if (v.length === 0)
|
|
7
|
-
return null;
|
|
8
|
-
// Render arrays as comma-separated by default for known keys, otherwise JSON
|
|
9
|
-
if (arrayKeys.has(key))
|
|
10
|
-
return v.join(", ");
|
|
11
|
-
return JSON.stringify(v);
|
|
12
|
-
}
|
|
13
|
-
if (typeof v === "string")
|
|
14
|
-
return v;
|
|
15
|
-
if (typeof v === "number" || typeof v === "boolean")
|
|
16
|
-
return String(v);
|
|
17
|
-
// Fallback
|
|
18
|
-
return String(v);
|
|
19
|
-
}
|
|
20
|
-
function shouldSkipMetaValue(v) {
|
|
21
|
-
if (v === null || v === undefined)
|
|
22
|
-
return true;
|
|
23
|
-
if (typeof v === "string" && v.trim() === "")
|
|
24
|
-
return true;
|
|
25
|
-
if (Array.isArray(v) && v.length === 0)
|
|
26
|
-
return true;
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
function buildHeader(frame) {
|
|
30
|
-
// [[type key=value ...]]
|
|
31
|
-
const parts = [`[[${frame.type}`];
|
|
32
|
-
// include common attrs if present (role/id/ts) and any other top-level string attrs
|
|
33
|
-
const knownOrder = ["role", "id", "ts"];
|
|
34
|
-
for (const k of knownOrder) {
|
|
35
|
-
const val = frame[k];
|
|
36
|
-
if (typeof val === "string" && val.trim().length) {
|
|
37
|
-
parts.push(`${String(k)}=${quoteIfNeeded(val)}`);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
// If the user adds extra header-like attributes, you can support them by convention:
|
|
41
|
-
// we intentionally do NOT auto-include unknown keys to keep JSON schema clean.
|
|
42
|
-
return parts.join(" ") + "]]";
|
|
43
|
-
}
|
|
44
|
-
export function stringifyFlexMd(doc, options = {}) {
|
|
45
|
-
const skipEmpty = options.skipEmpty ?? true;
|
|
46
|
-
const fence = options.fence ?? "```";
|
|
47
|
-
const arrayKeys = new Set((options.arrayMetaKeys ?? ["tags", "refs"]).map((s) => s.trim()));
|
|
48
|
-
const out = [];
|
|
49
|
-
for (let idx = 0; idx < doc.frames.length; idx++) {
|
|
50
|
-
const frame = doc.frames[idx];
|
|
51
|
-
out.push(buildHeader(frame));
|
|
52
|
-
// META
|
|
53
|
-
if (!isEmptyObject(frame.meta)) {
|
|
54
|
-
const keys = Object.keys(frame.meta ?? {});
|
|
55
|
-
for (const k of keys) {
|
|
56
|
-
const v = frame.meta[k];
|
|
57
|
-
if (skipEmpty && shouldSkipMetaValue(v))
|
|
58
|
-
continue;
|
|
59
|
-
const rendered = metaValueToString(k, v, arrayKeys);
|
|
60
|
-
if (skipEmpty && (rendered === null || rendered.trim() === ""))
|
|
61
|
-
continue;
|
|
62
|
-
out.push(`@${k}: ${rendered ?? ""}`.trimEnd());
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
// BODY
|
|
66
|
-
if (typeof frame.body_md === "string") {
|
|
67
|
-
const body = frame.body_md;
|
|
68
|
-
if (!(skipEmpty && isBlank(body))) {
|
|
69
|
-
// Keep body verbatim, but avoid double-blanking: ensure we don't accidentally
|
|
70
|
-
// merge with next header by always preserving its internal newlines.
|
|
71
|
-
const normalized = body.endsWith("\n") ? body.slice(0, -1) : body;
|
|
72
|
-
if (normalized.length > 0)
|
|
73
|
-
out.push(normalized);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
// PAYLOADS
|
|
77
|
-
const payloads = frame.payloads ?? {};
|
|
78
|
-
const payloadNames = Object.keys(payloads);
|
|
79
|
-
if (!(skipEmpty && payloadNames.length === 0)) {
|
|
80
|
-
for (const name of payloadNames) {
|
|
81
|
-
const p = payloads[name];
|
|
82
|
-
// Skip empty payload if requested
|
|
83
|
-
if (skipEmpty &&
|
|
84
|
-
(p == null ||
|
|
85
|
-
(typeof p.raw === "string" && p.raw.trim() === "") ||
|
|
86
|
-
(p.value == null && (p.raw ?? "").trim() === ""))) {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
out.push(`@payload:name: ${name}`);
|
|
90
|
-
const lang = (p.lang ?? "").trim();
|
|
91
|
-
const header = lang ? `${fence}${lang}` : fence;
|
|
92
|
-
out.push(header);
|
|
93
|
-
// Prefer raw if present; if missing raw but value exists, serialize value
|
|
94
|
-
const raw = typeof p.raw === "string" && p.raw.length > 0
|
|
95
|
-
? p.raw
|
|
96
|
-
: p.value !== undefined
|
|
97
|
-
? typeof p.value === "string"
|
|
98
|
-
? p.value
|
|
99
|
-
: JSON.stringify(p.value, null, 2)
|
|
100
|
-
: "";
|
|
101
|
-
out.push(raw);
|
|
102
|
-
out.push(fence);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
// Frame separator: blank line between frames (readable) unless last
|
|
106
|
-
if (idx !== doc.frames.length - 1)
|
|
107
|
-
out.push("");
|
|
108
|
-
}
|
|
109
|
-
return out.join("\n") + "\n";
|
|
110
|
-
}
|