%PDF- %PDF-
Direktori : /var/www/html/buggydubrovnik.com/wp-content/plugins/coming-soon/app/includes/ |
Current File : /var/www/html/buggydubrovnik.com/wp-content/plugins/coming-soon/app/includes/seedprod_lessc.inc.php |
<?php /** * lessphp v0.3.8 * http://leafo.net/lessphp * * LESS css compiler, adapted from http://seedprod_lesscss.org * * Copyright 2012, Leaf Corcoran <leafot@gmail.com> * Licensed under MIT or GPLv3, see LICENSE */ /** * The less compiler and parser. * * Converting LESS to CSS is a three stage process. The incoming file is parsed * by `seedprod_lessc_parser` into a syntax tree, then it is compiled into another tree * representing the CSS structure by `seedprod_lessc`. The CSS tree is fed into a * formatter, like `seedprod_lessc_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 `seedprod_lessc` is the entry point. * * In summary: * * The `seedprod_lessc` class creates an intstance of the parser, feeds it LESS 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 `seedprod_lessc_parser` class is only concerned with parsing its input. * * The `seedprod_lessc_formatter` takes a CSS tree, and dumps it to a formatted string, * handling things like indentation. */ class seedprod_lessc { public static $VERSION = 'v0.3.8'; protected static $TRUE = array( 'keyword', 'true' ); protected static $FALSE = array( 'keyword', 'false' ); protected $libFunctions = array(); protected $registeredVars = array(); protected $preserveComments = false; public $vPrefix = '@'; // prefix of abstract properties public $mPrefix = '$'; // prefix of abstract blocks public $parentSelector = '&'; public $importDisabled = false; public $importDir = ''; protected $numberPrecision = null; // set to the parser that generated the current line when compiling // so we know how to create error messages protected $sourceParser = null; protected $sourceLoc = null; public static $defaultValue = array( 'keyword', '' ); protected static $nextImportId = 0; // uniquely identify imports // attempts to find the path of an import url, returns null for css files protected function findImport( $url ) { foreach ( (array) $this->importDir as $dir ) { $full = $dir . ( substr( $dir, -1 ) != '/' ? '/' : '' ) . $url; if ( $this->fileExists( $file = $full . '.less' ) || $this->fileExists( $file = $full ) ) { return $file; } } return null; } protected function fileExists( $name ) { return is_file( $name ); } public static function compressList( $items, $delim ) { if ( ! isset( $items[1] ) && isset( $items[0] ) ) { return $items[0]; } else { return array( 'list', $delim, $items ); } } public static function preg_quote( $what ) { return preg_quote( $what, '/' ); } protected function tryImport( $importPath, $parentBlock, $out ) { if ( $importPath[0] == 'function' && $importPath[1] == 'url' ) { $importPath = $this->flattenList( $importPath[2] ); } $str = $this->coerceString( $importPath ); if ( $str === null ) { return false; } $url = $this->compileValue( $this->lib_e( $str ) ); // don't import if it ends in css 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 */' ); } $this->addParsedFile( $realPath ); $parser = $this->makeParser( $realPath ); $root = $parser->parse( file_get_contents( $realPath ) ); // set the parents of all the block props foreach ( $root->props as $prop ) { if ( $prop[0] == 'block' ) { $prop[1]->parent = $parentBlock; } } // copy mixins into scope, set their parents // bring blocks from import into current block // TODO: need to mark the source parser these came from this file foreach ( $root->children as $childName => $child ) { if ( isset( $parentBlock->children[ $childName ] ) ) { $parentBlock->children[ $childName ] = array_merge( $parentBlock->children[ $childName ], $child ); } else { $parentBlock->children[ $childName ] = $child; } } $pi = pathinfo( $realPath ); $dir = $pi['dirname']; list($top, $bottom) = $this->sortProps( $root->props, true ); $this->compileImportedProps( $top, $parentBlock, $out, $parser, $dir ); return array( true, $bottom, $parser, $dir ); } protected function compileImportedProps( $props, $block, $out, $sourceParser, $importDir ) { $oldSourceParser = $this->sourceParser; $oldImport = $this->importDir; // TODO: this is because the importDir api is stupid $this->importDir = (array) $this->importDir; array_unshift( $this->importDir, $importDir ); foreach ( $props as $prop ) { $this->compileProp( $prop, $block, $out ); } $this->importDir = $oldImport; $this->sourceParser = $oldSourceParser; } /** * Recursively compiles a block. * * A block is analogous to a CSS block in most cases. A single LESS document * is encapsulated in a block when parsed, but it does not have parent tags * so all of it's children appear on the root level when compiled. * * Blocks are made up of props and children. * * Props are property instructions, array tuples which describe an action * to be taken, eg. write a property, set a variable, mixin a block. * * The children of a block are just all the blocks that are defined within. * This is used to look up mixins when performing a mixin. * * Compiling the block involves pushing a fresh environment on the stack, * and iterating through the props, compiling each one. * * See seedprod_lessc::compileProp() * */ protected function compileBlock( $block ) { switch ( $block->type ) { case 'root': $this->compileRoot( $block ); break; case null: $this->compileCSSBlock( $block ); break; case 'media': $this->compileMedia( $block ); break; case 'directive': $name = '@' . $block->name; if ( ! empty( $block->value ) ) { $name .= ' ' . $this->compileValue( $this->reduce( $block->value ) ); } $this->compileNestedBlock( $block, array( $name ) ); break; default: $this->throwError( "unknown block type: $block->type\n" ); } } protected function compileCSSBlock( $block ) { $env = $this->pushEnv(); $selectors = $this->compileSelectors( $block->tags ); $env->selectors = $this->multiplySelectors( $selectors ); $out = $this->makeOutputBlock( null, $env->selectors ); $this->scope->children[] = $out; $this->compileProps( $block, $out ); $block->scope = $env; // mixins carry scope with them! $this->popEnv(); } protected function compileMedia( $media ) { $env = $this->pushEnv( $media ); $parentScope = $this->mediaParent( $this->scope ); $query = $this->compileMediaQuery( $this->multiplyMedia( $env ) ); $this->scope = $this->makeOutputBlock( $media->type, array( $query ) ); $parentScope->children[] = $this->scope; $this->compileProps( $media, $this->scope ); if ( count( $this->scope->lines ) > 0 ) { $orphanSelelectors = $this->findClosestSelectors(); if ( ! is_null( $orphanSelelectors ) ) { $orphan = $this->makeOutputBlock( null, $orphanSelelectors ); $orphan->lines = $this->scope->lines; array_unshift( $this->scope->children, $orphan ); $this->scope->lines = array(); } } $this->scope = $this->scope->parent; $this->popEnv(); } protected function mediaParent( $scope ) { while ( ! empty( $scope->parent ) ) { if ( ! empty( $scope->type ) && $scope->type != 'media' ) { break; } $scope = $scope->parent; } return $scope; } protected function compileNestedBlock( $block, $selectors ) { $this->pushEnv( $block ); $this->scope = $this->makeOutputBlock( $block->type, $selectors ); $this->scope->parent->children[] = $this->scope; $this->compileProps( $block, $this->scope ); $this->scope = $this->scope->parent; $this->popEnv(); } protected function compileRoot( $root ) { $this->pushEnv(); $this->scope = $this->makeOutputBlock( $root->type ); $this->compileProps( $root, $this->scope ); $this->popEnv(); } protected function compileProps( $block, $out ) { foreach ( $this->sortProps( $block->props ) as $prop ) { $this->compileProp( $prop, $block, $out ); } } protected function sortProps( $props, $split = false ) { $vars = array(); $imports = array(); $other = array(); foreach ( $props as $prop ) { switch ( $prop[0] ) { case 'assign': if ( isset( $prop[1][0] ) && $prop[1][0] == $this->vPrefix ) { $vars[] = $prop; } else { $other[] = $prop; } break; case 'import': $id = self::$nextImportId++; $prop[] = $id; $imports[] = $prop; $other[] = array( 'import_mixin', $id ); break; default: $other[] = $prop; } } if ( $split ) { return array( array_merge( $vars, $imports ), $other ); } else { return array_merge( $vars, $imports, $other ); } } protected function compileMediaQuery( $queries ) { $compiledQueries = array(); foreach ( $queries as $query ) { $parts = array(); foreach ( $query as $q ) { switch ( $q[0] ) { case 'mediaType': $parts[] = implode( ' ', array_slice( $q, 1 ) ); break; case 'mediaExp': if ( isset( $q[2] ) ) { $parts[] = "($q[1]: " . $this->compileValue( $this->reduce( $q[2] ) ) . ')'; } else { $parts[] = "($q[1])"; } break; } } if ( count( $parts ) > 0 ) { $compiledQueries[] = implode( ' and ', $parts ); } } $out = '@media'; if ( ! empty( $parts ) ) { $out .= ' ' . implode( $this->formatter->selectorSeparator, $compiledQueries ); } return $out; } protected function multiplyMedia( $env, $childQueries = null ) { if ( is_null( $env ) || ! empty( $env->block->type ) && $env->block->type != 'media' ) { return $childQueries; } // plain old block, skip if ( empty( $env->block->type ) ) { return $this->multiplyMedia( $env->parent, $childQueries ); } $out = array(); $queries = $env->block->queries; if ( is_null( $childQueries ) ) { $out = $queries; } else { foreach ( $queries as $parent ) { foreach ( $childQueries as $child ) { $out[] = array_merge( $parent, $child ); } } } return $this->multiplyMedia( $env->parent, $out ); } protected function expandParentSelectors( &$tag, $replace ) { $parts = explode( '$&$', $tag ); $count = 0; foreach ( $parts as &$part ) { $part = str_replace( $this->parentSelector, $replace, $part, $c ); $count += $c; } $tag = implode( $this->parentSelector, $parts ); return $count; } protected function findClosestSelectors() { $env = $this->env; $selectors = null; while ( $env !== null ) { if ( isset( $env->selectors ) ) { $selectors = $env->selectors; break; } $env = $env->parent; } return $selectors; } // multiply $selectors against the nearest selectors in env protected function multiplySelectors( $selectors ) { // find parent selectors $parentSelectors = $this->findClosestSelectors(); if ( is_null( $parentSelectors ) ) { // kill parent reference in top level selector foreach ( $selectors as &$s ) { $this->expandParentSelectors( $s, '' ); } return $selectors; } $out = array(); foreach ( $parentSelectors as $parent ) { foreach ( $selectors as $child ) { $count = $this->expandParentSelectors( $child, $parent ); // don't prepend the parent tag if & was used if ( $count > 0 ) { $out[] = trim( $child ); } else { $out[] = trim( $parent . ' ' . $child ); } } } return $out; } // reduces selector expressions protected function compileSelectors( $selectors ) { $out = array(); foreach ( $selectors as $s ) { if ( is_array( $s ) ) { list(, $value) = $s; $out[] = $this->compileValue( $this->reduce( $value ) ); } else { $out[] = $s; } } return $out; } protected function eq( $left, $right ) { return $left == $right; } protected function patternMatch( $block, $callingArgs ) { // 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 ) ) { $groupPassed = false; foreach ( $block->guards as $guardGroup ) { foreach ( $guardGroup as $guard ) { $this->pushEnv(); $this->zipSetArgs( $block->args, $callingArgs ); $negate = false; if ( $guard[0] == 'negate' ) { $guard = $guard[1]; $negate = true; } $passed = $this->reduce( $guard ) == self::$TRUE; if ( $negate ) { $passed = ! $passed; } $this->popEnv(); if ( $passed ) { $groupPassed = true; } else { $groupPassed = false; break; } } if ( $groupPassed ) { break; } } if ( ! $groupPassed ) { return false; } } $numCalling = count( $callingArgs ); if ( empty( $block->args ) ) { return $block->isVararg || $numCalling == 0; } $i = -1; // no args // try to match by arity or by argument literal foreach ( $block->args as $i => $arg ) { switch ( $arg[0] ) { case 'lit': if ( empty( $callingArgs[ $i ] ) || ! $this->eq( $arg[1], $callingArgs[ $i ] ) ) { return false; } break; case 'arg': // no arg and no default value if ( ! isset( $callingArgs[ $i ] ) && ! isset( $arg[2] ) ) { return false; } break; case 'rest': $i--; // rest can be empty break 2; } } if ( $block->isVararg ) { return true; // not having enough is handled above } else { $numMatched = $i + 1; // greater than becuase default values always match return $numMatched >= $numCalling; } } protected function patternMatchAll( $blocks, $callingArgs ) { $matches = null; foreach ( $blocks as $block ) { if ( $this->patternMatch( $block, $callingArgs ) ) { $matches[] = $block; } } return $matches; } // attempt to find blocks matched by path and args protected function findBlocks( $searchIn, $path, $args, $seen = array() ) { if ( $searchIn == null ) { return null; } if ( isset( $seen[ $searchIn->id ] ) ) { return null; } $seen[ $searchIn->id ] = true; $name = $path[0]; if ( isset( $searchIn->children[ $name ] ) ) { $blocks = $searchIn->children[ $name ]; if ( count( $path ) == 1 ) { $matches = $this->patternMatchAll( $blocks, $args ); if ( ! empty( $matches ) ) { // This will return all blocks that match in the closest // scope that has any matching block, like lessjs return $matches; } } else { $matches = array(); foreach ( $blocks as $subBlock ) { $subMatches = $this->findBlocks( $subBlock, array_slice( $path, 1 ), $args, $seen ); if ( ! is_null( $subMatches ) ) { foreach ( $subMatches as $sm ) { $matches[] = $sm; } } } return count( $matches ) > 0 ? $matches : null; } } if ( $searchIn->parent === $searchIn ) { return null; } return $this->findBlocks( $searchIn->parent, $path, $args, $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; $assignedValues = array(); foreach ( $args as $a ) { if ( $a[0] == 'arg' ) { if ( $i < count( $values ) && ! is_null( $values[ $i ] ) ) { $value = $values[ $i ]; } elseif ( isset( $a[2] ) ) { $value = $a[2]; } else { $value = null; } $value = $this->reduce( $value ); $this->set( $a[1], $value ); $assignedValues[] = $value; } $i++; } // check for a rest $last = end( $args ); if ( is_array( $last ) && $last[0] == 'rest' ) { $rest = array_slice( $values, count( $args ) - 1 ); $this->set( $last[1], $this->reduce( array( 'list', ' ', $rest ) ) ); } $this->env->arguments = $assignedValues; } // compile a prop and update $lines or $blocks appropriately protected function compileProp( $prop, $block, $out ) { // set error position context $this->sourceLoc = isset( $prop[-1] ) ? $prop[-1] : -1; switch ( $prop[0] ) { case 'assign': list(, $name, $value) = $prop; if ( $name[0] == $this->vPrefix ) { $this->set( $name, $value ); } else { $out->lines[] = $this->formatter->property( $name, $this->compileValue( $this->reduce( $value ) ) ); } break; case 'block': list(, $child) = $prop; $this->compileBlock( $child ); break; case 'mixin': list(, $path, $args, $suffix) = $prop; $args = array_map( array( $this, 'reduce' ), (array) $args ); $mixins = $this->findBlocks( $block, $path, $args ); if ( $mixins === null ) { // fwrite(STDERR,"failed to find block: ".implode(" > ", $path)."\n"); break; // throw error here?? } foreach ( $mixins as $mixin ) { $haveScope = false; if ( isset( $mixin->parent->scope ) ) { $haveScope = true; $mixinParentEnv = $this->pushEnv(); $mixinParentEnv->storeParent = $mixin->parent->scope; } $haveArgs = false; if ( isset( $mixin->args ) ) { $haveArgs = true; $this->pushEnv(); $this->zipSetArgs( $mixin->args, $args ); } $oldParent = $mixin->parent; if ( $mixin != $block ) { $mixin->parent = $block; } foreach ( $this->sortProps( $mixin->props ) as $subProp ) { if ( $suffix !== null && $subProp[0] == 'assign' && is_string( $subProp[1] ) && $subProp[1][0] != $this->vPrefix ) { $subProp[2] = array( 'list', ' ', array( $subProp[2], array( 'keyword', $suffix ) ), ); } $this->compileProp( $subProp, $mixin, $out ); } $mixin->parent = $oldParent; if ( $haveArgs ) { $this->popEnv(); } if ( $haveScope ) { $this->popEnv(); } } break; case 'raw': $out->lines[] = $prop[1]; break; case 'directive': list(, $name, $value) = $prop; $out->lines[] = "@$name " . $this->compileValue( $this->reduce( $value ) ) . ';'; break; case 'comment': $out->lines[] = $prop[1]; break; case 'import'; list(, $importPath, $importId) = $prop; $importPath = $this->reduce( $importPath ); if ( ! isset( $this->env->imports ) ) { $this->env->imports = array(); } $result = $this->tryImport( $importPath, $block, $out ); $this->env->imports[ $importId ] = $result === false ? array( false, '@import ' . $this->compileValue( $importPath ) . ';' ) : $result; break; case 'import_mixin': list(,$importId) = $prop; $import = $this->env->imports[ $importId ]; if ( $import[0] === false ) { $out->lines[] = $import[1]; } else { list(, $bottom, $parser, $importDir) = $import; $this->compileImportedProps( $bottom, $block, $out, $parser, $importDir ); } break; default: $this->throwError( "unknown op: {$prop[0]}\n" ); } } /** * Compiles a primitive value into a CSS property value. * * Values in lessphp 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. */ protected function compileValue( $value ) { switch ( $value[0] ) { case 'list': // [1] - delimiter // [2] - array of values return implode( $value[1], array_map( array( $this, 'compileValue' ), $value[2] ) ); case 'raw_color': if ( ! empty( $this->formatter->compressColors ) ) { return $this->compileValue( $this->coerceColor( $value ) ); } return $value[1]; case 'keyword': // [1] - the keyword return $value[1]; case 'number': list(, $num, $unit) = $value; // [1] - the number // [2] - the unit if ( $this->numberPrecision !== null ) { $num = round( $num, $this->numberPrecision ); } return $num . $unit; case 'string': // [1] - contents of string (includes quotes) list(, $delim, $content) = $value; foreach ( $content as &$part ) { if ( is_array( $part ) ) { $part = $this->compileValue( $part ); } } return $delim . implode( $content ) . $delim; case 'color': // [1] - red component (either number or a %) // [2] - green component // [3] - blue component // [4] - optional alpha component list(, $r, $g, $b) = $value; $r = round( $r ); $g = round( $g ); $b = round( $b ); if ( count( $value ) == 5 && $value[4] != 1 ) { // rgba return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . $value[4] . ')'; } $h = sprintf( '#%02x%02x%02x', $r, $g, $b ); if ( ! empty( $this->formatter->compressColors ) ) { // Converting hex color to short notation (e.g. #003399 to #039) if ( $h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6] ) { $h = '#' . $h[1] . $h[3] . $h[5]; } } return $h; case 'function': list(, $name, $args) = $value; return $name . '(' . $this->compileValue( $args ) . ')'; default: // assumed to be unit $this->throwError( "unknown value type: $value[0]" ); } } protected function lib_isnumber( $value ) { return $this->toBool( $value[0] == 'number' ); } protected function lib_isstring( $value ) { return $this->toBool( $value[0] == 'string' ); } protected function lib_iscolor( $value ) { return $this->toBool( $this->coerceColor( $value ) ); } protected function lib_iskeyword( $value ) { return $this->toBool( $value[0] == 'keyword' ); } protected function lib_ispixel( $value ) { return $this->toBool( $value[0] == 'number' && $value[2] == 'px' ); } protected function lib_ispercentage( $value ) { return $this->toBool( $value[0] == 'number' && $value[2] == '%' ); } protected function lib_isem( $value ) { return $this->toBool( $value[0] == 'number' && $value[2] == 'em' ); } protected function lib_rgbahex( $color ) { $color = $this->coerceColor( $color ); if ( is_null( $color ) ) { $this->throwError( 'color expected for rgbahex' ); } return sprintf( '#%02x%02x%02x%02x', isset( $color[4] ) ? $color[4] * 255 : 255, $color[1], $color[2], $color[3] ); } protected function lib_argb( $color ) { return $this->lib_rgbahex( $color ); } // utility func to unquote a string protected function lib_e( $arg ) { switch ( $arg[0] ) { case 'list': $items = $arg[2]; if ( isset( $items[0] ) ) { return $this->lib_e( $items[0] ); } return self::$defaultValue; case 'string': $arg[1] = ''; return $arg; case 'keyword': return $arg; default: return array( 'keyword', $this->compileValue( $arg ) ); } } protected function lib__sprintf( $args ) { if ( $args[0] != 'list' ) { return $args; } $values = $args[2]; $string = array_shift( $values ); $template = $this->compileValue( $this->lib_e( $string ) ); $i = 0; if ( preg_match_all( '/%[dsa]/', $template, $m ) ) { foreach ( $m[0] as $match ) { $val = isset( $values[ $i ] ) ? $this->reduce( $values[ $i ] ) : array( 'keyword', '' ); // lessjs compat, renders fully expanded color, not raw color if ( $color = $this->coerceColor( $val ) ) { $val = $color; } $i++; $rep = $this->compileValue( $this->lib_e( $val ) ); $template = preg_replace( '/' . self::preg_quote( $match ) . '/', $rep, $template, 1 ); } } $d = $string[0] == 'string' ? $string[1] : '"'; return array( 'string', $d, array( $template ) ); } protected function lib_floor( $arg ) { $value = $this->assertNumber( $arg ); return array( 'number', floor( $value ), $arg[2] ); } protected function lib_ceil( $arg ) { $value = $this->assertNumber( $arg ); return array( 'number', ceil( $value ), $arg[2] ); } protected function lib_round( $arg ) { $value = $this->assertNumber( $arg ); return array( 'number', round( $value ), $arg[2] ); } /** * Helper function to get arguments for color manipulation functions. * takes a list that contains a color like thing and a percentage */ protected function colorArgs( $args ) { if ( $args[0] != 'list' || count( $args[2] ) < 2 ) { return array( array( 'color', 0, 0, 0 ), 0 ); } list($color, $delta) = $args[2]; $color = $this->assertColor( $color ); $delta = floatval( $delta[1] ); return array( $color, $delta ); } protected function lib_darken( $args ) { list($color, $delta) = $this->colorArgs( $args ); $hsl = $this->toHSL( $color ); $hsl[3] = $this->clamp( $hsl[3] - $delta, 100 ); return $this->toRGB( $hsl ); } protected function lib_lighten( $args ) { list($color, $delta) = $this->colorArgs( $args ); $hsl = $this->toHSL( $color ); $hsl[3] = $this->clamp( $hsl[3] + $delta, 100 ); return $this->toRGB( $hsl ); } protected function lib_saturate( $args ) { list($color, $delta) = $this->colorArgs( $args ); $hsl = $this->toHSL( $color ); $hsl[2] = $this->clamp( $hsl[2] + $delta, 100 ); return $this->toRGB( $hsl ); } protected function lib_desaturate( $args ) { list($color, $delta) = $this->colorArgs( $args ); $hsl = $this->toHSL( $color ); $hsl[2] = $this->clamp( $hsl[2] - $delta, 100 ); return $this->toRGB( $hsl ); } protected function lib_spin( $args ) { list($color, $delta) = $this->colorArgs( $args ); $hsl = $this->toHSL( $color ); $hsl[1] = $hsl[1] + $delta % 360; if ( $hsl[1] < 0 ) { $hsl[1] += 360; } return $this->toRGB( $hsl ); } protected function lib_fadeout( $args ) { list($color, $delta) = $this->colorArgs( $args ); $color[4] = $this->clamp( ( isset( $color[4] ) ? $color[4] : 1 ) - $delta / 100 ); return $color; } protected function lib_fadein( $args ) { list($color, $delta) = $this->colorArgs( $args ); $color[4] = $this->clamp( ( isset( $color[4] ) ? $color[4] : 1 ) + $delta / 100 ); return $color; } protected function lib_hue( $color ) { $hsl = $this->toHSL( $this->assertColor( $color ) ); return round( $hsl[1] ); } protected function lib_saturation( $color ) { $hsl = $this->toHSL( $this->assertColor( $color ) ); return round( $hsl[2] ); } protected function lib_lightness( $color ) { $hsl = $this->toHSL( $this->assertColor( $color ) ); return round( $hsl[3] ); } // get the alpha of a color // defaults to 1 for non-colors or colors without an alpha protected function lib_alpha( $value ) { if ( ! is_null( $color = $this->coerceColor( $value ) ) ) { return isset( $color[4] ) ? $color[4] : 1; } } // set the alpha of the color protected function lib_fade( $args ) { list($color, $alpha) = $this->colorArgs( $args ); $color[4] = $this->clamp( $alpha / 100.0 ); return $color; } protected function lib_percentage( $arg ) { $num = $this->assertNumber( $arg ); return array( 'number', $num * 100, '%' ); } // mixes two colors by weight // mix(@color1, @color2, @weight); // 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 ) { $this->throwError( 'mix expects (color1, color2, weight)' ); } list($first, $second, $weight) = $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; $w = $weight * 2 - 1; $a = $first_a - $second_a; $w1 = ( ( $w * $a == -1 ? $w : ( $w + $a ) / ( 1 + $w * $a ) ) + 1 ) / 2.0; $w2 = 1.0 - $w1; $new = array( 'color', $w1 * $first[1] + $w2 * $second[1], $w1 * $first[2] + $w2 * $second[2], $w1 * $first[3] + $w2 * $second[3], ); if ( $first_a != 1.0 || $second_a != 1.0 ) { $new[] = $first_a * $weight + $second_a * ( $weight - 1 ); } return $this->fixColor( $new ); } protected function assertColor( $value, $error = 'expected color value' ) { $color = $this->coerceColor( $value ); if ( is_null( $color ) ) { $this->throwError( $error ); } return $color; } protected function assertNumber( $value, $error = 'expecting number' ) { if ( $value[0] == 'number' ) { return $value[1]; } $this->throwError( $error ); } protected function toHSL( $color ) { if ( $color[0] == 'hsl' ) { return $color; } $r = $color[1] / 255; $g = $color[2] / 255; $b = $color[3] / 255; $min = min( $r, $g, $b ); $max = max( $r, $g, $b ); $L = ( $min + $max ) / 2; if ( $min == $max ) { $S = $H = 0; } else { if ( $L < 0.5 ) { $S = ( $max - $min ) / ( $max + $min ); } 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 ); } } $out = array( 'hsl', ( $H < 0 ? $H + 6 : $H ) * 60, $S * 100, $L * 100, ); if ( count( $color ) > 4 ) { $out[] = $color[4]; // copy alpha } return $out; } protected function toRGB_helper( $comp, $temp1, $temp2 ) { if ( $comp < 0 ) { $comp += 1.0; } elseif ( $comp > 1 ) { $comp -= 1.0; } 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; } return $temp1; } /** * Converts a hsl array into a color value in rgb. * 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; } $H = $color[1] / 360; $S = $color[2] / 100; $L = $color[3] / 100; if ( $S == 0 ) { $r = $g = $b = $L; } else { $temp2 = $L < 0.5 ? $L * ( 1.0 + $S ) : $L + $S - $L * $S; $temp1 = 2.0 * $L - $temp2; $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 ); } // $out = array('color', round($r*255), round($g*255), round($b*255)); $out = array( 'color', $r * 255, $g * 255, $b * 255 ); if ( count( $color ) > 4 ) { $out[] = $color[4]; // copy alpha } return $out; } protected function clamp( $v, $max = 1, $min = 0 ) { return min( $max, max( $min, $v ) ); } /** * Convert the rgb, rgba, hsl color literals of function type * as returned by the parser into values of color type. */ protected function funcToColor( $func ) { $fname = $func[1]; if ( $func[2][0] != 'list' ) { return false; // need a list of arguments } $rawComponents = $func[2][2]; if ( $fname == 'hsl' || $fname == 'hsla' ) { $hsl = array( 'hsl' ); $i = 0; foreach ( $rawComponents as $c ) { $val = $this->reduce( $c ); $val = isset( $val[1] ) ? floatval( $val[1] ) : 0; if ( $i == 0 ) { $clamp = 360; } elseif ( $i < 3 ) { $clamp = 100; } else { $clamp = 1; } $hsl[] = $this->clamp( $val, $clamp ); $i++; } while ( count( $hsl ) < 4 ) { $hsl[] = 0; } return $this->toRGB( $hsl ); } elseif ( $fname == 'rgb' || $fname == 'rgba' ) { $components = array(); $i = 1; foreach ( $rawComponents as $c ) { $c = $this->reduce( $c ); if ( $i < 4 ) { if ( $c[0] == 'number' && $c[2] == '%' ) { $components[] = 255 * ( $c[1] / 100 ); } else { $components[] = floatval( $c[1] ); } } elseif ( $i == 4 ) { if ( $c[0] == 'number' && $c[2] == '%' ) { $components[] = 1.0 * ( $c[1] / 100 ); } else { $components[] = floatval( $c[1] ); } } else { break; } $i++; } while ( count( $components ) < 3 ) { $components[] = 0; } array_unshift( $components, 'color' ); return $this->fixColor( $components ); } return false; } protected function reduce( $value, $forExpression = false ) { switch ( $value[0] ) { case 'variable': $key = $value[1]; if ( is_array( $key ) ) { $key = $this->reduce( $key ); $key = $this->vPrefix . $this->compileValue( $this->lib_e( $key ) ); } $seen =& $this->env->seenNames; if ( ! empty( $seen[ $key ] ) ) { $this->throwError( "infinite loop detected: $key" ); } $seen[ $key ] = true; $out = $this->reduce( $this->get( $key, self::$defaultValue ) ); $seen[ $key ] = false; return $out; case 'list': foreach ( $value[2] as &$item ) { $item = $this->reduce( $item, $forExpression ); } return $value; case 'expression': return $this->evaluate( $value ); case 'string': foreach ( $value[2] as &$part ) { if ( is_array( $part ) ) { $strip = $part[0] == 'variable'; $part = $this->reduce( $part ); if ( $strip ) { $part = $this->lib_e( $part ); } } } return $value; case 'escape': list(,$inner) = $value; return $this->lib_e( $this->reduce( $inner ) ); case 'function': $color = $this->funcToColor( $value ); if ( $color ) { return $color; } list(, $name, $args) = $value; if ( $name == '%' ) { $name = '_sprintf'; } $f = isset( $this->libFunctions[ $name ] ) ? $this->libFunctions[ $name ] : array( $this, 'lib_' . $name ); if ( is_callable( $f ) ) { if ( $args[0] == 'list' ) { $args = self::compressList( $args[2], $args[1] ); } $ret = call_user_func( $f, $this->reduce( $args, true ), $this ); if ( is_null( $ret ) ) { return array( 'string', '', array( $name, '(', $args, ')', ), ); } // convert to a typed value if the result is a php primitive if ( is_numeric( $ret ) ) { $ret = array( 'number', $ret, '' ); } elseif ( ! is_array( $ret ) ) { $ret = array( 'keyword', $ret ); } return $ret; } // plain function, reduce args $value[2] = $this->reduce( $value[2] ); return $value; case 'unary': list(, $op, $exp) = $value; $exp = $this->reduce( $exp ); if ( $exp[0] == 'number' ) { switch ( $op ) { case '+': return $exp; case '-': $exp[1] *= -1; return $exp; } } return array( 'string', '', array( $op, $exp ) ); } if ( $forExpression ) { switch ( $value[0] ) { case 'keyword': if ( $color = $this->coerceColor( $value ) ) { return $color; } break; case 'raw_color': return $this->coerceColor( $value ); } } return $value; } // coerce a value for use in color operation protected function coerceColor( $value ) { switch ( $value[0] ) { case 'color': return $value; case 'raw_color': $c = array( 'color', 0, 0, 0 ); $colorStr = substr( $value[1], 1 ); $num = hexdec( $colorStr ); $width = strlen( $colorStr ) == 3 ? 16 : 256; for ( $i = 3; $i > 0; $i-- ) { // 3 2 1 $t = $num % $width; $num /= $width; $c[ $i ] = $t * ( 256 / $width ) + $t * floor( 16 / $width ); } return $c; case 'keyword': $name = $value[1]; if ( isset( self::$cssColors[ $name ] ) ) { list($r, $g, $b) = explode( ',', self::$cssColors[ $name ] ); return array( 'color', $r, $g, $b ); } return null; } } // make something string like into a string protected function coerceString( $value ) { switch ( $value[0] ) { case 'string': return $value; case 'keyword': return array( 'string', '', array( $value[1] ) ); } return null; } // turn list of length 1 into value type protected function flattenList( $value ) { if ( $value[0] == 'list' && count( $value[2] ) == 1 ) { return $this->flattenList( $value[2][0] ); } return $value; } protected function toBool( $a ) { if ( $a ) { return self::$TRUE; } else { return self::$FALSE; } } // evaluate an expression protected function evaluate( $exp ) { list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp; $left = $this->reduce( $left, true ); $right = $this->reduce( $right, true ); if ( $leftColor = $this->coerceColor( $left ) ) { $left = $leftColor; } if ( $rightColor = $this->coerceColor( $right ) ) { $right = $rightColor; } $ltype = $left[0]; $rtype = $right[0]; // operators that work on all types if ( $op == 'and' ) { return $this->toBool( $left == self::$TRUE && $right == self::$TRUE ); } if ( $op == '=' ) { return $this->toBool( $this->eq( $left, $right ) ); } if ( $op == '+' && ! is_null( $str = $this->stringConcatenate( $left, $right ) ) ) { return $str; } // type based operators $fname = "op_${ltype}_${rtype}"; if ( is_callable( array( $this, $fname ) ) ) { $out = $this->$fname( $op, $left, $right ); if ( ! is_null( $out ) ) { return $out; } } // make the expression look it did before being parsed $paddedOp = $op; if ( $whiteBefore ) { $paddedOp = ' ' . $paddedOp; } if ( $whiteAfter ) { $paddedOp .= ' '; } return array( 'string', '', array( $left, $paddedOp, $right ) ); } protected function stringConcatenate( $left, $right ) { if ( $strLeft = $this->coerceString( $left ) ) { if ( $right[0] == 'string' ) { $right[1] = ''; } $strLeft[2][] = $right; return $strLeft; } if ( $strRight = $this->coerceString( $right ) ) { array_unshift( $strRight[2], $left ); return $strRight; } } // make sure a color's components don't go out of bounds protected function fixColor( $c ) { foreach ( range( 1, 3 ) as $i ) { if ( $c[ $i ] < 0 ) { $c[ $i ] = 0; } if ( $c[ $i ] > 255 ) { $c[ $i ] = 255; } } return $c; } protected function op_number_color( $op, $lft, $rgt ) { if ( $op == '+' || $op == '*' ) { return $this->op_color_number( $op, $rgt, $lft ); } } protected function op_color_number( $op, $lft, $rgt ) { if ( $rgt[0] == '%' ) { $rgt[1] /= 100; } return $this->op_color_color( $op, $lft, array_fill( 1, count( $lft ) - 1, $rgt[1] ) ); } protected function op_color_color( $op, $left, $right ) { $out = array( 'color' ); $max = count( $left ) > count( $right ) ? count( $left ) : count( $right ); foreach ( range( 1, $max - 1 ) as $i ) { $lval = isset( $left[ $i ] ) ? $left[ $i ] : 0; $rval = isset( $right[ $i ] ) ? $right[ $i ] : 0; switch ( $op ) { case '+': $out[] = $lval + $rval; break; case '-': $out[] = $lval - $rval; break; case '*': $out[] = $lval * $rval; break; case '%': $out[] = $lval % $rval; break; case '/': if ( $rval == 0 ) { $this->throwError( "evaluate error: can't divide by zero" ); } $out[] = $lval / $rval; break; default: $this->throwError( 'evaluate error: color op number failed on op ' . $op ); } } return $this->fixColor( $out ); } // operator on two numbers protected function op_number_number( $op, $left, $right ) { $unit = empty( $left[2] ) ? $right[2] : $left[2]; $value = 0; switch ( $op ) { case '+': $value = $left[1] + $right[1]; break; case '*': $value = $left[1] * $right[1]; break; case '-': $value = $left[1] - $right[1]; break; case '%': $value = $left[1] % $right[1]; break; case '/': if ( $right[1] == 0 ) { $this->throwError( 'parse error: divide by zero' ); } $value = $left[1] / $right[1]; break; case '<': return $this->toBool( $left[1] < $right[1] ); case '>': return $this->toBool( $left[1] > $right[1] ); case '>=': return $this->toBool( $left[1] >= $right[1] ); case '=<': return $this->toBool( $left[1] <= $right[1] ); default: $this->throwError( 'parse error: unknown number operator: ' . $op ); } return array( 'number', $value, $unit ); } /* environment functions */ protected function makeOutputBlock( $type, $selectors = null ) { $b = new stdclass(); $b->lines = array(); $b->children = array(); $b->selectors = $selectors; $b->type = $type; $b->parent = $this->scope; return $b; } // the state of execution protected function pushEnv( $block = null ) { $e = new stdclass(); $e->parent = $this->env; $e->store = array(); $e->block = $block; $this->env = $e; return $e; } // pop something off the stack protected function popEnv() { $old = $this->env; $this->env = $this->env->parent; return $old; } // set something in the current env protected function set( $name, $value ) { $this->env->store[ $name ] = $value; } // get the highest occurrence entry for a name protected function get( $name, $default = null ) { $current = $this->env; $isArguments = $name == $this->vPrefix . 'arguments'; while ( $current ) { if ( $isArguments && isset( $current->arguments ) ) { return array( 'list', ' ', $current->arguments ); } if ( isset( $current->store[ $name ] ) ) { return $current->store[ $name ]; } else { $current = isset( $current->storeParent ) ? $current->storeParent : $current->parent; } } return $default; } // inject array of unparsed strings into environment as variables protected function injectVariables( $args ) { $this->pushEnv(); $parser = new seedprod_lessc_parser( $this, __METHOD__ ); foreach ( $args as $name => $strValue ) { if ( $name[0] != '@' ) { $name = '@' . $name; } $parser->count = 0; $parser->buffer = (string) $strValue; if ( ! $parser->propertyValue( $value ) ) { throw new Exception( "failed to parse passed in variable $name: $strValue" ); } $this->set( $name, $value ); } } /** * Initialize any static state, can initialize parser for a file * $opts isn't used yet */ public function __construct( $fname = null ) { if ( $fname !== null ) { // used for deprecated parse method $this->_parseFile = $fname; } } public function compile( $string, $name = null ) { $locale = setlocale( LC_NUMERIC, 0 ); setlocale( LC_NUMERIC, 'C' ); $this->parser = $this->makeParser( $name ); $root = $this->parser->parse( $string ); $this->env = null; $this->scope = null; $this->formatter = $this->newFormatter(); if ( ! empty( $this->registeredVars ) ) { $this->injectVariables( $this->registeredVars ); } $this->sourceParser = $this->parser; // used for error messages $this->compileBlock( $root ); ob_start(); $this->formatter->block( $this->scope ); $out = ob_get_clean(); setlocale( LC_NUMERIC, $locale ); return $out; } public function compileFile( $fname, $outFname = null ) { if ( ! is_readable( $fname ) ) { throw new Exception( 'load error: failed to find ' . $fname ); } $pi = pathinfo( $fname ); $oldImport = $this->importDir; $this->importDir = (array) $this->importDir; $this->importDir[] = $pi['dirname'] . '/'; $this->allParsedFiles = array(); $this->addParsedFile( $fname ); $out = $this->compile( file_get_contents( $fname ), $fname ); $this->importDir = $oldImport; if ( $outFname !== null ) { return file_put_contents( $outFname, $out ); } return $out; } // compile only if changed input has changed or output doesn't exist public function checkedCompile( $in, $out ) { if ( ! is_file( $out ) || filemtime( $in ) > filemtime( $out ) ) { $this->compileFile( $in, $out ); return true; } return false; } /** * Execute lessphp on a .less file or a lessphp cache structure * * The lessphp cache structure contains information about a specific * less file having been parsed. It can be used as a hint for future * calls to determine whether or not a rebuild is required. * * The cache structure contains two important keys that may be used * externally: * * compiled: The final compiled CSS * updated: The time (in seconds) the CSS was last compiled * * The cache structure is a plain-ol' PHP associative array and can * be serialized and unserialized without a hitch. * * @param mixed $in Input * @param bool $force Force rebuild? * @return array lessphp cache structure */ public function cachedCompile( $in, $force = false ) { // assume no root $root = null; if ( is_string( $in ) ) { $root = $in; } elseif ( is_array( $in ) and isset( $in['root'] ) ) { if ( $force or ! isset( $in['files'] ) ) { // If we are forcing a recompile or if for some reason the // structure does not contain any file information we should // specify the root to trigger a rebuild. $root = $in['root']; } elseif ( isset( $in['files'] ) and is_array( $in['files'] ) ) { foreach ( $in['files'] as $fname => $ftime ) { if ( ! file_exists( $fname ) or filemtime( $fname ) > $ftime ) { // One of the files we knew about previously has changed // so we should look at our incoming root again. $root = $in['root']; break; } } } } else { // TODO: Throw an exception? We got neither a string nor something // that looks like a compatible lessphp cache structure. return null; } if ( $root !== null ) { // If we have a root value which means we should rebuild. $out = array(); $out['root'] = $root; $out['compiled'] = $this->compileFile( $root ); $out['files'] = $this->allParsedFiles(); $out['updated'] = time(); return $out; } else { // No changes, pass back the structure // we were given initially. return $in; } } // parse and compile buffer // This is deprecated public function parse( $str = null, $initialVariables = null ) { if ( is_array( $str ) ) { $initialVariables = $str; $str = null; } $oldVars = $this->registeredVars; if ( $initialVariables !== null ) { $this->setVariables( $initialVariables ); } if ( $str == null ) { if ( empty( $this->_parseFile ) ) { throw new exception( 'nothing to parse' ); } $out = $this->compileFile( $this->_parseFile ); } else { $out = $this->compile( $str ); } $this->registeredVars = $oldVars; return $out; } protected function makeParser( $name ) { $parser = new seedprod_lessc_parser( $this, $name ); $parser->writeComments = $this->preserveComments; return $parser; } public function setFormatter( $name ) { $this->formatterName = $name; } protected function newFormatter() { $className = 'seedprod_lessc_formatter_lessjs'; if ( ! empty( $this->formatterName ) ) { if ( ! is_string( $this->formatterName ) ) { return $this->formatterName; } $className = "seedprod_lessc_formatter_$this->formatterName"; } return new $className(); } public function setPreserveComments( $preserve ) { $this->preserveComments = $preserve; } public function registerFunction( $name, $func ) { $this->libFunctions[ $name ] = $func; } public function unregisterFunction( $name ) { unset( $this->libFunctions[ $name ] ); } public function setVariables( $variables ) { $this->registeredVars = array_merge( $this->registeredVars, $variables ); } public function unsetVariable( $name ) { unset( $this->registeredVars[ $name ] ); } public function setImportDir( $dirs ) { $this->importDir = (array) $dirs; } public function addImportDir( $dir ) { $this->importDir = (array) $this->importDir; $this->importDir[] = $dir; } public function allParsedFiles() { return $this->allParsedFiles; } protected function addParsedFile( $file ) { $this->allParsedFiles[ realpath( $file ) ] = filemtime( $file ); } /** * Uses the current value of $this->count to show line and line number */ protected function throwError( $msg = null ) { if ( $this->sourceLoc >= 0 ) { $this->sourceParser->throwError( $msg, $this->sourceLoc ); } throw new exception( $msg ); } // compile file $in to file $out if $in is newer than $out // returns true when it compiles, false otherwise public static function ccompile( $in, $out, $less = null ) { if ( $less === null ) { $less = new self(); } return $less->checkedCompile( $in, $out ); } public static function cexecute( $in, $force = false, $less = null ) { if ( $less === null ) { $less = new self(); } return $less->cachedCompile( $in, $force ); } protected static $cssColors = array( 'aliceblue' => '240,248,255', 'antiquewhite' => '250,235,215', 'aqua' => '0,255,255', 'aquamarine' => '127,255,212', 'azure' => '240,255,255', 'beige' => '245,245,220', 'bisque' => '255,228,196', 'black' => '0,0,0', 'blanchedalmond' => '255,235,205', 'blue' => '0,0,255', 'blueviolet' => '138,43,226', 'brown' => '165,42,42', 'burlywood' => '222,184,135', 'cadetblue' => '95,158,160', 'chartreuse' => '127,255,0', 'chocolate' => '210,105,30', 'coral' => '255,127,80', 'cornflowerblue' => '100,149,237', 'cornsilk' => '255,248,220', 'crimson' => '220,20,60', 'cyan' => '0,255,255', 'darkblue' => '0,0,139', 'darkcyan' => '0,139,139', 'darkgoldenrod' => '184,134,11', 'darkgray' => '169,169,169', 'darkgreen' => '0,100,0', 'darkgrey' => '169,169,169', 'darkkhaki' => '189,183,107', 'darkmagenta' => '139,0,139', 'darkolivegreen' => '85,107,47', 'darkorange' => '255,140,0', 'darkorchid' => '153,50,204', 'darkred' => '139,0,0', 'darksalmon' => '233,150,122', 'darkseagreen' => '143,188,143', 'darkslateblue' => '72,61,139', 'darkslategray' => '47,79,79', 'darkslategrey' => '47,79,79', 'darkturquoise' => '0,206,209', 'darkviolet' => '148,0,211', 'deeppink' => '255,20,147', 'deepskyblue' => '0,191,255', 'dimgray' => '105,105,105', 'dimgrey' => '105,105,105', 'dodgerblue' => '30,144,255', 'firebrick' => '178,34,34', 'floralwhite' => '255,250,240', 'forestgreen' => '34,139,34', 'fuchsia' => '255,0,255', 'gainsboro' => '220,220,220', 'ghostwhite' => '248,248,255', 'gold' => '255,215,0', 'goldenrod' => '218,165,32', 'gray' => '128,128,128', 'green' => '0,128,0', 'greenyellow' => '173,255,47', 'grey' => '128,128,128', 'honeydew' => '240,255,240', 'hotpink' => '255,105,180', 'indianred' => '205,92,92', 'indigo' => '75,0,130', 'ivory' => '255,255,240', 'khaki' => '240,230,140', 'lavender' => '230,230,250', 'lavenderblush' => '255,240,245', 'lawngreen' => '124,252,0', 'lemonchiffon' => '255,250,205', 'lightblue' => '173,216,230', 'lightcoral' => '240,128,128', 'lightcyan' => '224,255,255', 'lightgoldenrodyellow' => '250,250,210', 'lightgray' => '211,211,211', 'lightgreen' => '144,238,144', 'lightgrey' => '211,211,211', 'lightpink' => '255,182,193', 'lightsalmon' => '255,160,122', 'lightseagreen' => '32,178,170', 'lightskyblue' => '135,206,250', 'lightslategray' => '119,136,153', 'lightslategrey' => '119,136,153', 'lightsteelblue' => '176,196,222', 'lightyellow' => '255,255,224', 'lime' => '0,255,0', 'limegreen' => '50,205,50', 'linen' => '250,240,230', 'magenta' => '255,0,255', 'maroon' => '128,0,0', 'mediumaquamarine' => '102,205,170', 'mediumblue' => '0,0,205', 'mediumorchid' => '186,85,211', 'mediumpurple' => '147,112,219', 'mediumseagreen' => '60,179,113', 'mediumslateblue' => '123,104,238', 'mediumspringgreen' => '0,250,154', 'mediumturquoise' => '72,209,204', 'mediumvioletred' => '199,21,133', 'midnightblue' => '25,25,112', 'mintcream' => '245,255,250', 'mistyrose' => '255,228,225', 'moccasin' => '255,228,181', 'navajowhite' => '255,222,173', 'navy' => '0,0,128', 'oldlace' => '253,245,230', 'olive' => '128,128,0', 'olivedrab' => '107,142,35', 'orange' => '255,165,0', 'orangered' => '255,69,0', 'orchid' => '218,112,214', 'palegoldenrod' => '238,232,170', 'palegreen' => '152,251,152', 'paleturquoise' => '175,238,238', 'palevioletred' => '219,112,147', 'papayawhip' => '255,239,213', 'peachpuff' => '255,218,185', 'peru' => '205,133,63', 'pink' => '255,192,203', 'plum' => '221,160,221', 'powderblue' => '176,224,230', 'purple' => '128,0,128', 'red' => '255,0,0', 'rosybrown' => '188,143,143', 'royalblue' => '65,105,225', 'saddlebrown' => '139,69,19', 'salmon' => '250,128,114', 'sandybrown' => '244,164,96', 'seagreen' => '46,139,87', 'seashell' => '255,245,238', 'sienna' => '160,82,45', 'silver' => '192,192,192', 'skyblue' => '135,206,235', 'slateblue' => '106,90,205', 'slategray' => '112,128,144', 'slategrey' => '112,128,144', 'snow' => '255,250,250', 'springgreen' => '0,255,127', 'steelblue' => '70,130,180', 'tan' => '210,180,140', 'teal' => '0,128,128', 'thistle' => '216,191,216', 'tomato' => '255,99,71', 'turquoise' => '64,224,208', 'violet' => '238,130,238', 'wheat' => '245,222,179', 'white' => '255,255,255', 'whitesmoke' => '245,245,245', 'yellow' => '255,255,0', 'yellowgreen' => '154,205,50', ); } // responsible for taking a string of LESS code and converting it into a // syntax tree class seedprod_lessc_parser { protected static $nextBlockId = 0; // used to uniquely identify blocks protected static $precedence = array( '=<' => 0, '>=' => 0, '=' => 0, '<' => 0, '>' => 0, '+' => 1, '-' => 1, '*' => 2, '/' => 2, '%' => 2, ); protected static $whitePattern; protected static $commentMulti; protected static $commentSingle = '//'; protected static $commentMultiLeft = '/*'; protected static $commentMultiRight = '*/'; // regex string to match any of the operators protected static $operatorString; // these properties will supress division unless it's inside parenthases protected static $supressDivisionProps = array( '/border-radius$/i', '/^font$/i' ); protected $blockDirectives = array( 'font-face', 'keyframes', 'page', '-moz-document' ); protected $lineDirectives = array( 'charset' ); /** * if we are in parens we can be more liberal with whitespace around * operators because it must evaluate to a single value and thus is less * ambiguous. * * Consider: * property1: 10 -5; // is two numbers, 10 and -5 * property2: (10 -5); // should evaluate to 5 */ protected $inParens = false; // caches preg escaped literals protected static $literalCache = array(); public function __construct( $seedprod_lessc, $sourceName = null ) { $this->eatWhiteDefault = true; // reference to less needed for vPrefix, mPrefix, and parentSelector $this->seedprod_lessc = $seedprod_lessc; $this->sourceName = $sourceName; // name used for error messages $this->writeComments = false; if ( ! self::$operatorString ) { self::$operatorString = '(' . implode( '|', array_map( array( 'seedprod_lessc', 'preg_quote' ), array_keys( self::$precedence ) ) ) . ')'; $commentSingle = seedprod_lessc::preg_quote( self::$commentSingle ); $commentMultiLeft = seedprod_lessc::preg_quote( self::$commentMultiLeft ); $commentMultiRight = seedprod_lessc::preg_quote( self::$commentMultiRight ); self::$commentMulti = $commentMultiLeft . '.*?' . $commentMultiRight; self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentMulti . ')\s*|\s+/Ais'; } } public function parse( $buffer ) { $this->count = 0; $this->line = 1; $this->env = null; // block stack $this->buffer = $this->writeComments ? $buffer : $this->removeComments( $buffer ); $this->pushSpecialBlock( 'root' ); $this->eatWhiteDefault = true; $this->seenComments = array(); // trim whitespace on head // if (preg_match('/^\s+/', $this->buffer, $m)) { // $this->line += substr_count($m[0], "\n"); // $this->buffer = ltrim($this->buffer); // } $this->whitespace(); // parse the entire file $lastCount = $this->count; while ( false !== $this->parseChunk() ); if ( $this->count != strlen( $this->buffer ) ) { $this->throwError(); } // TODO report where the block was opened if ( ! is_null( $this->env->parent ) ) { throw new exception( 'parse error: unclosed block' ); } 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 seedprod_lessc::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 seedprod_lessc::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, seedprod_lessc::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. */ protected function parseChunk() { if ( empty( $this->buffer ) ) { return false; } $s = $this->seek(); // setting a property if ( $this->keyword( $key ) && $this->assign() && $this->propertyValue( $value, $key ) && $this->end() ) { $this->append( array( 'assign', $key, $value ), $s ); return true; } else { $this->seek( $s ); } // look for special css blocks if ( $this->literal( '@', false ) ) { $this->count--; // media if ( $this->literal( '@media' ) ) { if ( ( $this->mediaQueryList( $mediaQueries ) || true ) && $this->literal( '{' ) ) { $media = $this->pushSpecialBlock( 'media' ); $media->queries = is_null( $mediaQueries ) ? array() : $mediaQueries; return true; } else { $this->seek( $s ); return false; } } if ( $this->literal( '@', false ) && $this->keyword( $dirName ) ) { if ( $this->isDirective( $dirName, $this->blockDirectives ) ) { if ( ( $this->openString( '{', $dirValue, null, array( ';' ) ) || true ) && $this->literal( '{' ) ) { $dir = $this->pushSpecialBlock( 'directive' ); $dir->name = $dirName; if ( isset( $dirValue ) ) { $dir->value = $dirValue; } return true; } } elseif ( $this->isDirective( $dirName, $this->lineDirectives ) ) { if ( $this->propertyValue( $dirValue ) && $this->end() ) { $this->append( array( 'directive', $dirName, $dirValue ) ); return true; } } } $this->seek( $s ); } // setting a variable if ( $this->variable( $var ) && $this->assign() && $this->propertyValue( $value ) && $this->end() ) { $this->append( array( 'assign', $var, $value ), $s ); return true; } else { $this->seek( $s ); } if ( $this->import( $importValue ) ) { $this->append( $importValue, $s ); return true; } // opening parametric mixin if ( $this->tag( $tag, true ) && $this->argumentDef( $args, $isVararg ) && ( $this->guards( $guards ) || true ) && $this->literal( '{' ) ) { $block = $this->pushBlock( $this->fixTags( array( $tag ) ) ); $block->args = $args; $block->isVararg = $isVararg; if ( ! empty( $guards ) ) { $block->guards = $guards; } return true; } else { $this->seek( $s ); } // opening a simple block if ( $this->tags( $tags ) && $this->literal( '{' ) ) { $tags = $this->fixTags( $tags ); $this->pushBlock( $tags ); return true; } else { $this->seek( $s ); } // closing a block if ( $this->literal( '}', false ) ) { try { $block = $this->pop(); } catch ( exception $e ) { $this->seek( $s ); $this->throwError( $e->getMessage() ); } $hidden = false; if ( is_null( $block->type ) ) { $hidden = true; if ( ! isset( $block->args ) ) { foreach ( $block->tags as $tag ) { if ( ! is_string( $tag ) || $tag[0] != $this->seedprod_lessc->mPrefix ) { $hidden = false; break; } } } foreach ( $block->tags as $tag ) { if ( is_string( $tag ) ) { $this->env->children[ $tag ][] = $block; } } } if ( ! $hidden ) { $this->append( array( 'block', $block ), $s ); } // this is done here so comments aren't bundled into he block that // was just closed $this->whitespace(); return true; } // mixin if ( $this->mixinTags( $tags ) && ( $this->argumentValues( $argv ) || true ) && ( $this->keyword( $suffix ) || true ) && $this->end() ) { $tags = $this->fixTags( $tags ); $this->append( array( 'mixin', $tags, $argv, $suffix ), $s ); return true; } else { $this->seek( $s ); } // spare ; if ( $this->literal( ';' ) ) { return true; } return false; // got nothing, throw error } protected function isDirective( $dirname, $directives ) { // TODO: cache pattern in parser $pattern = implode( '|', array_map( array( 'seedprod_lessc', 'preg_quote' ), $directives ) ); $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i'; return preg_match( $pattern, $dirname ); } protected function fixTags( $tags ) { // move @ tags out of variable namespace foreach ( $tags as &$tag ) { if ( $tag[0] == $this->seedprod_lessc->vPrefix ) { $tag[0] = $this->seedprod_lessc->mPrefix; } } return $tags; } // a list of expressions protected function expressionList( &$exps ) { $values = array(); while ( $this->expression( $exp ) ) { $values[] = $exp; } if ( count( $values ) == 0 ) { return false; } $exps = seedprod_lessc::compressList( $values, ' ' ); return true; } /** * Attempt to consume an expression. * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code */ protected function expression( &$out ) { if ( $this->value( $lhs ) ) { $out = $this->expHelper( $lhs, 0 ); // look for / shorthand if ( ! empty( $this->env->supressedDivision ) ) { unset( $this->env->supressedDivision ); $s = $this->seek(); if ( $this->literal( '/' ) && $this->value( $rhs ) ) { $out = array( 'list', '', array( $out, array( 'keyword', '/' ), $rhs ), ); } else { $this->seek( $s ); } } return true; } return false; } /** * recursively parse infix equation with $lhs at precedence $minP */ protected function expHelper( $lhs, $minP ) { $this->inExp = true; $ss = $this->seek(); while ( true ) { $whiteBefore = isset( $this->buffer[ $this->count - 1 ] ) && ctype_space( $this->buffer[ $this->count - 1 ] ); // If there is whitespace before the operator, then we require // whitespace after the operator for it to be an expression $needWhite = $whiteBefore && ! $this->inParens; if ( $this->match( self::$operatorString . ( $needWhite ? '\s' : '' ), $m ) && self::$precedence[ $m[1] ] >= $minP ) { if ( ! $this->inParens && isset( $this->env->currentProperty ) && $m[1] == '/' && empty( $this->env->supressedDivision ) ) { foreach ( self::$supressDivisionProps as $pattern ) { if ( preg_match( $pattern, $this->env->currentProperty ) ) { $this->env->supressedDivision = true; break 2; } } } $whiteAfter = isset( $this->buffer[ $this->count - 1 ] ) && ctype_space( $this->buffer[ $this->count - 1 ] ); if ( ! $this->value( $rhs ) ) { break; } // peek for next operator to see what to do with rhs if ( $this->peek( self::$operatorString, $next ) && self::$precedence[ $next[1] ] > self::$precedence[ $m[1] ] ) { $rhs = $this->expHelper( $rhs, self::$precedence[ $next[1] ] ); } $lhs = array( 'expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter ); $ss = $this->seek(); continue; } break; } $this->seek( $ss ); return $lhs; } // consume a list of values for a property public function propertyValue( &$value, $keyName = null ) { $values = array(); if ( $keyName !== null ) { $this->env->currentProperty = $keyName; } $s = null; while ( $this->expressionList( $v ) ) { $values[] = $v; $s = $this->seek(); if ( ! $this->literal( ',' ) ) { break; } } if ( $s ) { $this->seek( $s ); } if ( $keyName !== null ) { unset( $this->env->currentProperty ); } if ( count( $values ) == 0 ) { return false; } $value = seedprod_lessc::compressList( $values, ', ' ); return true; } protected function parenValue( &$out ) { $s = $this->seek(); // speed shortcut if ( isset( $this->buffer[ $this->count ] ) && $this->buffer[ $this->count ] != '(' ) { return false; } $inParens = $this->inParens; if ( $this->literal( '(' ) && ( $this->inParens = true ) && $this->expression( $exp ) && $this->literal( ')' ) ) { $out = $exp; $this->inParens = $inParens; return true; } else { $this->inParens = $inParens; $this->seek( $s ); } return false; } // a single value protected function value( &$value ) { $s = $this->seek(); // speed shortcut if ( isset( $this->buffer[ $this->count ] ) && $this->buffer[ $this->count ] == '-' ) { // negation if ( $this->literal( '-', false ) && ( ( $this->variable( $inner ) && $inner = array( 'variable', $inner ) ) || $this->unit( $inner ) || $this->parenValue( $inner ) ) ) { $value = array( 'unary', '-', $inner ); return true; } else { $this->seek( $s ); } } if ( $this->parenValue( $value ) ) { return true; } if ( $this->unit( $value ) ) { return true; } if ( $this->color( $value ) ) { return true; } if ( $this->func( $value ) ) { return true; } if ( $this->lstring( $value ) ) { return true; } if ( $this->keyword( $word ) ) { $value = array( 'keyword', $word ); return true; } // try a variable if ( $this->variable( $var ) ) { $value = array( 'variable', $var ); return true; } // unquote string (should this work on any type? if ( $this->literal( '~' ) && $this->lstring( $str ) ) { $value = array( 'escape', $str ); return true; } else { $this->seek( $s ); } // css hack: \0 if ( $this->literal( '\\' ) && $this->match( '([0-9]+)', $m ) ) { $value = array( 'keyword', '\\' . $m[1] ); return true; } else { $this->seek( $s ); } return false; } // an import statement protected function import( &$out ) { $s = $this->seek(); if ( ! $this->literal( '@import' ) ) { return false; } // @import "something.css" media; // @import url("something.css") media; // @import url(something.css) media; if ( $this->propertyValue( $value ) ) { $out = array( 'import', $value ); return true; } } protected function mediaQueryList( &$out ) { if ( $this->genericList( $list, 'mediaQuery', ',', false ) ) { $out = $list[2]; return true; } return false; } protected function mediaQuery( &$out ) { $s = $this->seek(); $expressions = null; $parts = array(); if ( ( $this->literal( 'only' ) && ( $only = true ) || $this->literal( 'not' ) && ( $not = true ) || true ) && $this->keyword( $mediaType ) ) { $prop = array( 'mediaType' ); if ( isset( $only ) ) { $prop[] = 'only'; } if ( isset( $not ) ) { $prop[] = 'not'; } $prop[] = $mediaType; $parts[] = $prop; } else { $this->seek( $s ); } if ( ! empty( $mediaType ) && ! $this->literal( 'and' ) ) { // ~ } else { $this->genericList( $expressions, 'mediaExpression', 'and', false ); if ( is_array( $expressions ) ) { $parts = array_merge( $parts, $expressions[2] ); } } if ( count( $parts ) == 0 ) { $this->seek( $s ); return false; } $out = $parts; return true; } protected function mediaExpression( &$out ) { $s = $this->seek(); $value = null; if ( $this->literal( '(' ) && $this->keyword( $feature ) && ( $this->literal( ':' ) && $this->expression( $value ) || true ) && $this->literal( ')' ) ) { $out = array( 'mediaExp', $feature ); if ( $value ) { $out[] = $value; } return true; } $this->seek( $s ); return false; } // an unbounded string stopped by $end protected function openString( $end, &$out, $nestingOpen = null, $rejectStrs = null ) { $oldWhite = $this->eatWhiteDefault; $this->eatWhiteDefault = false; $stop = array( "'", '"', '@{', $end ); $stop = array_map( array( 'seedprod_lessc', 'preg_quote' ), $stop ); // $stop[] = self::$commentMulti; if ( ! is_null( $rejectStrs ) ) { $stop = array_merge( $stop, $rejectStrs ); } $patt = '(.*?)(' . implode( '|', $stop ) . ')'; $nestingLevel = 0; $content = array(); while ( $this->match( $patt, $m, false ) ) { if ( ! empty( $m[1] ) ) { $content[] = $m[1]; if ( $nestingOpen ) { $nestingLevel += substr_count( $m[1], $nestingOpen ); } } $tok = $m[2]; $this->count -= strlen( $tok ); if ( $tok == $end ) { if ( $nestingLevel == 0 ) { break; } else { $nestingLevel--; } } if ( ( $tok == "'" || $tok == '"' ) && $this->lstring( $str ) ) { $content[] = $str; continue; } if ( $tok == '@{' && $this->interpolation( $inter ) ) { $content[] = $inter; continue; } if ( in_array( $tok, $rejectStrs ) ) { $count = null; break; } $content[] = $tok; $this->count += strlen( $tok ); } $this->eatWhiteDefault = $oldWhite; if ( count( $content ) == 0 ) { return false; } // trim the end if ( is_string( end( $content ) ) ) { $content[ count( $content ) - 1 ] = rtrim( end( $content ) ); } $out = array( 'string', '', $content ); return true; } protected function lstring( &$out ) { $s = $this->seek(); if ( $this->literal( '"', false ) ) { $delim = '"'; } elseif ( $this->literal( "'", false ) ) { $delim = "'"; } else { return false; } $content = array(); // look for either ending delim , escape, or string interpolation $patt = '([^\n]*?)(@\{|\\\\|' . seedprod_lessc::preg_quote( $delim ) . ')'; $oldWhite = $this->eatWhiteDefault; $this->eatWhiteDefault = false; while ( $this->match( $patt, $m, false ) ) { $content[] = $m[1]; if ( $m[2] == '@{' ) { $this->count -= strlen( $m[2] ); if ( $this->interpolation( $inter, false ) ) { $content[] = $inter; } else { $this->count += strlen( $m[2] ); $content[] = '@{'; // ignore it } } elseif ( $m[2] == '\\' ) { $content[] = $m[2]; if ( $this->literal( $delim, false ) ) { $content[] = $delim; } } else { $this->count -= strlen( $delim ); break; // delim } } $this->eatWhiteDefault = $oldWhite; if ( $this->literal( $delim ) ) { $out = array( 'string', $delim, $content ); return true; } $this->seek( $s ); return false; } protected function interpolation( &$out ) { $oldWhite = $this->eatWhiteDefault; $this->eatWhiteDefault = true; $s = $this->seek(); if ( $this->literal( '@{' ) && $this->keyword( $var ) && $this->literal( '}', false ) ) { $out = array( 'variable', $this->seedprod_lessc->vPrefix . $var ); $this->eatWhiteDefault = $oldWhite; if ( $this->eatWhiteDefault ) { $this->whitespace(); } return true; } $this->eatWhiteDefault = $oldWhite; $this->seek( $s ); return false; } protected function unit( &$unit ) { // speed shortcut if ( isset( $this->buffer[ $this->count ] ) ) { $char = $this->buffer[ $this->count ]; if ( ! ctype_digit( $char ) && $char != '.' ) { return false; } } if ( $this->match( '([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m ) ) { $unit = array( 'number', $m[1], empty( $m[2] ) ? '' : $m[2] ); return true; } return false; } // a # color protected function color( &$out ) { if ( $this->match( '(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m ) ) { if ( strlen( $m[1] ) > 7 ) { $out = array( 'string', '', array( $m[1] ) ); } else { $out = array( 'raw_color', $m[1] ); } return true; } 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 = ',' ) { $s = $this->seek(); if ( ! $this->literal( '(' ) ) { return false; } $values = array(); $isVararg = false; while ( true ) { if ( $this->literal( '...' ) ) { $isVararg = true; 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; } } $values[] = $arg; if ( $isVararg ) { break; } continue; } if ( $this->value( $literal ) ) { $values[] = array( 'lit', $literal ); } if ( ! $this->literal( $delim ) ) { break; } } if ( ! $this->literal( ')' ) ) { $this->seek( $s ); return false; } $args = $values; return true; } // consume a list of tags // this accepts a hanging delimiter protected function tags( &$tags, $simple = false, $delim = ',' ) { $tags = array(); while ( $this->tag( $tt, $simple ) ) { $tags[] = $tt; if ( ! $this->literal( $delim ) ) { break; } } if ( count( $tags ) == 0 ) { return false; } return true; } // list of tags of specifying mixin path // optionally separated by > (lazy, accepts extra >) protected function mixinTags( &$tags ) { $s = $this->seek(); $tags = array(); while ( $this->tag( $tt, true ) ) { $tags[] = $tt; $this->literal( '>' ); } if ( count( $tags ) == 0 ) { return false; } return true; } // a bracketed value (contained within in a tag definition) protected function tagBracket( &$value ) { // 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->seedprod_lessc->parentSelector, '$&$', $value ); return true; } $this->seek( $s ); return false; } protected function tagExpression( &$value ) { $s = $this->seek(); if ( $this->literal( '(' ) && $this->expression( $exp ) && $this->literal( ')' ) ) { $value = array( 'exp', $exp ); return true; } $this->seek( $s ); return false; } // a single tag protected function tag( &$tag, $simple = false ) { if ( $simple ) { $chars = '^,:;{}\][>\(\) "\''; } else { $chars = '^,;{}["\''; } if ( ! $simple && $this->tagExpression( $tag ) ) { return true; } $tag = ''; while ( $this->tagBracket( $first ) ) { $tag .= $first; } while ( true ) { if ( $this->match( '([' . $chars . '0-9][' . $chars . ']*)', $m ) ) { $tag .= $m[1]; if ( $simple ) { break; } while ( $this->tagBracket( $brack ) ) { $tag .= $brack; } continue; } elseif ( $this->unit( $unit ) ) { // for keyframes $tag .= $unit[1] . $unit[2]; continue; } break; } $tag = trim( $tag ); if ( $tag == '' ) { return false; } return true; } // a css function protected function func( &$func ) { $s = $this->seek(); if ( $this->match( '(%|[\w\-_][\w\-_:\.]+|[\w_])', $m ) && $this->literal( '(' ) ) { $fname = $m[1]; $sPreArgs = $this->seek(); $args = array(); while ( true ) { $ss = $this->seek(); // this ugly nonsense is for ie filter properties if ( $this->keyword( $name ) && $this->literal( '=' ) && $this->expressionList( $value ) ) { $args[] = array( 'string', '', array( $name, '=', $value ) ); } else { $this->seek( $ss ); if ( $this->expressionList( $value ) ) { $args[] = $value; } } if ( ! $this->literal( ',' ) ) { break; } } $args = array( 'list', ',', $args ); if ( $this->literal( ')' ) ) { $func = array( 'function', $fname, $args ); return true; } elseif ( $fname == 'url' ) { // couldn't parse and in url? treat as string $this->seek( $sPreArgs ); if ( $this->openString( ')', $string ) && $this->literal( ')' ) ) { $func = array( 'function', $fname, $string ); return true; } } } $this->seek( $s ); return false; } // consume a less variable protected function variable( &$name ) { $s = $this->seek(); if ( $this->literal( $this->seedprod_lessc->vPrefix, false ) && ( $this->variable( $sub ) || $this->keyword( $name ) ) ) { if ( ! empty( $sub ) ) { $name = array( 'variable', $sub ); } else { $name = $this->seedprod_lessc->vPrefix . $name; } return true; } $name = null; $this->seek( $s ); return false; } /** * Consume an assignment operator * Can optionally take a name that will be set to the current property name */ protected function assign( $name = null ) { if ( $name ) { $this->currentProperty = $name; } return $this->literal( ':' ) || $this->literal( '=' ); } // consume a keyword protected function keyword( &$word ) { if ( $this->match( '([\w_\-\*!"][\w\-_"]*)', $m ) ) { $word = $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 ] == '}' ) { // if there is end of file or a closing block next then we don't need a ; return true; } return false; } protected function guards( &$guards ) { $s = $this->seek(); if ( ! $this->literal( 'when' ) ) { $this->seek( $s ); return false; } $guards = array(); while ( $this->guardGroup( $g ) ) { $guards[] = $g; if ( ! $this->literal( ',' ) ) { break; } } if ( count( $guards ) == 0 ) { $guards = null; $this->seek( $s ); return false; } return true; } // a bunch of guards that are and'd together // TODO rename to guardGroup protected function guardGroup( &$guardGroup ) { $s = $this->seek(); $guardGroup = array(); while ( $this->guard( $guard ) ) { $guardGroup[] = $guard; if ( ! $this->literal( 'and' ) ) { break; } } if ( count( $guardGroup ) == 0 ) { $guardGroup = null; $this->seek( $s ); return false; } return true; } protected function guard( &$guard ) { $s = $this->seek(); $negate = $this->literal( 'not' ); if ( $this->literal( '(' ) && $this->expression( $exp ) && $this->literal( ')' ) ) { $guard = $exp; if ( $negate ) { $guard = array( 'negate', $guard ); } return true; } $this->seek( $s ); return false; } /* raw parsing functions */ protected function literal( $what, $eatWhitespace = null ) { if ( $eatWhitespace === null ) { $eatWhitespace = $this->eatWhiteDefault; } // shortcut on single letter 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; } } if ( ! isset( self::$literalCache[ $what ] ) ) { self::$literalCache[ $what ] = seedprod_lessc::preg_quote( $what ); } return $this->match( self::$literalCache[ $what ], $m, $eatWhitespace ); } protected function genericList( &$out, $parseItem, $delim = '', $flatten = true ) { $s = $this->seek(); $items = array(); while ( $this->$parseItem( $value ) ) { $items[] = $value; if ( $delim ) { if ( ! $this->literal( $delim ) ) { break; } } } if ( count( $items ) == 0 ) { $this->seek( $s ); return false; } if ( $flatten && count( $items ) == 1 ) { $out = $items[0]; } else { $out = array( 'list', $delim, $items ); } return true; } // advance counter to next occurrence of $what // $until - don't include $what in advance // $allowNewline, if string, will be used as valid char set protected function to( $what, &$out, $until = false, $allowNewline = false ) { if ( is_string( $allowNewline ) ) { $validChars = $allowNewline; } else { $validChars = $allowNewline ? '.' : "[^\n]"; } if ( ! $this->match( '(' . $validChars . '*?)' . seedprod_lessc::preg_quote( $what ), $m, ! $until ) ) { return false; } if ( $until ) { $this->count -= strlen( $what ); // give back $what } $out = $m[1]; return true; } // try to match something on head of buffer protected function match( $regex, &$out, $eatWhitespace = null ) { if ( $eatWhitespace === null ) { $eatWhitespace = $this->eatWhiteDefault; } $r = '/' . $regex . ( $eatWhitespace && ! $this->writeComments ? '\s*' : '' ) . '/Ais'; if ( preg_match( $r, $this->buffer, $out, null, $this->count ) ) { $this->count += strlen( $out[0] ); if ( $eatWhitespace && $this->writeComments ) { $this->whitespace(); } return true; } return false; } // match some whitespace protected function whitespace() { if ( $this->writeComments ) { $gotWhite = false; while ( preg_match( self::$whitePattern, $this->buffer, $m, null, $this->count ) ) { if ( isset( $m[1] ) && empty( $this->commentsSeen[ $this->count ] ) ) { $this->append( array( 'comment', $m[1] ) ); $this->commentsSeen[ $this->count ] = true; } $this->count += strlen( $m[0] ); $gotWhite = true; } return $gotWhite; } else { $this->match( '', $m ); return strlen( $m[0] ) > 0; } } // match something without consuming it protected function peek( $regex, &$out = null, $from = null ) { if ( is_null( $from ) ) { $from = $this->count; } $r = '/' . $regex . '/Ais'; $result = preg_match( $r, $this->buffer, $out, null, $from ); return $result; } // seek to a spot in the buffer or return where we are on no argument protected function seek( $where = null ) { if ( $where === null ) { return $this->count; } else { $this->count = $where; } return true; } /* misc functions */ public function throwError( $msg = 'parse error', $count = null ) { $count = is_null( $count ) ? $this->count : $count; $line = $this->line + substr_count( substr( $this->buffer, 0, $count ), "\n" ); if ( ! empty( $this->sourceName ) ) { $loc = "$this->sourceName on line $line"; } else { $loc = "line: $line"; } // TODO this depends on $this->count if ( $this->peek( "(.*?)(\n|$)", $m, $count ) ) { throw new exception( "$msg: failed at `$m[1]` $loc" ); } else { throw new exception( "$msg: $loc" ); } } protected function pushBlock( $selectors = null, $type = null ) { $b = new stdclass(); $b->parent = $this->env; $b->type = $type; $b->id = self::$nextBlockId++; $b->isVararg = false; // TODO: kill me from here $b->tags = $selectors; $b->props = array(); $b->children = array(); $this->env = $b; return $b; } // push a block that doesn't multiply tags protected function pushSpecialBlock( $type ) { return $this->pushBlock( null, $type ); } // append a property to the current block protected function append( $prop, $pos = null ) { if ( $pos !== null ) { $prop[-1] = $pos; } $this->env->props[] = $prop; } // pop something off the stack protected function pop() { $old = $this->env; $this->env = $this->env->parent; return $old; } // remove comments from $text // todo: make it work for all functions, not just url protected function removeComments( $text ) { $look = array( 'url(', '//', '/*', '"', "'", ); $out = ''; $min = null; while ( true ) { // find the next item foreach ( $look as $token ) { $pos = strpos( $text, $token ); if ( $pos !== false ) { if ( ! isset( $min ) || $pos < $min[1] ) { $min = array( $token, $pos ); } } } if ( is_null( $min ) ) { break; } $count = $min[1]; $skip = 0; $newlines = 0; switch ( $min[0] ) { case 'url(': if ( preg_match( '/url\(.*?\)/', $text, $m, 0, $count ) ) { $count += strlen( $m[0] ) - strlen( $min[0] ); } break; case '"': case "'": if ( preg_match( '/' . $min[0] . '.*?' . $min[0] . '/', $text, $m, 0, $count ) ) { $count += strlen( $m[0] ) - 1; } break; case '//': $skip = strpos( $text, "\n", $count ); if ( $skip === false ) { $skip = strlen( $text ) - $count; } else { $skip -= $count; } break; case '/*': if ( preg_match( '/\/\*.*?\*\//s', $text, $m, 0, $count ) ) { $skip = strlen( $m[0] ); $newlines = substr_count( $m[0], "\n" ); } break; } if ( $skip == 0 ) { $count += strlen( $min[0] ); } $out .= substr( $text, 0, $count ) . str_repeat( "\n", $newlines ); $text = substr( $text, $count + $skip ); $min = null; } return $out . $text; } } class seedprod_lessc_formatter_classic { public $indentChar = ' '; public $break = "\n"; public $open = ' {'; public $close = '}'; public $selectorSeparator = ', '; public $assignSeparator = ':'; public $openSingle = ' { '; public $closeSingle = ' }'; public $disableSingle = false; public $breakSelectors = false; public $compressColors = false; public function __construct() { $this->indentLevel = 0; } public function indentStr( $n = 0 ) { return str_repeat( $this->indentChar, max( $this->indentLevel + $n, 0 ) ); } public function property( $name, $value ) { return $name . $this->assignSeparator . $value . ';'; } protected function isEmpty( $block ) { if ( empty( $block->lines ) ) { foreach ( $block->children as $child ) { if ( ! $this->isEmpty( $child ) ) { return false; } } return true; } return false; } public function block( $block ) { if ( $this->isEmpty( $block ) ) { return; } $inner = $pre = $this->indentStr(); $isSingle = ! $this->disableSingle && is_null( $block->type ) && count( $block->lines ) == 1; if ( ! empty( $block->selectors ) ) { $this->indentLevel++; if ( $this->breakSelectors ) { $selectorSeparator = $this->selectorSeparator . $this->break . $pre; } else { $selectorSeparator = $this->selectorSeparator; } echo $pre . implode( $selectorSeparator, $block->selectors ); if ( $isSingle ) { echo $this->openSingle; $inner = ''; } else { echo $this->open . $this->break; $inner = $this->indentStr(); } } if ( ! empty( $block->lines ) ) { $glue = $this->break . $inner; echo $inner . implode( $glue, $block->lines ); if ( ! $isSingle && ! empty( $block->children ) ) { echo $this->break; } } foreach ( $block->children as $child ) { $this->block( $child ); } if ( ! empty( $block->selectors ) ) { if ( ! $isSingle && empty( $block->children ) ) { echo $this->break; } if ( $isSingle ) { echo $this->closeSingle . $this->break; } else { echo $pre . $this->close . $this->break; } $this->indentLevel--; } } } class seedprod_lessc_formatter_compressed extends seedprod_lessc_formatter_classic { public $disableSingle = true; public $open = '{'; public $selectorSeparator = ','; public $assignSeparator = ':'; public $break = ''; public $compressColors = true; public function indentStr( $n = 0 ) { return ''; } } class seedprod_lessc_formatter_lessjs extends seedprod_lessc_formatter_classic { public $disableSingle = true; public $breakSelectors = true; public $assignSeparator = ': '; public $selectorSeparator = ','; }