Completed
Push — master ( 77f96d...4d3370 )
by Josh
24:27 queued 11:54
created

Ruleset   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 489
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
wmc 49
lcom 1
cbo 3
dl 0
loc 489
rs 8.5454
c 0
b 0
f 0

33 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A offsetExists() 0 4 1
A offsetGet() 0 4 1
A offsetSet() 0 4 1
A offsetUnset() 0 4 1
B asConfig() 0 55 5
C merge() 0 23 8
B remove() 0 36 5
A addBooleanRule() 0 11 2
A addTargetedRule() 0 6 1
A allowChild() 0 4 1
A allowDescendant() 0 4 1
A autoClose() 0 4 1
A autoReopen() 0 4 1
A breakParagraph() 0 4 1
A closeAncestor() 0 4 1
A closeParent() 0 4 1
A createChild() 0 4 1
A createParagraphs() 0 4 1
A denyChild() 0 4 1
A denyDescendant() 0 4 1
A disableAutoLineBreaks() 0 4 1
A enableAutoLineBreaks() 0 4 1
A fosterParent() 0 4 1
A ignoreSurroundingWhitespace() 0 4 1
A ignoreTags() 0 4 1
A ignoreText() 0 4 1
A isTransparent() 0 4 1
A preventLineBreaks() 0 4 1
A requireParent() 0 4 1
A requireAncestor() 0 4 1
A suspendAutoLineBreaks() 0 4 1
A trimFirstLine() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Ruleset 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Ruleset, and based on these observations, apply Extract Interface, too.

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\Configurator\Collections;
9
10
use ArrayAccess;
11
use InvalidArgumentException;
12
use RuntimeException;
13
use s9e\TextFormatter\Configurator\ConfigProvider;
14
use s9e\TextFormatter\Configurator\JavaScript\Dictionary;
15
use s9e\TextFormatter\Configurator\Validators\TagName;
16
use s9e\TextFormatter\Parser;
17
18
/**
19
* @see docs/Rules.md
20
*/
21
class Ruleset extends Collection implements ArrayAccess, ConfigProvider
22
{
23
	/**
24
	* Constructor
25
	*/
26
	public function __construct()
27
	{
28
		$this->clear();
29
	}
30
31
	//==========================================================================
32
	// ArrayAccess methods
33
	//==========================================================================
34
35
	/**
36
	* Test whether a rule category exists
37
	*
38
	* @param  string $k Rule name, e.g. "allowChild" or "isTransparent"
39
	*/
40
	public function offsetExists($k)
41
	{
42
		return isset($this->items[$k]);
43
	}
44
45
	/**
46
	* Return the content of a rule category
47
	*
48
	* @param  string $k Rule name, e.g. "allowChild" or "isTransparent"
49
	* @return mixed
50
	*/
51
	public function offsetGet($k)
52
	{
53
		return $this->items[$k];
54
	}
55
56
	/**
57
	* Not supported
58
	*/
59
	public function offsetSet($k, $v)
60
	{
61
		throw new RuntimeException('Not supported');
62
	}
63
64
	/**
65
	* Clear a subset of the rules
66
	*
67
	* @see clear()
68
	*
69
	* @param  string $k Rule name, e.g. "allowChild" or "isTransparent"
70
	*/
71
	public function offsetUnset($k)
72
	{
73
		return $this->remove($k);
74
	}
75
76
	//==========================================================================
77
	// Generic methods
78
	//==========================================================================
79
80
	/**
81
	* {@inheritdoc}
82
	*/
83
	public function asConfig()
84
	{
85
		$config = $this->items;
86
87
		// Remove rules that are not needed at parsing time. All of those are resolved when building
88
		// the allowed bitfields
89
		unset($config['allowChild']);
90
		unset($config['allowDescendant']);
91
		unset($config['denyChild']);
92
		unset($config['denyDescendant']);
93
		unset($config['requireParent']);
94
95
		// Pack boolean rules into a bitfield
96
		$bitValues = [
97
			'autoClose'                   => Parser::RULE_AUTO_CLOSE,
98
			'autoReopen'                  => Parser::RULE_AUTO_REOPEN,
99
			'breakParagraph'              => Parser::RULE_BREAK_PARAGRAPH,
100
			'createParagraphs'            => Parser::RULE_CREATE_PARAGRAPHS,
101
			'disableAutoLineBreaks'       => Parser::RULE_DISABLE_AUTO_BR,
102
			'enableAutoLineBreaks'        => Parser::RULE_ENABLE_AUTO_BR,
103
			'ignoreSurroundingWhitespace' => Parser::RULE_IGNORE_WHITESPACE,
104
			'ignoreTags'                  => Parser::RULE_IGNORE_TAGS,
105
			'ignoreText'                  => Parser::RULE_IGNORE_TEXT,
106
			'isTransparent'               => Parser::RULE_IS_TRANSPARENT,
107
			'preventLineBreaks'           => Parser::RULE_PREVENT_BR,
108
			'suspendAutoLineBreaks'       => Parser::RULE_SUSPEND_AUTO_BR,
109
			'trimFirstLine'               => Parser::RULE_TRIM_FIRST_LINE
110
		];
111
112
		$bitfield = 0;
113
		foreach ($bitValues as $ruleName => $bitValue)
114
		{
115
			if (!empty($config[$ruleName]))
116
			{
117
				$bitfield |= $bitValue;
118
			}
119
120
			unset($config[$ruleName]);
121
		}
122
123
		// In order to speed up lookups, we use the tag names as keys
124
		foreach (['closeAncestor', 'closeParent', 'fosterParent'] as $ruleName)
125
		{
126
			if (isset($config[$ruleName]))
127
			{
128
				$targets = array_fill_keys($config[$ruleName], 1);
129
				$config[$ruleName] = new Dictionary($targets);
130
			}
131
		}
132
133
		// Add the bitfield to the config
134
		$config['flags'] = $bitfield;
135
136
		return $config;
137
	}
138
139
	/**
140
	* Merge a set of rules into this collection
141
	*
142
	* @param array|Ruleset $rules     2D array of rule definitions, or instance of Ruleset
143
	* @param bool          $overwrite Whether to overwrite scalar rules (e.g. boolean rules)
144
	*/
145
	public function merge($rules, $overwrite = true)
146
	{
147
		if (!is_array($rules)
148
		 && !($rules instanceof self))
149
		{
150
			throw new InvalidArgumentException('merge() expects an array or an instance of Ruleset');
151
		}
152
153
		foreach ($rules as $action => $value)
154
		{
155
			if (is_array($value))
156
			{
157
				foreach ($value as $tagName)
158
				{
159
					$this->$action($tagName);
160
				}
161
			}
162
			elseif ($overwrite || !isset($this->items[$action]))
163
			{
164
				$this->$action($value);
165
			}
166
		}
167
	}
168
169
	/**
170
	* Remove a specific rule, or all the rules of a given type
171
	*
172
	* @param  string $type    Type of rules to clear
173
	* @param  string $tagName Name of the target tag, or none to remove all rules of given type
174
	* @return void
175
	*/
176
	public function remove($type, $tagName = null)
177
	{
178
		if (preg_match('(^default(?:Child|Descendant)Rule)', $type))
179
		{
180
			throw new RuntimeException('Cannot remove ' . $type);
181
		}
182
183
		if (isset($tagName))
184
		{
185
			$tagName = TagName::normalize($tagName);
186
187
			if (isset($this->items[$type]))
188
			{
189
				// Compute the difference between current list and our one tag name
190
				$this->items[$type] = array_diff(
191
					$this->items[$type],
192
					[$tagName]
193
				);
194
195
				if (empty($this->items[$type]))
196
				{
197
					// If the list is now empty, keep it neat and unset it
198
					unset($this->items[$type]);
199
				}
200
				else
201
				{
202
					// If the list still have names, keep it neat and rearrange keys
203
					$this->items[$type] = array_values($this->items[$type]);
204
				}
205
			}
206
		}
207
		else
208
		{
209
			unset($this->items[$type]);
210
		}
211
	}
212
213
	//==========================================================================
214
	// Rules
215
	//==========================================================================
216
217
	/**
218
	* Add a boolean rule
219
	*
220
	* @param  string $ruleName Name of the rule
221
	* @param  bool   $bool     Whether to enable or disable the rule
222
	* @return self
223
	*/
224
	protected function addBooleanRule($ruleName, $bool)
225
	{
226
		if (!is_bool($bool))
227
		{
228
			throw new InvalidArgumentException($ruleName . '() expects a boolean');
229
		}
230
231
		$this->items[$ruleName] = $bool;
232
233
		return $this;
234
	}
235
236
	/**
237
	* Add a targeted rule
238
	*
239
	* @param  string $ruleName Name of the rule
240
	* @param  string $tagName  Name of the target tag
241
	* @return self
242
	*/
243
	protected function addTargetedRule($ruleName, $tagName)
244
	{
245
		$this->items[$ruleName][] = TagName::normalize($tagName);
246
247
		return $this;
248
	}
249
250
	/**
251
	* Add an allowChild rule
252
	*
253
	* @param  string $tagName Name of the target tag
254
	* @return self
255
	*/
256
	public function allowChild($tagName)
257
	{
258
		return $this->addTargetedRule('allowChild', $tagName);
259
	}
260
261
	/**
262
	* Add an allowDescendant rule
263
	*
264
	* @param  string $tagName Name of the target tag
265
	* @return self
266
	*/
267
	public function allowDescendant($tagName)
268
	{
269
		return $this->addTargetedRule('allowDescendant', $tagName);
270
	}
271
272
	/**
273
	* Add an autoClose rule
274
	*
275
	* NOTE: this rule exists so that plugins don't have to specifically handle tags whose end tag
276
	*       may/must be omitted such as <hr> or [img]
277
	*
278
	* @param  bool $bool Whether or not the tag should automatically be closed if its start tag is not followed by an end tag
279
	* @return self
280
	*/
281
	public function autoClose($bool = true)
282
	{
283
		return $this->addBooleanRule('autoClose', $bool);
284
	}
285
286
	/**
287
	* Add an autoReopen rule
288
	*
289
	* @param  bool $bool Whether or not the tag should automatically be reopened if closed by an end tag of a different name
290
	* @return self
291
	*/
292
	public function autoReopen($bool = true)
293
	{
294
		return $this->addBooleanRule('autoReopen', $bool);
295
	}
296
297
	/**
298
	* Add a breakParagraph rule
299
	*
300
	* @param  bool $bool Whether or not this tag breaks current paragraph if applicable
301
	* @return self
302
	*/
303
	public function breakParagraph($bool = true)
304
	{
305
		return $this->addBooleanRule('breakParagraph', $bool);
306
	}
307
308
	/**
309
	* Add a closeAncestor rule
310
	*
311
	* @param  string $tagName Name of the target tag
312
	* @return self
313
	*/
314
	public function closeAncestor($tagName)
315
	{
316
		return $this->addTargetedRule('closeAncestor', $tagName);
317
	}
318
319
	/**
320
	* Add a closeParent rule
321
	*
322
	* @param  string $tagName Name of the target tag
323
	* @return self
324
	*/
325
	public function closeParent($tagName)
326
	{
327
		return $this->addTargetedRule('closeParent', $tagName);
328
	}
329
330
	/**
331
	* Add a createChild rule
332
	*
333
	* @param  string $tagName Name of the target tag
334
	* @return self
335
	*/
336
	public function createChild($tagName)
337
	{
338
		return $this->addTargetedRule('createChild', $tagName);
339
	}
340
341
	/**
342
	* Add a createParagraphs rule
343
	*
344
	* @param  bool $bool Whether or not paragraphs should automatically be created to handle content
345
	* @return self
346
	*/
347
	public function createParagraphs($bool = true)
348
	{
349
		return $this->addBooleanRule('createParagraphs', $bool);
350
	}
351
352
	/**
353
	* Add a denyChild rule
354
	*
355
	* @param  string $tagName Name of the target tag
356
	* @return self
357
	*/
358
	public function denyChild($tagName)
359
	{
360
		return $this->addTargetedRule('denyChild', $tagName);
361
	}
362
363
	/**
364
	* Add a denyDescendant rule
365
	*
366
	* @param  string $tagName Name of the target tag
367
	* @return self
368
	*/
369
	public function denyDescendant($tagName)
370
	{
371
		return $this->addTargetedRule('denyDescendant', $tagName);
372
	}
373
374
	/**
375
	* Add a disableAutoLineBreaks rule
376
	*
377
	* @param  bool $bool Whether or not automatic line breaks should be disabled
378
	* @return self
379
	*/
380
	public function disableAutoLineBreaks($bool = true)
381
	{
382
		return $this->addBooleanRule('disableAutoLineBreaks', $bool);
383
	}
384
385
	/**
386
	* Add a enableAutoLineBreaks rule
387
	*
388
	* @param  bool $bool Whether or not automatic line breaks should be enabled
389
	* @return self
390
	*/
391
	public function enableAutoLineBreaks($bool = true)
392
	{
393
		return $this->addBooleanRule('enableAutoLineBreaks', $bool);
394
	}
395
396
	/**
397
	* Add a fosterParent rule
398
	*
399
	* @param  string $tagName Name of the target tag
400
	* @return self
401
	*/
402
	public function fosterParent($tagName)
403
	{
404
		return $this->addTargetedRule('fosterParent', $tagName);
405
	}
406
407
	/**
408
	* Ignore (some) whitespace around tags
409
	*
410
	* When true, some whitespace around this tag will be ignored (not transformed to line breaks.)
411
	* Up to 2 lines outside of a tag pair and 1 line inside of it:
412
	*     {2 lines}{START_TAG}{1 line}{CONTENT}{1 line}{END_TAG}{2 lines}
413
	*
414
	* @param  bool $bool Whether whitespace around this tag should be ignored
415
	* @return self
416
	*/
417
	public function ignoreSurroundingWhitespace($bool = true)
418
	{
419
		return $this->addBooleanRule('ignoreSurroundingWhitespace', $bool);
420
	}
421
422
	/**
423
	* Add an ignoreTags rule
424
	*
425
	* @param  bool $bool Whether to silently ignore all tags until current tag is closed
426
	* @return self
427
	*/
428
	public function ignoreTags($bool = true)
429
	{
430
		return $this->addBooleanRule('ignoreTags', $bool);
431
	}
432
433
	/**
434
	* Add an ignoreText rule
435
	*
436
	* @param  bool $bool Whether or not the tag should ignore text nodes
437
	* @return self
438
	*/
439
	public function ignoreText($bool = true)
440
	{
441
		return $this->addBooleanRule('ignoreText', $bool);
442
	}
443
444
	/**
445
	* Add a isTransparent rule
446
	*
447
	* @param  bool $bool Whether or not the tag should use the "transparent" content model
448
	* @return self
449
	*/
450
	public function isTransparent($bool = true)
451
	{
452
		return $this->addBooleanRule('isTransparent', $bool);
453
	}
454
455
	/**
456
	* Add a preventLineBreaks rule
457
	*
458
	* @param  bool $bool Whether or not manual line breaks should be ignored in this tag's context
459
	* @return self
460
	*/
461
	public function preventLineBreaks($bool = true)
462
	{
463
		return $this->addBooleanRule('preventLineBreaks', $bool);
464
	}
465
466
	/**
467
	* Add a requireParent rule
468
	*
469
	* @param  string $tagName Name of the target tag
470
	* @return self
471
	*/
472
	public function requireParent($tagName)
473
	{
474
		return $this->addTargetedRule('requireParent', $tagName);
475
	}
476
477
	/**
478
	* Add a requireAncestor rule
479
	*
480
	* @param  string $tagName Name of the target tag
481
	* @return self
482
	*/
483
	public function requireAncestor($tagName)
484
	{
485
		return $this->addTargetedRule('requireAncestor', $tagName);
486
	}
487
488
	/**
489
	* Add a suspendAutoLineBreaks rule
490
	*
491
	* @param  bool $bool Whether or not automatic line breaks should be temporarily suspended
492
	* @return self
493
	*/
494
	public function suspendAutoLineBreaks($bool = true)
495
	{
496
		return $this->addBooleanRule('suspendAutoLineBreaks', $bool);
497
	}
498
499
	/**
500
	* Add a trimFirstLine rule
501
	*
502
	* @param  bool $bool Whether the white space inside this tag should be trimmed 
503
	* @return self
504
	*/
505
	public function trimFirstLine($bool = true)
506
	{
507
		return $this->addBooleanRule('trimFirstLine', $bool);
508
	}
509
}