cascade-cards-source-markdown
Version: 
Cascade Cards Markdown data source adapter
212 lines (210 loc) • 7.5 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
  MarkdownSource: () => MarkdownSource,
  markdownSource: () => markdownSource
});
module.exports = __toCommonJS(index_exports);
// src/markdown-source.ts
var import_promises = __toESM(require("fs/promises"), 1);
var import_path = __toESM(require("path"), 1);
var import_gray_matter = __toESM(require("gray-matter"), 1);
var import_remark = require("remark");
var import_remark_html = __toESM(require("remark-html"), 1);
var import_fast_glob = __toESM(require("fast-glob"), 1);
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 (0, import_fast_glob.default)(this.options.glob, {
        cwd: this.options.baseDir,
        absolute: true
      });
      for (const filePath of files) {
        const relativePath = import_path.default.relative(this.options.baseDir, filePath);
        const slug = this.pathToSlug(relativePath);
        this.fileMap.set(slug, filePath);
        const basename = import_path.default.basename(filePath, import_path.default.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 import_promises.default.readFile(filePath, "utf-8");
    const { data: frontmatter, content: markdownContent } = (0, import_gray_matter.default)(content);
    const processor = (0, import_remark.remark)().use(import_remark_html.default, { sanitize: false });
    const result = await processor.process(markdownContent);
    const html = result.toString();
    const title = frontmatter.title || this.extractTitleFromMarkdown(markdownContent) || import_path.default.basename(filePath, import_path.default.extname(filePath));
    const links = [];
    if (this.options.extractLinks) {
      links.push(...this.extractLinksFromContent(markdownContent, frontmatter));
    }
    const slug = this.pathToSlug(import_path.default.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);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  MarkdownSource,
  markdownSource
});
//# sourceMappingURL=index.cjs.map