The Gutenberg editor is now WordPress’s default and support for the previous Classic editor is through a plugin. That support is not guaranteed after 31 December 2021. I wanted to patch Aram Kocharyan’s Crayon Syntax Highlighter plugin to work with Gutenberg.
Other people have had the same objective. I relied heavily on Fedor Urvanov’s resuscitation of the Syntax Highlighter to understand what was required and largely followed his example.
Registering the block type
On the server side, a block type needs to be registered with the register_block_type()
function.
1 2 3 4 |
register_block_type( 'crayon-syntax-highlighter/crayon', ['editor_style' => 'crayon-syntax-highlighter-editor'] ); |
The block type identifier, which comprises the name space and the block name, appears in the HTML comments that delimit the block in the code editor, so I chose a short name for the block:
1 2 3 4 5 6 |
<!-- wp:crayon-syntax-highlighter/crayon --> <div class="wp-block-crayon-syntax-highlighter-crayon"><pre class="font:cascadia-code lang:php decode:true " title="Register block type">register_block_type( 'crayon-syntax-highlighter/crayon', ['editor_style' => 'crayon-syntax-highlighter-editor'] );</pre></div> <!-- /wp:crayon-syntax-highlighter/crayon --> |
Cascading Style Sheets (CSS) used by a web page need to be made available to it (enqueued). The 'editor_style'
key of the array second argument of register_block_type()
enqueues a CSS applied in the editor.
Registering the block
On the client side, the block needs to be registered with the wp.blocks.registerBlockType()
function, which takes a string
name
(the block type) and an Object
settings
. The settings has properties title
, category
, icon
(optional) and attributes
. It also has methods edit
and save
.
The attributes
property is an Object
. Each of its properties specifies a source. For example, this specifies that content
is a string
stored in the inner text of the div
HTML element:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
registerBlockType( 'crayon-syntax-highlighter/crayon', { title: 'Crayon', icon: 'editor-code', category: 'formatting', attributes: { content: { type: 'string', source: 'html', selector: 'div' } }, ... } ); |
The edit
method represents what the Gutenberg editor will render when the block is used in the editor. It takes an Object
parameter props
. In the following, the variable el
contains the function wp.element.createElement()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
edit: function(props) { var content = props.attributes.content; function onChangeContent(newContent) { props.setAttributes({content: newContent}); } return el( wp.element.Fragment, null, el( wp.editor.BlockControls, null, el( wp.components.Toolbar, null, el( wp.components.IconButton, { icon: 'editor-code', title: 'Crayon', onClick: function() { window.CrayonTagEditor.showDialog({ update: function(shortcode) {}, br_html_block_after: '', input: 'decode', output: 'encode', node: content ? CrayonUtil.htmlToElements(content)[0] : null, insert: function(shortcode) { onChangeContent(shortcode); } }); } }, "Crayon" ) ) ), el( 'div', { style: blockStyle, dangerouslySetInnerHTML: {__html: props.attributes.content} } ) ); }, |
The function renders two elements: a block control containing a toolbar containing an icon button; and a div
element containing props.attributes.content
.
The onClick
event of the icon button is set to a call of the showDialog()
method of the CrayonTagEditor
class:
1 2 3 4 5 6 7 8 9 10 |
function() { window.CrayonTagEditor.showDialog({ update: function(shortcode) {}, br_html_block_after: '', input: 'decode', output: 'encode', node: content ? CrayonUtil.htmlToElements(content)[0] : null, insert: function(shortcode) {onChangeContent(shortcode);} }); } |
The save
method represents what the front end will render:
1 2 3 4 5 6 7 |
save: function(props) { var content = props.attributes.content; return el( 'div', {dangerouslySetInnerHTML: {__html: content}} ); }, |
Registering the format
The Format API allows a custom button to be added to the formatting toolbar that applies a format to a text selection. The first step is to register the new format type:
1 2 3 4 5 6 7 8 9 |
wp.richText.registerFormatType( 'crayon-syntax-highlighter/code-inline', { title: 'Crayon', tagName: 'span', className: CRAYON_INLINE_CSS, edit: CrayonButton } ); |
The CrayonButton
attached to the edit
method renders a richtext toolbar button. The start
property of the value
property is the index of the first character in the selection and similarly for the end
property.
If the selection is of the relevant format type, it is expanded to cover all adjacent indices with the relevant format type. The selection is sliced out of the rich text, converted to HTML, and then to a node, which is passed to a call of the showDialog()
method of the CrayonTagEditor
class.
JavaScript arrays are objects and have the method slice([begin[, end]])
, which extracts from begin
up to, but not including, end
. wp.richText.slice()
behaves similarly.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
var CrayonButton = function(props) { return el( wp.editor.RichTextToolbarButton, { icon: 'editor-code', title: 'Crayon', onClick: function(onClickArg) { var activeFormat = wp.richText.getActiveFormat( props.value, 'crayon-syntax-highlighter/code-inline' ); var startIndex = props.value.start; var endIndex = props.value.end; if (activeFormat) { var findFormat = function(formats) { var isFormat = function(el) { return el.type == 'crayon-syntax-highlighter/code-inline'; }; return formats && formats.find(isFormat); }; while (findFormat(props.value.formats[startIndex])) { startIndex--; } startIndex++; endIndex++; while (findFormat(props.value.formats[endIndex])) { endIndex++; } var inputRichTextValue = wp.richText.slice( props.value, startIndex, endIndex ); var inputValue = wp.richText.toHTMLString({ value: inputRichTextValue}); var inputNode = CrayonUtil.htmlToFirstNode(inputValue); } else { var inputRichTextValue = wp.richText.slice( props.value, startIndex, endIndex ); var inputValue = '<span class="' + CRAYON_INLINE_CSS + '">' + wp.richText.toHTMLString({ value: inputRichTextValue}) + '</span>'; var inputNode = CrayonUtil.htmlToFirstNode(inputValue); } window.CrayonTagEditor.showDialog({ update: function(shortcode) {}, node: inputNode, input: 'decode', output: 'encode', insert: function(shortcode) { props.onChange( wp.richText.insert( props.value, wp.richText.create({html:shortcode}), startIndex, endIndex ) ); } }); }, isActive: props.isActive } ); } |
The format is given class name CRAYON_INLINE_CSS
('crayon-inline'
). That class is used to style the format in the editor:
1 2 3 4 |
.wp-block-paragraph .crayon-inline { background-color: lightgray; border: 1px solid black; } |
On the server side, the CSS file needs to be registered with wp_register_style()
function:
1 2 3 4 5 |
wp_register_style('crayon-syntax-highlighter-editor', plugins_url(CRAYON_SYNTAX_HIGHLIGHTER_EDITOR_CSS, __FILE__), ['wp-edit-blocks'], $CRAYON_VERSION ); |
Minifying
JavaScript code includes whitespace characters to make it more intelligible. Similarly, local variable names are usually chosen for intelligibility not brevity. The minification of JavaScript removes unnecessary whitespace and replaces longer local variable names with shorter ones. Similarly, the minification of a CSS file removes characters that are not needed for the CSS to function.
The author of the plugin used minifier application YUICompressor-2.4.7 and bash scripts to minify JavaScript and CSS source code.
util.js
, jquery.popup.js
and crayon.js
were minified into crayon.min.js
. Those files and crayon_qt.js
, jquery.colorbox-min.js
and crayon_tag_editor.js
were minified into crayon.te.min.js
.
colorbox.css
, admin_style.css
, crayon_style.css
and global_style.css
were minified into crayon.min.css
.
The compressor was part of the Yahoo User Interface (YUI) library, which is no longer an active project. The most recent release of the application, version 2.4.8, was released in July 2013. Other compressors are available but, as a temporary measure, I used version 2.4.8 and replaced the bash scripts with PowerShell equivalents.
Clearing the browser cache
As I did not update the Crayon Syntax Highlighter version, I found I needed to clear the brower’s cache of images and files for the updated JavaScript files to be recognised.
Registering Crayon posts
The plugin only renders crayons in those posts that are registered as containing one or more crayons. In the original plugin for the Classic editor, that was done by adding functions to update_post
, save_post
and wp_insert_post_data
hooks:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Only if WP is loaded if (defined('ABSPATH')) { if (!is_admin()) { // Filters and Actions ... } else { // Update between versions CrayonWP::update(); // For marking a post as containing a Crayon add_action('update_post', 'CrayonWP::save_post', 10, 2); add_action('save_post', 'CrayonWP::save_post', 10, 2); add_filter('wp_insert_post_data', 'CrayonWP::filter_post_data', '99', 2); } ... } |
However, in the Gutenberg editor, is_admin()
does not return a true value when a draft post is saved or previewed. The solution is to move adding the functions outside of the application of the is_admin()
condition.
In addition, I can find no record that WordPress has, or has ever had, an update_post
hook (including by reference to Adam R Brown’s database). The use of this hook was introduced by Mr Kocharyan on 1 January 2013. Code adding an action to that hook appears to be redundant.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Only if WP is loaded if (defined('ABSPATH')) { if (!is_admin()) { // Filters and Actions ... } else { // Update between versions CrayonWP::update(); } // For marking a post as containing a Crayon add_action('save_post', 'CrayonWP::save_post', 10, 2); add_filter('wp_insert_post_data', 'CrayonWP::filter_post_data', '99', 2); ... } |
Using the block
The use of the block is illustrated in the screenshot below. Clicking on the icon button in the tool bar in the block controls bar above the block opens the same dialogue box as used in the Classic editor version of the plugin. The content of the crayon is shown below the block controls bar; initially this is empty.