Completed
Branch TemplateInspector (5726eb)
by Josh
09:25
created

Parser::parseBBCode()   D

Complexity

Conditions 17
Paths 166

Size

Total Lines 80
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 306

Importance

Changes 0
Metric Value
dl 0
loc 80
ccs 0
cts 32
cp 0
rs 4.7009
c 0
b 0
f 0
cc 17
eloc 32
nc 166
nop 0
crap 306

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2017 The s9e Authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\TextFormatter\Plugins\BBCodes;
9
10
use RuntimeException;
11
use s9e\TextFormatter\Parser\Tag;
12
use s9e\TextFormatter\Plugins\ParserBase;
13
14
class Parser extends ParserBase
15
{
16
	/**
17
	* @var array Attributes of the BBCode being parsed
18
	*/
19
	protected $attributes;
20
21
	/**
22
	* @var array Configuration for the BBCode being parsed
23
	*/
24
	protected $bbcodeConfig;
25
26
	/**
27
	* @var string Name of the BBCode being parsed
28
	*/
29
	protected $bbcodeName;
30
31
	/**
32
	* @var string Suffix of the BBCode being parsed, including its colon
33
	*/
34
	protected $bbcodeSuffix;
35
36
	/**
37
	* @var integer Position of the cursor in the original text
38
	*/
39
	protected $pos;
40
41
	/**
42
	* @var integer Position of the start of the BBCode being parsed
43
	*/
44
	protected $startPos;
45
46
	/**
47
	* @var string Text being parsed
48
	*/
49
	protected $text;
50
51
	/**
52
	* @var integer Length of the text being parsed
53
	*/
54
	protected $textLen;
55
56
	/**
57
	* @var string Text being parsed, normalized to uppercase
58
	*/
59
	protected $uppercaseText;
60
61
	/**
62
	* {@inheritdoc}
63
	*/
64
	public function parse($text, array $matches)
65
	{
66
		$this->text          = $text;
67
		$this->textLen       = strlen($text);
68
		$this->uppercaseText = '';
69
		foreach ($matches as $m)
70
		{
71
			$this->bbcodeName = strtoupper($m[1][0]);
72
			if (!isset($this->config['bbcodes'][$this->bbcodeName]))
73
			{
74
				continue;
75
			}
76
			$this->bbcodeConfig = $this->config['bbcodes'][$this->bbcodeName];
77
			$this->startPos     = $m[0][1];
78
			$this->pos          = $this->startPos + strlen($m[0][0]);
79
80
			try
81
			{
82
				$this->parseBBCode();
83
			}
84
			catch (RuntimeException $e)
85
			{
86
				// Do nothing
87
			}
88
		}
89
	}
90
91
	/**
92
	* Add the end tag that matches current BBCode
93
	*
94
	* @return Tag
95
	*/
96
	protected function addBBCodeEndTag()
97
	{
98
		return $this->parser->addEndTag($this->getTagName(), $this->startPos, $this->pos - $this->startPos);
99
	}
100
101
	/**
102
	* Add the self-closing tag that matches current BBCode
103
	*
104
	* @return Tag
105
	*/
106
	protected function addBBCodeSelfClosingTag()
107
	{
108
		$tag = $this->parser->addSelfClosingTag($this->getTagName(), $this->startPos, $this->pos - $this->startPos);
109
		$tag->setAttributes($this->attributes);
110
111
		return $tag;
112
	}
113
114
	/**
115
	* Add the start tag that matches current BBCode
116
	*
117
	* @return Tag
118
	*/
119
	protected function addBBCodeStartTag()
120
	{
121
		$tag = $this->parser->addStartTag($this->getTagName(), $this->startPos, $this->pos - $this->startPos);
122
		$tag->setAttributes($this->attributes);
123
124
		return $tag;
125
	}
126
127
	/**
128
	* Parse the end tag that matches given BBCode name and suffix starting at current position
129
	*
130
	* @return Tag|null
131
	*/
132
	protected function captureEndTag()
133
	{
134
		if (empty($this->uppercaseText))
135
		{
136
			$this->uppercaseText = strtoupper($this->text);
137
		}
138
		$match     = '[/' . $this->bbcodeName . $this->bbcodeSuffix . ']';
139
		$endTagPos = strpos($this->uppercaseText, $match, $this->pos);
140
		if ($endTagPos === false)
141
		{
142
			return;
143
		}
144
145
		return $this->parser->addEndTag($this->getTagName(), $endTagPos, strlen($match));
146
	}
147
148
	/**
149
	* Get the tag name for current BBCode
150
	*
151
	* @return string
152
	*/
153
	protected function getTagName()
154
	{
155
		// Use the configured tagName if available, or reuse the BBCode's name otherwise
156
		return (isset($this->bbcodeConfig['tagName']))
157
		     ? $this->bbcodeConfig['tagName']
158
		     : $this->bbcodeName;
159
	}
160
161
	/**
162
	* Parse attributes starting at current position
163
	*
164
	* @return array Associative array of [name => value]
165
	*/
166
	protected function parseAttributes()
167
	{
168
		$firstPos = $this->pos;
169
		$this->attributes = [];
170
		while ($this->pos < $this->textLen)
171
		{
172
			$c = $this->text[$this->pos];
173
			if (strpos(" \n\t", $c) !== false)
174
			{
175
				++$this->pos;
176
				continue;
177
			}
178
			if (strpos('/]', $c) !== false)
179
			{
180
				return;
181
			}
182
183
			// Capture the attribute name
184
			$spn = strspn($this->text, 'abcdefghijklmnopqrstuvwxyz_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-', $this->pos);
185
			if ($spn)
186
			{
187
				$attrName = strtolower(substr($this->text, $this->pos, $spn));
188
				$this->pos += $spn;
189
				if ($this->pos >= $this->textLen)
190
				{
191
					// The attribute name extends to the end of the text
192
					throw new RuntimeException;
193
				}
194
				if ($this->text[$this->pos] !== '=')
195
				{
196
					// It's an attribute name not followed by an equal sign, ignore it
197
					continue;
198
				}
199
			}
200
			elseif ($c === '=' && $this->pos === $firstPos)
201
			{
202
				// This is the default param, e.g. [quote=foo]
203
				$attrName = (isset($this->bbcodeConfig['defaultAttribute']))
204
				          ? $this->bbcodeConfig['defaultAttribute']
205
				          : strtolower($this->bbcodeName);
206
			}
207
			else
208
			{
209
				throw new RuntimeException;
210
			}
211
212
			// Move past the = and make sure we're not at the end of the text
213
			if (++$this->pos >= $this->textLen)
214
			{
215
				throw new RuntimeException;
216
			}
217
218
			$this->attributes[$attrName] = $this->parseAttributeValue();
219
		}
220
	}
221
222
	/**
223
	* Parse the attribute value starting at current position
224
	*
225
	* @return string
226
	*/
227
	protected function parseAttributeValue()
228
	{
229
		// Test whether the value is in quotes
230
		if ($this->text[$this->pos] === '"' || $this->text[$this->pos] === "'")
231
		{
232
			return $this->parseQuotedAttributeValue();
233
		}
234
235
		// Capture everything up to whichever comes first:
236
		//  - an endline
237
		//  - whitespace followed by a slash and a closing bracket
238
		//  - a closing bracket, optionally preceded by whitespace
239
		//  - whitespace followed by another attribute (name followed by equal sign)
240
		//
241
		// NOTE: this is for compatibility with some forums (such as vBulletin it seems)
242
		//       that do not put attribute values in quotes, e.g.
243
		//       [quote=John Smith;123456] (quoting "John Smith" from post #123456)
244
		if (!preg_match('#[^\\]\\n]*?(?=\\s*(?:\\s/)?\\]|\\s+[-\\w]+=)#', $this->text, $m, null, $this->pos))
245
		{
246
			throw new RuntimeException;
247
		}
248
249
		$attrValue  = $m[0];
250
		$this->pos += strlen($attrValue);
251
252
		return $attrValue;
253
	}
254
255
	/**
256
	* Parse current BBCode
257
	*
258
	* @return void
259
	*/
260
	protected function parseBBCode()
261
	{
262
		$this->parseBBCodeSuffix();
263
264
		// Test whether this is an end tag
265
		if ($this->text[$this->startPos + 1] === '/')
266
		{
267
			// Test whether the tag is properly closed and whether this tag has an identifier.
268
			// We skip end tags that carry an identifier because they're automatically added
269
			// when their start tag is processed
270
			if (substr($this->text, $this->pos, 1) === ']' && $this->bbcodeSuffix === '')
271
			{
272
				++$this->pos;
273
				$this->addBBCodeEndTag();
274
			}
275
276
			return;
277
		}
278
279
		// Parse attributes and fill in the blanks with predefined attributes
280
		$this->parseAttributes();
281
		if (isset($this->bbcodeConfig['predefinedAttributes']))
282
		{
283
			$this->attributes += $this->bbcodeConfig['predefinedAttributes'];
284
		}
285
286
		// Test whether the tag is properly closed
287
		if (substr($this->text, $this->pos, 1) === ']')
288
		{
289
			++$this->pos;
290
		}
291
		else
292
		{
293
			// Test whether this is a self-closing tag
294
			if (substr($this->text, $this->pos, 2) === '/]')
295
			{
296
				$this->pos += 2;
297
				$this->addBBCodeSelfClosingTag();
298
			}
299
300
			return;
301
		}
302
303
		// Record the names of attributes that need the content of this tag
304
		$contentAttributes = [];
305
		if (isset($this->bbcodeConfig['contentAttributes']))
306
		{
307
			foreach ($this->bbcodeConfig['contentAttributes'] as $attrName)
308
			{
309
				if (!isset($this->attributes[$attrName]))
310
				{
311
					$contentAttributes[] = $attrName;
312
				}
313
			}
314
		}
315
316
		// Look ahead and parse the end tag that matches this tag, if applicable
317
		$requireEndTag = ($this->bbcodeSuffix || !empty($this->bbcodeConfig['forceLookahead']));
318
		$endTag = ($requireEndTag || !empty($contentAttributes)) ? $this->captureEndTag() : null;
319
		if (isset($endTag))
320
		{
321
			foreach ($contentAttributes as $attrName)
322
			{
323
				$this->attributes[$attrName] = substr($this->text, $this->pos, $endTag->getPos() - $this->pos);
324
			}
325
		}
326
		elseif ($requireEndTag)
327
		{
328
			return;
329
		}
330
331
		// Create this start tag
332
		$tag = $this->addBBCodeStartTag();
333
334
		// If an end tag was created, pair it with this start tag
335
		if (isset($endTag))
336
		{
337
			$tag->pairWith($endTag);
338
		}
339
	}
340
341
	/**
342
	* Parse the BBCode suffix starting at current position
343
	*
344
	* Used to explicitly pair specific tags together, e.g.
345
	*   [code:123][code]type your code here[/code][/code:123]
346
	*
347
	* @return void
348
	*/
349
	protected function parseBBCodeSuffix()
350
	{
351
		$this->bbcodeSuffix = '';
352
		if ($this->text[$this->pos] === ':')
353
		{
354
			// Capture the colon and the (0 or more) digits following it
355
			$spn = 1 + strspn($this->text, '0123456789', 1 + $this->pos);
356
			$this->bbcodeSuffix = substr($this->text, $this->pos, $spn);
357
358
			// Move past the suffix
359
			$this->pos += $spn;
360
		}
361
	}
362
363
	/**
364
	* Parse a quoted attribute value that starts at current offset
365
	*
366
	* @return string
367
	*/
368
	protected function parseQuotedAttributeValue()
369
	{
370
		$quote    = $this->text[$this->pos];
371
		$valuePos = $this->pos + 1;
372
		while (1)
373
		{
374
			// Look for the next quote
375
			$this->pos = strpos($this->text, $quote, $this->pos + 1);
376
			if ($this->pos === false)
377
			{
378
				// No matching quote. Apparently that string never ends...
379
				throw new RuntimeException;
380
			}
381
382
			// Test for an odd number of backslashes before this character
383
			$n = 0;
384
			do
385
			{
386
				++$n;
387
			}
388
			while ($this->text[$this->pos - $n] === '\\');
389
390
			if ($n % 2)
391
			{
392
				// If $n is odd, it means there's an even number of backslashes. We can exit this
393
				// loop
394
				break;
395
			}
396
		}
397
398
		// Unescape special characters ' " and \
399
		$attrValue = preg_replace(
400
			'#\\\\([\\\\\'"])#',
401
			'$1',
402
			substr($this->text, $valuePos, $this->pos - $valuePos)
403
		);
404
405
		// Skip past the closing quote
406
		++$this->pos;
407
408
		return $attrValue;
409
	}
410
}