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