├── 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 |
--------------------------------------------------------------------------------