sdocs 0.0.41 → 0.0.43
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 +42 -0
- package/dist/explorer/tree-builder.js +2 -4
- package/dist/explorer/views/ComponentView.svelte +85 -29
- package/dist/explorer/views/LayoutView.svelte +1 -1
- package/dist/explorer/views/PageView.svelte +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/language/index.d.ts +1 -0
- package/dist/language/index.js +1 -0
- package/dist/language/projection.d.ts +23 -0
- package/dist/language/projection.js +136 -0
- package/dist/language/projection.test.d.ts +1 -0
- package/dist/language/projection.test.js +117 -0
- package/dist/server/app-gen.js +18 -21
- package/dist/server/discovery.d.ts +0 -2
- package/dist/server/discovery.js +0 -9
- package/dist/server/doc-model.d.ts +26 -0
- package/dist/server/doc-model.js +58 -0
- package/dist/server/page-markdown.d.ts +15 -0
- package/dist/server/page-markdown.js +104 -0
- package/dist/server/snippet-compiler.d.ts +18 -18
- package/dist/server/snippet-compiler.js +32 -29
- package/dist/types.d.ts +35 -19
- package/dist/ui/About.sdoc +28 -0
- package/dist/ui/Button/Button.sdoc +41 -44
- package/dist/ui/Control/Checkbox.sdoc +7 -13
- package/dist/ui/Control/Color.sdoc +7 -13
- package/dist/ui/Control/Dimension.sdoc +7 -13
- package/dist/ui/Control/Number.sdoc +7 -13
- package/dist/ui/Control/Select.sdoc +7 -14
- package/dist/ui/Control/Text.sdoc +7 -13
- package/dist/ui/Dashboard.sdoc +57 -0
- package/dist/ui/Frame/Frame.sdoc +46 -48
- package/dist/ui/Icon/Icon.sdoc +33 -38
- package/dist/ui/NavTree/Group.sdoc +36 -38
- package/dist/ui/NavTree/Item.sdoc +52 -57
- package/dist/ui/NavTree/NavTree.sdoc +89 -91
- package/dist/ui/Stack/Stack.sdoc +41 -43
- package/dist/vite.js +168 -99
- package/package.json +2 -1
- package/dist/server/meta-parser.d.ts +0 -11
- package/dist/server/meta-parser.js +0 -109
- package/dist/server/snippet-extractor.d.ts +0 -11
- package/dist/server/snippet-extractor.js +0 -83
- package/dist/server/toc-extractor.d.ts +0 -3
- package/dist/server/toc-extractor.js +0 -23
- package/dist/ui/About.page.sdoc +0 -37
- package/dist/ui/Dashboard.layout.sdoc +0 -60
package/dist/vite.js
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { loadRawConfig, resolveAndFinalize } from './server/config.js';
|
|
3
|
-
import { discoverDocFiles
|
|
4
|
-
import { parseDocSource } from './server/meta-parser.js';
|
|
3
|
+
import { discoverDocFiles } from './server/discovery.js';
|
|
5
4
|
import { parseComponent } from './server/prop-parser.js';
|
|
6
|
-
import {
|
|
5
|
+
import { parseSdoc, offsetToPosition } from './language/index.js';
|
|
6
|
+
import { planEntitySnippets, extractImports, resolveComponentImport, } from './server/doc-model.js';
|
|
7
|
+
import { renderPageMarkdown } from './server/page-markdown.js';
|
|
7
8
|
import { highlight, disposeHighlighter } from './server/highlighter.js';
|
|
8
|
-
import {
|
|
9
|
-
import { parseIframeId, parseMountId, parsePreviewUrl, resolveImportsToAbsolute, generateIframeComponent, generateMountScript, generatePreviewHtml, generateStaticPreviewHtml, iframeVirtualId, mountVirtualId, previewUrl, buildPreviewUrl, encodeDocPath, setDocPathRoot, } from './server/snippet-compiler.js';
|
|
9
|
+
import { parseIframeId, parseMountId, parsePreviewUrl, resolveImportsToAbsolute, generateIframeComponent, generateMountScript, generatePreviewHtml, generateStaticPreviewHtml, iframeVirtualId, mountVirtualId, previewUrl, buildPreviewUrl, encodeEntityId, entityKey, setDocPathRoot, } from './server/snippet-compiler.js';
|
|
10
10
|
const VIRTUAL_MODULE_ID = 'virtual:sdocs';
|
|
11
11
|
const RESOLVED_VIRTUAL_ID = '\0virtual:sdocs';
|
|
12
12
|
const IFRAME_PREFIX = '/@sdocs/iframe/';
|
|
13
13
|
const PREVIEW_PREFIX = '/@sdocs/preview/';
|
|
14
14
|
const MOUNT_PREFIX = '/@sdocs/mount/';
|
|
15
|
+
/** Every renderable snippet of an entry, in order. */
|
|
16
|
+
function allSnippets(entry) {
|
|
17
|
+
return [
|
|
18
|
+
...entry.previews.map((p) => p.snippet),
|
|
19
|
+
...entry.examples,
|
|
20
|
+
...(entry.content ? [entry.content] : []),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
15
23
|
export function sdocsPlugin(userConfig) {
|
|
16
24
|
let config;
|
|
17
25
|
let root;
|
|
@@ -48,19 +56,19 @@ export function sdocsPlugin(userConfig) {
|
|
|
48
56
|
const parsed = parsePreviewUrl(req.url);
|
|
49
57
|
if (!parsed)
|
|
50
58
|
return next();
|
|
51
|
-
const entry = docEntries.get(parsed.docFilePath);
|
|
59
|
+
const entry = docEntries.get(entityKey(parsed.docFilePath, parsed.entitySlug));
|
|
52
60
|
if (!entry) {
|
|
53
61
|
res.statusCode = 404;
|
|
54
62
|
res.end('Doc not found');
|
|
55
63
|
return;
|
|
56
64
|
}
|
|
57
|
-
const snippet = entry.
|
|
65
|
+
const snippet = allSnippets(entry).find((s) => s.slug === parsed.snippetSlug);
|
|
58
66
|
if (!snippet) {
|
|
59
67
|
res.statusCode = 404;
|
|
60
68
|
res.end('Snippet not found');
|
|
61
69
|
return;
|
|
62
70
|
}
|
|
63
|
-
const iframeId = iframeVirtualId(parsed.docFilePath, parsed.
|
|
71
|
+
const iframeId = iframeVirtualId(parsed.docFilePath, parsed.entitySlug, snippet.slug);
|
|
64
72
|
const html = generatePreviewHtml(iframeId, config.css);
|
|
65
73
|
res.setHeader('Content-Type', 'text/html');
|
|
66
74
|
// Let Vite transform the HTML (resolves imports, injects HMR client)
|
|
@@ -83,7 +91,7 @@ export function sdocsPlugin(userConfig) {
|
|
|
83
91
|
server.watcher.on('unlink', (filePath) => {
|
|
84
92
|
if (isDocFile(filePath)) {
|
|
85
93
|
console.log(`[sdocs] Removed doc file: ${filePath}`);
|
|
86
|
-
|
|
94
|
+
deleteEntriesOf(filePath);
|
|
87
95
|
docImportsCache.delete(filePath);
|
|
88
96
|
invalidateVirtualModule();
|
|
89
97
|
}
|
|
@@ -140,17 +148,17 @@ export function sdocsPlugin(userConfig) {
|
|
|
140
148
|
}
|
|
141
149
|
}
|
|
142
150
|
for (const entry of docEntries.values()) {
|
|
143
|
-
const encoded =
|
|
144
|
-
for (const snippet of entry
|
|
145
|
-
const jsFileName = `previews/${encoded}/${snippet.
|
|
151
|
+
const encoded = encodeEntityId(entry.filePath, entry.entitySlug);
|
|
152
|
+
for (const snippet of allSnippets(entry)) {
|
|
153
|
+
const jsFileName = `previews/${encoded}/${snippet.slug}.js`;
|
|
146
154
|
this.emitFile({
|
|
147
155
|
type: 'chunk',
|
|
148
|
-
id: mountVirtualId(entry.filePath, snippet.
|
|
156
|
+
id: mountVirtualId(entry.filePath, entry.entitySlug, snippet.slug),
|
|
149
157
|
fileName: jsFileName,
|
|
150
158
|
});
|
|
151
159
|
plannedPreviews.push({
|
|
152
160
|
jsFileName,
|
|
153
|
-
htmlFileName: `previews/${encoded}/${snippet.
|
|
161
|
+
htmlFileName: `previews/${encoded}/${snippet.slug}.html`,
|
|
154
162
|
});
|
|
155
163
|
}
|
|
156
164
|
}
|
|
@@ -178,7 +186,7 @@ export function sdocsPlugin(userConfig) {
|
|
|
178
186
|
cssFiles.add(css);
|
|
179
187
|
queue.push(...mod.imports);
|
|
180
188
|
}
|
|
181
|
-
// The HTML sits at previews/<
|
|
189
|
+
// The HTML sits at previews/<entity>/<name>.html — two levels deep.
|
|
182
190
|
const cssLinks = [
|
|
183
191
|
...emittedCssLinks,
|
|
184
192
|
...[...cssFiles].map((file) => ({ href: `../../${file}` })),
|
|
@@ -209,23 +217,25 @@ export function sdocsPlugin(userConfig) {
|
|
|
209
217
|
const parsed = parseIframeId(realId);
|
|
210
218
|
if (!parsed)
|
|
211
219
|
return null;
|
|
212
|
-
const entry = docEntries.get(parsed.docFilePath);
|
|
220
|
+
const entry = docEntries.get(entityKey(parsed.docFilePath, parsed.entitySlug));
|
|
213
221
|
if (!entry)
|
|
214
222
|
return null;
|
|
215
|
-
const snippet = entry.
|
|
223
|
+
const snippet = allSnippets(entry).find((s) => s.slug === parsed.snippetSlug);
|
|
216
224
|
if (!snippet)
|
|
217
225
|
return null;
|
|
218
226
|
const absoluteImports = docImportsCache.get(parsed.docFilePath) ?? [];
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
227
|
+
// Method calls and live state bind to the snippet's own preview;
|
|
228
|
+
// example iframes fall back to the first preview's component.
|
|
229
|
+
const preview = entry.previews.find((p) => p.snippet.slug === snippet.slug) ?? entry.previews[0];
|
|
230
|
+
const stateNames = (preview?.componentData?.state ?? []).map((s) => s.name);
|
|
231
|
+
return generateIframeComponent(absoluteImports, snippet.body, stateNames, preview?.componentName ?? undefined);
|
|
222
232
|
}
|
|
223
233
|
// Virtual mount script for an emitted preview page
|
|
224
234
|
if (id.startsWith('\0' + MOUNT_PREFIX)) {
|
|
225
235
|
const parsed = parseMountId(id.slice(1));
|
|
226
236
|
if (!parsed)
|
|
227
237
|
return null;
|
|
228
|
-
return generateMountScript(iframeVirtualId(parsed.docFilePath, parsed.
|
|
238
|
+
return generateMountScript(iframeVirtualId(parsed.docFilePath, parsed.entitySlug, parsed.snippetSlug));
|
|
229
239
|
}
|
|
230
240
|
},
|
|
231
241
|
async buildEnd() {
|
|
@@ -234,90 +244,132 @@ export function sdocsPlugin(userConfig) {
|
|
|
234
244
|
};
|
|
235
245
|
// ─── Process a single doc file ───
|
|
236
246
|
async function processDocFile(filePath) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const parsed = parseDocSource(source, filePath);
|
|
240
|
-
const meta = parsed.meta;
|
|
241
|
-
const componentPath = parsed.componentPath;
|
|
242
|
-
const imports = parsed.imports;
|
|
243
|
-
let snippets;
|
|
244
|
-
let toc;
|
|
245
|
-
if (kind === 'page' || kind === 'layout') {
|
|
246
|
-
const body = extractMarkupBody(source);
|
|
247
|
-
snippets = [{ name: 'Content', body }];
|
|
248
|
-
if (kind === 'page') {
|
|
249
|
-
toc = extractTocFromHtml(body);
|
|
250
|
-
}
|
|
247
|
+
try {
|
|
248
|
+
await processDocFileInner(filePath);
|
|
251
249
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const componentName = componentPath?.split('/').pop()?.replace('.svelte', '') ?? 'Component';
|
|
256
|
-
snippets.unshift({
|
|
257
|
-
name: 'Default',
|
|
258
|
-
body: generateAutoDefault(componentName),
|
|
259
|
-
});
|
|
260
|
-
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
// A half-written file must never kill the dev server.
|
|
252
|
+
console.warn(`[sdocs] Failed to process ${filePath}:`, err);
|
|
261
253
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
254
|
+
}
|
|
255
|
+
async function processDocFileInner(filePath) {
|
|
256
|
+
const source = await readFile(filePath, 'utf-8');
|
|
257
|
+
const doc = parseSdoc(source);
|
|
258
|
+
for (const d of doc.diagnostics) {
|
|
259
|
+
const pos = offsetToPosition(source, d.span.start);
|
|
260
|
+
console.warn(`[sdocs] ${filePath}:${pos.line + 1}:${pos.column + 1} — ${d.message}`);
|
|
269
261
|
}
|
|
270
|
-
|
|
262
|
+
deleteEntriesOf(filePath);
|
|
263
|
+
const scriptContent = doc.script?.content ?? '';
|
|
264
|
+
const imports = extractImports(scriptContent);
|
|
271
265
|
docImportsCache.set(filePath, resolveImportsToAbsolute(imports, filePath));
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
266
|
+
// One component parse per component file per rebuild, however many
|
|
267
|
+
// previews reference it.
|
|
268
|
+
const componentCache = new Map();
|
|
269
|
+
const loadComponent = async (componentPath) => {
|
|
270
|
+
let cached = componentCache.get(componentPath);
|
|
271
|
+
if (!cached) {
|
|
272
|
+
cached = { data: null, highlighted: null };
|
|
273
|
+
try {
|
|
274
|
+
cached.data = await parseComponent(componentPath);
|
|
275
|
+
const componentSource = await readFile(componentPath, 'utf-8');
|
|
276
|
+
cached.highlighted = await highlight(componentSource);
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
console.warn(`[sdocs] Failed to parse component: ${componentPath}`, err);
|
|
280
|
+
}
|
|
281
|
+
componentCache.set(componentPath, cached);
|
|
282
|
+
}
|
|
283
|
+
return cached;
|
|
284
|
+
};
|
|
285
|
+
for (const entity of doc.entities) {
|
|
286
|
+
const planned = planEntitySnippets(entity);
|
|
287
|
+
const snippets = planned.map((p) => ({
|
|
288
|
+
name: p.name,
|
|
289
|
+
slug: p.slug,
|
|
290
|
+
role: p.role,
|
|
291
|
+
body: p.body,
|
|
292
|
+
}));
|
|
293
|
+
const entry = {
|
|
294
|
+
kind: entity.kind === 'DOCS' ? 'component' : entity.kind === 'PAGE' ? 'page' : 'layout',
|
|
295
|
+
filePath,
|
|
296
|
+
entitySlug: entity.slug,
|
|
297
|
+
meta: { title: entity.title },
|
|
298
|
+
previews: [],
|
|
299
|
+
examples: [],
|
|
300
|
+
content: null,
|
|
301
|
+
};
|
|
302
|
+
if (entity.kind === 'DOCS') {
|
|
303
|
+
if (entity.description)
|
|
304
|
+
entry.meta.description = entity.description;
|
|
305
|
+
for (const [i, preview] of entity.previews.entries()) {
|
|
306
|
+
const snippet = snippets[i];
|
|
307
|
+
let componentPath = null;
|
|
308
|
+
let componentData = null;
|
|
309
|
+
let highlightedSource = null;
|
|
310
|
+
if (preview.componentName) {
|
|
311
|
+
componentPath = resolveComponentImport(preview.componentName, imports, filePath);
|
|
312
|
+
if (componentPath) {
|
|
313
|
+
const loaded = await loadComponent(componentPath);
|
|
314
|
+
componentData = loaded.data;
|
|
315
|
+
highlightedSource = loaded.highlighted;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
console.warn(`[sdocs] ${filePath}: component {${preview.componentName}} is not imported in the file's <script>`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
snippet.highlightedHtml = await highlight(snippet.body);
|
|
322
|
+
entry.previews.push({
|
|
323
|
+
label: preview.label,
|
|
324
|
+
componentName: preview.componentName,
|
|
325
|
+
componentPath,
|
|
326
|
+
componentData,
|
|
327
|
+
highlightedSource,
|
|
328
|
+
args: preview.args ?? {},
|
|
329
|
+
snippet,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
entry.examples = snippets.filter((s) => s.role === 'example');
|
|
333
|
+
for (const example of entry.examples) {
|
|
334
|
+
example.highlightedHtml = await highlight(example.body);
|
|
335
|
+
}
|
|
279
336
|
}
|
|
280
|
-
|
|
281
|
-
|
|
337
|
+
else if (entity.kind === 'PAGE') {
|
|
338
|
+
const rendered = await renderPageMarkdown(entity.body);
|
|
339
|
+
snippets[0].body = rendered.html;
|
|
340
|
+
entry.content = snippets[0];
|
|
341
|
+
entry.toc = rendered.toc;
|
|
282
342
|
}
|
|
343
|
+
else {
|
|
344
|
+
entry.content = snippets[0];
|
|
345
|
+
entry.padding = entity.padding;
|
|
346
|
+
}
|
|
347
|
+
docEntries.set(entityKey(filePath, entity.slug), entry);
|
|
283
348
|
}
|
|
284
|
-
for (const snippet of snippets) {
|
|
285
|
-
snippet.highlightedHtml = await highlight(snippet.body);
|
|
286
|
-
}
|
|
287
|
-
docEntries.set(filePath, {
|
|
288
|
-
kind,
|
|
289
|
-
filePath,
|
|
290
|
-
componentPath,
|
|
291
|
-
meta,
|
|
292
|
-
componentData,
|
|
293
|
-
snippets,
|
|
294
|
-
highlightedSource,
|
|
295
|
-
toc,
|
|
296
|
-
});
|
|
297
349
|
}
|
|
298
350
|
// ─── Generate the virtual module ───
|
|
299
351
|
function generateVirtualModule() {
|
|
300
|
-
const
|
|
301
|
-
|
|
352
|
+
const urlFor = (entry, snippet) =>
|
|
353
|
+
// Only absolute bases prefix here; SvelteKit builds with a relative
|
|
354
|
+
// base ('./') and passes its real path via the Explorer's previewBase.
|
|
355
|
+
(base.startsWith('/') && base !== '/' ? base.replace(/\/$/, '') : '') +
|
|
356
|
+
(buildMode || isBuild
|
|
357
|
+
? buildPreviewUrl(entry.filePath, entry.entitySlug, snippet.slug)
|
|
358
|
+
: previewUrl(entry.filePath, entry.entitySlug, snippet.slug));
|
|
359
|
+
const withUrl = (entry, snippet) => ({
|
|
360
|
+
...snippet,
|
|
361
|
+
previewUrl: urlFor(entry, snippet),
|
|
362
|
+
});
|
|
363
|
+
const data = Array.from(docEntries.values()).map((e) => ({
|
|
302
364
|
kind: e.kind,
|
|
303
365
|
filePath: e.filePath,
|
|
304
|
-
|
|
366
|
+
entitySlug: e.entitySlug,
|
|
305
367
|
meta: e.meta,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
body: s.body,
|
|
310
|
-
highlightedHtml: s.highlightedHtml,
|
|
311
|
-
previewUrl:
|
|
312
|
-
// Only absolute bases prefix here; SvelteKit builds with a relative
|
|
313
|
-
// base ('./') and passes its real path via the Explorer's previewBase.
|
|
314
|
-
(base.startsWith('/') && base !== '/' ? base.replace(/\/$/, '') : '') +
|
|
315
|
-
(buildMode || isBuild
|
|
316
|
-
? buildPreviewUrl(e.filePath, s.name)
|
|
317
|
-
: previewUrl(e.filePath, s.name)),
|
|
318
|
-
})),
|
|
319
|
-
highlightedSource: e.highlightedSource,
|
|
368
|
+
previews: e.previews.map((p) => ({ ...p, snippet: withUrl(e, p.snippet) })),
|
|
369
|
+
examples: e.examples.map((s) => withUrl(e, s)),
|
|
370
|
+
content: e.content ? withUrl(e, e.content) : null,
|
|
320
371
|
toc: e.toc,
|
|
372
|
+
padding: e.padding,
|
|
321
373
|
}));
|
|
322
374
|
// Extract named CSS stylesheet names (empty array if single string or null)
|
|
323
375
|
const cssNames = config.css && typeof config.css === 'object'
|
|
@@ -335,10 +387,9 @@ export function sdocsPlugin(userConfig) {
|
|
|
335
387
|
}
|
|
336
388
|
// Also invalidate iframe virtual modules for the changed doc file
|
|
337
389
|
if (docFilePath) {
|
|
338
|
-
const entry
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const iframeId = '\0' + iframeVirtualId(docFilePath, snippet.name);
|
|
390
|
+
for (const entry of entriesOf(docFilePath)) {
|
|
391
|
+
for (const snippet of allSnippets(entry)) {
|
|
392
|
+
const iframeId = '\0' + iframeVirtualId(docFilePath, entry.entitySlug, snippet.slug);
|
|
342
393
|
const iframeMod = server.moduleGraph.getModuleById(iframeId);
|
|
343
394
|
if (iframeMod) {
|
|
344
395
|
server.moduleGraph.invalidateModule(iframeMod);
|
|
@@ -351,6 +402,20 @@ export function sdocsPlugin(userConfig) {
|
|
|
351
402
|
function isDocFile(filePath) {
|
|
352
403
|
return filePath.endsWith('.sdoc');
|
|
353
404
|
}
|
|
405
|
+
function entriesOf(filePath) {
|
|
406
|
+
const entries = [];
|
|
407
|
+
for (const [key, entry] of docEntries) {
|
|
408
|
+
if (key.startsWith(filePath + '#'))
|
|
409
|
+
entries.push(entry);
|
|
410
|
+
}
|
|
411
|
+
return entries;
|
|
412
|
+
}
|
|
413
|
+
function deleteEntriesOf(filePath) {
|
|
414
|
+
for (const key of [...docEntries.keys()]) {
|
|
415
|
+
if (key.startsWith(filePath + '#'))
|
|
416
|
+
docEntries.delete(key);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
354
419
|
/** Emit a user stylesheet as a build asset; returns its href relative to a preview page. */
|
|
355
420
|
async function emitCssAsset(ctx, href, name) {
|
|
356
421
|
if (href.startsWith('http'))
|
|
@@ -362,16 +427,20 @@ export function sdocsPlugin(userConfig) {
|
|
|
362
427
|
}
|
|
363
428
|
function isComponentReferencedByDoc(filePath) {
|
|
364
429
|
for (const entry of docEntries.values()) {
|
|
365
|
-
if (entry.componentPath === filePath)
|
|
430
|
+
if (entry.previews.some((p) => p.componentPath === filePath))
|
|
366
431
|
return true;
|
|
367
432
|
}
|
|
368
433
|
return false;
|
|
369
434
|
}
|
|
370
435
|
async function reprocessComponentEntries(componentPath) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
436
|
+
const files = new Set();
|
|
437
|
+
for (const entry of docEntries.values()) {
|
|
438
|
+
if (entry.previews.some((p) => p.componentPath === componentPath)) {
|
|
439
|
+
files.add(entry.filePath);
|
|
374
440
|
}
|
|
375
441
|
}
|
|
442
|
+
for (const file of files) {
|
|
443
|
+
await processDocFile(file);
|
|
444
|
+
}
|
|
376
445
|
}
|
|
377
446
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sdocs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.43",
|
|
4
4
|
"description": "A lightweight documentation tool for Svelte 5 components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"test": "vitest run"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
+
"marked": "^16.4.2",
|
|
64
65
|
"shiki": "^3.22.0",
|
|
65
66
|
"tinyglobby": "^0.2.15",
|
|
66
67
|
"typescript": "^5.7.0"
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { SdocMeta } from '../types.js';
|
|
2
|
-
interface MetaParseResult {
|
|
3
|
-
meta: SdocMeta;
|
|
4
|
-
componentPath: string | null;
|
|
5
|
-
imports: string[];
|
|
6
|
-
}
|
|
7
|
-
/** Extract meta and imports from a .sdoc file */
|
|
8
|
-
export declare function parseDocFile(filePath: string): Promise<MetaParseResult>;
|
|
9
|
-
/** Parse meta from .sdoc source */
|
|
10
|
-
export declare function parseDocSource(source: string, filePath: string): MetaParseResult;
|
|
11
|
-
export {};
|
|
@@ -1,109 +0,0 @@
|
|
|
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
|
-
const componentValue = typeof meta.component === 'string' ? meta.component : null;
|
|
89
|
-
if (!componentValue)
|
|
90
|
-
return null;
|
|
91
|
-
// If component is a relative path (e.g. './Button.svelte'), resolve directly
|
|
92
|
-
if (componentValue.startsWith('./') || componentValue.startsWith('../')) {
|
|
93
|
-
return resolve(dirname(docFilePath), componentValue);
|
|
94
|
-
}
|
|
95
|
-
// Otherwise it's an identifier (e.g. 'Button') — find matching import
|
|
96
|
-
for (const imp of imports) {
|
|
97
|
-
const match = imp.match(new RegExp(`import\\s+${componentValue}\\s+from\\s+['"](.+?)['"]`));
|
|
98
|
-
if (match) {
|
|
99
|
-
const importPath = match[1];
|
|
100
|
-
return resolve(dirname(docFilePath), importPath);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
/** Guess a title from the file path */
|
|
106
|
-
function guessTitle(filePath) {
|
|
107
|
-
const fileName = filePath.split('/').pop() ?? '';
|
|
108
|
-
return fileName.replace(/\.sdoc$/, '').replace(/\./g, ' ');
|
|
109
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { ExtractedSnippet } from '../types.js';
|
|
2
|
-
/** Extract top-level {#snippet} blocks from .sdoc source, handling nested snippets */
|
|
3
|
-
export declare function extractSnippets(source: string): ExtractedSnippet[];
|
|
4
|
-
/** Check if a Default snippet exists or needs auto-generation */
|
|
5
|
-
export declare function hasDefaultSnippet(snippets: ExtractedSnippet[]): boolean;
|
|
6
|
-
/** Generate an auto-generated Default snippet body */
|
|
7
|
-
export declare function generateAutoDefault(componentName: string): string;
|
|
8
|
-
/** Extract the markup body of a .sdoc file (everything outside <script> and <style>) */
|
|
9
|
-
export declare function extractMarkupBody(source: string): string;
|
|
10
|
-
/** Get the snippet names in order (Default first, then alphabetical) */
|
|
11
|
-
export declare function getSnippetOrder(snippets: ExtractedSnippet[]): string[];
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/** Extract top-level {#snippet} blocks from .sdoc source, handling nested snippets */
|
|
2
|
-
export function extractSnippets(source) {
|
|
3
|
-
const snippets = [];
|
|
4
|
-
// Strip <script> and <style> to search only markup
|
|
5
|
-
const markup = source
|
|
6
|
-
.replace(/<script[^>]*>[\s\S]*?<\/script>/g, '')
|
|
7
|
-
.replace(/<style[^>]*>[\s\S]*?<\/style>/g, '');
|
|
8
|
-
const openRegex = /\{#snippet\s+(\w+)\s*\([^)]*\)\}/g;
|
|
9
|
-
let match;
|
|
10
|
-
let searchFrom = 0;
|
|
11
|
-
while (true) {
|
|
12
|
-
openRegex.lastIndex = searchFrom;
|
|
13
|
-
match = openRegex.exec(markup);
|
|
14
|
-
if (!match)
|
|
15
|
-
break;
|
|
16
|
-
const name = match[1];
|
|
17
|
-
const bodyStart = match.index + match[0].length;
|
|
18
|
-
// Count nesting depth to find the matching {/snippet}
|
|
19
|
-
let depth = 1;
|
|
20
|
-
let i = bodyStart;
|
|
21
|
-
while (i < markup.length && depth > 0) {
|
|
22
|
-
if (markup.startsWith('{/snippet}', i)) {
|
|
23
|
-
depth--;
|
|
24
|
-
if (depth === 0)
|
|
25
|
-
break;
|
|
26
|
-
i += '{/snippet}'.length;
|
|
27
|
-
}
|
|
28
|
-
else if (markup.startsWith('{#snippet', i)) {
|
|
29
|
-
depth++;
|
|
30
|
-
const closeBrace = markup.indexOf('}', i);
|
|
31
|
-
i = closeBrace !== -1 ? closeBrace + 1 : i + 1;
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
i++;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
if (depth === 0) {
|
|
38
|
-
const body = dedent(markup.slice(bodyStart, i).trim());
|
|
39
|
-
snippets.push({ name, body });
|
|
40
|
-
searchFrom = i + '{/snippet}'.length;
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
break; // malformed source, stop
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return snippets;
|
|
47
|
-
}
|
|
48
|
-
/** Remove common leading whitespace from all lines */
|
|
49
|
-
function dedent(text) {
|
|
50
|
-
const lines = text.split('\n');
|
|
51
|
-
// Skip first line (already trimmed) when computing minimum indent
|
|
52
|
-
const rest = lines.slice(1).filter((l) => l.trim().length > 0);
|
|
53
|
-
const indents = rest.map((l) => l.match(/^(\s*)/)?.[1].length ?? 0);
|
|
54
|
-
const min = indents.length > 0 ? Math.min(...indents) : 0;
|
|
55
|
-
if (min === 0)
|
|
56
|
-
return text;
|
|
57
|
-
return [lines[0], ...lines.slice(1).map((l) => l.slice(min))].join('\n');
|
|
58
|
-
}
|
|
59
|
-
/** Check if a Default snippet exists or needs auto-generation */
|
|
60
|
-
export function hasDefaultSnippet(snippets) {
|
|
61
|
-
return snippets.some((s) => s.name === 'Default');
|
|
62
|
-
}
|
|
63
|
-
/** Generate an auto-generated Default snippet body */
|
|
64
|
-
export function generateAutoDefault(componentName) {
|
|
65
|
-
return `<${componentName} {...args} />`;
|
|
66
|
-
}
|
|
67
|
-
/** Extract the markup body of a .sdoc file (everything outside <script> and <style>) */
|
|
68
|
-
export function extractMarkupBody(source) {
|
|
69
|
-
return source
|
|
70
|
-
.replace(/<script[^>]*>[\s\S]*?<\/script>/g, '')
|
|
71
|
-
.replace(/<style[^>]*>[\s\S]*?<\/style>/g, '')
|
|
72
|
-
.trim();
|
|
73
|
-
}
|
|
74
|
-
/** Get the snippet names in order (Default first, then alphabetical) */
|
|
75
|
-
export function getSnippetOrder(snippets) {
|
|
76
|
-
const names = snippets.map((s) => s.name);
|
|
77
|
-
const defaultIndex = names.indexOf('Default');
|
|
78
|
-
if (defaultIndex > 0) {
|
|
79
|
-
names.splice(defaultIndex, 1);
|
|
80
|
-
names.unshift('Default');
|
|
81
|
-
}
|
|
82
|
-
return names;
|
|
83
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/** Slugify a heading text into an ID */
|
|
2
|
-
function slugify(text) {
|
|
3
|
-
return text
|
|
4
|
-
.toLowerCase()
|
|
5
|
-
.replace(/[^\w\s-]/g, '')
|
|
6
|
-
.replace(/\s+/g, '-')
|
|
7
|
-
.replace(/-+/g, '-')
|
|
8
|
-
.trim();
|
|
9
|
-
}
|
|
10
|
-
/** Extract ToC headings from HTML markup (for .sdoc pages) */
|
|
11
|
-
export function extractTocFromHtml(body) {
|
|
12
|
-
const headings = [];
|
|
13
|
-
const regex = /<h([2-4])[^>]*>([\s\S]*?)<\/h[2-4]>/gi;
|
|
14
|
-
let match;
|
|
15
|
-
while ((match = regex.exec(body)) !== null) {
|
|
16
|
-
const level = parseInt(match[1]);
|
|
17
|
-
const text = match[2].replace(/<[^>]*>/g, '').trim();
|
|
18
|
-
if (text) {
|
|
19
|
-
headings.push({ text, level, id: slugify(text) });
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return headings;
|
|
23
|
-
}
|