@sveltek/markdown
Version:
Svelte Markdown Preprocessor.
300 lines (299 loc) • 10.2 kB
JavaScript
import { visit } from "unist-util-visit";
import { parse, preprocess as preprocess$1, print } from "svelte/compiler";
import { unified } from "unified";
import { VFile } from "vfile";
import MagicString from "magic-string";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import { isFalse, isObject, meta } from "./shared/index.js";
import { parse as parse$1 } from "yaml";
import { basename, relative, resolve } from "node:path";
import { readFile } from "node:fs/promises";
export * from "unist-util-visit";
//#region src/config/index.ts
function defineConfig(options) {
return options;
}
//#endregion
//#region src/utils/escape.ts
function escapeSvelte(value) {
return value.replace(/[{}`]/g, (v) => ({
"{": "{",
"}": "}",
"`": "`"
})[v] || v).replace(/\\([trn])/g, "\$1");
}
//#endregion
//#region src/preprocess/file.ts
function parseFile(vfile, { frontmatter = {} } = {}) {
const { marker = "-", parser, defaults = {} } = frontmatter;
const parsedFile = { svelte: "" };
const data = vfile.data;
if (defaults) data.frontmatter = { ...defaults };
let file = String(vfile);
const rgxFm = new RegExp(`^\\s*[${marker}]{3}\\s*\\r?\\n([\\s\\S]*?)\\r?\\n\\s*[${marker}]{3}`);
if (parser) {
const parsed = parser(file);
if (parsed) data.frontmatter = {
...defaults,
...parsed
};
} else {
const match = rgxFm.exec(file);
if (match && match[1]) {
data.frontmatter = {
...defaults,
...parse$1(match[1])
};
file = file.slice(match[0].length);
vfile.value = file;
}
}
if (data.frontmatter?.specialElements) {
if (file.includes("<svelte:")) {
file = file.replace(/<svelte:([a-zA-Z]+)(\s[^>]*)?(?:\/>|>([\s\S]*?)<\/svelte:\1>)/g, (match) => {
parsedFile.svelte += match;
return "";
});
vfile.value = file;
}
}
return parsedFile;
}
//#endregion
//#region src/preprocess/layouts.ts
function getLayoutData(data, { layouts } = {}) {
const { layout } = data.frontmatter;
if (!layouts || !layout) return;
const layoutName = isObject(layout) ? layout.name : layout;
const layoutOptions = layouts.find(({ name }) => name === layoutName);
if (!layoutOptions) {
const names = layouts.map((layout) => `"${layout.name}"`).join(", ");
throw new TypeError(`Invalid layout name. Valid names are: ${names}.`);
}
return layoutOptions;
}
//#endregion
//#region src/preprocess/entries.ts
function getEntryData(data, { entries } = {}) {
const { entry } = data.frontmatter;
if (!entries || !entry) return;
const entryName = isObject(entry) ? entry.name : entry;
const entryOptions = entries.find(({ name }) => name === entryName);
if (!entryOptions) {
const names = entries.map((entry) => `"${entry.name}"`).join(", ");
throw new TypeError(`Invalid entry name. Valid names are: ${names}.`);
}
return entryOptions;
}
//#endregion
//#region src/preprocess/module.ts
function createSvelteModule(module, data) {
const keys = Object.keys(data.frontmatter);
let frontmatter = `export const frontmatter = ${JSON.stringify(data.frontmatter)};\n`;
if (keys.length) frontmatter += `const { ${keys.join(", ")} } = frontmatter;\n`;
if (!module) return {
start: 0,
end: 0,
content: `<script module>\n${frontmatter}<\/script>\n`
};
const content = `<script module>\n${frontmatter}${print(module.content).code}\n<\/script>\n`;
return {
start: module.start,
end: module.end,
content
};
}
//#endregion
//#region src/preprocess/instance.ts
const posix = (path) => path.replace(/\\/g, "/");
const getPath = (from, to) => {
if ([
".svelte",
".ts",
".mts",
".js",
".mjs"
].some((ext) => to.endsWith(ext))) {
const path = posix(relative(resolve(from, ".."), to));
return path.startsWith(".") ? path : `./${path}`;
}
return to;
};
const parseComponents = (filePath, components) => components?.map((value) => {
const { form = "default" } = value;
let path = getPath(filePath, value.path);
return `import { ${form === "default" ? `default as ${value.name}` : value.name} } from "${path}";`;
}).join("\n").concat("\n") || "";
function createSvelteInstance(instance, { filePath, layoutPath, components }) {
const isLayout = filePath && layoutPath;
let code = "";
const comps = parseComponents(filePath, components);
if (comps) code += comps;
if (isLayout) {
const path = getPath(filePath, layoutPath);
code += `import ${meta.layoutName}, * as ${meta.componentName} from "${path}";\n`;
}
if (!instance) return {
start: 0,
end: 0,
content: code ? `<script>\n${code}<\/script>\n` : ""
};
const content = `<script>\n${code}${print(instance.content).code}\n<\/script>\n`;
return {
start: instance.start,
end: instance.end,
content
};
}
//#endregion
//#region src/unplugins/remark/html.ts
const rgxSvelteBlock = /{[#:/@]\w+.*}/;
const rgxElementOrComponent = /<[A-Za-z]+[\s\S]*>/;
const remarkSvelteHtml = () => {
return (tree, vfile) => {
visit(tree, "paragraph", (node) => {
const [child] = node.children;
if (child?.type !== "text" && child?.type !== "html") return;
if ((rgxSvelteBlock.test(child.value) || rgxElementOrComponent.test(child.value)) && node.position) {
const value = vfile.value.slice(node.position.start.offset, node.position.end.offset);
Object.assign(node, {
type: "html",
value
});
}
});
};
};
//#endregion
//#region src/unplugins/rehype/layouts.ts
const getExportedNames = (module) => {
const names = [];
if (module?.content) module.content.body.forEach((node) => {
if (node.type === "ExportNamedDeclaration") node.specifiers.forEach((specifier) => {
if (specifier.exported.type === "Identifier") names.push(specifier.exported.name);
});
});
return names;
};
const rehypeLayout = () => {
return async (_, vfile) => {
const data = vfile.data;
const { layout } = data;
if (!layout) return;
const source = await readFile(layout.path, { encoding: "utf8" });
const filename = basename(layout.path);
const { code, dependencies } = await preprocess$1(source, data.preprocessors, { filename });
if (dependencies) data.dependencies?.push(...dependencies);
const { module } = parse(code, {
filename,
modern: true
});
if (module) {
const namedExports = getExportedNames(module);
if (namedExports.length) data.components = namedExports;
}
};
};
//#endregion
//#region src/unplugins/rehype/components.ts
const rehypeComponents = () => {
return (tree, vfile) => {
const { layout, components } = vfile.data;
if (!layout || !components) return;
visit(tree, "element", (node) => {
if (components.includes(node.tagName)) node.tagName = `${meta.componentName}.${node.tagName}`;
});
};
};
//#endregion
//#region src/unplugins/utils.ts
const usePlugins = (plugins) => plugins ?? [];
//#endregion
//#region src/preprocess/index.ts
async function preprocess(source, options = {}) {
const { filename, preprocessors = [], frontmatter, plugins: { remark = [], rehype = [] } = {}, layouts, entries, components, module: optionModule = true } = options;
const file = new VFile({
value: source,
path: filename,
data: {
preprocessors,
plugins: {
remark,
rehype
},
dependencies: [],
frontmatter: {}
}
});
const parsed = parseFile(file, { frontmatter });
const data = file.data;
if (isFalse(data.frontmatter?.plugins?.remark)) data.plugins.remark = [];
if (isFalse(data.frontmatter?.plugins?.rehype)) data.plugins.rehype = [];
const layout = getLayoutData(data, { layouts });
if (layout) {
data.layout = layout;
data.dependencies?.push(layout.path);
if (layout.plugins && isObject(data.frontmatter?.layout)) {
if (isFalse(data.frontmatter?.layout?.plugins?.remark)) layout.plugins.remark = [];
if (isFalse(data.frontmatter?.layout?.plugins?.rehype)) layout.plugins.rehype = [];
}
}
const entry = getEntryData(data, { entries });
if (entry) {
if (entry.plugins && isObject(data.frontmatter?.entry)) {
if (isFalse(data.frontmatter?.entry?.plugins?.remark)) entry.plugins.remark = [];
if (isFalse(data.frontmatter?.entry?.plugins?.rehype)) entry.plugins.rehype = [];
}
}
const processed = await unified().use(remarkParse).use(remarkSvelteHtml).use(usePlugins(data.plugins?.remark)).use(usePlugins(layout?.plugins?.remark)).use(usePlugins(entry?.plugins?.remark)).use(remarkRehype, { allowDangerousHtml: true }).use(usePlugins(data.plugins?.rehype)).use(usePlugins(layout?.plugins?.rehype)).use(usePlugins(entry?.plugins?.rehype)).use(rehypeLayout).use(rehypeComponents).use(rehypeStringify, { allowDangerousHtml: true }).process(file);
const { code, dependencies } = await preprocess$1(parsed.svelte + String(processed), preprocessors, { filename });
if (dependencies) data.dependencies?.push(...dependencies);
const s = new MagicString(code);
const { instance, module, css } = parse(code, { modern: true });
const svelteModule = createSvelteModule(module, data);
const svelteInstance = createSvelteInstance(instance, {
filePath: file.path,
layoutPath: layout?.path,
components
});
if (instance) s.remove(instance.start, instance.end);
if (layout) {
let styles;
if (module) s.remove(module.start, module.end);
if (css) {
styles = s.original.substring(css.start, css.end);
s.remove(css.start, css.end);
}
s.prepend(`<${meta.layoutName} {frontmatter}>\n`);
s.append(`</${meta.layoutName}>\n`);
if (styles) s.prepend(styles);
}
if (svelteInstance.content) s.prepend(svelteInstance.content);
if (optionModule) s.prepend(svelteModule.content);
const preprocessed = s.toString();
return {
code: preprocessed,
map: s.generateMap({ source: filename }),
dependencies: data.dependencies,
toString: () => preprocessed
};
}
//#endregion
//#region src/preprocessor/index.ts
function svelteMarkdown(options = {}) {
return {
name: meta.name,
async markup({ content, filename }) {
const { extensions = [".md"] } = options;
if (!extensions.some((ext) => filename?.endsWith(ext))) return;
return await preprocess(content, {
filename,
...options
});
}
};
}
//#endregion
export { defineConfig, escapeSvelte, preprocess, svelteMarkdown };