Completed
Push — master ( 3e9a48...a60375 )
by Josh
22:10
created

Configurator::finalize()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 38
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 38
ccs 21
cts 21
cp 1
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 16
nc 8
nop 1
crap 4
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 48
	public function __construct()
113
	{
114 48
		$this->attributeFilters   = new AttributeFilterCollection;
115 48
		$this->bundleGenerator    = new BundleGenerator($this);
116 48
		$this->plugins            = new PluginCollection($this);
117 48
		$this->registeredVars     = ['urlConfig' => new UrlConfig];
118 48
		$this->rendering          = new Rendering($this);
119 48
		$this->rootRules          = new Ruleset;
120 48
		$this->rulesGenerator     = new RulesGenerator;
121 48
		$this->tags               = new TagCollection;
122 48
		$this->templateChecker    = new TemplateChecker;
123 48
		$this->templateNormalizer = new TemplateNormalizer;
124 48
	}
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:
227
	*
228
	*  - addHTML5Rules:    whether to call addHTML5Rules()
229
	*  - optimizeConfig:   whether to optimize the parser's config using references
230
	*
231
	* @param  array $options
232
	* @return array One "parser" element and one "renderer" element unless specified otherwise
233
	*/
234 11
	public function finalize(array $options = [])
235
	{
236 11
		$return = [];
237
238
		// Add default options
239
		$options += [
240 11
			'addHTML5Rules'  => true,
241
			'optimizeConfig' => true
242 11
		];
243
244
		// Add the HTML5 rules if applicable
245 11
		if ($options['addHTML5Rules'])
246 11
		{
247 8
			$this->addHTML5Rules();
248 8
		}
249
250
		// Create a renderer
251 11
		$return['renderer'] = $this->rendering->getRenderer();
252
253
		// Prepare the parser config
254 11
		$config = $this->asConfig();
255 11
		if (isset($this->javascript))
256 11
		{
257 1
			$return['js'] = $this->javascript->getParser(ConfigHelper::filterConfig($config, 'JS'));
258 1
		}
259
260
		// Remove JS-specific data from the config
261 11
		$config = ConfigHelper::filterConfig($config, 'PHP');
262 11
		if ($options['optimizeConfig'])
263 11
		{
264 9
			ConfigHelper::optimizeArray($config);
265 9
		}
266
267
		// Create a parser
268 11
		$return['parser'] = new Parser($config);
269
270 11
		return $return;
271
	}
272
273
	/**
274
	* Load a bundle into this configuration
275
	*
276
	* @param  string $bundleName Name of the bundle
277
	* @return void
278
	*/
279 2
	public function loadBundle($bundleName)
280
	{
281 2
		if (!preg_match('#^[A-Z][A-Za-z0-9]+$#D', $bundleName))
282 2
		{
283 1
			throw new InvalidArgumentException("Invalid bundle name '" . $bundleName . "'");
284
		}
285
286 1
		$className = __CLASS__ . '\\Bundles\\' . $bundleName;
287
288 1
		$bundle = new $className;
289 1
		$bundle->configure($this);
290 1
	}
291
292
	/**
293
	* Create and save a bundle based on this configuration
294
	*
295
	* @param  string $className Name of the bundle class
296
	* @param  string $filepath  Path where to save the bundle file
297
	* @param  array  $options   Options passed to the bundle generator
298
	* @return bool              Whether the write succeeded
299
	*/
300 3
	public function saveBundle($className, $filepath, array $options = [])
301
	{
302 3
		$file = "<?php\n\n" . $this->bundleGenerator->generate($className, $options);
303
304 3
		return (file_put_contents($filepath, $file) !== false);
305
	}
306
307
	/**
308
	* Add the rules that are generated based on HTML5 specs
309
	*
310
	* @return void
311
	*/
312 10
	public function addHTML5Rules()
313
	{
314
		// Finalize the plugins' config
315 10
		$this->plugins->finalize();
316
317
		// Normalize the tags' templates
318 10
		foreach ($this->tags as $tag)
319
		{
320 4
			$this->templateNormalizer->normalizeTag($tag);
321 10
		}
322
323
		// Get the rules
324 10
		$rules = $this->rulesGenerator->getRules($this->tags);
325
326
		// Add the rules pertaining to the root
327 10
		$this->rootRules->merge($rules['root'], false);
328
329
		// Add the rules pertaining to each tag
330 10
		foreach ($rules['tags'] as $tagName => $tagRules)
331
		{
332 4
			$this->tags[$tagName]->rules->merge($tagRules, false);
333 10
		}
334 10
	}
335
336
	/**
337
	* Generate and return the complete config array
338
	*
339
	* @return array
340
	*/
341 20
	public function asConfig()
342
	{
343
		// Finalize the plugins' config
344 20
		$this->plugins->finalize();
345
346
		// Remove properties that shouldn't be turned into config arrays
347 20
		$properties = get_object_vars($this);
348 20
		unset($properties['attributeFilters']);
349 20
		unset($properties['bundleGenerator']);
350 20
		unset($properties['javascript']);
351 20
		unset($properties['rendering']);
352 20
		unset($properties['rulesGenerator']);
353 20
		unset($properties['registeredVars']);
354 20
		unset($properties['templateChecker']);
355 20
		unset($properties['templateNormalizer']);
356 20
		unset($properties['stylesheet']);
357
358
		// Create the config array
359 20
		$config    = ConfigHelper::toArray($properties);
360 20
		$bitfields = RulesHelper::getBitfields($this->tags, $this->rootRules);
361
362
		// Save the root context
363 20
		$config['rootContext'] = $bitfields['root'];
364 20
		$config['rootContext']['flags'] = $config['rootRules']['flags'];
365
366
		// Save the registered vars (including the empty ones)
367 20
		$config['registeredVars'] = ConfigHelper::toArray($this->registeredVars, true);
368
369
		// Make sure those keys exist even if they're empty
370
		$config += [
371 20
			'plugins' => [],
372 20
			'tags'    => []
373 20
		];
374
375
		// Remove unused tags
376 20
		$config['tags'] = array_intersect_key($config['tags'], $bitfields['tags']);
377
378
		// Add the bitfield information to each tag
379 20
		foreach ($bitfields['tags'] as $tagName => $tagBitfields)
380
		{
381 4
			$config['tags'][$tagName] += $tagBitfields;
382 20
		}
383
384
		// Remove unused entries
385 20
		unset($config['rootRules']);
386
387 20
		return $config;
388
	}
389
}