Today, I wrote three new Habari plugins. They're all fairly simple, but they each use some techniques that plugin developers might like to know. So I'm going to take a few posts here in a series to outline how I go through the construction of these plugins.

The first plugin I'll start with is one that will be of particular use here on RedAlt, a code highlighter.

With this plugin, I'd simply like to take a chunk of code that's output inside of "code" tags and print it nicely formatted and colorized so that it's easy to read. With the prettify project on Google Code, neatly licensed under the ASL just like Habari and eventually my plugin, I should be able to accomplish this fairly easily.

Basic Framework

So, let's start out with the basic plugin framework:

'Prettify', 'url' => 'http://habariproject.org/', 'author' => 'Owen Winkler', 'authorurl' => 'http://asymptomatic.net/', 'version' => '1.0', 'description' => 'Displays pretty source code in the site output', 'license' => 'Apache License 2.0', ); } ?>

The above code essentially tells Habari all about this plugin. Without this function, the plugin won't run. I create a directory in user/plugins named "prettify" and save this file as "prettify.plugin.php" for a starting point. The ".plugin." part of the file is what keys Habari that this file is a plugin and is a candidate for activation.

In future releases of Habari, the primary source of the information provided by the info() method will be instead via an xml file that is packaged with the plugin.

Add Dependent Files

Next, we need the plugin to actually do something. I know I'm going to use the CSS and JavaScript from the Prettify project on Google Code, so I know I need to at least add those files to the page output.

How do I add those files to the output? I use another plugin hook:

public function action_template_header() { Stack::add('template_stylesheet', array($this->get_url(true) . 'prettify.css', 'screen')); Stack::add('template_header_javascript', $this->get_url(true) . 'prettify.js', 'prettify', 'jquery'); }

This chunk of code does two very basic things, but may seem a little complicated.

First, the function action_template_header() connects the plugin to the "template_header" hook. This hook executes in the theme when the template executes $theme->header();

The two lines in the function add things to a Habari Stack. A Stack is just a term for a list of things that you are usually going to output. Stacks have a couple of nice features that I'm using in addition to naming the Stack itself. They let you name the item that you're inserting as the third parameter, and they let you name dependencies of that thing in the fourth parameter.

In the case of the css file, I'm just adding the file to the stack and don't care about the name or the order. In the case of the Prettify javascript library file, I'm naming the stack item "prettify" and I'm making sure that it is loaded after any item in the stack named "jquery", which we'll be using later.

When the rest of the theme class executes, the stacks will be appropriately output according to the formatting specified in the theme class.

One handy thing here that's easy to overlook is the $this->get_url() method. In this case, $this is an instance of the plugin, and so the get_url() method is in the base Plugin class. The function returns the URL of the plugin directory, which is really useful to know if you need to link to a file that is distributed with the plugin.

With just this code, the plugin outputs the required javascript and css to support the Prettify Google project.

Implement Highlighting

With the dependent code being output properly, I need to add code to make use of it. I'll have to change the action_template_header() function slightly to make the requisite calls to the prettify javascript library.

I want to make sure that I don't call the prettify code before the whole page loads. I can do this easiest with jquery, which is often used for other features in the theme. I'll add that to the dependencies in the javascript stack, and add the code to make the prettify library colorize my code:

public function action_template_header() { Stack::add('template_stylesheet', array($this->get_url(true) . 'prettify.css', 'screen')); Stack::add('template_header_javascript', Site::get_url('scripts', true) . 'jquery.js', 'jquery'); Stack::add('template_header_javascript', $this->get_url(true) . 'prettify.js', 'prettify', 'jquery'); Stack::add('template_header_javascript', '$(function(){ PR_TAB_WIDTH = 2;prettyPrint()});', 'prettify_inline', array('jquery', 'prettify')); }

My new code chunk is "prettify_inline", which is dependent on both jquery and our previously added "prettify" stack item. Note that the value added to the stack is raw javascript, not a URL to a javascript file like the other values. Habari sorts this out automatically and uses the appropriate tag formatting on output.

The inline code here sets the tab width to 2 (the default is 8) and executes the prettyPrint() command, which finds all of the code blocks using the class "prettyprint" and colorizes them. This is done inside of a jQuery function that executes after the DOM for the page loads.

Tricky Line Numbers and Entities

This might be good as-is, but I would really like to add line-numbers to the output. There are a number of ways to do line numbers. Some people use the ordered list HTML tag, some people use tables. There are drawbacks to both of those. The method I'm currently preferring is using a floated list of line numbers that sits to the left of the lines.

So what I want to do is create a second code element before the code that is being highlighted, and then put the same number of numbers in it as lines in the highlighted code block. I'd prefer not to do this in the raw HTML output.

To do this, I'm going to filter the content of the post, and wherever there is a line break, I'm going to replace that with a br tag with a specific class name. This will let me insert real HTML breaks and count the exact number of breaks in the code with javascript so I can insert line numbers.

Also, when I add content to a post that I intend to syntax-highlight, I want Habari to automatically turn any HTML code inside that block into entities. This will make it easier to paste code that contains HTML tags, since I won't have to convert them into entities myself.

To do this, I will add to the filter some code that turns any code within the prettyprint code tags into its HTML entity counterpart.

Here's the code that does it:

public function filter_post_content_out($content) { $content = preg_replace_callback('%(<\s*code [^="">]*class\s*=\s*(["\'])[^\'"]*prettyprint[^\'"]*\2[^>]*>)(.*?)()%si', array($this, 'content_callback'), $content); return $content; } public function content_callback($matches) { $code = trim($matches[3], "\r\n"); $code = str_replace( "\r\n", "\n", $code); $code = htmlentities($code); $code = str_replace( "\n", "
", $code); return $matches[1] . $code . $matches[4]; }

The filter_post_content_out() method attaches to a plugin hook that executes when the post content is output, typically via echo $post->content_out; using the content_callback() method as the replacement function for preg_replace_callback().

The only really tricky bit here is the regular expression that captures the code blocks, and only if they have the "prettyprint" class assigned to them.

I also need to add the javascript code to count the breaks and output the line numbers, which results in this final version of action_template_header():

public function action_template_header() { Stack::add('template_stylesheet', array($this->get_url(true) . 'prettify.css', 'screen')); Stack::add('template_header_javascript', Site::get_url('scripts', true) . 'jquery.js', 'jquery'); Stack::add('template_header_javascript', $this->get_url(true) . 'prettify.js', 'prettify', 'jquery'); Stack::add('template_header_javascript', '$(function(){ $("code.prettyprint").each(function(){ l=$("br.pretty", this).length+1;oz="";for(z=1;z<=l;z++) oz+="z+" "; $(this).wrap("

").before(""+oz+""); }); PR_TAB_WIDTH = 2;prettyPrint()});', 'prettify_inline', array('jquery', 'prettify')); }

The Final Plugin

So all of that gets basically what I want. I made a few tweaks to the prettify CSS so that it looks nice within the site. It's the CSS that does the expanding of the code on hover in Firefox. Apart from that the two extra files come straight from the Prettify Project of Google, and the last file, the prettify.plugin.php is this one here:

'Prettify', 'url' => 'http://habariproject.org/', 'author' => 'Owen Winkler', 'authorurl' => 'http://asymptomatic.net/', 'version' => '1.0', 'description' => 'Displays pretty source code in the site output', 'license' => 'Apache License 2.0', ); } public function filter_post_content_out($content) { $content = preg_replace_callback('%(<\s*code [^="">]*class\s*=\s*(["\'])[^\'"]*prettyprint[^\'"]*\2[^>]*>)(.*?)()%si', array($this, 'content_callback'), $content); return $content; } public function content_callback($matches) { $code = trim($matches[3]); $code = str_replace( "\r\n", "\n", $code); $code = htmlentities($code); $code = str_replace( "\n", "
", $code); return $matches[1] . $code . $matches[4]; } public function action_template_header() { Stack::add('template_stylesheet', array($this->get_url(true) . 'prettify.css', 'screen')); Stack::add('template_header_javascript', Site::get_url('scripts', true) . 'jquery.js', 'jquery'); Stack::add('template_header_javascript', $this->get_url(true) . 'prettify.js', 'prettify', 'jquery'); Stack::add('template_header_javascript', '$(function(){ $("code.prettyprint").each(function(){ l=$("br.pretty", this).length+1;oz="";for(z=1;z<=l;z++) oz+="z+" "; $(this).wrap("

").before(""+oz+""); }); PR_TAB_WIDTH = 2;prettyPrint()});', 'prettify_inline', array('jquery', 'prettify')); } } ?>

Future Improvements

I'd potentially like to add a couple of things to this plugin, both relating to downloads. I'd like to be able to link a code block to a download, so if you click a link near the code, it would download that code.

It might also be useful to have a button or link that automatically copies a code block to the clipboard. For right now, that would include some Flash (because you can't access the clipboard from javascript), and I'm not really too interested in that right now.

Having the code output expand and contract or enable vertical scrolling on user interaction would probably be a useful addition.

It might also be nice to have some settings that allow the code to link to cross-references. For example, it would be useful if PHP functions and keywords linked to their documentation on the php.net site. It would be even more nice if the code could link to a documentation center of my choosing, although I don't know of a documentation standard or API that would make that easy enough to enable as a web service. Still, it might be worth thinking about.

Comments

There are no comments on this post.