UNPKG

@sveltek/markdown

Version:

Svelte Markdown Preprocessor.

300 lines (299 loc) 10.2 kB
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) => ({ "{": "&#123;", "}": "&#125;", "`": "&#96;" })[v] || v).replace(/\\([trn])/g, "&#92;$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 };