diff options
Diffstat (limited to 'plugins/jetpack/modules/custom-css')
5 files changed, 1285 insertions, 363 deletions
diff --git a/plugins/jetpack/modules/custom-css/custom-css.php b/plugins/jetpack/modules/custom-css/custom-css.php index 02fa7586..9c3e7e7c 100644 --- a/plugins/jetpack/modules/custom-css/custom-css.php +++ b/plugins/jetpack/modules/custom-css/custom-css.php @@ -901,14 +901,25 @@ class Jetpack_Custom_CSS { /** * Render metabox listing CSS revisions and the themes that correspond to the revisions. - * Called by afecss_admin * - * @param array $safecss_post + * Called by safecss_admin + * * @global $post - * @uses WP_Query, wp_post_revision_title, esc_html, add_query_arg, menu_page_url, wp_reset_query + * @param array $safecss_post + * @uses wp_revisions_to_keep + * @uses WP_Query + * @uses wp_post_revision_title + * @uses esc_html + * @uses add_query_arg + * @uses menu_page_url + * @uses wp_reset_query * @return string */ static function revisions_meta_box( $safecss_post ) { - $max_revisions = defined( 'WP_POST_REVISIONS' ) && is_numeric( WP_POST_REVISIONS ) ? (int) WP_POST_REVISIONS : 25; + if ( function_exists( 'wp_revisions_to_keep' ) ) + $max_revisions = wp_revisions_to_keep( $safecss_post ); + else + $max_revisions = defined( 'WP_POST_REVISIONS' ) && is_numeric( WP_POST_REVISIONS ) ? (int) WP_POST_REVISIONS : 25; + $posts_per_page = isset( $_GET['show_all_rev'] ) ? $max_revisions : 6; $revisions = new WP_Query( array( diff --git a/plugins/jetpack/modules/custom-css/custom-css/blank.css b/plugins/jetpack/modules/custom-css/custom-css/blank.css deleted file mode 100644 index 79a9626b..00000000 --- a/plugins/jetpack/modules/custom-css/custom-css/blank.css +++ /dev/null @@ -1 +0,0 @@ -/* */ diff --git a/plugins/jetpack/modules/custom-css/custom-css/css/css-editor.css b/plugins/jetpack/modules/custom-css/custom-css/css/css-editor.css index 4dec4125..230072c6 100644 --- a/plugins/jetpack/modules/custom-css/custom-css/css/css-editor.css +++ b/plugins/jetpack/modules/custom-css/custom-css/css/css-editor.css @@ -21,4 +21,7 @@ .misc-pub-section > div { margin-top: 3px; +} +#safecss-ace .ace_gutter { + z-index: 1; }
\ No newline at end of file diff --git a/plugins/jetpack/modules/custom-css/custom-css/preprocessors/lessc.inc.php b/plugins/jetpack/modules/custom-css/custom-css/preprocessors/lessc.inc.php index c42147c5..8bdfa3fb 100644 --- a/plugins/jetpack/modules/custom-css/custom-css/preprocessors/lessc.inc.php +++ b/plugins/jetpack/modules/custom-css/custom-css/preprocessors/lessc.inc.php @@ -1,7 +1,7 @@ <?php /** - * lessphp v0.3.8 + * lessphp v0.4.0 * http://leafo.net/lessphp * * LESS css compiler, adapted from http://lesscss.org @@ -38,7 +38,7 @@ * handling things like indentation. */ class lessc { - static public $VERSION = "v0.3.8"; + static public $VERSION = "v0.4.0"; static protected $TRUE = array("keyword", "true"); static protected $FALSE = array("keyword", "false"); @@ -55,6 +55,8 @@ class lessc { protected $numberPrecision = null; + protected $allParsedFiles = array(); + // set to the parser that generated the current line when compiling // so we know how to create error messages protected $sourceParser = null; @@ -103,12 +105,17 @@ class lessc { if (substr_compare($url, '.css', -4, 4) === 0) return false; $realPath = $this->findImport($url); + if ($realPath === null) return false; if ($this->importDisabled) { return array(false, "/* import disabled */"); } + if (isset($this->allParsedFiles[realpath($realPath)])) { + return array(false, null); + } + $this->addParsedFile($realPath); $parser = $this->makeParser($realPath); $root = $parser->parse(file_get_contents($realPath)); @@ -276,6 +283,8 @@ class lessc { foreach ($this->sortProps($block->props) as $prop) { $this->compileProp($prop, $block, $out); } + + $out->lines = array_values(array_unique($out->lines)); } protected function sortProps($props, $split = false) { @@ -327,6 +336,9 @@ class lessc { $parts[] = "($q[1])"; } break; + case "variable": + $parts[] = $this->compileValue($this->reduce($q)); + break; } } @@ -434,7 +446,7 @@ class lessc { foreach ($selectors as $s) { if (is_array($s)) { list(, $value) = $s; - $out[] = $this->compileValue($this->reduce($value)); + $out[] = trim($this->compileValue($this->reduce($value))); } else { $out[] = $s; } @@ -447,7 +459,7 @@ class lessc { return $left == $right; } - protected function patternMatch($block, $callingArgs) { + protected function patternMatch($block, $orderedArgs, $keywordArgs) { // match the guards if it has them // any one of the groups must have all its guards pass for a match if (!empty($block->guards)) { @@ -455,7 +467,7 @@ class lessc { foreach ($block->guards as $guardGroup) { foreach ($guardGroup as $guard) { $this->pushEnv(); - $this->zipSetArgs($block->args, $callingArgs); + $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs); $negate = false; if ($guard[0] == "negate") { @@ -484,24 +496,34 @@ class lessc { } } - $numCalling = count($callingArgs); - if (empty($block->args)) { - return $block->isVararg || $numCalling == 0; + return $block->isVararg || empty($orderedArgs) && empty($keywordArgs); + } + + $remainingArgs = $block->args; + if ($keywordArgs) { + $remainingArgs = array(); + foreach ($block->args as $arg) { + if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) { + continue; + } + + $remainingArgs[] = $arg; + } } $i = -1; // no args // try to match by arity or by argument literal - foreach ($block->args as $i => $arg) { + foreach ($remainingArgs as $i => $arg) { switch ($arg[0]) { case "lit": - if (empty($callingArgs[$i]) || !$this->eq($arg[1], $callingArgs[$i])) { + if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) { return false; } break; case "arg": // no arg and no default value - if (!isset($callingArgs[$i]) && !isset($arg[2])) { + if (!isset($orderedArgs[$i]) && !isset($arg[2])) { return false; } break; @@ -516,14 +538,19 @@ class lessc { } else { $numMatched = $i + 1; // greater than becuase default values always match - return $numMatched >= $numCalling; + return $numMatched >= count($orderedArgs); } } - protected function patternMatchAll($blocks, $callingArgs) { + protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) { $matches = null; foreach ($blocks as $block) { - if ($this->patternMatch($block, $callingArgs)) { + // skip seen blocks that don't have arguments + if (isset($skip[$block->id]) && !isset($block->args)) { + continue; + } + + if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) { $matches[] = $block; } } @@ -532,7 +559,7 @@ class lessc { } // attempt to find blocks matched by path and args - protected function findBlocks($searchIn, $path, $args, $seen=array()) { + protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen=array()) { if ($searchIn == null) return null; if (isset($seen[$searchIn->id])) return null; $seen[$searchIn->id] = true; @@ -542,7 +569,7 @@ class lessc { if (isset($searchIn->children[$name])) { $blocks = $searchIn->children[$name]; if (count($path) == 1) { - $matches = $this->patternMatchAll($blocks, $args); + $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen); if (!empty($matches)) { // This will return all blocks that match in the closest // scope that has any matching block, like lessjs @@ -552,7 +579,7 @@ class lessc { $matches = array(); foreach ($blocks as $subBlock) { $subMatches = $this->findBlocks($subBlock, - array_slice($path, 1), $args, $seen); + array_slice($path, 1), $orderedArgs, $keywordArgs, $seen); if (!is_null($subMatches)) { foreach ($subMatches as $sm) { @@ -564,39 +591,51 @@ class lessc { return count($matches) > 0 ? $matches : null; } } - if ($searchIn->parent === $searchIn) return null; - return $this->findBlocks($searchIn->parent, $path, $args, $seen); + return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen); } // sets all argument names in $args to either the default value // or the one passed in through $values - protected function zipSetArgs($args, $values) { - $i = 0; + protected function zipSetArgs($args, $orderedValues, $keywordValues) { $assignedValues = array(); - foreach ($args as $a) { + + $i = 0; + foreach ($args as $a) { if ($a[0] == "arg") { - if ($i < count($values) && !is_null($values[$i])) { - $value = $values[$i]; + if (isset($keywordValues[$a[1]])) { + // has keyword arg + $value = $keywordValues[$a[1]]; + } elseif (isset($orderedValues[$i])) { + // has ordered arg + $value = $orderedValues[$i]; + $i++; } elseif (isset($a[2])) { + // has default value $value = $a[2]; - } else $value = null; + } else { + $this->throwError("Failed to assign arg " . $a[1]); + $value = null; // :( + } $value = $this->reduce($value); $this->set($a[1], $value); $assignedValues[] = $value; + } else { + // a lit + $i++; } - $i++; } // check for a rest $last = end($args); if ($last[0] == "rest") { - $rest = array_slice($values, count($args) - 1); + $rest = array_slice($orderedValues, count($args) - 1); $this->set($last[1], $this->reduce(array("list", " ", $rest))); } - $this->env->arguments = $assignedValues; + // wow is this the only true use of PHP's + operator for arrays? + $this->env->arguments = $assignedValues + $orderedValues; } // compile a prop and update $lines or $blocks appropriately @@ -621,8 +660,28 @@ class lessc { case 'mixin': list(, $path, $args, $suffix) = $prop; - $args = array_map(array($this, "reduce"), (array)$args); - $mixins = $this->findBlocks($block, $path, $args); + $orderedArgs = array(); + $keywordArgs = array(); + foreach ((array)$args as $arg) { + $argval = null; + switch ($arg[0]) { + case "arg": + if (!isset($arg[2])) { + $orderedArgs[] = $this->reduce(array("variable", $arg[1])); + } else { + $keywordArgs[$arg[1]] = $this->reduce($arg[2]); + } + break; + + case "lit": + $orderedArgs[] = $this->reduce($arg[1]); + break; + default: + $this->throwError("Unknown arg type: " . $arg[0]); + } + } + + $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs); if ($mixins === null) { // fwrite(STDERR,"failed to find block: ".implode(" > ", $path)."\n"); @@ -630,6 +689,10 @@ class lessc { } foreach ($mixins as $mixin) { + if ($mixin === $block && !$orderedArgs) { + continue; + } + $haveScope = false; if (isset($mixin->parent->scope)) { $haveScope = true; @@ -641,7 +704,7 @@ class lessc { if (isset($mixin->args)) { $haveArgs = true; $this->pushEnv(); - $this->zipSetArgs($mixin->args, $args); + $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs); } $oldParent = $mixin->parent; @@ -698,7 +761,9 @@ class lessc { list(,$importId) = $prop; $import = $this->env->imports[$importId]; if ($import[0] === false) { - $out->lines[] = $import[1]; + if (isset($import[1])) { + $out->lines[] = $import[1]; + } } else { list(, $bottom, $parser, $importDir) = $import; $this->compileImportedProps($bottom, $block, $out, $parser, $importDir); @@ -786,6 +851,60 @@ class lessc { } } + protected function lib_pow($args) { + list($base, $exp) = $this->assertArgs($args, 2, "pow"); + return pow($this->assertNumber($base), $this->assertNumber($exp)); + } + + protected function lib_pi() { + return pi(); + } + + protected function lib_mod($args) { + list($a, $b) = $this->assertArgs($args, 2, "mod"); + return $this->assertNumber($a) % $this->assertNumber($b); + } + + protected function lib_tan($num) { + return tan($this->assertNumber($num)); + } + + protected function lib_sin($num) { + return sin($this->assertNumber($num)); + } + + protected function lib_cos($num) { + return cos($this->assertNumber($num)); + } + + protected function lib_atan($num) { + $num = atan($this->assertNumber($num)); + return array("number", $num, "rad"); + } + + protected function lib_asin($num) { + $num = asin($this->assertNumber($num)); + return array("number", $num, "rad"); + } + + protected function lib_acos($num) { + $num = acos($this->assertNumber($num)); + return array("number", $num, "rad"); + } + + protected function lib_sqrt($num) { + return sqrt($this->assertNumber($num)); + } + + protected function lib_extract($value) { + list($list, $idx) = $this->assertArgs($value, 2, "extract"); + $idx = $this->assertNumber($idx); + // 1 indexed + if ($list[0] == "list" && isset($list[2][$idx - 1])) { + return $list[2][$idx - 1]; + } + } + protected function lib_isnumber($value) { return $this->toBool($value[0] == "number"); } @@ -814,6 +933,10 @@ class lessc { return $this->toBool($value[0] == "number" && $value[2] == "em"); } + protected function lib_isrem($value) { + return $this->toBool($value[0] == "number" && $value[2] == "rem"); + } + protected function lib_rgbahex($color) { $color = $this->coerceColor($color); if (is_null($color)) @@ -890,6 +1013,16 @@ class lessc { return array("number", round($value), $arg[2]); } + protected function lib_unit($arg) { + if ($arg[0] == "list") { + list($number, $newUnit) = $arg[2]; + return array("number", $this->assertNumber($number), + $this->compileValue($this->lib_e($newUnit))); + } else { + return array("number", $this->assertNumber($arg), ""); + } + } + /** * Helper function to get arguments for color manipulation functions. * takes a list that contains a color like thing and a percentage @@ -996,19 +1129,24 @@ class lessc { } // mixes two colors by weight - // mix(@color1, @color2, @weight); + // mix(@color1, @color2, [@weight: 50%]); // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method protected function lib_mix($args) { - if ($args[0] != "list" || count($args[2]) < 3) + if ($args[0] != "list" || count($args[2]) < 2) $this->throwError("mix expects (color1, color2, weight)"); - list($first, $second, $weight) = $args[2]; + list($first, $second) = $args[2]; $first = $this->assertColor($first); $second = $this->assertColor($second); $first_a = $this->lib_alpha($first); $second_a = $this->lib_alpha($second); - $weight = $weight[1] / 100.0; + + if (isset($args[2][2])) { + $weight = $args[2][2][1] / 100.0; + } else { + $weight = 0.5; + } $w = $weight * 2 - 1; $a = $first_a - $second_a; @@ -1029,6 +1167,25 @@ class lessc { return $this->fixColor($new); } + protected function lib_contrast($args) { + if ($args[0] != 'list' || count($args[2]) < 3) { + return array(array('color', 0, 0, 0), 0); + } + + list($inputColor, $darkColor, $lightColor) = $args[2]; + + $inputColor = $this->assertColor($inputColor); + $darkColor = $this->assertColor($darkColor); + $lightColor = $this->assertColor($lightColor); + $hsl = $this->toHSL($inputColor); + + if ($hsl[3] > 50) { + return $darkColor; + } + + return $lightColor; + } + protected function assertColor($value, $error = "expected color value") { $color = $this->coerceColor($value); if (is_null($color)) $this->throwError($error); @@ -1040,6 +1197,25 @@ class lessc { $this->throwError($error); } + protected function assertArgs($value, $expectedArgs, $name="") { + if ($expectedArgs == 1) { + return $value; + } else { + if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list"); + $values = $value[2]; + $numValues = count($values); + if ($expectedArgs != $numValues) { + if ($name) { + $name = $name . ": "; + } + + $this->throwError("${name}expecting $expectedArgs arguments, got $numValues"); + } + + return $values; + } + } + protected function toHSL($color) { if ($color[0] == 'hsl') return $color; @@ -1091,7 +1267,7 @@ class lessc { * Expects H to be in range of 0 to 360, S and L in 0 to 100 */ protected function toRGB($color) { - if ($color == 'color') return $color; + if ($color[0] == 'color') return $color; $H = $color[1] / 360; $S = $color[2] / 100; @@ -1179,6 +1355,18 @@ class lessc { protected function reduce($value, $forExpression = false) { switch ($value[0]) { + case "interpolate": + $reduced = $this->reduce($value[1]); + $var = $this->compileValue($reduced); + $res = $this->reduce(array("variable", $this->vPrefix . $var)); + + if ($res[0] == "raw_color") { + $res = $this->coerceColor($res); + } + + if (empty($value[2])) $res = $this->lib_e($res); + + return $res; case "variable": $key = $value[1]; if (is_array($key)) { @@ -1299,8 +1487,12 @@ class lessc { case 'keyword': $name = $value[1]; if (isset(self::$cssColors[$name])) { - list($r, $g, $b) = explode(',', self::$cssColors[$name]); - return array('color', $r, $g, $b); + $rgba = explode(',', self::$cssColors[$name]); + + if(isset($rgba[3])) + return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]); + + return array('color', $rgba[0], $rgba[1], $rgba[2]); } return null; } @@ -1445,6 +1637,34 @@ class lessc { return $this->fixColor($out); } + function lib_red($color){ + $color = $this->coerceColor($color); + if (is_null($color)) { + $this->throwError('color expected for red()'); + } + + return $color[1]; + } + + function lib_green($color){ + $color = $this->coerceColor($color); + if (is_null($color)) { + $this->throwError('color expected for green()'); + } + + return $color[2]; + } + + function lib_blue($color){ + $color = $this->coerceColor($color); + if (is_null($color)) { + $this->throwError('color expected for blue()'); + } + + return $color[3]; + } + + // operator on two numbers protected function op_number_number($op, $left, $right) { $unit = empty($left[2]) ? $right[2] : $left[2]; @@ -1605,7 +1825,6 @@ class lessc { $this->importDir = (array)$this->importDir; $this->importDir[] = $pi['dirname'].'/'; - $this->allParsedFiles = array(); $this->addParsedFile($fname); $out = $this->compile(file_get_contents($fname), $fname); @@ -1945,6 +2164,7 @@ class lessc { 'teal' => '0,128,128', 'thistle' => '216,191,216', 'tomato' => '255,99,71', + 'transparent' => '0,0,0,0', 'turquoise' => '64,224,208', 'violet' => '238,130,238', 'wheat' => '245,222,179', @@ -1988,7 +2208,7 @@ class lessc_parser { static protected $supressDivisionProps = array('/border-radius$/i', '/^font$/i'); - protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document"); + protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport"); protected $lineDirectives = array("charset"); /** @@ -2227,7 +2447,7 @@ class lessc_parser { // mixin if ($this->mixinTags($tags) && - ($this->argumentValues($argv) || true) && + ($this->argumentDef($argv, $isVararg) || true) && ($this->keyword($suffix) || true) && $this->end()) { $tags = $this->fixTags($tags); @@ -2519,6 +2739,9 @@ class lessc_parser { $out = array("mediaExp", $feature); if ($value) $out[] = $value; return true; + } elseif ($this->variable($variable)) { + $out = array('variable', $variable); + return true; } $this->seek($s); @@ -2572,12 +2795,10 @@ class lessc_parser { continue; } - if (in_array($tok, $rejectStrs)) { - $count = null; + if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) { break; } - $content[] = $tok; $this->count+= strlen($tok); } @@ -2652,10 +2873,10 @@ class lessc_parser { $s = $this->seek(); if ($this->literal("@{") && - $this->keyword($var) && + $this->openString("}", $interp, null, array("'", '"', ";")) && $this->literal("}", false)) { - $out = array("variable", $this->lessc->vPrefix . $var); + $out = array("interpolate", $interp); $this->eatWhiteDefault = $oldWhite; if ($this->eatWhiteDefault) $this->whitespace(); return true; @@ -2694,38 +2915,18 @@ class lessc_parser { return false; } - // consume a list of property values delimited by ; and wrapped in () - protected function argumentValues(&$args, $delim = ',') { - $s = $this->seek(); - if (!$this->literal('(')) return false; - - $values = array(); - while (true) { - if ($this->expressionList($value)) $values[] = $value; - if (!$this->literal($delim)) break; - else { - if ($value == null) $values[] = null; - $value = null; - } - } - - if (!$this->literal(')')) { - $this->seek($s); - return false; - } - - $args = $values; - return true; - } - // consume an argument definition list surrounded by () // each argument is a variable name with optional value // or at the end a ... or a variable named followed by ... - protected function argumentDef(&$args, &$isVararg, $delim = ',') { + // arguments are separated by , unless a ; is in the list, then ; is the + // delimiter. + protected function argumentDef(&$args, &$isVararg) { $s = $this->seek(); if (!$this->literal('(')) return false; $values = array(); + $delim = ","; + $method = "expressionList"; $isVararg = false; while (true) { @@ -2734,28 +2935,77 @@ class lessc_parser { break; } - if ($this->variable($vname)) { - $arg = array("arg", $vname); - $ss = $this->seek(); - if ($this->assign() && $this->expressionList($value)) { - $arg[] = $value; - } else { - $this->seek($ss); - if ($this->literal("...")) { - $arg[0] = "rest"; - $isVararg = true; + if ($this->$method($value)) { + if ($value[0] == "variable") { + $arg = array("arg", $value[1]); + $ss = $this->seek(); + + if ($this->assign() && $this->$method($rhs)) { + $arg[] = $rhs; + } else { + $this->seek($ss); + if ($this->literal("...")) { + $arg[0] = "rest"; + $isVararg = true; + } } + + $values[] = $arg; + if ($isVararg) break; + continue; + } else { + $values[] = array("lit", $value); } - $values[] = $arg; - if ($isVararg) break; - continue; } - if ($this->value($literal)) { - $values[] = array("lit", $literal); - } - if (!$this->literal($delim)) break; + if (!$this->literal($delim)) { + if ($delim == "," && $this->literal(";")) { + // found new delim, convert existing args + $delim = ";"; + $method = "propertyValue"; + + // transform arg list + if (isset($values[1])) { // 2 items + $newList = array(); + foreach ($values as $i => $arg) { + switch($arg[0]) { + case "arg": + if ($i) { + $this->throwError("Cannot mix ; and , as delimiter types"); + } + $newList[] = $arg[2]; + break; + case "lit": + $newList[] = $arg[1]; + break; + case "rest": + $this->throwError("Unexpected rest before semicolon"); + } + } + + $newList = array("list", ", ", $newList); + + switch ($values[0][0]) { + case "arg": + $newArg = array("arg", $values[0][1], $newList); + break; + case "lit": + $newArg = array("lit", $newList); + break; + } + + } elseif ($values) { // 1 item + $newArg = $values[0]; + } + + if ($newArg) { + $values = array($newArg); + } + } else { + break; + } + } } if (!$this->literal(')')) { @@ -2797,70 +3047,136 @@ class lessc_parser { } // a bracketed value (contained within in a tag definition) - protected function tagBracket(&$value) { + protected function tagBracket(&$parts, &$hasExpression) { // speed shortcut if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") { return false; } $s = $this->seek(); - if ($this->literal('[') && $this->to(']', $c, true) && $this->literal(']', false)) { - $value = '['.$c.']'; - // whitespace? - if ($this->whitespace()) $value .= " "; - // escape parent selector, (yuck) - $value = str_replace($this->lessc->parentSelector, "$&$", $value); - return true; - } + $hasInterpolation = false; - $this->seek($s); - return false; - } + if ($this->literal("[", false)) { + $attrParts = array("["); + // keyword, string, operator + while (true) { + if ($this->literal("]", false)) { + $this->count--; + break; // get out early + } - protected function tagExpression(&$value) { - $s = $this->seek(); - if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) { - $value = array('exp', $exp); - return true; + if ($this->match('\s+', $m)) { + $attrParts[] = " "; + continue; + } + if ($this->string($str)) { + // escape parent selector, (yuck) + foreach ($str[2] as &$chunk) { + $chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk); + } + + $attrParts[] = $str; + $hasInterpolation = true; + continue; + } + + if ($this->keyword($word)) { + $attrParts[] = $word; + continue; + } + + if ($this->interpolation($inter, false)) { + $attrParts[] = $inter; + $hasInterpolation = true; + continue; + } + + // operator, handles attr namespace too + if ($this->match('[|-~\$\*\^=]+', $m)) { + $attrParts[] = $m[0]; + continue; + } + + break; + } + + if ($this->literal("]", false)) { + $attrParts[] = "]"; + foreach ($attrParts as $part) { + $parts[] = $part; + } + $hasExpression = $hasExpression || $hasInterpolation; + return true; + } + $this->seek($s); } $this->seek($s); return false; } - // a single tag + // a space separated list of selectors protected function tag(&$tag, $simple = false) { if ($simple) - $chars = '^,:;{}\][>\(\) "\''; + $chars = '^@,:;{}\][>\(\) "\''; else - $chars = '^,;{}["\''; + $chars = '^@,;{}["\''; - if (!$simple && $this->tagExpression($tag)) { - return true; - } + $s = $this->seek(); - $tag = ''; - while ($this->tagBracket($first)) $tag .= $first; + $hasExpression = false; + $parts = array(); + while ($this->tagBracket($parts, $hasExpression)); + + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; while (true) { if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) { - $tag .= $m[1]; + $parts[] = $m[1]; if ($simple) break; - while ($this->tagBracket($brack)) $tag .= $brack; + while ($this->tagBracket($parts, $hasExpression)); continue; - } elseif ($this->unit($unit)) { // for keyframes - $tag .= $unit[1] . $unit[2]; + } + + if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") { + if ($this->interpolation($interp)) { + $hasExpression = true; + $interp[2] = true; // don't unescape + $parts[] = $interp; + continue; + } + + if ($this->literal("@")) { + $parts[] = "@"; + continue; + } + } + + if ($this->unit($unit)) { // for keyframes + $parts[] = $unit[1]; + $parts[] = $unit[2]; continue; } + break; } + $this->eatWhiteDefault = $oldWhite; + if (!$parts) { + $this->seek($s); + return false; + } - $tag = trim($tag); - if ($tag == '') return false; + if ($hasExpression) { + $tag = array("exp", array("string", "", $parts)); + } else { + $tag = trim(implode($parts)); + } + $this->whitespace(); return true; } @@ -2948,7 +3264,7 @@ class lessc_parser { protected function end() { if ($this->literal(';')) { return true; - } elseif ($this->count == strlen($this->buffer) || $this->buffer{$this->count} == '}') { + } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') { // if there is end of file or a closing block next then we don't need a ; return true; } @@ -3212,7 +3528,7 @@ class lessc_parser { break; case '"': case "'": - if (preg_match('/'.$min[0].'.*?'.$min[0].'/', $text, $m, 0, $count)) + if (preg_match('/'.$min[0].'.*?(?<!\\\\)'.$min[0].'/', $text, $m, 0, $count)) $count += strlen($m[0]) - 1; break; case '//': @@ -3356,4 +3672,3 @@ class lessc_formatter_lessjs extends lessc_formatter_classic { public $selectorSeparator = ","; } - diff --git a/plugins/jetpack/modules/custom-css/custom-css/preprocessors/scss.inc.php b/plugins/jetpack/modules/custom-css/custom-css/preprocessors/scss.inc.php index 5ad188cc..ff0493a7 100644 --- a/plugins/jetpack/modules/custom-css/custom-css/preprocessors/scss.inc.php +++ b/plugins/jetpack/modules/custom-css/custom-css/preprocessors/scss.inc.php @@ -1,7 +1,49 @@ <?php - +/** + * SCSS compiler written in PHP + * + * @copyright 2012-2013 Leaf Corcoran + * + * @license http://opensource.org/licenses/gpl-license GPL-3.0 + * @license http://opensource.org/licenses/MIT MIT + * + * @link http://leafo.net/scssphp + */ + +/** + * The scss compiler and parser. + * + * Converting SCSS to CSS is a three stage process. The incoming file is parsed + * by `scssc_parser` into a syntax tree, then it is compiled into another tree + * representing the CSS structure by `scssc`. The CSS tree is fed into a + * formatter, like `scssc_formatter` which then outputs CSS as a string. + * + * During the first compile, all values are *reduced*, which means that their + * types are brought to the lowest form before being dump as strings. This + * handles math equations, variable dereferences, and the like. + * + * The `parse` function of `scssc` is the entry point. + * + * In summary: + * + * The `scssc` class creates an instance of the parser, feeds it SCSS code, + * then transforms the resulting tree to a CSS tree. This class also holds the + * evaluation context, such as all available mixins and variables at any given + * time. + * + * The `scssc_parser` class is only concerned with parsing its input. + * + * The `scssc_formatter` takes a CSS tree, and dumps it to a formatted string, + * handling things like indentation. + */ + +/** + * SCSS compiler + * + * @author Leaf Corcoran <leafot@gmail.com> + */ class scssc { - static public $VERSION = "v0.0.4"; + static public $VERSION = "v0.0.7"; static protected $operatorNames = array( '+' => "add", @@ -25,7 +67,7 @@ class scssc { "function" => "^", ); - static protected $numberPrecision = 3; + static protected $numberPrecision = 5; static protected $unitTable = array( "in" => array( "in" => 1, @@ -39,6 +81,7 @@ class scssc { static public $true = array("keyword", "true"); static public $false = array("keyword", "false"); + static public $null = array("null"); static public $defaultValue = array("keyword", ""); static public $selfSelector = array("self"); @@ -69,17 +112,28 @@ class scssc { $this->scope = null; $this->compileRoot($tree); - $this->flattenSelectors($this->scope); - ob_start(); - $this->formatter->block($this->scope); - $out = ob_get_clean(); + $out = $this->formatter->format($this->scope); setlocale(LC_NUMERIC, $locale); return $out; } + protected function isSelfExtend($target, $origin) { + foreach ($origin as $sel) { + if (in_array($target, $sel)) { + return true; + } + } + + return false; + } + protected function pushExtends($target, $origin) { + if ($this->isSelfExtend($target, $origin)) { + return; + } + $i = count($this->extends); $this->extends[] = array($target, $origin); @@ -93,7 +147,7 @@ class scssc { } protected function makeOutputBlock($type, $selectors = null) { - $out = new stdclass; + $out = new stdClass; $out->type = $type; $out->lines = array(); $out->children = array(); @@ -104,7 +158,7 @@ class scssc { return $out; } - protected function matchExtendsSingle($single, &$out_origin, &$out_rem) { + protected function matchExtendsSingle($single, &$outOrigin) { $counts = array(); foreach ($single as $part) { if (!is_string($part)) return false; // hmm @@ -117,20 +171,30 @@ class scssc { } } + $outOrigin = array(); + $found = false; + foreach ($counts as $idx => $count) { list($target, $origin) = $this->extends[$idx]; + // check count if ($count != count($target)) continue; + // check if target is subset of single if (array_diff(array_intersect($single, $target), $target)) continue; - $out_origin = $origin; - $out_rem = array_diff($single, $target); + $rem = array_diff($single, $target); - return true; + foreach ($origin as $j => $new) { + $origin[$j][count($origin[$j]) - 1] = $this->combineSelectorSingle(end($new), $rem); + } + + $outOrigin = array_merge($outOrigin, $origin); + + $found = true; } - return false; + return $found; } protected function combineSelectorSingle($base, $other) { @@ -139,7 +203,7 @@ class scssc { foreach (array($base, $other) as $single) { foreach ($single as $part) { - if (preg_match('/^[^.#:]/', $part)) { + if (preg_match('/^[^\[.#:]/', $part)) { $tag = $part; } else { $out[] = $part; @@ -158,15 +222,13 @@ class scssc { foreach ($selector as $i => $part) { if ($i < $from) continue; - if ($this->matchExtendsSingle($part, $origin, $rem)) { + if ($this->matchExtendsSingle($part, $origin)) { $before = array_slice($selector, 0, $i); $after = array_slice($selector, $i + 1); foreach ($origin as $new) { - $new[count($new) - 1] = - $this->combineSelectorSingle(end($new), $rem); - $k = 0; + // remove shared parts if ($initial) { foreach ($before as $k => $val) { @@ -203,7 +265,7 @@ class scssc { } } - protected function flattenSelectors($block) { + protected function flattenSelectors($block, $parentKey = null) { if ($block->selectors) { $selectors = array(); foreach ($block->selectors as $s) { @@ -215,19 +277,34 @@ class scssc { } } - $selectors = array_map(array($this, "compileSelector"), $selectors); - $block->selectors = $selectors; + $block->selectors = array(); + $placeholderSelector = false; + foreach ($selectors as $selector) { + if ($this->hasSelectorPlaceholder($selector)) { + $placeholderSelector = true; + continue; + } + $block->selectors[] = $this->compileSelector($selector); + } + + if ($placeholderSelector && 0 == count($block->selectors) && null !== $parentKey) { + unset($block->parent->children[$parentKey]); + return; + } } - foreach ($block->children as $child) { - $this->flattenSelectors($child); + foreach ($block->children as $key => $child) { + $this->flattenSelectors($child, $key); } } protected function compileRoot($rootBlock) { $this->pushEnv($rootBlock); $this->scope = $this->makeOutputBlock("root"); + $this->compileChildren($rootBlock->children, $this->scope); + $this->flattenSelectors($this->scope); + $this->popEnv(); } @@ -241,6 +318,24 @@ class scssc { $parentScope->children[] = $this->scope; + // top level properties in a media cause it to be wrapped + $needsWrap = false; + foreach ($media->children as $child) { + $type = $child[0]; + if ($type !== 'block' && $type !== 'media' && $type !== 'directive') { + $needsWrap = true; + break; + } + } + + if ($needsWrap) { + $wrapped = (object)array( + "selectors" => array(), + "children" => $media->children + ); + $media->children = array(array("block", $wrapped)); + } + $this->compileChildren($media->children, $this->scope); $this->scope = $this->scope->parent; @@ -270,6 +365,24 @@ class scssc { $this->popEnv(); } + /** + * Recursively compiles a block. + * + * A block is analogous to a CSS block in most cases. A single SCSS document + * is encapsulated in a block when parsed, but it does not have parent tags + * so all of its children appear on the root level when compiled. + * + * Blocks are made up of selectors and children. + * + * The children of a block are just all the blocks that are defined within. + * + * Compiling the block involves pushing a fresh environment on the stack, + * and iterating through the props, compiling each one. + * + * @see scss::compileChild() + * + * @param \StdClass $block + */ protected function compileBlock($block) { $env = $this->pushEnv($block); @@ -289,7 +402,7 @@ class scssc { foreach ($single as $part) { if (empty($joined) || !is_string($part) || - preg_match('/[.:#]/', $part)) + preg_match('/[\[.:#%]/', $part)) { $joined[] = $part; continue; @@ -318,6 +431,9 @@ class scssc { case "interpolate": $p = $this->compileValue($p); break; + case "string": + $p = $this->compileValue($p); + break; } } @@ -350,6 +466,21 @@ class scssc { return implode($piece); } + protected function hasSelectorPlaceholder($selector) + { + if (!is_array($selector)) return false; + + foreach ($selector as $parts) { + foreach ($parts as $part) { + if ('%' == $part[0]) { + return true; + } + } + } + + return false; + } + protected function compileChildren($stms, $out) { foreach ($stms as $stm) { $ret = $this->compileChild($stm, $out); @@ -365,13 +496,13 @@ class scssc { foreach ($query as $q) { switch ($q[0]) { case "mediaType": - $parts[] = implode(" ", array_slice($q, 1)); + $parts[] = implode(" ", array_map(array($this, "compileValue"), array_slice($q, 1))); break; case "mediaExp": if (isset($q[2])) { - $parts[] = "($q[1]" . $this->formatter->assignSeparator . $this->compileValue($q[2]) . ")"; + $parts[] = "(". $this->compileValue($q[1]) . $this->formatter->assignSeparator . $this->compileValue($q[2]) . ")"; } else { - $parts[] = "($q[1])"; + $parts[] = "(" . $this->compileValue($q[1]) . ")"; } break; } @@ -398,7 +529,8 @@ class scssc { return true; } return false; - } if ($rawPath[0] == "list") { + } + if ($rawPath[0] == "list") { // handle a list of strings if (count($rawPath[2]) == 0) return false; foreach ($rawPath[2] as $path) { @@ -417,6 +549,9 @@ class scssc { // return a value to halt execution protected function compileChild($child, $out) { + $this->sourcePos = isset($child[-1]) ? $child[-1] : -1; + $this->sourceParser = isset($child[-2]) ? $child[-2] : $this->parser; + switch ($child[0]) { case "import": list(,$rawPath) = $child; @@ -446,15 +581,31 @@ class scssc { list(,$name, $value) = $child; if ($name[0] == "var") { $isDefault = !empty($child[3]); - if (!$isDefault || $this->get($name[1], true) === true) { + + if ($isDefault) { + $existingValue = $this->get($name[1], true); + $shouldSet = $existingValue === true || $existingValue == self::$null; + } + + if (!$isDefault || $shouldSet) { $this->set($name[1], $this->reduce($value)); } break; } + // if the value reduces to null from something else then + // the property should be discarded + if ($value[0] != "null") { + $value = $this->reduce($value); + if ($value[0] == "null") { + break; + } + } + + $compiledValue = $this->compileValue($value); $out->lines[] = $this->formatter->property( - $this->compileValue($child[1]), - $this->compileValue($child[2])); + $this->compileValue($name), + $compiledValue); break; case "comment": $out->lines[] = $child[1]; @@ -474,12 +625,12 @@ class scssc { break; case "if": list(, $if) = $child; - if ($this->reduce($if->cond, true) != self::$false) { + if ($this->isTruthy($this->reduce($if->cond, true))) { return $this->compileChildren($if->children, $out); } else { foreach ($if->cases as $case) { if ($case->type == "else" || - $case->type == "elseif" && ($this->reduce($case->cond) != self::$false)) + $case->type == "elseif" && $this->isTruthy($this->reduce($case->cond))) { return $this->compileChildren($case->children, $out); } @@ -490,7 +641,7 @@ class scssc { return $this->reduce($child[1], true); case "each": list(,$each) = $child; - $list = $this->reduce($this->coerceList($each->list)); + $list = $this->coerceList($this->reduce($each->list)); foreach ($list[2] as $item) { $this->pushEnv(); $this->set($each->var, $item); @@ -501,7 +652,7 @@ class scssc { break; case "while": list(,$while) = $child; - while ($this->reduce($while->cond, true) != self::$false) { + while ($this->isTruthy($this->reduce($while->cond, true))) { $ret = $this->compileChildren($while->children, $out); if ($ret) return $ret; } @@ -547,12 +698,17 @@ class scssc { case "include": // including a mixin list(,$name, $argValues, $content) = $child; $mixin = $this->get(self::$namespaces["mixin"] . $name, false); - if (!$mixin) break; // throw error? + if (!$mixin) { + $this->throwError("Undefined mixin $name"); + } $callingScope = $this->env; // push scope, apply args $this->pushEnv(); + if ($this->env->depth > 0) { + $this->env->depth--; + } if (!is_null($content)) { $content->scope = $callingScope; @@ -573,12 +729,14 @@ class scssc { case "mixin_content": $content = $this->get(self::$namespaces["special"] . "content"); if (is_null($content)) { - throw new \Exception("Unexpected @content inside of mixin"); + $this->throwError("Expected @content inside of mixin"); } - $this->storeEnv = $content->scope; - foreach ($content->children as $child) { + $this->storeEnv = ($child[0] == 'include' || $child[0] == 'block') + ? null + : $content->scope; + $this->compileChild($child, $out); } @@ -591,20 +749,24 @@ class scssc { fwrite(STDERR, "Line $line DEBUG: $value\n"); break; default: - throw new exception("unknown child type: $child[0]"); + $this->throwError("unknown child type: $child[0]"); } } protected function expToString($exp) { list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp; - $content = array($left); + $content = array($this->reduce($left)); if ($whiteLeft) $content[] = " "; $content[] = $op; if ($whiteRight) $content[] = " "; - $content[] = $right; + $content[] = $this->reduce($right); return array("string", "", $content); } + protected function isTruthy($value) { + return $value != self::$false && $value != self::$null; + } + // should $value cause its operand to eval protected function shouldEval($value) { switch ($value[0]) { @@ -662,7 +824,7 @@ class scssc { $left[0] == "number" && $right[0] == "number") { if ($opName == "mod" && $right[2] != "") { - throw new \Exception(sprintf('Cannot modulo by a number with units: %s%s.', $right[1], $right[2])); + $this->throwError("Cannot modulo by a number with units: $right[1]$right[2]."); } $unitChange = true; @@ -796,6 +958,27 @@ class scssc { } } + public function normalizeValue($value) { + $value = $this->coerceForExpression($this->reduce($value)); + list($type) = $value; + + switch ($type) { + case "list": + $value = $this->extractInterpolation($value); + if ($value[0] != "list") { + return array("keyword", $this->compileValue($value)); + } + foreach ($value[2] as $key => $item) { + $value[2][$key] = $this->normalizeValue($item); + } + return $value; + case "number": + return $this->normalizeNumber($value); + default: + return $value; + } + } + // just does physical lengths for now protected function normalizeNumber($number) { list(, $value, $unit) = $number; @@ -887,12 +1070,16 @@ class scssc { break; case '/': if ($rval == 0) { - throw new exception("color: Can't divide by zero"); + $this->throwError("color: Can't divide by zero"); } $out[] = $lval / $rval; break; + case "==": + return $this->op_eq($left, $right); + case "!=": + return $this->op_neq($left, $right); default: - throw new exception("color: unknow op $op"); + $this->throwError("color: unknown op $op"); } } @@ -948,6 +1135,19 @@ class scssc { return $thing ? self::$true : self::$false; } + /** + * Compiles a primitive value into a CSS property value. + * + * Values in scssphp are typed by being wrapped in arrays, their format is + * typically: + * + * array(type, contents [, additional_contents]*) + * + * The input is expected to be reduced. This function will not work on + * things like expressions and variables. + * + * @param array $value + */ protected function compileValue($value) { $value = $this->reduce($value); @@ -990,10 +1190,14 @@ class scssc { if ($value[0] != "list") return $this->compileValue($value); list(, $delim, $items) = $value; - foreach ($items as &$item) { - $item = $this->compileValue($item); + + $filtered = array(); + foreach ($items as $item) { + if ($item[0] == "null") continue; + $filtered[] = $this->compileValue($item); } - return implode("$delim ", $items); + + return implode("$delim ", $filtered); case "interpolated": # node created by extractInterpolation list(, $interpolate, $left, $right) = $value; list(,, $whiteLeft, $whiteRight) = $interpolate; @@ -1011,14 +1215,20 @@ class scssc { // strip quotes if it's a string $reduced = $this->reduce($exp); - if ($reduced[0] == "string") { - $reduced = array("keyword", - $this->compileStringContent($reduced)); + switch ($reduced[0]) { + case "string": + $reduced = array("keyword", + $this->compileStringContent($reduced)); + break; + case "null": + $reduced = array("keyword", ""); } return $this->compileValue($reduced); + case "null": + return "null"; default: - throw new exception("unknown value type: $type"); + $this->throwError("unknown value type: $type"); } } @@ -1049,28 +1259,28 @@ class scssc { } // find the final set of selectors - protected function multiplySelectors($env, $childSelectors = null) { - if (is_null($env)) { - return $childSelectors; - } - - // skip env, has no selectors - if (empty($env->selectors)) { - return $this->multiplySelectors($env->parent, $childSelectors); - } + protected function multiplySelectors($env) { + $envs = array(); + while (null !== $env) { + if (!empty($env->selectors)) { + $envs[] = $env; + } + $env = $env->parent; + }; - if (is_null($childSelectors)) { - $selectors = $env->selectors; - } else { + $selectors = array(); + $parentSelectors = array(array()); + while ($env = array_pop($envs)) { $selectors = array(); - foreach ($env->selectors as $parent) { - foreach ($childSelectors as $child) { - $selectors[] = $this->joinSelectors($parent, $child); + foreach ($env->selectors as $selector) { + foreach ($parentSelectors as $parent) { + $selectors[] = $this->joinSelectors($parent, $selector); } } + $parentSelectors = $selectors; } - return $this->multiplySelectors($env->parent, $selectors); + return $selectors; } // looks for & to replace, or append parent before child @@ -1142,31 +1352,69 @@ class scssc { } protected function applyArguments($argDef, $argValues) { - $argValues = (array)$argValues; + $hasVariable = false; + $args = array(); + foreach ($argDef as $i => $arg) { + list($name, $default, $isVariable) = $argDef[$i]; + $args[$name] = array($i, $name, $default, $isVariable); + $hasVariable |= $isVariable; + } $keywordArgs = array(); + $deferredKeywordArgs = array(); $remaining = array(); - // assign the keyword args - foreach ($argValues as $arg) { + foreach ((array) $argValues as $arg) { if (!empty($arg[0])) { - $keywordArgs[$arg[0][1]] = $arg[1]; + if (!isset($args[$arg[0][1]])) { + if ($hasVariable) { + $deferredKeywordArgs[$arg[0][1]] = $arg[1]; + } else { + $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]); + } + } elseif ($args[$arg[0][1]][0] < count($remaining)) { + $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]); + } else { + $keywordArgs[$arg[0][1]] = $arg[1]; + } + } elseif (count($keywordArgs)) { + $this->throwError('Positional arguments must come before keyword arguments.'); + } elseif ($arg[2] == true) { + $val = $this->reduce($arg[1], true); + if ($val[0] == "list") { + foreach ($val[2] as $name => $item) { + if (!is_numeric($name)) { + $keywordArgs[$name] = $item; + } else { + $remaining[] = $item; + } + } + } else { + $remaining[] = $val; + } } else { $remaining[] = $arg[1]; } } - foreach ($argDef as $i => $arg) { - list($name, $default) = $arg; - - if (isset($remaining[$i])) { + foreach ($args as $arg) { + list($i, $name, $default, $isVariable) = $arg; + if ($isVariable) { + $val = array("list", ",", array()); + for ($count = count($remaining); $i < $count; $i++) { + $val[2][] = $remaining[$i]; + } + foreach ($deferredKeywordArgs as $itemName => $item) { + $val[2][$itemName] = $item; + } + } elseif (isset($remaining[$i])) { $val = $remaining[$i]; } elseif (isset($keywordArgs[$name])) { $val = $keywordArgs[$name]; } elseif (!empty($default)) { $val = $default; } else { - $val = self::$defaultValue; + $this->throwError("Missing argument $$name"); } $this->set($name, $this->reduce($val, true), true); @@ -1174,7 +1422,7 @@ class scssc { } protected function pushEnv($block=null) { - $env = new stdclass; + $env = new stdClass; $env->parent = $this->env; $env->store = array(); $env->block = $block; @@ -1194,6 +1442,7 @@ class scssc { protected function set($name, $value, $shadow=false) { $name = $this->normalizeName($name); + if ($shadow) { $this->setRaw($name, $value); } else { @@ -1226,7 +1475,7 @@ class scssc { if (isset($env->store[$name])) { return $env->store[$name]; - } elseif (!is_null($env->parent)) { + } elseif (isset($env->parent)) { return $this->get($name, $defaultValue, $env->parent); } @@ -1270,7 +1519,7 @@ class scssc { $tree = $this->importCache[$realPath]; } else { $code = file_get_contents($path); - $parser = new scss_parser($path); + $parser = new scss_parser($path, false); $tree = $parser->parse($code); $this->parsedFiles[] = $path; @@ -1336,7 +1585,7 @@ class scssc { $val = $this->reduce($val, true); } $returnValue = call_user_func($f, $sorted, $this); - } else if (isset($this->userFunctions[$name])) { + } elseif (isset($this->userFunctions[$name])) { // see if we can find a user function $fn = $this->userFunctions[$name]; @@ -1420,7 +1669,7 @@ class scssc { $name = $value[1]; if (isset(self::$cssColors[$name])) { list($r, $g, $b) = explode(',', self::$cssColors[$name]); - return array('color', $r, $g, $b); + return array('color', (int) $r, (int) $g, (int) $b); } return null; } @@ -1438,14 +1687,20 @@ class scssc { return null; } + protected function assertList($value) { + if ($value[0] != "list") + $this->throwError("expecting list"); + return $value; + } + protected function assertColor($value) { if ($color = $this->coerceColor($value)) return $color; - throw new exception("expecting color"); + $this->throwError("expecting color"); } protected function assertNumber($value) { if ($value[0] != "number") - throw new exception("expecting number"); + $this->throwError("expecting number"); return $value[1]; } @@ -1469,74 +1724,71 @@ class scssc { return $c; } - function toHSL($r, $g, $b) { - $r = $r / 255; - $g = $g / 255; - $b = $b / 255; + function toHSL($red, $green, $blue) { + $r = $red / 255; + $g = $green / 255; + $b = $blue / 255; $min = min($r, $g, $b); $max = max($r, $g, $b); + $d = $max - $min; + $l = ($min + $max) / 2; - $L = ($min + $max) / 2; if ($min == $max) { - $S = $H = 0; + $s = $h = 0; } else { - if ($L < 0.5) - $S = ($max - $min)/($max + $min); + if ($l < 0.5) + $s = $d / (2 * $l); else - $S = ($max - $min)/(2.0 - $max - $min); - - if ($r == $max) $H = ($g - $b)/($max - $min); - elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min); - elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min); + $s = $d / (2 - 2 * $l); + if ($r == $max) + $h = 60 * ($g - $b) / $d; + elseif ($g == $max) + $h = 60 * ($b - $r) / $d + 120; + elseif ($b == $max) + $h = 60 * ($r - $g) / $d + 240; } - return array('hsl', - ($H < 0 ? $H + 6 : $H)*60, - $S*100, - $L*100, - ); + return array('hsl', fmod($h, 360), $s * 100, $l * 100); } - function toRGB_helper($comp, $temp1, $temp2) { - if ($comp < 0) $comp += 1.0; - elseif ($comp > 1) $comp -= 1.0; + function hueToRGB($m1, $m2, $h) { + if ($h < 0) + $h += 1; + elseif ($h > 1) + $h -= 1; - if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp; - if (2 * $comp < 1) return $temp2; - if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6; + if ($h * 6 < 1) + return $m1 + ($m2 - $m1) * $h * 6; - return $temp1; - } + if ($h * 2 < 1) + return $m2; - // H from 0 to 360, S and L from 0 to 100 - function toRGB($H, $S, $L) { - $H = $H % 360; - if ($H < 0) $H += 360; + if ($h * 3 < 2) + return $m1 + ($m2 - $m1) * (2/3 - $h) * 6; - $S = min(100, max(0, $S)); - $L = min(100, max(0, $L)); + return $m1; + } - $H = $H / 360; - $S = $S / 100; - $L = $L / 100; + // H from 0 to 360, S and L from 0 to 100 + function toRGB($hue, $saturation, $lightness) { + if ($hue < 0) { + $hue += 360; + } - if ($S == 0) { - $r = $g = $b = $L; - } else { - $temp2 = $L < 0.5 ? - $L*(1.0 + $S) : - $L + $S - $L * $S; + $h = $hue / 360; + $s = min(100, max(0, $saturation)) / 100; + $l = min(100, max(0, $lightness)) / 100; - $temp1 = 2.0 * $L - $temp2; + $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s; + $m1 = $l * 2 - $m2; - $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2); - $g = $this->toRGB_helper($H, $temp1, $temp2); - $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2); - } + $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255; + $g = $this->hueToRGB($m1, $m2, $h) * 255; + $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255; - $out = array('color', $r*255, $g*255, $b*255); + $out = array('color', $r, $g, $b); return $out; } @@ -1549,6 +1801,20 @@ class scssc { return $t; } + protected static $lib_index = array("list", "value"); + protected function lib_index($args) { + list($list, $value) = $args; + $list = $this->assertList($list); + + $values = array(); + foreach ($list[2] as $item) { + $values[] = $this->normalizeValue($item); + } + $key = array_search($this->normalizeValue($value), $values); + + return false === $key ? false : $key + 1; + } + protected static $lib_rgb = array("red", "green", "blue"); protected function lib_rgb($args) { list($r,$g,$b) = $args; @@ -1687,11 +1953,13 @@ class scssc { } // this might be the IE function, so return value unchanged - return array("function", "alpha", array("list", ",", $args)); + return null; } protected static $lib_opacity = array("color"); protected function lib_opacity($args) { + $value = $args[0]; + if ($value[0] === 'number') return null; return $this->lib_alpha($args); } @@ -1708,11 +1976,11 @@ class scssc { $weight = $this->coercePercent($weight); } - $first_a = isset($first[4]) ? $first[4] : 1; - $second_a = isset($second[4]) ? $second[4] : 1; + $firstAlpha = isset($first[4]) ? $first[4] : 1; + $secondAlpha = isset($second[4]) ? $second[4] : 1; $w = $weight * 2 - 1; - $a = $first_a - $second_a; + $a = $firstAlpha - $secondAlpha; $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0; $w2 = 1.0 - $w1; @@ -1723,8 +1991,8 @@ class scssc { $w1 * $first[3] + $w2 * $second[3], ); - if ($first_a != 1.0 || $second_a != 1.0) { - $new[] = $first_a * $weight + $second_a * ($weight - 1); + if ($firstAlpha != 1.0 || $secondAlpha != 1.0) { + $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1); } return $this->fixColor($new); @@ -1766,7 +2034,6 @@ class scssc { return array("number", $hsl[3], "%"); } - protected function adjustHsl($color, $idx, $amount) { $hsl = $this->toHSL($color[1], $color[2], $color[3]); $hsl[$idx] += $amount; @@ -1798,7 +2065,9 @@ class scssc { protected static $lib_saturate = array("color", "amount"); protected function lib_saturate($args) { - $color = $this->assertColor($args[0]); + $value = $args[0]; + if ($value[0] === 'number') return null; + $color = $this->assertColor($value); $amount = 100*$this->coercePercent($args[1]); return $this->adjustHsl($color, 2, $amount); } @@ -1812,7 +2081,9 @@ class scssc { protected static $lib_grayscale = array("color"); protected function lib_grayscale($args) { - return $this->adjustHsl($this->assertColor($args[0]), 2, -100); + $value = $args[0]; + if ($value[0] === 'number') return null; + return $this->adjustHsl($this->assertColor($value), 2, -100); } protected static $lib_complement = array("color"); @@ -1822,14 +2093,15 @@ class scssc { protected static $lib_invert = array("color"); protected function lib_invert($args) { - $color = $this->assertColor($args[0]); + $value = $args[0]; + if ($value[0] === 'number') return null; + $color = $this->assertColor($value); $color[1] = 255 - $color[1]; $color[2] = 255 - $color[2]; $color[3] = 255 - $color[3]; return $color; } - // increases opacity by amount protected static $lib_opacify = array("color", "amount"); protected function lib_opacify($args) { @@ -1942,14 +2214,14 @@ class scssc { $numbers = array(); foreach ($args as $key => $item) { if ('number' != $item[0]) { - throw new Exception(sprintf('%s is not a number', $item[0])); + $this->throwError("%s is not a number", $item[0]); } $number = $this->normalizeNumber($item); if (null === $unit) { $unit = $number[2]; } elseif ($unit !== $number[2]) { - throw new \Exception(sprintf('Incompatible units: "%s" and "%s".', $originalUnit, $item[2])); + $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item[2]); } $originalUnit = $item[2]; @@ -2002,6 +2274,27 @@ class scssc { return array("list", $sep, array_merge($list1[2], array($value))); } + protected function lib_zip($args) { + foreach ($args as $arg) { + $this->assertList($arg); + } + + $lists = array(); + $firstList = array_shift($args); + foreach ($firstList[2] as $key => $item) { + $list = array("list", "", array($item)); + foreach ($args as $arg) { + if (isset($arg[2][$key])) { + $list[2][] = $arg[2][$key]; + } else { + break 2; + } + } + $lists[] = $list; + } + + return array("list", ",", $lists); + } protected static $lib_type_of = array("value"); protected function lib_type_of($args) { @@ -2037,10 +2330,29 @@ class scssc { return $value[0] == "number" && empty($value[2]); } - protected static $lib_comparable = array("number-1", "number-2"); protected function lib_comparable($args) { - return true; // TODO: THIS + list($number1, $number2) = $args; + if (!isset($number1[0]) || $number1[0] != "number" || !isset($number2[0]) || $number2[0] != "number") { + $this->throwError('Invalid argument(s) for "comparable"'); + } + + $number1 = $this->normalizeNumber($number1); + $number2 = $this->normalizeNumber($number2); + + return $number1[2] == $number2[2] || $number1[2] == "" || $number2[2] == ""; + } + + protected function throwError($msg = null) { + if (func_num_args() > 1) { + $msg = call_user_func_array("sprintf", func_get_args()); + } + + if ($this->sourcePos >= 0 && isset($this->sourceParser)) { + $this->sourceParser->throwParseError($msg, $this->sourcePos); + } + + throw new Exception($msg); } static protected $cssColors = array( @@ -2194,6 +2506,11 @@ class scssc { ); } +/** + * SCSS parser + * + * @author Leaf Corcoran <leafot@gmail.com> + */ class scss_parser { static protected $precedence = array( "or" => 0, @@ -2225,8 +2542,9 @@ class scss_parser { static protected $commentMultiLeft = "/*"; static protected $commentMultiRight = "*/"; - function __construct($sourceName = null) { + function __construct($sourceName = null, $rootParser = true) { $this->sourceName = $sourceName; + $this->rootParser = $rootParser; if (empty(self::$operatorStr)) { self::$operatorStr = $this->makeOperatorStr(self::$operators); @@ -2268,6 +2586,45 @@ class scss_parser { return $this->env; } + /** + * Parse a single chunk off the head of the buffer and append it to the + * current parse environment. + * + * Returns false when the buffer is empty, or when there is an error. + * + * This function is called repeatedly until the entire document is + * parsed. + * + * This parser is most similar to a recursive descent parser. Single + * functions represent discrete grammatical rules for the language, and + * they are able to capture the text that represents those rules. + * + * Consider the function scssc::keyword(). (All parse functions are + * structured the same.) + * + * The function takes a single reference argument. When calling the + * function it will attempt to match a keyword on the head of the buffer. + * If it is successful, it will place the keyword in the referenced + * argument, advance the position in the buffer, and return true. If it + * fails then it won't advance the buffer and it will return false. + * + * All of these parse functions are powered by scssc::match(), which behaves + * the same way, but takes a literal regular expression. Sometimes it is + * more convenient to use match instead of creating a new function. + * + * Because of the format of the functions, to parse an entire string of + * grammatical rules, you can chain them together using &&. + * + * But, if some of the rules in the chain succeed before one fails, then + * the buffer position will be left at an invalid state. In order to + * avoid this, scssc::seek() is used to remember and set buffer positions. + * + * Before parsing a chain, use $s = $this->seek() to remember the current + * position into $s. Then if a chain fails, use $this->seek($s) to + * go back where we started. + * + * @return boolean + */ protected function parseChunk() { $s = $this->seek(); @@ -2309,7 +2666,7 @@ class scss_parser { $include = $this->pushSpecialBlock("include"); $include->child = $child; } else { - $this->append($child); + $this->append($child, $s); } return true; @@ -2321,7 +2678,7 @@ class scss_parser { $this->valueList($importPath) && $this->end()) { - $this->append(array("import", $importPath)); + $this->append(array("import", $importPath), $s); return true; } else { $this->seek($s); @@ -2331,19 +2688,19 @@ class scss_parser { $this->selectors($selector) && $this->end()) { - $this->append(array("extend", $selector)); + $this->append(array("extend", $selector), $s); return true; } else { $this->seek($s); } if ($this->literal("@function") && - $this->keyword($fn_name) && + $this->keyword($fnName) && $this->argumentDef($args) && $this->literal("{")) { $func = $this->pushSpecialBlock("function"); - $func->name = $fn_name; + $func->name = $fnName; $func->args = $args; return true; } else { @@ -2351,7 +2708,7 @@ class scss_parser { } if ($this->literal("@return") && $this->valueList($retVal) && $this->end()) { - $this->append(array("return", $retVal)); + $this->append(array("return", $retVal), $s); return true; } else { $this->seek($s); @@ -2413,14 +2770,14 @@ class scss_parser { if (($this->literal("@debug") || $this->literal("@warn")) && $this->valueList($value) && $this->end()) { - $this->append(array("debug", $value, $s)); + $this->append(array("debug", $value, $s), $s); return true; } else { $this->seek($s); } if ($this->literal("@content") && $this->end()) { - $this->append(array("mixin_content")); + $this->append(array("mixin_content"), $s); return true; } else { $this->seek($s); @@ -2450,7 +2807,7 @@ class scss_parser { if ($this->literal("@charset") && $this->valueList($charset) && $this->end()) { - $this->append(array("charset", $charset)); + $this->append(array("charset", $charset), $s); return true; } else { $this->seek($s); @@ -2479,7 +2836,7 @@ class scss_parser { $this->end()) { $name = array("string", "", array($name)); - $this->append(array("assign", $name, $value)); + $this->append(array("assign", $name, $value), $s); return true; } else { $this->seek($s); @@ -2490,17 +2847,9 @@ class scss_parser { $this->literal(":") && $this->valueList($value) && $this->end()) { - $defaultVar = false; // check for !default - if ($value[0] == "list") { - $def = end($value[2]); - if ($def[0] == "keyword" && $def[1] == "!default") { - array_pop($value[2]); - $value = $this->flattenList($value); - $defaultVar = true; - } - } - $this->append(array("assign", $name, $value, $defaultVar)); + $defaultVar = $value[0] == "list" && $this->stripDefault($value); + $this->append(array("assign", $name, $value, $defaultVar), $s); return true; } else { $this->seek($s); @@ -2527,7 +2876,7 @@ class scss_parser { if ($this->propertyName($name) && $this->literal(":")) { $foundSomething = false; if ($this->valueList($value)) { - $this->append(array("assign", $name, $value)); + $this->append(array("assign", $name, $value), $s); $foundSomething = true; } @@ -2555,10 +2904,10 @@ class scss_parser { $include = $block->child; unset($block->child); $include[3] = $block; - $this->append($include); - } else if (empty($block->dontAppend)) { + $this->append($include, $s); + } elseif (empty($block->dontAppend)) { $type = isset($block->type) ? $block->type : "block"; - $this->append(array($type, $block)); + $this->append(array($type, $block), $s); } return true; } @@ -2573,19 +2922,35 @@ class scss_parser { return false; } + protected function stripDefault(&$value) { + $def = end($value[2]); + if ($def[0] == "keyword" && $def[1] == "!default") { + array_pop($value[2]); + $value = $this->flattenList($value); + return true; + } + + if ($def[0] == "list") { + return $this->stripDefault($value[2][count($value[2]) - 1]); + } + + return false; + } + protected function literal($what, $eatWhitespace = null) { if (is_null($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault; - // this is here mainly prevent notice from { } string accessor - if ($this->count >= strlen($this->buffer)) return false; - // shortcut on single letter - if (!$eatWhitespace && strlen($what) == 1) { - if ($this->buffer{$this->count} == $what) { - $this->count++; - return true; + if (!isset($what[1]) && isset($this->buffer[$this->count])) { + if ($this->buffer[$this->count] == $what) { + if (!$eatWhitespace) { + $this->count++; + return true; + } + // goes below... + } else { + return false; } - else return false; } return $this->match($this->preg_quote($what), $m, $eatWhitespace); @@ -2594,7 +2959,7 @@ class scss_parser { // tree builders protected function pushBlock($selectors) { - $b = new stdclass; + $b = new stdClass; $b->parent = $this->env; // not sure if we need this yet $b->selectors = $selectors; @@ -2621,7 +2986,11 @@ class scss_parser { return $old; } - protected function append($statement) { + protected function append($statement, $pos=null) { + if ($pos !== null) { + $statement[-1] = $pos; + if (!$this->rootParser) $statement[-2] = $this; + } $this->env->children[] = $statement; } @@ -2644,20 +3013,23 @@ class scss_parser { $expressions = null; $parts = array(); - if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) { + if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->mixedKeyword($mediaType)) { $prop = array("mediaType"); - if (isset($only)) $prop[] = "only"; - if (isset($not)) $prop[] = "not"; - $prop[] = $mediaType; + if (isset($only)) $prop[] = array("keyword", "only"); + if (isset($not)) $prop[] = array("keyword", "not"); + $media = array("list", "", array()); + foreach ((array)$mediaType as $type) { + if (is_array($type)) { + $media[2][] = $type; + } else { + $media[2][] = array("keyword", $type); + } + } + $prop[] = $media; $parts[] = $prop; - } else { - $this->seek($s); } - - if (!empty($mediaType) && !$this->literal("and")) { - // ~ - } else { + if (empty($parts) || $this->literal("and")) { $this->genericList($expressions, "mediaExpression", "and", false); if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]); } @@ -2670,7 +3042,7 @@ class scss_parser { $s = $this->seek(); $value = null; if ($this->literal("(") && - $this->keyword($feature) && + $this->expression($feature) && ($this->literal(":") && $this->expression($value) || true) && $this->literal(")")) { @@ -2701,7 +3073,13 @@ class scss_parser { } if ($this->genericList($value, "expression")) { - $out = array($keyword, $value); + $out = array($keyword, $value, false); + $s = $this->seek(); + if ($this->literal("...")) { + $out[2] = true; + } else { + $this->seek($s); + } return true; } @@ -2710,11 +3088,11 @@ class scss_parser { protected function valueList(&$out) { - return $this->genericList($out, "commaList"); + return $this->genericList($out, "spaceList", ","); } - protected function commaList(&$out) { - return $this->genericList($out, "expression", ","); + protected function spaceList(&$out) { + return $this->genericList($out, "expression"); } protected function genericList(&$out, $parseItem, $delim="", $flatten=true) { @@ -2838,7 +3216,11 @@ class scss_parser { if ($this->progid($out)) return true; if ($this->keyword($keyword)) { - $out = array("keyword", $keyword); + if ($keyword == "null") { + $out = array("null"); + } else { + $out = array("keyword", $keyword); + } return true; } @@ -2890,7 +3272,12 @@ class scss_parser { if ($this->keyword($name, false) && $this->literal("(")) { - if ($name != "expression" && false == preg_match("/^(-[a-z]+-)?calc$/", $name)) { + if ($name == "alpha" && $this->argumentList($args)) { + $func = array("function", $name, array("string", "", $args)); + return true; + } + + if ($name != "expression" && !preg_match("/^(-[a-z]+-)?calc$/", $name)) { $ss = $this->seek(); if ($this->argValues($args) && $this->literal(")")) { $func = array("fncall", $name, $args); @@ -2916,21 +3303,64 @@ class scss_parser { return false; } + protected function argumentList(&$out) { + $s = $this->seek(); + $this->literal("("); + + $args = array(); + while ($this->keyword($var)) { + $ss = $this->seek(); + + if ($this->literal("=") && $this->expression($exp)) { + $args[] = array("string", "", array($var."=")); + $arg = $exp; + } else { + break; + } + + $args[] = $arg; + + if (!$this->literal(",")) break; + + $args[] = array("string", "", array(", ")); + } + + if (!$this->literal(")")) { + $this->seek($s); + return false; + } + + $out = $args; + return true; + } + protected function argumentDef(&$out) { $s = $this->seek(); $this->literal("("); $args = array(); while ($this->variable($var)) { - $arg = array($var[1], null); + $arg = array($var[1], null, false); $ss = $this->seek(); - if ($this->literal(":") && $this->expression($defaultVal)) { + if ($this->literal(":") && $this->genericList($defaultVal, "expression")) { $arg[1] = $defaultVal; } else { $this->seek($ss); } + $ss = $this->seek(); + if ($this->literal("...")) { + $sss = $this->seek(); + if (!$this->literal(")")) { + $this->throwParseError("... has to be after the final argument"); + } + $arg[2] = true; + $this->seek($sss); + } else { + $this->seek($ss); + } + $args[] = $arg; if (!$this->literal(",")) break; } @@ -2990,15 +3420,10 @@ class scss_parser { } $content = array(); - - // look for either ending delim , escape, or string interpolation - $patt = '([^\n]*?)(#\{|\\\\|' . - $this->preg_quote($delim).')'; - $oldWhite = $this->eatWhiteDefault; $this->eatWhiteDefault = false; - while ($this->match($patt, $m, false)) { + while ($this->matchString($m, $delim)) { $content[] = $m[1]; if ($m[2] == "#{") { $this->count -= strlen($m[2]); @@ -3056,6 +3481,10 @@ class scss_parser { if (count($parts) == 0) return false; + if ($this->eatWhiteDefault) { + $this->whitespace(); + } + $out = $parts; return true; } @@ -3075,7 +3504,7 @@ class scss_parser { $content = array(); while ($this->match($patt, $m, false)) { - if (!empty($m[1])) { + if (isset($m[1]) && $m[1] !== '') { $content[] = $m[1]; if ($nestingOpen) { $nestingLevel += substr_count($m[1], $nestingOpen); @@ -3209,7 +3638,7 @@ class scss_parser { return true; } - // whitepsace separated list of selectorSingle + // whitespace separated list of selectorSingle protected function selector(&$out) { $selector = array(); @@ -3219,6 +3648,8 @@ class scss_parser { } elseif ($this->selectorSingle($part)) { $selector[] = $part; $this->whitespace(); + } elseif ($this->match('\/[^\/]+\/', $m)) { + $selector[] = array($m[0]); } else { break; } @@ -3234,7 +3665,7 @@ class scss_parser { } // the parts that make up - // div[yes=no]#something.hello.world:nth-child(-2n+1) + // div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder protected function selectorSingle(&$out) { $oldWhite = $this->eatWhiteDefault; $this->eatWhiteDefault = false; @@ -3285,6 +3716,12 @@ class scss_parser { continue; } + if ($this->literal('%', false) && $this->placeholder($placeholder)) { + $parts[] = '%'; + $parts[] = $placeholder; + continue; + } + if ($this->literal("#", false)) { $parts[] = "#"; continue; @@ -3395,11 +3832,19 @@ class scss_parser { return false; } + protected function placeholder(&$placeholder) { + if ($this->match('([\w\-_]+)', $m)) { + $placeholder = $m[1]; + return true; + } + return false; + } + // consume an end of statement delimiter protected function end() { if ($this->literal(';')) { return true; - } elseif ($this->count == strlen($this->buffer) || $this->buffer{$this->count} == '}') { + } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') { // if there is end of file or a closing block next then we don't need a ; return true; } @@ -3421,7 +3866,7 @@ class scss_parser { return true; } - protected function throwParseError($msg = "parse error", $count = null) { + public function throwParseError($msg = "parse error", $count = null) { $count = is_null($count) ? $this->count : $count; $line = $this->getLineNo($count); @@ -3433,9 +3878,9 @@ class scss_parser { } if ($this->peek("(.*?)(\n|$)", $m, $count)) { - throw new exception("$msg: failed at `$m[1]` $loc"); + throw new Exception("$msg: failed at `$m[1]` $loc"); } else { - throw new exception("$msg: $loc"); + throw new Exception("$msg: $loc"); } } @@ -3443,6 +3888,48 @@ class scss_parser { return 1 + substr_count(substr($this->buffer, 0, $pos), "\n"); } + /** + * Match string looking for either ending delim, escape, or string interpolation + * + * {@internal This is a workaround for preg_match's 250K string match limit. }} + * + * @param array $m Matches (passed by reference) + * @param string $delim Delimeter + * + * @return boolean True if match; false otherwise + */ + protected function matchString(&$m, $delim) { + $token = null; + + $end = strpos($this->buffer, "\n", $this->count); + if ($end === false) { + $end = strlen($this->buffer); + } + + // look for either ending delim, escape, or string interpolation + foreach (array('#{', '\\', $delim) as $lookahead) { + $pos = strpos($this->buffer, $lookahead, $this->count); + if ($pos !== false && $pos < $end) { + $end = $pos; + $token = $lookahead; + } + } + + if (!isset($token)) { + return false; + } + + $match = substr($this->buffer, $this->count, $end - $this->count); + $m = array( + $match . $token, + $match, + $token + ); + $this->count = $end + strlen($token); + + return true; + } + // try to match something on head of buffer protected function match($regex, &$out, $eatWhitespace = null) { if (is_null($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault; @@ -3507,6 +3994,11 @@ class scss_parser { } } +/** + * SCSS base formatter + * + * @author Leaf Corcoran <leafot@gmail.com> + */ class scss_formatter { public $indentChar = " "; @@ -3528,7 +4020,7 @@ class scss_formatter { return $name . $this->assignSeparator . $value . ";"; } - public function block($block) { + protected function block($block) { if (empty($block->lines) && empty($block->children)) return; $inner = $pre = $this->indentStr(); @@ -3559,9 +4051,21 @@ class scss_formatter { echo $pre . $this->close . $this->break; } } -} + public function format($block) { + ob_start(); + $this->block($block); + $out = ob_get_clean(); + + return $out; + } +} +/** + * SCSS nested formatter + * + * @author Leaf Corcoran <leafot@gmail.com> + */ class scss_formatter_nested extends scss_formatter { public $close = " }"; @@ -3578,6 +4082,21 @@ class scss_formatter_nested extends scss_formatter { } $children[] = $child; } + + $count = count($children); + for ($i = 0; $i < $count; $i++) { + $depth = $children[$i]->depth; + $j = $i + 1; + if (isset($children[$j]) && $depth < $children[$j]->depth) { + $childDepth = $children[$j]->depth; + for (; $j < $count; $j++) { + if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) { + $children[$j]->depth = $depth + 1; + } + } + } + } + $block->children = $children; // make relative to parent @@ -3587,7 +4106,7 @@ class scss_formatter_nested extends scss_formatter { } } - public function block($block) { + protected function block($block) { if ($block->type == "root") { $this->adjustAllChildren($block); } @@ -3633,6 +4152,11 @@ class scss_formatter_nested extends scss_formatter { } } +/** + * SCSS compressed formatter + * + * @author Leaf Corcoran <leafot@gmail.com> + */ class scss_formatter_compressed extends scss_formatter { public $open = "{"; public $tagSeparator = ","; @@ -3644,37 +4168,86 @@ class scss_formatter_compressed extends scss_formatter { } } +/** + * SCSS server + * + * @author Leaf Corcoran <leafot@gmail.com> + */ class scss_server { - + /** + * Join path components + * + * @param string $left Path component, left of the directory separator + * @param string $right Path component, right of the directory separator + * + * @return string + */ protected function join($left, $right) { - return rtrim($left, "/") . "/" . ltrim($right, "/"); + return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\'); } + /** + * Get name of requested .scss file + * + * @return string|null + */ protected function inputName() { - if (isset($_GET["p"])) return $_GET["p"]; - - if (isset($_SERVER["PATH_INFO"])) return $_SERVER["PATH_INFO"]; - if (isset($_SERVER["DOCUMENT_URI"])) { - return substr($_SERVER["DOCUMENT_URI"], strlen($_SERVER["SCRIPT_NAME"])); + switch (true) { + case isset($_GET['p']): + return $_GET['p']; + case isset($_SERVER['PATH_INFO']): + return $_SERVER['PATH_INFO']; + case isset($_SERVER['DOCUMENT_URI']): + return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME'])); } } + /** + * Get path to requested .scss file + * + * @return string + */ protected function findInput() { - if ($input = $this->inputName()) { + if ($input = $this->inputName() + && strpos($input, '..') === false + && substr($input, -5) === '.scss' + ) { $name = $this->join($this->dir, $input); - if (is_readable($name)) return $name; + + if (is_file($name) && is_readable($name)) { + return $name; + } } + return false; } + /** + * Get path to cached .css file + * + * @return string + */ protected function cacheName($fname) { - return $this->join($this->cacheDir, md5($fname) . ".css"); + return $this->join($this->cacheDir, md5($fname) . '.css'); } + /** + * Get path to cached imports + * + * @return string + */ protected function importsCacheName($out) { - return $out . ".imports"; + return $out . '.imports'; } + /** + * Determine whether .scss file needs to be re-compiled. + * + * @param string $in Input path + * @param string $out Output path + * + * @return boolean True if compile required. + */ protected function needsCompile($in, $out) { if (!is_file($out)) return true; @@ -3692,13 +4265,21 @@ class scss_server { return false; } + /** + * Compile .scss file + * + * @param string $in Input path (.scss) + * @param string $out Output path (.css) + * + * @return string + */ protected function compile($in, $out) { $start = microtime(true); $css = $this->scss->compile(file_get_contents($in), $in); $elapsed = round((microtime(true) - $start), 4); $v = scssc::$VERSION; - $t = date("r"); + $t = date('r'); $css = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css; file_put_contents($out, $css); @@ -3707,17 +4288,20 @@ class scss_server { return $css; } + /** + * Compile requested scss and serve css. Outputs HTTP response. + */ public function serve() { if ($input = $this->findInput()) { $output = $this->cacheName($input); - header("Content-type: text/css"); + header('Content-type: text/css'); if ($this->needsCompile($input, $output)) { try { echo $this->compile($input, $output); - } catch (exception $e) { + } catch (Exception $e) { header('HTTP/1.1 500 Internal Server Error'); - echo "Parse error: " . $e->getMessage() . "\n"; + echo 'Parse error: ' . $e->getMessage() . "\n"; } } else { header('X-SCSS-Cache: true'); @@ -3728,16 +4312,23 @@ class scss_server { } header('HTTP/1.0 404 Not Found'); - header("Content-type: text"); + header('Content-type: text'); $v = scssc::$VERSION; echo "/* INPUT NOT FOUND scss $v */\n"; } + /** + * Constructor + * + * @param string $dir Root directory to .scss files + * @param string $cacheDir Cache directory + * @param \scssc|null $scss SCSS compiler instance + */ public function __construct($dir, $cacheDir=null, $scss=null) { $this->dir = $dir; if (is_null($cacheDir)) { - $cacheDir = $this->join($dir, "scss_cache"); + $cacheDir = $this->join($dir, 'scss_cache'); } $this->cacheDir = $cacheDir; @@ -3750,10 +4341,13 @@ class scss_server { $this->scss = $scss; } + /** + * Helper method to serve compiled scss + * + * @param string $path Root path + */ static public function serveFrom($path) { $server = new self($path); $server->serve(); } } - - |