From 6ebed1386a2f03969c2bcbe7b9b22475570d9471 Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Tue, 4 Sep 2018 14:05:15 +0200 Subject: [PATCH] Adds color duplicates analysis --- package.json | 3 +- readme.md | 3 +- src/analyzer/values/colors.js | 81 ++++++++++++++++- test/analyzer/index.js | 3 +- test/analyzer/values/input.css | 11 +++ test/analyzer/values/output.json | 148 ++++++++++++++++++++++++++++++- yarn.lock | 4 + 7 files changed, 245 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 182e734..945f066 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "path": "^0.12.7", "postcss": "^7.0.1", "postcss-values-parser": "^1.5.0", - "specificity": "^0.4.0" + "specificity": "^0.4.0", + "tinycolor2": "^1.4.1" }, "devDependencies": { "ava": "^0.25.0", diff --git a/readme.md b/readme.md index 6c70123..47c9579 100644 --- a/readme.md +++ b/readme.md @@ -158,7 +158,8 @@ analyze('foo {}') // colors: { // total: 0, // totalUnique: 0, -// unique: [] +// unique: [], +// duplicates: [] // }, // fontfamilies: { // total: 0, diff --git a/src/analyzer/values/colors.js b/src/analyzer/values/colors.js index 6cd0b16..b6c4af2 100644 --- a/src/analyzer/values/colors.js +++ b/src/analyzer/values/colors.js @@ -1,5 +1,6 @@ const valueParser = require('postcss-values-parser') const cssColorNames = require('css-color-names') +const tinycolor = require('tinycolor2') const uniquer = require('../../utils/uniquer') @@ -47,6 +48,81 @@ function extractColorsFromDeclaration(declaration) { return declaration } +const addCount = color => { + return { + ...color, + count: color.aliases.reduce((acc, curr) => { + return acc + curr.count + }, 0) + } +} + +const addShortestNotation = color => { + return { + ...color, + value: [...color.aliases].sort((a, b) => { + return a.value.length - b.value.length + }).shift().value + } +} + +const addAliases = (acc, curr) => { + if (!acc[curr.key]) { + acc[curr.key] = { + key: curr.key, + aliases: [] + } + } + + acc[curr.key] = { + ...acc[curr.key], + aliases: [...acc[curr.key].aliases, curr] + } + + return acc +} + +const filterDuplicateColors = color => { + // Filter out the actual duplicate colors + return color.aliases.length > 1 +} + +const validateColor = color => { + return tinycolor(color.value).isValid() +} + +const normalizeColors = color => { + // Add a normalized value + return { + ...color, + key: tinycolor(color.value).toHslString() + } +} + +const rmTmpProps = color => { + // Remove temporary props that were needed for analysis + const {key, ...restColor} = color + return { + ...restColor, + aliases: color.aliases.map(alias => { + const {key, ...restAlias} = alias + return restAlias + }) + } +} + +const withAliases = colors => Object + .values( + colors + .filter(validateColor) + .map(normalizeColors) + .reduce(addAliases, {}) + ) + .filter(filterDuplicateColors) + .map(addCount) + .map(addShortestNotation) + .map(rmTmpProps) + module.exports = declarations => { const all = declarations .map(extractColorsFromDeclaration) @@ -55,9 +131,12 @@ module.exports = declarations => { .reduce((allColors, declarationColors) => { return [...allColors, ...declarationColors] }, []) + const {totalUnique, unique} = uniquer(all) return { total: all.length, - ...uniquer(all) + unique, + totalUnique, + duplicates: withAliases(unique) } } diff --git a/test/analyzer/index.js b/test/analyzer/index.js index 0f4628f..ba6291a 100644 --- a/test/analyzer/index.js +++ b/test/analyzer/index.js @@ -141,7 +141,8 @@ test('Returns the correct analysis object structure', async t => { colors: { total: 0, totalUnique: 0, - unique: [] + unique: [], + duplicates: [] }, fontfamilies: { total: 0, diff --git a/test/analyzer/values/input.css b/test/analyzer/values/input.css index ba7ee80..d6ac3d9 100644 --- a/test/analyzer/values/input.css +++ b/test/analyzer/values/input.css @@ -48,6 +48,7 @@ color: hsl(270,60%,70%); color: hsl(270, 60%, 70%); color: hsl(270 60% 70%); + /** tinycolor doesn't support these color formats, so they won't show up as aliases/duplicates */ color: hsl(270deg, 60%, 70%); color: hsl(4.71239rad, 60%, 70%); color: hsl(.75turn, 60%, 70%); @@ -57,6 +58,16 @@ color: hsl(270, 60%, 50%, 15%); color: hsl(270 60% 50% / .15); color: hsl(270 60% 50% / 15%); + + /* Duplicate colors */ + color: #000; + color: #000000; + color: black; + color: black; /* duplicate */ + color: rgb(0,0,0); + color: rgba(0,0,0,1); + color: hsl(0,0,0); + color: hsla(0,0,0,1); } .color-keyword { outline: 1px solid tomato; diff --git a/test/analyzer/values/output.json b/test/analyzer/values/output.json index e2c450c..eb56975 100644 --- a/test/analyzer/values/output.json +++ b/test/analyzer/values/output.json @@ -1,5 +1,5 @@ { - "total": 83, + "total": 91, "fontfamilies": { "total": 18, "totalUnique": 12, @@ -121,12 +121,20 @@ "count": 1 } ], - "share": 0.04819277108433735 + "share": 0.04395604395604396 }, "colors": { - "total": 29, - "totalUnique": 28, + "total": 37, + "totalUnique": 35, "unique": [ + { + "value": "#000", + "count": 1 + }, + { + "value": "#000000", + "count": 1 + }, { "value": "#0000ff00", "count": 1 @@ -147,10 +155,18 @@ "value": "Aqua", "count": 1 }, + { + "value": "black", + "count": 2 + }, { "value": "hsl(.75turn, 60%, 70%)", "count": 1 }, + { + "value": "hsl(0,0,0)", + "count": 1 + }, { "value": "hsl(100, 10%, 20%)", "count": 1 @@ -198,6 +214,10 @@ "value": "hsl(4.71239rad, 60%, 70%)", "count": 1 }, + { + "value": "hsla(0,0,0,1)", + "count": 1 + }, { "value": "hsla(100, 20%, 30%, 0.5)", "count": 1 @@ -206,6 +226,10 @@ "value": "purple", "count": 2 }, + { + "value": "rgb(0,0,0)", + "count": 1 + }, { "value": "rgb(100, 200, 10)", "count": 1 @@ -214,6 +238,10 @@ "value": "rgb(255, 255, 255)", "count": 1 }, + { + "value": "rgba(0,0,0,1)", + "count": 1 + }, { "value": "rgba(100, 200, 10, .5)", "count": 1 @@ -238,6 +266,118 @@ "value": "whitesmoke", "count": 1 } + ], + "duplicates": [ + { + "count": 8, + "value": "#000", + "aliases": [ + { + "value": "#000", + "count": 1 + }, + { + "value": "#000000", + "count": 1 + }, + { + "value": "black", + "count": 2 + }, + { + "value": "hsl(0,0,0)", + "count": 1 + }, + { + "value": "hsla(0,0,0,1)", + "count": 1 + }, + { + "value": "rgb(0,0,0)", + "count": 1 + }, + { + "value": "rgba(0,0,0,1)", + "count": 1 + } + ] + }, + { + "value": "#fff", + "count": 4, + "aliases": [ + { + "count": 1, + "value": "#fff" + }, + { + "count": 1, + "value": "hsl(360, 100%, 100%)" + }, + { + "count": 1, + "value": "rgb(255, 255, 255)" + }, + { + "count": 1, + "value": "white" + } + ] + }, + { + "value": "hsl(270 60% 50% / .15)", + "count": 4, + "aliases": [ + { + "count": 1, + "value": "hsl(270 60% 50% / .15)" + }, + { + "count": 1, + "value": "hsl(270 60% 50% / 15%)" + }, + { + "count": 1, + "value": "hsl(270, 60%, 50%, .15)" + }, + { + "count": 1, + "value": "hsl(270, 60%, 50%, 15%)" + } + ] + }, + { + "value": "hsl(270 60% 70%)", + "count": 3, + "aliases": [ + { + "count": 1, + "value": "hsl(270 60% 70%)" + }, + { + "count": 1, + "value": "hsl(270, 60%, 70%)" + }, + { + "count": 1, + "value": "hsl(270,60%,70%)" + } + ] + }, + { + "value": "rgba(100, 200, 10, .5)", + "count": 2, + "aliases": [ + { + "count": 1, + "value": "rgba(100, 200, 10, .5)" + }, + { + "count": 1, + "value": "rgba(100, 200, 10, 0.5)" + } + ] + } ] } } diff --git a/yarn.lock b/yarn.lock index 3879cf0..a29de9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4474,6 +4474,10 @@ timed-out@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" +tinycolor2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"