Browse Source

Initial commit

Add files.
master
TheoryOfNekomata 3 years ago
commit
14212b5b97
17 changed files with 6901 additions and 0 deletions
  1. +11
    -0
      .editorconfig
  2. +6
    -0
      .gitignore
  3. +6
    -0
      .prettierrc
  4. +21
    -0
      LICENSE
  5. +99
    -0
      README.md
  6. +56
    -0
      package.json
  7. +66
    -0
      src/formatPixelCount.test.ts
  8. +28
    -0
      src/formatPixelCount.ts
  9. +102
    -0
      src/getDimensions.test.ts
  10. +31
    -0
      src/getDimensions.ts
  11. +132
    -0
      src/getProminentColors.test.ts
  12. +71
    -0
      src/getProminentColors.ts
  13. +2
    -0
      src/global.d.ts
  14. +19
    -0
      src/index.test.ts
  15. +9
    -0
      src/index.ts
  16. +19
    -0
      tsconfig.json
  17. +6223
    -0
      yarn.lock

+ 11
- 0
.editorconfig View File

@@ -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

+ 6
- 0
.gitignore View File

@@ -0,0 +1,6 @@
*.log
.DS_Store
node_modules
dist
coverage/
.idea/

+ 6
- 0
.prettierrc View File

@@ -0,0 +1,6 @@
{
"jsxSingleQuote": false,
"singleQuote": true,
"semi": false,
"trailingComma": "es5"
}

+ 21
- 0
LICENSE View File

@@ -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.

+ 99
- 0
README.md View File

@@ -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).

+ 56
- 0
package.json View File

@@ -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"
}
}

+ 66
- 0
src/formatPixelCount.test.ts View File

@@ -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$/)
}
)
)
})
})

+ 28
- 0
src/formatPixelCount.ts View File

@@ -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

+ 102
- 0
src/getDimensions.test.ts View File

@@ -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()
}
}
)
)
})
})

+ 31
- 0
src/getDimensions.ts View File

@@ -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

+ 132
- 0
src/getProminentColors.test.ts View File

@@ -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)
}
)
)
})
})

+ 71
- 0
src/getProminentColors.ts View File

@@ -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

+ 2
- 0
src/global.d.ts View File

@@ -0,0 +1,2 @@
declare module 'get-rgba-palette'
declare module 'get-pixels'

+ 19
- 0
src/index.test.ts View File

@@ -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()
})
})

+ 9
- 0
src/index.ts View File

@@ -0,0 +1,9 @@
import formatPixelCount from './formatPixelCount'
import getDimensions from './getDimensions'
import getProminentColors from './getProminentColors'

export {
formatPixelCount,
getDimensions,
getProminentColors,
}

+ 19
- 0
tsconfig.json View File

@@ -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
}
}

+ 6223
- 0
yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save