UNPKG

radish

Version:

Radish is a React-based static site generator that outputs plain HTML and CSS.

194 lines (193 loc) 7.91 kB
import * as fs from "node:fs"; import * as path from "node:path"; import { globby } from "globby"; import grayMatter from "gray-matter"; import { compile as compileMdx } from "@mdx-js/mdx"; import remarkGfm from "remark-gfm"; import rehypeHighlight from "rehype-highlight"; import toml from "toml"; import yaml from "js-yaml"; import fetch from "../util/fetch.js"; const content = new Map(); export const contentMap = () => { const map = {}; for (const [file, matter] of content) { const dirs = file.split(path.sep); let current = map; for (let i = 0; i < dirs.length; i++) { const dir = dirs[i]; if (dir === undefined) break; if (i === dirs.length - 1) current[dir] = matter; else current[dir] ??= {}; current = current[dir]; } } return map; }; export const contentPlugin = (options) => ({ name: "content", setup(build) { build.onResolve({ filter: /CONTENT_INDEX$/ }, args => { return { path: args.path, namespace: "content" }; }); build.onLoad({ filter: /.*/, namespace: "content" }, async () => { const paths = await globby([ path.join(options.src, "**/!(_)*.{md,mdx,json,toml,yaml,yml,js,ts}") ]); const files = paths.map(filepath => path.relative(options.src, filepath)); const src = ["export const content = {};\n"]; const levels = new Set(); let i = 1; for (const file of files) { const filepath = path.parse(file); const dirs = filepath.dir .split(path.sep) .filter(Boolean) .map(dir => `["${dir}"]`); for (let j = 1; j <= dirs.length; j++) { const nested = dirs.slice(0, j).join(""); if (levels.has(nested)) continue; levels.add(nested); src.push(`content${nested} = {};`); } const joined = dirs.join("") + `["${filepath.name}"]`; src.push(`import * as content${i} from "./${file}";`); if (/\.mdx?$/.test(filepath.base)) { src.push(`content${joined} = { ...content${i}, Component: content${i}.default };`, `delete content${joined}.default;\n`); } else src.push(`content${joined} = content${i};`); i += 1; } return { contents: src.join("\n"), loader: "js", resolveDir: options.src }; }); const src = options.src.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); build.onLoad({ filter: new RegExp(`${src}/.*\\.toml$`) }, async (args) => { const file = await fs.promises.readFile(args.path, "utf-8"); const json = toml.parse(file); const filepath = path.parse(args.path); const key = path.relative(options.src, path.join(filepath.dir, filepath.name)); content.set(key, json); return { contents: compileDataFile(json), loader: "js", resolveDir: path.dirname(args.path) }; }); build.onLoad({ filter: new RegExp(`${src}/.*\\.ya?ml$`) }, async (args) => { const file = await fs.promises.readFile(args.path, "utf-8"); const json = yaml.load(file); const filepath = path.parse(args.path); const key = path.relative(options.src, path.join(filepath.dir, filepath.name)); content.set(key, json); return { contents: compileDataFile(json), loader: "js", resolveDir: path.dirname(args.path) }; }); build.onLoad({ filter: new RegExp(`${src}/.*\\.json$`) }, async (args) => { const file = await fs.promises.readFile(args.path, "utf-8"); const json = JSON.parse(file); const filepath = path.parse(args.path); const key = path.relative(options.src, path.join(filepath.dir, filepath.name)); content.set(key, json); return { contents: compileDataFile(json), loader: "js", resolveDir: path.dirname(args.path) }; }); build.onLoad({ filter: new RegExp(`${src}/.*\\.mdx?$`) }, async (args) => { const md = await fs.promises.readFile(args.path, "utf-8"); const filepath = path.parse(args.path); const key = path.relative(options.src, path.join(filepath.dir, filepath.name)); let language; const delimiters = md.substring(0, md.indexOf("\n")).trim(); if (delimiters === "+++") language = "toml"; const matter = grayMatter(md, { language, delimiters, engines: { toml: toml.parse.bind(toml) } }); content.set(key, matter.data); const file = await compileMdx(matter.content, { jsx: true, rehypePlugins: [rehypeHighlight], remarkPlugins: [remarkGfm] }); const lines = file.value.toString().split("\n"); const from = path.join(filepath.dir, filepath.name + ".mdjson"); const imports = Object.keys(matter.data).join(", "); lines.splice(1, 0, `import { ${imports} } from "${from}"`, `export { ${imports} } from "${from}"`); return { contents: lines.join("\n"), loader: "jsx" }; }); build.onResolve({ filter: /\.mdjson$/ }, args => ({ path: args.path, namespace: "mdjson" })); build.onLoad({ filter: /.*/, namespace: "mdjson" }, async (args) => { const filepath = path.parse(args.path); const key = path.relative(options.src, path.join(filepath.dir, filepath.name)); const matter = content.get(key); if (!matter) return; return { contents: compileDataFile(matter), loader: "js", resolveDir: path.dirname(args.path) }; }); build.onResolve({ filter: /^https?:\/\// }, args => ({ path: args.path, namespace: "remote" })); const http = new Map(); build.onLoad({ filter: /.*/, namespace: "remote" }, async (args) => { let contents = http.get(args.path); if (!contents) { contents = await fetch(args.path); http.set(args.path, contents); } return { contents, loader: "json" }; }); } }); function compileDataFile(obj) { const src = []; for (const [key, value] of Object.entries(obj)) { src.push(`export let ${key} = ${JSON.stringify(value)};`); } const index = indexObject(obj); let i = 1; for (const [path, value] of index) { if (typeof value !== "string") continue; const [, url] = value.match(/^url\(["'](.*)["']\)/) ?? []; if (!url) continue; const first = path.shift(); if (!first) continue; const bracketed = path.map(key => `["${key}"]`).join(""); src.push("", `import file${i} from "${url}";`, `${first}${bracketed} = file${i};`); i += 1; } return src.join("\n"); } function indexObject(obj) { if (Object(obj) !== obj) return [[[], obj]]; return Object.entries(obj).flatMap(([key, nested]) => { return indexObject(nested).map(([path, val]) => [[Array.isArray(obj) ? Number(key) : key, ...path], val]); }); }