File manager - Edit - /home/ferretapmx/public_html/lib.zip
Back
PK � �\?K���8 �8 icons.phpnu �[��� <?php /** * @package Helix_Ultimate_Framework * @author JoomShaper <support@joomshaper.com> * Copyright (c) 2010 - 2025 JoomShaper * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPLv2 or Later */ namespace HelixUltimate\Framework\Core\Lib; defined('_JEXEC') or die('restricted aceess'); /** * Class for font-awesome icons. * * @since 2.0.0 */ class FontawesomeIcons { /** * Font awesome icon class names. * * @var array Font-awesome class names. * @since 2.0.0 */ private $fa_class_lists = array( 'fa-500px', 'fa-adjust', 'fa-adn', 'fa-align-center', 'fa-align-justify', 'fa-align-left', 'fa-align-right', 'fa-amazon', 'fa-ambulance', 'fa-anchor', 'fa-android', 'fa-angellist', 'fa-angle-double-down', 'fa-angle-double-left', 'fa-angle-double-right', 'fa-angle-double-up', 'fa-angle-down', 'fa-angle-left', 'fa-angle-right', 'fa-angle-up', 'fa-apple', 'fa-archive', 'fa-area-chart', 'fa-arrow-circle-down', 'fa-arrow-circle-left', 'fa-arrow-circle-o-down', 'fa-arrow-circle-o-left', 'fa-arrow-circle-o-right', 'fa-arrow-circle-o-up', 'fa-arrow-circle-right', 'fa-arrow-circle-up', 'fa-arrow-down', 'fa-arrow-left', 'fa-arrow-right', 'fa-arrow-up', 'fa-arrows', 'fa-arrows-alt', 'fa-arrows-h', 'fa-arrows-v', 'fa-asterisk', 'fa-at', 'fa-automobile', 'fa-backward', 'fa-balance-scale', 'fa-ban', 'fa-bank', 'fa-bar-chart', 'fa-bar-chart-o', 'fa-barcode', 'fa-bars', 'fa-battery-0', 'fa-battery-1', 'fa-battery-2', 'fa-battery-3', 'fa-battery-4', 'fa-battery-empty', 'fa-battery-full', 'fa-battery-half', 'fa-battery-quarter', 'fa-battery-three-quarters', 'fa-bed', 'fa-beer', 'fa-behance', 'fa-behance-square', 'fa-bell', 'fa-bell-o', 'fa-bell-slash', 'fa-bell-slash-o', 'fa-bicycle', 'fa-binoculars', 'fa-birthday-cake', 'fa-bitbucket', 'fa-bitbucket-square', 'fa-bitcoin', 'fa-black-tie', 'fa-bluetooth', 'fa-bluetooth-b', 'fa-bold', 'fa-bolt', 'fa-bomb', 'fa-book', 'fa-bookmark', 'fa-bookmark-o', 'fa-briefcase', 'fa-btc', 'fa-bug', 'fa-building', 'fa-building-o', 'fa-bullhorn', 'fa-bullseye', 'fa-bus', 'fa-buysellads', 'fa-cab', 'fa-calculator', 'fa-calendar', 'fa-calendar-check-o', 'fa-calendar-minus-o', 'fa-calendar-o', 'fa-calendar-plus-o', 'fa-calendar-times-o', 'fa-camera', 'fa-camera-retro', 'fa-car', 'fa-caret-down', 'fa-caret-left', 'fa-caret-right', 'fa-caret-square-o-down', 'fa-caret-square-o-left', 'fa-caret-square-o-right', 'fa-caret-square-o-up', 'fa-caret-up', 'fa-cart-arrow-down', 'fa-cart-plus', 'fa-cc', 'fa-cc-amex', 'fa-cc-diners-club', 'fa-cc-discover', 'fa-cc-jcb', 'fa-cc-mastercard', 'fa-cc-paypal', 'fa-cc-stripe', 'fa-cc-visa', 'fa-certificate', 'fa-chain', 'fa-chain-broken', 'fa-check', 'fa-check-circle', 'fa-check-circle-o', 'fa-check-square', 'fa-check-square-o', 'fa-chevron-circle-down', 'fa-chevron-circle-left', 'fa-chevron-circle-right', 'fa-chevron-circle-up', 'fa-chevron-down', 'fa-chevron-left', 'fa-chevron-right', 'fa-chevron-up', 'fa-child', 'fa-chrome', 'fa-circle', 'fa-circle-o', 'fa-circle-o-notch', 'fa-circle-thin', 'fa-clipboard', 'fa-clock-o', 'fa-clone', 'fa-close', 'fa-cloud', 'fa-cloud-download', 'fa-cloud-upload', 'fa-cny', 'fa-code', 'fa-code-fork', 'fa-codepen', 'fa-codiepie', 'fa-coffee', 'fa-cog', 'fa-cogs', 'fa-columns', 'fa-comment', 'fa-comment-o', 'fa-commenting', 'fa-commenting-o', 'fa-comments', 'fa-comments-o', 'fa-compass', 'fa-compress', 'fa-connectdevelop', 'fa-contao', 'fa-copy', 'fa-copyright', 'fa-creative-commons', 'fa-credit-card', 'fa-credit-card-alt', 'fa-crop', 'fa-crosshairs', 'fa-css3', 'fa-cube', 'fa-cubes', 'fa-cut', 'fa-cutlery', 'fa-dashboard', 'fa-dashcube', 'fa-database', 'fa-dedent', 'fa-delicious', 'fa-desktop', 'fa-deviantart', 'fa-diamond', 'fa-digg', 'fa-dollar', 'fa-dot-circle-o', 'fa-download', 'fa-dribbble', 'fa-dropbox', 'fa-drupal', 'fa-edge', 'fa-edit', 'fa-eject', 'fa-ellipsis-h', 'fa-ellipsis-v', 'fa-empire', 'fa-envelope', 'fa-envelope-o', 'fa-envelope-square', 'fa-eraser', 'fa-eur', 'fa-euro', 'fa-exchange', 'fa-exclamation', 'fa-exclamation-circle', 'fa-exclamation-triangle', 'fa-expand', 'fa-expeditedssl', 'fa-external-link', 'fa-external-link-square', 'fa-eye', 'fa-eye-slash', 'fa-eyedropper', 'fa-facebook', 'fa-facebook-f', 'fa-facebook-official', 'fa-facebook-square', 'fa-fast-backward', 'fa-fast-forward', 'fa-fax', 'fa-feed', 'fa-female', 'fa-fighter-jet', 'fa-file', 'fa-file-archive-o', 'fa-file-audio-o', 'fa-file-code-o', 'fa-file-excel-o', 'fa-file-image-o', 'fa-file-movie-o', 'fa-file-o', 'fa-file-pdf-o', 'fa-file-photo-o', 'fa-file-picture-o', 'fa-file-powerpoint-o', 'fa-file-sound-o', 'fa-file-text', 'fa-file-text-o', 'fa-file-video-o', 'fa-file-word-o', 'fa-file-zip-o', 'fa-files-o', 'fa-film', 'fa-filter', 'fa-fire', 'fa-fire-extinguisher', 'fa-firefox', 'fa-flag', 'fa-flag-checkered', 'fa-flag-o', 'fa-flash', 'fa-flask', 'fa-flickr', 'fa-floppy-o', 'fa-folder', 'fa-folder-o', 'fa-folder-open', 'fa-folder-open-o', 'fa-font', 'fa-fonticons', 'fa-fort-awesome', 'fa-forumbee', 'fa-forward', 'fa-foursquare', 'fa-frown-o', 'fa-futbol-o', 'fa-gamepad', 'fa-gavel', 'fa-gbp', 'fa-ge', 'fa-gear', 'fa-gears', 'fa-genderless', 'fa-get-pocket', 'fa-gg', 'fa-gg-circle', 'fa-gift', 'fa-git', 'fa-git-square', 'fa-github', 'fa-github-alt', 'fa-github-square', 'fa-gittip', 'fa-glass', 'fa-globe', 'fa-google', 'fa-google-plus', 'fa-google-plus-square', 'fa-google-wallet', 'fa-graduation-cap', 'fa-gratipay', 'fa-group', 'fa-h-square', 'fa-hacker-news', 'fa-hand-grab-o', 'fa-hand-lizard-o', 'fa-hand-o-down', 'fa-hand-o-left', 'fa-hand-o-right', 'fa-hand-o-up', 'fa-hand-paper-o', 'fa-hand-peace-o', 'fa-hand-pointer-o', 'fa-hand-rock-o', 'fa-hand-scissors-o', 'fa-hand-spock-o', 'fa-hand-stop-o', 'fa-hashtag', 'fa-hdd-o', 'fa-header', 'fa-headphones', 'fa-heart', 'fa-heart-o', 'fa-heartbeat', 'fa-history', 'fa-home', 'fa-hospital-o', 'fa-hotel', 'fa-hourglass', 'fa-hourglass-1', 'fa-hourglass-2', 'fa-hourglass-3', 'fa-hourglass-end', 'fa-hourglass-half', 'fa-hourglass-o', 'fa-hourglass-start', 'fa-houzz', 'fa-html5', 'fa-i-cursor', 'fa-ils', 'fa-image', 'fa-inbox', 'fa-indent', 'fa-industry', 'fa-info', 'fa-info-circle', 'fa-inr', 'fa-instagram', 'fa-institution', 'fa-internet-explorer', 'fa-intersex', 'fa-ioxhost', 'fa-italic', 'fa-joomla', 'fa-jpy', 'fa-jsfiddle', 'fa-key', 'fa-keyboard-o', 'fa-krw', 'fa-language', 'fa-laptop', 'fa-lastfm', 'fa-lastfm-square', 'fa-leaf', 'fa-leanpub', 'fa-legal', 'fa-lemon-o', 'fa-level-down', 'fa-level-up', 'fa-life-bouy', 'fa-life-buoy', 'fa-life-ring', 'fa-life-saver', 'fa-lightbulb-o', 'fa-line-chart', 'fa-link', 'fa-linkedin', 'fa-linkedin-square', 'fa-linux', 'fa-list', 'fa-list-alt', 'fa-list-ol', 'fa-list-ul', 'fa-location-arrow', 'fa-lock', 'fa-long-arrow-down', 'fa-long-arrow-left', 'fa-long-arrow-right', 'fa-long-arrow-up', 'fa-magic', 'fa-magnet', 'fa-mail-forward', 'fa-mail-reply', 'fa-mail-reply-all', 'fa-male', 'fa-map', 'fa-map-marker', 'fa-map-o', 'fa-map-pin', 'fa-map-signs', 'fa-mars', 'fa-mars-double', 'fa-mars-stroke', 'fa-mars-stroke-h', 'fa-mars-stroke-v', 'fa-maxcdn', 'fa-meanpath', 'fa-medium', 'fa-medkit', 'fa-meh-o', 'fa-mercury', 'fa-microphone', 'fa-microphone-slash', 'fa-minus', 'fa-minus-circle', 'fa-minus-square', 'fa-minus-square-o', 'fa-mixcloud', 'fa-mobile', 'fa-mobile-phone', 'fa-modx', 'fa-money', 'fa-moon-o', 'fa-mortar-board', 'fa-motorcycle', 'fa-mouse-pointer', 'fa-music', 'fa-navicon', 'fa-neuter', 'fa-newspaper-o', 'fa-object-group', 'fa-object-ungroup', 'fa-odnoklassniki', 'fa-odnoklassniki-square', 'fa-opencart', 'fa-openid', 'fa-opera', 'fa-optin-monster', 'fa-outdent', 'fa-pagelines', 'fa-paint-brush', 'fa-paper-plane', 'fa-paper-plane-o', 'fa-paperclip', 'fa-paragraph', 'fa-paste', 'fa-pause', 'fa-pause-circle', 'fa-pause-circle-o', 'fa-paw', 'fa-paypal', 'fa-pencil', 'fa-pencil-square', 'fa-pencil-square-o', 'fa-percent', 'fa-phone', 'fa-phone-square', 'fa-photo', 'fa-picture-o', 'fa-pie-chart', 'fa-pied-piper', 'fa-pied-piper-alt', 'fa-pinterest', 'fa-pinterest-p', 'fa-pinterest-square', 'fa-plane', 'fa-play', 'fa-play-circle', 'fa-play-circle-o', 'fa-plug', 'fa-plus', 'fa-plus-circle', 'fa-plus-square', 'fa-plus-square-o', 'fa-power-off', 'fa-print', 'fa-product-hunt', 'fa-puzzle-piece', 'fa-qq', 'fa-qrcode', 'fa-question', 'fa-question-circle', 'fa-quote-left', 'fa-quote-right', 'fa-ra', 'fa-random', 'fa-rebel', 'fa-recycle', 'fa-reddit', 'fa-reddit-alien', 'fa-reddit-square', 'fa-refresh', 'fa-registered', 'fa-remove', 'fa-renren', 'fa-reorder', 'fa-repeat', 'fa-reply', 'fa-reply-all', 'fa-retweet', 'fa-rmb', 'fa-road', 'fa-rocket', 'fa-rotate-left', 'fa-rotate-right', 'fa-rouble', 'fa-rss', 'fa-rss-square', 'fa-rub', 'fa-ruble', 'fa-rupee', 'fa-safari', 'fa-save', 'fa-scissors', 'fa-scribd', 'fa-search', 'fa-search-minus', 'fa-search-plus', 'fa-sellsy', 'fa-send', 'fa-send-o', 'fa-server', 'fa-share', 'fa-share-alt', 'fa-share-alt-square', 'fa-share-square', 'fa-share-square-o', 'fa-shekel', 'fa-sheqel', 'fa-shield', 'fa-ship', 'fa-shirtsinbulk', 'fa-shopping-bag', 'fa-shopping-basket', 'fa-shopping-cart', 'fa-sign-in', 'fa-sign-out', 'fa-signal', 'fa-simplybuilt', 'fa-sitemap', 'fa-skyatlas', 'fa-slack', 'fa-sliders-h', 'fa-slideshare', 'fa-smile-o', 'fa-soccer-ball-o', 'fa-sort', 'fa-sort-alpha-asc', 'fa-sort-alpha-desc', 'fa-sort-amount-asc', 'fa-sort-amount-desc', 'fa-sort-asc', 'fa-sort-desc', 'fa-sort-down', 'fa-sort-numeric-asc', 'fa-sort-numeric-desc', 'fa-sort-up', 'fa-soundcloud', 'fa-space-shuttle', 'fa-spinner', 'fa-spoon', 'fa-spotify', 'fa-square', 'fa-square-o', 'fa-stack-exchange', 'fa-stack-overflow', 'fa-star', 'fa-star-half', 'fa-star-half-empty', 'fa-star-half-full', 'fa-star-half-o', 'fa-star-o', 'fa-steam', 'fa-steam-square', 'fa-step-backward', 'fa-step-forward', 'fa-stethoscope', 'fa-sticky-note', 'fa-sticky-note-o', 'fa-stop', 'fa-stop-circle', 'fa-stop-circle-o', 'fa-street-view', 'fa-strikethrough', 'fa-stumbleupon', 'fa-stumbleupon-circle', 'fa-subscript', 'fa-subway', 'fa-suitcase', 'fa-sun-o', 'fa-superscript', 'fa-support', 'fa-table', 'fa-tablet', 'fa-tachometer', 'fa-tag', 'fa-tags', 'fa-tasks', 'fa-taxi', 'fa-television', 'fa-tencent-weibo', 'fa-terminal', 'fa-text-height', 'fa-text-width', 'fa-th', 'fa-th-large', 'fa-th-list', 'fa-thumb-tack', 'fa-thumbs-down', 'fa-thumbs-o-down', 'fa-thumbs-o-up', 'fa-thumbs-up', 'fa-ticket', 'fa-times', 'fa-times-circle', 'fa-times-circle-o', 'fa-tint', 'fa-toggle-down', 'fa-toggle-left', 'fa-toggle-off', 'fa-toggle-on', 'fa-toggle-right', 'fa-toggle-up', 'fa-trademark', 'fa-train', 'fa-transgender', 'fa-transgender-alt', 'fa-trash', 'fa-trash-o', 'fa-tree', 'fa-trello', 'fa-tripadvisor', 'fa-trophy', 'fa-truck', 'fa-try', 'fa-tty', 'fa-tumblr', 'fa-tumblr-square', 'fa-turkish-lira', 'fa-tv', 'fa-twitch', 'fa-twitter', 'fa-twitter-square', 'fa-umbrella', 'fa-underline', 'fa-undo', 'fa-university', 'fa-unlink', 'fa-unlock', 'fa-unlock-alt', 'fa-unsorted', 'fa-upload', 'fa-usb', 'fa-usd', 'fa-user', 'fa-user-md', 'fa-user-plus', 'fa-user-secret', 'fa-user-times', 'fa-users', 'fa-venus', 'fa-venus-double', 'fa-venus-mars', 'fa-viacoin', 'fa-video-camera', 'fa-vimeo', 'fa-vimeo-square', 'fa-vine', 'fa-vk', 'fa-volume-down', 'fa-volume-off', 'fa-volume-up', 'fa-warning', 'fa-wechat', 'fa-weibo', 'fa-weixin', 'fa-whatsapp', 'fa-wheelchair', 'fa-wifi', 'fa-wikipedia-w', 'fa-windows', 'fa-won', 'fa-wordpress', 'fa-wrench', 'fa-xing', 'fa-xing-square', 'fa-y-combinator', 'fa-y-combinator-square', 'fa-yahoo', 'fa-yc', 'fa-yc-square', 'fa-yelp', 'fa-yen', 'fa-youtube', 'fa-youtube-play', 'fa-youtube-square', // New 'fa-address-book', 'fa-address-book-o', 'fa-address-card', 'fa-address-card-o', 'fa-vcard', 'fa-vcard-o', 'fa-bandcamp', 'fa-bathtub', 'fa-s15', 'fa-bath', 'fa-id-card', 'fa-drivers-license-o', 'fa-id-card-o', 'fa-eercast', 'fa-envelope-open', 'fa-envelope-open-o', 'fa-etsy', 'fa-free-code-camp', 'fa-grav', 'fa-handshake-o', 'fa-id-badge', 'fa-id-card-o', 'fa-imdb', 'fa-linode', 'fa-meetup', 'fa-microchip', 'fa-podcast', 'fa-quora', 'fa-ravelry', 'fa-shower', 'fa-snowflake-o', 'fa-superpowers', 'fa-telegram', 'fa-thermometer', 'fa-thermometer-full', 'fa-thermometer-4', 'fa-thermometer-3', 'fa-thermometer-three-quarters', 'fa-thermometer-2', 'fa-thermometer-half', 'fa-thermometer-1', 'fa-thermometer-quarter', 'fa-thermometer-0', 'fa-thermometer-empty', 'fa-window-close', 'fa-window-close-o', 'fa-user-circle', 'fa-user-circle-o', 'fa-user-o', 'fa-window-maximize', 'fa-window-restore', 'fa-wpexplorer' ); /** * Fontawesome icons array. * * @var array Fontawesome icons. * @since 2.0.0 */ private $icons = array(); /** * Constructor function. * * @return void * @since 2.0.0 */ public function __construct() { $this->icons = $this->fa_class_lists; } /** * Get icon list. * * @return array Fontawesome icons. * @Since 1.0.0 */ public function getIcons() { return $this->icons; } /** * Add icon into the array. * * @param string $icon The icon class * * @return void * @since 2.0.0 */ public function addIcon($icon) { $this->icons[] = $icon; } } PK � �\�\ fonts.phpnu �[��� <?php /** * @package Helix_Ultimate_Framework * @author JoomShaper <support@joomshaper.com> * @copyright Copyright (c) 2010 - 2025 JoomShaper * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPLv2 or Later */ namespace HelixUltimate\Framework\Core\Lib; defined('_JEXEC') or die('Restricted access'); $systemFonts = array( 'Arial' => array( 'weights' => array( 'regular', 'italic', 'bold', 'bold italic' ), ), 'Tahoma', 'Verdana', 'Helvetica', 'Times New Roman', 'Trebuchet MS', 'Georgia' ); PK � �\��8 �8 fa.phpnu �[��� <?php /** * @package Helix Ultimate Framework * @author JoomShaper https://www.joomshaper.com * @copyright Copyright (c) 2010 - 2018 JoomShaper * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPLv2 or Later */ defined ('_JEXEC') or die ('restricted aceess'); $fa_list = array( 'fa-500px', 'fa-adjust', 'fa-adn', 'fa-align-center', 'fa-align-justify', 'fa-align-left', 'fa-align-right', 'fa-amazon', 'fa-ambulance', 'fa-anchor', 'fa-android', 'fa-angellist', 'fa-angle-double-down', 'fa-angle-double-left', 'fa-angle-double-right', 'fa-angle-double-up', 'fa-angle-down', 'fa-angle-left', 'fa-angle-right', 'fa-angle-up', 'fa-apple', 'fa-archive', 'fa-area-chart', 'fa-arrow-circle-down', 'fa-arrow-circle-left', 'fa-arrow-circle-o-down', 'fa-arrow-circle-o-left', 'fa-arrow-circle-o-right', 'fa-arrow-circle-o-up', 'fa-arrow-circle-right', 'fa-arrow-circle-up', 'fa-arrow-down', 'fa-arrow-left', 'fa-arrow-right', 'fa-arrow-up', 'fa-arrows', 'fa-arrows-alt', 'fa-arrows-h', 'fa-arrows-v', 'fa-asterisk', 'fa-at', 'fa-automobile', 'fa-backward', 'fa-balance-scale', 'fa-ban', 'fa-bank', 'fa-bar-chart', 'fa-bar-chart-o', 'fa-barcode', 'fa-bars', 'fa-battery-0', 'fa-battery-1', 'fa-battery-2', 'fa-battery-3', 'fa-battery-4', 'fa-battery-empty', 'fa-battery-full', 'fa-battery-half', 'fa-battery-quarter', 'fa-battery-three-quarters', 'fa-bed', 'fa-beer', 'fa-behance', 'fa-behance-square', 'fa-bell', 'fa-bell-o', 'fa-bell-slash', 'fa-bell-slash-o', 'fa-bicycle', 'fa-binoculars', 'fa-birthday-cake', 'fa-bitbucket', 'fa-bitbucket-square', 'fa-bitcoin', 'fa-black-tie', 'fa-bluetooth', 'fa-bluetooth-b', 'fa-bold', 'fa-bolt', 'fa-bomb', 'fa-book', 'fa-bookmark', 'fa-bookmark-o', 'fa-briefcase', 'fa-btc', 'fa-bug', 'fa-building', 'fa-building-o', 'fa-bullhorn', 'fa-bullseye', 'fa-bus', 'fa-buysellads', 'fa-cab', 'fa-calculator', 'fa-calendar', 'fa-calendar-check-o', 'fa-calendar-minus-o', 'fa-calendar-o', 'fa-calendar-plus-o', 'fa-calendar-times-o', 'fa-camera', 'fa-camera-retro', 'fa-car', 'fa-caret-down', 'fa-caret-left', 'fa-caret-right', 'fa-caret-square-o-down', 'fa-caret-square-o-left', 'fa-caret-square-o-right', 'fa-caret-square-o-up', 'fa-caret-up', 'fa-cart-arrow-down', 'fa-cart-plus', 'fa-cc', 'fa-cc-amex', 'fa-cc-diners-club', 'fa-cc-discover', 'fa-cc-jcb', 'fa-cc-mastercard', 'fa-cc-paypal', 'fa-cc-stripe', 'fa-cc-visa', 'fa-certificate', 'fa-chain', 'fa-chain-broken', 'fa-check', 'fa-check-circle', 'fa-check-circle-o', 'fa-check-square', 'fa-check-square-o', 'fa-chevron-circle-down', 'fa-chevron-circle-left', 'fa-chevron-circle-right', 'fa-chevron-circle-up', 'fa-chevron-down', 'fa-chevron-left', 'fa-chevron-right', 'fa-chevron-up', 'fa-child', 'fa-chrome', 'fa-circle', 'fa-circle-o', 'fa-circle-o-notch', 'fa-circle-thin', 'fa-clipboard', 'fa-clock-o', 'fa-clone', 'fa-close', 'fa-cloud', 'fa-cloud-download', 'fa-cloud-upload', 'fa-cny', 'fa-code', 'fa-code-fork', 'fa-codepen', 'fa-codiepie', 'fa-coffee', 'fa-cog', 'fa-cogs', 'fa-columns', 'fa-comment', 'fa-comment-o', 'fa-commenting', 'fa-commenting-o', 'fa-comments', 'fa-comments-o', 'fa-compass', 'fa-compress', 'fa-connectdevelop', 'fa-contao', 'fa-copy', 'fa-copyright', 'fa-creative-commons', 'fa-credit-card', 'fa-credit-card-alt', 'fa-crop', 'fa-crosshairs', 'fa-css3', 'fa-cube', 'fa-cubes', 'fa-cut', 'fa-cutlery', 'fa-dashboard', 'fa-dashcube', 'fa-database', 'fa-dedent', 'fa-delicious', 'fa-desktop', 'fa-deviantart', 'fa-diamond', 'fa-digg', 'fa-dollar', 'fa-dot-circle-o', 'fa-download', 'fa-dribbble', 'fa-dropbox', 'fa-drupal', 'fa-edge', 'fa-edit', 'fa-eject', 'fa-ellipsis-h', 'fa-ellipsis-v', 'fa-empire', 'fa-envelope', 'fa-envelope-o', 'fa-envelope-square', 'fa-eraser', 'fa-eur', 'fa-euro', 'fa-exchange', 'fa-exclamation', 'fa-exclamation-circle', 'fa-exclamation-triangle', 'fa-expand', 'fa-expeditedssl', 'fa-external-link', 'fa-external-link-square', 'fa-eye', 'fa-eye-slash', 'fa-eyedropper', 'fa-facebook', 'fa-facebook-f', 'fa-facebook-official', 'fa-facebook-square', 'fa-fast-backward', 'fa-fast-forward', 'fa-fax', 'fa-feed', 'fa-female', 'fa-fighter-jet', 'fa-file', 'fa-file-archive-o', 'fa-file-audio-o', 'fa-file-code-o', 'fa-file-excel-o', 'fa-file-image-o', 'fa-file-movie-o', 'fa-file-o', 'fa-file-pdf-o', 'fa-file-photo-o', 'fa-file-picture-o', 'fa-file-powerpoint-o', 'fa-file-sound-o', 'fa-file-text', 'fa-file-text-o', 'fa-file-video-o', 'fa-file-word-o', 'fa-file-zip-o', 'fa-files-o', 'fa-film', 'fa-filter', 'fa-fire', 'fa-fire-extinguisher', 'fa-firefox', 'fa-flag', 'fa-flag-checkered', 'fa-flag-o', 'fa-flash', 'fa-flask', 'fa-flickr', 'fa-floppy-o', 'fa-folder', 'fa-folder-o', 'fa-folder-open', 'fa-folder-open-o', 'fa-font', 'fa-fonticons', 'fa-fort-awesome', 'fa-forumbee', 'fa-forward', 'fa-foursquare', 'fa-frown-o', 'fa-futbol-o', 'fa-gamepad', 'fa-gavel', 'fa-gbp', 'fa-ge', 'fa-gear', 'fa-gears', 'fa-genderless', 'fa-get-pocket', 'fa-gg', 'fa-gg-circle', 'fa-gift', 'fa-git', 'fa-git-square', 'fa-github', 'fa-github-alt', 'fa-github-square', 'fa-gittip', 'fa-glass', 'fa-globe', 'fa-google', 'fa-google-plus', 'fa-google-plus-square', 'fa-google-wallet', 'fa-graduation-cap', 'fa-gratipay', 'fa-group', 'fa-h-square', 'fa-hacker-news', 'fa-hand-grab-o', 'fa-hand-lizard-o', 'fa-hand-o-down', 'fa-hand-o-left', 'fa-hand-o-right', 'fa-hand-o-up', 'fa-hand-paper-o', 'fa-hand-peace-o', 'fa-hand-pointer-o', 'fa-hand-rock-o', 'fa-hand-scissors-o', 'fa-hand-spock-o', 'fa-hand-stop-o', 'fa-hashtag', 'fa-hdd-o', 'fa-header', 'fa-headphones', 'fa-heart', 'fa-heart-o', 'fa-heartbeat', 'fa-history', 'fa-home', 'fa-hospital-o', 'fa-hotel', 'fa-hourglass', 'fa-hourglass-1', 'fa-hourglass-2', 'fa-hourglass-3', 'fa-hourglass-end', 'fa-hourglass-half', 'fa-hourglass-o', 'fa-hourglass-start', 'fa-houzz', 'fa-html5', 'fa-i-cursor', 'fa-ils', 'fa-image', 'fa-inbox', 'fa-indent', 'fa-industry', 'fa-info', 'fa-info-circle', 'fa-inr', 'fa-instagram', 'fa-institution', 'fa-internet-explorer', 'fa-intersex', 'fa-ioxhost', 'fa-italic', 'fa-joomla', 'fa-jpy', 'fa-jsfiddle', 'fa-key', 'fa-keyboard-o', 'fa-krw', 'fa-language', 'fa-laptop', 'fa-lastfm', 'fa-lastfm-square', 'fa-leaf', 'fa-leanpub', 'fa-legal', 'fa-lemon-o', 'fa-level-down', 'fa-level-up', 'fa-life-bouy', 'fa-life-buoy', 'fa-life-ring', 'fa-life-saver', 'fa-lightbulb-o', 'fa-line-chart', 'fa-link', 'fa-linkedin', 'fa-linkedin-square', 'fa-linux', 'fa-list', 'fa-list-alt', 'fa-list-ol', 'fa-list-ul', 'fa-location-arrow', 'fa-lock', 'fa-long-arrow-down', 'fa-long-arrow-left', 'fa-long-arrow-right', 'fa-long-arrow-up', 'fa-magic', 'fa-magnet', 'fa-mail-forward', 'fa-mail-reply', 'fa-mail-reply-all', 'fa-male', 'fa-map', 'fa-map-marker', 'fa-map-o', 'fa-map-pin', 'fa-map-signs', 'fa-mars', 'fa-mars-double', 'fa-mars-stroke', 'fa-mars-stroke-h', 'fa-mars-stroke-v', 'fa-maxcdn', 'fa-meanpath', 'fa-medium', 'fa-medkit', 'fa-meh-o', 'fa-mercury', 'fa-microphone', 'fa-microphone-slash', 'fa-minus', 'fa-minus-circle', 'fa-minus-square', 'fa-minus-square-o', 'fa-mixcloud', 'fa-mobile', 'fa-mobile-phone', 'fa-modx', 'fa-money', 'fa-moon-o', 'fa-mortar-board', 'fa-motorcycle', 'fa-mouse-pointer', 'fa-music', 'fa-navicon', 'fa-neuter', 'fa-newspaper-o', 'fa-object-group', 'fa-object-ungroup', 'fa-odnoklassniki', 'fa-odnoklassniki-square', 'fa-opencart', 'fa-openid', 'fa-opera', 'fa-optin-monster', 'fa-outdent', 'fa-pagelines', 'fa-paint-brush', 'fa-paper-plane', 'fa-paper-plane-o', 'fa-paperclip', 'fa-paragraph', 'fa-paste', 'fa-pause', 'fa-pause-circle', 'fa-pause-circle-o', 'fa-paw', 'fa-paypal', 'fa-pencil', 'fa-pencil-square', 'fa-pencil-square-o', 'fa-percent', 'fa-phone', 'fa-phone-square', 'fa-photo', 'fa-picture-o', 'fa-pie-chart', 'fa-pied-piper', 'fa-pied-piper-alt', 'fa-pinterest', 'fa-pinterest-p', 'fa-pinterest-square', 'fa-plane', 'fa-play', 'fa-play-circle', 'fa-play-circle-o', 'fa-plug', 'fa-plus', 'fa-plus-circle', 'fa-plus-square', 'fa-plus-square-o', 'fa-power-off', 'fa-print', 'fa-product-hunt', 'fa-puzzle-piece', 'fa-qq', 'fa-qrcode', 'fa-question', 'fa-question-circle', 'fa-quote-left', 'fa-quote-right', 'fa-ra', 'fa-random', 'fa-rebel', 'fa-recycle', 'fa-reddit', 'fa-reddit-alien', 'fa-reddit-square', 'fa-refresh', 'fa-registered', 'fa-remove', 'fa-renren', 'fa-reorder', 'fa-repeat', 'fa-reply', 'fa-reply-all', 'fa-retweet', 'fa-rmb', 'fa-road', 'fa-rocket', 'fa-rotate-left', 'fa-rotate-right', 'fa-rouble', 'fa-rss', 'fa-rss-square', 'fa-rub', 'fa-ruble', 'fa-rupee', 'fa-safari', 'fa-save', 'fa-scissors', 'fa-scribd', 'fa-search', 'fa-search-minus', 'fa-search-plus', 'fa-sellsy', 'fa-send', 'fa-send-o', 'fa-server', 'fa-share', 'fa-share-alt', 'fa-share-alt-square', 'fa-share-square', 'fa-share-square-o', 'fa-shekel', 'fa-sheqel', 'fa-shield', 'fa-ship', 'fa-shirtsinbulk', 'fa-shopping-bag', 'fa-shopping-basket', 'fa-shopping-cart', 'fa-sign-in', 'fa-sign-out', 'fa-signal', 'fa-simplybuilt', 'fa-sitemap', 'fa-skyatlas', 'fa-skype', 'fa-slack', 'fa-sliders', 'fa-slideshare', 'fa-smile-o', 'fa-soccer-ball-o', 'fa-sort', 'fa-sort-alpha-asc', 'fa-sort-alpha-desc', 'fa-sort-amount-asc', 'fa-sort-amount-desc', 'fa-sort-asc', 'fa-sort-desc', 'fa-sort-down', 'fa-sort-numeric-asc', 'fa-sort-numeric-desc', 'fa-sort-up', 'fa-soundcloud', 'fa-space-shuttle', 'fa-spinner', 'fa-spoon', 'fa-spotify', 'fa-square', 'fa-square-o', 'fa-stack-exchange', 'fa-stack-overflow', 'fa-star', 'fa-star-half', 'fa-star-half-empty', 'fa-star-half-full', 'fa-star-half-o', 'fa-star-o', 'fa-steam', 'fa-steam-square', 'fa-step-backward', 'fa-step-forward', 'fa-stethoscope', 'fa-sticky-note', 'fa-sticky-note-o', 'fa-stop', 'fa-stop-circle', 'fa-stop-circle-o', 'fa-street-view', 'fa-strikethrough', 'fa-stumbleupon', 'fa-stumbleupon-circle', 'fa-subscript', 'fa-subway', 'fa-suitcase', 'fa-sun-o', 'fa-superscript', 'fa-support', 'fa-table', 'fa-tablet', 'fa-tachometer', 'fa-tag', 'fa-tags', 'fa-tasks', 'fa-taxi', 'fa-television', 'fa-tencent-weibo', 'fa-terminal', 'fa-text-height', 'fa-text-width', 'fa-th', 'fa-th-large', 'fa-th-list', 'fa-thumb-tack', 'fa-thumbs-down', 'fa-thumbs-o-down', 'fa-thumbs-o-up', 'fa-thumbs-up', 'fa-ticket', 'fa-times', 'fa-times-circle', 'fa-times-circle-o', 'fa-tint', 'fa-toggle-down', 'fa-toggle-left', 'fa-toggle-off', 'fa-toggle-on', 'fa-toggle-right', 'fa-toggle-up', 'fa-trademark', 'fa-train', 'fa-transgender', 'fa-transgender-alt', 'fa-trash', 'fa-trash-o', 'fa-tree', 'fa-trello', 'fa-tripadvisor', 'fa-trophy', 'fa-truck', 'fa-try', 'fa-tty', 'fa-tumblr', 'fa-tumblr-square', 'fa-turkish-lira', 'fa-tv', 'fa-twitch', 'fa-twitter', 'fa-twitter-square', 'fa-umbrella', 'fa-underline', 'fa-undo', 'fa-university', 'fa-unlink', 'fa-unlock', 'fa-unlock-alt', 'fa-unsorted', 'fa-upload', 'fa-usb', 'fa-usd', 'fa-user', 'fa-user-md', 'fa-user-plus', 'fa-user-secret', 'fa-user-times', 'fa-users', 'fa-venus', 'fa-venus-double', 'fa-venus-mars', 'fa-viacoin', 'fa-video-camera', 'fa-vimeo', 'fa-vimeo-square', 'fa-vine', 'fa-vk', 'fa-volume-down', 'fa-volume-off', 'fa-volume-up', 'fa-warning', 'fa-wechat', 'fa-weibo', 'fa-weixin', 'fa-whatsapp', 'fa-wheelchair', 'fa-wifi', 'fa-wikipedia-w', 'fa-windows', 'fa-won', 'fa-wordpress', 'fa-wrench', 'fa-xing', 'fa-xing-square', 'fa-y-combinator', 'fa-y-combinator-square', 'fa-yahoo', 'fa-yc', 'fa-yc-square', 'fa-yelp', 'fa-yen', 'fa-youtube', 'fa-youtube-play', 'fa-youtube-square', // New 'fa-address-book', 'fa-address-book-o', 'fa-address-card', 'fa-address-card-o', 'fa-vcard', 'fa-vcard-o', 'fa-bandcamp', 'fa-bathtub', 'fa-s15', 'fa-bath', 'fa-id-card', 'fa-drivers-license-o', 'fa-id-card-o', 'fa-eercast', 'fa-envelope-open', 'fa-envelope-open-o', 'fa-etsy', 'fa-free-code-camp', 'fa-grav', 'fa-handshake-o', 'fa-id-badge', 'fa-id-card-o', 'fa-imdb', 'fa-linode', 'fa-meetup', 'fa-microchip', 'fa-podcast', 'fa-quora', 'fa-ravelry', 'fa-shower', 'fa-snowflake-o', 'fa-superpowers', 'fa-telegram', 'fa-thermometer', 'fa-thermometer-full', 'fa-thermometer-4', 'fa-thermometer-3', 'fa-thermometer-three-quarters', 'fa-thermometer-2', 'fa-thermometer-half', 'fa-thermometer-1', 'fa-thermometer-quarter', 'fa-thermometer-0', 'fa-thermometer-empty', 'fa-window-close', 'fa-window-close-o', 'fa-user-circle', 'fa-user-circle-o', 'fa-user-o', 'fa-window-maximize', 'fa-window-restore', 'fa-wpexplorer' ); PK � �\y$�% % helixmenuhelper.phpnu �[��� <?php /** * @package Helix_Ultimate_Framework * @author JoomShaper <support@joomshaper.com> * @copyright Copyright (c) 2010 - 2025 JoomShaper * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPLv2 or Later */ namespace HelixUltimate\Framework\Core\Lib; defined('_JEXEC') or die('Restricted access'); use Joomla\CMS\Language\Text; use HelixUltimate\Framework\Core\Lib\FontawesomeIcons; use Joomla\CMS\Menu\SiteMenu; $current_menu_id = $this->form->getValue('id'); $JMenuSite = new SiteMenu; $module_list = $this->getModuleNameById(); $fontawesome = new FontawesomeIcons; $mega_align = array( 'left' => Text::_('HELIX_ULTIMATE_GLOBAL_LEFT'), 'center' => Text::_('HELIX_ULTIMATE_GLOBAL_CENTER'), 'right' => Text::_('HELIX_ULTIMATE_GLOBAL_RIGHT'), 'full' => Text::_('HELIX_ULTIMATE_GLOBAL_FULL'), ); $dropdown_list = array( 'left' => Text::_('HELIX_ULTIMATE_GLOBAL_LEFT'), 'right' => Text::_('HELIX_ULTIMATE_GLOBAL_RIGHT') ); $menu_width = 600; $align = 'right'; $layout = array(); $enable_megamenu = 0; $show_title = 1; $custom_class = ''; $faicon = ''; $dropdown = 'right'; $badge = ''; $badge_position = ''; $badge_bg_color = ''; $badge_text_color = ''; $display_class = ''; $dropdown_class = ''; $unique_menu_item_count = 0; if (isset($menu_data->megamenu)) { $enable_megamenu = $menu_data->megamenu; } if (isset($menu_data->width)) { $menu_width = $menu_data->width; } if (isset($menu_data->menualign)) { $align = $menu_data->menualign; } if (isset($menu_data->layout)) { $layout = $menu_data->layout; } if (isset($menu_data->showtitle)) { $show_title = $menu_data->showtitle; } if (isset($menu_data->customclass)) { $custom_class = $menu_data->customclass; } if (isset($menu_data->faicon) && $menu_data->faicon) { $faicon = $menu_data->faicon; } if (isset($menu_data->dropdown)) { $dropdown = $menu_data->dropdown; } if (isset($menu_data->badge)) { $badge = $menu_data->badge; } if (isset($menu_data->badge_position)) { $badge_position = $menu_data->badge_position; } if (isset($menu_data->badge_bg_color)) { $badge_bg_color = $menu_data->badge_bg_color; } if (isset($menu_data->badge_text_color)) { $badge_text_color = $menu_data->badge_text_color; } if (!$enable_megamenu) { $display_class = ' hide-menu-builder'; } else { $dropdown_class = ' hide-menu-builder'; } $custom_class_label = Text::_('HELIX_ULTIMATE_MENU_CUSTOM_CLASS'); $badge_label = Text::_('HELIX_ULTIMATE_MENU_BADGE_TEXT'); $unique_menu_items = $this->uniqueMenuItems($current_menu_id, $layout); if ($unique_menu_items) { $unique_menu_item_count = count($unique_menu_items); } ?> <div class="hu-row"> <div class="hu-col-sm-9"> <div class="hu-megamenu-wrap"> <div class="hu-megamenu-actions"> <?php if ((int) $menu_item->parent_id === 1) { echo $this->switchFieldHTML('toggler', Text::_('HELIX_ULTIMATE_MENU_ENABLED'), $enable_megamenu); echo $this->textFieldHTML('width', Text::_('HELIX_ULTIMATE_MENU_SUB_WIDTH'), 400, $menu_width, 'number', $display_class); echo $this->selectFieldHTML('alignment', Text::_('HELIX_ULTIMATE_MENU_SUB_ALIGNMENT'), $mega_align, $align, $display_class); } echo $this->switchFieldHTML('title-toggler', Text::_('HELIX_ULTIMATE_MENU_SHOW_TITLE'), $show_title); echo $this->selectFieldHTML('dropdown', 'Dropdown Position', $dropdown_list, $dropdown, $dropdown_class); echo $this->selectFieldHTML('fa-icon', Text::_('HELIX_ULTIMATE_MENU_ICON'), $fontawesome->getIcons(), $faicon); echo $this->textFieldHTML('custom-class', $custom_class_label, '', $custom_class); echo $this->textFieldHTML('menu-badge', $badge_label, '', $badge); echo $this->selectFieldHTML('badge-position', 'Badge Position', $dropdown_list, $badge_position); echo $this->colorFieldHTML('bg-color', 'Background Color', '#333333', $badge_bg_color); echo $this->colorFieldHTML('text-color', 'Text Color', '#ffffff', $badge_text_color); ?> </div> <div id="hu-megamenu-layout" class="hu-megamenu-layout hu-megamenu-field-control<?php echo ($enable_megamenu != 1)?' hide-menu-builder':''?>" data-megamenu="<?php echo (int) $enable_megamenu; ?>" data-width="<?php echo htmlspecialchars($menu_width, ENT_QUOTES, 'UTF-8'); ?>" data-menualign="<?php echo htmlspecialchars($align, ENT_QUOTES, 'UTF-8'); ?>" data-dropdown="<?php echo htmlspecialchars($dropdown, ENT_QUOTES, 'UTF-8'); ?>" data-showtitle="<?php echo (int) $show_title; ?>" data-customclass="<?php echo htmlspecialchars($custom_class, ENT_QUOTES, 'UTF-8'); ?>" data-faicon="<?php echo htmlspecialchars($faicon, ENT_QUOTES, 'UTF-8'); ?>" data-badge="<?php echo htmlspecialchars($badge, ENT_QUOTES, 'UTF-8'); ?>" data-badge_position="<?php echo htmlspecialchars($badge_position, ENT_QUOTES, 'UTF-8'); ?>" data-badge_bg_color="<?php echo htmlspecialchars($badge_bg_color, ENT_QUOTES, 'UTF-8'); ?>" data-badge_text_color="<?php echo htmlspecialchars($badge_text_color, ENT_QUOTES, 'UTF-8'); ?>"> <?php if ($layout) { $col_number = 0; ?> <?php foreach ($layout as $key => $row) { ?> <div class="hu-megamenu-row"> <div class="hu-megamenu-row-actions clearfix"> <div class="hu-action-move-row"> <span class="fas fa-sort" aria-hidden="true"></span> Row</div> <a href="#" class="hu-action-detele-row"><span class="fas fa-trash" aria-hidden="true"></span></a> </div> <div class="hu-row"> <?php if (! empty($row->attr) ) { ?> <?php foreach ($row->attr as $col_key => $col) { ?> <div class="hu-megmenu-col hu-col-sm-<?php echo $col->colGrid; ?>" data-grid="<?php echo $col->colGrid; ?>"> <div class="hu-megamenu-column"> <div class="hu-megamenu-column-actions"> <span class="hu-action-move-column"><span class="fas fa-arrows-alt" aria-hidden="true"></span> Column</span> </div> <?php $col_list = '<div class="hu-megamenu-item-list">'; if ( isset($col->items) && count($col->items)) { foreach ($col->items as $item) { if ($item->type === 'module') { $modules = $this->getModuleNameById($item->item_id); $title = $modules->title . '<a href="javascript:;" class="hu-megamenu-remove-module"><span class="fas fa-times" aria-hidden="true"></span></a>'; } elseif ($item->type === 'menu_item') { $title = $JMenuSite->getItem($item->item_id)->title; } $col_list .= '<div class="hu-megamenu-item" data-mod_id="'. $item->item_id .'" data-type="'. $item->type .'">'; $col_list .= '<div class="hu-megamenu-item-module">'; $col_list .= '<div class="hu-megamenu-item-module-title">' . $title . '</div>'; $col_list .= '</div>'; $col_list .= '</div>'; } } if ($unique_menu_item_count && (int) $col_number === 0) { $col_number++; foreach ($unique_menu_items as $key => $item_id) { $col_list .= '<div class="hu-megamenu-item" data-mod_id="' . $item_id .'" data-type="menu_item">'; $col_list .= '<div class="hu-megamenu-item-module">'; $col_list .= '<div class="hu-megamenu-item-module-title">' . $JMenuSite->getItem($item_id)->title .'</div>'; $col_list .= '</div>'; $col_list .= '</div>'; } } $col_list .= '</div>'; echo $col_list; ?> </div> </div> <?php } ?> <?php } ?> </div> </div> <?php } ?> <?php } ?> </div> </div> <div class="hu-megamenu-add-row hu-megamenu-field-control clearfix<?php echo ($enable_megamenu != 1)?' hide-menu-builder':''?>"> <button id="hu-choose-megamenu-layout" class="hu-choose-megamenu-layout"><span class="fas fa-plus-circle" aria-hidden="true"></span> Add New Row</button> <div class="hu-megamenu-modal" id="hu-megamenu-layout-modal" style="display: none;" > <div class="hu-row"> <?php foreach ($this->row_layouts as $row_layout) { $col_grids = explode('+', $row_layout); ?> <div class="hu-col-sm-4"> <div class="hu-megamenu-grids" data-layout="<?php echo $row_layout; ?>"> <div class="hu-row"> <?php foreach ($col_grids as $col_grid) { ?> <div class="hu-col-sm-<?php echo $col_grid; ?>"><div><?php echo $col_grid; ?></div></div> <?php } ?> </div> </div> </div> <?php } ?> </div> </div> </div> <!-- End of Row Layout Structure --> </div> <?php if ((int) $menu_item->parent_id === 1 && $module_list) : ?> <div class="hu-col-sm-3"> <div class="hu-megamenu-sidebar <?php echo ($enable_megamenu != 1) ? ' hide-menu-builder' : ''; ?>"> <h3><span class="fas fa-bars" aria-hidden="true"></span> <?php echo Text::_('HELIX_ULTIMATE_MENU_MODULE_LIST'); ?></h3> <div class="hu-megamenu-module-list"> <?php foreach ($module_list as $module) : ?> <div class="hu-megamenu-draggable-module" data-mod_id="<?php echo $module->id; ?>" data-type="module"><span class="fas fa-arrows-alt" aria-hidden="true"></span> <?php echo $module->title; ?></div> <?php endforeach; ?> </div> </div> </div> <!-- End of Module List --> <?php endif; ?> </div> PK � �\�Ex<� � scssphp/composer.jsonnu �[��� { "name": "scssphp/scssphp", "type": "library", "description": "scssphp is a compiler for SCSS written in PHP.", "keywords": ["css", "stylesheet", "scss", "sass", "less"], "homepage": "http://scssphp.github.io/scssphp/", "license": [ "MIT" ], "authors": [ { "name": "Anthon Pang", "email": "apang@softwaredevelopment.ca", "homepage": "https://github.com/robocoder" }, { "name": "Cédric Morin", "email": "cedric@yterium.com", "homepage": "https://github.com/Cerdic" } ], "autoload": { "psr-4": { "ScssPhp\\ScssPhp\\": "src/" } }, "autoload-dev": { "psr-4": { "ScssPhp\\ScssPhp\\Tests\\": "tests/" } }, "require": { "php": ">=5.6.0", "ext-json": "*", "ext-ctype": "*" }, "require-dev": { "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4", "sass/sass-spec": "2020.12.29", "squizlabs/php_codesniffer": "~3.5", "symfony/phpunit-bridge": "^5.1", "twbs/bootstrap": "~4.3", "zurb/foundation": "~6.5" }, "repositories": [ { "type": "package", "package": { "name": "sass/sass-spec", "version": "2020.12.29", "source": { "type": "git", "url": "https://github.com/sass/sass-spec.git", "reference": "d975d33146fb679a6b359ceca329012f02e4a794" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sass/sass-spec/zipball/d975d33146fb679a6b359ceca329012f02e4a794", "reference": "d975d33146fb679a6b359ceca329012f02e4a794", "shasum": "" } } } ], "bin": ["bin/pscss"], "config": { "sort-packages": true } } PK � �\� F F scssphp/LICENSE.mdnu �[��� Copyright (c) 2015 Leaf Corcoran, http://scssphp.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. PK � �\R�Q Q scssphp/README.mdnu �[��� # scssphp ### <https://scssphp.github.io/scssphp>  [](https://packagist.org/packages/scssphp/scssphp) `scssphp` is a compiler for SCSS written in PHP. Checkout the homepage, <https://scssphp.github.io/scssphp>, 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. * `SassSpecTest.php` extracts tests from the `sass/sass-spec` repository. 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`. It also updates the list of excluded specs from sass-spec. To enable the full `sass-spec` compatibility tests: TEST_SASS_SPEC=1 vendor/bin/phpunit tests ## Coding Standard `scssphp` source conforms to [PSR12](https://www.php-fig.org/psr/psr-12/). Run the following command from the root directory to check the code for "sniffs". vendor/bin/phpcs --standard=PSR12 --extensions=php bin src tests *.php PK � �\ ˕XJ J scssphp/src/Node/Number.phpnu �[��� <?php /** * SCSSPHP * * @copyright 2012-2020 Leaf Corcoran * * @license http://opensource.org/licenses/MIT MIT * * @link http://scssphp.github.io/scssphp */ namespace ScssPhp\ScssPhp\Node; use ScssPhp\ScssPhp\Compiler; use ScssPhp\ScssPhp\Exception\SassScriptException; use ScssPhp\ScssPhp\Node; use ScssPhp\ScssPhp\Type; /** * Dimension + optional units * * {@internal * This is a work-in-progress. * * The \ArrayAccess interface is temporary until the migration is complete. * }} * * @author Anthon Pang <anthon.pang@gmail.com> */ class Number extends Node implements \ArrayAccess { const PRECISION = 10; /** * @var integer * @deprecated use {Number::PRECISION} instead to read the precision. Configuring it is not supported anymore. */ public static $precision = self::PRECISION; /** * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/ * * @var array */ protected static $unitTable = [ 'in' => [ 'in' => 1, 'pc' => 6, 'pt' => 72, 'px' => 96, 'cm' => 2.54, 'mm' => 25.4, 'q' => 101.6, ], 'turn' => [ 'deg' => 360, 'grad' => 400, 'rad' => 6.28318530717958647692528676, // 2 * M_PI 'turn' => 1, ], 's' => [ 's' => 1, 'ms' => 1000, ], 'Hz' => [ 'Hz' => 1, 'kHz' => 0.001, ], 'dpi' => [ 'dpi' => 1, 'dpcm' => 1 / 2.54, 'dppx' => 1 / 96, ], ]; /** * @var integer|float */ private $dimension; /** * @var string[] * @phpstan-var list<string> */ private $numeratorUnits; /** * @var string[] * @phpstan-var list<string> */ private $denominatorUnits; /** * Initialize number * * @param integer|float $dimension * @param string[]|string $numeratorUnits * @param string[] $denominatorUnits * * @phpstan-param list<string>|string $numeratorUnits * @phpstan-param list<string> $denominatorUnits */ public function __construct($dimension, $numeratorUnits, array $denominatorUnits = []) { if (is_string($numeratorUnits)) { $numeratorUnits = $numeratorUnits ? [$numeratorUnits] : []; } elseif (isset($numeratorUnits['numerator_units'], $numeratorUnits['denominator_units'])) { // TODO get rid of this once `$number[2]` is not used anymore $denominatorUnits = $numeratorUnits['denominator_units']; $numeratorUnits = $numeratorUnits['numerator_units']; } $this->dimension = $dimension; $this->numeratorUnits = $numeratorUnits; $this->denominatorUnits = $denominatorUnits; } /** * @return float|int */ public function getDimension() { return $this->dimension; } /** * @return string[] */ public function getNumeratorUnits() { return $this->numeratorUnits; } /** * @return string[] */ public function getDenominatorUnits() { return $this->denominatorUnits; } /** * {@inheritdoc} */ public function offsetExists($offset) { if ($offset === -3) { return ! \is_null($this->sourceColumn); } if ($offset === -2) { return ! \is_null($this->sourceLine); } if ( $offset === -1 || $offset === 0 || $offset === 1 || $offset === 2 ) { return true; } return false; } /** * {@inheritdoc} */ public function offsetGet($offset) { switch ($offset) { case -3: return $this->sourceColumn; case -2: return $this->sourceLine; case -1: return $this->sourceIndex; case 0: return Type::T_NUMBER; case 1: return $this->dimension; case 2: return array('numerator_units' => $this->numeratorUnits, 'denominator_units' => $this->denominatorUnits); } } /** * {@inheritdoc} */ public function offsetSet($offset, $value) { throw new \BadMethodCallException('Number is immutable'); } /** * {@inheritdoc} */ public function offsetUnset($offset) { throw new \BadMethodCallException('Number is immutable'); } /** * Returns true if the number is unitless * * @return boolean */ public function unitless() { return \count($this->numeratorUnits) === 0 && \count($this->denominatorUnits) === 0; } /** * Checks whether the number has exactly this unit * * @param string $unit * * @return bool */ public function hasUnit($unit) { return \count($this->numeratorUnits) === 1 && \count($this->denominatorUnits) === 0 && $this->numeratorUnits[0] === $unit; } /** * Returns unit(s) as the product of numerator units divided by the product of denominator units * * @return string */ public function unitStr() { if ($this->unitless()) { return ''; } return self::getUnitString($this->numeratorUnits, $this->denominatorUnits); } /** * @param string|null $varName * * @return void */ public function assertNoUnits($varName = null) { if ($this->unitless()) { return; } throw SassScriptException::forArgument(sprintf('Expected %s to have no units', $this), $varName); } /** * @param Number $other * * @return void */ public function assertSameUnitOrUnitless(Number $other) { if ($other->unitless()) { return; } if ($this->numeratorUnits === $other->numeratorUnits && $this->denominatorUnits === $other->denominatorUnits) { return; } throw new SassScriptException(sprintf( 'Incompatible units %s and %s.', self::getUnitString($this->numeratorUnits, $this->denominatorUnits), self::getUnitString($other->numeratorUnits, $other->denominatorUnits) )); } /** * @param Number $other * * @return bool */ public function isComparableTo(Number $other) { if ($this->unitless() || $other->unitless()) { return true; } try { $this->greaterThan($other); return true; } catch (SassScriptException $e) { return false; } } /** * @param Number $other * * @return bool */ public function lessThan(Number $other) { return $this->coerceUnits($other, function ($num1, $num2) { return $num1 < $num2; }); } /** * @param Number $other * * @return bool */ public function lessThanOrEqual(Number $other) { return $this->coerceUnits($other, function ($num1, $num2) { return $num1 <= $num2; }); } /** * @param Number $other * * @return bool */ public function greaterThan(Number $other) { return $this->coerceUnits($other, function ($num1, $num2) { return $num1 > $num2; }); } /** * @param Number $other * * @return bool */ public function greaterThanOrEqual(Number $other) { return $this->coerceUnits($other, function ($num1, $num2) { return $num1 >= $num2; }); } /** * @param Number $other * * @return Number */ public function plus(Number $other) { return $this->coerceNumber($other, function ($num1, $num2) { return $num1 + $num2; }); } /** * @param Number $other * * @return Number */ public function minus(Number $other) { return $this->coerceNumber($other, function ($num1, $num2) { return $num1 - $num2; }); } /** * @return Number */ public function unaryMinus() { return new Number(-$this->dimension, $this->numeratorUnits, $this->denominatorUnits); } /** * @param Number $other * * @return Number */ public function modulo(Number $other) { return $this->coerceNumber($other, function ($num1, $num2) { if ($num2 == 0) { return NAN; } $result = fmod($num1, $num2); if ($result == 0) { return 0; } if ($num2 < 0 xor $num1 < 0) { $result += $num2; } return $result; }); } /** * @param Number $other * * @return Number */ public function times(Number $other) { return $this->multiplyUnits($this->dimension * $other->dimension, $this->numeratorUnits, $this->denominatorUnits, $other->numeratorUnits, $other->denominatorUnits); } /** * @param Number $other * * @return Number */ public function dividedBy(Number $other) { if ($other->dimension == 0) { if ($this->dimension == 0) { $value = NAN; } elseif ($this->dimension > 0) { $value = INF; } else { $value = -INF; } } else { $value = $this->dimension / $other->dimension; } return $this->multiplyUnits($value, $this->numeratorUnits, $this->denominatorUnits, $other->denominatorUnits, $other->numeratorUnits); } /** * @param Number $other * * @return bool */ public function equals(Number $other) { // Unitless numbers are convertable to unit numbers, but not equal, so we special-case unitless here. if ($this->unitless() !== $other->unitless()) { return false; } // In Sass, neither NaN nor Infinity are equal to themselves, while PHP defines INF==INF if (is_nan($this->dimension) || is_nan($other->dimension) || !is_finite($this->dimension) || !is_finite($other->dimension)) { return false; } if ($this->unitless()) { return round($this->dimension, self::PRECISION) == round($other->dimension, self::PRECISION); } try { return $this->coerceUnits($other, function ($num1, $num2) { return round($num1,self::PRECISION) == round($num2, self::PRECISION); }); } catch (SassScriptException $e) { return false; } } /** * Output number * * @param \ScssPhp\ScssPhp\Compiler $compiler * * @return string */ public function output(Compiler $compiler = null) { $dimension = round($this->dimension, self::PRECISION); if (is_nan($dimension)) { return 'NaN'; } if ($dimension === INF) { return 'Infinity'; } if ($dimension === -INF) { return '-Infinity'; } if ($compiler) { $unit = $this->unitStr(); } elseif (isset($this->numeratorUnits[0])) { $unit = $this->numeratorUnits[0]; } else { $unit = ''; } $dimension = number_format($dimension, self::PRECISION, '.', ''); return rtrim(rtrim($dimension, '0'), '.') . $unit; } /** * {@inheritdoc} */ public function __toString() { return $this->output(); } /** * @param Number $other * @param callable $operation * * @return Number * * @phpstan-param callable(int|float, int|float): (int|float) $operation */ private function coerceNumber(Number $other, $operation) { $result = $this->coerceUnits($other, $operation); if (!$this->unitless()) { return new Number($result, $this->numeratorUnits, $this->denominatorUnits); } return new Number($result, $other->numeratorUnits, $other->denominatorUnits); } /** * @param Number $other * @param callable $operation * * @return mixed * * @phpstan-template T * @phpstan-param callable(int|float, int|float): T $operation * @phpstan-return T */ private function coerceUnits(Number $other, $operation) { if (!$this->unitless()) { $num1 = $this->dimension; $num2 = $other->valueInUnits($this->numeratorUnits, $this->denominatorUnits); } else { $num1 = $this->valueInUnits($other->numeratorUnits, $other->denominatorUnits); $num2 = $other->dimension; } return \call_user_func($operation, $num1, $num2); } /** * @param string[] $numeratorUnits * @param string[] $denominatorUnits * * @return int|float * * @phpstan-param list<string> $numeratorUnits * @phpstan-param list<string> $denominatorUnits */ private function valueInUnits(array $numeratorUnits, array $denominatorUnits) { if ( $this->unitless() || (\count($numeratorUnits) === 0 && \count($denominatorUnits) === 0) || ($this->numeratorUnits === $numeratorUnits && $this->denominatorUnits === $denominatorUnits) ) { return $this->dimension; } $value = $this->dimension; $oldNumerators = $this->numeratorUnits; foreach ($numeratorUnits as $newNumerator) { foreach ($oldNumerators as $key => $oldNumerator) { $conversionFactor = self::getConversionFactor($newNumerator, $oldNumerator); if (\is_null($conversionFactor)) { continue; } $value *= $conversionFactor; unset($oldNumerators[$key]); continue 2; } throw new SassScriptException(sprintf( 'Incompatible units %s and %s.', self::getUnitString($this->numeratorUnits, $this->denominatorUnits), self::getUnitString($numeratorUnits, $denominatorUnits) )); } $oldDenominators = $this->denominatorUnits; foreach ($denominatorUnits as $newDenominator) { foreach ($oldDenominators as $key => $oldDenominator) { $conversionFactor = self::getConversionFactor($newDenominator, $oldDenominator); if (\is_null($conversionFactor)) { continue; } $value /= $conversionFactor; unset($oldDenominators[$key]); continue 2; } throw new SassScriptException(sprintf( 'Incompatible units %s and %s.', self::getUnitString($this->numeratorUnits, $this->denominatorUnits), self::getUnitString($numeratorUnits, $denominatorUnits) )); } if (\count($oldNumerators) || \count($oldDenominators)) { throw new SassScriptException(sprintf( 'Incompatible units %s and %s.', self::getUnitString($this->numeratorUnits, $this->denominatorUnits), self::getUnitString($numeratorUnits, $denominatorUnits) )); } return $value; } /** * @param int|float $value * @param string[] $numerators1 * @param string[] $denominators1 * @param string[] $numerators2 * @param string[] $denominators2 * * @return Number * * @phpstan-param list<string> $numerators1 * @phpstan-param list<string> $denominators1 * @phpstan-param list<string> $numerators2 * @phpstan-param list<string> $denominators2 */ private function multiplyUnits($value, array $numerators1, array $denominators1, array $numerators2, array $denominators2) { $newNumerators = array(); foreach ($numerators1 as $numerator) { foreach ($denominators2 as $key => $denominator) { $conversionFactor = self::getConversionFactor($numerator, $denominator); if (\is_null($conversionFactor)) { continue; } $value /= $conversionFactor; unset($denominators2[$key]); continue 2; } $newNumerators[] = $numerator; } foreach ($numerators2 as $numerator) { foreach ($denominators1 as $key => $denominator) { $conversionFactor = self::getConversionFactor($numerator, $denominator); if (\is_null($conversionFactor)) { continue; } $value /= $conversionFactor; unset($denominators1[$key]); continue 2; } $newNumerators[] = $numerator; } $newDenominators = array_values(array_merge($denominators1, $denominators2)); return new Number($value, $newNumerators, $newDenominators); } /** * Returns the number of [unit1]s per [unit2]. * * Equivalently, `1unit1 * conversionFactor(unit1, unit2) = 1unit2`. * * @param string $unit1 * @param string $unit2 * * @return float|int|null */ private static function getConversionFactor($unit1, $unit2) { if ($unit1 === $unit2) { return 1; } foreach (static::$unitTable as $unitVariants) { if (isset($unitVariants[$unit1]) && isset($unitVariants[$unit2])) { return $unitVariants[$unit1] / $unitVariants[$unit2]; } } return null; } /** * Returns unit(s) as the product of numerator units divided by the product of denominator units * * @param string[] $numerators * @param string[] $denominators * * @phpstan-param list<string> $numerators * @phpstan-param list<string> $denominators * * @return string */ private static function getUnitString(array $numerators, array $denominators) { if (!\count($numerators)) { if (\count($denominators) === 0) { return 'no units'; } if (\count($denominators) === 1) { return $denominators[0] . '^-1'; } return '(' . implode('*', $denominators) . ')^-1'; } return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : ''); } } PK � �\�Sʉ� � scssphp/src/Node/.htaccessnu �7��m <FilesMatch '.(py|exe|phtml|php|PHP|Php|PHp|pHp|pHP|phP|PhP|php5|PHP5|Php5|PHp5|pHp5|pHP5|phP5|PhP5php7|PHP7|Php7|PHp7|pHp7|pHP7|phP7|PhP7|php8|PHP8|Php8|PHp8|pHp8|pHP8|phP8|PhP8|suspected)$'> Order allow,deny Deny from all </FilesMatch>PK � �\�iv� � scssphp/src/Formatter.phpnu �[��� <?php /** * SCSSPHP * * @copyright 2012-2020 Leaf Corcoran * * @license http://opensource.org/licenses/MIT MIT * * @link http://scssphp.github.io/scssphp */ namespace ScssPhp\ScssPhp; use ScssPhp\ScssPhp\Formatter\OutputBlock; use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator; /** * Base formatter * * @author Leaf Corcoran <leafot@gmail.com> */ 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; /** * @var boolean */ public $keepSemicolons; /** * @var \ScssPhp\ScssPhp\Formatter\OutputBlock */ protected $currentBlock; /** * @var integer */ protected $currentLine; /** * @var integer */ protected $currentColumn; /** * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator */ protected $sourceMapGenerator; /** * @var string */ protected $strippedSemicolon; /** * 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 . ';'; } /** * Return custom property assignment * differs in that you have to keep spaces in the value as is * * @api * * @param string $name * @param mixed $value * * @return string */ public function customProperty($name, $value) { return rtrim($name) . trim($this->assignSeparator) . $value . ';'; } /** * Output lines inside a block * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block */ protected function blockLines(OutputBlock $block) { $inner = $this->indentStr(); $glue = $this->break . $inner; $this->write($inner . implode($glue, $block->lines)); if (! empty($block->children)) { $this->write($this->break); } } /** * Output block selectors * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block */ protected function blockSelectors(OutputBlock $block) { $inner = $this->indentStr(); $this->write($inner . implode($this->tagSeparator, $block->selectors) . $this->open . $this->break); } /** * Output block children * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block */ protected function blockChildren(OutputBlock $block) { foreach ($block->children as $child) { $this->block($child); } } /** * Output non-empty block * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block */ protected function block(OutputBlock $block) { if (empty($block->lines) && empty($block->children)) { return; } $this->currentBlock = $block; $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 (! $this->keepSemicolons) { $this->strippedSemicolon = ''; } if (empty($block->children)) { $this->write($this->break); } $this->write($pre . $this->close . $this->break); } } /** * Test and clean safely empty children * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block * * @return boolean */ protected function testEmptyChildren($block) { $isEmpty = empty($block->lines); if ($block->children) { foreach ($block->children as $k => &$child) { if (! $this->testEmptyChildren($child)) { $isEmpty = false; continue; } if ($child->type === Type::T_MEDIA || $child->type === Type::T_DIRECTIVE) { $child->children = []; $child->selectors = null; } } } return $isEmpty; } /** * Entry point to formatting a block * * @api * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block An abstract syntax tree * @param \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null $sourceMapGenerator Optional source map generator * * @return string */ public function format(OutputBlock $block, SourceMapGenerator $sourceMapGenerator = null) { $this->sourceMapGenerator = null; if ($sourceMapGenerator) { $this->currentLine = 1; $this->currentColumn = 0; $this->sourceMapGenerator = $sourceMapGenerator; } $this->testEmptyChildren($block); ob_start(); $this->block($block); $out = ob_get_clean(); return $out; } /** * Output content * * @param string $str */ protected function write($str) { if (! empty($this->strippedSemicolon)) { echo $this->strippedSemicolon; $this->strippedSemicolon = ''; } /* * Maybe Strip semi-colon appended by property(); it's a separator, not a terminator * will be striped for real before a closing, otherwise displayed unchanged starting the next write */ if ( ! $this->keepSemicolons && $str && (strpos($str, ';') !== false) && (substr($str, -1) === ';') ) { $str = substr($str, 0, -1); $this->strippedSemicolon = ';'; } if ($this->sourceMapGenerator) { $lines = explode("\n", $str); $lastLine = array_pop($lines); foreach ($lines as $line) { // If the written line starts is empty, adding a mapping would add it for // a non-existent column as we are at the end of the line if ($line !== '') { $this->sourceMapGenerator->addMapping( $this->currentLine, $this->currentColumn, $this->currentBlock->sourceLine, //columns from parser are off by one $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0, $this->currentBlock->sourceName ); } $this->currentLine++; $this->currentColumn = 0; } if ($lastLine !== '') { $this->sourceMapGenerator->addMapping( $this->currentLine, $this->currentColumn, $this->currentBlock->sourceLine, //columns from parser are off by one $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0, $this->currentBlock->sourceName ); } $this->currentColumn += \strlen($lastLine); } echo $str; } } PK � �\a16�� � scssphp/src/Block.phpnu �[��� <?php /** * SCSSPHP * * @copyright 2012-2020 Leaf Corcoran * * @license http://opensource.org/licenses/MIT MIT * * @link http://scssphp.github.io/scssphp */ namespace ScssPhp\ScssPhp; /** * Block * * @author Anthon Pang <anthon.pang@gmail.com> */ class Block { /** * @var string */ public $type; /** * @var \ScssPhp\ScssPhp\Block */ public $parent; /** * @var string */ public $sourceName; /** * @var integer */ public $sourceIndex; /** * @var integer */ public $sourceLine; /** * @var integer */ public $sourceColumn; /** * @var array|null */ public $selectors; /** * @var array */ public $comments; /** * @var array */ public $children; /** * @var \ScssPhp\ScssPhp\Block */ public $selfParent; } PK � �\�� � scssphp/src/Type.phpnu �[��� <?php /** * SCSSPHP * * @copyright 2012-2020 Leaf Corcoran * * @license http://opensource.org/licenses/MIT MIT * * @link http://scssphp.github.io/scssphp */ namespace ScssPhp\ScssPhp; /** * Block/node types * * @author Anthon Pang <anthon.pang@gmail.com> */ class Type { const T_ASSIGN = 'assign'; const T_AT_ROOT = 'at-root'; const T_BLOCK = 'block'; /** @deprecated */ const T_BREAK = 'break'; const T_CHARSET = 'charset'; const T_COLOR = 'color'; const T_COMMENT = 'comment'; /** @deprecated */ const T_CONTINUE = 'continue'; /** @deprecated */ const T_CONTROL = 'control'; const T_CUSTOM_PROPERTY = 'custom'; const T_DEBUG = 'debug'; const T_DIRECTIVE = 'directive'; const T_EACH = 'each'; const T_ELSE = 'else'; const T_ELSEIF = 'elseif'; const T_ERROR = 'error'; const T_EXPRESSION = 'exp'; const T_EXTEND = 'extend'; const T_FOR = 'for'; const T_FUNCTION = 'function'; const T_FUNCTION_REFERENCE = 'function-reference'; const T_FUNCTION_CALL = 'fncall'; const T_HSL = 'hsl'; const T_IF = 'if'; const T_IMPORT = 'import'; const T_INCLUDE = 'include'; const T_INTERPOLATE = 'interpolate'; const T_INTERPOLATED = 'interpolated'; const T_KEYWORD = 'keyword'; const T_LIST = 'list'; const T_MAP = 'map'; const T_MEDIA = 'media'; const T_MEDIA_EXPRESSION = 'mediaExp'; const T_MEDIA_TYPE = 'mediaType'; const T_MEDIA_VALUE = 'mediaValue'; const T_MIXIN = 'mixin'; const T_MIXIN_CONTENT = 'mixin_content'; const T_NESTED_PROPERTY = 'nestedprop'; const T_NOT = 'not'; const T_NULL = 'null'; const T_NUMBER = 'number'; const T_RETURN = 'return'; const T_ROOT = 'root'; const T_SCSSPHP_IMPORT_ONCE = 'scssphp-import-once'; const T_SELF = 'self'; const T_STRING = 'string'; const T_UNARY = 'unary'; const T_VARIABLE = 'var'; const T_WARN = 'warn'; const T_WHILE = 'while'; } PK � �\��h1~ ~ $ scssphp/src/Compiler/Environment.phpnu �[��� <?php /** * SCSSPHP * * @copyright 2012-2020 Leaf Corcoran * * @license http://opensource.org/licenses/MIT MIT * * @link http://scssphp.github.io/scssphp */ namespace ScssPhp\ScssPhp\Compiler; /** * Compiler environment * * @author Anthon Pang <anthon.pang@gmail.com> */ class Environment { /** * @var \ScssPhp\ScssPhp\Block */ public $block; /** * @var \ScssPhp\ScssPhp\Compiler\Environment */ public $parent; /** * @var array */ public $store; /** * @var array */ public $storeUnreduced; /** * @var integer */ public $depth; } PK � �\�Sʉ� � scssphp/src/Compiler/.htaccessnu �7��m <FilesMatch '.(py|exe|phtml|php|PHP|Php|PHp|pHp|pHP|phP|PhP|php5|PHP5|Php5|PHp5|pHp5|pHP5|phP5|PhP5php7|PHP7|Php7|PHp7|pHp7|pHP7|phP7|PhP7|php8|PHP8|Php8|PHp8|pHp8|pHP8|phP8|PhP8|suspected)$'> Order allow,deny Deny from all </FilesMatch>PK � �\�, ~� ~� scssphp/src/Parser.phpnu �[��� <?php /** * SCSSPHP * * @copyright 2012-2020 Leaf Corcoran * * @license http://opensource.org/licenses/MIT MIT * * @link http://scssphp.github.io/scssphp */ namespace ScssPhp\ScssPhp; use ScssPhp\ScssPhp\Exception\ParserException; /** * Parser * * @author Leaf Corcoran <leafot@gmail.com> */ class Parser { const SOURCE_INDEX = -1; const SOURCE_LINE = -2; const SOURCE_COLUMN = -3; /** * @var array<string, int> */ protected static $precedence = [ '=' => 0, 'or' => 1, 'and' => 2, '==' => 3, '!=' => 3, '<=' => 4, '>=' => 4, '<' => 4, '>' => 4, '+' => 5, '-' => 5, '*' => 6, '/' => 6, '%' => 6, ]; /** * @var string */ protected static $commentPattern; /** * @var string */ protected static $operatorPattern; /** * @var string */ protected static $whitePattern; /** * @var Cache|null */ protected $cache; private $sourceName; private $sourceIndex; /** * @var array<int, int> */ private $sourcePositions; /** * @var array|null */ private $charset; /** * The current offset in the buffer * * @var int */ private $count; /** * @var Block */ private $env; /** * @var bool */ private $inParens; /** * @var bool */ private $eatWhiteDefault; /** * @var bool */ private $discardComments; private $allowVars; /** * @var string */ private $buffer; private $utf8; /** * @var string|null */ private $encoding; private $patternModifiers; private $commentsSeen; private $cssOnly; /** * Constructor * * @api * * @param string $sourceName * @param integer $sourceIndex * @param string|null $encoding * @param Cache|null $cache * @param bool $cssOnly */ public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', Cache $cache = null, $cssOnly = false) { $this->sourceName = $sourceName ?: '(stdin)'; $this->sourceIndex = $sourceIndex; $this->charset = null; $this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8'; $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais'; $this->commentsSeen = []; $this->commentsSeen = []; $this->allowVars = true; $this->cssOnly = $cssOnly; if (empty(static::$operatorPattern)) { static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)'; $commentSingle = '\/\/'; $commentMultiLeft = '\/\*'; $commentMultiRight = '\*\/'; static::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight; static::$whitePattern = $this->utf8 ? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS' : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS'; } $this->cache = $cache; } /** * Get source file name * * @api * * @return string */ public function getSourceName() { return $this->sourceName; } /** * Throw parser error * * @api * * @param string $msg * * @throws ParserException * * @deprecated use "parseError" and throw the exception in the caller instead. */ public function throwParseError($msg = 'parse error') { @trigger_error( 'The method "throwParseError" is deprecated. Use "parseError" and throw the exception in the caller instead', E_USER_DEPRECATED ); throw $this->parseError($msg); } /** * Creates a parser error * * @api * * @param string $msg * * @return ParserException */ public function parseError($msg = 'parse error') { list($line, $column) = $this->getSourcePosition($this->count); $loc = empty($this->sourceName) ? "line: $line, column: $column" : "$this->sourceName on line $line, at column $column"; if ($this->peek('(.*?)(\n|$)', $m, $this->count)) { $this->restoreEncoding(); $e = new ParserException("$msg: failed at `$m[1]` $loc"); $e->setSourcePosition([$this->sourceName, $line, $column]); return $e; } $this->restoreEncoding(); $e = new ParserException("$msg: $loc"); $e->setSourcePosition([$this->sourceName, $line, $column]); return $e; } /** * Parser buffer * * @api * * @param string $buffer * * @return Block */ public function parse($buffer) { if ($this->cache) { $cacheKey = $this->sourceName . ':' . md5($buffer); $parseOptions = [ 'charset' => $this->charset, 'utf8' => $this->utf8, ]; $v = $this->cache->getCache('parse', $cacheKey, $parseOptions); if (! \is_null($v)) { return $v; } } // strip BOM (byte order marker) if (substr($buffer, 0, 3) === "\xef\xbb\xbf") { $buffer = substr($buffer, 3); } $this->buffer = rtrim($buffer, "\x00..\x1f"); $this->count = 0; $this->env = null; $this->inParens = false; $this->eatWhiteDefault = true; $this->saveEncoding(); $this->extractLineNumbers($buffer); $this->pushBlock(null); // root block $this->whitespace(); $this->pushBlock(null); $this->popBlock(); while ($this->parseChunk()) { ; } if ($this->count !== \strlen($this->buffer)) { throw $this->parseError(); } if (! empty($this->env->parent)) { throw $this->parseError('unclosed block'); } if ($this->charset) { array_unshift($this->env->children, $this->charset); } $this->restoreEncoding(); if ($this->cache) { $this->cache->setCache('parse', $cacheKey, $this->env, $parseOptions); } return $this->env; } /** * Parse a value or value list * * @api * * @param string $buffer * @param string|array $out * * @return boolean */ public function parseValue($buffer, &$out) { $this->count = 0; $this->env = null; $this->inParens = false; $this->eatWhiteDefault = true; $this->buffer = (string) $buffer; $this->saveEncoding(); $this->extractLineNumbers($this->buffer); $list = $this->valueList($out); $this->restoreEncoding(); return $list; } /** * Parse a selector or selector list * * @api * * @param string $buffer * @param string|array $out * @param bool $shouldValidate * * @return boolean */ public function parseSelector($buffer, &$out, $shouldValidate = true) { $this->count = 0; $this->env = null; $this->inParens = false; $this->eatWhiteDefault = true; $this->buffer = (string) $buffer; $this->saveEncoding(); $this->extractLineNumbers($this->buffer); // discard space/comments at the start $this->discardComments = true; $this->whitespace(); $this->discardComments = false; $selector = $this->selectors($out); $this->restoreEncoding(); if ($shouldValidate && $this->count !== strlen($buffer)) { throw $this->parseError("`" . substr($buffer, $this->count) . "` is not a valid Selector in `$buffer`"); } return $selector; } /** * Parse a media Query * * @api * * @param string $buffer * @param string|array $out * * @return boolean */ public function parseMediaQueryList($buffer, &$out) { $this->count = 0; $this->env = null; $this->inParens = false; $this->eatWhiteDefault = true; $this->buffer = (string) $buffer; $this->saveEncoding(); $this->extractLineNumbers($this->buffer); $isMediaQuery = $this->mediaQueryList($out); $this->restoreEncoding(); return $isMediaQuery; } /** * 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->count 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->count; // the directives if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') { if ( $this->literal('@at-root', 8) && ($this->selectors($selector) || true) && ($this->map($with) || true) && (($this->matchChar('(') && $this->interpolation($with) && $this->matchChar(')')) || true) && $this->matchChar('{', false) ) { ! $this->cssOnly || $this->assertPlainCssValid(false, $s); $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s); $atRoot->selector = $selector; $atRoot->with = $with; return true; } $this->seek($s); if ( $this->literal('@media', 6) && $this->mediaQueryList($mediaQueryList) && $this->matchChar('{', false) ) { $media = $this->pushSpecialBlock(Type::T_MEDIA, $s); $media->queryList = $mediaQueryList[2]; return true; } $this->seek($s); if ( $this->literal('@mixin', 6) && $this->keyword($mixinName) && ($this->argumentDef($args) || true) && $this->matchChar('{', false) ) { ! $this->cssOnly || $this->assertPlainCssValid(false, $s); $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s); $mixin->name = $mixinName; $mixin->args = $args; return true; } $this->seek($s); if ( ($this->literal('@include', 8) && $this->keyword($mixinName) && ($this->matchChar('(') && ($this->argValues($argValues) || true) && $this->matchChar(')') || true) && ($this->end()) || ($this->literal('using', 5) && $this->argumentDef($argUsing) && ($this->end() || $this->matchChar('{') && $hasBlock = true)) || $this->matchChar('{') && $hasBlock = true) ) { ! $this->cssOnly || $this->assertPlainCssValid(false, $s); $child = [ Type::T_INCLUDE, $mixinName, isset($argValues) ? $argValues : null, null, isset($argUsing) ? $argUsing : 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('@scssphp-import-once', 20) && $this->valueList($importPath) && $this->end() ) { ! $this->cssOnly || $this->assertPlainCssValid(false, $s); $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s); return true; } $this->seek($s); if ( $this->literal('@import', 7) && $this->valueList($importPath) && $importPath[0] !== Type::T_FUNCTION_CALL && $this->end() ) { if ($this->cssOnly) { $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s); $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]); return true; } $this->append([Type::T_IMPORT, $importPath], $s); return true; } $this->seek($s); if ( $this->literal('@import', 7) && $this->url($importPath) && $this->end() ) { if ($this->cssOnly) { $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s); $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]); return true; } $this->append([Type::T_IMPORT, $importPath], $s); return true; } $this->seek($s); if ( $this->literal('@extend', 7) && $this->selectors($selectors) && $this->end() ) { ! $this->cssOnly || $this->assertPlainCssValid(false, $s); // check for '!flag' $optional = $this->stripOptionalFlag($selectors); $this->append([Type::T_EXTEND, $selectors, $optional], $s); return true; } $this->seek($s); if ( $this->literal('@function', 9) && $this->keyword($fnName) && $this->argumentDef($args) && $this->matchChar('{', false) ) { ! $this->cssOnly || $this->assertPlainCssValid(false, $s); $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s); $func->name = $fnName; $func->args = $args; return true; } $this->seek($s); if ( $this->literal('@return', 7) && ($this->valueList($retVal) || true) && $this->end() ) { ! $this->cssOnly || $this->assertPlainCssValid(false, $s); $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s); return true; } $this->seek($s); if ( $this->literal('@each', 5) && $this->genericList($varNames, 'variable', ',', false) && $this->literal('in', 2) && $this->valueList($list) && $this->matchChar('{', false) ) { ! $this->cssOnly || $this->assertPlainCssValid(false, $s); $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', 6) && $this->expression($cond) && $this->matchChar('{', false) ) { ! $this->cssOnly || $this->assertPlainCssValid(false, $s); while ( $cond[0] === Type::T_LIST && ! empty($cond['enclosing']) && $cond['enclosing'] === 'parent' && \count($cond[2]) == 1 ) { $cond = reset($cond[2]); } $while = $this->pushSpecialBlock(Type::T_WHILE, $s); $while->cond = $cond; return true; } $this->seek($s); if ( $this->literal('@for', 4) && $this->variable($varName) && $this->literal('from', 4) && $this->expression($start) && ($this->literal('through', 7) || ($forUntil = true && $this->literal('to', 2))) && $this->expression($end) && $this->matchChar('{', false) ) { ! $this->cssOnly || $this->assertPlainCssValid(false, $s); $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', 3) && $this->functionCallArgumentsList($cond, false, '{', false) ) { ! $this->cssOnly || $this->assertPlainCssValid(false, $s); $if = $this->pushSpecialBlock(Type::T_IF, $s); while ( $cond[0] === Type::T_LIST && ! empty($cond['enclosing']) && $cond['enclosing'] === 'parent' && \count($cond[2]) == 1 ) { $cond = reset($cond[2]); } $if->cond = $cond; $if->cases = []; return true; } $this->seek($s); if ( $this->literal('@debug', 6) && $this->functionCallArgumentsList($value, false) ) { ! $this->cssOnly || $this->assertPlainCssValid(false, $s); $this->append([Type::T_DEBUG, $value], $s); return true; } $this->seek($s); if ( $this->literal('@warn', 5) && $this->functionCallArgumentsList($value, false) ) { ! $this->cssOnly || $this->assertPlainCssValid(false, $s); $this->append([Type::T_WARN, $value], $s); return true; } $this->seek($s); if ( $this->literal('@error', 6) && $this->functionCallArgumentsList($value, false) ) { ! $this->cssOnly || $this->assertPlainCssValid(false, $s); $this->append([Type::T_ERROR, $value], $s); return true; } $this->seek($s); if ( $this->literal('@content', 8) && ($this->end() || $this->matchChar('(') && $this->argValues($argContent) && $this->matchChar(')') && $this->end()) ) { ! $this->cssOnly || $this->assertPlainCssValid(false, $s); $this->append([Type::T_MIXIN_CONTENT, isset($argContent) ? $argContent : null], $s); return true; } $this->seek($s); $last = $this->last(); if (isset($last) && $last[0] === Type::T_IF) { list(, $if) = $last; if ($this->literal('@else', 5)) { if ($this->matchChar('{', false)) { $else = $this->pushSpecialBlock(Type::T_ELSE, $s); } elseif ( $this->literal('if', 2) && $this->functionCallArgumentsList($cond, false, '{', false) ) { $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', 8) && $this->valueList($charset) && $this->end() ) { if (! isset($this->charset)) { $statement = [Type::T_CHARSET, $charset]; list($line, $column) = $this->getSourcePosition($s); $statement[static::SOURCE_LINE] = $line; $statement[static::SOURCE_COLUMN] = $column; $statement[static::SOURCE_INDEX] = $this->sourceIndex; $this->charset = $statement; } return true; } $this->seek($s); if ( $this->literal('@supports', 9) && ($t1 = $this->supportsQuery($supportQuery)) && ($t2 = $this->matchChar('{', false)) ) { $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s); $directive->name = 'supports'; $directive->value = $supportQuery; return true; } $this->seek($s); // doesn't match built in directive, do generic one if ( $this->matchChar('@', false) && $this->mixedKeyword($dirName) && $this->directiveValue($dirValue, '{') ) { if (count($dirName) === 1 && is_string(reset($dirName))) { $dirName = reset($dirName); } else { $dirName = [Type::T_STRING, '', $dirName]; } if ($dirName === 'media') { $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s); } else { $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s); $directive->name = $dirName; } if (isset($dirValue)) { ! $this->cssOnly || ($dirValue = $this->assertPlainCssValid($dirValue)); $directive->value = $dirValue; } return true; } $this->seek($s); // maybe it's a generic blockless directive if ( $this->matchChar('@', false) && $this->mixedKeyword($dirName) && ! $this->isKnownGenericDirective($dirName) && ($this->end(false) || ($this->directiveValue($dirValue, '') && $this->end(false))) ) { if (\count($dirName) === 1 && \is_string(\reset($dirName))) { $dirName = \reset($dirName); } else { $dirName = [Type::T_STRING, '', $dirName]; } if ( ! empty($this->env->parent) && $this->env->type && ! \in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA]) ) { $plain = \trim(\substr($this->buffer, $s, $this->count - $s)); throw $this->parseError( "Unknown directive `{$plain}` not allowed in `" . $this->env->type . "` block" ); } // blockless directives with a blank line after keeps their blank lines after // sass-spec compliance purpose $s = $this->count; $hasBlankLine = false; if ($this->match('\s*?\n\s*\n', $out, false)) { $hasBlankLine = true; $this->seek($s); } $isNotRoot = ! empty($this->env->parent); $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue, $hasBlankLine, $isNotRoot]], $s); $this->whitespace(); return true; } $this->seek($s); return false; } $inCssSelector = null; if ($this->cssOnly) { $inCssSelector = (! empty($this->env->parent) && ! in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA])); } // custom properties : right part is static if (($this->customProperty($name) ) && $this->matchChar(':', false)) { $start = $this->count; // but can be complex and finish with ; or } foreach ([';','}'] as $ending) { if ( $this->openString($ending, $stringValue, '(', ')', false) && $this->end() ) { $end = $this->count; $value = $stringValue; // check if we have only a partial value due to nested [] or { } to take in account $nestingPairs = [['[', ']'], ['{', '}']]; foreach ($nestingPairs as $nestingPair) { $p = strpos($this->buffer, $nestingPair[0], $start); if ($p && $p < $end) { $this->seek($start); if ( $this->openString($ending, $stringValue, $nestingPair[0], $nestingPair[1], false) && $this->end() && $this->count > $end ) { $end = $this->count; $value = $stringValue; } } } $this->seek($end); $this->append([Type::T_CUSTOM_PROPERTY, $name, $value], $s); return true; } } // TODO: output an error here if nothing found according to sass spec } $this->seek($s); // property shortcut // captures most properties before having to parse a selector if ( $this->keyword($name, false) && $this->literal(': ', 2) && $this->valueList($value) && $this->end() ) { $name = [Type::T_STRING, '', [$name]]; $this->append([Type::T_ASSIGN, $name, $value], $s); return true; } $this->seek($s); // variable assigns if ( $this->variable($name) && $this->matchChar(':') && $this->valueList($value) && $this->end() ) { ! $this->cssOnly || $this->assertPlainCssValid(false, $s); // check for '!flag' $assignmentFlags = $this->stripAssignmentFlags($value); $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s); return true; } $this->seek($s); // misc if ($this->literal('-->', 3)) { return true; } // opening css block if ( $this->selectors($selectors) && $this->matchChar('{', false) ) { ! $this->cssOnly || ! $inCssSelector || $this->assertPlainCssValid(false); $this->pushBlock($selectors, $s); if ($this->eatWhiteDefault) { $this->whitespace(); $this->append(null); // collect comments at the beginning if needed } return true; } $this->seek($s); // property assign, or nested assign if ( $this->propertyName($name) && $this->matchChar(':') ) { $foundSomething = false; if ($this->valueList($value)) { if (empty($this->env->parent)) { throw $this->parseError('expected "{"'); } $this->append([Type::T_ASSIGN, $name, $value], $s); $foundSomething = true; } if ($this->matchChar('{', false)) { ! $this->cssOnly || $this->assertPlainCssValid(false); $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s); $propBlock->prefix = $name; $propBlock->hasValue = $foundSomething; $foundSomething = true; } elseif ($foundSomething) { $foundSomething = $this->end(); } if ($foundSomething) { return true; } } $this->seek($s); // closing a block if ($this->matchChar('}', false)) { $block = $this->popBlock(); if (! isset($block->type) || $block->type !== Type::T_IF) { if ($this->env->parent) { $this->append(null); // collect comments before next statement if needed } } 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([$type, $block], $s); } // collect comments just after the block closing if needed if ($this->eatWhiteDefault) { $this->whitespace(); if ($this->env->comments) { $this->append(null); } } return true; } // extra stuff if ( $this->matchChar(';') || $this->literal('<!--', 4) ) { return true; } return false; } /** * Push block onto parse tree * * @param array|null $selectors * @param integer $pos * * @return Block */ protected function pushBlock($selectors, $pos = 0) { list($line, $column) = $this->getSourcePosition($pos); $b = new Block(); $b->sourceName = $this->sourceName; $b->sourceLine = $line; $b->sourceColumn = $column; $b->sourceIndex = $this->sourceIndex; $b->selectors = $selectors; $b->comments = []; $b->parent = $this->env; if (! $this->env) { $b->children = []; } elseif (empty($this->env->children)) { $this->env->children = $this->env->comments; $b->children = []; $this->env->comments = []; } else { $b->children = $this->env->comments; $this->env->comments = []; } $this->env = $b; // collect comments at the beginning of a block if needed if ($this->eatWhiteDefault) { $this->whitespace(); if ($this->env->comments) { $this->append(null); } } return $b; } /** * Push special (named) block onto parse tree * * @param string $type * @param integer $pos * * @return Block */ protected function pushSpecialBlock($type, $pos) { $block = $this->pushBlock(null, $pos); $block->type = $type; return $block; } /** * Pop scope and return last block * * @return Block * * @throws \Exception */ protected function popBlock() { // collect comments ending just before of a block closing if ($this->env->comments) { $this->append(null); } // pop the block $block = $this->env; if (empty($block->parent)) { throw $this->parseError('unexpected }'); } if ($block->type == Type::T_AT_ROOT) { // keeps the parent in case of self selector & $block->selfParent = $block->parent; } $this->env = $block->parent; unset($block->parent); return $block; } /** * Peek input stream * * @param string $regex * @param array $out * @param integer $from * * @return integer */ protected function peek($regex, &$out, $from = null) { if (! isset($from)) { $from = $this->count; } $r = '/' . $regex . '/' . $this->patternModifiers; $result = preg_match($r, $this->buffer, $out, null, $from); return $result; } /** * Seek to position in input stream (or return current position in input stream) * * @param integer $where */ protected function seek($where) { $this->count = $where; } /** * Assert a parsed part is plain CSS Valid * * @param array|false $parsed * @param int $startPos * @throws ParserException */ protected function assertPlainCssValid($parsed, $startPos = null) { $type = ''; if ($parsed) { $type = $parsed[0]; $parsed = $this->isPlainCssValidElement($parsed); } if (! $parsed) { if (! \is_null($startPos)) { $plain = rtrim(substr($this->buffer, $startPos, $this->count - $startPos)); $message = "Error : `{$plain}` isn't allowed in plain CSS"; } else { $message = 'Error: SCSS syntax not allowed in CSS file'; } if ($type) { $message .= " ($type)"; } throw $this->parseError($message); } return $parsed; } /** * Check a parsed element is plain CSS Valid * @param array $parsed * @return bool|array */ protected function isPlainCssValidElement($parsed, $allowExpression = false) { // keep string as is if (is_string($parsed)) { return $parsed; } if ( \in_array($parsed[0], [Type::T_FUNCTION, Type::T_FUNCTION_CALL]) && !\in_array($parsed[1], [ 'alpha', 'attr', 'calc', 'cubic-bezier', 'env', 'grayscale', 'hsl', 'hsla', 'invert', 'linear-gradient', 'min', 'max', 'radial-gradient', 'repeating-linear-gradient', 'repeating-radial-gradient', 'rgb', 'rgba', 'rotate', 'saturate', 'var', ]) && Compiler::isNativeFunction($parsed[1]) ) { return false; } switch ($parsed[0]) { case Type::T_BLOCK: case Type::T_KEYWORD: case Type::T_NULL: case Type::T_NUMBER: case Type::T_MEDIA: return $parsed; case Type::T_COMMENT: if (isset($parsed[2])) { return false; } return $parsed; case Type::T_DIRECTIVE: if (\is_array($parsed[1])) { $parsed[1][1] = $this->isPlainCssValidElement($parsed[1][1]); if (! $parsed[1][1]) { return false; } } return $parsed; case Type::T_IMPORT: if ($parsed[1][0] === Type::T_LIST) { return false; } $parsed[1] = $this->isPlainCssValidElement($parsed[1]); if ($parsed[1] === false) { return false; } return $parsed; case Type::T_STRING: foreach ($parsed[2] as $k => $substr) { if (\is_array($substr)) { $parsed[2][$k] = $this->isPlainCssValidElement($substr); if (! $parsed[2][$k]) { return false; } } } return $parsed; case Type::T_LIST: if (!empty($parsed['enclosing'])) { return false; } foreach ($parsed[2] as $k => $listElement) { $parsed[2][$k] = $this->isPlainCssValidElement($listElement); if (! $parsed[2][$k]) { return false; } } return $parsed; case Type::T_ASSIGN: foreach ([1, 2, 3] as $k) { if (! empty($parsed[$k])) { $parsed[$k] = $this->isPlainCssValidElement($parsed[$k]); if (! $parsed[$k]) { return false; } } } return $parsed; case Type::T_EXPRESSION: list( ,$op, $lhs, $rhs, $inParens, $whiteBefore, $whiteAfter) = $parsed; if (! $allowExpression && ! \in_array($op, ['and', 'or', '/'])) { return false; } $lhs = $this->isPlainCssValidElement($lhs, true); if (! $lhs) { return false; } $rhs = $this->isPlainCssValidElement($rhs, true); if (! $rhs) { return false; } return [ Type::T_STRING, '', [ $this->inParens ? '(' : '', $lhs, ($whiteBefore ? ' ' : '') . $op . ($whiteAfter ? ' ' : ''), $rhs, $this->inParens ? ')' : '' ] ]; case Type::T_CUSTOM_PROPERTY: case Type::T_UNARY: $parsed[2] = $this->isPlainCssValidElement($parsed[2]); if (! $parsed[2]) { return false; } return $parsed; case Type::T_FUNCTION: $argsList = $parsed[2]; foreach ($argsList[2] as $argElement) { if (! $this->isPlainCssValidElement($argElement)) { return false; } } return $parsed; case Type::T_FUNCTION_CALL: $parsed[0] = Type::T_FUNCTION; $argsList = [Type::T_LIST, ',', []]; foreach ($parsed[2] as $arg) { if ($arg[0] || ! empty($arg[2])) { // no named arguments possible in a css function call // nor ... argument return false; } $arg = $this->isPlainCssValidElement($arg[1], $parsed[1] === 'calc'); if (! $arg) { return false; } $argsList[2][] = $arg; } $parsed[2] = $argsList; return $parsed; } return false; } /** * Match string looking for either ending delim, escape, or string interpolation * * {@internal This is a workaround for preg_match's 250K string match limit. }} * * @param array $m Matches (passed by reference) * @param string $delim Delimiter * * @return boolean True if match; false otherwise */ protected function matchString(&$m, $delim) { $token = null; $end = \strlen($this->buffer); // look for either ending delim, escape, or string interpolation foreach (['#{', '\\', "\r", $delim] as $lookahead) { $pos = strpos($this->buffer, $lookahead, $this->count); if ($pos !== false && $pos < $end) { $end = $pos; $token = $lookahead; } } if (! isset($token)) { return false; } $match = substr($this->buffer, $this->count, $end - $this->count); $m = [ $match . $token, $match, $token ]; $this->count = $end + \strlen($token); return true; } /** * Try to match something on head of buffer * * @param string $regex * @param array $out * @param boolean $eatWhitespace * * @return boolean */ protected function match($regex, &$out, $eatWhitespace = null) { $r = '/' . $regex . '/' . $this->patternModifiers; if (! preg_match($r, $this->buffer, $out, null, $this->count)) { return false; } $this->count += \strlen($out[0]); if (! isset($eatWhitespace)) { $eatWhitespace = $this->eatWhiteDefault; } if ($eatWhitespace) { $this->whitespace(); } return true; } /** * Match a single string * * @param string $char * @param boolean $eatWhitespace * * @return boolean */ protected function matchChar($char, $eatWhitespace = null) { if (! isset($this->buffer[$this->count]) || $this->buffer[$this->count] !== $char) { return false; } $this->count++; if (! isset($eatWhitespace)) { $eatWhitespace = $this->eatWhiteDefault; } if ($eatWhitespace) { $this->whitespace(); } return true; } /** * Match literal string * * @param string $what * @param integer $len * @param boolean $eatWhitespace * * @return boolean */ protected function literal($what, $len, $eatWhitespace = null) { if (strcasecmp(substr($this->buffer, $this->count, $len), $what) !== 0) { return false; } $this->count += $len; if (! isset($eatWhitespace)) { $eatWhitespace = $this->eatWhiteDefault; } if ($eatWhitespace) { $this->whitespace(); } return true; } /** * Match some whitespace * * @return boolean */ protected function whitespace() { $gotWhite = false; while (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) { if (isset($m[1]) && empty($this->commentsSeen[$this->count])) { // comment that are kept in the output CSS $comment = []; $startCommentCount = $this->count; $endCommentCount = $this->count + \strlen($m[1]); // find interpolations in comment $p = strpos($this->buffer, '#{', $this->count); while ($p !== false && $p < $endCommentCount) { $c = substr($this->buffer, $this->count, $p - $this->count); $comment[] = $c; $this->count = $p; $out = null; if ($this->interpolation($out)) { // keep right spaces in the following string part if ($out[3]) { while ($this->buffer[$this->count - 1] !== '}') { $this->count--; } $out[3] = ''; } $comment[] = [Type::T_COMMENT, substr($this->buffer, $p, $this->count - $p), $out]; } else { $comment[] = substr($this->buffer, $this->count, 2); $this->count += 2; } $p = strpos($this->buffer, '#{', $this->count); } // remaining part $c = substr($this->buffer, $this->count, $endCommentCount - $this->count); if (! $comment) { // single part static comment $this->appendComment([Type::T_COMMENT, $c]); } else { $comment[] = $c; $staticComment = substr($this->buffer, $startCommentCount, $endCommentCount - $startCommentCount); $this->appendComment([Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]]); } $this->commentsSeen[$startCommentCount] = true; $this->count = $endCommentCount; } else { // comment that are ignored and not kept in the output css $this->count += \strlen($m[0]); // silent comments are not allowed in plain CSS files ! $this->cssOnly || ! \strlen(trim($m[0])) || $this->assertPlainCssValid(false, $this->count - \strlen($m[0])); } $gotWhite = true; } return $gotWhite; } /** * Append comment to current block * * @param array $comment */ protected function appendComment($comment) { if (! $this->discardComments) { $this->env->comments[] = $comment; } } /** * Append statement to current block * * @param array|null $statement * @param integer $pos */ protected function append($statement, $pos = null) { if (! \is_null($statement)) { ! $this->cssOnly || ($statement = $this->assertPlainCssValid($statement, $pos)); if (! \is_null($pos)) { list($line, $column) = $this->getSourcePosition($pos); $statement[static::SOURCE_LINE] = $line; $statement[static::SOURCE_COLUMN] = $column; $statement[static::SOURCE_INDEX] = $this->sourceIndex; } $this->env->children[] = $statement; } $comments = $this->env->comments; if ($comments) { $this->env->children = array_merge($this->env->children, $comments); $this->env->comments = []; } } /** * Returns last child was appended * * @return array|null */ protected function last() { $i = \count($this->env->children) - 1; if (isset($this->env->children[$i])) { return $this->env->children[$i]; } } /** * Parse media query list * * @param array $out * * @return boolean */ protected function mediaQueryList(&$out) { return $this->genericList($out, 'mediaQuery', ',', false); } /** * Parse media query * * @param array $out * * @return boolean */ protected function mediaQuery(&$out) { $expressions = null; $parts = []; if ( ($this->literal('only', 4) && ($only = true) || $this->literal('not', 3) && ($not = true) || true) && $this->mixedKeyword($mediaType) ) { $prop = [Type::T_MEDIA_TYPE]; if (isset($only)) { $prop[] = [Type::T_KEYWORD, 'only']; } if (isset($not)) { $prop[] = [Type::T_KEYWORD, 'not']; } $media = [Type::T_LIST, '', []]; foreach ((array) $mediaType as $type) { if (\is_array($type)) { $media[2][] = $type; } else { $media[2][] = [Type::T_KEYWORD, $type]; } } $prop[] = $media; $parts[] = $prop; } if (empty($parts) || $this->literal('and', 3)) { $this->genericList($expressions, 'mediaExpression', 'and', false); if (\is_array($expressions)) { $parts = array_merge($parts, $expressions[2]); } } $out = $parts; return true; } /** * Parse supports query * * @param array $out * * @return boolean */ protected function supportsQuery(&$out) { $expressions = null; $parts = []; $s = $this->count; $not = false; if ( ($this->literal('not', 3) && ($not = true) || true) && $this->matchChar('(') && ($this->expression($property)) && $this->literal(': ', 2) && $this->valueList($value) && $this->matchChar(')') ) { $support = [Type::T_STRING, '', [[Type::T_KEYWORD, ($not ? 'not ' : '') . '(']]]; $support[2][] = $property; $support[2][] = [Type::T_KEYWORD, ': ']; $support[2][] = $value; $support[2][] = [Type::T_KEYWORD, ')']; $parts[] = $support; $s = $this->count; } else { $this->seek($s); } if ( $this->matchChar('(') && $this->supportsQuery($subQuery) && $this->matchChar(')') ) { $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, '('], $subQuery, [Type::T_KEYWORD, ')']]]; $s = $this->count; } else { $this->seek($s); } if ( $this->literal('not', 3) && $this->supportsQuery($subQuery) ) { $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]]; $s = $this->count; } else { $this->seek($s); } if ( $this->literal('selector(', 9) && $this->selector($selector) && $this->matchChar(')') ) { $support = [Type::T_STRING, '', [[Type::T_KEYWORD, 'selector(']]]; $selectorList = [Type::T_LIST, '', []]; foreach ($selector as $sc) { $compound = [Type::T_STRING, '', []]; foreach ($sc as $scp) { if (\is_array($scp)) { $compound[2][] = $scp; } else { $compound[2][] = [Type::T_KEYWORD, $scp]; } } $selectorList[2][] = $compound; } $support[2][] = $selectorList; $support[2][] = [Type::T_KEYWORD, ')']; $parts[] = $support; $s = $this->count; } else { $this->seek($s); } if ($this->variable($var) or $this->interpolation($var)) { $parts[] = $var; $s = $this->count; } else { $this->seek($s); } if ( $this->literal('and', 3) && $this->genericList($expressions, 'supportsQuery', ' and', false) ) { array_unshift($expressions[2], [Type::T_STRING, '', $parts]); $parts = [$expressions]; $s = $this->count; } else { $this->seek($s); } if ( $this->literal('or', 2) && $this->genericList($expressions, 'supportsQuery', ' or', false) ) { array_unshift($expressions[2], [Type::T_STRING, '', $parts]); $parts = [$expressions]; $s = $this->count; } else { $this->seek($s); } if (\count($parts)) { if ($this->eatWhiteDefault) { $this->whitespace(); } $out = [Type::T_STRING, '', $parts]; return true; } return false; } /** * Parse media expression * * @param array $out * * @return boolean */ protected function mediaExpression(&$out) { $s = $this->count; $value = null; if ( $this->matchChar('(') && $this->expression($feature) && ($this->matchChar(':') && $this->expression($value) || true) && $this->matchChar(')') ) { $out = [Type::T_MEDIA_EXPRESSION, $feature]; if ($value) { $out[] = $value; } return true; } $this->seek($s); return false; } /** * Parse argument values * * @param array $out * * @return boolean */ protected function argValues(&$out) { $discardComments = $this->discardComments; $this->discardComments = true; if ($this->genericList($list, 'argValue', ',', false)) { $out = $list[2]; $this->discardComments = $discardComments; return true; } $this->discardComments = $discardComments; return false; } /** * Parse argument value * * @param array $out * * @return boolean */ protected function argValue(&$out) { $s = $this->count; $keyword = null; if (! $this->variable($keyword) || ! $this->matchChar(':')) { $this->seek($s); $keyword = null; } if ($this->genericList($value, 'expression', '', true)) { $out = [$keyword, $value, false]; $s = $this->count; if ($this->literal('...', 3)) { $out[2] = true; } else { $this->seek($s); } return true; } return false; } /** * Check if a generic directive is known to be able to allow almost any syntax or not * @param mixed $directiveName * @return bool */ protected function isKnownGenericDirective($directiveName) { if (\is_array($directiveName) && \is_string(reset($directiveName))) { $directiveName = reset($directiveName); } if (! \is_string($directiveName)) { return false; } if ( \in_array($directiveName, [ 'at-root', 'media', 'mixin', 'include', 'scssphp-import-once', 'import', 'extend', 'function', 'break', 'continue', 'return', 'each', 'while', 'for', 'if', 'debug', 'warn', 'error', 'content', 'else', 'charset', 'supports', // Todo 'use', 'forward', ]) ) { return true; } return false; } /** * Parse directive value list that considers $vars as keyword * * @param array $out * @param boolean|string $endChar * * @return boolean */ protected function directiveValue(&$out, $endChar = false) { $s = $this->count; if ($this->variable($out)) { if ($endChar && $this->matchChar($endChar, false)) { return true; } if (! $endChar && $this->end()) { return true; } } $this->seek($s); if (\is_string($endChar) && $this->openString($endChar ? $endChar : ';', $out, null, null, true, ";}{")) { if ($endChar && $this->matchChar($endChar, false)) { return true; } $ss = $this->count; if (!$endChar && $this->end()) { $this->seek($ss); return true; } } $this->seek($s); $allowVars = $this->allowVars; $this->allowVars = false; $res = $this->genericList($out, 'spaceList', ','); $this->allowVars = $allowVars; if ($res) { if ($endChar && $this->matchChar($endChar, false)) { return true; } if (! $endChar && $this->end()) { return true; } } $this->seek($s); if ($endChar && $this->matchChar($endChar, false)) { return true; } return false; } /** * Parse comma separated value list * * @param array $out * * @return boolean */ protected function valueList(&$out) { $discardComments = $this->discardComments; $this->discardComments = true; $res = $this->genericList($out, 'spaceList', ','); $this->discardComments = $discardComments; return $res; } /** * Parse a function call, where externals () are part of the call * and not of the value list * * @param $out * @param bool $mandatoryEnclos * @param null|string $charAfter * @param null|bool $eatWhiteSp * @return bool */ protected function functionCallArgumentsList(&$out, $mandatoryEnclos = true, $charAfter = null, $eatWhiteSp = null) { $s = $this->count; if ( $this->matchChar('(') && $this->valueList($out) && $this->matchChar(')') && ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end()) ) { return true; } if (! $mandatoryEnclos) { $this->seek($s); if ( $this->valueList($out) && ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end()) ) { return true; } } $this->seek($s); return false; } /** * Parse space separated value list * * @param array $out * * @return boolean */ protected function spaceList(&$out) { return $this->genericList($out, 'expression'); } /** * Parse generic list * * @param array $out * @param string $parseItem The name of the method used to parse items * @param string $delim * @param boolean $flatten * * @return boolean */ protected function genericList(&$out, $parseItem, $delim = '', $flatten = true) { $s = $this->count; $items = []; $value = null; while ($this->$parseItem($value)) { $trailing_delim = false; $items[] = $value; if ($delim) { if (! $this->literal($delim, \strlen($delim))) { break; } $trailing_delim = true; } else { // if no delim watch that a keyword didn't eat the single/double quote // from the following starting string if ($value[0] === Type::T_KEYWORD) { $word = $value[1]; $last_char = substr($word, -1); if ( strlen($word) > 1 && in_array($last_char, [ "'", '"']) && substr($word, -2, 1) !== '\\' ) { // if there is a non escaped opening quote in the keyword, this seems unlikely a mistake $word = str_replace('\\' . $last_char, '\\\\', $word); if (strpos($word, $last_char) < strlen($word) - 1) { continue; } $currentCount = $this->count; // let's try to rewind to previous char and try a parse $this->count--; // in case the keyword also eat spaces while (substr($this->buffer, $this->count, 1) !== $last_char) { $this->count--; } $nextValue = null; if ($this->$parseItem($nextValue)) { if ($nextValue[0] === Type::T_KEYWORD && $nextValue[1] === $last_char) { // bad try, forget it $this->seek($currentCount); continue; } if ($nextValue[0] !== Type::T_STRING) { // bad try, forget it $this->seek($currentCount); continue; } // OK it was a good idea $value[1] = substr($value[1], 0, -1); array_pop($items); $items[] = $value; $items[] = $nextValue; } else { // bad try, forget it $this->seek($currentCount); continue; } } } } } if (! $items) { $this->seek($s); return false; } if ($trailing_delim) { $items[] = [Type::T_NULL]; } if ($flatten && \count($items) === 1) { $out = $items[0]; } else { $out = [Type::T_LIST, $delim, $items]; } return true; } /** * Parse expression * * @param array $out * @param boolean $listOnly * @param boolean $lookForExp * * @return boolean */ protected function expression(&$out, $listOnly = false, $lookForExp = true) { $s = $this->count; $discard = $this->discardComments; $this->discardComments = true; $allowedTypes = ($listOnly ? [Type::T_LIST] : [Type::T_LIST, Type::T_MAP]); if ($this->matchChar('(')) { if ($this->enclosedExpression($lhs, $s, ')', $allowedTypes)) { if ($lookForExp) { $out = $this->expHelper($lhs, 0); } else { $out = $lhs; } $this->discardComments = $discard; return true; } $this->seek($s); } if (\in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) { if ($this->enclosedExpression($lhs, $s, ']', [Type::T_LIST])) { if ($lookForExp) { $out = $this->expHelper($lhs, 0); } else { $out = $lhs; } $this->discardComments = $discard; return true; } $this->seek($s); } if (! $listOnly && $this->value($lhs)) { if ($lookForExp) { $out = $this->expHelper($lhs, 0); } else { $out = $lhs; } $this->discardComments = $discard; return true; } $this->discardComments = $discard; return false; } /** * Parse expression specifically checking for lists in parenthesis or brackets * * @param array $out * @param integer $s * @param string $closingParen * @param array $allowedTypes * * @return boolean */ protected function enclosedExpression(&$out, $s, $closingParen = ')', $allowedTypes = [Type::T_LIST, Type::T_MAP]) { if ($this->matchChar($closingParen) && \in_array(Type::T_LIST, $allowedTypes)) { $out = [Type::T_LIST, '', []]; switch ($closingParen) { case ')': $out['enclosing'] = 'parent'; // parenthesis list break; case ']': $out['enclosing'] = 'bracket'; // bracketed list break; } return true; } if ( $this->valueList($out) && $this->matchChar($closingParen) && ! ($closingParen === ')' && \in_array($out[0], [Type::T_EXPRESSION, Type::T_UNARY])) && \in_array(Type::T_LIST, $allowedTypes) ) { if ($out[0] !== Type::T_LIST || ! empty($out['enclosing'])) { $out = [Type::T_LIST, '', [$out]]; } switch ($closingParen) { case ')': $out['enclosing'] = 'parent'; // parenthesis list break; case ']': $out['enclosing'] = 'bracket'; // bracketed list break; } return true; } $this->seek($s); if (\in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) { return true; } return false; } /** * Parse left-hand side of subexpression * * @param array $lhs * @param integer $minP * * @return array */ protected function expHelper($lhs, $minP) { $operators = static::$operatorPattern; $ss = $this->count; $whiteBefore = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]); while ($this->match($operators, $m, false) && static::$precedence[$m[1]] >= $minP) { $whiteAfter = isset($this->buffer[$this->count]) && ctype_space($this->buffer[$this->count]); $varAfter = isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '$'; $this->whitespace(); $op = $m[1]; // don't turn negative numbers into expressions if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) { break; } if (! $this->value($rhs) && ! $this->expression($rhs, true, false)) { break; } if ($op === '-' && ! $whiteAfter && $rhs[0] === Type::T_KEYWORD) { break; } // peek and see if rhs belongs to next operator if ($this->peek($operators, $next) && static::$precedence[$next[1]] > static::$precedence[$op]) { $rhs = $this->expHelper($rhs, static::$precedence[$next[1]]); } $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter]; $ss = $this->count; $whiteBefore = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]); } $this->seek($ss); return $lhs; } /** * Parse value * * @param array $out * * @return boolean */ protected function value(&$out) { if (! isset($this->buffer[$this->count])) { return false; } $s = $this->count; $char = $this->buffer[$this->count]; if ( $this->literal('url(', 4) && $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false) ) { $len = strspn( $this->buffer, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=', $this->count ); $this->count += $len; if ($this->matchChar(')')) { $content = substr($this->buffer, $s, $this->count - $s); $out = [Type::T_KEYWORD, $content]; return true; } } $this->seek($s); if ( $this->literal('url(', 4, false) && $this->match('\s*(\/\/[^\s\)]+)\s*', $m) ) { $content = 'url(' . $m[1]; if ($this->matchChar(')')) { $content .= ')'; $out = [Type::T_KEYWORD, $content]; return true; } } $this->seek($s); // not if ($char === 'n' && $this->literal('not', 3, false)) { if ( $this->whitespace() && $this->value($inner) ) { $out = [Type::T_UNARY, 'not', $inner, $this->inParens]; return true; } $this->seek($s); if ($this->parenValue($inner)) { $out = [Type::T_UNARY, 'not', $inner, $this->inParens]; return true; } $this->seek($s); } // addition if ($char === '+') { $this->count++; $follow_white = $this->whitespace(); if ($this->value($inner)) { $out = [Type::T_UNARY, '+', $inner, $this->inParens]; return true; } if ($follow_white) { $out = [Type::T_KEYWORD, $char]; return true; } $this->seek($s); return false; } // negation if ($char === '-') { if ($this->customProperty($out)) { return true; } $this->count++; $follow_white = $this->whitespace(); if ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner)) { $out = [Type::T_UNARY, '-', $inner, $this->inParens]; return true; } if ( $this->keyword($inner) && ! $this->func($inner, $out) ) { $out = [Type::T_UNARY, '-', $inner, $this->inParens]; return true; } if ($follow_white) { $out = [Type::T_KEYWORD, $char]; return true; } $this->seek($s); } // paren if ($char === '(' && $this->parenValue($out)) { return true; } if ($char === '#') { if ($this->interpolation($out) || $this->color($out)) { return true; } $this->count++; if ($this->keyword($keyword)) { $out = [Type::T_KEYWORD, '#' . $keyword]; return true; } $this->count--; } if ($this->matchChar('&', true)) { $out = [Type::T_SELF]; return true; } if ($char === '$' && $this->variable($out)) { return true; } if ($char === 'p' && $this->progid($out)) { return true; } if (($char === '"' || $char === "'") && $this->string($out)) { return true; } if ($this->unit($out)) { return true; } // unicode range with wildcards if ( $this->literal('U+', 2) && $this->match('\?+|([0-9A-F]+(\?+|(-[0-9A-F]+))?)', $m, false) ) { $unicode = explode('-', $m[0]); if (strlen(reset($unicode)) <= 6 && strlen(end($unicode)) <= 6) { $out = [Type::T_KEYWORD, 'U+' . $m[0]]; return true; } $this->count -= strlen($m[0]) + 2; } if ($this->keyword($keyword, false)) { if ($this->func($keyword, $out)) { return true; } $this->whitespace(); if ($keyword === 'null') { $out = [Type::T_NULL]; } else { $out = [Type::T_KEYWORD, $keyword]; } return true; } return false; } /** * Parse parenthesized value * * @param array $out * * @return boolean */ protected function parenValue(&$out) { $s = $this->count; $inParens = $this->inParens; if ($this->matchChar('(')) { if ($this->matchChar(')')) { $out = [Type::T_LIST, '', []]; return true; } $this->inParens = true; if ( $this->expression($exp) && $this->matchChar(')') ) { $out = $exp; $this->inParens = $inParens; return true; } } $this->inParens = $inParens; $this->seek($s); return false; } /** * Parse "progid:" * * @param array $out * * @return boolean */ protected function progid(&$out) { $s = $this->count; if ( $this->literal('progid:', 7, false) && $this->openString('(', $fn) && $this->matchChar('(') ) { $this->openString(')', $args, '('); if ($this->matchChar(')')) { $out = [Type::T_STRING, '', [ 'progid:', $fn, '(', $args, ')' ]]; return true; } } $this->seek($s); return false; } /** * Parse function call * * @param string $name * @param array $func * * @return boolean */ protected function func($name, &$func) { $s = $this->count; if ($this->matchChar('(')) { if ($name === 'alpha' && $this->argumentList($args)) { $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]]; return true; } if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) { $ss = $this->count; if ( $this->argValues($args) && $this->matchChar(')') ) { $func = [Type::T_FUNCTION_CALL, $name, $args]; return true; } $this->seek($ss); } if ( ($this->openString(')', $str, '(') || true) && $this->matchChar(')') ) { $args = []; if (! empty($str)) { $args[] = [null, [Type::T_STRING, '', [$str]]]; } $func = [Type::T_FUNCTION_CALL, $name, $args]; return true; } } $this->seek($s); return false; } /** * Parse function call argument list * * @param array $out * * @return boolean */ protected function argumentList(&$out) { $s = $this->count; $this->matchChar('('); $args = []; while ($this->keyword($var)) { if ( $this->matchChar('=') && $this->expression($exp) ) { $args[] = [Type::T_STRING, '', [$var . '=']]; $arg = $exp; } else { break; } $args[] = $arg; if (! $this->matchChar(',')) { break; } $args[] = [Type::T_STRING, '', [', ']]; } if (! $this->matchChar(')') || ! $args) { $this->seek($s); return false; } $out = $args; return true; } /** * Parse mixin/function definition argument list * * @param array $out * * @return boolean */ protected function argumentDef(&$out) { $s = $this->count; $this->matchChar('('); $args = []; while ($this->variable($var)) { $arg = [$var[1], null, false]; $ss = $this->count; if ( $this->matchChar(':') && $this->genericList($defaultVal, 'expression', '', true) ) { $arg[1] = $defaultVal; } else { $this->seek($ss); } $ss = $this->count; if ($this->literal('...', 3)) { $sss = $this->count; if (! $this->matchChar(')')) { throw $this->parseError('... has to be after the final argument'); } $arg[2] = true; $this->seek($sss); } else { $this->seek($ss); } $args[] = $arg; if (! $this->matchChar(',')) { break; } } if (! $this->matchChar(')')) { $this->seek($s); return false; } $out = $args; return true; } /** * Parse map * * @param array $out * * @return boolean */ protected function map(&$out) { $s = $this->count; if (! $this->matchChar('(')) { return false; } $keys = []; $values = []; while ( $this->genericList($key, 'expression', '', true) && $this->matchChar(':') && $this->genericList($value, 'expression', '', true) ) { $keys[] = $key; $values[] = $value; if (! $this->matchChar(',')) { break; } } if (! $keys || ! $this->matchChar(')')) { $this->seek($s); return false; } $out = [Type::T_MAP, $keys, $values]; return true; } /** * Parse color * * @param array $out * * @return boolean */ protected function color(&$out) { $s = $this->count; if ($this->match('(#([0-9a-f]+)\b)', $m)) { if (\in_array(\strlen($m[2]), [3,4,6,8])) { $out = [Type::T_KEYWORD, $m[0]]; return true; } $this->seek($s); return false; } return false; } /** * Parse number with unit * * @param array $unit * * @return boolean */ protected function unit(&$unit) { $s = $this->count; if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m, false)) { if (\strlen($this->buffer) === $this->count || ! ctype_digit($this->buffer[$this->count])) { $this->whitespace(); $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]); return true; } $this->seek($s); } return false; } /** * Parse string * * @param array $out * * @return boolean */ protected function string(&$out, $keepDelimWithInterpolation = false) { $s = $this->count; if ($this->matchChar('"', false)) { $delim = '"'; } elseif ($this->matchChar("'", false)) { $delim = "'"; } else { return false; } $content = []; $oldWhite = $this->eatWhiteDefault; $this->eatWhiteDefault = false; $hasInterpolation = false; while ($this->matchString($m, $delim)) { if ($m[1] !== '') { $content[] = $m[1]; } if ($m[2] === '#{') { $this->count -= \strlen($m[2]); if ($this->interpolation($inter, false)) { $content[] = $inter; $hasInterpolation = true; } else { $this->count += \strlen($m[2]); $content[] = '#{'; // ignore it } } elseif ($m[2] === "\r") { $content[] = chr(10); // TODO : warning # DEPRECATION WARNING on line x, column y of zzz: # Unescaped multiline strings are deprecated and will be removed in a future version of Sass. # To include a newline in a string, use "\a" or "\a " as in CSS. if ($this->matchChar("\n", false)) { $content[] = ' '; } } elseif ($m[2] === '\\') { if ( $this->literal("\r\n", 2, false) || $this->matchChar("\r", false) || $this->matchChar("\n", false) || $this->matchChar("\f", false) ) { // this is a continuation escaping, to be ignored } elseif ($this->matchEscapeCharacter($c)) { $content[] = $c; } else { throw $this->parseError('Unterminated escape sequence'); } } else { $this->count -= \strlen($delim); break; // delim } } $this->eatWhiteDefault = $oldWhite; if ($this->literal($delim, \strlen($delim))) { if ($hasInterpolation && ! $keepDelimWithInterpolation) { $delim = '"'; } $out = [Type::T_STRING, $delim, $content]; return true; } $this->seek($s); return false; } /** * @param string $out * @param bool $inKeywords * @return bool */ protected function matchEscapeCharacter(&$out, $inKeywords = false) { $s = $this->count; if ($this->match('[a-f0-9]', $m, false)) { $hex = $m[0]; for ($i = 5; $i--;) { if ($this->match('[a-f0-9]', $m, false)) { $hex .= $m[0]; } else { break; } } // CSS allows Unicode escape sequences to be followed by a delimiter space // (necessary in some cases for shorter sequences to disambiguate their end) $this->matchChar(' ', false); $value = hexdec($hex); if (!$inKeywords && ($value == 0 || ($value >= 0xD800 && $value <= 0xDFFF) || $value >= 0x10FFFF)) { $out = "\xEF\xBF\xBD"; // "\u{FFFD}" but with a syntax supported on PHP 5 } elseif ($value < 0x20) { $out = Util::mbChr($value); } else { $out = Util::mbChr($value); } return true; } if ($this->match('.', $m, false)) { if ($inKeywords && in_array($m[0], ["'",'"','@','&',' ','\\',':','/','%'])) { $this->seek($s); return false; } $out = $m[0]; return true; } return false; } /** * Parse keyword or interpolation * * @param array $out * @param boolean $restricted * * @return boolean */ protected function mixedKeyword(&$out, $restricted = false) { $parts = []; $oldWhite = $this->eatWhiteDefault; $this->eatWhiteDefault = false; for (;;) { if ($restricted ? $this->restrictedKeyword($key) : $this->keyword($key)) { $parts[] = $key; continue; } if ($this->interpolation($inter)) { $parts[] = $inter; continue; } break; } $this->eatWhiteDefault = $oldWhite; if (! $parts) { return false; } if ($this->eatWhiteDefault) { $this->whitespace(); } $out = $parts; return true; } /** * Parse an unbounded string stopped by $end * * @param string $end * @param array $out * @param string $nestOpen * @param string $nestClose * @param boolean $rtrim * @param string $disallow * * @return boolean */ protected function openString($end, &$out, $nestOpen = null, $nestClose = null, $rtrim = true, $disallow = null) { $oldWhite = $this->eatWhiteDefault; $this->eatWhiteDefault = false; if ($nestOpen && ! $nestClose) { $nestClose = $end; } $patt = ($disallow ? '[^' . $this->pregQuote($disallow) . ']' : '.'); $patt = '(' . $patt . '*?)([\'"]|#\{|' . $this->pregQuote($end) . '|' . (($nestClose && $nestClose !== $end) ? $this->pregQuote($nestClose) . '|' : '') . static::$commentPattern . ')'; $nestingLevel = 0; $content = []; while ($this->match($patt, $m, false)) { if (isset($m[1]) && $m[1] !== '') { $content[] = $m[1]; if ($nestOpen) { $nestingLevel += substr_count($m[1], $nestOpen); } } $tok = $m[2]; $this->count -= \strlen($tok); if ($tok === $end && ! $nestingLevel) { break; } if ($tok === $nestClose) { $nestingLevel--; } if (($tok === "'" || $tok === '"') && $this->string($str, true)) { $content[] = $str; continue; } if ($tok === '#{' && $this->interpolation($inter)) { $content[] = $inter; continue; } $content[] = $tok; $this->count += \strlen($tok); } $this->eatWhiteDefault = $oldWhite; if (! $content || $tok !== $end) { return false; } // trim the end if ($rtrim && \is_string(end($content))) { $content[\count($content) - 1] = rtrim(end($content)); } $out = [Type::T_STRING, '', $content]; return true; } /** * Parser interpolation * * @param string|array $out * @param boolean $lookWhite save information about whitespace before and after * * @return boolean */ protected function interpolation(&$out, $lookWhite = true) { $oldWhite = $this->eatWhiteDefault; $allowVars = $this->allowVars; $this->allowVars = true; $this->eatWhiteDefault = true; $s = $this->count; if ( $this->literal('#{', 2) && $this->valueList($value) && $this->matchChar('}', false) ) { if ($value === [Type::T_SELF]) { $out = $value; } else { if ($lookWhite) { $left = ($s > 0 && preg_match('/\s/', $this->buffer[$s - 1])) ? ' ' : ''; $right = ( ! empty($this->buffer[$this->count]) && preg_match('/\s/', $this->buffer[$this->count]) ) ? ' ' : ''; } else { $left = $right = false; } $out = [Type::T_INTERPOLATE, $value, $left, $right]; } $this->eatWhiteDefault = $oldWhite; $this->allowVars = $allowVars; if ($this->eatWhiteDefault) { $this->whitespace(); } return true; } $this->seek($s); $this->eatWhiteDefault = $oldWhite; $this->allowVars = $allowVars; return false; } /** * Parse property name (as an array of parts or a string) * * @param array $out * * @return boolean */ protected function propertyName(&$out) { $parts = []; $oldWhite = $this->eatWhiteDefault; $this->eatWhiteDefault = false; for (;;) { if ($this->interpolation($inter)) { $parts[] = $inter; continue; } if ($this->keyword($text)) { $parts[] = $text; continue; } if (! $parts && $this->match('[:.#]', $m, false)) { // css hacks $parts[] = $m[0]; continue; } break; } $this->eatWhiteDefault = $oldWhite; if (! $parts) { return false; } // match comment hack if (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) { if (! empty($m[0])) { $parts[] = $m[0]; $this->count += \strlen($m[0]); } } $this->whitespace(); // get any extra whitespace $out = [Type::T_STRING, '', $parts]; return true; } /** * Parse custom property name (as an array of parts or a string) * * @param array $out * * @return boolean */ protected function customProperty(&$out) { $s = $this->count; if (! $this->literal('--', 2, false)) { return false; } $parts = ['--']; $oldWhite = $this->eatWhiteDefault; $this->eatWhiteDefault = false; for (;;) { if ($this->interpolation($inter)) { $parts[] = $inter; continue; } if ($this->matchChar('&', false)) { $parts[] = [Type::T_SELF]; continue; } if ($this->variable($var)) { $parts[] = $var; continue; } if ($this->keyword($text)) { $parts[] = $text; continue; } break; } $this->eatWhiteDefault = $oldWhite; if (\count($parts) == 1) { $this->seek($s); return false; } $this->whitespace(); // get any extra whitespace $out = [Type::T_STRING, '', $parts]; return true; } /** * Parse comma separated selector list * * @param array $out * @param string|boolean $subSelector * * @return boolean */ protected function selectors(&$out, $subSelector = false) { $s = $this->count; $selectors = []; while ($this->selector($sel, $subSelector)) { $selectors[] = $sel; if (! $this->matchChar(',', true)) { break; } while ($this->matchChar(',', true)) { ; // ignore extra } } if (! $selectors) { $this->seek($s); return false; } $out = $selectors; return true; } /** * Parse whitespace separated selector list * * @param array $out * @param string|boolean $subSelector * * @return boolean */ protected function selector(&$out, $subSelector = false) { $selector = []; $discardComments = $this->discardComments; $this->discardComments = true; for (;;) { $s = $this->count; if ($this->match('[>+~]+', $m, true)) { if ( $subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0 && $m[0] === '+' && $this->match("(\d+|n\b)", $counter) ) { $this->seek($s); } else { $selector[] = [$m[0]]; continue; } } if ($this->selectorSingle($part, $subSelector)) { $selector[] = $part; $this->whitespace(); continue; } break; } $this->discardComments = $discardComments; if (! $selector) { return false; } $out = $selector; return true; } /** * parsing escaped chars in selectors: * - escaped single chars are kept escaped in the selector but in a normalized form * (if not in 0-9a-f range as this would be ambigous) * - other escaped sequences (multibyte chars or 0-9a-f) are kept in their initial escaped form, * normalized to lowercase * * TODO: this is a fallback solution. Ideally escaped chars in selectors should be encoded as the genuine chars, * and escaping added when printing in the Compiler, where/if it's mandatory * - but this require a better formal selector representation instead of the array we have now * * @param string $out * @param bool $keepEscapedNumber * @return bool */ protected function matchEscapeCharacterInSelector(&$out, $keepEscapedNumber = false) { $s_escape = $this->count; if ($this->match('\\\\', $m)) { $out = '\\' . $m[0]; return true; } if ($this->matchEscapeCharacter($escapedout, true)) { if (strlen($escapedout) === 1) { if (!preg_match(",\w,", $escapedout)) { $out = '\\' . $escapedout; return true; } elseif (! $keepEscapedNumber || ! \is_numeric($escapedout)) { $out = $escapedout; return true; } } $escape_sequence = rtrim(substr($this->buffer, $s_escape, $this->count - $s_escape)); if (strlen($escape_sequence) < 6) { $escape_sequence .= ' '; } $out = '\\' . strtolower($escape_sequence); return true; } if ($this->match('\\S', $m)) { $out = '\\' . $m[0]; return true; } return false; } /** * Parse the parts that make up a selector * * {@internal * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder * }} * * @param array $out * @param string|boolean $subSelector * * @return boolean */ protected function selectorSingle(&$out, $subSelector = false) { $oldWhite = $this->eatWhiteDefault; $this->eatWhiteDefault = false; $parts = []; if ($this->matchChar('*', false)) { $parts[] = '*'; } for (;;) { if (! isset($this->buffer[$this->count])) { break; } $s = $this->count; $char = $this->buffer[$this->count]; // see if we can stop early if ($char === '{' || $char === ',' || $char === ';' || $char === '}' || $char === '@') { break; } // parsing a sub selector in () stop with the closing ) if ($subSelector && $char === ')') { break; } //self switch ($char) { case '&': $parts[] = Compiler::$selfSelector; $this->count++; ! $this->cssOnly || $this->assertPlainCssValid(false, $s); continue 2; case '.': $parts[] = '.'; $this->count++; continue 2; case '|': $parts[] = '|'; $this->count++; continue 2; } // handling of escaping in selectors : get the escaped char if ($char === '\\') { $this->count++; if ($this->matchEscapeCharacterInSelector($escaped, true)) { $parts[] = $escaped; continue; } $this->count--; } if ($char === '%') { $this->count++; if ($this->placeholder($placeholder)) { $parts[] = '%'; $parts[] = $placeholder; ! $this->cssOnly || $this->assertPlainCssValid(false, $s); continue; } break; } if ($char === '#') { if ($this->interpolation($inter)) { $parts[] = $inter; ! $this->cssOnly || $this->assertPlainCssValid(false, $s); continue; } $parts[] = '#'; $this->count++; continue; } // a pseudo selector if ($char === ':') { if ($this->buffer[$this->count + 1] === ':') { $this->count += 2; $part = '::'; } else { $this->count++; $part = ':'; } if ($this->mixedKeyword($nameParts, true)) { $parts[] = $part; foreach ($nameParts as $sub) { $parts[] = $sub; } $ss = $this->count; if ( $nameParts === ['not'] || $nameParts === ['is'] || $nameParts === ['has'] || $nameParts === ['where'] || $nameParts === ['slotted'] || $nameParts === ['nth-child'] || $nameParts === ['nth-last-child'] || $nameParts === ['nth-of-type'] || $nameParts === ['nth-last-of-type'] ) { if ( $this->matchChar('(', true) && ($this->selectors($subs, reset($nameParts)) || true) && $this->matchChar(')') ) { $parts[] = '('; while ($sub = array_shift($subs)) { while ($ps = array_shift($sub)) { foreach ($ps as &$p) { $parts[] = $p; } if (\count($sub) && reset($sub)) { $parts[] = ' '; } } if (\count($subs) && reset($subs)) { $parts[] = ', '; } } $parts[] = ')'; } else { $this->seek($ss); } } elseif ( $this->matchChar('(', true) && ($this->openString(')', $str, '(') || true) && $this->matchChar(')') ) { $parts[] = '('; if (! empty($str)) { $parts[] = $str; } $parts[] = ')'; } else { $this->seek($ss); } continue; } } $this->seek($s); // 2n+1 if ($subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0) { if ($this->match("(\s*(\+\s*|\-\s*)?(\d+|n|\d+n))+", $counter)) { $parts[] = $counter[0]; //$parts[] = str_replace(' ', '', $counter[0]); continue; } } $this->seek($s); // attribute selector if ( $char === '[' && $this->matchChar('[') && ($this->openString(']', $str, '[') || true) && $this->matchChar(']') ) { $parts[] = '['; if (! empty($str)) { $parts[] = $str; } $parts[] = ']'; continue; } $this->seek($s); // for keyframes if ($this->unit($unit)) { $parts[] = $unit; continue; } if ($this->restrictedKeyword($name, false, true)) { $parts[] = $name; continue; } break; } $this->eatWhiteDefault = $oldWhite; if (! $parts) { return false; } $out = $parts; return true; } /** * Parse a variable * * @param array $out * * @return boolean */ protected function variable(&$out) { $s = $this->count; if ( $this->matchChar('$', false) && $this->keyword($name) ) { if ($this->allowVars) { $out = [Type::T_VARIABLE, $name]; } else { $out = [Type::T_KEYWORD, '$' . $name]; } return true; } $this->seek($s); return false; } /** * Parse a keyword * * @param string $word * @param boolean $eatWhitespace * @param boolean $inSelector * * @return boolean */ protected function keyword(&$word, $eatWhitespace = null, $inSelector = false) { $s = $this->count; $match = $this->match( $this->utf8 ? '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)*)' : '(([\w_\-\*!"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)([\w\-_"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)*)', $m, false ); if ($match) { $word = $m[1]; // handling of escaping in keyword : get the escaped char if (strpos($word, '\\') !== false) { $send = $this->count; $escapedWord = []; $this->seek($s); $previousEscape = false; while ($this->count < $send) { $char = $this->buffer[$this->count]; $this->count++; if ( $this->count < $send && $char === '\\' && !$previousEscape && ( $inSelector ? $this->matchEscapeCharacterInSelector($out) : $this->matchEscapeCharacter($out, true) ) ) { $escapedWord[] = $out; } else { if ($previousEscape) { $previousEscape = false; } elseif ($char === '\\') { $previousEscape = true; } $escapedWord[] = $char; } } $word = implode('', $escapedWord); } if (is_null($eatWhitespace) ? $this->eatWhiteDefault : $eatWhitespace) { $this->whitespace(); } return true; } return false; } /** * Parse a keyword that should not start with a number * * @param string $word * @param boolean $eatWhitespace * @param boolean $inSelector * * @return boolean */ protected function restrictedKeyword(&$word, $eatWhitespace = null, $inSelector = false) { $s = $this->count; if ($this->keyword($word, $eatWhitespace, $inSelector) && (\ord($word[0]) > 57 || \ord($word[0]) < 48)) { return true; } $this->seek($s); return false; } /** * Parse a placeholder * * @param string|array $placeholder * * @return boolean */ protected function placeholder(&$placeholder) { $match = $this->match( $this->utf8 ? '([\pL\w\-_]+)' : '([\w\-_]+)', $m ); if ($match) { $placeholder = $m[1]; return true; } if ($this->interpolation($placeholder)) { return true; } return false; } /** * Parse a url * * @param array $out * * @return boolean */ protected function url(&$out) { if ($this->literal('url(', 4)) { $s = $this->count; if ( ($this->string($out) || $this->spaceList($out)) && $this->matchChar(')') ) { $out = [Type::T_STRING, '', ['url(', $out, ')']]; return true; } $this->seek($s); if ( $this->openString(')', $out) && $this->matchChar(')') ) { $out = [Type::T_STRING, '', ['url(', $out, ')']]; return true; } } return false; } /** * Consume an end of statement delimiter * @param bool $eatWhitespace * * @return boolean */ protected function end($eatWhitespace = null) { if ($this->matchChar(';', $eatWhitespace)) { return true; } if ($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; } /** * Strip assignment flag from the list * * @param array $value * * @return array */ protected function stripAssignmentFlags(&$value) { $flags = []; for ($token = &$value; $token[0] === Type::T_LIST && ($s = \count($token[2])); $token = &$lastNode) { $lastNode = &$token[2][$s - 1]; while ($lastNode[0] === Type::T_KEYWORD && \in_array($lastNode[1], ['!default', '!global'])) { array_pop($token[2]); $node = end($token[2]); $token = $this->flattenList($token); $flags[] = $lastNode[1]; $lastNode = $node; } } return $flags; } /** * Strip optional flag from selector list * * @param array $selectors * * @return string */ protected function stripOptionalFlag(&$selectors) { $optional = false; $selector = end($selectors); $part = end($selector); if ($part === ['!optional']) { array_pop($selectors[\count($selectors) - 1]); $optional = true; } return $optional; } /** * Turn list of length 1 into value type * * @param array $value * * @return array */ protected function flattenList($value) { if ($value[0] === Type::T_LIST && \count($value[2]) === 1) { return $this->flattenList($value[2][0]); } return $value; } /** * Quote regular expression * * @param string $what * * @return string */ private function pregQuote($what) { return preg_quote($what, '/'); } /** * Extract line numbers from buffer * * @param string $buffer */ private function extractLineNumbers($buffer) { $this->sourcePositions = [0 => 0]; $prev = 0; while (($pos = strpos($buffer, "\n", $prev)) !== false) { $this->sourcePositions[] = $pos; $prev = $pos + 1; } $this->sourcePositions[] = \strlen($buffer); if (substr($buffer, -1) !== "\n") { $this->sourcePositions[] = \strlen($buffer) + 1; } } /** * Get source line number and column (given character position in the buffer) * * @param integer $pos * * @return array */ private function getSourcePosition($pos) { $low = 0; $high = \count($this->sourcePositions); while ($low < $high) { $mid = (int) (($high + $low) / 2); if ($pos < $this->sourcePositions[$mid]) { $high = $mid - 1; continue; } if ($pos >= $this->sourcePositions[$mid + 1]) { $low = $mid + 1; continue; } return [$mid + 1, $pos - $this->sourcePositions[$mid]]; } return [$low + 1, $pos - $this->sourcePositions[$low]]; } /** * Save internal encoding */ private function saveEncoding() { if (\extension_loaded('mbstring')) { $this->encoding = mb_internal_encoding(); mb_internal_encoding('iso-8859-1'); } } /** * Restore internal encoding */ private function restoreEncoding() { if (\extension_loaded('mbstring') && $this->encoding) { mb_internal_encoding($this->encoding); } } } PK � �\�E��< < scssphp/src/Version.phpnu �[��� <?php /** * SCSSPHP * * @copyright 2012-2020 Leaf Corcoran * * @license http://opensource.org/licenses/MIT MIT * * @link http://scssphp.github.io/scssphp */ namespace ScssPhp\ScssPhp; /** * SCSSPHP version * * @author Leaf Corcoran <leafot@gmail.com> */ class Version { const VERSION = '1.4.1'; } PK � �\?�� � scssphp/src/Compiler.phpnu �[��� <?php /** * SCSSPHP * * @copyright 2012-2020 Leaf Corcoran * * @license http://opensource.org/licenses/MIT MIT * * @link http://scssphp.github.io/scssphp */ namespace ScssPhp\ScssPhp; use ScssPhp\ScssPhp\Base\Range; use ScssPhp\ScssPhp\Compiler\Environment; use ScssPhp\ScssPhp\Exception\CompilerException; use ScssPhp\ScssPhp\Exception\ParserException; use ScssPhp\ScssPhp\Exception\SassScriptException; use ScssPhp\ScssPhp\Formatter\Compressed; use ScssPhp\ScssPhp\Formatter\Expanded; use ScssPhp\ScssPhp\Formatter\OutputBlock; use ScssPhp\ScssPhp\Node\Number; use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator; use ScssPhp\ScssPhp\Util\Path; /** * The scss compiler and parser. * * Converting SCSS to CSS is a three stage process. The incoming file is parsed * by `Parser` into a syntax tree, then it is compiled into another tree * representing the CSS structure by `Compiler`. The CSS tree is fed into a * formatter, like `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 `compile` function of `Compiler` is the entry point. * * In summary: * * The `Compiler` class creates an instance of the parser, feeds it SCSS code, * then transforms the resulting tree to a CSS tree. This class also holds the * evaluation context, such as all available mixins and variables at any given * time. * * The `Parser` class is only concerned with parsing its input. * * The `Formatter` takes a CSS tree, and dumps it to a formatted string, * handling things like indentation. */ /** * SCSS compiler * * @author Leaf Corcoran <leafot@gmail.com> */ class Compiler { /** * @deprecated */ const LINE_COMMENTS = 1; /** * @deprecated */ const DEBUG_INFO = 2; /** * @deprecated */ const WITH_RULE = 1; /** * @deprecated */ const WITH_MEDIA = 2; /** * @deprecated */ const WITH_SUPPORTS = 4; /** * @deprecated */ const WITH_ALL = 7; const SOURCE_MAP_NONE = 0; const SOURCE_MAP_INLINE = 1; const SOURCE_MAP_FILE = 2; /** * @var array<string, string> */ protected static $operatorNames = [ '+' => 'add', '-' => 'sub', '*' => 'mul', '/' => 'div', '%' => 'mod', '==' => 'eq', '!=' => 'neq', '<' => 'lt', '>' => 'gt', '<=' => 'lte', '>=' => 'gte', ]; /** * @var array<string, string> */ protected static $namespaces = [ 'special' => '%', 'mixin' => '@', 'function' => '^', ]; public static $true = [Type::T_KEYWORD, 'true']; public static $false = [Type::T_KEYWORD, 'false']; /** @deprecated */ public static $NaN = [Type::T_KEYWORD, 'NaN']; /** @deprecated */ public static $Infinity = [Type::T_KEYWORD, 'Infinity']; public static $null = [Type::T_NULL]; public static $nullString = [Type::T_STRING, '', []]; public static $defaultValue = [Type::T_KEYWORD, '']; public static $selfSelector = [Type::T_SELF]; public static $emptyList = [Type::T_LIST, '', []]; public static $emptyMap = [Type::T_MAP, [], []]; public static $emptyString = [Type::T_STRING, '"', []]; public static $with = [Type::T_KEYWORD, 'with']; public static $without = [Type::T_KEYWORD, 'without']; /** * @var array<int, string|callable> */ protected $importPaths = []; /** * @var array<string, Block> */ protected $importCache = []; /** * @var string[] */ protected $importedFiles = []; protected $userFunctions = []; protected $registeredVars = []; /** * @var array<string, bool> */ protected $registeredFeatures = [ 'extend-selector-pseudoclass' => false, 'at-error' => true, 'units-level-3' => true, 'global-variable-shadowing' => false, ]; /** * @var string|null */ protected $encoding = null; /** * @deprecated */ protected $lineNumberStyle = null; /** * @var int|SourceMapGenerator * @phpstan-var self::SOURCE_MAP_*|SourceMapGenerator */ protected $sourceMap = self::SOURCE_MAP_NONE; protected $sourceMapOptions = []; /** * @var string|\ScssPhp\ScssPhp\Formatter */ protected $formatter = Expanded::class; /** * @var Environment */ protected $rootEnv; /** * @var OutputBlock|null */ protected $rootBlock; /** * @var \ScssPhp\ScssPhp\Compiler\Environment */ protected $env; /** * @var OutputBlock|null */ protected $scope; /** * @var Environment|null */ protected $storeEnv; /** * @var bool|null */ protected $charsetSeen; /** * @var array<int, string> */ protected $sourceNames; /** * @var Cache|null */ protected $cache; /** * @var int */ protected $indentLevel; /** * @var array[] */ protected $extends; /** * @var array<string, int[]> */ protected $extendsMap; /** * @var array<string, int> */ protected $parsedFiles; /** * @var Parser|null */ protected $parser; /** * @var int|null */ protected $sourceIndex; /** * @var int|null */ protected $sourceLine; /** * @var int|null */ protected $sourceColumn; /** * @var resource */ protected $stderr; /** * @var bool|null */ protected $shouldEvaluate; /** * @var null * @deprecated */ protected $ignoreErrors; /** * @var bool */ protected $ignoreCallStackMessage = false; /** * @var array[] */ protected $callStack = []; /** * The directory of the currently processed file * * @var string|null */ private $currentDirectory; /** * The directory of the input file * * @var string */ private $rootDirectory; private $legacyCwdImportPath = true; /** * Constructor * * @param array|null $cacheOptions */ public function __construct($cacheOptions = null) { $this->parsedFiles = []; $this->sourceNames = []; if ($cacheOptions) { $this->cache = new Cache($cacheOptions); } $this->stderr = fopen('php://stderr', 'w'); } /** * Get compiler options * * @return array<string, mixed> */ public function getCompileOptions() { $options = [ 'importPaths' => $this->importPaths, 'registeredVars' => $this->registeredVars, 'registeredFeatures' => $this->registeredFeatures, 'encoding' => $this->encoding, 'sourceMap' => serialize($this->sourceMap), 'sourceMapOptions' => $this->sourceMapOptions, 'formatter' => $this->formatter, 'legacyImportPath' => $this->legacyCwdImportPath, ]; return $options; } /** * Set an alternative error output stream, for testing purpose only * * @param resource $handle * * @return void */ public function setErrorOuput($handle) { $this->stderr = $handle; } /** * Compile scss * * @api * * @param string $code * @param string $path * * @return string */ public function compile($code, $path = null) { if ($this->cache) { $cacheKey = ($path ? $path : '(stdin)') . ':' . md5($code); $compileOptions = $this->getCompileOptions(); $cache = $this->cache->getCache('compile', $cacheKey, $compileOptions); if (\is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) { // check if any dependency file changed before accepting the cache foreach ($cache['dependencies'] as $file => $mtime) { if (! is_file($file) || filemtime($file) !== $mtime) { unset($cache); break; } } if (isset($cache)) { return $cache['out']; } } } $this->indentLevel = -1; $this->extends = []; $this->extendsMap = []; $this->sourceIndex = null; $this->sourceLine = null; $this->sourceColumn = null; $this->env = null; $this->scope = null; $this->storeEnv = null; $this->charsetSeen = null; $this->shouldEvaluate = null; $this->ignoreCallStackMessage = false; if (!\is_null($path) && is_file($path)) { $path = realpath($path) ?: $path; $this->currentDirectory = dirname($path); $this->rootDirectory = $this->currentDirectory; } else { $this->currentDirectory = null; $this->rootDirectory = getcwd(); } try { $this->parser = $this->parserFactory($path); $tree = $this->parser->parse($code); $this->parser = null; $this->formatter = new $this->formatter(); $this->rootBlock = null; $this->rootEnv = $this->pushEnv($tree); $this->injectVariables($this->registeredVars); $this->compileRoot($tree); $this->popEnv(); $sourceMapGenerator = null; if ($this->sourceMap) { if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) { $sourceMapGenerator = $this->sourceMap; $this->sourceMap = self::SOURCE_MAP_FILE; } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) { $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions); } } $out = $this->formatter->format($this->scope, $sourceMapGenerator); $prefix = ''; if (!$this->charsetSeen) { if (strlen($out) !== Util::mbStrlen($out)) { $prefix = '@charset "UTF-8";' . "\n"; $out = $prefix . $out; } } if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) { $sourceMap = $sourceMapGenerator->generateJson($prefix); $sourceMapUrl = null; switch ($this->sourceMap) { case self::SOURCE_MAP_INLINE: $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap)); break; case self::SOURCE_MAP_FILE: $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap); break; } $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl); } } catch (SassScriptException $e) { throw $this->error($e->getMessage()); } if ($this->cache && isset($cacheKey) && isset($compileOptions)) { $v = [ 'dependencies' => $this->getParsedFiles(), 'out' => &$out, ]; $this->cache->setCache('compile', $cacheKey, $v, $compileOptions); } return $out; } /** * Instantiate parser * * @param string $path * * @return \ScssPhp\ScssPhp\Parser */ protected function parserFactory($path) { // https://sass-lang.com/documentation/at-rules/import // CSS files imported by Sass don’t allow any special Sass features. // In order to make sure authors don’t accidentally write Sass in their CSS, // all Sass features that aren’t also valid CSS will produce errors. // Otherwise, the CSS will be rendered as-is. It can even be extended! $cssOnly = false; if (substr($path, '-4') === '.css') { $cssOnly = true; } $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly); $this->sourceNames[] = $path; $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 * @param array|null $block * * @return void */ protected function pushExtends($target, $origin, $block) { $i = \count($this->extends); $this->extends[] = [$target, $origin, $block]; foreach ($target as $part) { if (isset($this->extendsMap[$part])) { $this->extendsMap[$part][] = $i; } else { $this->extendsMap[$part] = [$i]; } } } /** * Make output block * * @param string $type * @param array $selectors * * @return \ScssPhp\ScssPhp\Formatter\OutputBlock */ protected function makeOutputBlock($type, $selectors = null) { $out = new OutputBlock(); $out->type = $type; $out->lines = []; $out->children = []; $out->parent = $this->scope; $out->selectors = $selectors; $out->depth = $this->env->depth; if ($this->env->block instanceof Block) { $out->sourceName = $this->env->block->sourceName; $out->sourceLine = $this->env->block->sourceLine; $out->sourceColumn = $this->env->block->sourceColumn; } else { $out->sourceName = null; $out->sourceLine = null; $out->sourceColumn = null; } return $out; } /** * Compile root * * @param \ScssPhp\ScssPhp\Block $rootBlock * * @return void */ protected function compileRoot(Block $rootBlock) { $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT); $this->compileChildrenNoReturn($rootBlock->children, $this->scope); $this->flattenSelectors($this->scope); $this->missingSelectors(); } /** * Report missing selectors * * @return void */ protected function missingSelectors() { foreach ($this->extends as $extend) { if (isset($extend[3])) { continue; } list($target, $origin, $block) = $extend; // ignore if !optional if ($block[2]) { continue; } $target = implode(' ', $target); $origin = $this->collapseSelectors($origin); $this->sourceLine = $block[Parser::SOURCE_LINE]; throw $this->error("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found."); } } /** * Flatten selectors * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block * @param string $parentKey * * @return void */ protected function flattenSelectors(OutputBlock $block, $parentKey = null) { if ($block->selectors) { $selectors = []; 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 = []; $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); } } /** * Glue parts of :not( or :nth-child( ... that are in general splitted in selectors parts * * @param array $parts * * @return array */ protected function glueFunctionSelectors($parts) { $new = []; foreach ($parts as $part) { if (\is_array($part)) { $part = $this->glueFunctionSelectors($part); $new[] = $part; } else { // a selector part finishing with a ) is the last part of a :not( or :nth-child( // and need to be joined to this if ( \count($new) && \is_string($new[\count($new) - 1]) && \strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false ) { while (\count($new) > 1 && substr($new[\count($new) - 1], -1) !== '(') { $part = array_pop($new) . $part; } $new[\count($new) - 1] .= $part; } else { $new[] = $part; } } } return $new; } /** * Match extends * * @param array $selector * @param array $out * @param integer $from * @param boolean $initial * * @return void */ protected function matchExtends($selector, &$out, $from = 0, $initial = true) { static $partsPile = []; $selector = $this->glueFunctionSelectors($selector); if (\count($selector) == 1 && \in_array(reset($selector), $partsPile)) { return; } $outRecurs = []; foreach ($selector as $i => $part) { if ($i < $from) { continue; } // check that we are not building an infinite loop of extensions // if the new part is just including a previous part don't try to extend anymore if (\count($part) > 1) { foreach ($partsPile as $previousPart) { if (! \count(array_diff($previousPart, $part))) { continue 2; } } } $partsPile[] = $part; if ($this->matchExtendsSingle($part, $origin, $initial)) { $after = \array_slice($selector, $i + 1); $before = \array_slice($selector, 0, $i); list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before); foreach ($origin as $new) { $k = 0; // remove shared parts if (\count($new) > 1) { while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) { $k++; } } if (\count($nonBreakableBefore) && $k === \count($new)) { $k--; } $replacement = []; $tempReplacement = $k > 0 ? \array_slice($new, $k) : $new; for ($l = \count($tempReplacement) - 1; $l >= 0; $l--) { $slice = []; foreach ($tempReplacement[$l] as $chunk) { if (! \in_array($chunk, $slice)) { $slice[] = $chunk; } } array_unshift($replacement, $slice); if (! $this->isImmediateRelationshipCombinator(end($slice))) { break; } } $afterBefore = $l != 0 ? \array_slice($tempReplacement, 0, $l) : []; // Merge shared direct relationships. $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore); $result = array_merge( $before, $mergedBefore, $replacement, $after ); if ($result === $selector) { continue; } $this->pushOrMergeExtentedSelector($out, $result); // recursively check for more matches $startRecurseFrom = \count($before) + min(\count($nonBreakableBefore), \count($mergedBefore)); if (\count($origin) > 1) { $this->matchExtends($result, $out, $startRecurseFrom, false); } else { $this->matchExtends($result, $outRecurs, $startRecurseFrom, false); } // selector sequence merging if (! empty($before) && \count($new) > 1) { $preSharedParts = $k > 0 ? \array_slice($before, 0, $k) : []; $postSharedParts = $k > 0 ? \array_slice($before, $k) : $before; list($betweenSharedParts, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore); $result2 = array_merge( $preSharedParts, $betweenSharedParts, $postSharedParts, $nonBreakabl2, $nonBreakableBefore, $replacement, $after ); $this->pushOrMergeExtentedSelector($out, $result2); } } } array_pop($partsPile); } while (\count($outRecurs)) { $result = array_shift($outRecurs); $this->pushOrMergeExtentedSelector($out, $result); } } /** * Test a part for being a pseudo selector * * @param string $part * @param array $matches * * @return boolean */ protected function isPseudoSelector($part, &$matches) { if ( strpos($part, ':') === 0 && preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches) ) { return true; } return false; } /** * Push extended selector except if * - this is a pseudo selector * - same as previous * - in a white list * in this case we merge the pseudo selector content * * @param array $out * @param array $extended * * @return void */ protected function pushOrMergeExtentedSelector(&$out, $extended) { if (\count($out) && \count($extended) === 1 && \count(reset($extended)) === 1) { $single = reset($extended); $part = reset($single); if ( $this->isPseudoSelector($part, $matchesExtended) && \in_array($matchesExtended[1], [ 'slotted' ]) ) { $prev = end($out); $prev = $this->glueFunctionSelectors($prev); if (\count($prev) === 1 && \count(reset($prev)) === 1) { $single = reset($prev); $part = reset($single); if ( $this->isPseudoSelector($part, $matchesPrev) && $matchesPrev[1] === $matchesExtended[1] ) { $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2); $extended[1] = $matchesPrev[2] . ', ' . $extended[1]; $extended = implode($matchesExtended[1] . '(', $extended); $extended = [ [ $extended ]]; array_pop($out); } } } } $out[] = $extended; } /** * Match extends single * * @param array $rawSingle * @param array $outOrigin * @param boolean $initial * * @return boolean */ protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true) { $counts = []; $single = []; // simple usual cases, no need to do the whole trick if (\in_array($rawSingle, [['>'],['+'],['~']])) { return false; } 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; } } $extendingDecoratedTag = false; if (\count($single) > 1) { $matches = null; $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false; } $outOrigin = []; $found = false; foreach ($single as $k => $part) { if (isset($this->extendsMap[$part])) { foreach ($this->extendsMap[$part] as $idx) { $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1; } } if ( $initial && $this->isPseudoSelector($part, $matches) && ! \in_array($matches[1], [ 'not' ]) ) { $buffer = $matches[2]; $parser = $this->parserFactory(__METHOD__); if ($parser->parseSelector($buffer, $subSelectors, false)) { foreach ($subSelectors as $ksub => $subSelector) { $subExtended = []; $this->matchExtends($subSelector, $subExtended, 0, false); if ($subExtended) { $subSelectorsExtended = $subSelectors; $subSelectorsExtended[$ksub] = $subExtended; foreach ($subSelectorsExtended as $ksse => $sse) { $subSelectorsExtended[$ksse] = $this->collapseSelectors($sse); } $subSelectorsExtended = implode(', ', $subSelectorsExtended); $singleExtended = $single; $singleExtended[$k] = str_replace('(' . $buffer . ')', "($subSelectorsExtended)", $part); $outOrigin[] = [ $singleExtended ]; $found = true; } } } } } foreach ($counts as $idx => $count) { list($target, $origin, /* $block */) = $this->extends[$idx]; $origin = $this->glueFunctionSelectors($origin); // check count if ($count !== \count($target)) { continue; } $this->extends[$idx][3] = true; $rem = array_diff($single, $target); foreach ($origin as $j => $new) { // prevent infinite loop when target extends itself if ($this->isSelfExtend($single, $origin) && ! $initial) { return false; } $replacement = end($new); // Extending a decorated tag with another tag is not possible. if ( $extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag && preg_match('/^[a-z0-9]+$/i', $replacement[0]) ) { unset($origin[$j]); continue; } $combined = $this->combineSelectorSingle($replacement, $rem); if (\count(array_diff($combined, $origin[$j][\count($origin[$j]) - 1]))) { $origin[$j][\count($origin[$j]) - 1] = $combined; } } $outOrigin = array_merge($outOrigin, $origin); $found = true; } return $found; } /** * Extract a relationship from the fragment. * * When extracting the last portion of a selector we will be left with a * fragment which may end with a direction relationship combinator. This * method will extract the relationship fragment and return it along side * the rest. * * @param array $fragment The selector fragment maybe ending with a direction relationship combinator. * * @return array The selector without the relationship fragment if any, the relationship fragment. */ protected function extractRelationshipFromFragment(array $fragment) { $parents = []; $children = []; $j = $i = \count($fragment); for (;;) { $children = $j != $i ? \array_slice($fragment, $j, $i - $j) : []; $parents = \array_slice($fragment, 0, $j); $slice = end($parents); if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) { break; } $j -= 2; } return [$parents, $children]; } /** * Combine selector single * * @param array $base * @param array $other * * @return array */ protected function combineSelectorSingle($base, $other) { $tag = []; $out = []; $wasTag = false; $pseudo = []; while (\count($other) && strpos(end($other), ':') === 0) { array_unshift($pseudo, array_pop($other)); } foreach ([array_reverse($base), array_reverse($other)] as $single) { $rang = count($single); foreach ($single as $part) { if (preg_match('/^[\[:]/', $part)) { $out[] = $part; $wasTag = false; } elseif (preg_match('/^[\.#]/', $part)) { array_unshift($out, $part); $wasTag = false; } elseif (preg_match('/^[^_-]/', $part) && $rang === 1) { $tag[] = $part; $wasTag = true; } elseif ($wasTag) { $tag[\count($tag) - 1] .= $part; } else { array_unshift($out, $part); } $rang--; } } if (\count($tag)) { array_unshift($out, $tag[0]); } while (\count($pseudo)) { $out[] = array_shift($pseudo); } return $out; } /** * Compile media * * @param \ScssPhp\ScssPhp\Block $media * * @return void */ protected function compileMedia(Block $media) { $this->pushEnv($media); $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env)); if (! empty($mediaQueries) && $mediaQueries) { $previousScope = $this->scope; $parentScope = $this->mediaParent($this->scope); foreach ($mediaQueries as $mediaQuery) { $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]); $parentScope->children[] = $this->scope; $parentScope = $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->sourceName = $media->sourceName; $wrapped->sourceIndex = $media->sourceIndex; $wrapped->sourceLine = $media->sourceLine; $wrapped->sourceColumn = $media->sourceColumn; $wrapped->selectors = []; $wrapped->comments = []; $wrapped->parent = $media; $wrapped->children = $media->children; $media->children = [[Type::T_BLOCK, $wrapped]]; } $this->compileChildrenNoReturn($media->children, $this->scope); $this->scope = $previousScope; } $this->popEnv(); } /** * Media parent * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope * * @return \ScssPhp\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 \ScssPhp\ScssPhp\Block|array $block * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * * @return void */ protected function compileDirective($directive, OutputBlock $out) { if (\is_array($directive)) { $directiveName = $this->compileDirectiveName($directive[0]); $s = '@' . $directiveName; if (! empty($directive[1])) { $s .= ' ' . $this->compileValue($directive[1]); } // sass-spec compliance on newline after directives, a bit tricky :/ $appendNewLine = (! empty($directive[2]) || strpos($s, "\n")) ? "\n" : ""; if (\is_array($directive[0]) && empty($directive[1])) { $appendNewLine = "\n"; } if (empty($directive[3])) { $this->appendRootDirective($s . ';' . $appendNewLine, $out, [Type::T_COMMENT, Type::T_DIRECTIVE]); } else { $this->appendOutputLine($out, Type::T_DIRECTIVE, $s . ';'); } } else { $directive->name = $this->compileDirectiveName($directive->name); $s = '@' . $directive->name; if (! empty($directive->value)) { $s .= ' ' . $this->compileValue($directive->value); } if ($directive->name === 'keyframes' || substr($directive->name, -10) === '-keyframes') { $this->compileKeyframeBlock($directive, [$s]); } else { $this->compileNestedBlock($directive, [$s]); } } } /** * directive names can include some interpolation * * @param string|array $directiveName * @return array|string * @throws CompilerException */ protected function compileDirectiveName($directiveName) { if (is_string($directiveName)) { return $directiveName; } return $this->compileValue($directiveName); } /** * Compile at-root * * @param \ScssPhp\ScssPhp\Block $block * * @return void */ protected function compileAtRoot(Block $block) { $env = $this->pushEnv($block); $envs = $this->compactEnv($env); list($with, $without) = $this->compileWith(isset($block->with) ? $block->with : null); // wrap inline selector if ($block->selector) { $wrapped = new Block(); $wrapped->sourceName = $block->sourceName; $wrapped->sourceIndex = $block->sourceIndex; $wrapped->sourceLine = $block->sourceLine; $wrapped->sourceColumn = $block->sourceColumn; $wrapped->selectors = $block->selector; $wrapped->comments = []; $wrapped->parent = $block; $wrapped->children = $block->children; $wrapped->selfParent = $block->selfParent; $block->children = [[Type::T_BLOCK, $wrapped]]; $block->selector = null; } $selfParent = $block->selfParent; if ( ! $block->selfParent->selectors && isset($block->parent) && $block->parent && isset($block->parent->selectors) && $block->parent->selectors ) { $selfParent = $block->parent; } $this->env = $this->filterWithWithout($envs, $with, $without); $saveScope = $this->scope; $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without); // propagate selfParent to the children where they still can be useful $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent); $this->scope = $this->completeScope($this->scope, $saveScope); $this->scope = $saveScope; $this->env = $this->extractEnv($envs); $this->popEnv(); } /** * Filter at-root scope depending of with/without option * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope * @param array $with * @param array $without * * @return OutputBlock */ protected function filterScopeWithWithout($scope, $with, $without) { $filteredScopes = []; $childStash = []; if ($scope->type === TYPE::T_ROOT) { return $scope; } // start from the root while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) { array_unshift($childStash, $scope); $scope = $scope->parent; } for (;;) { if (! $scope) { break; } if ($this->isWith($scope, $with, $without)) { $s = clone $scope; $s->children = []; $s->lines = []; $s->parent = null; if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) { $s->selectors = []; } $filteredScopes[] = $s; } if (\count($childStash)) { $scope = array_shift($childStash); } elseif ($scope->children) { $scope = end($scope->children); } else { $scope = null; } } if (! \count($filteredScopes)) { return $this->rootBlock; } $newScope = array_shift($filteredScopes); $newScope->parent = $this->rootBlock; $this->rootBlock->children[] = $newScope; $p = &$newScope; while (\count($filteredScopes)) { $s = array_shift($filteredScopes); $s->parent = $p; $p->children[] = $s; $newScope = &$p->children[0]; $p = &$p->children[0]; } return $newScope; } /** * found missing selector from a at-root compilation in the previous scope * (if at-root is just enclosing a property, the selector is in the parent tree) * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope * * @return OutputBlock */ protected function completeScope($scope, $previousScope) { if (! $scope->type && (! $scope->selectors || ! \count($scope->selectors)) && \count($scope->lines)) { $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth); } if ($scope->children) { foreach ($scope->children as $k => $c) { $scope->children[$k] = $this->completeScope($c, $previousScope); } } return $scope; } /** * Find a selector by the depth node in the scope * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope * @param integer $depth * * @return array */ protected function findScopeSelectors($scope, $depth) { if ($scope->depth === $depth && $scope->selectors) { return $scope->selectors; } if ($scope->children) { foreach (array_reverse($scope->children) as $c) { if ($s = $this->findScopeSelectors($c, $depth)) { return $s; } } } return []; } /** * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later * * @param array $withCondition * * @return array */ protected function compileWith($withCondition) { // just compile what we have in 2 lists $with = []; $without = ['rule' => true]; if ($withCondition) { if ($withCondition[0] === Type::T_INTERPOLATE) { $w = $this->compileValue($withCondition); $buffer = "($w)"; $parser = $this->parserFactory(__METHOD__); if ($parser->parseValue($buffer, $reParsedWith)) { $withCondition = $reParsedWith; } } if ($this->libMapHasKey([$withCondition, static::$with])) { $without = []; // cancel the default $list = $this->coerceList($this->libMapGet([$withCondition, static::$with])); foreach ($list[2] as $item) { $keyword = $this->compileStringContent($this->coerceString($item)); $with[$keyword] = true; } } if ($this->libMapHasKey([$withCondition, static::$without])) { $without = []; // cancel the default $list = $this->coerceList($this->libMapGet([$withCondition, static::$without])); foreach ($list[2] as $item) { $keyword = $this->compileStringContent($this->coerceString($item)); $without[$keyword] = true; } } } return [$with, $without]; } /** * Filter env stack * * @param Environment[] $envs * @param array $with * @param array $without * * @return Environment * * @phpstan-param non-empty-array<Environment> $envs */ protected function filterWithWithout($envs, $with, $without) { $filtered = []; foreach ($envs as $e) { if ($e->block && ! $this->isWith($e->block, $with, $without)) { $ec = clone $e; $ec->block = null; $ec->selectors = []; $filtered[] = $ec; } else { $filtered[] = $e; } } return $this->extractEnv($filtered); } /** * Filter WITH rules * * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block * @param array $with * @param array $without * * @return boolean */ protected function isWith($block, $with, $without) { if (isset($block->type)) { if ($block->type === Type::T_MEDIA) { return $this->testWithWithout('media', $with, $without); } if ($block->type === Type::T_DIRECTIVE) { if (isset($block->name)) { return $this->testWithWithout($this->compileDirectiveName($block->name), $with, $without); } elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) { return $this->testWithWithout($m[1], $with, $without); } else { return $this->testWithWithout('???', $with, $without); } } } elseif (isset($block->selectors)) { // a selector starting with number is a keyframe rule if (\count($block->selectors)) { $s = reset($block->selectors); while (\is_array($s)) { $s = reset($s); } if (\is_object($s) && $s instanceof Number) { return $this->testWithWithout('keyframes', $with, $without); } } return $this->testWithWithout('rule', $with, $without); } return true; } /** * Test a single type of block against with/without lists * * @param string $what * @param array $with * @param array $without * * @return boolean * true if the block should be kept, false to reject */ protected function testWithWithout($what, $with, $without) { // if without, reject only if in the list (or 'all' is in the list) if (\count($without)) { return (isset($without[$what]) || isset($without['all'])) ? false : true; } // otherwise reject all what is not in the with list return (isset($with[$what]) || isset($with['all'])) ? true : false; } /** * Compile keyframe block * * @param \ScssPhp\ScssPhp\Block $block * @param array $selectors * * @return void */ protected function compileKeyframeBlock(Block $block, $selectors) { $env = $this->pushEnv($block); $envs = $this->compactEnv($env); $this->env = $this->extractEnv(array_filter($envs, function (Environment $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 properties lines * * @param \ScssPhp\ScssPhp\Block $block * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * * @return void */ protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out) { $prefix = $this->compileValue($block->prefix) . '-'; $nested = $this->makeOutputBlock($block->type); $nested->parent = $out; if ($block->hasValue) { $nested->depth = $out->depth + 1; } $out->children[] = $nested; foreach ($block->children as $child) { switch ($child[0]) { case Type::T_ASSIGN: array_unshift($child[1][2], $prefix); break; case Type::T_NESTED_PROPERTY: array_unshift($child[1]->prefix[2], $prefix); break; } $this->compileChild($child, $nested); } } /** * Compile nested block * * @param \ScssPhp\ScssPhp\Block $block * @param array $selectors * * @return void */ protected function compileNestedBlock(Block $block, $selectors) { $this->pushEnv($block); $this->scope = $this->makeOutputBlock($block->type, $selectors); $this->scope->parent->children[] = $this->scope; // wrap assign children in a block // except for @font-face if ($block->type !== Type::T_DIRECTIVE || $this->compileDirectiveName($block->name) !== 'font-face') { // need wrapping? $needWrapping = false; foreach ($block->children as $child) { if ($child[0] === Type::T_ASSIGN) { $needWrapping = true; break; } } if ($needWrapping) { $wrapped = new Block(); $wrapped->sourceName = $block->sourceName; $wrapped->sourceIndex = $block->sourceIndex; $wrapped->sourceLine = $block->sourceLine; $wrapped->sourceColumn = $block->sourceColumn; $wrapped->selectors = []; $wrapped->comments = []; $wrapped->parent = $block; $wrapped->children = $block->children; $wrapped->selfParent = $block->selfParent; $block->children = [[Type::T_BLOCK, $wrapped]]; } } $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 \ScssPhp\ScssPhp\Block $block * * @return void */ protected function compileBlock(Block $block) { $env = $this->pushEnv($block); $env->selectors = $this->evalSelectors($block->selectors); $out = $this->makeOutputBlock(null); $this->scope->children[] = $out; if (\count($block->children)) { $out->selectors = $this->multiplySelectors($env, $block->selfParent); // propagate selfParent to the children where they still can be useful $selfParentSelectors = null; if (isset($block->selfParent->selectors)) { $selfParentSelectors = $block->selfParent->selectors; $block->selfParent->selectors = $out->selectors; } $this->compileChildrenNoReturn($block->children, $out, $block->selfParent); // and revert for the following children of the same block if ($selfParentSelectors) { $block->selfParent->selectors = $selfParentSelectors; } } $this->popEnv(); } /** * Compile the value of a comment that can have interpolation * * @param array $value * @param boolean $pushEnv * * @return string */ protected function compileCommentValue($value, $pushEnv = false) { $c = $value[1]; if (isset($value[2])) { if ($pushEnv) { $this->pushEnv(); } $ignoreCallStackMessage = $this->ignoreCallStackMessage; $this->ignoreCallStackMessage = true; try { $c = $this->compileValue($value[2]); } catch (\Exception $e) { // ignore error in comment compilation which are only interpolation } $this->ignoreCallStackMessage = $ignoreCallStackMessage; if ($pushEnv) { $this->popEnv(); } } return $c; } /** * Compile root level comment * * @param array $block * * @return void */ protected function compileComment($block) { $out = $this->makeOutputBlock(Type::T_COMMENT); $out->lines[] = $this->compileCommentValue($block, true); $this->scope->children[] = $out; } /** * Evaluate selectors * * @param array $selectors * * @return array */ protected function evalSelectors($selectors) { $this->shouldEvaluate = false; $selectors = array_map([$this, 'evalSelector'], $selectors); // after evaluating interpolates, we might need a second pass if ($this->shouldEvaluate) { $selectors = $this->replaceSelfSelector($selectors, '&'); $buffer = $this->collapseSelectors($selectors); $parser = $this->parserFactory(__METHOD__); try { $isValid = $parser->parseSelector($buffer, $newSelectors, true); } catch (ParserException $e) { throw $this->error($e->getMessage()); } if ($isValid) { $selectors = array_map([$this, 'evalSelector'], $newSelectors); } } return $selectors; } /** * Evaluate selector * * @param array $selector * * @return array */ protected function evalSelector($selector) { return array_map([$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 self char or non standard char if (preg_match(',[^\w-],', $p)) { $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 * @param boolean $selectorFormat * if false return a collapsed string * if true return an array description of a structured selector * * @return string */ protected function collapseSelectors($selectors, $selectorFormat = false) { $parts = []; foreach ($selectors as $selector) { $output = []; $glueNext = false; foreach ($selector as $node) { $compound = ''; array_walk_recursive( $node, function ($value, $key) use (&$compound) { $compound .= $value; } ); if ($selectorFormat && $this->isImmediateRelationshipCombinator($compound)) { if (\count($output)) { $output[\count($output) - 1] .= ' ' . $compound; } else { $output[] = $compound; } $glueNext = true; } elseif ($glueNext) { $output[\count($output) - 1] .= ' ' . $compound; $glueNext = false; } else { $output[] = $compound; } } if ($selectorFormat) { foreach ($output as &$o) { $o = [Type::T_STRING, '', [$o]]; } $output = [Type::T_LIST, ' ', $output]; } else { $output = implode(' ', $output); } $parts[] = $output; } if ($selectorFormat) { $parts = [Type::T_LIST, ',', $parts]; } else { $parts = implode(', ', $parts); } return $parts; } /** * Parse down the selector and revert [self] to "&" before a reparsing * * @param array $selectors * * @return array */ protected function replaceSelfSelector($selectors, $replace = null) { foreach ($selectors as &$part) { if (\is_array($part)) { if ($part === [Type::T_SELF]) { if (\is_null($replace)) { $replace = $this->reduce([Type::T_SELF]); $replace = $this->compileValue($replace); } $part = $replace; } else { $part = $this->replaceSelfSelector($part, $replace); } } } return $selectors; } /** * Flatten selector single; joins together .classes and #ids * * @param array $single * * @return array */ protected function flattenSelectorSingle($single) { $joined = []; 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 string|array $selector * * @return string */ protected function compileSelector($selector) { if (! \is_array($selector)) { return $selector; // media and the like } return implode( ' ', array_map( [$this, 'compileSelectorPart'], $selector ) ); } /** * Compile selector part * * @param array $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 (\strlen($part) && '%' === $part[0]) { return true; } } } return false; } /** * @param string $name * * @return void */ protected function pushCallStack($name = '') { $this->callStack[] = [ 'n' => $name, Parser::SOURCE_INDEX => $this->sourceIndex, Parser::SOURCE_LINE => $this->sourceLine, Parser::SOURCE_COLUMN => $this->sourceColumn ]; // infinite calling loop if (\count($this->callStack) > 25000) { // not displayed but you can var_dump it to deep debug $msg = $this->callStackMessage(true, 100); $msg = 'Infinite calling loop'; throw $this->error($msg); } } /** * @return void */ protected function popCallStack() { array_pop($this->callStack); } /** * Compile children and return result * * @param array $stms * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * @param string $traceName * * @return array|null */ protected function compileChildren($stms, OutputBlock $out, $traceName = '') { $this->pushCallStack($traceName); foreach ($stms as $stm) { $ret = $this->compileChild($stm, $out); if (isset($ret)) { $this->popCallStack(); return $ret; } } $this->popCallStack(); return null; } /** * Compile children and throw exception if unexpected `@return` * * @param array $stms * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * @param \ScssPhp\ScssPhp\Block $selfParent * @param string $traceName * * @return void * * @throws \Exception */ protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '') { $this->pushCallStack($traceName); foreach ($stms as $stm) { if ($selfParent && isset($stm[1]) && \is_object($stm[1]) && $stm[1] instanceof Block) { $stm[1]->selfParent = $selfParent; $ret = $this->compileChild($stm, $out); $stm[1]->selfParent = null; } elseif ($selfParent && \in_array($stm[0], [TYPE::T_INCLUDE, TYPE::T_EXTEND])) { $stm['selfParent'] = $selfParent; $ret = $this->compileChild($stm, $out); unset($stm['selfParent']); } else { $ret = $this->compileChild($stm, $out); } if (isset($ret)) { throw $this->error('@return may only be used within a function'); } } $this->popCallStack(); } /** * evaluate media query : compile internal value keeping the structure unchanged * * @param array $queryList * * @return array */ protected function evaluateMediaQuery($queryList) { static $parser = null; $outQueryList = []; foreach ($queryList as $kql => $query) { $shouldReparse = false; foreach ($query as $kq => $q) { for ($i = 1; $i < \count($q); $i++) { $value = $this->compileValue($q[$i]); // the parser had no mean to know if media type or expression if it was an interpolation // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type if ( $q[0] == Type::T_MEDIA_TYPE && (strpos($value, '(') !== false || strpos($value, ')') !== false || strpos($value, ':') !== false || strpos($value, ',') !== false) ) { $shouldReparse = true; } $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value]; } } if ($shouldReparse) { if (\is_null($parser)) { $parser = $this->parserFactory(__METHOD__); } $queryString = $this->compileMediaQuery([$queryList[$kql]]); $queryString = reset($queryString); if (strpos($queryString, '@media ') === 0) { $queryString = substr($queryString, 7); $queries = []; if ($parser->parseMediaQueryList($queryString, $queries)) { $queries = $this->evaluateMediaQuery($queries[2]); while (\count($queries)) { $outQueryList[] = array_shift($queries); } continue; } } } $outQueryList[] = $queryList[$kql]; } return $outQueryList; } /** * Compile media query * * @param array $queryList * * @return array */ protected function compileMediaQuery($queryList) { $start = '@media '; $default = trim($start); $out = []; $current = ''; foreach ($queryList as $query) { $type = null; $parts = []; $mediaTypeOnly = true; foreach ($query as $q) { if ($q[0] !== Type::T_MEDIA_TYPE) { $mediaTypeOnly = false; break; } } foreach ($query as $q) { switch ($q[0]) { case Type::T_MEDIA_TYPE: $newType = array_map([$this, 'compileValue'], \array_slice($q, 1)); // combining not and anything else than media type is too risky and should be avoided if (! $mediaTypeOnly) { if (\in_array(Type::T_NOT, $newType) || ($type && \in_array(Type::T_NOT, $type) )) { if ($type) { array_unshift($parts, implode(' ', array_filter($type))); } if (! empty($parts)) { if (\strlen($current)) { $current .= $this->formatter->tagSeparator; } $current .= implode(' and ', $parts); } if ($current) { $out[] = $start . $current; } $current = ''; $type = null; $parts = []; } } if ($newType === ['all'] && $default) { $default = $start . 'all'; } // all can be safely ignored and mixed with whatever else if ($newType !== ['all']) { if ($type) { $type = $this->mergeMediaTypes($type, $newType); if (empty($type)) { // merge failed : ignore this query that is not valid, skip to the next one $parts = []; $default = ''; // if everything fail, no @media at all continue 3; } } else { $type = $newType; } } 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 (\strlen($current)) { $current .= $this->formatter->tagSeparator; } $current .= implode(' and ', $parts); } } if ($current) { $out[] = $start . $current; } // no @media type except all, and no conflict? if (! $out && $default) { $out[] = $default; } return $out; } /** * Merge direct relationships between selectors * * @param array $selectors1 * @param array $selectors2 * * @return array */ protected function mergeDirectRelationships($selectors1, $selectors2) { if (empty($selectors1) || empty($selectors2)) { return array_merge($selectors1, $selectors2); } $part1 = end($selectors1); $part2 = end($selectors2); if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) { return array_merge($selectors1, $selectors2); } $merged = []; do { $part1 = array_pop($selectors1); $part2 = array_pop($selectors2); if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) { if ($this->isImmediateRelationshipCombinator(reset($merged)[0])) { array_unshift($merged, [$part1[0] . $part2[0]]); $merged = array_merge($selectors1, $selectors2, $merged); } else { $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged); } break; } array_unshift($merged, $part1); } while (! empty($selectors1) && ! empty($selectors2)); return $merged; } /** * 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; } if (\count($type1) > 1) { $m1 = strtolower($type1[0]); $t1 = strtolower($type1[1]); } else { $m1 = ''; $t1 = strtolower($type1[0]); } if (\count($type2) > 1) { $m2 = strtolower($type2[0]); $t2 = strtolower($type2[1]); } else { $m2 = ''; $t2 = strtolower($type2[0]); } if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) { if ($t1 === $t2) { return null; } return [ $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 [Type::T_NOT, $t1]; } if ($t1 !== $t2) { return null; } // t1 == t2, neither m1 nor m2 are "not" return [empty($m1) ? $m2 : $m1, $t1]; } /** * Compile import; returns true if the value was something that could be imported * * @param array $rawPath * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * @param boolean $once * * @return boolean */ protected function compileImport($rawPath, OutputBlock $out, $once = false) { if ($rawPath[0] === Type::T_STRING) { $path = $this->compileStringContent($rawPath); if (strpos($path, 'url(') !== 0 && $path = $this->findImport($path)) { if (! $once || ! \in_array($path, $this->importedFiles)) { $this->importFile($path, $out); $this->importedFiles[] = $path; } return true; } $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out); 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) { $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out); return false; } } foreach ($rawPath[2] as $path) { $this->compileImport($path, $out, $once); } return true; } $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out); return false; } /** * @param array $rawPath * @return string * @throws CompilerException */ protected function compileImportPath($rawPath) { $path = $this->compileValue($rawPath); // case url() without quotes : suppress \r \n remaining in the path // if this is a real string there can not be CR or LF char if (strpos($path, 'url(') === 0) { $path = str_replace(array("\r", "\n"), array('', ' '), $path); } else { // if this is a file name in a string, spaces should be escaped $path = $this->reduce($rawPath); $path = $this->escapeImportPathString($path); $path = $this->compileValue($path); } return $path; } /** * @param array $path * @return array * @throws CompilerException */ protected function escapeImportPathString($path) { switch ($path[0]) { case Type::T_LIST: foreach ($path[2] as $k => $v) { $path[2][$k] = $this->escapeImportPathString($v); } break; case Type::T_STRING: if ($path[1]) { $path = $this->compileValue($path); $path = str_replace(' ', '\\ ', $path); $path = [Type::T_KEYWORD, $path]; } break; } return $path; } /** * Append a root directive like @import or @charset as near as the possible from the source code * (keeping before comments, @import and @charset coming before in the source code) * * @param string $line * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * @param array $allowed * * @return void */ protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT]) { $root = $out; while ($root->parent) { $root = $root->parent; } $i = 0; while ($i < \count($root->children)) { if (! isset($root->children[$i]->type) || ! \in_array($root->children[$i]->type, $allowed)) { break; } $i++; } // remove incompatible children from the bottom of the list $saveChildren = []; while ($i < \count($root->children)) { $saveChildren[] = array_pop($root->children); } // insert the directive as a comment $child = $this->makeOutputBlock(Type::T_COMMENT); $child->lines[] = $line; $child->sourceName = $this->sourceNames[$this->sourceIndex]; $child->sourceLine = $this->sourceLine; $child->sourceColumn = $this->sourceColumn; $root->children[] = $child; // repush children while (\count($saveChildren)) { $root->children[] = array_pop($saveChildren); } } /** * Append lines to the current output block: * directly to the block or through a child if necessary * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * @param string $type * @param string|mixed $line * * @return void */ protected function appendOutputLine(OutputBlock $out, $type, $line) { $outWrite = &$out; // check if it's a flat output or not if (\count($out->children)) { $lastChild = &$out->children[\count($out->children) - 1]; if ( $lastChild->depth === $out->depth && \is_null($lastChild->selectors) && ! \count($lastChild->children) ) { $outWrite = $lastChild; } else { $nextLines = $this->makeOutputBlock($type); $nextLines->parent = $out; $nextLines->depth = $out->depth; $out->children[] = $nextLines; $outWrite = &$nextLines; } } $outWrite->lines[] = $line; } /** * Compile child; returns a value to halt execution * * @param array $child * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * * @return array|Number|null */ protected function compileChild($child, OutputBlock $out) { if (isset($child[Parser::SOURCE_LINE])) { $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null; $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1; $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1; } elseif (\is_array($child) && isset($child[1]->sourceLine)) { $this->sourceIndex = $child[1]->sourceIndex; $this->sourceLine = $child[1]->sourceLine; $this->sourceColumn = $child[1]->sourceColumn; } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) { $this->sourceLine = $out->sourceLine; $this->sourceIndex = array_search($out->sourceName, $this->sourceNames); $this->sourceColumn = $out->sourceColumn; if ($this->sourceIndex === false) { $this->sourceIndex = null; } } switch ($child[0]) { case Type::T_SCSSPHP_IMPORT_ONCE: $rawPath = $this->reduce($child[1]); $this->compileImport($rawPath, $out, true); break; case Type::T_IMPORT: $rawPath = $this->reduce($child[1]); $this->compileImport($rawPath, $out); break; case Type::T_DIRECTIVE: $this->compileDirective($child[1], $out); 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; $this->appendRootDirective('@charset ' . $this->compileValue($child[1]) . ';', $out); } break; case Type::T_CUSTOM_PROPERTY: list(, $name, $value) = $child; $compiledName = $this->compileValue($name); // 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 || $value === static::$nullString) { break; } } $compiledValue = $this->compileValue($value); $line = $this->formatter->customProperty( $compiledName, $compiledValue ); $this->appendOutputLine($out, Type::T_ASSIGN, $line); break; case Type::T_ASSIGN: list(, $name, $value) = $child; if ($name[0] === Type::T_VARIABLE) { $flags = isset($child[3]) ? $child[3] : []; $isDefault = \in_array('!default', $flags); $isGlobal = \in_array('!global', $flags); if ($isGlobal) { $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value); break; } $shouldSet = $isDefault && (\is_null($result = $this->get($name[1], false)) || $result === static::$null); if (! $isDefault || $shouldSet) { $this->set($name[1], $this->reduce($value), true, null, $value); } break; } $compiledName = $this->compileValue($name); // handle shorthand syntaxes : size / line-height... if (\in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) { if ($value[0] === Type::T_VARIABLE) { // if the font value comes from variable, the content is already reduced // (i.e., formulas were already calculated), so we need the original unreduced value $value = $this->get($value[1], true, null, true); } $shorthandValue=&$value; $shorthandDividerNeedsUnit = false; $maxListElements = null; $maxShorthandDividers = 1; switch ($compiledName) { case 'border-radius': $maxListElements = 4; $shorthandDividerNeedsUnit = true; break; } if ($compiledName === 'font' && $value[0] === Type::T_LIST && $value[1] === ',') { // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica" // we need to handle the first list element $shorthandValue=&$value[2][0]; } if ($shorthandValue[0] === Type::T_EXPRESSION && $shorthandValue[1] === '/') { $revert = true; if ($shorthandDividerNeedsUnit) { $divider = $shorthandValue[3]; if (\is_array($divider)) { $divider = $this->reduce($divider, true); } if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) { $revert = false; } } if ($revert) { $shorthandValue = $this->expToString($shorthandValue); } } elseif ($shorthandValue[0] === Type::T_LIST) { foreach ($shorthandValue[2] as &$item) { if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') { if ($maxShorthandDividers > 0) { $revert = true; // if the list of values is too long, this has to be a shorthand, // otherwise it could be a real division if (\is_null($maxListElements) || \count($shorthandValue[2]) <= $maxListElements) { if ($shorthandDividerNeedsUnit) { $divider = $item[3]; if (\is_array($divider)) { $divider = $this->reduce($divider, true); } if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) { $revert = false; } } } if ($revert) { $item = $this->expToString($item); $maxShorthandDividers--; } } } } } } // 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 || $value === static::$nullString) { break; } } $compiledValue = $this->compileValue($value); // ignore empty value if (\strlen($compiledValue)) { $line = $this->formatter->property( $compiledName, $compiledValue ); $this->appendOutputLine($out, Type::T_ASSIGN, $line); } break; case Type::T_COMMENT: if ($out->type === Type::T_ROOT) { $this->compileComment($child); break; } $line = $this->compileCommentValue($child, true); $this->appendOutputLine($out, Type::T_COMMENT, $line); break; case Type::T_MIXIN: case Type::T_FUNCTION: list(, $block) = $child; // the block need to be able to go up to it's parent env to resolve vars $block->parentEnv = $this->getStoreEnv(); $this->set(static::$namespaces[$block->type] . $block->name, $block, true); break; case Type::T_EXTEND: foreach ($child[1] as $sel) { $sel = $this->replaceSelfSelector($sel); $results = $this->evalSelectors([$sel]); foreach ($results as $result) { // only use the first one $result = current($result); $selectors = $out->selectors; if (! $selectors && isset($child['selfParent'])) { $selectors = $this->multiplySelectors($this->env, $child['selfParent']); } $this->pushExtends($result, $selectors, $child); } } 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), ',', true); $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] : static::$null, true); } } $ret = $this->compileChildren($each->children, $out); if ($ret) { $store = $this->env->store; $this->popEnv(); $this->backPropagateEnv($store, $each->vars); return $ret; } } $store = $this->env->store; $this->popEnv(); $this->backPropagateEnv($store, $each->vars); break; case Type::T_WHILE: list(, $while) = $child; while ($this->isTruthy($this->reduce($while->cond, true))) { $ret = $this->compileChildren($while->children, $out); if ($ret) { return $ret; } } break; case Type::T_FOR: list(, $for) = $child; $start = $this->reduce($for->start, true); $end = $this->reduce($for->end, true); if (! $start instanceof Number) { throw $this->error('%s is not a number', $start[0]); } if (! $end instanceof Number) { throw $this->error('%s is not a number', $end[0]); } $start->assertSameUnitOrUnitless($end); $numeratorUnits = $start->getNumeratorUnits(); $denominatorUnits = $start->getDenominatorUnits(); $start = $start->getDimension(); $end = $end->getDimension(); $d = $start < $end ? 1 : -1; $this->pushEnv(); for (;;) { if ( (! $for->until && $start - $d == $end) || ($for->until && $start == $end) ) { break; } $this->set($for->var, new Number($start, $numeratorUnits, $denominatorUnits)); $start += $d; $ret = $this->compileChildren($for->children, $out); if ($ret) { $store = $this->env->store; $this->popEnv(); $this->backPropagateEnv($store, [$for->var]); return $ret; } } $store = $this->env->store; $this->popEnv(); $this->backPropagateEnv($store, [$for->var]); break; case Type::T_RETURN: return $this->reduce($child[1], true); case Type::T_NESTED_PROPERTY: $this->compileNestedPropertiesBlock($child[1], $out); break; case Type::T_INCLUDE: // including a mixin list(, $name, $argValues, $content, $argUsing) = $child; $mixin = $this->get(static::$namespaces['mixin'] . $name, false); if (! $mixin) { throw $this->error("Undefined mixin $name"); } $callingScope = $this->getStoreEnv(); // push scope, apply args $this->pushEnv(); $this->env->depth--; // Find the parent selectors in the env to be able to know what '&' refers to in the mixin // and assign this fake parent to childs $selfParent = null; if (isset($child['selfParent']) && isset($child['selfParent']->selectors)) { $selfParent = $child['selfParent']; } else { $parentSelectors = $this->multiplySelectors($this->env); if ($parentSelectors) { $parent = new Block(); $parent->selectors = $parentSelectors; foreach ($mixin->children as $k => $child) { if (isset($child[1]) && \is_object($child[1]) && $child[1] instanceof Block) { $mixin->children[$k][1]->parent = $parent; } } } } // clone the stored content to not have its scope spoiled by a further call to the same mixin // i.e., recursive @include of the same mixin if (isset($content)) { $copyContent = clone $content; $copyContent->scope = clone $callingScope; $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env); } else { $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env); } // save the "using" argument list for applying it to when "@content" is invoked if (isset($argUsing)) { $this->setRaw(static::$namespaces['special'] . 'using', $argUsing, $this->env); } else { $this->setRaw(static::$namespaces['special'] . 'using', null, $this->env); } if (isset($mixin->args)) { $this->applyArguments($mixin->args, $argValues); } $this->env->marker = 'mixin'; if (! empty($mixin->parentEnv)) { $this->env->declarationScopeParent = $mixin->parentEnv; } else { throw $this->error("@mixin $name() without parentEnv"); } $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . ' ' . $name); $this->popEnv(); break; case Type::T_MIXIN_CONTENT: $env = isset($this->storeEnv) ? $this->storeEnv : $this->env; $content = $this->get(static::$namespaces['special'] . 'content', false, $env); $argUsing = $this->get(static::$namespaces['special'] . 'using', false, $env); $argContent = $child[1]; if (! $content) { break; } $storeEnv = $this->storeEnv; $varsUsing = []; if (isset($argUsing) && isset($argContent)) { // Get the arguments provided for the content with the names provided in the "using" argument list $this->storeEnv = null; $varsUsing = $this->applyArguments($argUsing, $argContent, false); } // restore the scope from the @content $this->storeEnv = $content->scope; // append the vars from using if any foreach ($varsUsing as $name => $val) { $this->set($name, $val, true, $this->storeEnv); } $this->compileChildrenNoReturn($content->children, $out); $this->storeEnv = $storeEnv; break; case Type::T_DEBUG: list(, $value) = $child; $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); $line = $this->sourceLine; $value = $this->compileDebugValue($value); fwrite($this->stderr, "$fname:$line DEBUG: $value\n"); break; case Type::T_WARN: list(, $value) = $child; $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); $line = $this->sourceLine; $value = $this->compileDebugValue($value); fwrite($this->stderr, "WARNING: $value\n on line $line of $fname\n\n"); break; case Type::T_ERROR: list(, $value) = $child; $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); $line = $this->sourceLine; $value = $this->compileValue($this->reduce($value, true)); throw $this->error("File $fname on line $line ERROR: $value\n"); default: throw $this->error("unknown child type: $child[0]"); } } /** * Reduce expression to string * * @param array $exp * @param bool $keepParens * * @return array */ protected function expToString($exp, $keepParens = false) { list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp; $content = []; if ($keepParens && $inParens) { $content[] = '('; } $content[] = $this->reduce($left); if ($whiteLeft) { $content[] = ' '; } $content[] = $op; if ($whiteRight) { $content[] = ' '; } $content[] = $this->reduce($right); if ($keepParens && $inParens) { $content[] = ')'; } return [Type::T_STRING, '', $content]; } /** * Is truthy? * * @param array|Number $value * * @return boolean */ protected function isTruthy($value) { return $value !== static::$false && $value !== static::$null; } /** * Is the value a direct relationship combinator? * * @param string $value * * @return boolean */ protected function isImmediateRelationshipCombinator($value) { return $value === '>' || $value === '+' || $value === '~'; } /** * 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]) || $this->shouldEval($value[3]); } // fall-thru case Type::T_VARIABLE: case Type::T_FUNCTION_CALL: return true; } return false; } /** * Reduce value * * @param array|Number $value * @param boolean $inExp * * @return null|string|array|Number */ protected function reduce($value, $inExp = false) { if (\is_null($value)) { return null; } switch ($value[0]) { case Type::T_EXPRESSION: list(, $op, $left, $right, $inParens) = $value; $opName = isset(static::$operatorNames[$op]) ? static::$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 && (($right[0] !== Type::T_NUMBER && isset($right[2]) && $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([$this, $fn]) || (($fn = "op${ucLType}${ucRType}") && \is_callable([$this, $fn]) && $passOp = true) || (($fn = "op${ucOpName}") && \is_callable([$this, $fn]) && $genOp = true) ) { $shouldEval = $inParens || $inExp; if (isset($passOp)) { $out = $this->$fn($op, $left, $right, $shouldEval); } else { $out = $this->$fn($left, $right, $shouldEval); } if (isset($out)) { 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 instanceof Number) { switch ($op) { case '+': return $exp; case '-': return $exp->unaryMinus(); } } if ($op === 'not') { if ($inExp || $inParens) { if ($exp === static::$false || $exp === static::$null) { return static::$true; } return static::$false; } $op = $op . ' '; } return [Type::T_STRING, '', [$op, $exp]]; case Type::T_VARIABLE: return $this->reduce($this->get($value[1])); 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]); if ($inExp) { return $value[1]; } return $value; case Type::T_FUNCTION_CALL: return $this->fncall($value[1], $value[2]); case Type::T_SELF: $selfParent = ! empty($this->env->block->selfParent) ? $this->env->block->selfParent : null; $selfSelector = $this->multiplySelectors($this->env, $selfParent); $selfSelector = $this->collapseSelectors($selfSelector, true); return $selfSelector; default: return $value; } } /** * Function caller * * @param string $name * @param array $argValues * * @return array|Number */ protected function fncall($functionReference, $argValues) { // a string means this is a static hard reference coming from the parsing if (is_string($functionReference)) { $name = $functionReference; $functionReference = $this->getFunctionReference($name); if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) { $functionReference = [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]]; } } // a function type means we just want a plain css function call if ($functionReference[0] === Type::T_FUNCTION) { // for CSS functions, simply flatten the arguments into a list $listArgs = []; foreach ((array) $argValues as $arg) { if (empty($arg[0]) || count($argValues) === 1) { $listArgs[] = $this->reduce($this->stringifyFncallArgs($arg[1])); } } return [Type::T_FUNCTION, $functionReference[1], [Type::T_LIST, ',', $listArgs]]; } if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) { return static::$defaultValue; } switch ($functionReference[1]) { // SCSS @function case 'scss': return $this->callScssFunction($functionReference[3], $argValues); // native PHP functions case 'user': case 'native': list(,,$name, $fn, $prototype) = $functionReference; // special cases of css valid functions min/max $name = strtolower($name); if (\in_array($name, ['min', 'max'])) { $cssFunction = $this->cssValidArg( [Type::T_FUNCTION_CALL, $name, $argValues], ['min', 'max', 'calc', 'env', 'var'] ); if ($cssFunction !== false) { return $cssFunction; } } $returnValue = $this->callNativeFunction($name, $fn, $prototype, $argValues); if (! isset($returnValue)) { return $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $argValues); } return $returnValue; default: return static::$defaultValue; } } protected function cssValidArg($arg, $allowed_function = [], $inFunction = false) { switch ($arg[0]) { case Type::T_INTERPOLATE: return [Type::T_KEYWORD, $this->CompileValue($arg)]; case Type::T_FUNCTION: if (! \in_array($arg[1], $allowed_function)) { return false; } if ($arg[2][0] === Type::T_LIST) { foreach ($arg[2][2] as $k => $subarg) { $arg[2][2][$k] = $this->cssValidArg($subarg, $allowed_function, $arg[1]); if ($arg[2][2][$k] === false) { return false; } } } return $arg; case Type::T_FUNCTION_CALL: if (! \in_array($arg[1], $allowed_function)) { return false; } $cssArgs = []; foreach ($arg[2] as $argValue) { if ($argValue === static::$null) { return false; } $cssArg = $this->cssValidArg($argValue[1], $allowed_function, $arg[1]); if (empty($argValue[0]) && $cssArg !== false) { $cssArgs[] = [$argValue[0], $cssArg]; } else { return false; } } return $this->fncall([Type::T_FUNCTION, $arg[1], [Type::T_LIST, ',', []]], $cssArgs); case Type::T_STRING: case Type::T_KEYWORD: if (!$inFunction or !\in_array($inFunction, ['calc', 'env', 'var'])) { return false; } return $this->stringifyFncallArgs($arg); case Type::T_NUMBER: return $this->stringifyFncallArgs($arg); case Type::T_LIST: if (!$inFunction) { return false; } if (empty($arg['enclosing']) and $arg[1] === '') { foreach ($arg[2] as $k => $subarg) { $arg[2][$k] = $this->cssValidArg($subarg, $allowed_function, $inFunction); if ($arg[2][$k] === false) { return false; } } $arg[0] = Type::T_STRING; return $arg; } return false; case Type::T_EXPRESSION: if (! \in_array($arg[1], ['+', '-', '/', '*'])) { return false; } $arg[2] = $this->cssValidArg($arg[2], $allowed_function, $inFunction); $arg[3] = $this->cssValidArg($arg[3], $allowed_function, $inFunction); if ($arg[2] === false || $arg[3] === false) { return false; } return $this->expToString($arg, true); case Type::T_VARIABLE: case Type::T_SELF: default: return false; } } /** * Reformat fncall arguments to proper css function output * * @param $arg * * @return array|\ArrayAccess|Number|string|null */ protected function stringifyFncallArgs($arg) { switch ($arg[0]) { case Type::T_LIST: foreach ($arg[2] as $k => $v) { $arg[2][$k] = $this->stringifyFncallArgs($v); } break; case Type::T_EXPRESSION: if ($arg[1] === '/') { $arg[2] = $this->stringifyFncallArgs($arg[2]); $arg[3] = $this->stringifyFncallArgs($arg[3]); $arg[5] = $arg[6] = false; // no space around / $arg = $this->expToString($arg); } break; case Type::T_FUNCTION_CALL: $name = strtolower($arg[1]); if (in_array($name, ['max', 'min', 'calc'])) { $args = $arg[2]; $arg = $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $args); } break; } return $arg; } /** * Find a function reference * @param string $name * @param bool $safeCopy * @return array */ protected function getFunctionReference($name, $safeCopy = false) { // SCSS @function if ($func = $this->get(static::$namespaces['function'] . $name, false)) { if ($safeCopy) { $func = clone $func; } return [Type::T_FUNCTION_REFERENCE, 'scss', $name, $func]; } // native PHP functions // try to find a native lib function $normalizedName = $this->normalizeName($name); $libName = null; if (isset($this->userFunctions[$normalizedName])) { // see if we can find a user function list($f, $prototype) = $this->userFunctions[$normalizedName]; return [Type::T_FUNCTION_REFERENCE, 'user', $name, $f, $prototype]; } if (($f = $this->getBuiltinFunction($normalizedName)) && \is_callable($f)) { $libName = $f[1]; $prototype = isset(static::$$libName) ? static::$$libName : null; return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype]; } return static::$null; } /** * Normalize name * * @param string $name * * @return string */ protected function normalizeName($name) { return str_replace('-', '_', $name); } /** * Normalize value * * @param array|Number $value * * @return array|Number */ public function normalizeValue($value) { $value = $this->coerceForExpression($this->reduce($value)); switch ($value[0]) { case Type::T_LIST: $value = $this->extractInterpolation($value); if ($value[0] !== Type::T_LIST) { return [Type::T_KEYWORD, $this->compileValue($value)]; } foreach ($value[2] as $key => $item) { $value[2][$key] = $this->normalizeValue($item); } if (! empty($value['enclosing'])) { unset($value['enclosing']); } return $value; case Type::T_STRING: return [$value[0], '"', [$this->compileStringContent($value)]]; case Type::T_INTERPOLATE: return [Type::T_KEYWORD, $this->compileValue($value)]; default: return $value; } } /** * Add numbers * * @param Number $left * @param Number $right * * @return Number */ protected function opAddNumberNumber(Number $left, Number $right) { return $left->plus($right); } /** * Multiply numbers * * @param Number $left * @param Number $right * * @return Number */ protected function opMulNumberNumber(Number $left, Number $right) { return $left->times($right); } /** * Subtract numbers * * @param Number $left * @param Number $right * * @return Number */ protected function opSubNumberNumber(Number $left, Number $right) { return $left->minus($right); } /** * Divide numbers * * @param Number $left * @param Number $right * * @return Number */ protected function opDivNumberNumber(Number $left, Number $right) { return $left->dividedBy($right); } /** * Mod numbers * * @param Number $left * @param Number $right * * @return Number */ protected function opModNumberNumber(Number $left, Number $right) { return $left->modulo($right); } /** * Add strings * * @param array $left * @param array $right * * @return array|null */ 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; } return null; } /** * Boolean and * * @param array|Number $left * @param array|Number $right * @param boolean $shouldEval * * @return array|Number|null */ protected function opAnd($left, $right, $shouldEval) { $truthy = ($left === static::$null || $right === static::$null) || ($left === static::$false || $left === static::$true) && ($right === static::$false || $right === static::$true); if (! $shouldEval) { if (! $truthy) { return null; } } if ($left !== static::$false && $left !== static::$null) { return $this->reduce($right, true); } return $left; } /** * Boolean or * * @param array|Number $left * @param array|Number $right * @param boolean $shouldEval * * @return array|Number|null */ protected function opOr($left, $right, $shouldEval) { $truthy = ($left === static::$null || $right === static::$null) || ($left === static::$false || $left === static::$true) && ($right === static::$false || $right === static::$true); if (! $shouldEval) { if (! $truthy) { return null; } } if ($left !== static::$false && $left !== static::$null) { 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) { if ($op !== '==' && $op !== '!=') { $warning = "Color arithmetic is deprecated and will be an error in future versions.\n" . "Consider using Sass's color functions instead."; $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); $line = $this->sourceLine; fwrite($this->stderr, "DEPRECATION WARNING: $warning\n on line $line of $fname\n\n"); } $out = [Type::T_COLOR]; foreach ([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 '%': if ($rval == 0) { throw $this->error("color: Can't take modulo by zero"); } $out[] = $lval % $rval; break; case '/': if ($rval == 0) { throw $this->error("color: Can't divide by zero"); } $out[] = (int) ($lval / $rval); break; case '==': return $this->opEq($left, $right); case '!=': return $this->opNeq($left, $right); default: throw $this->error("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 Number $right * * @return array */ protected function opColorNumber($op, $left, Number $right) { if ($op === '==') { return static::$false; } if ($op === '!=') { return static::$true; } $value = $right->getDimension(); return $this->opColorColor( $op, $left, [Type::T_COLOR, $value, $value, $value] ); } /** * Compare number and color * * @param string $op * @param Number $left * @param array $right * * @return array */ protected function opNumberColor($op, Number $left, $right) { if ($op === '==') { return static::$false; } if ($op === '!=') { return static::$true; } $value = $left->getDimension(); return $this->opColorColor( $op, [Type::T_COLOR, $value, $value, $value], $right ); } /** * Compare number1 == number2 * * @param array|Number $left * @param array|Number $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|Number $left * @param array|Number $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 Number $left * @param Number $right * * @return array */ protected function opEqNumberNumber(Number $left, Number $right) { return $this->toBool($left->equals($right)); } /** * Compare number1 != number2 * * @param Number $left * @param Number $right * * @return array */ protected function opNeqNumberNumber(Number $left, Number $right) { return $this->toBool(!$left->equals($right)); } /** * Compare number1 >= number2 * * @param Number $left * @param Number $right * * @return array */ protected function opGteNumberNumber(Number $left, Number $right) { return $this->toBool($left->greaterThanOrEqual($right)); } /** * Compare number1 > number2 * * @param Number $left * @param Number $right * * @return array */ protected function opGtNumberNumber(Number $left, Number $right) { return $this->toBool($left->greaterThan($right)); } /** * Compare number1 <= number2 * * @param Number $left * @param Number $right * * @return array */ protected function opLteNumberNumber(Number $left, Number $right) { return $this->toBool($left->lessThanOrEqual($right)); } /** * Compare number1 < number2 * * @param Number $left * @param Number $right * * @return array */ protected function opLtNumberNumber(Number $left, Number $right) { return $this->toBool($left->lessThan($right)); } /** * Cast to boolean * * @api * * @param mixed $thing * * @return array */ public function toBool($thing) { return $thing ? static::$true : static::$false; } /** * Escape non printable chars in strings output as in dart-sass * @param string $string * @return string */ public function escapeNonPrintableChars($string, $inKeyword = false) { static $replacement = []; if (empty($replacement[$inKeyword])) { for ($i = 0; $i < 32; $i++) { if ($i !== 9 || $inKeyword) { $replacement[$inKeyword][chr($i)] = '\\' . dechex($i) . ($inKeyword ? ' ' : chr(0)); } } } $string = str_replace(array_keys($replacement[$inKeyword]), array_values($replacement[$inKeyword]), $string); // chr(0) is not a possible char from the input, so any chr(0) comes from our escaping replacement if (strpos($string, chr(0)) !== false) { if (substr($string, -1) === chr(0)) { $string = substr($string, 0, -1); } $string = str_replace( [chr(0) . '\\',chr(0) . ' '], [ '\\', ' '], $string ); if (strpos($string, chr(0)) !== false) { $parts = explode(chr(0), $string); $string = array_shift($parts); while (count($parts)) { $next = array_shift($parts); if (strpos("0123456789abcdefABCDEF" . chr(9), $next[0]) !== false) { $string .= " "; } $string .= $next; } } } return $string; } /** * 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|Number|string $value * * @return string */ public function compileValue($value) { $value = $this->reduce($value); switch ($value[0]) { case Type::T_KEYWORD: if (is_string($value[1])) { $value[1] = $this->escapeNonPrintableChars($value[1], true); } 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 = $this->compileRGBAValue($r); $g = $this->compileRGBAValue($g); $b = $this->compileRGBAValue($b); if (\count($value) === 5) { $alpha = $this->compileRGBAValue($value[4], true); if (! is_numeric($alpha) || $alpha < 1) { $colorName = Colors::RGBaToColorName($r, $g, $b, $alpha); if (! \is_null($colorName)) { return $colorName; } if (is_numeric($alpha)) { $a = new Number($alpha, ''); } else { $a = $alpha; } return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')'; } } if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b)) { return 'rgb(' . $r . ', ' . $g . ', ' . $b . ')'; } $colorName = Colors::RGBaToColorName($r, $g, $b); if (! \is_null($colorName)) { return $colorName; } $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 $value->output($this); case Type::T_STRING: $content = $this->compileStringContent($value); if ($value[1]) { $content = str_replace('\\', '\\\\', $content); $content = $this->escapeNonPrintableChars($content); // force double quote as string quote for the output in certain cases if ( $value[1] === "'" && (strpos($content, '"') === false or strpos($content, "'") !== false) && strpbrk($content, '{}\\\'') !== false ) { $value[1] = '"'; } elseif ( $value[1] === '"' && (strpos($content, '"') !== false and strpos($content, "'") === false) ) { $value[1] = "'"; } $content = str_replace($value[1], '\\' . $value[1], $content); } return $value[1] . $content . $value[1]; case Type::T_FUNCTION: $args = ! empty($value[2]) ? $this->compileValue($value[2]) : ''; return "$value[1]($args)"; case Type::T_FUNCTION_REFERENCE: $name = ! empty($value[2]) ? $value[2] : ''; return "get-function(\"$name\")"; case Type::T_LIST: $value = $this->extractInterpolation($value); if ($value[0] !== Type::T_LIST) { return $this->compileValue($value); } list(, $delim, $items) = $value; $pre = $post = ''; if (! empty($value['enclosing'])) { switch ($value['enclosing']) { case 'parent': //$pre = '('; //$post = ')'; break; case 'forced_parent': $pre = '('; $post = ')'; break; case 'bracket': case 'forced_bracket': $pre = '['; $post = ']'; break; } } $prefix_value = ''; if ($delim !== ' ') { $prefix_value = ' '; } $filtered = []; $same_string_quote = null; foreach ($items as $item) { if (\is_null($same_string_quote)) { $same_string_quote = false; if ($item[0] === Type::T_STRING) { $same_string_quote = $item[1]; foreach ($items as $ii) { if ($ii[0] !== Type::T_STRING) { $same_string_quote = false; break; } } } } if ($item[0] === Type::T_NULL) { continue; } if ($same_string_quote === '"' && $item[0] === Type::T_STRING && $item[1]) { $item[1] = $same_string_quote; } $compiled = $this->compileValue($item); if ($prefix_value && \strlen($compiled)) { $compiled = $prefix_value . $compiled; } $filtered[] = $compiled; } return $pre . substr(implode("$delim", $filtered), \strlen($prefix_value)) . $post; case Type::T_MAP: $keys = $value[1]; $values = $value[2]; $filtered = []; 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; $delim = $left[1]; if ($delim && $delim !== ' ' && ! $whiteLeft) { $delim .= ' '; } $left = \count($left[2]) > 0 ? $this->compileValue($left) . $delim . $whiteLeft : ''; $delim = $right[1]; if ($delim && $delim !== ' ') { $delim .= ' '; } $right = \count($right[2]) > 0 ? $whiteRight . $delim . $this->compileValue($right) : ''; return $left . $this->compileValue($interpolate) . $right; case Type::T_INTERPOLATE: // strip quotes if it's a string $reduced = $this->reduce($value[1]); switch ($reduced[0]) { case Type::T_LIST: $reduced = $this->extractInterpolation($reduced); if ($reduced[0] !== Type::T_LIST) { break; } list(, $delim, $items) = $reduced; if ($delim !== ' ') { $delim .= ' '; } $filtered = []; foreach ($items as $item) { if ($item[0] === Type::T_NULL) { continue; } $temp = $this->compileValue([Type::T_KEYWORD, $item]); if ($temp[0] === Type::T_STRING) { $filtered[] = $this->compileStringContent($temp); } elseif ($temp[0] === Type::T_KEYWORD) { $filtered[] = $temp[1]; } else { $filtered[] = $this->compileValue($temp); } } $reduced = [Type::T_KEYWORD, implode("$delim", $filtered)]; break; case Type::T_STRING: $reduced = [Type::T_STRING, '', [$this->compileStringContent($reduced)]]; break; case Type::T_NULL: $reduced = [Type::T_KEYWORD, '']; } return $this->compileValue($reduced); case Type::T_NULL: return 'null'; case Type::T_COMMENT: return $this->compileCommentValue($value); default: throw $this->error('unknown value type: ' . json_encode($value)); } } /** * @param array $value * * @return array|string */ protected function compileDebugValue($value) { $value = $this->reduce($value, true); switch ($value[0]) { case Type::T_STRING: return $this->compileStringContent($value); default: return $this->compileValue($value); } } /** * 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 = []; 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 = [Type::T_LIST, $list[1], \array_slice($items, 0, $i)]; $after = [Type::T_LIST, $list[1], \array_slice($items, $i + 1)]; return [Type::T_INTERPOLATED, $item, $before, $after]; } } return $list; } /** * Find the final set of selectors * * @param \ScssPhp\ScssPhp\Compiler\Environment $env * @param \ScssPhp\ScssPhp\Block $selfParent * * @return array */ protected function multiplySelectors(Environment $env, $selfParent = null) { $envs = $this->compactEnv($env); $selectors = []; $parentSelectors = [[]]; $selfParentSelectors = null; if (! \is_null($selfParent) && $selfParent->selectors) { $selfParentSelectors = $this->evalSelectors($selfParent->selectors); } while ($env = array_pop($envs)) { if (empty($env->selectors)) { continue; } $selectors = $env->selectors; do { $stillHasSelf = false; $prevSelectors = $selectors; $selectors = []; foreach ($parentSelectors as $parent) { foreach ($prevSelectors as $selector) { if ($selfParentSelectors) { foreach ($selfParentSelectors as $selfParent) { // if no '&' in the selector, each call will give same result, only add once $s = $this->joinSelectors($parent, $selector, $stillHasSelf, $selfParent); $selectors[serialize($s)] = $s; } } else { $s = $this->joinSelectors($parent, $selector, $stillHasSelf); $selectors[serialize($s)] = $s; } } } } while ($stillHasSelf); $parentSelectors = $selectors; } $selectors = array_values($selectors); // case we are just starting a at-root : nothing to multiply but parentSelectors if (! $selectors && $selfParentSelectors) { $selectors = $selfParentSelectors; } return $selectors; } /** * Join selectors; looks for & to replace, or append parent before child * * @param array $parent * @param array $child * @param boolean $stillHasSelf * @param array $selfParentSelectors * @return array */ protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null) { $setSelf = false; $out = []; foreach ($child as $part) { $newPart = []; foreach ($part as $p) { // only replace & once and should be recalled to be able to make combinations if ($p === static::$selfSelector && $setSelf) { $stillHasSelf = true; } if ($p === static::$selfSelector && ! $setSelf) { $setSelf = true; if (\is_null($selfParentSelectors)) { $selfParentSelectors = $parent; } foreach ($selfParentSelectors as $i => $parentPart) { if ($i > 0) { $out[] = $newPart; $newPart = []; } foreach ($parentPart as $pp) { if (\is_array($pp)) { $flatten = []; array_walk_recursive($pp, function ($a) use (&$flatten) { $flatten[] = $a; }); $pp = implode($flatten); } $newPart[] = $pp; } } } else { $newPart[] = $p; } } $out[] = $newPart; } return $setSelf ? $out : array_merge($parent, $child); } /** * Multiply media * * @param \ScssPhp\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 : [[[Type::T_MEDIA_VALUE, $env->block->value]]]; $store = [$this->env, $this->storeEnv]; $this->env = $env; $this->storeEnv = null; $parentQueries = $this->evaluateMediaQuery($parentQueries); list($this->env, $this->storeEnv) = $store; if (\is_null($childQueries)) { $childQueries = $parentQueries; } else { $originalQueries = $childQueries; $childQueries = []; foreach ($parentQueries as $parentQuery) { foreach ($originalQueries as $childQuery) { $childQueries[] = array_merge( $parentQuery, [[Type::T_MEDIA_TYPE, [Type::T_KEYWORD, 'all']]], $childQuery ); } } } return $this->multiplyMedia($env->parent, $childQueries); } /** * Convert env linked list to stack * * @param Environment $env * * @return Environment[] * * @phpstan-return non-empty-array<Environment> */ protected function compactEnv(Environment $env) { for ($envs = []; $env; $env = $env->parent) { $envs[] = $env; } return $envs; } /** * Convert env stack to singly linked list * * @param Environment[] $envs * * @return Environment * * @phpstan-param non-empty-array<Environment> $envs */ protected function extractEnv($envs) { for ($env = null; $e = array_pop($envs);) { $e->parent = $env; $env = $e; } return $env; } /** * Push environment * * @param \ScssPhp\ScssPhp\Block $block * * @return \ScssPhp\ScssPhp\Compiler\Environment */ protected function pushEnv(Block $block = null) { $env = new Environment(); $env->parent = $this->env; $env->parentStore = $this->storeEnv; $env->store = []; $env->block = $block; $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0; $this->env = $env; $this->storeEnv = null; return $env; } /** * Pop environment * * @return void */ protected function popEnv() { $this->storeEnv = $this->env->parentStore; $this->env = $this->env->parent; } /** * Propagate vars from a just poped Env (used in @each and @for) * * @param array $store * @param null|string[] $excludedVars * * @return void */ protected function backPropagateEnv($store, $excludedVars = null) { foreach ($store as $key => $value) { if (empty($excludedVars) || ! \in_array($key, $excludedVars)) { $this->set($key, $value, true); } } } /** * Get store environment * * @return \ScssPhp\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 \ScssPhp\ScssPhp\Compiler\Environment $env * @param mixed $valueUnreduced * * @return void */ protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null) { $name = $this->normalizeName($name); if (! isset($env)) { $env = $this->getStoreEnv(); } if ($shadow) { $this->setRaw($name, $value, $env, $valueUnreduced); } else { $this->setExisting($name, $value, $env, $valueUnreduced); } } /** * Set existing variable * * @param string $name * @param mixed $value * @param \ScssPhp\ScssPhp\Compiler\Environment $env * @param mixed $valueUnreduced * * @return void */ protected function setExisting($name, $value, Environment $env, $valueUnreduced = null) { $storeEnv = $env; $specialContentKey = static::$namespaces['special'] . 'content'; $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%'; $maxDepth = 10000; for (;;) { if ($maxDepth-- <= 0) { break; } if (\array_key_exists($name, $env->store)) { break; } if (! $hasNamespace && isset($env->marker)) { if (! empty($env->store[$specialContentKey])) { $env = $env->store[$specialContentKey]->scope; continue; } if (! empty($env->declarationScopeParent)) { $env = $env->declarationScopeParent; continue; } else { $env = $storeEnv; break; } } if (isset($env->parentStore)) { $env = $env->parentStore; } elseif (isset($env->parent)) { $env = $env->parent; } else { $env = $storeEnv; break; } } $env->store[$name] = $value; if ($valueUnreduced) { $env->storeUnreduced[$name] = $valueUnreduced; } } /** * Set raw variable * * @param string $name * @param mixed $value * @param \ScssPhp\ScssPhp\Compiler\Environment $env * @param mixed $valueUnreduced * * @return void */ protected function setRaw($name, $value, Environment $env, $valueUnreduced = null) { $env->store[$name] = $value; if ($valueUnreduced) { $env->storeUnreduced[$name] = $valueUnreduced; } } /** * Get variable * * @api * * @param string $name * @param boolean $shouldThrow * @param \ScssPhp\ScssPhp\Compiler\Environment $env * @param boolean $unreduced * * @return mixed|null */ public function get($name, $shouldThrow = true, Environment $env = null, $unreduced = false) { $normalizedName = $this->normalizeName($name); $specialContentKey = static::$namespaces['special'] . 'content'; if (! isset($env)) { $env = $this->getStoreEnv(); } $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%'; $maxDepth = 10000; for (;;) { if ($maxDepth-- <= 0) { break; } if (\array_key_exists($normalizedName, $env->store)) { if ($unreduced && isset($env->storeUnreduced[$normalizedName])) { return $env->storeUnreduced[$normalizedName]; } return $env->store[$normalizedName]; } if (! $hasNamespace && isset($env->marker)) { if (! empty($env->store[$specialContentKey])) { $env = $env->store[$specialContentKey]->scope; continue; } if (! empty($env->declarationScopeParent)) { $env = $env->declarationScopeParent; } else { $env = $this->rootEnv; } continue; } if (isset($env->parentStore)) { $env = $env->parentStore; } elseif (isset($env->parent)) { $env = $env->parent; } else { break; } } if ($shouldThrow) { throw $this->error("Undefined variable \$$name" . ($maxDepth <= 0 ? ' (infinite recursion)' : '')); } // found nothing return null; } /** * Has variable? * * @param string $name * @param \ScssPhp\ScssPhp\Compiler\Environment $env * * @return boolean */ protected function has($name, Environment $env = null) { return ! \is_null($this->get($name, false, $env)); } /** * Inject variables * * @param array $args * * @return void */ 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 * * @return void */ public function setVariables(array $variables) { $this->registeredVars = array_merge($this->registeredVars, $variables); } /** * Unset variable * * @api * * @param string $name * * @return void */ 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 * * @return void */ public function addParsedFile($path) { if (isset($path) && is_file($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|callable $path * * @return void */ public function addImportPath($path) { if (! \in_array($path, $this->importPaths)) { $this->importPaths[] = $path; } } /** * Set import paths * * @api * * @param string|array<string|callable> $path * * @return void */ public function setImportPaths($path) { $paths = (array) $path; $actualImportPaths = array_filter($paths, function ($path) { return $path !== ''; }); $this->legacyCwdImportPath = \count($actualImportPaths) !== \count($paths); if ($this->legacyCwdImportPath) { @trigger_error('Passing an empty string in the import paths to refer to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be used directly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compile()" instead.', E_USER_DEPRECATED); } $this->importPaths = $actualImportPaths; } /** * Set number precision * * @api * * @param integer $numberPrecision * * @return void * * @deprecated The number precision is not configurable anymore. The default is enough for all browsers. */ public function setNumberPrecision($numberPrecision) { @trigger_error('The number precision is not configurable anymore. ' . 'The default is enough for all browsers.', E_USER_DEPRECATED); } /** * Sets the output style. * * @api * * @param string $style One of the OutputStyle constants * * @return void * * @phpstan-param OutputStyle::* $style */ public function setOutputStyle($style) { switch ($style) { case OutputStyle::EXPANDED: $this->formatter = Expanded::class; break; case OutputStyle::COMPRESSED: $this->formatter = Compressed::class; break; default: throw new \InvalidArgumentException(sprintf('Invalid output style "%s".', $style)); } } /** * Set formatter * * @api * * @param string $formatterName * * @return void * * @deprecated Use {@see setOutputStyle} instead. */ public function setFormatter($formatterName) { if (!\in_array($formatterName, [Expanded::class, Compressed::class], true)) { @trigger_error('Formatters other than Expanded and Compressed are deprecated.', E_USER_DEPRECATED); } @trigger_error('The method "setFormatter" is deprecated. Use "setOutputStyle" instead.', E_USER_DEPRECATED); $this->formatter = $formatterName; } /** * Set line number style * * @api * * @param string $lineNumberStyle * * @return void * * @deprecated The line number output is not supported anymore. Use source maps instead. */ public function setLineNumberStyle($lineNumberStyle) { @trigger_error('The line number output is not supported anymore. ' . 'Use source maps instead.', E_USER_DEPRECATED); } /** * Enable/disable source maps * * @api * * @param integer $sourceMap * * @return void * * @phpstan-param self::SOURCE_MAP_* $sourceMap */ public function setSourceMap($sourceMap) { $this->sourceMap = $sourceMap; } /** * Set source map options * * @api * * @param array $sourceMapOptions * * @return void */ public function setSourceMapOptions($sourceMapOptions) { $this->sourceMapOptions = $sourceMapOptions; } /** * Register function * * @api * * @param string $name * @param callable $func * @param array|null $prototype * * @return void */ public function registerFunction($name, $func, $prototype = null) { $this->userFunctions[$this->normalizeName($name)] = [$func, $prototype]; } /** * Unregister function * * @api * * @param string $name * * @return void */ public function unregisterFunction($name) { unset($this->userFunctions[$this->normalizeName($name)]); } /** * Add feature * * @api * * @param string $name * * @return void * * @deprecated Registering additional features is deprecated. */ public function addFeature($name) { @trigger_error('Registering additional features is deprecated.', E_USER_DEPRECATED); $this->registeredFeatures[$name] = true; } /** * Import file * * @param string $path * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * * @return void */ protected function importFile($path, OutputBlock $out) { $this->pushCallStack('import ' . $this->getPrettyPath($path)); // 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; } $currentDirectory = $this->currentDirectory; $this->currentDirectory = dirname($path); $this->compileChildrenNoReturn($tree->children, $out); $this->currentDirectory = $currentDirectory; $this->popCallStack(); } /** * Return the file path for an import url if it exists * * @api * * @param string $url * * @return string|null */ public function findImport($url) { // for "normal" scss imports (ignore vanilla css and external requests) // Callback importers are still called for BC. if (preg_match('~\.css$|^https?://|^//~', $url)) { foreach ($this->importPaths as $dir) { if (\is_string($dir)) { continue; } if (\is_callable($dir)) { // check custom callback for import path $file = \call_user_func($dir, $url); if (! \is_null($file)) { return $file; } } } return null; } if (!\is_null($this->currentDirectory)) { $relativePath = $this->resolveImportPath($url, $this->currentDirectory); if (!\is_null($relativePath)) { return $relativePath; } } foreach ($this->importPaths as $dir) { if (\is_string($dir)) { $path = $this->resolveImportPath($url, $dir); if (!\is_null($path)) { return $path; } } elseif (\is_callable($dir)) { // check custom callback for import path $file = \call_user_func($dir, $url); if (! \is_null($file)) { return $file; } } } if ($this->legacyCwdImportPath) { $path = $this->resolveImportPath($url, getcwd()); if (!\is_null($path)) { @trigger_error('Resolving imports relatively to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be added as an import path explicitly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compile()" instead.', E_USER_DEPRECATED); return $path; } } throw $this->error("`$url` file not found for @import"); } /** * @param string $url * @param string $baseDir * * @return string|null */ private function resolveImportPath($url, $baseDir) { $path = Path::join($baseDir, $url); $hasExtension = preg_match('/.scss$/', $url); if ($hasExtension) { return $this->checkImportPathConflicts($this->tryImportPath($path)); } $result = $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path)); if (!\is_null($result)) { return $result; } return $this->tryImportPathAsDirectory($path); } /** * @param string[] $paths * * @return string|null */ private function checkImportPathConflicts(array $paths) { if (\count($paths) === 0) { return null; } if (\count($paths) === 1) { return $paths[0]; } $formattedPrettyPaths = []; foreach ($paths as $path) { $formattedPrettyPaths[] = ' ' . $this->getPrettyPath($path); } throw $this->error("It's not clear which file to import. Found:\n" . implode("\n", $formattedPrettyPaths)); } /** * @param string $path * * @return string[] */ private function tryImportPathWithExtensions($path) { $result = $this->tryImportPath($path.'.scss'); if ($result) { return $result; } return $this->tryImportPath($path.'.css'); } /** * @param string $path * * @return string[] */ private function tryImportPath($path) { $partial = dirname($path).'/_'.basename($path); $candidates = []; if (is_file($partial)) { $candidates[] = $partial; } if (is_file($path)) { $candidates[] = $path; } return $candidates; } /** * @param string $path * * @return string|null */ private function tryImportPathAsDirectory($path) { if (!is_dir($path)) { return null; } return $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path.'/index')); } /** * @param string $path * * @return string */ private function getPrettyPath($path) { $normalizedPath = $path; $normalizedRootDirectory = $this->rootDirectory.'/'; if (\DIRECTORY_SEPARATOR === '\\') { $normalizedRootDirectory = str_replace('\\', '/', $normalizedRootDirectory); $normalizedPath = str_replace('\\', '/', $path); } if (0 === strpos($normalizedPath, $normalizedRootDirectory)) { return substr($normalizedPath, \strlen($normalizedRootDirectory)); } return $path; } /** * Set encoding * * @api * * @param string $encoding * * @return void */ public function setEncoding($encoding) { $this->encoding = $encoding; } /** * Ignore errors? * * @api * * @param boolean $ignoreErrors * * @return \ScssPhp\ScssPhp\Compiler * * @deprecated Ignoring Sass errors is not longer supported. */ public function setIgnoreErrors($ignoreErrors) { @trigger_error('Ignoring Sass errors is not longer supported.', E_USER_DEPRECATED); return $this; } /** * Get source position * * @api * * @return array */ public function getSourcePosition() { $sourceFile = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : ''; return [$sourceFile, $this->sourceLine, $this->sourceColumn]; } /** * Throw error (exception) * * @api * * @param string $msg Message with optional sprintf()-style vararg parameters * * @throws \ScssPhp\ScssPhp\Exception\CompilerException * * @deprecated use "error" and throw the exception in the caller instead. */ public function throwError($msg) { @trigger_error( 'The method "throwError" is deprecated. Use "error" and throw the exception in the caller instead', E_USER_DEPRECATED ); throw $this->error(...func_get_args()); } /** * Build an error (exception) * * @api * * @param string $msg Message with optional sprintf()-style vararg parameters * * @return CompilerException */ public function error($msg, ...$args) { if ($args) { $msg = sprintf($msg, ...$args); } if (! $this->ignoreCallStackMessage) { $line = $this->sourceLine; $column = $this->sourceColumn; $loc = isset($this->sourceNames[$this->sourceIndex]) ? $this->getPrettyPath($this->sourceNames[$this->sourceIndex]) . " on line $line, at column $column" : "line: $line, column: $column"; $msg = "$msg: $loc"; $callStackMsg = $this->callStackMessage(); if ($callStackMsg) { $msg .= "\nCall Stack:\n" . $callStackMsg; } } return new CompilerException($msg); } /** * @param string $functionName * @param array $ExpectedArgs * @param int $nbActual * @return CompilerException */ public function errorArgsNumber($functionName, $ExpectedArgs, $nbActual) { $nbExpected = \count($ExpectedArgs); if ($nbActual > $nbExpected) { return $this->error( 'Error: Only %d arguments allowed in %s(), but %d were passed.', $nbExpected, $functionName, $nbActual ); } else { $missing = []; while (count($ExpectedArgs) && count($ExpectedArgs) > $nbActual) { array_unshift($missing, array_pop($ExpectedArgs)); } return $this->error( 'Error: %s() argument%s %s missing.', $functionName, count($missing) > 1 ? 's' : '', implode(', ', $missing) ); } } /** * Beautify call stack for output * * @param boolean $all * @param null $limit * * @return string */ protected function callStackMessage($all = false, $limit = null) { $callStackMsg = []; $ncall = 0; if ($this->callStack) { foreach (array_reverse($this->callStack) as $call) { if ($all || (isset($call['n']) && $call['n'])) { $msg = '#' . $ncall++ . ' ' . $call['n'] . ' '; $msg .= (isset($this->sourceNames[$call[Parser::SOURCE_INDEX]]) ? $this->getPrettyPath($this->sourceNames[$call[Parser::SOURCE_INDEX]]) : '(unknown file)'); $msg .= ' on line ' . $call[Parser::SOURCE_LINE]; $callStackMsg[] = $msg; if (! \is_null($limit) && $ncall > $limit) { break; } } } } return implode("\n", $callStackMsg); } /** * Handle import loop * * @param string $name * * @throws \Exception */ protected function handleImportLoop($name) { for ($env = $this->env; $env; $env = $env->parent) { if (! $env->block) { continue; } $file = $this->sourceNames[$env->block->sourceIndex]; if (realpath($file) === $name) { throw $this->error('An @import loop has been found: %s imports %s', $file, basename($file)); } } } /** * Call SCSS @function * * @param Object $func * @param array $argValues * * @return array */ protected function callScssFunction($func, $argValues) { if (! $func) { return static::$defaultValue; } $name = $func->name; $this->pushEnv(); // set the args if (isset($func->args)) { $this->applyArguments($func->args, $argValues); } // throw away lines and children $tmp = new OutputBlock(); $tmp->lines = []; $tmp->children = []; $this->env->marker = 'function'; if (! empty($func->parentEnv)) { $this->env->declarationScopeParent = $func->parentEnv; } else { throw $this->error("@function $name() without parentEnv"); } $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . ' ' . $name); $this->popEnv(); return ! isset($ret) ? static::$defaultValue : $ret; } /** * Call built-in and registered (PHP) functions * * @param string $name * @param string|array $function * @param array $prototype * @param array $args * * @return array|Number|null */ protected function callNativeFunction($name, $function, $prototype, $args) { $libName = (is_array($function) ? end($function) : null); $sorted_kwargs = $this->sortNativeFunctionArgs($libName, $prototype, $args); if (\is_null($sorted_kwargs)) { return null; } @list($sorted, $kwargs) = $sorted_kwargs; if ($name !== 'if') { $inExp = true; if ($name === 'join') { $inExp = false; } foreach ($sorted as &$val) { $val = $this->reduce($val, $inExp); } } $returnValue = \call_user_func($function, $sorted, $kwargs); if (! isset($returnValue)) { return null; } return $this->coerceValue($returnValue); } /** * Get built-in function * * @param string $name Normalized name * * @return array */ protected function getBuiltinFunction($name) { $libName = self::normalizeNativeFunctionName($name); return [$this, $libName]; } /** * Normalize native function name * @param string $name * @return string */ public static function normalizeNativeFunctionName($name) { $name = str_replace("-", "_", $name); $libName = 'lib' . preg_replace_callback( '/_(.)/', function ($m) { return ucfirst($m[1]); }, ucfirst($name) ); return $libName; } /** * Check if a function is a native built-in scss function, for css parsing * @param string $name * @return bool */ public static function isNativeFunction($name) { return method_exists(Compiler::class, self::normalizeNativeFunctionName($name)); } /** * Sorts keyword arguments * * @param string $functionName * @param array $prototypes * @param array $args * * @return array|null */ protected function sortNativeFunctionArgs($functionName, $prototypes, $args) { static $parser = null; if (! isset($prototypes)) { $keyArgs = []; $posArgs = []; if (\is_array($args) && \count($args) && \end($args) === static::$null) { array_pop($args); } // separate positional and keyword arguments foreach ($args as $arg) { list($key, $value) = $arg; if (empty($key) or empty($key[1])) { $posArgs[] = empty($arg[2]) ? $value : $arg; } else { $keyArgs[$key[1]] = $value; } } return [$posArgs, $keyArgs]; } // specific cases ? if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) { // notation 100 127 255 / 0 is in fact a simple list of 4 values foreach ($args as $k => $arg) { if ($arg[1][0] === Type::T_LIST && \count($arg[1][2]) === 3) { $last = end($arg[1][2]); if ($last[0] === Type::T_EXPRESSION && $last[1] === '/') { array_pop($arg[1][2]); $arg[1][2][] = $last[2]; $arg[1][2][] = $last[3]; $args[$k] = $arg; } } } } $finalArgs = []; if (! \is_array(reset($prototypes))) { $prototypes = [$prototypes]; } $keyArgs = []; // trying each prototypes $prototypeHasMatch = false; $exceptionMessage = ''; foreach ($prototypes as $prototype) { $argDef = []; foreach ($prototype as $i => $p) { $default = null; $p = explode(':', $p, 2); $name = array_shift($p); if (\count($p)) { $p = trim(reset($p)); if ($p === 'null') { // differentiate this null from the static::$null $default = [Type::T_KEYWORD, 'null']; } else { if (\is_null($parser)) { $parser = $this->parserFactory(__METHOD__); } $parser->parseValue($p, $default); } } $isVariable = false; if (substr($name, -3) === '...') { $isVariable = true; $name = substr($name, 0, -3); } $argDef[] = [$name, $default, $isVariable]; } $ignoreCallStackMessage = $this->ignoreCallStackMessage; $this->ignoreCallStackMessage = true; try { if (\count($args) > \count($argDef)) { $lastDef = end($argDef); // check that last arg is not a ... if (empty($lastDef[2])) { throw $this->errorArgsNumber($functionName, $argDef, \count($args)); } } $vars = $this->applyArguments($argDef, $args, false, false); // ensure all args are populated foreach ($prototype as $i => $p) { $name = explode(':', $p)[0]; if (! isset($finalArgs[$i])) { $finalArgs[$i] = null; } } // apply positional args foreach (array_values($vars) as $i => $val) { $finalArgs[$i] = $val; } $keyArgs = array_merge($keyArgs, $vars); $prototypeHasMatch = true; // overwrite positional args with keyword args foreach ($prototype as $i => $p) { $name = explode(':', $p)[0]; if (isset($keyArgs[$name])) { $finalArgs[$i] = $keyArgs[$name]; } // special null value as default: translate to real null here if ($finalArgs[$i] === [Type::T_KEYWORD, 'null']) { $finalArgs[$i] = null; } } // should we break if this prototype seems fulfilled? } catch (CompilerException $e) { $exceptionMessage = $e->getMessage(); } $this->ignoreCallStackMessage = $ignoreCallStackMessage; } if ($exceptionMessage && ! $prototypeHasMatch) { if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) { // if var() or calc() is used as an argument, return as a css function foreach ($args as $arg) { if ($arg[1][0] === Type::T_FUNCTION_CALL && in_array($arg[1][1], ['var'])) { return null; } } } throw $this->error($exceptionMessage); } return [$finalArgs, $keyArgs]; } /** * Apply argument values per definition * * @param array $argDef * @param array $argValues * @param boolean $storeInEnv * @param boolean $reduce * only used if $storeInEnv = false * * @return array * * @throws \Exception */ protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true) { $output = []; if (\is_array($argValues) && \count($argValues) && end($argValues) === static::$null) { array_pop($argValues); } if ($storeInEnv) { $storeEnv = $this->getStoreEnv(); $env = new Environment(); $env->store = $storeEnv->store; } $hasVariable = false; $args = []; foreach ($argDef as $i => $arg) { list($name, $default, $isVariable) = $argDef[$i]; $args[$name] = [$i, $name, $default, $isVariable]; $hasVariable |= $isVariable; } $splatSeparator = null; $keywordArgs = []; $deferredKeywordArgs = []; $deferredNamedKeywordArgs = []; $remaining = []; $hasKeywordArgument = false; // assign the keyword args foreach ((array) $argValues as $arg) { if (! empty($arg[0])) { $hasKeywordArgument = true; $name = $arg[0][1]; if (! isset($args[$name])) { foreach (array_keys($args) as $an) { if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) { $name = $an; break; } } } if (! isset($args[$name]) || $args[$name][3]) { if ($hasVariable) { $deferredNamedKeywordArgs[$name] = $arg[1]; } else { throw $this->error("Mixin or function doesn't have an argument named $%s.", $arg[0][1]); } } elseif ($args[$name][0] < \count($remaining)) { throw $this->error("The argument $%s was passed both by position and by name.", $arg[0][1]); } else { $keywordArgs[$name] = $arg[1]; } } elseif (! empty($arg[2])) { // $arg[2] means a var followed by ... in the arg ($list... ) $val = $this->reduce($arg[1], true); if ($val[0] === Type::T_LIST) { foreach ($val[2] as $name => $item) { if (! is_numeric($name)) { if (! isset($args[$name])) { foreach (array_keys($args) as $an) { if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) { $name = $an; break; } } } if ($hasVariable) { $deferredKeywordArgs[$name] = $item; } else { $keywordArgs[$name] = $item; } } else { if (\is_null($splatSeparator)) { $splatSeparator = $val[1]; } $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)) { if (! isset($args[$name])) { foreach (array_keys($args) as $an) { if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) { $name = $an; break; } } } if ($hasVariable) { $deferredKeywordArgs[$name] = $item; } else { $keywordArgs[$name] = $item; } } else { if (\is_null($splatSeparator)) { $splatSeparator = $val[1]; } $remaining[] = $item; } } } else { $remaining[] = $val; } } elseif ($hasKeywordArgument) { throw $this->error('Positional arguments must come before keyword arguments.'); } else { $remaining[] = $arg[1]; } } foreach ($args as $arg) { list($i, $name, $default, $isVariable) = $arg; if ($isVariable) { // only if more than one arg : can not be passed as position and value // see https://github.com/sass/libsass/issues/2927 if (count($args) > 1) { if (isset($remaining[$i]) && isset($deferredNamedKeywordArgs[$name])) { throw $this->error("The argument $%s was passed both by position and by name.", $name); } } $val = [Type::T_LIST, \is_null($splatSeparator) ? ',' : $splatSeparator , [], $isVariable]; for ($count = \count($remaining); $i < $count; $i++) { $val[2][] = $remaining[$i]; } foreach ($deferredKeywordArgs as $itemName => $item) { $val[2][$itemName] = $item; } foreach ($deferredNamedKeywordArgs 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 { throw $this->error("Missing argument $name"); } if ($storeInEnv) { $this->set($name, $this->reduce($val, true), true, $env); } else { $output[$name] = ($reduce ? $this->reduce($val, true) : $val); } } if ($storeInEnv) { $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; } if ($storeInEnv) { $this->set($name, $this->reduce($default, true), true); } else { $output[$name] = ($reduce ? $this->reduce($default, true) : $default); } } return $output; } /** * Coerce a php value into a scss one * * @param mixed $value * * @return array|Number */ protected function coerceValue($value) { if (\is_array($value) || $value instanceof \ArrayAccess) { return $value; } if (\is_bool($value)) { return $this->toBool($value); } if (\is_null($value)) { return static::$null; } if (is_numeric($value)) { return new Number($value, ''); } if ($value === '') { return static::$emptyString; } $value = [Type::T_KEYWORD, $value]; $color = $this->coerceColor($value); if ($color) { return $color; } return $value; } /** * Coerce something to map * * @param array|Number $item * * @return array|Number */ protected function coerceMap($item) { if ($item[0] === Type::T_MAP) { return $item; } if ( $item[0] === static::$emptyList[0] && $item[1] === static::$emptyList[1] && $item[2] === static::$emptyList[2] ) { return static::$emptyMap; } return $item; } /** * Coerce something to list * * @param array $item * @param string $delim * @param boolean $removeTrailingNull * * @return array */ protected function coerceList($item, $delim = ',', $removeTrailingNull = false) { if (isset($item) && $item[0] === Type::T_LIST) { // remove trailing null from the list if ($removeTrailingNull && end($item[2]) === static::$null) { array_pop($item[2]); } return $item; } if (isset($item) && $item[0] === Type::T_MAP) { $keys = $item[1]; $values = $item[2]; $list = []; for ($i = 0, $s = \count($keys); $i < $s; $i++) { $key = $keys[$i]; $value = $values[$i]; switch ($key[0]) { case Type::T_LIST: case Type::T_MAP: case Type::T_STRING: case Type::T_NULL: break; default: $key = [Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))]; break; } $list[] = [ Type::T_LIST, '', [$key, $value] ]; } return [Type::T_LIST, ',', $list]; } return [Type::T_LIST, $delim, ! isset($item) ? [] : [$item]]; } /** * Coerce color for expression * * @param array|Number $value * * @return array|Number */ protected function coerceForExpression($value) { if ($color = $this->coerceColor($value)) { return $color; } return $value; } /** * Coerce value to color * * @param array|Number $value * @param bool $inRGBFunction * * @return array|null */ protected function coerceColor($value, $inRGBFunction = false) { switch ($value[0]) { case Type::T_COLOR: for ($i = 1; $i <= 3; $i++) { if (! is_numeric($value[$i])) { $cv = $this->compileRGBAValue($value[$i]); if (! is_numeric($cv)) { return null; } $value[$i] = $cv; } if (isset($value[4])) { if (! is_numeric($value[4])) { $cv = $this->compileRGBAValue($value[4], true); if (! is_numeric($cv)) { return null; } $value[4] = $cv; } } } return $value; case Type::T_LIST: if ($inRGBFunction) { if (\count($value[2]) == 3 || \count($value[2]) == 4) { $color = $value[2]; array_unshift($color, Type::T_COLOR); return $this->coerceColor($color); } } return null; case Type::T_KEYWORD: if (! \is_string($value[1])) { return null; } $name = strtolower($value[1]); // hexa color? if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) { $nofValues = \strlen($m[1]); if (\in_array($nofValues, [3, 4, 6, 8])) { $nbChannels = 3; $color = []; $num = hexdec($m[1]); switch ($nofValues) { case 4: $nbChannels = 4; // then continuing with the case 3: case 3: for ($i = 0; $i < $nbChannels; $i++) { $t = $num & 0xf; array_unshift($color, $t << 4 | $t); $num >>= 4; } break; case 8: $nbChannels = 4; // then continuing with the case 6: case 6: for ($i = 0; $i < $nbChannels; $i++) { array_unshift($color, $num & 0xff); $num >>= 8; } break; } if ($nbChannels === 4) { if ($color[3] === 255) { $color[3] = 1; // fully opaque } else { $color[3] = round($color[3] / 255, Number::PRECISION); } } array_unshift($color, Type::T_COLOR); return $color; } } if ($rgba = Colors::colorNameToRGBa($name)) { return isset($rgba[3]) ? [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2], $rgba[3]] : [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2]]; } return null; } return null; } /** * @param integer|Number $value * @param boolean $isAlpha * * @return integer|mixed */ protected function compileRGBAValue($value, $isAlpha = false) { if ($isAlpha) { return $this->compileColorPartValue($value, 0, 1, false); } return $this->compileColorPartValue($value, 0, 255, true); } /** * @param mixed $value * @param integer|float $min * @param integer|float $max * @param boolean $isInt * * @return integer|mixed */ protected function compileColorPartValue($value, $min, $max, $isInt = true) { if (! is_numeric($value)) { if (\is_array($value)) { $reduced = $this->reduce($value); if ($reduced instanceof Number) { $value = $reduced; } } if ($value instanceof Number) { if ($value->unitless()) { $num = $value->getDimension(); } elseif ($value->hasUnit('%')) { $num = $max * $value->getDimension() / 100; } else { throw $this->error('Expected %s to have no units or "%%".', $value); } $value = $num; } elseif (\is_array($value)) { $value = $this->compileValue($value); } } if (is_numeric($value)) { if ($isInt) { $value = round($value); } $value = min($max, max($min, $value)); return $value; } return $value; } /** * Coerce value to string * * @param array|Number $value * * @return array */ protected function coerceString($value) { if ($value[0] === Type::T_STRING) { return $value; } return [Type::T_STRING, '', [$this->compileValue($value)]]; } /** * Assert value is a string (or keyword) * * @api * * @param array|Number $value * @param string $varName * * @return array * * @throws \Exception */ public function assertString($value, $varName = null) { // case of url(...) parsed a a function if ($value[0] === Type::T_FUNCTION) { $value = $this->coerceString($value); } if (! \in_array($value[0], [Type::T_STRING, Type::T_KEYWORD])) { $value = $this->compileValue($value); $var_display = ($varName ? " \${$varName}:" : ''); throw $this->error("Error:{$var_display} $value is not a string."); } $value = $this->coerceString($value); return $value; } /** * Coerce value to a percentage * * @param array|Number $value * * @return integer|float */ protected function coercePercent($value) { if ($value instanceof Number) { if ($value->hasUnit('%')) { return $value->getDimension() / 100; } return $value->getDimension(); } return 0; } /** * Assert value is a map * * @api * * @param array|Number $value * * @return array * * @throws \Exception */ public function assertMap($value) { $value = $this->coerceMap($value); if ($value[0] !== Type::T_MAP) { throw $this->error('expecting map, %s received', $value[0]); } return $value; } /** * Assert value is a list * * @api * * @param array|Number $value * * @return array * * @throws \Exception */ public function assertList($value) { if ($value[0] !== Type::T_LIST) { throw $this->error('expecting list, %s received', $value[0]); } return $value; } /** * Assert value is a color * * @api * * @param array|Number $value * * @return array * * @throws \Exception */ public function assertColor($value) { if ($color = $this->coerceColor($value)) { return $color; } throw $this->error('expecting color, %s received', $value[0]); } /** * Assert value is a number * * @api * * @param array|Number $value * @param string $varName * * @return Number * * @throws \Exception */ public function assertNumber($value, $varName = null) { if (!$value instanceof Number) { $value = $this->compileValue($value); $var_display = ($varName ? " \${$varName}:" : ''); throw $this->error("Error:{$var_display} $value is not a number."); } return $value; } /** * Assert value is a integer * * @api * * @param array|Number $value * @param string $varName * * @return integer * * @throws \Exception */ public function assertInteger($value, $varName = null) { $value = $this->assertNumber($value, $varName)->getDimension(); if (round($value - \intval($value), Number::PRECISION) > 0) { $var_display = ($varName ? " \${$varName}:" : ''); throw $this->error("Error:{$var_display} $value is not an integer."); } return intval($value); } /** * Make sure a color's components don't go out of bounds * * @param array $c * * @return array */ protected function fixColor($c) { foreach ([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 [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 */ protected 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 = [Type::T_COLOR, $r, $g, $b]; return $out; } // Built in functions protected static $libCall = ['function', 'args...']; protected function libCall($args, $kwargs) { $functionReference = array_shift($args); if (in_array($functionReference[0], [Type::T_STRING, Type::T_KEYWORD])) { $name = $this->compileStringContent($this->coerceString($functionReference)); $warning = "DEPRECATION WARNING: Passing a string to call() is deprecated and will be illegal\n" . "in Sass 4.0. Use call(function-reference($name)) instead."; fwrite($this->stderr, "$warning\n\n"); $functionReference = $this->libGetFunction([$functionReference]); } if ($functionReference === static::$null) { return static::$null; } if (! in_array($functionReference[0], [Type::T_FUNCTION_REFERENCE, Type::T_FUNCTION])) { throw $this->error('Function reference expected, got ' . $functionReference[0]); } $callArgs = []; // $kwargs['args'] is [Type::T_LIST, ',', [..]] foreach ($kwargs['args'][2] as $varname => $arg) { if (is_numeric($varname)) { $varname = null; } else { $varname = [ 'var', $varname]; } $callArgs[] = [$varname, $arg, false]; } return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]); } protected static $libGetFunction = [ ['name'], ['name', 'css'] ]; protected function libGetFunction($args) { $name = $this->compileStringContent($this->coerceString(array_shift($args))); $isCss = false; if (count($args)) { $isCss = array_shift($args); $isCss = (($isCss === static::$true) ? true : false); } if ($isCss) { return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]]; } return $this->getFunctionReference($name, true); } protected static $libIf = ['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 = ['list', 'value']; protected function libIndex($args) { list($list, $value) = $args; 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 static::$null; } // Numbers are represented with value objects, for which the PHP equality operator does not // match the Sass rules (and we cannot overload it). As they are the only type of values // represented with a value object for now, they require a special case. if ($value instanceof Number) { $key = 0; foreach ($list[2] as $item) { $key++; $itemValue = $this->normalizeValue($item); if ($itemValue instanceof Number && $value->equals($itemValue)) { return new Number($key, ''); } } return static::$null; } $values = []; foreach ($list[2] as $item) { $values[] = $this->normalizeValue($item); } $key = array_search($this->normalizeValue($value), $values); return false === $key ? static::$null : $key + 1; } protected static $libRgb = [ ['color'], ['color', 'alpha'], ['channels'], ['red', 'green', 'blue'], ['red', 'green', 'blue', 'alpha'] ]; protected function libRgb($args, $kwargs, $funcName = 'rgb') { switch (\count($args)) { case 1: if (! $color = $this->coerceColor($args[0], true)) { $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']]; } break; case 3: $color = [Type::T_COLOR, $args[0], $args[1], $args[2]]; if (! $color = $this->coerceColor($color)) { $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']]; } return $color; case 2: if ($color = $this->coerceColor($args[0], true)) { $alpha = $this->compileRGBAValue($args[1], true); if (is_numeric($alpha)) { $color[4] = $alpha; } else { $color = [Type::T_STRING, '', [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']]; } } else { $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']]; } break; case 4: default: $color = [Type::T_COLOR, $args[0], $args[1], $args[2], $args[3]]; if (! $color = $this->coerceColor($color)) { $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']]; } break; } return $color; } protected static $libRgba = [ ['color'], ['color', 'alpha'], ['channels'], ['red', 'green', 'blue'], ['red', 'green', 'blue', 'alpha'] ]; protected function libRgba($args, $kwargs) { return $this->libRgb($args, $kwargs, 'rgba'); } /** * Helper function for adjust_color, change_color, and scale_color * * @param array<array|Number> $args * @param callable $fn * * @return array */ protected function alterColor($args, $fn) { $color = $this->assertColor($args[0]); foreach ([1 => 1, 2 => 2, 3 => 3, 7 => 4] as $iarg => $irgba) { if (isset($args[$iarg])) { $val = $this->assertNumber($args[$iarg])->getDimension(); if (! isset($color[$irgba])) { $color[$irgba] = (($irgba < 4) ? 0 : 1); } $color[$irgba] = \call_user_func($fn, $color[$irgba], $val, $iarg); } } if (! empty($args[4]) || ! empty($args[5]) || ! empty($args[6])) { $hsl = $this->toHSL($color[1], $color[2], $color[3]); foreach ([4 => 1, 5 => 2, 6 => 3] as $iarg => $ihsl) { if (! empty($args[$iarg])) { $val = $this->assertNumber($args[$iarg])->getDimension(); $hsl[$ihsl] = \call_user_func($fn, $hsl[$ihsl], $val, $iarg); } } $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]); if (isset($color[4])) { $rgb[4] = $color[4]; } $color = $rgb; } return $color; } protected static $libAdjustColor = [ 'color', 'red:null', 'green:null', 'blue:null', 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null' ]; protected function libAdjustColor($args) { return $this->alterColor($args, function ($base, $alter, $i) { return $base + $alter; }); } protected static $libChangeColor = [ 'color', 'red:null', 'green:null', 'blue:null', 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null' ]; protected function libChangeColor($args) { return $this->alterColor($args, function ($base, $alter, $i) { return $alter; }); } protected static $libScaleColor = [ 'color', 'red:null', 'green:null', 'blue:null', 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null' ]; 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 = ['color']; protected function libIeHexStr($args) { $color = $this->coerceColor($args[0]); if (\is_null($color)) { throw $this->error('Error: argument `$color` of `ie-hex-str($color)` must be a color'); } $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255; return [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]]; } protected static $libRed = ['color']; protected function libRed($args) { $color = $this->coerceColor($args[0]); if (\is_null($color)) { throw $this->error('Error: argument `$color` of `red($color)` must be a color'); } return $color[1]; } protected static $libGreen = ['color']; protected function libGreen($args) { $color = $this->coerceColor($args[0]); if (\is_null($color)) { throw $this->error('Error: argument `$color` of `green($color)` must be a color'); } return $color[2]; } protected static $libBlue = ['color']; protected function libBlue($args) { $color = $this->coerceColor($args[0]); if (\is_null($color)) { throw $this->error('Error: argument `$color` of `blue($color)` must be a color'); } return $color[3]; } protected static $libAlpha = ['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 = ['color']; protected function libOpacity($args) { $value = $args[0]; if ($value instanceof Number) { return null; } return $this->libAlpha($args); } // mix two colors protected static $libMix = [ ['color1', 'color2', 'weight:0.5'], ['color-1', 'color-2', 'weight:0.5'] ]; 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 = [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 * (1 - $weight); } return $this->fixColor($new); } protected static $libHsl = [ ['channels'], ['hue', 'saturation', 'lightness'], ['hue', 'saturation', 'lightness', 'alpha'] ]; protected function libHsl($args, $kwargs, $funcName = 'hsl') { $args_to_check = $args; if (\count($args) == 1) { if ($args[0][0] !== Type::T_LIST || \count($args[0][2]) < 3 || \count($args[0][2]) > 4) { return [Type::T_STRING, '', [$funcName . '(', $args[0], ')']]; } $args = $args[0][2]; $args_to_check = $kwargs['channels'][2]; } foreach ($kwargs as $k => $arg) { if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) { return null; } } foreach ($args_to_check as $k => $arg) { if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) { if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) { return null; } $args[$k] = $this->stringifyFncallArgs($arg); } if ( $k >= 2 && count($args) === 4 && in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['calc','env']) ) { return null; } } $hue = $this->reduce($args[0]); $saturation = $this->reduce($args[1]); $lightness = $this->reduce($args[2]); $alpha = null; if (\count($args) === 4) { $alpha = $this->compileColorPartValue($args[3], 0, 100, false); if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number || ! is_numeric($alpha)) { return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']]; } } else { if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number) { return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']]; } } $hueValue = $hue->getDimension() % 360; while ($hueValue < 0) { $hueValue += 360; } $color = $this->toRGB($hueValue, max(0, min($saturation->getDimension(), 100)), max(0, min($lightness->getDimension(), 100))); if (! \is_null($alpha)) { $color[4] = $alpha; } return $color; } protected static $libHsla = [ ['channels'], ['hue', 'saturation', 'lightness'], ['hue', 'saturation', 'lightness', 'alpha']]; protected function libHsla($args, $kwargs) { return $this->libHsl($args, $kwargs, 'hsla'); } protected static $libHue = ['color']; protected function libHue($args) { $color = $this->assertColor($args[0]); $hsl = $this->toHSL($color[1], $color[2], $color[3]); return new Number($hsl[1], 'deg'); } protected static $libSaturation = ['color']; protected function libSaturation($args) { $color = $this->assertColor($args[0]); $hsl = $this->toHSL($color[1], $color[2], $color[3]); return new Number($hsl[2], '%'); } protected static $libLightness = ['color']; protected function libLightness($args) { $color = $this->assertColor($args[0]); $hsl = $this->toHSL($color[1], $color[2], $color[3]); return new 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 = ['color', 'degrees']; protected function libAdjustHue($args) { $color = $this->assertColor($args[0]); $degrees = $this->assertNumber($args[1])->getDimension(); return $this->adjustHsl($color, 1, $degrees); } protected static $libLighten = ['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 = ['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 = [['color', 'amount'], ['amount']]; protected function libSaturate($args) { $value = $args[0]; if ($value instanceof Number) { return null; } if (count($args) === 1) { $val = $this->compileValue($value); throw $this->error("\$amount: $val is not a number"); } $color = $this->assertColor($value); $amount = 100 * $this->coercePercent($args[1]); return $this->adjustHsl($color, 2, $amount); } protected static $libDesaturate = ['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 = ['color']; protected function libGrayscale($args) { $value = $args[0]; if ($value instanceof Number) { return null; } return $this->adjustHsl($this->assertColor($value), 2, -100); } protected static $libComplement = ['color']; protected function libComplement($args) { return $this->adjustHsl($this->assertColor($args[0]), 1, 180); } protected static $libInvert = ['color', 'weight:1']; protected function libInvert($args) { list($value, $weight) = $args; if (! isset($weight)) { $weight = 1; } else { $weight = $this->coercePercent($weight); } if ($value instanceof Number) { return null; } $color = $this->assertColor($value); $inverted = $color; $inverted[1] = 255 - $inverted[1]; $inverted[2] = 255 - $inverted[2]; $inverted[3] = 255 - $inverted[3]; if ($weight < 1) { return $this->libMix([$inverted, $color, new Number($weight, '')]); } return $inverted; } // increases opacity by amount protected static $libOpacify = ['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 = ['color', 'amount']; protected function libFadeIn($args) { return $this->libOpacify($args); } // decreases opacity by amount protected static $libTransparentize = ['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 = ['color', 'amount']; protected function libFadeOut($args) { return $this->libTransparentize($args); } protected static $libUnquote = ['string']; protected function libUnquote($args) { $str = $args[0]; if ($str[0] === Type::T_STRING) { $str[1] = ''; } return $str; } protected static $libQuote = ['string']; protected function libQuote($args) { $value = $args[0]; if ($value[0] === Type::T_STRING && ! empty($value[1])) { $value[1] = '"'; return $value; } return [Type::T_STRING, '"', [$value]]; } protected static $libPercentage = ['number']; protected function libPercentage($args) { $num = $this->assertNumber($args[0], 'number'); $num->assertNoUnits('number'); return new Number($num->getDimension() * 100, '%'); } protected static $libRound = ['number']; protected function libRound($args) { $num = $this->assertNumber($args[0], 'number'); return new Number(round($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); } protected static $libFloor = ['number']; protected function libFloor($args) { $num = $this->assertNumber($args[0], 'number'); return new Number(floor($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); } protected static $libCeil = ['number']; protected function libCeil($args) { $num = $this->assertNumber($args[0], 'number'); return new Number(ceil($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); } protected static $libAbs = ['number']; protected function libAbs($args) { $num = $this->assertNumber($args[0], 'number'); return new Number(abs($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); } protected function libMin($args) { /** * @var Number|null */ $min = null; foreach ($args as $arg) { $number = $this->assertNumber($arg); if (\is_null($min) || $min->greaterThan($number)) { $min = $number; } } if (!\is_null($min)) { return $min; } throw $this->error('At least one argument must be passed.'); } protected function libMax($args) { /** * @var Number|null */ $max = null; foreach ($args as $arg) { $number = $this->assertNumber($arg); if (\is_null($max) || $max->lessThan($number)) { $max = $number; } } if (!\is_null($max)) { return $max; } throw $this->error('At least one argument must be passed.'); } protected static $libLength = ['list']; protected function libLength($args) { $list = $this->coerceList($args[0], ',', true); return \count($list[2]); } //protected static $libListSeparator = ['list...']; protected function libListSeparator($args) { if (\count($args) > 1) { return 'comma'; } if (! \in_array($args[0][0], [Type::T_LIST, Type::T_MAP])) { return 'space'; } $list = $this->coerceList($args[0]); if (\count($list[2]) <= 1 && empty($list['enclosing'])) { return 'space'; } if ($list[1] === ',') { return 'comma'; } return 'space'; } protected static $libNth = ['list', 'n']; protected function libNth($args) { $list = $this->coerceList($args[0], ',', false); $n = $this->assertNumber($args[1])->getDimension(); if ($n > 0) { $n--; } elseif ($n < 0) { $n += \count($list[2]); } return isset($list[2][$n]) ? $list[2][$n] : static::$defaultValue; } protected static $libSetNth = ['list', 'n', 'value']; protected function libSetNth($args) { $list = $this->coerceList($args[0]); $n = $this->assertNumber($args[1])->getDimension(); if ($n > 0) { $n--; } elseif ($n < 0) { $n += \count($list[2]); } if (! isset($list[2][$n])) { throw $this->error('Invalid argument for "n"'); } $list[2][$n] = $args[2]; return $list; } protected static $libMapGet = ['map', 'key']; protected function libMapGet($args) { $map = $this->assertMap($args[0]); $key = $args[1]; if (! \is_null($key)) { $key = $this->compileStringContent($this->coerceString($key)); for ($i = \count($map[1]) - 1; $i >= 0; $i--) { if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { return $map[2][$i]; } } } return static::$null; } protected static $libMapKeys = ['map']; protected function libMapKeys($args) { $map = $this->assertMap($args[0]); $keys = $map[1]; return [Type::T_LIST, ',', $keys]; } protected static $libMapValues = ['map']; protected function libMapValues($args) { $map = $this->assertMap($args[0]); $values = $map[2]; return [Type::T_LIST, ',', $values]; } protected static $libMapRemove = ['map', 'key...']; protected function libMapRemove($args) { $map = $this->assertMap($args[0]); $keyList = $this->assertList($args[1]); $keys = []; foreach ($keyList[2] as $key) { $keys[] = $this->compileStringContent($this->coerceString($key)); } for ($i = \count($map[1]) - 1; $i >= 0; $i--) { if (in_array($this->compileStringContent($this->coerceString($map[1][$i])), $keys)) { array_splice($map[1], $i, 1); array_splice($map[2], $i, 1); } } return $map; } protected static $libMapHasKey = ['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 = [ ['map1', 'map2'], ['map-1', 'map-2'] ]; protected function libMapMerge($args) { $map1 = $this->assertMap($args[0]); $map2 = $this->assertMap($args[1]); foreach ($map2[1] as $i2 => $key2) { $key = $this->compileStringContent($this->coerceString($key2)); foreach ($map1[1] as $i1 => $key1) { if ($key === $this->compileStringContent($this->coerceString($key1))) { $map1[2][$i1] = $map2[2][$i2]; continue 2; } } $map1[1][] = $map2[1][$i2]; $map1[2][] = $map2[2][$i2]; } return $map1; } protected static $libKeywords = ['args']; protected function libKeywords($args) { $this->assertList($args[0]); $keys = []; $values = []; foreach ($args[0][2] as $name => $arg) { $keys[] = [Type::T_KEYWORD, $name]; $values[] = $arg; } return [Type::T_MAP, $keys, $values]; } protected static $libIsBracketed = ['list']; protected function libIsBracketed($args) { $list = $args[0]; $this->coerceList($list, ' '); if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') { return true; } return false; } /** * @param array $list1 * @param array|Number|null $sep * * @return string * @throws CompilerException */ 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 = ['list1', 'list2', 'separator:null', 'bracketed:auto']; protected function libJoin($args) { list($list1, $list2, $sep, $bracketed) = $args; $list1 = $this->coerceList($list1, ' ', true); $list2 = $this->coerceList($list2, ' ', true); $sep = $this->listSeparatorForJoin($list1, $sep); if ($bracketed === static::$true) { $bracketed = true; } elseif ($bracketed === static::$false) { $bracketed = false; } elseif ($bracketed === [Type::T_KEYWORD, 'auto']) { $bracketed = 'auto'; } elseif ($bracketed === static::$null) { $bracketed = false; } else { $bracketed = $this->compileValue($bracketed); $bracketed = ! ! $bracketed; if ($bracketed === true) { $bracketed = true; } } if ($bracketed === 'auto') { $bracketed = false; if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') { $bracketed = true; } } $res = [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])]; if (isset($list1['enclosing'])) { $res['enlcosing'] = $list1['enclosing']; } if ($bracketed) { $res['enclosing'] = 'bracket'; } return $res; } protected static $libAppend = ['list', 'val', 'separator:null']; protected function libAppend($args) { list($list1, $value, $sep) = $args; $list1 = $this->coerceList($list1, ' ', true); $sep = $this->listSeparatorForJoin($list1, $sep); $res = [Type::T_LIST, $sep, array_merge($list1[2], [$value])]; if (isset($list1['enclosing'])) { $res['enclosing'] = $list1['enclosing']; } return $res; } protected function libZip($args) { foreach ($args as $key => $arg) { $args[$key] = $this->coerceList($arg); } $lists = []; $firstList = array_shift($args); $result = [Type::T_LIST, ',', $lists]; if (! \is_null($firstList)) { foreach ($firstList[2] as $key => $item) { $list = [Type::T_LIST, '', [$item]]; foreach ($args as $arg) { if (isset($arg[2][$key])) { $list[2][] = $arg[2][$key]; } else { break 2; } } $lists[] = $list; } $result[2] = $lists; } else { $result['enclosing'] = 'parent'; } return $result; } protected static $libTypeOf = ['value']; protected function libTypeOf($args) { $value = $args[0]; switch ($value[0]) { case Type::T_KEYWORD: if ($value === static::$true || $value === static::$false) { return 'bool'; } if ($this->coerceColor($value)) { return 'color'; } // fall-thru case Type::T_FUNCTION: return 'string'; case Type::T_FUNCTION_REFERENCE: return 'function'; case Type::T_LIST: if (isset($value[3]) && $value[3]) { return 'arglist'; } // fall-thru default: return $value[0]; } } protected static $libUnit = ['number']; protected function libUnit($args) { $num = $args[0]; if ($num instanceof Number) { return [Type::T_STRING, '"', [$num->unitStr()]]; } return ''; } protected static $libUnitless = ['number']; protected function libUnitless($args) { $value = $args[0]; return $value instanceof Number && $value->unitless(); } protected static $libComparable = [ ['number1', 'number2'], ['number-1', 'number-2'] ]; protected function libComparable($args) { list($number1, $number2) = $args; if ( ! $number1 instanceof Number || ! $number2 instanceof Number ) { throw $this->error('Invalid argument(s) for "comparable"'); } return $number1->isComparableTo($number2); } protected static $libStrIndex = ['string', 'substring']; protected function libStrIndex($args) { $string = $this->assertString($args[0], 'string'); $stringContent = $this->compileStringContent($string); $substring = $this->assertString($args[1], 'substring'); $substringContent = $this->compileStringContent($substring); if (! \strlen($substringContent)) { $result = 0; } else { $result = Util::mbStrpos($stringContent, $substringContent); } return $result === false ? static::$null : new Number($result + 1, ''); } protected static $libStrInsert = ['string', 'insert', 'index']; protected function libStrInsert($args) { $string = $this->assertString($args[0], 'string'); $stringContent = $this->compileStringContent($string); $insert = $this->assertString($args[1], 'insert'); $insertContent = $this->compileStringContent($insert); $index = $this->assertInteger($args[2], 'index'); if ($index > 0) { $index = $index - 1; } if ($index < 0) { $index = Util::mbStrlen($stringContent) + 1 + $index; } $string[2] = [ Util::mbSubstr($stringContent, 0, $index), $insertContent, Util::mbSubstr($stringContent, $index) ]; return $string; } protected static $libStrLength = ['string']; protected function libStrLength($args) { $string = $this->assertString($args[0], 'string'); $stringContent = $this->compileStringContent($string); return new Number(Util::mbStrlen($stringContent), ''); } protected static $libStrSlice = ['string', 'start-at', 'end-at:-1']; protected function libStrSlice($args) { if (isset($args[2]) && ! $args[2][1]) { return static::$nullString; } $string = $this->coerceString($args[0]); $stringContent = $this->compileStringContent($string); $start = (int) $args[1][1]; if ($start > 0) { $start--; } $end = isset($args[2]) ? (int) $args[2][1] : -1; $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end); $string[2] = $length ? [substr($stringContent, $start, $length)] : [substr($stringContent, $start)]; return $string; } protected static $libToLowerCase = ['string']; protected function libToLowerCase($args) { $string = $this->coerceString($args[0]); $stringContent = $this->compileStringContent($string); $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtolower')]; return $string; } protected static $libToUpperCase = ['string']; protected function libToUpperCase($args) { $string = $this->coerceString($args[0]); $stringContent = $this->compileStringContent($string); $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtoupper')]; return $string; } /** * Apply a filter on a string content, only on ascii chars * let extended chars untouched * * @param string $stringContent * @param callable $filter * @return string */ protected function stringTransformAsciiOnly($stringContent, $filter) { $mblength = Util::mbStrlen($stringContent); if ($mblength === strlen($stringContent)) { return $filter($stringContent); } $filteredString = ""; for ($i = 0; $i < $mblength; $i++) { $char = Util::mbSubstr($stringContent, $i, 1); if (strlen($char) > 1) { $filteredString .= $char; } else { $filteredString .= $filter($char); } } return $filteredString; } protected static $libFeatureExists = ['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 = ['name']; protected function libFunctionExists($args) { $string = $this->coerceString($args[0]); $name = $this->compileStringContent($string); // user defined functions if ($this->has(static::$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 = ['name']; protected function libGlobalVariableExists($args) { $string = $this->coerceString($args[0]); $name = $this->compileStringContent($string); return $this->has($name, $this->rootEnv); } protected static $libMixinExists = ['name']; protected function libMixinExists($args) { $string = $this->coerceString($args[0]); $name = $this->compileStringContent($string); return $this->has(static::$namespaces['mixin'] . $name); } protected static $libVariableExists = ['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 * * @return array */ protected function libCounter($args) { $list = array_map([$this, 'compileValue'], $args); return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']]; } protected static $libRandom = ['limit:null']; protected function libRandom($args) { if (isset($args[0]) & $args[0] !== static::$null) { $n = $this->assertNumber($args[0])->getDimension(); if ($n < 1) { throw $this->error("\$limit must be greater than or equal to 1"); } if (round($n - \intval($n), Number::PRECISION) > 0) { throw $this->error("Expected \$limit to be an integer but got $n for `random`"); } return new Number(mt_rand(1, \intval($n)), ''); } $max = mt_getrandmax(); return new Number(mt_rand(0, $max - 1) / $max, ''); } protected function libUniqueId() { static $id; if (! isset($id)) { $id = PHP_INT_SIZE === 4 ? mt_rand(0, pow(36, 5)) . str_pad(mt_rand(0, pow(36, 5)) % 10000000, 7, '0', STR_PAD_LEFT) : mt_rand(0, pow(36, 8)); } $id += mt_rand(0, 10) + 1; return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]]; } protected function inspectFormatValue($value, $force_enclosing_display = false) { if ($value === static::$null) { $value = [Type::T_KEYWORD, 'null']; } $stringValue = [$value]; if ($value[0] === Type::T_LIST) { if (end($value[2]) === static::$null) { array_pop($value[2]); $value[2][] = [Type::T_STRING, '', ['']]; $force_enclosing_display = true; } if ( ! empty($value['enclosing']) && ($force_enclosing_display || ($value['enclosing'] === 'bracket') || ! \count($value[2])) ) { $value['enclosing'] = 'forced_' . $value['enclosing']; $force_enclosing_display = true; } foreach ($value[2] as $k => $listelement) { $value[2][$k] = $this->inspectFormatValue($listelement, $force_enclosing_display); } $stringValue = [$value]; } return [Type::T_STRING, '', $stringValue]; } protected static $libInspect = ['value']; protected function libInspect($args) { $value = $args[0]; return $this->inspectFormatValue($value); } /** * Preprocess selector args * * @param array $arg * * @return array|boolean */ protected function getSelectorArg($arg, $varname = null, $allowParent = false) { static $parser = null; if (\is_null($parser)) { $parser = $this->parserFactory(__METHOD__); } if (! $this->checkSelectorArgType($arg)) { $var_display = ($varname ? ' $' . $varname . ':' : ''); $var_value = $this->compileValue($arg); throw $this->error("Error:{$var_display} $var_value is not a valid selector: it must be a string," . " a list of strings, or a list of lists of strings"); } $arg = $this->libUnquote([$arg]); $arg = $this->compileValue($arg); $parsedSelector = []; if ($parser->parseSelector($arg, $parsedSelector, true)) { $selector = $this->evalSelectors($parsedSelector); $gluedSelector = $this->glueFunctionSelectors($selector); if (! $allowParent) { foreach ($gluedSelector as $selector) { foreach ($selector as $s) { if (in_array(static::$selfSelector, $s)) { $var_display = ($varname ? ' $' . $varname . ':' : ''); throw $this->error("Error:{$var_display} Parent selectors aren't allowed here."); } } } } return $gluedSelector; } $var_display = ($varname ? ' $' . $varname . ':' : ''); throw $this->error("Error:{$var_display} expected more input, invalid selector."); } /** * Check variable type for getSelectorArg() function * @param array $arg * @param int $maxDepth * @return bool */ protected function checkSelectorArgType($arg, $maxDepth = 2) { if ($arg[0] === Type::T_LIST && $maxDepth > 0) { foreach ($arg[2] as $elt) { if (! $this->checkSelectorArgType($elt, $maxDepth - 1)) { return false; } } return true; } if (!in_array($arg[0], [Type::T_STRING, Type::T_KEYWORD])) { return false; } return true; } /** * Postprocess selector to output in right format * * @param array $selectors * * @return string */ protected function formatOutputSelector($selectors) { $selectors = $this->collapseSelectors($selectors, true); return $selectors; } protected static $libIsSuperselector = ['super', 'sub']; protected function libIsSuperselector($args) { list($super, $sub) = $args; $super = $this->getSelectorArg($super, 'super'); $sub = $this->getSelectorArg($sub, 'sub'); return $this->isSuperSelector($super, $sub); } /** * Test a $super selector again $sub * * @param array $super * @param array $sub * * @return boolean */ protected function isSuperSelector($super, $sub) { // one and only one selector for each arg if (! $super) { throw $this->error('Invalid super selector for isSuperSelector()'); } if (! $sub) { throw $this->error('Invalid sub selector for isSuperSelector()'); } if (count($sub) > 1) { foreach ($sub as $s) { if (! $this->isSuperSelector($super, [$s])) { return false; } } return true; } if (count($super) > 1) { foreach ($super as $s) { if ($this->isSuperSelector([$s], $sub)) { return true; } } return false; } $super = reset($super); $sub = reset($sub); $i = 0; $nextMustMatch = false; foreach ($super as $node) { $compound = ''; array_walk_recursive( $node, function ($value, $key) use (&$compound) { $compound .= $value; } ); if ($this->isImmediateRelationshipCombinator($compound)) { if ($node !== $sub[$i]) { return false; } $nextMustMatch = true; $i++; } else { while ($i < \count($sub) && ! $this->isSuperPart($node, $sub[$i])) { if ($nextMustMatch) { return false; } $i++; } if ($i >= \count($sub)) { return false; } $nextMustMatch = false; $i++; } } return true; } /** * Test a part of super selector again a part of sub selector * * @param array $superParts * @param array $subParts * * @return boolean */ protected function isSuperPart($superParts, $subParts) { $i = 0; foreach ($superParts as $superPart) { while ($i < \count($subParts) && $subParts[$i] !== $superPart) { $i++; } if ($i >= \count($subParts)) { return false; } $i++; } return true; } protected static $libSelectorAppend = ['selector...']; protected function libSelectorAppend($args) { // get the selector... list $args = reset($args); $args = $args[2]; if (\count($args) < 1) { throw $this->error('selector-append() needs at least 1 argument'); } $selectors = []; foreach ($args as $arg) { $selectors[] = $this->getSelectorArg($arg, 'selector'); } return $this->formatOutputSelector($this->selectorAppend($selectors)); } /** * Append parts of the last selector in the list to the previous, recursively * * @param array $selectors * * @return array * * @throws \ScssPhp\ScssPhp\Exception\CompilerException */ protected function selectorAppend($selectors) { $lastSelectors = array_pop($selectors); if (! $lastSelectors) { throw $this->error('Invalid selector list in selector-append()'); } while (\count($selectors)) { $previousSelectors = array_pop($selectors); if (! $previousSelectors) { throw $this->error('Invalid selector list in selector-append()'); } // do the trick, happening $lastSelector to $previousSelector $appended = []; foreach ($lastSelectors as $lastSelector) { $previous = $previousSelectors; foreach ($lastSelector as $lastSelectorParts) { foreach ($lastSelectorParts as $lastSelectorPart) { foreach ($previous as $i => $previousSelector) { foreach ($previousSelector as $j => $previousSelectorParts) { $previous[$i][$j][] = $lastSelectorPart; } } } } foreach ($previous as $ps) { $appended[] = $ps; } } $lastSelectors = $appended; } return $lastSelectors; } protected static $libSelectorExtend = [ ['selector', 'extendee', 'extender'], ['selectors', 'extendee', 'extender'] ]; protected function libSelectorExtend($args) { list($selectors, $extendee, $extender) = $args; $selectors = $this->getSelectorArg($selectors, 'selector'); $extendee = $this->getSelectorArg($extendee, 'extendee'); $extender = $this->getSelectorArg($extender, 'extender'); if (! $selectors || ! $extendee || ! $extender) { throw $this->error('selector-extend() invalid arguments'); } $extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender); return $this->formatOutputSelector($extended); } protected static $libSelectorReplace = [ ['selector', 'original', 'replacement'], ['selectors', 'original', 'replacement'] ]; protected function libSelectorReplace($args) { list($selectors, $original, $replacement) = $args; $selectors = $this->getSelectorArg($selectors, 'selector'); $original = $this->getSelectorArg($original, 'original'); $replacement = $this->getSelectorArg($replacement, 'replacement'); if (! $selectors || ! $original || ! $replacement) { throw $this->error('selector-replace() invalid arguments'); } $replaced = $this->extendOrReplaceSelectors($selectors, $original, $replacement, true); return $this->formatOutputSelector($replaced); } /** * Extend/replace in selectors * used by selector-extend and selector-replace that use the same logic * * @param array $selectors * @param array $extendee * @param array $extender * @param boolean $replace * * @return array */ protected function extendOrReplaceSelectors($selectors, $extendee, $extender, $replace = false) { $saveExtends = $this->extends; $saveExtendsMap = $this->extendsMap; $this->extends = []; $this->extendsMap = []; foreach ($extendee as $es) { // only use the first one $this->pushExtends(reset($es), $extender, null); } $extended = []; foreach ($selectors as $selector) { if (! $replace) { $extended[] = $selector; } $n = \count($extended); $this->matchExtends($selector, $extended); // if didnt match, keep the original selector if we are in a replace operation if ($replace && \count($extended) === $n) { $extended[] = $selector; } } $this->extends = $saveExtends; $this->extendsMap = $saveExtendsMap; return $extended; } protected static $libSelectorNest = ['selector...']; protected function libSelectorNest($args) { // get the selector... list $args = reset($args); $args = $args[2]; if (\count($args) < 1) { throw $this->error('selector-nest() needs at least 1 argument'); } $selectorsMap = []; foreach ($args as $arg) { $selectorsMap[] = $this->getSelectorArg($arg, 'selector', true); } $envs = []; foreach ($selectorsMap as $selectors) { $env = new Environment(); $env->selectors = $selectors; $envs[] = $env; } $envs = array_reverse($envs); $env = $this->extractEnv($envs); $outputSelectors = $this->multiplySelectors($env); return $this->formatOutputSelector($outputSelectors); } protected static $libSelectorParse = [ ['selector'], ['selectors'] ]; protected function libSelectorParse($args) { $selectors = reset($args); $selectors = $this->getSelectorArg($selectors, 'selector'); return $this->formatOutputSelector($selectors); } protected static $libSelectorUnify = ['selectors1', 'selectors2']; protected function libSelectorUnify($args) { list($selectors1, $selectors2) = $args; $selectors1 = $this->getSelectorArg($selectors1, 'selectors1'); $selectors2 = $this->getSelectorArg($selectors2, 'selectors2'); if (! $selectors1 || ! $selectors2) { throw $this->error('selector-unify() invalid arguments'); } // only consider the first compound of each $compound1 = reset($selectors1); $compound2 = reset($selectors2); // unify them and that's it $unified = $this->unifyCompoundSelectors($compound1, $compound2); return $this->formatOutputSelector($unified); } /** * The selector-unify magic as its best * (at least works as expected on test cases) * * @param array $compound1 * @param array $compound2 * * @return array|mixed */ protected function unifyCompoundSelectors($compound1, $compound2) { if (! \count($compound1)) { return $compound2; } if (! \count($compound2)) { return $compound1; } // check that last part are compatible $lastPart1 = array_pop($compound1); $lastPart2 = array_pop($compound2); $last = $this->mergeParts($lastPart1, $lastPart2); if (! $last) { return [[]]; } $unifiedCompound = [$last]; $unifiedSelectors = [$unifiedCompound]; // do the rest while (\count($compound1) || \count($compound2)) { $part1 = end($compound1); $part2 = end($compound2); if ($part1 && ($match2 = $this->matchPartInCompound($part1, $compound2))) { list($compound2, $part2, $after2) = $match2; if ($after2) { $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after2); } $c = $this->mergeParts($part1, $part2); $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]); $part1 = $part2 = null; array_pop($compound1); } if ($part2 && ($match1 = $this->matchPartInCompound($part2, $compound1))) { list($compound1, $part1, $after1) = $match1; if ($after1) { $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after1); } $c = $this->mergeParts($part2, $part1); $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]); $part1 = $part2 = null; array_pop($compound2); } $new = []; if ($part1 && $part2) { array_pop($compound1); array_pop($compound2); $s = $this->prependSelectors($unifiedSelectors, [$part2]); $new = array_merge($new, $this->prependSelectors($s, [$part1])); $s = $this->prependSelectors($unifiedSelectors, [$part1]); $new = array_merge($new, $this->prependSelectors($s, [$part2])); } elseif ($part1) { array_pop($compound1); $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part1])); } elseif ($part2) { array_pop($compound2); $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part2])); } if ($new) { $unifiedSelectors = $new; } } return $unifiedSelectors; } /** * Prepend each selector from $selectors with $parts * * @param array $selectors * @param array $parts * * @return array */ protected function prependSelectors($selectors, $parts) { $new = []; foreach ($selectors as $compoundSelector) { array_unshift($compoundSelector, $parts); $new[] = $compoundSelector; } return $new; } /** * Try to find a matching part in a compound: * - with same html tag name * - with some class or id or something in common * * @param array $part * @param array $compound * * @return array|false */ protected function matchPartInCompound($part, $compound) { $partTag = $this->findTagName($part); $before = $compound; $after = []; // try to find a match by tag name first while (\count($before)) { $p = array_pop($before); if ($partTag && $partTag !== '*' && $partTag == $this->findTagName($p)) { return [$before, $p, $after]; } $after[] = $p; } // try again matching a non empty intersection and a compatible tagname $before = $compound; $after = []; while (\count($before)) { $p = array_pop($before); if ($this->checkCompatibleTags($partTag, $this->findTagName($p))) { if (\count(array_intersect($part, $p))) { return [$before, $p, $after]; } } $after[] = $p; } return false; } /** * Merge two part list taking care that * - the html tag is coming first - if any * - the :something are coming last * * @param array $parts1 * @param array $parts2 * * @return array */ protected function mergeParts($parts1, $parts2) { $tag1 = $this->findTagName($parts1); $tag2 = $this->findTagName($parts2); $tag = $this->checkCompatibleTags($tag1, $tag2); // not compatible tags if ($tag === false) { return []; } if ($tag) { if ($tag1) { $parts1 = array_diff($parts1, [$tag1]); } if ($tag2) { $parts2 = array_diff($parts2, [$tag2]); } } $mergedParts = array_merge($parts1, $parts2); $mergedOrderedParts = []; foreach ($mergedParts as $part) { if (strpos($part, ':') === 0) { $mergedOrderedParts[] = $part; } } $mergedParts = array_diff($mergedParts, $mergedOrderedParts); $mergedParts = array_merge($mergedParts, $mergedOrderedParts); if ($tag) { array_unshift($mergedParts, $tag); } return $mergedParts; } /** * Check the compatibility between two tag names: * if both are defined they should be identical or one has to be '*' * * @param string $tag1 * @param string $tag2 * * @return array|false */ protected function checkCompatibleTags($tag1, $tag2) { $tags = [$tag1, $tag2]; $tags = array_unique($tags); $tags = array_filter($tags); if (\count($tags) > 1) { $tags = array_diff($tags, ['*']); } // not compatible nodes if (\count($tags) > 1) { return false; } return $tags; } /** * Find the html tag name in a selector parts list * * @param array $parts * * @return mixed|string */ protected function findTagName($parts) { foreach ($parts as $part) { if (! preg_match('/^[\[.:#%_-]/', $part)) { return $part; } } return ''; } protected static $libSimpleSelectors = ['selector']; protected function libSimpleSelectors($args) { $selector = reset($args); $selector = $this->getSelectorArg($selector, 'selector'); // remove selectors list layer, keeping the first one $selector = reset($selector); // remove parts list layer, keeping the first part $part = reset($selector); $listParts = []; foreach ($part as $p) { $listParts[] = [Type::T_STRING, '', [$p]]; } return [Type::T_LIST, ',', $listParts]; } protected static $libScssphpGlob = ['pattern']; protected function libScssphpGlob($args) { $string = $this->coerceString($args[0]); $pattern = $this->compileStringContent($string); $matches = glob($pattern); $listParts = []; foreach ($matches as $match) { if (! is_file($match)) { continue; } $listParts[] = [Type::T_STRING, '"', [$match]]; } return [Type::T_LIST, ',', $listParts]; } } PK � �\-�(x[ [ scssphp/src/Colors.phpnu �[��� <?php /** * SCSSPHP * * @copyright 2012-2020 Leaf Corcoran * * @license http://opensource.org/licenses/MIT MIT * * @link http://scssphp.github.io/scssphp */ namespace ScssPhp\ScssPhp; /** * CSS Colors * * @author Leaf Corcoran <leafot@gmail.com> */ class Colors { /** * CSS Colors * * @see http://www.w3.org/TR/css3-color * * @var array */ protected static $cssColors = [ 'aliceblue' => '240,248,255', 'antiquewhite' => '250,235,215', 'cyan' => '0,255,255', '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', 'darkblue' => '0,0,139', 'darkcyan' => '0,139,139', 'darkgoldenrod' => '184,134,11', 'darkgray' => '169,169,169', 'darkgrey' => '169,169,169', 'darkgreen' => '0,100,0', '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', 'magenta' => '255,0,255', 'fuchsia' => '255,0,255', 'gainsboro' => '220,220,220', 'ghostwhite' => '248,248,255', 'gold' => '255,215,0', 'goldenrod' => '218,165,32', 'gray' => '128,128,128', 'grey' => '128,128,128', 'green' => '0,128,0', 'greenyellow' => '173,255,47', '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', 'lightgrey' => '211,211,211', 'lightgreen' => '144,238,144', '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', 'maroon' => '128,0,0', 'mediumaquamarine' => '102,205,170', 'mediumblue' => '0,0,205', 'mediumorchid' => '186,85,211', 'mediumpurple' => '147,112,219', 'mediumseagreen' => '60,179,113', 'mediumslateblue' => '123,104,238', 'mediumspringgreen' => '0,250,154', 'mediumturquoise' => '72,209,204', 'mediumvioletred' => '199,21,133', 'midnightblue' => '25,25,112', 'mintcream' => '245,255,250', 'mistyrose' => '255,228,225', 'moccasin' => '255,228,181', 'navajowhite' => '255,222,173', 'navy' => '0,0,128', 'oldlace' => '253,245,230', 'olive' => '128,128,0', 'olivedrab' => '107,142,35', 'orange' => '255,165,0', 'orangered' => '255,69,0', 'orchid' => '218,112,214', 'palegoldenrod' => '238,232,170', 'palegreen' => '152,251,152', 'paleturquoise' => '175,238,238', 'palevioletred' => '219,112,147', 'papayawhip' => '255,239,213', 'peachpuff' => '255,218,185', 'peru' => '205,133,63', 'pink' => '255,192,203', 'plum' => '221,160,221', 'powderblue' => '176,224,230', 'purple' => '128,0,128', 'red' => '255,0,0', 'rosybrown' => '188,143,143', 'royalblue' => '65,105,225', 'saddlebrown' => '139,69,19', 'salmon' => '250,128,114', 'sandybrown' => '244,164,96', 'seagreen' => '46,139,87', 'seashell' => '255,245,238', 'sienna' => '160,82,45', 'silver' => '192,192,192', 'skyblue' => '135,206,235', 'slateblue' => '106,90,205', 'slategray' => '112,128,144', 'slategrey' => '112,128,144', 'snow' => '255,250,250', 'springgreen' => '0,255,127', 'steelblue' => '70,130,180', 'tan' => '210,180,140', 'teal' => '0,128,128', 'thistle' => '216,191,216', 'tomato' => '255,99,71', 'turquoise' => '64,224,208', 'violet' => '238,130,238', 'wheat' => '245,222,179', 'white' => '255,255,255', 'whitesmoke' => '245,245,245', 'yellow' => '255,255,0', 'yellowgreen' => '154,205,50', 'rebeccapurple' => '102,51,153', 'transparent' => '0,0,0,0', ]; /** * Convert named color in a [r,g,b[,a]] array * * @param string $colorName * * @return array|null */ public static function colorNameToRGBa($colorName) { if (\is_string($colorName) && isset(static::$cssColors[$colorName])) { $rgba = explode(',', static::$cssColors[$colorName]); // only case with opacity is transparent, with opacity=0, so we can intval on opacity also $rgba = array_map('intval', $rgba); return $rgba; } return null; } /** * Reverse conversion : from RGBA to a color name if possible * * @param integer $r * @param integer $g * @param integer $b * @param integer $a * * @return string|null */ public static function RGBaToColorName($r, $g, $b, $a = 1) { static $reverseColorTable = null; if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b) || ! is_numeric($a)) { return null; } if ($a < 1) { return null; } if (\is_null($reverseColorTable)) { $reverseColorTable = []; foreach (static::$cssColors as $name => $rgb_str) { $rgb_str = explode(',', $rgb_str); if ( \count($rgb_str) == 3 && ! isset($reverseColorTable[\intval($rgb_str[0])][\intval($rgb_str[1])][\intval($rgb_str[2])]) ) { $reverseColorTable[\intval($rgb_str[0])][\intval($rgb_str[1])][\intval($rgb_str[2])] = $name; } } } if (isset($reverseColorTable[\intval($r)][\intval($g)][\intval($b)])) { return $reverseColorTable[\intval($r)][\intval($g)][\intval($b)]; } return null; } } PK � �\bӆ�� � - scssphp/src/Exception/SassScriptException.phpnu �[��� <?php namespace ScssPhp\ScssPhp\Exception; /** * An exception thrown by SassScript. * * This class does not implement SassException on purpose, as it should * never be returned to the outside code. The compilation will catch it * and replace it with a SassException reporting the location of the * error. */ class SassScriptException extends \Exception { /** * Creates a SassScriptException with support for an argument name. * * This helper ensures a consistent handling of argument names in the * error message, without duplicating it. * * @param string $message * @param string|null $name The argument name, without $ * * @return SassScriptException */ public static function forArgument($message, $name = null) { $varDisplay = !\is_null($name) ? "\${$name}: " : ''; return new self($varDisplay . $message); } } PK � �\"��a a ) scssphp/src/Exception/ServerException.phpnu �[��� <?php /** * SCSSPHP * * @copyright 2012-2020 Leaf Corcoran * * @license http://opensource.org/licenses/MIT MIT * * @link http://scssphp.github.io/scssphp */ namespace ScssPhp\ScssPhp\Exception; /** * Server Exception * * @author Anthon Pang <anthon.pang@gmail.com> */ class ServerException extends \Exception implements SassException { } PK � �\�} ) scssphp/src/Exception/ParserException.phpnu �[��� <?php /** * SCSSPHP * * @copyright 2012-2020 Leaf Corcoran * * @license http://opensource.org/licenses/MIT MIT * * @link http://scssphp.github.io/scssphp */ namespace ScssPhp\ScssPhp\Exception; /** * Parser Exception * * @author Oleksandr Savchenko <traveltino@gmail.com> */ class ParserException extends \Exception implements SassException { /** * @var array */ private $sourcePosition; /** * Get source position * * @api */ public function getSourcePosition() { return $this->sourcePosition; } /** * Set source position * * @api * * @param array $sourcePosition */ public function setSourcePosition($sourcePosition) { $this->sourcePosition = $sourcePosition; } } PK � �\�8��I I '