MasatoMakino/qiita-to-md

View on GitHub
src/plugin/RemarkNotePlugin.ts

Summary

Maintainability
A
1 hr
Test Coverage
import { all, H } from "mdast-util-to-hast";
import { Plugin } from "unified";
import { visit } from "unist-util-visit";

export class RemarkNotePlugin {
  private static readonly NOTE_REGEXP = /^:::\s*note[ \t]*([a-z]*)\n/m;
  private static readonly NOTE_ENDING: string = "\n:::";

  public static plugin: Plugin = () => {
    return (tree) => {
      visit(tree, this.isNote, this.visitor);
    };
  };

  /**
   * パラグラフがnote記法に準じているか否かを判定する
   * @param node
   * @private
   */
  private static isNote(node): boolean {
    if (!RemarkNotePlugin.isTextParagraph(node)) return false;
    return RemarkNotePlugin.isNoteParagraph(node);
  }

  private static isTextParagraph(node): boolean {
    return (
      node.type == "paragraph" &&
      node.children &&
      node.children[0].type === "text" &&
      node.children.at(-1).type === "text"
    );
  }

  private static getFirstIndex(node): number {
    return node.children.findIndex((child) => {
      return child.value.match(RemarkNotePlugin.NOTE_REGEXP);
    });
  }
  private static getLastIndex(node): number {
    return node.children.findIndex((child) => {
      return child.value.endsWith(RemarkNotePlugin.NOTE_ENDING);
    });
  }

  private static isNoteParagraph(node): boolean {
    const startIndex = this.getFirstIndex(node);
    if (startIndex === -1) return false;

    const endIndex = this.getLastIndex(node);
    if (endIndex === -1) return false;

    return startIndex <= endIndex;
  }

  private static visitor(node, index, parent) {
    const responseFirstBlock = RemarkNotePlugin.processFirstChild(node);
    const responseLastBlock = RemarkNotePlugin.processLastChild(
      node,
      RemarkNotePlugin.NOTE_ENDING,
    );

    parent.children[index] = {
      type: "note",
      properties: { className: ["note", responseFirstBlock.noteType] },
      children: node.children,
    };
  }

  /**
   * noteパラグラフの先端を削除する
   * @param node
   * @private
   */
  private static processFirstChild(node): { noteType: string; index: number } {
    const index = this.getFirstIndex(node);
    const firstChild = node.children[index];
    const firstValue = firstChild.value as string;

    const noteType = RemarkNotePlugin.getNoteType(firstValue);
    node.children[index] = {
      ...firstChild,
      value: firstValue.slice(
        RemarkNotePlugin.getNoteFirstLineLength(firstValue),
      ),
    };

    return { noteType, index };
  }

  /**
   * noteパラグラフの末端を削除する
   * @param node
   * @param identifier
   * @private
   */
  private static processLastChild(node, identifier: string) {
    const lastIndex = this.getLastIndex(node);
    const lastChild = node.children[lastIndex];
    const lastValue = lastChild.value as string;
    node.children[lastIndex] = {
      ...lastChild,
      value: lastValue.slice(0, lastValue.length - identifier.length),
    };
    return { lastIndex };
  }

  private static getNoteType(value: string): string {
    const match = value.match(RemarkNotePlugin.NOTE_REGEXP);
    return match?.[1];
  }

  private static getNoteFirstLineLength(value: string): number {
    const match = value.match(RemarkNotePlugin.NOTE_REGEXP);
    return match?.[0].length;
  }

  /**
   * note型を指定されたmdastをhastのdivに変換する
   * @param h
   * @param node
   */
  public static rehypeNoteHandler(h: H, node: any) {
    return {
      type: "element",
      tagName: "div",
      properties: {
        className: node.properties.className,
      },
      children: all(h, node),
    };
  }
}