Completed
Push — master ( e52dbb...b532f4 )
by smiley
02:46
created

Parser   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 360
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 14
Bugs 0 Features 5
Metric Value
wmc 37
lcom 1
cbo 7
dl 0
loc 360
c 14
b 0
f 5
rs 8.6

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 2
A setOptions() 0 7 1
A getTagmap() 0 4 1
A getAllowed() 0 4 1
A getNoparse() 0 4 1
A getSingle() 0 4 1
A parse() 0 17 2
C __parse() 0 47 10
A getAttributes() 0 20 4
A loadInterfaces() 0 10 2
B loadTags() 0 22 6
A loadModules() 0 19 3
A throwPregError() 0 21 3
1
<?php
2
/**
3
 * Class Parser
4
 *
5
 * @version      1.1.0
6
 * @date         29.02.2016
7
 *
8
 * @filesource   Parser.php
9
 * @created      18.09.2015
10
 * @package      chillerlan\bbcode
11
 * @author       Smiley <[email protected]>
12
 * @copyright    2015 Smiley
13
 * @license      MIT
14
 */
15
16
namespace chillerlan\bbcode;
17
18
use chillerlan\bbcode\Language\LanguageInterface;
19
use chillerlan\bbcode\Modules\{BaseModuleInterface, ModuleInterface};
20
use chillerlan\bbcode\Traits\ClassLoaderTrait;
21
22
/**
23
 * Regexp BBCode parser
24
 *
25
 * idea and regexes from developers-guide.net years ago
26
 *
27
 * @link http://www.developers-guide.net/c/152-bbcode-parser-mit-noparse-tag-selbst-gemacht.html
28
 */
29
class Parser{
30
	use ClassLoaderTrait;
31
32
	/**
33
	 * Holds the preparsed BBCode
34
	 *
35
	 * @var string
36
	 */
37
	public $bbcode_pre;
38
39
	/**
40
	 * Map of Tag -> Module
41
	 *
42
	 * @var array
43
	 */
44
	protected $tagmap = [];
45
46
	/**
47
	 * Holds an array of noparse tags
48
	 *
49
	 * @var array
50
	 * @see \chillerlan\bbcode\Modules\Tagmap::$noparse_tags
51
	 */
52
	protected $noparse_tags = [];
53
54
	/**
55
	 * Holds an array of singletags
56
	 *
57
	 * @var array
58
	 * @see \chillerlan\bbcode\Modules\Tagmap::$singletags
59
	 */
60
	protected $singletags = [];
61
62
	/**
63
	 * Holds an array of allowed tags
64
	 *
65
	 * @var array
66
	 * @see \chillerlan\bbcode\Modules\Tagmap::$allowed_tags
67
	 */
68
	protected $allowed_tags = [];
69
70
	/**
71
	 * Holds an array of encoder module instances
72
	 *
73
	 * @var array
74
	 * @see \chillerlan\bbcode\Modules\ModuleInfo::$modules
75
	 */
76
	protected $modules = [];
77
78
	/**
79
	 * Holds the parser options
80
	 *
81
	 * @var \chillerlan\bbcode\ParserOptions
82
	 * @see \chillerlan\bbcode\Modules\Tagmap::$options
83
	 */
84
	protected $parserOptions;
85
86
	/**
87
	 * Holds the parser extension instance
88
	 *
89
	 * @var \chillerlan\bbcode\ParserExtensionInterface
90
	 */
91
	protected $parserExtensionInterface;
92
93
	/**
94
	 * Holds the base module instance
95
	 *
96
	 * @var \chillerlan\bbcode\Modules\BaseModuleInterface
97
	 */
98
	protected $baseModuleInterface;
99
100
	/**
101
	 * Holds the current encoder module
102
	 *
103
	 * @var \chillerlan\bbcode\Modules\ModuleInterface
104
	 */
105
	protected $moduleInterface;
106
107
	/**
108
	 * Holds a BBTemp instance
109
	 *
110
	 * @var \chillerlan\bbcode\BBTemp
111
	 */
112
	protected $BBTemp;
113
114
	/**
115
	 * Holds the translation class for the current language
116
	 *
117
	 * @var \chillerlan\bbcode\Language\LanguageInterface
118
	 */
119
	protected $languageInterface;
120
121
	/**
122
	 * Constructor.
123
	 *
124
	 * @param \chillerlan\bbcode\ParserOptions|null $options [optional]
125
	 */
126
	public function __construct(ParserOptions $options = null){
127
		$this->setOptions(!$options ? new ParserOptions : $options);
128
		$this->BBTemp = new BBTemp;
129
	}
130
131
	/**
132
	 * Sets the parser options
133
	 *
134
	 * @param \chillerlan\bbcode\ParserOptions $options
135
	 *
136
	 * @throws \chillerlan\bbcode\BBCodeException
137
	 */
138
	public function setOptions(ParserOptions $options){
139
		$this->parserOptions       = $options;
140
141
		$this->loadInterfaces();
142
		$this->loadModules();
143
		$this->loadTags();
144
	}
145
146
	/**
147
	 * Returns the current tagmap
148
	 */
149
	public function getTagmap():array{
150
		ksort($this->tagmap);
151
		return $this->tagmap;
152
	}
153
154
	/**
155
	 * Returns the currently allowed tags
156
	 */
157
	public function getAllowed():array{
158
		sort($this->allowed_tags);
159
		return $this->allowed_tags;
160
	}
161
162
	/**
163
	 * Returns the noparse tags
164
	 */
165
	public function getNoparse():array{
166
		sort($this->noparse_tags);
167
		return $this->noparse_tags;
168
	}
169
170
	/**
171
	 * Returns the singletags
172
	 */
173
	public function getSingle():array{
174
		sort($this->singletags);
175
		return $this->singletags;
176
	}
177
178
	/**
179
	 * Encodes a BBCode string to HTML (or whatevs)
180
	 *
181
	 * @param string $bbcode
182
	 *
183
	 * @return string
184
	 */
185
	public function parse(string $bbcode):string{
186
187
		if($this->parserOptions->sanitize){
188
			$bbcode = $this->baseModuleInterface->sanitize($bbcode);
189
		}
190
191
		$bbcode = $this->parserExtensionInterface->pre($bbcode);
192
		// change/move potentially closed singletags before -> base module
193
		$this->bbcode_pre = $bbcode;
194
		$bbcode = preg_replace('#\[('.$this->parserOptions->singletags.')((?:\s|=)[^]]*)?]#is', '[$1$2][/$1]', $bbcode);
195
		$bbcode = str_replace(["\r", "\n"], ['', $this->parserOptions->eol_placeholder], $bbcode);
196
		$bbcode = $this->__parse($bbcode);
197
		$bbcode = $this->parserExtensionInterface->post($bbcode);
198
		$bbcode = str_replace($this->parserOptions->eol_placeholder, $this->parserOptions->eol_token, $bbcode);
199
200
		return $bbcode;
201
	}
202
203
	/**
204
	 * strng regexp bbcode killer
205
	 *
206
	 * @param string|array $bbcode BBCode as string or matches as array - callback from preg_replace_callback()
207
	 *
208
	 * @return string
209
	 * @throws \chillerlan\bbcode\BBCodeException
210
	 */
211
	protected function __parse($bbcode):string{
212
		static $callback_count = 0;
213
		$callback = false;
214
		$preg_error = PREG_NO_ERROR;
215
216
		if(is_array($bbcode) && isset($bbcode['tag'], $bbcode['attributes'], $bbcode['content'])){
217
			$tag = strtolower($bbcode['tag']);
218
			$attributes = $this->getAttributes($bbcode['attributes']);
219
			$content = $bbcode['content'];
220
221
			$callback = true;
222
			$callback_count++;
223
		}
224
		else if(is_string($bbcode) && !empty($bbcode)){
225
			$tag = null;
226
			$attributes = [];
227
			$content = $bbcode;
228
		}
229
		else{
230
			return '';
231
		}
232
233
		if($callback_count < (int)$this->parserOptions->nesting_limit && !in_array($tag, $this->noparse_tags)){
234
			$pattern = '#\[(?<tag>\w+)(?<attributes>(?:\s|=)[^]]*)?](?<content>(?:[^[]|\[(?!/?\1((?:\s|=)[^]]*)?])|(?R))*)\[/\1]#';
235
			$content = preg_replace_callback($pattern, __METHOD__, $content);
236
			$preg_error = preg_last_error();
237
		}
238
239
		$this->throwPregError($preg_error, $tag);
0 ignored issues
show
Bug introduced by
It seems like $tag defined by strtolower($bbcode['tag']) on line 217 can also be of type string; however, chillerlan\bbcode\Parser::throwPregError() does only seem to accept null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
240
241
		if($callback && isset($this->tagmap[$tag]) && in_array($tag, $this->allowed_tags)){
242
			$this->BBTemp->tag               = $tag;
243
			$this->BBTemp->attributes        = $attributes;
244
			$this->BBTemp->content           = $content;
245
			$this->BBTemp->parserOptions     = $this->parserOptions;
246
			$this->BBTemp->languageInterface = $this->languageInterface;
247
			$this->BBTemp->depth             = $callback_count;
248
249
			$this->moduleInterface = $this->modules[$this->tagmap[$tag]];
250
			$this->moduleInterface->setBBTemp($this->BBTemp);
251
			$content = $this->moduleInterface->transform();
252
		}
253
254
		$callback_count = 0;
255
256
		return $content;
257
	}
258
259
	/**
260
	 * The attributes parser
261
	 *
262
	 * @param string $attributes
263
	 *
264
	 * @return array
265
	 * @throws \chillerlan\bbcode\BBCodeException
266
	 */
267
	protected function getAttributes(string $attributes):array{
268
		$attr    = [];
269
		$pattern = '#(?<name>^|\w+)\=(\'?)(?<value>[^\']*?)\2(?: |$)#';
270
271
		if(preg_match_all($pattern, $attributes, $matches, PREG_SET_ORDER) > 0){
272
273
			foreach($matches as $attribute){
274
				$name = empty($attribute['name']) ? $this->parserOptions->bbtag_placeholder : strtolower(trim($attribute['name']));
275
276
				$value = trim($attribute['value']);
277
				$value = $this->baseModuleInterface->sanitize($value);
278
279
				$attr[$name] = $value;
280
			}
281
		}
282
283
		$this->throwPregError(preg_last_error());
284
285
		return $attr;
286
	}
287
288
	/**
289
	 * Loads the base Interfaces (BaseModule, Language, ParserExtension)
290
	 *
291
	 * @throws \chillerlan\bbcode\BBCodeException
292
	 */
293
	protected function loadInterfaces(){
294
		$this->baseModuleInterface = $this->__loadClass($this->parserOptions->baseModuleInterface, BaseModuleInterface::class);
295
		$this->languageInterface   = $this->__loadClass($this->parserOptions->languageInterface, LanguageInterface::class);
296
297
		if($this->parserOptions->parserExtensionInterface){
298
			$this->parserExtensionInterface =
299
				$this->__loadClass($this->parserOptions->parserExtensionInterface, ParserExtensionInterface::class, $this->parserOptions);
300
		}
301
302
	}
303
304
	/**
305
	 * Loads allowed/all tags
306
	 */
307
	protected function loadTags(){
308
309
		if(is_array($this->parserOptions->allowed_tags) && !empty($this->parserOptions->allowed_tags)){
310
311
			foreach($this->parserOptions->allowed_tags as $tag){
312
313
				if(array_key_exists($tag, $this->tagmap)){
314
					$this->allowed_tags[] = $tag;
315
				}
316
317
			}
318
319
		}
320
		else{
321
322
			if($this->parserOptions->allow_all){
323
				$this->allowed_tags = array_keys($this->tagmap);
324
			}
325
326
		}
327
328
	}
329
330
	/**
331
	 * Loads the parser modules
332
	 *
333
	 * @throws \chillerlan\bbcode\BBCodeException
334
	 */
335
	protected function loadModules(){
336
		$module_info = $this->baseModuleInterface->getInfo();
337
338
		foreach($module_info->modules as $module){
339
			$this->moduleInterface = $this->__loadClass($module, ModuleInterface::class);
340
			$tagmap = $this->moduleInterface->getTags();
341
342
			foreach($tagmap->tags as $tag){
343
				$this->tagmap[$tag] = $module;
344
			}
345
346
			$this->modules[$module] = $this->moduleInterface;
347
			$this->noparse_tags     = array_merge($this->noparse_tags, $tagmap->noparse_tags);
348
			$this->singletags       = array_merge($this->singletags, $tagmap->singletags);
349
		}
350
351
		$this->parserOptions->eol_token  = $module_info->eol_token;
352
		$this->parserOptions->singletags = implode('|', $this->singletags);
353
	}
354
355
	/**
356
	 * testing...
357
	 *
358
	 * @param      $preg_error
359
	 *
360
	 * @param null $tag
361
	 *
362
	 * @throws \chillerlan\bbcode\BBCodeException
363
	 * @link https://github.com/chillerlan/bbcode/issues/1
364
	 * @codeCoverageIgnore
365
	 */
366
	protected function throwPregError($preg_error, $tag = null){
367
368
		if($preg_error !== PREG_NO_ERROR){
369
370
			$error = [
371
				PREG_INTERNAL_ERROR        => 'PREG_INTERNAL_ERROR',
372
				PREG_BACKTRACK_LIMIT_ERROR => 'PREG_BACKTRACK_LIMIT_ERROR',
373
				PREG_RECURSION_LIMIT_ERROR => 'PREG_RECURSION_LIMIT_ERROR',
374
				PREG_BAD_UTF8_ERROR        => 'PREG_BAD_UTF8_ERROR',
375
				PREG_BAD_UTF8_OFFSET_ERROR => 'PREG_BAD_UTF8_OFFSET_ERROR',
376
				PREG_JIT_STACKLIMIT_ERROR  => 'PREG_JIT_STACKLIMIT_ERROR',
377
			][$preg_error];
378
379
			$str = $tag
380
				? $this->languageInterface->parserExceptionCallback()
0 ignored issues
show
Documentation Bug introduced by
The method parserExceptionCallback does not exist on object<chillerlan\bbcode...uage\LanguageInterface>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
381
				: $this->languageInterface->parserExceptionMatchall();
0 ignored issues
show
Documentation Bug introduced by
The method parserExceptionMatchall does not exist on object<chillerlan\bbcode...uage\LanguageInterface>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
382
383
			throw new BBCodeException(sprintf($str, $error, $preg_error));
384
		}
385
386
	}
387
388
}
389