@contentbit/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,87 @@
1
+ export interface OutlineEntry {
2
+ /** ATX heading level, 1-6. */
3
+ level: number;
4
+ text: string;
5
+ /** 1-based source line. */
6
+ line: number;
7
+ /** Prose words from this heading (inclusive) until the next heading of any level. */
8
+ words: number;
9
+ }
10
+ export interface BlockInstance {
11
+ name: string;
12
+ /** 1-based line of the opening fence. */
13
+ line: number;
14
+ /** 1 for top-level blocks, +1 per nesting level. */
15
+ depth: number;
16
+ }
17
+ export interface LinkItem {
18
+ url: string;
19
+ text: string;
20
+ line: number;
21
+ external: boolean;
22
+ }
23
+ export interface DocumentStats {
24
+ file: {
25
+ path: string | null;
26
+ bytes: number;
27
+ lines: number;
28
+ };
29
+ frontmatter: {
30
+ present: boolean;
31
+ keys: string[];
32
+ data: Record<string, unknown>;
33
+ lines: {
34
+ start: number;
35
+ end: number;
36
+ } | null;
37
+ };
38
+ length: {
39
+ /** Prose words only: frontmatter, code, and markup syntax are excluded. */
40
+ words: number;
41
+ /** Characters of the full source, frontmatter included. */
42
+ characters: number;
43
+ /** ceil(words / 200). */
44
+ readingMinutes: number;
45
+ /** Rough heuristic: ceil(characters / 4). */
46
+ approxTokens: number;
47
+ };
48
+ outline: OutlineEntry[];
49
+ blocks: {
50
+ total: number;
51
+ byName: Record<string, number>;
52
+ maxDepth: number;
53
+ instances: BlockInstance[];
54
+ };
55
+ links: {
56
+ total: number;
57
+ external: number;
58
+ internal: number;
59
+ domains: string[];
60
+ items: LinkItem[];
61
+ };
62
+ images: {
63
+ total: number;
64
+ missingAlt: number;
65
+ };
66
+ code: {
67
+ fences: number;
68
+ languages: string[];
69
+ inlineSpans: number;
70
+ };
71
+ structure: {
72
+ listItems: number;
73
+ tables: number;
74
+ blockquotes: number;
75
+ };
76
+ }
77
+ export interface AnalyzeOptions {
78
+ /** Reported back in `file.path`; analyze never touches the filesystem. */
79
+ path?: string;
80
+ }
81
+ /**
82
+ * Scan a document and return structured stats: prose length, heading outline,
83
+ * block census, links, images, code, and structure counts. Purely syntactic
84
+ * and deterministic — no registry, no validation, no filesystem access.
85
+ */
86
+ export declare function analyzeDocument(source: string, options?: AnalyzeOptions): DocumentStats;
87
+ //# sourceMappingURL=analyze.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../src/analyze.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,YAAY;IAC3B,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IAC3D,WAAW,EAAE;QACX,OAAO,EAAE,OAAO,CAAA;QAChB,IAAI,EAAE,MAAM,EAAE,CAAA;QACd,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QAC7B,KAAK,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;KAC7C,CAAA;IACD,MAAM,EAAE;QACN,2EAA2E;QAC3E,KAAK,EAAE,MAAM,CAAA;QACb,2DAA2D;QAC3D,UAAU,EAAE,MAAM,CAAA;QAClB,yBAAyB;QACzB,cAAc,EAAE,MAAM,CAAA;QACtB,6CAA6C;QAC7C,YAAY,EAAE,MAAM,CAAA;KACrB,CAAA;IACD,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,MAAM,EAAE;QACN,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC9B,QAAQ,EAAE,MAAM,CAAA;QAChB,SAAS,EAAE,aAAa,EAAE,CAAA;KAC3B,CAAA;IACD,KAAK,EAAE;QACL,KAAK,EAAE,MAAM,CAAA;QACb,QAAQ,EAAE,MAAM,CAAA;QAChB,QAAQ,EAAE,MAAM,CAAA;QAChB,OAAO,EAAE,MAAM,EAAE,CAAA;QACjB,KAAK,EAAE,QAAQ,EAAE,CAAA;KAClB,CAAA;IACD,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAA;IAC7C,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,EAAE,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;IAClE,SAAS,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;CACtE;AAED,MAAM,WAAW,cAAc;IAC7B,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAsBD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,aAAa,CA4L3F"}
@@ -0,0 +1,204 @@
1
+ import { extractFrontmatter, stripFrontmatter } from './frontmatter.js';
2
+ import { parseDocument } from './parser.js';
3
+ /** UTF-8 byte length without Node Buffer or TextEncoder (core is environment-agnostic). */
4
+ function utf8Length(source) {
5
+ let bytes = 0;
6
+ for (const ch of source) {
7
+ const cp = ch.codePointAt(0);
8
+ bytes += cp <= 0x7f ? 1 : cp <= 0x7ff ? 2 : cp <= 0xffff ? 3 : 4;
9
+ }
10
+ return bytes;
11
+ }
12
+ const CODE_FENCE_RE = /^(`{3,}|~{3,})(.*)$/;
13
+ const HEADING_RE = /^(#{1,6})\s+(.*)$/;
14
+ const LIST_ITEM_RE = /^\s*(?:[-*+]|\d+[.)])\s+/;
15
+ const TABLE_SEPARATOR_RE = /^\|?[\s:|-]+$/;
16
+ const CODE_SPAN_RE = /`[^`]+`/g;
17
+ const IMAGE_RE = /!\[([^\]]*)\]\(([^)]*)\)/g;
18
+ const LINK_RE = /\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
19
+ const AUTOLINK_RE = /<(https?:\/\/[^>\s]+)>/g;
20
+ const EXTERNAL_URL_RE = /^(?:https?:)?\/\//i;
21
+ /**
22
+ * Scan a document and return structured stats: prose length, heading outline,
23
+ * block census, links, images, code, and structure counts. Purely syntactic
24
+ * and deterministic — no registry, no validation, no filesystem access.
25
+ */
26
+ export function analyzeDocument(source, options = {}) {
27
+ const frontmatter = extractFrontmatter(source);
28
+ const { document } = parseDocument(stripFrontmatter(source));
29
+ const outline = [];
30
+ const blocks = { total: 0, byName: {}, maxDepth: 0, instances: [] };
31
+ const links = {
32
+ total: 0,
33
+ external: 0,
34
+ internal: 0,
35
+ domains: [],
36
+ items: [],
37
+ };
38
+ const images = { total: 0, missingAlt: 0 };
39
+ const code = { fences: 0, languages: [], inlineSpans: 0 };
40
+ const structure = { listItems: 0, tables: 0, blockquotes: 0 };
41
+ const languages = new Set();
42
+ const domains = new Set();
43
+ let words = 0;
44
+ function addWords(count) {
45
+ words += count;
46
+ const section = outline[outline.length - 1];
47
+ if (section)
48
+ section.words += count;
49
+ }
50
+ function recordLink(url, text, line) {
51
+ const external = EXTERNAL_URL_RE.test(url);
52
+ links.total++;
53
+ if (external) {
54
+ links.external++;
55
+ const host = url.match(/^(?:https?:)?\/\/(?:[^/?#@]*@)?([^/?#:]+)/i);
56
+ if (host)
57
+ domains.add(host[1].toLowerCase());
58
+ }
59
+ else {
60
+ links.internal++;
61
+ }
62
+ links.items.push({ url, text, line, external });
63
+ }
64
+ /** Replace inline markup with its prose equivalent, recording links/images/spans. */
65
+ function inlineToProse(text, line) {
66
+ return text
67
+ .replace(CODE_SPAN_RE, () => {
68
+ code.inlineSpans++;
69
+ return ' ';
70
+ })
71
+ .replace(IMAGE_RE, (_, alt) => {
72
+ images.total++;
73
+ if (alt.trim() === '')
74
+ images.missingAlt++;
75
+ return ' ';
76
+ })
77
+ .replace(LINK_RE, (_, label, url) => {
78
+ recordLink(url, label, line);
79
+ return label;
80
+ })
81
+ .replace(AUTOLINK_RE, (_, url) => {
82
+ recordLink(url, url, line);
83
+ return ' ';
84
+ });
85
+ }
86
+ function countWords(text) {
87
+ return text.split(/\s+/).filter((token) => /[\p{L}\p{N}]/u.test(token)).length;
88
+ }
89
+ function scanMarkdown(value, startLine) {
90
+ let fence = null;
91
+ let inBlockquote = false;
92
+ let prevLineHasPipe = false;
93
+ const lines = value.split('\n');
94
+ for (let i = 0; i < lines.length; i++) {
95
+ const line = lines[i];
96
+ const lineNo = startLine + i;
97
+ const trimmed = line.trim();
98
+ const fenceMatch = trimmed.match(CODE_FENCE_RE);
99
+ if (fence !== null) {
100
+ if (fenceMatch && fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length) {
101
+ fence = null;
102
+ }
103
+ continue;
104
+ }
105
+ if (fenceMatch) {
106
+ fence = fenceMatch[1];
107
+ code.fences++;
108
+ const lang = fenceMatch[2].trim().split(/\s+/)[0];
109
+ if (lang)
110
+ languages.add(lang);
111
+ inBlockquote = false;
112
+ prevLineHasPipe = false;
113
+ continue;
114
+ }
115
+ if (trimmed === '') {
116
+ inBlockquote = false;
117
+ prevLineHasPipe = false;
118
+ continue;
119
+ }
120
+ const heading = trimmed.match(HEADING_RE);
121
+ if (heading) {
122
+ const raw = heading[2].replace(/\s+#+\s*$/, '');
123
+ const text = inlineToProse(raw, lineNo).replace(/\s+/g, ' ').trim();
124
+ outline.push({ level: heading[1].length, text, line: lineNo, words: 0 });
125
+ addWords(countWords(text));
126
+ inBlockquote = false;
127
+ prevLineHasPipe = false;
128
+ continue;
129
+ }
130
+ let content = trimmed;
131
+ if (content.startsWith('>')) {
132
+ if (!inBlockquote) {
133
+ structure.blockquotes++;
134
+ inBlockquote = true;
135
+ }
136
+ content = content.replace(/^(>\s*)+/, '');
137
+ }
138
+ else {
139
+ inBlockquote = false;
140
+ }
141
+ if (LIST_ITEM_RE.test(content)) {
142
+ structure.listItems++;
143
+ content = content.replace(LIST_ITEM_RE, '');
144
+ }
145
+ if (content.includes('|')) {
146
+ if (TABLE_SEPARATOR_RE.test(content) && content.includes('-') && prevLineHasPipe) {
147
+ structure.tables++;
148
+ }
149
+ prevLineHasPipe = true;
150
+ content = inlineToProse(content, lineNo).replaceAll('|', ' ');
151
+ }
152
+ else {
153
+ prevLineHasPipe = false;
154
+ content = inlineToProse(content, lineNo);
155
+ }
156
+ addWords(countWords(content));
157
+ }
158
+ }
159
+ function walk(nodes, depth) {
160
+ for (const node of nodes) {
161
+ if (node.type === 'block') {
162
+ blocks.total++;
163
+ blocks.byName[node.name] = (blocks.byName[node.name] ?? 0) + 1;
164
+ blocks.maxDepth = Math.max(blocks.maxDepth, depth);
165
+ blocks.instances.push({ name: node.name, line: node.openPosition.start.line, depth });
166
+ walk(node.children, depth + 1);
167
+ }
168
+ else {
169
+ scanMarkdown(node.value, node.position.start.line);
170
+ }
171
+ }
172
+ }
173
+ walk(document.children, 1);
174
+ code.languages = [...languages];
175
+ links.domains = [...domains].sort();
176
+ const sourceLines = source === '' ? 0 : source.split('\n').length - (source.endsWith('\n') ? 1 : 0);
177
+ return {
178
+ file: {
179
+ path: options.path ?? null,
180
+ bytes: utf8Length(source),
181
+ lines: sourceLines,
182
+ },
183
+ frontmatter: frontmatter
184
+ ? {
185
+ present: true,
186
+ keys: frontmatter.keys,
187
+ data: frontmatter.data,
188
+ lines: frontmatter.lines,
189
+ }
190
+ : { present: false, keys: [], data: {}, lines: null },
191
+ length: {
192
+ words,
193
+ characters: source.length,
194
+ readingMinutes: Math.ceil(words / 200),
195
+ approxTokens: Math.ceil(source.length / 4),
196
+ },
197
+ outline,
198
+ blocks,
199
+ links,
200
+ images,
201
+ code,
202
+ structure,
203
+ };
204
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"authoring.d.ts","sourceRoot":"","sources":["../src/authoring.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAEpD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,KAAK,GAAG,OAAO,CAAA;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAWD,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,EAChC,IAAI,GAAE,qBAA0B,GAC/B,MAAM,CA0BR"}
1
+ {"version":3,"file":"authoring.d.ts","sourceRoot":"","sources":["../src/authoring.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAkEpD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,KAAK,GAAG,OAAO,CAAA;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAWD,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,EAChC,IAAI,GAAE,qBAA0B,GAC/B,MAAM,CA8BR"}
package/dist/authoring.js CHANGED
@@ -1,3 +1,50 @@
1
+ function typeLabel(def) {
2
+ switch (def.type) {
3
+ case 'enum':
4
+ return `one of ${Object.values(def.entries ?? {}).join('|')}`;
5
+ case 'literal':
6
+ return `one of ${(def.values ?? []).map(String).join('|')}`;
7
+ case 'union':
8
+ return `one of ${(def.options ?? [])
9
+ .map((o) => typeLabel(o.def).replace(/^one of /, ''))
10
+ .join('|')}`;
11
+ default:
12
+ return def.type;
13
+ }
14
+ }
15
+ /** One `- name: type (required|optional[, default: x]) [— description]` line per prop. */
16
+ function describeProps(schema) {
17
+ const root = schema.def;
18
+ if (root.type !== 'object' || !root.shape)
19
+ return [];
20
+ const lines = [];
21
+ for (const [name, field] of Object.entries(root.shape)) {
22
+ // Unwrap optional/default/nullable wrappers; .describe() may sit on any layer.
23
+ let current = field;
24
+ let optional = false;
25
+ let defaultValue;
26
+ let description = current.description;
27
+ while (['optional', 'default', 'nullable'].includes(current.def.type)) {
28
+ optional = true;
29
+ if (current.def.type === 'default') {
30
+ const dv = current.def.defaultValue;
31
+ defaultValue = typeof dv === 'function' ? dv() : dv;
32
+ }
33
+ if (!current.def.innerType)
34
+ break;
35
+ current = current.def.innerType;
36
+ description ??= current.description;
37
+ }
38
+ const presence = defaultValue !== undefined
39
+ ? `(optional, default: ${String(defaultValue)})`
40
+ : optional
41
+ ? '(optional)'
42
+ : '(required)';
43
+ const suffix = description ? ` — ${description}` : '';
44
+ lines.push(`- ${name}: ${typeLabel(current.def)} ${presence}${suffix}`);
45
+ }
46
+ return lines;
47
+ }
1
48
  const LLM_PREAMBLE = `# Content block authoring rules
2
49
 
3
50
  - write regular Markdown by default; use blocks only when they improve scanning or structure
@@ -17,6 +64,11 @@ export function generateAuthoringGuide(defs, opts = {}) {
17
64
  lines.push(def.childOnly
18
65
  ? `${def.description} (child block — only inside a parent that allows it)`
19
66
  : def.description);
67
+ if (def.props) {
68
+ const props = describeProps(def.props);
69
+ if (props.length > 0)
70
+ lines.push('', 'Props:', ...props);
71
+ }
20
72
  lines.push('', `Content: ${def.content.describe()}`);
21
73
  if (def.authoring.useWhen.length > 0) {
22
74
  lines.push('', 'Use when:', ...def.authoring.useWhen.map((u) => `- ${u}`));
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Blank out a leading YAML frontmatter block so block syntax inside it is
3
+ * never parsed as content. Every line is replaced by an empty line (the
4
+ * newlines stay), so diagnostic positions in the remaining document are
5
+ * unchanged. Sources without frontmatter are returned as-is.
6
+ */
7
+ export declare function stripFrontmatter(source: string): string;
8
+ export interface Frontmatter {
9
+ /** Raw YAML between the fences (fences excluded). */
10
+ raw: string;
11
+ /** Best-effort parsed data; values the subset parser can't handle stay raw strings. */
12
+ data: Record<string, unknown>;
13
+ /** Top-level keys in document order. */
14
+ keys: string[];
15
+ /** 1-based fence lines, inclusive. */
16
+ lines: {
17
+ start: number;
18
+ end: number;
19
+ };
20
+ }
21
+ /**
22
+ * Extract and parse a leading YAML frontmatter block. Parsing covers the flat
23
+ * subset real content frontmatter uses: `key: value` scalars (quoted/plain
24
+ * strings, numbers, booleans, null), inline arrays, dash lists, and block
25
+ * scalars. Nested mappings and anything else fall back to the raw text of the
26
+ * value, so no input ever throws. Returns null when there is no frontmatter.
27
+ */
28
+ export declare function extractFrontmatter(source: string): Frontmatter | null;
29
+ //# sourceMappingURL=frontmatter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../src/frontmatter.ts"],"names":[],"mappings":"AAMA;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAIvD;AAED,MAAM,WAAW,WAAW;IAC1B,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAA;IACX,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7B,wCAAwC;IACxC,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,sCAAsC;IACtC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CACtC;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAOrE"}
@@ -0,0 +1,136 @@
1
+ // A YAML frontmatter block at the very start of the file: an opening ---
2
+ // fence, an optional body (empty frontmatter is legal), and a closing ---
3
+ // fence. Both fences tolerate trailing spaces/tabs, which editors and CMS
4
+ // exports routinely leave behind.
5
+ const FM_RE = /^---[ \t]*\r?\n(?:[\s\S]*?\r?\n)?---[ \t]*(?:\r?\n|$)/;
6
+ /**
7
+ * Blank out a leading YAML frontmatter block so block syntax inside it is
8
+ * never parsed as content. Every line is replaced by an empty line (the
9
+ * newlines stay), so diagnostic positions in the remaining document are
10
+ * unchanged. Sources without frontmatter are returned as-is.
11
+ */
12
+ export function stripFrontmatter(source) {
13
+ const match = FM_RE.exec(source);
14
+ if (!match)
15
+ return source;
16
+ return match[0].replace(/[^\n]+/g, '') + source.slice(match[0].length);
17
+ }
18
+ /**
19
+ * Extract and parse a leading YAML frontmatter block. Parsing covers the flat
20
+ * subset real content frontmatter uses: `key: value` scalars (quoted/plain
21
+ * strings, numbers, booleans, null), inline arrays, dash lists, and block
22
+ * scalars. Nested mappings and anything else fall back to the raw text of the
23
+ * value, so no input ever throws. Returns null when there is no frontmatter.
24
+ */
25
+ export function extractFrontmatter(source) {
26
+ const match = source.match(FM_RE);
27
+ if (!match)
28
+ return null;
29
+ const blockLines = match[0].replace(/\r?\n$/, '').split(/\r?\n/);
30
+ const inner = blockLines.slice(1, -1);
31
+ const { data, keys } = parseYamlSubset(inner);
32
+ return { raw: inner.join('\n'), data, keys, lines: { start: 1, end: blockLines.length } };
33
+ }
34
+ const KEY_RE = /^([A-Za-z0-9_.-]+):(.*)$/;
35
+ function parseYamlSubset(lines) {
36
+ const data = {};
37
+ const keys = [];
38
+ let i = 0;
39
+ while (i < lines.length) {
40
+ const line = lines[i];
41
+ const trimmed = line.trim();
42
+ if (trimmed === '' || trimmed.startsWith('#') || /^[ \t]/.test(line)) {
43
+ i++;
44
+ continue;
45
+ }
46
+ const m = line.match(KEY_RE);
47
+ if (!m) {
48
+ i++;
49
+ continue;
50
+ }
51
+ const [, key, rawValue] = m;
52
+ const value = rawValue.trim();
53
+ i++;
54
+ const indented = [];
55
+ while (i < lines.length && /^[ \t]/.test(lines[i]) && lines[i].trim() !== '') {
56
+ indented.push(lines[i]);
57
+ i++;
58
+ }
59
+ keys.push(key);
60
+ data[key] = parseValue(value, indented);
61
+ }
62
+ return { data, keys };
63
+ }
64
+ function parseValue(value, indented) {
65
+ if (/^[|>][+-]?$/.test(value))
66
+ return dedent(indented).join('\n');
67
+ if (value === '') {
68
+ if (indented.length === 0)
69
+ return null;
70
+ const items = dedent(indented);
71
+ if (items.every((l) => l.startsWith('- ')))
72
+ return items.map((l) => parseScalar(l.slice(2).trim()));
73
+ return items.join('\n');
74
+ }
75
+ return parseScalar(value);
76
+ }
77
+ function dedent(lines) {
78
+ const indent = Math.min(...lines.map((l) => l.match(/^[ \t]*/)[0].length));
79
+ return lines.map((l) => l.slice(indent));
80
+ }
81
+ function parseScalar(value) {
82
+ if (value === '' || value === 'null' || value === '~')
83
+ return null;
84
+ if (value === 'true')
85
+ return true;
86
+ if (value === 'false')
87
+ return false;
88
+ if (/^[+-]?\d+$/.test(value))
89
+ return Number.parseInt(value, 10);
90
+ if (/^[+-]?\d*\.\d+$/.test(value))
91
+ return Number.parseFloat(value);
92
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
93
+ try {
94
+ return JSON.parse(value);
95
+ }
96
+ catch {
97
+ return value.slice(1, -1);
98
+ }
99
+ }
100
+ if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
101
+ return value.slice(1, -1).replace(/''/g, "'");
102
+ }
103
+ if (value.startsWith('[') && value.endsWith(']')) {
104
+ const inner = value.slice(1, -1).trim();
105
+ if (inner === '')
106
+ return [];
107
+ return splitInlineItems(inner).map((item) => parseScalar(item.trim()));
108
+ }
109
+ return value;
110
+ }
111
+ /** Split inline array items on top-level commas, respecting quotes. */
112
+ function splitInlineItems(inner) {
113
+ const items = [];
114
+ let current = '';
115
+ let quote = null;
116
+ for (const ch of inner) {
117
+ if (quote) {
118
+ current += ch;
119
+ if (ch === quote)
120
+ quote = null;
121
+ }
122
+ else if (ch === '"' || ch === "'") {
123
+ current += ch;
124
+ quote = ch;
125
+ }
126
+ else if (ch === ',') {
127
+ items.push(current);
128
+ current = '';
129
+ }
130
+ else {
131
+ current += ch;
132
+ }
133
+ }
134
+ items.push(current);
135
+ return items;
136
+ }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@ export type { SourcePoint, SourceRange, Severity, Diagnostic } from './diagnosti
3
3
  export { formatDiagnostic } from './diagnostics.js';
4
4
  export type { DocumentNode, ContentNode, MarkdownNode, BlockNode } from './ast.js';
5
5
  export { parseDocument, type ParseResult } from './parser.js';
6
+ export { stripFrontmatter, extractFrontmatter, type Frontmatter } from './frontmatter.js';
7
+ export { analyzeDocument, type AnalyzeOptions, type DocumentStats, type OutlineEntry, type BlockInstance, type LinkItem, } from './analyze.js';
6
8
  export { parseProps } from './props.js';
7
9
  export { bodyLineRange } from './position.js';
8
10
  export { createBlockRegistry, defineBlock, BlockRegistry, type BlockDefinition, type ContentModel, type AuthoringMeta, type Report, } from './registry.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACtF,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACnD,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAClF,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAA;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,aAAa,EACb,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,MAAM,GACZ,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,eAAe,GACrB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,sBAAsB,EAAE,KAAK,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AACnF,OAAO,EACL,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,GAC7B,MAAM,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACtF,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACnD,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAClF,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAA;AAC7D,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACzF,OAAO,EACL,eAAe,EACf,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,QAAQ,GACd,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,aAAa,EACb,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,MAAM,GACZ,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,eAAe,GACrB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,sBAAsB,EAAE,KAAK,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AACnF,OAAO,EACL,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,GAC7B,MAAM,sBAAsB,CAAA"}
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  export { VERSION } from './version.js';
2
2
  export { formatDiagnostic } from './diagnostics.js';
3
3
  export { parseDocument } from './parser.js';
4
+ export { stripFrontmatter, extractFrontmatter } from './frontmatter.js';
5
+ export { analyzeDocument, } from './analyze.js';
4
6
  export { parseProps } from './props.js';
5
7
  export { bodyLineRange } from './position.js';
6
8
  export { createBlockRegistry, defineBlock, BlockRegistry, } from './registry.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentbit/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Parser, validator, and registry for Content Blocks: structured Markdown components without framework lock-in.",
5
5
  "keywords": [
6
6
  "ast",