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