Completed
Push — master ( a7088b...77f96d )
by Josh
22:56
created

Configurator::addHTML5Rules()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 23
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 11
cts 11
cp 1
rs 9.0856
c 0
b 0
f 0
cc 3
eloc 8
nc 4
nop 0
crap 3
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;
9
10
use InvalidArgumentException;
11
use RuntimeException;
12
use s9e\TextFormatter\Configurator\BundleGenerator;
13
use s9e\TextFormatter\Configurator\Collections\AttributeFilterCollection;
14
use s9e\TextFormatter\Configurator\Collections\PluginCollection;
15
use s9e\TextFormatter\Configurator\Collections\Ruleset;
16
use s9e\TextFormatter\Configurator\Collections\TagCollection;
17
use s9e\TextFormatter\Configurator\ConfigProvider;
18
use s9e\TextFormatter\Configurator\Helpers\ConfigHelper;
19
use s9e\TextFormatter\Configurator\Helpers\RulesHelper;
20
use s9e\TextFormatter\Configurator\JavaScript;
21
use s9e\TextFormatter\Configurator\JavaScript\Dictionary;
22
use s9e\TextFormatter\Configurator\Rendering;
23
use s9e\TextFormatter\Configurator\RulesGenerator;
24
use s9e\TextFormatter\Configurator\TemplateChecker;
25
use s9e\TextFormatter\Configurator\TemplateNormalizer;
26
use s9e\TextFormatter\Configurator\UrlConfig;
27
28
/**
29
* @property Plugins\Autoemail\Configurator $Autoemail Autoemail plugin's configurator
30
* @property Plugins\Autoimage\Configurator $Autolink Autoimage plugin's configurator
31
* @property Plugins\Autolink\Configurator $Autolink Autolink plugin's configurator
32
* @property Plugins\Autovideo\Configurator $Autovideo Autovideo plugin's configurator
33
* @property Plugins\BBCodes\Configurator $BBCodes BBCodes plugin's configurator
34
* @property Plugins\Censor\Configurator $Censor Censor plugin's configurator
35
* @property Plugins\Emoji\Configurator $Emoji Emoji plugin's configurator
36
* @property Plugins\Emoticons\Configurator $Emoticons Emoticons plugin's configurator
37
* @property Plugins\Escaper\Configurator $Escaper Escaper plugin's configurator
38
* @property Plugins\FancyPants\Configurator $FancyPants FancyPants plugin's configurator
39
* @property Plugins\HTMLComments\Configurator $HTMLComments HTMLComments plugin's configurator
40
* @property Plugins\HTMLElements\Configurator $HTMLElements HTMLElements plugin's configurator
41
* @property Plugins\HTMLEntities\Configurator $HTMLEntities HTMLEntities plugin's configurator
42
* @property Plugins\Keywords\Configurator $Keywords Keywords plugin's configurator
43
* @property Plugins\Litedown\Configurator $Litedown Litedown plugin's configurator
44
* @property Plugins\MediaEmbed\Configurator $MediaEmbed MediaEmbed plugin's configurator
45
* @property Plugins\PipeTables\Configurator $PipeTables PipeTables plugin's configurator
46
* @property Plugins\Preg\Configurator $Preg Preg plugin's configurator
47
* @property UrlConfig $urlConfig Default URL config
48
*/
49
class Configurator implements ConfigProvider
50
{
51
	/**
52
	* @var AttributeFilterCollection Dynamically-populated collection of AttributeFilter instances
53
	*/
54
	public $attributeFilters;
55
56
	/**
57
	* @var BundleGenerator Default bundle generator
58
	*/
59
	public $bundleGenerator;
60
61
	/**
62
	* @var JavaScript JavaScript manipulation object
63
	*/
64
	public $javascript;
65
66
	/**
67
	* @var PluginCollection Loaded plugins
68
	*/
69
	public $plugins;
70
71
	/**
72
	* @var array Array of variables that are available to the filters during parsing
73
	*/
74
	public $registeredVars;
75
76
	/**
77
	* @var Rendering Rendering configuration
78
	*/
79
	public $rendering;
80
81
	/**
82
	* @var Ruleset Rules that apply at the root of the text
83
	*/
84
	public $rootRules;
85
86
	/**
87
	* @var RulesGenerator Generator used by $this->getRenderer()
88
	*/
89
	public $rulesGenerator;
90
91
	/**
92
	* @var TagCollection Tags repository
93
	*/
94
	public $tags;
95
96
	/**
97
	* @var TemplateChecker Default template checker
98
	*/
99
	public $templateChecker;
100
101
	/**
102
	* @var TemplateNormalizer Default template normalizer
103
	*/
104
	public $templateNormalizer;
105
106
	/**
107
	* Constructor
108
	*
109
	* Prepares the collections that hold tags and filters, the UrlConfig object as well as the
110
	* various helpers required to generate a full config.
111
	*/
112 53
	public function __construct()
113
	{
114 53
		$this->attributeFilters   = new AttributeFilterCollection;
115 53
		$this->bundleGenerator    = new BundleGenerator($this);
116 53
		$this->plugins            = new PluginCollection($this);
117 53
		$this->registeredVars     = ['urlConfig' => new UrlConfig];
118 53
		$this->rendering          = new Rendering($this);
119 53
		$this->rootRules          = new Ruleset;
120 53
		$this->rulesGenerator     = new RulesGenerator;
121 53
		$this->tags               = new TagCollection;
122 53
		$this->templateChecker    = new TemplateChecker;
123 53
		$this->templateNormalizer = new TemplateNormalizer;
124 53
	}
125
126
	//==========================================================================
127
	// Magic methods
128
	//==========================================================================
129
130
	/**
131
	* Magic __get automatically loads plugins, returns registered vars
132
	*
133
	* @param  string $k Property name
134
	* @return mixed
135
	*/
136 6
	public function __get($k)
137
	{
138 6
		if (preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $k))
139 6
		{
140 4
			return (isset($this->plugins[$k]))
141 4
			     ? $this->plugins[$k]
142 4
			     : $this->plugins->load($k);
143
		}
144
145 2
		if (isset($this->registeredVars[$k]))
146 2
		{
147 1
			return $this->registeredVars[$k];
148
		}
149
150 1
		throw new RuntimeException("Undefined property '" . __CLASS__ . '::$' . $k . "'");
151
	}
152
153
	/**
154
	* Magic __isset checks existence in the plugins collection and registered vars
155
	*
156
	* @param  string $k Property name
157
	* @return bool
158
	*/
159 5
	public function __isset($k)
160
	{
161 5
		if (preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $k))
162 5
		{
163 2
			return isset($this->plugins[$k]);
164
		}
165
166 3
		return isset($this->registeredVars[$k]);
167
	}
168
169
	/**
170
	* Magic __set adds to the plugins collection, registers vars
171
	*
172
	* @param  string $k Property name
173
	* @param  mixed  $v Property value
174
	* @return mixed
175
	*/
176 2
	public function __set($k, $v)
177
	{
178 2
		if (preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $k))
179 2
		{
180 1
			$this->plugins[$k] = $v;
181 1
		}
182
		else
183
		{
184 1
			$this->registeredVars[$k] = $v;
185
		}
186 2
	}
187
188
	/**
189
	* Magic __set removes plugins from the plugins collection, unregisters vars
190
	*
191
	* @param  string $k Property name
192
	* @return mixed
193
	*/
194 2
	public function __unset($k)
195
	{
196 2
		if (preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $k))
197 2
		{
198 1
			unset($this->plugins[$k]);
199 1
		}
200
		else
201
		{
202 1
			unset($this->registeredVars[$k]);
203
		}
204 2
	}
205
206
	//==========================================================================
207
	// API
208
	//==========================================================================
209
210
	/**
211
	* Enable the creation of a JavaScript parser
212
	*
213
	* @return void
214
	*/
215 3
	public function enableJavaScript()
216
	{
217 3
		if (!isset($this->javascript))
218 3
		{
219 3
			$this->javascript = new JavaScript($this);
220 3
		}
221 3
	}
222
223
	/**
224
	* Finalize this configuration and return all the relevant objects
225
	*
226
	* Options: (also see addHTMLRules() options)
227
	*
228
	*  - addHTML5Rules:    whether to call addHTML5Rules()
229
	*  - finalizeParser:   callback executed after the parser is created (gets the parser as arg)
230
	*  - finalizeRenderer: same with the renderer
231
	*  - optimizeConfig:   whether to optimize the parser's config using references
232
	*  - returnParser:     whether to return an instance of Parser in the "parser" key
233
	*  - returnRenderer:   whether to return an instance of Renderer in the "renderer" key
234
	*
235
	* @param  array $options
236
	* @return array One "parser" element and one "renderer" element unless specified otherwise
237
	*/
238 15
	public function finalize(array $options = [])
239
	{
240 15
		$return = [];
241
242
		// Add default options
243
		$options += [
244 15
			'addHTML5Rules'  => true,
245 15
			'optimizeConfig' => true,
246 15
			'returnJS'       => isset($this->javascript),
247 15
			'returnParser'   => true,
248
			'returnRenderer' => true
249 15
		];
250
251
		// Add the HTML5 rules if applicable
252 15
		if ($options['addHTML5Rules'])
253 15
		{
254 12
			$this->addHTML5Rules();
255 12
		}
256
257
		// Create a renderer as needed
258 15
		if ($options['returnRenderer'])
259 15
		{
260
			// Create a renderer
261 12
			$renderer = $this->rendering->getRenderer();
262
263
			// Execute the renderer callback if applicable
264 12
			if (isset($options['finalizeRenderer']))
265 12
			{
266 1
				$options['finalizeRenderer']($renderer);
267 1
			}
268
269 12
			$return['renderer'] = $renderer;
270 12
		}
271
272 15
		if ($options['returnJS'] || $options['returnParser'])
273 15
		{
274 14
			$config = $this->asConfig();
275
276 14
			if ($options['returnJS'])
277 14
			{
278 1
				$return['js'] = $this->javascript->getParser(ConfigHelper::filterConfig($config, 'JS'));
279 1
			}
280
281 14
			if ($options['returnParser'])
282 14
			{
283
				// Remove JS-specific data from the config
284 14
				$config = ConfigHelper::filterConfig($config, 'PHP');
285
286 14
				if ($options['optimizeConfig'])
287 14
				{
288 12
					ConfigHelper::optimizeArray($config);
289 12
				}
290
291
				// Create a parser
292 14
				$parser = new Parser($config);
293
294
				// Execute the parser callback if applicable
295 14
				if (isset($options['finalizeParser']))
296 14
				{
297 1
					$options['finalizeParser']($parser);
298 1
				}
299
300 14
				$return['parser'] = $parser;
301 14
			}
302 14
		}
303
304 15
		return $return;
305
	}
306
307
	/**
308
	* Load a bundle into this configuration
309
	*
310
	* @param  string $bundleName Name of the bundle
311
	* @return void
312
	*/
313 2
	public function loadBundle($bundleName)
314
	{
315 2
		if (!preg_match('#^[A-Z][A-Za-z0-9]+$#D', $bundleName))
316 2
		{
317 1
			throw new InvalidArgumentException("Invalid bundle name '" . $bundleName . "'");
318
		}
319
320 1
		$className = __CLASS__ . '\\Bundles\\' . $bundleName;
321
322 1
		$bundle = new $className;
323 1
		$bundle->configure($this);
324 1
	}
325
326
	/**
327
	* Create and save a bundle based on this configuration
328
	*
329
	* @param  string $className Name of the bundle class
330
	* @param  string $filepath  Path where to save the bundle file
331
	* @param  array  $options   Options passed to the bundle generator
332
	* @return bool              Whether the write succeeded
333
	*/
334 4
	public function saveBundle($className, $filepath, array $options = [])
335
	{
336 4
		$file = "<?php\n\n" . $this->bundleGenerator->generate($className, $options);
337
338 4
		return (file_put_contents($filepath, $file) !== false);
339
	}
340
341
	/**
342
	* Add the rules that are generated based on HTML5 specs
343
	*
344
	* @return void
345
	*/
346 14
	public function addHTML5Rules()
347
	{
348
		// Finalize the plugins' config
349 14
		$this->plugins->finalize();
350
351
		// Normalize the tags' templates
352 14
		foreach ($this->tags as $tag)
353
		{
354 4
			$this->templateNormalizer->normalizeTag($tag);
355 14
		}
356
357
		// Get the rules
358 14
		$rules = $this->rulesGenerator->getRules($this->tags);
359
360
		// Add the rules pertaining to the root
361 14
		$this->rootRules->merge($rules['root'], false);
362
363
		// Add the rules pertaining to each tag
364 14
		foreach ($rules['tags'] as $tagName => $tagRules)
365
		{
366 4
			$this->tags[$tagName]->rules->merge($tagRules, false);
367 14
		}
368 14
	}
369
370
	/**
371
	* Generate and return the complete config array
372
	*
373
	* @return array
374
	*/
375 23
	public function asConfig()
376
	{
377
		// Finalize the plugins' config
378 23
		$this->plugins->finalize();
379
380
		// Remove properties that shouldn't be turned into config arrays
381 23
		$properties = get_object_vars($this);
382 23
		unset($properties['attributeFilters']);
383 23
		unset($properties['bundleGenerator']);
384 23
		unset($properties['javascript']);
385 23
		unset($properties['rendering']);
386 23
		unset($properties['rulesGenerator']);
387 23
		unset($properties['registeredVars']);
388 23
		unset($properties['templateChecker']);
389 23
		unset($properties['templateNormalizer']);
390 23
		unset($properties['stylesheet']);
391
392
		// Create the config array
393 23
		$config    = ConfigHelper::toArray($properties);
394 23
		$bitfields = RulesHelper::getBitfields($this->tags, $this->rootRules);
395
396
		// Save the root context
397 23
		$config['rootContext'] = $bitfields['root'];
398 23
		$config['rootContext']['flags'] = $config['rootRules']['flags'];
399
400
		// Save the registered vars (including the empty ones)
401 23
		$config['registeredVars'] = ConfigHelper::toArray($this->registeredVars, true);
402
403
		// Make sure those keys exist even if they're empty
404
		$config += [
405 23
			'plugins' => [],
406 23
			'tags'    => []
407 23
		];
408
409
		// Remove unused tags
410 23
		$config['tags'] = array_intersect_key($config['tags'], $bitfields['tags']);
411
412
		// Add the bitfield information to each tag
413 23
		foreach ($bitfields['tags'] as $tagName => $tagBitfields)
414
		{
415 4
			$config['tags'][$tagName] += $tagBitfields;
416 23
		}
417
418
		// Remove unused entries
419 23
		unset($config['rootRules']);
420
421 23
		return $config;
422
	}
423
}