独自の「ルビ」修飾を追加する方法

Cocoonカスタマイズ集
この記事は約9分で読めます。
記事内に広告が含まれています。

Cocoonには、標準機能として備わっています。

はじめに

Gutenbergエディターで文章にルビ(ふりがな)を付けることは、特に日本語コンテンツで読者の理解を助ける重要な機能です。
しかし、Gutenbergエディターには標準でルビ修飾の機能はありません。

標準では <ruby>や <rt>タグを直接入力する必要があり、手作業での編集が求められます。

そこで、投稿編集画面にルビ用のボタンを追加し、選択した文字に簡単にふりがなを付与できる仕組みを作ることができます。

この記事では、この方法について説明します。

完成イメージ

実装手順

以下のコードをfunctions.phpに追加します。

add_action('enqueue_block_editor_assets', function () {
  ob_start();
  ?>
  (function (wp) {
    const { Fragment } = wp.element;
    const { __ } = wp.i18n;
    const { toggleFormat, registerFormatType, insert, applyFormat, removeFormat } = wp.richText;
    const { RichTextToolbarButton, RichTextShortcut } = wp.blockEditor || wp.editor;

    const isRubyVisible = 1;

    // ルビ機能が無効な場合は処理を中断
    if (!isRubyVisible) {
      return;
    }

    // ルビ内のキャラクター(rtタグ)用のフォーマットを登録
    registerFormatType('custom/rt', {
      title: __('ふりがな(ルビ)キャラクター', 'text-domain'),
      tagName: 'rt',
      className: null,
      edit() {
        return wp.element.createElement(Fragment, {});
      }
    });

    // ルビ全体(rubyタグ)用のフォーマットを登録
    registerFormatType('custom/ruby', {
      title: __('ふりがな(ルビ)', 'text-domain'),
      tagName: 'ruby',
      className: null,
      edit({ isActive, value, onChange }) {
        const hasTypeAt = (formatsArr, idx, type) => {
          const f = formatsArr?.[idx];
          return Array.isArray(f) && f.some(fmt => fmt && fmt.type === type);
        };

        const findContiguousSpanForTypeAround = (formatsArr, idx, type) => {
          let start = idx;
          let end = idx;
          while (start > 0 && hasTypeAt(formatsArr, start - 1, type)) {
            start--;
          }
          while (end < (formatsArr?.length ?? 0) && hasTypeAt(formatsArr, end, type)) {
            end++;
          }
          return [start, end];
        };

        const deleteRangeFromValue = (val, start, end) => {
          if (end <= start) {
            return val;
          }
          val.text = val.text.slice(0, start) + val.text.slice(end);
          if (Array.isArray(val.formats)) {
            val.formats.splice(start, end - start);
          }
          val.start = val.end = start;
          return val;
        };

        const removeAllRtInsideRubySpan = (val, rubyStart, rubyEnd) => {
          const fmts = val.formats || [];
          let i = rubyStart;
          const ranges = [];
          while (i < rubyEnd) {
            if (hasTypeAt(fmts, i, 'custom/rt')) {
              let s = i;
              let e = i + 1;
              while (e < rubyEnd && hasTypeAt(fmts, e, 'custom/rt')) {
                e++;
              }
              ranges.push([s, e]);
              i = e;
            } else {
              i++;
            }
          }
          for (let r = ranges.length - 1; r >= 0; r--) {
            const [s, e] = ranges[r];
            val = deleteRangeFromValue(val, s, e);
            rubyEnd -= (e - s);
          }
          return { val, rubyStart, rubyEnd };
        };

        const onToggle = () => {
          let v = value;

          // ルビがアクティブでない場合は新しく追加する
          if (!isActive) {
            const ruby = window.prompt(__('ふりがな(ルビ)を入力してください', 'text-domain')) || v.text.substr(v.start, v.end - v.start);
            const rubyEnd = v.end;
            const rubyStart = v.start;
            v = insert(v, ruby, rubyEnd);
            v.start = rubyStart;
            v.end = rubyEnd + ruby.length;
            v = applyFormat(v, { type: 'custom/ruby' }, rubyStart, v.end);
            v = applyFormat(v, { type: 'custom/rt' }, rubyEnd, rubyEnd + ruby.length);
            return onChange(v);
          }

          const fmts = v.formats || [];
          let anchor = v.start;
          if (!hasTypeAt(fmts, anchor, 'custom/ruby') && anchor > 0 && hasTypeAt(fmts, anchor - 1, 'custom/ruby')) {
            v.start = v.end = anchor - 1;
          }

          // すでにルビが適用されている場合は解除する
          if (hasTypeAt(v.formats || [], v.start, 'custom/ruby')) {
            let [rubyStart, rubyEnd] = findContiguousSpanForTypeAround(v.formats || [], v.start, 'custom/ruby');
            const removed = removeAllRtInsideRubySpan(v, rubyStart, rubyEnd);
            v = removed.val;
            rubyStart = removed.rubyStart;
            rubyEnd = removed.rubyEnd;
            if (rubyEnd > rubyStart) {
              v = removeFormat(v, 'custom/ruby', rubyStart, rubyEnd);
              v.start = v.end = rubyStart;
            }
            return onChange(v);
          }

          if (v.start !== v.end) {
            v = removeFormat(v, 'custom/rt', v.start, v.end);
          }
          v = toggleFormat(v, { type: 'custom/ruby' });
          return onChange(v);
        };

        return wp.element.createElement(Fragment, {},
          wp.element.createElement(RichTextShortcut, { type: 'primaryAlt', character: 'r', onUse: onToggle }),
          wp.element.createElement(RichTextToolbarButton, { icon: 'ellipsis', title: __('ふりがな(ルビ)', 'text-domain'), onClick: onToggle, isActive: isActive, shortcutType: 'primaryAlt', shortcutCharacter: 'r' })
        );
      }
    });
  })(window.wp);
  <?php

  $handle = 'block-ruby';

  wp_register_script($handle, '', ['wp-rich-text', 'wp-block-editor', 'wp-element', 'wp-i18n'], false, true);
  wp_enqueue_script($handle);
  wp_add_inline_script($handle, ob_get_clean());
});

さいごに

今回紹介した方法を使えば、Gutenbergエディター標準では対応していないルビ修飾機能を投稿編集画面に追加でき、読みやすい日本語コンテンツを効率的に作成できます。
投稿者が手作業で<ruby>タグや<rt>タグを挿入する必要がなくなり、作業時間の短縮にもつながります。

タイトルとURLをコピーしました