📚

Tiptapコードリーディング:Paragraph

に公開

前回
https://zenn.dev/karintou/articles/bedb7a2719b999

TiptapコードリーディングのParagprph編です。
少し雑に続けることが目標。

Paragraph

(version3)
https://tiptap.dev/docs/editor/extensions/nodes/paragraph

https://github.com/ueberdosis/tiptap/blob/main/packages/extension-paragraph/src/paragraph.ts

少し複雑になってきました。Paragraphは馴染みある段落です。p要素に位置してます。

基本

まず基本的なところから、group: blockcontent: inline*です。つまり、Documentの直下に入り、Textノードなどのインライン要素を0個以上入れることができます。

次にparseとrenderを確認します

  parseHTML() {
    return [{ tag: 'p' }]
  },

  renderHTML({ HTMLAttributes }) {
    return ['p', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
  },

parseDOM

parseHTMLはHTMLからノードに変換するメソッドです。

type ParseRule = TagParseRule | StyleParseRuleを戻り値に指定します。
TagParseRule{tag: 'p[data-color]'}のようなCSS Selectorをtagプロパティで指定することで変換します。
ここで使えるgetAttrsはDOMからアトリビュートを抜き出すためのものです。

今回はpタグをParagraphノードに変換しています。

renderHTML

renderHTMLは逆で、ノードからHTMLに変換します。
DOMOutputSpecを戻り値に指定します。

公式の説明では以下のように述べられてます。

DOM構造の説明。文字列(テキストノードとして解釈される)、DOMノード(自身として解釈される)、{dom, contentDOM} オブジェクト、または配列のいずれかです。
配列はDOM要素を記述します。配列の最初の値は文字列で、DOM要素の名前を表します。必要に応じて名前空間URLとスペースでプレフィックスが付く場合があります。2番目の要素が単純なオブジェクトの場合、その要素の属性セットとして解釈されます。それ以降の要素(2番目の要素が属性オブジェクトでない場合を含む)は、DOM要素の子要素として解釈され、有効なDOMOutputSpec値または数値0でなければなりません。
数値0(発音は「ホール」)は、ノードの子ノードを挿入する位置を示すために使用されます。出力仕様で出現する場合、親ノード内の唯一の子要素でなければなりません。

ぱっと見よくわからんのですが、配列を受け取る・第2要素がオブジェクトなら属性として認識・それ以外は固定の子ノードとして認識・0は特殊で他の子ノードを挿入する位置です。

今回の例だと以下になります。
第1要素:"p" => pタグ
第2要素:HTMLAttributes
第3要素:0 => inline要素のテキストなどが子ノードとして挿入

renderHTMLの引数にHTMLAttributesを受け取れるのですが、これはノード内attrsのrenderHTMLを呼ばれたものがここに入ります。
これとoptionsで受け取ったものをマージしていますね。

options

Tiptapでは挙動をNodeを使用する側からoptionsで調整できます。
今回はHTMLAttributesを受け取れるようになっていて、classなどを当てれます。

Paragraph.configure({
  HTMLAttributes: {
    class: 'my-custom-class',
  },
})

commands

  addCommands() {
    return {
      setParagraph:
        () =>
        ({ commands }) => {
          return commands.setNode(this.name)
        },
    }
  },

Tiptapではエディターへの操作をcommandsという一連の流れを規定したものを通じてします。
拡張機能ごとにcommandsを設定するので、ここではsetParagraphという現在選択中の要素をParagraphノードに変換するコマンドを用意していますね。

commands.setNode(this.name)について少し深掘りしますか。

https://tiptap.dev/docs/editor/api/commands/nodes-and-marks/set-node

https://github.com/ueberdosis/tiptap/blob/224cb2c77a7921e62b18f4b510bb314b582926df/packages/core/src/commands/setNode.ts

Nodeの名前と属性を引数に受け取り、変換するみたいです。
この処理のコア部分はここでしょう。

return (
      chain()
        // try to convert node to default node if needed
        .command(({ commands }) => {
          const canSetBlock = setBlockType(type, { ...attributesToCopy, ...attributes })(state)

          if (canSetBlock) {
            return true
          }

          return commands.clearNodes()
        })
        .command(({ state: updatedState }) => {
          return setBlockType(type, { ...attributesToCopy, ...attributes })(updatedState, dispatch)
        })
        .run()
    )

まず1つ目のcommandの中では、setBlockTypeで変換可能か試みています。(dispatchがないため)
ここら辺はProseMirrorの知識ですが、commandsはdispatchを引数に渡すとステートが更新され、無しだと可能かどうかの判定ができます。
また、setBlockTypeはProseMirrorが用意してるコマンドです。
こちらも別記事で読んでみます。

さて、そこで変換可能なら2つ目のコマンドにそのままチェーン、出来ないならclearNodeでデフォルトノード(大体の場合でParahraph)に変換してから、指定のノードにしています。

その他

addKeyboardShortcutsでコマンドの追加、priorityでプラグインの登録順番・スキーマの順番を制御しています。
priorityが高いほど優先的に読み込まれます。例えば、リンク:priority-1000, 太字:priority-10だと、<a><b>link</b></a>のように、aタグをより外側に配置できたりします。

さいごに

Paragraphだけでも結構機能が豊富でしたね。
次はText行きましょう。

Discussion