Wordpress 是世界上最为流行的博客工具,很多喜爱编程的同学都使用它写技术博客,涉及到代码时,没有语法高亮是一件让读者头疼的事。

这篇文章就是记录我如何为自己的博客添加代码高亮功能,同时后台支持为代码区块(Gutenberg)设置语法高亮的语言。这是一种通用的方法,可以为任何区块新增想要的功能。

由于不可抗力,文章将分为两部分,本文为第二部分:

本文需要读者具有一定的 Wordpress 主题开发的基础知识。

将会用到的 API

Wordpress Javascript Packages Api:
  • wp.element.createElement()
  • wp.components.DropdownMenu()
  • wp.components.MenuItem()
  • wp.blockEditor.PlainText()
  • wp.hooks.addFilter()
  • wp.i18n.__()

准备工作

创建 myguten.js 和 myguten.css 2 个文件到主题目录下。

引入创建的文件

Wordpress 提供了添加区块功能的方法,只不过是通过 Javascript 来控制,因此要先添加用于修改区块编辑器的相关脚本文件(myguten.js)和样式文件(myguten.css),在 functions.php 中添加以下代码:

// 升级 code Block 以适配 highlightjs
function myguten_enqueue() {
    wp_enqueue_script(
        'myguten-script',
        get_template_directory_uri() . '/js/myguten.js',
        array( 'wp-blocks', 'wp-dom-ready', 'wp-edit-post' ),
        filemtime( get_template_directory_uri() . '/js/myguten.js' )
    );
    wp_enqueue_style(
	'myguten-style',
	get_template_directory_uri() . '/css/myguten.css'
    );
}
add_action( 'enqueue_block_editor_assets', 'myguten_enqueue' );

值得注意的是,脚本要求添加三个依赖项 wp-blocks、wp-dom-ready、wp-edit-post,这样在 myguten.js 的上下文里就可以获取到 Wordpress 提供的 Api。

添加 Hook

在 myguten.js 中,为 blocks.registerBlockType 钩子添加函数:

wp.hooks.addFilter(
    'blocks.registerBlockType',
    'Namespace',
    setBlockType
);

添加完成后,当 blocks.registerBlockType 钩子被触发,Wordpress 便会执行 setBlockType 方法,我们就在这个方法里为区块添加功能。

blocks.registerBlockType 钩子

首先要知道 blocks.registerBlockType 钩子会在什么时候触发,顾名思义,在注册一个区块类型的时候会被触发。

该钩子会传入 2 个参数给 setBlockType 方法:

  1. settingsObject,该类型区块的所有配置
  2. nameString,该类型区块的名字

我们就是通过判断区块名来修改特定区块的配置,从而达到为区块添加、删除或者修改功能的。

接下来开始编写 setBlockType 方法。

判断区块类型

好的,让我们写的通用一点,这样方便以后修改别的类型:

function setBlockType(settings, blockName) {
    switch (blockName) {
        case 'core/code':
            return modifyCodeBlock(settings);
        default:
            return settings;
    }
}

通过 switch 来判断区块类型,然后调用特定的方法来修改区块并返回修改后的配置对象。

修改区块的配置对象

你需要了解它,才能正确的修改它。本文的需求只需要使用到配置对象里的其中 3 个属性:

  1. attributesObject,里面是配置对象的所有属性。
  2. editFunction,这是一个方法,返回 React Element 列表,用来渲染对应类型的区块编辑器,第一个元素是编辑器的控制器部分,第二个元素是编辑器的编辑部分。
  3. saveFunction,这是一个方法,返回 React Element,就是显示在文章中的元素。

对应的处理方法也很直接,在 attributes 里面添加新的属性,用来表示选择的高亮语言,重写 editsave 方法。

添加新的属性到 attributes

我们把新的属性命名为 language

settings.attributes.language = {
    type: String,
    default: 'plain'
}

重写 save 方法

先看一下 hightlighjs 要求的 HTML 格式:

<pre><code class="language">...</code></pre>

然后按要求在 save 方法里返回即可:

settings.save = function (_ref) {
    var attrs = _ref.attributes;
    return wp.element.createElement("pre", null, Object(wp.element.createElement)("code", { className: attrs.language || 'plain' }, attrs.content))
}

这里的 wp.element.createElement 其实就是 React 的 createElement 方法。

重写 edit 方法

目标有两个:添加语言选择器,和添加当前选择语言的水印。

settings.edit = function (_ref) {
    var attributes = _ref.attributes,
        setAttributes = _ref.setAttributes,
        className = _ref.className;
    // Build Language Selector
    var langSelector = wp.element.createElement(wp.blockEditor.BlockControls, { key: 'controls' }, wp.element.createElement(wp.components.DropdownMenu, {
        className: 'animus-code-block-language-dropdown-menu',
        icon: 'hammer',
        label: '选择编程语言',
        children: function (props) {
            let langs = [
                { name: 'Plain', val: 'plain' },
                { name: 'Javascript', val: 'javascript' },
                { name: 'Python', val: 'python' },
                { name: 'PHP', val: 'php' },
                { name: 'HTML', val: 'html' },
                { name: 'CSS', val: 'css' },
                { name: 'SASS', val: 'sass' },
                { name: 'Shell', val: 'shell' }
            ];
            let children = [];
            for (let i = 0; i < langs.length; i++) {
                let lang = langs[i];
                let langElem = wp.element.createElement(wp.components.MenuItem, { value: lang.val, onClick: function () {
                    props.onClose();
                    setAttributes({
                        language: lang.val
                    });
                } }, lang.name);
                children.push(langElem);
            }

            return children;
        }
    }));
    // Build Plain Text Editor
    var plainTextEditor = wp.element.createElement(wp.blockEditor.PlainText, {
        className: 'animus-code-editor',
        value: utils_unescape(attributes.content),
        onChange: function (content) {
            return setAttributes({
                content: utils_escape(content)
            });
        },
        placeholder: wp.i18n.__('Write code…'),
        "aria-label": wp.i18n.__('Code')
    });
    var codeEditorWaterMark = wp.element.createElement("div", {
        className: 'animus-code-editor-watermark',
    }, attributes.language);
    var element =  wp.element.createElement("div", {
        className: className
    }, plainTextEditor, codeEditorWaterMark);
    
    return [langSelector, element];
}

附上文中的一些内容处理方法

这些方法均来自 Wordpress

/**
 * Converts the first two forward slashes of any isolated URL from the HTML entity
 * &#73; into /.
 *
 * An isolated URL is a URL that sits in its own line, surrounded only by spacing
 * characters.
 *
 * See https://github.com/WordPress/wordpress-develop/blob/5.1.1/src/wp-includes/class-wp-embed.php#L403
 *
 * @param {string}  content The content of a code block.
 * @return {string} The given content with the first two forward slashes of any
 *                  isolated URL from the HTML entity &#73; into /.
 */
function unescapeProtocolInIsolatedUrls(content) {
    return content.replace(/^(\s*https?:)&#47;&#47;([^\s<>"]+\s*)$/m, '$1//$2');
}

/**
 * Returns the given content translating all &#91; into [.
 *
 * @param {string}  content The content of a code block.
 * @return {string} The given content with all &#91; into [.
 */
function unescapeOpeningSquareBrackets(content) {
    return content.replace(/&#91;/g, '[');
}

/**
 * Returns the given content with all its ampersand characters converted
 * into their HTML entity counterpart (i.e. & => &amp;)
 *
 * @param {string}  content The content of a code block.
 * @return {string} The given content with its ampersands converted into
 *                  their HTML entity counterpart (i.e. & => &amp;)
 */
function unescapeAmpersands(content) {
    return content.replace(/&amp;/g, '&');
}

/**
 * Escapes ampersands, shortcodes, and links.
 *
 * @param {string} content The content of a code block.
 * @return {string} The given content with some characters escaped.
 */
function utils_escape(content) {
    return lodash.flow(escapeAmpersands, escapeOpeningSquareBrackets, escapeProtocolInIsolatedUrls)(content || '');
}

/**
 * Unescapes escaped ampersands, shortcodes, and links.
 *
 * @param {string} content Content with (maybe) escaped ampersands, shortcodes, and links.
 * @return {string} The given content with escaped characters unescaped.
 */
function utils_unescape(content) {
    return lodash.flow(unescapeProtocolInIsolatedUrls, unescapeOpeningSquareBrackets, unescapeAmpersands)(content || '');
}

/**
 * Converts the first two forward slashes of any isolated URL into their HTML
 * counterparts (i.e. // => &#47;&#47;). For instance, https://youtube.com/watch?x
 * becomes https:&#47;&#47;youtube.com/watch?x.
 *
 * An isolated URL is a URL that sits in its own line, surrounded only by spacing
 * characters.
 *
 * See https://github.com/WordPress/wordpress-develop/blob/5.1.1/src/wp-includes/class-wp-embed.php#L403
 *
 * @param {string}  content The content of a code block.
 * @return {string} The given content with its ampersands converted into
 *                  their HTML entity counterpart (i.e. & => &amp;)
 */
function escapeProtocolInIsolatedUrls(content) {
    return content.replace(/^(\s*https?:)\/\/([^\s<>"]+\s*)$/m, '$1&#47;&#47;$2');
}

/**
 * Returns the given content with all opening shortcode characters converted
 * into their HTML entity counterpart (i.e. [ => &#91;). For instance, a
 * shortcode like [embed] becomes &#91;embed]
 *
 * This function replicates the escaping of HTML tags, where a tag like
 * <strong> becomes &lt;strong>.
 *
 * @param {string}  content The content of a code block.
 * @return {string} The given content with its opening shortcode characters
 *                  converted into their HTML entity counterpart
 *                  (i.e. [ => &#91;)
 */
function escapeOpeningSquareBrackets(content) {
    return content.replace(/\[/g, '&#91;');
}

/**
 * Returns the given content with all its ampersand characters converted
 * into their HTML entity counterpart (i.e. & => &amp;)
 *
 * @param {string}  content The content of a code block.
 * @return {string} The given content with its ampersands converted into
 *                  their HTML entity counterpart (i.e. & => &amp;)
 */
function escapeAmpersands(content) {
    return content.replace(/&/g, '&amp;');
}

以上便是为 Wordpress 代码区块添加高亮语言选择的方法。