sdocs 0.0.40 → 0.0.42

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 (35) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/explorer/tree-builder.js +2 -4
  3. package/dist/explorer/views/ComponentView.svelte +85 -29
  4. package/dist/explorer/views/LayoutView.svelte +1 -1
  5. package/dist/explorer/views/PageView.svelte +1 -1
  6. package/dist/grammar/sdoc.tmLanguage.json +245 -0
  7. package/dist/index.d.ts +1 -1
  8. package/dist/language/index.d.ts +2 -0
  9. package/dist/language/index.js +2 -0
  10. package/dist/language/parser.d.ts +82 -0
  11. package/dist/language/parser.js +293 -0
  12. package/dist/language/parser.test.d.ts +1 -0
  13. package/dist/language/parser.test.js +158 -0
  14. package/dist/language/scanner.d.ts +82 -0
  15. package/dist/language/scanner.js +529 -0
  16. package/dist/language/scanner.test.d.ts +1 -0
  17. package/dist/language/scanner.test.js +146 -0
  18. package/dist/server/app-gen.js +18 -21
  19. package/dist/server/discovery.d.ts +0 -2
  20. package/dist/server/discovery.js +0 -9
  21. package/dist/server/doc-model.d.ts +26 -0
  22. package/dist/server/doc-model.js +58 -0
  23. package/dist/server/page-markdown.d.ts +15 -0
  24. package/dist/server/page-markdown.js +104 -0
  25. package/dist/server/snippet-compiler.d.ts +18 -18
  26. package/dist/server/snippet-compiler.js +32 -29
  27. package/dist/types.d.ts +35 -19
  28. package/dist/vite.js +168 -99
  29. package/package.json +7 -1
  30. package/dist/server/meta-parser.d.ts +0 -11
  31. package/dist/server/meta-parser.js +0 -109
  32. package/dist/server/snippet-extractor.d.ts +0 -11
  33. package/dist/server/snippet-extractor.js +0 -83
  34. package/dist/server/toc-extractor.d.ts +0 -3
  35. package/dist/server/toc-extractor.js +0 -23
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, getSdocKind } from './server/discovery.js';
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 { extractSnippets, extractMarkupBody, hasDefaultSnippet, generateAutoDefault, } from './server/snippet-extractor.js';
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 { extractTocFromHtml } from './server/toc-extractor.js';
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.snippets.find((s) => s.name === parsed.snippetName);
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.snippetName);
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
- docEntries.delete(filePath);
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 = encodeDocPath(entry.filePath);
144
- for (const snippet of entry.snippets) {
145
- const jsFileName = `previews/${encoded}/${snippet.name}.js`;
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.name),
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.name}.html`,
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/<doc>/<name>.html — two levels deep.
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.snippets.find((s) => s.name === parsed.snippetName);
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
- const stateNames = (entry.componentData?.state ?? []).map((s) => s.name);
220
- const componentName = entry.componentPath?.split('/').pop()?.replace('.svelte', '');
221
- return generateIframeComponent(absoluteImports, snippet.body, stateNames, componentName);
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.snippetName));
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
- const source = await readFile(filePath, 'utf-8');
238
- const kind = getSdocKind(filePath);
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
- else {
253
- snippets = extractSnippets(source);
254
- if (!hasDefaultSnippet(snippets)) {
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
- // If component is specified as a path but not imported, auto-add the import
263
- if (componentPath) {
264
- const componentName = componentPath.split('/').pop()?.replace('.svelte', '') ?? 'Component';
265
- const hasImport = imports.some((imp) => imp.includes(componentName));
266
- if (!hasImport) {
267
- imports.push(`import ${componentName} from '${componentPath}'`);
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
- // Cache resolved imports for iframe component generation
262
+ deleteEntriesOf(filePath);
263
+ const scriptContent = doc.script?.content ?? '';
264
+ const imports = extractImports(scriptContent);
271
265
  docImportsCache.set(filePath, resolveImportsToAbsolute(imports, filePath));
272
- let componentData = null;
273
- let highlightedSource = null;
274
- if (kind === 'component' && componentPath) {
275
- try {
276
- componentData = await parseComponent(componentPath);
277
- const componentSource = await readFile(componentPath, 'utf-8');
278
- highlightedSource = await highlight(componentSource);
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
- catch (err) {
281
- console.warn(`[sdocs] Failed to parse component: ${componentPath}`, err);
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 entries = Array.from(docEntries.values());
301
- const data = entries.map((e) => ({
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
- componentPath: e.componentPath,
366
+ entitySlug: e.entitySlug,
305
367
  meta: e.meta,
306
- componentData: e.componentData,
307
- snippets: e.snippets.map((s) => ({
308
- name: s.name,
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 = docEntries.get(docFilePath);
339
- if (entry) {
340
- for (const snippet of entry.snippets) {
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
- for (const [docPath, entry] of docEntries) {
372
- if (entry.componentPath === componentPath) {
373
- await processDocFile(docPath);
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.40",
3
+ "version": "0.0.42",
4
4
  "description": "A lightweight documentation tool for Svelte 5 components",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -21,6 +21,11 @@
21
21
  "types": "./dist/vite.d.ts",
22
22
  "default": "./dist/vite.js"
23
23
  },
24
+ "./language": {
25
+ "types": "./dist/language/index.d.ts",
26
+ "default": "./dist/language/index.js"
27
+ },
28
+ "./grammar/sdoc.tmLanguage.json": "./dist/grammar/sdoc.tmLanguage.json",
24
29
  "./explorer": {
25
30
  "svelte": "./dist/explorer/Explorer.svelte"
26
31
  },
@@ -56,6 +61,7 @@
56
61
  "test": "vitest run"
57
62
  },
58
63
  "dependencies": {
64
+ "marked": "^16.4.2",
59
65
  "shiki": "^3.22.0",
60
66
  "tinyglobby": "^0.2.15",
61
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,3 +0,0 @@
1
- import type { TocHeading } from '../types.js';
2
- /** Extract ToC headings from HTML markup (for .sdoc pages) */
3
- export declare function extractTocFromHtml(body: string): TocHeading[];
@@ -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
- }