Completed
Push — master ( 4d3b0b...d0543d )
by Josh
20:23
created

StylesheetCompressor::estimateSavings()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 8
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 13
ccs 10
cts 10
cp 1
crap 2
rs 9.4285
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\JavaScript;
9
10
use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
11
use s9e\TextFormatter\Configurator\JavaScript\Code;
12
13
class StylesheetCompressor
14
{
15
	/**
16
	* @var string[] List of regular expressions that match strings to deduplicate
17
	*/
18
	protected $deduplicateTargets = [
19
		'<xsl:template match="',
20
		'</xsl:template>',
21
		'<xsl:apply-templates/>',
22
		'<param name="allowfullscreen" value="true"/>',
23
		'<xsl:value-of select="',
24
		'<xsl:copy-of select="@',
25
		'<iframe allowfullscreen="" scrolling="no"',
26
		'overflow:hidden;position:relative;padding-bottom:',
27
		'display:inline-block;width:100%;max-width:',
28
		' [-:\\w]++="',
29
		'\\{[^}]++\\}',
30
		'@[-\\w]{4,}+',
31
		'(?<=<)[-:\\w]{4,}+',
32
		'(?<==")[^"]{4,}+"'
33
	];
34
35
	/**
36
	* @var array Associative array of string replacements as [match => replace]
37
	*/
38
	protected $dictionary;
39
40
	/**
41
	* @var string Prefix used for dictionary keys
42
	*/
43
	protected $keyPrefix = '$';
44
45
	/**
46
	* @var integer Number of bytes each global substitution must save to be considered
47
	*/
48
	public $minSaving = 10;
49
50
	/**
51
	* @var array Associative array of [string => saving]
52
	*/
53
	protected $savings;
54
55
	/**
56
	* @var string
57
	*/
58
	protected $xsl;
59
60
	/**
61
	* Encode given stylesheet into a compact JavaScript representation
62
	*
63
	* @param  string $xsl Original stylesheet
64
	* @return string      JavaScript representation of the compressed stylesheet
65
	*/
66 6
	public function encode($xsl)
67
	{
68 6
		$this->xsl = $xsl;
69
70 6
		$this->estimateSavings();
71 6
		$this->filterSavings();
72 6
		$this->buildDictionary();
73
74 6
		$js = json_encode($this->getCompressedStylesheet());
75 6
		if (!empty($this->dictionary))
76 6
		{
77 4
			$js .= '.replace(' . $this->getReplacementRegexp() . ',function(k){return' . json_encode($this->dictionary) . '[k]})';
78 4
		}
79
80 6
		return $js;
81
	}
82
83
	/**
84
	* Build a dictionary of all cost-effective string replacements
85
	*
86
	* @return void
87
	*/
88 6
	protected function buildDictionary()
89
	{
90 6
		$keys = $this->getAvailableKeys();
91 6
		rsort($keys);
92
93 6
		$this->dictionary = [];
94 6
		arsort($this->savings);
95 6
		foreach (array_keys($this->savings) as $str)
96
		{
97 5
			$key = array_pop($keys);
98 5
			if (!$key)
99 5
			{
100 1
				break;
101
			}
102
103 4
			$this->dictionary[$key] = $str;
104 6
		}
105 6
	}
106
107
	/**
108
	* Estimate the savings of every possible string replacement
109
	*
110
	* @return void
111
	*/
112 6
	protected function estimateSavings()
113
	{
114 6
		$this->savings = [];
115 6
		foreach ($this->getStringsFrequency() as $str => $cnt)
116
		{
117 6
			$len             = strlen($str);
118 6
			$originalCost    = $cnt * $len;
119 6
			$replacementCost = $cnt * 2;
120 6
			$overhead        = $len + 6;
121
122 6
			$this->savings[$str] = $originalCost - ($replacementCost + $overhead);
123 6
		}
124 6
	}
125
126
	/**
127
	* Filter the savings according to the minSaving property
128
	*
129
	* @return void
130
	*/
131 6
	protected function filterSavings()
132
	{
133 6
		$this->savings = array_filter(
134 6
			$this->savings,
135 6
			function ($saving)
136
			{
137 6
				return ($saving >= $this->minSaving);
138
			}
139 6
		);
140 6
	}
141
142
	/**
143
	* Return all the possible dictionary keys that are not present in the original stylesheet
144
	*
145
	* @return string[]
146
	*/
147 6
	protected function getAvailableKeys()
148
	{
149 6
		return array_diff($this->getPossibleKeys(), $this->getUnavailableKeys());
150
	}
151
152
	/**
153
	* Return the stylesheet after dictionary replacements
154
	*
155
	* @return string
156
	*/
157 6
	protected function getCompressedStylesheet()
158
	{
159 6
		return strtr($this->xsl, array_flip($this->dictionary));
160
	}
161
162
	/**
163
	* Return a list of possible dictionary keys
164
	*
165
	* @return string[]
166
	*/
167 6
	protected function getPossibleKeys()
168
	{
169 6
		$keys = [];
170 6
		foreach (range('a', 'z') as $char)
171
		{
172 6
			$keys[] = $this->keyPrefix . $char;
173 6
		}
174
175 6
		return $keys;
176
	}
177
178
	/**
179
	* Return a regexp that matches all used dictionary keys
180
	*
181
	* @return string
182
	*/
183 4
	protected function getReplacementRegexp()
184
	{
185 4
		return '/' . RegexpBuilder::fromList(array_keys($this->dictionary)) . '/g';
186
	}
187
188
	/**
189
	* Return the frequency of all deduplicatable strings
190
	*
191
	* @return array Array of [string => frequency]
192
	*/
193 6
	protected function getStringsFrequency()
194
	{
195 6
		$regexp = '(' . implode('|', $this->deduplicateTargets) . ')S';
196 6
		preg_match_all($regexp, $this->xsl, $matches);
197
198 6
		return array_count_values($matches[0]);
199
	}
200
201
	/**
202
	* Return the list of possible dictionary keys that appear in the original stylesheet
203
	*
204
	* @return string[]
205
	*/
206 6
	protected function getUnavailableKeys()
207
	{
208 6
		preg_match_all('(' . preg_quote($this->keyPrefix) . '.)', $this->xsl, $matches);
209
210 6
		return array_unique($matches[0]);
211
	}
212
}