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)
960 Crush::runStat(
'compile_time');
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;
1065 $line_text = substr_replace($line_text,
'', $dest_col, strlen(
$token));
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
__construct($user_options=[], $context=[])
$config
Advanced site settings, debugging section.
$args
Some servers don't allow PHP to check the rewrite, so try via AJAX.
Balanced bracket matching on string objects.
if(parse_url(elgg_get_site_url(), PHP_URL_PATH) !=='/') if(file_exists(elgg_get_root_path() . 'robots.txt'))
Set robots.txt.