Elgg  Version master
Process.php
Go to the documentation of this file.
1 <?php
8 namespace CssCrush;
9 #[\AllowDynamicProperties]
10 class Process
11 {
12  use EventEmitter;
13 
15  public $aliases;
16  public $cacheData;
17  public $cacheFile;
18  public $charset;
19  public $docRoot;
20  public $fragments;
21  public $functions;
22  public $generateMap;
23  public $input;
24  public $io;
25  public $ioContext;
26  public $minifyOutput;
27  public $misc;
28  public $mixins;
29  public $newline;
30  public $options;
31  public $output;
32  public $references;
36  public $sourceMap;
37  public $sources;
38  public $string;
39  public $tokens;
40  public $vars;
41 
42  public $debugLog;
43  public $errors;
44  public $stat;
45  public $warnings;
46 
47  private $plugins;
48 
49  public function __construct($user_options = [], $context = [])
50  {
52 
53  Crush::loadAssets();
54 
55  // Initialize properties.
56  $this->cacheData = [];
57  $this->mixins = [];
58  $this->fragments = [];
59  $this->references = [];
60  $this->absoluteImports = [];
61  $this->charset = null;
62  $this->sources = [];
63  $this->vars = [];
64  $this->plugins = [];
65  $this->misc = new \stdClass();
66  $this->input = new \stdClass();
67  $this->output = new \stdClass();
68  $this->tokens = new Tokens();
69  $this->functions = new Functions();
70  $this->sourceMap = null;
71  $this->selectorAliases = [];
72  $this->selectorAliasesPatt = null;
73  $this->io = new Crush::$config->io($this);
74 
75  $this->errors = [];
76  $this->warnings = [];
77  $this->debugLog = [];
78  $this->stat = [];
79 
80  // Copy config values.
81  $this->aliases = $config->aliases;
82 
83  // Options.
84  $this->options = new Options($user_options, $config->options);
85 
86  // Context options.
87  $context += ['type' => 'filter', 'data' => ''];
88  $this->ioContext = $context['type'];
89 
90  // Keep track of global vars to maintain cache integrity.
91  $this->options->global_vars = $config->vars;
92 
93  // Shortcut commonly used options to avoid __get() overhead.
94  $this->docRoot = isset($this->options->doc_root) ? $this->options->doc_root : $config->docRoot;
95  $this->generateMap = $this->ioContext === 'file' && $this->options->__get('source_map');
96  $this->ruleFormatter = $this->options->__get('formatter');
97  $this->minifyOutput = $this->options->__get('minify');
98  $this->newline = $this->options->__get('newlines');
99 
100  $useContextOption = ! empty($this->options->context)
101  && (php_sapi_name() === 'cli' || $context['type'] === 'filter');
102 
103  if ($context['type'] === 'file') {
104  $file = $context['data'];
105  $this->input->raw = $file;
106  if (! ($inputFile = Util::resolveUserPath($file, null, $this->docRoot))) {
107  throw new \Exception('Input file \'' . basename($file) . '\' not found.');
108  }
109  $inputDir = $useContextOption
110  ? $this->options->context
111  : dirname($inputFile);
112  $this->resolveContext($inputDir, $inputFile);
113  }
114  elseif ($context['type'] === 'filter') {
115  if ($useContextOption) {
116  $this->resolveContext($this->options->context);
117  }
118  else {
119  $this->resolveContext();
120  }
121  $this->input->string = $context['data'];
122  }
123  }
124 
125  public function release()
126  {
127  unset(
128  $this->tokens,
129  $this->mixins,
130  $this->references,
131  $this->cacheData,
132  $this->misc,
133  $this->plugins,
134  $this->aliases,
135  $this->selectorAliases
136  );
137  }
138 
139  public function resolveContext($input_dir = null, $input_file = null)
140  {
141  if ($input_file) {
142  $this->input->path = $input_file;
143  $this->input->filename = basename($input_file);
144  $this->input->mtime = filemtime($input_file);
145  }
146  else {
147  $this->input->path = null;
148  $this->input->filename = null;
149  }
150 
151  $this->input->dir = $input_dir ?: $this->docRoot;
152  $this->input->dirUrl = substr($this->input->dir, strlen($this->docRoot));
153  $this->output->dir = $this->io->getOutputDir();
154  $this->output->filename = $this->io->getOutputFileName();
155  $this->output->dirUrl = substr($this->output->dir, strlen($this->docRoot));
156 
157  $context_resolved = true;
158  if ($input_file) {
159  $output_dir = $this->output->dir;
160 
161  if (! file_exists($output_dir)) {
162  warning("Output directory '$output_dir' doesn't exist.");
163  $context_resolved = false;
164  }
165  elseif (! is_writable($output_dir)) {
166 
167  debug('Attempting to change permissions.');
168 
169  if (! @chmod($output_dir, 0755)) {
170  warning("Output directory '$output_dir' is unwritable.");
171  $context_resolved = false;
172  }
173  else {
174  debug('Permissions updated.');
175  }
176  }
177  }
178 
179  $this->io->init();
180 
181  return $context_resolved;
182  }
183 
184 
185  #############################
186  # Boilerplate.
187 
188  protected function getBoilerplate()
189  {
190  $file = false;
191  $boilerplateOption = $this->options->boilerplate;
192 
193  if ($boilerplateOption === true) {
194  $file = Crush::$dir . '/boilerplate.txt';
195  }
196  elseif (is_string($boilerplateOption)) {
197  if (file_exists($boilerplateOption)) {
198  $file = $boilerplateOption;
199  }
200  }
201 
202  // Return an empty string if no file is found.
203  if (! $file) {
204  return '';
205  }
206 
207  $boilerplate = file_get_contents($file);
208 
209  // Substitute any tags
210  if (preg_match_all('~\{\{([^}]+)\}\}~', $boilerplate, $boilerplateMatches)) {
211 
212  // Command line arguments (if any).
213  $commandArgs = 'n/a';
214  if (isset($_SERVER['argv'])) {
215  $argv = $_SERVER['argv'];
216  array_shift($argv);
217  $commandArgs = 'csscrush ' . implode(' ', $argv);
218  }
219 
220  $tags = [
221  'datetime' => @date('Y-m-d H:i:s O'),
222  'year' => @date('Y'),
223  'command' => $commandArgs,
224  'plugins' => implode(',', $this->plugins),
225  'version' => function () {
226  return Version::detect();
227  },
228  'compile_time' => function () {
229  $now = microtime(true) - Crush::$process->stat['compile_start_time'];
230  return round($now, 4) . ' seconds';
231  },
232  ];
233 
234  $replacements = [];
235 
236  foreach (array_keys($boilerplateMatches[0]) as $index) {
237  $tagName = trim($boilerplateMatches[1][$index]);
238  $replacement = '?';
239  if (isset($tags[$tagName])) {
240  $replacement = is_callable($tags[$tagName]) ? $tags[$tagName]() : $tags[$tagName];
241  }
242  $replacements[] = $replacement;
243  }
244  $boilerplate = str_replace($boilerplateMatches[0], $replacements, $boilerplate);
245  }
246 
247  // Pretty print.
248  $EOL = $this->newline;
249  $boilerplate = preg_split('~[\t]*'. Regex::$classes->newline . '[\t]*~', trim($boilerplate));
250  $boilerplate = array_map('trim', $boilerplate);
251  $boilerplate = "$EOL * " . implode("$EOL * ", $boilerplate);
252 
253  return "/*$boilerplate$EOL */$EOL";
254  }
255 
256 
257  #############################
258  # Selector aliases.
259 
260  protected function resolveSelectorAliases()
261  {
262  $this->string->pregReplaceCallback(
263  Regex::make('~@selector(?:-(?<type>alias|splat))? +\:?(?<name>{{ident}}) +(?<handler>[^;]+) *;~iS'),
264  function ($m) {
265  $name = strtolower($m['name']);
266  $type = ! empty($m['type']) ? strtolower($m['type']) : 'alias';
267  $handler = Util::stripCommentTokens($m['handler']);
268  Crush::$process->selectorAliases[$name] = new SelectorAlias($handler, $type);
269  });
270 
271  // Create the selector aliases pattern and store it.
272  if ($this->selectorAliases) {
273  $names = implode('|', array_keys($this->selectorAliases));
274  $this->selectorAliasesPatt
275  = Regex::make('~\:(' . $names . '){{RB}}(\‍()?~iS');
276  }
277  }
278 
279  public function addSelectorAlias($name, $handler, $type = 'alias')
280  {
281  if ($type != 'callback') {
282  $handler = $this->tokens->capture($handler, 's');
283  }
284  $this->selectorAliases[$name] = new SelectorAlias($handler, $type);
285  }
286 
287 
288  #############################
289  # Aliases.
290 
291  protected function filterAliases()
292  {
293  // If a vendor target is given, we prune the aliases array.
294  $vendors = $this->options->vendor_target;
295 
296  // Default vendor argument, so use all aliases as normal.
297  if ('all' === $vendors) {
298 
299  return;
300  }
301 
302  // For expicit 'none' argument turn off aliases.
303  if ('none' === $vendors) {
304  $this->aliases = Crush::$config->bareAliases;
305 
306  return;
307  }
308 
309  // Normalize vendor names and create regex patt.
310  $vendor_names = (array) $vendors;
311  foreach ($vendor_names as &$vendor_name) {
312  $vendor_name = trim($vendor_name, '-');
313  }
314  $vendor_patt = '~^\-(' . implode('|', $vendor_names) . ')\-~i';
315 
316 
317  // Loop the aliases array, filter down to the target vendor.
318  foreach ($this->aliases as $section => $group_array) {
319 
320  // Declarations aliases.
321  if ($section === 'declarations') {
322 
323  foreach ($group_array as $property => $values) {
324  foreach ($values as $value => $prefix_values) {
325  foreach ($prefix_values as $index => $declaration) {
326 
327  if (in_array($declaration[2], $vendor_names)) {
328  continue;
329  }
330 
331  // Unset uneeded aliases.
332  unset($this->aliases[$section][$property][$value][$index]);
333 
334  if (empty($this->aliases[$section][$property][$value])) {
335  unset($this->aliases[$section][$property][$value]);
336  }
337  if (empty($this->aliases[$section][$property])) {
338  unset($this->aliases[$section][$property]);
339  }
340  }
341  }
342  }
343  }
344 
345  // Function group aliases.
346  elseif ($section === 'function_groups') {
347 
348  foreach ($group_array as $func_group => $vendors) {
349  foreach (array_keys($vendors) as $vendor) {
350  if (! in_array($vendor, $vendor_names)) {
351  unset($this->aliases['function_groups'][$func_group][$vendor]);
352  }
353  }
354  }
355  }
356 
357  // Everything else.
358  else {
359  foreach ($group_array as $alias_keyword => $prefix_array) {
360 
361  // Skip over pointers to function groups.
362  if ($prefix_array[0] === '.') {
363  continue;
364  }
365 
366  $result = [];
367 
368  foreach ($prefix_array as $prefix) {
369  if (preg_match($vendor_patt, $prefix)) {
370  $result[] = $prefix;
371  }
372  }
373 
374  // Prune the whole alias keyword if there is no result.
375  if (empty($result)) {
376  unset($this->aliases[$section][$alias_keyword]);
377  }
378  else {
379  $this->aliases[$section][$alias_keyword] = $result;
380  }
381  }
382  }
383  }
384  }
385 
386 
387  #############################
388  # Plugins.
389 
390  protected function filterPlugins()
391  {
392  $this->plugins = array_unique($this->options->plugins);
393 
394  foreach ($this->plugins as $plugin) {
395  Crush::enablePlugin($plugin);
396  }
397  }
398 
399 
400  #############################
401  # Variables.
402 
403  protected function captureVars()
404  {
405  Crush::$process->vars = Crush::$process->string->captureDirectives(['set', 'define'], [
406  'singles' => true,
407  'lowercase_keys' => false,
408  ]) + Crush::$process->vars;
409 
410  // For convenience adding a runtime variable for cache busting linked resources.
411  $this->vars['timestamp'] = (int) $this->stat['compile_start_time'];
412 
413  // In-file variables override global variables.
414  $this->vars += Crush::$config->vars;
415 
416  // Runtime variables override in-file variables.
417  if (! empty($this->options->vars)) {
418  $this->vars = $this->options->vars + $this->vars;
419  }
420 
421  // Place variables referenced inside variables.
422  foreach ($this->vars as &$value) {
423  $this->placeVars($value);
424  }
425  }
426 
427  protected function placeAllVars()
428  {
429  $this->placeVars($this->string->raw);
430 
431  $rawTokens =& $this->tokens->store;
432 
433  // Repeat above steps for variables embedded in string tokens.
434  foreach ($rawTokens->s as $label => &$value) {
435  $this->placeVars($value);
436  }
437 
438  // Repeat above steps for variables embedded in URL tokens.
439  foreach ($rawTokens->u as $label => $url) {
440  if (! $url->isData && $this->placeVars($url->value)) {
441  // Re-evaluate $url->value if anything has been interpolated.
442  $url->evaluate();
443  }
444  }
445  }
446 
447  protected function placeVars(&$value)
448  {
449  static $varFunction, $varFunctionSimple;
450  if (! $varFunction) {
451  $varFunctionSimple = Regex::make('~\$\‍( \s* ({{ ident }}) \s* \‍)~xS');
452  $varFunction = new Functions(['$' => function ($rawArgs) {
453  $args = Functions::parseArgsSimple($rawArgs);
454  if (isset(Crush::$process->vars[$args[0]])) {
455  return Crush::$process->vars[$args[0]];
456  }
457  else {
458  return isset($args[1]) ? $args[1] : '';
459  }
460  }]);
461  }
462 
463  // Variables with no default value.
464  $value = preg_replace_callback($varFunctionSimple, function ($m) {
465  $varName = $m[1];
466  if (isset(Crush::$process->vars[$varName])) {
467  return Crush::$process->vars[$varName];
468  }
469  }, $value ?? '', -1, $varsPlaced);
470 
471  // Variables with default value.
472  if (strpos($value, '$(') !== false) {
473 
474  // Assume at least one replace.
475  $varsPlaced = true;
476 
477  // Variables may be nested so need to apply full function parsing.
478  $value = $varFunction->apply($value);
479  }
480 
481  // If we know replacements have been made we may want to update $value. e.g URL tokens.
482  return $varsPlaced;
483  }
484 
485  #############################
486  # @for..in blocks.
487 
488  protected function resolveLoops()
489  {
490  $LOOP_VAR_PATT = '~\#\‍( \s* (?<arg>[a-zA-Z][\.a-zA-Z0-9-_]*) \s* \‍)~x';
491  $LOOP_PATT = Regex::make('~
492  (?<expression>
493  @for \s+ (?<var>{{ident}}) \s+ in \s+ (?<list>[^{]+)
494  ) \s*
495  {{ block }}
496  ~xiS');
497 
498  $apply_scope = function ($str, $context) use ($LOOP_VAR_PATT, $LOOP_PATT) {
499  // Need to temporarily hide child block scopes.
500  $child_scopes = [];
501  $str = preg_replace_callback($LOOP_PATT, function ($m) use (&$child_scopes) {
502  $label = '?B' . count($child_scopes) . '?';
503  $child_scopes[$label] = $m['block'];
504  return $m['expression'] . $label;
505  }, $str);
506 
507  $str = preg_replace_callback($LOOP_VAR_PATT, function ($m) use ($context) {
508  // Normalize casing of built-in loop variables.
509  // User variables are case-sensitive.
510  $arg = preg_replace_callback('~^loop\.(parent\.)?counter0?$~i', function ($m) {
511  return strtolower($m[0]);
512  }, $m['arg']);
513 
514  return isset($context[$arg]) ? $context[$arg] : '';
515  }, $str);
516 
517  return str_replace(array_keys($child_scopes), array_values($child_scopes), $str);
518  };
519 
520  $resolve_list = function ($list) {
521  // Resolve the list of items for iteration.
522  // Either a generator function or a plain list.
523  $items = [];
524  $this->placeVars($list);
525  $list = $this->functions->apply($list);
526  if (preg_match(Regex::make('~(?<func>range){{ parens }}~ix'), $list, $m)) {
527  $func = strtolower($m['func']);
528  $args = Functions::parseArgs($m['parens_content']);
529  switch ($func) {
530  case 'range':
531  $items = range(...$args);
532  break;
533  }
534  }
535  else {
536  $items = Util::splitDelimList($list);
537  }
538 
539  return $items;
540  };
541 
542  $unroll = function ($str, $context = []) use (&$unroll, $LOOP_PATT, $apply_scope, $resolve_list) {
543  $str = $apply_scope($str, $context);
544  while (preg_match($LOOP_PATT, $str, $m, PREG_OFFSET_CAPTURE)) {
545  $str = substr_replace($str, '', $m[0][1], strlen($m[0][0]));
546  $context['loop.parent.counter'] = isset($context['loop.counter']) ? $context['loop.counter'] : -1;
547  $context['loop.parent.counter0'] = isset($context['loop.counter0']) ? $context['loop.counter0'] : -1;
548  foreach ($resolve_list($m['list'][0]) as $index => $value) {
549  $str .= $unroll($m['block_content'][0], [
550  $m['var'][0] => $value,
551  'loop.counter' => $index + 1,
552  'loop.counter0' => $index,
553  ] + $context);
554  }
555  }
556 
557  return $str;
558  };
559 
560  $this->string->pregReplaceCallback($LOOP_PATT, function ($m) use ($unroll) {
561  return Template::tokenize($unroll(Template::unTokenize($m[0])));
562  });
563  }
564 
565  #############################
566  # @ifdefine blocks.
567 
568  protected function resolveIfDefines()
569  {
570  $ifdefinePatt = Regex::make('~@if(?:set|define) \s+ (?<negate>not \s+)? (?<name>{{ ident }}) \s* {{ parens }}? \s* \{~ixS');
571 
572  $matches = $this->string->matchAll($ifdefinePatt);
573 
574  while ($match = array_pop($matches)) {
575 
576  $curlyMatch = new BalancedMatch($this->string, $match[0][1]);
577 
578  if (! $curlyMatch->match) {
579  continue;
580  }
581 
582  $negate = $match['negate'][1] != -1;
583  $nameDefined = isset($this->vars[$match['name'][0]]);
584 
585  $valueDefined = isset($match['parens_content'][0]);
586  $valueMatch = false;
587  if ($nameDefined && $valueDefined) {
588  $testValue = Util::rawValue(trim($match['parens_content'][0]));
589  $varValue = Util::rawValue($this->vars[$match['name'][0]]);
590  $valueMatch = $varValue == $testValue;
591  }
592 
593  if (
594  ( $valueDefined && !$negate && $valueMatch )
595  || ( $valueDefined && $negate && !$valueMatch )
596  || ( !$valueDefined && !$negate && $nameDefined )
597  || ( !$valueDefined && $negate && !$nameDefined )
598  ) {
599  $curlyMatch->unWrap();
600  }
601  else {
602  $curlyMatch->replace('');
603  }
604  }
605  }
606 
607 
608  #############################
609  # Mixins.
610 
611  protected function captureMixins()
612  {
613  $this->string->pregReplaceCallback(Regex::$patt->mixin, function ($m) {
614  Crush::$process->mixins[$m['name']] = new Mixin($m['block_content']);
615  });
616  }
617 
618 
619  #############################
620  # Fragments.
621 
622  protected function resolveFragments()
623  {
624  $fragments =& Crush::$process->fragments;
625 
626  $this->string->pregReplaceCallback(Regex::$patt->fragmentCapture, function ($m) use (&$fragments) {
627  $fragments[$m['name']] = new Fragment(
628  $m['block_content'],
629  ['name' => strtolower($m['name'])]
630  );
631  return '';
632  });
633 
634  $this->string->pregReplaceCallback(Regex::$patt->fragmentInvoke, function ($m) use (&$fragments) {
635  $fragment = isset($fragments[$m['name']]) ? $fragments[$m['name']] : null;
636  if ($fragment) {
637  $args = [];
638  if (isset($m['parens'])) {
639  $args = Functions::parseArgs($m['parens_content']);
640  }
641  return $fragment($args);
642  }
643  return '';
644  });
645  }
646 
647 
648  #############################
649  # Rules.
650 
651  public function captureRules()
652  {
653  $tokens = $this->tokens;
654 
655  $rulePatt = Regex::make('~
656  (?<trace_token> {{ t_token }})
657  \s*
658  (?<selector> [^{]+)
659  \s*
660  {{ block }}
661  ~xiS');
662  $rulesAndMediaPatt = Regex::make('~{{ r_token }}|@media[^\{]+{{ block }}~iS');
663 
664  $count = preg_match_all(Regex::$patt->t_token, $this->string->raw, $traceMatches, PREG_OFFSET_CAPTURE);
665  while ($count--) {
666 
667  $traceOffset = $traceMatches[0][$count][1];
668 
669  preg_match($rulePatt, $this->string->raw, $ruleMatch, PREG_UNMATCHED_AS_NULL, $traceOffset);
670 
671  $selector = trim($ruleMatch['selector'] ?? '');
672  $block = trim($ruleMatch['block_content'] ?? '');
673  $replace = '';
674 
675  // If rules are nested inside we set their parent property.
676  if (preg_match_all(Regex::$patt->r_token, $block, $childMatches)) {
677 
678  $block = preg_replace_callback($rulesAndMediaPatt, function ($m) use (&$replace) {
679  $replace .= $m[0];
680  return '';
681  }, $block);
682 
683  $rule = new Rule($selector, $block, $ruleMatch['trace_token']);
684  foreach ($childMatches[0] as $childToken) {
685  $childRule = $tokens->get($childToken);
686  if (! $childRule->parent) {
687  $childRule->parent = $rule;
688  }
689  }
690  }
691  else {
692  $rule = new Rule($selector, $block, $ruleMatch['trace_token'] ?? '');
693  }
694 
695  $replace = $tokens->add($rule, 'r', $rule->label) . $replace;
696 
697  $this->string->splice($replace, $traceOffset, strlen($ruleMatch[0]) ?? '');
698  }
699 
700  // Flip, since we just captured rules in reverse order.
701  $tokens->store->r = array_reverse($tokens->store->r);
702 
703  foreach ($tokens->store->r as $rule) {
704  if ($rule->parent) {
705  $rule->selectors->merge(array_keys($rule->parent->selectors->store));
706  }
707  }
708 
709  // Cleanup unusable rules.
710  $this->string->pregReplaceCallback(Regex::$patt->r_token, function ($m) use ($tokens) {
711  $ruleToken = $m[0];
712  $rule = $tokens->store->r[$ruleToken];
713  if (empty($rule->declarations->store) && ! $rule->extendArgs) {
714  unset($tokens->store->r[$ruleToken]);
715  return '';
716  }
717  return $ruleToken;
718  });
719  }
720 
721  protected function processRules()
722  {
723  // Create table of name/selector to rule references.
724  $namedReferences = [];
725 
726  $previousRule = null;
727  foreach ($this->tokens->store->r as $rule) {
728  if ($rule->name) {
729  $namedReferences[$rule->name] = $rule;
730  }
731  foreach ($rule->selectors as $selector) {
732  $this->references[$selector->readableValue] = $rule;
733  }
734  if ($previousRule) {
735  $rule->previous = $previousRule;
736  $previousRule->next = $rule;
737  }
738  $previousRule = $rule;
739  }
740 
741  // Explicit named references take precedence.
742  $this->references = $namedReferences + $this->references;
743 
744  foreach ($this->tokens->store->r as $rule) {
745 
746  $rule->declarations->flatten();
747  $rule->declarations->process();
748 
749  $this->emit('rule_prealias', $rule);
750 
751  $rule->declarations->aliasProperties($rule->vendorContext);
752  $rule->declarations->aliasFunctions($rule->vendorContext);
753  $rule->declarations->aliasDeclarations($rule->vendorContext);
754 
755  $this->emit('rule_postalias', $rule);
756 
757  $rule->selectors->expand();
758  $rule->applyExtendables();
759 
760  $this->emit('rule_postprocess', $rule);
761  }
762  }
763 
764 
765  #############################
766  # @-rule aliasing.
767 
768  protected function aliasAtRules()
769  {
770  if (empty($this->aliases['at-rules'])) {
771 
772  return;
773  }
774 
775  $aliases = $this->aliases['at-rules'];
776  $regex = Regex::$patt;
777 
778  foreach ($aliases as $at_rule => $at_rule_aliases) {
779 
780  $matches = $this->string->matchAll("~@$at_rule" . '[\s{]~i');
781 
782  // Find at-rules that we want to alias.
783  while ($match = array_pop($matches)) {
784 
785  $curly_match = new BalancedMatch($this->string, $match[0][1]);
786 
787  if (! $curly_match->match) {
788  // Couldn't match the block.
789  continue;
790  }
791 
792  // Build up string with aliased blocks for splicing.
793  $original_block = $curly_match->whole();
794  $new_blocks = [];
795 
796  foreach ($at_rule_aliases as $alias) {
797 
798  // Copy original block, replacing at-rule with alias name.
799  $copy_block = str_replace("@$at_rule", "@$alias", $original_block);
800 
801  // Aliases are nearly always prefixed, capture the current vendor name.
802  preg_match($regex->vendorPrefix, $alias, $vendor);
803 
804  $vendor = $vendor ? $vendor[1] : null;
805 
806  // Duplicate rules.
807  if (preg_match_all($regex->r_token, $copy_block, $copy_matches)) {
808 
809  $originals = [];
810  $replacements = [];
811 
812  foreach ($copy_matches[0] as $rule_label) {
813 
814  // Clone the matched rule.
815  $originals[] = $rule_label;
816  $clone_rule = clone $this->tokens->get($rule_label);
817 
818  $clone_rule->vendorContext = $vendor;
819 
820  // Store the clone.
821  $replacements[] = $this->tokens->add($clone_rule);
822  }
823 
824  // Finally replace the original labels with the cloned rule labels.
825  $copy_block = str_replace($originals, $replacements, $copy_block);
826  }
827 
828  // Add the copied block to the stack.
829  $new_blocks[] = $copy_block;
830  }
831 
832  // The original version is always pushed last in the list.
833  $new_blocks[] = $original_block;
834 
835  // Splice in the blocks.
836  $curly_match->replace(implode("\n", $new_blocks));
837  }
838  }
839  }
840 
841 
842  #############################
843  # Compile / collate.
844 
845  protected function collate()
846  {
847  $options = $this->options;
848  $minify = $options->minify;
849  $EOL = $this->newline;
850 
851  // Formatting replacements.
852  // Strip newlines added during processing.
853  $regex_replacements = [];
854  $regex_replacements['~\n+~'] = '';
855 
856  if ($minify) {
857  // Strip whitespace around colons used in @-rule arguments.
858  $regex_replacements['~ ?\: ?~'] = ':';
859  }
860  else {
861  // Pretty printing.
862  $regex_replacements['~}~'] = "$0$EOL$EOL";
863  $regex_replacements['~([^\s])\{~'] = "$1 {";
864  $regex_replacements['~ ?(@[^{]+\{)~'] = "$1$EOL";
865  $regex_replacements['~ ?(@[^;]+\;)~'] = "$1$EOL";
866 
867  // Trim leading spaces on @-rules and some tokens.
868  $regex_replacements[Regex::make('~ +([@}]|\?[rc]{{token_id}}\?)~S')] = "$1";
869 
870  // Additional newline between adjacent rules and comments.
871  $regex_replacements[Regex::make('~({{r_token}}) (\s*) ({{c_token}})~xS')] = "$1$EOL$2$3";
872  }
873 
874  // Apply all formatting replacements.
875  $this->string->pregReplaceHash($regex_replacements)->lTrim();
876 
877  $this->string->restore('r');
878 
879  // Record stats then drop rule objects to reclaim memory.
880  Crush::runStat('selector_count', 'rule_count', 'vars');
881  $this->tokens->store->r = [];
882 
883  // If specified, apply advanced minification.
884  if (is_array($minify)) {
885  if (in_array('colors', $minify)) {
886  $this->minifyColors();
887  }
888  }
889 
890  $this->decruft();
891 
892  if (! $minify) {
893  // Add newlines after comments.
894  foreach ($this->tokens->store->c as $token => &$comment) {
895  $comment .= $EOL;
896  }
897 
898  // Insert comments and do final whitespace cleanup.
899  $this->string
900  ->restore('c')
901  ->trim()
902  ->append($EOL);
903  }
904 
905  // Insert URLs.
906  $urls = $this->tokens->store->u;
907  if ($urls) {
908 
909  $link = Util::getLinkBetweenPaths($this->output->dir, $this->input->dir);
910  $make_urls_absolute = $options->rewrite_import_urls === 'absolute';
911 
912  foreach ($urls as $token => $url) {
913 
914  if ($url->isRelative && ! $url->noRewrite) {
915  if ($make_urls_absolute) {
916  $url->toRoot();
917  }
918  // If output dir is different to input dir prepend a link between the two.
919  elseif ($link && $options->rewrite_import_urls) {
920  $url->prepend($link);
921  }
922  }
923  }
924  }
925 
926  if ($this->absoluteImports) {
927  $absoluteImports = '';
928  $closing = $minify ? ';' : ";$EOL";
929  foreach ($this->absoluteImports as $import) {
930  $absoluteImports .= "@import $import->url" . ($import->media ? " $import->media" : '') . $closing;
931  }
932  $this->string->prepend($absoluteImports);
933  }
934 
935  if ($options->boilerplate) {
936  $this->string->prepend($this->getBoilerplate());
937  }
938 
939  if ($this->charset) {
940  $this->string->prepend("@charset \"$this->charset\";$EOL");
941  }
942 
943  $this->string->restore(['u', 's']);
944 
945  if ($this->generateMap) {
946  $this->generateSourceMap();
947  }
948  }
949 
950  private $iniOriginal = [];
951  public function preCompile()
952  {
953  foreach ([
954  'pcre.backtrack_limit' => 1000000,
955  'pcre.jit' => 0, // Have run into PREG_JIT_STACKLIMIT_ERROR (issue #82).
956  'memory_limit' => '128M',
957  ] as $name => $value) {
958  $this->iniOriginal[$name] = ini_get($name);
959  if ($name === 'memory_limit' && $this->returnBytes(ini_get($name)) > $this->returnBytes($value)) {
960  continue;
961  }
962  ini_set($name, $value);
963  }
964 
965  $this->filterPlugins();
966  $this->filterAliases();
967 
968  $this->functions->setPattern(true);
969 
970  $this->stat['compile_start_time'] = microtime(true);
971  }
972 
973  private function returnBytes(string $value)
974  {
975  $value = trim($value);
976  $last = strtolower($value[strlen($value) - 1]);
977  $value = (float) $value;
978 
979  switch ($last) {
980  // The 'G' modifier is available
981  case 'g':
982  $value *= 1024;
983  case 'm':
984  $value *= 1024;
985  case 'k':
986  $value *= 1024;
987  }
988 
989  return $value;
990  }
991 
992  public function postCompile()
993  {
994  $this->release();
995 
996  Crush::runStat('compile_time');
997 
998  foreach ($this->iniOriginal as $name => $value) {
999  ini_set($name, $value);
1000  }
1001  }
1002 
1003  public function compile()
1004  {
1005  $this->preCompile();
1006 
1007  $importer = new Importer($this);
1008  $this->string = new StringObject($importer->collate());
1009 
1010  // Capture phase 0 hook: Before all variables have resolved.
1011  $this->emit('capture_phase0', $this);
1012 
1013  $this->captureVars();
1014 
1015  $this->resolveIfDefines();
1016 
1017  $this->resolveLoops();
1018 
1019  $this->placeAllVars();
1020 
1021  // Capture phase 1 hook: After all variables have resolved.
1022  $this->emit('capture_phase1', $this);
1023 
1024  $this->resolveSelectorAliases();
1025 
1026  $this->captureMixins();
1027 
1028  $this->resolveFragments();
1029 
1030  // Capture phase 2 hook: After most built-in directives have resolved.
1031  $this->emit('capture_phase2', $this);
1032 
1033  $this->captureRules();
1034 
1035  // Calling functions on media query lists.
1036  $process = $this;
1037  $this->string->pregReplaceCallback('~@media\s+(?<media_list>[^{]+)\{~i', function ($m) use (&$process) {
1038  return "@media {$process->functions->apply($m['media_list'])}{";
1039  });
1040 
1041  $this->aliasAtRules();
1042 
1043  $this->processRules();
1044 
1045  $this->collate();
1046 
1047  $this->postCompile();
1048 
1049  return $this->string;
1050  }
1051 
1052 
1053  #############################
1054  # Source maps.
1055 
1056  public function generateSourceMap()
1057  {
1058  $this->sourceMap = [
1059  'version' => 3,
1060  'file' => $this->output->filename,
1061  'sources' => [],
1062  ];
1063  foreach ($this->sources as $source) {
1064  $this->sourceMap['sources'][] = Util::getLinkBetweenPaths($this->output->dir, $source, false);
1065  }
1066 
1067  $token_patt = Regex::make('~\?[tm]{{token_id}}\?~S');
1068  $mappings = [];
1069  $lines = preg_split(Regex::$patt->newline, $this->string->raw);
1070  $tokens =& $this->tokens->store;
1071 
1072  // All mappings are calculated as delta values.
1073  $previous_dest_col = 0;
1074  $previous_src_file = 0;
1075  $previous_src_line = 0;
1076  $previous_src_col = 0;
1077 
1078  foreach ($lines as &$line_text) {
1079 
1080  $line_segments = [];
1081 
1082  while (preg_match($token_patt, $line_text, $m, PREG_OFFSET_CAPTURE)) {
1083 
1084  list($token, $dest_col) = $m[0];
1085  $token_type = $token[1];
1086 
1087  if (isset($tokens->{$token_type}[$token])) {
1088 
1089  list($src_file, $src_line, $src_col) = explode(',', $tokens->{$token_type}[$token]);
1090  $line_segments[] =
1091  Util::vlqEncode($dest_col - $previous_dest_col) .
1092  Util::vlqEncode($src_file - $previous_src_file) .
1093  Util::vlqEncode($src_line - $previous_src_line) .
1094  Util::vlqEncode($src_col - $previous_src_col);
1095 
1096  $previous_dest_col = $dest_col;
1097  $previous_src_file = $src_file;
1098  $previous_src_line = $src_line;
1099  $previous_src_col = $src_col;
1100  }
1101  $line_text = substr_replace($line_text, '', $dest_col, strlen($token));
1102  }
1103 
1104  $mappings[] = implode(',', $line_segments);
1105  }
1106 
1107  $this->string->raw = implode($this->newline, $lines);
1108  $this->sourceMap['mappings'] = implode(';', $mappings);
1109  }
1110 
1111 
1112  #############################
1113  # Decruft.
1114 
1115  protected function decruft()
1116  {
1117  return $this->string->pregReplaceHash([
1118 
1119  // Strip leading zeros on floats.
1120  '~([: \‍(,])(-?)0(\.\d+)~S' => '$1$2$3',
1121 
1122  // Strip unnecessary units on zero values for length types.
1123  '~([: \‍(,])\.?0' . Regex::$classes->length_unit . '~iS' => '${1}0',
1124 
1125  // Collapse zero lists.
1126  '~(\: *)(?:0 0 0|0 0 0 0) *([;}])~S' => '${1}0$2',
1127 
1128  // Collapse zero lists 2nd pass.
1129  '~(padding|margin|border-radius) ?(\: *)0 0 *([;}])~iS' => '${1}${2}0$3',
1130 
1131  // Dropping redundant trailing zeros on TRBL lists.
1132  '~(\: *)(-?(?:\d+)?\.?\d+[a-z]{1,4}) 0 0 0 *([;}])~iS' => '$1$2 0 0$3',
1133  '~(\: *)0 0 (-?(?:\d+)?\.?\d+[a-z]{1,4}) 0 *([;}])~iS' => '${1}0 0 $2$3',
1134 
1135  // Compress hex codes.
1136  Regex::$patt->cruftyHex => '#$1$2$3',
1137  ]);
1138  }
1139 
1140 
1141  #############################
1142  # Advanced minification.
1143 
1144  protected function minifyColors()
1145  {
1146  static $keywords_patt, $functions_patt;
1147 
1148  $minified_keywords = Color::getMinifyableKeywords();
1149 
1150  if (! $keywords_patt) {
1151  $keywords_patt = '~(?<![\w\.#-])(' . implode('|', array_keys($minified_keywords)) . ')(?![\w\.#\]-])~iS';
1152  $functions_patt = Regex::make('~{{ LB }}(rgb|hsl)\‍(([^\‍)]{5,})\‍)~iS');
1153  }
1154 
1155  $this->string->pregReplaceCallback($keywords_patt, function ($m) use ($minified_keywords) {
1156  return $minified_keywords[strtolower($m[0])];
1157  });
1158 
1159  $this->string->pregReplaceCallback($functions_patt, function ($m) {
1160  $args = Functions::parseArgs(trim($m[2]));
1161  if (stripos($m[1], 'hsl') === 0) {
1162  $args = Color::cssHslToRgb($args);
1163  }
1164  return Color::rgbToHex($args);
1165  });
1166  }
1167 }
if(! $user||! $user->canDelete()) $name
Definition: delete.php:22
$context
Definition: add.php:8
$source
__construct($user_options=[], $context=[])
Definition: Process.php:49
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
$value
Definition: generic.php:51
$args
Some servers don't allow PHP to check the rewrite, so try via AJAX.
The main class for compiling.
Definition: Process.php:8
string release
Definition: conf.py:62
if(parse_url(elgg_get_site_url(), PHP_URL_PATH) !=='/') if(file_exists(elgg_get_root_path() . 'robots.txt'))
Set robots.txt.
Definition: robots.php:10
$token
$classes
Definition: users.php:29