├── LICENSE ├── README.md ├── composer.json └── src └── Generator.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Luiz Bills 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSS Generator 2 | 3 | Write CSS programatically using PHP. 4 | 5 | ## Install 6 | 7 | ```bash 8 | composer require luizbills/css-generator 9 | ``` 10 | 11 | ## Usage 12 | 13 | 14 | ### Create a generator 15 | 16 | ```php 17 | require_once 'vendor/autoload.php'; 18 | 19 | use luizbills\CSS_Generator\Generator as CSS_Generator; 20 | 21 | $options = [ 22 | // default values 23 | // 'indent_style' => 'space', // you can change to 'tab' 24 | // 'indent_size' => 4 // 4 spaces by default 25 | ]; 26 | 27 | $css = new CSS_Generator( $options ); 28 | 29 | // define your css code (see below) 30 | 31 | // output the generated code 32 | echo ""; 33 | ``` 34 | 35 | ### Add rules 36 | 37 | ```php 38 | $css->add_rule( 'a', [ 'color' => 'red' ] ); 39 | 40 | $css->add_rule( 41 | [ 'p', 'div' ], 42 | [ 43 | 'margin' => '13px', 44 | 'padding' => '9px' 45 | ] 46 | ); 47 | ``` 48 | 49 | Output: 50 | 51 | ```css 52 | a { 53 | color: red; 54 | } 55 | p, 56 | div { 57 | margin: 13px; 58 | padding: 9px; 59 | } 60 | 61 | ``` 62 | 63 | ### Add global variables 64 | 65 | ```php 66 | $css->root_variable( 'color1', 'red' ); 67 | $css->add_rule( 'a', [ 'color' => 'var(--color3)' ] ); 68 | $css->root_variable( 'color2', 'green' ); 69 | $css->root_variable( 'color3', 'blue' ); 70 | ``` 71 | 72 | Output: 73 | 74 | ```css 75 | :root { 76 | --color1: red; 77 | --color2: green; 78 | --color3: blue; 79 | } 80 | 81 | a { 82 | color: var(--color3); 83 | } 84 | 85 | ``` 86 | 87 | **Note:** all variables declared by `root_variable` will be placed at the beginning. 88 | 89 | ### Add comments 90 | 91 | ```php 92 | $css->add_comment( 'Lorem ipsum...' ) 93 | ``` 94 | 95 | Output: 96 | 97 | ```css 98 | /* Lorem ipsum... */ 99 | 100 | ``` 101 | 102 | ### Open and close blocks 103 | 104 | ```php 105 | $css->open_block( 'media', 'screen and (min-width: 30em)' ); 106 | $css->add_rule( 'a', [ 'color' => 'red' ] ); 107 | $css->close_block(); // close the last opened block 108 | ``` 109 | 110 | Output: 111 | 112 | ```css 113 | @media screen and (min-width: 30em) { 114 | a { 115 | color: red; 116 | } 117 | } 118 | 119 | ``` 120 | 121 | ### Escape selectors 122 | 123 | Sometimes you need to escape your selectors. 124 | 125 | ```html 126 | 127 |
128 |
129 |
130 | ``` 131 | 132 | ```php 133 | $css->add_rule( '#' . $css->esc( '@' ), [ 134 | 'animation' => 'shake 1s' 135 | ] ); 136 | $css->add_rule( '.' . $css->esc( '3dots' ) . '::after', [ 137 | 'content' => '"..."' 138 | ] ); 139 | $css->add_rule( '.' . $css->esc( 'red:hover' ) . ':hover', [ 140 | 'color' => 'red' 141 | ] ); 142 | ``` 143 | 144 | Output: 145 | 146 | ```css 147 | #\@ { 148 | animation: shake 1s; 149 | } 150 | .\33 dots::after { 151 | content: "..."; 152 | } 153 | .red\:hover:hover { 154 | color: red; 155 | } 156 | 157 | ``` 158 | 159 | ### Include anything (be careful) 160 | 161 | ```php 162 | $css->add_raw( 'a{color:red}' ); 163 | ``` 164 | 165 | Output: 166 | 167 | ```css 168 | a{color:red} 169 | ``` 170 | 171 | ### Minify your CSS 172 | 173 | ```php 174 | echo $css->get_output( true ); // returns the compressed code 175 | echo $css->get_output( false ); // returns the pretty code 176 | ``` 177 | 178 | ## License 179 | 180 | MIT License © 2022 Luiz Bills 181 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "luizbills/css-generator", 3 | "description": "Write CSS programatically using PHP.", 4 | "type": "library", 5 | "require": { 6 | "php": ">=7.4.0", 7 | "ext-mbstring": "*" 8 | }, 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Luiz Bills", 13 | "email": "luizpbills@gmail.com" 14 | } 15 | ], 16 | "autoload": { 17 | "psr-4": { 18 | "luizbills\\CSS_Generator\\": "src" 19 | } 20 | }, 21 | "require-dev": { 22 | "phpstan/phpstan": "^1.8", 23 | "phpstan/phpstan-strict-rules": "^1.4", 24 | "codeception/codeception": "^4.2", 25 | "codeception/module-asserts": "^1.0.0" 26 | }, 27 | "scripts": { 28 | "test": "codecept run unit", 29 | "check": "phpstan" 30 | } 31 | } -------------------------------------------------------------------------------- /src/Generator.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2022 Luiz Bills 8 | * @license MIT 9 | */ 10 | 11 | namespace luizbills\CSS_Generator; 12 | 13 | /** 14 | * @phpstan-type GeneratorConfig array{indent_style?: string, indent_size?: int} 15 | */ 16 | class Generator { 17 | const VERSION = '4.0.0'; 18 | 19 | /** 20 | * Stores the global variables 21 | * 22 | * @var array 23 | */ 24 | protected array $variables = []; 25 | 26 | /** 27 | * Store the declared CSS blocks and comments 28 | * 29 | * @var array> 30 | */ 31 | protected array $blocks = []; 32 | 33 | /** 34 | * Store the config options 35 | * 36 | * @var array 37 | */ 38 | protected array $options = []; 39 | 40 | /** 41 | * Store the indentation level 42 | * 43 | * @var int 44 | */ 45 | protected int $indent_level = 0; 46 | 47 | /** 48 | * @var bool 49 | */ 50 | protected bool $compress_output; 51 | 52 | /** 53 | * Stores the last generated CSS in prettry format (with tabs, spaces and line breaks). 54 | * 55 | * @var string|null 56 | */ 57 | protected $cache_pretty; 58 | 59 | /** 60 | * Stores the last generated CSS in minified format 61 | * 62 | * @var string|null 63 | */ 64 | protected $cache_compressed; 65 | 66 | /** 67 | * Class constructor 68 | * 69 | * @param GeneratorConfig $options 70 | */ 71 | public function __construct ( $options = [] ) { 72 | $default_options = [ 73 | 'indent_size' => 4, // only works with indent_style = space 74 | 'indent_style' => 'space', // or "tab" 75 | ]; 76 | $this->options = array_merge( $default_options, $options ); 77 | } 78 | 79 | /** 80 | * Returns the generated CSS 81 | * 82 | * @param boolean $compressed 83 | * @return string 84 | */ 85 | public function get_output ( $compressed = false ) { 86 | $result = ''; 87 | if ( ! $compressed ) { 88 | $result = $this->cache_pretty = $this->generate( false ); 89 | } else { 90 | $result = $this->cache_compressed = $this->generate( true ); 91 | } 92 | return $result; 93 | } 94 | 95 | /** 96 | * Print anything (be careful) 97 | * 98 | * @param string $string 99 | * @return $this 100 | */ 101 | public function add_raw ( $string ) { 102 | $this->blocks[] = [ 103 | 'type' => 'raw', 104 | 'raw' => $string 105 | ]; 106 | $this->clear_cache(); 107 | return $this; 108 | } 109 | 110 | /** 111 | * Print a code comment 112 | * 113 | * @param string $string 114 | * @return $this 115 | */ 116 | public function add_comment ( $string ) { 117 | $this->blocks[] = [ 118 | 'type' => 'comment', 119 | 'comment' => $string 120 | ]; 121 | $this->clear_cache(); 122 | return $this; 123 | } 124 | 125 | /** 126 | * Declares a CSS rule 127 | * 128 | * @param string|string[] $selectors 129 | * @param array $declarations 130 | * @return $this 131 | */ 132 | public function add_rule ( $selectors, $declarations ) { 133 | $selectors = ! is_array( $selectors ) ? [ $selectors ] : $selectors; 134 | $this->blocks[] = [ 135 | 'type' => 'rule', 136 | 'selectors' => $selectors, 137 | 'declarations' => $declarations, 138 | ]; 139 | $this->clear_cache(); 140 | return $this; 141 | } 142 | 143 | /** 144 | * Declares a global variable (in :root) 145 | * 146 | * @param string $name The variable name 147 | * @param string|numeric-string $value The variable value 148 | * @return $this 149 | */ 150 | public function root_variable ( $name, $value ) { 151 | $this->variables[ trim( $name ) ] = trim( strval( $value ) ); 152 | $this->clear_cache(); 153 | return $this; 154 | } 155 | 156 | /** 157 | * Opens a block like @media, @supports, etc. 158 | * 159 | * @param string $name 160 | * @param string $props 161 | * @return $this 162 | */ 163 | public function open_block ( $name, $props = '' ) { 164 | $this->blocks[] = [ 165 | 'type' => 'open', 166 | 'name' => $name, 167 | 'props' => $props 168 | ]; 169 | 170 | $this->clear_cache(); 171 | return $this; 172 | } 173 | 174 | /** 175 | * Closes the last opened block 176 | * 177 | * @return $this 178 | */ 179 | public function close_block () { 180 | $this->blocks[] = [ 181 | 'type' => 'close', 182 | ]; 183 | $this->clear_cache(); 184 | return $this; 185 | } 186 | 187 | /** 188 | * @return void 189 | */ 190 | public function clear_cache () { 191 | $this->cache_compressed = null; 192 | $this->cache_pretty = null; 193 | } 194 | 195 | /** 196 | * Delete all declared blocks and resets the instance. 197 | * 198 | * @return void 199 | */ 200 | public function reset () { 201 | $this->clear_cache(); 202 | $this->blocks = []; 203 | } 204 | 205 | /** 206 | * Alias for self::escape 207 | * 208 | * @param string $selector 209 | * @return string 210 | */ 211 | public function esc ( $selector ) { 212 | return self::escape( $selector ); 213 | } 214 | 215 | /** 216 | * Returns one unit of indentation 217 | * 218 | * @return string 219 | */ 220 | public function get_indent_unit () { 221 | if ( 'space' === $this->options['indent_style'] ) { 222 | $size = intval( $this->options['indent_size'] ); 223 | return str_repeat( ' ', max( $size, 2 ) ); 224 | } 225 | return "\t"; 226 | } 227 | 228 | /** 229 | * Returns the current indentation (if not generating a minified code). 230 | * 231 | * @return string 232 | */ 233 | protected function tab () { 234 | if ( ! $this->compress_output ) { 235 | if ( $this->indent_level > 0 ) { 236 | return str_repeat( $this->get_indent_unit(), $this->indent_level ); 237 | } 238 | } 239 | return ''; 240 | } 241 | 242 | /** 243 | * Build the CSS code 244 | * 245 | * @param boolean $compressed 246 | * @return string 247 | */ 248 | protected function generate ( $compressed = false ) { 249 | $this->indent_level = 0; 250 | $this->compress_output = $compressed; 251 | 252 | $br = $this->compress_output ? '' : "\n"; 253 | $s = $this->compress_output ? '' : ' '; 254 | $open = $s . '{' . $br; 255 | $close = '}' . $br; 256 | $output = ''; 257 | $has_variables = count( $this->variables ) > 0; 258 | 259 | if ( $has_variables ) { 260 | $output .= ':root' . $open; 261 | $this->indent_level++; 262 | foreach ( $this->variables as $name => $value ) { 263 | $output .= $this->tab(); 264 | $output .= "--$name:$s$value;$br"; 265 | } 266 | $output .= $close . $br; 267 | $this->indent_level--; 268 | } 269 | 270 | foreach ( $this->blocks as $block ) { 271 | switch ( $block['type'] ) { 272 | case 'comment': 273 | if ( ! $compressed ) { 274 | $output .= $this->tab(); 275 | $output .= "/* {$block['comment']} */$br"; 276 | } 277 | break; 278 | case 'raw': 279 | $output .= $block['raw']; 280 | break; 281 | case 'rule': 282 | $output .= $this->tab(); 283 | $selectors = array_map( 'trim', $block['selectors'] ); 284 | $output .= implode( ",$br" . $this->tab(), $selectors ); 285 | $output .= $open; 286 | $this->indent_level++; 287 | foreach ( $block['declarations'] as $key => $value ) { 288 | $output .= $this->tab(); 289 | $output .= trim( $key ) . ":$s" . trim( $value ) . ";$br"; 290 | } 291 | $this->indent_level--; 292 | $output .= $this->tab() . $close; 293 | break; 294 | case 'open': 295 | $output .= $this->tab(); 296 | $output .= '@' . $block['name']; 297 | $output .= '' !== $block['props'] ? " {$block['props']}" : ''; 298 | $output .= "$open"; 299 | $this->indent_level++; 300 | break; 301 | case 'close': 302 | $this->indent_level--; 303 | $output .= $this->tab() . $close; 304 | break; 305 | default: 306 | break; 307 | } 308 | } 309 | 310 | while ( $this->indent_level > 0 ) { 311 | error_log( 'level = ' . $this->indent_level ); 312 | $this->indent_level--; 313 | $output .= $this->tab() . $close; 314 | } 315 | 316 | return $output; 317 | } 318 | 319 | /** 320 | * Escapes a CSS rule selector. Based on https://github.com/mathiasbynens/CSS.escape 321 | * 322 | * @static 323 | * @param string $selector 324 | * @return string 325 | */ 326 | public static function escape ( $selector ) { 327 | if ( '' === $selector ) return $selector; 328 | 329 | $length = mb_strlen( $selector ); 330 | $result = ''; 331 | $index = -1; 332 | $first_char = mb_substr( $selector, 0, 1 ); 333 | $first_char_code = ord( $first_char ); 334 | 335 | if ( 336 | // If the character is the first character and is a `-` (U+002D), and 337 | // there is no second character, […] 338 | 1 === $length && 339 | 0x2D === $first_char_code 340 | ) { 341 | return '\\' . $selector; 342 | } 343 | 344 | while ( ++$index < $length ) { 345 | $char = mb_substr( $selector, $index, 1 ); 346 | if ( '' === $char ) continue; 347 | 348 | $char_code = ord( $char ); 349 | 350 | // If the character is NULL 351 | if ( 0 === $char_code ) { 352 | $result .= "\u{FFFD}"; 353 | continue; 354 | } 355 | 356 | if ( 357 | // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is U+007F, […] 358 | $char_code <= 0x1F || 0x7F === $char_code || 359 | // If the character is the first character and is in the range [0-9] 360 | // (U+0030 to U+0039), […] 361 | ( 0 === $index && $char_code >= 0x30 && $char_code <= 0x39 ) || 362 | // If the character is the second character and is in the range [0-9] 363 | // (U+0030 to U+0039) and the first character is a `-` (U+002D), […] 364 | ( 365 | 1 === $index && 366 | $char_code >= 0x0030 && $char_code <= 0x0039 && 367 | 0x002D === $first_char_code 368 | ) 369 | ) { 370 | // https://drafts.csswg.org/cssom/#escape-a-character-as-code-point 371 | $result .= '\\' . dechex( $char_code ) . ' '; 372 | continue; 373 | } 374 | 375 | if ( 376 | // If the character is not handled by one of the above rules and is 377 | // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or 378 | $char_code >= 0x0080 || 0x002D === $char_code || 0x005F === $char_code || 379 | // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to U+005A) 380 | $char_code >= 0x0030 && $char_code <= 0x0039 || 381 | $char_code >= 0x0041 && $char_code <= 0x005A || 382 | // , or [a-z] (U+0061 to U+007A), […] 383 | $char_code >= 0x0061 && $char_code <= 0x007A 384 | ) { 385 | // the character itself 386 | $result .= $char; 387 | continue; 388 | } 389 | 390 | // Otherwise, the escaped character. 391 | // https://drafts.csswg.org/cssom/#escape-a-character 392 | $result .= "\\" . $char; 393 | } 394 | 395 | return $result; 396 | } 397 | } 398 | --------------------------------------------------------------------------------