├── src ├── index.ts ├── utilities │ ├── __tests__ │ │ ├── fixtures │ │ │ ├── 1-no-config │ │ │ │ └── .gitkeep │ │ │ ├── 5-invalid-custom-config │ │ │ │ └── .gitkeep │ │ │ ├── 4-custom-config │ │ │ │ └── foo.json │ │ │ ├── 3-generate-css-config │ │ │ │ └── generate-css.config.json │ │ │ └── 2-package-json-config │ │ │ │ └── package.json │ │ └── read-config-async.ts │ ├── create-css │ │ ├── declarations │ │ │ ├── outline.ts │ │ │ ├── cursor.ts │ │ │ ├── flex-wrap.ts │ │ │ ├── flex.ts │ │ │ ├── position.ts │ │ │ ├── text-decoration.ts │ │ │ ├── select.ts │ │ │ ├── text-align.ts │ │ │ ├── display.ts │ │ │ ├── text-transform.ts │ │ │ ├── align-items.ts │ │ │ ├── justify-content.ts │ │ │ └── index.ts │ │ ├── map-selector-to-declaration.ts │ │ ├── compute-color-value-factory.ts │ │ ├── create-css.ts │ │ ├── plugins │ │ │ ├── color.ts │ │ │ ├── index.ts │ │ │ ├── line-height.ts │ │ │ ├── border-color.ts │ │ │ ├── letter-spacing.ts │ │ │ ├── background-color.ts │ │ │ ├── position.ts │ │ │ ├── text-style.ts │ │ │ ├── font.ts │ │ │ ├── padding.ts │ │ │ ├── border-width.ts │ │ │ ├── width-and-height.ts │ │ │ ├── margin.ts │ │ │ └── border-radius.ts │ │ ├── __tests__ │ │ │ ├── create-css-declaration-block │ │ │ │ ├── declarations │ │ │ │ │ ├── position │ │ │ │ │ │ ├── top │ │ │ │ │ │ │ ├── negative-top-position.ts │ │ │ │ │ │ │ └── positive-top-position.ts │ │ │ │ │ │ ├── left │ │ │ │ │ │ │ ├── negative-left-position.ts │ │ │ │ │ │ │ └── positive-left-position.ts │ │ │ │ │ │ ├── right │ │ │ │ │ │ │ ├── negative-right-position.ts │ │ │ │ │ │ │ └── positive-right-position.ts │ │ │ │ │ │ └── bottom │ │ │ │ │ │ │ ├── negative-bottom-position.ts │ │ │ │ │ │ │ └── positive-bottom-position.ts │ │ │ │ │ ├── font-weight.ts │ │ │ │ │ ├── font-size.ts │ │ │ │ │ ├── text-style.ts │ │ │ │ │ ├── color.ts │ │ │ │ │ ├── font-family.ts │ │ │ │ │ ├── line-height.ts │ │ │ │ │ ├── letter-spacing.ts │ │ │ │ │ ├── width-and-height │ │ │ │ │ │ ├── width │ │ │ │ │ │ │ ├── width.ts │ │ │ │ │ │ │ ├── max-width.ts │ │ │ │ │ │ │ └── min-width.ts │ │ │ │ │ │ └── height │ │ │ │ │ │ │ ├── height.ts │ │ │ │ │ │ │ ├── max-height.ts │ │ │ │ │ │ │ └── min-height.ts │ │ │ │ │ ├── background-color.ts │ │ │ │ │ ├── border-color.ts │ │ │ │ │ ├── margin │ │ │ │ │ │ ├── positive-margin.ts │ │ │ │ │ │ └── negative-margin.ts │ │ │ │ │ ├── padding.ts │ │ │ │ │ ├── border-width.ts │ │ │ │ │ └── border-radius.ts │ │ │ │ └── create-css-declaration-block.ts │ │ │ ├── parse-class-name.ts │ │ │ └── compute-numeric-value-factory.ts │ │ ├── parse-class-name.ts │ │ ├── create-css-declaration-block.ts │ │ ├── group-css-declaration-blocks-by-breakpoint.ts │ │ └── compute-numeric-value-factory.ts │ ├── extract-class-names-async │ │ ├── extract-class-names-from-js.ts │ │ ├── extract-class-names-from-html.ts │ │ ├── extract-class-names-from-string-factory.ts │ │ ├── extract-class-names-async.ts │ │ └── __tests__ │ │ │ ├── extract-class-names-from-html.ts │ │ │ └── extract-class-names-from-js.ts │ ├── log.ts │ ├── create-base-font-size-css.ts │ ├── read-config-async.ts │ └── stringify-css.ts ├── build.ts ├── watch.ts ├── css │ └── reset.css ├── cli.ts ├── types.ts └── generate-css.ts ├── .gitignore ├── example ├── example.html └── generate-css.config.json ├── .github └── workflows │ └── build.yml ├── tsconfig.json ├── scripts ├── print-theme-keys.ts └── create-css-docs.ts ├── LICENSE.md ├── package.json ├── README.md └── docs └── css.md /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generate-css' 2 | -------------------------------------------------------------------------------- /src/utilities/__tests__/fixtures/1-no-config/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utilities/__tests__/fixtures/5-invalid-custom-config/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utilities/create-css/declarations/outline.ts: -------------------------------------------------------------------------------- 1 | export const outline = { 2 | 'outline-none': { 3 | outline: 'none' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | *.tsbuildinfo 4 | .nyc_output/ 5 | coverage/ 6 | example/style.css 7 | lib/ 8 | node_modules/ 9 | sandbox/ 10 | -------------------------------------------------------------------------------- /example/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/utilities/__tests__/fixtures/4-custom-config/foo.json: -------------------------------------------------------------------------------- 1 | { 2 | "reset": false, 3 | "theme": { 4 | "color": { 5 | "default": "#ffffff" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utilities/__tests__/fixtures/3-generate-css-config/generate-css.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "reset": false, 3 | "theme": { 4 | "color": { 5 | "default": "#ffffff" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utilities/create-css/declarations/cursor.ts: -------------------------------------------------------------------------------- 1 | export const cursor = { 2 | 'cursor-default': { 3 | cursor: 'default' 4 | }, 5 | 'cursor-pointer': { 6 | cursor: 'pointer' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utilities/__tests__/fixtures/2-package-json-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "generate-css": { 3 | "reset": false, 4 | "theme": { 5 | "color": { 6 | "default": "#ffffff" 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/utilities/create-css/declarations/flex-wrap.ts: -------------------------------------------------------------------------------- 1 | export const flexWrap = { 2 | 'flex-nowrap': { 3 | 'flex-wrap': 'nowrap' 4 | }, 5 | 'flex-wrap': { 6 | 'flex-wrap': 'wrap' 7 | }, 8 | 'flex-wrap-reverse': { 9 | 'flex-wrap': 'wrap-reverse' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utilities/create-css/declarations/flex.ts: -------------------------------------------------------------------------------- 1 | export const flex = { 2 | 'flex-1': { 3 | flex: '1 1 0%' 4 | }, 5 | 'flex-auto': { 6 | flex: '1 1 auto' 7 | }, 8 | 'flex-initial': { 9 | flex: '0 1 auto' 10 | }, 11 | 'flex-none': { 12 | flex: 'none' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/utilities/create-css/declarations/position.ts: -------------------------------------------------------------------------------- 1 | export const position = { 2 | absolute: { 3 | position: 'absolute' 4 | }, 5 | fixed: { 6 | position: 'fixed' 7 | }, 8 | relative: { 9 | position: 'relative' 10 | }, 11 | static: { 12 | position: 'static' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/utilities/create-css/declarations/text-decoration.ts: -------------------------------------------------------------------------------- 1 | export const textDecoration = { 2 | 'line-through': { 3 | 'text-decoration': 'line-through' 4 | }, 5 | 'no-underline': { 6 | 'text-decoration': 'none' 7 | }, 8 | 'underline': { 9 | 'text-decoration': 'underline' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utilities/create-css/declarations/select.ts: -------------------------------------------------------------------------------- 1 | export const select = { 2 | 'select-all': { 3 | 'user-select': 'all' 4 | }, 5 | 'select-auto': { 6 | 'user-select': 'auto' 7 | }, 8 | 'select-none': { 9 | 'user-select': 'none' 10 | }, 11 | 'select-text': { 12 | 'user-select': 'text' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/utilities/create-css/declarations/text-align.ts: -------------------------------------------------------------------------------- 1 | export const textAlign = { 2 | 'text-center': { 3 | 'text-align': 'center' 4 | }, 5 | 'text-justify': { 6 | 'text-align': 'justify' 7 | }, 8 | 'text-left': { 9 | 'text-align': 'left' 10 | }, 11 | 'text-right': { 12 | 'text-align': 'right' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/utilities/create-css/declarations/display.ts: -------------------------------------------------------------------------------- 1 | export const display = { 2 | 'block': { 3 | display: 'block' 4 | }, 5 | 'flex': { 6 | display: 'flex' 7 | }, 8 | 'hidden': { 9 | display: 'none' 10 | }, 11 | 'inline': { 12 | display: 'inline' 13 | }, 14 | 'inline-flex': { 15 | display: 'inline-flex' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/utilities/extract-class-names-async/extract-class-names-from-js.ts: -------------------------------------------------------------------------------- 1 | import { extractClassNamesFromStringFactory } from './extract-class-names-from-string-factory' 2 | 3 | const regex = /(?:const|var) \w*class_?(?:names?)? ?= ?(['"])((?:(?!\1).)+)\1/gi 4 | 5 | export const extractClassNamesFromJs = extractClassNamesFromStringFactory( 6 | regex, 7 | 2 8 | ) 9 | -------------------------------------------------------------------------------- /src/utilities/create-css/declarations/text-transform.ts: -------------------------------------------------------------------------------- 1 | export const textTransform = { 2 | 'caps': { 3 | 'text-transform': 'capitalize' 4 | }, 5 | 'lowercase': { 6 | 'text-transform': 'lowercase' 7 | }, 8 | 'normal-case': { 9 | 'text-transform': 'none' 10 | }, 11 | 'uppercase': { 12 | 'text-transform': 'uppercase' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/utilities/extract-class-names-async/extract-class-names-from-html.ts: -------------------------------------------------------------------------------- 1 | import { extractClassNamesFromStringFactory } from './extract-class-names-from-string-factory' 2 | 3 | const regex = /class=(['"])((?:(?!\1).)+)\1/gi // https://stackoverflow.com/a/8057827 4 | 5 | export const extractClassNamesFromHtml = extractClassNamesFromStringFactory( 6 | regex, 7 | 2 8 | ) 9 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v1 11 | with: 12 | node-version: 12 13 | - run: yarn install --frozen-lockfile 14 | - run: yarn run lint 15 | - run: yarn run fix 16 | - run: yarn run test 17 | -------------------------------------------------------------------------------- /src/utilities/create-css/map-selector-to-declaration.ts: -------------------------------------------------------------------------------- 1 | import { Declarations } from '../../types' 2 | import { declarations } from './declarations' 3 | 4 | export function mapSelectorToDeclaration( 5 | selector: string 6 | ): null | Declarations { 7 | const declaration = declarations[selector] 8 | if (typeof declaration === 'undefined') { 9 | return null 10 | } 11 | return declaration 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationMap": true, 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "outDir": "./lib", 8 | "removeComments": true, 9 | "rootDir": "./src", 10 | "strict": true, 11 | "target": "es2020", 12 | "typeRoots": ["./node_modules/@types"] 13 | }, 14 | "include": ["./src/**/*.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /src/utilities/create-css/declarations/align-items.ts: -------------------------------------------------------------------------------- 1 | export const alignItems = { 2 | 'items-baseline': { 3 | 'align-items': 'baseline' 4 | }, 5 | 'items-center': { 6 | 'align-items': 'center' 7 | }, 8 | 'items-end': { 9 | 'align-items': 'flex-end' 10 | }, 11 | 'items-start': { 12 | 'align-items': 'flex-start' 13 | }, 14 | 'items-stretch': { 15 | 'align-items': 'stretch' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/generate-css.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "reset": true, 3 | "theme": { 4 | "baseFontSize": { 5 | "default": "16px" 6 | }, 7 | "baseSpace": "1rem", 8 | "color": { 9 | "black": "#000", 10 | "blue": "#00f", 11 | "white": "#fff" 12 | }, 13 | "fontFamily": { 14 | "default": "Helvetica, Arial, sans-serif" 15 | }, 16 | "fontWeight": { 17 | "bold": "bolder" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/utilities/log.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import * as kleur from 'kleur' 4 | 5 | function error(message: string): void { 6 | console.log(`${kleur.red().bold('error')} ${message}`) 7 | } 8 | 9 | function info(message: string): void { 10 | console.log(`${kleur.blue().bold('info')} ${message}`) 11 | } 12 | 13 | function success(message: string): void { 14 | console.log(`${kleur.green().bold('success')} ${message}`) 15 | } 16 | 17 | export const log = { 18 | error, 19 | info, 20 | success 21 | } 22 | -------------------------------------------------------------------------------- /src/utilities/create-css/declarations/justify-content.ts: -------------------------------------------------------------------------------- 1 | export const justifyContent = { 2 | 'justify-around': { 3 | 'justify-content': 'space-around' 4 | }, 5 | 'justify-between': { 6 | 'justify-content': 'space-between' 7 | }, 8 | 'justify-center': { 9 | 'justify-content': 'center' 10 | }, 11 | 'justify-end': { 12 | 'justify-content': 'flex-end' 13 | }, 14 | 'justify-evenly': { 15 | 'justify-content': 'space-evenly' 16 | }, 17 | 'justify-start': { 18 | 'justify-content': 'flex-start' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/build.ts: -------------------------------------------------------------------------------- 1 | import { generateCssAsync } from './generate-css' 2 | import { CliOptions } from './types' 3 | import { log } from './utilities/log' 4 | import { readConfigAsync } from './utilities/read-config-async' 5 | 6 | export async function build(cliOptions: CliOptions): Promise { 7 | try { 8 | const config = await readConfigAsync(cliOptions) 9 | if (config.outputPath !== null) { 10 | log.info('Generating CSS...') 11 | } 12 | await generateCssAsync(config) 13 | if (config.outputPath !== null) { 14 | log.success('Done') 15 | } 16 | } catch (error) { 17 | log.error(error.message) 18 | process.exit(1) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/utilities/extract-class-names-async/extract-class-names-from-string-factory.ts: -------------------------------------------------------------------------------- 1 | const consecutiveSpaceRegex = / +/ 2 | 3 | export function extractClassNamesFromStringFactory( 4 | regex: RegExp, 5 | matchesIndex: number 6 | ): (string: string) => Array { 7 | return function (string: string): Array { 8 | const iterator = string.matchAll(regex) 9 | const result: { [key: string]: boolean } = {} 10 | for (const matches of iterator) { 11 | const classNames = matches[matchesIndex] 12 | .trim() 13 | .split(consecutiveSpaceRegex) 14 | for (const className of classNames) { 15 | result[className] = true 16 | } 17 | } 18 | return Object.keys(result).sort() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/utilities/create-css/compute-color-value-factory.ts: -------------------------------------------------------------------------------- 1 | import { Theme, ThemeItem, ThemeKeys } from '../../types' 2 | 3 | export function computeColorValueFactory( 4 | theme: Theme 5 | ): (value: undefined | string, themeKeys: Array) => null | string { 6 | return function ( 7 | value = 'default', 8 | themeKeys: Array 9 | ): null | string { 10 | for (const themeKey of themeKeys) { 11 | if ( 12 | typeof themeKey !== 'undefined' && 13 | typeof theme[themeKey] !== 'undefined' 14 | ) { 15 | const result = (theme[themeKey] as ThemeItem)[value] 16 | if (typeof result !== 'undefined') { 17 | return result 18 | } 19 | } 20 | } 21 | return null 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/utilities/create-css/create-css.ts: -------------------------------------------------------------------------------- 1 | import { CssDeclarationBlock, CssDeclarationBlocks, Theme } from '../../types' 2 | import { createCssDeclarationBlock } from './create-css-declaration-block' 3 | import { groupCssDeclarationBlocksByBreakpoint } from './group-css-declaration-blocks-by-breakpoint' 4 | 5 | export function createCss( 6 | classNames: Array, 7 | theme: Theme 8 | ): Array { 9 | const result: Array = [] 10 | for (const className of classNames) { 11 | const cssDeclarationBlock = createCssDeclarationBlock(className, theme) 12 | if (cssDeclarationBlock === null) { 13 | continue 14 | } 15 | result.push(cssDeclarationBlock) 16 | } 17 | return groupCssDeclarationBlocksByBreakpoint(result, theme.breakpoint) 18 | } 19 | -------------------------------------------------------------------------------- /src/utilities/create-css/plugins/color.ts: -------------------------------------------------------------------------------- 1 | /* 2 | color 3 | 4 | `.color` | `color: ${theme.color.default};` 5 | `.color-${key}` | `color: ${theme.color[key]};` 6 | */ 7 | 8 | import { Plugin, ThemeKeys } from '../../../types' 9 | 10 | export const color: Plugin = { 11 | createDeclarations: function ({ 12 | matches, 13 | computeColorValue 14 | }: { 15 | matches: RegExpMatchArray 16 | computeColorValue: ( 17 | value: string, 18 | themeKeys: Array 19 | ) => null | string 20 | }): { [property: string]: string } { 21 | const color = computeColorValue(matches[1], ['color']) 22 | if (color === null) { 23 | throw new Error(`Invalid color: ${matches[1]}`) 24 | } 25 | return { 26 | color: `${color}` 27 | } 28 | }, 29 | regex: /^color(?:-(.+))?$/ 30 | } 31 | -------------------------------------------------------------------------------- /src/utilities/create-css/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import { backgroundColor } from './background-color' 2 | import { borderColor } from './border-color' 3 | import { borderRadius } from './border-radius' 4 | import { borderWidth } from './border-width' 5 | import { color } from './color' 6 | import { font } from './font' 7 | import { letterSpacing } from './letter-spacing' 8 | import { lineHeight } from './line-height' 9 | import { margin } from './margin' 10 | import { padding } from './padding' 11 | import { position } from './position' 12 | import { textStyle } from './text-style' 13 | import { widthAndHeight } from './width-and-height' 14 | 15 | export const plugins = [ 16 | backgroundColor, 17 | borderColor, 18 | borderWidth, 19 | borderRadius, 20 | color, 21 | font, 22 | letterSpacing, 23 | lineHeight, 24 | margin, 25 | padding, 26 | position, 27 | textStyle, 28 | widthAndHeight 29 | ] 30 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/position/top/negative-top-position.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block' 4 | 5 | test('custom top position defined in `theme.space`', function (t) { 6 | t.plan(1) 7 | t.deepEqual(createCssDeclarationBlock('-top-sm', { space: { sm: '4px' } }), { 8 | breakpoint: null, 9 | className: '-top-sm', 10 | declarations: { 11 | top: '-4px' 12 | }, 13 | pseudoClass: null, 14 | selector: '-top-sm' 15 | }) 16 | }) 17 | 18 | test('pixel top position', function (t) { 19 | t.plan(1) 20 | t.deepEqual(createCssDeclarationBlock('-top-2px', {}), { 21 | breakpoint: null, 22 | className: '-top-2px', 23 | declarations: { 24 | top: '-2px' 25 | }, 26 | pseudoClass: null, 27 | selector: '-top-2px' 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /src/utilities/create-css/plugins/line-height.ts: -------------------------------------------------------------------------------- 1 | /* 2 | line-height 3 | 4 | `.leading` | `line-height: ${theme.lineHeight.default};` 5 | `.leading-${key}` | `line-height: ${theme.lineHeight[key]};` 6 | */ 7 | 8 | import { Plugin, ThemeKeys } from '../../../types' 9 | 10 | export const lineHeight: Plugin = { 11 | createDeclarations: function ({ 12 | computeNumericValue, 13 | matches 14 | }: { 15 | computeNumericValue: ( 16 | value: string, 17 | themeKeys: Array 18 | ) => null | string 19 | matches: RegExpMatchArray 20 | }): { [property: string]: string } { 21 | const lineHeight = computeNumericValue(matches[1], ['lineHeight']) 22 | if (lineHeight === null) { 23 | throw new Error(`Invalid line-height: ${matches[1]}`) 24 | } 25 | return { 26 | 'line-height': `${lineHeight}` 27 | } 28 | }, 29 | regex: /^(?:leading|line-height)(?:-(.+))?$/ 30 | } 31 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/position/left/negative-left-position.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block' 4 | 5 | test('custom left position defined in `theme.space`', function (t) { 6 | t.plan(1) 7 | t.deepEqual(createCssDeclarationBlock('-left-sm', { space: { sm: '4px' } }), { 8 | breakpoint: null, 9 | className: '-left-sm', 10 | declarations: { 11 | left: '-4px' 12 | }, 13 | pseudoClass: null, 14 | selector: '-left-sm' 15 | }) 16 | }) 17 | 18 | test('pixel left position', function (t) { 19 | t.plan(1) 20 | t.deepEqual(createCssDeclarationBlock('-left-2px', {}), { 21 | breakpoint: null, 22 | className: '-left-2px', 23 | declarations: { 24 | left: '-2px' 25 | }, 26 | pseudoClass: null, 27 | selector: '-left-2px' 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /src/watch.ts: -------------------------------------------------------------------------------- 1 | import * as chokidar from 'chokidar' 2 | 3 | import { build } from './build' 4 | import { CliOptions } from './types' 5 | import { log } from './utilities/log' 6 | 7 | export function watch(cliOptions: CliOptions): void { 8 | const files = [cliOptions.sourceFilesPattern] 9 | if (typeof cliOptions.configFilePath === 'string') { 10 | files.push(cliOptions.configFilePath) 11 | } else { 12 | files.push('package.json') 13 | } 14 | if (cliOptions.prependCssFilesPattern !== null) { 15 | files.push(cliOptions.prependCssFilesPattern) 16 | } 17 | if (cliOptions.appendCssFilesPattern !== null) { 18 | files.push(cliOptions.appendCssFilesPattern) 19 | } 20 | const watcher = chokidar.watch(files) 21 | async function onChangeAsync() { 22 | await build(cliOptions) 23 | log.info('Watching...') 24 | } 25 | watcher.on('ready', onChangeAsync) 26 | watcher.on('change', onChangeAsync) 27 | } 28 | -------------------------------------------------------------------------------- /scripts/print-theme-keys.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra' 2 | import * as path from 'path' 3 | 4 | const themeTypeRegex = /export type Theme = {([\S\s]*?)}/ 5 | const keysToOmit = ['baseSpace', 'baseFontSize', 'breakpoint'] 6 | 7 | async function main() { 8 | const file = path.join(path.resolve(__dirname, '..'), 'src', 'types.ts') 9 | const content = await fs.readFile(file, 'utf8') 10 | const match = content.match(themeTypeRegex) 11 | if (match === null) { 12 | throw new Error('`Theme` type not found') 13 | } 14 | const lines = match[1].trim().split('\n') 15 | const result = [] 16 | for (const line of lines) { 17 | const split = line.trim().split('?:') 18 | if (keysToOmit.indexOf(split[0]) === -1) { 19 | result.push(split[0]) 20 | } 21 | } 22 | result.sort() 23 | for (const key of result) { 24 | console.log(`- \`theme.${key}\``) // eslint-disable-line no-console 25 | } 26 | } 27 | main() 28 | -------------------------------------------------------------------------------- /src/utilities/extract-class-names-async/extract-class-names-async.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra' 2 | import * as globby from 'globby' 3 | 4 | import { extractClassNamesFromHtml } from './extract-class-names-from-html' 5 | import { extractClassNamesFromJs } from './extract-class-names-from-js' 6 | 7 | const jsFileRegex = /.[jt]sx?$/ 8 | 9 | export async function extractClassNamesAsync( 10 | pattern: string 11 | ): Promise> { 12 | const files = await globby(pattern) 13 | const result: { [key: string]: boolean } = {} 14 | for (const file of files) { 15 | const string = await fs.readFile(file, 'utf8') 16 | const classNames = 17 | jsFileRegex.test(file) === true 18 | ? extractClassNamesFromJs(string) 19 | : extractClassNamesFromHtml(string) 20 | for (const className of classNames) { 21 | result[className] = true 22 | } 23 | } 24 | return Object.keys(result).sort() 25 | } 26 | -------------------------------------------------------------------------------- /src/utilities/create-css/declarations/index.ts: -------------------------------------------------------------------------------- 1 | import { Declarations } from '../../../types' 2 | import { alignItems } from './align-items' 3 | import { cursor } from './cursor' 4 | import { display } from './display' 5 | import { flex } from './flex' 6 | import { flexWrap } from './flex-wrap' 7 | import { justifyContent } from './justify-content' 8 | import { outline } from './outline' 9 | import { position } from './position' 10 | import { select } from './select' 11 | import { textAlign } from './text-align' 12 | import { textDecoration } from './text-decoration' 13 | import { textTransform } from './text-transform' 14 | 15 | export const declarations: { [selector: string]: Declarations } = { 16 | ...alignItems, 17 | ...cursor, 18 | ...display, 19 | ...flex, 20 | ...flexWrap, 21 | ...outline, 22 | ...justifyContent, 23 | ...position, 24 | ...select, 25 | ...textAlign, 26 | ...textDecoration, 27 | ...textTransform 28 | } 29 | -------------------------------------------------------------------------------- /src/utilities/create-css/plugins/border-color.ts: -------------------------------------------------------------------------------- 1 | /* 2 | border-color 3 | 4 | `.border` | `border-color: ${theme.borderColor.default || theme.color.default};` 5 | `.border-${key}` | `border-color: ${theme.borderColor[key] || theme.color[key]};` 6 | */ 7 | 8 | import { Plugin, ThemeKeys } from '../../../types' 9 | 10 | export const borderColor: Plugin = { 11 | createDeclarations: function ({ 12 | matches, 13 | computeColorValue 14 | }: { 15 | matches: RegExpMatchArray 16 | computeColorValue: ( 17 | value: string, 18 | themeKeys: Array 19 | ) => null | string 20 | }): { [property: string]: string } { 21 | const borderColor = computeColorValue(matches[1], ['borderColor', 'color']) 22 | if (borderColor === null) { 23 | throw new Error(`Invalid border color: ${matches[1]}`) 24 | } 25 | return { 26 | 'border-color': `${borderColor}` 27 | } 28 | }, 29 | regex: /^border(?:-(.+))?$/ 30 | } 31 | -------------------------------------------------------------------------------- /src/utilities/create-css/plugins/letter-spacing.ts: -------------------------------------------------------------------------------- 1 | /* 2 | letter-spacing 3 | 4 | `.kerning` | `letter-spacing: ${theme.letterSpacing.default};` 5 | `.kerning-${key}` | `letter-spacing: ${theme.letterSpacing[key]};` 6 | */ 7 | 8 | import { Plugin, ThemeKeys } from '../../../types' 9 | 10 | export const letterSpacing: Plugin = { 11 | createDeclarations: function ({ 12 | computeNumericValue, 13 | matches 14 | }: { 15 | computeNumericValue: ( 16 | value: string, 17 | themeKeys: Array 18 | ) => null | string 19 | matches: RegExpMatchArray 20 | }): { [property: string]: string } { 21 | const letterSpacing = computeNumericValue(matches[1], ['letterSpacing']) 22 | if (letterSpacing === null) { 23 | throw new Error(`Invalid letter-spacing: ${matches[1]}`) 24 | } 25 | return { 26 | 'letter-spacing': `${letterSpacing}` 27 | } 28 | }, 29 | regex: /^(?:kerning|letter-spacing|tracking)(?:-(.+))?$/ 30 | } 31 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/font-weight.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block' 4 | 5 | test('font-weight not defined in `theme`', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | createCssDeclarationBlock('font-bold', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('font-bold', { 12 | fontWeight: {} 13 | }) 14 | }) 15 | }) 16 | 17 | test('custom font weight defined in `theme.fontWeight`', function (t) { 18 | t.plan(1) 19 | t.deepEqual( 20 | createCssDeclarationBlock('font-bold', { 21 | fontWeight: { 22 | bold: '600' 23 | } 24 | }), 25 | { 26 | breakpoint: null, 27 | className: 'font-bold', 28 | declarations: { 29 | 'font-weight': '600' 30 | }, 31 | pseudoClass: null, 32 | selector: 'font-bold' 33 | } 34 | ) 35 | }) 36 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/position/right/negative-right-position.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block' 4 | 5 | test('custom right position defined in `theme.space`', function (t) { 6 | t.plan(1) 7 | t.deepEqual( 8 | createCssDeclarationBlock('-right-sm', { space: { sm: '4px' } }), 9 | { 10 | breakpoint: null, 11 | className: '-right-sm', 12 | declarations: { 13 | right: '-4px' 14 | }, 15 | pseudoClass: null, 16 | selector: '-right-sm' 17 | } 18 | ) 19 | }) 20 | 21 | test('pixel right position', function (t) { 22 | t.plan(1) 23 | t.deepEqual(createCssDeclarationBlock('-right-2px', {}), { 24 | breakpoint: null, 25 | className: '-right-2px', 26 | declarations: { 27 | right: '-2px' 28 | }, 29 | pseudoClass: null, 30 | selector: '-right-2px' 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/position/bottom/negative-bottom-position.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block' 4 | 5 | test('custom bottom position defined in `theme.space`', function (t) { 6 | t.plan(1) 7 | t.deepEqual( 8 | createCssDeclarationBlock('-bottom-sm', { space: { sm: '4px' } }), 9 | { 10 | breakpoint: null, 11 | className: '-bottom-sm', 12 | declarations: { 13 | bottom: '-4px' 14 | }, 15 | pseudoClass: null, 16 | selector: '-bottom-sm' 17 | } 18 | ) 19 | }) 20 | 21 | test('pixel bottom position', function (t) { 22 | t.plan(1) 23 | t.deepEqual(createCssDeclarationBlock('-bottom-2px', {}), { 24 | breakpoint: null, 25 | className: '-bottom-2px', 26 | declarations: { 27 | bottom: '-2px' 28 | }, 29 | pseudoClass: null, 30 | selector: '-bottom-2px' 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/utilities/create-css/plugins/background-color.ts: -------------------------------------------------------------------------------- 1 | /* 2 | background-color 3 | 4 | `.bg` | `background-color: ${theme.backgroundColor.default || theme.color.default};` 5 | `.bg-${key}` | `background-color: ${theme.backgroundColor[key] || theme.color[key]};` 6 | */ 7 | 8 | import { Plugin, ThemeKeys } from '../../../types' 9 | 10 | export const backgroundColor: Plugin = { 11 | createDeclarations: function ({ 12 | matches, 13 | computeColorValue 14 | }: { 15 | matches: RegExpMatchArray 16 | computeColorValue: ( 17 | value: string, 18 | themeKeys: Array 19 | ) => null | string 20 | }): { [property: string]: string } { 21 | const backgroundColor = computeColorValue(matches[1], [ 22 | 'backgroundColor', 23 | 'color' 24 | ]) 25 | if (backgroundColor === null) { 26 | throw new Error(`Invalid background color: ${matches[1]}`) 27 | } 28 | return { 29 | 'background-color': `${backgroundColor}` 30 | } 31 | }, 32 | regex: /^bg(?:-(.+))?$/ 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Yuan Qing Lim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utilities/create-base-font-size-css.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../types' 2 | 3 | export function createBaseFontSizeCss(config: Config): string { 4 | if (typeof config.theme.baseFontSize === 'undefined') { 5 | return '' 6 | } 7 | const result: Array = [] 8 | for (const breakpoint of Object.keys(config.theme.baseFontSize)) { 9 | const fontSize = config.theme.baseFontSize[breakpoint] 10 | if (breakpoint === 'default') { 11 | result.push(createHtmlFontSizeCss(fontSize)) 12 | continue 13 | } 14 | if (typeof config.theme.breakpoint === 'undefined') { 15 | throw new Error('`theme.breakpoint` not defined in configuration') 16 | } 17 | if (typeof config.theme.breakpoint[breakpoint] === 'undefined') { 18 | throw new Error( 19 | `Breakpoint not defined in \`theme.breakpoint\`: ${breakpoint}` 20 | ) 21 | } 22 | result.push( 23 | `@media (min-width: ${ 24 | config.theme.breakpoint[breakpoint] 25 | }) { ${createHtmlFontSizeCss(fontSize)} }` 26 | ) 27 | } 28 | return result.join('') 29 | } 30 | 31 | function createHtmlFontSizeCss(fontSize: string): string { 32 | return `html { font-size: ${fontSize}; }` 33 | } 34 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/position/top/positive-top-position.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block' 4 | 5 | test('default top position', function (t) { 6 | t.plan(1) 7 | t.deepEqual(createCssDeclarationBlock('top', {}), { 8 | breakpoint: null, 9 | className: 'top', 10 | declarations: { 11 | top: '0' 12 | }, 13 | pseudoClass: null, 14 | selector: 'top' 15 | }) 16 | }) 17 | 18 | test('custom top position defined in `theme.space`', function (t) { 19 | t.plan(1) 20 | t.deepEqual(createCssDeclarationBlock('top-sm', { space: { sm: '4px' } }), { 21 | breakpoint: null, 22 | className: 'top-sm', 23 | declarations: { 24 | top: '4px' 25 | }, 26 | pseudoClass: null, 27 | selector: 'top-sm' 28 | }) 29 | }) 30 | 31 | test('pixel top position', function (t) { 32 | t.plan(1) 33 | t.deepEqual(createCssDeclarationBlock('top-2px', {}), { 34 | breakpoint: null, 35 | className: 'top-2px', 36 | declarations: { 37 | top: '2px' 38 | }, 39 | pseudoClass: null, 40 | selector: 'top-2px' 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/position/left/positive-left-position.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block' 4 | 5 | test('default left position', function (t) { 6 | t.plan(1) 7 | t.deepEqual(createCssDeclarationBlock('left', {}), { 8 | breakpoint: null, 9 | className: 'left', 10 | declarations: { 11 | left: '0' 12 | }, 13 | pseudoClass: null, 14 | selector: 'left' 15 | }) 16 | }) 17 | 18 | test('custom left position defined in `theme.space`', function (t) { 19 | t.plan(1) 20 | t.deepEqual(createCssDeclarationBlock('left-sm', { space: { sm: '4px' } }), { 21 | breakpoint: null, 22 | className: 'left-sm', 23 | declarations: { 24 | left: '4px' 25 | }, 26 | pseudoClass: null, 27 | selector: 'left-sm' 28 | }) 29 | }) 30 | 31 | test('pixel left position', function (t) { 32 | t.plan(1) 33 | t.deepEqual(createCssDeclarationBlock('left-2px', {}), { 34 | breakpoint: null, 35 | className: 'left-2px', 36 | declarations: { 37 | left: '2px' 38 | }, 39 | pseudoClass: null, 40 | selector: 'left-2px' 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/position/right/positive-right-position.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block' 4 | 5 | test('default right position', function (t) { 6 | t.plan(1) 7 | t.deepEqual(createCssDeclarationBlock('right', {}), { 8 | breakpoint: null, 9 | className: 'right', 10 | declarations: { 11 | right: '0' 12 | }, 13 | pseudoClass: null, 14 | selector: 'right' 15 | }) 16 | }) 17 | 18 | test('custom right position defined in `theme.space`', function (t) { 19 | t.plan(1) 20 | t.deepEqual(createCssDeclarationBlock('right-sm', { space: { sm: '4px' } }), { 21 | breakpoint: null, 22 | className: 'right-sm', 23 | declarations: { 24 | right: '4px' 25 | }, 26 | pseudoClass: null, 27 | selector: 'right-sm' 28 | }) 29 | }) 30 | 31 | test('pixel right position', function (t) { 32 | t.plan(1) 33 | t.deepEqual(createCssDeclarationBlock('right-2px', {}), { 34 | breakpoint: null, 35 | className: 'right-2px', 36 | declarations: { 37 | right: '2px' 38 | }, 39 | pseudoClass: null, 40 | selector: 'right-2px' 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/font-size.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block' 4 | 5 | test('font size not defined in `theme`', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | createCssDeclarationBlock('font-sm', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('font-sm', { 12 | fontSize: {} 13 | }) 14 | }) 15 | }) 16 | 17 | test('custom font size defined in `theme.fontSize`', function (t) { 18 | t.plan(1) 19 | t.deepEqual( 20 | createCssDeclarationBlock('font-sm', { 21 | fontSize: { 22 | sm: '1.0rem' 23 | } 24 | }), 25 | { 26 | breakpoint: null, 27 | className: 'font-sm', 28 | declarations: { 29 | 'font-size': '1.0rem' 30 | }, 31 | pseudoClass: null, 32 | selector: 'font-sm' 33 | } 34 | ) 35 | }) 36 | 37 | test('pixel font size', function (t) { 38 | t.plan(1) 39 | t.deepEqual(createCssDeclarationBlock('font-16px', {}), { 40 | breakpoint: null, 41 | className: 'font-16px', 42 | declarations: { 43 | 'font-size': '16px' 44 | }, 45 | pseudoClass: null, 46 | selector: 'font-16px' 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/position/bottom/positive-bottom-position.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block' 4 | 5 | test('default bottom position', function (t) { 6 | t.plan(1) 7 | t.deepEqual(createCssDeclarationBlock('bottom', {}), { 8 | breakpoint: null, 9 | className: 'bottom', 10 | declarations: { 11 | bottom: '0' 12 | }, 13 | pseudoClass: null, 14 | selector: 'bottom' 15 | }) 16 | }) 17 | 18 | test('custom bottom position defined in `theme.space`', function (t) { 19 | t.plan(1) 20 | t.deepEqual( 21 | createCssDeclarationBlock('bottom-sm', { space: { sm: '4px' } }), 22 | { 23 | breakpoint: null, 24 | className: 'bottom-sm', 25 | declarations: { 26 | bottom: '4px' 27 | }, 28 | pseudoClass: null, 29 | selector: 'bottom-sm' 30 | } 31 | ) 32 | }) 33 | 34 | test('pixel bottom position', function (t) { 35 | t.plan(1) 36 | t.deepEqual(createCssDeclarationBlock('bottom-2px', {}), { 37 | breakpoint: null, 38 | className: 'bottom-2px', 39 | declarations: { 40 | bottom: '2px' 41 | }, 42 | pseudoClass: null, 43 | selector: 'bottom-2px' 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/css/reset.css: -------------------------------------------------------------------------------- 1 | *, 2 | ::before, 3 | ::after { 4 | box-sizing: border-box; 5 | border-color: currentColor; 6 | border-style: solid; 7 | border-width: 0; 8 | } 9 | 10 | audio, 11 | canvas, 12 | embed, 13 | iframe, 14 | img, 15 | object, 16 | svg, 17 | video { 18 | display: block; 19 | } 20 | 21 | blockquote, 22 | dd, 23 | dl, 24 | fieldset, 25 | figure, 26 | h1, 27 | h2, 28 | h3, 29 | h4, 30 | h5, 31 | h6, 32 | hr, 33 | ol, 34 | p, 35 | pre, 36 | ul { 37 | margin: 0; 38 | } 39 | 40 | a { 41 | color: inherit; 42 | text-decoration: inherit; 43 | } 44 | 45 | button, 46 | input, 47 | optgroup, 48 | select, 49 | textarea { 50 | padding: 0; 51 | color: inherit; 52 | line-height: inherit; 53 | } 54 | 55 | button { 56 | background-color: transparent; 57 | background-image: none; 58 | } 59 | 60 | button:focus { 61 | outline: 1px dotted; 62 | outline: 5px auto -webkit-focus-ring-color; 63 | } 64 | 65 | fieldset { 66 | padding: 0; 67 | } 68 | 69 | h1, 70 | h2, 71 | h3, 72 | h4, 73 | h5, 74 | h6 { 75 | font-weight: inherit; 76 | font-size: inherit; 77 | } 78 | 79 | hr { 80 | border-top-width: 1px; 81 | } 82 | 83 | img { 84 | border-style: solid; 85 | } 86 | 87 | ol, 88 | ul { 89 | padding: 0; 90 | list-style: none; 91 | } 92 | 93 | table { 94 | border-collapse: collapse; 95 | } 96 | -------------------------------------------------------------------------------- /src/utilities/create-css/parse-class-name.ts: -------------------------------------------------------------------------------- 1 | import { ParsedClassName } from '../../types' 2 | 3 | const classRegex = /^(?:([^@]+)@)?([^:]+)(?::(.+))?$/ 4 | 5 | export function parseClassName( 6 | className: string, 7 | breakpoints: Array 8 | ): ParsedClassName { 9 | const matches = className.match(classRegex) 10 | if (matches === null) { 11 | throw new Error(`Invalid class name: ${className}`) 12 | } 13 | const breakpoint = typeof matches[1] === 'undefined' ? null : matches[1] 14 | if (breakpoint !== null && breakpoints.indexOf(breakpoint) === -1) { 15 | throw new Error(`Invalid breakpoint: ${breakpoint}`) 16 | } 17 | if (typeof matches[3] === 'undefined') { 18 | return { 19 | breakpoint, 20 | pseudoClass: null, 21 | selector: matches[2] 22 | } 23 | } 24 | return { 25 | breakpoint, 26 | pseudoClass: parsePseudoClass(matches[2]), 27 | selector: matches[3] 28 | } 29 | } 30 | 31 | const groupPseudoClassRegex = /^group-(.+)$/ 32 | 33 | function parsePseudoClass( 34 | pseudoClass: string 35 | ): { 36 | value: string 37 | isParent: boolean 38 | } { 39 | const matches = pseudoClass.match(groupPseudoClassRegex) 40 | if (matches === null) { 41 | return { 42 | isParent: false, 43 | value: pseudoClass 44 | } 45 | } 46 | return { 47 | isParent: true, 48 | value: matches[1] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/text-style.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block' 4 | 5 | test('text style not defined in `theme`', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | createCssDeclarationBlock('text-sm', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('text-sm', { 12 | fontSize: {}, 13 | fontWeight: {}, 14 | letterSpacing: {}, 15 | lineHeight: {} 16 | }) 17 | }) 18 | }) 19 | 20 | test('text style with font size, font weight, letter-spacing and line-height defined in `theme`', function (t) { 21 | t.plan(1) 22 | t.deepEqual( 23 | createCssDeclarationBlock('text-sm', { 24 | fontSize: { 25 | sm: '1.0rem' 26 | }, 27 | fontWeight: { 28 | sm: '400' 29 | }, 30 | letterSpacing: { 31 | sm: '0.025em' 32 | }, 33 | lineHeight: { 34 | sm: '1.25rem' 35 | } 36 | }), 37 | { 38 | breakpoint: null, 39 | className: 'text-sm', 40 | declarations: { 41 | 'font-size': '1.0rem', 42 | 'font-weight': '400', 43 | 'letter-spacing': '0.025em', 44 | 'line-height': '1.25rem' 45 | }, 46 | pseudoClass: null, 47 | selector: 'text-sm' 48 | } 49 | ) 50 | }) 51 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/color.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block' 4 | 5 | test('color not defined in `theme`', function (t) { 6 | t.plan(3) 7 | t.throw(function () { 8 | createCssDeclarationBlock('color', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('color-black', {}) 12 | }) 13 | t.throw(function () { 14 | createCssDeclarationBlock('color-black', { 15 | color: {} 16 | }) 17 | }) 18 | }) 19 | 20 | test('default color defined in `theme.color`', function (t) { 21 | t.plan(1) 22 | t.deepEqual( 23 | createCssDeclarationBlock('color', { 24 | color: { 25 | default: '#ffffff' 26 | } 27 | }), 28 | { 29 | breakpoint: null, 30 | className: 'color', 31 | declarations: { 32 | color: '#ffffff' 33 | }, 34 | pseudoClass: null, 35 | selector: 'color' 36 | } 37 | ) 38 | }) 39 | 40 | test('custom color defined in `theme.color`', function (t) { 41 | t.plan(1) 42 | t.deepEqual( 43 | createCssDeclarationBlock('color-black', { 44 | color: { 45 | black: '#000000' 46 | } 47 | }), 48 | { 49 | breakpoint: null, 50 | className: 'color-black', 51 | declarations: { 52 | color: '#000000' 53 | }, 54 | pseudoClass: null, 55 | selector: 'color-black' 56 | } 57 | ) 58 | }) 59 | -------------------------------------------------------------------------------- /src/utilities/create-css/plugins/position.ts: -------------------------------------------------------------------------------- 1 | /* 2 | top, right, bottom, left 3 | 4 | - `value` = `theme.space[key]` || `resolveNumericValue(key)` 5 | 6 | `.top` | `top: 0;` 7 | `.top-${key}` | `top: ${value};` 8 | `.-top-${key}` | `top: -${value};` 9 | `.right` | `right: 0;` 10 | `.right-${key}` | `right: ${value};` 11 | `.-right-${key}` | `right: -${value};` 12 | `.bottom` | `bottom: 0;` 13 | `.bottom-${key}` | `bottom: ${value};` 14 | `.-bottom-${key}` | `bottom: -${value};` 15 | `.left` | `left: 0;` 16 | `.left-${key}` | `left: ${value};` 17 | `.-left-${key}` | `left: -${value};` 18 | */ 19 | 20 | import { Plugin, ThemeKeys } from '../../../types' 21 | 22 | export const position: Plugin = { 23 | createDeclarations: function ({ 24 | matches, 25 | computeNumericValue 26 | }: { 27 | matches: RegExpMatchArray 28 | computeNumericValue: ( 29 | value: string, 30 | themeKeys: Array 31 | ) => null | string 32 | }): { [property: string]: string } { 33 | const property = matches[2] 34 | const value = computeNumericValue( 35 | `${matches[1] === '-' ? '-' : ''}${ 36 | typeof matches[3] === 'undefined' ? '0' : matches[3] 37 | }`, 38 | ['space'] 39 | ) 40 | if (value === null) { 41 | throw new Error( 42 | `Invalid ${ 43 | matches[1] === '-' ? 'negative ' : '' 44 | }${property} position: ${matches[3]}` 45 | ) 46 | } 47 | return { 48 | [property]: value 49 | } 50 | }, 51 | regex: /^(-?)(top|right|bottom|left)?(?:-(.+))?$/ 52 | } 53 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/font-family.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block' 4 | 5 | test('font-family not defined in `theme`', function (t) { 6 | t.plan(3) 7 | t.throw(function () { 8 | createCssDeclarationBlock('font', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('font', { 12 | fontFamily: {} 13 | }) 14 | }) 15 | t.throw(function () { 16 | createCssDeclarationBlock('font-serif', { 17 | fontFamily: {} 18 | }) 19 | }) 20 | }) 21 | 22 | test('default font defined in `theme.fontFamily`', function (t) { 23 | t.plan(1) 24 | t.deepEqual( 25 | createCssDeclarationBlock('font', { 26 | fontFamily: { 27 | default: 'sans-serif' 28 | } 29 | }), 30 | { 31 | breakpoint: null, 32 | className: 'font', 33 | declarations: { 34 | 'font-family': 'sans-serif' 35 | }, 36 | pseudoClass: null, 37 | selector: 'font' 38 | } 39 | ) 40 | }) 41 | 42 | test('custom font defined in `theme.font`', function (t) { 43 | t.plan(1) 44 | t.deepEqual( 45 | createCssDeclarationBlock('font-serif', { 46 | fontFamily: { 47 | serif: 'serif' 48 | } 49 | }), 50 | { 51 | breakpoint: null, 52 | className: 'font-serif', 53 | declarations: { 54 | 'font-family': 'serif' 55 | }, 56 | pseudoClass: null, 57 | selector: 'font-serif' 58 | } 59 | ) 60 | }) 61 | -------------------------------------------------------------------------------- /src/utilities/create-css/create-css-declaration-block.ts: -------------------------------------------------------------------------------- 1 | import { CssDeclarationBlock, Theme } from '../../types' 2 | import { computeColorValueFactory } from './compute-color-value-factory' 3 | import { computeNumericValueFactory } from './compute-numeric-value-factory' 4 | import { mapSelectorToDeclaration } from './map-selector-to-declaration' 5 | import { parseClassName } from './parse-class-name' 6 | import { plugins } from './plugins' 7 | 8 | export function createCssDeclarationBlock( 9 | className: string, 10 | theme: Theme 11 | ): null | CssDeclarationBlock { 12 | const { breakpoint, pseudoClass, selector } = parseClassName( 13 | className, 14 | typeof theme.breakpoint === 'undefined' ? [] : Object.keys(theme.breakpoint) 15 | ) 16 | const declarations = mapSelectorToDeclaration(selector) 17 | if (declarations !== null) { 18 | return { 19 | breakpoint, 20 | className, 21 | declarations, 22 | pseudoClass, 23 | selector 24 | } 25 | } 26 | const computeNumericValue = computeNumericValueFactory(theme) 27 | const computeColorValue = computeColorValueFactory(theme) 28 | let matches 29 | for (const plugin of plugins) { 30 | matches = selector.match(plugin.regex) 31 | if (matches !== null) { 32 | return { 33 | breakpoint, 34 | className, 35 | declarations: plugin.createDeclarations({ 36 | computeColorValue, 37 | computeNumericValue, 38 | matches, 39 | theme 40 | }), 41 | pseudoClass, 42 | selector 43 | } 44 | } 45 | } 46 | return null 47 | } 48 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/line-height.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block' 4 | 5 | test('line-height not defined in `theme`', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | createCssDeclarationBlock('leading', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('leading', { lineHeight: {} }) 12 | }) 13 | }) 14 | 15 | test('default line-height defined in `theme.lineHeight`', function (t) { 16 | t.plan(1) 17 | t.deepEqual( 18 | createCssDeclarationBlock('leading', { 19 | lineHeight: { 20 | default: '1.5rem' 21 | } 22 | }), 23 | { 24 | breakpoint: null, 25 | className: 'leading', 26 | declarations: { 27 | 'line-height': '1.5rem' 28 | }, 29 | pseudoClass: null, 30 | selector: 'leading' 31 | } 32 | ) 33 | }) 34 | 35 | test('custom line-height defined in `theme.lineHeight`', function (t) { 36 | t.plan(1) 37 | t.deepEqual( 38 | createCssDeclarationBlock('leading-sm', { lineHeight: { sm: '1.25rem' } }), 39 | { 40 | breakpoint: null, 41 | className: 'leading-sm', 42 | declarations: { 43 | 'line-height': '1.25rem' 44 | }, 45 | pseudoClass: null, 46 | selector: 'leading-sm' 47 | } 48 | ) 49 | }) 50 | 51 | test('pixel line-height', function (t) { 52 | t.plan(1) 53 | t.deepEqual(createCssDeclarationBlock('leading-16px', {}), { 54 | breakpoint: null, 55 | className: 'leading-16px', 56 | declarations: { 57 | 'line-height': '16px' 58 | }, 59 | pseudoClass: null, 60 | selector: 'leading-16px' 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /src/utilities/extract-class-names-async/__tests__/extract-class-names-from-html.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { extractClassNamesFromHtml } from '../extract-class-names-from-html' 4 | 5 | test('empty string', function (t) { 6 | t.plan(1) 7 | t.deepEqual(extractClassNamesFromHtml(''), []) 8 | }) 9 | 10 | test('no class attributes', function (t) { 11 | t.plan(1) 12 | t.deepEqual(extractClassNamesFromHtml('
'), []) 13 | }) 14 | 15 | test('empty class attribute', function (t) { 16 | t.plan(1) 17 | t.deepEqual(extractClassNamesFromHtml('
'), []) 18 | }) 19 | 20 | test('single class', function (t) { 21 | t.plan(1) 22 | t.deepEqual(extractClassNamesFromHtml('
'), ['flex']) 23 | }) 24 | 25 | test('multiple classes', function (t) { 26 | t.plan(1) 27 | t.deepEqual(extractClassNamesFromHtml('
'), [ 28 | 'bg-black', 29 | 'flex' 30 | ]) 31 | }) 32 | 33 | test('multiple matches', function (t) { 34 | t.plan(1) 35 | t.deepEqual( 36 | extractClassNamesFromHtml( 37 | '

' 38 | ), 39 | ['bg-black', 'color-red', 'flex'] 40 | ) 41 | }) 42 | 43 | test('single-quoted attributes', function (t) { 44 | t.plan(1) 45 | t.deepEqual( 46 | extractClassNamesFromHtml( 47 | "

" 48 | ), 49 | ['bg-black', 'color-red', 'flex'] 50 | ) 51 | }) 52 | 53 | test('leading and trailing consecutive spaces', function (t) { 54 | t.plan(1) 55 | t.deepEqual( 56 | extractClassNamesFromHtml( 57 | "

" 58 | ), 59 | ['bg-black', 'color-red', 'flex'] 60 | ) 61 | }) 62 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/letter-spacing.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block' 4 | 5 | test('letter-spacing not defined in `theme`', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | createCssDeclarationBlock('kerning', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('kerning', { letterSpacing: {} }) 12 | }) 13 | }) 14 | 15 | test('default letter-spacing defined in `theme.letterSpacing`', function (t) { 16 | t.plan(1) 17 | t.deepEqual( 18 | createCssDeclarationBlock('kerning', { 19 | letterSpacing: { 20 | default: '0.05em' 21 | } 22 | }), 23 | { 24 | breakpoint: null, 25 | className: 'kerning', 26 | declarations: { 27 | 'letter-spacing': '0.05em' 28 | }, 29 | pseudoClass: null, 30 | selector: 'kerning' 31 | } 32 | ) 33 | }) 34 | 35 | test('custom letter-spacing defined in `theme.letterSpacing`', function (t) { 36 | t.plan(1) 37 | t.deepEqual( 38 | createCssDeclarationBlock('kerning-sm', { 39 | letterSpacing: { sm: '0.025em' } 40 | }), 41 | { 42 | breakpoint: null, 43 | className: 'kerning-sm', 44 | declarations: { 45 | 'letter-spacing': '0.025em' 46 | }, 47 | pseudoClass: null, 48 | selector: 'kerning-sm' 49 | } 50 | ) 51 | }) 52 | 53 | test('pixel letter-spacing', function (t) { 54 | t.plan(1) 55 | t.deepEqual(createCssDeclarationBlock('kerning-1px', {}), { 56 | breakpoint: null, 57 | className: 'kerning-1px', 58 | declarations: { 59 | 'letter-spacing': '1px' 60 | }, 61 | pseudoClass: null, 62 | selector: 'kerning-1px' 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/width-and-height/width/width.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block' 4 | 5 | test('width not defined in `theme`', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | createCssDeclarationBlock('w', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('w', { width: {} }) 12 | }) 13 | }) 14 | 15 | test('default width defined in `theme.width`', function (t) { 16 | t.plan(1) 17 | t.deepEqual( 18 | createCssDeclarationBlock('w', { 19 | width: { 20 | default: '8px' 21 | } 22 | }), 23 | { 24 | breakpoint: null, 25 | className: 'w', 26 | declarations: { 27 | width: '8px' 28 | }, 29 | pseudoClass: null, 30 | selector: 'w' 31 | } 32 | ) 33 | }) 34 | 35 | test('custom width defined in `theme.width`', function (t) { 36 | t.plan(1) 37 | t.deepEqual(createCssDeclarationBlock('w-sm', { width: { sm: '4px' } }), { 38 | breakpoint: null, 39 | className: 'w-sm', 40 | declarations: { 41 | width: '4px' 42 | }, 43 | pseudoClass: null, 44 | selector: 'w-sm' 45 | }) 46 | }) 47 | 48 | test('pixel width', function (t) { 49 | t.plan(1) 50 | t.deepEqual(createCssDeclarationBlock('w-2px', {}), { 51 | breakpoint: null, 52 | className: 'w-2px', 53 | declarations: { 54 | width: '2px' 55 | }, 56 | pseudoClass: null, 57 | selector: 'w-2px' 58 | }) 59 | }) 60 | 61 | test('screen width', function (t) { 62 | t.plan(1) 63 | t.deepEqual(createCssDeclarationBlock('w-screen', {}), { 64 | breakpoint: null, 65 | className: 'w-screen', 66 | declarations: { 67 | width: '100vw' 68 | }, 69 | pseudoClass: null, 70 | selector: 'w-screen' 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/width-and-height/height/height.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block' 4 | 5 | test('height not defined in `theme`', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | createCssDeclarationBlock('h', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('h', { height: {} }) 12 | }) 13 | }) 14 | 15 | test('default height defined in `theme.height`', function (t) { 16 | t.plan(1) 17 | t.deepEqual( 18 | createCssDeclarationBlock('h', { 19 | height: { 20 | default: '8px' 21 | } 22 | }), 23 | { 24 | breakpoint: null, 25 | className: 'h', 26 | declarations: { 27 | height: '8px' 28 | }, 29 | pseudoClass: null, 30 | selector: 'h' 31 | } 32 | ) 33 | }) 34 | 35 | test('custom height defined in `theme.height`', function (t) { 36 | t.plan(1) 37 | t.deepEqual(createCssDeclarationBlock('h-sm', { height: { sm: '4px' } }), { 38 | breakpoint: null, 39 | className: 'h-sm', 40 | declarations: { 41 | height: '4px' 42 | }, 43 | pseudoClass: null, 44 | selector: 'h-sm' 45 | }) 46 | }) 47 | 48 | test('pixel height', function (t) { 49 | t.plan(1) 50 | t.deepEqual(createCssDeclarationBlock('h-2px', {}), { 51 | breakpoint: null, 52 | className: 'h-2px', 53 | declarations: { 54 | height: '2px' 55 | }, 56 | pseudoClass: null, 57 | selector: 'h-2px' 58 | }) 59 | }) 60 | 61 | test('screen height', function (t) { 62 | t.plan(1) 63 | t.deepEqual(createCssDeclarationBlock('h-screen', {}), { 64 | breakpoint: null, 65 | className: 'h-screen', 66 | declarations: { 67 | height: '100vh' 68 | }, 69 | pseudoClass: null, 70 | selector: 'h-screen' 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /src/utilities/create-css/plugins/text-style.ts: -------------------------------------------------------------------------------- 1 | /* 2 | text style 3 | 4 | `.text` | `font-size: ${theme.fontSize.default};`
`font-weight: ${theme.fontWeight.default};`
`letter-spacing: ${theme.letterSpacing.default};`
`line-height: ${theme.lineHeight.default};` 5 | `.text-${key}` | `font-size: ${theme.fontSize[key]};`
`font-weight: ${theme.fontWeight[key]};`
`letter-spacing: ${theme.letterSpacing[key]};`
`line-height: ${theme.lineHeight[key]};` 6 | */ 7 | 8 | import { Plugin, Theme } from '../../../types' 9 | 10 | export const textStyle: Plugin = { 11 | createDeclarations: function ({ 12 | matches, 13 | theme 14 | }: { 15 | matches: RegExpMatchArray 16 | theme: Theme 17 | }): { [property: string]: string } { 18 | const result: { [property: string]: string } = {} 19 | const key = typeof matches[1] === 'undefined' ? 'default' : matches[1] 20 | if (typeof theme.fontSize !== 'undefined') { 21 | const fontSize = theme.fontSize[key] 22 | if (typeof fontSize !== 'undefined') { 23 | result['font-size'] = fontSize 24 | } 25 | } 26 | if (typeof theme.fontWeight !== 'undefined') { 27 | const fontWeight = theme.fontWeight[key] 28 | if (typeof fontWeight !== 'undefined') { 29 | result['font-weight'] = fontWeight 30 | } 31 | } 32 | if (typeof theme.letterSpacing !== 'undefined') { 33 | const letterSpacing = theme.letterSpacing[key] 34 | if (typeof letterSpacing !== 'undefined') { 35 | result['letter-spacing'] = letterSpacing 36 | } 37 | } 38 | if (typeof theme.lineHeight !== 'undefined') { 39 | const lineHeight = theme.lineHeight[key] 40 | if (typeof lineHeight !== 'undefined') { 41 | result['line-height'] = lineHeight 42 | } 43 | } 44 | if (Object.keys(result).length === 0) { 45 | throw new Error('Invalid text style') 46 | } 47 | return result 48 | }, 49 | regex: /^text(?:-(.+))?$/ 50 | } 51 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as sade from 'sade' 3 | 4 | import { build } from './build' 5 | import { watch } from './watch' 6 | 7 | sade('generate-css ', true) 8 | .describe( 9 | 'Dynamically generate functional CSS classes from HTML and JavaScript source files' 10 | ) 11 | .option( 12 | '-a, --append', 13 | 'Glob pattern for CSS files to append to the generated CSS file' 14 | ) 15 | .option('-c, --config', 'Path to a `generate-css` configuration file') 16 | .option('-m, --minify', 'Whether to minify the generated CSS file', false) 17 | .option('-o, --output', 'Path to write the generated CSS file') 18 | .option( 19 | '-p, --prepend', 20 | 'Glob pattern for CSS files to prepend to the generated CSS file' 21 | ) 22 | .option( 23 | '-w, --watch', 24 | 'Whether to automatically generate a CSS file on changes to the source files', 25 | false 26 | ) 27 | .example('--append reset.css') 28 | .example('--minify') 29 | .example('--output style.css') 30 | .example('--prepend custom.css') 31 | .example('--watch') 32 | .action(async function ( 33 | pattern: string, 34 | options: { 35 | append: string 36 | config: string 37 | minify: boolean 38 | output: string 39 | prepend: string 40 | watch: boolean 41 | } 42 | ) { 43 | const cliOptions = { 44 | appendCssFilesPattern: 45 | typeof options.append === 'undefined' ? null : options.append, 46 | configFilePath: 47 | typeof options.config === 'undefined' ? null : options.config, 48 | minify: options.minify, 49 | outputPath: typeof options.output === 'undefined' ? null : options.output, 50 | prependCssFilesPattern: 51 | typeof options.prepend === 'undefined' ? null : options.prepend, 52 | sourceFilesPattern: pattern 53 | } 54 | if (options.watch === true) { 55 | watch(cliOptions) 56 | return 57 | } 58 | await build(cliOptions) 59 | }) 60 | .parse(process.argv) 61 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/width-and-height/width/max-width.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block' 4 | 5 | test('max-width not defined in `theme`', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | createCssDeclarationBlock('maxw', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('maxw', { maxWidth: {} }) 12 | }) 13 | }) 14 | 15 | test('default max-width defined in `theme.maxWidth`', function (t) { 16 | t.plan(1) 17 | t.deepEqual( 18 | createCssDeclarationBlock('maxw', { 19 | maxWidth: { 20 | default: '100%' 21 | } 22 | }), 23 | { 24 | breakpoint: null, 25 | className: 'maxw', 26 | declarations: { 27 | 'max-width': '100%' 28 | }, 29 | pseudoClass: null, 30 | selector: 'maxw' 31 | } 32 | ) 33 | }) 34 | 35 | test('custom max-width defined in `theme.maxWidth`', function (t) { 36 | t.plan(1) 37 | t.deepEqual( 38 | createCssDeclarationBlock('maxw-sm', { maxWidth: { sm: '320px' } }), 39 | { 40 | breakpoint: null, 41 | className: 'maxw-sm', 42 | declarations: { 43 | 'max-width': '320px' 44 | }, 45 | pseudoClass: null, 46 | selector: 'maxw-sm' 47 | } 48 | ) 49 | }) 50 | 51 | test('pixel max-width', function (t) { 52 | t.plan(1) 53 | t.deepEqual(createCssDeclarationBlock('maxw-320px', {}), { 54 | breakpoint: null, 55 | className: 'maxw-320px', 56 | declarations: { 57 | 'max-width': '320px' 58 | }, 59 | pseudoClass: null, 60 | selector: 'maxw-320px' 61 | }) 62 | }) 63 | 64 | test('screen max-width', function (t) { 65 | t.plan(1) 66 | t.deepEqual(createCssDeclarationBlock('maxw-screen', {}), { 67 | breakpoint: null, 68 | className: 'maxw-screen', 69 | declarations: { 70 | 'max-width': '100vw' 71 | }, 72 | pseudoClass: null, 73 | selector: 'maxw-screen' 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/width-and-height/width/min-width.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block' 4 | 5 | test('min-width not defined in `theme`', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | createCssDeclarationBlock('minw', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('minw', { minWidth: {} }) 12 | }) 13 | }) 14 | 15 | test('default min-width defined in `theme.minWidth`', function (t) { 16 | t.plan(1) 17 | t.deepEqual( 18 | createCssDeclarationBlock('minw', { 19 | minWidth: { 20 | default: '100%' 21 | } 22 | }), 23 | { 24 | breakpoint: null, 25 | className: 'minw', 26 | declarations: { 27 | 'min-width': '100%' 28 | }, 29 | pseudoClass: null, 30 | selector: 'minw' 31 | } 32 | ) 33 | }) 34 | 35 | test('custom min-width defined in `theme.minWidth`', function (t) { 36 | t.plan(1) 37 | t.deepEqual( 38 | createCssDeclarationBlock('minw-sm', { minWidth: { sm: '320px' } }), 39 | { 40 | breakpoint: null, 41 | className: 'minw-sm', 42 | declarations: { 43 | 'min-width': '320px' 44 | }, 45 | pseudoClass: null, 46 | selector: 'minw-sm' 47 | } 48 | ) 49 | }) 50 | 51 | test('pixel min-width', function (t) { 52 | t.plan(1) 53 | t.deepEqual(createCssDeclarationBlock('minw-320px', {}), { 54 | breakpoint: null, 55 | className: 'minw-320px', 56 | declarations: { 57 | 'min-width': '320px' 58 | }, 59 | pseudoClass: null, 60 | selector: 'minw-320px' 61 | }) 62 | }) 63 | 64 | test('screen min-width', function (t) { 65 | t.plan(1) 66 | t.deepEqual(createCssDeclarationBlock('minw-screen', {}), { 67 | breakpoint: null, 68 | className: 'minw-screen', 69 | declarations: { 70 | 'min-width': '100vw' 71 | }, 72 | pseudoClass: null, 73 | selector: 'minw-screen' 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/width-and-height/height/max-height.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block' 4 | 5 | test('max-height not defined in `theme`', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | createCssDeclarationBlock('maxh', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('maxh', { maxHeight: {} }) 12 | }) 13 | }) 14 | 15 | test('default max-height defined in `theme.maxHeight`', function (t) { 16 | t.plan(1) 17 | t.deepEqual( 18 | createCssDeclarationBlock('maxh', { 19 | maxHeight: { 20 | default: '100%' 21 | } 22 | }), 23 | { 24 | breakpoint: null, 25 | className: 'maxh', 26 | declarations: { 27 | 'max-height': '100%' 28 | }, 29 | pseudoClass: null, 30 | selector: 'maxh' 31 | } 32 | ) 33 | }) 34 | 35 | test('custom max-height defined in `theme.maxHeight`', function (t) { 36 | t.plan(1) 37 | t.deepEqual( 38 | createCssDeclarationBlock('maxh-sm', { maxHeight: { sm: '320px' } }), 39 | { 40 | breakpoint: null, 41 | className: 'maxh-sm', 42 | declarations: { 43 | 'max-height': '320px' 44 | }, 45 | pseudoClass: null, 46 | selector: 'maxh-sm' 47 | } 48 | ) 49 | }) 50 | 51 | test('pixel max-height', function (t) { 52 | t.plan(1) 53 | t.deepEqual(createCssDeclarationBlock('maxh-320px', {}), { 54 | breakpoint: null, 55 | className: 'maxh-320px', 56 | declarations: { 57 | 'max-height': '320px' 58 | }, 59 | pseudoClass: null, 60 | selector: 'maxh-320px' 61 | }) 62 | }) 63 | 64 | test('screen max-height', function (t) { 65 | t.plan(1) 66 | t.deepEqual(createCssDeclarationBlock('maxh-screen', {}), { 67 | breakpoint: null, 68 | className: 'maxh-screen', 69 | declarations: { 70 | 'max-height': '100vh' 71 | }, 72 | pseudoClass: null, 73 | selector: 'maxh-screen' 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/width-and-height/height/min-height.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block' 4 | 5 | test('min-height not defined in `theme`', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | createCssDeclarationBlock('minh', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('minh', { minHeight: {} }) 12 | }) 13 | }) 14 | 15 | test('default min-height defined in `theme.minHeight`', function (t) { 16 | t.plan(1) 17 | t.deepEqual( 18 | createCssDeclarationBlock('minh', { 19 | minHeight: { 20 | default: '100%' 21 | } 22 | }), 23 | { 24 | breakpoint: null, 25 | className: 'minh', 26 | declarations: { 27 | 'min-height': '100%' 28 | }, 29 | pseudoClass: null, 30 | selector: 'minh' 31 | } 32 | ) 33 | }) 34 | 35 | test('custom min-height defined in `theme.minHeight`', function (t) { 36 | t.plan(1) 37 | t.deepEqual( 38 | createCssDeclarationBlock('minh-sm', { minHeight: { sm: '320px' } }), 39 | { 40 | breakpoint: null, 41 | className: 'minh-sm', 42 | declarations: { 43 | 'min-height': '320px' 44 | }, 45 | pseudoClass: null, 46 | selector: 'minh-sm' 47 | } 48 | ) 49 | }) 50 | 51 | test('pixel min-height', function (t) { 52 | t.plan(1) 53 | t.deepEqual(createCssDeclarationBlock('minh-320px', {}), { 54 | breakpoint: null, 55 | className: 'minh-320px', 56 | declarations: { 57 | 'min-height': '320px' 58 | }, 59 | pseudoClass: null, 60 | selector: 'minh-320px' 61 | }) 62 | }) 63 | 64 | test('screen min-height', function (t) { 65 | t.plan(1) 66 | t.deepEqual(createCssDeclarationBlock('minh-screen', {}), { 67 | breakpoint: null, 68 | className: 'minh-screen', 69 | declarations: { 70 | 'min-height': '100vh' 71 | }, 72 | pseudoClass: null, 73 | selector: 'minh-screen' 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/parse-class-name.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { parseClassName } from '../parse-class-name' 4 | 5 | test('invalid class name', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | parseClassName('', []) 9 | }) 10 | t.throw(function () { 11 | parseClassName(':', []) 12 | }) 13 | }) 14 | 15 | test('plain selector', function (t) { 16 | t.plan(1) 17 | t.deepEqual(parseClassName('block', []), { 18 | breakpoint: null, 19 | pseudoClass: null, 20 | selector: 'block' 21 | }) 22 | }) 23 | 24 | test('breakpoint not defined', function (t) { 25 | t.plan(1) 26 | t.throw(function () { 27 | parseClassName('sm@block', []) 28 | }) 29 | }) 30 | 31 | test('breakpoint defined', function (t) { 32 | t.plan(1) 33 | t.deepEqual(parseClassName('sm@block', ['sm']), { 34 | breakpoint: 'sm', 35 | pseudoClass: null, 36 | selector: 'block' 37 | }) 38 | }) 39 | 40 | test('pseudo-class', function (t) { 41 | t.plan(1) 42 | t.deepEqual(parseClassName('hover:block', []), { 43 | breakpoint: null, 44 | pseudoClass: { 45 | isParent: false, 46 | value: 'hover' 47 | }, 48 | selector: 'block' 49 | }) 50 | }) 51 | 52 | test('group pseudo-class', function (t) { 53 | t.plan(1) 54 | t.deepEqual(parseClassName('group-hover:block', []), { 55 | breakpoint: null, 56 | pseudoClass: { 57 | isParent: true, 58 | value: 'hover' 59 | }, 60 | selector: 'block' 61 | }) 62 | }) 63 | 64 | test('valid breakpoint and pseudo-class', function (t) { 65 | t.plan(1) 66 | t.deepEqual(parseClassName('sm@hover:block', ['sm']), { 67 | breakpoint: 'sm', 68 | pseudoClass: { 69 | isParent: false, 70 | value: 'hover' 71 | }, 72 | selector: 'block' 73 | }) 74 | }) 75 | 76 | test('valid breakpoint and group pseudo-class', function (t) { 77 | t.plan(1) 78 | t.deepEqual(parseClassName('sm@group-hover:block', ['sm']), { 79 | breakpoint: 'sm', 80 | pseudoClass: { 81 | isParent: true, 82 | value: 'hover' 83 | }, 84 | selector: 'block' 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /src/utilities/create-css/plugins/font.ts: -------------------------------------------------------------------------------- 1 | /* 2 | font-family 3 | 4 | `.font` | `font-family: ${theme.fontFamily.default};` 5 | `.font-${key}` | `font-family: ${theme.fontFamily[key]};` 6 | --- 7 | font-size 8 | 9 | `.font-${key}` | `font-size: ${theme.fontSize[key]};` 10 | --- 11 | font-weight 12 | 13 | `.font-${key}` | `font-weight: ${theme.fontWeight[key]};` 14 | */ 15 | 16 | import { Plugin, Theme, ThemeKeys } from '../../../types' 17 | 18 | export const font: Plugin = { 19 | createDeclarations: function ({ 20 | computeNumericValue, 21 | matches, 22 | theme 23 | }: { 24 | computeNumericValue: ( 25 | value: string, 26 | themeKeys: Array 27 | ) => null | string 28 | matches: RegExpMatchArray 29 | theme: Theme 30 | }): { [property: string]: string } { 31 | if (typeof matches[1] === 'undefined') { 32 | if (typeof theme.fontFamily === 'undefined') { 33 | throw new Error('`theme.fontFamily` not defined in configuration') 34 | } 35 | const fontFamily = theme.fontFamily.default 36 | if (typeof fontFamily === 'undefined') { 37 | throw new Error( 38 | '`theme.fontFamily.default` not defined in configuration' 39 | ) 40 | } 41 | return { 42 | 'font-family': `${fontFamily}` 43 | } 44 | } 45 | if (typeof theme.fontFamily !== 'undefined') { 46 | const fontFamily = theme.fontFamily[matches[1]] 47 | if (typeof fontFamily !== 'undefined') { 48 | return { 49 | 'font-family': `${fontFamily}` 50 | } 51 | } 52 | } 53 | if (typeof theme.fontWeight !== 'undefined') { 54 | const fontWeight = theme.fontWeight[matches[1]] 55 | if (typeof fontWeight !== 'undefined') { 56 | return { 57 | 'font-weight': `${fontWeight}` 58 | } 59 | } 60 | } 61 | const fontSize = computeNumericValue(matches[1], ['fontSize']) 62 | if (fontSize === null) { 63 | throw new Error(`Invalid font size: ${matches[1]}`) 64 | } 65 | return { 66 | 'font-size': `${fontSize}` 67 | } 68 | }, 69 | regex: /^font(?:-(.+))?$/ 70 | } 71 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface CliOptions { 2 | appendCssFilesPattern: null | string 3 | configFilePath: null | string 4 | minify: boolean 5 | outputPath: null | string 6 | prependCssFilesPattern: null | string 7 | sourceFilesPattern: string 8 | } 9 | 10 | export interface Config extends CliOptions { 11 | reset: boolean 12 | theme: Theme 13 | } 14 | 15 | export type Theme = { 16 | backgroundColor?: ThemeItem 17 | baseFontSize?: ThemeItem 18 | baseSpace?: string 19 | borderColor?: ThemeItem 20 | borderRadius?: ThemeItem 21 | borderWidth?: ThemeItem 22 | breakpoint?: ThemeItem 23 | color?: ThemeItem 24 | fontFamily?: ThemeItem 25 | fontSize?: ThemeItem 26 | fontWeight?: ThemeItem 27 | height?: ThemeItem 28 | letterSpacing?: ThemeItem 29 | lineHeight?: ThemeItem 30 | margin?: ThemeItem 31 | maxHeight?: ThemeItem 32 | minHeight?: ThemeItem 33 | maxWidth?: ThemeItem 34 | minWidth?: ThemeItem 35 | padding?: ThemeItem 36 | space?: ThemeItem 37 | width?: ThemeItem 38 | } 39 | 40 | export type ThemeItem = { 41 | [key: string]: string 42 | } 43 | 44 | export type ThemeKeys = Exclude 45 | 46 | export interface ParsedClassName { 47 | breakpoint: null | string 48 | pseudoClass: null | PseudoClass 49 | selector: string 50 | } 51 | 52 | export type PseudoClass = { 53 | isParent: boolean 54 | value: string 55 | } 56 | 57 | export type CssDeclarationBlocks = { 58 | breakpoint: null | string 59 | cssDeclarationBlocks: Array 60 | } 61 | export interface CssDeclarationBlock extends ParsedClassName { 62 | className: string 63 | declarations: Declarations 64 | } 65 | export type Declarations = { [property: string]: string } 66 | 67 | export type Plugin = { 68 | regex: RegExp 69 | createDeclarations: (options: { 70 | computeColorValue: ( 71 | value: undefined | string, 72 | themeKeys: Array 73 | ) => null | string 74 | computeNumericValue: ( 75 | value: undefined | string, 76 | themeKeys: Array 77 | ) => null | string 78 | matches: RegExpMatchArray 79 | theme: Theme 80 | }) => { [property: string]: string } 81 | } 82 | -------------------------------------------------------------------------------- /src/utilities/read-config-async.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra' 2 | import * as path from 'path' 3 | 4 | import { CliOptions, Config, Theme } from '../types' 5 | 6 | const CONFIG_FILE_NAME = 'generate-css.config.json' 7 | const PACKAGE_JSON_KEY = 'generate-css' 8 | 9 | const defaultConfig = { 10 | reset: true, 11 | theme: {} 12 | } 13 | 14 | export async function readConfigAsync(cliOptions: CliOptions): Promise { 15 | const configFilePath = cliOptions.configFilePath 16 | if (typeof configFilePath === 'string') { 17 | if ((await fs.pathExists(configFilePath)) === false) { 18 | throw new Error(`Configuration file not found: ${configFilePath}`) 19 | } 20 | const config = await readGenerateCssConfig(configFilePath) 21 | return { 22 | ...defaultConfig, 23 | ...cliOptions, 24 | ...config 25 | } 26 | } 27 | const packageJsonConfig = await readPackageJsonConfig() 28 | if (packageJsonConfig !== null) { 29 | return { 30 | ...defaultConfig, 31 | ...cliOptions, 32 | ...packageJsonConfig 33 | } 34 | } 35 | const defaultConfigFilePath = path.join(process.cwd(), CONFIG_FILE_NAME) 36 | if ((await fs.pathExists(defaultConfigFilePath)) === true) { 37 | const config = await readGenerateCssConfig(defaultConfigFilePath) 38 | return { 39 | ...defaultConfig, 40 | ...cliOptions, 41 | ...config 42 | } 43 | } 44 | return { 45 | ...defaultConfig, 46 | ...cliOptions 47 | } 48 | } 49 | 50 | async function readPackageJsonConfig(): Promise { 54 | const packageJsonFilePath = path.join(process.cwd(), 'package.json') 55 | if ((await fs.pathExists(packageJsonFilePath)) === false) { 56 | return null 57 | } 58 | const packageJson = JSON.parse(await fs.readFile(packageJsonFilePath, 'utf8')) 59 | const config = packageJson[PACKAGE_JSON_KEY] 60 | if (typeof config === 'undefined') { 61 | return null 62 | } 63 | return config 64 | } 65 | 66 | async function readGenerateCssConfig( 67 | filePath: string 68 | ): Promise<{ 69 | reset: boolean 70 | theme: Theme 71 | }> { 72 | return JSON.parse(await fs.readFile(filePath, 'utf8')) 73 | } 74 | -------------------------------------------------------------------------------- /src/utilities/create-css/plugins/padding.ts: -------------------------------------------------------------------------------- 1 | /* 2 | padding 3 | 4 | defaultValue = theme.padding.default || theme.space.default 5 | value = theme.padding[key] || theme.space[key] || computeNumericValue(key) 6 | 7 | `.p` | `padding: ${defaultValue};` 8 | `.px` | `padding-left: ${defaultValue};`
`padding-right: ${defaultValue};` 9 | `.py` | `padding-top: ${defaultValue};`
`padding-bottom: ${defaultValue};` 10 | `.pt` | `padding-top: ${defaultValue};` 11 | `.pr` | `padding-right: ${defaultValue};` 12 | `.pb` | `padding-bottom: ${defaultValue};` 13 | `.pl` | `padding-left: ${defaultValue};` 14 | `.px-${key}` | `padding-left: ${value};`
`padding-right: ${value};` 15 | `.py-${key}` | `padding-top: ${value};`
`padding-bottom: ${value};` 16 | `.pt-${key}` | `padding-top: ${value};` 17 | `.pr-${key}` | `padding-right: ${value};` 18 | `.pb-${key}` | `padding-bottom: ${value};` 19 | `.pl-${key}` | `padding-left: ${value};` 20 | */ 21 | 22 | import { Plugin, ThemeKeys } from '../../../types' 23 | 24 | export const padding: Plugin = { 25 | createDeclarations: function ({ 26 | matches, 27 | computeNumericValue 28 | }: { 29 | matches: RegExpMatchArray 30 | computeNumericValue: ( 31 | value: string, 32 | themeKeys: Array 33 | ) => null | string 34 | }): { [property: string]: string } { 35 | const value = computeNumericValue(matches[2], ['padding', 'space']) 36 | if (value === null) { 37 | throw new Error(`Invalid padding: ${matches[2]}`) 38 | } 39 | switch (matches[1]) { 40 | case 'x': { 41 | return { 42 | 'padding-left': value, 43 | 'padding-right': value 44 | } 45 | } 46 | case 'y': { 47 | return { 48 | 'padding-bottom': value, 49 | 'padding-top': value 50 | } 51 | } 52 | case 't': { 53 | return { 54 | 'padding-top': value 55 | } 56 | } 57 | case 'r': { 58 | return { 59 | 'padding-right': value 60 | } 61 | } 62 | case 'b': { 63 | return { 64 | 'padding-bottom': value 65 | } 66 | } 67 | case 'l': { 68 | return { 69 | 'padding-left': value 70 | } 71 | } 72 | } 73 | return { 74 | padding: value 75 | } 76 | }, 77 | regex: /^p([xytrbl])?(?:-(.+))?$/ 78 | } 79 | -------------------------------------------------------------------------------- /src/utilities/stringify-css.ts: -------------------------------------------------------------------------------- 1 | import { Config, CssDeclarationBlock, CssDeclarationBlocks } from '../types' 2 | 3 | export function stringifyCss( 4 | css: Array, 5 | config: Config 6 | ): string { 7 | const result = [] 8 | for (const { breakpoint, cssDeclarationBlocks } of css) { 9 | if (breakpoint !== null) { 10 | if (typeof config.theme.breakpoint === 'undefined') { 11 | throw new Error('`theme.breakpoint` not defined in configuration') 12 | } 13 | if (typeof config.theme.breakpoint[breakpoint] === 'undefined') { 14 | throw new Error( 15 | `Breakpoint not defined in \`theme.breakpoint\`: ${breakpoint}` 16 | ) 17 | } 18 | result.push( 19 | `@media (min-width: ${config.theme.breakpoint[breakpoint]}) {` 20 | ) 21 | } 22 | result.push(stringifyCssDeclarationBlocks(cssDeclarationBlocks)) 23 | if (breakpoint !== null) { 24 | result.push('}') 25 | } 26 | } 27 | return result.join('') 28 | } 29 | 30 | function stringifyCssDeclarationBlocks( 31 | cssDeclarationBlocks: Array 32 | ): string { 33 | const result = [] 34 | for (const { className, declarations, pseudoClass } of cssDeclarationBlocks) { 35 | if (pseudoClass !== null && pseudoClass.isParent === true) { 36 | result.push(`.group${stringifyPseudoClass(pseudoClass.value)} `) 37 | } 38 | result.push('.') 39 | result.push(escapeSpecialCharacters(className)) 40 | if (pseudoClass !== null && pseudoClass.isParent === false) { 41 | result.push(stringifyPseudoClass(pseudoClass.value)) 42 | } 43 | result.push(`{${stringifyDeclarations(declarations)}}`) 44 | } 45 | return result.join('') 46 | } 47 | 48 | const specialCharactersRegex = /[/:@%]/g 49 | 50 | function escapeSpecialCharacters(string: string) { 51 | return string.replace(specialCharactersRegex, function (match: string) { 52 | return `\\${match}` 53 | }) 54 | } 55 | 56 | function stringifyPseudoClass(pseudoClass: string): string { 57 | if (pseudoClass === 'selection') { 58 | return ` ::${pseudoClass}` 59 | } 60 | if (pseudoClass === 'placeholder') { 61 | return `::${pseudoClass}` 62 | } 63 | return `:${pseudoClass}` 64 | } 65 | 66 | function stringifyDeclarations(declarations: { [property: string]: string }) { 67 | const result = [] 68 | for (const property of Object.keys(declarations)) { 69 | result.push(`${property}:${declarations[property]}`) 70 | } 71 | return result.join(';') 72 | } 73 | -------------------------------------------------------------------------------- /src/utilities/create-css/plugins/border-width.ts: -------------------------------------------------------------------------------- 1 | /* 2 | border-width 3 | 4 | - `defaultValue` = `theme.borderWidth.default` 5 | - `value` = `theme.borderWidth[key]` || `resolveNumericValue(key)` 6 | 7 | `.b` | `border-width: ${defaultValue};` 8 | `.bx` | `border-left-width: ${defaultValue};`
`border-right-width: ${defaultValue};` 9 | `.by` | `border-top-width: ${defaultValue};`
`border-bottom-width: ${defaultValue};` 10 | `.bt` | `border-top-width: ${defaultValue};` 11 | `.br` | `border-right-width: ${defaultValue};` 12 | `.bb` | `border-bottom-width: ${defaultValue};` 13 | `.bl` | `border-left-width: ${defaultValue};` 14 | `.b-${key}` | `border-width: ${value};` 15 | `.bx-${key}` | `border-left-width: ${value};`
`border-right-width: ${value};` 16 | `.by-${key}` | `border-top-width: ${value};`
`border-bottom-width: ${value};` 17 | `.bt-${key}` | `border-top-width: ${value};` 18 | `.br-${key}` | `border-right-width: ${value};` 19 | `.bb-${key}` | `border-bottom-width: ${value};` 20 | `.bl-${key}` | `border-left-width: ${value};` 21 | */ 22 | 23 | import { Plugin, ThemeKeys } from '../../../types' 24 | 25 | export const borderWidth: Plugin = { 26 | createDeclarations: function ({ 27 | matches, 28 | computeNumericValue 29 | }: { 30 | matches: RegExpMatchArray 31 | computeNumericValue: ( 32 | value: string, 33 | themeKeys: Array 34 | ) => null | string 35 | }): { [property: string]: string } { 36 | const borderWidth = computeNumericValue(matches[2], ['borderWidth']) 37 | if (borderWidth === null) { 38 | throw new Error(`Invalid border width: ${matches[2]}`) 39 | } 40 | switch (matches[1]) { 41 | case 'x': { 42 | return { 43 | 'border-left-width': borderWidth, 44 | 'border-right-width': borderWidth 45 | } 46 | } 47 | case 'y': { 48 | return { 49 | 'border-bottom-width': borderWidth, 50 | 'border-top-width': borderWidth 51 | } 52 | } 53 | case 't': { 54 | return { 55 | 'border-top-width': borderWidth 56 | } 57 | } 58 | case 'r': { 59 | return { 60 | 'border-right-width': borderWidth 61 | } 62 | } 63 | case 'b': { 64 | return { 65 | 'border-bottom-width': borderWidth 66 | } 67 | } 68 | case 'l': { 69 | return { 70 | 'border-left-width': borderWidth 71 | } 72 | } 73 | } 74 | return { 75 | 'border-width': borderWidth 76 | } 77 | }, 78 | regex: /^b([xytrbl])?(?:-(.+))?$/ 79 | } 80 | -------------------------------------------------------------------------------- /src/utilities/create-css/plugins/width-and-height.ts: -------------------------------------------------------------------------------- 1 | /* 2 | height 3 | 4 | `.h` | `height: ${theme.height.default};` 5 | `.h-screen` | `height: 100vh;` 6 | `.h-${key}` | `height: ${theme.height[key] || computeNumericValue(key)};` 7 | `.minh` | `min-height: ${theme.height.default};` 8 | `.minh-screen` | `min-height: 100vh;` 9 | `.minh-${key}` | `min-height: ${theme.height[key] || computeNumericValue(key)};` 10 | `.maxh` | `max-height: ${theme.height.default};` 11 | `.maxh-screen` | `max-height: 100vh;` 12 | `.maxh-${key}` | `max-height: ${theme.height[key] || computeNumericValue(key)};` 13 | --- 14 | width 15 | 16 | `.w` | `width: ${theme.width.default};` 17 | `.w-screen` | `width: 100vw;` 18 | `.w-${key}` | `width: ${theme.width[key] || computeNumericValue(key)};` 19 | `.minw` | `min-width: ${theme.width.default};` 20 | `.minw-screen` | `min-width: 100vw;` 21 | `.minw-${key}` | `min-width: ${theme.width[key] || computeNumericValue(key)};` 22 | `.maxw` | `max-width: ${theme.width.default};` 23 | `.maxw-screen` | `max-width: 100vw;` 24 | `.maxw-${key}` | `max-width: ${theme.width[key] || computeNumericValue(key)};` 25 | */ 26 | 27 | import { Plugin, ThemeKeys } from '../../../types' 28 | 29 | export const widthAndHeight: Plugin = { 30 | createDeclarations: function ({ 31 | matches, 32 | computeNumericValue 33 | }: { 34 | matches: RegExpMatchArray 35 | computeNumericValue: ( 36 | value: string, 37 | themeKeys: Array 38 | ) => null | string 39 | }): { [property: string]: string } { 40 | const prefix = (typeof matches[1] === 'undefined' ? '' : matches[1]) as 41 | | '' 42 | | 'min' 43 | | 'max' 44 | const suffix = matches[2] === 'w' ? 'width' : 'height' 45 | const property = prefix === '' ? suffix : `${prefix}-${suffix}` 46 | if (matches[3] === 'screen') { 47 | return { 48 | [property]: suffix === 'width' ? '100vw' : '100vh' 49 | } 50 | } 51 | const value = computeNumericValue( 52 | matches[3], 53 | resolveProperty(prefix, suffix) 54 | ) 55 | if (value === null) { 56 | throw new Error(`Invalid ${property}: ${matches[3]}`) 57 | } 58 | return { 59 | [property]: value 60 | } 61 | }, 62 | regex: /^(max|min)?([wh])(?:-(.+))?$/ 63 | } 64 | 65 | function resolveProperty( 66 | prefix: '' | 'min' | 'max', 67 | suffix: 'width' | 'height' 68 | ): Array< 69 | 'width' | 'minWidth' | 'maxHeight' | 'height' | 'minHeight' | 'maxWidth' 70 | > { 71 | if (prefix === '') { 72 | return [suffix] 73 | } 74 | if (suffix === 'width') { 75 | return [suffix, prefix === 'min' ? 'minWidth' : 'maxWidth'] 76 | } 77 | return [suffix, prefix === 'min' ? 'minHeight' : 'maxHeight'] 78 | } 79 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/background-color.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block' 4 | 5 | test('background color not defined in `theme`', function (t) { 6 | t.plan(4) 7 | t.throw(function () { 8 | createCssDeclarationBlock('bg', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('bg-black', {}) 12 | }) 13 | t.throw(function () { 14 | createCssDeclarationBlock('bg-black', { 15 | backgroundColor: {} 16 | }) 17 | }) 18 | t.throw(function () { 19 | createCssDeclarationBlock('bg-black', { 20 | color: {} 21 | }) 22 | }) 23 | }) 24 | 25 | test('default background color defined in `theme.backgroundColor`', function (t) { 26 | t.plan(1) 27 | t.deepEqual( 28 | createCssDeclarationBlock('bg', { 29 | backgroundColor: { 30 | default: '#ffffff' 31 | } 32 | }), 33 | { 34 | breakpoint: null, 35 | className: 'bg', 36 | declarations: { 37 | 'background-color': '#ffffff' 38 | }, 39 | pseudoClass: null, 40 | selector: 'bg' 41 | } 42 | ) 43 | }) 44 | 45 | test('default background color defined in `theme.color`', function (t) { 46 | t.plan(1) 47 | t.deepEqual( 48 | createCssDeclarationBlock('bg', { 49 | color: { 50 | default: '#ffffff' 51 | } 52 | }), 53 | { 54 | breakpoint: null, 55 | className: 'bg', 56 | declarations: { 57 | 'background-color': '#ffffff' 58 | }, 59 | pseudoClass: null, 60 | selector: 'bg' 61 | } 62 | ) 63 | }) 64 | 65 | test('custom background color defined in `theme.backgroundColor`', function (t) { 66 | t.plan(1) 67 | t.deepEqual( 68 | createCssDeclarationBlock('bg-black', { 69 | backgroundColor: { 70 | black: '#000000' 71 | } 72 | }), 73 | { 74 | breakpoint: null, 75 | className: 'bg-black', 76 | declarations: { 77 | 'background-color': '#000000' 78 | }, 79 | pseudoClass: null, 80 | selector: 'bg-black' 81 | } 82 | ) 83 | }) 84 | 85 | test('custom background color defined in `theme.color`', function (t) { 86 | t.plan(1) 87 | t.deepEqual( 88 | createCssDeclarationBlock('bg-black', { 89 | color: { 90 | black: '#000000' 91 | } 92 | }), 93 | { 94 | breakpoint: null, 95 | className: 'bg-black', 96 | declarations: { 97 | 'background-color': '#000000' 98 | }, 99 | pseudoClass: null, 100 | selector: 'bg-black' 101 | } 102 | ) 103 | }) 104 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/border-color.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block' 4 | 5 | test('border color not defined in `theme`', function (t) { 6 | t.plan(4) 7 | t.throw(function () { 8 | createCssDeclarationBlock('border', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('border-black', {}) 12 | }) 13 | t.throw(function () { 14 | createCssDeclarationBlock('border-black', { 15 | borderColor: {} 16 | }) 17 | }) 18 | t.throw(function () { 19 | createCssDeclarationBlock('border-black', { 20 | color: {} 21 | }) 22 | }) 23 | }) 24 | 25 | test('default border color defined in `theme.borderColor`', function (t) { 26 | t.plan(1) 27 | t.deepEqual( 28 | createCssDeclarationBlock('border', { 29 | borderColor: { 30 | default: '#ffffff' 31 | } 32 | }), 33 | { 34 | breakpoint: null, 35 | className: 'border', 36 | declarations: { 37 | 'border-color': '#ffffff' 38 | }, 39 | pseudoClass: null, 40 | selector: 'border' 41 | } 42 | ) 43 | }) 44 | 45 | test('default border color defined in `theme.color`', function (t) { 46 | t.plan(1) 47 | t.deepEqual( 48 | createCssDeclarationBlock('border', { 49 | color: { 50 | default: '#ffffff' 51 | } 52 | }), 53 | { 54 | breakpoint: null, 55 | className: 'border', 56 | declarations: { 57 | 'border-color': '#ffffff' 58 | }, 59 | pseudoClass: null, 60 | selector: 'border' 61 | } 62 | ) 63 | }) 64 | 65 | test('custom border color defined in `theme.borderColor`', function (t) { 66 | t.plan(1) 67 | t.deepEqual( 68 | createCssDeclarationBlock('border-black', { 69 | borderColor: { 70 | black: '#000000' 71 | } 72 | }), 73 | { 74 | breakpoint: null, 75 | className: 'border-black', 76 | declarations: { 77 | 'border-color': '#000000' 78 | }, 79 | pseudoClass: null, 80 | selector: 'border-black' 81 | } 82 | ) 83 | }) 84 | 85 | test('custom border color defined in `theme.color`', function (t) { 86 | t.plan(1) 87 | t.deepEqual( 88 | createCssDeclarationBlock('border-black', { 89 | color: { 90 | black: '#000000' 91 | } 92 | }), 93 | { 94 | breakpoint: null, 95 | className: 'border-black', 96 | declarations: { 97 | 'border-color': '#000000' 98 | }, 99 | pseudoClass: null, 100 | selector: 'border-black' 101 | } 102 | ) 103 | }) 104 | -------------------------------------------------------------------------------- /src/generate-css.ts: -------------------------------------------------------------------------------- 1 | import * as csso from 'csso' 2 | import * as findUp from 'find-up' 3 | import * as fs from 'fs-extra' 4 | import * as globby from 'globby' 5 | import * as path from 'path' 6 | import * as prettier from 'prettier' 7 | 8 | import { Config } from './types' 9 | import { createBaseFontSizeCss } from './utilities/create-base-font-size-css' 10 | import { createCss } from './utilities/create-css/create-css' 11 | import { extractClassNamesAsync } from './utilities/extract-class-names-async/extract-class-names-async' 12 | import { stringifyCss } from './utilities/stringify-css' 13 | 14 | export async function generateCssAsync(config: Config): Promise { 15 | const classNames = await extractClassNamesAsync(config.sourceFilesPattern) 16 | const generatedCss = stringifyCss(createCss(classNames, config.theme), config) 17 | const prependCss = 18 | config.prependCssFilesPattern === null 19 | ? '' 20 | : await readFilesAsync(config.prependCssFilesPattern) 21 | const resetCss = config.reset === true ? await readResetCssFilesAsync() : '' 22 | const baseFontSizeCss = 23 | typeof config.theme.baseFontSize === 'undefined' 24 | ? '' 25 | : createBaseFontSizeCss(config) 26 | const appendCss = 27 | config.appendCssFilesPattern === null 28 | ? '' 29 | : await readFilesAsync(config.appendCssFilesPattern) 30 | const css = formatCss( 31 | [prependCss, resetCss, baseFontSizeCss, generatedCss, appendCss].join(''), 32 | config.minify 33 | ) 34 | if (config.outputPath === null) { 35 | console.log(css) // eslint-disable-line no-console 36 | return 37 | } 38 | await fs.outputFile(config.outputPath, css) 39 | } 40 | 41 | async function readResetCssFilesAsync(): Promise { 42 | const normalizeCssPath = await findUp( 43 | path.join('node_modules', 'normalize.css', 'normalize.css') 44 | ) 45 | if (typeof normalizeCssPath === 'undefined') { 46 | throw new Error('Cannot find normalize.css') 47 | } 48 | const resetCssPath = path.join(__dirname, 'css', 'reset.css') 49 | return [ 50 | await fs.readFile(normalizeCssPath, 'utf8'), 51 | await fs.readFile(resetCssPath, 'utf8') 52 | ].join('') 53 | } 54 | 55 | async function readFilesAsync(pattern: string): Promise { 56 | const files = await globby(pattern) 57 | if (files.length === 0) { 58 | throw new Error(`No files matched by pattern: ${pattern}`) 59 | } 60 | const result = [] 61 | for (const file of files) { 62 | result.push(await fs.readFile(file, 'utf8')) 63 | } 64 | return result.join('') 65 | } 66 | 67 | function formatCss(css: string, minify: boolean) { 68 | if (minify === true) { 69 | return csso.minify(css, { comments: false, forceMediaMerge: true }).css 70 | } 71 | return prettier.format(css, { parser: 'css' }) 72 | } 73 | -------------------------------------------------------------------------------- /src/utilities/create-css/group-css-declaration-blocks-by-breakpoint.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CssDeclarationBlock, 3 | CssDeclarationBlocks, 4 | ThemeItem 5 | } from '../../types' 6 | 7 | export function groupCssDeclarationBlocksByBreakpoint( 8 | cssDeclarationBlocks: Array, 9 | themeBreakpoints?: ThemeItem 10 | ): Array { 11 | if (cssDeclarationBlocks.length === 0) { 12 | return [] 13 | } 14 | const base = [] 15 | const breakpoints: { [breakpoint: string]: Array } = {} 16 | for (const cssDeclarationBlock of cssDeclarationBlocks) { 17 | const breakpoint = cssDeclarationBlock.breakpoint 18 | if (breakpoint === null) { 19 | base.push(cssDeclarationBlock) 20 | continue 21 | } 22 | if (typeof breakpoints[breakpoint] === 'undefined') { 23 | breakpoints[breakpoint] = [] 24 | } 25 | breakpoints[breakpoint].push(cssDeclarationBlock) 26 | } 27 | const result: Array = [] 28 | result.push({ 29 | breakpoint: null, 30 | cssDeclarationBlocks: base 31 | }) 32 | for (const breakpoint of Object.keys(breakpoints)) { 33 | result.push({ 34 | breakpoint, 35 | cssDeclarationBlocks: breakpoints[breakpoint] 36 | }) 37 | } 38 | return sortCss(result, themeBreakpoints) 39 | } 40 | 41 | function sortCss( 42 | css: Array, 43 | themeBreakpoints?: ThemeItem 44 | ) { 45 | return css 46 | .slice() 47 | .sort(function (a, b) { 48 | if (a.breakpoint === null) { 49 | return -1 50 | } 51 | if (b.breakpoint === null) { 52 | return 1 53 | } 54 | if (typeof themeBreakpoints !== 'undefined') { 55 | const aBreakpointWidth = themeBreakpoints[a.breakpoint] 56 | const bBreakpointWidth = themeBreakpoints[b.breakpoint] 57 | if ( 58 | typeof aBreakpointWidth !== 'undefined' && 59 | typeof bBreakpointWidth !== 'undefined' 60 | ) { 61 | return parseInt(aBreakpointWidth, 10) - parseInt(bBreakpointWidth, 10) 62 | } 63 | } 64 | return a.breakpoint.localeCompare(b.breakpoint) 65 | }) 66 | .map(function ({ breakpoint, cssDeclarationBlocks }: CssDeclarationBlocks) { 67 | return { 68 | breakpoint, 69 | cssDeclarationBlocks: cssDeclarationBlocks 70 | .slice() 71 | .sort(function (x, y): number { 72 | if (x.selector !== y.selector) { 73 | return x.selector.localeCompare(y.selector) 74 | } 75 | if (x.pseudoClass === null) { 76 | return -1 77 | } 78 | if (y.pseudoClass === null) { 79 | return 1 80 | } 81 | return x.pseudoClass.value.localeCompare(y.pseudoClass.value) 82 | }) 83 | } 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /src/utilities/create-css/compute-numeric-value-factory.ts: -------------------------------------------------------------------------------- 1 | import { Theme, ThemeItem, ThemeKeys } from '../../types' 2 | 3 | const minusPrefixRegex = /^(-?)(.+)$/ 4 | const pixelValueRegex = /^\d+px$/ 5 | const fractionRegex = /^(\d+)\/(\d+)$/ 6 | const integerRegex = /^\d+$/ 7 | 8 | export function computeNumericValueFactory( 9 | theme: Theme 10 | ): (value: undefined | string, themeKeys: Array) => null | string { 11 | const parsedSpace = parseSpace(theme.baseSpace) 12 | return function ( 13 | value = 'default', 14 | themeKeys: Array 15 | ): null | string { 16 | let matches 17 | matches = value.match(minusPrefixRegex) as RegExpMatchArray 18 | const prefix = matches[1] === '-' ? '-' : '' 19 | const parsedValue = matches[2] 20 | for (const themeKey of themeKeys) { 21 | if ( 22 | typeof themeKey !== 'undefined' && 23 | typeof theme[themeKey] !== 'undefined' 24 | ) { 25 | const result = (theme[themeKey] as ThemeItem)[parsedValue] 26 | if (typeof result !== 'undefined') { 27 | return `${prefix}${result}` 28 | } 29 | } 30 | } 31 | switch (parsedValue) { 32 | case 'auto': { 33 | return 'auto' 34 | } 35 | case 'full': { 36 | return `${prefix}100%` 37 | } 38 | case 'px': { 39 | return `${prefix}1px` 40 | } 41 | } 42 | matches = parsedValue.match(pixelValueRegex) 43 | if (matches !== null) { 44 | if (matches[0] === '0px') { 45 | return '0' 46 | } 47 | return `${prefix}${matches[0]}` 48 | } 49 | matches = parsedValue.match(fractionRegex) 50 | if (matches !== null) { 51 | return `${prefix}${formatNumber( 52 | (parseFloat(matches[1]) / parseFloat(matches[2])) * 100 53 | )}%` 54 | } 55 | matches = parsedValue.match(integerRegex) 56 | if (matches !== null) { 57 | if (matches[0] === '0') { 58 | return '0' 59 | } 60 | if (parsedSpace === null) { 61 | throw new Error('`theme.space` not defined in configuration') 62 | } 63 | return `${prefix}${parseFloat(matches[0]) * parsedSpace.value}${ 64 | parsedSpace.unit 65 | }` 66 | } 67 | return null 68 | } 69 | } 70 | 71 | const valueAndUnitRegex = /((?:\d*\.)?\d+) ?([A-Za-z]+)/ 72 | 73 | function parseSpace(space?: string): null | { value: number; unit: string } { 74 | if (typeof space === 'undefined') { 75 | return null 76 | } 77 | const matches = space.match(valueAndUnitRegex) 78 | if (matches === null) { 79 | throw new Error(`Invalid space: ${space}`) 80 | } 81 | return { 82 | unit: matches[2], 83 | value: parseFloat(matches[1]) 84 | } 85 | } 86 | 87 | const zeroesSuffixRegex = /0+$/ 88 | const dotSuffixRegex = /\.$/ 89 | 90 | function formatNumber(number: number): string { 91 | return number 92 | .toFixed(6) 93 | .replace(zeroesSuffixRegex, '') 94 | .replace(dotSuffixRegex, '') 95 | } 96 | -------------------------------------------------------------------------------- /src/utilities/extract-class-names-async/__tests__/extract-class-names-from-js.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { extractClassNamesFromJs } from '../extract-class-names-from-js' 4 | 5 | test('empty string', function (t) { 6 | t.plan(1) 7 | t.deepEqual(extractClassNamesFromJs(''), []) 8 | }) 9 | 10 | test('does not match constants without a "CLASS" suffix', function (t) { 11 | t.plan(2) 12 | t.deepEqual(extractClassNamesFromJs('const HIDDEN = "hidden"'), []) 13 | t.deepEqual(extractClassNamesFromJs("const HIDDEN = 'hidden'"), []) 14 | }) 15 | 16 | test('matches string constants with a "CLASS" suffix containing a single class', function (t) { 17 | t.plan(5) 18 | t.deepEqual(extractClassNamesFromJs('const DIV_CLASS = "flex"'), ['flex']) 19 | t.deepEqual(extractClassNamesFromJs('const DIV_CLASSNAME = "flex"'), ['flex']) 20 | t.deepEqual(extractClassNamesFromJs('const DIV_CLASSNAMES = "flex"'), [ 21 | 'flex' 22 | ]) 23 | t.deepEqual(extractClassNamesFromJs('const DIV_CLASS_NAME = "flex"'), [ 24 | 'flex' 25 | ]) 26 | t.deepEqual(extractClassNamesFromJs('const DIV_CLASS_NAMES = "flex"'), [ 27 | 'flex' 28 | ]) 29 | }) 30 | 31 | test('matches string constants with a "CLASS" suffix containing multiple classes', function (t) { 32 | t.plan(5) 33 | t.deepEqual(extractClassNamesFromJs('const DIV_CLASS = "flex bg-black"'), [ 34 | 'bg-black', 35 | 'flex' 36 | ]) 37 | t.deepEqual( 38 | extractClassNamesFromJs('const DIV_CLASSNAME = "flex bg-black"'), 39 | ['bg-black', 'flex'] 40 | ) 41 | t.deepEqual( 42 | extractClassNamesFromJs('const DIV_CLASSNAMES = "flex bg-black"'), 43 | ['bg-black', 'flex'] 44 | ) 45 | t.deepEqual( 46 | extractClassNamesFromJs('const DIV_CLASS_NAME = "flex bg-black"'), 47 | ['bg-black', 'flex'] 48 | ) 49 | t.deepEqual( 50 | extractClassNamesFromJs('const DIV_CLASS_NAMES = "flex bg-black"'), 51 | ['bg-black', 'flex'] 52 | ) 53 | }) 54 | 55 | test('multiple matches', function (t) { 56 | t.plan(1) 57 | const js = ` 58 | const DIV_CLASS = "flex bg-black" 59 | const H1_CLASS = "color-red" 60 | ` 61 | t.deepEqual(extractClassNamesFromJs(js), ['bg-black', 'color-red', 'flex']) 62 | }) 63 | 64 | test('var', function (t) { 65 | t.plan(1) 66 | t.deepEqual(extractClassNamesFromJs('var DIV_CLASS = "flex bg-black"'), [ 67 | 'bg-black', 68 | 'flex' 69 | ]) 70 | }) 71 | 72 | test('single-quoted strings', function (t) { 73 | t.plan(1) 74 | t.deepEqual(extractClassNamesFromJs("var DIV_CLASS = 'flex bg-black'"), [ 75 | 'bg-black', 76 | 'flex' 77 | ]) 78 | }) 79 | 80 | test('lowercase', function (t) { 81 | t.plan(1) 82 | t.deepEqual(extractClassNamesFromJs("const div_class = 'flex bg-black'"), [ 83 | 'bg-black', 84 | 'flex' 85 | ]) 86 | }) 87 | 88 | test('leading and trailing consecutive spaces', function (t) { 89 | t.plan(1) 90 | t.deepEqual( 91 | extractClassNamesFromJs("const div_class = ' flex bg-black '"), 92 | ['bg-black', 'flex'] 93 | ) 94 | }) 95 | -------------------------------------------------------------------------------- /src/utilities/__tests__/read-config-async.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra' 2 | import * as path from 'path' 3 | import { test } from 'tap' 4 | 5 | import { readConfigAsync } from '../read-config-async' 6 | 7 | const defaultCliOptions = { 8 | appendCssFilesPattern: null, 9 | minify: false, 10 | outputPath: null, 11 | prependCssFilesPattern: null, 12 | sourceFilesPattern: '*' 13 | } 14 | 15 | test('no config', async function (t) { 16 | t.plan(3) 17 | process.chdir(path.join(__dirname, 'fixtures', '1-no-config')) 18 | t.false(await fs.pathExists('package.json')) 19 | t.false(await fs.pathExists('generate-css.config.json')) 20 | const config = await readConfigAsync({ 21 | ...defaultCliOptions, 22 | configFilePath: null 23 | }) 24 | t.deepEqual(config, { 25 | ...config, 26 | reset: true, 27 | theme: {} 28 | }) 29 | }) 30 | 31 | test('package.json config', async function (t) { 32 | t.plan(3) 33 | process.chdir(path.join(__dirname, 'fixtures', '2-package-json-config')) 34 | t.true(await fs.pathExists('package.json')) 35 | t.false(await fs.pathExists('generate-css.config.json')) 36 | const config = await readConfigAsync({ 37 | ...defaultCliOptions, 38 | configFilePath: null 39 | }) 40 | t.deepEqual(config, { 41 | ...config, 42 | reset: false, 43 | theme: { 44 | color: { 45 | default: '#ffffff' 46 | } 47 | } 48 | }) 49 | }) 50 | 51 | test('generate-css.config', async function (t) { 52 | t.plan(3) 53 | process.chdir(path.join(__dirname, 'fixtures', '3-generate-css-config')) 54 | t.false(await fs.pathExists('package.json')) 55 | t.true(await fs.pathExists('generate-css.config.json')) 56 | const config = await readConfigAsync({ 57 | ...defaultCliOptions, 58 | configFilePath: null 59 | }) 60 | t.deepEqual(config, { 61 | ...config, 62 | reset: false, 63 | theme: { 64 | color: { 65 | default: '#ffffff' 66 | } 67 | } 68 | }) 69 | }) 70 | 71 | test('custom config', async function (t) { 72 | t.plan(4) 73 | process.chdir(path.join(__dirname, 'fixtures', '4-custom-config')) 74 | t.false(await fs.pathExists('package.json')) 75 | t.false(await fs.pathExists('generate-css.config.json')) 76 | t.true(await fs.pathExists('foo.json')) 77 | const config = await readConfigAsync({ 78 | ...defaultCliOptions, 79 | configFilePath: 'foo.json' 80 | }) 81 | t.deepEqual(config, { 82 | ...config, 83 | reset: false, 84 | theme: { 85 | color: { 86 | default: '#ffffff' 87 | } 88 | } 89 | }) 90 | }) 91 | 92 | test('invalid custom config', async function (t) { 93 | t.plan(4) 94 | process.chdir(path.join(__dirname, 'fixtures', '5-invalid-custom-config')) 95 | t.false(await fs.pathExists('package.json')) 96 | t.false(await fs.pathExists('generate-css.config.json')) 97 | t.false(await fs.pathExists('foo.json')) 98 | try { 99 | await readConfigAsync({ 100 | ...defaultCliOptions, 101 | configFilePath: 'foo.json' 102 | }) 103 | t.fail() 104 | } catch { 105 | t.pass() 106 | } 107 | }) 108 | -------------------------------------------------------------------------------- /src/utilities/create-css/plugins/margin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | margin 3 | 4 | - `defaultValue` = `theme.margin.default` || `theme.space.default` 5 | - `value` = `theme.margin[key]` || `theme.space[key]` || `resolveNumericValue(key)` 6 | 7 | `.m` | `margin: ${defaultValue};` 8 | `.mx` | `margin-left: ${defaultValue};`
`margin-right: ${defaultValue};` 9 | `.my` | `margin-top: ${defaultValue};`
`margin-bottom: ${defaultValue};` 10 | `.mt` | `margin-top: ${defaultValue};` 11 | `.mr` | `margin-right: ${defaultValue};` 12 | `.mb` | `margin-bottom: ${defaultValue};` 13 | `.ml` | `margin-left: ${defaultValue};` 14 | `.mx-${key}` | `margin-left: ${value};`
`margin-right: ${value};` 15 | `.my-${key}` | `margin-top: ${value};`
`margin-bottom: ${value};` 16 | `.mt-${key}` | `margin-top: ${value};` 17 | `.mr-${key}` | `margin-right: ${value};` 18 | `.mb-${key}` | `margin-bottom: ${value};` 19 | `.ml-${key}` | `margin-left: ${value};` 20 | `.-m` | `margin: -${defaultValue};` 21 | `.-mx` | `margin-left: -${defaultValue};`
`margin-right: -${defaultValue};` 22 | `.-my` | `margin-top: -${defaultValue};`
`margin-bottom: -${defaultValue};` 23 | `.-mt` | `margin-top: -${defaultValue};` 24 | `.-mr` | `margin-right: -${defaultValue};` 25 | `.-mb` | `margin-bottom: -${defaultValue};` 26 | `.-ml` | `margin-left: -${defaultValue};` 27 | `.-mx-${key}` | `margin-left: -${value};`
`margin-right: -${value};` 28 | `.-my-${key}` | `margin-top: -${value};`
`margin-bottom: -${value};` 29 | `.-mt-${key}` | `margin-top: -${value};` 30 | `.-mr-${key}` | `margin-right: -${value};` 31 | `.-mb-${key}` | `margin-bottom: -${value};` 32 | `.-ml-${key}` | `margin-left: -${value};` 33 | */ 34 | 35 | import { Plugin, ThemeKeys } from '../../../types' 36 | 37 | export const margin: Plugin = { 38 | createDeclarations: function ({ 39 | matches, 40 | computeNumericValue 41 | }: { 42 | matches: RegExpMatchArray 43 | computeNumericValue: ( 44 | value: string, 45 | themeKeys: Array 46 | ) => null | string 47 | }): { [property: string]: string } { 48 | const value = computeNumericValue( 49 | `${matches[1] === '-' ? '-' : ''}${ 50 | typeof matches[3] === 'undefined' ? 'default' : matches[3] 51 | }`, 52 | ['margin', 'space'] 53 | ) 54 | if (value === null) { 55 | throw new Error( 56 | `Invalid ${matches[1] === '-' ? 'negative ' : ''}margin: ${matches[3]}` 57 | ) 58 | } 59 | switch (matches[2]) { 60 | case 'x': { 61 | return { 62 | 'margin-left': value, 63 | 'margin-right': value 64 | } 65 | } 66 | case 'y': { 67 | return { 68 | 'margin-bottom': value, 69 | 'margin-top': value 70 | } 71 | } 72 | case 't': { 73 | return { 74 | 'margin-top': value 75 | } 76 | } 77 | case 'r': { 78 | return { 79 | 'margin-right': value 80 | } 81 | } 82 | case 'b': { 83 | return { 84 | 'margin-bottom': value 85 | } 86 | } 87 | case 'l': { 88 | return { 89 | 'margin-left': value 90 | } 91 | } 92 | } 93 | return { 94 | margin: value 95 | } 96 | }, 97 | regex: /^(-?)m([xytrbl])?(?:-(.+))?$/ 98 | } 99 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/create-css-declaration-block.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../create-css-declaration-block' 4 | 5 | test('invalid class name', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | createCssDeclarationBlock('', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock(':', {}) 12 | }) 13 | }) 14 | 15 | test('unrecognized class name', function (t) { 16 | t.plan(1) 17 | t.equal(createCssDeclarationBlock('foo', {}), null) 18 | }) 19 | 20 | test('plain selector', function (t) { 21 | t.plan(1) 22 | t.deepEqual(createCssDeclarationBlock('block', {}), { 23 | breakpoint: null, 24 | className: 'block', 25 | declarations: { 26 | display: 'block' 27 | }, 28 | pseudoClass: null, 29 | selector: 'block' 30 | }) 31 | }) 32 | 33 | test('breakpoint not defined', function (t) { 34 | t.plan(1) 35 | t.throw(function () { 36 | createCssDeclarationBlock('sm@block', {}) 37 | }) 38 | }) 39 | 40 | test('breakpoint defined', function (t) { 41 | t.plan(1) 42 | t.deepEqual( 43 | createCssDeclarationBlock('sm@block', { 44 | breakpoint: { 45 | sm: '320px' 46 | } 47 | }), 48 | { 49 | breakpoint: 'sm', 50 | className: 'sm@block', 51 | declarations: { 52 | display: 'block' 53 | }, 54 | pseudoClass: null, 55 | selector: 'block' 56 | } 57 | ) 58 | }) 59 | 60 | test('pseudo-class', function (t) { 61 | t.plan(1) 62 | t.deepEqual(createCssDeclarationBlock('hover:block', {}), { 63 | breakpoint: null, 64 | className: 'hover:block', 65 | declarations: { 66 | display: 'block' 67 | }, 68 | pseudoClass: { isParent: false, value: 'hover' }, 69 | selector: 'block' 70 | }) 71 | }) 72 | 73 | test('group pseudo-class', function (t) { 74 | t.plan(1) 75 | t.deepEqual(createCssDeclarationBlock('group-hover:block', {}), { 76 | breakpoint: null, 77 | className: 'group-hover:block', 78 | declarations: { 79 | display: 'block' 80 | }, 81 | pseudoClass: { isParent: true, value: 'hover' }, 82 | selector: 'block' 83 | }) 84 | }) 85 | 86 | test('valid breakpoint and pseudo-class', function (t) { 87 | t.plan(1) 88 | t.deepEqual( 89 | createCssDeclarationBlock('sm@hover:block', { 90 | breakpoint: { 91 | sm: '320px' 92 | } 93 | }), 94 | { 95 | breakpoint: 'sm', 96 | className: 'sm@hover:block', 97 | declarations: { 98 | display: 'block' 99 | }, 100 | pseudoClass: { isParent: false, value: 'hover' }, 101 | selector: 'block' 102 | } 103 | ) 104 | }) 105 | 106 | test('valid breakpoint and group pseudo-class', function (t) { 107 | t.plan(1) 108 | t.deepEqual( 109 | createCssDeclarationBlock('sm@group-hover:block', { 110 | breakpoint: { 111 | sm: '320px' 112 | } 113 | }), 114 | { 115 | breakpoint: 'sm', 116 | className: 'sm@group-hover:block', 117 | declarations: { 118 | display: 'block' 119 | }, 120 | pseudoClass: { isParent: true, value: 'hover' }, 121 | selector: 'block' 122 | } 123 | ) 124 | }) 125 | -------------------------------------------------------------------------------- /scripts/create-css-docs.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra' 2 | import * as globby from 'globby' 3 | import * as path from 'path' 4 | 5 | async function main(): Promise { 6 | const data = [ 7 | ...(await readDeclarationsAsync()), 8 | ...(await readPluginsAsync()) 9 | ].sort(function (x, y) { 10 | return x.name.localeCompare(y.name) 11 | }) 12 | const result = [] 13 | result.push('# Functional CSS classes') 14 | for (const { classes, name, value } of data) { 15 | const section = [] 16 | section.push(`## ${name}\n\n`) 17 | if (value !== null) { 18 | section.push(`${value}\n\n`) 19 | } 20 | section.push('Class name | CSS rules\n') 21 | section.push(':--|:--\n') 22 | section.push(`${classes.replace(/\|\|/g, '\\|\\|')}`) 23 | result.push(section.join('')) 24 | } 25 | const file = path.join(path.resolve(__dirname, '..'), 'docs', 'css.md') 26 | await fs.outputFile(file, `${result.join('\n\n')}\n`) 27 | } 28 | main() 29 | 30 | async function readDeclarationsAsync(): Promise> { 31 | const directory = path.join( 32 | path.resolve(__dirname, '..'), 33 | 'src', 34 | 'utilities', 35 | 'create-css', 36 | 'declarations' 37 | ) 38 | const files = await globby([ 39 | path.join(directory, '*.ts'), 40 | `!${path.join(directory, 'index.ts')}` 41 | ]) 42 | const result: Array = [] 43 | for (const file of files) { 44 | const name = path.basename(file, '.ts') 45 | const classes = Object.values(require(file))[0] 46 | result.push({ 47 | classes: stringifyClasses(classes), 48 | name, 49 | value: null 50 | }) 51 | } 52 | return result 53 | } 54 | 55 | function stringifyClasses(classes: any): string { 56 | const result = [] 57 | for (const className of Object.keys(classes)) { 58 | result.push( 59 | `\`.${className}\` | ${stringifyDeclarations(classes[className])}` 60 | ) 61 | } 62 | return result.join('\n') 63 | } 64 | 65 | function stringifyDeclarations(declarations: any): string { 66 | const result = [] 67 | for (const property of Object.keys(declarations)) { 68 | result.push(`\`${property}: ${declarations[property]};\``) 69 | } 70 | return result.join('
') 71 | } 72 | 73 | async function readPluginsAsync(): Promise> { 74 | const directory = path.join( 75 | path.resolve(__dirname, '..'), 76 | 'src', 77 | 'utilities', 78 | 'create-css', 79 | 'plugins' 80 | ) 81 | const files = await globby([ 82 | path.join(directory, '*.ts'), 83 | `!${path.join(directory, 'index.ts')}` 84 | ]) 85 | const result: Array = [] 86 | for (const file of files) { 87 | const string = await fs.readFile(file, 'utf8') 88 | if (string.indexOf('/*\n') !== 0) { 89 | continue 90 | } 91 | const comment = string.slice(3, string.indexOf('\n*/')) 92 | for (const section of comment.split('\n---\n')) { 93 | const split = section.split('\n\n') 94 | const name = split[0] 95 | if (split.length === 2) { 96 | result.push({ 97 | classes: split[1], 98 | name, 99 | value: null 100 | }) 101 | continue 102 | } 103 | result.push({ 104 | classes: split[2], 105 | name, 106 | value: split[1] 107 | }) 108 | } 109 | } 110 | return result 111 | } 112 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/margin/positive-margin.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../../create-css-declaration-block' 4 | 5 | test('margin not defined in `theme`', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | createCssDeclarationBlock('m', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('m', { margin: {} }) 12 | }) 13 | }) 14 | 15 | test('default margin defined in `theme.margin`', function (t) { 16 | t.plan(1) 17 | t.deepEqual( 18 | createCssDeclarationBlock('m', { 19 | margin: { 20 | default: '8px' 21 | } 22 | }), 23 | { 24 | breakpoint: null, 25 | className: 'm', 26 | declarations: { 27 | margin: '8px' 28 | }, 29 | pseudoClass: null, 30 | selector: 'm' 31 | } 32 | ) 33 | }) 34 | 35 | test('custom margin defined in `theme.margin`', function (t) { 36 | t.plan(1) 37 | t.deepEqual(createCssDeclarationBlock('m-sm', { margin: { sm: '4px' } }), { 38 | breakpoint: null, 39 | className: 'm-sm', 40 | declarations: { 41 | margin: '4px' 42 | }, 43 | pseudoClass: null, 44 | selector: 'm-sm' 45 | }) 46 | }) 47 | 48 | test('pixel margin', function (t) { 49 | t.plan(1) 50 | t.deepEqual(createCssDeclarationBlock('m-2px', {}), { 51 | breakpoint: null, 52 | className: 'm-2px', 53 | declarations: { 54 | margin: '2px' 55 | }, 56 | pseudoClass: null, 57 | selector: 'm-2px' 58 | }) 59 | }) 60 | 61 | test('two sides only', function (t) { 62 | t.plan(2) 63 | t.deepEqual( 64 | createCssDeclarationBlock('mx', { 65 | margin: { 66 | default: '8px' 67 | } 68 | }), 69 | { 70 | breakpoint: null, 71 | className: 'mx', 72 | declarations: { 73 | 'margin-left': '8px', 74 | 'margin-right': '8px' 75 | }, 76 | pseudoClass: null, 77 | selector: 'mx' 78 | } 79 | ) 80 | t.deepEqual( 81 | createCssDeclarationBlock('my-sm', { 82 | margin: { 83 | sm: '4px' 84 | } 85 | }), 86 | { 87 | breakpoint: null, 88 | className: 'my-sm', 89 | declarations: { 90 | 'margin-bottom': '4px', 91 | 'margin-top': '4px' 92 | }, 93 | pseudoClass: null, 94 | selector: 'my-sm' 95 | } 96 | ) 97 | }) 98 | 99 | test('one side only', function (t) { 100 | t.plan(4) 101 | t.deepEqual(createCssDeclarationBlock('mt-2px', {}), { 102 | breakpoint: null, 103 | className: 'mt-2px', 104 | declarations: { 105 | 'margin-top': '2px' 106 | }, 107 | pseudoClass: null, 108 | selector: 'mt-2px' 109 | }) 110 | t.deepEqual( 111 | createCssDeclarationBlock('mr', { 112 | margin: { default: '8px' } 113 | }), 114 | { 115 | breakpoint: null, 116 | className: 'mr', 117 | declarations: { 118 | 'margin-right': '8px' 119 | }, 120 | pseudoClass: null, 121 | selector: 'mr' 122 | } 123 | ) 124 | t.deepEqual(createCssDeclarationBlock('mb-sm', { margin: { sm: '4px' } }), { 125 | breakpoint: null, 126 | className: 'mb-sm', 127 | declarations: { 128 | 'margin-bottom': '4px' 129 | }, 130 | pseudoClass: null, 131 | selector: 'mb-sm' 132 | }) 133 | t.deepEqual(createCssDeclarationBlock('ml-2px', {}), { 134 | breakpoint: null, 135 | className: 'ml-2px', 136 | declarations: { 137 | 'margin-left': '2px' 138 | }, 139 | pseudoClass: null, 140 | selector: 'ml-2px' 141 | }) 142 | }) 143 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/padding.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block' 4 | 5 | test('padding not defined in `theme`', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | createCssDeclarationBlock('p', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('p', { padding: {} }) 12 | }) 13 | }) 14 | 15 | test('default padding defined in `theme.padding`', function (t) { 16 | t.plan(1) 17 | t.deepEqual( 18 | createCssDeclarationBlock('p', { 19 | padding: { 20 | default: '8px' 21 | } 22 | }), 23 | { 24 | breakpoint: null, 25 | className: 'p', 26 | declarations: { 27 | padding: '8px' 28 | }, 29 | pseudoClass: null, 30 | selector: 'p' 31 | } 32 | ) 33 | }) 34 | 35 | test('custom padding defined in `theme.padding`', function (t) { 36 | t.plan(1) 37 | t.deepEqual(createCssDeclarationBlock('p-sm', { padding: { sm: '4px' } }), { 38 | breakpoint: null, 39 | className: 'p-sm', 40 | declarations: { 41 | padding: '4px' 42 | }, 43 | pseudoClass: null, 44 | selector: 'p-sm' 45 | }) 46 | }) 47 | 48 | test('pixel padding', function (t) { 49 | t.plan(1) 50 | t.deepEqual(createCssDeclarationBlock('p-2px', {}), { 51 | breakpoint: null, 52 | className: 'p-2px', 53 | declarations: { 54 | padding: '2px' 55 | }, 56 | pseudoClass: null, 57 | selector: 'p-2px' 58 | }) 59 | }) 60 | 61 | test('two sides only', function (t) { 62 | t.plan(2) 63 | t.deepEqual( 64 | createCssDeclarationBlock('px', { 65 | padding: { 66 | default: '8px' 67 | } 68 | }), 69 | { 70 | breakpoint: null, 71 | className: 'px', 72 | declarations: { 73 | 'padding-left': '8px', 74 | 'padding-right': '8px' 75 | }, 76 | pseudoClass: null, 77 | selector: 'px' 78 | } 79 | ) 80 | t.deepEqual( 81 | createCssDeclarationBlock('py-sm', { 82 | padding: { 83 | sm: '4px' 84 | } 85 | }), 86 | { 87 | breakpoint: null, 88 | className: 'py-sm', 89 | declarations: { 90 | 'padding-bottom': '4px', 91 | 'padding-top': '4px' 92 | }, 93 | pseudoClass: null, 94 | selector: 'py-sm' 95 | } 96 | ) 97 | }) 98 | 99 | test('one side only', function (t) { 100 | t.plan(4) 101 | t.deepEqual(createCssDeclarationBlock('pt-2px', {}), { 102 | breakpoint: null, 103 | className: 'pt-2px', 104 | declarations: { 105 | 'padding-top': '2px' 106 | }, 107 | pseudoClass: null, 108 | selector: 'pt-2px' 109 | }) 110 | t.deepEqual( 111 | createCssDeclarationBlock('pr', { 112 | padding: { default: '8px' } 113 | }), 114 | { 115 | breakpoint: null, 116 | className: 'pr', 117 | declarations: { 118 | 'padding-right': '8px' 119 | }, 120 | pseudoClass: null, 121 | selector: 'pr' 122 | } 123 | ) 124 | t.deepEqual(createCssDeclarationBlock('pb-sm', { padding: { sm: '4px' } }), { 125 | breakpoint: null, 126 | className: 'pb-sm', 127 | declarations: { 128 | 'padding-bottom': '4px' 129 | }, 130 | pseudoClass: null, 131 | selector: 'pb-sm' 132 | }) 133 | t.deepEqual(createCssDeclarationBlock('pl-2px', {}), { 134 | breakpoint: null, 135 | className: 'pl-2px', 136 | declarations: { 137 | 'padding-left': '2px' 138 | }, 139 | pseudoClass: null, 140 | selector: 'pl-2px' 141 | }) 142 | }) 143 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/margin/negative-margin.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../../create-css-declaration-block' 4 | 5 | test('margin not defined in `theme`', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | createCssDeclarationBlock('-m', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('-m', { margin: {} }) 12 | }) 13 | }) 14 | 15 | test('default margin defined in `theme.margin`', function (t) { 16 | t.plan(1) 17 | t.deepEqual( 18 | createCssDeclarationBlock('-m', { 19 | margin: { 20 | default: '8px' 21 | } 22 | }), 23 | { 24 | breakpoint: null, 25 | className: '-m', 26 | declarations: { 27 | margin: '-8px' 28 | }, 29 | pseudoClass: null, 30 | selector: '-m' 31 | } 32 | ) 33 | }) 34 | 35 | test('custom margin defined in `theme.margin`', function (t) { 36 | t.plan(1) 37 | t.deepEqual(createCssDeclarationBlock('-m-sm', { margin: { sm: '4px' } }), { 38 | breakpoint: null, 39 | className: '-m-sm', 40 | declarations: { 41 | margin: '-4px' 42 | }, 43 | pseudoClass: null, 44 | selector: '-m-sm' 45 | }) 46 | }) 47 | 48 | test('pixel margin', function (t) { 49 | t.plan(1) 50 | t.deepEqual(createCssDeclarationBlock('-m-2px', {}), { 51 | breakpoint: null, 52 | className: '-m-2px', 53 | declarations: { 54 | margin: '-2px' 55 | }, 56 | pseudoClass: null, 57 | selector: '-m-2px' 58 | }) 59 | }) 60 | 61 | test('two sides only', function (t) { 62 | t.plan(2) 63 | t.deepEqual( 64 | createCssDeclarationBlock('-mx', { 65 | margin: { 66 | default: '8px' 67 | } 68 | }), 69 | { 70 | breakpoint: null, 71 | className: '-mx', 72 | declarations: { 73 | 'margin-left': '-8px', 74 | 'margin-right': '-8px' 75 | }, 76 | pseudoClass: null, 77 | selector: '-mx' 78 | } 79 | ) 80 | t.deepEqual( 81 | createCssDeclarationBlock('-my-sm', { 82 | margin: { 83 | sm: '4px' 84 | } 85 | }), 86 | { 87 | breakpoint: null, 88 | className: '-my-sm', 89 | declarations: { 90 | 'margin-bottom': '-4px', 91 | 'margin-top': '-4px' 92 | }, 93 | pseudoClass: null, 94 | selector: '-my-sm' 95 | } 96 | ) 97 | }) 98 | 99 | test('one side only', function (t) { 100 | t.plan(4) 101 | t.deepEqual(createCssDeclarationBlock('-mt-2px', {}), { 102 | breakpoint: null, 103 | className: '-mt-2px', 104 | declarations: { 105 | 'margin-top': '-2px' 106 | }, 107 | pseudoClass: null, 108 | selector: '-mt-2px' 109 | }) 110 | t.deepEqual( 111 | createCssDeclarationBlock('-mr', { 112 | margin: { default: '8px' } 113 | }), 114 | { 115 | breakpoint: null, 116 | className: '-mr', 117 | declarations: { 118 | 'margin-right': '-8px' 119 | }, 120 | pseudoClass: null, 121 | selector: '-mr' 122 | } 123 | ) 124 | t.deepEqual(createCssDeclarationBlock('-mb-sm', { margin: { sm: '4px' } }), { 125 | breakpoint: null, 126 | className: '-mb-sm', 127 | declarations: { 128 | 'margin-bottom': '-4px' 129 | }, 130 | pseudoClass: null, 131 | selector: '-mb-sm' 132 | }) 133 | t.deepEqual(createCssDeclarationBlock('-ml-2px', {}), { 134 | breakpoint: null, 135 | className: '-ml-2px', 136 | declarations: { 137 | 'margin-left': '-2px' 138 | }, 139 | pseudoClass: null, 140 | selector: '-ml-2px' 141 | }) 142 | }) 143 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generate-css", 3 | "version": "0.0.10", 4 | "description": "Dynamically generate functional CSS classes from HTML and JavaScript source files", 5 | "keywords": [ 6 | "css", 7 | "dynamic-css", 8 | "functional-css", 9 | "generate-css", 10 | "style" 11 | ], 12 | "license": "MIT", 13 | "author": "Yuan Qing Lim", 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/yuanqing/generate-css.git" 17 | }, 18 | "files": [ 19 | "lib", 20 | "docs", 21 | "example" 22 | ], 23 | "bin": { 24 | "generate-css": "lib/cli.js" 25 | }, 26 | "main": "lib/index.js", 27 | "scripts": { 28 | "build": "yarn run clean && concurrently --raw 'yarn run build-css' 'yarn run build-ts'", 29 | "build-css": "cpy 'css/**/*.css' '../lib' --cwd src --parents", 30 | "build-ts": "tsc", 31 | "clean": "rimraf '*.log' .nyc_output coverage example/style.css lib", 32 | "docs": "markdown-interpolate README.md && ts-node scripts/create-css-docs.ts", 33 | "fix": "concurrently --raw 'yarn run fix-css' 'yarn run fix-json' 'yarn run fix-ts'", 34 | "fix-css": "stylelint --fix 'src/**/*.css'", 35 | "fix-json": "prettier --loglevel error --write '*.json'", 36 | "fix-ts": "eslint --fix '{scripts,src}/**/*.ts'", 37 | "lint": "concurrently --raw 'yarn run lint-css' 'yarn run lint-ts'", 38 | "lint-css": "stylelint 'src/**/*.css'", 39 | "lint-ts": "eslint '{scripts,src}/**/*.ts'", 40 | "prepublishOnly": "yarn run clean && yarn run docs && yarn run build", 41 | "reset": "yarn run clean && rimraf node_modules yarn.lock && yarn install", 42 | "start": "ts-node src/cli.ts 'example/*.html' --config example/generate-css.config.json --output example/style.css", 43 | "test": "yarn run clean && tap 'src/**/__tests__/**/*.ts' --coverage-report html --coverage-report text --jobs-auto --no-browser --reporter terse", 44 | "watch": "yarn run clean && concurrently --raw 'yarn run watch-css' 'yarn run watch-ts'", 45 | "watch-css": "chokidar 'src/css/**/*.css' --command 'yarn run build-css' --initial --silent", 46 | "watch-ts": "tsc --preserveWatchOutput --watch" 47 | }, 48 | "dependencies": { 49 | "chokidar": "^3.4.3", 50 | "csso": "^4.2.0", 51 | "find-up": "^5.0.0", 52 | "fs-extra": "^9.0.1", 53 | "globby": "^11.0.1", 54 | "kleur": "^4.1.3", 55 | "normalize.css": "^8.0.1", 56 | "prettier": "^2.2.1", 57 | "sade": "^1.7.4" 58 | }, 59 | "devDependencies": { 60 | "@types/csso": "^3.5.1", 61 | "@types/fs-extra": "^9.0.6", 62 | "@types/node": "^14.14.16", 63 | "@types/npmlog": "^4.1.2", 64 | "@types/prettier": "^2.1.6", 65 | "@types/sade": "^1.7.2", 66 | "@types/tap": "^14.10.1", 67 | "chokidar-cli": "^2.1.0", 68 | "concurrently": "^5.3.0", 69 | "cpy-cli": "^3.1.1", 70 | "eslint": "^7.16.0", 71 | "eslint-config-yuanqing": "^0.0.4", 72 | "husky": "^4.3.6", 73 | "lint-staged": "^10.5.3", 74 | "markdown-interpolate": "^0.0.7", 75 | "prettier": "^2.2.1", 76 | "rimraf": "^3.0.2", 77 | "stylelint": "^13.8.0", 78 | "stylelint-config-yuanqing": "^0.0.1", 79 | "tap": "^14.11.0", 80 | "ts-node": "^9.1.1", 81 | "typescript": "^4.1.3" 82 | }, 83 | "eslintConfig": { 84 | "extends": "eslint-config-yuanqing" 85 | }, 86 | "husky": { 87 | "hooks": { 88 | "pre-commit": "lint-staged" 89 | } 90 | }, 91 | "lint-staged": { 92 | "*.css": [ 93 | "stylelint" 94 | ], 95 | "*.ts": [ 96 | "eslint" 97 | ] 98 | }, 99 | "prettier": "eslint-config-yuanqing/prettier", 100 | "stylelint": { 101 | "extends": "stylelint-config-yuanqing" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/border-width.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block' 4 | 5 | test('border width not defined in `theme`', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | createCssDeclarationBlock('b', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('b', { borderWidth: {} }) 12 | }) 13 | }) 14 | 15 | test('default border width defined in `theme.borderWidth`', function (t) { 16 | t.plan(1) 17 | t.deepEqual( 18 | createCssDeclarationBlock('b', { 19 | borderWidth: { 20 | default: '8px' 21 | } 22 | }), 23 | { 24 | breakpoint: null, 25 | className: 'b', 26 | declarations: { 27 | 'border-width': '8px' 28 | }, 29 | pseudoClass: null, 30 | selector: 'b' 31 | } 32 | ) 33 | }) 34 | 35 | test('custom border width defined in `theme.borderWidth`', function (t) { 36 | t.plan(1) 37 | t.deepEqual( 38 | createCssDeclarationBlock('b-sm', { borderWidth: { sm: '4px' } }), 39 | { 40 | breakpoint: null, 41 | className: 'b-sm', 42 | declarations: { 43 | 'border-width': '4px' 44 | }, 45 | pseudoClass: null, 46 | selector: 'b-sm' 47 | } 48 | ) 49 | }) 50 | 51 | test('pixel border width', function (t) { 52 | t.plan(1) 53 | t.deepEqual(createCssDeclarationBlock('b-2px', {}), { 54 | breakpoint: null, 55 | className: 'b-2px', 56 | declarations: { 57 | 'border-width': '2px' 58 | }, 59 | pseudoClass: null, 60 | selector: 'b-2px' 61 | }) 62 | }) 63 | 64 | test('two sides only', function (t) { 65 | t.plan(2) 66 | t.deepEqual( 67 | createCssDeclarationBlock('bx', { 68 | borderWidth: { 69 | default: '8px' 70 | } 71 | }), 72 | { 73 | breakpoint: null, 74 | className: 'bx', 75 | declarations: { 76 | 'border-left-width': '8px', 77 | 'border-right-width': '8px' 78 | }, 79 | pseudoClass: null, 80 | selector: 'bx' 81 | } 82 | ) 83 | t.deepEqual( 84 | createCssDeclarationBlock('by-sm', { 85 | borderWidth: { 86 | sm: '4px' 87 | } 88 | }), 89 | { 90 | breakpoint: null, 91 | className: 'by-sm', 92 | declarations: { 93 | 'border-bottom-width': '4px', 94 | 'border-top-width': '4px' 95 | }, 96 | pseudoClass: null, 97 | selector: 'by-sm' 98 | } 99 | ) 100 | }) 101 | 102 | test('one side only', function (t) { 103 | t.plan(4) 104 | t.deepEqual(createCssDeclarationBlock('bt-2px', {}), { 105 | breakpoint: null, 106 | className: 'bt-2px', 107 | declarations: { 108 | 'border-top-width': '2px' 109 | }, 110 | pseudoClass: null, 111 | selector: 'bt-2px' 112 | }) 113 | t.deepEqual( 114 | createCssDeclarationBlock('br', { 115 | borderWidth: { default: '8px' } 116 | }), 117 | { 118 | breakpoint: null, 119 | className: 'br', 120 | declarations: { 121 | 'border-right-width': '8px' 122 | }, 123 | pseudoClass: null, 124 | selector: 'br' 125 | } 126 | ) 127 | t.deepEqual( 128 | createCssDeclarationBlock('bb-sm', { borderWidth: { sm: '4px' } }), 129 | { 130 | breakpoint: null, 131 | className: 'bb-sm', 132 | declarations: { 133 | 'border-bottom-width': '4px' 134 | }, 135 | pseudoClass: null, 136 | selector: 'bb-sm' 137 | } 138 | ) 139 | t.deepEqual(createCssDeclarationBlock('bl-2px', {}), { 140 | breakpoint: null, 141 | className: 'bl-2px', 142 | declarations: { 143 | 'border-left-width': '2px' 144 | }, 145 | pseudoClass: null, 146 | selector: 'bl-2px' 147 | }) 148 | }) 149 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/compute-numeric-value-factory.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { computeNumericValueFactory } from '../compute-numeric-value-factory' 4 | 5 | test('invalid `theme.space`', function (t) { 6 | t.plan(1) 7 | t.throw(function () { 8 | computeNumericValueFactory({ baseSpace: '' }) 9 | }) 10 | }) 11 | 12 | test('invalid `value`', function (t) { 13 | t.plan(2) 14 | const computeNumericValue = computeNumericValueFactory({ 15 | baseSpace: '0.5rem' 16 | }) 17 | t.equal(computeNumericValue('foo', []), null) 18 | t.equal(computeNumericValue('-foo', []), null) 19 | }) 20 | 21 | test('theme `value`, single `themeKey`', function (t) { 22 | t.plan(4) 23 | const computeNumericValue = computeNumericValueFactory({ 24 | breakpoint: { sm: '320px' }, 25 | padding: { sm: '8px' } 26 | }) 27 | t.equal(computeNumericValue('sm', ['breakpoint']), '320px') 28 | t.equal(computeNumericValue('-sm', ['breakpoint']), '-320px') 29 | t.equal(computeNumericValue('sm', ['padding']), '8px') 30 | t.equal(computeNumericValue('-sm', ['padding']), '-8px') 31 | }) 32 | 33 | test('theme `value`, multiple `themeKey`', function (t) { 34 | t.plan(4) 35 | const computeNumericValue = computeNumericValueFactory({ 36 | breakpoint: { sm: '320px' }, 37 | padding: { sm: '8px' } 38 | }) 39 | t.equal(computeNumericValue('sm', ['breakpoint', 'padding']), '320px') 40 | t.equal(computeNumericValue('-sm', ['breakpoint', 'padding']), '-320px') 41 | t.equal(computeNumericValue('sm', ['padding', 'breakpoint']), '8px') 42 | t.equal(computeNumericValue('-sm', ['padding', 'breakpoint']), '-8px') 43 | }) 44 | 45 | test('auto `value`', function (t) { 46 | t.plan(1) 47 | const computeNumericValue = computeNumericValueFactory({ 48 | baseSpace: '0.5rem' 49 | }) 50 | t.equal(computeNumericValue('auto', []), 'auto') 51 | }) 52 | 53 | test('full `value`', function (t) { 54 | t.plan(2) 55 | const computeNumericValue = computeNumericValueFactory({ 56 | baseSpace: '0.5rem' 57 | }) 58 | t.equal(computeNumericValue('full', []), '100%') 59 | t.equal(computeNumericValue('-full', []), '-100%') 60 | }) 61 | 62 | test('pixel `value`', function (t) { 63 | t.plan(6) 64 | const computeNumericValue = computeNumericValueFactory({ 65 | baseSpace: '0.5rem' 66 | }) 67 | t.equal(computeNumericValue('0px', []), '0') 68 | t.equal(computeNumericValue('-0px', []), '0') 69 | t.equal(computeNumericValue('1px', []), '1px') 70 | t.equal(computeNumericValue('-1px', []), '-1px') 71 | t.equal(computeNumericValue('px', []), '1px') 72 | t.equal(computeNumericValue('-px', []), '-1px') 73 | }) 74 | 75 | test('fraction `value`', function (t) { 76 | t.plan(6) 77 | const computeNumericValue = computeNumericValueFactory({ 78 | baseSpace: '0.5rem' 79 | }) 80 | t.equal(computeNumericValue('1/2', []), '50%') 81 | t.equal(computeNumericValue('-1/2', []), '-50%') 82 | t.equal(computeNumericValue('1/3', []), '33.333333%') 83 | t.equal(computeNumericValue('-1/3', []), '-33.333333%') 84 | t.equal(computeNumericValue('2/3', []), '66.666667%') 85 | t.equal(computeNumericValue('-2/3', []), '-66.666667%') 86 | }) 87 | 88 | test('numeric `value`, with `theme.space` not defined', function (t) { 89 | t.plan(2) 90 | const computeNumericValue = computeNumericValueFactory({}) 91 | t.throw(function () { 92 | computeNumericValue('1', []) 93 | }) 94 | t.throw(function () { 95 | computeNumericValue('-1', []) 96 | }) 97 | }) 98 | 99 | test('numeric `value`, with `theme.space` defined', function (t) { 100 | t.plan(6) 101 | const computeNumericValue = computeNumericValueFactory({ 102 | baseSpace: '0.5rem' 103 | }) 104 | t.equal(computeNumericValue('0', []), '0') 105 | t.equal(computeNumericValue('-0', []), '0') 106 | t.equal(computeNumericValue('1', []), '0.5rem') 107 | t.equal(computeNumericValue('-1', []), '-0.5rem') 108 | t.equal(computeNumericValue('3', []), '1.5rem') 109 | t.equal(computeNumericValue('-3', []), '-1.5rem') 110 | }) 111 | -------------------------------------------------------------------------------- /src/utilities/create-css/plugins/border-radius.ts: -------------------------------------------------------------------------------- 1 | /* 2 | border-radius 3 | 4 | - `defaultValue` = `theme.borderRadius.default` 5 | - `value` = `theme.borderRadius[key]` || `resolveNumericValue(key)` 6 | 7 | `.rounded` | `border-radius: ${defaultValue};` 8 | `.rounded-t` | `border-top-left-radius: ${defaultValue};`
`border-top-right-radius: ${defaultValue};` 9 | `.rounded-r` | `border-top-right-radius: ${defaultValue};`
`border-bottom-right-radius: ${defaultValue};` 10 | `.rounded-b` | `border-bottom-left-radius: ${defaultValue};`
`border-bottom-right-radius: ${defaultValue};` 11 | `.rounded-l` | `border-top-left-radius: ${defaultValue};`
`border-bottom-left-radius: ${defaultValue};` 12 | `.rounded-tl` | `border-top-left-radius: ${defaultValue};` 13 | `.rounded-tr` | `border-top-right-radius: ${defaultValue};` 14 | `.rounded-bl` | `border-bottom-left-radius: ${defaultValue};` 15 | `.rounded-br` | `border-bottom-right-radius: ${defaultValue};` 16 | `.rounded-full` | `border-radius: 9999px;` 17 | `.rounded-t-full` | `border-top-left-radius: 9999px;`
`border-top-right-radius: 9999px;` 18 | `.rounded-r-full` | `border-top-right-radius: 9999px;`
`border-bottom-right-radius: 9999px;` 19 | `.rounded-b-full` | `border-bottom-left-radius: 9999px;`
`border-bottom-right-radius: 9999px;` 20 | `.rounded-l-full` | `border-top-left-radius: 9999px;`
`border-bottom-left-radius: 9999px;` 21 | `.rounded-tl-full` | `border-top-left-radius: 9999px;` 22 | `.rounded-tr-full` | `border-top-right-radius: 9999px;` 23 | `.rounded-bl-full` | `border-bottom-left-radius: 9999px;` 24 | `.rounded-br-full` | `border-bottom-right-radius: 9999px;` 25 | `.rounded-${key}` | `border-radius: ${value};` 26 | `.rounded-t-${key}` | `border-top-left-radius: ${value};`
`border-top-right-radius: ${value};` 27 | `.rounded-r-${key}` | `border-top-right-radius: ${value};`
`border-bottom-right-radius: ${value};` 28 | `.rounded-b-${key}` | `border-bottom-left-radius: ${value};`
`border-bottom-right-radius: ${value};` 29 | `.rounded-l-${key}` | `border-top-left-radius: ${value};`
`border-bottom-left-radius: ${value};` 30 | `.rounded-tl-${key}` | `border-top-left-radius: ${value};` 31 | `.rounded-tr-${key}` | `border-top-right-radius: ${value};` 32 | `.rounded-bl-${key}` | `border-bottom-left-radius: ${value};` 33 | `.rounded-br-${key}` | `border-bottom-right-radius: ${value};` 34 | */ 35 | 36 | import { Plugin, ThemeKeys } from '../../../types' 37 | 38 | export const borderRadius: Plugin = { 39 | createDeclarations: function ({ 40 | matches, 41 | computeNumericValue 42 | }: { 43 | matches: RegExpMatchArray 44 | computeNumericValue: ( 45 | value: string, 46 | themeKeys: Array 47 | ) => null | string 48 | }): { [property: string]: string } { 49 | const borderRadius = 50 | matches[2] === 'full' 51 | ? '9999px' 52 | : computeNumericValue(matches[2], ['borderRadius']) 53 | if (borderRadius === null) { 54 | throw new Error(`Invalid border radius: ${matches[2]}`) 55 | } 56 | switch (matches[1]) { 57 | case 't': { 58 | return { 59 | 'border-top-left-radius': borderRadius, 60 | 'border-top-right-radius': borderRadius 61 | } 62 | } 63 | case 'r': { 64 | return { 65 | 'border-bottom-right-radius': borderRadius, 66 | 'border-top-right-radius': borderRadius 67 | } 68 | } 69 | case 'b': { 70 | return { 71 | 'border-bottom-left-radius': borderRadius, 72 | 'border-bottom-right-radius': borderRadius 73 | } 74 | } 75 | case 'l': { 76 | return { 77 | 'border-bottom-left-radius': borderRadius, 78 | 'border-top-left-radius': borderRadius 79 | } 80 | } 81 | case 'tl': { 82 | return { 83 | 'border-top-left-radius': borderRadius 84 | } 85 | } 86 | case 'tr': { 87 | return { 88 | 'border-top-right-radius': borderRadius 89 | } 90 | } 91 | case 'bl': { 92 | return { 93 | 'border-bottom-left-radius': borderRadius 94 | } 95 | } 96 | case 'br': { 97 | return { 98 | 'border-bottom-right-radius': borderRadius 99 | } 100 | } 101 | } 102 | return { 103 | 'border-radius': borderRadius 104 | } 105 | }, 106 | regex: /^rounded(?:-([trbl]|[tb][lr]))?(?:-(.+))?$/ 107 | } 108 | -------------------------------------------------------------------------------- /src/utilities/create-css/__tests__/create-css-declaration-block/declarations/border-radius.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | 3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block' 4 | 5 | test('border radius not defined in `theme`', function (t) { 6 | t.plan(2) 7 | t.throw(function () { 8 | createCssDeclarationBlock('rounded', {}) 9 | }) 10 | t.throw(function () { 11 | createCssDeclarationBlock('rounded', { borderRadius: {} }) 12 | }) 13 | }) 14 | 15 | test('full border radius', function (t) { 16 | t.plan(1) 17 | t.deepEqual(createCssDeclarationBlock('rounded-full', {}), { 18 | breakpoint: null, 19 | className: 'rounded-full', 20 | declarations: { 21 | 'border-radius': '9999px' 22 | }, 23 | pseudoClass: null, 24 | selector: 'rounded-full' 25 | }) 26 | }) 27 | 28 | test('default border radius defined in `theme.borderRadius`', function (t) { 29 | t.plan(1) 30 | t.deepEqual( 31 | createCssDeclarationBlock('rounded', { 32 | borderRadius: { 33 | default: '8px' 34 | } 35 | }), 36 | { 37 | breakpoint: null, 38 | className: 'rounded', 39 | declarations: { 40 | 'border-radius': '8px' 41 | }, 42 | pseudoClass: null, 43 | selector: 'rounded' 44 | } 45 | ) 46 | }) 47 | 48 | test('custom border radius defined in `theme.borderRadius`', function (t) { 49 | t.plan(1) 50 | t.deepEqual( 51 | createCssDeclarationBlock('rounded-sm', { borderRadius: { sm: '4px' } }), 52 | { 53 | breakpoint: null, 54 | className: 'rounded-sm', 55 | declarations: { 56 | 'border-radius': '4px' 57 | }, 58 | pseudoClass: null, 59 | selector: 'rounded-sm' 60 | } 61 | ) 62 | }) 63 | 64 | test('pixel border radius', function (t) { 65 | t.plan(1) 66 | t.deepEqual(createCssDeclarationBlock('rounded-2px', {}), { 67 | breakpoint: null, 68 | className: 'rounded-2px', 69 | declarations: { 70 | 'border-radius': '2px' 71 | }, 72 | pseudoClass: null, 73 | selector: 'rounded-2px' 74 | }) 75 | }) 76 | 77 | test('two corners only', function (t) { 78 | t.plan(4) 79 | t.deepEqual(createCssDeclarationBlock('rounded-t-full', {}), { 80 | breakpoint: null, 81 | className: 'rounded-t-full', 82 | declarations: { 83 | 'border-top-left-radius': '9999px', 84 | 'border-top-right-radius': '9999px' 85 | }, 86 | pseudoClass: null, 87 | selector: 'rounded-t-full' 88 | }) 89 | t.deepEqual( 90 | createCssDeclarationBlock('rounded-r', { 91 | borderRadius: { default: '8px' } 92 | }), 93 | { 94 | breakpoint: null, 95 | className: 'rounded-r', 96 | declarations: { 97 | 'border-bottom-right-radius': '8px', 98 | 'border-top-right-radius': '8px' 99 | }, 100 | pseudoClass: null, 101 | selector: 'rounded-r' 102 | } 103 | ) 104 | t.deepEqual( 105 | createCssDeclarationBlock('rounded-b-sm', { borderRadius: { sm: '4px' } }), 106 | { 107 | breakpoint: null, 108 | className: 'rounded-b-sm', 109 | declarations: { 110 | 'border-bottom-left-radius': '4px', 111 | 'border-bottom-right-radius': '4px' 112 | }, 113 | pseudoClass: null, 114 | selector: 'rounded-b-sm' 115 | } 116 | ) 117 | t.deepEqual(createCssDeclarationBlock('rounded-l-2px', {}), { 118 | breakpoint: null, 119 | className: 'rounded-l-2px', 120 | declarations: { 121 | 'border-bottom-left-radius': '2px', 122 | 'border-top-left-radius': '2px' 123 | }, 124 | pseudoClass: null, 125 | selector: 'rounded-l-2px' 126 | }) 127 | }) 128 | 129 | test('one corner only', function (t) { 130 | t.plan(4) 131 | t.deepEqual(createCssDeclarationBlock('rounded-tl-full', {}), { 132 | breakpoint: null, 133 | className: 'rounded-tl-full', 134 | declarations: { 135 | 'border-top-left-radius': '9999px' 136 | }, 137 | pseudoClass: null, 138 | selector: 'rounded-tl-full' 139 | }) 140 | t.deepEqual( 141 | createCssDeclarationBlock('rounded-tr', { 142 | borderRadius: { default: '8px' } 143 | }), 144 | { 145 | breakpoint: null, 146 | className: 'rounded-tr', 147 | declarations: { 148 | 'border-top-right-radius': '8px' 149 | }, 150 | pseudoClass: null, 151 | selector: 'rounded-tr' 152 | } 153 | ) 154 | t.deepEqual( 155 | createCssDeclarationBlock('rounded-bl-sm', { borderRadius: { sm: '4px' } }), 156 | { 157 | breakpoint: null, 158 | className: 'rounded-bl-sm', 159 | declarations: { 160 | 'border-bottom-left-radius': '4px' 161 | }, 162 | pseudoClass: null, 163 | selector: 'rounded-bl-sm' 164 | } 165 | ) 166 | t.deepEqual(createCssDeclarationBlock('rounded-br-2px', {}), { 167 | breakpoint: null, 168 | className: 'rounded-br-2px', 169 | declarations: { 170 | 'border-bottom-right-radius': '2px' 171 | }, 172 | pseudoClass: null, 173 | selector: 'rounded-br-2px' 174 | }) 175 | }) 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generate CSS [![npm Version](https://img.shields.io/npm/v/generate-css?cacheSeconds=1800)](https://www.npmjs.com/package/generate-css) [![build](https://github.com/yuanqing/generate-css/workflows/build/badge.svg)](https://github.com/yuanqing/generate-css/actions?query=workflow%3Abuild) ![stability experimental](https://img.shields.io/badge/stability-experimental-red) 2 | 3 | > Dynamically generate functional CSS classes from HTML and JavaScript source files 4 | 5 | ## Features 6 | 7 | - Style your HTML using functional CSS classes, with support for applying styles specific to a pseudo-class (eg. `hover:bg-black`, `parent-hover:bg-black`) and/or specific to a breakpoint (eg. `sm@bg-black`) 8 | - Guarantees zero unused CSS; CSS classes are only included in the generated CSS file if they are actually used 9 | 10 | ## Example 11 | 12 | Given the following `example.html` file: 13 | 14 | 15 | ```html 16 | 17 | 18 | ``` 19 | 20 | 21 | …and the following `generate-css.config.json` configuration file: 22 | 23 | 24 | ```json 25 | { 26 | "reset": true, 27 | "theme": { 28 | "baseFontSize": { 29 | "default": "16px" 30 | }, 31 | "baseSpace": "1rem", 32 | "color": { 33 | "black": "#000", 34 | "blue": "#00f", 35 | "white": "#fff" 36 | }, 37 | "fontFamily": { 38 | "default": "Helvetica, Arial, sans-serif" 39 | }, 40 | "fontWeight": { 41 | "bold": "bolder" 42 | } 43 | } 44 | } 45 | ``` 46 | 47 | 48 | Do: 49 | 50 | ``` 51 | $ npx generate-css example.html --output style.css 52 | ``` 53 | 54 | This will generate the following `style.css` file (with the opening reset rules omitted): 55 | 56 | ``` 57 | // ...reset rules... 58 | 59 | html { 60 | font-size: 16px; 61 | } 62 | .hover\:bg-black:hover { 63 | background-color: #000; 64 | } 65 | .bg-blue { 66 | background-color: #00f; 67 | } 68 | .color-white { 69 | color: #fff; 70 | } 71 | .font { 72 | font-family: Helvetica, Arial, sans-serif; 73 | } 74 | .font-bold { 75 | font-weight: bolder; 76 | } 77 | .px-2 { 78 | padding-right: 2rem; 79 | padding-left: 2rem; 80 | } 81 | .py-1 { 82 | padding-top: 1rem; 83 | padding-bottom: 1rem; 84 | } 85 | .rounded-full { 86 | border-radius: 9999px; 87 | } 88 | ``` 89 | 90 | See that: 91 | 92 | - With `theme.baseFontSize.default` set to `16px`, `font-size: 16px;` is applied on `html`. 93 | - With `theme.baseSpace` set to `1rem`, the padding value of `.px-2` is `2rem` (ie. `1rem` × `2`), and the padding value of `.py-1` is `1rem` (ie. `1rem` × `2`). 94 | 95 | ## Usage 96 | 97 | ### Functional CSS classes 98 | 99 | See the [full list of functional CSS classes](/docs/css.md#readme) currently supported by Generate CSS. 100 | 101 | There are two “types” of functional CSS classes: 102 | 103 | #### Classes *without* a `${key}` 104 | 105 | For these classes, the value used in the generated CSS would generally be resolved from `theme[propertyName].default`. 106 | 107 | #### Classes *with* a `${key}` 108 | 109 | For these classes, the value used in the generated CSS would generally be resolved from `theme[propertyName][key]`. 110 | 111 | For certain CSS classes, if `theme[propertyName][key]` is `undefined`, the value used in the generated CSS might then be resolved through the following mapping: 112 | 113 | `key` | `resolveNumericValue(key)` | Example 114 | :--|:--|:-- 115 | `auto` | `auto` | `w-auto` → `width: auto;` 116 | `full` | `100%` | `w-full` → `width: 100%;` 117 | `px` | `1px` | `w-px` → `width: 1px;` 118 | `([0-9]+)px` | `($1)px` | `w-8px` → `width: 8px;` 119 | `([0-9]+)/([0-9]+)` | `($1 / $2 * 100)%` | `w-2/3` → `width: 66.666667%;` 120 | `([0-9]+)` | `theme.baseSpace` × `($1)` | `w-2` → `width: 2rem;`
(assuming `theme.baseSpace` is set to `1rem`) 121 | 122 | ### Pseudo-classes 123 | 124 | To apply a style on an element for a particular pseudo-class state only, add the pseudo-class keyword followed by a `:` character (eg. `hover:`) *before* the functional CSS class name. 125 | 126 | For example, using the class `hover:bg-black` in your HTML would result in the following CSS being generated: 127 | 128 | ``` 129 | .hover\:bg-black:hover { 130 | background-color: #000; 131 | } 132 | ``` 133 | 134 | ### Parent pseudo-classes 135 | 136 | To apply a style on an element for a particular *parent* pseudo-class state only, add the special parent pseudo-class keyword followed by a `:` character (eg. `parent-hover:`) *before* the functional CSS class name. 137 | 138 | For example, using the class `parent-hover:bg-black` in your HTML would result in the following CSS being generated: 139 | 140 | ``` 141 | .parent:hover .parent-hover\:bg-black { 142 | background-color: #000; 143 | } 144 | ``` 145 | 146 | ### Breakpoints 147 | 148 | Breakpoints must first be defined under the `theme.breakpoint` key in `generate-css.config.json`: 149 | 150 | ```json 151 | { 152 | "theme": { 153 | "breakpoint": { 154 | "sm": "540px", 155 | "md": "960px" 156 | } 157 | } 158 | } 159 | ``` 160 | 161 | To apply a style on an element for a particular breakpoint and higher (the media-query is a `min-width` on the particular breakpoint), add the name of the breakpoint followed by an `@` character (eg. `sm@`) *before* the functional CSS class name. 162 | 163 | For example, using the class `sm@bg-black` in your HTML would result in the following CSS being generated: 164 | 165 | ``` 166 | @media (min-width: 540px) { 167 | .sm\@bg-black { 168 | background-color: #000; 169 | } 170 | } 171 | ``` 172 | 173 | ### Configuration 174 | 175 | Configure Generate CSS via a `generate-css.config.json` file or a `generate-css` key in your `package.json`. 176 | 177 | #### `"reset"` 178 | 179 | (*`boolean`*) 180 | 181 | Whether to prepend reset rules from [`normalize.css`](https://github.com/necolas/normalize.css/blob/master/normalize.css) and Generate CSS’s [`reset.css` file](https://github.com/yuanqing/generate-css/blob/master/src/css/reset.css) to the generated CSS file. Defaults to `true`. 182 | 183 | #### `"theme"` 184 | 185 | (*`object`*) 186 | 187 | All keys are optional. 188 | 189 | - `theme.baseSpace` (*`string`*) — The base space unit used to resolve functional class names with a `${key}`, eg. `w-2` → `width: 2rem;` (assuming `theme.baseSpace` is set to `1rem`) 190 | - `theme.baseFontSize` (*`object`*) — A mapping of breakpoint names to the base `font-size` to be applied on the `html` element for that breakpoint. `theme.baseFontSize.default` is the base `font-size` applied on the `html` element. 191 | - `theme.breakpoint` (*`object`*) — A mapping of breakpoint names to screen widths. 192 | 193 | All other keys are objects that map `${keys}` referenced in your functional CSS classes to their corresponding values: 194 | 195 | 196 | - `theme.backgroundColor` 197 | - `theme.borderColor` 198 | - `theme.borderRadius` 199 | - `theme.borderWidth` 200 | - `theme.color` 201 | - `theme.fontFamily` 202 | - `theme.fontSize` 203 | - `theme.fontWeight` 204 | - `theme.height` 205 | - `theme.letterSpacing` 206 | - `theme.lineHeight` 207 | - `theme.margin` 208 | - `theme.maxHeight` 209 | - `theme.maxWidth` 210 | - `theme.minHeight` 211 | - `theme.minWidth` 212 | - `theme.padding` 213 | - `theme.space` 214 | - `theme.width` 215 | 216 | 217 | ## CLI 218 | 219 | 220 | ``` 221 | 222 | Description 223 | Dynamically generate functional CSS classes from HTML and JavaScript source files 224 | 225 | Usage 226 | $ generate-css [options] 227 | 228 | Options 229 | -a, --append Glob pattern for CSS files to append to the generated CSS file 230 | -c, --config Path to a `generate-css` configuration file 231 | -m, --minify Whether to minify the generated CSS file (default false) 232 | -o, --output Path to write the generated CSS file 233 | -p, --prepend Glob pattern for CSS files to prepend to the generated CSS file 234 | -w, --watch Whether to automatically generate a CSS file on changes to the source files (default false) 235 | -v, --version Displays current version 236 | -h, --help Displays this message 237 | 238 | Examples 239 | $ generate-css --append reset.css 240 | $ generate-css --minify 241 | $ generate-css --output style.css 242 | $ generate-css --prepend custom.css 243 | $ generate-css --watch 244 | 245 | ``` 246 | 247 | 248 | ## Prior art 249 | 250 | - The functional CSS class naming convention used in Generate CSS is heavily inspired by [Tailwind](https://tailwindcss.com/) and [Tachyons](https://tachyons.io/). 251 | 252 | ## License 253 | 254 | [MIT](/LICENSE.md) 255 | -------------------------------------------------------------------------------- /docs/css.md: -------------------------------------------------------------------------------- 1 | # Functional CSS classes 2 | 3 | ## align-items 4 | 5 | Class name | CSS rules 6 | :--|:-- 7 | `.items-baseline` | `align-items: baseline;` 8 | `.items-center` | `align-items: center;` 9 | `.items-end` | `align-items: flex-end;` 10 | `.items-start` | `align-items: flex-start;` 11 | `.items-stretch` | `align-items: stretch;` 12 | 13 | ## background-color 14 | 15 | Class name | CSS rules 16 | :--|:-- 17 | `.bg` | `background-color: ${theme.backgroundColor.default \|\| theme.color.default};` 18 | `.bg-${key}` | `background-color: ${theme.backgroundColor[key] \|\| theme.color[key]};` 19 | 20 | ## border-color 21 | 22 | Class name | CSS rules 23 | :--|:-- 24 | `.border` | `border-color: ${theme.borderColor.default \|\| theme.color.default};` 25 | `.border-${key}` | `border-color: ${theme.borderColor[key] \|\| theme.color[key]};` 26 | 27 | ## border-radius 28 | 29 | - `defaultValue` = `theme.borderRadius.default` 30 | - `value` = `theme.borderRadius[key]` || `resolveNumericValue(key)` 31 | 32 | Class name | CSS rules 33 | :--|:-- 34 | `.rounded` | `border-radius: ${defaultValue};` 35 | `.rounded-t` | `border-top-left-radius: ${defaultValue};`
`border-top-right-radius: ${defaultValue};` 36 | `.rounded-r` | `border-top-right-radius: ${defaultValue};`
`border-bottom-right-radius: ${defaultValue};` 37 | `.rounded-b` | `border-bottom-left-radius: ${defaultValue};`
`border-bottom-right-radius: ${defaultValue};` 38 | `.rounded-l` | `border-top-left-radius: ${defaultValue};`
`border-bottom-left-radius: ${defaultValue};` 39 | `.rounded-tl` | `border-top-left-radius: ${defaultValue};` 40 | `.rounded-tr` | `border-top-right-radius: ${defaultValue};` 41 | `.rounded-bl` | `border-bottom-left-radius: ${defaultValue};` 42 | `.rounded-br` | `border-bottom-right-radius: ${defaultValue};` 43 | `.rounded-full` | `border-radius: 9999px;` 44 | `.rounded-t-full` | `border-top-left-radius: 9999px;`
`border-top-right-radius: 9999px;` 45 | `.rounded-r-full` | `border-top-right-radius: 9999px;`
`border-bottom-right-radius: 9999px;` 46 | `.rounded-b-full` | `border-bottom-left-radius: 9999px;`
`border-bottom-right-radius: 9999px;` 47 | `.rounded-l-full` | `border-top-left-radius: 9999px;`
`border-bottom-left-radius: 9999px;` 48 | `.rounded-tl-full` | `border-top-left-radius: 9999px;` 49 | `.rounded-tr-full` | `border-top-right-radius: 9999px;` 50 | `.rounded-bl-full` | `border-bottom-left-radius: 9999px;` 51 | `.rounded-br-full` | `border-bottom-right-radius: 9999px;` 52 | `.rounded-${key}` | `border-radius: ${value};` 53 | `.rounded-t-${key}` | `border-top-left-radius: ${value};`
`border-top-right-radius: ${value};` 54 | `.rounded-r-${key}` | `border-top-right-radius: ${value};`
`border-bottom-right-radius: ${value};` 55 | `.rounded-b-${key}` | `border-bottom-left-radius: ${value};`
`border-bottom-right-radius: ${value};` 56 | `.rounded-l-${key}` | `border-top-left-radius: ${value};`
`border-bottom-left-radius: ${value};` 57 | `.rounded-tl-${key}` | `border-top-left-radius: ${value};` 58 | `.rounded-tr-${key}` | `border-top-right-radius: ${value};` 59 | `.rounded-bl-${key}` | `border-bottom-left-radius: ${value};` 60 | `.rounded-br-${key}` | `border-bottom-right-radius: ${value};` 61 | 62 | ## border-width 63 | 64 | - `defaultValue` = `theme.borderWidth.default` 65 | - `value` = `theme.borderWidth[key]` || `resolveNumericValue(key)` 66 | 67 | Class name | CSS rules 68 | :--|:-- 69 | `.b` | `border-width: ${defaultValue};` 70 | `.bx` | `border-left-width: ${defaultValue};`
`border-right-width: ${defaultValue};` 71 | `.by` | `border-top-width: ${defaultValue};`
`border-bottom-width: ${defaultValue};` 72 | `.bt` | `border-top-width: ${defaultValue};` 73 | `.br` | `border-right-width: ${defaultValue};` 74 | `.bb` | `border-bottom-width: ${defaultValue};` 75 | `.bl` | `border-left-width: ${defaultValue};` 76 | `.b-${key}` | `border-width: ${value};` 77 | `.bx-${key}` | `border-left-width: ${value};`
`border-right-width: ${value};` 78 | `.by-${key}` | `border-top-width: ${value};`
`border-bottom-width: ${value};` 79 | `.bt-${key}` | `border-top-width: ${value};` 80 | `.br-${key}` | `border-right-width: ${value};` 81 | `.bb-${key}` | `border-bottom-width: ${value};` 82 | `.bl-${key}` | `border-left-width: ${value};` 83 | 84 | ## color 85 | 86 | Class name | CSS rules 87 | :--|:-- 88 | `.color` | `color: ${theme.color.default};` 89 | `.color-${key}` | `color: ${theme.color[key]};` 90 | 91 | ## cursor 92 | 93 | Class name | CSS rules 94 | :--|:-- 95 | `.cursor-default` | `cursor: default;` 96 | `.cursor-pointer` | `cursor: pointer;` 97 | 98 | ## display 99 | 100 | Class name | CSS rules 101 | :--|:-- 102 | `.block` | `display: block;` 103 | `.flex` | `display: flex;` 104 | `.hidden` | `display: none;` 105 | `.inline-flex` | `display: inline-flex;` 106 | 107 | ## flex 108 | 109 | Class name | CSS rules 110 | :--|:-- 111 | `.flex-1` | `flex: 1 1 0%;` 112 | `.flex-auto` | `flex: 1 1 auto;` 113 | `.flex-initial` | `flex: 0 1 auto;` 114 | `.flex-none` | `flex: none;` 115 | 116 | ## flex-wrap 117 | 118 | Class name | CSS rules 119 | :--|:-- 120 | `.flex-nowrap` | `flex-wrap: nowrap;` 121 | `.flex-wrap` | `flex-wrap: wrap;` 122 | `.flex-wrap-reverse` | `flex-wrap: wrap-reverse;` 123 | 124 | ## font-family 125 | 126 | Class name | CSS rules 127 | :--|:-- 128 | `.font` | `font-family: ${theme.fontFamily.default};` 129 | `.font-${key}` | `font-family: ${theme.fontFamily[key]};` 130 | 131 | ## font-size 132 | 133 | Class name | CSS rules 134 | :--|:-- 135 | `.font-${key}` | `font-size: ${theme.fontSize[key]};` 136 | 137 | ## font-weight 138 | 139 | Class name | CSS rules 140 | :--|:-- 141 | `.font-${key}` | `font-weight: ${theme.fontWeight[key]};` 142 | 143 | ## height 144 | 145 | Class name | CSS rules 146 | :--|:-- 147 | `.h` | `height: ${theme.height.default};` 148 | `.h-screen` | `height: 100vh;` 149 | `.h-${key}` | `height: ${theme.height[key] \|\| computeNumericValue(key)};` 150 | `.minh` | `min-height: ${theme.height.default};` 151 | `.minh-screen` | `min-height: 100vh;` 152 | `.minh-${key}` | `min-height: ${theme.height[key] \|\| computeNumericValue(key)};` 153 | `.maxh` | `max-height: ${theme.height.default};` 154 | `.maxh-screen` | `max-height: 100vh;` 155 | `.maxh-${key}` | `max-height: ${theme.height[key] \|\| computeNumericValue(key)};` 156 | 157 | ## justify-content 158 | 159 | Class name | CSS rules 160 | :--|:-- 161 | `.justify-around` | `justify-content: space-around;` 162 | `.justify-between` | `justify-content: space-between;` 163 | `.justify-center` | `justify-content: center;` 164 | `.justify-end` | `justify-content: flex-end;` 165 | `.justify-evenly` | `justify-content: space-evenly;` 166 | `.justify-start` | `justify-content: flex-start;` 167 | 168 | ## letter-spacing 169 | 170 | Class name | CSS rules 171 | :--|:-- 172 | `.kerning` | `letter-spacing: ${theme.letterSpacing.default};` 173 | `.kerning-${key}` | `letter-spacing: ${theme.letterSpacing[key]};` 174 | 175 | ## line-height 176 | 177 | Class name | CSS rules 178 | :--|:-- 179 | `.leading` | `line-height: ${theme.lineHeight.default};` 180 | `.leading-${key}` | `line-height: ${theme.lineHeight[key]};` 181 | 182 | ## margin 183 | 184 | - `defaultValue` = `theme.margin.default` || `theme.space.default` 185 | - `value` = `theme.margin[key]` || `theme.space[key]` || `resolveNumericValue(key)` 186 | 187 | Class name | CSS rules 188 | :--|:-- 189 | `.m` | `margin: ${defaultValue};` 190 | `.mx` | `margin-left: ${defaultValue};`
`margin-right: ${defaultValue};` 191 | `.my` | `margin-top: ${defaultValue};`
`margin-bottom: ${defaultValue};` 192 | `.mt` | `margin-top: ${defaultValue};` 193 | `.mr` | `margin-right: ${defaultValue};` 194 | `.mb` | `margin-bottom: ${defaultValue};` 195 | `.ml` | `margin-left: ${defaultValue};` 196 | `.mx-${key}` | `margin-left: ${value};`
`margin-right: ${value};` 197 | `.my-${key}` | `margin-top: ${value};`
`margin-bottom: ${value};` 198 | `.mt-${key}` | `margin-top: ${value};` 199 | `.mr-${key}` | `margin-right: ${value};` 200 | `.mb-${key}` | `margin-bottom: ${value};` 201 | `.ml-${key}` | `margin-left: ${value};` 202 | `.-m` | `margin: -${defaultValue};` 203 | `.-mx` | `margin-left: -${defaultValue};`
`margin-right: -${defaultValue};` 204 | `.-my` | `margin-top: -${defaultValue};`
`margin-bottom: -${defaultValue};` 205 | `.-mt` | `margin-top: -${defaultValue};` 206 | `.-mr` | `margin-right: -${defaultValue};` 207 | `.-mb` | `margin-bottom: -${defaultValue};` 208 | `.-ml` | `margin-left: -${defaultValue};` 209 | `.-mx-${key}` | `margin-left: -${value};`
`margin-right: -${value};` 210 | `.-my-${key}` | `margin-top: -${value};`
`margin-bottom: -${value};` 211 | `.-mt-${key}` | `margin-top: -${value};` 212 | `.-mr-${key}` | `margin-right: -${value};` 213 | `.-mb-${key}` | `margin-bottom: -${value};` 214 | `.-ml-${key}` | `margin-left: -${value};` 215 | 216 | ## outline 217 | 218 | Class name | CSS rules 219 | :--|:-- 220 | `.outline-none` | `outline: none;` 221 | 222 | ## padding 223 | 224 | defaultValue = theme.padding.default || theme.space.default 225 | value = theme.padding[key] || theme.space[key] || computeNumericValue(key) 226 | 227 | Class name | CSS rules 228 | :--|:-- 229 | `.p` | `padding: ${defaultValue};` 230 | `.px` | `padding-left: ${defaultValue};`
`padding-right: ${defaultValue};` 231 | `.py` | `padding-top: ${defaultValue};`
`padding-bottom: ${defaultValue};` 232 | `.pt` | `padding-top: ${defaultValue};` 233 | `.pr` | `padding-right: ${defaultValue};` 234 | `.pb` | `padding-bottom: ${defaultValue};` 235 | `.pl` | `padding-left: ${defaultValue};` 236 | `.px-${key}` | `padding-left: ${value};`
`padding-right: ${value};` 237 | `.py-${key}` | `padding-top: ${value};`
`padding-bottom: ${value};` 238 | `.pt-${key}` | `padding-top: ${value};` 239 | `.pr-${key}` | `padding-right: ${value};` 240 | `.pb-${key}` | `padding-bottom: ${value};` 241 | `.pl-${key}` | `padding-left: ${value};` 242 | 243 | ## position 244 | 245 | Class name | CSS rules 246 | :--|:-- 247 | `.absolute` | `position: absolute;` 248 | `.fixed` | `position: fixed;` 249 | `.relative` | `position: relative;` 250 | `.static` | `position: static;` 251 | 252 | ## select 253 | 254 | Class name | CSS rules 255 | :--|:-- 256 | `.select-all` | `user-select: all;` 257 | `.select-auto` | `user-select: auto;` 258 | `.select-none` | `user-select: none;` 259 | `.select-text` | `user-select: text;` 260 | 261 | ## text style 262 | 263 | Class name | CSS rules 264 | :--|:-- 265 | `.text` | `font-size: ${theme.fontSize.default};`
`font-weight: ${theme.fontWeight.default};`
`letter-spacing: ${theme.letterSpacing.default};`
`line-height: ${theme.lineHeight.default};` 266 | `.text-${key}` | `font-size: ${theme.fontSize[key]};`
`font-weight: ${theme.fontWeight[key]};`
`letter-spacing: ${theme.letterSpacing[key]};`
`line-height: ${theme.lineHeight[key]};` 267 | 268 | ## text-align 269 | 270 | Class name | CSS rules 271 | :--|:-- 272 | `.text-center` | `text-align: center;` 273 | `.text-justify` | `text-align: justify;` 274 | `.text-left` | `text-align: left;` 275 | `.text-right` | `text-align: right;` 276 | 277 | ## text-decoration 278 | 279 | Class name | CSS rules 280 | :--|:-- 281 | `.line-through` | `text-decoration: line-through;` 282 | `.no-underline` | `text-decoration: none;` 283 | `.underline` | `text-decoration: underline;` 284 | 285 | ## text-transform 286 | 287 | Class name | CSS rules 288 | :--|:-- 289 | `.caps` | `text-transform: capitalize;` 290 | `.lowercase` | `text-transform: lowercase;` 291 | `.normal-case` | `text-transform: none;` 292 | `.uppercase` | `text-transform: uppercase;` 293 | 294 | ## top, right, bottom, left 295 | 296 | - `value` = `theme.space[key]` || `resolveNumericValue(key)` 297 | 298 | Class name | CSS rules 299 | :--|:-- 300 | `.top` | `top: 0;` 301 | `.top-${key}` | `top: ${value};` 302 | `.-top-${key}` | `top: -${value};` 303 | `.right` | `right: 0;` 304 | `.right-${key}` | `right: ${value};` 305 | `.-right-${key}` | `right: -${value};` 306 | `.bottom` | `bottom: 0;` 307 | `.bottom-${key}` | `bottom: ${value};` 308 | `.-bottom-${key}` | `bottom: -${value};` 309 | `.left` | `left: 0;` 310 | `.left-${key}` | `left: ${value};` 311 | `.-left-${key}` | `left: -${value};` 312 | 313 | ## width 314 | 315 | Class name | CSS rules 316 | :--|:-- 317 | `.w` | `width: ${theme.width.default};` 318 | `.w-screen` | `width: 100vw;` 319 | `.w-${key}` | `width: ${theme.width[key] \|\| computeNumericValue(key)};` 320 | `.minw` | `min-width: ${theme.width.default};` 321 | `.minw-screen` | `min-width: 100vw;` 322 | `.minw-${key}` | `min-width: ${theme.width[key] \|\| computeNumericValue(key)};` 323 | `.maxw` | `max-width: ${theme.width.default};` 324 | `.maxw-screen` | `max-width: 100vw;` 325 | `.maxw-${key}` | `max-width: ${theme.width[key] \|\| computeNumericValue(key)};` 326 | --------------------------------------------------------------------------------