Prism Syntax Highlighter for WordPress

Most lightweight, yet most configurable and advanced PrismJS WordPress integration plugin. Custom fields display (detached) supported.

Despite two plugins already on wordpress.org I decided to write my own from scratch. The result is a very capable minimalistic plugin with only ~250 lines of PHP.

  • Supports every possible Prism configuration.
  • Does not load prism on every page, only when needed.
  • Supports automatic detection of prism HTML.
  • Supports displaying code from custom fields with [prism] shortcode.
  • Developers may use the prism handle to enqueue the pre-registered script and style.
  • Has admin editor button for quick shortcode template snippet.
  • Has no options page by design.

Whats PRISM?

PRISM is a clandestine mass electronic surveillance data mining program launched in 2007 by the National Security Agency (NSA), with participation from an unknown date by the British equivalent agency, GCHQ. PRISM is a government code name for a data-collection effort known officially by the SIGAD US-984XN. The Prism program collects stored Internet communications based on demands made to Internet companies such as Google Inc. under Section 702 of the FISA Amendments Act of 2008 to turn over any data that match court-approved search terms – damn wrong text, sorry lets start over:

Prism is a is a lightweight, extensible syntax highlighter, written in JavaScript and CSS by code pirate Lea Verou built with modern web standards in mind.

How Does it Work?

  • Show code from custom fields with [prism field=my_custom_field_name language=php] shortcodes.
  • Show code from any URL with [prism url=http://some.file.to/show.css language=css]. For example a Gists or files on Github but remember it has to be the raw URL or you will get the HTML or the entire Github page! This is a way more flexible alternative to prisms file-highlight plugin (data-src) because of this: “Please note that the files are fetched with XMLHttpRequest. This means that if the file is on a different origin, fetching it will fail, unless CORS is enabled on that website.”
  • Write the normal prism HTML inside your post content, this plugin automatically detects when <code class="language- is used in your content, and will load prism only then.

Thanks to Jannik Zschiesche and his Prism Detached plugin for introducing me to this great idea. The plugin seems not to be maintained, and considered the code bloated so I wrote this from scratch.

Why is the ‘Detached’ Idea so Great?

When dealing with code inside the WordPress TinyMCE things can get messy fast. WordPress loves reformat code and things like that. I was never comfortable with it. But if you handle the code inside custom fields you always know WordPress will not touch it, you also can type HTML entities like < directly in there without the need for them being escaped unlike when using the code editor. This is also incredibly helpful when it comes to line numbers since you may combine this with another plugin that lets you display custom meta boxes for custom fields as a code editor. I have tried Advanced Custom Fields with the Code Area Addon but it requires you to predefine languages and fields ids to display a Codemirror box. I am sure there is a even better solution. If you know one, please let me know.

How to configure it?

This is the the great part: You head over to prismjs.com/download.html select what you like included and store the prism.js and prism.css files inside a prism folder (you have to create it) inside your WordPress upload folder.

.../wp-content/uploads/prism/prism.js + .css on single site WP installs + main site for multisite

.../wp-content/uploads/sites/<site_id>/prism/prism.js + .css for sites on a multisite install

This plugin will detect your prism version and load it, if not present it will load its bundled default version.

Where is the Options Page?

There is none by design, please read the above again 😉

Shortcode usage

Shortcode attr. Usage
field id of the custom field to be used. This is needed with exception when data_src is used
post_id optionally take the custom field from another post
url display a file from any URL catched with wp_remote_get()
language will end up as language-xxx class on the <code> element (<pre> when data_src is used)
Shortcode attr. <pre> attributes, same as the shortcode except underscores become dashes
id defaults to same value as field, only set if you need something different
class add any class for example line-numbers to use the line-numbers plugin
data_line data-line for line-highlight plugin
data_line_offset data-line-offset
data_start data-start
data_src data-src for use of the file-highlight plugin
data_manual always data-manual without value no matter what value you set

Examples

Show custom field prism_css_demo with some CSS

[prism field=prism_css_demo language=css]

pre.line-numbers {
	position: relative;
	padding-left: 3.8em;
	counter-reset: linenumber;
}

Line numbers

[prism field=prism_php_demo language=php class=line-numbers]

extract( shortcode_atts( 
	array(
		'field'            => false,
		'post_id'          => false,
		//* <code>
		'language'         => 'none', 
		//* <pre>
		'id'               => false, 
		'class'            => false,
		'data_src'         => false, 
		'data_start'       => false, 
		'data_line'        => false, 
		'data_line_offset' => false
	),
	$atts,
	'prism'
) );

Line highlight (note that I specified a ID since I used the same field I already used above, else I would end up with invalid HTML on this page because it would use the ID 2 times)

[prism field=prism_php_demo language=php id=prism-data-line-demo data_line=2-4,7-10]

extract( shortcode_atts( 
	array(
		'field'            => false,
		'post_id'          => false,
		//* <code>
		'language'         => 'none', 
		//* <pre>
		'id'               => false, 
		'class'            => false,
		'data_src'         => false, 
		'data_start'       => false, 
		'data_line'        => false, 
		'data_line_offset' => false
	),
	$atts,
	'prism'
) );

Show this plugins code from Github. Set id to use line highlighter triggered by URL

[prism url=https://raw.githubusercontent.com/nextgenthemes/prism/master/prism.php language=php id=github-file-demo]

<?php
/**
* @package   Prism Syntax Highlighter for WordPress
* @author    Nicolas Jonas
* @license   GPL-3.0
* @link      http://nextgenthemes.com/plugins/prism
* @copyright Copyright (c) 2015 Nicolas Jonas
*
* @wordpress-plugin
* Plugin Name:       Prism Syntax Highlighter for WordPress
* Plugin URI:        http://nextgenthemes.com/plugins/prism
* Description:       Most minimalistic yet most configurabale Prismjs integration plugin, includes shortcode for custom field content (detached)
* Version:           1.1.1
* Author:            Nicolas Jonas
* Author URI:        https://nextgenthemes.com
* License:           GPL-3.0
* License URI:       https://www.gnu.org/licenses/gpl-3.0.html
* GitHub Plugin URI: https://github.com/nextgenthemes/prism
*
* WordPress-Plugin-Boilerplate: v2.6.1 (Only parts of it)
*/

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
	die( 'NSA spyware installed, thank you!' );
}

add_action( 'plugins_loaded', array( 'Prism', 'get_instance' ) );

class Prism {

	protected static $instance = null;

	const PRISM_VERSION = '2016-10-02';

	private function __construct() {

		add_action( 'wp_enqueue_scripts', array( $this, 'register_styles' ), 0 );
		add_filter( 'mce_css', array( $this, 'plugin_editor_style' ) );
		add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ), 0 );
		add_action( 'wp_enqueue_scripts', array( $this, 'maybe_load_prism' ) );

		add_action( 'admin_enqueue_scripts', array( $this, 'admin_load_prism' ) );

		add_action( 'admin_head',    array( $this, 'print_admin_css' ) );
		add_action( 'media_buttons', array( $this, 'add_media_button' ), 11 );
		add_action( 'admin_footer',  array( $this, 'print_admin_javascript' ) );

		add_shortcode( 'prism', array( $this, 'shortcode' ) );

		add_filter( 'mce_buttons_2', array( $this, 'mce_add_buttons' ) );
		add_filter( 'tiny_mce_before_init', array( $this, 'filter_tiny_mce_before_init' ) );
	}

	public function register_styles() {

		$upload_dir = wp_upload_dir();

		$cssfile_dir = trailingslashit( $upload_dir['basedir'] ) . 'prism/prism.css';
		$cssfile_url = trailingslashit( $upload_dir['baseurl'] ) . 'prism/prism.css';

		if ( is_file( $cssfile_dir ) ) {

			wp_register_style( 'prism', $cssfile_url, array(), filemtime( $cssfile_dir ) );

		} else {

			wp_register_style( 'prism', plugins_url( 'prism.css', __FILE__ ), array(), self::PRISM_VERSION );
		}
	}

	public function plugin_editor_style( $mce_css ){

			$mce_css .= ', ' . plugins_url( 'prism.css', __FILE__ );
			return $mce_css;
	}

	public function register_scripts() {

		$upload_dir = wp_upload_dir();

		$jsfile_dir = trailingslashit( $upload_dir['basedir'] ) . 'prism/prism.js';
		$jsfile_url = trailingslashit( $upload_dir['baseurl'] ) . 'prism/prism.js';

		if ( is_file( $jsfile_dir ) ) {

			wp_register_script( 'prism', $jsfile_url, array(), filemtime( $jsfile_dir ), true );

		} else {

			wp_register_script( 'prism', plugins_url( 'prism.js', __FILE__ ), array(), self::PRISM_VERSION, true );
		}
	}

	public function maybe_load_prism() {

		global $post, $wp_query;

		$post_contents = '';

		if ( is_singular() ) {

			$post_contents = $post->post_content;

		} else {

			$post_ids = wp_list_pluck( $wp_query->posts, 'ID' );

			foreach ( $post_ids as $post_id ) {

				$post_contents .= get_post_field( 'post_content', $post_id );
			}
		}

		if ( strpos( $post_contents, '<code class="language-' ) !== false ) {

			wp_enqueue_style( 'prism' );
			wp_enqueue_script( 'prism' );
		}
	}

	public function admin_load_prism() {

		#wp_enqueue_style( 'prism' );
		$this->register_scripts();
		wp_enqueue_script( 'prism' );
	}

	public function shortcode( $atts, $content = null ) {

		$pairs = array(
			'field'            => false,
			'url'              => false,
			'post_id'          => false,
			//* <code>
			'language'         => 'none',
			//* <pre>
			'id'               => false,
			'class'            => false,
			'data_src'         => false,
			'data_start'       => false,
			'data_line'        => false,
			'data_line_offset' => false,
			'data_manual'      => false,
		);

		$atts = shortcode_atts( $pairs, $atts, 'prism' );

		$pre_attr = array(
			'id'               => ( $atts['id'] ) ? $atts['id'] : $atts['field'],
			'class'            => $atts['class'],
			'data-src'         => esc_url( $atts['data_src'] ),
			'data-start'       => $atts['data_start'],
			'data-line'        => $atts['data_line'],
			'data-line-offset' => $atts['data_line_offset'],
			'data-manual'      => $atts['data_manual'],
		);

		$code_attr = array(
			'class' => 'language-' . $atts['language'],
		);

		if ( $atts['url'] ) {

			if ( false === filter_var( $atts['url'], FILTER_VALIDATE_URL ) ) {

				return sprintf( '<p><strong>Prism Shortcode Error:</strong> URL <code>%s</code> is invalid </p>', esc_html( $atts['url'] ) );
			}

			$response = wp_remote_get( esc_url( $atts['url'] ) );

			if ( is_wp_error( $response ) ) {

				return sprintf( '<p><strong>Prism Shortcode Error:</strong> could not get remote content. WP_Error message:<br>%s</p>', esc_html( $response->get_error_message() ) );

			} elseif( 200 != $response['response']['code'] ) {

				return sprintf( '<p><strong>Prism Shortcode Error:</strong> could not get remote content. HTTP response code %s</p>', esc_html( $response['response']['code'] ) );
			}

			wp_enqueue_style( 'prism' );
			wp_enqueue_script( 'prism' );

			return sprintf(
				'<pre %s><code %s>%s</code></pre>',
				$this->parse_attr( $pre_attr ),
				$this->parse_attr( $code_attr ),
				esc_html( $response['body'] )
			);
		}

		if ( $atts['data_src'] ) {

			wp_enqueue_style( 'prism' );
			wp_enqueue_script( 'prism' );

			$pre_attr['class'] .= " language-{$atts['language']}";

			return sprintf( '<pre %s></pre>', $this->parse_attr( $pre_attr ) );
		}

		if ( ! $atts['field'] ) {

			return '<p><strong>Prism Shortcode Error:</strong> field, url, data_src is missing</p>';
		}

		global $post;

		$from_post = ( $atts['post_id'] ) ? $atts['post_id'] : $post->ID;

		$field_content = get_post_meta( $from_post, $atts['field'], true );

		if ( empty( $field_content ) ) {

			return '<p><strong>Prism Shortcode Error:</strong> Custom field not set or empty</p>';
		}

		wp_enqueue_style( 'prism' );
		wp_enqueue_script( 'prism' );

		return sprintf(
			'<pre %s><code %s>%s</code></pre>',
			$this->parse_attr( $pre_attr ),
			$this->parse_attr( $code_attr ),
			esc_html( $field_content )
		);
	}

	public function parse_attr( $attr = array() ) {

		$out = '';

		foreach ( $attr as $key => $value ) {

			if ( 'data-manual' == $key && false !== $value ) {
				$out .= ' data-manual';
				continue;
			}

			if ( empty( $value ) ) {
				continue;
			}

			$out .= sprintf( ' %s="%s"', esc_html( $key ), esc_attr( $value ) );
		}

		return trim( $out );
	}

	public function add_media_button() {

?>
<a id="prism-shortcode" class="button add_media" title="Prism Shortcode Snippet">
	<span class="wp-media-buttons-icon prism-icon"></span> Prism
</a>
<?php

	}

	public function print_admin_css() {

?>
<style>
#prism-shortcode {
	padding-left: 1px;
}
.prism-icon:before {
	content: "\f499" !important;
}
</style>
<?php

	}

	public function print_admin_javascript() {

?>
<script>
(function ( $ ) {
	"use strict";

	$( '#prism-shortcode' ).click( function( event ) {

		event.preventDefault();

		send_to_editor( '[prism field="" language=""]' );
	} );

}(jQuery));
</script>
<?php

	}

	public static function get_instance() {

		// If the single instance hasn't been set, set it now.
		if ( null == self::$instance ) {
			self::$instance = new self;
		}

		return self::$instance;
	}

	function filter_tiny_mce_before_init( $settings ) {

		$languages = array(
			'Bash',
			'CSS',
			'JavaScript',
			'Markup',
			'PHP',
			'SCSS',
		);

		$style_formats[] = array(
			'title'    => "<code>",
			'inline'   => 'code'
		);

		foreach ( $languages as $lang ) {

			$lang_lowercase = strtolower( $lang );

			$style_formats[] = array(
				'title'    => "$lang <pre>",
				'block'    => 'pre',
				'classes'  => "language-$lang_lowercase",
			);
			$style_formats[] = array(
				'title'    => "$lang <code>",
				'inline'   => 'code',
				'classes'  => "language-$lang_lowercase",
			);
		}

	  $settings['style_formats'] = json_encode( $style_formats );
		$settings['style_formats_merge'] = false;
		#$settings['block_formats'] = 'Paragraph=p;Heading 3=h3;Heading 4=h4;CSS Code=pre';

	  return $settings;
	}

	function mce_add_buttons( $buttons ) {
    array_splice( $buttons, 1, 0, 'styleselect' );
    return $buttons;
	}
}

Link to #github-file-demo.50-55,60 will trigger file-highlight above.

Activate archive scan

If you want to use prism HTML and have a theme that displays the full page content on archives you can define( 'PRISM_ARCHIVE_SCAN', true ); in a mu-plugin or in your themes functions.php and this plugin will scan all current displayed posts for <code class="language- on archive pages and will load prism there if needed. If you use shortcodes to display fields this is not needed.

Make it load in other places/everywhere

This plugin registers the prism script and style with the prism handle, you can use this for your purposes. For example like this:

add_action( 'wp_enqueue_scripts', 'my_prism_loader' );

function my_prism_loader() {
	
	if ( /* your condition */ ) {

		wp_enqueue_style( 'prism' );
		wp_enqueue_script( 'prism' );
	}
}

Limitations

WordPress theme styles might be overwriting prisms styles for the Twentytwelve theme for example I needed to add …

pre > code {
	line-height: inherit !important;
}

… to your CSS or the line highlight will get out of sync. I also had to add !important to the paddings rules for pre. This is already included in the bundled CSS, see the CSS file above.

On archive pages with infinite scroll the prism detection will not work for posts that load up later. If none of the initial displayed posts has <code class="language-"' or the [prism] shortcode in it, prism will not be loaded. You might use your own function and load or on all archive pages if you need this edge case.

Note

  • There is no HTML language in prism, use markup.
  • I am not the author of prism.js, this is the wrong place for any issues … related to prism. Only for this integration plugin.

The NOT to do list

  • No translation planned
  • No more features planned

Comments are closed, but trackbacks and pingbacks are open.