1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace phpDocumentor\Guides\Nodes; |
6
|
|
|
|
7
|
|
|
use Exception; |
8
|
|
|
use LogicException; |
9
|
|
|
use phpDocumentor\Guides\Nodes\Table\TableColumn; |
10
|
|
|
use phpDocumentor\Guides\Nodes\Table\TableRow; |
11
|
|
|
use phpDocumentor\Guides\RestructuredText\Exception\InvalidTableStructure; |
12
|
|
|
use phpDocumentor\Guides\RestructuredText\Parser; |
13
|
|
|
use phpDocumentor\Guides\RestructuredText\Parser\LineChecker; |
14
|
|
|
use phpDocumentor\Guides\RestructuredText\Parser\TableSeparatorLineConfig; |
15
|
|
|
use function array_keys; |
16
|
|
|
use function array_reverse; |
17
|
|
|
use function array_values; |
18
|
|
|
use function count; |
19
|
|
|
use function explode; |
20
|
|
|
use function implode; |
21
|
|
|
use function ksort; |
22
|
|
|
use function max; |
23
|
|
|
use function preg_match; |
24
|
|
|
use function sprintf; |
25
|
|
|
use function str_repeat; |
26
|
|
|
use function strlen; |
27
|
|
|
use function strpos; |
28
|
|
|
use function substr; |
29
|
|
|
use function trim; |
30
|
|
|
use function utf8_decode; |
31
|
|
|
|
32
|
|
|
class TableNode extends Node |
33
|
|
|
{ |
34
|
|
|
public const TYPE_SIMPLE = 'simple'; |
35
|
|
|
public const TYPE_PRETTY = 'pretty'; |
36
|
|
|
|
37
|
|
|
/** @var TableSeparatorLineConfig[] */ |
38
|
|
|
private $separatorLineConfigs = []; |
39
|
|
|
|
40
|
|
|
/** @var string[] */ |
41
|
|
|
private $rawDataLines = []; |
42
|
|
|
|
43
|
|
|
/** @var int */ |
44
|
|
|
private $currentLineNumber = 0; |
45
|
|
|
|
46
|
|
|
/** @var bool */ |
47
|
|
|
private $isCompiled = false; |
48
|
|
|
|
49
|
|
|
/** @var TableRow[] */ |
50
|
|
|
protected $data = []; |
51
|
|
|
|
52
|
|
|
/** @var bool[] */ |
53
|
|
|
protected $headers = []; |
54
|
|
|
|
55
|
|
|
/** @var string[] */ |
56
|
|
|
private $errors = []; |
57
|
|
|
|
58
|
|
|
/** @var string */ |
59
|
|
|
protected $type; |
60
|
|
|
|
61
|
|
|
/** @var LineChecker */ |
62
|
|
|
private $lineChecker; |
63
|
|
|
|
64
|
|
|
public function __construct(TableSeparatorLineConfig $separatorLineConfig, string $type, LineChecker $lineChecker) |
65
|
|
|
{ |
66
|
|
|
parent::__construct(); |
67
|
|
|
|
68
|
|
|
$this->pushSeparatorLine($separatorLineConfig); |
69
|
|
|
$this->type = $type; |
70
|
|
|
$this->lineChecker = $lineChecker; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
public function getCols() : int |
74
|
|
|
{ |
75
|
|
|
if ($this->isCompiled === false) { |
76
|
|
|
throw new LogicException('Call compile() first.'); |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
$columns = 0; |
80
|
|
|
foreach ($this->data as $row) { |
81
|
|
|
$columns = max($columns, count($row->getColumns())); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
return $columns; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
public function getRows() : int |
88
|
|
|
{ |
89
|
|
|
if ($this->isCompiled === false) { |
90
|
|
|
throw new LogicException('Call compile() first.'); |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
return count($this->data); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* @return TableRow[] |
98
|
|
|
*/ |
99
|
|
|
public function getData() : array |
100
|
|
|
{ |
101
|
|
|
if ($this->isCompiled === false) { |
102
|
|
|
throw new LogicException('Call compile() first.'); |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
return $this->data; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* Returns an of array of which rows should be headers, |
110
|
|
|
* where the row index is the key of the array and |
111
|
|
|
* the value is always true. |
112
|
|
|
* |
113
|
|
|
* @return bool[] |
114
|
|
|
*/ |
115
|
|
|
public function getHeaders() : array |
116
|
|
|
{ |
117
|
|
|
if ($this->isCompiled === false) { |
118
|
|
|
throw new LogicException('Call compile() first.'); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
return $this->headers; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
public function pushSeparatorLine(TableSeparatorLineConfig $separatorLineConfig) : void |
125
|
|
|
{ |
126
|
|
|
if ($this->isCompiled === true) { |
127
|
|
|
throw new LogicException('Cannot push data after TableNode is compiled'); |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
$this->separatorLineConfigs[$this->currentLineNumber] = $separatorLineConfig; |
131
|
|
|
$this->currentLineNumber++; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
public function pushContentLine(string $line) : void |
135
|
|
|
{ |
136
|
|
|
if ($this->isCompiled === true) { |
137
|
|
|
throw new LogicException('Cannot push data after TableNode is compiled'); |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
$this->rawDataLines[$this->currentLineNumber] = utf8_decode($line); |
141
|
|
|
$this->currentLineNumber++; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
public function finalize(Parser $parser) : void |
145
|
|
|
{ |
146
|
|
|
if ($this->isCompiled === false) { |
147
|
|
|
$this->compile(); |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
$tableAsString = $this->getTableAsString(); |
151
|
|
|
|
152
|
|
|
if (count($this->errors) > 0) { |
153
|
|
|
$parser->getEnvironment() |
154
|
|
|
->addError(sprintf("%s\nin file %s\n\n%s", $this->errors[0], $parser->getFilename(), $tableAsString)); |
155
|
|
|
|
156
|
|
|
$this->data = []; |
157
|
|
|
$this->headers = []; |
158
|
|
|
|
159
|
|
|
return; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
foreach ($this->data as $i => $row) { |
163
|
|
|
foreach ($row->getColumns() as $col) { |
164
|
|
|
$lines = explode("\n", $col->getContent()); |
165
|
|
|
|
166
|
|
|
if ($this->lineChecker->isListLine($lines[0], false)) { |
167
|
|
|
$node = $parser->parseFragment($col->getContent())->getNodes()[0]; |
168
|
|
|
} else { |
169
|
|
|
$node = $parser->createSpanNode($col->getContent()); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
$col->setNode($node); |
173
|
|
|
} |
174
|
|
|
} |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Looks at all the raw data and finally populates the data |
179
|
|
|
* and headers. |
180
|
|
|
*/ |
181
|
|
|
private function compile() : void |
182
|
|
|
{ |
183
|
|
|
$this->isCompiled = true; |
184
|
|
|
|
185
|
|
|
if ($this->type === self::TYPE_SIMPLE) { |
186
|
|
|
$this->compileSimpleTable(); |
187
|
|
|
} else { |
188
|
|
|
$this->compilePrettyTable(); |
189
|
|
|
} |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
private function compileSimpleTable() : void |
193
|
|
|
{ |
194
|
|
|
// determine if there is second === separator line (other than |
195
|
|
|
// the last line): this would mean there are header rows |
196
|
|
|
$finalHeadersRow = 0; |
197
|
|
|
foreach ($this->separatorLineConfigs as $i => $separatorLine) { |
198
|
|
|
// skip the first line: we're looking for the *next* line |
199
|
|
|
if ($i === 0) { |
200
|
|
|
continue; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
// we found the next ==== line |
204
|
|
|
if ($separatorLine->getLineCharacter() === '=') { |
205
|
|
|
// found the end of the header rows |
206
|
|
|
$finalHeadersRow = $i; |
207
|
|
|
|
208
|
|
|
break; |
209
|
|
|
} |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
// if the final header row is *after* the last data line, it's not |
213
|
|
|
// really a header "ending" and so there are no headers |
214
|
|
|
$lastDataLineNumber = array_keys($this->rawDataLines)[count($this->rawDataLines)-1]; |
215
|
|
|
if ($finalHeadersRow > $lastDataLineNumber) { |
216
|
|
|
$finalHeadersRow = 0; |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
// todo - support "---" in the future for colspan |
220
|
|
|
$columnRanges = $this->separatorLineConfigs[0]->getPartRanges(); |
221
|
|
|
$lastColumnRangeEnd = array_values($columnRanges)[count($columnRanges)-1][1]; |
222
|
|
|
foreach ($this->rawDataLines as $i => $line) { |
223
|
|
|
$row = new TableRow(); |
224
|
|
|
// loop over where all the columns should be |
225
|
|
|
|
226
|
|
|
$previousColumnEnd = null; |
227
|
|
|
foreach ($columnRanges as $columnRange) { |
228
|
|
|
$isRangeBeyondText = $columnRange[0] >= strlen($line); |
229
|
|
|
// check for content in the "gap" |
230
|
|
|
if ($previousColumnEnd !== null && ! $isRangeBeyondText) { |
231
|
|
|
$gapText = substr($line, $previousColumnEnd, $columnRange[0] - $previousColumnEnd); |
232
|
|
|
if (strlen(trim($gapText)) !== 0) { |
233
|
|
|
$this->addError(sprintf('Malformed table: content "%s" appears in the "gap" on row "%s"', $gapText, $line)); |
234
|
|
|
} |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
if ($isRangeBeyondText) { |
238
|
|
|
// the text for this line ended earlier. This column should be blank |
239
|
|
|
|
240
|
|
|
$content = ''; |
241
|
|
|
} elseif ($lastColumnRangeEnd === $columnRange[1]) { |
242
|
|
|
// this is the last column, so get the rest of the line |
243
|
|
|
// this is because content can go *beyond* the table legally |
244
|
|
|
$content = substr( |
245
|
|
|
$line, |
246
|
|
|
$columnRange[0] |
247
|
|
|
); |
248
|
|
|
} else { |
249
|
|
|
$content = substr( |
250
|
|
|
$line, |
251
|
|
|
$columnRange[0], |
252
|
|
|
$columnRange[1] - $columnRange[0] |
253
|
|
|
); |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
$content = trim($content); |
257
|
|
|
$row->addColumn($content, 1); |
258
|
|
|
|
259
|
|
|
$previousColumnEnd = $columnRange[1]; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
// is header row? |
263
|
|
|
if ($i <= $finalHeadersRow) { |
264
|
|
|
$this->headers[$i] = true; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
$this->data[$i] = $row; |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
/** @var TableRow|null $previousRow */ |
271
|
|
|
$previousRow = null; |
272
|
|
|
// check for empty first columns, which means this is |
273
|
|
|
// not a new row, but the continuation of the previous row |
274
|
|
|
foreach ($this->data as $i => $row) { |
275
|
|
|
if ($row->getFirstColumn()->isCompletelyEmpty() && $previousRow !== null) { |
276
|
|
|
try { |
277
|
|
|
$previousRow->absorbRowContent($row); |
278
|
|
|
} catch (InvalidTableStructure $e) { |
279
|
|
|
$this->addError($e->getMessage()); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
unset($this->data[$i]); |
283
|
|
|
|
284
|
|
|
continue; |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
$previousRow = $row; |
288
|
|
|
} |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
private function compilePrettyTable() : void |
292
|
|
|
{ |
293
|
|
|
// loop over ALL separator lines to find ALL of the column ranges |
294
|
|
|
$columnRanges = []; |
295
|
|
|
$finalHeadersRow = 0; |
296
|
|
|
foreach ($this->separatorLineConfigs as $rowIndex => $separatorLine) { |
297
|
|
|
if ($separatorLine->isHeader()) { |
298
|
|
|
if ($finalHeadersRow !== 0) { |
299
|
|
|
$this->addError(sprintf('Malformed table: multiple "header rows" using "===" were found. See table lines "%d" and "%d"', $finalHeadersRow + 1, $rowIndex)); |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
// indicates that "=" was used |
303
|
|
|
$finalHeadersRow = $rowIndex - 1; |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
foreach ($separatorLine->getPartRanges() as $columnRange) { |
307
|
|
|
$colStart = $columnRange[0]; |
308
|
|
|
$colEnd = $columnRange[1]; |
309
|
|
|
|
310
|
|
|
// we don't have this "start" yet? just add it |
311
|
|
|
// in theory, should only happen for the first row |
312
|
|
|
if (! isset($columnRanges[$colStart])) { |
313
|
|
|
$columnRanges[$colStart] = $colEnd; |
314
|
|
|
|
315
|
|
|
continue; |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
// an exact column range we've already seen |
319
|
|
|
// OR, this new column goes beyond what we currently |
320
|
|
|
// have recorded, which means its a colspan, and so |
321
|
|
|
// we already have correctly recorded the "smallest" |
322
|
|
|
// current column ranges |
323
|
|
|
if ($columnRanges[$colStart] <= $colEnd) { |
324
|
|
|
continue; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
// this is not a new "start", but it is a new "end" |
328
|
|
|
// this means that we've found a "shorter" column that |
329
|
|
|
// we've seen before. We need to update the "end" of |
330
|
|
|
// the existing column, and add a "new" column |
331
|
|
|
$previousEnd = $columnRanges[$colStart]; |
332
|
|
|
|
333
|
|
|
// A) update the end of this column to the new end |
334
|
|
|
$columnRanges[$colStart] = $colEnd; |
335
|
|
|
// B) add a new column from this new end, to the previous end |
336
|
|
|
$columnRanges[$colEnd + 1] = $previousEnd; |
337
|
|
|
ksort($columnRanges); |
338
|
|
|
} |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
/** @var TableRow[] $rows */ |
342
|
|
|
$rows = []; |
343
|
|
|
$partialSeparatorRows = []; |
344
|
|
|
foreach ($this->rawDataLines as $rowIndex => $line) { |
345
|
|
|
$row = new TableRow(); |
346
|
|
|
|
347
|
|
|
// if the row is part separator row, part content, this |
348
|
|
|
// is a rowspan situation - e.g. |
349
|
|
|
// | +----------------+----------------------------+ |
350
|
|
|
// look for +-----+ pattern |
351
|
|
|
if (preg_match('/\+[-]+\+/', $this->rawDataLines[$rowIndex]) === 1) { |
352
|
|
|
$partialSeparatorRows[$rowIndex] = true; |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
$currentColumnStart = null; |
356
|
|
|
$currentSpan = 1; |
357
|
|
|
$previousColumnEnd = null; |
358
|
|
|
foreach ($columnRanges as $start => $end) { |
359
|
|
|
// a content line that ends before it should |
360
|
|
|
if ($end >= strlen($line)) { |
361
|
|
|
$this->errors[] = sprintf("Malformed table: Line\n\n%s\n\ndoes not appear to be a complete table row", $line); |
362
|
|
|
|
363
|
|
|
break; |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
if ($currentColumnStart !== null) { |
367
|
|
|
if ($previousColumnEnd === null) { |
368
|
|
|
throw new LogicException('The previous column end is not set yet'); |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
$gapText = substr($line, $previousColumnEnd, $start - $previousColumnEnd); |
372
|
|
|
if (strpos($gapText, '|') === false && strpos($gapText, '+') === false) { |
373
|
|
|
// text continued through the "gap". This is a colspan |
374
|
|
|
// "+" is an odd character - it's usually "|", but "+" can |
375
|
|
|
// happen in row-span situations |
376
|
|
|
$currentSpan++; |
377
|
|
|
} else { |
378
|
|
|
// we just hit a proper "gap" record the line up until now |
379
|
|
|
$row->addColumn( |
380
|
|
|
substr($line, $currentColumnStart, $previousColumnEnd - $currentColumnStart), |
381
|
|
|
$currentSpan |
382
|
|
|
); |
383
|
|
|
$currentSpan = 1; |
384
|
|
|
$currentColumnStart = null; |
385
|
|
|
} |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
// if the current column start is null, then set it |
389
|
|
|
// other wise, leave it - this is a colspan, and eventually |
390
|
|
|
// we want to get all the text starting here |
391
|
|
|
if ($currentColumnStart === null) { |
392
|
|
|
$currentColumnStart = $start; |
393
|
|
|
} |
394
|
|
|
$previousColumnEnd = $end; |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
// record the last column |
398
|
|
|
if ($currentColumnStart !== null) { |
399
|
|
|
if ($previousColumnEnd === null) { |
400
|
|
|
throw new LogicException('The previous column end is not set yet'); |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
$row->addColumn( |
404
|
|
|
substr($line, $currentColumnStart, $previousColumnEnd - $currentColumnStart), |
405
|
|
|
$currentSpan |
406
|
|
|
); |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
$rows[$rowIndex] = $row; |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
$columnIndexesCurrentlyInRowspan = []; |
413
|
|
|
foreach ($rows as $rowIndex => $row) { |
414
|
|
|
if (isset($partialSeparatorRows[$rowIndex])) { |
415
|
|
|
// this row is part content, part separator due to a rowspan |
416
|
|
|
// for each column that contains content, we need to |
417
|
|
|
// push it onto the last real row's content and record |
418
|
|
|
// that this column in the next row should also be |
419
|
|
|
// included in that previous row's content |
420
|
|
|
foreach ($row->getColumns() as $columnIndex => $column) { |
421
|
|
|
if (! $column->isCompletelyEmpty() && str_repeat('-', strlen($column->getContent())) === $column->getContent()) { |
422
|
|
|
// only a line separator in this column - not content! |
423
|
|
|
continue; |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
$prevTargetColumn = $this->findColumnInPreviousRows((int) $columnIndex, $rows, (int) $rowIndex); |
427
|
|
|
$prevTargetColumn->addContent("\n" . $column->getContent()); |
428
|
|
|
$prevTargetColumn->incrementRowSpan(); |
429
|
|
|
// mark that this column on the next row should also be added |
430
|
|
|
// to the previous row |
431
|
|
|
$columnIndexesCurrentlyInRowspan[] = $columnIndex; |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
// remove the row - it's not real |
435
|
|
|
unset($rows[$rowIndex]); |
436
|
|
|
|
437
|
|
|
continue; |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
// check if the previous row was a partial separator row, and |
441
|
|
|
// we need to take some columns and add them to a previous row's content |
442
|
|
|
foreach ($columnIndexesCurrentlyInRowspan as $columnIndex) { |
443
|
|
|
$prevTargetColumn = $this->findColumnInPreviousRows($columnIndex, $rows, (int) $rowIndex); |
444
|
|
|
$columnInRowspan = $row->getColumn($columnIndex); |
445
|
|
|
if ($columnInRowspan === null) { |
446
|
|
|
throw new LogicException('Cannot find column for index "%s"', $columnIndex); |
447
|
|
|
} |
448
|
|
|
$prevTargetColumn->addContent("\n" . $columnInRowspan->getContent()); |
449
|
|
|
|
450
|
|
|
// now this column actually needs to be removed from this row, |
451
|
|
|
// as it's not a real column that needs to be printed |
452
|
|
|
$row->removeColumn($columnIndex); |
453
|
|
|
} |
454
|
|
|
$columnIndexesCurrentlyInRowspan = []; |
455
|
|
|
|
456
|
|
|
// if the next row is just $i+1, it means there |
457
|
|
|
// was no "separator" and this is really just a |
458
|
|
|
// continuation of this row. |
459
|
|
|
$nextRowCounter = 1; |
460
|
|
|
while (isset($rows[(int) $rowIndex + $nextRowCounter])) { |
461
|
|
|
// but if the next line is actually a partial separator, then |
462
|
|
|
// it is not a continuation of the content - quit now |
463
|
|
|
if (isset($partialSeparatorRows[(int) $rowIndex + $nextRowCounter])) { |
464
|
|
|
break; |
465
|
|
|
} |
466
|
|
|
|
467
|
|
|
$targetRow = $rows[(int) $rowIndex + $nextRowCounter]; |
468
|
|
|
unset($rows[(int) $rowIndex + $nextRowCounter]); |
469
|
|
|
|
470
|
|
|
try { |
471
|
|
|
$row->absorbRowContent($targetRow); |
472
|
|
|
} catch (InvalidTableStructure $e) { |
473
|
|
|
$this->addError($e->getMessage()); |
474
|
|
|
} |
475
|
|
|
|
476
|
|
|
$nextRowCounter++; |
477
|
|
|
} |
478
|
|
|
} |
479
|
|
|
|
480
|
|
|
// one more loop to set headers |
481
|
|
|
foreach ($rows as $rowIndex => $row) { |
482
|
|
|
if ($rowIndex > $finalHeadersRow) { |
483
|
|
|
continue; |
484
|
|
|
} |
485
|
|
|
|
486
|
|
|
$this->headers[$rowIndex] = true; |
487
|
|
|
} |
488
|
|
|
|
489
|
|
|
$this->data = $rows; |
490
|
|
|
} |
491
|
|
|
|
492
|
|
|
private function getTableAsString() : string |
493
|
|
|
{ |
494
|
|
|
$lines = []; |
495
|
|
|
$i = 0; |
496
|
|
|
while (isset($this->separatorLineConfigs[$i]) || isset($this->rawDataLines[$i])) { |
497
|
|
|
if (isset($this->separatorLineConfigs[$i])) { |
498
|
|
|
$lines[] = $this->separatorLineConfigs[$i]->getRawContent(); |
499
|
|
|
} else { |
500
|
|
|
$lines[] = $this->rawDataLines[$i]; |
501
|
|
|
} |
502
|
|
|
|
503
|
|
|
$i++; |
504
|
|
|
} |
505
|
|
|
|
506
|
|
|
return implode("\n", $lines); |
507
|
|
|
} |
508
|
|
|
|
509
|
|
|
private function addError(string $message) : void |
510
|
|
|
{ |
511
|
|
|
$this->errors[] = $message; |
512
|
|
|
} |
513
|
|
|
|
514
|
|
|
/** |
515
|
|
|
* @param TableRow[] $rows |
516
|
|
|
*/ |
517
|
|
|
private function findColumnInPreviousRows(int $columnIndex, array $rows, int $currentRowIndex) : TableColumn |
518
|
|
|
{ |
519
|
|
|
/** @var TableRow[] $reversedRows */ |
520
|
|
|
$reversedRows = array_reverse($rows, true); |
521
|
|
|
|
522
|
|
|
// go through the rows backwards to find the last/previous |
523
|
|
|
// row that actually had a real column at this position |
524
|
|
|
foreach ($reversedRows as $k => $row) { |
525
|
|
|
// start by skipping any future rows, as we go backward |
526
|
|
|
if ($k >= $currentRowIndex) { |
527
|
|
|
continue; |
528
|
|
|
} |
529
|
|
|
|
530
|
|
|
$prevTargetColumn = $row->getColumn($columnIndex); |
531
|
|
|
if ($prevTargetColumn !== null) { |
532
|
|
|
return $prevTargetColumn; |
533
|
|
|
} |
534
|
|
|
} |
535
|
|
|
|
536
|
|
|
throw new Exception('Could not find column in any previous rows'); |
537
|
|
|
} |
538
|
|
|
} |
539
|
|
|
|