Skip to content
41 changes: 39 additions & 2 deletions docs/src/lib/components/Code.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script module>
import { createHighlighter } from 'shiki';
import { transformerMetaHighlight } from '@shikijs/transformers';

const highlighter = createHighlighter({
themes: ['github-light-default', 'github-dark-default'],
Expand All @@ -26,6 +27,8 @@
language?: string;
title?: string;
copyButton?: boolean | 'hover';
showLineNumbers?: boolean;
highlight?: string;
classes?: { root?: string; pre?: string; code?: string };
class?: string;
}
Expand All @@ -35,6 +38,8 @@
source = null,
language = 'svelte',
copyButton = true,
showLineNumbers = false,
highlight,
classes = {},
class: className
}: Props & HTMLAttributes<HTMLDivElement> = $props();
Expand Down Expand Up @@ -69,7 +74,8 @@
class={cls(
'Code',
'relative bg-surface-200 dark:bg-surface-300 p-4 overflow-auto not-prose [tab-size:2]',
copyButton === 'hover' && 'group'
copyButton === 'hover' && 'group',
showLineNumbers && 'show-line-numbers'
)}
>
{#if source}
Expand All @@ -84,7 +90,9 @@
themes: {
light: 'github-light-default',
dark: 'github-dark-default'
}
},
meta: highlight ? { __raw: `{${highlight}}` } : undefined,
transformers: highlight ? [transformerMetaHighlight()] : undefined
})}
{:catch error}
<div class="text-red-500">Error loading code highlighting: {error.message}</div>
Expand Down Expand Up @@ -124,4 +132,33 @@
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}

/* Line highlighting */
:global(.shiki .line.highlighted) {
background-color: rgba(101, 117, 133, 0.16);
margin: 0 -1rem;
padding: 0 1rem;
display: inline-block;
width: calc(100% + 2rem);
}

:global(html.dark .shiki .line.highlighted) {
background-color: rgba(142, 150, 170, 0.14);
}

/* Line numbers */
.show-line-numbers :global(.shiki code) {
counter-reset: line;
}

.show-line-numbers :global(.shiki .line::before) {
counter-increment: line;
content: counter(line);
display: inline-block;
width: 2rem;
margin-right: 1rem;
text-align: right;
color: rgba(115, 138, 148, 0.4);
user-select: none;
}
</style>
39 changes: 33 additions & 6 deletions docs/src/lib/components/Example.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,52 @@
let {
component = page.params.name!,
name,
path,
showCode = false,
showLineNumbers = false,
highlight,
variant = 'default',
noResize = false,
clip = false,
class: className
}: {
component?: string;
name?: string;
path?: string;
showCode?: boolean;
showLineNumbers?: boolean;
highlight?: string;
variant?: 'default' | 'basic';
noResize?: boolean;
clip?: boolean;
class?: string;
} = $props();

/**
* Resolve a relative or absolute path to a full path from /src
*/
function resolveExamplePath(examplePath: string, currentPath: string): string {
if (examplePath.startsWith('./')) {
return `/src/routes${currentPath}/${examplePath.slice(2)}`;
} else if (examplePath.startsWith('/')) {
return `/src${examplePath}`;
} else {
return `/src/routes${currentPath}/${examplePath}`;
}
}

// Get example from context (eagerly loaded by layout)
let example = $derived(
component && name ? examples.get()?.current[component]?.[name] : undefined
);
let example = $derived.by(() => {
if (path) {
// Path-based example
const resolvedPath = resolveExamplePath(path, page.url.pathname);
return examples.get()?.current['__path__']?.[resolvedPath];
} else if (component && name) {
// Component/name-based example
return examples.get()?.current[component]?.[name];
}
return undefined;
});

let containerEl = $state<HTMLElement | null>(null);
let containerWidth = $state<number | undefined>(undefined);
Expand Down Expand Up @@ -132,7 +159,7 @@

{#if showCode}
<div transition:slide class={cls('border border-t-0', showCode && 'rounded-b-sm')}>
<Code source={example.source} class="outline-none" />
<Code source={example.source} {showLineNumbers} {highlight} class="outline-none" />
</div>
{/if}

Expand Down Expand Up @@ -206,8 +233,8 @@
{/if}
{:else}
<div class="border border-danger bg-danger/5 text-danger px-4 py-2 rounded-md">
Example <span class="font-bold">`{name ?? path}`</span>
{#if component}
Example <span class="font-bold">`{path ?? name}`</span>
{#if component && !path}
for <span class="font-bold">`{component}`</span>
{/if}
not found.
Expand Down
32 changes: 32 additions & 0 deletions docs/src/lib/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,35 @@ export async function loadExamples(

return results;
}

/**
* Load an example by its path (relative to /src/routes or absolute from /src)
* @param resolvedPath - The resolved path (e.g., "/src/routes/docs/guides/styles/color-schemes.svelte")
*/
export async function loadExampleByPath(resolvedPath: string): Promise<LoadedExample | null> {
try {
// Use import.meta.glob to load path-based examples
// This is necessary because dynamic imports with fully dynamic paths don't work in Vite
const modules = import.meta.glob<{ default: Component }>('/src/routes/**/*.svelte');
const rawModules = import.meta.glob<{ default: string }>('/src/routes/**/*.svelte', {
query: '?raw',
import: 'default'
});

const componentModule = await modules[resolvedPath]?.();
const rawSource = await rawModules[resolvedPath]?.();

if (!componentModule || rawSource === undefined) {
console.warn(`Failed to load example by path: ${resolvedPath}`);
return null;
}

const comp = componentModule.default as Component;
const source = (rawSource as string).replace(/^.*export .*;.*$/gm, '');

return { component: comp, source };
} catch (e) {
console.warn(`Failed to load example by path: ${resolvedPath}`, e);
return null;
}
}
78 changes: 61 additions & 17 deletions docs/src/lib/markdown/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from 'content-collections';

import type { Examples } from '$lib/types.js';
import { loadExample } from '$lib/examples.js';
import { loadExample, loadExampleByPath } from '$lib/examples.js';

/**
* Get markdown document component and metadata (frontmatter)
Expand Down Expand Up @@ -60,6 +60,22 @@ function getMetadata(
return allComponents.find((c) => c.slug === slug) as any;
}

/**
* Resolve a relative or absolute path to a full path from /src
* @param path - The path from the example (e.g., "./color-schemes.svelte" or "/routes/docs/guides/styles/color-schemes.svelte")
* @param currentPath - The current page URL pathname (e.g., "/docs/guides/styles")
* @returns The resolved path (e.g., "/src/routes/docs/guides/styles/color-schemes.svelte")
*/
export function resolveExamplePath(path: string, currentPath: string): string {
if (path.startsWith('./')) {
return `/src/routes${currentPath}/${path.slice(2)}`;
} else if (path.startsWith('/')) {
return `/src${path}`;
} else {
return `/src/routes${currentPath}/${path}`;
}
}

/**
* Extract examples from markdown content and eagerly load them.
*
Expand All @@ -69,12 +85,14 @@ function getMetadata(
* @param markdownContent - The markdown content to extract examples from
* @param defaultComponent - Optional default component name (from route params)
* @param type - The type of content ('components' or 'utils')
* @param currentPath - The current page URL pathname (needed for resolving relative paths)
* @returns Examples object with loaded components and sources
*/
export async function loadExamplesFromMarkdown(
markdownContent: string,
defaultComponent?: string,
type: 'components' | 'utils' = 'components'
type: 'components' | 'utils' = 'components',
currentPath?: string
): Promise<Examples> {
// Extract all <Example component="..." name="..."> from markdown content
// Also support :example{component="..." name="..."} syntax
Expand All @@ -83,29 +101,55 @@ export async function loadExamplesFromMarkdown(
const componentMatches = [...markdownContent.matchAll(componentRegex)];
const mdcMatches = [...markdownContent.matchAll(mdcRegex)];
const matches = [...componentMatches, ...mdcMatches];
const pageExamples = matches.map((match) => {

const componentExamples: Array<{ component: string; name: string }> = [];
const pathExamples: Array<{ path: string; resolvedPath: string }> = [];

for (const match of matches) {
const attrs = match[1];
const component = attrs.match(/component="([^"]*?)"/)?.[1] || defaultComponent;
const name = attrs.match(/name="([^"]*?)"/)?.[1] || null;
return { component, name };
});
const path = attrs.match(/path="([^"]*?)"/)?.[1];

if (path && currentPath) {
// Path-based example
const resolvedPath = resolveExamplePath(path, currentPath);
pathExamples.push({ path, resolvedPath });
} else {
// Component/name-based example
const component = attrs.match(/component="([^"]*?)"/)?.[1] || defaultComponent;
const name = attrs.match(/name="([^"]*?)"/)?.[1] || null;
if (component && name) {
componentExamples.push({ component, name });
}
}
}

const examples: Examples = {};

// Load examples in parallel using dynamic imports
// Load component-based examples in parallel
await Promise.all(
pageExamples
.filter((ex) => ex.component && ex.name)
.map(async (ex) => {
const loaded = await loadExample(ex.component!, ex.name!, type);
componentExamples.map(async (ex) => {
const loaded = await loadExample(ex.component, ex.name, type);
if (loaded) {
if (!examples[ex.component]) {
examples[ex.component] = {};
}
examples[ex.component][ex.name] = loaded;
}
})
);

// Load path-based examples in parallel
if (pathExamples.length > 0) {
examples['__path__'] = {};
await Promise.all(
pathExamples.map(async (ex) => {
const loaded = await loadExampleByPath(ex.resolvedPath);
if (loaded) {
if (!examples[ex.component!]) {
examples[ex.component!] = {};
}
examples[ex.component!][ex.name!] = loaded;
examples['__path__']![ex.resolvedPath] = loaded;
}
})
);
);
}

return examples;
}
3 changes: 2 additions & 1 deletion docs/src/routes/docs/+layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export const load = async ({ url }) => {
examples = await loadExamplesFromMarkdown(
markdownContent,
undefined, // no default component
'components'
'components',
pathname // for resolving relative paths
);
break;
} catch (e) {
Expand Down
Loading
Loading