10 #[\AllowDynamicProperties] 22 $this->cacheData = [];
24 $this->fragments = [];
25 $this->references = [];
26 $this->absoluteImports = [];
27 $this->charset = null;
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;
47 $this->aliases =
$config->aliases;
50 $this->options =
new Options($user_options,
$config->options);
53 $context += [
'type' =>
'filter',
'data' =>
''];
57 $this->options->global_vars =
$config->vars;
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');
66 $useContextOption = ! empty($this->options->context)
67 && (php_sapi_name() ===
'cli' ||
$context[
'type'] ===
'filter');
71 $this->input->raw = $file;
72 if (! ($inputFile = Util::resolveUserPath($file, null, $this->docRoot))) {
73 throw new \Exception(
'Input file \'' . basename($file) .
'\' not found.
'); 75 $inputDir = $useContextOption 76 ? $this->options->context 77 : dirname($inputFile); 78 $this->resolveContext($inputDir, $inputFile); 80 elseif ($context['type
'] === 'filter') { 81 if ($useContextOption) { 82 $this->resolveContext($this->options->context); 85 $this->resolveContext(); 87 $this->input->string = $context['data
']; 91 public function release() 101 $this->selectorAliases 105 public function resolveContext($input_dir = null, $input_file = null) 108 $this->input->path = $input_file; 109 $this->input->filename = basename($input_file); 110 $this->input->mtime = filemtime($input_file); 113 $this->input->path = null; 114 $this->input->filename = null; 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)); 123 $context_resolved = true; 125 $output_dir = $this->output->dir; 127 if (! file_exists($output_dir)) { 128 warning("Output directory '$output_dir
' doesn't exist.
"); 129 $context_resolved = false; 131 elseif (! is_writable($output_dir)) { 133 debug('Attempting to change permissions.'); 135 if (! @chmod($output_dir, 0755)) { 136 warning("Output directory
'$output_dir' is unwritable.
"); 137 $context_resolved = false; 140 debug('Permissions updated.'); 147 return $context_resolved; 151 ############################# 154 protected function getBoilerplate() 157 $boilerplateOption = $this->options->boilerplate; 159 if ($boilerplateOption === true) { 160 $file = Crush::$dir . '/boilerplate.txt'; 162 elseif (is_string($boilerplateOption)) { 163 if (file_exists($boilerplateOption)) { 164 $file = $boilerplateOption; 168 // Return an empty string if no file is found. 173 $boilerplate = file_get_contents($file); 175 // Substitute any tags 176 if (preg_match_all('~\{\{([^}]+)\}\}~', $boilerplate, $boilerplateMatches)) { 178 // Command line arguments (if any). 179 $commandArgs = 'n/a'; 180 if (isset($_SERVER['argv'])) { 181 $argv = $_SERVER['argv']; 183 $commandArgs = 'csscrush ' . implode(' ', $argv); 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(); 194 'compile_time' => function () { 195 $now = microtime(true) - Crush::$process->stat['compile_start_time']; 196 return round($now, 4) . ' seconds'; 200 foreach (array_keys($boilerplateMatches[0]) as $index) { 201 $tagName = trim($boilerplateMatches[1][$index]); 203 if (isset($tags[$tagName])) { 204 $replacement = is_callable($tags[$tagName]) ? $tags[$tagName]() : $tags[$tagName]; 206 $replacements[] = $replacement; 208 $boilerplate = str_replace($boilerplateMatches[0], $replacements, $boilerplate); 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); 221 ############################# 224 protected function resolveSelectorAliases() 226 $this->string->pregReplaceCallback( 227 Regex::make('~@selector(?:-(?<type>alias|splat))? +\:?(?<name>{{ident}}) +(?<handler>[^;]+) *;~iS'), 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); 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'); 243 public function addSelectorAlias($name, $handler, $type = 'alias') 245 if ($type != 'callback') { 246 $handler = $this->tokens->capture($handler, 's'); 248 $this->selectorAliases[$name] = new SelectorAlias($handler, $type); 252 ############################# 255 protected function filterAliases() 257 // If a vendor target is given, we prune the aliases array. 258 $vendors = $this->options->vendor_target; 260 // Default vendor argument, so use all aliases as normal. 261 if ('all' === $vendors) { 266 // For expicit 'none' argument turn off aliases. 267 if ('none' === $vendors) { 268 $this->aliases = Crush::$config->bareAliases; 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, '-'); 278 $vendor_patt = '~^\-(' . implode('|', $vendor_names) . ')\-~i'; 281 // Loop the aliases array, filter down to the target vendor. 282 foreach ($this->aliases as $section => $group_array) { 284 // Declarations aliases. 285 if ($section === 'declarations') { 287 foreach ($group_array as $property => $values) { 288 foreach ($values as $value => $prefix_values) { 289 foreach ($prefix_values as $index => $declaration) { 291 if (in_array($declaration[2], $vendor_names)) { 295 // Unset uneeded aliases. 296 unset($this->aliases[$section][$property][$value][$index]); 298 if (empty($this->aliases[$section][$property][$value])) { 299 unset($this->aliases[$section][$property][$value]); 301 if (empty($this->aliases[$section][$property])) { 302 unset($this->aliases[$section][$property]); 309 // Function group aliases. 310 elseif ($section === 'function_groups') { 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]); 323 foreach ($group_array as $alias_keyword => $prefix_array) { 325 // Skip over pointers to function groups. 326 if ($prefix_array[0] === '.') { 332 foreach ($prefix_array as $prefix) { 333 if (preg_match($vendor_patt, $prefix)) { 338 // Prune the whole alias keyword if there is no result. 339 if (empty($result)) { 340 unset($this->aliases[$section][$alias_keyword]); 343 $this->aliases[$section][$alias_keyword] = $result; 351 ############################# 354 protected function filterPlugins() 356 $this->plugins = array_unique($this->options->plugins); 358 foreach ($this->plugins as $plugin) { 359 Crush::enablePlugin($plugin); 364 ############################# 367 protected function captureVars() 369 Crush::$process->vars = Crush::$process->string->captureDirectives(['set', 'define'], [ 371 'lowercase_keys' => false, 372 ]) + Crush::$process->vars; 374 // For convenience adding a runtime variable for cache busting linked resources. 375 $this->vars['timestamp'] = (int) $this->stat['compile_start_time']; 377 // In-file variables override global variables. 378 $this->vars += Crush::$config->vars; 380 // Runtime variables override in-file variables. 381 if (! empty($this->options->vars)) { 382 $this->vars = $this->options->vars + $this->vars; 385 // Place variables referenced inside variables. 386 foreach ($this->vars as &$value) { 387 $this->placeVars($value); 391 protected function placeAllVars() 393 $this->placeVars($this->string->raw); 395 $rawTokens =& $this->tokens->store; 397 // Repeat above steps for variables embedded in string tokens. 398 foreach ($rawTokens->s as $label => &$value) { 399 $this->placeVars($value); 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. 411 protected function placeVars(&$value) 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]]; 422 return isset($args[1]) ? $args[1] : ''; 427 // Variables with no default value. 428 $value = preg_replace_callback($varFunctionSimple, function ($m) { 430 if (isset(Crush::$process->vars[$varName])) { 431 return Crush::$process->vars[$varName]; 433 }, $value ?? '', -1, $varsPlaced); 435 // Variables with default value. 436 if (strpos($value, '$(') !== false) { 438 // Assume at least one replace. 441 // Variables may be nested so need to apply full function parsing. 442 $value = $varFunction->apply($value); 445 // If we know replacements have been made we may want to update $value. e.g URL tokens. 449 ############################# 452 protected function resolveLoops() 454 $LOOP_VAR_PATT = '~\#\( \s* (?<arg>[a-zA-Z][\.a-zA-Z0-9-_]*) \s* \)~x'; 455 $LOOP_PATT = Regex::make('~ 457 @for \s+ (?<var>{{ident}}) \s+ in \s+ (?<list>[^{]+) 462 $apply_scope = function ($str, $context) use ($LOOP_VAR_PATT, $LOOP_PATT) { 463 // Need to temporarily hide child block 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; 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]); 478 return isset($context[$arg]) ? $context[$arg] : ''; 481 return str_replace(array_keys($child_scopes), array_values($child_scopes), $str); 484 $resolve_list = function ($list) { 485 // Resolve the list of items for iteration. 486 // Either a generator function or a plain list. 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']); 495 $items = range(...$args); 500 $items = Util::splitDelimList($list); 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, 524 $this->string->pregReplaceCallback($LOOP_PATT, function ($m) use ($unroll) { 525 return Template::tokenize($unroll(Template::unTokenize($m[0]))); 529 ############################# 532 protected function resolveIfDefines() 534 $ifdefinePatt = Regex::make('~@if(?:set|define) \s+ (?<negate>not \s+)? (?<name>{{ ident }}) \s* {{ parens }}? \s* \{~ixS'); 536 $matches = $this->string->matchAll($ifdefinePatt); 538 while ($match = array_pop($matches)) { 540 $curlyMatch = new BalancedMatch($this->string, $match[0][1]); 542 if (! $curlyMatch->match) { 546 $negate = $match['negate'][1] != -1; 547 $nameDefined = isset($this->vars[$match['name'][0]]); 549 $valueDefined = isset($match['parens_content'][0]); 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; 558 ( $valueDefined && !$negate && $valueMatch ) 559 || ( $valueDefined && $negate && !$valueMatch ) 560 || ( !$valueDefined && !$negate && $nameDefined ) 561 || ( !$valueDefined && $negate && !$nameDefined ) 563 $curlyMatch->unWrap(); 566 $curlyMatch->replace(''); 572 ############################# 575 protected function captureMixins() 577 $this->string->pregReplaceCallback(Regex::$patt->mixin, function ($m) { 578 Crush::$process->mixins[$m['name']] = new Mixin($m['block_content']); 583 ############################# 586 protected function resolveFragments() 588 $fragments =& Crush::$process->fragments; 590 $this->string->pregReplaceCallback(Regex::$patt->fragmentCapture, function ($m) use (&$fragments) { 591 $fragments[$m['name']] = new Fragment( 593 ['name' => strtolower($m['name'])] 598 $this->string->pregReplaceCallback(Regex::$patt->fragmentInvoke, function ($m) use (&$fragments) { 599 $fragment = isset($fragments[$m['name']]) ? $fragments[$m['name']] : null; 602 if (isset($m['parens'])) { 603 $args = Functions::parseArgs($m['parens_content']); 605 return $fragment($args); 612 ############################# 615 public function captureRules() 617 $tokens = $this->tokens; 619 $rulePatt = Regex::make('~ 620 (?<trace_token> {{ t_token }}) 626 $rulesAndMediaPatt = Regex::make('~{{ r_token }}|@media[^\{]+{{ block }}~iS'); 628 $count = preg_match_all(Regex::$patt->t_token, $this->string->raw, $traceMatches, PREG_OFFSET_CAPTURE); 631 $traceOffset = $traceMatches[0][$count][1]; 633 preg_match($rulePatt, $this->string->raw, $ruleMatch, PREG_UNMATCHED_AS_NULL, $traceOffset); 635 $selector = trim($ruleMatch['selector']); 636 $block = trim($ruleMatch['block_content']); 639 // If rules are nested inside we set their parent property. 640 if (preg_match_all(Regex::$patt->r_token, $block, $childMatches)) { 642 $block = preg_replace_callback($rulesAndMediaPatt, function ($m) use (&$replace) { 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; 656 $rule = new Rule($selector, $block, $ruleMatch['trace_token']); 659 $replace = $tokens->add($rule, 'r', $rule->label) . $replace; 661 $this->string->splice($replace, $traceOffset, strlen($ruleMatch[0])); 664 // Flip, since we just captured rules in reverse order. 665 $tokens->store->r = array_reverse($tokens->store->r); 667 foreach ($tokens->store->r as $rule) { 669 $rule->selectors->merge(array_keys($rule->parent->selectors->store)); 673 // Cleanup unusable rules. 674 $this->string->pregReplaceCallback(Regex::$patt->r_token, function ($m) use ($tokens) { 676 $rule = $tokens->store->r[$ruleToken]; 677 if (empty($rule->declarations->store) && ! $rule->extendArgs) { 678 unset($tokens->store->r[$ruleToken]); 685 protected function processRules() 687 // Create table of name/selector to rule references. 688 $namedReferences = []; 690 $previousRule = null; 691 foreach ($this->tokens->store->r as $rule) { 693 $namedReferences[$rule->name] = $rule; 695 foreach ($rule->selectors as $selector) { 696 $this->references[$selector->readableValue] = $rule; 699 $rule->previous = $previousRule; 700 $previousRule->next = $rule; 702 $previousRule = $rule; 705 // Explicit named references take precedence. 706 $this->references = $namedReferences + $this->references; 708 foreach ($this->tokens->store->r as $rule) { 710 $rule->declarations->flatten(); 711 $rule->declarations->process(); 713 $this->emit('rule_prealias', $rule); 715 $rule->declarations->aliasProperties($rule->vendorContext); 716 $rule->declarations->aliasFunctions($rule->vendorContext); 717 $rule->declarations->aliasDeclarations($rule->vendorContext); 719 $this->emit('rule_postalias', $rule); 721 $rule->selectors->expand(); 722 $rule->applyExtendables(); 724 $this->emit('rule_postprocess', $rule); 729 ############################# 732 protected function aliasAtRules() 734 if (empty($this->aliases['at-rules'])) { 739 $aliases = $this->aliases['at-rules']; 740 $regex = Regex::$patt; 742 foreach ($aliases as $at_rule => $at_rule_aliases) { 744 $matches = $this->string->matchAll("~@$at_rule
" . '[\s{]~i'); 746 // Find at-rules that we want to alias. 747 while ($match = array_pop($matches)) { 749 $curly_match = new BalancedMatch($this->string, $match[0][1]); 751 if (! $curly_match->match) { 752 // Couldn't match the block. 756 // Build up string with aliased blocks for splicing. 757 $original_block = $curly_match->whole(); 760 foreach ($at_rule_aliases as $alias) { 762 // Copy original block, replacing at-rule with alias name. 763 $copy_block = str_replace("@$at_rule
", "@$alias
", $original_block); 765 // Aliases are nearly always prefixed, capture the current vendor name. 766 preg_match($regex->vendorPrefix, $alias, $vendor); 768 $vendor = $vendor ? $vendor[1] : null; 771 if (preg_match_all($regex->r_token, $copy_block, $copy_matches)) { 776 foreach ($copy_matches[0] as $rule_label) { 778 // Clone the matched rule. 779 $originals[] = $rule_label; 780 $clone_rule = clone $this->tokens->get($rule_label); 782 $clone_rule->vendorContext = $vendor; 785 $replacements[] = $this->tokens->add($clone_rule); 788 // Finally replace the original labels with the cloned rule labels. 789 $copy_block = str_replace($originals, $replacements, $copy_block); 792 // Add the copied block to the stack. 793 $new_blocks[] = $copy_block; 796 // The original version is always pushed last in the list. 797 $new_blocks[] = $original_block; 799 // Splice in the blocks. 800 $curly_match->replace(implode("\n
", $new_blocks)); 806 ############################# 809 protected function collate() 811 $options = $this->options; 812 $minify = $options->minify; 813 $EOL = $this->newline; 815 // Formatting replacements. 816 // Strip newlines added during processing. 817 $regex_replacements = []; 818 $regex_replacements['~\n+~'] = ''; 821 // Strip whitespace around colons used in @-rule arguments. 822 $regex_replacements['~ ?\: ?~'] = ':'; 826 $regex_replacements['~}~'] = "$0$EOL$EOL
"; 827 $regex_replacements['~([^\s])\{~'] = "$1 {
"; 828 $regex_replacements['~ ?(@[^{]+\{)~'] = "$1$EOL
"; 829 $regex_replacements['~ ?(@[^;]+\;)~'] = "$1$EOL
"; 831 // Trim leading spaces on @-rules and some tokens. 832 $regex_replacements[Regex::make('~ +([@}]|\?[rc]{{token_id}}\?)~S')] = "$1
"; 834 // Additional newline between adjacent rules and comments. 835 $regex_replacements[Regex::make('~({{r_token}}) (\s*) ({{c_token}})~xS')] = "$1$EOL$2$3
"; 838 // Apply all formatting replacements. 839 $this->string->pregReplaceHash($regex_replacements)->lTrim(); 841 $this->string->restore('r'); 843 // Record stats then drop rule objects to reclaim memory. 844 Crush::runStat('selector_count', 'rule_count', 'vars'); 845 $this->tokens->store->r = []; 847 // If specified, apply advanced minification. 848 if (is_array($minify)) { 849 if (in_array('colors', $minify)) { 850 $this->minifyColors(); 857 // Add newlines after comments. 858 foreach ($this->tokens->store->c as $token => &$comment) { 862 // Insert comments and do final whitespace cleanup. 870 $urls = $this->tokens->store->u; 873 $link = Util::getLinkBetweenPaths($this->output->dir, $this->input->dir); 874 $make_urls_absolute = $options->rewrite_import_urls === 'absolute'; 876 foreach ($urls as $token => $url) { 878 if ($url->isRelative && ! $url->noRewrite) { 879 if ($make_urls_absolute) { 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); 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; 896 $this->string->prepend($absoluteImports); 899 if ($options->boilerplate) { 900 $this->string->prepend($this->getBoilerplate()); 903 if ($this->charset) { 904 $this->string->prepend("@charset \
"$this->charset\";$EOL");
907 $this->string->restore([
'u',
's']);
909 if ($this->generateMap) {
910 $this->generateSourceMap();
914 private $iniOriginal = [];
918 'pcre.backtrack_limit' => 1000000,
920 'memory_limit' =>
'128M',
923 if (
$name ===
'memory_limit' && $this->returnBytes(ini_get(
$name)) > $this->returnBytes(
$value)) {
929 $this->filterPlugins();
930 $this->filterAliases();
932 $this->functions->setPattern(
true);
934 $this->stat[
'compile_start_time'] =
microtime(
true);
937 private function returnBytes(
string $value)
939 $value =
trim($value);
940 $last = strtolower($value[
strlen($value) - 1]);
941 $value = (float) $value;
960 Crush::runStat(
'compile_time');
962 foreach ($this->iniOriginal as
$name => $value) {
963 ini_set(
$name, $value);
971 $importer =
new Importer($this);
975 $this->emit(
'capture_phase0', $this);
977 $this->captureVars();
979 $this->resolveIfDefines();
981 $this->resolveLoops();
983 $this->placeAllVars();
986 $this->emit(
'capture_phase1', $this);
988 $this->resolveSelectorAliases();
990 $this->captureMixins();
992 $this->resolveFragments();
995 $this->emit(
'capture_phase2', $this);
997 $this->captureRules();
1001 $this->
string->pregReplaceCallback(
'~@media\s+(?<media_list>[^{]+)\{~i',
function ($m) use (&$process) {
1002 return "@media {$process->functions->apply($m['media_list'])}{";
1005 $this->aliasAtRules();
1007 $this->processRules();
1011 $this->postCompile();
1013 return $this->string;
1017 ############################# 1022 $this->sourceMap = [
1024 'file' => $this->output->filename,
1027 foreach ($this->sources as
$source) {
1028 $this->sourceMap[
'sources'][] = Util::getLinkBetweenPaths($this->output->dir, $source,
false);
1031 $token_patt = Regex::make(
'~\?[tm]{{token_id}}\?~S');
1033 $lines = preg_split(Regex::$patt->newline, $this->string->raw);
1034 $tokens =& $this->tokens->store;
1037 $previous_dest_col = 0;
1038 $previous_src_file = 0;
1039 $previous_src_line = 0;
1040 $previous_src_col = 0;
1042 foreach ($lines as &$line_text) {
1044 $line_segments = [];
1046 while (
preg_match($token_patt, $line_text, $m, PREG_OFFSET_CAPTURE)) {
1048 list(
$token, $dest_col) = $m[0];
1051 if (isset($tokens->{$token_type}[
$token])) {
1053 list($src_file, $src_line, $src_col) = explode(
',', $tokens->{$token_type}[
$token]);
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);
1060 $previous_dest_col = $dest_col;
1061 $previous_src_file = $src_file;
1062 $previous_src_line = $src_line;
1063 $previous_src_col = $src_col;
1068 $mappings[] =
implode(
',', $line_segments);
1071 $this->
string->raw =
implode($this->newline, $lines);
1072 $this->sourceMap[
'mappings'] =
implode(
';', $mappings);
1076 ############################# 1081 return $this->
string->pregReplaceHash([
1084 '~([: \(,])(-?)0(\.\d+)~S' =>
'$1$2$3',
1090 '~(\: *)(?:0 0 0|0 0 0 0) *([;}])~S' =>
'${1}0$2',
1093 '~(padding|margin|border-radius) ?(\: *)0 0 *([;}])~iS' =>
'${1}${2}0$3',
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',
1100 Regex::$patt->cruftyHex =>
'#$1$2$3',
1105 ############################# 1106 # Advanced minification. 1110 static $keywords_patt, $functions_patt;
1112 $minified_keywords = Color::getMinifyableKeywords();
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');
1119 $this->
string->pregReplaceCallback($keywords_patt,
function ($m) use ($minified_keywords) {
1120 return $minified_keywords[strtolower($m[0])];
1123 $this->
string->pregReplaceCallback($functions_patt,
function ($m) {
1124 $args = Functions::parseArgs(
trim($m[2]));
1125 if (stripos($m[1],
'hsl') === 0) {
1128 return Color::rgbToHex(
$args);
if(!$user||!$user->canDelete()) $name
$args
Some servers don't allow PHP to check the rewrite, so try via AJAX.
Balanced bracket matching on string objects.
$config
Advanced site settings, debugging section.
function filter(array, term)
__construct($user_options=[], $context=[])