Parser   B
last analyzed

Complexity

Total Complexity 50

Size/Duplication

Total Lines 381
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 50
eloc 122
dl 0
loc 381
ccs 118
cts 118
cp 1
rs 8.4
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A parse() 0 22 4
A addBBCodeSelfClosingTag() 0 6 1
A addBBCodeEndTag() 0 3 1
A parseBBCodeSuffix() 0 11 2
C parseBBCode() 0 74 16
B parseAttributes() 0 53 11
A captureEndTag() 0 14 3
A getTagName() 0 6 2
A addBBCodeStartTag() 0 7 2
A parseAttributeValue() 0 23 3
A parseQuotedAttributeValue() 0 33 5

How to fix   Complexity   

Complex Class

Complex classes like Parser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Parser, and based on these observations, apply Extract Interface, too.

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\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 52
	public function parse($text, array $matches)
65
	{
66 52
		$this->text          = $text;
67 52
		$this->textLen       = strlen($text);
68 52
		$this->uppercaseText = '';
69 52
		foreach ($matches as $m)
70
		{
71 52
			$this->bbcodeName = strtoupper($m[1][0]);
72 52
			if (!isset($this->config['bbcodes'][$this->bbcodeName]))
73
			{
74 3
				continue;
75
			}
76 52
			$this->bbcodeConfig = $this->config['bbcodes'][$this->bbcodeName];
77 52
			$this->startPos     = $m[0][1];
78 52
			$this->pos          = $this->startPos + strlen($m[0][0]);
79
80
			try
81
			{
82 52
				$this->parseBBCode();
83
			}
84 6
			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 24
	protected function addBBCodeEndTag()
97
	{
98 24
		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 25
	protected function addBBCodeStartTag()
120
	{
121 25
		$prio = ($this->bbcodeSuffix !== '') ? -10 : 0;
122 25
		$tag  = $this->parser->addStartTag($this->getTagName(), $this->startPos, $this->pos - $this->startPos, $prio);
123
		$tag->setAttributes($this->attributes);
124 25
125
		return $tag;
126
	}
127
128
	/**
129
	* Parse the end tag that matches given BBCode name and suffix starting at current position
130
	*
131
	* @return Tag|null
132 12
	*/
133
	protected function captureEndTag()
134 12
	{
135
		if (empty($this->uppercaseText))
136 12
		{
137
			$this->uppercaseText = strtoupper($this->text);
138 12
		}
139 12
		$match     = '[/' . $this->bbcodeName . $this->bbcodeSuffix . ']';
140 12
		$endTagPos = strpos($this->uppercaseText, $match, $this->pos);
141
		if ($endTagPos === false)
142 5
		{
143
			return;
144
		}
145 8
146
		return $this->parser->addEndTag($this->getTagName(), $endTagPos, strlen($match));
147
	}
148
149
	/**
150
	* Get the tag name for current BBCode
151
	*
152
	* @return string
153 39
	*/
154
	protected function getTagName()
155
	{
156 39
		// Use the configured tagName if available, or reuse the BBCode's name otherwise
157 1
		return (isset($this->bbcodeConfig['tagName']))
158 39
		     ? $this->bbcodeConfig['tagName']
159
		     : $this->bbcodeName;
160
	}
161
162
	/**
163
	* Parse attributes starting at current position
164
	*
165
	* @return void
166 51
	*/
167
	protected function parseAttributes()
168 51
	{
169 51
		$firstPos = $this->pos;
170 51
		$this->attributes = [];
171
		while ($this->pos < $this->textLen)
172 51
		{
173 51
			$c = $this->text[$this->pos];
174
			if (strpos(" \n\t", $c) !== false)
175 26
			{
176 26
				++$this->pos;
177
				continue;
178 50
			}
179
			if (strpos('/]', $c) !== false)
180 43
			{
181
				return;
182
			}
183
184 29
			// Capture the attribute name
185 29
			$spn = strspn($this->text, 'abcdefghijklmnopqrstuvwxyz_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-', $this->pos);
186
			if ($spn)
187 20
			{
188 20
				$attrName = strtolower(substr($this->text, $this->pos, $spn));
189 20
				$this->pos += $spn;
190
				if ($this->pos >= $this->textLen)
191
				{
192 1
					// The attribute name extends to the end of the text
193
					throw new RuntimeException;
194 19
				}
195
				if ($this->text[$this->pos] !== '=')
196
				{
197 19
					// It's an attribute name not followed by an equal sign, ignore it
198
					continue;
199
				}
200 10
			}
201
			elseif ($c === '=' && $this->pos === $firstPos)
202
			{
203 9
				// This is the default param, e.g. [quote=foo]
204 2
				$attrName = (isset($this->bbcodeConfig['defaultAttribute']))
205 9
				          ? $this->bbcodeConfig['defaultAttribute']
206
				          : strtolower($this->bbcodeName);
207
			}
208
			else
209 1
			{
210
				throw new RuntimeException;
211
			}
212
213 26
			// Move past the = and make sure we're not at the end of the text
214
			if (++$this->pos >= $this->textLen)
215 1
			{
216
				throw new RuntimeException;
217
			}
218 25
219
			$this->attributes[$attrName] = $this->parseAttributeValue();
220
		}
221
	}
222
223
	/**
224
	* Parse the attribute value starting at current position
225
	*
226
	* @return string
227 25
	*/
228
	protected function parseAttributeValue()
229
	{
230 25
		// Test whether the value is in quotes
231
		if ($this->text[$this->pos] === '"' || $this->text[$this->pos] === "'")
232 14
		{
233
			return $this->parseQuotedAttributeValue();
234
		}
235
236
		// Capture everything up to whichever comes first:
237
		//  - an endline
238
		//  - whitespace followed by a slash and a closing bracket
239
		//  - a closing bracket, optionally preceded by whitespace
240
		//  - whitespace followed by another attribute (name followed by equal sign)
241
		//
242
		// NOTE: this is for compatibility with some forums (such as vBulletin it seems)
243
		//       that do not put attribute values in quotes, e.g.
244 11
		//       [quote=John Smith;123456] (quoting "John Smith" from post #123456)
245
		preg_match('((?:[^\\s\\]]|[ \\t](?!\\s*(?:[-\\w]+=|/?\\])))*)', $this->text, $m, 0, $this->pos);
246 11
247 11
		$attrValue  = $m[0];
248
		$this->pos += strlen($attrValue);
249 11
250
		return $attrValue;
251
	}
252
253
	/**
254
	* Parse current BBCode
255
	*
256
	* @return void
257 52
	*/
258
	protected function parseBBCode()
259 52
	{
260
		$this->parseBBCodeSuffix();
261
262 52
		// Test whether this is an end tag
263
		if ($this->text[$this->startPos + 1] === '/')
264
		{
265
			// Test whether the tag is properly closed and whether this tag has an identifier.
266
			// We skip end tags that carry an identifier because they're automatically added
267 27
			// when their start tag is processed
268
			if (substr($this->text, $this->pos, 1) === ']' && $this->bbcodeSuffix === '')
269 24
			{
270 24
				++$this->pos;
271
				$this->addBBCodeEndTag();
272
			}
273 27
274
			return;
275
		}
276
277 51
		// Parse attributes
278
		$this->parseAttributes();
279
280 45
		// Test whether the tag is properly closed
281
		if (substr($this->text, $this->pos, 1) === ']')
282 28
		{
283
			++$this->pos;
284
		}
285
		else
286
		{
287 17
			// Test whether this is a self-closing tag
288
			if (substr($this->text, $this->pos, 2) === '/]')
289 13
			{
290 13
				$this->pos += 2;
291
				$this->addBBCodeSelfClosingTag();
292
			}
293 17
294
			return;
295
		}
296
297 28
		// Record the names of attributes that need the content of this tag
298 28
		$contentAttributes = [];
299
		if (isset($this->bbcodeConfig['contentAttributes']))
300 5
		{
301
			foreach ($this->bbcodeConfig['contentAttributes'] as $attrName)
302 5
			{
303
				if (!isset($this->attributes[$attrName]))
304 4
				{
305
					$contentAttributes[] = $attrName;
306
				}
307
			}
308
		}
309
310 28
		// Look ahead and parse the end tag that matches this tag, if applicable
311 28
		$requireEndTag = ($this->bbcodeSuffix || !empty($this->bbcodeConfig['forceLookahead']));
312 28
		$endTag = ($requireEndTag || !empty($contentAttributes)) ? $this->captureEndTag() : null;
313
		if (isset($endTag))
314 8
		{
315
			foreach ($contentAttributes as $attrName)
316 3
			{
317
				$this->attributes[$attrName] = substr($this->text, $this->pos, $endTag->getPos() - $this->pos);
318
			}
319 23
		}
320
		elseif ($requireEndTag)
321 4
		{
322
			return;
323
		}
324
325 25
		// Create this start tag
326
		$tag = $this->addBBCodeStartTag();
327
328 25
		// If an end tag was created, pair it with this start tag
329
		if (isset($endTag))
330 8
		{
331
			$tag->pairWith($endTag);
332
		}
333
	}
334
335
	/**
336
	* Parse the BBCode suffix starting at current position
337
	*
338
	* Used to explicitly pair specific tags together, e.g.
339
	*   [code:123][code]type your code here[/code][/code:123]
340
	*
341
	* @return void
342 52
	*/
343
	protected function parseBBCodeSuffix()
344 52
	{
345 52
		$this->bbcodeSuffix = '';
346
		if ($this->text[$this->pos] === ':')
347
		{
348 7
			// Capture the colon and the (0 or more) digits following it
349 7
			$spn = 1 + strspn($this->text, '0123456789', 1 + $this->pos);
350
			$this->bbcodeSuffix = substr($this->text, $this->pos, $spn);
351
352 7
			// Move past the suffix
353
			$this->pos += $spn;
354
		}
355
	}
356
357
	/**
358
	* Parse a quoted attribute value that starts at current offset
359
	*
360
	* @return string
361 14
	*/
362
	protected function parseQuotedAttributeValue()
363 14
	{
364 14
		$quote    = $this->text[$this->pos];
365
		$valuePos = $this->pos + 1;
366
		do
367
		{
368 14
			// Look for the next quote
369 14
			$this->pos = strpos($this->text, $quote, $this->pos + 1);
370
			if ($this->pos === false)
371
			{
372 3
				// No matching quote. Apparently that string never ends...
373
				throw new RuntimeException;
374
			}
375
376 11
			// Test for an odd number of backslashes before this character
377 11
			$n = 1;
378
			while ($this->text[$this->pos - $n] === '\\')
379 3
			{
380
				++$n;
381
			}
382 11
		}
383
		while ($n % 2 === 0);
384 11
385 11
		$attrValue = substr($this->text, $valuePos, $this->pos - $valuePos);
386
		if (strpos($attrValue, '\\') !== false)
387 3
		{
388
			$attrValue = strtr($attrValue, ['\\\\' => '\\', '\\"' => '"', "\\'" => "'"]);
389
		}
390
391 11
		// Skip past the closing quote
392
		++$this->pos;
393 11
394
		return $attrValue;
395
	}
396
}