', '', '', ' replace] */ protected $dictionary; /** * @var string Prefix used for dictionary keys */ protected $keyPrefix = '$'; /** * @var integer Number of bytes each global substitution must save to be considered */ public $minSaving = 10; /** * @var array Associative array of [string => saving] */ protected $savings; /** * @var string */ protected $xsl; /** * Encode given stylesheet into a compact JavaScript representation * * @param string $xsl Original stylesheet * @return string JavaScript representation of the compressed stylesheet */ public function encode($xsl) { $this->xsl = $xsl; $this->estimateSavings(); $this->filterSavings(); $this->buildDictionary(); $str = $this->getCompressedStylesheet(); // Split the stylesheet's string into 2000 chars chunks to appease Google Closure Compiler preg_match_all('(.{1,2000})su', $str, $matches); $js = implode("+\n", array_map('json_encode', $matches[0])); if (!empty($this->dictionary)) { $js = '(' . $js . ').replace(' . $this->getReplacementRegexp() . ',function(k){return' . json_encode($this->dictionary) . '[k];})'; } return $js; } /** * Build a dictionary of all cost-effective string replacements * * @return void */ protected function buildDictionary() { $keys = $this->getAvailableKeys(); rsort($keys); $this->dictionary = []; arsort($this->savings); foreach (array_keys($this->savings) as $str) { $key = array_pop($keys); if (!$key) { break; } $this->dictionary[$key] = $str; } } /** * Estimate the savings of every possible string replacement * * @return void */ protected function estimateSavings() { $this->savings = []; foreach ($this->getStringsFrequency() as $str => $cnt) { $len = strlen($str); $originalCost = $cnt * $len; $replacementCost = $cnt * 2; $overhead = $len + 6; $this->savings[$str] = $originalCost - ($replacementCost + $overhead); } } /** * Filter the savings according to the minSaving property * * @return void */ protected function filterSavings() { $this->savings = array_filter( $this->savings, function ($saving) { return ($saving >= $this->minSaving); } ); } /** * Return all the possible dictionary keys that are not present in the original stylesheet * * @return string[] */ protected function getAvailableKeys() { return array_diff($this->getPossibleKeys(), $this->getUnavailableKeys()); } /** * Return the stylesheet after dictionary replacements * * @return string */ protected function getCompressedStylesheet() { return strtr($this->xsl, array_flip($this->dictionary)); } /** * Return a list of possible dictionary keys * * @return string[] */ protected function getPossibleKeys() { $keys = []; foreach (range('a', 'z') as $char) { $keys[] = $this->keyPrefix . $char; } return $keys; } /** * Return a regexp that matches all used dictionary keys * * @return string */ protected function getReplacementRegexp() { return '/' . RegexpBuilder::fromList(array_keys($this->dictionary)) . '/g'; } /** * Return the frequency of all deduplicatable strings * * @return array Array of [string => frequency] */ protected function getStringsFrequency() { $regexp = '(' . implode('|', $this->deduplicateTargets) . ')S'; preg_match_all($regexp, $this->xsl, $matches); return array_count_values($matches[0]); } /** * Return the list of possible dictionary keys that appear in the original stylesheet * * @return string[] */ protected function getUnavailableKeys() { preg_match_all('(' . preg_quote($this->keyPrefix) . '.)', $this->xsl, $matches); return array_unique($matches[0]); } }