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