diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6e52563..c53261f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,46 +1,37 @@ # Contributing to Colorus.js πŸŽ‰ -We welcome contributions to Colorus.js! Whether you're fixing bugs πŸ›, improving documentation πŸ“š, or adding new features through plugins ✨, your help is valuable in making this library even better. +Thank you for considering a contribution to **Colorus.js**! From fixing bugs to improving documentation or expanding functionality with plugins and parsers, every contribution is valuable. -## Future Plans: Mono-repo πŸ“¦ +## Design Choice -In the future, we intend to transition the Colorus.js repository into a mono-repo structure. This will allow us to manage the core package and official plugins within a single repository, streamlining development and maintenance. +The design choice ensures that colors are parsed, transformed, and returned in predictable and reliable ways, even if it means some operations may be slower than highly optimized alternatives. -## Core Functionality: Non-Extensible (Intentional) πŸ”’ +Despite this focus, **Colorus.js** performs efficiently in most use cases. While the library might not aim to be the absolute fastest, it’s still capable of handling high-demand operations quickly. If you identify areas where optimizations can be made without sacrificing validation, please feel free to propose changes or submit a pull request. -| Core Functionality | Extensible via Plugins | -| -------------------------- | ---------------------- | -| Color creation (`dye`) | No | -| Color conversions | No | -| Color adjustments | No | -| Accessibility calculations | No | +## Core and Extensible Features πŸ”’ -The plugin system provides a powerful and flexible mechanism for adding custom features and behaviors to color objects created by `dye`. We encourage you to leverage this system to enhance the capabilities of Colorus.js without modifying its core. πŸ’ͺ +| Feature | Extensible via Plugins or Parsers | +| --------------------------------- | --------------------------------------------- | +| Color creation (`dye`) | [Parsers](docs/guide/WORKING_WITH_PARSERS.md) | +| Color conversions and adjustments | [Plugins](docs/guide/WORKING_WITH_PLUGINS.md) | -## Focus on Performance πŸš€ - -We're always looking for ways to improve the performance of Colorus.js. Contributions that enhance the speed or reduce the bundle size of the library are highly appreciated. If you have ideas for optimizations or performance improvements, please share them with us! +The plugin and parser systems in version 2.0.0 allow easy extensibility, making it possible to add custom features while preserving the core library’s structure. ## How to Contribute πŸ› οΈ -1. **For complex or significant changes:** Please open an issue first to discuss your proposed changes with the maintainers. This helps ensure alignment with the project's goals and avoids potential conflicts or wasted effort. πŸ’¬ - -2. **For simple fixes (e.g., typos, grammar):** Feel free to directly open a pull request. πŸ‘ - -3. In either case, follow these steps: - - Fork the repository and create a new branch for your feature or bug fix. 🍴 - - Make your changes and ensure that the code is well-documented and tested. πŸ§ͺ - - Submit a pull request, clearly describing your changes and their benefits. πŸš€ +1. **Significant Changes:** Open an issue first to discuss. +2. **Minor Fixes (e.g., typos):** Open a pull request directly. +3. **Steps:** + - Fork the repository, create a branch, and make your changes. + - Ensure documentation and tests accompany your updates. + - Submit a pull request with a clear, concise description. ## Documentation and Type Support πŸ“š -Clear and comprehensive documentation is essential for any library, and Colorus.js is no exception. We strive to provide accurate and up-to-date documentation for all core functions, types, and the plugin system. Contributions that improve the documentation, add examples, or clarify usage are highly valued. -✍️ - -### Type Safety and Function Chaining πŸ”’β›“οΈ +Complete documentation and robust TypeScript support are priorities for **Colorus.js**. Contributions that improve docs, examples, or types are always welcome. -We place a strong emphasis on type safety. The function chaining mechanism, in particular, relies heavily on TypeScript's type inference to provide a smooth and error-free developer experience. When contributing, pay close attention to maintaining and enhancing the type definitions, especially when modifying or adding new features that interact with the chaining mechanism. +## Type Safety and Chaining πŸ”’β›“οΈ -We appreciate your interest in contributing to Colorus.js! Please don't hesitate to reach out if you have any questions or need further guidance. πŸ™Œ +Type safety is critical to **Colorus.js**, particularly in the chaining API. Please ensure typings are preserved or improved when contributing any functions affecting chaining behavior. -**Thank you for your support!** πŸ™ +**Thank you for supporting Colorus.js!** πŸ™ diff --git a/README.md b/README.md index b391120..dbeaf6b 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,65 @@ -

- -

- # Colorus.js -[![NPM](https://img.shields.io/badge/NPM-%23CB3837.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=black&color=black)](https://www.npmjs.com/package/colorus-js) -[![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white&labelColor=black&color=black)](https://www.typescriptlang.org/) -[![GitHub stars](https://img.shields.io/github/stars/supitsdu/colorus-js?style=for-the-badge&logo=Github&logoColor=white&labelColor=black&color=black)](https://github.com/supitsdu/colorus-js) - -A versatile and powerful color manipulation library for JavaScript. +**Colorus.js** is a flexible color manipulation library with multi-format support and TypeScript compatibility. ## Features -- **Intuitive API:** Work with colors effortlessly using a simple and expressive function-based API. -- **Multiple Color Models:** Supports various color models, including RGB, HSL, HSV, and CMYK. -- **Flexible Input/Output:** Accepts color inputs in different formats (hex, rgb, hsl, etc.) and provides various output options. -- **Color Conversions:** Easily convert between different color models and formats. -- **Color Adjustments:** Perform common color adjustments like lightening, darkening, saturating, desaturating, and more. -- **Accessibility:** Calculate relative luminance and contrast ratios for improved accessibility. -- **Extensible:** Extend the core functionality with custom plugins. -- **TypeScript Support:** Provides full TypeScript support for enhanced type safety and developer experience. +- 🌈 **Model-Agnostic Design** – Supports HEX, RGB, HSL, HSV, CMYK, and is extendable to any color model. +- ⚑️ **Effortless Chaining** – Chain transformations with seamless TypeScript support for clarity and reliability. +- 🧩 **Extensible by Design** – Add custom parsers and plugins to unlock new models and functions. +- πŸ”’ **Solid Type Safety** – Robust TypeScript types deliver consistent, predictable color transformations. -### Usage +## Quick Start ```javascript import { dye } from "colorus-js" -const color = dye("#ff0000") // Create a color from a hex code +const color = dye("rgb(255 0 0)") + +console.log(color.hsl) // { h: 0, s: 100, l: 50, a: 1 } +console.log(color.luminance) // 0.21 +console.log(color.source.isValid) // true +console.log(color.source.model) // "rgb" +``` + +### Multi-Format Support + +```javascript +import { dye, hslParser } from "colorus-js" -console.log(color.rgb) // Output: { r: 255, g: 0, b: 0, a: 1 } -console.log(color.toHsl()) // Output: hsl(0, 100%, 50%) +const color = dye("hsl(120deg 50% 30%)", { parsers: [hslParser] }) -const lighterColor = color.lighten(0.2) // Lighten the color by 20% -console.log(lighterColor.toHex()) // Output: #ff6666 +console.log(color.luminance) // 0.13 +console.log(color.rgb) // { r: 38.25, g: 114.75, b: 38.25, a: 1 } ``` -### Plugins +**Built-in Parsers:** `cmykParser`, `hexParser`, `hslParser`, `hsvParser`, `rgbParser` (default) -Extend the `dye` function with custom methods using plugins. +### Custom Plugins ```javascript -const colorWithPlugin = dye("#0000ff", { - plugins: { - isBlue() { - return this.rgb.b > 200 - } - } +import { createPlugin, dye } from "colorus-js" + +// Custom grayscale plugin definition +const grayscale = createPlugin("grayscale", function () { + const avg = (this.rgb.r + this.rgb.g + this.rgb.b) / 3 + return dye({ r: avg, g: avg, b: avg, a: this.rgb.a }, this.options) }) -console.log(colorWithPlugin.isBlue()) // Output: true +// Usage +const color = dye("rgb(255, 0, 0)", { plugins: { grayscale } }) + +console.log(color.grayscale().rgb) // { r: 85, g: 85, b: 85, a: 1 } ``` -For more information see [Working with Plugins Guide](docs/WORKING_WITH_PLUGINS.md). +**Built-in Plugins:** `invert`, `lighten`, `darken`, `saturate`, `desaturate`, `toCmyk`, `toHex`, `toHsl`, `toHsv`, `toRgb` -## Contributing +## Further Reading + +- [API Reference](docs/API.md) +- [Working with Plugins](docs/guide/WORKING_WITH_PLUGINS.md) +- [Working with Parsers](docs/guide/WORKING_WITH_PARSERS.md) -Contributions are welcome! Please read the [Contributing Guide](CONTRIBUTING.md). +## Contributing -[**Leave a star and help spread the hues! 🎨⭐**](https://github.com/supitsdu/colorus-js) +Contributions are welcome! See the [Contributing Guide](CONTRIBUTING.md). diff --git a/biome.json b/biome.json index 288e26d..f95adc3 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,15 @@ { "files": { - "ignore": ["node_modules", "coverage", "docs", ".github", "_test"], - "include": ["src/*.ts", "test/*.ts"] + "ignore": [ + "node_modules", + "coverage", + "docs", + ".github", + "_test", + "build", + "dist" + ], + "include": ["src/*.ts", "test/*.ts", "rollup.config.js", "jest.config.js"] }, "organizeImports": { "enabled": true }, "formatter": { @@ -31,18 +39,25 @@ "enabled": true, "rules": { "complexity": { - "noStaticOnlyClass": "warn" + "noStaticOnlyClass": "warn", + "noForEach": "off" }, "suspicious": { "noDebugger": "off", - "noConsoleLog": "info", + "noConsoleLog": "warn", "noExplicitAny": "off", "noConfusingVoidType": "off" }, "style": { "noShoutyConstants": "warn", "useNamingConvention": "error", - "noNonNullAssertion": "off" + "noNonNullAssertion": "off", + "noInferrableTypes": "off", + "useNumberNamespace": "off" + }, + "correctness": { + "noUnusedVariables": "warn", + "noUnusedImports": "warn" } } } diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..f352ce6 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,68 @@ +# API Reference + +Detailed information about the core functionalities, methods, and types available in the library. + +## Core Method + +### `dye(input: Colors.Input, options?: Dye.Options): Dye.Instance` + +The primary function for creating a color instance. + +**Parameters:** + +- `input`: A color input, which can be either a color string (e.g., `"#FF5733"`, `"rgb(255, 0, 0)"`, `"hsl(120, 100%, 50%)"`, etc.) or an object representing RGB values. +- `options`: An optional configuration object to customize parsing and plugin behavior. + - `plugins`: An object containing custom plugins to extend functionality. + - `parsers`: An array of `ColorParser` instances to use for the input. + - `formatOptions`: Options for output formatting. + +**Returns:** An instance of `Dye.Instance` with methods for color manipulation. + +**Example:** + +```javascript +import { dye } from "colorus-js" + +const color = dye("rgb(255, 0, 0)") +console.log(color.hsl) // { h: 0, s: 100, l: 50, a: 1 } +``` + +## Color Types + +Colorus.js supports various color representations defined as follows: + +- **`Colors.Rgb`**: Represents colors in RGB format as `{ r: number, g: number, b: number, a: number }`. +- **`Colors.Hsl`**: Represents colors in HSL format as `{ h: number, s: number, l: number, a: number }`. +- **`Colors.Hsv`**: Represents colors in HSV format as `{ h: number, s: number, v: number, a: number }`. +- **`Colors.Cmyk`**: Represents colors in CMYK format as `{ c: number, m: number, y: number, k: number, a: number }`. + +All these formats maintain an alpha channel for transparency, represented as a number (0 to 1). + +## Color Properties + +Each `Dye.Instance` provides access to various color properties calculated based on the initial input: + +- `rgb`: Returns the RGB representation. +- `hsl`: Returns the HSL representation. +- `hsv`: Returns the HSV representation. +- `cmyk`: Returns the CMYK representation. +- `luminance`: Calculates and returns the luminance value. +- `alpha`: Returns the alpha (transparency) value. +- `hue`: Returns the hue value. +- `source`: Metadata and validity. +- `error`: Returns the error message, if any. + +**Example:** + +```javascript +const color = dye("hsl(120, 100%, 50%)") +console.log(color.rgb) // { r: 0, g: 255, b: 0, a: 1 } +``` + +## Built-in Parsers + +For detailed information about the built-in parsers available in Colorus.js, please refer to the [Working with Parsers](/docs/guide/WORKING_WITH_PARSERS.md). + +## Built-in Plugins + +For detailed information about the built-in plugins available in Colorus.js, please refer to the [Working with Plugins](/docs/guide/WORKING_WITH_PLUGINS.md). diff --git a/docs/WORKING_WITH_PLUGINS.md b/docs/WORKING_WITH_PLUGINS.md deleted file mode 100644 index 5dfd870..0000000 --- a/docs/WORKING_WITH_PLUGINS.md +++ /dev/null @@ -1,72 +0,0 @@ -# Working with Plugins - -**Colorus.js** offers a powerful plugin system to extend the capabilities of the `dye()` function, allowing you to add custom methods and functionalities to color objects. This guide will walk you through the process of creating, using, and understanding plugins in Colorus.js. - -## What are Plugins? πŸ€” - -Plugins are essentially functions that you can attach to color objects created by the `dye()` function. These functions can access the color's properties and methods, enabling you to perform custom calculations, transformations, or any other color-related operations. 🎨 - -### Defining a Plugin 🧩 - -A plugin is defined as a function that takes the color object as its `this` context and any additional arguments you might need - -```typescript -import type { Dye } from "colorus-js" - -export function myPlugin(this: Dye, ...args: any[]) { - // Access color properties and methods using 'this' keyword - console.debug(this.rgb) - console.debug(this.toHex()) - - // Perform custom logic or calculations... - - return someValue // Optionally return a value -} -``` - -### Using Plugins with `dye()` πŸ§ͺ - -You can attach plugins to a color object by passing them in the `options.plugins` object when calling `dye()`. - -```typescript -import { dye } from "colorus-js" -import { myPlugin } from "./myPlugin.ts" - -const color = dye("#ff0000", { - plugins: { - myPlugin, - isRed() { - return this.rgb.r > 200 && this.rgb.g < 50 && this.rgb.b < 50 - } - } -}) - -console.debug(color.myPlugin()) -console.debug(color.isRed()) -``` - -### Plugin Chaining ⛓️ - -One of the powerful features of Colorus.js plugins is the ability to chain them with the core color manipulation methods. - -```typescript -const lighterColor = color.lighten(0.2) -console.debug(lighterColor.getHue()) -``` - -### Type Safety with Plugins βœ… - -Colorus.js leverages TypeScript to provide strong type safety when working with plugins. - -- The `this` context within plugin methods is automatically typed as a `DyeReturns` object, giving you access to all the core color properties and methods through autocompletion and type checking. πŸ‘ -- If your plugin returns a new color object, TypeScript will infer the correct type for the chained calls, ensuring that any subsequent plugin methods are also available. πŸ”„ - -### Important Considerations ⚠️ - -- **Plugin Naming:** Choose descriptive and unique names for your plugin methods to avoid conflicts with core methods or other plugins. πŸ“› -- **Side Effects:** Be mindful of side effects within your plugins. Avoid modifying the original color object directly unless that's the intended behavior. πŸ§ͺ -- **Performance:** If your plugin performs complex calculations, consider optimizing it for performance, especially if it will be used in computationally intensive scenarios. ⚑ - -### Conclusion - -The plugin system in Colorus.js opens up a world of possibilities for extending and customizing the library to fit your specific needs. We encourage you to explore this feature and create plugins that enhance your color manipulation workflows! πŸš€ diff --git a/docs/guide/WORKING_WITH_PARSERS.md b/docs/guide/WORKING_WITH_PARSERS.md new file mode 100644 index 0000000..48a6545 --- /dev/null +++ b/docs/guide/WORKING_WITH_PARSERS.md @@ -0,0 +1,92 @@ +# Working with Parsers + +Extend **Colorus.js** with custom parsers to convert diverse color formats into a `Dye.Instance`. + +## What are Parsers? + +Parsers transform color formats (like HSL and HEX) into `Dye.ParserMatchArray`, providing: + +- **Original Input**: The initial color value. +- **Parsed Color**: The standardized color model. +- **Metadata**: Model details, color values, and validation status. + +## HSL Parser + +Processes HSL color values to enable transformations within `dye()`, with built-in clamping for valid HSL ranges. + +### HSL Parser - Configuration + +- **Model**: `"hsl"` +- **Regex**: Pattern to match HSL strings. +- **Extract**: Converts matches to `{ h, s, l, a }`. +- **Serialize**: Converts HSL to RGB, ensuring valid output. +- **Clamp**: Confirms HSL value limits. +- **Channels**: Specifies `["h", "s", "l", "a"]` as color properties. + +```typescript +export const hslParser = new ColorParser({ + model: "hsl", + extract: match => ({ + h: match[0], + s: match[1], + l: match[2], + a: match[3] + }), + serialize: hslToRgb, + clamp: clampHsl, + regex: regexHsl, + channels: chHsl +}) +``` + +### HSL Parser - Example Usage + +```typescript +const color = dye("hsl(120, 100%, 50%)", { parsers: [hslParser] }) + +console.log(color.rgb) // { "r": 0, "g": 255, "b": 0, "a": 1 } +console.log(color.source.model) // "hsl" +console.log(color.source.isValid) // true +``` + +## HEX Parser + +A HEX parser for colors like `#FF5733`, converting them into RGB. + +### HEX Parser - Configuration + +- **Model**: `"hex"` +- **Regex**: Pattern to match HEX values. +- **Extract**: Retrieves the hex string. +- **Serialize**: Converts HEX to RGB, ensuring valid output. + +```typescript +export const hexParser = new ColorParser({ + model: "hex", + extract: match => match[0] as string, + serialize: convertHexToRgb, + regex: regexHex +}) +``` + +### HEX Parser - Example Usage + +```typescript +const color = dye("#FF5733", { parsers: [hexParser] }) + +console.log(color.rgb) // { "r": 255, "g": 87, "b": 51, "a": 1 } +console.log(color.source.model) // "hex" +console.log(color.source.isValid) // true +``` + +## Key Tips for Parsers + +- **Extract Function**: Focus on obtaining essential color values. +- **Regex Capturing**: Ensure the regex captures only necessary channels, e.g., for HSL: + +```jsonc +// regexHsl.match("hsl(240, 70, 50)") +["hsl(240, 70, 50)", "240", "70", "50", null] +``` + +- **Serializer Function**: Always return a valid RGB object to maintain consistent conversion. diff --git a/docs/guide/WORKING_WITH_PLUGINS.md b/docs/guide/WORKING_WITH_PLUGINS.md new file mode 100644 index 0000000..7b2142f --- /dev/null +++ b/docs/guide/WORKING_WITH_PLUGINS.md @@ -0,0 +1,53 @@ +# Working with Plugins + +Extend **Colorus.js** with plugins to create custom color transformations and calculations. + +## Overview + +Plugins attach directly to any `Dye.Instance`, allowing flexible extension of core functionality. + +## Creating a Plugin + +Define a new plugin with `createPlugin`: + +```typescript +export const myPlugin = createPlugin("myPlugin", function () { + return `The Red value is ${this.rgb.r}` +}) +``` + +## Adding Plugins to `dye()` + +Pass plugins to `dye` to make them accessible in the color instance: + +```typescript +const color = dye("rgb(255 0 0)", { + plugins: { + myPlugin, + isRed(): boolean { + return this.rgb.r > 200 && this.rgb.g < 50 && this.rgb.b < 50 + } + } +}) + +color.myPlugin() // "The Red value is 255" +color.isRed() // true +``` + +## Chaining Plugins + +Combine plugins and methods seamlessly: + +```typescript +color.lighten(0.2).desaturate(0.7).anotherCustomPlugin().rgb // Fully typed +``` + +## Type Safety + +Colorus.js infers `Dye.Instance` type for `this` within plugins, enabling type-safe access to instance properties and methods. + +## Plugin Guidelines + +- **Unique Names**: Avoid conflicts by ensuring plugin names don’t overlap with built-in methods. +- **Side Effect-Free**: Plugins should not alter the `Dye.Instance` state directly. +- **Performance**: Optimize for intensive operations to maintain efficiency. diff --git a/jest.config.js b/jest.config.js index 1786558..389c546 100644 --- a/jest.config.js +++ b/jest.config.js @@ -14,13 +14,18 @@ export default { // Indicates whether the coverage information should be collected while executing the test collectCoverage: true, + collectCoverageFrom: [ + "src/**/*.ts", // Include all .js files in the src folder + "!src/types/*.ts", // Exclude types files + "!test/**/*.ts", // Exclude test files + ], // The directory where Jest should output its coverage files coverageDirectory: "coverage", // Indicates which provider should be used to instrument code for coverage coverageProvider: "v8", - coverageReporters: ["html-spa"], + coverageReporters: ["html-spa", "lcov"], // An array of file extensions your modules use moduleFileExtensions: ["js", "ts"], diff --git a/package-lock.json b/package-lock.json index 3a8db1e..9a9bf80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,14 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "@biomejs/biome": "1.9.3", + "@biomejs/biome": "1.9.4", "@rollup/plugin-typescript": "^12.1.0", "@swc/core": "^1.7.22", "@swc/jest": "^0.2.36", "@types/jest": "^29.5.12", "jest": "^29.7.0", "prettier": "^3.3.3", - "rollup": "4.24.0", + "rollup": "4.24.2", "rollup-plugin-bundle-size": "^1.0.3", "rollup-plugin-dts": "^6.1.1", "rollup-plugin-esbuild": "^6.1.1", @@ -632,9 +632,9 @@ "license": "MIT" }, "node_modules/@biomejs/biome": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.3.tgz", - "integrity": "sha512-POjAPz0APAmX33WOQFGQrwLvlu7WLV4CFJMlB12b6ZSg+2q6fYu9kZwLCOA+x83zXfcPd1RpuWOKJW0GbBwLIQ==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", + "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", "dev": true, "hasInstallScript": true, "bin": { @@ -648,20 +648,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "1.9.3", - "@biomejs/cli-darwin-x64": "1.9.3", - "@biomejs/cli-linux-arm64": "1.9.3", - "@biomejs/cli-linux-arm64-musl": "1.9.3", - "@biomejs/cli-linux-x64": "1.9.3", - "@biomejs/cli-linux-x64-musl": "1.9.3", - "@biomejs/cli-win32-arm64": "1.9.3", - "@biomejs/cli-win32-x64": "1.9.3" + "@biomejs/cli-darwin-arm64": "1.9.4", + "@biomejs/cli-darwin-x64": "1.9.4", + "@biomejs/cli-linux-arm64": "1.9.4", + "@biomejs/cli-linux-arm64-musl": "1.9.4", + "@biomejs/cli-linux-x64": "1.9.4", + "@biomejs/cli-linux-x64-musl": "1.9.4", + "@biomejs/cli-win32-arm64": "1.9.4", + "@biomejs/cli-win32-x64": "1.9.4" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.3.tgz", - "integrity": "sha512-QZzD2XrjJDUyIZK+aR2i5DDxCJfdwiYbUKu9GzkCUJpL78uSelAHAPy7m0GuPMVtF/Uo+OKv97W3P9nuWZangQ==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", + "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", "cpu": [ "arm64" ], @@ -675,9 +675,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.3.tgz", - "integrity": "sha512-vSCoIBJE0BN3SWDFuAY/tRavpUtNoqiceJ5PrU3xDfsLcm/U6N93JSM0M9OAiC/X7mPPfejtr6Yc9vSgWlEgVw==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", + "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", "cpu": [ "x64" ], @@ -691,9 +691,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.3.tgz", - "integrity": "sha512-vJkAimD2+sVviNTbaWOGqEBy31cW0ZB52KtpVIbkuma7PlfII3tsLhFa+cwbRAcRBkobBBhqZ06hXoZAN8NODQ==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", + "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", "cpu": [ "arm64" ], @@ -707,9 +707,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.3.tgz", - "integrity": "sha512-VBzyhaqqqwP3bAkkBrhVq50i3Uj9+RWuj+pYmXrMDgjS5+SKYGE56BwNw4l8hR3SmYbLSbEo15GcV043CDSk+Q==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", + "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", "cpu": [ "arm64" ], @@ -723,9 +723,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.3.tgz", - "integrity": "sha512-x220V4c+romd26Mu1ptU+EudMXVS4xmzKxPVb9mgnfYlN4Yx9vD5NZraSx/onJnd3Gh/y8iPUdU5CDZJKg9COA==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", + "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", "cpu": [ "x64" ], @@ -739,9 +739,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.3.tgz", - "integrity": "sha512-TJmnOG2+NOGM72mlczEsNki9UT+XAsMFAOo8J0me/N47EJ/vkLXxf481evfHLlxMejTY6IN8SdRSiPVLv6AHlA==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", + "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", "cpu": [ "x64" ], @@ -755,9 +755,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.3.tgz", - "integrity": "sha512-lg/yZis2HdQGsycUvHWSzo9kOvnGgvtrYRgoCEwPBwwAL8/6crOp3+f47tPwI/LI1dZrhSji7PNsGKGHbwyAhw==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", + "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", "cpu": [ "arm64" ], @@ -771,9 +771,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.3.tgz", - "integrity": "sha512-cQMy2zanBkVLpmmxXdK6YePzmZx0s5Z7KEnwmrW54rcXK3myCNbQa09SwGZ8i/8sLw0H9F3X7K4rxVNGU8/D4Q==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", + "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", "cpu": [ "x64" ], @@ -1653,9 +1653,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", - "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.2.tgz", + "integrity": "sha512-ufoveNTKDg9t/b7nqI3lwbCG/9IJMhADBNjjz/Jn6LxIZxD7T5L8l2uO/wD99945F1Oo8FvgbbZJRguyk/BdzA==", "cpu": [ "arm" ], @@ -1666,9 +1666,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", - "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.2.tgz", + "integrity": "sha512-iZoYCiJz3Uek4NI0J06/ZxUgwAfNzqltK0MptPDO4OR0a88R4h0DSELMsflS6ibMCJ4PnLvq8f7O1d7WexUvIA==", "cpu": [ "arm64" ], @@ -1679,9 +1679,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", - "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.2.tgz", + "integrity": "sha512-/UhrIxobHYCBfhi5paTkUDQ0w+jckjRZDZ1kcBL132WeHZQ6+S5v9jQPVGLVrLbNUebdIRpIt00lQ+4Z7ys4Rg==", "cpu": [ "arm64" ], @@ -1692,9 +1692,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", - "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.2.tgz", + "integrity": "sha512-1F/jrfhxJtWILusgx63WeTvGTwE4vmsT9+e/z7cZLKU8sBMddwqw3UV5ERfOV+H1FuRK3YREZ46J4Gy0aP3qDA==", "cpu": [ "x64" ], @@ -1704,10 +1704,36 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.2.tgz", + "integrity": "sha512-1YWOpFcGuC6iGAS4EI+o3BV2/6S0H+m9kFOIlyFtp4xIX5rjSnL3AwbTBxROX0c8yWtiWM7ZI6mEPTI7VkSpZw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.2.tgz", + "integrity": "sha512-3qAqTewYrCdnOD9Gl9yvPoAoFAVmPJsBvleabvx4bnu1Kt6DrB2OALeRVag7BdWGWLhP1yooeMLEi6r2nYSOjg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", - "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.2.tgz", + "integrity": "sha512-ArdGtPHjLqWkqQuoVQ6a5UC5ebdX8INPuJuJNWRe0RGa/YNhVvxeWmCTFQ7LdmNCSUzVZzxAvUznKaYx645Rig==", "cpu": [ "arm" ], @@ -1718,9 +1744,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", - "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.2.tgz", + "integrity": "sha512-B6UHHeNnnih8xH6wRKB0mOcJGvjZTww1FV59HqJoTJ5da9LCG6R4SEBt6uPqzlawv1LoEXSS0d4fBlHNWl6iYw==", "cpu": [ "arm" ], @@ -1731,9 +1757,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", - "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.2.tgz", + "integrity": "sha512-kr3gqzczJjSAncwOS6i7fpb4dlqcvLidqrX5hpGBIM1wtt0QEVtf4wFaAwVv8QygFU8iWUMYEoJZWuWxyua4GQ==", "cpu": [ "arm64" ], @@ -1744,9 +1770,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", - "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.2.tgz", + "integrity": "sha512-TDdHLKCWgPuq9vQcmyLrhg/bgbOvIQ8rtWQK7MRxJ9nvaxKx38NvY7/Lo6cYuEnNHqf6rMqnivOIPIQt6H2AoA==", "cpu": [ "arm64" ], @@ -1757,9 +1783,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", - "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.2.tgz", + "integrity": "sha512-xv9vS648T3X4AxFFZGWeB5Dou8ilsv4VVqJ0+loOIgDO20zIhYfDLkk5xoQiej2RiSQkld9ijF/fhLeonrz2mw==", "cpu": [ "ppc64" ], @@ -1770,9 +1796,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", - "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.2.tgz", + "integrity": "sha512-tbtXwnofRoTt223WUZYiUnbxhGAOVul/3StZ947U4A5NNjnQJV5irKMm76G0LGItWs6y+SCjUn/Q0WaMLkEskg==", "cpu": [ "riscv64" ], @@ -1783,9 +1809,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", - "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.2.tgz", + "integrity": "sha512-gc97UebApwdsSNT3q79glOSPdfwgwj5ELuiyuiMY3pEWMxeVqLGKfpDFoum4ujivzxn6veUPzkGuSYoh5deQ2Q==", "cpu": [ "s390x" ], @@ -1796,9 +1822,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", - "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.2.tgz", + "integrity": "sha512-jOG/0nXb3z+EM6SioY8RofqqmZ+9NKYvJ6QQaa9Mvd3RQxlH68/jcB/lpyVt4lCiqr04IyaC34NzhUqcXbB5FQ==", "cpu": [ "x64" ], @@ -1809,9 +1835,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", - "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.2.tgz", + "integrity": "sha512-XAo7cJec80NWx9LlZFEJQxqKOMz/lX3geWs2iNT5CHIERLFfd90f3RYLLjiCBm1IMaQ4VOX/lTC9lWfzzQm14Q==", "cpu": [ "x64" ], @@ -1822,9 +1848,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", - "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.2.tgz", + "integrity": "sha512-A+JAs4+EhsTjnPQvo9XY/DC0ztaws3vfqzrMNMKlwQXuniBKOIIvAAI8M0fBYiTCxQnElYu7mLk7JrhlQ+HeOw==", "cpu": [ "arm64" ], @@ -1835,9 +1861,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", - "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.2.tgz", + "integrity": "sha512-ZhcrakbqA1SCiJRMKSU64AZcYzlZ/9M5LaYil9QWxx9vLnkQ9Vnkve17Qn4SjlipqIIBFKjBES6Zxhnvh0EAEw==", "cpu": [ "ia32" ], @@ -1848,9 +1874,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", - "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.2.tgz", + "integrity": "sha512-2mLH46K1u3r6uwc95hU+OR9q/ggYMpnS7pSp83Ece1HUQgF9Nh/QwTK5rcgbFnV9j+08yBrU5sA/P0RK2MSBNA==", "cpu": [ "x64" ], @@ -1888,9 +1914,9 @@ } }, "node_modules/@swc/core": { - "version": "1.7.35", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.35.tgz", - "integrity": "sha512-3cUteCTbr2r5jqfgx0r091sfq5Mgh6F1SQh8XAOnSvtKzwv2bC31mvBHVAieD1uPa2kHJhLav20DQgXOhpEitw==", + "version": "1.7.40", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.40.tgz", + "integrity": "sha512-0HIzM5vigVT5IvNum+pPuST9p8xFhN6mhdIKju7qYYeNuZG78lwms/2d8WgjTJJlzp6JlPguXGrMMNzjQw0qNg==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -1905,16 +1931,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.7.35", - "@swc/core-darwin-x64": "1.7.35", - "@swc/core-linux-arm-gnueabihf": "1.7.35", - "@swc/core-linux-arm64-gnu": "1.7.35", - "@swc/core-linux-arm64-musl": "1.7.35", - "@swc/core-linux-x64-gnu": "1.7.35", - "@swc/core-linux-x64-musl": "1.7.35", - "@swc/core-win32-arm64-msvc": "1.7.35", - "@swc/core-win32-ia32-msvc": "1.7.35", - "@swc/core-win32-x64-msvc": "1.7.35" + "@swc/core-darwin-arm64": "1.7.40", + "@swc/core-darwin-x64": "1.7.40", + "@swc/core-linux-arm-gnueabihf": "1.7.40", + "@swc/core-linux-arm64-gnu": "1.7.40", + "@swc/core-linux-arm64-musl": "1.7.40", + "@swc/core-linux-x64-gnu": "1.7.40", + "@swc/core-linux-x64-musl": "1.7.40", + "@swc/core-win32-arm64-msvc": "1.7.40", + "@swc/core-win32-ia32-msvc": "1.7.40", + "@swc/core-win32-x64-msvc": "1.7.40" }, "peerDependencies": { "@swc/helpers": "*" @@ -1926,9 +1952,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.7.35", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.35.tgz", - "integrity": "sha512-BQSSozVxjxS+SVQz6e3GC/+OBWGIK3jfe52pWdANmycdjF3ch7lrCKTHTU7eHwyoJ96mofszPf5AsiVJF34Fwg==", + "version": "1.7.40", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.40.tgz", + "integrity": "sha512-LRRrCiRJLb1kpQtxMNNsr5W82Inr0dy5Imho+4HQzVx/Ismi0qX4hQBgzJAnyOBNLK1+OBVb/912UVhKXppdfQ==", "cpu": [ "arm64" ], @@ -1942,9 +1968,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.7.35", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.35.tgz", - "integrity": "sha512-44TYdKN/EWtkU88foXR7IGki9JzhEJzaFOoPevfi9Xe7hjAD/x2+AJOWWqQNzDPMz9+QewLdUVLyR6s5okRgtg==", + "version": "1.7.40", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.40.tgz", + "integrity": "sha512-Lpl0XK/4fLzS5jsK48opUuGXrqJXwqJckYYPwyGbCfCXm4MsBe+7dX2hq/Kc4YMY25+NeTmzAXhla8TT4WYD/g==", "cpu": [ "x64" ], @@ -1958,9 +1984,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.7.35", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.35.tgz", - "integrity": "sha512-ccfA5h3zxwioD+/z/AmYtkwtKz9m4rWTV7RoHq6Jfsb0cXHrd6tbcvgqRWXra1kASlE+cDWsMtEZygs9dJRtUQ==", + "version": "1.7.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.40.tgz", + "integrity": "sha512-4bEvvjptpoc5BRPr/R419h6fXTEuub+frpxxlxBOEKxgXjAF/S3xdxyPijUAakmW/xXBF0u7OC4KYI+38yQp6g==", "cpu": [ "arm" ], @@ -1974,9 +2000,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.7.35", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.35.tgz", - "integrity": "sha512-hx65Qz+G4iG/IVtxJKewC5SJdki8PAPFGl6gC/57Jb0+jA4BIoGLD/J3Q3rCPeoHfdqpkCYpahtyUq8CKx41Jg==", + "version": "1.7.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.40.tgz", + "integrity": "sha512-v2fBlHJ/6Ovz0L2xFAI9TRiKyl9DTdx139PuAHD9gyzp16Utl/W0MPd4t2cYdkI6hPXE9PsJCSzMOrduh+YoDg==", "cpu": [ "arm64" ], @@ -1990,9 +2016,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.7.35", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.35.tgz", - "integrity": "sha512-kL6tQL9No7UEoEvDRuPxzPTpxrvbwYteNRbdChSSP74j13/55G2/2hLmult5yFFaWuyoyU/2lvzjRL/i8OLZxg==", + "version": "1.7.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.40.tgz", + "integrity": "sha512-uMkduQuU4LFVkW6txv8AVArT8GjJVJ5IHoWloXaUBMT447iE8NALmpePdZWhMyj6KV7j0y23CM5rzV/I2eNGLg==", "cpu": [ "arm64" ], @@ -2006,9 +2032,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.7.35", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.35.tgz", - "integrity": "sha512-Ke4rcLQSwCQ2LHdJX1FtnqmYNQ3IX6BddKlUtS7mcK13IHkQzZWp0Dcu6MgNA3twzb/dBpKX5GLy07XdGgfmyw==", + "version": "1.7.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.40.tgz", + "integrity": "sha512-4LZdY1MBSnXyTpW5fpBU/+JGAhkuHT+VnFTDNegRboN5nSPh7y0Yvn4LmIioESV+sWzjKkEXujJPGjrp+oSp5w==", "cpu": [ "x64" ], @@ -2022,9 +2048,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.7.35", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.35.tgz", - "integrity": "sha512-T30tlLnz0kYyDFyO5RQF5EQ4ENjW9+b56hEGgFUYmfhFhGA4E4V67iEx7KIG4u0whdPG7oy3qjyyIeTb7nElEw==", + "version": "1.7.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.40.tgz", + "integrity": "sha512-FPjOwT3SgI6PAwH1O8bhOGBPzuvzOlzKeCtxLaCjruHJu9V8KKBrMTWOZT/FJyYC9mX5Ip1+l9j30UqUZdQxtA==", "cpu": [ "x64" ], @@ -2038,9 +2064,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.7.35", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.35.tgz", - "integrity": "sha512-CfM/k8mvtuMyX+okRhemfLt784PLS0KF7Q9djA8/Dtavk0L5Ghnq+XsGltO3d8B8+XZ7YOITsB14CrjehzeHsg==", + "version": "1.7.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.40.tgz", + "integrity": "sha512-//ovXdD9GsTmhPmXJlXnIbRQkeuL6PSrYSr7uCMNcclrUdJG0YkO0GMM2afUKYbdJcunylDDWsSS8PFWn0QxmA==", "cpu": [ "arm64" ], @@ -2054,9 +2080,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.7.35", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.35.tgz", - "integrity": "sha512-ATB3uuH8j/RmS64EXQZJSbo2WXfRNpTnQszHME/sGaexsuxeijrp3DTYSFAA3R2Bu6HbIIX6jempe1Au8I3j+A==", + "version": "1.7.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.40.tgz", + "integrity": "sha512-iD/1auVhHGlhWAPrWmfRWL3w4AvXIWGVXZiSA109/xnRIPiHKb/HqqTp/qB94E/ZHMPRgLKkLTNwamlkueUs8g==", "cpu": [ "ia32" ], @@ -2070,9 +2096,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.7.35", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.35.tgz", - "integrity": "sha512-iDGfQO1571NqWUXtLYDhwIELA/wadH42ioGn+J9R336nWx40YICzy9UQyslWRhqzhQ5kT+QXAW/MoCWc058N6Q==", + "version": "1.7.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.40.tgz", + "integrity": "sha512-ZlFAV1WFPhhWQ/8esiygmetkb905XIcMMtHRRG0FBGCllO+HVL5nikUaLDgTClz1onmEY9sMXUFQeoPtvliV+w==", "cpu": [ "x64" ], @@ -4736,9 +4762,9 @@ } }, "node_modules/rollup": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", - "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.2.tgz", + "integrity": "sha512-do/DFGq5g6rdDhdpPq5qb2ecoczeK6y+2UAjdJ5trjQJj5f1AiVdLRWRc9A9/fFukfvJRgM0UXzxBIYMovm5ww==", "dev": true, "dependencies": { "@types/estree": "1.0.6" @@ -4751,22 +4777,24 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.24.0", - "@rollup/rollup-android-arm64": "4.24.0", - "@rollup/rollup-darwin-arm64": "4.24.0", - "@rollup/rollup-darwin-x64": "4.24.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", - "@rollup/rollup-linux-arm-musleabihf": "4.24.0", - "@rollup/rollup-linux-arm64-gnu": "4.24.0", - "@rollup/rollup-linux-arm64-musl": "4.24.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", - "@rollup/rollup-linux-riscv64-gnu": "4.24.0", - "@rollup/rollup-linux-s390x-gnu": "4.24.0", - "@rollup/rollup-linux-x64-gnu": "4.24.0", - "@rollup/rollup-linux-x64-musl": "4.24.0", - "@rollup/rollup-win32-arm64-msvc": "4.24.0", - "@rollup/rollup-win32-ia32-msvc": "4.24.0", - "@rollup/rollup-win32-x64-msvc": "4.24.0", + "@rollup/rollup-android-arm-eabi": "4.24.2", + "@rollup/rollup-android-arm64": "4.24.2", + "@rollup/rollup-darwin-arm64": "4.24.2", + "@rollup/rollup-darwin-x64": "4.24.2", + "@rollup/rollup-freebsd-arm64": "4.24.2", + "@rollup/rollup-freebsd-x64": "4.24.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.2", + "@rollup/rollup-linux-arm-musleabihf": "4.24.2", + "@rollup/rollup-linux-arm64-gnu": "4.24.2", + "@rollup/rollup-linux-arm64-musl": "4.24.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.2", + "@rollup/rollup-linux-riscv64-gnu": "4.24.2", + "@rollup/rollup-linux-s390x-gnu": "4.24.2", + "@rollup/rollup-linux-x64-gnu": "4.24.2", + "@rollup/rollup-linux-x64-musl": "4.24.2", + "@rollup/rollup-win32-arm64-msvc": "4.24.2", + "@rollup/rollup-win32-ia32-msvc": "4.24.2", + "@rollup/rollup-win32-x64-msvc": "4.24.2", "fsevents": "~2.3.2" } }, diff --git a/package.json b/package.json index e778b7f..2457624 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "colorus-js", - "version": "1.0.0", + "version": "2.0.0", "description": "Extend, enhance, create! 🎨 Colorus.js: Your extensible, type-safe color library.", "type": "module", "sideEffects": false, @@ -43,18 +43,20 @@ "hex", "rgb", "hsl", + "cmyk", + "hsv", "convert", "adjust" ], "devDependencies": { - "@biomejs/biome": "1.9.3", + "@biomejs/biome": "1.9.4", "@rollup/plugin-typescript": "^12.1.0", "@swc/core": "^1.7.22", "@swc/jest": "^0.2.36", "@types/jest": "^29.5.12", "jest": "^29.7.0", "prettier": "^3.3.3", - "rollup": "4.24.0", + "rollup": "4.24.2", "rollup-plugin-bundle-size": "^1.0.3", "rollup-plugin-dts": "^6.1.1", "rollup-plugin-esbuild": "^6.1.1", diff --git a/rollup.config.js b/rollup.config.js index 4862f8f..64d8388 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -6,10 +6,10 @@ // // The `tsconfig.json` file is used for TypeScript compilation settings, including the output location for declaration files. -import esbuild from "rollup-plugin-esbuild"; import typescript from "@rollup/plugin-typescript"; -import dts from "rollup-plugin-dts"; import bundleSize from "rollup-plugin-bundle-size"; +import dts from "rollup-plugin-dts"; +import esbuild from "rollup-plugin-esbuild"; export default [ // Phase 1: Transpile TypeScript to JavaScript and generate declaration files (`.d.ts`) @@ -55,7 +55,7 @@ export default [ // Phase 3: Bundle TypeScript declaration files into a single file { - input: "build/@types/main.d.ts", // Take the output from phase 1 as TypeScript declarations, as configured in tsconfig.json ("declarationDir") + input: "build/@types/src/main.d.ts", // Take the output from phase 1 as TypeScript declarations, as configured in tsconfig.json ("declarationDir") plugins: [ dts(), // Bundles .d.ts files. See: https://github.com/Swatinem/rollup-plugin-dts bundleSize(), diff --git a/src/constants/errorMessages.ts b/src/constants/errorMessages.ts deleted file mode 100644 index 30b5b8c..0000000 --- a/src/constants/errorMessages.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const errorMessages = { - invalidOptions: "Invalid options: Expected a plain object.", - invalidPlugins: - "Invalid plugins: Expected a plain object with method names as keys.", - invalidPlugin: (methodName: string) => - `Invalid plugin for '${methodName}': Expected a function.`, - invalidPluginOverwrite: (methodName: string) => - `Invalid plugin for '${methodName}': Overwritring an existing method is not allowed.`, - invalidColorString: (input: string) => `Invalid color string: ${input}`, - invalidColorObject: (input: unknown) => - `Invalid color object: ${JSON.stringify(input)}`, - invalidColorType: - "Invalid color type, expected a valid color string or object.", -}; diff --git a/src/constants/namedColors.ts b/src/constants/namedColors.ts deleted file mode 100644 index ab31b69..0000000 --- a/src/constants/namedColors.ts +++ /dev/null @@ -1,178 +0,0 @@ -import type { ExecMatchClone, NamedColorsParsers } from "../types"; - -export const namedColorsMap = { - aliceblue: "f0f8ff", - antiquewhite: "faebd7", - aqua: "00ffff", - aquamarine: "7fffd4", - azure: "f0ffff", - beige: "f5f5dc", - bisque: "ffe4c4", - black: "000000", - blanchedalmond: "ffebcd", - blue: "0000ff", - blueviolet: "8a2be2", - brown: "a52a2a", - burlywood: "deb887", - cadetblue: "5f9ea0", - chartreuse: "7fff00", - chocolate: "d2691e", - coral: "ff7f50", - cornflowerblue: "6495ed", - cornsilk: "fff8dc", - crimson: "dc143c", - cyan: "00ffff", - darkblue: "00008b", - darkcyan: "008b8b", - darkgoldenrod: "b8860b", - darkgray: "a9a9a9", - darkgreen: "006400", - darkgrey: "a9a9a9", - darkkhaki: "bdb76b", - darkmagenta: "8b008b", - darkolivegreen: "556b2f", - darkorange: "ff8c00", - darkorchid: "9932cc", - darkred: "8b0000", - darksalmon: "e9967a", - darkseagreen: "8fbc8f", - darkslateblue: "483d8b", - darkslategray: "2f4f4f", - darkslategrey: "2f4f4f", - darkturquoise: "00ced1", - darkviolet: "9400d3", - deeppink: "ff1493", - deepskyblue: "00bfff", - dimgray: "696969", - dimgrey: "696969", - dodgerblue: "1e90ff", - firebrick: "b22222", - floralwhite: "fffaf0", - forestgreen: "228b22", - fuchsia: "ff00ff", - gainsboro: "dcdcdc", - ghostwhite: "f8f8ff", - gold: "ffd700", - goldenrod: "daa520", - gray: "808080", - green: "008000", - greenyellow: "adff2f", - grey: "808080", - honeydew: "f0fff0", - hotpink: "ff69b4", - indianred: "cd5c5c", - indigo: "4b0082", - ivory: "fffff0", - khaki: "f0e68c", - lavender: "e6e6fa", - lavenderblush: "fff0f5", - lawngreen: "7cfc00", - lemonchiffon: "fffacd", - lightblue: "add8e6", - lightcoral: "f08080", - lightcyan: "e0ffff", - lightgoldenrodyellow: "fafad2", - lightgray: "d3d3d3", - lightgreen: "90ee90", - lightgrey: "d3d3d3", - lightpink: "ffb6c1", - lightsalmon: "ffa07a", - lightseagreen: "20b2aa", - lightskyblue: "87cefa", - lightslategray: "778899", - lightslategrey: "778899", - lightsteelblue: "b0c4de", - lightyellow: "ffffe0", - lime: "00ff00", - limegreen: "32cd32", - linen: "faf0e6", - magenta: "ff00ff", - maroon: "800000", - mediumaquamarine: "66cdaa", - mediumblue: "0000cd", - mediumorchid: "ba55d3", - mediumpurple: "9370db", - mediumseagreen: "3cb371", - mediumslateblue: "7b68ee", - mediumspringgreen: "00fa9a", - mediumturquoise: "48d1cc", - mediumvioletred: "c71585", - midnightblue: "191970", - mintcream: "f5fffa", - mistyrose: "ffe4e1", - moccasin: "ffe4b5", - navajowhite: "ffdead", - navy: "000080", - oldlace: "fdf5e6", - olive: "808000", - olivedrab: "6b8e23", - orange: "ffa500", - orangered: "ff4500", - orchid: "da70d6", - palegoldenrod: "eee8aa", - palegreen: "98fb98", - paleturquoise: "afeeee", - palevioletred: "db7093", - papayawhip: "ffefd5", - peachpuff: "ffdab9", - peru: "cd853f", - pink: "ffc0cb", - plum: "dda0dd", - powderblue: "b0e0e6", - purple: "800080", - rebeccapurple: "663399", - red: "ff0000", - rosybrown: "bc8f8f", - royalblue: "4169e1", - saddlebrown: "8b4513", - salmon: "fa8072", - sandybrown: "f4a460", - seagreen: "2e8b57", - seashell: "fff5ee", - sienna: "a0522d", - silver: "c0c0c0", - skyblue: "87ceeb", - slateblue: "6a5acd", - slategray: "708090", - slategrey: "708090", - snow: "fffafa", - springgreen: "00ff7f", - steelblue: "4682b4", - tan: "d2b48c", - teal: "008080", - thistle: "d8bfd8", - tomato: "ff6347", - turquoise: "40e0d0", - violet: "ee82ee", - wheat: "f5deb3", - white: "ffffff", - whitesmoke: "f5f5f5", - yellow: "ffff00", - yellowgreen: "9acd32", -}; - -/** - * Formats a named color into its standardized CSS representation. - * - * @param name - The named color string (e.g., "Red", "Rebecca Purple", "slate-grey"). - * @returns The standardized CSS named color (e.g., "red", "rebeccapurple", "slategrey") or the original input if not found. - */ -const formatNamedColor = (name: string): string => - name.toLowerCase().replace(/[\s-]+/g, ""); - -const execMatch: ExecMatchClone = { - pattern: { - lastIndex: 0, - exec(input: string) { - const color = formatNamedColor(input); - if (!Object.hasOwn(namedColorsMap, color)) return null; - const match = namedColorsMap[color as keyof typeof namedColorsMap]; - return match !== undefined ? [match] : null; - }, - }, -}; - -export default { - colors: namedColorsMap, - ...execMatch, -} as NamedColorsParsers; diff --git a/src/conversions/cmykConversions.ts b/src/conversions/cmykConversions.ts deleted file mode 100644 index 130dc72..0000000 --- a/src/conversions/cmykConversions.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Clamp } from "../core/colorNormalizer"; -import type { Cmyk, Rgb } from "../types"; - -/** - * Converts an Cmyk color object into its Rgb representation. - * @param {object} cmyk - An Cmyk color object. - * @return {object} An Rgb color object representation. - */ -export function cmykToRgb({ c, m, y, k, a = 1 }: Cmyk): Rgb { - c /= 100; - m /= 100; - y /= 100; - k /= 100; - - const r = 255 * ((1 - c) * (1 - k)); - const g = 255 * ((1 - m) * (1 - k)); - const b = 255 * ((1 - y) * (1 - k)); - - return Clamp.rgb({ r, g, b, a }); -} diff --git a/src/conversions/hexConversions.ts b/src/conversions/hexConversions.ts deleted file mode 100644 index 448bd45..0000000 --- a/src/conversions/hexConversions.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Clamp } from "../core/colorNormalizer"; -import type { AnyObject, Rgb } from "../types"; - -/** - * Converts a HEX color into an Rgb color object representation. - * @param hex - A valid HEX color without the hashtag "#". The alpha channel is optional. - */ -export function hexToRgb(hex: string): Rgb { - const delta = Number.parseInt(hex, 16); - const value: AnyObject = {}; - - if (hex.length === 6) { - value.r = (delta >> 16) & 255; - value.g = (delta >> 8) & 255; - value.b = delta & 255; - value.a = 1; - } else { - value.r = (delta >> 24) & 255; - value.g = (delta >> 16) & 255; - value.b = (delta >> 8) & 255; - value.a = (delta & 255) / 255; - } - - return Clamp.rgb(value); -} diff --git a/src/conversions/hslConversions.ts b/src/conversions/hslConversions.ts deleted file mode 100644 index eda32fb..0000000 --- a/src/conversions/hslConversions.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Clamp } from "../core/colorNormalizer"; -import type { Hsl, Hsv, Rgb } from "../types"; -import { hsvToRgb } from "./hsvConversions"; - -/** - * Converts an Hsl color to its Hsv representation. - * @param {object} hsl - An Hsl color object. - * @return {object} - An Hsv color object representation. - */ -export function hslToHsv({ h, s, l, a = 1 }: Hsl): Hsv { - const deltaS = (s * (l < 50 ? l : 100 - l)) / 100; - const v = l + deltaS; - - const S = deltaS > 0 ? ((2 * deltaS) / (l + deltaS)) * 100 : 0; - - return Clamp.hsv({ h, s: S, v, a }); -} - -/** - * Converts Hsl color object into its Rgb representation using Hsv interconversion. - * @param {object} input - An Hsl color object. - * @return {object} An Rgb color object representation. - */ -export function hslToRgb({ h, s, l, a = 1 }: Hsl): Rgb { - return hsvToRgb(hslToHsv({ h, s, l, a })); -} diff --git a/src/conversions/hsvConversions.ts b/src/conversions/hsvConversions.ts deleted file mode 100644 index 4bbd00d..0000000 --- a/src/conversions/hsvConversions.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Clamp } from "../core/colorNormalizer"; -import type { Hsl, Hsv, Rgb } from "../types"; - -/** - * Converts Hsv color object into its Hsl representation using interconversion. - * @param hsv - An Hsv color object. - */ -export function hsvToHsl({ h, s, v, a = 1 }: Hsv): Hsl { - const deltaL = ((200 - s) * v) / 100; - - const l = deltaL / 2; - - if (deltaL > 0 && deltaL < 200) { - s = ((s * v) / 100 / (deltaL <= 100 ? deltaL : 200 - deltaL)) * 100; - } else { - s = 0; - } - - return Clamp.hsl({ h, s, l, a }); -} - -/** - * Converts an Hsv color object into its Rgb representation. - * @param hsv - An Hsv color object. - */ -export function hsvToRgb({ h, s, v, a = 1 }: Hsv): Rgb { - const hueRange = (h / 60) % 6; - const saturation = s / 100; - const value = v / 100; - - const chroma = value * saturation; - const secondLargestComponent = chroma * (1 - Math.abs((hueRange % 2) - 1)); - const minComponent = value - chroma; - - let red: number; - let green: number; - let blue: number; - - // Determine the Rgb components based on the hue range - if (0 <= hueRange && hueRange < 1) { - [red, green, blue] = [chroma, secondLargestComponent, 0]; - } else if (1 <= hueRange && hueRange < 2) { - [red, green, blue] = [secondLargestComponent, chroma, 0]; - } else if (2 <= hueRange && hueRange < 3) { - [red, green, blue] = [0, chroma, secondLargestComponent]; - } else if (3 <= hueRange && hueRange < 4) { - [red, green, blue] = [0, secondLargestComponent, chroma]; - } else if (4 <= hueRange && hueRange < 5) { - [red, green, blue] = [secondLargestComponent, 0, chroma]; - } else { - [red, green, blue] = [chroma, 0, secondLargestComponent]; - } - - return Clamp.rgb({ - r: (red + minComponent) * 255, - g: (green + minComponent) * 255, - b: (blue + minComponent) * 255, - a: a, - }); -} diff --git a/src/conversions/rgbConversions.ts b/src/conversions/rgbConversions.ts deleted file mode 100644 index b205672..0000000 --- a/src/conversions/rgbConversions.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { namedColorsMap } from "../constants/namedColors"; -import { Clamp, Round, eightBit } from "../core/colorNormalizer"; -import { - computeColorDistance, - computeHsvHue, - isRgbShortanable, -} from "../core/conversionHelpers"; -import { hexString } from "../helpers"; -import type { Cmyk, FormatOptions, Hsl, Hsv, Rgb } from "../types"; -import { hexToRgb } from "./hexConversions"; -import { hsvToHsl } from "./hsvConversions"; - -/** - * Converts Rgb values to the nearest CSS named color. - * @param color - The Rgb color object. - */ -export function rgbToNamedColor({ r, g, b }: Rgb): string { - let closestColor = "black"; - let shortestDistance = Number.POSITIVE_INFINITY; - - for (const [name, color] of Object.entries(namedColorsMap)) { - const distance = computeColorDistance({ r, g, b }, hexToRgb(color)); - - if (distance < shortestDistance) { - shortestDistance = distance; - closestColor = name; - } - } - - return closestColor; -} - -/** - * Converts Rgb color object to HEX color string. - * @param rgb an valid Rgb color object - * @param options options to customize output format and precision - * @param options.minify set `true` for minified hexadecimal notation. - */ -export function rgbToHex( - { r, g, b, a = 1 }: Rgb, - options?: FormatOptions, -): string { - const { minify } = { minify: false, ...options }; - - const { r: R, g: G, b: B, a: A } = Round.rgb({ r, g, b, a }); - - const alphaInEightBit = eightBit(A * 255); - - let value = hexString((R << 16) | (G << 8) | B, 6); - - if (alphaInEightBit < 255) { - const alphaHex = hexString(alphaInEightBit, 2); - value += alphaHex; - } - - if (minify && isRgbShortanable(R, G, B, alphaInEightBit)) { - value = value.replace(/(.)\1/g, "$1"); - } - - return `#${value}`; -} - -/** - * Converts an Rgb color object into its Hsv representation. - * @param {object} rgb - An Rgb color object. - * @return {object} - An Hsv color object representation. - */ -export function rgbToHsv({ r, g, b, a = 1 }: Rgb): Hsv { - const maxRgb = Math.max(r, g, b); - const minRgb = Math.min(r, g, b); - const segment = maxRgb - minRgb; - - const v = (maxRgb / 255) * 100; - const s = (maxRgb > 0 ? segment / maxRgb : 0) * 100; - const h = computeHsvHue({ r, g, b }, { segment, maxRgb, minRgb }); - - return Clamp.hsv({ h, s, v, a }); -} - -/** - * Converts an Rgb color object into its Cmyk representation. - * @param {object} rgb - An Rgb color object. - * @return {object} An Cmyk color object representation. - */ -export function rgbToCmyk({ r, g, b, a = 1 }: Rgb): Cmyk { - const R = r / 255; - const G = g / 255; - const B = b / 255; - - const k = 1 - Math.max(R, G, B); - const c = (1 - R - k) / (1 - k); - const m = (1 - G - k) / (1 - k); - const y = (1 - B - k) / (1 - k); - - return Clamp.cmyk({ c: c * 100, m: m * 100, y: y * 100, k: k * 100, a }); -} - -/** - * Converts Rgb color object into its Hsl representation using Hsv interconversion. - * @param {object} input - An Rgb color object. - * @return {object} An Hsl color object representation. - */ -export function rgbToHsl({ r, g, b, a = 1 }: Rgb): Hsl { - return hsvToHsl(rgbToHsv({ r, g, b, a })); -} diff --git a/src/core/accessibility.ts b/src/core/accessibility.ts deleted file mode 100644 index 3780935..0000000 --- a/src/core/accessibility.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Rgb } from "../types"; - -/** - * Calculate the relative luminance of an sRGB color. - * @param {object} color - An object containing the sRGB components of the color. - * @return The luminance value of the color. - */ -export const relativeLuminance = ({ r, g, b }: Rgb): number => { - const fn = (c: number) => { - const d = c / 255; - return d <= 0.03928 ? d / 12.92 : ((d + 0.055) / 1.055) ** 2.4; - }; - - return fn(r) * 0.2126 + fn(g) * 0.7152 + fn(b) * 0.0722; -}; - -/** - * Calculate the contrast ratio between two relative luminance values. - * @param L1 - The relative luminance of the lighter color. - * @param L2 - The relative luminance of the darker color. - * @return The contrast ratio between the two colors. - */ -export const calculateContrastRatio = (L1: number, L2: number): number => - (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05); - -/** - * Calculate the contrast ratio between a foreground color and its adjacent background. - * @param fg - The sRGB color values of the foreground. - * @param bg - The sRGB color values of the background. - * @return The contrast ratio between the two colors. - */ -export const contrastRatio = (fg: Rgb, bg: Rgb) => - calculateContrastRatio(relativeLuminance(fg), relativeLuminance(bg)); diff --git a/src/core/colorAdjustments.ts b/src/core/colorAdjustments.ts deleted file mode 100644 index e369e01..0000000 --- a/src/core/colorAdjustments.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { nan, precision } from "../helpers"; -import type { Hsl, Rgb } from "../types"; -import { Clamp } from "./colorNormalizer"; - -/** - * Modifies the given `value` by a certain `amount`. - * - * @param value - The value to modify. - * @param amount - The amount to modify the `value` by, a value between 0 and 1. - * @return The modified `value`. - */ -export const modBy = (value: number, amount: number): number => { - const a = Number(amount); - - if (nan(a)) return value; - - return precision((1 + a) * value); -}; - -/** - * Lightens an Hsl color by the specified amount. - * @param color - Hsl color object to lighten. - * @param amount - A value between 0 and 1. - * @return New Hsl color object. - */ -export const lighten = (color: Hsl, amount: number): Hsl => { - return Clamp.hsl({ ...color, l: modBy(color.l, amount) }); -}; - -/** - * Saturate an Hsl color by the specified amount. - * @param color - Hsl color object to lighten. - * @param amount - A value between 0 and 1. - * @return New Hsl color object. - */ -export const saturate = (color: Hsl, amount: number): Hsl => { - return Clamp.hsl({ ...color, s: modBy(color.s, amount) }); -}; - -/** - * Adjust the hue of a Hsl color. - * @param color - Hsl color object.. - * @param amount - A value between 0 and 1. - * @return New Hsl color object. - */ -export const hue = (color: Hsl, amount: number): Hsl => { - return Clamp.hsl({ ...color, h: modBy(color.h, amount) }); -}; - -/** - * Adjust the alpha channel of a Rgb color. - * @param color - Rgb color object.. - * @param amount - A value between 0 and 1. - * @return New Rgb color object. - */ -export const alpha = (color: Rgb, amount: number): Rgb => { - return Clamp.rgb({ ...color, a: modBy(color.a ?? 1, amount) }); // Handle optional alpha -}; diff --git a/src/core/colorFormatter.ts b/src/core/colorFormatter.ts deleted file mode 100644 index a49efa2..0000000 --- a/src/core/colorFormatter.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Round } from "./colorNormalizer"; - -import type { Cmyk, FormatOptions, Hsl, Hsv, Rgb } from "../types"; - -interface FormatPrefs { - spacer: string; - percent: string; - suffix: (a?: number) => string; - alpha: (a?: number) => string; -} - -const processFormatOptions = ({ - minify, - cssNext, -}: FormatOptions = {}): FormatPrefs => { - const space = !minify ? " " : ""; - const alphaSpacer = cssNext ? `${space}/${space}` : `,${space}`; - - return { - spacer: cssNext ? " " : `,${space}`, - percent: !minify ? "%" : "", - suffix: (a?: number) => (a === 1 || cssNext ? "" : "a"), - alpha: (a?: number) => (a === 1 ? "" : `${alphaSpacer}${a}`), - }; -}; - -export default { - /** - * Converts an RGB Object into its string representation. - * @param input An valid RGB object. - * @returns an color string supported by CSS and other styling tools. - */ - rgb(input: Rgb, options?: FormatOptions) { - const { r, g, b, a } = Round.rgb(input); - const { suffix, spacer, alpha } = processFormatOptions(options); - return `rgb${suffix(a)}(${r}${spacer}${g}${spacer}${b}${alpha(a)})`; - }, - - /** - * Converts an HSL Object into its string representation. - * @param input An valid HSL object. - * @returns a color string supported by CSS and other styling tools. - */ - hsl(input: Hsl, options?: FormatOptions): string { - const { h, s, l, a } = Round.hsl(input); - const { suffix, spacer, percent, alpha } = processFormatOptions(options); - return `hsl${suffix(a)}(${h}${spacer}${s}${percent}${spacer}${l}${percent}${alpha(a)})`; - }, - - /** - * Converts an HSV Object into its string representation. - * @param input An valid HSV object. - * @returns a color string supported by CSS and other styling tools. - */ - hsv(input: Hsv, options?: FormatOptions): string { - const { h, s, v, a } = Round.hsv(input); - const { suffix, spacer, percent, alpha } = processFormatOptions(options); - return `hsv${suffix(a)}(${h}${spacer}${s}${percent}${spacer}${v}${percent}${alpha(a)})`; - }, - - /** - * Converts an CMYK Object into its string representation. - * @param input An valid CMYK object. - * @returns a color string supported by CSS and other styling tools. - */ - cmyk(input: Cmyk, options?: FormatOptions): string { - const { c, m, k, y, a } = Round.cmyk(input); - const { suffix, spacer, percent, alpha } = processFormatOptions(options); - return `cmyk${suffix(a)}(${c}${percent}${spacer}${m}${percent}${spacer}${y}${percent}${spacer}${k}${percent}${alpha(a)})`; - }, -}; diff --git a/src/core/colorNormalizer.ts b/src/core/colorNormalizer.ts deleted file mode 100644 index 650bcb7..0000000 --- a/src/core/colorNormalizer.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { precision, utmost } from "../helpers"; -import type { ColorNormalizers } from "../types"; - -type Helper = ( - value: string | number, - fn?: (value: number) => number, -) => number; - -export const degree: Helper = (value, fn = Math.round): number => - fn(utmost(value, 360)) || 0; - -export const percent: Helper = (value, fn = Math.round): number => - fn(utmost(value, 100)) || 0; - -export const eightBit: Helper = (value, fn = Math.round): number => - fn(utmost(value, 255)) || 0; - -export const alpha: Helper = (value, fn = precision): number => - fn(utmost(value, 1)) || 0; - -export const Round: ColorNormalizers = { - fn: Math.round, - - rgb({ r, g, b, a = 1 }) { - return { - r: eightBit(r, this.fn), - g: eightBit(g, this.fn), - b: eightBit(b, this.fn), - a: alpha(a), - }; - }, - - hsl({ h, s, l, a = 1 }) { - return { - h: degree(h, this.fn), - s: percent(s, this.fn), - l: percent(l, this.fn), - a: alpha(a), - }; - }, - - hsv({ h, s, v, a = 1 }) { - return { - h: degree(h, this.fn), - s: percent(s, this.fn), - v: percent(v, this.fn), - a: alpha(a), - }; - }, - - cmyk({ c, m, y, k, a = 1 }) { - return { - c: percent(c, this.fn), - m: percent(m, this.fn), - y: percent(y, this.fn), - k: percent(k, this.fn), - a: alpha(a), - }; - }, -}; - -export const Clamp: ColorNormalizers = Object.assign(Object.create(Round), { - fn: precision, -}); diff --git a/src/core/colorParser.ts b/src/core/colorParser.ts deleted file mode 100644 index f81b6cc..0000000 --- a/src/core/colorParser.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { hexToRgb } from "../conversions/hexConversions"; -import { padString } from "../helpers"; -import { execColorStringTest } from "./colorTypeAnalyzer"; - -import type { AnyColorData, ColorParsers } from "../types"; - -export const colorParsers: ColorParsers = { - hex: match => hexToRgb(padString(match[1])), - rgb: match => ({ - r: Number(match[1]), - g: Number(match[2]), - b: Number(match[3]), - a: Number(match[4]) || 1, - }), - hsl: match => ({ - h: Number(match[1]), - s: Number(match[2]), - l: Number(match[3]), - a: Number(match[4]) || 1, - }), - hsv: match => ({ - h: Number(match[1]), - s: Number(match[2]), - v: Number(match[3]), - a: Number(match[4]) || 1, - }), - cmyk: match => ({ - c: Number(match[1]), - m: Number(match[2]), - y: Number(match[3]), - k: Number(match[4]), - a: Number(match[5]) || 1, - }), - named: (match: string[]) => hexToRgb(match[0]), -}; - -/** - * Parses a color string and converts it to a color object. - * ``` - * parseColor('hsl(360,0,100)') // Returns: { colorType: "hsl", colorObject: { h: 360, s: 0, l: 100, a: 1 } } - * ``` - * @param input - The input color string. - */ -export function parseColor(input?: string): AnyColorData | null { - const result = execColorStringTest(input); - return result != null - ? { - originalInput: input, - isValid: true, - value: colorParsers[result[0]](result[1]), - format: result[0], - } - : null; -} diff --git a/src/core/colorTypeAnalyzer.ts b/src/core/colorTypeAnalyzer.ts deleted file mode 100644 index a3925fb..0000000 --- a/src/core/colorTypeAnalyzer.ts +++ /dev/null @@ -1,106 +0,0 @@ -import namedColors from "../constants/namedColors"; -import { isString, nan } from "../helpers"; -import type { - AnyColorData, - AnyObject, - ColorPatterns, - SupportedColorFormat, -} from "../types"; - -export const isColorData = ( - input: Record, -): input is AnyColorData => - "format" in input || - "value" in input || - "isValid" in input || - "originalInput" in input; - -export const colorPatterns: ColorPatterns = [ - ["hex", /^#([a-f\d]{8}|[a-f\d]{6}|[a-f\d]{3,4})$/iy], - [ - "rgb", - /^rgba?\(\s*(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*(?:,|\/)\s*(0?\.\d+|1|0))?\s*\)$/iy, - ], - [ - "hsl", - /^hsla?\(\s*(\d{1,3})(?:deg|Β°)?(?:\s*,\s*|\s+)(\d{1,3})%?(?:\s*,\s*|\s+)(\d{1,3})%?(?:\s*(?:,|\/)\s*(0?\.\d+|1|0))?\s*\)$/iy, - ], - [ - "hsv", - /^hsva?\(\s*(\d{1,3})(?:deg|Β°)?(?:\s*,\s*|\s+)(\d{1,3})%?(?:\s*,\s*|\s+)(\d{1,3})%?(?:\s*(?:,|\/)\s*(0?\.\d+|1|0))?\s*\)$/iy, - ], - [ - "cmyk", - /^cmyka?\(\s*(\d{1,3})%?(?:\s*,\s*|\s+)(\d{1,3})%?(?:\s*,\s*|\s+)(\d{1,3})%?(?:\s*,\s*|\s+)(\d{1,3})%?(?:\s*(?:,|\/)\s*(0?\.\d+|1|0))?\s*\)$/iy, - ], - ["named", namedColors.pattern], -]; - -/** - * Performs a test on a color string. - * ``` - * execColorStringTest('hsl(360,0,100)') // Returns: "hsl" - * ``` - * @param input - The input color string. - */ -export function execColorStringTest( - input = "", -): [SupportedColorFormat, string[]] | null { - if (!isString(input) || !input) return null; - - for (const [name, pattern] of colorPatterns) { - pattern.lastIndex = 0; - const match = pattern.exec(input); - if (match !== null) return [name, match]; - } - - return null; -} - -/** - * Determine the color type based on the provided color object. - * @param colorObject - The color object to be analyzed. - * @return The determined color type ('rgb', 'hsl', 'hsv', 'cmyk'). - */ -export const determineColorType = (colorObject?: Record) => { - if (!colorObject) return undefined; - - if (isRgbObject(colorObject)) return "rgb"; - if (isHslObject(colorObject)) return "hsl"; - if (isHsvObject(colorObject)) return "hsv"; - if (isCmykObject(colorObject)) return "cmyk"; - - return undefined; -}; - -/** - * Checks if the provided object represents an Rgb color. - * @param input - The object to be checked. - * @return True if the object represents an Rgb color, false otherwise. - */ -export const isRgbObject = ({ r, g, b, a = 1 }: AnyObject): boolean => - !(nan(r) || nan(g) || nan(b) || nan(a)); - -/** - * Checks if the provided object represents an Hsl color. - * @param input - The object to be checked. - * @return True if the object represents an Hsl color, false otherwise. - */ -export const isHslObject = ({ h, s, l, a = 1 }: AnyObject): boolean => - !(nan(h) || nan(s) || nan(l) || nan(a)); - -/** - * Checks if the provided object represents an Hsv color. - * @param input - The object to be checked. - * @return True if the object represents an Hsv color, false otherwise. - */ -export const isHsvObject = ({ h, s, v, a = 1 }: AnyObject): boolean => - !(nan(h) || nan(s) || nan(v) || nan(a)); - -/** - * Checks if the provided object represents a Cmyk color. - * @param input - The object to be checked. - * @return True if the object represents a Cmyk color, false otherwise. - */ -export const isCmykObject = ({ c, m, y, k, a = 1 }: AnyObject): boolean => - !(nan(c) || nan(m) || nan(y) || nan(k) || nan(a)); diff --git a/src/core/conversionHelpers.ts b/src/core/conversionHelpers.ts deleted file mode 100644 index bdedcf7..0000000 --- a/src/core/conversionHelpers.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { AnyRgb } from "../types"; - -/** - * Check if an HEX color is shortanable by comparing the Rgb components. - * @param r the red channel component of Rgb color - * @param g the green channel component of Rgb color - * @param b the blue channel component of Rgb color - * @param a the alpha channel component of RGBA color - */ -export const isRgbShortanable = ( - r: number, - g: number, - b: number, - a = 255, -): boolean => !(r % 17 !== 0 || g % 17 !== 0 || b % 17 !== 0 || a % 17 !== 0); - -/** - * Calculates the Euclidean distance between two Rgb colors. - * @param primary - The first Rgb color string. - * @param secondary - The second Rgb color string. - */ -export const computeColorDistance = ( - { r: r1, g: g1, b: b1 }: AnyRgb, - { r: r2, g: g2, b: b2 }: AnyRgb, -): number => Math.sqrt((r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2); - -interface ComputeHsvHueParams { - segment: number; - maxRgb: number; - minRgb: number; -} - -/** - * Calculates the hue component of an Hsv color object. - * @param rgb - An Rgb color object. - * @param params - Parameters including segment, maxRgb, and minRgb. - */ -export function computeHsvHue( - { r, g, b }: AnyRgb, - { segment, maxRgb, minRgb }: ComputeHsvHueParams, -): number { - let h = 0; - - if (segment === h) return h; - - const delta = maxRgb - minRgb; - const rDelta = (r - minRgb) / delta; - const gDelta = (g - minRgb) / delta; - const bDelta = (b - minRgb) / delta; - - // Calculate hue based on which color component is the max - if (r === maxRgb) { - h = (60 * (gDelta - bDelta)) % 360; - } else if (g === maxRgb) { - h = 60 * (bDelta - rDelta) + 120; - } else { - h = 60 * (rDelta - gDelta) + 240; - } - - // Ensure hue is within [0, 360) - if (h < 0) { - h += 360; - } - - return h; -} diff --git a/src/core/inputSerializer.ts b/src/core/inputSerializer.ts deleted file mode 100644 index 00892c8..0000000 --- a/src/core/inputSerializer.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { errorMessages } from "../constants/errorMessages"; -import { cmykToRgb } from "../conversions/cmykConversions"; -import { hslToRgb } from "../conversions/hslConversions"; -import { hsvToRgb } from "../conversions/hsvConversions"; -import { isObject, isString, isUndefined } from "../helpers"; -import type { - AnyColorData, - ColorConverters, - ColorData, - ColorObject, -} from "../types"; -import { Clamp } from "./colorNormalizer"; -import { parseColor } from "./colorParser"; -import { determineColorType, isColorData } from "./colorTypeAnalyzer"; - -export const fallbackColor: ColorData = { - originalInput: undefined, - isValid: false, - value: { r: 0, g: 0, b: 0, a: 1 }, - format: undefined, -}; - -const converters: ColorConverters = { - rgb: input => Clamp.rgb(input), - hsl: input => hslToRgb(Clamp.hsl(input)), - hsv: input => hsvToRgb(Clamp.hsv(input)), - cmyk: input => cmykToRgb(Clamp.cmyk(input)), -}; - -/** - * Converts a color object to a standardized format (Rgb). - * - * @param input - The input color object or `AnyColorData`. - * @returns The standardized `ColorData` with the color represented in Rgb format. - */ -export function fromObject( - input: AnyColorData | ColorObject | unknown | null, -): ColorData { - if (!isObject(input)) { - throw new TypeError(errorMessages.invalidColorType); - } - - const hasData = isColorData(input); - - const value = hasData ? input.value : input; - if (!value) { - throw new TypeError(errorMessages.invalidColorType); - } - - const originalInput = ( - hasData ? input.originalInput || value : value - ) as ColorObject; - - const colorTypeFromObject = determineColorType(value); - if (!colorTypeFromObject || !Object.hasOwn(converters, colorTypeFromObject)) { - throw new TypeError(errorMessages.invalidColorObject(value)); - } - - const format = hasData ? input.format : colorTypeFromObject; - - return { - originalInput: originalInput, - isValid: value !== undefined && format !== undefined, - value: converters[colorTypeFromObject](value as ColorObject), - format, - }; -} - -/** - * Processes a color input (string or object) and returns its standardized ColorData representation. - * - * @param input - The color input (string, color object, or undefined). - * @returns The standardized ColorData representation of the input color. - * @throws {TypeError} If the input is not a valid color string or object. - */ -export function processColorInput( - input?: unknown | string | ColorObject | ColorData, -): ColorData { - if (isString(input)) { - const parsedColorData = parseColor(input); - if (parsedColorData !== null) return fromObject(parsedColorData); - - throw new TypeError(errorMessages.invalidColorString(input)); - } - - if (isUndefined(input)) return fallbackColor; - - return fromObject(input); -} diff --git a/src/core/pluginValidation.ts b/src/core/pluginValidation.ts deleted file mode 100644 index f33cf04..0000000 --- a/src/core/pluginValidation.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { errorMessages } from "../constants/errorMessages"; -import type { DyePlugins } from "../types"; - -/** - * Check if the plugin is Not a Plugin - * @param plugins An key-value object with plugin functions to apply. - * @param name Method name of the Plugin - * @return True if the plugins is not valid, undefined in case it's valid. - */ -export const isValidPlugin = ( - plugins: DyePlugins, - name: string, -): name is keyof typeof plugins => { - if (!Object.hasOwn(plugins, name)) return false; - - if (typeof plugins[name] !== "function") { - throw new TypeError(errorMessages.invalidPlugin(name)); - } - - return true; -}; diff --git a/src/dye.ts b/src/dye.ts index 56f1837..0d454bd 100644 --- a/src/dye.ts +++ b/src/dye.ts @@ -1,133 +1,87 @@ +import { rgbParser } from "./parsers/rgbParser"; +import { convertRgbToCmyk, convertRgbToHsv } from "./processing/conversions"; +import { rgbToHsl } from "./processing/interconversions"; +import { matchColor } from "./processing/matchColor"; +import type { Colors, Dye } from "./types"; +import { relativeLuminance } from "./utils/accessibility"; +import { integratePlugins } from "./utils/pluginHelpers"; + /** - * @file Defines the core `dye` function for creating and manipulating color objects in the colorus-js library. - * - * The `dye` function serves as the main entry point for working with colors. It accepts various color inputs (strings or objects) and optional configuration options, including plugins for extending functionality. + * Creates and manipulates colors. * - * It returns a `DyeReturns` object that provides access to color properties, conversion methods, adjustment methods, and any custom plugin methods. + * @param {Colors.Input} input - The color input (string or object). + * @param {Dye.Options

} [options] - Optional configuration (e.g.: `plugins`, `parsers`, `formatOptions`). + * @returns {Dye.Instance

} The `Dye.Instance` object with color properties and methods. */ +export function dye( + input: I, + options?: Dye.Options

, +): Dye.Instance

; +export function dye( + input: I, + options?: Dye.Options

, +): Dye.Instance

; +export function dye( + input: I, + options: Dye.Options

= {}, +): Dye.Instance

{ + let processedInput: Dye.ParserMatchArray | undefined; + let error: Dye.Instance["error"]; + + options.parsers = [rgbParser, ...(options.parsers || [])]; -// Core library modules -import { contrastRatio, relativeLuminance } from "./core/accessibility"; -import { alpha, hue, lighten, saturate } from "./core/colorAdjustments"; -import colorFormatter from "./core/colorFormatter"; -import { fallbackColor, processColorInput } from "./core/inputSerializer"; -import { isValidPlugin } from "./core/pluginValidation"; - -// Conversion utilities -import { - rgbToCmyk, - rgbToHex, - rgbToHsl, - rgbToHsv, - rgbToNamedColor, -} from "./conversions/rgbConversions"; - -// Helper functions -import { isObject, isUndefined } from "./helpers"; - -// Constants -import { errorMessages } from "./constants/errorMessages"; - -// Types -import type { - ColorInput, - Dye, - DyeOptions, - DyePlugins, - DyeReturns, -} from "./types"; - -function isValidDyeOptions

( - options?: DyeOptions

, -): options is DyeOptions

{ - if (isUndefined(options)) return false; - - if ( - !isObject(options) || - (!isUndefined(options.plugins) && !isObject(options.plugins)) || - (!isUndefined(options.formatOptions) && !isObject(options.formatOptions)) - ) { - return false; + try { + processedInput = matchColor(input, { parsers: options.parsers }); + } catch (err) { + error = { message: (err as Error).message }; } - return true; -} + if (!processedInput) { + processedInput = [ + input, + { r: 0, g: 0, b: 0, a: 1 }, + { model: "unknown", value: input, isValid: false }, + ]; + } -/** - * Creates a color object for manipulation and conversion. - * - * @param {ColorInput} input - The color input (string or object). - * @param {DyeOptions

} [options] - Optional configuration (plugins, format options). - * - * @returns {DyeReturns

} The color object with core and plugin methods. - */ -export function dye

( - input: ColorInput, - options: DyeOptions

= {}, -): DyeReturns

{ - try { - const processedInput = processColorInput(input); + const [_, rgb, source] = processedInput; - if (!isValidDyeOptions(options)) { - throw new TypeError(errorMessages.invalidOptions); - } + return integratePlugins

( + { + source: source, - const result: Dye

= { - ...processedInput, + options, - get luminance() { - return relativeLuminance(result.rgb); + get rgb() { + return rgb; }, - get rgb() { - return result.value || fallbackColor.value; + get luminance() { + return relativeLuminance(this.rgb); }, get hsl() { - return rgbToHsl(result.rgb); + return rgbToHsl(this.rgb); }, get hsv() { - return rgbToHsv(result.rgb); + return convertRgbToHsv(this.rgb); }, get cmyk() { - return rgbToCmyk(result.rgb); + return convertRgbToCmyk(this.rgb); }, - toHex: () => rgbToHex(result.rgb, options.formatOptions), - toRgb: () => colorFormatter.rgb(result.rgb, options.formatOptions), - toHsl: () => colorFormatter.hsl(result.hsl, options.formatOptions), - toHsv: () => colorFormatter.hsv(result.hsv, options.formatOptions), - toCmyk: () => colorFormatter.cmyk(result.cmyk, options.formatOptions), - toNamed: () => rgbToNamedColor(result.rgb), - - lighten: (amount = 0.1) => dye(lighten(result.hsl, amount), options), - darken: (amount = 0.1) => dye(lighten(result.hsl, -amount), options), - saturate: (amount = 0.1) => dye(saturate(result.hsl, amount), options), - desaturate: (amount = 0.1) => dye(saturate(result.hsl, -amount), options), - hue: (amount = 0.1) => dye(hue(result.hsl, amount), options), - alpha: (amount = 0.1) => dye(alpha(result.rgb, amount), options), - contrastRatio: bgColor => contrastRatio(result.rgb, dye(bgColor).rgb), - }; - - for (const methodName in options.plugins) { - if (isValidPlugin(options.plugins, methodName)) { - (result as any)[methodName] = (...args: unknown[]) => - options.plugins?.[methodName]?.call(result, ...args); - } - } - - return result as DyeReturns

; - } catch (err) { - if (err instanceof TypeError) { - throw new TypeError(`Invalid dye() usage: ${err.message}`); - } + get alpha() { + return this.rgb.a; + }, - if (err instanceof Error) { - throw new Error(err.message); - } + get hue() { + return this.hsl.h; + }, - throw new Error("An unknown error occurred during color processing."); - } + error, + } as Dye.Instance

, + options.plugins as P, + ); } diff --git a/src/helpers.ts b/src/helpers.ts deleted file mode 100644 index f5e0344..0000000 --- a/src/helpers.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { AnyObject } from "./types"; - -/** - * Clamps a value between a minimum and maximum limit. - * - * - If the input value is greater than the maximum, the maximum will be returned. - * - If the input value is less than the minimum, the minimum will be returned. - * - * The clamped value is guaranteed to be greater than or equal to zero. - * - * @param v - The input value to clamp - * @param max - The maximum value for the input - * @return The clamped value between 0 and max - */ -export const utmost = (v: number | string, max: number): number => - Math.max(Math.min(Number(v), max), 0); - -/** - * Returns the input value with a precision level of two decimal places. - * - * @param value - The input value to format - * @return The input value formatted with the specified precision - */ -export const precision = (value: number): number => - Math.trunc(value) !== value ? Math.round(value * 100) / 100 : value; - -/** - * Converts the input to a string with radix 16 (HEX). - * - * @param input - The Rgb channel values. - * @param minSize - Minimum size of the HEX string. If not provided, defaults to 6. - * @return An HEX string. (e.g., "FF" or "00FF00" depending on the minSize value.) - */ -export const hexString = (input: number, minSize: number): string => - input.toString(16).padStart(minSize, "0").toUpperCase(); - -/** - * Converts a minified HEX color into a HEX 6 or 8. - * - * **Warning**: Only use this with minified HEX strings. - * - * @param minHex - A minified HEX string. (e.g., "FFF" or "E3EF".) - * @return A HEX string with a length of 6 or 8. - */ -export const padString = (minHex: string): string => { - if (minHex.length > 4) return minHex; - - let value = ""; - - for (const slice of minHex) { - value += slice + slice; - } - - return value; -}; - -/** - * Check if input is NOT a Number (NaN) - * @param v - The value to check against. - * @return `true` if it is not an number, `false` otherwise. - */ -export const nan = (v: unknown): boolean => - typeof v !== "number" || Number.isNaN(v) || !Number.isFinite(v); - -/** - * Check if input is a valid object - * @param v - The object to check against. - * @return `true` if it is not an object, `false` otherwise. - */ -export const isObject = (v: unknown): v is AnyObject => - !(typeof v !== "object" || Array.isArray(v) || v === null); - -export const isUndefined = (v: unknown): v is undefined => - typeof v === "undefined"; - -export const isString = (v: unknown): v is string => typeof v === "string"; diff --git a/src/isValidColor.ts b/src/isValidColor.ts deleted file mode 100644 index 9e37952..0000000 --- a/src/isValidColor.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { - determineColorType, - execColorStringTest, -} from "./core/colorTypeAnalyzer"; -import { isObject, isString } from "./helpers"; -import type { AnyObject, SupportedColorFormat } from "./types"; - -/** Tests the `input` for a valid color. - * @param input - The color input string or object. - * @return The type of the string (e.g.: `rgb`) if color is valid, otherwise `null`. - */ -export function isValidColor( - input: object | string | unknown, -): SupportedColorFormat | null { - if (!input) return null; - - if (isObject(input)) { - return determineColorType(input as AnyObject) || null; - } - - if (isString(input)) { - const match = execColorStringTest(input); - if (match) return match[0]; - } - - return null; -} diff --git a/src/main.ts b/src/main.ts index 2c9a946..f7a1411 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,29 @@ -export { isValidColor } from "./isValidColor"; - export { dye } from "./dye"; - -export type { Dye, Cmyk, Hsl, Hsv, Rgb } from "./types"; +export { cmykParser } from "./parsers/cmykParser"; +export { hexParser } from "./parsers/hexParser"; +export { hslParser } from "./parsers/hslPaser"; +export { hsvParser } from "./parsers/hsvParser"; +export { rgbParser } from "./parsers/rgbParser"; +export { invert } from "./plugins/invert"; +export * from "./plugins/lighten"; +export * from "./plugins/saturate"; +export { toCmyk } from "./plugins/toCmyk"; +export { toHex } from "./plugins/toHex"; +export { toHsl } from "./plugins/toHsl"; +export { toHsv } from "./plugins/toHsv"; +export { toRgb } from "./plugins/toRgb"; +export { ColorParser } from "./processing/colorParser"; +export * from "./processing/conversions"; +export * from "./processing/interconversions"; +export type { Colors, Dye } from "./types"; +export * from "./utils/clampColorHelpers"; +export { + clamp, + formatDecimal, + generateColorComponents, + normalize8Bit, + normalizeAlpha, + normalizeDegrees, + normalizePercentage, +} from "./utils/colorUtils"; +export { createPlugin } from "./utils/pluginHelpers"; diff --git a/src/parsers/cmykParser.ts b/src/parsers/cmykParser.ts new file mode 100644 index 0000000..f3e7b10 --- /dev/null +++ b/src/parsers/cmykParser.ts @@ -0,0 +1,21 @@ +import { chCmyk, regexCmyk } from "../patterns"; +import { ColorParser } from "../processing/colorParser"; +import { convertCmykToRgb } from "../processing/conversions"; +import type { Colors } from "../types"; +import { clampCmyk } from "../utils/clampColorHelpers"; + +export const cmykParser = new ColorParser({ + model: "cmyk", + extract: match => + ({ + c: match[0], + m: match[1], + y: match[2], + k: match[3], + a: match[4], + }) as Colors.Cmyk, + serialize: convertCmykToRgb, + clamp: clampCmyk, + regex: regexCmyk, + channels: chCmyk, +}); diff --git a/src/parsers/hexParser.ts b/src/parsers/hexParser.ts new file mode 100644 index 0000000..ce810af --- /dev/null +++ b/src/parsers/hexParser.ts @@ -0,0 +1,10 @@ +import { regexHex } from "../patterns"; +import { ColorParser } from "../processing/colorParser"; +import { convertHexToRgb } from "../processing/conversions"; + +export const hexParser = new ColorParser({ + model: "hex", + extract: match => match[0] as string, + serialize: convertHexToRgb, + regex: regexHex, +}); diff --git a/src/parsers/hslPaser.ts b/src/parsers/hslPaser.ts new file mode 100644 index 0000000..d66b43e --- /dev/null +++ b/src/parsers/hslPaser.ts @@ -0,0 +1,20 @@ +import { chHsl, regexHsl } from "../patterns"; +import { ColorParser } from "../processing/colorParser"; +import { hslToRgb } from "../processing/interconversions"; +import type { Colors } from "../types"; +import { clampHsl } from "../utils/clampColorHelpers"; + +export const hslParser = new ColorParser({ + model: "hsl", + extract: match => + ({ + h: match[0], + s: match[1], + l: match[2], + a: match[3], + }) as Colors.Hsl, + serialize: hslToRgb, + clamp: clampHsl, + regex: regexHsl, + channels: chHsl, +}); diff --git a/src/parsers/hsvParser.ts b/src/parsers/hsvParser.ts new file mode 100644 index 0000000..8131b75 --- /dev/null +++ b/src/parsers/hsvParser.ts @@ -0,0 +1,20 @@ +import { chHsv, regexHsv } from "../patterns"; +import { ColorParser } from "../processing/colorParser"; +import { convertHsvToRgb } from "../processing/conversions"; +import type { Colors } from "../types"; +import { clampHsv } from "../utils/clampColorHelpers"; + +export const hsvParser = new ColorParser({ + model: "hsv", + extract: match => + ({ + h: match[0], + s: match[1], + v: match[2], + a: match[3], + }) as Colors.Hsv, + serialize: convertHsvToRgb, + clamp: clampHsv, + regex: regexHsv, + channels: chHsv, +}); diff --git a/src/parsers/rgbParser.ts b/src/parsers/rgbParser.ts new file mode 100644 index 0000000..af5a0e5 --- /dev/null +++ b/src/parsers/rgbParser.ts @@ -0,0 +1,19 @@ +import { chRgb, regexRgb } from "../patterns"; +import { ColorParser } from "../processing/colorParser"; +import type { Colors } from "../types"; +import { clampRgb } from "../utils/clampColorHelpers"; + +export const rgbParser = new ColorParser({ + model: "rgb", + extract: match => + ({ + r: match[0], + g: match[1], + b: match[2], + a: match[3], + }) as Colors.Rgb, + serialize: rgb => rgb, + clamp: clampRgb, + regex: regexRgb, + channels: chRgb, +}); diff --git a/src/patterns.ts b/src/patterns.ts new file mode 100644 index 0000000..8a369bb --- /dev/null +++ b/src/patterns.ts @@ -0,0 +1,68 @@ +// Color channels used to check dictionary-like objects. + +/** This array contains the color channels of a RGB color. */ +export const chRgb = ["r", "g", "b", "a"]; + +/** This array contains the color channels of a HSL color. */ +export const chHsl = ["h", "s", "l", "a"]; + +/** This array contains the color channels of a HSV color. */ +export const chHsv = ["h", "s", "v", "a"]; + +/** This array contains the color channels of a CMYK color. */ +export const chCmyk = ["c", "m", "y", "k", "a"]; + +// Color Regular Expressions used to match color strings. +// These expressions should match the color strings and extract the color channels. + +/** + * This regular expression matches a hexadecimal color string. + * @example + * regexHex.test("f00"); // true + * regexHex.test("#ff0000"); // true + * regexHex.test("#ff0000ff"); // true + */ +export const regexHex = /^#?([a-f0-9]{8}|[a-f0-9]{6}|[a-f0-9]{3,4})$/i; + +/** + * This regular expression matches a RGB color string. + * @example + * regexRgb.test("rgb(255 0 0)"); // true + * regexRgb.test("rgb(255 0 0 / 1)"); // true + * regexRgb.test("rgba(255, 0, 0, 1)"); // true + */ +export const regexRgb = + /^rgba?\(\s*(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*(?:,|\/)\s*(0?\.\d+|1|0))?\s*\)$/i; + +/** + * This regular expression matches a HSL color string. + * @example + * regexHsl.test("hsl(0 100% 50%)"); // true + * regexHsl.test("hsla(0, 100%, 50%, 1)"); // true + * regexHsl.test("hsl(0deg 100% 50%)"); // true + * regexHsl.test("hsla(0deg, 100%, 50%, 1)"); // true + */ +export const regexHsl = + /^hsla?\(\s*(\d{1,3})(?:\s*(?:deg|Β°))?(?:\s*,\s*|\s+)(\d{1,3})%?(?:\s*,\s*|\s+)(\d{1,3})%?(?:\s*(?:,|\/)\s*(0?\.\d+|1|0))?\s*\)$/i; + +/** + * This regular expression matches a HSV color string. + * @example + * regexHsv.test("hsv(0 100% 100%)"); // true + * regexHsv.test("hsva(0, 100%, 100%, 1)"); // true + * regexHsv.test("hsv(0deg 100% 100%)"); // true + * regexHsv.test("hsva(0deg, 100%, 100%, 1)"); // true + */ +export const regexHsv = + /^hsva?\(\s*(\d{1,3})(?:\s*(?:deg|Β°))?(?:\s*,\s*|\s+)(\d{1,3})%?(?:\s*,\s*|\s+)(\d{1,3})%?(?:\s*(?:,|\/)\s*(0?\.\d+|1|0))?\s*\)$/i; + +/** + * This regular expression matches a CMYK color string. + * @example + * regexCmyk.test("cmyk(0 100% 100% 0)"); // true + * regexCmyk.test("cmyka(0, 100%, 100%, 0, 1)"); // true + * regexCmyk.test("cmyk(0% 100% 100% 0%)"); // true + * regexCmyk.test("cmyka(0%, 100%, 100%, 0%, 1)"); // true + */ +export const regexCmyk = + /^cmyka?\(\s*(\d{1,3})%?(?:\s*,\s*|\s+)(\d{1,3})%?(?:\s*,\s*|\s+)(\d{1,3})%?(?:\s*,\s*|\s+)(\d{1,3})%?(?:\s*(?:,|\/)\s*(0?\.\d+|1|0))?\s*\)$/i; diff --git a/src/plugins/invert.ts b/src/plugins/invert.ts new file mode 100644 index 0000000..c66f811 --- /dev/null +++ b/src/plugins/invert.ts @@ -0,0 +1,14 @@ +import { dye } from "../dye"; +import { createPlugin } from "../utils/pluginHelpers"; + +export const invert = createPlugin("invert", function () { + return dye( + { + r: 255 - this.rgb.r, + g: 255 - this.rgb.g, + b: 255 - this.rgb.b, + a: this.rgb.a, + }, + this.options, + ); +}); diff --git a/src/plugins/lighten.ts b/src/plugins/lighten.ts new file mode 100644 index 0000000..ea98aa9 --- /dev/null +++ b/src/plugins/lighten.ts @@ -0,0 +1,15 @@ +import { dye } from "../dye"; +import { hslToRgb } from "../processing/interconversions"; +import { modBy } from "../utils/colorUtils"; +import { createPlugin } from "../utils/pluginHelpers"; + +export const lighten = createPlugin("lighten", function (amount: number = 0.1) { + return dye( + hslToRgb({ ...this.hsl, l: modBy(this.hsl.l, amount) }), + this.options, + ); +}); + +export const darken = createPlugin("darken", function (amount: number = 0.1) { + return lighten.call(this, -amount); +}); diff --git a/src/plugins/saturate.ts b/src/plugins/saturate.ts new file mode 100644 index 0000000..fb96db5 --- /dev/null +++ b/src/plugins/saturate.ts @@ -0,0 +1,21 @@ +import { dye } from "../dye"; +import { hslToRgb } from "../processing/interconversions"; +import { modBy } from "../utils/colorUtils"; +import { createPlugin } from "../utils/pluginHelpers"; + +export const saturate = createPlugin( + "saturate", + function (amount: number = 0.1) { + return dye( + hslToRgb({ ...this.hsl, s: modBy(this.hsl.s, amount) }), + this.options, + ); + }, +); + +export const desaturate = createPlugin( + "desaturate", + function (amount: number = 0.1) { + return saturate.call(this, -amount); + }, +); diff --git a/src/plugins/toCmyk.ts b/src/plugins/toCmyk.ts new file mode 100644 index 0000000..75c6631 --- /dev/null +++ b/src/plugins/toCmyk.ts @@ -0,0 +1,14 @@ +import { clampCmyk } from "../utils/clampColorHelpers"; +import { generateColorComponents } from "../utils/colorUtils"; +import { createPlugin } from "../utils/pluginHelpers"; + +export const toCmyk = createPlugin("toCmyk", function () { + const [separator, a, suffix, percent] = generateColorComponents( + this.alpha, + this.options.formatOptions, + ); + + const { c, m, y, k } = clampCmyk(this.cmyk, true); + + return `cmyk${suffix}(${c}${percent}${separator}${m}${percent}${separator}${y}${percent}${separator}${k}${percent}${a})`; +}); diff --git a/src/plugins/toHex.ts b/src/plugins/toHex.ts new file mode 100644 index 0000000..46eca75 --- /dev/null +++ b/src/plugins/toHex.ts @@ -0,0 +1,18 @@ +import { convertRgbToHex } from "../processing/conversions"; +import type { Colors } from "../types"; +import { createPlugin } from "../utils/pluginHelpers"; + +export const isRgbShortanable = ({ r, g, b, a = 1 }: Colors.Rgb): boolean => + !(r % 17 !== 0 || g % 17 !== 0 || b % 17 !== 0 || (a * 255) % 17 !== 0); + +export const toHex = createPlugin("toHex", function () { + const { minify } = this.options?.formatOptions || {}; + + const hex = convertRgbToHex(this.rgb); + + if (minify && isRgbShortanable(this.rgb)) { + return `#${hex.slice(1).replace(/(.)\1/g, "$1")}`; + } + + return hex; +}); diff --git a/src/plugins/toHsl.ts b/src/plugins/toHsl.ts new file mode 100644 index 0000000..e6dc1a6 --- /dev/null +++ b/src/plugins/toHsl.ts @@ -0,0 +1,14 @@ +import { clampHsl } from "../utils/clampColorHelpers"; +import { generateColorComponents } from "../utils/colorUtils"; +import { createPlugin } from "../utils/pluginHelpers"; + +export const toHsl = createPlugin("toHsl", function () { + const [separator, a, suffix, percent] = generateColorComponents( + this.alpha, + this.options.formatOptions, + ); + + const { h, s, l } = clampHsl(this.hsl, true); + + return `hsl${suffix}(${h}${separator}${s}${percent}${separator}${l}${percent}${a})`; +}); diff --git a/src/plugins/toHsv.ts b/src/plugins/toHsv.ts new file mode 100644 index 0000000..f0f37a4 --- /dev/null +++ b/src/plugins/toHsv.ts @@ -0,0 +1,14 @@ +import { clampHsv } from "../utils/clampColorHelpers"; +import { generateColorComponents } from "../utils/colorUtils"; +import { createPlugin } from "../utils/pluginHelpers"; + +export const toHsv = createPlugin("toHsv", function () { + const [separator, a, suffix, percent] = generateColorComponents( + this.alpha, + this.options.formatOptions, + ); + + const { h, s, v } = clampHsv(this.hsv, true); + + return `hsv${suffix}(${h}${separator}${s}${percent}${separator}${v}${percent}${a})`; +}); diff --git a/src/plugins/toRgb.ts b/src/plugins/toRgb.ts new file mode 100644 index 0000000..8a4de4d --- /dev/null +++ b/src/plugins/toRgb.ts @@ -0,0 +1,14 @@ +import { clampRgb } from "../utils/clampColorHelpers"; +import { generateColorComponents } from "../utils/colorUtils"; +import { createPlugin } from "../utils/pluginHelpers"; + +export const toRgb = createPlugin("toRgb", function () { + const [separator, a, suffix] = generateColorComponents( + this.alpha, + this.options.formatOptions, + ); + + const { r, g, b } = clampRgb(this.rgb, true); + + return `rgb${suffix}(${r}${separator}${g}${separator}${b}${a})`; +}); diff --git a/src/processing/colorParser.ts b/src/processing/colorParser.ts new file mode 100644 index 0000000..ec6733a --- /dev/null +++ b/src/processing/colorParser.ts @@ -0,0 +1,90 @@ +import type { Colors, Dye } from "../types"; +import { isColorValue, isObject } from "../utils/validation"; + +/** + * `ColorParser` is a configurable utility for parsing and serializing color values + * in different formats (e.g., RGB, HSL). + * + * @throws Error if required config properties (`regex`, `model`, `extract`, `serialize`) are missing or invalid. + */ +export class ColorParser { + private model: string; + private extract: Dye.ParserExtractor; + private serialize: Dye.ParserSerializer; + private regex: RegExp; + private channels?: string[]; + private clamp: (value: E) => E; + + constructor(config: Dye.ParserConfig) { + if (!config.regex || !(config.regex instanceof RegExp)) { + throw new Error("Missing 'regex' RegExp in the configuration"); + } + + if (!config.model || typeof config.model !== "string") { + throw new Error("Missing 'model' string in the configuration"); + } + + if (!config.extract || typeof config.extract !== "function") { + throw new Error("Missing 'extract' function in the configuration"); + } + + if (!config.serialize || typeof config.serialize !== "function") { + throw new Error(`Missing 'serialize' function in the configuration.`); + } + + if (config.clamp && typeof config.clamp !== "function") { + throw new Error(`Invalid 'clamp' function in the configuration.`); + } + + this.model = config.model; + this.extract = config.extract; + this.serialize = config.serialize; + this.regex = config.regex; + this.clamp = config.clamp ?? ((value: E): E => value); + this.channels = config.channels; + } + + public parse>( + input: T, + ): Dye.ParserMatchArray | null { + const match = this.matchColorString(input) ?? this.matchColorObject(input); + + if (!match) return null; + + const extractedValue = this.extract(match); + + return [ + input, + this.serialize(this.clamp(extractedValue)), + { + value: extractedValue, + model: this.model, + isValid: true, + }, + ]; + } + + private matchColorString( + input: unknown, + ): Colors.MatchArray | null { + if (typeof input !== "string") return null; + this.regex.lastIndex = 0; // Always reset the regex before using it, to avoid unexpected results + return input.match(this.regex)?.slice(1) ?? null; + } + + private matchColorObject( + colorInput: unknown, + ): Colors.MatchArray | null { + if (!this.channels || !isObject(colorInput)) return null; + + const color = colorInput as Colors.Object; + + const values = this.channels.filter( + k => isColorValue(color[k]) || (k === "a" && color[k] === undefined), + ); + + if (values.length !== this.channels.length) return null; + + return values.map(k => color[k]); + } +} diff --git a/src/processing/conversions.ts b/src/processing/conversions.ts new file mode 100644 index 0000000..1ee3b93 --- /dev/null +++ b/src/processing/conversions.ts @@ -0,0 +1,186 @@ +import type { Colors } from "../types"; +import { + clampCmyk, + clampHsl, + clampHsv, + clampRgb, +} from "../utils/clampColorHelpers"; +import { expandHexString, toHexString } from "../utils/colorUtils"; + +export const convertCmykToRgb = ( + { c, m, y, k, a }: Colors.Cmyk, + round: boolean = false, +): Colors.Rgb => { + const fn = (value: number) => 255 * (1 - value / 100) * (1 - k / 100); + return clampRgb( + { + r: fn(c), + g: fn(m), + b: fn(y), + a, + }, + round, + ); +}; + +export const convertHexToRgb = ( + color: string, + round: boolean = false, +): Colors.Rgb => { + const hex = expandHexString(color); + const num = Number.parseInt(hex, 16); + + const hasAlpha = hex.length === 8; + + return clampRgb( + { + r: (num >> (hasAlpha ? 24 : 16)) & 0xff, + g: (num >> (hasAlpha ? 16 : 8)) & 0xff, + b: (num >> (hasAlpha ? 8 : 0)) & 0xff, + a: hasAlpha ? (num & 0xff) / 255 : 1, + }, + round, + ); +}; + +export const convertHslToHsv = ( + { h, s, l, a }: Colors.Hsl, + round: boolean = false, +): Colors.Hsv => { + const sPercent = s / 100; + const lPercent = l / 100; + const v = lPercent + sPercent * Math.min(lPercent, 1 - lPercent); + const saturation = v === 0 ? 0 : (2 * (v - lPercent)) / v; + + return clampHsv({ h, s: saturation * 100, v: v * 100, a }, round); +}; + +export const convertHsvToHsl = ( + { h, s, v, a }: Colors.Hsv, + round: boolean = false, +): Colors.Hsl => { + const sPercent = s / 100; + const vPercent = v / 100; + + const l = ((2 - sPercent) * vPercent) / 2; + const lightnessSaturation = + (sPercent * vPercent) / (l <= 0.5 ? l * 2 : 2 - l * 2); + + return clampHsl( + { + h, + s: lightnessSaturation * 100, + l: l * 100, + a, + }, + round, + ); +}; + +export const convertHsvToRgb = ( + { h, s, v, a }: Colors.Hsv, + round: boolean = false, +): Colors.Rgb => { + const normalizedH = h / 360; + const normalizedS = s / 100; + const normalizedV = v / 100; + + const chroma = normalizedV * normalizedS; + const huePrime = normalizedH * 6; + const x = chroma * (1 - Math.abs((huePrime % 2) - 1)); + + const rgbValues = [ + [chroma, x, 0], + [x, chroma, 0], + [0, chroma, x], + [0, x, chroma], + [x, 0, chroma], + [chroma, 0, x], + ]; + + const [r, g, b] = rgbValues[Math.floor(huePrime) % 6]; + + return clampRgb( + { + r: (normalizedV - chroma + r) * 255, + g: (normalizedV - chroma + g) * 255, + b: (normalizedV - chroma + b) * 255, + a, + }, + round, + ); +}; + +export const convertRgbToCmyk = ( + { r, g, b, a }: Colors.Rgb, + round: boolean = false, +): Colors.Cmyk => { + const rNormalized = r / 255; + const gNormalized = g / 255; + const bNormalized = b / 255; + + const k = 1 - Math.max(rNormalized, gNormalized, bNormalized); + + const fn = (ch: number) => ((1 - ch - k) / (1 - k)) * 100; + + return clampCmyk( + { + c: fn(rNormalized), + m: fn(gNormalized), + y: fn(bNormalized), + k: k * 100, + a, + }, + round, + ); +}; + +export const convertRgbToHex = (rgb: Colors.Rgb): string => { + const { r, g, b, a } = clampRgb(rgb, true); + let hexMap = [r, g, b].map(value => toHexString(value, 2)).join(""); + + if (a < 1) hexMap += toHexString(Math.round(a * 255), 2); + + return `#${hexMap}`; +}; + +export const convertRgbToHsv = ( + { r, g, b, a }: Colors.Rgb, + round: boolean = false, +): Colors.Hsv => { + const rPercent = r / 255; + const gPercent = g / 255; + const bPercent = b / 255; + + const max = Math.max(rPercent, gPercent, bPercent); + const min = Math.min(rPercent, gPercent, bPercent); + + const delta = max - min; + const v = max; + const s = max === 0 ? 0 : delta / max; + + let h = 0; + if (delta !== 0) { + if (rPercent === max) { + h = (gPercent - bPercent) / delta; + } else if (gPercent === max) { + h = 2 + (bPercent - rPercent) / delta; + } else { + h = 4 + (rPercent - gPercent) / delta; + } + h *= 60; + if (h < 0) { + h += 360; + } + } + + return clampHsv( + { + h: h, + s: s * 100, + v: v * 100, + a, + }, + round, + ); +}; diff --git a/src/processing/interconversions.ts b/src/processing/interconversions.ts new file mode 100644 index 0000000..32a16af --- /dev/null +++ b/src/processing/interconversions.ts @@ -0,0 +1,15 @@ +import type { Colors } from "../types"; +import { + convertHslToHsv, + convertHsvToHsl, + convertHsvToRgb, + convertRgbToHsv, +} from "./conversions"; + +export const rgbToHsl = (color: Colors.Rgb, round = false): Colors.Hsl => { + return convertHsvToHsl(convertRgbToHsv(color), round); +}; + +export const hslToRgb = (color: Colors.Hsl, round = false): Colors.Rgb => { + return convertHsvToRgb(convertHslToHsv(color), round); +}; diff --git a/src/processing/matchColor.ts b/src/processing/matchColor.ts new file mode 100644 index 0000000..b21bfe9 --- /dev/null +++ b/src/processing/matchColor.ts @@ -0,0 +1,54 @@ +import type { Colors, Dye } from "../types"; +import { ColorParser } from "./colorParser"; + +/** + * Attempts to match a color input to a valid color object using the available parsers. + * The function iterates over the registered parsers and attempts to parse the input + * using each one. If a valid color object is found, it is returned. If no valid color + * object is found, an error is thrown. + * + * @param input - The color input to parse. + * @param options - The parsing options to be used for processing the input. + * @returns The parsed color object. + * @throws An error if no valid color parsers are found for the input. + */ +export function matchColor( + input?: Colors.Input, + options?: Dye.Options, +): Dye.ParserMatchArray; +export function matchColor( + this: Dye.Options, + input?: Colors.Input, + options?: Dye.Options, +): Dye.ParserMatchArray { + let error = ""; + const parsers = [...(options?.parsers || []), ...(this?.parsers || [])]; + if (!input) { + throw new Error("No color input provided"); + } + + if (parsers.length === 0) { + throw new Error( + "No parsers were provided for the color input. Ensure at least one parser is available", + ); + } + + for (const parser of parsers) { + try { + if (!(parser instanceof ColorParser)) { + throw new Error("Invalid parser provided"); + } + + const parsedValue = parser.parse(input); + if (parsedValue) return parsedValue; + } catch (err) { + error = (err as Error).message; + } + } + + if (error) throw new Error(`Failed to parse color input: ${error}`); + + throw new Error( + `Failed to parse color input: No valid parser found for '${typeof input}'`, + ); +} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index faa20d0..0000000 --- a/src/types.ts +++ /dev/null @@ -1,195 +0,0 @@ -import type { namedColorsMap } from "./constants/namedColors"; - -export type AnyObject = Record; - -export interface AnyRgb { - r: number; - g: number; - b: number; - a?: number; -} -export interface Rgb extends AnyRgb { - a: number; -} - -export interface AnyHsl { - h: number; - s: number; - l: number; - a?: number; -} - -export interface Hsl extends AnyHsl { - a: number; -} - -export interface AnyHsv { - h: number; - s: number; - v: number; - a?: number; -} - -export interface Hsv extends AnyHsv { - a: number; -} - -export interface AnyCmyk { - c: number; - m: number; - y: number; - k: number; - a?: number; -} - -export interface Cmyk extends AnyCmyk { - a: number; -} - -export type SupportedColorFormat = - | "rgb" - | "hsl" - | "hsv" - | "cmyk" - | "hex" - | "named"; - -export type ColorObject = AnyRgb | AnyHsl | AnyHsv | AnyCmyk; - -export type ColorInput = string | ColorObject; - -export type ColorData = { - originalInput?: ColorInput; - isValid: boolean; - value: C; - format?: SupportedColorFormat; -}; - -export type AnyColorData = Partial>; - -export type NamedColors = keyof typeof namedColorsMap; - -export type ColorConverters = { - [MethodKey in Exclude]: ( - colorObject: ColorObject, - ) => Rgb; -}; -export type ExecMatchClone = { - pattern: { - lastIndex: number; - exec: (input: string) => string[] | null; - }; -}; - -export type ColorPatterns = [ - SupportedColorFormat, - RegExp | ExecMatchClone["pattern"], -][]; - -export type NamedColorsParsers = { - colors: Record; -} & ExecMatchClone; - -export type ColorParsers = Omit< - { - hex: (input: I) => Rgb; - rgb: (input: I) => Rgb; - hsv: (input: I) => Hsv; - hsl: (input: I) => Hsl; - cmyk: (input: I) => Cmyk; - named: (input: I) => Rgb; - }, - E ->; - -export type ColorNormalizers = { - rgb: (input: AnyObject) => Rgb; - hsv: (input: AnyObject) => Hsv; - hsl: (input: AnyObject) => Hsl; - cmyk: (input: AnyObject) => Cmyk; - fn: (x: number) => number; -}; - -export interface FormatOptions { - /** Whether to attempt to minify the output. */ - minify?: boolean; - - /** Whether to generate CSSNext compatible formatting. */ - cssNext?: boolean; -} - -/** - * Represents a plugin function that extends the Color instance with custom methods. - * - * @param this The Color instance to which the plugin methods will be added. - * @param args Any additional arguments passed to the plugin method. - */ -type Plugin> = ( - this: ThisArg, - ...args: any[] -) => T | void; - -export type DyePlugins = Record>; - -export interface DyeOptions { - /** An object containing the plugin methods to be added to the Colorus instance. */ - plugins?: T; - formatOptions?: FormatOptions; -} - -export type Dye

= { - /** The color value in RGB format, if available. */ - value?: Rgb; - /** The original format of the color input (e.g., 'hex', 'rgb', 'hsl'). */ - format?: SupportedColorFormat; - /** The original input used to create the color. */ - originalInput?: ColorInput; - /** Indicates whether the color input was valid. */ - isValid?: boolean; - /** Calculates and returns the relative luminance of the color. */ - luminance: number; - /** Returns the RGB representation of the color. */ - rgb: Rgb; - /** Converts and returns the color in HSL format. */ - hsl: Hsl; - /** Converts and returns the color in HSV format. */ - hsv: Hsv; - /** Converts and returns the color in CMYK format. */ - cmyk: Cmyk; - /** Converts the color to a hexadecimal string representation. */ - toHex: () => string; - /** Converts the color to an RGB string representation. */ - toRgb: () => string; - /** Converts the color to an HSL string representation. */ - toHsl: () => string; - /** Converts the color to an HSV string representation. */ - toHsv: () => string; - /** Converts the color to a CMYK string representation. */ - toCmyk: () => string; - /** Converts the color to its nearest CSS named color. */ - toNamed: () => string; - /** Creates a new lighter color. */ - lighten: (amount?: number) => DyeReturns

; - /** Creates a new darker color. */ - darken: (amount?: number) => DyeReturns

; - /** Creates a new more saturated color. */ - saturate: (amount?: number) => DyeReturns

; - /** Creates a new less saturated color. */ - desaturate: (amount?: number) => DyeReturns

; - /** Creates a new color with adjusted hue. */ - hue: (amount?: number) => DyeReturns

; - /** Creates a new color with adjusted alpha (opacity). */ - alpha: (amount?: number) => DyeReturns

; - /** Calculates the contrast ratio between this color and a background color. */ - contrastRatio: (bgColor: ColorInput) => number; -}; - -/** Represents the return type of the `dye` function, encompassing both the core properties and any additional plugin methods. */ -export type DyeReturns

= Dye

& { - [K in keyof P]: P[K] extends Plugin - ? ( - this: ThisArg, - ...params: Parameters - ) => R extends DyeReturns

? DyeReturns

: R - : never; -}; diff --git a/src/types/colorModels.ts b/src/types/colorModels.ts new file mode 100644 index 0000000..2b5c19d --- /dev/null +++ b/src/types/colorModels.ts @@ -0,0 +1,40 @@ +export namespace Colors { + export type Object = { + [Key in Keys]: Key extends "a" ? number | undefined : number; + }; + + export type AnyRgb = Object<"r" | "g" | "b" | "a">; + export type AnyHsl = Object<"h" | "s" | "l" | "a">; + export type AnyHsv = Object<"h" | "s" | "v" | "a">; + export type AnyCmyk = Object<"c" | "m" | "y" | "k" | "a">; + + export interface Rgb extends AnyRgb { + a: number; + } + + export interface Hsl extends AnyHsl { + a: number; + } + + export interface Hsv extends AnyHsv { + a: number; + } + + export interface Cmyk extends AnyCmyk { + a: number; + } + + export type Channels = + | keyof AnyRgb + | keyof AnyHsl + | keyof AnyHsv + | keyof AnyCmyk; + + export type All = Rgb | Hsl | Hsv | Cmyk; + + export type Any = AnyRgb | AnyHsl | AnyHsv | AnyCmyk; + + export type Input = string | Object; + + export type MatchArray = Array; +} diff --git a/src/types/dye.ts b/src/types/dye.ts new file mode 100644 index 0000000..2767122 --- /dev/null +++ b/src/types/dye.ts @@ -0,0 +1,81 @@ +import type { ColorParser } from "../processing/colorParser"; +import type { Colors } from "./colorModels"; + +export namespace Dye { + export interface Source { + model: string; + value: V; + isValid?: boolean; + } + + export type ParserMatchArray< + I = Colors.Input, + C = Colors.Rgb, + V = Colors.Input | Colors.Object, + > = [I, C, Source]; + + export type ParserExtractor = ( + match: Colors.MatchArray, + ) => E; + + export type ParserSerializer = (value: E) => R; + + export interface ParserConfig { + model: string; + extract: ParserExtractor; + serialize: ParserSerializer; + regex: RegExp; + clamp?: (value: E) => E; + channels?: string[]; + } + + export interface FormatOptions { + minify?: boolean; + cssNext?: boolean; + } + + export type PluginFunction< + Returns = void, + Params extends any[] = any[], + Props = Properties, + > = (this: Props, ...params: Params) => Returns; + + export type DefaultPlugins = Record; + + export type Plugins = + T extends DefaultPlugins + ? DefaultPlugins + : { + [K in keyof T]: T[K] extends PluginFunction + ? PluginFunction + : never; + }; + + export interface Options { + plugins?: { + [K in keyof E]: K extends keyof Properties ? never : E[K]; + }; + formatOptions?: FormatOptions; + fallback?: Colors.Rgb; + parsers?: ColorParser[]; + } + + export interface Properties

{ + error?: { message?: string }; + source: Source; + get luminance(): number; + get rgb(): Colors.Rgb; + get hsl(): Colors.Hsl; + get hsv(): Colors.Hsv; + get cmyk(): Colors.Cmyk; + get alpha(): number; + get hue(): number; + options: Options

; + } + + export type Instance = Properties & { + [K in keyof E]: E[K] extends PluginFunction + ? (...params: P) => R extends Instance ? Instance : R + : never; + }; +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..9ec7c27 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,3 @@ +export * from "./colorModels"; +export * from "./dye"; +export * from "./utils"; diff --git a/src/types/utils.ts b/src/types/utils.ts new file mode 100644 index 0000000..5a4c6a0 --- /dev/null +++ b/src/types/utils.ts @@ -0,0 +1,8 @@ +export namespace Utils { + export type NormalizeFunction = ( + value?: number | string, + fn?: (value: number) => number, + ) => number; + + export type ClampFunction = (color: T) => T; +} diff --git a/src/utils/accessibility.ts b/src/utils/accessibility.ts new file mode 100644 index 0000000..058b6be --- /dev/null +++ b/src/utils/accessibility.ts @@ -0,0 +1,16 @@ +import type { Colors } from "../types"; +import { formatDecimal } from "./colorUtils"; + +/** + * Calculate the relative luminance of an sRGB color. + * @param color - An object containing the sRGB components of the color. + * @return The luminance value of the color. + */ +export const relativeLuminance = ({ r, g, b }: Colors.Rgb): number => { + const fn = (c: number) => { + const d = c / 255; + return d <= 0.03928 ? d / 12.92 : ((d + 0.055) / 1.055) ** 2.4; + }; + + return formatDecimal(fn(r) * 0.2126 + fn(g) * 0.7152 + fn(b) * 0.0722); +}; diff --git a/src/utils/clampColorHelpers.ts b/src/utils/clampColorHelpers.ts new file mode 100644 index 0000000..4226505 --- /dev/null +++ b/src/utils/clampColorHelpers.ts @@ -0,0 +1,56 @@ +import type { Colors } from "../types"; +import { + normalize8Bit, + normalizeAlpha, + normalizeDegrees, + normalizePercentage, +} from "./colorUtils"; + +export const clampCmyk = ( + { c, m, y, k, a }: Colors.AnyCmyk, + round = false, +): Colors.Cmyk => { + return { + c: normalizePercentage(c, round), + m: normalizePercentage(m, round), + y: normalizePercentage(y, round), + k: normalizePercentage(k, round), + a: normalizeAlpha(a), + }; +}; + +export const clampHsl = ( + { h, s, l, a }: Colors.AnyHsl, + round = false, +): Colors.Hsl => { + return { + h: normalizeDegrees(h, round), + s: normalizePercentage(s, round), + l: normalizePercentage(l, round), + a: normalizeAlpha(a), + }; +}; + +export const clampHsv = ( + { h, s, v, a }: Colors.AnyHsv, + round = false, +): Colors.Hsv => { + return { + h: normalizeDegrees(h, round), + s: normalizePercentage(s, round), + v: normalizePercentage(v, round), + a: normalizeAlpha(a), + }; +}; + +export const clampRgb = ( + { r, g, b, a }: Colors.AnyRgb, + round = false, +): Colors.Rgb => { + return { + r: normalize8Bit(r, round), + g: normalize8Bit(g, round), + b: normalize8Bit(b, round), + a: normalizeAlpha(a), + }; +}; diff --git a/src/utils/colorUtils.ts b/src/utils/colorUtils.ts new file mode 100644 index 0000000..56c608c --- /dev/null +++ b/src/utils/colorUtils.ts @@ -0,0 +1,83 @@ +import type { Dye, Utils } from "../types"; +import { isColorValue } from "./validation"; + +/** + * Modifies the given `value` by a certain `amount`. + * + * @param value - The value to modify. + * @param amount - The amount to modify the `value` by, a value between 0 and 1. + * @return The modified `value`. + */ +export const modBy = (value: number, amount: number): number => { + const a = Number(amount); + + if (!isColorValue(a)) return value; + + return clamp(formatDecimal((1 + a) * value), 100); +}; + +/** + * Converts an integer to a hexadecimal string with a specified minimum length. + * @param input - The integer to convert. + * @param minSize - The minimum length of the resulting hex string. + * @returns A hex string with the specified minimum length. + */ +export const toHexString = (input: number, minSize: number): string => + input.toString(16).padStart(minSize, "0"); + +/** + * Expands a shortened hex color (e.g., "FFF" or "E3EF") to a full 6- or 8-character hex string. + * @param hexString - A shortened hex color string. + * @returns A full hex string with 6 or 8 characters. + */ +export const expandHexString = (hexString: string): string => { + const minHex = hexString.startsWith("#") ? hexString.slice(1) : hexString; + if (minHex.length > 4) return minHex; + + let expanded = ""; + for (const char of minHex) { + expanded += char + char; + } + return expanded; +}; + +export const clamp = (value: number | string, max: number): number => + Math.max(Math.min(Number(value) || 0, max), 0); + +export const formatDecimal = (value = 1): number => + Math.trunc(value) !== value ? Math.round(value * 100) / 100 : value; + +export const normalizeDegrees = (value = 0, round = false) => { + const fn = round ? Math.round : formatDecimal; + return fn(clamp(value, 360)) || 0; +}; + +export const normalizePercentage = (value = 0, round = false) => { + const fn = round ? Math.round : formatDecimal; + return fn(clamp(value, 100)) || 0; +}; + +export const normalize8Bit = (value = 0, round = false) => { + const fn = round ? Math.round : formatDecimal; + return fn(clamp(value, 255)) || 0; +}; + +export const normalizeAlpha: Utils.NormalizeFunction = (value = 1) => + formatDecimal(clamp(value, 1)); + +export function generateColorComponents( + alphaChannel: number = 1, + { minify, cssNext }: Dye.FormatOptions = {}, +): string[] { + let alpha = ""; + const space = !minify ? " " : ""; + const spacers = cssNext ? [" ", `${space}/${space}`] : [`,${space}`]; + const percent = !minify ? "%" : ""; + const suffix = alphaChannel === 1 || cssNext ? "" : "a"; + + if (alphaChannel !== 1) { + alpha = `${spacers[1] || spacers[0]}${alphaChannel}`; + } + + return [spacers[0], alpha, suffix, percent]; +} diff --git a/src/utils/pluginHelpers.ts b/src/utils/pluginHelpers.ts new file mode 100644 index 0000000..b145ad3 --- /dev/null +++ b/src/utils/pluginHelpers.ts @@ -0,0 +1,61 @@ +import type { Dye } from "../types"; + +/** + * Creates a new plugin with the specified name and methods. + * + * @param name the name of the plugin + * @param plugin the plugin function + * @returns the plugin with the specified name + */ +export const createPlugin = ( + name: K extends string ? (K extends keyof Dye.Properties ? never : K) : never, + plugin: T, +): T => Object.defineProperty(plugin, "name", { value: name }) as T; + +/** + * Extends the main Dye instance with custom methods. + * This function ensures that no main methods are overwritten by plugins. + * + * @param props the Dye instance properties + * @param plugins the custom plugins to add to the Dye instance + * @returns the extended Dye instance + */ +export const integratePlugins =

( + props: Dye.Properties

, + plugins?: P, +): Dye.Instance

=> { + for (const methodName in plugins) { + try { + if (typeof plugins[methodName] !== "function") { + throw new TypeError( + `Plugin '${methodName}' must be a function, received ${typeof plugins[methodName]}`, + ); + } + + if (methodName in props) continue; // Prevent overwriting main methods + + Object.defineProperties(props, { + // Add the plugin method to the Dye instance + [methodName]: { + value: (plugins[methodName] as Dye.PluginFunction).bind(props), + writable: false, + enumerable: false, + configurable: true, + }, + // Add the plugin name as a property, helpful for debugging + [`${methodName}.name`]: { + value: methodName, + writable: false, + enumerable: false, + configurable: true, + }, + }); + } catch (e) { + Object.assign(props, { + error: { message: (e as Error).message }, + }); + } + } + + return props as Dye.Instance

; +}; diff --git a/src/utils/validation.ts b/src/utils/validation.ts new file mode 100644 index 0000000..7205f6b --- /dev/null +++ b/src/utils/validation.ts @@ -0,0 +1,20 @@ +/** + * Checks if the input value is a dictionary-like object with key-value pairs. + * @param v - The value to check against. + * @return `true` if it is not an object, `false` otherwise. + */ +export const isObject = (v: unknown): boolean => + typeof v === "object" && v !== null && !Array.isArray(v); + +/** + * Checks if a value represents a number, including support for numerical strings. + * + * @param v - The value to be checked. + * @returns `true` if the value is a number or a numerical string, `false` otherwise. + */ +export const isColorValue = (v: unknown): boolean => { + if (typeof v === "number") return Number.isFinite(v); + if (typeof v === "string") + return isColorValue(Number.parseFloat(v)) && isColorValue(Number(v)); + return false; +}; diff --git a/test/__fixtures__/cmykColors.ts b/test/__fixtures__/cmykColors.ts deleted file mode 100644 index 198c5e4..0000000 --- a/test/__fixtures__/cmykColors.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { TestColors } from "."; - -export const cmykColors: TestColors["cmyk"] = { - black: { - object: { c: 0, m: 0, y: 0, k: 100, a: 1 }, - string: "cmyk(0%, 0%, 0%, 100%)", - }, - white: { - object: { c: 0, m: 0, y: 0, k: 0, a: 1 }, - string: "cmyk(0%, 0%, 0%, 0%)", - }, - red: { - object: { c: 0, m: 100, y: 100, k: 0, a: 1 }, - string: "cmyk(0%, 100%, 100%, 0%)", - }, - lime: { - object: { c: 100, m: 0, y: 100, k: 0, a: 1 }, - string: "cmyk(100%, 0%, 100%, 0%)", - }, - blue: { - object: { c: 100, m: 100, y: 0, k: 0, a: 1 }, - string: "cmyk(100%, 100%, 0%, 0%)", - }, - yellow: { - object: { c: 0, m: 0, y: 100, k: 0, a: 1 }, - string: "cmyk(0%, 0%, 100%, 0%)", - }, - aqua: { - object: { c: 100, m: 0, y: 0, k: 0, a: 1 }, - string: "cmyk(100%, 0%, 0%, 0%)", - }, - fuchsia: { - object: { c: 0, m: 100, y: 0, k: 0, a: 1 }, - string: "cmyk(0%, 100%, 0%, 0%)", - }, - orange: { - object: { c: 0, m: 33, y: 100, k: 0, a: 1 }, - string: "cmyk(0%, 33%, 100%, 0%)", - rgb: { r: 255, g: 170.85, b: 0, a: 1 }, - }, - brown: { - object: { c: 0, m: 75, y: 75, k: 35, a: 1 }, - string: "cmyk(0%, 75%, 75%, 35%)", - rgb: { r: 165.75, g: 41.44, b: 41.44, a: 1 }, - }, - lightgray: { - object: { c: 0, m: 0, y: 0, k: 17, a: 1 }, - string: "cmyk(0%, 0%, 0%, 17%)", - rgb: { r: 211.65, g: 211.65, b: 211.65, a: 1 }, - }, -}; diff --git a/test/__fixtures__/hexColors.ts b/test/__fixtures__/hexColors.ts deleted file mode 100644 index 9f63c91..0000000 --- a/test/__fixtures__/hexColors.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { TestColors } from "."; -import { rgbColors } from "./rgbColors"; - -export const hexColors: TestColors["hex"] = { - black: { - object: rgbColors.black.object, - string: "#000000", - }, - white: { - object: rgbColors.white.object, - string: "#ffffff", - }, - red: { - object: rgbColors.red.object, - string: "#ff0000", - }, - lime: { - object: rgbColors.lime.object, - string: "#00ff00", - }, - blue: { - object: rgbColors.blue.object, - string: "#0000ff", - }, - yellow: { - object: rgbColors.yellow.object, - string: "#ffff00", - }, - aqua: { - object: rgbColors.aqua.object, - string: "#00ffff", - }, - fuchsia: { - object: rgbColors.fuchsia.object, - string: "#ff00ff", - }, - orange: { - object: rgbColors.orange.object, - string: "#ffaa00", - }, - brown: { - object: rgbColors.brown.object, - string: "#a52a2a", - }, - lightgray: { - object: rgbColors.lightgray.object, - string: "#d3d3d3", - }, -}; diff --git a/test/__fixtures__/hslColors.ts b/test/__fixtures__/hslColors.ts deleted file mode 100644 index cf9bb84..0000000 --- a/test/__fixtures__/hslColors.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { TestColors } from "."; - -export const hslColors: TestColors["hsl"] = { - white: { - object: { h: 0, s: 0, l: 100, a: 1 }, - string: "hsl(0, 0%, 100%)", - }, - black: { - object: { h: 0, s: 0, l: 0, a: 1 }, - string: "hsl(0, 0%, 0%)", - }, - red: { - object: { h: 0, s: 100, l: 50, a: 1 }, - string: "hsl(0, 100%, 50%)", - }, - lime: { - object: { h: 120, s: 100, l: 50, a: 1 }, - string: "hsl(120, 100%, 50%)", - }, - blue: { - object: { h: 240, s: 100, l: 50, a: 1 }, - string: "hsl(240, 100%, 50%)", - }, - yellow: { - object: { h: 60, s: 100, l: 50, a: 1 }, - string: "hsl(60, 100%, 50%)", - }, - aqua: { - object: { h: 180, s: 100, l: 50, a: 1 }, - string: "hsl(180, 100%, 50%)", - }, - fuchsia: { - object: { h: 300, s: 100, l: 50, a: 1 }, - string: "hsl(300, 100%, 50%)", - }, - orange: { - object: { h: 40, s: 100, l: 50, a: 1 }, - string: "hsl(40, 100%, 50%)", - }, - brown: { - object: { h: 0, s: 59, l: 41, a: 1 }, - string: "hsl(0, 59%, 41%)", - rgb: { r: 166.23, g: 42.87, b: 42.87, a: 1 }, - }, - lightgray: { - object: { h: 0, s: 0, l: 83, a: 1 }, - string: "hsl(0, 0%, 83%)", - rgb: { r: 211.65, g: 211.65, b: 211.65, a: 1 }, - }, -}; diff --git a/test/__fixtures__/hsvColors.ts b/test/__fixtures__/hsvColors.ts deleted file mode 100644 index 72ab6dd..0000000 --- a/test/__fixtures__/hsvColors.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { TestColors } from "."; - -export const hsvColors: TestColors["hsv"] = { - white: { - object: { h: 0, s: 0, v: 100, a: 1 }, - string: "hsv(0, 0%, 100%)", - }, - black: { - object: { h: 0, s: 0, v: 0, a: 1 }, - string: "hsv(0, 0%, 0%)", - }, - red: { - object: { h: 0, s: 100, v: 100, a: 1 }, - string: "hsv(0, 100%, 100%)", - }, - lime: { - object: { h: 120, s: 100, v: 100, a: 1 }, - string: "hsv(120, 100%, 100%)", - }, - blue: { - object: { h: 240, s: 100, v: 100, a: 1 }, - string: "hsv(240, 100%, 100%)", - }, - yellow: { - object: { h: 60, s: 100, v: 100, a: 1 }, - string: "hsv(60, 100%, 100%)", - }, - aqua: { - object: { h: 180, s: 100, v: 100, a: 1 }, - string: "hsv(180, 100%, 100%)", - }, - fuchsia: { - object: { h: 300, s: 100, v: 100, a: 1 }, - string: "hsv(300, 100%, 100%)", - }, - orange: { - object: { h: 40, s: 100, v: 100, a: 1 }, - string: "hsv(40, 100%, 100%)", - }, - brown: { - object: { h: 0, s: 75, v: 65, a: 1 }, - string: "hsv(0, 75%, 65%)", - rgb: { r: 165.75, g: 41.44, b: 41.44, a: 1 }, - }, - lightgray: { - object: { h: 0, s: 0, v: 83, a: 1 }, - string: "hsv(0, 0%, 83%)", - rgb: { r: 211.65, g: 211.65, b: 211.65, a: 1 }, - }, -}; diff --git a/test/__fixtures__/index.ts b/test/__fixtures__/index.ts deleted file mode 100644 index 1d1d082..0000000 --- a/test/__fixtures__/index.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { Cmyk, ColorObject, Hsl, Hsv, Rgb } from "../../src/types"; -import { cmykColors } from "./cmykColors"; -import { hexColors } from "./hexColors"; -import { hslColors } from "./hslColors"; -import { hsvColors } from "./hsvColors"; -import { rgbColors } from "./rgbColors"; - -export type ColorNames = - | "black" - | "white" - | "red" - | "lime" - | "blue" - | "yellow" - | "aqua" - | "fuchsia" - | "orange" - | "brown" - | "lightgray"; - -export type ColorRepresentations = { - object: T; - string: string; - rgb?: Rgb; -}; - -export type FormatColorMap< - T extends ColorObject = Rgb, - C extends string = ColorNames, -> = Record>; - -export interface TestColors { - rgb: FormatColorMap; - hsl: FormatColorMap; - hsv: FormatColorMap; - cmyk: FormatColorMap; - hex: FormatColorMap; - withAlpha: (color: ColorObject, value?: number) => ColorObject; -} - -export const testColors: TestColors = { - withAlpha: (color, value = 0.5) => ({ ...color, a: value }), - hex: hexColors, - rgb: rgbColors, - hsl: hslColors, - hsv: hsvColors, - cmyk: cmykColors, -}; - -export type Formats = keyof typeof testColors; - -export type TestColorObject = ColorRepresentations & { colorName: string }; - -type TestColorsFunc = ( - format: Formats, - color: TestColorObject, - expectedRgb: Rgb, -) => void; - -export function forEachColorFormat< - N extends string, - F extends Omit, ->(name: N, func: TestColorsFunc, formats: F) { - it.each(formats)(name, format => { - const colors = testColors[format as keyof TestColors] as FormatColorMap; - - for (const [colorName, colorData] of Object.entries(colors)) { - const expectedRgb = colorData?.rgb || testColors.rgb[colorName].object; - const color = { ...colorData, colorName }; - - func(format, color, expectedRgb); - } - }); -} - -export function forEachMethod( - name: N, - func: (method: string) => void, - methods: M, -) { - it.each(methods)(name, method => { - func(method); - }); -} diff --git a/test/__fixtures__/rgbColors.ts b/test/__fixtures__/rgbColors.ts deleted file mode 100644 index 48b03a9..0000000 --- a/test/__fixtures__/rgbColors.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { TestColors } from "."; - -export const rgbColors: TestColors["rgb"] = { - black: { - object: { r: 0, g: 0, b: 0, a: 1 }, - string: "rgb(0, 0, 0)", - }, - white: { - object: { r: 255, g: 255, b: 255, a: 1 }, - string: "rgb(255, 255, 255)", - }, - red: { - object: { r: 255, g: 0, b: 0, a: 1 }, - string: "rgb(255, 0, 0)", - }, - lime: { - object: { r: 0, g: 255, b: 0, a: 1 }, - string: "rgb(0, 255, 0)", - }, - blue: { - object: { r: 0, g: 0, b: 255, a: 1 }, - string: "rgb(0, 0, 255)", - }, - yellow: { - object: { r: 255, g: 255, b: 0, a: 1 }, - string: "rgb(255, 255, 0)", - }, - aqua: { - object: { r: 0, g: 255, b: 255, a: 1 }, - string: "rgb(0, 255, 255)", - }, - fuchsia: { - object: { r: 255, g: 0, b: 255, a: 1 }, - string: "rgb(255, 0, 255)", - }, - orange: { - object: { r: 255, g: 170, b: 0, a: 1 }, - string: "rgb(255, 170, 0)", - }, - brown: { - object: { r: 165, g: 42, b: 42, a: 1 }, - string: "rgb(165, 42, 42)", - }, - lightgray: { - object: { r: 211, g: 211, b: 211, a: 1 }, - string: "rgb(211, 211, 211)", - }, -}; diff --git a/test/conversions/cmykConversions.test.ts b/test/conversions/cmykConversions.test.ts deleted file mode 100644 index f83cc4a..0000000 --- a/test/conversions/cmykConversions.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { cmykToRgb } from "../../src/conversions/cmykConversions"; - -describe("CMYK Color Conversion", () => { - describe("cmykToRgb", () => { - it("should convert basic CMYK colors to RGB", () => { - expect(cmykToRgb({ c: 0, m: 100, y: 100, k: 0, a: 1 })).toEqual({ - r: 255, - g: 0, - b: 0, - a: 1, - }); - expect(cmykToRgb({ c: 100, m: 0, y: 100, k: 0, a: 1 })).toEqual({ - r: 0, - g: 255, - b: 0, - a: 1, - }); - expect(cmykToRgb({ c: 100, m: 100, y: 0, k: 0, a: 1 })).toEqual({ - r: 0, - g: 0, - b: 255, - a: 1, - }); - }); - - it("should handle alpha values correctly", () => { - expect(cmykToRgb({ c: 0, m: 100, y: 100, k: 0, a: 0.5 })).toEqual({ - r: 255, - g: 0, - b: 0, - a: 0.5, - }); - }); - - it("should handle grayscale colors correctly", () => { - expect(cmykToRgb({ c: 0, m: 0, y: 0, k: 50, a: 1 })).toEqual({ - r: 127.5, - g: 127.5, - b: 127.5, - a: 1, - }); - }); - }); -}); diff --git a/test/conversions/hexConversions.test.ts b/test/conversions/hexConversions.test.ts deleted file mode 100644 index 9850974..0000000 --- a/test/conversions/hexConversions.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { hexToRgb } from "../../src/conversions/hexConversions"; - -describe("HEX Color Conversion", () => { - describe("hexToRgb", () => { - it("should convert basic 6-digit hex colors to RGB", () => { - expect(hexToRgb("ff0000")).toEqual({ r: 255, g: 0, b: 0, a: 1 }); - expect(hexToRgb("00ff00")).toEqual({ r: 0, g: 255, b: 0, a: 1 }); - expect(hexToRgb("0000ff")).toEqual({ r: 0, g: 0, b: 255, a: 1 }); - }); - - it("should handle 8-digit hex colors with alpha correctly", () => { - expect(hexToRgb("ff000080")).toEqual({ r: 255, g: 0, b: 0, a: 0.5 }); - expect(hexToRgb("00ff00ff")).toEqual({ r: 0, g: 255, b: 0, a: 1 }); - }); - }); -}); diff --git a/test/conversions/hslConversions.test.ts b/test/conversions/hslConversions.test.ts deleted file mode 100644 index e09fc02..0000000 --- a/test/conversions/hslConversions.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { hslToHsv, hslToRgb } from "../../src/conversions/hslConversions"; - -describe("HSL Color Conversions", () => { - describe("hslToHsv", () => { - it("should convert basic HSL colors to HSV", () => { - expect(hslToHsv({ h: 0, s: 100, l: 50, a: 1 })).toEqual({ - h: 0, - s: 100, - v: 100, - a: 1, - }); - expect(hslToHsv({ h: 120, s: 50, l: 62.5, a: 1 })).toEqual({ - h: 120, - s: 46.15, - v: 81.25, - a: 1, - }); - expect(hslToHsv({ h: 240, s: 25, l: 12.5, a: 1 })).toEqual({ - h: 240, - s: 40, - v: 15.63, - a: 1, - }); - }); - - it("should handle alpha values correctly", () => { - expect(hslToHsv({ h: 0, s: 100, l: 50, a: 0.5 })).toEqual({ - h: 0, - s: 100, - v: 100, - a: 0.5, - }); - }); - - it("should handle edge cases correctly", () => { - expect(hslToHsv({ h: 270, s: 0, l: 100, a: 0.5 })).toEqual({ - h: 270, - s: 0, - v: 100, - a: 0.5, - }); - - expect(hslToHsv({ h: 270, s: 100, l: 0, a: 0.5 })).toEqual({ - h: 270, - s: 0, - v: 0, - a: 0.5, - }); - }); - }); - - describe("hslToRgb", () => { - it("should convert basic HSL colors to RGB", () => { - expect(hslToRgb({ h: 0, s: 100, l: 50, a: 1 })).toEqual({ - r: 255, - g: 0, - b: 0, - a: 1, - }); - expect(hslToRgb({ h: 120, s: 50, l: 50, a: 1 })).toEqual({ - r: 63.74, - g: 191.25, - b: 63.74, - a: 1, - }); - expect(hslToRgb({ h: 240, s: 100, l: 50, a: 1 })).toEqual({ - r: 0, - g: 0, - b: 255, - a: 1, - }); - }); - - it("should handle alpha values correctly", () => { - expect(hslToRgb({ h: 0, s: 100, l: 50, a: 0.5 })).toEqual({ - r: 255, - g: 0, - b: 0, - a: 0.5, - }); - }); - - it("should handle grayscale colors correctly", () => { - expect(hslToRgb({ h: 0, s: 0, l: 0, a: 1 })).toEqual({ - r: 0, - g: 0, - b: 0, - a: 1, - }); - expect(hslToRgb({ h: 0, s: 0, l: 100, a: 1 })).toEqual({ - r: 255, - g: 255, - b: 255, - a: 1, - }); - }); - }); -}); diff --git a/test/conversions/hsvConversions.test.ts b/test/conversions/hsvConversions.test.ts deleted file mode 100644 index b56e32e..0000000 --- a/test/conversions/hsvConversions.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { hsvToHsl, hsvToRgb } from "../../src/conversions/hsvConversions"; - -describe("HSV Color Conversions", () => { - describe("hsvToHsl", () => { - it("should convert basic HSV colors to HSL", () => { - expect(hsvToHsl({ h: 0, s: 100, v: 100, a: 1 })).toEqual({ - h: 0, - s: 100, - l: 50, - a: 1, - }); - expect(hsvToHsl({ h: 120, s: 50, v: 75, a: 1 })).toEqual({ - h: 120, - s: 42.86, - l: 56.25, - a: 1, - }); - expect(hsvToHsl({ h: 240, s: 25, v: 25, a: 1 })).toEqual({ - h: 240, - s: 14.29, - l: 21.88, - a: 1, - }); - }); - - it("should handle alpha values correctly", () => { - expect(hsvToHsl({ h: 0, s: 100, v: 100, a: 0.5 })).toEqual({ - h: 0, - s: 100, - l: 50, - a: 0.5, - }); - }); - - it("should handle edge cases correctly", () => { - expect(hsvToHsl({ h: 270, s: 0, v: 100, a: 0.5 })).toEqual({ - h: 270, - s: 0, - l: 100, - a: 0.5, - }); - - expect(hsvToHsl({ h: 270, s: 100, v: 0, a: 0.5 })).toEqual({ - h: 270, - s: 0, - l: 0, - a: 0.5, - }); - }); - }); - - describe("hsvToRgb", () => { - it("should convert basic HSV colors to RGB", () => { - expect(hsvToRgb({ h: 0, s: 100, v: 100, a: 1 })).toEqual({ - r: 255, - g: 0, - b: 0, - a: 1, - }); - expect(hsvToRgb({ h: 120, s: 50, v: 75, a: 1 })).toEqual({ - r: 95.63, - g: 191.25, - b: 95.63, - a: 1, - }); - expect(hsvToRgb({ h: 240, s: 25, v: 25, a: 1 })).toEqual({ - r: 47.81, - g: 47.81, - b: 63.75, - a: 1, - }); - }); - - it("should handle alpha values correctly", () => { - expect(hsvToRgb({ h: 0, s: 100, v: 100, a: 0.5 })).toEqual({ - r: 255, - g: 0, - b: 0, - a: 0.5, - }); - }); - }); -}); diff --git a/test/conversions/rgbConversions.test.ts b/test/conversions/rgbConversions.test.ts deleted file mode 100644 index 1c9320a..0000000 --- a/test/conversions/rgbConversions.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { - rgbToCmyk, - rgbToHex, - rgbToHsl, - rgbToHsv, - rgbToNamedColor, -} from "../../src/conversions/rgbConversions"; -import { forEachColorFormat } from "../__fixtures__"; - -describe("RGB Color Conversions", () => { - describe("rgbToNamedColor", () => { - forEachColorFormat( - "should return the correct named color for basic colors", - (_, color, rgb) => { - expect(rgbToNamedColor(rgb)).toBe(color.colorName); - }, - ["rgb"], - ); - - it("should return the closest named color for non-exact matches", () => { - expect(rgbToNamedColor({ r: 250, g: 8, b: 7, a: 1 })).toBe("red"); - expect(rgbToNamedColor({ r: 8, g: 255, b: 7, a: 1 })).toBe("lime"); - expect(rgbToNamedColor({ r: 8, g: 7, b: 255, a: 1 })).toBe("blue"); - }); - }); - - describe("rgbToHex", () => { - forEachColorFormat( - "should convert basic rgb colors to %s", - (_, color, rgb) => { - expect(rgbToHex(rgb)).toBe(color.string.toUpperCase()); - }, - ["hex"], - ); - - it("should handle alpha values correctly", () => { - expect(rgbToHex({ r: 255, g: 0, b: 0, a: 0.5 })).toBe("#FF000080"); - }); - - it("should minify hex values when possible", () => { - expect(rgbToHex({ r: 255, g: 0, b: 0, a: 1 }, { minify: true })).toBe( - "#F00", - ); - }); - }); - - describe("rgbToHsv", () => { - it("should convert basic RGB colors to HSV", () => { - expect(rgbToHsv({ r: 255, g: 0, b: 0, a: 1 })).toEqual({ - h: 0, - s: 100, - v: 100, - a: 1, - }); - expect(rgbToHsv({ r: 0, g: 255, b: 0, a: 1 })).toEqual({ - h: 120, - s: 100, - v: 100, - a: 1, - }); - expect(rgbToHsv({ r: 0, g: 0, b: 255, a: 1 })).toEqual({ - h: 240, - s: 100, - v: 100, - a: 1, - }); - }); - - it("should handle alpha values correctly", () => { - expect(rgbToHsv({ r: 255, g: 0, b: 0, a: 0.5 })).toEqual({ - h: 0, - s: 100, - v: 100, - a: 0.5, - }); - }); - - it("should handle grayscale colors correctly", () => { - expect(rgbToHsv({ r: 128, g: 128, b: 128, a: 1 })).toEqual({ - h: 0, - s: 0, - v: 50.2, - a: 1, - }); - }); - }); - - describe("rgbToCmyk", () => { - it("should convert basic RGB colors to CMYK", () => { - expect(rgbToCmyk({ r: 255, g: 0, b: 0, a: 1 })).toEqual({ - c: 0, - m: 100, - y: 100, - k: 0, - a: 1, - }); - expect(rgbToCmyk({ r: 0, g: 255, b: 0, a: 1 })).toEqual({ - c: 100, - m: 0, - y: 100, - k: 0, - a: 1, - }); - expect(rgbToCmyk({ r: 0, g: 0, b: 255, a: 1 })).toEqual({ - c: 100, - m: 100, - y: 0, - k: 0, - a: 1, - }); - }); - - it("should handle alpha values correctly", () => { - expect(rgbToCmyk({ r: 255, g: 0, b: 0, a: 0.5 })).toEqual({ - c: 0, - m: 100, - y: 100, - k: 0, - a: 0.5, - }); - }); - - it("should handle grayscale colors correctly", () => { - expect(rgbToCmyk({ r: 128, g: 128, b: 128, a: 1 })).toEqual({ - c: 0, - m: 0, - y: 0, - k: 49.8, - a: 1, - }); - }); - }); - - describe("rgbToHsl", () => { - it("should convert basic RGB colors to HSL", () => { - expect(rgbToHsl({ r: 255, g: 0, b: 0, a: 1 })).toEqual({ - h: 0, - s: 100, - l: 50, - a: 1, - }); - expect(rgbToHsl({ r: 0, g: 255, b: 0, a: 1 })).toEqual({ - h: 120, - s: 100, - l: 50, - a: 1, - }); - expect(rgbToHsl({ r: 0, g: 0, b: 255, a: 1 })).toEqual({ - h: 240, - s: 100, - l: 50, - a: 1, - }); - }); - - it("should handle alpha values correctly", () => { - expect(rgbToHsl({ r: 255, g: 0, b: 0, a: 0.5 })).toEqual({ - h: 0, - s: 100, - l: 50, - a: 0.5, - }); - }); - - it("should handle grayscale colors correctly", () => { - expect(rgbToHsl({ r: 128, g: 128, b: 128, a: 1 })).toEqual({ - h: 0, - s: 0, - l: 50.2, - a: 1, - }); - }); - }); -}); diff --git a/test/core/accessibility.test.ts b/test/core/accessibility.test.ts deleted file mode 100644 index 54e260d..0000000 --- a/test/core/accessibility.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { - calculateContrastRatio, - contrastRatio, - relativeLuminance, -} from "../../src/core/accessibility"; -import { testColors } from "../__fixtures__"; - -describe("Accessibility functions", () => { - describe("relativeLuminance", () => { - it("should calculate luminance correctly for black", () => { - expect(relativeLuminance(testColors.rgb.black.object)).toBe(0); - }); - - it("should calculate luminance correctly for white", () => { - expect(relativeLuminance(testColors.rgb.white.object)).toBe(1); - }); - - it("should calculate luminance correctly for a lightgray", () => { - expect(relativeLuminance(testColors.rgb.lightgray.object)).toBeCloseTo( - 0.65, - 1, - ); - }); - }); - - describe("calculateContrastRatio", () => { - it("should calculate contrast ratio correctly for black and white", () => { - const blackLuminance = 0; - const whiteLuminance = 1; - expect(calculateContrastRatio(whiteLuminance, blackLuminance)).toBe(21); - }); - - it("should handle cases where L1 is less than L2", () => { - const L1 = 0.3; - const L2 = 0.7; - expect(calculateContrastRatio(L1, L2)).toBe( - calculateContrastRatio(L2, L1), - ); - }); - - it("should handle cases where L1 is equal to L2", () => { - const L1 = 0.5; - const L2 = 0.5; - expect(calculateContrastRatio(L1, L2)).toBe(1); - }); - - it("should handle cases where L1 is close to L2", () => { - const L1 = 0.4; - const L2 = 0.41; - expect(calculateContrastRatio(L1, L2)).toBeCloseTo(1.04, 1); - }); - - it("should handle cases where L1 is much higher than L2", () => { - const L1 = 0.9; - const L2 = 0.1; - expect(calculateContrastRatio(L1, L2)).toBeCloseTo(6.33, 1); - }); - - it("should handle cases where L1 is much lower than L2", () => { - const L1 = 0.1; - const L2 = 0.9; - expect(calculateContrastRatio(L1, L2)).toBeCloseTo(6.33, 1); - }); - }); - - describe("contrastRatio", () => { - it("should calculate contrast ratio correctly for two colors", () => { - const { white, black } = testColors.rgb; - expect(contrastRatio(white.object, black.object)).toBeCloseTo(21); - }); - - it("should calculate contrast ratio correctly for black and light gray", () => { - const { black, lightgray } = testColors.rgb; - expect(contrastRatio(lightgray.object, black.object)).toBeCloseTo( - 14.02, - 1, - ); - }); - - it("should calculate contrast ratio correctly for blue and yellow", () => { - const { yellow, blue } = testColors.rgb; - expect(contrastRatio(yellow.object, blue.object)).toBeCloseTo(8.0, 1); - }); - - it("should calculate contrast ratio correctly for red and lime", () => { - const { lime, red } = testColors.rgb; - expect(contrastRatio(lime.object, red.object)).toBeCloseTo(2.91, 1); - }); - }); -}); diff --git a/test/core/colorAdjustments.test.ts b/test/core/colorAdjustments.test.ts deleted file mode 100644 index 3516fbe..0000000 --- a/test/core/colorAdjustments.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { - alpha, - hue, - lighten, - modBy, - saturate, -} from "../../src/core/colorAdjustments"; -import { testColors } from "../__fixtures__"; - -const hslColor = testColors.hsl; - -describe("Color Adjustment Functions", () => { - describe("modBy", () => { - it("should modify a value by the given amount", () => { - expect(modBy(50, 0.2)).toBe(60); - expect(modBy(100, -0.3)).toBe(70); - }); - - it("should clamp the result to be within 0 and the specified maximum", () => { - expect(modBy(80, 0.5)).toBe(120); - expect(modBy(80, 1)).toBe(160); - expect(modBy(50, -1)).toBe(0); - }); - - it("should handle string input for value", () => { - // @ts-expect-error Ignores since the function can actually handle this - expect(modBy("80", 0.2)).toBe(96); - }); - - it("should return the original value if amount is NaN", () => { - expect(modBy(50, Number.NaN)).toBe(50); - // @ts-expect-error Ignores for testing of invalid amount - expect(modBy(50, "invalid")).toBe(50); - }); - - it("should round the result to two decimal places if necessary", () => { - expect(modBy(33.3333, 0.1)).toBe(36.67); - }); - }); - - describe("lighten", () => { - it("should lighten an HSL color by the default amount", () => { - const lightened = lighten(hslColor.blue.object, 0.1); - expect(lightened).toEqual({ h: 240, s: 100, l: 55, a: 1 }); - }); - - it("should lighten an HSL color by a specific amount", () => { - const lightened = lighten(hslColor.blue.object, 0.3); - expect(lightened).toEqual({ h: 240, s: 100, l: 65, a: 1 }); - }); - }); - - describe("saturate", () => { - it("should adjust saturation of HSL color", () => { - const newColor = saturate(hslColor.orange.object, 0.2); - expect(newColor).toEqual({ h: 40, s: 100, l: 50, a: 1 }); - }); - - it("should clamp saturation value between 0 and 1", () => { - const color = { h: 120, s: 150, l: 50, a: 1 }; - const newColor = saturate(color, 2); - expect(newColor).toEqual({ h: 120, s: 100, l: 50, a: 1 }); - }); - }); - - describe("hue", () => { - it("should adjust hue of HSL color", () => { - const newColor = hue(hslColor.orange.object, 0.2); - expect(newColor).toEqual({ h: 48, s: 100, l: 50, a: 1 }); - }); - - it("should wrap hue value around 360 degrees", () => { - const color = { h: 350, s: 50, l: 50, a: 1 }; - const newColor = hue(color, 0.2); - expect(newColor).toEqual({ h: 360, s: 50, l: 50, a: 1 }); - }); - }); - - describe("alpha", () => { - it("should adjust alpha channel of RGB color", () => { - const color = { r: 255, g: 128, b: 64, a: 0.5 }; - const newColor = alpha(color, 0.2); - expect(newColor).toEqual({ r: 255, g: 128, b: 64, a: 0.6 }); - }); - - it("should handle optional alpha channel", () => { - const color = { r: 255, g: 128, b: 64 }; - // @ts-expect-error It expects an alpha, but as safe guard should work without - const newColor = alpha(color, 0.2); - expect(newColor).toEqual({ r: 255, g: 128, b: 64, a: 1 }); - }); - }); -}); diff --git a/test/core/colorFormatter.test.ts b/test/core/colorFormatter.test.ts deleted file mode 100644 index cdb3128..0000000 --- a/test/core/colorFormatter.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import colorFormatter from "../../src/core/colorFormatter"; -import { forEachColorFormat } from "../__fixtures__"; - -describe("ColorFormatter", () => { - describe("ColorFormatter - Format Valid Color Object", () => { - forEachColorFormat( - "should format valid %s color to string", - (format, color) => { - expect(colorFormatter[format](color.object)).toBe(color.string); - }, - ["rgb", "cmyk", "hsl", "hsv"], - ); - }); -}); diff --git a/test/core/colorNormalizer.test.ts b/test/core/colorNormalizer.test.ts deleted file mode 100644 index 26af39f..0000000 --- a/test/core/colorNormalizer.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { - Clamp, - Round, - alpha, - degree, - eightBit, - percent, -} from "../../src/core/colorNormalizer"; - -describe("Color Normalizers", () => { - describe("Value Converters", () => { - describe("degree", () => { - it("should convert value to degree (0-360)", () => { - expect(degree(720)).toBe(360); - expect(degree(-120)).toBe(0); - expect(degree(180)).toBe(180); - }); - - it("should round value to nearest integer", () => { - expect(degree(180.5)).toBe(181); - }); - }); - - describe("percent", () => { - it("should convert value to percent (0-100)", () => { - expect(percent(200)).toBe(100); - expect(percent(-50)).toBe(0); - expect(percent(75)).toBe(75); - }); - - it("should round value to nearest integer", () => { - expect(percent(75.5)).toBe(76); - }); - }); - - describe("eightBit", () => { - it("should convert value to 8-bit integer (0-255)", () => { - expect(eightBit(512)).toBe(255); - expect(eightBit(-128)).toBe(0); - expect(eightBit(192)).toBe(192); - }); - - it("should round value to nearest integer", () => { - expect(eightBit(192.5)).toBe(193); - }); - }); - - describe("alpha", () => { - it("should convert value to alpha (0-1)", () => { - expect(alpha(2)).toBe(1); - expect(alpha(-0.5)).toBe(0); - expect(alpha(0.75)).toBe(0.75); - }); - - it("should precision value to 3 decimal places", () => { - expect(alpha(0.75321)).toBe(0.75); - }); - }); - }); - - describe("Color Object Normalizers", () => { - describe("Round", () => { - it("should round RGB values to nearest integer", () => { - const rgb = { r: 128.5, g: 64.2, b: 192.8, a: 0.5 }; - const roundedRgb = Round.rgb(rgb); - expect(roundedRgb).toEqual({ r: 129, g: 64, b: 193, a: 0.5 }); - }); - - it("should round HSL values to nearest integer", () => { - const hsl = { h: 120.5, s: 75.2, l: 50.1, a: 0.5 }; - const roundedHsl = Round.hsl(hsl); - expect(roundedHsl).toEqual({ h: 121, s: 75, l: 50, a: 0.5 }); - }); - - it("should round HSV values to nearest integer", () => { - const hsv = { h: 240.5, s: 80.2, v: 60.2, a: 0.5 }; - const roundedHsv = Round.hsv(hsv); - expect(roundedHsv).toEqual({ h: 241, s: 80, v: 60, a: 0.5 }); - }); - - it("should round CMYK values to nearest integer", () => { - const cmyk = { c: 50.5, m: 30.2, y: 20.8, k: 10.5, a: 0.5 }; - const roundedCmyk = Round.cmyk(cmyk); - expect(roundedCmyk).toEqual({ c: 51, m: 30, y: 21, k: 11, a: 0.5 }); - }); - }); - - describe("Clamp", () => { - it("should precision RGB values to 3 decimal places", () => { - const rgb = { r: 128.512, g: 64.234, b: 192.876, a: 0.512 }; - const clampedRgb = Clamp.rgb(rgb); - expect(clampedRgb).toEqual({ - r: 128.51, - g: 64.23, - b: 192.88, - a: 0.51, - }); - }); - - it("should precision HSL values to 3 decimal places", () => { - const hsl = { h: 120.512, s: 75.3, l: 51.2, a: 0.512 }; - const clampedHsl = Clamp.hsl(hsl); - expect(clampedHsl).toEqual({ h: 120.51, s: 75.3, l: 51.2, a: 0.51 }); - }); - - it("should precision HSV values to 3 decimal places", () => { - const hsv = { h: 240.512, s: 75.3, v: 51.2, a: 0.512 }; - const clampedHsv = Clamp.hsv(hsv); - expect(clampedHsv).toEqual({ h: 240.51, s: 75.3, v: 51.2, a: 0.51 }); - }); - - it("should precision CMYK values to 3 decimal places", () => { - const cmyk = { c: 50.512, m: 30.234, y: 20.876, k: 10.512, a: 0.512 }; - const clampedCmyk = Clamp.cmyk(cmyk); - expect(clampedCmyk).toEqual({ - c: 50.51, - m: 30.23, - y: 20.88, - k: 10.51, - a: 0.51, - }); - }); - - it("should clamp RGB values to valid range", () => { - const rgb = { r: 300, g: -10, b: 150, a: 2 }; - const clampedRgb = Clamp.rgb(rgb); - expect(clampedRgb).toEqual({ r: 255, g: 0, b: 150, a: 1 }); - }); - - it("should clamp HSL values to valid range", () => { - const hsl = { h: 420, s: -20, l: 10.21, a: 2 }; - const clampedHsl = Clamp.hsl(hsl); - expect(clampedHsl).toEqual({ h: 360, s: 0, l: 10.21, a: 1 }); - }); - - it("should clamp HSV values to valid range", () => { - const hsv = { h: 420, s: -20, v: 10.21, a: 2 }; - const clampedHsv = Clamp.hsv(hsv); - expect(clampedHsv).toEqual({ h: 360, s: 0, v: 10.21, a: 1 }); - }); - - it("should clamp CMYK values to valid range", () => { - const cmyk = { c: 150, m: -20, y: 50, k: 200, a: 2 }; - const clampedCmyk = Clamp.cmyk(cmyk); - expect(clampedCmyk).toEqual({ c: 100, m: 0, y: 50, k: 100, a: 1 }); - }); - }); - }); -}); diff --git a/test/core/colorParser.test.ts b/test/core/colorParser.test.ts deleted file mode 100644 index 7901c72..0000000 --- a/test/core/colorParser.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { parseColor } from "../../src/core/colorParser"; -import { forEachColorFormat } from "../__fixtures__"; - -describe("parseColor Function", () => { - describe("Handling Error", () => { - it("should return null for an invalid color string", () => { - expect(parseColor("invalidcolor")).toBeNull(); - }); - - it("should return null for an empty string", () => { - expect(parseColor("")).toBeNull(); - }); - - it("should return null for undefined input", () => { - expect(parseColor()).toBeNull(); - expect(parseColor(undefined)).toBeNull(); - }); - }); - - describe("Color String Parsing", () => { - forEachColorFormat( - "should parse from a %s string", - (format, color) => { - expect(parseColor(color.string)).toEqual({ - originalInput: color.string, - isValid: true, - value: color.object, - format, - }); - }, - ["hex", "rgb", "hsl", "hsv", "cmyk"], - ); - }); -}); diff --git a/test/core/colorTypeAnalyzer.test.ts b/test/core/colorTypeAnalyzer.test.ts deleted file mode 100644 index b935f45..0000000 --- a/test/core/colorTypeAnalyzer.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { - determineColorType, - execColorStringTest, - isCmykObject, - isColorData, - isHslObject, - isHsvObject, - isRgbObject, -} from "../../src/core/colorTypeAnalyzer"; -import { forEachColorFormat, testColors } from "../__fixtures__"; - -describe("Color Type Analyzis", () => { - describe("Helper Functions", () => { - describe("isColorData", () => { - it("should return true for a valid color data object", () => { - const colorData = { format: "hex", value: "#ff0000" }; - expect(isColorData(colorData)).toBeTruthy(); - }); - - it('should return true for a color data object with "value" property', () => { - const colorData = { value: "rgb(255, 0, 0)" }; - expect(isColorData(colorData)).toBeTruthy(); - }); - - it('should return true for a color data object with "isValid" property', () => { - const colorData = { isValid: true }; - expect(isColorData(colorData)).toBeTruthy(); - }); - - it('should return true for a color data object with "originalInput" property', () => { - const colorData = { originalInput: "hsl(360, 0, 100)" }; - expect(isColorData(colorData)).toBeTruthy(); - }); - - it("should return false for an invalid color data object", () => { - const colorData = { foo: "bar" }; - expect(isColorData(colorData)).toBeFalsy(); - }); - }); - - describe("execColorStringTest", () => { - describe("Handling Error", () => { - it("should return null for an invalid color string", () => { - const result = execColorStringTest("invalidcolor"); - expect(result).toBeNull(); - }); - - it("should return null for an empty string", () => { - const result = execColorStringTest(""); - expect(result).toBeNull(); - }); - }); - - describe("Default Behavior", () => { - forEachColorFormat( - "should recognize %s color string", - (format, color) => { - color.object.a = undefined; - - const res = execColorStringTest(color.string); - - expect(res![0]).toBe(format); - expect(res![1][0]).toEqual(color.string); - }, - ["cmyk", "hex", "hsl", "hsv", "rgb"], - ); - }); - }); - }); - - describe("Color Type Identification", () => { - describe("determineColorType Function", () => { - describe("Handling Error", () => { - it("should return undefined for an invalid color object", () => { - const invalidColor = { x: 10, y: 20 }; - expect(determineColorType(invalidColor)).toBeUndefined(); - }); - - it("should return undefined for null or undefined input", () => { - // @ts-expect-error - expect(determineColorType(null)).toBeUndefined(); - expect(determineColorType(undefined)).toBeUndefined(); - }); - }); - - describe("Default Behavior", () => { - forEachColorFormat( - "should identify %s color object", - (format, color) => { - expect(determineColorType(color.object)).toBe(format); - }, - ["rgb", "hsl", "hsv", "cmyk"], - ); - }); - }); - - describe("isRgbObject Function", () => { - it("should return true for valid RGB color objects", () => { - expect(isRgbObject(testColors.rgb.red.object)).toBeTruthy(); - expect( - isRgbObject(testColors.withAlpha(testColors.rgb.red.object)), - ).toBeTruthy(); - }); - - it("should return false for invalid RGB color objects", () => { - expect(isRgbObject({ r: 255, g: 0 })).toBeFalsy(); - expect(isRgbObject({ r: "255", g: 0, b: 0 })).toBeFalsy(); - expect(isRgbObject({ h: 0, s: 100, l: 50 })).toBeFalsy(); - }); - }); - - describe("isHslObject Function", () => { - it("should return true for valid HSL color objects", () => { - expect(isHslObject(testColors.hsl.red.object)).toBeTruthy(); - expect( - isHslObject(testColors.withAlpha(testColors.hsl.red.object)), - ).toBeTruthy(); - }); - - it("should return false for invalid HSL color objects", () => { - expect(isHslObject({ h: 255, s: 0 })).toBeFalsy(); - expect(isHslObject({ h: "255", s: 0, l: 0 })).toBeFalsy(); - expect(isHslObject({ r: 0, g: 100, b: 50 })).toBeFalsy(); - }); - }); - - describe("isHsvObject Function", () => { - it("should return true for valid HSV color objects", () => { - expect(isHsvObject(testColors.hsv.red.object)).toBeTruthy(); - expect( - isHsvObject(testColors.withAlpha(testColors.hsv.red.object)), - ).toBeTruthy(); - }); - - it("should return false for invalid HSV color objects", () => { - expect(isHsvObject({ h: 255, s: 0 })).toBeFalsy(); - expect(isHsvObject({ h: "255", s: 0, v: 0 })).toBeFalsy(); - expect(isHsvObject({ r: 0, g: 100, b: 50 })).toBeFalsy(); - }); - }); - - describe("isCmykObject Function", () => { - it("should return true for valid CMYK color objects", () => { - expect(isCmykObject(testColors.cmyk.aqua.object)).toBeTruthy(); - expect( - isCmykObject(testColors.withAlpha(testColors.cmyk.aqua.object)), - ).toBeTruthy(); - }); - - it("should return false for invalid CMYK color objects", () => { - expect(isCmykObject({ c: 0, m: 100, y: 100 })).toBeFalsy(); - expect(isCmykObject({ c: "0", m: 100, y: 100, k: 0 })).toBeFalsy(); - expect(isCmykObject({ r: 0, g: 100, b: 50 })).toBeFalsy(); - }); - }); - }); -}); diff --git a/test/core/conversionHelpers.test.ts b/test/core/conversionHelpers.test.ts deleted file mode 100644 index 23599b8..0000000 --- a/test/core/conversionHelpers.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { - computeColorDistance, - computeHsvHue, - isRgbShortanable, -} from "../../src/core/conversionHelpers"; -import { testColors } from "../__fixtures__"; - -const { red, brown, blue, aqua, lime, white, black } = testColors.rgb; - -describe("Conversion Helper Functions", () => { - describe("Color String Shortening", () => { - describe("isRgbShortanable Function", () => { - it("should return false for non-shortenable RGB colors", () => { - expect(isRgbShortanable(128, 128, 128)).toBeFalsy(); - expect(isRgbShortanable(34, 128, 17)).toBeFalsy(); - expect(isRgbShortanable(17, 34, 170, 150)).toBeFalsy(); - }); - - it("should return true for shortenable RGB colors", () => { - expect(isRgbShortanable(34, 51, 17)).toBeTruthy(); - expect(isRgbShortanable(0, 170, 187)).toBeTruthy(); - expect(isRgbShortanable(255, 0, 0)).toBeTruthy(); - expect(isRgbShortanable(0, 255, 0)).toBeTruthy(); - expect(isRgbShortanable(0, 0, 255)).toBeTruthy(); - expect(isRgbShortanable(17, 34, 170, 153)).toBeTruthy(); - }); - - it("should handle the default alpha value of 255", () => { - expect(isRgbShortanable(34, 51, 17)).toBeTruthy(); - }); - }); - }); - - describe("Color Distance Calculation", () => { - describe("computeColorDistance function", () => { - it("should calculate the distance between two identical colors as 0", () => { - expect(computeColorDistance(red.object, red.object)).toBe(0); - }); - - it("should calculate the distance between black and white", () => { - // Euclidean distance in 3D space: sqrt((255-0)^2 + (255-0)^2 + (255-0)^2) = sqrt(3*255^2) = 441.67 - expect(computeColorDistance(black.object, white.object)).toBeCloseTo( - 441.67, - 2, - ); - }); - - it("should calculate the distance between two similar colors", () => { - expect(computeColorDistance(red.object, brown.object)).toBeCloseTo( - 107.83, - 1, - ); - }); - - it("should handle colors with alpha values (ignoring alpha)", () => { - const redWithAlpha = { ...red.object, a: 0.65 }; - expect(computeColorDistance(red.object, redWithAlpha)).toBe(0); - }); - }); - }); - - describe("HSV Hue Calculation", () => { - describe("computeHsvHue function", () => { - it("should return 0 when segment is 0", () => { - const params = { segment: 0, maxRgb: 255, minRgb: 255 }; - expect(computeHsvHue(white.object, params)).toBe(0); - }); - - it("should calculate hue correctly when red is max", () => { - const params = { segment: 255, maxRgb: 255, minRgb: 0 }; - expect(computeHsvHue(red.object, params)).toBe(0); - }); - - it("should calculate hue correctly when green is max", () => { - const params = { segment: 255, maxRgb: 255, minRgb: 0 }; - expect(computeHsvHue(lime.object, params)).toBe(120); - }); - - it("should calculate hue correctly when blue is max", () => { - const params = { segment: 255, maxRgb: 255, minRgb: 0 }; - expect(computeHsvHue(blue.object, params)).toBe(240); - }); - - it("should handle intermediate colors", () => { - const params = { segment: 128, maxRgb: 128, minRgb: 0 }; - expect(computeHsvHue(brown.object, params)).toBeCloseTo(297.65, 1); - }); - - it("should normalize negative hue values", () => { - const params = { segment: 255, maxRgb: 255, minRgb: 0 }; - const expectedHue = 180; - expect(computeHsvHue(aqua.object, params)).toBe(expectedHue); - }); - }); - }); -}); diff --git a/test/core/inputSerializer.test.ts b/test/core/inputSerializer.test.ts deleted file mode 100644 index 10cef6f..0000000 --- a/test/core/inputSerializer.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { - fallbackColor, - fromObject, - processColorInput, -} from "../../src/core/inputSerializer"; -import type { AnyObject } from "../../src/types"; -import { forEachColorFormat, testColors } from "../__fixtures__"; - -describe("Color Input Processing", () => { - describe("fromObject Helper", () => { - describe("Error Handling", () => { - it("should throw error for null or undefined input", () => { - expect(() => fromObject(null)).toThrow(); - expect(() => fromObject(undefined)).toThrow(); - }); - - it("should throw error for unsupported color types", () => { - const input: AnyObject = { x: 100, y: 200 }; - expect(() => fromObject(input)).toThrow(); - }); - }); - }); - - describe("processColorInput Function", () => { - describe("Error Handling", () => { - it("should throw TypeError for invalid color strings", () => { - const input = "invalid color"; - expect(() => processColorInput(input)).toThrow(TypeError); - }); - - it("should throw TypeError for invalid color objects", () => { - const input: AnyObject = { x: 100, y: 200 }; - expect(() => processColorInput(input)).toThrow(TypeError); - }); - }); - - describe("Default Behavior", () => { - it("should return fallbackColor for undefined input", () => { - expect(processColorInput(undefined)).toEqual(fallbackColor); - }); - - it("should handle ColorData input and return the same ColorData", () => { - const input = { - isValid: true, - format: "rgb", - value: testColors.rgb.white.object, - }; - - const expectedOutput = { - originalInput: input.value, - isValid: true, - value: input.value, - format: input.format, - }; - - expect(processColorInput(input)).toEqual(expectedOutput); - }); - }); - - describe("Color Conversions", () => { - describe("Object Type Conversions", () => { - forEachColorFormat( - "should handle valid %s color object", - (format, color, expectedRgb) => { - const result = processColorInput(color.object); - - expect(result.format).toBe(format); - expect(result.isValid).toBeTruthy(); - expect(result.value).toEqual(expectedRgb); - expect(result.originalInput).toEqual(color.object); - }, - ["rgb", "hsl", "hsv", "cmyk"], - ); - }); - - describe("String Type Conversions", () => { - forEachColorFormat( - "should handle valid %s color strings", - (format, color, expectedRgb) => { - const result = processColorInput(color.string); - - expect(result.format).toBe(format); - expect(result.isValid).toBeTruthy(); - expect(result.value).toEqual(expectedRgb); - expect(result.originalInput).toEqual(color.string); - }, - ["hex", "rgb", "hsl", "hsv", "cmyk"], - ); - }); - }); - }); -}); diff --git a/test/core/pluginValidation.test.ts b/test/core/pluginValidation.test.ts deleted file mode 100644 index 045335a..0000000 --- a/test/core/pluginValidation.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { isValidPlugin } from "../../src/core/pluginValidation"; - -describe("Plugin Validation", () => { - describe("isValidPlugin function", () => { - it("should correctly identify an valid plugin", () => { - const plugins = { - myPlugin: function () { - return this.object; - }, - }; - - expect(isValidPlugin(plugins, "myPlugin")).toBeTruthy(); - }); - - it("should correctly identify an invalid plugin", () => { - const plugins = { - notFunction: "hello", - }; - - // @ts-expect-error should report error for invalid type - expect(() => isValidPlugin(plugins, "notFunction")).toThrow(); - }); - }); -}); diff --git a/test/dye.test.ts b/test/dye.test.ts index 26e1365..a380ca2 100644 --- a/test/dye.test.ts +++ b/test/dye.test.ts @@ -1,161 +1,252 @@ -import { dye } from "../src/dye"; +import { + type Dye, + createPlugin, + dye, + hslParser, + lighten, + saturate, + toHsl, + toHsv, + toRgb, +} from "../src/main"; + +const redColor = { r: 255, g: 0, b: 0, a: 1 }; +const blackColor = { r: 0, g: 0, b: 0, a: 1 }; +const whiteColor = { r: 255, g: 255, b: 255, a: 1 }; +const invalidColorString = "invalid color"; +const invalidRgbObject = { r: 300, g: -20, b: "invalid" }; +const invalidRgbString = "rgb(300, -20, invalid)"; +const hslColorString = "hsl(0, 60%, 50%)"; +const customHslInput = "hsl(0, 100%, 50%)"; +const customPluginColor = "rgb(255, 0, 0)"; +const chainColor = { r: 128, g: 100, b: 100, a: 1 }; +const expectedResults = { + lightenedColor: { r: 140.81, g: 110.02, b: 110.02, a: 1 }, + saturatedColor: { r: 129.41, g: 98.61, b: 98.61, a: 1 }, + lightenedAndSaturatedColor: { r: 142.34, g: 108.46, b: 108.46, a: 1 }, +}; + +describe("dye function", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); -import { forEachColorFormat, forEachMethod, testColors } from "./__fixtures__"; + describe("Color Calculations", () => { + it("should calculate the correct luminance for a given color", () => { + expect(dye(redColor).luminance).toBe(0.21); // Luminance for red + expect(dye(blackColor).luminance).toBe(0); + expect(dye(whiteColor).luminance).toBe(1); + }); -describe("dye Function", () => { - describe("Error Handling", () => { - it("should throw an error for invalid input", () => { - expect(() => dye("invalid color")).toThrow(TypeError); - // @ts-expect-error Should ignore error for invalid color format - expect(() => dye({ x: 10, y: 20 })).toThrow(TypeError); + it("should calculate the correct HSL values for a given color", () => { + expect(dye(redColor).hsl).toEqual({ h: 0, s: 100, l: 50, a: 1 }); }); - }); - describe("Color Instance Properties and Methods", () => { - it("should calculate luminance correctly", () => { - const color = dye("#ff0000"); - expect(color.luminance).toBeCloseTo(0.2126); + it("should calculate the correct HSV values for a given color", () => { + const color = dye(redColor); + expect(color.hsv).toEqual({ h: 0, s: 100, v: 100, a: 1 }); }); - describe("Object Representations (Conversions)", () => { - forEachMethod( - "should make the Color.%s() getter public", - method => { - const color = testColors[method].white; - const result = dye(color.string); + it("should calculate the correct CMYK values for a given color", () => { + const color = dye(redColor); + expect(color.cmyk).toEqual({ c: 0, m: 100, y: 100, k: 0, a: 1 }); + }); - expect(result[method]).toEqual(color.object); - }, - ["rgb", "hsl", "hsv", "cmyk"], - ); + it("should return the correct RGB values for a given color", () => { + const color = dye(redColor); + expect(color.rgb).toEqual({ r: 255, g: 0, b: 0, a: 1 }); }); - describe("String Representations (Conversions)", () => { - const methods = { - toRgb: "rgb", - toHsl: "hsl", - toHsv: "hsv", - toCmyk: "cmyk", - }; - forEachMethod( - "should make the Color.%s() getter public", - method => { - const color = testColors[methods[method]].white; - const result = dye(color.object); - - expect(result[method]()).toEqual(color.string); - }, - ["toRgb", "toHsl", "toHsv", "toCmyk"], - ); + it("should return the correct alpha value for a given color", () => { + const color = dye("rgb(255, 0, 0, 0.5)"); + expect(color.alpha).toBe(0.5); }); - describe("Named Colors (Conversions)", () => { - forEachColorFormat( - "should convert from %s to named color", - (_, color) => { - const result = dye(color.string); - expect(result.toNamed()).toBe(color.colorName); - }, - ["hex", "rgb", "hsl", "hsv", "cmyk"], + it("should return the correct hue value for a given color", () => { + const color = dye( + { h: 243, s: 50, l: 30, a: 1 }, + { parsers: [hslParser] }, ); + expect(color.hue).toBe(243); }); + }); - describe("Adjustments Methods", () => { - forEachMethod( - "should make the Color.%s() method public", - method => { - const color = dye("#fff"); - expect(color).toHaveProperty(method); - }, - [ - "lighten", - "darken", - "saturate", - "desaturate", - "hue", - "alpha", - "contrastRatio", - ], + describe("Error Handling", () => { + it("should return an error object when the color string parsing fails", () => { + const invalidColor = dye(invalidColorString); + expect(invalidColor.error?.message).toMatch( + "Failed to parse color input:", ); }); + + it("should handle non-object type input types gracefully", () => { + const color = dye([] as unknown as string); + expect(color.error?.message).toMatch("Failed to parse color input:"); + }); + + it("should handle null input gracefully", () => { + const color = dye(null as unknown as string); + expect(color.error?.message).toMatch("No color input provided"); + }); + + it("should handle undefined input gracefully", () => { + const color = dye(undefined as unknown as string); + expect(color.error?.message).toMatch("No color input provided"); + }); + + it("should handle empty string input gracefully", () => { + const color = dye(""); + expect(color.error?.message).toMatch("No color input provided"); + }); + + it("should handle non-string and non-object input gracefully", () => { + // @ts-expect-error Testing invalid input + const color = dye(123); + expect(color.error?.message).toMatch("Failed to parse color input:"); + }); + + it("should handle invalid RGB object input gracefully", () => { + const color = dye(invalidRgbObject as unknown as string); + expect(color.error?.message).toMatch("Failed to parse color input:"); + }); + + it("should handle invalid color format gracefully", () => { + const color = dye(invalidRgbString); + expect(color.error?.message).toMatch("Failed to parse color input:"); + }); + + it("should handle known color format without parsers gracefully", () => { + const color = dye(hslColorString); + expect(color.error?.message).toMatch("Failed to parse color input:"); + }); }); - describe("Color Instance Creation", () => { - describe("Object Type", () => { - forEachColorFormat( - "should create a Color instance from a %s string", - (format, color, expectedRgb) => { - const instance = dye(color.object); - - expect(instance.isValid).toBeTruthy(); - expect(instance.format).toBe(format); - expect(instance.originalInput).toBe(color.object); - expect(instance.value).toEqual(expectedRgb); - }, - ["rgb", "hsl", "hsv", "cmyk"], - ); + describe("Custom Parsers", () => { + it("should use custom parsers correctly", () => { + const result = dye(customHslInput, { parsers: [hslParser] }); + + expect(result.source).toEqual({ + value: { h: "0", s: "100", l: "50", a: undefined }, + model: "hsl", + isValid: true, + }); + expect(result.rgb).toEqual({ r: 255, g: 0, b: 0, a: 1 }); }); + }); - describe("String Type", () => { - forEachColorFormat( - "should create a Color instance from a %s string", - (format, color, expectedRgb) => { - const instance = dye(color.string); + describe("Custom Plugins", () => { + it("should expose custom plugins correctly", () => { + const customPlugin = createPlugin("customPlugin", function () { + return `Custom plugin called with color: ${this.rgb.r}, ${this.rgb.g}, ${this.rgb.b}`; + }); - expect(instance.isValid).toBeTruthy(); - expect(instance.format).toBe(format); - expect(instance.originalInput).toBe(color.string); - expect(instance.value).toEqual(expectedRgb); - }, - ["hex", "rgb", "hsl", "hsv", "cmyk"], + const color = dye(customPluginColor, { + plugins: { customPlugin }, + }); + + expect(color.customPlugin()).toBe( + "Custom plugin called with color: 255, 0, 0", ); }); }); - describe("Plugin Functionality", () => { - forEachColorFormat( - "should add a plugin for a %s string Color instance", - (_, color) => { - const result = dye(color.string, { - plugins: { - getHue: function () { - return this.hsl.h; - }, - }, - }); - - expect(result).toHaveProperty("getHue"); - expect(typeof result.getHue).toBe("function"); - }, - ["hex", "rgb", "hsl", "hsv", "cmyk"], - ); - - it("should allow the plugin method to access the Colorus instance data", () => { - const color = dye("rgb(20, 120, 80)", { + describe("Chaining Plugin Functions", () => { + const chain = createPlugin("chain", function () { + return dye(this.rgb, this.options); + }); + + let o: Dye.Instance<{ + lighten: typeof lighten; + chain: typeof chain; + saturate: typeof saturate; + }>; + + beforeEach(() => { + o = dye(chainColor, { plugins: { - getHue: function () { - return this.hsl.h; - }, + chain, + lighten, + saturate, }, }); + }); - expect(color.getHue()).toBe(156); + it("should return the original color", () => { + expect(o.rgb).toEqual(chainColor); }); - it("should handle multiple plugin methods", () => { - const color = dye("#FF0000", { - plugins: { - getHue: function () { - return this.hsl.h; - }, - isRed: function () { - return this.rgb.r === 255 && this.rgb.g === 0 && this.rgb.b === 0; - }, - }, - }); + it("should lighten the color", () => { + expect(o.lighten().rgb).toEqual(expectedResults.lightenedColor); + }); + + it("should saturate the color", () => { + expect(o.chain().saturate().rgb).toEqual(expectedResults.saturatedColor); + }); + + it("should chain saturate calls", () => { + expect(o.chain().chain().saturate().rgb).toEqual( + expectedResults.saturatedColor, + ); + }); + + it("should chain lighten calls", () => { + expect(o.chain().chain().chain().lighten().rgb).toEqual( + expectedResults.lightenedColor, + ); + }); + + it("should chain lighten and saturate calls", () => { + expect(o.chain().lighten().chain().chain().rgb).toEqual( + expectedResults.lightenedColor, + ); + expect(o.lighten().chain().saturate().chain().rgb).toEqual( + expectedResults.lightenedAndSaturatedColor, + ); + }); + }); + + describe("Repeated Use of Color with Edge Cases", () => { + const input = "rgb(100 100 200)"; + it("should handle HSL color string with different format options", () => { + const minifyEnable = dye(input, { + plugins: { toHsl }, + formatOptions: { minify: true }, + }).toHsl(); + const cssNextEnable = dye(input, { + plugins: { toHsl }, + formatOptions: { cssNext: true }, + }).toHsl(); + + expect(minifyEnable).toBe("hsl(240,48,59)"); + expect(cssNextEnable).toBe("hsl(240 48% 59%)"); + }); + + test("should handle HSV color string with different format options", () => { + const minifyEnable = dye(input, { + plugins: { toHsv }, + formatOptions: { minify: true }, + }).toHsv(); + const cssNextEnable = dye(input, { + plugins: { toHsv }, + formatOptions: { cssNext: true }, + }).toHsv(); + + expect(minifyEnable).toBe("hsv(240,50,78)"); + expect(cssNextEnable).toBe("hsv(240 50% 78%)"); + }); - expect(color.getHue()).toBe(0); - expect(color.isRed()).toBeTruthy(); + test("should handle RGB color string with different format options", () => { + const minifyEnable = dye(input, { + plugins: { toRgb }, + formatOptions: { minify: true }, + }).toRgb(); + const cssNextEnable = dye(input, { + plugins: { toRgb }, + formatOptions: { cssNext: true }, + }).toRgb(); + + expect(minifyEnable).toBe("rgb(100,100,200)"); + expect(cssNextEnable).toBe("rgb(100 100 200)"); }); }); }); diff --git a/test/helpers.test.ts b/test/helpers.test.ts deleted file mode 100644 index f7a4e0d..0000000 --- a/test/helpers.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { - hexString, - isObject, - nan, - padString, - precision, - utmost, -} from "../src/helpers"; - -describe("Helper Functions", () => { - describe("Number Utilities", () => { - describe("utmost", () => { - it("should clamp values within the specified range", () => { - expect(utmost(50, 100)).toBe(50); - expect(utmost(150, 100)).toBe(100); - expect(utmost(-50, 100)).toBe(0); - expect(utmost("75", 100)).toBe(75); - expect(utmost("120", 100)).toBe(100); - }); - }); - - describe("precision", () => { - it("should round to two decimal places", () => { - expect(precision(3.1419)).toBe(3.14); - expect(precision(12.3456)).toBe(12.35); - expect(precision(10)).toBe(10); - }); - }); - }); - - describe("String Utilities", () => { - describe("hexString", () => { - it("should convert numbers to uppercase hex strings with padding", () => { - expect(hexString(255, 2)).toBe("FF"); - expect(hexString(255, 4)).toBe("00FF"); - expect(hexString(4095, 3)).toBe("FFF"); - expect(hexString(4095, 6)).toBe("000FFF"); - }); - }); - - describe("padString", () => { - it("should pad short hex strings", () => { - expect(padString("abc")).toBe("aabbcc"); - expect(padString("f0f")).toBe("ff00ff"); - expect(padString("123a")).toBe("112233aa"); - }); - - it("should not pad long hex strings", () => { - expect(padString("abcdef")).toBe("abcdef"); - expect(padString("12345678")).toBe("12345678"); - }); - }); - }); - - describe("Type Checks", () => { - describe("nan", () => { - it("should correctly identify NaN values", () => { - expect(nan(Number.NaN)).toBeTruthy(); - expect(nan("not a number")).toBeTruthy(); - expect(nan(Number.POSITIVE_INFINITY)).toBeTruthy(); - expect(nan(Number.NEGATIVE_INFINITY)).toBeTruthy(); - expect(nan(123)).toBeFalsy(); - expect(nan(0)).toBeFalsy(); - expect(nan(3.14)).toBeFalsy(); - }); - }); - - describe("isObject", () => { - it("should correctly identify objects", () => { - expect(isObject({})).toBeTruthy(); - expect(isObject({ a: 1, b: "hello" })).toBeTruthy(); - expect(isObject([])).toBeFalsy(); - expect(isObject(null)).toBeFalsy(); - expect(isObject(undefined)).toBeFalsy(); - expect(isObject("string")).toBeFalsy(); - expect(isObject(123)).toBeFalsy(); - }); - }); - }); -}); diff --git a/test/isValidColor.test.ts b/test/isValidColor.test.ts deleted file mode 100644 index ea4d0cd..0000000 --- a/test/isValidColor.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { isValidColor } from "../src/isValidColor"; -import { forEachColorFormat } from "./__fixtures__"; - -describe("isValidColor", () => { - it("should return null for an invalid color object", () => { - expect(isValidColor({ x: 10, y: 20 })).toBeNull(); - }); - - it("should return null for an invalid color string", () => { - expect(isValidColor("invalidcolor")).toBeNull(); - }); - - it("should return null for an empty string", () => { - expect(isValidColor("")).toBeNull(); - }); - - it("should return null for undefined", () => { - expect(isValidColor(undefined)).toBeNull(); - }); - - it("should return null for null", () => { - expect(isValidColor(null)).toBeNull(); - }); - - describe("Validation - Color String", () => { - forEachColorFormat( - "should parse from a %s string", - (format, color) => expect(isValidColor(color.string)).toBe(format), - ["hex", "rgb", "hsl", "hsv", "cmyk"], - ); - }); - - describe("Validation - Color Object", () => { - forEachColorFormat( - "should parse from a %s string", - (format, color) => expect(isValidColor(color.object)).toBe(format), - ["rgb", "hsl", "hsv", "cmyk"], - ); - }); -}); diff --git a/test/main.test.ts b/test/main.test.ts new file mode 100644 index 0000000..11d7b60 --- /dev/null +++ b/test/main.test.ts @@ -0,0 +1,87 @@ +import * as main from "../src/main"; + +describe("main exports", () => { + it("should export dye", () => { + expect(main.dye).toBeDefined(); + }); + + it("should export cmykParser", () => { + expect(main.cmykParser).toBeDefined(); + }); + + it("should export hexParser", () => { + expect(main.hexParser).toBeDefined(); + }); + + it("should export hslParser", () => { + expect(main.hslParser).toBeDefined(); + }); + + it("should export hsvParser", () => { + expect(main.hsvParser).toBeDefined(); + }); + + it("should export rgbParser", () => { + expect(main.rgbParser).toBeDefined(); + }); + + it("should export invert", () => { + expect(main.invert).toBeDefined(); + }); + + it("should export toCmyk", () => { + expect(main.toCmyk).toBeDefined(); + }); + + it("should export toHex", () => { + expect(main.toHex).toBeDefined(); + }); + + it("should export toHsl", () => { + expect(main.toHsl).toBeDefined(); + }); + + it("should export toHsv", () => { + expect(main.toHsv).toBeDefined(); + }); + + it("should export toRgb", () => { + expect(main.toRgb).toBeDefined(); + }); + + it("should export ColorParser", () => { + expect(main.ColorParser).toBeDefined(); + }); + + it("should export clamp", () => { + expect(main.clamp).toBeDefined(); + }); + + it("should export formatDecimal", () => { + expect(main.formatDecimal).toBeDefined(); + }); + + it("should export normalize8Bit", () => { + expect(main.normalize8Bit).toBeDefined(); + }); + + it("should export normalizeAlpha", () => { + expect(main.normalizeAlpha).toBeDefined(); + }); + + it("should export normalizeDegrees", () => { + expect(main.normalizeDegrees).toBeDefined(); + }); + + it("should export normalizePercentage", () => { + expect(main.normalizePercentage).toBeDefined(); + }); + + it("should export generateColorComponents", () => { + expect(main.generateColorComponents).toBeDefined(); + }); + + it("should export createPlugin", () => { + expect(main.createPlugin).toBeDefined(); + }); +}); diff --git a/test/parsers/cmykParser.test.ts b/test/parsers/cmykParser.test.ts new file mode 100644 index 0000000..c951b66 --- /dev/null +++ b/test/parsers/cmykParser.test.ts @@ -0,0 +1,17 @@ +import { cmykParser } from "../../src/parsers/cmykParser"; + +describe("cmykParser", () => { + it("parses valid CMYK object", () => { + const input = { c: 0, m: 100, y: 0, k: 0 }; + expect(cmykParser.parse(input)).toEqual([ + input, + { r: 255, g: 0, b: 255, a: 1 }, + { value: { c: 0, m: 100, y: 0, k: 0 }, model: "cmyk", isValid: true }, + ]); + }); + + it("returns null for invalid CMYK object", () => { + // @ts-expect-error Testing invalid input + expect(cmykParser.parse({ c: 0, m: "invalid", y: 0, k: 0 })).toBeNull(); + }); +}); diff --git a/test/parsers/hexParser.test.ts b/test/parsers/hexParser.test.ts new file mode 100644 index 0000000..c9d842a --- /dev/null +++ b/test/parsers/hexParser.test.ts @@ -0,0 +1,16 @@ +import { hexParser } from "../../src/parsers/hexParser"; + +describe("hexParser", () => { + it("parses valid hex string", () => { + const input = "#ff00ff"; + expect(hexParser.parse(input)).toEqual([ + input, + { r: 255, g: 0, b: 255, a: 1 }, + { value: "ff00ff", model: "hex", isValid: true }, + ]); + }); + + it("returns null for invalid hex string", () => { + expect(hexParser.parse("invalid")).toBeNull(); + }); +}); diff --git a/test/parsers/hslParser.test.ts b/test/parsers/hslParser.test.ts new file mode 100644 index 0000000..66a0ead --- /dev/null +++ b/test/parsers/hslParser.test.ts @@ -0,0 +1,18 @@ +import { hslParser } from "../../src/parsers/hslPaser"; + +describe("hslParser", () => { + it("parses valid HSL object", () => { + const input = { h: 300, s: 100, l: 50 }; + + expect(hslParser.parse(input)).toEqual([ + input, + { r: 255, g: 0, b: 255, a: 1 }, + { value: { h: 300, s: 100, l: 50 }, model: "hsl", isValid: true }, + ]); + }); + + it("returns null for invalid HSL object", () => { + // @ts-expect-error Testing invalid input + expect(hslParser.parse({ h: 300, s: "invalid", l: 50 })).toBeNull(); + }); +}); diff --git a/test/parsers/hsvParser.test.ts b/test/parsers/hsvParser.test.ts new file mode 100644 index 0000000..464911c --- /dev/null +++ b/test/parsers/hsvParser.test.ts @@ -0,0 +1,17 @@ +import { hsvParser } from "../../src/parsers/hsvParser"; + +describe("hsvParser", () => { + it("parses valid HSV object", () => { + const input = { h: 300, s: 100, v: 100 }; + expect(hsvParser.parse(input)).toEqual([ + input, + { r: 255, g: 0, b: 255, a: 1 }, + { value: { h: 300, s: 100, v: 100 }, model: "hsv", isValid: true }, + ]); + }); + + it("returns null for invalid HSV object", () => { + // @ts-expect-error Testing invalid input + expect(hsvParser.parse({ h: 300, s: "invalid", v: 100 })).toBeNull(); + }); +}); diff --git a/test/parsers/rgbParser.test.ts b/test/parsers/rgbParser.test.ts new file mode 100644 index 0000000..cf54ca1 --- /dev/null +++ b/test/parsers/rgbParser.test.ts @@ -0,0 +1,17 @@ +import { rgbParser } from "../../src/parsers/rgbParser"; + +describe("rgbParser", () => { + it("parses valid RGB object", () => { + const input = { r: 255, g: 0, b: 255 }; + expect(rgbParser.parse(input)).toEqual([ + input, + { r: 255, g: 0, b: 255, a: 1 }, + { value: { r: 255, g: 0, b: 255 }, model: "rgb", isValid: true }, + ]); + }); + + it("returns null for invalid RGB object", () => { + // @ts-expect-error Testing invalid input + expect(rgbParser.parse({ r: 255, g: "invalid", b: 255 })).toBeNull(); + }); +}); diff --git a/test/patterns.test.ts b/test/patterns.test.ts new file mode 100644 index 0000000..cdd3f5f --- /dev/null +++ b/test/patterns.test.ts @@ -0,0 +1,215 @@ +import { + regexCmyk, + regexHex, + regexHsl, + regexHsv, + regexRgb, +} from "../src/patterns"; + +describe("regexHex", () => { + // Run on every test + beforeEach(() => { + regexHex.lastIndex = 0; + }); + + it("matches valid hex color", () => { + expect("#f00").toMatch(regexHex); + expect("#ff0000").toMatch(regexHex); + expect("#ff0000ff").toMatch(regexHex); + expect("FF0000").toMatch(regexHex); + expect("FCFCFCFC").toMatch(regexHex); + expect("000").toMatch(regexHex); + }); + + it("extracts correct values from valid hex color", () => { + const result = regexHex.exec("#ff0000"); + expect(result).not.toBeNull(); + expect(result![1]).toBe("ff0000"); + }); + + it("extracts correct values from valid hex with alpha color", () => { + const result = regexHex.exec("#ff0000c3"); + expect(result).not.toBeNull(); + expect(result![1]).toBe("ff0000c3"); + }); + + it("returns null for invalid hex color", () => { + expect(regexHex.exec("#ff0000ff00")).toBeNull(); + expect(regexHex.exec("#ff0000 extra")).toBeNull(); + expect(regexHex.exec("#ff0000,")).toBeNull(); + expect(regexHex.exec("#ff0000/")).toBeNull(); + }); +}); + +describe("regexRgb", () => { + // Run on every test + beforeEach(() => { + regexRgb.lastIndex = 0; + }); + + it("matches valid RGB color", () => { + expect("rgb(255 0 255)").toMatch(regexRgb); + expect("rgb(255 0 255 / 0.2)").toMatch(regexRgb); + expect("rgb(255 0 255 / 1)").toMatch(regexRgb); + expect("rgba(255 0 255 / 1)").toMatch(regexRgb); + + expect("rgb(255, 0, 255)").toMatch(regexRgb); + expect("rgb(255, 0, 255, 0.2)").toMatch(regexRgb); + expect("rgb(255, 0, 255, 1)").toMatch(regexRgb); + expect("rgba(255, 0, 255, 1)").toMatch(regexRgb); + }); + + it("extracts correct values from valid RGB color", () => { + const result = regexRgb.exec("rgb(255 0 255)"); + expect(result).not.toBeNull(); + expect(result![1]).toBe("255"); + expect(result![2]).toBe("0"); + expect(result![3]).toBe("255"); + expect(result![4]).toBeUndefined(); + }); + + it("extracts correct values from valid RGBA color", () => { + const result = regexRgb.exec("rgb(255 0 255 / 0.2)"); + + expect(result).not.toBeNull(); + expect(result![1]).toBe("255"); + expect(result![2]).toBe("0"); + expect(result![3]).toBe("255"); + expect(result![4]).toBe("0.2"); + }); + + it("returns null for invalid RGB color", () => { + expect(regexRgb.exec("rgb(255, 0, 255, 255, 0.2)")).toBeNull(); + expect(regexRgb.exec("rgb(255,0,255,255,0.3)")).toBeNull(); + expect(regexRgb.exec("rgb(255, 0, 255) extra")).toBeNull(); + expect(regexRgb.exec("rgb(255, 0, 255,)")).toBeNull(); + expect(regexRgb.exec("rgb(255, 0, 255,/ 0)")).toBeNull(); + }); +}); + +describe("regexHsl", () => { + // Run on every test + beforeEach(() => { + regexHsl.lastIndex = 0; + }); + + it("matches valid HSL color", () => { + expect("hsl(0 100% 50%)").toMatch(regexHsl); + expect("hsl(0 100% 50% / 0.2)").toMatch(regexHsl); + expect("hsl(0, 100%, 50%)").toMatch(regexHsl); + expect("hsl(0, 100%, 50%, 0.2)").toMatch(regexHsl); + expect("hsla(0, 100%, 50%, 1)").toMatch(regexHsl); + }); + + it("extracts correct values from valid HSL color", () => { + const result = regexHsl.exec("hsl(0 100% 50%)"); + expect(result).not.toBeNull(); + expect(result![1]).toBe("0"); + expect(result![2]).toBe("100"); + expect(result![3]).toBe("50"); + expect(result![4]).toBeUndefined(); + }); + + it("extracts correct values from valid HSLA color", () => { + const result = regexHsl.exec("hsl(0 100% 50% / 0.2)"); + expect(result).not.toBeNull(); + expect(result![1]).toBe("0"); + expect(result![2]).toBe("100"); + expect(result![3]).toBe("50"); + expect(result![4]).toBe("0.2"); + }); + + it("returns null for invalid HSL color", () => { + expect(regexHsl.exec("hsl(0, 100%, 50%, 1, 0.2)")).toBeNull(); + expect(regexHsl.exec("hsl(0,100%,50%,1,0.3)")).toBeNull(); + expect(regexHsl.exec("hsl(0, 100%, 50%) extra")).toBeNull(); + expect(regexHsl.exec("hsl(0 100% 50% /)")).toBeNull(); + expect(regexHsl.exec("hsl(0, 100%, 50%,)")).toBeNull(); + expect(regexHsl.exec("hsl(0, 100%, 50%,/ 0)")).toBeNull(); + }); +}); + +describe("regexHsv", () => { + // Run on every test + beforeEach(() => { + regexHsv.lastIndex = 0; + }); + + it("matches valid HSV color", () => { + expect("hsv(0 100% 100%)").toMatch(regexHsv); + expect("hsv(0 100% 100% / 0.2)").toMatch(regexHsv); + expect("hsv(0, 100%, 100%)").toMatch(regexHsv); + expect("hsv(0, 100%, 100%, 0.2)").toMatch(regexHsv); + expect("hsva(0, 100%, 100%, 1)").toMatch(regexHsv); + }); + + it("extracts correct values from valid HSV color", () => { + const result = regexHsv.exec("hsv(0 100% 100%)"); + expect(result).not.toBeNull(); + expect(result![1]).toBe("0"); + expect(result![2]).toBe("100"); + expect(result![3]).toBe("100"); + expect(result![4]).toBeUndefined(); + }); + + it("extracts correct values from valid HSVA color", () => { + const result = regexHsv.exec("hsv(0 100% 100% / 0.2)"); + expect(result).not.toBeNull(); + expect(result![1]).toBe("0"); + expect(result![2]).toBe("100"); + expect(result![3]).toBe("100"); + expect(result![4]).toBe("0.2"); + }); + + it("returns null for invalid HSV color", () => { + expect(regexHsv.exec("hsv(0, 100%, 100%, 1, 0.2)")).toBeNull(); + expect(regexHsv.exec("hsv(0,100%,100%,1,0.3)")).toBeNull(); + expect(regexHsv.exec("hsv(0, 100%, 100%) extra")).toBeNull(); + expect(regexHsv.exec("hsv(0 100% 100% /)")).toBeNull(); + expect(regexHsv.exec("hsv(0, 100%, 100%,)")).toBeNull(); + expect(regexHsv.exec("hsv(0, 100%, 100%,/ 0)")).toBeNull(); + }); +}); + +describe("regexCmyk", () => { + // Run on every test + beforeEach(() => { + regexCmyk.lastIndex = 0; + }); + + it("matches valid CMYK color", () => { + expect("cmyk(0 100% 100% 0)").toMatch(regexCmyk); + expect("cmyk(0 100% 100% 0 / 0.2)").toMatch(regexCmyk); + expect("cmyk(0, 100%, 100%, 0)").toMatch(regexCmyk); + expect("cmyk(0, 100%, 100%, 0, 0.2)").toMatch(regexCmyk); + expect("cmyka(0, 100%, 100%, 0, 1)").toMatch(regexCmyk); + }); + + it("extracts correct values from valid CMYK color", () => { + const result = regexCmyk.exec("cmyk(0 100% 100% 0)"); + expect(result).not.toBeNull(); + expect(result![1]).toBe("0"); + expect(result![2]).toBe("100"); + expect(result![3]).toBe("100"); + expect(result![4]).toBe("0"); + expect(result![5]).toBeUndefined(); + }); + + it("extracts correct values from valid CMYKA color", () => { + const result = regexCmyk.exec("cmyk(0 100% 100% 0 / 0.2)"); + expect(result).not.toBeNull(); + expect(result![1]).toBe("0"); + expect(result![2]).toBe("100"); + expect(result![3]).toBe("100"); + expect(result![4]).toBe("0"); + expect(result![5]).toBe("0.2"); + }); + + it("returns null for invalid CMYK color", () => { + expect(regexCmyk.exec("cmyk(0, 100%, 100%, 0, 0.2, 0.2)")).toBeNull(); + expect(regexCmyk.exec("cmyk(0,100%,100%,0,2,0.3)")).toBeNull(); + expect(regexCmyk.exec("cmyk(0, 100%, 100%, 0) extra")).toBeNull(); + expect(regexCmyk.exec("cmyk(0 100 100 0 /)")).toBeNull(); + expect(regexCmyk.exec("cmyk(0, 100, 100, 0, 1, 0.2)")).toBeNull(); + }); +}); diff --git a/test/plugins/invert.test.ts b/test/plugins/invert.test.ts new file mode 100644 index 0000000..85b626a --- /dev/null +++ b/test/plugins/invert.test.ts @@ -0,0 +1,31 @@ +import { dye } from "../../src/dye"; +import { invert } from "../../src/plugins/invert"; + +describe("invert plugin", () => { + it("should invert the colors correctly", () => { + const inputColor = dye({ r: 100, g: 150, b: 200, a: 1 }); + const expectedColor = dye({ r: 155, g: 105, b: 55, a: 1 }); + + const result = invert.call(inputColor); + + expect(result.rgb).toEqual(expectedColor.rgb); + }); + + it("should handle edge case of black color", () => { + const inputColor = dye({ r: 0, g: 0, b: 0, a: 1 }); + const expectedColor = dye({ r: 255, g: 255, b: 255, a: 1 }); + + const result = invert.call(inputColor); + + expect(result.rgb).toEqual(expectedColor.rgb); + }); + + it("should handle edge case of white color", () => { + const inputColor = dye({ r: 255, g: 255, b: 255, a: 1 }); + const expectedColor = dye({ r: 0, g: 0, b: 0, a: 1 }); + + const result = invert.call(inputColor); + + expect(result.rgb).toEqual(expectedColor.rgb); + }); +}); diff --git a/test/plugins/lighten.test.ts b/test/plugins/lighten.test.ts new file mode 100644 index 0000000..114fd12 --- /dev/null +++ b/test/plugins/lighten.test.ts @@ -0,0 +1,29 @@ +import { darken, lighten } from "../../src/plugins/lighten"; + +describe("lighten plugin", () => { + it("should lighten the color by the given amount", () => { + const color = { hsl: { h: 0, s: 100, l: 50 }, options: {} }; + const result = lighten.call(color, 0.1); + expect(result.hsl.l).toBeCloseTo(55); + }); + + it("should not exceed the lightness value of 1", () => { + const color = { hsl: { h: 0, s: 100, l: 95 }, options: {} }; + const result = lighten.call(color, 0.1); + expect(result.hsl.l).toBeLessThanOrEqual(100); + }); +}); + +describe("darken plugin", () => { + it("should darken the color by the given amount", () => { + const color = { hsl: { h: 0, s: 100, l: 50 }, options: {} }; + const result = darken.call(color, 0.1); + expect(result.hsl.l).toBeCloseTo(45); + }); + + it("should not go below the lightness value of 0", () => { + const color = { hsl: { h: 0, s: 100, l: 5 }, options: {} }; + const result = darken.call(color, 0.1); + expect(result.hsl.l).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/test/plugins/saturate.test.ts b/test/plugins/saturate.test.ts new file mode 100644 index 0000000..6c5e0e4 --- /dev/null +++ b/test/plugins/saturate.test.ts @@ -0,0 +1,19 @@ +import { desaturate, saturate } from "../../src/plugins/saturate"; + +describe("saturate plugin", () => { + it("should increase the saturation by the given amount", () => { + const initContext = { hsl: { h: 0, s: 50, l: 50 }, options: {} }; + + const result = saturate.call(initContext, 0.2); + expect(result.hsl.s).toBeCloseTo(60); + }); +}); + +describe("desaturate plugin", () => { + it("should decrease the saturation by the given amount", () => { + const initContext = { hsl: { h: 0, s: 50, l: 50 }, options: {} }; + + const result = desaturate.call(initContext, 0.1); + expect(result.hsl.s).toBeCloseTo(45); + }); +}); diff --git a/test/plugins/toCmyk.test.ts b/test/plugins/toCmyk.test.ts new file mode 100644 index 0000000..1a448e6 --- /dev/null +++ b/test/plugins/toCmyk.test.ts @@ -0,0 +1,52 @@ +import { toCmyk } from "../../src/plugins/toCmyk"; + +describe("toCmyk plugin", () => { + test("should convert to CMYK with minify enabled", () => { + const result = toCmyk.call({ + cmyk: { c: 50, m: 50, y: 0, k: 20, a: 1 }, + alpha: 1, + options: { formatOptions: { minify: true } }, + }); + expect(result).toBe("cmyk(50,50,0,20)"); + }); + + test("should convert to CMYK with minify disabled", () => { + const result = toCmyk.call({ + cmyk: { c: 50, m: 50, y: 0, k: 20, a: 1 }, + alpha: 1, + options: { formatOptions: { minify: false } }, + }); + + expect(result).toBe("cmyk(50%, 50%, 0%, 20%)"); + }); + + test("should convert to CMYK with CSS Next enabled", () => { + const result = toCmyk.call({ + cmyk: { c: 50, m: 50, y: 0, k: 20, a: 1 }, + alpha: 1, + options: { formatOptions: { cssNext: true } }, + }); + + expect(result).toBe("cmyk(50% 50% 0% 20%)"); + }); + + test("should convert to CMYK with CSS Next disabled", () => { + const result = toCmyk.call({ + cmyk: { c: 50, m: 50, y: 0, k: 20, a: 1 }, + alpha: 1, + options: { formatOptions: { cssNext: false } }, + }); + + expect(result).toBe("cmyk(50%, 50%, 0%, 20%)"); + }); + + test("should convert to CMYK with combined options", () => { + const result = toCmyk.call({ + cmyk: { c: 50, m: 50, y: 0, k: 20, a: 1 }, + alpha: 1, + options: { formatOptions: { cssNext: true, minify: true } }, + }); + + expect(result).toBe("cmyk(50 50 0 20)"); + }); +}); diff --git a/test/plugins/toHex.test.ts b/test/plugins/toHex.test.ts new file mode 100644 index 0000000..ea994b0 --- /dev/null +++ b/test/plugins/toHex.test.ts @@ -0,0 +1,56 @@ +import { isRgbShortanable, toHex } from "../../src/plugins/toHex"; + +describe("toHex plugin", () => { + it("should check if RGB is shortanable", () => { + expect(isRgbShortanable({ r: 255, g: 0, b: 0, a: 1 })).toBe(true); + expect(isRgbShortanable({ r: 255, g: 255, b: 255, a: 1 })).toBe(true); + expect(isRgbShortanable({ r: 0, g: 0, b: 0, a: 1 })).toBe(true); + expect(isRgbShortanable({ r: 0, g: 255, b: 0, a: 1 })).toBe(true); + expect(isRgbShortanable({ r: 0, g: 0, b: 255, a: 1 })).toBe(true); + }); + + it("should check if RGB is not shortanable", () => { + expect(isRgbShortanable({ r: 255, g: 0, b: 0, a: 0.5 })).toBe(false); + expect(isRgbShortanable({ r: 255, g: 255, b: 255, a: 0.5 })).toBe(false); + expect(isRgbShortanable({ r: 0, g: 0, b: 0, a: 0.5 })).toBe(false); + expect(isRgbShortanable({ r: 0, g: 255, b: 0, a: 0.5 })).toBe(false); + expect(isRgbShortanable({ r: 0, g: 0, b: 255, a: 0.5 })).toBe(false); + // @ts-ignore - Testing edge case + expect(isRgbShortanable({ r: 0, g: 30, b: 25 })).toBe(false); + }); + + it("should convert RGB to HEX", () => { + const mockDyeInstance = { rgb: { r: 255, g: 0, b: 0 } }; + + const plugin = toHex.bind(mockDyeInstance); + + expect(plugin()).toBe("#ff0000"); + }); + + it("should handle RGB with alpha channel", () => { + const mockDyeInstance = { rgb: { r: 255, g: 0, b: 0, a: 0.5 } }; + + const plugin = toHex.bind(mockDyeInstance); + + expect(plugin()).toBe("#ff000080"); + }); + + it("should handle RGB with default alpha channel", () => { + const mockDyeInstance = { rgb: { r: 0, g: 255, b: 0, a: 1 } }; + + const plugin = toHex.bind(mockDyeInstance); + + expect(plugin()).toBe("#00ff00"); + }); + + it("should minify HEX color", () => { + const mockDyeInstance = { + rgb: { r: 255, g: 255, b: 255, a: 1 }, + options: { formatOptions: { minify: true } }, + }; + + const plugin = toHex.bind(mockDyeInstance); + + expect(plugin()).toBe("#fff"); + }); +}); diff --git a/test/plugins/toHsl.test.ts b/test/plugins/toHsl.test.ts new file mode 100644 index 0000000..ee29ba3 --- /dev/null +++ b/test/plugins/toHsl.test.ts @@ -0,0 +1,52 @@ +import { toHsl } from "../../src/plugins/toHsl"; + +describe("toHsl plugin", () => { + test("should convert to HSL with minify enabled", () => { + const result = toHsl.call({ + hsl: { h: 240, s: 50, l: 59, a: 1 }, + alpha: 1, + options: { formatOptions: { minify: true } }, + }); + expect(result).toBe("hsl(240,50,59)"); + }); + + test("should convert to HSL with minify disabled", () => { + const result = toHsl.call({ + hsl: { h: 240, s: 50, l: 59, a: 1 }, + alpha: 1, + options: { formatOptions: { minify: false } }, + }); + + expect(result).toBe("hsl(240, 50%, 59%)"); + }); + + test("should convert to HSL with CSS Next enabled", () => { + const result = toHsl.call({ + hsl: { h: 240, s: 50, l: 59, a: 1 }, + alpha: 1, + options: { formatOptions: { cssNext: true } }, + }); + + expect(result).toBe("hsl(240 50% 59%)"); + }); + + test("should convert to HSL with CSS Next disabled", () => { + const result = toHsl.call({ + hsl: { h: 240, s: 50, l: 59, a: 1 }, + alpha: 1, + options: { formatOptions: { cssNext: false } }, + }); + + expect(result).toBe("hsl(240, 50%, 59%)"); + }); + + test("should convert to HSL with combined options", () => { + const result = toHsl.call({ + hsl: { h: 240, s: 50, l: 59, a: 1 }, + alpha: 1, + options: { formatOptions: { cssNext: true, minify: true } }, + }); + + expect(result).toBe("hsl(240 50 59)"); + }); +}); diff --git a/test/plugins/toHsv.test.ts b/test/plugins/toHsv.test.ts new file mode 100644 index 0000000..b020299 --- /dev/null +++ b/test/plugins/toHsv.test.ts @@ -0,0 +1,52 @@ +import { toHsv } from "../../src/plugins/toHsv"; + +describe("toHsv plugin", () => { + test("should convert to HSV with minify enabled", () => { + const result = toHsv.call({ + hsv: { h: 240, s: 47.62, v: 58.82, a: 1 }, + alpha: 1, + options: { formatOptions: { minify: true } }, + }); + expect(result).toBe("hsv(240,48,59)"); + }); + + test("should convert to HSV with minify disabled", () => { + const result = toHsv.call({ + hsv: { h: 240, s: 47.62, v: 58.82, a: 1 }, + alpha: 1, + options: { formatOptions: { minify: false } }, + }); + + expect(result).toBe("hsv(240, 48%, 59%)"); + }); + + test("should convert to HSV with CSS Next enabled", () => { + const result = toHsv.call({ + hsv: { h: 240, s: 47.62, v: 58.82, a: 1 }, + alpha: 1, + options: { formatOptions: { cssNext: true } }, + }); + + expect(result).toBe("hsv(240 48% 59%)"); + }); + + test("should convert to HSV with CSS Next disabled", () => { + const result = toHsv.call({ + hsv: { h: 240, s: 47.62, v: 58.82, a: 1 }, + alpha: 1, + options: { formatOptions: { cssNext: false } }, + }); + + expect(result).toBe("hsv(240, 48%, 59%)"); + }); + + test("should convert to HSV with combined options", () => { + const result = toHsv.call({ + hsv: { h: 240, s: 47.62, v: 58.82, a: 1 }, + alpha: 1, + options: { formatOptions: { cssNext: true, minify: true } }, + }); + + expect(result).toBe("hsv(240 48 59)"); + }); +}); diff --git a/test/plugins/toRgb.test.ts b/test/plugins/toRgb.test.ts new file mode 100644 index 0000000..2c1ce31 --- /dev/null +++ b/test/plugins/toRgb.test.ts @@ -0,0 +1,52 @@ +import { toRgb } from "../../src/plugins/toRgb"; + +describe("toRgb plugin", () => { + test("should convert to RGB with minify enabled", () => { + const result = toRgb.call({ + rgb: { r: 100, g: 100, b: 200, a: 1 }, + alpha: 1, + options: { formatOptions: { minify: true } }, + }); + expect(result).toBe("rgb(100,100,200)"); + }); + + test("should convert to RGB with minify disabled", () => { + const result = toRgb.call({ + rgb: { r: 100, g: 100, b: 200, a: 1 }, + alpha: 1, + options: { formatOptions: { minify: false } }, + }); + + expect(result).toBe("rgb(100, 100, 200)"); + }); + + test("should convert to RGB with CSS Next enabled", () => { + const result = toRgb.call({ + rgb: { r: 100, g: 100, b: 200, a: 1 }, + alpha: 1, + options: { formatOptions: { cssNext: true } }, + }); + + expect(result).toBe("rgb(100 100 200)"); + }); + + test("should convert to RGB with CSS Next disabled", () => { + const result = toRgb.call({ + rgb: { r: 100, g: 100, b: 200, a: 1 }, + alpha: 1, + options: { formatOptions: { cssNext: false } }, + }); + + expect(result).toBe("rgb(100, 100, 200)"); + }); + + test("should convert to RGB with combined options", () => { + const result = toRgb.call({ + rgb: { r: 100, g: 100, b: 200, a: 1 }, + alpha: 1, + options: { formatOptions: { cssNext: true, minify: true } }, + }); + + expect(result).toBe("rgb(100 100 200)"); + }); +}); diff --git a/test/processing/cases/cmykToRgbCases.ts b/test/processing/cases/cmykToRgbCases.ts new file mode 100644 index 0000000..54d4bb7 --- /dev/null +++ b/test/processing/cases/cmykToRgbCases.ts @@ -0,0 +1,58 @@ +export const cmykToRgbCases = [ + { + input: { c: 0, m: 100, y: 100, k: 0, a: 1 }, + expected: { r: 255, g: 0, b: 0, a: 1 }, + }, // red + { + input: { c: 100, m: 0, y: 100, k: 0, a: 1 }, + expected: { r: 0, g: 255, b: 0, a: 1 }, + }, // green + { + input: { c: 100, m: 100, y: 0, k: 0, a: 1 }, + expected: { r: 0, g: 0, b: 255, a: 1 }, + }, // blue + { + input: { c: 0, m: 0, y: 100, k: 0, a: 1 }, + expected: { r: 255, g: 255, b: 0, a: 1 }, + }, // yellow + { + input: { c: 100, m: 0, y: 0, k: 0, a: 1 }, + expected: { r: 0, g: 255, b: 255, a: 1 }, + }, // cyan + { + input: { c: 0, m: 100, y: 0, k: 0, a: 1 }, + expected: { r: 255, g: 0, b: 255, a: 1 }, + }, // magenta + { + input: { c: 0, m: 0, y: 0, k: 100, a: 1 }, + expected: { r: 0, g: 0, b: 0, a: 1 }, + }, // black + { + input: { c: 0, m: 0, y: 0, k: 0, a: 1 }, + expected: { r: 255, g: 255, b: 255, a: 1 }, + }, // white + { + input: { c: 0, m: 0, y: 0, k: 50, a: 1 }, + expected: { r: 128, g: 128, b: 128, a: 1 }, + }, // gray + { + input: { c: 0, m: 60, y: 50, k: 10, a: 1 }, + expected: { r: 230, g: 92, b: 115, a: 1 }, + }, // pastel pink + { + input: { c: 70, m: 80, y: 0, k: 40, a: 1 }, + expected: { r: 46, g: 31, b: 153, a: 1 }, + }, // deep purple + { + input: { c: 0, m: 60, y: 90, k: 5, a: 1 }, + expected: { r: 242, g: 97, b: 24, a: 1 }, + }, // bright coral + { + input: { c: 40, m: 0, y: 30, k: 30, a: 1 }, + expected: { r: 107, g: 179, b: 125, a: 1 }, + }, // muted sage + { + input: { c: 0, m: 15, y: 90, k: 5, a: 1 }, + expected: { r: 242, g: 206, b: 24, a: 1 }, + }, // rich gold +]; diff --git a/test/processing/cases/hexToRgbCases.ts b/test/processing/cases/hexToRgbCases.ts new file mode 100644 index 0000000..9a098c8 --- /dev/null +++ b/test/processing/cases/hexToRgbCases.ts @@ -0,0 +1,16 @@ +export const hexToRgbCases = [ + { input: "#ff0000", expected: { r: 255, g: 0, b: 0, a: 1 } }, // red + { input: "#00ff00", expected: { r: 0, g: 255, b: 0, a: 1 } }, // green + { input: "#0000ff", expected: { r: 0, g: 0, b: 255, a: 1 } }, // blue + { input: "#ffff00", expected: { r: 255, g: 255, b: 0, a: 1 } }, // yellow + { input: "#00ffff", expected: { r: 0, g: 255, b: 255, a: 1 } }, // cyan + { input: "#ff00ff", expected: { r: 255, g: 0, b: 255, a: 1 } }, // magenta + { input: "#000000", expected: { r: 0, g: 0, b: 0, a: 1 } }, // black + { input: "#ffffff", expected: { r: 255, g: 255, b: 255, a: 1 } }, // white + { input: "#808080", expected: { r: 128, g: 128, b: 128, a: 1 } }, // gray + { input: "#e65c73", expected: { r: 230, g: 92, b: 115, a: 1 } }, // pastel pink + { input: "#701f99", expected: { r: 112, g: 31, b: 153, a: 1 } }, // deep purple + { input: "#f26118", expected: { r: 242, g: 97, b: 24, a: 1 } }, // bright coral + { input: "#6bb383", expected: { r: 107, g: 179, b: 131, a: 1 } }, // muted sage + { input: "#f2ce18", expected: { r: 242, g: 206, b: 24, a: 1 } }, // rich gold +]; diff --git a/test/processing/cases/hslToHsvCases.ts b/test/processing/cases/hslToHsvCases.ts new file mode 100644 index 0000000..9507d3a --- /dev/null +++ b/test/processing/cases/hslToHsvCases.ts @@ -0,0 +1,58 @@ +export const hslToHsvCases = [ + { + input: { h: 0, s: 100, l: 50, a: 1 }, + expected: { h: 0, s: 100, v: 100, a: 1 }, + }, // red + { + input: { h: 120, s: 100, l: 50, a: 1 }, + expected: { h: 120, s: 100, v: 100, a: 1 }, + }, // green + { + input: { h: 240, s: 100, l: 50, a: 1 }, + expected: { h: 240, s: 100, v: 100, a: 1 }, + }, // blue + { + input: { h: 60, s: 100, l: 50, a: 1 }, + expected: { h: 60, s: 100, v: 100, a: 1 }, + }, // yellow + { + input: { h: 180, s: 100, l: 50, a: 1 }, + expected: { h: 180, s: 100, v: 100, a: 1 }, + }, // cyan + { + input: { h: 300, s: 100, l: 50, a: 1 }, + expected: { h: 300, s: 100, v: 100, a: 1 }, + }, // magenta + { + input: { h: 0, s: 0, l: 0, a: 1 }, + expected: { h: 0, s: 0, v: 0, a: 1 }, + }, // black + { + input: { h: 0, s: 0, l: 100, a: 1 }, + expected: { h: 0, s: 0, v: 100, a: 1 }, + }, // white + { + input: { h: 0, s: 0, l: 50, a: 1 }, + expected: { h: 0, s: 0, v: 50, a: 1 }, + }, // gray + { + input: { h: 350, s: 60, l: 90, a: 1 }, + expected: { h: 350, s: 12, v: 96, a: 1 }, + }, // pastel pink + { + input: { h: 280, s: 80, l: 30, a: 1 }, + expected: { h: 280, s: 89, v: 54, a: 1 }, + }, // deep purple + { + input: { h: 20, s: 90, l: 70, a: 1 }, + expected: { h: 20, s: 56, v: 97, a: 1 }, + }, // bright coral + { + input: { h: 140, s: 40, l: 70, a: 1 }, + expected: { h: 140, s: 29, v: 82, a: 1 }, + }, // muted sage + { + input: { h: 50, s: 90, l: 70, a: 1 }, + expected: { h: 50, s: 56, v: 97, a: 1 }, + }, // rich gold +]; diff --git a/test/processing/cases/hslToRgbCases.ts b/test/processing/cases/hslToRgbCases.ts new file mode 100644 index 0000000..42d5b2d --- /dev/null +++ b/test/processing/cases/hslToRgbCases.ts @@ -0,0 +1,58 @@ +export const hslToRgbCases = [ + { + input: { h: 0, s: 100, l: 50, a: 1 }, + expected: { r: 255, g: 0, b: 0, a: 1 }, + }, // red + { + input: { h: 120, s: 100, l: 50, a: 1 }, + expected: { r: 0, g: 255, b: 0, a: 1 }, + }, // green + { + input: { h: 240, s: 100, l: 50, a: 1 }, + expected: { r: 0, g: 0, b: 255, a: 1 }, + }, // blue + { + input: { h: 60, s: 100, l: 50, a: 1 }, + expected: { r: 255, g: 255, b: 0, a: 1 }, + }, // yellow + { + input: { h: 180, s: 100, l: 50, a: 1 }, + expected: { r: 0, g: 255, b: 255, a: 1 }, + }, // cyan + { + input: { h: 300, s: 100, l: 50, a: 1 }, + expected: { r: 255, g: 0, b: 255, a: 1 }, + }, // magenta + { + input: { h: 0, s: 0, l: 0, a: 1 }, + expected: { r: 0, g: 0, b: 0, a: 1 }, + }, // black + { + input: { h: 0, s: 0, l: 100, a: 1 }, + expected: { r: 255, g: 255, b: 255, a: 1 }, + }, // white + { + input: { h: 0, s: 0, l: 50, a: 1 }, + expected: { r: 128, g: 128, b: 128, a: 1 }, + }, // gray + { + input: { h: 350, s: 60, l: 90, a: 1 }, + expected: { r: 245, g: 214, b: 219, a: 1 }, + }, // pastel pink + { + input: { h: 280, s: 80, l: 30, a: 1 }, + expected: { r: 97, g: 15, b: 138, a: 1 }, + }, // deep purple + { + input: { h: 20, s: 90, l: 70, a: 1 }, + expected: { r: 247, g: 156, b: 110, a: 1 }, + }, // bright coral + { + input: { h: 140, s: 40, l: 70, a: 1 }, + expected: { r: 148, g: 209, b: 168, a: 1 }, + }, // muted sage + { + input: { h: 50, s: 90, l: 70, a: 1 }, + expected: { r: 247, g: 224, b: 110, a: 1 }, + }, // rich gold +]; diff --git a/test/processing/cases/hsvToHslCases.ts b/test/processing/cases/hsvToHslCases.ts new file mode 100644 index 0000000..82f3bcb --- /dev/null +++ b/test/processing/cases/hsvToHslCases.ts @@ -0,0 +1,58 @@ +export const hsvToHslCases = [ + { + input: { h: 0, s: 100, v: 100, a: 1 }, + expected: { h: 0, s: 100, l: 50, a: 1 }, + }, // red + { + input: { h: 120, s: 100, v: 100, a: 1 }, + expected: { h: 120, s: 100, l: 50, a: 1 }, + }, // green + { + input: { h: 240, s: 100, v: 100, a: 1 }, + expected: { h: 240, s: 100, l: 50, a: 1 }, + }, // blue + { + input: { h: 60, s: 100, v: 100, a: 1 }, + expected: { h: 60, s: 100, l: 50, a: 1 }, + }, // yellow + { + input: { h: 180, s: 100, v: 100, a: 1 }, + expected: { h: 180, s: 100, l: 50, a: 1 }, + }, // cyan + { + input: { h: 300, s: 100, v: 100, a: 1 }, + expected: { h: 300, s: 100, l: 50, a: 1 }, + }, // magenta + { + input: { h: 0, s: 0, v: 0, a: 1 }, + expected: { h: 0, s: 0, l: 0, a: 1 }, + }, // black + { + input: { h: 0, s: 0, v: 100, a: 1 }, + expected: { h: 0, s: 0, l: 100, a: 1 }, + }, // white + { + input: { h: 0, s: 0, v: 50, a: 1 }, + expected: { h: 0, s: 0, l: 50, a: 1 }, + }, // gray + { + input: { h: 350, s: 12, v: 96, a: 1 }, + expected: { h: 350, s: 59, l: 90, a: 1 }, + }, // pastel pink + { + input: { h: 280, s: 89, v: 54, a: 1 }, + expected: { h: 280, s: 80, l: 30, a: 1 }, + }, // deep purple + { + input: { h: 20, s: 56, v: 97, a: 1 }, + expected: { h: 20, s: 90, l: 70, a: 1 }, + }, // bright coral + { + input: { h: 140, s: 29, v: 82, a: 1 }, + expected: { h: 140, s: 40, l: 70, a: 1 }, + }, // muted sage + { + input: { h: 50, s: 56, v: 97, a: 1 }, + expected: { h: 50, s: 90, l: 70, a: 1 }, + }, // rich gold +]; diff --git a/test/processing/cases/hsvToRgbCases.ts b/test/processing/cases/hsvToRgbCases.ts new file mode 100644 index 0000000..674ac07 --- /dev/null +++ b/test/processing/cases/hsvToRgbCases.ts @@ -0,0 +1,58 @@ +export const hsvToRgbCases = [ + { + input: { h: 0, s: 100, v: 100, a: 1 }, + expected: { r: 255, g: 0, b: 0, a: 1 }, + }, // red + { + input: { h: 120, s: 100, v: 100, a: 1 }, + expected: { r: 0, g: 255, b: 0, a: 1 }, + }, // green + { + input: { h: 240, s: 100, v: 100, a: 1 }, + expected: { r: 0, g: 0, b: 255, a: 1 }, + }, // blue + { + input: { h: 60, s: 100, v: 100, a: 1 }, + expected: { r: 255, g: 255, b: 0, a: 1 }, + }, // yellow + { + input: { h: 180, s: 100, v: 100, a: 1 }, + expected: { r: 0, g: 255, b: 255, a: 1 }, + }, // cyan + { + input: { h: 300, s: 100, v: 100, a: 1 }, + expected: { r: 255, g: 0, b: 255, a: 1 }, + }, // magenta + { + input: { h: 0, s: 0, v: 0, a: 1 }, + expected: { r: 0, g: 0, b: 0, a: 1 }, + }, // black + { + input: { h: 0, s: 0, v: 100, a: 1 }, + expected: { r: 255, g: 255, b: 255, a: 1 }, + }, // white + { + input: { h: 0, s: 0, v: 50, a: 1 }, + expected: { r: 128, g: 128, b: 128, a: 1 }, + }, // gray + { + input: { h: 350, s: 60, v: 90, a: 1 }, + expected: { r: 230, g: 92, b: 115, a: 1 }, + }, // pastel pink + { + input: { h: 280, s: 80, v: 60, a: 1 }, + expected: { r: 112, g: 31, b: 153, a: 1 }, + }, // deep purple + { + input: { h: 20, s: 90, v: 95, a: 1 }, + expected: { r: 242, g: 97, b: 24, a: 1 }, + }, // bright coral + { + input: { h: 140, s: 40, v: 70, a: 1 }, + expected: { r: 107, g: 179, b: 131, a: 1 }, + }, // muted sage + { + input: { h: 50, s: 90, v: 95, a: 1 }, + expected: { r: 242, g: 206, b: 24, a: 1 }, + }, // rich gold +]; diff --git a/test/processing/cases/rgbToCmykCases.ts b/test/processing/cases/rgbToCmykCases.ts new file mode 100644 index 0000000..13d7a5c --- /dev/null +++ b/test/processing/cases/rgbToCmykCases.ts @@ -0,0 +1,58 @@ +export const rgbToCmykCases = [ + { + input: { r: 255, g: 0, b: 0, a: 1 }, + expected: { c: 0, m: 100, y: 100, k: 0, a: 1 }, + }, // red + { + input: { r: 0, g: 255, b: 0, a: 1 }, + expected: { c: 100, m: 0, y: 100, k: 0, a: 1 }, + }, // green + { + input: { r: 0, g: 0, b: 255, a: 1 }, + expected: { c: 100, m: 100, y: 0, k: 0, a: 1 }, + }, // blue + { + input: { r: 255, g: 255, b: 0, a: 1 }, + expected: { c: 0, m: 0, y: 100, k: 0, a: 1 }, + }, // yellow + { + input: { r: 0, g: 255, b: 255, a: 1 }, + expected: { c: 100, m: 0, y: 0, k: 0, a: 1 }, + }, // cyan + { + input: { r: 255, g: 0, b: 255, a: 1 }, + expected: { c: 0, m: 100, y: 0, k: 0, a: 1 }, + }, // magenta + { + input: { r: 0, g: 0, b: 0, a: 1 }, + expected: { c: 0, m: 0, y: 0, k: 100, a: 1 }, + }, // black + { + input: { r: 255, g: 255, b: 255, a: 1 }, + expected: { c: 0, m: 0, y: 0, k: 0, a: 1 }, + }, // white + { + input: { r: 128, g: 128, b: 128, a: 1 }, + expected: { c: 0, m: 0, y: 0, k: 50, a: 1 }, + }, // gray + { + input: { r: 230, g: 92, b: 115, a: 1 }, + expected: { c: 0, m: 60, y: 50, k: 10, a: 1 }, + }, // pastel pink + { + input: { r: 46, g: 31, b: 153, a: 1 }, + expected: { c: 70, m: 80, y: 0, k: 40, a: 1 }, + }, // deep purple + { + input: { r: 242, g: 97, b: 24, a: 1 }, + expected: { c: 0, m: 60, y: 90, k: 5, a: 1 }, + }, // bright coral + { + input: { r: 107, g: 179, b: 125, a: 1 }, + expected: { c: 40, m: 0, y: 30, k: 30, a: 1 }, + }, // muted sage + { + input: { r: 242, g: 206, b: 24, a: 1 }, + expected: { c: 0, m: 15, y: 90, k: 5, a: 1 }, + }, // rich gold +]; diff --git a/test/processing/cases/rgbToHexCases.ts b/test/processing/cases/rgbToHexCases.ts new file mode 100644 index 0000000..be584f6 --- /dev/null +++ b/test/processing/cases/rgbToHexCases.ts @@ -0,0 +1,17 @@ +export const rgbToHexCases = [ + { input: { r: 255, g: 0, b: 0, a: 0.5 }, expected: "#ff000080" }, // red + { input: { r: 255, g: 0, b: 0, a: 1 }, expected: "#ff0000" }, // red + { input: { r: 0, g: 255, b: 0, a: 1 }, expected: "#00ff00" }, // green + { input: { r: 0, g: 0, b: 255, a: 1 }, expected: "#0000ff" }, // blue + { input: { r: 255, g: 255, b: 0, a: 1 }, expected: "#ffff00" }, // yellow + { input: { r: 0, g: 255, b: 255, a: 1 }, expected: "#00ffff" }, // cyan + { input: { r: 255, g: 0, b: 255, a: 1 }, expected: "#ff00ff" }, // magenta + { input: { r: 0, g: 0, b: 0, a: 1 }, expected: "#000000" }, // black + { input: { r: 255, g: 255, b: 255, a: 1 }, expected: "#ffffff" }, // white + { input: { r: 128, g: 128, b: 128, a: 1 }, expected: "#808080" }, // gray + { input: { r: 230, g: 92, b: 115, a: 1 }, expected: "#e65c73" }, // pastel pink + { input: { r: 112, g: 31, b: 153, a: 1 }, expected: "#701f99" }, // deep purple + { input: { r: 242, g: 97, b: 24, a: 1 }, expected: "#f26118" }, // bright coral + { input: { r: 107, g: 179, b: 131, a: 1 }, expected: "#6bb383" }, // muted sage + { input: { r: 242, g: 206, b: 24, a: 1 }, expected: "#f2ce18" }, // rich gold +]; diff --git a/test/processing/cases/rgbToHslCases.ts b/test/processing/cases/rgbToHslCases.ts new file mode 100644 index 0000000..2e81582 --- /dev/null +++ b/test/processing/cases/rgbToHslCases.ts @@ -0,0 +1,58 @@ +export const rgbToHslCases = [ + { + input: { r: 255, g: 0, b: 0, a: 1 }, + expected: { h: 0, s: 100, l: 50, a: 1 }, + }, // red + { + input: { r: 0, g: 255, b: 0, a: 1 }, + expected: { h: 120, s: 100, l: 50, a: 1 }, + }, // green + { + input: { r: 0, g: 0, b: 255, a: 1 }, + expected: { h: 240, s: 100, l: 50, a: 1 }, + }, // blue + { + input: { r: 255, g: 255, b: 0, a: 1 }, + expected: { h: 60, s: 100, l: 50, a: 1 }, + }, // yellow + { + input: { r: 0, g: 255, b: 255, a: 1 }, + expected: { h: 180, s: 100, l: 50, a: 1 }, + }, // cyan + { + input: { r: 255, g: 0, b: 255, a: 1 }, + expected: { h: 300, s: 100, l: 50, a: 1 }, + }, // magenta + { + input: { r: 0, g: 0, b: 0, a: 1 }, + expected: { h: 0, s: 0, l: 0, a: 1 }, + }, // black + { + input: { r: 255, g: 255, b: 255, a: 1 }, + expected: { h: 0, s: 0, l: 100, a: 1 }, + }, // white + { + input: { r: 128, g: 128, b: 128, a: 1 }, + expected: { h: 0, s: 0, l: 50, a: 1 }, + }, // gray + { + input: { r: 230, g: 92, b: 115, a: 1 }, + expected: { h: 350, s: 73, l: 63, a: 1 }, + }, // pastel pink + { + input: { r: 112, g: 31, b: 153, a: 1 }, + expected: { h: 280, s: 66, l: 36, a: 1 }, + }, // deep purple + { + input: { r: 242, g: 97, b: 24, a: 1 }, + expected: { h: 20, s: 89, l: 52, a: 1 }, + }, // bright coral + { + input: { r: 107, g: 179, b: 131, a: 1 }, + expected: { h: 140, s: 32, l: 56, a: 1 }, + }, // muted sage + { + input: { r: 242, g: 206, b: 24, a: 1 }, + expected: { h: 50, s: 89, l: 52, a: 1 }, + }, // rich gold +]; diff --git a/test/processing/cases/rgbToHsvCases.ts b/test/processing/cases/rgbToHsvCases.ts new file mode 100644 index 0000000..880b4a5 --- /dev/null +++ b/test/processing/cases/rgbToHsvCases.ts @@ -0,0 +1,55 @@ +export const rgbToHsvCases = [ + { + input: { r: 255, g: 0, b: 0, a: 1 }, + expected: { h: 0, s: 100, v: 100, a: 1 }, + }, // red + { + input: { r: 0, g: 255, b: 0, a: 1 }, + expected: { h: 120, s: 100, v: 100, a: 1 }, + }, // green + { + input: { r: 0, g: 0, b: 255, a: 1 }, + expected: { h: 240, s: 100, v: 100, a: 1 }, + }, // blue + { + input: { r: 255, g: 255, b: 0, a: 1 }, + expected: { h: 60, s: 100, v: 100, a: 1 }, + }, // yellow + { + input: { r: 0, g: 255, b: 255, a: 1 }, + expected: { h: 180, s: 100, v: 100, a: 1 }, + }, // cyan + { + input: { r: 255, g: 0, b: 255, a: 1 }, + expected: { h: 300, s: 100, v: 100, a: 1 }, + }, // magenta + { input: { r: 0, g: 0, b: 0, a: 1 }, expected: { h: 0, s: 0, v: 0, a: 1 } }, // black + { + input: { r: 255, g: 255, b: 255, a: 1 }, + expected: { h: 0, s: 0, v: 100, a: 1 }, + }, // white + { + input: { r: 128, g: 128, b: 128, a: 1 }, + expected: { h: 0, s: 0, v: 50, a: 1 }, + }, // gray + { + input: { r: 230, g: 92, b: 115, a: 1 }, + expected: { h: 350, s: 60, v: 90, a: 1 }, + }, // pastel pink + { + input: { r: 112, g: 31, b: 153, a: 1 }, + expected: { h: 280, s: 80, v: 60, a: 1 }, + }, // deep purple + { + input: { r: 242, g: 97, b: 24, a: 1 }, + expected: { h: 20, s: 90, v: 95, a: 1 }, + }, // bright coral + { + input: { r: 107, g: 179, b: 131, a: 1 }, + expected: { h: 140, s: 40, v: 70, a: 1 }, + }, // muted sage + { + input: { r: 242, g: 206, b: 24, a: 1 }, + expected: { h: 50, s: 90, v: 95, a: 1 }, + }, // rich gold +]; diff --git a/test/processing/colorParser.test.ts b/test/processing/colorParser.test.ts new file mode 100644 index 0000000..131776c --- /dev/null +++ b/test/processing/colorParser.test.ts @@ -0,0 +1,166 @@ +import { ColorParser } from "../../src/processing/colorParser"; +import type { Colors } from "../../src/types"; + +describe("ColorParser", () => { + let colorParser: ColorParser; + + beforeEach(() => { + /** Testing the ColorParser class with a simple configuration */ + colorParser = new ColorParser({ + model: "rgb", + extract: match => match.join(","), + serialize: value => `rgb(${value})`, + regex: /rgb\((\d+),\s*(\d+),\s*(\d+)\)/, + channels: ["r", "g", "b"], + clamp: value => value, + }); + }); + + it("should parse a valid color string", () => { + const input = "rgb(255, 0, 0)"; + const result = colorParser.parse(input); + expect(result).toEqual([ + input, + "rgb(255,0,0)", + { + value: "255,0,0", + model: "rgb", + isValid: true, + }, + ]); + }); + + it("should parse a valid color object", () => { + const input = { r: 255, g: 0, b: 0 }; + const result = colorParser.parse(input); + expect(result).toEqual([ + input, + "rgb(255,0,0)", + { + value: "255,0,0", + model: "rgb", + isValid: true, + }, + ]); + }); + + it("should return null for an invalid color string", () => { + const input = "invalid color"; + const result = colorParser.parse(input); + expect(result).toBeNull(); + }); + + it("should return null for an invalid color object", () => { + const input = { r: "255", g: "0" }; // Missing 'b' channel + // @ts-expect-error Testing invalid input + const result = colorParser.parse(input); + expect(result).toBeNull(); + }); + + it("should use the clamp function if provided", () => { + const clampMock = jest.fn(value => ({ + r: Math.min(255, Math.max(0, Number(value.r))), + g: Math.min(255, Math.max(0, Number(value.g))), + b: Math.min(255, Math.max(0, Number(value.b))), + })); + + const colorParser = new ColorParser({ + model: "rgb", + extract: match => + ({ r: match[0], g: match[1], b: match[2] }) as Colors.Rgb, + serialize: value => `rgb(${value.r},${value.g},${value.b})`, + regex: /rgb\((\d+),\s*(\d+),\s*(\d+)\)/, + channels: ["r", "g", "b"], + clamp: clampMock, + }); + + const input = { r: "300", g: "-10", b: "0" }; + + // @ts-expect-error Edge case numerical string input + const result = colorParser.parse(input); + + expect(clampMock).toHaveBeenCalledWith({ r: "300", g: "-10", b: "0" }); + expect(result).toEqual([ + input, + "rgb(255,0,0)", + { + value: { r: "300", g: "-10", b: "0" }, + model: "rgb", + isValid: true, + }, + ]); + }); +}); + +describe("ColorParser - Invalid Configurations", () => { + it("should throw an error if 'serialize' function is not provided", () => { + expect( + () => + // @ts-expect-error Testing invalid configuration + new ColorParser({ + model: "rgb", + extract: match => match.join(","), + regex: /rgb\((\d+),\s*(\d+),\s*(\d+)\)/, + channels: ["r", "g", "b"], + clamp: value => value, + }), + ).toThrow("Missing 'serialize' function in the configuration."); + }); + + it("should throw an error if 'regex' is not provided", () => { + expect( + () => + // @ts-expect-error Testing invalid configuration + new ColorParser({ + model: "rgb", + extract: match => match.join(","), + serialize: value => `rgb(${value})`, + channels: ["r", "g", "b"], + clamp: value => value, + }), + ).toThrow("Missing 'regex' RegExp in the configuration"); + }); + + it("should throw an error if 'model' is not provided", () => { + expect( + () => + // @ts-expect-error Testing invalid configuration + new ColorParser({ + extract: match => match.join(","), + serialize: value => `rgb(${value})`, + regex: /rgb\((\d+),\s*(\d+),\s*(\d+)\)/, + channels: ["r", "g", "b"], + clamp: value => value, + }), + ).toThrow("Missing 'model' string in the configuration"); + }); + + it("should throw an error if 'extract' function is not provided", () => { + expect( + () => + // @ts-expect-error Testing invalid configuration + new ColorParser({ + model: "rgb", + serialize: value => `rgb(${value})`, + regex: /rgb\((\d+),\s*(\d+),\s*(\d+)\)/, + channels: ["r", "g", "b"], + clamp: value => value, + }), + ).toThrow("Missing 'extract' function in the configuration"); + }); + + it("should throw an error if 'clamp' function is invalid", () => { + expect( + () => + new ColorParser({ + model: "rgb", + extract: match => match.join(","), + serialize: value => `rgb(${value})`, + regex: /rgb\((\d+),\s*(\d+),\s*(\d+)\)/, + channels: ["r", "g", "b"], + // @ts-expect-error Testing invalid configuration + clamp: {}, + }), + ).toThrow("Invalid 'clamp' function in the configuration"); + }); +}); diff --git a/test/processing/conversions.test.ts b/test/processing/conversions.test.ts new file mode 100644 index 0000000..7faedf9 --- /dev/null +++ b/test/processing/conversions.test.ts @@ -0,0 +1,86 @@ +import { + convertCmykToRgb, + convertHexToRgb, + convertHslToHsv, + convertHsvToHsl, + convertHsvToRgb, + convertRgbToCmyk, + convertRgbToHex, + convertRgbToHsv, +} from "../../src/processing/conversions"; +import { cmykToRgbCases } from "./cases/cmykToRgbCases"; +import { hexToRgbCases } from "./cases/hexToRgbCases"; +import { hslToHsvCases } from "./cases/hslToHsvCases"; +import { hsvToHslCases } from "./cases/hsvToHslCases"; +import { hsvToRgbCases } from "./cases/hsvToRgbCases"; +import { rgbToCmykCases } from "./cases/rgbToCmykCases"; +import { rgbToHexCases } from "./cases/rgbToHexCases"; +import { rgbToHsvCases } from "./cases/rgbToHsvCases"; +import { runColorConversionTests } from "./conversionsTestHelpers"; + +describe("Conversions", () => { + describe("convertCmykToRgb", () => { + runColorConversionTests( + cmykToRgbCases, + convertCmykToRgb, + input => `converts CMYK ${JSON.stringify(input)} to RGB`, + ); + }); + + describe("convertHexToRgb", () => { + runColorConversionTests( + hexToRgbCases, + convertHexToRgb, + input => `converts HEX ${input} to RGB`, + ); + }); + + describe("convertHslToHsv", () => { + runColorConversionTests( + hslToHsvCases, + convertHslToHsv, + input => `converts HSL ${JSON.stringify(input)} to HSV`, + ); + }); + + describe("convertHsvToHsl", () => { + runColorConversionTests( + hsvToHslCases, + convertHsvToHsl, + input => `converts HSV ${JSON.stringify(input)} to HSL`, + ); + }); + + describe("convertHsvToRgb", () => { + runColorConversionTests( + hsvToRgbCases, + convertHsvToRgb, + input => `converts HSV ${JSON.stringify(input)} to RGB`, + ); + }); + + describe("convertRgbToCmyk", () => { + runColorConversionTests( + rgbToCmykCases, + convertRgbToCmyk, + input => `converts RGB ${JSON.stringify(input)} to CMYK`, + ); + }); + + describe("convertRgbToHex", () => { + runColorConversionTests( + rgbToHexCases, + convertRgbToHex, + input => `converts RGB ${JSON.stringify(input)} to HEX`, + false, + ); + }); + + describe("convertRgbToHsv", () => { + runColorConversionTests( + rgbToHsvCases, + convertRgbToHsv, + input => `converts RGB ${JSON.stringify(input)} to HSV`, + ); + }); +}); diff --git a/test/processing/conversionsTestHelpers.ts b/test/processing/conversionsTestHelpers.ts new file mode 100644 index 0000000..ddd32bd --- /dev/null +++ b/test/processing/conversionsTestHelpers.ts @@ -0,0 +1,20 @@ +import type { Colors } from "../../src/types"; +import {} from "../../src/utils/clampColorHelpers"; + +export const runColorConversionTests = ( + testCases: { input: InputType; expected: ExpectedType }[], + convertFn: (input: InputType, round: boolean) => ExpectedType, + description: (input: InputType) => string, + round = true, +) => { + testCases.forEach(({ input, expected }) => { + it(description(input), () => { + const result = convertFn(input, true); + if (round) { + expect(result as Colors.All).toEqual(expected as Colors.All); + } else { + expect(result).toEqual(expected); + } + }); + }); +}; diff --git a/test/processing/interconversions.test.ts b/test/processing/interconversions.test.ts new file mode 100644 index 0000000..a2cf17d --- /dev/null +++ b/test/processing/interconversions.test.ts @@ -0,0 +1,20 @@ +import { hslToRgb, rgbToHsl } from "../../src/processing/interconversions"; +import { hslToRgbCases } from "./cases/hslToRgbCases"; +import { rgbToHslCases } from "./cases/rgbToHslCases"; +import { runColorConversionTests } from "./conversionsTestHelpers"; + +describe("rgbToHsl", () => { + runColorConversionTests( + rgbToHslCases, + rgbToHsl, + input => `converts RGB ${JSON.stringify(input)} to HSV`, + ); +}); + +describe("hslToRgb", () => { + runColorConversionTests( + hslToRgbCases, + hslToRgb, + input => `converts RGB ${JSON.stringify(input)} to HSV`, + ); +}); diff --git a/test/processing/matchColor.test.ts b/test/processing/matchColor.test.ts new file mode 100644 index 0000000..0a26537 --- /dev/null +++ b/test/processing/matchColor.test.ts @@ -0,0 +1,155 @@ +import { ColorParser } from "../../src/processing/colorParser"; +import { matchColor } from "../../src/processing/matchColor"; +import type { Colors, Dye } from "../../src/types"; + +describe("matchColor", () => { + class MockParser extends ColorParser { + // @ts-expect-error Mocking parse method to return a valid color object + parse(input: Colors.Input): [Colors.Input, any, Dye.Source] | null { + if (input === "validColor") { + return [ + "validColor", + { color: "parsedColor" }, + { value: "parsedColor", model: "mock", isValid: true }, + ]; + } + return null; + } + } + + it("should throw an error if no input is provided", () => { + expect(() => matchColor()).toThrow("No color input provided"); + }); + + it("should throw an error if no parsers are provided", () => { + expect(() => matchColor("someColor")).toThrow( + "No parsers were provided for the color input. Ensure at least one parser is available", + ); + }); + + it("should throw an error if an invalid parser is provided", () => { + expect(() => matchColor.call({ parsers: [{}] }, "someColor")).toThrow( + "Invalid parser provided", + ); + }); + + it("should return parsed color if a valid parser is provided", () => { + const parsers = [ + new MockParser({ + model: "mock", + extract: match => match[0], + serialize: value => value, + regex: /validColor/, + }), + ]; + const result = matchColor.call({ parsers }, "validColor"); + expect(result).toEqual([ + "validColor", + { color: "parsedColor" }, + { value: "parsedColor", model: "mock", isValid: true }, + ]); + }); + + it("should throw an error if no valid parser is found for the input", () => { + const parsers = [ + new MockParser({ + model: "mock", + extract: match => match[0], + serialize: value => value, + regex: /validColor/, + }), + ]; + expect(() => matchColor.call({ parsers }, "invalidColor")).toThrow( + "Failed to parse color input: No valid parser found for 'string'", + ); + }); + + it("should throw an error if parser throws an error", () => { + class ErrorParser extends ColorParser { + // @ts-expect-error Mocking parse method to throw an error + parse(): Dye.ParserMatchArray | null { + throw new Error("Parser error"); + } + } + const parsers = [ + new ErrorParser({ + model: "mock", + extract: match => match[0], + serialize: value => value, + regex: /validColor/, + }), + ]; + expect(() => matchColor.call({ parsers }, "someColor")).toThrow( + "Failed to parse color input: Parser error", + ); + }); + + it("should handle multiple parsers and return the first valid parsed color", () => { + class AnotherMockParser extends ColorParser { + // @ts-expect-error Mocking parse method to return a valid color object + parse(input: Colors.Input) { + if (input === "anotherValidColor") { + return [ + "anotherValidColor", + { color: "anotherParsedColor" }, + { + value: "anotherParsedColor", + model: "anotherMock", + isValid: true, + }, + ]; + } + return null; + } + } + const parsers = [ + new MockParser({ + model: "mock", + extract: match => match[0], + serialize: value => value, + regex: /validColor/, + }), + new AnotherMockParser({ + model: "anotherMock", + extract: match => match[0], + serialize: value => value, + regex: /anotherValidColor/, + }), + ]; + const result = matchColor.call({ parsers }, "anotherValidColor"); + expect(result).toEqual([ + "anotherValidColor", + { color: "anotherParsedColor" }, + { value: "anotherParsedColor", model: "anotherMock", isValid: true }, + ]); + }); + + it("should handle multiple parsers and return the first valid parsed color even if the first parser fails", () => { + class FailingParser extends ColorParser { + // @ts-expect-error Mocking parse method to throw an error + parse(): Dye.ParserMatchArray | null { + throw new Error("Parser error"); + } + } + const parsers = [ + new FailingParser({ + model: "mock", + extract: match => match[0], + serialize: value => value, + regex: /validColor/, + }), + new MockParser({ + model: "mock", + extract: match => match[0], + serialize: value => value, + regex: /validColor/, + }), + ]; + const result = matchColor.call({ parsers }, "validColor"); + expect(result).toEqual([ + "validColor", + { color: "parsedColor" }, + { value: "parsedColor", model: "mock", isValid: true }, + ]); + }); +}); diff --git a/test/utils/acessibility.test.ts b/test/utils/acessibility.test.ts new file mode 100644 index 0000000..fab60b5 --- /dev/null +++ b/test/utils/acessibility.test.ts @@ -0,0 +1,33 @@ +import { relativeLuminance } from "../../src/utils/accessibility"; + +describe("relativeLuminance", () => { + it("should calculate the correct luminance for black", () => { + const black = { r: 0, g: 0, b: 0, a: 1 }; + expect(relativeLuminance(black)).toBeCloseTo(0); + }); + + it("should calculate the correct luminance for white", () => { + const white = { r: 255, g: 255, b: 255, a: 1 }; + expect(relativeLuminance(white)).toBeCloseTo(1); + }); + + it("should calculate the correct luminance for red", () => { + const red = { r: 255, g: 0, b: 0, a: 1 }; + expect(relativeLuminance(red)).toBeCloseTo(0.2126); + }); + + it("should calculate the correct luminance for green", () => { + const green = { r: 0, g: 255, b: 0, a: 1 }; + expect(relativeLuminance(green)).toBeCloseTo(0.7152); + }); + + it("should calculate the correct luminance for blue", () => { + const blue = { r: 0, g: 0, b: 255, a: 1 }; + expect(relativeLuminance(blue)).toBeCloseTo(0.0722); + }); + + it("should calculate the correct luminance for a gray color", () => { + const gray = { r: 128, g: 128, b: 128, a: 1 }; + expect(relativeLuminance(gray)).toBeCloseTo(0.22, 4); + }); +}); diff --git a/test/utils/colorUtils.test.ts b/test/utils/colorUtils.test.ts new file mode 100644 index 0000000..7f98ae3 --- /dev/null +++ b/test/utils/colorUtils.test.ts @@ -0,0 +1,205 @@ +import { + clamp, + expandHexString, + formatDecimal, + generateColorComponents, + modBy, + normalize8Bit, + normalizeAlpha, + normalizeDegrees, + normalizePercentage, + toHexString, +} from "../../src/utils/colorUtils"; + +describe("toHexString", () => { + it("should convert an integer to a hexadecimal string with a specified minimum length", () => { + expect(toHexString(255, 2)).toBe("ff"); + expect(toHexString(10, 2)).toBe("0a"); + expect(toHexString(10, 4)).toBe("000a"); + }); +}); + +describe("expandHexString", () => { + it("should expand a shortened hex color to a full 6- or 8-character hex string", () => { + expect(expandHexString("FFF")).toBe("FFFFFF"); + expect(expandHexString("E3E")).toBe("EE33EE"); + expect(expandHexString("E3EF")).toBe("EE33EEFF"); + expect(expandHexString("#E3EF")).toBe("EE33EEFF"); + }); + + it("should return the input string if it is already a full hex string", () => { + expect(expandHexString("#FFFFFF")).toBe("FFFFFF"); + expect(expandHexString("EE33EE")).toBe("EE33EE"); + expect(expandHexString("EE33EEFF")).toBe("EE33EEFF"); + }); +}); + +describe("modBy", () => { + test("modifies the value by the given amount", () => { + expect(modBy(50, 0.2)).toBe(60); + expect(modBy(100, 0.5)).toBe(100); + expect(modBy(200, -0.5)).toBe(100); + }); + + test("returns the original value if amount is not a valid color value", () => { + expect(modBy(50, NaN)).toBe(50); + expect(modBy(100, Infinity)).toBe(100); + expect(modBy(200, -Infinity)).toBe(200); + }); + + test("clamps the result to a maximum of 100", () => { + expect(modBy(100, 1)).toBe(100); + expect(modBy(150, 0.5)).toBe(100); + expect(modBy(200, 0.5)).toBe(100); + }); + + test("formats the result to a decimal", () => { + expect(modBy(33.3333, 0.1)).toBeCloseTo(36.67, 5); + expect(modBy(66.6667, 0.1)).toBeCloseTo(73.33, 5); + }); +}); + +describe("formatDecimal", () => { + it("should format a number to two decimal places if necessary", () => { + expect(formatDecimal(1.234)).toBe(1.23); + expect(formatDecimal(1)).toBe(1); + expect(formatDecimal()).toBe(1); + }); + + it("should handle negative numbers correctly", () => { + expect(formatDecimal(-1.234)).toBe(-1.23); + expect(formatDecimal(-1)).toBe(-1); + }); + + it("should handle zero correctly", () => { + expect(formatDecimal(0)).toBe(0); + expect(formatDecimal(0.123)).toBe(0.12); + }); + + it("should handle very small numbers correctly", () => { + expect(formatDecimal(0.0001)).toBe(0); + expect(formatDecimal(0.0099)).toBe(0.01); + }); + + it("should handle very large numbers correctly", () => { + expect(formatDecimal(1234567.987654321)).toBe(1234567.99); + expect(formatDecimal(1000000000)).toBe(1000000000); + }); +}); + +describe("clamp", () => { + it("should clamp a number within the range 0 to max", () => { + expect(clamp(5, 10)).toBe(5); + expect(clamp(15, 10)).toBe(10); + expect(clamp(-5, 10)).toBe(0); + }); + + it("should clamp a string number within the range 0 to max", () => { + expect(clamp("5", 10)).toBe(5); + expect(clamp("15", 10)).toBe(10); + expect(clamp("-5", 10)).toBe(0); + }); + + it("should handle non-numeric strings by clamping to 0", () => { + expect(clamp("abc", 10)).toBe(0); + expect(clamp("", 10)).toBe(0); + }); + + it("should handle edge cases", () => { + expect(clamp(0, 10)).toBe(0); + expect(clamp(10, 10)).toBe(10); + expect(clamp(0, 0)).toBe(0); + expect(clamp(10, 0)).toBe(0); + }); +}); +describe("normalizeDegrees", () => { + it("should normalize values within the range 0-360", () => { + expect(normalizeDegrees(370)).toBe(360); + expect(normalizeDegrees(-10)).toBe(0); + expect(normalizeDegrees(180)).toBe(180); + }); +}); + +describe("normalizePercentage", () => { + it("should normalize values within the range 0-100", () => { + expect(normalizePercentage(110)).toBe(100); + expect(normalizePercentage(-10)).toBe(0); + expect(normalizePercentage(50)).toBe(50); + }); +}); + +describe("normalize8Bit", () => { + it("should normalize values within the range 0-255", () => { + expect(normalize8Bit(300)).toBe(255); + expect(normalize8Bit(-10)).toBe(0); + expect(normalize8Bit(128)).toBe(128); + }); +}); + +describe("normalizeAlpha", () => { + it("should normalize values within the range 0-1", () => { + expect(normalizeAlpha(1.5)).toBe(1); + expect(normalizeAlpha(-0.5)).toBe(0); + expect(normalizeAlpha(0.5)).toBe(0.5); + }); + + it("should use the default value of 1 if no value is provided", () => { + expect(normalizeAlpha()).toBe(1); + }); +}); + +describe("generateColorComponents", () => { + it("should return correct symbols for non-minified, non-cssNext format with alpha = 1", () => { + const alphaValue = 1; + const result = generateColorComponents(alphaValue); + expect(result).toEqual([", ", "", "", "%"]); + }); + + it("should return correct symbols for minified, non-cssNext format with alpha = 1", () => { + const alphaValue = 1; + const result = generateColorComponents(alphaValue, { minify: true }); + expect(result).toEqual([",", "", "", ""]); + }); + + it("should return correct symbols for non-minified, cssNext format with alpha = 1", () => { + const alphaValue = 1; + const result = generateColorComponents(alphaValue, { cssNext: true }); + expect(result).toEqual([" ", "", "", "%"]); + }); + + it("should return correct symbols for minified, cssNext format with alpha = 1", () => { + const alphaValue = 1; + const result = generateColorComponents(alphaValue, { + minify: true, + cssNext: true, + }); + expect(result).toEqual([" ", "", "", ""]); + }); + + it("should return correct symbols for non-minified, non-cssNext format with alpha < 1", () => { + const alphaValue = 0.5; + const result = generateColorComponents(alphaValue); + expect(result).toEqual([", ", ", 0.5", "a", "%"]); + }); + + it("should return correct symbols for minified, non-cssNext format with alpha < 1", () => { + const alphaValue = 0.5; + const result = generateColorComponents(alphaValue, { minify: true }); + expect(result).toEqual([",", ",0.5", "a", ""]); + }); + + it("should return correct symbols for non-minified, cssNext format with alpha < 1", () => { + const alphaValue = 0.5; + const result = generateColorComponents(alphaValue, { cssNext: true }); + expect(result).toEqual([" ", " / 0.5", "", "%"]); + }); + + it("should return correct symbols for minified, cssNext format with alpha < 1", () => { + const alphaValue = 0.5; + const result = generateColorComponents(alphaValue, { + minify: true, + cssNext: true, + }); + expect(result).toEqual([" ", "/0.5", "", ""]); + }); +}); diff --git a/test/utils/normalization.test.ts b/test/utils/normalization.test.ts new file mode 100644 index 0000000..9be1e4a --- /dev/null +++ b/test/utils/normalization.test.ts @@ -0,0 +1,56 @@ +import { + clampCmyk, + clampHsl, + clampHsv, + clampRgb, +} from "../../src/utils/clampColorHelpers"; + +describe("clampColor", () => { + it("should clamp and normalize RGB values correctly", () => { + const rgb = { r: 300, g: -10, b: 128.35, a: 1.5 }; + const clampedRgb = clampRgb(rgb); + expect(clampedRgb).toEqual({ r: 255, g: 0, b: 128.35, a: 1 }); + }); + + it("should clamp and normalize RGB values correctly with rounding", () => { + const rgb = { r: 300, g: -10, b: 128.35, a: 1.5 }; + const clampedRgb = clampRgb(rgb, true); + expect(clampedRgb).toEqual({ r: 255, g: 0, b: 128, a: 1 }); + }); + + it("should clamp and normalize HSL values correctly", () => { + const hsl = { h: 370, s: 90.32, l: -10, a: 0.5 }; + const clampedHsl = clampHsl(hsl); + expect(clampedHsl).toEqual({ h: 360, s: 90.32, l: 0, a: 0.5 }); + }); + + it("should clamp and normalize HSL values correctly with rounding", () => { + const hsl = { h: 370, s: 90.32, l: -10, a: 0.5 }; + const clampedHsl = clampHsl(hsl, true); + expect(clampedHsl).toEqual({ h: 360, s: 90, l: 0, a: 0.5 }); + }); + + it("should clamp and normalize HSV values correctly", () => { + const hsv = { h: 370, s: 90.52, v: -10, a: 0.5 }; + const clampedHsv = clampHsv(hsv); + expect(clampedHsv).toEqual({ h: 360, s: 90.52, v: 0, a: 0.5 }); + }); + + it("should clamp and normalize HSV values correctly with rounding", () => { + const hsv = { h: 370, s: 90.52, v: -10, a: 0.5 }; + const clampedHsv = clampHsv(hsv, true); + expect(clampedHsv).toEqual({ h: 360, s: 91, v: 0, a: 0.5 }); + }); + + it("should clamp and normalize CMYK values correctly", () => { + const cmyk = { c: 110, m: -10, y: 2.3341, k: 300, a: 1.5 }; + const clampedCmyk = clampCmyk(cmyk); + expect(clampedCmyk).toEqual({ c: 100, m: 0, y: 2.33, k: 100, a: 1 }); + }); + + it("should clamp and normalize CMYK values correctly with rounding", () => { + const cmyk = { c: 110, m: -10, y: 2.3341, k: 300, a: 1.5 }; + const clampedCmyk = clampCmyk(cmyk, true); + expect(clampedCmyk).toEqual({ c: 100, m: 0, y: 2, k: 100, a: 1 }); + }); +}); diff --git a/test/utils/pluginHelpers.test.ts b/test/utils/pluginHelpers.test.ts new file mode 100644 index 0000000..9c6d38f --- /dev/null +++ b/test/utils/pluginHelpers.test.ts @@ -0,0 +1,56 @@ +import { createPlugin, integratePlugins } from "../../src/utils/pluginHelpers"; + +describe("createPlugin", () => { + it("should create a plugin with the specified name", () => { + const plugin = () => {}; + const namedPlugin = createPlugin("testPlugin", plugin); + expect(namedPlugin.name).toBe("testPlugin"); + }); +}); + +describe("integratePlugins", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it("should integrate custom plugins into the Dye instance", () => { + const props = { existingMethod: () => "existing" }; + const plugins = { + newMethod: () => { + return "new"; + }, + }; + + // @ts-expect-error - we're calling the plugin function directly + const extendedProps = integratePlugins(props, plugins); + + // @ts-expect-error - we're testing the plugin function directly + expect(extendedProps.existingMethod()).toBe("existing"); + expect(extendedProps.newMethod()).toBe("new"); + }); + + it("should not overwrite existing methods in the Dye instance", () => { + const props = { existingMethod: () => "existing" }; + const plugins = { + existingMethod: () => { + return "new"; + }, + }; + + // @ts-expect-error - we're calling the plugin function directly + const extendedProps = integratePlugins(props, plugins); + expect(extendedProps.existingMethod()).toBe("existing"); + }); + + it("should handle errors gracefully", () => { + const props = {}; + const plugins = { invalidPlugin: "not a function" }; + + // @ts-expect-error - we're calling the plugin function directly + const extendedProps = integratePlugins(props, plugins); + expect(extendedProps).toEqual({ + error: { + message: "Plugin 'invalidPlugin' must be a function, received string", + }, + }); + }); +}); diff --git a/test/utils/validation.test.ts b/test/utils/validation.test.ts new file mode 100644 index 0000000..dd58fb0 --- /dev/null +++ b/test/utils/validation.test.ts @@ -0,0 +1,45 @@ +import { isColorValue, isObject } from "../../src/utils/validation"; + +describe("isColorValue", () => { + it("should return true for finite numbers", () => { + expect(isColorValue(123)).toBe(true); + expect(isColorValue(0)).toBe(true); + expect(isColorValue(+123)).toBe(true); + expect(isColorValue(-123)).toBe(true); + }); + + it("should return true for numerical strings", () => { + expect(isColorValue("123")).toBe(true); + expect(isColorValue("0")).toBe(true); + expect(isColorValue("0.233")).toBe(true); + }); + + it("should return false for non-numerical strings", () => { + expect(isColorValue("abc")).toBe(false); + expect(isColorValue("123abc")).toBe(false); + expect(isColorValue("NaN")).toBe(false); + }); + + it("should return false for non-number values", () => { + expect(isColorValue(NaN)).toBe(false); + expect(isColorValue(null)).toBe(false); + expect(isColorValue(undefined)).toBe(false); + expect(isColorValue({})).toBe(false); + expect(isColorValue([])).toBe(false); + }); +}); + +describe("isObject", () => { + it("should return true for dictionary-like objects", () => { + expect(isObject({})).toBe(true); + expect(isObject({ key: "value" })).toBe(true); + }); + + it("should return false for non-objects", () => { + expect(isObject(null)).toBe(false); + expect(isObject(undefined)).toBe(false); + expect(isObject(123)).toBe(false); + expect(isObject("string")).toBe(false); + expect(isObject([])).toBe(false); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index abf9118..d359a88 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,21 +5,23 @@ // https://www.typescriptlang.org/tsconfig { - "compilerOptions": { - "declaration": true, // Generate .d.ts declaration files for better tooling and type safety. - "declarationDir": "build/@types/", // Place declaration files in this directory. - "emitDeclarationOnly": false, // Emit both JavaScript and declaration files. - "declarationMap": false, // Don't generate source maps for declaration files. - "rootDir": "src", // Specify the root directory for source files. - "outDir": "build", // Output compiled JavaScript files to this directory. - "moduleResolution": "node", // Use Node.js' module resolution strategy. - "esModuleInterop": true, // Enables interoperability between CommonJS and ES Modules. - "verbatimModuleSyntax": true, // Preserve 'import' and 'export' statements in the output. - "target": "esnext", // Compile to the latest JavaScript version. - "module": "esnext", // Use the latest ECMAScript module syntax. - "strict": true, // Enable all strict type-checking options. - "composite": false // Don't enable project references for incremental builds. - }, - "include": ["src/**/*.ts"], // Include all TypeScript files within the 'src' directory and its subdirectories. - "exclude": ["node_modules", "dist"] // Exclude the 'node_modules' and 'dist' directories from compilation. + "compilerOptions": { + "declaration": true, // Generate .d.ts declaration files for better tooling and type safety. + "declarationDir": "build/@types/", // Place declaration files in this directory. + "emitDeclarationOnly": false, // Emit both JavaScript and declaration files. + "declarationMap": false, // Don't generate source maps for declaration files. + "rootDir": ".", // Specify the root directory for source files. + "outDir": "build", // Output compiled JavaScript files to this directory. + "moduleResolution": "node", // Use Node.js' module resolution strategy. + "esModuleInterop": true, // Enables interoperability between CommonJS and ES Modules. + "verbatimModuleSyntax": true, // Preserve 'import' and 'export' statements in the output. + "target": "esnext", // Compile to the latest JavaScript version. + "module": "esnext", // Use the latest ECMAScript module syntax. + "strict": true, // Enable all strict type-checking options. + "composite": false, // Don't enable project references for incremental builds. + "noUnusedLocals": true, // Report errors on unused local variables. + "noUnusedParameters": true // Report errors on unused parameters. + }, + "include": ["src"], // Include all TypeScript files within the 'src' directory and its subdirectories. + "exclude": ["node_modules", "dist"] // Exclude the 'node_modules' and 'dist' directories from compilation. }