1<?php 2 3namespace dokuwiki\Parsing\Handler; 4 5/** 6 * Handler for paragraphs 7 * 8 * @author Harry Fuecks <hfuecks@gmail.com> 9 */ 10class Block 11{ 12 protected $calls = []; 13 protected $skipEol = false; 14 protected $inParagraph = false; 15 16 /** 17 * Strip leading [ \t]+ from the next cdata that lands in a paragraph. 18 * Set when a paragraph opens (drops indent on the first line) and after 19 * a soft-break joiner is emitted (drops indent on continuation lines). 20 */ 21 protected $trimNextCdataLeft = false; 22 23 // Blocks these should not be inside paragraphs 24 protected $blockOpen = [ 25 'header', 'listu_open', 'listo_open', 'listo_open_start', 'listitem_open', 'listcontent_open', 26 'table_open', 'tablerow_open', 'tablecell_open', 'tableheader_open', 'tablethead_open', 'tabletbody_open', 27 'tabletfoot_open', 'quote_open', 'code', 'file', 'hr', 'preformatted', 'rss', 'footnote_open' 28 ]; 29 30 protected $blockClose = [ 31 'header', 'listu_close', 'listo_close', 'listitem_close', 'listcontent_close', 'table_close', 32 'tablerow_close', 'tablecell_close', 'tableheader_close', 'tablethead_close', 'tabletbody_close', 33 'tabletfoot_close', 'quote_close', 'code', 'file', 'hr', 'preformatted', 'rss', 'footnote_close' 34 ]; 35 36 // Stacks can contain paragraphs 37 protected $stackOpen = ['section_open']; 38 39 protected $stackClose = ['section_close']; 40 41 42 /** 43 * Constructor. Adds loaded syntax plugins to the block and stack 44 * arrays 45 * 46 * @author Andreas Gohr <andi@splitbrain.org> 47 */ 48 public function __construct() 49 { 50 global $DOKU_PLUGINS; 51 //check if syntax plugins were loaded 52 if (empty($DOKU_PLUGINS['syntax'])) return; 53 foreach ($DOKU_PLUGINS['syntax'] as $n => $p) { 54 $ptype = $p->getPType(); 55 if ($ptype == 'block') { 56 $this->blockOpen[] = 'plugin_' . $n; 57 $this->blockClose[] = 'plugin_' . $n; 58 } elseif ($ptype == 'stack') { 59 $this->stackOpen[] = 'plugin_' . $n; 60 $this->stackClose[] = 'plugin_' . $n; 61 } 62 } 63 } 64 65 protected function openParagraph($pos) 66 { 67 if ($this->inParagraph) return; 68 $this->calls[] = ['p_open', [], $pos]; 69 $this->inParagraph = true; 70 $this->skipEol = true; 71 $this->trimNextCdataLeft = true; 72 } 73 74 /** 75 * Close a paragraph if needed 76 * 77 * This function makes sure there are no empty paragraphs on the stack 78 * 79 * @author Andreas Gohr <andi@splitbrain.org> 80 * 81 * @param string|integer $pos 82 */ 83 protected function closeParagraph($pos) 84 { 85 if (!$this->inParagraph) return; 86 // look back if there was any content - we don't want empty paragraphs 87 $content = ''; 88 $ccount = count($this->calls); 89 for ($i = $ccount - 1; $i >= 0; $i--) { 90 if ($this->calls[$i][0] == 'p_open') { 91 break; 92 } elseif ($this->calls[$i][0] == 'cdata') { 93 $content .= $this->calls[$i][1][0]; 94 } else { 95 $content = 'found markup'; 96 break; 97 } 98 } 99 100 if (trim($content) == '') { 101 //remove the whole paragraph 102 //array_splice($this->calls,$i); // <- this is much slower than the loop below 103 for ( 104 $x = $ccount; $x > $i; 105 $x-- 106 ) array_pop($this->calls); 107 } else { 108 // remove ending linebreaks in the paragraph 109 $last = array_key_last($this->calls); 110 if ($this->calls[$last][0] == 'cdata') { 111 $this->calls[$last][1][0] = rtrim($this->calls[$last][1][0], "\n"); 112 } 113 $this->calls[] = ['p_close', [], $pos]; 114 } 115 116 $this->inParagraph = false; 117 $this->skipEol = true; 118 $this->trimNextCdataLeft = false; 119 } 120 121 protected function addCall($call) 122 { 123 if ($call[0] == 'cdata' && $this->trimNextCdataLeft) { 124 $call[1][0] = ltrim($call[1][0], " \t"); 125 } 126 $this->trimNextCdataLeft = false; 127 128 $last = array_key_last($this->calls); 129 if ($call[0] == 'cdata' && $this->calls[$last][0] == 'cdata') { 130 $this->calls[$last][1][0] .= $call[1][0]; 131 } else { 132 $this->calls[] = $call; 133 } 134 } 135 136 // simple version of addCall, without checking cdata 137 protected function storeCall($call) 138 { 139 $this->calls[] = $call; 140 } 141 142 /** 143 * Processes the whole instruction stack to open and close paragraphs 144 * 145 * @author Harry Fuecks <hfuecks@gmail.com> 146 * @author Andreas Gohr <andi@splitbrain.org> 147 * 148 * @param array $calls 149 * 150 * @return array 151 */ 152 public function process($calls) 153 { 154 // open first paragraph 155 $this->openParagraph(0); 156 foreach ($calls as $key => $call) { 157 $cname = $call[0]; 158 if ($cname == 'plugin') { 159 $cname = 'plugin_' . $call[1][0]; 160 $plugin = true; 161 $plugin_open = (($call[1][2] == DOKU_LEXER_ENTER) || ($call[1][2] == DOKU_LEXER_SPECIAL)); 162 $plugin_close = (($call[1][2] == DOKU_LEXER_EXIT) || ($call[1][2] == DOKU_LEXER_SPECIAL)); 163 } else { 164 $plugin = false; 165 } 166 /* stack */ 167 if (in_array($cname, $this->stackClose) && (!$plugin || $plugin_close)) { 168 $this->closeParagraph($call[2]); 169 $this->storeCall($call); 170 $this->openParagraph($call[2]); 171 continue; 172 } 173 if (in_array($cname, $this->stackOpen) && (!$plugin || $plugin_open)) { 174 $this->closeParagraph($call[2]); 175 $this->storeCall($call); 176 $this->openParagraph($call[2]); 177 continue; 178 } 179 /* block */ 180 // If it's a substition it opens and closes at the same call. 181 // To make sure next paragraph is correctly started, let close go first. 182 if (in_array($cname, $this->blockClose) && (!$plugin || $plugin_close)) { 183 $this->closeParagraph($call[2]); 184 $this->storeCall($call); 185 $this->openParagraph($call[2]); 186 continue; 187 } 188 if (in_array($cname, $this->blockOpen) && (!$plugin || $plugin_open)) { 189 $this->closeParagraph($call[2]); 190 $this->storeCall($call); 191 continue; 192 } 193 /* eol */ 194 if ($cname == 'eol') { 195 // Check this isn't an eol instruction to skip... 196 if (!$this->skipEol) { 197 // Next is EOL => double eol => mark as paragraph 198 if (isset($calls[$key + 1]) && $calls[$key + 1][0] == 'eol') { 199 $this->closeParagraph($call[2]); 200 $this->openParagraph($call[2]); 201 } else { 202 // single eol → soft break inside a paragraph. 203 // Strip [ \t]+ from the previous cdata's tail and mark 204 // the next cdata to be left-trimmed, so paragraph wrap 205 // collapses [ \t]*\n[ \t]* to a single \n. 206 $last = array_key_last($this->calls); 207 if ($this->calls[$last][0] == 'cdata') { 208 $this->calls[$last][1][0] = rtrim($this->calls[$last][1][0], " \t"); 209 } 210 $this->addCall(['cdata', ["\n"], $call[2]]); 211 $this->trimNextCdataLeft = true; 212 } 213 } 214 continue; 215 } 216 /* normal */ 217 $this->addCall($call); 218 $this->skipEol = false; 219 } 220 // close last paragraph 221 $call = end($this->calls); 222 $this->closeParagraph($call[2]); 223 return $this->calls; 224 } 225} 226