Emoji favicons for the web
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: fixes issues with firefox and regex

- fix issue where ff is auto-loading favicons on sites with favicons
- fix issue where regex strings are not properly parsed

+105 -52
+2 -1
source/components/list_input.tsx
··· 5 5 import { h } from 'preact'; 6 6 import { useCallback, useMemo } from 'preact/hooks'; 7 7 8 - import { isRegexString } from '../utilities/predicates.ts'; 8 + import { isRegexString } from '../utilities/regex_utils.ts'; 9 9 import EmojiSelector from './emoji_selector/mod.tsx'; 10 10 import Only from './only.tsx'; 11 11 ··· 36 36 'https://favioli.com', 37 37 '/fa.ioli$/', 38 38 '/favioli/', 39 + '/http:\\/\\//', 39 40 ]; 40 41 41 42 export default function ListInput({
+11 -10
source/content_script.ts
··· 9 9 */ 10 10 import browserAPI from 'browser'; 11 11 import appendFaviconLink from './utilities/append_favicon_link.ts'; 12 + import { parseRegExp } from './utilities/regex_utils.ts'; 13 + 14 + browserAPI.runtime.onMessage.addListener(({ emoji, shouldOverride }: { 15 + emoji: Emoji; 16 + shouldOverride: boolean; 17 + }) => { 18 + if (emoji) appendFaviconLink(emoji, { shouldOverride }); 19 + }); 12 20 13 21 /** 14 22 * Reload the webpage if new Favioli settings may have updated the favicon ··· 69 77 } 70 78 71 79 function includesCurrUrl({ matcher }: Favicon) { 72 - return (new RegExp(matcher)).test(location.href); 80 + const regex = parseRegExp(matcher); 81 + if (regex) return regex.test(location.href); 82 + return location.href.indexOf(matcher) != -1; 73 83 } 74 - 75 - browserAPI.runtime.onMessage.addListener(({ emoji, shouldOverride }: { 76 - emoji: Emoji; 77 - shouldOverride: boolean; 78 - }) => { 79 - if (emoji) { 80 - appendFaviconLink(emoji, { shouldOverride }); 81 - } 82 - });
+2 -1
source/manifest.json
··· 9 9 "content_scripts": [ 10 10 { 11 11 "matches": ["*://*/*"], 12 - "js": ["content_script.js"] 12 + "js": ["content_script.js"], 13 + "run_at": "document_idle" 13 14 } 14 15 ], 15 16 "options_page": "options.html",
+1 -1
source/models/settings.ts
··· 37 37 38 38 features: { 39 39 enableAutoselectorIncludeCountryFlags: false, 40 - enableFaviconAutofill: false, 40 + enableFaviconAutofill: true, 41 41 enableSiteIgnore: false, 42 42 enableOverrideAll: false, 43 43 },
-1
source/popup.tsx
··· 31 31 32 32 const siteList = (cache.siteList || []) 33 33 .filter(({ matcher }) => matcher !== origin); 34 - console.log(cache.siteList); 35 34 36 35 if (selectedFavicon && !hasQuickOverride) { 37 36 siteList.push({ ...selectedFavicon, matcher: origin });
-10
source/utilities/__tests__/predicates.test.ts
··· 1 - import { assertStrictEquals } from 'std/asserts'; 2 - import { it } from 'std/bdd'; 3 - 4 - import { isRegexString } from '../predicates.ts'; 5 - 6 - it('should check if a string is regex', () => { 7 - assertStrictEquals(isRegexString('/myurlorsomething'), false); 8 - assertStrictEquals(isRegexString('/myregex/aa'), false); 9 - assertStrictEquals(isRegexString('/myregex/'), true); 10 - });
+27
source/utilities/__tests__/regex_utils.test.ts
··· 1 + import { assertEquals, assertStrictEquals } from 'std/asserts'; 2 + import { describe, it } from 'std/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 + });
+29 -14
source/utilities/append_favicon_link.ts
··· 1 1 import type { Emoji } from '../models/emoji.ts'; 2 + import { isFirefox } from './predicates.ts'; 2 3 3 4 import { createFaviconURLFromChar, ICON_SIZE } from './image_helpers.ts'; 4 - import { isIconLink } from './predicates.ts'; 5 5 6 6 const head = document.getElementsByTagName('head')[0]; 7 7 let appendedFavicon: HTMLElement | null = null; 8 + 9 + // Cache `true`, to give site every opportunity 10 + let hasFavicon = false; 8 11 9 12 interface Options { 10 13 shouldOverride?: boolean; ··· 21 24 : createFaviconURLFromChar(emoji.emoji || ''); 22 25 23 26 if (!faviconURL) return; 24 - 25 27 if (shouldOverride) removeAllFaviconLinks(); 26 28 27 29 // Already appended favicon; just update 28 - if (appendedFavicon) { 29 - appendedFavicon.setAttribute('href', faviconURL); 30 - } else if (shouldOverride || await doesSiteHaveFavicon() === false) { 30 + if (appendedFavicon) return appendedFavicon.setAttribute('href', faviconURL); 31 + if (shouldOverride || !(await doesSiteHaveFavicon())) { 31 32 appendedFavicon = head.appendChild( 32 33 createLink(faviconURL, ICON_SIZE, 'image/png'), 33 34 ); ··· 38 39 function getAllIconLinks(): HTMLLinkElement[] { 39 40 return Array.prototype.slice 40 41 .call(document.getElementsByTagName('link')) 41 - .filter(isIconLink); 42 + .filter((link: HTMLLinkElement) => { 43 + return new RegExp(/icon/i).test(link.rel); 44 + }); 42 45 } 43 46 44 - async function doesSiteHaveFavicon() { 45 - const iconLinkFound = getAllIconLinks() 46 - .concat(createLink('/favicon.ico')) // Browsers fallback to favicon.ico 47 - .map(async ({ href }: HTMLLinkElement) => { 48 - if ((await fetch(href)).status < 400) return true; 49 - throw new Error('not found'); 47 + async function doesSiteHaveFavicon(): Promise<boolean> { 48 + if (hasFavicon) return hasFavicon; 49 + const iconLinks = getAllIconLinks(); 50 + 51 + // Browsers fallback to favicon.ico 52 + if (!isFirefox()) iconLinks.push(createLink('/favicon.ico')); 53 + 54 + const iconLinkURLs = iconLinks.map(({ href }: HTMLLinkElement) => href); 55 + const iconLinkFound = Array.from(new Set(iconLinkURLs)) 56 + .map(async (href: string): Promise<boolean> => { 57 + // For Firefox, don't test urls. They all fail for me 58 + // (Although it might be a setting on my browser. Maybe double-check) 59 + if (isFirefox()) return true; 60 + const result = await fetch(href); 61 + if (!result || result.status >= 400) throw new Error('not found'); 62 + return true; 50 63 }); 64 + 51 65 try { 52 - return await Promise.any(iconLinkFound); 66 + hasFavicon = hasFavicon || Boolean(await Promise.any(iconLinkFound)); 53 67 } catch { 54 - return false; 68 + /* Do Nothing*/ 55 69 } 70 + return hasFavicon; 56 71 } 57 72 58 73 // Removes all icon link tags
+11 -2
source/utilities/favicon_selector.ts
··· 1 1 import type { Favicon } from '../models/favicon.ts'; 2 2 import type { Settings } from '../models/settings.ts'; 3 3 import type Autoselector from './favicon_autoselector.ts'; 4 + import { parseRegExp } from './regex_utils.ts'; 4 5 5 6 /** 6 7 * Override Priority ··· 29 30 } 30 31 31 32 function listItemMatchesURL(url: string): (favicon: Favicon) => boolean { 32 - return (favicon: Favicon): boolean => 33 - favicon?.matcher ? new RegExp(favicon.matcher || '^$').test(url) : false; 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.indexOf(favicon.matcher) != -1; 42 + }; 34 43 }
-12
source/utilities/predicates.ts
··· 3 3 4 4 type Browser = 'FIREFOX' | 'CHROME'; 5 5 6 - // Checks whether a link is an icon rel 7 - export function isIconLink(link: HTMLLinkElement): boolean { 8 - return link.rel.toLowerCase().indexOf('icon') !== -1; 9 - } 10 - 11 - // Determines whether a string is in the shape of a regex 12 - export function isRegexString(filter: string): boolean { 13 - return filter.length > 2 && 14 - filter.startsWith('/') && 15 - filter.endsWith('/'); 16 - } 17 - 18 6 export const isChrome = (): boolean => isBrowser(CHROME); 19 7 export const isFirefox = (): boolean => isBrowser(FIREFOX); 20 8
+22
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 + }