PicoGeSHi.php 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. <?php
  2. /*
  3. * PicoGeSHi
  4. */
  5. class PicoGeSHi extends AbstractPicoPlugin
  6. {
  7. const API_VERSION = 3;
  8. protected $enabled = null;
  9. // only act if this var is set to true
  10. private $yeah = FALSE;
  11. public function onConfigLoaded(array &$config)
  12. {
  13. $this->debug = FALSE;
  14. $this->geshi_class = 'geshi';
  15. $this->geshi_line_numbers = FALSE;
  16. $this->geshi_fancy_line_numbers = 0;
  17. $this->geshi_line_start = 0;
  18. $this->geshi_template = "post";
  19. $this->geshi_enable_ids = FALSE;
  20. $this->geshi_col_enabled = TRUE;
  21. // default CSS template - path is relative to plugin dir (where __FILE__ resides)
  22. $this->css_template = "default.template.css";
  23. // not (currently) configurable
  24. // parsedown prepends this to the language for code block classes, e.g.: <code class="language-php">...
  25. $this->langprefix = "language-";
  26. // colors used for stylesheet
  27. $this->colorname = array('fg_def','fg_alt','bg_def','bg_alt','red_norm','red_bold','green_norm','green_bold','yellow_norm','yellow_bold','blue_norm','blue_bold','magenta_norm','magenta_bold','cyan_norm','cyan_bold');
  28. if($config['debug'] === TRUE) {
  29. $this->debug = TRUE;
  30. ini_set('display_errors', 'On');
  31. error_reporting(E_ALL);
  32. echo "function ". __FUNCTION__ ." in ". __FILE__ .'<br/>';
  33. }
  34. // reading config values or setting fallback config values - importing them to this plugin's context
  35. if (isset($config['geshi']['class'])) $this->geshi_class = $config['geshi']['class'];
  36. if (isset($config['geshi']['line-numbers'])) $this->geshi_line_numbers = $config['geshi']['line-numbers'];
  37. if (isset($config['geshi']['fancy-line-numbers']) && $config['geshi']['fancy-line-numbers'] > 1) $this->geshi_fancy_line_numbers = $config['geshi']['fancy-line-numbers'];
  38. if (isset($config['geshi']['line-start']) && $config['geshi']['line-start'] >= 0) $this->geshi_line_start = $config['geshi']['line-start'];
  39. if (isset($config['geshi']['template']) && $config['geshi']['template'] != "") $this->geshi_template = $config['geshi']['template'];
  40. if (isset($config['geshi']['enable-ids'])) $this->geshi_enable_ids = $config['geshi']['enable-ids'];
  41. // check geshi colors defined in some config .yml
  42. if (isset($config['geshi_col']['enabled'])) $this->geshi_col_enabled = $config['geshi_col']['enabled'];
  43. // - use fallback if desired (geshi_col.enabled is TRUE) but undefined
  44. if ($this->geshi_col_enabled == TRUE)
  45. {
  46. // fallback colors, with a light (dark text on light background) theme in mind
  47. // names from here: www.w3schools.com/cssref/css_colors.asp
  48. $fallback=array('Black','DimGrey','Mocassin','OldLace','DarkRed','FireBrick','DarkGreen','ForestGreen','DarkGoldenRod','DarkOrange','CadetBlue','CornflowerBlue','DarkViolet','DarkOrchid','DarkCyan','DeepSkyBlue');
  49. for($i=0;$i<16;$i++) {
  50. $color = $this->colorname[$i];
  51. if(isset($config['geshi_col'][$color])) $this->$color = $config['geshi_col'][$color];
  52. else $this->$color = $fallback[$i];
  53. }
  54. }
  55. // define default css twig template if not user-defined
  56. if (isset($config['geshi']['css_template'])) $this->css_template = $config['geshi']['css_template'];
  57. }
  58. public function onMetaParsed(array $meta)
  59. {
  60. if($this->debug === TRUE) echo "function ". __FUNCTION__ ." found template \"". $meta['template'] ."\" in ". __FILE__ .'<br/>';
  61. // Only Do Things if we have the correct template
  62. if($meta['template'] === $this->geshi_template || $this->geshi_template === "all" ) $this->yeah = TRUE;
  63. }
  64. public function onContentParsed(&$content)
  65. {
  66. if($this->debug === TRUE) echo "function ". __FUNCTION__ ." in ". __FILE__ .'<br/>';
  67. if($this->yeah === FALSE ) {
  68. if($this->debug === TRUE) echo "We're not doing anything. Bye!".'<br/>';
  69. return;
  70. }
  71. if(trim($content)=="") {
  72. if($this->debug === TRUE) echo "Content is empty. Bye!".'<br/>';
  73. return;
  74. }
  75. $doc = new DOMDocument('1.0', 'utf-8');
  76. // enable user error handling
  77. // stackoverflow.com/questions/8218230/php-domdocument-loadhtml-not-encoding-utf-8-correctly
  78. libxml_use_internal_errors(true);
  79. if(!$doc->loadHTML($content))
  80. {
  81. foreach (libxml_get_errors() as $error) {
  82. $this->content['err'][] = $error;
  83. }
  84. libxml_clear_errors();
  85. return;
  86. }
  87. $doc->encoding="UTF-8"; // someone on the 'net wrote this should be done _after_ loadHTML
  88. $nodes = $doc->getElementsByTagName('code'); // it is possible to put a language class on a single line code element
  89. foreach($nodes as $node)
  90. {
  91. if($this->debug === TRUE) echo "We have at least one 'code' tag...";
  92. $lang = $node->getAttribute('class');
  93. // is there at least one with a language-class?
  94. $len = strlen($this->langprefix);
  95. if(substr($lang,0,$len) === $this->langprefix)
  96. // if yes, we start for real:
  97. {
  98. if($this->debug === TRUE) echo "...with a language:<br/>";
  99. //////////////////////////////////////
  100. require_once 'geshi-1.0/src/geshi.php';
  101. //////////////////////////////////////
  102. $geshi = new GeSHi('dummytext','php'); // so we need to open this only once for the whole page
  103. $geshi->set_encoding('UTF-8');
  104. $geshi->enable_strict_mode(TRUE);
  105. $geshi->enable_keyword_links(false);
  106. if($this->geshi_line_numbers === TRUE) $geshi->enable_line_numbers(GESHI_NORMAL_LINE_NUMBERS);
  107. if($this->geshi_fancy_line_numbers > 1) $geshi->enable_line_numbers(GESHI_FANCY_LINE_NUMBERS,$this->geshi_fancy_line_numbers);
  108. $geshi->start_line_numbers_at($this->geshi_line_start);
  109. if($this->geshi_enable_ids) {
  110. $geshi->enable_ids(TRUE);
  111. $geshi->set_overall_id($this->geshi_class);
  112. }
  113. $geshi->enable_classes();
  114. $geshi->set_header_type(GESHI_HEADER_PRE);
  115. foreach($nodes as $node)
  116. {
  117. $lang = $node->getAttribute('class');
  118. // no "language-" class? No highlighting desired.
  119. $len = strlen($this->langprefix);
  120. if(substr($lang,0,$len) !== $this->langprefix) continue;
  121. $str = $node->nodeValue;
  122. $glang = substr($lang,$len); // remove classprefix, usually "language-"
  123. $glang = preg_replace('/[^[:alnum:]]/','',$glang); // remove dots or curly brackets - everything but letters and digits
  124. switch($glang) {
  125. case 'sh': $glang = 'bash'; break;
  126. case 'patch': $glang = 'diff'; break;
  127. }
  128. if($this->debug === TRUE) echo "--- ".$lang." becomes ".$glang."<br/>";
  129. $geshi->set_source($str);
  130. $geshi->set_language($glang);
  131. $str = $geshi->parse_code();
  132. $code = $doc->createElement('code');
  133. $code->setAttribute("class",$this->geshi_class .' '.$glang.' '.$lang);
  134. $code->setAttribute("id",$this->geshi_class);
  135. $tmpDoc = new DOMDocument();
  136. $tmpDoc->loadHTML($str);
  137. $tmpDoc->encoding="UTF-8";
  138. foreach ($tmpDoc->getElementsByTagName('pre')->item(0)->childNodes as $tmpnode) {
  139. $tmpnode = $doc->importNode($tmpnode, true);
  140. $code->appendChild($tmpnode);
  141. }
  142. $node->parentNode->replaceChild($code,$node);
  143. }
  144. $content = $doc->saveXML($doc->documentElement);
  145. $len = strlen('<html><body>');
  146. if(substr($content,0,$len) === '<html><body>') $content = substr($content,$len);
  147. $len = '-'.strlen('</body></html>');
  148. if(substr($content,$len) === '</body></html>') $content = substr($content,0,$len);
  149. // applying CSS colors
  150. if($this->geshi_col_enabled === TRUE) {
  151. $styles = file_get_contents(dirname(__FILE__).'/'.$this->css_template);
  152. if($styles !== '')
  153. {
  154. $styles = str_replace('%class%',$this->geshi_class,$styles);
  155. for($i=0;$i<16;$i++) {
  156. $color = $this->colorname[$i];
  157. $styles = str_replace('%'.$color.'%',$this->$color,$styles);
  158. }
  159. //// MINIFY ON THE FLY
  160. // Remove comments
  161. $styles = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $styles);
  162. // Remove space after colons
  163. $styles = str_replace(': ', ':', $styles);
  164. // Remove newlines and tabs
  165. $styles = str_replace(array("\r\n", "\r", "\n", "\t"), '', $styles);
  166. // Collapse adjacent spaces into a single space
  167. $styles = preg_replace('!\s+!', ' ',$styles);
  168. // Remove spaces that might still be left where we know they aren't needed
  169. $srch = array('} ', '{ ', '; ' ,', ' ,' }' ,' {' ,' ;' ,' ,' );
  170. $rplc = array('}' , '{' , ';' ,',' ,'}' ,'{' ,';' ,',' );
  171. $styles = str_replace($srch,$rplc,$styles);
  172. $content = '<style>'.$styles.'</style>'.$content;
  173. }
  174. }
  175. }
  176. else {
  177. if($this->debug === TRUE) echo "...but it doesn't have a language.<br/>";
  178. continue;
  179. }
  180. // this was not the real loop, we were only testing if we had at least one, can break now
  181. break;
  182. }
  183. }
  184. }