├── .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 | [](https://packagist.org/packages/awssat/tailwindo)
4 | [](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 |
--------------------------------------------------------------------------------