From 83a9b38c168559ab26769649d63430398e430236 Mon Sep 17 00:00:00 2001 From: Bart Veneman <1536852+bartveneman@users.noreply.github.com> Date: Sat, 28 Jun 2025 00:06:58 +0200 Subject: [PATCH 1/3] add tests for index file --- readme.md | 9 +- src/index.test.ts | 380 ++++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 12 +- tsconfig.json | 4 +- 4 files changed, 398 insertions(+), 7 deletions(-) create mode 100644 src/index.test.ts diff --git a/readme.md b/readme.md index fc78cce..2822f85 100644 --- a/readme.md +++ b/readme.md @@ -16,16 +16,19 @@ import { css_to_tokens } from '@projectwallace/css-design-tokens' let tokens = css_to_tokens(`.my-design-system { color: green; }`) // Or if you already have done CSS analysis with @projectwallace/css-analyzer: +// NOTE: it is important that `useLocations` is true import { analyze } from '@projectwallace/css-analyzer' import { analysis_to_tokens } from '@projectwallace/css-design-tokens' -let analysis = analyze(`.my-design-system { color: green; }`) -let tokens = css_to_tokens(analysis) +let analysis = analyze(`.my-design-system { color: green; }`, { + useLocations: true // MUST be true +}) +let tokens = analysis_to_tokens(analysis) ``` ## Acknowledgements -- ColorJS.io powers all color conversions necessary for grouping +- ColorJS.io powers all color conversions necessary for grouping and sorting ## Related projects diff --git a/src/index.test.ts b/src/index.test.ts new file mode 100644 index 0000000..2471070 --- /dev/null +++ b/src/index.test.ts @@ -0,0 +1,380 @@ +import { test, expect, describe } from 'vitest' +import { analysis_to_tokens, css_to_tokens } from './index.js' +import { EXTENSION_AUTHORED_AS } from './types.js' + +test('css_to_tokens', () => { + expect(typeof css_to_tokens).toBe('function') +}) + +test('analysis_to_tokens', () => { + expect(typeof analysis_to_tokens).toBe('function') +}) + +describe('colors', () => { + test('output a colors section', () => { + let actual = css_to_tokens(` + .my-design-system { + color: green; + color: rgb(100 100 100 / 0.2); + } + `) + expect(actual.Color).toEqual({ + 'green-5e0cf03': { + $value: 'green' + }, + 'grey-812aeee': { + $value: 'rgb(100 100 100 / 0.2)' + }, + }) + }) +}) + +describe('font sizes', () => { + test('outputs a dimension type when using rem or px', () => { + let actual = css_to_tokens(` + .my-design-system { + font-size: 16px; + } + `) + expect(actual.FontSize).toEqual({ + 'fontSize-171eed': { + $type: 'dimension', + $value: { + value: 16, + unit: 'px' + }, + $extensions: { + [EXTENSION_AUTHORED_AS]: '16px' + } + }, + }) + }) + + test('outputs a value type when not using rem or px', () => { + let actual = css_to_tokens(` + .my-design-system { + font-size: 20vmin; + } + `) + expect(actual.FontSize).toEqual({ + 'fontSize-582e015a': { + $value: '20vmin', + $extensions: { + [EXTENSION_AUTHORED_AS]: '20vmin' + } + }, + }) + }) +}) + +describe('font families', () => { + test('outputs a font family token for each font family', () => { + let actual = css_to_tokens(` + .my-design-system { + font-family: 'Inter', sans-serif; + } + `) + expect(actual.FontFamily).toEqual({ + 'fontFamily-3375cf09': { + $type: 'fontFamily', + $value: ['Inter', 'sans-serif'], + $extensions: { + [EXTENSION_AUTHORED_AS]: "'Inter', sans-serif" + } + }, + }) + }) +}) + +describe('line heights', () => { + test('outputs a dimension type when using rem or px', () => { + let actual = css_to_tokens(` + .my-design-system { + line-height: 1.5rem; + } + `) + expect(actual.LineHeight).toEqual({ + 'lineHeight-563f7fe2': { + $type: 'dimension', + $value: { + value: 1.5, + unit: 'rem' + }, + $extensions: { + [EXTENSION_AUTHORED_AS]: '1.5rem' + } + } + }) + }) + + test('outputs a number type when using a number', () => { + let actual = css_to_tokens(` + .my-design-system { + line-height: 1.5; + } + `) + expect(actual.LineHeight).toEqual({ + 'lineHeight-bdb8': { + $type: 'number', + $value: 1.5, + $extensions: { + [EXTENSION_AUTHORED_AS]: '1.5' + } + } + }) + }) + + test('outputs an unparsed type when using a value that is not a number or rem or px', () => { + let actual = css_to_tokens(` + .my-design-system { + line-height: 20vmin; + } + `) + expect(actual.LineHeight).toEqual({ + 'lineHeight-582e015a': { + $value: '20vmin', + $extensions: { + [EXTENSION_AUTHORED_AS]: '20vmin' + } + } + }) + }) +}) + +describe('gradients', () => { + test('outputs a gradient token for each gradient', () => { + let actual = css_to_tokens(` + .my-design-system { + background: linear-gradient(to right, red, blue); + } + `) + expect(actual.Gradient).toEqual({ + 'gradient-2aec04e5': { + $value: 'linear-gradient(to right, red, blue)', + }, + }) + }) +}) + +describe('box shadows', () => { + test('outputs a single box shadow token when 1 shadow is used', () => { + let actual = css_to_tokens(` + .my-design-system { + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.5); + } + `) + expect(actual.BoxShadow).toEqual({ + 'boxShadow-6f90da6b': { + $type: 'shadow', + $value: { + offsetX: { + $type: 'dimension', + value: 0, + unit: 'px' + }, + offsetY: { + $type: 'dimension', + value: 0, + unit: 'px' + }, + blur: { + $type: 'dimension', + value: 10, + unit: 'px' + }, + spread: { + $type: 'dimension', + value: 0, + unit: 'px' + }, + inset: false, + color: 'rgba(0, 0, 0, 0.5)', + }, + $extensions: { + [EXTENSION_AUTHORED_AS]: '0 0 10px 0 rgba(0, 0, 0, 0.5)' + } + }, + }) + }) + + test('outputs multiple box shadow tokens when multiple shadows are used', () => { + let actual = css_to_tokens(` + .my-design-system { + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.5), 0 0 10px 0 rgba(0, 0, 0, 0.5); + } + `) + expect(actual.BoxShadow).toEqual({ + 'boxShadow-be2751ac': { + $type: 'shadow', + $value: [ + { + offsetX: { + $type: 'dimension', + value: 0, + unit: 'px' + }, + offsetY: { + $type: 'dimension', + value: 0, + unit: 'px' + }, + blur: { + $type: 'dimension', + value: 10, + unit: 'px' + }, + spread: { + $type: 'dimension', + value: 0, + unit: 'px' + }, + inset: false, + color: 'rgba(0, 0, 0, 0.5)' + }, + { + offsetX: { + $type: 'dimension', + value: 0, + unit: 'px' + }, + offsetY: { + $type: 'dimension', + value: 0, + unit: 'px' + }, + blur: { + $type: 'dimension', + value: 10, + unit: 'px' + }, + spread: { + $type: 'dimension', + value: 0, + unit: 'px' + }, + inset: false, + color: 'rgba(0, 0, 0, 0.5)' + } + ], + $extensions: { + [EXTENSION_AUTHORED_AS]: '0 0 10px 0 rgba(0, 0, 0, 0.5), 0 0 10px 0 rgba(0, 0, 0, 0.5)' + } + }, + }) + }) +}) + +describe('border radius', () => { + test('outputs a radius token for each border radius', () => { + let actual = css_to_tokens(` + .my-design-system { + border-radius: 10px; + } + `) + expect(actual.Radius).toEqual({ + 'radius-170867': { + $value: '10px', + }, + }) + }) +}) + +describe('duration', () => { + test('outputs a token when using a valid duration', () => { + let actual = css_to_tokens(` + .my-design-system { + animation-duration: 1s; + } + `) + expect(actual.Duration).toEqual({ + 'duration-17005f': { + $type: 'duration', + $value: { + value: 1000, + unit: 'ms' + }, + $extensions: { + [EXTENSION_AUTHORED_AS]: '1s' + } + }, + }) + }) + + test('outputs an unparsed token when using an invalid duration', () => { + let actual = css_to_tokens(` + .my-design-system { + animation-duration: var(--test); + } + `) + expect(actual.Duration).toEqual({ + 'duration-452f2b3b': { + $value: 'var(--test)', + $extensions: { + [EXTENSION_AUTHORED_AS]: 'var(--test)' + } + }, + }) + }) +}) + +describe('easing', () => { + test('outputs a token when using an easing keyword', () => { + let actual = css_to_tokens(` + .my-design-system { + animation-timing-function: ease-in-out; + } + `) + expect(actual.Easing).toEqual({ + 'easing-ea6c7565': { + $type: 'cubicBezier', + $value: [ + 0.42, + 0, + 0.58, + 1 + ], + $extensions: { + [EXTENSION_AUTHORED_AS]: 'ease-in-out' + } + }, + }) + }) + + test('outputs a token when describing a bezier curve', () => { + let actual = css_to_tokens(` + .my-design-system { + animation-timing-function: cubic-bezier(0, 0, 0.5, .8); + } + `) + expect(actual.Easing).toEqual({ + 'easing-90111eba': { + $type: 'cubicBezier', + $value: [ + 0, + 0, + 0.5, + 0.8 + ], + $extensions: { + [EXTENSION_AUTHORED_AS]: 'cubic-bezier(0, 0, 0.5, .8)' + } + }, + }) + }) + + test('outputs an unparsed token when using an invalid easing', () => { + let actual = css_to_tokens(` + .my-design-system { + animation-timing-function: var(--test); + } + `) + expect(actual.Easing).toEqual({ + 'easing-12bb7f36': { + $value: 'var(--test)', + $extensions: { + [EXTENSION_AUTHORED_AS]: 'var(--test)' + } + }, + }) + }) +}) \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 794c9c5..2b7da0d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import { analyze } from '@projectwallace/css-analyzer' import { convert as convert_duration } from 'css-time-sort' import { group_colors, color_dict } from './group-colors.js' import { destructure_box_shadow, type DestructuredShadow } from './destructure-box-shadow.js' @@ -20,7 +21,14 @@ import { const TYPE_CUBIC_BEZIER = 'cubicBezier' as const -export function generate_tokens(analysis: CssAnalysis) { +export function css_to_tokens(css: string) { + let analysis = analyze(css, { + useLocations: true, + }) + return analysis_to_tokens(analysis) +} + +export function analysis_to_tokens(analysis: CssAnalysis) { return { Color: (() => { let colors = Object.create(null) as Record @@ -35,7 +43,7 @@ export function generate_tokens(analysis: CssAnalysis) { } return colors })(), - FontSizes: (() => { + FontSize: (() => { let font_sizes = Object.create(null) as Record for (let font_size in analysis.values.fontSizes.uniqueWithLocations) { diff --git a/tsconfig.json b/tsconfig.json index 53ceec3..ff8f71a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,7 @@ "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, - "noEmit": true, + // "noEmit": true, // Code runs in the DOM "lib": [ "ES2022", @@ -23,7 +23,7 @@ ], }, "include": [ - "src/**/*" + "src/index.ts" ], "exclude": [ "node_modules" From 15b10500e16ccc243ea81d369b1b395c16db65be Mon Sep 17 00:00:00 2001 From: Bart Veneman <1536852+bartveneman@users.noreply.github.com> Date: Sat, 28 Jun 2025 00:11:14 +0200 Subject: [PATCH 2/3] fix types --- tsconfig.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index ff8f71a..3904641 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,7 @@ "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, - // "noEmit": true, + "noEmit": true, // Code runs in the DOM "lib": [ "ES2022", @@ -23,9 +23,10 @@ ], }, "include": [ - "src/index.ts" + "src/**/*.ts" ], "exclude": [ - "node_modules" + "node_modules", + "src/**/*.test.ts" ] } \ No newline at end of file From fff1d9b90b480f7b035ae8fe6a2467bee62b33d0 Mon Sep 17 00:00:00 2001 From: Bart Veneman <1536852+bartveneman@users.noreply.github.com> Date: Sat, 28 Jun 2025 00:13:24 +0200 Subject: [PATCH 3/3] readme fixes --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 2822f85..95742d4 100644 --- a/readme.md +++ b/readme.md @@ -28,7 +28,8 @@ let tokens = analysis_to_tokens(analysis) ## Acknowledgements -- ColorJS.io powers all color conversions necessary for grouping and sorting +- [CSSTree](https://github.com/csstree/csstree) does all the heavy lifting of parsing CSS +- [ColorJS.io](https://colorjs.io/) powers all color conversions necessary for grouping and sorting ## Related projects