Table.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. <?php
  2. namespace dokuwiki\Parsing\Handler;
  3. class Table extends AbstractRewriter
  4. {
  5. protected $tableCalls = array();
  6. protected $maxCols = 0;
  7. protected $maxRows = 1;
  8. protected $currentCols = 0;
  9. protected $firstCell = false;
  10. protected $lastCellType = 'tablecell';
  11. protected $inTableHead = true;
  12. protected $currentRow = array('tableheader' => 0, 'tablecell' => 0);
  13. protected $countTableHeadRows = 0;
  14. /** @inheritdoc */
  15. public function finalise()
  16. {
  17. $last_call = end($this->calls);
  18. $this->writeCall(array('table_end',array(), $last_call[2]));
  19. $this->process();
  20. $this->callWriter->finalise();
  21. unset($this->callWriter);
  22. }
  23. /** @inheritdoc */
  24. public function process()
  25. {
  26. foreach ($this->calls as $call) {
  27. switch ($call[0]) {
  28. case 'table_start':
  29. $this->tableStart($call);
  30. break;
  31. case 'table_row':
  32. $this->tableRowClose($call);
  33. $this->tableRowOpen(array('tablerow_open',$call[1],$call[2]));
  34. break;
  35. case 'tableheader':
  36. case 'tablecell':
  37. $this->tableCell($call);
  38. break;
  39. case 'table_end':
  40. $this->tableRowClose($call);
  41. $this->tableEnd($call);
  42. break;
  43. default:
  44. $this->tableDefault($call);
  45. break;
  46. }
  47. }
  48. $this->callWriter->writeCalls($this->tableCalls);
  49. return $this->callWriter;
  50. }
  51. protected function tableStart($call)
  52. {
  53. $this->tableCalls[] = array('table_open',$call[1],$call[2]);
  54. $this->tableCalls[] = array('tablerow_open',array(),$call[2]);
  55. $this->firstCell = true;
  56. }
  57. protected function tableEnd($call)
  58. {
  59. $this->tableCalls[] = array('table_close',$call[1],$call[2]);
  60. $this->finalizeTable();
  61. }
  62. protected function tableRowOpen($call)
  63. {
  64. $this->tableCalls[] = $call;
  65. $this->currentCols = 0;
  66. $this->firstCell = true;
  67. $this->lastCellType = 'tablecell';
  68. $this->maxRows++;
  69. if ($this->inTableHead) {
  70. $this->currentRow = array('tablecell' => 0, 'tableheader' => 0);
  71. }
  72. }
  73. protected function tableRowClose($call)
  74. {
  75. if ($this->inTableHead && ($this->inTableHead = $this->isTableHeadRow())) {
  76. $this->countTableHeadRows++;
  77. }
  78. // Strip off final cell opening and anything after it
  79. while ($discard = array_pop($this->tableCalls)) {
  80. if ($discard[0] == 'tablecell_open' || $discard[0] == 'tableheader_open') {
  81. break;
  82. }
  83. if (!empty($this->currentRow[$discard[0]])) {
  84. $this->currentRow[$discard[0]]--;
  85. }
  86. }
  87. $this->tableCalls[] = array('tablerow_close', array(), $call[2]);
  88. if ($this->currentCols > $this->maxCols) {
  89. $this->maxCols = $this->currentCols;
  90. }
  91. }
  92. protected function isTableHeadRow()
  93. {
  94. $td = $this->currentRow['tablecell'];
  95. $th = $this->currentRow['tableheader'];
  96. if (!$th || $td > 2) return false;
  97. if (2*$td > $th) return false;
  98. return true;
  99. }
  100. protected function tableCell($call)
  101. {
  102. if ($this->inTableHead) {
  103. $this->currentRow[$call[0]]++;
  104. }
  105. if (!$this->firstCell) {
  106. // Increase the span
  107. $lastCall = end($this->tableCalls);
  108. // A cell call which follows an open cell means an empty cell so span
  109. if ($lastCall[0] == 'tablecell_open' || $lastCall[0] == 'tableheader_open') {
  110. $this->tableCalls[] = array('colspan',array(),$call[2]);
  111. }
  112. $this->tableCalls[] = array($this->lastCellType.'_close',array(),$call[2]);
  113. $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]);
  114. $this->lastCellType = $call[0];
  115. } else {
  116. $this->tableCalls[] = array($call[0].'_open',array(1,null,1),$call[2]);
  117. $this->lastCellType = $call[0];
  118. $this->firstCell = false;
  119. }
  120. $this->currentCols++;
  121. }
  122. protected function tableDefault($call)
  123. {
  124. $this->tableCalls[] = $call;
  125. }
  126. protected function finalizeTable()
  127. {
  128. // Add the max cols and rows to the table opening
  129. if ($this->tableCalls[0][0] == 'table_open') {
  130. // Adjust to num cols not num col delimeters
  131. $this->tableCalls[0][1][] = $this->maxCols - 1;
  132. $this->tableCalls[0][1][] = $this->maxRows;
  133. $this->tableCalls[0][1][] = array_shift($this->tableCalls[0][1]);
  134. } else {
  135. trigger_error('First element in table call list is not table_open');
  136. }
  137. $lastRow = 0;
  138. $lastCell = 0;
  139. $cellKey = array();
  140. $toDelete = array();
  141. // if still in tableheader, then there can be no table header
  142. // as all rows can't be within <THEAD>
  143. if ($this->inTableHead) {
  144. $this->inTableHead = false;
  145. $this->countTableHeadRows = 0;
  146. }
  147. // Look for the colspan elements and increment the colspan on the
  148. // previous non-empty opening cell. Once done, delete all the cells
  149. // that contain colspans
  150. for ($key = 0; $key < count($this->tableCalls); ++$key) {
  151. $call = $this->tableCalls[$key];
  152. switch ($call[0]) {
  153. case 'table_open':
  154. if ($this->countTableHeadRows) {
  155. array_splice($this->tableCalls, $key+1, 0, array(
  156. array('tablethead_open', array(), $call[2])));
  157. }
  158. break;
  159. case 'tablerow_open':
  160. $lastRow++;
  161. $lastCell = 0;
  162. break;
  163. case 'tablecell_open':
  164. case 'tableheader_open':
  165. $lastCell++;
  166. $cellKey[$lastRow][$lastCell] = $key;
  167. break;
  168. case 'table_align':
  169. $prev = in_array($this->tableCalls[$key-1][0], array('tablecell_open', 'tableheader_open'));
  170. $next = in_array($this->tableCalls[$key+1][0], array('tablecell_close', 'tableheader_close'));
  171. // If the cell is empty, align left
  172. if ($prev && $next) {
  173. $this->tableCalls[$key-1][1][1] = 'left';
  174. // If the previous element was a cell open, align right
  175. } elseif ($prev) {
  176. $this->tableCalls[$key-1][1][1] = 'right';
  177. // If the next element is the close of an element, align either center or left
  178. } elseif ($next) {
  179. if ($this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] == 'right') {
  180. $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'center';
  181. } else {
  182. $this->tableCalls[$cellKey[$lastRow][$lastCell]][1][1] = 'left';
  183. }
  184. }
  185. // Now convert the whitespace back to cdata
  186. $this->tableCalls[$key][0] = 'cdata';
  187. break;
  188. case 'colspan':
  189. $this->tableCalls[$key-1][1][0] = false;
  190. for ($i = $key-2; $i >= $cellKey[$lastRow][1]; $i--) {
  191. if ($this->tableCalls[$i][0] == 'tablecell_open' ||
  192. $this->tableCalls[$i][0] == 'tableheader_open'
  193. ) {
  194. if (false !== $this->tableCalls[$i][1][0]) {
  195. $this->tableCalls[$i][1][0]++;
  196. break;
  197. }
  198. }
  199. }
  200. $toDelete[] = $key-1;
  201. $toDelete[] = $key;
  202. $toDelete[] = $key+1;
  203. break;
  204. case 'rowspan':
  205. if ($this->tableCalls[$key-1][0] == 'cdata') {
  206. // ignore rowspan if previous call was cdata (text mixed with :::)
  207. // we don't have to check next call as that wont match regex
  208. $this->tableCalls[$key][0] = 'cdata';
  209. } else {
  210. $spanning_cell = null;
  211. // can't cross thead/tbody boundary
  212. if (!$this->countTableHeadRows || ($lastRow-1 != $this->countTableHeadRows)) {
  213. for ($i = $lastRow-1; $i > 0; $i--) {
  214. if ($this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tablecell_open' ||
  215. $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tableheader_open'
  216. ) {
  217. if ($this->tableCalls[$cellKey[$i][$lastCell]][1][2] >= $lastRow - $i) {
  218. $spanning_cell = $i;
  219. break;
  220. }
  221. }
  222. }
  223. }
  224. if (is_null($spanning_cell)) {
  225. // No spanning cell found, so convert this cell to
  226. // an empty one to avoid broken tables
  227. $this->tableCalls[$key][0] = 'cdata';
  228. $this->tableCalls[$key][1][0] = '';
  229. break;
  230. }
  231. $this->tableCalls[$cellKey[$spanning_cell][$lastCell]][1][2]++;
  232. $this->tableCalls[$key-1][1][2] = false;
  233. $toDelete[] = $key-1;
  234. $toDelete[] = $key;
  235. $toDelete[] = $key+1;
  236. }
  237. break;
  238. case 'tablerow_close':
  239. // Fix broken tables by adding missing cells
  240. $moreCalls = array();
  241. while (++$lastCell < $this->maxCols) {
  242. $moreCalls[] = array('tablecell_open', array(1, null, 1), $call[2]);
  243. $moreCalls[] = array('cdata', array(''), $call[2]);
  244. $moreCalls[] = array('tablecell_close', array(), $call[2]);
  245. }
  246. $moreCallsLength = count($moreCalls);
  247. if ($moreCallsLength) {
  248. array_splice($this->tableCalls, $key, 0, $moreCalls);
  249. $key += $moreCallsLength;
  250. }
  251. if ($this->countTableHeadRows == $lastRow) {
  252. array_splice($this->tableCalls, $key+1, 0, array(
  253. array('tablethead_close', array(), $call[2])));
  254. }
  255. break;
  256. }
  257. }
  258. // condense cdata
  259. $cnt = count($this->tableCalls);
  260. for ($key = 0; $key < $cnt; $key++) {
  261. if ($this->tableCalls[$key][0] == 'cdata') {
  262. $ckey = $key;
  263. $key++;
  264. while ($this->tableCalls[$key][0] == 'cdata') {
  265. $this->tableCalls[$ckey][1][0] .= $this->tableCalls[$key][1][0];
  266. $toDelete[] = $key;
  267. $key++;
  268. }
  269. continue;
  270. }
  271. }
  272. foreach ($toDelete as $delete) {
  273. unset($this->tableCalls[$delete]);
  274. }
  275. $this->tableCalls = array_values($this->tableCalls);
  276. }
  277. }