Source code in WordPress
Because I wanted to integrate this Code Snippets section (which was previously a bunch of static pages with a few PHP includes) into WordPress, I needed to figure out how to enter source code into WordPress without it mangling that code with its numerous filters, and how to display it on the site. Apparently, that’s not quite a solved problem, evidenced by the number of plugins available to that effect.
Because plugins tend to either be way too bloated for my purposes or do things in a way I don’t like, I ended up doing it my own way. There may be better ways (in fact, there probably are) but this is what I came up with, and it works well for me.
Entering source code into WordPress
WordPress has all kinds of filters it applies to posts (I refer to posts here, but the same goes for pages and custom post types). Generally, these filters do the right thing, wrapping paragraphs in the right HTML tags, converting smilies to images, converting ugly straight quotes into nice curly quotes, stuff like that. In the case of source code, however, this behavior is highly undesirable.
Those filters can be disabled so they don’t mess with the source code, but then, any accompanying text won’t get the treatment, either. Better is to work around the filters, and that’s what I did. Basically, I replace the source code in the post with a placeholder that I know WordPress won’t mess with (a specially formatted <div> element in this case) before WordPress applies its filters, and then I put the source code back after WordPress has finished applying its filters. That way, WordPress won’t have a chance to touch the source code, with the added benefit that I can also do some converting of my own on the source code, replacing < and & with their HTML entities.
I can now just paste source code as-is into the WordPress post edit screen, without escaping anything. All I have to do is enclose it in [code lang="foo"]...[/code] shortcodes (there are more arguments I can use) and a few functions in my theme’s functions.php file will do the heavy lifting for me.
First, the function that searches for these [code] tags. It gets to run as early as possible, when WordPress hasn’t applied its filters yet:
function hlvCodeShortcodePre($content) {
global $shortcode_tags;
// use WordPress' shortcode regex with only our "code" shortcode
$tmp = $shortcode_tags;
$shortcode_tags = array('code' => null);
$regex = get_shortcode_regex();
$shortcode_tags = $tmp;
return preg_replace_callback('/' . $regex . '/s', 'hlvCodeShortcodeCallback', $content);
}
It temporarily replaces the global shortcodes array with an array containing only our [code] shortcode, then asks WordPress to generate a regular expression, which is in turn used to search for source code blocks. For every found occurrence, it calls the function that actually replaces the source code with a placeholder, among other things:
function hlvCodeShortcodeCallback($matches) {
global $hlvCode;
// extract options & set defaults
extract(shortcode_atts(array(
'lang' => 'plain',
'title' => '',
'start' => 1,
'highlight' => 0,
'collapse' => 0,
'light' => 0
// let WordPress parse the shortcode attributes
), shortcode_parse_atts($matches[3])));
// if highlighting multiple lines, make it look like an Array literal for SyntaxHighlighter
if (strpos($highlight, ',') !== false) {
$highlight = '[' . $highlight . ']';
}
// construct attributes for the <pre> tag
$attribs = 'class="brush:' . $lang . ';collapse:' . $collapse . ';light:' . $light . ';' .
'first-line:' . $start . ';highlight:' . $highlight . '"';
if (!empty($title)) $attribs .= ' title="' . $title . '"';
// escape & and < in the code
$code = str_replace(array('&', '<'), array('&', '<'), $matches[5]); // & first!!!
// save the code ans return a placeholder
$key = count($hlvCode);
$hlvCode[$key] = '<pre ' . $attribs . '>' . $code . '</pre>';
return $matches[1] . '<div>%code=' . $key . '%</div>' . $matches[6];
}
This function first defines some defaults, then uses WordPress again to parse the arguments passed to the shortcode, which will overwrite those defaults. It goes on to construct some HTML attributes out of those arguments that will come in handy later. Then it escapes &’s and <’s in the source code and finally it saves the source code, wrapped in <pre> tags with aforementioned attributes, and returns a placeholder.
After WordPress has finished applying its filters, this next function is called to replace the placeholders with my prepared source code blocks wrapped in <pre> tags.
function hlvCodeShortcodePost($content) {
global $hlvCode;
foreach ($hlvCode as $key => $code) {
// replace placeholder with the corresponding code
$content = str_replace('<div>%code=' . $key . '%</div>', $code, $content);
}
return $content;
}
Needs little more explanation I suppose.
Then, to get the whole thing working, those “pre” and “post” functions have to be hooked into WordPress:
function hlvFilterSetup() {
if (is_singular('code')) {
// 3rd argument is priority, lower runs earlier
add_filter('the_content', 'hlvCodeShortcodePre', 0);
add_filter('the_content', 'hlvCodeShortcodePost', 99);
}
}
add_action('template_redirect', 'hlvFilterSetup');
The “template_redirect” hook is just a convenient point where WordPress knows what it’s supposed to do, but hasn’t started actually doing it yet. The function checks for the necessary criterion to apply the filters (in my case custom posts of type “code”) because I don’t want this to run on all posts and pages. Especially on the home page, it would have to run ten times, once for each blog post, which are very unlikely to contain any source code anyway.
So why not just register a “code” shortcode with WordPress and be done? Because WordPress handles shortcodes after applying its other filters, so in that case, it would already have messed with the source code by the time my shortcode gets handled. It is possible to let WordPress handle all shortcodes earlier on, but that might have other unintended side effects. Best not to mess with core functionality. But I figured using WordPress’ own shortcode parsing would work best, so I employed it on a lower level to get it to run when I need it to.
Displaying the source code
Source code wants to be syntax highlighted. Or more specifically, we humans want it to be. Fortunately, pulling that off was relatively easy, compared to getting it into and out of WordPress in the first place. I decided to use Alex Gorbatchev’s SyntaxHighlighter. It’s a JavaScript tool that runs in the visitor’s browser. It saves my server the strain of doing it, and also saves bandwith, because highlighted source code is by definition much larger than the plain text, thanks to all the HTML elements used to colorize it. I have version 3 running, which has an option to load brushes (SyntaxHighlighter parlor for language specific highlighting instructions) on-demand. In other words, it will only load those brushes it actually needs to render a page.
Remember I wrapped the code in special <pre> tags earlier? Those turn out to look something like this:
<pre class="brush:php;collapse:0;light:0;first-line:1;highlight:0"> <!-- source code --> </pre>
The stuff in the class attribute comes straight from the shortcode and comprises directives telling SyntaxHighlighter how exactly to render that specific block of source code. The result is abundantly visible on this page, provided you’re not reading this through an RSS reader or read-later-service, and you’re not one of these people with JavaScript disabled (in either case you’ll get plain, non-colorized code). It might be worth noting that double-clicking a code block gives you the plain text pre-selected, so all you have to do is hit Ctrl/Cmd+C to copy it.
Hi,
I like the idea of this, but couldn’t get it working. My [code] shortcodes stays visable, does nothing and the tabs are removed from the code. I got the SyntaxHighlighter working (tested with <pre class...).
Can you please help me?
Not without more info. It’s working fine for me, and I don’t really see why it wouldn’t elsewhere.
I’m using the latest WordPress (3.3) added SyntaxHighlighter (3.0.83) in the footer.php with SyntaxHighlighter.autoloader.apply (you can view my footer.php at http://pastebin.com/6eEdVyCL). In my functions.php (you can view it at http://pastebin.com/7Un1SwT5) I pasted your code.
I don’t use any other scripts.
I think I know what’s going wrong. On line 278 of your functions.php file, it checks to see for a custom post type of “code” (
if (is_singular('code')) {) which I set up for my blog (so it doesn’t have to run all that stuff on regular blog posts). You probably have no custom post type of “code” so the filters never run.You could either adjust the condition to something that meets your criteria, or remove the check altogether by commenting out/deleting lines 278 and 282 (but not the ones in between).
Removed line 278 and 282 and now it works, tanks!
Glad I could help.