UNPKG

markdown-rambler

Version:

Yet another opinionated & powerful static site generator.

843 lines (821 loc) 29.8 kB
// src/util/at.ts if (!("at" in Array.prototype)) { Array.prototype.at = function(index) { if (index >= 0) return this[index]; return this[this.length + index]; }; } // src/MarkdownRambler.ts import { EOL as EOL2 } from "os"; import { debuglog } from "util"; import { join as join3, extname as extname3 } from "path"; import { globby } from "globby"; import _2 from "lodash"; import { unified } from "unified"; import { toVFile, readSync } from "to-vfile"; import { matter } from "vfile-matter"; import { rename } from "vfile-rename"; import { mkdirp } from "vfile-mkdirp"; import remark2rehype from "remark-rehype"; import rehypeStringify2 from "rehype-stringify"; import { rss } from "xast-util-feed"; import { toXml } from "xast-util-to-xml"; import MiniSearch from "minisearch"; // src/mdast/parsers.ts import parse from "remark-parse"; import front from "remark-frontmatter"; import directives from "remark-directive"; import { h } from "hastscript"; import { visit } from "unist-util-visit"; // src/mdast/table.ts import { gfmTable } from "micromark-extension-gfm-table"; import { gfmTableFromMarkdown, gfmTableToMarkdown } from "mdast-util-gfm-table"; function table() { const data = this.data(); add("micromarkExtensions", gfmTable); add("fromMarkdownExtensions", gfmTableFromMarkdown); add("toMarkdownExtensions", gfmTableToMarkdown); function add(field, value) { data[field] = data[field] || []; data[field].push(value); } } // src/mdast/parsers.ts var parsers_default = [parse, front, table, directives]; var transformDirectives = (directives2) => (tree, vFile) => { const visitor = (node, index, parent) => { if (node.type === "textDirective" || node.type === "leafDirective" || node.type === "containerDirective") { const previousSibling = node.type === "textDirective" && parent.children[index - 1]; if (node.type === "textDirective" && previousSibling && !previousSibling.value.endsWith(" ")) { node.type = "text"; node.value = `:${node.name}`; return; } if (node.name in directives2) { const hast = directives2[node.name](node, index, parent, vFile); const data = node.data || (node.data = {}); data.hName = hast.tagName; data.hProperties = hast.properties; data.hChildren = hast.children; } else { const hast = h(node.name, node.attributes); const data = node.data || (node.data = {}); data.hName = hast.tagName; data.hProperties = hast.properties; } } }; visit(tree, visitor); }; // src/mdast/formatters.ts import remarkPrettier from "remark-prettier"; import remarkReferenceLinks from "remark-reference-links"; // src/unist/order-links.ts import { visit as visit2 } from "unist-util-visit"; import { is } from "unist-util-is"; import { remove } from "unist-util-remove"; function orderDefinitions() { return transformer; } function transformer(tree) { let id = 0; const refs = []; const defs = []; const store = []; visit2(tree, ["linkReference", "imageReference", "definition"], (node) => { if (is(node, ["linkReference", "imageReference"])) refs.push(node); if (is(node, "definition")) defs.push(node); }); refs.forEach((ref) => { const def = defs.find((d) => d.identifier === ref.identifier); const reusableDef = store.find((d) => d.url === def.url); if (reusableDef) { ref.identifier = reusableDef.identifier; ref.label = reusableDef.identifier; } else { const identifier = isNaN(ref.identifier) ? ref.identifier : String(++id); ref.identifier = identifier; ref.label = identifier; store.push({ type: "definition", title: def.title, url: def.url, identifier, label: identifier }); } }); remove(tree, "definition"); store.sort(definitionSorter).forEach((def) => { tree.children.push(def); }); } function definitionSorter(a, b) { if (isNaN(a.identifier) && !isNaN(b.identifier)) return 1; if (!isNaN(a.identifier) && isNaN(b.identifier)) return -1; if (!isNaN(b.identifier) && !isNaN(a.identifier)) return Number(a.identifier) - Number(b.identifier); return a.identifier - b.identifier; } var order_links_default = orderDefinitions; // src/mdast/formatters.ts var formatters_default = [remarkPrettier, remarkReferenceLinks, order_links_default]; // src/mdast/helpers.ts import { visit as visit3 } from "unist-util-visit"; var getDocumentTitle = (tree) => { let title = ""; visit3(tree, "heading", (node) => { if (node.depth === 1) { title = node.children[0].value; return false; } }); return title; }; var removeDocumentTitle = () => (tree) => { visit3(tree, "heading", (node, index, parent) => { if (node.depth === 1) { parent == null ? void 0 : parent.children.splice(index, 1); return false; } }); }; // src/hast/transformers.ts import { extname as extname2 } from "path"; import urls from "rehype-urls"; import slug from "rehype-slug"; import autoLinkHeadings from "rehype-autolink-headings"; import doc from "rehype-document"; // src/hast/plugins.ts import { h as h2 } from "hastscript"; // src/hast/traversal.ts import { visit as visit4 } from "unist-util-visit"; var findElement = (tree, test, callback) => { const matcher = typeof test === "function" ? test : (node) => node.tagName === test; visit4(tree, "element", (node, index, parent) => { if (matcher(node) && parent && index !== null) { return callback(node, index, parent); } }); }; var append = (tree, test, ...elements) => { findElement(tree, test, (node) => { node.children.push(...elements); return false; }); return tree; }; var insertBefore = (tree, test, element) => { findElement(tree, test, (node, index, parent) => { parent == null ? void 0 : parent.children.splice(index, 0, element); return false; }); return tree; }; var insertBeforeStylesheets = (tree, element) => { const test = (node) => { var _a; const rel = [(_a = node == null ? void 0 : node.properties) == null ? void 0 : _a.rel].flat(); return node.tagName === "link" && rel.includes("stylesheet"); }; return insertBefore(tree, test, element); }; // src/hast/plugins.ts var addInlineScript = ({ type, content }) => (tree) => { const script = h2("script", { type }, content); return append(tree, "body", script); }; var addScript = ({ src }) => (tree) => { const script = h2("script", { src }); return append(tree, "body", script); }; // src/util/index.ts import { isAbsolute, join } from "path"; var groupByType = (vFiles) => vFiles.reduce((group, vFile) => { const { type } = vFile.data.meta; group[type] = group[type] ?? []; group[type].push(vFile); return group; }, {}); var getDefaults = (type, defaults) => { if (!type || type === "page" || !(type in defaults)) { return (defaults == null ? void 0 : defaults.page) ?? {}; } return { ...defaults.page, ...defaults[type] }; }; var resolveFrontMatter = (matter2, vFile) => { const obj = {}; for (const key in matter2) { switch (key) { case "tags": const tags = typeof matter2[key] === "string" ? matter2[key].split(/[ ,]+/) : matter2[key]; obj[key] = tags; break; case "image": const image = typeof matter2[key] === "string" ? { src: matter2[key] } : matter2[key]; image.src = isAbsolute(image.src) ? image.src : join(vFile.data.pathname, image.src); obj[key] = image; break; case "published": case "modified": obj[key] = new Date(matter2[key]); break; default: obj[key] = matter2[key]; break; } } return obj; }; var buildMetaData = (vFile, type, options) => { const defaults = getDefaults(type, options.defaults); const { pathname } = vFile.data; const base = { type, host: options.host, pathname, href: options.host + pathname, name: options.name, language: options.language, manifest: options.manifest, feed: options.feed }; const matter2 = resolveFrontMatter(vFile.data.matter, vFile); const pageStylesheets = typeof matter2.stylesheets === "string" ? [matter2.stylesheets] : matter2.stylesheets ?? []; const pageScripts = typeof matter2.scripts === "string" ? [matter2.scripts] : matter2.scripts ?? []; delete matter2.stylesheets; delete matter2.scripts; return Object.assign(base, defaults, matter2, { pageStylesheets: pageStylesheets.map((sheet) => join(pathname, sheet)), pageScripts: pageScripts.map((script) => join(pathname, script)) }); }; var iso = (value) => !value ? void 0 : (typeof value === "string" ? new Date(value) : value).toISOString(); var unique = (value, index, self) => self.indexOf(value) === index; var ucFirst = (value) => value[0].toUpperCase() + value.slice(1); // src/hast/metaTags.ts var getMetaTags = (meta) => { var _a; const tags = []; const type = meta.type === "article" ? "article" : "website"; tags.push({ property: "og:type", content: type }); if (meta.draft) tags.push({ name: "robots", content: "noindex" }); if (meta.description) tags.push({ name: "description", property: "og:description", content: meta.description }); if (meta.name) tags.push({ property: "og:site_name", content: meta.name }); if (meta.href) tags.push({ property: "og:url", content: meta.href }); if (meta.author) tags.push({ name: "author", content: meta.author.name }); if (meta.published) tags.push({ property: "article:published_time", content: iso(meta.published) }); if (meta.modified) tags.push({ property: "article:modified_time", content: iso(meta.modified) }); if ((_a = meta.author) == null ? void 0 : _a.twitter) { tags.push( { name: "twitter:title", property: "og:title", content: meta.title }, { name: "twitter:description", content: meta.description }, { name: "twitter:card", content: "summary_large_image" }, { name: "twitter:image", property: "og:image", content: meta.host + meta.image.src }, { name: "twitter:site", content: meta.author.twitter }, { name: "twitter:creator", content: meta.author.twitter }, { name: "twitter:image:alt", content: meta.title } ); } return tags; }; // src/hast/linkTags.ts import { extname } from "path"; var mimeTypes = { ".ico": "image/vnd.microsoft.icon", ".jpeg": "image/jpeg", ".jpg": "image/jpeg", ".png": "image/png", ".svg": "image/svg+xml", ".webp": "image/webp" }; var getLinkTags = (meta) => { const tags = [ { rel: "icon", href: "/favicon.ico", sizes: "any" }, { rel: "apple-touch-icon", href: "/apple-touch-icon.png" } ]; if (meta.manifest) tags.push({ rel: "manifest", href: meta.manifest }); if (meta.host && meta.href) tags.push({ rel: "canonical", href: meta.href }); const stylesheets = meta.bundledStylesheets ?? meta.stylesheets; if (stylesheets) tags.push(...stylesheets.map((href) => ({ rel: "stylesheet", href }))); if (meta.pageStylesheets) tags.push(...meta.pageStylesheets.map((href) => ({ rel: "stylesheet", href }))); if (meta.icon) { const ext = extname(meta.icon.src); const type = mimeTypes[ext]; tags.push({ rel: "icon", href: meta.icon.src, type }); } if (meta.prefetch) { tags.push({ rel: "prefetch", href: meta.prefetch }); } if (meta.feed) { tags.push({ rel: "alternate", type: "application/rss+xml", href: meta.host + meta.feed.pathname, title: meta.feed.title }); } return tags; }; // src/util/fs.ts import { EOL } from "os"; import { watch, constants } from "fs"; import { access, mkdir, readFile, writeFile, copyFile, appendFile } from "fs/promises"; import { resolve, dirname, join as join2 } from "path"; import { optimize } from "svgo"; var isFloating = (pathname) => !/^[\.\/]/.test(pathname); var isIndex = (filename) => /(README|index)\.md$/.test(filename); var resolveTargetPathname = (from, to) => { const fromPathname = resolvePathname(from); const [, filename, suffix] = to.match(/(?<filename>[^\?#]*)(?<suffix>.*)/); const toPathname = resolve(fromPathname, ...[isIndex(from) ? "." : ".."], resolvePathname(filename)); return toPathname + suffix; }; var resolvePathname = (filename) => { return (isFloating(filename) ? "/" : "") + filename.replace(/(README|index)?\.md$/, "").replace(/(.+)\/$/, "$1"); }; var ensureDir = async (target) => { const dir = dirname(target); try { await access(dir, constants.W_OK); } catch (error) { await mkdir(dir, { recursive: true }); } }; var isDir = async (dir) => { try { await access(dir, constants.W_OK); return true; } catch (error) { return false; } }; var write = async (target, output) => { await ensureDir(target); writeFile(target, output); }; var copy = async (source, target) => { await ensureDir(target); copyFile(source, target); }; var append2 = async (source, target) => { await appendFile(target, await readFile(source) + EOL); }; var optimizeSVG = async (source, target) => { const optimized = optimize(await readFile(source), { path: source, plugins: ["removeDimensions", { name: "removeAttrs", params: { attrs: "content" } }] }); await ensureDir(target); await writeFile(target, optimized.data, "utf8"); }; var isInIgnoreDir = (file, ignorePatterns) => { return ignorePatterns.some((dir) => { if (dir instanceof RegExp) return dir.test(file); if (typeof dir === "string") return new RegExp(dir).test(file); if (typeof dir === "function") return dir(file); throw new Error(`Invalid ignoreDir value (${dir})`); }); }; var watchDir = async ({ dir, cb, ignorePatterns }) => { const callback = (eventType, filename) => { const file = join2(dir, filename); if (isInIgnoreDir(file, ignorePatterns)) return; if (eventType === "change") { if (filename) { cb([dir, filename]); } else { console.warn("Filename not provided for", eventType, filename); } } }; if (await isDir(dir)) { watch(dir, { recursive: true }, callback); return dir; } }; // src/hast/transformers.ts var fixRelativeLinks = (vFile) => (url, node) => { const { href, pathname } = url; if (href.startsWith(".") && (extname2(pathname) === ".md" || node.tagName === "img")) { return resolveTargetPathname(vFile.history.at(0), href); } return url; }; var defaultTransformers = (vFile) => { const { data } = vFile; const { meta, structuredContent } = data; const { title, language, scripts, bundledScripts, pageScripts } = meta; const js = [...bundledScripts ?? scripts ?? [], ...pageScripts ?? []]; return [ [() => urls(fixRelativeLinks(vFile))], // Hack to allow rehype-urls plugin again downstream slug, [autoLinkHeadings, { behavior: "wrap", test: ["h2", "h3", "h4", "h5", "h6"] }], [doc, { title, language, meta: getMetaTags(meta), link: getLinkTags(meta), js }], [addInlineScript, { type: "application/ld+json", content: JSON.stringify(structuredContent) }] ]; }; var transformers_default = defaultTransformers; // src/hast/render.ts import rehypeStringify from "rehype-stringify"; import rehypeFormat from "rehype-format"; var render_default = [rehypeFormat, rehypeStringify]; // src/util/structured-content.ts import _ from "lodash"; var getPage = (meta, base) => _.merge( {}, base, { "@type": "WebSite", sameAs: meta.sameAs }, meta.structuredContent ); var getArticle = (meta, base) => { var _a, _b, _c; const image = ((_a = meta.image) == null ? void 0 : _a.src) || ((_c = (_b = meta.publisher) == null ? void 0 : _b.logo) == null ? void 0 : _c.src); return _.merge( {}, base, { "@type": "Article", headline: meta.title, description: meta.description }, image ? { image } : {}, meta.structuredContent ); }; var getStructuredContent = (meta) => { var _a, _b, _c; const base = { "@context": "https://schema.org", "@type": "WebSite", mainEntityOfPage: { "@type": "WebPage", "@id": meta.href }, datePublished: iso(meta.published), dateModified: iso(meta.modified), inLanguage: meta.language }; if (((_a = meta.tags) == null ? void 0 : _a.length) > 0) { base.keywords = meta.tags.join(","); } if (meta.author) { base.author = { "@type": "Person", name: meta.author.name, url: meta.author.href }; } if (meta.publisher) { base.publisher = { "@type": "Organization", "@id": `${meta.publisher.href}/#organization`, name: meta.publisher.name, logo: { "@type": "ImageObject", url: meta.publisher.logo.src } }; } const type = ((_c = (_b = meta.structuredContent) == null ? void 0 : _b["@type"]) == null ? void 0 : _c.toLowerCase()) ?? meta.type; switch (type) { case "article": return getArticle(meta, base); default: return getPage(meta, base); } }; // src/hast/layout.ts import htm from "htm"; import { h as h3 } from "hastscript"; var html = htm.bind(h3); var layout = (options) => (node, vFile) => { const children = options.layout(node.children, vFile.data.meta); return { ...node, children: Array.isArray(children) ? children : [children] }; }; // src/MarkdownRambler.ts var debug = debuglog("markdown-rambler"); var dbg = (vFile, text) => { const filename = typeof vFile === "string" ? vFile : vFile.history.at(0); const padded = filename.length > 28 ? `...${filename.slice(-25)}` : filename.padStart(28); debug(`[${padded}] ${text}`); }; var MarkdownRambler = class { constructor(options = {}) { this.options = Object.freeze(this.setDefaultOptions(options)); } setDefaultOptions(options) { return _2.defaultsDeep(options, { contentDir: !options.contentFiles ? "content" : ".", contentFiles: "**/*", publicDir: "public", outputDir: "dist", ignorePattern: /^(\.|node_modules)/, host: "", name: "", language: "en", manifest: false, sitemap: true, feed: false, search: false }); } getTransformers(vFile) { const { rehypePlugins = [] } = this.options; const plugins = [ typeof transformers_default === "function" ? transformers_default(vFile) : transformers_default, typeof rehypePlugins === "function" ? rehypePlugins(vFile) : rehypePlugins ]; return plugins.flat(); } /** @public */ async run() { const files = await this.getContentFiles(); if (!this.options.watch && this.options.publicDir) { await this.bundleAssets("stylesheets"); await this.bundleAssets("scripts"); } const markdownFiles = files.filter(([dir, filename]) => filename.endsWith(".md")); const parsedVFiles = await this.parseMarkdownFiles(markdownFiles); if (this.options.linkFiles) { const files2 = groupByType(parsedVFiles); parsedVFiles.forEach((vFile) => { vFile.data.vFiles = files2; }); } const vFiles = await this.renderMarkdownFiles(parsedVFiles); vFiles.map(async (vFile) => { dbg(vFile, `Writing ${vFile.history.at(-1)}`); this.verbose(`Writing ${vFile.history.at(-1)}`); await mkdirp(vFile); await toVFile.write(vFile); }); const assetFiles = files.filter(([dir, filename]) => !filename.endsWith(".md")); await Promise.all(assetFiles.map((file) => this.copyAsset(file))); console.log(`\u2714 ${vFiles.length} pages and ${assetFiles.length} asset files (in ${this.options.outputDir})`); const publishedVFiles = vFiles.filter((vFile) => !vFile.data.meta.draft); if (this.options.feed) { const feed = await this.renderFeed(publishedVFiles); if (feed) console.log(`\u2714 Feed (at ${feed})`); } if (this.options.sitemap) { const sitemap = await this.renderSitemap(publishedVFiles); if (sitemap) console.log(`\u2714 Sitemap (at ${sitemap})`); } if (this.options.search) { const search = await this.renderSearchIndex(publishedVFiles); if (search) console.log(`\u2714 Search index (at ${search})`); } if (this.options.watch) { const watchDirs = await this.watch([this.options.contentDir, this.options.publicDir].flat().filter(unique)); watchDirs.forEach((dir) => console.log(`\u25B6\uFE0E Watching for changes in ${dir}`)); } } async getContentFiles() { const dirs = [this.options.publicDir, this.options.contentDir].flat().filter(unique); const glob = [this.options.contentFiles].flat().filter(unique); const files = []; for (const cwd of dirs) { const result = await globby(glob, { cwd, ignore: ["**/node_modules"] }); files.push(result.map((filename) => [cwd ?? "", filename])); } return files.flat(); } async bundleAssets(assetType) { const bundled = `bundled${ucFirst(assetType)}`; const pageTypes = Object.keys(this.options.defaults); const orderedPageTypes = ["page", ...pageTypes].filter(unique); const assets = {}; for (const pageType of orderedPageTypes) { assets[pageType] = assets[pageType] ?? []; if (this.options.defaults[pageType][assetType]) { for (const asset of this.options.defaults[pageType][assetType]) { if (!assets[pageType].includes(asset)) { const source = join3(this.options.publicDir, asset); const ext = extname3(source); const bundle = join3(asset, `../${pageType}.bundle${ext}`); const target = join3(this.options.outputDir, bundle); if (pageType !== "page" && assets.page.includes(asset)) { } else if (assets[pageType].length === 0) { dbg(source, `Copying ${target} (${asset})`); this.verbose(`Copying ${target} (${asset})`); await copy(source, target); assets[pageType].push(asset); if (pageType === "page") { this.options.defaults[pageType][bundled] = [bundle]; } else { this.options.defaults[pageType][bundled] = [this.options.defaults.page[bundled][0], bundle]; } } else { dbg(source, `Appending ${source} to ${target}`); this.verbose(`Appending ${source} to ${target}`); await append2(source, target); assets[pageType].push(asset); } } } } } } async copyAsset(file) { const [dir, filename] = file; const source = join3(dir, filename); const target = join3(this.options.outputDir, filename); if (filename.endsWith(".svg")) { dbg(source, `Optimizing to ${target}`); this.verbose(`Optimizing ${target}`); await optimizeSVG(source, target); } else { dbg(source, `Copying to ${target}`); this.verbose(`Copying ${target}`); await copy(source, target); } } async handleFile(file) { const [dir, filename] = file; if (filename.endsWith(".md")) { const parsedVFile = await this.parseMarkdownFile(file); const vFile = await this.renderMarkdownFile(parsedVFile); dbg(vFile, `Writing ${vFile.history.at(-1)}`); this.verbose(`Writing ${vFile.history.at(-1)}`); await mkdirp(vFile); await toVFile.write(vFile); } else { this.copyAsset(file); } } async parseMarkdownFile([dir, filename]) { const source = join3(dir, filename); const vFile = readSync(source); vFile.history.unshift(filename); return this.parseMarkdownVFile(vFile); } async parseMarkdownVFile(vFile) { dbg(vFile, `Parsing source file`); const options = this.options; const outputDir = options.outputDir; const parsers = options.parsers ?? parsers_default; const remarkPlugins = options.remarkPlugins ?? []; const parser = unified().use([...parsers, ...remarkPlugins]); const tree = parser.parse(vFile); if (options.formatMarkdown) { dbg(vFile, `Formatting`); const plugins = [parsers_default[0], parsers_default[1], ...formatters_default]; const formattedVFile = await unified().use(plugins).process(String(vFile)); if (formattedVFile.value.toString() !== vFile.value.toString()) { this.verbose(`Formatting ${vFile.history.at(-1)}`); await write(vFile.history.at(-1), formattedVFile.value); } } const filename = vFile.history.at(0); const pathname = resolvePathname(filename); vFile = rename(vFile, { dirname: join3(outputDir, pathname), stem: "index", extname: ".html" }); dbg(vFile, `Rendered file will be written to ${vFile.history.at(-1)}`); const type = typeof options.type === "function" ? options.type(filename, vFile.data.matter) || "page" : "page"; matter(vFile); vFile.data.pathname = pathname; const meta = buildMetaData(vFile, type, options); vFile.data.markdown = String(vFile.value); vFile.data.tree = tree; vFile.data.meta = meta; vFile.data.meta.title = meta.title ?? getDocumentTitle(tree); vFile.data.structuredContent = getStructuredContent(meta); dbg(vFile, `The ${type} "${meta.title}" will be served from ${pathname}`); return vFile; } parseMarkdownFiles(files) { return Promise.all(files.map((file) => this.parseMarkdownFile(file))); } renderMarkdownFiles(vFiles) { return Promise.all(vFiles.map((vFile) => this.renderMarkdownFile(vFile))); } async renderMarkdownFile(vFile) { dbg(vFile, `Rendering ${vFile.history.at(-1)}`); const { options } = this; const transformers = this.getTransformers(vFile); const renderers = options.renderers ?? render_default; const processor = unified().use(transformDirectives, options.directives ? options.directives : false).use(remark2rehype, options.remarkRehypeOptions ?? {}).use(layout, vFile.data.meta.layout ? { layout: vFile.data.meta.layout } : false).use(transformers).use(renderers); const tree = await processor.run(vFile.data.tree, vFile); vFile.value = processor.stringify(tree); return vFile; } async renderFeed(vFiles) { if (!this.options.feed) return; if (!this.options.host) { this.verbose("Unable to render RSS feed without `host`"); return; } const { filter } = this.options.feed; const filtered = typeof filter === "function" ? vFiles.filter((vFile) => filter(vFile.data.meta.type, vFile)) : [...vFiles]; const items = await Promise.all( filtered.sort((a, b) => +new Date(b.data.meta.published) - +new Date(a.data.meta.published)).slice(0, 10).map(async (vFile) => { const plugins = [removeDocumentTitle, remark2rehype, rehypeStringify2]; const processor = unified().use(plugins); const tree = await processor.run(vFile.data.tree); const html2 = processor.stringify(tree); const { meta } = vFile.data; return { title: meta.title, description: meta.description, descriptionHtml: html2, author: meta.author, url: join3(this.options.host, meta.pathname), modified: meta.modified, published: meta.published, tags: meta.tags }; }) ); const filename = join3(this.options.outputDir, this.options.feed.pathname); const title = this.options.feed.title ?? this.options.name; const description = this.options.feed.description ?? this.options.defaults.page.description; const author = this.options.feed.author ?? this.options.defaults.page.author.name; const contents = rss( { title, description, author, tags: this.options.feed.tags, url: this.options.host, lang: this.options.language, feedUrl: join3(this.options.host, this.options.feed.pathname) }, items ); await write(filename, toXml(contents)); return filename; } async renderSitemap(vFiles) { if (!this.options.host) { this.verbose("Unable to render sitemap without `host`"); return; } const { host } = this.options; const filename = join3(this.options.outputDir, "sitemap.txt"); const items = vFiles.map((vFile) => vFile.data.meta.href); await write(filename, items.sort().join(EOL2) + EOL2); return filename; } async renderSearchIndex(vFiles) { const defaults = { outputDir: "_search", filter: () => true }; const options = this.options.search === true ? defaults : _2.defaults(this.options.search, defaults); const documents = vFiles.filter((vFile) => options.filter(vFile.data.meta.type, vFile)).map((vFile, index) => ({ id: index, title: vFile.data.meta.title, description: vFile.data.meta.description, pathname: vFile.data.meta.pathname, content: vFile.data.markdown, tags: vFile.data.meta.tags })); const miniSearch = new MiniSearch({ fields: ["title", "tags", "description", "content"], storeFields: ["title", "description", "pathname"] }); await miniSearch.addAllAsync(documents); const filename = join3(this.options.outputDir, options.outputDir, "index.json"); await write(filename, JSON.stringify(miniSearch.toJSON())); return filename; } async watch(dirs) { const ignorePatterns = [this.options.outputDir, this.options.ignorePattern].flat(); if (dirs) { const watchDirs = await Promise.all( dirs.map((dir) => watchDir({ dir, cb: (file) => this.handleFile(file), ignorePatterns })) ); return watchDirs.filter(Boolean); } return []; } verbose(text) { if (this.options.verbose) { console.log(text); } } }; export { MarkdownRambler, addScript, append, findElement, html, insertBefore, insertBeforeStylesheets };