CMarkdownParser.php 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. <?php
  2. /**
  3. * CMarkdownParser class file.
  4. *
  5. * @author Qiang Xue <qiang.xue@gmail.com>
  6. * @link http://www.yiiframework.com/
  7. * @copyright 2008-2013 Yii Software LLC
  8. * @license http://www.yiiframework.com/license/
  9. */
  10. require_once(Yii::getPathOfAlias('system.vendors.markdown.markdown').'.php');
  11. if(!class_exists('HTMLPurifier_Bootstrap',false))
  12. {
  13. require_once(Yii::getPathOfAlias('system.vendors.htmlpurifier').DIRECTORY_SEPARATOR.'HTMLPurifier.standalone.php');
  14. HTMLPurifier_Bootstrap::registerAutoload();
  15. }
  16. /**
  17. * CMarkdownParser is a wrapper of {@link http://michelf.com/projects/php-markdown/extra/ MarkdownExtra_Parser}.
  18. *
  19. * CMarkdownParser extends MarkdownExtra_Parser by using Text_Highlighter
  20. * to highlight code blocks with specific language syntax.
  21. * In particular, if a code block starts with the following:
  22. * <pre>
  23. * [language]
  24. * </pre>
  25. * The syntax for the specified language will be used to highlight
  26. * code block. The languages supported include (case-insensitive):
  27. * ABAP, CPP, CSS, DIFF, DTD, HTML, JAVA, JAVASCRIPT,
  28. * MYSQL, PERL, PHP, PYTHON, RUBY, SQL, XML
  29. *
  30. * You can also specify options to be passed to the syntax highlighter. For example:
  31. * <pre>
  32. * [php showLineNumbers=1]
  33. * </pre>
  34. * which will show line numbers in each line of the code block.
  35. *
  36. * For details about the standard markdown syntax, please check the following:
  37. * <ul>
  38. * <li>{@link http://daringfireball.net/projects/markdown/syntax official markdown syntax}</li>
  39. * <li>{@link http://michelf.com/projects/php-markdown/extra/ markdown extra syntax}</li>
  40. * </ul>
  41. *
  42. * @property string $defaultCssFile The default CSS file that is used to highlight code blocks.
  43. *
  44. * @author Qiang Xue <qiang.xue@gmail.com>
  45. * @package system.utils
  46. * @since 1.0
  47. */
  48. class CMarkdownParser extends MarkdownExtra_Parser
  49. {
  50. /**
  51. * @var string the css class for the div element containing
  52. * the code block that is highlighted. Defaults to 'hl-code'.
  53. */
  54. public $highlightCssClass='hl-code';
  55. /**
  56. * @var mixed the options to be passed to {@link http://htmlpurifier.org HTML Purifier}.
  57. * This can be a HTMLPurifier_Config object, an array of directives (Namespace.Directive => Value)
  58. * or the filename of an ini file.
  59. * This property is used only when {@link safeTransform} is invoked.
  60. * @see http://htmlpurifier.org/live/configdoc/plain.html
  61. * @since 1.1.4
  62. */
  63. public $purifierOptions=null;
  64. /**
  65. * Transforms the content and purifies the result.
  66. * This method calls the transform() method to convert
  67. * markdown content into HTML content. It then
  68. * uses {@link CHtmlPurifier} to purify the HTML content
  69. * to avoid XSS attacks.
  70. * @param string $content the markdown content
  71. * @return string the purified HTML content
  72. */
  73. public function safeTransform($content)
  74. {
  75. $content=$this->transform($content);
  76. $purifier=new HTMLPurifier($this->purifierOptions);
  77. $purifier->config->set('Cache.SerializerPath',Yii::app()->getRuntimePath());
  78. return $purifier->purify($content);
  79. }
  80. /**
  81. * @return string the default CSS file that is used to highlight code blocks.
  82. */
  83. public function getDefaultCssFile()
  84. {
  85. return Yii::getPathOfAlias('system.vendors.TextHighlighter.highlight').'.css';
  86. }
  87. /**
  88. * Callback function when a code block is matched.
  89. * @param array $matches matches
  90. * @return string the highlighted code block
  91. */
  92. public function _doCodeBlocks_callback($matches)
  93. {
  94. $codeblock = $this->outdent($matches[1]);
  95. if(($codeblock = $this->highlightCodeBlock($codeblock)) !== null)
  96. return "\n\n".$this->hashBlock($codeblock)."\n\n";
  97. else
  98. return parent::_doCodeBlocks_callback($matches);
  99. }
  100. /**
  101. * Callback function when a fenced code block is matched.
  102. * @param array $matches matches
  103. * @return string the highlighted code block
  104. */
  105. public function _doFencedCodeBlocks_callback($matches)
  106. {
  107. return "\n\n".$this->hashBlock($this->highlightCodeBlock($matches[2]))."\n\n";
  108. }
  109. /**
  110. * Highlights the code block.
  111. * @param string $codeblock the code block
  112. * @return string the highlighted code block. Null if the code block does not need to highlighted
  113. */
  114. protected function highlightCodeBlock($codeblock)
  115. {
  116. if(($tag=$this->getHighlightTag($codeblock))!==null && ($highlighter=$this->createHighLighter($tag)))
  117. {
  118. $codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
  119. $tagLen = strpos($codeblock, $tag)+strlen($tag);
  120. $codeblock = ltrim(substr($codeblock, $tagLen));
  121. $output=preg_replace('/<span\s+[^>]*>(\s*)<\/span>/', '\1', $highlighter->highlight($codeblock));
  122. return "<div class=\"{$this->highlightCssClass}\">".$output."</div>";
  123. }
  124. else
  125. return "<pre>".CHtml::encode($codeblock)."</pre>";
  126. }
  127. /**
  128. * Returns the user-entered highlighting options.
  129. * @param string $codeblock code block with highlighting options.
  130. * @return string the user-entered highlighting options. Null if no option is entered.
  131. */
  132. protected function getHighlightTag($codeblock)
  133. {
  134. $str = trim(current(preg_split("/\r|\n/", $codeblock,2)));
  135. if(strlen($str) > 2 && $str[0] === '[' && $str[strlen($str)-1] === ']')
  136. return $str;
  137. }
  138. /**
  139. * Creates a highlighter instance.
  140. * @param string $options the user-entered options
  141. * @return Text_Highlighter the highlighter instance
  142. */
  143. protected function createHighLighter($options)
  144. {
  145. if(!class_exists('Text_Highlighter', false))
  146. {
  147. require_once(Yii::getPathOfAlias('system.vendors.TextHighlighter.Text.Highlighter').'.php');
  148. require_once(Yii::getPathOfAlias('system.vendors.TextHighlighter.Text.Highlighter.Renderer.Html').'.php');
  149. }
  150. $lang = current(preg_split('/\s+/', substr(substr($options,1), 0,-1),2));
  151. $highlighter = Text_Highlighter::factory($lang);
  152. if($highlighter)
  153. $highlighter->setRenderer(new Text_Highlighter_Renderer_Html($this->getHighlightConfig($options)));
  154. return $highlighter;
  155. }
  156. /**
  157. * Generates the config for the highlighter.
  158. * @param string $options user-entered options
  159. * @return array the highlighter config
  160. */
  161. public function getHighlightConfig($options)
  162. {
  163. $config = array('use_language'=>true);
  164. if( $this->getInlineOption('showLineNumbers', $options, false) )
  165. $config['numbers'] = HL_NUMBERS_LI;
  166. $config['tabsize'] = $this->getInlineOption('tabSize', $options, 4);
  167. return $config;
  168. }
  169. /**
  170. * Generates the config for the highlighter.
  171. *
  172. * NOTE: This method is deprecated due to a mistake in the method name.
  173. * Use {@link getHighlightConfig} instead of this.
  174. *
  175. * @param string $options user-entered options
  176. * @return array the highlighter config
  177. */
  178. public function getHiglightConfig($options)
  179. {
  180. return $this->getHighlightConfig($options);
  181. }
  182. /**
  183. * Retrieves the specified configuration.
  184. * @param string $name the configuration name
  185. * @param string $str the user-entered options
  186. * @param mixed $defaultValue default value if the configuration is not present
  187. * @return mixed the configuration value
  188. */
  189. protected function getInlineOption($name, $str, $defaultValue)
  190. {
  191. if(preg_match('/'.$name.'(\s*=\s*(\d+))?/i', $str, $v) && count($v) > 2)
  192. return $v[2];
  193. else
  194. return $defaultValue;
  195. }
  196. }