ListTrait.php 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  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\block;
  8. /**
  9. * Adds the list blocks
  10. */
  11. trait ListTrait
  12. {
  13. /**
  14. * @var bool enable support `start` attribute of ordered lists. This means that lists
  15. * will start with the number you actually type in markdown and not the HTML generated one.
  16. * Defaults to `false` which means that numeration of all ordered lists(<ol>) starts with 1.
  17. */
  18. public $keepListStartNumber = false;
  19. /**
  20. * identify a line as the beginning of an ordered list.
  21. */
  22. protected function identifyOl($line)
  23. {
  24. return (($l = $line[0]) > '0' && $l <= '9' || $l === ' ') && preg_match('/^ {0,3}\d+\.[ \t]/', $line);
  25. }
  26. /**
  27. * identify a line as the beginning of an unordered list.
  28. */
  29. protected function identifyUl($line)
  30. {
  31. $l = $line[0];
  32. return ($l === '-' || $l === '+' || $l === '*') && (isset($line[1]) && (($l1 = $line[1]) === ' ' || $l1 === "\t")) ||
  33. ($l === ' ' && preg_match('/^ {0,3}[\-\+\*][ \t]/', $line));
  34. }
  35. /**
  36. * Consume lines for an ordered list
  37. */
  38. protected function consumeOl($lines, $current)
  39. {
  40. // consume until newline
  41. $block = [
  42. 'list',
  43. 'list' => 'ol',
  44. 'attr' => [],
  45. 'items' => [],
  46. ];
  47. return $this->consumeList($lines, $current, $block, 'ol');
  48. }
  49. /**
  50. * Consume lines for an unordered list
  51. */
  52. protected function consumeUl($lines, $current)
  53. {
  54. // consume until newline
  55. $block = [
  56. 'list',
  57. 'list' => 'ul',
  58. 'items' => [],
  59. ];
  60. return $this->consumeList($lines, $current, $block, 'ul');
  61. }
  62. private function consumeList($lines, $current, $block, $type)
  63. {
  64. $item = 0;
  65. $indent = '';
  66. $len = 0;
  67. $lastLineEmpty = false;
  68. // track the indentation of list markers, if indented more than previous element
  69. // a list marker is considered to be long to a lower level
  70. $leadSpace = 3;
  71. $marker = $type === 'ul' ? ltrim($lines[$current])[0] : '';
  72. for ($i = $current, $count = count($lines); $i < $count; $i++) {
  73. $line = $lines[$i];
  74. // match list marker on the beginning of the line
  75. $pattern = ($type === 'ol') ? '/^( {0,'.$leadSpace.'})(\d+)\.[ \t]+/' : '/^( {0,'.$leadSpace.'})\\'.$marker.'[ \t]+/';
  76. if (preg_match($pattern, $line, $matches)) {
  77. if (($len = substr_count($matches[0], "\t")) > 0) {
  78. $indent = str_repeat("\t", $len);
  79. $line = substr($line, strlen($matches[0]));
  80. } else {
  81. $len = strlen($matches[0]);
  82. $indent = str_repeat(' ', $len);
  83. $line = substr($line, $len);
  84. }
  85. if ($i === $current) {
  86. $leadSpace = strlen($matches[1]) + 1;
  87. }
  88. if ($type === 'ol' && $this->keepListStartNumber) {
  89. // attr `start` for ol
  90. if (!isset($block['attr']['start']) && isset($matches[2])) {
  91. $block['attr']['start'] = $matches[2];
  92. }
  93. }
  94. $block['items'][++$item][] = $line;
  95. $block['lazyItems'][$item] = $lastLineEmpty;
  96. $lastLineEmpty = false;
  97. } elseif (ltrim($line) === '') {
  98. // line is empty, may be a lazy list
  99. $lastLineEmpty = true;
  100. // two empty lines will end the list
  101. if (!isset($lines[$i + 1][0])) {
  102. break;
  103. // next item is the continuation of this list -> lazy list
  104. } elseif (preg_match($pattern, $lines[$i + 1])) {
  105. $block['items'][$item][] = $line;
  106. $block['lazyItems'][$item] = true;
  107. // next item is indented as much as this list -> lazy list if it is not a reference
  108. } elseif (strncmp($lines[$i + 1], $indent, $len) === 0 || !empty($lines[$i + 1]) && $lines[$i + 1][0] == "\t") {
  109. $block['items'][$item][] = $line;
  110. $nextLine = $lines[$i + 1][0] === "\t" ? substr($lines[$i + 1], 1) : substr($lines[$i + 1], $len);
  111. $block['lazyItems'][$item] = empty($nextLine) || !method_exists($this, 'identifyReference') || !$this->identifyReference($nextLine);
  112. // everything else ends the list
  113. } else {
  114. break;
  115. }
  116. } else {
  117. if ($line[0] === "\t") {
  118. $line = substr($line, 1);
  119. } elseif (strncmp($line, $indent, $len) === 0) {
  120. $line = substr($line, $len);
  121. }
  122. $block['items'][$item][] = $line;
  123. $lastLineEmpty = false;
  124. }
  125. // if next line is <hr>, end the list
  126. if (!empty($lines[$i + 1]) && method_exists($this, 'identifyHr') && $this->identifyHr($lines[$i + 1])) {
  127. break;
  128. }
  129. }
  130. foreach($block['items'] as $itemId => $itemLines) {
  131. $content = [];
  132. if (!$block['lazyItems'][$itemId]) {
  133. $firstPar = [];
  134. while (!empty($itemLines) && rtrim($itemLines[0]) !== '' && $this->detectLineType($itemLines, 0) === 'paragraph') {
  135. $firstPar[] = array_shift($itemLines);
  136. }
  137. $content = $this->parseInline(implode("\n", $firstPar));
  138. }
  139. if (!empty($itemLines)) {
  140. $content = array_merge($content, $this->parseBlocks($itemLines));
  141. }
  142. $block['items'][$itemId] = $content;
  143. }
  144. return [$block, $i];
  145. }
  146. /**
  147. * Renders a list
  148. */
  149. protected function renderList($block)
  150. {
  151. $type = $block['list'];
  152. if (!empty($block['attr'])) {
  153. $output = "<$type " . $this->generateHtmlAttributes($block['attr']) . ">\n";
  154. } else {
  155. $output = "<$type>\n";
  156. }
  157. foreach ($block['items'] as $item => $itemLines) {
  158. $output .= '<li>' . $this->renderAbsy($itemLines). "</li>\n";
  159. }
  160. return $output . "</$type>\n";
  161. }
  162. /**
  163. * Return html attributes string from [attrName => attrValue] list
  164. * @param array $attributes the attribute name-value pairs.
  165. * @return string
  166. */
  167. private function generateHtmlAttributes($attributes)
  168. {
  169. foreach ($attributes as $name => $value) {
  170. $attributes[$name] = "$name=\"$value\"";
  171. }
  172. return implode(' ', $attributes);
  173. }
  174. abstract protected function parseBlocks($lines);
  175. abstract protected function parseInline($text);
  176. abstract protected function renderAbsy($absy);
  177. abstract protected function detectLineType($lines, $current);
  178. }