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