UNPKG

cascade-cards-source-markdown

Version:

Cascade Cards Markdown data source adapter

174 lines 5.49 kB
// src/markdown-source.ts import fs from "fs/promises"; import path from "path"; import matter from "gray-matter"; import { remark } from "remark"; import remarkHtml from "remark-html"; import fg from "fast-glob"; var MarkdownSource = class { name = "markdown"; options; cache = /* @__PURE__ */ new Map(); fileMap = /* @__PURE__ */ new Map(); // term -> filePath initialized = false; constructor(options) { this.options = { baseDir: process.cwd(), cache: true, termResolver: (term) => this.defaultTermResolver(term), extractLinks: true, ...options }; } async resolve(term) { await this.ensureInitialized(); const filePath = this.options.termResolver(term); if (!filePath) { return null; } if (this.options.cache && this.cache.has(filePath)) { const cached = this.cache.get(filePath); return this.toDataSourceContent(cached); } try { const parsed = await this.parseMarkdownFile(filePath); if (this.options.cache) { this.cache.set(filePath, parsed); } return this.toDataSourceContent(parsed); } catch (error) { console.warn(`Failed to load markdown file for term "${term}":`, error); return null; } } async ensureInitialized() { if (this.initialized) return; try { const files = await fg(this.options.glob, { cwd: this.options.baseDir, absolute: true }); for (const filePath of files) { const relativePath = path.relative(this.options.baseDir, filePath); const slug = this.pathToSlug(relativePath); this.fileMap.set(slug, filePath); const basename = path.basename(filePath, path.extname(filePath)); this.fileMap.set(basename.toLowerCase(), filePath); } this.initialized = true; } catch (error) { console.error("Failed to initialize markdown source:", error); throw error; } } defaultTermResolver(term) { const normalizedTerm = term.toLowerCase(); if (this.fileMap.has(normalizedTerm)) { return this.fileMap.get(normalizedTerm); } const variations = [ normalizedTerm.replace(/\s+/g, "-"), normalizedTerm.replace(/\s+/g, "_"), normalizedTerm.replace(/[^a-z0-9]/g, ""), normalizedTerm.replace(/[^a-z0-9]/g, "-") ]; for (const variation of variations) { if (this.fileMap.has(variation)) { return this.fileMap.get(variation); } } return null; } pathToSlug(relativePath) { return relativePath.replace(/\.md$/, "").replace(/\\/g, "/").toLowerCase(); } async parseMarkdownFile(filePath) { const content = await fs.readFile(filePath, "utf-8"); const { data: frontmatter, content: markdownContent } = matter(content); const processor = remark().use(remarkHtml, { sanitize: false }); const result = await processor.process(markdownContent); const html = result.toString(); const title = frontmatter.title || this.extractTitleFromMarkdown(markdownContent) || path.basename(filePath, path.extname(filePath)); const links = []; if (this.options.extractLinks) { links.push(...this.extractLinksFromContent(markdownContent, frontmatter)); } const slug = this.pathToSlug(path.relative(this.options.baseDir, filePath)); return { title, content: markdownContent, html, frontmatter, filePath, slug, links }; } extractTitleFromMarkdown(content) { const match = content.match(/^#\s+(.+)$/m); return match ? match[1].trim() : null; } extractLinksFromContent(content, frontmatter) { const links = []; if (frontmatter.related && Array.isArray(frontmatter.related)) { links.push(...frontmatter.related.map( (item) => typeof item === "string" ? { term: item } : { term: item.term, label: item.label } )); } if (frontmatter.tags && Array.isArray(frontmatter.tags)) { links.push(...frontmatter.tags.map((tag) => ({ term: tag }))); } const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; let match; while ((match = linkRegex.exec(content)) !== null) { const [, label, href] = match; if (!href.startsWith("http") && !href.startsWith("mailto:")) { const term = href.replace(/\.md$/, "").replace(/^\//, ""); links.push({ term, label }); } } const wikiLinkRegex = /\[\[([^\]]+)\]\]/g; while ((match = wikiLinkRegex.exec(content)) !== null) { const reference = match[1]; const [term, label] = reference.includes("|") ? reference.split("|", 2) : [reference, void 0]; links.push({ term: term.trim(), label: label?.trim() }); } return links; } toDataSourceContent(parsed) { return { title: parsed.title, html: parsed.html, markdown: parsed.content, links: parsed.links, meta: { ...parsed.frontmatter, filePath: parsed.filePath, slug: parsed.slug } }; } // Utility methods for external use async getAllTerms() { await this.ensureInitialized(); return Array.from(this.fileMap.keys()); } async clearCache() { this.cache.clear(); } async reloadFiles() { this.initialized = false; this.cache.clear(); this.fileMap.clear(); await this.ensureInitialized(); } }; function markdownSource(options) { return new MarkdownSource(options); } export { MarkdownSource, markdownSource }; //# sourceMappingURL=index.js.map