svem
Version:
Svelte in Markdown preprocessor
113 lines (112 loc) • 3.92 kB
JavaScript
import { transform } from "./core/index.js";
import { parse } from "svelte/compiler";
import { remarkSvelteEscape, remarkSvelteParse } from "./plugins/index.js";
const LAYOUT_DATA_TOKEN = "__svemLayoutData";
const LAYOUT_WRAP_TOKEN = "SvemLayout";
const LAYOUT_META_TOKEN = "meta";
const svem = (options) => {
const { extensions = [".svem", ".svx"] } = options ?? {};
return {
name: "svem",
async markup({ content, filename }) {
if (extensions.some((ext) => filename.endsWith(ext))) {
const layouts = await resolveLayouts(options);
if (options?.annotateScript) {
content = transformScripts(content, options?.annotateOpen, options?.annotateClose);
}
let { code, data } = await transform(content, filename, {
...options,
decoder: [remarkSvelteParse, ...options?.decoder ?? []],
encoder: [remarkSvelteEscape, ...options?.encoder ?? []]
});
code = wrapLayout(code, layouts, data);
if (code.match(/\$svem\(\)/g)) {
code = code.replace(/globalThis\.\$svem\(\)/g, LAYOUT_DATA_TOKEN).replace(/\$svem\(\)/g, LAYOUT_DATA_TOKEN);
}
return { code };
}
}
};
};
function wrapLayout(code, layouts, data) {
const outputData = { ...data, ...data[LAYOUT_META_TOKEN] };
delete outputData[LAYOUT_META_TOKEN];
const layout = layouts[data?.[LAYOUT_META_TOKEN]?.layout] ?? layouts.default;
let output = code;
const ast = parse(output);
const template = output.slice(ast.html.start, ast.html.end);
const { start = 0, end = 0 } = ast.module ?? {};
let outScript = '<script lang="ts" module>\n<\/script>';
let rawScript = "";
if (end > start) {
rawScript = output.slice(start, end);
outScript = rawScript;
}
outScript = outScript.replace(/<\/script>$/, (match) => {
return `const ${LAYOUT_DATA_TOKEN} = ${JSON.stringify(outputData)};
${match}`;
});
if (layout) {
const openingTag = `<${layout.tag} {...${LAYOUT_DATA_TOKEN}}>`;
const closingTag = `</${layout.tag}>`;
const wrapped = `${openingTag}
${template}
${closingTag}`;
output = output.replace(template, wrapped);
outScript = outScript.replace(/^<script(?:\s+lang="(?:ts|js)")?(?:\s+module)?\s*>/, (match) => {
return `${match}
${layout.script}`;
});
}
if (rawScript) {
output = output.replace(rawScript, outScript);
} else {
output = `${outScript}
${output}`;
}
return output;
}
async function resolveLayouts(options = {}) {
const { layout: layoutInput, layouts: layoutInputs = {} } = options ?? {};
const layouts = {};
if (layoutInput) {
layoutInputs.default = layoutInput;
}
for (const [name, path] of Object.entries(layoutInputs)) {
if (typeof path === "string") {
const tag = `${LAYOUT_WRAP_TOKEN}${toTitleCase(name)}`;
layouts[name] = {
tag,
path,
script: `import ${tag} from '${path}';
`,
openingTag: `<${tag}>`,
closingTag: `</${tag}>`
};
}
}
return layouts;
}
function toTitleCase(text) {
const words = text.match(/\w+/g) ?? [];
return words.map((word) => word.replace(/^\w/, (c) => c.toUpperCase())).join("");
}
function annotationOpenRegex(open = "--", close = "--") {
return new RegExp(`${open}(script|style)(?:\\s+[^${close}]+)?\\s*${close}`, "i");
}
function annotationCloseRegex(open = "--", close = "--") {
return new RegExp(`${open}/(script|style)${close}`, "i");
}
function transformScripts(content, open = "--", close = "--") {
return content.replace(annotationOpenRegex(open, close), (match) => {
return match.replace(new RegExp(`^${open}`), "<").replace(new RegExp(`${close}$`), ">");
}).replace(annotationCloseRegex(open, close), (match) => {
return match.replace(new RegExp(`^${open}`), "<").replace(new RegExp(`${close}$`), ">");
});
}
export {
LAYOUT_DATA_TOKEN,
LAYOUT_META_TOKEN,
LAYOUT_WRAP_TOKEN,
svem
};