markdown-rambler
Version:
Yet another opinionated & powerful static site generator.
843 lines (821 loc) • 29.8 kB
JavaScript
// 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
};