1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @copyright Copyright (c) 2015 Nobuo Kihara |
4
|
|
|
* @license https://github.com/softark/creole/blob/master/LICENSE |
5
|
|
|
* @link https://github.com/softark/creole#readme |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace softark\creole\block; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* Adds the list blocks |
12
|
|
|
*/ |
13
|
|
|
trait ListTrait |
14
|
|
|
{ |
15
|
|
|
/** |
16
|
|
|
* @var int the current depth of the nested lists |
17
|
|
|
*/ |
18
|
|
|
private $_listDepth = 1; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* @var array the types of the nested lists |
22
|
|
|
*/ |
23
|
|
|
private $_nestedListTypes = []; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Identify a line as the beginning of an ordered list. |
27
|
|
|
* It should start with '#', with leading white spaces permitted. |
28
|
|
|
*/ |
29
|
23 |
|
protected function identifyOl($line) |
30
|
|
|
{ |
31
|
23 |
|
return preg_match('/^\s*#{' . $this->_listDepth . '}[^#]+/', $line); |
32
|
|
|
} |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Identify a line as the beginning of an unordered list. |
36
|
|
|
* It should start with '*', with leading white spaces permitted. |
37
|
|
|
*/ |
38
|
21 |
|
protected function identifyUl($line) |
39
|
|
|
{ |
40
|
21 |
|
return preg_match('/^\s*\*{' . $this->_listDepth . '}[^\*]+/', $line); |
41
|
|
|
} |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* check if a line is an item that belongs to a parent list |
45
|
|
|
* @param $line |
46
|
|
|
* @return bool true if the line is an item of a parent list |
47
|
|
|
*/ |
48
|
5 |
|
protected function isParentItem($line) |
49
|
|
|
{ |
50
|
5 |
|
if ($this->_listDepth === 1 || ($marker = $line[0]) !== '*' && $marker !== '#') { |
51
|
5 |
|
return false; |
52
|
|
|
} |
53
|
3 |
|
$depthMax = $this->_listDepth - 1; |
54
|
3 |
View Code Duplication |
if (preg_match('/^(#{1,' . $depthMax . '})[^#]+/', $line, $matches)) { |
|
|
|
|
55
|
|
|
return $this->_nestedListTypes[strlen($matches[1])] === 'ol'; |
56
|
|
|
} |
57
|
3 |
View Code Duplication |
if (preg_match('/^(\*{1,' . $depthMax . '})[^\*]+/', $line, $matches)) { |
|
|
|
|
58
|
2 |
|
return $this->_nestedListTypes[strlen($matches[1])] === 'ul'; |
59
|
|
|
} |
60
|
3 |
|
return false; |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* check if a line is an item that belongs to a sibling list |
65
|
|
|
* @param $line |
66
|
|
|
* @return bool true if the line is an item of a sibling list |
67
|
|
|
*/ |
68
|
5 |
|
protected function isSiblingItem($line) |
69
|
|
|
{ |
70
|
5 |
|
$siblingMarker = $this->_nestedListTypes[$this->_listDepth] == 'ol' ? '*' : '#'; |
71
|
5 |
|
if ($line[0] !== $siblingMarker) { |
72
|
5 |
|
return false; |
73
|
|
|
} |
74
|
|
|
return |
75
|
1 |
|
($siblingMarker === '#' && preg_match('/^#{' . $this->_listDepth . '}[^#]+/', $line)) || |
76
|
1 |
|
($siblingMarker === '*' && preg_match('/^\*{' . $this->_listDepth . '}[^\*]+/', $line)); |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Consume lines for an ordered list |
81
|
|
|
*/ |
82
|
4 |
View Code Duplication |
protected function consumeOl($lines, $current) |
|
|
|
|
83
|
|
|
{ |
84
|
|
|
// consume until newline |
85
|
|
|
|
86
|
|
|
$block = [ |
87
|
4 |
|
'list', |
88
|
|
|
'list' => 'ol', |
89
|
|
|
'items' => [], |
90
|
|
|
]; |
91
|
4 |
|
return $this->consumeList($lines, $current, $block, 'ol'); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* Consume lines for an unordered list |
96
|
|
|
*/ |
97
|
5 |
View Code Duplication |
protected function consumeUl($lines, $current) |
|
|
|
|
98
|
|
|
{ |
99
|
|
|
// consume until newline |
100
|
|
|
|
101
|
|
|
$block = [ |
102
|
5 |
|
'list', |
103
|
|
|
'list' => 'ul', |
104
|
|
|
'items' => [], |
105
|
|
|
]; |
106
|
5 |
|
return $this->consumeList($lines, $current, $block, 'ul'); |
107
|
|
|
} |
108
|
|
|
|
109
|
5 |
|
private function consumeList($lines, $current, $block, $type) |
110
|
|
|
{ |
111
|
5 |
|
$this->_nestedListTypes[$this->_listDepth] = $type; |
112
|
5 |
|
$item = 0; |
113
|
5 |
|
$pattern = $type === 'ul' ? '/^\*{' . $this->_listDepth . '}([^\*]+.*|)$/' : '/^#{' . $this->_listDepth . '}([^#]+.*|)$/'; |
114
|
5 |
|
for ($i = $current, $count = count($lines); $i < $count; $i++) { |
115
|
5 |
|
$line = ltrim($lines[$i]); |
116
|
|
|
// A list ends with a blank new line, other block elements, a parent item, or a sibling list. |
117
|
5 |
|
if ($line === '' || |
118
|
5 |
|
$this->identifyHeadline($line) || |
119
|
5 |
|
$this->identifyHr($line) || |
120
|
5 |
|
$this->identifyTable($line) || |
121
|
5 |
|
$this->identifyCode($line) || |
122
|
5 |
|
$this->isParentItem($line) || |
123
|
5 |
|
$this->isSiblingItem($line) |
124
|
|
|
) { |
125
|
|
|
// list ended |
126
|
5 |
|
$i--; |
127
|
5 |
|
break; |
128
|
|
|
} |
129
|
5 |
|
if (preg_match($pattern, $line)) { |
130
|
|
|
// match list marker on the beginning of the line ... the next item begins |
131
|
5 |
|
$line = ltrim(substr($line, $this->_listDepth)); |
132
|
5 |
|
$block['items'][++$item][] = $line; |
133
|
|
|
} else { |
134
|
|
|
// child list? |
135
|
4 |
|
$this->_listDepth++; |
136
|
4 |
|
if ($this->identifyOl($line)) { |
137
|
3 |
|
list($childBlock, $i) = $this->consumeOl($lines, $i); |
138
|
3 |
|
$block['items'][$item][] = $childBlock; |
139
|
4 |
|
} elseif ($this->identifyUl($line)) { |
140
|
3 |
|
list($childBlock, $i) = $this->consumeUl($lines, $i); |
141
|
3 |
|
$block['items'][$item][] = $childBlock; |
142
|
|
|
} else { |
143
|
|
|
// the continuing content of the current item |
144
|
3 |
|
$line = ltrim($line); |
145
|
3 |
|
$block['items'][$item][] = $line; |
146
|
|
|
} |
147
|
4 |
|
$this->_listDepth--; |
148
|
|
|
} |
149
|
|
|
} |
150
|
|
|
|
151
|
5 |
|
foreach ($block['items'] as $itemId => $itemLines) { |
152
|
5 |
|
$content = []; |
153
|
5 |
|
$texts = []; |
154
|
5 |
|
foreach ($itemLines as $line) { |
155
|
5 |
|
if (!isset($line['list'])) { |
156
|
5 |
|
$texts[] = $line; |
157
|
|
|
} else { |
158
|
|
|
// child list |
159
|
3 |
|
if (!empty($texts)) { |
160
|
|
|
// text before child list |
161
|
3 |
|
$content = array_merge($content, $this->parseInline(implode("\n", $texts))); |
162
|
3 |
|
$texts = []; |
163
|
|
|
} |
164
|
5 |
|
$content[] = $line; |
165
|
|
|
} |
166
|
|
|
} |
167
|
5 |
|
if (!empty($texts)) { |
168
|
5 |
|
$content = array_merge($content, $this->parseInline(implode("\n", $texts))); |
169
|
|
|
} |
170
|
5 |
|
$block['items'][$itemId] = $content; |
171
|
|
|
} |
172
|
|
|
|
173
|
5 |
|
return [$block, $i]; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Renders a list |
178
|
|
|
*/ |
179
|
5 |
|
protected function renderList($block) |
180
|
|
|
{ |
181
|
5 |
|
$type = $block['list']; |
182
|
5 |
|
$output = "<$type>\n"; |
183
|
5 |
|
foreach ($block['items'] as $item => $itemLines) { |
184
|
5 |
|
$output .= '<li>' . $this->renderAbsy($itemLines) . "</li>\n"; |
185
|
|
|
} |
186
|
5 |
|
return $output . "</$type>\n"; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
abstract protected function parseInline($text); |
190
|
|
|
|
191
|
|
|
abstract protected function renderAbsy($absy); |
192
|
|
|
|
193
|
|
|
abstract protected function identifyHeadline($line); |
194
|
|
|
|
195
|
|
|
abstract protected function identifyHr($line); |
196
|
|
|
|
197
|
|
|
abstract protected function identifyTable($line); |
198
|
|
|
|
199
|
|
|
abstract protected function identifyCode($line); |
200
|
|
|
} |
201
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.