UNPKG

cascade-cards-source-markdown

Version:

Cascade Cards Markdown data source adapter

1 lines 12.8 kB
{"version":3,"sources":["../src/index.ts","../src/markdown-source.ts"],"sourcesContent":["export { MarkdownSource, markdownSource } from './markdown-source.js';\r\n","import fs from 'fs/promises';\r\nimport path from 'path';\r\nimport matter from 'gray-matter';\r\nimport { remark } from 'remark';\r\nimport remarkHtml from 'remark-html';\r\nimport fg from 'fast-glob';\r\nimport type { DataSource, DataSourceContent } from 'cascade-cards-core';\r\n\r\ninterface MarkdownSourceOptions {\r\n /** Glob pattern to find markdown files like 'docs/subfolder/file.md' */\r\n glob: string;\r\n /** Base directory to resolve relative paths (defaults to cwd) */\r\n baseDir?: string;\r\n /** Whether to cache parsed files in memory */\r\n cache?: boolean;\r\n /** Custom term resolver function */\r\n termResolver?: (term: string) => string | null;\r\n /** Whether to extract links from markdown content */\r\n extractLinks?: boolean;\r\n}\r\n\r\ninterface ParsedMarkdownFile {\r\n title: string;\r\n content: string;\r\n html: string;\r\n frontmatter: Record<string, any>;\r\n filePath: string;\r\n slug: string;\r\n links: Array<{ term: string; label?: string }>;\r\n}\r\n\r\nexport class MarkdownSource implements DataSource {\r\n name = 'markdown';\r\n private options: Required<MarkdownSourceOptions>;\r\n private cache: Map<string, ParsedMarkdownFile> = new Map();\r\n private fileMap: Map<string, string> = new Map(); // term -> filePath\r\n private initialized = false;\r\n\r\n constructor(options: MarkdownSourceOptions) {\r\n this.options = {\r\n baseDir: process.cwd(),\r\n cache: true,\r\n termResolver: (term) => this.defaultTermResolver(term),\r\n extractLinks: true,\r\n ...options\r\n };\r\n }\r\n\r\n async resolve(term: string): Promise<DataSourceContent | null> {\r\n await this.ensureInitialized();\r\n\r\n // Try to resolve term to a file path\r\n const filePath = this.options.termResolver(term);\r\n if (!filePath) {\r\n return null;\r\n }\r\n\r\n // Check if we have this file cached\r\n if (this.options.cache && this.cache.has(filePath)) {\r\n const cached = this.cache.get(filePath)!;\r\n return this.toDataSourceContent(cached);\r\n }\r\n\r\n try {\r\n // Read and parse the file\r\n const parsed = await this.parseMarkdownFile(filePath);\r\n \r\n if (this.options.cache) {\r\n this.cache.set(filePath, parsed);\r\n }\r\n\r\n return this.toDataSourceContent(parsed);\r\n } catch (error) {\r\n console.warn(`Failed to load markdown file for term \"${term}\":`, error);\r\n return null;\r\n }\r\n }\r\n\r\n private async ensureInitialized(): Promise<void> {\r\n if (this.initialized) return;\r\n\r\n try {\r\n // Find all markdown files\r\n const files = await fg(this.options.glob, {\r\n cwd: this.options.baseDir,\r\n absolute: true\r\n });\r\n\r\n // Build the term -> file mapping\r\n for (const filePath of files) {\r\n const relativePath = path.relative(this.options.baseDir, filePath);\r\n const slug = this.pathToSlug(relativePath);\r\n this.fileMap.set(slug, filePath);\r\n\r\n // Also map filename without extension\r\n const basename = path.basename(filePath, path.extname(filePath));\r\n this.fileMap.set(basename.toLowerCase(), filePath);\r\n }\r\n\r\n this.initialized = true;\r\n } catch (error) {\r\n console.error('Failed to initialize markdown source:', error);\r\n throw error;\r\n }\r\n }\r\n\r\n private defaultTermResolver(term: string): string | null {\r\n const normalizedTerm = term.toLowerCase();\r\n \r\n // Try exact match first\r\n if (this.fileMap.has(normalizedTerm)) {\r\n return this.fileMap.get(normalizedTerm)!;\r\n }\r\n\r\n // Try with common variations\r\n const variations = [\r\n normalizedTerm.replace(/\\s+/g, '-'),\r\n normalizedTerm.replace(/\\s+/g, '_'),\r\n normalizedTerm.replace(/[^a-z0-9]/g, ''),\r\n normalizedTerm.replace(/[^a-z0-9]/g, '-')\r\n ];\r\n\r\n for (const variation of variations) {\r\n if (this.fileMap.has(variation)) {\r\n return this.fileMap.get(variation)!;\r\n }\r\n }\r\n\r\n return null;\r\n }\r\n\r\n private pathToSlug(relativePath: string): string {\r\n return relativePath\r\n .replace(/\\.md$/, '')\r\n .replace(/\\\\/g, '/')\r\n .toLowerCase();\r\n }\r\n\r\n private async parseMarkdownFile(filePath: string): Promise<ParsedMarkdownFile> {\r\n const content = await fs.readFile(filePath, 'utf-8');\r\n const { data: frontmatter, content: markdownContent } = matter(content);\r\n\r\n // Convert markdown to HTML\r\n const processor = remark().use(remarkHtml, { sanitize: false });\r\n const result = await processor.process(markdownContent);\r\n const html = result.toString();\r\n\r\n // Extract title\r\n const title = frontmatter.title || \r\n this.extractTitleFromMarkdown(markdownContent) ||\r\n path.basename(filePath, path.extname(filePath));\r\n\r\n // Extract links if enabled\r\n const links: Array<{ term: string; label?: string }> = [];\r\n if (this.options.extractLinks) {\r\n links.push(...this.extractLinksFromContent(markdownContent, frontmatter));\r\n }\r\n\r\n const slug = this.pathToSlug(path.relative(this.options.baseDir, filePath));\r\n\r\n return {\r\n title,\r\n content: markdownContent,\r\n html,\r\n frontmatter,\r\n filePath,\r\n slug,\r\n links\r\n };\r\n }\r\n\r\n private extractTitleFromMarkdown(content: string): string | null {\r\n // Look for first # heading\r\n const match = content.match(/^#\\s+(.+)$/m);\r\n return match ? match[1].trim() : null;\r\n }\r\n\r\n private extractLinksFromContent(\r\n content: string, \r\n frontmatter: Record<string, any>\r\n ): Array<{ term: string; label?: string }> {\r\n const links: Array<{ term: string; label?: string }> = [];\r\n\r\n // Extract from frontmatter\r\n if (frontmatter.related && Array.isArray(frontmatter.related)) {\r\n links.push(...frontmatter.related.map((item: any) => \r\n typeof item === 'string' \r\n ? { term: item }\r\n : { term: item.term, label: item.label }\r\n ));\r\n }\r\n\r\n if (frontmatter.tags && Array.isArray(frontmatter.tags)) {\r\n links.push(...frontmatter.tags.map((tag: string) => ({ term: tag })));\r\n }\r\n\r\n // Extract markdown links that look like internal references\r\n const linkRegex = /\\[([^\\]]+)\\]\\(([^)]+)\\)/g;\r\n let match;\r\n \r\n while ((match = linkRegex.exec(content)) !== null) {\r\n const [, label, href] = match;\r\n \r\n // Only include if it looks like an internal reference\r\n if (!href.startsWith('http') && !href.startsWith('mailto:')) {\r\n const term = href.replace(/\\.md$/, '').replace(/^\\//, '');\r\n links.push({ term, label });\r\n }\r\n }\r\n\r\n // Extract [[wikilinks]] style references\r\n const wikiLinkRegex = /\\[\\[([^\\]]+)\\]\\]/g;\r\n while ((match = wikiLinkRegex.exec(content)) !== null) {\r\n const reference = match[1];\r\n const [term, label] = reference.includes('|') \r\n ? reference.split('|', 2)\r\n : [reference, undefined];\r\n \r\n links.push({ term: term.trim(), label: label?.trim() });\r\n }\r\n\r\n return links;\r\n }\r\n\r\n private toDataSourceContent(parsed: ParsedMarkdownFile): DataSourceContent {\r\n return {\r\n title: parsed.title,\r\n html: parsed.html,\r\n markdown: parsed.content,\r\n links: parsed.links,\r\n meta: {\r\n ...parsed.frontmatter,\r\n filePath: parsed.filePath,\r\n slug: parsed.slug\r\n }\r\n };\r\n }\r\n\r\n // Utility methods for external use\r\n async getAllTerms(): Promise<string[]> {\r\n await this.ensureInitialized();\r\n return Array.from(this.fileMap.keys());\r\n }\r\n\r\n async clearCache(): Promise<void> {\r\n this.cache.clear();\r\n }\r\n\r\n async reloadFiles(): Promise<void> {\r\n this.initialized = false;\r\n this.cache.clear();\r\n this.fileMap.clear();\r\n await this.ensureInitialized();\r\n }\r\n}\r\n\r\n// Factory function for easier usage\r\nexport function markdownSource(options: MarkdownSourceOptions): MarkdownSource {\r\n return new MarkdownSource(options);\r\n}\r\n"],"mappings":"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","names":["fg","path","fs","matter","remarkHtml"]}