1*3dabe4e0SAndreas Gohr<?php 2*3dabe4e0SAndreas Gohr 3*3dabe4e0SAndreas Gohrnamespace dokuwiki\Parsing\Handler; 4*3dabe4e0SAndreas Gohr 5*3dabe4e0SAndreas Gohr/** 6*3dabe4e0SAndreas Gohr * CallWriter rewriter for GFM tables. 7*3dabe4e0SAndreas Gohr * 8*3dabe4e0SAndreas Gohr * GfmTable's lexer state emits a flat token stream of marker calls 9*3dabe4e0SAndreas Gohr * (`gfm_table_start`, `gfm_table_row`, `gfm_table_cell`, `gfm_table_end`) 10*3dabe4e0SAndreas Gohr * interleaved with whatever inline modes (emphasis, code spans, links, …) 11*3dabe4e0SAndreas Gohr * matched inside the cells. This rewriter: 12*3dabe4e0SAndreas Gohr * 13*3dabe4e0SAndreas Gohr * 1. Groups the flat stream into rows-of-cells, where each cell carries 14*3dabe4e0SAndreas Gohr * its own list of nested handler calls. 15*3dabe4e0SAndreas Gohr * 2. Strips the empty leading and trailing cells that result from leading 16*3dabe4e0SAndreas Gohr * and trailing pipes (`| a | b |` → cells `["", " a ", " b ", ""]` → 17*3dabe4e0SAndreas Gohr * `[" a ", " b "]`). 18*3dabe4e0SAndreas Gohr * 3. Parses the second row as the GFM delimiter row, deriving per-column 19*3dabe4e0SAndreas Gohr * alignment from `:-+:?` patterns and the column count from the cell 20*3dabe4e0SAndreas Gohr * count. 21*3dabe4e0SAndreas Gohr * 4. Validates that the header row's cell count matches the delimiter's. 22*3dabe4e0SAndreas Gohr * On mismatch (spec example 203), emits the captured text back as a 23*3dabe4e0SAndreas Gohr * single cdata so the Block rewriter wraps it in a paragraph. 24*3dabe4e0SAndreas Gohr * 5. Pads body rows that are short (spec 202) and truncates body rows 25*3dabe4e0SAndreas Gohr * that are long (spec 204) to the header's column count. 26*3dabe4e0SAndreas Gohr * 6. Trims leading/trailing whitespace from each cell's edge cdata calls 27*3dabe4e0SAndreas Gohr * ("Spaces between pipes and cell content are trimmed"). 28*3dabe4e0SAndreas Gohr * 7. Emits the canonical DokuWiki table call sequence — `table_open`, 29*3dabe4e0SAndreas Gohr * `tablethead_open`, `tablerow_open`, per-column `tableheader_open` 30*3dabe4e0SAndreas Gohr * with alignment, `tablethead_close`, then (only when there are 31*3dabe4e0SAndreas Gohr * body rows — spec 205) `tabletbody_open`, per-row `tablerow_open` 32*3dabe4e0SAndreas Gohr * with `tablecell_open`, `tabletbody_close`, and finally 33*3dabe4e0SAndreas Gohr * `table_close`. No new handler instructions are introduced; 34*3dabe4e0SAndreas Gohr * `tabletbody_open` / `tabletbody_close` are part of DokuWiki's 35*3dabe4e0SAndreas Gohr * base renderer API but were never emitted before — DW Table omits 36*3dabe4e0SAndreas Gohr * `<tbody>` entirely. Activating them here is what frees the test 37*3dabe4e0SAndreas Gohr * renderer from having to track tbody state. 38*3dabe4e0SAndreas Gohr * 39*3dabe4e0SAndreas Gohr * Backslash-escaped pipes (`\|`) are not unescaped here — that is 40*3dabe4e0SAndreas Gohr * GfmEscape's responsibility and applies project-wide. Until that mode 41*3dabe4e0SAndreas Gohr * lands, the literal `\|` survives in cell content. The lexer's cell- 42*3dabe4e0SAndreas Gohr * separator lookbehind ensures the escape at least keeps cells from 43*3dabe4e0SAndreas Gohr * being split on the protected pipe (spec 200, partially). 44*3dabe4e0SAndreas Gohr */ 45*3dabe4e0SAndreas Gohrclass GfmTable extends AbstractRewriter 46*3dabe4e0SAndreas Gohr{ 47*3dabe4e0SAndreas Gohr /** @inheritdoc */ 48*3dabe4e0SAndreas Gohr protected function getClosingCall(): string 49*3dabe4e0SAndreas Gohr { 50*3dabe4e0SAndreas Gohr return 'gfm_table_end'; 51*3dabe4e0SAndreas Gohr } 52*3dabe4e0SAndreas Gohr 53*3dabe4e0SAndreas Gohr /** @inheritdoc */ 54*3dabe4e0SAndreas Gohr public function process() 55*3dabe4e0SAndreas Gohr { 56*3dabe4e0SAndreas Gohr ['rows' => $rows, 'startPos' => $startPos, 'endPos' => $endPos] = $this->groupRows(); 57*3dabe4e0SAndreas Gohr $rows = array_map($this->stripBoundaryEmpty(...), $rows); 58*3dabe4e0SAndreas Gohr 59*3dabe4e0SAndreas Gohr $alignments = array_map( 60*3dabe4e0SAndreas Gohr fn($cell) => $this->parseAlign($this->cellText($cell)), 61*3dabe4e0SAndreas Gohr $rows[1] 62*3dabe4e0SAndreas Gohr ); 63*3dabe4e0SAndreas Gohr $cols = count($alignments); 64*3dabe4e0SAndreas Gohr 65*3dabe4e0SAndreas Gohr // Header / delimiter column-count mismatch is the spec-203 fallback. 66*3dabe4e0SAndreas Gohr if (count($rows[0]) !== $cols) { 67*3dabe4e0SAndreas Gohr $this->emitFallback($rows, $startPos); 68*3dabe4e0SAndreas Gohr return $this->callWriter; 69*3dabe4e0SAndreas Gohr } 70*3dabe4e0SAndreas Gohr 71*3dabe4e0SAndreas Gohr $headerRow = $this->trimCellEdges($rows[0]); 72*3dabe4e0SAndreas Gohr $bodyRows = array_map( 73*3dabe4e0SAndreas Gohr fn($row) => $this->trimCellEdges($this->padOrTruncate($row, $cols)), 74*3dabe4e0SAndreas Gohr array_slice($rows, 2) 75*3dabe4e0SAndreas Gohr ); 76*3dabe4e0SAndreas Gohr 77*3dabe4e0SAndreas Gohr $out = $this->buildOutput($headerRow, $bodyRows, $alignments, $cols, $startPos, $endPos); 78*3dabe4e0SAndreas Gohr $this->callWriter->writeCalls($out); 79*3dabe4e0SAndreas Gohr return $this->callWriter; 80*3dabe4e0SAndreas Gohr } 81*3dabe4e0SAndreas Gohr 82*3dabe4e0SAndreas Gohr /** 83*3dabe4e0SAndreas Gohr * Walk $this->calls and bucket them into rows-of-cells-of-calls. 84*3dabe4e0SAndreas Gohr * 85*3dabe4e0SAndreas Gohr * @return array{rows: array<int, array<int, array<int, array>>>, startPos: int, endPos: int} 86*3dabe4e0SAndreas Gohr * `rows[r][c]` is a list of handler calls captured inside row `r`'s 87*3dabe4e0SAndreas Gohr * cell `c`. `startPos` and `endPos` carry the table's opening and 88*3dabe4e0SAndreas Gohr * closing source positions. 89*3dabe4e0SAndreas Gohr */ 90*3dabe4e0SAndreas Gohr protected function groupRows(): array 91*3dabe4e0SAndreas Gohr { 92*3dabe4e0SAndreas Gohr $rows = []; 93*3dabe4e0SAndreas Gohr $rowIdx = -1; 94*3dabe4e0SAndreas Gohr $startPos = 0; 95*3dabe4e0SAndreas Gohr $endPos = 0; 96*3dabe4e0SAndreas Gohr 97*3dabe4e0SAndreas Gohr foreach ($this->calls as $call) { 98*3dabe4e0SAndreas Gohr switch ($call[0]) { 99*3dabe4e0SAndreas Gohr case 'gfm_table_start': 100*3dabe4e0SAndreas Gohr $startPos = $call[1][0] ?? $call[2]; 101*3dabe4e0SAndreas Gohr break; 102*3dabe4e0SAndreas Gohr case 'gfm_table_end': 103*3dabe4e0SAndreas Gohr $endPos = $call[2]; 104*3dabe4e0SAndreas Gohr break; 105*3dabe4e0SAndreas Gohr case 'gfm_table_row': 106*3dabe4e0SAndreas Gohr $rows[] = []; 107*3dabe4e0SAndreas Gohr $rowIdx++; 108*3dabe4e0SAndreas Gohr break; 109*3dabe4e0SAndreas Gohr case 'gfm_table_cell': 110*3dabe4e0SAndreas Gohr $rows[$rowIdx][] = []; 111*3dabe4e0SAndreas Gohr break; 112*3dabe4e0SAndreas Gohr default: 113*3dabe4e0SAndreas Gohr if ($rowIdx >= 0 && !empty($rows[$rowIdx])) { 114*3dabe4e0SAndreas Gohr $cellIdx = count($rows[$rowIdx]) - 1; 115*3dabe4e0SAndreas Gohr $rows[$rowIdx][$cellIdx][] = $call; 116*3dabe4e0SAndreas Gohr } 117*3dabe4e0SAndreas Gohr break; 118*3dabe4e0SAndreas Gohr } 119*3dabe4e0SAndreas Gohr } 120*3dabe4e0SAndreas Gohr 121*3dabe4e0SAndreas Gohr return ['rows' => $rows, 'startPos' => $startPos, 'endPos' => $endPos]; 122*3dabe4e0SAndreas Gohr } 123*3dabe4e0SAndreas Gohr 124*3dabe4e0SAndreas Gohr /** 125*3dabe4e0SAndreas Gohr * Remove leading and trailing empty cell from given row. 126*3dabe4e0SAndreas Gohr * 127*3dabe4e0SAndreas Gohr * Effects of leading and trailing pipes: `| a | b |` parses into four 128*3dabe4e0SAndreas Gohr * cells `["", " a ", " b ", ""]`. A row with no surrounding pipes 129*3dabe4e0SAndreas Gohr * (`a | b`) parses into two non-empty cells, which stay untouched. 130*3dabe4e0SAndreas Gohr * 131*3dabe4e0SAndreas Gohr * @param array $row a row as a list of cells; each cell is a list of 132*3dabe4e0SAndreas Gohr * handler calls captured between separators 133*3dabe4e0SAndreas Gohr * @return array the row with at most one boundary empty cell stripped 134*3dabe4e0SAndreas Gohr * from each end 135*3dabe4e0SAndreas Gohr */ 136*3dabe4e0SAndreas Gohr protected function stripBoundaryEmpty(array $row): array 137*3dabe4e0SAndreas Gohr { 138*3dabe4e0SAndreas Gohr if ($row && $row[0] === []) array_shift($row); 139*3dabe4e0SAndreas Gohr if ($row && end($row) === []) array_pop($row); 140*3dabe4e0SAndreas Gohr return $row; 141*3dabe4e0SAndreas Gohr } 142*3dabe4e0SAndreas Gohr 143*3dabe4e0SAndreas Gohr /** 144*3dabe4e0SAndreas Gohr * Concatenate the original source text of every text-bearing call in a 145*3dabe4e0SAndreas Gohr * cell. Used for delimiter parsing and the spec-203 fallback. 146*3dabe4e0SAndreas Gohr * 147*3dabe4e0SAndreas Gohr * Relies on the project-wide convention that any inline mode which 148*3dabe4e0SAndreas Gohr * swallows source text records the matched string at args[0] — true 149*3dabe4e0SAndreas Gohr * for `cdata`, `entity`, `unformatted`, `smiley`, `multiplyentity`, 150*3dabe4e0SAndreas Gohr * plugin substitutions, etc. Open/close pairs carry empty args and 151*3dabe4e0SAndreas Gohr * drop out naturally. 152*3dabe4e0SAndreas Gohr * 153*3dabe4e0SAndreas Gohr * Motivating case: Entity eats runs of `---` as em-dash entities, so 154*3dabe4e0SAndreas Gohr * a naive cdata-only join would lose the delimiter dashes and 155*3dabe4e0SAndreas Gohr * parseAlign() would refuse the column. 156*3dabe4e0SAndreas Gohr * 157*3dabe4e0SAndreas Gohr * Implementation: extract every call's args list, extract index 0 158*3dabe4e0SAndreas Gohr * from each, implode. 159*3dabe4e0SAndreas Gohr * 160*3dabe4e0SAndreas Gohr * @param array $cellCalls handler calls captured inside one cell 161*3dabe4e0SAndreas Gohr * @return string the concatenated source text 162*3dabe4e0SAndreas Gohr */ 163*3dabe4e0SAndreas Gohr protected function cellText(array $cellCalls): string 164*3dabe4e0SAndreas Gohr { 165*3dabe4e0SAndreas Gohr return implode('', array_column(array_column($cellCalls, 1), 0)); 166*3dabe4e0SAndreas Gohr } 167*3dabe4e0SAndreas Gohr 168*3dabe4e0SAndreas Gohr /** 169*3dabe4e0SAndreas Gohr * Decode a single delimiter cell into 'left' / 'center' / 'right' / null. 170*3dabe4e0SAndreas Gohr * 171*3dabe4e0SAndreas Gohr * Trusts the entry pattern's validation that the cell has the shape 172*3dabe4e0SAndreas Gohr * `:?-+:?`; just checks for colons at the edges. 173*3dabe4e0SAndreas Gohr * 174*3dabe4e0SAndreas Gohr * @param string $cellText the joined source text of one delimiter cell 175*3dabe4e0SAndreas Gohr * @return string|null 'left', 'center', 'right', or null when no 176*3dabe4e0SAndreas Gohr * alignment marker is present 177*3dabe4e0SAndreas Gohr */ 178*3dabe4e0SAndreas Gohr protected function parseAlign(string $cellText): ?string 179*3dabe4e0SAndreas Gohr { 180*3dabe4e0SAndreas Gohr $trimmed = trim($cellText); 181*3dabe4e0SAndreas Gohr $left = str_starts_with($trimmed, ':'); 182*3dabe4e0SAndreas Gohr $right = str_ends_with($trimmed, ':'); 183*3dabe4e0SAndreas Gohr return match (true) { 184*3dabe4e0SAndreas Gohr $left && $right => 'center', 185*3dabe4e0SAndreas Gohr $right => 'right', 186*3dabe4e0SAndreas Gohr $left => 'left', 187*3dabe4e0SAndreas Gohr default => null, 188*3dabe4e0SAndreas Gohr }; 189*3dabe4e0SAndreas Gohr } 190*3dabe4e0SAndreas Gohr 191*3dabe4e0SAndreas Gohr /** 192*3dabe4e0SAndreas Gohr * Return a copy of the row padded with empty cells (spec 202) or 193*3dabe4e0SAndreas Gohr * truncated to the header column count (spec 204). 194*3dabe4e0SAndreas Gohr * 195*3dabe4e0SAndreas Gohr * @param array $row a body row as a list of cells 196*3dabe4e0SAndreas Gohr * @param int $cols the target column count derived from the delimiter row 197*3dabe4e0SAndreas Gohr * @return array the row with exactly $cols cells 198*3dabe4e0SAndreas Gohr */ 199*3dabe4e0SAndreas Gohr protected function padOrTruncate(array $row, int $cols): array 200*3dabe4e0SAndreas Gohr { 201*3dabe4e0SAndreas Gohr $count = count($row); 202*3dabe4e0SAndreas Gohr if ($count < $cols) { 203*3dabe4e0SAndreas Gohr return array_pad($row, $cols, []); 204*3dabe4e0SAndreas Gohr } 205*3dabe4e0SAndreas Gohr if ($count > $cols) { 206*3dabe4e0SAndreas Gohr return array_slice($row, 0, $cols); 207*3dabe4e0SAndreas Gohr } 208*3dabe4e0SAndreas Gohr return $row; 209*3dabe4e0SAndreas Gohr } 210*3dabe4e0SAndreas Gohr 211*3dabe4e0SAndreas Gohr /** 212*3dabe4e0SAndreas Gohr * Return a copy of the row with each cell's first cdata ltrimmed, 213*3dabe4e0SAndreas Gohr * its last cdata rtrimmed, and any cdata that became empty dropped. 214*3dabe4e0SAndreas Gohr * Intermediate cdata are left intact so internal spaces are preserved. 215*3dabe4e0SAndreas Gohr * 216*3dabe4e0SAndreas Gohr * @param array $row a row as a list of cells 217*3dabe4e0SAndreas Gohr * @return array the row with each cell's edge cdata trimmed 218*3dabe4e0SAndreas Gohr */ 219*3dabe4e0SAndreas Gohr protected function trimCellEdges(array $row): array 220*3dabe4e0SAndreas Gohr { 221*3dabe4e0SAndreas Gohr return array_map($this->trimCell(...), $row); 222*3dabe4e0SAndreas Gohr } 223*3dabe4e0SAndreas Gohr 224*3dabe4e0SAndreas Gohr /** 225*3dabe4e0SAndreas Gohr * Helper for trimCellEdges: trim edge cdata of a single cell. 226*3dabe4e0SAndreas Gohr * 227*3dabe4e0SAndreas Gohr * @param array $cell the cell as a list of handler calls 228*3dabe4e0SAndreas Gohr * @return array the cell with its first cdata ltrimmed, its last 229*3dabe4e0SAndreas Gohr * cdata rtrimmed, and any cdata that became empty 230*3dabe4e0SAndreas Gohr * dropped 231*3dabe4e0SAndreas Gohr */ 232*3dabe4e0SAndreas Gohr protected function trimCell(array $cell): array 233*3dabe4e0SAndreas Gohr { 234*3dabe4e0SAndreas Gohr // get all cdata call indexes 235*3dabe4e0SAndreas Gohr $cdataIdx = array_keys(array_filter($cell, fn($c) => $c[0] === 'cdata')); 236*3dabe4e0SAndreas Gohr if ($cdataIdx) { 237*3dabe4e0SAndreas Gohr // if any, trim the first and last one's text 238*3dabe4e0SAndreas Gohr $cell[$cdataIdx[0]][1][0] = ltrim($cell[$cdataIdx[0]][1][0]); 239*3dabe4e0SAndreas Gohr $cell[end($cdataIdx)][1][0] = rtrim($cell[end($cdataIdx)][1][0]); 240*3dabe4e0SAndreas Gohr } 241*3dabe4e0SAndreas Gohr // return all cells that are not cdate or are not empty after trimming 242*3dabe4e0SAndreas Gohr return array_values(array_filter( 243*3dabe4e0SAndreas Gohr $cell, 244*3dabe4e0SAndreas Gohr fn($c) => $c[0] !== 'cdata' || $c[1][0] !== '' 245*3dabe4e0SAndreas Gohr )); 246*3dabe4e0SAndreas Gohr } 247*3dabe4e0SAndreas Gohr 248*3dabe4e0SAndreas Gohr /** 249*3dabe4e0SAndreas Gohr * Spec-203 fallback. Reconstruct a `|a|b|`-style line from each row's 250*3dabe4e0SAndreas Gohr * cells via cellText() and emit the joined block as a single cdata so 251*3dabe4e0SAndreas Gohr * the Block rewriter wraps it in a paragraph. Because cellText() also 252*3dabe4e0SAndreas Gohr * walks `entity` / `unformatted` / etc., the source-text delimiter 253*3dabe4e0SAndreas Gohr * characters survive even when an inline mode consumed them. 254*3dabe4e0SAndreas Gohr * 255*3dabe4e0SAndreas Gohr * @param array $rows the captured rows-of-cells-of-calls structure 256*3dabe4e0SAndreas Gohr * @param int $pos the source position to attach to the emitted cdata 257*3dabe4e0SAndreas Gohr */ 258*3dabe4e0SAndreas Gohr protected function emitFallback(array $rows, int $pos): void 259*3dabe4e0SAndreas Gohr { 260*3dabe4e0SAndreas Gohr $lines = []; 261*3dabe4e0SAndreas Gohr foreach ($rows as $row) { 262*3dabe4e0SAndreas Gohr $cellTexts = []; 263*3dabe4e0SAndreas Gohr foreach ($row as $cell) { 264*3dabe4e0SAndreas Gohr $cellTexts[] = $this->cellText($cell); 265*3dabe4e0SAndreas Gohr } 266*3dabe4e0SAndreas Gohr $lines[] = '|' . implode('|', $cellTexts) . '|'; 267*3dabe4e0SAndreas Gohr } 268*3dabe4e0SAndreas Gohr $text = implode("\n", $lines); 269*3dabe4e0SAndreas Gohr if ($text === '') return; 270*3dabe4e0SAndreas Gohr $this->callWriter->writeCall(['cdata', [$text], $pos]); 271*3dabe4e0SAndreas Gohr } 272*3dabe4e0SAndreas Gohr 273*3dabe4e0SAndreas Gohr /** 274*3dabe4e0SAndreas Gohr * Assemble the canonical DokuWiki table-instruction sequence. 275*3dabe4e0SAndreas Gohr * 276*3dabe4e0SAndreas Gohr * `tabletbody_open` / `tabletbody_close` are emitted only when there 277*3dabe4e0SAndreas Gohr * are body rows. Suppressing them for empty-body tables (spec 205) 278*3dabe4e0SAndreas Gohr * matches the spec's "<thead> only, no <tbody>" expectation without 279*3dabe4e0SAndreas Gohr * any state-tracking on the renderer side. 280*3dabe4e0SAndreas Gohr * 281*3dabe4e0SAndreas Gohr * @param array $headerRow trimmed header row, one cell per column 282*3dabe4e0SAndreas Gohr * @param array $bodyRows trimmed body rows, each padded or truncated 283*3dabe4e0SAndreas Gohr * to $cols 284*3dabe4e0SAndreas Gohr * @param array $alignments per-column alignment from the delimiter 285*3dabe4e0SAndreas Gohr * row; each entry is 'left' / 'center' / 286*3dabe4e0SAndreas Gohr * 'right' / null 287*3dabe4e0SAndreas Gohr * @param int $cols column count derived from the delimiter row 288*3dabe4e0SAndreas Gohr * @param int $startPos source position of the table's start 289*3dabe4e0SAndreas Gohr * @param int $endPos source position of the table's end 290*3dabe4e0SAndreas Gohr * @return array the canonical DokuWiki table call sequence ready for 291*3dabe4e0SAndreas Gohr * the outer call writer 292*3dabe4e0SAndreas Gohr */ 293*3dabe4e0SAndreas Gohr protected function buildOutput( 294*3dabe4e0SAndreas Gohr array $headerRow, 295*3dabe4e0SAndreas Gohr array $bodyRows, 296*3dabe4e0SAndreas Gohr array $alignments, 297*3dabe4e0SAndreas Gohr int $cols, 298*3dabe4e0SAndreas Gohr int $startPos, 299*3dabe4e0SAndreas Gohr int $endPos 300*3dabe4e0SAndreas Gohr ): array { 301*3dabe4e0SAndreas Gohr $out = []; 302*3dabe4e0SAndreas Gohr $out[] = ['table_open', [$cols, 1 + count($bodyRows), $startPos], $startPos]; 303*3dabe4e0SAndreas Gohr $out[] = ['tablethead_open', [], $startPos]; 304*3dabe4e0SAndreas Gohr $out[] = ['tablerow_open', [], $startPos]; 305*3dabe4e0SAndreas Gohr foreach ($headerRow as $i => $cell) { 306*3dabe4e0SAndreas Gohr $out[] = ['tableheader_open', [1, $alignments[$i], 1], $startPos]; 307*3dabe4e0SAndreas Gohr foreach ($cell as $c) $out[] = $c; 308*3dabe4e0SAndreas Gohr $out[] = ['tableheader_close', [], $startPos]; 309*3dabe4e0SAndreas Gohr } 310*3dabe4e0SAndreas Gohr $out[] = ['tablerow_close', [], $startPos]; 311*3dabe4e0SAndreas Gohr $out[] = ['tablethead_close', [], $startPos]; 312*3dabe4e0SAndreas Gohr 313*3dabe4e0SAndreas Gohr if ($bodyRows) { 314*3dabe4e0SAndreas Gohr $out[] = ['tabletbody_open', [], $startPos]; 315*3dabe4e0SAndreas Gohr foreach ($bodyRows as $row) { 316*3dabe4e0SAndreas Gohr $out[] = ['tablerow_open', [], $startPos]; 317*3dabe4e0SAndreas Gohr foreach ($row as $i => $cell) { 318*3dabe4e0SAndreas Gohr $out[] = ['tablecell_open', [1, $alignments[$i], 1], $startPos]; 319*3dabe4e0SAndreas Gohr foreach ($cell as $c) $out[] = $c; 320*3dabe4e0SAndreas Gohr $out[] = ['tablecell_close', [], $startPos]; 321*3dabe4e0SAndreas Gohr } 322*3dabe4e0SAndreas Gohr $out[] = ['tablerow_close', [], $startPos]; 323*3dabe4e0SAndreas Gohr } 324*3dabe4e0SAndreas Gohr $out[] = ['tabletbody_close', [], $startPos]; 325*3dabe4e0SAndreas Gohr } 326*3dabe4e0SAndreas Gohr $out[] = ['table_close', [$endPos], $endPos]; 327*3dabe4e0SAndreas Gohr return $out; 328*3dabe4e0SAndreas Gohr } 329*3dabe4e0SAndreas Gohr} 330