Completed
Push — master ( 71fe44...f72332 )
by Josh
21:07
created

Ruleset::clear()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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