Markdown.php 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. <?php
  2. /**
  3. * Custom Markdown parser for Kohana documentation.
  4. *
  5. * @package Kohana/Userguide
  6. * @category Base
  7. * @author Kohana Team
  8. * @copyright (c) Kohana Team
  9. * @license https://koseven.ga/LICENSE.md
  10. */
  11. class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser {
  12. /**
  13. * @var string base url for links
  14. */
  15. public static $base_url = '';
  16. /**
  17. * @var string base url for images
  18. */
  19. public static $image_url = '';
  20. /**
  21. * Currently defined heading ids.
  22. * Used to prevent creating multiple headings with same id.
  23. *
  24. * @var array
  25. */
  26. protected $_heading_ids = [];
  27. /**
  28. * @var array the generated table of contents
  29. */
  30. protected static $_toc = [];
  31. /**
  32. * Slightly less terrible way to make it so the TOC only shows up when we
  33. * want it to. set this to true to show the toc.
  34. */
  35. public static $show_toc = FALSE;
  36. /**
  37. * Transform some text using [Kodoc_Markdown]
  38. *
  39. * @see Markdown()
  40. *
  41. * @param string Text to parse
  42. * @return string Transformed text
  43. */
  44. public static function markdown($text)
  45. {
  46. static $instance;
  47. if ($instance === NULL)
  48. {
  49. $instance = new Kodoc_Markdown;
  50. }
  51. return $instance->transform($text);
  52. }
  53. public function __construct()
  54. {
  55. // doImage is 10, add image url just before
  56. $this->span_gamut['doImageURL'] = 9;
  57. // doLink is 20, add base url just before
  58. $this->span_gamut['doBaseURL'] = 19;
  59. // Add API links
  60. $this->span_gamut['doAPI'] = 90;
  61. // Add note spans last
  62. $this->span_gamut['doNotes'] = 100;
  63. // Parse Kohana view inclusions at the very end
  64. $this->document_gamut['doIncludeViews'] = 99;
  65. // Show table of contents for userguide pages
  66. $this->document_gamut['doTOC'] = 100;
  67. // Call parent constructor.
  68. parent::__construct();
  69. }
  70. /**
  71. * Callback for the heading setext style
  72. *
  73. * Heading 1
  74. * =========
  75. *
  76. * @param array Matches from regex call
  77. * @return string Generated html
  78. */
  79. function _doHeaders_callback_setext($matches)
  80. {
  81. if ($matches[3] == '-' AND preg_match('{^- }', $matches[1]))
  82. return $matches[0];
  83. $level = ($matches[3][0] == '=') ? 1 : 2;
  84. $attr = $this->_doHeaders_attr($id =& $matches[2]);
  85. // Only auto-generate id if one doesn't exist
  86. if (empty($attr))
  87. {
  88. $attr = ' id="'.$this->make_heading_id($matches[1]).'"';
  89. }
  90. // Add this header to the page toc
  91. $this->_add_to_toc($level,$matches[1],$this->make_heading_id($matches[1]));
  92. $block = "<h$level$attr>".$this->runSpanGamut($matches[1])."</h$level>";
  93. return "\n".$this->hashBlock($block)."\n\n";
  94. }
  95. /**
  96. * Callback for the heading atx style
  97. *
  98. * # Heading 1
  99. *
  100. * @param array Matches from regex call
  101. * @return string Generated html
  102. */
  103. function _doHeaders_callback_atx($matches)
  104. {
  105. $level = strlen($matches[1]);
  106. $attr = $this->_doHeaders_attr($id =& $matches[3]);
  107. // Only auto-generate id if one doesn't exist
  108. if (empty($attr))
  109. {
  110. $attr = ' id="'.$this->make_heading_id($matches[2]).'"';
  111. }
  112. // Add this header to the page toc
  113. $this->_add_to_toc($level, $matches[2], $this->make_heading_id(empty($matches[3]) ? $matches[2] : $matches[3]));
  114. $block = "<h$level$attr>".$this->runSpanGamut($matches[2])."</h$level>";
  115. return "\n".$this->hashBlock($block)."\n\n";
  116. }
  117. /**
  118. * Makes a heading id from the heading text
  119. * If any heading share the same name then subsequent headings will have an integer appended
  120. *
  121. * @param string The heading text
  122. * @return string ID for the heading
  123. */
  124. function make_heading_id($heading)
  125. {
  126. $id = url::title($heading, '-', TRUE);
  127. if (isset($this->_heading_ids[$id]))
  128. {
  129. $id .= '-';
  130. $count = 0;
  131. while (isset($this->_heading_ids[$id]) AND ++$count)
  132. {
  133. $id .= $count;
  134. }
  135. }
  136. return $id;
  137. }
  138. public function doIncludeViews($text)
  139. {
  140. if (preg_match_all('/{{([^\s{}]++)}}/', $text, $matches, PREG_SET_ORDER))
  141. {
  142. $replace = [];
  143. foreach ($matches as $set)
  144. {
  145. list($search, $view) = $set;
  146. if (Kohana::find_file('views', $view))
  147. {
  148. try
  149. {
  150. $replace[$search] = View::factory($view)->render();
  151. }
  152. catch (Exception $e)
  153. {
  154. /**
  155. * Capture the exception handler output and insert it instead.
  156. *
  157. * NOTE: Is this really the correct way to handle an exception?
  158. */
  159. $response = Kohana_exception::_handler($e);
  160. $replace[$search] = $response->body();
  161. }
  162. }
  163. }
  164. $text = strtr($text, $replace);
  165. }
  166. return $text;
  167. }
  168. /**
  169. * Add the current base url to all local links.
  170. *
  171. * [filesystem](about.filesystem "Optional title")
  172. *
  173. * @param string Span text
  174. * @return string
  175. */
  176. public function doBaseURL($text)
  177. {
  178. // URLs containing "://" are left untouched
  179. return preg_replace('~(?<!!)(\[.+?\]\()(?!\w++://)(?!#)(\S*(?:\s*+".+?")?\))~', '$1'.Kodoc_Markdown::$base_url.'$2', $text);
  180. }
  181. /**
  182. * Add the current base url to all local images.
  183. *
  184. * ![Install Page](img/install.png "Optional title")
  185. *
  186. * @param string Span text
  187. * @return string
  188. */
  189. public function doImageURL($text)
  190. {
  191. // URLs containing "://" are left untouched
  192. return preg_replace('~(!\[.+?\]\()(?!\w++://)(\S*(?:\s*+".+?")?\))~', '$1'.Kodoc_Markdown::$image_url.'$2', $text);
  193. }
  194. /**
  195. * Parses links to the API browser.
  196. *
  197. * [Class_Name], [Class::method] or [Class::$property]
  198. *
  199. * @param string Span text
  200. * @return string
  201. */
  202. public function doAPI($text)
  203. {
  204. return preg_replace_callback('/\['.Kodoc::$regex_class_member.'\]/i', 'Kodoc::link_class_member', $text);
  205. }
  206. /**
  207. * Wrap notes in the applicable markup. Notes can contain single newlines.
  208. *
  209. * [!!] Remember the milk!
  210. *
  211. * @param string Span text
  212. * @return string
  213. */
  214. public function doNotes($text)
  215. {
  216. if ( ! preg_match('/^\[!!\]\s*+(.+?)(?=\n{2,}|$)/s', $text, $match))
  217. {
  218. return $text;
  219. }
  220. return $this->hashBlock('<p class="note">'.$match[1].'</p>');
  221. }
  222. protected function _add_to_toc($level, $name, $id)
  223. {
  224. self::$_toc[] = [
  225. 'level' => $level,
  226. 'name' => $name,
  227. 'id' => $id];
  228. }
  229. public function doTOC($text)
  230. {
  231. // Only add the toc do userguide pages, not api since they already have one
  232. if (self::$show_toc AND Route::name(Request::current()->route()) == "docs/guide")
  233. {
  234. $toc = View::factory('userguide/page-toc')
  235. ->set('array', self::$_toc)
  236. ->render()
  237. ;
  238. if (($offset = strpos($text, '<p>')) !== FALSE)
  239. {
  240. // Insert the page TOC just before the first <p>, which every
  241. // Markdown page should (will?) have.
  242. $text = substr_replace($text, $toc, $offset, 0);
  243. }
  244. }
  245. return $text;
  246. }
  247. } // End Kodoc_Markdown