MarkdownExtra.php 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626
  1. <?php
  2. #
  3. # Markdown Extra - A text-to-HTML conversion tool for web writers
  4. #
  5. # PHP Markdown Extra
  6. # Copyright (c) 2004-2015 Michel Fortin
  7. # <https://michelf.ca/projects/php-markdown/>
  8. #
  9. # Original Markdown
  10. # Copyright (c) 2004-2006 John Gruber
  11. # <https://daringfireball.net/projects/markdown/>
  12. #
  13. namespace Michelf;
  14. #
  15. # Markdown Extra Parser Class
  16. #
  17. class MarkdownExtra extends \Michelf\Markdown {
  18. ### Configuration Variables ###
  19. # Prefix for footnote ids.
  20. public $fn_id_prefix = "";
  21. # Optional title attribute for footnote links and backlinks.
  22. public $fn_link_title = "";
  23. public $fn_backlink_title = "";
  24. # Optional class attribute for footnote links and backlinks.
  25. public $fn_link_class = "footnote-ref";
  26. public $fn_backlink_class = "footnote-backref";
  27. # Content to be displayed within footnote backlinks. The default is '↩';
  28. # the U+FE0E on the end is a Unicode variant selector used to prevent iOS
  29. # from displaying the arrow character as an emoji.
  30. public $fn_backlink_html = '&#8617;&#xFE0E;';
  31. # Class name for table cell alignment (%% replaced left/center/right)
  32. # For instance: 'go-%%' becomes 'go-left' or 'go-right' or 'go-center'
  33. # If empty, the align attribute is used instead of a class name.
  34. public $table_align_class_tmpl = '';
  35. # Optional class prefix for fenced code block.
  36. public $code_class_prefix = "";
  37. # Class attribute for code blocks goes on the `code` tag;
  38. # setting this to true will put attributes on the `pre` tag instead.
  39. public $code_attr_on_pre = false;
  40. # Predefined abbreviations.
  41. public $predef_abbr = array();
  42. ### Parser Implementation ###
  43. public function __construct() {
  44. #
  45. # Constructor function. Initialize the parser object.
  46. #
  47. # Add extra escapable characters before parent constructor
  48. # initialize the table.
  49. $this->escape_chars .= ':|';
  50. # Insert extra document, block, and span transformations.
  51. # Parent constructor will do the sorting.
  52. $this->document_gamut += array(
  53. "doFencedCodeBlocks" => 5,
  54. "stripFootnotes" => 15,
  55. "stripAbbreviations" => 25,
  56. "appendFootnotes" => 50,
  57. );
  58. $this->block_gamut += array(
  59. "doFencedCodeBlocks" => 5,
  60. "doTables" => 15,
  61. "doDefLists" => 45,
  62. );
  63. $this->span_gamut += array(
  64. "doFootnotes" => 5,
  65. "doAbbreviations" => 70,
  66. );
  67. $this->enhanced_ordered_list = true;
  68. parent::__construct();
  69. }
  70. # Extra variables used during extra transformations.
  71. protected $footnotes = array();
  72. protected $footnotes_ordered = array();
  73. protected $footnotes_ref_count = array();
  74. protected $footnotes_numbers = array();
  75. protected $abbr_desciptions = array();
  76. protected $abbr_word_re = '';
  77. # Give the current footnote number.
  78. protected $footnote_counter = 1;
  79. protected function setup() {
  80. #
  81. # Setting up Extra-specific variables.
  82. #
  83. parent::setup();
  84. $this->footnotes = array();
  85. $this->footnotes_ordered = array();
  86. $this->footnotes_ref_count = array();
  87. $this->footnotes_numbers = array();
  88. $this->abbr_desciptions = array();
  89. $this->abbr_word_re = '';
  90. $this->footnote_counter = 1;
  91. foreach ($this->predef_abbr as $abbr_word => $abbr_desc) {
  92. if ($this->abbr_word_re)
  93. $this->abbr_word_re .= '|';
  94. $this->abbr_word_re .= preg_quote($abbr_word);
  95. $this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
  96. }
  97. }
  98. protected function teardown() {
  99. #
  100. # Clearing Extra-specific variables.
  101. #
  102. $this->footnotes = array();
  103. $this->footnotes_ordered = array();
  104. $this->footnotes_ref_count = array();
  105. $this->footnotes_numbers = array();
  106. $this->abbr_desciptions = array();
  107. $this->abbr_word_re = '';
  108. parent::teardown();
  109. }
  110. ### Extra Attribute Parser ###
  111. # Expression to use to catch attributes (includes the braces)
  112. protected $id_class_attr_catch_re = '\{((?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,})[ ]*\}';
  113. # Expression to use when parsing in a context when no capture is desired
  114. protected $id_class_attr_nocatch_re = '\{(?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,}[ ]*\}';
  115. protected function doExtraAttributes($tag_name, $attr, $defaultIdValue = null, $classes = array()) {
  116. #
  117. # Parse attributes caught by the $this->id_class_attr_catch_re expression
  118. # and return the HTML-formatted list of attributes.
  119. #
  120. # Currently supported attributes are .class and #id.
  121. #
  122. # In addition, this method also supports supplying a default Id value,
  123. # which will be used to populate the id attribute in case it was not
  124. # overridden.
  125. if (empty($attr) && !$defaultIdValue && empty($classes)) return "";
  126. # Split on components
  127. preg_match_all('/[#.a-z][-_:a-zA-Z0-9=]+/', $attr, $matches);
  128. $elements = $matches[0];
  129. # handle classes and ids (only first id taken into account)
  130. $attributes = array();
  131. $id = false;
  132. foreach ($elements as $element) {
  133. if ($element{0} == '.') {
  134. $classes[] = substr($element, 1);
  135. } else if ($element{0} == '#') {
  136. if ($id === false) $id = substr($element, 1);
  137. } else if (strpos($element, '=') > 0) {
  138. $parts = explode('=', $element, 2);
  139. $attributes[] = $parts[0] . '="' . $parts[1] . '"';
  140. }
  141. }
  142. if (!$id) $id = $defaultIdValue;
  143. # compose attributes as string
  144. $attr_str = "";
  145. if (!empty($id)) {
  146. $attr_str .= ' id="'.$this->encodeAttribute($id) .'"';
  147. }
  148. if (!empty($classes)) {
  149. $attr_str .= ' class="'. implode(" ", $classes) . '"';
  150. }
  151. if (!$this->no_markup && !empty($attributes)) {
  152. $attr_str .= ' '.implode(" ", $attributes);
  153. }
  154. return $attr_str;
  155. }
  156. protected function stripLinkDefinitions($text) {
  157. #
  158. # Strips link definitions from text, stores the URLs and titles in
  159. # hash references.
  160. #
  161. $less_than_tab = $this->tab_width - 1;
  162. # Link defs are in the form: ^[id]: url "optional title"
  163. $text = preg_replace_callback('{
  164. ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1
  165. [ ]*
  166. \n? # maybe *one* newline
  167. [ ]*
  168. (?:
  169. <(.+?)> # url = $2
  170. |
  171. (\S+?) # url = $3
  172. )
  173. [ ]*
  174. \n? # maybe one newline
  175. [ ]*
  176. (?:
  177. (?<=\s) # lookbehind for whitespace
  178. ["(]
  179. (.*?) # title = $4
  180. [")]
  181. [ ]*
  182. )? # title is optional
  183. (?:[ ]* '.$this->id_class_attr_catch_re.' )? # $5 = extra id & class attr
  184. (?:\n+|\Z)
  185. }xm',
  186. array($this, '_stripLinkDefinitions_callback'),
  187. $text);
  188. return $text;
  189. }
  190. protected function _stripLinkDefinitions_callback($matches) {
  191. $link_id = strtolower($matches[1]);
  192. $url = $matches[2] == '' ? $matches[3] : $matches[2];
  193. $this->urls[$link_id] = $url;
  194. $this->titles[$link_id] =& $matches[4];
  195. $this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]);
  196. return ''; # String that will replace the block
  197. }
  198. ### HTML Block Parser ###
  199. # Tags that are always treated as block tags:
  200. protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption|figure';
  201. # Tags treated as block tags only if the opening tag is alone on its line:
  202. protected $context_block_tags_re = 'script|noscript|style|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video';
  203. # Tags where markdown="1" default to span mode:
  204. protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address';
  205. # Tags which must not have their contents modified, no matter where
  206. # they appear:
  207. protected $clean_tags_re = 'script|style|math|svg';
  208. # Tags that do not need to be closed.
  209. protected $auto_close_tags_re = 'hr|img|param|source|track';
  210. protected function hashHTMLBlocks($text) {
  211. #
  212. # Hashify HTML Blocks and "clean tags".
  213. #
  214. # We only want to do this for block-level HTML tags, such as headers,
  215. # lists, and tables. That's because we still want to wrap <p>s around
  216. # "paragraphs" that are wrapped in non-block-level tags, such as anchors,
  217. # phrase emphasis, and spans. The list of tags we're looking for is
  218. # hard-coded.
  219. #
  220. # This works by calling _HashHTMLBlocks_InMarkdown, which then calls
  221. # _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1"
  222. # attribute is found within a tag, _HashHTMLBlocks_InHTML calls back
  223. # _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag.
  224. # These two functions are calling each other. It's recursive!
  225. #
  226. if ($this->no_markup) return $text;
  227. #
  228. # Call the HTML-in-Markdown hasher.
  229. #
  230. list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text);
  231. return $text;
  232. }
  233. protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0,
  234. $enclosing_tag_re = '', $span = false)
  235. {
  236. #
  237. # Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags.
  238. #
  239. # * $indent is the number of space to be ignored when checking for code
  240. # blocks. This is important because if we don't take the indent into
  241. # account, something like this (which looks right) won't work as expected:
  242. #
  243. # <div>
  244. # <div markdown="1">
  245. # Hello World. <-- Is this a Markdown code block or text?
  246. # </div> <-- Is this a Markdown code block or a real tag?
  247. # <div>
  248. #
  249. # If you don't like this, just don't indent the tag on which
  250. # you apply the markdown="1" attribute.
  251. #
  252. # * If $enclosing_tag_re is not empty, stops at the first unmatched closing
  253. # tag with that name. Nested tags supported.
  254. #
  255. # * If $span is true, text inside must treated as span. So any double
  256. # newline will be replaced by a single newline so that it does not create
  257. # paragraphs.
  258. #
  259. # Returns an array of that form: ( processed text , remaining text )
  260. #
  261. if ($text === '') return array('', '');
  262. # Regex to check for the presense of newlines around a block tag.
  263. $newline_before_re = '/(?:^\n?|\n\n)*$/';
  264. $newline_after_re =
  265. '{
  266. ^ # Start of text following the tag.
  267. (?>[ ]*<!--.*?-->)? # Optional comment.
  268. [ ]*\n # Must be followed by newline.
  269. }xs';
  270. # Regex to match any tag.
  271. $block_tag_re =
  272. '{
  273. ( # $2: Capture whole tag.
  274. </? # Any opening or closing tag.
  275. (?> # Tag name.
  276. '.$this->block_tags_re.' |
  277. '.$this->context_block_tags_re.' |
  278. '.$this->clean_tags_re.' |
  279. (?!\s)'.$enclosing_tag_re.'
  280. )
  281. (?:
  282. (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name.
  283. (?>
  284. ".*?" | # Double quotes (can contain `>`)
  285. \'.*?\' | # Single quotes (can contain `>`)
  286. .+? # Anything but quotes and `>`.
  287. )*?
  288. )?
  289. > # End of tag.
  290. |
  291. <!-- .*? --> # HTML Comment
  292. |
  293. <\?.*?\?> | <%.*?%> # Processing instruction
  294. |
  295. <!\[CDATA\[.*?\]\]> # CData Block
  296. '. ( !$span ? ' # If not in span.
  297. |
  298. # Indented code block
  299. (?: ^[ ]*\n | ^ | \n[ ]*\n )
  300. [ ]{'.($indent+4).'}[^\n]* \n
  301. (?>
  302. (?: [ ]{'.($indent+4).'}[^\n]* | [ ]* ) \n
  303. )*
  304. |
  305. # Fenced code block marker
  306. (?<= ^ | \n )
  307. [ ]{0,'.($indent+3).'}(?:~{3,}|`{3,})
  308. [ ]*
  309. (?: \.?[-_:a-zA-Z0-9]+ )? # standalone class name
  310. [ ]*
  311. (?: '.$this->id_class_attr_nocatch_re.' )? # extra attributes
  312. [ ]*
  313. (?= \n )
  314. ' : '' ). ' # End (if not is span).
  315. |
  316. # Code span marker
  317. # Note, this regex needs to go after backtick fenced
  318. # code blocks but it should also be kept outside of the
  319. # "if not in span" condition adding backticks to the parser
  320. `+
  321. )
  322. }xs';
  323. $depth = 0; # Current depth inside the tag tree.
  324. $parsed = ""; # Parsed text that will be returned.
  325. #
  326. # Loop through every tag until we find the closing tag of the parent
  327. # or loop until reaching the end of text if no parent tag specified.
  328. #
  329. do {
  330. #
  331. # Split the text using the first $tag_match pattern found.
  332. # Text before pattern will be first in the array, text after
  333. # pattern will be at the end, and between will be any catches made
  334. # by the pattern.
  335. #
  336. $parts = preg_split($block_tag_re, $text, 2,
  337. PREG_SPLIT_DELIM_CAPTURE);
  338. # If in Markdown span mode, add a empty-string span-level hash
  339. # after each newline to prevent triggering any block element.
  340. if ($span) {
  341. $void = $this->hashPart("", ':');
  342. $newline = "$void\n";
  343. $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void;
  344. }
  345. $parsed .= $parts[0]; # Text before current tag.
  346. # If end of $text has been reached. Stop loop.
  347. if (count($parts) < 3) {
  348. $text = "";
  349. break;
  350. }
  351. $tag = $parts[1]; # Tag to handle.
  352. $text = $parts[2]; # Remaining text after current tag.
  353. $tag_re = preg_quote($tag); # For use in a regular expression.
  354. #
  355. # Check for: Fenced code block marker.
  356. # Note: need to recheck the whole tag to disambiguate backtick
  357. # fences from code spans
  358. #
  359. if (preg_match('{^\n?([ ]{0,'.($indent+3).'})(~{3,}|`{3,})[ ]*(?:\.?[-_:a-zA-Z0-9]+)?[ ]*(?:'.$this->id_class_attr_nocatch_re.')?[ ]*\n?$}', $tag, $capture)) {
  360. # Fenced code block marker: find matching end marker.
  361. $fence_indent = strlen($capture[1]); # use captured indent in re
  362. $fence_re = $capture[2]; # use captured fence in re
  363. if (preg_match('{^(?>.*\n)*?[ ]{'.($fence_indent).'}'.$fence_re.'[ ]*(?:\n|$)}', $text,
  364. $matches))
  365. {
  366. # End marker found: pass text unchanged until marker.
  367. $parsed .= $tag . $matches[0];
  368. $text = substr($text, strlen($matches[0]));
  369. }
  370. else {
  371. # No end marker: just skip it.
  372. $parsed .= $tag;
  373. }
  374. }
  375. #
  376. # Check for: Indented code block.
  377. #
  378. else if ($tag{0} == "\n" || $tag{0} == " ") {
  379. # Indented code block: pass it unchanged, will be handled
  380. # later.
  381. $parsed .= $tag;
  382. }
  383. #
  384. # Check for: Code span marker
  385. # Note: need to check this after backtick fenced code blocks
  386. #
  387. else if ($tag{0} == "`") {
  388. # Find corresponding end marker.
  389. $tag_re = preg_quote($tag);
  390. if (preg_match('{^(?>.+?|\n(?!\n))*?(?<!`)'.$tag_re.'(?!`)}',
  391. $text, $matches))
  392. {
  393. # End marker found: pass text unchanged until marker.
  394. $parsed .= $tag . $matches[0];
  395. $text = substr($text, strlen($matches[0]));
  396. }
  397. else {
  398. # Unmatched marker: just skip it.
  399. $parsed .= $tag;
  400. }
  401. }
  402. #
  403. # Check for: Opening Block level tag or
  404. # Opening Context Block tag (like ins and del)
  405. # used as a block tag (tag is alone on it's line).
  406. #
  407. else if (preg_match('{^<(?:'.$this->block_tags_re.')\b}', $tag) ||
  408. ( preg_match('{^<(?:'.$this->context_block_tags_re.')\b}', $tag) &&
  409. preg_match($newline_before_re, $parsed) &&
  410. preg_match($newline_after_re, $text) )
  411. )
  412. {
  413. # Need to parse tag and following text using the HTML parser.
  414. list($block_text, $text) =
  415. $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true);
  416. # Make sure it stays outside of any paragraph by adding newlines.
  417. $parsed .= "\n\n$block_text\n\n";
  418. }
  419. #
  420. # Check for: Clean tag (like script, math)
  421. # HTML Comments, processing instructions.
  422. #
  423. else if (preg_match('{^<(?:'.$this->clean_tags_re.')\b}', $tag) ||
  424. $tag{1} == '!' || $tag{1} == '?')
  425. {
  426. # Need to parse tag and following text using the HTML parser.
  427. # (don't check for markdown attribute)
  428. list($block_text, $text) =
  429. $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false);
  430. $parsed .= $block_text;
  431. }
  432. #
  433. # Check for: Tag with same name as enclosing tag.
  434. #
  435. else if ($enclosing_tag_re !== '' &&
  436. # Same name as enclosing tag.
  437. preg_match('{^</?(?:'.$enclosing_tag_re.')\b}', $tag))
  438. {
  439. #
  440. # Increase/decrease nested tag count.
  441. #
  442. if ($tag{1} == '/') $depth--;
  443. else if ($tag{strlen($tag)-2} != '/') $depth++;
  444. if ($depth < 0) {
  445. #
  446. # Going out of parent element. Clean up and break so we
  447. # return to the calling function.
  448. #
  449. $text = $tag . $text;
  450. break;
  451. }
  452. $parsed .= $tag;
  453. }
  454. else {
  455. $parsed .= $tag;
  456. }
  457. } while ($depth >= 0);
  458. return array($parsed, $text);
  459. }
  460. protected function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) {
  461. #
  462. # Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags.
  463. #
  464. # * Calls $hash_method to convert any blocks.
  465. # * Stops when the first opening tag closes.
  466. # * $md_attr indicate if the use of the `markdown="1"` attribute is allowed.
  467. # (it is not inside clean tags)
  468. #
  469. # Returns an array of that form: ( processed text , remaining text )
  470. #
  471. if ($text === '') return array('', '');
  472. # Regex to match `markdown` attribute inside of a tag.
  473. $markdown_attr_re = '
  474. {
  475. \s* # Eat whitespace before the `markdown` attribute
  476. markdown
  477. \s*=\s*
  478. (?>
  479. (["\']) # $1: quote delimiter
  480. (.*?) # $2: attribute value
  481. \1 # matching delimiter
  482. |
  483. ([^\s>]*) # $3: unquoted attribute value
  484. )
  485. () # $4: make $3 always defined (avoid warnings)
  486. }xs';
  487. # Regex to match any tag.
  488. $tag_re = '{
  489. ( # $2: Capture whole tag.
  490. </? # Any opening or closing tag.
  491. [\w:$]+ # Tag name.
  492. (?:
  493. (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name.
  494. (?>
  495. ".*?" | # Double quotes (can contain `>`)
  496. \'.*?\' | # Single quotes (can contain `>`)
  497. .+? # Anything but quotes and `>`.
  498. )*?
  499. )?
  500. > # End of tag.
  501. |
  502. <!-- .*? --> # HTML Comment
  503. |
  504. <\?.*?\?> | <%.*?%> # Processing instruction
  505. |
  506. <!\[CDATA\[.*?\]\]> # CData Block
  507. )
  508. }xs';
  509. $original_text = $text; # Save original text in case of faliure.
  510. $depth = 0; # Current depth inside the tag tree.
  511. $block_text = ""; # Temporary text holder for current text.
  512. $parsed = ""; # Parsed text that will be returned.
  513. #
  514. # Get the name of the starting tag.
  515. # (This pattern makes $base_tag_name_re safe without quoting.)
  516. #
  517. if (preg_match('/^<([\w:$]*)\b/', $text, $matches))
  518. $base_tag_name_re = $matches[1];
  519. #
  520. # Loop through every tag until we find the corresponding closing tag.
  521. #
  522. do {
  523. #
  524. # Split the text using the first $tag_match pattern found.
  525. # Text before pattern will be first in the array, text after
  526. # pattern will be at the end, and between will be any catches made
  527. # by the pattern.
  528. #
  529. $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
  530. if (count($parts) < 3) {
  531. #
  532. # End of $text reached with unbalenced tag(s).
  533. # In that case, we return original text unchanged and pass the
  534. # first character as filtered to prevent an infinite loop in the
  535. # parent function.
  536. #
  537. return array($original_text{0}, substr($original_text, 1));
  538. }
  539. $block_text .= $parts[0]; # Text before current tag.
  540. $tag = $parts[1]; # Tag to handle.
  541. $text = $parts[2]; # Remaining text after current tag.
  542. #
  543. # Check for: Auto-close tag (like <hr/>)
  544. # Comments and Processing Instructions.
  545. #
  546. if (preg_match('{^</?(?:'.$this->auto_close_tags_re.')\b}', $tag) ||
  547. $tag{1} == '!' || $tag{1} == '?')
  548. {
  549. # Just add the tag to the block as if it was text.
  550. $block_text .= $tag;
  551. }
  552. else {
  553. #
  554. # Increase/decrease nested tag count. Only do so if
  555. # the tag's name match base tag's.
  556. #
  557. if (preg_match('{^</?'.$base_tag_name_re.'\b}', $tag)) {
  558. if ($tag{1} == '/') $depth--;
  559. else if ($tag{strlen($tag)-2} != '/') $depth++;
  560. }
  561. #
  562. # Check for `markdown="1"` attribute and handle it.
  563. #
  564. if ($md_attr &&
  565. preg_match($markdown_attr_re, $tag, $attr_m) &&
  566. preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3]))
  567. {
  568. # Remove `markdown` attribute from opening tag.
  569. $tag = preg_replace($markdown_attr_re, '', $tag);
  570. # Check if text inside this tag must be parsed in span mode.
  571. $this->mode = $attr_m[2] . $attr_m[3];
  572. $span_mode = $this->mode == 'span' || $this->mode != 'block' &&
  573. preg_match('{^<(?:'.$this->contain_span_tags_re.')\b}', $tag);
  574. # Calculate indent before tag.
  575. if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) {
  576. $strlen = $this->utf8_strlen;
  577. $indent = $strlen($matches[1], 'UTF-8');
  578. } else {
  579. $indent = 0;
  580. }
  581. # End preceding block with this tag.
  582. $block_text .= $tag;
  583. $parsed .= $this->$hash_method($block_text);
  584. # Get enclosing tag name for the ParseMarkdown function.
  585. # (This pattern makes $tag_name_re safe without quoting.)
  586. preg_match('/^<([\w:$]*)\b/', $tag, $matches);
  587. $tag_name_re = $matches[1];
  588. # Parse the content using the HTML-in-Markdown parser.
  589. list ($block_text, $text)
  590. = $this->_hashHTMLBlocks_inMarkdown($text, $indent,
  591. $tag_name_re, $span_mode);
  592. # Outdent markdown text.
  593. if ($indent > 0) {
  594. $block_text = preg_replace("/^[ ]{1,$indent}/m", "",
  595. $block_text);
  596. }
  597. # Append tag content to parsed text.
  598. if (!$span_mode) $parsed .= "\n\n$block_text\n\n";
  599. else $parsed .= "$block_text";
  600. # Start over with a new block.
  601. $block_text = "";
  602. }
  603. else $block_text .= $tag;
  604. }
  605. } while ($depth > 0);
  606. #
  607. # Hash last block text that wasn't processed inside the loop.
  608. #
  609. $parsed .= $this->$hash_method($block_text);
  610. return array($parsed, $text);
  611. }
  612. protected function hashClean($text) {
  613. #
  614. # Called whenever a tag must be hashed when a function inserts a "clean" tag
  615. # in $text, it passes through this function and is automaticaly escaped,
  616. # blocking invalid nested overlap.
  617. #
  618. return $this->hashPart($text, 'C');
  619. }
  620. protected function doAnchors($text) {
  621. #
  622. # Turn Markdown link shortcuts into XHTML <a> tags.
  623. #
  624. if ($this->in_anchor) return $text;
  625. $this->in_anchor = true;
  626. #
  627. # First, handle reference-style links: [link text] [id]
  628. #
  629. $text = preg_replace_callback('{
  630. ( # wrap whole match in $1
  631. \[
  632. ('.$this->nested_brackets_re.') # link text = $2
  633. \]
  634. [ ]? # one optional space
  635. (?:\n[ ]*)? # one optional newline followed by spaces
  636. \[
  637. (.*?) # id = $3
  638. \]
  639. )
  640. }xs',
  641. array($this, '_doAnchors_reference_callback'), $text);
  642. #
  643. # Next, inline-style links: [link text](url "optional title")
  644. #
  645. $text = preg_replace_callback('{
  646. ( # wrap whole match in $1
  647. \[
  648. ('.$this->nested_brackets_re.') # link text = $2
  649. \]
  650. \( # literal paren
  651. [ \n]*
  652. (?:
  653. <(.+?)> # href = $3
  654. |
  655. ('.$this->nested_url_parenthesis_re.') # href = $4
  656. )
  657. [ \n]*
  658. ( # $5
  659. ([\'"]) # quote char = $6
  660. (.*?) # Title = $7
  661. \6 # matching quote
  662. [ \n]* # ignore any spaces/tabs between closing quote and )
  663. )? # title is optional
  664. \)
  665. (?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes
  666. )
  667. }xs',
  668. array($this, '_doAnchors_inline_callback'), $text);
  669. #
  670. # Last, handle reference-style shortcuts: [link text]
  671. # These must come last in case you've also got [link text][1]
  672. # or [link text](/foo)
  673. #
  674. $text = preg_replace_callback('{
  675. ( # wrap whole match in $1
  676. \[
  677. ([^\[\]]+) # link text = $2; can\'t contain [ or ]
  678. \]
  679. )
  680. }xs',
  681. array($this, '_doAnchors_reference_callback'), $text);
  682. $this->in_anchor = false;
  683. return $text;
  684. }
  685. protected function _doAnchors_reference_callback($matches) {
  686. $whole_match = $matches[1];
  687. $link_text = $matches[2];
  688. $link_id =& $matches[3];
  689. if ($link_id == "") {
  690. # for shortcut links like [this][] or [this].
  691. $link_id = $link_text;
  692. }
  693. # lower-case and turn embedded newlines into spaces
  694. $link_id = strtolower($link_id);
  695. $link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
  696. if (isset($this->urls[$link_id])) {
  697. $url = $this->urls[$link_id];
  698. $url = $this->encodeURLAttribute($url);
  699. $result = "<a href=\"$url\"";
  700. if ( isset( $this->titles[$link_id] ) ) {
  701. $title = $this->titles[$link_id];
  702. $title = $this->encodeAttribute($title);
  703. $result .= " title=\"$title\"";
  704. }
  705. if (isset($this->ref_attr[$link_id]))
  706. $result .= $this->ref_attr[$link_id];
  707. $link_text = $this->runSpanGamut($link_text);
  708. $result .= ">$link_text</a>";
  709. $result = $this->hashPart($result);
  710. }
  711. else {
  712. $result = $whole_match;
  713. }
  714. return $result;
  715. }
  716. protected function _doAnchors_inline_callback($matches) {
  717. $whole_match = $matches[1];
  718. $link_text = $this->runSpanGamut($matches[2]);
  719. $url = $matches[3] == '' ? $matches[4] : $matches[3];
  720. $title =& $matches[7];
  721. $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]);
  722. // if the URL was of the form <s p a c e s> it got caught by the HTML
  723. // tag parser and hashed. Need to reverse the process before using the URL.
  724. $unhashed = $this->unhash($url);
  725. if ($unhashed != $url)
  726. $url = preg_replace('/^<(.*)>$/', '\1', $unhashed);
  727. $url = $this->encodeURLAttribute($url);
  728. $result = "<a href=\"$url\"";
  729. if (isset($title)) {
  730. $title = $this->encodeAttribute($title);
  731. $result .= " title=\"$title\"";
  732. }
  733. $result .= $attr;
  734. $link_text = $this->runSpanGamut($link_text);
  735. $result .= ">$link_text</a>";
  736. return $this->hashPart($result);
  737. }
  738. protected function doImages($text) {
  739. #
  740. # Turn Markdown image shortcuts into <img> tags.
  741. #
  742. #
  743. # First, handle reference-style labeled images: ![alt text][id]
  744. #
  745. $text = preg_replace_callback('{
  746. ( # wrap whole match in $1
  747. !\[
  748. ('.$this->nested_brackets_re.') # alt text = $2
  749. \]
  750. [ ]? # one optional space
  751. (?:\n[ ]*)? # one optional newline followed by spaces
  752. \[
  753. (.*?) # id = $3
  754. \]
  755. )
  756. }xs',
  757. array($this, '_doImages_reference_callback'), $text);
  758. #
  759. # Next, handle inline images: ![alt text](url "optional title")
  760. # Don't forget: encode * and _
  761. #
  762. $text = preg_replace_callback('{
  763. ( # wrap whole match in $1
  764. !\[
  765. ('.$this->nested_brackets_re.') # alt text = $2
  766. \]
  767. \s? # One optional whitespace character
  768. \( # literal paren
  769. [ \n]*
  770. (?:
  771. <(\S*)> # src url = $3
  772. |
  773. ('.$this->nested_url_parenthesis_re.') # src url = $4
  774. )
  775. [ \n]*
  776. ( # $5
  777. ([\'"]) # quote char = $6
  778. (.*?) # title = $7
  779. \6 # matching quote
  780. [ \n]*
  781. )? # title is optional
  782. \)
  783. (?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes
  784. )
  785. }xs',
  786. array($this, '_doImages_inline_callback'), $text);
  787. return $text;
  788. }
  789. protected function _doImages_reference_callback($matches) {
  790. $whole_match = $matches[1];
  791. $alt_text = $matches[2];
  792. $link_id = strtolower($matches[3]);
  793. if ($link_id == "") {
  794. $link_id = strtolower($alt_text); # for shortcut links like ![this][].
  795. }
  796. $alt_text = $this->encodeAttribute($alt_text);
  797. if (isset($this->urls[$link_id])) {
  798. $url = $this->encodeURLAttribute($this->urls[$link_id]);
  799. $result = "<img src=\"$url\" alt=\"$alt_text\"";
  800. if (isset($this->titles[$link_id])) {
  801. $title = $this->titles[$link_id];
  802. $title = $this->encodeAttribute($title);
  803. $result .= " title=\"$title\"";
  804. }
  805. if (isset($this->ref_attr[$link_id]))
  806. $result .= $this->ref_attr[$link_id];
  807. $result .= $this->empty_element_suffix;
  808. $result = $this->hashPart($result);
  809. }
  810. else {
  811. # If there's no such link ID, leave intact:
  812. $result = $whole_match;
  813. }
  814. return $result;
  815. }
  816. protected function _doImages_inline_callback($matches) {
  817. $whole_match = $matches[1];
  818. $alt_text = $matches[2];
  819. $url = $matches[3] == '' ? $matches[4] : $matches[3];
  820. $title =& $matches[7];
  821. $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]);
  822. $alt_text = $this->encodeAttribute($alt_text);
  823. $url = $this->encodeURLAttribute($url);
  824. $result = "<img src=\"$url\" alt=\"$alt_text\"";
  825. if (isset($title)) {
  826. $title = $this->encodeAttribute($title);
  827. $result .= " title=\"$title\""; # $title already quoted
  828. }
  829. $result .= $attr;
  830. $result .= $this->empty_element_suffix;
  831. return $this->hashPart($result);
  832. }
  833. protected function doHeaders($text) {
  834. #
  835. # Redefined to add id and class attribute support.
  836. #
  837. # Setext-style headers:
  838. # Header 1 {#header1}
  839. # ========
  840. #
  841. # Header 2 {#header2 .class1 .class2}
  842. # --------
  843. #
  844. $text = preg_replace_callback(
  845. '{
  846. (^.+?) # $1: Header text
  847. (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes
  848. [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer
  849. }mx',
  850. array($this, '_doHeaders_callback_setext'), $text);
  851. # atx-style headers:
  852. # # Header 1 {#header1}
  853. # ## Header 2 {#header2}
  854. # ## Header 2 with closing hashes ## {#header3.class1.class2}
  855. # ...
  856. # ###### Header 6 {.class2}
  857. #
  858. $text = preg_replace_callback('{
  859. ^(\#{1,6}) # $1 = string of #\'s
  860. [ ]*
  861. (.+?) # $2 = Header text
  862. [ ]*
  863. \#* # optional closing #\'s (not counted)
  864. (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes
  865. [ ]*
  866. \n+
  867. }xm',
  868. array($this, '_doHeaders_callback_atx'), $text);
  869. return $text;
  870. }
  871. protected function _doHeaders_callback_setext($matches) {
  872. if ($matches[3] == '-' && preg_match('{^- }', $matches[1]))
  873. return $matches[0];
  874. $level = $matches[3]{0} == '=' ? 1 : 2;
  875. $defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[1]) : null;
  876. $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2], $defaultId);
  877. $block = "<h$level$attr>".$this->runSpanGamut($matches[1])."</h$level>";
  878. return "\n" . $this->hashBlock($block) . "\n\n";
  879. }
  880. protected function _doHeaders_callback_atx($matches) {
  881. $level = strlen($matches[1]);
  882. $defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[2]) : null;
  883. $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3], $defaultId);
  884. $block = "<h$level$attr>".$this->runSpanGamut($matches[2])."</h$level>";
  885. return "\n" . $this->hashBlock($block) . "\n\n";
  886. }
  887. protected function doTables($text) {
  888. #
  889. # Form HTML tables.
  890. #
  891. $less_than_tab = $this->tab_width - 1;
  892. #
  893. # Find tables with leading pipe.
  894. #
  895. # | Header 1 | Header 2
  896. # | -------- | --------
  897. # | Cell 1 | Cell 2
  898. # | Cell 3 | Cell 4
  899. #
  900. $text = preg_replace_callback('
  901. {
  902. ^ # Start of a line
  903. [ ]{0,'.$less_than_tab.'} # Allowed whitespace.
  904. [|] # Optional leading pipe (present)
  905. (.+) \n # $1: Header row (at least one pipe)
  906. [ ]{0,'.$less_than_tab.'} # Allowed whitespace.
  907. [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline
  908. ( # $3: Cells
  909. (?>
  910. [ ]* # Allowed whitespace.
  911. [|] .* \n # Row content.
  912. )*
  913. )
  914. (?=\n|\Z) # Stop at final double newline.
  915. }xm',
  916. array($this, '_doTable_leadingPipe_callback'), $text);
  917. #
  918. # Find tables without leading pipe.
  919. #
  920. # Header 1 | Header 2
  921. # -------- | --------
  922. # Cell 1 | Cell 2
  923. # Cell 3 | Cell 4
  924. #
  925. $text = preg_replace_callback('
  926. {
  927. ^ # Start of a line
  928. [ ]{0,'.$less_than_tab.'} # Allowed whitespace.
  929. (\S.*[|].*) \n # $1: Header row (at least one pipe)
  930. [ ]{0,'.$less_than_tab.'} # Allowed whitespace.
  931. ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline
  932. ( # $3: Cells
  933. (?>
  934. .* [|] .* \n # Row content
  935. )*
  936. )
  937. (?=\n|\Z) # Stop at final double newline.
  938. }xm',
  939. array($this, '_DoTable_callback'), $text);
  940. return $text;
  941. }
  942. protected function _doTable_leadingPipe_callback($matches) {
  943. $head = $matches[1];
  944. $underline = $matches[2];
  945. $content = $matches[3];
  946. # Remove leading pipe for each row.
  947. $content = preg_replace('/^ *[|]/m', '', $content);
  948. return $this->_doTable_callback(array($matches[0], $head, $underline, $content));
  949. }
  950. protected function _doTable_makeAlignAttr($alignname)
  951. {
  952. if (empty($this->table_align_class_tmpl))
  953. return " align=\"$alignname\"";
  954. $classname = str_replace('%%', $alignname, $this->table_align_class_tmpl);
  955. return " class=\"$classname\"";
  956. }
  957. protected function _doTable_callback($matches) {
  958. $head = $matches[1];
  959. $underline = $matches[2];
  960. $content = $matches[3];
  961. # Remove any tailing pipes for each line.
  962. $head = preg_replace('/[|] *$/m', '', $head);
  963. $underline = preg_replace('/[|] *$/m', '', $underline);
  964. $content = preg_replace('/[|] *$/m', '', $content);
  965. # Reading alignement from header underline.
  966. $separators = preg_split('/ *[|] */', $underline);
  967. foreach ($separators as $n => $s) {
  968. if (preg_match('/^ *-+: *$/', $s))
  969. $attr[$n] = $this->_doTable_makeAlignAttr('right');
  970. else if (preg_match('/^ *:-+: *$/', $s))
  971. $attr[$n] = $this->_doTable_makeAlignAttr('center');
  972. else if (preg_match('/^ *:-+ *$/', $s))
  973. $attr[$n] = $this->_doTable_makeAlignAttr('left');
  974. else
  975. $attr[$n] = '';
  976. }
  977. # Parsing span elements, including code spans, character escapes,
  978. # and inline HTML tags, so that pipes inside those gets ignored.
  979. $head = $this->parseSpan($head);
  980. $headers = preg_split('/ *[|] */', $head);
  981. $col_count = count($headers);
  982. $attr = array_pad($attr, $col_count, '');
  983. # Write column headers.
  984. $text = "<table>\n";
  985. $text .= "<thead>\n";
  986. $text .= "<tr>\n";
  987. foreach ($headers as $n => $header)
  988. $text .= " <th$attr[$n]>".$this->runSpanGamut(trim($header))."</th>\n";
  989. $text .= "</tr>\n";
  990. $text .= "</thead>\n";
  991. # Split content by row.
  992. $rows = explode("\n", trim($content, "\n"));
  993. $text .= "<tbody>\n";
  994. foreach ($rows as $row) {
  995. # Parsing span elements, including code spans, character escapes,
  996. # and inline HTML tags, so that pipes inside those gets ignored.
  997. $row = $this->parseSpan($row);
  998. # Split row by cell.
  999. $row_cells = preg_split('/ *[|] */', $row, $col_count);
  1000. $row_cells = array_pad($row_cells, $col_count, '');
  1001. $text .= "<tr>\n";
  1002. foreach ($row_cells as $n => $cell)
  1003. $text .= " <td$attr[$n]>".$this->runSpanGamut(trim($cell))."</td>\n";
  1004. $text .= "</tr>\n";
  1005. }
  1006. $text .= "</tbody>\n";
  1007. $text .= "</table>";
  1008. return $this->hashBlock($text) . "\n";
  1009. }
  1010. protected function doDefLists($text) {
  1011. #
  1012. # Form HTML definition lists.
  1013. #
  1014. $less_than_tab = $this->tab_width - 1;
  1015. # Re-usable pattern to match any entire dl list:
  1016. $whole_list_re = '(?>
  1017. ( # $1 = whole list
  1018. ( # $2
  1019. [ ]{0,'.$less_than_tab.'}
  1020. ((?>.*\S.*\n)+) # $3 = defined term
  1021. \n?
  1022. [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
  1023. )
  1024. (?s:.+?)
  1025. ( # $4
  1026. \z
  1027. |
  1028. \n{2,}
  1029. (?=\S)
  1030. (?! # Negative lookahead for another term
  1031. [ ]{0,'.$less_than_tab.'}
  1032. (?: \S.*\n )+? # defined term
  1033. \n?
  1034. [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
  1035. )
  1036. (?! # Negative lookahead for another definition
  1037. [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
  1038. )
  1039. )
  1040. )
  1041. )'; // mx
  1042. $text = preg_replace_callback('{
  1043. (?>\A\n?|(?<=\n\n))
  1044. '.$whole_list_re.'
  1045. }mx',
  1046. array($this, '_doDefLists_callback'), $text);
  1047. return $text;
  1048. }
  1049. protected function _doDefLists_callback($matches) {
  1050. # Re-usable patterns to match list item bullets and number markers:
  1051. $list = $matches[1];
  1052. # Turn double returns into triple returns, so that we can make a
  1053. # paragraph for the last item in a list, if necessary:
  1054. $result = trim($this->processDefListItems($list));
  1055. $result = "<dl>\n" . $result . "\n</dl>";
  1056. return $this->hashBlock($result) . "\n\n";
  1057. }
  1058. protected function processDefListItems($list_str) {
  1059. #
  1060. # Process the contents of a single definition list, splitting it
  1061. # into individual term and definition list items.
  1062. #
  1063. $less_than_tab = $this->tab_width - 1;
  1064. # trim trailing blank lines:
  1065. $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
  1066. # Process definition terms.
  1067. $list_str = preg_replace_callback('{
  1068. (?>\A\n?|\n\n+) # leading line
  1069. ( # definition terms = $1
  1070. [ ]{0,'.$less_than_tab.'} # leading whitespace
  1071. (?!\:[ ]|[ ]) # negative lookahead for a definition
  1072. # mark (colon) or more whitespace.
  1073. (?> \S.* \n)+? # actual term (not whitespace).
  1074. )
  1075. (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed
  1076. # with a definition mark.
  1077. }xm',
  1078. array($this, '_processDefListItems_callback_dt'), $list_str);
  1079. # Process actual definitions.
  1080. $list_str = preg_replace_callback('{
  1081. \n(\n+)? # leading line = $1
  1082. ( # marker space = $2
  1083. [ ]{0,'.$less_than_tab.'} # whitespace before colon
  1084. \:[ ]+ # definition mark (colon)
  1085. )
  1086. ((?s:.+?)) # definition text = $3
  1087. (?= \n+ # stop at next definition mark,
  1088. (?: # next term or end of text
  1089. [ ]{0,'.$less_than_tab.'} \:[ ] |
  1090. <dt> | \z
  1091. )
  1092. )
  1093. }xm',
  1094. array($this, '_processDefListItems_callback_dd'), $list_str);
  1095. return $list_str;
  1096. }
  1097. protected function _processDefListItems_callback_dt($matches) {
  1098. $terms = explode("\n", trim($matches[1]));
  1099. $text = '';
  1100. foreach ($terms as $term) {
  1101. $term = $this->runSpanGamut(trim($term));
  1102. $text .= "\n<dt>" . $term . "</dt>";
  1103. }
  1104. return $text . "\n";
  1105. }
  1106. protected function _processDefListItems_callback_dd($matches) {
  1107. $leading_line = $matches[1];
  1108. $marker_space = $matches[2];
  1109. $def = $matches[3];
  1110. if ($leading_line || preg_match('/\n{2,}/', $def)) {
  1111. # Replace marker with the appropriate whitespace indentation
  1112. $def = str_repeat(' ', strlen($marker_space)) . $def;
  1113. $def = $this->runBlockGamut($this->outdent($def . "\n\n"));
  1114. $def = "\n". $def ."\n";
  1115. }
  1116. else {
  1117. $def = rtrim($def);
  1118. $def = $this->runSpanGamut($this->outdent($def));
  1119. }
  1120. return "\n<dd>" . $def . "</dd>\n";
  1121. }
  1122. protected function doFencedCodeBlocks($text) {
  1123. #
  1124. # Adding the fenced code block syntax to regular Markdown:
  1125. #
  1126. # ~~~
  1127. # Code block
  1128. # ~~~
  1129. #
  1130. $less_than_tab = $this->tab_width;
  1131. $text = preg_replace_callback('{
  1132. (?:\n|\A)
  1133. # 1: Opening marker
  1134. (
  1135. (?:~{3,}|`{3,}) # 3 or more tildes/backticks.
  1136. )
  1137. [ ]*
  1138. (?:
  1139. \.?([-_:a-zA-Z0-9]+) # 2: standalone class name
  1140. )?
  1141. [ ]*
  1142. (?:
  1143. '.$this->id_class_attr_catch_re.' # 3: Extra attributes
  1144. )?
  1145. [ ]* \n # Whitespace and newline following marker.
  1146. # 4: Content
  1147. (
  1148. (?>
  1149. (?!\1 [ ]* \n) # Not a closing marker.
  1150. .*\n+
  1151. )+
  1152. )
  1153. # Closing marker.
  1154. \1 [ ]* (?= \n )
  1155. }xm',
  1156. array($this, '_doFencedCodeBlocks_callback'), $text);
  1157. return $text;
  1158. }
  1159. protected function _doFencedCodeBlocks_callback($matches) {
  1160. $classname =& $matches[2];
  1161. $attrs =& $matches[3];
  1162. $codeblock = $matches[4];
  1163. if ($this->code_block_content_func) {
  1164. $codeblock = call_user_func($this->code_block_content_func, $codeblock, $classname);
  1165. } else {
  1166. $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
  1167. }
  1168. $codeblock = preg_replace_callback('/^\n+/',
  1169. array($this, '_doFencedCodeBlocks_newlines'), $codeblock);
  1170. $classes = array();
  1171. if ($classname != "") {
  1172. if ($classname{0} == '.')
  1173. $classname = substr($classname, 1);
  1174. $classes[] = $this->code_class_prefix.$classname;
  1175. }
  1176. $attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs, null, $classes);
  1177. $pre_attr_str = $this->code_attr_on_pre ? $attr_str : '';
  1178. $code_attr_str = $this->code_attr_on_pre ? '' : $attr_str;
  1179. $codeblock = "<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre>";
  1180. return "\n\n".$this->hashBlock($codeblock)."\n\n";
  1181. }
  1182. protected function _doFencedCodeBlocks_newlines($matches) {
  1183. return str_repeat("<br$this->empty_element_suffix",
  1184. strlen($matches[0]));
  1185. }
  1186. #
  1187. # Redefining emphasis markers so that emphasis by underscore does not
  1188. # work in the middle of a word.
  1189. #
  1190. protected $em_relist = array(
  1191. '' => '(?:(?<!\*)\*(?!\*)|(?<![a-zA-Z0-9_])_(?!_))(?![\.,:;]?\s)',
  1192. '*' => '(?<![\s*])\*(?!\*)',
  1193. '_' => '(?<![\s_])_(?![a-zA-Z0-9_])',
  1194. );
  1195. protected $strong_relist = array(
  1196. '' => '(?:(?<!\*)\*\*(?!\*)|(?<![a-zA-Z0-9_])__(?!_))(?![\.,:;]?\s)',
  1197. '**' => '(?<![\s*])\*\*(?!\*)',
  1198. '__' => '(?<![\s_])__(?![a-zA-Z0-9_])',
  1199. );
  1200. protected $em_strong_relist = array(
  1201. '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<![a-zA-Z0-9_])___(?!_))(?![\.,:;]?\s)',
  1202. '***' => '(?<![\s*])\*\*\*(?!\*)',
  1203. '___' => '(?<![\s_])___(?![a-zA-Z0-9_])',
  1204. );
  1205. protected function formParagraphs($text) {
  1206. #
  1207. # Params:
  1208. # $text - string to process with html <p> tags
  1209. #
  1210. # Strip leading and trailing lines:
  1211. $text = preg_replace('/\A\n+|\n+\z/', '', $text);
  1212. $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
  1213. #
  1214. # Wrap <p> tags and unhashify HTML blocks
  1215. #
  1216. foreach ($grafs as $key => $value) {
  1217. $value = trim($this->runSpanGamut($value));
  1218. # Check if this should be enclosed in a paragraph.
  1219. # Clean tag hashes & block tag hashes are left alone.
  1220. $is_p = !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value);
  1221. if ($is_p) {
  1222. $value = "<p>$value</p>";
  1223. }
  1224. $grafs[$key] = $value;
  1225. }
  1226. # Join grafs in one text, then unhash HTML tags.
  1227. $text = implode("\n\n", $grafs);
  1228. # Finish by removing any tag hashes still present in $text.
  1229. $text = $this->unhash($text);
  1230. return $text;
  1231. }
  1232. ### Footnotes
  1233. protected function stripFootnotes($text) {
  1234. #
  1235. # Strips link definitions from text, stores the URLs and titles in
  1236. # hash references.
  1237. #
  1238. $less_than_tab = $this->tab_width - 1;
  1239. # Link defs are in the form: [^id]: url "optional title"
  1240. $text = preg_replace_callback('{
  1241. ^[ ]{0,'.$less_than_tab.'}\[\^(.+?)\][ ]?: # note_id = $1
  1242. [ ]*
  1243. \n? # maybe *one* newline
  1244. ( # text = $2 (no blank lines allowed)
  1245. (?:
  1246. .+ # actual text
  1247. |
  1248. \n # newlines but
  1249. (?!\[.+?\][ ]?:\s)# negative lookahead for footnote or link definition marker.
  1250. (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed
  1251. # by non-indented content
  1252. )*
  1253. )
  1254. }xm',
  1255. array($this, '_stripFootnotes_callback'),
  1256. $text);
  1257. return $text;
  1258. }
  1259. protected function _stripFootnotes_callback($matches) {
  1260. $note_id = $this->fn_id_prefix . $matches[1];
  1261. $this->footnotes[$note_id] = $this->outdent($matches[2]);
  1262. return ''; # String that will replace the block
  1263. }
  1264. protected function doFootnotes($text) {
  1265. #
  1266. # Replace footnote references in $text [^id] with a special text-token
  1267. # which will be replaced by the actual footnote marker in appendFootnotes.
  1268. #
  1269. if (!$this->in_anchor) {
  1270. $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text);
  1271. }
  1272. return $text;
  1273. }
  1274. protected function appendFootnotes($text) {
  1275. #
  1276. # Append footnote list to text.
  1277. #
  1278. $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
  1279. array($this, '_appendFootnotes_callback'), $text);
  1280. if (!empty($this->footnotes_ordered)) {
  1281. $text .= "\n\n";
  1282. $text .= "<div class=\"footnotes\">\n";
  1283. $text .= "<hr". $this->empty_element_suffix ."\n";
  1284. $text .= "<ol>\n\n";
  1285. $attr = "";
  1286. if ($this->fn_backlink_class != "") {
  1287. $class = $this->fn_backlink_class;
  1288. $class = $this->encodeAttribute($class);
  1289. $attr .= " class=\"$class\"";
  1290. }
  1291. if ($this->fn_backlink_title != "") {
  1292. $title = $this->fn_backlink_title;
  1293. $title = $this->encodeAttribute($title);
  1294. $attr .= " title=\"$title\"";
  1295. }
  1296. $backlink_text = $this->fn_backlink_html;
  1297. $num = 0;
  1298. while (!empty($this->footnotes_ordered)) {
  1299. $footnote = reset($this->footnotes_ordered);
  1300. $note_id = key($this->footnotes_ordered);
  1301. unset($this->footnotes_ordered[$note_id]);
  1302. $ref_count = $this->footnotes_ref_count[$note_id];
  1303. unset($this->footnotes_ref_count[$note_id]);
  1304. unset($this->footnotes[$note_id]);
  1305. $footnote .= "\n"; # Need to append newline before parsing.
  1306. $footnote = $this->runBlockGamut("$footnote\n");
  1307. $footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
  1308. array($this, '_appendFootnotes_callback'), $footnote);
  1309. $attr = str_replace("%%", ++$num, $attr);
  1310. $note_id = $this->encodeAttribute($note_id);
  1311. # Prepare backlink, multiple backlinks if multiple references
  1312. $backlink = "<a href=\"#fnref:$note_id\"$attr>$backlink_text</a>";
  1313. for ($ref_num = 2; $ref_num <= $ref_count; ++$ref_num) {
  1314. $backlink .= " <a href=\"#fnref$ref_num:$note_id\"$attr>$backlink_text</a>";
  1315. }
  1316. # Add backlink to last paragraph; create new paragraph if needed.
  1317. if (preg_match('{</p>$}', $footnote)) {
  1318. $footnote = substr($footnote, 0, -4) . "&#160;$backlink</p>";
  1319. } else {
  1320. $footnote .= "\n\n<p>$backlink</p>";
  1321. }
  1322. $text .= "<li id=\"fn:$note_id\">\n";
  1323. $text .= $footnote . "\n";
  1324. $text .= "</li>\n\n";
  1325. }
  1326. $text .= "</ol>\n";
  1327. $text .= "</div>";
  1328. }
  1329. return $text;
  1330. }
  1331. protected function _appendFootnotes_callback($matches) {
  1332. $node_id = $this->fn_id_prefix . $matches[1];
  1333. # Create footnote marker only if it has a corresponding footnote *and*
  1334. # the footnote hasn't been used by another marker.
  1335. if (isset($this->footnotes[$node_id])) {
  1336. $num =& $this->footnotes_numbers[$node_id];
  1337. if (!isset($num)) {
  1338. # Transfer footnote content to the ordered list and give it its
  1339. # number
  1340. $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id];
  1341. $this->footnotes_ref_count[$node_id] = 1;
  1342. $num = $this->footnote_counter++;
  1343. $ref_count_mark = '';
  1344. } else {
  1345. $ref_count_mark = $this->footnotes_ref_count[$node_id] += 1;
  1346. }
  1347. $attr = "";
  1348. if ($this->fn_link_class != "") {
  1349. $class = $this->fn_link_class;
  1350. $class = $this->encodeAttribute($class);
  1351. $attr .= " class=\"$class\"";
  1352. }
  1353. if ($this->fn_link_title != "") {
  1354. $title = $this->fn_link_title;
  1355. $title = $this->encodeAttribute($title);
  1356. $attr .= " title=\"$title\"";
  1357. }
  1358. $attr = str_replace("%%", $num, $attr);
  1359. $node_id = $this->encodeAttribute($node_id);
  1360. return
  1361. "<sup id=\"fnref$ref_count_mark:$node_id\">".
  1362. "<a href=\"#fn:$node_id\"$attr>$num</a>".
  1363. "</sup>";
  1364. }
  1365. return "[^".$matches[1]."]";
  1366. }
  1367. ### Abbreviations ###
  1368. protected function stripAbbreviations($text) {
  1369. #
  1370. # Strips abbreviations from text, stores titles in hash references.
  1371. #
  1372. $less_than_tab = $this->tab_width - 1;
  1373. # Link defs are in the form: [id]*: url "optional title"
  1374. $text = preg_replace_callback('{
  1375. ^[ ]{0,'.$less_than_tab.'}\*\[(.+?)\][ ]?: # abbr_id = $1
  1376. (.*) # text = $2 (no blank lines allowed)
  1377. }xm',
  1378. array($this, '_stripAbbreviations_callback'),
  1379. $text);
  1380. return $text;
  1381. }
  1382. protected function _stripAbbreviations_callback($matches) {
  1383. $abbr_word = $matches[1];
  1384. $abbr_desc = $matches[2];
  1385. if ($this->abbr_word_re)
  1386. $this->abbr_word_re .= '|';
  1387. $this->abbr_word_re .= preg_quote($abbr_word);
  1388. $this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
  1389. return ''; # String that will replace the block
  1390. }
  1391. protected function doAbbreviations($text) {
  1392. #
  1393. # Find defined abbreviations in text and wrap them in <abbr> elements.
  1394. #
  1395. if ($this->abbr_word_re) {
  1396. // cannot use the /x modifier because abbr_word_re may
  1397. // contain significant spaces:
  1398. $text = preg_replace_callback('{'.
  1399. '(?<![\w\x1A])'.
  1400. '(?:'.$this->abbr_word_re.')'.
  1401. '(?![\w\x1A])'.
  1402. '}',
  1403. array($this, '_doAbbreviations_callback'), $text);
  1404. }
  1405. return $text;
  1406. }
  1407. protected function _doAbbreviations_callback($matches) {
  1408. $abbr = $matches[0];
  1409. if (isset($this->abbr_desciptions[$abbr])) {
  1410. $desc = $this->abbr_desciptions[$abbr];
  1411. if (empty($desc)) {
  1412. return $this->hashPart("<abbr>$abbr</abbr>");
  1413. } else {
  1414. $desc = $this->encodeAttribute($desc);
  1415. return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>");
  1416. }
  1417. } else {
  1418. return $matches[0];
  1419. }
  1420. }
  1421. }