Skip to content

Commit

Permalink
fix(color-validator): improve handling of non-strings
Browse files Browse the repository at this point in the history
  • Loading branch information
decanTyme committed Feb 11, 2023
1 parent 8798cac commit d07665c
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 102 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ yarn-*.log
# Parcel Artifacts
.parcel-cache

# Build Artifacts
dist

# Jest Artifacts
coverage
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
"dist"
],
"scripts": {
"prepare": "yarn test",
"prepublishOnly": "cross-env NODE_ENV=production yarn build",
"watch": "parcel watch",
"dev": "concurrently \"parcel watch\" \"yarn test:dev\"",
"dev": "concurrently \"yarn watch\" \"yarn test:dev\"",
"prebuild": "yarn test",
"build": "parcel build",
"test": "jest",
"test:dev": "jest --watch",
Expand Down
69 changes: 27 additions & 42 deletions src/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,23 @@
// Copyright (c) Tailwind Labs, Inc. (https://tailwindcss.com/)
// and used under the terms of the MIT license

import namedColors from "color-name"
import Colors from "color-name"
import {
TailwindColorGroup,
TailwindThemeColors,
} from "tailwindcss/tailwind-config"
import { Color, Maybe } from "./types"
import invariant from "tiny-invariant"
import { Color } from "./types"

const HEX = /^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i
const SHORT_HEX = /^#([a-f\d])([a-f\d])([a-f\d])([a-f\d])?$/i
const VALUE = `(?:\\d+|\\d*\\.\\d+)%?`
const SEP = `(?:\\s*,\\s*|\\s+)`
const ALPHA_SEP = `\\s*[,/]\\s*`

const RGB = new RegExp(
`^rgba?\\(\\s*(${VALUE})${SEP}(${VALUE})${SEP}(${VALUE})(?:${ALPHA_SEP}(${VALUE}))?\\s*\\)$`
)
const HSL = new RegExp(
`^hsla?\\(\\s*((?:${VALUE})(?:deg|rad|grad|turn)?)${SEP}(${VALUE})${SEP}(${VALUE})(?:${ALPHA_SEP}(${VALUE}))?\\s*\\)$`
)

export const flattenColorPalette = (
colors: TailwindThemeColors
Expand All @@ -35,38 +34,17 @@ export const flattenColorPalette = (
)
)

export function parseColor(value: string): Maybe<Color> {
if (!value) return null

export function parseColor(value: string): Color {
value = value.trim()

if (value === "transparent") {
return { mode: "rgb", color: ["0", "0", "0"], alpha: "0" }
}

if (value in namedColors) {
return {
mode: "rgb",
color: namedColors[value as keyof typeof namedColors].map((v) =>
v.toString()
),
}
}

const hex = value
.replace(SHORT_HEX, (_, r, g, b, a) =>
["#", r, r, g, g, b, b, a ? a + a : ""].join("")
)
.match(HEX)

if (hex) {
if (value in Colors) {
return {
mode: "rgb",
color: [
parseInt(hex[1], 16),
parseInt(hex[2], 16),
parseInt(hex[3], 16),
].map((v) => v.toString()),
alpha: hex[4] ? (parseInt(hex[4], 16) / 255).toString() : undefined,
color: Colors[value as keyof typeof Colors].map((v) => v.toString()),
}
}

Expand All @@ -80,20 +58,27 @@ export function parseColor(value: string): Maybe<Color> {
}
}

const hslMatch = value.match(HSL)
// We already filter out and validate before even trying
// to parse, so this should now always match a hex
const hex = value
.replace(SHORT_HEX, (_, r, g, b, a) =>
["#", r, r, g, g, b, b, a ? a + a : ""].join("")
)
.match(HEX)

invariant(hex, `Invalid value: ${value}`)

if (hslMatch) {
return {
mode: "hsl",
color: [hslMatch[1], hslMatch[2], hslMatch[3]].map((v) => v.toString()),
alpha: hslMatch[4]?.toString?.(),
}
return {
mode: "rgb",
color: [
parseInt(hex[1], 16),
parseInt(hex[2], 16),
parseInt(hex[3], 16),
].map((v) => v.toString()),
alpha: hex[4] ? (parseInt(hex[4], 16) / 255).toString() : undefined,
}

return null
}

export function formatColor({ mode, color, alpha }: Color) {
if (!color) return null
return `${mode}(${color?.join(" ")}${alpha ? ` / ${alpha}` : ""})`
export function formatColor({ mode, color, alpha }: Color): string {
return `${mode}(${color.join(" ")}${alpha ? ` / ${alpha}` : ""})`
}
29 changes: 13 additions & 16 deletions src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import type { Chart, Plugin } from "chart.js"
import set from "lodash.set"
import get from "lodash.get"
import set from "lodash.set"
import resolveConfig from "tailwindcss/resolveConfig"
import { TailwindConfig } from "tailwindcss/tailwind-config"
import invariant from "tiny-invariant"

import { flattenColorPalette, formatColor, parseColor } from "./color"
import { Maybe, MaybeArray, ParsableOptions } from "./types"
import { hasAlpha, twColorValidator } from "./utils"
import { MaybeArray, ParsableOptions } from "./types"
import { hasValidAlpha, isValidArray, twColorValidator } from "./utils"

const twColorsPlugin = (
tailwindConfig: TailwindConfig,
Expand All @@ -25,30 +26,25 @@ const twColorsPlugin = (

const parseTailwindColor = (
value: MaybeArray<string>
): Maybe<MaybeArray<string>> => {
if (!value) return null
): MaybeArray<string> => {
invariant(value, `Invalid value: ${value}`)

if (Array.isArray(value))
if (isValidArray(value)) {
return value.map((_val) => <string>parseTailwindColor(_val))
}

if (hasAlpha(value)) {
if (hasValidAlpha(value)) {
const [color, alpha] = value.split("/")

const parsedColor = parseColor(<string>parseTailwindColor(color))

if (!parsedColor) return null

return formatColor({
...parsedColor,
alpha: parseInt(alpha, 10) / 100,
})
}

const parsedColor = parseColor(colorPalette[value] ?? value)

if (!parsedColor) return null

return formatColor(parsedColor)
return formatColor(parseColor(colorPalette[value] ?? value))
}

const plugin = (chart: Chart) => {
Expand All @@ -64,7 +60,6 @@ const twColorsPlugin = (
"pointHoverBorderColor",
"fill.above",
"fill.below",
"fill",
]

parsableOpts.forEach((parsableOpt) => {
Expand All @@ -76,7 +71,9 @@ const twColorsPlugin = (
get(dataset, parsableOpt) ||
(isValidTwColor(chartOpt) ? chartOpt : defaultOpt)

if (color) set(dataset, parsableOpt, parseTailwindColor(color))
if (isValidTwColor(color, { strict: false })) {
set(dataset, parsableOpt, parseTailwindColor(color))
}
})
})
}
Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ export type Color = {
color: Array<string>
alpha?: string | number
}

export interface TwColorValidatorOpts {
strict?: boolean
}
40 changes: 32 additions & 8 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,46 @@
import { TailwindColorGroup } from "tailwindcss/tailwind-config"
import { TwColorValidatorOpts } from "./types"

const VALID_COLOR_FORM = `((#[0-9A-Fa-f]{6})|(?!#)[a-z]+(-?[0-9]{2,3}|))`
const VALID_HEX = `#([A-Fa-f\\d]{6})([A-Fa-f\\d]{2})?(?!\\/(?!\\S))`
const VALID_COLOR_FORM = `${VALID_HEX}|(?<!#)\\b[a-z]+\\b(-?[0-9]{2,3}|)(?!-)`
const VALID_ALPHA = `\\/(?=(\\b([1-9]|[1-9][0-9]|100)\\b))`

export const isValidArray = (value: unknown): value is Array<string> =>
Array.isArray(value) && value.every((v) => typeof v === "string")

/**
* Checks if a given color is valid according to the
* specified TailwindCSS config.
* Checks if a given color/value is valid, and whether it should to be parsed.
*
* @returns A validator function
* @returns A validator function.
*/
export const twColorValidator =
(colorPalette: TailwindColorGroup) =>
(value: string): boolean =>
Object.hasOwn(colorPalette, value)
(value: unknown, { strict = true }: TwColorValidatorOpts = {}): boolean => {
if (!value) return false

// Can be any valid value, not just what's in the config
if (!strict) {
// Since some colors are stored in arrays, assuming the values
// in the array are valid, the array itself is valid
if (isValidArray(value)) return true

if (
typeof value !== "string" ||
/\b(?:rgba?)\b|\b(?:hsla?)\b/gi.test(value)
) {
return false
}

return new RegExp(`${VALID_COLOR_FORM}(${VALID_ALPHA})?`).test(value)
}

// Strictly from the specified config
return typeof value === "string" && Object.hasOwn(colorPalette, value)
}

/**
* Checks first if the color is in a valid form, then
* checks if it has a valid `alpha` color channel.
*/
export const hasAlpha = (value: string): boolean =>
new RegExp(`(?<=(${VALID_COLOR_FORM}))${VALID_ALPHA}`, "g").test(value)
export const hasValidAlpha = (value: string): boolean =>
new RegExp(`(?<=(${VALID_COLOR_FORM}))${VALID_ALPHA}`).test(value)
Loading

0 comments on commit d07665c

Please sign in to comment.