├── static ├── icons │ ├── 32.png │ ├── 48.png │ ├── 64.png │ ├── 128.png │ └── 256.png ├── popup.html ├── options.html └── styles │ ├── popup.css │ ├── options.css │ └── shared.css ├── .gitattributes ├── .gitignore ├── .editorconfig ├── source ├── utilities │ ├── test_dom.ts │ ├── parse_version.ts │ ├── regex_utils.ts │ ├── __tests__ │ │ ├── parse_version.ts │ │ ├── regex_utils.test.ts │ │ ├── favicon_autoselector.test.ts │ │ └── favicon_selector.test.ts │ ├── permissions.ts │ ├── i18n.ts │ ├── favicon_selector.ts │ ├── image_helpers.ts │ ├── append_favicon_link.ts │ └── favicon_autoselector.ts ├── components │ ├── only.tsx │ ├── emoji_selector │ │ ├── types.ts │ │ ├── components │ │ │ ├── emoji_button.tsx │ │ │ ├── custom_delete.tsx │ │ │ ├── groups.tsx │ │ │ ├── custom_upload.tsx │ │ │ └── popup.tsx │ │ └── mod.tsx │ ├── switch.tsx │ ├── __tests__ │ │ └── only.test.tsx │ ├── header.tsx │ ├── checkbox.tsx │ ├── list.tsx │ └── list_input.tsx ├── models │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── emoji.test.ts.snap │ │ ├── storage_legacy.test.ts │ │ └── emoji.test.ts │ ├── favicon.ts │ ├── settings.ts │ ├── storage_legacy.ts │ ├── __fixtures__ │ │ └── settings_fixtures.ts │ └── emoji.ts ├── hooks │ ├── use_route.ts │ ├── use_status.ts │ ├── use_focus_observer.ts │ ├── use_active_tab.ts │ ├── use_list_state.ts │ ├── use_selected_favicon.ts │ └── use_browser_storage.ts ├── manifest.json ├── pages │ ├── favicons_page.tsx │ ├── about_page.tsx │ └── settings_page.tsx ├── options.tsx ├── background.ts ├── content_script.ts ├── popup.tsx └── config │ └── legacy_autoselect_set.ts ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── deno.yml ├── import_map.json ├── LICENSE ├── deno.json ├── README.md └── deno.lock /static/icons/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpevs/favioli/HEAD/static/icons/32.png -------------------------------------------------------------------------------- /static/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpevs/favioli/HEAD/static/icons/48.png -------------------------------------------------------------------------------- /static/icons/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpevs/favioli/HEAD/static/icons/64.png -------------------------------------------------------------------------------- /static/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpevs/favioli/HEAD/static/icons/128.png -------------------------------------------------------------------------------- /static/icons/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpevs/favioli/HEAD/static/icons/256.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize eol 2 | *.css text 3 | *.txt text 4 | *.md text 5 | *.html text 6 | *.js text 7 | *.json text 8 | *.ts text 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # build files 4 | dist 5 | 6 | # misc 7 | ._* 8 | .DS_Store 9 | .env 10 | .Trashes 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | tab_width = 2 10 | trim_trailing_whitespace = true 11 | 12 | [Makefile] 13 | indent_style = tab 14 | -------------------------------------------------------------------------------- /source/utilities/test_dom.ts: -------------------------------------------------------------------------------- 1 | import { DOMParser } from 'deno-dom'; 2 | 3 | globalThis.document = new DOMParser() 4 | .parseFromString( 5 | ``, 6 | 'text/html', 7 | // deno-lint-ignore no-explicit-any 8 | ) as any; 9 | -------------------------------------------------------------------------------- /source/components/only.tsx: -------------------------------------------------------------------------------- 1 | /* @jsx h */ 2 | import { Fragment, h, VNode } from 'preact'; 3 | 4 | export interface OnlyProps { 5 | if: boolean; 6 | children: VNode | string | (VNode | string)[]; 7 | } 8 | 9 | export default function Only({ if: predicate, children }: OnlyProps) { 10 | return ( 11 | 12 | {predicate ? children : null} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Description** Insert description of your change. Be sure to tag the issue if 2 | this is solving something from our 3 | [Issues page](https://github.com/ivebencrazy/favioli/issues). Also make sure 4 | your title has an emoji in it. 5 | 6 | **Changes** 7 | 8 | - [x] Change1 9 | - [x] Change2 10 | - [x] Change3 11 | 12 | **Screenshots** Insert a screenshot of the change 13 | -------------------------------------------------------------------------------- /source/models/__tests__/__snapshots__/emoji.test.ts.snap: -------------------------------------------------------------------------------- 1 | export const snapshot = {}; 2 | 3 | snapshot[`createEmoji 1`] = ` 4 | { 5 | aliases: [ 6 | "poro", 7 | ], 8 | description: "poro", 9 | emoji: "", 10 | emojiVersion: 0, 11 | group: "Custom Emojis", 12 | imageURL: "poro://poro-url", 13 | subgroup: "custom-emojis", 14 | tags: [ 15 | ], 16 | unicodeVersion: 0, 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /static/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Favioli 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /static/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Favioli 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /source/hooks/use_route.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'preact/hooks'; 2 | 3 | export default function useRoute() { 4 | const [route, setRoute] = useState(location.hash); 5 | 6 | useEffect(() => { 7 | const updateRoute = () => setRoute(location.hash); 8 | globalThis.addEventListener('hashchange', updateRoute, false); 9 | return () => { 10 | globalThis.removeEventListener('hashchange', updateRoute); 11 | }; 12 | }, []); 13 | 14 | return route; 15 | } 16 | -------------------------------------------------------------------------------- /source/components/emoji_selector/types.ts: -------------------------------------------------------------------------------- 1 | import { Emoji } from '../../models/emoji.ts'; 2 | 3 | export type SetSwitch = (state: boolean) => void; 4 | export type OnSelected = (emoji: Emoji) => void; 5 | export type SetRoute = (state: string) => void; 6 | 7 | export const ROUTE = { 8 | DEFAULT: 'DEFAULT', 9 | CREATE_CUSTOM: 'CREATE_CUSTOM', 10 | DELETE_CUSTOM: 'DELETE_CUSTOM', 11 | }; 12 | 13 | export type Route = 14 | | typeof ROUTE.DEFAULT 15 | | typeof ROUTE.CREATE_CUSTOM 16 | | typeof ROUTE.DELETE_CUSTOM; 17 | -------------------------------------------------------------------------------- /source/components/switch.tsx: -------------------------------------------------------------------------------- 1 | /* @jsx h */ 2 | import { Fragment, h, VNode } from 'preact'; 3 | 4 | export interface SwitchProps { 5 | value?: string; 6 | defaultCase: VNode | string | null; 7 | cases: { [name: string]: VNode | string | null }; 8 | } 9 | 10 | export default function Switch( 11 | { value, defaultCase = null, cases }: SwitchProps, 12 | ) { 13 | if (value == null) return null; 14 | return ( 15 | 16 | {cases[value] != null ? cases[value] : defaultCase} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /static/styles/popup.css: -------------------------------------------------------------------------------- 1 | /** RESET **/ 2 | html, body { 3 | margin: 0; 4 | padding: 0; 5 | line-height: 1.6; 6 | font-family: BlinkMacSystemFont, Avenir, "Avenir Next", "Roboto", "Ubuntu", "Helvetica Neue", sans-serif; 7 | color: var(--text-color); 8 | background: var(--background-color); 9 | } 10 | 11 | .favicon-icon { 12 | vertical-align: middle; 13 | padding-left: 5px; 14 | } 15 | 16 | .popup-wrapper { 17 | width: 300px; 18 | display: flex; 19 | flex-flow: column; 20 | } 21 | 22 | button { 23 | padding: 5px; 24 | margin: 5px; 25 | } 26 | -------------------------------------------------------------------------------- /source/models/favicon.ts: -------------------------------------------------------------------------------- 1 | import type { Emoji } from './emoji.ts'; 2 | 3 | export interface Favicon { 4 | // String (inc RegExp string) representing the url to match 5 | matcher: string; 6 | /** 7 | * Unique ID representing favicon (emoji.description) 8 | * We store emojis by ID and retrieve from storage when we want to use. 9 | * This allows us to save custom emoji image data and access on demand, 10 | * saving us space in chrome storage. 11 | */ 12 | emojiId?: string; 13 | } 14 | 15 | export function createFavicon(matcher = '', emoji?: Emoji): Favicon { 16 | return { matcher, emojiId: emoji?.description }; 17 | } 18 | -------------------------------------------------------------------------------- /source/utilities/parse_version.ts: -------------------------------------------------------------------------------- 1 | export default function parseVersion(version: string): { 2 | major: number; 3 | minor: number; 4 | patch: number; 5 | descriptor: string; 6 | } { 7 | if (!version) throw new Error('No Version Detected'); 8 | 9 | const [major, minor, patch, ...descriptors] = version.split(/\.|-/); 10 | 11 | if (major == null || minor == null || patch == null) { 12 | throw new Error(`Error Parsing Version ${version}`); 13 | } 14 | 15 | return { 16 | major: Number(major), 17 | minor: Number(minor), 18 | patch: Number(patch), 19 | descriptor: descriptors.join('-') || '', 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** A clear and 8 | concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** A clear and concise description of what you 11 | want to happen. 12 | 13 | **Describe alternatives you've considered** A clear and concise description of 14 | any alternative solutions or features you've considered. 15 | 16 | **Additional context** Add any other context or screenshots about the feature 17 | request here. 18 | -------------------------------------------------------------------------------- /source/models/__tests__/storage_legacy.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from 'test/asserts'; 2 | import { describe, it } from 'test/bdd'; 3 | 4 | import { v0, v1, v2 } from '../__fixtures__/settings_fixtures.ts'; 5 | import { isSettingsV1, migrateStorageFromV1 } from '../storage_legacy.ts'; 6 | 7 | describe('migrateStorageFromV1', () => { 8 | it('should migrate from v0 to v2', () => { 9 | assertEquals(isSettingsV1(v0), true); 10 | assertEquals(migrateStorageFromV1(v0), v2); 11 | }); 12 | 13 | it('should migrate from v1 to v2', () => { 14 | assertEquals(isSettingsV1(v1), true); 15 | assertEquals(migrateStorageFromV1(v1), v2); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /source/utilities/regex_utils.ts: -------------------------------------------------------------------------------- 1 | // Determines whether a string is in the shape of a regex 2 | export function isRegexString(filter: string): boolean { 3 | return Boolean(parseRegExp(filter)); 4 | } 5 | 6 | export function parseRegExp(filter: string): RegExp | null { 7 | try { 8 | const parts = filter.trim().split('/'); 9 | if (parts.length < 3) return null; 10 | else if (parts.length === 3) { 11 | const [_, regex, options] = parts; 12 | return new RegExp(regex, options); 13 | } else { 14 | // regex could have escaped "/" 15 | const options = parts.pop(); 16 | const regex = parts.slice(1).join('\/'); 17 | return new RegExp(regex, options); 18 | } 19 | } catch { 20 | return null; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /source/components/emoji_selector/components/emoji_button.tsx: -------------------------------------------------------------------------------- 1 | /* @jsx h */ 2 | import type { Emoji } from '../../../models/emoji.ts'; 3 | import type { Ref } from 'preact'; 4 | 5 | import { h } from 'preact'; 6 | 7 | interface EmojiButtonProps { 8 | emoji: Emoji; 9 | className?: string; 10 | onClick?: (e: Event) => void; 11 | // deno-lint-ignore no-explicit-any 12 | ref?: Ref; 13 | } 14 | 15 | export default function EmojiButton({ 16 | emoji, 17 | ...props 18 | }: EmojiButtonProps) { 19 | if (emoji?.imageURL) { 20 | return ( 21 | 24 | ); 25 | } 26 | 27 | return ; 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** A clear and concise description of what the bug is. 8 | 9 | **To Reproduce** Steps to reproduce the behavior: 10 | 11 | 1. Go to '...' 12 | 2. Click on '....' 13 | 3. Scroll down to '....' 14 | 4. See error 15 | 16 | **Expected behavior** A clear and concise description of what you expected to 17 | happen. 18 | 19 | **Screenshots** If applicable, add screenshots to help explain your problem. 20 | 21 | **Desktop (please complete the following information):** 22 | 23 | - OS: [e.g. iOS] 24 | - Browser [e.g. chrome, safari] 25 | - Version [e.g. 22] 26 | 27 | **Additional context** Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /source/components/__tests__/only.test.tsx: -------------------------------------------------------------------------------- 1 | /* @jsx h */ 2 | import { h } from 'preact'; 3 | import { assertEquals } from 'test/asserts'; 4 | import { describe, it } from 'test/bdd'; 5 | import { render } from '@testing-library/preact'; 6 | 7 | import '../../utilities/test_dom.ts'; 8 | import Only from '../only.tsx'; 9 | 10 | it('should render if === true', () => { 11 | const { container } = render( 12 | 13 | hello 14 | , 15 | ); 16 | assertEquals(container.textContent, 'hello'); 17 | }); 18 | 19 | it('should not render if === false', () => { 20 | const { container } = render( 21 | 22 | hello 23 | , 24 | ); 25 | assertEquals(container.textContent, ''); 26 | }); 27 | -------------------------------------------------------------------------------- /import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "browser": "https://deno.land/x/bext@v1.0.0/mod.ts", 4 | "browser/": "https://deno.land/x/bext@v1.0.0/", 5 | "emoji": "https://deno.land/x/emoji@0.2.1/mod.ts", 6 | "preact": "https://esm.sh/preact@10.11.3?dev", 7 | "preact/hooks": "https://esm.sh/preact@10.11.3/hooks?dev", 8 | 9 | "deno-dom": "https://deno.land/x/deno_dom@v0.1.36-alpha/deno-dom-wasm.ts", 10 | "test/asserts": "https://deno.land/std@0.170.0/testing/asserts.ts", 11 | "test/bdd": "https://deno.land/std@0.170.0/testing/bdd.ts", 12 | "test/mock": "https://deno.land/std@0.170.0/testing/mock.ts", 13 | "test/snapshot": "https://deno.land/std@0.170.0/testing/snapshot.ts", 14 | "@testing-library/preact": "https://esm.sh/@testing-library/preact@3.2.3/pure?dev" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /source/components/header.tsx: -------------------------------------------------------------------------------- 1 | /* @jsx h */ 2 | import { h } from 'preact'; 3 | 4 | export interface HeaderProps { 5 | route?: string; 6 | } 7 | 8 | export default function Header({ route }: HeaderProps) { 9 | const isDefaultRoute = route !== '#settings' && route !== '#about'; 10 | 11 | return ( 12 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /source/hooks/use_status.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'preact/hooks'; 2 | 3 | const STATUS_TIME = 4000; 4 | 5 | // deno-lint-ignore no-explicit-any 6 | type SaveFunc = (...args: any[]) => any | void; 7 | 8 | export default function useStatus( 9 | error: string, 10 | save: SaveFunc, 11 | ) { 12 | const [status, setStatus] = useState(''); 13 | 14 | useEffect(() => { 15 | setStatus(error); 16 | setTimeout(() => setStatus(''), STATUS_TIME); 17 | }, [error]); 18 | 19 | return { 20 | status, 21 | save: useCallback(async (...args) => { 22 | try { 23 | await save(...args); 24 | setStatus('Successfully Saved'); 25 | } catch (e) { 26 | setStatus(`Error: ${e}`); 27 | } 28 | setTimeout(() => setStatus(''), STATUS_TIME); 29 | }, [save]), 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /source/hooks/use_focus_observer.ts: -------------------------------------------------------------------------------- 1 | import type { RefObject } from 'preact'; 2 | 3 | import { useEffect, useRef } from 'preact/hooks'; 4 | 5 | export default function useFocusObserver( 6 | callback: () => void, 7 | // deno-lint-ignore no-explicit-any 8 | ignoredRefs: RefObject[] = [], 9 | ) { 10 | const ref = useRef(); 11 | 12 | useEffect(() => { 13 | function handleClick(event: Event) { 14 | if ( 15 | event.target instanceof HTMLElement && 16 | ![ref, ...ignoredRefs].some((ref) => { 17 | if (!ref?.current?.contains) return false; 18 | return ref?.current.contains(event.target); 19 | }) 20 | ) { 21 | callback(); 22 | } 23 | } 24 | 25 | document.addEventListener('mousedown', handleClick); 26 | 27 | return () => { 28 | document.removeEventListener('mousedown', handleClick); 29 | }; 30 | }, [ref]); 31 | 32 | return ref; 33 | } 34 | -------------------------------------------------------------------------------- /source/hooks/use_active_tab.ts: -------------------------------------------------------------------------------- 1 | import Chrome from 'browser/types/chrome.ts'; 2 | 3 | import { useEffect, useState } from 'preact/hooks'; 4 | import browserAPI from 'browser'; 5 | 6 | const queryOptions = { active: true }; 7 | const { storage, tabs } = browserAPI; 8 | 9 | export default function useActiveTab(): Chrome.Tab | void { 10 | const [activeTab, setActiveTab] = useState(); 11 | 12 | useEffect(function updateactiveTab() { 13 | async function queryAndSetActiveTab() { 14 | setActiveTab((await tabs.query(queryOptions))[0]); 15 | } 16 | storage.onChanged.addListener(queryAndSetActiveTab); 17 | tabs.onUpdated.addListener(queryAndSetActiveTab); 18 | queryAndSetActiveTab().catch(console.error); 19 | (() => { 20 | storage.onChanged.removeListener(queryAndSetActiveTab); 21 | tabs.onUpdated.removeListener(queryAndSetActiveTab); 22 | }); 23 | }, []); 24 | 25 | return activeTab; 26 | } 27 | -------------------------------------------------------------------------------- /source/utilities/__tests__/parse_version.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from 'test/asserts'; 2 | import { describe, it } from 'test/bdd'; 3 | 4 | describe('parseVersion', () => { 5 | it('should parse version with descriptor', () => { 6 | assertEquals(parseVersion('2.0.0-beta-1'), { 7 | major: 2, 8 | minor: 0, 9 | patch: 0, 10 | descriptor: 'beta-1', 11 | }); 12 | }); 13 | 14 | it('should parse version without descriptor', () => { 15 | assertEquals(parseVersion('1.0.3'), { 16 | major: 1, 17 | minor: 0, 18 | patch: 3, 19 | descriptor: '', 20 | }); 21 | }); 22 | 23 | it('should error if no version', () => { 24 | try { 25 | parseVersion(''); 26 | } catch (e) { 27 | assertEquals(e.message, 'No Version Detected'); 28 | } 29 | }); 30 | 31 | it('should error if invalid version', () => { 32 | try { 33 | parseVersion('51234'); 34 | } catch (e) { 35 | assertEquals(e.message, 'Error Parsing Version 51234'); 36 | } 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /.github/workflows/deno.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow will install Deno then run Deno lint and test. 7 | # For more information see: https://github.com/denoland/setup-deno 8 | 9 | name: Deno 10 | 11 | on: 12 | push: 13 | branches: ["main", "staging"] 14 | pull_request: 15 | branches: ["main", "staging"] 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | test: 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - name: Setup repo 26 | uses: actions/checkout@v3 27 | 28 | - name: Setup Deno 29 | # uses: denoland/setup-deno@v1 30 | uses: denoland/setup-deno@004814556e37c54a2f6e31384c9e18e983317366 31 | with: 32 | deno-version: v1.x 33 | 34 | - name: Verify formatting 35 | run: deno fmt --check 36 | 37 | - name: Run tests 38 | run: deno task test:all 39 | -------------------------------------------------------------------------------- /source/utilities/__tests__/regex_utils.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertStrictEquals } from 'test/asserts'; 2 | import { describe, it } from 'test/bdd'; 3 | 4 | import { isRegexString, parseRegExp } from '../regex_utils.ts'; 5 | 6 | describe('isRegexString and parseRegExp', () => { 7 | it('/myurlorsomething', () => { 8 | assertStrictEquals(isRegexString('/myurlorsomething'), false); 9 | assertEquals(parseRegExp('/myurlorsomething'), null); 10 | }); 11 | it('/myregex/i', () => { 12 | assertStrictEquals(isRegexString('/myregex/i'), true); 13 | assertEquals(parseRegExp('/myregex/i'), /myregex/i); 14 | }); 15 | it('/myregex/', () => { 16 | assertStrictEquals(isRegexString('/myregex/'), true); 17 | assertEquals(parseRegExp('/myregex/'), /myregex/); 18 | }); 19 | it('/myr\\/egex/', () => { 20 | assertStrictEquals(isRegexString('/myr\\/egex/'), true); 21 | assertEquals(parseRegExp('/myr\\/egex/'), /myr\/egex/); 22 | }); 23 | it('/myregex/aaa', () => { 24 | assertStrictEquals(isRegexString('/myregex/aaa'), false); 25 | assertEquals(parseRegExp('/myregex/aaa'), null); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Ben Pevsner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "deno.ns", 5 | "dom", 6 | "dom.iterable", 7 | "dom.asynciterable", 8 | "esnext" 9 | ], 10 | "jsx": "react", 11 | "jsxFactory": "h", 12 | "jsxFragmentFactory": "Fragment" 13 | }, 14 | "importMap": "./import_map.json", 15 | "fmt": { 16 | "options": { 17 | "singleQuote": true, 18 | "proseWrap": "preserve" 19 | } 20 | }, 21 | "lint": { 22 | "files": { 23 | "exclude": ["dist"] 24 | } 25 | }, 26 | "tasks": { 27 | "dev": "bext --watch", 28 | "build": "bext", 29 | "test": "deno test -A source", 30 | "test:all": "deno fmt && deno task check:all && deno task test && deno lint", 31 | "test:update": "deno test -A -- --update source", 32 | "check:all": "deno task check:background && deno task check:content_script && deno task check:options && deno task check:popup", 33 | "check:background": "deno check source/background.ts", 34 | "check:content_script": "deno check source/content_script.ts", 35 | "check:options": "deno check source/options.tsx", 36 | "check:popup": "deno check source/popup.tsx" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /source/utilities/permissions.ts: -------------------------------------------------------------------------------- 1 | // https://developer.chrome.com/docs/extensions/reference/permissions/ 2 | import browserAPI from 'bext'; 3 | const { contains, request } = browserAPI.permissions; 4 | 5 | const permissions = ['tabs']; 6 | const allOrigins = ['https://*/*', 'http://*/*']; 7 | 8 | export function requestPermissionToSites(origins: string[]): Promise { 9 | return requestPermission({ permissions, origins }); 10 | } 11 | 12 | export function requestPermissionToAllSites(): Promise { 13 | return requestPermission({ permissions, origins: allOrigins }); 14 | } 15 | 16 | /** 17 | * @return Promise boolean 18 | * true - user has granted permission (now or previously) 19 | * false - user has not grated permission 20 | */ 21 | async function requestPermission(args: { 22 | permissions: string[]; 23 | origins: string[]; 24 | }): Promise { 25 | const userAlreadyHasPermission = 26 | await (new Promise((resolve) => 27 | contains(args, (result: boolean) => resolve(result)) 28 | )); 29 | 30 | if (userAlreadyHasPermission) return true; 31 | 32 | return new Promise((resolve) => { 33 | return request(args, (granted: boolean) => resolve(granted)); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /source/components/checkbox.tsx: -------------------------------------------------------------------------------- 1 | /* @jsx h */ 2 | import { h } from 'preact'; 3 | import { useCallback, useState } from 'preact/hooks'; 4 | 5 | export type Target = { [name: string]: boolean }; 6 | export type CheckboxOnChange = (target: Target) => void; 7 | 8 | export interface CheckboxProps { 9 | name: string; 10 | checked?: boolean; 11 | label: string; 12 | onChange?: CheckboxOnChange; 13 | } 14 | 15 | export default function Checkbox({ 16 | checked = false, 17 | name, 18 | label, 19 | ...props 20 | }: CheckboxProps) { 21 | const onChange = useCallback((e: Event) => { 22 | if (e.target instanceof HTMLInputElement) { 23 | const { name, checked } = e.target; 24 | if (props.onChange && name && typeof checked === 'boolean') { 25 | props.onChange({ [name]: checked }); 26 | } 27 | } 28 | }, [props.onChange]); 29 | 30 | return ( 31 |
32 | 33 | 41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /source/hooks/use_list_state.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'preact/hooks'; 2 | 3 | export interface ListState { 4 | contents: Type[]; 5 | addItem: (item: Type) => void; 6 | updateItem: (index: number, item: Type) => void; 7 | deleteItem: (index: number) => void; 8 | } 9 | 10 | export default function useListState( 11 | initialValue: Type[], 12 | ): ListState { 13 | const [contents, setContents] = useState(initialValue); 14 | 15 | useEffect(() => setContents(initialValue), [initialValue]); 16 | 17 | return { 18 | contents, 19 | 20 | addItem: useCallback((listItem: Type) => { 21 | setContents([...contents, listItem]); 22 | }, [contents]), 23 | 24 | updateItem: useCallback( 25 | (indexToUpdate: number, newListItem: Type) => { 26 | const newList: Type[] = contents 27 | .map((oldListItem, index) => 28 | index === indexToUpdate ? newListItem : oldListItem 29 | ); 30 | setContents(newList); 31 | }, 32 | [contents], 33 | ), 34 | 35 | deleteItem: useCallback((itemIndex: number) => { 36 | setContents(contents.filter((_, index) => index !== itemIndex)); 37 | }, [contents]), 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /source/utilities/i18n.ts: -------------------------------------------------------------------------------- 1 | const broadPermissionsWarning = `\ 2 | This feature requires you give Favioli the ability to view and modify all websites you visit. 3 | This permission is used to: 4 | 5 | 1. Determine if a website has a favicon. 6 | 2. Select a favicon based on the website url. 7 | 3. Modify a website and add a favicon. 8 | 9 | Favioli does NOT collect or store any of this data, and does NOT use these permissions for anything besides what is stated here.`; 10 | 11 | const en: { [name: string]: string } = { 12 | enableOverrideAllLabel: 'Override All Favicons', 13 | enableOverrideIndicatorLabel: 'Indicate Overridden Favicons', 14 | enableFaviconAutofillDesc: 15 | 'If a website doesn\'t have a favicon, Favioli will automatically create one for it using an emoji.', 16 | enableFaviconAutofillLabel: 'Enable Autofill', 17 | enableFaviconAutofillPopup: broadPermissionsWarning, 18 | enableSiteIgnoreDesc: 'Select sites that autofill should ignore', 19 | enableSiteIgnoreLabel: 'Enable Ignore List', 20 | faviconListTitle: 'Override Favicons on these Sites', 21 | ignoreListTitle: 'Ignore These Sites', 22 | saveLabel: 'Save', 23 | }; 24 | 25 | export function t(id: string): string { 26 | if (!id) { 27 | console.warn(`Invalid string id: ${id}`); 28 | } 29 | return en[id] || ''; 30 | } 31 | -------------------------------------------------------------------------------- /source/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Favioli", 4 | "description": "Emoji favicons for the web", 5 | "version": "2.0.1", 6 | "background": { 7 | "service_worker": "background.js" 8 | }, 9 | "content_scripts": [ 10 | { 11 | "matches": ["*://*/*"], 12 | "js": ["content_script.js"], 13 | "run_at": "document_idle" 14 | } 15 | ], 16 | "options_page": "options.html", 17 | "options_ui": { 18 | "page": "options.html", 19 | "open_in_tab": true, 20 | "browser_style": false 21 | }, 22 | "permissions": [ 23 | "storage", 24 | "tabs" 25 | ], 26 | "host_permissions": [ 27 | "http://*/*", 28 | "https://*/*" 29 | ], 30 | "action": { 31 | "default_popup": "popup.html", 32 | "default_icon": { 33 | "32": "icons/32.png", 34 | "48": "icons/48.png", 35 | "64": "icons/64.png", 36 | "128": "icons/128.png", 37 | "256": "icons/256.png" 38 | } 39 | }, 40 | "browser_action": { 41 | "default_icon": "icons/32.png", 42 | "default_popup": "popup.html" 43 | }, 44 | "icons": { 45 | "32": "icons/32.png", 46 | "48": "icons/48.png", 47 | "64": "icons/64.png", 48 | "128": "icons/128.png", 49 | "256": "icons/256.png" 50 | }, 51 | "applications": { 52 | "gecko": { 53 | "id": "favioli@bpev.me" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /source/components/list.tsx: -------------------------------------------------------------------------------- 1 | /* @jsx h */ 2 | 3 | import type { ListState } from '../hooks/use_list_state.ts'; 4 | import type { Favicon } from '../models/favicon.ts'; 5 | 6 | import { h } from 'preact'; 7 | import { useRef } from 'preact/hooks'; 8 | 9 | import ListInput, { LIST_TYPE, ListType } from './list_input.tsx'; 10 | 11 | export type { ListType }; 12 | export { LIST_TYPE }; 13 | 14 | export interface ListProps { 15 | type: ListType; 16 | state: ListState; 17 | } 18 | 19 | export default function List({ type, state }: ListProps) { 20 | const listRef = useRef(null); 21 | const { addItem, deleteItem, updateItem, contents } = state; 22 | return ( 23 |
24 | {contents.map((listItem: Favicon, index: number) => ( 25 | { 33 | deleteItem(index); 34 | const firstInput = listRef?.current?.querySelector('input'); 35 | if (firstInput) firstInput.focus(); 36 | }} 37 | /> 38 | )).concat( 39 | , 45 | )} 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /source/utilities/favicon_selector.ts: -------------------------------------------------------------------------------- 1 | import type { Favicon } from '../models/favicon.ts'; 2 | import type { Settings } from '../models/settings.ts'; 3 | import type Autoselector from './favicon_autoselector.ts'; 4 | import { parseRegExp } from './regex_utils.ts'; 5 | 6 | /** 7 | * Override Priority 8 | * 9 | * 1. Ignore list (always ignore if ignore list enabled) 10 | * 2. Site list (if matched in site list, user manually added) 11 | * 3. Autofill (if autofill is enabled) 12 | * 4. Ignore (autofill NOT enabled, user hasn't added to sitelist) 13 | */ 14 | export default function selectFavicon( 15 | url: string, 16 | settings: Settings, 17 | autoselector?: Autoselector, 18 | ): [Favicon | void, boolean] { 19 | const { ignoreList, siteList, features } = settings; 20 | if (features.enableSiteIgnore && ignoreList.some(listItemMatchesURL(url))) { 21 | return [undefined, false]; 22 | } 23 | 24 | const firstMatchingFavicon = siteList.filter(listItemMatchesURL(url))?.[0]; 25 | if (firstMatchingFavicon) return [firstMatchingFavicon, true]; 26 | 27 | return autoselector && features.enableFaviconAutofill 28 | ? [autoselector.selectFavicon(url), Boolean(features.enableOverrideAll)] 29 | : [undefined, false]; 30 | } 31 | 32 | function listItemMatchesURL(url: string): (favicon: Favicon) => boolean { 33 | return (favicon: Favicon): boolean => { 34 | if (!url || !favicon?.matcher) return false; 35 | 36 | const regex = parseRegExp(favicon.matcher); 37 | if (regex) { 38 | return regex.test(url); 39 | } 40 | 41 | return url.toLocaleLowerCase().includes( 42 | favicon.matcher.toLocaleLowerCase(), 43 | ); 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /source/models/settings.ts: -------------------------------------------------------------------------------- 1 | import type { BrowserStorage } from '../hooks/use_browser_storage.ts'; 2 | import type { AutoselectorVersion } from '../utilities/favicon_autoselector.ts'; 3 | 4 | import { createContext } from 'preact'; 5 | 6 | import manifest from '../manifest.json' assert { type: 'json' }; 7 | import { AUTOSELECTOR_VERSION } from '../utilities/favicon_autoselector.ts'; 8 | import { Favicon } from './favicon.ts'; 9 | 10 | export const SETTINGS_KEY = 'settings'; 11 | 12 | export interface Settings { 13 | version: string; 14 | autoselectorVersion: AutoselectorVersion; 15 | 16 | siteList: Favicon[]; 17 | ignoreList: Favicon[]; 18 | frequentlyUsed: Favicon[]; 19 | customEmojiIds: string[]; 20 | 21 | features: { 22 | enableAutoselectorIncludeCountryFlags: boolean; 23 | enableFaviconAutofill: boolean; 24 | enableSiteIgnore: boolean; 25 | enableOverrideAll: boolean; 26 | enableOverrideIndicator: boolean; 27 | }; 28 | } 29 | 30 | export const DEFAULT_SETTINGS: Settings = { 31 | version: manifest.version, 32 | autoselectorVersion: AUTOSELECTOR_VERSION.UNICODE_12, 33 | 34 | siteList: [], 35 | ignoreList: [], 36 | customEmojiIds: [], 37 | frequentlyUsed: [], 38 | 39 | features: { 40 | enableAutoselectorIncludeCountryFlags: false, 41 | enableFaviconAutofill: true, 42 | enableSiteIgnore: false, 43 | enableOverrideAll: false, 44 | enableOverrideIndicator: false, 45 | }, 46 | }; 47 | 48 | export const SettingsContext = createContext>({ 49 | loading: true, 50 | cache: DEFAULT_SETTINGS, 51 | setCache: () => {}, 52 | saveCacheToStorage: async () => {}, 53 | saveToStorageBypassCache: async () => {}, 54 | }); 55 | -------------------------------------------------------------------------------- /source/utilities/__tests__/favicon_autoselector.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertStrictEquals } from 'test/asserts'; 2 | import { it } from 'test/bdd'; 3 | 4 | import Autoselector, { AUTOSELECTOR_VERSION } from '../favicon_autoselector.ts'; 5 | 6 | const { FAVIOLI_LEGACY, UNICODE_12, UNICODE_11, UNICODE_09 } = 7 | AUTOSELECTOR_VERSION; 8 | 9 | it('Should select favicon', () => { 10 | const autoselector = new Autoselector(AUTOSELECTOR_VERSION.UNICODE_12); 11 | const favicon = autoselector.selectFavicon('https://favioli.com'); 12 | assertStrictEquals(favicon.emojiId, 'tractor'); 13 | assertStrictEquals(favicon.matcher, 'https://favioli.com'); 14 | }); 15 | 16 | it('Should select different emojis for different sets', () => { 17 | const url = 'https://favioli.com'; 18 | 19 | const legacyFavicon = new Autoselector(FAVIOLI_LEGACY).selectFavicon(url); 20 | assertEquals(legacyFavicon.emojiId, 'sad but relieved face'); 21 | 22 | const unicode11Favicon = new Autoselector(UNICODE_11).selectFavicon(url); 23 | assertEquals(unicode11Favicon.emojiId, 'mouth'); 24 | 25 | const unicode12Favicon = new Autoselector(UNICODE_12).selectFavicon(url); 26 | assertEquals(unicode12Favicon.emojiId, 'tractor'); 27 | }); 28 | 29 | it('Should default to no flags', () => { 30 | const url = 'http://bpev.me'; 31 | 32 | const noFlags = new Autoselector(UNICODE_09).selectFavicon(url); 33 | assertEquals(noFlags.emojiId, 'duck'); 34 | 35 | const hasFlags = new Autoselector(UNICODE_09, { includeFlags: true }) 36 | .selectFavicon(url); 37 | assertEquals(hasFlags.emojiId, 'flag: Gambia'); 38 | }); 39 | 40 | it('Should give the same emoji for the same domain', () => { 41 | const autoselect = new Autoselector(UNICODE_12); 42 | 43 | assertEquals( 44 | autoselect.selectFavicon('https://favioli.com').emojiId, 45 | autoselect.selectFavicon('http://favioli.com/lala/blah?hehe=hoho').emojiId, 46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /source/pages/favicons_page.tsx: -------------------------------------------------------------------------------- 1 | /* @jsx h */ 2 | import type { BrowserStorage } from '../hooks/use_browser_storage.ts'; 3 | import type { Favicon } from '../models/favicon.ts'; 4 | import type { Settings } from '../models/settings.ts'; 5 | 6 | import { h } from 'preact'; 7 | import { useContext, useEffect } from 'preact/hooks'; 8 | 9 | import List, { LIST_TYPE } from '../components/list.tsx'; 10 | import Only from '../components/only.tsx'; 11 | import useListState from '../hooks/use_list_state.ts'; 12 | import { SettingsContext } from '../models/settings.ts'; 13 | import { t } from '../utilities/i18n.ts'; 14 | 15 | export default function FaviconsPage({ save }: { save?: (e: Event) => void }) { 16 | const storage = useContext>(SettingsContext); 17 | const { siteList, ignoreList, features } = storage.cache; 18 | const { enableSiteIgnore } = features; 19 | const siteListState = useListState(siteList); 20 | const ignoreListState = useListState(ignoreList); 21 | 22 | useEffect(() => { 23 | if (storage) { 24 | storage.setCache({ 25 | siteList: siteListState.contents, 26 | ignoreList: ignoreListState.contents, 27 | }); 28 | } 29 | }, [siteListState.contents, ignoreListState.contents]); 30 | 31 | return ( 32 |
33 |

{t('faviconListTitle')}

34 | 35 | 36 | 37 |

38 | {t('ignoreListTitle')} 39 | 40 | (Disabled) 41 | 42 |

43 | 44 | 45 |
46 | 47 | 61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /source/models/storage_legacy.ts: -------------------------------------------------------------------------------- 1 | import type { Favicon } from './favicon.ts'; 2 | import type { Settings } from './settings.ts'; 3 | 4 | import { AUTOSELECTOR_VERSION } from '../utilities/favicon_autoselector.ts'; 5 | import { emoji } from './emoji.ts'; 6 | import { createFavicon } from './favicon.ts'; 7 | import { DEFAULT_SETTINGS } from './settings.ts'; 8 | 9 | /** 10 | * Storage from Favioli V1 11 | * Includes tools for migration 12 | */ 13 | export interface SettingsV1 { 14 | flagReplaced: boolean; 15 | overrideAll: boolean; 16 | overrides: EmojiV1[]; 17 | skips: string[]; 18 | } 19 | 20 | export interface EmojiV1 { 21 | emoji: string | { 22 | colons: string; 23 | emoticons: string[]; 24 | id: string; 25 | name: string; 26 | native: string; 27 | short_names: string[]; 28 | skin: null; 29 | unified: string; 30 | }; 31 | filter: string; 32 | } 33 | 34 | export const LEGACY_STORAGE_KEYS = [ 35 | 'flagReplaced', 36 | 'overrideAll', 37 | 'overrides', 38 | 'skips', 39 | ]; 40 | 41 | export function isSettingsV1(settings: unknown): settings is SettingsV1 { 42 | if (typeof settings !== 'object' || settings == null) return false; 43 | return ( 44 | 'flagReplaced' in settings || 45 | 'overrideAll' in settings || 46 | 'overrides' in settings || 47 | 'skips' in settings 48 | ); 49 | } 50 | 51 | export function migrateStorageFromV1(legacySettings: SettingsV1): Settings { 52 | const settings = { ...DEFAULT_SETTINGS }; 53 | 54 | settings.features = { 55 | enableAutoselectorIncludeCountryFlags: false, 56 | enableFaviconAutofill: true, 57 | enableOverrideAll: legacySettings.overrideAll, 58 | enableOverrideIndicator: legacySettings.flagReplaced, 59 | enableSiteIgnore: Boolean(legacySettings?.skips?.length), 60 | }; 61 | 62 | settings.siteList = (legacySettings?.overrides || []) 63 | .map((legacyFavicon): Favicon => { 64 | const emojiInput = typeof legacyFavicon.emoji === 'string' 65 | ? emoji.infoByCode(legacyFavicon.emoji) 66 | : emoji.infoByCode(legacyFavicon.emoji.native); 67 | return createFavicon(legacyFavicon.filter, emojiInput); 68 | }); 69 | 70 | settings.ignoreList = (legacySettings?.skips || []) 71 | .map((skip) => createFavicon(skip)); 72 | 73 | settings.autoselectorVersion = AUTOSELECTOR_VERSION.FAVIOLI_LEGACY; 74 | 75 | return settings; 76 | } 77 | -------------------------------------------------------------------------------- /source/hooks/use_selected_favicon.ts: -------------------------------------------------------------------------------- 1 | import type { Emoji } from '../models/emoji.ts'; 2 | import type { Favicon } from '../models/favicon.ts'; 3 | import type { Settings } from '../models/settings.ts'; 4 | 5 | import { useEffect, useMemo, useState } from 'preact/hooks'; 6 | 7 | import { getEmoji } from '../models/emoji.ts'; 8 | import Autoselector from '../utilities/favicon_autoselector.ts'; 9 | import selectFavicon from '../utilities/favicon_selector.ts'; 10 | import { createFaviconURLFromChar } from '../utilities/image_helpers.ts'; 11 | 12 | /** 13 | * Get the favicon, emoji, and displayed favicon url for a specific location. 14 | * Treat as if we are autofilling, and not ignoring sites. 15 | */ 16 | export default function useSelectedFavicon( 17 | url: string, 18 | settings: Settings, 19 | ): { 20 | selectedFavicon: Favicon | null; 21 | selectedEmoji: Emoji | null; 22 | selectedImageURL: string; 23 | } { 24 | const { autoselectorVersion, features } = settings; 25 | const includeFlags = Boolean(features?.enableAutoselectorIncludeCountryFlags); 26 | 27 | const [selectedFavicon, setFavicon] = useState(null); 28 | const [selectedEmoji, setEmoji] = useState(null); 29 | 30 | const autoselector = useMemo(function () { 31 | if (!autoselectorVersion) return null; 32 | return new Autoselector(autoselectorVersion, { includeFlags }); 33 | }, [autoselectorVersion]); 34 | 35 | useEffect(function () { 36 | if (!url || !settings || !autoselector) return; 37 | (async () => { 38 | const features = { 39 | ...settings.features, 40 | enableFaviconAutofill: true, 41 | enableSiteIgnore: false, 42 | }; 43 | const checkSettings = { ...settings, features }; 44 | const [favicon] = await selectFavicon(url, checkSettings, autoselector); 45 | const emoji = favicon?.emojiId ? await getEmoji(favicon.emojiId) : null; 46 | setFavicon(favicon || null); 47 | setEmoji(emoji || null); 48 | })(); 49 | }, [autoselector, settings, url]); 50 | 51 | const selectedImageURL = useMemo((): string => { 52 | if (!selectedEmoji) return ''; 53 | const { imageURL, emoji } = selectedEmoji; 54 | return imageURL || 55 | createFaviconURLFromChar(emoji || '', features.enableOverrideIndicator); 56 | }, [selectedEmoji, features.enableOverrideIndicator]); 57 | 58 | if (!url || !settings) { 59 | return { 60 | selectedFavicon: null, 61 | selectedEmoji: null, 62 | selectedImageURL: '', 63 | }; 64 | } 65 | 66 | return { selectedFavicon, selectedEmoji, selectedImageURL }; 67 | } 68 | -------------------------------------------------------------------------------- /source/pages/about_page.tsx: -------------------------------------------------------------------------------- 1 | /* @jsx h */ 2 | import type { BrowserStorage } from '../hooks/use_browser_storage.ts'; 3 | import type { Settings } from '../models/settings.ts'; 4 | 5 | import { Fragment, h } from 'preact'; 6 | import { useContext } from 'preact/hooks'; 7 | 8 | import Only from '../components/only.tsx'; 9 | import { SettingsContext } from '../models/settings.ts'; 10 | 11 | export default function () { 12 | const { cache } = useContext>(SettingsContext); 13 | 14 | return ( 15 | 16 |

About Favioli

17 | 18 |

Version {cache.version}

19 |
20 | 21 |

What is Favioli?

22 |

23 | Favioli is a tool for modifying website favicons (icons that represent 24 | websites in tabs, your browsing history, and your bookmarks). 25 |

26 | 27 |

Privacy Policy

28 |

29 | Favioli does not track you, does not transmit any data outside of your 30 | computer, and does not modify anything on websites outside of favicons. 31 |

32 |

33 | The only links to external sites/etc are informational links on this 34 | about page. 35 |

36 |

37 | I am actively trying to respect your privacy. The source code is freely 38 | available for inspection and/or to build from source: 39 |

40 |
41 | https://github.com/ivebencrazy/favioli 42 | 43 | 44 |

Permissions

45 |

46 | To replace favicons, Favioli runs code on websites that you are 47 | browsing. If you are very nervous about that, some browsers now allow 48 | you to restrict extensions to only run on specific websites. As features 49 | are added, I will try to keep permissions as granular as possible. 50 |

51 | 52 |

Something is broken!

53 |

54 | Please report any bugs you find on{' '} 55 | 56 | Github Issues 57 | ! Since Favioli does not collect any automated feedback, any kind of 58 | feedback is very useful! 59 |

60 |

61 | In the future, I will add a form to{' '} 62 | favioli.com/contact{' '} 63 | so that sending feedback will be easier. 64 |

65 | 66 |

Thanks for using Favioli!

67 |

68 | – Ben 69 |

70 |

71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /source/models/__fixtures__/settings_fixtures.ts: -------------------------------------------------------------------------------- 1 | import type { Settings } from '../settings.ts'; 2 | import type { SettingsV1 } from '../storage_legacy.ts'; 3 | 4 | import { emoji } from '../emoji.ts'; 5 | import { createFavicon } from '../favicon.ts'; 6 | 7 | export const v0: SettingsV1 = { 8 | flagReplaced: true, 9 | overrideAll: false, 10 | overrides: [ 11 | { 12 | emoji: '😍', 13 | filter: 'hello', 14 | }, 15 | { 16 | emoji: '😃', 17 | filter: 'goodbye', 18 | }, 19 | { 20 | emoji: '🤩', 21 | filter: 'sweet lahd', 22 | }, 23 | ], 24 | skips: [ 25 | 'hahahahh', 26 | ], 27 | }; 28 | 29 | export const v1: SettingsV1 = { 30 | flagReplaced: true, 31 | overrideAll: false, 32 | overrides: [ 33 | { 34 | emoji: { 35 | colons: ':heart_eyes:', 36 | emoticons: [], 37 | id: 'heart_eyes', 38 | name: 'Smiling Face with Heart-Shaped Eyes', 39 | native: '😍', 40 | short_names: [ 41 | 'heart_eyes', 42 | ], 43 | skin: null, 44 | unified: '1f60d', 45 | }, 46 | filter: 'hello', 47 | }, 48 | { 49 | emoji: { 50 | colons: ':smiley:', 51 | emoticons: [ 52 | '=)', 53 | '=-)', 54 | ], 55 | id: 'smiley', 56 | name: 'Smiling Face with Open Mouth', 57 | native: '😃', 58 | short_names: [ 59 | 'smiley', 60 | ], 61 | skin: null, 62 | unified: '1f603', 63 | }, 64 | filter: 'goodbye', 65 | }, 66 | { 67 | emoji: { 68 | colons: ':star-struck:', 69 | emoticons: [], 70 | id: 'star-struck', 71 | name: 'Grinning Face with Star Eyes', 72 | native: '🤩', 73 | short_names: [ 74 | 'star-struck', 75 | 'grinning_face_with_star_eyes', 76 | ], 77 | skin: null, 78 | unified: '1f929', 79 | }, 80 | filter: 'sweet lahd', 81 | }, 82 | ], 83 | skips: [ 84 | 'hahahahh', 85 | ], 86 | }; 87 | 88 | export const v2: Settings = { 89 | autoselectorVersion: 'FAVIOLI_LEGACY', 90 | frequentlyUsed: [], 91 | customEmojiIds: [], 92 | features: { 93 | enableAutoselectorIncludeCountryFlags: false, 94 | enableFaviconAutofill: true, 95 | enableOverrideAll: false, 96 | enableSiteIgnore: true, 97 | enableOverrideIndicator: true, 98 | }, 99 | ignoreList: [ 100 | createFavicon('hahahahh'), 101 | ], 102 | siteList: [ 103 | createFavicon('hello', emoji.infoByCode('😍')), 104 | createFavicon('goodbye', emoji.infoByCode('😃')), 105 | createFavicon('sweet lahd', emoji.infoByCode('🤩')), 106 | ], 107 | version: '2.0.1', 108 | }; 109 | -------------------------------------------------------------------------------- /source/background.ts: -------------------------------------------------------------------------------- 1 | import type Chrome from 'browser/types/chrome.ts'; 2 | import type { Settings } from './models/settings.ts'; 3 | import type { SettingsV1 } from './models/storage_legacy.ts'; 4 | 5 | import browserAPI from 'browser'; 6 | 7 | import { getEmoji } from './models/emoji.ts'; 8 | import { DEFAULT_SETTINGS, SETTINGS_KEY } from './models/settings.ts'; 9 | import { 10 | isSettingsV1, 11 | LEGACY_STORAGE_KEYS, 12 | migrateStorageFromV1, 13 | } from './models/storage_legacy.ts'; 14 | import Autoselector from './utilities/favicon_autoselector.ts'; 15 | import selectFavicon from './utilities/favicon_selector.ts'; 16 | 17 | let settings: Settings; 18 | let autoselect: Autoselector | undefined; 19 | 20 | syncSettings(); 21 | browserAPI.storage.onChanged.addListener(syncSettings); 22 | 23 | // Send tab a favicon 24 | browserAPI.tabs.onUpdated.addListener( 25 | async (tabId: number, _: Chrome.TabChangeInfo, { url }: Chrome.Tab) => { 26 | if (!tabId || !url) return; 27 | if (!settings) await syncSettings(); 28 | 29 | const [favicon, shouldOverride] = selectFavicon(url, settings, autoselect); 30 | 31 | if (!favicon?.emojiId) return; 32 | 33 | const emoji = await getEmoji(favicon.emojiId); 34 | if (!emoji) return; 35 | 36 | try { 37 | await browserAPI.tabs.sendMessage(tabId, { 38 | emoji, 39 | shouldOverride, 40 | enableOverrideIndicator: settings.features.enableOverrideIndicator, 41 | }); 42 | } catch (e) { 43 | console.log(e); 44 | } 45 | }, 46 | ); 47 | 48 | type StoredSettings = { 49 | settings: Settings; 50 | }; 51 | 52 | async function syncSettings() { 53 | const storage: StoredSettings | SettingsV1 = await browserAPI.storage.sync 54 | .get([SETTINGS_KEY, ...LEGACY_STORAGE_KEYS]) as StoredSettings | SettingsV1; 55 | 56 | if (isSettingsV1(storage)) { 57 | console.info('Version < 2 versions found', storage); 58 | settings = migrateStorageFromV1(storage); 59 | console.info('Migrating to', settings); 60 | await browserAPI.storage.sync.remove(LEGACY_STORAGE_KEYS); 61 | await browserAPI.storage.sync.set({ settings }); 62 | } else if ( 63 | !storage?.settings || 64 | Object.keys(storage.settings).length !== 65 | Object.keys(DEFAULT_SETTINGS).length 66 | ) { 67 | settings = DEFAULT_SETTINGS; 68 | } else { 69 | settings = storage.settings; 70 | } 71 | 72 | const { features, autoselectorVersion } = settings; 73 | const includeFlags = features.enableAutoselectorIncludeCountryFlags; 74 | 75 | if (!features.enableFaviconAutofill) { 76 | autoselect = undefined; 77 | } else if ( 78 | !autoselect || autoselectorVersion !== autoselect.version || 79 | includeFlags !== autoselect.includeFlags 80 | ) { 81 | autoselect = new Autoselector(autoselectorVersion, { includeFlags }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /source/components/emoji_selector/components/groups.tsx: -------------------------------------------------------------------------------- 1 | /* @jsx h */ 2 | import type { OnSelected, SetRoute } from '../types.ts'; 3 | import type { Emoji, EmojiGroup, EmojiGroups } from '../../../models/emoji.ts'; 4 | 5 | import { Fragment, h } from 'preact'; 6 | import { useCallback, useMemo } from 'preact/hooks'; 7 | 8 | import Only from '../../only.tsx'; 9 | import EmojiButton from './emoji_button.tsx'; 10 | import { ROUTE } from '../types.ts'; 11 | 12 | export default function Groups( 13 | { groupFilter, filter, onSelected, emojiGroups, setRoute }: { 14 | groupFilter: string; 15 | filter: string; 16 | onSelected: OnSelected; 17 | emojiGroups: EmojiGroups; 18 | setRoute: SetRoute; 19 | }, 20 | ) { 21 | const emojiFilter = useCallback((emoji: Emoji) => { 22 | const { tags, aliases, group, subgroup } = emoji; 23 | return new RegExp(filter) 24 | .test([...tags, ...aliases, group, subgroup].join(' ')); 25 | }, [filter]); 26 | 27 | const filteredEmojiGroups = useMemo(() => { 28 | return Object.keys(emojiGroups) 29 | .filter((name: string) => !groupFilter || groupFilter === name) 30 | .map((name: string): EmojiGroup => ({ 31 | ...emojiGroups[name], 32 | emojis: filter 33 | ? emojiGroups[name].emojis.filter(emojiFilter) 34 | : emojiGroups[name].emojis, 35 | })); 36 | }, [emojiGroups, groupFilter, filter]); 37 | 38 | const emojiGroupComponents = filteredEmojiGroups 39 | .filter((emojiGroup: EmojiGroup) => { 40 | if (emojiGroup?.emojis?.length) return true; 41 | if (emojiGroup.name === groupFilter) return true; 42 | return false; 43 | }) 44 | .map((emojiGroup: EmojiGroup) => ( 45 |
46 |

{emojiGroup.name}

47 | {emojiGroup.emojis.map((emoji) => ( 48 | onSelected(emoji)} 51 | emoji={emoji} 52 | /> 53 | ))} 54 |
55 | )); 56 | 57 | // If groupFilter, still show group, just for the title 58 | const shouldNotShowGroups = !groupFilter && !emojiGroupComponents.length; 59 | 60 | return ( 61 | 62 | {shouldNotShowGroups ? '' : emojiGroupComponents} 63 | {!emojiGroupComponents.length ? 'No Matches' : ''} 64 | 65 | 72 | 79 | 80 | 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Favioli 🤯 2 | 3 |

4 | 5 | Download for Chrome 6 | 7 | 8 | Download for Firefox 9 | 10 |

11 | 12 | Favioli is a tool for overriding Favicons for websites. 13 | 14 | ## Quick Setup (For Building Release) 15 | 16 | Release 2.0.2 was build with: 17 | 18 | - macOS Monterey 12.6.3 19 | - [Deno](https://deno.land/) 1.30.0 20 | - Bext v1.0.0 21 | 22 | Directions for Shell on OSX 23 | (PowerShell on Windows is same, except for Deno installation step) 24 | 25 | ```sh 26 | # install Deno @ v1.30.0 27 | curl -fsSL https://deno.land/install.sh | sh -s v1.30.0 28 | 29 | # Install bext packager @ v1.0.0 30 | deno run -A https://deno.land/x/bext@v1.0.0/main.ts 31 | 32 | # Change directory to this repo 33 | cd favioli 34 | 35 | # Run bext packager @ v1.0.0 36 | deno run -A https://deno.land/x/bext@v1.0.0/main.ts 37 | 38 | # Unpacked extension output should be available in `dist/{browser}` 39 | open dist/firefox 40 | open dist/chrome 41 | 42 | # You should be able to load your unpacked extension using a browser. 43 | ``` 44 | 45 | ## Development Setup 46 | 47 | [Deno](https://deno.land/) is a javascript/typescript runtime (think spiritual successor to node.js) 48 | 49 | [bext](https://github.com/bpevs/bext) is a set of browser extension build tools, types, and utilities for deno. It was created for Favioli. 50 | 51 | To load Favioli into a browser, point to each browser's respective dist directory. 52 | [Google](https://developer.chrome.com/extensions) and 53 | [Mozilla](https://developer.mozilla.org/docs/Mozilla/Add-ons) each have 54 | resources about developing for their respective platforms. 55 | 56 | If you want to install latest bext with more limited permissions: 57 | 58 | ```sh 59 | deno install --name=bext --allow-read --allow-write --allow-run --allow-env -f https://deno.land/x/bext/main.ts 60 | ``` 61 | 62 | | Commands | What they Do | 63 | | ----------------------- | ----------------------------------- | 64 | | `bext` | bundles extension and watch code | 65 | | `bext chrome` | bundles extension only for chrome | 66 | | `bext firefox` | bundles extension only for firefox | 67 | | `deno task test:all` | run code formatter, then unit tests | 68 | | `deno task test:update` | run code formatter, then unit tests | 69 | 70 | ## Inspiration 71 | 72 | - [Emoji-Favicon-Toolkit](https://github.com/eligrey/emoji-favicon-toolkit) by 73 | [OFTN Inc.](https://oftn.org) and [Eli Grey](https://eligrey.com) 74 | - [eft-input-modified-indicator.js](https://gist.github.com/eligrey/4df9453c3bc20acd38728ccba7bb7160) 75 | by [Eli Grey](https://eligrey.com) 76 | -------------------------------------------------------------------------------- /source/utilities/image_helpers.ts: -------------------------------------------------------------------------------- 1 | import { isFirefox } from 'browser'; 2 | 3 | export const ICON_SIZE = 256; // Larger will causes problems in Google Chrome 4 | export const STORED_IMAGE_SIZE = 40; // Larger exceeds QUOTA_BYTES_PER_ITEM 5 | 6 | const VERTICAL_OFFSET = isFirefox() ? 20 : 0; // ff is off-center 7 | 8 | const canvas = document.createElement('canvas'); 9 | const ctx = canvas.getContext('2d'); 10 | 11 | canvas.width = canvas.height = ICON_SIZE; 12 | 13 | if (ctx) { 14 | ctx.font = `normal normal normal ${ICON_SIZE}px/${ICON_SIZE}px sans-serif`; 15 | ctx.textAlign = 'center'; 16 | ctx.textBaseline = 'middle'; 17 | } 18 | 19 | /** 20 | * Create a favicon image using the system's native emoji character. 21 | * We render native emojis (rather than emoji spritesheets) for two reasons. 22 | * 1. Avoid needing to package large image spritesheets with Favioli. 23 | * 2. Avoid any legal complications from packaging emoji images. 24 | * 25 | * This function is expected to be used primarily for favicon autofill. 26 | * 27 | * Heavily inspired by emoji-favicon-toolkit 28 | * @source https://github.com/eligrey/emoji-favicon-toolkit/blob/master/src/emoji-favicon-toolkit.ts 29 | */ 30 | export function createFaviconURLFromChar( 31 | char: string, 32 | showIndicator = false, 33 | ): string { 34 | if (!char || !ctx) return ''; 35 | 36 | // Calculate sizing 37 | const { width } = ctx.measureText(char); 38 | // (ICON_SIZE + (ICON_SIZE / PIXEL_GRID)) / 2 39 | const center = (ICON_SIZE + (ICON_SIZE / 16)) / 2; 40 | const scale = Math.min(ICON_SIZE / width, 1); 41 | const centerScaled = center / scale; 42 | 43 | // Draw emoji 44 | ctx.clearRect(0, 0, ICON_SIZE, ICON_SIZE); 45 | ctx.save(); 46 | ctx.scale(scale, scale); 47 | ctx.fillText(char, centerScaled, centerScaled + VERTICAL_OFFSET); 48 | 49 | if (showIndicator) { 50 | const FLAG_SIZE = 30; 51 | ctx.beginPath(); 52 | ctx.arc( 53 | ICON_SIZE - FLAG_SIZE, 54 | ICON_SIZE - FLAG_SIZE, 55 | FLAG_SIZE, 56 | 0, 57 | 2 * Math.PI, 58 | ); 59 | ctx.fillStyle = 'red'; 60 | ctx.fill(); 61 | } 62 | 63 | ctx.restore(); 64 | return canvas.toDataURL('image/png'); 65 | } 66 | 67 | /** 68 | * Create a favicon image using a png, for custom images. Primarily, this means: 69 | * 1. Build into dataURL string to store in browser.storage.sync 70 | * 2. Resize image, so it will fit in browser.storage.sync (<4kb) 71 | * @reference QUOTA_BYPES_PER_ITEM 72 | */ 73 | export function createFaviconURLFromImage(url: string): Promise { 74 | const image = new Image(); 75 | image.src = url; 76 | return new Promise((resolve) => { 77 | image.onload = function () { 78 | const canvas = document.createElement('canvas'); 79 | canvas.width = STORED_IMAGE_SIZE; 80 | canvas.height = STORED_IMAGE_SIZE; 81 | const ctx = canvas.getContext('2d'); 82 | if (!ctx) return ''; 83 | ctx.drawImage(image, 0, 0, STORED_IMAGE_SIZE, STORED_IMAGE_SIZE); 84 | resolve(canvas.toDataURL('image/png')); 85 | }; 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /source/utilities/append_favicon_link.ts: -------------------------------------------------------------------------------- 1 | import type { Emoji } from '../models/emoji.ts'; 2 | import { isFirefox } from 'browser'; 3 | import { createFaviconURLFromChar, ICON_SIZE } from './image_helpers.ts'; 4 | 5 | const head = document.getElementsByTagName('head')[0]; 6 | let appendedFavicon: HTMLElement | null = null; 7 | 8 | // Cache `true`, to give site every opportunity 9 | let hasFavicon = false; 10 | 11 | interface Options { 12 | shouldOverride?: boolean; 13 | enableOverrideIndicator?: boolean; 14 | } 15 | 16 | // Given an emoji string, append it to the document head 17 | export default async function appendFaviconLink( 18 | emoji: Emoji, 19 | options?: Options | void, 20 | ) { 21 | const { shouldOverride = false, enableOverrideIndicator = false } = options || 22 | {}; 23 | const faviconURL = emoji.imageURL 24 | ? emoji.imageURL 25 | : createFaviconURLFromChar(emoji.emoji || '', enableOverrideIndicator); 26 | 27 | if (!faviconURL) return; 28 | if (shouldOverride) removeAllFaviconLinks(); 29 | 30 | // Already appended favicon; just update 31 | if (appendedFavicon) return appendedFavicon.setAttribute('href', faviconURL); 32 | if (shouldOverride || !(await doesSiteHaveFavicon())) { 33 | appendedFavicon = head.appendChild( 34 | createLink(faviconURL, ICON_SIZE, 'image/png'), 35 | ); 36 | } 37 | } 38 | 39 | // Return an array of link tags that have an icon rel 40 | function getAllIconLinks(): HTMLLinkElement[] { 41 | return Array.prototype.slice 42 | .call(document.getElementsByTagName('link')) 43 | .filter((link: HTMLLinkElement) => { 44 | return new RegExp(/icon/i).test(link.rel); 45 | }); 46 | } 47 | 48 | async function doesSiteHaveFavicon(): Promise { 49 | if (hasFavicon) return hasFavicon; 50 | const iconLinks = getAllIconLinks(); 51 | 52 | // Browsers fallback to favicon.ico 53 | if (!isFirefox()) iconLinks.push(createLink('/favicon.ico')); 54 | 55 | const iconLinkURLs = iconLinks.map(({ href }: HTMLLinkElement) => href); 56 | const iconLinkFound = Array.from(new Set(iconLinkURLs)) 57 | .map(async (href: string): Promise => { 58 | // For Firefox, don't test urls. They all fail for me 59 | // (Although it might be a setting on my browser. Maybe double-check) 60 | if (isFirefox()) return true; 61 | const result = await fetch(href); 62 | if (!result || result.status >= 400) throw new Error('not found'); 63 | return true; 64 | }); 65 | 66 | try { 67 | hasFavicon = hasFavicon || Boolean(await Promise.any(iconLinkFound)); 68 | } catch { 69 | /* Do Nothing*/ 70 | } 71 | return hasFavicon; 72 | } 73 | 74 | // Removes all icon link tags 75 | function removeAllFaviconLinks(): void { 76 | getAllIconLinks().forEach((link) => link.remove()); 77 | appendedFavicon = null; 78 | } 79 | 80 | // Given a url, create a favicon link 81 | function createLink( 82 | href: string, 83 | size?: number, 84 | type?: string, 85 | ): HTMLLinkElement { 86 | const link = document.createElement('link'); 87 | link.rel = 'icon'; 88 | link.href = href; 89 | if (type) link.type = type; 90 | if (size) link.setAttribute('sizes', `${size}x${size}`); 91 | return link; 92 | } 93 | -------------------------------------------------------------------------------- /source/components/emoji_selector/components/custom_upload.tsx: -------------------------------------------------------------------------------- 1 | /* @jsx h */ 2 | import type { BrowserStorage } from '../../../hooks/use_browser_storage.ts'; 3 | import type { Settings } from '../../../models/settings.ts'; 4 | import type { SetRoute } from '../types.ts'; 5 | 6 | import { h } from 'preact'; 7 | import { useCallback, useContext, useState } from 'preact/hooks'; 8 | 9 | import { createEmoji, emoji, saveEmoji } from '../../../models/emoji.ts'; 10 | import { SettingsContext } from '../../../models/settings.ts'; 11 | import { createFaviconURLFromImage } from '../../../utilities/image_helpers.ts'; 12 | import Only from '../../only.tsx'; 13 | import { ROUTE } from '../types.ts'; 14 | 15 | export default function CustomUpload({ setRoute }: { setRoute: SetRoute }) { 16 | const settings = useContext>(SettingsContext); 17 | const { cache, saveToStorageBypassCache } = settings; 18 | const { customEmojiIds } = cache; 19 | 20 | const [imageURL, setImageURL] = useState(''); 21 | const [description, setDescription] = useState(''); 22 | 23 | const updateImageURL = useCallback(async (event: Event) => { 24 | if (event.target instanceof HTMLInputElement) { 25 | const file = event.target?.files?.[0]; 26 | const name = (file?.name || '').match(/(.*)\..*$/)?.[1] || ''; 27 | if (name && !description) setDescription(name); // Autofill desc if none 28 | if (file instanceof Blob) { 29 | setImageURL(await createFaviconURLFromImage(URL.createObjectURL(file))); 30 | } 31 | } 32 | }, [description, setImageURL, setDescription]); 33 | 34 | const updateDescription = useCallback(({ target }: Event) => { 35 | if (target instanceof HTMLInputElement) setDescription(target.value || ''); 36 | }, [setDescription]); 37 | 38 | const saveCustomEmoji = useCallback(async () => { 39 | try { 40 | await saveEmoji(createEmoji(description, imageURL)); 41 | const deduped = Array.from(new Set(customEmojiIds.concat(description))); 42 | await saveToStorageBypassCache({ ...cache, customEmojiIds: deduped }); 43 | setRoute(ROUTE.DEFAULT); 44 | } catch (e) { 45 | confirm(e); 46 | } 47 | }, [cache, description, imageURL]); 48 | 49 | const goBack = useCallback(() => setRoute(ROUTE.DEFAULT), [setRoute]); 50 | 51 | return ( 52 |
53 |
Upload Custom Favicon
54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 69 |
70 | 71 | 72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /source/components/list_input.tsx: -------------------------------------------------------------------------------- 1 | /* @jsx h */ 2 | import type { Emoji } from '../models/emoji.ts'; 3 | import type { Favicon } from '../models/favicon.ts'; 4 | 5 | import { h } from 'preact'; 6 | import { useCallback, useMemo } from 'preact/hooks'; 7 | 8 | import { DEFAULT_EMOJI } from '../models/emoji.ts'; 9 | import { isRegexString } from '../utilities/regex_utils.ts'; 10 | import EmojiSelector from './emoji_selector/mod.tsx'; 11 | import Only from './only.tsx'; 12 | 13 | const IGNORE = 'IGNORE'; 14 | const FAVICON = 'FAVICON'; 15 | export type ListType = typeof IGNORE | typeof FAVICON; 16 | export const LIST_TYPE: { [name: string]: ListType } = { IGNORE, FAVICON }; 17 | 18 | interface ListInputProps { 19 | autoFocus?: boolean; 20 | canDelete?: boolean; 21 | type: ListType; 22 | index: number; 23 | value?: Favicon; 24 | placeholder?: string; 25 | deleteItem?: (index: number) => void; 26 | addItem?: (listitem: Favicon) => void; 27 | updateItem?: (index: number, listItem: Favicon) => void; 28 | } 29 | 30 | const choices = [ 31 | 'favioli.com', 32 | 'https://favioli.com', 33 | 'favioli', 34 | '/favioli.com$/i', 35 | '/http:\\/\\//', 36 | ]; 37 | 38 | export default function ListInput({ 39 | autoFocus = false, 40 | addItem, 41 | deleteItem, 42 | updateItem = () => {}, 43 | type, 44 | value, 45 | index, 46 | }: ListInputProps) { 47 | const onChangeMatcher = useCallback((e: Event) => { 48 | if (e.target instanceof HTMLInputElement) { 49 | const next = { 50 | emojiId: value?.emojiId || DEFAULT_EMOJI.description, 51 | matcher: e.target.value, 52 | }; 53 | addItem ? addItem(next) : updateItem(index, next); 54 | } 55 | }, [index, value, updateItem, addItem]); 56 | 57 | const onChangeEmoji = useCallback((selectedEmoji: Emoji) => { 58 | const next = { 59 | emojiId: selectedEmoji.description, 60 | matcher: value?.matcher || '', 61 | }; 62 | addItem ? addItem(next) : updateItem(index, next); 63 | }, [index, value, updateItem, addItem]); 64 | 65 | const onClickDelete = useCallback(() => { 66 | if (deleteItem) deleteItem(index); 67 | }, [index, deleteItem]); 68 | 69 | const color = isRegexString(value?.matcher || '') ? 'green' : 'black'; 70 | const placeholder = useMemo(() => { 71 | return choices[Math.floor(Math.random() * choices.length)]; 72 | }, []); 73 | 74 | return ( 75 |
76 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /source/content_script.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import type Chrome from 'browser/types/chrome.ts'; 3 | import type { Emoji } from './models/emoji.ts'; 4 | import type { Favicon } from './models/favicon.ts'; 5 | 6 | type StorageAreaChangedEvent = Chrome.StorageAreaChangedEvent; 7 | 8 | /** 9 | * Check siteList and ignoreList from chrome storage 10 | * Use those to determine if we should override favicon 11 | * Override favicon if applicable 12 | */ 13 | import browserAPI from 'browser'; 14 | import appendFaviconLink from './utilities/append_favicon_link.ts'; 15 | import { parseRegExp } from './utilities/regex_utils.ts'; 16 | 17 | browserAPI.runtime.onMessage.addListener( 18 | ({ emoji, shouldOverride, enableOverrideIndicator }: { 19 | emoji: Emoji; 20 | shouldOverride: boolean; 21 | enableOverrideIndicator: boolean; 22 | }) => { 23 | if (emoji) { 24 | appendFaviconLink(emoji, { shouldOverride, enableOverrideIndicator }); 25 | } 26 | }, 27 | ); 28 | 29 | /** 30 | * Reload the webpage if new Favioli settings may have updated the favicon 31 | * 1. Did this url get added/removed from the sitelist? 32 | * 2. Did this url get added/removed from the ignorelist? 33 | * 3. Did we start/stop using ignoreList? 34 | * 4. Did the emoji set change? 35 | */ 36 | browserAPI.storage.onChanged.addListener( 37 | (changes: StorageAreaChangedEvent): void => { 38 | if (!changes?.settings) return; 39 | const { newValue, oldValue } = changes.settings; 40 | 41 | if (newValue.autoselectorVersion !== oldValue.autoselectorVersion) { 42 | location.reload(); 43 | return; 44 | } 45 | 46 | if (!shallowCompare(newValue.features, oldValue.features)) { 47 | location.reload(); 48 | return; 49 | } 50 | 51 | const newSiteList = newValue.siteList.filter(includesCurrUrl); 52 | const oldSiteList = oldValue.siteList.filter(includesCurrUrl); 53 | 54 | if (!shallowCompare(newSiteList, oldSiteList)) { 55 | location.reload(); 56 | return; 57 | } 58 | 59 | const newIgnoreList = newValue.ignoreList.filter(includesCurrUrl); 60 | const oldIgnoreList = oldValue.ignoreList.filter(includesCurrUrl); 61 | if (!shallowCompare(newIgnoreList, oldIgnoreList)) { 62 | location.reload(); 63 | return; 64 | } 65 | }, 66 | ); 67 | 68 | // Return true if objects are equivalent. 69 | function shallowCompare(obj1: unknown, obj2: unknown) { 70 | if (typeof obj1 !== typeof obj2) return false; 71 | else if ( 72 | (obj1 == null || obj2 == null) || 73 | (typeof obj1 !== 'object' || typeof obj2 !== 'object') 74 | ) { 75 | return obj1 == obj2; 76 | } else if (Object.keys(obj1).length !== Object.keys(obj2).length) { 77 | return false; 78 | } else { 79 | return Object.keys(obj1) 80 | .every((obj1Key: string) => 81 | Object.hasOwn(obj1, obj1Key) && 82 | Object.hasOwn(obj2, obj1Key) && 83 | (obj1 as Record)[obj1Key] === 84 | (obj2 as Record)[obj1Key] 85 | ); 86 | } 87 | } 88 | 89 | function includesCurrUrl({ matcher }: Favicon) { 90 | const regex = parseRegExp(matcher); 91 | if (regex) return regex.test(location.href); 92 | return location.href.indexOf(matcher) != -1; 93 | } 94 | -------------------------------------------------------------------------------- /source/pages/settings_page.tsx: -------------------------------------------------------------------------------- 1 | /* @jsx h */ 2 | import type { BrowserStorage } from '../hooks/use_browser_storage.ts'; 3 | import type { Settings } from '../models/settings.ts'; 4 | 5 | import { h } from 'preact'; 6 | import { useCallback, useContext } from 'preact/hooks'; 7 | 8 | import Checkbox, { Target } from '../components/checkbox.tsx'; 9 | import Only from '../components/only.tsx'; 10 | import { SettingsContext } from '../models/settings.ts'; 11 | import { AUTOSELECTOR_VERSION } from '../utilities/favicon_autoselector.ts'; 12 | import { t } from '../utilities/i18n.ts'; 13 | 14 | export default function SettingsPage({ save }: { save?: (e: Event) => void }) { 15 | const storage = useContext>(SettingsContext); 16 | const { cache, setCache } = storage; 17 | const { autoselectorVersion, features } = cache; 18 | const { 19 | enableAutoselectorIncludeCountryFlags, 20 | enableFaviconAutofill, 21 | enableSiteIgnore, 22 | enableOverrideAll, 23 | enableOverrideIndicator, 24 | } = features; 25 | 26 | const setFeature = useCallback((nextFeature: Target) => { 27 | if (storage) setCache({ features: { ...features, ...nextFeature } }); 28 | }, [features]); 29 | 30 | const setAutoselectorVersion = useCallback((e: Event) => { 31 | const autoselectorVersion = (e.target as HTMLInputElement).value; 32 | if (storage) setCache({ autoselectorVersion }); 33 | }, [storage]); 34 | 35 | return ( 36 |
37 |

Settings

38 | 44 | 50 | 51 |
52 | 58 | 64 |
65 | 66 | 75 |
76 |
77 |
78 | 84 | 82 | 87 |
{status}
88 | 89 | ); 90 | }; 91 | 92 | const mountPoint = document.getElementById('mount'); 93 | if (mountPoint) render(, mountPoint); 94 | -------------------------------------------------------------------------------- /source/utilities/__tests__/favicon_selector.test.ts: -------------------------------------------------------------------------------- 1 | import type { Settings } from '../../models/settings.ts'; 2 | 3 | import { assertStrictEquals } from 'test/asserts'; 4 | import { describe, it } from 'test/bdd'; 5 | 6 | import { DEFAULT_SETTINGS } from '../../models/settings.ts'; 7 | import Autoselector, { AUTOSELECTOR_VERSION } from '../favicon_autoselector.ts'; 8 | import selectFavicon from '../favicon_selector.ts'; 9 | 10 | const url = 'https://fAvIolI.cOm'; 11 | const emojiId = 'tractor'; 12 | 13 | describe('matchers', () => { 14 | const settings: Settings = { ...DEFAULT_SETTINGS }; 15 | 16 | it('Should match strings, ignoring case', () => { 17 | settings.siteList = [{ matcher: 'fAvIolI.cOm', emojiId }]; 18 | const [favicon, shouldOverride] = selectFavicon(url, settings); 19 | assertStrictEquals(shouldOverride, true); 20 | assertStrictEquals(favicon?.emojiId, 'tractor'); 21 | assertStrictEquals(favicon?.matcher, 'fAvIolI.cOm'); 22 | }); 23 | 24 | it('Should match regex', () => { 25 | settings.siteList = [{ matcher: '/favi/i', emojiId }]; 26 | const matchedFavicon = selectFavicon(url, settings)[0]; 27 | 28 | settings.siteList = [{ matcher: '/favi/', emojiId }]; 29 | const noMatchFavicon = selectFavicon(url, settings)[0]; 30 | 31 | assertStrictEquals(matchedFavicon?.emojiId, 'tractor'); 32 | assertStrictEquals(matchedFavicon?.matcher, '/favi/i'); 33 | assertStrictEquals(noMatchFavicon, undefined); 34 | }); 35 | 36 | it('Should return [undefined, false] if no matches', () => { 37 | settings.siteList = []; 38 | const [favicon, shouldOverride] = selectFavicon(url, settings); 39 | 40 | assertStrictEquals(favicon, undefined); 41 | assertStrictEquals(shouldOverride, false); 42 | }); 43 | 44 | it('Should return [undefined, false] if site is ignored', () => { 45 | const favicon = { matcher: 'fAvIolI.cOm', emojiId }; 46 | settings.siteList = [favicon]; 47 | settings.ignoreList = [favicon]; 48 | settings.features.enableSiteIgnore = false; 49 | const matchedFavicon = selectFavicon(url, settings)[0]; 50 | 51 | settings.features.enableSiteIgnore = true; 52 | const noMatchFavicon = selectFavicon(url, settings)[0]; 53 | 54 | assertStrictEquals(matchedFavicon, favicon); 55 | assertStrictEquals(noMatchFavicon, undefined); 56 | }); 57 | }); 58 | 59 | describe('autoselect', () => { 60 | const autoselect = new Autoselector(AUTOSELECTOR_VERSION.UNICODE_11); 61 | const settings: Settings = { ...DEFAULT_SETTINGS }; 62 | 63 | it('Should not autoselect if no autoselect enabled', () => { 64 | settings.features.enableFaviconAutofill = false; 65 | const [favicon, shouldOverride] = selectFavicon(url, settings, autoselect); 66 | 67 | assertStrictEquals(favicon, undefined); 68 | assertStrictEquals(shouldOverride, false); 69 | }); 70 | 71 | it('Should autoselect if autoselect is enabled', () => { 72 | settings.features.enableFaviconAutofill = true; 73 | const [favicon, shouldOverride] = selectFavicon(url, settings, autoselect); 74 | 75 | assertStrictEquals(favicon?.emojiId, 'mouth'); 76 | assertStrictEquals(favicon?.matcher, 'https://fAvIolI.cOm'); 77 | assertStrictEquals(shouldOverride, false); 78 | }); 79 | 80 | it('Should override if overrideAll is enabled', () => { 81 | settings.features.enableFaviconAutofill = true; 82 | settings.features.enableOverrideAll = true; 83 | const [favicon, shouldOverride] = selectFavicon(url, settings, autoselect); 84 | 85 | assertStrictEquals(favicon?.emojiId, 'mouth'); 86 | assertStrictEquals(favicon?.matcher, 'https://fAvIolI.cOm'); 87 | assertStrictEquals(shouldOverride, true); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /source/utilities/favicon_autoselector.ts: -------------------------------------------------------------------------------- 1 | import type { Favicon } from '../models/favicon.ts'; 2 | import type { Emoji } from '../models/emoji.ts'; 3 | 4 | import LEGACY_EMOJI_SET from '../config/legacy_autoselect_set.ts'; 5 | import { createFavicon } from '../models/favicon.ts'; 6 | import { emoji, emojis } from '../models/emoji.ts'; 7 | 8 | export const AUTOSELECTOR_VERSION = Object.freeze({ 9 | UNICODE_12: 'UNICODE_12', 10 | UNICODE_11: 'UNICODE_11', 11 | UNICODE_10: 'UNICODE_10', 12 | UNICODE_09: 'UNICODE_09', 13 | UNICODE_08: 'UNICODE_08', 14 | UNICODE_07: 'UNICODE_07', 15 | UNICODE_06: 'UNICODE_06', 16 | FAVIOLI_LEGACY: 'FAVIOLI_LEGACY', 17 | }); 18 | 19 | export type AutoselectorVersion = string; 20 | 21 | const { UNICODE_12, FAVIOLI_LEGACY } = AUTOSELECTOR_VERSION; 22 | 23 | /** 24 | * For selecting random favicon from a set. Currently, only select from 25 | * Emoji set, so it remains more static (read: CUSTOM EMOJIS ARE NOT AUTOGEN'd) 26 | * 27 | * @property version 28 | * Generally this means unicode version. Used to sandbox to a specific set of 29 | * favicons, so that they remain the same. Special version is provided for 30 | * legacy Favioli users to use the original set. 31 | * 32 | * @property includeFlags 33 | * Flags are like, half the emojis. So ignore them by default. 34 | * 35 | * @property favicons 36 | * A set of favicons we can autoselect from. 37 | */ 38 | export default class Autoselector { 39 | constructor(version: string, options: { includeFlags?: boolean } = {}) { 40 | this.version = version; 41 | this.includeFlags = options?.includeFlags || false; 42 | } 43 | 44 | cache: { 45 | [versionId: string]: Emoji[]; 46 | } = {}; 47 | 48 | version: AutoselectorVersion = UNICODE_12; 49 | includeFlags = false; 50 | 51 | get favicons() { 52 | const cacheId = `${this.version}_${this.includeFlags}`; 53 | if (this.cache[cacheId]) return this.cache[cacheId]; 54 | 55 | let next; 56 | 57 | if (this.version === FAVIOLI_LEGACY) { 58 | next = LEGACY_EMOJI_SET.map((str) => emoji.infoByCode(str)); 59 | } 60 | 61 | if (/^UNICODE_\d\d$/.test(this.version)) { 62 | next = getFilteredFavicons( 63 | Number(this.version.split('_')[1]), 64 | this.includeFlags, 65 | ); 66 | } 67 | 68 | if (next && Array.isArray(next)) { 69 | this.cache[cacheId] = next.filter(Boolean) as Emoji[]; 70 | return this.cache[cacheId]; 71 | } 72 | 73 | throw new Error('Invalid Autoselector Version'); 74 | } 75 | 76 | selectFavicon(url: string): Favicon { 77 | let hostname = ''; 78 | try { 79 | hostname = (new URL(url)).host; 80 | } catch { 81 | // Use URL 82 | } 83 | 84 | const index = Math.abs(sdbm(hostname || url)) % this.favicons.length; 85 | const emoji = this.favicons[index] || this.favicons[0]; 86 | return createFavicon(url, emoji); 87 | } 88 | } 89 | 90 | function getFilteredFavicons(unicodeVersion: number, includeFlags: boolean) { 91 | if (!(unicodeVersion > 0)) { 92 | throw new Error(`invalid unicode version ${unicodeVersion}`); 93 | } 94 | return emojis 95 | .filter((emoji: Emoji) => { 96 | if (!includeFlags && emoji.subgroup.includes('flag')) return false; 97 | return emoji.unicodeVersion <= unicodeVersion; 98 | }); 99 | } 100 | 101 | /** 102 | * Non-cryptographic hashing to get the same index for different keys 103 | * @source http://www.cse.yorku.ca/~oz/hash.html 104 | * @source https://github.com/sindresorhus/sdbm 105 | */ 106 | function sdbm(key: string): number { 107 | return String(key).split('').reduce((hash, _, i) => { 108 | const charCode = key.charCodeAt(i); 109 | return charCode + (hash << 6) + (hash << 16) - hash; 110 | }, 0) >>> 0; 111 | } 112 | -------------------------------------------------------------------------------- /source/components/emoji_selector/components/popup.tsx: -------------------------------------------------------------------------------- 1 | /* @jsx h */ 2 | import type { Emoji, EmojiGroup, EmojiMap } from '../../../models/emoji.ts'; 3 | import type { OnSelected, Route, SetRoute, SetSwitch } from '../types.ts'; 4 | import type { Ref } from 'preact'; 5 | 6 | import { h } from 'preact'; 7 | import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'; 8 | 9 | import { emoji, emojiGroups, emojiGroupsArray } from '../../../models/emoji.ts'; 10 | import Groups from './groups.tsx'; 11 | import CustomUpload from './custom_upload.tsx'; 12 | import CustomDelete from './custom_delete.tsx'; 13 | import { ROUTE } from '../types.ts'; 14 | 15 | const POPUP_WIDTH = 350; 16 | const BUTTON_HEIGHT = 32; 17 | 18 | export default function Popup( 19 | { 20 | customEmojis, 21 | isOpen, 22 | onSelected, 23 | popupRef, 24 | route, 25 | setIsOpen, 26 | setRoute, 27 | }: { 28 | customEmojis: EmojiMap; 29 | isOpen: boolean; 30 | onSelected: OnSelected; 31 | // deno-lint-ignore no-explicit-any 32 | popupRef: Ref; 33 | route: Route; 34 | setIsOpen: SetSwitch; 35 | setRoute: SetRoute; 36 | }, 37 | ) { 38 | const [groupFilter, setGroupFilter] = useState(''); 39 | const [filter, setFilter] = useFilterState(''); 40 | 41 | const allEmojis = useMemo(() => { 42 | emojiGroups['Custom Emojis'].emojis = Object.keys(customEmojis) 43 | .map((id) => customEmojis[id]); 44 | return { ...emojiGroups }; 45 | }, [customEmojis]); 46 | 47 | if (!isOpen) return null; 48 | 49 | if (route === ROUTE.CREATE_CUSTOM) { 50 | return ( 51 |
52 | 53 |
54 | ); 55 | } 56 | 57 | if (route === ROUTE.DELETE_CUSTOM) { 58 | return ( 59 |
60 | 64 |
65 | ); 66 | } 67 | 68 | return ( 69 |
70 |
71 |
72 | {emojiGroupsArray 73 | .map((emojiGroup: EmojiGroup) => { 74 | const { name, representativeEmoji } = emojiGroup; 75 | const isSelected = name === groupFilter; 76 | const isSelectedClass = isSelected ? 'selected' : ''; 77 | return ( 78 |
setGroupFilter(isSelected ? '' : name)} 81 | > 82 | {representativeEmoji} 83 |
84 | ); 85 | })} 86 |
87 | 96 |
97 | 104 |
105 | ); 106 | } 107 | 108 | function useFilterState(initialValue: string): [string, (e: Event) => void] { 109 | const [filter, setFilter] = useState(initialValue); 110 | 111 | const updateFilter = useCallback((e: Event) => { 112 | if (e.target instanceof HTMLInputElement) { 113 | setFilter(e.target?.value || ''); 114 | } 115 | }, [setFilter]); 116 | 117 | return [filter, updateFilter]; 118 | } 119 | -------------------------------------------------------------------------------- /source/hooks/use_browser_storage.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'preact/hooks'; 2 | import browserAPI from 'browser'; 3 | 4 | const { storage, runtime } = browserAPI; 5 | 6 | export interface BrowserStorage { 7 | error?: string; 8 | cache: Type; 9 | loading: boolean; 10 | setCache: (nextCache: Partial, saveImmediately?: boolean) => void; 11 | saveCacheToStorage: () => Promise; 12 | saveToStorageBypassCache: (next: Partial) => Promise; 13 | } 14 | 15 | /** 16 | * Interact with BrowserStorage as little as possible. 17 | * This method probably does NOT work well if multiple sessions open at once. 18 | * Basically: 19 | * - Fetch current saved data from Browser on init, and save locally 20 | * - get `cache` data from the locally cached copy 21 | * - `setCache` onChange data to the locally cached copy without interacting with BrowserStorage 22 | * - `saveCacheToStorage` saves that local data into browserStorage on a separate interaction 23 | */ 24 | type Keys = string | readonly string[]; 25 | export interface Changes { 26 | [key: string]: { 27 | newValue?: Type; 28 | oldValue?: Type; 29 | }; 30 | } 31 | 32 | export default function useBrowserStorage( 33 | keys: Keys, 34 | defaultState: Type, 35 | ): BrowserStorage { 36 | const [error, setError] = useState(); 37 | const [cache, setCache] = useState(defaultState); 38 | const [loading, setLoading] = useState(true); 39 | 40 | const updateState = useCallback( 41 | async function (changes: void | Changes) { 42 | const keyArray = Array.isArray(keys) ? keys : [keys]; 43 | const noChange = changes && 44 | !keyArray.some((key) => Boolean(changes[key])); 45 | if (!keyArray.length || noChange) return; 46 | 47 | const nextState: Type = Array.isArray(keys) 48 | ? await storage.sync.get(keyArray) as Type 49 | : (await storage.sync.get(keyArray))[keys as string] as Type; 50 | if (runtime?.lastError?.message) setError(runtime?.lastError?.message); 51 | if (nextState) setCache(nextState); 52 | setLoading(false); 53 | }, 54 | [keys], 55 | ); 56 | 57 | useEffect(function () { 58 | updateState(); 59 | if (!storage.onChanged.hasListener(updateState)) { 60 | storage.onChanged.addListener(updateState); 61 | } 62 | return () => { 63 | storage.onChanged.removeListener(updateState); 64 | }; 65 | }, []); 66 | 67 | const saveToStorage = useCallback( 68 | async (next: Partial | void): Promise => { 69 | if (!next) return; 70 | 71 | await storage.sync.set( 72 | Array.isArray(keys) ? next : { [keys as string]: next }, 73 | ); 74 | 75 | if (runtime?.lastError?.message) setError(runtime?.lastError?.message); 76 | }, 77 | [keys], 78 | ); 79 | 80 | const result: BrowserStorage = { 81 | error, 82 | cache, 83 | loading, 84 | 85 | setCache: useCallback( 86 | (nextCache: Partial, saveImmediately = false): void => { 87 | const nextStorage = { ...cache, ...nextCache }; 88 | setCache(nextStorage as Type); 89 | if (saveImmediately) saveToStorage(nextStorage); 90 | }, 91 | [cache, setCache], 92 | ), 93 | 94 | saveCacheToStorage: useCallback((): Promise => { 95 | return saveToStorage(cache); 96 | }, [cache]), 97 | 98 | // Save new data; don't save pre-existing cache (set storage before cache) 99 | saveToStorageBypassCache: useCallback( 100 | async (next: Partial): Promise => { 101 | await saveToStorage(next); 102 | setCache({ ...cache, ...next }); 103 | }, 104 | [cache, setCache], 105 | ), 106 | }; 107 | 108 | return result; 109 | } 110 | -------------------------------------------------------------------------------- /source/components/emoji_selector/mod.tsx: -------------------------------------------------------------------------------- 1 | /* @jsx h */ 2 | import type { Emoji, EmojiMap } from '../../models/emoji.ts'; 3 | import type { Settings } from '../../models/settings.ts'; 4 | import type { BrowserStorage } from '../../hooks/use_browser_storage.ts'; 5 | import type { OnSelected, Route } from './types.ts'; 6 | 7 | import { Fragment, h } from 'preact'; 8 | import { 9 | useCallback, 10 | useContext, 11 | useEffect, 12 | useMemo, 13 | useRef, 14 | useState, 15 | } from 'preact/hooks'; 16 | 17 | import useBrowserStorage from '../../hooks/use_browser_storage.ts'; 18 | import useFocusObserver from '../../hooks/use_focus_observer.ts'; 19 | import { 20 | areEqualEmojis, 21 | createEmoji, 22 | DEFAULT_EMOJI, 23 | emoji, 24 | getEmoji, 25 | getEmojis, 26 | getEmojiStorageId, 27 | saveEmoji, 28 | } from '../../models/emoji.ts'; 29 | import { SettingsContext } from '../../models/settings.ts'; 30 | import EmojiButton from './components/emoji_button.tsx'; 31 | import Popup from './components/popup.tsx'; 32 | import { ROUTE } from './types.ts'; 33 | 34 | const defaultState = {}; 35 | export default function EmojiSelector({ onSelected, emojiId }: { 36 | emojiId?: string; 37 | onSelected: OnSelected; 38 | }) { 39 | const buttonRef = useRef(); 40 | const settings = useContext>(SettingsContext); 41 | const { cache, setCache, saveToStorageBypassCache } = settings; 42 | 43 | const [customEmojis, setCustomEmojis] = useState({}); 44 | const [isOpen, setIsOpen] = useState(false); 45 | const [route, setRoute] = useState(ROUTE.DEFAULT); 46 | const [selectedEmoji, setSelectedEmoji] = useState(DEFAULT_EMOJI); 47 | 48 | useEffect(() => { 49 | const currIds = Object.keys(customEmojis).sort(); 50 | if (currIds.length > cache.customEmojiIds.length) { 51 | const nextEmojis: EmojiMap = {}; 52 | cache.customEmojiIds.forEach((id: string) => { 53 | nextEmojis[id] = customEmojis[id]; 54 | }); 55 | setCustomEmojis(nextEmojis); 56 | return; 57 | } 58 | 59 | const matches = cache.customEmojiIds.sort() 60 | .every((value, index) => currIds[index] === value); 61 | 62 | if (matches) return; 63 | 64 | (async function fetchEmojis() { 65 | setCustomEmojis(await getEmojis(cache.customEmojiIds)); 66 | })(); 67 | }, [cache.customEmojiIds]); 68 | 69 | // For reverting to default state when list_items are deleted 70 | useEffect(function updateStateWithNewValue() { 71 | emojiIdToEmoji(); 72 | async function emojiIdToEmoji() { 73 | if (!emojiId) return setSelectedEmoji(DEFAULT_EMOJI); 74 | const nextEmoji = await getEmoji(emojiId); 75 | if (!nextEmoji) return setSelectedEmoji(DEFAULT_EMOJI); 76 | if (!areEqualEmojis(nextEmoji, selectedEmoji)) { 77 | setSelectedEmoji(nextEmoji); 78 | } 79 | } 80 | }, [emojiId]); 81 | 82 | return ( 83 | 84 | { 88 | setIsOpen(!isOpen); 89 | setRoute(ROUTE.DEFAULT); 90 | }, [isOpen])} 91 | ref={buttonRef} 92 | /> 93 | { 98 | setIsOpen(false); 99 | setRoute(ROUTE.DEFAULT); 100 | }, [setIsOpen]), 101 | [buttonRef], 102 | )} 103 | onSelected={useCallback((emoji: Emoji) => { 104 | if (!isOpen) return; 105 | onSelected(emoji); 106 | setSelectedEmoji(emoji); 107 | setIsOpen(false); 108 | }, [isOpen, setIsOpen])} 109 | route={route} 110 | setIsOpen={setIsOpen} 111 | setRoute={setRoute} 112 | /> 113 | 114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /source/models/__tests__/emoji.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, assertEquals, assertRejects } from 'test/asserts'; 2 | import { describe, it } from 'test/bdd'; 3 | import { assertSpyCall, assertSpyCalls, stub } from 'test/mock'; 4 | import { assertSnapshot } from 'test/snapshot'; 5 | 6 | import browserAPI from 'browser'; 7 | 8 | import { 9 | areEqualEmojis, 10 | createEmoji, 11 | getEmoji, 12 | getEmojis, 13 | saveEmoji, 14 | } from '../emoji.ts'; 15 | 16 | it('createEmoji', async (t) => { 17 | await assertSnapshot(t, createEmoji('poro', 'poro://poro-url')); 18 | }); 19 | 20 | describe('getEmoji', () => { 21 | it('should get emoji from local Emoji DB', async () => { 22 | const emoji = await getEmoji('grinning squinting face'); 23 | assertEquals(emoji?.emoji, '😆'); 24 | }); 25 | 26 | it('should get emoji from storage.sync', async () => { 27 | const storageStub = stub(browserAPI.storage.sync, 'get', () => { 28 | return Promise.resolve({ 29 | 'Custom Emoji: poro': createEmoji('poro', 'poro://poro-url'), 30 | }); 31 | }); 32 | const emoji = await getEmoji('poro'); 33 | assertEquals(emoji?.imageURL, 'poro://poro-url'); 34 | assertEquals(emoji?.description, 'poro'); 35 | assertSpyCalls(storageStub, 1); 36 | assertSpyCall(storageStub, 0, { 37 | args: [['Custom Emoji: poro']], 38 | }); 39 | storageStub.restore(); 40 | }); 41 | }); 42 | 43 | describe('saveEmoji', () => { 44 | it('should set emoji to storage.sync', async () => { 45 | const storageStub = stub( 46 | browserAPI.storage.sync, 47 | 'set', 48 | () => Promise.resolve(), 49 | ); 50 | const emoji = createEmoji('poro', 'poro://poro-url'); 51 | await saveEmoji(emoji); 52 | assertSpyCalls(storageStub, 1); 53 | assertSpyCall(storageStub, 0, { 54 | args: [{ 'Custom Emoji: poro': emoji }], 55 | returned: Promise.resolve(), 56 | }); 57 | storageStub.restore(); 58 | }); 59 | 60 | it('should error if emoji exists in local DB', async () => { 61 | const storageStub = stub( 62 | browserAPI.storage.sync, 63 | 'set', 64 | () => Promise.resolve(), 65 | ); 66 | const emoji = createEmoji('grinning squinting face', 'poro://poro-url'); 67 | const errorMessage = 'This Emoji Already Exists!'; 68 | await assertRejects(() => saveEmoji(emoji), Error, errorMessage); 69 | storageStub.restore(); 70 | }); 71 | }); 72 | 73 | it('getEmojis', async () => { 74 | const storageStub = stub(browserAPI.storage.sync, 'get', () => { 75 | return Promise.resolve([ 76 | createEmoji('poro', 'poro://poro-url'), 77 | createEmoji('nother', 'nother://nother-custom-emoji'), 78 | ]); 79 | }); 80 | const emojis = await getEmojis(['nother', 'poro', 'grinning squinting face']); 81 | assertEquals(emojis['grinning squinting face'].emoji, '😆'); 82 | assertEquals(emojis['poro'].imageURL, 'poro://poro-url'); 83 | assertEquals(emojis['nother'].imageURL, 'nother://nother-custom-emoji'); 84 | 85 | assertSpyCalls(storageStub, 1); 86 | 87 | const expectedArgs = [['Custom Emoji: nother', 'Custom Emoji: poro']]; 88 | assertSpyCall(storageStub, 0, { args: expectedArgs }); 89 | storageStub.restore(); 90 | }); 91 | 92 | describe('areEqualEmojis', async () => { 93 | const emoji_1a = await getEmoji('grinning squinting face'); 94 | const emoji_1b = await getEmoji('grinning squinting face'); 95 | const emoji_2 = await getEmoji('tractor'); 96 | const customEmoji_1a = createEmoji('poro', 'poro://poro-url'); 97 | const customEmoji_1b = createEmoji('poro', 'poro://poro-url'); 98 | 99 | if (!emoji_1a || !emoji_1b || !emoji_2) throw new Error('Bad emoji creation'); 100 | 101 | it('emoji_1a === emoji_1b', () => assert(areEqualEmojis(emoji_1a, emoji_1b))); 102 | it('emoji_1a !== emoji_2', () => assert(!areEqualEmojis(emoji_1a, emoji_2))); 103 | it('emoji_1b !== emoji_2', () => assert(!areEqualEmojis(emoji_1b, emoji_2))); 104 | it('customEmoji_1a === customEmoji_1b', () => 105 | assert(areEqualEmojis(customEmoji_1a, customEmoji_1b))); 106 | it('customEmoji_1a !== emoji_1a', () => 107 | assert(!areEqualEmojis(customEmoji_1a, emoji_1a))); 108 | }); 109 | -------------------------------------------------------------------------------- /source/config/legacy_autoselect_set.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the set of emojis used for Favioli 1.0 autoselect. Since favicons are 3 | * selected via algorithm, new sets can change what emoji is autoselected for 4 | * any given site. Thereforce, keep this set available so that Favioli 1.x users 5 | * can keep their setting intact. Future sets will all be based around unicode 6 | * version, so this should be the only set we need to store in this way. 7 | * @reference https://github.com/ivebencrazy/favioli/blob/v1.3.0/source/constants/defaults.js 8 | */ 9 | 10 | // deno-fmt-ignore 11 | const LEGACY_EMOJI_SET = [ 12 | '😀', '😁', '😂', '🤣', '😃', '😄', '😅', '😆', '😉', '😊', '😋', '😎', 13 | '😍', '😘', '😗', '😙', '😚', '🙂', '🤗', '🤩', '🤔', '🤨', '😐', '😑', 14 | '😶', '🙄', '😏', '😣', '😥', '😮', '🤐', '😯', '😪', '😫', '😴', '😌', 15 | '😛', '😜', '😝', '🤤', '😒', '😓', '😔', '😕', '🙃', '🤑', '😲', '☹️', 16 | '🙁', '😖', '😞', '😟', '😤', '😢', '😦', '😧', '😨', '😩', '🤯', '😬', 17 | '😰', '😳', '🤪', '😵', '😡', '😠', '🤬', '😷', '🤒', '🤕', '🤢', '🤮', 18 | '🤧', '😇', '🤠', '🤡', '🤥', '🤫', '🤭', '🧐', '🤓', '😈', '👿', '👹', 19 | '👺', '💀', '👻', '👽', '🤖', '💩', '😺', '😸', '😹', '😻', '😼', '😽', 20 | '🙀', '😿', '😾', '👶', '👦', '👧', '👨', '👩', '👴', '👵', '🤳', '💪', 21 | '👈', '👉', '☝️', '👆', '🖕', '👇', '✌️', '🤞', '🖖', '🤘', '🖐', '✋', 22 | '👌', '👍', '👎', '👊', '🤛', '🤜', '🤚', '👋', '🤟', '✍️', '👏', '👐', 23 | '🙌', '🤲', '🙏', '🤝', '💅', '👂', '👃', '👣', '👀', '👁', '🧠', '👅', 24 | '👄', '💋', '👓', '🕶', '👔', '👕', '👖', '🧣', '🧤', '🧥', '🧦', '👗', 25 | '👘', '👙', '👚', '👛', '👜', '👝', '🎒', '👞', '👟', '👠', '👡', '👢', 26 | '👑', '👒', '🎩', '🎓', '🧢', '⛑', '💄', '💍', '🌂', '💼', '🐶', '🐱', 27 | '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮', '🐷', '🐽', 28 | '🐸', '🐵', '🙊', '🙉', '🙊', '🐒', '🐔', '🐧', '🐦', '🐤', '🐣', '🐥', 29 | '🦆', '🦅', '🦉', '🦇', '🐺', '🐗', '🐴', '🦄', '🐝', '🐛', '🦋', '🐌', 30 | '🐚', '🐞', '🐜', '🕷', '🕸', '🐢', '🐍', '🦎', '🦂', '🦀', '🦑', '🐙', 31 | '🦐', '🐟', '🐡', '🐬', '🦈', '🐳', '🐋', '🐊', '🐆', '🐅', '🐃', '🐂', 32 | '🐄', '🦌', '🐪', '🐫', '🐘', '🦏', '🦍', '🐎', '🐖', '🐐', '🐏', '🐑', 33 | '🐕', '🐩', '🐈', '🐓', '🦃', '🕊', '🐇', '🐁', '🐀', '🐿', '🐾', '🐉', 34 | '🐲', '🌵', '🎄', '🌲', '🌳', '🌴', '🌱', '🌿', '☘️', '🍀', '🎍', '🎋', 35 | '🍃', '🍂', '🍁', '🍄', '🌾', '💐', '🌷', '🌹', '🥀', '🌻', '🌼', '🌸', 36 | '🌺', '🌎', '🌕', '🌚', '🌝', '🌞', '🌜', '🌙', '💫', '⭐', '🌟', '✨', 37 | '⚡️', '🔥', '💥', '☄️', '☀️', '🌤', '⛅️', '🌥', '🌦', '🌈', '☁️', '⛄️', 38 | '❄️', '🌬', '💨', '🌪', '🌫', '🌊', '💧', '💦', '☔', '🍏', '🍎', '🍐', 39 | '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🍈', '🍒', '🍑', '🍍', '🥝', '🥑', 40 | '🍅', '🍆', '🥒', '🥕', '🌽', '🌶', '🥔', '🍠', '🌰', '🥜', '🍯', '🥐', 41 | '🍞', '🥖', '🧀', '🥚', '🍳', '🥓', '🥞', '🍤', '🍗', '🍖', '🍕', '🌭', 42 | '🍔', '🍟', '🥙', '🌮', '🌯', '🥗', '🥘', '🍝', '🍜', '🍲', '🍥', '🍣', 43 | '🍱', '🍛', '🍚', '🍙', '🍘', '🍢', '🍡', '🍧', '🍨', '🍦', '🍰', '🎂', 44 | '🍮', '🍭', '🍬', '🍫', '🍿', '🍩', '🍪', '🥛', '🍼', '☕️', '🍵', '🍶', 45 | '🍺', '🥂', '🍷', '🥃', '🍸', '🍹', '🍾', '🥄', '🍴', '🍽', '⚽️', '🏀', 46 | '🏈', '⚾️', '🎾', '🏐', '🏉', '🎱', '🏓', '🏸', '🥅', '🏒', '🏑', '🏏', 47 | '⛳️', '🏹', '🎣', '🥊', '🥋', '⛸', '🏆', '🎪', '🤹‍', '🎭', '🎨', '🎬', 48 | '🎤', '🎧', '🎼', '🎹', '🥁', '🎷', '🎺', '🎸', '🎻', '🎲', '🎯', '🎳', 49 | '🎮', '🎰', '🚗', '🚕', '🚙', '🚌', '🚎', '🏎', '🚓', '🚑', '🚒', '🚐', 50 | '🚚', '🚛', '🚜', '🛴', '🚲', '🛵', '🏍', '🚨', '🚔', '🚍', '🚘', '🚖', 51 | '🚡', '🚠', '🚟', '🚃', '🚋', '🚞', '🚝', '🚄', '🚅', '🚈', '🚂', '🚆', 52 | '🚇', '🚊', '🚉', '🚁', '🛩', '✈️', '🛫', '🛬', '🚀', '🛰', '💺', '🛶', 53 | '⛵️', '🛥', '🚤', '🛳', '⛴', '🚢', '⚓️', '🚧', '⛽️', '🚏', '🚦', '🚥', 54 | '🗺', '🗿', '🗽', '⛲️', '🗼', '🏰', '🏯', '🏟', '🎡', '🎢', '🎠', '⛱', 55 | '🏖', '🏝', '🏔', '🗻', '🌋', '🏜', '🏕', '⛺️', '🛤', '🛣', '🏗', '🏭', 56 | '🏠', '🏢', '🏛', '⛪️', '🕌', '🕍', '🕋', '⛩', '❤️', '💔', '🙎', '🙅', 57 | '🙆', '💁', '🙋', '🙇', '🤦', '🤷', '💆', '💇', '🚶', '🏃', '💃', '🕺', 58 | '👯', '🧖‍', '👩‍👧‍👧' 59 | ]; 60 | 61 | // Only Windows has hacker cat 62 | if (/^Win\d+$/.test(navigator.platform)) { 63 | LEGACY_EMOJI_SET.push('🐱‍💻'); 64 | } 65 | 66 | export default Object.freeze(LEGACY_EMOJI_SET); 67 | -------------------------------------------------------------------------------- /static/styles/options.css: -------------------------------------------------------------------------------- 1 | /** RESET **/ 2 | html, body { 3 | margin: 0; 4 | padding: 0; 5 | line-height: 1.6; 6 | font-size: 24px; 7 | font-weight: 400; 8 | font-family: BlinkMacSystemFont, Avenir, "Avenir Next", "Roboto", "Ubuntu", "Helvetica Neue", sans-serif; 9 | color: var(--text-color); 10 | background: var(--background-color); 11 | } 12 | 13 | html { 14 | font-size: 24px; 15 | min-width: 600px; 16 | } 17 | 18 | body { 19 | width: 100%; 20 | height: 100%; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | #mount { 26 | width: 100%; 27 | } 28 | 29 | h1 { 30 | font-size: 1.5em; 31 | margin-bottom: 0.5em; 32 | user-select: none; 33 | -moz-user-select: none; 34 | } 35 | 36 | h2, p { 37 | font-weight: 300; 38 | } 39 | 40 | .emoji-label { 41 | margin: 0.2em; 42 | } 43 | 44 | .page { 45 | display: flex; 46 | align-content: center; 47 | justify-content: center; 48 | padding-top: 2em; 49 | } 50 | 51 | .page-content { 52 | max-width: 600px; 53 | width: 100%; 54 | } 55 | 56 | #status { 57 | bottom: 10px; 58 | left: 50%; 59 | padding-top: 20px; 60 | position: fixed; 61 | transform: translateX(-50%); 62 | } 63 | 64 | /** Nav **/ 65 | nav { 66 | position: absolute; 67 | margin: 0; 68 | padding: 0; 69 | user-select: none; 70 | -moz-user-select: none; 71 | top: 0; 72 | left: 0; 73 | width: 100%; 74 | } 75 | 76 | nav ul { 77 | position: absolute; 78 | top: 0; 79 | left: 0; 80 | list-style: none; 81 | padding: 0; 82 | margin: 0; 83 | width: 100%; 84 | height: 100%; 85 | background: var(--text-color); 86 | } 87 | 88 | nav li { 89 | display: inline-block; 90 | padding: 0; 91 | } 92 | 93 | nav li.logo { 94 | margin: 0 15px 0 15px; 95 | } 96 | 97 | nav a { 98 | color: black; 99 | font-weight: 300; 100 | opacity: 0.5; 101 | text-decoration: none; 102 | padding: 10px 15px 10px 15px; 103 | margin: 0; 104 | height: 100%; 105 | } 106 | 107 | nav li.active a { 108 | background-color: rgba(0, 0, 0, 0.1); 109 | opacity: 0.7; 110 | } 111 | 112 | nav li a:hover { 113 | background-color: rgba(0, 0, 0, 0.1); 114 | opacity: 1; 115 | } 116 | 117 | /** Save Button **/ 118 | .save { 119 | display: inline-block; 120 | position: relative; 121 | font-size: 0.8em; 122 | font-weight: 200; 123 | color: white; 124 | cursor: pointer; 125 | background-color: #2196F3; 126 | border: none; 127 | border-radius: 2px; 128 | margin-top: 2em; 129 | padding: 10px 30px 10px 30px; 130 | text-decoration: none; 131 | transition: all 0.2s; 132 | } 133 | 134 | .save:active { 135 | opacity: 0.7; 136 | } 137 | 138 | .save:hover, 139 | .save:focus { 140 | opacity: 0.9; 141 | } 142 | 143 | .save[disabled] { 144 | cursor: not-allowed; 145 | opacity: 0.8; 146 | } 147 | 148 | .save.outline { 149 | background: none; 150 | color: #2196F3; 151 | } 152 | 153 | .save.outline:hover, 154 | .save.outline:focus { 155 | background: #2196F3; 156 | color: white; 157 | } 158 | 159 | /** List **/ 160 | .list-item { 161 | position: relative; 162 | } 163 | 164 | .list-item .filter { 165 | background-color: #eee; 166 | border: none; 167 | border-radius: 2px; 168 | color: rgba(0, 0, 0, 0.8); 169 | cursor: text; 170 | display: inline-block; 171 | font-weight: 300; 172 | font-size: 1em; 173 | height: 1.3em; 174 | line-height: 1; 175 | padding-left: 0.2em; 176 | position: relative; 177 | transition: ease background 0.2s; 178 | vertical-align: middle; 179 | width: 75%; 180 | z-index: 1; 181 | } 182 | 183 | .list-item .filter:hover, 184 | .list-item .filter:focus { 185 | background: #ddd; 186 | } 187 | 188 | .list-item .remove { 189 | background-color: transparent; 190 | border: none; 191 | color: rgba(0, 0, 0, 0.4); 192 | cursor: pointer; 193 | font-size: 0.5em; 194 | transition: ease color 0.2s; 195 | user-select: none; 196 | vertical-align: middle; 197 | margin-left: 5px; 198 | } 199 | 200 | .list-item .remove:hover { 201 | color: rgba(0, 0, 0, 0.8); 202 | } 203 | 204 | .version-selector { 205 | color: rgba(0, 0, 0, 0.8); 206 | display: block; 207 | font-size: 24px; 208 | font-weight: 200; 209 | padding-left: 1.5em; 210 | position: relative; 211 | margin-bottom: 12px; 212 | user-select: none; 213 | -moz-user-select: none; 214 | } 215 | 216 | .version-selector select { 217 | font-size: 16px; 218 | font-weight: 200; 219 | margin-left: 0.5em; 220 | vertical-align: middle; 221 | } 222 | -------------------------------------------------------------------------------- /source/models/emoji.ts: -------------------------------------------------------------------------------- 1 | import type { Emoji as BaseEmoji } from 'https://deno.land/x/emoji@0.2.0/types.ts'; 2 | import type { BrowserStorage } from '../hooks/use_browser_storage.ts'; 3 | 4 | import browserAPI from 'browser'; 5 | import * as emoji from 'emoji'; 6 | import { createContext } from 'preact'; 7 | 8 | const { freeze, fromEntries, keys } = Object; 9 | const { storage } = browserAPI || {}; 10 | 11 | export interface Emoji extends BaseEmoji { 12 | imageURL?: string; // Support Custom Emojis 13 | } 14 | 15 | export interface EmojiGroup { 16 | name: string; 17 | representativeEmoji: string; 18 | emojis: readonly Emoji[]; 19 | } 20 | 21 | export interface EmojiGroups { 22 | [name: string]: EmojiGroup; 23 | } 24 | 25 | export interface EmojiMap { 26 | [name: string]: Emoji; 27 | } 28 | 29 | export { emoji }; 30 | export const emojis = freeze(emoji.all()); 31 | 32 | export const DEFAULT_EMOJI = freeze(emoji.infoByCode('😀') as Emoji); 33 | export const CUSTOM_GROUP_NAME = 'Custom Emojis'; 34 | 35 | export const emojiGroups: EmojiGroups = createEmojiGroups(emojis); 36 | export const emojiGroupsArray = freeze( 37 | keys(emojiGroups).map((name) => emojiGroups[name]), 38 | ); 39 | 40 | const DEFAULT_CUSTOM_EMOJI_IDS: string[] = []; 41 | export const CustomEmojiContext = createContext>({ 42 | loading: true, 43 | cache: DEFAULT_CUSTOM_EMOJI_IDS, 44 | setCache: () => {}, 45 | saveCacheToStorage: async () => {}, 46 | saveToStorageBypassCache: async () => {}, 47 | }); 48 | 49 | export function createEmoji( 50 | description: string, 51 | imageURL: string, 52 | ): Emoji { 53 | return { 54 | emoji: '', 55 | description, 56 | group: CUSTOM_GROUP_NAME, 57 | subgroup: 'custom-emojis', 58 | emojiVersion: 0, 59 | unicodeVersion: 0, 60 | tags: [], 61 | aliases: [description], 62 | imageURL, 63 | }; 64 | } 65 | 66 | export async function deleteEmoji(emojiToDelete: Emoji): Promise { 67 | const desc: string = emojiToDelete.description; 68 | await storage.sync.remove(getEmojiStorageId(desc)); 69 | } 70 | 71 | export const getEmojiStorageId = (id: string) => `Custom Emoji: ${id}`; 72 | const byDescription: EmojiMap = fromEntries( 73 | emojis.map((emoji) => [emoji.description, emoji]), 74 | ); 75 | 76 | export async function getEmoji(desc: string): Promise { 77 | if (byDescription[desc]) return byDescription[desc]; 78 | const storageID = getEmojiStorageId(desc); 79 | try { 80 | const resp = await storage.sync.get([storageID]); 81 | return resp[storageID] as Emoji; 82 | } catch { 83 | return; 84 | } 85 | } 86 | 87 | export async function getEmojis(descs: string[]): Promise { 88 | const localEmojis: EmojiMap = {}; 89 | const customDescIds: string[] = []; 90 | descs.forEach((desc: string) => { 91 | if (byDescription[desc]) localEmojis[desc] = byDescription[desc]; 92 | else customDescIds.push(getEmojiStorageId(desc)); 93 | }); 94 | const customEmojis = await storage.sync.get(customDescIds) as EmojiMap; 95 | const customEmojiWithProperName: EmojiMap = {}; 96 | Object.keys(customEmojis).forEach((storageId) => { 97 | const emoji = customEmojis[storageId]; 98 | customEmojiWithProperName[emoji.description] = emoji; 99 | }); 100 | return { ...localEmojis, ...customEmojiWithProperName }; 101 | } 102 | 103 | export async function saveEmoji(emojiToSave: Emoji): Promise { 104 | const desc: string = emojiToSave.description; 105 | if (byDescription[desc]) throw new Error('This Emoji Already Exists!'); 106 | await storage.sync.set({ [getEmojiStorageId(desc)]: emojiToSave }); 107 | } 108 | 109 | export function areEqualEmojis(emoji1?: Emoji, emoji2?: Emoji): boolean { 110 | if (!emoji1 || !emoji2) return false; 111 | if (emoji1.imageURL && (emoji1.imageURL === emoji2.imageURL)) return true; 112 | if (emoji1.emoji && (emoji1.emoji === emoji2.emoji)) return true; 113 | return false; 114 | } 115 | 116 | function createEmojiGroups(emojis: readonly Emoji[]): EmojiGroups { 117 | const emojiGroups: { 118 | [name: string]: { 119 | name: string; 120 | representativeEmoji: string; 121 | emojis: Emoji[]; 122 | }; 123 | } = {}; 124 | 125 | emojis.forEach((emoji) => { 126 | if (!emojiGroups[emoji.group]) { 127 | emojiGroups[emoji.group] = { 128 | name: emoji.group, 129 | emojis: [emoji], 130 | representativeEmoji: emoji.emoji, 131 | }; 132 | } else { 133 | emojiGroups[emoji.group].emojis.push(emoji); 134 | } 135 | }); 136 | 137 | emojiGroups[CUSTOM_GROUP_NAME] = { 138 | name: CUSTOM_GROUP_NAME, 139 | emojis: [], 140 | representativeEmoji: '*', 141 | }; 142 | 143 | return freeze(emojiGroups); 144 | } 145 | -------------------------------------------------------------------------------- /static/styles/shared.css: -------------------------------------------------------------------------------- 1 | /** CHECKBOX **/ 2 | .checkbox { 3 | color: rgba(0, 0, 0, 0.8); 4 | display: block; 5 | font-size: 24px; 6 | font-weight: 200; 7 | padding-left: 1.5em; 8 | position: relative; 9 | margin-bottom: 12px; 10 | user-select: none; 11 | -moz-user-select: none; 12 | } 13 | 14 | .checkbox input { 15 | cursor: pointer; 16 | font-size: 24px; 17 | height: 1.4em; 18 | left: -0.3em; 19 | opacity: 0; 20 | position: absolute; 21 | top: 0; 22 | width: 1.4em; 23 | } 24 | 25 | .checkbox .checkmark { 26 | background-color: #eee; 27 | border-radius: 2px; 28 | cursor: pointer; 29 | height: 1.3em; 30 | left: 0; 31 | position: absolute; 32 | top: 0.2em; 33 | width: 1.3em; 34 | transition: background ease 0.2s; 35 | z-index: -1; 36 | } 37 | 38 | .checkbox:hover input ~ .checkmark { 39 | background-color: #ccc; 40 | } 41 | 42 | .checkbox input:checked ~ .checkmark { 43 | background-color: #2196F3; 44 | } 45 | 46 | .checkbox .checkmark:after { 47 | content: ""; 48 | position: absolute; 49 | display: none; 50 | } 51 | 52 | .checkbox input:checked ~ .checkmark:after { 53 | display: block; 54 | } 55 | 56 | .checkbox .checkmark:after { 57 | left: 0.48em; 58 | top: 0.2em; 59 | width: 0.3em; 60 | height: 0.6em; 61 | border: solid white; 62 | border-width: 0 0.1em 0.1em 0; 63 | transform: rotate(45deg); 64 | } 65 | 66 | /** Emoji Selector Button **/ 67 | button.emoji-selector-button { 68 | background: #eee; 69 | border: none; 70 | border-radius: 2px; 71 | cursor: pointer; 72 | font-size: 1rem; 73 | height: 32px; 74 | line-height: 1; 75 | margin-top: 0; 76 | margin-left: 10px; 77 | padding: 4px 0; 78 | transition: background ease 0.2s; 79 | user-select: none; 80 | vertical-align: middle; 81 | width: 1.3rem; 82 | } 83 | 84 | button.emoji-selector-button:hover, 85 | button.emoji-selector-button:focus { 86 | background: #ddd; 87 | } 88 | 89 | button.emoji-selector-button img { 90 | width: 1rem; 91 | height: 1rem; 92 | padding: 0; 93 | margin: 0; 94 | } 95 | 96 | /** Emoji Selector **/ 97 | .emoji-selector-popup { 98 | position: absolute; 99 | right: 17%; 100 | display: flex; 101 | flex-direction: column; 102 | width: 350px; 103 | height: 400px; 104 | border: 2px solid black; 105 | border-radius: 5px; 106 | background-color: white; 107 | z-index: 1000; 108 | overflow: scroll; 109 | line-height: 1em; 110 | padding: 0 10px 10px 10px; 111 | } 112 | 113 | p { 114 | padding-bottom: 0; 115 | margin-bottom: 0; 116 | } 117 | 118 | .emoji-filter-input { 119 | width: 100%; 120 | font-size: 0.8em; 121 | margin: 10px auto 10px auto; 122 | box-sizing: border-box; 123 | } 124 | 125 | .emoji-group { 126 | display: flex; 127 | flex-wrap: wrap; 128 | justify-content: space-between; 129 | } 130 | 131 | .emoji-group::after { 132 | content: ""; 133 | flex: auto; 134 | } 135 | 136 | .emoji-group-title { 137 | text-align: left; 138 | font-size: 0.6em; 139 | font-weight: bold; 140 | width: 100%; 141 | } 142 | 143 | .emoji-group-button { 144 | cursor: pointer; 145 | border: 0; 146 | font-size: 1em; 147 | margin: 0; 148 | width: 1.5em; 149 | height: 1.5em; 150 | background-color: white; 151 | border-radius: 4px; 152 | } 153 | 154 | .emoji-group-button:hover { 155 | background-color: rgba(0, 0, 0, 0.3); 156 | } 157 | 158 | .emoji-group-button img { 159 | width: 1em; 160 | height: 1em; 161 | } 162 | 163 | .emoji-header { 164 | position: sticky; 165 | background-color: white; 166 | 167 | top: 0; 168 | height: 3em; 169 | margin: 0 auto 0 auto; 170 | width: 100%; 171 | text-align: center; 172 | } 173 | 174 | .emoji-footer { 175 | position: absolute; 176 | bottom: 0; 177 | height: 15px; 178 | } 179 | 180 | .emoji-group-selector { 181 | display: flex; 182 | flex-direction: row; 183 | justify-content: space-evenly; 184 | align-content: flex-end; 185 | text-align: center; 186 | } 187 | 188 | .emoji-group-selector-button { 189 | cursor: pointer; 190 | border: 0; 191 | font-size: 0.8em; 192 | margin: 0; 193 | width: 1.3em; 194 | height: 1.3em; 195 | background-color: white; 196 | padding: 5px; 197 | } 198 | 199 | .emoji-group-selector-button.selected, 200 | .emoji-group-selector-button:hover { 201 | background-color: rgba(0, 0, 0, 0.2); 202 | } 203 | 204 | .emoji-group-selector-button.selected { 205 | border-bottom: 2px solid rgba(0, 0, 0, 0.6); 206 | } 207 | 208 | .emoji-custom-upload { 209 | display: flex; 210 | flex-direction:column; 211 | padding: 20px; 212 | height: 100%; 213 | } 214 | 215 | .custom-emoji-group-title { 216 | margin-top: 20px; 217 | text-align: left; 218 | font-size: 0.6em; 219 | font-weight: bold; 220 | width: 100%; 221 | } 222 | 223 | .custom-emoji-button { 224 | margin: 5px; 225 | } 226 | 227 | .emoji-custom-upload button { 228 | margin: 5px; 229 | } 230 | 231 | .emoji-custom-upload .preview { 232 | display: block; 233 | margin: auto; 234 | } 235 | 236 | .emoji-custom-upload-form { 237 | flex-grow: 1; 238 | display:flex; 239 | flex-direction: column; 240 | padding-top: 1em; 241 | } 242 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2", 3 | "remote": { 4 | "https://deno.land/std@0.145.0/_deno_unstable.ts": "be3276fd42cffb49f51b705c4b0aa8656aaf2a34be22d769455c8e50ea38e51a", 5 | "https://deno.land/std@0.145.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", 6 | "https://deno.land/std@0.145.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49", 7 | "https://deno.land/std@0.145.0/async/abortable.ts": "87aa7230be8360c24ad437212311c9e8d4328854baec27b4c7abb26e85515c06", 8 | "https://deno.land/std@0.145.0/async/deadline.ts": "48ac998d7564969f3e6ec6b6f9bf0217ebd00239b1b2292feba61272d5dd58d0", 9 | "https://deno.land/std@0.145.0/async/debounce.ts": "564273ef242bcfcda19a439132f940db8694173abffc159ea34f07d18fc42620", 10 | "https://deno.land/std@0.145.0/async/deferred.ts": "bc18e28108252c9f67dfca2bbc4587c3cbf3aeb6e155f8c864ca8ecff992b98a", 11 | "https://deno.land/std@0.145.0/async/delay.ts": "cbbdf1c87d1aed8edc7bae13592fb3e27e3106e0748f089c263390d4f49e5f6c", 12 | "https://deno.land/std@0.145.0/async/mod.ts": "9852cd8ed897ab2d41a8fbee611d574e97898327db5c19d9d58e41126473f02c", 13 | "https://deno.land/std@0.145.0/async/mux_async_iterator.ts": "5b4aca6781ad0f2e19ccdf1d1a1c092ccd3e00d52050d9c27c772658c8367256", 14 | "https://deno.land/std@0.145.0/async/pool.ts": "ef9eb97b388543acbf0ac32647121e4dbe629236899586c4d4311a8770fbb239", 15 | "https://deno.land/std@0.145.0/async/tee.ts": "bcfae0017ebb718cf4eef9e2420e8675d91cb1bcc0ed9b668681af6e6caad846", 16 | "https://deno.land/std@0.145.0/bytes/bytes_list.ts": "aba5e2369e77d426b10af1de0dcc4531acecec27f9b9056f4f7bfbf8ac147ab4", 17 | "https://deno.land/std@0.145.0/bytes/equals.ts": "3c3558c3ae85526f84510aa2b48ab2ad7bdd899e2e0f5b7a8ffc85acb3a6043a", 18 | "https://deno.land/std@0.145.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf", 19 | "https://deno.land/std@0.145.0/encoding/base64.ts": "c8c16b4adaa60d7a8eee047c73ece26844435e8f7f1328d74593dbb2dd58ea4f", 20 | "https://deno.land/std@0.145.0/encoding/base64url.ts": "0f25223d77ddc3133590cd7136fb6ca0a7754008131ef8cc8a2240566e8a3fba", 21 | "https://deno.land/std@0.145.0/flags/mod.ts": "387fd528c1c518eec8eb58ae886b15d3dabab27985e55e886c10ae37a540c6ee", 22 | "https://deno.land/std@0.145.0/fmt/colors.ts": "6f9340b7fb8cc25a993a99e5efc56fe81bb5af284ff412129dd06df06f53c0b4", 23 | "https://deno.land/std@0.145.0/fmt/printf.ts": "e2c0f72146aed1efecf0c39ab928b26ae493a2278f670a871a0fbdcf36ff3379", 24 | "https://deno.land/std@0.145.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b", 25 | "https://deno.land/std@0.145.0/io/types.d.ts": "0cae3a62da7a37043661746c65c021058bae020b54e50c0e774916e5d4baee43", 26 | "https://deno.land/std@0.145.0/node/_buffer.d.ts": "90f674081428a61978b6d481c5f557ff743a3f4a85d7ae113caab48fdf5b8a63", 27 | "https://deno.land/std@0.145.0/node/_buffer.mjs": "4e3e6b0f0613300340705a99a1998c655ba22bf08644c49df21f4aaa44e951d0", 28 | "https://deno.land/std@0.145.0/node/_core.ts": "568d277be2e086af996cbdd599fec569f5280e9a494335ca23ad392b130d7bb9", 29 | "https://deno.land/std@0.145.0/node/_events.d.ts": "5b6d1a7931eb692d3b64018e7a3f57310284d3bf467aa2e6371c65bb626c1859", 30 | "https://deno.land/std@0.145.0/node/_events.mjs": "a87d6bc0e2a139ce1bb89e56fe36969dc960c1af70b6d5fafaab8782659c57a0", 31 | "https://deno.land/std@0.145.0/node/_global.d.ts": "6dadaf8cec2a0c506b22170617286e0bdc80be53dd0673e67fc7dd37a1130c68", 32 | "https://deno.land/std@0.145.0/node/_next_tick.ts": "3546559be2b353208f8b10df81c6d9c26c045fa4ea811926f6596f2dc6b1b0b1", 33 | "https://deno.land/std@0.145.0/node/_process/exiting.ts": "bc9694769139ffc596f962087155a8bfef10101d03423b9dcbc51ce6e1f88fce", 34 | "https://deno.land/std@0.145.0/node/_process/process.ts": "84644b184053835670f79652d1ce3312c9ad079c211e6207ebefeedf159352a3", 35 | "https://deno.land/std@0.145.0/node/_process/stdio.mjs": "971c3b086040d8521562155db13f22f9971d5c42c852b2081d4d2f0d8b6ab6bd", 36 | "https://deno.land/std@0.145.0/node/_process/streams.mjs": "da4dc14ad62a72d3efbaec6a660567a778653cb1b8d7a0d17fedba909574263d", 37 | "https://deno.land/std@0.145.0/node/_stream.d.ts": "cbec8328d2dc46b13090420117e1d55018b0632b69b4fb0692f1a028ef91d1aa", 38 | "https://deno.land/std@0.145.0/node/_stream.mjs": "07f6cbabaad0382fb4b9a25e70ac3093a44022b859247f64726746e6373f1c91", 39 | "https://deno.land/std@0.145.0/node/_util/_util_callbackify.ts": "79928ad80df3e469f7dcdb198118a7436d18a9f6c08bd7a4382332ad25a718cf", 40 | "https://deno.land/std@0.145.0/node/_utils.ts": "facb40140c9f1e6f23a047071a0d7e00f2be32afe5a7f063f7e55b1bd83f6488", 41 | "https://deno.land/std@0.145.0/node/buffer.ts": "fbecbf3f237fa49bec96e97ecf56a7b92d48037b3d11219288e68943cc921600", 42 | "https://deno.land/std@0.145.0/node/events.ts": "a1d40fc0dbccc944379ef968b80ea08f9fce579e88b5057fdb64e4f0812476dd", 43 | "https://deno.land/std@0.145.0/node/internal/blob.mjs": "52080b2f40b114203df67f8a6650f9fe3c653912b8b3ef2f31f029853df4db53", 44 | "https://deno.land/std@0.145.0/node/internal/buffer.mjs": "6662fe7fe517329453545be34cea27a24f8ccd6d09afd4f609f11ade2b6dfca7", 45 | "https://deno.land/std@0.145.0/node/internal/crypto/_keys.ts": "7f993ece8c8e94a292944518cf4173521c6bf01785e75be014cd45a9cc2e4ad5", 46 | "https://deno.land/std@0.145.0/node/internal/crypto/constants.ts": "d2c8821977aef55e4d66414d623c24a2447791a8b49b6404b8db32d81e20c315", 47 | "https://deno.land/std@0.145.0/node/internal/error_codes.ts": "ac03c4eae33de3a69d6c98e8678003207eecf75a6900eb847e3fea3c8c9e6d8f", 48 | "https://deno.land/std@0.145.0/node/internal/errors.ts": "fe669a2482099ddd2d5c9d76a8971b5fb47d7a4c6a98c7f3ef9f366bf10e1998", 49 | "https://deno.land/std@0.145.0/node/internal/fixed_queue.ts": "455b3c484de48e810b13bdf95cd1658ecb1ba6bcb8b9315ffe994efcde3ba5f5", 50 | "https://deno.land/std@0.145.0/node/internal/hide_stack_frames.ts": "a91962ec84610bc7ec86022c4593cdf688156a5910c07b5bcd71994225c13a03", 51 | "https://deno.land/std@0.145.0/node/internal/net.ts": "1239886cd2508a68624c2dae8abf895e8aa3bb15a748955349f9ac5539032238", 52 | "https://deno.land/std@0.145.0/node/internal/normalize_encoding.mjs": "3779ec8a7adf5d963b0224f9b85d1bc974a2ec2db0e858396b5d3c2c92138a0a", 53 | "https://deno.land/std@0.145.0/node/internal/options.ts": "a23c285975e058cb26a19abcb048cd8b46ab12d21cfb028868ac8003fffb43ac", 54 | "https://deno.land/std@0.145.0/node/internal/process/per_thread.mjs": "bc1be72a6a662bf81573c20fe74893374847a7302065ddf52fb3fb2af505f31f", 55 | "https://deno.land/std@0.145.0/node/internal/readline/callbacks.mjs": "1aa7d97dbc10ed85474ca046518793cfe6490ec008aac875bb437ded32f9254e", 56 | "https://deno.land/std@0.145.0/node/internal/readline/utils.mjs": "5278e8e48a075a375530e9f083d54bcc057a97e8d18ba37331cfb8d9a4d4c7ef", 57 | "https://deno.land/std@0.145.0/node/internal/streams/_utils.ts": "77fceaa766679847e4d4c3c96b2573c00a790298d90551e8e4df1d5e0fdaad3b", 58 | "https://deno.land/std@0.145.0/node/internal/streams/add-abort-signal.mjs": "5623b83fa64d439cc4a1f09ae47ec1db29512cc03479389614d8f62a37902f5e", 59 | "https://deno.land/std@0.145.0/node/internal/streams/buffer_list.mjs": "c6a7b29204fae025ff5e9383332acaea5d44bc7c522a407a79b8f7a6bc6c312d", 60 | "https://deno.land/std@0.145.0/node/internal/streams/compose.mjs": "b522daab35a80ae62296012a4254fd7edfc0366080ffe63ddda4e38fe6b6803e", 61 | "https://deno.land/std@0.145.0/node/internal/streams/destroy.mjs": "9c9bbeb172a437041d529829f433df72cf0b63ae49f3ee6080a55ffbef7572ad", 62 | "https://deno.land/std@0.145.0/node/internal/streams/duplex.mjs": "95e10e60d31ed3290c33110f8485196bdee19e12550b46e0be9d93b51f8dec23", 63 | "https://deno.land/std@0.145.0/node/internal/streams/end-of-stream.mjs": "38be76eaceac231dfde643e72bc0940625446bf6d1dbd995c91c5ba9fd59b338", 64 | "https://deno.land/std@0.145.0/node/internal/streams/from.mjs": "134255c698ed63b33199911eb8e042f8f67e9682409bb11552e6120041ed1872", 65 | "https://deno.land/std@0.145.0/node/internal/streams/legacy.mjs": "6ea28db95d4503447473e62f0b23ff473bfe1751223c33a3c5816652e93b257a", 66 | "https://deno.land/std@0.145.0/node/internal/streams/passthrough.mjs": "a51074193b959f3103d94de41e23a78dfcff532bdba53af9146b86340d85eded", 67 | "https://deno.land/std@0.145.0/node/internal/streams/pipeline.mjs": "9890b121759ede869174ef70c011fde964ca94d81f2ed97b8622d7cb17b49285", 68 | "https://deno.land/std@0.145.0/node/internal/streams/readable.mjs": "a70c41171ae25c556b52785b0c178328014bd33d8c0e4d229d4adaac7414b6ca", 69 | "https://deno.land/std@0.145.0/node/internal/streams/state.mjs": "9ef917392a9d8005a6e038260c5fd31518d2753aea0bc9e39824c199310434cb", 70 | "https://deno.land/std@0.145.0/node/internal/streams/transform.mjs": "3b361abad2ac78f7ccb6f305012bafdc0e983dfa4bb6ecddb4626e34a781a5f5", 71 | "https://deno.land/std@0.145.0/node/internal/streams/utils.mjs": "06c21d0db0d51f1bf1e3225a661c3c29909be80355d268e64ee5922fc5eb6c5e", 72 | "https://deno.land/std@0.145.0/node/internal/streams/writable.mjs": "ad4e2b176ffdf752c8e678ead3a514679a5a8d652f4acf797115dceb798744d5", 73 | "https://deno.land/std@0.145.0/node/internal/util.mjs": "2f0c8ff553c175ea6e4ed13d7cd7cd6b86dc093dc2f783c6c3dfc63f60a0943e", 74 | "https://deno.land/std@0.145.0/node/internal/util/comparisons.ts": "666e75e01a85b5d3410e43625ab9fc165811439aa1298c14054acb64b670dc48", 75 | "https://deno.land/std@0.145.0/node/internal/util/debuglog.ts": "6f12a764f5379e9d2675395d15d2fb48bd7376921ef64006ffb022fc7f44ab82", 76 | "https://deno.land/std@0.145.0/node/internal/util/inspect.mjs": "d1c2569c66a3dab45eec03208f22ad4351482527859c0011a28a6c797288a0aa", 77 | "https://deno.land/std@0.145.0/node/internal/util/types.ts": "bc2d35582946525f6068b678bb798e2a10298fefeefd3cb9b85f6bdc9014da16", 78 | "https://deno.land/std@0.145.0/node/internal/validators.mjs": "a7e82eafb7deb85c332d5f8d9ffef052f46a42d4a121eada4a54232451acc49a", 79 | "https://deno.land/std@0.145.0/node/internal_binding/_libuv_winerror.ts": "801e05c2742ae6cd42a5f0fd555a255a7308a65732551e962e5345f55eedc519", 80 | "https://deno.land/std@0.145.0/node/internal_binding/_listen.ts": "c15a356ef4758770fc72d3ca4db33f0cc321016df1aafb927c027c0d73ac2c42", 81 | "https://deno.land/std@0.145.0/node/internal_binding/_node.ts": "e4075ba8a37aef4eb5b592c8e3807c39cb49ca8653faf8e01a43421938076c1b", 82 | "https://deno.land/std@0.145.0/node/internal_binding/_timingSafeEqual.ts": "4a4ef17e482889d9d82138d5ffc0e787c32c04b1f12b28d076b1a69ceca46af1", 83 | "https://deno.land/std@0.145.0/node/internal_binding/_utils.ts": "1c50883b5751a9ea1b38951e62ed63bacfdc9d69ea665292edfa28e1b1c5bd94", 84 | "https://deno.land/std@0.145.0/node/internal_binding/_winerror.ts": "8811d4be66f918c165370b619259c1f35e8c3e458b8539db64c704fbde0a7cd2", 85 | "https://deno.land/std@0.145.0/node/internal_binding/ares.ts": "33ff8275bc11751219af8bd149ea221c442d7e8676e3e9f20ccb0e1f0aac61b8", 86 | "https://deno.land/std@0.145.0/node/internal_binding/async_wrap.ts": "b83e4021a4854b2e13720f96d21edc11f9905251c64c1bc625a361f574400959", 87 | "https://deno.land/std@0.145.0/node/internal_binding/buffer.ts": "781e1d13adc924864e6e37ecb5152e8a4e994cf394695136e451c47f00bda76c", 88 | "https://deno.land/std@0.145.0/node/internal_binding/cares_wrap.ts": "17ff0dd7dbf77e835a7b8fa5361efd26c0b3d610cba2641e0b613a572ebab412", 89 | "https://deno.land/std@0.145.0/node/internal_binding/config.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 90 | "https://deno.land/std@0.145.0/node/internal_binding/connection_wrap.ts": "0380444ee94d5bd7b0b09921223d16729c9762a94e80b7f51eda49c7f42e6d0a", 91 | "https://deno.land/std@0.145.0/node/internal_binding/constants.ts": "f4afc504137fb21f3908ab549931604968dfa62432b285a0874f41c4cade9ed2", 92 | "https://deno.land/std@0.145.0/node/internal_binding/contextify.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 93 | "https://deno.land/std@0.145.0/node/internal_binding/credentials.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 94 | "https://deno.land/std@0.145.0/node/internal_binding/crypto.ts": "33344221de729bfb06f4953ed9d68efbb7e6412e60ff212883680b4bf08c011f", 95 | "https://deno.land/std@0.145.0/node/internal_binding/errors.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 96 | "https://deno.land/std@0.145.0/node/internal_binding/fs.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 97 | "https://deno.land/std@0.145.0/node/internal_binding/fs_dir.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 98 | "https://deno.land/std@0.145.0/node/internal_binding/fs_event_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 99 | "https://deno.land/std@0.145.0/node/internal_binding/handle_wrap.ts": "eadbeea68deee9768b50f8f797738a088e8a7a1c9aa7d092c955faeacac53d58", 100 | "https://deno.land/std@0.145.0/node/internal_binding/heap_utils.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 101 | "https://deno.land/std@0.145.0/node/internal_binding/http_parser.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 102 | "https://deno.land/std@0.145.0/node/internal_binding/icu.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 103 | "https://deno.land/std@0.145.0/node/internal_binding/inspector.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 104 | "https://deno.land/std@0.145.0/node/internal_binding/js_stream.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 105 | "https://deno.land/std@0.145.0/node/internal_binding/messaging.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 106 | "https://deno.land/std@0.145.0/node/internal_binding/mod.ts": "f68e74e8eed84eaa6b0de24f0f4c47735ed46866d7ee1c5a5e7c0667b4f0540f", 107 | "https://deno.land/std@0.145.0/node/internal_binding/module_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 108 | "https://deno.land/std@0.145.0/node/internal_binding/native_module.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 109 | "https://deno.land/std@0.145.0/node/internal_binding/natives.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 110 | "https://deno.land/std@0.145.0/node/internal_binding/node_file.ts": "c96ee0b2af319a3916de950a6c4b0d5fb00d09395c51cd239c54d95d62567aaf", 111 | "https://deno.land/std@0.145.0/node/internal_binding/node_options.ts": "3cd5706153d28a4f5944b8b162c1c61b7b8e368a448fb1a2cff9f7957d3db360", 112 | "https://deno.land/std@0.145.0/node/internal_binding/options.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 113 | "https://deno.land/std@0.145.0/node/internal_binding/os.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 114 | "https://deno.land/std@0.145.0/node/internal_binding/performance.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 115 | "https://deno.land/std@0.145.0/node/internal_binding/pipe_wrap.ts": "792e3bbcbdb7ce3b51a430a85331a90408113160739d72d050ab243714219430", 116 | "https://deno.land/std@0.145.0/node/internal_binding/process_methods.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 117 | "https://deno.land/std@0.145.0/node/internal_binding/report.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 118 | "https://deno.land/std@0.145.0/node/internal_binding/serdes.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 119 | "https://deno.land/std@0.145.0/node/internal_binding/signal_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 120 | "https://deno.land/std@0.145.0/node/internal_binding/spawn_sync.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 121 | "https://deno.land/std@0.145.0/node/internal_binding/stream_wrap.ts": "7a418a7c57bbfb642a111753c98e9cdfcd4aafa5aec4b48be1dbd62e08c2d9a7", 122 | "https://deno.land/std@0.145.0/node/internal_binding/string_decoder.ts": "5cb1863763d1e9b458bc21d6f976f16d9c18b3b3f57eaf0ade120aee38fba227", 123 | "https://deno.land/std@0.145.0/node/internal_binding/symbols.ts": "51cfca9bb6132d42071d4e9e6b68a340a7f274041cfcba3ad02900886e972a6c", 124 | "https://deno.land/std@0.145.0/node/internal_binding/task_queue.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 125 | "https://deno.land/std@0.145.0/node/internal_binding/tcp_wrap.ts": "dc30a903d7589dc82b8056a473b0318ecf3262e5c9e5974375fee8548b847056", 126 | "https://deno.land/std@0.145.0/node/internal_binding/timers.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 127 | "https://deno.land/std@0.145.0/node/internal_binding/tls_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 128 | "https://deno.land/std@0.145.0/node/internal_binding/trace_events.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 129 | "https://deno.land/std@0.145.0/node/internal_binding/tty_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 130 | "https://deno.land/std@0.145.0/node/internal_binding/types.ts": "4c26fb74ba2e45de553c15014c916df6789529a93171e450d5afb016b4c765e7", 131 | "https://deno.land/std@0.145.0/node/internal_binding/udp_wrap.ts": "74ef0ac9bd1e4ad2f2f470edf25a805f9dc7250cd7aaea43116a959da9134590", 132 | "https://deno.land/std@0.145.0/node/internal_binding/url.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 133 | "https://deno.land/std@0.145.0/node/internal_binding/util.ts": "faf5146c3cc3b2d6c26026a818b4a16e91488ab26e63c069f36ba3c3ae24c97b", 134 | "https://deno.land/std@0.145.0/node/internal_binding/uv.ts": "aa1db842936e77654522d9136bb2ae191bf334423f58962a8a7404b6635b5b49", 135 | "https://deno.land/std@0.145.0/node/internal_binding/v8.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 136 | "https://deno.land/std@0.145.0/node/internal_binding/worker.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 137 | "https://deno.land/std@0.145.0/node/internal_binding/zlib.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", 138 | "https://deno.land/std@0.145.0/node/process.ts": "52e34ac80ce94e157c6df9e25343b3863182d639f0113d0dabb99f7fb59d55ca", 139 | "https://deno.land/std@0.145.0/node/stream.ts": "d127faa074a9e3886e4a01dcfe9f9a6a4b5641f76f6acc356e8ded7da5dc2c81", 140 | "https://deno.land/std@0.145.0/node/stream/promises.mjs": "b263c09f2d6bd715dc514fab3f99cca84f442e2d23e87adbe76e32ea46fc87e6", 141 | "https://deno.land/std@0.145.0/node/string_decoder.ts": "51ce85a173d2e36ac580d418bb48b804adb41732fc8bd85f7d5d27b7accbc61f", 142 | "https://deno.land/std@0.145.0/node/util.ts": "3c312dae662b0973bdf86f95211f57b65cc07e128e7460b52536f71c4ba379a0", 143 | "https://deno.land/std@0.145.0/node/util/types.ts": "f9288198cacd374b41bae7e92a23179d3160f4c0eaf14e19be3a4e7057219a60", 144 | "https://deno.land/std@0.145.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", 145 | "https://deno.land/std@0.145.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", 146 | "https://deno.land/std@0.145.0/path/_util.ts": "c1e9686d0164e29f7d880b2158971d805b6e0efc3110d0b3e24e4b8af2190d2b", 147 | "https://deno.land/std@0.145.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", 148 | "https://deno.land/std@0.145.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee", 149 | "https://deno.land/std@0.145.0/path/mod.ts": "4945b430b759b0b3d98f2a278542cbcf95e0ad2bd8eaaed3c67322b306b2b346", 150 | "https://deno.land/std@0.145.0/path/posix.ts": "c1f7afe274290ea0b51da07ee205653b2964bd74909a82deb07b69a6cc383aaa", 151 | "https://deno.land/std@0.145.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", 152 | "https://deno.land/std@0.145.0/path/win32.ts": "bd7549042e37879c68ff2f8576a25950abbfca1d696d41d82c7bca0b7e6f452c", 153 | "https://deno.land/std@0.145.0/streams/conversion.ts": "fc3db02026183da795fa32ac7549868e9f19c75ba029d4b4c3739af62b48517a", 154 | "https://deno.land/std@0.145.0/testing/_diff.ts": "029a00560b0d534bc0046f1bce4bd36b3b41ada3f2a3178c85686eb2ff5f1413", 155 | "https://deno.land/std@0.145.0/testing/_format.ts": "0d8dc79eab15b67cdc532826213bbe05bccfd276ca473a50a3fc7bbfb7260642", 156 | "https://deno.land/std@0.145.0/testing/asserts.ts": "319df43e1e6bba2520508f6090a21a9a640cbe2754d255aee17cd1dfa78c2ff6", 157 | "https://deno.land/std@0.170.0/_util/asserts.ts": "d0844e9b62510f89ce1f9878b046f6a57bf88f208a10304aab50efcb48365272", 158 | "https://deno.land/std@0.170.0/_util/os.ts": "8a33345f74990e627b9dfe2de9b040004b08ea5146c7c9e8fe9a29070d193934", 159 | "https://deno.land/std@0.170.0/fmt/colors.ts": "03ad95e543d2808bc43c17a3dd29d25b43d0f16287fe562a0be89bf632454a12", 160 | "https://deno.land/std@0.170.0/fs/_util.ts": "fdc156f897197f261a1c096dcf8ff9267ed0ff42bd5b31f55053a4763a4bae3b", 161 | "https://deno.land/std@0.170.0/fs/copy.ts": "c6303e52f544c81271c929931f5b59c9cfa4f81930719d2d3f777188c38aac9f", 162 | "https://deno.land/std@0.170.0/fs/empty_dir.ts": "453d6232ff109f2afb5e57ec14c3228e399205c1b408d85536aed7230290c414", 163 | "https://deno.land/std@0.170.0/fs/ensure_dir.ts": "5e9e3d7da7fc5b5e391e6d9ccead17086d76e82fb46ccc7cc9b9ee3491bab6e0", 164 | "https://deno.land/std@0.170.0/fs/ensure_file.ts": "76ef3a8ebef60d8da1fc4316fcb8e20c1b6f52b1baed3a9692ad3b0d1a9a1b03", 165 | "https://deno.land/std@0.170.0/fs/ensure_link.ts": "adc8919063e26819f5971a0010fedc1bfd71d6350a24db1a36dff432bc35c7d7", 166 | "https://deno.land/std@0.170.0/fs/ensure_symlink.ts": "5273557b8c50be69477aa9cb003b54ff2240a336db52a40851c97abce76b96ab", 167 | "https://deno.land/std@0.170.0/fs/eol.ts": "6e784ff8120c8d5589cb258e56dc39bc5b408ac9827a2e914163cbf9f2e3ce92", 168 | "https://deno.land/std@0.170.0/fs/exists.ts": "6a447912e49eb79cc640adacfbf4b0baf8e17ede6d5bed057062ce33c4fa0d68", 169 | "https://deno.land/std@0.170.0/fs/expand_glob.ts": "3a92ee4921d2b063b8dfefd1d87c35bf81126f0f1cb16e5a0f4e9ecb88ec6fe3", 170 | "https://deno.land/std@0.170.0/fs/mod.ts": "79c209c6e66903b3426f9245a4f216380a0ed47ffe9d253f5a61a0bc9ad1f314", 171 | "https://deno.land/std@0.170.0/fs/move.ts": "02ab1fc9b744da8b496f406e9fc77b0bf7960b6faaa7ec9f5fb0a129e5bef215", 172 | "https://deno.land/std@0.170.0/fs/walk.ts": "677eac2e5386217a7a4e7526769ae28b41ff4ae7a3cd0389f3aa4eb662545edd", 173 | "https://deno.land/std@0.170.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", 174 | "https://deno.land/std@0.170.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", 175 | "https://deno.land/std@0.170.0/path/_util.ts": "d16be2a16e1204b65f9d0dfc54a9bc472cafe5f4a190b3c8471ec2016ccd1677", 176 | "https://deno.land/std@0.170.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", 177 | "https://deno.land/std@0.170.0/path/glob.ts": "81cc6c72be002cd546c7a22d1f263f82f63f37fe0035d9726aa96fc8f6e4afa1", 178 | "https://deno.land/std@0.170.0/path/mod.ts": "cf7cec7ac11b7048bb66af8ae03513e66595c279c65cfa12bfc07d9599608b78", 179 | "https://deno.land/std@0.170.0/path/posix.ts": "b859684bc4d80edfd4cad0a82371b50c716330bed51143d6dcdbe59e6278b30c", 180 | "https://deno.land/std@0.170.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", 181 | "https://deno.land/std@0.170.0/path/win32.ts": "7cebd2bda6657371adc00061a1d23fdd87bcdf64b4843bb148b0b24c11b40f69", 182 | "https://deno.land/std@0.170.0/testing/_diff.ts": "a23e7fc2b4d8daa3e158fa06856bedf5334ce2a2831e8bf9e509717f455adb2c", 183 | "https://deno.land/std@0.170.0/testing/_format.ts": "cd11136e1797791045e639e9f0f4640d5b4166148796cad37e6ef75f7d7f3832", 184 | "https://deno.land/std@0.170.0/testing/_test_suite.ts": "2d07073d5460a4e3ec50c55ae822cd9bd136926d7363091379947fef9c73c3e4", 185 | "https://deno.land/std@0.170.0/testing/asserts.ts": "51353e79437361d4b02d8e32f3fc83b22231bc8f8d4c841d86fd32b0b0afe940", 186 | "https://deno.land/std@0.170.0/testing/bdd.ts": "9c2cd8473968e14fa8fe29f261c607aac0350ab5a63bc4f5650b8d1554372c5f", 187 | "https://deno.land/std@0.170.0/testing/mock.ts": "e19bf1f42b4b0aa7bc85d1f3aa01ed6d99368347c8df2d38e33d177a3976718b", 188 | "https://deno.land/std@0.170.0/testing/snapshot.ts": "721480723b386ce4c4082de51892ecbdf6a900ef5d8878f78d15938165b251f4", 189 | "https://deno.land/std@0.175.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", 190 | "https://deno.land/std@0.175.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", 191 | "https://deno.land/std@0.175.0/async/abortable.ts": "73acfb3ed7261ce0d930dbe89e43db8d34e017b063cf0eaa7d215477bf53442e", 192 | "https://deno.land/std@0.175.0/async/deadline.ts": "b98e50d2c42399af03ad13bbb8cf59dadb9f0cd5d70648cc0c3b9202d75ab565", 193 | "https://deno.land/std@0.175.0/async/debounce.ts": "adab11d04ca38d699444ac8a9d9856b4155e8dda2afd07ce78276c01ea5a4332", 194 | "https://deno.land/std@0.175.0/async/deferred.ts": "42790112f36a75a57db4a96d33974a936deb7b04d25c6084a9fa8a49f135def8", 195 | "https://deno.land/std@0.175.0/async/delay.ts": "73aa04cec034c84fc748c7be49bb15cac3dd43a57174bfdb7a4aec22c248f0dd", 196 | "https://deno.land/std@0.175.0/async/mod.ts": "f04344fa21738e5ad6bea37a6bfffd57c617c2d372bb9f9dcfd118a1b622e576", 197 | "https://deno.land/std@0.175.0/async/mux_async_iterator.ts": "70c7f2ee4e9466161350473ad61cac0b9f115cff4c552eaa7ef9d50c4cbb4cc9", 198 | "https://deno.land/std@0.175.0/async/pool.ts": "fd082bd4aaf26445909889435a5c74334c017847842ec035739b4ae637ae8260", 199 | "https://deno.land/std@0.175.0/async/retry.ts": "5efa3ba450ac0c07a40a82e2df296287b5013755d232049efd7ea2244f15b20f", 200 | "https://deno.land/std@0.175.0/async/tee.ts": "47e42d35f622650b02234d43803d0383a89eb4387e1b83b5a40106d18ae36757", 201 | "https://deno.land/std@0.175.0/bytes/index_of_needle.ts": "65c939607df609374c4415598fa4dad04a2f14c4d98cd15775216f0aaf597f24", 202 | "https://deno.land/std@0.175.0/crypto/timing_safe_equal.ts": "8d69ab611c67fe51b6127d97fcfb4d8e7d0e1b6b4f3e0cc4ab86744c3691f965", 203 | "https://deno.land/std@0.175.0/encoding/base64.ts": "7de04c2f8aeeb41453b09b186480be90f2ff357613b988e99fabb91d2eeceba1", 204 | "https://deno.land/std@0.175.0/encoding/base64url.ts": "3f1178f6446834457b16bfde8b559c1cd3481727fe384d3385e4a9995dc2d851", 205 | "https://deno.land/std@0.175.0/flags/mod.ts": "d1cdefa18472ef69858a17df5cf7c98445ed27ac10e1460183081303b0ebc270", 206 | "https://deno.land/std@0.175.0/node/_core.ts": "32a72a2166688d0051db7771e76af823d87a19e84ed073604a0bf4fa2706403f", 207 | "https://deno.land/std@0.175.0/node/_events.d.ts": "1347437fd6b084d7c9a4e16b9fe7435f00b030970086482edeeb3b179d0775af", 208 | "https://deno.land/std@0.175.0/node/_events.mjs": "d4ba4e629abe3db9f1b14659fd5c282b7da8b2b95eaf13238eee4ebb142a2448", 209 | "https://deno.land/std@0.175.0/node/_global.d.ts": "2d88342f38b4083b858998e27c706725fb03a74aa14ef8d985dc18438b5188e4", 210 | "https://deno.land/std@0.175.0/node/_next_tick.ts": "9a3cf107d59b019a355d3cf32275b4c6157282e4b68ea85b46a799cb1d379305", 211 | "https://deno.land/std@0.175.0/node/_process/exiting.ts": "6e336180aaabd1192bf99ffeb0d14b689116a3dec1dfb34a2afbacd6766e98ab", 212 | "https://deno.land/std@0.175.0/node/_process/process.ts": "c96bb1f6253824c372f4866ee006dcefda02b7050d46759736e403f862d91051", 213 | "https://deno.land/std@0.175.0/node/_process/stdio.mjs": "cf17727eac8da3a665851df700b5aca6a12bacc3ebbf33e63e4b919f80ba44a6", 214 | "https://deno.land/std@0.175.0/node/_process/streams.mjs": "c1461c4dbf963a93a0ca8233467573a685bbde347562573761cc9435fd7080f6", 215 | "https://deno.land/std@0.175.0/node/_stream.d.ts": "112e1a0677cd6db932c3ce0e6e5bbdc7a2ac1874572f449044ecc82afcf5ee2e", 216 | "https://deno.land/std@0.175.0/node/_stream.mjs": "d6e2c86c1158ac65b4c2ca4fa019d7e84374ff12e21e2175345fe68c0823efe3", 217 | "https://deno.land/std@0.175.0/node/_utils.ts": "7fd55872a0cf9275e3c080a60e2fa6d45b8de9e956ebcde9053e72a344185884", 218 | "https://deno.land/std@0.175.0/node/buffer.ts": "85617be2063eccaf177dbb84c7580d1e32023724ed14bd9df4e453b152a26167", 219 | "https://deno.land/std@0.175.0/node/events.ts": "d2de352d509de11a375e2cb397d6b98f5fed4e562fc1d41be33214903a38e6b0", 220 | "https://deno.land/std@0.175.0/node/internal/buffer.d.ts": "bdfa991cd88cb02fd08bf8235d2618550e3e511c970b2a8f2e1a6885a2793cac", 221 | "https://deno.land/std@0.175.0/node/internal/buffer.mjs": "e92303a3cc6d9aaabcd270a937ad9319825d9ba08cb332650944df4562029b27", 222 | "https://deno.land/std@0.175.0/node/internal/crypto/_keys.ts": "8f3c3b5a141aa0331a53c205e9338655f1b3b307a08085fd6ff6dda6f7c4190b", 223 | "https://deno.land/std@0.175.0/node/internal/crypto/constants.ts": "544d605703053218499b08214f2e25cf4310651d535b7ab995891c4b7a217693", 224 | "https://deno.land/std@0.175.0/node/internal/error_codes.ts": "8495e33f448a484518d76fa3d41d34fc20fe03c14b30130ad8e936b0035d4b8b", 225 | "https://deno.land/std@0.175.0/node/internal/errors.ts": "1c699b8a3cb93174f697a348c004b1c6d576b66688eac8a48ebb78e65c720aae", 226 | "https://deno.land/std@0.175.0/node/internal/fixed_queue.ts": "62bb119afa5b5ae8fc0c7048b50502347bec82e2588017d0b250c4671d6eff8f", 227 | "https://deno.land/std@0.175.0/node/internal/hide_stack_frames.ts": "9dd1bad0a6e62a1042ce3a51eb1b1ecee2f246907bff44835f86e8f021de679a", 228 | "https://deno.land/std@0.175.0/node/internal/net.ts": "5538d31b595ac63d4b3e90393168bc65ace2f332c3317cffa2fd780070b2d86c", 229 | "https://deno.land/std@0.175.0/node/internal/normalize_encoding.mjs": "fd1d9df61c44d7196432f6e8244621468715131d18cc79cd299fc78ac549f707", 230 | "https://deno.land/std@0.175.0/node/internal/options.ts": "888f267c3fe8f18dc7b2f2fbdbe7e4a0fd3302ff3e99f5d6645601e924f3e3fb", 231 | "https://deno.land/std@0.175.0/node/internal/primordials.mjs": "a72d86b5aa55d3d50b8e916b6a59b7cc0dc5a31da8937114b4a113ad5aa08c74", 232 | "https://deno.land/std@0.175.0/node/internal/process/per_thread.mjs": "10142bbb13978c2f8f79778ad90f3a67a8ea6d8d2970f3dfc6bf2c6fff0162a2", 233 | "https://deno.land/std@0.175.0/node/internal/readline/callbacks.mjs": "bdb129b140c3b21b5e08cdc3d8e43517ad818ac03f75197338d665cca1cbaed3", 234 | "https://deno.land/std@0.175.0/node/internal/readline/utils.mjs": "c3dbf3a97c01ed14052cca3848f09e2fc24818c1822ceed57c33b9f0840f3b87", 235 | "https://deno.land/std@0.175.0/node/internal/streams/destroy.mjs": "b665fc71178919a34ddeac8389d162a81b4bc693ff7dc2557fa41b3a91011967", 236 | "https://deno.land/std@0.175.0/node/internal/streams/end-of-stream.mjs": "a4fb1c2e32d58dff440d4e716e2c4daaa403b3095304a028bb428575cfeed716", 237 | "https://deno.land/std@0.175.0/node/internal/streams/utils.mjs": "f2fe2e6bdc506da24c758970890cc2a21642045b129dee618bd3827c60dd9e33", 238 | "https://deno.land/std@0.175.0/node/internal/util.mjs": "f7fe2e1ca5e66f550ad0856b9f5ee4d666f0c071fe212ea7fc7f37cfa81f97a5", 239 | "https://deno.land/std@0.175.0/node/internal/util/inspect.mjs": "11d7c9cab514b8e485acc3978c74b837263ff9c08ae4537fa18ad56bae633259", 240 | "https://deno.land/std@0.175.0/node/internal/util/types.ts": "4f3625ea39111eaae1443c834e769b0c5ce9ea33b31d5a853b02af6a78105178", 241 | "https://deno.land/std@0.175.0/node/internal/validators.mjs": "e02f2b02dd072a5d623970292588d541204dc82207b4c58985d933a5f4b382e6", 242 | "https://deno.land/std@0.175.0/node/internal_binding/_libuv_winerror.ts": "30c9569603d4b97a1f1a034d88a3f74800d5ea1f12fcc3d225c9899d4e1a518b", 243 | "https://deno.land/std@0.175.0/node/internal_binding/_listen.ts": "c6038be47116f7755c01fd98340a0d1e8e66ef874710ab59ed3f5607d50d7a25", 244 | "https://deno.land/std@0.175.0/node/internal_binding/_node.ts": "cb2389b0eab121df99853eb6a5e3a684e4537e065fb8bf2cca0cbf219ce4e32e", 245 | "https://deno.land/std@0.175.0/node/internal_binding/_timingSafeEqual.ts": "7d9732464d3c669ff07713868ce5d25bc974a06112edbfb5f017fc3c70c0853e", 246 | "https://deno.land/std@0.175.0/node/internal_binding/_utils.ts": "7c58a2fbb031a204dee9583ba211cf9c67922112fe77e7f0b3226112469e9fe1", 247 | "https://deno.land/std@0.175.0/node/internal_binding/_winerror.ts": "3e8cfdfe22e89f13d2b28529bab35155e6b1730c0221ec5a6fc7077dc037be13", 248 | "https://deno.land/std@0.175.0/node/internal_binding/ares.ts": "bdd34c679265a6c115a8cfdde000656837a0a0dcdb0e4c258e622e136e9c31b8", 249 | "https://deno.land/std@0.175.0/node/internal_binding/async_wrap.ts": "0dc5ae64eea2c9e57ab17887ef1573922245167ffe38e3685c28d636f487f1b7", 250 | "https://deno.land/std@0.175.0/node/internal_binding/buffer.ts": "31729e0537921d6c730ad0afea44a7e8a0a1044d070ade8368226cb6f7390c8b", 251 | "https://deno.land/std@0.175.0/node/internal_binding/cares_wrap.ts": "9b7247772167f8ed56acd0244a232d9d50e8d7c9cfc379f77f3d54cecc2f32ab", 252 | "https://deno.land/std@0.175.0/node/internal_binding/config.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 253 | "https://deno.land/std@0.175.0/node/internal_binding/connection_wrap.ts": "7dd089ea46de38e4992d0f43a09b586e4cf04878fb06863c1cb8cb2ece7da521", 254 | "https://deno.land/std@0.175.0/node/internal_binding/constants.ts": "21ff9d1ee71d0a2086541083a7711842fc6ae25e264dbf45c73815aadce06f4c", 255 | "https://deno.land/std@0.175.0/node/internal_binding/contextify.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 256 | "https://deno.land/std@0.175.0/node/internal_binding/credentials.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 257 | "https://deno.land/std@0.175.0/node/internal_binding/crypto.ts": "29e8f94f283a2e7d4229d3551369c6a40c2af9737fad948cb9be56bef6c468cd", 258 | "https://deno.land/std@0.175.0/node/internal_binding/errors.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 259 | "https://deno.land/std@0.175.0/node/internal_binding/fs.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 260 | "https://deno.land/std@0.175.0/node/internal_binding/fs_dir.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 261 | "https://deno.land/std@0.175.0/node/internal_binding/fs_event_wrap.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 262 | "https://deno.land/std@0.175.0/node/internal_binding/handle_wrap.ts": "adf0b8063da2c54f26edd5e8ec50296a4d38e42716a70a229f14654b17a071d9", 263 | "https://deno.land/std@0.175.0/node/internal_binding/heap_utils.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 264 | "https://deno.land/std@0.175.0/node/internal_binding/http_parser.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 265 | "https://deno.land/std@0.175.0/node/internal_binding/icu.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 266 | "https://deno.land/std@0.175.0/node/internal_binding/inspector.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 267 | "https://deno.land/std@0.175.0/node/internal_binding/js_stream.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 268 | "https://deno.land/std@0.175.0/node/internal_binding/messaging.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 269 | "https://deno.land/std@0.175.0/node/internal_binding/mod.ts": "9fc65f7af1d35e2d3557539a558ea9ad7a9954eefafe614ad82d94bddfe25845", 270 | "https://deno.land/std@0.175.0/node/internal_binding/module_wrap.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 271 | "https://deno.land/std@0.175.0/node/internal_binding/native_module.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 272 | "https://deno.land/std@0.175.0/node/internal_binding/natives.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 273 | "https://deno.land/std@0.175.0/node/internal_binding/node_file.ts": "21edbbc95653e45514aff252b6cae7bf127a4338cbc5f090557d258aa205d8a5", 274 | "https://deno.land/std@0.175.0/node/internal_binding/node_options.ts": "0b5cb0bf4379a39278d7b7bb6bb2c2751baf428fe437abe5ed3e8441fae1f18b", 275 | "https://deno.land/std@0.175.0/node/internal_binding/options.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 276 | "https://deno.land/std@0.175.0/node/internal_binding/os.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 277 | "https://deno.land/std@0.175.0/node/internal_binding/performance.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 278 | "https://deno.land/std@0.175.0/node/internal_binding/pipe_wrap.ts": "e5429879551fb7195039986fe6da920a86971fad4342046cbf653643e6c85e21", 279 | "https://deno.land/std@0.175.0/node/internal_binding/process_methods.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 280 | "https://deno.land/std@0.175.0/node/internal_binding/report.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 281 | "https://deno.land/std@0.175.0/node/internal_binding/serdes.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 282 | "https://deno.land/std@0.175.0/node/internal_binding/signal_wrap.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 283 | "https://deno.land/std@0.175.0/node/internal_binding/spawn_sync.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 284 | "https://deno.land/std@0.175.0/node/internal_binding/stream_wrap.ts": "452bff74d1db280a0cd78c75a95bb6d163e849e06e9638c4af405d40296bd050", 285 | "https://deno.land/std@0.175.0/node/internal_binding/string_decoder.ts": "54c3c1cbd5a9254881be58bf22637965dc69535483014dab60487e299cb95445", 286 | "https://deno.land/std@0.175.0/node/internal_binding/symbols.ts": "4dee2f3a400d711fd57fa3430b8de1fdb011e08e260b81fef5b81cc06ed77129", 287 | "https://deno.land/std@0.175.0/node/internal_binding/task_queue.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 288 | "https://deno.land/std@0.175.0/node/internal_binding/tcp_wrap.ts": "cbede7224fcf0adc4b04e2e1222488a7a9c137807f143bd32cc8b1a121e0d4fa", 289 | "https://deno.land/std@0.175.0/node/internal_binding/timers.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 290 | "https://deno.land/std@0.175.0/node/internal_binding/tls_wrap.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 291 | "https://deno.land/std@0.175.0/node/internal_binding/trace_events.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 292 | "https://deno.land/std@0.175.0/node/internal_binding/tty_wrap.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 293 | "https://deno.land/std@0.175.0/node/internal_binding/types.ts": "5a658bf08975af30d0fad6fa6247274379be26ba3f023425bec03e61c74083ef", 294 | "https://deno.land/std@0.175.0/node/internal_binding/udp_wrap.ts": "cc86f7e51bf56fd619505cf9d4f77d7aae1526abdf295399dd277162d28ca6c1", 295 | "https://deno.land/std@0.175.0/node/internal_binding/url.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 296 | "https://deno.land/std@0.175.0/node/internal_binding/util.ts": "808ff3b92740284184ab824adfc420e75398c88c8bccf5111f0c24ac18c48f10", 297 | "https://deno.land/std@0.175.0/node/internal_binding/uv.ts": "eb0048e30af4db407fb3f95563e30d70efd6187051c033713b0a5b768593a3a3", 298 | "https://deno.land/std@0.175.0/node/internal_binding/v8.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 299 | "https://deno.land/std@0.175.0/node/internal_binding/worker.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 300 | "https://deno.land/std@0.175.0/node/internal_binding/zlib.ts": "37d293009d1718205bf28e878e54a9f1ca24c1c320cee2416c20dc054104c6ea", 301 | "https://deno.land/std@0.175.0/node/process.ts": "6608012d6d51a17a7346f36079c574b9b9f81f1b5c35436489ad089f39757466", 302 | "https://deno.land/std@0.175.0/node/stream.ts": "09e348302af40dcc7dc58aa5e40fdff868d11d8d6b0cfb85cbb9c75b9fe450c7", 303 | "https://deno.land/std@0.175.0/node/string_decoder.ts": "1a17e3572037c512cc5fc4b29076613e90f225474362d18da908cb7e5ccb7e88", 304 | "https://deno.land/std@0.175.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", 305 | "https://deno.land/std@0.175.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", 306 | "https://deno.land/std@0.175.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", 307 | "https://deno.land/std@0.175.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", 308 | "https://deno.land/std@0.175.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", 309 | "https://deno.land/std@0.175.0/path/mod.ts": "4b83694ac500d7d31b0cdafc927080a53dc0c3027eb2895790fb155082b0d232", 310 | "https://deno.land/std@0.175.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", 311 | "https://deno.land/std@0.175.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", 312 | "https://deno.land/std@0.175.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", 313 | "https://deno.land/std@0.175.0/streams/write_all.ts": "3b2e1ce44913f966348ce353d02fa5369e94115181037cd8b602510853ec3033", 314 | "https://deno.land/std@0.175.0/types.d.ts": "220ed56662a0bd393ba5d124aa6ae2ad36a00d2fcbc0e8666a65f4606aaa9784", 315 | "https://deno.land/x/bext@v0.1.2/mock_browser/main.ts": "58a817b10d52d151adf12d02131c9e17271f7476dc8813aacadbe5a1d6ab12a7", 316 | "https://deno.land/x/bext@v0.1.2/mod.ts": "e314c55b7ffa8639e086a1bc9819416e9cafb07ad4f960850ad2a448eaa9c5c2", 317 | "https://deno.land/x/bext@v0.1.2/types/browser_api.ts": "03d759262f879bb39bf5e1937231d1c2bab190d832956676a3d14b26728ff1b7", 318 | "https://deno.land/x/bext@v0.1.2/types/browser_api_modules/event.ts": "aba4560d7f6c7ecb9831d594621b9e344a488c39ad667936e114803491a8b3ee", 319 | "https://deno.land/x/bext@v0.1.2/types/browser_api_modules/manifest.ts": "ad032694be806d3d100eb685ccc39d03850d6d3eaf10a3061247edf51dbc4131", 320 | "https://deno.land/x/bext@v0.1.2/types/browser_api_modules/permissions.ts": "c7b62554e3aa46bcdaf3c9b39580a204ac48c1abf8158ca11d736e70abb44a68", 321 | "https://deno.land/x/bext@v0.1.2/types/browser_api_modules/platform.ts": "35a745dbc586ae967aca8388c0d5423d0b45f7cc48f23f20d778fa86c597d318", 322 | "https://deno.land/x/bext@v0.1.2/types/browser_api_modules/runtime.ts": "defc23fb34396f1c87de682e7bc36f03e0efa096a0d42b77db0bf730ff14c0da", 323 | "https://deno.land/x/bext@v0.1.2/types/browser_api_modules/storage.ts": "7c17ca08ea2ab7d9b0abbb12ae8c3e8fb133ebfad77e3a83df6c5b013dbcbab1", 324 | "https://deno.land/x/bext@v0.1.2/types/browser_api_modules/tabs.ts": "b49b2c1c281fba39a6409fb5544b75bc4b1b5ce15c3b2bc7980320defdfeda8d", 325 | "https://deno.land/x/bext@v0.1.2/types/browser_api_modules/web_request.ts": "bf236abf029f796d2b62c5d0dda17734a8dbc86f48c7e79ac6a8e3a3cc7da8e8", 326 | "https://deno.land/x/bext@v0.1.2/types/browser_api_modules/windows.ts": "2e93dcb8aa587673d5af62195470e852b73b08f0d88881d24276aaea502c9a87", 327 | "https://deno.land/x/bext@v0.1.2/types/manifest.ts": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 328 | "https://deno.land/x/bext@v0.1.2/utilities/predicates.ts": "4c6460bf27aa4b53f519bea3daa05df3ab1d6c13b1d7499343f396f70a478eca", 329 | "https://deno.land/x/bext@v1.0.0/mock_browser/main.ts": "7f70c9e88d0036b738eb55020add2ee3be001e4f947dd362224d7972155eaa71", 330 | "https://deno.land/x/bext@v1.0.0/mod.ts": "c6bf05b7d112bfbccc65b6bc57c28c76b6849e2f968b43c4b418b4ea1c443017", 331 | "https://deno.land/x/bext@v1.0.0/types/chrome.ts": "47bb8360629a17f0883d737cf92559f37b7bed1b01644bd0ca98870b10ab969a", 332 | "https://deno.land/x/bext@v1.0.0/utilities/predicates.ts": "4c6460bf27aa4b53f519bea3daa05df3ab1d6c13b1d7499343f396f70a478eca", 333 | "https://deno.land/x/deno_dom@v0.1.36-alpha/build/deno-wasm/deno-wasm.js": "3fa41dba4813e6d4b024a53a146b76e1afcbdf218fc02063442378c61239ed14", 334 | "https://deno.land/x/deno_dom@v0.1.36-alpha/deno-dom-wasm.ts": "bfd999a493a6974e9fca4d331bee03bfb68cfc600c662cd0b48b21d67a2a8ba0", 335 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/api.ts": "0ff5790f0a3eeecb4e00b7d8fbfa319b165962cf6d0182a65ba90f158d74f7d7", 336 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/constructor-lock.ts": "59714df7e0571ec7bd338903b1f396202771a6d4d7f55a452936bd0de9deb186", 337 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/deserialize.ts": "f4d34514ca00473ca428b69ad437ba345925744b5d791cb9552e2d7a0e7b0439", 338 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/dom/document-fragment.ts": "a40c6e18dd0efcf749a31552c1c9a6f7fa614452245e86ee38fc92ba0235e5ae", 339 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/dom/document.ts": "b8f4e4ccabaaa063d6562a0f2f8dea9c0419515d63d8bd79bfde95f7cd64bd93", 340 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/dom/dom-parser.ts": "609097b426f8c2358f3e5d2bca55ed026cf26cdf86562e94130dfdb0f2537f92", 341 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/dom/element.ts": "4a267c24d0e20b70741a14ab371a7511a4f3db682d3a1d229adaa66a46445fff", 342 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/dom/elements/html-template-element.ts": "19ad97c55222115e8daaca2788b9c98cc31a7f9d2547ed5bca0c56a4a12bfec8", 343 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/dom/html-collection.ts": "ae90197f5270c32074926ad6cf30ee07d274d44596c7e413c354880cebce8565", 344 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/dom/node-list.ts": "4c6e4b4585301d4147addaccd90cb5f5a80e8d6290a1ba7058c5e3dfea16e15d", 345 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/dom/node.ts": "3069e6fc93ac4111a136ed68199d76673339842b9751610ba06f111ba7dc10a7", 346 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/dom/selectors/custom-api.ts": "852696bd58e534bc41bd3be9e2250b60b67cd95fd28ed16b1deff1d548531a71", 347 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/dom/selectors/nwsapi-types.ts": "c43b36c36acc5d32caabaa54fda8c9d239b2b0fcbce9a28efb93c84aa1021698", 348 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/dom/selectors/nwsapi.js": "985d7d8fc1eabbb88946b47a1c44c1b2d4aa79ff23c21424219f1528fa27a2ff", 349 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/dom/selectors/selectors.ts": "83eab57be2290fb48e3130533448c93c6c61239f2a2f3b85f1917f80ca0fdc75", 350 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/dom/selectors/sizzle-types.ts": "78149e2502409989ce861ed636b813b059e16bc267bb543e7c2b26ef43e4798b", 351 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/dom/selectors/sizzle.js": "c3aed60c1045a106d8e546ac2f85cc82e65f62d9af2f8f515210b9212286682a", 352 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/dom/utils-types.ts": "96db30e3e4a75b194201bb9fa30988215da7f91b380fca6a5143e51ece2a8436", 353 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/dom/utils.ts": "ecd889ba74f3ce282620d8ca1d4d5e0365e6cc86101d2352f3bbf936ae496e2c", 354 | "https://deno.land/x/deno_dom@v0.1.36-alpha/src/parser.ts": "b65eb7e673fa7ca611de871de109655f0aa9fa35ddc1de73df1a5fc2baafc332", 355 | "https://deno.land/x/emoji@0.2.0/types.ts": "5897345f500088c719a35388c5b192bc5d93fc2c13e37d94eafa8ff3480edd17", 356 | "https://deno.land/x/emoji@0.2.1/all.json": "9beb741e050f6eaeab2e4529517a68fd55f4cde98d9a5008c5ef5537e4d2ddb5", 357 | "https://deno.land/x/emoji@0.2.1/emoji.ts": "59baa579fead2ea631cde96aefa03689ee2f1241ab76398965eddc5c66096047", 358 | "https://deno.land/x/emoji@0.2.1/mod.ts": "733bed20d9489f91fd16823bc0bff4943d995d7f1fdae6fea458b3e0927bde96", 359 | "https://deno.land/x/emoji@0.2.1/types.ts": "5897345f500088c719a35388c5b192bc5d93fc2c13e37d94eafa8ff3480edd17", 360 | "https://deno.land/x/emoji@0.2.1/unicode.ts": "ac8079e8e1da66ae9e601c1fdd0e7641120c2b07ca7bd2875e65fe23e16e6199", 361 | "https://esm.sh/@testing-library/preact@3.2.2/pure?dev": "4663aaabf8db67a77e369b0eebe465dd72558c42a34c08592707d8655fce739d", 362 | "https://esm.sh/@testing-library/preact@3.2.3/pure?dev": "cb60f47e46192012db9f5d06dc7dfac2790a47c6b729e8d842348b7499e68347", 363 | "https://esm.sh/preact@10.11.3/hooks?dev": "0f1eda4f2feebb3bf232362d90601388d46872fc75eaacd0a9f221ad0871a76c", 364 | "https://esm.sh/preact@10.11.3?dev": "31b76911403b1a8276741b424af05281b47e54217f58cd30e80d22aea3bb0e40", 365 | "https://esm.sh/stable/preact@10.11.3/deno/hooks.development.js": "606e18f2342a9d8c2e92d6492e7f747232191e9382c1fe0e6358bddfbadb1489", 366 | "https://esm.sh/stable/preact@10.11.3/deno/preact.development.js": "2d6e41505e2e5d5e894296aae9c3261c67a62f024d5d2703a051cba1aacf320d", 367 | "https://esm.sh/stable/preact@10.11.3/deno/test-utils.development.js": "f1b41a86e717d5e6f08c8f7efcb6273849559e527a3ab7ec044b223ff6f21bb7", 368 | "https://esm.sh/v102/preact@10.11.3/hooks/src/index.d.ts": "5c29febb624fc25d71cb0e125848c9b711e233337a08f7eacfade38fd4c14cc3", 369 | "https://esm.sh/v102/preact@10.11.3/src/index.d.ts": "76842a9d103548261ee001f2aee5fa5cacc8b1b4b4032af537ed84603c8f4a31", 370 | "https://esm.sh/v102/preact@10.11.3/src/jsx.d.ts": "77ce5bd7324455c9f6dc85bfc3c94b733acb92cc98fdf82e4b1059c8bca7866d", 371 | "https://esm.sh/v106/@testing-library/dom@8.20.0/deno/dom.development.js": "72be9b49305c7d617e45d4018a8831148e55812737f36b27ae041c7ef3663fc0", 372 | "https://esm.sh/v106/@testing-library/dom@8.20.0/types/config.d.ts": "2a90177ebaef25de89351de964c2c601ab54d6e3a157cba60d9cd3eaf5a5ee1a", 373 | "https://esm.sh/v106/@testing-library/dom@8.20.0/types/events.d.ts": "1d2699a343a347a830be26eb17ab340d7875c6f549c8d7477efb1773060cc7e5", 374 | "https://esm.sh/v106/@testing-library/dom@8.20.0/types/get-node-text.d.ts": "a0a6f0095f25f08a7129bc4d7cb8438039ec422dc341218d274e1e5131115988", 375 | "https://esm.sh/v106/@testing-library/dom@8.20.0/types/get-queries-for-element.d.ts": "61b0bd9a20e0738fd87e67017a69df89106f12e516fdd15ce0a889f7c60d479f", 376 | "https://esm.sh/v106/@testing-library/dom@8.20.0/types/index.d.ts": "561aeabb2e1fa95bc9d1f9153ccb5e8cd8fb7ffd5a412616c3cfb24dfa613d79", 377 | "https://esm.sh/v106/@testing-library/dom@8.20.0/types/matches.d.ts": "0c5b29970635b93dbaaed6c6bfe494d3213ad6e6712bd2d54e495acba60dc2f2", 378 | "https://esm.sh/v106/@testing-library/dom@8.20.0/types/pretty-dom.d.ts": "8fd476289df1370ab50325ba2365411b42a756acb261d45c01e06b48b2e79a2c", 379 | "https://esm.sh/v106/@testing-library/dom@8.20.0/types/queries.d.ts": "8008a67a579af89fe977900215159d1f470c1c2e2c6a5e638b6e9b053ca37f7a", 380 | "https://esm.sh/v106/@testing-library/dom@8.20.0/types/query-helpers.d.ts": "56f39bce2cd0e3f3cdcb4bea7175eddba13ee18b91aa3ecc5d42dadc5fb64ccc", 381 | "https://esm.sh/v106/@testing-library/dom@8.20.0/types/role-helpers.d.ts": "a3ce619711ff1bcdaaf4b5187d1e3f84e76064909a7c7ecb2e2f404f145b7b5c", 382 | "https://esm.sh/v106/@testing-library/dom@8.20.0/types/screen.d.ts": "e754ce8620710d706ce751e65ac1ad7080ddd33dc57d4dea6b53972b8de5064f", 383 | "https://esm.sh/v106/@testing-library/dom@8.20.0/types/suggestions.d.ts": "82200e963d3c767976a5a9f41ecf8c65eca14a6b33dcbe00214fcbe959698c46", 384 | "https://esm.sh/v106/@testing-library/dom@8.20.0/types/wait-for-element-to-be-removed.d.ts": "278ba90329ef4874f485dbd9f4e2ede0a71f0b10dbca0a6b0562d013f343d247", 385 | "https://esm.sh/v106/@testing-library/dom@8.20.0/types/wait-for.d.ts": "8387ec1601cf6b8948672537cf8d430431ba0d87b1f9537b4597c1ab8d3ade5b", 386 | "https://esm.sh/v106/@testing-library/preact@3.2.3/deno/pure.development.js": "f30fa0bb039f169a7a90ed3197e52ac6fcf23752c584b59f4577318170655e5f", 387 | "https://esm.sh/v106/@testing-library/preact@3.2.3/pure.d.ts": "f06991c4779f4d8b8e5f992db67130b140d81ab396f7c9633d4c046f4b74ee64", 388 | "https://esm.sh/v106/@testing-library/preact@3.2.3/types/index.d.ts": "ae54fa346140aecd177a1ae07395ece3e9bb1a3d367331b825c302c9f3f47a7d", 389 | "https://esm.sh/v106/@testing-library/preact@3.2.3/types/pure.d.ts": "9a187a4af742d488d6cc2c73e8aca58e0fe8977e5364920da39c95026539e363", 390 | "https://esm.sh/v106/@types/aria-query@5.0.1/index.d.ts": "21522c0f405e58c8dd89cd97eb3d1aa9865ba017fde102d01f86ab50b44e5610", 391 | "https://esm.sh/v106/ansi-regex@5.0.1/deno/ansi-regex.development.js": "87a6144552a9804b95172ad773709181d52779bbc364b08fa13a2a49e91bf4d9", 392 | "https://esm.sh/v106/ansi-styles@5.2.0/deno/ansi-styles.development.js": "8690a7f1fc87caccb37f999e70d3ded796b198332ac2233452088144d6be859f", 393 | "https://esm.sh/v106/aria-query@5.1.3/deno/aria-query.development.js": "1498e89659d6fc4156649aa171335e6c818ae1e76d17c40ac02b8d81c00e5151", 394 | "https://esm.sh/v106/available-typed-arrays@1.0.5/deno/available-typed-arrays.development.js": "27affe9d3ad2025cf4b8ae3869ab108ca41f5c2b426b6bcca82a54cb3df407f6", 395 | "https://esm.sh/v106/call-bind@1.0.2/deno/call-bind.development.js": "26f72e3a709bbbb15bc27c594096a9ce53e19cc61dcc73e408116bb74a6fe0e1", 396 | "https://esm.sh/v106/call-bind@1.0.2/deno/callBound.development.js": "c12f109de89fa6b47901ab1a8d27919dca8ee77e8cd38ec513171c826363310b", 397 | "https://esm.sh/v106/deep-equal@2.2.0/deno/deep-equal.development.js": "f72697d8f795765ab56cd189d498b1594d12e1f20fc80543b805a9e96d5740be", 398 | "https://esm.sh/v106/define-properties@1.1.4/deno/define-properties.development.js": "37d94bd7d0d3c771d32b538a7faf98577a2dcdb33a31b3c3825f2812687f0c13", 399 | "https://esm.sh/v106/dom-accessibility-api@0.5.16/deno/dom-accessibility-api.development.js": "0aff8a5bda697ef2c05f0c1adab89167d477db507795847a5a5606b1f6f12630", 400 | "https://esm.sh/v106/es-get-iterator@1.1.3/deno/es-get-iterator.development.js": "abd4850aea2f576c15be3a1467cafc01b8f75c9dacdd6f8f1ac337675b9d9237", 401 | "https://esm.sh/v106/for-each@0.3.3/deno/for-each.development.js": "68943e00cc650c2a20681092dd79e389562f6b4afa955beee9076a601fe3d1a8", 402 | "https://esm.sh/v106/function-bind@1.1.1/deno/function-bind.development.js": "cd9de632e5ba2672eaf9fc1d1cb49c2d55cedda19e0d685898293a8d18c45620", 403 | "https://esm.sh/v106/functions-have-names@1.2.3/deno/functions-have-names.development.js": "a3b90c058cf077f0b07a804c9d00f63bfe3cde419bcd65c2944d94768818b413", 404 | "https://esm.sh/v106/get-intrinsic@1.2.0/deno/get-intrinsic.development.js": "a6877f20b9e710645870eb374804242ebfd16a689d5e9d7fce03a4ebbaeccf11", 405 | "https://esm.sh/v106/gopd@1.0.1/deno/gopd.development.js": "f37ad7816ecc1f14c0e01f63084b433e133a408c0a252bc1e596bc17756287b4", 406 | "https://esm.sh/v106/has-bigints@1.0.2/deno/has-bigints.development.js": "49df6eafae2f85322f0bb5765d0131fd8ca4a0f131c7000871e1501bc16db14a", 407 | "https://esm.sh/v106/has-property-descriptors@1.0.0/deno/has-property-descriptors.development.js": "ccd0e2b2fc87337a267b9d5df0c85b7031f2eeaa1e5892605f49f83bbd2977e0", 408 | "https://esm.sh/v106/has-symbols@1.0.3/deno/has-symbols.development.js": "110118bad2fd9acdde377124818c2132d93529fd86c7fca6c1760457f8bf73f5", 409 | "https://esm.sh/v106/has-symbols@1.0.3/deno/shams.development.js": "286d6eeaf0af427c09c1c11cfee08d9a65445065569ee9a15fbeda8c40d23b59", 410 | "https://esm.sh/v106/has-tostringtag@1.0.0/deno/shams.development.js": "af260056f496dba5522ff309149136bf8dd5a7a200e2310cebd17deaa092d9f6", 411 | "https://esm.sh/v106/has@1.0.3/deno/has.development.js": "1f7424f10716a434a0e879840278fdd2ed56a6f9fe88ea68d1f340c8ea007144", 412 | "https://esm.sh/v106/internal-slot@1.0.4/deno/internal-slot.development.js": "9bd5e609cef45c5dd51bede79ea2407afe27f7f97f01812d0e484a1f3ec3e4ac", 413 | "https://esm.sh/v106/is-arguments@1.1.1/deno/is-arguments.development.js": "ad2a2dafafb92c80393dd8470aab2a62bbef22ab3a750594b38cd1edc2a0675c", 414 | "https://esm.sh/v106/is-array-buffer@3.0.1/deno/is-array-buffer.development.js": "66a2aaef44c3fa17c8550b220ad6c39aee68d99befcf1444a1298cd3a0d95d7a", 415 | "https://esm.sh/v106/is-bigint@1.0.4/deno/is-bigint.development.js": "de759d25bf7f7d67229275d7931c8008bab47b5b0186f84d68f54098def1e705", 416 | "https://esm.sh/v106/is-boolean-object@1.1.2/deno/is-boolean-object.development.js": "b04850ab5c0a75358f090dc45c1eb244aaac26a114361e5d830d05e7952d23c4", 417 | "https://esm.sh/v106/is-callable@1.2.7/deno/is-callable.development.js": "e111ff8b7a134610fe22d8d4f3a0597c24403ac4dbddc6fb3a33dd50fa149a13", 418 | "https://esm.sh/v106/is-date-object@1.0.5/deno/is-date-object.development.js": "1cb45e9daceb02c50856e17efa5b9b5933ddf7c5d91def137c6d46bb3570fb15", 419 | "https://esm.sh/v106/is-map@2.0.2/deno/is-map.development.js": "29b86dfd640b5916e3e769e2e95e0847f18a7c760b971ef589645e1f03c2d494", 420 | "https://esm.sh/v106/is-number-object@1.0.7/deno/is-number-object.development.js": "30539fe9e68c61539ded2d994efd59f392bed68ffd92fe40e209f511984857ed", 421 | "https://esm.sh/v106/is-regex@1.1.4/deno/is-regex.development.js": "9ee70ac25924cfd6dbee5c9f0c8ead90f0b707c3cd3cd72425ee410f497edbf1", 422 | "https://esm.sh/v106/is-set@2.0.2/deno/is-set.development.js": "1b9f696ff7ac470c8c2c627cd0ca472305cd3aca956b534cd9105380fa2ff99f", 423 | "https://esm.sh/v106/is-shared-array-buffer@1.0.2/deno/is-shared-array-buffer.development.js": "2c53b9b9b04bda2e810a34711466cfbe6731f59dc4537ec0ad983f5cc8614952", 424 | "https://esm.sh/v106/is-string@1.0.7/deno/is-string.development.js": "b01f2541b102a9a2494e6f6df10c390a8b434c8058742e92f6fa4cbbf65e2f09", 425 | "https://esm.sh/v106/is-symbol@1.0.4/deno/is-symbol.development.js": "4e6dda312348ad95d7616f4158038fb43062ed8b8239deddd8db73b2c4e7a586", 426 | "https://esm.sh/v106/is-typed-array@1.1.10/deno/is-typed-array.development.js": "648d38f996c8a3534f0aaf455b843b8c5b215c6ed17976864a5f6a944acbfed1", 427 | "https://esm.sh/v106/is-weakmap@2.0.1/deno/is-weakmap.development.js": "ae90e59ee2357054cbd0653a5292b28329e24425108306590241109a79c72385", 428 | "https://esm.sh/v106/is-weakset@2.0.2/deno/is-weakset.development.js": "01e9d9238760e41e753787a3703fac6c676c2accbca5984f3a58c9e1397ebac0", 429 | "https://esm.sh/v106/isarray@2.0.5/deno/isarray.development.js": "dfb96889eea6755738f78aa7b9d5624d63aee343f1a6a62783e00e78dd6486cd", 430 | "https://esm.sh/v106/lz-string@1.4.4/deno/lz-string.development.js": "f7556aedf06530ce9f7a334ca790964e74e36a0e5c75ee55578326f383fb8e34", 431 | "https://esm.sh/v106/object-inspect@1.12.3/deno/object-inspect.development.js": "1eb8342b86a3dd9e9fb76615f6a82ce98338f78b92538675849aa8bfb3281886", 432 | "https://esm.sh/v106/object-is@1.1.5/deno/object-is.development.js": "139e619cba5e575ce1b44974947e301f345bc78a61634c8c5c194913e042ab8c", 433 | "https://esm.sh/v106/object-keys@1.1.1/deno/object-keys.development.js": "2b7cf5fc12c79cc0afbe13c8fc8ff3083b348df037df1ee2fb03c54c00a50074", 434 | "https://esm.sh/v106/object.assign@4.1.4/deno/object.assign.development.js": "249bae8ce7532633ffe04d44e20608d50dcb140108a9d630041e5fd60ca9b005", 435 | "https://esm.sh/v106/preact@10.11.3/src/index.d.ts": "76842a9d103548261ee001f2aee5fa5cacc8b1b4b4032af537ed84603c8f4a31", 436 | "https://esm.sh/v106/preact@10.11.3/src/jsx.d.ts": "77ce5bd7324455c9f6dc85bfc3c94b733acb92cc98fdf82e4b1059c8bca7866d", 437 | "https://esm.sh/v106/preact@10.11.3/test-utils/src/index.d.ts": "e12f1adddaf2e426f662f7de8529e8657ff2b801f28cc9e0ef99cd66db013ad5", 438 | "https://esm.sh/v106/pretty-format@27.5.1/build/index.d.ts": "56a50e283257c7fc16958f9e31d8260262505551782a6afc31030970ff48775a", 439 | "https://esm.sh/v106/pretty-format@27.5.1/build/types.d.ts": "462bccdf75fcafc1ae8c30400c9425e1a4681db5d605d1a0edb4f990a54d8094", 440 | "https://esm.sh/v106/pretty-format@27.5.1/deno/pretty-format.development.js": "0933419cced4d7d79fae0062749a2d482ab4b2b5017ecb4788d44a4586e93e0a", 441 | "https://esm.sh/v106/react-is@17.0.2/deno/react-is.development.js": "8fa54284ed894869508ae85a9251c98078f97e074f5229fccfb235c39a7576d5", 442 | "https://esm.sh/v106/regexp.prototype.flags@1.4.3/deno/regexp.prototype.flags.development.js": "eeaf909e890b8b5b916599114fc5649ffa665bd44ee6a936172a43c4ac00a9dd", 443 | "https://esm.sh/v106/side-channel@1.0.4/deno/side-channel.development.js": "53d4a77853237023a191fe6d57d00b749fcc946c6fd89bfb132eaa89ca1517c3", 444 | "https://esm.sh/v106/stop-iteration-iterator@1.0.0/deno/stop-iteration-iterator.development.js": "84d4cb21ca35848e03f67a625b2da9f8093305778744b883fc22039b52dd928e", 445 | "https://esm.sh/v106/which-boxed-primitive@1.0.2/deno/which-boxed-primitive.development.js": "e926126e660d2233e20c1373b4988ccf808b8241e977c578a322f4e38e679a81", 446 | "https://esm.sh/v106/which-collection@1.0.1/deno/which-collection.development.js": "f2ffcfdd8956fa03bda3163579eacf111a15484b54479c33f320cb92cd52e719", 447 | "https://esm.sh/v106/which-typed-array@1.1.9/deno/which-typed-array.development.js": "a00a42661adaa1a49e4d4d7e13403f0088961c79ddd2e39ba534afac31586178", 448 | "https://esm.sh/v86/@babel/runtime@7.18.3/deno/helpers/esm/asyncToGenerator.development.js": "ba3fae631c209d49ababdf027ba01ddebf4c114d88bef7dd73fe70e4b4071949", 449 | "https://esm.sh/v86/@babel/runtime@7.18.3/deno/helpers/esm/extends.development.js": "a7f467c508d776c65118707d7de78d76120388bd6947ae7436d7a3252d695965", 450 | "https://esm.sh/v86/@babel/runtime@7.18.3/deno/helpers/esm/objectWithoutPropertiesLoose.development.js": "a22d90d9a22a0a3416a281a01471b9a3c18f8483ddbfde168e85d7bcba3c1a71", 451 | "https://esm.sh/v86/@babel/runtime@7.18.3/deno/regenerator.development.js": "26219424de0e754ee8591a6d1e89084905b2e3112d889a7e3c266a01d2db07b0", 452 | "https://esm.sh/v86/@testing-library/dom@8.14.0/deno/dom.development.js": "17e1b43bc47761a33282a01d9973cf8b567eb0eff9ab0bd5aec18a7422937155", 453 | "https://esm.sh/v86/@testing-library/dom@8.14.0/types/config.d.ts": "2a90177ebaef25de89351de964c2c601ab54d6e3a157cba60d9cd3eaf5a5ee1a", 454 | "https://esm.sh/v86/@testing-library/dom@8.14.0/types/events.d.ts": "cb3aaf306b5ff2ec718359e2e2244263c0b364c35759b1467c16caa113ccb849", 455 | "https://esm.sh/v86/@testing-library/dom@8.14.0/types/get-node-text.d.ts": "a0a6f0095f25f08a7129bc4d7cb8438039ec422dc341218d274e1e5131115988", 456 | "https://esm.sh/v86/@testing-library/dom@8.14.0/types/get-queries-for-element.d.ts": "61b0bd9a20e0738fd87e67017a69df89106f12e516fdd15ce0a889f7c60d479f", 457 | "https://esm.sh/v86/@testing-library/dom@8.14.0/types/index.d.ts": "561aeabb2e1fa95bc9d1f9153ccb5e8cd8fb7ffd5a412616c3cfb24dfa613d79", 458 | "https://esm.sh/v86/@testing-library/dom@8.14.0/types/matches.d.ts": "d432d9f06fa44227ba9b71501496ba678fbeb31dec108c79c81e899bb6f3b5d4", 459 | "https://esm.sh/v86/@testing-library/dom@8.14.0/types/pretty-dom.d.ts": "06d6c5f1d4d574f5a5fd68de3de86d957b8039c8f3e52766d335c11357cbe7aa", 460 | "https://esm.sh/v86/@testing-library/dom@8.14.0/types/queries.d.ts": "8008a67a579af89fe977900215159d1f470c1c2e2c6a5e638b6e9b053ca37f7a", 461 | "https://esm.sh/v86/@testing-library/dom@8.14.0/types/query-helpers.d.ts": "56f39bce2cd0e3f3cdcb4bea7175eddba13ee18b91aa3ecc5d42dadc5fb64ccc", 462 | "https://esm.sh/v86/@testing-library/dom@8.14.0/types/role-helpers.d.ts": "a3ce619711ff1bcdaaf4b5187d1e3f84e76064909a7c7ecb2e2f404f145b7b5c", 463 | "https://esm.sh/v86/@testing-library/dom@8.14.0/types/screen.d.ts": "858c5973eec077681094e59a0c09411ebd855e0002fcfd00865d944ed367b40b", 464 | "https://esm.sh/v86/@testing-library/dom@8.14.0/types/suggestions.d.ts": "82200e963d3c767976a5a9f41ecf8c65eca14a6b33dcbe00214fcbe959698c46", 465 | "https://esm.sh/v86/@testing-library/dom@8.14.0/types/wait-for-element-to-be-removed.d.ts": "278ba90329ef4874f485dbd9f4e2ede0a71f0b10dbca0a6b0562d013f343d247", 466 | "https://esm.sh/v86/@testing-library/dom@8.14.0/types/wait-for.d.ts": "8387ec1601cf6b8948672537cf8d430431ba0d87b1f9537b4597c1ab8d3ade5b", 467 | "https://esm.sh/v86/@testing-library/preact@3.2.2/deno/pure.development.js": "1f4f2eb0f031f00d374e73ec07c965a8ddc6166a2791aa88f35cfaebd36cc890", 468 | "https://esm.sh/v86/@testing-library/preact@3.2.2/pure.d.ts": "f06991c4779f4d8b8e5f992db67130b140d81ab396f7c9633d4c046f4b74ee64", 469 | "https://esm.sh/v86/@testing-library/preact@3.2.2/types/index.d.ts": "e2963855b548abe8e7f6863c248468900478c71ca5c7288d804cead50c2584ba", 470 | "https://esm.sh/v86/@testing-library/preact@3.2.2/types/pure.d.ts": "9a187a4af742d488d6cc2c73e8aca58e0fe8977e5364920da39c95026539e363", 471 | "https://esm.sh/v86/@types/aria-query@4.2.2/index.d.ts": "5024433f8da3a7968f6d12cffd32f2cefae4442a9ad1c965fa2d23342338b700", 472 | "https://esm.sh/v86/ansi-regex@5.0.1/deno/ansi-regex.development.js": "eecf5158096e31be2f7a375c266497125e56767da130c60e1aa91955c64d8423", 473 | "https://esm.sh/v86/ansi-styles@5.2.0/deno/ansi-styles.development.js": "77c2c8069555da69c56a2062c92480673f9a05abdcce42313c23fd11a2fdf330", 474 | "https://esm.sh/v86/aria-query@5.0.0/deno/aria-query.development.js": "1b15a58427b9eb82fe450bbe14b5960bac8de78b2aba08114b6999de23e6af27", 475 | "https://esm.sh/v86/dom-accessibility-api@0.5.14/deno/dom-accessibility-api.development.js": "c792a3e9164de14b6c423f2d3b484045f4e927bdab24e50f2a9e100c048ea246", 476 | "https://esm.sh/v86/lz-string@1.4.4/deno/lz-string.development.js": "2f4b7881bafe340edab1b3a3b71240d425959a2e9df7268351d7f6c30e6c7a34", 477 | "https://esm.sh/v86/preact@10.8.2/deno/hooks.development.js": "288a0e48f0e7d2d561d8707c13554d64d5be042fa2cfacd306dff735068cdcc0", 478 | "https://esm.sh/v86/preact@10.8.2/deno/preact.development.js": "6e28e6af1461748a583365a7a0df8da7dcd0239ca01e2d1a324d39c94dc8ab11", 479 | "https://esm.sh/v86/preact@10.8.2/deno/test-utils.development.js": "86143f47eb92fcd939089e5f8377868d0362146d96068113b2d988dd98a8465b", 480 | "https://esm.sh/v86/preact@10.8.2/src/index.d.ts": "d3e2e5125321d592d2458226f311de66411f2fa949e546cbaaefa533c4fb89e3", 481 | "https://esm.sh/v86/preact@10.8.2/src/jsx.d.ts": "c5d73e52438e7ce5f9333778aad2c38f5f45023c5035d0cc309badceb2ba2a55", 482 | "https://esm.sh/v86/preact@10.8.2/test-utils/src/index.d.ts": "e12f1adddaf2e426f662f7de8529e8657ff2b801f28cc9e0ef99cd66db013ad5", 483 | "https://esm.sh/v86/pretty-format@27.5.1/build/index.d.ts": "56a50e283257c7fc16958f9e31d8260262505551782a6afc31030970ff48775a", 484 | "https://esm.sh/v86/pretty-format@27.5.1/build/types.d.ts": "462bccdf75fcafc1ae8c30400c9425e1a4681db5d605d1a0edb4f990a54d8094", 485 | "https://esm.sh/v86/pretty-format@27.5.1/deno/pretty-format.development.js": "8c65aa70783dd996ffecacc3d86ca6888e07e298b29d559cc7764896f74a8ee9", 486 | "https://esm.sh/v86/react-is@17.0.2/deno/react-is.development.js": "4a63c7ec7713d3786b5403f5e82fe1208eec1b5aa1c61e4ef7f0277e183f3a52", 487 | "https://raw.githubusercontent.com/bpevs/bext/main/source/mock_browser/main.ts": "58a817b10d52d151adf12d02131c9e17271f7476dc8813aacadbe5a1d6ab12a7", 488 | "https://raw.githubusercontent.com/bpevs/bext/main/source/mod.ts": "e314c55b7ffa8639e086a1bc9819416e9cafb07ad4f960850ad2a448eaa9c5c2", 489 | "https://raw.githubusercontent.com/bpevs/bext/main/source/types/browser_api.ts": "03d759262f879bb39bf5e1937231d1c2bab190d832956676a3d14b26728ff1b7", 490 | "https://raw.githubusercontent.com/bpevs/bext/main/source/types/browser_api_modules/event.ts": "aba4560d7f6c7ecb9831d594621b9e344a488c39ad667936e114803491a8b3ee", 491 | "https://raw.githubusercontent.com/bpevs/bext/main/source/types/browser_api_modules/manifest.ts": "ad032694be806d3d100eb685ccc39d03850d6d3eaf10a3061247edf51dbc4131", 492 | "https://raw.githubusercontent.com/bpevs/bext/main/source/types/browser_api_modules/permissions.ts": "c7b62554e3aa46bcdaf3c9b39580a204ac48c1abf8158ca11d736e70abb44a68", 493 | "https://raw.githubusercontent.com/bpevs/bext/main/source/types/browser_api_modules/platform.ts": "35a745dbc586ae967aca8388c0d5423d0b45f7cc48f23f20d778fa86c597d318", 494 | "https://raw.githubusercontent.com/bpevs/bext/main/source/types/browser_api_modules/runtime.ts": "defc23fb34396f1c87de682e7bc36f03e0efa096a0d42b77db0bf730ff14c0da", 495 | "https://raw.githubusercontent.com/bpevs/bext/main/source/types/browser_api_modules/storage.ts": "7c17ca08ea2ab7d9b0abbb12ae8c3e8fb133ebfad77e3a83df6c5b013dbcbab1", 496 | "https://raw.githubusercontent.com/bpevs/bext/main/source/types/browser_api_modules/tabs.ts": "b49b2c1c281fba39a6409fb5544b75bc4b1b5ce15c3b2bc7980320defdfeda8d", 497 | "https://raw.githubusercontent.com/bpevs/bext/main/source/types/browser_api_modules/web_request.ts": "bf236abf029f796d2b62c5d0dda17734a8dbc86f48c7e79ac6a8e3a3cc7da8e8", 498 | "https://raw.githubusercontent.com/bpevs/bext/main/source/types/browser_api_modules/windows.ts": "2e93dcb8aa587673d5af62195470e852b73b08f0d88881d24276aaea502c9a87", 499 | "https://raw.githubusercontent.com/bpevs/bext/main/source/types/chrome.ts": "47bb8360629a17f0883d737cf92559f37b7bed1b01644bd0ca98870b10ab969a", 500 | "https://raw.githubusercontent.com/bpevs/bext/main/source/types/manifest.ts": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 501 | "https://raw.githubusercontent.com/bpevs/bext/main/source/utilities/predicates.ts": "4c6460bf27aa4b53f519bea3daa05df3ab1d6c13b1d7499343f396f70a478eca" 502 | }, 503 | "npm": { 504 | "specifiers": { 505 | "@types/chrome": "@types/chrome@0.0.210" 506 | }, 507 | "packages": { 508 | "@types/chrome@0.0.210": { 509 | "integrity": "sha512-VSjQu1k6a/rAfuqR1Gi/oxHZj4+t6+LG+GobNI3ZWI6DQ+fmphNSF6TrLHG6BYK2bXc9Gb4c1uXFKRRVLaGl5Q==", 510 | "dependencies": { 511 | "@types/filesystem": "@types/filesystem@0.0.32", 512 | "@types/har-format": "@types/har-format@1.2.10" 513 | } 514 | }, 515 | "@types/filesystem@0.0.32": { 516 | "integrity": "sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==", 517 | "dependencies": { 518 | "@types/filewriter": "@types/filewriter@0.0.29" 519 | } 520 | }, 521 | "@types/filewriter@0.0.29": { 522 | "integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==", 523 | "dependencies": {} 524 | }, 525 | "@types/har-format@1.2.10": { 526 | "integrity": "sha512-o0J30wqycjF5miWDKYKKzzOU1ZTLuA42HZ4HE7/zqTOc/jTLdQ5NhYWvsRQo45Nfi1KHoRdNhteSI4BAxTF1Pg==", 527 | "dependencies": {} 528 | } 529 | } 530 | } 531 | } --------------------------------------------------------------------------------