WordPress Logo Highlight.js カスタムブロック サンプル

WordPress で Highlight.js を使って入力したコードをシンタックスハイライトするカスタムブロックのサンプルです。インスペクターのオプションで表示をカスタマイズできるようにしています。

@wordpress/create-block を使ってブロックのプラグインを作成するので、node や npm、WordPress のローカル環境が必要です。

カスタムブロックを作成せず、WordPress で簡単に Highlight.js を使ってハイライト表示する方法は以下のページを御覧ください。

create-block を使用してブロックのプラグインを作成する詳細については以下のページを御覧ください。

また Highlight.js の使い方やカスタマイズ方法については以下のページを御覧ください。

以下ではコードのサンプルがメインで、詳しい説明はほとんどありません。また、以下のコードには改善すべきところが多々あると思いますので予めご了承ください。

以下で使用している環境

更新日:2024年02月28日

作成日:2024年02月11日

[更新]

このサンプルのプラグインを作成すると、投稿の編集画面で以下のようなブロックを挿入することができ、コードをエスケープせずに直接入力することができます。

必要に応じて右側のインスペクターで言語名や行番号の表示の有無、行のハイライトなどを指定することができます(指定できるオプション)。

また、以下のようなプレビュー表示の機能を追加することもできます。

フロントエンド側では以下のような表示(このページのシンタックスハイライトとほぼ同じ)になります。

事前準備

Highlight.js から JavaScript とテーマの CSS をダウンロードしておきます。

以下の例では Highlight.js のテーマの CSS は atom-one-dark.min.css を使用しています。

ブロックのひな形を作成

@wordpress/create-block を使用してブロックの初期構成のひな形を作成します。

ターミナルでプラグインディレクトリに移動します。

% cd wp-content/plugins

以下を実行してブロックのひな形を作成します。以下の場合、custom-highlight-block というディレクトリが plugins ディレクトリの中に作成され、その中にひな形のファイルが生成されます。

また、以下の例では --namespace オプションで名前空間に wdl を指定していますが、任意の文字列を指定できます(--namespace を省略した場合の名前空間の値は create-block になります)。

% npx @wordpress/create-block@latest custom-highlight-block --namespace wdl

上記コマンドを実行して、インストールが完了すると以下のように表示されます。

インストールには数分かかります。

プラグインを有効化

プラグインページで、作成したひな形のブロックのプラグインを有効化します。

投稿にブロックを挿入します。ブロックが表示されない場合は、ページを再読込します。

ひな形のブロックが問題なく挿入でき、表示されることを確認して保存します。

[注意] WordPress Highlight.js カスタムブロックの作成に掲載されいるブロック(my-highlight-block)を作成して有効化している場合は、無効化(または削除)する必要があります。

これから作成するブロックも、同じ JavaScript ファイルを読み込んでいるため、Highlight.js が重複して適用されてされしまうため正しく表示されません。

※ 但し、削除するとプラグインフォルダ内のすべてのファイルが失われるので注意が必要です。

また、別途 Highlight.js を読み込んで初期化している場合も正しく表示されません。

プラグインファイルの編集

作成されたプラグインフォルダ(custom-highlight-block)のプラグインファイル(custom-highlight-block.php)を編集します。

Description や Author を適宜変更し、Highlight.js 関連ファイルの読み込みの記述を追加します。

<?php
/**
* Plugin Name:       Custom Highlight Block
* Description:       Custom Syntax Highlight Block using Hightlight.js.
* Requires at least: 6.1
* Requires PHP:      7.0
* Version:           0.1.0
* Author:            WebDesignLeaves
* License:           GPL-2.0-or-later
* License URI:       https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain:       custom-highlight-block
*
* @package           wdl
*/

if ( ! defined( 'ABSPATH' ) ) {
  exit;
}

// 以下を追加
function add_wdl_custom_code_block_scripts_and_styles() {
  $dir = dirname(__FILE__);

  //管理画面以外(フロントエンド側でのみ読み込む)
  if (!is_admin()) {
    // highlight.js の JavaScript ファイルのエンキュー
    wp_enqueue_script(
      'highlight-js',
      plugins_url('/highlight-js/highlight.min.js', __FILE__),
      array(),
      filemtime("$dir/highlight-js/highlight.min.js"),
      true
    );
    // カスタマイズ用 JavaScript ファイルのエンキュー
    wp_enqueue_script(
      'custom-js',
      plugins_url('/highlight-js/custom.js', __FILE__),
      array('highlight-js'),
      filemtime("$dir/highlight-js/custom.js"),
      true
    );
    // highlight.js の基本スタイルのエンキュー
    wp_enqueue_style(
      'highlight-js-style',
      plugins_url('/highlight-js/highlight.min.css', __FILE__),
      array(),
      filemtime("$dir/highlight-js/highlight.min.css")
    );
    // カスタマイズ用スタイルのエンキュー
    wp_enqueue_style(
      'custom-style',
      plugins_url('/highlight-js/custom.css', __FILE__),
      array(),
      filemtime("$dir/highlight-js/custom.css")
    );
  }
}
add_action('enqueue_block_assets', 'add_wdl_custom_code_block_scripts_and_styles');

// 以下はひな形のコードから変更なし
function custom_highlight_block_custom_highlight_block_block_init() {
  register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'custom_highlight_block_custom_highlight_block_block_init' );
 

block.json の編集

src/block.json を編集します。

description を変更し、attributes(属性)を追加します。

attributes の codeText は、編集画面(エディタ)で入力するコードの文字列を保持するための属性で、type に string、default(初期値)に ""(空文字列)を指定し、ブロックの code 要素にテキストとして保存するように、source に text を、selector に code(要素)を指定します。

codeText 以外は、インスペクターに表示するオプションの値を保持するための属性です。

この時点では style.scss と view.js は使用しないので、"style": "file:./style-index.css""viewScript": "file:./view.js" の行を削除します。

また、アイコンは独自のアイコンを設定するので、"icon": "smiley" の行も削除します。

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "wdl/custom-highlight-block",
  "version": "0.1.0",
  "title": "Custom Highlight Block",
  "category": "widgets",
  "description": "Custom Syntax Highlight Block using Hightlight.js.",
  "example": {},
  "attributes": {
    "codeText": {
      "type": "string",
      "default": "",
      "source": "text",
      "selector": "code"
    },
    "language": {
      "type": "string"
    },
    "wrap": {
      "type": "boolean"
    },
    "noLineNum": {
      "type": "boolean"
    },
    "showNoLang": {
      "type": "boolean"
    },
    "noCopyBtn": {
      "type": "boolean"
    },
    "noToolbar": {
      "type": "boolean"
    },
    "label": {
      "type": "string"
    },
    "labelUrl": {
      "type": "string"
    },
    "targetBlank": {
      "type": "boolean"
    },
    "lineHighlight": {
      "type": "string"
    },
    "lineNumStart": {
      "type": "string"
    },
    "setLang": {
      "type": "string"
    },
    "maxLines": {
      "type": "string"
    },
    "maxLinesOffset": {
      "type": "string"
    },
    "maxLinesScrollTo": {
      "type": "string"
    },
    "copyNoPrompt": {
      "type": "boolean"
    },
    "copyNoSlComments": {
      "type": "boolean"
    },
    "copyNoMlComments": {
      "type": "boolean"
    },
    "copyNoHTMLComments": {
      "type": "boolean"
    },
    "toggleCode": {
      "type": "boolean"
    },
    "accodionOpenBtnDefaultLabel": {
      "type": "string"
    },
    "accodionCloseBtnDefaultLabel": {
      "type": "string"
    }
  },
  "supports": {
    "html": false
  },
  "textdomain": "custom-highlight-block",
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css"
}

edit.js の編集

src/edit.js ではブロックがエディターでどのように機能し、どのように表示されるかを定義します。

エディターでは、textarea にコードを入力し、サイドバーのインスペクターで言語名を入力したり、折り返しや行番号表示などのオプションを設定できるようにします。そのための必要なコンポーネントをインポートします。

Edit() では属性(attributes)と属性を更新する関数(setAttributes)、及び ブロックが現在選択されているかどうかを表す isSelected を変数に受け取ります。

codeTextRows は TextareaControl コンポーネントの行数を指定するための変数で、入力されたコードの行数から算出しています。

インスペクターで表示するオプションの数が多いので、インスペクター部分は別途 getInspectorControls という関数で定義しています。項目はグループに分け、PanelBody の initialOpen で初期状態で表示するかどうかを関連項目の attributes の値を使って指定しています。

Edit() の return は配列で指定してインスペクター部分の関数 getInspectorControls() を呼び出し、エディターにレンダリングする入力エリアのブロックのラッパー要素に {...useBlockProps()} を指定して必要な属性を展開するようにします。

import { __ } from "@wordpress/i18n";
import { useBlockProps, InspectorControls } from "@wordpress/block-editor";
import { TextareaControl, PanelBody, TextControl, PanelRow, CheckboxControl } from "@wordpress/components";
import "./editor.scss";

export default function Edit({ attributes, setAttributes, isSelected }) {
  const codeTextRowCount = attributes.codeText.split(/\r|\r\n|\n/).length;
  const codeTextRows = codeTextRowCount > 3 ? codeTextRowCount : 3;

  // インスペクターを出力する関数
  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody title={__("Highlight Basic Settings", "custom-highlight-block")}>
          <PanelRow>
            <TextControl
              label={__("Language", "custom-highlight-block")}
              value={attributes.language}
              onChange={(value) => setAttributes({ language: value })}
              className="lang-name"
            />
          </PanelRow>
          <PanelRow>
            <TextControl
              label={__("Label", "custom-highlight-block")}
              value={attributes.label}
              onChange={(value) => setAttributes({ label: value })}
              className="label-name"
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label={__("white-space: pre-wrap", "custom-highlight-block")}
              checked={attributes.wrap}
              onChange={(val) => setAttributes({ wrap: val })}
              help={__("Enable Text Auto Wrap", "custom-highlight-block")}
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label={__("No Line Number", "custom-highlight-block")}
              checked={attributes.noLineNum}
              onChange={(val) => setAttributes({ noLineNum: val })}
              help={__("Hide Line Number", "custom-highlight-block")}
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label={__("No Language Name", "custom-highlight-block")}
              checked={attributes.showNoLang}
              onChange={(val) => setAttributes({ showNoLang: val })}
              help={__("Hide Language Name", "custom-highlight-block")}
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label={__("No Copy Button", "custom-highlight-block")}
              checked={attributes.noCopyBtn}
              onChange={(val) => setAttributes({ noCopyBtn: val })}
              help={__("Do Not Show Copy Button", "custom-highlight-block")}
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label={__("No Toolbar", "custom-highlight-block")}
              checked={attributes.noToolbar}
              onChange={(val) => setAttributes({ noToolbar: val })}
              help={__("Hide Toolbar", "custom-highlight-block")}
            />
          </PanelRow>
          <PanelRow>
            <TextControl
              label={__("Line Highlight", "custom-highlight-block")}
              value={attributes.lineHighlight}
              onChange={(value) => setAttributes({ lineHighlight: value })}
              className="line-highlight"
              placeholder={__("e.g. 1, 3-5", "custom-highlight-block")}
            />
          </PanelRow>
          <PanelRow>
            <TextControl
              label={__("Start Line Number", "custom-highlight-block")}
              value={attributes.lineNumStart}
              onChange={(value) => setAttributes({ lineNumStart: value })}
              className="line-num-start"
            />
          </PanelRow>
          <PanelRow>
            <TextControl
              label={__("Override Language Text", "custom-highlight-block")}
              value={attributes.setLang}
              onChange={(value) => setAttributes({ setLang: value })}
              className="set-lang"
              placeholder={__("Language Text", "custom-highlight-block")}
            />
          </PanelRow>
          <PanelRow>
            <PanelBody
              title={__("LABEL Option", "custom-highlight-block")}
              initialOpen={attributes.labelUrl ? true : false}
            >
              <PanelRow>
                <TextControl
                  label={__("Label URL", "custom-highlight-block")}
                  value={attributes.labelUrl}
                  onChange={(value) => setAttributes({ labelUrl: value })}
                  className="label-url"
                />
              </PanelRow>
              <PanelRow>
                <CheckboxControl
                  label={__("target=_blank", "custom-highlight-block")}
                  checked={attributes.targetBlank}
                  onChange={(val) => setAttributes({ targetBlank: val })}
                  help={__("target attribute", "custom-highlight-block")}
                />
              </PanelRow>
            </PanelBody>
          </PanelRow>
          <PanelRow>
            <PanelBody
              title={__("Max Lines", "custom-highlight-block")}
              initialOpen={attributes.maxLines ? true : false}
            >
              <PanelRow>
                <TextControl
                  label={__("Set Max Lines", "custom-highlight-block")}
                  value={attributes.maxLines}
                  onChange={(value) => setAttributes({ maxLines: value })}
                  className="max-lines"
                  placeholder={__("specify max number of lines", "custom-highlight-block")}
									help={attributes.maxLines && attributes.toggleCode ? "Can not work properly with toggleCode on Firefox and iOS!" : "" }
                />
              </PanelRow>
              <PanelRow>
                <TextControl
                  label={__("Max Lines Offset (px)", "custom-highlight-block")}
                  value={attributes.maxLinesOffset}
                  onChange={(value) => setAttributes({ maxLinesOffset: value })}
                  className="max-lines-offset"
                  placeholder={__("offset height (pixel)", "custom-highlight-block")}
                />
              </PanelRow>
              <PanelRow>
                <TextControl
                  label={__("Scroll To Line Number", "custom-highlight-block")}
                  value={attributes.maxLinesScrollTo}
                  onChange={(value) => setAttributes({ maxLinesScrollTo: value })}
                  className="max-lines-scroll-to"
                />
              </PanelRow>
            </PanelBody>
          </PanelRow>
          <PanelRow>
            <PanelBody
              title={__("Copy Option", "custom-highlight-block")}
              initialOpen={
                attributes.copyNoPrompt ||
                attributes.copyNoSlComments ||
                attributes.copyNoMlComments ||
                attributes.copyNoHTMLComments
                  ? true
                  : false
              }
            >
              <PanelRow>
                <CheckboxControl
                  label={__("No Prompt", "custom-highlight-block")}
                  checked={attributes.copyNoPrompt}
                  onChange={(val) => setAttributes({ copyNoPrompt: val })}
                  help={__("Exclude prompt ($, %)", "custom-highlight-block")}
                />
              </PanelRow>
              <PanelRow>
                <CheckboxControl
                  label={__("No Single-Line Comments", "custom-highlight-block")}
                  checked={attributes.copyNoSlComments}
                  onChange={(val) => setAttributes({ copyNoSlComments: val })}
                  help={__("Exclude Single Line Comments", "custom-highlight-block")}
                />
              </PanelRow>
              <PanelRow>
                <CheckboxControl
                  label={__("No Multi-Line Comments", "custom-highlight-block")}
                  checked={attributes.copyNoMlComments}
                  onChange={(val) => setAttributes({ copyNoMlComments: val })}
                  help={__("Exclude Single Line Comments", "custom-highlight-block")}
                />
              </PanelRow>
              <PanelRow>
                <CheckboxControl
                  label={__("No HTML Comments", "custom-highlight-block")}
                  checked={attributes.copyNoHTMLComments}
                  onChange={(val) => setAttributes({ copyNoHTMLComments: val })}
                  help={__("Exclude HTML Comments", "custom-highlight-block")}
                />
              </PanelRow>
            </PanelBody>
          </PanelRow>
          <PanelRow>
            <PanelBody
              title={__("Toggle Code Accordion", "custom-highlight-block")}
              initialOpen={attributes.toggleCode ? true : false}
            >
              <PanelRow>
                <CheckboxControl
                  label={__("Toggle Code", "custom-highlight-block")}
                  checked={attributes.toggleCode}
                  onChange={(val) => setAttributes({ toggleCode: val })}
                  help={__("Toggle (Show/Hide) Code ", "custom-highlight-block")}
                />
              </PanelRow>
              <PanelRow>
                <TextControl
                  label={__("Open Button Label", "custom-highlight-block")}
                  value={attributes.accodionOpenBtnDefaultLabel}
                  onChange={(value) => setAttributes({ accodionOpenBtnDefaultLabel: value })}
                  className="accordion-open-label"
                  placeholder={__("Open (Default)", "custom-highlight-block")}
                />
              </PanelRow>
              <PanelRow>
                <TextControl
                  label={__("Close Button Label", "custom-highlight-block")}
                  value={attributes.accodionCloseBtnDefaultLabel}
                  onChange={(value) => setAttributes({ accodionCloseBtnDefaultLabel: value })}
                  className="accordion-close-label"
                  placeholder={__("Close (Default)", "custom-highlight-block")}
                />
              </PanelRow>
            </PanelBody>
          </PanelRow>
        </PanelBody>
      </InspectorControls>
    );
  };

  //配列で指定
  return [
    getInspectorControls(),
    <div {...useBlockProps()}>
      <TextareaControl
        label={__("Highlight Code", "custom-highlight-block")}
        value={attributes.codeText}
        onChange={(value) => setAttributes({ codeText: value })}
        rows={codeTextRows}
        placeholder={__("Write your code...", "custom-highlight-block")}
        hideLabelFromVision={isSelected ? false : true}
      />
    </div>,
  ];
}

ファイルを保存して編集画面を再読込し、挿入したブロックの入力エリアを選択してフォーカスすると以下のように右側のインスペクターにオプションが表示されます。

save.js の編集

src/save.js ファイルは、ブロックが保存(出力)されたときに表示される HTML 構造を定義します。

save() では attributes を引数にとり、テキストエリアに入力されたコードのテキスト(attributes.codeText)をエスケープ処理して code 要素のコンテンツに指定し、属性(attributes)の値を使って code 要素と pre 要素のクラス属性や data-* 属性に指定します。

code 要素と pre 要素に指定する属性のオブジェクトを定義し、指定する属性を追加して、return ステートメントでそれぞれの要素に展開します。

どのようなクラスや data-* 属性をどの要素に指定するかは、Highlight.js のカスタマイズ用の JavaScript で定義されています。

return ステートメントでは、フラグメント (<>〜</>) で全体を囲み、attributes.toggleCode の値(アコーディオンで開閉表示するかどうか)が true の場合はアコーディオンのマークアップを追加します。

また、ブロックのラッパー要素に useBlockProps.save() から返されるブロック props を追加します。

import { useBlockProps } from "@wordpress/block-editor";

export default function save({ attributes }) {

  // pre 要素に設定する属性のオブジェクト
  const preAttributes = {};

  // attributes の値により、pre 要素に設定するクラス属性を作成
  let preClassList = attributes.wrap ? "pre-wrap" : "pre";
  preClassList += attributes.noLineNum ? " no-line-num" : "";
  preClassList += attributes.noCopyBtn ? " no-copy-btn" : "";
  preClassList += attributes.targetBlank ? " target-blank" : "";
  preClassList += attributes.scroll ? " scroll" : "";
  preClassList += attributes.smooth ? " smooth" : "";
  preClassList += attributes.showResetBtn ? " reset-btn" : "";
  preClassList += attributes.copyNoPrompt ? " copy-no-prompt" : "";
  preClassList += attributes.copyNoSlComments ? " copy-no-sl-comments" : "";
  preClassList += attributes.copyNoMlComments ? " copy-no-ml-comments" : "";
  preClassList += attributes.copyNoHTMLComments ? " copy-no-html-comments" : "";

  // pre 要素に クラスを追加
  preAttributes.className = preClassList;

  // attributes の値により、pre 要素に data-* 属性を追加
  if (attributes.label) preAttributes["data-label"] = attributes.label;
  if (attributes.labelUrl) preAttributes["data-label-url"] = attributes.labelUrl;
  if (attributes.lineHighlight) preAttributes["data-line-highlight"] = attributes.lineHighlight;
  if (attributes.lineNumStart) preAttributes["data-line-num-start"] = attributes.lineNumStart;
  if (attributes.showUntil) preAttributes["data-show-until"] = attributes.showUntil;
  if (attributes.showUntilOffset) preAttributes["data-show-until-offset"] = attributes.showUntilOffset;
  if (attributes.showUntilLabel) preAttributes["data-show-until-label"] = attributes.showUntilLabel;
  if (attributes.showUntilLabelReduce) preAttributes["data-show-until-label-reduce"] = attributes.showUntilLabelReduce;
  if (attributes.maxLines) preAttributes["data-max-lines"] = attributes.maxLines;
  if (attributes.maxLinesScrollTo) preAttributes["data-scroll-to"] = attributes.maxLinesScrollTo;

  // code 要素に設定する属性のオブジェクト
  const codeAttributes = {};

  // code 要素に設定するクラス属性
  let codeClassList;
  if(attributes.language) codeClassList = `language-${attributes.language}`;
  if (codeClassList) {
    codeClassList += attributes.showNoLang ? " show-no-lang" : "";
  } else {
    codeClassList = attributes.showNoLang ? "show-no-lang" : "";
  }
  // code 要素に クラスを追加
  if(codeClassList) codeAttributes.className = codeClassList;

  // code 要素に設定する data-* 属性
  if (attributes.setLang) codeAttributes["data-set-lang"] = attributes.setLang;

  // テキストエリアに入力されたコードのテキストをエスケープ
  const escapedCodeText = attributes.codeText.replace(/[<>&'"]/g, (match) => {
    const specialChars = {
      "<": "&lt;",
      ">": "&gt;",
      "&": "&amp;",
      "'": "&#39;",
      '"': "&quot;",
    };
    return specialChars[match];
  });

  return (
    <>
      {attributes.toggleCode && ( // アコーディオンパネルで表示
        <div {...useBlockProps.save()}>
          <details class="toggle-code-animation">
            <summary data-close-text={attributes.accodionCloseBtnDefaultLabel}>
              {attributes.accodionOpenBtnDefaultLabel}
            </summary>
            <div class="details-content-wrapper">
              <div class="details-content">
                <div className={ attributes.noToolbar ? "hljs-wrap no-toolbar" : "hljs-wrap" }>
                  <pre {...preAttributes}>
                    <code {...codeAttributes}>{escapedCodeText}</code>
                  </pre>
                </div>
              </div>
            </div>
          </details>
        </div>
      )}
      {!attributes.toggleCode && ( // アコーディオンパネルなし
        <div {...useBlockProps.save()}>
          <div className={ attributes.noToolbar ? "hljs-wrap no-toolbar" : "hljs-wrap" }>
            <pre {...preAttributes}>
              <code {...codeAttributes}>{escapedCodeText}</code>
            </pre>
          </div>
        </div>
      )}
    </>
  );
}

ファイルを保存してエディターを再読み込みすると、save() 関数の return ステートメントが変更されたため以下のように表示されるので、「ブロックのリカバリーを試行」をクリックします。

例えば、以下のようなコードを記述して、インスペクターで LANGUAGE に JavaScript と入力し、white-space: pre-wrap にチェックを入れて投稿を保存すると、

フロント側では以下のように表示されます。

editor.scss の編集

src/editor.scss を編集してエディターのスタイルを設定します。

以下では、フォーカス時のテキストエリアの背景色(薄緑色)とラベルの文字サイズと help メッセージの色を設定しています。

/* フォーカス時のテキストエリアの背景色 */
.wp-block-wdl-custom-highlight-block textarea:focus {
  background-color: #f6fdf6;
}

/* テキストエリアのラベルの文字サイズ */
.wp-block-wdl-custom-highlight-block label.components-base-control__label {
  font-size: 16px;
}

/* Toggle Code と Max Lines を指定した場合の help メッセージの色 */
.max-lines .components-base-control__help {
  color: red;
}

オプションの Toggle Code を有効にしてアコーディオンパネルで表示する場合、Max Lines を指定すると Google Chrome 以外(iOS や Firefox など)では正しく表示されないので、Toggle Code と Max Lines を指定した場合は、オプションの下に赤字でメッセージを表示するようにスタイルを設定しています。

index.js の編集

src/index.js を編集します。

この時点では style.scss は使わないので import './style.scss'; の行を削除します(後で style.scss を使う構成で戻すのでコメントアウトしておいてもOKです)。

また、エディターのブロックには カスタムアイコン を設定するので、その定義と registerBlockType() に icon プロパティを追加します。

import { registerBlockType } from "@wordpress/blocks";
// import './style.scss'; // 削除
import Edit from "./edit";
import save from "./save";
import metadata from "./block.json";

// SVG アイコンの定義
const highlightIcon = (
  <svg
    viewBox="0 0 16 16"
    xmlns="http://www.w3.org/2000/svg"
    aria-hidden="true"
    focusable="false"
    width="24"
    height="24"
  >
    <path d="M16 8A8 8 0 1 0 0 8a8 8 0 0 0 16 0m-8 5v1H4.5a.5.5 0 0 0-.093.009A7 7 0 0 1 3.1 13zm0-1H2.255a7 7 0 0 1-.581-1H8zm-6.71-2a7 7 0 0 1-.22-1H8v1zM1 8q0-.51.07-1H8v1zm.29-2q.155-.519.384-1H8v1zm.965-2q.377-.54.846-1H8v1zm2.137-2A6.97 6.97 0 0 1 8 1v1z"/>
  </svg>
);

registerBlockType(metadata.name, {
  icon: highlightIcon,  // icon プロパティを追加
  edit: Edit,
  save,
});

ファイルを保存してエディターを開くと設定したカスタムアイコンが表示されます。

プレビュー機能を追加

エディター画面で、入力したコードのハイライト表示をプレビューする機能を追加します。

block.json

block.json に現在編集中かプレビュー中かの真偽値を保持する属性 isEditMode を追加します。

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "wdl/custom-highlight-block",
  "version": "0.1.0",
  "title": "Custom Highlight Block",
  "category": "widgets",
  "description": "Custom Syntax Highlight Block using Hightlight.js.",
  "example": {},
  "attributes": {
    "codeText": {
      "type": "string",
      "default": "",
      "source": "text",
      "selector": "code"
    },
    "language": {
      "type": "string"
    },
    "wrap": {
      "type": "boolean"
    },
    "noLineNum": {
      "type": "boolean"
    },
    "showNoLang": {
      "type": "boolean"
    },
    "noCopyBtn": {
      "type": "boolean"
    },
    "noToolbar": {
      "type": "boolean"
    },
    "label": {
      "type": "string"
    },
    "labelUrl": {
      "type": "string"
    },
    "targetBlank": {
      "type": "boolean"
    },
    "lineHighlight": {
      "type": "string"
    },
    "lineNumStart": {
      "type": "string"
    },
    "setLang": {
      "type": "string"
    },
    "maxLines": {
      "type": "string"
    },
    "maxLinesOffset": {
      "type": "string"
    },
    "maxLinesScrollTo": {
      "type": "string"
    },
    "copyNoPrompt": {
      "type": "boolean"
    },
    "copyNoSlComments": {
      "type": "boolean"
    },
    "copyNoMlComments": {
      "type": "boolean"
    },
    "copyNoHTMLComments": {
      "type": "boolean"
    },
    "toggleCode": {
      "type": "boolean"
    },
    "accodionOpenBtnDefaultLabel": {
      "type": "string"
    },
    "accodionCloseBtnDefaultLabel": {
      "type": "string"
    },
    "isEditMode": {
      "type": "boolean",
      "default": true
    }
  },
  "supports": {
    "html": false
  },
  "textdomain": "custom-highlight-block",
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css"
}

プラグインファイル

プラグインファイル(custom-highlight-block.php)を以下のように、全てのファイルを管理画面(エディター側)とフロントエンド側の両方で読み込むように変更します。

<?php

/**
* Plugin Name:       Custom Highlight Block
* Description:       Custom Syntax Highlight Block using Hightlight.js.
* Requires at least: 6.1
* Requires PHP:      7.0
* Version:           0.1.0
* Author:            WebDesignLeaves
* License:           GPL-2.0-or-later
* License URI:       https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain:       custom-highlight-block
*
* @package           wdl
*/

if (!defined('ABSPATH')) {
  exit;
}

// 以下を変更(管理画面とフロントエンド側の両方で読み込む)
function add_wdl_custom_code_block_scripts_and_styles() {
  $dir = dirname(__FILE__);

  // highlight.js の JavaScript ファイルのエンキュー
  wp_enqueue_script(
    'highlight-js',
    plugins_url('/highlight-js/highlight.min.js', __FILE__),
    array(),
    filemtime("$dir/highlight-js/highlight.min.js"),
    true
  );
  // カスタマイズ用 JavaScript ファイルのエンキュー
  wp_enqueue_script(
    'custom-js',
    plugins_url('/highlight-js/custom.js', __FILE__),
    array('highlight-js'),
    filemtime("$dir/highlight-js/custom.js"),
    true
  );
  // highlight.js の基本スタイルのエンキュー
  wp_enqueue_style(
    'highlight-js-style',
    plugins_url('/highlight-js/highlight.min.css', __FILE__),
    array(),
    filemtime("$dir/highlight-js/highlight.min.css")
  );
  // カスタマイズ用スタイルのエンキュー
  wp_enqueue_style(
    'custom-style',
    plugins_url('/highlight-js/custom.css', __FILE__),
    array(),
    filemtime("$dir/highlight-js/custom.css")
  );

}
add_action('enqueue_block_assets', 'add_wdl_custom_code_block_scripts_and_styles');

function custom_highlight_block_custom_highlight_block_block_init() {
  register_block_type(__DIR__ . '/build');
}
add_action('init', 'custom_highlight_block_custom_highlight_block_block_init');
 

カスタマイズ用の JavaScript

このサンプルのカスタマイズ用の JavaScript に定義してある関数 mySetupHighlightJs(settings, targetWrapper = false) は Highlight.js の初期化とカスタマイズする関数です。

第1引数 settings はオプションの設定オブジェクトです。第2引数にシンタックスハイライト表示する要素を渡すと、その要素のみに Highlight.js の初期化とカスタマイズを適用するようになっています。

フロントエンド側では第2引数は指定せずに呼び出して、全ての div.hljs-wrap 要素を対象に初期化とカスタマイズを適用し、プレビュー時には編集中の div.hljs-wrap 要素のみを対象に呼び出します。

同様にアコーディオンアニメーションの関数 mySetupToggleDetailsAnimation(elem) も引数に要素を渡すと、その要素のみにアニメーションを適用するようになっているので、フロントエンド側では引数は指定せずに呼び出して、全ての details.toggle-code-animation 要素を対象にアニメーションを適用し、プレビュー時には編集中の details.toggle-code-animation 要素のみを対象に呼び出します。

edit.js

edit.js ではツールバーに必要なコンポーネント(BlockControls、ToolbarGroup、ToolbarButton)やプレビュー時に Highlight.js の初期化とカスタマイズを適用する際に使用するフック(useRef、useEffect)、プレビュー時にインスペクタを無効にする際に使用するフック(useDisabled)をインポートします。

そしてツールバーを出力する関数 getToolbarControls を定義して、ツールバーにプレビューと編集を切り替えるボタンを表示します(ボタンにはカスタムアイコンを表示します)。

プレビュー時のハイライト表示は、useRef で ref(codeWrapperRef)を宣言してcode 要素の祖先の div.hljs-wrap に指定し、isEditMode が切り替わった際に useEffect で div.hljs-wrap を参照してプレビュー用の JavaScript で定義した関数を適用します。

toggleCode 属性が true の場合はアコーディオンパネルでアニメーション表示するので、その場合は useRef で宣言した ref(codeDetailsRef)を details 要素に指定し、useEffect で details 要素を参照してアニメーションの関数を適用します。

また、プレビュー時の表示に必要な属性の設定やマークアップは、ほぼ save.js と同様ですが、ブロックのラッパー要素には {...useBlockProps()} を指定します。

import { __ } from "@wordpress/i18n";
// BlockControls を追加
import { useBlockProps, InspectorControls, BlockControls } from "@wordpress/block-editor";
// ToolbarGroup, ToolbarButton を追加
import { TextareaControl, PanelBody, TextControl, PanelRow, CheckboxControl, ToolbarGroup, ToolbarButton } from "@wordpress/components";
import "./editor.scss";
// useEffect, useRef を追加
import { useEffect, useRef } from "@wordpress/element";
// useDisabled を追加
import { useDisabled } from "@wordpress/compose";

export default function Edit({ attributes, setAttributes, isSelected }) {
  // useDisabled フック
  const disabledRef = useDisabled();
  // インスペクタの各要素のラッパーの div 要素へ設定する属性
  const inspectorDivAttributes = {};
  inspectorDivAttributes.className = 'inspectorDiv';
  if (!attributes.isEditMode) {
    // プレビューモードではインスペクタへの入力を disalbed に(ref 属性に disabledRef を指定)
    inspectorDivAttributes.ref = disabledRef;
  }

  const codeTextRowCount = attributes.codeText.split(/\r|\r\n|\n/).length;
  const codeTextRows = codeTextRowCount > 3 ? codeTextRowCount : 3;

  // インスペクターを出力する関数
  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody title={__("Highlight Basic Settings", "custom-highlight-block")}>
        <div {...inspectorDivAttributes}>
          <PanelRow>
            <TextControl
              label={__("Language", "custom-highlight-block")}
              value={attributes.language}
              onChange={(value) => setAttributes({ language: value })}
              className="lang-name"
            />
          </PanelRow>
          <PanelRow>
            <TextControl
              label={__("Label", "custom-highlight-block")}
              value={attributes.label}
              onChange={(value) => setAttributes({ label: value })}
              className="label-name"
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label={__("white-space: pre-wrap", "custom-highlight-block")}
              checked={attributes.wrap}
              onChange={(val) => setAttributes({ wrap: val })}
              help={__("Enable Text Auto Wrap", "custom-highlight-block")}
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label={__("No Line Number", "custom-highlight-block")}
              checked={attributes.noLineNum}
              onChange={(val) => setAttributes({ noLineNum: val })}
              help={__("Hide Line Number", "custom-highlight-block")}
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label={__("No Language Name", "custom-highlight-block")}
              checked={attributes.showNoLang}
              onChange={(val) => setAttributes({ showNoLang: val })}
              help={__("Hide Language Name", "custom-highlight-block")}
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label={__("No Copy Button", "custom-highlight-block")}
              checked={attributes.noCopyBtn}
              onChange={(val) => setAttributes({ noCopyBtn: val })}
              help={__("Do Not Show Copy Button", "custom-highlight-block")}
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label={__("No Toolbar", "custom-highlight-block")}
              checked={attributes.noToolbar}
              onChange={(val) => setAttributes({ noToolbar: val })}
              help={__("Hide Toolbar", "custom-highlight-block")}
            />
          </PanelRow>
          <PanelRow>
            <TextControl
              label={__("Line Highlight", "custom-highlight-block")}
              value={attributes.lineHighlight}
              onChange={(value) => setAttributes({ lineHighlight: value })}
              className="line-highlight"
              placeholder={__("e.g. 1, 3-5", "custom-highlight-block")}
            />
          </PanelRow>
          <PanelRow>
            <TextControl
              label={__("Start Line Number", "custom-highlight-block")}
              value={attributes.lineNumStart}
              onChange={(value) => setAttributes({ lineNumStart: value })}
              className="line-num-start"
            />
          </PanelRow>
          <PanelRow>
            <TextControl
              label={__("Override Language Text", "custom-highlight-block")}
              value={attributes.setLang}
              onChange={(value) => setAttributes({ setLang: value })}
              className="set-lang"
              placeholder={__("Language Text", "custom-highlight-block")}
            />
          </PanelRow>
          <PanelRow>
            <PanelBody
              title={__("LABEL Option", "custom-highlight-block")}
              initialOpen={attributes.labelUrl ? true : false}
            >
              <PanelRow>
                <TextControl
                  label={__("Label URL", "custom-highlight-block")}
                  value={attributes.labelUrl}
                  onChange={(value) => setAttributes({ labelUrl: value })}
                  className="label-url"
                />
              </PanelRow>
              <PanelRow>
                <CheckboxControl
                  label={__("target=_blank", "custom-highlight-block")}
                  checked={attributes.targetBlank}
                  onChange={(val) => setAttributes({ targetBlank: val })}
                  help={__("target attribute", "custom-highlight-block")}
                />
              </PanelRow>
            </PanelBody>
          </PanelRow>
          <PanelRow>
            <PanelBody
              title={__("Max Lines", "custom-highlight-block")}
              initialOpen={attributes.maxLines ? true : false}
            >
              <PanelRow>
                <TextControl
                  label={__("Set Max Lines", "custom-highlight-block")}
                  value={attributes.maxLines}
                  onChange={(value) => setAttributes({ maxLines: value })}
                  className="max-lines"
                  placeholder={__( "specify max number of lines", "custom-highlight-block" )}
                  help={attributes.maxLines && attributes.toggleCode ? "Can not work properly with toggleCode on Firefox and iOS!" : "" }
                />
              </PanelRow>
              <PanelRow>
                <TextControl
                  label={__("Max Lines Offset (px)", "custom-highlight-block")}
                  value={attributes.maxLinesOffset}
                  onChange={(value) => setAttributes({ maxLinesOffset: value })}
                  className="max-lines-offset"
                  placeholder={__("offset height (pixel)", "custom-highlight-block")}
                />
              </PanelRow>
              <PanelRow>
                <TextControl
                  label={__("Scroll To Line Number", "custom-highlight-block")}
                  value={attributes.maxLinesScrollTo}
                  onChange={(value) => setAttributes({ maxLinesScrollTo: value })}
                  className="max-lines-scroll-to"
                />
              </PanelRow>
            </PanelBody>
          </PanelRow>
          <PanelRow>
            <PanelBody
              title={__("Copy Option", "custom-highlight-block")}
              initialOpen={
                attributes.copyNoPrompt ||
                attributes.copyNoSlComments ||
                attributes.copyNoMlComments ||
                attributes.copyNoHTMLComments
                  ? true
                  : false
              }
            >
              <PanelRow>
                <CheckboxControl
                  label={__("No Prompt", "custom-highlight-block")}
                  checked={attributes.copyNoPrompt}
                  onChange={(val) => setAttributes({ copyNoPrompt: val })}
                  help={__("Exclude prompt ($, %)", "custom-highlight-block")}
                />
              </PanelRow>
              <PanelRow>
                <CheckboxControl
                  label={__("No Single-Line Comments", "custom-highlight-block")}
                  checked={attributes.copyNoSlComments}
                  onChange={(val) => setAttributes({ copyNoSlComments: val })}
                  help={__("Exclude Single Line Comments", "custom-highlight-block")}
                />
              </PanelRow>
              <PanelRow>
                <CheckboxControl
                  label={__("No Multi-Line Comments", "custom-highlight-block")}
                  checked={attributes.copyNoMlComments}
                  onChange={(val) => setAttributes({ copyNoMlComments: val })}
                  help={__("Exclude Single Line Comments", "custom-highlight-block")}
                />
              </PanelRow>
              <PanelRow>
                <CheckboxControl
                  label={__("No HTML Comments", "custom-highlight-block")}
                  checked={attributes.copyNoHTMLComments}
                  onChange={(val) => setAttributes({ copyNoHTMLComments: val })}
                  help={__("Exclude HTML Comments", "custom-highlight-block")}
                />
              </PanelRow>
            </PanelBody>
          </PanelRow>
          <PanelRow>
            <PanelBody
              title={__("Toggle Code Accordion", "custom-highlight-block")}
              initialOpen={attributes.toggleCode ? true : false}
            >
              <PanelRow>
                <CheckboxControl
                  label={__("Toggle Code", "custom-highlight-block")}
                  checked={attributes.toggleCode}
                  onChange={(val) => setAttributes({ toggleCode: val })}
                  help={__("Toggle (Show/Hide) Code ", "custom-highlight-block")}
                />
              </PanelRow>
              <PanelRow>
                <TextControl
                  label={__("Open Button Label", "custom-highlight-block")}
                  value={attributes.accodionOpenBtnDefaultLabel}
                  onChange={(value) => setAttributes({ accodionOpenBtnDefaultLabel: value })}
                  className="accordion-open-label"
                  placeholder={__("Open (Default)", "custom-highlight-block")}
                />
              </PanelRow>
              <PanelRow>
                <TextControl
                  label={__("Close Button Label", "custom-highlight-block")}
                  value={attributes.accodionCloseBtnDefaultLabel}
                  onChange={(value) => setAttributes({ accodionCloseBtnDefaultLabel: value })}
                  className="accordion-close-label"
                  placeholder={__("Close (Default)", "custom-highlight-block")}
                />
              </PanelRow>
            </PanelBody>
          </PanelRow>
          </div>
        </PanelBody>
      </InspectorControls>
    );
  };

  // プレビューモードのボタンに使用するアイコン
  const codeIcon = (
    <svg
      viewBox="0 0 16 16"
      xmlns="http://www.w3.org/2000/svg"
      aria-hidden="true"
      focusable="false"
      width="24"
      height="24"
    >
      <path d="M10.478 1.647a.5.5 0 1 0-.956-.294l-4 13a.5.5 0 0 0 .956.294l4-13zM4.854 4.146a.5.5 0 0 1 0 .708L1.707 8l3.147 3.146a.5.5 0 0 1-.708.708l-3.5-3.5a.5.5 0 0 1 0-.708l3.5-3.5a.5.5 0 0 1 .708 0zm6.292 0a.5.5 0 0 0 0 .708L14.293 8l-3.147 3.146a.5.5 0 0 0 .708.708l3.5-3.5a.5.5 0 0 0 0-.708l-3.5-3.5a.5.5 0 0 0-.708 0z" />
    </svg>
  );

  // ツールバーを出力する関数
  const getToolbarControls = () => {
    return (
      <BlockControls>
        <ToolbarGroup>
          <ToolbarButton
            text={attributes.isEditMode ? "Preview" : "Edit"}
            icon={attributes.isEditMode ? codeIcon : "edit"}
            label={attributes.isEditMode ? "Preview" : "Edit"}
            className="edit-preview-button"
            onClick={() =>
              setAttributes({ isEditMode: !attributes.isEditMode }) // 値を反転
            }
          />
        </ToolbarGroup>
      </BlockControls>
    );
  };

  // ref を宣言してcode 要素の祖先の div.hljs-wrap に指定
  const codeWrapperRef = useRef(null);
  // ref を宣言してアコーディオンパネルの details 要素に指定
  const codeDetailsRef = useRef(null);

  useEffect(() => {
    // プレビューモードであれば
    if (!attributes.isEditMode) {
      // Highlight.js の初期化とカスタマイズの適用(codeWrapperRef.current は div.hljs-wrap)
      mySetupHighlightJs(myCustomHighlightJsSettings, codeWrapperRef.current);
      // アコーディオンパネルで表示する場合は更に以下の関数を適用
      if(attributes.toggleCode) {
        // アコーディオンパネルアニメーションを設定(codeDetailsRef.current は details 要素)
        mySetupToggleDetailsAnimation(codeDetailsRef.current);
      }
    }
  }, [attributes.isEditMode]);

  // ブロックが選択されていない場合はプレビューモードを終了
  if (!isSelected) {
    setAttributes({ isEditMode: true });
  }

  // pre 要素に設定する属性のオブジェクト
  const preAttributes = {};
  // attributes の値により、pre 要素に設定するクラス属性を作成
  let preClassList = attributes.wrap ? "pre-wrap" : "pre";
  preClassList += attributes.noLineNum ? " no-line-num" : "";
  preClassList += attributes.noCopyBtn ? " no-copy-btn" : "";
  preClassList += attributes.targetBlank ? " target-blank" : "";
  preClassList += attributes.scroll ? " scroll" : "";
  preClassList += attributes.smooth ? " smooth" : "";
  preClassList += attributes.showResetBtn ? " reset-btn" : "";
  preClassList += attributes.copyNoPrompt ? " copy-no-prompt" : "";
  preClassList += attributes.copyNoSlComments ? " copy-no-sl-comments" : "";
  preClassList += attributes.copyNoMlComments ? " copy-no-ml-comments" : "";
  preClassList += attributes.copyNoHTMLComments ? " copy-no-html-comments" : "";
  // pre 要素に クラスを追加
  preAttributes.className = preClassList;

  // attributes の値により、pre 要素に data-* 属性を追加
  if (attributes.label) preAttributes["data-label"] = attributes.label;
  if (attributes.labelUrl) preAttributes["data-label-url"] = attributes.labelUrl;
  if (attributes.lineHighlight) preAttributes["data-line-highlight"] = attributes.lineHighlight;
  if (attributes.lineNumStart) preAttributes["data-line-num-start"] = attributes.lineNumStart;
  if (attributes.showUntil) preAttributes["data-show-until"] = attributes.showUntil;
  if (attributes.showUntilOffset) preAttributes["data-show-until-offset"] = attributes.showUntilOffset;
  if (attributes.showUntilLabel) preAttributes["data-show-until-label"] = attributes.showUntilLabel;
  if (attributes.showUntilLabelReduce) preAttributes["data-show-until-label-reduce"] = attributes.showUntilLabelReduce;
  if (attributes.maxLines) preAttributes["data-max-lines"] = attributes.maxLines;
  if (attributes.maxLinesScrollTo) preAttributes["data-scroll-to"] = attributes.maxLinesScrollTo;

  // code 要素に設定する属性のオブジェクト
  const codeAttributes = {};
  // code 要素に設定するクラス属性
  let codeClassList;
  if(attributes.language) codeClassList = `language-${attributes.language}`;
  if (codeClassList) {
    codeClassList += attributes.showNoLang ? " show-no-lang" : "";
  } else {
    codeClassList = attributes.showNoLang ? "show-no-lang" : "";
  }
  // code 要素に クラスを追加
  if(codeClassList) codeAttributes.className = codeClassList;

  // code 要素に設定する data-* 属性
  if (attributes.setLang) codeAttributes["data-set-lang"] = attributes.setLang;

  //配列で指定
  return [
    getToolbarControls(),
    getInspectorControls(),
    <>
      {attributes.isEditMode && (  // 編集モード
        <div {...useBlockProps()}>
          <TextareaControl
            label={__("Highlight Code", "custom-highlight-block")}
            value={attributes.codeText}
            onChange={(value) => setAttributes({ codeText: value })}
            rows={codeTextRows}
            placeholder={__("Write your code...", "custom-highlight-block")}
            hideLabelFromVision={isSelected ? false : true}
            className="textarea-control"
          />
        </div>
      )}
      {!attributes.isEditMode && (  // プレビューモード
        <>
        {attributes.toggleCode && ( // アコーディオンパネルで表示
          <div {...useBlockProps()}>
            <details class="toggle-code-animation" ref={codeDetailsRef}>
              <summary data-close-text={attributes.accodionCloseBtnDefaultLabel}>
                {attributes.accodionOpenBtnDefaultLabel}
              </summary>
              <div class="details-content-wrapper">
                <div class="details-content">
                  <div className={ attributes.noToolbar ? "hljs-wrap no-toolbar" : "hljs-wrap" } ref={codeWrapperRef}>
                    <pre {...preAttributes}>
                      <code {...codeAttributes}>{attributes.codeText}</code>
                    </pre>
                  </div>
                </div>
              </div>
            </details>
          </div>
        )}
        {!attributes.toggleCode && ( // アコーディオンパネルなし
          <div {...useBlockProps()}>
            <div className={ attributes.noToolbar ? "hljs-wrap no-toolbar" : "hljs-wrap" } ref={codeWrapperRef}>
              <pre {...preAttributes}>
                <code {...codeAttributes}>{attributes.codeText}</code>
              </pre>
            </div>
          </div>
        )}
      </>
      )}
    </>,
  ];
}

editor.scss

プレビュー時にインスペクタを操作できないように disabled にしていますが、その際に見た目が変わらないため、表示がわかりやすいようにスタイルを追加します。

useDisabled フックを使って disabled にする場合、実際には inert="true" が要素に追加されるので、セレクタに [inert="true"] を使ってスタイルを指定します。

.wp-block-wdl-custom-highlight-block textarea:focus {
  background-color: #f6fdf6;
}

.wp-block-wdl-custom-highlight-block label.components-base-control__label {
  font-size: 16px;
}

.max-lines .components-base-control__help {
  color: red;
}

/* 追加 */
.inspectorDiv [inert="true"] input {
  background-color: #eee;
  color: #ccc;
}

.inspectorDiv [inert="true"] input[type="checkbox"] {
  background-color: #ccc;
}

.inspectorDiv [inert="true"] label,
.inspectorDiv [inert="true"] input::placeholder,
.inspectorDiv [inert="true"] .components-button,
.inspectorDiv [inert="true"] .components-base-control__help {
  color: #ccc;
}

ファイルを保存して、エディタでブロックを選択すると、ツールバーにプレビューと編集の切り替えボタンが表示されます。

ボタンをクリックすると、プレビュー表示に切り替わり、ボタンのラベルとインスペクタの表示も切り替わります。

view.js と style.scss を使用する場合

これまでのサンプルでは view.js と style.scss は使用しませんでしたが、これらのファイルを使用して構成することもできます。

view.js はブロックが表示されたときにフロントエンドで読み込まれ、style.scssはエディターとフロントエンドの両方で読み込まれます。

view.js でフロントエンド用の JavaScript をまとめて読み込み、style.scss(エディターとフロントエンド)で CSS をまとめて読み込みます。エディター用の JavaScript はプラグインファイルで読み込みます。

この場合、コンパイル(ビルド)時にフロントエンド側のファイルはミニファイされるので、マニュアルでミニファイする必要がなく、また、ファイルをまとめるのでファイルの読み込みが減ります。

以下は view.js と style.scss を使用する場合の例です。

view.js

view.js に highlight.min.js と custom.js をまとめて記述します。

// highlight.min.js をコピーしてペースト
/*!
  Highlight.js v11.9.0 (git: b7ec4bfafc)
  (c) 2006-2023 undefined and other contributors
  License: BSD-3-Clause
*/
var hljs=function(){"use strict";function e(t){
return t instanceof Map?t.clear=t.delete=t.set=()=>{

・・・中略・・・

},{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},t,g,s],r=[...b]
;return r.pop(),r.push(i),l.contains=r,{name:"YAML",case_insensitive:!0,
aliases:["yml"],contains:b}}})();hljs.registerLanguage("yaml",e)})();

// custom.js をコピーしてペースト
document.addEventListener('DOMContentLoaded', () => {
  setupHighlightJs();
  setupToggleDetailsAnimation();
});

function setupHighlightJs() {
  const bodyClassList = document.body.classList;
  const wrapperSelector = 'div.hljs-wrap';
  let removeLineBrake = false;

・・・中略・・・

};

style.scss

style.scss に highlight.min.css と custom.css をまとめて記述します。

/* highlight.min.css(Highlight.js のテーマ CSS)をコピーしてペースト */
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}・・・中略・・・.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}

/* カスタマイズ用 CSS をコピーしてペースト */
.hljs-wrap {
  max-width: 780px;
  margin: 3rem 0;
  position: relative;
}

・・・中略・・・

details.code summary::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 10px;
  margin: auto 0;
  width: 8px;
  height: 8px;
  border-top: 3px solid #097b27;
  border-right: 3px solid #097b27;
  transform: rotate(45deg);
}
.hljs-wrap .hljs-comment {
  color: #6b788f;
}

index.js

style.scss のインポートを追加します。

import { registerBlockType } from "@wordpress/blocks";
import './style.scss'; // 追加
import Edit from "./edit";
import save from "./save";
import metadata from "./block.json";

const highlightIcon = (
  <svg
    viewBox="0 0 16 16"
    xmlns="http://www.w3.org/2000/svg"
    aria-hidden="true"
    focusable="false"
    width="24"
    height="24"
  >
    <path d="M16 8A8 8 0 1 0 0 8a8 8 0 0 0 16 0m-8 5v1H4.5a.5.5 0 0 0-.093.009A7 7 0 0 1 3.1 13zm0-1H2.255a7 7 0 0 1-.581-1H8zm-6.71-2a7 7 0 0 1-.22-1H8v1zM1 8q0-.51.07-1H8v1zm.29-2q.155-.519.384-1H8v1zm.965-2q.377-.54.846-1H8v1zm2.137-2A6.97 6.97 0 0 1 8 1v1z"/>
  </svg>
);

registerBlockType(metadata.name, {
  icon: highlightIcon,
  edit: Edit,
  save,
});

block.json

"style": "file:./style-index.css", と "viewScript": "file:./view.js" の行を追加します。

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "wdl/custom-highlight-block",
  "version": "0.1.0",
  "title": "Custom Highlight Block",
  "category": "widgets",
  "description": "Custom Syntax Highlight Block using Hightlight.js.",
  "example": {},
  "attributes": {
    "codeText": {
      "type": "string",
      "default": "",
      "source": "text",
      "selector": "code"
    },
    "language": {
      "type": "string"
    },
    "wrap": {
      "type": "boolean"
    },
    "noLineNum": {
      "type": "boolean"
    },
    "showNoLang": {
      "type": "boolean"
    },
    "noCopyBtn": {
      "type": "boolean"
    },
    "noToolbar": {
      "type": "boolean"
    },
    "label": {
      "type": "string"
    },
    "labelUrl": {
      "type": "string"
    },
    "targetBlank": {
      "type": "boolean"
    },
    "lineHighlight": {
      "type": "string"
    },
    "lineNumStart": {
      "type": "string"
    },
    "setLang": {
      "type": "string"
    },
    "maxLines": {
      "type": "string"
    },
    "maxLinesOffset": {
      "type": "string"
    },
    "maxLinesScrollTo": {
      "type": "string"
    },
    "copyNoPrompt": {
      "type": "boolean"
    },
    "copyNoSlComments": {
      "type": "boolean"
    },
    "copyNoMlComments": {
      "type": "boolean"
    },
    "copyNoHTMLComments": {
      "type": "boolean"
    },
    "toggleCode": {
      "type": "boolean"
    },
    "accodionOpenBtnDefaultLabel": {
      "type": "string"
    },
    "accodionCloseBtnDefaultLabel": {
      "type": "string"
    },
    "isEditMode": {
      "type": "boolean",
      "default": true
    }
  },
  "supports": {
    "html": false
  },
  "textdomain": "custom-highlight-block",
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css",
  "viewScript": "file:./view.js"
}

プラグインファイル

プラグインファイル custom-highlight-block.php では、管理画面(エディター)の場合にのみ、Highlight.js の highlight.min.js と プレビュー用の custom-preview.js を読み込みます。

<?php

/**
* Plugin Name:       Custom Highlight Block
* Description:       Custom Syntax Highlight Block using Hightlight.js.
* Requires at least: 6.1
* Requires PHP:      7.0
* Version:           0.1.0
* Author:            WebDesignLeaves
* License:           GPL-2.0-or-later
* License URI:       https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain:       custom-highlight-block
*
* @package           wdl
*/

if (!defined('ABSPATH')) {
  exit;
}

function add_wdl_custom_code_block_scripts_and_styles() {
  $dir = dirname(__FILE__);

  // 管理画面(エディター)の場合にのみ JavaScript ファイルをエンキュー
  if(is_admin()) {
    wp_enqueue_script(
      'highlight-js',
      plugins_url('/highlight-js/highlight.min.js', __FILE__),
      array(),
      filemtime("$dir/highlight-js/highlight.min.js"),
      true
    );
    wp_enqueue_script(
      'custom-js',
      plugins_url('/highlight-js/custom.js', __FILE__),
      array('highlight-js'),
      filemtime("$dir/highlight-js/custom.js"),
      true
    );
  }
}
add_action('enqueue_block_assets', 'add_wdl_custom_code_block_scripts_and_styles');

function custom_highlight_block_custom_highlight_block_block_init() {
  register_block_type(__DIR__ . '/build');
}
add_action('init', 'custom_highlight_block_custom_highlight_block_block_init');
 

edit.js と save.js に変更はありません。

これでファイルを保存すれば、view.js と style.scss を使用した構成になります。

クリーンアップ

不要なコードやファイルを削除します。

view.js と style.scss を使用しない場合は、index.js の style.scss の import や block.json の style と viewScript の行は削除し、view.js と style.scss のファイルを削除することができます。

view.js と style.scss を使用する構成の場合は、highlight-js フォルダの CSS(custom.css、highlight.min.css)を削除することができます。JavaScript は残しておきます。

後でわかりにくくなければ、ファイルを残しておいても問題ありません。

ビルド

すべてのファイルを保存して問題がなければ、control + c を押して npm run start コマンドを終了し、npm run build を実行して、本番環境用にビルドします。

% npm run build

> custom-highlight-block@0.1.0 build
> wp-scripts build

assets by chunk 16.6 KiB (name: index)
  asset index.js 15.6 KiB [emitted] [minimized] (name: index)
  asset index.css 824 bytes [emitted] (name: index)
  asset index.asset.php 179 bytes [emitted] (name: index)
assets by chunk 133 KiB (name: view)
  asset view.js 133 KiB [emitted] [minimized] (name: view)
  asset view.asset.php 84 bytes [emitted] (name: view)
asset ./style-index.css 5.4 KiB [emitted] (name: ./style-index) (id hint: style)
asset block.json 2.2 KiB [emitted] [from: src/block.json] [copied]
Entrypoint view 133 KiB = view.js 133 KiB view.asset.php 84 bytes
Entrypoint index 22 KiB = ./style-index.css 5.4 KiB index.css 824 bytes index.js 15.6 KiB index.asset.php 179 bytes
orphan modules 33.6 KiB (javascript) 1.83 KiB (runtime) [orphan] 24 modules
runtime modules 2.52 KiB 3 modules
built modules 225 KiB (javascript) 6.2 KiB (css/mini-extract) [built]
  javascript modules 225 KiB
    ./src/view.js 200 KiB [built] [code generated]
    ./src/index.js + 10 modules 24.9 KiB [not cacheable] [built] [code generated]
  modules by path ./src/*.scss 6.2 KiB
    css ./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[3].use[1]!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[3].use[2]!./node_modules/sass-loader/dist/cjs.js??ruleSet[1].rules[3].use[3]!./src/style.scss 5.4 KiB [built] [code generated]
    css ./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[3].use[1]!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[3].use[2]!./node_modules/sass-loader/dist/cjs.js??ruleSet[1].rules[3].use[3]!./src/editor.scss 823 bytes [built] [code generated]
webpack 5.90.1 compiled successfully in 1400 ms

plugin-zip

npm run plugin-zip を実行して、プラグインの zip ファイルを作成することができますが、デフォルトではプラグインファイルと readme.txt、及び build ディレクトリのファイルが zip ファイルに含まれます。

% npm run plugin-zip

> custom-highlight-block@0.1.0 plugin-zip
> wp-scripts plugin-zip

Creating archive for `custom-highlight-block` plugin... 🎁

Using Plugin Handbook best practices to discover files:

  Adding `custom-highlight-block.php`.
  Adding `readme.txt`.
  Adding `build/block.json`.
  Adding `build/index.asset.php`.
  Adding `build/index.css`.
  Adding `build/index.js`.
  Adding `build/style-index.css`.
  Adding `build/view.asset.php`.
  Adding `build/view.js`.

Done. `custom-highlight-block.zip` is ready! 🎉

zip ファイルに highlight-js フォルダに配置した JavaScript や CSS などを含めるには、package.json の files フィールドに zip ファイルに含めるファイルやディレクトリを指定します。

以下は、package.json にfiles フィールドを追加して、生成される zip ファイルに highlight-js フォルダ内のファイルと src フォルダのファイルを含めるようにする例です。

{
  "name": "custom-highlight-block",
  "version": "0.1.0",
  "description": "Example block scaffolded with Create Block tool.",
  "author": "The WordPress Contributors",
  "license": "GPL-2.0-or-later",
  "main": "build/index.js",
  "scripts": {
    "build": "wp-scripts build",
    "format": "wp-scripts format",
    "lint:css": "wp-scripts lint-style",
    "lint:js": "wp-scripts lint-js",
    "packages-update": "wp-scripts packages-update",
    "plugin-zip": "wp-scripts plugin-zip",
    "start": "wp-scripts start"
  },
  "files": [ "highlight-js", "build", "src", "custom-highlight-block.php" ],
  "devDependencies": {
    "@wordpress/scripts": "^27.3.0"
  }
}

上記の場合、例えば、以下のような実行結果になります(view.js と style.scss を使用する構成で、CSS ファイルも残している場合)。

% npm run plugin-zip

> custom-highlight-block@0.1.0 plugin-zip
> wp-scripts plugin-zip

Creating archive for `custom-highlight-block` plugin... 🎁

Using the `files` field from `package.json` to detect files:

  Adding `highlight-js/custom.css`.
  Adding `highlight-js/highlight.min.css`.
  Adding `build/index.css`.
  Adding `build/style-index.css`.
  Adding `highlight-js/custom.js`.
  Adding `src/edit.js`.
  Adding `highlight-js/highlight.min.js`.
  Adding `build/index.js`.
  Adding `src/index.js`.
  Adding `src/save.js`.
  Adding `build/view.js`.
  Adding `src/view.js`.
  Adding `build/block.json`.
  Adding `src/block.json`.
  Adding `package.json`.
  Adding `custom-highlight-block.php`.
  Adding `build/index.asset.php`.
  Adding `build/view.asset.php`.
  Adding `src/editor.scss`.
  Adding `src/style.scss`.
  Adding `readme.txt`.

Done. `custom-highlight-block.zip` is ready! 🎉

プラグインを無効化・削除した場合

プラグインを無効化すると、ハイライト表示はされなくなりますが、save() 関数で保存したマークアップが残ります。例えば、以下のようなコードが入力されて保存されている場合、

コードエディターで確認すると、以下のようなマークアップになっています。

コメントタグに囲まれた以下のようなマークアップ部分が保存されます。

save() 関数で useBlockProps.save() を指定した div 要素(.wp-block-wdl-custom-highlight-block)でラップされ、オプションで指定したクラスや data-* 属性が pre 要素や code 要素に追加され、入力内容がエスケープされてテキストコンテンツになっています。

<div class="wp-block-wdl-custom-highlight-block"><div class="hljs-wrap"><pre class="pre-wrap" data-label="foo.js"><code class="language-JavaScript">const foo = document.getElementById(&#39;foo&#39;);
foo.textContent = hello(&#39;foo&#39;);

function hello(name) {
  return `Hello, ${name}!`;
}

//&lt;p id=&quot;foo&quot;&gt;Hello, foo!&lt;/p&gt;</code></pre></div></div>

プラグインを無効化(※または削除)すると、以下のように表示されます。

[注意]※ プラグインを削除すると、作成したプラグインフォルダとその中身がすべて削除され、戻すことはできないので注意が必要です(開発中のファイルが全てなくなります)。

「HTMLとして保存」をクリックすると、以下のように前述のマークアップがカスタム HTML ブロックに変換されます。

save() 関数の変更

save() 関数を変更して return ステートメント内に変更が発生すると、妥当性検証プロセスにより、ブロックでバリデーションエラーが発生します。

例えば、以下のように return ステートメントにクラス属性を追加しただけでも、全てのブロックでバリデーションエラーが発生します。

export default function save({ attributes }) {
  ・・・中略・・・
  return (
    <>
    ・・・中略・・・
      {!attributes.toggleCode && (
        <div {...useBlockProps.save()}>
          { /* div 要素のクラス属性に foo を追加 */ }
          <div className={ attributes.noToolbar ? "hljs-wrap no-toolbar" : "hljs-wrap foo" }>
            <pre {...preAttributes}>
              <code {...codeAttributes}>{escapedCodeText}</code>
            </pre>
          </div>
        </div>
      )}
    </>
  );
}

エディターでは全てのブロックで以下のようなエラーが表示されます。この例の場合、フロントエンド側は特にエラーはなく、今まで通りの表示になります。

「ブロックのリカバリーを試行」をクリックすればエラーはなくなりますが、全てのブロックでその操作を行うのは大変です。

このような場合は、非推奨(deprecated)バージョンを用意して、バリデーションエラーを回避することができます。

詳細:ブロックの非推奨プロセス