Completed
Push — master ( 7d71e7...f5f494 )
by Josh
20:31
created

RulesGenerator::generateRootInspector()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 24
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 11
nc 2
nop 1
dl 0
loc 24
rs 8.9713
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;
9
10
use ArrayAccess;
11
use DOMDocument;
12
use Iterator;
13
use s9e\TextFormatter\Configurator\Collections\RulesGeneratorList;
14
use s9e\TextFormatter\Configurator\Collections\TagCollection;
15
use s9e\TextFormatter\Configurator\Helpers\TemplateInspector;
16
use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator;
17
use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\TargetedRulesGenerator;
18
use s9e\TextFormatter\Configurator\Traits\CollectionProxy;
19
20
/**
21
* @method mixed   add(mixed $value, null $void)
22
* @method mixed   append(mixed $value)
23
* @method array   asConfig()
24
* @method void    clear()
25
* @method bool    contains(mixed $value)
26
* @method integer count()
27
* @method mixed   current()
28
* @method void    delete(string $key)
29
* @method bool    exists(string $key)
30
* @method mixed   get(string $key)
31
* @method mixed   indexOf(mixed $value)
32
* @method mixed   insert(integer $offset, mixed $value)
33
* @method integer|string key()
34
* @method mixed   next()
35
* @method integer normalizeKey(mixed $key)
36
* @method BooleanRulesGenerator|TargetedRulesGenerator normalizeValue(string|BooleanRulesGenerator|TargetedRulesGenerator $generator)
37
* @method bool    offsetExists(string|integer $offset)
38
* @method mixed   offsetGet(string|integer $offset)
39
* @method void    offsetSet(mixed $offset, mixed $value)
40
* @method void    offsetUnset(string|integer $offset)
41
* @method string  onDuplicate(string|null $action)
42
* @method mixed   prepend(mixed $value)
43
* @method integer remove(mixed $value)
44
* @method void    rewind()
45
* @method mixed   set(string $key, mixed $value)
46
* @method bool    valid()
47
*/
48
class RulesGenerator implements ArrayAccess, Iterator
49
{
50
	use CollectionProxy;
51
52
	/**
53
	* @var RulesGeneratorList Collection of objects
54
	*/
55
	protected $collection;
56
57
	/**
58
	* Constructor
59
	*
60
	* Will load the default rule generators
61
	*/
62
	public function __construct()
63
	{
64
		$this->collection = new RulesGeneratorList;
65
		$this->collection->append('AutoCloseIfVoid');
66
		$this->collection->append('AutoReopenFormattingElements');
67
		$this->collection->append('BlockElementsFosterFormattingElements');
68
		$this->collection->append('DisableAutoLineBreaksIfNewLinesArePreserved');
69
		$this->collection->append('EnforceContentModels');
70
		$this->collection->append('EnforceOptionalEndTags');
71
		$this->collection->append('IgnoreTagsInCode');
72
		$this->collection->append('IgnoreTextIfDisallowed');
73
		$this->collection->append('IgnoreWhitespaceAroundBlockElements');
74
		$this->collection->append('TrimFirstLineInCodeBlocks');
75
	}
76
77
	/**
78
	* Generate rules for given tag collection
79
	*
80
	* Possible options:
81
	*
82
	*  parentHTML: HTML leading to the start of the rendered text. Defaults to "<div>"
83
	*
84
	* @param  TagCollection $tags    Tags collection
85
	* @param  array         $options Array of option settings
86
	* @return array
87
	*/
88
	public function getRules(TagCollection $tags, array $options = [])
89
	{
90
		// Unless specified otherwise, we consider that the renderered text will be displayed as
91
		// the child of a <div> element
92
		$parentHTML = (isset($options['parentHTML'])) ? $options['parentHTML'] : '<div>';
93
94
		// Create a proxy for the parent markup so that we can determine which tags are allowed at
95
		// the root of the text (IOW, with no parent) or even disabled altogether
96
		$rootInspector = $this->generateRootInspector($parentHTML);
97
98
		// Study the tags
99
		$templateInspector = [];
100
		foreach ($tags as $tagName => $tag)
101
		{
102
			// Use the tag's template if applicable or XSLT's implicit default otherwise
103
			$template = (isset($tag->template)) ? $tag->template : '<xsl:apply-templates/>';
104
			$templateInspector[$tagName] = new TemplateInspector($template);
105
		}
106
107
		// Generate a full set of rules
108
		$rules = $this->generateRulesets($templateInspector, $rootInspector);
109
110
		// Remove root rules that wouldn't be applied anyway
111
		unset($rules['root']['autoClose']);
112
		unset($rules['root']['autoReopen']);
113
		unset($rules['root']['breakParagraph']);
114
		unset($rules['root']['closeAncestor']);
115
		unset($rules['root']['closeParent']);
116
		unset($rules['root']['fosterParent']);
117
		unset($rules['root']['ignoreSurroundingWhitespace']);
118
		unset($rules['root']['isTransparent']);
119
		unset($rules['root']['requireAncestor']);
120
		unset($rules['root']['requireParent']);
121
122
		return $rules;
123
	}
124
125
	/**
126
	* Generate a TemplateInspector instance for the root element
127
	*
128
	* @param  string            $html Root HTML, e.g. "<div>"
129
	* @return TemplateInspector
130
	*/
131
	protected function generateRootInspector($html)
132
	{
133
		$dom = new DOMDocument;
134
		$dom->loadHTML($html);
135
136
		// Get the document's <body> element
137
		$body = $dom->getElementsByTagName('body')->item(0);
138
139
		// Grab the deepest node
140
		$node = $body;
141
		while ($node->firstChild)
142
		{
143
			$node = $node->firstChild;
144
		}
145
146
		// Now append an <xsl:apply-templates/> node to make the markup look like a normal template
147
		$node->appendChild($dom->createElementNS(
148
			'http://www.w3.org/1999/XSL/Transform',
149
			'xsl:apply-templates'
150
		));
151
152
		// Finally create and return a new TemplateInspector instance
153
		return new TemplateInspector($dom->saveXML($body));
154
	}
155
156
	/**
157
	* Generate and return rules based on a set of TemplateInspector
158
	*
159
	* @param  array             $templateInspector Array of [tagName => TemplateInspector]
160
	* @param  TemplateInspector $rootInspector     TemplateInspector for the root of the text
161
	* @return array
162
	*/
163
	protected function generateRulesets(array $templateInspector, TemplateInspector $rootInspector)
164
	{
165
		$rules = [
166
			'root' => $this->generateRuleset($rootInspector, $templateInspector),
167
			'tags' => []
168
		];
169
		foreach ($templateInspector as $tagName => $src)
170
		{
171
			$rules['tags'][$tagName] = $this->generateRuleset($src, $templateInspector);
172
		}
173
174
		return $rules;
175
	}
176
177
	/**
178
	* Generate a set of rules for a single TemplateInspector instance
179
	*
180
	* @param  TemplateInspector $src     Source of the rules
181
	* @param  array             $targets Array of [tagName => TemplateInspector]
182
	* @return array
183
	*/
184
	protected function generateRuleset(TemplateInspector $src, array $targets)
185
	{
186
		$rules = [];
187
		foreach ($this->collection as $rulesGenerator)
188
		{
189
			if ($rulesGenerator instanceof BooleanRulesGenerator)
190
			{
191
				foreach ($rulesGenerator->generateBooleanRules($src) as $ruleName => $bool)
192
				{
193
					$rules[$ruleName] = $bool;
194
				}
195
			}
196
197
			if ($rulesGenerator instanceof TargetedRulesGenerator)
198
			{
199
				foreach ($targets as $tagName => $trg)
200
				{
201
					foreach ($rulesGenerator->generateTargetedRules($src, $trg) as $ruleName)
202
					{
203
						$rules[$ruleName][] = $tagName;
204
					}
205
				}
206
			}
207
		}
208
209
		return $rules;
210
	}
211
}