sdocs 0.0.39 → 0.0.41

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/CHANGELOG.md CHANGED
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.0.41] - 2026-07-04
11
+
12
+ ### Added
13
+
14
+ - **`sdocs/language`** — a scanner and parser for the block-based sdoc
15
+ format: line-anchored `[DOCS]`/`[PAGE]`/`[LAYOUT]` entities with
16
+ `[preview]`/`[example]` sub-blocks, Svelte-style attributes, precise
17
+ source spans, and recoverable diagnostics (it never throws, and keeps
18
+ scanning past mistakes). Groundwork for the format switch — the runtime
19
+ still reads the current `.sdoc` format.
20
+ - **A TextMate grammar for sdoc** at `sdocs/grammar/sdoc.tmLanguage.json`,
21
+ usable by any TextMate-compatible highlighter; the documentation site
22
+ uses it to highlight ```` ```sdoc ```` fences.
23
+
24
+ ## [0.0.40] - 2026-07-04
25
+
26
+ ### Fixed
27
+
28
+ - **Wrapped previews bind to the documented component.** When a preview
29
+ wraps the documented component in another one (a `<Tab>` shown inside
30
+ `<Tabs>`), method calls and live state used to attach to the outer
31
+ wrapper — the first capitalized tag in the snippet. The preview ref now
32
+ binds to the doc's own component wherever it appears in the snippet,
33
+ falling back to the first capitalized tag when it's absent.
34
+ - **Usage-code patching matches the component name exactly.** Attribute
35
+ updates from the controls located the root tag with a prefix search, so
36
+ a component like `Tab` could patch a sibling `<Tabs>` tag instead of its
37
+ own. The tag search now requires a whole-name match.
38
+
10
39
  ## [0.0.39] - 2026-07-04
11
40
 
12
41
  ### Changed
@@ -106,8 +106,11 @@
106
106
  if (changes.length === 0) return snippetBody;
107
107
 
108
108
  // Find the opening tag: <ComponentName ...> or <ComponentName ... />
109
- const tagStart = snippetBody.indexOf(`<${name}`);
110
- if (tagStart === -1) return snippetBody;
109
+ // (whole-name match, so <Tab doesn't hit <Tabs)
110
+ const escapedName = name.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
111
+ const tagMatch = snippetBody.match(new RegExp(`<${escapedName}(?=[\\s/>])`));
112
+ if (!tagMatch || tagMatch.index === undefined) return snippetBody;
113
+ const tagStart = tagMatch.index;
111
114
 
112
115
  // Find the end of the opening tag, respecting {} expressions
113
116
  let braceDepth = 0;
@@ -0,0 +1,245 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
3
+ "name": "sdoc",
4
+ "scopeName": "source.sdoc",
5
+ "fileTypes": ["sdoc"],
6
+ "patterns": [
7
+ { "include": "#comment" },
8
+ { "include": "#script-ts" },
9
+ { "include": "#script-js" },
10
+ { "include": "#style" },
11
+ { "include": "#docs" },
12
+ { "include": "#page" },
13
+ { "include": "#layout" }
14
+ ],
15
+ "repository": {
16
+ "comment": {
17
+ "name": "comment.block.html.sdoc",
18
+ "begin": "<!--",
19
+ "end": "-->"
20
+ },
21
+ "script-ts": {
22
+ "begin": "^\\s*(<)(script)(?=[^>]*lang=([\"'])ts\\3)",
23
+ "beginCaptures": {
24
+ "1": { "name": "punctuation.definition.tag.begin.sdoc" },
25
+ "2": { "name": "entity.name.tag.script.sdoc" }
26
+ },
27
+ "end": "(</)(script)\\s*(>)",
28
+ "endCaptures": {
29
+ "1": { "name": "punctuation.definition.tag.begin.sdoc" },
30
+ "2": { "name": "entity.name.tag.script.sdoc" },
31
+ "3": { "name": "punctuation.definition.tag.end.sdoc" }
32
+ },
33
+ "patterns": [
34
+ { "include": "#tag-rest" },
35
+ {
36
+ "begin": "(?<=>)",
37
+ "end": "(?=</script)",
38
+ "contentName": "source.ts",
39
+ "patterns": [{ "include": "source.ts" }]
40
+ }
41
+ ]
42
+ },
43
+ "script-js": {
44
+ "begin": "^\\s*(<)(script)(?![^>]*lang=([\"'])ts\\3)(?=[\\s>])",
45
+ "beginCaptures": {
46
+ "1": { "name": "punctuation.definition.tag.begin.sdoc" },
47
+ "2": { "name": "entity.name.tag.script.sdoc" }
48
+ },
49
+ "end": "(</)(script)\\s*(>)",
50
+ "endCaptures": {
51
+ "1": { "name": "punctuation.definition.tag.begin.sdoc" },
52
+ "2": { "name": "entity.name.tag.script.sdoc" },
53
+ "3": { "name": "punctuation.definition.tag.end.sdoc" }
54
+ },
55
+ "patterns": [
56
+ { "include": "#tag-rest" },
57
+ {
58
+ "begin": "(?<=>)",
59
+ "end": "(?=</script)",
60
+ "contentName": "source.js",
61
+ "patterns": [{ "include": "source.js" }]
62
+ }
63
+ ]
64
+ },
65
+ "style": {
66
+ "begin": "^\\s*(<)(style)(?=[\\s>])",
67
+ "beginCaptures": {
68
+ "1": { "name": "punctuation.definition.tag.begin.sdoc" },
69
+ "2": { "name": "entity.name.tag.style.sdoc" }
70
+ },
71
+ "end": "(</)(style)\\s*(>)",
72
+ "endCaptures": {
73
+ "1": { "name": "punctuation.definition.tag.begin.sdoc" },
74
+ "2": { "name": "entity.name.tag.style.sdoc" },
75
+ "3": { "name": "punctuation.definition.tag.end.sdoc" }
76
+ },
77
+ "patterns": [
78
+ { "include": "#tag-rest" },
79
+ {
80
+ "begin": "(?<=>)",
81
+ "end": "(?=</style)",
82
+ "contentName": "source.css",
83
+ "patterns": [{ "include": "source.css" }]
84
+ }
85
+ ]
86
+ },
87
+ "tag-rest": {
88
+ "begin": "\\G",
89
+ "end": "(>)",
90
+ "endCaptures": {
91
+ "1": { "name": "punctuation.definition.tag.end.sdoc" }
92
+ },
93
+ "patterns": [{ "include": "#attributes" }]
94
+ },
95
+ "docs": {
96
+ "name": "meta.block.docs.sdoc",
97
+ "begin": "^\\s*(\\[)(DOCS)\\b",
98
+ "beginCaptures": {
99
+ "1": { "name": "punctuation.definition.tag.begin.sdoc" },
100
+ "2": { "name": "keyword.control.entity.sdoc" }
101
+ },
102
+ "end": "^\\s*(\\[/)(DOCS)(\\])\\s*$",
103
+ "endCaptures": {
104
+ "1": { "name": "punctuation.definition.tag.begin.sdoc" },
105
+ "2": { "name": "keyword.control.entity.sdoc" },
106
+ "3": { "name": "punctuation.definition.tag.end.sdoc" }
107
+ },
108
+ "patterns": [
109
+ { "include": "#opener-attrs" },
110
+ { "include": "#comment" },
111
+ { "include": "#preview" },
112
+ { "include": "#example" }
113
+ ]
114
+ },
115
+ "page": {
116
+ "name": "meta.block.page.sdoc",
117
+ "begin": "^\\s*(\\[)(PAGE)\\b",
118
+ "beginCaptures": {
119
+ "1": { "name": "punctuation.definition.tag.begin.sdoc" },
120
+ "2": { "name": "keyword.control.entity.sdoc" }
121
+ },
122
+ "end": "^\\s*(\\[/)(PAGE)(\\])\\s*$",
123
+ "endCaptures": {
124
+ "1": { "name": "punctuation.definition.tag.begin.sdoc" },
125
+ "2": { "name": "keyword.control.entity.sdoc" },
126
+ "3": { "name": "punctuation.definition.tag.end.sdoc" }
127
+ },
128
+ "patterns": [
129
+ { "include": "#opener-attrs" },
130
+ { "include": "source.svelte" }
131
+ ]
132
+ },
133
+ "layout": {
134
+ "name": "meta.block.layout.sdoc",
135
+ "begin": "^\\s*(\\[)(LAYOUT)\\b",
136
+ "beginCaptures": {
137
+ "1": { "name": "punctuation.definition.tag.begin.sdoc" },
138
+ "2": { "name": "keyword.control.entity.sdoc" }
139
+ },
140
+ "end": "^\\s*(\\[/)(LAYOUT)(\\])\\s*$",
141
+ "endCaptures": {
142
+ "1": { "name": "punctuation.definition.tag.begin.sdoc" },
143
+ "2": { "name": "keyword.control.entity.sdoc" },
144
+ "3": { "name": "punctuation.definition.tag.end.sdoc" }
145
+ },
146
+ "patterns": [
147
+ { "include": "#opener-attrs" },
148
+ { "include": "source.svelte" }
149
+ ]
150
+ },
151
+ "preview": {
152
+ "name": "meta.block.preview.sdoc",
153
+ "begin": "^\\s*(\\[)(preview)\\b",
154
+ "beginCaptures": {
155
+ "1": { "name": "punctuation.definition.tag.begin.sdoc" },
156
+ "2": { "name": "entity.name.tag.sdoc" }
157
+ },
158
+ "end": "^\\s*(\\[/)(preview)(\\])\\s*$",
159
+ "endCaptures": {
160
+ "1": { "name": "punctuation.definition.tag.begin.sdoc" },
161
+ "2": { "name": "entity.name.tag.sdoc" },
162
+ "3": { "name": "punctuation.definition.tag.end.sdoc" }
163
+ },
164
+ "patterns": [
165
+ { "include": "#opener-attrs" },
166
+ { "include": "source.svelte" }
167
+ ]
168
+ },
169
+ "example": {
170
+ "name": "meta.block.example.sdoc",
171
+ "begin": "^\\s*(\\[)(example)\\b",
172
+ "beginCaptures": {
173
+ "1": { "name": "punctuation.definition.tag.begin.sdoc" },
174
+ "2": { "name": "entity.name.tag.sdoc" }
175
+ },
176
+ "end": "^\\s*(\\[/)(example)(\\])\\s*$",
177
+ "endCaptures": {
178
+ "1": { "name": "punctuation.definition.tag.begin.sdoc" },
179
+ "2": { "name": "entity.name.tag.sdoc" },
180
+ "3": { "name": "punctuation.definition.tag.end.sdoc" }
181
+ },
182
+ "patterns": [
183
+ { "include": "#opener-attrs" },
184
+ { "include": "source.svelte" }
185
+ ]
186
+ },
187
+ "opener-attrs": {
188
+ "begin": "\\G",
189
+ "end": "(\\])",
190
+ "endCaptures": {
191
+ "1": { "name": "punctuation.definition.tag.end.sdoc" }
192
+ },
193
+ "patterns": [{ "include": "#attributes" }]
194
+ },
195
+ "attributes": {
196
+ "patterns": [
197
+ {
198
+ "match": "([A-Za-z_][\\w-]*)\\s*(=)",
199
+ "captures": {
200
+ "1": { "name": "entity.other.attribute-name.sdoc" },
201
+ "2": { "name": "punctuation.separator.key-value.sdoc" }
202
+ }
203
+ },
204
+ {
205
+ "match": "([A-Za-z_][\\w-]*)",
206
+ "name": "entity.other.attribute-name.sdoc"
207
+ },
208
+ {
209
+ "name": "string.quoted.double.sdoc",
210
+ "begin": "\"",
211
+ "end": "\"",
212
+ "beginCaptures": {
213
+ "0": { "name": "punctuation.definition.string.begin.sdoc" }
214
+ },
215
+ "endCaptures": {
216
+ "0": { "name": "punctuation.definition.string.end.sdoc" }
217
+ }
218
+ },
219
+ { "include": "#expression" }
220
+ ]
221
+ },
222
+ "expression": {
223
+ "name": "meta.embedded.expression.sdoc",
224
+ "begin": "\\{",
225
+ "end": "\\}",
226
+ "beginCaptures": {
227
+ "0": { "name": "punctuation.section.embedded.begin.sdoc" }
228
+ },
229
+ "endCaptures": {
230
+ "0": { "name": "punctuation.section.embedded.end.sdoc" }
231
+ },
232
+ "patterns": [{ "include": "#expression-inner" }]
233
+ },
234
+ "expression-inner": {
235
+ "patterns": [
236
+ {
237
+ "begin": "\\{",
238
+ "end": "\\}",
239
+ "patterns": [{ "include": "#expression-inner" }]
240
+ },
241
+ { "include": "source.ts" }
242
+ ]
243
+ }
244
+ }
245
+ }
@@ -0,0 +1,2 @@
1
+ export { scanSdoc, offsetToPosition, ENTITY_KINDS, SUB_BLOCK_KINDS, type Span, type EntityKind, type SubBlockKind, type AttrValue, type Attrs, type SubBlock, type Entity, type TagBlock, type ScanError, type SdocFile, } from './scanner.js';
2
+ export { parseSdoc, parseArgsLiteral, slugifyTitle, normalizeBody, type ArgValue, type PreviewBlock, type ExampleBlock, type DocsEntity, type PageEntity, type LayoutEntity, type SdocEntity, type SdocDocument, } from './parser.js';
@@ -0,0 +1,2 @@
1
+ export { scanSdoc, offsetToPosition, ENTITY_KINDS, SUB_BLOCK_KINDS, } from './scanner.js';
2
+ export { parseSdoc, parseArgsLiteral, slugifyTitle, normalizeBody, } from './parser.js';
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Semantic layer over the syntactic scanner: validates attributes against
3
+ * the sdoc language rules and produces typed entities ready for the Vite
4
+ * plugin, the CLI app-gen, and the editor tooling.
5
+ */
6
+ import { type ScanError, type Span, type TagBlock } from './scanner.js';
7
+ export type ArgValue = string | number | boolean;
8
+ export interface PreviewBlock {
9
+ /** The identifier from component={X}; null when invalid/missing (already reported) */
10
+ componentName: string | null;
11
+ /** Parsed args literal; null when absent or invalid */
12
+ args: Record<string, ArgValue> | null;
13
+ /** Exact source of the args expression (inner object), for code display */
14
+ argsRaw: string | null;
15
+ /** Explicit title="…" override, when present */
16
+ title: string | null;
17
+ /** Tab label: the title override or the component name */
18
+ label: string;
19
+ body: string;
20
+ bodySpan: Span;
21
+ span: Span;
22
+ }
23
+ export interface ExampleBlock {
24
+ title: string;
25
+ body: string;
26
+ bodySpan: Span;
27
+ span: Span;
28
+ }
29
+ export interface DocsEntity {
30
+ kind: 'DOCS';
31
+ title: string;
32
+ slug: string;
33
+ description: string | null;
34
+ previews: PreviewBlock[];
35
+ examples: ExampleBlock[];
36
+ openerSpan: Span;
37
+ span: Span;
38
+ }
39
+ export interface PageEntity {
40
+ kind: 'PAGE';
41
+ title: string;
42
+ slug: string;
43
+ body: string;
44
+ bodySpan: Span;
45
+ openerSpan: Span;
46
+ span: Span;
47
+ }
48
+ export interface LayoutEntity {
49
+ kind: 'LAYOUT';
50
+ title: string;
51
+ slug: string;
52
+ padding: string | null;
53
+ body: string;
54
+ bodySpan: Span;
55
+ openerSpan: Span;
56
+ span: Span;
57
+ }
58
+ export type SdocEntity = DocsEntity | PageEntity | LayoutEntity;
59
+ export interface SdocDocument {
60
+ script: TagBlock | null;
61
+ style: TagBlock | null;
62
+ entities: SdocEntity[];
63
+ diagnostics: ScanError[];
64
+ source: string;
65
+ }
66
+ /**
67
+ * Normalize a raw block body for consumption: strip the common leading
68
+ * indentation (bodies sit one level deep inside their block), drop a
69
+ * trailing carriage return per line, and unescape lines that start with
70
+ * `\[` (the escape for body lines that would otherwise read as tags).
71
+ * The scanner keeps the raw text and spans; this is the display/runtime form.
72
+ */
73
+ export declare function normalizeBody(raw: string): string;
74
+ /** Slug used for entity addressing: relPath + '#' + slug. */
75
+ export declare function slugifyTitle(title: string): string;
76
+ /**
77
+ * Parse a preview args expression: a flat object literal whose values are
78
+ * plain literals (strings, numbers, booleans). Anything richer belongs in
79
+ * the block body, so it is rejected here.
80
+ */
81
+ export declare function parseArgsLiteral(raw: string, span: Span, diagnostics: ScanError[]): Record<string, ArgValue> | null;
82
+ export declare function parseSdoc(source: string): SdocDocument;
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Semantic layer over the syntactic scanner: validates attributes against
3
+ * the sdoc language rules and produces typed entities ready for the Vite
4
+ * plugin, the CLI app-gen, and the editor tooling.
5
+ */
6
+ import { scanSdoc, } from './scanner.js';
7
+ /**
8
+ * Normalize a raw block body for consumption: strip the common leading
9
+ * indentation (bodies sit one level deep inside their block), drop a
10
+ * trailing carriage return per line, and unescape lines that start with
11
+ * `\[` (the escape for body lines that would otherwise read as tags).
12
+ * The scanner keeps the raw text and spans; this is the display/runtime form.
13
+ */
14
+ export function normalizeBody(raw) {
15
+ const lines = raw.split('\n').map((line) => line.replace(/\r$/, ''));
16
+ let common = null;
17
+ for (const line of lines) {
18
+ if (line.trim() === '')
19
+ continue;
20
+ const indent = line.match(/^[ \t]*/)[0];
21
+ if (common === null) {
22
+ common = indent;
23
+ }
24
+ else {
25
+ let k = 0;
26
+ while (k < common.length && k < indent.length && common[k] === indent[k])
27
+ k++;
28
+ common = common.slice(0, k);
29
+ }
30
+ }
31
+ const cut = common?.length ?? 0;
32
+ return lines
33
+ .map((line) => (line.trim() === '' ? '' : line.slice(cut)))
34
+ .map((line) => (line.startsWith('\\[') ? line.slice(1) : line))
35
+ .join('\n')
36
+ .replace(/^\n+|\n+$/g, '');
37
+ }
38
+ /** Slug used for entity addressing: relPath + '#' + slug. */
39
+ export function slugifyTitle(title) {
40
+ return (title
41
+ .toLowerCase()
42
+ .replace(/[^a-z0-9]+/g, '-')
43
+ .replace(/^-+|-+$/g, '') || 'untitled');
44
+ }
45
+ const IDENTIFIER_RE = /^[A-Z][A-Za-z0-9_]*$/;
46
+ const ENTITY_ATTR_RULES = {
47
+ DOCS: {
48
+ title: { required: true, kind: 'string', hint: 'title="Group / Name"' },
49
+ description: { required: false, kind: 'string', hint: 'description="…"' },
50
+ },
51
+ PAGE: {
52
+ title: { required: true, kind: 'string', hint: 'title="Group / Name"' },
53
+ },
54
+ LAYOUT: {
55
+ title: { required: true, kind: 'string', hint: 'title="Group / Name"' },
56
+ padding: { required: false, kind: 'string', hint: 'padding="48px"' },
57
+ },
58
+ };
59
+ const SUB_BLOCK_ATTR_RULES = {
60
+ preview: {
61
+ component: { required: true, kind: 'expression', hint: 'component={Button}' },
62
+ args: { required: false, kind: 'expression', hint: 'args={{ label: "Hi" }}' },
63
+ title: { required: false, kind: 'string', hint: 'title="…"' },
64
+ },
65
+ example: {
66
+ title: { required: true, kind: 'string', hint: 'title="…"' },
67
+ },
68
+ };
69
+ function checkAttrs(owner, attrs, rules, ownerSpan, diagnostics) {
70
+ for (const [name, value] of Object.entries(attrs)) {
71
+ const rule = rules[name];
72
+ if (!rule) {
73
+ diagnostics.push({
74
+ code: 'unknown-attr',
75
+ message: `Unknown attribute "${name}" on ${owner}. Allowed: ${Object.keys(rules).join(', ')}.`,
76
+ span: value.span,
77
+ });
78
+ continue;
79
+ }
80
+ if (value.kind !== rule.kind) {
81
+ diagnostics.push({
82
+ code: 'attr-value-kind',
83
+ message: `Attribute "${name}" on ${owner} must be written ${rule.hint}.`,
84
+ span: value.span,
85
+ });
86
+ }
87
+ }
88
+ for (const [name, rule] of Object.entries(rules)) {
89
+ if (rule.required && !attrs[name]) {
90
+ diagnostics.push({
91
+ code: 'missing-attr',
92
+ message: `${owner} requires ${rule.hint}.`,
93
+ span: ownerSpan,
94
+ });
95
+ }
96
+ }
97
+ }
98
+ function stringAttr(attrs, name) {
99
+ const v = attrs[name];
100
+ return v && v.kind === 'string' ? v.raw : null;
101
+ }
102
+ /**
103
+ * Parse a preview args expression: a flat object literal whose values are
104
+ * plain literals (strings, numbers, booleans). Anything richer belongs in
105
+ * the block body, so it is rejected here.
106
+ */
107
+ export function parseArgsLiteral(raw, span, diagnostics) {
108
+ const fail = (message) => {
109
+ diagnostics.push({ code: 'args-literal', message, span });
110
+ return null;
111
+ };
112
+ let i = 0;
113
+ const ws = () => {
114
+ while (i < raw.length && /\s/.test(raw[i]))
115
+ i++;
116
+ };
117
+ ws();
118
+ if (raw[i] !== '{')
119
+ return fail('args must be an object literal: args={{ name: value }}.');
120
+ i++;
121
+ const values = {};
122
+ ws();
123
+ while (i < raw.length && raw[i] !== '}') {
124
+ const keyMatch = raw.slice(i).match(/^([A-Za-z_$][A-Za-z0-9_$]*|'[^']*'|"[^"]*")\s*:/);
125
+ if (!keyMatch)
126
+ return fail('args keys must be plain names, like args={{ label: "Hi" }}.');
127
+ const key = keyMatch[1].replace(/^['"]|['"]$/g, '');
128
+ i += keyMatch[0].length;
129
+ ws();
130
+ const rest = raw.slice(i);
131
+ let m;
132
+ if ((m = rest.match(/^'((?:[^'\\]|\\.)*)'/)) || (m = rest.match(/^"((?:[^"\\]|\\.)*)"/))) {
133
+ values[key] = m[1].replace(/\\(.)/g, '$1');
134
+ i += m[0].length;
135
+ }
136
+ else if ((m = rest.match(/^-?\d+(\.\d+)?/))) {
137
+ values[key] = Number(m[0]);
138
+ i += m[0].length;
139
+ }
140
+ else if ((m = rest.match(/^(true|false)\b/))) {
141
+ values[key] = m[0] === 'true';
142
+ i += m[0].length;
143
+ }
144
+ else {
145
+ return fail(`args value for "${key}" must be a plain literal (string, number, or boolean); richer values go in the block body.`);
146
+ }
147
+ ws();
148
+ if (raw[i] === ',') {
149
+ i++;
150
+ ws();
151
+ }
152
+ else if (raw[i] !== '}') {
153
+ return fail('args entries must be separated by commas.');
154
+ }
155
+ }
156
+ if (raw[i] !== '}')
157
+ return fail('args object literal is missing its closing "}".');
158
+ i++;
159
+ ws();
160
+ if (i !== raw.length)
161
+ return fail('Unexpected text after the args object literal.');
162
+ return values;
163
+ }
164
+ function parsePreview(block, diagnostics) {
165
+ checkAttrs('[preview]', block.attrs, SUB_BLOCK_ATTR_RULES.preview, block.openerSpan, diagnostics);
166
+ let componentName = null;
167
+ const component = block.attrs.component;
168
+ if (component && component.kind === 'expression') {
169
+ const name = component.raw.trim();
170
+ if (IDENTIFIER_RE.test(name)) {
171
+ componentName = name;
172
+ }
173
+ else {
174
+ diagnostics.push({
175
+ code: 'component-identifier',
176
+ message: 'component must be a component identifier imported in the file\'s <script>, like component={Button}.',
177
+ span: component.valueSpan,
178
+ });
179
+ }
180
+ }
181
+ let args = null;
182
+ let argsRaw = null;
183
+ const argsAttr = block.attrs.args;
184
+ if (argsAttr && argsAttr.kind === 'expression') {
185
+ argsRaw = argsAttr.raw.trim();
186
+ args = parseArgsLiteral(argsAttr.raw, argsAttr.valueSpan, diagnostics);
187
+ }
188
+ const title = stringAttr(block.attrs, 'title');
189
+ return {
190
+ componentName,
191
+ args,
192
+ argsRaw,
193
+ title,
194
+ label: title ?? componentName ?? 'Preview',
195
+ body: normalizeBody(block.body),
196
+ bodySpan: block.bodySpan,
197
+ span: block.span,
198
+ };
199
+ }
200
+ function parseDocs(entity, diagnostics) {
201
+ checkAttrs('[DOCS]', entity.attrs, ENTITY_ATTR_RULES.DOCS, entity.openerSpan, diagnostics);
202
+ const previews = [];
203
+ const examples = [];
204
+ const exampleTitles = new Set();
205
+ const previewLabels = new Set();
206
+ for (const block of entity.blocks) {
207
+ if (block.kind === 'preview') {
208
+ const preview = parsePreview(block, diagnostics);
209
+ if (previewLabels.has(preview.label)) {
210
+ diagnostics.push({
211
+ code: 'duplicate-preview-label',
212
+ message: `Two previews are both labeled "${preview.label}" — give one a title="…".`,
213
+ span: block.openerSpan,
214
+ });
215
+ }
216
+ previewLabels.add(preview.label);
217
+ previews.push(preview);
218
+ }
219
+ else {
220
+ checkAttrs('[example]', block.attrs, SUB_BLOCK_ATTR_RULES.example, block.openerSpan, diagnostics);
221
+ const title = stringAttr(block.attrs, 'title') ?? '';
222
+ if (title && exampleTitles.has(title)) {
223
+ diagnostics.push({
224
+ code: 'duplicate-example-title',
225
+ message: `Duplicate example title "${title}" — titles are unique within a [DOCS] block.`,
226
+ span: block.openerSpan,
227
+ });
228
+ }
229
+ exampleTitles.add(title);
230
+ examples.push({
231
+ title,
232
+ body: normalizeBody(block.body),
233
+ bodySpan: block.bodySpan,
234
+ span: block.span,
235
+ });
236
+ }
237
+ }
238
+ const title = stringAttr(entity.attrs, 'title') ?? '';
239
+ return {
240
+ kind: 'DOCS',
241
+ title,
242
+ slug: slugifyTitle(title),
243
+ description: stringAttr(entity.attrs, 'description'),
244
+ previews,
245
+ examples,
246
+ openerSpan: entity.openerSpan,
247
+ span: entity.span,
248
+ };
249
+ }
250
+ export function parseSdoc(source) {
251
+ const scanned = scanSdoc(source);
252
+ const diagnostics = [...scanned.errors];
253
+ const entities = [];
254
+ const slugs = new Set();
255
+ for (const entity of scanned.entities) {
256
+ let typed;
257
+ if (entity.kind === 'DOCS') {
258
+ typed = parseDocs(entity, diagnostics);
259
+ }
260
+ else {
261
+ checkAttrs(`[${entity.kind}]`, entity.attrs, ENTITY_ATTR_RULES[entity.kind], entity.openerSpan, diagnostics);
262
+ const title = stringAttr(entity.attrs, 'title') ?? '';
263
+ const base = {
264
+ title,
265
+ slug: slugifyTitle(title),
266
+ body: normalizeBody(entity.body),
267
+ bodySpan: entity.bodySpan,
268
+ openerSpan: entity.openerSpan,
269
+ span: entity.span,
270
+ };
271
+ typed =
272
+ entity.kind === 'PAGE'
273
+ ? { kind: 'PAGE', ...base }
274
+ : { kind: 'LAYOUT', padding: stringAttr(entity.attrs, 'padding'), ...base };
275
+ }
276
+ if (slugs.has(typed.slug)) {
277
+ diagnostics.push({
278
+ code: 'duplicate-entity-title',
279
+ message: `Two entities in this file resolve to the same address ("${typed.slug}") — make their titles distinct.`,
280
+ span: typed.openerSpan,
281
+ });
282
+ }
283
+ slugs.add(typed.slug);
284
+ entities.push(typed);
285
+ }
286
+ return {
287
+ script: scanned.script,
288
+ style: scanned.style,
289
+ entities,
290
+ diagnostics,
291
+ source,
292
+ };
293
+ }
@@ -0,0 +1 @@
1
+ export {};