Completed
Push — master ( 44baaa...9b2ed8 )
by Josh
18:12
created

Ruleset::trimFirstLine()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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