@heliosgraphics/utils
Advanced tools
Comparing version 4.0.0-alpha-5 to 4.1.0
@@ -1,10 +0,11 @@ | ||
export const copyValue = (text: string) => { | ||
const inp = document.createElement('input'); | ||
// copies the given value to the clipboard. | ||
export const copyValue = (text: string): void => { | ||
const input: HTMLInputElement = document.createElement("input") | ||
document.body.appendChild(inp); | ||
inp.value = text; | ||
inp.select(); | ||
document.execCommand('copy', false); | ||
document.body.appendChild(input) | ||
input.value = text | ||
input.select() | ||
document.execCommand("copy", false) | ||
return inp.remove(); | ||
}; | ||
return input.remove() | ||
} |
@@ -1,11 +0,26 @@ | ||
import { rgbToHex } from './colors' | ||
import { it, describe, expect } from 'vitest'; | ||
import { it, describe, expect } from "vitest" | ||
import { rgbToHex, hexToRgb, DEFAULT_PROFILE_RGB } from "./colors" | ||
describe('colors', () => { | ||
describe('rgbToHex', () => { | ||
it('converts rgb to hex', () => expect(rgbToHex(12, 44, 120)).toEqual('#0c2c78')); | ||
it('converts strings to hex', () => expect(rgbToHex('12' as any as number, '44' as any as number, '120' as any as number)).toEqual('#ffffff')); | ||
it('converts even with a null input to hex', () => expect(rgbToHex(null as any, 44, 120)).toEqual('#ff2c78')); | ||
it('converts undefined to hex', () => expect(rgbToHex(12, undefined as any, 120)).toEqual('#0cff78')); | ||
}); | ||
}); | ||
describe("colors", () => { | ||
describe("hexToRgb", () => { | ||
it("converts hex to rgb", () => | ||
expect(hexToRgb("#0c2c78")).toEqual([12, 44, 120])) | ||
it("returns default for 0", () => | ||
expect(hexToRgb(<any>0)).toEqual(DEFAULT_PROFILE_RGB)) | ||
it("returns default for undefined", () => | ||
expect(hexToRgb(<any>undefined)).toEqual(DEFAULT_PROFILE_RGB)) | ||
}) | ||
describe("rgbToHex", () => { | ||
it("converts rgb to hex", () => | ||
expect(rgbToHex(12, 44, 120)).toEqual("#0c2c78")) | ||
it("converts string to hex", () => | ||
expect(rgbToHex(<any>"12", <any>"44", <any>"120")).toEqual( | ||
"#0c2c78", | ||
)) | ||
it("converts null to hex", () => | ||
expect(rgbToHex(<any>null, 44, 120)).toEqual("#002c78")) | ||
it("returns undefined to hex", () => | ||
expect(rgbToHex(12, <any>undefined, 120)).toEqual("#0cff78")) | ||
}) | ||
}) |
@@ -1,39 +0,39 @@ | ||
import type { TypeRGB } from '@heliosgraphics/library/types/colors' | ||
import type { TypeRGB } from "@heliosgraphics/library/types/colors" | ||
const DEFAULT_PROFILE_RGB: TypeRGB = [199, 201, 209]; | ||
export const DEFAULT_PROFILE_RGB: TypeRGB = [199, 201, 209] as const | ||
// converts a hex value to a TypeRGB. | ||
export const hexToRgb = (hex?: string | null): TypeRGB => { | ||
const isString: boolean = !!hex && typeof hex === 'string'; | ||
const isValid: boolean = !!hex && typeof hex === "string" | ||
if (!isString) return DEFAULT_PROFILE_RGB | ||
if (!isValid) return DEFAULT_PROFILE_RGB | ||
hex = hex!.replace(/^#/, ''); | ||
hex = hex!.replace(/^#/, "") | ||
const bigint = parseInt(hex, 16); | ||
const r: number = (bigint >> 16) & 255; | ||
const g: number = (bigint >> 8) & 255; | ||
const b: number = bigint & 255; | ||
const bigint = parseInt(hex, 16) | ||
const r: number = (bigint >> 16) & 255 | ||
const g: number = (bigint >> 8) & 255 | ||
const b: number = bigint & 255 | ||
return [r, g, b]; | ||
return [r, g, b] | ||
} | ||
export const rgbToHex = (r: number = 255, g: number = 255, b: number = 255): string => { | ||
const _toHex = (c: number): string => { | ||
const hex = c.toString(16); | ||
return hex.length === 1 ? '0' + hex : hex; | ||
}; | ||
// converts an rgb value to a hex string (#0cd0cd). | ||
export const rgbToHex = ( | ||
r: number | string = 255, | ||
g: number | string = 255, | ||
b: number | string = 255, | ||
): string => { | ||
const _toHex = (c: unknown): string => { | ||
const value = Number(c) | ||
const isValid: boolean = isNaN(value) || value < 0 || value > 255 | ||
const isRNumber: boolean = typeof r === 'number'; | ||
const isGNumber: boolean = typeof g === 'number'; | ||
const isBNumber: boolean = typeof b === 'number'; | ||
if (isValid) return "FF" | ||
const RR: number = isRNumber ? r : 255; | ||
const GG: number = isGNumber ? g : 255; | ||
const BB: number = isBNumber ? b : 255; | ||
const hex = value.toString(16) | ||
const hexR: string = _toHex(RR); | ||
const hexG: string = _toHex(GG); | ||
const hexB: string = _toHex(BB); | ||
return hex.length === 1 ? `0${hex}` : hex | ||
} | ||
return `#${hexR}${hexG}${hexB}`; | ||
} | ||
return `#${_toHex(r)}${_toHex(g)}${_toHex(b)}` | ||
} |
@@ -1,11 +0,17 @@ | ||
export const debounce = (callback: Function, wait: number) => { | ||
let timeoutId: any; | ||
export type CallbackFunction = (...args: Array<unknown>) => void | ||
return (...args: any) => { | ||
globalThis.clearTimeout(timeoutId); | ||
// debounces the function with wait time passed. | ||
export const debounce = ( | ||
callback: CallbackFunction, | ||
wait: number, | ||
): CallbackFunction => { | ||
let timeoutId: any | ||
timeoutId = globalThis.setTimeout(() => { | ||
callback.apply(null, args); | ||
}, wait); | ||
}; | ||
return (...args: Array<unknown>) => { | ||
globalThis.clearTimeout(timeoutId) | ||
timeoutId = globalThis.setTimeout(() => { | ||
callback.apply(null, args) | ||
}, wait) | ||
} | ||
} |
The MIT License (MIT) | ||
Copyright (c) 2015 Chris Puska | ||
Copyright (c) 2015 Helios Graphics | ||
@@ -5,0 +5,0 @@ Permission is hereby granted, free of charge, to any person obtaining a copy |
{ | ||
"name": "@heliosgraphics/utils", | ||
"version": "4.0.0-alpha-5", | ||
"version": "4.1.0", | ||
"private": false, | ||
"type": "module", | ||
"author": "Chris Puska <03b8@helios.graphics>", | ||
"author": "03b8 <03b8@helios.graphics>", | ||
"description": "Helios Utils", | ||
"dependencies": { | ||
@@ -13,4 +14,4 @@ "@heliosgraphics/library": "latest", | ||
"devDependencies": { | ||
"@types/uuid": "9.0.7" | ||
"@types/uuid": "latest" | ||
} | ||
} |
@@ -1,15 +0,23 @@ | ||
import { getSlug } from "./slug"; | ||
import { it, describe, expect } from 'vitest'; | ||
import { it, describe, expect } from "vitest" | ||
import { getSlug } from "./slug" | ||
describe('slug', () => { | ||
describe('getSlug', () => { | ||
// Good | ||
it('Gets a lowercase subdomain from case 1', () => expect(getSlug('--BuRn--')).toEqual('-burn-')); | ||
it('Gets a lowercase subdomain from case 2', () => expect(getSlug('#$%^B#uR#n-')).toEqual('burn-')); | ||
it('Gets a nice slug for a category', () => expect(getSlug('Gaussian Blur')).toEqual('gaussian-blur')); | ||
// Empty | ||
it('Gets empty string if subdomain is undefined', () => expect(getSlug(undefined)).toEqual('')); | ||
}); | ||
}); | ||
describe("slug", () => { | ||
describe("getSlug", () => { | ||
it("returns valid from string with dashes", () => | ||
expect(getSlug("--B—uRn--")).toEqual("-burn-")) | ||
it("returns valid from special string", () => | ||
expect(getSlug("#$%^B#uR#n-")).toEqual("burn-")) | ||
it("returns valid from parens string", () => | ||
expect(getSlug("Gaussian Blur [1](2){3}")).toEqual( | ||
"gaussian-blur-123", | ||
)) | ||
it("replaces àáäâèéëêìíïîòóöôùúüûñç", () => | ||
expect(getSlug("àáäâèéëêìíïîòóöôùúüûñç")).toEqual( | ||
"aaaaeeeeiiiioooouuuunc", | ||
)) | ||
it("fails silently from undefined", () => | ||
expect(getSlug(undefined)).toEqual("")) | ||
it("fails silently from null", () => | ||
expect(getSlug(<any>null)).toEqual("")) | ||
}) | ||
}) |
38
slug.ts
@@ -1,20 +0,24 @@ | ||
export const getSlug = (subdomain: string | undefined): string => { | ||
if (!subdomain) return '' | ||
// gets a valid slug from the given string. | ||
export const getSlug = (text?: string): string => { | ||
const isValid: boolean = Boolean(text && typeof text == "string") | ||
let str = subdomain; | ||
if (!isValid) return "" | ||
str = str.replace(/^\s+|\s+$/g, '') | ||
str = str.toLowerCase() | ||
var from = "àáäâèéëêìíïîòóöôùúüûñç·/_,:;" | ||
var to = "aaaaeeeeiiiioooouuuunc------" | ||
for (var i = 0, l = from.length; i < l; i++) { | ||
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i)) | ||
} | ||
str = str.replace(/[^a-z0-9 -]/g, '') | ||
.replace(/\s+/g, '-') | ||
.replace(/-+/g, '-') | ||
return str | ||
return ( | ||
text! | ||
// normalize characters to their base characters and diacritics | ||
.normalize("NFD") | ||
// remove diacritic marks | ||
.replace(/[\u0300-\u036f]/g, "") | ||
// remove leading and trailing whitespace | ||
.trim() | ||
// convert to lowercase | ||
.toLowerCase() | ||
// remove invalid characters | ||
.replace(/[^a-z0-9 -]/g, "") | ||
// replace spaces with hyphens | ||
.replace(/\s+/g, "-") | ||
// replace multiple hyphens with a single hyphen | ||
.replace(/-+/g, "-") | ||
) | ||
} |
@@ -1,52 +0,77 @@ | ||
import xss from 'xss'; | ||
import xss from "xss" | ||
export const sanitizeText = (input: string = ''): string => { | ||
const clean: string = xss(input); | ||
// sanitizes a given input. you're trusting the `xss` package here. | ||
export const sanitizeText = (input: string = ""): string => { | ||
const clean: string = xss(input) | ||
return clean; | ||
return clean | ||
} | ||
export const middleEllipsis = (str: string = '', length: number = 19): string => { | ||
const diff: number = Math.floor((length - 3) / 2); | ||
// removes markdown formatting. | ||
export const removeMarkdown = (markdownText: string): string => { | ||
const patternsToRemove: Array<{ pattern: RegExp; replacement: string }> = [ | ||
// ![alt text](url) | ||
{ pattern: /!\[.*?\]\(.*?\)/g, replacement: "" }, | ||
// [text](url) | ||
{ pattern: /\[(.*?)\]\(.*?\)/g, replacement: "$1" }, | ||
// #, ##, etc. | ||
{ pattern: /#{1,6}\s/g, replacement: "" }, | ||
// **bold** | ||
{ pattern: /\*\*(.*?)\*\*/g, replacement: "$1" }, | ||
// __bold__ | ||
{ pattern: /__(.*?)__/g, replacement: "$1" }, | ||
// *emphasized* | ||
{ pattern: /\*(.*?)\*/g, replacement: "$1" }, | ||
// _emphasized_ | ||
{ pattern: /_(.*?)_/g, replacement: "$1" }, | ||
// ~~strikethrough~~ | ||
{ pattern: /~~(.*?)~~/g, replacement: "$1" }, | ||
// > | ||
{ pattern: />/g, replacement: "" }, | ||
// --- | ||
{ pattern: /-{3,}/g, replacement: "" }, | ||
// ``` | ||
{ pattern: /`{3}.*?`{3}/gs, replacement: "" }, | ||
// `code` | ||
{ pattern: /`{1,2}(.*?)`{1,2}/g, replacement: "$1" }, | ||
] | ||
if (str.length > length) { | ||
return str.substring(0, diff) + '...' + str.substring(str.length - diff, str.length); | ||
} | ||
let cleanText = markdownText | ||
return str; | ||
for (const { pattern, replacement } of patternsToRemove) { | ||
cleanText = cleanText.replace(pattern, replacement) | ||
} | ||
return cleanText | ||
} | ||
// TODO @chrispuska Make this account for possible markdown parts in `text` add tests. | ||
export const removeNewlines = (text?: string | null, limit?: number): string => { | ||
if (!text) return ''; | ||
// adds a middle ellipsis, eg.: (ellipsis, 6) gets "ell...sis". | ||
export const middleEllipsis = ( | ||
text: string = "", | ||
length: number = 64, | ||
): string => { | ||
const diff: number = Math.floor((length - 3) / 2) | ||
const isValid: boolean = Boolean( | ||
!!text && typeof text === "string" && text.length > length, | ||
) | ||
const length: number = text?.length; | ||
const trimmedString = text.substring(0, limit ?? length); | ||
if (!isValid) return "" | ||
return sanitizeText(trimmedString.replace(/(?:\r\n|\r|\n)/g, ' ')); | ||
return ( | ||
text.substring(0, diff) + | ||
"..." + | ||
text.substring(text.length - diff, text.length) | ||
) | ||
} | ||
// TODO @chrispuska This should only touch semantics, not ui | ||
export const formatRawText = (inputText: string = '', pattern?: RegExp, match?: string): string => { | ||
// Links | ||
const URL_PATTERN = /\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim; | ||
const URL_PSEUDO_PATTERN = /(^|[^\/])(www\.[\S]+(\b|$))/gim; | ||
const URL_EMAIL_PATTERN = /(([a-zA-Z0-9_\-\.]+)@[a-zA-Z_]+?(?:\.[a-zA-Z]{2,6}))+/gim; | ||
export const removeNewlines = ( | ||
text?: string | null, | ||
limit?: number, | ||
): string => { | ||
if (!text) return "" | ||
// New lines | ||
const NEW_LINES_PATTERN = /(?:\r\n|\r|\n)/gim; | ||
const length: number = text?.length | ||
const trimmedString: string = text.substring(0, limit ?? length ?? 0) | ||
let formattedText: string = inputText | ||
// Links | ||
.replace(URL_PATTERN, '<a class="underline medium" href="$&">$&</a>') | ||
.replace(URL_PSEUDO_PATTERN, '$1<a class="underline medium" href="http://$2">$2</a>') | ||
.replace(URL_EMAIL_PATTERN, '<a class="underline medium" href="mailto:$1">$1</a>') | ||
// New lines | ||
.replace(NEW_LINES_PATTERN, '<br/>') | ||
// Replace a custom one | ||
// TODO @chrispuska Make this multi pattern instead. | ||
if (!pattern) formattedText.replace(pattern!, `<a class=" medium" href="${match}">$&</a>`) | ||
return sanitizeText(formattedText); | ||
return sanitizeText(trimmedString.replace(/(?:\r\n|\r|\n)/g, " ")) | ||
} |
@@ -1,11 +0,17 @@ | ||
export const throttle = (fn, delay: number) => { | ||
let lastCall = 0; | ||
export type CallbackFunction = (...args: Array<unknown>) => void | ||
return (...args) => { | ||
const now = new Date().getTime(); | ||
if (now - lastCall < delay) return; | ||
// throttles the passed function with wait. | ||
export const throttle = (callbackFunction: CallbackFunction, wait: number) => { | ||
let lastCall = 0 | ||
lastCall = now; | ||
return fn(...args); | ||
}; | ||
}; | ||
return (...args: Array<unknown>) => { | ||
const now: number = new Date().getTime() | ||
const isInvalid: boolean = Boolean(now - lastCall < wait) | ||
if (isInvalid) return | ||
lastCall = now | ||
return callbackFunction(...args) | ||
} | ||
} |
@@ -1,11 +0,24 @@ | ||
import { getUUID, TEST_UUID } from './uuid' | ||
import { it, describe, expect } from 'vitest'; | ||
import { it, describe, expect } from "vitest" | ||
import { getUUID, isUUID, TEST_UUID } from "./uuid" | ||
describe('validations', () => { | ||
describe('getUUID', () => { | ||
const MOCK_UUID = getUUID() | ||
describe("validations", () => { | ||
describe("getUUID", () => { | ||
const MOCK_UUID = getUUID() | ||
it('returns something', () => expect(MOCK_UUID).toEqual(TEST_UUID)); | ||
it('return any string', () => expect(getUUID('any-string')).toEqual('any-string')); | ||
}); | ||
}); | ||
it("returns test uuid", () => | ||
expect(getUUID(MOCK_UUID)).toEqual(TEST_UUID)) | ||
it("returns any string", () => | ||
expect(getUUID("any-string")).toEqual("any-string")) | ||
}) | ||
describe("isUUID", () => { | ||
it("validates test uuid", () => expect(isUUID(TEST_UUID)).toEqual(true)) | ||
it("validates a random uuid", () => | ||
expect(isUUID("101bfe56-8c16-4f94-9b45-759ea5e67cea")).toEqual( | ||
true, | ||
)) | ||
it("catches empty string", () => expect(isUUID("")).toEqual(false)) | ||
it("catches undefined", () => | ||
expect(isUUID(<any>undefined)).toEqual(false)) | ||
}) | ||
}) |
30
uuid.ts
@@ -1,25 +0,23 @@ | ||
import { v4 as uuidv4 } from 'uuid'; | ||
import { v4 as uuidv4 } from "uuid" | ||
export const IS_TEST: boolean = process.env.NODE_ENV === 'test' as const; | ||
export const TEST_UUID: string = '00000000-0000-0000-000000000000' as const; | ||
export const IS_TEST: boolean = process.env.NODE_ENV === ("test" as const) | ||
export const TEST_UUID: string = "00000000-0000-0000-0000-000000000000" as const | ||
export const getUUID = (id?: string) => { | ||
// Do not generate if given | ||
if (!!id) return id; | ||
export const getUUID = (id?: unknown) => { | ||
if (!!id) return id | ||
// Do not generate in test env | ||
if (IS_TEST) return TEST_UUID | ||
// this is necessary for snapshot tests, but should be dynamic. | ||
if (IS_TEST) return TEST_UUID | ||
return uuidv4(); | ||
return uuidv4() | ||
} | ||
export const isUUID = (uuid): boolean => { | ||
let s: any = "" + uuid; | ||
s = s.match('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'); | ||
export const isUUID = (uuid?: unknown): boolean => { | ||
const uuidRegex = | ||
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/ | ||
const isValid = typeof uuid === "string" | ||
if (s === null) { | ||
return false; | ||
} | ||
if (!isValid) return false | ||
return true; | ||
return uuidRegex.test(uuid) | ||
} |
@@ -1,23 +0,44 @@ | ||
import { validateHttpUrl, validateEmail } from './validations' | ||
import { it, describe, expect } from 'vitest'; | ||
import { validateHttpUrl, validateEmail } from "./validations" | ||
import { it, describe, expect } from "vitest" | ||
describe('validations', () => { | ||
describe('validateEmail', () => { | ||
it('Valid email', () => expect(validateEmail('x@x.com')).toEqual(true)); | ||
it('Valid email +', () => expect(validateEmail('x+x@x.com')).toEqual(true)); | ||
it('Valid email weird', () => expect(validateEmail('0@space.city')).toEqual(true)); | ||
it('Invalid email', () => expect(validateEmail('@space.city')).toEqual(false)); | ||
it('Invalid email', () => expect(validateEmail('')).toEqual(false)); | ||
it('Invalid email', () => expect(validateEmail(undefined as any as string)).toEqual(false)); | ||
}); | ||
describe("validations", () => { | ||
describe("validateEmail", () => { | ||
it("validates email", () => | ||
expect(validateEmail("x@x.com")).toEqual(true)) | ||
it("validates email with +", () => | ||
expect(validateEmail("x+x@x.com")).toEqual(true)) | ||
it("validates email with long tld", () => | ||
expect(validateEmail("0@helios.graphics")).toEqual(true)) | ||
it("catches long invalid string", () => | ||
expect(validateEmail("@space.city")).toEqual(false)) | ||
it("catches empty string", () => | ||
expect(validateEmail("")).toEqual(false)) | ||
it("catches undefined", () => | ||
expect(validateEmail(<any>undefined)).toEqual(false)) | ||
it("catches multi @", () => | ||
expect(validateEmail("x@@x.com")).toEqual(false)) | ||
it("catches a weird one", () => | ||
expect(validateEmail("x@@@x.com@/@x.com")).toEqual(false)) | ||
}) | ||
describe('validateHttpUrl', () => { | ||
it('Valid http url', () => expect(validateHttpUrl('https://x.com')).toEqual(true)); | ||
it('Valid http url long', () => expect(validateHttpUrl('http://x.city')).toEqual(true)); | ||
it('Valid subdomain', () => expect(validateHttpUrl('https://0.x.x.com')).toEqual(true)); | ||
it('Invalid ftp', () => expect(validateHttpUrl('ftp://x.com')).toEqual(false)); | ||
it('Invalid string', () => expect(validateHttpUrl('lorem ipsum https://x.com')).toEqual(false)); | ||
it('Invalid empty string', () => expect(validateHttpUrl('')).toEqual(false)); | ||
it('Invalid null', () => expect(validateHttpUrl(null as any as string)).toEqual(false)); | ||
}); | ||
}); | ||
describe("validateHttpUrl", () => { | ||
it("validates url", () => | ||
expect(validateHttpUrl("https://x.com")).toEqual(true)) | ||
it("validates url with long tld", () => | ||
expect(validateHttpUrl("https://lorem-ipsum.graphics")).toEqual( | ||
true, | ||
)) | ||
it("validates url with double subdomain", () => | ||
expect(validateHttpUrl("https://0.x.x.com")).toEqual(true)) | ||
it("catches ftp", () => | ||
expect(validateHttpUrl("ftp://x.com")).toEqual(false)) | ||
it("catches string containing url", () => | ||
expect(validateHttpUrl("lorem ipsum https://x.com")).toEqual(false)) | ||
it("catches unsafe http", () => | ||
expect(validateHttpUrl("http://x.com")).toEqual(false)) | ||
it("catches empty string", () => | ||
expect(validateHttpUrl("")).toEqual(false)) | ||
it("catches undefined", () => | ||
expect(validateHttpUrl(<any>undefined)).toEqual(false)) | ||
}) | ||
}) |
@@ -0,17 +1,22 @@ | ||
// validates a URL. | ||
export const validateHttpUrl = (text?: string | null): boolean => { | ||
let url; | ||
let url: URL | ||
try { | ||
url = new URL(text as string); | ||
} catch (_) { | ||
return false; | ||
} | ||
try { | ||
url = new URL(text as string) | ||
} catch (_) { | ||
return false | ||
} | ||
return Boolean(url.protocol === "http:" || url.protocol === "https:"); | ||
// "http" is unsafe. | ||
const isValid: boolean = Boolean(url.protocol === "https:") | ||
return isValid | ||
} | ||
export const validateEmail = (email: string): boolean => { | ||
const re: RegExp = /\S+@\S+\.\S+/; | ||
// validates an email. | ||
export const validateEmail = (email: string = ""): boolean => { | ||
const re: RegExp = /^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ | ||
return re.test(email); | ||
}; | ||
return re.test(email) | ||
} |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
15504
18
410
1
1