diff --git a/composer.json b/composer.json index 5da1f2385..f73391383 100644 --- a/composer.json +++ b/composer.json @@ -1,39 +1,44 @@ { - "name": "rhymix/rhymix", - "description": "Rhymix", - "homepage": "https://www.rhymix.org", - "license": "GPL-2.0+", - "type": "project", - "authors": [ - { "name": "Rhymix Developers and Contributors", "email": "devops@rhymix.org" }, - { "name": "NAVER", "email": "developers@xpressengine.com" } - ], - "require": { - "php": ">=5.3.3", - "ext-curl": "*", - "ext-gd": "*", - "ext-iconv": "*", - "ext-json": "*", - "ext-mbstring": "*", - "ext-mcrypt": "*", - "ext-openssl": "*", - "ext-pcre": "*", - "ext-xml": "*", - "defuse/php-encryption": "1.2.1", - "ezyang/htmlpurifier": "4.7.*", - "firephp/firephp-core": "0.4.0", - "hautelook/phpass": "0.3.*", - "matthiasmullie/minify": "1.3.*", - "michelf/php-markdown": "1.5.*", - "rmccue/requests": "1.6.*", - "sunra/php-simple-html-dom-parser": "1.5.*", + "name": "rhymix/rhymix", + "description": "Rhymix", + "homepage": "https://www.rhymix.org", + "license": "GPL-2.0+", + "type": "project", + "authors": [ + { "name": "Rhymix Developers and Contributors", "email": "devops@rhymix.org" }, + { "name": "NAVER", "email": "developers@xpressengine.com" } + ], + "require": { + "php": ">=5.3.3", + "ext-curl": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-mcrypt": "*", + "ext-openssl": "*", + "ext-pcre": "*", + "ext-xml": "*", + "defuse/php-encryption": "1.2.1", + "ezyang/htmlpurifier": "4.7.*", + "firephp/firephp-core": "0.4.0", + "hautelook/phpass": "0.3.*", + "jbbcode/jbbcode": "1.3.*", + "leafo/lessphp": "0.5.*", + "leafo/scssphp": "0.4.*", + "league/html-to-markdown": "4.2.*", + "matthiasmullie/minify": "1.3.*", + "michelf/php-markdown": "1.6.*", + "michelf/php-smartypants": "1.6.0-beta1", + "rmccue/requests": "1.6.*", + "sunra/php-simple-html-dom-parser": "1.5.*", "swiftmailer/swiftmailer": "5.4.*", "true/punycode": "2.*" - }, - "require-dev": { - "php": ">=5.4.0", - "codeception/codeception": "2.1.*", - "codeception/verify": "0.3.*", - "codeception/specify": "0.4.*" - } + }, + "require-dev": { + "php": ">=5.4.0", + "codeception/codeception": "2.1.*", + "codeception/verify": "0.3.*", + "codeception/specify": "0.4.*" + } } diff --git a/composer.lock b/composer.lock index 05bcf4331..faa67f0d3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "9b062f27815a9b2ef1e700b4664ee864", - "content-hash": "69345a9112733f99c09fa298ba72fa22", + "hash": "61ea4cb999dbb61cd01ba1a4ede5ead6", + "content-hash": "ec214228f19d828ce0ea5feeb0a72aba", "packages": [ { "name": "defuse/php-encryption", @@ -176,6 +176,213 @@ ], "time": "2012-08-31 00:00:00" }, + { + "name": "jbbcode/jbbcode", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/jbowens/jBBCode.git", + "reference": "645b6a1c0afa92b7d029d3417ebd8b60a5c578b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jbowens/jBBCode/zipball/645b6a1c0afa92b7d029d3417ebd8b60a5c578b3", + "reference": "645b6a1c0afa92b7d029d3417ebd8b60a5c578b3", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, + "type": "library", + "autoload": { + "psr-0": { + "JBBCode": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jackson Owens", + "email": "jackson_owens@alumni.brown.edu", + "homepage": "http://jbowens.org/", + "role": "Developer" + } + ], + "description": "A lightweight but extensible BBCode parser written in PHP 5.3.", + "homepage": "http://jbbcode.com/", + "keywords": [ + "BB", + "bbcode" + ], + "time": "2014-07-06 05:48:20" + }, + { + "name": "leafo/lessphp", + "version": "v0.5.0", + "source": { + "type": "git", + "url": "https://github.com/leafo/lessphp.git", + "reference": "0f5a7f5545d2bcf4e9fad9a228c8ad89cc9aa283" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/leafo/lessphp/zipball/0f5a7f5545d2bcf4e9fad9a228c8ad89cc9aa283", + "reference": "0f5a7f5545d2bcf4e9fad9a228c8ad89cc9aa283", + "shasum": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "lessc.inc.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT", + "GPL-3.0" + ], + "authors": [ + { + "name": "Leaf Corcoran", + "email": "leafot@gmail.com", + "homepage": "http://leafo.net" + } + ], + "description": "lessphp is a compiler for LESS written in PHP.", + "homepage": "http://leafo.net/lessphp/", + "time": "2014-11-24 18:39:20" + }, + { + "name": "leafo/scssphp", + "version": "v0.4.0", + "source": { + "type": "git", + "url": "https://github.com/leafo/scssphp.git", + "reference": "78a6f27aa4eaf70bb3ff4d13b639bab71fdaf47a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/leafo/scssphp/zipball/78a6f27aa4eaf70bb3ff4d13b639bab71fdaf47a", + "reference": "78a6f27aa4eaf70bb3ff4d13b639bab71fdaf47a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "kherge/box": "~2.5", + "phpunit/phpunit": "~3.7", + "squizlabs/php_codesniffer": "~2.3" + }, + "bin": [ + "bin/pscss" + ], + "type": "library", + "autoload": { + "classmap": [ + "classmap.php" + ], + "psr-4": { + "Leafo\\ScssPhp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Leaf Corcoran", + "email": "leafot@gmail.com", + "homepage": "http://leafo.net" + } + ], + "description": "scssphp is a compiler for SCSS written in PHP.", + "homepage": "http://leafo.github.io/scssphp/", + "keywords": [ + "css", + "less", + "sass", + "scss", + "stylesheet" + ], + "time": "2015-11-09 14:44:09" + }, + { + "name": "league/html-to-markdown", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/html-to-markdown.git", + "reference": "9a5becc8c6b520920fb846afefcfd7faf4c31712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/9a5becc8c6b520920fb846afefcfd7faf4c31712", + "reference": "9a5becc8c6b520920fb846afefcfd7faf4c31712", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "mikehaertl/php-shellcommand": "~1.1.0", + "phpunit/phpunit": "4.*", + "scrutinizer/ocular": "~1.1" + }, + "bin": [ + "bin/html-to-markdown" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.3-dev" + } + }, + "autoload": { + "psr-4": { + "League\\HTMLToMarkdown\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "http://www.colinodell.com", + "role": "Lead Developer" + }, + { + "name": "Nick Cernis", + "email": "nick@cern.is", + "homepage": "http://modernnerd.net", + "role": "Original Author" + } + ], + "description": "An HTML-to-markdown conversion helper for PHP", + "homepage": "https://github.com/thephpleague/html-to-markdown", + "keywords": [ + "html", + "markdown" + ], + "time": "2016-02-01 16:49:02" + }, { "name": "matthiasmullie/minify", "version": "1.3.34", @@ -284,16 +491,16 @@ }, { "name": "michelf/php-markdown", - "version": "1.5.0", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/michelf/php-markdown.git", - "reference": "e1aabe18173231ebcefc90e615565742fc1c7fd9" + "reference": "156e56ee036505ec637d761ee62dc425d807183c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/michelf/php-markdown/zipball/e1aabe18173231ebcefc90e615565742fc1c7fd9", - "reference": "e1aabe18173231ebcefc90e615565742fc1c7fd9", + "url": "https://api.github.com/repos/michelf/php-markdown/zipball/156e56ee036505ec637d761ee62dc425d807183c", + "reference": "156e56ee036505ec637d761ee62dc425d807183c", "shasum": "" }, "require": { @@ -315,15 +522,15 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "John Gruber", - "homepage": "http://daringfireball.net/" - }, { "name": "Michel Fortin", "email": "michel.fortin@michelf.ca", "homepage": "https://michelf.ca/", "role": "Developer" + }, + { + "name": "John Gruber", + "homepage": "https://daringfireball.net/" } ], "description": "PHP Markdown", @@ -331,7 +538,62 @@ "keywords": [ "markdown" ], - "time": "2015-03-01 12:03:08" + "time": "2015-12-24 01:37:31" + }, + { + "name": "michelf/php-smartypants", + "version": "1.6.0-beta1", + "source": { + "type": "git", + "url": "https://github.com/michelf/php-smartypants.git", + "reference": "c0465c6d4c5ab853c2fa45df6c10bce7e35cc137" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/michelf/php-smartypants/zipball/c0465c6d4c5ab853c2fa45df6c10bce7e35cc137", + "reference": "c0465c6d4c5ab853c2fa45df6c10bce7e35cc137", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-lib": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "Michelf": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Michel Fortin", + "email": "michel.fortin@michelf.ca", + "homepage": "http://michelf.ca/", + "role": "Developer" + }, + { + "name": "John Gruber", + "homepage": "http://daringfireball.net/" + } + ], + "description": "PHP SmartyPants", + "homepage": "http://michelf.ca/projects/php-smartypants/", + "keywords": [ + "dashes", + "quotes", + "spaces", + "typographer", + "typography" + ], + "time": "2013-07-31 18:13:10" }, { "name": "rmccue/requests", @@ -528,7 +790,9 @@ "packages-dev": null, "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "michelf/php-smartypants": 10 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/vendor/bin/html-to-markdown b/vendor/bin/html-to-markdown new file mode 120000 index 000000000..6f8d9d2b1 --- /dev/null +++ b/vendor/bin/html-to-markdown @@ -0,0 +1 @@ +../league/html-to-markdown/bin/html-to-markdown \ No newline at end of file diff --git a/vendor/bin/pscss b/vendor/bin/pscss new file mode 120000 index 000000000..395e1c8bd --- /dev/null +++ b/vendor/bin/pscss @@ -0,0 +1 @@ +../leafo/scssphp/bin/pscss \ No newline at end of file diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index b85d24e5d..3308cbc96 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -238,6 +238,67 @@ return array( 'HTMLPurifier_VarParser_Native' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/VarParser/Native.php', 'HTMLPurifier_Zipper' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Zipper.php', 'Hautelook\\Phpass\\PasswordHash' => $vendorDir . '/hautelook/phpass/src/Hautelook/Phpass/PasswordHash.php', + 'JBBCode\\CodeDefinition' => $vendorDir . '/jbbcode/jbbcode/JBBCode/CodeDefinition.php', + 'JBBCode\\CodeDefinitionBuilder' => $vendorDir . '/jbbcode/jbbcode/JBBCode/CodeDefinitionBuilder.php', + 'JBBCode\\CodeDefinitionSet' => $vendorDir . '/jbbcode/jbbcode/JBBCode/CodeDefinitionSet.php', + 'JBBCode\\DefaultCodeDefinitionSet' => $vendorDir . '/jbbcode/jbbcode/JBBCode/DefaultCodeDefinitionSet.php', + 'JBBCode\\DocumentElement' => $vendorDir . '/jbbcode/jbbcode/JBBCode/DocumentElement.php', + 'JBBCode\\ElementNode' => $vendorDir . '/jbbcode/jbbcode/JBBCode/ElementNode.php', + 'JBBCode\\InputValidator' => $vendorDir . '/jbbcode/jbbcode/JBBCode/InputValidator.php', + 'JBBCode\\Node' => $vendorDir . '/jbbcode/jbbcode/JBBCode/Node.php', + 'JBBCode\\NodeVisitor' => $vendorDir . '/jbbcode/jbbcode/JBBCode/NodeVisitor.php', + 'JBBCode\\Parser' => $vendorDir . '/jbbcode/jbbcode/JBBCode/Parser.php', + 'JBBCode\\ParserException' => $vendorDir . '/jbbcode/jbbcode/JBBCode/ParserException.php', + 'JBBCode\\TextNode' => $vendorDir . '/jbbcode/jbbcode/JBBCode/TextNode.php', + 'JBBCode\\Tokenizer' => $vendorDir . '/jbbcode/jbbcode/JBBCode/Tokenizer.php', + 'JBBCode\\validators\\CssColorValidator' => $vendorDir . '/jbbcode/jbbcode/JBBCode/validators/CssColorValidator.php', + 'JBBCode\\validators\\UrlValidator' => $vendorDir . '/jbbcode/jbbcode/JBBCode/validators/UrlValidator.php', + 'JBBCode\\visitors\\HTMLSafeVisitor' => $vendorDir . '/jbbcode/jbbcode/JBBCode/visitors/HTMLSafeVisitor.php', + 'JBBCode\\visitors\\NestLimitVisitor' => $vendorDir . '/jbbcode/jbbcode/JBBCode/visitors/NestLimitVisitor.php', + 'JBBCode\\visitors\\SmileyVisitor' => $vendorDir . '/jbbcode/jbbcode/JBBCode/visitors/SmileyVisitor.php', + 'JBBCode\\visitors\\TagCountingVisitor' => $vendorDir . '/jbbcode/jbbcode/JBBCode/visitors/TagCountingVisitor.php', + 'Leafo\\ScssPhp\\Base\\Range' => $vendorDir . '/leafo/scssphp/src/Base/Range.php', + 'Leafo\\ScssPhp\\Block' => $vendorDir . '/leafo/scssphp/src/Block.php', + 'Leafo\\ScssPhp\\Colors' => $vendorDir . '/leafo/scssphp/src/Colors.php', + 'Leafo\\ScssPhp\\Compiler' => $vendorDir . '/leafo/scssphp/src/Compiler.php', + 'Leafo\\ScssPhp\\Compiler\\Environment' => $vendorDir . '/leafo/scssphp/src/Compiler/Environment.php', + 'Leafo\\ScssPhp\\Formatter' => $vendorDir . '/leafo/scssphp/src/Formatter.php', + 'Leafo\\ScssPhp\\Formatter\\Compact' => $vendorDir . '/leafo/scssphp/src/Formatter/Compact.php', + 'Leafo\\ScssPhp\\Formatter\\Compressed' => $vendorDir . '/leafo/scssphp/src/Formatter/Compressed.php', + 'Leafo\\ScssPhp\\Formatter\\Crunched' => $vendorDir . '/leafo/scssphp/src/Formatter/Crunched.php', + 'Leafo\\ScssPhp\\Formatter\\Debug' => $vendorDir . '/leafo/scssphp/src/Formatter/Debug.php', + 'Leafo\\ScssPhp\\Formatter\\Expanded' => $vendorDir . '/leafo/scssphp/src/Formatter/Expanded.php', + 'Leafo\\ScssPhp\\Formatter\\Nested' => $vendorDir . '/leafo/scssphp/src/Formatter/Nested.php', + 'Leafo\\ScssPhp\\Formatter\\OutputBlock' => $vendorDir . '/leafo/scssphp/src/Formatter/OutputBlock.php', + 'Leafo\\ScssPhp\\Node' => $vendorDir . '/leafo/scssphp/src/Node.php', + 'Leafo\\ScssPhp\\Node\\Number' => $vendorDir . '/leafo/scssphp/src/Node/Number.php', + 'Leafo\\ScssPhp\\Parser' => $vendorDir . '/leafo/scssphp/src/Parser.php', + 'Leafo\\ScssPhp\\Server' => $vendorDir . '/leafo/scssphp/src/Server.php', + 'Leafo\\ScssPhp\\Type' => $vendorDir . '/leafo/scssphp/src/Type.php', + 'Leafo\\ScssPhp\\Util' => $vendorDir . '/leafo/scssphp/src/Util.php', + 'Leafo\\ScssPhp\\Version' => $vendorDir . '/leafo/scssphp/src/Version.php', + 'League\\HTMLToMarkdown\\Configuration' => $vendorDir . '/league/html-to-markdown/src/Configuration.php', + 'League\\HTMLToMarkdown\\ConfigurationAwareInterface' => $vendorDir . '/league/html-to-markdown/src/ConfigurationAwareInterface.php', + 'League\\HTMLToMarkdown\\Converter\\BlockquoteConverter' => $vendorDir . '/league/html-to-markdown/src/Converter/BlockquoteConverter.php', + 'League\\HTMLToMarkdown\\Converter\\CommentConverter' => $vendorDir . '/league/html-to-markdown/src/Converter/CommentConverter.php', + 'League\\HTMLToMarkdown\\Converter\\ConverterInterface' => $vendorDir . '/league/html-to-markdown/src/Converter/ConverterInterface.php', + 'League\\HTMLToMarkdown\\Converter\\DefaultConverter' => $vendorDir . '/league/html-to-markdown/src/Converter/DefaultConverter.php', + 'League\\HTMLToMarkdown\\Converter\\DivConverter' => $vendorDir . '/league/html-to-markdown/src/Converter/DivConverter.php', + 'League\\HTMLToMarkdown\\Converter\\EmphasisConverter' => $vendorDir . '/league/html-to-markdown/src/Converter/EmphasisConverter.php', + 'League\\HTMLToMarkdown\\Converter\\HardBreakConverter' => $vendorDir . '/league/html-to-markdown/src/Converter/HardBreakConverter.php', + 'League\\HTMLToMarkdown\\Converter\\HeaderConverter' => $vendorDir . '/league/html-to-markdown/src/Converter/HeaderConverter.php', + 'League\\HTMLToMarkdown\\Converter\\HorizontalRuleConverter' => $vendorDir . '/league/html-to-markdown/src/Converter/HorizontalRuleConverter.php', + 'League\\HTMLToMarkdown\\Converter\\ImageConverter' => $vendorDir . '/league/html-to-markdown/src/Converter/ImageConverter.php', + 'League\\HTMLToMarkdown\\Converter\\LinkConverter' => $vendorDir . '/league/html-to-markdown/src/Converter/LinkConverter.php', + 'League\\HTMLToMarkdown\\Converter\\ListBlockConverter' => $vendorDir . '/league/html-to-markdown/src/Converter/ListBlockConverter.php', + 'League\\HTMLToMarkdown\\Converter\\ListItemConverter' => $vendorDir . '/league/html-to-markdown/src/Converter/ListItemConverter.php', + 'League\\HTMLToMarkdown\\Converter\\ParagraphConverter' => $vendorDir . '/league/html-to-markdown/src/Converter/ParagraphConverter.php', + 'League\\HTMLToMarkdown\\Converter\\PreformattedConverter' => $vendorDir . '/league/html-to-markdown/src/Converter/PreformattedConverter.php', + 'League\\HTMLToMarkdown\\Converter\\TextConverter' => $vendorDir . '/league/html-to-markdown/src/Converter/TextConverter.php', + 'League\\HTMLToMarkdown\\Element' => $vendorDir . '/league/html-to-markdown/src/Element.php', + 'League\\HTMLToMarkdown\\ElementInterface' => $vendorDir . '/league/html-to-markdown/src/ElementInterface.php', + 'League\\HTMLToMarkdown\\Environment' => $vendorDir . '/league/html-to-markdown/src/Environment.php', + 'League\\HTMLToMarkdown\\HtmlConverter' => $vendorDir . '/league/html-to-markdown/src/HtmlConverter.php', 'MatthiasMullie\\Minify\\CSS' => $vendorDir . '/matthiasmullie/minify/src/CSS.php', 'MatthiasMullie\\Minify\\Exception' => $vendorDir . '/matthiasmullie/minify/src/Exception.php', 'MatthiasMullie\\Minify\\Exceptions\\BasicException' => $vendorDir . '/matthiasmullie/minify/src/Exceptions/BasicException.php', @@ -304,4 +365,16 @@ return array( 'Requests_Utility_FilteredIterator' => $vendorDir . '/rmccue/requests/library/Requests/Utility/FilteredIterator.php', 'Sunra\\PhpSimple\\HtmlDomParser' => $vendorDir . '/sunra/php-simple-html-dom-parser/Src/Sunra/PhpSimple/HtmlDomParser.php', 'TrueBV\\Punycode' => $vendorDir . '/true/punycode/src/Punycode.php', + 'lessc' => $vendorDir . '/leafo/lessphp/lessc.inc.php', + 'lessc_formatter_classic' => $vendorDir . '/leafo/lessphp/lessc.inc.php', + 'lessc_formatter_compressed' => $vendorDir . '/leafo/lessphp/lessc.inc.php', + 'lessc_formatter_lessjs' => $vendorDir . '/leafo/lessphp/lessc.inc.php', + 'lessc_parser' => $vendorDir . '/leafo/lessphp/lessc.inc.php', + 'scss_formatter' => $vendorDir . '/leafo/scssphp/classmap.php', + 'scss_formatter_compressed' => $vendorDir . '/leafo/scssphp/classmap.php', + 'scss_formatter_crunched' => $vendorDir . '/leafo/scssphp/classmap.php', + 'scss_formatter_nested' => $vendorDir . '/leafo/scssphp/classmap.php', + 'scss_parser' => $vendorDir . '/leafo/scssphp/classmap.php', + 'scss_server' => $vendorDir . '/leafo/scssphp/classmap.php', + 'scssc' => $vendorDir . '/leafo/scssphp/classmap.php', ); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php index 942fa8072..9d294bbb4 100644 --- a/vendor/composer/autoload_namespaces.php +++ b/vendor/composer/autoload_namespaces.php @@ -8,7 +8,8 @@ $baseDir = dirname($vendorDir); return array( 'Sunra\\PhpSimple\\HtmlDomParser' => array($vendorDir . '/sunra/php-simple-html-dom-parser/Src'), 'Requests' => array($vendorDir . '/rmccue/requests/library'), - 'Michelf' => array($vendorDir . '/michelf/php-markdown'), + 'Michelf' => array($vendorDir . '/michelf/php-markdown', $vendorDir . '/michelf/php-smartypants'), + 'JBBCode' => array($vendorDir . '/jbbcode/jbbcode'), 'Hautelook' => array($vendorDir . '/hautelook/phpass/src'), 'HTMLPurifier' => array($vendorDir . '/ezyang/htmlpurifier/library'), ); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index fada27478..30cc463c6 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -9,4 +9,6 @@ return array( 'TrueBV\\' => array($vendorDir . '/true/punycode/src'), 'MatthiasMullie\\PathConverter\\' => array($vendorDir . '/matthiasmullie/path-converter/src'), 'MatthiasMullie\\Minify\\' => array($vendorDir . '/matthiasmullie/minify/src'), + 'League\\HTMLToMarkdown\\' => array($vendorDir . '/league/html-to-markdown/src'), + 'Leafo\\ScssPhp\\' => array($vendorDir . '/leafo/scssphp/src'), ); diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 18e6cbbfb..77f6e6a65 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -92,59 +92,6 @@ "html" ] }, - { - "name": "michelf/php-markdown", - "version": "1.5.0", - "version_normalized": "1.5.0.0", - "source": { - "type": "git", - "url": "https://github.com/michelf/php-markdown.git", - "reference": "e1aabe18173231ebcefc90e615565742fc1c7fd9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/michelf/php-markdown/zipball/e1aabe18173231ebcefc90e615565742fc1c7fd9", - "reference": "e1aabe18173231ebcefc90e615565742fc1c7fd9", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "time": "2015-03-01 12:03:08", - "type": "library", - "extra": { - "branch-alias": { - "dev-lib": "1.4.x-dev" - } - }, - "installation-source": "dist", - "autoload": { - "psr-0": { - "Michelf": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "John Gruber", - "homepage": "http://daringfireball.net/" - }, - { - "name": "Michel Fortin", - "email": "michel.fortin@michelf.ca", - "homepage": "https://michelf.ca/", - "role": "Developer" - } - ], - "description": "PHP Markdown", - "homepage": "https://michelf.ca/projects/php-markdown/", - "keywords": [ - "markdown" - ] - }, { "name": "sunra/php-simple-html-dom-parser", "version": "v1.5.0", @@ -537,5 +484,330 @@ "password", "security" ] + }, + { + "name": "michelf/php-markdown", + "version": "1.6.0", + "version_normalized": "1.6.0.0", + "source": { + "type": "git", + "url": "https://github.com/michelf/php-markdown.git", + "reference": "156e56ee036505ec637d761ee62dc425d807183c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/michelf/php-markdown/zipball/156e56ee036505ec637d761ee62dc425d807183c", + "reference": "156e56ee036505ec637d761ee62dc425d807183c", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2015-12-24 01:37:31", + "type": "library", + "extra": { + "branch-alias": { + "dev-lib": "1.4.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-0": { + "Michelf": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Michel Fortin", + "email": "michel.fortin@michelf.ca", + "homepage": "https://michelf.ca/", + "role": "Developer" + }, + { + "name": "John Gruber", + "homepage": "https://daringfireball.net/" + } + ], + "description": "PHP Markdown", + "homepage": "https://michelf.ca/projects/php-markdown/", + "keywords": [ + "markdown" + ] + }, + { + "name": "jbbcode/jbbcode", + "version": "v1.3.0", + "version_normalized": "1.3.0.0", + "source": { + "type": "git", + "url": "https://github.com/jbowens/jBBCode.git", + "reference": "645b6a1c0afa92b7d029d3417ebd8b60a5c578b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jbowens/jBBCode/zipball/645b6a1c0afa92b7d029d3417ebd8b60a5c578b3", + "reference": "645b6a1c0afa92b7d029d3417ebd8b60a5c578b3", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, + "time": "2014-07-06 05:48:20", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "JBBCode": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jackson Owens", + "email": "jackson_owens@alumni.brown.edu", + "homepage": "http://jbowens.org/", + "role": "Developer" + } + ], + "description": "A lightweight but extensible BBCode parser written in PHP 5.3.", + "homepage": "http://jbbcode.com/", + "keywords": [ + "BB", + "bbcode" + ] + }, + { + "name": "leafo/lessphp", + "version": "v0.5.0", + "version_normalized": "0.5.0.0", + "source": { + "type": "git", + "url": "https://github.com/leafo/lessphp.git", + "reference": "0f5a7f5545d2bcf4e9fad9a228c8ad89cc9aa283" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/leafo/lessphp/zipball/0f5a7f5545d2bcf4e9fad9a228c8ad89cc9aa283", + "reference": "0f5a7f5545d2bcf4e9fad9a228c8ad89cc9aa283", + "shasum": "" + }, + "time": "2014-11-24 18:39:20", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.4.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "lessc.inc.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT", + "GPL-3.0" + ], + "authors": [ + { + "name": "Leaf Corcoran", + "email": "leafot@gmail.com", + "homepage": "http://leafo.net" + } + ], + "description": "lessphp is a compiler for LESS written in PHP.", + "homepage": "http://leafo.net/lessphp/" + }, + { + "name": "leafo/scssphp", + "version": "v0.4.0", + "version_normalized": "0.4.0.0", + "source": { + "type": "git", + "url": "https://github.com/leafo/scssphp.git", + "reference": "78a6f27aa4eaf70bb3ff4d13b639bab71fdaf47a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/leafo/scssphp/zipball/78a6f27aa4eaf70bb3ff4d13b639bab71fdaf47a", + "reference": "78a6f27aa4eaf70bb3ff4d13b639bab71fdaf47a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "kherge/box": "~2.5", + "phpunit/phpunit": "~3.7", + "squizlabs/php_codesniffer": "~2.3" + }, + "time": "2015-11-09 14:44:09", + "bin": [ + "bin/pscss" + ], + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "classmap.php" + ], + "psr-4": { + "Leafo\\ScssPhp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Leaf Corcoran", + "email": "leafot@gmail.com", + "homepage": "http://leafo.net" + } + ], + "description": "scssphp is a compiler for SCSS written in PHP.", + "homepage": "http://leafo.github.io/scssphp/", + "keywords": [ + "css", + "less", + "sass", + "scss", + "stylesheet" + ] + }, + { + "name": "league/html-to-markdown", + "version": "4.2.0", + "version_normalized": "4.2.0.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/html-to-markdown.git", + "reference": "9a5becc8c6b520920fb846afefcfd7faf4c31712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/9a5becc8c6b520920fb846afefcfd7faf4c31712", + "reference": "9a5becc8c6b520920fb846afefcfd7faf4c31712", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "mikehaertl/php-shellcommand": "~1.1.0", + "phpunit/phpunit": "4.*", + "scrutinizer/ocular": "~1.1" + }, + "time": "2016-02-01 16:49:02", + "bin": [ + "bin/html-to-markdown" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.3-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "League\\HTMLToMarkdown\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "http://www.colinodell.com", + "role": "Lead Developer" + }, + { + "name": "Nick Cernis", + "email": "nick@cern.is", + "homepage": "http://modernnerd.net", + "role": "Original Author" + } + ], + "description": "An HTML-to-markdown conversion helper for PHP", + "homepage": "https://github.com/thephpleague/html-to-markdown", + "keywords": [ + "html", + "markdown" + ] + }, + { + "name": "michelf/php-smartypants", + "version": "1.6.0-beta1", + "version_normalized": "1.6.0.0-beta1", + "source": { + "type": "git", + "url": "https://github.com/michelf/php-smartypants.git", + "reference": "c0465c6d4c5ab853c2fa45df6c10bce7e35cc137" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/michelf/php-smartypants/zipball/c0465c6d4c5ab853c2fa45df6c10bce7e35cc137", + "reference": "c0465c6d4c5ab853c2fa45df6c10bce7e35cc137", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2013-07-31 18:13:10", + "type": "library", + "extra": { + "branch-alias": { + "dev-lib": "1.6.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-0": { + "Michelf": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Michel Fortin", + "email": "michel.fortin@michelf.ca", + "homepage": "http://michelf.ca/", + "role": "Developer" + }, + { + "name": "John Gruber", + "homepage": "http://daringfireball.net/" + } + ], + "description": "PHP SmartyPants", + "homepage": "http://michelf.ca/projects/php-smartypants/", + "keywords": [ + "dashes", + "quotes", + "spaces", + "typographer", + "typography" + ] } ] diff --git a/vendor/jbbcode/jbbcode/.gitignore b/vendor/jbbcode/jbbcode/.gitignore new file mode 100644 index 000000000..987e2a253 --- /dev/null +++ b/vendor/jbbcode/jbbcode/.gitignore @@ -0,0 +1,2 @@ +composer.lock +vendor diff --git a/vendor/jbbcode/jbbcode/JBBCode/CodeDefinition.php b/vendor/jbbcode/jbbcode/JBBCode/CodeDefinition.php new file mode 100644 index 000000000..f7a07f75e --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/CodeDefinition.php @@ -0,0 +1,328 @@ +elCounter = 0; + $def->setTagName($tagName); + $def->setReplacementText($replacementText); + $def->useOption = $useOption; + $def->parseContent = $parseContent; + $def->nestLimit = $nestLimit; + $def->optionValidator = $optionValidator; + $def->bodyValidator = $bodyValidator; + return $def; + } + + /** + * Constructs a new CodeDefinition. + * + * This constructor is deprecated. You should use the static construct() method or the + * CodeDefinitionBuilder class to construct a new CodeDefiniton. + * + * @deprecated + */ + public function __construct() + { + /* WARNING: This function is deprecated and will be made protected in a future + * version of jBBCode. */ + $this->parseContent = true; + $this->useOption = false; + $this->nestLimit = -1; + $this->elCounter = 0; + $this->optionValidator = array(); + $this->bodyValidator = null; + } + + /** + * Determines if the arguments to the given element are valid based on + * any validators attached to this CodeDefinition. + * + * @param $el the ElementNode to validate + * @return true if the ElementNode's {option} and {param} are OK, false if they're not + */ + public function hasValidInputs(ElementNode $el) + { + if ($this->usesOption() && $this->optionValidator) { + $att = $el->getAttribute(); + + foreach($att as $name => $value){ + if(isset($this->optionValidator[$name]) && !$this->optionValidator[$name]->validate($value)){ + return false; + } + } + } + + if (!$this->parseContent() && $this->bodyValidator) { + /* We only evaluate the content if we're not parsing the content. */ + $content = ""; + foreach ($el->getChildren() as $child) { + $content .= $child->getAsBBCode(); + } + if (!$this->bodyValidator->validate($content)) { + /* The content of the element is not valid. */ + return false; + } + } + + return true; + } + + /** + * Accepts an ElementNode that is defined by this CodeDefinition and returns the HTML + * markup of the element. This is a commonly overridden class for custom CodeDefinitions + * so that the content can be directly manipulated. + * + * @param $el the element to return an html representation of + * + * @return the parsed html of this element (INCLUDING ITS CHILDREN) + */ + public function asHtml(ElementNode $el) + { + if (!$this->hasValidInputs($el)) { + return $el->getAsBBCode(); + } + + $html = $this->getReplacementText(); + + if ($this->usesOption()) { + $options = $el->getAttribute(); + if(count($options)==1){ + $vals = array_values($options); + $html = str_ireplace('{option}', reset($vals), $html); + } + else{ + foreach($options as $key => $val){ + $html = str_ireplace('{' . $key . '}', $val, $html); + } + } + } + + $content = $this->getContent($el); + + $html = str_ireplace('{param}', $content, $html); + + return $html; + } + + protected function getContent(ElementNode $el){ + if ($this->parseContent()) { + $content = ""; + foreach ($el->getChildren() as $child) + $content .= $child->getAsHTML(); + } else { + $content = ""; + foreach ($el->getChildren() as $child) + $content .= $child->getAsBBCode(); + } + return $content; + } + + /** + * Accepts an ElementNode that is defined by this CodeDefinition and returns the text + * representation of the element. This may be overridden by a custom CodeDefinition. + * + * @param $el the element to return a text representation of + * + * @return the text representation of $el + */ + public function asText(ElementNode $el) + { + if (!$this->hasValidInputs($el)) { + return $el->getAsBBCode(); + } + + $s = ""; + foreach ($el->getChildren() as $child) + $s .= $child->getAsText(); + return $s; + } + + /** + * Returns the tag name of this code definition + * + * @return this definition's associated tag name + */ + public function getTagName() + { + return $this->tagName; + } + + /** + * Returns the replacement text of this code definition. This usually has little, if any meaning if the + * CodeDefinition class was extended. For default, html replacement CodeDefinitions this returns the html + * markup for the definition. + * + * @return the replacement text of this CodeDefinition + */ + public function getReplacementText() + { + return $this->replacementText; + } + + /** + * Returns whether or not this CodeDefinition uses the optional {option} + * + * @return true if this CodeDefinition uses the option, false otherwise + */ + public function usesOption() + { + return $this->useOption; + } + + /** + * Returns whether or not this CodeDefnition parses elements contained within it, + * or just treats its children as text. + * + * @return true if this CodeDefinition parses elements contained within itself + */ + public function parseContent() + { + return $this->parseContent; + } + + /** + * Returns the limit of how many elements defined by this CodeDefinition may be + * nested together. If after parsing elements are nested beyond this limit, the + * subtrees formed by those nodes will be removed from the parse tree. A nest + * limit of -1 signifies no limit. + */ + public function getNestLimit() + { + return $this->nestLimit; + } + + /** + * Sets the tag name of this CodeDefinition + * + * @deprecated + * + * @param the new tag name of this definition + */ + public function setTagName($tagName) + { + $this->tagName = strtolower($tagName); + } + + /** + * Sets the html replacement text of this CodeDefinition + * + * @deprecated + * + * @param the new replacement text + */ + public function setReplacementText($txt) + { + $this->replacementText = $txt; + } + + /** + * Sets whether or not this CodeDefinition uses the {option} + * + * @deprecated + * + * @param boolean $bool + */ + public function setUseOption($bool) + { + $this->useOption = $bool; + } + + /** + * Sets whether or not this CodeDefinition allows its children to be parsed as html + * + * @deprecated + * + * @param boolean $bool + */ + public function setParseContent($bool) + { + $this->parseContent = $bool; + } + + /** + * Increments the element counter. This is used for tracking depth of elements of the same type for next limits. + * + * @deprecated + * + * @return void + */ + public function incrementCounter() + { + $this->elCounter++; + } + + /** + * Decrements the element counter. + * + * @deprecated + * + * @return void + */ + public function decrementCounter() + { + $this->elCounter--; + } + + /** + * Resets the element counter. + * + * @deprecated + */ + public function resetCounter() + { + $this->elCounter = 0; + } + + /** + * Returns the current value of the element counter. + * + * @deprecated + * + * @return int + */ + public function getCounter() + { + return $this->elCounter; + } +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/CodeDefinitionBuilder.php b/vendor/jbbcode/jbbcode/JBBCode/CodeDefinitionBuilder.php new file mode 100644 index 000000000..6e8bbc190 --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/CodeDefinitionBuilder.php @@ -0,0 +1,160 @@ +tagName = $tagName; + $this->replacementText = $replacementText; + } + + /** + * Sets the tag name the CodeDefinition should be built with. + * + * @param $tagName the tag name for the new CodeDefinition + */ + public function setTagName($tagName) + { + $this->tagName = $tagName; + return $this; + } + + /** + * Sets the replacement text that the new CodeDefinition should be + * built with. + * + * @param $replacementText the replacement text for the new CodeDefinition + */ + public function setReplacementText($replacementText) + { + $this->replacementText = $replacementText; + return $this; + } + + /** + * Set whether or not the built CodeDefinition should use the {option} bbcode + * argument. + * + * @param $option ture iff the definition includes an option + */ + public function setUseOption($option) + { + $this->useOption = $option; + return $this; + } + + /** + * Set whether or not the built CodeDefinition should allow its content + * to be parsed and evaluated as bbcode. + * + * @param $parseContent true iff the content should be parsed + */ + public function setParseContent($parseContent) + { + $this->parseContent = $parseContent; + return $this; + } + + /** + * Sets the nest limit for this code definition. + * + * @param $nestLimit a positive integer, or -1 if there is no limit. + * @throws \InvalidArgumentException if the nest limit is invalid + */ + public function setNestLimit($limit) + { + if(!is_int($limit) || ($limit <= 0 && -1 != $limit)) { + throw new \InvalidArgumentException("A nest limit must be a positive integer " . + "or -1."); + } + $this->nestLimit = $limit; + return $this; + } + + /** + * Sets the InputValidator that option arguments should be validated with. + * + * @param $validator the InputValidator instance to use + */ + public function setOptionValidator(\JBBCode\InputValidator $validator, $option=null) + { + if(empty($option)){ + $option = $this->tagName; + } + $this->optionValidator[$option] = $validator; + return $this; + } + + /** + * Sets the InputValidator that body ({param}) text should be validated with. + * + * @param $validator the InputValidator instance to use + */ + public function setBodyValidator(\JBBCode\InputValidator $validator) + { + $this->bodyValidator = $validator; + return $this; + } + + /** + * Removes the attached option validator if one is attached. + */ + public function removeOptionValidator() + { + $this->optionValidator = array(); + return $this; + } + + /** + * Removes the attached body validator if one is attached. + */ + public function removeBodyValidator() + { + $this->bodyValidator = null; + return $this; + } + + /** + * Builds a CodeDefinition with the current state of the builder. + * + * @return a new CodeDefinition instance + */ + public function build() + { + $definition = CodeDefinition::construct($this->tagName, + $this->replacementText, + $this->useOption, + $this->parseContent, + $this->nestLimit, + $this->optionValidator, + $this->bodyValidator); + return $definition; + } + + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/CodeDefinitionSet.php b/vendor/jbbcode/jbbcode/JBBCode/CodeDefinitionSet.php new file mode 100644 index 000000000..1b166503a --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/CodeDefinitionSet.php @@ -0,0 +1,22 @@ +{param}'); + array_push($this->definitions, $builder->build()); + + /* [i] italics tag */ + $builder = new CodeDefinitionBuilder('i', '{param}'); + array_push($this->definitions, $builder->build()); + + /* [u] underline tag */ + $builder = new CodeDefinitionBuilder('u', '{param}'); + array_push($this->definitions, $builder->build()); + + $urlValidator = new \JBBCode\validators\UrlValidator(); + + /* [url] link tag */ + $builder = new CodeDefinitionBuilder('url', '{param}'); + $builder->setParseContent(false)->setBodyValidator($urlValidator); + array_push($this->definitions, $builder->build()); + + /* [url=http://example.com] link tag */ + $builder = new CodeDefinitionBuilder('url', '{param}'); + $builder->setUseOption(true)->setParseContent(true)->setOptionValidator($urlValidator); + array_push($this->definitions, $builder->build()); + + /* [img] image tag */ + $builder = new CodeDefinitionBuilder('img', ''); + $builder->setUseOption(false)->setParseContent(false)->setBodyValidator($urlValidator); + array_push($this->definitions, $builder->build()); + + /* [img=alt text] image tag */ + $builder = new CodeDefinitionBuilder('img', '{option}'); + $builder->setUseOption(true)->setParseContent(false)->setBodyValidator($urlValidator); + array_push($this->definitions, $builder->build()); + + /* [color] color tag */ + $builder = new CodeDefinitionBuilder('color', '{param}'); + $builder->setUseOption(true)->setOptionValidator(new \JBBCode\validators\CssColorValidator()); + array_push($this->definitions, $builder->build()); + } + + /** + * Returns an array of the default code definitions. + */ + public function getCodeDefinitions() + { + return $this->definitions; + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/DocumentElement.php b/vendor/jbbcode/jbbcode/JBBCode/DocumentElement.php new file mode 100644 index 000000000..54b40c60f --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/DocumentElement.php @@ -0,0 +1,67 @@ +setTagName("Document"); + $this->setNodeId(0); + } + + /** + * (non-PHPdoc) + * @see JBBCode.ElementNode::getAsBBCode() + * + * Returns the BBCode representation of this document + * + * @return this document's bbcode representation + */ + public function getAsBBCode() + { + $s = ""; + foreach($this->getChildren() as $child){ + $s .= $child->getAsBBCode(); + } + + return $s; + } + + /** + * (non-PHPdoc) + * @see JBBCode.ElementNode::getAsHTML() + * + * Documents don't add any html. They only exist as a container for their + * children, so getAsHTML() simply iterates through the document's children, + * returning their html. + * + * @return the HTML representation of this document + */ + public function getAsHTML() + { + $s = ""; + foreach($this->getChildren() as $child) + $s .= $child->getAsHTML(); + + return $s; + } + + public function accept(NodeVisitor $visitor) + { + $visitor->visitDocumentElement($this); + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/ElementNode.php b/vendor/jbbcode/jbbcode/JBBCode/ElementNode.php new file mode 100644 index 000000000..5393bb19b --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/ElementNode.php @@ -0,0 +1,241 @@ +children = array(); + $this->nestDepth = 0; + } + + /** + * Accepts the given NodeVisitor. This is part of an implementation + * of the Visitor pattern. + * + * @param $nodeVisitor the visitor attempting to visit this node + */ + public function accept(NodeVisitor $nodeVisitor) + { + $nodeVisitor->visitElementNode($this); + } + + /** + * Gets the CodeDefinition that defines this element. + * + * @return this element's code definition + */ + public function getCodeDefinition() + { + return $this->codeDefinition; + } + + /** + * Sets the CodeDefinition that defines this element. + * + * @param codeDef the code definition that defines this element node + */ + public function setCodeDefinition(CodeDefinition $codeDef) + { + $this->codeDefinition = $codeDef; + $this->setTagName($codeDef->getTagName()); + } + + /** + * Returns the tag name of this element. + * + * @return the element's tag name + */ + public function getTagName() + { + return $this->tagName; + } + + /** + * Returns the attribute (used as the option in bbcode definitions) of this element. + * + * @return the attribute of this element + */ + public function getAttribute() + { + return $this->attribute; + } + + /** + * Returns all the children of this element. + * + * @return an array of this node's child nodes + */ + public function getChildren() + { + return $this->children; + } + + /** + * (non-PHPdoc) + * @see JBBCode.Node::getAsText() + * + * Returns the element as text (not including any bbcode markup) + * + * @return the plain text representation of this node + */ + public function getAsText() + { + if ($this->codeDefinition) { + return $this->codeDefinition->asText($this); + } else { + $s = ""; + foreach ($this->getChildren() as $child) + $s .= $child->getAsText(); + return $s; + } + } + + /** + * (non-PHPdoc) + * @see JBBCode.Node::getAsBBCode() + * + * Returns the element as bbcode (with all unclosed tags closed) + * + * @return the bbcode representation of this element + */ + public function getAsBBCode() + { + $str = "[".$this->tagName; + if (!empty($this->attribute)) { + + foreach($this->attribute as $key => $value){ + if($key == $this->tagName){ + $str .= "=".$value; + } + else{ + $str .= " ".$key."=" . $value; + } + } + } + $str .= "]"; + foreach ($this->getChildren() as $child) { + $str .= $child->getAsBBCode(); + } + $str .= "[/".$this->tagName."]"; + + return $str; + } + + /** + * (non-PHPdoc) + * @see JBBCode.Node::getAsHTML() + * + * Returns the element as html with all replacements made + * + * @return the html representation of this node + */ + public function getAsHTML() + { + if($this->codeDefinition) { + return $this->codeDefinition->asHtml($this); + } else { + return ""; + } + } + + /** + * Adds a child to this node's content. A child may be a TextNode, or + * another ElementNode... or anything else that may extend the + * abstract Node class. + * + * @param child the node to add as a child + */ + public function addChild(Node $child) + { + array_push($this->children, $child); + $child->setParent($this); + } + + /** + * Removes a child from this node's contnet. + * + * @param child the child node to remove + */ + public function removeChild(Node $child) + { + foreach ($this->children as $key => $value) { + if ($value == $child) + unset($this->children[$key]); + } + } + + /** + * Sets the tag name of this element node. + * + * @param tagName the element's new tag name + */ + public function setTagName($tagName) + { + $this->tagName = $tagName; + } + + /** + * Sets the attribute (option) of this element node. + * + * @param attribute the attribute of this element node + */ + public function setAttribute($attribute) + { + $this->attribute = $attribute; + } + + /** + * Traverses the parse tree upwards, going from parent to parent, until it finds a + * parent who has the given tag name. Returns the parent with the matching tag name + * if it exists, otherwise returns null. + * + * @param str the tag name to search for + * + * @return the closest parent with the given tag name + */ + public function closestParentOfType($str) + { + $str = strtolower($str); + $currentEl = $this; + + while (strtolower($currentEl->getTagName()) != $str && $currentEl->hasParent()) { + $currentEl = $currentEl->getParent(); + } + + if (strtolower($currentEl->getTagName()) != $str) { + return null; + } else { + return $currentEl; + } + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/InputValidator.php b/vendor/jbbcode/jbbcode/JBBCode/InputValidator.php new file mode 100644 index 000000000..677470964 --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/InputValidator.php @@ -0,0 +1,20 @@ +nodeid; + } + + /** + * Returns this node's immediate parent. + * + * @return the node's parent + */ + public function getParent() + { + return $this->parent; + } + + /** + * Determines if this node has a parent. + * + * @return true if this node has a parent, false otherwise + */ + public function hasParent() + { + return $this->parent != null; + } + + /** + * Returns true if this is a text node. Returns false otherwise. + * (Overridden by TextNode to return true) + * + * @return true if this node is a text node + */ + public function isTextNode() + { + return false; + } + + /** + * Accepts a NodeVisitor + * + * @param nodeVisitor the NodeVisitor traversing the graph + */ + abstract public function accept(NodeVisitor $nodeVisitor); + + /** + * Returns this node as text (without any bbcode markup) + * + * @return the plain text representation of this node + */ + abstract public function getAsText(); + + /** + * Returns this node as bbcode + * + * @return the bbcode representation of this node + */ + abstract public function getAsBBCode(); + + /** + * Returns this node as HTML + * + * @return the html representation of this node + */ + abstract public function getAsHTML(); + + /** + * Sets this node's parent to be the given node. + * + * @param parent the node to set as this node's parent + */ + public function setParent(Node $parent) + { + $this->parent = $parent; + } + + /** + * Sets this node's nodeid + * + * @param nodeid this node's node id + */ + public function setNodeId($nodeid) + { + $this->nodeid = $nodeid; + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/NodeVisitor.php b/vendor/jbbcode/jbbcode/JBBCode/NodeVisitor.php new file mode 100644 index 000000000..1dd228a97 --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/NodeVisitor.php @@ -0,0 +1,20 @@ +reset(); + $this->bbcodes = array(); + } + + /** + * Adds a simple (text-replacement only) bbcode definition + * + * @param string $tagName the tag name of the code (for example the b in [b]) + * @param string $replace the html to use, with {param} and optionally {option} for replacements + * @param boolean $useOption whether or not this bbcode uses the secondary {option} replacement + * @param boolean $parseContent whether or not to parse the content within these elements + * @param integer $nestLimit an optional limit of the number of elements of this kind that can be nested within + * each other before the parser stops parsing them. + * @param InputValidator $optionValidator the validator to run {option} through + * @param BodyValidator $bodyValidator the validator to run {param} through (only used if $parseContent == false) + * + * @return Parser + */ + public function addBBCode($tagName, $replace, $useOption = false, $parseContent = true, $nestLimit = -1, + InputValidator $optionValidator = null, InputValidator $bodyValidator = null) + { + $builder = new CodeDefinitionBuilder($tagName, $replace); + + $builder->setUseOption($useOption); + $builder->setParseContent($parseContent); + $builder->setNestLimit($nestLimit); + + if ($optionValidator) { + $builder->setOptionValidator($optionValidator); + } + + if ($bodyValidator) { + $builder->setBodyValidator($bodyValidator); + } + + $this->addCodeDefinition($builder->build()); + + return $this; + } + + /** + * Adds a complex bbcode definition. You may subclass the CodeDefinition class, instantiate a definition of your new + * class and add it to the parser through this method. + * + * @param CodeDefinition $definition the bbcode definition to add + * + * @return Parser + */ + public function addCodeDefinition(CodeDefinition $definition) + { + array_push($this->bbcodes, $definition); + + return $this; + } + + /** + * Adds a set of CodeDefinitions. + * + * @param CodeDefinitionSet $set the set of definitions to add + * + * @return Parser + */ + public function addCodeDefinitionSet(CodeDefinitionSet $set) { + foreach ($set->getCodeDefinitions() as $def) { + $this->addCodeDefinition($def); + } + + return $this; + } + + /** + * Returns the entire parse tree as text. Only {param} content is returned. BBCode markup will be ignored. + * + * @return string a text representation of the parse tree + */ + public function getAsText() + { + return $this->treeRoot->getAsText(); + } + + /** + * Returns the entire parse tree as bbcode. This will be identical to the inputted string, except unclosed tags + * will be closed. + * + * @return string a bbcode representation of the parse tree + */ + public function getAsBBCode() + { + return $this->treeRoot->getAsBBCode(); + } + + /** + * Returns the entire parse tree as HTML. All BBCode replacements will be made. This is generally the method + * you will want to use to retrieve the parsed bbcode. + * + * @return string a parsed html string + */ + public function getAsHTML() + { + return $this->treeRoot->getAsHTML(); + } + + /** + * Accepts the given NodeVisitor at the root. + * + * @param NodeVisitor a NodeVisitor + * + * @return Parser + */ + public function accept(NodeVisitor $nodeVisitor) + { + $this->treeRoot->accept($nodeVisitor); + + return $this; + } + /** + * Constructs the parse tree from a string of bbcode markup. + * + * @param string $str the bbcode markup to parse + * + * @return Parser + */ + public function parse($str) + { + /* Set the tree root back to a fresh DocumentElement. */ + $this->reset(); + + $parent = $this->treeRoot; + $tokenizer = new Tokenizer($str); + + while ($tokenizer->hasNext()) { + $parent = $this->parseStartState($parent, $tokenizer); + if ($parent->getCodeDefinition() && false === + $parent->getCodeDefinition()->parseContent()) { + /* We're inside an element that does not allow its contents to be parseable. */ + $this->parseAsTextUntilClose($parent, $tokenizer); + $parent = $parent->getParent(); + } + } + + /* We parsed ignoring nest limits. Do an O(n) traversal to remove any elements that + * are nested beyond their CodeDefinition's nest limit. */ + $this->removeOverNestedElements(); + + return $this; + } + + /** + * Removes any elements that are nested beyond their nest limit from the parse tree. This + * method is now deprecated. In a future release its access privileges will be made + * protected. + * + * @deprecated + */ + public function removeOverNestedElements() + { + $nestLimitVisitor = new \JBBCode\visitors\NestLimitVisitor(); + $this->accept($nestLimitVisitor); + } + + /** + * Removes the old parse tree if one exists. + */ + protected function reset() + { + // remove any old tree information + $this->treeRoot = new DocumentElement(); + /* The document element is created with nodeid 0. */ + $this->nextNodeid = 1; + } + + /** + * Determines whether a bbcode exists based on its tag name and whether or not it uses an option + * + * @param string $tagName the bbcode tag name to check + * @param boolean $usesOption whether or not the bbcode accepts an option + * + * @return bool true if the code exists, false otherwise + */ + public function codeExists($tagName, $usesOption = false) + { + foreach ($this->bbcodes as $code) { + if (strtolower($tagName) == $code->getTagName() && $usesOption == $code->usesOption()) { + return true; + } + } + + return false; + } + + /** + * Returns the CodeDefinition of a bbcode with the matching tag name and usesOption parameter + * + * @param string $tagName the tag name of the bbcode being searched for + * @param boolean $usesOption whether or not the bbcode accepts an option + * + * @return CodeDefinition if the bbcode exists, null otherwise + */ + public function getCode($tagName, $usesOption = false) + { + foreach ($this->bbcodes as $code) { + if (strtolower($tagName) == $code->getTagName() && $code->usesOption() == $usesOption) { + return $code; + } + } + + return null; + } + + /** + * Adds a set of default, standard bbcode definitions commonly used across the web. + * + * This method is now deprecated. Please use DefaultCodeDefinitionSet and + * addCodeDefinitionSet() instead. + * + * @deprecated + */ + public function loadDefaultCodes() + { + $defaultSet = new DefaultCodeDefinitionSet(); + $this->addCodeDefinitionSet($defaultSet); + } + + /** + * Creates a new text node with the given parent and text string. + * + * @param $parent the parent of the text node + * @param $string the text of the text node + * + * @return TextNode the newly created TextNode + */ + protected function createTextNode(ElementNode $parent, $string) + { + if (count($parent->getChildren())) { + $children = $parent->getChildren(); + $lastElement = end($children); + reset($children); + + if ($lastElement->isTextNode()) { + $lastElement->setValue($lastElement->getValue() . $string); + return $lastElement; + } + } + + $textNode = new TextNode($string); + $textNode->setNodeId(++$this->nextNodeid); + $parent->addChild($textNode); + return $textNode; + } + + /** + * jBBCode parsing logic is loosely modelled after a FSM. While not every function maps + * to a unique DFSM state, each function handles the logic of one or more FSM states. + * This function handles the beginning parse state when we're not currently in a tag + * name. + * + * @param ElementNode $parent the current parent node we're under + * @param Tokenizer $tokenizer the tokenizer we're using + * + * @return ElementNode the new parent we should use for the next iteration. + */ + protected function parseStartState(ElementNode $parent, Tokenizer $tokenizer) + { + $next = $tokenizer->next(); + + if ('[' == $next) { + return $this->parseTagOpen($parent, $tokenizer); + } + else { + $this->createTextNode($parent, $next); + /* Drop back into the main parse loop which will call this + * same method again. */ + return $parent; + } + } + + /** + * This function handles parsing the beginnings of an open tag. When we see a [ + * at an appropriate time, this function is entered. + * + * @param ElementNode $parent the current parent node + * @param Tokenizer $tokenizer the tokenizer we're using + * + * @return ElementNode the new parent node + */ + protected function parseTagOpen(ElementNode $parent, Tokenizer $tokenizer) + { + + if (!$tokenizer->hasNext()) { + /* The [ that sent us to this state was just a trailing [, not the + * opening for a new tag. Treat it as such. */ + $this->createTextNode($parent, '['); + return $parent; + } + + $next = $tokenizer->next(); + + /* This while loop could be replaced by a recursive call to this same method, + * which would likely be a lot clearer but I decided to use a while loop to + * prevent stack overflow with a string like [[[[[[[[[...[[[. + */ + while ('[' == $next) { + /* The previous [ was just a random bracket that should be treated as text. + * Continue until we get a non open bracket. */ + $this->createTextNode($parent, '['); + if (!$tokenizer->hasNext()) { + $this->createTextNode($parent, '['); + return $parent; + } + $next = $tokenizer->next(); + } + + if (!$tokenizer->hasNext()) { + $this->createTextNode($parent, '['.$next); + return $parent; + } + + $after_next = $tokenizer->next(); + $tokenizer->stepBack(); + + if ($after_next != ']') + { + $this->createTextNode($parent, '['.$next); + return $parent; + } + + /* At this point $next is either ']' or plain text. */ + if (']' == $next) { + $this->createTextNode($parent, '['); + $this->createTextNode($parent, ']'); + return $parent; + } else { + /* $next is plain text... likely a tag name. */ + return $this->parseTag($parent, $tokenizer, $next); + } + } + + protected function parseOptions($tagContent) + { + $buffer = ""; + $tagName = ""; + $state = static::OPTION_STATE_TAGNAME; + $keys = array(); + $values = array(); + $options = array(); + + $len = strlen($tagContent); + $done = false; + $idx = 0; + + try{ + while(!$done){ + $char = $idx < $len ? $tagContent[$idx]:null; + switch($state){ + case static::OPTION_STATE_TAGNAME: + switch($char){ + case '=': + $state = static::OPTION_STATE_VALUE; + $tagName = $buffer; + $keys[] = $tagName; + $buffer = ""; + break; + case ' ': + $state = static::OPTION_STATE_DEFAULT; + $tagName = $buffer; + $buffer = ''; + $keys[] = $tagName; + break; + + case null: + $tagName = $buffer; + $buffer = ''; + $keys[] = $tagName; + break; + default: + $buffer .= $char; + } + break; + + case static::OPTION_STATE_DEFAULT: + switch($char){ + case ' ': + // do nothing + default: + $state = static::OPTION_STATE_KEY; + $buffer .= $char; + } + break; + + case static::OPTION_STATE_VALUE: + switch($char){ + case '"': + $state = static::OPTION_STATE_QUOTED_VALUE; + break; + case null: // intentional fall-through + case ' ': // key=value delimits to next key + $values[] = $buffer; + $buffer = ""; + $state = static::OPTION_STATE_KEY; + break; + case ":": + if($buffer=="javascript"){ + $state = static::OPTION_STATE_JAVASCRIPT; + } + $buffer .= $char; + break; + default: + $buffer .= $char; + + } + break; + + case static::OPTION_STATE_JAVASCRIPT: + switch($char){ + case ";": + $buffer .= $char; + $values[] = $buffer; + $buffer = ""; + $state = static::OPTION_STATE_KEY; + + break; + default: + $buffer .= $char; + } + break; + + case static::OPTION_STATE_KEY: + switch($char){ + case '=': + $state = static::OPTION_STATE_VALUE; + $keys[] = $buffer; + $buffer = ''; + break; + case ' ': // ignore key=value + break; + default: + $buffer .= $char; + break; + } + break; + + case static::OPTION_STATE_QUOTED_VALUE: + switch($char){ + case null: + case '"': + $state = static::OPTION_STATE_KEY; + $values[] = $buffer; + $buffer = ''; + + // peek ahead. If the next character is not a space or a closing brace, we have a bad tag and need to abort + if(isset($tagContent[$idx+1]) && $tagContent[$idx+1]!=" " && $tagContent[$idx+1]!="]" ){ + throw new ParserException("Badly formed attribute: $tagContent"); + } + break; + default: + $buffer .= $char; + break; + } + break; + default: + if(!empty($char)){ + $state = static::OPTION_STATE_KEY; + } + + } + if($idx >= $len){ + $done = true; + } + $idx++; + } + + if(count($keys) && count($values)){ + if(count($keys)==(count($values)+1)){ + array_unshift($values, ""); + } + + $options = array_combine($keys, $values); + } + } + catch(ParserException $e){ + // if we're in this state, then something evidently went wrong. We'll consider everything that came after the tagname to be the attribute for that keyname + $options[$tagName]= substr($tagContent, strpos($tagContent, "=")+1); + } + return array($tagName, $options); + } + + /** + * This is the next step in parsing a tag. It's possible for it to still be invalid at this + * point but many of the basic invalid tag name conditions have already been handled. + * + * @param ElementNode $parent the current parent element + * @param Tokenizer $tokenizer the tokenizer we're using + * @param string $tagContent the text between the [ and the ], assuming there is actually a ] + * + * @return ElementNode the new parent element + */ + protected function parseTag(ElementNode $parent, Tokenizer $tokenizer, $tagContent) + { + + $next; + if (!$tokenizer->hasNext() || ($next = $tokenizer->next()) != ']') { + /* This is a malformed tag. Both the previous [ and the tagContent + * is really just plain text. */ + $this->createTextNode($parent, '['); + $this->createTextNode($parent, $tagContent); + return $parent; + } + + /* This is a well-formed tag consisting of [something] or [/something], but + * we still need to ensure that 'something' is a valid tag name. Additionally, + * if it's a closing tag, we need to ensure that there was a previous matching + * opening tag. + */ + /* There could be attributes. */ + list($tmpTagName, $options) = $this->parseOptions($tagContent); + + // $tagPieces = explode('=', $tagContent); + // $tmpTagName = $tagPieces[0]; + + $actualTagName; + if ('' != $tmpTagName && '/' == $tmpTagName[0]) { + /* This is a closing tag name. */ + $actualTagName = substr($tmpTagName, 1); + } else { + $actualTagName = $tmpTagName; + } + + if ('' != $tmpTagName && '/' == $tmpTagName[0]) { + /* This is attempting to close an open tag. We must verify that there exists an + * open tag of the same type and that there is no option (options on closing + * tags don't make any sense). */ + $elToClose = $parent->closestParentOfType($actualTagName); + if (null == $elToClose || count($options) > 1) { + /* Closing an unopened tag or has an option. Treat everything as plain text. */ + $this->createTextNode($parent, '['); + $this->createTextNode($parent, $tagContent); + $this->createTextNode($parent, ']'); + return $parent; + } else { + /* We're closing $elToClose. In order to do that, we just need to return + * $elToClose's parent, since that will change our effective parent to be + * elToClose's parent. */ + return $elToClose->getParent(); + } + } + + /* Verify that this is a known bbcode tag name. */ + if ('' == $actualTagName || !$this->codeExists($actualTagName, !empty($options))) { + /* This is an invalid tag name! Treat everything we've seen as plain text. */ + $this->createTextNode($parent, '['); + $this->createTextNode($parent, $tagContent); + $this->createTextNode($parent, ']'); + return $parent; + } + + /* If we're here, this is a valid opening tag. Let's make a new node for it. */ + $el = new ElementNode(); + $el->setNodeId(++$this->nextNodeid); + $code = $this->getCode($actualTagName, !empty($options)); + $el->setCodeDefinition($code); + if (!empty($options)) { + /* We have an attribute we should save. */ + $el->setAttribute($options); + } + $parent->addChild($el); + return $el; + } + + /** + * Handles parsing elements whose CodeDefinitions disable parsing of element + * contents. This function uses a rolling window of 3 tokens until it finds the + * appropriate closing tag or reaches the end of the token stream. + * + * @param ElementNode $parent the current parent element + * @param Tokenizer $tokenizer the tokenizer we're using + * + * @return ElementNode the new parent element + */ + protected function parseAsTextUntilClose(ElementNode $parent, Tokenizer $tokenizer) + { + /* $parent's code definition doesn't allow its contents to be parsed. Here we use + * a sliding window of three tokens until we find [ /tagname ], signifying the + * end of the parent. */ + if (!$tokenizer->hasNext()) { + return $parent; + } + $prevPrev = $tokenizer->next(); + if (!$tokenizer->hasNext()) { + $this->createTextNode($parent, $prevPrev); + return $parent; + } + $prev = $tokenizer->next(); + if (!$tokenizer->hasNext()) { + $this->createTextNode($parent, $prevPrev); + $this->createTextNode($parent, $prev); + return $parent; + } + $curr = $tokenizer->next(); + while ('[' != $prevPrev || '/'.$parent->getTagName() != strtolower($prev) || + ']' != $curr) { + $this->createTextNode($parent, $prevPrev); + $prevPrev = $prev; + $prev = $curr; + if (!$tokenizer->hasNext()) { + $this->createTextNode($parent, $prevPrev); + $this->createTextNode($parent, $prev); + return $parent; + } + $curr = $tokenizer->next(); + } + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/ParserException.php b/vendor/jbbcode/jbbcode/JBBCode/ParserException.php new file mode 100644 index 000000000..89501cfcb --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/ParserException.php @@ -0,0 +1,7 @@ +value = $val; + } + + public function accept(NodeVisitor $visitor) + { + $visitor->visitTextNode($this); + } + + /** + * (non-PHPdoc) + * @see JBBCode.Node::isTextNode() + * + * returns true + */ + public function isTextNode() + { + return true; + } + + /** + * Returns the text string value of this text node. + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * (non-PHPdoc) + * @see JBBCode.Node::getAsText() + * + * Returns the text representation of this node. + * + * @return this node represented as text + */ + public function getAsText() + { + return $this->getValue(); + } + + /** + * (non-PHPdoc) + * @see JBBCode.Node::getAsBBCode() + * + * Returns the bbcode representation of this node. (Just its value) + * + * @return this node represented as bbcode + */ + public function getAsBBCode() + { + return $this->getValue(); + } + + /** + * (non-PHPdoc) + * @see JBBCode.Node::getAsHTML() + * + * Returns the html representation of this node. (Just its value) + * + * @return this node represented as HTML + */ + public function getAsHTML() + { + return $this->getValue(); + } + + /** + * Edits the text value contained within this text node. + * + * @param newValue the new text value of the text node + */ + public function setValue($newValue) + { + $this->value = $newValue; + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/Tokenizer.php b/vendor/jbbcode/jbbcode/JBBCode/Tokenizer.php new file mode 100644 index 000000000..6d47c44bc --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/Tokenizer.php @@ -0,0 +1,105 @@ +tokens, substr($str, $strStart, $index - $strStart)); + $strStart = $index; + } + + /* Add the [ or ] to the tokens array. */ + array_push($this->tokens, $str[$index]); + $strStart = $index+1; + } + } + + if ($strStart < strlen($str)) { + /* There are still characters in the buffer. Add them to the tokens. */ + array_push($this->tokens, substr($str, $strStart, strlen($str) - $strStart)); + } + } + + /** + * Returns true if there is another token in the token stream. + */ + public function hasNext() + { + return count($this->tokens) > 1 + $this->i; + } + + /** + * Advances the token stream to the next token and returns the new token. + */ + public function next() + { + if (!$this->hasNext()) { + return null; + } else { + return $this->tokens[++$this->i]; + } + } + + /** + * Retrieves the current token. + */ + public function current() + { + if ($this->i < 0) { + return null; + } else { + return $this->tokens[$this->i]; + } + } + + /** + * Moves the token stream back a token. + */ + public function stepBack() + { + if ($this->i > -1) { + $this->i--; + } + } + + /** + * Restarts the tokenizer, returning to the beginning of the token stream. + */ + public function restart() + { + $this->i = -1; + } + + /** + * toString method that returns the entire string from the current index on. + */ + public function toString() + { + return implode('', array_slice($this->tokens, $this->i + 1)); + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/examples/1-GettingStarted.php b/vendor/jbbcode/jbbcode/JBBCode/examples/1-GettingStarted.php new file mode 100644 index 000000000..dd0c17440 --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/examples/1-GettingStarted.php @@ -0,0 +1,12 @@ +addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + +$text = "The default codes include: [b]bold[/b], [i]italics[/i], [u]underlining[/u], "; +$text .= "[url=http://jbbcode.com]links[/url], [color=red]color![/color] and more."; + +$parser->parse($text); + +print $parser->getAsHtml(); diff --git a/vendor/jbbcode/jbbcode/JBBCode/examples/2-ClosingUnclosedTags.php b/vendor/jbbcode/jbbcode/JBBCode/examples/2-ClosingUnclosedTags.php new file mode 100644 index 000000000..35ee7fdcd --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/examples/2-ClosingUnclosedTags.php @@ -0,0 +1,10 @@ +addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + +$text = "The bbcode in here [b]is never closed!"; +$parser->parse($text); + +print $parser->getAsBBCode(); diff --git a/vendor/jbbcode/jbbcode/JBBCode/examples/3-MarkuplessText.php b/vendor/jbbcode/jbbcode/JBBCode/examples/3-MarkuplessText.php new file mode 100644 index 000000000..47f20a3cd --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/examples/3-MarkuplessText.php @@ -0,0 +1,11 @@ +addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + +$text = "[b][u]There is [i]a lot[/i] of [url=http://en.wikipedia.org/wiki/Markup_language]markup[/url] in this"; +$text .= "[color=#333333]text[/color]![/u][/b]"; +$parser->parse($text); + +print $parser->getAsText(); diff --git a/vendor/jbbcode/jbbcode/JBBCode/examples/4-CreatingNewCodes.php b/vendor/jbbcode/jbbcode/JBBCode/examples/4-CreatingNewCodes.php new file mode 100644 index 000000000..e8335b017 --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/examples/4-CreatingNewCodes.php @@ -0,0 +1,7 @@ +addBBCode("quote", '
{param}
'); +$parser->addBBCode("code", '
{param}
', false, false, 1); diff --git a/vendor/jbbcode/jbbcode/JBBCode/examples/SmileyVisitorTest.php b/vendor/jbbcode/jbbcode/JBBCode/examples/SmileyVisitorTest.php new file mode 100644 index 000000000..cfea90fa2 --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/examples/SmileyVisitorTest.php @@ -0,0 +1,22 @@ +addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + +if (count($argv) < 2) { + die("Usage: " . $argv[0] . " \"bbcode string\"\n"); +} + +$inputText = $argv[1]; + +$parser->parse($inputText); + +$smileyVisitor = new \JBBCode\visitors\SmileyVisitor(); +$parser->accept($smileyVisitor); + +echo $parser->getAsHTML() . "\n"; diff --git a/vendor/jbbcode/jbbcode/JBBCode/examples/TagCountingVisitorTest.php b/vendor/jbbcode/jbbcode/JBBCode/examples/TagCountingVisitorTest.php new file mode 100644 index 000000000..8ce5d9923 --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/examples/TagCountingVisitorTest.php @@ -0,0 +1,23 @@ +addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + +if (count($argv) < 3) { + die("Usage: " . $argv[0] . " \"bbcode string\" \n"); +} + +$inputText = $argv[1]; +$tagName = $argv[2]; + +$parser->parse($inputText); + +$tagCountingVisitor = new \JBBCode\visitors\TagCountingVisitor(); +$parser->accept($tagCountingVisitor); + +echo $tagCountingVisitor->getFrequency($tagName) . "\n"; diff --git a/vendor/jbbcode/jbbcode/JBBCode/tests/BBCodeToBBCodeTest.php b/vendor/jbbcode/jbbcode/JBBCode/tests/BBCodeToBBCodeTest.php new file mode 100644 index 000000000..c832fcc7a --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/tests/BBCodeToBBCodeTest.php @@ -0,0 +1,85 @@ +addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + $parser->parse($bbcode); + return $parser->getAsBBCode(); + } + + /** + * Asserts that the given bbcode matches the given text when + * the bbcode is run through defaultBBCodeParse + */ + private function assertBBCodeOutput($bbcode, $text) + { + $this->assertEquals($this->defaultBBCodeParse($bbcode), $text); + } + + public function testEmptyString() + { + $this->assertBBCodeOutput('', ''); + } + + public function testOneTag() + { + $this->assertBBCodeOutput('[b]this is bold[/b]', '[b]this is bold[/b]'); + } + + public function testOneTagWithSurroundingText() + { + $this->assertBBCodeOutput('buffer text [b]this is bold[/b] buffer text', + 'buffer text [b]this is bold[/b] buffer text'); + } + + public function testMultipleTags() + { + $bbcode = 'this is some text with [b]bold tags[/b] and [i]italics[/i] and ' . + 'things like [u]that[/u].'; + $bbcodeOutput = 'this is some text with [b]bold tags[/b] and [i]italics[/i] and ' . + 'things like [u]that[/u].'; + $this->assertBBCodeOutput($bbcode, $bbcodeOutput); + } + + public function testCodeOptions() + { + $code = 'This contains a [url=http://jbbcode.com]url[/url] which uses an option.'; + $codeOutput = 'This contains a [url=http://jbbcode.com]url[/url] which uses an option.'; + $this->assertBBCodeOutput($code, $codeOutput); + } + + /** + * @depends testCodeOptions + */ + public function testOmittedOption() + { + $code = 'This doesn\'t use the url option [url]http://jbbcode.com[/url].'; + $codeOutput = 'This doesn\'t use the url option [url]http://jbbcode.com[/url].'; + $this->assertBBCodeOutput($code, $codeOutput); + } + + public function testUnclosedTags() + { + $code = '[b]bold'; + $codeOutput = '[b]bold[/b]'; + $this->assertBBCodeOutput($code, $codeOutput); + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/tests/BBCodeToTextTest.php b/vendor/jbbcode/jbbcode/JBBCode/tests/BBCodeToTextTest.php new file mode 100644 index 000000000..193fc7cbe --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/tests/BBCodeToTextTest.php @@ -0,0 +1,78 @@ +addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + $parser->parse($bbcode); + return $parser->getAsText(); + } + + /** + * Asserts that the given bbcode matches the given text when + * the bbcode is run through defaultTextParse + */ + private function assertTextOutput($bbcode, $text) + { + $this->assertEquals($text, $this->defaultTextParse($bbcode)); + } + + public function testEmptyString() + { + $this->assertTextOutput('', ''); + } + + public function testOneTag() + { + $this->assertTextOutput('[b]this is bold[/b]', 'this is bold'); + } + + public function testOneTagWithSurroundingText() + { + $this->assertTextOutput('buffer text [b]this is bold[/b] buffer text', + 'buffer text this is bold buffer text'); + } + + public function testMultipleTags() + { + $bbcode = 'this is some text with [b]bold tags[/b] and [i]italics[/i] and ' . + 'things like [u]that[/u].'; + $text = 'this is some text with bold tags and italics and things like that.'; + $this->assertTextOutput($bbcode, $text); + } + + public function testCodeOptions() + { + $code = 'This contains a [url=http://jbbcode.com]url[/url] which uses an option.'; + $text = 'This contains a url which uses an option.'; + $this->assertTextOutput($code, $text); + } + + /** + * @depends testCodeOptions + */ + public function testOmittedOption() + { + $code = 'This doesn\'t use the url option [url]http://jbbcode.com[/url].'; + $text = 'This doesn\'t use the url option http://jbbcode.com.'; + $this->assertTextOutput($code, $text); + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/tests/DefaultCodesTest.php b/vendor/jbbcode/jbbcode/JBBCode/tests/DefaultCodesTest.php new file mode 100644 index 000000000..e93399282 --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/tests/DefaultCodesTest.php @@ -0,0 +1,54 @@ +addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + $parser->parse($bbcode); + $this->assertEquals($html, $parser->getAsHtml()); + } + + /** + * Tests the [b] bbcode. + */ + public function testBold() + { + $this->assertProduces('[b]this should be bold[/b]', 'this should be bold'); + } + + /** + * Tests the [color] bbcode. + */ + public function testColor() + { + $this->assertProduces('[color=red]red[/color]', 'red'); + } + + /** + * Tests the example from the documentation. + */ + public function testExample() + { + $text = "The default codes include: [b]bold[/b], [i]italics[/i], [u]underlining[/u], "; + $text .= "[url=http://jbbcode.com]links[/url], [color=red]color![/color] and more."; + $html = 'The default codes include: bold, italics, underlining, '; + $html .= 'links, color! and more.'; + $this->assertProduces($text, $html); + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/tests/HTMLSafeTest.php b/vendor/jbbcode/jbbcode/JBBCode/tests/HTMLSafeTest.php new file mode 100644 index 000000000..bd9391b6e --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/tests/HTMLSafeTest.php @@ -0,0 +1,77 @@ +addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + $parser->parse($bbcode); + + $htmlsafer = new JBBCode\visitors\HTMLSafeVisitor(); + $parser->accept($htmlsafer); + + $this->assertEquals($html, $parser->getAsHtml()); + } + + /** + * Tests escaping quotes and ampersands in simple text + */ + public function testQuoteAndAmp() + { + $this->assertProduces('te"xt te&xt', 'te"xt te&xt'); + } + + /** + * Tests escaping quotes and ampersands inside a BBCode tag + */ + public function testQuoteAndAmpInTag() + { + $this->assertProduces('[b]te"xt te&xt[/b]', 'te"xt te&xt'); + } + + /** + * Tests escaping HTML tags + */ + public function testHtmlTag() + { + $this->assertProduces('not bold', '<b>not bold</b>'); + $this->assertProduces('[b]bold[/b]
', '<b>bold</b> <hr>'); + } + + /** + * Tests escaping ampersands in URL using [url]...[/url] + */ + public function testUrlParam() + { + $this->assertProduces('text [url]http://example.com/?a=b&c=d[/url] more text', 'text http://example.com/?a=b&c=d more text'); + } + + /** + * Tests escaping ampersands in URL using [url=...] tag + */ + public function testUrlOption() + { + $this->assertProduces('text [url=http://example.com/?a=b&c=d]this is a "link"[/url]', 'text this is a "link"'); + } + + /** + * Tests escaping ampersands in URL using [url=...] tag when URL is in quotes + */ + public function testUrlOptionQuotes() + { + $this->assertProduces('text [url="http://example.com/?a=b&c=d"]this is a "link"[/url]', 'text this is a "link"'); + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/tests/NestLimitTest.php b/vendor/jbbcode/jbbcode/JBBCode/tests/NestLimitTest.php new file mode 100644 index 000000000..d826fefde --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/tests/NestLimitTest.php @@ -0,0 +1,46 @@ +addBBCode('b', '{param}', false, true, -1); + $parser->parse('[b][b][b][b][b][b][b][b]bold text[/b][/b][/b][/b][/b][/b][/b][/b]'); + $this->assertEquals('' . + 'bold text' . + '', + $parser->getAsHtml()); + } + + /** + * Test over nesting. + */ + public function testOverNesting() + { + $parser = new JBBCode\Parser(); + $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + $parser->addBBCode('quote', '
{param}
', false, true, 2); + $bbcode = '[quote][quote][quote]wut[/quote] huh?[/quote] i don\'t know[/quote]'; + $parser->parse($bbcode); + $expectedBbcode = '[quote][quote] huh?[/quote] i don\'t know[/quote]'; + $expectedHtml = '
huh?
i don\'t know
'; + $this->assertEquals($expectedBbcode, $parser->getAsBBCode()); + $this->assertEquals($expectedHtml, $parser->getAsHtml()); + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/tests/ParseContentTest.php b/vendor/jbbcode/jbbcode/JBBCode/tests/ParseContentTest.php new file mode 100644 index 000000000..1ea2c78a5 --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/tests/ParseContentTest.php @@ -0,0 +1,97 @@ +addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + $parser->addBBCode('verbatim', '{param}', false, false); + + $parser->parse('[verbatim]plain text[/verbatim]'); + $this->assertEquals('plain text', $parser->getAsHtml()); + + $parser->parse('[verbatim][b]bold[/b][/verbatim]'); + $this->assertEquals('[b]bold[/b]', $parser->getAsHtml()); + + } + + public function testNoParsingWithBufferText() + { + + $parser = new JBBCode\Parser(); + $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + $parser->addBBCode('verbatim', '{param}', false, false); + + $parser->parse('buffer text[verbatim]buffer text[b]bold[/b]buffer text[/verbatim]buffer text'); + $this->assertEquals('buffer textbuffer text[b]bold[/b]buffer textbuffer text', $parser->getAsHtml()); + } + + /** + * Tests that when a tag is not closed within an unparseable tag, + * the BBCode output does not automatically close that tag (because + * the contents were not parsed). + */ + public function testUnclosedTag() + { + + $parser = new JBBCode\Parser(); + $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + $parser->addBBCode('verbatim', '{param}', false, false); + + $parser->parse('[verbatim]i wonder [b]what will happen[/verbatim]'); + $this->assertEquals('i wonder [b]what will happen', $parser->getAsHtml()); + $this->assertEquals('[verbatim]i wonder [b]what will happen[/verbatim]', $parser->getAsBBCode()); + } + + /** + * Tests that an unclosed tag with parseContent = false ends cleanly. + */ + public function testUnclosedVerbatimTag() + { + $parser = new JBBCode\Parser(); + $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + $parser->addBBCode('verbatim', '{param}', false, false); + + $parser->parse('[verbatim]yo this [b]text should not be bold[/b]'); + $this->assertEquals('yo this [b]text should not be bold[/b]', $parser->getAsHtml()); + } + + /** + * Tests a malformed closing tag for a verbatim block. + */ + public function testMalformedVerbatimClosingTag() + { + $parser = new JBBCode\Parser(); + $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + $parser->addBBCode('verbatim', '{param}', false, false); + $parser->parse('[verbatim]yo this [b]text should not be bold[/b][/verbatim'); + $this->assertEquals('yo this [b]text should not be bold[/b][/verbatim', $parser->getAsHtml()); + } + + /** + * Tests an immediate end after a verbatim. + */ + public function testVerbatimThenEof() + { + $parser = new JBBCode\Parser(); + $parser->addBBCode('verbatim', '{param}', false, false); + $parser->parse('[verbatim]'); + $this->assertEquals('', $parser->getAsHtml()); + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/tests/ParsingEdgeCaseTest.php b/vendor/jbbcode/jbbcode/JBBCode/tests/ParsingEdgeCaseTest.php new file mode 100644 index 000000000..a08f713ce --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/tests/ParsingEdgeCaseTest.php @@ -0,0 +1,130 @@ +addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + $parser->parse($bbcode); + return $parser->getAsHtml(); + } + + /** + * Asserts that the given bbcode matches the given html when + * the bbcode is run through defaultParse. + */ + private function assertProduces($bbcode, $html) + { + $this->assertEquals($html, $this->defaultParse($bbcode)); + } + + /** + * Tests attempting to use a code that doesn't exist. + */ + public function testNonexistentCodeMalformed() + { + $this->assertProduces('[wat]', '[wat]'); + } + + /** + * Tests attempting to use a code that doesn't exist, but this + * time in a well-formed fashion. + * + * @depends testNonexistentCodeMalformed + */ + public function testNonexistentCodeWellformed() + { + $this->assertProduces('[wat]something[/wat]', '[wat]something[/wat]'); + } + + /** + * Tests a whole bunch of meaningless left brackets. + */ + public function testAllLeftBrackets() + { + $this->assertProduces('[[[[[[[[', '[[[[[[[['); + } + + /** + * Tests a whole bunch of meaningless right brackets. + */ + public function testAllRightBrackets() + { + $this->assertProduces(']]]]]', ']]]]]'); + } + + /** + * Intermixes well-formed, meaningful tags with meaningless brackets. + */ + public function testRandomBracketsInWellformedCode() + { + $this->assertProduces('[b][[][[i]heh[/i][/b]', + '[[][heh'); + } + + /** + * Tests an unclosed tag within a closed tag. + */ + public function testUnclosedWithinClosed() + { + $this->assertProduces('[url=http://jbbcode.com][b]oh yeah[/url]', + 'oh yeah'); + } + + /** + * Tests half completed opening tag. + */ + public function testHalfOpenTag() + { + $this->assertProduces('[b', '[b'); + $this->assertProduces('wut [url=http://jbbcode.com', + 'wut [url=http://jbbcode.com'); + } + + /** + * Tests half completed closing tag. + */ + public function testHalfClosingTag() + { + $this->assertProduces('[b]this should be bold[/b', + 'this should be bold[/b'); + } + + /** + * Tests lots of left brackets before the actual tag. For example: + * [[[[[[[[b]bold![/b] + */ + public function testLeftBracketsThenTag() + { + $this->assertProduces('[[[[[b]bold![/b]', + '[[[[bold!'); + } + + /** + * Tests a whitespace after left bracket. + */ + public function testWhitespaceAfterLeftBracketWhithoutTag() + { + $this->assertProduces('[ ABC ] ', + '[ ABC ] '); + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/tests/SimpleEvaluationTest.php b/vendor/jbbcode/jbbcode/JBBCode/tests/SimpleEvaluationTest.php new file mode 100644 index 000000000..65fb236c5 --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/tests/SimpleEvaluationTest.php @@ -0,0 +1,131 @@ +addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + $parser->parse($bbcode); + return $parser->getAsHtml(); + } + + /** + * Asserts that the given bbcode matches the given html when + * the bbcode is run through defaultParse. + */ + private function assertProduces($bbcode, $html) + { + $this->assertEquals($html, $this->defaultParse($bbcode)); + } + + + public function testEmptyString() + { + $this->assertProduces('', ''); + } + + public function testOneTag() + { + $this->assertProduces('[b]this is bold[/b]', 'this is bold'); + } + + public function testOneTagWithSurroundingText() + { + $this->assertProduces('buffer text [b]this is bold[/b] buffer text', + 'buffer text this is bold buffer text'); + } + + public function testMultipleTags() + { + $bbcode = <<bold tags and italics and +things like that. +EOD; + $this->assertProduces($bbcode, $html); + } + + public function testCodeOptions() + { + $code = 'This contains a [url=http://jbbcode.com/?b=2]url[/url] which uses an option.'; + $html = 'This contains a url which uses an option.'; + $this->assertProduces($code, $html); + } + + public function testAttributes() + { + $parser = new JBBCode\Parser(); + $builder = new JBBCode\CodeDefinitionBuilder('img', '{alt}'); + $parser->addCodeDefinition($builder->setUseOption(true)->setParseContent(false)->build()); + + $expected = 'Multiple alt text options.'; + + $code = 'Multiple [img height="50" alt="alt text"]http://jbbcode.com/img.png[/img] options.'; + $parser->parse($code); + $result = $parser->getAsHTML(); + $this->assertEquals($expected, $result); + + $code = 'Multiple [img height=50 alt="alt text"]http://jbbcode.com/img.png[/img] options.'; + $parser->parse($code); + $result = $parser->getAsHTML(); + $this->assertEquals($expected, $result); + } + + /** + * @depends testCodeOptions + */ + public function testOmittedOption() + { + $code = 'This doesn\'t use the url option [url]http://jbbcode.com[/url].'; + $html = 'This doesn\'t use the url option http://jbbcode.com.'; + $this->assertProduces($code, $html); + } + + public function testUnclosedTag() + { + $code = 'hello [b]world'; + $html = 'hello world'; + $this->assertProduces($code, $html); + } + + public function testNestingTags() + { + $code = '[url=http://jbbcode.com][b]hello [u]world[/u][/b][/url]'; + $html = 'hello world'; + $this->assertProduces($code, $html); + } + + public function testBracketInTag() + { + $this->assertProduces('[b]:-[[/b]', ':-['); + } + + public function testBracketWithSpaceInTag() + { + $this->assertProduces('[b]:-[ [/b]', ':-[ '); + } + + public function testBracketWithTextInTag() + { + $this->assertProduces('[b]:-[ foobar[/b]', ':-[ foobar'); + } + + public function testMultibleBracketsWithTextInTag() + { + $this->assertProduces('[b]:-[ [fo[o[bar[/b]', ':-[ [fo[o[bar'); + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/tests/TokenizerTest.php b/vendor/jbbcode/jbbcode/JBBCode/tests/TokenizerTest.php new file mode 100644 index 000000000..a5431d33c --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/tests/TokenizerTest.php @@ -0,0 +1,74 @@ +assertFalse($tokenizer->hasNext()); + } + + public function testPlainTextOnly() + { + $tokenizer = new JBBCode\Tokenizer('this is some plain text.'); + $this->assertEquals('this is some plain text.', $tokenizer->next()); + $this->assertEquals('this is some plain text.', $tokenizer->current()); + $this->assertFalse($tokenizer->hasNext()); + } + + public function testStartingBracket() + { + $tokenizer = new JBBCode\Tokenizer('[this has a starting bracket.'); + $this->assertEquals('[', $tokenizer->next()); + $this->assertEquals('[', $tokenizer->current()); + $this->assertEquals('this has a starting bracket.', $tokenizer->next()); + $this->assertEquals('this has a starting bracket.', $tokenizer->current()); + $this->assertFalse($tokenizer->hasNext()); + $this->assertEquals(null, $tokenizer->next()); + } + + public function testOneTag() + { + $tokenizer = new JBBCode\Tokenizer('[b]'); + $this->assertEquals('[', $tokenizer->next()); + $this->assertEquals('b', $tokenizer->next()); + $this->assertEquals(']', $tokenizer->next()); + $this->assertFalse($tokenizer->hasNext()); + } + + public function testMatchingTags() + { + $tokenizer = new JBBCode\Tokenizer('[url]http://jbbcode.com[/url]'); + $this->assertEquals('[', $tokenizer->next()); + $this->assertEquals('url', $tokenizer->next()); + $this->assertEquals(']', $tokenizer->next()); + $this->assertEquals('http://jbbcode.com', $tokenizer->next()); + $this->assertEquals('[', $tokenizer->next()); + $this->assertEquals('/url', $tokenizer->next()); + $this->assertEquals(']', $tokenizer->next()); + $this->assertFalse($tokenizer->hasNext()); + } + + public function testLotsOfBrackets() + { + $tokenizer = new JBBCode\Tokenizer('[[][]]['); + $this->assertEquals('[', $tokenizer->next()); + $this->assertEquals('[', $tokenizer->next()); + $this->assertEquals(']', $tokenizer->next()); + $this->assertEquals('[', $tokenizer->next()); + $this->assertEquals(']', $tokenizer->next()); + $this->assertEquals(']', $tokenizer->next()); + $this->assertEquals('[', $tokenizer->next()); + $this->assertFalse($tokenizer->hasNext()); + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/tests/ValidatorTest.php b/vendor/jbbcode/jbbcode/JBBCode/tests/ValidatorTest.php new file mode 100644 index 000000000..e7dacb07b --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/tests/ValidatorTest.php @@ -0,0 +1,151 @@ +assertFalse($urlValidator->validate('#yolo#swag')); + $this->assertFalse($urlValidator->validate('giehtiehwtaw352353%3')); + } + + /** + * Tests a valid url directly on the UrlValidator. + */ + public function testValidUrl() + { + $urlValidator = new \JBBCode\validators\UrlValidator(); + $this->assertTrue($urlValidator->validate('http://google.com')); + $this->assertTrue($urlValidator->validate('http://jbbcode.com/docs')); + $this->assertTrue($urlValidator->validate('https://www.maps.google.com')); + } + + /** + * Tests an invalid url as an option to a url bbcode. + * + * @depends testInvalidUrl + */ + public function testInvalidOptionUrlBBCode() + { + $parser = new JBBCode\Parser(); + $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + $parser->parse('[url=javascript:alert("HACKED!");]click me[/url]'); + $this->assertEquals('[url=javascript:alert("HACKED!");]click me[/url]', + $parser->getAsHtml()); + } + + /** + * Tests an invalid url as the body to a url bbcode. + * + * @depends testInvalidUrl + */ + public function testInvalidBodyUrlBBCode() + { + $parser = new JBBCode\Parser(); + $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + $parser->parse('[url]javascript:alert("HACKED!");[/url]'); + $this->assertEquals('[url]javascript:alert("HACKED!");[/url]', $parser->getAsHtml()); + } + + /** + * Tests a valid url as the body to a url bbcode. + * + * @depends testValidUrl + */ + public function testValidUrlBBCode() + { + $parser = new JBBCode\Parser(); + $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + $parser->parse('[url]http://jbbcode.com[/url]'); + $this->assertEquals('http://jbbcode.com', + $parser->getAsHtml()); + } + + /** + * Tests valid english CSS color descriptions on the CssColorValidator. + */ + public function testCssColorEnglish() + { + $colorValidator = new JBBCode\validators\CssColorValidator(); + $this->assertTrue($colorValidator->validate('red')); + $this->assertTrue($colorValidator->validate('yellow')); + $this->assertTrue($colorValidator->validate('LightGoldenRodYellow')); + } + + /** + * Tests valid hexadecimal CSS color values on the CssColorValidator. + */ + public function testCssColorHex() + { + $colorValidator = new JBBCode\validators\CssColorValidator(); + $this->assertTrue($colorValidator->validate('#000')); + $this->assertTrue($colorValidator->validate('#ff0000')); + $this->assertTrue($colorValidator->validate('#aaaaaa')); + } + + /** + * Tests valid rgba CSS color values on the CssColorValidator. + */ + public function testCssColorRgba() + { + $colorValidator = new JBBCode\validators\CssColorValidator(); + $this->assertTrue($colorValidator->validate('rgba(255, 0, 0, 0.5)')); + $this->assertTrue($colorValidator->validate('rgba(50, 50, 50, 0.0)')); + } + + /** + * Tests invalid CSS color values on the CssColorValidator. + */ + public function testInvalidCssColor() + { + $colorValidator = new JBBCode\validators\CssColorValidator(); + $this->assertFalse($colorValidator->validate('" onclick="javascript: alert(\"gotcha!\");')); + $this->assertFalse($colorValidator->validate('">colorful text', + $parser->getAsHtml()); + $parser->parse('[color=#00ff00]green[/color]'); + $this->assertEquals('green', $parser->getAsHtml()); + } + + /** + * Tests invalid css colors in a color bbcode. + * + * @depends testInvalidCssColor + */ + public function testInvalidColorBBCode() + { + $parser = new JBBCode\Parser(); + $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + $parser->parse('[color=" onclick="alert(\'hey ya!\');]click me[/color]'); + $this->assertEquals('[color=" onclick="alert(\'hey ya!\');]click me[/color]', + $parser->getAsHtml()); + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/validators/CssColorValidator.php b/vendor/jbbcode/jbbcode/JBBCode/validators/CssColorValidator.php new file mode 100644 index 000000000..ce51fa487 --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/validators/CssColorValidator.php @@ -0,0 +1,30 @@ +getChildren() as $child) { + $child->accept($this); + } + } + + public function visitTextNode(\JBBCode\TextNode $textNode) + { + $textNode->setValue($this->htmlSafe($textNode->getValue())); + } + + public function visitElementNode(\JBBCode\ElementNode $elementNode) + { + $attrs = $elementNode->getAttribute(); + if (is_array($attrs)) + { + foreach ($attrs as &$el) + $el = $this->htmlSafe($el); + + $elementNode->setAttribute($attrs); + } + + foreach ($elementNode->getChildren() as $child) { + $child->accept($this); + } + } + + protected function htmlSafe($str, $options = null) + { + if (is_null($options)) + { + if (defined('ENT_DISALLOWED')) + $options = ENT_QUOTES | ENT_DISALLOWED | ENT_HTML401; // PHP 5.4+ + else + $options = ENT_QUOTES; // PHP 5.3 + } + + return htmlspecialchars($str, $options, 'UTF-8'); + } +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/visitors/NestLimitVisitor.php b/vendor/jbbcode/jbbcode/JBBCode/visitors/NestLimitVisitor.php new file mode 100644 index 000000000..f550dd023 --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/visitors/NestLimitVisitor.php @@ -0,0 +1,65 @@ +getChildren() as $child) { + $child->accept($this); + } + } + + public function visitTextNode(\JBBCode\TextNode $textNode) + { + /* Nothing to do. Text nodes don't have tag names or children. */ + } + + public function visitElementNode(\JBBCode\ElementNode $elementNode) + { + $tagName = strtolower($elementNode->getTagName()); + + /* Update the current depth for this tag name. */ + if (isset($this->depth[$tagName])) { + $this->depth[$tagName]++; + } else { + $this->depth[$tagName] = 1; + } + + /* Check if $elementNode is nested too deeply. */ + if ($elementNode->getCodeDefinition()->getNestLimit() != -1 && + $elementNode->getCodeDefinition()->getNestLimit() < $this->depth[$tagName]) { + /* This element is nested too deeply. We need to remove it and not visit any + * of its children. */ + $elementNode->getParent()->removeChild($elementNode); + } else { + /* This element is not nested too deeply. Visit all of its children. */ + foreach ($elementNode->getChildren() as $child) { + $child->accept($this); + } + } + + /* Now that we're done visiting this node, decrement the depth. */ + $this->depth[$tagName]--; + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/visitors/SmileyVisitor.php b/vendor/jbbcode/jbbcode/JBBCode/visitors/SmileyVisitor.php new file mode 100644 index 000000000..16fb22a91 --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/visitors/SmileyVisitor.php @@ -0,0 +1,42 @@ +getChildren() as $child) { + $child->accept($this); + } + } + + function visitTextNode(\JBBCode\TextNode $textNode) + { + /* Convert :) into an image tag. */ + $textNode->setValue(str_replace(':)', + ':)', + $textNode->getValue())); + } + + function visitElementNode(\JBBCode\ElementNode $elementNode) + { + /* We only want to visit text nodes within elements if the element's + * code definition allows for its content to be parsed. + */ + if ($elementNode->getCodeDefinition()->parseContent()) { + foreach ($elementNode->getChildren() as $child) { + $child->accept($this); + } + } + } + +} diff --git a/vendor/jbbcode/jbbcode/JBBCode/visitors/TagCountingVisitor.php b/vendor/jbbcode/jbbcode/JBBCode/visitors/TagCountingVisitor.php new file mode 100644 index 000000000..3e52b431a --- /dev/null +++ b/vendor/jbbcode/jbbcode/JBBCode/visitors/TagCountingVisitor.php @@ -0,0 +1,60 @@ +getChildren() as $child) { + $child->accept($this); + } + } + + public function visitTextNode(\JBBCode\TextNode $textNode) + { + // Nothing to do here, text nodes do not have tag names or children + } + + public function visitElementNode(\JBBCode\ElementNode $elementNode) + { + $tagName = strtolower($elementNode->getTagName()); + + // Update this tag name's frequency + if (isset($this->frequencies[$tagName])) { + $this->frequencies[$tagName]++; + } else { + $this->frequencies[$tagName] = 1; + } + + // Visit all the node's childrens + foreach ($elementNode->getChildren() as $child) { + $child->accept($this); + } + + } + + /** + * Retrieves the frequency of the given tag name. + * + * @param $tagName the tag name to look up + */ + public function getFrequency($tagName) + { + if (!isset($this->frequencies[$tagName])) { + return 0; + } else { + return $this->frequencies[$tagName]; + } + } + +} diff --git a/vendor/jbbcode/jbbcode/README.md b/vendor/jbbcode/jbbcode/README.md new file mode 100644 index 000000000..718bcf12b --- /dev/null +++ b/vendor/jbbcode/jbbcode/README.md @@ -0,0 +1,78 @@ +jBBCode +======= + +jBBCode is a bbcode parser written in php 5.3. It's relatively lightweight and parses +bbcodes without resorting to expensive regular expressions. + +Documentation +------------- + +For complete documentation and examples visit [jbbcode.com](http://jbbcode.com). + +###A basic example + +jBBCode includes a few optional, default bbcode definitions that may be loaded through the +`DefaultCodeDefinitionSet` class. Below is a simple example of using these codes to convert +a bbcode string to html. + +```php +addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + +$text = "The default codes include: [b]bold[/b], [i]italics[/i], [u]underlining[/u], "; +$text .= "[url=http://jbbcode.com]links[/url], [color=red]color![/color] and more."; + +$parser->parse($text); + +print $parser->getAsHtml(); +``` + +Contribute +---------- + +I would love help maintaining jBBCode. Look at [open issues](http://github.com/jbowens/jBBCode/issues) for ideas on +what needs to be done. Before submitting a pull request, verify that all unit tests still pass. + +#### Running unit tests +To run the unit tests, +ensure that [phpunit](http://github.com/sebastianbergmann/phpunit) is installed, or install it through the composer +dev dependencies. Then run `phpunit ./tests` from the project directory. If you're adding new functionality, writing +additional unit tests is a great idea. + +Author +------ + +jBBCode was written by Jackson Owens. You may reach him at [jackson_owens@brown.edu](mailto:jackson_owens@brown.edu). + +Other languages +--------------- + +This library is in the process of being ported to java. For more information, see [jBBCode-java](https://github.com/jbowens/jBBCode-java). + +License +------- + + The MIT License + + Copyright (c) 2011 Jackson Owens + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. diff --git a/vendor/jbbcode/jbbcode/composer.json b/vendor/jbbcode/jbbcode/composer.json new file mode 100644 index 000000000..e64dab5b7 --- /dev/null +++ b/vendor/jbbcode/jbbcode/composer.json @@ -0,0 +1,31 @@ +{ + "name": "jbbcode/jbbcode", + "type": "library", + "description": "A lightweight but extensible BBCode parser written in PHP 5.3.", + "keywords": ["BBCode", "BB"], + "homepage": "http://jbbcode.com/", + "license": "MIT", + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, + "config": { + "bin-dir": "/usr/local/bin" + }, + "authors": [ + { + "name": "Jackson Owens", + "email": "jackson_owens@brown.edu", + "homepage": "http://jbowens.org/", + "role": "Developer" + } + ], + "autoload": { + "psr-0": { + "JBBCode": "." + } + }, + "minimum-stability": "stable" +} diff --git a/vendor/leafo/lessphp/.gitignore b/vendor/leafo/lessphp/.gitignore new file mode 100644 index 000000000..ed09d55dc --- /dev/null +++ b/vendor/leafo/lessphp/.gitignore @@ -0,0 +1,8 @@ +*.swp +*~ +/*.less +/*.css +tests/bootstrap +tests/tmp +vendor +composer.lock diff --git a/vendor/leafo/lessphp/.travis.yml b/vendor/leafo/lessphp/.travis.yml new file mode 100644 index 000000000..586a11c15 --- /dev/null +++ b/vendor/leafo/lessphp/.travis.yml @@ -0,0 +1,17 @@ +language: php + +php: + - 5.3 + - 5.4 + - 5.5 + - hhvm + +before_script: + - composer self-update + - composer install --no-interaction --prefer-source --dev + +script: + - phpunit tests + +matrix: + fast_finish: true diff --git a/vendor/leafo/lessphp/LICENSE b/vendor/leafo/lessphp/LICENSE new file mode 100644 index 000000000..49c9ea417 --- /dev/null +++ b/vendor/leafo/lessphp/LICENSE @@ -0,0 +1,660 @@ +For ease of distribution, lessphp 0.4.0 is under a dual license. +You are free to pick which one suits your needs. + + + + +MIT LICENSE + + + + +Copyright (c) 2013 Leaf Corcoran, http://leafo.net/lessphp + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + + +GPL VERSION 3 + + + + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + diff --git a/vendor/leafo/lessphp/Makefile b/vendor/leafo/lessphp/Makefile new file mode 100644 index 000000000..a5d262cd8 --- /dev/null +++ b/vendor/leafo/lessphp/Makefile @@ -0,0 +1,7 @@ + +test: + phpunit --colors tests + +release: + ./package.sh + diff --git a/vendor/leafo/lessphp/README.md b/vendor/leafo/lessphp/README.md new file mode 100644 index 000000000..1379ddce2 --- /dev/null +++ b/vendor/leafo/lessphp/README.md @@ -0,0 +1,96 @@ +[![Build Status](https://travis-ci.org/leafo/lessphp.svg?branch=master)](https://travis-ci.org/leafo/lessphp) + +# lessphp v0.5.0 +### + +`lessphp` is a compiler for LESS written in PHP. The documentation is great, +so check it out: . + +Here's a quick tutorial: + +### How to use in your PHP project + +The only file required is `lessc.inc.php`, so copy that to your include directory. + +The typical flow of **lessphp** is to create a new instance of `lessc`, +configure it how you like, then tell it to compile something using one built in +compile methods. + +The `compile` method compiles a string of LESS code to CSS. + +```php +compile(".block { padding: 3 + 4px }"); +``` + +The `compileFile` method reads and compiles a file. It will either return the +result or write it to the path specified by an optional second argument. + +```php +compileFile("input.less"); +``` + +The `compileChecked` method is like `compileFile`, but it only compiles if the output +file doesn't exist or it's older than the input file: + +```php +checkedCompile("input.less", "output.css"); +``` + +If there any problem compiling your code, an exception is thrown with a helpful message: + +```php +compile("invalid LESS } {"); +} catch (exception $e) { + echo "fatal error: " . $e->getMessage(); +} +``` + +The `lessc` object can be configured through an assortment of instance methods. +Some possible configuration options include [changing the output format][1], +[setting variables from PHP][2], and [controlling the preservation of +comments][3], writing [custom functions][4] and much more. It's all described +in [the documentation][0]. + + + [0]: http://leafo.net/lessphp/docs/ + [1]: http://leafo.net/lessphp/docs/#output_formatting + [2]: http://leafo.net/lessphp/docs/#setting_variables_from_php + [3]: http://leafo.net/lessphp/docs/#preserving_comments + [4]: http://leafo.net/lessphp/docs/#custom_functions + + +### How to use from the command line + +An additional script has been included to use the compiler from the command +line. In the simplest invocation, you specify an input file and the compiled +css is written to standard out: + + $ plessc input.less > output.css + +Using the -r flag, you can specify LESS code directly as an argument or, if +the argument is left off, from standard in: + + $ plessc -r "my less code here" + +Finally, by using the -w flag you can watch a specified input file and have it +compile as needed to the output file: + + $ plessc -w input-file output-file + +Errors from watch mode are written to standard out. + +The -f flag sets the [output formatter][1]. For example, to compress the +output run this: + + $ plessc -f=compressed myfile.less + +For more help, run `plessc --help` + diff --git a/vendor/leafo/lessphp/composer.json b/vendor/leafo/lessphp/composer.json new file mode 100644 index 000000000..0f06ba090 --- /dev/null +++ b/vendor/leafo/lessphp/composer.json @@ -0,0 +1,25 @@ +{ + "name": "leafo/lessphp", + "type": "library", + "description": "lessphp is a compiler for LESS written in PHP.", + "homepage": "http://leafo.net/lessphp/", + "license": [ + "MIT", + "GPL-3.0" + ], + "authors": [ + { + "name": "Leaf Corcoran", + "email": "leafot@gmail.com", + "homepage": "http://leafo.net" + } + ], + "autoload": { + "classmap": ["lessc.inc.php"] + }, + "extra": { + "branch-alias": { + "dev-master": "0.4.x-dev" + } + } +} diff --git a/vendor/leafo/lessphp/docs/docs.md b/vendor/leafo/lessphp/docs/docs.md new file mode 100644 index 000000000..112dc2ee3 --- /dev/null +++ b/vendor/leafo/lessphp/docs/docs.md @@ -0,0 +1,1400 @@ + title: v0.5.0 documentation + link_to_home: true +-- + +

Documentation v0.5.0

+ +
$index
+ +**lessphp** is a compiler that generates CSS from a superset language which +adds a collection of convenient features often seen in other languages. All CSS +is compatible with LESS, so you can start using new features with your existing CSS. + +It is designed to be compatible with [less.js](http://lesscss.org), and suitable +as a drop in replacement for PHP projects. + +## Getting Started + +The homepage for **lessphp** can be found at [http://leafo.net/lessphp/][1]. + +You can follow development at the project's [GitHub][2]. + +Including **lessphp** in your project is as simple as dropping the single +include file into your code base and running the appropriate compile method as +described in the [PHP Interface](#php_interface). + + [1]: http://leafo.net/lessphp "lessphp homepage" + [2]: https://github.com/leafo/lessphp "lessphp GitHub page" + +## Installation + +**lessphp** is distributed entirely in a single stand-alone file. Download the +latest version from either [the homepage][1] or [GitHub][2]. + +Development versions can also be downloading from GitHub. + +Place `lessphp.inc.php` in a location available to your PHP scripts, and +include it. That's it! you're ready to begin. + +## The Language + +**lessphp** is very easy to learn because it generally functions how you would +expect it to. If you feel something is challenging or missing, feel free to +open an issue on the [bug tracker](https://github.com/leafo/lessphp/issues). + +It is also easy to learn because any standards-compliant CSS code is valid LESS +code. You are free to gradually enhance your existing CSS code base with LESS +features without having to worry about rewriting anything. + +The following is a description of the new languages features provided by LESS. + +### Line Comments + +Simple but very useful; line comments are started with `//`: + + ```less + // this is a comment + body { + color: red; // as is this + /* block comments still work also */ + } + ``` + +### Variables + +Variables are identified with a name that starts with `@`. To declare a +variable, you create an appropriately named CSS property and assign it a value: + + ```less + @family: "verdana"; + @color: red; + body { + @mycolor: red; + font-family: @family; + color: @color; + border-bottom: 1px solid @color; + } + ``` + +Variable declarations will not appear in the output. Variables can be declared +in the outer most scope of the file, or anywhere else a CSS property may +appear. They can hold any CSS property value. + +Variables are only visible for use from their current scope, or any enclosed +scopes. + +If you have a string or keyword in a variable, you can reference another +variable by that name by repeating the `@`: + + ```less + @value: 20px; + @value_name: "value"; + + width: @@value_name; + ``` + +### Expressions + +Expressions let you combine values and variables in meaningful ways. For +example you can add to a color to make it a different shade. Or divide up the +width of your layout logically. You can even concatenate strings. + +Use the mathematical operators to evaluate an expression: + + ```less + @width: 960px; + .nav { + width: @width / 3; + color: #001 + #abc; + } + .body { + width: 2 * @width / 3; + font-family: "hel" + "vetica"; + } + ``` + +Parentheses can be used to control the order of evaluation. They can also be +used to force an evaluation for cases where CSS's syntax makes the expression +ambiguous. + +The following property will produce two numbers, instead of doing the +subtraction: + + ```less + margin: 10px -5px; + ``` + +To force the subtraction: + + ```less + margin: (10px -5px); + ``` + +It is also safe to surround mathematical operators by spaces to ensure that +they are evaluated: + + ```less + margin: 10px - 5px; + ``` + +Division has a special quirk. There are certain CSS properties that use the `/` +operator as part of their value's syntax. Namely, the [font][4] shorthand and +[border-radius][3]. + + [3]: https://developer.mozilla.org/en/CSS/border-radius + [4]: https://developer.mozilla.org/en/CSS/font + + +Thus, **lessphp** will ignore any division in these properties unless it is +wrapped in parentheses. For example, no division will take place here: + + ```less + .font { + font: 20px/80px "Times New Roman"; + } + ``` + +In order to force division we must wrap the expression in parentheses: + + ```less + .font { + font: (20px/80px) "Times New Roman"; + } + ``` + +If you want to write a literal `/` expression without dividing in another +property (or a variable), you can use [string unquoting](#string_unquoting): + + ```less + .var { + @size: ~"20px/80px"; + font: @size sans-serif; + } + ``` + +### Nested Blocks + +By nesting blocks we can build up a chain of CSS selectors through scope +instead of repeating them. In addition to reducing repetition, this also helps +logically organize the structure of our CSS. + + ```less + ol.list { + li.special { + border: 1px solid red; + } + + li.plain { + font-weight: bold; + } + } + ``` + + +This will produce two blocks, a `ol.list li.special` and `ol.list li.plain`. + +Blocks can be nested as deep as required in order to build a hierarchy of +relationships. + +The `&` operator can be used in a selector to represent its parent's selector. +If the `&` operator is used, then the default action of appending the parent to +the front of the child selector separated by space is not performed. + + ```less + b { + a & { + color: red; + } + + // the following have the same effect + + & i { + color: blue; + } + + i { + color: blue; + } + } + ``` + + +Because the `&` operator respects the whitespace around it, we can use it to +control how the child blocks are joined. Consider the differences between the +following: + + ```less + div { + .child-class { color: purple; } + + &.isa-class { color: green; } + + #child-id { height: 200px; } + + &#div-id { height: 400px; } + + &:hover { color: red; } + + :link { color: blue; } + } + ``` + +The `&` operator also works with [mixins](#mixins), which produces interesting results: + + ```less + .within_box_style() { + .box & { + color: blue; + } + } + + #menu { + .within_box_style; + } + ``` + +### Mixins + +Any block can be mixed in just by naming it: + + ```less + .mymixin { + color: blue; + border: 1px solid red; + + .special { + font-weight: bold; + } + } + + + h1 { + font-size: 200px; + .mymixin; + } + ``` + +All properties and child blocks are mixed in. + +Mixins can be made parametric, meaning they can take arguments, in order to +enhance their utility. A parametric mixin all by itself is not outputted when +compiled. Its properties will only appear when mixed into another block. + +The canonical example is to create a rounded corners mixin that works across +browsers: + + ```less + .rounded-corners(@radius: 5px) { + border-radius: @radius; + -webkit-border-radius: @radius; + -moz-border-radius: @radius; + } + + .header { + .rounded-corners(); + } + + .info { + background: red; + .rounded-corners(14px); + } + ``` + +If you have a mixin that doesn't have any arguments, but you don't want it to +show up in the output, give it a blank argument list: + + ```less + .secret() { + font-size: 6000px; + } + + .div { + .secret; + } + ``` + +If the mixin doesn't need any arguments, you can leave off the parentheses when +mixing it in, as seen above. + +You can also mixin a block that is nested inside other blocks. You can think of +the outer block as a way of making a scope for your mixins. You just list the +names of the mixins separated by spaces, which describes the path to the mixin +you want to include. Optionally you can separate them by `>`. + + ```less + .my_scope { + .some_color { + color: red; + .inner_block { + text-decoration: underline; + } + } + .bold { + font-weight: bold; + color: blue; + } + } + + .a_block { + .my_scope .some_color; + .my_scope .some_color .inner_block; + } + + .another_block { + // the alternative syntax + .my_scope > .bold; + } + ``` + + +#### Mixin Arguments + +When declaring a mixin you can specify default values for each argument. Any +argument left out will be given the default value specified. Here's the +syntax: + +```less +.mix(@color: red, @height: 20px, @pad: 12px) { + border: 1px solid @color; + height: @height - @pad; + padding: @pad; +} + +.default1 { + .mix(); +} + +.default2 { + .mix(blue); +} + +.default3 { + .mix(blue, 40px, 5px); +} +``` + +Additionally, you can also call a mixin using the argument names, this is +useful if you want to replace a specific argument while having all the others +take the default regardless of what position the argument appears in. The +syntax looks something like this: + + +```lessbasic +div { + .my_mixin(@paddding: 4px); // @color and @height get default values + .my_mixin(@paddding: 4px, @height: 50px); // you can specify them in any order +} +``` + +You can also combine the ordered arguments with the named ones: + +```lessbasic +div { + // @color is blue, @padding is 4px, @height is default + .my_mixin(blue, @padding: 4px); +} +``` + +Mixin arguments can be delimited with either a `,` or `;`, but only one can be +active at once. This means that each argument is separated by either `,` or +`;`. By default `,` is the delimiter, in all the above examples we used a `,`. + +A problem arises though, sometimes CSS value lists are made up with commas. In +order to be able to pass a comma separated list literal we need to use `;` as +the delimiter. (You don't need to worry about this if your list is stored in a +variable) + +If a `;` appears anywhere in the argument list, then it will be used as the +argument delimiter, and all commas we be used as part of the argument values. + +Here's a basic example: + +```less +.fancy_mixin(@box_shadow, @color: blue) { + border: 1px solid @color; + box-shadow: @box_shadow; +} + + +div { + // two arguments passed separated by ; + .fancy_mixin(2px 2px, -2px -2px; red); +} + +pre { + // one argument passed, ends in ; + .fancy_mixin(inset 4px 4px, -2px 2px;); +} + +``` + +If we only want to pass a single comma separated value we still need to use +`;`, to do this we stick it on the end as demonstrated above. + + +#### `@arguments` Variable + +Within an mixin there is a special variable named `@arguments` that contains +all the arguments passed to the mixin along with any remaining arguments that +have default values. The value of the variable has all the values separated by +spaces. + +This useful for quickly assigning all the arguments: + + ```less + .box-shadow(@x, @y, @blur, @color) { + box-shadow: @arguments; + -webkit-box-shadow: @arguments; + -moz-box-shadow: @arguments; + } + .menu { + .box-shadow(1px, 1px, 5px, #aaa); + } + ``` + +In addition to the arguments passed to the mixin, `@arguments` will also include +remaining default values assigned by the mixin: + + + ```less + .border-mixin(@width, @style: solid, @color: black) { + border: @arguments; + } + + pre { + .border-mixin(4px, dotted); + } + + ``` + + +#### Pattern Matching + +When you *mix in* a mixin, all the available mixins of that name in the current +scope are checked to see if they match based on what was passed to the mixin +and how it was declared. + +The simplest case is matching by number of arguments. Only the mixins that +match the number of arguments passed in are used. + + ```less + .simple() { // matches no arguments + height: 10px; + } + + .simple(@a, @b) { // matches two arguments + color: red; + } + + .simple(@a) { // matches one argument + color: blue; + } + + div { + .simple(10); + } + + span { + .simple(10, 20); + } + ``` + +Whether an argument has default values is also taken into account when matching +based on number of arguments: + + ```less + // matches one or two arguments + .hello(@a, @b: blue) { + height: @a; + color: @b; + } + + .hello(@a, @b) { // matches only two + width: @a; + border-color: @b; + } + + .hello(@a) { // matches only one + padding: 1em; + } + + div { + .hello(10px); + } + + pre { + .hello(10px, yellow); + } + ``` + +Additionally, a *vararg* value can be used to further control how things are +matched. A mixin's argument list can optionally end in the special argument +named `...`. The `...` may match any number of arguments, including 0. + + ```less + // this will match any number of arguments + .first(...) { + color: blue; + } + + // matches at least 1 argument + .second(@arg, ...) { + height: 200px + @arg; + } + + div { .first("some", "args"); } + pre { .second(10px); } + ``` + +If you want to capture the values that get captured by the *vararg* you can +give it a variable name by putting it directly before the `...`. This variable +must be the last argument defined. It's value is just like the special +[`@arguments` variable](#arguments_variable), a space separated list. + + + ```less + .hello(@first, @rest...) { + color: @first; + text-shadow: @rest; + } + + span { + .hello(red, 1px, 1px, 0px, white); + } + + ``` + +Another way of controlling whether a mixin matches is by specifying a value in +place of an argument name when declaring the mixin: + + ```less + .style(old, @size) { + font: @size serif; + } + + .style(new, @size) { + font: @size sans-serif; + } + + .style(@_, @size) { + letter-spacing: floor(@size / 6px); + } + + em { + @switch: old; + .style(@switch, 15px); + } + ``` + +Notice that two of the three mixins were matched. The mixin with a matching +first argument, and the generic mixin that matches two arguments. It's common +to use `@_` as the name of a variable we intend to not use. It has no special +meaning to LESS, just to the reader of the code. + +#### Guards + +Another way of restricting when a mixin is mixed in is by using guards. A guard +is a special expression that is associated with a mixin declaration that is +evaluated during the mixin process. It must evaluate to true before the mixin +can be used. + +We use the `when` keyword to begin describing a list of guard expressions. + +Here's a simple example: + + ```less + .guarded(@arg) when (@arg = hello) { + color: blue; + } + + div { + .guarded(hello); // match + } + + span { + .guarded(world); // no match + } + ``` +Only the `div`'s mixin will match in this case, because the guard expression +requires that `@arg` is equal to `hello`. + +We can include many different guard expressions by separating them by commas. +Only one of them needs to match to trigger the mixin: + + ```less + .x(@a, @b) when (@a = hello), (@b = world) { + width: 960px; + } + + div { + .x(hello, bar); // match + } + + span { + .x(foo, world); // match + } + + pre { + .x(foo, bar); // no match + } + ``` + +Instead of a comma, we can use `and` keyword to make it so all of the guards +must match in order to trigger the mixin. `and` has higher precedence than the +comma. + + ```less + .y(@a, @b) when (@a = hello) and (@b = world) { + height: 600px; + } + + div { + .y(hello, world); // match + } + + span { + .y(hello, bar); // no match + } + ``` + +Commas and `and`s can be mixed and matched. + +You can also negate a guard expression by using `not` in from of the parentheses: + + ```less + .x(@a) when not (@a = hello) { + color: blue; + } + + div { + .x(hello); // no match + } + ``` + +The `=` operator is used to check equality between any two values. For numbers +the following comparison operators are also defined: + +`<`, `>`, `=<`, `>=` + +There is also a collection of predicate functions that can be used to test the +type of a value. + +These are `isnumber`, `iscolor`, `iskeyword`, `isstring`, `ispixel`, +`ispercentage` and `isem`. + + ```less + .mix(@a) when (ispercentage(@a)) { + height: 500px * @a; + } + .mix(@a) when (ispixel(@a)) { + height: @a; + } + + div.a { + .mix(50%); + } + + div.a { + .mix(350px); + } + ``` + +#### !important + +If you want to apply the `!important` suffix to every property when mixing in a +mixin, just append `!important` to the end of the call to the mixin: + + ```less + .make_bright { + color: red; + font-weight: bold; + } + + .color { + color: green; + } + + body { + .make_bright() !important; + .color(); + } + + ``` + +### Selector Expressions + +Sometimes we want to dynamically generate the selector of a block based on some +variable or expression. We can do this by using *selector expressions*. Selector +expressions are CSS selectors that are evaluated in the current scope before +being written out. + +A simple example is a mixin that dynamically creates a selector named after the +mixin's argument: + + ```less + .create-selector(@name) { + @{name} { + color: red; + } + } + + .create-selector(hello); + .create-selector(world); + ``` + +The string interpolation syntax works inside of selectors, letting you insert varaibles. + +Here's an interesting example adapted from Twitter Bootstrap. A couple advanced +things are going on. We are using [Guards](#guards) along with a recursive +mixin to work like a loop to generate a series of CSS blocks. + + + ```less + // create our recursive mixin: + .spanX (@index) when (@index > 0) { + .span@{index} { + width: @index * 100px; + } + .spanX(@index - 1); + } + .spanX (0) {} + + // mix it into the global scopee: + .spanX(4); + ``` + +### Import + +Multiple LESS files can be compiled into a single CSS file by using the +`@import` statement. Be careful, the LESS import statement shares syntax with +the CSS import statement. If the file being imported ends in a `.less` +extension, or no extension, then it is treated as a LESS import. Otherwise it +is left alone and outputted directly: + + ```lessbasic + // my_file.less + .some-mixin(@height) { + height: @height; + } + + // main.less + @import "main.less" // will import the file if it can be found + @import "main.css" // will be left alone + + body { + .some-mixin(400px); + } + ``` + +All of the following lines are valid ways to import the same file: + + ```lessbasic + @import "file"; + @import 'file.less'; + @import url("file"); + @import url('file'); + @import url(file); + ``` + +When importing, the `importDir` is searched for files. This can be configured, +see [PHP Interface](#php_interface). + +A file is only imported once. If you try to include the same file multiple +times all the import statements after the first produce no output. + +### String Interpolation + +String interpolation is a convenient way to insert the value of a variable +right into a string literal. Given some variable named `@var_name`, you just +need to write it as `@{var_name}` from within the string to have its value +inserted: + + ```less + @symbol: ">"; + h1:before { + content: "@{symbol}: "; + } + + h2:before { + content: "@{symbol}@{symbol}: "; + } + ``` + +There are two kinds of strings, implicit and explicit strings. Explicit strings +are wrapped by double quotes, `"hello I am a string"`, or single quotes `'I am +another string'`. Implicit strings only appear when using `url()`. The text +between the parentheses is considered a string and thus string interpolation is +possible: + + ```less + @path: "files/"; + body { + background: url(@{path}my_background.png); + } + ``` + +### String Format Function + +The `%` function can be used to insert values into strings using a *format +string*. It works similar to `printf` seen in other languages. It has the +same purpose as string interpolation above, but gives explicit control over +the output format. + + ```less + @symbol: ">"; + h1:before { + content: %("%s: ", @symbol); + } + ``` + +The `%` function takes as its first argument the format string, following any +number of addition arguments that are inserted in place of the format +directives. + +A format directive starts with a `%` and is followed by a single character that +is either `a`, `d`, or `s`: + + ```less + strings: %("%a %d %s %a", hi, 1, 'ok', 'cool'); + ``` + +`%a` and `%d` format the value the same way: they compile the argument to its +CSS value and insert it directly. When used with a string, the quotes are +included in the output. This typically isn't what we want, so we have the `%s` +format directive which strips quotes from strings before inserting them. + +The `%d` directive functions the same as `%a`, but is typically used for numbers +assuming the output format of numbers might change in the future. + +### String Unquoting + +Sometimes you will need to write proprietary CSS syntax that is unable to be +parsed. As a workaround you can place the code into a string and unquote it. +Unquoting is the process of outputting a string without its surrounding quotes. +There are two ways to unquote a string. + +The `~` operator in front of a string will unquote that string: + + ```less + .class { + // a made up, but problematic vendor specific CSS + filter: ~"Microsoft.AlphaImage(src='image.png')"; + } + ``` + +If you are working with other types, such as variables, there is a built in +function that let's you unquote any value. It is called `e`. + + ```less + @color: "red"; + .class { + color: e(@color); + } + ``` + +### Built In Functions + +**lessphp** has a collection of built in functions: + +* `e(str)` -- returns a string without the surrounding quotes. + See [String Unquoting](#string_unquoting) + +* `floor(number)` -- returns the floor of a numerical input +* `round(number, [precision])` -- returns the rounded value of numerical input with optional precision + +* `lighten(color, percent)` -- lightens `color` by `percent` and returns it +* `darken(color, percent)` -- darkens `color` by `percent` and returns it + +* `saturate(color, percent)` -- saturates `color` by `percent` and returns it +* `desaturate(color, percent)` -- desaturates `color` by `percent` and returns it + +* `fadein(color, percent)` -- makes `color` less transparent by `percent` and returns it +* `fadeout(color, percent)` -- makes `color` more transparent by `percent` and returns it + +* `spin(color, amount)` -- returns a color with `amount` degrees added to hue + +* `fade(color, amount)` -- returns a color with the alpha set to `amount` + +* `hue(color)` -- returns the hue of `color` + +* `saturation(color)` -- returns the saturation of `color` + +* `lightness(color)` -- returns the lightness of `color` + +* `alpha(color)` -- returns the alpha value of `color` or 1.0 if it doesn't have an alpha + +* `percentage(number)` -- converts a floating point number to a percentage, e.g. `0.65` -> `65%` + +* `mix(color1, color1, percent)` -- mixes two colors by percentage where 100% + keeps all of `color1`, and 0% keeps all of `color2`. Will take into account + the alpha of the colors if it exists. See + . + +* `contrast(color, dark, light)` -- if `color` has a lightness value greater + than 50% then `dark` is returned, otherwise return `light`. + +* `extract(list, index)` -- returns the `index`th item from `list`. The list is + `1` indexed, meaning the first item's index is 1, the second is 2, and etc. + +* `pow(base, exp)` -- returns `base` raised to the power of `exp` + +* `pi()` -- returns pi + +* `mod(a,b)` -- returns `a` modulus `b` + +* `tan(a)` -- returns tangent of `a` where `a` is in radians + +* `cos(a)` -- returns cosine of `a` where `a` is in radians + +* `sin(a)` -- returns sine of `a` where `a` is in radians + +* `atan(a)` -- returns arc tangent of `a` + +* `acos(a)` -- returns arc cosine of `a` + +* `asin(a)` -- returns arc sine of `a` + +* `sqrt(a)` -- returns square root of `a` + +* `rgbahex(color)` -- returns a string containing 4 part hex color. + + This is used to convert a CSS color into the hex format that IE's filter + method expects when working with an alpha component. + + ```less + .class { + @start: rgbahex(rgba(25, 34, 23, .5)); + @end: rgbahex(rgba(85, 74, 103, .6)); + // abridged example + -ms-filter: + e("gradient(start=@{start},end=@{end})"); + } + ``` + +## PHP Interface + +When working with **lessphp** from PHP, the typical flow is to create a new +instance of `lessc`, configure it how you like, then tell it to compile +something using one built in compile methods. + +Methods: + +* [`compile($string)`](#compiling[) -- Compile a string + +* [`compileFile($inFile, [$outFile])`](#compiling) -- Compile a file to another or return it + +* [`checkedCompile($inFile, $outFile)`](#compiling) -- Compile a file only if it's newer + +* [`cachedCompile($cacheOrFile, [$force])`](#compiling_automatically) -- Conditionally compile while tracking imports + +* [`setFormatter($formatterName)`](#output_formatting) -- Change how CSS output looks + +* [`setPreserveComments($keepComments)`](#preserving_comments) -- Change if comments are kept in output + +* [`registerFunction($name, $callable)`](#custom_functions) -- Add a custom function + +* [`unregisterFunction($name)`](#custom_functions) -- Remove a registered function + +* [`setVariables($vars)`](#setting_variables_from_php) -- Set a variable from PHP + +* [`unsetVariable($name)`](#setting_variables_from_php) -- Remove a PHP variable + +* [`setImportDir($dirs)`](#import_directory) -- Set the search path for imports + +* [`addImportDir($dir)`](#import_directory) -- Append directory to search path for imports + + +### Compiling + +The `compile` method compiles a string of LESS code to CSS. + + ```php + compile(".block { padding: 3 + 4px }"); + ``` + +The `compileFile` method reads and compiles a file. It will either return the +result or write it to the path specified by an optional second argument. + + ```php + echo $less->compileFile("input.less"); + ``` + +The `compileChecked` method is like `compileFile`, but it only compiles if the output +file doesn't exist or it's older than the input file: + + ```php + $less->checkedCompile("input.less", "output.css"); + ``` + +See [Compiling Automatically](#compiling_automatically) for a description of +the more advanced `cachedCompile` method. + +### Output Formatting + +Output formatting controls the indentation of the output CSS. Besides the +default formatter, two additional ones are included and it's also easy to make +your own. + +To use a formatter, the method `setFormatter` is used. Just +pass the name of the formatter: + + ```php + $less = new lessc; + + $less->setFormatter("compressed"); + echo $less->compile("div { color: lighten(blue, 10%) }"); + ``` + +In this example, the `compressed` formatter is used. The formatters are: + + * `lessjs` *(default)* -- Same style used in LESS for JavaScript + + * `compressed` -- Compresses all the unrequired whitespace + + * `classic` -- **lessphp**'s original formatter + +To revert to the default formatter, call `setFormatter` with a value of `null`. + +#### Custom Formatter + +The easiest way to customize the formatter is to create your own instance of an +existing formatter and alter its public properties before passing it off to +**lessphp**. The `setFormatter` method can also take an instance of a +formatter. + +Each of the formatter names corresponds to a class with `lessc_formatter_` +prepended in front of it. Here the classic formatter is customized to use tabs +instead of spaces: + + + ```php + $formatter = new lessc_formatter_classic; + $formatter->indentChar = "\t"; + + $less = new lessc; + $less->setFormatter($formatter); + echo $less->compileFile("myfile.less"); + ``` + +For more information about what can be configured with the formatter consult +the source code. + +### Preserving Comments + +By default, all comments in the source LESS file are stripped when compiling. +You might want to keep the `/* */` comments in the output though. For +example, bundling a license in the file. + +Enable or disable comment preservation by calling `setPreserveComments`: + + ```php + $less = new lessc; + $less->setPreserveComments(true); + echo $less->compile("/* hello! */"); + ``` + +Comments are disabled by default because there is additional overhead, and more +often than not they aren't needed. + + +### Compiling Automatically + +Often, you want to only compile a LESS file only if it has been modified since +last compile. This is very important because compiling is performance intensive +and you should avoid a recompile if it possible. + +The `checkedCompile` compile method will do just that. It will check if the +input file is newer than the output file, or if the output file doesn't exist +yet, and compile only then. + + ```php + $less->checkedCompile("input.less", "output.css"); + ``` + +There's a problem though. `checkedCompile` is very basic, it only checks the +input file's modification time. It is unaware of any files from `@import`. + + +For this reason we also have `cachedCompile`. It's slightly more complex, but +gives us the ability to check changes to all files including those imported. It +takes one argument, either the name of the file we want to compile, or an +existing *cache object*. Its return value is an updated cache object. + +If we don't have a cache object, then we call the function with the name of the +file to get the initial cache object. If we do have a cache object, then we +call the function with it. In both cases, an updated cache object is returned. + +The cache object keeps track of all the files that must be checked in order to +determine if a rebuild is required. + +The cache object is a plain PHP `array`. It stores the last time it compiled in +`$cache["updated"]` and output of the compile in `$cache["compiled"]`. + +Here we demonstrate creating an new cache object, then using it to see if we +have a recompiled version available to be written: + + + ```php + $inputFile = "myfile.less"; + $outputFile = "myfile.css"; + + $less = new lessc; + + // create a new cache object, and compile + $cache = $less->cachedCompile($inputFile); + + file_put_contents($outputFile, $cache["compiled"]); + + // the next time we run, write only if it has updated + $last_updated = $cache["updated"]; + $cache = $less->cachedCompile($cache); + if ($cache["updated"] > $last_updated) { + file_put_contents($outputFile, $cache["compiled"]); + } + + ``` + +In order for the system to fully work, we must save cache object between +requests. Because it's a plain PHP `array`, it's sufficient to +[`serialize`](http://php.net/serialize) it and save it the string somewhere +like a file or in persistent memory. + +An example with saving cache object to a file: + + ```php + function autoCompileLess($inputFile, $outputFile) { + // load the cache + $cacheFile = $inputFile.".cache"; + + if (file_exists($cacheFile)) { + $cache = unserialize(file_get_contents($cacheFile)); + } else { + $cache = $inputFile; + } + + $less = new lessc; + $newCache = $less->cachedCompile($cache); + + if (!is_array($cache) || $newCache["updated"] > $cache["updated"]) { + file_put_contents($cacheFile, serialize($newCache)); + file_put_contents($outputFile, $newCache['compiled']); + } + } + + autoCompileLess('myfile.less', 'myfile.css'); + ``` + +`cachedCompile` method takes an optional second argument, `$force`. Passing in +true will cause the input to always be recompiled. + +### Error Handling + +All of the compile methods will throw an `Exception` if the parsing fails or +there is a compile time error. Compile time errors include things like passing +incorrectly typed values for functions that expect specific things, like the +color manipulation functions. + + ```php + $less = new lessc; + try { + $less->compile("} invalid LESS }}}"); + } catch (Exception $ex) { + echo "lessphp fatal error: ".$ex->getMessage(); + } + ``` +### Setting Variables From PHP + +Before compiling any code you can set initial LESS variables from PHP. The +`setVariables` method lets us do this. It takes an associative array of names +to values. The values must be strings, and will be parsed into correct CSS +values. + + + ```php + $less = new lessc; + + $less->setVariables(array( + "color" => "red", + "base" => "960px" + )); + + echo $less->compile(".magic { color: @color; width: @base - 200; }"); + ``` + +If you need to unset a variable, the `unsetVariable` method is available. It +takes the name of the variable to unset. + + ```php + $less->unsetVariable("color"); + ``` + +Be aware that the value of the variable is a string containing a CSS value. So +if you want to pass a LESS string in, you're going to need two sets of quotes. +One for PHP and one for LESS. + + + ```php + $less->setVariables(array( + "url" => "'http://example.com.com/'" + )); + + echo $less->compile("body { background: url("@{url}/bg.png"); }"); + ``` + +### Import Directory + +When running the `@import` directive, an array of directories called the import +search path is searched through to find the file being asked for. + +By default, when using `compile`, the import search path just contains `""`, +which is equivalent to the current directory of the script. If `compileFile` is +used, then the directory of the file being compiled is used as the starting +import search path. + +Two methods are available for configuring the search path. + +`setImportDir` will overwrite the search path with its argument. If the value +isn't an array it will be converted to one. + + +In this example, `@import "colors";` will look for either +`assets/less/colors.less` or `assets/bootstrap/colors.less` in that order: + + ```php + $less->setImportDir(array("assets/less/", "assets/bootstrap")); + + echo $less->compile('@import "colors";'); + ``` + +`addImportDir` will append a single path to the import search path instead of +overwriting the whole thing. + + ```php + $less->addImportDir("public/stylesheets"); + ``` + +### Custom Functions + +**lessphp** has a simple extension interface where you can implement user +functions that will be exposed in LESS code during the compile. They can be a +little tricky though because you need to work with the **lessphp** type system. + +The two methods we are interested in are `registerFunction` and +`unregisterFunction`. `registerFunction` takes two arguments, a name and a +callable value. `unregisterFunction` just takes the name of an existing +function to remove. + +Here's an example that adds a function called `double` that doubles any numeric +argument: + + ```php + registerFunction("double", "lessphp_double"); + + // gives us a width of 800px + echo $less->compile("div { width: double(400px); }"); + ``` + +The second argument to `registerFunction` is any *callable value* that is +understood by [`call_user_func`](http://php.net/call_user_func). + +If we are using PHP 5.3 or above then we are free to pass a function literal +like so: + + ```php + $less->registerFunction("double", function($arg) { + list($type, $value, $unit) = $arg; + return array($type, $value*2, $unit); + }); + ``` + +Now let's talk about the `double` function itself. + +Although a little verbose, the implementation gives us some insight on the type +system. All values in **lessphp** are stored in an array where the 0th element +is a string representing the type, and the other elements make up the +associated data for that value. + +The best way to get an understanding of the system is to register is dummy +function which does a `var_dump` on the argument. Try passing the function +different values from LESS and see what the results are. + +The return value of the registered function must also be a **lessphp** type, +but if it is a string or numeric value, it will automatically be coerced into +an appropriate typed value. In our example, we reconstruct the value with our +modifications while making sure that we preserve the original type. + +The instance of **lessphp** itself is sent to the registered function as the +second argument in addition to the arguments array. + +## Command Line Interface + +**lessphp** comes with a command line script written in PHP that can be used to +invoke the compiler from the terminal. On Linux and OSX, all you need to do is +place `plessc` and `lessc.inc.php` somewhere in your PATH (or you can run it in +the current directory as well). On windows you'll need a copy of `php.exe` to +run the file. To compile a file, `input.less` to CSS, run: + + ```bash + $ plessc input.less + ``` + +To write to a file, redirect standard out: + + ```bash + $ plessc input.less > output.css + ``` + +To compile code directly on the command line: + + ```bash + $ plessc -r "@color: red; body { color: @color; }" + ``` + +To watch a file for changes, and compile it as needed, use the `-w` flag: + + ```bash + $ plessc -w input-file output-file + ``` + +Errors from watch mode are written to standard out. + + +## License + +Copyright (c) 2012 Leaf Corcoran, + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +*Also under GPL3 if required, see `LICENSE` file* + diff --git a/vendor/leafo/lessphp/lessc.inc.php b/vendor/leafo/lessphp/lessc.inc.php new file mode 100644 index 000000000..2292f219d --- /dev/null +++ b/vendor/leafo/lessphp/lessc.inc.php @@ -0,0 +1,3768 @@ + + * 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 `lessc_parser` into a syntax tree, then it is compiled into another tree + * representing the CSS structure by `lessc`. The CSS tree is fed into a + * formatter, like `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 `lessc` is the entry point. + * + * In summary: + * + * The `lessc` class creates an instance 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 `lessc_parser` class is only concerned with parsing its input. + * + * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string, + * handling things like indentation. + */ +class lessc { + static public $VERSION = "v0.5.0"; + + static public $TRUE = array("keyword", "true"); + static public $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; + + 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; + protected $sourceLoc = null; + + static protected $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); + } + + static public function compressList($items, $delim) { + if (!isset($items[1]) && isset($items[0])) return $items[0]; + else return array('list', $delim, $items); + } + + static public 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 */"); + } + + if (isset($this->allParsedFiles[realpath($realPath)])) { + return array(false, null); + } + + $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 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); + } + $out->lines = $this->deduplicate($out->lines); + } + + /** + * Deduplicate lines in a block. Comments are not deduplicated. If a + * duplicate rule is detected, the comments immediately preceding each + * occurence are consolidated. + */ + protected function deduplicate($lines) { + $unique = array(); + $comments = array(); + + foreach($lines as $line) { + if (strpos($line, '/*') === 0) { + $comments[] = $line; + continue; + } + if (!in_array($line, $unique)) { + $unique[] = $line; + } + array_splice($unique, array_search($line, $unique), 0, $comments); + $comments = array(); + } + return array_merge($unique, $comments); + } + + protected function sortProps($props, $split = false) { + $vars = array(); + $imports = array(); + $other = array(); + $stack = array(); + + foreach ($props as $prop) { + switch ($prop[0]) { + case "comment": + $stack[] = $prop; + break; + case "assign": + $stack[] = $prop; + if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) { + $vars = array_merge($vars, $stack); + } else { + $other = array_merge($other, $stack); + } + $stack = array(); + break; + case "import": + $id = self::$nextImportId++; + $prop[] = $id; + $stack[] = $prop; + $imports = array_merge($imports, $stack); + $other[] = array("import_mixin", $id); + $stack = array(); + break; + default: + $stack[] = $prop; + $other = array_merge($other, $stack); + $stack = array(); + break; + } + } + $other = array_merge($other, $stack); + + if ($split) { + return array(array_merge($imports, $vars), $other); + } else { + return array_merge($imports, $vars, $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; + case "variable": + $parts[] = $this->compileValue($this->reduce($q)); + 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[] = trim($this->compileValue($this->reduce($value))); + } else { + $out[] = $s; + } + } + + return $out; + } + + protected function eq($left, $right) { + return $left == $right; + } + + 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)) { + $groupPassed = false; + foreach ($block->guards as $guardGroup) { + foreach ($guardGroup as $guard) { + $this->pushEnv(); + $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs); + + $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; + } + } + + if (empty($block->args)) { + 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 ($remainingArgs as $i => $arg) { + switch ($arg[0]) { + case "lit": + if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) { + return false; + } + break; + case "arg": + // no arg and no default value + if (!isset($orderedArgs[$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 >= count($orderedArgs); + } + } + + protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) { + $matches = null; + foreach ($blocks as $block) { + // 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; + } + } + + return $matches; + } + + // attempt to find blocks matched by path and args + 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; + + $name = $path[0]; + + if (isset($searchIn->children[$name])) { + $blocks = $searchIn->children[$name]; + if (count($path) == 1) { + $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 + return $matches; + } + } else { + $matches = array(); + foreach ($blocks as $subBlock) { + $subMatches = $this->findBlocks($subBlock, + array_slice($path, 1), $orderedArgs, $keywordArgs, $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, $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, $orderedValues, $keywordValues) { + $assignedValues = array(); + + $i = 0; + foreach ($args as $a) { + if ($a[0] == "arg") { + 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 { + $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++; + } + } + + // check for a rest + $last = end($args); + if ($last[0] == "rest") { + $rest = array_slice($orderedValues, count($args) - 1); + $this->set($last[1], $this->reduce(array("list", " ", $rest))); + } + + // 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 + 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; + + $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) { + $this->throwError("{$prop[1][0]} is undefined"); + } + + foreach ($mixins as $mixin) { + if ($mixin === $block && !$orderedArgs) { + continue; + } + + $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, $orderedArgs, $keywordArgs); + } + + $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) { + if (isset($import[1])) { + $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. + */ + public 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_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"); + } + + 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_isrem($value) { + return $this->toBool($value[0] == "number" && $value[2] == "rem"); + } + + 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); + } + + /** + * Given an url, decide whether to output a regular link or the base64-encoded contents of the file + * + * @param array $value either an argument list (two strings) or a single string + * @return string formatted url(), either as a link or base64-encoded + */ + protected function lib_data_uri($value) { + $mime = ($value[0] === 'list') ? $value[2][0][2] : null; + $url = ($value[0] === 'list') ? $value[2][1][2][0] : $value[2][0]; + + $fullpath = $this->findImport($url); + + if($fullpath && ($fsize = filesize($fullpath)) !== false) { + // IE8 can't handle data uris larger than 32KB + if($fsize/1024 < 32) { + if(is_null($mime)) { + if(class_exists('finfo')) { // php 5.3+ + $finfo = new finfo(FILEINFO_MIME); + $mime = explode('; ', $finfo->file($fullpath)); + $mime = $mime[0]; + } elseif(function_exists('mime_content_type')) { // PHP 5.2 + $mime = mime_content_type($fullpath); + } + } + + if(!is_null($mime)) // fallback if the mime type is still unknown + $url = sprintf('data:%s;base64,%s', $mime, base64_encode(file_get_contents($fullpath))); + } + } + + return 'url("'.$url.'")'; + } + + // 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]); + } + $this->throwError("unrecognised input"); + 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) { + if($arg[0] != "list") { + $value = $this->assertNumber($arg); + return array("number", round($value), $arg[2]); + } else { + $value = $this->assertNumber($arg[2][0]); + $precision = $this->assertNumber($arg[2][1]); + return array("number", round($value, $precision), $arg[2][0][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 + */ + public 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: 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]) < 2) + $this->throwError("mix expects (color1, color2, weight)"); + + 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); + + 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; + + $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 lib_contrast($args) { + $darkColor = array('color', 0, 0, 0); + $lightColor = array('color', 255, 255, 255); + $threshold = 0.43; + + if ( $args[0] == 'list' ) { + $inputColor = ( isset($args[2][0]) ) ? $this->assertColor($args[2][0]) : $lightColor; + $darkColor = ( isset($args[2][1]) ) ? $this->assertColor($args[2][1]) : $darkColor; + $lightColor = ( isset($args[2][2]) ) ? $this->assertColor($args[2][2]) : $lightColor; + $threshold = ( isset($args[2][3]) ) ? $this->assertNumber($args[2][3]) : $threshold; + } + else { + $inputColor = $this->assertColor($args); + } + + $inputColor = $this->coerceColor($inputColor); + $darkColor = $this->coerceColor($darkColor); + $lightColor = $this->coerceColor($lightColor); + + //Figure out which is actually light and dark! + if ( $this->lib_luma($darkColor) > $this->lib_luma($lightColor) ) { + $t = $lightColor; + $lightColor = $darkColor; + $darkColor = $t; + } + + $inputColor_alpha = $this->lib_alpha($inputColor); + if ( ( $this->lib_luma($inputColor) * $inputColor_alpha) < $threshold) { + return $lightColor; + } + return $darkColor; + } + + protected function lib_luma($color) { + $color = $this->coerceColor($color); + return (0.2126 * $color[0] / 255) + (0.7152 * $color[1] / 255) + (0.0722 * $color[2] / 255); + } + + + public function assertColor($value, $error = "expected color value") { + $color = $this->coerceColor($value); + if (is_null($color)) $this->throwError($error); + return $color; + } + + public function assertNumber($value, $error = "expecting number") { + if ($value[0] == "number") return $value[1]; + $this->throwError($error); + } + + public 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; + + $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[0] == '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 "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)) { + $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)); + $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_'.str_replace('-', '_', $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])) { + $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; + } + } + + // 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; + } + + public 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); + } + + 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]; + + $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) { + $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; + } + } + + $this->throwError("variable $name is undefined"); + } + + // inject array of unparsed strings into environment as variables + protected function injectVariables($args) { + $this->pushEnv(); + $parser = new 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->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 lessc_parser($this, $name); + $parser->writeComments = $this->preserveComments; + + return $parser; + } + + public function setFormatter($name) { + $this->formatterName = $name; + } + + protected function newFormatter() { + $className = "lessc_formatter_lessjs"; + if (!empty($this->formatterName)) { + if (!is_string($this->formatterName)) + return $this->formatterName; + $className = "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; + } + + public function addParsedFile($file) { + $this->allParsedFiles[realpath($file)] = filemtime($file); + } + + /** + * Uses the current value of $this->count to show line and line number + */ + public 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); + } + + static protected $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', + 'transparent' => '0,0,0,0', + '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 lessc_parser { + static protected $nextBlockId = 0; // used to uniquely identify blocks + + static protected $precedence = array( + '=<' => 0, + '>=' => 0, + '=' => 0, + '<' => 0, + '>' => 0, + + '+' => 1, + '-' => 1, + '*' => 2, + '/' => 2, + '%' => 2, + ); + + static protected $whitePattern; + static protected $commentMulti; + + static protected $commentSingle = "//"; + static protected $commentMultiLeft = "/*"; + static protected $commentMultiRight = "*/"; + + // regex string to match any of the operators + static protected $operatorString; + + // these properties will supress division unless it's inside parenthases + static protected $supressDivisionProps = + array('/border-radius$/i', '/^font$/i'); + + protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport"); + 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 + static protected $literalCache = array(); + + public function __construct($lessc, $sourceName = null) { + $this->eatWhiteDefault = true; + // reference to less needed for vPrefix, mPrefix, and parentSelector + $this->lessc = $lessc; + + $this->sourceName = $sourceName; // name used for error messages + + $this->writeComments = false; + + if (!self::$operatorString) { + self::$operatorString = + '('.implode('|', array_map(array('lessc', 'preg_quote'), + array_keys(self::$precedence))).')'; + + $commentSingle = lessc::preg_quote(self::$commentSingle); + $commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft); + $commentMultiRight = 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 + while (false !== $this->parseChunk()); + + if ($this->count != strlen($this->buffer)) + $this->throwError(); + + // TODO report where the block was opened + if ( !property_exists($this->env, 'parent') || !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 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 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, 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(); + + if ($this->whitespace()) { + return true; + } + + // 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('{', false)) { + $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->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->argumentDef($argv, $isVararg) || 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("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->lessc->vPrefix) + $tag[0] = $this->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 = 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 = 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->string($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->string($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) { + 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; + } elseif ($this->variable($variable)) { + $out = array('variable', $variable); + 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("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->string($str)) { + $content[] = $str; + continue; + } + + if ($tok == "@{" && $this->interpolation($inter)) { + $content[] = $inter; + continue; + } + + if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) { + 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 string(&$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]*?)(@\{|\\\\|' . + 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->openString("}", $interp, null, array("'", '"', ";")) && + $this->literal("}", false)) + { + $out = array("interpolate", $interp); + $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 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 ... + // 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) { + if ($this->literal("...")) { + $isVararg = true; + break; + } + + 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); + } + } + + + 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(')')) { + $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) { + $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(&$parts, &$hasExpression) { + // speed shortcut + if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") { + return false; + } + + $s = $this->seek(); + + $hasInterpolation = false; + + if ($this->literal("[", false)) { + $attrParts = array("["); + // keyword, string, operator + while (true) { + if ($this->literal("]", false)) { + $this->count--; + break; // get out early + } + + 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 space separated list of selectors + protected function tag(&$tag, $simple = false) { + if ($simple) + $chars = '^@,:;{}\][>\(\) "\''; + else + $chars = '^@,;{}["\''; + + $s = $this->seek(); + + $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)) { + $parts[] = $m[1]; + if ($simple) break; + + while ($this->tagBracket($parts, $hasExpression)); + continue; + } + + 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; + } + + if ($hasExpression) { + $tag = array("exp", array("string", "", $parts)); + } else { + $tag = trim(implode($parts)); + } + + $this->whitespace(); + 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->lessc->vPrefix, false) && + ($this->variable($sub) || $this->keyword($name))) + { + if (!empty($sub)) { + $name = array('variable', $sub); + } else { + $name = $this->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(';', false)) { + 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] = 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.'*?)'.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->seenComments[$this->count])) { + $this->append(array("comment", $m[1])); + $this->seenComments[$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].'.*?(?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 lessc_formatter_compressed extends lessc_formatter_classic { + public $disableSingle = true; + public $open = "{"; + public $selectorSeparator = ","; + public $assignSeparator = ":"; + public $break = ""; + public $compressColors = true; + + public function indentStr($n = 0) { + return ""; + } +} + +class lessc_formatter_lessjs extends lessc_formatter_classic { + public $disableSingle = true; + public $breakSelectors = true; + public $assignSeparator = ": "; + public $selectorSeparator = ","; +} + + diff --git a/vendor/leafo/lessphp/lessify b/vendor/leafo/lessphp/lessify new file mode 100644 index 000000000..becc53887 --- /dev/null +++ b/vendor/leafo/lessphp/lessify @@ -0,0 +1,23 @@ +#!/usr/bin/php +parse(); +} catch (exception $e) { + exit("Fatal error: ".$e->getMessage()."\n"); +} + + diff --git a/vendor/leafo/lessphp/lessify.inc.php b/vendor/leafo/lessphp/lessify.inc.php new file mode 100644 index 000000000..91c144230 --- /dev/null +++ b/vendor/leafo/lessphp/lessify.inc.php @@ -0,0 +1,447 @@ + + * + * WARNING: THIS DOES NOT WORK ANYMORE. NEEDS TO BE UPDATED FOR + * LATEST VERSION OF LESSPHP. + * + */ + +require "lessc.inc.php"; + +// +// check if the merge during mixin is overwriting values. should or should it not? +// + +// +// 1. split apart class tags +// + +class easyparse { + var $buffer; + var $count; + + function __construct($str) { + $this->count = 0; + $this->buffer = trim($str); + } + + function seek($where = null) { + if ($where === null) return $this->count; + else $this->count = $where; + return true; + } + + function preg_quote($what) { + return preg_quote($what, '/'); + } + + function match($regex, &$out, $eatWhitespace = true) { + $r = '/'.$regex.($eatWhitespace ? '\s*' : '').'/Ais'; + if (preg_match($r, $this->buffer, $out, null, $this->count)) { + $this->count += strlen($out[0]); + return true; + } + return false; + } + + function literal($what, $eatWhitespace = true) { + // this is here mainly prevent notice from { } string accessor + if ($this->count >= strlen($this->buffer)) return false; + + // shortcut on single letter + if (!$eatWhitespace and strlen($what) == 1) { + if ($this->buffer{$this->count} == $what) { + $this->count++; + return true; + } + else return false; + } + + return $this->match($this->preg_quote($what), $m, $eatWhitespace); + } + +} + +class tagparse extends easyparse { + static private $combinators = null; + static private $match_opts = null; + + function parse() { + if (empty(self::$combinators)) { + self::$combinators = '('.implode('|', array_map(array($this, 'preg_quote'), + array('+', '>', '~'))).')'; + self::$match_opts = '('.implode('|', array_map(array($this, 'preg_quote'), + array('=', '~=', '|=', '$=', '*='))).')'; + } + + // crush whitespace + $this->buffer = preg_replace('/\s+/', ' ', $this->buffer).' '; + + $tags = array(); + while ($this->tag($t)) $tags[] = $t; + + return $tags; + } + + static function compileString($string) { + list(, $delim, $str) = $string; + $str = str_replace($delim, "\\".$delim, $str); + $str = str_replace("\n", "\\\n", $str); + return $delim.$str.$delim; + } + + static function compilePaths($paths) { + return implode(', ', array_map(array('self', 'compilePath'), $paths)); + } + + // array of tags + static function compilePath($path) { + return implode(' ', array_map(array('self', 'compileTag'), $path)); + } + + + static function compileTag($tag) { + ob_start(); + if (isset($tag['comb'])) echo $tag['comb']." "; + if (isset($tag['front'])) echo $tag['front']; + if (isset($tag['attr'])) { + echo '['.$tag['attr']; + if (isset($tag['op'])) { + echo $tag['op'].$tag['op_value']; + } + echo ']'; + } + return ob_get_clean(); + } + + function string(&$out) { + $s = $this->seek(); + + if ($this->literal('"')) { + $delim = '"'; + } elseif ($this->literal("'")) { + $delim = "'"; + } else { + return false; + } + + while (true) { + // step through letters looking for either end or escape + $buff = ""; + $escapeNext = false; + $finished = false; + for ($i = $this->count; $i < strlen($this->buffer); $i++) { + $char = $this->buffer[$i]; + switch ($char) { + case $delim: + if ($escapeNext) { + $buff .= $char; + $escapeNext = false; + break; + } + $finished = true; + break 2; + case "\\": + if ($escapeNext) { + $buff .= $char; + $escapeNext = false; + } else { + $escapeNext = true; + } + break; + case "\n": + if (!$escapeNext) { + break 3; + } + + $buff .= $char; + $escapeNext = false; + break; + default: + if ($escapeNext) { + $buff .= "\\"; + $escapeNext = false; + } + $buff .= $char; + } + } + if (!$finished) break; + $out = array('string', $delim, $buff); + $this->seek($i+1); + return true; + } + + $this->seek($s); + return false; + } + + function tag(&$out) { + $s = $this->seek(); + $tag = array(); + if ($this->combinator($op)) $tag['comb'] = $op; + + if (!$this->match('(.*?)( |$|\[|'.self::$combinators.')', $match)) { + $this->seek($s); + return false; + } + + if (!empty($match[3])) { + // give back combinator + $this->count-=strlen($match[3]); + } + + if (!empty($match[1])) $tag['front'] = $match[1]; + + if ($match[2] == '[') { + if ($this->ident($i)) { + $tag['attr'] = $i; + + if ($this->match(self::$match_opts, $m) && $this->value($v)) { + $tag['op'] = $m[1]; + $tag['op_value'] = $v; + } + + if ($this->literal(']')) { + $out = $tag; + return true; + } + } + } elseif (isset($tag['front'])) { + $out = $tag; + return true; + } + + $this->seek($s); + return false; + } + + function ident(&$out) { + // [-]?{nmstart}{nmchar}* + // nmstart: [_a-z]|{nonascii}|{escape} + // nmchar: [_a-z0-9-]|{nonascii}|{escape} + if ($this->match('(-?[_a-z][_\w]*)', $m)) { + $out = $m[1]; + return true; + } + return false; + } + + function value(&$out) { + if ($this->string($str)) { + $out = $this->compileString($str); + return true; + } elseif ($this->ident($id)) { + $out = $id; + return true; + } + return false; + } + + + function combinator(&$op) { + if ($this->match(self::$combinators, $m)) { + $op = $m[1]; + return true; + } + return false; + } +} + +class nodecounter { + var $count = 0; + var $children = array(); + + var $name; + var $child_blocks; + var $the_block; + + function __construct($name) { + $this->name = $name; + } + + function dump($stack = null) { + if (is_null($stack)) $stack = array(); + $stack[] = $this->getName(); + echo implode(' -> ', $stack)." ($this->count)\n"; + foreach ($this->children as $child) { + $child->dump($stack); + } + } + + static function compileProperties($c, $block) { + foreach($block as $name => $value) { + if ($c->isProperty($name, $value)) { + echo $c->compileProperty($name, $value)."\n"; + } + } + } + + function compile($c, $path = null) { + if (is_null($path)) $path = array(); + $path[] = $this->name; + + $isVisible = !is_null($this->the_block) || !is_null($this->child_blocks); + + if ($isVisible) { + echo $c->indent(implode(' ', $path).' {'); + $c->indentLevel++; + $path = array(); + + if ($this->the_block) { + $this->compileProperties($c, $this->the_block); + } + + if ($this->child_blocks) { + foreach ($this->child_blocks as $block) { + echo $c->indent(tagparse::compilePaths($block['__tags']).' {'); + $c->indentLevel++; + $this->compileProperties($c, $block); + $c->indentLevel--; + echo $c->indent('}'); + } + } + } + + // compile child nodes + foreach($this->children as $node) { + $node->compile($c, $path); + } + + if ($isVisible) { + $c->indentLevel--; + echo $c->indent('}'); + } + + } + + function getName() { + if (is_null($this->name)) return "[root]"; + else return $this->name; + } + + function getNode($name) { + if (!isset($this->children[$name])) { + $this->children[$name] = new nodecounter($name); + } + + return $this->children[$name]; + } + + function findNode($path) { + $current = $this; + for ($i = 0; $i < count($path); $i++) { + $t = tagparse::compileTag($path[$i]); + $current = $current->getNode($t); + } + + return $current; + } + + function addBlock($path, $block) { + $node = $this->findNode($path); + if (!is_null($node->the_block)) throw new exception("can this happen?"); + + unset($block['__tags']); + $node->the_block = $block; + } + + function addToNode($path, $block) { + $node = $this->findNode($path); + $node->child_blocks[] = $block; + } +} + +/** + * create a less file from a css file by combining blocks where appropriate + */ +class lessify extends lessc { + public function dump() { + print_r($this->env); + } + + public function parse($str = null) { + $this->prepareParser($str ? $str : $this->buffer); + while (false !== $this->parseChunk()); + + $root = new nodecounter(null); + + // attempt to preserve some of the block order + $order = array(); + + $visitedTags = array(); + foreach (end($this->env) as $name => $block) { + if (!$this->isBlock($name, $block)) continue; + if (isset($visitedTags[$name])) continue; + + foreach ($block['__tags'] as $t) { + $visitedTags[$t] = true; + } + + // skip those with more than 1 + if (count($block['__tags']) == 1) { + $p = new tagparse(end($block['__tags'])); + $path = $p->parse(); + $root->addBlock($path, $block); + $order[] = array('compressed', $path, $block); + continue; + } else { + $common = null; + $paths = array(); + foreach ($block['__tags'] as $rawtag) { + $p = new tagparse($rawtag); + $paths[] = $path = $p->parse(); + if (is_null($common)) $common = $path; + else { + $new_common = array(); + foreach ($path as $tag) { + $head = array_shift($common); + if ($tag == $head) { + $new_common[] = $head; + } else break; + } + $common = $new_common; + if (empty($common)) { + // nothing in common + break; + } + } + } + + if (!empty($common)) { + $new_paths = array(); + foreach ($paths as $p) $new_paths[] = array_slice($p, count($common)); + $block['__tags'] = $new_paths; + $root->addToNode($common, $block); + $order[] = array('compressed', $common, $block); + continue; + } + + } + + $order[] = array('none', $block['__tags'], $block); + } + + + $compressed = $root->children; + foreach ($order as $item) { + list($type, $tags, $block) = $item; + if ($type == 'compressed') { + $top = tagparse::compileTag(reset($tags)); + if (isset($compressed[$top])) { + $compressed[$top]->compile($this); + unset($compressed[$top]); + } + } else { + echo $this->indent(implode(', ', $tags).' {'); + $this->indentLevel++; + nodecounter::compileProperties($this, $block); + $this->indentLevel--; + echo $this->indent('}'); + } + } + } +} diff --git a/vendor/leafo/lessphp/package.sh b/vendor/leafo/lessphp/package.sh new file mode 100644 index 000000000..f0888f127 --- /dev/null +++ b/vendor/leafo/lessphp/package.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +# creates tar.gz for current version + +VERSION=`./plessc -v | sed -n 's/^v\(.*\)$/\1/p'` +OUT_DIR="tmp/lessphp" +TMP=`dirname $OUT_DIR` + +mkdir -p $OUT_DIR +tar -c `git ls-files` | tar -C $OUT_DIR -x + +rm $OUT_DIR/.gitignore +rm $OUT_DIR/package.sh +rm $OUT_DIR/lessify +rm $OUT_DIR/lessify.inc.php + +OUT_NAME="lessphp-$VERSION.tar.gz" +tar -czf $OUT_NAME -C $TMP lessphp/ +echo "Wrote $OUT_NAME" + +rm -r $TMP + + +echo +echo "Don't forget to" +echo "* Update the version in lessc.inc.php (two places)" +echo "* Update the version in the README.md" +echo "* Update the version in docs.md (two places)" +echo "* Update the version in LICENSE" +echo "* Update @current_version in site.moon" +echo "* Add entry to feed.moon for changelog" +echo "* Update the -New- area on homepage with date and features" +echo + + diff --git a/vendor/leafo/lessphp/plessc b/vendor/leafo/lessphp/plessc new file mode 100644 index 000000000..1871dc77b --- /dev/null +++ b/vendor/leafo/lessphp/plessc @@ -0,0 +1,250 @@ +#!/usr/bin/env php +, 2013 + +$exe = array_shift($argv); // remove filename + +$HELP = << 0 && preg_match('/^-([-hvrwncXT]$|[f]=)/', $argv[0])) { + array_shift($argv); +} + +function has() { + global $opts; + foreach (func_get_args() as $arg) { + if (isset($opts[$arg])) return true; + } + return false; +} + +if (has("h", "help")) { + exit($HELP); +} + +error_reporting(E_ALL); +$path = realpath(dirname(__FILE__)).'/'; + +require $path."lessc.inc.php"; + +$VERSION = lessc::$VERSION; + +$fa = "Fatal Error: "; +function err($msg) { + fwrite(STDERR, $msg."\n"); +} + +if (php_sapi_name() != "cli") { + err($fa.$argv[0]." must be run in the command line."); + exit(1); +} + +function make_less($fname = null) { + global $opts; + $l = new lessc($fname); + + if (has("f")) { + $format = $opts["f"]; + if ($format != "default") $l->setFormatter($format); + } + + if (has("c")) { + $l->setPreserveComments(true); + } + + return $l; +} + +function process($data, $import = null) { + global $fa; + + $l = make_less(); + if ($import) $l->importDir = $import; + + try { + echo $l->parse($data); + exit(0); + } catch (exception $ex) { + err($fa."\n".str_repeat('=', 20)."\n". + $ex->getMessage()); + exit(1); + } +} + +if (has("v")) { + exit($VERSION."\n"); +} + +if (has("r")) { + if (!empty($argv)) { + $data = $argv[0]; + } else { + $data = ""; + while (!feof(STDIN)) { + $data .= fread(STDIN, 8192); + } + } + exit(process($data)); +} + +if (has("w")) { + // need two files + if (!is_file($in = array_shift($argv)) || + null == $out = array_shift($argv)) + { + err($fa.$exe." -w infile outfile"); + exit(1); + } + + echo "Watching ".$in. + (has("n") ? ' with notifications' : ''). + ", press Ctrl + c to exit.\n"; + + $cache = $in; + $last_action = 0; + while (true) { + clearstatcache(); + + // check if anything has changed since last fail + $updated = false; + if (is_array($cache)) { + foreach ($cache['files'] as $fname=>$_) { + if (filemtime($fname) > $last_action) { + $updated = true; + break; + } + } + } else $updated = true; + + // try to compile it + if ($updated) { + $last_action = time(); + + try { + $cache = lessc::cexecute($cache); + echo "Writing updated file: ".$out."\n"; + if (!file_put_contents($out, $cache['compiled'])) { + err($fa."Could not write to file ".$out); + exit(1); + } + } catch (exception $ex) { + echo "\nFatal Error:\n".str_repeat('=', 20)."\n". + $ex->getMessage()."\n\n"; + + if (has("n")) { + `notify-send -u critical "compile failed" "{$ex->getMessage()}"`; + } + } + } + + sleep(1); + } + exit(0); +} + +if (!$fname = array_shift($argv)) { + echo $HELP; + exit(1); +} + +function dumpValue($node, $depth = 0) { + if (is_object($node)) { + $indent = str_repeat(" ", $depth); + $out = array(); + foreach ($node->props as $prop) { + $out[] = $indent . dumpValue($prop, $depth + 1); + } + $out = implode("\n", $out); + if (!empty($node->tags)) { + $out = "+ ".implode(", ", $node->tags)."\n".$out; + } + return $out; + } elseif (is_array($node)) { + if (empty($node)) return "[]"; + $type = $node[0]; + if ($type == "block") + return dumpValue($node[1], $depth); + + $out = array(); + foreach ($node as $value) { + $out[] = dumpValue($value, $depth); + } + return "{ ".implode(", ", $out)." }"; + } else { + if (is_string($node) && preg_match("/[\s,]/", $node)) { + return '"'.$node.'"'; + } + return $node; // normal value + } +} + + +function stripValue($o, $toStrip) { + if (is_array($o) || is_object($o)) { + $isObject = is_object($o); + $o = (array)$o; + foreach ($toStrip as $removeKey) { + if (!empty($o[$removeKey])) { + $o[$removeKey] = "*stripped*"; + } + } + + foreach ($o as $k => $v) { + $o[$k] = stripValue($v, $toStrip); + } + + if ($isObject) { + $o = (object)$o; + } + } + + return $o; +} + +function dumpWithoutParent($o, $alsoStrip=array()) { + $toStrip = array_merge(array("parent"), $alsoStrip); + print_r(stripValue($o, $toStrip)); +} + +try { + $less = make_less($fname); + if (has("T", "X")) { + $parser = new lessc_parser($less, $fname); + $tree = $parser->parse(file_get_contents($fname)); + if (has("X")) + $out = print_r($tree, 1); + else + $out = dumpValue($tree)."\n"; + } else { + $out = $less->parse(); + } + + if (!$fout = array_shift($argv)) { + echo $out; + } else { + file_put_contents($fout, $out); + } + +} catch (exception $ex) { + err($fa.$ex->getMessage()); + exit(1); +} + +?> diff --git a/vendor/leafo/lessphp/tests/ApiTest.php b/vendor/leafo/lessphp/tests/ApiTest.php new file mode 100644 index 000000000..fe1bbffa1 --- /dev/null +++ b/vendor/leafo/lessphp/tests/ApiTest.php @@ -0,0 +1,196 @@ +less = new lessc(); + $this->less->importDir = array(__DIR__ . "/inputs/test-imports"); + } + + public function testPreserveComments() { + $input = <<assertEquals($this->compile($input), trim($outputWithoutComments)); + $this->less->setPreserveComments(true); + $this->assertEquals($this->compile($input), trim($outputWithComments)); + } + + public function testOldInterface() { + $this->less = new lessc(__DIR__ . "/inputs/hi.less"); + $out = $this->less->parse(array("hello" => "10px")); + $this->assertEquals(trim($out), trim(' +div:before { + content: "hi!"; +}')); + + } + + public function testInjectVars() { + $out = $this->less->parse(".magic { color: @color; width: @base - 200; }", + array( + 'color' => 'red', + 'base' => '960px' + )); + + $this->assertEquals(trim($out), trim(" +.magic { + color: red; + width: 760px; +}")); + + } + + public function testDisableImport() { + $this->less->importDisabled = true; + $this->assertEquals( + "/* import disabled */", + $this->compile("@import 'file3';")); + } + + public function testUserFunction() { + $this->less->registerFunction("add-two", function($list) { + list($a, $b) = $list[2]; + return $a[1] + $b[1]; + }); + + $this->assertEquals( + $this->compile("result: add-two(10, 20);"), + "result: 30;"); + + return $this->less; + } + + /** + * @depends testUserFunction + */ + public function testUnregisterFunction($less) { + $less->unregisterFunction("add-two"); + + $this->assertEquals( + $this->compile("result: add-two(10, 20);"), + "result: add-two(10,20);"); + } + + + + public function testFormatters() { + $src = " + div, pre { + color: blue; + span, .big, hello.world { + height: 20px; + color:#ffffff + #000; + } + }"; + + $this->less->setFormatter("compressed"); + $this->assertEquals( + $this->compile($src), "div,pre{color:blue;}div span,div .big,div hello.world,pre span,pre .big,pre hello.world{height:20px;color:#fff;}"); + + // TODO: fix the output order of tags + $this->less->setFormatter("lessjs"); + $this->assertEquals( + $this->compile($src), +"div, +pre { + color: blue; +} +div span, +div .big, +div hello.world, +pre span, +pre .big, +pre hello.world { + height: 20px; + color: #ffffff; +}"); + + $this->less->setFormatter("classic"); + $this->assertEquals( + $this->compile($src), +trim("div, pre { color:blue; } +div span, div .big, div hello.world, pre span, pre .big, pre hello.world { + height:20px; + color:#ffffff; +} +")); + + } + + public function compile($str) { + return trim($this->less->parse($str)); + } + +} diff --git a/vendor/leafo/lessphp/tests/ErrorHandlingTest.php b/vendor/leafo/lessphp/tests/ErrorHandlingTest.php new file mode 100644 index 000000000..de02f0651 --- /dev/null +++ b/vendor/leafo/lessphp/tests/ErrorHandlingTest.php @@ -0,0 +1,81 @@ +less = new lessc(); + } + + public function compile() { + $source = join("\n", func_get_args()); + return $this->less->compile($source); + } + + /** + * @expectedException Exception + * @expectedExceptionMessage .parametric-mixin is undefined + */ + public function testRequiredParametersMissing() { + $this->compile( + '.parametric-mixin (@a, @b) { a: @a; b: @b; }', + '.selector { .parametric-mixin(12px); }' + ); + } + + /** + * @expectedException Exception + * @expectedExceptionMessage .parametric-mixin is undefined + */ + public function testTooManyParameters() { + $this->compile( + '.parametric-mixin (@a, @b) { a: @a; b: @b; }', + '.selector { .parametric-mixin(12px, 13px, 14px); }' + ); + } + + /** + * @expectedException Exception + * @expectedExceptionMessage unrecognised input + */ + public function testRequiredArgumentsMissing() { + $this->compile('.selector { rule: e(); }'); + } + + /** + * @expectedException Exception + * @expectedExceptionMessage variable @missing is undefined + */ + public function testVariableMissing() { + $this->compile('.selector { rule: @missing; }'); + } + + /** + * @expectedException Exception + * @expectedExceptionMessage .missing-mixin is undefined + */ + public function testMixinMissing() { + $this->compile('.selector { .missing-mixin; }'); + } + + /** + * @expectedException Exception + * @expectedExceptionMessage .flipped is undefined + */ + public function testGuardUnmatchedValue() { + $this->compile( + '.flipped(@x) when (@x =< 10) { rule: value; }', + '.selector { .flipped(12); }' + ); + } + + /** + * @expectedException Exception + * @expectedExceptionMessage .colors-only is undefined + */ + public function testGuardUnmatchedType() { + $this->compile( + '.colors-only(@x) when (iscolor(@x)) { rule: value; }', + '.selector { .colors-only("string value"); }' + ); + } +} diff --git a/vendor/leafo/lessphp/tests/InputTest.php b/vendor/leafo/lessphp/tests/InputTest.php new file mode 100644 index 000000000..32db95bc7 --- /dev/null +++ b/vendor/leafo/lessphp/tests/InputTest.php @@ -0,0 +1,89 @@ + "outputs", + "inputs_lessjs" => "outputs_lessjs", + ); + + public function setUp() { + $this->less = new lessc(); + $this->less->importDir = array_map(function($path) { + return __DIR__ . "/" . $path; + }, self::$importDirs); + } + + /** + * @dataProvider fileNameProvider + */ + public function testInputFile($inFname) { + if ($pattern = getenv("BUILD")) { + return $this->buildInput($inFname); + } + + $outFname = self::outputNameFor($inFname); + + if (!is_readable($outFname)) { + $this->fail("$outFname is missing, ". + "consider building tests with BUILD=true"); + } + + $input = file_get_contents($inFname); + $output = file_get_contents($outFname); + + $this->assertEquals($output, $this->less->parse($input)); + } + + public function fileNameProvider() { + return array_map(function($a) { return array($a); }, + self::findInputNames()); + } + + // only run when env is set + public function buildInput($inFname) { + $css = $this->less->parse(file_get_contents($inFname)); + file_put_contents(self::outputNameFor($inFname), $css); + } + + static public function findInputNames($pattern="*.less") { + $files = array(); + foreach (self::$testDirs as $inputDir => $outputDir) { + $files = array_merge($files, glob(__DIR__ . "/" . $inputDir . "/" . $pattern)); + } + + return array_filter($files, "is_file"); + } + + static public function outputNameFor($input) { + $front = _quote(__DIR__ . "/"); + $out = preg_replace("/^$front/", "", $input); + + foreach (self::$testDirs as $inputDir => $outputDir) { + $in = _quote($inputDir . "/"); + $rewritten = preg_replace("/$in/", $outputDir . "/", $out); + if ($rewritten != $out) { + $out = $rewritten; + break; + } + } + + $out = preg_replace("/.less$/", ".css", $out); + + return __DIR__ . "/" . $out; + } +} + diff --git a/vendor/leafo/lessphp/tests/README.md b/vendor/leafo/lessphp/tests/README.md new file mode 100644 index 000000000..85a75c053 --- /dev/null +++ b/vendor/leafo/lessphp/tests/README.md @@ -0,0 +1,24 @@ +lessphp uses [phpunit](https://github.com/sebastianbergmann/phpunit/) for its tests + +* `InputTest.php` iterates through all the less files in `inputs/`, compiles + them, then compares the result with the respective file in `outputs/`. + +* `ApiTest.php` tests the behavior of lessphp's public API methods. + +* `ErrorHandlingTest.php` tests that lessphp throws appropriate errors when + given invalid LESS as input. + +From the root you can run `make` to run all the tests. + +## lessjs tests + +Tests found in `inputs_lessjs` are extracted directly from +[less.js](https://github.com/less/less.js). The following license applies to +those tests: https://github.com/less/less.js/blob/master/LICENSE + +## bootstrap.sh + +Clones twitter bootsrap, compiles it with lessc and lessphp, cleans up results +with sort.php, and outputs diff. To run it, you need to have git and lessc +installed. + diff --git a/vendor/leafo/lessphp/tests/bootstrap.sh b/vendor/leafo/lessphp/tests/bootstrap.sh new file mode 100644 index 000000000..18a90e879 --- /dev/null +++ b/vendor/leafo/lessphp/tests/bootstrap.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +echo "This script clones Twitter Bootstrap, compiles it with lessc and lessphp," +echo "cleans up results with sort.php, and outputs diff. To run it, you need to" +echo "have git and lessc installed." +echo "" + +if [ -z "$input" ]; then + input="bootstrap/less/bootstrap.less" +fi +dest=$(basename "$input") +dest="${dest%.*}" + +if [ -z "$@" ]; then + diff_tool="diff -b -u -t -B" +else + diff_tool=$@ +fi + +mkdir -p tmp + +if [ ! -d 'bootstrap/' ]; then + echo ">> Cloning bootstrap to bootstrap/" + git clone https://github.com/twbs/bootstrap +fi + +echo ">> lessc compilation ($input)" +lessc "$input" "tmp/$dest.lessc.css" + +echo ">> lessphp compilation ($input)" +../plessc "$input" "tmp/$dest.lessphp.css" +echo ">> Cleanup and convert" + +php sort.php "tmp/$dest.lessc.css" > "tmp/$dest.lessc.clean.css" +php sort.php "tmp/$dest.lessphp.css" > "tmp/$dest.lessphp.clean.css" + +echo ">> Doing diff" +$diff_tool "tmp/$dest.lessc.clean.css" "tmp/$dest.lessphp.clean.css" diff --git a/vendor/leafo/lessphp/tests/inputs/accessors.less.disable b/vendor/leafo/lessphp/tests/inputs/accessors.less.disable new file mode 100644 index 000000000..2990faab3 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/accessors.less.disable @@ -0,0 +1,36 @@ +/* accessors */ + +#defaults { + @width: 960px; + @color: black; + .something { + @space: 10px; + @hello { + color: green; + } + } +} + +.article { color: #294366; } + +.comment { + width: #defaults[@width]; + color: .article['color']; + padding: #defaults > .something[@space]; +} + +.wow { + height: .comment['width']; + background-color: .comment['color']; + color: #defaults > .something > @hello['color']; + + padding: #defaults > non-existant['padding']; + margin: #defaults > .something['non-existant']; +} + +.mix { + #defaults; + font-size: .something[@space]; +} + + diff --git a/vendor/leafo/lessphp/tests/inputs/arity.less b/vendor/leafo/lessphp/tests/inputs/arity.less new file mode 100644 index 000000000..9998fd4a6 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/arity.less @@ -0,0 +1,77 @@ + +// simple arity + +.hello(@a) { + hello: one; +} + +.hello(@a, @b) { + hello: two; +} + +.hello(@a, @b, @c) { + hello: three; +} + + +.world(@a, @b, @c) { + world: three; +} + +.world(@a, @b) { + world: two; +} + +.world(@a) { + world: one; +} + +.one { + .hello(1); + .world(1); +} + +.two { + .hello(1, 1); + .world(1, 1); +} + +.three { + .hello(1, 1, 1); + .world(1, 1, 1); +} + + +// arity with default values + +.foo(@a, @b: cool) { + foo: two @b; +} + +.foo(@a, @b: cool, @c: yeah) { + foo: three @b @c; +} + + +.baz(@a, @b, @c: yeah) { + baz: three @c; +} + +.baz(@a, @b: cool) { + baz: two @b; +} + + +.multi-foo { + .foo(1); + .foo(1, 1); + .foo(1,1,1); +} + +.multi-baz { + .baz(1); + .baz(1, 1); + .baz(1,1,1); +} + + diff --git a/vendor/leafo/lessphp/tests/inputs/attributes.less b/vendor/leafo/lessphp/tests/inputs/attributes.less new file mode 100644 index 000000000..7ede4fc4e --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/attributes.less @@ -0,0 +1,41 @@ +* { color: blue; } +E { color: blue; } +E[foo] { color: blue; } +[foo] { color: blue; } +[foo] .helloWorld { color: blue; } +[foo].helloWorld { color: blue; } +E[foo="barbar"] { color: blue; } +E[foo~="hello#$@%@$#^"] { color: blue; } +E[foo^="color: green;"] { color: blue; } +E[foo$="239023"] { color: blue; } +E[foo*="29302"] { color: blue; } +E[foo|="239032"] { color: blue; } +E:root { color: blue; } + +E:nth-child(odd) { color: blue; } +E:nth-child(2n+1) { color: blue; } +E:nth-child(5) { color: blue; } +E:nth-last-child(-n+2) { color: blue; } +E:nth-of-type(2n) { color: blue; } +E:nth-last-of-type(n) { color: blue; } + +E:first-child { color: blue; } +E:last-child { color: blue; } +E:first-of-type { color: blue; } +E:last-of-type { color: blue; } +E:only-child { color: blue; } +E:only-of-type { color: blue; } +E:empty { color: blue; } + +E:lang(en) { color: blue; } +E::first-line { color: blue; } +E::before { color: blue; } + +E#id { color: blue; } +E:not(:link) { color: blue; } + +E F { color: blue; } +E > F { color: blue; } +E + F { color: blue; } +E ~ F { color: blue; } + diff --git a/vendor/leafo/lessphp/tests/inputs/builtins.less b/vendor/leafo/lessphp/tests/inputs/builtins.less new file mode 100644 index 000000000..f9301e048 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/builtins.less @@ -0,0 +1,96 @@ +// builtin + +@something: "hello world"; +@color: #112233; +@color2: rgba(44,55,66, .6); + +body { + color: @something; + + @num: 7 / 6; + num-basic: @num + 4; + num-floor: floor(@num) + 4px; + num-ceil: ceil(@num) + 4px; + + @num2: 2 / 3; + num2: @num2; + num2-round: round(@num2); + num2-floor: floor(@num2); + num2-ceil: ceil(@num2); + + round-lit: round(10px / 3); + + rgba1: rgbahex(@color); + rgba2: rgbahex(@color2); + argb: argb(@color2); +} + + +format { + @r: 32; + format: %("rgb(%d, %d, %d)", @r, 128, 64); + format-string: %("hello %s", "world"); + format-multiple: %("hello %s %d", "earth", 2); + format-url-encode: %('red is %A', #ff0000); + eformat: e(%("rgb(%d, %d, %d)", @r, 128, 64)); +} + + +#functions { + str1: isstring("hello"); + str2: isstring(one, two); + + num1: isnumber(2323px); + num2: isnumber(2323); + num3: isnumber(4/5); + num4: isnumber("hello"); + + col1: iscolor(red); + col2: iscolor(hello); + col3: iscolor(rgba(0,0,0,0.3)); + col4: iscolor(#fff); + + key1: iskeyword(hello); + key2: iskeyword(3D); + + px1: ispixel(10px); + px2: ispixel(10); + + per1: ispercentage(10%); + per2: ispercentage(10); + + em1: isem(10em); + em2: isem(10); + + ex1: extract(1 2 3 4, 2); + ex2: extract(1 2, 1); + ex3: extract(1, 1); + + @list: 1,2,3,4; + + ex4: extract(@list, 2); + + pow: pow(2,4); + pi: pi(); + mod: mod(14,10); + + tan: tan(1); + cos: cos(1); + sin: sin(1); + + atan: atan(1); + acos: acos(1); + asin: asin(1); + + sqrt: sqrt(8); +} + + +#unit { + @unit: "em"; + unit-lit: unit(10px); + unit-arg: unit(10px, "s"); + unit-arg2: unit(10px, @unit); + unit-math: unit(0.07407s) * 100%; +} + diff --git a/vendor/leafo/lessphp/tests/inputs/colors.less b/vendor/leafo/lessphp/tests/inputs/colors.less new file mode 100644 index 000000000..7fd47a3c1 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/colors.less @@ -0,0 +1,154 @@ + +body { + color:rgb(red(#f00), red(#0F0), red(#00f)); + color:rgb(red(#f00), green(#0F0), blue(#00f)); + color:rgb(red(#0ff), green(#f0f), blue(#ff0)); + + color: hsl(34, 50%, 40%); + color: hsla(34, 50%, 40%, 0.3); + + lighten1: lighten(#efefef, 10%); + lighten2: lighten(rgb(23, 53, 231), 22%); + lighten3: lighten(rgba(212, 103, 88, 0.5), 10%); + + darken1: darken(#efefef, 10%); + darken2: darken(rgb(23, 53, 231), 22%); + darken3: darken(rgba(23, 53, 231, 0.5), 10%); + + saturate1: saturate(#efefef, 10%); + saturate2: saturate(rgb(23, 53, 231), 22%); + saturate3: saturate(rgba(23, 53, 231, 0.5), 10%); + + desaturate1: desaturate(#efefef, 10%); + desaturate2: desaturate(rgb(23, 53, 231), 22%); + desaturate3: desaturate(rgba(23, 53, 231, 0.5), 10%); + + spin1: spin(#efefef, 12); + spin2: spin(rgb(23, 53, 231), 15); + spin3: spin(rgba(23, 53, 231, 0.5), 19); + + spin2: spin(#efefef, -12); + spin3: spin(rgb(23, 53, 231), -15); + spin4: spin(rgba(23, 53, 231, 0.5), -19); + + one1: fadein(#abcdef, 10%); + one2: fadeout(#abcdef, -10%); + + two1: fadeout(#029f23, 10%); + two2: fadein(#029f23, -10%); + + + three1: fadein(rgba(1,2,3, 0.5), 10%); + three2: fadeout(rgba(1,2,3, 0.5), -10%); + + four1: fadeout(rgba(1,2,3, 0), 10%); + four2: fadein(rgba(1,2,3, 0), -10%); + + hue: hue(rgb(34,20,40)); + sat: saturation(rgb(34,20,40)); + lit: lightness(rgb(34,20,40)); + + @old: #34fa03; + @new: hsl(hue(@old), 45%, 90%); + what: @new; + + zero1: saturate(#123456, -100%); + zero2: saturate(#123456, 100%); + zero3: saturate(#000000, 100%); + zero4: saturate(#ffffff, 100%); + + zero5: lighten(#123456, -100%); + zero6: lighten(#123456, 100%); + zero7: lighten(#000000, 100%); + zero8: lighten(#ffffff, 100%); + + zero9: spin(#123456, -100); + zeroa: spin(#123456, 100); + zerob: spin(#000000, 100); + zeroc: spin(#ffffff, 100); +} + + +alpha { + // g: alpha(red); + g1: alpha(rgba(0,0,0,0)); + g2: alpha(rgb(155,55,0)); +} + +fade { + f1: fade(red, 50%); + f2: fade(#fff, 20%); + f3: fade(rgba(34,23,64,0.4), 50%); +} + +@a: rgb(255,255,255); +@b: rgb(0,0,0); + +.mix { + color1: mix(@a, @b, 50%); + color2: mix(@a, @b); + color3: mix(rgba(5,3,1,0.3), rgba(6,3,2, 0.8), 50%); +} + +.contrast { + color1: contrast(#000, red, blue); + color2: contrast(#fff, red, blue); +} + +.percent { + per: percentage(0.5); +} + +// color keywords + +.colorz { + color1: whitesmoke - 10; + color2: spin(red, 34); + color3: spin(blah); +} + + + +// purposfuly whacky to match less.js + +@color: #fcf8e3; + +body { + start: @color; + spin: spin(@color, -10); // #fcf4e3 + chained: darken(spin(@color, -10), 3%); // gives #fbeed5, should be #fbefd5 + direct: darken(#fcf4e3, 3%); // #fbefd5 +} + +// spin around +pre { + @errorBackground: #f2dede; + spin: spin(@errorBackground, -10); +} + +dd { + @white: #fff; + background-color: mix(@white, darken(@white, 10%), 60%); +} + +// math + +.ops { + c1: red * 0.3; + c2: green / 2; + c3: purple % 7; + c4: 4 * salmon; + c5: 1 + salmon; + + c6: 132 / red; + c7: 132 - red; + c8: 132- red; +} + +.transparent { + r: red(transparent); + g: green(transparent); + b: blue(transparent); + a: alpha(transparent); +} + diff --git a/vendor/leafo/lessphp/tests/inputs/compile_on_mixin.less b/vendor/leafo/lessphp/tests/inputs/compile_on_mixin.less new file mode 100644 index 000000000..79d628f49 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/compile_on_mixin.less @@ -0,0 +1,39 @@ + +@mixin { + height: 22px; + ul { + height: 20px; + li { + @color: red; + height: 10px; + div span, link { + margin: 10px; + color: @color; + } + } + + div, p { + border: 1px; + &.hello { + color: green; + } + + :what { + color: blue; + } + } + + + a { + b { + color: blue; + } + } + } +} + + + +body { + @mixin; +} diff --git a/vendor/leafo/lessphp/tests/inputs/data-uri.less b/vendor/leafo/lessphp/tests/inputs/data-uri.less new file mode 100644 index 000000000..0cfa941a8 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/data-uri.less @@ -0,0 +1,7 @@ +.small { + background: data-uri("../hi.less"); +} + +.large { + background: data-uri('../../../lessc.inc.php'); +} diff --git a/vendor/leafo/lessphp/tests/inputs/directives.less b/vendor/leafo/lessphp/tests/inputs/directives.less new file mode 100644 index 000000000..1b3a9b58a --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/directives.less @@ -0,0 +1,28 @@ + +@hello: "utf-8"; +@charset @hello; + +@-moz-document url-prefix(){ + div { + color: red; + } +} + +@page :left { margin-left: 4cm; } +@page :right { margin-left: 3cm; } +@page { margin: 2cm } + +@-ms-viewport { + width: device-width; +} +@-moz-viewport { + width: device-width; +} +@-o-viewport { + width: device-width; +} +@viewport { + width: device-width; +} + + diff --git a/vendor/leafo/lessphp/tests/inputs/escape.less b/vendor/leafo/lessphp/tests/inputs/escape.less new file mode 100644 index 000000000..5c15e7862 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/escape.less @@ -0,0 +1,18 @@ + +body { + @hello: "world"; + e1: e("this is simple"); + e2: e("this is simple", "cool lad"); + e3: e(1232); + e4: e(@hello); + e5: e("one" + 'more'); + + t1: ~"eating rice"; + t2: ~"string cheese"; + t3: a b c ~"string me" d e f; + t4: ~"string @{hello}"; +} + +.class { + filter: ~"progid:DXImageTransform.Microsoft.AlphaImageLoader(src='image.png')"; +} diff --git a/vendor/leafo/lessphp/tests/inputs/font_family.less b/vendor/leafo/lessphp/tests/inputs/font_family.less new file mode 100644 index 000000000..d1dfb72df --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/font_family.less @@ -0,0 +1,28 @@ + +@font-directory: 'fonts/'; +@some-family: Gentium; + +@font-face: maroon; // won't collide with @font-face { } + +@font-face { + font-family: Graublau Sans Web; + src: url(@{font-directory}GraublauWeb.otf) format("opentype"); +} + +@font-face { + font-family: @some-family; + src: url('@{font-directory}Gentium.ttf'); +} + +@font-face { + font-family: @some-family; + src: url("@{font-directory}GentiumItalic.ttf"); + font-style: italic; +} + +h2 { + font-family: @some-family; + crazy: @font-face; +} + + diff --git a/vendor/leafo/lessphp/tests/inputs/guards.less b/vendor/leafo/lessphp/tests/inputs/guards.less new file mode 100644 index 000000000..fc4e344e1 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/guards.less @@ -0,0 +1,74 @@ + +.simple(@hi) when (@hi) { + simple: yellow; +} + + +.something(@hi) when (@hi = cool) { + something: red; +} + +.another(@x) when (@x > 10) { + another: green; +} + + +.flipped(@x) when (@x =< 10) { + flipped: teal; +} + +.yeah(@arg) when (isnumber(@arg)) { + yeah-number: purple @arg; +} + + +.yeah(@arg) when (ispixel(@arg)) { + yeah-pixel: silver; +} + + +.hello(@arg) when not (@arg) { + hello: orange; +} + +dd { + .simple(true); +} + +b { + .something(cool); + .something(birthday); +} + +img { + .another(12); + .flipped(2); +} + +body { + .yeah(232px); + .yeah(232); +} + +.something(@x) when (@x) and (@y), not (@x = what) { + something-complex: blue @x; +} + +div { + @y: true; + .something(true); + +} + +.coloras(@g) when (iscolor(@g)) { + color: true @g; +} + +link { + .coloras(red); + .coloras(#fff); + .coloras(#fffddd); + .coloras(rgb(0,0,0)); + .coloras(rgba(0,0,0, .34)); +} + diff --git a/vendor/leafo/lessphp/tests/inputs/hacks.less b/vendor/leafo/lessphp/tests/inputs/hacks.less new file mode 100644 index 000000000..e69b7bf98 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/hacks.less @@ -0,0 +1,6 @@ +// css hacks + +:root .alert-message, :root .btn { + border-radius: 0 \0; +} + diff --git a/vendor/leafo/lessphp/tests/inputs/hi.less b/vendor/leafo/lessphp/tests/inputs/hi.less new file mode 100644 index 000000000..9a3d8198a --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/hi.less @@ -0,0 +1,5 @@ + +div:before { + content: "hi!"; +} + diff --git a/vendor/leafo/lessphp/tests/inputs/ie.less b/vendor/leafo/lessphp/tests/inputs/ie.less new file mode 100644 index 000000000..37a5f1f67 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/ie.less @@ -0,0 +1,12 @@ + +foo { + filter: progid:DXImageTransform.Microsoft.gradient(GradientType=1, startColorstr=#c0ff3300, endColorstr=#ff000000); + filter:progid:DXImageTransform.Microsoft.gradient(GradientType=1, startColorstr=#c0ff3300, endColorstr=#ff000001); +} + + +foo { + filter: alpha(opacity=20); + filter: alpha(opacity=20, enabled=true); + filter: blaznicate(foo=bar, baz=bang bip, bart=#fa4600); +} diff --git a/vendor/leafo/lessphp/tests/inputs/import.less b/vendor/leafo/lessphp/tests/inputs/import.less new file mode 100644 index 000000000..a5005674e --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/import.less @@ -0,0 +1,56 @@ + +@import 'file1.less'; // file found and imported + +@import "not-found"; + +@import "something.css" media; +@import url("something.css") media; +@import url(something.css) media, screen, print; + +@color: maroon; + +@import url(file2); // found and imported + +body { + line-height: 10em; + @colors; +} + +div { + @color: fuchsia; + @import "file2"; +} + + +.mixin-import() { + @import "file3"; +} + +.one { + .mixin-import(); + color: blue; +} + +.two { + .mixin-import(); +} + + +#merge-import-mixins { + @import "a"; + @import "b"; + div { .just-a-class; } +} + + +@import "inner/file1"; + + +// test bubbling variables up from imports, while preserving order + +pre { + color: @someValue; +} + +@import "file3"; + diff --git a/vendor/leafo/lessphp/tests/inputs/interpolation.less b/vendor/leafo/lessphp/tests/inputs/interpolation.less new file mode 100644 index 000000000..8aad00042 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/interpolation.less @@ -0,0 +1,47 @@ + +@cool-hello: "yes"; +@cool-yes: "okay"; +@var: "hello"; + +div { + interp1: ~"@{cool-hello}"; + interp2: ~"@{cool-@{var}}"; + interp3: ~"@{cool-@{cool-@{var}}}"; +} + +// interpolation in selectors + +@hello: 10; +@world: "yeah"; + +@{hello}@{world} { + color: blue; +} + +@{hello} { + color: blue; +} + +hello world @{hello} { + color: red; +} + +#@{world} { + color: "hello @{hello}"; +} + + +@num: 3; + +[prop], +[prop="value@{num}"], +[prop*="val@{num}"], +[|prop~="val@{num}"], +[*|prop$="val@{num}"], +[ns|prop^="val@{num}"], +[@{num}^="val@{num}"], +[@{num}=@{num}], +[@{num}] { + attributes: yes; +} + diff --git a/vendor/leafo/lessphp/tests/inputs/keyframes.less b/vendor/leafo/lessphp/tests/inputs/keyframes.less new file mode 100644 index 000000000..e65a38b95 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/keyframes.less @@ -0,0 +1,52 @@ +@keyframes 'bounce' { + from { + top: 100px; + animation-timing-function: ease-out; + } + + 25% { + top: 50px; + animation-timing-function: ease-in; + } + + 50% { + top: 100px; + animation-timing-function: ease-out; + } + + 75% { + top: 75px; + animation-timing-function: ease-in; + } + + to { + top: 100px; + } +} + +@-webkit-keyframes flowouttoleft { + 0% { -webkit-transform: translateX(0) scale(1); } + 60%, 70% { -webkit-transform: translateX(0) scale(.7); } + 100% { -webkit-transform: translateX(-100%) scale(.7); } +} + +div { + animation-name: 'diagonal-slide'; + animation-duration: 5s; + animation-iteration-count: 10; +} + +@keyframes 'diagonal-slide' { + + from { + left: 0; + top: 0; + } + + to { + left: 100px; + top: 100px; + } + +} + diff --git a/vendor/leafo/lessphp/tests/inputs/math.less b/vendor/leafo/lessphp/tests/inputs/math.less new file mode 100644 index 000000000..db59d3564 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/math.less @@ -0,0 +1,122 @@ + +.unary { + // all operators are parsable as unary operators, anything + // but - throws an error right now though, + + // this gives two numbers + sub: 10 -5; + // add: 10 +5; // error + // div: 10 /5; // error + // mul: 10 *5; // error +} + +.spaces { + // we can make the parser do math by leaving out the + // space after the first value, or putting spaces on both sides + + sub1: 10-5; + sub2: 10 - 5; + + add1: 10+5; + add2: 10 + 5; + + // div: 10/5; // this wont work, read on + div: 10 / 5; + + mul1: 10*5; + mul2: 10 * 5; +} + +// these properties have divison not in parenthases +.supress-division { + border-radius: 10px / 10px; + border-radius: 10px/12px; + border-radius: hello (10px/10px) world; + @x: 10px; + font: @x/30 sans-serif; + font: 10px / 20px sans-serif; + font: 10px/22px sans-serif; + border-radius:0 15px 15px 15px / 0 50% 50% 50%; +} + + +.parens { + // if you are unsure, then just wrap the expression in parentheses and it will + // always evaluate. + + // notice we no longer have unary operators, and these will evaluate + sub: (10 -5); + add: (10 +5); + div1: (10 /5); + div2: (10/5); // no longer interpreted as the shorthand + mul: (10 *5); +} + +.keyword-names { + // watch out when doing math with keywords, - is a valid keyword character + @a: 100; + @b: 25; + @a-: "hello"; + height: @a-@b; // here we get "hello" 25, not 75 +} + + +.negation { + neg1: -(1px); + neg2: 0-(1px); + + @something: 10; + neg3: -@something; +} + + +// and now here are the tests + +.test { + single1: (5); + single2: 5+(5); + single3: (5)+((5)); + + parens: (5 +(5)) -2; + // parens: ((5 +(5)) -2); // FAILS - fixme + + math1: (5 + 5)*(2 / 1); + math2: (5+5)*(2/1); + + complex1: 2 * (4 * (2 + (1 + 6))) - 1; + complex2: ((2+3)*(2+3) / (9-4)) + 1; + complex3: (2px + 4px) 1em 2px 2; + + @var: (2 * 2); + var1: (2 * @var) 4 4 (@var * 1px); + var2: (@var * @var) * 6; + var3: 4 * (5 + 5) / 2 - (@var * 2); + + complex4: (7 * 7) + (8 * 8); +} + +.percents { + p1: 100 * 10%; + p2: 10% * 100; + p3: 10% * 10%; + + p4: 100px * 10%; // lessjs makes this px + p5: 10% * 100px; // lessjs makes this % + + p6: 20% + 10%; + p7: 20% - 10%; + + p8: 20% / 10%; +} + +.misc { + x: 10px * 4em; + y: 10 * 4em; +} + + +.cond { + c1: 10 < 10; + c2: 10 >= 10; +} + diff --git a/vendor/leafo/lessphp/tests/inputs/media.less b/vendor/leafo/lessphp/tests/inputs/media.less new file mode 100644 index 000000000..8c16a3cf6 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/media.less @@ -0,0 +1,68 @@ +@media screen, 3D { + P { color: green; } +} +@media print { + body { font-size: 10pt } +} +@media screen { + body { font-size: 13px } +} +@media screen, print { + body { line-height: 1.2 } +} + +@media all and (min-width: 0px) { + body { line-height: 1.2 } +} + +@media all and (min-width: 0) { + body { line-height: 1.2 } +} + +@media + screen and (min-width: 102.5em) and (max-width: 117.9375em), + screen and (min-width: 150em) { + body { color: blue } +} + + +@media screen and (min-height: 100px + 10px) { + body { color: red; } +} + +@cool: 100px; + +@media screen and (height: @cool) and (width: @cool + 10), (size: @cool + 20) { + body { color: red; } +} + + +// media bubbling + +@media test { + div { + height: 20px; + @media (hello) { + color: red; + + pre { + color: orange; + } + } + } +} + +// should not cross boundary +@media yeah { + @page { + @media cool { + color: red; + } + } +} + +// variable in query +@mobile: ~"(max-width: 599px)"; +@media @mobile { + .helloworld { color: blue } +} diff --git a/vendor/leafo/lessphp/tests/inputs/misc.less b/vendor/leafo/lessphp/tests/inputs/misc.less new file mode 100644 index 000000000..eb690b9a3 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/misc.less @@ -0,0 +1,100 @@ + +@color: #fff; +@base_path: "/assets/images/"; +@images: @base_path + "test/"; +.topbar { background: url(@{images}topbar.png); } +.hello { test: empty-function(@images, 40%, to(@color)); } + +.css3 { + background-image: -webkit-gradient(linear, 0% 0%, 0% 90%, + from(#E9A000), to(#A37000)); +} + + +/** + +Here is a block comment + +**/ + + +// this is a comment + +.test, /*hello*/.world { + border: 1px solid red; // world + /* another property */ + color: url(http://mage-page.com); + string: "hello /* this is not a comment */"; + world: "// neither is this"; + string: 'hello /* this is not a comment */' /*what if this is a comment */; + world: '// neither is this' // hell world; + ; + what-/*something?*/ever: 100px; + background: url(/*no comment here*/); +} + + +.urls { + @var: "http://google.com"; + background1: url(@var); + background2: url(@{var}); + background3: url("@{var}"); +} + +.mix(@arg) { color: @arg; } +@aaa: aaa; +@bbb: bbb; +// make sure the opening selector isn't too greedy +.cool {.mix("@{aaa}, @{bbb}")} +.cool(); + + + +// merging of mixins +.span-17 { float: left; } +.span-17 { width: 660px; } + +.x {.span-17;} + +.hi { + pre { + color: red; + } +} + +.hi { + pre { + color: blue; + } +} + +.rad { + .hi; +} + + +hello { + numbers: 1.0 0.1 .1 1.; + numbers: 1.0s 0.1s .1s 1.s; + numbers: -1.0s -0.1s -.1s -1.s; + numbers: -1.0 -0.1 -.1 -1.; +} + + +#string { + hello: 'what\'s going on here'; + hello: 'blah blag @{ blah blah'; + + join: 3434 + "hello"; + join: 3434 + hello; +} + + +.duplicates { + hello: world; + hello: "world"; + hello: world; + hello: "what"; + hello: world; + hello: "world"; +} diff --git a/vendor/leafo/lessphp/tests/inputs/mixin_functions.less b/vendor/leafo/lessphp/tests/inputs/mixin_functions.less new file mode 100644 index 000000000..2d858ad6d --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/mixin_functions.less @@ -0,0 +1,33 @@ + +@outer: 10px; +@class(@var:22px, @car: 400px + @outer) { + margin: @var; + height: @car; +} + +@group { + @f(@color) { + color: @color; + } + .cool { + border-bottom: 1px solid green; + } +} + +.class(@width:200px) { + padding: @width; +} + +body { + .class(2.0em); + @group > @f(red); + @class(10px, 10px + 2); + @group > .cool; +} + + +@lots(@a: 10px, @b: 20px, @c: 30px, @d: 40px, @e: 4px, @f:3px, @g:2px, @h: 1px) { + padding: @a @b @c @d; + margin: @e @f @g @h; +} + diff --git a/vendor/leafo/lessphp/tests/inputs/mixin_merging.less.disable b/vendor/leafo/lessphp/tests/inputs/mixin_merging.less.disable new file mode 100644 index 000000000..86b3e0cbb --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/mixin_merging.less.disable @@ -0,0 +1,100 @@ + +@tester { + p, div { height: 10px; } +} + +#test1 { + div { color: red; } + @tester; +} + + +@cool { + a,b,i { width: 1px; } +} + +#test2 { + b { color: red; } + @cool; +} + +#test3 { + @cool; + b { color: red; } +} + +@cooler { + a { margin: 1px; } +} + +#test4 { + a, div, html { color: blue; } + @cooler; +} + +@hi { + img, strong { float: right; } +} + +#test5 { + img, strong { padding: 2px; } + @hi; +} + +@nested { + div, span { + a { + color: red; + } + } +} + +#test6 { + div, span { + a { + line-height: 10px; + } + } + @nested; +} + +@broken-nesting { + div, span { + strong, b { + color: red; + } + } + +} + +#test7 { + div { + strong { + margin: 1px; + } + } + @broken-nesting; +} + + +@another-nest { + a,b { + i { + color: red; + } + + s { + color: blue; + } + } +} + +#test8 { + a, b { + i,s { + background: red; + } + } + @another-nest; +} + diff --git a/vendor/leafo/lessphp/tests/inputs/mixins.less b/vendor/leafo/lessphp/tests/inputs/mixins.less new file mode 100644 index 000000000..768e6384e --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/mixins.less @@ -0,0 +1,197 @@ + +@rounded-corners { + border-radius: 10px; +} + +.bold { + @font-size: 20px; + font-size: @font-size; + font-weight: bold; +} + +body #window { + @rounded-corners; + .bold; + line-height: @font-size * 1.5; +} + +#bundle { + .button { + display: block; + border: 1px solid black; + background-color: grey; + &:hover { background-color: white } + } +} +#header a { + color: orange; + #bundle > .button; // mixin the button class +} + +div { + @abstract { + hello: world; + b { + color: blue; + } + } + + @abstract > b; + @abstract; +} + +@poop { + big: baby; +} + +body { + div; +} + +// not using > to list mixins + +.hello { + .world { + color: blue; + } +} + +.foobar { + .hello .world; +} + + +// arguments + +.spam(@something: 100, @dad: land) { + @wow: 23434; + foo: @arguments; + bar: @arguments; +} + +.eggs { + .spam(1px, 2px); + .spam(); +} + +.first(@one, @two, @three, @four: cool) { + cool: @arguments; +} + +#hello { + .first(one, two, three); +} + +#hello-important { + .first(one, two, three) !important; +} + +.rad(@name) { + cool: @arguments; +} + +#world { + @hello: "world"; + .rad("@{hello}"); +} + +.second(@x, @y:skip, @z: me) { + things: @arguments; +} + +#another { + .second(red, blue, green); + .second(red blue green); +} + + +.another(@x, @y:skip, @z:me) { + .cool { + color: @arguments; + } +} + +#day { + .another(one,two, three); + .another(one two three); +} + + +.to-be-important() { + color: red; + @color: red; + height: 20px; + + pre { + color: @color; + } +} + +.mix-suffix { + .to-be-important() !important; +} + + + + +#search-all { + .red() { + color:#f00 !important; + } +} + +#search-all { + .green() { + color: #0f0 !important; + } +} + +.search-test { + #search-all > .red(); + #search-all > .green(); +} + + +// mixin self without infinite loop +.cowboy() { + color: blue; +} + +.cowboy { + .cowboy; +} + + +// semicolon + +.semi1(@color: red, blue, green;) { + color: @color; +} + +.semi2(@color: red, blue, green; dad) { + color: @color; +} + +.semi3(hello; world; piss) { + hello: world; +} + + + +// self referencing skipping + +.nav-divider(@color: red){ + padding: 10px; +} + +.nav { + .nav-divider { + .nav-divider(); + } +} + +.nav-divider { + .nav-divider(); +} + + diff --git a/vendor/leafo/lessphp/tests/inputs/nested.less b/vendor/leafo/lessphp/tests/inputs/nested.less new file mode 100644 index 000000000..0b62ea19b --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/nested.less @@ -0,0 +1,60 @@ +#header { + color: black; + + .navigation { + font-size: 12px; + .border { + .outside { + color: blue; + } + } + } + .logo { + width: 300px; + &:hover { text-decoration: none } + } +} + +a { b { ul { li { color: green; } } } } + +this { will { not { show { } } } } + +.cool { + div & { color: green; } + p & span { color: yellow; } +} + +another { + .cool; +} + +b { + & .something { + color: blue; + } + + &.something { + color: blue; + } +} + +.foo { + .bar, .baz { + & .qux { + display: block; + } + .qux & { + display: inline; + } + .qux & .biz { + display: none; + } + } +} + +b { + hello [x="&yeah"] { + color: red; + } +} + diff --git a/vendor/leafo/lessphp/tests/inputs/pattern_matching.less b/vendor/leafo/lessphp/tests/inputs/pattern_matching.less new file mode 100644 index 000000000..c2a0da917 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/pattern_matching.less @@ -0,0 +1,157 @@ + +.demo (light, @color) { + color: lighten(@color, 10%); +} +.demo (@_, @color) { + display: block; +} + +@switch: light; + +.class { + .demo(@switch, #888); +} + +// by arity + +.mixin () { + zero: 0; +} +.mixin (@a: 1px) { + one: 1; +} +.mixin (@a) { + one-req: 1; +} +.mixin (@a: 1px, @b: 2px) { + two: 2; +} + +.mixin (@a, @b, @c) { + three-req: 3; +} + +.mixin (@a: 1px, @b: 2px, @c: 3px) { + three: 3; +} + +.zero { + .mixin(); +} + +.one { + .mixin(1); +} + +.two { + .mixin(1, 2); +} + +.three { + .mixin(1, 2, 3); +} + +// + +.mixout ('left') { + left: 1; +} + +.mixout ('right') { + right: 1; +} + +.left { + .mixout('left'); +} +.right { + .mixout('right'); +} + +// + +.border (@side, @width) { + color: black; + .border-side(@side, @width); +} +.border-side (left, @w) { + border-left: @w; +} +.border-side (right, @w) { + border-right: @w; +} + +.border-right { + .border(right, 4px); +} +.border-left { + .border(left, 4px); +} + +// + + +.border-radius (@r) { + both: @r * 10; +} +.border-radius (@r, left) { + left: @r; +} +.border-radius (@r, right) { + right: @r; +} + +.only-right { + .border-radius(33, right); +} +.only-left { + .border-radius(33, left); +} +.left-right { + .border-radius(33); +} + +.hola(hello, @hello...) { + color: blue; +} + +#hola { + .hola(hello, world); +} + +.resty(@hello, @world, @the_rest...) { + padding: @hello @world; + rest: @the_rest; +} + +.defaults(@aa, @bb:e343, @cc: "heah", ...) { + height: @aa; +} + +#defaults_1 { + .defaults(one); + .defaults(two, one); + .defaults(three, two, one); + .defaults(four, three, two, one); +} + + +.thing() { color: green; } +.thing(...) { color: blue; } +.thing { color: red; } + +#aa { + .thing(); +} + +#bb { + .thing; +} + + +#cc { + .thing(1); +} + + + diff --git a/vendor/leafo/lessphp/tests/inputs/scopes.less b/vendor/leafo/lessphp/tests/inputs/scopes.less new file mode 100644 index 000000000..42e469696 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/scopes.less @@ -0,0 +1,40 @@ + + +@a: 10; +@some { + @b: @a + 10; + div { + @c: @b + 10; + other { + @d: @c + 10; + world { + @e: @d + 10; + height: @e; + } + } + } +} + + +body { + @some; +} + +@some; + +.test(@x: 10) { + height: @x; + .test(@y: 11) { + height: @y; + .test(@z: 12) { + height: @z; + } + .test; + } + .test; +} + +pre { + .test; +} + diff --git a/vendor/leafo/lessphp/tests/inputs/selector_expressions.less b/vendor/leafo/lessphp/tests/inputs/selector_expressions.less new file mode 100644 index 000000000..b8aa2214a --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/selector_expressions.less @@ -0,0 +1,29 @@ + +@color: blue; + +something @{color}, world { + color: blue; +} + +.div { + @color: red; + (3434) { + height: 100px; + } + + cool @{color} { + height: 4000px; + } +} + +.heck(@a) { color: @a+10 } + +.spanX (@index) when (@index > 0) { + .span@{index} { .heck(@index) } + .spanX(@index - 1); +} +.spanX (0) {} + +.spanX (5); + + diff --git a/vendor/leafo/lessphp/tests/inputs/site_demos.less b/vendor/leafo/lessphp/tests/inputs/site_demos.less new file mode 100644 index 000000000..08d7f1e5c --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/site_demos.less @@ -0,0 +1,120 @@ +// these are the demos from the lessphp homepage + +default { + @base: 24px; + @border-color: #B2B; + + .underline { border-bottom: 1px solid green } + + #header { + color: black; + border: 1px solid @border-color + #222222; + + .navigation { + font-size: @base / 2; + a { + .underline; + } + } + .logo { + width: 300px; + &:hover { text-decoration: none } + } + } +} + +variables { + @a: 2; + @x: @a * @a; + @y: @x + 1; + @z: @x * 2 + @y; + + @nice-blue: #5B83AD; + @light-blue: @nice-blue + #111; + + @b: @a * 10; + @c: #888; + @fonts: "Trebuchet MS", Verdana, sans-serif; + + .variables { + width: @z + 1cm; // 14cm + height: @b + @x + 0px; // 24px + color: @c; + background: @light-blue; + font-family: @fonts; + } +} + +mixins { + .bordered { + border-top: dotted 1px black; + border-bottom: solid 2px black; + } + #menu a { + color: #111; + .bordered; + } + + .post a { + color: red; + .bordered; + } +} + +nested-rules { + #header { + color: black; + + .navigation { + font-size: 12px; + } + .logo { + width: 300px; + &:hover { text-decoration: none } + } + } +} + +namespaces { + #bundle { + .button { + display: block; + border: 1px solid black; + background-color: grey; + &:hover { background-color: white } + } + } + #header a { + color: orange; + #bundle > .button; // mixin the button class + } +} + +mixin-functions { + @outer: 10px; + @class(@var:22px, @car: 400px + @outer) { + margin: @var; + height: @car; + } + + @group { + @f(@color) { + color: @color; + } + .cool { + border-bottom: 1px solid green; + } + } + + .class(@width:200px) { + padding: @width; + } + + body { + .class(2.0em); + @group > @f(red); + @class(10px, 10px + 2); + @group > .cool; + } +} + diff --git a/vendor/leafo/lessphp/tests/inputs/test-imports/a.less b/vendor/leafo/lessphp/tests/inputs/test-imports/a.less new file mode 100644 index 000000000..a00464dbb --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/test-imports/a.less @@ -0,0 +1,6 @@ +.just-a-class { background: red; } + +.some-mixin() { + height: 200px; +} + diff --git a/vendor/leafo/lessphp/tests/inputs/test-imports/b.less b/vendor/leafo/lessphp/tests/inputs/test-imports/b.less new file mode 100644 index 000000000..599ed3a46 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/test-imports/b.less @@ -0,0 +1,12 @@ +.just-a-class { background: blue; } + +.hello { + .some-mixin(); +} + + +@media cool { + color: red; + .some-mixin(); +} + diff --git a/vendor/leafo/lessphp/tests/inputs/test-imports/file1.less b/vendor/leafo/lessphp/tests/inputs/test-imports/file1.less new file mode 100644 index 000000000..658de0c53 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/test-imports/file1.less @@ -0,0 +1,16 @@ + + +/** + * This is a test import file + */ + +@colors { + div.bright { + color: red; + } + + div.sad { + color: blue; + } +} + diff --git a/vendor/leafo/lessphp/tests/inputs/test-imports/file2.less b/vendor/leafo/lessphp/tests/inputs/test-imports/file2.less new file mode 100644 index 000000000..2cae8dce0 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/test-imports/file2.less @@ -0,0 +1,6 @@ + +b { + color: @color; + padding: 16px; +} + diff --git a/vendor/leafo/lessphp/tests/inputs/test-imports/file3.less b/vendor/leafo/lessphp/tests/inputs/test-imports/file3.less new file mode 100644 index 000000000..28b643ea9 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/test-imports/file3.less @@ -0,0 +1,7 @@ + +h2 { + background: url("../images/logo.png") no-repeat; +} + +@someValue: hello-from-file-3; + diff --git a/vendor/leafo/lessphp/tests/inputs/test-imports/inner/file1.less b/vendor/leafo/lessphp/tests/inputs/test-imports/inner/file1.less new file mode 100644 index 000000000..df654a7ee --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/test-imports/inner/file1.less @@ -0,0 +1,6 @@ + +.inner { + content: "inner/file1.less" +} + +@import "file2"; // should get the one in inner diff --git a/vendor/leafo/lessphp/tests/inputs/test-imports/inner/file2.less b/vendor/leafo/lessphp/tests/inputs/test-imports/inner/file2.less new file mode 100644 index 000000000..f40d3c67b --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/test-imports/inner/file2.less @@ -0,0 +1,4 @@ + +.inner { + content: "inner/file2.less" +} diff --git a/vendor/leafo/lessphp/tests/inputs/variables.less b/vendor/leafo/lessphp/tests/inputs/variables.less new file mode 100644 index 000000000..6e18eb807 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs/variables.less @@ -0,0 +1,44 @@ +@a: 2; +@x: @a * @a; +@y: @x + 1; +@z: @y + @x * 2; +@m: @z % @y; + +@nice-blue: #5B83AD; +@light-blue: @nice-blue + #111; + +@rgb-color: rgb(20%, 15%, 80%); +@rgba-color: rgba(23,68,149,0.5); + +@b: @a * 10px; +@c: #888; +@fonts: "Trebuchet MS", Verdana, sans-serif; + +.variables { + width: @z + 1cm; // 14cm + height: @b + @x + 0px; // 24px + margin-top: -@b; // -20px + margin-bottom: 10 - -@b; // 30px + @d: @c + #001; + color: @d; + background: @light-blue; + font-family: @fonts; + margin: @m + 0px; // 3px + font: 10px/12px serif; + font: 120%/120% serif; +} + +.external { + color: @c; + border: 1px solid @rgb-color; + background: @rgba-color; +} + +@hello: 44px; +@something: "hello"; +@cool: something; + +outer1: @@something; +outer2: @@@cool; + + diff --git a/vendor/leafo/lessphp/tests/inputs_lessjs/mixins-args.less b/vendor/leafo/lessphp/tests/inputs_lessjs/mixins-args.less new file mode 100644 index 000000000..c39659242 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs_lessjs/mixins-args.less @@ -0,0 +1,205 @@ +.mixin (@a: 1px, @b: 50%) { + width: (@a * 5); + height: (@b - 1%); +} + +.mixina (@style, @width, @color: black) { + border: @width @style @color; +} + +.mixiny +(@a: 0, @b: 0) { + margin: @a; + padding: @b; +} + +.hidden() { + color: transparent; // asd +} + +#hidden { + .hidden; +} + +#hidden1 { + .hidden(); +} + +.two-args { + color: blue; + .mixin(2px, 100%); + .mixina(dotted, 2px); +} + +.one-arg { + .mixin(3px); +} + +.no-parens { + .mixin; +} + +.no-args { + .mixin(); +} + +.var-args { + @var: 9; + .mixin(@var, (@var * 2)); +} + +.multi-mix { + .mixin(2px, 30%); + .mixiny(4, 5); +} + +.maxa(@arg1: 10, @arg2: #f00) { + padding: (@arg1 * 2px); + color: @arg2; +} + +body { + .maxa(15); +} + +@glob: 5; +.global-mixin(@a:2) { + width: (@glob + @a); +} + +.scope-mix { + .global-mixin(3); +} + +.nested-ruleset (@width: 200px) { + width: @width; + .column { margin: @width; } +} +.content { + .nested-ruleset(600px); +} + +// + +.same-var-name2(@radius) { + radius: @radius; +} +.same-var-name(@radius) { + .same-var-name2(@radius); +} +#same-var-name { + .same-var-name(5px); +} + +// + +.var-inside () { + @var: 10px; + width: @var; +} +#var-inside { .var-inside; } + +.mixin-arguments (@width: 0px, ...) { + border: @arguments; + width: @width; +} + +.arguments { + .mixin-arguments(1px, solid, black); +} +.arguments2 { + .mixin-arguments(); +} +.arguments3 { + .mixin-arguments; +} + +.mixin-arguments2 (@width, @rest...) { + border: @arguments; + rest: @rest; + width: @width; +} +.arguments4 { + .mixin-arguments2(0, 1, 2, 3, 4); +} + +// Edge cases + +.edge-case { + .mixin-arguments("{"); +} + +// Division vs. Literal Slash +.border-radius(@r: 2px/5px) { + border-radius: @r; +} +.slash-vs-math { + .border-radius(); + .border-radius(5px/10px); + .border-radius((3px * 2)); +} +// semi-colon vs comma for delimiting + +.mixin-takes-one(@a) { + one: @a; +} + +.mixin-takes-two(@a; @b) { + one: @a; + two: @b; +} + +.comma-vs-semi-colon { + .mixin-takes-two(@a : a; @b : b, c); + .mixin-takes-two(@a : d, e; @b : f); + .mixin-takes-one(@a: g); + .mixin-takes-one(@a : h;); + .mixin-takes-one(i); + .mixin-takes-one(j;); + .mixin-takes-two(k, l); + .mixin-takes-one(m, n;); + .mixin-takes-two(o, p; q); + .mixin-takes-two(r, s; t;); +} + +.mixin-conflict(@a:defA, @b:defB, @c:defC) { + three: @a, @b, @c; +} + +.mixin-conflict(@a:defA, @b:defB, @c:defC, @d:defD) { + four: @a, @b, @c, @d; +} + +#named-conflict { + .mixin-conflict(11, 12, 13, @a:a); + .mixin-conflict(@a:a, 21, 22, 23); +} +@a: 3px; +.mixin-default-arg(@a: 1px, @b: @a, @c: @b) { + defaults: 1px 1px 1px; + defaults: 2px 2px 2px; +} + +.test-mixin-default-arg { + .mixin-default-arg(); + .mixin-default-arg(2px); +} + +.mixin-comma-default1(@color; @padding; @margin: 2, 2, 2, 2) { + margin: @margin; +} +.selector { + .mixin-comma-default1(#33acfe; 4); +} +.mixin-comma-default2(@margin: 2, 2, 2, 2;) { + margin: @margin; +} +.selector2 { + .mixin-comma-default2(); +} +.mixin-comma-default3(@margin: 2, 2, 2, 2) { + margin: @margin; +} +.selector3 { + .mixin-comma-default3(4,2,2,2); +} diff --git a/vendor/leafo/lessphp/tests/inputs_lessjs/mixins-named-args.less b/vendor/leafo/lessphp/tests/inputs_lessjs/mixins-named-args.less new file mode 100644 index 000000000..f62dc86a2 --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs_lessjs/mixins-named-args.less @@ -0,0 +1,36 @@ +.mixin (@a: 1px, @b: 50%) { + width: (@a * 5); + height: (@b - 1%); + args: @arguments; +} +.mixin (@a: 1px, @b: 50%) when (@b > 75%){ + text-align: center; +} + +.named-arg { + color: blue; + .mixin(@b: 100%); +} + +.class { + @var: 20%; + .mixin(@b: @var); +} + +.all-args-wrong-args { + .mixin(@b: 10%, @a: 2px); +} + +.mixin2 (@a: 1px, @b: 50%, @c: 50) { + width: (@a * 5); + height: (@b - 1%); + color: (#000000 + @c); +} + +.named-args2 { + .mixin2(3px, @c: 100); +} + +.named-args3 { + .mixin2(@b: 30%, @c: #123456); +} diff --git a/vendor/leafo/lessphp/tests/inputs_lessjs/strings.less b/vendor/leafo/lessphp/tests/inputs_lessjs/strings.less new file mode 100644 index 000000000..32fad721a --- /dev/null +++ b/vendor/leafo/lessphp/tests/inputs_lessjs/strings.less @@ -0,0 +1,51 @@ +#strings { + background-image: url("http://son-of-a-banana.com"); + quotes: "~" "~"; + content: "#*%:&^,)!.(~*})"; + empty: ""; + brackets: "{" "}"; + escapes: "\"hello\" \\world"; + escapes2: "\"llo"; +} +#comments { + content: "/* hello */ // not-so-secret"; +} +#single-quote { + quotes: "'" "'"; + content: '""#!&""'; + empty: ''; + semi-colon: ';'; +} +#escaped { + filter: ~"DX.Transform.MS.BS.filter(opacity=50)"; +} +#one-line { image: url(http://tooks.com) } +#crazy { image: url(http://), "}", url("http://}") } +#interpolation { + @var: '/dev'; + url: "http://lesscss.org@{var}/image.jpg"; + + @var2: 256; + url2: "http://lesscss.org/image-@{var2}.jpg"; + + @var3: #456; + url3: "http://lesscss.org@{var3}"; + + @var4: hello; + url4: "http://lesscss.org/@{var4}"; + + @var5: 54.4px; + url5: "http://lesscss.org/@{var5}"; +} + +// multiple calls with string interpolation + +.mix-mul (@a: green) { + color: ~"@{a}"; +} +.mix-mul-class { + .mix-mul(blue); + .mix-mul(red); + .mix-mul(black); + .mix-mul(orange); +} diff --git a/vendor/leafo/lessphp/tests/outputs/accessors.css b/vendor/leafo/lessphp/tests/outputs/accessors.css new file mode 100644 index 000000000..2f3c9e619 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/accessors.css @@ -0,0 +1,14 @@ +.article { color:#294366; } +.comment { + width:960px; + color:#294366; + padding:10px; +} +.wow { + height:960px; + background-color:#294366; + color:green; + padding:; + margin:; +} +.mix { font-size:10px; } diff --git a/vendor/leafo/lessphp/tests/outputs/arity.css b/vendor/leafo/lessphp/tests/outputs/arity.css new file mode 100644 index 000000000..5173561d4 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/arity.css @@ -0,0 +1,25 @@ +.one { + hello: one; + world: one; +} +.two { + hello: two; + world: two; +} +.three { + hello: three; + world: three; +} +.multi-foo { + foo: two cool; + foo: three cool yeah; + foo: two 1; + foo: three 1 yeah; + foo: three 1 1; +} +.multi-baz { + baz: two cool; + baz: three yeah; + baz: two 1; + baz: three 1; +} diff --git a/vendor/leafo/lessphp/tests/outputs/attributes.css b/vendor/leafo/lessphp/tests/outputs/attributes.css new file mode 100644 index 000000000..fb0e176cf --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/attributes.css @@ -0,0 +1,105 @@ +* { + color: blue; +} +E { + color: blue; +} +E[foo] { + color: blue; +} +[foo] { + color: blue; +} +[foo] .helloWorld { + color: blue; +} +[foo].helloWorld { + color: blue; +} +E[foo="barbar"] { + color: blue; +} +E[foo~="hello#$@%@$#^"] { + color: blue; +} +E[foo^="color: green;"] { + color: blue; +} +E[foo$="239023"] { + color: blue; +} +E[foo*="29302"] { + color: blue; +} +E[foo|="239032"] { + color: blue; +} +E:root { + color: blue; +} +E:nth-child(odd) { + color: blue; +} +E:nth-child(2n+1) { + color: blue; +} +E:nth-child(5) { + color: blue; +} +E:nth-last-child(-n+2) { + color: blue; +} +E:nth-of-type(2n) { + color: blue; +} +E:nth-last-of-type(n) { + color: blue; +} +E:first-child { + color: blue; +} +E:last-child { + color: blue; +} +E:first-of-type { + color: blue; +} +E:last-of-type { + color: blue; +} +E:only-child { + color: blue; +} +E:only-of-type { + color: blue; +} +E:empty { + color: blue; +} +E:lang(en) { + color: blue; +} +E::first-line { + color: blue; +} +E::before { + color: blue; +} +E#id { + color: blue; +} +E:not(:link) { + color: blue; +} +E F { + color: blue; +} +E > F { + color: blue; +} +E + F { + color: blue; +} +E ~ F { + color: blue; +} diff --git a/vendor/leafo/lessphp/tests/outputs/builtins.css b/vendor/leafo/lessphp/tests/outputs/builtins.css new file mode 100644 index 000000000..6ac21f2c9 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/builtins.css @@ -0,0 +1,61 @@ +body { + color: "hello world"; + num-basic: 5.1666666666667; + num-floor: 5px; + num-ceil: 6px; + num2: 0.66666666666667; + num2-round: 1; + num2-floor: 0; + num2-ceil: 1; + round-lit: 3px; + rgba1: #ff112233; + rgba2: #992c3742; + argb: #992c3742; +} +format { + format: "rgb(32, 128, 64)"; + format-string: "hello world"; + format-multiple: "hello earth 2"; + format-url-encode: 'red is %A'; + eformat: rgb(32, 128, 64); +} +#functions { + str1: true; + str2: false; + num1: true; + num2: true; + num3: true; + num4: false; + col1: true; + col2: false; + col3: true; + col4: true; + key1: true; + key2: false; + px1: true; + px2: false; + per1: true; + per2: false; + em1: true; + em2: false; + ex1: 2; + ex2: 1; + ex3: extract(1,1); + ex4: 2; + pow: 16; + pi: 3.1415926535898; + mod: 4; + tan: 1.5574077246549; + cos: 0.54030230586814; + sin: 0.8414709848079; + atan: 0.78539816339745rad; + acos: 0rad; + asin: 1.5707963267949rad; + sqrt: 2.8284271247462; +} +#unit { + unit-lit: 10; + unit-arg: 10s; + unit-arg2: 10em; + unit-math: 7.407%; +} diff --git a/vendor/leafo/lessphp/tests/outputs/colors.css b/vendor/leafo/lessphp/tests/outputs/colors.css new file mode 100644 index 000000000..5310ffb08 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/colors.css @@ -0,0 +1,103 @@ +body { + color: #ff0000; + color: #ffffff; + color: #000000; + color: #996d33; + color: rgba(153,109,51,0.3); + lighten1: #ffffff; + lighten2: #7c8df2; + lighten3: rgba(222,140,129,0.5); + darken1: #d6d6d6; + darken2: #0d1e81; + darken3: rgba(18,42,185,0.5); + saturate1: #f1eded; + saturate2: #0025fe; + saturate3: rgba(10,44,244,0.5); + desaturate1: #efefef; + desaturate2: #3349cb; + desaturate3: rgba(36,62,218,0.5); + spin1: #efefef; + spin2: #2d17e7; + spin3: rgba(59,23,231,0.5); + spin2: #efefef; + spin3: #1769e7; + spin4: rgba(23,119,231,0.5); + one1: #abcdef; + one2: #abcdef; + two1: rgba(2,159,35,0.9); + two2: rgba(2,159,35,0.9); + three1: rgba(1,2,3,0.6); + three2: rgba(1,2,3,0.6); + four1: rgba(1,2,3,0); + four2: rgba(1,2,3,0); + hue: 282; + sat: 33; + lit: 12; + what: #dff1da; + zero1: #343434; + zero2: #003468; + zero3: #000000; + zero4: #ffffff; + zero5: #000000; + zero6: #ffffff; + zero7: #ffffff; + zero8: #ffffff; + zero9: #1d5612; + zeroa: #56124b; + zerob: #000000; + zeroc: #ffffff; +} +alpha { + g1: 0; + g2: 1; +} +fade { + f1: rgba(255,0,0,0.5); + f2: rgba(255,255,255,0.2); + f3: rgba(34,23,64,0.5); +} +.mix { + color1: #808080; + color2: #808080; + color3: rgba(6,3,2,-0.25); +} +.contrast { + color1: #ff0000; + color2: #0000ff; +} +.percent { + per: 50%; +} +.colorz { + color1: #ebebeb; + color2: #ff9100; + color3: #000000; +} +body { + start: #fcf8e3; + spin: #fcf4e3; + chained: #fbeed5; + direct: #fbefd5; +} +pre { + spin: #f2dee1; +} +dd { + background-color: #f5f5f5; +} +.ops { + c1: #4d0000; + c2: #004000; + c3: #020002; + c4: #ffffff; + c5: #fb8173; + c6: 132 / #ff0000; + c7: 132 - #ff0000; + c8: 132- #ff0000; +} +.transparent { + r: 0; + g: 0; + b: 0; + a: 0; +} diff --git a/vendor/leafo/lessphp/tests/outputs/compile_on_mixin.css b/vendor/leafo/lessphp/tests/outputs/compile_on_mixin.css new file mode 100644 index 000000000..318526a5e --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/compile_on_mixin.css @@ -0,0 +1,29 @@ +body { + height: 22px; +} +body ul { + height: 20px; +} +body ul li { + height: 10px; +} +body ul li div span, +body ul li link { + margin: 10px; + color: red; +} +body ul div, +body ul p { + border: 1px; +} +body ul div.hello, +body ul p.hello { + color: green; +} +body ul div :what, +body ul p :what { + color: blue; +} +body ul a b { + color: blue; +} diff --git a/vendor/leafo/lessphp/tests/outputs/data-uri.css b/vendor/leafo/lessphp/tests/outputs/data-uri.css new file mode 100644 index 000000000..e51b8f9c9 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/data-uri.css @@ -0,0 +1,6 @@ +.small { + background: url("data:text/plain;base64,CmRpdjpiZWZvcmUgewoJY29udGVudDogImhpISI7Cn0KCg=="); +} +.large { + background: url("../../../lessc.inc.php"); +} diff --git a/vendor/leafo/lessphp/tests/outputs/directives.css b/vendor/leafo/lessphp/tests/outputs/directives.css new file mode 100644 index 000000000..c2d82b833 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/directives.css @@ -0,0 +1,27 @@ +@charset "utf-8"; +@-moz-document url-prefix() { + div { + color: red; + } +} +@page :left { + margin-left: 4cm; +} +@page :right { + margin-left: 3cm; +} +@page { + margin: 2cm; +} +@-ms-viewport { + width: device-width; +} +@-moz-viewport { + width: device-width; +} +@-o-viewport { + width: device-width; +} +@viewport { + width: device-width; +} diff --git a/vendor/leafo/lessphp/tests/outputs/escape.css b/vendor/leafo/lessphp/tests/outputs/escape.css new file mode 100644 index 000000000..0587bcab6 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/escape.css @@ -0,0 +1,14 @@ +body { + e1: this is simple; + e2: this is simple; + e3: 1232; + e4: world; + e5: onemore; + t1: eating rice; + t2: string cheese; + t3: a b c string me d e f; + t4: string world; +} +.class { + filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='image.png'); +} diff --git a/vendor/leafo/lessphp/tests/outputs/font_family.css b/vendor/leafo/lessphp/tests/outputs/font_family.css new file mode 100644 index 000000000..fc260fd4d --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/font_family.css @@ -0,0 +1,17 @@ +@font-face { + font-family: Graublau Sans Web; + src: url(fonts/GraublauWeb.otf) format("opentype"); +} +@font-face { + font-family: Gentium; + src: url('fonts/Gentium.ttf'); +} +@font-face { + font-family: Gentium; + src: url("fonts/GentiumItalic.ttf"); + font-style: italic; +} +h2 { + font-family: Gentium; + crazy: maroon; +} diff --git a/vendor/leafo/lessphp/tests/outputs/guards.css b/vendor/leafo/lessphp/tests/outputs/guards.css new file mode 100644 index 000000000..34af5495a --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/guards.css @@ -0,0 +1,27 @@ +dd { + simple: yellow; +} +b { + something: red; + something-complex: blue cool; + something-complex: blue birthday; +} +img { + another: green; + flipped: teal; +} +body { + yeah-number: purple 232px; + yeah-pixel: silver; + yeah-number: purple 232; +} +div { + something-complex: blue true; +} +link { + color: true red; + color: true #fff; + color: true #fffddd; + color: true #000000; + color: true rgba(0,0,0,0.34); +} diff --git a/vendor/leafo/lessphp/tests/outputs/hacks.css b/vendor/leafo/lessphp/tests/outputs/hacks.css new file mode 100644 index 000000000..b8327eb51 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/hacks.css @@ -0,0 +1,4 @@ +:root .alert-message, +:root .btn { + border-radius: 0 \0; +} diff --git a/vendor/leafo/lessphp/tests/outputs/hi.css b/vendor/leafo/lessphp/tests/outputs/hi.css new file mode 100644 index 000000000..3bffcf0b0 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/hi.css @@ -0,0 +1,3 @@ +div:before { + content: "hi!"; +} diff --git a/vendor/leafo/lessphp/tests/outputs/ie.css b/vendor/leafo/lessphp/tests/outputs/ie.css new file mode 100644 index 000000000..7e4571c89 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/ie.css @@ -0,0 +1,9 @@ +foo { + filter: progid:DXImageTransform.Microsoft.gradient(GradientType=1,startColorstr=#c0ff3300,endColorstr=#ff000000); + filter: progid:DXImageTransform.Microsoft.gradient(GradientType=1,startColorstr=#c0ff3300,endColorstr=#ff000001); +} +foo { + filter: alpha(opacity=20); + filter: alpha(opacity=20,enabled=true); + filter: blaznicate(foo=bar,baz=bang bip,bart=#fa4600); +} diff --git a/vendor/leafo/lessphp/tests/outputs/import.css b/vendor/leafo/lessphp/tests/outputs/import.css new file mode 100644 index 000000000..b76c25b51 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/import.css @@ -0,0 +1,51 @@ +@import "not-found"; +@import "something.css" media; +@import url("something.css") media; +@import url(something.css) media, screen, print; +b { + color: maroon; + padding: 16px; +} +body { + line-height: 10em; +} +body div.bright { + color: red; +} +body div.sad { + color: blue; +} +.one { + color: blue; +} +#merge-import-mixins .just-a-class { + background: red; +} +#merge-import-mixins .just-a-class { + background: blue; +} +#merge-import-mixins .hello { + height: 200px; +} +@media cool { + #merge-import-mixins { + color: red; + height: 200px; + } +} +#merge-import-mixins div { + background: red; + background: blue; +} +.inner { + content: "inner/file1.less"; +} +.inner { + content: "inner/file2.less"; +} +pre { + color: hello-from-file-3; +} +h2 { + background: url("../images/logo.png") no-repeat; +} diff --git a/vendor/leafo/lessphp/tests/outputs/interpolation.css b/vendor/leafo/lessphp/tests/outputs/interpolation.css new file mode 100644 index 000000000..c3ff1f42a --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/interpolation.css @@ -0,0 +1,28 @@ +div { + interp1: yes; + interp2: yes; + interp3: okay; +} +10"yeah" { + color: blue; +} +10 { + color: blue; +} +hello world 10 { + color: red; +} +#"yeah" { + color: "hello 10"; +} +[prop], +[prop="value3"], +[prop*="val3"], +[|prop~="val3"], +[*|prop$="val3"], +[ns|prop^="val3"], +[3^="val3"], +[3=3], +[3] { + attributes: yes; +} diff --git a/vendor/leafo/lessphp/tests/outputs/keyframes.css b/vendor/leafo/lessphp/tests/outputs/keyframes.css new file mode 100644 index 000000000..cf62be113 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/keyframes.css @@ -0,0 +1,48 @@ +@keyframes 'bounce' { + from { + top: 100px; + animation-timing-function: ease-out; + } + 25% { + top: 50px; + animation-timing-function: ease-in; + } + 50% { + top: 100px; + animation-timing-function: ease-out; + } + 75% { + top: 75px; + animation-timing-function: ease-in; + } + to { + top: 100px; + } +} +@-webkit-keyframes flowouttoleft { + 0% { + -webkit-transform: translateX(0) scale(1); + } + 60%, + 70% { + -webkit-transform: translateX(0) scale(.7); + } + 100% { + -webkit-transform: translateX(-100%) scale(.7); + } +} +div { + animation-name: 'diagonal-slide'; + animation-duration: 5s; + animation-iteration-count: 10; +} +@keyframes 'diagonal-slide' { + from { + left: 0; + top: 0; + } + to { + left: 100px; + top: 100px; + } +} diff --git a/vendor/leafo/lessphp/tests/outputs/math.css b/vendor/leafo/lessphp/tests/outputs/math.css new file mode 100644 index 000000000..8d425f30d --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/math.css @@ -0,0 +1,69 @@ +.unary { + sub: 10 -5; +} +.spaces { + sub1: 5; + sub2: 5; + add1: 15; + add2: 15; + div: 2; + mul1: 50; + mul2: 50; +} +.supress-division { + border-radius: 10px/10px; + border-radius: 10px/12px; + border-radius: hello(10px/10px) world; + font: 10px/30 sans-serif; + font: 10px/20px sans-serif; + font: 10px/22px sans-serif; + border-radius: 0 15px 15px 15px/0 50% 50% 50%; +} +.parens { + sub: 5; + add: 15; + div1: 2; + div2: 2; + mul: 50; +} +.keyword-names { + height: "hello" 25; +} +.negation { + neg1: -1px; + neg2: -1px; + neg3: -10; +} +.test { + single1: 5; + single2: 10; + single3: 10; + parens: 10 -2; + math1: 20; + math2: 20; + complex1: 71; + complex2: 6; + complex3: 6px 1em 2px 2; + var1: 8 4 4 4px; + var2: 96; + var3: 12; + complex4: 113; +} +.percents { + p1: 1000%; + p2: 1000%; + p3: 100%; + p4: 1000px; + p5: 1000%; + p6: 30%; + p7: 10%; + p8: 2%; +} +.misc { + x: 40px; + y: 40em; +} +.cond { + c1: false; + c2: true; +} diff --git a/vendor/leafo/lessphp/tests/outputs/media.css b/vendor/leafo/lessphp/tests/outputs/media.css new file mode 100644 index 000000000..99da8c315 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/media.css @@ -0,0 +1,70 @@ +@media screen,3D { + P { + color: green; + } +} +@media print { + body { + font-size: 10pt; + } +} +@media screen { + body { + font-size: 13px; + } +} +@media screen,print { + body { + line-height: 1.2; + } +} +@media all and (min-width: 0px) { + body { + line-height: 1.2; + } +} +@media all and (min-width: 0) { + body { + line-height: 1.2; + } +} +@media screen and (min-width: 102.5em) and (max-width: 117.9375em),screen and (min-width: 150em) { + body { + color: blue; + } +} +@media screen and (min-height: 110px) { + body { + color: red; + } +} +@media screen and (height: 100px) and (width: 110px),(size: 120px) { + body { + color: red; + } +} +@media test { + div { + height: 20px; + } +} +@media test and (hello) { + div { + color: red; + } + div pre { + color: orange; + } +} +@media yeah { + @page { + @media cool { + color: red; + } + } +} +@media (max-width: 599px) { + .helloworld { + color: blue; + } +} diff --git a/vendor/leafo/lessphp/tests/outputs/misc.css b/vendor/leafo/lessphp/tests/outputs/misc.css new file mode 100644 index 000000000..6c99cc392 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/misc.css @@ -0,0 +1,68 @@ +color: "aaa, bbb"; +.topbar { + background: url(/assets/images/test/topbar.png); +} +.hello { + test: empty-function("/assets/images/test/",40%,to(#fff)); +} +.css3 { + background-image: -webkit-gradient(linear,0% 0%,0% 90%,from(#E9A000),to(#A37000)); +} +.test, +.world { + border: 1px solid red; + color: url(http://mage-page.com); + string: "hello /* this is not a comment */"; + world: "// neither is this"; + string: 'hello /* this is not a comment */'; + world: '// neither is this'; + what-ever: 100px; + background: url(/*no comment here*/); +} +.urls { + background1: url("http://google.com"); + background2: url(http://google.com); + background3: url("http://google.com"); +} +.cool { + color: "aaa, bbb"; +} +.span-17 { + float: left; +} +.span-17 { + width: 660px; +} +.x { + float: left; + width: 660px; +} +.hi pre { + color: red; +} +.hi pre { + color: blue; +} +.rad pre { + color: red; +} +.rad pre { + color: blue; +} +hello { + numbers: 1.0 0.1 .1 1.; + numbers: 1.0s 0.1s .1s 1.s; + numbers: -1s -0.1s -0.1s -1s; + numbers: -1 -0.1 -0.1 -1; +} +#string { + hello: 'what\'s going on here'; + hello: 'blah blag @{ blah blah'; + join: "3434hello"; + join: 3434hello; +} +.duplicates { + hello: world; + hello: "world"; + hello: "what"; +} diff --git a/vendor/leafo/lessphp/tests/outputs/mixin_functions.css b/vendor/leafo/lessphp/tests/outputs/mixin_functions.css new file mode 100644 index 000000000..537858034 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/mixin_functions.css @@ -0,0 +1,7 @@ +body { + padding: 2.0em; + color: red; + margin: 10px; + height: 12px; + border-bottom: 1px solid green; +} diff --git a/vendor/leafo/lessphp/tests/outputs/mixin_merging.css b/vendor/leafo/lessphp/tests/outputs/mixin_merging.css new file mode 100644 index 000000000..f396ba927 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/mixin_merging.css @@ -0,0 +1,42 @@ +#test1 div { + color:red; + height:10px; +} +#test1 p { height:10px; } +#test2 b { + color:red; + width:1px; +} +#test2 a, #test2 i { width:1px; } +#test3 a, #test3 i { width:1px; } +#test3 b { + width:1px; + color:red; +} +#test4 a { + color:blue; + margin:1px; +} +#test4 div, #test4 html { color:blue; } +#test5 img, #test5 strong { + padding:2px; + float:right; +} +#test6 div a, #test6 span a { + line-height:10px; + color:red; +} +#test7 div strong { + margin:1px; + color:red; +} +#test7 div b { color:red; } +#test7 span strong, #test7 span b { color:red; } +#test8 a i, #test8 b i { + background:red; + color:red; +} +#test8 a s, #test8 b s { + background:red; + color:blue; +} diff --git a/vendor/leafo/lessphp/tests/outputs/mixins.css b/vendor/leafo/lessphp/tests/outputs/mixins.css new file mode 100644 index 000000000..cce87eb47 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/mixins.css @@ -0,0 +1,92 @@ +.bold { + font-size: 20px; + font-weight: bold; +} +body #window { + border-radius: 10px; + font-size: 20px; + font-weight: bold; + line-height: 30px; +} +#bundle .button { + display: block; + border: 1px solid black; + background-color: grey; +} +#bundle .button:hover { + background-color: white; +} +#header a { + color: orange; + display: block; + border: 1px solid black; + background-color: grey; +} +#header a:hover { + background-color: white; +} +div { + color: blue; + hello: world; +} +div b { + color: blue; +} +body { + color: blue; + hello: world; +} +body b { + color: blue; +} +.hello .world { + color: blue; +} +.foobar { + color: blue; +} +.eggs { + foo: 1px 2px; + bar: 1px 2px; + foo: 100 land; + bar: 100 land; +} +#hello { + cool: one two three cool; +} +#hello-important { + cool: one two three cool !important; +} +#world { + cool: "world"; +} +#another { + things: red blue green; + things: red blue green skip me; +} +#day .cool { + color: one two three; +} +#day .cool { + color: one two three skip me; +} +.mix-suffix { + color: red !important; + height: 20px !important; +} +.mix-suffix pre { + color: red; +} +.search-test { + color: #f00 !important; + color: #0f0 !important; +} +.cowboy { + color: blue; +} +.nav .nav-divider { + padding: 10px; +} +.nav-divider { + padding: 10px; +} diff --git a/vendor/leafo/lessphp/tests/outputs/nested.css b/vendor/leafo/lessphp/tests/outputs/nested.css new file mode 100644 index 000000000..454dcfcc2 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/nested.css @@ -0,0 +1,51 @@ +#header { + color: black; +} +#header .navigation { + font-size: 12px; +} +#header .navigation .border .outside { + color: blue; +} +#header .logo { + width: 300px; +} +#header .logo:hover { + text-decoration: none; +} +a b ul li { + color: green; +} +div .cool { + color: green; +} +p .cool span { + color: yellow; +} +div another { + color: green; +} +p another span { + color: yellow; +} +b .something { + color: blue; +} +b.something { + color: blue; +} +.foo .bar .qux, +.foo .baz .qux { + display: block; +} +.qux .foo .bar, +.qux .foo .baz { + display: inline; +} +.qux .foo .bar .biz, +.qux .foo .baz .biz { + display: none; +} +b hello [x="&yeah"] { + color: red; +} diff --git a/vendor/leafo/lessphp/tests/outputs/nesting.css b/vendor/leafo/lessphp/tests/outputs/nesting.css new file mode 100644 index 000000000..804a56bff --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/nesting.css @@ -0,0 +1,6 @@ +#header .navigation .border .outside { color:blue; } +#header .navigation { font-size:12px; } +#header .logo:hover { text-decoration:none; } +#header .logo { width:300px; } +#header { color:black; } +a b ul li { color:green; } diff --git a/vendor/leafo/lessphp/tests/outputs/pattern_matching.css b/vendor/leafo/lessphp/tests/outputs/pattern_matching.css new file mode 100644 index 000000000..215371b0a --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/pattern_matching.css @@ -0,0 +1,72 @@ +.class { + color: #a2a2a2; + display: block; +} +.zero { + zero: 0; + one: 1; + two: 2; + three: 3; +} +.one { + one: 1; + one-req: 1; + two: 2; + three: 3; +} +.two { + two: 2; + three: 3; +} +.three { + three-req: 3; + three: 3; +} +.left { + left: 1; +} +.right { + right: 1; +} +.border-right { + color: black; + border-right: 4px; +} +.border-left { + color: black; + border-left: 4px; +} +.only-right { + right: 33; +} +.only-left { + left: 33; +} +.left-right { + both: 330; +} +#hola { + color: blue; +} +#defaults_1 { + height: one; + height: two; + height: three; + height: four; +} +.thing { + color: red; +} +#aa { + color: green; + color: blue; + color: red; +} +#bb { + color: green; + color: blue; + color: red; +} +#cc { + color: blue; +} diff --git a/vendor/leafo/lessphp/tests/outputs/scopes.css b/vendor/leafo/lessphp/tests/outputs/scopes.css new file mode 100644 index 000000000..ea2a45731 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/scopes.css @@ -0,0 +1,11 @@ +body div other world { + height: 50; +} +div other world { + height: 50; +} +pre { + height: 10; + height: 11; + height: 12; +} diff --git a/vendor/leafo/lessphp/tests/outputs/selector_expressions.css b/vendor/leafo/lessphp/tests/outputs/selector_expressions.css new file mode 100644 index 000000000..71d4a5d83 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/selector_expressions.css @@ -0,0 +1,25 @@ +something blue, +world { + color: blue; +} +.div (3434) { + height: 100px; +} +.div cool red { + height: 4000px; +} +.span5 { + color: 15; +} +.span4 { + color: 14; +} +.span3 { + color: 13; +} +.span2 { + color: 12; +} +.span1 { + color: 11; +} diff --git a/vendor/leafo/lessphp/tests/outputs/site_demos.css b/vendor/leafo/lessphp/tests/outputs/site_demos.css new file mode 100644 index 000000000..3428ad3aa --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/site_demos.css @@ -0,0 +1,76 @@ +default .underline { + border-bottom: 1px solid green; +} +default #header { + color: black; + border: 1px solid #dd44dd; +} +default #header .navigation { + font-size: 12px; +} +default #header .navigation a { + border-bottom: 1px solid green; +} +default #header .logo { + width: 300px; +} +default #header .logo:hover { + text-decoration: none; +} +variables .variables { + width: 14cm; + height: 24px; + color: #888; + background: #6c94be; + font-family: "Trebuchet MS", Verdana, sans-serif; +} +mixins .bordered { + border-top: dotted 1px black; + border-bottom: solid 2px black; +} +mixins #menu a { + color: #111; + border-top: dotted 1px black; + border-bottom: solid 2px black; +} +mixins .post a { + color: red; + border-top: dotted 1px black; + border-bottom: solid 2px black; +} +nested-rules #header { + color: black; +} +nested-rules #header .navigation { + font-size: 12px; +} +nested-rules #header .logo { + width: 300px; +} +nested-rules #header .logo:hover { + text-decoration: none; +} +namespaces #bundle .button { + display: block; + border: 1px solid black; + background-color: grey; +} +namespaces #bundle .button:hover { + background-color: white; +} +namespaces #header a { + color: orange; + display: block; + border: 1px solid black; + background-color: grey; +} +namespaces #header a:hover { + background-color: white; +} +mixin-functions body { + padding: 2.0em; + color: red; + margin: 10px; + height: 12px; + border-bottom: 1px solid green; +} diff --git a/vendor/leafo/lessphp/tests/outputs/variables.css b/vendor/leafo/lessphp/tests/outputs/variables.css new file mode 100644 index 000000000..c4857cc69 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs/variables.css @@ -0,0 +1,19 @@ +outer1: 44px; +outer2: 44px; +.variables { + width: 14cm; + height: 24px; + margin-top: -20px; + margin-bottom: 30px; + color: #888899; + background: #6c94be; + font-family: "Trebuchet MS", Verdana, sans-serif; + margin: 3px; + font: 10px/12px serif; + font: 120%/120% serif; +} +.external { + color: #888; + border: 1px solid #3326cc; + background: rgba(23,68,149,0.5); +} diff --git a/vendor/leafo/lessphp/tests/outputs_lessjs/mixins-args.css b/vendor/leafo/lessphp/tests/outputs_lessjs/mixins-args.css new file mode 100644 index 000000000..0a8e6bee7 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs_lessjs/mixins-args.css @@ -0,0 +1,113 @@ +#hidden { + color: transparent; +} +#hidden1 { + color: transparent; +} +.two-args { + color: blue; + width: 10px; + height: 99%; + border: 2px dotted black; +} +.one-arg { + width: 15px; + height: 49%; +} +.no-parens { + width: 5px; + height: 49%; +} +.no-args { + width: 5px; + height: 49%; +} +.var-args { + width: 45; + height: 17%; +} +.multi-mix { + width: 10px; + height: 29%; + margin: 4; + padding: 5; +} +body { + padding: 30px; + color: #f00; +} +.scope-mix { + width: 8; +} +.content { + width: 600px; +} +.content .column { + margin: 600px; +} +#same-var-name { + radius: 5px; +} +#var-inside { + width: 10px; +} +.arguments { + border: 1px solid black; + width: 1px; +} +.arguments2 { + border: 0px; + width: 0px; +} +.arguments3 { + border: 0px; + width: 0px; +} +.arguments4 { + border: 0 1 2 3 4; + rest: 1 2 3 4; + width: 0; +} +.edge-case { + border: "{"; + width: "{"; +} +.slash-vs-math { + border-radius: 0.4px; + border-radius: 0.5px; + border-radius: 6px; +} +.comma-vs-semi-colon { + one: a; + two: b, c; + one: d, e; + two: f; + one: g; + one: h; + one: i; + one: j; + one: k; + two: l; + one: m, n; + one: o, p; + two: q; + one: r, s; + two: t; +} +#named-conflict { + four: a, 11, 12, 13; + four: a, 21, 22, 23; +} +.test-mixin-default-arg { + defaults: 1px 1px 1px; + defaults: 2px 2px 2px; +} +.selector { + margin: 2, 2, 2, 2; +} +.selector2 { + margin: 2, 2, 2, 2; +} +.selector3 { + margin: 4; +} diff --git a/vendor/leafo/lessphp/tests/outputs_lessjs/mixins-named-args.css b/vendor/leafo/lessphp/tests/outputs_lessjs/mixins-named-args.css new file mode 100644 index 000000000..e460aa104 --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs_lessjs/mixins-named-args.css @@ -0,0 +1,27 @@ +.named-arg { + color: blue; + width: 5px; + height: 99%; + args: 1px 100%; + text-align: center; +} +.class { + width: 5px; + height: 19%; + args: 1px 20%; +} +.all-args-wrong-args { + width: 10px; + height: 9%; + args: 2px 10%; +} +.named-args2 { + width: 15px; + height: 49%; + color: #646464; +} +.named-args3 { + width: 5px; + height: 29%; + color: #123456; +} diff --git a/vendor/leafo/lessphp/tests/outputs_lessjs/strings.css b/vendor/leafo/lessphp/tests/outputs_lessjs/strings.css new file mode 100644 index 000000000..f7b9c27df --- /dev/null +++ b/vendor/leafo/lessphp/tests/outputs_lessjs/strings.css @@ -0,0 +1,40 @@ +#strings { + background-image: url("http://son-of-a-banana.com"); + quotes: "~" "~"; + content: "#*%:&^,)!.(~*})"; + empty: ""; + brackets: "{" "}"; + escapes: "\"hello\" \\world"; + escapes2: "\"llo"; +} +#comments { + content: "/* hello */ // not-so-secret"; +} +#single-quote { + quotes: "'" "'"; + content: '""#!&""'; + empty: ''; + semi-colon: ';'; +} +#escaped { + filter: DX.Transform.MS.BS.filter(opacity=50); +} +#one-line { + image: url(http://tooks.com); +} +#crazy { + image: url(http://), "}", url("http://}"); +} +#interpolation { + url: "http://lesscss.org/dev/image.jpg"; + url2: "http://lesscss.org/image-256.jpg"; + url3: "http://lesscss.org#445566"; + url4: "http://lesscss.org/hello"; + url5: "http://lesscss.org/54.4px"; +} +.mix-mul-class { + color: blue; + color: red; + color: black; + color: orange; +} diff --git a/vendor/leafo/lessphp/tests/sort.php b/vendor/leafo/lessphp/tests/sort.php new file mode 100644 index 000000000..70b907eac --- /dev/null +++ b/vendor/leafo/lessphp/tests/sort.php @@ -0,0 +1,57 @@ +coerceColor($value); + } + + return parent::compileValue($value); + } +} + +class SortingFormatter extends lessc_formatter_lessjs { + function sortKey($block) { + if (!isset($block->sortKey)) { + sort($block->selectors, SORT_STRING); + $block->sortKey = implode(",", $block->selectors); + } + + return $block->sortKey; + } + + function sortBlock($block) { + usort($block->children, function($a, $b) { + $sort = strcmp($this->sortKey($a), $this->sortKey($b)); + if ($sort == 0) { + // TODO + } + return $sort; + }); + + } + + function block($block) { + $this->sortBlock($block); + return parent::block($block); + } + +} + +$less = new lesscNormalized(); +$less->setFormatter(new SortingFormatter); +echo $less->parse(file_get_contents($fname)); + diff --git a/vendor/leafo/scssphp/LICENSE.md b/vendor/leafo/scssphp/LICENSE.md new file mode 100644 index 000000000..2f5412f9b --- /dev/null +++ b/vendor/leafo/scssphp/LICENSE.md @@ -0,0 +1,20 @@ +Copyright (c) 2015 Leaf Corcoran, http://leafo.github.io/scssphp + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/leafo/scssphp/README.md b/vendor/leafo/scssphp/README.md new file mode 100644 index 000000000..53a4c7a41 --- /dev/null +++ b/vendor/leafo/scssphp/README.md @@ -0,0 +1,47 @@ +# scssphp +### + +[![Build](https://travis-ci.org/leafo/scssphp.svg?branch=master)](http://travis-ci.org/leafo/scssphp) +[![License](https://poser.pugx.org/leafo/scssphp/license.svg)](https://packagist.org/packages/leafo/scssphp) + +`scssphp` is a compiler for SCSS written in PHP. + +Checkout the homepage, , for directions on how to use. + +## Running Tests + +`scssphp` uses [PHPUnit](https://github.com/sebastianbergmann/phpunit) for testing. + +Run the following command from the root directory to run every test: + + vendor/bin/phpunit tests + +There are several tests in the `tests/` directory: + +* `ApiTest.php` contains various unit tests that test the PHP interface. +* `ExceptionTest.php` contains unit tests that test for exceptions thrown by the parser and compiler. +* `FailingTest.php` contains tests reported in Github issues that demonstrate compatibility bugs. +* `InputTest.php` compiles every `.scss` file in the `tests/inputs` directory + then compares to the respective `.css` file in the `tests/outputs` directory. +* `ScssTest.php` extracts (ruby) `scss` tests from the `tests/scss_test.rb` file. +* `ServerTest.php` contains functional tests for the `Server` class. + +When changing any of the tests in `tests/inputs`, the tests will most likely +fail because the output has changed. Once you verify that the output is correct +you can run the following command to rebuild all the tests: + + BUILD=1 vendor/bin/phpunit tests + +This will compile all the tests, and save results into `tests/outputs`. + +To enable the `scss` compatibility tests: + + TEST_SCSS_COMPAT=1 vendor/bin/phpunit tests + +## Coding Standard + +`scssphp` source conforms to [PSR2](http://www.php-fig.org/psr/psr-2/). + +Run the following command from the root directory to check the code for "sniffs". + + vendor/bin/phpcs --standard=PSR2 bin src tests diff --git a/vendor/leafo/scssphp/bin/pscss b/vendor/leafo/scssphp/bin/pscss new file mode 100755 index 000000000..f4b7ba8f1 --- /dev/null +++ b/vendor/leafo/scssphp/bin/pscss @@ -0,0 +1,182 @@ +#!/usr/bin/env php +parse($data)), true)); + + exit(); +} + +$scss = new Compiler(); + +if ($debugInfo && $inputFile) { + $scss->setLineNumberStyle(Compiler::DEBUG_INFO); +} + +if ($lineNumbers && $inputFile) { + $scss->setLineNumberStyle(Compiler::LINE_COMMENTS); +} + +if ($loadPaths) { + $scss->setImportPaths(explode(PATH_SEPARATOR, $loadPaths)); +} + +if ($precision) { + $scss->setNumberPrecision($precision); +} + +if ($style) { + $scss->setFormatter('Leafo\\ScssPhp\\Formatter\\' . ucfirst($style)); +} + +echo $scss->compile($data, $inputFile); + +if ($changeDir) { + chdir($oldWorkingDir); +} diff --git a/vendor/leafo/scssphp/classmap.php b/vendor/leafo/scssphp/classmap.php new file mode 100644 index 000000000..2d8a52b8e --- /dev/null +++ b/vendor/leafo/scssphp/classmap.php @@ -0,0 +1,63 @@ +=5.3.0" + }, + "require-dev": { + "squizlabs/php_codesniffer": "~2.3", + "phpunit/phpunit": "~3.7", + "kherge/box": "~2.5" + }, + "bin": ["bin/pscss"], + "archive": { + "exclude": [ + "/Makefile", + "/.gitattributes", + "/.gitignore", + "/.travis.yml", + "/box.json.dist", + "/phpunit.xml.dist", + "/tests" + ] + } +} diff --git a/vendor/leafo/scssphp/scss.inc.php b/vendor/leafo/scssphp/scss.inc.php new file mode 100644 index 000000000..d28711552 --- /dev/null +++ b/vendor/leafo/scssphp/scss.inc.php @@ -0,0 +1,28 @@ + + */ +class Range +{ + public $first; + public $last; + + /** + * Initialize range + * + * @param integer|float $first + * @param integer|float $last + */ + public function __construct($first, $last) + { + $this->first = $first; + $this->last = $last; + } + + /** + * Test for inclusion in range + * + * @param integer|float $value + * + * @return boolean + */ + public function includes($value) + { + return $value >= $this->first && $value <= $this->last; + } +} diff --git a/vendor/leafo/scssphp/src/Block.php b/vendor/leafo/scssphp/src/Block.php new file mode 100644 index 000000000..16f4a9683 --- /dev/null +++ b/vendor/leafo/scssphp/src/Block.php @@ -0,0 +1,55 @@ + + */ +class Block +{ + /** + * @var string + */ + public $type; + + /** + * @var \Leafo\ScssPhp\Block + */ + public $parent; + + /** + * @var integer + */ + public $sourcePosition; + + /** + * @var integer + */ + public $sourceIndex; + + /** + * @var array + */ + public $selectors; + + /** + * @var array + */ + public $comments; + + /** + * @var array + */ + public $children; +} diff --git a/vendor/leafo/scssphp/src/Colors.php b/vendor/leafo/scssphp/src/Colors.php new file mode 100644 index 000000000..0227fbe0f --- /dev/null +++ b/vendor/leafo/scssphp/src/Colors.php @@ -0,0 +1,178 @@ + + */ +class Colors +{ + /** + * CSS Colors + * + * @see http://www.w3.org/TR/css3-color + * + * @var array + */ + public 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', + 'transparent' => '0,0,0,0', + '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' + ); +} diff --git a/vendor/leafo/scssphp/src/Compiler.php b/vendor/leafo/scssphp/src/Compiler.php new file mode 100644 index 000000000..7cf114650 --- /dev/null +++ b/vendor/leafo/scssphp/src/Compiler.php @@ -0,0 +1,4924 @@ + + */ +class Compiler +{ + const LINE_COMMENTS = 1; + const DEBUG_INFO = 2; + + const WITH_RULE = 1; + const WITH_MEDIA = 2; + const WITH_SUPPORTS = 4; + const WITH_ALL = 7; + + /** + * @var array + */ + static protected $operatorNames = array( + '+' => 'add', + '-' => 'sub', + '*' => 'mul', + '/' => 'div', + '%' => 'mod', + + '==' => 'eq', + '!=' => 'neq', + '<' => 'lt', + '>' => 'gt', + + '<=' => 'lte', + '>=' => 'gte', + '<=>' => 'cmp', + ); + + /** + * @var array + */ + static protected $namespaces = array( + 'special' => '%', + 'mixin' => '@', + 'function' => '^', + ); + + static public $true = array(Type::T_KEYWORD, 'true'); + static public $false = array(Type::T_KEYWORD, 'false'); + static public $null = array(Type::T_NULL); + static public $defaultValue = array(Type::T_KEYWORD, ''); + static public $selfSelector = array(Type::T_SELF); + static public $emptyList = array(Type::T_LIST, '', array()); + static public $emptyMap = array(Type::T_MAP, array(), array()); + static public $emptyString = array(Type::T_STRING, '"', array()); + static public $with = array(Type::T_KEYWORD, 'with'); + static public $without = array(Type::T_KEYWORD, 'without'); + + protected $importPaths = array(''); + protected $importCache = array(); + protected $userFunctions = array(); + protected $registeredVars = array(); + protected $registeredFeatures = array( + 'extend-selector-pseudoclass' => false, + 'at-error' => true, + 'units-level-3' => false, + 'global-variable-shadowing' => false, + ); + + protected $lineNumberStyle = null; + + protected $formatter = 'Leafo\ScssPhp\Formatter\Nested'; + + protected $rootEnv; + protected $rootBlock; + + private $indentLevel; + private $commentsSeen; + private $extends; + private $extendsMap; + private $parsedFiles; + private $env; + private $scope; + private $parser; + private $sourcePos; + private $sourceParsers; + private $sourceIndex; + private $storeEnv; + private $charsetSeen; + private $stderr; + private $shouldEvaluate; + + /** + * Compile scss + * + * @api + * + * @param string $code + * @param string $path + * + * @return string + */ + public function compile($code, $path = null) + { + $locale = setlocale(LC_NUMERIC, 0); + setlocale(LC_NUMERIC, 'C'); + + $this->indentLevel = -1; + $this->commentsSeen = array(); + $this->extends = array(); + $this->extendsMap = array(); + $this->parsedFiles = array(); + $this->sourceParsers = array(); + $this->sourceIndex = null; + $this->env = null; + $this->scope = null; + $this->storeEnv = null; + $this->stderr = fopen('php://stderr', 'w'); + + $this->parser = $this->parserFactory($path); + $tree = $this->parser->parse($code); + + $this->formatter = new $this->formatter(); + + $this->rootEnv = $this->pushEnv($tree); + $this->injectVariables($this->registeredVars); + $this->compileRoot($tree); + $this->popEnv(); + + $out = $this->formatter->format($this->scope); + + setlocale(LC_NUMERIC, $locale); + + return $out; + } + + /** + * Instantiate parser + * + * @param string $path + * + * @return \Leafo\ScssPhp\Parser + */ + private function parserFactory($path) + { + $parser = new Parser($path, count($this->sourceParsers)); + + $this->sourceParsers[] = $parser; + $this->addParsedFile($path); + + return $parser; + } + + /** + * Is self extend? + * + * @param array $target + * @param array $origin + * + * @return boolean + */ + protected function isSelfExtend($target, $origin) + { + foreach ($origin as $sel) { + if (in_array($target, $sel)) { + return true; + } + } + + return false; + } + + /** + * Push extends + * + * @param array $target + * @param array $origin + */ + protected function pushExtends($target, $origin) + { + if ($this->isSelfExtend($target, $origin)) { + return; + } + + $i = count($this->extends); + $this->extends[] = array($target, $origin); + + foreach ($target as $part) { + if (isset($this->extendsMap[$part])) { + $this->extendsMap[$part][] = $i; + } else { + $this->extendsMap[$part] = array($i); + } + } + } + + /** + * Make output block + * + * @param string $type + * @param array $selectors + * + * @return \Leafo\ScssPhp\Formatter\OutputBlock + */ + protected function makeOutputBlock($type, $selectors = null) + { + $out = new OutputBlock; + $out->type = $type; + $out->lines = array(); + $out->children = array(); + $out->parent = $this->scope; + $out->selectors = $selectors; + $out->depth = $this->env->depth; + + return $out; + } + + /** + * Compile root + * + * @param \Leafo\ScssPhp\Block $rootBlock + */ + protected function compileRoot(Block $rootBlock) + { + $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT); + + $this->compileChildrenNoReturn($rootBlock->children, $this->scope); + $this->flattenSelectors($this->scope); + } + + /** + * Flatten selectors + * + * @param \Leafo\ScssPhp\Formatter\OutputBlock $block + * @param string $parentKey + */ + protected function flattenSelectors(OutputBlock $block, $parentKey = null) + { + if ($block->selectors) { + $selectors = array(); + + foreach ($block->selectors as $s) { + $selectors[] = $s; + + if (! is_array($s)) { + continue; + } + + // check extends + if (! empty($this->extendsMap)) { + $this->matchExtends($s, $selectors); + + // remove duplicates + array_walk($selectors, function (&$value) { + $value = serialize($value); + }); + + $selectors = array_unique($selectors); + + array_walk($selectors, function (&$value) { + $value = unserialize($value); + }); + } + } + + $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 $key => $child) { + $this->flattenSelectors($child, $key); + } + } + + /** + * Match extends + * + * @param array $selector + * @param array $out + * @param integer $from + * @param boolean $initial + */ + protected function matchExtends($selector, &$out, $from = 0, $initial = true) + { + foreach ($selector as $i => $part) { + if ($i < $from) { + continue; + } + + if ($this->matchExtendsSingle($part, $origin)) { + $before = array_slice($selector, 0, $i); + $after = array_slice($selector, $i + 1); + $s = count($before); + + foreach ($origin as $new) { + $k = 0; + + // remove shared parts + if ($initial) { + while ($k < $s && isset($new[$k]) && $before[$k] === $new[$k]) { + $k++; + } + } + + $result = array_merge( + $before, + $k > 0 ? array_slice($new, $k) : $new, + $after + ); + + if ($result === $selector) { + continue; + } + + $out[] = $result; + + // recursively check for more matches + $this->matchExtends($result, $out, $i, false); + + // selector sequence merging + if (! empty($before) && count($new) > 1) { + $result2 = array_merge( + array_slice($new, 0, -1), + $k > 0 ? array_slice($before, $k) : $before, + array_slice($new, -1), + $after + ); + + $out[] = $result2; + } + } + } + } + } + + /** + * Match extends single + * + * @param array $rawSingle + * @param array $outOrigin + * + * @return boolean + */ + protected function matchExtendsSingle($rawSingle, &$outOrigin) + { + $counts = array(); + $single = array(); + + foreach ($rawSingle as $part) { + // matches Number + if (! is_string($part)) { + return false; + } + + if (! preg_match('/^[\[.:#%]/', $part) && count($single)) { + $single[count($single) - 1] .= $part; + } else { + $single[] = $part; + } + } + + foreach ($single as $part) { + if (isset($this->extendsMap[$part])) { + foreach ($this->extendsMap[$part] as $idx) { + $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1; + } + } + } + + $outOrigin = array(); + $found = false; + + foreach ($counts as $idx => $count) { + list($target, $origin) = $this->extends[$idx]; + + // check count + if ($count !== count($target)) { + continue; + } + + $rem = array_diff($single, $target); + + foreach ($origin as $j => $new) { + // prevent infinite loop when target extends itself + if ($this->isSelfExtend($single, $origin)) { + return false; + } + + $origin[$j][count($origin[$j]) - 1] = $this->combineSelectorSingle(end($new), $rem); + } + + $outOrigin = array_merge($outOrigin, $origin); + + $found = true; + } + + return $found; + } + + /** + * Combine selector single + * + * @param array $base + * @param array $other + * + * @return array + */ + protected function combineSelectorSingle($base, $other) + { + $tag = null; + $out = array(); + + foreach (array($base, $other) as $single) { + foreach ($single as $part) { + if (preg_match('/^[^\[.#:]/', $part)) { + $tag = $part; + } else { + $out[] = $part; + } + } + } + + if ($tag) { + array_unshift($out, $tag); + } + + return $out; + } + + /** + * Compile media + * + * @param \Leafo\ScssPhp\Block $media + */ + protected function compileMedia(Block $media) + { + $this->pushEnv($media); + + $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env)); + + if (! empty($mediaQuery)) { + $this->scope = $this->makeOutputBlock(Type::T_MEDIA, array($mediaQuery)); + + $parentScope = $this->mediaParent($this->scope); + $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 !== Type::T_BLOCK && + $type !== Type::T_MEDIA && + $type !== Type::T_DIRECTIVE && + $type !== Type::T_IMPORT + ) { + $needsWrap = true; + break; + } + } + + if ($needsWrap) { + $wrapped = new Block; + $wrapped->selectors = array(); + $wrapped->children = $media->children; + + $media->children = array(array(Type::T_BLOCK, $wrapped)); + } + + $this->compileChildrenNoReturn($media->children, $this->scope); + + $this->scope = $this->scope->parent; + } + + $this->popEnv(); + } + + /** + * Media parent + * + * @param \Leafo\ScssPhp\Formatter\OutputBlock $scope + * + * @return \Leafo\ScssPhp\Formatter\OutputBlock + */ + protected function mediaParent(OutputBlock $scope) + { + while (! empty($scope->parent)) { + if (! empty($scope->type) && $scope->type !== Type::T_MEDIA) { + break; + } + + $scope = $scope->parent; + } + + return $scope; + } + + /** + * Compile directive + * + * @param \Leafo\ScssPhp\Block $block + */ + protected function compileDirective(Block $block) + { + $s = '@' . $block->name; + + if (! empty($block->value)) { + $s .= ' ' . $this->compileValue($block->value); + } + + if ($block->name === 'keyframes' || substr($block->name, -10) === '-keyframes') { + $this->compileKeyframeBlock($block, array($s)); + } else { + $this->compileNestedBlock($block, array($s)); + } + } + + /** + * Compile at-root + * + * @param \Leafo\ScssPhp\Block $block + */ + protected function compileAtRoot(Block $block) + { + $env = $this->pushEnv($block); + $envs = $this->compactEnv($env); + $without = isset($block->with) ? $this->compileWith($block->with) : self::WITH_RULE; + + // wrap inline selector + if ($block->selector) { + $wrapped = new Block; + $wrapped->parent = $block; + $wrapped->sourcePosition = $block->sourcePosition; + $wrapped->sourceIndex = $block->sourceIndex; + $wrapped->selectors = $block->selector; + $wrapped->comments = array(); + $wrapped->children = $block->children; + + $block->children = array(array(Type::T_BLOCK, $wrapped)); + } + + $this->env = $this->filterWithout($envs, $without); + $newBlock = $this->spliceTree($envs, $block, $without); + + $saveScope = $this->scope; + $this->scope = $this->rootBlock; + + $this->compileChild($newBlock, $this->scope); + + $this->scope = $saveScope; + $this->env = $this->extractEnv($envs); + + $this->popEnv(); + } + + /** + * Splice parse tree + * + * @param array $envs + * @param \Leafo\ScssPhp\Block $block + * @param integer $without + * + * @return array + */ + private function spliceTree($envs, Block $block, $without) + { + $newBlock = null; + + foreach ($envs as $e) { + if (! isset($e->block)) { + continue; + } + + if (isset($e->block) && $e->block === $block) { + continue; + } + + if (isset($e->block->type) && $e->block->type === Type::T_AT_ROOT) { + continue; + } + + if (($without & self::WITH_RULE) && isset($e->block->selectors)) { + continue; + } + + if (($without & self::WITH_MEDIA) && + isset($e->block->type) && $e->block->type === Type::T_MEDIA + ) { + continue; + } + + if (($without & self::WITH_SUPPORTS) && + isset($e->block->type) && $e->block->type === Type::T_DIRECTIVE && + isset($e->block->name) && $e->block->name === 'supports' + ) { + continue; + } + + $b = new Block; + + if (isset($e->block->sourcePosition)) { + $b->sourcePosition = $e->block->sourcePosition; + } + + if (isset($e->block->sourceIndex)) { + $b->sourceIndex = $e->block->sourceIndex; + } + + $b->selectors = array(); + + if (isset($e->block->comments)) { + $b->comments = $e->block->comments; + } + + if (isset($e->block->type)) { + $b->type = $e->block->type; + } + + if (isset($e->block->name)) { + $b->name = $e->block->name; + } + + if (isset($e->block->queryList)) { + $b->queryList = $e->block->queryList; + } + + if (isset($e->block->value)) { + $b->value = $e->block->value; + } + + if ($newBlock) { + $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK; + + $b->children = array(array($type, $newBlock)); + + $newBlock->parent = $b; + } elseif (count($block->children)) { + foreach ($block->children as $child) { + if ($child[0] === Type::T_BLOCK) { + $child[1]->parent = $b; + } + } + + $b->children = $block->children; + } + + $b->parent = null; + + $newBlock = $b; + } + + $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK; + + return array($type, $newBlock); + } + + /** + * Compile @at-root's with: inclusion / without: exclusion into filter flags + * + * @param array $with + * + * @return integer + */ + private function compileWith($with) + { + static $mapping = array( + 'rule' => self::WITH_RULE, + 'media' => self::WITH_MEDIA, + 'supports' => self::WITH_SUPPORTS, + 'all' => self::WITH_ALL, + ); + + // exclude selectors by default + $without = self::WITH_RULE; + + if ($this->libMapHasKey(array($with, self::$with))) { + $without = self::WITH_ALL; + + $list = $this->coerceList($this->libMapGet(array($with, self::$with))); + + foreach ($list[2] as $item) { + $keyword = $this->compileStringContent($this->coerceString($item)); + + if (array_key_exists($keyword, $mapping)) { + $without &= ~($mapping[$keyword]); + } + } + } + + if ($this->libMapHasKey(array($with, self::$without))) { + $without = 0; + + $list = $this->coerceList($this->libMapGet(array($with, self::$without))); + + foreach ($list[2] as $item) { + $keyword = $this->compileStringContent($this->coerceString($item)); + + if (array_key_exists($keyword, $mapping)) { + $without |= $mapping[$keyword]; + } + } + } + + return $without; + } + + /** + * Filter env stack + * + * @param array $envs + * @param integer $without + * + * @return \Leafo\ScssPhp\Compiler\Environment + */ + private function filterWithout($envs, $without) + { + $filtered = array(); + + foreach ($envs as $e) { + if (($without & self::WITH_RULE) && isset($e->block->selectors)) { + continue; + } + + if (($without & self::WITH_MEDIA) && + isset($e->block->type) && $e->block->type === Type::T_MEDIA + ) { + continue; + } + + if (($without & self::WITH_SUPPORTS) && + isset($e->block->type) && $e->block->type === Type::T_DIRECTIVE && + isset($e->block->name) && $e->block->name === 'supports' + ) { + continue; + } + + $filtered[] = $e; + } + + return $this->extractEnv($filtered); + } + + /** + * Compile keyframe block + * + * @param \Leafo\ScssPhp\Block $block + * @param array $selectors + */ + protected function compileKeyframeBlock(Block $block, $selectors) + { + $env = $this->pushEnv($block); + + $envs = $this->compactEnv($env); + + $this->env = $this->extractEnv(array_filter($envs, function ($e) { + return ! isset($e->block->selectors); + })); + + $this->scope = $this->makeOutputBlock($block->type, $selectors); + $this->scope->depth = 1; + $this->scope->parent->children[] = $this->scope; + + $this->compileChildrenNoReturn($block->children, $this->scope); + + $this->scope = $this->scope->parent; + $this->env = $this->extractEnv($envs); + + $this->popEnv(); + } + + /** + * Compile nested block + * + * @param \Leafo\ScssPhp\Block $block + * @param array $selectors + */ + protected function compileNestedBlock(Block $block, $selectors) + { + $this->pushEnv($block); + + $this->scope = $this->makeOutputBlock($block->type, $selectors); + $this->scope->parent->children[] = $this->scope; + + $this->compileChildrenNoReturn($block->children, $this->scope); + + $this->scope = $this->scope->parent; + + $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 Compiler::compileChild() + * + * @param \Leafo\ScssPhp\Block $block + */ + protected function compileBlock(Block $block) + { + $env = $this->pushEnv($block); + $env->selectors = $this->evalSelectors($block->selectors); + + $out = $this->makeOutputBlock(null); + + if (isset($this->lineNumberStyle) && count($env->selectors) && count($block->children)) { + $annotation = $this->makeOutputBlock(Type::T_COMMENT); + $annotation->depth = 0; + + $parser = $this->sourceParsers[$block->sourceIndex]; + $file = $parser->getSourceName(); + $line = $parser->getLineNo($block->sourcePosition); + + switch ($this->lineNumberStyle) { + case self::LINE_COMMENTS: + $annotation->lines[] = '/* line ' . $line . ', ' . $file . ' */'; + break; + + case self::DEBUG_INFO: + $annotation->lines[] = '@media -sass-debug-info{filename{font-family:"' . $file + . '"}line{font-family:' . $line . '}}'; + break; + } + + $this->scope->children[] = $annotation; + } + + $this->scope->children[] = $out; + + if (count($block->children)) { + $out->selectors = $this->multiplySelectors($env); + + $this->compileChildrenNoReturn($block->children, $out); + } + + $this->formatter->stripSemicolon($out->lines); + + $this->popEnv(); + } + + /** + * Compile root level comment + * + * @param array $block + */ + protected function compileComment($block) + { + $out = $this->makeOutputBlock(Type::T_COMMENT); + $out->lines[] = $block[1]; + $this->scope->children[] = $out; + } + + /** + * Evaluate selectors + * + * @param array $selectors + * + * @return array + */ + protected function evalSelectors($selectors) + { + $this->shouldEvaluate = false; + + $selectors = array_map(array($this, 'evalSelector'), $selectors); + + // after evaluating interpolates, we might need a second pass + if ($this->shouldEvaluate) { + $buffer = $this->collapseSelectors($selectors); + $parser = $this->parserFactory(__METHOD__); + + if ($parser->parseSelector($buffer, $newSelectors)) { + $selectors = array_map(array($this, 'evalSelector'), $newSelectors); + } + } + + return $selectors; + } + + /** + * Evaluate selector + * + * @param array $selector + * + * @return array + */ + protected function evalSelector($selector) + { + return array_map(array($this, 'evalSelectorPart'), $selector); + } + + /** + * Evaluate selector part; replaces all the interpolates, stripping quotes + * + * @param array $part + * + * @return array + */ + protected function evalSelectorPart($part) + { + foreach ($part as &$p) { + if (is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) { + $p = $this->compileValue($p); + + // force re-evaluation + if (strpos($p, '&') !== false || strpos($p, ',') !== false) { + $this->shouldEvaluate = true; + } + } elseif (is_string($p) && strlen($p) >= 2 && + ($first = $p[0]) && ($first === '"' || $first === "'") && + substr($p, -1) === $first + ) { + $p = substr($p, 1, -1); + } + } + + return $this->flattenSelectorSingle($part); + } + + /** + * Collapse selectors + * + * @param array $selectors + * + * @return string + */ + protected function collapseSelectors($selectors) + { + $parts = array(); + + foreach ($selectors as $selector) { + $output = ''; + + array_walk_recursive( + $selector, + function ($value, $key) use (&$output) { + $output .= $value; + } + ); + + $parts[] = $output; + } + + return implode(', ', $parts); + } + + /** + * Flatten selector single; joins together .classes and #ids + * + * @param array $single + * + * @return array + */ + protected function flattenSelectorSingle($single) + { + $joined = array(); + + foreach ($single as $part) { + if (empty($joined) || + ! is_string($part) || + preg_match('/[\[.:#%]/', $part) + ) { + $joined[] = $part; + continue; + } + + if (is_array(end($joined))) { + $joined[] = $part; + } else { + $joined[count($joined) - 1] .= $part; + } + } + + return $joined; + } + + /** + * Compile selector to string; self(&) should have been replaced by now + * + * @param array $selector + * + * @return string + */ + protected function compileSelector($selector) + { + if (! is_array($selector)) { + return $selector; // media and the like + } + + return implode( + ' ', + array_map( + array($this, 'compileSelectorPart'), + $selector + ) + ); + } + + /** + * Compile selector part + * + * @param arary $piece + * + * @return string + */ + protected function compileSelectorPart($piece) + { + foreach ($piece as &$p) { + if (! is_array($p)) { + continue; + } + + switch ($p[0]) { + case Type::T_SELF: + $p = '&'; + break; + + default: + $p = $this->compileValue($p); + break; + } + } + + return implode($piece); + } + + /** + * Has selector placeholder? + * + * @param array $selector + * + * @return boolean + */ + 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; + } + + /** + * Compile children and return result + * + * @param array $stms + * @param \Leafo\ScssPhp\Formatter\OutputBlock $out + * + * @return array + */ + protected function compileChildren($stms, OutputBlock $out) + { + foreach ($stms as $stm) { + $ret = $this->compileChild($stm, $out); + + if (isset($ret)) { + return $ret; + } + } + } + + /** + * Compile children and throw exception if unexpected @return + * + * @param array $stms + * @param \Leafo\ScssPhp\Formatter\OutputBlock $out + * + * @throws \Exception + */ + protected function compileChildrenNoReturn($stms, OutputBlock $out) + { + foreach ($stms as $stm) { + $ret = $this->compileChild($stm, $out); + + if (isset($ret)) { + $this->throwError('@return may only be used within a function'); + } + } + } + + /** + * Compile media query + * + * @param array $queryList + * + * @return string + */ + protected function compileMediaQuery($queryList) + { + $out = '@media'; + $first = true; + + foreach ($queryList as $query) { + $type = null; + $parts = array(); + + foreach ($query as $q) { + switch ($q[0]) { + case Type::T_MEDIA_TYPE: + if ($type) { + $type = $this->mergeMediaTypes( + $type, + array_map(array($this, 'compileValue'), array_slice($q, 1)) + ); + + if (empty($type)) { // merge failed + return null; + } + } else { + $type = array_map(array($this, 'compileValue'), array_slice($q, 1)); + } + break; + + case Type::T_MEDIA_EXPRESSION: + if (isset($q[2])) { + $parts[] = '(' + . $this->compileValue($q[1]) + . $this->formatter->assignSeparator + . $this->compileValue($q[2]) + . ')'; + } else { + $parts[] = '(' + . $this->compileValue($q[1]) + . ')'; + } + break; + + case Type::T_MEDIA_VALUE: + $parts[] = $this->compileValue($q[1]); + break; + } + } + + if ($type) { + array_unshift($parts, implode(' ', array_filter($type))); + } + + if (! empty($parts)) { + if ($first) { + $first = false; + $out .= ' '; + } else { + $out .= $this->formatter->tagSeparator; + } + + $out .= implode(' and ', $parts); + } + } + + return $out; + } + + /** + * Merge media types + * + * @param array $type1 + * @param array $type2 + * + * @return array|null + */ + protected function mergeMediaTypes($type1, $type2) + { + if (empty($type1)) { + return $type2; + } + + if (empty($type2)) { + return $type1; + } + + $m1 = ''; + $t1 = ''; + + if (count($type1) > 1) { + $m1= strtolower($type1[0]); + $t1= strtolower($type1[1]); + } else { + $t1 = strtolower($type1[0]); + } + + $m2 = ''; + $t2 = ''; + + if (count($type2) > 1) { + $m2 = strtolower($type2[0]); + $t2 = strtolower($type2[1]); + } else { + $t2 = strtolower($type2[0]); + } + + if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) { + if ($t1 === $t2) { + return null; + } + + return array( + $m1 === Type::T_NOT ? $m2 : $m1, + $m1 === Type::T_NOT ? $t2 : $t1, + ); + } + + if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) { + // CSS has no way of representing "neither screen nor print" + if ($t1 !== $t2) { + return null; + } + + return array(Type::T_NOT, $t1); + } + + if ($t1 !== $t2) { + return null; + } + + // t1 == t2, neither m1 nor m2 are "not" + return array(empty($m1)? $m2 : $m1, $t1); + } + + /** + * Compile import; returns true if the value was something that could be imported + * + * @param array $rawPath + * @param array $out + * + * @return boolean + */ + protected function compileImport($rawPath, $out) + { + if ($rawPath[0] === Type::T_STRING) { + $path = $this->compileStringContent($rawPath); + + if ($path = $this->findImport($path)) { + $this->importFile($path, $out); + + return true; + } + + return false; + } + + if ($rawPath[0] === Type::T_LIST) { + // handle a list of strings + if (count($rawPath[2]) === 0) { + return false; + } + + foreach ($rawPath[2] as $path) { + if ($path[0] !== Type::T_STRING) { + return false; + } + } + + foreach ($rawPath[2] as $path) { + $this->compileImport($path, $out); + } + + return true; + } + + return false; + } + + /** + * Compile child; returns a value to halt execution + * + * @param array $child + * @param \Leafo\ScssPhp\Formatter\OutputBlock $out + * + * @return array + */ + protected function compileChild($child, OutputBlock $out) + { + $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null; + $this->sourcePos = isset($child[Parser::SOURCE_POSITION]) ? $child[Parser::SOURCE_POSITION] : -1; + + switch ($child[0]) { + case Type::T_IMPORT: + list(, $rawPath) = $child; + + $rawPath = $this->reduce($rawPath); + + if (! $this->compileImport($rawPath, $out)) { + $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';'; + } + break; + + case Type::T_DIRECTIVE: + $this->compileDirective($child[1]); + break; + + case Type::T_AT_ROOT: + $this->compileAtRoot($child[1]); + break; + + case Type::T_MEDIA: + $this->compileMedia($child[1]); + break; + + case Type::T_BLOCK: + $this->compileBlock($child[1]); + break; + + case Type::T_CHARSET: + if (! $this->charsetSeen) { + $this->charsetSeen = true; + + $out->lines[] = '@charset ' . $this->compileValue($child[1]) . ';'; + } + break; + + case Type::T_ASSIGN: + list(, $name, $value) = $child; + + if ($name[0] === Type::T_VARIABLE) { + $flag = isset($child[3]) ? $child[3] : null; + $isDefault = $flag === '!default'; + $isGlobal = $flag === '!global'; + + if ($isGlobal) { + $this->set($name[1], $this->reduce($value), false, $this->rootEnv); + break; + } + + $shouldSet = $isDefault && + (($result = $this->get($name[1], false)) === null + || $result === self::$null); + + if (! $isDefault || $shouldSet) { + $this->set($name[1], $this->reduce($value)); + } + break; + } + + $compiledName = $this->compileValue($name); + + // handle shorthand syntax: size / line-height + if ($compiledName === 'font') { + if ($value[0] === Type::T_EXPRESSION && $value[1] === '/') { + $value = $this->expToString($value); + } elseif ($value[0] === Type::T_LIST) { + foreach ($value[2] as &$item) { + if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') { + $item = $this->expToString($item); + } + } + } + } + + // if the value reduces to null from something else then + // the property should be discarded + if ($value[0] !== Type::T_NULL) { + $value = $this->reduce($value); + + if ($value[0] === Type::T_NULL) { + break; + } + } + + $compiledValue = $this->compileValue($value); + + $out->lines[] = $this->formatter->property( + $compiledName, + $compiledValue + ); + break; + + case Type::T_COMMENT: + if ($out->type === Type::T_ROOT) { + $this->compileComment($child); + break; + } + + $out->lines[] = $child[1]; + break; + + case Type::T_MIXIN: + case Type::T_FUNCTION: + list(, $block) = $child; + + $this->set(self::$namespaces[$block->type] . $block->name, $block); + break; + + case Type::T_EXTEND: + list(, $selectors) = $child; + + foreach ($selectors as $sel) { + $results = $this->evalSelectors(array($sel)); + + foreach ($results as $result) { + // only use the first one + $result = current($result); + + $this->pushExtends($result, $out->selectors); + } + } + break; + + case Type::T_IF: + list(, $if) = $child; + + if ($this->isTruthy($this->reduce($if->cond, true))) { + return $this->compileChildren($if->children, $out); + } + + foreach ($if->cases as $case) { + if ($case->type === Type::T_ELSE || + $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond)) + ) { + return $this->compileChildren($case->children, $out); + } + } + break; + + case Type::T_EACH: + list(, $each) = $child; + + $list = $this->coerceList($this->reduce($each->list)); + + $this->pushEnv(); + + foreach ($list[2] as $item) { + if (count($each->vars) === 1) { + $this->set($each->vars[0], $item, true); + } else { + list(,, $values) = $this->coerceList($item); + + foreach ($each->vars as $i => $var) { + $this->set($var, isset($values[$i]) ? $values[$i] : self::$null, true); + } + } + + $ret = $this->compileChildren($each->children, $out); + + if ($ret) { + if ($ret[0] !== Type::T_CONTROL) { + $this->popEnv(); + + return $ret; + } + + if ($ret[1]) { + break; + } + } + } + + $this->popEnv(); + break; + + case Type::T_WHILE: + list(, $while) = $child; + + while ($this->isTruthy($this->reduce($while->cond, true))) { + $ret = $this->compileChildren($while->children, $out); + + if ($ret) { + if ($ret[0] !== Type::T_CONTROL) { + return $ret; + } + + if ($ret[1]) { + break; + } + } + } + break; + + case Type::T_FOR: + list(, $for) = $child; + + $start = $this->reduce($for->start, true); + $start = $start[1]; + $end = $this->reduce($for->end, true); + $end = $end[1]; + $d = $start < $end ? 1 : -1; + + while (true) { + if ((! $for->until && $start - $d == $end) || + ($for->until && $start == $end) + ) { + break; + } + + $this->set($for->var, new Node\Number($start, '')); + $start += $d; + + $ret = $this->compileChildren($for->children, $out); + + if ($ret) { + if ($ret[0] !== Type::T_CONTROL) { + return $ret; + } + + if ($ret[1]) { + break; + } + } + } + break; + + case Type::T_BREAK: + return array(Type::T_CONTROL, true); + + case Type::T_CONTINUE: + return array(Type::T_CONTROL, false); + + case Type::T_RETURN: + return $this->reduce($child[1], true); + + case Type::T_NESTED_PROPERTY: + list(, $prop) = $child; + + $prefixed = array(); + $prefix = $this->compileValue($prop->prefix) . '-'; + + foreach ($prop->children as $child) { + if ($child[0] === Type::T_ASSIGN) { + array_unshift($child[1][2], $prefix); + } + + if ($child[0] === Type::T_NESTED_PROPERTY) { + array_unshift($child[1]->prefix[2], $prefix); + } + + $prefixed[] = $child; + } + + $this->compileChildrenNoReturn($prefixed, $out); + break; + + case Type::T_INCLUDE: + // including a mixin + list(, $name, $argValues, $content) = $child; + + $mixin = $this->get(self::$namespaces['mixin'] . $name, false); + + if (! $mixin) { + $this->throwError("Undefined mixin $name"); + } + + $callingScope = $this->getStoreEnv(); + + // push scope, apply args + $this->pushEnv(); + $this->env->depth--; + + if (isset($content)) { + $content->scope = $callingScope; + + $this->setRaw(self::$namespaces['special'] . 'content', $content, $this->getStoreEnv()); + } + + if (isset($mixin->args)) { + $this->applyArguments($mixin->args, $argValues); + } + + $this->env->marker = 'mixin'; + + $this->compileChildrenNoReturn($mixin->children, $out); + + $this->popEnv(); + break; + + case Type::T_MIXIN_CONTENT: + $content = $this->get(self::$namespaces['special'] . 'content', false, $this->getStoreEnv()); + + if (! $content) { + $this->throwError('Expected @content inside of mixin'); + } + + if (! isset($content->children)) { + break; + } + + $storeEnv = $this->storeEnv; + $this->storeEnv = $content->scope; + + $this->compileChildrenNoReturn($content->children, $out); + + $this->storeEnv = $storeEnv; + break; + + case Type::T_DEBUG: + list(, $value) = $child; + + $line = $this->parser->getLineNo($this->sourcePos); + $value = $this->compileValue($this->reduce($value, true)); + fwrite($this->stderr, "Line $line DEBUG: $value\n"); + break; + + case Type::T_WARN: + list(, $value) = $child; + + $line = $this->parser->getLineNo($this->sourcePos); + $value = $this->compileValue($this->reduce($value, true)); + echo "Line $line WARN: $value\n"; + break; + + case Type::T_ERROR: + list(, $value) = $child; + + $line = $this->parser->getLineNo($this->sourcePos); + $value = $this->compileValue($this->reduce($value, true)); + $this->throwError("Line $line ERROR: $value\n"); + break; + + case Type::T_CONTROL: + $this->throwError('@break/@continue not permitted in this scope'); + break; + + default: + $this->throwError("unknown child type: $child[0]"); + } + } + + /** + * Reduce expression to string + * + * @param array $exp + * + * @return array + */ + protected function expToString($exp) + { + list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp; + + $content = array($this->reduce($left)); + + if ($whiteLeft) { + $content[] = ' '; + } + + $content[] = $op; + + if ($whiteRight) { + $content[] = ' '; + } + + $content[] = $this->reduce($right); + + return array(Type::T_STRING, '', $content); + } + + /** + * Is truthy? + * + * @param array $value + * + * @return array + */ + protected function isTruthy($value) + { + return $value !== self::$false && $value !== self::$null; + } + + /** + * Should $value cause its operand to eval + * + * @param array $value + * + * @return boolean + */ + protected function shouldEval($value) + { + switch ($value[0]) { + case Type::T_EXPRESSION: + if ($value[1] === '/') { + return $this->shouldEval($value[2], $value[3]); + } + + // fall-thru + case Type::T_VARIABLE: + case Type::T_FUNCTION_CALL: + return true; + } + + return false; + } + + /** + * Reduce value + * + * @param array $value + * @param boolean $inExp + * + * @return array + */ + protected function reduce($value, $inExp = false) + { + list($type) = $value; + + switch ($type) { + case Type::T_EXPRESSION: + list(, $op, $left, $right, $inParens) = $value; + + $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op; + $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right); + + $left = $this->reduce($left, true); + + if ($op !== 'and' && $op !== 'or') { + $right = $this->reduce($right, true); + } + + // special case: looks like css shorthand + if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2]) + && (($right[0] !== Type::T_NUMBER && $right[2] != '') + || ($right[0] === Type::T_NUMBER && ! $right->unitless())) + ) { + return $this->expToString($value); + } + + $left = $this->coerceForExpression($left); + $right = $this->coerceForExpression($right); + + $ltype = $left[0]; + $rtype = $right[0]; + + $ucOpName = ucfirst($opName); + $ucLType = ucfirst($ltype); + $ucRType = ucfirst($rtype); + + // this tries: + // 1. op[op name][left type][right type] + // 2. op[left type][right type] (passing the op as first arg + // 3. op[op name] + $fn = "op${ucOpName}${ucLType}${ucRType}"; + + if (is_callable(array($this, $fn)) || + (($fn = "op${ucLType}${ucRType}") && + is_callable(array($this, $fn)) && + $passOp = true) || + (($fn = "op${ucOpName}") && + is_callable(array($this, $fn)) && + $genOp = true) + ) { + $unitChange = false; + + if (! isset($genOp) && + $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER + ) { + if ($opName === 'mod' && ! $right->unitless()) { + $this->throwError( + 'Cannot modulo by a number with units: %s%s', + $right[1], + $right->unitStr() + ); + } + + $unitChange = true; + $emptyUnit = $left->unitless() || $right->unitless(); + $targetUnit = $left->unitless() ? $right[2] : $left[2]; + + if ($opName !== 'mul') { + $left[2] = $left->unitless() ? $targetUnit : $left[2]; + $right[2] = $right->unitless() ? $targetUnit : $right[2]; + } + + if ($opName !== 'mod') { + $left = $left->normalize(); + $right = $right->normalize(); + } + + if ($opName === 'div' && ! $emptyUnit && $left[2] === $right[2]) { + $targetUnit = ''; + } + + if ($opName === 'mul') { + $left[2] = $left->unitless() ? $right[2] : $left[2]; + $right[2] = $right->unitless() ? $left[2] : $right[2]; + } elseif ($opName === 'div' && $left[2] === $right[2]) { + $left[2] = ''; + $right[2] = ''; + } + } + + $shouldEval = $inParens || $inExp; + + if (isset($passOp)) { + $out = $this->$fn($op, $left, $right, $shouldEval); + } else { + $out = $this->$fn($left, $right, $shouldEval); + } + + if (isset($out)) { + if ($unitChange && $out[0] === Type::T_NUMBER) { + $out = $out->coerce($targetUnit); + } + + return $out; + } + } + + return $this->expToString($value); + + case Type::T_UNARY: + list(, $op, $exp, $inParens) = $value; + + $inExp = $inExp || $this->shouldEval($exp); + $exp = $this->reduce($exp); + + if ($exp[0] === Type::T_NUMBER) { + switch ($op) { + case '+': + return new Node\Number($exp[1], $exp[2]); + + case '-': + return new Node\Number(-$exp[1], $exp[2]); + } + } + + if ($op === 'not') { + if ($inExp || $inParens) { + if ($exp === self::$false) { + return self::$true; + } + + return self::$false; + } + + $op = $op . ' '; + } + + return array(Type::T_STRING, '', array($op, $exp)); + + case Type::T_VARIABLE: + list(, $name) = $value; + + return $this->reduce($this->get($name)); + + case Type::T_LIST: + foreach ($value[2] as &$item) { + $item = $this->reduce($item); + } + + return $value; + + case Type::T_MAP: + foreach ($value[1] as &$item) { + $item = $this->reduce($item); + } + + foreach ($value[2] as &$item) { + $item = $this->reduce($item); + } + + return $value; + + case Type::T_STRING: + foreach ($value[2] as &$item) { + if (is_array($item) || $item instanceof \ArrayAccess) { + $item = $this->reduce($item); + } + } + + return $value; + + case Type::T_INTERPOLATE: + $value[1] = $this->reduce($value[1]); + + return $value; + + case Type::T_FUNCTION_CALL: + list(, $name, $argValues) = $value; + + return $this->fncall($name, $argValues); + + default: + return $value; + } + } + + /** + * Function caller + * + * @param string $name + * @param array $argValues + * + * @return array|null + */ + private function fncall($name, $argValues) + { + // SCSS @function + if ($this->callScssFunction($name, $argValues, $returnValue)) { + return $returnValue; + } + + // native PHP functions + if ($this->callNativeFunction($name, $argValues, $returnValue)) { + return $returnValue; + } + + // for CSS functions, simply flatten the arguments into a list + $listArgs = array(); + + foreach ((array) $argValues as $arg) { + if (empty($arg[0])) { + $listArgs[] = $this->reduce($arg[1]); + } + } + + return array(Type::T_FUNCTION, $name, array(Type::T_LIST, ',', $listArgs)); + } + + /** + * Normalize name + * + * @param string $name + * + * @return string + */ + protected function normalizeName($name) + { + return str_replace('-', '_', $name); + } + + /** + * Normalize value + * + * @param array $value + * + * @return array + */ + public function normalizeValue($value) + { + $value = $this->coerceForExpression($this->reduce($value)); + list($type) = $value; + + switch ($type) { + case Type::T_LIST: + $value = $this->extractInterpolation($value); + + if ($value[0] !== Type::T_LIST) { + return array(Type::T_KEYWORD, $this->compileValue($value)); + } + + foreach ($value[2] as $key => $item) { + $value[2][$key] = $this->normalizeValue($item); + } + + return $value; + + case Type::T_STRING: + return array($type, '"', array($this->compileStringContent($value))); + + case Type::T_NUMBER: + return $value->normalize(); + + case Type::T_INTERPOLATE: + return array(Type::T_KEYWORD, $this->compileValue($value)); + + default: + return $value; + } + } + + /** + * Add numbers + * + * @param array $left + * @param array $right + * + * @return array + */ + protected function opAddNumberNumber($left, $right) + { + return new Node\Number($left[1] + $right[1], $left[2]); + } + + /** + * Multiply numbers + * + * @param array $left + * @param array $right + * + * @return array + */ + protected function opMulNumberNumber($left, $right) + { + return new Node\Number($left[1] * $right[1], $left[2]); + } + + /** + * Subtract numbers + * + * @param array $left + * @param array $right + * + * @return array + */ + protected function opSubNumberNumber($left, $right) + { + return new Node\Number($left[1] - $right[1], $left[2]); + } + + /** + * Divide numbers + * + * @param array $left + * @param array $right + * + * @return array + */ + protected function opDivNumberNumber($left, $right) + { + if ($right[1] == 0) { + return array(Type::T_STRING, '', array($left[1] . $left[2] . '/' . $right[1] . $right[2])); + } + + return new Node\Number($left[1] / $right[1], $left[2]); + } + + /** + * Mod numbers + * + * @param array $left + * @param array $right + * + * @return array + */ + protected function opModNumberNumber($left, $right) + { + return new Node\Number($left[1] % $right[1], $left[2]); + } + + /** + * Add strings + * + * @param array $left + * @param array $right + * + * @return array + */ + protected function opAdd($left, $right) + { + if ($strLeft = $this->coerceString($left)) { + if ($right[0] === Type::T_STRING) { + $right[1] = ''; + } + + $strLeft[2][] = $right; + + return $strLeft; + } + + if ($strRight = $this->coerceString($right)) { + if ($left[0] === Type::T_STRING) { + $left[1] = ''; + } + + array_unshift($strRight[2], $left); + + return $strRight; + } + } + + /** + * Boolean and + * + * @param array $left + * @param array $right + * @param boolean $shouldEval + * + * @return array + */ + protected function opAnd($left, $right, $shouldEval) + { + if (! $shouldEval) { + return; + } + + if ($left !== self::$false) { + return $this->reduce($right, true); + } + + return $left; + } + + /** + * Boolean or + * + * @param array $left + * @param array $right + * @param boolean $shouldEval + * + * @return array + */ + protected function opOr($left, $right, $shouldEval) + { + if (! $shouldEval) { + return; + } + + if ($left !== self::$false) { + return $left; + } + + return $this->reduce($right, true); + } + + /** + * Compare colors + * + * @param string $op + * @param array $left + * @param array $right + * + * @return array + */ + protected function opColorColor($op, $left, $right) + { + $out = array(Type::T_COLOR); + + foreach (array(1, 2, 3) 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("color: Can't divide by zero"); + } + + $out[] = (int) ($lval / $rval); + break; + + case '==': + return $this->opEq($left, $right); + + case '!=': + return $this->opNeq($left, $right); + + default: + $this->throwError("color: unknown op $op"); + } + } + + if (isset($left[4])) { + $out[4] = $left[4]; + } elseif (isset($right[4])) { + $out[4] = $right[4]; + } + + return $this->fixColor($out); + } + + /** + * Compare color and number + * + * @param string $op + * @param array $left + * @param array $right + * + * @return array + */ + protected function opColorNumber($op, $left, $right) + { + $value = $right[1]; + + return $this->opColorColor( + $op, + $left, + array(Type::T_COLOR, $value, $value, $value) + ); + } + + /** + * Compare number and color + * + * @param string $op + * @param array $left + * @param array $right + * + * @return array + */ + protected function opNumberColor($op, $left, $right) + { + $value = $left[1]; + + return $this->opColorColor( + $op, + array(Type::T_COLOR, $value, $value, $value), + $right + ); + } + + /** + * Compare number1 == number2 + * + * @param array $left + * @param array $right + * + * @return array + */ + protected function opEq($left, $right) + { + if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) { + $lStr[1] = ''; + $rStr[1] = ''; + + $left = $this->compileValue($lStr); + $right = $this->compileValue($rStr); + } + + return $this->toBool($left === $right); + } + + /** + * Compare number1 != number2 + * + * @param array $left + * @param array $right + * + * @return array + */ + protected function opNeq($left, $right) + { + if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) { + $lStr[1] = ''; + $rStr[1] = ''; + + $left = $this->compileValue($lStr); + $right = $this->compileValue($rStr); + } + + return $this->toBool($left !== $right); + } + + /** + * Compare number1 >= number2 + * + * @param array $left + * @param array $right + * + * @return array + */ + protected function opGteNumberNumber($left, $right) + { + return $this->toBool($left[1] >= $right[1]); + } + + /** + * Compare number1 > number2 + * + * @param array $left + * @param array $right + * + * @return array + */ + protected function opGtNumberNumber($left, $right) + { + return $this->toBool($left[1] > $right[1]); + } + + /** + * Compare number1 <= number2 + * + * @param array $left + * @param array $right + * + * @return array + */ + protected function opLteNumberNumber($left, $right) + { + return $this->toBool($left[1] <= $right[1]); + } + + /** + * Compare number1 < number2 + * + * @param array $left + * @param array $right + * + * @return array + */ + protected function opLtNumberNumber($left, $right) + { + return $this->toBool($left[1] < $right[1]); + } + + /** + * Three-way comparison, aka spaceship operator + * + * @param array $left + * @param array $right + * + * @return array + */ + protected function opCmpNumberNumber($left, $right) + { + $n = $left[1] - $right[1]; + + return new Node\Number($n ? $n / abs($n) : 0, ''); + } + + /** + * Cast to boolean + * + * @api + * + * @param mixed $thing + * + * @return array + */ + public function toBool($thing) + { + 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. + * + * @api + * + * @param array $value + * + * @return string + */ + public function compileValue($value) + { + $value = $this->reduce($value); + + list($type) = $value; + + switch ($type) { + case Type::T_KEYWORD: + return $value[1]; + + case Type::T_COLOR: + // [1] - red component (either number for 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); + + // 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 Type::T_NUMBER: + return (string) $value; + + case Type::T_STRING: + return $value[1] . $this->compileStringContent($value) . $value[1]; + + case Type::T_FUNCTION: + $args = ! empty($value[2]) ? $this->compileValue($value[2]) : ''; + + return "$value[1]($args)"; + + case Type::T_LIST: + $value = $this->extractInterpolation($value); + + if ($value[0] !== Type::T_LIST) { + return $this->compileValue($value); + } + + list(, $delim, $items) = $value; + + if ($delim !== ' ') { + $delim .= ' '; + } + + $filtered = array(); + + foreach ($items as $item) { + if ($item[0] === Type::T_NULL) { + continue; + } + + $filtered[] = $this->compileValue($item); + } + + return implode("$delim", $filtered); + + case Type::T_MAP: + $keys = $value[1]; + $values = $value[2]; + $filtered = array(); + + for ($i = 0, $s = count($keys); $i < $s; $i++) { + $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]); + } + + array_walk($filtered, function (&$value, $key) { + $value = $key . ': ' . $value; + }); + + return '(' . implode(', ', $filtered) . ')'; + + case Type::T_INTERPOLATED: + // node created by extractInterpolation + list(, $interpolate, $left, $right) = $value; + list(,, $whiteLeft, $whiteRight) = $interpolate; + + $left = count($left[2]) > 0 ? + $this->compileValue($left) . $whiteLeft : ''; + + $right = count($right[2]) > 0 ? + $whiteRight . $this->compileValue($right) : ''; + + return $left . $this->compileValue($interpolate) . $right; + + case Type::T_INTERPOLATE: + // raw parse node + list(, $exp) = $value; + + // strip quotes if it's a string + $reduced = $this->reduce($exp); + + switch ($reduced[0]) { + case Type::T_STRING: + $reduced = array(Type::T_KEYWORD, $this->compileStringContent($reduced)); + break; + + case Type::T_NULL: + $reduced = array(Type::T_KEYWORD, ''); + } + + return $this->compileValue($reduced); + + case Type::T_NULL: + return 'null'; + + default: + $this->throwError("unknown value type: $type"); + } + } + + /** + * Flatten list + * + * @param array $list + * + * @return string + */ + protected function flattenList($list) + { + return $this->compileValue($list); + } + + /** + * Compile string content + * + * @param array $string + * + * @return string + */ + protected function compileStringContent($string) + { + $parts = array(); + + foreach ($string[2] as $part) { + if (is_array($part) || $part instanceof \ArrayAccess) { + $parts[] = $this->compileValue($part); + } else { + $parts[] = $part; + } + } + + return implode($parts); + } + + /** + * Extract interpolation; it doesn't need to be recursive, compileValue will handle that + * + * @param array $list + * + * @return array + */ + protected function extractInterpolation($list) + { + $items = $list[2]; + + foreach ($items as $i => $item) { + if ($item[0] === Type::T_INTERPOLATE) { + $before = array(Type::T_LIST, $list[1], array_slice($items, 0, $i)); + $after = array(Type::T_LIST, $list[1], array_slice($items, $i + 1)); + + return array(Type::T_INTERPOLATED, $item, $before, $after); + } + } + + return $list; + } + + /** + * Find the final set of selectors + * + * @param \Leafo\ScssPhp\Compiler\Environment $env + * + * @return array + */ + protected function multiplySelectors(Environment $env) + { + $envs = $this->compactEnv($env); + $selectors = array(); + $parentSelectors = array(array()); + + while ($env = array_pop($envs)) { + if (empty($env->selectors)) { + continue; + } + + $selectors = array(); + + foreach ($env->selectors as $selector) { + foreach ($parentSelectors as $parent) { + $selectors[] = $this->joinSelectors($parent, $selector); + } + } + + $parentSelectors = $selectors; + } + + return $selectors; + } + + /** + * Join selectors; looks for & to replace, or append parent before child + * + * @param array $parent + * @param array $child + * + * @return array + */ + protected function joinSelectors($parent, $child) + { + $setSelf = false; + $out = array(); + + foreach ($child as $part) { + $newPart = array(); + + foreach ($part as $p) { + if ($p === self::$selfSelector) { + $setSelf = true; + + foreach ($parent as $i => $parentPart) { + if ($i > 0) { + $out[] = $newPart; + $newPart = array(); + } + + foreach ($parentPart as $pp) { + $newPart[] = $pp; + } + } + } else { + $newPart[] = $p; + } + } + + $out[] = $newPart; + } + + return $setSelf ? $out : array_merge($parent, $child); + } + + /** + * Multiply media + * + * @param \Leafo\ScssPhp\Compiler\Environment $env + * @param array $childQueries + * + * @return array + */ + protected function multiplyMedia(Environment $env = null, $childQueries = null) + { + if (! isset($env) || + ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA + ) { + return $childQueries; + } + + // plain old block, skip + if (empty($env->block->type)) { + return $this->multiplyMedia($env->parent, $childQueries); + } + + $parentQueries = isset($env->block->queryList) + ? $env->block->queryList + : array(array(array(Type::T_MEDIA_VALUE, $env->block->value))); + + if ($childQueries === null) { + $childQueries = $parentQueries; + } else { + $originalQueries = $childQueries; + $childQueries = array(); + + foreach ($parentQueries as $parentQuery) { + foreach ($originalQueries as $childQuery) { + $childQueries []= array_merge($parentQuery, $childQuery); + } + } + } + + return $this->multiplyMedia($env->parent, $childQueries); + } + + /** + * Convert env linked list to stack + * + * @param \Leafo\ScssPhp\Compiler\Environment $env + * + * @return array + */ + private function compactEnv(Environment $env) + { + for ($envs = array(); $env; $env = $env->parent) { + $envs[] = $env; + } + + return $envs; + } + + /** + * Convert env stack to singly linked list + * + * @param array $envs + * + * @return \Leafo\ScssPhp\Compiler\Environment + */ + private function extractEnv($envs) + { + for ($env = null; $e = array_pop($envs);) { + $e->parent = $env; + $env = $e; + } + + return $env; + } + + /** + * Push environment + * + * @param \Leafo\ScssPhp\Block $block + * + * @return \Leafo\ScssPhp\Compiler\Environment + */ + protected function pushEnv(Block $block = null) + { + $env = new Environment; + $env->parent = $this->env; + $env->store = array(); + $env->block = $block; + $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0; + + $this->env = $env; + + return $env; + } + + /** + * Pop environment + */ + protected function popEnv() + { + $this->env = $this->env->parent; + } + + /** + * Get store environment + * + * @return \Leafo\ScssPhp\Compiler\Environment + */ + protected function getStoreEnv() + { + return isset($this->storeEnv) ? $this->storeEnv : $this->env; + } + + /** + * Set variable + * + * @param string $name + * @param mixed $value + * @param boolean $shadow + * @param \Leafo\ScssPhp\Compiler\Environment $env + */ + protected function set($name, $value, $shadow = false, Environment $env = null) + { + $name = $this->normalizeName($name); + + if (! isset($env)) { + $env = $this->getStoreEnv(); + } + + if ($shadow) { + $this->setRaw($name, $value, $env); + } else { + $this->setExisting($name, $value, $env); + } + } + + /** + * Set existing variable + * + * @param string $name + * @param mixed $value + * @param \Leafo\ScssPhp\Compiler\Environment $env + */ + protected function setExisting($name, $value, Environment $env) + { + $storeEnv = $env; + + $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%'; + + for (;;) { + if (array_key_exists($name, $env->store)) { + break; + } + + if (! $hasNamespace && isset($env->marker)) { + $env = $storeEnv; + break; + } + + if (! isset($env->parent)) { + $env = $storeEnv; + break; + } + + $env = $env->parent; + } + + $env->store[$name] = $value; + } + + /** + * Set raw variable + * + * @param string $name + * @param mixed $value + * @param \Leafo\ScssPhp\Compiler\Environment $env + */ + protected function setRaw($name, $value, Environment $env) + { + $env->store[$name] = $value; + } + + /** + * Get variable + * + * @api + * + * @param string $name + * @param boolean $shouldThrow + * @param \Leafo\ScssPhp\Compiler\Environment $env + * + * @return mixed + */ + public function get($name, $shouldThrow = true, Environment $env = null) + { + $name = $this->normalizeName($name); + + if (! isset($env)) { + $env = $this->getStoreEnv(); + } + + $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%'; + + for (;;) { + if (array_key_exists($name, $env->store)) { + return $env->store[$name]; + } + + if (! $hasNamespace && isset($env->marker)) { + $env = $this->rootEnv; + continue; + } + + if (! isset($env->parent)) { + break; + } + + $env = $env->parent; + } + + if ($shouldThrow) { + $this->throwError("Undefined variable \$$name"); + } + + // found nothing + } + + /** + * Has variable? + * + * @param string $name + * @param \Leafo\ScssPhp\Compiler\Environment $env + * + * @return boolean + */ + protected function has($name, Environment $env = null) + { + return $this->get($name, false, $env) !== null; + } + + /** + * Inject variables + * + * @param array $args + */ + protected function injectVariables(array $args) + { + if (empty($args)) { + return; + } + + $parser = $this->parserFactory(__METHOD__); + + foreach ($args as $name => $strValue) { + if ($name[0] === '$') { + $name = substr($name, 1); + } + + if (! $parser->parseValue($strValue, $value)) { + $value = $this->coerceValue($strValue); + } + + $this->set($name, $value); + } + } + + /** + * Set variables + * + * @api + * + * @param array $variables + */ + public function setVariables(array $variables) + { + $this->registeredVars = array_merge($this->registeredVars, $variables); + } + + /** + * Unset variable + * + * @api + * + * @param string $name + */ + public function unsetVariable($name) + { + unset($this->registeredVars[$name]); + } + + /** + * Returns list of variables + * + * @api + * + * @return array + */ + public function getVariables() + { + return $this->registeredVars; + } + + /** + * Adds to list of parsed files + * + * @api + * + * @param string $path + */ + public function addParsedFile($path) + { + if (isset($path) && file_exists($path)) { + $this->parsedFiles[realpath($path)] = filemtime($path); + } + } + + /** + * Returns list of parsed files + * + * @api + * + * @return array + */ + public function getParsedFiles() + { + return $this->parsedFiles; + } + + /** + * Add import path + * + * @api + * + * @param string $path + */ + public function addImportPath($path) + { + if (! in_array($path, $this->importPaths)) { + $this->importPaths[] = $path; + } + } + + /** + * Set import paths + * + * @api + * + * @param string|array $path + */ + public function setImportPaths($path) + { + $this->importPaths = (array) $path; + } + + /** + * Set number precision + * + * @api + * + * @param integer $numberPrecision + */ + public function setNumberPrecision($numberPrecision) + { + Node\Number::$precision = $numberPrecision; + } + + /** + * Set formatter + * + * @api + * + * @param string $formatterName + */ + public function setFormatter($formatterName) + { + $this->formatter = $formatterName; + } + + /** + * Set line number style + * + * @api + * + * @param string $lineNumberStyle + */ + public function setLineNumberStyle($lineNumberStyle) + { + $this->lineNumberStyle = $lineNumberStyle; + } + + /** + * Register function + * + * @api + * + * @param string $name + * @param callable $func + * @param array $prototype + */ + public function registerFunction($name, $func, $prototype = null) + { + $this->userFunctions[$this->normalizeName($name)] = array($func, $prototype); + } + + /** + * Unregister function + * + * @api + * + * @param string $name + */ + public function unregisterFunction($name) + { + unset($this->userFunctions[$this->normalizeName($name)]); + } + + /** + * Add feature + * + * @api + * + * @param string $name + */ + public function addFeature($name) + { + $this->registeredFeatures[$name] = true; + } + + /** + * Import file + * + * @param string $path + * @param array $out + */ + protected function importFile($path, $out) + { + // see if tree is cached + $realPath = realpath($path); + + if (isset($this->importCache[$realPath])) { + $this->handleImportLoop($realPath); + + $tree = $this->importCache[$realPath]; + } else { + $code = file_get_contents($path); + $parser = $this->parserFactory($path); + $tree = $parser->parse($code); + + $this->importCache[$realPath] = $tree; + } + + $pi = pathinfo($path); + array_unshift($this->importPaths, $pi['dirname']); + $this->compileChildrenNoReturn($tree->children, $out); + array_shift($this->importPaths); + } + + /** + * Return the file path for an import url if it exists + * + * @api + * + * @param string $url + * + * @return string|null + */ + public function findImport($url) + { + $urls = array(); + + // for "normal" scss imports (ignore vanilla css and external requests) + if (! preg_match('/\.css$|^https?:\/\//', $url)) { + // try both normal and the _partial filename + $urls = array($url, preg_replace('/[^\/]+$/', '_\0', $url)); + } + + foreach ($this->importPaths as $dir) { + if (is_string($dir)) { + // check urls for normal import paths + foreach ($urls as $full) { + $full = $dir + . (! empty($dir) && substr($dir, -1) !== '/' ? '/' : '') + . $full; + + if ($this->fileExists($file = $full . '.scss') || + $this->fileExists($file = $full) + ) { + return $file; + } + } + } elseif (is_callable($dir)) { + // check custom callback for import path + $file = call_user_func($dir, $url); + + if ($file !== null) { + return $file; + } + } + } + + return null; + } + + /** + * Throw error (exception) + * + * @api + * + * @param string $msg Message with optional sprintf()-style vararg parameters + * + * @throws \Exception + */ + public function throwError($msg) + { + if (func_num_args() > 1) { + $msg = call_user_func_array('sprintf', func_get_args()); + } + + if ($this->sourcePos >= 0 && isset($this->sourceIndex)) { + $parser = $this->sourceParsers[$this->sourceIndex]; + $parser->throwParseError($msg, $this->sourcePos); + } + + throw new \Exception($msg); + } + + /** + * Handle import loop + * + * @param string $name + * + * @throws \Exception + */ + private function handleImportLoop($name) + { + for ($env = $this->env; $env; $env = $env->parent) { + $parser = $this->sourceParsers[$env->block->sourceIndex]; + $file = $parser->getSourceName(); + + if (realpath($file) === $name) { + $this->throwError('An @import loop has been found: %s imports %s', $file, basename($file)); + } + } + } + + /** + * Does file exist? + * + * @param string $name + * + * @return boolean + */ + protected function fileExists($name) + { + return is_file($name); + } + + /** + * Call SCSS @function + * + * @param string $name + * @param array $args + * @param array $returnValue + * + * @return boolean Returns true if returnValue is set; otherwise, false + */ + protected function callScssFunction($name, $argValues, &$returnValue) + { + $func = $this->get(self::$namespaces['function'] . $name, false); + + if (! $func) { + return false; + } + + $this->pushEnv(); + + // set the args + if (isset($func->args)) { + $this->applyArguments($func->args, $argValues); + } + + // throw away lines and children + $tmp = new OutputBlock; + $tmp->lines = array(); + $tmp->children = array(); + + $this->env->marker = 'function'; + + $ret = $this->compileChildren($func->children, $tmp); + + $this->popEnv(); + + $returnValue = ! isset($ret) ? self::$defaultValue : $ret; + + return true; + } + + /** + * Call built-in and registered (PHP) functions + * + * @param string $name + * @param array $args + * @param array $returnValue + * + * @return boolean Returns true if returnValue is set; otherwise, false + */ + protected function callNativeFunction($name, $args, &$returnValue) + { + // try a lib function + $name = $this->normalizeName($name); + + if (isset($this->userFunctions[$name])) { + // see if we can find a user function + list($f, $prototype) = $this->userFunctions[$name]; + } elseif (($f = $this->getBuiltinFunction($name)) && is_callable($f)) { + $libName = $f[1]; + $prototype = isset(self::$$libName) ? self::$$libName : null; + } else { + return false; + } + + list($sorted, $kwargs) = $this->sortArgs($prototype, $args); + + if ($name !== 'if' && $name !== 'call') { + foreach ($sorted as &$val) { + $val = $this->reduce($val, true); + } + } + + $returnValue = call_user_func($f, $sorted, $kwargs); + + if (! isset($returnValue)) { + return false; + } + + $returnValue = $this->coerceValue($returnValue); + + return true; + } + + /** + * Get built-in function + * + * @param string $name Normalized name + * + * @return array + */ + protected function getBuiltinFunction($name) + { + $libName = 'lib' . preg_replace_callback( + '/_(.)/', + function ($m) { + return ucfirst($m[1]); + }, + ucfirst($name) + ); + + return array($this, $libName); + } + + /** + * Sorts keyword arguments + * + * @todo Merge with applyArguments()? + * + * @param array $prototype + * @param array $args + * + * @return array + */ + protected function sortArgs($prototype, $args) + { + $keyArgs = array(); + $posArgs = array(); + + // separate positional and keyword arguments + foreach ($args as $arg) { + list($key, $value) = $arg; + + $key = $key[1]; + + if (empty($key)) { + $posArgs[] = $value; + } else { + $keyArgs[$key] = $value; + } + } + + if (! isset($prototype)) { + return array($posArgs, $keyArgs); + } + + // copy positional args + $finalArgs = array_pad($posArgs, count($prototype), null); + + // overwrite positional args with keyword args + foreach ($prototype as $i => $names) { + foreach ((array) $names as $name) { + if (isset($keyArgs[$name])) { + $finalArgs[$i] = $keyArgs[$name]; + } + } + } + + return array($finalArgs, $keyArgs); + } + + /** + * Apply argument values per definition + * + * @param array $argDef + * @param array $argValues + * + * @throws \Exception + */ + protected function applyArguments($argDef, $argValues) + { + $storeEnv = $this->getStoreEnv(); + + $env = new Environment; + $env->store = $storeEnv->store; + + $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 ((array) $argValues as $arg) { + if (! empty($arg[0])) { + 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] === Type::T_LIST) { + foreach ($val[2] as $name => $item) { + if (! is_numeric($name)) { + $keywordArgs[$name] = $item; + } else { + $remaining[] = $item; + } + } + } elseif ($val[0] === Type::T_MAP) { + foreach ($val[1] as $i => $name) { + $name = $this->compileStringContent($this->coerceString($name)); + $item = $val[2][$i]; + + if (! is_numeric($name)) { + $keywordArgs[$name] = $item; + } else { + $remaining[] = $item; + } + } + } else { + $remaining[] = $val; + } + } else { + $remaining[] = $arg[1]; + } + } + + foreach ($args as $arg) { + list($i, $name, $default, $isVariable) = $arg; + + if ($isVariable) { + $val = array(Type::T_LIST, ',', array(), $isVariable); + + 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)) { + continue; + } else { + $this->throwError("Missing argument $name"); + } + + $this->set($name, $this->reduce($val, true), true, $env); + } + + $storeEnv->store = $env->store; + + foreach ($args as $arg) { + list($i, $name, $default, $isVariable) = $arg; + + if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) { + continue; + } + + $this->set($name, $this->reduce($default, true), true); + } + } + + /** + * Coerce a php value into a scss one + * + * @param mixed $value + * + * @return array + */ + private function coerceValue($value) + { + if (is_array($value) || $value instanceof \ArrayAccess) { + return $value; + } + + if (is_bool($value)) { + return $this->toBool($value); + } + + if ($value === null) { + $value = self::$null; + } + + if (is_numeric($value)) { + return new Node\Number($value, ''); + } + + if ($value === '') { + return self::$emptyString; + } + + return array(Type::T_KEYWORD, $value); + } + + /** + * Coerce something to map + * + * @param array $item + * + * @return array + */ + protected function coerceMap($item) + { + if ($item[0] === Type::T_MAP) { + return $item; + } + + if ($item === self::$emptyList) { + return self::$emptyMap; + } + + return array(Type::T_MAP, array($item), array(self::$null)); + } + + /** + * Coerce something to list + * + * @param array $item + * + * @return array + */ + protected function coerceList($item, $delim = ',') + { + if (isset($item) && $item[0] === Type::T_LIST) { + return $item; + } + + if (isset($item) && $item[0] === Type::T_MAP) { + $keys = $item[1]; + $values = $item[2]; + $list = array(); + + for ($i = 0, $s = count($keys); $i < $s; $i++) { + $key = $keys[$i]; + $value = $values[$i]; + + $list[] = array( + Type::T_LIST, + '', + array(array(Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))), $value) + ); + } + + return array(Type::T_LIST, ',', $list); + } + + return array(Type::T_LIST, $delim, ! isset($item) ? array(): array($item)); + } + + /** + * Coerce color for expression + * + * @param array $value + * + * @return array|null + */ + protected function coerceForExpression($value) + { + if ($color = $this->coerceColor($value)) { + return $color; + } + + return $value; + } + + /** + * Coerce value to color + * + * @param array $value + * + * @return array|null + */ + protected function coerceColor($value) + { + switch ($value[0]) { + case Type::T_COLOR: + return $value; + + case Type::T_KEYWORD: + $name = strtolower($value[1]); + + if (isset(Colors::$cssColors[$name])) { + $rgba = explode(',', Colors::$cssColors[$name]); + + return isset($rgba[3]) + ? array(Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3]) + : array(Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]); + } + + return null; + } + + return null; + } + + /** + * Coerce value to string + * + * @param array $value + * + * @return array|null + */ + protected function coerceString($value) + { + if ($value[0] === Type::T_STRING) { + return $value; + } + + return array(Type::T_STRING, '', array($this->compileValue($value))); + } + + /** + * Coerce value to a percentage + * + * @param array $value + * + * @return integer|float + */ + protected function coercePercent($value) + { + if ($value[0] === Type::T_NUMBER) { + if ($value[2] === '%') { + return $value[1] / 100; + } + + return $value[1]; + } + + return 0; + } + + /** + * Assert value is a map + * + * @api + * + * @param array $value + * + * @return array + * + * @throws \Exception + */ + public function assertMap($value) + { + $value = $this->coerceMap($value); + + if ($value[0] !== Type::T_MAP) { + $this->throwError('expecting map'); + } + + return $value; + } + + /** + * Assert value is a list + * + * @api + * + * @param array $value + * + * @return array + * + * @throws \Exception + */ + public function assertList($value) + { + if ($value[0] !== Type::T_LIST) { + $this->throwError('expecting list'); + } + + return $value; + } + + /** + * Assert value is a color + * + * @api + * + * @param array $value + * + * @return array + * + * @throws \Exception + */ + public function assertColor($value) + { + if ($color = $this->coerceColor($value)) { + return $color; + } + + $this->throwError('expecting color'); + } + + /** + * Assert value is a number + * + * @api + * + * @param array $value + * + * @return integer|float + * + * @throws \Exception + */ + public function assertNumber($value) + { + if ($value[0] !== Type::T_NUMBER) { + $this->throwError('expecting number'); + } + + return $value[1]; + } + + /** + * Make sure a color's components don't go out of bounds + * + * @param array $c + * + * @return array + */ + protected function fixColor($c) + { + foreach (array(1, 2, 3) as $i) { + if ($c[$i] < 0) { + $c[$i] = 0; + } + + if ($c[$i] > 255) { + $c[$i] = 255; + } + } + + return $c; + } + + /** + * Convert RGB to HSL + * + * @api + * + * @param integer $red + * @param integer $green + * @param integer $blue + * + * @return array + */ + public function toHSL($red, $green, $blue) + { + $min = min($red, $green, $blue); + $max = max($red, $green, $blue); + + $l = $min + $max; + $d = $max - $min; + + if ((int) $d === 0) { + $h = $s = 0; + } else { + if ($l < 255) { + $s = $d / $l; + } else { + $s = $d / (510 - $l); + } + + if ($red == $max) { + $h = 60 * ($green - $blue) / $d; + } elseif ($green == $max) { + $h = 60 * ($blue - $red) / $d + 120; + } elseif ($blue == $max) { + $h = 60 * ($red - $green) / $d + 240; + } + } + + return array(Type::T_HSL, fmod($h, 360), $s * 100, $l / 5.1); + } + + /** + * Hue to RGB helper + * + * @param float $m1 + * @param float $m2 + * @param float $h + * + * @return float + */ + private function hueToRGB($m1, $m2, $h) + { + if ($h < 0) { + $h += 1; + } elseif ($h > 1) { + $h -= 1; + } + + if ($h * 6 < 1) { + return $m1 + ($m2 - $m1) * $h * 6; + } + + if ($h * 2 < 1) { + return $m2; + } + + if ($h * 3 < 2) { + return $m1 + ($m2 - $m1) * (2/3 - $h) * 6; + } + + return $m1; + } + + /** + * Convert HSL to RGB + * + * @api + * + * @param integer $hue H from 0 to 360 + * @param integer $saturation S from 0 to 100 + * @param integer $lightness L from 0 to 100 + * + * @return array + */ + public function toRGB($hue, $saturation, $lightness) + { + if ($hue < 0) { + $hue += 360; + } + + $h = $hue / 360; + $s = min(100, max(0, $saturation)) / 100; + $l = min(100, max(0, $lightness)) / 100; + + $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s; + $m1 = $l * 2 - $m2; + + $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(Type::T_COLOR, $r, $g, $b); + + return $out; + } + + // Built in functions + + //protected static $libCall = array('name', 'args...'); + protected function libCall($args, $kwargs) + { + $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true))); + + $args = array_map( + function ($a) { + return array(null, $a, false); + }, + $args + ); + + if (count($kwargs)) { + foreach ($kwargs as $key => $value) { + $args[] = array(array(Type::T_VARIABLE, $key), $value, false); + } + } + + return $this->reduce(array(Type::T_FUNCTION_CALL, $name, $args)); + } + + protected static $libIf = array('condition', 'if-true', 'if-false'); + protected function libIf($args) + { + list($cond, $t, $f) = $args; + + if (! $this->isTruthy($this->reduce($cond, true))) { + return $this->reduce($f, true); + } + + return $this->reduce($t, true); + } + + protected static $libIndex = array('list', 'value'); + protected function libIndex($args) + { + list($list, $value) = $args; + + if ($value[0] === Type::T_MAP) { + return self::$null; + } + + if ($list[0] === Type::T_MAP || + $list[0] === Type::T_STRING || + $list[0] === Type::T_KEYWORD || + $list[0] === Type::T_INTERPOLATE + ) { + $list = $this->coerceList($list, ' '); + } + + if ($list[0] !== Type::T_LIST) { + return self::$null; + } + + $values = array(); + + foreach ($list[2] as $item) { + $values[] = $this->normalizeValue($item); + } + + $key = array_search($this->normalizeValue($value), $values); + + return false === $key ? self::$null : $key + 1; + } + + protected static $libRgb = array('red', 'green', 'blue'); + protected function libRgb($args) + { + list($r, $g, $b) = $args; + + return array(Type::T_COLOR, $r[1], $g[1], $b[1]); + } + + protected static $libRgba = array( + array('red', 'color'), + 'green', 'blue', 'alpha'); + protected function libRgba($args) + { + if ($color = $this->coerceColor($args[0])) { + // workaround https://github.com/facebook/hhvm/issues/5457 + reset($args); + + $num = ! isset($args[1]) ? $args[3] : $args[1]; + $alpha = $this->assertNumber($num); + $color[4] = $alpha; + + return $color; + } + + list($r, $g, $b, $a) = $args; + + return array(Type::T_COLOR, $r[1], $g[1], $b[1], $a[1]); + } + + // helper function for adjust_color, change_color, and scale_color + protected function alterColor($args, $fn) + { + $color = $this->assertColor($args[0]); + + // workaround https://github.com/facebook/hhvm/issues/5457 + reset($args); + + foreach (array(1, 2, 3, 7) as $i) { + if (isset($args[$i])) { + $val = $this->assertNumber($args[$i]); + $ii = $i === 7 ? 4 : $i; // alpha + $color[$ii] = call_user_func($fn, isset($color[$ii]) ? $color[$ii] : 0, $val, $i); + } + } + + if (isset($args[4]) || isset($args[5]) || isset($args[6])) { + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + + foreach (array(4, 5, 6) as $i) { + if (isset($args[$i])) { + $val = $this->assertNumber($args[$i]); + $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i); + } + } + + $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]); + + if (isset($color[4])) { + $rgb[4] = $color[4]; + } + + $color = $rgb; + } + + return $color; + } + + protected static $libAdjustColor = array( + 'color', 'red', 'green', 'blue', + 'hue', 'saturation', 'lightness', 'alpha' + ); + protected function libAdjustColor($args) + { + return $this->alterColor($args, function ($base, $alter, $i) { + return $base + $alter; + }); + } + + protected static $libChangeColor = array( + 'color', 'red', 'green', 'blue', + 'hue', 'saturation', 'lightness', 'alpha' + ); + protected function libChangeColor($args) + { + return $this->alterColor($args, function ($base, $alter, $i) { + return $alter; + }); + } + + protected static $libScaleColor = array( + 'color', 'red', 'green', 'blue', + 'hue', 'saturation', 'lightness', 'alpha' + ); + protected function libScaleColor($args) + { + return $this->alterColor($args, function ($base, $scale, $i) { + // 1, 2, 3 - rgb + // 4, 5, 6 - hsl + // 7 - a + switch ($i) { + case 1: + case 2: + case 3: + $max = 255; + break; + + case 4: + $max = 360; + break; + + case 7: + $max = 1; + break; + + default: + $max = 100; + } + + $scale = $scale / 100; + + if ($scale < 0) { + return $base * $scale + $base; + } + + return ($max - $base) * $scale + $base; + }); + } + + protected static $libIeHexStr = array('color'); + protected function libIeHexStr($args) + { + $color = $this->coerceColor($args[0]); + $color[4] = isset($color[4]) ? round(255*$color[4]) : 255; + + return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]); + } + + protected static $libRed = array('color'); + protected function libRed($args) + { + $color = $this->coerceColor($args[0]); + + return $color[1]; + } + + protected static $libGreen = array('color'); + protected function libGreen($args) + { + $color = $this->coerceColor($args[0]); + + return $color[2]; + } + + protected static $libBlue = array('color'); + protected function libBlue($args) + { + $color = $this->coerceColor($args[0]); + + return $color[3]; + } + + protected static $libAlpha = array('color'); + protected function libAlpha($args) + { + if ($color = $this->coerceColor($args[0])) { + return isset($color[4]) ? $color[4] : 1; + } + + // this might be the IE function, so return value unchanged + return null; + } + + protected static $libOpacity = array('color'); + protected function libOpacity($args) + { + $value = $args[0]; + + if ($value[0] === Type::T_NUMBER) { + return null; + } + + return $this->libAlpha($args); + } + + // mix two colors + protected static $libMix = array('color-1', 'color-2', 'weight'); + protected function libMix($args) + { + list($first, $second, $weight) = $args; + + $first = $this->assertColor($first); + $second = $this->assertColor($second); + + if (! isset($weight)) { + $weight = 0.5; + } else { + $weight = $this->coercePercent($weight); + } + + $firstAlpha = isset($first[4]) ? $first[4] : 1; + $secondAlpha = isset($second[4]) ? $second[4] : 1; + + $w = $weight * 2 - 1; + $a = $firstAlpha - $secondAlpha; + + $w1 = (($w * $a === -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0; + $w2 = 1.0 - $w1; + + $new = array(Type::T_COLOR, + $w1 * $first[1] + $w2 * $second[1], + $w1 * $first[2] + $w2 * $second[2], + $w1 * $first[3] + $w2 * $second[3], + ); + + if ($firstAlpha != 1.0 || $secondAlpha != 1.0) { + $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1); + } + + return $this->fixColor($new); + } + + protected static $libHsl = array('hue', 'saturation', 'lightness'); + protected function libHsl($args) + { + list($h, $s, $l) = $args; + + return $this->toRGB($h[1], $s[1], $l[1]); + } + + protected static $libHsla = array('hue', 'saturation', 'lightness', 'alpha'); + protected function libHsla($args) + { + list($h, $s, $l, $a) = $args; + + $color = $this->toRGB($h[1], $s[1], $l[1]); + $color[4] = $a[1]; + + return $color; + } + + protected static $libHue = array('color'); + protected function libHue($args) + { + $color = $this->assertColor($args[0]); + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + + return new Node\Number($hsl[1], 'deg'); + } + + protected static $libSaturation = array('color'); + protected function libSaturation($args) + { + $color = $this->assertColor($args[0]); + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + + return new Node\Number($hsl[2], '%'); + } + + protected static $libLightness = array('color'); + protected function libLightness($args) + { + $color = $this->assertColor($args[0]); + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + + return new Node\Number($hsl[3], '%'); + } + + protected function adjustHsl($color, $idx, $amount) + { + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + $hsl[$idx] += $amount; + $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]); + + if (isset($color[4])) { + $out[4] = $color[4]; + } + + return $out; + } + + protected static $libAdjustHue = array('color', 'degrees'); + protected function libAdjustHue($args) + { + $color = $this->assertColor($args[0]); + $degrees = $this->assertNumber($args[1]); + + return $this->adjustHsl($color, 1, $degrees); + } + + protected static $libLighten = array('color', 'amount'); + protected function libLighten($args) + { + $color = $this->assertColor($args[0]); + $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%'); + + return $this->adjustHsl($color, 3, $amount); + } + + protected static $libDarken = array('color', 'amount'); + protected function libDarken($args) + { + $color = $this->assertColor($args[0]); + $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%'); + + return $this->adjustHsl($color, 3, -$amount); + } + + protected static $libSaturate = array('color', 'amount'); + protected function libSaturate($args) + { + $value = $args[0]; + + if ($value[0] === Type::T_NUMBER) { + return null; + } + + $color = $this->assertColor($value); + $amount = 100 * $this->coercePercent($args[1]); + + return $this->adjustHsl($color, 2, $amount); + } + + protected static $libDesaturate = array('color', 'amount'); + protected function libDesaturate($args) + { + $color = $this->assertColor($args[0]); + $amount = 100 * $this->coercePercent($args[1]); + + return $this->adjustHsl($color, 2, -$amount); + } + + protected static $libGrayscale = array('color'); + protected function libGrayscale($args) + { + $value = $args[0]; + + if ($value[0] === Type::T_NUMBER) { + return null; + } + + return $this->adjustHsl($this->assertColor($value), 2, -100); + } + + protected static $libComplement = array('color'); + protected function libComplement($args) + { + return $this->adjustHsl($this->assertColor($args[0]), 1, 180); + } + + protected static $libInvert = array('color'); + protected function libInvert($args) + { + $value = $args[0]; + + if ($value[0] === Type::T_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 $libOpacify = array('color', 'amount'); + protected function libOpacify($args) + { + $color = $this->assertColor($args[0]); + $amount = $this->coercePercent($args[1]); + + $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount; + $color[4] = min(1, max(0, $color[4])); + + return $color; + } + + protected static $libFadeIn = array('color', 'amount'); + protected function libFadeIn($args) + { + return $this->libOpacify($args); + } + + // decreases opacity by amount + protected static $libTransparentize = array('color', 'amount'); + protected function libTransparentize($args) + { + $color = $this->assertColor($args[0]); + $amount = $this->coercePercent($args[1]); + + $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount; + $color[4] = min(1, max(0, $color[4])); + + return $color; + } + + protected static $libFadeOut = array('color', 'amount'); + protected function libFadeOut($args) + { + return $this->libTransparentize($args); + } + + protected static $libUnquote = array('string'); + protected function libUnquote($args) + { + $str = $args[0]; + + if ($str[0] === Type::T_STRING) { + $str[1] = ''; + } + + return $str; + } + + protected static $libQuote = array('string'); + protected function libQuote($args) + { + $value = $args[0]; + + if ($value[0] === Type::T_STRING && ! empty($value[1])) { + return $value; + } + + return array(Type::T_STRING, '"', array($value)); + } + + protected static $libPercentage = array('value'); + protected function libPercentage($args) + { + return new Node\Number($this->coercePercent($args[0]) * 100, '%'); + } + + protected static $libRound = array('value'); + protected function libRound($args) + { + $num = $args[0]; + $num[1] = round($num[1]); + + return $num; + } + + protected static $libFloor = array('value'); + protected function libFloor($args) + { + $num = $args[0]; + $num[1] = floor($num[1]); + + return $num; + } + + protected static $libCeil = array('value'); + protected function libCeil($args) + { + $num = $args[0]; + $num[1] = ceil($num[1]); + + return $num; + } + + protected static $libAbs = array('value'); + protected function libAbs($args) + { + $num = $args[0]; + $num[1] = abs($num[1]); + + return $num; + } + + protected function libMin($args) + { + $numbers = $this->getNormalizedNumbers($args); + $min = null; + + foreach ($numbers as $key => $number) { + if (null === $min || $number[1] <= $min[1]) { + $min = array($key, $number[1]); + } + } + + return $args[$min[0]]; + } + + protected function libMax($args) + { + $numbers = $this->getNormalizedNumbers($args); + $max = null; + + foreach ($numbers as $key => $number) { + if (null === $max || $number[1] >= $max[1]) { + $max = array($key, $number[1]); + } + } + + return $args[$max[0]]; + } + + /** + * Helper to normalize args containing numbers + * + * @param array $args + * + * @return array + */ + protected function getNormalizedNumbers($args) + { + $unit = null; + $originalUnit = null; + $numbers = array(); + + foreach ($args as $key => $item) { + if ($item[0] !== Type::T_NUMBER) { + $this->throwError('%s is not a number', $item[0]); + } + + $number = $item->normalize(); + + if (null === $unit) { + $unit = $number[2]; + $originalUnit = $item->unitStr(); + } elseif ($unit !== $number[2]) { + $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr()); + } + + $numbers[$key] = $number; + } + + return $numbers; + } + + protected static $libLength = array('list'); + protected function libLength($args) + { + $list = $this->coerceList($args[0]); + + return count($list[2]); + } + + // TODO: need a way to declare this built-in as varargs + //protected static $libListSeparator = array('list...'); + protected function libListSeparator($args) + { + if (count($args) > 1) { + return 'comma'; + } + + $list = $this->coerceList($args[0]); + + if (count($list[2]) <= 1) { + return 'space'; + } + + if ($list[1] === ',') { + return 'comma'; + } + + return 'space'; + } + + protected static $libNth = array('list', 'n'); + protected function libNth($args) + { + $list = $this->coerceList($args[0]); + $n = $this->assertNumber($args[1]); + + if ($n > 0) { + $n--; + } elseif ($n < 0) { + $n += count($list[2]); + } + + return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue; + } + + protected static $libSetNth = array('list', 'n', 'value'); + protected function libSetNth($args) + { + $list = $this->coerceList($args[0]); + $n = $this->assertNumber($args[1]); + + if ($n > 0) { + $n--; + } elseif ($n < 0) { + $n += count($list[2]); + } + + if (! isset($list[2][$n])) { + $this->throwError('Invalid argument for "n"'); + } + + $list[2][$n] = $args[2]; + + return $list; + } + + protected static $libMapGet = array('map', 'key'); + protected function libMapGet($args) + { + $map = $this->assertMap($args[0]); + $key = $this->compileStringContent($this->coerceString($args[1])); + + for ($i = count($map[1]) - 1; $i >= 0; $i--) { + if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { + return $map[2][$i]; + } + } + + return self::$null; + } + + protected static $libMapKeys = array('map'); + protected function libMapKeys($args) + { + $map = $this->assertMap($args[0]); + $keys = $map[1]; + + return array(Type::T_LIST, ',', $keys); + } + + protected static $libMapValues = array('map'); + protected function libMapValues($args) + { + $map = $this->assertMap($args[0]); + $values = $map[2]; + + return array(Type::T_LIST, ',', $values); + } + + protected static $libMapRemove = array('map', 'key'); + protected function libMapRemove($args) + { + $map = $this->assertMap($args[0]); + $key = $this->compileStringContent($this->coerceString($args[1])); + + for ($i = count($map[1]) - 1; $i >= 0; $i--) { + if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { + array_splice($map[1], $i, 1); + array_splice($map[2], $i, 1); + } + } + + return $map; + } + + protected static $libMapHasKey = array('map', 'key'); + protected function libMapHasKey($args) + { + $map = $this->assertMap($args[0]); + $key = $this->compileStringContent($this->coerceString($args[1])); + + for ($i = count($map[1]) - 1; $i >= 0; $i--) { + if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { + return true; + } + } + + return false; + } + + protected static $libMapMerge = array('map-1', 'map-2'); + protected function libMapMerge($args) + { + $map1 = $this->assertMap($args[0]); + $map2 = $this->assertMap($args[1]); + + return array(Type::T_MAP, array_merge($map1[1], $map2[1]), array_merge($map1[2], $map2[2])); + } + + protected function listSeparatorForJoin($list1, $sep) + { + if (! isset($sep)) { + return $list1[1]; + } + + switch ($this->compileValue($sep)) { + case 'comma': + return ','; + + case 'space': + return ''; + + default: + return $list1[1]; + } + } + + protected static $libJoin = array('list1', 'list2', 'separator'); + protected function libJoin($args) + { + list($list1, $list2, $sep) = $args; + + $list1 = $this->coerceList($list1, ' '); + $list2 = $this->coerceList($list2, ' '); + $sep = $this->listSeparatorForJoin($list1, $sep); + + return array(Type::T_LIST, $sep, array_merge($list1[2], $list2[2])); + } + + protected static $libAppend = array('list', 'val', 'separator'); + protected function libAppend($args) + { + list($list1, $value, $sep) = $args; + + $list1 = $this->coerceList($list1, ' '); + $sep = $this->listSeparatorForJoin($list1, $sep); + + return array(Type::T_LIST, $sep, array_merge($list1[2], array($value))); + } + + protected function libZip($args) + { + foreach ($args as $arg) { + $this->assertList($arg); + } + + $lists = array(); + $firstList = array_shift($args); + + foreach ($firstList[2] as $key => $item) { + $list = array(Type::T_LIST, '', array($item)); + + foreach ($args as $arg) { + if (isset($arg[2][$key])) { + $list[2][] = $arg[2][$key]; + } else { + break 2; + } + } + + $lists[] = $list; + } + + return array(Type::T_LIST, ',', $lists); + } + + protected static $libTypeOf = array('value'); + protected function libTypeOf($args) + { + $value = $args[0]; + + switch ($value[0]) { + case Type::T_KEYWORD: + if ($value === self::$true || $value === self::$false) { + return 'bool'; + } + + if ($this->coerceColor($value)) { + return 'color'; + } + + // fall-thru + case Type::T_FUNCTION: + return 'string'; + + case Type::T_LIST: + if (isset($value[3]) && $value[3]) { + return 'arglist'; + } + + // fall-thru + default: + return $value[0]; + } + } + + protected static $libUnit = array('number'); + protected function libUnit($args) + { + $num = $args[0]; + + if ($num[0] === Type::T_NUMBER) { + return array(Type::T_STRING, '"', array($num->unitStr())); + } + + return ''; + } + + protected static $libUnitless = array('number'); + protected function libUnitless($args) + { + $value = $args[0]; + + return $value[0] === Type::T_NUMBER && $value->unitless(); + } + + protected static $libComparable = array('number-1', 'number-2'); + protected function libComparable($args) + { + list($number1, $number2) = $args; + + if (! isset($number1[0]) || $number1[0] !== Type::T_NUMBER || + ! isset($number2[0]) || $number2[0] !== Type::T_NUMBER + ) { + $this->throwError('Invalid argument(s) for "comparable"'); + } + + $number1 = $number1->normalize(); + $number2 = $number2->normalize(); + + return $number1[2] === $number2[2] || $number1->unitless() || $number2->unitless(); + } + + protected static $libStrIndex = array('string', 'substring'); + protected function libStrIndex($args) + { + $string = $this->coerceString($args[0]); + $stringContent = $this->compileStringContent($string); + + $substring = $this->coerceString($args[1]); + $substringContent = $this->compileStringContent($substring); + + $result = strpos($stringContent, $substringContent); + + return $result === false ? self::$null : new Node\Number($result + 1, ''); + } + + protected static $libStrInsert = array('string', 'insert', 'index'); + protected function libStrInsert($args) + { + $string = $this->coerceString($args[0]); + $stringContent = $this->compileStringContent($string); + + $insert = $this->coerceString($args[1]); + $insertContent = $this->compileStringContent($insert); + + list(, $index) = $args[2]; + + $string[2] = array(substr_replace($stringContent, $insertContent, $index - 1, 0)); + + return $string; + } + + protected static $libStrLength = array('string'); + protected function libStrLength($args) + { + $string = $this->coerceString($args[0]); + $stringContent = $this->compileStringContent($string); + + return new Node\Number(strlen($stringContent), ''); + } + + protected static $libStrSlice = array('string', 'start-at', 'end-at'); + protected function libStrSlice($args) + { + if ($args[2][1] == 0) { + return self::$null; + } + + $string = $this->coerceString($args[0]); + $stringContent = $this->compileStringContent($string); + + $start = (int) $args[1][1] ?: 1; + $end = (int) $args[2][1]; + + $string[2] = array(substr($stringContent, $start - 1, ($end < 0 ? $end : $end - $start) + 1)); + + return $string; + } + + protected static $libToLowerCase = array('string'); + protected function libToLowerCase($args) + { + $string = $this->coerceString($args[0]); + $stringContent = $this->compileStringContent($string); + + $string[2] = array(mb_strtolower($stringContent)); + + return $string; + } + + protected static $libToUpperCase = array('string'); + protected function libToUpperCase($args) + { + $string = $this->coerceString($args[0]); + $stringContent = $this->compileStringContent($string); + + $string[2] = array(mb_strtoupper($stringContent)); + + return $string; + } + + protected static $libFeatureExists = array('feature'); + protected function libFeatureExists($args) + { + $string = $this->coerceString($args[0]); + $name = $this->compileStringContent($string); + + return $this->toBool( + array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false + ); + } + + protected static $libFunctionExists = array('name'); + protected function libFunctionExists($args) + { + $string = $this->coerceString($args[0]); + $name = $this->compileStringContent($string); + + // user defined functions + if ($this->has(self::$namespaces['function'] . $name)) { + return true; + } + + $name = $this->normalizeName($name); + + if (isset($this->userFunctions[$name])) { + return true; + } + + // built-in functions + $f = $this->getBuiltinFunction($name); + + return $this->toBool(is_callable($f)); + } + + protected static $libGlobalVariableExists = array('name'); + protected function libGlobalVariableExists($args) + { + $string = $this->coerceString($args[0]); + $name = $this->compileStringContent($string); + + return $this->has($name, $this->rootEnv); + } + + protected static $libMixinExists = array('name'); + protected function libMixinExists($args) + { + $string = $this->coerceString($args[0]); + $name = $this->compileStringContent($string); + + return $this->has(self::$namespaces['mixin'] . $name); + } + + protected static $libVariableExists = array('name'); + protected function libVariableExists($args) + { + $string = $this->coerceString($args[0]); + $name = $this->compileStringContent($string); + + return $this->has($name); + } + + /** + * Workaround IE7's content counter bug. + * + * @param array $args + */ + protected function libCounter($args) + { + $list = array_map(array($this, 'compileValue'), $args); + + return array(Type::T_STRING, '', array('counter(' . implode(',', $list) . ')')); + } + + protected function libRandom($args) + { + if (isset($args[0])) { + $n = $this->assertNumber($args[0]); + + if ($n < 1) { + $this->throwError("limit must be greater than or equal to 1"); + } + + return new Node\Number(mt_rand(1, $n), ''); + } + + return new Node\Number(mt_rand(1, mt_getrandmax()), ''); + } + + protected function libUniqueId() + { + static $id; + + if (! isset($id)) { + $id = mt_rand(0, pow(36, 8)); + } + + $id += mt_rand(0, 10) + 1; + + return array(Type::T_STRING, '', array('u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT))); + } +} diff --git a/vendor/leafo/scssphp/src/Compiler/Environment.php b/vendor/leafo/scssphp/src/Compiler/Environment.php new file mode 100644 index 000000000..f929c74c0 --- /dev/null +++ b/vendor/leafo/scssphp/src/Compiler/Environment.php @@ -0,0 +1,42 @@ + + */ +class Environment +{ + /** + * @var \Leafo\ScssPhp\Block + */ + public $block; + + /** + * @var \Leafo\ScssPhp\Compiler\Environment + */ + public $parent; + + /** + * @var array + */ + public $store; + + /** + * @var integer + */ + public $depth; +} diff --git a/vendor/leafo/scssphp/src/Formatter.php b/vendor/leafo/scssphp/src/Formatter.php new file mode 100644 index 000000000..88b7d867d --- /dev/null +++ b/vendor/leafo/scssphp/src/Formatter.php @@ -0,0 +1,202 @@ + + */ +abstract class Formatter +{ + /** + * @var integer + */ + public $indentLevel; + + /** + * @var string + */ + public $indentChar; + + /** + * @var string + */ + public $break; + + /** + * @var string + */ + public $open; + + /** + * @var string + */ + public $close; + + /** + * @var string + */ + public $tagSeparator; + + /** + * @var string + */ + public $assignSeparator; + + /** + * Initialize formatter + * + * @api + */ + abstract public function __construct(); + + /** + * Return indentation (whitespace) + * + * @return string + */ + protected function indentStr() + { + return ''; + } + + /** + * Return property assignment + * + * @api + * + * @param string $name + * @param mixed $value + * + * @return string + */ + public function property($name, $value) + { + return rtrim($name) . $this->assignSeparator . $value . ';'; + } + + /** + * Strip semi-colon appended by property(); it's a separator, not a terminator + * + * @api + * + * @param array $lines + */ + public function stripSemicolon(&$lines) + { + } + + /** + * Output lines inside a block + * + * @param \Leafo\ScssPhp\Formatter\OutputBlock $block + */ + protected function blockLines(OutputBlock $block) + { + $inner = $this->indentStr(); + + $glue = $this->break . $inner; + + echo $inner . implode($glue, $block->lines); + + if (! empty($block->children)) { + echo $this->break; + } + } + + /** + * Output block selectors + * + * @param \Leafo\ScssPhp\Formatter\OutputBlock $block + */ + protected function blockSelectors(OutputBlock $block) + { + $inner = $this->indentStr(); + + echo $inner + . implode($this->tagSeparator, $block->selectors) + . $this->open . $this->break; + } + + /** + * Output block children + * + * @param \Leafo\ScssPhp\Formatter\OutputBlock $block + */ + protected function blockChildren(OutputBlock $block) + { + foreach ($block->children as $child) { + $this->block($child); + } + } + + /** + * Output non-empty block + * + * @param \Leafo\ScssPhp\Formatter\OutputBlock $block + */ + protected function block(OutputBlock $block) + { + if (empty($block->lines) && empty($block->children)) { + return; + } + + $pre = $this->indentStr(); + + if (! empty($block->selectors)) { + $this->blockSelectors($block); + + $this->indentLevel++; + } + + if (! empty($block->lines)) { + $this->blockLines($block); + } + + if (! empty($block->children)) { + $this->blockChildren($block); + } + + if (! empty($block->selectors)) { + $this->indentLevel--; + + if (empty($block->children)) { + echo $this->break; + } + + echo $pre . $this->close . $this->break; + } + } + + /** + * Entry point to formatting a block + * + * @api + * + * @param \Leafo\ScssPhp\Formatter\OutputBlock $block An abstract syntax tree + * + * @return string + */ + public function format(OutputBlock $block) + { + ob_start(); + + $this->block($block); + + $out = ob_get_clean(); + + return $out; + } +} diff --git a/vendor/leafo/scssphp/src/Formatter/Compact.php b/vendor/leafo/scssphp/src/Formatter/Compact.php new file mode 100644 index 000000000..eda6ec24b --- /dev/null +++ b/vendor/leafo/scssphp/src/Formatter/Compact.php @@ -0,0 +1,44 @@ + + */ +class Compact extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + $this->indentLevel = 0; + $this->indentChar = ''; + $this->break = ''; + $this->open = ' {'; + $this->close = "}\n\n"; + $this->tagSeparator = ','; + $this->assignSeparator = ':'; + } + + /** + * {@inheritdoc} + */ + public function indentStr() + { + return ' '; + } +} diff --git a/vendor/leafo/scssphp/src/Formatter/Compressed.php b/vendor/leafo/scssphp/src/Formatter/Compressed.php new file mode 100644 index 000000000..c51bb26b1 --- /dev/null +++ b/vendor/leafo/scssphp/src/Formatter/Compressed.php @@ -0,0 +1,84 @@ + + */ +class Compressed extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + $this->indentLevel = 0; + $this->indentChar = ' '; + $this->break = ''; + $this->open = '{'; + $this->close = '}'; + $this->tagSeparator = ','; + $this->assignSeparator = ':'; + } + + /** + * {@inheritdoc} + */ + public function stripSemicolon(&$lines) + { + if (($count = count($lines)) + && substr($lines[$count - 1], -1) === ';' + ) { + $lines[$count - 1] = substr($lines[$count - 1], 0, -1); + } + } + + /** + * {@inheritdoc} + */ + public function blockLines(OutputBlock $block) + { + $inner = $this->indentStr(); + + $glue = $this->break . $inner; + + foreach ($block->lines as $index => $line) { + if (substr($line, 0, 2) === '/*' && substr($line, 2, 1) !== '!') { + unset($block->lines[$index]); + } elseif (substr($line, 0, 3) === '/*!') { + $block->lines[$index] = '/*' . substr($line, 3); + } + } + + echo $inner . implode($glue, $block->lines); + + if (! empty($block->children)) { + echo $this->break; + } + } + + /** + * {@inherit} + */ + public function format(OutputBlock $block) + { + return parent::format($block); + + // TODO: we need to fix the 2 "compressed" tests where the "close" is applied + return trim(str_replace(';}', '}', parent::format($block))); + } +} diff --git a/vendor/leafo/scssphp/src/Formatter/Crunched.php b/vendor/leafo/scssphp/src/Formatter/Crunched.php new file mode 100644 index 000000000..29120cc9e --- /dev/null +++ b/vendor/leafo/scssphp/src/Formatter/Crunched.php @@ -0,0 +1,71 @@ + + */ +class Crunched extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + $this->indentLevel = 0; + $this->indentChar = ' '; + $this->break = ''; + $this->open = '{'; + $this->close = '}'; + $this->tagSeparator = ','; + $this->assignSeparator = ':'; + } + + /** + * {@inheritdoc} + */ + public function stripSemicolon(&$lines) + { + if (($count = count($lines)) + && substr($lines[$count - 1], -1) === ';' + ) { + $lines[$count - 1] = substr($lines[$count - 1], 0, -1); + } + } + + /** + * {@inheritdoc} + */ + public function blockLines(OutputBlock $block) + { + $inner = $this->indentStr(); + + $glue = $this->break . $inner; + + foreach ($block->lines as $index => $line) { + if (substr($line, 0, 2) === '/*') { + unset($block->lines[$index]); + } + } + + echo $inner . implode($glue, $block->lines); + + if (! empty($block->children)) { + echo $this->break; + } + } +} diff --git a/vendor/leafo/scssphp/src/Formatter/Debug.php b/vendor/leafo/scssphp/src/Formatter/Debug.php new file mode 100644 index 000000000..e330b2d63 --- /dev/null +++ b/vendor/leafo/scssphp/src/Formatter/Debug.php @@ -0,0 +1,118 @@ + + */ +class Debug extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + $this->indentLevel = 0; + $this->indentChar = ''; + $this->break = "\n"; + $this->open = ' {'; + $this->close = ' }'; + $this->tagSeparator = ', '; + $this->assignSeparator = ': '; + } + + /** + * {@inheritdoc} + */ + protected function indentStr() + { + return str_repeat(' ', $this->indentLevel); + } + + /** + * {@inheritdoc} + */ + protected function blockLines(OutputBlock $block) + { + $indent = $this->indentStr(); + + if (empty($block->lines)) { + echo "{$indent}block->lines: []\n"; + + return; + } + + foreach ($block->lines as $index => $line) { + echo "{$indent}block->lines[{$index}]: $line\n"; + } + } + + /** + * {@inheritdoc} + */ + protected function blockSelectors(OutputBlock $block) + { + $indent = $this->indentStr(); + + if (empty($block->selectors)) { + echo "{$indent}block->selectors: []\n"; + + return; + } + + foreach ($block->selectors as $index => $selector) { + echo "{$indent}block->selectors[{$index}]: $selector\n"; + } + } + + /** + * {@inheritdoc} + */ + protected function blockChildren(OutputBlock $block) + { + $indent = $this->indentStr(); + + if (empty($block->children)) { + echo "{$indent}block->children: []\n"; + + return; + } + + $this->indentLevel++; + + foreach ($block->children as $i => $child) { + $this->block($child); + } + + $this->indentLevel--; + } + + /** + * {@inheritdoc} + */ + protected function block(OutputBlock $block) + { + $indent = $this->indentStr(); + + echo "{$indent}block->type: {$block->type}\n" . + "{$indent}block->depth: {$block->depth}\n"; + + $this->blockSelectors($block); + $this->blockLines($block); + $this->blockChildren($block); + } +} diff --git a/vendor/leafo/scssphp/src/Formatter/Expanded.php b/vendor/leafo/scssphp/src/Formatter/Expanded.php new file mode 100644 index 000000000..477318b43 --- /dev/null +++ b/vendor/leafo/scssphp/src/Formatter/Expanded.php @@ -0,0 +1,67 @@ + + */ +class Expanded extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + $this->indentLevel = 0; + $this->indentChar = ' '; + $this->break = "\n"; + $this->open = ' {'; + $this->close = '}'; + $this->tagSeparator = ', '; + $this->assignSeparator = ': '; + } + + /** + * {@inheritdoc} + */ + protected function indentStr() + { + return str_repeat($this->indentChar, $this->indentLevel); + } + + /** + * {@inheritdoc} + */ + protected function blockLines(OutputBlock $block) + { + $inner = $this->indentStr(); + + $glue = $this->break . $inner; + + foreach ($block->lines as $index => $line) { + if (substr($line, 0, 2) === '/*') { + $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line); + } + } + + echo $inner . implode($glue, $block->lines); + + if (empty($block->selectors) || ! empty($block->children)) { + echo $this->break; + } + } +} diff --git a/vendor/leafo/scssphp/src/Formatter/Nested.php b/vendor/leafo/scssphp/src/Formatter/Nested.php new file mode 100644 index 000000000..8040adaba --- /dev/null +++ b/vendor/leafo/scssphp/src/Formatter/Nested.php @@ -0,0 +1,197 @@ + + */ +class Nested extends Formatter +{ + /** + * @var integer + */ + private $depth; + + /** + * {@inheritdoc} + */ + public function __construct() + { + $this->indentLevel = 0; + $this->indentChar = ' '; + $this->break = "\n"; + $this->open = ' {'; + $this->close = ' }'; + $this->tagSeparator = ', '; + $this->assignSeparator = ': '; + } + + /** + * {@inheritdoc} + */ + protected function indentStr() + { + $n = $this->depth - 1; + + return str_repeat($this->indentChar, max($this->indentLevel + $n, 0)); + } + + /** + * {@inheritdoc} + */ + protected function blockLines(OutputBlock $block) + { + $inner = $this->indentStr(); + + $glue = $this->break . $inner; + + foreach ($block->lines as $index => $line) { + if (substr($line, 0, 2) === '/*') { + $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line); + } + } + + echo $inner . implode($glue, $block->lines); + + if (! empty($block->children)) { + echo $this->break; + } + } + + /** + * {@inheritdoc} + */ + protected function blockSelectors(OutputBlock $block) + { + $inner = $this->indentStr(); + + echo $inner + . implode($this->tagSeparator, $block->selectors) + . $this->open . $this->break; + } + + /** + * {@inheritdoc} + */ + protected function blockChildren(OutputBlock $block) + { + foreach ($block->children as $i => $child) { + $this->block($child); + + if ($i < count($block->children) - 1) { + echo $this->break; + + if (isset($block->children[$i + 1])) { + $next = $block->children[$i + 1]; + + if ($next->depth === max($block->depth, 1) && $child->depth >= $next->depth) { + echo $this->break; + } + } + } + } + } + + /** + * {@inheritdoc} + */ + protected function block(OutputBlock $block) + { + if ($block->type === 'root') { + $this->adjustAllChildren($block); + } + + if (empty($block->lines) && empty($block->children)) { + return; + } + + $this->depth = $block->depth; + + if (! empty($block->selectors)) { + $this->blockSelectors($block); + + $this->indentLevel++; + } + + if (! empty($block->lines)) { + $this->blockLines($block); + } + + if (! empty($block->children)) { + $this->blockChildren($block); + } + + if (! empty($block->selectors)) { + $this->indentLevel--; + + echo $this->close; + } + + if ($block->type === 'root') { + echo $this->break; + } + } + + /** + * Adjust the depths of all children, depth first + * + * @param \Leafo\ScssPhp\Formatter\OutputBlock $block + */ + private function adjustAllChildren(OutputBlock $block) + { + // flatten empty nested blocks + $children = array(); + + foreach ($block->children as $i => $child) { + if (empty($child->lines) && empty($child->children)) { + if (isset($block->children[$i + 1])) { + $block->children[$i + 1]->depth = $child->depth; + } + + continue; + } + + $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 + foreach ($block->children as $child) { + $this->adjustAllChildren($child); + + $child->depth = $child->depth - $block->depth; + } + } +} diff --git a/vendor/leafo/scssphp/src/Formatter/OutputBlock.php b/vendor/leafo/scssphp/src/Formatter/OutputBlock.php new file mode 100644 index 000000000..bfc383e24 --- /dev/null +++ b/vendor/leafo/scssphp/src/Formatter/OutputBlock.php @@ -0,0 +1,50 @@ + + */ +class OutputBlock +{ + /** + * @var string + */ + public $type; + + /** + * @var integer + */ + public $depth; + + /** + * @var array + */ + public $selectors; + + /** + * @var array + */ + public $lines; + + /** + * @var array + */ + public $children; + + /** + * @var \Leafo\ScssPhp\Formatter\OutputBlock + */ + public $parent; +} diff --git a/vendor/leafo/scssphp/src/Node.php b/vendor/leafo/scssphp/src/Node.php new file mode 100644 index 000000000..d09f597eb --- /dev/null +++ b/vendor/leafo/scssphp/src/Node.php @@ -0,0 +1,35 @@ + + */ +abstract class Node +{ + /** + * @var string + */ + public $type; + + /** + * @var integer + */ + public $sourcePosition; + + /** + * @var integer + */ + public $sourceIndex; +} diff --git a/vendor/leafo/scssphp/src/Node/Number.php b/vendor/leafo/scssphp/src/Node/Number.php new file mode 100644 index 000000000..a0abcd2bd --- /dev/null +++ b/vendor/leafo/scssphp/src/Node/Number.php @@ -0,0 +1,236 @@ + + */ +class Number extends Node implements \ArrayAccess +{ + /** + * @var integer + */ + static public $precision = 5; + + /** + * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/ + * + * @var array + */ + static protected $unitTable = array( + 'in' => array( + 'in' => 1, + 'pc' => 6, + 'pt' => 72, + 'px' => 96, + 'cm' => 2.54, + 'mm' => 25.4, + 'q' => 101.6, + ), + 'turn' => array( + 'deg' => 180, + 'grad' => 200, + 'rad' => M_PI, + 'turn' => 0.5, + ), + 's' => array( + 's' => 1, + 'ms' => 1000, + ), + 'Hz' => array( + 'Hz' => 1, + 'kHz' => 0.001, + ), + 'dpi' => array( + 'dpi' => 1, + 'dpcm' => 2.54, + 'dppx' => 96, + ), + ); + + /** + * @var integer|float + */ + public $dimension; + + /** + * @var string + */ + public $units; + + /** + * Initialize number + * + * @param mixed $dimension + * @param string $initialUnit + */ + public function __construct($dimension, $initialUnit) + { + $this->type = Type::T_NUMBER; + $this->dimension = $dimension; + $this->units = $initialUnit; + } + + /** + * Coerce number to target units + * + * @param array $units + * + * @return \Leafo\ScssPhp\Node\Number + */ + public function coerce($units) + { + $value = $this->dimension; + + if (isset(self::$unitTable[$this->units][$units])) { + $value *= self::$unitTable[$this->units][$units]; + } + + return new Number($value, $units); + } + + /** + * Normalize number + * + * @return \Leafo\ScssPhp\Node\Number + */ + public function normalize() + { + if (isset(self::$unitTable['in'][$this->units])) { + $conv = self::$unitTable['in'][$this->units]; + + return new Number($this->dimension / $conv, 'in'); + } + + return new Number($this->dimension, $this->units); + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) + { + if ($offset === -2) { + return $sourceIndex !== null; + } + + if ($offset === -1 + || $offset === 0 + || $offset === 1 + || $offset === 2 + ) { + return true; + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset) + { + switch ($offset) { + case -2: + return $this->sourceIndex; + + case -1: + return $this->sourcePosition; + + case 0: + return $this->type; + + case 1: + return $this->dimension; + + case 2: + return $this->units; + } + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value) + { + if ($offset === 1) { + $this->dimension = $value; + } elseif ($offset === 2) { + $this->units = $value; + } elseif ($offset == -1) { + $this->sourcePosition = $value; + } elseif ($offset == -2) { + $this->sourceIndex = $value; + } + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset) + { + if ($offset === 1) { + $this->dimension = null; + } elseif ($offset === 2) { + $this->units = null; + } elseif ($offset === -1) { + $this->sourcePosition = null; + } elseif ($offset === -2) { + $this->sourceIndex = null; + } + } + + /** + * Returns true if the number is unitless + * + * @return boolean + */ + public function unitless() + { + return empty($this->units); + } + + /** + * Returns unit(s) as the product of numerator units divided by the product of denominator units + * + * @return string + */ + public function unitStr() + { + return $this->units; + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + $value = round($this->dimension, self::$precision); + + if (empty($this->units)) { + return (string) $value; + } + + return (string) $value . $this->units; + } +} diff --git a/vendor/leafo/scssphp/src/Parser.php b/vendor/leafo/scssphp/src/Parser.php new file mode 100644 index 000000000..cbf868293 --- /dev/null +++ b/vendor/leafo/scssphp/src/Parser.php @@ -0,0 +1,2377 @@ + + */ +class Parser +{ + const SOURCE_INDEX = -1; + const SOURCE_POSITION = -2; + + /** + * @var array + */ + protected static $precedence = array( + '=' => 0, + 'or' => 1, + 'and' => 2, + '==' => 3, + '!=' => 3, + '<=>' => 3, + '<=' => 4, + '>=' => 4, + '<' => 4, + '>' => 4, + '+' => 5, + '-' => 5, + '*' => 6, + '/' => 6, + '%' => 6, + ); + + protected static $commentPattern; + protected static $operatorPattern; + protected static $whitePattern; + + private $sourceName; + private $sourceIndex; + private $charset; + private $count; + private $env; + private $inParens; + private $eatWhiteDefault; + private $buffer; + + /** + * Constructor + * + * @api + * + * @param string $sourceName + * @param integer $sourceIndex + */ + public function __construct($sourceName, $sourceIndex = 0) + { + $this->sourceName = $sourceName ?: '(stdin)'; + $this->sourceIndex = $sourceIndex; + $this->charset = null; + + if (empty(self::$operatorPattern)) { + self::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)'; + + $commentSingle = '\/\/'; + $commentMultiLeft = '\/\*'; + $commentMultiRight = '\*\/'; + + self::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight; + self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentPattern . ')\s*|\s+/Ais'; + } + } + + /** + * Get source file name + * + * @api + * + * @return string + */ + public function getSourceName() + { + return $this->sourceName; + } + + /** + * Get source line number (given character position in the buffer) + * + * @api + * + * @param integer $pos + * + * @return integer + */ + public function getLineNo($pos) + { + return 1 + substr_count(substr($this->buffer, 0, $pos), "\n"); + } + + /** + * Throw parser error + * + * @api + * + * @param string $msg + * @param integer $count + * + * @throws \Exception + */ + public function throwParseError($msg = 'parse error', $count = null) + { + $count = ! isset($count) ? $this->count : $count; + + $line = $this->getLineNo($count); + + if (! empty($this->sourceName)) { + $loc = "$this->sourceName on line $line"; + } else { + $loc = "line: $line"; + } + + if ($this->peek("(.*?)(\n|$)", $m, $count)) { + throw new \Exception("$msg: failed at `$m[1]` $loc"); + } + + throw new \Exception("$msg: $loc"); + } + + /** + * Parser buffer + * + * @api + * + * @param string $buffer + * + * @return \Leafo\ScssPhp\Block + */ + public function parse($buffer) + { + $this->count = 0; + $this->env = null; + $this->inParens = false; + $this->eatWhiteDefault = true; + $this->buffer = rtrim($buffer, "\x00..\x1f"); + + $this->pushBlock(null); // root block + + $this->whitespace(); + $this->pushBlock(null); + $this->popBlock(); + + while ($this->parseChunk()) { + ; + } + + if ($this->count !== strlen($this->buffer)) { + $this->throwParseError(); + } + + if (! empty($this->env->parent)) { + $this->throwParseError('unclosed block'); + } + + if ($this->charset) { + array_unshift($this->env->children, $this->charset); + } + + $this->env->isRoot = true; + + return $this->env; + } + + /** + * Parse a value or value list + * + * @api + * + * @param string $buffer + * @param string $out + * + * @return boolean + */ + public function parseValue($buffer, &$out) + { + $this->count = 0; + $this->env = null; + $this->inParens = false; + $this->eatWhiteDefault = true; + $this->buffer = (string) $buffer; + + return $this->valueList($out); + } + + /** + * Parse a selector or selector list + * + * @api + * + * @param string $buffer + * @param string $out + * + * @return boolean + */ + public function parseSelector($buffer, &$out) + { + $this->count = 0; + $this->env = null; + $this->inParens = false; + $this->eatWhiteDefault = true; + $this->buffer = (string) $buffer; + + return $this->selectors($out); + } + + /** + * 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 Compiler::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 Compiler::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, Compiler::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(); + + // the directives + if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') { + if ($this->literal('@at-root') && + ($this->selectors($selector) || true) && + ($this->map($with) || true) && + $this->literal('{') + ) { + $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s); + $atRoot->selector = $selector; + $atRoot->with = $with; + + return true; + } + + $this->seek($s); + + if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) { + $media = $this->pushSpecialBlock(Type::T_MEDIA, $s); + $media->queryList = $mediaQueryList[2]; + + return true; + } + + $this->seek($s); + + if ($this->literal('@mixin') && + $this->keyword($mixinName) && + ($this->argumentDef($args) || true) && + $this->literal('{') + ) { + $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s); + $mixin->name = $mixinName; + $mixin->args = $args; + + return true; + } + + $this->seek($s); + + if ($this->literal('@include') && + $this->keyword($mixinName) && + ($this->literal('(') && + ($this->argValues($argValues) || true) && + $this->literal(')') || true) && + ($this->end() || + $this->literal('{') && $hasBlock = true) + ) { + $child = array(Type::T_INCLUDE, + $mixinName, isset($argValues) ? $argValues : null, null); + + if (! empty($hasBlock)) { + $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s); + $include->child = $child; + } else { + $this->append($child, $s); + } + + return true; + } + + $this->seek($s); + + if ($this->literal('@import') && + $this->valueList($importPath) && + $this->end() + ) { + $this->append(array(Type::T_IMPORT, $importPath), $s); + + return true; + } + + $this->seek($s); + + if ($this->literal('@import') && + $this->url($importPath) && + $this->end() + ) { + $this->append(array(Type::T_IMPORT, $importPath), $s); + + return true; + } + + $this->seek($s); + + if ($this->literal('@extend') && + $this->selectors($selector) && + $this->end() + ) { + $this->append(array(Type::T_EXTEND, $selector), $s); + + return true; + } + + $this->seek($s); + + if ($this->literal('@function') && + $this->keyword($fnName) && + $this->argumentDef($args) && + $this->literal('{') + ) { + $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s); + $func->name = $fnName; + $func->args = $args; + + return true; + } + + $this->seek($s); + + if ($this->literal('@break') && $this->end()) { + $this->append(array(Type::T_BREAK), $s); + + return true; + } + + $this->seek($s); + + if ($this->literal('@continue') && $this->end()) { + $this->append(array(Type::T_CONTINUE), $s); + + return true; + } + + $this->seek($s); + + + if ($this->literal('@return') && ($this->valueList($retVal) || true) && $this->end()) { + $this->append(array(Type::T_RETURN, isset($retVal) ? $retVal : array(Type::T_NULL)), $s); + + return true; + } + + $this->seek($s); + + if ($this->literal('@each') && + $this->genericList($varNames, 'variable', ',', false) && + $this->literal('in') && + $this->valueList($list) && + $this->literal('{') + ) { + $each = $this->pushSpecialBlock(Type::T_EACH, $s); + + foreach ($varNames[2] as $varName) { + $each->vars[] = $varName[1]; + } + + $each->list = $list; + + return true; + } + + $this->seek($s); + + if ($this->literal('@while') && + $this->expression($cond) && + $this->literal('{') + ) { + $while = $this->pushSpecialBlock(Type::T_WHILE, $s); + $while->cond = $cond; + + return true; + } + + $this->seek($s); + + if ($this->literal('@for') && + $this->variable($varName) && + $this->literal('from') && + $this->expression($start) && + ($this->literal('through') || + ($forUntil = true && $this->literal('to'))) && + $this->expression($end) && + $this->literal('{') + ) { + $for = $this->pushSpecialBlock(Type::T_FOR, $s); + $for->var = $varName[1]; + $for->start = $start; + $for->end = $end; + $for->until = isset($forUntil); + + return true; + } + + $this->seek($s); + + if ($this->literal('@if') && $this->valueList($cond) && $this->literal('{')) { + $if = $this->pushSpecialBlock(Type::T_IF, $s); + $if->cond = $cond; + $if->cases = array(); + + return true; + } + + $this->seek($s); + + if ($this->literal('@debug') && + $this->valueList($value) && + $this->end() + ) { + $this->append(array(Type::T_DEBUG, $value), $s); + + return true; + } + + $this->seek($s); + + if ($this->literal('@warn') && + $this->valueList($value) && + $this->end() + ) { + $this->append(array(Type::T_WARN, $value), $s); + + return true; + } + + $this->seek($s); + + if ($this->literal('@error') && + $this->valueList($value) && + $this->end() + ) { + $this->append(array(Type::T_ERROR, $value), $s); + + return true; + } + + $this->seek($s); + + if ($this->literal('@content') && $this->end()) { + $this->append(array(Type::T_MIXIN_CONTENT), $s); + + return true; + } + + $this->seek($s); + + $last = $this->last(); + + if (isset($last) && $last[0] === Type::T_IF) { + list(, $if) = $last; + + if ($this->literal('@else')) { + if ($this->literal('{')) { + $else = $this->pushSpecialBlock(Type::T_ELSE, $s); + } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) { + $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s); + $else->cond = $cond; + } + + if (isset($else)) { + $else->dontAppend = true; + $if->cases[] = $else; + + return true; + } + } + + $this->seek($s); + } + + // only retain the first @charset directive encountered + if ($this->literal('@charset') && + $this->valueList($charset) && + $this->end() + ) { + if (! isset($this->charset)) { + $statement = array(Type::T_CHARSET, $charset); + + $statement[self::SOURCE_POSITION] = $s; + $statement[self::SOURCE_INDEX] = $this->sourceIndex; + + $this->charset = $statement; + } + + return true; + } + + $this->seek($s); + + // doesn't match built in directive, do generic one + if ($this->literal('@', false) && + $this->keyword($dirName) && + ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) && + $this->literal('{') + ) { + if ($dirName === 'media') { + $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s); + } else { + $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s); + $directive->name = $dirName; + } + + if (isset($dirValue)) { + $directive->value = $dirValue; + } + + return true; + } + + $this->seek($s); + + return false; + } + + // property shortcut + // captures most properties before having to parse a selector + if ($this->keyword($name, false) && + $this->literal(': ') && + $this->valueList($value) && + $this->end() + ) { + $name = array(Type::T_STRING, '', array($name)); + $this->append(array(Type::T_ASSIGN, $name, $value), $s); + + return true; + } + + $this->seek($s); + + // variable assigns + if ($this->variable($name) && + $this->literal(':') && + $this->valueList($value) && + $this->end() + ) { + // check for '!flag' + $assignmentFlag = $this->stripAssignmentFlag($value); + $this->append(array(Type::T_ASSIGN, $name, $value, $assignmentFlag), $s); + + return true; + } + + $this->seek($s); + + // misc + if ($this->literal('-->')) { + return true; + } + + // opening css block + if ($this->selectors($selectors) && $this->literal('{')) { + $b = $this->pushBlock($selectors, $s); + + return true; + } + + $this->seek($s); + + // property assign, or nested assign + if ($this->propertyName($name) && $this->literal(':')) { + $foundSomething = false; + + if ($this->valueList($value)) { + $this->append(array(Type::T_ASSIGN, $name, $value), $s); + $foundSomething = true; + } + + if ($this->literal('{')) { + $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s); + $propBlock->prefix = $name; + $foundSomething = true; + } elseif ($foundSomething) { + $foundSomething = $this->end(); + } + + if ($foundSomething) { + return true; + } + } + + $this->seek($s); + + // closing a block + if ($this->literal('}')) { + $block = $this->popBlock(); + + if (isset($block->type) && $block->type === Type::T_INCLUDE) { + $include = $block->child; + unset($block->child); + $include[3] = $block; + $this->append($include, $s); + } elseif (empty($block->dontAppend)) { + $type = isset($block->type) ? $block->type : Type::T_BLOCK; + $this->append(array($type, $block), $s); + } + + return true; + } + + // extra stuff + if ($this->literal(';') || + $this->literal(')|'. # comment + '(?s:<\?.*?\?>)|'. # processing instruction + # regular tags + '(?:<[/!$]?[-a-zA-Z0-9:]+\b(?>[^"\'>]+|"[^"]*"|\'[^\']*\')*>)'; + + $parts = preg_split("{($match)}", $str, -1, PREG_SPLIT_DELIM_CAPTURE); + + foreach ($parts as $part) { + if (++$index % 2 && $part != '') + $tokens[] = array('text', $part); + else + $tokens[] = array('tag', $part); + } + return $tokens; + } + +} + + +# +# SmartyPants Typographer Parser Class +# +class _SmartyPantsTypographer_TmpImpl extends \michelf\SmartyPants { + + ### Configuration Variables ### + + # Options to specify which transformations to make: + public $do_comma_quotes = 0; + public $do_guillemets = 0; + public $do_space_emdash = 0; + public $do_space_endash = 0; + public $do_space_colon = 0; + public $do_space_semicolon = 0; + public $do_space_marks = 0; + public $do_space_frenchquote = 0; + public $do_space_thousand = 0; + public $do_space_unit = 0; + + # Smart quote characters: + # Opening and closing smart double-quotes. + public $smart_doublequote_open = '“'; + public $smart_doublequote_close = '”'; + public $smart_singlequote_open = '‘'; + public $smart_singlequote_close = '’'; # Also apostrophe. + + # Space characters for different places: + # Space around em-dashes. "He_—_or she_—_should change that." + public $space_emdash = " "; + # Space around en-dashes. "He_–_or she_–_should change that." + public $space_endash = " "; + # Space before a colon. "He said_: here it is." + public $space_colon = " "; + # Space before a semicolon. "That's what I said_; that's what he said." + public $space_semicolon = " "; + # Space before a question mark and an exclamation mark: "¡_Holà_! What_?" + public $space_marks = " "; + # Space inside french quotes. "Voici la «_chose_» qui m'a attaqué." + public $space_frenchquote = " "; + # Space as thousand separator. "On compte 10_000 maisons sur cette liste." + public $space_thousand = " "; + # Space before a unit abreviation. "This 12_kg of matter costs 10_$." + public $space_unit = " "; + + # Expression of a space (breakable or not): + public $space = '(?: | | |�*160;|�*[aA]0;)'; + + + ### Parser Implementation ### + + public function __construct($attr = SMARTYPANTS_ATTR_DEFAULT) { + # + # Initialize a SmartyPantsTypographer_Parser with certain attributes. + # + # Parser attributes: + # 0 : do nothing + # 1 : set all, except dash spacing + # 2 : set all, except dash spacing, using old school en- and em- dash shortcuts + # 3 : set all, except dash spacing, using inverted old school en and em- dash shortcuts + # + # Punctuation: + # q -> quotes + # b -> backtick quotes (``double'' only) + # B -> backtick quotes (``double'' and `single') + # c -> comma quotes (,,double`` only) + # g -> guillemets (<> only) + # d -> dashes + # D -> old school dashes + # i -> inverted old school dashes + # e -> ellipses + # w -> convert " entities to " for Dreamweaver users + # + # Spacing: + # : -> colon spacing +- + # ; -> semicolon spacing +- + # m -> question and exclamation marks spacing +- + # h -> em-dash spacing +- + # H -> en-dash spacing +- + # f -> french quote spacing +- + # t -> thousand separator spacing - + # u -> unit spacing +- + # (you can add a plus sign after some of these options denoted by + to + # add the space when it is not already present, or you can add a minus + # sign to completly remove any space present) + # + # Initialize inherited SmartyPants parser. + parent::__construct($attr); + + if ($attr == "1" || $attr == "2" || $attr == "3") { + # Do everything, turn all options on. + $this->do_comma_quotes = 1; + $this->do_guillemets = 1; + $this->do_space_emdash = 1; + $this->do_space_endash = 1; + $this->do_space_colon = 1; + $this->do_space_semicolon = 1; + $this->do_space_marks = 1; + $this->do_space_frenchquote = 1; + $this->do_space_thousand = 1; + $this->do_space_unit = 1; + } + else if ($attr == "-1") { + # Special "stupefy" mode. + $this->do_stupefy = 1; + } + else { + $chars = preg_split('//', $attr); + foreach ($chars as $c){ + if ($c == "c") { $current =& $this->do_comma_quotes; } + else if ($c == "g") { $current =& $this->do_guillemets; } + else if ($c == ":") { $current =& $this->do_space_colon; } + else if ($c == ";") { $current =& $this->do_space_semicolon; } + else if ($c == "m") { $current =& $this->do_space_marks; } + else if ($c == "h") { $current =& $this->do_space_emdash; } + else if ($c == "H") { $current =& $this->do_space_endash; } + else if ($c == "f") { $current =& $this->do_space_frenchquote; } + else if ($c == "t") { $current =& $this->do_space_thousand; } + else if ($c == "u") { $current =& $this->do_space_unit; } + else if ($c == "+") { + $current = 2; + unset($current); + } + else if ($c == "-") { + $current = -1; + unset($current); + } + else { + # Unknown attribute option, ignore. + } + $current = 1; + } + } + } + + + function educate($t, $prev_token_last_char) { + $t = parent::educate($t, $prev_token_last_char); + + if ($this->do_comma_quotes) $t = $this->educateCommaQuotes($t); + if ($this->do_guillemets) $t = $this->educateGuillemets($t); + + if ($this->do_space_emdash) $t = $this->spaceEmDash($t); + if ($this->do_space_endash) $t = $this->spaceEnDash($t); + if ($this->do_space_colon) $t = $this->spaceColon($t); + if ($this->do_space_semicolon) $t = $this->spaceSemicolon($t); + if ($this->do_space_marks) $t = $this->spaceMarks($t); + if ($this->do_space_frenchquote) $t = $this->spaceFrenchQuotes($t); + if ($this->do_space_thousand) $t = $this->spaceThousandSeparator($t); + if ($this->do_space_unit) $t = $this->spaceUnit($t); + + return $t; + } + + + protected function educateQuotes($_) { + # + # Parameter: String. + # + # Returns: The string, with "educated" curly quote HTML entities. + # + # Example input: "Isn't this fun?" + # Example output: “Isn’t this fun?” + # + $dq_open = $this->smart_doublequote_open; + $dq_close = $this->smart_doublequote_close; + $sq_open = $this->smart_singlequote_open; + $sq_close = $this->smart_singlequote_close; + + # Make our own "punctuation" character class, because the POSIX-style + # [:PUNCT:] is only available in Perl 5.6 or later: + $punct_class = "[!\"#\\$\\%'()*+,-.\\/:;<=>?\\@\\[\\\\\]\\^_`{|}~]"; + + # Special case if the very first character is a quote + # followed by punctuation at a non-word-break. Close the quotes by brute force: + $_ = preg_replace( + array("/^'(?=$punct_class\\B)/", "/^\"(?=$punct_class\\B)/"), + array($sq_close, $dq_close), $_); + + # Special case for double sets of quotes, e.g.: + #

He said, "'Quoted' words in a larger quote."

+ $_ = preg_replace( + array("/\"'(?=\w)/", "/'\"(?=\w)/"), + array($dq_open.$sq_open, $sq_open.$dq_open), $_); + + # Special case for decade abbreviations (the '80s): + $_ = preg_replace("/'(?=\\d{2}s)/", $sq_close, $_); + + $close_class = '[^\ \t\r\n\[\{\(\-]'; + $dec_dashes = '&\#8211;|&\#8212;'; + + # Get most opening single quotes: + $_ = preg_replace("{ + ( + \\s | # a whitespace char, or +   | # a non-breaking space entity, or + -- | # dashes, or + &[mn]dash; | # named dash entities + $dec_dashes | # or decimal entities + &\\#x201[34]; # or hex + ) + ' # the quote + (?=\\w) # followed by a word character + }x", '\1'.$sq_open, $_); + # Single closing quotes: + $_ = preg_replace("{ + ($close_class)? + ' + (?(1)| # If $1 captured, then do nothing; + (?=\\s | s\\b) # otherwise, positive lookahead for a whitespace + ) # char or an 's' at a word ending position. This + # is a special case to handle something like: + # \"Custer's Last Stand.\" + }xi", '\1'.$sq_close, $_); + + # Any remaining single quotes should be opening ones: + $_ = str_replace("'", $sq_open, $_); + + + # Get most opening double quotes: + $_ = preg_replace("{ + ( + \\s | # a whitespace char, or +   | # a non-breaking space entity, or + -- | # dashes, or + &[mn]dash; | # named dash entities + $dec_dashes | # or decimal entities + &\\#x201[34]; # or hex + ) + \" # the quote + (?=\\w) # followed by a word character + }x", '\1'.$dq_open, $_); + + # Double closing quotes: + $_ = preg_replace("{ + ($close_class)? + \" + (?(1)|(?=\\s)) # If $1 captured, then do nothing; + # if not, then make sure the next char is whitespace. + }x", '\1'.$dq_close, $_); + + # Any remaining quotes should be opening ones. + $_ = str_replace('"', $dq_open, $_); + + return $_; + } + + + protected function educateCommaQuotes($_) { + # + # Parameter: String. + # Returns: The string, with ,,comma,, -style double quotes + # translated into HTML curly quote entities. + # + # Example input: ,,Isn't this fun?,, + # Example output: „Isn't this fun?„ + # + # Note: this is meant to be used alongside with backtick quotes; there is + # no language that use only lower quotations alone mark like in the example. + # + $_ = str_replace(",,", '„', $_); + return $_; + } + + + protected function educateGuillemets($_) { + # + # Parameter: String. + # Returns: The string, with << guillemets >> -style quotes + # translated into HTML guillemets entities. + # + # Example input: << Isn't this fun? >> + # Example output: „ Isn't this fun? „ + # + $_ = preg_replace("/(?:<|<){2}/", '«', $_); + $_ = preg_replace("/(?:>|>){2}/", '»', $_); + return $_; + } + + + protected function spaceFrenchQuotes($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # inside french-style quotes, only french quotes. + # + # Example input: Quotes in « French », »German« and »Finnish» style. + # Example output: Quotes in «_French_», »German« and »Finnish» style. + # + $opt = ( $this->do_space_frenchquote == 2 ? '?' : '' ); + $chr = ( $this->do_space_frenchquote != -1 ? $this->space_frenchquote : '' ); + + # Characters allowed immediatly outside quotes. + $outside_char = $this->space . '|\s|[.,:;!?\[\](){}|@*~=+-]|¡|¿'; + + $_ = preg_replace( + "/(^|$outside_char)(«|«|›|‹)$this->space$opt/", + "\\1\\2$chr", $_); + $_ = preg_replace( + "/$this->space$opt(»|»|‹|›)($outside_char|$)/", + "$chr\\1\\2", $_); + return $_; + } + + + protected function spaceColon($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # before colons. + # + # Example input: Ingredients : fun. + # Example output: Ingredients_: fun. + # + $opt = ( $this->do_space_colon == 2 ? '?' : '' ); + $chr = ( $this->do_space_colon != -1 ? $this->space_colon : '' ); + + $_ = preg_replace("/$this->space$opt(:)(\\s|$)/m", + "$chr\\1\\2", $_); + return $_; + } + + + protected function spaceSemicolon($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # before semicolons. + # + # Example input: There he goes ; there she goes. + # Example output: There he goes_; there she goes. + # + $opt = ( $this->do_space_semicolon == 2 ? '?' : '' ); + $chr = ( $this->do_space_semicolon != -1 ? $this->space_semicolon : '' ); + + $_ = preg_replace("/$this->space(;)(?=\\s|$)/m", + " \\1", $_); + $_ = preg_replace("/((?:^|\\s)(?>[^&;\\s]+|&#?[a-zA-Z0-9]+;)*)". + " $opt(;)(?=\\s|$)/m", + "\\1$chr\\2", $_); + return $_; + } + + + protected function spaceMarks($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # around question and exclamation marks. + # + # Example input: ¡ Holà ! What ? + # Example output: ¡_Holà_! What_? + # + $opt = ( $this->do_space_marks == 2 ? '?' : '' ); + $chr = ( $this->do_space_marks != -1 ? $this->space_marks : '' ); + + // Regular marks. + $_ = preg_replace("/$this->space$opt([?!]+)/", "$chr\\1", $_); + + // Inverted marks. + $imarks = "(?:¡|¡|¡|&#x[Aa]1;|¿|¿|¿|&#x[Bb][Ff];)"; + $_ = preg_replace("/($imarks+)$this->space$opt/", "\\1$chr", $_); + + return $_; + } + + + protected function spaceEmDash($_) { + # + # Parameters: String, two replacement characters separated by a hyphen (`-`), + # and forcing flag. + # + # Returns: The string, with appropriates spaces replaced + # around dashes. + # + # Example input: Then — without any plan — the fun happend. + # Example output: Then_—_without any plan_—_the fun happend. + # + $opt = ( $this->do_space_emdash == 2 ? '?' : '' ); + $chr = ( $this->do_space_emdash != -1 ? $this->space_emdash : '' ); + $_ = preg_replace("/$this->space$opt(—|—)$this->space$opt/", + "$chr\\1$chr", $_); + return $_; + } + + + protected function spaceEnDash($_) { + # + # Parameters: String, two replacement characters separated by a hyphen (`-`), + # and forcing flag. + # + # Returns: The string, with appropriates spaces replaced + # around dashes. + # + # Example input: Then — without any plan — the fun happend. + # Example output: Then_—_without any plan_—_the fun happend. + # + $opt = ( $this->do_space_endash == 2 ? '?' : '' ); + $chr = ( $this->do_space_endash != -1 ? $this->space_endash : '' ); + $_ = preg_replace("/$this->space$opt(–|–)$this->space$opt/", + "$chr\\1$chr", $_); + return $_; + } + + + protected function spaceThousandSeparator($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # inside numbers (thousand separator in french). + # + # Example input: Il y a 10 000 insectes amusants dans ton jardin. + # Example output: Il y a 10_000 insectes amusants dans ton jardin. + # + $chr = ( $this->do_space_thousand != -1 ? $this->space_thousand : '' ); + $_ = preg_replace('/([0-9]) ([0-9])/', "\\1$chr\\2", $_); + return $_; + } + + + protected $units = ' + ### Metric units (with prefixes) + (?: + p | + µ | µ | &\#0*181; | &\#[xX]0*[Bb]5; | + [mcdhkMGT] + )? + (?: + [mgstAKNJWCVFSTHBL]|mol|cd|rad|Hz|Pa|Wb|lm|lx|Bq|Gy|Sv|kat| + Ω | Ohm | Ω | &\#0*937; | &\#[xX]0*3[Aa]9; + )| + ### Computers units (KB, Kb, TB, Kbps) + [kKMGT]?(?:[oBb]|[oBb]ps|flops)| + ### Money + ¢ | ¢ | &\#0*162; | &\#[xX]0*[Aa]2; | + M?(?: + £ | £ | &\#0*163; | &\#[xX]0*[Aa]3; | + ¥ | ¥ | &\#0*165; | &\#[xX]0*[Aa]5; | + € | € | &\#0*8364; | &\#[xX]0*20[Aa][Cc]; | + $ + )| + ### Other units + (?: ° | ° | &\#0*176; | &\#[xX]0*[Bb]0; ) [CF]? | + %|pt|pi|M?px|em|en|gal|lb|[NSEOW]|[NS][EOW]|ha|mbar + '; //x + + protected function spaceUnit($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # before unit symbols. + # + # Example input: Get 3 mol of fun for 3 $. + # Example output: Get 3_mol of fun for 3_$. + # + $opt = ( $this->do_space_unit == 2 ? '?' : '' ); + $chr = ( $this->do_space_unit != -1 ? $this->space_unit : '' ); + + $_ = preg_replace('/ + (?:([0-9])[ ]'.$opt.') # Number followed by space. + ('.$this->units.') # Unit. + (?![a-zA-Z0-9]) # Negative lookahead for other unit characters. + /x', + "\\1$chr\\2", $_); + + return $_; + } + + + protected function spaceAbbr($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # around abbreviations. + # + # Example input: Fun i.e. something pleasant. + # Example output: Fun i.e._something pleasant. + # + $opt = ( $this->do_space_abbr == 2 ? '?' : '' ); + + $_ = preg_replace("/(^|\s)($this->abbr_after) $opt/m", + "\\1\\2$this->space_abbr", $_); + $_ = preg_replace("/( )$opt($this->abbr_sp_before)(?![a-zA-Z'])/m", + "\\1$this->space_abbr\\2", $_); + return $_; + } + + + protected function stupefyEntities($_) { + # + # Adding angle quotes and lower quotes to SmartyPants's stupefy mode. + # + $_ = parent::stupefyEntities($_); + + $_ = str_replace(array('„', '«', '»'), '"', $_); + + return $_; + } + + + protected function processEscapes($_) { + # + # Adding a few more escapes to SmartyPants's escapes: + # + # Escape Value + # ------ ----- + # \, , + # \< < + # \> > + # + $_ = parent::processEscapes($_); + + $_ = str_replace( + array('\,', '\<', '\>', '\<', '\>'), + array(',', '<', '>', '<', '>'), $_); + + return $_; + } +} + + +?> \ No newline at end of file diff --git a/vendor/michelf/php-smartypants/Michelf/SmartyPantsTypographer.php b/vendor/michelf/php-smartypants/Michelf/SmartyPantsTypographer.php new file mode 100644 index 000000000..65f499f3b --- /dev/null +++ b/vendor/michelf/php-smartypants/Michelf/SmartyPantsTypographer.php @@ -0,0 +1,37 @@ + +# +# Original SmartyPants +# Copyright (c) 2003-2004 John Gruber +# +# +namespace michelf; + + +# +# SmartyPants Typographer Parser Class +# +# Note: Currently the implementation resides in the temporary class +# \michelf\_SmartyPantsTypographer_TmpImpl (in the same file as +# \michelf\SmartyPants). This makes it easier to propagate the changes between +# the three different packaging styles of PHP SmartyPants. Once this issue is +# resolved, the _SmartyPantsTypographer_TmpImpl class will disappear and this +# one will contain the code. +# +use \michelf\SmartyPants; + +class SmartyPantsTypographer extends \michelf\_SmartyPantsTypographer_TmpImpl { + + ### Parser Implementation ### + + # Temporarily, the implemenation is in the _SmartyPantsTypographer_TmpImpl + # class. See note above. + +} + +?> \ No newline at end of file diff --git a/vendor/michelf/php-smartypants/Readme.md b/vendor/michelf/php-smartypants/Readme.md new file mode 100644 index 000000000..3023b9a10 --- /dev/null +++ b/vendor/michelf/php-smartypants/Readme.md @@ -0,0 +1,362 @@ +PHP SmartyPants +=============== + +PHP SmartyPants Lib 1.6.0-beta1 - Sun 23 Jan 2013 + +by Michel Fortin + + +Original SmartyPants by John Gruber + + + +Introduction +------------ + +This is a library package that includes the PHP SmartyPants and its +sibling PHP SmartyPants Typographer with additional features. + +SmartyPants is a free web typography prettifyier tool for web writers. It +easily translates plain ASCII punctuation characters into "smart" typographic +punctuation HTML entities. + +PHP SmartyPants is a port to PHP of the original SmartyPants written +in Perl by John Gruber. + +SmartyPants can perform the following transformations: + +* Straight quotes (`"` and `'`) into “curly” quote HTML entities +* Backtick-style quotes (` ``like this'' `) into “curly” quote HTML + entities +* Dashes (`--` and `---`) into en- and em-dash entities +* Three consecutive dots (`...`) into an ellipsis entity + +SmartyPants Typographer can perform those additional transformations: + +* French guillements done using (`<<` and `>>`) into true « guillemets » + HTML entities. +* Replace existing spaces with non-break spaces around punctuation marks + where appropriate, can also add or remove them if configured to. +* Replace existing spaces with non-break spaces for spaces used as + a thousand separator and between a number and the unit symbol that + follows it (for most common units). + +This means you can write, edit, and save using plain old ASCII straight +quotes, plain dashes, and plain dots, but your published posts (and +final HTML output) will appear with smart quotes, em-dashes, proper +ellipses, and proper no-break spaces (with Typographer). + +SmartyPants does not modify characters within `
`, ``,
+``, or `