Completed
Push — master ( 87972f...9ffe58 )
by Carsten
02:01
created

block/ListTrait.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\markdown\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 198
	protected function identifyOl($line)
26
	{
27 198
		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 199
	protected function identifyUl($line)
34
	{
35 199
		$l = $line[0];
36 199
		return ($l === '-' || $l === '+' || $l === '*') && (isset($line[1]) && (($l1 = $line[1]) === ' ' || $l1 === "\t")) ||
37 199
		       ($l === ' ' && preg_match('/^ {0,3}[\-\+\*][ \t]/', $line));
38
	}
39
40
	/**
41
	 * Consume lines for an ordered list
42
	 */
43 22 View Code Duplication
	protected function consumeOl($lines, $current)
0 ignored issues
show
This method seems to be duplicated in your project.

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.

Loading history...
44
	{
45
		// consume until newline
46
47
		$block = [
48 22
			'list',
49
			'list' => 'ol',
50
			'attr' => [],
51
			'items' => [],
52
		];
53 22
		return $this->consumeList($lines, $current, $block, 'ol');
54
	}
55
56
	/**
57
	 * Consume lines for an unordered list
58
	 */
59 36 View Code Duplication
	protected function consumeUl($lines, $current)
0 ignored issues
show
This method seems to be duplicated in your project.

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.

Loading history...
60
	{
61
		// consume until newline
62
63
		$block = [
64 36
			'list',
65
			'list' => 'ul',
66
			'items' => [],
67
		];
68 36
		return $this->consumeList($lines, $current, $block, 'ul');
69
	}
70
71 41
	private function consumeList($lines, $current, $block, $type)
72
	{
73 41
		$item = 0;
74 41
		$indent = '';
75 41
		$len = 0;
76 41
		$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 41
		$leadSpace = 3;
80 41
		$marker = $type === 'ul' ? ltrim($lines[$current])[0] : '';
81 41
		for ($i = $current, $count = count($lines); $i < $count; $i++) {
82 41
			$line = $lines[$i];
83
			// match list marker on the beginning of the line
84 41
			$pattern = ($type == 'ol') ? '/^( {0,'.$leadSpace.'})(\d+)\.[ \t]+/' : '/^( {0,'.$leadSpace.'})\\'.$marker.'[ \t]+/';
85 41
			if (preg_match($pattern, $line, $matches)) {
86 41
				if (($len = substr_count($matches[0], "\t")) > 0) {
87 10
					$indent = str_repeat("\t", $len);
88 10
					$line = substr($line, strlen($matches[0]));
89
				} else {
90 38
					$len = strlen($matches[0]);
91 38
					$indent = str_repeat(' ', $len);
92 38
					$line = substr($line, $len);
93
				}
94 41
				if ($i === $current) {
95 41
					$leadSpace = strlen($matches[1]) + 1;
96
				}
97
98 41
				if ($type == 'ol' && $this->keepListStartNumber) {
99
					// attr `start` for ol
100 2
					if (!isset($block['attr']['start']) && isset($matches[2])) {
101 2
						$block['attr']['start'] = $matches[2];
102
					}
103
				}
104
105 41
				$block['items'][++$item][] = $line;
106 41
				$block['lazyItems'][$item] = $lastLineEmpty;
107 41
				$lastLineEmpty = false;
108 38
			} elseif (ltrim($line) === '') {
109
				// line is empty, may be a lazy list
110 38
				$lastLineEmpty = true;
111
112
				// two empty lines will end the list
113 38
				if (!isset($lines[$i + 1][0])) {
114 23
					break;
115
116
				// next item is the continuation of this list -> lazy list
117 38
				} elseif (preg_match($pattern, $lines[$i + 1])) {
118 18
					$block['items'][$item][] = $line;
119 18
					$block['lazyItems'][$item] = true;
120
121
				// next item is indented as much as this list -> lazy list if it is not a reference
122 35
				} elseif (strncmp($lines[$i + 1], $indent, $len) === 0 || !empty($lines[$i + 1]) && $lines[$i + 1][0] == "\t") {
123 18
					$block['items'][$item][] = $line;
124 18
					$nextLine = $lines[$i + 1][0] === "\t" ? substr($lines[$i + 1], 1) : substr($lines[$i + 1], $len);
125 18
					$block['lazyItems'][$item] = empty($nextLine) || !method_exists($this, 'identifyReference') || !$this->identifyReference($nextLine);
126
127
				// everything else ends the list
128
				} else {
129 38
					break;
130
				}
131
			} else {
132 28
				if ($line[0] === "\t") {
133 7
					$line = substr($line, 1);
134 24
				} elseif (strncmp($line, $indent, $len) === 0) {
135 21
					$line = substr($line, $len);
136
				}
137 28
				$block['items'][$item][] = $line;
138 28
				$lastLineEmpty = false;
139
			}
140
		}
141
142 41
		foreach($block['items'] as $itemId => $itemLines) {
143 41
			$content = [];
144 41
			if (!$block['lazyItems'][$itemId]) {
145 35
				$firstPar = [];
146 35
				while (!empty($itemLines) && rtrim($itemLines[0]) !== '' && $this->detectLineType($itemLines, 0) === 'paragraph') {
147 35
					$firstPar[] = array_shift($itemLines);
148
				}
149 35
				$content = $this->parseInline(implode("\n", $firstPar));
150
			}
151 41
			if (!empty($itemLines)) {
152 28
				$content = array_merge($content, $this->parseBlocks($itemLines));
153
			}
154 41
			$block['items'][$itemId] = $content;
155
		}
156
157 41
		return [$block, $i];
158
	}
159
160
	/**
161
	 * Renders a list
162
	 */
163 41
	protected function renderList($block)
164
	{
165 41
		$type = $block['list'];
166
167 41
		if (!empty($block['attr'])) {
168 2
			$output = "<$type " . $this->generateHtmlAttributes($block['attr']) . ">\n";
169
		} else {
170 40
			$output = "<$type>\n";
171
		}
172
173 41
		foreach ($block['items'] as $item => $itemLines) {
174 41
			$output .= '<li>' . $this->renderAbsy($itemLines). "</li>\n";
175
		}
176 41
		return $output . "</$type>\n";
177
	}
178
179
180
	/**
181
	 * Return html attributes string from [attrName => attrValue] list
182
	 * @param array $attributes the attribute name-value pairs.
183
	 * @return string
184
	 */
185 2
	private function generateHtmlAttributes($attributes)
186
	{
187 2
		foreach ($attributes as $name => $value) {
188 2
			$attributes[$name] = "$name=\"$value\"";
189
		}
190 2
		return implode(' ', $attributes);
191
	}
192
193
	abstract protected function parseBlocks($lines);
194
	abstract protected function parseInline($text);
195
	abstract protected function renderAbsy($absy);
196
	abstract protected function detectLineType($lines, $current);
197
}
198