PluginProbe ʕ •ᴥ•ʔ
Syntax-highlighting Code Block (with Server-side Rendering) / trunk
Syntax-highlighting Code Block (with Server-side Rendering) vtrunk
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 / inc / functions.php
syntax-highlighting-code-block / inc Last commit date
functions.php 1 month ago
functions.php
894 lines
1 <?php
2 /**
3 * Functions file.
4 *
5 * TODO: Refactor into classes.
6 *
7 * @package Syntax_Highlighting_Code_Block
8 */
9
10 namespace Syntax_Highlighting_Code_Block;
11
12 use Exception;
13 use WP_Block_Type;
14 use WP_Block_Type_Registry;
15 use WP_Error;
16 use WP_Customize_Manager;
17 use WP_Styles;
18 use WP_REST_Server;
19 use WP_REST_Request;
20 use WP_REST_Response;
21 use WP_Customize_Color_Control;
22 use Highlight\Highlighter;
23 use function HighlightUtilities\splitCodeIntoArray;
24 use function HighlightUtilities\getAvailableStyleSheets;
25 use function HighlightUtilities\getThemeBackgroundColor;
26
27 if ( ! defined( 'ABSPATH' ) ) {
28 exit; // Exit if accessed directly.
29 }
30
31 /**
32 * Boot the plugin.
33 *
34 * @noinspection PhpUnused -- See https://youtrack.jetbrains.com/issue/WI-22217/Extend-possible-linking-between-function-and-callback-using-different-constants-NAMESPACE-CLASS-and-class
35 */
36 function boot(): void {
37 add_action( 'init', __NAMESPACE__ . '\init', 100 );
38 add_action( 'customize_register', __NAMESPACE__ . '\customize_register', 100 );
39 add_action( 'rest_api_init', __NAMESPACE__ . '\register_rest_endpoint' );
40 add_action( 'enqueue_block_assets', __NAMESPACE__ . '\register_styles' );
41 }
42
43 /**
44 * Add a tint to an RGB color and make it lighter.
45 *
46 * @param float[] $rgb_array An array representing an RGB color.
47 * @param float $tint How much of a tint to apply; a number between 0 and 1.
48 * @return float[] The new color as an RGB array.
49 */
50 function add_tint_to_rgb( array $rgb_array, float $tint ): array {
51 return [
52 'r' => $rgb_array['r'] + ( 255 - $rgb_array['r'] ) * $tint,
53 'g' => $rgb_array['g'] + ( 255 - $rgb_array['g'] ) * $tint,
54 'b' => $rgb_array['b'] + ( 255 - $rgb_array['b'] ) * $tint,
55 ];
56 }
57
58 /**
59 * Get the relative luminance of a color.
60 *
61 * @link https://en.wikipedia.org/wiki/Relative_luminance
62 *
63 * @param float[] $rgb_array An array representing an RGB color.
64 * @return float A value between 0 and 100 representing the luminance of a color.
65 * The closer to 100, the higher the luminance is; i.e. the lighter it is.
66 */
67 function get_relative_luminance( array $rgb_array ): float {
68 return 0.2126 * ( $rgb_array['r'] / 255 ) +
69 0.7152 * ( $rgb_array['g'] / 255 ) +
70 0.0722 * ( $rgb_array['b'] / 255 );
71 }
72
73 /**
74 * Check whether a given RGB array is considered a "dark theme."
75 *
76 * @param float[] $rgb_array The RGB array to test.
77 * @return bool True if the theme's background has a "dark" luminance.
78 */
79 function is_dark_theme( array $rgb_array ): bool {
80 return get_relative_luminance( $rgb_array ) <= 0.6;
81 }
82
83 /**
84 * Convert an RGB array to hexadecimal representation.
85 *
86 * @param float[] $rgb_array The RGB array to convert.
87 * @return string A hexadecimal representation.
88 */
89 function get_hex_from_rgb( array $rgb_array ): string {
90 return sprintf(
91 '#%02X%02X%02X',
92 $rgb_array['r'],
93 $rgb_array['g'],
94 $rgb_array['b']
95 );
96 }
97
98 /**
99 * Get the default highlighted line background color.
100 *
101 * In a dark theme, the background color is decided by adding a 15% tint to the
102 * color.
103 *
104 * In a light theme, a default light blue is used.
105 *
106 * @param string $theme_name The theme name to get a color for.
107 * @return string A hexadecimal value.
108 */
109 function get_default_line_background_color( string $theme_name ): string {
110 require_highlight_php_functions();
111
112 $theme_rgb = getThemeBackgroundColor( $theme_name );
113
114 if ( is_dark_theme( $theme_rgb ) ) {
115 return get_hex_from_rgb(
116 add_tint_to_rgb( $theme_rgb, 0.15 )
117 );
118 }
119
120 return DEFAULT_HIGHLIGHTED_COLOR;
121 }
122
123 /**
124 * Get an array of all the options tied to this plugin.
125 *
126 * @return array{
127 * theme_name: string,
128 * highlighted_line_background_color: string
129 * }
130 */
131 function get_plugin_options(): array {
132 $options = get_option( OPTION_NAME );
133 if ( ! is_array( $options ) ) {
134 $options = [];
135 }
136
137 if ( isset( $options['theme_name'] ) && is_string( $options['theme_name'] ) ) {
138 $theme_name = $options['theme_name'];
139 } else {
140 $theme_name = DEFAULT_THEME;
141 }
142
143 if ( isset( $options['highlighted_line_background_color'] ) && is_string( $options['highlighted_line_background_color'] ) ) {
144 $highlighted_line_background_color = $options['highlighted_line_background_color'];
145 } else {
146 $highlighted_line_background_color = get_default_line_background_color( $theme_name );
147 }
148
149 return compact( 'theme_name', 'highlighted_line_background_color' );
150 }
151
152 /**
153 * Get the single, specified plugin option.
154 *
155 * @param string $option_name The plugin option name.
156 * @return string|null
157 */
158 function get_plugin_option( string $option_name ): ?string {
159 $options = get_plugin_options();
160 if ( array_key_exists( $option_name, $options ) ) {
161 return $options[ $option_name ];
162 }
163 return null;
164 }
165
166 /**
167 * Require the highlight.php functions file.
168 */
169 function require_highlight_php_functions(): void {
170 require_once PLUGIN_DIR . '/' . get_highlight_php_vendor_path() . '/HighlightUtilities/functions.php';
171 }
172
173 /**
174 * Initialize plugin.
175 *
176 * As of Gutenberg 8.3, this must run after `init` priority 10, because at that point the core blocks are registered
177 * server-side via `gutenberg_reregister_core_block_types()`.
178 *
179 * @see gutenberg_reregister_core_block_types()
180 * @see https://github.com/WordPress/gutenberg/issues/2751
181 * @see https://github.com/WordPress/gutenberg/pull/22491
182 */
183 function init(): void {
184 if ( ! function_exists( 'register_block_type' ) ) {
185 return;
186 }
187
188 if ( DEVELOPMENT_MODE && ! file_exists( PLUGIN_DIR . '/build/index.asset.php' ) ) {
189 add_action( 'admin_notices', __NAMESPACE__ . '\print_build_required_admin_notice' );
190 return;
191 }
192
193 $registry = WP_Block_Type_Registry::get_instance();
194
195 $block = $registry->get_registered( BLOCK_NAME );
196 if ( $block instanceof WP_Block_Type ) {
197 $block->render_callback = __NAMESPACE__ . '\render_block';
198 $block->attributes = array_merge( $block->attributes ?? [], ATTRIBUTE_SCHEMA );
199 $block->style_handles = array_merge( $block->style_handles, STYLE_HANDLES );
200 } else {
201 $block = register_block_type(
202 BLOCK_NAME,
203 [
204 'render_callback' => __NAMESPACE__ . '\render_block',
205 'attributes' => ATTRIBUTE_SCHEMA,
206 'style_handles' => STYLE_HANDLES,
207 ]
208 );
209 }
210
211 if ( $block instanceof WP_Block_Type ) {
212 register_editor_assets( $block );
213 $block->editor_script_handles[] = EDITOR_SCRIPT_HANDLE;
214 $block->editor_style_handles[] = EDITOR_STYLE_HANDLE;
215 }
216 }
217
218 /**
219 * Print admin notice when plugin installed from source but no build being performed.
220 *
221 * @noinspection PhpUnused -- See https://youtrack.jetbrains.com/issue/WI-22217/Extend-possible-linking-between-function-and-callback-using-different-constants-NAMESPACE-CLASS-and-class
222 */
223 function print_build_required_admin_notice(): void {
224 ?>
225 <div class="notice notice-error">
226 <p>
227 <strong><?php esc_html_e( 'Syntax-highlighting Code Block', 'syntax-highlighting-code-block' ); ?>:</strong>
228 <?php
229 echo wp_kses_post(
230 sprintf(
231 /* translators: %s is the command to run */
232 __( 'Unable to initialize plugin due to being installed from source without running a build. Please run %s', 'syntax-highlighting-code-block' ),
233 '<code>composer install &amp;&amp; npm install &amp;&amp; npm run build</code>'
234 )
235 );
236 ?>
237 </p>
238 </div>
239 <?php
240 }
241
242 /**
243 * Register assets for editor.
244 *
245 * @param WP_Block_Type $block Block.
246 */
247 function register_editor_assets( WP_Block_Type $block ): void {
248 $style_path = '/editor-styles.css';
249 wp_register_style(
250 EDITOR_STYLE_HANDLE,
251 plugins_url( $style_path, PLUGIN_MAIN_FILE ),
252 [],
253 SCRIPT_DEBUG
254 ? (string) filemtime( plugin_dir_path( PLUGIN_MAIN_FILE ) . $style_path )
255 : PLUGIN_VERSION
256 );
257
258 $script_path = '/build/index.js';
259 $script_asset = require PLUGIN_DIR . '/build/index.asset.php';
260 wp_register_script(
261 EDITOR_SCRIPT_HANDLE,
262 plugins_url( $script_path, PLUGIN_MAIN_FILE ),
263 $script_asset['dependencies'],
264 $script_asset['version'],
265 true
266 );
267
268 wp_set_script_translations( EDITOR_SCRIPT_HANDLE, 'syntax-highlighting-code-block' );
269
270 $data = [
271 'name' => $block->name,
272 'attributes' => $block->attributes,
273 'deprecated' => [
274 'selectedLines' => [
275 'type' => 'string',
276 'default' => '',
277 ],
278 'showLines' => [
279 'type' => 'boolean',
280 'default' => false,
281 ],
282 ],
283 ];
284 wp_add_inline_script(
285 EDITOR_SCRIPT_HANDLE,
286 sprintf( 'const syntaxHighlightingCodeBlockType = %s;', wp_json_encode( $data ) ),
287 'before'
288 );
289
290 wp_add_inline_script(
291 EDITOR_SCRIPT_HANDLE,
292 sprintf( 'const syntaxHighlightingCodeBlockLanguageNames = %s;', wp_json_encode( get_language_names() ) ),
293 'before'
294 );
295 }
296
297 /**
298 * Get highlight theme name.
299 *
300 * @return string Theme name or empty string if disabled.
301 */
302 function get_theme_name(): string {
303 if ( has_filter( 'syntax_highlighting_code_block_style' ) ) {
304 /**
305 * Filters the style used for the code syntax block.
306 *
307 * The string returned must correspond to the filenames found at <https://github.com/scrivo/highlight.php/tree/master/styles>,
308 * minus the file extension.
309 *
310 * This filter takes precedence over any settings set in the database as an option. Additionally, if this filter
311 * is provided, then a theme selector will not be provided in Customizer.
312 *
313 * @since 1.0.0
314 * @param string $style Style.
315 */
316 $style = apply_filters( 'syntax_highlighting_code_block_style', DEFAULT_THEME );
317 if ( ! is_string( $style ) ) {
318 $style = DEFAULT_THEME;
319 }
320 } else {
321 $style = get_plugin_options()['theme_name'];
322 }
323 return is_string( $style ) ? $style : '';
324 }
325
326 /**
327 * Register styles for the frontend.
328 *
329 * @noinspection PhpUnused -- See https://youtrack.jetbrains.com/issue/WI-22217/Extend-possible-linking-between-function-and-callback-using-different-constants-NAMESPACE-CLASS-and-class
330 */
331 function register_styles(): void {
332 if ( ! is_styling_enabled() || is_admin() ) { // TODO: The same styling should be used in the admin.
333 return;
334 }
335 $styles = wp_styles();
336 $theme = get_theme_name();
337
338 $theme_style_path = sprintf(
339 '%s/styles/%s.css',
340 get_highlight_php_vendor_path(),
341 0 === validate_file( $theme ) ? $theme : DEFAULT_THEME
342 );
343 $styles->add(
344 THEME_STYLE_HANDLE,
345 plugins_url( $theme_style_path, PLUGIN_MAIN_FILE ),
346 [],
347 SCRIPT_DEBUG
348 ? (string) filemtime( plugin_dir_path( PLUGIN_MAIN_FILE ) . $theme_style_path )
349 : PLUGIN_VERSION
350 );
351
352 // TODO: Ideally this would be minified.
353 $block_style_name = 'style.css';
354 $block_style_path = plugin_dir_path( PLUGIN_MAIN_FILE ) . $block_style_name;
355 $styles->add(
356 BLOCK_STYLE_HANDLE,
357 plugins_url( $block_style_name, PLUGIN_MAIN_FILE ),
358 [],
359 SCRIPT_DEBUG
360 ? (string) filemtime( $block_style_path )
361 : PLUGIN_VERSION
362 );
363 wp_style_add_data( BLOCK_STYLE_HANDLE, 'path', $block_style_path );
364
365 if ( has_filter( 'syntax_highlighted_line_background_color' ) ) {
366 $default_line_color = get_default_line_background_color( DEFAULT_THEME );
367 /**
368 * Filters the background color of a highlighted line.
369 *
370 * This filter takes precedence over any settings set in the database as an option. Additionally, if this filter
371 * is provided, then a color selector will not be provided in Customizer.
372 *
373 * @param string $rgb_color An RGB hexadecimal (with the #) to be used as the background color of a highlighted line.
374 *
375 * @since 1.1.5
376 */
377 $line_color = apply_filters( 'syntax_highlighted_line_background_color', $default_line_color );
378 if ( ! is_string( $line_color ) ) {
379 $line_color = $default_line_color;
380 }
381 } else {
382 $line_color = get_plugin_options()['highlighted_line_background_color'];
383 }
384 wp_add_inline_style(
385 BLOCK_STYLE_HANDLE,
386 /* language=CSS */
387 ".hljs > mark.shcb-loc { background-color: $line_color; }"
388 );
389 }
390
391 /**
392 * Determines whether styling is enabled.
393 *
394 * @return bool Styling.
395 */
396 function is_styling_enabled(): bool {
397 /**
398 * Filters whether the Syntax-highlighting Code Block's default styling is enabled.
399 *
400 * @param bool $enabled Default styling enabled.
401 */
402 return (bool) apply_filters( 'syntax_highlighting_code_block_styling', true );
403 }
404
405 /**
406 * Language names.
407 *
408 * @return array<string, string> Mapping slug to name.
409 */
410 function get_language_names(): array {
411 return require PLUGIN_DIR . '/language-names.php';
412 }
413
414 /**
415 * Inject class names and styles into the
416 *
417 * @param string $pre_start_tag The `<pre>` start tag.
418 * @param string $code_start_tag The `<code>` start tag.
419 * @param array{
420 * language: string,
421 * highlightedLines: string,
422 * showLineNumbers: bool,
423 * wrapLines: bool
424 * } $attributes Attributes.
425 * @param string $content Content.
426 * @return string Injected markup.
427 */
428 function inject_markup( string $pre_start_tag, string $code_start_tag, array $attributes, string $content ): string {
429 $added_classes = 'hljs';
430
431 if ( $attributes['language'] ) {
432 $added_classes .= " language-{$attributes['language']}";
433 }
434
435 if ( $attributes['showLineNumbers'] || $attributes['highlightedLines'] ) {
436 $added_classes .= ' shcb-code-table';
437 }
438
439 if ( $attributes['showLineNumbers'] ) {
440 $added_classes .= ' shcb-line-numbers';
441 }
442
443 if ( $attributes['wrapLines'] ) {
444 $added_classes .= ' shcb-wrap-lines';
445 }
446
447 // @todo Update this to use WP_HTML_Tag_Processor.
448 $code_start_tag = (string) preg_replace(
449 '/(<code[^>]*\sclass=")/',
450 '$1' . esc_attr( $added_classes ) . ' ',
451 $code_start_tag,
452 1,
453 $count
454 );
455 if ( 0 === $count ) {
456 $code_start_tag = (string) preg_replace(
457 '/(?<=<code\b)/',
458 sprintf( ' class="%s"', esc_attr( $added_classes ) ),
459 $code_start_tag,
460 1
461 );
462 }
463
464 $end_tags = '</code></span>';
465
466 // Add language label if one was detected and if we're not in a feed.
467 if ( ! is_feed() && ! empty( $attributes['language'] ) ) {
468 $language_names = get_language_names();
469 $language_name = $language_names[ $attributes['language'] ] ?? $attributes['language'];
470
471 $element_id = wp_unique_id( 'shcb-language-' );
472
473 // Add the language info to markup with semantic label.
474 $end_tags .= sprintf(
475 '<small class="shcb-language" id="%s"><span class="shcb-language__label">%s</span> <span class="shcb-language__name">%s</span> <span class="shcb-language__paren">(</span><span class="shcb-language__slug">%s</span><span class="shcb-language__paren">)</span></small>',
476 esc_attr( $element_id ),
477 esc_html__( 'Code language:', 'syntax-highlighting-code-block' ),
478 esc_html( $language_name ),
479 esc_html( $attributes['language'] )
480 );
481
482 // Also include the language in data attributes on the root <pre> element for maximum styling flexibility.
483 $pre_start_tag = str_replace(
484 '>',
485 sprintf(
486 ' aria-describedby="%s" data-shcb-language-name="%s" data-shcb-language-slug="%s">',
487 esc_attr( $element_id ),
488 esc_attr( $language_name ),
489 esc_attr( $attributes['language'] )
490 ),
491 $pre_start_tag
492 );
493 }
494 $end_tags .= '</pre>';
495
496 return $pre_start_tag . '<span>' . $code_start_tag . escape( $content ) . $end_tags;
497 }
498
499 /**
500 * Escape content.
501 *
502 * In order to prevent WordPress the_content filters from rendering embeds/shortcodes, it's important
503 * to re-escape the content in the same way as the editor is doing with the Code block's save function.
504 * Note this does not need to escape ampersands because they will already be escaped by highlight.php.
505 * Also, escaping of ampersands was removed in <https://github.com/WordPress/gutenberg/commit/f5c32f8>
506 * once HTML editing of Code blocks was implemented.
507 *
508 * @link <https://github.com/westonruter/syntax-highlighting-code-block/issues/668>
509 * @link <https://github.com/WordPress/gutenberg/blob/32b4481/packages/block-library/src/code/utils.js>
510 * @link <https://github.com/WordPress/gutenberg/pull/13996>
511 *
512 * @param string $content Highlighted content.
513 * @return string Escaped content.
514 */
515 function escape( string $content ): string {
516 // See escapeOpeningSquareBrackets: <https://github.com/WordPress/gutenberg/blob/32b4481/packages/block-library/src/code/utils.js#L19-L34>.
517 $content = str_replace( '[', '&#91;', $content );
518
519 // See escapeProtocolInIsolatedUrls: <https://github.com/WordPress/gutenberg/blob/32b4481/packages/block-library/src/code/utils.js#L36-L55>.
520 return (string) preg_replace( '/^(\s*https?:)\/\/([^\s<>"]+\s*)$/m', '$1&#47;&#47;$2', $content );
521 }
522
523 /**
524 * Get transient key.
525 *
526 * Returns null if key cannot be computed.
527 *
528 * @param string $content Content.
529 * @param array{
530 * language: string,
531 * highlightedLines: string,
532 * showLineNumbers: bool,
533 * wrapLines: bool
534 * } $attributes Attributes.
535 * @param bool $is_feed Is feed.
536 * @param string[] $auto_detect_languages Auto-detect languages.
537 *
538 * @return string|null Transient key.
539 */
540 function get_transient_key( string $content, array $attributes, bool $is_feed, array $auto_detect_languages ): ?string {
541 $hash_input = wp_json_encode(
542 [
543 'content' => $content,
544 'attributes' => $attributes,
545 'is_feed' => $is_feed, // TODO: This is obsolete.
546 'auto_detect_languages' => $auto_detect_languages,
547 'version' => PLUGIN_VERSION,
548 ]
549 );
550 if ( ! is_string( $hash_input ) ) {
551 return null;
552 }
553 return 'shcb-' . md5( $hash_input );
554 }
555
556 /**
557 * Render code block.
558 *
559 * @param array{
560 * language: string,
561 * highlightedLines: string,
562 * showLineNumbers: bool,
563 * wrapLines: bool,
564 * selectedLines?: string,
565 * showLines?: bool
566 * } $attributes Attributes.
567 * @param string $content Content.
568 * @return string Highlighted content.
569 */
570 function render_block( array $attributes, string $content ): string {
571 $pattern = '(?P<pre_start_tag><pre\b[^>]*?>)(?P<code_start_tag><code\b[^>]*?>)';
572 $pattern .= '(?P<content>.*)';
573 $pattern .= '</code></pre>';
574
575 if ( ! preg_match( '#^\s*' . $pattern . '\s*$#s', $content, $matches ) ) {
576 return $content;
577 }
578
579 // Migrate legacy attribute names.
580 if ( isset( $attributes['selectedLines'] ) ) {
581 $attributes['highlightedLines'] = $attributes['selectedLines'];
582 unset( $attributes['selectedLines'] );
583 }
584 if ( isset( $attributes['showLines'] ) ) {
585 $attributes['showLineNumbers'] = $attributes['showLines'];
586 unset( $attributes['showLines'] );
587 }
588
589 /**
590 * Filters the list of languages that are used for auto-detection.
591 *
592 * @param string[] $auto_detect_language Auto-detect languages.
593 */
594 $auto_detect_languages = apply_filters( 'syntax_highlighting_code_block_auto_detect_languages', [] );
595 if ( ! is_array( $auto_detect_languages ) ) {
596 $auto_detect_languages = [];
597 }
598 $auto_detect_languages = array_filter( $auto_detect_languages, 'is_string' );
599
600 // Use the previously-highlighted content if cached.
601 $transient_key = ! DEVELOPMENT_MODE ? get_transient_key( $matches['content'], $attributes, is_feed(), $auto_detect_languages ) : null;
602 $highlighted = $transient_key ? get_transient( $transient_key ) : null;
603 if (
604 is_array( $highlighted )
605 &&
606 isset( $highlighted['content'] ) && is_string( $highlighted['content'] )
607 &&
608 is_array( $highlighted['attributes'] )
609 &&
610 isset( $highlighted['attributes']['language'] ) && is_string( $highlighted['attributes']['language'] )
611 &&
612 isset( $highlighted['attributes']['highlightedLines'] ) && is_string( $highlighted['attributes']['highlightedLines'] )
613 &&
614 isset( $highlighted['attributes']['showLineNumbers'] ) && is_bool( $highlighted['attributes']['showLineNumbers'] )
615 &&
616 isset( $highlighted['attributes']['wrapLines'] ) && is_bool( $highlighted['attributes']['wrapLines'] )
617 ) {
618 return inject_markup( $matches['pre_start_tag'], $matches['code_start_tag'], $highlighted['attributes'], $highlighted['content'] );
619 }
620
621 try {
622 if ( ! class_exists( '\Highlight\Autoloader' ) ) {
623 require_once PLUGIN_DIR . '/' . get_highlight_php_vendor_path() . '/Highlight/Autoloader.php';
624 spl_autoload_register( 'Highlight\Autoloader::load' );
625 }
626
627 $highlighter = new Highlighter();
628 if ( ! empty( $auto_detect_languages ) ) {
629 $highlighter->setAutodetectLanguages( $auto_detect_languages );
630 }
631
632 $language = $attributes['language'];
633
634 // As of Gutenberg 17.1, line breaks in Code blocks are serialized as <br> tags whereas previously they were newlines.
635 $content = str_replace( '<br>', "\n", $matches['content'] );
636
637 // Note that the decoding here is reversed later in the escape() function.
638 // @todo Now that Code blocks may have markup (e.g. bolding, italics, and hyperlinks), these need to be removed and then restored after highlighting is completed.
639 $content = html_entity_decode( $content, ENT_QUOTES );
640
641 // Convert from Prism.js languages names.
642 if ( 'clike' === $language ) {
643 $language = 'cpp';
644 } elseif ( 'git' === $language ) {
645 $language = 'diff'; // Best match.
646 } elseif ( 'markup' === $language ) {
647 $language = 'xml';
648 }
649
650 if ( $language ) {
651 $r = $highlighter->highlight( $language, $content );
652 } else {
653 $r = $highlighter->highlightAuto( $content );
654 }
655 $attributes['language'] = $r->language;
656
657 $content = $r->value;
658 if ( $attributes['showLineNumbers'] || $attributes['highlightedLines'] ) {
659 require_highlight_php_functions();
660
661 $highlighted_lines = parse_highlighted_lines( $attributes['highlightedLines'] );
662 $lines = split_code_into_array( $content );
663 $content = '';
664
665 // We need to wrap the line of code twice in order to let out `white-space: pre` CSS setting to be respected
666 // by our `table-row`.
667 foreach ( $lines as $i => $line ) {
668 $tag_name = in_array( $i, $highlighted_lines, true ) ? 'mark' : 'span';
669 $content .= "<$tag_name class='shcb-loc'><span>$line\n</span></$tag_name>";
670 }
671 }
672
673 if ( $transient_key ) {
674 set_transient( $transient_key, compact( 'content', 'attributes' ), MONTH_IN_SECONDS );
675 }
676
677 return inject_markup( $matches['pre_start_tag'], $matches['code_start_tag'], $attributes, $content );
678 } catch ( Exception $e ) {
679 return sprintf(
680 '<!-- %s(%s): %s -->%s',
681 get_class( $e ),
682 $e->getCode(),
683 str_replace( '--', '', $e->getMessage() ),
684 $content
685 );
686 }
687 }
688
689 /**
690 * Split code into an array.
691 *
692 * @param string $code Code to split.
693 * @return string[] Lines.
694 * @throws Exception If an error occurred in splitting up by lines.
695 */
696 function split_code_into_array( string $code ): array {
697 $lines = splitCodeIntoArray( $code );
698 if ( ! is_array( $lines ) ) {
699 throw new Exception( 'Unable to split code into array.' );
700 }
701 return $lines;
702 }
703
704 /**
705 * Parse the highlighted line syntax from the front-end and return an array of highlighted line numbers.
706 *
707 * @param string $highlighted_lines The highlighted line syntax.
708 * @return int[]
709 */
710 function parse_highlighted_lines( string $highlighted_lines ): array {
711 $highlighted_line_numbers = [];
712
713 if ( ! $highlighted_lines || empty( trim( $highlighted_lines ) ) ) {
714 return $highlighted_line_numbers;
715 }
716
717 $ranges = explode( ',', (string) preg_replace( '/\s/', '', $highlighted_lines ) );
718
719 foreach ( $ranges as $chunk ) {
720 if ( strpos( $chunk, '-' ) !== false ) {
721 $range = explode( '-', $chunk );
722
723 if ( count( $range ) === 2 ) {
724 for ( $i = (int) $range[0]; $i <= (int) $range[1]; $i++ ) {
725 $highlighted_line_numbers[] = $i - 1;
726 }
727 }
728 } else {
729 $highlighted_line_numbers[] = (int) $chunk - 1;
730 }
731 }
732
733 return $highlighted_line_numbers;
734 }
735
736 /**
737 * Validate the given stylesheet name against available stylesheets.
738 *
739 * @param WP_Error $validity Validator object.
740 * @param string $input Incoming theme name.
741 * @return WP_Error Amended errors.
742 */
743 function validate_theme_name( WP_Error $validity, string $input ): WP_Error {
744 require_highlight_php_functions();
745
746 $themes = getAvailableStyleSheets();
747
748 if ( ! in_array( $input, $themes, true ) ) {
749 $validity->add( 'invalid_theme', __( 'Unrecognized theme', 'syntax-highlighting-code-block' ) );
750 }
751
752 return $validity;
753 }
754
755 /**
756 * Add plugin settings to Customizer.
757 *
758 * @param WP_Customize_Manager $wp_customize The Customizer object.
759 */
760 function customize_register( WP_Customize_Manager $wp_customize ): void {
761 if ( has_filter( 'syntax_highlighting_code_block_style' ) && has_filter( 'syntax_highlighted_line_background_color' ) ) {
762 return;
763 }
764
765 if ( ! is_styling_enabled() ) {
766 return;
767 }
768
769 require_highlight_php_functions();
770
771 $theme_name = get_theme_name();
772
773 if ( ! has_filter( 'syntax_highlighting_code_block_style' ) ) {
774 $themes = getAvailableStyleSheets();
775 sort( $themes );
776 $choices = array_combine( $themes, $themes );
777
778 $setting = $wp_customize->add_setting(
779 'syntax_highlighting[theme_name]',
780 [
781 'type' => 'option',
782 'default' => DEFAULT_THEME,
783 'validate_callback' => __NAMESPACE__ . '\validate_theme_name',
784 ]
785 );
786
787 // Obtain the working theme name in the changeset.
788 /**
789 * Theme name sanitized by Customizer setting callback & default
790 *
791 * @var string $theme_name
792 */
793 $theme_name = $setting->post_value( $theme_name );
794
795 $wp_customize->add_control(
796 'syntax_highlighting[theme_name]',
797 [
798 'type' => 'select',
799 'section' => 'colors',
800 'label' => __( 'Syntax Highlighting Theme', 'syntax-highlighting-code-block' ),
801 'description' => __( 'Preview the theme by navigating to a page with a Code block to see the different themes in action.', 'syntax-highlighting-code-block' ),
802 'choices' => $choices,
803 ]
804 );
805 }
806
807 if ( ! has_filter( 'syntax_highlighted_line_background_color' ) && $theme_name ) {
808 $default_color = strtolower( get_default_line_background_color( $theme_name ) );
809 $wp_customize->add_setting(
810 'syntax_highlighting[highlighted_line_background_color]',
811 [
812 'type' => 'option',
813 'default' => $default_color,
814 'sanitize_callback' => 'sanitize_hex_color',
815 ]
816 );
817 $wp_customize->add_control(
818 new WP_Customize_Color_Control(
819 $wp_customize,
820 'syntax_highlighting[highlighted_line_background_color]',
821 [
822 'section' => 'colors',
823 'setting' => 'syntax_highlighting[highlighted_line_background_color]',
824 'label' => __( 'Highlighted Line Color', 'syntax-highlighting-code-block' ),
825 'description' => __( 'The background color of a highlighted line in a Code block.', 'syntax-highlighting-code-block' ),
826 ]
827 )
828 );
829
830 // Add the script to synchronize the default highlighting line color with the selected theme.
831 if ( ! has_filter( 'syntax_highlighting_code_block_style' ) ) {
832 add_action( 'customize_controls_enqueue_scripts', __NAMESPACE__ . '\enqueue_customize_scripts' );
833 }
834 }
835 }
836
837 /**
838 * Enqueue scripts for Customizer.
839 *
840 * @noinspection PhpUnused -- See https://youtrack.jetbrains.com/issue/WI-22217/Extend-possible-linking-between-function-and-callback-using-different-constants-NAMESPACE-CLASS-and-class
841 */
842 function enqueue_customize_scripts(): void {
843 $script_handle = 'syntax-highlighting-code-block-customize-controls';
844 $script_path = '/build/customize-controls.js';
845 $script_asset = require PLUGIN_DIR . '/build/customize-controls.asset.php';
846
847 wp_enqueue_script(
848 $script_handle,
849 plugins_url( $script_path, PLUGIN_MAIN_FILE ),
850 array_merge( [ 'customize-controls' ], $script_asset['dependencies'] ),
851 $script_asset['version'],
852 true
853 );
854 }
855
856 /**
857 * Register REST endpoint.
858 *
859 * @noinspection PhpUnused -- See https://youtrack.jetbrains.com/issue/WI-22217/Extend-possible-linking-between-function-and-callback-using-different-constants-NAMESPACE-CLASS-and-class
860 */
861 function register_rest_endpoint(): void {
862 register_rest_route(
863 REST_API_NAMESPACE,
864 '/highlighted-line-background-color/(?P<theme_name>[^/]+)',
865 [
866 'methods' => WP_REST_Server::READABLE,
867 'permission_callback' => static function () {
868 return current_user_can( 'customize' );
869 },
870 'callback' => static function ( WP_REST_Request $request ) {
871 $theme_name = $request['theme_name'];
872 $validity = validate_theme_name( new WP_Error(), $theme_name );
873 if ( $validity->errors ) {
874 return $validity;
875 }
876 return new WP_REST_Response( get_default_line_background_color( $theme_name ) );
877 },
878 ]
879 );
880 }
881
882 /**
883 * Gets relative path to highlight.php library in vendor directory.
884 *
885 * @return string Relative path.
886 */
887 function get_highlight_php_vendor_path(): string {
888 if ( DEVELOPMENT_MODE && file_exists( PLUGIN_DIR . '/vendor/scrivo/highlight.php' ) ) {
889 return 'vendor/scrivo/highlight.php';
890 } else {
891 return 'vendor/scrivo/highlight-php';
892 }
893 }
894