svelte-multiselect
Version:
Svelte multi-select component
188 lines (186 loc) • 8.91 kB
JavaScript
// Remark plugin - transforms ```svelte example code blocks into rendered components
import { Buffer } from 'node:buffer';
import path from 'node:path';
import { hast_to_html } from './hast.js';
import { starry_night } from './highlighter.js';
// Escape backticks and template literal syntax for embedding in template literals
const encode_escapes = (src) => src.replaceAll(`\``, `\\\``).replaceAll(`\${`, `\\$\{`);
// Regex to find <script> block in svelte
// Note: These patterns handle common cases but may have edge cases with nested
// comments containing </script> strings or complex attribute syntax
const RE_SCRIPT_START = /<script\b(?:[^<>"']|"[^"]*"|'[^']*')*>/u;
const RE_SCRIPT_BLOCK = /<script[\s\S]*?>[\s\S]*?<\/script>/gu;
const RE_STYLE_BLOCK = /<style[\s\S]*?>[\s\S]*?<\/style>/gu;
// Parses key=value pairs from a string. Supports strings (with escaped quotes),
// numbers, booleans, and arrays. Note: nested structures in arrays are not supported.
// Bare values (key=foo, key=true, key=-1.5) are captured greedily so invalid JSON
// throws a parse error instead of silently splitting into two bare-word keys.
const RE_PARSE_META = /(?:\w+="(?:[^"\\]|\\.)*"|\w+=\[[^\]]*\]|\w+=[^\s"[\]]+|\w+)/gu;
export const EXAMPLE_MODULE_PREFIX = `___live_example___`;
export const EXAMPLE_COMPONENT_PREFIX = `LiveExample___`;
// Languages that render as live Svelte components (O(1) lookup)
const LIVE_LANGUAGES = new Set([`svelte`, `html`]);
// Inline lang-label style for code-only examples, which are raw HTML with no
// component scope (pre is white-space: pre; an in-flow label would indent the
// first code line). Components emit the same label but style it via scoped CSS.
const LABEL_STYLE = `position:absolute;bottom:2px;right:6px;font-size:0.65rem;opacity:0.35;text-transform:uppercase;pointer-events:none;user-select:none;line-height:1`;
// Simple tree traversal - finds all nodes of a given type
const visit = (tree, type, callback) => {
const walk = (nodes) => {
for (const node of nodes) {
if (node.type === type)
callback(node);
if (node.children)
walk(node.children);
}
};
walk(tree.children);
};
// Default wrapper component
const DEFAULT_WRAPPER = `$lib/CodeExample.svelte`;
function remark(options = {}) {
const { defaults = {} } = options;
return function transformer(tree, file) {
// csr flag per live example (index = component index); drives the import loop below
const example_csr = [];
// Deduped wrapper imports: JSON key identifies the wrapper (string path or
// [module, export] tuple), value holds the generated alias + import statement
const wrapper_imports = new Map();
const filename = path.relative(file.cwd, file.filename);
// Helper to get or create a wrapper alias
function get_wrapper_alias(wrapper) {
const key = JSON.stringify(wrapper);
let entry = wrapper_imports.get(key);
if (!entry) {
const alias = `Example_${wrapper_imports.size}`;
const statement = typeof wrapper === `string`
? `import ${alias} from "${wrapper}";\n`
: `import { ${wrapper[1]} as ${alias} } from "${wrapper[0]}";\n`;
entry = { alias, statement };
wrapper_imports.set(key, entry);
}
return entry.alias;
}
visit(tree, `code`, (node) => {
const meta = {
Wrapper: DEFAULT_WRAPPER,
filename,
...defaults,
...parse_meta(node.meta ?? ``),
};
const { csr, example, Wrapper } = meta;
const scope = node.lang ? starry_night.flagToScope(node.lang) : undefined;
// find code blocks with `example` meta in supported languages
if (example && node.lang && scope) {
const is_live = LIVE_LANGUAGES.has(node.lang);
const wrapper_alias = is_live ? get_wrapper_alias(Wrapper ?? DEFAULT_WRAPPER) : ``;
const value = create_example_component(node.value ?? ``, meta, is_live ? example_csr.length : -1, // -1 for code-only (no component import needed)
node.lang, scope, wrapper_alias);
// Only track live examples for component imports
if (is_live)
example_csr.push(csr);
node.type = `paragraph`;
node.children = [{ type: `text`, value }];
delete node.lang;
delete node.meta;
delete node.value;
}
});
// add wrapper + example component imports for each generated example
let scripts = ``;
for (const { statement } of wrapper_imports.values())
scripts += statement;
for (const [idx, csr] of example_csr.entries()) {
if (!csr) {
scripts += `import ${EXAMPLE_COMPONENT_PREFIX}${idx} from "${EXAMPLE_MODULE_PREFIX}${idx}.svelte";\n`;
}
}
// Nothing to inject when the file contains no live examples
if (!scripts)
return;
// Try to inject imports into existing script block. Only consider nodes that
// *start* with <script> — raw HTML nodes merely containing a <script> tag
// mid-content (e.g. inside {@html `...`}) must not receive the imports.
let injected = false;
visit(tree, `html`, (node) => {
if (injected || !node.value)
return;
const trimmed = node.value.trimStart();
if (trimmed.startsWith(`<script`) && RE_SCRIPT_START.test(trimmed)) {
node.value = node.value.replace(RE_SCRIPT_START, (opening_tag) => `${opening_tag}\n${scripts}`);
injected = true;
}
});
// Create script block if none existed
if (!injected) {
tree.children.push({ type: `html`, value: `<script>\n${scripts}</script>` });
}
};
}
function parse_meta(meta) {
const result = {};
for (const part of meta.match(RE_PARSE_META) ?? []) {
const eq = part.indexOf(`=`);
const key = eq === -1 ? part : part.slice(0, eq);
const value = eq === -1 ? `true` : part.slice(eq + 1);
try {
result[key] = JSON.parse(value);
}
catch {
throw new Error(`Unable to parse meta \`${key}=${value}\``);
}
}
return result;
}
function format_code(code, meta) {
let result = code;
if (meta.hide_script)
result = result.replace(RE_SCRIPT_BLOCK, ``);
if (meta.hide_style)
result = result.replace(RE_STYLE_BLOCK, ``);
return result.trim();
}
function create_example_component(value, meta, index, // -1 for code-only examples (ts, js, css, ...) without a live component
lang, scope, wrapper_alias) {
const code = format_code(value, meta);
const tree = starry_night.highlight(code, scope);
// Convert newlines to to prevent bundlers from stripping whitespace
const highlighted = hast_to_html(tree).replaceAll(`\n`, ` `);
// Code-only examples (ts, js, css, etc.) - just render highlighted code block
if (index === -1) {
// Close and reopen <p> to avoid block-in-inline HTML nesting issues
return `</p><pre class="highlight highlight-${lang}" style="position:relative"><span class="lang-label" style="${LABEL_STYLE}">${lang}</span><code>{@html ${JSON.stringify(highlighted)}}</code></pre><p>`;
}
// Live examples (svelte, html) - render with CodeExample wrapper.
// JSON.stringify alone produces a valid double-quoted JS string literal for the
// src={...} expression — escaping backticks/`${` on top of it would double-escape
// and inject literal backslashes into the runtime src prop.
const component = `${EXAMPLE_COMPONENT_PREFIX}${index}`;
// base64-encode to prevent preprocessors from modifying the content
const base64_src = Buffer.from(value, `utf-8`).toString(`base64`);
const escaped_src = JSON.stringify(code);
const escaped_meta = encode_escapes(JSON.stringify({ ...meta, lang }));
// Close and reopen <p> to avoid block-in-inline HTML nesting issues
return `</p>
<${wrapper_alias}
__live_example_src={"${base64_src}"}
src={${escaped_src}}
meta={${escaped_meta}}
>
{#snippet example()}
${meta.csr
? `{#if typeof window !== 'undefined'}
{#await import("${EXAMPLE_MODULE_PREFIX}${index}.svelte") then module}
{@const ${component} = module.default}
<${component} />
{/await}
{/if}`
: `<${component} />`}
{/snippet}
{#snippet code()}
{@html ${JSON.stringify(highlighted)}}
{/snippet}
</${wrapper_alias}>
<p>`;
}
export default remark;