diff --git a/README.md b/README.md index 1a1b092..14db068 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,12 @@ Console output | tailwindConfig | `Config` | {} | Set your tailwind config here | | postCSSPlugins | `AcceptedPlugin[]` | [] | Array of acceptable postcss plugins | +### ConverterOptions(options?) + +| Option | Type | Default | Description | +| ------------------------- | --------- | ------- | -------------------------------------------------------------------- | +| skipAddingTailwindClasses | `boolean` | `false` | Defines whether to skip adding tailwind classes to the converted CSS | + [build-img]: https://github.com/jackardios/css-to-tailwindcss/actions/workflows/release.yml/badge.svg [build-url]: https://github.com/jackardios/css-to-tailwindcss/actions/workflows/release.yml [downloads-img]: https://img.shields.io/npm/dt/css-to-tailwindcss diff --git a/src/TailwindConverter.ts b/src/TailwindConverter.ts index 59a17d1..e7d040d 100644 --- a/src/TailwindConverter.ts +++ b/src/TailwindConverter.ts @@ -46,6 +46,10 @@ export interface ResolvedTailwindConverterConfig mapping: ConverterMapping; } +export interface ConverterOptions { + skipAddingTailwindClasses?: boolean; +} + export const DEFAULT_CONVERTER_CONFIG: Omit< TailwindConverterConfig, 'tailwindConfig' @@ -76,7 +80,7 @@ export class TailwindConverter { }; } - async convertCSS(css: string) { + async convertCSS(css: string, options?: ConverterOptions) { const nodesManager = new TailwindNodesManager(); const parsed = await postcss(this.config.postCSSPlugins).process(css, { parser: postcssSafeParser, @@ -90,16 +94,19 @@ export class TailwindConverter { }); const nodes = nodesManager.getNodes(); - nodes.forEach(node => { - if (node.tailwindClasses.length) { - node.rule.prepend( - new AtRule({ - name: 'apply', - params: node.tailwindClasses.join(' '), - }) - ); - } - }); + + if (!options?.skipAddingTailwindClasses) { + nodes.forEach(node => { + if (node.tailwindClasses.length) { + node.rule.prepend( + new AtRule({ + name: 'apply', + params: node.tailwindClasses.join(' '), + }) + ); + } + }); + } this.cleanRaws(parsed.root); diff --git a/test/TailwindConverter.spec.ts b/test/TailwindConverter.spec.ts index 3d34411..bf441cd 100644 --- a/test/TailwindConverter.spec.ts +++ b/test/TailwindConverter.spec.ts @@ -286,7 +286,346 @@ describe('TailwindConverter', () => { '-right-32', 'lg:backdrop-brightness-90', 'lg:backdrop-sepia-[25%]', - 'lg:backdrop-blur-none', + 'lg:backdrop-blur-0', + 'motion-safe:custom-screen:supports-flex:order-[-123]', + 'motion-safe:custom-screen:supports-flex:tracking-[0.25rem]', + 'motion-safe:custom-screen:supports-flex:leading-snug', + 'motion-safe:custom-screen:supports-flex:list-inside', + 'motion-safe:custom-screen:supports-flex:list-decimal', + 'motion-safe:custom-screen:supports-flex:mb-[-0.875rem]', + 'motion-safe:custom-screen:supports-flex:max-h-full', + 'motion-safe:custom-screen:supports-flex:max-w-screen-2xl', + 'motion-safe:custom-screen:supports-flex:min-h-fit', + 'motion-safe:custom-screen:supports-flex:min-w-min', + 'motion-safe:custom-screen:supports-flex:mix-blend-color-dodge', + 'motion-safe:custom-screen:supports-flex:object-fill', + 'motion-safe:custom-screen:supports-flex:object-right-top', + 'motion-safe:custom-screen:supports-flex:ml-[2em]', + 'motion-safe:custom-screen:supports-flex:mr-[1vh]', + 'motion-safe:custom-screen:supports-flex:mt-3', + 'motion-safe:custom-screen:supports-flex:mt-[3vw]', + 'motion-safe:custom-screen:supports-flex:-mb-2.5', + 'motion-safe:custom-screen:supports-flex:mx-6', + 'motion-safe:custom-screen:supports-flex:left-2', + 'supports-[scroll-snap-align:end]:snap-end', + 'supports-[scroll-snap-align:end]:snap-always', + 'supports-[scroll-snap-align:end]:line-through', + 'supports-[scroll-snap-align:end]:scroll-mt-[12%]', + 'supports-[scroll-snap-align:end]:scroll-pl-3.5', + 'supports-[scroll-snap-align:end]:scroll-pr-[10vw]', + 'supports-[scroll-snap-align:end]:scroll-pt-[10em]', + 'supports-[scroll-snap-align:end]:scroll-pb-5', + 'supports-[scroll-snap-align:end]:scroll-p-[100px]', + ], + }, + { + rule: expect.objectContaining({ selector: '.bar' }), + tailwindClasses: [ + 'md:content-center', + 'md:items-start', + 'lg:backdrop-brightness-75', + 'lg:backdrop-sepia', + 'lg:bg-local', + 'lg:content-end', + 'lg:items-center', + 'animate-[some-animation_2s_linear_infinite]', + 'origin-[12%_25.5%]', + 'ease-[cubic-bezier(0.23,0,0.25,1)]', + 'lg:bg-blend-difference', + 'lg:bg-clip-padding', + 'lg:bg-[hsl(30,51%,22%)]', + 'lg:disabled:bg-gradient-to-tr', + 'lg:disabled:bg-origin-padding', + 'lg:disabled:bg-left-bottom', + 'lg:disabled:bg-no-repeat', + 'lg:disabled:bg-contain', + 'lg:disabled:border-b-2', + 'after:border-spacing-[5%]', + 'after:rounded-full', + 'after:border-l-[3px]', + 'after:border-l-transparent', + 'after:border-l-[1rem]', + 'after:border-r-2', + 'after:border-r-[aqua]', + 'after:border-r-8', + 'after:border-dashed', + 'after:border-t-[some-invalid-value]', + 'after:flex-1', + 'after:rounded-tl-none', + 'after:rounded-tr-[0.25%]', + 'after:border-t-[current]', + 'after:border-t-[100vh]', + 'after:border-dotted', + 'after:border-0', + 'after:bottom-[100vw]', + 'box-decoration-slice', + 'shadow', + 'box-border', + 'break-after-all', + 'break-before-page', + 'break-inside-avoid-column', + 'caret-[color:var(--cyan)]', + 'h-9', + 'flex-1', + 'xl:flex-[1_0]', + 'xl:clear-both', + 'xl:text-lime-200', + 'xl:gap-x-48', + 'xl:columns-3', + 'xl:content-none', + 'xl:cursor-pointer', + 'xl:hidden', + 'xl:fill-sky-800', + 'xl:blur-sm', + 'xl:brightness-50', + 'xl:sepia', + 'xl:contrast-100', + 'xl:hue-rotate-30', + 'xl:invert-0', + 'xl:opacity-5', + 'xl:saturate-150', + 'xl:flex-auto', + 'xl:basis-3', + 'xl:flex-col-reverse', + 'xl:grow', + 'xl:shrink-0', + 'xl:flex-wrap-reverse', + 'xl:float-right', + 'xl:text-2xl', + 'xl:antialiased', + 'xl:italic', + 'xl:ordinal', + 'xl:font-semibold', + ], + }, + { + rule: expect.objectContaining({ selector: '.foo' }), + tailwindClasses: [ + 'appearance-none', + 'disabled:opacity-0', + 'disabled:invisible', + 'disabled:select-none', + ], + }, + { + rule: expect.objectContaining({ selector: '.foo[some-attribute]' }), + tailwindClasses: ['select-text'], + }, + { + rule: expect.objectContaining({ + selector: ".foo [aria-role='button']", + }), + tailwindClasses: [ + 'uppercase', + 'underline-offset-[1rem]', + 'touch-pan-left', + 'origin-bottom-right', + 'transition-none', + ], + }, + { + rule: expect.objectContaining({ + selector: ".foo[aria-hidden='false']", + }), + tailwindClasses: [ + 'collapse', + 'whitespace-pre-line', + 'w-6/12', + 'will-change-transform', + 'break-all', + 'z-40', + 'translate-x-3', + 'translate-y-[-0.5em]', + 'skew-x-1', + 'skew-y-3', + 'rotate-[-0.25turn]', + 'transition-colors', + 'duration-200', + 'ease-out', + '-scale-x-75', + 'scale-y-105', + ], + }, + { + rule: expect.objectContaining({ selector: '.foo .bar' }), + tailwindClasses: [ + 'translate-x-[10px_0.625rem]', + 'skew-x-2', + '-rotate-45', + 'pl-[12%]', + 'pr-[100vw]', + 'pt-64', + 'pb-1', + 'px-6', + 'py-8', + '-scale-75', + ], + }, + { + rule: expect.objectContaining({ selector: '.foo .baz > .foo-bar' }), + tailwindClasses: [ + 'text-left', + 'text-[length:var(--some-size)]', + 'text-[color:var(--some-color)]', + 'active:focus:break-after-avoid', + 'active:focus:break-before-left', + 'active:focus:break-inside-auto', + 'xl:isolate', + 'xl:justify-center', + 'xl:active:text-sky-800', + 'xl:active:focus:justify-items-start', + 'xl:active:focus:justify-self-end', + 'motion-safe:custom-screen:supports-flex:opacity-20', + 'motion-safe:custom-screen:supports-flex:-order-last', + 'motion-safe:custom-screen:supports-flex:outline-lime-600', + 'motion-safe:custom-screen:supports-flex:outline-offset-2', + 'motion-safe:custom-screen:supports-flex:outline-dotted', + 'motion-safe:custom-screen:supports-flex:outline-2', + 'motion-safe:custom-screen:supports-flex:overflow-hidden', + 'motion-safe:custom-screen:supports-flex:break-words', + 'motion-safe:custom-screen:supports-flex:overflow-x-scroll', + 'motion-safe:custom-screen:supports-flex:overflow-y-visible', + 'motion-safe:custom-screen:supports-flex:overscroll-contain', + 'motion-safe:custom-screen:supports-flex:overscroll-x-auto', + 'motion-safe:custom-screen:supports-flex:overscroll-y-none', + ], + }, + { + rule: expect.objectContaining({ selector: '.foo div > [data-zoo]' }), + tailwindClasses: [ + 'border', + 'pl-[25%]', + 'pr-[1.5em]', + 'pt-0', + 'pb-px', + 'bottom-full', + ], + }, + { + rule: expect.objectContaining({ selector: '.loving .bar > .testing' }), + tailwindClasses: [ + "lg:bg-[url('/some-path/to/large\\_image.jpg')]", + 'lg:border-custom-color-gold', + 'lg:border-b-custom-color-200', + 'lg:border-b-[length:var(--some-size)]', + 'lg:border-neutral-600', + 'lg:border-t-custom-color-400', + 'lg:rounded-br-sm', + 'lg:rounded-bl', + 'lg:border-b-[2em]', + 'lg:border-b-[#ff0000]', + 'lg:border-4', + 'lg:border-solid', + 'lg:border-dashed', + 'lg:border-separate', + ], + }, + { + rule: expect.objectContaining({ selector: '.foo .baz' }), + tailwindClasses: [ + 'gap-[19px]', + 'auto-cols-min', + 'grid-flow-row', + 'auto-rows-max', + 'col-span-3', + 'col-end-4', + 'gap-x-12', + 'col-start-3', + ], + }, + { + rule: expect.objectContaining({ selector: '.foo .baz > .foo-bar' }), + tailwindClasses: [ + 'gap-8', + 'row-span-full', + 'row-end-2', + 'gap-y-6', + 'row-start-auto', + 'grid-cols-2', + 'grid-rows-5', + 'h-[$some-invalid]', + 'active:text-[red]', + ], + }, + { + rule: expect.objectContaining({ selector: '#some-id' }), + tailwindClasses: [ + 'opacity-40', + 'order-last', + 'outline-teal-900', + 'outline-offset-2', + 'outline-8', + 'supports-[display:block]:gap-y-80', + 'supports-[display:block]:scroll-smooth', + 'supports-[display:block]:scroll-ml-2', + 'supports-[display:block]:scroll-mr-[1.5em]', + 'supports-[display:block]:scroll-mb-8', + 'supports-[display:block]:scroll-m-40', + ], + }, + { + rule: expect.objectContaining({ selector: 'div > [data-zoo]' }), + tailwindClasses: [ + 'stroke-[black]', + 'stroke-2', + 'table-fixed', + 'text-justify', + 'decoration-custom-color-gold', + 'line-through', + 'decoration-dotted', + 'decoration-8', + 'indent-[0.125rem]', + 'text-ellipsis', + ], + }, + ]); + }); + + it('should convert the complex CSS with converter options', async () => { + const converter = createTailwindConverter(); + const converted = await converter.convertCSS(complexCSS, { + skipAddingTailwindClasses: true, + }); + + expect(converted.convertedRoot.toString()).toMatchSnapshot(); + + fs.writeFileSync( + path.resolve(__dirname, './fixtures/converted-complex-css.css'), + converted.convertedRoot.toString() + ); + expect(converted.nodes).toEqual([ + { + rule: expect.objectContaining({ selector: '.foo' }), + tailwindClasses: [ + 'md:accent-custom-color-gold', + 'animate-spin', + 'aspect-video', + 'content-center', + 'content-end', + 'portrait:text-[black]', + "after:content-['*']", + 'after:align-text-top', + 'after:origin-top', + 'hidden:delay-150', + 'hidden:duration-200', + 'hidden:transition', + 'hidden:ease-in', + ], + }, + { + rule: expect.objectContaining({ selector: '.foo .baz' }), + tailwindClasses: [ + 'md:text-center', + 'place-content-around', + 'place-items-center', + 'place-self-stretch', + 'pointer-events-auto', + 'relative', + 'resize-x', + '-right-32', + 'lg:backdrop-brightness-90', + 'lg:backdrop-sepia-[25%]', + 'lg:backdrop-blur-0', 'motion-safe:custom-screen:supports-flex:order-[-123]', 'motion-safe:custom-screen:supports-flex:tracking-[0.25rem]', 'motion-safe:custom-screen:supports-flex:leading-snug', diff --git a/test/__snapshots__/TailwindConverter.spec.ts.snap b/test/__snapshots__/TailwindConverter.spec.ts.snap index 2813366..4b8b54e 100644 --- a/test/__snapshots__/TailwindConverter.spec.ts.snap +++ b/test/__snapshots__/TailwindConverter.spec.ts.snap @@ -24,7 +24,7 @@ exports[`TailwindConverter should convert the complex CSS 1`] = ` @apply md:accent-custom-color-gold animate-spin aspect-video content-center content-end portrait:text-[black] after:content-['*'] after:align-text-top after:origin-top hidden:delay-150 hidden:duration-200 hidden:transition hidden:ease-in; } .foo .baz{ - @apply md:text-center place-content-around place-items-center place-self-stretch pointer-events-auto relative resize-x -right-32 lg:backdrop-brightness-90 lg:backdrop-sepia-[25%] lg:backdrop-blur-none motion-safe:custom-screen:supports-flex:order-[-123] motion-safe:custom-screen:supports-flex:tracking-[0.25rem] motion-safe:custom-screen:supports-flex:leading-snug motion-safe:custom-screen:supports-flex:list-inside motion-safe:custom-screen:supports-flex:list-decimal motion-safe:custom-screen:supports-flex:mb-[-0.875rem] motion-safe:custom-screen:supports-flex:max-h-full motion-safe:custom-screen:supports-flex:max-w-screen-2xl motion-safe:custom-screen:supports-flex:min-h-fit motion-safe:custom-screen:supports-flex:min-w-min motion-safe:custom-screen:supports-flex:mix-blend-color-dodge motion-safe:custom-screen:supports-flex:object-fill motion-safe:custom-screen:supports-flex:object-right-top motion-safe:custom-screen:supports-flex:ml-[2em] motion-safe:custom-screen:supports-flex:mr-[1vh] motion-safe:custom-screen:supports-flex:mt-3 motion-safe:custom-screen:supports-flex:mt-[3vw] motion-safe:custom-screen:supports-flex:-mb-2.5 motion-safe:custom-screen:supports-flex:mx-6 motion-safe:custom-screen:supports-flex:left-2 supports-[scroll-snap-align:end]:snap-end supports-[scroll-snap-align:end]:snap-always supports-[scroll-snap-align:end]:line-through supports-[scroll-snap-align:end]:scroll-mt-[12%] supports-[scroll-snap-align:end]:scroll-pl-3.5 supports-[scroll-snap-align:end]:scroll-pr-[10vw] supports-[scroll-snap-align:end]:scroll-pt-[10em] supports-[scroll-snap-align:end]:scroll-pb-5 supports-[scroll-snap-align:end]:scroll-p-[100px]; + @apply md:text-center place-content-around place-items-center place-self-stretch pointer-events-auto relative resize-x -right-32 lg:backdrop-brightness-90 lg:backdrop-sepia-[25%] lg:backdrop-blur-0 motion-safe:custom-screen:supports-flex:order-[-123] motion-safe:custom-screen:supports-flex:tracking-[0.25rem] motion-safe:custom-screen:supports-flex:leading-snug motion-safe:custom-screen:supports-flex:list-inside motion-safe:custom-screen:supports-flex:list-decimal motion-safe:custom-screen:supports-flex:mb-[-0.875rem] motion-safe:custom-screen:supports-flex:max-h-full motion-safe:custom-screen:supports-flex:max-w-screen-2xl motion-safe:custom-screen:supports-flex:min-h-fit motion-safe:custom-screen:supports-flex:min-w-min motion-safe:custom-screen:supports-flex:mix-blend-color-dodge motion-safe:custom-screen:supports-flex:object-fill motion-safe:custom-screen:supports-flex:object-right-top motion-safe:custom-screen:supports-flex:ml-[2em] motion-safe:custom-screen:supports-flex:mr-[1vh] motion-safe:custom-screen:supports-flex:mt-3 motion-safe:custom-screen:supports-flex:mt-[3vw] motion-safe:custom-screen:supports-flex:-mb-2.5 motion-safe:custom-screen:supports-flex:mx-6 motion-safe:custom-screen:supports-flex:left-2 supports-[scroll-snap-align:end]:snap-end supports-[scroll-snap-align:end]:snap-always supports-[scroll-snap-align:end]:line-through supports-[scroll-snap-align:end]:scroll-mt-[12%] supports-[scroll-snap-align:end]:scroll-pl-3.5 supports-[scroll-snap-align:end]:scroll-pr-[10vw] supports-[scroll-snap-align:end]:scroll-pt-[10em] supports-[scroll-snap-align:end]:scroll-pb-5 supports-[scroll-snap-align:end]:scroll-p-[100px]; } .bar{ @apply md:content-center md:items-start lg:backdrop-brightness-75 lg:backdrop-sepia lg:bg-local lg:content-end lg:items-center animate-[some-animation_2s_linear_infinite] origin-[12%_25.5%] ease-[cubic-bezier(0.23,0,0.25,1)] lg:bg-blend-difference lg:bg-clip-padding lg:bg-[hsl(30,51%,22%)] lg:disabled:bg-gradient-to-tr lg:disabled:bg-origin-padding lg:disabled:bg-left-bottom lg:disabled:bg-no-repeat lg:disabled:bg-contain lg:disabled:border-b-2 after:border-spacing-[5%] after:rounded-full after:border-l-[3px] after:border-l-transparent after:border-l-[1rem] after:border-r-2 after:border-r-[aqua] after:border-r-8 after:border-dashed after:border-t-[some-invalid-value] after:flex-1 after:rounded-tl-none after:rounded-tr-[0.25%] after:border-t-[current] after:border-t-[100vh] after:border-dotted after:border-0 after:bottom-[100vw] box-decoration-slice shadow box-border break-after-all break-before-page break-inside-avoid-column caret-[color:var(--cyan)] h-9 flex-1 xl:flex-[1_0] xl:clear-both xl:text-lime-200 xl:gap-x-48 xl:columns-3 xl:content-none xl:cursor-pointer xl:hidden xl:fill-sky-800 xl:blur-sm xl:brightness-50 xl:sepia xl:contrast-100 xl:hue-rotate-30 xl:invert-0 xl:opacity-5 xl:saturate-150 xl:flex-auto xl:basis-3 xl:flex-col-reverse xl:grow xl:shrink-0 xl:flex-wrap-reverse xl:float-right xl:text-2xl xl:antialiased xl:italic xl:ordinal xl:font-semibold; @@ -163,6 +163,116 @@ div > [data-zoo] { " `; +exports[`TailwindConverter should convert the complex CSS with converter options 1`] = ` +"@charset \\"UTF-8\\"; +@media (min-width: 768px) { + + /* custom-color-gold */ +} +.foo { + --some-size: 12px; + --some-color: #333333; + align-self: self-end; + + /* unconvertable value */ + + /* duplicate */ +} + +/* duplicate with another value */ +.foo [aria-role='button'] { + + /* unconvertable aria selector */ + transform: translateX(12px) translateY(0.5em) translateZ(0.5rem) + scaleY(0.725) rotate(124deg); +} +.foo[aria-hidden='true'] { + transform: some invalid-transform; +} +.foo[aria-hidden='false'] { + + /* unconvertable aria selector */ +} +.foo .bar { + transition: transform 200ms cubic-bezier(0, 0, 0.2, 1), color, + background-color, border-color, text-decoration-color, fill, + stroke 200ms cubic-bezier(0.4, 0, 0.2, 1); +} +.bar { + + /* arbitary value */ + animation-delay: 200ms; + + /* unconvertable value */ +} +@media screen and (min-width: 1024px) { + .bar[aria-disabled='true'] { + background-attachment: initial; + } + .loving .bar > .testing { + + /* custom-color-gold */ + + /* custom-color-200 */ + + /* neutral-600 */ + } +} +.bar { + --cyan: #06b6d4; +} +@media (min-width: 1280px) { + .bar { + + /* sky-800 */ + } +} +@media screen and (min-width: 1280px) { + .bar { + flex-flow: column; + } +} +@media screen and (min-width: 1280px) and (max-width: 1440px) { + .foo .baz { + grid: 'a' 200px 'b' max-content; + } +} +@supports (display: flex) { + @media (min-width: 768px) { + @media (prefers-reduced-motion: no-preference) { + @media (max-width: 1024px) { + .foo .baz > .foo-bar { + outline: 4px dashed rgb(254, 240, 138); + + /* yellow-200 */ + + /* lime-600 */ + } + } + } + } +} +#some-id { + outline: none; + + /* teal-900 */ + outline-style: groove; +} +@supports (scroll-snap-align: end) { + .foo .baz { + scroll-snap-type: x mandatory; + } +} +div > [data-zoo] { + text-decoration: underline #123456 dashed 4px; + + /* custom-color-100 */ + + /* custom-color-gold */ +} +" +`; + exports[`TailwindConverter should convert the css part string 1`] = ` "{ @apply text-center text-xs hover:text-base md:font-semibold;