13dabe4e0SAndreas Gohr<?php 23dabe4e0SAndreas Gohr 33dabe4e0SAndreas Gohrnamespace dokuwiki\Parsing\Handler; 43dabe4e0SAndreas Gohr 53dabe4e0SAndreas Gohr/** 63dabe4e0SAndreas Gohr * CallWriter rewriter for GFM tables. 73dabe4e0SAndreas Gohr * 83dabe4e0SAndreas Gohr * GfmTable's lexer state emits a flat token stream of marker calls 93dabe4e0SAndreas Gohr * (`gfm_table_start`, `gfm_table_row`, `gfm_table_cell`, `gfm_table_end`) 103dabe4e0SAndreas Gohr * interleaved with whatever inline modes (emphasis, code spans, links, …) 113dabe4e0SAndreas Gohr * matched inside the cells. This rewriter: 123dabe4e0SAndreas Gohr * 133dabe4e0SAndreas Gohr * 1. Groups the flat stream into rows-of-cells, where each cell carries 143dabe4e0SAndreas Gohr * its own list of nested handler calls. 153dabe4e0SAndreas Gohr * 2. Strips the empty leading and trailing cells that result from leading 163dabe4e0SAndreas Gohr * and trailing pipes (`| a | b |` → cells `["", " a ", " b ", ""]` → 173dabe4e0SAndreas Gohr * `[" a ", " b "]`). 183dabe4e0SAndreas Gohr * 3. Parses the second row as the GFM delimiter row, deriving per-column 193dabe4e0SAndreas Gohr * alignment from `:-+:?` patterns and the column count from the cell 203dabe4e0SAndreas Gohr * count. 213dabe4e0SAndreas Gohr * 4. Validates that the header row's cell count matches the delimiter's. 223dabe4e0SAndreas Gohr * On mismatch (spec example 203), emits the captured text back as a 233dabe4e0SAndreas Gohr * single cdata so the Block rewriter wraps it in a paragraph. 243dabe4e0SAndreas Gohr * 5. Pads body rows that are short (spec 202) and truncates body rows 253dabe4e0SAndreas Gohr * that are long (spec 204) to the header's column count. 263dabe4e0SAndreas Gohr * 6. Trims leading/trailing whitespace from each cell's edge cdata calls 273dabe4e0SAndreas Gohr * ("Spaces between pipes and cell content are trimmed"). 283dabe4e0SAndreas Gohr * 7. Emits the canonical DokuWiki table call sequence — `table_open`, 293dabe4e0SAndreas Gohr * `tablethead_open`, `tablerow_open`, per-column `tableheader_open` 303dabe4e0SAndreas Gohr * with alignment, `tablethead_close`, then (only when there are 313dabe4e0SAndreas Gohr * body rows — spec 205) `tabletbody_open`, per-row `tablerow_open` 323dabe4e0SAndreas Gohr * with `tablecell_open`, `tabletbody_close`, and finally 333dabe4e0SAndreas Gohr * `table_close`. No new handler instructions are introduced; 343dabe4e0SAndreas Gohr * `tabletbody_open` / `tabletbody_close` are part of DokuWiki's 353dabe4e0SAndreas Gohr * base renderer API but were never emitted before — DW Table omits 363dabe4e0SAndreas Gohr * `<tbody>` entirely. Activating them here is what frees the test 373dabe4e0SAndreas Gohr * renderer from having to track tbody state. 383dabe4e0SAndreas Gohr * 39*74031e46SAndreas Gohr * Backslash-escaped pipes outside protected regions are consumed by 40*74031e46SAndreas Gohr * GfmEscape before the cell content reaches this rewriter. Inside 41*74031e46SAndreas Gohr * code spans (and any other whole-span PROTECTED capture) the `\|` 42*74031e46SAndreas Gohr * survives as literal text — and the GFM tables extension demands 43*74031e46SAndreas Gohr * that `\|` unescape to `|` even there, overriding §6.1's 44*74031e46SAndreas Gohr * "escapes don't work in code spans" rule. unescapePipes() applies 45*74031e46SAndreas Gohr * that rewrite per cell to every text-bearing call. 463dabe4e0SAndreas Gohr */ 473dabe4e0SAndreas Gohrclass GfmTable extends AbstractRewriter 483dabe4e0SAndreas Gohr{ 493dabe4e0SAndreas Gohr /** @inheritdoc */ 503dabe4e0SAndreas Gohr protected function getClosingCall(): string 513dabe4e0SAndreas Gohr { 523dabe4e0SAndreas Gohr return 'gfm_table_end'; 533dabe4e0SAndreas Gohr } 543dabe4e0SAndreas Gohr 553dabe4e0SAndreas Gohr /** @inheritdoc */ 563dabe4e0SAndreas Gohr public function process() 573dabe4e0SAndreas Gohr { 583dabe4e0SAndreas Gohr ['rows' => $rows, 'startPos' => $startPos, 'endPos' => $endPos] = $this->groupRows(); 593dabe4e0SAndreas Gohr $rows = array_map($this->stripBoundaryEmpty(...), $rows); 603dabe4e0SAndreas Gohr 613dabe4e0SAndreas Gohr $alignments = array_map( 623dabe4e0SAndreas Gohr fn($cell) => $this->parseAlign($this->cellText($cell)), 633dabe4e0SAndreas Gohr $rows[1] 643dabe4e0SAndreas Gohr ); 653dabe4e0SAndreas Gohr $cols = count($alignments); 663dabe4e0SAndreas Gohr 673dabe4e0SAndreas Gohr // Header / delimiter column-count mismatch is the spec-203 fallback. 683dabe4e0SAndreas Gohr if (count($rows[0]) !== $cols) { 693dabe4e0SAndreas Gohr $this->emitFallback($rows, $startPos); 703dabe4e0SAndreas Gohr return $this->callWriter; 713dabe4e0SAndreas Gohr } 723dabe4e0SAndreas Gohr 73*74031e46SAndreas Gohr $headerRow = $this->unescapePipes($this->trimCellEdges($rows[0])); 743dabe4e0SAndreas Gohr $bodyRows = array_map( 75*74031e46SAndreas Gohr fn($row) => $this->unescapePipes($this->trimCellEdges($this->padOrTruncate($row, $cols))), 763dabe4e0SAndreas Gohr array_slice($rows, 2) 773dabe4e0SAndreas Gohr ); 783dabe4e0SAndreas Gohr 793dabe4e0SAndreas Gohr $out = $this->buildOutput($headerRow, $bodyRows, $alignments, $cols, $startPos, $endPos); 803dabe4e0SAndreas Gohr $this->callWriter->writeCalls($out); 813dabe4e0SAndreas Gohr return $this->callWriter; 823dabe4e0SAndreas Gohr } 833dabe4e0SAndreas Gohr 843dabe4e0SAndreas Gohr /** 853dabe4e0SAndreas Gohr * Walk $this->calls and bucket them into rows-of-cells-of-calls. 863dabe4e0SAndreas Gohr * 873dabe4e0SAndreas Gohr * @return array{rows: array<int, array<int, array<int, array>>>, startPos: int, endPos: int} 883dabe4e0SAndreas Gohr * `rows[r][c]` is a list of handler calls captured inside row `r`'s 893dabe4e0SAndreas Gohr * cell `c`. `startPos` and `endPos` carry the table's opening and 903dabe4e0SAndreas Gohr * closing source positions. 913dabe4e0SAndreas Gohr */ 923dabe4e0SAndreas Gohr protected function groupRows(): array 933dabe4e0SAndreas Gohr { 943dabe4e0SAndreas Gohr $rows = []; 953dabe4e0SAndreas Gohr $rowIdx = -1; 963dabe4e0SAndreas Gohr $startPos = 0; 973dabe4e0SAndreas Gohr $endPos = 0; 983dabe4e0SAndreas Gohr 993dabe4e0SAndreas Gohr foreach ($this->calls as $call) { 1003dabe4e0SAndreas Gohr switch ($call[0]) { 1013dabe4e0SAndreas Gohr case 'gfm_table_start': 1023dabe4e0SAndreas Gohr $startPos = $call[1][0] ?? $call[2]; 1033dabe4e0SAndreas Gohr break; 1043dabe4e0SAndreas Gohr case 'gfm_table_end': 1053dabe4e0SAndreas Gohr $endPos = $call[2]; 1063dabe4e0SAndreas Gohr break; 1073dabe4e0SAndreas Gohr case 'gfm_table_row': 1083dabe4e0SAndreas Gohr $rows[] = []; 1093dabe4e0SAndreas Gohr $rowIdx++; 1103dabe4e0SAndreas Gohr break; 1113dabe4e0SAndreas Gohr case 'gfm_table_cell': 1123dabe4e0SAndreas Gohr $rows[$rowIdx][] = []; 1133dabe4e0SAndreas Gohr break; 1143dabe4e0SAndreas Gohr default: 1153dabe4e0SAndreas Gohr if ($rowIdx >= 0 && !empty($rows[$rowIdx])) { 1163dabe4e0SAndreas Gohr $cellIdx = count($rows[$rowIdx]) - 1; 1173dabe4e0SAndreas Gohr $rows[$rowIdx][$cellIdx][] = $call; 1183dabe4e0SAndreas Gohr } 1193dabe4e0SAndreas Gohr break; 1203dabe4e0SAndreas Gohr } 1213dabe4e0SAndreas Gohr } 1223dabe4e0SAndreas Gohr 1233dabe4e0SAndreas Gohr return ['rows' => $rows, 'startPos' => $startPos, 'endPos' => $endPos]; 1243dabe4e0SAndreas Gohr } 1253dabe4e0SAndreas Gohr 1263dabe4e0SAndreas Gohr /** 1273dabe4e0SAndreas Gohr * Remove leading and trailing empty cell from given row. 1283dabe4e0SAndreas Gohr * 1293dabe4e0SAndreas Gohr * Effects of leading and trailing pipes: `| a | b |` parses into four 1303dabe4e0SAndreas Gohr * cells `["", " a ", " b ", ""]`. A row with no surrounding pipes 1313dabe4e0SAndreas Gohr * (`a | b`) parses into two non-empty cells, which stay untouched. 1323dabe4e0SAndreas Gohr * 1333dabe4e0SAndreas Gohr * @param array $row a row as a list of cells; each cell is a list of 1343dabe4e0SAndreas Gohr * handler calls captured between separators 1353dabe4e0SAndreas Gohr * @return array the row with at most one boundary empty cell stripped 1363dabe4e0SAndreas Gohr * from each end 1373dabe4e0SAndreas Gohr */ 1383dabe4e0SAndreas Gohr protected function stripBoundaryEmpty(array $row): array 1393dabe4e0SAndreas Gohr { 1403dabe4e0SAndreas Gohr if ($row && $row[0] === []) array_shift($row); 1413dabe4e0SAndreas Gohr if ($row && end($row) === []) array_pop($row); 1423dabe4e0SAndreas Gohr return $row; 1433dabe4e0SAndreas Gohr } 1443dabe4e0SAndreas Gohr 1453dabe4e0SAndreas Gohr /** 1463dabe4e0SAndreas Gohr * Concatenate the original source text of every text-bearing call in a 1473dabe4e0SAndreas Gohr * cell. Used for delimiter parsing and the spec-203 fallback. 1483dabe4e0SAndreas Gohr * 1493dabe4e0SAndreas Gohr * Relies on the project-wide convention that any inline mode which 1503dabe4e0SAndreas Gohr * swallows source text records the matched string at args[0] — true 1513dabe4e0SAndreas Gohr * for `cdata`, `entity`, `unformatted`, `smiley`, `multiplyentity`, 1523dabe4e0SAndreas Gohr * plugin substitutions, etc. Open/close pairs carry empty args and 1533dabe4e0SAndreas Gohr * drop out naturally. 1543dabe4e0SAndreas Gohr * 1553dabe4e0SAndreas Gohr * Motivating case: Entity eats runs of `---` as em-dash entities, so 1563dabe4e0SAndreas Gohr * a naive cdata-only join would lose the delimiter dashes and 1573dabe4e0SAndreas Gohr * parseAlign() would refuse the column. 1583dabe4e0SAndreas Gohr * 1593dabe4e0SAndreas Gohr * Implementation: extract every call's args list, extract index 0 1603dabe4e0SAndreas Gohr * from each, implode. 1613dabe4e0SAndreas Gohr * 1623dabe4e0SAndreas Gohr * @param array $cellCalls handler calls captured inside one cell 1633dabe4e0SAndreas Gohr * @return string the concatenated source text 1643dabe4e0SAndreas Gohr */ 1653dabe4e0SAndreas Gohr protected function cellText(array $cellCalls): string 1663dabe4e0SAndreas Gohr { 1673dabe4e0SAndreas Gohr return implode('', array_column(array_column($cellCalls, 1), 0)); 1683dabe4e0SAndreas Gohr } 1693dabe4e0SAndreas Gohr 1703dabe4e0SAndreas Gohr /** 1713dabe4e0SAndreas Gohr * Decode a single delimiter cell into 'left' / 'center' / 'right' / null. 1723dabe4e0SAndreas Gohr * 1733dabe4e0SAndreas Gohr * Trusts the entry pattern's validation that the cell has the shape 1743dabe4e0SAndreas Gohr * `:?-+:?`; just checks for colons at the edges. 1753dabe4e0SAndreas Gohr * 1763dabe4e0SAndreas Gohr * @param string $cellText the joined source text of one delimiter cell 1773dabe4e0SAndreas Gohr * @return string|null 'left', 'center', 'right', or null when no 1783dabe4e0SAndreas Gohr * alignment marker is present 1793dabe4e0SAndreas Gohr */ 1803dabe4e0SAndreas Gohr protected function parseAlign(string $cellText): ?string 1813dabe4e0SAndreas Gohr { 1823dabe4e0SAndreas Gohr $trimmed = trim($cellText); 1833dabe4e0SAndreas Gohr $left = str_starts_with($trimmed, ':'); 1843dabe4e0SAndreas Gohr $right = str_ends_with($trimmed, ':'); 1853dabe4e0SAndreas Gohr return match (true) { 1863dabe4e0SAndreas Gohr $left && $right => 'center', 1873dabe4e0SAndreas Gohr $right => 'right', 1883dabe4e0SAndreas Gohr $left => 'left', 1893dabe4e0SAndreas Gohr default => null, 1903dabe4e0SAndreas Gohr }; 1913dabe4e0SAndreas Gohr } 1923dabe4e0SAndreas Gohr 1933dabe4e0SAndreas Gohr /** 1943dabe4e0SAndreas Gohr * Return a copy of the row padded with empty cells (spec 202) or 1953dabe4e0SAndreas Gohr * truncated to the header column count (spec 204). 1963dabe4e0SAndreas Gohr * 1973dabe4e0SAndreas Gohr * @param array $row a body row as a list of cells 1983dabe4e0SAndreas Gohr * @param int $cols the target column count derived from the delimiter row 1993dabe4e0SAndreas Gohr * @return array the row with exactly $cols cells 2003dabe4e0SAndreas Gohr */ 2013dabe4e0SAndreas Gohr protected function padOrTruncate(array $row, int $cols): array 2023dabe4e0SAndreas Gohr { 2033dabe4e0SAndreas Gohr $count = count($row); 2043dabe4e0SAndreas Gohr if ($count < $cols) { 2053dabe4e0SAndreas Gohr return array_pad($row, $cols, []); 2063dabe4e0SAndreas Gohr } 2073dabe4e0SAndreas Gohr if ($count > $cols) { 2083dabe4e0SAndreas Gohr return array_slice($row, 0, $cols); 2093dabe4e0SAndreas Gohr } 2103dabe4e0SAndreas Gohr return $row; 2113dabe4e0SAndreas Gohr } 2123dabe4e0SAndreas Gohr 2133dabe4e0SAndreas Gohr /** 2143dabe4e0SAndreas Gohr * Return a copy of the row with each cell's first cdata ltrimmed, 2153dabe4e0SAndreas Gohr * its last cdata rtrimmed, and any cdata that became empty dropped. 2163dabe4e0SAndreas Gohr * Intermediate cdata are left intact so internal spaces are preserved. 2173dabe4e0SAndreas Gohr * 2183dabe4e0SAndreas Gohr * @param array $row a row as a list of cells 2193dabe4e0SAndreas Gohr * @return array the row with each cell's edge cdata trimmed 2203dabe4e0SAndreas Gohr */ 2213dabe4e0SAndreas Gohr protected function trimCellEdges(array $row): array 2223dabe4e0SAndreas Gohr { 2233dabe4e0SAndreas Gohr return array_map($this->trimCell(...), $row); 2243dabe4e0SAndreas Gohr } 2253dabe4e0SAndreas Gohr 2263dabe4e0SAndreas Gohr /** 2273dabe4e0SAndreas Gohr * Helper for trimCellEdges: trim edge cdata of a single cell. 2283dabe4e0SAndreas Gohr * 2293dabe4e0SAndreas Gohr * @param array $cell the cell as a list of handler calls 2303dabe4e0SAndreas Gohr * @return array the cell with its first cdata ltrimmed, its last 2313dabe4e0SAndreas Gohr * cdata rtrimmed, and any cdata that became empty 2323dabe4e0SAndreas Gohr * dropped 2333dabe4e0SAndreas Gohr */ 2343dabe4e0SAndreas Gohr protected function trimCell(array $cell): array 2353dabe4e0SAndreas Gohr { 2363dabe4e0SAndreas Gohr // get all cdata call indexes 2373dabe4e0SAndreas Gohr $cdataIdx = array_keys(array_filter($cell, fn($c) => $c[0] === 'cdata')); 2383dabe4e0SAndreas Gohr if ($cdataIdx) { 2393dabe4e0SAndreas Gohr // if any, trim the first and last one's text 2403dabe4e0SAndreas Gohr $cell[$cdataIdx[0]][1][0] = ltrim($cell[$cdataIdx[0]][1][0]); 2413dabe4e0SAndreas Gohr $cell[end($cdataIdx)][1][0] = rtrim($cell[end($cdataIdx)][1][0]); 2423dabe4e0SAndreas Gohr } 2433dabe4e0SAndreas Gohr // return all cells that are not cdate or are not empty after trimming 2443dabe4e0SAndreas Gohr return array_values(array_filter( 2453dabe4e0SAndreas Gohr $cell, 2463dabe4e0SAndreas Gohr fn($c) => $c[0] !== 'cdata' || $c[1][0] !== '' 2473dabe4e0SAndreas Gohr )); 2483dabe4e0SAndreas Gohr } 2493dabe4e0SAndreas Gohr 2503dabe4e0SAndreas Gohr /** 251*74031e46SAndreas Gohr * Apply the GFM tables-extension rule that `\|` always unescapes to 252*74031e46SAndreas Gohr * `|` inside table cells — including the bodies of code spans and 253*74031e46SAndreas Gohr * other whole-span PROTECTED captures, where standard §6.1 escape 254*74031e46SAndreas Gohr * rules don't fire. Walks every text-bearing call (cdata, 255*74031e46SAndreas Gohr * unformatted, entity, plugin substitutions, …) and str_replace's 256*74031e46SAndreas Gohr * the literal two-char sequence on its first arg. Other escapes 257*74031e46SAndreas Gohr * inside code spans are left alone — only `\|` gets the special 258*74031e46SAndreas Gohr * table treatment. 259*74031e46SAndreas Gohr * 260*74031e46SAndreas Gohr * In normal cell text, GfmEscape has already consumed `\|` upstream, 261*74031e46SAndreas Gohr * so this pass is a no-op there; its job is to catch the codespan 262*74031e46SAndreas Gohr * case that bypasses the lexer. 263*74031e46SAndreas Gohr * 264*74031e46SAndreas Gohr * @param array $row a row as a list of cells 265*74031e46SAndreas Gohr * @return array the row with `\|` rewritten to `|` in every cell 266*74031e46SAndreas Gohr */ 267*74031e46SAndreas Gohr protected function unescapePipes(array $row): array 268*74031e46SAndreas Gohr { 269*74031e46SAndreas Gohr foreach ($row as &$cell) { 270*74031e46SAndreas Gohr foreach ($cell as &$call) { 271*74031e46SAndreas Gohr if (isset($call[1][0]) && is_string($call[1][0])) { 272*74031e46SAndreas Gohr $call[1][0] = str_replace('\\|', '|', $call[1][0]); 273*74031e46SAndreas Gohr } 274*74031e46SAndreas Gohr } 275*74031e46SAndreas Gohr } 276*74031e46SAndreas Gohr return $row; 277*74031e46SAndreas Gohr } 278*74031e46SAndreas Gohr 279*74031e46SAndreas Gohr /** 2803dabe4e0SAndreas Gohr * Spec-203 fallback. Reconstruct a `|a|b|`-style line from each row's 2813dabe4e0SAndreas Gohr * cells via cellText() and emit the joined block as a single cdata so 2823dabe4e0SAndreas Gohr * the Block rewriter wraps it in a paragraph. Because cellText() also 2833dabe4e0SAndreas Gohr * walks `entity` / `unformatted` / etc., the source-text delimiter 2843dabe4e0SAndreas Gohr * characters survive even when an inline mode consumed them. 2853dabe4e0SAndreas Gohr * 2863dabe4e0SAndreas Gohr * @param array $rows the captured rows-of-cells-of-calls structure 2873dabe4e0SAndreas Gohr * @param int $pos the source position to attach to the emitted cdata 2883dabe4e0SAndreas Gohr */ 2893dabe4e0SAndreas Gohr protected function emitFallback(array $rows, int $pos): void 2903dabe4e0SAndreas Gohr { 2913dabe4e0SAndreas Gohr $lines = []; 2923dabe4e0SAndreas Gohr foreach ($rows as $row) { 2933dabe4e0SAndreas Gohr $cellTexts = []; 2943dabe4e0SAndreas Gohr foreach ($row as $cell) { 2953dabe4e0SAndreas Gohr $cellTexts[] = $this->cellText($cell); 2963dabe4e0SAndreas Gohr } 2973dabe4e0SAndreas Gohr $lines[] = '|' . implode('|', $cellTexts) . '|'; 2983dabe4e0SAndreas Gohr } 2993dabe4e0SAndreas Gohr $text = implode("\n", $lines); 3003dabe4e0SAndreas Gohr if ($text === '') return; 3013dabe4e0SAndreas Gohr $this->callWriter->writeCall(['cdata', [$text], $pos]); 3023dabe4e0SAndreas Gohr } 3033dabe4e0SAndreas Gohr 3043dabe4e0SAndreas Gohr /** 3053dabe4e0SAndreas Gohr * Assemble the canonical DokuWiki table-instruction sequence. 3063dabe4e0SAndreas Gohr * 3073dabe4e0SAndreas Gohr * `tabletbody_open` / `tabletbody_close` are emitted only when there 3083dabe4e0SAndreas Gohr * are body rows. Suppressing them for empty-body tables (spec 205) 3093dabe4e0SAndreas Gohr * matches the spec's "<thead> only, no <tbody>" expectation without 3103dabe4e0SAndreas Gohr * any state-tracking on the renderer side. 3113dabe4e0SAndreas Gohr * 3123dabe4e0SAndreas Gohr * @param array $headerRow trimmed header row, one cell per column 3133dabe4e0SAndreas Gohr * @param array $bodyRows trimmed body rows, each padded or truncated 3143dabe4e0SAndreas Gohr * to $cols 3153dabe4e0SAndreas Gohr * @param array $alignments per-column alignment from the delimiter 3163dabe4e0SAndreas Gohr * row; each entry is 'left' / 'center' / 3173dabe4e0SAndreas Gohr * 'right' / null 3183dabe4e0SAndreas Gohr * @param int $cols column count derived from the delimiter row 3193dabe4e0SAndreas Gohr * @param int $startPos source position of the table's start 3203dabe4e0SAndreas Gohr * @param int $endPos source position of the table's end 3213dabe4e0SAndreas Gohr * @return array the canonical DokuWiki table call sequence ready for 3223dabe4e0SAndreas Gohr * the outer call writer 3233dabe4e0SAndreas Gohr */ 3243dabe4e0SAndreas Gohr protected function buildOutput( 3253dabe4e0SAndreas Gohr array $headerRow, 3263dabe4e0SAndreas Gohr array $bodyRows, 3273dabe4e0SAndreas Gohr array $alignments, 3283dabe4e0SAndreas Gohr int $cols, 3293dabe4e0SAndreas Gohr int $startPos, 3303dabe4e0SAndreas Gohr int $endPos 3313dabe4e0SAndreas Gohr ): array { 3323dabe4e0SAndreas Gohr $out = []; 3333dabe4e0SAndreas Gohr $out[] = ['table_open', [$cols, 1 + count($bodyRows), $startPos], $startPos]; 3343dabe4e0SAndreas Gohr $out[] = ['tablethead_open', [], $startPos]; 3353dabe4e0SAndreas Gohr $out[] = ['tablerow_open', [], $startPos]; 3363dabe4e0SAndreas Gohr foreach ($headerRow as $i => $cell) { 3373dabe4e0SAndreas Gohr $out[] = ['tableheader_open', [1, $alignments[$i], 1], $startPos]; 3383dabe4e0SAndreas Gohr foreach ($cell as $c) $out[] = $c; 3393dabe4e0SAndreas Gohr $out[] = ['tableheader_close', [], $startPos]; 3403dabe4e0SAndreas Gohr } 3413dabe4e0SAndreas Gohr $out[] = ['tablerow_close', [], $startPos]; 3423dabe4e0SAndreas Gohr $out[] = ['tablethead_close', [], $startPos]; 3433dabe4e0SAndreas Gohr 3443dabe4e0SAndreas Gohr if ($bodyRows) { 3453dabe4e0SAndreas Gohr $out[] = ['tabletbody_open', [], $startPos]; 3463dabe4e0SAndreas Gohr foreach ($bodyRows as $row) { 3473dabe4e0SAndreas Gohr $out[] = ['tablerow_open', [], $startPos]; 3483dabe4e0SAndreas Gohr foreach ($row as $i => $cell) { 3493dabe4e0SAndreas Gohr $out[] = ['tablecell_open', [1, $alignments[$i], 1], $startPos]; 3503dabe4e0SAndreas Gohr foreach ($cell as $c) $out[] = $c; 3513dabe4e0SAndreas Gohr $out[] = ['tablecell_close', [], $startPos]; 3523dabe4e0SAndreas Gohr } 3533dabe4e0SAndreas Gohr $out[] = ['tablerow_close', [], $startPos]; 3543dabe4e0SAndreas Gohr } 3553dabe4e0SAndreas Gohr $out[] = ['tabletbody_close', [], $startPos]; 3563dabe4e0SAndreas Gohr } 3573dabe4e0SAndreas Gohr $out[] = ['table_close', [$endPos], $endPos]; 3583dabe4e0SAndreas Gohr return $out; 3593dabe4e0SAndreas Gohr } 3603dabe4e0SAndreas Gohr} 361