Completed
Push — master ( 0cc554...fa9e93 )
by Josh
16:05
created

JavaScript::injectConfig()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 28
ccs 13
cts 13
cp 1
rs 8.8571
c 0
b 0
f 0
cc 1
eloc 15
nc 1
nop 1
crap 1
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;
9
10
use ReflectionClass;
11
use s9e\TextFormatter\Configurator;
12
use s9e\TextFormatter\Configurator\Helpers\ConfigHelper;
13
use s9e\TextFormatter\Configurator\JavaScript\CallbackGenerator;
14
use s9e\TextFormatter\Configurator\JavaScript\Code;
15
use s9e\TextFormatter\Configurator\JavaScript\ConfigOptimizer;
16
use s9e\TextFormatter\Configurator\JavaScript\Dictionary;
17
use s9e\TextFormatter\Configurator\JavaScript\Encoder;
18
use s9e\TextFormatter\Configurator\JavaScript\HintGenerator;
19
use s9e\TextFormatter\Configurator\JavaScript\Minifier;
20
use s9e\TextFormatter\Configurator\JavaScript\Minifiers\Noop;
21
use s9e\TextFormatter\Configurator\JavaScript\RegexpConvertor;
22
use s9e\TextFormatter\Configurator\JavaScript\StylesheetCompressor;
23
use s9e\TextFormatter\Configurator\RendererGenerators\XSLT;
24
25
class JavaScript
26
{
27
	/**
28
	* @var CallbackGenerator
29
	*/
30
	protected $callbackGenerator;
31
32
	/**
33
	* @var array Configuration, filtered for JavaScript
34
	*/
35
	protected $config;
36
37
	/**
38
	* @var ConfigOptimizer
39
	*/
40
	protected $configOptimizer;
41
42
	/**
43
	* @var Configurator Configurator this instance belongs to
44
	*/
45
	protected $configurator;
46
47
	/**
48
	* @var Encoder
49
	*/
50
	public $encoder;
51
52
	/**
53
	* @var array List of methods to be exported in the s9e.TextFormatter object
54
	*/
55
	public $exportMethods = [
56
		'disablePlugin',
57
		'disableTag',
58
		'enablePlugin',
59
		'enableTag',
60
		'getLogger',
61
		'parse',
62
		'preview',
63
		'setNestingLimit',
64
		'setParameter',
65
		'setTagLimit'
66
	];
67
68
	/**
69
	* @var HintGenerator
70
	*/
71
	protected $hintGenerator;
72
73
	/**
74
	* @var Minifier Instance of Minifier used to minify the JavaScript parser
75
	*/
76
	protected $minifier;
77
78
	/**
79
	* @var StylesheetCompressor
80
	*/
81
	protected $stylesheetCompressor;
82
83
	/**
84
	* @var string Stylesheet used for rendering
85
	*/
86
	protected $xsl;
87
88
	/**
89
	* Constructor
90
	*
91
	* @param  Configurator $configurator Configurator
92
	*/
93 26
	public function __construct(Configurator $configurator)
94
	{
95 26
		$this->encoder              = new Encoder;
96 26
		$this->callbackGenerator    = new CallbackGenerator;
97 26
		$this->configOptimizer      = new ConfigOptimizer($this->encoder);
98 26
		$this->configurator         = $configurator;
99 26
		$this->hintGenerator        = new HintGenerator;
100 26
		$this->stylesheetCompressor = new StylesheetCompressor;
101 26
	}
102
103
	/**
104
	* Return the cached instance of Minifier (creates one if necessary)
105
	*
106
	* @return Minifier
107
	*/
108 23
	public function getMinifier()
109
	{
110 23
		if (!isset($this->minifier))
111
		{
112 21
			$this->minifier = new Noop;
113
		}
114
115 23
		return $this->minifier;
116
	}
117
118
	/**
119
	* Get a JavaScript parser
120
	*
121
	* @param  array  $config Config array returned by the configurator
122
	* @return string         JavaScript parser
123
	*/
124 21
	public function getParser(array $config = null)
125
	{
126 21
		$this->configOptimizer->reset();
127
128
		// Get the stylesheet used for rendering
129 21
		$this->xsl = (new XSLT)->getXSL($this->configurator->rendering);
130
131
		// Prepare the parser's config
132 21
		$this->config = (isset($config)) ? $config : $this->configurator->asConfig();
133 21
		$this->config = ConfigHelper::filterConfig($this->config, 'JS');
134 21
		$this->config = $this->callbackGenerator->replaceCallbacks($this->config);
135
136
		// Get the parser's source and inject its config
137 21
		$src = $this->getHints() . $this->injectConfig($this->getSource());
138
139
		// Export the public API
140 20
		$src .= "if (!window['s9e']) window['s9e'] = {};\n" . $this->getExports();
141
142
		// Minify the source
143 20
		$src = $this->getMinifier()->get($src);
144
145
		// Wrap the source in a function to protect the global scope
146 20
		$src = '(function(){' . $src . '})()';
147
148 20
		return $src;
149
	}
150
151
	/**
152
	* Set the cached instance of Minifier
153
	*
154
	* Extra arguments will be passed to the minifier's constructor
155
	*
156
	* @param  string|Minifier $minifier Name of a supported minifier, or an instance of Minifier
157
	* @return Minifier                  The new minifier
158
	*/
159 5
	public function setMinifier($minifier)
160
	{
161 5
		if (is_string($minifier))
162
		{
163 3
			$className = __NAMESPACE__ . '\\JavaScript\\Minifiers\\' . $minifier;
164
165
			// Pass the extra argument to the constructor, if applicable
166 3
			$args = array_slice(func_get_args(), 1);
167 3
			if (!empty($args))
168
			{
169 1
				$reflection = new ReflectionClass($className);
170 1
				$minifier   = $reflection->newInstanceArgs($args);
171
			}
172
			else
173
			{
174 2
				$minifier = new $className;
175
			}
176
		}
177
178 5
		$this->minifier = $minifier;
179
180 5
		return $minifier;
181
	}
182
183
	//==========================================================================
184
	// Internal
185
	//==========================================================================
186
187
	/**
188
	* Encode a PHP value into an equivalent JavaScript representation
189
	*
190
	* @param  mixed  $value Original value
191
	* @return string        JavaScript representation
192
	*/
193 20
	protected function encode($value)
194
	{
195 20
		return $this->encoder->encode($value);
196
	}
197
198
	/**
199
	* Generate and return the public API
200
	*
201
	* @return string JavaScript Code
202
	*/
203 20
	protected function getExports()
204
	{
205 20
		if (empty($this->exportMethods))
206
		{
207 1
			return '';
208
		}
209
210 19
		$methods = [];
211 19
		foreach ($this->exportMethods as $method)
212
		{
213 19
			$methods[] = "'" . $method . "':" . $method;
214
		}
215
216 19
		return "window['s9e']['TextFormatter'] = {" . implode(',', $methods) . '}';
217
	}
218
219
	/**
220
	* Generate a HINT object that contains informations about the configuration
221
	*
222
	* @return string JavaScript Code
223
	*/
224 21
	protected function getHints()
225
	{
226 21
		$this->hintGenerator->setConfig($this->config);
227 21
		$this->hintGenerator->setPlugins($this->configurator->plugins);
228 21
		$this->hintGenerator->setXSL($this->xsl);
229
230 21
		return $this->hintGenerator->getHints();
231
	}
232
233
	/**
234
	* Return the plugins' config
235
	*
236
	* @return Dictionary
237
	*/
238 21
	protected function getPluginsConfig()
239
	{
240 21
		$plugins = new Dictionary;
241
242 21
		foreach ($this->config['plugins'] as $pluginName => $pluginConfig)
243
		{
244 5
			if (!isset($pluginConfig['js']))
245
			{
246
				// Skip this plugin
247 1
				continue;
248
			}
249 4
			$js = $pluginConfig['js'];
250 4
			unset($pluginConfig['js']);
251
252
			// Not needed in JavaScript
253 4
			unset($pluginConfig['className']);
254
255
			// Ensure that quickMatch is UTF-8 if present
256 4
			if (isset($pluginConfig['quickMatch']))
257
			{
258
				// Well-formed UTF-8 sequences
259
				$valid = [
260 4
					'[[:ascii:]]',
261
					// [1100 0000-1101 1111] [1000 0000-1011 1111]
262
					'[\\xC0-\\xDF][\\x80-\\xBF]',
263
					// [1110 0000-1110 1111] [1000 0000-1011 1111]{2}
264
					'[\\xE0-\\xEF][\\x80-\\xBF]{2}',
265
					// [1111 0000-1111 0111] [1000 0000-1011 1111]{3}
266
					'[\\xF0-\\xF7][\\x80-\\xBF]{3}'
267
				];
268
269 4
				$regexp = '#(?>' . implode('|', $valid) . ')+#';
270
271
				// Keep only the first valid sequence of UTF-8, or unset quickMatch if none is found
272 4
				if (preg_match($regexp, $pluginConfig['quickMatch'], $m))
273
				{
274 3
					$pluginConfig['quickMatch'] = $m[0];
275
				}
276
				else
277
				{
278 1
					unset($pluginConfig['quickMatch']);
279
				}
280
			}
281
282
			/**
283
			* @var array Keys of elements that are kept in the global scope. Everything else will be
284
			*            moved into the plugin's parser
285
			*/
286
			$globalKeys = [
287 4
				'quickMatch'  => 1,
288
				'regexp'      => 1,
289
				'regexpLimit' => 1
290
			];
291
292 4
			$globalConfig = array_intersect_key($pluginConfig, $globalKeys);
293 4
			$localConfig  = array_diff_key($pluginConfig, $globalKeys);
294
295 4
			if (isset($globalConfig['regexp']) && !($globalConfig['regexp'] instanceof Code))
296
			{
297 4
				$globalConfig['regexp'] = new Code(RegexpConvertor::toJS($globalConfig['regexp'], true));
298
			}
299
300 4
			$globalConfig['parser'] = new Code(
301
				'/**
302
				* @param {!string} text
303
				* @param {!Array.<Array>} matches
304
				*/
305
				function(text, matches)
306
				{
307
					/** @const */
308 4
					var config=' . $this->encode($localConfig) . ';
309 4
					' . $js . '
310
				}'
311
			);
312
313 4
			$plugins[$pluginName] = $globalConfig;
314
		}
315
316 21
		return $plugins;
317
	}
318
319
	/**
320
	* Return the registeredVars config
321
	*
322
	* @return Dictionary
323
	*/
324 21
	protected function getRegisteredVarsConfig()
325
	{
326 21
		$registeredVars = $this->config['registeredVars'];
327
328
		// Remove cacheDir from the registered vars. Not only it is useless in JavaScript, it could
329
		// leak some informations about the server
330 21
		unset($registeredVars['cacheDir']);
331
332 21
		return new Dictionary($registeredVars);
333
	}
334
335
	/**
336
	* Return the root context config
337
	*
338
	* @return array
339
	*/
340 21
	protected function getRootContext()
341
	{
342 21
		return $this->config['rootContext'];
343
	}
344
345
	/**
346
	* Return the parser's source
347
	*
348
	* @return string
349
	*/
350 21
	protected function getSource()
351
	{
352 21
		$rootDir = __DIR__ . '/..';
353 21
		$src     = '';
354
355
		// If getLogger() is not exported we use a dummy Logger that can be optimized away
356 21
		$logger = (in_array('getLogger', $this->exportMethods)) ? 'Logger.js' : 'NullLogger.js';
357
358
		// Prepare the list of files
359 21
		$files   = glob($rootDir . '/Parser/AttributeFilters/*.js');
360 21
		$files[] = $rootDir . '/Parser/utils.js';
361 21
		$files[] = $rootDir . '/Parser/' . $logger;
362 21
		$files[] = $rootDir . '/Parser/Tag.js';
363 21
		$files[] = $rootDir . '/Parser.js';
364
365
		// Append render.js if we export the preview method
366 21
		if (in_array('preview', $this->exportMethods, true))
367
		{
368 20
			$files[] = $rootDir . '/render.js';
369 20
			$src .= '/** @const */ var xsl=' . $this->getStylesheet() . ";\n";
370
		}
371
372 21
		$src .= implode("\n", array_map('file_get_contents', $files));
373
374 21
		return $src;
375
	}
376
377
	/**
378
	* Return the JavaScript representation of the stylesheet
379
	*
380
	* @return string
381
	*/
382 20
	protected function getStylesheet()
383
	{
384 20
		return $this->stylesheetCompressor->encode($this->xsl);
385
	}
386
387
	/**
388
	* Return the tags' config
389
	*
390
	* @return Dictionary
391
	*/
392 21
	protected function getTagsConfig()
393
	{
394
		// Prepare a Dictionary that will preserve tags' names
395 21
		$tags = new Dictionary;
396 21
		foreach ($this->config['tags'] as $tagName => $tagConfig)
397
		{
398 8
			if (isset($tagConfig['attributes']))
399
			{
400
				// Make the attributes array a Dictionary, to preserve the attributes' names
401 6
				$tagConfig['attributes'] = new Dictionary($tagConfig['attributes']);
402
			}
403
404 8
			$tags[$tagName] = $tagConfig;
405
		}
406
407 21
		return $tags;
408
	}
409
410
	/**
411
	* Inject the parser config into given source
412
	*
413
	* @param  string $src Parser's source
414
	* @return string      Modified source
415
	*/
416 21
	protected function injectConfig($src)
417
	{
418 21
		$config = array_map(
419 21
			[$this, 'encode'],
420 21
			$this->configOptimizer->optimize(
421
				[
422 21
					'plugins'        => $this->getPluginsConfig(),
423 21
					'registeredVars' => $this->getRegisteredVarsConfig(),
424 21
					'rootContext'    => $this->getRootContext(),
425 21
					'tagsConfig'     => $this->getTagsConfig()
426
				]
427
			)
428
		);
429
430 20
		$src = preg_replace_callback(
431 20
			'/(\\nvar (' . implode('|', array_keys($config)) . '))(;)/',
432 20
			function ($m) use ($config)
433
			{
434 20
				return $m[1] . '=' . $config[$m[2]] . $m[3];
435 20
			},
436
			$src
437
		);
438
439
		// Prepend the deduplicated objects
440
		$src = $this->configOptimizer->getVarDeclarations() . $src;
441
442
		return $src;
443
	}
444
}