iterator_to_array($rootRules)]; foreach ($tags as $tagName => $tag) { $rules[$tagName] = iterator_to_array($tag->rules); } // Create a matrix that contains all of the tags and whether every other tag is allowed as // a child and as a descendant $matrix = self::unrollRules($rules); // Remove unusable tags from the matrix self::pruneMatrix($matrix); // Group together tags are allowed in the exact same contexts $groupedTags = []; foreach (array_keys($matrix) as $tagName) { if ($tagName === '*root*') { continue; } $k = ''; foreach ($matrix as $tagMatrix) { $k .= $tagMatrix['allowedChildren'][$tagName]; $k .= $tagMatrix['allowedDescendants'][$tagName]; } $groupedTags[$k][] = $tagName; } // Record the bit number of each tag, and the name of a tag for each bit $bitTag = []; $bitNumber = 0; $tagsConfig = []; foreach ($groupedTags as $tagNames) { foreach ($tagNames as $tagName) { $tagsConfig[$tagName]['bitNumber'] = $bitNumber; $bitTag[$bitNumber] = $tagName; } ++$bitNumber; } // Build the bitfields of each tag, including the *root* pseudo-tag foreach ($matrix as $tagName => $tagMatrix) { $allowedChildren = ''; $allowedDescendants = ''; foreach ($bitTag as $targetName) { $allowedChildren .= $tagMatrix['allowedChildren'][$targetName]; $allowedDescendants .= $tagMatrix['allowedDescendants'][$targetName]; } $tagsConfig[$tagName]['allowed'] = self::pack($allowedChildren, $allowedDescendants); } // Prepare the return value $return = [ 'root' => $tagsConfig['*root*'], 'tags' => $tagsConfig ]; unset($return['tags']['*root*']); return $return; } /** * Initialize a matrix of settings * * @param array $rules Rules for each tag * @return array Multidimensional array of [tagName => [scope => [targetName => setting]]] */ protected static function initMatrix(array $rules) { $matrix = []; $tagNames = array_keys($rules); foreach ($rules as $tagName => $tagRules) { $matrix[$tagName]['allowedChildren'] = array_fill_keys($tagNames, 0); $matrix[$tagName]['allowedDescendants'] = array_fill_keys($tagNames, 0); } return $matrix; } /** * Apply given rule from each applicable tag * * For each tag, if the rule has any target we set the corresponding value for each target in the * matrix * * @param array &$matrix Settings matrix * @param array $rules Rules for each tag * @param string $ruleName Rule name * @param string $key Key in the matrix * @param integer $value Value to be set * @return void */ protected static function applyTargetedRule(array &$matrix, $rules, $ruleName, $key, $value) { foreach ($rules as $tagName => $tagRules) { if (!isset($tagRules[$ruleName])) { continue; } foreach ($tagRules[$ruleName] as $targetName) { $matrix[$tagName][$key][$targetName] = $value; } } } /** * @param array $rules * @return array */ protected static function unrollRules(array $rules) { // Initialize the matrix with default values $matrix = self::initMatrix($rules); // Convert ignoreTags and requireParent to denyDescendant and denyChild rules $tagNames = array_keys($rules); foreach ($rules as $tagName => $tagRules) { if (!empty($tagRules['ignoreTags'])) { $rules[$tagName]['denyChild'] = $tagNames; $rules[$tagName]['denyDescendant'] = $tagNames; } if (!empty($tagRules['requireParent'])) { $denyParents = array_diff($tagNames, $tagRules['requireParent']); foreach ($denyParents as $parentName) { $rules[$parentName]['denyChild'][] = $tagName; } } } // Apply "allow" rules to grant usage, overwriting the default settings self::applyTargetedRule($matrix, $rules, 'allowChild', 'allowedChildren', 1); self::applyTargetedRule($matrix, $rules, 'allowDescendant', 'allowedDescendants', 1); // Apply "deny" rules to remove usage self::applyTargetedRule($matrix, $rules, 'denyChild', 'allowedChildren', 0); self::applyTargetedRule($matrix, $rules, 'denyDescendant', 'allowedDescendants', 0); return $matrix; } /** * Remove unusable tags from the matrix * * @param array &$matrix * @return void */ protected static function pruneMatrix(array &$matrix) { $usableTags = ['*root*' => 1]; // Start from the root and keep digging $parentTags = $usableTags; do { $nextTags = []; foreach (array_keys($parentTags) as $tagName) { // Accumulate the names of tags that are allowed as children of our parent tags $nextTags += array_filter($matrix[$tagName]['allowedChildren']); } // Keep only the tags that are in the matrix but aren't in the usable array yet, then // add them to the array $parentTags = array_diff_key($nextTags, $usableTags); $parentTags = array_intersect_key($parentTags, $matrix); $usableTags += $parentTags; } while (!empty($parentTags)); // Remove unusable tags from the matrix $matrix = array_intersect_key($matrix, $usableTags); unset($usableTags['*root*']); // Remove unusable tags from the targets foreach ($matrix as $tagName => &$tagMatrix) { $tagMatrix['allowedChildren'] = array_intersect_key($tagMatrix['allowedChildren'], $usableTags); $tagMatrix['allowedDescendants'] = array_intersect_key($tagMatrix['allowedDescendants'], $usableTags); } unset($tagMatrix); } /** * Convert a binary representation such as "101011" to an array of integer * * Each bitfield is split in groups of 8 bits, then converted to a 16-bit integer where the * allowedChildren bitfield occupies the least significant bits and the allowedDescendants * bitfield occupies the most significant bits * * @param string $allowedChildren * @param string $allowedDescendants * @return integer[] */ protected static function pack($allowedChildren, $allowedDescendants) { $allowedChildren = str_split($allowedChildren, 8); $allowedDescendants = str_split($allowedDescendants, 8); $allowed = []; foreach (array_keys($allowedChildren) as $k) { $allowed[] = bindec(sprintf( '%1$08s%2$08s', strrev($allowedDescendants[$k]), strrev($allowedChildren[$k]) )); } return $allowed; } }