sdocs 0.0.41 → 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.
@@ -15,13 +15,25 @@ let docPathRoot = process.cwd();
15
15
  export function setDocPathRoot(root) {
16
16
  docPathRoot = root;
17
17
  }
18
- /** Encode a doc file path for use in URLs and emitted file names */
19
- export function encodeDocPath(filePath) {
20
- return base64urlEncode(relative(docPathRoot, filePath).split(sep).join('/'));
18
+ /** Encode a doc entity (file + entity slug) for URLs and emitted file names.
19
+ * The slug rides inside the token as a '#' fragment so every URL shape keeps
20
+ * exactly one token path segment. */
21
+ export function encodeEntityId(filePath, entitySlug) {
22
+ return base64urlEncode(relative(docPathRoot, filePath).split(sep).join('/') + '#' + entitySlug);
21
23
  }
22
- /** Decode an encoded doc path back to an absolute path */
23
- function decodeDocPath(encoded) {
24
- return resolve(docPathRoot, base64urlDecode(encoded));
24
+ /** Decode an encoded entity id back to an absolute path + entity slug */
25
+ function decodeEntityId(encoded) {
26
+ const decoded = base64urlDecode(encoded);
27
+ const hash = decoded.lastIndexOf('#');
28
+ const relPath = hash === -1 ? decoded : decoded.slice(0, hash);
29
+ return {
30
+ docFilePath: resolve(docPathRoot, relPath),
31
+ entitySlug: hash === -1 ? '' : decoded.slice(hash + 1),
32
+ };
33
+ }
34
+ /** The docEntries key for one entity of one file */
35
+ export function entityKey(filePath, entitySlug) {
36
+ return `${filePath}#${entitySlug}`;
25
37
  }
26
38
  /** Resolve relative imports to absolute paths for use in virtual components */
27
39
  export function resolveImportsToAbsolute(imports, docFilePath) {
@@ -206,48 +218,39 @@ export function generateStaticPreviewHtml(scriptSrc, cssLinks) {
206
218
  return previewHtmlShell(links, `<script type="module" src="${scriptSrc}"></script>`);
207
219
  }
208
220
  /** Build the virtual module ID for an iframe wrapper component */
209
- export function iframeVirtualId(docFilePath, snippetName) {
210
- return `/@sdocs/iframe/${encodeDocPath(docFilePath)}/${snippetName}.svelte`;
221
+ export function iframeVirtualId(docFilePath, entitySlug, snippetSlug) {
222
+ return `/@sdocs/iframe/${encodeEntityId(docFilePath, entitySlug)}/${snippetSlug}.svelte`;
211
223
  }
212
224
  /** Build the preview URL for an iframe HTML page (dev mode) */
213
- export function previewUrl(docFilePath, snippetName) {
214
- return `/@sdocs/preview/${encodeDocPath(docFilePath)}/${snippetName}`;
225
+ export function previewUrl(docFilePath, entitySlug, snippetSlug) {
226
+ return `/@sdocs/preview/${encodeEntityId(docFilePath, entitySlug)}/${snippetSlug}`;
215
227
  }
216
228
  /** Build the preview URL for static build output */
217
- export function buildPreviewUrl(docFilePath, snippetName) {
218
- return `/previews/${encodeDocPath(docFilePath)}/${snippetName}.html`;
229
+ export function buildPreviewUrl(docFilePath, entitySlug, snippetSlug) {
230
+ return `/previews/${encodeEntityId(docFilePath, entitySlug)}/${snippetSlug}.html`;
219
231
  }
220
232
  /** Virtual module ID for a preview's mount script (embedded production builds) */
221
- export function mountVirtualId(docFilePath, snippetName) {
222
- return `/@sdocs/mount/${encodeDocPath(docFilePath)}/${snippetName}.js`;
233
+ export function mountVirtualId(docFilePath, entitySlug, snippetSlug) {
234
+ return `/@sdocs/mount/${encodeEntityId(docFilePath, entitySlug)}/${snippetSlug}.js`;
223
235
  }
224
236
  /** Parse a mount virtual ID back into its parts */
225
237
  export function parseMountId(id) {
226
- const match = id.match(/^\/@sdocs\/mount\/([^/]+)\/(\w+)\.js$/);
238
+ const match = id.match(/^\/@sdocs\/mount\/([^/]+)\/([\w-]+)\.js$/);
227
239
  if (!match)
228
240
  return null;
229
- return {
230
- docFilePath: decodeDocPath(match[1]),
231
- snippetName: match[2],
232
- };
241
+ return { ...decodeEntityId(match[1]), snippetSlug: match[2] };
233
242
  }
234
243
  /** Parse an iframe virtual ID back into its parts */
235
244
  export function parseIframeId(id) {
236
- const match = id.match(/^\/@sdocs\/iframe\/([^/]+)\/(\w+)\.svelte$/);
245
+ const match = id.match(/^\/@sdocs\/iframe\/([^/]+)\/([\w-]+)\.svelte$/);
237
246
  if (!match)
238
247
  return null;
239
- return {
240
- docFilePath: decodeDocPath(match[1]),
241
- snippetName: match[2],
242
- };
248
+ return { ...decodeEntityId(match[1]), snippetSlug: match[2] };
243
249
  }
244
250
  /** Parse a preview URL back into its parts */
245
251
  export function parsePreviewUrl(url) {
246
- const match = url.match(/^\/@sdocs\/preview\/([^/]+)\/(\w+)$/);
252
+ const match = url.match(/^\/@sdocs\/preview\/([^/]+)\/([\w-]+)$/);
247
253
  if (!match)
248
254
  return null;
249
- return {
250
- docFilePath: decodeDocPath(match[1]),
251
- snippetName: match[2],
252
- };
255
+ return { ...decodeEntityId(match[1]), snippetSlug: match[2] };
253
256
  }
package/dist/types.d.ts CHANGED
@@ -33,18 +33,12 @@ export interface ResolvedSdocsConfig {
33
33
  open: string[];
34
34
  };
35
35
  }
36
- /** Meta extracted from a .sdoc file */
36
+ /** Entity metadata from a [DOCS]/[PAGE]/[LAYOUT] opener */
37
37
  export interface SdocMeta {
38
- /** The Svelte component being documented */
39
- component?: unknown;
40
38
  /** Sidebar path (e.g. 'Demo / Button') */
41
39
  title: string;
42
40
  /** Short description */
43
41
  description?: string;
44
- /** Default prop values */
45
- args?: Record<string, unknown>;
46
- /** Preview settings (padding, background, etc.) */
47
- settings?: Record<string, unknown>;
48
42
  }
49
43
  /** A parsed prop */
50
44
  export interface ParsedProp {
@@ -82,36 +76,58 @@ export interface ComponentData {
82
76
  state: ParsedState[];
83
77
  cssProps: ParsedCssProp[];
84
78
  }
85
- /** An extracted snippet */
79
+ /** A renderable snippet of an entity: a preview, an example, or the body */
86
80
  export interface ExtractedSnippet {
81
+ /** Display name: preview label / example title / 'Content' */
87
82
  name: string;
83
+ /** URL-safe id, unique within the entity */
84
+ slug: string;
85
+ role: 'preview' | 'example' | 'content';
88
86
  body: string;
89
87
  highlightedHtml?: string;
90
88
  /** Preview URL for iframe (added by virtual module) */
91
89
  previewUrl?: string;
92
90
  }
91
+ /** One [preview] of a DOCS entity: a live showcase of one component */
92
+ export interface PreviewEntry {
93
+ /** Tab label (title override or the component name) */
94
+ label: string;
95
+ /** The previewed component's identifier in the file's script */
96
+ componentName: string | null;
97
+ /** Absolute path to the previewed component */
98
+ componentPath: string | null;
99
+ /** Parsed component data (props, methods, state, CSS props) */
100
+ componentData: ComponentData | null;
101
+ /** Highlighted component source HTML */
102
+ highlightedSource: string | null;
103
+ /** Control defaults for this preview */
104
+ args: Record<string, unknown>;
105
+ snippet: ExtractedSnippet;
106
+ }
93
107
  /** A table of contents heading (for pages) */
94
108
  export interface TocHeading {
95
109
  text: string;
96
110
  level: number;
97
111
  id: string;
98
112
  }
99
- /** A complete doc entry (one .sdoc file) */
113
+ /** A complete doc entry (one entity of one .sdoc file) */
100
114
  export interface DocEntry {
101
- /** Doc kind */
115
+ /** Doc kind: DOCS / PAGE / LAYOUT */
102
116
  kind: 'component' | 'page' | 'layout';
103
117
  /** Absolute path to the .sdoc file */
104
118
  filePath: string;
105
- /** Absolute path to the documented component */
106
- componentPath: string | null;
107
- /** Parsed meta */
119
+ /** URL-safe entity id, unique within the file */
120
+ entitySlug: string;
121
+ /** Entity metadata (title drives the sidebar) */
108
122
  meta: SdocMeta;
109
- /** Parsed component data (props, methods, state, CSS props) */
110
- componentData: ComponentData | null;
111
- /** Extracted snippets */
112
- snippets: ExtractedSnippet[];
113
- /** Highlighted component source HTML */
114
- highlightedSource: string | null;
123
+ /** Live previews (component kind; empty otherwise) */
124
+ previews: PreviewEntry[];
125
+ /** Frozen examples (component kind; empty otherwise) */
126
+ examples: ExtractedSnippet[];
127
+ /** The rendered body (page/layout kind; null otherwise) */
128
+ content: ExtractedSnippet | null;
115
129
  /** Table of contents headings (pages only) */
116
130
  toc?: TocHeading[];
131
+ /** Stage padding (layouts only) */
132
+ padding?: string | null;
117
133
  }
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.41",
3
+ "version": "0.0.42",
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 {};