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.
- package/bin/sdocs.js +1 -1
- package/dist/app-gen.d.ts +10 -0
- package/dist/app-gen.js +147 -0
- package/dist/cli.js +71 -0
- package/dist/client/App.svelte +151 -0
- package/dist/client/App.svelte.d.ts +14 -0
- package/dist/client/CollapsiblePanel.svelte +63 -0
- package/dist/client/CollapsiblePanel.svelte.d.ts +9 -0
- package/dist/client/ComponentView.svelte +321 -0
- package/dist/client/ComponentView.svelte.d.ts +10 -0
- package/dist/client/ControlsPanel.svelte +191 -0
- package/dist/client/ControlsPanel.svelte.d.ts +13 -0
- package/dist/client/DataTable.svelte +78 -0
- package/dist/client/DataTable.svelte.d.ts +11 -0
- package/dist/client/HomePage.svelte +92 -0
- package/dist/client/HomePage.svelte.d.ts +8 -0
- package/dist/client/LayoutView.svelte +27 -0
- package/dist/client/LayoutView.svelte.d.ts +8 -0
- package/dist/client/PageView.svelte +130 -0
- package/dist/client/PageView.svelte.d.ts +8 -0
- package/dist/client/PreviewFrame.svelte +100 -0
- package/dist/client/PreviewFrame.svelte.d.ts +10 -0
- package/dist/client/Sidebar.svelte +329 -0
- package/dist/client/Sidebar.svelte.d.ts +16 -0
- package/dist/client/controls/CheckboxControl.svelte +37 -0
- package/dist/client/controls/CheckboxControl.svelte.d.ts +8 -0
- package/dist/client/controls/ColorControl.svelte +47 -0
- package/dist/client/controls/ColorControl.svelte.d.ts +8 -0
- package/dist/client/controls/DimensionControl.svelte +56 -0
- package/dist/client/controls/DimensionControl.svelte.d.ts +8 -0
- package/dist/client/controls/NumberControl.svelte +44 -0
- package/dist/client/controls/NumberControl.svelte.d.ts +8 -0
- package/dist/client/controls/SelectControl.svelte +48 -0
- package/dist/client/controls/SelectControl.svelte.d.ts +9 -0
- package/dist/client/controls/TextControl.svelte +43 -0
- package/dist/client/controls/TextControl.svelte.d.ts +8 -0
- package/dist/client/router.svelte.d.ts +11 -0
- package/dist/client/router.svelte.js +45 -0
- package/dist/client/theme.css +34 -0
- package/dist/client/tree-builder.d.ts +30 -0
- package/dist/client/tree-builder.js +162 -0
- package/dist/commands/build.d.ts +1 -0
- package/dist/commands/build.js +38 -0
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.js +40 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +41 -0
- package/dist/commands/preview.d.ts +1 -0
- package/dist/commands/preview.js +25 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +57 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -4
- package/dist/server/discovery.d.ts +6 -0
- package/dist/server/discovery.js +24 -0
- package/dist/server/highlighter.d.ts +4 -0
- package/dist/server/highlighter.js +31 -0
- package/dist/server/meta-parser.d.ts +11 -0
- package/dist/server/meta-parser.js +107 -0
- package/dist/server/prop-parser.d.ts +5 -0
- package/dist/server/prop-parser.js +275 -0
- package/dist/server/sdocx-parser.d.ts +11 -0
- package/dist/server/sdocx-parser.js +197 -0
- package/dist/server/snippet-compiler.d.ts +27 -0
- package/dist/server/snippet-compiler.js +145 -0
- package/dist/server/snippet-extractor.d.ts +11 -0
- package/dist/server/snippet-extractor.js +37 -0
- package/dist/server/toc-extractor.d.ts +5 -0
- package/dist/server/toc-extractor.js +37 -0
- package/dist/types.d.ts +100 -148
- package/dist/vite.d.ts +5 -2
- package/dist/vite.js +266 -2
- package/package.json +50 -74
- package/README.md +0 -43
- package/dist/Sdocs.svelte +0 -1210
- package/dist/Sdocs.svelte.d.ts +0 -5
- package/dist/cli/app-plugin.d.ts +0 -7
- package/dist/cli/app-plugin.js +0 -69
- package/dist/cli/config.d.ts +0 -12
- package/dist/cli/config.js +0 -34
- package/dist/cli/index.js +0 -72
- package/dist/cli/server.d.ts +0 -2
- package/dist/cli/server.js +0 -64
- package/dist/docgen.d.ts +0 -47
- package/dist/docgen.js +0 -463
- package/dist/internal/ComponentPreview.svelte +0 -58
- package/dist/internal/ComponentPreview.svelte.d.ts +0 -17
- package/dist/internal/CssPropsTable.svelte +0 -239
- package/dist/internal/CssPropsTable.svelte.d.ts +0 -11
- package/dist/internal/Home.svelte +0 -92
- package/dist/internal/Home.svelte.d.ts +0 -9
- package/dist/internal/MethodsTable.svelte +0 -72
- package/dist/internal/MethodsTable.svelte.d.ts +0 -7
- package/dist/internal/PropsTable.svelte +0 -342
- package/dist/internal/PropsTable.svelte.d.ts +0 -12
- package/dist/internal/Showcase.svelte +0 -130
- package/dist/internal/Showcase.svelte.d.ts +0 -21
- package/dist/ui/Badge/Badge.docs.svelte +0 -46
- package/dist/ui/Badge/Badge.docs.svelte.d.ts +0 -26
- package/dist/ui/Badge/Badge.svelte +0 -59
- package/dist/ui/Badge/Badge.svelte.d.ts +0 -17
- package/dist/ui/Badge/index.d.ts +0 -1
- package/dist/ui/Badge/index.js +0 -1
- package/dist/ui/Checkbox/Checkbox.docs.svelte +0 -51
- package/dist/ui/Checkbox/Checkbox.docs.svelte.d.ts +0 -27
- package/dist/ui/Checkbox/Checkbox.svelte +0 -169
- package/dist/ui/Checkbox/Checkbox.svelte.d.ts +0 -18
- package/dist/ui/Checkbox/index.d.ts +0 -1
- package/dist/ui/Checkbox/index.js +0 -1
- package/dist/ui/CodeBlock/CodeBlock.docs.svelte +0 -28
- package/dist/ui/CodeBlock/CodeBlock.docs.svelte.d.ts +0 -24
- package/dist/ui/CodeBlock/CodeBlock.svelte +0 -101
- package/dist/ui/CodeBlock/CodeBlock.svelte.d.ts +0 -7
- package/dist/ui/CodeBlock/index.d.ts +0 -1
- package/dist/ui/CodeBlock/index.js +0 -1
- package/dist/ui/Frame/Frame.docs.svelte +0 -140
- package/dist/ui/Frame/Frame.docs.svelte.d.ts +0 -26
- package/dist/ui/Frame/Frame.svelte +0 -88
- package/dist/ui/Frame/Frame.svelte.d.ts +0 -15
- package/dist/ui/Frame/index.d.ts +0 -1
- package/dist/ui/Frame/index.js +0 -1
- package/dist/ui/InputNumber/InputNumber.docs.svelte +0 -50
- package/dist/ui/InputNumber/InputNumber.docs.svelte.d.ts +0 -26
- package/dist/ui/InputNumber/InputNumber.svelte +0 -275
- package/dist/ui/InputNumber/InputNumber.svelte.d.ts +0 -26
- package/dist/ui/InputNumber/index.d.ts +0 -1
- package/dist/ui/InputNumber/index.js +0 -1
- package/dist/ui/InputText/InputText.docs.svelte +0 -43
- package/dist/ui/InputText/InputText.docs.svelte.d.ts +0 -26
- package/dist/ui/InputText/InputText.svelte +0 -116
- package/dist/ui/InputText/InputText.svelte.d.ts +0 -22
- package/dist/ui/InputText/index.d.ts +0 -1
- package/dist/ui/InputText/index.js +0 -1
- package/dist/ui/Panel/CollapsiblePanel.docs.svelte +0 -45
- package/dist/ui/Panel/CollapsiblePanel.docs.svelte.d.ts +0 -25
- package/dist/ui/Panel/CollapsiblePanel.svelte +0 -93
- package/dist/ui/Panel/CollapsiblePanel.svelte.d.ts +0 -14
- package/dist/ui/Panel/index.d.ts +0 -1
- package/dist/ui/Panel/index.js +0 -1
- package/dist/ui/Placeholder/Placeholder.docs.svelte +0 -49
- package/dist/ui/Placeholder/Placeholder.docs.svelte.d.ts +0 -26
- package/dist/ui/Placeholder/Placeholder.svelte +0 -99
- package/dist/ui/Placeholder/Placeholder.svelte.d.ts +0 -21
- package/dist/ui/Placeholder/index.d.ts +0 -1
- package/dist/ui/Placeholder/index.js +0 -1
- package/dist/ui/Radio/Radio.docs.svelte +0 -67
- package/dist/ui/Radio/Radio.docs.svelte.d.ts +0 -27
- package/dist/ui/Radio/Radio.svelte +0 -165
- package/dist/ui/Radio/Radio.svelte.d.ts +0 -22
- package/dist/ui/Radio/RadioGroup.docs.svelte +0 -70
- package/dist/ui/Radio/RadioGroup.docs.svelte.d.ts +0 -27
- package/dist/ui/Radio/RadioGroup.svelte +0 -98
- package/dist/ui/Radio/RadioGroup.svelte.d.ts +0 -27
- package/dist/ui/Radio/index.d.ts +0 -2
- package/dist/ui/Radio/index.js +0 -2
- package/dist/ui/SegmentControl/SegmentControl.docs.svelte +0 -54
- package/dist/ui/SegmentControl/SegmentControl.docs.svelte.d.ts +0 -25
- package/dist/ui/SegmentControl/SegmentControl.svelte +0 -120
- package/dist/ui/SegmentControl/SegmentControl.svelte.d.ts +0 -18
- package/dist/ui/SegmentControl/index.d.ts +0 -1
- package/dist/ui/SegmentControl/index.js +0 -1
- package/dist/ui/Stack/Stack.docs.svelte +0 -63
- package/dist/ui/Stack/Stack.docs.svelte.d.ts +0 -26
- package/dist/ui/Stack/Stack.svelte +0 -45
- package/dist/ui/Stack/Stack.svelte.d.ts +0 -19
- package/dist/ui/Stack/index.d.ts +0 -1
- package/dist/ui/Stack/index.js +0 -1
- package/dist/ui/Table/Body.svelte +0 -17
- package/dist/ui/Table/Body.svelte.d.ts +0 -11
- package/dist/ui/Table/Caption.svelte +0 -17
- package/dist/ui/Table/Caption.svelte.d.ts +0 -11
- package/dist/ui/Table/Cell.svelte +0 -24
- package/dist/ui/Table/Cell.svelte.d.ts +0 -15
- package/dist/ui/Table/Foot.svelte +0 -17
- package/dist/ui/Table/Foot.svelte.d.ts +0 -11
- package/dist/ui/Table/Head.svelte +0 -17
- package/dist/ui/Table/Head.svelte.d.ts +0 -11
- package/dist/ui/Table/Header.svelte +0 -27
- package/dist/ui/Table/Header.svelte.d.ts +0 -17
- package/dist/ui/Table/Row.svelte +0 -19
- package/dist/ui/Table/Row.svelte.d.ts +0 -13
- package/dist/ui/Table/Table.docs.svelte +0 -197
- package/dist/ui/Table/Table.docs.svelte.d.ts +0 -28
- package/dist/ui/Table/Table.svelte +0 -140
- package/dist/ui/Table/Table.svelte.d.ts +0 -27
- package/dist/ui/Table/index.js +0 -10
- package/dist/ui/css/colors.css +0 -377
- package/dist/ui/css/global.css +0 -10
- package/dist/ui/index.d.ts +0 -12
- package/dist/ui/index.js +0 -12
- package/dist/virtual-sdocs.d.ts +0 -20
- package/dist/vite-plugin.d.ts +0 -18
- package/dist/vite-plugin.js +0 -206
- /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;
|