sdocs 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/bin/sdocs.js +1 -1
  2. package/dist/app-gen.d.ts +10 -0
  3. package/dist/app-gen.js +147 -0
  4. package/dist/cli.js +71 -0
  5. package/dist/client/App.svelte +151 -0
  6. package/dist/client/App.svelte.d.ts +14 -0
  7. package/dist/client/CollapsiblePanel.svelte +63 -0
  8. package/dist/client/CollapsiblePanel.svelte.d.ts +9 -0
  9. package/dist/client/ComponentView.svelte +321 -0
  10. package/dist/client/ComponentView.svelte.d.ts +10 -0
  11. package/dist/client/ControlsPanel.svelte +191 -0
  12. package/dist/client/ControlsPanel.svelte.d.ts +13 -0
  13. package/dist/client/DataTable.svelte +78 -0
  14. package/dist/client/DataTable.svelte.d.ts +11 -0
  15. package/dist/client/HomePage.svelte +92 -0
  16. package/dist/client/HomePage.svelte.d.ts +8 -0
  17. package/dist/client/LayoutView.svelte +27 -0
  18. package/dist/client/LayoutView.svelte.d.ts +8 -0
  19. package/dist/client/PageView.svelte +130 -0
  20. package/dist/client/PageView.svelte.d.ts +8 -0
  21. package/dist/client/PreviewFrame.svelte +100 -0
  22. package/dist/client/PreviewFrame.svelte.d.ts +10 -0
  23. package/dist/client/Sidebar.svelte +329 -0
  24. package/dist/client/Sidebar.svelte.d.ts +16 -0
  25. package/dist/client/controls/CheckboxControl.svelte +37 -0
  26. package/dist/client/controls/CheckboxControl.svelte.d.ts +8 -0
  27. package/dist/client/controls/ColorControl.svelte +47 -0
  28. package/dist/client/controls/ColorControl.svelte.d.ts +8 -0
  29. package/dist/client/controls/DimensionControl.svelte +56 -0
  30. package/dist/client/controls/DimensionControl.svelte.d.ts +8 -0
  31. package/dist/client/controls/NumberControl.svelte +44 -0
  32. package/dist/client/controls/NumberControl.svelte.d.ts +8 -0
  33. package/dist/client/controls/SelectControl.svelte +48 -0
  34. package/dist/client/controls/SelectControl.svelte.d.ts +9 -0
  35. package/dist/client/controls/TextControl.svelte +43 -0
  36. package/dist/client/controls/TextControl.svelte.d.ts +8 -0
  37. package/dist/client/router.svelte.d.ts +11 -0
  38. package/dist/client/router.svelte.js +45 -0
  39. package/dist/client/theme.css +34 -0
  40. package/dist/client/tree-builder.d.ts +30 -0
  41. package/dist/client/tree-builder.js +162 -0
  42. package/dist/commands/build.d.ts +1 -0
  43. package/dist/commands/build.js +38 -0
  44. package/dist/commands/dev.d.ts +1 -0
  45. package/dist/commands/dev.js +40 -0
  46. package/dist/commands/init.d.ts +1 -0
  47. package/dist/commands/init.js +41 -0
  48. package/dist/commands/preview.d.ts +1 -0
  49. package/dist/commands/preview.js +25 -0
  50. package/dist/config.d.ts +7 -0
  51. package/dist/config.js +57 -0
  52. package/dist/index.d.ts +2 -2
  53. package/dist/index.js +1 -4
  54. package/dist/server/discovery.d.ts +6 -0
  55. package/dist/server/discovery.js +24 -0
  56. package/dist/server/highlighter.d.ts +4 -0
  57. package/dist/server/highlighter.js +31 -0
  58. package/dist/server/meta-parser.d.ts +11 -0
  59. package/dist/server/meta-parser.js +107 -0
  60. package/dist/server/prop-parser.d.ts +5 -0
  61. package/dist/server/prop-parser.js +275 -0
  62. package/dist/server/sdocx-parser.d.ts +11 -0
  63. package/dist/server/sdocx-parser.js +197 -0
  64. package/dist/server/snippet-compiler.d.ts +27 -0
  65. package/dist/server/snippet-compiler.js +145 -0
  66. package/dist/server/snippet-extractor.d.ts +11 -0
  67. package/dist/server/snippet-extractor.js +37 -0
  68. package/dist/server/toc-extractor.d.ts +5 -0
  69. package/dist/server/toc-extractor.js +37 -0
  70. package/dist/types.d.ts +100 -148
  71. package/dist/vite.d.ts +5 -2
  72. package/dist/vite.js +266 -2
  73. package/package.json +50 -74
  74. package/README.md +0 -43
  75. package/dist/Sdocs.svelte +0 -1210
  76. package/dist/Sdocs.svelte.d.ts +0 -5
  77. package/dist/cli/app-plugin.d.ts +0 -7
  78. package/dist/cli/app-plugin.js +0 -69
  79. package/dist/cli/config.d.ts +0 -12
  80. package/dist/cli/config.js +0 -34
  81. package/dist/cli/index.js +0 -72
  82. package/dist/cli/server.d.ts +0 -2
  83. package/dist/cli/server.js +0 -64
  84. package/dist/docgen.d.ts +0 -47
  85. package/dist/docgen.js +0 -463
  86. package/dist/internal/ComponentPreview.svelte +0 -58
  87. package/dist/internal/ComponentPreview.svelte.d.ts +0 -17
  88. package/dist/internal/CssPropsTable.svelte +0 -239
  89. package/dist/internal/CssPropsTable.svelte.d.ts +0 -11
  90. package/dist/internal/Home.svelte +0 -92
  91. package/dist/internal/Home.svelte.d.ts +0 -9
  92. package/dist/internal/MethodsTable.svelte +0 -72
  93. package/dist/internal/MethodsTable.svelte.d.ts +0 -7
  94. package/dist/internal/PropsTable.svelte +0 -342
  95. package/dist/internal/PropsTable.svelte.d.ts +0 -12
  96. package/dist/internal/Showcase.svelte +0 -130
  97. package/dist/internal/Showcase.svelte.d.ts +0 -21
  98. package/dist/ui/Badge/Badge.docs.svelte +0 -46
  99. package/dist/ui/Badge/Badge.docs.svelte.d.ts +0 -26
  100. package/dist/ui/Badge/Badge.svelte +0 -59
  101. package/dist/ui/Badge/Badge.svelte.d.ts +0 -17
  102. package/dist/ui/Badge/index.d.ts +0 -1
  103. package/dist/ui/Badge/index.js +0 -1
  104. package/dist/ui/Checkbox/Checkbox.docs.svelte +0 -51
  105. package/dist/ui/Checkbox/Checkbox.docs.svelte.d.ts +0 -27
  106. package/dist/ui/Checkbox/Checkbox.svelte +0 -169
  107. package/dist/ui/Checkbox/Checkbox.svelte.d.ts +0 -18
  108. package/dist/ui/Checkbox/index.d.ts +0 -1
  109. package/dist/ui/Checkbox/index.js +0 -1
  110. package/dist/ui/CodeBlock/CodeBlock.docs.svelte +0 -28
  111. package/dist/ui/CodeBlock/CodeBlock.docs.svelte.d.ts +0 -24
  112. package/dist/ui/CodeBlock/CodeBlock.svelte +0 -101
  113. package/dist/ui/CodeBlock/CodeBlock.svelte.d.ts +0 -7
  114. package/dist/ui/CodeBlock/index.d.ts +0 -1
  115. package/dist/ui/CodeBlock/index.js +0 -1
  116. package/dist/ui/Frame/Frame.docs.svelte +0 -140
  117. package/dist/ui/Frame/Frame.docs.svelte.d.ts +0 -26
  118. package/dist/ui/Frame/Frame.svelte +0 -88
  119. package/dist/ui/Frame/Frame.svelte.d.ts +0 -15
  120. package/dist/ui/Frame/index.d.ts +0 -1
  121. package/dist/ui/Frame/index.js +0 -1
  122. package/dist/ui/InputNumber/InputNumber.docs.svelte +0 -50
  123. package/dist/ui/InputNumber/InputNumber.docs.svelte.d.ts +0 -26
  124. package/dist/ui/InputNumber/InputNumber.svelte +0 -275
  125. package/dist/ui/InputNumber/InputNumber.svelte.d.ts +0 -26
  126. package/dist/ui/InputNumber/index.d.ts +0 -1
  127. package/dist/ui/InputNumber/index.js +0 -1
  128. package/dist/ui/InputText/InputText.docs.svelte +0 -43
  129. package/dist/ui/InputText/InputText.docs.svelte.d.ts +0 -26
  130. package/dist/ui/InputText/InputText.svelte +0 -116
  131. package/dist/ui/InputText/InputText.svelte.d.ts +0 -22
  132. package/dist/ui/InputText/index.d.ts +0 -1
  133. package/dist/ui/InputText/index.js +0 -1
  134. package/dist/ui/Panel/CollapsiblePanel.docs.svelte +0 -45
  135. package/dist/ui/Panel/CollapsiblePanel.docs.svelte.d.ts +0 -25
  136. package/dist/ui/Panel/CollapsiblePanel.svelte +0 -93
  137. package/dist/ui/Panel/CollapsiblePanel.svelte.d.ts +0 -14
  138. package/dist/ui/Panel/index.d.ts +0 -1
  139. package/dist/ui/Panel/index.js +0 -1
  140. package/dist/ui/Placeholder/Placeholder.docs.svelte +0 -49
  141. package/dist/ui/Placeholder/Placeholder.docs.svelte.d.ts +0 -26
  142. package/dist/ui/Placeholder/Placeholder.svelte +0 -99
  143. package/dist/ui/Placeholder/Placeholder.svelte.d.ts +0 -21
  144. package/dist/ui/Placeholder/index.d.ts +0 -1
  145. package/dist/ui/Placeholder/index.js +0 -1
  146. package/dist/ui/Radio/Radio.docs.svelte +0 -67
  147. package/dist/ui/Radio/Radio.docs.svelte.d.ts +0 -27
  148. package/dist/ui/Radio/Radio.svelte +0 -165
  149. package/dist/ui/Radio/Radio.svelte.d.ts +0 -22
  150. package/dist/ui/Radio/RadioGroup.docs.svelte +0 -70
  151. package/dist/ui/Radio/RadioGroup.docs.svelte.d.ts +0 -27
  152. package/dist/ui/Radio/RadioGroup.svelte +0 -98
  153. package/dist/ui/Radio/RadioGroup.svelte.d.ts +0 -27
  154. package/dist/ui/Radio/index.d.ts +0 -2
  155. package/dist/ui/Radio/index.js +0 -2
  156. package/dist/ui/SegmentControl/SegmentControl.docs.svelte +0 -54
  157. package/dist/ui/SegmentControl/SegmentControl.docs.svelte.d.ts +0 -25
  158. package/dist/ui/SegmentControl/SegmentControl.svelte +0 -120
  159. package/dist/ui/SegmentControl/SegmentControl.svelte.d.ts +0 -18
  160. package/dist/ui/SegmentControl/index.d.ts +0 -1
  161. package/dist/ui/SegmentControl/index.js +0 -1
  162. package/dist/ui/Stack/Stack.docs.svelte +0 -63
  163. package/dist/ui/Stack/Stack.docs.svelte.d.ts +0 -26
  164. package/dist/ui/Stack/Stack.svelte +0 -45
  165. package/dist/ui/Stack/Stack.svelte.d.ts +0 -19
  166. package/dist/ui/Stack/index.d.ts +0 -1
  167. package/dist/ui/Stack/index.js +0 -1
  168. package/dist/ui/Table/Body.svelte +0 -17
  169. package/dist/ui/Table/Body.svelte.d.ts +0 -11
  170. package/dist/ui/Table/Caption.svelte +0 -17
  171. package/dist/ui/Table/Caption.svelte.d.ts +0 -11
  172. package/dist/ui/Table/Cell.svelte +0 -24
  173. package/dist/ui/Table/Cell.svelte.d.ts +0 -15
  174. package/dist/ui/Table/Foot.svelte +0 -17
  175. package/dist/ui/Table/Foot.svelte.d.ts +0 -11
  176. package/dist/ui/Table/Head.svelte +0 -17
  177. package/dist/ui/Table/Head.svelte.d.ts +0 -11
  178. package/dist/ui/Table/Header.svelte +0 -27
  179. package/dist/ui/Table/Header.svelte.d.ts +0 -17
  180. package/dist/ui/Table/Row.svelte +0 -19
  181. package/dist/ui/Table/Row.svelte.d.ts +0 -13
  182. package/dist/ui/Table/Table.docs.svelte +0 -197
  183. package/dist/ui/Table/Table.docs.svelte.d.ts +0 -28
  184. package/dist/ui/Table/Table.svelte +0 -140
  185. package/dist/ui/Table/Table.svelte.d.ts +0 -27
  186. package/dist/ui/Table/index.js +0 -10
  187. package/dist/ui/css/colors.css +0 -377
  188. package/dist/ui/css/global.css +0 -10
  189. package/dist/ui/index.d.ts +0 -12
  190. package/dist/ui/index.js +0 -12
  191. package/dist/virtual-sdocs.d.ts +0 -20
  192. package/dist/vite-plugin.d.ts +0 -18
  193. package/dist/vite-plugin.js +0 -206
  194. /package/dist/{cli/index.d.ts → cli.d.ts} +0 -0
@@ -0,0 +1,107 @@
1
+ import { parse } from 'svelte/compiler';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { resolve, dirname } from 'node:path';
4
+ /** Extract meta and imports from a .sdoc file */
5
+ export async function parseDocFile(filePath) {
6
+ const source = await readFile(filePath, 'utf-8');
7
+ return parseDocSource(source, filePath);
8
+ }
9
+ /** Parse meta from .sdoc source */
10
+ export function parseDocSource(source, filePath) {
11
+ const ast = parse(source, { modern: true });
12
+ const scriptContent = extractScriptContent(source);
13
+ if (!scriptContent) {
14
+ return {
15
+ meta: { title: guessTitle(filePath) },
16
+ componentPath: null,
17
+ imports: [],
18
+ };
19
+ }
20
+ const imports = extractImports(scriptContent);
21
+ const meta = extractMeta(scriptContent);
22
+ const componentPath = resolveComponentPath(meta, imports, filePath);
23
+ return { meta, componentPath, imports };
24
+ }
25
+ /** Extract the content of the <script> tag */
26
+ function extractScriptContent(source) {
27
+ const match = source.match(/<script[^>]*>([\s\S]*?)<\/script>/);
28
+ return match ? match[1] : null;
29
+ }
30
+ /** Extract import statements */
31
+ function extractImports(scriptContent) {
32
+ const imports = [];
33
+ const regex = /^\s*import\s+.+$/gm;
34
+ let match;
35
+ while ((match = regex.exec(scriptContent)) !== null) {
36
+ imports.push(match[0].trim());
37
+ }
38
+ return imports;
39
+ }
40
+ /** Extract meta object from `export const meta = { ... }` using brace counting */
41
+ function extractMeta(scriptContent) {
42
+ // Find where the meta object starts
43
+ const startMatch = scriptContent.match(/export\s+const\s+meta\s*(?::\s*\w+\s*)?=\s*\{/);
44
+ if (!startMatch || startMatch.index === undefined)
45
+ return { title: 'Untitled' };
46
+ // Find the opening brace
47
+ const braceStart = startMatch.index + startMatch[0].length - 1;
48
+ let depth = 1;
49
+ let i = braceStart + 1;
50
+ // Count braces to find the matching closing brace
51
+ while (i < scriptContent.length && depth > 0) {
52
+ const ch = scriptContent[i];
53
+ if (ch === '{')
54
+ depth++;
55
+ else if (ch === '}')
56
+ depth--;
57
+ // Skip strings
58
+ else if (ch === "'" || ch === '"' || ch === '`') {
59
+ i++;
60
+ while (i < scriptContent.length && scriptContent[i] !== ch) {
61
+ if (scriptContent[i] === '\\')
62
+ i++; // skip escaped chars
63
+ i++;
64
+ }
65
+ }
66
+ i++;
67
+ }
68
+ const metaStr = scriptContent.slice(braceStart, i);
69
+ try {
70
+ // Replace component: Identifier with component: 'Identifier'
71
+ const cleaned = metaStr.replace(/component:\s*([A-Z]\w*)/, "component: '$1'");
72
+ const fn = new Function(`return (${cleaned})`);
73
+ const result = fn();
74
+ return result;
75
+ }
76
+ catch {
77
+ return extractMetaFields(metaStr);
78
+ }
79
+ }
80
+ /** Fallback: extract meta fields with regex */
81
+ function extractMetaFields(metaStr) {
82
+ const title = metaStr.match(/title:\s*['"](.+?)['"]/)?.[1] ?? 'Untitled';
83
+ const description = metaStr.match(/description:\s*['"](.+?)['"]/)?.[1];
84
+ return { title, description };
85
+ }
86
+ /** Resolve the component import path to an absolute path */
87
+ function resolveComponentPath(meta, imports, docFilePath) {
88
+ // meta.component is the identifier name (e.g. 'Button')
89
+ const componentName = typeof meta.component === 'string' ? meta.component : null;
90
+ if (!componentName)
91
+ return null;
92
+ // Find the import that imports this name
93
+ for (const imp of imports) {
94
+ // Match: import Name from './path'
95
+ const match = imp.match(new RegExp(`import\\s+${componentName}\\s+from\\s+['"](.+?)['"]`));
96
+ if (match) {
97
+ const importPath = match[1];
98
+ return resolve(dirname(docFilePath), importPath);
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+ /** Guess a title from the file path */
104
+ function guessTitle(filePath) {
105
+ const fileName = filePath.split('/').pop() ?? '';
106
+ return fileName.replace(/\.(sdoc|sdocx)$/, '').replace(/\./g, ' ');
107
+ }
@@ -0,0 +1,5 @@
1
+ import type { ComponentData } from '../types.js';
2
+ /** Parse all component data from a Svelte component file */
3
+ export declare function parseComponent(filePath: string): Promise<ComponentData>;
4
+ /** Parse component data from source */
5
+ export declare function parseComponentSource(source: string): ComponentData;
@@ -0,0 +1,275 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import ts from 'typescript';
3
+ /** Parse all component data from a Svelte component file */
4
+ export async function parseComponent(filePath) {
5
+ const source = await readFile(filePath, 'utf-8');
6
+ return parseComponentSource(source);
7
+ }
8
+ /** Parse component data from source */
9
+ export function parseComponentSource(source) {
10
+ const scriptContent = extractScriptContent(source);
11
+ const styleContent = extractStyleContent(source);
12
+ let props = [];
13
+ let methods = [];
14
+ let state = [];
15
+ if (scriptContent) {
16
+ const tsAst = ts.createSourceFile('component.ts', scriptContent, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
17
+ const interfaceProps = parseInterfaceProps(tsAst);
18
+ const destructuredProps = parsePropsDestructuring(tsAst);
19
+ const jsdocData = parseJsdocComments(tsAst);
20
+ props = mergeProps(interfaceProps, destructuredProps, jsdocData);
21
+ methods = parseExportedFunctions(tsAst);
22
+ state = parseExportedState(tsAst);
23
+ }
24
+ const cssProps = styleContent ? parseCssProps(source, styleContent) : [];
25
+ return { props, methods, state, cssProps };
26
+ }
27
+ // ─── Script extraction ───
28
+ function extractScriptContent(source) {
29
+ const match = source.match(/<script[^>]*>([\s\S]*?)<\/script>/);
30
+ return match ? match[1] : null;
31
+ }
32
+ function extractStyleContent(source) {
33
+ const match = source.match(/<style[^>]*>([\s\S]*?)<\/style>/);
34
+ return match ? match[1] : null;
35
+ }
36
+ function parseInterfaceProps(sourceFile) {
37
+ const props = [];
38
+ ts.forEachChild(sourceFile, (node) => {
39
+ if (ts.isInterfaceDeclaration(node) && node.name.text === 'Props') {
40
+ for (const member of node.members) {
41
+ if (ts.isPropertySignature(member) && member.name) {
42
+ const name = member.name.getText(sourceFile);
43
+ const type = member.type
44
+ ? member.type.getText(sourceFile)
45
+ : 'unknown';
46
+ const optional = !!member.questionToken;
47
+ const description = getJsdocComment(member, sourceFile);
48
+ props.push({ name, type, optional, description });
49
+ }
50
+ }
51
+ }
52
+ });
53
+ return props;
54
+ }
55
+ function parsePropsDestructuring(sourceFile) {
56
+ const props = [];
57
+ function visit(node) {
58
+ // Match: let { ... } = $props()
59
+ if (ts.isVariableDeclaration(node) &&
60
+ node.initializer &&
61
+ ts.isCallExpression(node.initializer) &&
62
+ node.initializer.expression.getText(sourceFile) === '$props' &&
63
+ node.name &&
64
+ ts.isObjectBindingPattern(node.name)) {
65
+ for (const element of node.name.elements) {
66
+ if (ts.isBindingElement(element)) {
67
+ const name = element.name.getText(sourceFile);
68
+ const defaultValue = element.initializer
69
+ ? element.initializer.getText(sourceFile)
70
+ : null;
71
+ props.push({ name, default: defaultValue });
72
+ }
73
+ }
74
+ }
75
+ ts.forEachChild(node, visit);
76
+ }
77
+ visit(sourceFile);
78
+ return props;
79
+ }
80
+ function parseJsdocComments(sourceFile) {
81
+ // JSDoc data is already captured from interface Props via getJsdocComment
82
+ // This handles per-prop JSDoc in destructuring (JS components)
83
+ const data = [];
84
+ function visit(node) {
85
+ if (ts.isVariableDeclaration(node) &&
86
+ node.name &&
87
+ ts.isObjectBindingPattern(node.name)) {
88
+ for (const element of node.name.elements) {
89
+ if (ts.isBindingElement(element)) {
90
+ const desc = getJsdocComment(element, sourceFile);
91
+ if (desc) {
92
+ data.push({
93
+ name: element.name.getText(sourceFile),
94
+ description: desc,
95
+ type: null,
96
+ });
97
+ }
98
+ }
99
+ }
100
+ }
101
+ ts.forEachChild(node, visit);
102
+ }
103
+ visit(sourceFile);
104
+ return data;
105
+ }
106
+ // ─── Merge props from all sources ───
107
+ function mergeProps(interfaceProps, destructuredProps, jsdocData) {
108
+ const propMap = new Map();
109
+ // Start with interface props
110
+ for (const ip of interfaceProps) {
111
+ propMap.set(ip.name, {
112
+ name: ip.name,
113
+ type: ip.type,
114
+ default: null,
115
+ description: ip.description,
116
+ required: !ip.optional,
117
+ category: classifyProp(ip.name, ip.type),
118
+ });
119
+ }
120
+ // Merge destructured defaults
121
+ for (const dp of destructuredProps) {
122
+ const existing = propMap.get(dp.name);
123
+ if (existing) {
124
+ existing.default = dp.default;
125
+ if (dp.default !== null)
126
+ existing.required = false;
127
+ }
128
+ else {
129
+ propMap.set(dp.name, {
130
+ name: dp.name,
131
+ type: null,
132
+ default: dp.default,
133
+ description: null,
134
+ required: dp.default === null,
135
+ category: 'prop',
136
+ });
137
+ }
138
+ }
139
+ // Merge JSDoc descriptions
140
+ for (const jd of jsdocData) {
141
+ const existing = propMap.get(jd.name);
142
+ if (existing && !existing.description && jd.description) {
143
+ existing.description = jd.description;
144
+ }
145
+ if (existing && !existing.type && jd.type) {
146
+ existing.type = jd.type;
147
+ }
148
+ }
149
+ return Array.from(propMap.values());
150
+ }
151
+ // ─── Classify prop ───
152
+ function classifyProp(name, type) {
153
+ if (name.startsWith('on') && type?.includes('=>'))
154
+ return 'event';
155
+ if (type?.startsWith('Snippet'))
156
+ return 'snippet';
157
+ return 'prop';
158
+ }
159
+ // ─── Exported functions ───
160
+ function parseExportedFunctions(sourceFile) {
161
+ const methods = [];
162
+ ts.forEachChild(sourceFile, (node) => {
163
+ if (ts.isFunctionDeclaration(node) &&
164
+ node.name &&
165
+ hasExportModifier(node)) {
166
+ const params = node.parameters
167
+ .map((p) => p.getText(sourceFile))
168
+ .join(', ');
169
+ const returnType = node.type
170
+ ? node.type.getText(sourceFile)
171
+ : null;
172
+ const description = getJsdocComment(node, sourceFile);
173
+ methods.push({
174
+ name: node.name.text,
175
+ params,
176
+ returnType,
177
+ description,
178
+ });
179
+ }
180
+ });
181
+ return methods;
182
+ }
183
+ // ─── Exported state ───
184
+ function parseExportedState(sourceFile) {
185
+ const state = [];
186
+ ts.forEachChild(sourceFile, (node) => {
187
+ if (ts.isVariableStatement(node) &&
188
+ hasExportModifier(node)) {
189
+ for (const decl of node.declarationList.declarations) {
190
+ if (ts.isIdentifier(decl.name)) {
191
+ const init = decl.initializer?.getText(sourceFile) ?? '';
192
+ if (init.includes('$state') ||
193
+ init.includes('$derived')) {
194
+ const description = getJsdocComment(node, sourceFile);
195
+ state.push({
196
+ name: decl.name.text,
197
+ type: decl.type
198
+ ? decl.type.getText(sourceFile)
199
+ : null,
200
+ description,
201
+ });
202
+ }
203
+ }
204
+ }
205
+ }
206
+ });
207
+ return state;
208
+ }
209
+ // ─── CSS custom properties ───
210
+ function parseCssProps(fullSource, styleContent) {
211
+ const propMap = new Map();
212
+ // Extract var(--name) and var(--name, default) from <style>
213
+ const varRegex = /var\(\s*(--[\w-]+)(?:\s*,\s*([^)]+))?\s*\)/g;
214
+ let match;
215
+ while ((match = varRegex.exec(styleContent)) !== null) {
216
+ const name = match[1];
217
+ const defaultVal = match[2]?.trim() ?? null;
218
+ propMap.set(name, {
219
+ name,
220
+ type: null,
221
+ default: defaultVal,
222
+ description: null,
223
+ });
224
+ }
225
+ // Extract @cssvar JSDoc annotations from <script>
226
+ const cssvarRegex = /@cssvar\s+\{(\w+)\}\s+(--[\w-]+)\s*-?\s*(.*?)(?:\(default:\s*([^)]+)\))?$/gm;
227
+ while ((match = cssvarRegex.exec(fullSource)) !== null) {
228
+ const type = match[1];
229
+ const name = match[2];
230
+ const description = match[3]?.trim() || null;
231
+ const defaultVal = match[4]?.trim() ?? null;
232
+ const existing = propMap.get(name);
233
+ if (existing) {
234
+ existing.type = type;
235
+ if (description)
236
+ existing.description = description;
237
+ if (defaultVal && !existing.default)
238
+ existing.default = defaultVal;
239
+ }
240
+ else {
241
+ propMap.set(name, { name, type, default: defaultVal, description });
242
+ }
243
+ }
244
+ return Array.from(propMap.values());
245
+ }
246
+ // ─── Helpers ───
247
+ function getJsdocComment(node, sourceFile) {
248
+ const fullText = sourceFile.getFullText();
249
+ const ranges = ts.getLeadingCommentRanges(fullText, node.getFullStart());
250
+ if (!ranges)
251
+ return null;
252
+ for (const range of ranges) {
253
+ const comment = fullText.slice(range.pos, range.end);
254
+ if (comment.startsWith('/**')) {
255
+ // Extract text between /** and */
256
+ const text = comment
257
+ .replace(/^\/\*\*\s*/, '')
258
+ .replace(/\s*\*\/$/, '')
259
+ .replace(/^\s*\*\s?/gm, '')
260
+ .trim();
261
+ // Skip @tags
262
+ const firstLine = text.split('\n')[0];
263
+ if (firstLine && !firstLine.startsWith('@')) {
264
+ return firstLine;
265
+ }
266
+ }
267
+ }
268
+ return null;
269
+ }
270
+ function hasExportModifier(node) {
271
+ const modifiers = ts.canHaveModifiers(node)
272
+ ? ts.getModifiers(node)
273
+ : undefined;
274
+ return modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
275
+ }
@@ -0,0 +1,11 @@
1
+ import type { SdocMeta, ExtractedSnippet, TocHeading } from '../types.js';
2
+ interface SdocxParseResult {
3
+ meta: SdocMeta;
4
+ componentPath: string | null;
5
+ imports: string[];
6
+ snippets: ExtractedSnippet[];
7
+ toc?: TocHeading[];
8
+ }
9
+ /** Parse a .sdocx file (markdown format) */
10
+ export declare function parseSdocx(source: string, filePath: string, kind: 'component' | 'page' | 'layout'): Promise<SdocxParseResult>;
11
+ export {};
@@ -0,0 +1,197 @@
1
+ import { compile } from 'mdsvex';
2
+ import { resolve, dirname } from 'node:path';
3
+ import { extractTocFromMarkdown } from './toc-extractor.js';
4
+ /** Parse a .sdocx file (markdown format) */
5
+ export async function parseSdocx(source, filePath, kind) {
6
+ // Compile the full source through mdsvex to extract frontmatter metadata
7
+ const compiled = await compile(source, { extension: '.sdocx' });
8
+ if (!compiled) {
9
+ return {
10
+ meta: { title: guessTitle(filePath) },
11
+ componentPath: null,
12
+ imports: [],
13
+ snippets: [],
14
+ };
15
+ }
16
+ // Extract metadata from the module script mdsvex generates
17
+ const metadata = extractMetadata(compiled.code);
18
+ const { title, description } = extractTitleAndDescription(source);
19
+ const meta = {
20
+ title: title ?? guessTitle(filePath),
21
+ description,
22
+ args: metadata.args,
23
+ settings: metadata.settings,
24
+ };
25
+ // Resolve component path for component kind
26
+ let componentPath = null;
27
+ if (kind === 'component' && metadata.component) {
28
+ componentPath = resolve(dirname(filePath), metadata.component);
29
+ }
30
+ // Extract user <script> imports (not the module script mdsvex adds)
31
+ const imports = extractUserImports(source);
32
+ // Auto-import the component if specified and not already imported
33
+ if (componentPath && metadata.component) {
34
+ const componentName = componentPath.split('/').pop()?.replace('.svelte', '') ?? '';
35
+ const hasImport = imports.some((imp) => imp.includes(componentName));
36
+ if (!hasImport && componentName) {
37
+ imports.unshift(`import ${componentName} from '${metadata.component}'`);
38
+ }
39
+ }
40
+ let snippets;
41
+ let toc;
42
+ if (kind === 'component') {
43
+ snippets = await extractComponentSnippets(source, compiled.code);
44
+ }
45
+ else {
46
+ // Page/layout: the entire body is one "Content" snippet
47
+ const body = extractMarkdownBody(source);
48
+ const compiledBody = await compile(body, { extension: '.sdocx' });
49
+ const htmlBody = compiledBody
50
+ ? removeModuleScript(compiledBody.code)
51
+ : body;
52
+ snippets = [{ name: 'Content', body: htmlBody }];
53
+ if (kind === 'page') {
54
+ toc = extractTocFromMarkdown(body);
55
+ }
56
+ }
57
+ return { meta, componentPath, imports, snippets, toc };
58
+ }
59
+ /** Extract metadata from the mdsvex-generated module script */
60
+ function extractMetadata(code) {
61
+ const match = code.match(/export\s+const\s+metadata\s*=\s*(\{[\s\S]*?\});/);
62
+ if (!match)
63
+ return {};
64
+ try {
65
+ return JSON.parse(match[1]);
66
+ }
67
+ catch {
68
+ return {};
69
+ }
70
+ }
71
+ /** Extract title (# heading) and description (first paragraph after #) from raw markdown */
72
+ function extractTitleAndDescription(source) {
73
+ // Remove frontmatter
74
+ const body = source.replace(/^---[\s\S]*?---\s*/, '');
75
+ const lines = body.split('\n');
76
+ let title = null;
77
+ let description = null;
78
+ for (let i = 0; i < lines.length; i++) {
79
+ const line = lines[i].trim();
80
+ if (!line)
81
+ continue;
82
+ if (line.startsWith('# ') && !title) {
83
+ title = line.slice(2).trim();
84
+ // Look for the first paragraph after the heading
85
+ for (let j = i + 1; j < lines.length; j++) {
86
+ const next = lines[j].trim();
87
+ if (!next)
88
+ continue;
89
+ if (next.startsWith('#') || next.startsWith('<'))
90
+ break;
91
+ description = next;
92
+ break;
93
+ }
94
+ break;
95
+ }
96
+ }
97
+ return { title, description };
98
+ }
99
+ /** Extract user-written <script> imports from raw markdown source */
100
+ function extractUserImports(source) {
101
+ // Remove frontmatter first
102
+ const body = source.replace(/^---[\s\S]*?---\s*/, '');
103
+ const scriptMatch = body.match(/<script[^>]*>([\s\S]*?)<\/script>/);
104
+ if (!scriptMatch)
105
+ return [];
106
+ const imports = [];
107
+ const regex = /^\s*import\s+.+$/gm;
108
+ let match;
109
+ while ((match = regex.exec(scriptMatch[1])) !== null) {
110
+ imports.push(match[0].trim());
111
+ }
112
+ return imports;
113
+ }
114
+ /** Extract the markdown body after frontmatter, removing the # title and first description paragraph */
115
+ function extractMarkdownBody(source) {
116
+ const body = source.replace(/^---[\s\S]*?---\s*/, '');
117
+ const lines = body.split('\n');
118
+ let startIndex = 0;
119
+ let foundTitle = false;
120
+ let skippedDescription = false;
121
+ for (let i = 0; i < lines.length; i++) {
122
+ const line = lines[i].trim();
123
+ if (!line)
124
+ continue;
125
+ if (line.startsWith('# ') && !foundTitle) {
126
+ foundTitle = true;
127
+ startIndex = i + 1;
128
+ // Skip the description paragraph
129
+ for (let j = i + 1; j < lines.length; j++) {
130
+ const next = lines[j].trim();
131
+ if (!next)
132
+ continue;
133
+ if (next.startsWith('#') || next.startsWith('<script')) {
134
+ startIndex = j;
135
+ skippedDescription = true;
136
+ break;
137
+ }
138
+ // This is the description paragraph, skip it
139
+ startIndex = j + 1;
140
+ skippedDescription = true;
141
+ break;
142
+ }
143
+ break;
144
+ }
145
+ }
146
+ return lines.slice(startIndex).join('\n').trim();
147
+ }
148
+ /** Extract component snippets from ## sections in markdown */
149
+ async function extractComponentSnippets(source, _compiledCode) {
150
+ const body = extractMarkdownBody(source);
151
+ const sections = splitBySections(body);
152
+ const snippets = [];
153
+ for (const section of sections) {
154
+ const compiled = await compile(section.body, { extension: '.sdocx' });
155
+ const html = compiled ? removeModuleScript(compiled.code) : section.body;
156
+ snippets.push({
157
+ name: section.name,
158
+ body: html.trim(),
159
+ });
160
+ }
161
+ return snippets;
162
+ }
163
+ /** Split markdown body by ## headings into named sections */
164
+ function splitBySections(body) {
165
+ const sections = [];
166
+ const lines = body.split('\n');
167
+ let currentName = null;
168
+ let currentLines = [];
169
+ for (const line of lines) {
170
+ const headingMatch = line.match(/^##\s+(.+)$/);
171
+ if (headingMatch) {
172
+ if (currentName !== null) {
173
+ sections.push({ name: currentName, body: currentLines.join('\n').trim() });
174
+ }
175
+ currentName = headingMatch[1].trim();
176
+ currentLines = [];
177
+ }
178
+ else if (currentName !== null) {
179
+ currentLines.push(line);
180
+ }
181
+ }
182
+ if (currentName !== null) {
183
+ sections.push({ name: currentName, body: currentLines.join('\n').trim() });
184
+ }
185
+ return sections;
186
+ }
187
+ /** Remove the mdsvex-generated module script from compiled code */
188
+ function removeModuleScript(code) {
189
+ return code.replace(/<script context="module">[\s\S]*?<\/script>\s*/, '').trim();
190
+ }
191
+ /** Guess a title from the file path */
192
+ function guessTitle(filePath) {
193
+ const fileName = filePath.split('/').pop() ?? '';
194
+ return fileName
195
+ .replace(/\.(page|layout|component)?\.(sdoc|sdocx)$/, '')
196
+ .replace(/\./g, ' ');
197
+ }
@@ -0,0 +1,27 @@
1
+ /** Base64url encode a string (URL-safe, no padding) */
2
+ export declare function base64urlEncode(str: string): string;
3
+ /** Base64url decode */
4
+ export declare function base64urlDecode(str: string): string;
5
+ /** Resolve relative imports to absolute paths for use in virtual components */
6
+ export declare function resolveImportsToAbsolute(imports: string[], docFilePath: string): string[];
7
+ /** Generate a virtual Svelte iframe wrapper component for a snippet.
8
+ * Includes $state for reactive prop updates via postMessage. */
9
+ export declare function generateIframeComponent(absoluteImports: string[], snippetBody: string): string;
10
+ /** Generate the HTML page served inside the iframe */
11
+ export declare function generatePreviewHtml(iframeComponentId: string, css: string | Record<string, string> | null): string;
12
+ /** Build the virtual module ID for an iframe wrapper component */
13
+ export declare function iframeVirtualId(docFilePath: string, snippetName: string): string;
14
+ /** Build the preview URL for an iframe HTML page (dev mode) */
15
+ export declare function previewUrl(docFilePath: string, snippetName: string): string;
16
+ /** Build the preview URL for static build output */
17
+ export declare function buildPreviewUrl(docFilePath: string, snippetName: string): string;
18
+ /** Parse an iframe virtual ID back into its parts */
19
+ export declare function parseIframeId(id: string): {
20
+ docFilePath: string;
21
+ snippetName: string;
22
+ } | null;
23
+ /** Parse a preview URL back into its parts */
24
+ export declare function parsePreviewUrl(url: string): {
25
+ docFilePath: string;
26
+ snippetName: string;
27
+ } | null;