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