Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

LinkTrait.php 7.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2014 Carsten Brandt
  4. * @license https://github.com/cebe/markdown/blob/master/LICENSE
  5. * @link https://github.com/cebe/markdown#readme
  6. */
  7. namespace cebe\markdown\inline;
  8. // work around https://github.com/facebook/hhvm/issues/1120
  9. defined('ENT_HTML401') || define('ENT_HTML401', 0);
  10. /**
  11. * Addes links and images as well as url markers.
  12. *
  13. * This trait conflicts with the HtmlTrait. If both are used together,
  14. * you have to define a resolution, by defining the HtmlTrait::parseInlineHtml
  15. * as private so it is not used directly:
  16. *
  17. * ```php
  18. * use block\HtmlTrait {
  19. * parseInlineHtml as private parseInlineHtml;
  20. * }
  21. * ```
  22. *
  23. * If the method exists it is called internally by this trait.
  24. *
  25. * Also make sure to reset references on prepare():
  26. *
  27. * ```php
  28. * protected function prepare()
  29. * {
  30. * // reset references
  31. * $this->references = [];
  32. * }
  33. * ```
  34. */
  35. trait LinkTrait
  36. {
  37. /**
  38. * @var array a list of defined references in this document.
  39. */
  40. protected $references = [];
  41. /**
  42. * Parses a link indicated by `[`.
  43. * @marker [
  44. */
  45. protected function parseLink($markdown)
  46. {
  47. if (!in_array('parseLink', array_slice($this->context, 1)) && ($parts = $this->parseLinkOrImage($markdown)) !== false) {
  48. list($text, $url, $title, $offset, $key) = $parts;
  49. return [
  50. [
  51. 'link',
  52. 'text' => $this->parseInline($text),
  53. 'url' => $url,
  54. 'title' => $title,
  55. 'refkey' => $key,
  56. 'orig' => substr($markdown, 0, $offset),
  57. ],
  58. $offset
  59. ];
  60. } else {
  61. // remove all starting [ markers to avoid next one to be parsed as link
  62. $result = '[';
  63. $i = 1;
  64. while (isset($markdown[$i]) && $markdown[$i] == '[') {
  65. $result .= '[';
  66. $i++;
  67. }
  68. return [['text', $result], $i];
  69. }
  70. }
  71. /**
  72. * Parses an image indicated by `![`.
  73. * @marker ![
  74. */
  75. protected function parseImage($markdown)
  76. {
  77. if (($parts = $this->parseLinkOrImage(substr($markdown, 1))) !== false) {
  78. list($text, $url, $title, $offset, $key) = $parts;
  79. return [
  80. [
  81. 'image',
  82. 'text' => $text,
  83. 'url' => $url,
  84. 'title' => $title,
  85. 'refkey' => $key,
  86. 'orig' => substr($markdown, 0, $offset + 1),
  87. ],
  88. $offset + 1
  89. ];
  90. } else {
  91. // remove all starting [ markers to avoid next one to be parsed as link
  92. $result = '!';
  93. $i = 1;
  94. while (isset($markdown[$i]) && $markdown[$i] == '[') {
  95. $result .= '[';
  96. $i++;
  97. }
  98. return [['text', $result], $i];
  99. }
  100. }
  101. protected function parseLinkOrImage($markdown)
  102. {
  103. if (strpos($markdown, ']') !== false && preg_match('/\[((?>[^\]\[]+|(?R))*)\]/', $markdown, $textMatches)) { // TODO improve bracket regex
  104. $text = $textMatches[1];
  105. $offset = strlen($textMatches[0]);
  106. $markdown = substr($markdown, $offset);
  107. $pattern = <<<REGEXP
  108. /(?(R) # in case of recursion match parentheses
  109. \(((?>[^\s()]+)|(?R))*\)
  110. | # else match a link with title
  111. ^\((((?>[^\s()]+)|(?R))*)(\s+"(.*?)")?\)
  112. )/x
  113. REGEXP;
  114. if (preg_match($pattern, $markdown, $refMatches)) {
  115. // inline link
  116. return [
  117. $text,
  118. isset($refMatches[2]) ? $refMatches[2] : '', // url
  119. empty($refMatches[5]) ? null: $refMatches[5], // title
  120. $offset + strlen($refMatches[0]), // offset
  121. null, // reference key
  122. ];
  123. } elseif (preg_match('/^([ \n]?\[(.*?)\])?/s', $markdown, $refMatches)) {
  124. // reference style link
  125. if (empty($refMatches[2])) {
  126. $key = strtolower($text);
  127. } else {
  128. $key = strtolower($refMatches[2]);
  129. }
  130. return [
  131. $text,
  132. null, // url
  133. null, // title
  134. $offset + strlen($refMatches[0]), // offset
  135. $key,
  136. ];
  137. }
  138. }
  139. return false;
  140. }
  141. /**
  142. * Parses inline HTML.
  143. * @marker <
  144. */
  145. protected function parseLt($text)
  146. {
  147. if (strpos($text, '>') !== false) {
  148. if (!in_array('parseLink', $this->context)) { // do not allow links in links
  149. if (preg_match('/^<([^\s]*?@[^\s]*?\.\w+?)>/', $text, $matches)) {
  150. // email address
  151. return [
  152. ['email', $matches[1]],
  153. strlen($matches[0])
  154. ];
  155. } elseif (preg_match('/^<([a-z]{3,}:\/\/[^\s]+?)>/', $text, $matches)) {
  156. // URL
  157. return [
  158. ['url', $matches[1]],
  159. strlen($matches[0])
  160. ];
  161. }
  162. }
  163. // try inline HTML if it was neither a URL nor email if HtmlTrait is included.
  164. if (method_exists($this, 'parseInlineHtml')) {
  165. return $this->parseInlineHtml($text);
  166. }
  167. }
  168. return [['text', '&lt;'], 1];
  169. }
  170. protected function renderEmail($block)
  171. {
  172. $email = htmlspecialchars($block[1], ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
  173. return "<a href=\"mailto:$email\">$email</a>";
  174. }
  175. protected function renderUrl($block)
  176. {
  177. $url = htmlspecialchars($block[1], ENT_COMPAT | ENT_HTML401, 'UTF-8');
  178. $text = htmlspecialchars(urldecode($block[1]), ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
  179. return "<a href=\"$url\">$text</a>";
  180. }
  181. protected function lookupReference($key)
  182. {
  183. $normalizedKey = preg_replace('/\s+/', ' ', $key);
  184. if (isset($this->references[$key]) || isset($this->references[$key = $normalizedKey])) {
  185. return $this->references[$key];
  186. }
  187. return false;
  188. }
  189. protected function renderLink($block)
  190. {
  191. if (isset($block['refkey'])) {
  192. if (($ref = $this->lookupReference($block['refkey'])) !== false) {
  193. $block = array_merge($block, $ref);
  194. } else {
  195. return $block['orig'];
  196. }
  197. }
  198. return '<a href="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
  199. . (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
  200. . '>' . $this->renderAbsy($block['text']) . '</a>';
  201. }
  202. protected function renderImage($block)
  203. {
  204. if (isset($block['refkey'])) {
  205. if (($ref = $this->lookupReference($block['refkey'])) !== false) {
  206. $block = array_merge($block, $ref);
  207. } else {
  208. return $block['orig'];
  209. }
  210. }
  211. return '<img src="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
  212. . ' alt="' . htmlspecialchars($block['text'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"'
  213. . (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
  214. . ($this->html5 ? '>' : ' />');
  215. }
  216. // references
  217. protected function identifyReference($line)
  218. {
  219. return ($line[0] === ' ' || $line[0] === '[') && preg_match('/^ {0,3}\[(.+?)\]:\s*([^\s]+?)(?:\s+[\'"](.+?)[\'"])?\s*$/', $line);
  220. }
  221. /**
  222. * Consume link references
  223. */
  224. protected function consumeReference($lines, $current)
  225. {
  226. while (isset($lines[$current]) && preg_match('/^ {0,3}\[(.+?)\]:\s*(.+?)(?:\s+[\(\'"](.+?)[\)\'"])?\s*$/', $lines[$current], $matches)) {
  227. $label = strtolower($matches[1]);
  228. $this->references[$label] = [
  229. 'url' => $matches[2],
  230. ];
  231. if (isset($matches[3])) {
  232. $this->references[$label]['title'] = $matches[3];
  233. } else {
  234. // title may be on the next line
  235. if (isset($lines[$current + 1]) && preg_match('/^\s+[\(\'"](.+?)[\)\'"]\s*$/', $lines[$current + 1], $matches)) {
  236. $this->references[$label]['title'] = $matches[1];
  237. $current++;
  238. }
  239. }
  240. $current++;
  241. }
  242. return [false, --$current];
  243. }
  244. }