@@ -0,0 +1,11 @@ | |||
root = true | |||
[*] | |||
charset = utf-8 | |||
end_of_line = lf | |||
indent_size = 2 | |||
indent_style = space | |||
insert_final_newline = true | |||
max_line_length = 120 | |||
tab_width = 8 | |||
trim_trailing_whitespace = true |
@@ -0,0 +1,6 @@ | |||
*.log | |||
.DS_Store | |||
node_modules | |||
dist | |||
coverage/ | |||
.idea/ |
@@ -0,0 +1,6 @@ | |||
{ | |||
"jsxSingleQuote": false, | |||
"singleQuote": true, | |||
"semi": false, | |||
"trailingComma": "es5" | |||
} |
@@ -0,0 +1,21 @@ | |||
MIT License | |||
Copyright (c) 2020 Allan Crisostomo | |||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||
of this software and associated documentation files (the "Software"), to deal | |||
in the Software without restriction, including without limitation the rights | |||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
copies of the Software, and to permit persons to whom the Software is | |||
furnished to do so, subject to the following conditions: | |||
The above copyright notice and this permission notice shall be included in all | |||
copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
SOFTWARE. |
@@ -0,0 +1,99 @@ | |||
# TSDX User Guide | |||
Congrats! You just saved yourself hours of work by bootstrapping this project with TSDX. Let’s get you oriented with what’s here and how to use it. | |||
> This TSDX setup is meant for developing libraries (not apps!) that can be published to NPM. If you’re looking to build a Node app, you could use `ts-node-dev`, plain `ts-node`, or simple `tsc`. | |||
> If you’re new to TypeScript, checkout [this handy cheatsheet](https://devhints.io/typescript) | |||
## Commands | |||
TSDX scaffolds your new library inside `/src`. | |||
To run TSDX, use: | |||
```bash | |||
npm start # or yarn start | |||
``` | |||
This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`. | |||
To do a one-off build, use `npm run build` or `yarn build`. | |||
To run tests, use `npm test` or `yarn test`. | |||
## Configuration | |||
Code quality is set up for you with `prettier`, `husky`, and `lint-staged`. Adjust the respective fields in `package.json` accordingly. | |||
### Jest | |||
Jest tests are set up to run with `npm test` or `yarn test`. | |||
#### Setup Files | |||
This is the folder structure we set up for you: | |||
```txt | |||
/src | |||
index.tsx # EDIT THIS | |||
/test | |||
blah.test.tsx # EDIT THIS | |||
.gitignore | |||
package.json | |||
README.md # EDIT THIS | |||
tsconfig.json | |||
``` | |||
### Rollup | |||
TSDX uses [Rollup](https://rollupjs.org) as a bundler and generates multiple rollup configs for various module formats and build settings. See [Optimizations](#optimizations) for details. | |||
### TypeScript | |||
`tsconfig.json` is set up to interpret `dom` and `esnext` types, as well as `react` for `jsx`. Adjust according to your needs. | |||
## Continuous Integration | |||
### GitHub Actions | |||
A simple action is included that runs these steps on all pushes: | |||
- Installs deps w/ cache | |||
- Lints, tests, and builds | |||
## Optimizations | |||
Please see the main `tsdx` [optimizations docs](https://github.com/palmerhq/tsdx#optimizations). In particular, know that you can take advantage of development-only optimizations: | |||
```js | |||
// ./types/index.d.ts | |||
declare var __DEV__: boolean; | |||
// inside your code... | |||
if (__DEV__) { | |||
console.log('foo'); | |||
} | |||
``` | |||
You can also choose to install and use [invariant](https://github.com/palmerhq/tsdx#invariant) and [warning](https://github.com/palmerhq/tsdx#warning) functions. | |||
## Module Formats | |||
CJS, ESModules, and UMD module formats are supported. | |||
The appropriate paths are configured in `package.json` and `dist/index.js` accordingly. Please report if any issues are found. | |||
## Named Exports | |||
Per Palmer Group guidelines, [always use named exports.](https://github.com/palmerhq/typescript#exports) Code split inside your React app instead of your React library. | |||
## Including Styles | |||
There are many ways to ship styles, including with CSS-in-JS. TSDX has no opinion on this, configure how you like. | |||
For vanilla CSS, you can include it at the root directory and add it to the `files` section in your `package.json`, so that it can be imported separately by your users and run through their bundler's loader. | |||
## Publishing to NPM | |||
We recommend using [np](https://github.com/sindresorhus/np). |
@@ -0,0 +1,56 @@ | |||
{ | |||
"version": "1.0.0", | |||
"license": "MIT", | |||
"main": "dist/index.js", | |||
"typings": "dist/index.d.ts", | |||
"files": [ | |||
"dist", | |||
"src" | |||
], | |||
"engines": { | |||
"node": ">=10" | |||
}, | |||
"scripts": { | |||
"start": "tsdx watch", | |||
"build": "tsdx build", | |||
"test": "tsdx test", | |||
"lint": "tsdx lint", | |||
"prepare": "tsdx build" | |||
}, | |||
"peerDependencies": { | |||
"get-pixels": "^3.3.2", | |||
"get-rgba-palette": "^2.0.1", | |||
"image-size": "^0.9.1", | |||
"numeral": "^2.0.6" | |||
}, | |||
"husky": { | |||
"hooks": { | |||
"pre-commit": "tsdx lint" | |||
} | |||
}, | |||
"prettier": { | |||
"printWidth": 80, | |||
"semi": true, | |||
"singleQuote": true, | |||
"trailingComma": "es5" | |||
}, | |||
"name": "@theoryofnekomata/image-commons", | |||
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | |||
"module": "dist/image-commons.esm.js", | |||
"repository": { | |||
"type": "git", | |||
"url": "git@code.modal.sh:TheoryOfNekomata/image-commons.git" | |||
}, | |||
"devDependencies": { | |||
"@types/numeral": "^0.0.28", | |||
"fast-check": "^2.3.0", | |||
"get-pixels": "^3.3.2", | |||
"get-rgba-palette": "^2.0.1", | |||
"husky": "^4.3.0", | |||
"image-size": "^0.9.1", | |||
"numeral": "^2.0.6", | |||
"tsdx": "^0.13.3", | |||
"tslib": "^2.0.1", | |||
"typescript": "^4.0.3" | |||
} | |||
} |
@@ -0,0 +1,66 @@ | |||
import * as fc from 'fast-check' | |||
import formatPixelCount from './formatPixelCount' | |||
it('should exist', () => { | |||
expect(formatPixelCount).toBeDefined() | |||
}) | |||
it('should be a function', () => { | |||
expect(typeof formatPixelCount).toBe('function') | |||
}) | |||
it('should take 1 argument', () => { | |||
expect(formatPixelCount).toHaveLength(1) | |||
}) | |||
describe('on non-numeric arguments', () => { | |||
it('should throw an error', () => { | |||
fc.assert( | |||
fc.property( | |||
fc.anything().filter(v => typeof v !== 'number'), | |||
v => { | |||
expect(() => formatPixelCount(v as number)).toThrow(TypeError) | |||
} | |||
) | |||
) | |||
}) | |||
}) | |||
describe('on numeric arguments', () => { | |||
it('should throw an error on NaN', () => { | |||
expect(() => formatPixelCount(NaN)).toThrow(RangeError) | |||
}) | |||
it('should format values of |x| < 1000', () => { | |||
fc.assert( | |||
fc.property( | |||
fc.integer().filter(v => Math.abs(v) < 1000), | |||
v => { | |||
expect(formatPixelCount(v)).toMatch(/^-?\d+ P$/) | |||
} | |||
) | |||
) | |||
}) | |||
it('should format values of 1000 < |x| < 1000000', () => { | |||
fc.assert( | |||
fc.property( | |||
fc.integer().filter(v => 1000 < Math.abs(v) && Math.abs(v) < 1000000), | |||
v => { | |||
expect(formatPixelCount(v)).toMatch(/^-?\d+\.\d\d kP$/) | |||
} | |||
) | |||
) | |||
}) | |||
it('should format values of |x| >= 1000000', () => { | |||
fc.assert( | |||
fc.property( | |||
fc.integer().filter(v => Math.abs(v) >= 1000000), | |||
v => { | |||
expect(formatPixelCount(v)).toMatch(/^-?\d+\.\d\d .P$/) | |||
} | |||
) | |||
) | |||
}) | |||
}) |
@@ -0,0 +1,28 @@ | |||
import numeral from 'numeral' | |||
type FormatPixelCount = (n: number) => string | |||
const formatPixelCount: FormatPixelCount = n => { | |||
if (typeof n as unknown !== 'number') { | |||
throw TypeError('Argument should be a number.') | |||
} | |||
if (isNaN(n)) { | |||
throw RangeError('Cannot format NaN.') | |||
} | |||
const absValue = Math.abs(n) | |||
const theNumeral = numeral(n) | |||
if (absValue < 1000) { | |||
return `${absValue} P` | |||
} | |||
if (absValue < 1000000) { | |||
return `${theNumeral.format('0.00 a')}P` | |||
} | |||
return `${theNumeral.format('0.00 a').toUpperCase()}P` | |||
} | |||
export default formatPixelCount |
@@ -0,0 +1,102 @@ | |||
import * as fc from 'fast-check' | |||
import { imageSize } from 'image-size' | |||
import getDimensions, { Dimensions } from './getDimensions' | |||
jest.mock('image-size') | |||
it('should exist', () => { | |||
expect(getDimensions).toBeDefined() | |||
}) | |||
it('should be a callable', () => { | |||
expect(typeof getDimensions).toBe('function') | |||
}) | |||
it('should accept 1 argument', () => { | |||
expect(getDimensions).toHaveLength(1) | |||
}) | |||
it('should throw an error given non-string arguments', async () => { | |||
await fc.assert( | |||
fc.asyncProperty( | |||
fc.anything().filter(v => typeof v !== 'string'), | |||
async notAString => { | |||
try { | |||
// expected to throw | |||
await getDimensions(notAString as string) | |||
} catch (err) { | |||
expect(err instanceof TypeError).toBe(true) | |||
} | |||
} | |||
) | |||
) | |||
}) | |||
it('should throw an error given an empty string argument', async () => { | |||
try { | |||
await getDimensions('') | |||
} catch (err) { | |||
expect(err instanceof RangeError).toBe(true) | |||
} | |||
}) | |||
describe('on string arguments', () => { | |||
beforeAll(() => { | |||
const imageSizeMock = imageSize as jest.Mock | |||
imageSizeMock.mockImplementation((_: string, cb: Function) => { | |||
cb(null, { images: [ { width: 0, height: 0, } ] }) | |||
}) | |||
}) | |||
it('should return an array of objects containing `width` properties', async () => { | |||
await fc.assert( | |||
fc.asyncProperty( | |||
fc.string(1, 20).filter(s => s.trim().length > 0), | |||
async aString => { | |||
const dimensionsArray: Dimensions[] = await getDimensions(aString) | |||
expect( | |||
dimensionsArray.every(d => 'width' in d) | |||
).toBe(true) | |||
} | |||
) | |||
) | |||
}) | |||
it('should return an array of objects containing `height` properties', async () => { | |||
await fc.assert( | |||
fc.asyncProperty( | |||
fc.string(1, 20).filter(s => s.trim().length > 0), | |||
async aString => { | |||
const dimensionsArray: Dimensions[] = await getDimensions(aString) | |||
expect( | |||
dimensionsArray.every(d => 'height' in d) | |||
).toBe(true) | |||
} | |||
) | |||
) | |||
}) | |||
}) | |||
describe('on error handling', () => { | |||
beforeAll(() => { | |||
const imageSizeMock = imageSize as jest.Mock | |||
imageSizeMock.mockImplementation((_: string, cb: Function) => { | |||
cb(new Error('vro.')) | |||
}) | |||
}) | |||
it('should throw caught error', async () => { | |||
await fc.assert( | |||
fc.asyncProperty( | |||
fc.string(1, 20).filter(s => s.trim().length > 0), | |||
async aString => { | |||
try { | |||
await getDimensions(aString) | |||
} catch (err) { | |||
expect(err).toBeTruthy() | |||
} | |||
} | |||
) | |||
) | |||
}) | |||
}) |
@@ -0,0 +1,31 @@ | |||
import { imageSize, } from 'image-size' | |||
export type Dimensions = { | |||
width: number | |||
height: number | |||
} | |||
type GetDimensions = (src: string) => Promise<Dimensions[]> | |||
const getDimensions: GetDimensions = async src => { | |||
if (typeof src as unknown !== 'string') { | |||
throw TypeError('Argument must be a string.') | |||
} | |||
if (src.trim().length < 1) { | |||
throw RangeError('Invalid value for argument.') | |||
} | |||
return new Promise((resolve, reject) => { | |||
imageSize(src, (err, dimensions) => { | |||
if (err) { | |||
reject(err) | |||
return | |||
} | |||
resolve(dimensions!.images as Dimensions[]) | |||
}) | |||
}) | |||
} | |||
export default getDimensions |
@@ -0,0 +1,132 @@ | |||
import * as fc from 'fast-check' | |||
import getProminentColors, { GetProminentColorsConfig } from './getProminentColors' | |||
jest.mock('get-pixels', () => () => [[255, 255, 255]]) | |||
jest.mock('get-rgba-palette', () => ({ | |||
bins: () => [ | |||
{ | |||
color: [255, 255, 255], | |||
size: 42069, | |||
amount: 0.42069, | |||
} | |||
] | |||
})) | |||
it('should exist', () => { | |||
expect(getProminentColors).toBeDefined() | |||
}) | |||
it('should be a callable', () => { | |||
expect(typeof getProminentColors).toBe('function') | |||
}) | |||
it('should accept 1 argument', () => { | |||
expect(getProminentColors).toHaveLength(1) | |||
}) | |||
it('should throw an error for invalid config', () => { | |||
fc.assert( | |||
fc.property( | |||
fc.anything().filter(s => !(typeof s === 'object' && s !== null && 'count' in s)), | |||
notAnObject => { | |||
expect(() => getProminentColors(notAnObject as GetProminentColorsConfig)).toThrow(TypeError) | |||
} | |||
) | |||
) | |||
}) | |||
it('should throw an error for non-number arguments as count', () => { | |||
fc.assert( | |||
fc.property( | |||
fc.anything().filter(s => typeof s !== 'number'), | |||
notANumber => { | |||
expect(() => getProminentColors({ count: notANumber as number })).toThrow(TypeError) | |||
} | |||
) | |||
) | |||
}) | |||
it('should throw an error for NaN as count', () => { | |||
expect(() => getProminentColors({ count: NaN })).toThrow(RangeError) | |||
}) | |||
it('should throw an error for 0 as count', () => { | |||
expect(() => getProminentColors({ count: 0 })).toThrow(RangeError) | |||
}) | |||
it('should throw an error for negative values as count', () => { | |||
fc.assert( | |||
fc.property( | |||
fc.nat().filter(n => n !== 0), | |||
anAbsoluteNumber => { | |||
expect(() => getProminentColors({ count: -anAbsoluteNumber })).toThrow(RangeError) | |||
} | |||
) | |||
) | |||
}) | |||
describe('on valid `count` values', () => { | |||
it('should throw an error for non-string sources', () => { | |||
fc.assert( | |||
fc.property( | |||
fc.anything().filter(v => typeof v !== 'string'), | |||
notAString => { | |||
expect(() => getProminentColors({ count: 1 })(notAString as string)).toThrow(TypeError) | |||
} | |||
) | |||
) | |||
}) | |||
it('should throw an error for blank strings as sources', () => { | |||
fc.assert( | |||
fc.property( | |||
fc.nat(20), | |||
blankStringLength => { | |||
let blankString = '' | |||
for (let i = 0; i < blankStringLength; i += 1) { | |||
blankString += ' ' | |||
} | |||
expect(() => getProminentColors({ count: 1 })(blankString)).toThrow(RangeError) | |||
} | |||
) | |||
) | |||
}) | |||
it('should return an array of objects with color attributes for valid sources', () => { | |||
fc.assert( | |||
fc.property( | |||
fc.string(1, 20).filter(s => s.trim().length > 0), | |||
aString => { | |||
const result = getProminentColors({ count: 1, })(aString) | |||
expect(result.every(s => 'color' in s)).toBe(true) | |||
} | |||
) | |||
) | |||
}) | |||
it('should return an array of objects with size attributes for valid sources', () => { | |||
fc.assert( | |||
fc.property( | |||
fc.string(1, 20).filter(s => s.trim().length > 0), | |||
aString => { | |||
const result = getProminentColors({ count: 1, })(aString) | |||
expect(result.every(s => 'size' in s)).toBe(true) | |||
} | |||
) | |||
) | |||
}) | |||
it('should return an array of objects with amount attributes for valid sources', () => { | |||
fc.assert( | |||
fc.property( | |||
fc.string(1, 20).filter(s => s.trim().length > 0), | |||
aString => { | |||
const result = getProminentColors({ count: 1, })(aString) | |||
expect(result.every(s => 'amount' in s)).toBe(true) | |||
} | |||
) | |||
) | |||
}) | |||
}) |
@@ -0,0 +1,71 @@ | |||
import getPixels from 'get-pixels' | |||
import getRgbaPalette from 'get-rgba-palette' | |||
type PaletteColor = [number, number, number] | |||
type Color = { | |||
red: number | |||
green: number | |||
blue: number | |||
} | |||
interface PaletteBin<ColorFormat> { | |||
color: ColorFormat | |||
size: number, | |||
amount: number | |||
} | |||
export type GetProminentColorsConfig = { | |||
count: number | |||
} | |||
type GetProminentColors = (params: GetProminentColorsConfig) => (src: string) => PaletteBin<Color>[] | |||
const getProminentColors: GetProminentColors = params => { | |||
if (typeof params as unknown !== 'object') { | |||
throw TypeError('Config must be an object.') | |||
} | |||
if ( | |||
params === null | |||
|| typeof params.count as unknown !== 'number' | |||
) { | |||
throw TypeError('Count must be a number.') | |||
} | |||
if (isNaN(params.count)) { | |||
throw RangeError('Config cannot be NaN.') | |||
} | |||
if (params.count <= 0) { | |||
throw RangeError('Only positive values allowed for count.') | |||
} | |||
return src => { | |||
if (typeof src as unknown !== 'string') { | |||
throw TypeError('Source must be a string.') | |||
} | |||
if (src.trim().length < 1) { | |||
throw RangeError('Source must be non-empty.') | |||
} | |||
const pixels: PaletteColor[] = getPixels(src) | |||
const colors: PaletteBin<PaletteColor>[] = getRgbaPalette.bins(pixels, params.count) | |||
return colors.map(bin => { | |||
const { color, ...etcBin } = bin | |||
const [red, green, blue] = color | |||
return { | |||
color: { | |||
red, | |||
green, | |||
blue | |||
}, | |||
...etcBin | |||
} | |||
}) | |||
} | |||
} | |||
export default getProminentColors |
@@ -0,0 +1,2 @@ | |||
declare module 'get-rgba-palette' | |||
declare module 'get-pixels' |
@@ -0,0 +1,19 @@ | |||
import * as index from './index' | |||
describe('formatPixelCount', () => { | |||
it('should exist', () => { | |||
expect(index.formatPixelCount).toBeDefined() | |||
}) | |||
}) | |||
describe('getDimensions', () => { | |||
it('should exist', () => { | |||
expect(index.getDimensions).toBeDefined() | |||
}) | |||
}) | |||
describe('getProminentColors', () => { | |||
it('should exist', () => { | |||
expect(index.getProminentColors).toBeDefined() | |||
}) | |||
}) |
@@ -0,0 +1,9 @@ | |||
import formatPixelCount from './formatPixelCount' | |||
import getDimensions from './getDimensions' | |||
import getProminentColors from './getProminentColors' | |||
export { | |||
formatPixelCount, | |||
getDimensions, | |||
getProminentColors, | |||
} |
@@ -0,0 +1,19 @@ | |||
{ | |||
"include": ["src", "types"], | |||
"compilerOptions": { | |||
"module": "esnext", | |||
"lib": ["dom", "esnext"], | |||
"importHelpers": true, | |||
"declaration": true, | |||
"sourceMap": true, | |||
"rootDir": "./src", | |||
"strict": true, | |||
"noUnusedLocals": true, | |||
"noUnusedParameters": true, | |||
"noImplicitReturns": true, | |||
"noFallthroughCasesInSwitch": true, | |||
"moduleResolution": "node", | |||
"jsx": "react", | |||
"esModuleInterop": true | |||
} | |||
} |