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 ( 96 $x = $ccount; $x > $i; 97 $x-- 98 ) array_pop($this->calls); 99 } else { 100 // remove ending linebreaks in the paragraph 101 $i = count($this->calls) - 1; 102 if ($this->calls[$i][0] == 'cdata') $this->calls[$i][1][0] = rtrim($this->calls[$i][1][0], "\n"); 103 $this->calls[] = ['p_close', [], $pos]; 104 } 105 106 $this->inParagraph = false; 107 $this->skipEol = true; 108 } 109 110 protected function addCall($call) 111 { 112 $key = count($this->calls); 113 if ($key && $call[0] == 'cdata' && $this->calls[$key - 1][0] == 'cdata') { 114 $this->calls[$key - 1][1][0] .= $call[1][0]; 115 } else { 116 $this->calls[] = $call; 117 } 118 } 119 120 // simple version of addCall, without checking cdata 121 protected function storeCall($call) 122 { 123 $this->calls[] = $call; 124 } 125 126 /** 127 * Processes the whole instruction stack to open and close paragraphs 128 * 129 * @author Harry Fuecks <hfuecks@gmail.com> 130 * @author Andreas Gohr <andi@splitbrain.org> 131 * 132 * @param array $calls 133 * 134 * @return array 135 */ 136 public function process($calls) 137 { 138 // open first paragraph 139 $this->openParagraph(0); 140 foreach ($calls as $key => $call) { 141 $cname = $call[0]; 142 if ($cname == 'plugin') { 143 $cname = 'plugin_' . $call[1][0]; 144 $plugin = true; 145 $plugin_open = (($call[1][2] == DOKU_LEXER_ENTER) || ($call[1][2] == DOKU_LEXER_SPECIAL)); 146 $plugin_close = (($call[1][2] == DOKU_LEXER_EXIT) || ($call[1][2] == DOKU_LEXER_SPECIAL)); 147 } else { 148 $plugin = false; 149 } 150 /* stack */ 151 if (in_array($cname, $this->stackClose) && (!$plugin || $plugin_close)) { 152 $this->closeParagraph($call[2]); 153 $this->storeCall($call); 154 $this->openParagraph($call[2]); 155 continue; 156 } 157 if (in_array($cname, $this->stackOpen) && (!$plugin || $plugin_open)) { 158 $this->closeParagraph($call[2]); 159 $this->storeCall($call); 160 $this->openParagraph($call[2]); 161 continue; 162 } 163 /* block */ 164 // If it's a substition it opens and closes at the same call. 165 // To make sure next paragraph is correctly started, let close go first. 166 if (in_array($cname, $this->blockClose) && (!$plugin || $plugin_close)) { 167 $this->closeParagraph($call[2]); 168 $this->storeCall($call); 169 $this->openParagraph($call[2]); 170 continue; 171 } 172 if (in_array($cname, $this->blockOpen) && (!$plugin || $plugin_open)) { 173 $this->closeParagraph($call[2]); 174 $this->storeCall($call); 175 continue; 176 } 177 /* eol */ 178 if ($cname == 'eol') { 179 // Check this isn't an eol instruction to skip... 180 if (!$this->skipEol) { 181 // Next is EOL => double eol => mark as paragraph 182 if (isset($calls[$key + 1]) && $calls[$key + 1][0] == 'eol') { 183 $this->closeParagraph($call[2]); 184 $this->openParagraph($call[2]); 185 } else { 186 //if this is just a single eol make a space from it 187 $this->addCall(['cdata', ["\n"], $call[2]]); 188 } 189 } 190 continue; 191 } 192 /* normal */ 193 $this->addCall($call); 194 $this->skipEol = false; 195 } 196 // close last paragraph 197 $call = end($this->calls); 198 $this->closeParagraph($call[2]); 199 return $this->calls; 200 } 201} 202