PluginProbe ʕ •ᴥ•ʔ
Syntax-highlighting Code Block (with Server-side Rendering) / 1.1.0
Syntax-highlighting Code Block (with Server-side Rendering) v1.1.0
trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 1.3.1 1.4.0 1.5.0 1.5.1 1.5.2
syntax-highlighting-code-block / syntax-highlighting-code-block.php
syntax-highlighting-code-block Last commit date
build 6 years ago vendor 6 years ago wp-assets 6 years ago LICENSE 6 years ago line-numbers.css 6 years ago readme.txt 6 years ago syntax-highlighting-code-block.php 6 years ago
syntax-highlighting-code-block.php
372 lines
1 <?php
2 /**
3 * Plugin Name: Syntax-highlighting Code Block (with Server-side Rendering)
4 * Plugin URI: https://github.com/westonruter/syntax-highlighting-code-block
5 * Description: Extending the Code block with syntax highlighting rendered on the server, thus being AMP-compatible and having faster frontend performance.
6 * Version: 1.1.0
7 * Author: Weston Ruter
8 * Author URI: https://weston.ruter.net/
9 * License: GPL2
10 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
11 * Text Domain: syntax-highlighting-code-block
12 * Requires PHP: 5.6
13 *
14 * @package Syntax_Highlighting_Code_Block
15 */
16
17 namespace Syntax_Highlighting_Code_Block;
18
19 const PLUGIN_VERSION = '1.1.0';
20
21 const DEVELOPMENT_MODE = false;
22
23 const OPTION_NAME = 'syntax_highlighting';
24
25 const DEFAULT_THEME = 'default';
26
27 const BLOCK_STYLE_FILTER = 'syntax_highlighting_code_block_style';
28
29 const FRONTEND_STYLE_HANDLE = 'syntax-highlighting-code-block';
30
31 /**
32 * Get an array of all the options tied to this plugin.
33 *
34 * @return array
35 */
36 function get_options() {
37 $options = \get_option( OPTION_NAME, [] );
38
39 return array_merge(
40 [
41 'theme_name' => DEFAULT_THEME,
42 ],
43 $options
44 );
45 }
46
47 /**
48 * Get the single, specified plugin option.
49 *
50 * @param string $option_name The plugin option name.
51 *
52 * @return mixed
53 */
54 function get_option( $option_name ) {
55 return get_options()[ $option_name ];
56 }
57
58 /**
59 * Get path to script deps file.
60 *
61 * @return string Path.
62 */
63 function get_script_deps_path() {
64 return __DIR__ . '/build/index.deps.json';
65 }
66
67 /**
68 * Initialize plugin.
69 */
70 function init() {
71 if ( ! function_exists( 'register_block_type' ) ) {
72 return;
73 }
74
75 if ( DEVELOPMENT_MODE && ! file_exists( get_script_deps_path() ) ) {
76 add_action( 'admin_notices', __NAMESPACE__ . '\print_build_required_admin_notice' );
77 return;
78 }
79
80 register_block_type(
81 'core/code',
82 [
83 'render_callback' => __NAMESPACE__ . '\render_block',
84 ]
85 );
86
87 add_action( 'enqueue_block_editor_assets', __NAMESPACE__ . '\enqueue_editor_assets' );
88 add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\register_frontend_assets' );
89 }
90 add_action( 'plugins_loaded', __NAMESPACE__ . '\init' );
91
92 /**
93 * Print admin notice when plugin installed from source but no build being performed.
94 */
95 function print_build_required_admin_notice() {
96 ?>
97 <div class="notice notice-error">
98 <p>
99 <strong><?php esc_html_e( 'Syntax-highlighting Code Block', 'amp' ); ?>:</strong>
100 <?php
101 echo wp_kses_post(
102 sprintf(
103 /* translators: %s is the command to run */
104 __( 'Unable to initialize plugin due to being installed from source without running a build. Please run %s', 'syntax-highlighting-code-block' ),
105 '<code>composer install &amp;&amp; npm install &amp;&amp; npm run build</code>'
106 )
107 );
108 ?>
109 </p>
110 </div>
111 <?php
112 }
113
114 /**
115 * Enqueue assets for editor.
116 */
117 function enqueue_editor_assets() {
118 $handle = 'syntax-highlighting-code-block';
119 $script_path = '/build/index.js';
120 $script_deps = json_decode( file_get_contents( get_script_deps_path() ), false ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
121 $in_footer = true;
122
123 wp_enqueue_script(
124 $handle,
125 plugins_url( $script_path, __FILE__ ),
126 $script_deps,
127 DEVELOPMENT_MODE ? filemtime( plugin_dir_path( __FILE__ ) . $script_path ) : PLUGIN_VERSION,
128 $in_footer
129 );
130
131 wp_set_script_translations( $handle, 'syntax-highlighting-code-block' );
132 }
133
134 /**
135 * Register assets for the frontend.
136 *
137 * Asset(s) will only be enqueued if needed.
138 */
139 function register_frontend_assets() {
140 if ( has_filter( BLOCK_STYLE_FILTER ) ) {
141 /**
142 * Filters the style used for the code syntax block.
143 *
144 * The string returned must correspond to the filenames found at <https://github.com/scrivo/highlight.php/tree/master/styles>,
145 * minus the file extension.
146 *
147 * This filter takes precedence over any settings set in the database as an option. Additionally, if this filter
148 * is provided, then a theme selector will not be provided in Customizer.
149 *
150 * @since 1.0.0
151 * @param string $style Style.
152 */
153 $style = apply_filters( BLOCK_STYLE_FILTER, DEFAULT_THEME );
154 } else {
155 $style = get_option( 'theme_name' );
156 }
157
158 $default_style_path = sprintf( 'vendor/scrivo/highlight.php/styles/%s.css', 0 === validate_file( $style ) ? $style : DEFAULT_THEME );
159 wp_register_style(
160 FRONTEND_STYLE_HANDLE,
161 plugins_url( $default_style_path, __FILE__ ),
162 [],
163 SCRIPT_DEBUG ? filemtime( plugin_dir_path( __FILE__ ) . $default_style_path ) : PLUGIN_VERSION
164 );
165 }
166
167 /**
168 * Render code block.
169 *
170 * @param array $attributes Attributes.
171 * @param string $content Content.
172 * @return string Highlighted content.
173 */
174 function render_block( $attributes, $content ) {
175 $pattern = '(?P<before><pre.*?><code.*?>)';
176 $pattern .= '(?P<code>.*)';
177 $after = '</code></pre>';
178 $pattern .= $after;
179
180 if ( ! preg_match( '#^\s*' . $pattern . '\s*$#s', $content, $matches ) ) {
181 return $content;
182 }
183
184 if ( ! isset( $attributes['language'] ) ) {
185 $attributes['language'] = '';
186 }
187
188 if ( ! isset( $attributes['showLines'] ) ) {
189 $attributes['showLines'] = false;
190 }
191
192 // Enqueue the style now that we know it will be needed.
193 wp_enqueue_style( FRONTEND_STYLE_HANDLE );
194
195 // Include line-number styles if requesting to show lines.
196 if ( $attributes['showLines'] ) {
197 $after_styles = wp_styles()->get_data( FRONTEND_STYLE_HANDLE, 'after' );
198 if ( ! is_array( $after_styles ) ) {
199 $after_styles = '';
200 } else {
201 $after_styles = implode( '', $after_styles );
202 }
203
204 // Only include line-number styles if not already included.
205 if ( false === strpos( $after_styles, '.hljs.line-numbers' ) ) {
206 wp_add_inline_style(
207 FRONTEND_STYLE_HANDLE,
208 file_get_contents( plugins_url( 'line-numbers.css', __FILE__ ) ) // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
209 );
210 }
211 }
212
213 $inject_classes = function( $start_tags, $language, $show_lines ) {
214 $added_classes = "hljs language-$language";
215
216 if ( $show_lines ) {
217 $added_classes .= ' line-numbers';
218 }
219
220 $start_tags = preg_replace(
221 '/(<code[^>]*class=")/',
222 '$1 ' . esc_attr( $added_classes ),
223 $start_tags,
224 1,
225 $count
226 );
227 if ( 0 === $count ) {
228 $start_tags = preg_replace(
229 '/(?<=<code)(?=>)/',
230 sprintf( ' class="%s"', esc_attr( $added_classes ) ),
231 $start_tags,
232 1
233 );
234 }
235 return $start_tags;
236 };
237
238 $transient_key = 'syntax-highlighting-code-block-' . md5( $attributes['showLines'] . $attributes['language'] . $matches['code'] ) . '-v' . PLUGIN_VERSION;
239 $highlighted = get_transient( $transient_key );
240
241 if ( $highlighted && isset( $highlighted['code'] ) ) {
242 if ( isset( $highlighted['language'] ) ) {
243 $matches['before'] = $inject_classes( $matches['before'], $highlighted['language'], $highlighted['show_lines'] );
244 }
245 return $matches['before'] . $highlighted['code'] . $after;
246 }
247
248 try {
249 if ( ! class_exists( '\Highlight\Autoloader' ) ) {
250 require_once __DIR__ . '/vendor/scrivo/highlight.php/Highlight/Autoloader.php';
251 spl_autoload_register( 'Highlight\Autoloader::load' );
252 }
253
254 $highlighter = new \Highlight\Highlighter();
255 $language = $attributes['language'];
256 $code = html_entity_decode( $matches['code'], ENT_QUOTES );
257
258 // Convert from Prism.js languages names.
259 if ( 'clike' === $language ) {
260 $language = 'cpp';
261 } elseif ( 'git' === $language ) {
262 $language = 'diff'; // Best match.
263 } elseif ( 'markup' === $language ) {
264 $language = 'xml';
265 }
266
267 if ( $language ) {
268 $r = $highlighter->highlight( $language, $code );
269 } else {
270 $r = $highlighter->highlightAuto( $code );
271 }
272
273 $code = $r->value;
274 $language = $r->language;
275 $show_lines = $attributes['showLines'];
276
277 if ( $show_lines ) {
278 require_once __DIR__ . '/vendor/scrivo/highlight.php/HighlightUtilities/functions.php';
279
280 $lines = \HighlightUtilities\splitCodeIntoArray( $code );
281 $code = '';
282
283 // We need to wrap the line of code twice in order to let out `white-space: pre` CSS setting to be respected
284 // by our `table-row`.
285 foreach ( $lines as $line ) {
286 $code .= sprintf( '<div class="loc"><span>%s</span></div>%s', $line, PHP_EOL );
287 }
288 }
289
290 $highlighted = compact( 'code', 'language', 'show_lines' );
291
292 set_transient( $transient_key, compact( 'code', 'language', 'show_lines' ), MONTH_IN_SECONDS );
293
294 $matches['before'] = $inject_classes( $matches['before'], $highlighted['language'], $highlighted['show_lines'] );
295
296 return $matches['before'] . $code . $after;
297 } catch ( \Exception $e ) {
298 return sprintf(
299 '<!-- %s(%s): %s -->%s',
300 get_class( $e ),
301 $e->getCode(),
302 str_replace( '--', '', $e->getMessage() ),
303 $content
304 );
305 }
306 }
307
308 /**
309 * Initialize admin settings.
310 */
311 function admin_init() {
312 register_setting( 'syntax_highlighting', OPTION_NAME );
313 }
314 add_action( 'admin_init', __NAMESPACE__ . '\admin_init' );
315
316 /**
317 * Validate the given stylesheet name against available stylesheets.
318 *
319 * @param \WP_Error $validity Validator object.
320 * @param string $input Incoming theme name.
321 *
322 * @return mixed
323 */
324 function validate_theme_name( $validity, $input ) {
325 require_once __DIR__ . '/vendor/scrivo/highlight.php/HighlightUtilities/functions.php';
326
327 $themes = \HighlightUtilities\getAvailableStyleSheets();
328
329 if ( ! in_array( $input, $themes, true ) ) {
330 $validity->add( 'invalid_theme', __( 'Unrecognized theme', 'syntax-highlighting-code-block' ) );
331 }
332
333 return $validity;
334 }
335
336 /**
337 * Add plugin settings to Customizer.
338 *
339 * @param \WP_Customize_Manager $wp_customize The Customizer object.
340 */
341 function customize_register( $wp_customize ) {
342 if ( has_filter( BLOCK_STYLE_FILTER ) ) {
343 return;
344 }
345
346 require_once __DIR__ . '/vendor/scrivo/highlight.php/HighlightUtilities/functions.php';
347
348 $themes = \HighlightUtilities\getAvailableStyleSheets();
349 sort( $themes );
350 $choices = array_combine( $themes, $themes );
351
352 $wp_customize->add_setting(
353 'syntax_highlighting[theme_name]',
354 [
355 'type' => 'option',
356 'default' => DEFAULT_THEME,
357 'validate_callback' => __NAMESPACE__ . '\validate_theme_name',
358 ]
359 );
360 $wp_customize->add_control(
361 'syntax_highlighting[theme_name]',
362 [
363 'type' => 'select',
364 'section' => 'colors',
365 'label' => __( 'Syntax Highlighting Theme', 'syntax-highlighting-code-block' ),
366 'description' => __( 'Preview the theme by navigating to a page with a code block to see the different themes in action.', 'syntax-highlighting-code-block' ),
367 'choices' => $choices,
368 ]
369 );
370 }
371 add_action( 'customize_register', __NAMESPACE__ . '\customize_register' );
372