import { existsSync, readFileSync, readdirSync } from "node:fs"; import { extname, join, relative, resolve } from "node:path"; export interface ContentSchema { parse(input: unknown): T; } export interface ContentEntry> { id: string; slug: string; collection: string; data: T; body: string; html: string; excerpt: string; headings: ContentHeading[]; draft: boolean; version?: string; source: string; } export interface ContentHeading { depth: number; text: string; slug: string; } export interface ContentLoaderResult { id: string; source: string; content: string; } export interface ContentLoader { load(): ContentLoaderResult[] | Promise; } export interface ContentCollectionOptions { name: string; schema: ContentSchema; loader: ContentLoader; includeDrafts?: boolean; previewToken?: string; references?: Record>; } export interface ContentCollection { name: string; load(options?: { drafts?: boolean; previewToken?: string; version?: string; }): Promise[]>; get( id: string, options?: { drafts?: boolean; previewToken?: string; version?: string }, ): Promise | null>; } function scalar(value: string): unknown { const text = value.trim(); if (/^(true|false)$/i.test(text)) return text.toLowerCase() === "true"; if (/^-?\d+(?:\.\d+)?$/.test(text)) return Number(text); if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) return text.slice(1, -1); if (text.startsWith("[") && text.endsWith("]")) return text .slice(1, -1) .split(",") .map((item) => scalar(item)); return text; } export function parseFrontmatter(source: string): { data: Record; body: string } { if (!source.startsWith("---\n") && !source.startsWith("---\r\n")) return { data: {}, body: source }; const normalized = source.replace(/\r\n/g, "\n"); const end = normalized.indexOf("\n---\n", 4); if (end < 0) throw new Error("WRN-CONTENT-FRONTMATTER: closing delimiter is missing."); const data: Record = {}; for (const line of normalized.slice(4, end).split("\n")) { if (!line.trim() || line.trimStart().startsWith("#")) continue; const match = /^([A-Za-z_$][\w$.-]*):\s*(.*)$/.exec(line); if (!match) throw new Error(`WRN-CONTENT-FRONTMATTER: invalid line '${line}'.`); data[match[1]!] = scalar(match[2]!); } return { data, body: normalized.slice(end + 5) }; } const escape = (value: string) => value.replace( /[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]!, ); const slugify = (value: string) => value .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, ""); export function renderMarkdown(source: string): { html: string; headings: ContentHeading[]; excerpt: string; } { const headings: ContentHeading[] = []; const lines = source.replace(/\r\n/g, "\n").split("\n"); const output: string[] = []; let code: string[] | null = null; let language = ""; for (const line of lines) { const fence = /^```([\w-]*)/.exec(line); if (fence) { if (code) { output.push( `
${escape(code.join("\n"))}
`, ); code = null; } else { code = []; language = fence[1] ?? ""; } continue; } if (code) { code.push(line); continue; } const heading = /^(#{1,6})\s+(.+)$/.exec(line); if (heading) { const text = heading[2]!.trim(); const item = { depth: heading[1]!.length, text, slug: slugify(text) }; headings.push(item); output.push(`${escape(text)}`); } else if (line.trim()) output.push( `

${escape(line.trim()).replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+|\/[^\s)]*)\)/g, '$1')}

`, ); } if (code) throw new Error("WRN-CONTENT-MARKDOWN: code fence is not closed."); const excerpt = lines .find((line) => line.trim() && !line.startsWith("#") && !line.startsWith("---")) ?.trim() .slice(0, 240) ?? ""; return { html: output.join("\n"), headings, excerpt }; } export function localContentLoader(directory: string): ContentLoader { const root = resolve(directory); return { load() { if (!existsSync(root)) return []; const values: ContentLoaderResult[] = []; const walk = (current: string) => { for (const entry of readdirSync(current, { withFileTypes: true })) { if (entry.name.startsWith(".")) continue; const path = join(current, entry.name); if (entry.isDirectory()) walk(path); else if ([".md", ".mdx"].includes(extname(entry.name))) values.push({ id: relative(root, path) .replace(/\\/g, "/") .replace(/\.mdx?$/, ""), source: path, content: readFileSync(path, "utf8"), }); } }; walk(root); return values.sort((left, right) => left.id.localeCompare(right.id)); }, }; } export function remoteContentLoader( url: string, options: { fetch?: (input: string | URL | Request, init?: RequestInit) => Promise; headers?: HeadersInit; } = {}, ): ContentLoader { return { async load() { const response = await (options.fetch ?? globalThis.fetch)(url, { headers: options.headers }); if (!response.ok) throw new Error(`WRN-CONTENT-REMOTE: ${response.status}`); const value = (await response.json()) as Array<{ id: string; content: string; source?: string; }>; if (!Array.isArray(value)) throw new TypeError("WRN-CONTENT-REMOTE: expected an array."); return value.map((entry) => ({ id: entry.id, content: entry.content, source: entry.source ?? url, })); }, }; } export function defineCollection(options: ContentCollectionOptions): ContentCollection { const parse = (raw: ContentLoaderResult): ContentEntry => { const parsed = parseFrontmatter(raw.content); const rendered = renderMarkdown(parsed.body); const data = options.schema.parse(parsed.data); const record = data as Record; return { id: raw.id, slug: String(record.slug ?? raw.id), collection: options.name, data, body: parsed.body, ...rendered, draft: record.draft === true, version: typeof record.version === "string" ? record.version : undefined, source: raw.source, }; }; const load = async ( filter: { drafts?: boolean; previewToken?: string; version?: string } = {}, ) => { const preview = Boolean(options.previewToken && filter.previewToken === options.previewToken); const drafts = options.includeDrafts || filter.drafts || preview; return (await options.loader.load()) .map(parse) .filter( (entry) => (drafts || !entry.draft) && (!filter.version || entry.version === filter.version), ); }; return { name: options.name, load, async get(id, filter) { return (await load(filter)).find((entry) => entry.id === id || entry.slug === id) ?? null; }, }; } export function resolveContentReference( collections: Record>, reference: string, ) { const [collection, id] = reference.split(":", 2); if (!collection || !id || !collections[collection]) throw new Error(`WRN-CONTENT-REFERENCE: '${reference}' is invalid.`); return collections[collection].get(id); } export function paginateContent(entries: T[], page = 1, pageSize = 10) { if (!Number.isInteger(page) || page < 1 || !Number.isInteger(pageSize) || pageSize < 1) throw new RangeError("content pagination values must be positive integers"); const totalPages = Math.max(1, Math.ceil(entries.length / pageSize)); return { items: entries.slice((page - 1) * pageSize, page * pageSize), page, pageSize, total: entries.length, totalPages, hasNext: page < totalPages, hasPrevious: page > 1, }; } export function createSearchIndex(entries: ContentEntry[]) { return entries.map((entry) => ({ id: entry.id, slug: entry.slug, title: String((entry.data as Record).title ?? entry.id), text: `${entry.excerpt} ${entry.headings.map((heading) => heading.text).join(" ")}`.toLowerCase(), })); } export function searchContent(index: ReturnType, query: string) { const terms = query.toLowerCase().split(/\s+/).filter(Boolean); return index.filter((entry) => terms.every((term) => `${entry.title} ${entry.text}`.toLowerCase().includes(term)), ); } export function contentSitemap(entries: ContentEntry[], baseUrl: string) { return `${entries.map((entry) => `${escape(new URL(entry.slug, baseUrl).href)}`).join("")}`; } export function contentRss( entries: ContentEntry[], options: { title: string; baseUrl: string; description?: string }, ) { return `${escape(options.title)}${escape(options.baseUrl)}${escape(options.description ?? options.title)}${entries.map((entry) => `${escape(String((entry.data as Record<string, unknown>).title ?? entry.id))}${escape(new URL(entry.slug, options.baseUrl).href)}${escape(entry.id)}${escape(entry.excerpt)}`).join("")}`; } export interface CmsAdapter { list(collection: string): Promise>; } export function cmsContentLoader(adapter: CmsAdapter, collection: string): ContentLoader { return { async load() { return (await adapter.list(collection)).map((entry) => ({ ...entry, source: entry.source ?? `cms:${collection}:${entry.id}`, })); }, }; } export { renderMdxComponents, createIncrementalHighlighter, contentfulAdapter, sanityAdapter, strapiAdapter, } from "./advanced.ts"; export type { MdxComponent, SyntaxLanguageBundle, VendorAdapterOptions } from "./advanced.ts";