svelte-multiselect 11.5.1 → 11.6.0
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/dist/CmdPalette.svelte.d.ts +2 -2
- package/dist/CodeExample.svelte +1 -1
- package/dist/MultiSelect.svelte +324 -161
- package/dist/MultiSelect.svelte.d.ts +2 -2
- package/dist/Nav.svelte +408 -172
- package/dist/Nav.svelte.d.ts +38 -37
- package/dist/Wiggle.svelte +9 -2
- package/dist/Wiggle.svelte.d.ts +7 -4
- package/dist/attachments.d.ts +7 -1
- package/dist/attachments.js +164 -108
- package/dist/heading-anchors.d.ts +14 -0
- package/dist/heading-anchors.js +114 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/live-examples/highlighter.js +62 -0
- package/dist/live-examples/index.d.ts +7 -0
- package/dist/live-examples/index.js +23 -0
- package/dist/live-examples/mdsvex-transform.d.ts +32 -0
- package/dist/live-examples/mdsvex-transform.js +184 -0
- package/dist/live-examples/vite-plugin.d.ts +6 -0
- package/dist/live-examples/vite-plugin.js +170 -0
- package/dist/types.d.ts +45 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +11 -0
- package/package.json +44 -20
- package/readme.md +25 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Svelte preprocessor that adds IDs to headings at build time for SSR support
|
|
2
|
+
// This ensures fragment navigation (#heading-id) works on initial page load
|
|
3
|
+
// Match headings in two contexts:
|
|
4
|
+
// 1. Start of line (for .svelte files with formatted HTML)
|
|
5
|
+
// 2. After > (for mdsvex output where HTML is on single line, e.g., "</p> <h2>")
|
|
6
|
+
// Avoid matching inside src={...} attributes by requiring these specific contexts
|
|
7
|
+
// Note: [^>]* for attributes won't match if an attribute value contains > (e.g., data-foo="a>b")
|
|
8
|
+
// This edge case is rare in practice and would require significantly more complex parsing
|
|
9
|
+
const heading_regex_line_start = /^(\s*)<(h[1-6])([^>]*)>([\s\S]*?)<\/\2>/gim;
|
|
10
|
+
const heading_regex_after_tag = /(>)(\s*)<(h[1-6])([^>]*)>([\s\S]*?)<\/\3>/gi;
|
|
11
|
+
// Remove Svelte expressions handling nested braces (e.g., {fn({a: 1})})
|
|
12
|
+
// Treats unmatched } as literal text to avoid dropping content
|
|
13
|
+
function strip_svelte_expressions(str) {
|
|
14
|
+
let result = ``;
|
|
15
|
+
let depth = 0;
|
|
16
|
+
for (const char of str) {
|
|
17
|
+
if (char === `{`)
|
|
18
|
+
depth++;
|
|
19
|
+
else if (char === `}` && depth > 0)
|
|
20
|
+
depth--;
|
|
21
|
+
else if (depth === 0)
|
|
22
|
+
result += char;
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
// Generate URL-friendly slug from text
|
|
27
|
+
const slugify = (text) => text
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/\s+/g, `-`)
|
|
30
|
+
.replace(/[^\w-]/g, ``)
|
|
31
|
+
.replace(/-+/g, `-`) // collapse multiple dashes
|
|
32
|
+
.replace(/^-|-$/g, ``); // trim leading/trailing dashes
|
|
33
|
+
/** @type {() => import('svelte/compiler').PreprocessorGroup} */
|
|
34
|
+
export function heading_ids() {
|
|
35
|
+
return {
|
|
36
|
+
name: `heading-ids`,
|
|
37
|
+
markup({ content }) {
|
|
38
|
+
const seen_ids = new Map();
|
|
39
|
+
let result = content;
|
|
40
|
+
const process_heading = (attrs, inner) => {
|
|
41
|
+
// Skip if already has an id (use ^|\s to avoid matching data-id, aria-id, etc.)
|
|
42
|
+
if (/(^|\s)id\s*=/.test(attrs))
|
|
43
|
+
return null;
|
|
44
|
+
const text = strip_svelte_expressions(inner.replace(/<[^>]+>/g, ``)).trim();
|
|
45
|
+
if (!text)
|
|
46
|
+
return null;
|
|
47
|
+
const base_id = slugify(text);
|
|
48
|
+
if (!base_id)
|
|
49
|
+
return null;
|
|
50
|
+
// Handle duplicates within same file
|
|
51
|
+
const count = seen_ids.get(base_id) ?? 0;
|
|
52
|
+
seen_ids.set(base_id, count + 1);
|
|
53
|
+
return count ? `${base_id}-${count}` : base_id;
|
|
54
|
+
};
|
|
55
|
+
// Pass 1: Match headings at start of line (for .svelte files)
|
|
56
|
+
result = result.replace(heading_regex_line_start, (match, indent, tag, attrs, inner) => {
|
|
57
|
+
const id = process_heading(attrs, inner);
|
|
58
|
+
return id ? `${indent}<${tag} id="${id}"${attrs}>${inner}</${tag}>` : match;
|
|
59
|
+
});
|
|
60
|
+
// Pass 2: Match headings after closing tag (for mdsvex single-line output)
|
|
61
|
+
result = result.replace(heading_regex_after_tag, (match, gt, space, tag, attrs, inner) => {
|
|
62
|
+
const id = process_heading(attrs, inner);
|
|
63
|
+
return id ? `${gt}${space}<${tag} id="${id}"${attrs}>${inner}</${tag}>` : match;
|
|
64
|
+
});
|
|
65
|
+
return { code: result };
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// SVG link icon for heading anchors
|
|
70
|
+
const link_svg = `<svg width="16" height="16" viewBox="0 0 16 16" aria-label="Link to heading" role="img"><path d="M7.775 3.275a.75.75 0 0 0 1.06 1.06l1.25-1.25a2 2 0 1 1 2.83 2.83l-2.5 2.5a2 2 0 0 1-2.83 0 .75.75 0 0 0-1.06 1.06 3.5 3.5 0 0 0 4.95 0l2.5-2.5a3.5 3.5 0 0 0-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 0 1 0-2.83l2.5-2.5a2 2 0 0 1 2.83 0 .75.75 0 0 0 1.06-1.06 3.5 3.5 0 0 0-4.95 0l-2.5 2.5a3.5 3.5 0 0 0 4.95 4.95l1.25-1.25a.75.75 0 0 0-1.06-1.06l-1.25 1.25a2 2 0 0 1-2.83 0z" fill="currentColor"/></svg>`;
|
|
71
|
+
// Add anchor link to a single heading element
|
|
72
|
+
function add_anchor_to_heading(heading, icon_svg = link_svg) {
|
|
73
|
+
if (heading.querySelector(`a[aria-hidden="true"]`))
|
|
74
|
+
return;
|
|
75
|
+
if (!heading.id) {
|
|
76
|
+
// Generate ID from text content (fallback for dynamic headings)
|
|
77
|
+
const base_id = slugify((heading.textContent ?? ``).trim());
|
|
78
|
+
if (!base_id)
|
|
79
|
+
return;
|
|
80
|
+
// Ensure unique ID in document
|
|
81
|
+
let counter = 0;
|
|
82
|
+
while (document.getElementById(counter ? `${base_id}-${counter}` : base_id))
|
|
83
|
+
counter++;
|
|
84
|
+
heading.id = counter ? `${base_id}-${counter}` : base_id;
|
|
85
|
+
}
|
|
86
|
+
const anchor = document.createElement(`a`);
|
|
87
|
+
anchor.href = `#${heading.id}`;
|
|
88
|
+
anchor.setAttribute(`aria-hidden`, `true`);
|
|
89
|
+
anchor.innerHTML = icon_svg;
|
|
90
|
+
heading.appendChild(anchor);
|
|
91
|
+
}
|
|
92
|
+
// Svelte 5 attachment that adds anchor links to headings within a container
|
|
93
|
+
// Uses MutationObserver to handle dynamically added headings
|
|
94
|
+
export const heading_anchors = (options = {}) => (node) => {
|
|
95
|
+
if (typeof document === `undefined`)
|
|
96
|
+
return;
|
|
97
|
+
// :scope refers to the element on which querySelectorAll is called
|
|
98
|
+
// This works whether the attachment is on <main> or a parent element
|
|
99
|
+
const selector = options.selector ??
|
|
100
|
+
`:scope > :is(h1, h2, h3, h4, h5, h6), :scope > * > :is(h1, h2, h3, h4, h5, h6)`;
|
|
101
|
+
const icon_svg = options.icon_svg ?? link_svg;
|
|
102
|
+
// Process existing headings
|
|
103
|
+
for (const heading of Array.from(node.querySelectorAll(selector))) {
|
|
104
|
+
add_anchor_to_heading(heading, icon_svg);
|
|
105
|
+
}
|
|
106
|
+
// Watch for new headings - requery the container to respect nesting depth constraints
|
|
107
|
+
const observer = new MutationObserver(() => {
|
|
108
|
+
for (const heading of Array.from(node.querySelectorAll(selector))) {
|
|
109
|
+
add_anchor_to_heading(heading, icon_svg);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
observer.observe(node, { childList: true, subtree: true });
|
|
113
|
+
return () => observer.disconnect();
|
|
114
|
+
};
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Starry-night highlighter for mdsvex
|
|
2
|
+
import { createStarryNight } from '@wooorm/starry-night';
|
|
3
|
+
import source_css from '@wooorm/starry-night/source.css';
|
|
4
|
+
import source_js from '@wooorm/starry-night/source.js';
|
|
5
|
+
import source_json from '@wooorm/starry-night/source.json';
|
|
6
|
+
import source_shell from '@wooorm/starry-night/source.shell';
|
|
7
|
+
import source_svelte from '@wooorm/starry-night/source.svelte';
|
|
8
|
+
import source_ts from '@wooorm/starry-night/source.ts';
|
|
9
|
+
import text_html_basic from '@wooorm/starry-night/text.html.basic';
|
|
10
|
+
// Escape HTML special characters in text content (not for attribute values)
|
|
11
|
+
const escape_html_text = (str) => str.replace(/&/g, `&`).replace(/</g, `<`).replace(/>/g, `>`);
|
|
12
|
+
// Convert HAST to HTML string (simplified - only handles what starry-night outputs)
|
|
13
|
+
export const hast_to_html = (node) => {
|
|
14
|
+
if (node.type === `text`)
|
|
15
|
+
return escape_html_text(node.value);
|
|
16
|
+
if (node.type === `root`)
|
|
17
|
+
return node.children?.map(hast_to_html).join(``) ?? ``;
|
|
18
|
+
const { tagName, properties, children } = node;
|
|
19
|
+
const cls = properties?.className?.join(` `);
|
|
20
|
+
const attrs = cls ? ` class="${cls}"` : ``;
|
|
21
|
+
const inner = children?.map(hast_to_html).join(``) ?? ``;
|
|
22
|
+
return `<${tagName}${attrs}>${inner}</${tagName}>`;
|
|
23
|
+
};
|
|
24
|
+
// Shared starry-night instance (grammars loaded once at build time)
|
|
25
|
+
export const starry_night = await createStarryNight([
|
|
26
|
+
source_svelte,
|
|
27
|
+
source_js,
|
|
28
|
+
source_ts,
|
|
29
|
+
source_css,
|
|
30
|
+
source_json,
|
|
31
|
+
source_shell,
|
|
32
|
+
text_html_basic,
|
|
33
|
+
]);
|
|
34
|
+
// Map code fence language to starry-night grammar scope
|
|
35
|
+
export const LANG_TO_SCOPE = {
|
|
36
|
+
svelte: `source.svelte`,
|
|
37
|
+
html: `text.html.basic`,
|
|
38
|
+
ts: `source.ts`,
|
|
39
|
+
typescript: `source.ts`,
|
|
40
|
+
js: `source.js`,
|
|
41
|
+
javascript: `source.js`,
|
|
42
|
+
css: `source.css`,
|
|
43
|
+
json: `source.json`,
|
|
44
|
+
shell: `source.shell`,
|
|
45
|
+
bash: `source.shell`,
|
|
46
|
+
sh: `source.shell`,
|
|
47
|
+
};
|
|
48
|
+
// Escape characters that would be interpreted as Svelte template syntax
|
|
49
|
+
const escape_svelte = (html) => html.replace(/\{/g, `{`).replace(/\}/g, `}`);
|
|
50
|
+
// mdsvex highlighter function
|
|
51
|
+
export function starry_night_highlighter(code, lang) {
|
|
52
|
+
const lang_key = lang?.toLowerCase();
|
|
53
|
+
const scope = lang_key ? LANG_TO_SCOPE[lang_key] : undefined;
|
|
54
|
+
if (!scope) {
|
|
55
|
+
// Return escaped code if language not supported
|
|
56
|
+
const escaped = escape_svelte(escape_html_text(code));
|
|
57
|
+
return `<pre class="highlight"><code>${escaped}</code></pre>`;
|
|
58
|
+
}
|
|
59
|
+
const tree = starry_night.highlight(code, scope);
|
|
60
|
+
const html = escape_svelte(hast_to_html(tree));
|
|
61
|
+
return `<pre class="highlight highlight-${lang_key}"><code>${html}</code></pre>`;
|
|
62
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { starry_night_highlighter } from './highlighter.js';
|
|
2
|
+
export { default as mdsvex_transform, EXAMPLE_COMPONENT_PREFIX, EXAMPLE_MODULE_PREFIX, } from './mdsvex-transform.js';
|
|
3
|
+
export { default as vite_plugin } from './vite-plugin.js';
|
|
4
|
+
import { sveltePreprocess as _sveltePreprocess } from 'svelte-preprocess';
|
|
5
|
+
type SveltePreprocessOptions = Parameters<typeof _sveltePreprocess>[0];
|
|
6
|
+
type PreprocessorGroup = ReturnType<typeof _sveltePreprocess>;
|
|
7
|
+
export declare function sveltePreprocess(opts?: SveltePreprocessOptions): PreprocessorGroup;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Live examples - transforms ```svelte example code blocks into rendered components
|
|
2
|
+
// with syntax highlighting and live preview
|
|
3
|
+
export { starry_night_highlighter } from './highlighter.js';
|
|
4
|
+
export { default as mdsvex_transform, EXAMPLE_COMPONENT_PREFIX, EXAMPLE_MODULE_PREFIX, } from './mdsvex-transform.js';
|
|
5
|
+
export { default as vite_plugin } from './vite-plugin.js';
|
|
6
|
+
import { sveltePreprocess as _sveltePreprocess } from 'svelte-preprocess';
|
|
7
|
+
// Wrap sveltePreprocess to skip markdown files - otherwise it transpiles code inside
|
|
8
|
+
// markdown code fences, losing whitespace formatting
|
|
9
|
+
const is_markdown = (filename) => /\.(md|mdx|svx)$/.test(filename ?? ``);
|
|
10
|
+
export function sveltePreprocess(opts) {
|
|
11
|
+
const base = _sveltePreprocess(opts);
|
|
12
|
+
return {
|
|
13
|
+
markup: async (args) => is_markdown(args.filename)
|
|
14
|
+
? { code: args.content }
|
|
15
|
+
: (await base.markup?.(args)) ?? { code: args.content },
|
|
16
|
+
script: async (args) => is_markdown(args.filename)
|
|
17
|
+
? { code: args.content }
|
|
18
|
+
: (await base.script?.(args)) ?? { code: args.content },
|
|
19
|
+
style: async (args) => is_markdown(args.filename)
|
|
20
|
+
? { code: args.content }
|
|
21
|
+
: (await base.style?.(args)) ?? { code: args.content },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const EXAMPLE_MODULE_PREFIX = "___live_example___";
|
|
2
|
+
export declare const EXAMPLE_COMPONENT_PREFIX = "LiveExample___";
|
|
3
|
+
interface RemarkMeta {
|
|
4
|
+
Wrapper?: string | [string, string];
|
|
5
|
+
filename?: string;
|
|
6
|
+
csr?: boolean;
|
|
7
|
+
example?: boolean;
|
|
8
|
+
hideScript?: boolean;
|
|
9
|
+
hideStyle?: boolean;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
interface RemarkOptions {
|
|
13
|
+
defaults?: Partial<RemarkMeta>;
|
|
14
|
+
}
|
|
15
|
+
interface RemarkTree {
|
|
16
|
+
type: string;
|
|
17
|
+
children: RemarkNode[];
|
|
18
|
+
}
|
|
19
|
+
interface RemarkNode {
|
|
20
|
+
type: string;
|
|
21
|
+
lang?: string;
|
|
22
|
+
meta?: string;
|
|
23
|
+
value?: string;
|
|
24
|
+
children?: RemarkNode[];
|
|
25
|
+
}
|
|
26
|
+
interface RemarkFile {
|
|
27
|
+
filename: string;
|
|
28
|
+
cwd: string;
|
|
29
|
+
}
|
|
30
|
+
type RemarkTransformer = (tree: RemarkTree, file: RemarkFile) => void;
|
|
31
|
+
declare function remark(options?: RemarkOptions): RemarkTransformer;
|
|
32
|
+
export default remark;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Remark plugin - transforms ```svelte example code blocks into rendered components
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { hast_to_html, LANG_TO_SCOPE, starry_night } from './highlighter.js';
|
|
5
|
+
// Base64 encode to prevent preprocessors from modifying the content
|
|
6
|
+
const to_base64 = (src) => Buffer.from(src, `utf-8`).toString(`base64`);
|
|
7
|
+
// Escape backticks and template literal syntax for embedding in template literals
|
|
8
|
+
const encode_escapes = (src) => src.replace(/`/g, `\\\``).replace(/\$\{/g, `\\$\{`);
|
|
9
|
+
// Regex to find <script> block in svelte
|
|
10
|
+
// Note: These patterns handle common cases but may have edge cases with nested
|
|
11
|
+
// comments containing </script> strings or complex attribute syntax
|
|
12
|
+
const RE_SCRIPT_START = /<script(?:\s+?[a-zA-Z]+(=(?:["']){0,1}[a-zA-Z0-9]+(?:["']){0,1}){0,1})*\s*?>/;
|
|
13
|
+
const RE_SCRIPT_BLOCK = /(<script[\s\S]*?>)([\s\S]*?)(<\/script>)/g;
|
|
14
|
+
const RE_STYLE_BLOCK = /(<style[\s\S]*?>)([\s\S]*?)(<\/style>)/g;
|
|
15
|
+
// Parses key=value pairs from a string. Supports strings (with escaped quotes),
|
|
16
|
+
// numbers, booleans, and arrays. Note: nested structures in arrays are not supported.
|
|
17
|
+
const RE_PARSE_META = /(\w+=\d+|\w+="(?:[^"\\]|\\.)*"|\w+=\[[^\]]*\]|\w+)/g;
|
|
18
|
+
export const EXAMPLE_MODULE_PREFIX = `___live_example___`;
|
|
19
|
+
export const EXAMPLE_COMPONENT_PREFIX = `LiveExample___`;
|
|
20
|
+
// Languages that render as live Svelte components (O(1) lookup)
|
|
21
|
+
const LIVE_LANGUAGES = new Set([`svelte`, `html`]);
|
|
22
|
+
// All languages that support the `example` meta (O(1) lookup)
|
|
23
|
+
const EXAMPLE_LANGUAGES = new Set(Object.keys(LANG_TO_SCOPE));
|
|
24
|
+
// Simple tree traversal - finds all nodes of a given type
|
|
25
|
+
const visit = (tree, type, callback) => {
|
|
26
|
+
const walk = (nodes) => {
|
|
27
|
+
for (const node of nodes) {
|
|
28
|
+
if (node.type === type)
|
|
29
|
+
callback(node);
|
|
30
|
+
if (node.children)
|
|
31
|
+
walk(node.children);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
walk(tree.children);
|
|
35
|
+
};
|
|
36
|
+
// Default wrapper component
|
|
37
|
+
const DEFAULT_WRAPPER = `$lib/CodeExample.svelte`;
|
|
38
|
+
function remark(options = {}) {
|
|
39
|
+
const { defaults = {} } = options;
|
|
40
|
+
return function transformer(tree, file) {
|
|
41
|
+
const examples = [];
|
|
42
|
+
// Track wrapper imports to avoid duplicates and generate unique aliases
|
|
43
|
+
const wrapper_aliases = new Map(); // wrapper key -> alias name
|
|
44
|
+
const filename = path.relative(file.cwd, file.filename);
|
|
45
|
+
// Helper to get or create a wrapper alias
|
|
46
|
+
function get_wrapper_alias(wrapper) {
|
|
47
|
+
// Use '::' as delimiter to avoid misparsing paths with colons (Windows, URLs)
|
|
48
|
+
const wrapper_key = typeof wrapper === `string`
|
|
49
|
+
? wrapper
|
|
50
|
+
: `${wrapper[0]}::${wrapper[1]}`;
|
|
51
|
+
let alias = wrapper_aliases.get(wrapper_key);
|
|
52
|
+
if (!alias) {
|
|
53
|
+
alias = `Example_${wrapper_aliases.size}`;
|
|
54
|
+
wrapper_aliases.set(wrapper_key, alias);
|
|
55
|
+
}
|
|
56
|
+
return alias;
|
|
57
|
+
}
|
|
58
|
+
visit(tree, `code`, (node) => {
|
|
59
|
+
const meta = {
|
|
60
|
+
Wrapper: DEFAULT_WRAPPER,
|
|
61
|
+
filename,
|
|
62
|
+
...defaults,
|
|
63
|
+
...parse_meta(node.meta || ``),
|
|
64
|
+
};
|
|
65
|
+
const { csr, example, Wrapper } = meta;
|
|
66
|
+
// find code blocks with `example` meta in supported languages
|
|
67
|
+
if (example && node.lang && EXAMPLE_LANGUAGES.has(node.lang)) {
|
|
68
|
+
const is_live = LIVE_LANGUAGES.has(node.lang);
|
|
69
|
+
const wrapper_alias = is_live ? get_wrapper_alias(Wrapper ?? DEFAULT_WRAPPER) : ``;
|
|
70
|
+
const value = create_example_component(node.value || ``, meta, is_live ? examples.length : -1, // -1 for code-only (no component import needed)
|
|
71
|
+
node.lang, is_live, wrapper_alias);
|
|
72
|
+
// Only track live examples for component imports
|
|
73
|
+
if (is_live) {
|
|
74
|
+
examples.push({ csr, wrapper_alias });
|
|
75
|
+
}
|
|
76
|
+
node.type = `paragraph`;
|
|
77
|
+
node.children = [{ type: `text`, value }];
|
|
78
|
+
delete node.lang;
|
|
79
|
+
delete node.meta;
|
|
80
|
+
delete node.value;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
// add imports for each generated example
|
|
84
|
+
let scripts = ``;
|
|
85
|
+
// Add wrapper imports
|
|
86
|
+
// Use '::' as the tuple delimiter to avoid misparsing Windows paths (C:\path)
|
|
87
|
+
// or URLs (https://example.com) that contain single colons
|
|
88
|
+
for (const [wrapper_key, alias] of wrapper_aliases) {
|
|
89
|
+
const double_colon_idx = wrapper_key.indexOf(`::`);
|
|
90
|
+
if (double_colon_idx === -1) {
|
|
91
|
+
// Simple string path (default import)
|
|
92
|
+
scripts += `import ${alias} from "${wrapper_key}";\n`;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// Tuple [module, export] using '::' delimiter
|
|
96
|
+
const module_path = wrapper_key.slice(0, double_colon_idx);
|
|
97
|
+
const export_name = wrapper_key.slice(double_colon_idx + 2);
|
|
98
|
+
scripts += `import { ${export_name} as ${alias} } from "${module_path}";\n`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Add example component imports
|
|
102
|
+
for (const [idx, ex] of examples.entries()) {
|
|
103
|
+
if (!ex.csr) {
|
|
104
|
+
scripts +=
|
|
105
|
+
`import ${EXAMPLE_COMPONENT_PREFIX}${idx} from "${EXAMPLE_MODULE_PREFIX}${idx}.svelte";\n`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Try to inject imports into existing script block
|
|
109
|
+
let injected = false;
|
|
110
|
+
visit(tree, `html`, (node) => {
|
|
111
|
+
if (!injected && node.value && RE_SCRIPT_START.test(node.value)) {
|
|
112
|
+
node.value = node.value.replace(RE_SCRIPT_START, (opening_tag) => `${opening_tag}\n${scripts}`);
|
|
113
|
+
injected = true;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
// Create script block if none existed
|
|
117
|
+
if (!injected) {
|
|
118
|
+
tree.children.push({ type: `html`, value: `<script>\n${scripts}</script>` });
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function parse_meta(meta) {
|
|
123
|
+
const result = {};
|
|
124
|
+
for (const part of meta.match(RE_PARSE_META) ?? []) {
|
|
125
|
+
const eq = part.indexOf(`=`);
|
|
126
|
+
const key = eq === -1 ? part : part.slice(0, eq);
|
|
127
|
+
const value = eq === -1 ? `true` : part.slice(eq + 1);
|
|
128
|
+
try {
|
|
129
|
+
result[key] = JSON.parse(value);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
throw new Error(`Unable to parse meta \`${key}=${value}\``);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
function format_code(code, meta) {
|
|
138
|
+
let result = code;
|
|
139
|
+
if (meta.hideScript)
|
|
140
|
+
result = result.replace(RE_SCRIPT_BLOCK, ``);
|
|
141
|
+
if (meta.hideStyle)
|
|
142
|
+
result = result.replace(RE_STYLE_BLOCK, ``);
|
|
143
|
+
return result.trim();
|
|
144
|
+
}
|
|
145
|
+
function create_example_component(value, meta, index, lang, is_live, wrapper_alias) {
|
|
146
|
+
const code = format_code(value, meta);
|
|
147
|
+
const tree = starry_night.highlight(code, LANG_TO_SCOPE[lang]);
|
|
148
|
+
// Convert newlines to to prevent bundlers from stripping whitespace
|
|
149
|
+
const highlighted = hast_to_html(tree).replace(/\n/g, ` `);
|
|
150
|
+
// Code-only examples (ts, js, css, etc.) - just render highlighted code block
|
|
151
|
+
if (!is_live) {
|
|
152
|
+
// Close and reopen <p> to avoid block-in-inline HTML nesting issues
|
|
153
|
+
return `</p><pre class="highlight highlight-${lang}"><code>{@html ${JSON.stringify(highlighted)}}</code></pre><p>`;
|
|
154
|
+
}
|
|
155
|
+
// Live examples (svelte, html) - render with CodeExample wrapper
|
|
156
|
+
const component = `${EXAMPLE_COMPONENT_PREFIX}${index}`;
|
|
157
|
+
const base64_src = to_base64(value);
|
|
158
|
+
const escaped_src = JSON.stringify(encode_escapes(code));
|
|
159
|
+
const escaped_meta = encode_escapes(JSON.stringify(meta));
|
|
160
|
+
// Close and reopen <p> to avoid block-in-inline HTML nesting issues
|
|
161
|
+
return `</p>
|
|
162
|
+
<${wrapper_alias}
|
|
163
|
+
__live_example_src={"${base64_src}"}
|
|
164
|
+
src={${escaped_src}}
|
|
165
|
+
meta={${escaped_meta}}
|
|
166
|
+
>
|
|
167
|
+
{#snippet example()}
|
|
168
|
+
${meta.csr
|
|
169
|
+
? `{#if typeof window !== 'undefined'}
|
|
170
|
+
{#await import("${EXAMPLE_MODULE_PREFIX}${index}.svelte") then module}
|
|
171
|
+
{@const ${component} = module.default}
|
|
172
|
+
<${component} />
|
|
173
|
+
{/await}
|
|
174
|
+
{/if}`
|
|
175
|
+
: `<${component} />`}
|
|
176
|
+
{/snippet}
|
|
177
|
+
|
|
178
|
+
{#snippet code()}
|
|
179
|
+
{@html ${JSON.stringify(highlighted)}}
|
|
180
|
+
{/snippet}
|
|
181
|
+
</${wrapper_alias}>
|
|
182
|
+
<p>`;
|
|
183
|
+
}
|
|
184
|
+
export default remark;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Vite plugin - handles virtual module resolution for example components
|
|
2
|
+
// @ts-expect-error no types available
|
|
3
|
+
import ast from 'abstract-syntax-tree';
|
|
4
|
+
import { Buffer } from 'node:buffer';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
import { EXAMPLE_MODULE_PREFIX } from './mdsvex-transform.js';
|
|
8
|
+
export { EXAMPLE_MODULE_PREFIX };
|
|
9
|
+
// Max chars to scan after property end for trailing comma/whitespace cleanup
|
|
10
|
+
const TRAILING_CLEANUP_BOUND = 50; // Generous bound - typical trailing content is ", " (2 chars)
|
|
11
|
+
// Apply edits in reverse order so positions stay valid
|
|
12
|
+
const apply_edits = (source, edits) => edits
|
|
13
|
+
.sort((a, b) => b.start - a.start)
|
|
14
|
+
.reduce((str, { start, end, content }) => str.slice(0, start) + content + str.slice(end), source);
|
|
15
|
+
export default function live_examples_plugin(options = {}) {
|
|
16
|
+
const { extensions = [`.svelte.md`, `.md`, `.svx`] } = options;
|
|
17
|
+
// Extracted examples as individual virtual files (id -> svelte source)
|
|
18
|
+
const virtual_files = new Map();
|
|
19
|
+
// Reverse lookup: parent markdown path -> set of virtual file IDs (for O(1) HMR lookups)
|
|
20
|
+
const parent_to_virtual = new Map();
|
|
21
|
+
let vite_server;
|
|
22
|
+
return {
|
|
23
|
+
name: `live-examples-plugin`,
|
|
24
|
+
configureServer(server) {
|
|
25
|
+
vite_server = server;
|
|
26
|
+
},
|
|
27
|
+
resolveId(id) {
|
|
28
|
+
if (id.includes(EXAMPLE_MODULE_PREFIX)) {
|
|
29
|
+
// Force absolute path (dev uses relative, prod uses absolute)
|
|
30
|
+
// Use posix.join to ensure forward slashes on all platforms (Vite normalizes to /)
|
|
31
|
+
const cwd = process.cwd().replace(/\\/g, `/`);
|
|
32
|
+
return id.includes(cwd) ? id : path.posix.join(cwd, id);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
load(id) {
|
|
36
|
+
if (id.includes(EXAMPLE_MODULE_PREFIX)) {
|
|
37
|
+
// Strip query parameters - Vite requests derived modules (styles, etc.) with queries
|
|
38
|
+
// like ?inline&svelte&type=style&lang.css but we store the base path
|
|
39
|
+
const [base_id, query = ``] = id.split(`?`);
|
|
40
|
+
const src = virtual_files.get(base_id);
|
|
41
|
+
if (src)
|
|
42
|
+
return src;
|
|
43
|
+
// Virtual file not found - can happen during SSR/parallel builds when derived
|
|
44
|
+
// modules (styles, scripts) are requested before parent markdown is transformed.
|
|
45
|
+
// For derived module requests, return empty content to avoid crashes.
|
|
46
|
+
if (query.includes(`type=style`) || query.includes(`type=script`) ||
|
|
47
|
+
query.includes(`type=module`)) {
|
|
48
|
+
return ``;
|
|
49
|
+
}
|
|
50
|
+
// For main component requests in production, fail the build
|
|
51
|
+
const msg = `Example src not found for ${id}`;
|
|
52
|
+
if (process.env.NODE_ENV === `production`) {
|
|
53
|
+
throw new Error(msg);
|
|
54
|
+
}
|
|
55
|
+
// In dev, warn and return error component to surface issue visibly
|
|
56
|
+
this.warn(msg);
|
|
57
|
+
return `<script>console.error(${JSON.stringify(msg)})</script><p style="color:red">${msg}</p>`;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
transform(code, id) {
|
|
61
|
+
// Strip query params for extension check (Vite adds ?query for HMR, styles, etc.)
|
|
62
|
+
const base_id = id.split(`?`)[0];
|
|
63
|
+
// Skip non-matching files
|
|
64
|
+
const is_example_module = id.includes(EXAMPLE_MODULE_PREFIX);
|
|
65
|
+
const is_markdown = extensions.some((ext) => base_id.endsWith(ext));
|
|
66
|
+
if (!is_example_module && !is_markdown)
|
|
67
|
+
return;
|
|
68
|
+
// Skip derived modules (styles, etc.) - only process the main markdown file
|
|
69
|
+
// Vite creates derived modules like ?svelte&type=style&lang.css for style blocks
|
|
70
|
+
if (id.includes(`?svelte&type=`))
|
|
71
|
+
return { code, map: { mappings: `` } };
|
|
72
|
+
if (is_markdown) {
|
|
73
|
+
// Use AST for precise node location, collect edits to apply at end
|
|
74
|
+
let tree;
|
|
75
|
+
try {
|
|
76
|
+
tree = ast.parse(code, { ranges: true });
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Code may contain Svelte syntax that the JS parser can't handle
|
|
80
|
+
// (e.g., template blocks, special directives). Skip transformation.
|
|
81
|
+
return { code, map: { mappings: `` } };
|
|
82
|
+
}
|
|
83
|
+
const edits = [];
|
|
84
|
+
// Find all __live_example_src properties
|
|
85
|
+
const src_props = ast.find(tree, {
|
|
86
|
+
type: `Property`,
|
|
87
|
+
key: { name: `__live_example_src` },
|
|
88
|
+
});
|
|
89
|
+
for (const [idx, prop] of src_props.entries()) {
|
|
90
|
+
// Extract the string literal content (base64 encoded)
|
|
91
|
+
const string_literals = ast.find(prop, {
|
|
92
|
+
type: `Literal`,
|
|
93
|
+
});
|
|
94
|
+
if (string_literals.length === 0)
|
|
95
|
+
continue;
|
|
96
|
+
const value_node = string_literals[0];
|
|
97
|
+
// AST Literal nodes store value in .value property (string for literals)
|
|
98
|
+
const src = Buffer.from(String(value_node.value ?? ``), `base64`).toString(`utf-8`);
|
|
99
|
+
// Use base_id (without query params) to ensure consistent virtual file IDs
|
|
100
|
+
const virtual_id = `${base_id}${EXAMPLE_MODULE_PREFIX}${idx}.svelte`;
|
|
101
|
+
if (src !== virtual_files.get(virtual_id)) {
|
|
102
|
+
virtual_files.set(virtual_id, src);
|
|
103
|
+
// Update reverse lookup for HMR (get-or-create pattern)
|
|
104
|
+
const virtual_set = parent_to_virtual.get(base_id) ?? new Set();
|
|
105
|
+
if (!parent_to_virtual.has(base_id)) {
|
|
106
|
+
parent_to_virtual.set(base_id, virtual_set);
|
|
107
|
+
}
|
|
108
|
+
virtual_set.add(virtual_id);
|
|
109
|
+
// Invalidate modules for HMR
|
|
110
|
+
if (vite_server) {
|
|
111
|
+
const mod = vite_server.moduleGraph.getModuleById(virtual_id);
|
|
112
|
+
const parent_mod = vite_server.moduleGraph.getModuleById(base_id);
|
|
113
|
+
if (mod)
|
|
114
|
+
vite_server.moduleGraph.invalidateModule(mod);
|
|
115
|
+
if (parent_mod)
|
|
116
|
+
vite_server.moduleGraph.invalidateModule(parent_mod);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Remove the property (including trailing comma/whitespace)
|
|
120
|
+
if (prop.start !== undefined && prop.end !== undefined) {
|
|
121
|
+
let end = prop.end;
|
|
122
|
+
const max_end = Math.min(prop.end + TRAILING_CLEANUP_BOUND, code.length);
|
|
123
|
+
while (end < max_end && /[\s,]/.test(code[end]))
|
|
124
|
+
end++;
|
|
125
|
+
edits.push({ start: prop.start, end, content: `` });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Update import paths (static and dynamic) to use virtual file IDs
|
|
129
|
+
const imports = [
|
|
130
|
+
...ast.find(tree, { type: `ImportDeclaration` }),
|
|
131
|
+
...ast.find(tree, { type: `ImportExpression` }),
|
|
132
|
+
];
|
|
133
|
+
for (const { source } of imports) {
|
|
134
|
+
const match = source?.value?.match(/___live_example___(\d+)\.svelte/);
|
|
135
|
+
if (match && source?.start !== undefined && source?.end !== undefined) {
|
|
136
|
+
const virtual_id = `${base_id}${EXAMPLE_MODULE_PREFIX}${match[1]}.svelte`;
|
|
137
|
+
edits.push({
|
|
138
|
+
start: source.start + 1,
|
|
139
|
+
end: source.end - 1,
|
|
140
|
+
content: virtual_id,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
code: apply_edits(code, edits),
|
|
146
|
+
map: { mappings: `` },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return { code, map: { mappings: `` } };
|
|
150
|
+
},
|
|
151
|
+
handleHotUpdate(ctx) {
|
|
152
|
+
// Collect virtual file modules that need HMR updates
|
|
153
|
+
const additional_modules = [];
|
|
154
|
+
// Normalize to forward slashes (ctx.file uses OS separators, Map keys use Vite's forward slashes)
|
|
155
|
+
const file = ctx.file.replace(/\\/g, `/`);
|
|
156
|
+
// O(1) lookup using reverse map instead of iterating all virtual files
|
|
157
|
+
if (extensions.some((ext) => file.endsWith(ext))) {
|
|
158
|
+
const virtual_ids = parent_to_virtual.get(file);
|
|
159
|
+
if (virtual_ids) {
|
|
160
|
+
for (const id of virtual_ids) {
|
|
161
|
+
const mod = ctx.server.moduleGraph.getModuleById(id);
|
|
162
|
+
if (mod)
|
|
163
|
+
additional_modules.push(mod);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return [...additional_modules, ...ctx.modules];
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|