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

RulesHelper::applyTargetedRule()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 15
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 3
nop 5
dl 0
loc 15
rs 9.2
c 0
b 0
f 0
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\Helpers;
9
10
use s9e\TextFormatter\Configurator\Collections\Ruleset;
11
use s9e\TextFormatter\Configurator\Collections\TagCollection;
12
13
abstract class RulesHelper
14
{
15
	/**
16
	* Generate the allowedChildren and allowedDescendants bitfields for every tag and for the root context
17
	*
18
	* @param  TagCollection $tags
19
	* @param  Ruleset       $rootRules
20
	* @return array
21
	*/
22
	public static function getBitfields(TagCollection $tags, Ruleset $rootRules)
23
	{
24
		$rules = ['*root*' => iterator_to_array($rootRules)];
25
		foreach ($tags as $tagName => $tag)
26
		{
27
			$rules[$tagName] = iterator_to_array($tag->rules);
28
		}
29
30
		// Create a matrix that contains all of the tags and whether every other tag is allowed as
31
		// a child and as a descendant
32
		$matrix = self::unrollRules($rules);
33
34
		// Remove unusable tags from the matrix
35
		self::pruneMatrix($matrix);
36
37
		// Group together tags are allowed in the exact same contexts
38
		$groupedTags = [];
39
		foreach (array_keys($matrix) as $tagName)
40
		{
41
			if ($tagName === '*root*')
42
			{
43
				continue;
44
			}
45
46
			$k = '';
47
			foreach ($matrix as $tagMatrix)
48
			{
49
				$k .= $tagMatrix['allowedChildren'][$tagName];
50
				$k .= $tagMatrix['allowedDescendants'][$tagName];
51
			}
52
53
			$groupedTags[$k][] = $tagName;
54
		}
55
56
		// Record the bit number of each tag, and the name of a tag for each bit
57
		$bitTag     = [];
58
		$bitNumber  = 0;
59
		$tagsConfig = [];
60
		foreach ($groupedTags as $tagNames)
61
		{
62
			foreach ($tagNames as $tagName)
63
			{
64
				$tagsConfig[$tagName]['bitNumber'] = $bitNumber;
65
				$bitTag[$bitNumber] = $tagName;
66
			}
67
68
			++$bitNumber;
69
		}
70
71
		// Build the bitfields of each tag, including the *root* pseudo-tag
72
		foreach ($matrix as $tagName => $tagMatrix)
73
		{
74
			$allowedChildren    = '';
75
			$allowedDescendants = '';
76
			foreach ($bitTag as $targetName)
77
			{
78
				$allowedChildren    .= $tagMatrix['allowedChildren'][$targetName];
79
				$allowedDescendants .= $tagMatrix['allowedDescendants'][$targetName];
80
			}
81
82
			$tagsConfig[$tagName]['allowed'] = self::pack($allowedChildren, $allowedDescendants);
83
		}
84
85
		// Prepare the return value
86
		$return = [
87
			'root' => $tagsConfig['*root*'],
88
			'tags' => $tagsConfig
89
		];
90
		unset($return['tags']['*root*']);
91
92
		return $return;
93
	}
94
95
	/**
96
	* Initialize a matrix of settings
97
	*
98
	* @param  array $rules Rules for each tag
99
	* @return array        Multidimensional array of [tagName => [scope => [targetName => setting]]]
100
	*/
101
	protected static function initMatrix(array $rules)
102
	{
103
		$matrix   = [];
104
		$tagNames = array_keys($rules);
105
106
		foreach ($rules as $tagName => $tagRules)
107
		{
108
			$matrix[$tagName]['allowedChildren']    = array_fill_keys($tagNames, 1);
109
			$matrix[$tagName]['allowedDescendants'] = array_fill_keys($tagNames, 1);
110
		}
111
112
		return $matrix;
113
	}
114
115
	/**
116
	* Apply given rule from each applicable tag
117
	*
118
	* For each tag, if the rule has any target we set the corresponding value for each target in the
119
	* matrix
120
	*
121
	* @param  array  &$matrix   Settings matrix
122
	* @param  array   $rules    Rules for each tag
123
	* @param  string  $ruleName Rule name
124
	* @param  string  $key      Key in the matrix
125
	* @param  integer $value    Value to be set
126
	* @return void
127
	*/
128
	protected static function applyTargetedRule(array &$matrix, $rules, $ruleName, $key, $value)
129
	{
130
		foreach ($rules as $tagName => $tagRules)
131
		{
132
			if (!isset($tagRules[$ruleName]))
133
			{
134
				continue;
135
			}
136
137
			foreach ($tagRules[$ruleName] as $targetName)
138
			{
139
				$matrix[$tagName][$key][$targetName] = $value;
140
			}
141
		}
142
	}
143
144
	/**
145
	* @param  array $rules
146
	* @return array
147
	*/
148
	protected static function unrollRules(array $rules)
149
	{
150
		// Initialize the matrix with default values
151
		$matrix = self::initMatrix($rules);
152
153
		// Convert ignoreTags and requireParent to denyDescendant and denyChild rules
154
		$tagNames = array_keys($rules);
155
		foreach ($rules as $tagName => $tagRules)
156
		{
157
			if (!empty($tagRules['ignoreTags']))
158
			{
159
				$rules[$tagName]['denyDescendant'] = $tagNames;
160
			}
161
162
			if (!empty($tagRules['requireParent']))
163
			{
164
				$denyParents = array_diff($tagNames, $tagRules['requireParent']);
165
				foreach ($denyParents as $parentName)
166
				{
167
					$rules[$parentName]['denyChild'][] = $tagName;
168
				}
169
			}
170
		}
171
172
		// Apply "allow" rules to grant usage, overwriting the default settings
173
		self::applyTargetedRule($matrix, $rules, 'allowChild',      'allowedChildren',    1);
174
		self::applyTargetedRule($matrix, $rules, 'allowDescendant', 'allowedChildren',    1);
175
		self::applyTargetedRule($matrix, $rules, 'allowDescendant', 'allowedDescendants', 1);
176
177
		// Apply "deny" rules to remove usage
178
		self::applyTargetedRule($matrix, $rules, 'denyChild',      'allowedChildren',    0);
179
		self::applyTargetedRule($matrix, $rules, 'denyDescendant', 'allowedChildren',    0);
180
		self::applyTargetedRule($matrix, $rules, 'denyDescendant', 'allowedDescendants', 0);
181
182
		return $matrix;
183
	}
184
185
	/**
186
	* Remove unusable tags from the matrix
187
	*
188
	* @param  array &$matrix
189
	* @return void
190
	*/
191
	protected static function pruneMatrix(array &$matrix)
192
	{
193
		$usableTags = ['*root*' => 1];
194
195
		// Start from the root and keep digging
196
		$parentTags = $usableTags;
197
		do
198
		{
199
			$nextTags = [];
200
			foreach (array_keys($parentTags) as $tagName)
201
			{
202
				// Accumulate the names of tags that are allowed as children of our parent tags
203
				$nextTags += array_filter($matrix[$tagName]['allowedChildren']);
204
			}
205
206
			// Keep only the tags that are in the matrix but aren't in the usable array yet, then
207
			// add them to the array
208
			$parentTags  = array_diff_key($nextTags, $usableTags);
209
			$parentTags  = array_intersect_key($parentTags, $matrix);
210
			$usableTags += $parentTags;
211
		}
212
		while (!empty($parentTags));
213
214
		// Remove unusable tags from the matrix
215
		$matrix = array_intersect_key($matrix, $usableTags);
216
		unset($usableTags['*root*']);
217
218
		// Remove unusable tags from the targets
219
		foreach ($matrix as $tagName => &$tagMatrix)
220
		{
221
			$tagMatrix['allowedChildren']
222
				= array_intersect_key($tagMatrix['allowedChildren'], $usableTags);
223
224
			$tagMatrix['allowedDescendants']
225
				= array_intersect_key($tagMatrix['allowedDescendants'], $usableTags);
226
		}
227
		unset($tagMatrix);
228
	}
229
230
	/**
231
	* Convert a binary representation such as "101011" to an array of integer
232
	*
233
	* Each bitfield is split in groups of 8 bits, then converted to a 16-bit integer where the
234
	* allowedChildren bitfield occupies the least significant bits and the allowedDescendants
235
	* bitfield occupies the most significant bits
236
	*
237
	* @param  string    $allowedChildren
238
	* @param  string    $allowedDescendants
239
	* @return integer[]
240
	*/
241
	protected static function pack($allowedChildren, $allowedDescendants)
242
	{
243
		$allowedChildren    = str_split($allowedChildren,    8);
244
		$allowedDescendants = str_split($allowedDescendants, 8);
245
246
		$allowed = [];
247
		foreach (array_keys($allowedChildren) as $k)
248
		{
249
			$allowed[] = bindec(sprintf(
250
				'%1$08s%2$08s',
251
				strrev($allowedDescendants[$k]),
252
				strrev($allowedChildren[$k])
253
			));
254
		}
255
256
		return $allowed;
257
	}
258
}