syntax_plugin_code.php - A PHP4 class that implements the * DokuWiki plugin for highlighting code fragments. * *
* Usage:
* <code [language startno |[fh] text |[hs]]>...</code>
*
* Copyright (C) 2006, 2008 M.Watermann, D-10247 Berlin, FRG * All rights reserved * EMail : <support@mwat.de> *
* Note that we're using raw UTF-8 NonBreakable Spaces here. *
* @private * @see _addLines() */ var $_lead = array('', ' ', ' ', ' ', ' ', ' ', ' ', ' '); /** * Section counter for ODT export * * @private * @see render() * @since created 08-Jun-2008 */ var $_odtSect = 0; /** * Prepare the markup to render the DIFF text. * * @param $aText String The DIFF text to markup. * @param $aFormat String The DIFF format used ("u", "c", "n|r", "s"). * @param $aDoc String Reference to the current renderer's * doc property. * @return Boolean TRUE. * @private * @see render() */ function _addDiff(&$aText, &$aFormat, &$aDoc) { // Since we're inside a PRE block we need the leading LFs: $ADD = "\n" . ''; $DEL = "\n" . ''; $HEAD = "\n" . ''; $CLOSE = ''; // Common headers for all formats; // the RegEx needs at least ")#" appended! $DiffHead = '#\n((?:diff\s[^\n]*)|(?:Index:\s[^\n]*)|(?:={60,})' . '|(?:RCS file:\s[^\n]*)|(?:retrieving revision [0-9][^\n]*)'; switch ($aFormat) { case 'u': // unified output $aDoc .= preg_replace( array($DiffHead . '|(?:@@[^\n]*))#', '|\n(\+[^\n]*)|', '|\n(\-[^\n]*)|'), array($HEAD . '\1' . $CLOSE, $ADD . '\1' . $CLOSE, $DEL . '\1' . $CLOSE), $aText); return TRUE; case 'c': // context output $sections = preg_split('|(\n\*{5,})|', preg_replace($DiffHead . ')#', $HEAD . '\1' . $CLOSE, $aText), -1, PREG_SPLIT_DELIM_CAPTURE); $sections[0] = preg_replace( array('|\n(\-{3}[^\n]*)|', '|\n(\*{3}[^\n]*)|'), array($ADD . '\1' . $CLOSE, $DEL . '\1' . $CLOSE), $sections[0]); $c = count($sections); for ($i = 1; $c > $i; ++$i) { $hits = array(); if (preg_match('|^\n(\*{5,})|', $sections[$i], $hits)) { unset($hits[0]); $sections[$i] = $HEAD . $hits[1] . $CLOSE; } else if (preg_match('|^\n(\x2A{3}\s[^\n]*)(.*)|s', $sections[$i], $hits)) { unset($hits[0]); // free mem $parts = preg_split('|\n(\-{3}\s[^\n]*)|', $hits[2], -1, PREG_SPLIT_DELIM_CAPTURE); // $parts[0] == OLD code $parts[0] = preg_replace('|\n([!\-][^\n]*)|', $DEL . '\1' . $CLOSE, $parts[0]); // $parts[1] == head of NEW code $parts[1] = $ADD . $parts[1] . $CLOSE; // $parts[2] == NEW code $parts[2] = preg_replace( array('|\n([!\x2B][^\n]*)|', '|\n(\x2A{3}[^\n]*)|'), array($ADD . '\1' . $CLOSE, $DEL . '\1' . $CLOSE), $parts[2]); if (isset($parts[3])) { // TRUE when handling multi-file patches $parts[3] = preg_replace('|^(\x2D{3}[^\n]*)|', $ADD . '\1' . $CLOSE, $parts[3]); } // if $sections[$i] = $DEL . $hits[1] . $CLOSE . implode('', $parts); } // if // ELSE: leave $sections[$i] as is } // for $aDoc .= implode('', $sections); return TRUE; case 'n': // RCS output // Only added lines are there so we highlight just the // diff indicators while leaving the text alone. $aDoc .= preg_replace( array($DiffHead . ')#', '|\n(d[0-9]+\s+[0-9]+)|', '|\n(a[0-9]+\s+[0-9]+)|'), array($HEAD . '\1' . $CLOSE, $DEL . '\1' . $CLOSE, $ADD . '\1' . $CLOSE), $aText); return TRUE; case 's': // simple output $aDoc .= preg_replace( array($DiffHead . '|((?:[0-9a-z]+(?:,[0-9a-z]+)*)(?:[^\n]*)))#', '|\n(\x26#60;[^\n]*)|', '|\n(\x26#62;[^\n]*)|'), array($HEAD . '\1' . $CLOSE, $DEL . '\1' . $CLOSE, $ADD . '\1' . $CLOSE), $aText); return TRUE; default: // unknown diff format $aDoc .= $aText; // just append any unrecognized text return TRUE; } // switch } // _addDiff() /** * Add the lines of the given $aList to the specified * $aDoc beginning with the given $aStart linenumber. * * @param $aList Array [IN] the list of lines as prepared by * render(), [OUT] FALSE. * @param $aStart Integer The first linenumber to use. * @param $aDoc String Reference to the current renderer's * doc property. * @private * @see render() */ function _addLines(&$aList, $aStart, &$aDoc) { // Since we're dealing with monospaced fonts here the width of each // character (space, NBSP, digit) is the same. Hence the length of // a digits string gives us its width i.e. the number of digits. $i = $aStart + count($aList); // greatest line number $g = strlen("$i"); // width of greatest number while (list($i, $l) = each($aList)) { unset($aList[$i]); // free mem $aDoc .= '' . $this->_lead[$g - strlen("$aStart")] . "$aStart:" . ((($l) && (' ' != $l)) ? " $l\n" : "\n"); ++$aStart; // increment line number } // while $aList = FALSE; // release memory } // _addLines() /** * Internal convenience method to replace HTML special characters. * * @param $aString String [IN] The text to handle; * [OUT] the modified text (i.e. the method's result). * @return String The string with HTML special chars replaced. * @private * @since created 05-Feb-2007 */ function &_entities(&$aString) { $aString = str_replace(array('&', '<', '>'), array('&', '<', '>'), $aString); return $aString; } // _entities() /** * Try to fix some markup error of the GeSHi SHELL highlighting. * ** The GeShi highlighting for type "sh" (i.e. "bash") is, well, * seriously flawed (at least up to version 1.0.7.20 i.e. 2007-07-01). * Especially handling of comments and embedded string as well as * keyword is plain wrong. *
* This internal helper method tries to solve some minor problems by * removing highlight markup embedded in comment markup. * This is, however, by no means a final resolution: GeSHi obviously * keeps a kind of internal state resulting in highlighting markup * spawing (i.e. repeated on) several lines. * Which - if that state is wrong - causes great demage not by * corrupting the data but by confusing the reader with wrong markup. * The easiest way to trigger such a line spawning confusion is to use * solitary doublequotes or singlequotes (apostrophe) in a comment * line ... *
* @param $aMarkup String [IN] The highlight markup as returned by GeSHi; * [OUT] FALSE. * @param $aDoc String Reference to the current renderer's * doc property. * @private * @since created 04-Aug-2007 * @see render() */ function _fixGeSHi_Bash(&$aMarkup, &$aDoc) { $hits = array(); if (defined('GESHI_VERSION') && preg_match('|(\d+)\.(\d+)\.(\d+)\.(\d+)|', GESHI_VERSION, $hits) && ($hits = sprintf('%02u%02u%02u%03u', $hits[1] * 1, $hits[2] * 1, $hits[3] * 1, $hits[4] * 1)) && ('010007020' < $hits)) { // GeSHi v1.0.7.21 has the comments bug fixed $aDoc .= $aMarkup; $aMarkup = FALSE; // release memory return; } // if $lines = explode("\n", $aMarkup); $aMarkup = FALSE; // release memory while (list($i, $l) = each($lines)) { $hits = array(); // GeSHi "bash" module marks up comments with CSS class "re3": if (preg_match('|^((.*))(.*)$|i', $l, $hits)) { if ('#!/bin/' == substr($hits[3], 0, 7)) { $lines[$i] = $hits[2] . strip_tags($hits[3]); } else { $lines[$i] = $hits[1] . strip_tags($hits[3]) . ''; } // if } else if (! preg_match('|^\s*_JSmarkup) { // Markup already added (or not needed) return; } // if $localdir = realpath(dirname(__FILE__)) . '/'; $webdir = DOKU_BASE . 'lib/plugins/code/'; $css = ''; if (file_exists($localdir . 'style.css')) { ob_start(); @include($localdir . 'style.css'); // Remove whitespace from CSS and expand IMG paths: if ($css = preg_replace( array('|\s*/\x2A.*?\x2A/\s*|s', '|\s*([:;\{\},+!])\s*|', '|(?:url\x28\s*)([^/])|', '|^\s*|', '|\s*$|'), array(' ', '\1', 'url(' . $webdir . '\1'), ob_get_contents())) { $css = ''; } // if ob_end_clean(); } // if $js = (file_exists($localdir . 'script.js')) ? '' : ''; if ($this->_JSmarkup = $css . $js) { $aRenderer->doc = $this->_JSmarkup . preg_replace('|\s*\s*
\s*|', '', $aRenderer->doc); //ELSE: Neither CSS nor JS files found. } // if // Set member field to skip tests with next call: $this->_JSmarkup = TRUE; } // _fixJS() /** * RegEx callback to markup spaces in ODT mode. * * @param $aList Array A list of RegEx matches. * @private * @static * @since created 07-Jun-2008 * @see render() */ function _preserveSpaces($aList) { return ($len = strlen($aList[1])) ? '' . "\n"; } // if if ($aStart) { // Split the prepared data into a list of lines: $aText = explode("\n", $aText); // Add the numbered lines to the document: $this->_addLines($aText, $aStart, $aDoc); } else { $aDoc .= $aText; } // if if ($addTags) { $aDoc .= ''; } // if $aText = FALSE; // release memory } // _rawMarkup() /** * RegEx callback to replace SPAN tags in ODT mode. * * @param $aList Array A list of RegEx matches. * @private * @static * @since created 07-Jun-2008 * @see render() */ function _replaceSpan($aList) { return ($aList[3]) ? '
* The returned array holds the following fields: *
* This method is important for correct XHTML nesting. * It returns one of the following values: *
* The $aState parameter gives the type of pattern * which triggered the call to this method: *
* The method checks the given $aFormat to decide how to * handle the specified $aData. * The standard case (i.e. "xhtml") is handled completely * by this implementation, preparing linenumbers and/or head/foot * lines are requested. * For the "odt" format all plugin features (incl. linenumbers * and header/footer lines) are supported by generating the appropriate * ODT/XML markup. * All other formats are passed back to the given $aRenderer * instance for further handling. *
* $aRenderer contains a reference to the renderer object * which is currently in charge of the rendering. * The contents of the given $aData is the return value * of the handle() method. *
* @param $aFormat String The output format to generate. * @param $aRenderer Object A reference to the renderer object. * @param $aData Array The data created/returned by the * handle() method. * @return Boolean TRUE. * @public * @see handle() */ function render($aFormat, &$aRenderer, &$aData) { if (DOKU_LEXER_UNMATCHED != $aData[0]) { return TRUE; } // if if ('xhtml' == $aFormat) { if ($tdiv = (($aData[4]) || ($aData[5]))) { $this->_fixJS($aRenderer); // check for old DokuWiki versions $aRenderer->doc .= '' . $this->_entities($aData[4]) . '
'; $aData[4] = $aData[6] = FALSE; // free mem } // if } // if if ($aData[2]) { // lang was given if ('console' == $aData[2]) { $this->_rawMarkup($this->_entities($aData[1]), $aData[3], $aRenderer->doc, $aData[2]); } else if ('diff' == $aData[2]) { $this->_entities($aData[1]); $aRenderer->doc .= ''; $this->_addDiff($aData[1], $aData[3], $aRenderer->doc); $aRenderer->doc .= ''; } else { $isSH = ('bash' == $aData[2]); $geshi = new GeSHi($aData[1], $aData[2], GESHI_LANG_ROOT); if ($geshi->error()) { // Language not supported by "GeSHi" $geshi = NULL; // release memory $this->_rawMarkup($this->_entities($aData[1]), $aData[3], $aRenderer->doc, 'code'); } else { $aData[1] = FALSE; // free mem $geshi->enable_classes(); $geshi->set_encoding('utf-8'); $geshi->set_header_type(GESHI_HEADER_PRE); $geshi->set_overall_class('code ' . $aData[2]); global $conf; if ($conf['target']['extern']) { $geshi->set_link_target($conf['target']['extern']); } // if if ($aData[3]) { // line numbers requested // Separate PRE tag from parsed data: $aData[1] = explode('>', $geshi->parse_code(), 2); // [1][0] = leading "
doc .= $aData[1][0] . '>'; // Separate trailing PRE tag: $aData[1] = explode('', $aData[1][1], 2); // [1][0] = GeSHi markup // [1][1] = trailing "_fixGeSHi_Bash($aData[1][0], $aData[1][1]); } else { // Set reference to fixed markup to sync with // the "bash" execution path (above): $aData[1][1] =& $aData[1][0]; } // if // Split the parsed data into a list of lines: $aData[2] = explode("\n", $aData[1][1]); $aData[1] = FALSE; // free mem // Add the numbered lines to the document: $this->_addLines($aData[2], $aData[3], $aRenderer->doc); // Close the preformatted section markup: $aRenderer->doc .= ''; } else { // w/o line numbering if ($isSH) { // Separate trailing PRE tag which // sometimes is "forgotten" by GeSHi: $aData[2] = explode('', $geshi->parse_code(), 2); // [1][0] = GeSHi markup // [1][1] = trailing "_fixGeSHi_Bash($aData[2][0], $aRenderer->doc); $aRenderer->doc .= ''; } else { $aRenderer->doc .= $geshi->parse_code(); } // if $geshi = NULL; // release memory } // if } // if } // if } else { $this->_rawMarkup($this->_entities($aData[1]), $aData[3], $aRenderer->doc, 'code'); } // if if ($tdiv) { if ($aData[5]) { //XXX See "_headerToLink()" note above. $aRenderer->doc .= ''; } // if $aRenderer->doc .= '
', $aData[1][1], 2); // [1][0] = GeSHi markup // [1][1] = trailing "_fixGeSHi_Bash($aData[1], $aData[2]); } else { $aData[2] = $aData[1]; } // if $aData[1] = FALSE; // release memory if ($aData[3]) { // line numbers requested // Split the parsed data into a list of lines: $aData[1] = explode("\n", $aData[2]); $aData[2] = FALSE; // release memory // Add the numbered lines to the document: $this->_addLines($aData[1], $aData[3], $aData[0]); } else { // w/o line numbers $aData[0] = $aData[2]; $aData[2] = FALSE; // release memory } // if } // if } // if } else { $this->_rawMarkup($this->_entities($aData[1]), $aData[3], $aData[0], '', FALSE); } // if if ('console' == $aData[2]) { $aRenderer->doc .= '