You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

321 lines
12 KiB

  1. <?php
  2. namespace dokuwiki\Parsing\Handler;
  3. class Table extends AbstractRewriter
  4. {
  5. protected $tableCalls = [];
  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 = ['tableheader' => 0, 'tablecell' => 0];
  13. protected $countTableHeadRows = 0;
  14. /** @inheritdoc */
  15. public function finalise()
  16. {
  17. $last_call = end($this->calls);
  18. $this->writeCall(['table_end', [], $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(['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[] = ['table_open', $call[1], $call[2]];
  54. $this->tableCalls[] = ['tablerow_open', [], $call[2]];
  55. $this->firstCell = true;
  56. }
  57. protected function tableEnd($call)
  58. {
  59. $this->tableCalls[] = ['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 = ['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[] = ['tablerow_close', [], $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[] = ['colspan', [], $call[2]];
  111. }
  112. $this->tableCalls[] = [$this->lastCellType . '_close', [], $call[2]];
  113. $this->tableCalls[] = [$call[0] . '_open', [1, null, 1], $call[2]];
  114. $this->lastCellType = $call[0];
  115. } else {
  116. $this->tableCalls[] = [$call[0] . '_open', [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 = [];
  140. $toDelete = [];
  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. $key = -1;
  151. while (++$key < count($this->tableCalls)) {
  152. $call = $this->tableCalls[$key];
  153. switch ($call[0]) {
  154. case 'table_open':
  155. if ($this->countTableHeadRows) {
  156. array_splice($this->tableCalls, $key + 1, 0, [['tablethead_open', [], $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], ['tablecell_open', 'tableheader_open']);
  170. $next = in_array($this->tableCalls[$key + 1][0], ['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 (
  192. $this->tableCalls[$i][0] == 'tablecell_open' ||
  193. $this->tableCalls[$i][0] == 'tableheader_open'
  194. ) {
  195. if (false !== $this->tableCalls[$i][1][0]) {
  196. $this->tableCalls[$i][1][0]++;
  197. break;
  198. }
  199. }
  200. }
  201. $toDelete[] = $key - 1;
  202. $toDelete[] = $key;
  203. $toDelete[] = $key + 1;
  204. break;
  205. case 'rowspan':
  206. if ($this->tableCalls[$key - 1][0] == 'cdata') {
  207. // ignore rowspan if previous call was cdata (text mixed with :::)
  208. // we don't have to check next call as that wont match regex
  209. $this->tableCalls[$key][0] = 'cdata';
  210. } else {
  211. $spanning_cell = null;
  212. // can't cross thead/tbody boundary
  213. if (!$this->countTableHeadRows || ($lastRow - 1 != $this->countTableHeadRows)) {
  214. for ($i = $lastRow - 1; $i > 0; $i--) {
  215. if (
  216. $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tablecell_open' ||
  217. $this->tableCalls[$cellKey[$i][$lastCell]][0] == 'tableheader_open'
  218. ) {
  219. if ($this->tableCalls[$cellKey[$i][$lastCell]][1][2] >= $lastRow - $i) {
  220. $spanning_cell = $i;
  221. break;
  222. }
  223. }
  224. }
  225. }
  226. if (is_null($spanning_cell)) {
  227. // No spanning cell found, so convert this cell to
  228. // an empty one to avoid broken tables
  229. $this->tableCalls[$key][0] = 'cdata';
  230. $this->tableCalls[$key][1][0] = '';
  231. break;
  232. }
  233. $this->tableCalls[$cellKey[$spanning_cell][$lastCell]][1][2]++;
  234. $this->tableCalls[$key - 1][1][2] = false;
  235. $toDelete[] = $key - 1;
  236. $toDelete[] = $key;
  237. $toDelete[] = $key + 1;
  238. }
  239. break;
  240. case 'tablerow_close':
  241. // Fix broken tables by adding missing cells
  242. $moreCalls = [];
  243. while (++$lastCell < $this->maxCols) {
  244. $moreCalls[] = ['tablecell_open', [1, null, 1], $call[2]];
  245. $moreCalls[] = ['cdata', [''], $call[2]];
  246. $moreCalls[] = ['tablecell_close', [], $call[2]];
  247. }
  248. $moreCallsLength = count($moreCalls);
  249. if ($moreCallsLength) {
  250. array_splice($this->tableCalls, $key, 0, $moreCalls);
  251. $key += $moreCallsLength;
  252. }
  253. if ($this->countTableHeadRows == $lastRow) {
  254. array_splice($this->tableCalls, $key + 1, 0, [['tablethead_close', [], $call[2]]]);
  255. }
  256. break;
  257. }
  258. }
  259. // condense cdata
  260. $cnt = count($this->tableCalls);
  261. for ($key = 0; $key < $cnt; $key++) {
  262. if ($this->tableCalls[$key][0] == 'cdata') {
  263. $ckey = $key;
  264. $key++;
  265. while ($this->tableCalls[$key][0] == 'cdata') {
  266. $this->tableCalls[$ckey][1][0] .= $this->tableCalls[$key][1][0];
  267. $toDelete[] = $key;
  268. $key++;
  269. }
  270. continue;
  271. }
  272. }
  273. foreach ($toDelete as $delete) {
  274. unset($this->tableCalls[$delete]);
  275. }
  276. $this->tableCalls = array_values($this->tableCalls);
  277. }
  278. }