├── .gitignore ├── src ├── Framework │ ├── Framework.php │ └── BootstrapFramework.php ├── ConsoleHelper.php └── Converter.php ├── .travis.yml ├── docs ├── 2_installation.md ├── 1_introduction.md └── 3_quick-start.md ├── tests └── Bootstrap │ ├── TextTest.php │ ├── ConverterTest.php │ ├── BootstrapFrameworkTest.php │ └── SpacingTest.php ├── composer.json ├── LICENSE.md ├── phpunit.xml.dist ├── .github └── workflows │ └── phpunit.yml ├── README.md └── tailwindo /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | composer.lock 3 | vendor 4 | build 5 | .phpunit.result.cache 6 | -------------------------------------------------------------------------------- /src/Framework/Framework.php: -------------------------------------------------------------------------------- 1 | **Tip** To install this tool make sure that PHP & [Composer](https://getcomposer.org/doc/00-intro.md) (a PHP package manager) are installed on your system . 10 | 11 | --- 12 | 13 |

14 | Prev: < Introduction 15 |

16 | 17 |

18 | Next: Quick start > 19 |

20 | -------------------------------------------------------------------------------- /tests/Bootstrap/TextTest.php: -------------------------------------------------------------------------------- 1 | converter = (new Converter())->setFramework('bootstrap'); 16 | } 17 | 18 | /** @test */ 19 | public function it_converts_text_with_breakpoint() 20 | { 21 | $this->assertEquals( 22 | 'sm:text-left', 23 | $this->converter->classesOnly(true)->setContent('text-xs-left')->convert()->get() 24 | ); 25 | $this->assertEquals( 26 | 'lg:text-justify', 27 | $this->converter->classesOnly(true)->setContent('text-lg-justify')->convert()->get() 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awssat/tailwindo", 3 | "description": "Convert Bootstrap CSS to Tailwind CSS", 4 | "keywords": [ 5 | "CSS", 6 | "Tailwind", 7 | "Bootstrap" 8 | ], 9 | "homepage": "https://github.com/awssat/tailwindo", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Awssat", 14 | "email": "hello@awssat.com" 15 | } 16 | ], 17 | 18 | "require": { 19 | "php": "^7.2|^8.0", 20 | "symfony/console": "^4.0|^5.0|^6.0" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "^8" 24 | }, 25 | 26 | "autoload": { 27 | "psr-4": { 28 | "Awssat\\Tailwindo\\": "src" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Awssat\\Tailwindo\\Test\\": "tests" 34 | } 35 | }, 36 | "scripts": { 37 | "test": "vendor/bin/phpunit" 38 | }, 39 | "bin": [ 40 | "tailwindo" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /docs/1_introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Tailwindo can convert Your CSS framework (currently Boostrap) classes in HTML/PHP (any of your choice) files to equivalent Tailwind CSS classes. 4 | 5 | ## Features 6 | 7 | - Convert Bootstrap based files to TailwindCss. 8 | - Multiple files (Recursively) convert. 9 | - Multiple files extensions support (.html, .vue, .twig, .blade and more) 10 | - Safe converting, non-destructive option. 11 | - Can convert a given raw code without file. 12 | - Made to be easy to add more CSS frameworks in the future (currently Bootstrap). 13 | - Can extract changes to a separate css file as Tailwind components and keep old classes names. like: 14 | 15 | ``` 16 | .p-md-5 { 17 | @apply md:p-7; 18 | } 19 | ``` 20 | 21 | > **Tip** Check our newest tool, update your tailwindcss code to newer versions hassle-free ([tailwind-shift](https://github.com/awssat/tailwind-shift)) 22 | 23 | --- 24 | 25 |

26 | Next: Installation > 27 |

28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Abdulrahman M 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.github/workflows/phpunit.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | tests: 11 | strategy: 12 | fail-fast: true 13 | matrix: 14 | php: ['7.2', '7.3', '7.4', '8.0'] 15 | dependency-version: [prefer-lowest, prefer-stable] 16 | 17 | runs-on: ubuntu-latest 18 | 19 | name: PHP${{ matrix.php }}- ${{ matrix.dependency-version }} 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - name: Cache dependencies 25 | uses: actions/cache@v1 26 | with: 27 | path: ~/.composer/cache/files 28 | key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 29 | 30 | - name: Setup PHP 31 | uses: shivammathur/setup-php@v2 32 | with: 33 | php-version: ${{ matrix.php }} 34 | coverage: none 35 | 36 | - name: Install dependencies 37 | run: composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest --no-progress 38 | 39 | - name: Run tests 40 | run: composer run-script test 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tailwindo 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/awssat/tailwindo.svg?style=flat-square)](https://packagist.org/packages/awssat/tailwindo) 4 | [![Actions Status](https://github.com/awssat/tailwindo/workflows/Tests/badge.svg)](https://github.com/awssat/tailwindo/actions) 5 | 6 |

7 | 8 |

9 | 10 | This tool can **convert Your CSS framework (currently Bootstrap) classes** in HTML/PHP (any of your choice) files to equivalent **Tailwind CSS** classes. 11 | 12 | ## Features 13 | 14 | - Made to be easy to add more CSS frameworks in the future (currently Bootstrap). 15 | - Can convert single files/code snippets/folders. 16 | - Can extract changes to a separate css file as Tailwind components and keep old classes names. like: 17 | 18 | ``` 19 | .p-md-5 { 20 | @apply md:p-7; 21 | } 22 | ``` 23 | 24 | ## Help Us 25 | 26 | - If you find unexpected conversion result, create an issue; if you managed to fix it, please create a PR. 27 | - If you are familiar with another CSS frameworks (like Foundation, Pure..), please create a PR and add it (see BootstrapFramework.php file). 28 | 29 | ## Docs 30 | 31 | - [Introduction](docs/1_introduction.md) 32 | - [Installation](docs/2_installation.md) 33 | - [Quick start](docs/3_quick-start.md) 34 | -------------------------------------------------------------------------------- /tests/Bootstrap/ConverterTest.php: -------------------------------------------------------------------------------- 1 | converter = (new Converter())->setFramework('bootstrap'); 16 | } 17 | 18 | /** @test */ 19 | public function it_returns_output() 20 | { 21 | $this->assertEquals( 22 | 'sm:flex', 23 | $this->converter->classesOnly(true)->setContent('d-sm-flex')->convert()->get() 24 | ); 25 | $this->assertEquals( 26 | 'love', 27 | $this->converter->setContent('love')->convert()->get() 28 | ); 29 | } 30 | 31 | /** @test */ 32 | public function it_returns_output_with_prefix() 33 | { 34 | $this->converter->setPrefix('tw-'); 35 | $this->assertEquals( 36 | 'sm:tw-flex', 37 | $this->converter->classesOnly(true)->setContent('d-sm-flex')->convert()->get() 38 | ); 39 | $this->assertEquals( 40 | 'love', 41 | $this->converter->setContent('love')->convert()->get() 42 | ); 43 | } 44 | 45 | /** @test */ 46 | public function it_handles_jsx_class_name() 47 | { 48 | $this->assertEquals( 49 | 'love', 50 | $this->converter->setContent('love')->convert()->get() 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Bootstrap/BootstrapFrameworkTest.php: -------------------------------------------------------------------------------- 1 | bootstrap = (new BootstrapFramework())->get(); 18 | $this->converter = (new Converter())->setFramework('bootstrap'); 19 | } 20 | 21 | /** @test */ 22 | public function it_searches_for_no_classes_containing_double_dashes() 23 | { 24 | $match_array = []; 25 | 26 | foreach ($this->bootstrap as $item) { 27 | foreach ($item as $search => $replace) { 28 | if (strpos($search, '--') !== false) { 29 | array_push($match_array, $search); 30 | } 31 | } 32 | } 33 | 34 | print_r($match_array); 35 | $this->assertEmpty($match_array); 36 | } 37 | 38 | /** @test */ 39 | public function it_replaces_with_no_classes_containing_double_dashes() 40 | { 41 | $match_array = []; 42 | 43 | foreach ($this->bootstrap as $item) { 44 | foreach ($item as $search => $replace) { 45 | if ($replace instanceof \Closure) { 46 | $callableReplace = \Closure::bind($replace, $this->converter, Converter::class); 47 | $replace = $callableReplace(); 48 | } 49 | 50 | if (strpos($replace, '--') !== false) { 51 | array_push($match_array, [$search => $replace]); 52 | } 53 | } 54 | } 55 | 56 | // print_r($match_array); 57 | $this->assertEmpty($match_array); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docs/3_quick-start.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | ## Convert a whole directory 4 | 5 | just the files in a directory, it's not recursive 6 | 7 | ```bash 8 | tailwindo path/to/directory/ 9 | ``` 10 | 11 | ## Recursively convert a whole directory 12 | 13 | ```bash 14 | tailwindo path/to/directory/ --recursive=true 15 | ``` 16 | 17 | You can also use the short hand `-r true` instead of the full `--recursive=true` 18 | 19 | ## Convert different file extensions 20 | 21 | This will allow you to upgrade your `vue` files, `twig` files, and more! 22 | 23 | ```bash 24 | tailwindo path/to/directory/ --extensions=vue,php,html 25 | ``` 26 | 27 | You can also use the short hand `-e vue,php,html` instead of the full `--extensions` 28 | 29 | ## Overwrite the original files 30 | 31 | ```bash 32 | tailwindo path/to/directory/ --replace=true 33 | ``` 34 | 35 | > **Tip** Please note this can be considered a destructive action as it will replace the original file and will not leave a copy of the original any where. 36 | 37 | ## Convert one file 38 | 39 | By default this will copy the code into a new file like file.html -> file.tw.html 40 | 41 | ```bash 42 | tailwindo file.blade.php 43 | ``` 44 | 45 | This option works with the `--replace=true` option too. 46 | 47 | ## Convert raw code (a snippet of code) 48 | 49 | just CSS classes: 50 | 51 | ```bash 52 | tailwindo 'alert alert-info' 53 | ``` 54 | 55 | Or html: 56 | 57 | ```bash 58 | tailwindo '
hi
' 59 | ``` 60 | 61 | ## Extract changes to a single CSS file 62 | 63 | Extract changes as components to a separate css file (tailwindo-components.css). 64 | 65 | ```bash 66 | tailwindo --components=true path/to/directory/ 67 | ``` 68 | 69 | For example if you have a file called demo.html and contains: 70 | 71 | ```html 72 |
Love is a chemical reaction, soul has nothing to do with it.
73 | ``` 74 | 75 | and runs: 76 | 77 | ```bash 78 | tailwindo --components=true demo.html 79 | ``` 80 | 81 | then Tailwindo will not change demo.html and create a CSS file called 'tailwindo-components.css' that contains: 82 | 83 | ``` 84 | .alert { 85 | @apply relative px-3 py-3 mb-4 border rounded; 86 | } 87 | .alert-info { 88 | @apply bg-teal-200 bg-teal-300 bg-teal-800; 89 | } 90 | ``` 91 | 92 | This will let you keep older markup unchanged and you can just add the new extract components to your main css file. 93 | 94 | ### Supported Frameworks 95 | 96 | You can specify what CSS framework your code is written in, by using`framework` option in the command line. 97 | 98 | #### Currently we support these frameworks: 99 | 100 | - Bootstrap 101 | 102 | ```bash 103 | tailwindo --framework=bootstrap demo.html 104 | ``` 105 | 106 | --- 107 | 108 |

109 | Prev: < Installation 110 |

111 | -------------------------------------------------------------------------------- /tailwindo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | register('tailwindo') 20 | 21 | ->addArgument('arg', InputArgument::OPTIONAL, 'a file path/a folder path/Bootstrap CSS classes') 22 | 23 | ->addOption('replace', null, InputOption::VALUE_REQUIRED, 'This will overwrite the original file.', false) 24 | 25 | ->addOption('components', null, InputOption::VALUE_REQUIRED, 'Extract changes as components to a separate css file in the current directory.', false) 26 | 27 | ->addOption('recursive', 'r', InputOption::VALUE_OPTIONAL, 'This will recurs through all directories under the main directory', false) 28 | 29 | ->addOption('extensions', 'e', InputOption::VALUE_REQUIRED, 'This allows for custom extensions', 'php,html') 30 | 31 | ->addOption('framework', 't', InputOption::VALUE_REQUIRED, 'CSS Framework type to convert', 'bootstrap') 32 | 33 | ->addOption('prefix', 'p', InputOption::VALUE_REQUIRED, 'This allows you to add a custom prefix to all of Tailwind\'s generated utility classes', '') 34 | 35 | ->setCode(function (InputInterface $input, OutputInterface $output) { 36 | // output arguments and options 37 | $arg = trim($input->getFirstArgument()); 38 | 39 | if (empty($arg)) { 40 | $output->writeln('Oops! nothing to convert.'); 41 | return -1; 42 | } 43 | 44 | $acceptedExtensions = array_map('trim', array_map(function ($ext) { 45 | return trim($ext, '.'); 46 | }, array_filter(explode(',', $input->getOption('extensions')), function ($ext) { 47 | return !empty($ext); 48 | }))); 49 | 50 | $framework = strtolower($input->getOption('framework')); 51 | 52 | if (! class_exists('Awssat\\Tailwindo\\Framework\\' . ucfirst($framework).'Framework')) { 53 | $output->writeln("Oops! {$framework} is not supported!"); 54 | return -1; 55 | } 56 | 57 | $consoleHelper = new ConsoleHelper($output, [ 58 | 'recursive' => (bool) $input->getOption('recursive'), 59 | 'overwrite' => (bool) $input->getOption('replace'), 60 | 'extensions' => $acceptedExtensions, 61 | 'framework' => $framework, 62 | 'components' => (bool) $input->getOption('components'), 63 | 'prefix' => $input->getOption('prefix'), 64 | 'folderConvert' => is_dir($arg) 65 | ]); 66 | 67 | //file? 68 | if (is_file($arg)) { 69 | return $consoleHelper->fileConvert($arg); 70 | } 71 | 72 | //folder ? 73 | if (is_dir($arg)) { 74 | return $consoleHelper->folderConvert($arg); 75 | } 76 | 77 | //any html/css classes 78 | return $consoleHelper->codeConvert($arg); 79 | }) 80 | 81 | ->getApplication() 82 | 83 | ->setDefaultCommand('tailwindo', true) 84 | 85 | ->run(); 86 | -------------------------------------------------------------------------------- /src/ConsoleHelper.php: -------------------------------------------------------------------------------- 1 | converter = (new Converter()) 22 | ->setFramework($settings['framework'] ?? 'bootstrap') 23 | ->setGenerateComponents($settings['components'] ?? false) 24 | ->setPrefix($settings['prefix'] ?? ''); 25 | 26 | $this->output = $output; 27 | $this->recursive = $settings['recursive'] ?? false; 28 | $this->overwrite = $settings['overwrite'] ?? false; 29 | $this->extensions = $settings['extensions'] ?? 'php,html'; 30 | $this->components = $settings['components'] ?? false; 31 | $this->folderConvert = $settings['folderConvert'] ?? false; 32 | } 33 | 34 | public function folderConvert(string $folderPath) 35 | { 36 | [$frameworkVersion, $TailwindVersion] = $this->converter->getFramework()->supportedVersion(); 37 | 38 | $this->output->writeln('Converting Folder'.($this->components ? ' (extracted to tailwindo-components.css)' : '').': '.realpath($folderPath)); 39 | $this->output->writeln( 40 | 'Converting from '.$this->converter->getFramework()->frameworkName().' '. 41 | $frameworkVersion.' to Tailwind '.$TailwindVersion 42 | ); 43 | 44 | if ($this->recursive) { 45 | $iterator = new \RecursiveIteratorIterator( 46 | new \RecursiveDirectoryIterator( 47 | $folderPath, 48 | \RecursiveDirectoryIterator::SKIP_DOTS 49 | ), 50 | \RecursiveIteratorIterator::SELF_FIRST, 51 | \RecursiveIteratorIterator::CATCH_GET_CHILD 52 | ); 53 | } else { 54 | $iterator = new \DirectoryIterator($folderPath); 55 | } 56 | 57 | if ($this->folderConvert && $this->components) { 58 | $this->newComponentsFile(realpath($folderPath)); 59 | } 60 | 61 | foreach ($iterator as $_ => $directory) { 62 | $extensions = explode('.', $directory); 63 | $extension = end($extensions); 64 | if ($directory->isFile() && $this->isConvertibleFile($extension)) { 65 | $this->fileConvert($directory->getRealPath()); 66 | } 67 | } 68 | } 69 | 70 | public function fileConvert($filePath) 71 | { 72 | //just in case 73 | $filePath = realpath($filePath); 74 | 75 | if (!$this->folderConvert) { 76 | $this->output->writeln('Converting FIle: '.($this->components ? '(extracted to tailwindo-components.css)' : '').' '.$filePath); 77 | 78 | [$frameworkVersion, $TailwindVersion] = $this->converter->getFramework()->supportedVersion(); 79 | $this->output->writeln( 80 | 'Converting from '.$this->converter->getFramework()->frameworkName().' '. 81 | $frameworkVersion.' to Tailwind '.$TailwindVersion.PHP_EOL 82 | ); 83 | } 84 | 85 | if (!is_file($filePath)) { 86 | $this->output->writeln('Couldn\'t convert: '.basename($filePath)); 87 | 88 | return; 89 | } 90 | 91 | $content = file_get_contents($filePath); 92 | 93 | $lastDotPosition = strrpos($filePath, '.'); 94 | 95 | if ($lastDotPosition !== false && !$this->overwrite) { 96 | $newFilePath = substr_replace($filePath, '.tw', $lastDotPosition, 0); 97 | } elseif (!$this->overwrite) { 98 | $newFilePath = $filePath.'.tw'; 99 | } else { 100 | // Set the new path to the old path to make sure we overwrite it 101 | $newFilePath = $filePath; 102 | } 103 | 104 | $newContent = $this->converter 105 | ->setContent($content) 106 | ->convert() 107 | ->get($this->components); 108 | 109 | if ($content !== $newContent) { 110 | $this->output->writeln('processed: '.basename($newFilePath)); 111 | 112 | if ($this->components) { 113 | if (!$this->folderConvert) { 114 | $this->newComponentsFile(dirname($filePath)); 115 | } 116 | 117 | $this->writeComponentsToFile($newContent, dirname($filePath)); 118 | } else { 119 | file_put_contents($newFilePath, $newContent); 120 | } 121 | } else { 122 | $this->output->writeln('Nothing to convert: '.basename($filePath)); 123 | } 124 | } 125 | 126 | public function codeConvert(?string $code) 127 | { 128 | $convertedCode = $this->converter 129 | ->setContent($code) 130 | ->classesOnly(strpos($code, '<') === false && strpos($code, '>') === false) 131 | ->convert() 132 | ->get($this->components); 133 | 134 | if (!empty($convertedCode)) { 135 | $this->output->writeln('Converted Code: '.$convertedCode); 136 | } else { 137 | $this->output->writeln('Nothing generated! It means that TailwindCSS has no equivalent for that classes,'. 138 | 'or it has exactly classes with the same name.'); 139 | } 140 | } 141 | 142 | /** 143 | * Check whether a file is convertible or not based on its extension. 144 | */ 145 | protected function isConvertibleFile(string $extension): bool 146 | { 147 | return in_array($extension, $this->extensions); 148 | } 149 | 150 | protected function writeComponentsToFile($code, $path) 151 | { 152 | $cssFilePath = $path.'/tailwindo-components.css'; 153 | 154 | file_put_contents($cssFilePath, $code.PHP_EOL, FILE_APPEND); 155 | } 156 | 157 | protected function newComponentsFile($path) 158 | { 159 | $cssFilePath = $path.'/tailwindo-components.css'; 160 | 161 | if (file_exists($cssFilePath)) { 162 | unlink($cssFilePath); 163 | } 164 | 165 | file_put_contents($cssFilePath, '/** Auto-generated by Tailwindo: '.date('d-m-Y')." */\n\n"); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /tests/Bootstrap/SpacingTest.php: -------------------------------------------------------------------------------- 1 | converter = (new Converter())->setFramework('bootstrap'); 16 | } 17 | 18 | // https://getbootstrap.com/docs/4.0/utilities/spacing/ 19 | // https://tailwindcss.com/docs/padding/ 20 | 21 | /** 22 | * @group Padding 23 | */ 24 | 25 | /** @test */ 26 | public function padding_it_converts_on_all_sides() 27 | { 28 | $this->assertEquals( 29 | 'p-1', 30 | $this->converter->classesOnly(true)->setContent('p-1')->convert()->get() 31 | ); 32 | 33 | $this->assertEquals( 34 | 'sm:p-1', 35 | $this->converter->classesOnly(true)->setContent('p-sm-1')->convert()->get() 36 | ); 37 | 38 | $this->assertEquals( 39 | 'md:p-2', 40 | $this->converter->classesOnly(true)->setContent('p-md-2')->convert()->get() 41 | ); 42 | 43 | $this->assertEquals( 44 | 'lg:p-4', 45 | $this->converter->classesOnly(true)->setContent('p-lg-3')->convert()->get() 46 | ); 47 | 48 | $this->assertEquals( 49 | 'xl:p-6', 50 | $this->converter->classesOnly(true)->setContent('p-xl-4')->convert()->get() 51 | ); 52 | 53 | $this->assertEquals( 54 | 'xl:p-12', 55 | $this->converter->classesOnly(true)->setContent('p-xl-5')->convert()->get() 56 | ); 57 | } 58 | 59 | /** @test */ 60 | public function padding_it_converts_on_y() 61 | { 62 | $this->assertEquals( 63 | 'py-1', 64 | $this->converter->classesOnly(true)->setContent('py-1')->convert()->get() 65 | ); 66 | 67 | $this->assertEquals( 68 | 'sm:py-1', 69 | $this->converter->classesOnly(true)->setContent('py-sm-1')->convert()->get() 70 | ); 71 | 72 | $this->assertEquals( 73 | 'md:py-2', 74 | $this->converter->classesOnly(true)->setContent('py-md-2')->convert()->get() 75 | ); 76 | 77 | $this->assertEquals( 78 | 'lg:py-4', 79 | $this->converter->classesOnly(true)->setContent('py-lg-3')->convert()->get() 80 | ); 81 | 82 | $this->assertEquals( 83 | 'xl:py-6', 84 | $this->converter->classesOnly(true)->setContent('py-xl-4')->convert()->get() 85 | ); 86 | 87 | $this->assertEquals( 88 | 'xl:py-12', 89 | $this->converter->classesOnly(true)->setContent('py-xl-5')->convert()->get() 90 | ); 91 | } 92 | 93 | /** @test */ 94 | public function padding_it_converts_on_x() 95 | { 96 | $this->assertEquals( 97 | 'px-1', 98 | $this->converter->classesOnly(true)->setContent('px-1')->convert()->get() 99 | ); 100 | 101 | $this->assertEquals( 102 | 'sm:px-1', 103 | $this->converter->classesOnly(true)->setContent('px-sm-1')->convert()->get() 104 | ); 105 | 106 | $this->assertEquals( 107 | 'md:px-2', 108 | $this->converter->classesOnly(true)->setContent('px-md-2')->convert()->get() 109 | ); 110 | 111 | $this->assertEquals( 112 | 'lg:px-4', 113 | $this->converter->classesOnly(true)->setContent('px-lg-3')->convert()->get() 114 | ); 115 | 116 | $this->assertEquals( 117 | 'xl:px-6', 118 | $this->converter->classesOnly(true)->setContent('px-xl-4')->convert()->get() 119 | ); 120 | 121 | $this->assertEquals( 122 | 'xl:px-12', 123 | $this->converter->classesOnly(true)->setContent('px-xl-5')->convert()->get() 124 | ); 125 | } 126 | 127 | /** @test */ 128 | public function padding_it_converts_0_on_all_sides() 129 | { 130 | $this->assertEquals( 131 | 'p-0', 132 | $this->converter->classesOnly(true)->setContent('p-0')->convert()->get() 133 | ); 134 | 135 | $this->assertEquals( 136 | 'lg:py-0', 137 | $this->converter->classesOnly(true)->setContent('py-lg-0')->convert()->get() 138 | ); 139 | } 140 | 141 | /** 142 | * @group Margin 143 | */ 144 | 145 | /** @test */ 146 | public function margin_it_converts_on_all_sides() 147 | { 148 | $this->assertEquals( 149 | 'm-1', 150 | $this->converter->classesOnly(true)->setContent('m-1')->convert()->get() 151 | ); 152 | 153 | $this->assertEquals( 154 | 'sm:m-1', 155 | $this->converter->classesOnly(true)->setContent('m-sm-1')->convert()->get() 156 | ); 157 | 158 | $this->assertEquals( 159 | 'md:m-2', 160 | $this->converter->classesOnly(true)->setContent('m-md-2')->convert()->get() 161 | ); 162 | 163 | $this->assertEquals( 164 | 'lg:m-4', 165 | $this->converter->classesOnly(true)->setContent('m-lg-3')->convert()->get() 166 | ); 167 | 168 | $this->assertEquals( 169 | 'xl:m-6', 170 | $this->converter->classesOnly(true)->setContent('m-xl-4')->convert()->get() 171 | ); 172 | 173 | $this->assertEquals( 174 | 'xl:m-12', 175 | $this->converter->classesOnly(true)->setContent('m-xl-5')->convert()->get() 176 | ); 177 | } 178 | 179 | /** @test */ 180 | public function margin_it_converts_on_y() 181 | { 182 | $this->assertEquals( 183 | 'my-1', 184 | $this->converter->classesOnly(true)->setContent('my-1')->convert()->get() 185 | ); 186 | 187 | $this->assertEquals( 188 | 'sm:my-1', 189 | $this->converter->classesOnly(true)->setContent('my-sm-1')->convert()->get() 190 | ); 191 | 192 | $this->assertEquals( 193 | 'md:my-2', 194 | $this->converter->classesOnly(true)->setContent('my-md-2')->convert()->get() 195 | ); 196 | 197 | $this->assertEquals( 198 | 'lg:my-4', 199 | $this->converter->classesOnly(true)->setContent('my-lg-3')->convert()->get() 200 | ); 201 | 202 | $this->assertEquals( 203 | 'xl:my-6', 204 | $this->converter->classesOnly(true)->setContent('my-xl-4')->convert()->get() 205 | ); 206 | 207 | $this->assertEquals( 208 | 'xl:my-12', 209 | $this->converter->classesOnly(true)->setContent('my-xl-5')->convert()->get() 210 | ); 211 | } 212 | 213 | /** @test */ 214 | public function margin_it_converts_on_x() 215 | { 216 | $this->assertEquals( 217 | 'mx-1', 218 | $this->converter->classesOnly(true)->setContent('mx-1')->convert()->get() 219 | ); 220 | 221 | $this->assertEquals( 222 | 'sm:mx-1', 223 | $this->converter->classesOnly(true)->setContent('mx-sm-1')->convert()->get() 224 | ); 225 | 226 | $this->assertEquals( 227 | 'md:mx-2', 228 | $this->converter->classesOnly(true)->setContent('mx-md-2')->convert()->get() 229 | ); 230 | 231 | $this->assertEquals( 232 | 'lg:mx-4', 233 | $this->converter->classesOnly(true)->setContent('mx-lg-3')->convert()->get() 234 | ); 235 | 236 | $this->assertEquals( 237 | 'xl:mx-6', 238 | $this->converter->classesOnly(true)->setContent('mx-xl-4')->convert()->get() 239 | ); 240 | 241 | $this->assertEquals( 242 | 'xl:mx-12', 243 | $this->converter->classesOnly(true)->setContent('mx-xl-5')->convert()->get() 244 | ); 245 | } 246 | 247 | /** @test */ 248 | public function margin_it_converts_0_on_all_sides() 249 | { 250 | $this->assertEquals( 251 | 'm-0', 252 | $this->converter->classesOnly(true)->setContent('m-0')->convert()->get() 253 | ); 254 | 255 | $this->assertEquals( 256 | 'lg:my-0', 257 | $this->converter->classesOnly(true)->setContent('my-lg-0')->convert()->get() 258 | ); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/Converter.php: -------------------------------------------------------------------------------- 1 | setContent($content); 29 | } 30 | } 31 | 32 | public function setContent(string $content): self 33 | { 34 | $this->givenContent = $content; 35 | $this->lastSearches = []; 36 | $this->components = []; 37 | 38 | return $this; 39 | } 40 | 41 | public function setFramework(string $framework): self 42 | { 43 | $framework = 'Awssat\\Tailwindo\\Framework\\'.ucfirst($framework).'Framework'; 44 | 45 | $this->framework = new $framework(); 46 | 47 | return $this; 48 | } 49 | 50 | public function getFramework(): \Awssat\Tailwindo\Framework\Framework 51 | { 52 | return $this->framework; 53 | } 54 | 55 | /** 56 | * Is the given content a CSS content or HTML content. 57 | */ 58 | public function classesOnly(bool $value): self 59 | { 60 | $this->isCssClassesOnly = $value; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Is the given content a CSS content or HTML content. 67 | */ 68 | public function setGenerateComponents(bool $value): self 69 | { 70 | $this->generateComponents = $value; 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * The prefix option allows you to add a custom prefix to all of Tailwind's generated utility classes. This can be really useful when layering Tailwind on top of existing CSS where there might be naming conflicts. 77 | * 78 | * @param string $prefix 79 | * 80 | * @return Converter 81 | */ 82 | public function setPrefix(string $prefix): self 83 | { 84 | $prefix = trim($prefix); 85 | if (!empty($prefix)) { 86 | $this->prefix = $prefix; 87 | } 88 | 89 | return $this; 90 | } 91 | 92 | public function convert(): self 93 | { 94 | foreach ($this->getFramework()->get() as $item) { 95 | foreach ($item as $search => $replace) { 96 | $this->searchAndReplace($search, $replace); 97 | } 98 | } 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * Get the converted content. 105 | */ 106 | public function get($getComponents = false): string 107 | { 108 | if ($getComponents) { 109 | return $this->getComponents(); 110 | } 111 | 112 | $this->givenContent = preg_replace('/\{tailwindo\|([^\}]+)\}/', '$1', $this->givenContent); 113 | 114 | return $this->givenContent; 115 | } 116 | 117 | public function getComponents(): string 118 | { 119 | if (!$this->generateComponents) { 120 | return ''; 121 | } 122 | 123 | $result = ''; 124 | foreach ($this->components as $selector => $classes) { 125 | if ($selector == $classes) { 126 | continue; 127 | } 128 | 129 | $result .= ".{$selector} {\n\t@apply {$classes};\n}\n"; 130 | } 131 | 132 | return $result; 133 | } 134 | 135 | /** 136 | * Get the number of committed changes. 137 | */ 138 | public function changes(): int 139 | { 140 | return $this->changes; 141 | } 142 | 143 | /** 144 | * search for a word in the last searches. 145 | */ 146 | protected function isInLastSearches(string $searchFor, int $limit = 0): bool 147 | { 148 | $i = 0; 149 | 150 | foreach ($this->lastSearches as $search) { 151 | if (strpos($search, $searchFor) !== false) { 152 | return true; 153 | } 154 | 155 | if ($i++ >= $limit && $limit > 0) { 156 | return false; 157 | } 158 | } 159 | 160 | return false; 161 | } 162 | 163 | protected function addToLastSearches($search) 164 | { 165 | $this->changes++; 166 | 167 | $search = stripslashes($search); 168 | 169 | if ($this->isInLastSearches($search)) { 170 | return; 171 | } 172 | 173 | $this->lastSearches[] = $search; 174 | 175 | if (count($this->lastSearches) >= 50) { 176 | array_shift($this->lastSearches); 177 | } 178 | } 179 | 180 | /** 181 | * Search the given content and replace. 182 | * 183 | * @param string $search 184 | * @param string|\Closure $replace 185 | */ 186 | protected function searchAndReplace($search, $replace): void 187 | { 188 | if ($replace instanceof \Closure) { 189 | $callableReplace = \Closure::bind($replace, $this, self::class); 190 | $replace = $callableReplace(); 191 | } 192 | 193 | $regexStart = !$this->isCssClassesOnly ? '(?class(?:Name)?\s*=\s*(?["\'])((?!\k).)*)' : '(?\s*)'; 194 | $regexEnd = !$this->isCssClassesOnly ? '(?((?!\k).)*\k)' : '(?\s*)'; 195 | 196 | $search = preg_quote($search); 197 | 198 | $currentSubstitute = 0; 199 | 200 | while (true) { 201 | if (strpos($search, '\{regex_string\}') !== false || strpos($search, '\{regex_number\}') !== false) { 202 | $currentSubstitute++; 203 | foreach (['regex_string'=> '[a-zA-Z0-9]+', 'regex_number' => '[0-9]+'] as $regeName => $regexValue) { 204 | $regexMatchCount = preg_match_all('/\\\\?\{'.$regeName.'\\\\?\}/', $search); 205 | $search = preg_replace('/\\\\?\{'.$regeName.'\\\\?\}/', '(?<'.$regeName.'_'.$currentSubstitute.'>'.$regexValue.')', $search, 1); 206 | $replace = preg_replace('/\\\\?\{'.$regeName.'\\\\?\}/', '${'.$regeName.'_'.$currentSubstitute.'}', $replace, $regexMatchCount > 1 ? 1 : -1); 207 | } 208 | 209 | continue; 210 | } 211 | 212 | break; 213 | } 214 | 215 | if (!preg_match_all('/'.$regexStart.'(?(?givenContent, $matches, PREG_SET_ORDER)) { 216 | return; 217 | } 218 | 219 | foreach ($matches as $match) { 220 | $result = preg_replace_callback( 221 | '/(?(?generateComponents && !in_array($match['given'], $this->components)) { 228 | $this->components[$match['given']] = preg_replace('/\{tailwindo\|([^\}]+)\}/', '$1', $replace); 229 | } 230 | 231 | if ($this->prefix) { 232 | $arr = explode(' ', $replace); 233 | $arr = array_map(function ($class) { 234 | $responsiveOrStatePrefix = substr($class, 0, strpos($class, ':')); 235 | if ($responsiveOrStatePrefix) { 236 | $utilityName = str_replace($responsiveOrStatePrefix.':', '', $class); 237 | 238 | return "{$responsiveOrStatePrefix}:{$this->prefix}{$utilityName}"; 239 | } elseif ($class) { 240 | return "{$this->prefix}{$class}"; 241 | } 242 | 243 | return $class; 244 | }, $arr); 245 | $arr = array_filter($arr); 246 | 247 | return trim(implode(' ', $arr)); 248 | } 249 | 250 | return $replace; 251 | }, 252 | $match[0] 253 | ); 254 | 255 | if (strcmp($match[0], $result) !== 0) { 256 | if ($count = preg_match_all('/\{tailwindo\|.*?\}/', $result)) { 257 | if ($count > 1) { 258 | $result = preg_replace('/\{tailwindo\|.*?\}/', '', $result, $count - 1); 259 | } 260 | } 261 | 262 | $this->givenContent = str_replace($match[0], $result, $this->givenContent); 263 | $this->addToLastSearches($search); 264 | } 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/Framework/BootstrapFramework.php: -------------------------------------------------------------------------------- 1 | 'sm', // < 575px 10 | 'sm' => 'sm', // >=576px 11 | 'md' => 'md', // >=768px 12 | 'lg' => 'lg', // >=992px 13 | 'xl' => 'xl', // >=120px 14 | 'print' => 'print', 15 | ]; 16 | 17 | // https://getbootstrap.com/docs/4.6/utilities/spacing/ 18 | protected $spacings = [ 19 | '0' => '0', 20 | '1' => '1', 21 | '2' => '2', 22 | '3' => '4', 23 | '4' => '6', 24 | '5' => '12', 25 | 'auto' => 'auto', 26 | ]; 27 | 28 | protected $grid = [ 29 | '1' => '1/6', 30 | '2' => '1/5', 31 | '3' => '1/4', 32 | '4' => '1/3', 33 | '5' => '2/5', 34 | '6' => '1/2', 35 | '7' => '3/5', 36 | '8' => '2/3', 37 | '9' => '3/4', 38 | '10' => '4/5', 39 | '11' => '5/6', 40 | '12' => 'full', 41 | ]; 42 | 43 | protected $colors = [ 44 | 'primary' => 'blue-600', 45 | 'secondary' => 'gray-600', 46 | 'success' => 'green-500', 47 | 'danger' => 'red-600', 48 | 'warning' => 'yellow-500', 49 | 'info' => 'teal-500', 50 | 'light' => 'gray-100', 51 | 'dark' => 'gray-900', 52 | 'white' => 'white', 53 | 'muted' => 'gray-700', 54 | ]; 55 | 56 | public function frameworkName(): string 57 | { 58 | return 'Bootstrap'; 59 | } 60 | 61 | public function supportedVersion(): array 62 | { 63 | /** 64 | * latest versions of Bootstrap/Tailwind during the coding of this file. 65 | */ 66 | return [ 67 | '4.6.2', // bootstrap 68 | '4.1.3', // tailwind 69 | ]; 70 | } 71 | 72 | /** 73 | * This is the default css classes to be added to your main css file for compatibility. 74 | */ 75 | public function defaultCSS(): array 76 | { 77 | return [ 78 | //https://getbootstrap.com/docs/4.6/content/reboot/ 79 | 'h1' => '', 80 | //... 81 | 'fieldset' => '', 82 | 83 | //https://getbootstrap.com/docs/4.6/content/typography/ 84 | 'del' => '', 85 | //.. 86 | 'a' => '', 87 | 'p' => '', 88 | ]; 89 | } 90 | 91 | /** 92 | * .get all convertible items. 93 | */ 94 | public function get(): \Generator 95 | { 96 | foreach ([ 97 | 'general', 98 | 'grid', 99 | 'borders', 100 | 'mediaObject', 101 | 'colors', 102 | 'display', 103 | 'sizing', 104 | 'flexElements', 105 | 'spacing', 106 | 'text', 107 | 'floats', 108 | 'positioning', 109 | 'visibility', 110 | 'alerts', 111 | 'verticalAlignment', 112 | 'badges', 113 | 'breadcrumb', 114 | 'buttons', 115 | 'cards', 116 | 'dropdowns', 117 | 'forms', 118 | 'inputGroups', 119 | 'listGroups', 120 | 'modals', 121 | 'navs', 122 | 'pagination', 123 | ] as $component) { 124 | yield $this->$component(); 125 | } 126 | } 127 | 128 | protected function general(): array 129 | { 130 | $mainClasses = [ 131 | 'container-fluid' => 'container max-w-full mx-auto sm:px-4', 132 | 'container' => function () { 133 | if ($this->isInLastSearches('jumbotron', 1)) { 134 | return 'container mx-auto max-w-2xl sm:px-4'; 135 | } 136 | 137 | return 'container mx-auto sm:px-4'; 138 | }, 139 | 140 | //https://getbootstrap.com/docs/4.6/utilities/embed/ 141 | 'embed-responsive' => '', 142 | 'embed-responsive-item' => '', 143 | 'embed-responsive-21by9' => '', 144 | 'embed-responsive-16by9' => '', 145 | 'embed-responsive-4by3' => '', 146 | 'embed-responsive-1by1' => '', 147 | 148 | // https://getbootstrap.com/docs/4.6/utilities/image-replacement/ 149 | 'text-hide' => '', 150 | 151 | // https://getbootstrap.com/docs/4.6/utilities/screenreaders/ 152 | 'sr-only' => 'sr-only', 153 | 'sr-only-focusable' => 'focus:not-sr-only', 154 | 155 | // https://getbootstrap.com/docs/4.6/content/images/ 156 | 'img-fluid' => 'max-w-full h-auto', 157 | 'img-thumbnail' => 'max-w-full h-auto border-1 border-gray-200 rounded-sm p-1', 158 | 159 | //https://getbootstrap.com/docs/4.6/content/tables/ 160 | 'table' => 'w-full max-w-full mb-4 bg-transparent', 161 | 'table-sm' => 'p-1', 162 | // 'table-bordered' => '', 163 | // 'table-striped' => '', // scrolling-touch was removed in v2.0 164 | // see "The scrolling-touch and scrolling-auto utilities have been removed" 165 | 'table-responsive' => 'block w-full overflow-auto', 166 | 'table-responsive-{regex_string}' => 'block w-full overflow-auto', 167 | 168 | //https://getbootstrap.com/docs/4.6/content/figures/ 169 | 'figure' => 'inline-block mb-4', 170 | 'figure-img' => 'mb-2 leading-none', 171 | 'figure-caption' => 'text-gray-', 172 | 173 | 'fade' => 'opacity-0', 174 | 'show' => 'opacity-100 block', //need to be checked 175 | 'disabled' => 'opacity-75', 176 | 177 | //https://getbootstrap.com/docs/4.6/components/collapse/ 178 | // 'collapse' => 'hidden', 179 | 'collapsing' => 'relative h-0 overflow-hidden ', //there should be a h-0 180 | 181 | //https://getbootstrap.com/docs/4.6/utilities/close-icon/ 182 | 'close' => 'absolute top-0 bottom-0 right-0 px-4 py-3', 183 | 184 | //https://getbootstrap.com/docs/4.6/components/jumbotron/ 185 | 'jumbotron' => 'py-8 px-4 md:py-16 md:px-8 mb-8 bg-gray-200 rounded-sm', 186 | 'jumbotron-fluid' => 'pr-0 pl-0 rounded-none', 187 | ]; 188 | 189 | $mainClassesEachScreen = [ 190 | 'container-{screen}' => 'container min-w-{screen} mx-auto sm:px-4', 191 | ]; 192 | 193 | $items = []; 194 | foreach ($mainClasses as $btClass => $twClass) { 195 | $items[$btClass] = $twClass; 196 | } 197 | 198 | foreach ($mainClassesEachScreen as $btClass => $twClass) { 199 | foreach ($this->mediaOptions as $btMedia => $twMedia) { 200 | $items[str_replace('{screen}', $btMedia, $btClass)] = str_replace('{screen}', $twMedia, $twClass); 201 | } 202 | } 203 | 204 | return $items; 205 | } 206 | 207 | protected function grid(): array 208 | { 209 | $items = [ 210 | 'row' => 'flex flex-wrap ', 211 | 'col' => 'relative grow max-w-full flex-1 px-4', 212 | ]; 213 | 214 | //col-(xs|sm|md|lg|xl) = (sm|md|lg|xl):grow 215 | //ml-(xs|sm|md|lg|xl)-auto = (sm|md|lg|xl):mx-auto:ml-auto 216 | //mr-(xs|sm|md|lg|xl)-auto = (sm|md|lg|xl):mx-auto:mr-auto 217 | foreach ($this->mediaOptions as $btMedia => $twMedia) { 218 | $items['col-'.$btMedia] = 'relative '.$twMedia.':grow '.$twMedia.':flex-1'; 219 | $items['ml-'.$btMedia.'-auto'] = $twMedia.':ml-auto'; 220 | $items['mr-'.$btMedia.'-auto'] = $twMedia.':mr-auto'; 221 | 222 | //col-btElem 223 | //col-(xs|sm|md|lg|xl)-btElem = (sm|md|lg|xl):w-twElem 224 | //offset-(xs|sm|md|lg|xl)-btElem = (sm|md|lg|xl):mx-auto 225 | foreach ($this->grid as $btElem => $twElem) { 226 | if ($btMedia === 'xs') { 227 | $items['col-'.$btElem] = 'w-'.$twElem; 228 | } 229 | 230 | $items['col-'.$btMedia.'-'.$btElem] = $twMedia.':w-'.$twElem.' pr-4 pl-4'; 231 | 232 | //might work :) 233 | $items['offset-'.$btMedia.'-'.$btElem] = $twMedia.':mx-'.$twElem; 234 | } 235 | } 236 | 237 | return $items; 238 | } 239 | 240 | protected function mediaObject(): array 241 | { 242 | //https://getbootstrap.com/docs/4.6/layout/media-object/ 243 | return [ 244 | 'media' => 'flex items-start', 245 | 'media-body' => 'flex-1', 246 | ]; 247 | } 248 | 249 | protected function borders(): array 250 | { 251 | $items = []; 252 | 253 | foreach ([ 254 | 'top' => 't', 255 | 'right' => 'r', 256 | 'bottom' => 'b', 257 | 'left' => 'l', 258 | ] as $btSide => $twSide) { 259 | $items['border-'.$btSide] = 'border-'.$twSide; 260 | $items['border-'.$btSide.'-0'] = 'border-'.$twSide.'-0'; 261 | } 262 | 263 | foreach ($this->colors as $btColor => $twColor) { 264 | $items['border-'.$btColor] = 'border-'.$twColor; 265 | } 266 | 267 | foreach ([ 268 | 'top' => 't', 269 | 'right' => 'r', 270 | 'bottom' => 'b', 271 | 'left' => 'l', 272 | 'circle' => 'full', 273 | 'pill' => 'full py-2 px-4', 274 | '0' => 'none', 275 | ] as $btStyle => $twStyle) { 276 | $items['rounded-'.$btStyle] = 'rounded-'.$twStyle; 277 | } 278 | 279 | return $items; 280 | } 281 | 282 | protected function colors(): array 283 | { 284 | $items = []; 285 | 286 | foreach ($this->colors as $btColor => $twColor) { 287 | $items['text-'.$btColor] = 'text-'.$twColor; 288 | $items['bg-'.$btColor] = 'bg-'.$twColor; 289 | $items['table-'.$btColor] = 'bg-'.$twColor; 290 | // $items['bg-gradient-'.$btColor] = 'bg-'.$twColor; 291 | } 292 | 293 | return $items; 294 | } 295 | 296 | protected function display(): array 297 | { 298 | //.d-none 299 | //.d-{sm,md,lg,xl}-none 300 | $items = []; 301 | 302 | foreach ([ 303 | 'none' => 'hidden', 304 | 'inline' => 'inline', 305 | 'inline-block' => 'inline-block', 306 | 'block' => 'block', 307 | 'table' => 'table', 308 | 'table-cell' => 'table-cell', 309 | 'table-row' => 'table-row', 310 | 'flex' => 'flex', 311 | 'inline-flex' => 'inline-flex', 312 | ] as $btElem => $twElem) { 313 | $items['d-'.$btElem] = $twElem; 314 | 315 | foreach ($this->mediaOptions as $btMedia => $twMedia) { 316 | $items['d-'.$btMedia.'-'.$btElem] = $twMedia.':'.$twElem; 317 | } 318 | } 319 | 320 | return $items; 321 | } 322 | 323 | protected function flexElements(): array 324 | { 325 | $items = []; 326 | 327 | foreach (array_merge($this->mediaOptions, [''=>'']) as $btMedia => $twMedia) { 328 | foreach (['row', 'row-reverse', 'column', 'column-reverse'] as $key) { 329 | $items['flex'.(empty($btMedia) ? '' : '-').$btMedia.'-'.$key] = (empty($twMedia) ? '' : $twMedia.':').'flex-'.str_replace('column', 'col', $key); 330 | } 331 | 332 | foreach (['grow-0', 'grow-1', 'shrink-0', 'shrink-1'] as $key) { 333 | $items['flex'.(empty($btMedia) ? '' : '-').$btMedia.'-'.$key] = (empty($twMedia) ? '' : $twMedia.':').'flex-'.str_replace('-1', '', $key); 334 | } 335 | 336 | foreach (['start', 'end', 'center', 'between', 'around'] as $key) { 337 | $items['justify-content'.(empty($btMedia) ? '' : '-').$btMedia.'-'.$key] = (empty($twMedia) ? '' : $twMedia.':').'justify-'.$key; 338 | } 339 | 340 | foreach (['start', 'end', 'center', 'stretch', 'baseline'] as $key) { 341 | $items['align-items'.(empty($btMedia) ? '' : '-').$btMedia.'-'.$key] = (empty($twMedia) ? '' : $twMedia.':').'items-'.$key; 342 | } 343 | 344 | foreach (['start', 'end', 'center', 'stretch', 'baseline'] as $key) { 345 | $items['align-content'.(empty($btMedia) ? '' : '-').$btMedia.'-'.$key] = (empty($twMedia) ? '' : $twMedia.':').'content-'.$key; 346 | } 347 | 348 | foreach (['start', 'end', 'center', 'stretch', 'baseline'] as $key) { 349 | $items['align-self'.(empty($btMedia) ? '' : '-').$btMedia.'-'.$key] = (empty($twMedia) ? '' : $twMedia.':').'self-'.$key; 350 | } 351 | 352 | $items['flex'.(empty($btMedia) ? '' : '-').$btMedia.'-wrap'] = (empty($twMedia) ? '' : $twMedia.':').'flex-wrap'; 353 | $items['flex'.(empty($btMedia) ? '' : '-').$btMedia.'-wrap-reverse'] = (empty($twMedia) ? '' : $twMedia.':').'flex-wrap-reverse'; 354 | $items['flex'.(empty($btMedia) ? '' : '-').$btMedia.'-nowrap'] = (empty($twMedia) ? '' : $twMedia.':').'flex-nowrap'; 355 | 356 | $items['flex'.(empty($btMedia) ? '' : '-').$btMedia.'-nowrap'] = (empty($twMedia) ? '' : $twMedia.':').'flex-nowrap'; 357 | 358 | if ($btMedia != '') { 359 | $items['order-'.$btMedia.'-{regex_number}'] = $twMedia.':order-{regex_number}'; 360 | } 361 | } 362 | 363 | return $items; 364 | } 365 | 366 | protected function sizing(): array 367 | { 368 | $items = []; 369 | 370 | foreach ([ 371 | '25' => '1/4', 372 | '50' => '1/2', 373 | '75' => '3/4', 374 | '100' => 'full', 375 | ] as $btClass => $twClass) { 376 | $items['w-'.$btClass] = 'w-'.$twClass; 377 | 378 | //no percentages in TW for heights except for full 379 | if ($btClass == 100) { 380 | $items['h-'.$btClass] = 'h-'.$twClass; 381 | } 382 | } 383 | 384 | $items['mw-100'] = 'max-w-full'; 385 | $items['mh-100'] = 'max-h-full'; 386 | 387 | return $items; 388 | } 389 | 390 | protected function spacing(): array 391 | { 392 | $items = []; 393 | $spacingProperties = ['p', 'm']; 394 | 395 | foreach ($spacingProperties as $property) { 396 | foreach ($this->spacings as $btSpacing => $twSpacing) { 397 | $items[$property.'-'.$btSpacing] = $property.'-'.$twSpacing; 398 | } 399 | } 400 | 401 | foreach ($spacingProperties as $property) { 402 | foreach ($this->mediaOptions as $btMedia => $twMedia) { 403 | foreach ($this->spacings as $btSpacing => $twSpacing) { 404 | $items[$property.'-'.$btMedia.'-'.$btSpacing] = $twMedia.':'.$property.'-'.$twSpacing; 405 | $items[$property.'{regex_string}-'.$btMedia.'-'.$btSpacing] = $twMedia.':'.$property.'{regex_string}-'.$twSpacing; 406 | } 407 | 408 | $items[$property.'{regex_string}-'.$btMedia.'-auto'] = $twMedia.':'.$property.'{regex_string}-auto'; 409 | } 410 | } 411 | 412 | return $items; 413 | } 414 | 415 | protected function text(): array 416 | { 417 | $items = [ 418 | 'text-nowrap' => 'whitespace-nowrap', 419 | 'text-truncate' => 'truncate', 420 | 421 | 'text-lowercase' => 'lowercase', 422 | 'text-uppercase' => 'uppercase', 423 | 'text-capitalize' => 'capitalize', 424 | 425 | // https://getbootstrap.com/docs/4.6/content/typography/#abbreviations 426 | 'initialism' => 'text-xs uppercase', 427 | 'lead' => 'text-xl font-light', 428 | 'small' => 'text-xs', 429 | 'mark' => '', 430 | 'display-1' => 'text-xl', 431 | 'display-2' => 'text-2xl', 432 | 'display-3' => 'text-3xl', 433 | 'display-4' => 'text-4xl', 434 | 435 | // https://v3.tailwindcss.com/docs/line-height 436 | 'h-1' => 'mb-2 font-medium leading-[1.25] text-4xl', 437 | 'h-2' => 'mb-2 font-medium leading-[1.25] text-3xl', 438 | 'h-3' => 'mb-2 font-medium leading-[1.25] text-2xl', 439 | 'h-4' => 'mb-2 font-medium leading-[1.25] text-xl', 440 | 'h-5' => 'mb-2 font-medium leading-[1.25] text-lg', 441 | 'h-6' => 'mb-2 font-medium leading-[1.25] text-base', 442 | 443 | 'blockquote' => 'mb-6 text-lg', 444 | 'blockquote-footer' => 'block text-gray-', 445 | 446 | 'font-weight-bold' => 'font-bold', 447 | 'font-weight-normal' => 'font-normal', 448 | 'font-weight-300' => 'font-light', 449 | 'font-italic' => 'italic', 450 | ]; 451 | 452 | foreach (['left', 'right', 'center', 'justify'] as $alignment) { 453 | foreach (array_merge($this->mediaOptions, [''=>'']) as $btMedia => $twMedia) { 454 | $items['text'.(empty($btMedia) ? '' : '-'.$btMedia).'-'.$alignment] = (empty($twMedia) ? '' : $twMedia.':').'text-'.$alignment; 455 | } 456 | } 457 | 458 | return $items; 459 | } 460 | 461 | protected function floats(): array 462 | { 463 | $items = []; 464 | 465 | foreach ($this->mediaOptions as $btMedia => $twMedia) { 466 | foreach (['left', 'right', 'none'] as $alignment) { 467 | $items['float-'.$btMedia.'-'.$alignment] = $twMedia.':float-'.$alignment; 468 | } 469 | } 470 | 471 | return $items; 472 | } 473 | 474 | protected function positioning(): array 475 | { 476 | $items = []; 477 | //https://getbootstrap.com/docs/4.6/utilities/position 478 | foreach ([ 479 | 'position-static' => 'static', 480 | 'position-relative' => 'relative', 481 | 'position-absolute' => 'absolute', 482 | 'position-fixed' => 'fixed', 483 | 'position-sticky' => 'sticky', 484 | 'fixed-top' => 'top-0', 485 | 'fixed-bottom' => 'bottom-0', 486 | ] as $btPosition => $twPosition) { 487 | $items[$btPosition] = $twPosition; 488 | } 489 | 490 | return $items; 491 | } 492 | 493 | protected function verticalAlignment(): array 494 | { 495 | //same 496 | $items = []; 497 | // foreach ([ 498 | // 'baseline', 'top', 'middle', 'bottom', 'text-top', 'text-bottom' 499 | // ] as $btAlign=> $twAlign) { 500 | // $items['align-'.$btAlign] = 'align-'.$twAlign; 501 | // } 502 | return $items; 503 | } 504 | 505 | protected function visibility(): array 506 | { 507 | //same 508 | return []; 509 | } 510 | 511 | protected function alerts() 512 | { 513 | $items = [ 514 | 'alert' => 'relative px-3 py-3 mb-4 border rounded-sm', 515 | 'alert-heading' => '', //color: inherit 516 | 'alert-link' => 'font-bold no-underline text-current', 517 | 'alert-dismissible' => '', 518 | ]; 519 | 520 | $colors = [ 521 | 'primary' => 'bg-blue-200 border-blue-300 text-blue-800', 522 | 'secondary' => 'bg-gray-300 border-gray-400 text-gray-800', 523 | 'success' => 'bg-green-200 border-green-300 text-green-800', 524 | 'danger' => 'bg-red-200 border-red-300 text-red-800', 525 | 'warning' => 'bg-orange-200 border-orange-300 text-orange-800', 526 | 'info' => 'bg-teal-200 border-teal-300 text-teal-800', 527 | 'light' => 'bg-white text-gray-600', 528 | 'dark' => 'bg-gray-400 border-gray-500 text-gray-900', 529 | ]; 530 | 531 | foreach ($colors as $btColor => $twColor) { 532 | $items['alert-'.$btColor] = $twColor; 533 | } 534 | 535 | return $items; 536 | } 537 | 538 | protected function badges(): array 539 | { 540 | $items = [ 541 | 'badge' => 'inline-block p-1 text-center font-semibold text-sm align-baseline leading-none rounded-sm', 542 | 'badge-pill' => 'rounded-full py-1 px-3', 543 | ]; 544 | 545 | $colors = [ 546 | 'primary' => 'bg-blue-500 text-white hover:bg-blue-600', 547 | 'secondary' => 'bg-gray-600 text-white hover:bg-gray-700', 548 | 'success' => 'bg-green-500 text-white hover:green-600', 549 | 'danger' => 'bg-red-600 text-white hover:bg-red-700', 550 | 'warning' => 'bg-orange-400 text-black hover:bg-orange-500', 551 | 'info' => 'bg-teal-500 text-white hover:bg-teal-600', 552 | 'light' => 'bg-gray-100 text-gray-800 hover:bg-gray-200', 553 | 'dark' => 'bg-gray-900 text-white', 554 | ]; 555 | 556 | foreach ($colors as $btColor => $twColor) { 557 | $items['badge-'.$btColor] = $twColor; 558 | } 559 | 560 | return $items; 561 | } 562 | 563 | protected function breadcrumb(): array 564 | { 565 | return [ 566 | 'breadcrumb' => 'flex flex-wrap list-none pt-3 pb-3 py-4 px-4 mb-4 bg-gray-200 rounded-sm', 567 | 'breadcrumb-item'=> 'inline-block px-2 py-2 text-gray-700', 568 | ]; 569 | } 570 | 571 | protected function buttons(): array 572 | { 573 | $items = [ 574 | 'btn' => 'inline-block align-middle text-center select-none border font-normal whitespace-nowrap rounded-sm {tailwindo|py-1 px-3 leading-none} no-underline', 575 | 'btn-group' => 'relative inline-flex align-middle', 576 | 'btn-group-vertical' => 'relative inline-flex align-middle flex-col items-start justify-center', 577 | 'btn-toolbar' => 'flex flex-wrap justify-start', 578 | 'btn-link' => 'font-normal text-blue-700 bg-transparent', 579 | 'btn-block' => 'block w-full', 580 | ]; 581 | 582 | foreach ([ 583 | 'sm' => '{tailwindo|py-1 px-2 leading-[1.25]} text-xs ', 584 | 'lg' => '{tailwindo|py-3 px-4 leading-[1.25]} text-xl', 585 | ] as $btMedia => $twClasses) { 586 | $items['btn-'.$btMedia] = $twClasses; 587 | $items['btn-group-'.$btMedia] = $twClasses; 588 | } 589 | 590 | $colors = [ 591 | 'primary' => 'bg-blue-600 text-white hover:bg-blue-600', 592 | 'secondary' => 'bg-gray-600 text-white hover:bg-gray-700', 593 | 'success' => 'bg-green-500 text-white hover:bg-green-600', 594 | 'danger' => 'bg-red-600 text-white hover:bg-red-700', 595 | 'warning' => 'bg-orange-400 text-black hover:bg-orange-500', 596 | 'info' => 'bg-teal-500 text-white hover:bg-teal-600', 597 | 'light' => 'bg-gray-100 text-gray-800 hover:bg-gray-200', 598 | 'dark' => 'bg-gray-900 text-white hover:bg-gray-900', 599 | ]; 600 | 601 | foreach ($colors as $btColor => $twColor) { 602 | $items['btn-'.$btColor] = $twColor; 603 | $items['btn-outline-'.$btColor] = preg_replace_callback('/(? 'flex flex-row flex-wrap md:flex-nowrap -mx-1', 621 | 'card-group' => 'flex flex-col', 622 | 'card' => function () { 623 | if ($this->isInLastSearches('card-deck')) { 624 | return 'relative block md:flex w-full md:min-w-0 md:mx-4 flex-col shrink-0 grow rounded-sm break-words border bg-white border-1 border-gray-300'; 625 | } else { 626 | return 'relative flex flex-col min-w-0 rounded-sm break-words border bg-white border-1 border-gray-300'; 627 | } 628 | }, 629 | 'card-body' => 'flex-auto p-6', 630 | 'card-title' => 'mb-3', 631 | 'card-text' => 'mb-0', 632 | 'card-subtitle' => '-mt-2 mb-0', 633 | 'card-link' => 'ml-6', 634 | 'card-header' => 'py-3 px-6 mb-0 bg-gray-200 border-b-1 border-gray-300 text-gray-900', 635 | 'card-footer' => 'py-3 px-6 bg-gray-200 border-t-1 border-gray-300', 636 | 'card-header-tabs' => 'border-b-0 -ml-2 -mb-3', 637 | 'card-header-pills' => '-ml-3 -mr-3', 638 | 'card-img-overlay' => 'absolute inset-y-0 inset-x-0 p-6', 639 | 'card-img' => 'w-full rounded-sm', 640 | 'card-img-top' => 'w-full rounded-sm rounded-t', 641 | 'card-img-bottom' => 'w-full rounded-sm rounded-b', 642 | ]; 643 | } 644 | 645 | protected function dropdowns(): array 646 | { 647 | return [ 648 | 'dropdown' => 'relative', 649 | 'dropup' => 'relative', 650 | 'dropdown-toggle' => ' inline-block w-0 h-0 ml-1 align border-b-0 border-t-1 border-r-1 border-l-1', 651 | 'dropdown-menu' => ' absolute left-0 z-50 float-left hidden list-none py-2 mt-1 text-base bg-white border border-gray-300 rounded-sm', 652 | 'dropdown-divider' => 'h-0 my-2 overflow-hidden border-t-1 border-gray-300', 653 | 'dropdown-item' => 'block w-full py-1 px-6 font-normal text-gray-900 whitespace-nowrap border-0', 654 | 'dropdown-header' => 'block py-2 px-6 mb-0 text-sm text-gray-800 whitespace-nowrap', 655 | ]; 656 | } 657 | 658 | protected function forms(): array 659 | { 660 | return [ 661 | 'form-group' => 'mb-4', 662 | 'form-control' => 'block appearance-none w-full py-1 px-2 mb-1 text-base leading-none bg-white text-gray-800 border border-gray-200 rounded-sm', 663 | 'form-control-lg' => 'py-2 px-4 text-lg leading-none rounded-sm', 664 | 'form-control-sm' => 'py-1 px-2 text-sm leading-none rounded-sm', 665 | 'form-control-file' => 'block appearance-none', 666 | 'form-control-range' => 'block appearance-none', 667 | 668 | 'form-inline' => 'flex items-center', 669 | 670 | 'col-form-label' => 'pt-2 pb-2 mb-0 leading-none', 671 | 'col-form-label-lg' => 'pt-3 pb-3 mb-0 leading-none', 672 | 'col-form-label-sm' => 'pt-1 pb-1 mb-0 leading-none', 673 | 674 | 'col-form-legend' => 'pt-2 pb-2 mb-0 text-base', 675 | 'col-form-plaintext' => 'pt-2 pb-2 mb-0 leading-none bg-transparent border-transparent border-r-0 border-l-0 border-t border-b', 676 | 677 | 'form-text' => 'block mt-1', 678 | 'form-row' => 'flex flex-wrap -mr-1 -ml-1', 679 | 'form-check' => 'relative block mb-2', 680 | 'form-check-label' => 'text-gray-700 pl-6 mb-0', 681 | 'form-check-input' => 'absolute mt-1 -ml-6', 682 | 683 | 'form-check-inline' => 'inline-block mr-2', 684 | 'valid-feedback' => 'hidden mt-1 text-sm text-green', 685 | 'valid-tooltip' => 'absolute z-10 hidden w-4 font-normal leading-none text-white rounded-sm p-2 bg-green-700', 686 | 'is-valid' => 'bg-green-700', 687 | 'invalid-feedback' => 'hidden mt-1 text-sm text-red', 688 | 'invalid-tooltip' => 'absolute z-10 hidden w-4 font-normal leading-none text-white rounded-sm p-2 bg-red-700', 689 | 'is-invalid' => 'bg-red-700', 690 | ]; 691 | } 692 | 693 | protected function inputGroups(): array 694 | { 695 | return [ 696 | 'input-group' => 'relative flex items-stretch w-full', 697 | 'input-group-addon' => 'py-1 px-2 mb-1 text-base font-normal leading-none text-gray-900 text-center bg-gray-300 border border-4 border-gray-100 rounded-sm', 698 | 'input-group-addon-lg' => 'py-2 px-3 mb-0 text-lg', 699 | 'input-group-addon-sm' => 'py-3 px-4 mb-0 text-lg', 700 | ]; 701 | } 702 | 703 | protected function listGroups(): array 704 | { 705 | $items = [ 706 | 'list-group' => 'flex flex-col pl-0 mb-0 border rounded-sm border-gray-300', 707 | 'list-group-item-action' => 'w-full', 708 | 'list-group-item' => 'relative block py-3 px-6 -mb-px border border-r-0 border-l-0 border-gray-300 no-underline', 709 | 'list-group-flush' => '', 710 | ]; 711 | 712 | //TODO 713 | foreach ($this->colors as $btColor => $twColor) { 714 | if ($btColor === 'dark') { 715 | $items['list-group-item-'.$btColor] = 'text-white bg-gray-700'; 716 | } elseif ($btColor == 'light') { 717 | $items['list-group-item-'.$btColor] = 'text-black bg-gray-200'; 718 | } else { 719 | $items['list-group-item-'.$btColor] = 'bg-'.$twColor.'-200 text-'.$twColor.'-900'; 720 | } 721 | } 722 | 723 | return $items; 724 | } 725 | 726 | protected function modals(): array 727 | { 728 | //TODO 729 | return []; 730 | } 731 | 732 | protected function navs(): array 733 | { 734 | $items = [ 735 | 'nav' => 'flex flex-wrap list-none pl-0 mb-0', 736 | 'nav-tabs' => 'border border-t-0 border-r-0 border-l-0 border-b-1 border-gray-200', 737 | 'nav-pills' => '', 738 | 'nav-fill' => '', 739 | 'nav-justified' => '', 740 | ]; 741 | 742 | $items['nav-link'] = function () { 743 | $navLinkClasses = 'inline-block py-2 px-4 no-underline'; 744 | if ($this->isInLastSearches('nav-tabs', 5)) { 745 | $navLinkClasses .= ' border border-b-0 mx-1 rounded-sm rounded-t'; 746 | } elseif ($this->isInLastSearches('nav-pills', 5)) { 747 | $navLinkClasses .= ' border border-blue bg-blue rounded-sm text-white mx-1'; 748 | } 749 | 750 | return $navLinkClasses; 751 | }; 752 | 753 | $items['nav-item'] = function () { 754 | $navItemClasses = ''; 755 | 756 | if ($this->isInLastSearches('nav-tabs', 5)) { 757 | $navItemClasses .= '-mb-px'; 758 | } elseif ($this->isInLastSearches('nav-fill', 5)) { 759 | $navItemClasses .= ' flex-auto text-center'; 760 | } elseif ($this->isInLastSearches('nav-justified', 5)) { 761 | $navItemClasses .= ' grow text-center'; 762 | } 763 | 764 | return $navItemClasses; 765 | }; 766 | 767 | $items['navbar'] = 'relative flex flex-wrap items-center content-between py-3 px-4'; 768 | $items['navbar-brand'] = 'inline-block pt-1 pb-1 mr-4 text-lg whitespace-nowrap'; 769 | $items['navbar-nav'] = 'flex flex-wrap list-none pl-0 mb-0'; 770 | $items['navbar-text'] = 'inline-block pt-2 pb-2'; 771 | $items['navbar-dark'] = 'text-white'; 772 | $items['navbar-light'] = 'text-black'; 773 | $items['navbar-collapse'] = 'grow items-center'; 774 | $items['navbar-expand'] = 'flex-nowrap content-start'; 775 | $items['navbar-expand-{regex_string}'] = ''; 776 | $items['navbar-toggler'] = 'py-1 px-2 text-md leading-none bg-transparent border border-transparent rounded-sm'; 777 | 778 | //for now 779 | $items['collapse'] = 'hidden'; 780 | $items['navbar-toggler-icon'] = 'px-5 py-1 border border-gray-600 rounded-sm'; 781 | 782 | return $items; 783 | } 784 | 785 | protected function pagination(): array 786 | { 787 | return [ 788 | 'pagination' => 'flex list-none pl-0 rounded-sm', 789 | 'pagination-lg' => 'text-xl', 790 | 'pagination-sm' => 'text-sm', 791 | 'page-link' => 'relative block py-2 px-3 -ml-px leading-none text-blue bg-white border border-gray-200 no-underline hover:text-blue-800 hover:bg-gray-200', 792 | // 'page-link' => 'relative block py-2 px-3 -ml-px leading-none text-blue bg-white border border-gray-', 793 | ]; 794 | } 795 | } 796 | --------------------------------------------------------------------------------