Skip to content

Commit

Permalink
feat!: implement basic plugin system in Colorus (#17)
Browse files Browse the repository at this point in the history
- Add helper function to check if the plugin is not a valid plugin
- Add unit tests to make sure the basic plugin system works
- Add typing defs for the plugin system
- Update the typing defs for the color objects

BREAKING CHANGE: Removes old typing [Color]Object in place of a more well-designed and elegant solution. It effectively leverages generics, mapped types, and intersection types to create a flexible and maintainable codebase.
  • Loading branch information
supitsdu authored Aug 4, 2024
1 parent ecbad19 commit a72f467
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 36 deletions.
81 changes: 47 additions & 34 deletions @types/main.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,27 @@
* The `colorus-js` module allows for easy manipulation and conversion of colors between different formats.
*/
declare module 'colorus-js' {
export interface RgbObject {
r: number | string
g: number | string
b: number | string
a?: number | string
}
type ColorChannel = number | string

export interface HslObject {
h: number | string
s: number | string
l: number | string
a?: number | string
}
export type BaseColor<K extends string, T> = {
[key in K]: T
} & { a?: T }

export interface HsvObject {
h: number | string
s: number | string
v: number | string
a?: number | string
}
export type AnyRgb<T = ColorChannel> = BaseColor<'r' | 'g' | 'b', T>
export type RgbColor = AnyRgb<number>

export interface CmykObject {
c: number | string
m: number | string
y: number | string
k: number | string
a?: number | string
}
export type AnyHsl<T = ColorChannel> = BaseColor<'h' | 's' | 'l', T>
export type HslColor = AnyHsl<number>

export type AnyHsv<T = ColorChannel> = BaseColor<'h' | 's' | 'v', T>
export type HsvColor = AnyHsv<number>

export type AnyCmyk<T = ColorChannel> = BaseColor<'c' | 'm' | 'y' | 'k', T>
export type CmykColor = AnyCmyk<number>

export type AnyColorType = 'rgb' | 'hsl' | 'hsv' | 'cmyk'

export type AnyColorObject = RgbObject | HslObject | HsvObject | CmykObject
export type AnyColorObject = AnyRgb | AnyHsl | AnyHsv | AnyCmyk

export type AnyColor = string | AnyColorObject

Expand Down Expand Up @@ -65,6 +54,27 @@ declare module 'colorus-js' {
minify?: boolean
}

/**
* Represents a plugin function that extends the Colorus instance with custom methods.
*
* @param this The Colorus instance to which the plugin methods will be added.
* @param args Any additional arguments passed to the plugin method.
*
* @returns An object containing the plugin methods to be added to the Colorus instance.
* - Keys should be the names of the plugin methods.
* - Values should be the corresponding functions.
*/
export type ColorusPlugin = (this: Colorus, ...args: any[]) => any

export interface ColorusOptions {
/**
* An optional object containing plugin functions to extend the Colorus instance.
*/
plugins?: {
[methodName: string]: ColorusPlugin
}
}

/**
* Utility that provides methods for working with colors.
*
Expand All @@ -84,11 +94,14 @@ declare module 'colorus-js' {
*/
export class Colorus {
/**
* Creates a new Colorus instance with the provided input.
* Constructs a new Colorus instance with the given input and optional plugins.
* @param input - The color input string or object.
* @throws If the input is not `undefined` or valid color format (e.g. `string` or `object`).
* @param options - Optional configuration options, including plugins.
* @throws If the input is not `undefined` or a valid color format (e.g. `string` or `object`).
* @throws If `options` is not a plain object.
* @throws If `options.plugins` is present but not a plain object with method names as keys and plugin functions as values.
*/
constructor(input?: AnyColor)
constructor(input?: AnyColor, options?: ColorusOptions)

/**
* Analytical method to quickly test the `input` for any valid color.
Expand All @@ -99,7 +112,7 @@ declare module 'colorus-js' {
* Colorus.test({ r: 255, g: 0, b: 0 }); // Returns: 'rgb'
* Colorus.test('#c('); // Returns: null
*/
static test(input?: unknown): AnyColorType | null
static test(input?: AnyColor | unknown): AnyColorType | null

/** Get the type of the current color. */
get colorType(): AnyColorType | undefined
Expand All @@ -112,16 +125,16 @@ declare module 'colorus-js' {
get luminance(): number

/** Get the `sRGB` object representation of the current color. */
get rgb(): RgbObject
get rgb(): RgbColor

/** Get the `HSL` object representation of the current color. */
get hsl(): HslObject
get hsl(): HslColor

/** Get the `HSV` object representation of the current color. */
get hsv(): HsvObject
get hsv(): HsvColor

/** Get the `CMYK` object representation of the current color. */
get cmyk(): CmykObject
get cmyk(): CmykColor

/**
* Convert the current color to hexadecimal format.
Expand Down
14 changes: 14 additions & 0 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ export const nan = v => typeof v != 'number' || isNaN(v) || !isFinite(v)
*/
export const nao = v => typeof v !== 'object' || Array.isArray(v)

/**
* Check if the plugin is Not a Plugin
* @param {Object} plugins An key-value object with plugin functions to apply.
* @param {string} name Method name of the Plugin
* @return {boolean|undefined} True if the plugins is not valid, undefined in case it's valid.
*/
export const isNotPlugin = function (plugins, name) {
if (!Object.hasOwnProperty.call(plugins, name)) return true

if (typeof plugins[name] !== 'function') {
throw new TypeError(`Invalid plugin for '${name}': Expected a function.`)
}
}

/**
* Checks if the provided object represents an RGB color.
* @param {object} obj - The object to be checked.
Expand Down
43 changes: 41 additions & 2 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,59 @@ import { contrastRatio, relativeLuminance } from './accessibility'
import ColorFormatter from './colorFormatter'
import * as compose from './compose'
import * as conversion from './conversion'
import { nao, isNotPlugin } from './helpers'
import * as serialize from './serialize'

/**
* Validates the options object passed to the Colorus constructor.
*
* @param {Object} [options={}] - An optional configuration object.
* @param {Object} [options.plugins] - An optional object containing plugin functions to extend the Colorus instance.
* - Keys should be the names of the plugin methods.
* - Values should be functions that take the Colorus instance as `this` and any additional arguments.
*
* @throws {TypeError} If `options` is not a plain object or if `options.plugins` is present but not a plain object.
*/
function validateOptions(options = {}) {
if (nao(options)) {
throw new TypeError(
`Invalid options: Expected a plain object with method names as keys. Received ${Array.isArray(options) ? 'an array' : typeof options}`
)
}

const { plugins } = options

if (typeof plugins !== 'undefined' && nao(plugins)) {
throw new TypeError(
`Invalid plugins: Expected a plain object with method names as keys. Received ${Array.isArray(plugins) ? 'an array' : typeof plugins}`
)
}
}

/**
* Utility class providing methods for working with colors.
*/
export class Colorus {
#data = {}

/**
* Constructs a new Colorus instance with the given input.
* Constructs a new Colorus instance with the given input and optional plugins.
* @param {string|Object} input - The color input string or object.
* @param {Object} [options] - Optional configuration options.
* @param {Object} [options.plugins] - An key-value object with plugin functions to apply.
*/
constructor(input) {
constructor(input, options = {}) {
this.#data = serialize.fromUserInput(input)

validateOptions(options)

for (const methodName in options.plugins) {
if (isNotPlugin(options.plugins, methodName)) continue

this[methodName] = function (...args) {
return options.plugins[methodName].call(this, ...args)
}
}
}

/** Tests the `input` for a valid color.
Expand Down
43 changes: 43 additions & 0 deletions test/main.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,48 @@
import { Colorus } from '../src/main'

describe('Plugin Functionality', () => {
it('should add a plugin method to the Colorus instance', () => {
const color = new Colorus('#333', {
plugins: {
getHue: function () {
return this.hsl.h
}
}
})

expect(color).toHaveProperty('getHue')
expect(typeof color.getHue).toBe('function')
})

it('should allow the plugin method to access the Colorus instance data', () => {
const color = new Colorus('rgb(20, 120, 80)', {
plugins: {
getHue: function () {
return this.hsl.h
}
}
})

expect(color.getHue()).toBe(156)
})

it('should handle multiple plugin methods', () => {
const color = new Colorus('#FF0000', {
plugins: {
getHue: function () {
return this.hsl.h
},
isRed: function () {
return this.rgb.r === 255 && this.rgb.g === 0 && this.rgb.b === 0
}
}
})

expect(color.getHue()).toBe(0)
expect(color.isRed()).toBe(true)
})
})

describe('Colorus.darken()', () => {
it('darkens by default amount', () => {
expect(new Colorus('#333').darken().toHex()).toBe('#2E2E2E')
Expand Down

0 comments on commit a72f467

Please sign in to comment.