/lib/CssCrush/Process.php
PHP | 1102 lines | 854 code | 173 blank | 75 comment | 65 complexity | a44e2c9b50a082c30f97ff5fb617aa4e MD5 | raw file
- <?php
- /**
- *
- * The main class for compiling.
- *
- */
- namespace CssCrush;
- class Process
- {
- use EventEmitter;
- public function __construct($user_options = [], $context = [])
- {
- $config = Crush::$config;
- Crush::loadAssets();
- // Initialize properties.
- $this->cacheData = [];
- $this->mixins = [];
- $this->fragments = [];
- $this->references = [];
- $this->absoluteImports = [];
- $this->charset = null;
- $this->sources = [];
- $this->vars = [];
- $this->plugins = [];
- $this->misc = new \stdClass();
- $this->input = new \stdClass();
- $this->output = new \stdClass();
- $this->tokens = new Tokens();
- $this->functions = new Functions();
- $this->sourceMap = null;
- $this->selectorAliases = [];
- $this->selectorAliasesPatt = null;
- $this->io = new Crush::$config->io($this);
- $this->errors = [];
- $this->warnings = [];
- $this->debugLog = [];
- $this->stat = [];
- // Copy config values.
- $this->aliases = $config->aliases;
- // Options.
- $this->options = new Options($user_options, $config->options);
- // Context options.
- $context += ['type' => 'filter', 'data' => ''];
- $this->ioContext = $context['type'];
- // Keep track of global vars to maintain cache integrity.
- $this->options->global_vars = $config->vars;
- // Shortcut commonly used options to avoid __get() overhead.
- $this->docRoot = isset($this->options->doc_root) ? $this->options->doc_root : $config->docRoot;
- $this->generateMap = $this->ioContext === 'file' && $this->options->__get('source_map');
- $this->ruleFormatter = $this->options->__get('formatter');
- $this->minifyOutput = $this->options->__get('minify');
- $this->newline = $this->options->__get('newlines');
- if ($context['type'] === 'file') {
- $file = $context['data'];
- $this->input->raw = $file;
- if (! ($inputFile = Util::resolveUserPath($file, null, $this->docRoot))) {
- throw new \Exception('Input file \'' . basename($file) . '\' not found.');
- }
- $this->resolveContext(dirname($inputFile), $inputFile);
- }
- elseif ($context['type'] === 'filter') {
- if (! empty($this->options->context)) {
- $this->resolveContext($this->options->context);
- }
- else {
- $this->resolveContext();
- }
- $this->input->string = $context['data'];
- }
- }
- public function release()
- {
- unset(
- $this->tokens,
- $this->mixins,
- $this->references,
- $this->cacheData,
- $this->misc,
- $this->plugins,
- $this->aliases,
- $this->selectorAliases
- );
- }
- public function resolveContext($input_dir = null, $input_file = null)
- {
- if ($input_file) {
- $this->input->path = $input_file;
- $this->input->filename = basename($input_file);
- $this->input->mtime = filemtime($input_file);
- }
- else {
- $this->input->path = null;
- $this->input->filename = null;
- }
- $this->input->dir = $input_dir ?: $this->docRoot;
- $this->input->dirUrl = substr($input_dir, strlen($this->docRoot));
- $this->output->dir = $this->io->getOutputDir();
- $this->output->filename = $this->io->getOutputFileName();
- $this->output->dirUrl = substr($this->output->dir, strlen($this->docRoot));
- $context_resolved = true;
- if ($input_file) {
- $output_dir = $this->output->dir;
- if (! file_exists($output_dir)) {
- warning("Output directory '$output_dir' doesn't exist.");
- $context_resolved = false;
- }
- elseif (! is_writable($output_dir)) {
- debug('Attempting to change permissions.');
- if (! @chmod($output_dir, 0755)) {
- warning("Output directory '$output_dir' is unwritable.");
- $context_resolved = false;
- }
- else {
- debug('Permissions updated.');
- }
- }
- }
- $this->io->init();
- return $context_resolved;
- }
- #############################
- # Boilerplate.
- protected function getBoilerplate()
- {
- $file = false;
- $boilerplateOption = $this->options->boilerplate;
- if ($boilerplateOption === true) {
- $file = Crush::$dir . '/boilerplate.txt';
- }
- elseif (is_string($boilerplateOption)) {
- if (file_exists($boilerplateOption)) {
- $file = $boilerplateOption;
- }
- }
- // Return an empty string if no file is found.
- if (! $file) {
- return '';
- }
- $boilerplate = file_get_contents($file);
- // Substitute any tags
- if (preg_match_all('~\{\{([^}]+)\}\}~', $boilerplate, $boilerplateMatches)) {
- // Command line arguments (if any).
- $commandArgs = 'n/a';
- if (isset($_SERVER['argv'])) {
- $argv = $_SERVER['argv'];
- array_shift($argv);
- $commandArgs = 'csscrush ' . implode(' ', $argv);
- }
- $tags = [
- 'datetime' => @date('Y-m-d H:i:s O'),
- 'year' => @date('Y'),
- 'command' => $commandArgs,
- 'plugins' => implode(',', $this->plugins),
- 'version' => function () {
- return Version::detect();
- },
- 'compile_time' => function () {
- $now = microtime(true) - Crush::$process->stat['compile_start_time'];
- return round($now, 4) . ' seconds';
- },
- ];
- foreach (array_keys($boilerplateMatches[0]) as $index) {
- $tagName = trim($boilerplateMatches[1][$index]);
- $replacement = '?';
- if (isset($tags[$tagName])) {
- $replacement = is_callable($tags[$tagName]) ? $tags[$tagName]() : $tags[$tagName];
- }
- $replacements[] = $replacement;
- }
- $boilerplate = str_replace($boilerplateMatches[0], $replacements, $boilerplate);
- }
- // Pretty print.
- $EOL = $this->newline;
- $boilerplate = preg_split('~[\t]*'. Regex::$classes->newline . '[\t]*~', trim($boilerplate));
- $boilerplate = array_map('trim', $boilerplate);
- $boilerplate = "$EOL * " . implode("$EOL * ", $boilerplate);
- return "/*$boilerplate$EOL */$EOL";
- }
- #############################
- # Selector aliases.
- protected function resolveSelectorAliases()
- {
- $this->string->pregReplaceCallback(
- Regex::make('~@selector(?:-(?<type>alias|splat))? +\:?(?<name>{{ident}}) +(?<handler>[^;]+) *;~iS'),
- function ($m) {
- $name = strtolower($m['name']);
- $type = ! empty($m['type']) ? strtolower($m['type']) : 'alias';
- $handler = Util::stripCommentTokens($m['handler']);
- Crush::$process->selectorAliases[$name] = new SelectorAlias($handler, $type);
- });
- // Create the selector aliases pattern and store it.
- if ($this->selectorAliases) {
- $names = implode('|', array_keys($this->selectorAliases));
- $this->selectorAliasesPatt
- = Regex::make('~\:(' . $names . '){{RB}}(\()?~iS');
- }
- }
- public function addSelectorAlias($name, $handler, $type = 'alias')
- {
- if ($type != 'callback') {
- $handler = $this->tokens->capture($handler, 's');
- }
- $this->selectorAliases[$name] = new SelectorAlias($handler, $type);
- }
- #############################
- # Aliases.
- protected function filterAliases()
- {
- // If a vendor target is given, we prune the aliases array.
- $vendors = $this->options->vendor_target;
- // Default vendor argument, so use all aliases as normal.
- if ('all' === $vendors) {
- return;
- }
- // For expicit 'none' argument turn off aliases.
- if ('none' === $vendors) {
- $this->aliases = Crush::$config->bareAliases;
- return;
- }
- // Normalize vendor names and create regex patt.
- $vendor_names = (array) $vendors;
- foreach ($vendor_names as &$vendor_name) {
- $vendor_name = trim($vendor_name, '-');
- }
- $vendor_patt = '~^\-(' . implode($vendor_names, '|') . ')\-~i';
- // Loop the aliases array, filter down to the target vendor.
- foreach ($this->aliases as $section => $group_array) {
- // Declarations aliases.
- if ($section === 'declarations') {
- foreach ($group_array as $property => $values) {
- foreach ($values as $value => $prefix_values) {
- foreach ($prefix_values as $index => $declaration) {
- if (in_array($declaration[2], $vendor_names)) {
- continue;
- }
- // Unset uneeded aliases.
- unset($this->aliases[$section][$property][$value][$index]);
- if (empty($this->aliases[$section][$property][$value])) {
- unset($this->aliases[$section][$property][$value]);
- }
- if (empty($this->aliases[$section][$property])) {
- unset($this->aliases[$section][$property]);
- }
- }
- }
- }
- }
- // Function group aliases.
- elseif ($section === 'function_groups') {
- foreach ($group_array as $func_group => $vendors) {
- foreach (array_keys($vendors) as $vendor) {
- if (! in_array($vendor, $vendor_names)) {
- unset($this->aliases['function_groups'][$func_group][$vendor]);
- }
- }
- }
- }
- // Everything else.
- else {
- foreach ($group_array as $alias_keyword => $prefix_array) {
- // Skip over pointers to function groups.
- if ($prefix_array[0] === '.') {
- continue;
- }
- $result = [];
- foreach ($prefix_array as $prefix) {
- if (preg_match($vendor_patt, $prefix)) {
- $result[] = $prefix;
- }
- }
- // Prune the whole alias keyword if there is no result.
- if (empty($result)) {
- unset($this->aliases[$section][$alias_keyword]);
- }
- else {
- $this->aliases[$section][$alias_keyword] = $result;
- }
- }
- }
- }
- }
- #############################
- # Plugins.
- protected function filterPlugins()
- {
- $this->plugins = array_unique($this->options->plugins);
- foreach ($this->plugins as $plugin) {
- Crush::enablePlugin($plugin);
- }
- }
- #############################
- # Variables.
- protected function captureVars()
- {
- Crush::$process->vars = Crush::$process->string->captureDirectives(['set', 'define'], [
- 'singles' => true,
- 'lowercase_keys' => false,
- ]) + Crush::$process->vars;
- // For convenience adding a runtime variable for cache busting linked resources.
- $this->vars['timestamp'] = (int) $this->stat['compile_start_time'];
- // In-file variables override global variables.
- $this->vars += Crush::$config->vars;
- // Runtime variables override in-file variables.
- if (! empty($this->options->vars)) {
- $this->vars = $this->options->vars + $this->vars;
- }
- // Place variables referenced inside variables.
- foreach ($this->vars as &$value) {
- $this->placeVars($value);
- }
- }
- protected function placeAllVars()
- {
- $this->placeVars($this->string->raw);
- $rawTokens =& $this->tokens->store;
- // Repeat above steps for variables embedded in string tokens.
- foreach ($rawTokens->s as $label => &$value) {
- $this->placeVars($value);
- }
- // Repeat above steps for variables embedded in URL tokens.
- foreach ($rawTokens->u as $label => $url) {
- if (! $url->isData && $this->placeVars($url->value)) {
- // Re-evaluate $url->value if anything has been interpolated.
- $url->evaluate();
- }
- }
- }
- protected function placeVars(&$value)
- {
- static $varFunction, $varFunctionSimple;
- if (! $varFunction) {
- $varFunctionSimple = Regex::make('~\$\( \s* ({{ ident }}) \s* \)~xS');
- $varFunction = new Functions(['$' => function ($rawArgs) {
- $args = Functions::parseArgsSimple($rawArgs);
- if (isset(Crush::$process->vars[$args[0]])) {
- return Crush::$process->vars[$args[0]];
- }
- else {
- return isset($args[1]) ? $args[1] : '';
- }
- }]);
- }
- // Variables with no default value.
- $value = preg_replace_callback($varFunctionSimple, function ($m) {
- $varName = $m[1];
- if (isset(Crush::$process->vars[$varName])) {
- return Crush::$process->vars[$varName];
- }
- }, $value, -1, $varsPlaced);
- // Variables with default value.
- if (strpos($value, '$(') !== false) {
- // Assume at least one replace.
- $varsPlaced = true;
- // Variables may be nested so need to apply full function parsing.
- $value = $varFunction->apply($value);
- }
- // If we know replacements have been made we may want to update $value. e.g URL tokens.
- return $varsPlaced;
- }
- #############################
- # @for..in blocks.
- protected function resolveLoops()
- {
- $LOOP_VAR_PATT = '~\#\( \s* (?<arg>[a-zA-Z][\.a-zA-Z0-9-_]*) \s* \)~x';
- $LOOP_PATT = Regex::make('~
- (?<expression>
- @for \s+ (?<var>{{ident}}) \s+ in \s+ (?<list>[^{]+)
- ) \s*
- {{ block }}
- ~xiS');
- $apply_scope = function ($str, $context) use ($LOOP_VAR_PATT, $LOOP_PATT) {
- // Need to temporarily hide child block scopes.
- $child_scopes = [];
- $str = preg_replace_callback($LOOP_PATT, function ($m) use (&$child_scopes) {
- $label = '?B' . count($child_scopes) . '?';
- $child_scopes[$label] = $m['block'];
- return $m['expression'] . $label;
- }, $str);
- $str = preg_replace_callback($LOOP_VAR_PATT, function ($m) use ($context) {
- // Normalize casing of built-in loop variables.
- // User variables are case-sensitive.
- $arg = preg_replace_callback('~^loop\.(parent\.)?counter0?$~i', function ($m) {
- return strtolower($m[0]);
- }, $m['arg']);
- return isset($context[$arg]) ? $context[$arg] : '';
- }, $str);
- return str_replace(array_keys($child_scopes), array_values($child_scopes), $str);
- };
- $resolve_list = function ($list) {
- // Resolve the list of items for iteration.
- // Either a generator function or a plain list.
- $items = [];
- $this->placeVars($list);
- $list = $this->functions->apply($list);
- if (preg_match(Regex::make('~(?<func>range){{ parens }}~ix'), $list, $m)) {
- $func = strtolower($m['func']);
- $args = Functions::parseArgs($m['parens_content']);
- switch ($func) {
- case 'range':
- $items = range(...$args);
- break;
- }
- }
- else {
- $items = Util::splitDelimList($list);
- }
- return $items;
- };
- $unroll = function ($str, $context = []) use (&$unroll, $LOOP_PATT, $apply_scope, $resolve_list) {
- $str = $apply_scope($str, $context);
- while (preg_match($LOOP_PATT, $str, $m, PREG_OFFSET_CAPTURE)) {
- $str = substr_replace($str, '', $m[0][1], strlen($m[0][0]));
- $context['loop.parent.counter'] = isset($context['loop.counter']) ? $context['loop.counter'] : -1;
- $context['loop.parent.counter0'] = isset($context['loop.counter0']) ? $context['loop.counter0'] : -1;
- foreach ($resolve_list($m['list'][0]) as $index => $value) {
- $str .= $unroll($m['block_content'][0], [
- $m['var'][0] => $value,
- 'loop.counter' => $index + 1,
- 'loop.counter0' => $index,
- ] + $context);
- }
- }
- return $str;
- };
- $this->string->pregReplaceCallback($LOOP_PATT, function ($m) use ($unroll) {
- return Template::tokenize($unroll(Template::unTokenize($m[0])));
- });
- }
- #############################
- # @ifdefine blocks.
- protected function resolveIfDefines()
- {
- $ifdefinePatt = Regex::make('~@if(?:set|define) \s+ (?<negate>not \s+)? (?<name>{{ ident }}) \s* {{ parens }}? \s* \{~ixS');
- $matches = $this->string->matchAll($ifdefinePatt);
- while ($match = array_pop($matches)) {
- $curlyMatch = new BalancedMatch($this->string, $match[0][1]);
- if (! $curlyMatch->match) {
- continue;
- }
- $negate = $match['negate'][1] != -1;
- $nameDefined = isset($this->vars[$match['name'][0]]);
- $valueDefined = isset($match['parens_content'][0]);
- $valueMatch = false;
- if ($nameDefined && $valueDefined) {
- $testValue = Util::rawValue(trim($match['parens_content'][0]));
- $varValue = Util::rawValue($this->vars[$match['name'][0]]);
- $valueMatch = $varValue == $testValue;
- }
- if (
- ( $valueDefined && !$negate && $valueMatch )
- || ( $valueDefined && $negate && !$valueMatch )
- || ( !$valueDefined && !$negate && $nameDefined )
- || ( !$valueDefined && $negate && !$nameDefined )
- ) {
- $curlyMatch->unWrap();
- }
- else {
- $curlyMatch->replace('');
- }
- }
- }
- #############################
- # Mixins.
- protected function captureMixins()
- {
- $this->string->pregReplaceCallback(Regex::$patt->mixin, function ($m) {
- Crush::$process->mixins[$m['name']] = new Mixin($m['block_content']);
- });
- }
- #############################
- # Fragments.
- protected function resolveFragments()
- {
- $fragments =& Crush::$process->fragments;
- $this->string->pregReplaceCallback(Regex::$patt->fragmentCapture, function ($m) use (&$fragments) {
- $fragments[$m['name']] = new Fragment(
- $m['block_content'],
- ['name' => strtolower($m['name'])]
- );
- return '';
- });
- $this->string->pregReplaceCallback(Regex::$patt->fragmentInvoke, function ($m) use (&$fragments) {
- $fragment = isset($fragments[$m['name']]) ? $fragments[$m['name']] : null;
- if ($fragment) {
- $args = [];
- if (isset($m['parens'])) {
- $args = Functions::parseArgs($m['parens_content']);
- }
- return $fragment($args);
- }
- return '';
- });
- }
- #############################
- # Rules.
- public function captureRules()
- {
- $tokens = $this->tokens;
- $rulePatt = Regex::make('~
- (?<trace_token> {{ t_token }})
- \s*
- (?<selector> [^{]+)
- \s*
- {{ block }}
- ~xiS');
- $rulesAndMediaPatt = Regex::make('~{{ r_token }}|@media[^\{]+{{ block }}~iS');
- $count = preg_match_all(Regex::$patt->t_token, $this->string->raw, $traceMatches, PREG_OFFSET_CAPTURE);
- while ($count--) {
- $traceOffset = $traceMatches[0][$count][1];
- preg_match($rulePatt, $this->string->raw, $ruleMatch, null, $traceOffset);
- $selector = trim($ruleMatch['selector']);
- $block = trim($ruleMatch['block_content']);
- $replace = '';
- // If rules are nested inside we set their parent property.
- if (preg_match_all(Regex::$patt->r_token, $block, $childMatches)) {
- $block = preg_replace_callback($rulesAndMediaPatt, function ($m) use (&$replace) {
- $replace .= $m[0];
- return '';
- }, $block);
- $rule = new Rule($selector, $block, $ruleMatch['trace_token']);
- foreach ($childMatches[0] as $childToken) {
- $childRule = $tokens->get($childToken);
- if (! $childRule->parent) {
- $childRule->parent = $rule;
- }
- }
- }
- else {
- $rule = new Rule($selector, $block, $ruleMatch['trace_token']);
- }
- $replace = $tokens->add($rule, 'r', $rule->label) . $replace;
- $this->string->splice($replace, $traceOffset, strlen($ruleMatch[0]));
- }
- // Flip, since we just captured rules in reverse order.
- $tokens->store->r = array_reverse($tokens->store->r);
- foreach ($tokens->store->r as $rule) {
- if ($rule->parent) {
- $rule->selectors->merge(array_keys($rule->parent->selectors->store));
- }
- }
- // Cleanup unusable rules.
- $this->string->pregReplaceCallback(Regex::$patt->r_token, function ($m) use ($tokens) {
- $ruleToken = $m[0];
- $rule = $tokens->store->r[$ruleToken];
- if (empty($rule->declarations->store) && ! $rule->extendArgs) {
- unset($tokens->store->r[$ruleToken]);
- return '';
- }
- return $ruleToken;
- });
- }
- protected function processRules()
- {
- // Create table of name/selector to rule references.
- $namedReferences = [];
- $previousRule = null;
- foreach ($this->tokens->store->r as $rule) {
- if ($rule->name) {
- $namedReferences[$rule->name] = $rule;
- }
- foreach ($rule->selectors as $selector) {
- $this->references[$selector->readableValue] = $rule;
- }
- if ($previousRule) {
- $rule->previous = $previousRule;
- $previousRule->next = $rule;
- }
- $previousRule = $rule;
- }
- // Explicit named references take precedence.
- $this->references = $namedReferences + $this->references;
- foreach ($this->tokens->store->r as $rule) {
- $rule->declarations->flatten();
- $rule->declarations->process();
- $this->emit('rule_prealias', $rule);
- $rule->declarations->aliasProperties($rule->vendorContext);
- $rule->declarations->aliasFunctions($rule->vendorContext);
- $rule->declarations->aliasDeclarations($rule->vendorContext);
- $this->emit('rule_postalias', $rule);
- $rule->selectors->expand();
- $rule->applyExtendables();
- $this->emit('rule_postprocess', $rule);
- }
- }
- #############################
- # @-rule aliasing.
- protected function aliasAtRules()
- {
- if (empty($this->aliases['at-rules'])) {
- return;
- }
- $aliases = $this->aliases['at-rules'];
- $regex = Regex::$patt;
- foreach ($aliases as $at_rule => $at_rule_aliases) {
- $matches = $this->string->matchAll("~@$at_rule" . '[\s{]~i');
- // Find at-rules that we want to alias.
- while ($match = array_pop($matches)) {
- $curly_match = new BalancedMatch($this->string, $match[0][1]);
- if (! $curly_match->match) {
- // Couldn't match the block.
- continue;
- }
- // Build up string with aliased blocks for splicing.
- $original_block = $curly_match->whole();
- $new_blocks = [];
- foreach ($at_rule_aliases as $alias) {
- // Copy original block, replacing at-rule with alias name.
- $copy_block = str_replace("@$at_rule", "@$alias", $original_block);
- // Aliases are nearly always prefixed, capture the current vendor name.
- preg_match($regex->vendorPrefix, $alias, $vendor);
- $vendor = $vendor ? $vendor[1] : null;
- // Duplicate rules.
- if (preg_match_all($regex->r_token, $copy_block, $copy_matches)) {
- $originals = [];
- $replacements = [];
- foreach ($copy_matches[0] as $rule_label) {
- // Clone the matched rule.
- $originals[] = $rule_label;
- $clone_rule = clone $this->tokens->get($rule_label);
- $clone_rule->vendorContext = $vendor;
- // Store the clone.
- $replacements[] = $this->tokens->add($clone_rule);
- }
- // Finally replace the original labels with the cloned rule labels.
- $copy_block = str_replace($originals, $replacements, $copy_block);
- }
- // Add the copied block to the stack.
- $new_blocks[] = $copy_block;
- }
- // The original version is always pushed last in the list.
- $new_blocks[] = $original_block;
- // Splice in the blocks.
- $curly_match->replace(implode("\n", $new_blocks));
- }
- }
- }
- #############################
- # Compile / collate.
- protected function collate()
- {
- $options = $this->options;
- $minify = $options->minify;
- $EOL = $this->newline;
- // Formatting replacements.
- // Strip newlines added during processing.
- $regex_replacements = [];
- $regex_replacements['~\n+~'] = '';
- if ($minify) {
- // Strip whitespace around colons used in @-rule arguments.
- $regex_replacements['~ ?\: ?~'] = ':';
- }
- else {
- // Pretty printing.
- $regex_replacements['~}~'] = "$0$EOL$EOL";
- $regex_replacements['~([^\s])\{~'] = "$1 {";
- $regex_replacements['~ ?(@[^{]+\{)~'] = "$1$EOL";
- $regex_replacements['~ ?(@[^;]+\;)~'] = "$1$EOL";
- // Trim leading spaces on @-rules and some tokens.
- $regex_replacements[Regex::make('~ +([@}]|\?[rc]{{token_id}}\?)~S')] = "$1";
- // Additional newline between adjacent rules and comments.
- $regex_replacements[Regex::make('~({{r_token}}) (\s*) ({{c_token}})~xS')] = "$1$EOL$2$3";
- }
- // Apply all formatting replacements.
- $this->string->pregReplaceHash($regex_replacements)->lTrim();
- $this->string->restore('r');
- // Record stats then drop rule objects to reclaim memory.
- Crush::runStat('selector_count', 'rule_count', 'vars');
- $this->tokens->store->r = [];
- // If specified, apply advanced minification.
- if (is_array($minify)) {
- if (in_array('colors', $minify)) {
- $this->minifyColors();
- }
- }
- $this->decruft();
- if (! $minify) {
- // Add newlines after comments.
- foreach ($this->tokens->store->c as $token => &$comment) {
- $comment .= $EOL;
- }
- // Insert comments and do final whitespace cleanup.
- $this->string
- ->restore('c')
- ->trim()
- ->append($EOL);
- }
- // Insert URLs.
- $urls = $this->tokens->store->u;
- if ($urls) {
- $link = Util::getLinkBetweenPaths($this->output->dir, $this->input->dir);
- $make_urls_absolute = $options->rewrite_import_urls === 'absolute';
- foreach ($urls as $token => $url) {
- if ($url->isRelative && ! $url->noRewrite) {
- if ($make_urls_absolute) {
- $url->toRoot();
- }
- // If output dir is different to input dir prepend a link between the two.
- elseif ($link && $options->rewrite_import_urls) {
- $url->prepend($link);
- }
- }
- }
- }
- if ($this->absoluteImports) {
- $absoluteImports = '';
- $closing = $minify ? ';' : ";$EOL";
- foreach ($this->absoluteImports as $import) {
- $absoluteImports .= "@import $import->url" . ($import->media ? " $import->media" : '') . $closing;
- }
- $this->string->prepend($absoluteImports);
- }
- if ($options->boilerplate) {
- $this->string->prepend($this->getBoilerplate());
- }
- if ($this->charset) {
- $this->string->prepend("@charset \"$this->charset\";$EOL");
- }
- $this->string->restore(['u', 's']);
- if ($this->generateMap) {
- $this->generateSourceMap();
- }
- }
- private $iniOriginal = [];
- public function preCompile()
- {
- foreach ([
- 'pcre.backtrack_limit' => 1000000,
- 'pcre.jit' => 0, // Have run into PREG_JIT_STACKLIMIT_ERROR (issue #82).
- 'memory_limit' => '128M',
- ] as $name => $value) {
- $this->iniOriginal[$name] = ini_get($name);
- ini_set($name, $value);
- }
- $this->filterPlugins();
- $this->filterAliases();
- $this->functions->setPattern(true);
- $this->stat['compile_start_time'] = microtime(true);
- }
- public function postCompile()
- {
- $this->release();
- Crush::runStat('compile_time');
- foreach ($this->iniOriginal as $name => $value) {
- ini_set($name, $value);
- }
- }
- public function compile()
- {
- $this->preCompile();
- $importer = new Importer($this);
- $this->string = new StringObject($importer->collate());
- // Capture phase 0 hook: Before all variables have resolved.
- $this->emit('capture_phase0', $this);
- $this->captureVars();
- $this->resolveIfDefines();
- $this->resolveLoops();
- $this->placeAllVars();
- // Capture phase 1 hook: After all variables have resolved.
- $this->emit('capture_phase1', $this);
- $this->resolveSelectorAliases();
- $this->captureMixins();
- $this->resolveFragments();
- // Capture phase 2 hook: After most built-in directives have resolved.
- $this->emit('capture_phase2', $this);
- $this->captureRules();
- // Calling functions on media query lists.
- $process = $this;
- $this->string->pregReplaceCallback('~@media\s+(?<media_list>[^{]+)\{~i', function ($m) use (&$process) {
- return "@media {$process->functions->apply($m['media_list'])}{";
- });
- $this->aliasAtRules();
- $this->processRules();
- $this->collate();
- $this->postCompile();
- return $this->string;
- }
- #############################
- # Source maps.
- public function generateSourceMap()
- {
- $this->sourceMap = [
- 'version' => 3,
- 'file' => $this->output->filename,
- 'sources' => [],
- ];
- foreach ($this->sources as $source) {
- $this->sourceMap['sources'][] = Util::getLinkBetweenPaths($this->output->dir, $source, false);
- }
- $token_patt = Regex::make('~\?[tm]{{token_id}}\?~S');
- $mappings = [];
- $lines = preg_split(Regex::$patt->newline, $this->string->raw);
- $tokens =& $this->tokens->store;
- // All mappings are calculated as delta values.
- $previous_dest_col = 0;
- $previous_src_file = 0;
- $previous_src_line = 0;
- $previous_src_col = 0;
- foreach ($lines as &$line_text) {
- $line_segments = [];
- while (preg_match($token_patt, $line_text, $m, PREG_OFFSET_CAPTURE)) {
- list($token, $dest_col) = $m[0];
- $token_type = $token[1];
- if (isset($tokens->{$token_type}[$token])) {
- list($src_file, $src_line, $src_col) = explode(',', $tokens->{$token_type}[$token]);
- $line_segments[] =
- Util::vlqEncode($dest_col - $previous_dest_col) .
- Util::vlqEncode($src_file - $previous_src_file) .
- Util::vlqEncode($src_line - $previous_src_line) .
- Util::vlqEncode($src_col - $previous_src_col);
- $previous_dest_col = $dest_col;
- $previous_src_file = $src_file;
- $previous_src_line = $src_line;
- $previous_src_col = $src_col;
- }
- $line_text = substr_replace($line_text, '', $dest_col, strlen($token));
- }
- $mappings[] = implode(',', $line_segments);
- }
- $this->string->raw = implode($this->newline, $lines);
- $this->sourceMap['mappings'] = implode(';', $mappings);
- }
- #############################
- # Decruft.
- protected function decruft()
- {
- return $this->string->pregReplaceHash([
- // Strip leading zeros on floats.
- '~([: \(,])(-?)0(\.\d+)~S' => '$1$2$3',
- // Strip unnecessary units on zero values for length types.
- '~([: \(,])\.?0' . Regex::$classes->length_unit . '~iS' => '${1}0',
- // Collapse zero lists.
- '~(\: *)(?:0 0 0|0 0 0 0) *([;}])~S' => '${1}0$2',
- // Collapse zero lists 2nd pass.
- '~(padding|margin|border-radius) ?(\: *)0 0 *([;}])~iS' => '${1}${2}0$3',
- // Dropping redundant trailing zeros on TRBL lists.
- '~(\: *)(-?(?:\d+)?\.?\d+[a-z]{1,4}) 0 0 0 *([;}])~iS' => '$1$2 0 0$3',
- '~(\: *)0 0 (-?(?:\d+)?\.?\d+[a-z]{1,4}) 0 *([;}])~iS' => '${1}0 0 $2$3',
- // Compress hex codes.
- Regex::$patt->cruftyHex => '#$1$2$3',
- ]);
- }
- #############################
- # Advanced minification.
- protected function minifyColors()
- {
- static $keywords_patt, $functions_patt;
- $minified_keywords = Color::getMinifyableKeywords();
- if (! $keywords_patt) {
- $keywords_patt = '~(?<![\w\.#-])(' . implode('|', array_keys($minified_keywords)) . ')(?![\w\.#\]-])~iS';
- $functions_patt = Regex::make('~{{ LB }}(rgb|hsl)\(([^\)]{5,})\)~iS');
- }
- $this->string->pregReplaceCallback($keywords_patt, function ($m) use ($minified_keywords) {
- return $minified_keywords[strtolower($m[0])];
- });
- $this->string->pregReplaceCallback($functions_patt, function ($m) {
- $args = Functions::parseArgs(trim($m[2]));
- if (stripos($m[1], 'hsl') === 0) {
- $args = Color::cssHslToRgb($args);
- }
- return Color::rgbToHex($args);
- });
- }
- }