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