Plugin Enhancement

My previous modification to the CodeColorer syntax highlighting plugin for Wordpress has been integrated into the official 0.9.8 release. I received a comment request to enhance the modification furthur.

Instead of having a long comma separated list of individual line numbers to be highlighted it would be beneficial if ranges of numbers could be specified. I spent a few hours today playing about with PHP again and I have the following solution.

The lines are highlighted using [cc lang=”html” inline=”true”][cc lang=”php” highlight=”161-180”][/cc] rather than listing individual line numbers. You can specify a list of comma separated line numbers, number ranges or a combination of both.

Note

See the section of modified code below. Since moving to a static site this is not shown with CodeColorer.

  1<?php
  2/*
  3CodeColorer plugin core part
  4https://kpumuk.info/projects/wordpress-plugins/codecolorer
  5*/
  6/*
  7    Copyright 2006 - 2010  Dmytro Shteflyuk <kpumuk@kpumuk.info>
  8
  9    This program is free software; you can redistribute it and/or modify
 10    it under the terms of the GNU General Public License as published by
 11    the Free Software Foundation; either version 2 of the License, or
 12    (at your option) any later version.
 13
 14    This program is distributed in the hope that it will be useful,
 15    but WITHOUT ANY WARRANTY; without even the implied warranty of
 16    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 17    GNU General Public License for more details.
 18
 19    You should have received a copy of the GNU General Public License
 20    along with this program; if not, write to the Free Software
 21    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 22*/
 23
 24class CodeColorer {
 25  var $blocks = array();
 26  var $comments = array();
 27
 28  var $geshiExternal = false;
 29  var $geshiVersion = '1.0.8.6';
 30
 31  var $samplePhpCode = '
 32   [cc_php]
 33   /**
 34    * Comment
 35    */
 36   function hello() {
 37     echo "Hello!";
 38     return null;
 39   }
 40   exit();
 41   [/cc_php]
 42 ';
 43
 44  /** Search content for code tags and replace it */
 45  function BeforeHighlightCodeBlock($content) {
 46    $content = preg_replace('#(\s*)\[cc([^\s\]_]*(?:_[^\s\]]*)?)([^\]]*)\](.*?)\[/cc\2\](\s*)#sie', '$this->PerformHighlightCodeBlock(\'\\4\', \'\\3\', $content, \'\\2\', \'\\1\', \'\\5\');', $content);
 47    $content = preg_replace('#(\s*)\<code (.*?)\>(.*?)\</code>(\s*)#sie', '$this->PerformHighlightCodeBlock(\'\\3\', \'\\2\', $content, \'\', \'\\1\', \'\\4\');', $content);
 48
 49    return $content;
 50  }
 51
 52  function AfterHighlightCodeBlock($content) {
 53    $content = str_replace(array_keys($this->blocks), array_values($this->blocks), $content);
 54
 55    return $content;
 56  }
 57
 58  function BeforeProtectComment($content) {
 59    $content = preg_replace('#(\s*)(\[cc[^\s\]_]*(?:_[^\s\]]*)?[^\]]*\].*?\[/cc\1\])(\s*)#sie', '$this->PerformProtectComment(\'\\2\', $content, \'\\1\', \'\\3\');', $content);
 60    $content = preg_replace('#(\s*)(\<code .*?\>.*?\</code>)(\s*)#sie', '$this->PerformProtectComment(\'\\2\', $content, \'\\1\', \'\\3\');', $content);
 61
 62    return $content;
 63  }
 64
 65  function AfterProtectComment($content) {
 66    $content = str_replace(array_keys($this->comments), array_values($this->comments), $content);
 67    $this->comments = array();
 68
 69    return $content;
 70  }
 71
 72  /**
 73   * Perform code highlightning
 74   */
 75  function PerformHighlightCodeBlock($text, $opts, $content, $suffix = '', $before = '', $after = '') {
 76    // Preprocess source text
 77    $text = str_replace(array("\\"", "\\'"), array (""", "\'"), $text);
 78   $text = preg_replace('/(< \?php)/i', '<?php', $text);
 79   $text = preg_replace('/(?:^(?:\s*[\r\n])+|\s+$)/', '', $text);
 80
 81   // Parse options
 82   $options = CodeColorerOptions::ParseOptions($opts, $suffix);
 83
 84   if ($options['escaped']) {
 85     $text = html_entity_decode($text, ENT_QUOTES);
 86     $text = preg_replace('~&#x0*([0-9a-f]+);~ei', 'chr(hexdec("\\1"))', $text);
 87     $text = preg_replace('~&#0*([0-9]+);~e', 'chr(\\1)', $text);
 88    }
 89
 90    $result = '';
 91    // Check if CodeColorer has been disabled for this particular block
 92    if (!$options['enabled']) {
 93      $result = '<code>' . $text . '';
 94    } else {
 95      // See if we should force a height
 96      $num_lines = count(explode("\n", $text));
 97
 98      $result = $this->PerformHighlightGeshi($text, $options);
 99
100      $result = $this->AddContainer($result, $options, $num_lines);
101    }
102
103    if ($options['inline']) {
104      $blockID = $this->GetBlockID($content, false, '<span>', '</span>');
105    } else {
106      $blockID = $this->GetBlockID($content);
107    }
108    $this->blocks[$blockID] = $result;
109
110    if ($options['inline']) {
111      $result = $before . $blockID . $after;
112    } else {
113      $result = "\n\n$blockID\n\n";
114    }
115
116    return $result;
117  }
118
119  /**
120   * Perform code protecting from mangling by Wordpress (used in Comments)
121   */
122  function PerformProtectComment($text, $content, $before, $after) {
123    $text = str_replace(array("\\"", "\\'"), array (""", "\'"), $text);
124
125   $blockID = $this->GetBlockID($content, true, '', '');
126   $this->comments[$blockID] = $text;
127
128   return $before . $blockID . $after;
129 }
130
131 /**
132  * Perform code highlighting using GESHi engine
133  */
134 function PerformHighlightGeshi($content, $options) {
135   /* Geshi configuration */
136   if (!$this->geshi) {
137     $this->geshi = new GeSHi();
138     $this->geshi->enable_line_numbers(GESHI_NO_LINE_NUMBERS, 1);
139     if (is_feed()) {
140       $this->geshi->set_overall_style('padding:5px;font:normal 12px/1.4em Monaco, Lucida Console, monospace;white-space:nowrap');
141     }
142   }
143
144   $geshi = $this->geshi;
145   $geshi->set_source($content);
146   $geshi->set_language($options['lang']);
147   $geshi->set_overall_class('codecolorer');
148   $geshi->set_tab_width($options['tab_size']);
149   if (!is_feed()) {
150     $geshi->enable_classes($options['theme'] != 'geshi');
151     if ($options['nowrap']) {
152       $geshi->set_overall_style('white-space:nowrap');
153     } else {
154       $geshi->set_overall_style('');
155     }
156   } else {
157     $geshi->enable_classes(false);
158   }
159   if (!is_null($options['strict'])) $geshi->enable_strict_mode($options['strict']);
160   if ($options['no_links']) $geshi->enable_keyword_links(false);
161   /* Modification */
162    if ($options['highlight']){
163       $hlines = explode(',',$options['highlight']);
164       $b = array(); /* Empty array to store processed line numbers*/
165       foreach($hlines as $v) {
166           if (strstr($v,"-")) {
167               $c = explode("-",$v);
168
169               for ($i=$c[0]; $i< =$c[1]; $i++) {
170                   array_push($b,$i);
171               }
172           }else{
173               array_push($b,$v);
174           }
175       }
176       sort($b); /* Sort the array in ascending numerical order */
177       $geshi->highlight_lines_extra($b);
178       $geshi->set_highlight_lines_extra_style('background-color: #ffff66;');
179   }
180    /* End Modification */
181
182
183
184    if ($options['inline']) {
185      $geshi->set_header_type(GESHI_HEADER_NONE);
186    } else {
187      $geshi->set_header_type(GESHI_HEADER_DIV);
188    }
189
190    $result = $geshi->parse_code();
191
192    if ($geshi->error()) {
193      return $geshi->error();
194    }
195
196    if ($options['line_numbers'] && !$options['inline']) {
197      $table = '<table cellspacing="0" cellpadding="0"><tbody><tr><td ';
198      if (is_feed()) {
199        $table .= 'style="padding:5px;text-align:center;color:#888888;background-color:#EEEEEE;border-right: 1px solid #9F9F9F;font: normal 12px/1.4em Monaco, Lucida Console, monospace;"';
200      } else {
201        $table .= 'class="line-numbers"';
202      }
203      $table .= '><div>';
204      for ($i = 0, $count = substr_count($result, '<br />') + 1; $i < $count; $i++) {
205        $table .= ($i + $options['first_line']) . '<br />';
206      }
207      $result = $table . '</div></td><td>' . $result . '</td></tr></tbody></table>';
208    }
209
210    return $result;
211  }
212
213  function AddContainer($html, $options, $num_lines) {
214    $custom_css_class = empty($options['class']) ? '' : ' ' . $options['class'];
215    if ($options['inline']) {
216      $theme = empty($options['inline_theme']) ? 'default' : $options['inline_theme'];
217      $result  = '<code class="codecolorer ' . $options['lang'] . ' ' . $theme . $custom_css_class . '">';
218      $result .= '<span class="' . $options['lang'] . '">' . $html . '</span>';
219      $result .= '</code>';
220    } else {
221      $theme = empty($options['theme']) ? 'default' : $options['theme'];
222      $style = 'style="';
223      if ($options['nowrap']) $style .= 'overflow:auto;white-space:nowrap;';
224      if (is_feed()) $style .= 'border:1px solid #9F9F9F;';
225      $style .= $this->GetDimensionRule('width', is_feed() ? $options['rss_width'] : $options['width']);
226      if($num_lines > $options['lines'] && $options['lines'] > 0) {
227        $style .= $this->GetDimensionRule('height', $options['height']);
228      }
229      $style .= '"';
230
231      $css_class = 'codecolorer-container ' . $options['lang'] . ' ' . $theme . $custom_css_class;
232      if ($options['noborder']) $css_class .= ' codecolorer-noborder';
233      $result = '<div class="' . $css_class . '" ' . $style . '>' . $html . '</div>';
234    }
235    return $result;
236  }
237
238  /**
239   * Generate a block ID that will be replaced at the end (after all that
240   * crazy WP text work!) with the right code
241   */
242  function GetBlockID($content, $comment = false, $before = '<div>', $after = '</div>') {
243    static $num = 0;
244
245    $block = $comment ? 'COMMENT' : 'BLOCK';
246    $before = $before . '::CODECOLORER_' . $block . '_';
247    $after = '::' . $after;
248
249    // Just do a check to make sure the user
250    // hasn't (however unlikely) input block replacements
251    // as legit text
252    do {
253      ++$num;
254      $blockID = $before . $num . $after;
255    } while (strpos($content, $blockID) !== false);
256
257    return $blockID;
258  }
259
260  function GetDimensionRule($dimension, $value) {
261    $rule = '';
262    if (!empty($value)) $rule = "$dimension:$value" . (is_numeric($value) ? 'px;' : ';');
263    return $rule;
264  }
265
266  function ShowWarning($type, $title, $message) {
267    $disable = ' <a href="options-general.php?page=codecolorer.php&amp;disable=' . $type . '">' . __('Close', 'codecolorer') . '</a>';
268    echo '<div id="codecolorer-' . $type . '" class="updated fade"><p><strong>' . $title . "</strong> " . $message . $disable . "</p></div>\n";
269  }
270
271  function ShowGeshiWarning() {
272    if ($this->geshiExternal) {
273      $this->ShowWarning('concurrent', __('CodeColorer has detected a problem.', 'codecolorer'), sprintf(__('We found another plugin based on GeSHi library in your system. CodeColorer will work, but our version of GeSHi contain some patches, so we can\'t guarantee an ideal code highlighting now. Please review your <a href="%1$s">plugins</a>, maybe you don\'t need them all.', 'codecolorer'), "plugins.php"));
274    }
275  }
276
277  function ShowOptionsPage() {
278    $page = $this->GetOptionsPage();
279    $page->Show();
280  }
281
282  function GetOptionsPage() {
283    if (!$this->optionsPage) {
284      if (!class_exists('CodeColorerAdmin')) {
285        $path = dirname(__FILE__);
286        if (!file_exists("$path/codecolorer-admin.php")) return false;
287        require_once("$path/codecolorer-admin.php");
288      }
289      $this->optionsPage = new CodeColorerAdmin($this);
290    }
291    return $this->optionsPage;
292  }
293
294  function GetCodeHighlighted($code) {
295    $content = $this->BeforeHighlightCodeBlock($code);
296    return $this->AfterHighlightCodeBlock($content);
297  }
298
299  function GetSampleCodeHighlighted() {
300    return $this->GetCodeHighlighted($this->samplePhpCode);
301  }
302
303  function &GetInstance() {
304    static $instance = null;
305
306    if (null === $instance) {
307      $path = dirname(__FILE__);
308      if (!class_exists('CodeColorerOptions')) {
309        if (!file_exists("$path/codecolorer-options.php")) return null;
310        require_once("$path/codecolorer-options.php");
311      }
312
313      $instance = new CodeColorer();
314
315      # Maybe GeSHi has been loaded by some another plugin?
316     if (!class_exists('GeSHi')) {
317        if (!file_exists("$path/lib/geshi.php")) return null;
318        require_once("$path/lib/geshi.php");
319      } else {
320        $instance->geshiExternal = true;
321      }
322    }
323
324    return $instance;
325  }
326}