Parser::processCurrentTable()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 9
ccs 7
cts 7
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) The s9e authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\TextFormatter\Plugins\PipeTables;
9
10
use s9e\TextFormatter\Plugins\ParserBase;
11
12
class Parser extends ParserBase
13
{
14
	/**
15
	* @var integer Position of the cursor while parsing tables or adding tags
16
	*/
17
	protected $pos;
18
19
	/**
20
	* @var array Current table being parsed
21
	*/
22
	protected $table;
23
24
	/**
25
	* @var \s9e\TextFormatter\Parser\Tag
26
	*/
27
	protected $tableTag;
28
29
	/**
30
	* @var array[] List of tables
31
	*/
32
	protected $tables;
33
34
	/**
35
	* @var string Text being parsed
36
	*/
37
	protected $text;
38
39
	/**
40
	* {@inheritdoc}
41
	*/
42 27
	public function parse($text, array $matches)
43
	{
44 27
		$this->text = $text;
45 27
		if ($this->config['overwriteMarkdown'])
46
		{
47 9
			$this->overwriteMarkdown();
48
		}
49 27
		if ($this->config['overwriteEscapes'])
50
		{
51 1
			$this->overwriteEscapes();
52
		}
53
54 27
		$this->captureTables();
55 27
		$this->processTables();
56
57 27
		unset($this->tables);
58 27
		unset($this->text);
59
	}
60
61
	/**
62
	* Add current line to a table
63
	*
64
	* @param  string $line Line of text
65
	* @return void
66
	*/
67 26
	protected function addLine($line)
68
	{
69 26
		$ignoreLen = 0;
70
71 26
		if (!isset($this->table))
72
		{
73 26
			$this->table = [];
74
75
			// Make the table start at the first non-space character
76 26
			preg_match('/^ */', $line, $m);
77 26
			$ignoreLen = strlen($m[0]);
78 26
			$line      = substr($line, $ignoreLen);
79
		}
80
81
		// Overwrite the outermost pipes
82 26
		$line = preg_replace('/^( *)\\|/', '$1 ', $line);
83 26
		$line = preg_replace('/\\|( *)$/', ' $1', $line);
84
85 26
		$this->table['rows'][] = ['line' => $line, 'pos' => $this->pos + $ignoreLen];
86
	}
87
88
	/**
89
	* Process current table's body
90
	*
91
	* @return void
92
	*/
93 24
	protected function addTableBody()
94
	{
95 24
		$i   = 1;
96 24
		$cnt = count($this->table['rows']);
97 24
		while (++$i < $cnt)
98
		{
99 24
			$this->addTableRow('TD', $this->table['rows'][$i]);
100
		}
101
102 24
		$this->createBodyTags($this->table['rows'][2]['pos'], $this->pos);
103
	}
104
105
	/**
106
	* Add a cell's tags for current table at current position
107
	*
108
	* @param  string $tagName Either TD or TH
109
	* @param  string $align   Either "left", "center", "right" or ""
110
	* @param  string $content Cell's text content
111
	* @return void
112
	*/
113 24
	protected function addTableCell($tagName, $align, $content)
114
	{
115 24
		$startPos  = $this->pos;
116 24
		$endPos    = $startPos + strlen($content);
117 24
		$this->pos = $endPos;
118
119 24
		preg_match('/^( *).*?( *)$/', $content, $m);
120 24
		if ($m[1])
121
		{
122 13
			$ignoreLen = strlen($m[1]);
123 13
			$this->createIgnoreTag($startPos, $ignoreLen);
124 13
			$startPos += $ignoreLen;
125
		}
126 24
		if ($m[2])
127
		{
128 6
			$ignoreLen = strlen($m[2]);
129 6
			$this->createIgnoreTag($endPos - $ignoreLen, $ignoreLen);
130 6
			$endPos -= $ignoreLen;
131
		}
132
133 24
		$this->createCellTags($tagName, $startPos, $endPos, $align);
134
	}
135
136
	/**
137
	* Process current table's head
138
	*
139
	* @return void
140
	*/
141 24
	protected function addTableHead()
142
	{
143 24
		$this->addTableRow('TH', $this->table['rows'][0]);
144 24
		$this->createHeadTags($this->table['rows'][0]['pos'], $this->pos);
145
	}
146
147
	/**
148
	* Process given table row
149
	*
150
	* @param  string $tagName Either TD or TH
151
	* @param  array  $row
152
	* @return void
153
	*/
154 24
	protected function addTableRow($tagName, $row)
155
	{
156 24
		$this->pos = $row['pos'];
157 24
		foreach (explode('|', $row['line']) as $i => $str)
158
		{
159 24
			if ($i > 0)
160
			{
161 24
				$this->createIgnoreTag($this->pos, 1);
162 24
				++$this->pos;
163
			}
164
165 24
			$align = (empty($this->table['cols'][$i])) ? '' : $this->table['cols'][$i];
166 24
			$this->addTableCell($tagName, $align, $str);
167
		}
168
169 24
		$this->createRowTags($row['pos'], $this->pos);
170
	}
171
172
	/**
173
	* Capture all pipe tables in current text
174
	*
175
	* @return void
176
	*/
177 27
	protected function captureTables()
178
	{
179 27
		unset($this->table);
180 27
		$this->tables = [];
181
182 27
		$this->pos = 0;
183 27
		foreach (explode("\n", $this->text) as $line)
184
		{
185 27
			if (strpos($line, '|') === false)
186
			{
187 3
				$this->endTable();
188
			}
189
			else
190
			{
191 26
				$this->addLine($line);
192
			}
193 27
			$this->pos += 1 + strlen($line);
194
		}
195 27
		$this->endTable();
196
	}
197
198
	/**
199
	* Create a pair of TBODY tags for given text span
200
	*
201
	* @param  integer $startPos
202
	* @param  integer $endPos
203
	* @return void
204
	*/
205 24
	protected function createBodyTags($startPos, $endPos)
206
	{
207 24
		$this->parser->addTagPair('TBODY', $startPos, 0, $endPos, 0, -103);
208
	}
209
210
	/**
211
	* Create a pair of TD or TH tags for given text span
212
	*
213
	* @param  string  $tagName  Either TD or TH
214
	* @param  integer $startPos
215
	* @param  integer $endPos
216
	* @param  string  $align    Either "left", "center", "right" or ""
217
	* @return void
218
	*/
219 24
	protected function createCellTags($tagName, $startPos, $endPos, $align)
220
	{
221 24
		if ($startPos === $endPos)
222
		{
223 2
			$tag = $this->parser->addSelfClosingTag($tagName, $startPos, 0, -101);
224
		}
225
		else
226
		{
227 22
			$tag = $this->parser->addTagPair($tagName, $startPos, 0, $endPos, 0, -101);
228
		}
229 24
		if ($align)
230
		{
231 3
			$tag->setAttribute('align', $align);
232
		}
233
	}
234
235
	/**
236
	* Create a pair of THEAD tags for given text span
237
	*
238
	* @param  integer $startPos
239
	* @param  integer $endPos
240
	* @return void
241
	*/
242 24
	protected function createHeadTags($startPos, $endPos)
243
	{
244 24
		$this->parser->addTagPair('THEAD', $startPos, 0, $endPos, 0, -103);
245
	}
246
247
	/**
248
	* Create an ignore tag for given text span
249
	*
250
	* @param  integer $pos
251
	* @param  integer $len
252
	* @return void
253
	*/
254 24
	protected function createIgnoreTag($pos, $len)
255
	{
256 24
		$this->tableTag->cascadeInvalidationTo($this->parser->addIgnoreTag($pos, $len, 1000));
257
	}
258
259
	/**
260
	* Create a pair of TR tags for given text span
261
	*
262
	* @param  integer $startPos
263
	* @param  integer $endPos
264
	* @return void
265
	*/
266 24
	protected function createRowTags($startPos, $endPos)
267
	{
268 24
		$this->parser->addTagPair('TR', $startPos, 0, $endPos, 0, -102);
269
	}
270
271
	/**
272
	* Create an ignore tag for given separator row
273
	*
274
	* @param  array $row
275
	* @return void
276
	*/
277 24
	protected function createSeparatorTag(array $row)
278
	{
279 24
		$this->createIgnoreTag($row['pos'] - 1, 1 + strlen($row['line']));
280
	}
281
282
	/**
283
	* Create a pair of TABLE tags for given text span
284
	*
285
	* @param  integer $startPos
286
	* @param  integer $endPos
287
	* @return void
288
	*/
289 24
	protected function createTableTags($startPos, $endPos)
290
	{
291 24
		$this->tableTag = $this->parser->addTagPair('TABLE', $startPos, 0, $endPos, 0, -104);
292
	}
293
294
	/**
295
	* End current buffered table
296
	*
297
	* @return void
298
	*/
299 27
	protected function endTable()
300
	{
301 27
		if ($this->hasValidTable())
302
		{
303 24
			$this->table['cols'] = $this->parseColumnAlignments($this->table['rows'][1]['line']);
304 24
			$this->tables[]      = $this->table;
305
		}
306 27
		unset($this->table);
307
	}
308
309
	/**
310
	* Test whether a valid table is currently buffered
311
	*
312
	* @return bool
313
	*/
314 27
	protected function hasValidTable()
315
	{
316 27
		return (isset($this->table) && count($this->table['rows']) > 2 && $this->isValidSeparator($this->table['rows'][1]['line']));
317
	}
318
319
	/**
320
	* Test whether given line is a valid separator
321
	*
322
	* @param  string $line
323
	* @return bool
324
	*/
325 25
	protected function isValidSeparator($line)
326
	{
327 25
		return (bool) preg_match('/^ *:?-+:?(?:(?:\\+| *\\| *):?-+:?)+ *$/', $line);
328
	}
329
330
	/**
331
	* Overwrite right angle brackets in given match
332
	*
333
	* @param  string[] $m
334
	* @return string
335
	*/
336 5
	protected function overwriteBlockquoteCallback(array $m)
337
	{
338 5
		return strtr($m[0], '!>', '  ');
339
	}
340
341
	/**
342
	* Overwrite escape sequences in current text
343
	*
344
	* @return void
345
	*/
346 1
	protected function overwriteEscapes()
347
	{
348 1
		if (strpos($this->text, '\\|') !== false)
349
		{
350 1
			$this->text = preg_replace('/\\\\[\\\\|]/', '..', $this->text);
351
		}
352
	}
353
354
	/**
355
	* Overwrite backticks in given match
356
	*
357
	* @param  string[] $m
358
	* @return string
359
	*/
360 3
	protected function overwriteInlineCodeCallback(array $m)
361
	{
362 3
		return strtr($m[0], '|', '.');
363
	}
364
365
	/**
366
	* Overwrite Markdown-style markup in current text
367
	*
368
	* @return void
369
	*/
370 9
	protected function overwriteMarkdown()
371
	{
372
		// Overwrite inline code spans
373 9
		if (strpos($this->text, '`') !== false)
374
		{
375 3
			$this->text = preg_replace_callback('/`[^`]*`/', $this->overwriteInlineCodeCallback(...), $this->text);
376
		}
377
378
		// Overwrite blockquotes
379 9
		if (strpos($this->text, '>') !== false)
380
		{
381 5
			$this->text = preg_replace_callback('/^(?:>!? ?)+/m', $this->overwriteBlockquoteCallback(...), $this->text);
382
		}
383
	}
384
385
	/**
386
	* Parse and return column alignments in given separator line
387
	*
388
	* @param  string   $line
389
	* @return string[]
390
	*/
391 24
	protected function parseColumnAlignments($line)
392
	{
393
		// Use a bitfield to represent the colons' presence and map it to the CSS value
394
		$align = [
395 24
			0b00 => '',
396
			0b01 => 'right',
397
			0b10 => 'left',
398
			0b11 => 'center'
399
		];
400
401 24
		$cols = [];
402 24
		preg_match_all('/(:?)-+(:?)/', $line, $matches, PREG_SET_ORDER);
403 24
		foreach ($matches as $m)
404
		{
405 24
			$key = (!empty($m[1]) ? 2 : 0) + (!empty($m[2]) ? 1 : 0);
406 24
			$cols[] = $align[$key];
407
		}
408
409 24
		return $cols;
410
	}
411
412
	/**
413
	* Process current table declaration
414
	*
415
	* @return void
416
	*/
417 24
	protected function processCurrentTable()
418
	{
419 24
		$firstRow = $this->table['rows'][0];
420 24
		$lastRow  = end($this->table['rows']);
421 24
		$this->createTableTags($firstRow['pos'], $lastRow['pos'] + strlen($lastRow['line']));
422
423 24
		$this->addTableHead();
424 24
		$this->createSeparatorTag($this->table['rows'][1]);
425 24
		$this->addTableBody();
426
	}
427
428
	/**
429
	* Process all the captured tables
430
	*
431
	* @return void
432
	*/
433 27
	protected function processTables()
434
	{
435 27
		foreach ($this->tables as $table)
436
		{
437 24
			$this->table = $table;
438 24
			$this->processCurrentTable();
439
		}
440
	}
441
}