diff --git a/extension/helpers.js b/extension/helpers.js index 0b6ca06..2cf637e 100644 --- a/extension/helpers.js +++ b/extension/helpers.js @@ -6,33 +6,56 @@ 'use strict'; -export const ERROR = Symbol(); +export const ERROR = Symbol(), + env = {}, + storage = {}, + fs = {}, + web = {}, + fmt = {}, + regexers = {}, + registry = {}; -export const env = {}; env.name = 'extension'; env.version = chrome.runtime.getManifest().version; - env.openEnhancerMenu = () => chrome.runtime.sendMessage({ action: 'openEnhancerMenu' }); env.focusNotion = () => chrome.runtime.sendMessage({ action: 'focusNotion' }); -/** - */ - -export const storage = {}; - -storage.set = (id, key, value) => - new Promise((res, rej) => chrome.storage.sync.set({ [`[${id}]${key}`]: value }, res)); -storage.get = (id, key, fallback = undefined) => +storage.get = (namespace, key = undefined, fallback = undefined) => new Promise((res, rej) => - chrome.storage.sync.get([`[${id}]${key}`], (values) => - res(values[`[${id}]${key}`] ?? fallback) - ) + chrome.storage.sync.get([namespace], async (values) => { + values = + values[namespace] && Object.getOwnPropertyNames(values[namespace]).length + ? values[namespace] + : await registry.defaults(namespace); + res((key ? values[key] : values) ?? fallback); + }) ); +storage.set = (namespace, key, value) => + new Promise(async (res, rej) => { + const values = await storage.get(namespace, undefined, {}); + chrome.storage.sync.set({ [namespace]: { ...values, [key]: value } }, res); + }); +storage.reset = (namespace) => + new Promise((res, rej) => chrome.storage.sync.set({ [namespace]: undefined }, res)); -/** - */ +fs.getJSON = (path) => + fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) => + res.json() + ); +fs.getText = (path) => + fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) => + res.text() + ); +fs.isFile = async (path) => { + try { + await fetch(chrome.runtime.getURL(path)); + return true; + } catch { + return false; + } +}; -export const web = {}; - -web.whenReady = (selectors = [], callback = () => {}) => { +web.whenReady = (selectors = []) => { return new Promise((res, rej) => { function onLoad() { let isReadyInt; @@ -40,7 +63,6 @@ web.whenReady = (selectors = [], callback = () => {}) => { function isReadyTest() { if (selectors.every((selector) => document.querySelector(selector))) { clearInterval(isReadyInt); - callback(); res(true); } } @@ -53,11 +75,12 @@ web.whenReady = (selectors = [], callback = () => {}) => { } else onLoad(); }); }; - -/** - * @param {string} html - * @returns HTMLElement - */ +web.loadStyleset = (path) => { + document.head.appendChild( + web.createElement(``) + ); + return true; +}; web.createElement = (html) => { const template = document.createElement('template'); template.innerHTML = html.includes('
{ }; web.escapeHtml = (str) => str - .replace(/&/g, '&') // (?![^\s]+;) + .replace(/&/g, '&') .replace(//g, '>') .replace(/'/g, ''') .replace(/"/g, '"'); - // why a tagged template? because it syntax highlights // https://marketplace.visualstudio.com/items?itemName=bierner.lit-html -web.html = (html, ...templates) => html.map((str) => str + (templates.shift() || '')).join(''); - -/** - * @param {string} sheet - */ -web.loadStyleset = (sheet) => { - document.head.appendChild( - web.createElement(``) - ); - return true; -}; +web.html = (html, ...templates) => html.map((str) => str + (templates.shift() ?? '')).join(''); /** * @param {array} keys @@ -119,10 +131,6 @@ web.hotkeyListener = (keys, callback) => { web._hotkeys.push({ keys, callback }); }; -/** - */ - -export const fmt = {}; - import './dep/prism.js'; fmt.Prism = Prism; fmt.Prism.manual = true; @@ -188,69 +196,36 @@ fmt.slugger = (heading, slugs = new Set()) => { return slug; }; -/** - */ - -export const fs = {}; - -/** - * @param {string} path - */ -fs.getJSON = (path) => - fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) => - res.json() - ); -fs.getText = (path) => - fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) => - res.text() - ); - -fs.isFile = async (path) => { - try { - await fetch(chrome.runtime.getURL(path)); - return true; - } catch { - return false; - } +regexers.uuid = (str) => { + const match = str.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); + if (match && match.length) return true; + error(`invalid uuid ${str}`); + return false; }; - -/** - */ - -export const regexers = { - uuid(str) { - const match = str.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); - if (match && match.length) return true; - error(`invalid uuid ${str}`); - return false; - }, - semver(str) { - const match = str.match( - /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/i - ); - if (match && match.length) return true; - error(`invalid semver ${str}`); - return false; - }, - email(str) { - const match = str.match( - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i - ); - if (match && match.length) return true; - error(`invalid email ${str}`); - return false; - }, - url(str) { - const match = str.match( - /^[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/i - ); - if (match && match.length) return true; - error(`invalid url ${str}`); - return false; - }, +regexers.semver = (str) => { + const match = str.match( + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/i + ); + if (match && match.length) return true; + error(`invalid semver ${str}`); + return false; +}; +regexers.email = (str) => { + const match = str.match( + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i + ); + if (match && match.length) return true; + error(`invalid email ${str}`); + return false; +}; +regexers.url = (str) => { + const match = str.match( + /^[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/i + ); + if (match && match.length) return true; + error(`invalid url ${str}`); + return false; }; - -/** - */ - -export const registry = {}; registry.validate = async (mod, err, check) => { let conditions = [ @@ -444,8 +419,33 @@ registry.validate = async (mod, err, check) => { } while (conditions.some((condition) => Array.isArray(condition))); return conditions; }; +registry.defaults = async (id) => { + const mod = (await registry.get()).find((mod) => mod.id === id); + if (!mod || !mod.options) return {}; + const defaults = {}; + for (const opt of mod.options) { + switch (opt.type) { + case 'toggle': + defaults[opt.key] = opt.value; + break; + case 'select': + defaults[opt.key] = opt.values[0]; + break; + case 'text': + defaults[opt.key] = opt.value; + break; + case 'number': + defaults[opt.key] = opt.value; + break; + case 'file': + defaults[opt.key] = undefined; + break; + } + } + return defaults; +}; -registry.get = async (callback = () => {}) => { +registry.get = async () => { registry._list = []; if (!registry._errors) registry._errors = []; for (const dir of await fs.getJSON('repo/registry.json')) { @@ -466,12 +466,9 @@ registry.get = async (callback = () => {}) => { err('invalid mod.json'); } } - callback(registry._list); return registry._list; }; - -registry.errors = async (callback = () => {}) => { +registry.errors = async () => { if (!registry._errors) await registry.get(); - callback(registry._errors); return registry._errors; }; diff --git a/extension/launcher.js b/extension/launcher.js index 5a716c4..2cb71e9 100644 --- a/extension/launcher.js +++ b/extension/launcher.js @@ -7,7 +7,7 @@ 'use strict'; import(chrome.runtime.getURL('helpers.js')).then(({ web, registry }) => { - web.whenReady([], async () => { + web.whenReady().then(async () => { for (let mod of await registry.get()) { for (let sheet of mod.css?.client || []) { web.loadStyleset(`repo/${mod._dir}/${sheet}`); diff --git a/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/client.css b/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/client.css index 0e4776d..78d1843 100644 --- a/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/client.css +++ b/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/client.css @@ -51,8 +51,6 @@ display: flex; } .enhancer--notifications > div > :last-child > div { - margin-left: auto; - margin-bottom: 2px; display: inline-flex; align-items: center; justify-content: center; diff --git a/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/client.js b/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/client.js index b48889e..d44ff4d 100644 --- a/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/client.js +++ b/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/client.js @@ -11,7 +11,7 @@ import { env, storage, web, fs } from '../../helpers.js'; const sidebarSelector = '#notion-app > div > div.notion-cursor-listener > div.notion-sidebar-container > div > div > div > div:nth-child(4)'; -web.whenReady([sidebarSelector], async () => { +web.whenReady([sidebarSelector]).then(async () => { const $enhancerSidebarElement = web.createElement( web.html`