Completed
Push — master ( bcddd9...147c77 )
by Josh
19:27 queued 22s
created

Parser::parseBBCode()   C

Complexity

Conditions 16
Paths 84

Size

Total Lines 76
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 16

Importance

Changes 0
Metric Value
dl 0
loc 76
ccs 30
cts 30
cp 1
rs 5.248
c 0
b 0
f 0
cc 16
eloc 30
nc 84
nop 0
crap 16

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 50
	public function parse($text, array $matches)
65
	{
66 50
		$this->text          = $text;
67 50
		$this->textLen       = strlen($text);
68 50
		$this->uppercaseText = '';
69 50
		foreach ($matches as $m)
70
		{
71 50
			$this->bbcodeName = strtoupper($m[1][0]);
72 50
			if (!isset($this->config['bbcodes'][$this->bbcodeName]))
73
			{
74 3
				continue;
75
			}
76 50
			$this->bbcodeConfig = $this->config['bbcodes'][$this->bbcodeName];
77 50
			$this->startPos     = $m[0][1];
78 50
			$this->pos          = $this->startPos + strlen($m[0][0]);
79
80
			try
81
			{
82 50
				$this->parseBBCode();
83
			}
84 7
			catch (RuntimeException $e)
85 50
			{
86
				// Do nothing
87
			}
88
		}
89 50
	}
90
91
	/**
92
	* Add the end tag that matches current BBCode
93
	*
94
	* @return Tag
95
	*/
96 22
	protected function addBBCodeEndTag()
97
	{
98 22
		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 13
	protected function addBBCodeSelfClosingTag()
107
	{
108 13
		$tag = $this->parser->addSelfClosingTag($this->getTagName(), $this->startPos, $this->pos - $this->startPos);
109 13
		$tag->setAttributes($this->attributes);
110
111 13
		return $tag;
112
	}
113
114
	/**
115
	* Add the start tag that matches current BBCode
116
	*
117
	* @return Tag
118
	*/
119 23
	protected function addBBCodeStartTag()
120
	{
121 23
		$tag = $this->parser->addStartTag($this->getTagName(), $this->startPos, $this->pos - $this->startPos);
122 23
		$tag->setAttributes($this->attributes);
123
124 23
		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 11
	protected function captureEndTag()
133
	{
134 11
		if (empty($this->uppercaseText))
135
		{
136 11
			$this->uppercaseText = strtoupper($this->text);
137
		}
138 11
		$match     = '[/' . $this->bbcodeName . $this->bbcodeSuffix . ']';
139 11
		$endTagPos = strpos($this->uppercaseText, $match, $this->pos);
140 11
		if ($endTagPos === false)
141
		{
142 5
			return;
143
		}
144
145 7
		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 37
	protected function getTagName()
154
	{
155
		// Use the configured tagName if available, or reuse the BBCode's name otherwise
156 37
		return (isset($this->bbcodeConfig['tagName']))
157 1
		     ? $this->bbcodeConfig['tagName']
158 37
		     : $this->bbcodeName;
159
	}
160
161
	/**
162
	* Parse attributes starting at current position
163
	*
164
	* @return array Associative array of [name => value]
165
	*/
166 49
	protected function parseAttributes()
167
	{
168 49
		$firstPos = $this->pos;
169 49
		$this->attributes = [];
170 49
		while ($this->pos < $this->textLen)
171
		{
172 49
			$c = $this->text[$this->pos];
173 49
			if (strpos(" \n\t", $c) !== false)
174
			{
175 26
				++$this->pos;
176 26
				continue;
177
			}
178 48
			if (strpos('/]', $c) !== false)
179
			{
180 41
				return;
181
			}
182
183
			// Capture the attribute name
184 28
			$spn = strspn($this->text, 'abcdefghijklmnopqrstuvwxyz_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-', $this->pos);
185 28
			if ($spn)
186
			{
187 20
				$attrName = strtolower(substr($this->text, $this->pos, $spn));
188 20
				$this->pos += $spn;
189 20
				if ($this->pos >= $this->textLen)
190
				{
191
					// The attribute name extends to the end of the text
192 1
					throw new RuntimeException;
193
				}
194 19
				if ($this->text[$this->pos] !== '=')
195
				{
196
					// It's an attribute name not followed by an equal sign, ignore it
197 19
					continue;
198
				}
199
			}
200 9
			elseif ($c === '=' && $this->pos === $firstPos)
201
			{
202
				// This is the default param, e.g. [quote=foo]
203 8
				$attrName = (isset($this->bbcodeConfig['defaultAttribute']))
204 2
				          ? $this->bbcodeConfig['defaultAttribute']
205 8
				          : strtolower($this->bbcodeName);
206
			}
207
			else
208
			{
209 1
				throw new RuntimeException;
210
			}
211
212
			// Move past the = and make sure we're not at the end of the text
213 25
			if (++$this->pos >= $this->textLen)
214
			{
215 1
				throw new RuntimeException;
216
			}
217
218 24
			$this->attributes[$attrName] = $this->parseAttributeValue();
219
		}
220 1
	}
221
222
	/**
223
	* Parse the attribute value starting at current position
224
	*
225
	* @return string
226
	*/
227 24
	protected function parseAttributeValue()
228
	{
229
		// Test whether the value is in quotes
230 24
		if ($this->text[$this->pos] === '"' || $this->text[$this->pos] === "'")
231
		{
232 14
			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 10
		if (!preg_match('#[^\\]\\n]*?(?=\\s*(?:\\s/)?\\]|\\s+[-\\w]+=)#', $this->text, $m, null, $this->pos))
245
		{
246 1
			throw new RuntimeException;
247
		}
248
249 9
		$attrValue  = $m[0];
250 9
		$this->pos += strlen($attrValue);
251
252 9
		return $attrValue;
253
	}
254
255
	/**
256
	* Parse current BBCode
257
	*
258
	* @return void
259
	*/
260 50
	protected function parseBBCode()
261
	{
262 50
		$this->parseBBCodeSuffix();
263
264
		// Test whether this is an end tag
265 50
		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 25
			if (substr($this->text, $this->pos, 1) === ']' && $this->bbcodeSuffix === '')
271
			{
272 22
				++$this->pos;
273 22
				$this->addBBCodeEndTag();
274
			}
275
276 25
			return;
277
		}
278
279
		// Parse attributes
280 49
		$this->parseAttributes();
281
282
		// Test whether the tag is properly closed
283 42
		if (substr($this->text, $this->pos, 1) === ']')
284
		{
285 26
			++$this->pos;
286
		}
287
		else
288
		{
289
			// Test whether this is a self-closing tag
290 16
			if (substr($this->text, $this->pos, 2) === '/]')
291
			{
292 13
				$this->pos += 2;
293 13
				$this->addBBCodeSelfClosingTag();
294
			}
295
296 16
			return;
297
		}
298
299
		// Record the names of attributes that need the content of this tag
300 26
		$contentAttributes = [];
301 26
		if (isset($this->bbcodeConfig['contentAttributes']))
302
		{
303 4
			foreach ($this->bbcodeConfig['contentAttributes'] as $attrName)
304
			{
305 4
				if (!isset($this->attributes[$attrName]))
306
				{
307 4
					$contentAttributes[] = $attrName;
308
				}
309
			}
310
		}
311
312
		// Look ahead and parse the end tag that matches this tag, if applicable
313 26
		$requireEndTag = ($this->bbcodeSuffix || !empty($this->bbcodeConfig['forceLookahead']));
314 26
		$endTag = ($requireEndTag || !empty($contentAttributes)) ? $this->captureEndTag() : null;
315 26
		if (isset($endTag))
316
		{
317 7
			foreach ($contentAttributes as $attrName)
318
			{
319 7
				$this->attributes[$attrName] = substr($this->text, $this->pos, $endTag->getPos() - $this->pos);
320
			}
321
		}
322 21
		elseif ($requireEndTag)
323
		{
324 4
			return;
325
		}
326
327
		// Create this start tag
328 23
		$tag = $this->addBBCodeStartTag();
329
330
		// If an end tag was created, pair it with this start tag
331 23
		if (isset($endTag))
332
		{
333 7
			$tag->pairWith($endTag);
334
		}
335 23
	}
336
337
	/**
338
	* Parse the BBCode suffix starting at current position
339
	*
340
	* Used to explicitly pair specific tags together, e.g.
341
	*   [code:123][code]type your code here[/code][/code:123]
342
	*
343
	* @return void
344
	*/
345 50
	protected function parseBBCodeSuffix()
346
	{
347 50
		$this->bbcodeSuffix = '';
348 50
		if ($this->text[$this->pos] === ':')
349
		{
350
			// Capture the colon and the (0 or more) digits following it
351 7
			$spn = 1 + strspn($this->text, '0123456789', 1 + $this->pos);
352 7
			$this->bbcodeSuffix = substr($this->text, $this->pos, $spn);
353
354
			// Move past the suffix
355 7
			$this->pos += $spn;
356
		}
357 50
	}
358
359
	/**
360
	* Parse a quoted attribute value that starts at current offset
361
	*
362
	* @return string
363
	*/
364 14
	protected function parseQuotedAttributeValue()
365
	{
366 14
		$quote    = $this->text[$this->pos];
367 14
		$valuePos = $this->pos + 1;
368 14
		while (1)
369
		{
370
			// Look for the next quote
371 14
			$this->pos = strpos($this->text, $quote, $this->pos + 1);
372 14
			if ($this->pos === false)
373
			{
374
				// No matching quote. Apparently that string never ends...
375 3
				throw new RuntimeException;
376
			}
377
378
			// Test for an odd number of backslashes before this character
379 11
			$n = 0;
380
			do
381
			{
382 11
				++$n;
383
			}
384 11
			while ($this->text[$this->pos - $n] === '\\');
385
386 11
			if ($n % 2)
387
			{
388
				// If $n is odd, it means there's an even number of backslashes. We can exit this
389
				// loop
390 11
				break;
391
			}
392
		}
393
394
		// Unescape special characters ' " and \
395 11
		$attrValue = preg_replace(
396 11
			'#\\\\([\\\\\'"])#',
397 11
			'$1',
398 11
			substr($this->text, $valuePos, $this->pos - $valuePos)
399
		);
400
401
		// Skip past the closing quote
402 11
		++$this->pos;
403
404 11
		return $attrValue;
405
	}
406
}