MasatoMakino/qiita-to-md

View on GitHub
src/JsonGenerator.ts

Summary

Maintainability
A
1 hr
Test Coverage
import { format } from "date-fns";
import fs from "fs/promises";
import path from "path";

import rehypeHighlight from "rehype-highlight";
import rehypeStringify from "rehype-stringify";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import retextStringify from "retext-stringify";
import strip from "strip-markdown";
import { unified } from "unified";

import { MarkdownDownloader } from "./index.js";
import { Options, OptionsUtil } from "./Options.js";
import { RemarkLinkCardPlugin } from "./plugin/RemarkLinkCardPlugin.js";
import { RemarkNotePlugin } from "./plugin/RemarkNotePlugin.js";

export class JsonGenerator {
  public static async generate(options?: Options) {
    options = OptionsUtil.init(options);
    const items = await MarkdownDownloader.download(options);

    //記事jsonの生成と保存
    for (let item of items) {
      item.bodyHtml = await this.convertToHTML(item.bodyContent);
      item.preview = await this.convertToPreview(item.bodyContent);
      await fs.writeFile(
        this.getFilePath(item, options),
        JSON.stringify(item, null, 2)
      );
    }

    //記事オブジェクトからサマリーjsonの生成
    const summary = { fileMap: {}, sourceFileArray: [] };
    for (let item of items) {
      const filePath = path.relative(
        process.cwd(),
        this.getFilePath(item, options)
      );
      summary.fileMap[filePath] = {
        title: item.title,
        created_at: item.created_at,
        categories: item.categories,
        preview: item.preview,
        dir: path.join(options.contentsDir, options.jsonDir),
        base: this.getFileBase(item),
      };
      summary.sourceFileArray.unshift(filePath);
    }

    //サマリーの保存
    await fs.writeFile(
      path.resolve(options.contentsDir, options.jsonDir, `summary.json`),
      JSON.stringify(summary, null, 2)
    );
  }

  /**
   * 記事情報からjsonファイルの保存パスを生成する
   * @param item
   * @param options
   * @private
   */
  private static getFilePath(item, options?: Options): string {
    return path.resolve(
      options.contentsDir,
      options.jsonDir,
      this.getFileBase(item)
    );
  }

  /**
   * 記事作成日付とIDからユニークなファイル名を生成する
   * @param item
   * @private
   */
  private static getFileBase(item): string {
    return `${format(new Date(item.created_at), "yyyy-MM-dd-HHmmss_")}${
      item.id
    }.json`;
  }

  /**
   * マークダウン本文からHTMLを生成する
   * @param body
   */
  public static async convertToHTML(body: string) {
    const result = await unified()
      .use(remarkParse) // markdown -> mdast の変換
      .use(RemarkNotePlugin.plugin)
      .use(RemarkLinkCardPlugin.plugin)
      .use(remarkRehype, {
        handlers: {
          note: RemarkNotePlugin.rehypeNoteHandler as any,
          LinkCard: RemarkLinkCardPlugin.rehypeHandler as any,
        },
      }) // mdast -> hast の変換
      .use(rehypeHighlight, { ignoreMissing: true })
      .use(rehypeStringify) // hast -> html の変換
      .process(body); // 実行

    return result.value;
  }

  /**
   * マークダウン本文からプレビュー用プレーンテキストを生成する
   * @param body
   */
  static async convertToPreview(body: string) {
    const result = await unified()
      .use(remarkParse) // markdown -> mdast の変換
      .use(strip)
      .use(retextStringify)
      .process(body); // 実行
    return result.value.toString().substring(0, 600);
  }
}