Passed
Push — master ( 1e0853...e3245d )
by Josh
04:24
created

JavaScript::getPluginsConfig()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 79
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 7

Importance

Changes 0
Metric Value
eloc 38
dl 0
loc 79
ccs 19
cts 19
cp 1
rs 8.3786
c 0
b 0
f 0
cc 7
nc 8
nop 0
crap 7

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

448
			/** @scrutinizer ignore-type */ $this->configOptimizer->optimize(
Loading history...
449
				[
450
					'plugins'        => $this->getPluginsConfig(),
451
					'registeredVars' => $this->getRegisteredVarsConfig(),
452
					'rootContext'    => $this->getRootContext(),
453
					'tagsConfig'     => $this->getTagsConfig()
454
				]
455
			)
456
		);
457
458
		$src = preg_replace_callback(
459
			'/(\\nvar (' . implode('|', array_keys($config)) . '))(;)/',
460
			function ($m) use ($config)
461
			{
462
				return $m[1] . '=' . $config[$m[2]] . $m[3];
463
			},
464
			$src
465
		);
466
467
		// Prepend the deduplicated objects
468
		$src = $this->configOptimizer->getVarDeclarations() . $src;
469
470
		return $src;
471
	}
472
}