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
8namespace cebe\markdown\block;
9
10/**
11 * Adds the list blocks
12 */
13trait 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