Skip to content

Commit

Permalink
Merge pull request #7 from TurtIeSocks/constructor-options
Browse files Browse the repository at this point in the history
feat: add constructor options obj
  • Loading branch information
TurtIeSocks authored Aug 30, 2024
2 parents 3ddccf5 + 243d57a commit 3f7d1ea
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 95 deletions.
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# uicons.js

uicons.js is a JavaScript class based on the [UICONS](https://github.com/UIcons/UIcons) specification. It can be used with any file extensions, works in the browser and on the server.

View the [example](./example/) code to see implementation details for both audio and image files. See [tests](./src/uicons.test.ts) for additional examples.
uicons.js is a JavaScript class based on the [UICONS](https://github.com/UIcons/UIcons) specification. It provides a simple way to utilize the UICONS standard in your projects and reduces the need for tedious boilerplate code.

### [Demo Page](https://turtiesocks.github.io/uicons.js/)

Expand All @@ -16,8 +14,18 @@ yarn add uicons.js
pnpm add uicons.js
```

## Features

- File extension agnostic
- Provides helpful IntelliSense in your IDE based on latest protos
- Works in the browser and server
- Supports both remote and local initialization of the index.json file

## Usage

- View the [example](./example/) code to see implementation details for both audio and image files
- See [tests](./src/uicons.test.ts) for additional examples.

```typescript
import { UICONS } from 'uicons.js'

Expand Down Expand Up @@ -76,4 +84,7 @@ pnpm run install

# Build and Run Example
pnpm run start

# Run Tests
pnpm run test
```
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
export * from './types.js'
export type {
TimeOfDay,
TrainerCounts,
RewardTypeKeys,
UiconsIndex,
LureIDs,
} from './types.js'
export { UICONS } from './uicons.js'
43 changes: 28 additions & 15 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ export type TimeOfDay = 'day' | 'night'

export type TrainerCounts = StringOrNumber<0 | 1 | 2 | 3 | 4 | 5 | 6>

export type LureIDs = StringOrNumber<
| {
[K in KeysEndingWith<
typeof Rpc.Item,
'ITEM_TROY_DISK'
>]: (typeof Rpc.Item)[K]
}[KeysEndingWith<typeof Rpc.Item, 'ITEM_TROY_DISK'>]
| 0
>

export interface UiconsIndex<T extends string[] = string[]> {
device?: T
gym?: T
Expand All @@ -24,7 +34,22 @@ export interface UiconsIndex<T extends string[] = string[]> {
weather?: T
}

export type ExtensionMap<T = UiconsIndex> = {
export interface Options<T extends UiconsIndex> {
/**
* The path to the directory containing the icons.
*/
path: string
/**
* The index.json data from the uicons repository.
*/
data?: T
/**
* The label to use when logging messages.
*/
label?: string
}

export type ExtensionMap<T extends UiconsIndex = UiconsIndex> = {
[K in keyof T]: T[K] extends string[] | readonly string[]
? string
: T[K] extends object
Expand All @@ -46,23 +71,11 @@ export type Paths<T> = T extends object
}[keyof T]
: ''

export type EnumVal<
T extends Record<number | string, number | string>,
filter extends string = ''
> = StringOrNumber<T[keyof T]>
export type EnumVal<T extends Record<number | string, number | string>> =
StringOrNumber<T[keyof T]>

export type StringOrNumber<T extends number | string> = `${T}` | T

type KeysEndingWith<T, S extends string> = {
[K in keyof T]: K extends `${S}${infer _Suffix}` ? K : never
}[keyof T]

export type LureIDs = StringOrNumber<
| {
[K in KeysEndingWith<
typeof Rpc.Item,
'ITEM_TROY_DISK'
>]: (typeof Rpc.Item)[K]
}[KeysEndingWith<typeof Rpc.Item, 'ITEM_TROY_DISK'>]
| 0
>
2 changes: 1 addition & 1 deletion src/uicons.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('webp format', () => {
})
})

const audio = new UICONS(BASE_AUDIO_URL)
const audio = new UICONS({ path: BASE_AUDIO_URL })

describe('wav format', () => {
test('should load locally', async () => {
Expand Down
153 changes: 78 additions & 75 deletions src/uicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import type {
ExtensionMap,
Paths,
EnumVal,
StringOrNumber,
TrainerCounts,
LureIDs,
Options,
} from './types.js'

/**
Expand All @@ -17,13 +17,24 @@ import type {
* Can be used with any image or audio extensions, as long as they follow the UICONS guidelines
* @see https://github.com/UIcons/UIcons
* @example
* // Without generic and with the optional label argument
* const uicons = new UICONS('https://example.com/uicons', 'cagemons')
* import { UICONS } from 'uicons.js'
*
* // Basic usage
* const uicons = new UICONS('https://example.com/uicons')
* // With all constructor options
* const uicons = new UICONS({
* // base URL of the UICONS repository
* path: 'https://example.com/uicons',
* // label for debug purposes
* label: 'cagemons',
* // your own index.json data if you want to load in the constructor
* data: { pokemon: [...], device: [...] },
* })
* // With stronger typing from index file
* const uicons = new UICONS<UiconsIndex>('https://example.com/uicons', 'cagemons')
* const uicons = new UICONS<UiconsIndex>({ path: 'https://example.com/uicons' })
* // Async initialization fetches the index.json file for you
* await uicons.remoteInit()
* // Sync initialization if you already have the index.json
* // Sync initialization if you already have the index.json and want to load it manually
* uicons.init(indexJson)
*/
export class UICONS<Index extends UiconsIndex = UiconsIndex> {
Expand All @@ -45,54 +56,53 @@ export class UICONS<Index extends UiconsIndex = UiconsIndex> {
#type: Set<Index['type'][number]>
#weather: Set<Index['weather'][number]>

/**
* @param options The options object for the UICONS instance
*/
constructor(options: Options<Index>)
/**
* @param path The base URL of the UICONS repository
*/
constructor(path: string)
/**
* @param path The base URL of the UICONS repository
* @param label Optional label for debugging purposes
* @param label The optional label for the UICONS instance
* @deprecated Use the new constructor with an options object
*/
constructor(path: string, label?: string) {
constructor(path: string, label?: string)
constructor(optionsOrPath: string | Options<Index>, oldLabel?: string) {
const { path, label, data }: Options<Index> =
typeof optionsOrPath === 'string'
? { path: optionsOrPath, label: oldLabel }
: optionsOrPath

this.#path = path.endsWith('/') ? path.slice(0, -1) : path
this.#label = label || this.#path

this.#device = new Set()
this.#gym = new Set()
this.#invasion = new Set()
this.#misc = new Set()
this.#nest = new Set()
this.#pokemon = new Set()
this.#pokestop = new Set()
this.#raid = { egg: new Set() }
this.#reward = {
unset: new Set(),
experience: new Set(),
item: new Set(),
stardust: new Set(),
candy: new Set(),
avatar_clothing: new Set(),
quest: new Set(),
pokemon_encounter: new Set(),
pokecoin: new Set(),
xl_candy: new Set(),
level_cap: new Set(),
sticker: new Set(),
mega_resource: new Set(),
incident: new Set(),
player_attribute: new Set(),
event_badge: new Set(),
this.#label = label ?? this.#path
if (oldLabel) {
this.#warn(
'The label parameter is deprecated, use the options object instead'
)
}
if (data) this.init(data)
}

#warn(...args: unknown[]) {
if (
typeof process !== 'undefined' &&
process.env.NODE_ENV === 'development'
) {
console.warn(`[UICONS ${this.#label}]`, ...args)
}
this.#spawnpoint = new Set()
this.#team = new Set()
this.#type = new Set()
this.#weather = new Set()
}

static #buildExtensions(json: UiconsIndex): ExtensionMap {
#buildExtensions(json: Index): ExtensionMap<Index> {
return Object.fromEntries(
Object.entries(json)
.map(([category, values]) => {
if (Array.isArray(values) && values.length > 0) {
return [category, values[0].split('.').pop()]
} else if (typeof values === 'object') {
return [category, UICONS.#buildExtensions(values)]
return [category, this.#buildExtensions(values)]
}
return [category, '']
})
Expand Down Expand Up @@ -131,21 +141,18 @@ export class UICONS<Index extends UiconsIndex = UiconsIndex> {
this.#nest = new Set(data.nest || [])
this.#pokemon = new Set(data.pokemon || [])
this.#pokestop = new Set(data.pokestop || [])
this.#raid.egg = new Set(data.raid?.egg || [])
Object.assign(
this.#reward,
Object.fromEntries(
Object.entries(data.reward || {})
.filter(([, v]) => Array.isArray(v))
.map(([k, v]) => [k, new Set(v)])
)
this.#raid = { egg: new Set(data.raid?.egg || []) }
this.#reward = Object.fromEntries(
Object.entries(data.reward || {})
.filter(([, v]) => Array.isArray(v))
.map(([k, v]) => [k, new Set(v)])
)
this.#spawnpoint = new Set(data.spawnpoint || [])
this.#team = new Set(data.team || [])
this.#type = new Set(data.type || [])
this.#weather = new Set(data.weather || [])

this.#extensionMap = UICONS.#buildExtensions(data)
this.#extensionMap = this.#buildExtensions(data)
return this
}

Expand Down Expand Up @@ -492,38 +499,34 @@ export class UICONS<Index extends UiconsIndex = UiconsIndex> {
amount?: string | number
): string
reward<U extends RewardTypeKeys>(
questRewardType?: U,
questRewardType: U = 'unset' as U,
rewardId = 0,
amount = 0
): string {
this.#isReady()
const safeRewardType = questRewardType || 'unset'
const baseUrl = `${this.#path}/reward/${safeRewardType}`

if (this.#reward[safeRewardType]) {
const amountSafe = typeof amount === 'number' ? amount : +amount
const amountSuffixes =
Number.isInteger(amountSafe) && amountSafe > 1
? [`_a${amount}`, '']
: ['']
const safeId = +rewardId || amountSafe || 0
for (let a = 0; a < amountSuffixes.length; a += 1) {
const result = `${safeId}${amountSuffixes[a]}.${
this.#extensionMap.reward[safeRewardType]
}`
if (this.#reward[safeRewardType].has(result)) {
return `${baseUrl}/${result}`
}
}
} else {
console.warn(
'UICONS',
this.#label.toUpperCase(),
`Missing category: ${safeRewardType}`
)
const baseUrl = `${this.#path}/reward/${questRewardType}`
const rewardSet = this.#reward[questRewardType]
if (!rewardSet) {
this.#warn('Invalid quest reward type,', questRewardType)
return this.misc(0)
}
return `${baseUrl}/0.${this.#extensionMap.reward[safeRewardType]}`

const amountSafe = typeof amount === 'number' ? amount : +amount
const amountSuffixes =
Number.isInteger(amountSafe) && amountSafe > 1
? [`_a${amount}`, '']
: ['']
const safeId = +rewardId || amountSafe || 0

for (let a = 0; a < amountSuffixes.length; a += 1) {
const result = `${safeId}${amountSuffixes[a]}.${
this.#extensionMap.reward[questRewardType]
}`
if (rewardSet.has(result)) {
return `${baseUrl}/${result}`
}
}
return `${baseUrl}/0.${this.#extensionMap.reward[questRewardType]}`
}

/**
Expand Down

0 comments on commit 3f7d1ea

Please sign in to comment.