# [] WordPressでMarkdownとショー
トコードの自動変換が競合しないよ
うにするプラグインを作った

Markdown形式のテキストをGutenbergエディタに貼り付けると、エディタが停止する問題に遭遇しました。原因はefn_noteという脚注ショートコードです。
調査したところ、貼り付け時にショートコードがcore/shortcodeブロックへ自動分離される処理が、特定のショートコードと組み合わさったときに処理を停止させていました。

WordPressの内部処理を理解した上で、最小限の介入でこの問題を解決したプラグイン開発の過程を共有します。

関連記事

1. ブロックエディタとMarkdownとショートコード

Markdownテキストに、脚注プラグインEasy Footnotesのショートコードが含まれていると、Gutenbergに貼り付けたときに、エディタが反応しないことがあります1

通常のMarkdown記法だけなら、見出しやリストなどが正しくブロックに変換されるのに、ショートコードが入った瞬間に処理が止まることがあります(ショートコードによってはできるものもあります)。

そこで、あとから手動でショートコードブロックを追加してefn_noteを入力しています。
貼り付け時の自動変換処理に問題があるようです。

1.1. 原因の特定

Gutenbergは貼り付け時に、登録済みブロックのtransform定義を使ってテキストをブロック化します。
core/shortcodeブロックにはtype: "shortcode"の変換定義があり、これがショートコードを自動検出してブロック化する入口です。

実際の定義は@wordpress/block-libraryパッケージのshortcode/transforms.mjsで確認できます2

// @wordpress/block-library/build-module/shortcode/transforms.mjs
from: [
  {
    type: 'shortcode',
    isMatch({ shortcode }) {
      return !!shortcode?.tag;
    },
    transform(attributes, { shortcode }) {
      return createBlock('core/shortcode', {
        text: shortcode.content,
      });
    },
  },
]
Code language: JavaScript (javascript)

この変換がefn_noteのような特定ショートコードと組み合わさったとき、Markdown変換フローの途中で何らかの競合や無限ループを引き起こしていたようです。

efn_note以外のショートコードでも同様の問題が起きるか、すべてのケースで検証できているわけではありません。
特定の組み合わせでのみ発生する問題かもしれません。

2. ショートコードの自動ブロック化を無効化する

問題を解決する方法はいくつか考えられます。

  • カスタムペーストハンドラを実装してショートコードを検出・除外する方法は確実です。
    ただしMarkdown変換の全体に手を入れる必要があり、他の変換に影響が出る可能性があります。
  • efn_note専用のブロックを作成して、そちらへの変換を優先させる方法もあります。
    しかしショートコードは既存コンテンツで広く使われており、プラグインとしての汎用性が低くなります。

最終的に選んだのは、core/shortcodetype: "shortcode"変換だけを無効化する方法でした。
ショートコード由来の自動ブロック化を抑制しつつ、見出しやリストなど他のMarkdown変換には触れません。

2.1. 実装の詳細

WordPressにはblocks.registerBlockTypeというフィルターがあります。
ブロック登録時にクライアント側でブロック設定を変更できる仕組みです3

wp.hooks.addFilter(
  'blocks.registerBlockType',
  'chiilabo/markdown-paste-without-shortcode-block',
  function(settings, name) {
    // ブロック設定を変更して返す
    return settings;
  }
);
Code language: JavaScript (javascript)

このフィルターを使って、core/shortcodeだけを対象にtransforms.fromからtype: 'shortcode'を除外しました。

function disableShortcodeTransform(settings, name) {
  var isTargetBlock = name === 'core/shortcode';
  var hasTransforms = settings && settings.transforms;
  var fromTransforms = hasTransforms && settings.transforms.from;

  if (!isTargetBlock || !Array.isArray(fromTransforms)) {
    return settings;
  }

  var filteredFromTransforms = fromTransforms.filter(function(transform) {
    return transform && transform.type !== 'shortcode';
  });

  if (filteredFromTransforms.length === fromTransforms.length) {
    return settings;
  }

  return Object.assign({}, settings, {
    transforms: Object.assign({}, settings.transforms, {
      from: filteredFromTransforms,
    }),
  });
}
Code language: JavaScript (javascript)

対象がcore/shortcodeでない場合は何もしません。
transforms.fromが存在しない場合も同様です。
配列をフィルタリングしてtype: 'shortcode'以外の変換だけを残します。
変更がない場合は元の設定をそのまま返すことで、不要な処理を避けています。

(function(root, factory) {
  if (typeof module === 'object' && module.exports) {
    module.exports = factory();
    return;
  }
  root.ChiilaboMarkdownPasteWithoutShortcodeBlock = factory();
})(typeof globalThis !== 'undefined' ? globalThis : this, function() {
  'use strict';
  // 実装
});
Code language: JavaScript (javascript)

ビルドツールを使わず、ブラウザとNode.js両方で動作します。

2.2. WordPress側の統合

プラグインエントリポイントでは、ブロックエディタ読み込み時にJavaScriptを登録します。

add_action(
  'enqueue_block_editor_assets',
  static function() {
    $script_path = plugin_dir_path(__FILE__) . 'assets/editor.js';
    $script_url = plugins_url('assets/editor.js', __FILE__);

    wp_enqueue_script(
      'chiilabo-markdown-paste-without-shortcode-block-editor',
      $script_url,
      array('wp-hooks'),
      file_exists($script_path) ? filemtime($script_path) : '0.1.0',
      true
    );
  }
);
Code language: PHP (php)

依存関係はwp-hooksのみです。
wp.hooks.addFilterにアクセスできれば動作します。
バージョン管理にはファイルの更新時刻を使い、開発中のキャッシュ問題を回避しました。

2.3. なぜこの方法が有効か(貼り付け変換とコード解釈)

Gutenbergの貼り付け変換は、登録済みブロックのtransform定義を順番に試します。
core/shortcodetype: 'shortcode'の変換があると、ショートコード形式を検出した時点でその変換を実行しようとします。

この変換を登録時に外すことで、ショートコード由来のcore/shortcode自動生成が起きなくなります。
Markdown変換フローは他の変換定義を使って進むため、見出しやリストなどは正常に変換されます。

一方、ショートコード自体の解釈は後段のレンダリング時に行われるため、最終的な表示には影響しません。
貼り付け時の自動分離だけを抑制する、最小限の介入です。

つまり、ショートコードは、段落ブロックでも評価されます。
ショートコードブロックをあえて使うのは、意図の明確化や段落の変換処理の影響を受けないため。
通常は、段落ブロックのままでも大丈夫だと思います。

2.4. テスト設計(ユニットテストとE2Eテスト)

実装の正しさを保証するため、二段階のテストを用意しました。

ユニットテストでは変換除外ロジックが正しく動作するかを検証します。

test('core/shortcode の shortcode 変換を除外する', () => {
  const settings = {
    transforms: {
      from: [
        { type: 'shortcode', tag: 'test' },
        { type: 'block', blocks: ['core/paragraph'] },
      ],
    },
  };

  const result = disableShortcodeTransform(settings, 'core/shortcode');

  expect(result.transforms.from).toHaveLength(1);
  expect(result.transforms.from[0].type).toBe('block');
});
Code language: PHP (php)

E2Eテストでは実際のエディタ環境で貼り付け結果を確認します4

test('shortcode を含む Markdown 貼り付け時に core/shortcode が生成されない', async () => {
  const markdown = '# 見出し\n\n本文 5 続き';
  
  const blocks = await page.evaluate((md) => {
    return wp.blocks.pasteHandler({ plainText: md });
  }, markdown);

  const hasShortcodeBlock = blocks.some(
    block => block.name === 'core/shortcode'
  );

  expect(hasShortcodeBlock).toBe(false);
});
Code language: JavaScript (javascript)

@wordpress/env@playwright/testを組み合わせ、実際のWordPress環境で検証します。

3. 注意点(WordPressのバージョン依存)

Gutenbergのブロック変換は、複数の変換定義が協調して動作する仕組みです。
一つの変換を無効化しても、他の変換が正しく機能すれば全体は動作します。

ただし、blocks.registerBlockTypeフィルターは強力ですが、使い方を誤ると他のプラグインや将来のWordPressアップデートと衝突する可能性があるので注意が必要です。

  • type: 'shortcode'変換を完全に無効化するため、ショートコードから専用ブロックへの自動変換も動作しなくなります。
    もし、プラグイン開発者がカスタムショートコード用の変換を定義していた場合、その変換も実行されません。
  • あと、WordPress本体やプラグインがショートコード変換の仕組みを変更した場合、この実装も影響を受けます。
    @wordpress/block-libraryのアップデートで変換定義の構造が変わる可能性に注意が必要です。

将来的に、WordPress本体の変換処理が改善され、ショートコードとMarkdown変換が競合しなくなる可能性もあります。
その場合、このプラグインは不要になります。

  1. Easy FootnotesはWordPress公式プラグインディレクトリで公開されている、ホバー表示機能を持つ脚注プラグイン。
  2. @wordpress/block-libraryはWordPress公式のブロック実装を含むnpmパッケージ。unpkg.comで最新版のソースコードを直接参照できる。
  3. Block Filtersの公式ドキュメントはdeveloper.wordpress.org/block-editor/reference-guides/filters/block-filters/で参照可能。
  4. E2EはEnd-to-Endの略。実際のブラウザ環境でユーザー操作を再現してテストする手法。
  5. 脚注