PageRenderTime 74ms CodeModel.GetById 10ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/CssCrush/Process.php

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