helpers -> api, jsdoc comment them

This commit is contained in:
dragonwocky 2021-05-03 23:44:15 +10:00
parent af7e659c0e
commit 2671410fe9
8 changed files with 231 additions and 88 deletions

View File

@ -4,13 +4,9 @@
a rework of the enhancer and port to the browser as a chrome extension. a rework of the enhancer and port to the browser as a chrome extension.
- new: cross-environment helpers api - new: cross-environment and properly documented api to replace helpers
- new: cross-environment mod loader structure - new: cross-environment mod loader structure
#### todo
- jsdoc helpers?
**changelog below this point is a mix of the app enhancer and all mods.** **changelog below this point is a mix of the app enhancer and all mods.**
**above this, changelogs have been split: see the** **above this, changelogs have been split: see the**
**[app enhancer changelog](https://github.com/notion-enhancer/app/blob/dev/CHANGELOG.md)** **[app enhancer changelog](https://github.com/notion-enhancer/app/blob/dev/CHANGELOG.md)**

View File

@ -4,24 +4,36 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
/** @module notion-enhancer/api */
'use strict'; 'use strict';
export const ERROR = Symbol(), /** environment-specific methods and constants */
env = {}, export const env = {};
storage = {}, /** an error constant used in validation, distinct from null or undefined */
fs = {}, env.ERROR = Symbol();
web = {}, /** the environment/platform name code is currently being executed in */
fmt = {},
regexers = {},
registry = {};
env.name = 'extension'; env.name = 'extension';
/** all environments/platforms currently supported by the enhancer */
env.supported = ['linux', 'win32', 'darwin', 'extension']; env.supported = ['linux', 'win32', 'darwin', 'extension'];
/** the current version of the enhancer */
env.version = chrome.runtime.getManifest().version; env.version = chrome.runtime.getManifest().version;
/** open the enhancer's menu */
env.openEnhancerMenu = () => chrome.runtime.sendMessage({ action: 'openEnhancerMenu' }); env.openEnhancerMenu = () => chrome.runtime.sendMessage({ action: 'openEnhancerMenu' });
/** focus an active notion tab */
env.focusNotion = () => chrome.runtime.sendMessage({ action: 'focusNotion' }); env.focusNotion = () => chrome.runtime.sendMessage({ action: 'focusNotion' });
/** reload all notion and enhancer menu tabs to apply changes */
env.reloadTabs = () => chrome.runtime.sendMessage({ action: 'reloadTabs' }); env.reloadTabs = () => chrome.runtime.sendMessage({ action: 'reloadTabs' });
/** environment-specific data persistence */
export const storage = {};
/**
* get data persisted within an enhancer store
* @param {string} namespace - the name of the store, e.g. a mod id
* @param {string} [key] - the key being looked up
* @param {*} [fallback] - a default value if the key does not exist
* @returns {Promise} value ?? fallback
*/
storage.get = (namespace, key = undefined, fallback = undefined) => storage.get = (namespace, key = undefined, fallback = undefined) =>
new Promise((res, rej) => new Promise((res, rej) =>
chrome.storage.sync.get([namespace], async (values) => { chrome.storage.sync.get([namespace], async (values) => {
@ -35,43 +47,91 @@ storage.get = (namespace, key = undefined, fallback = undefined) =>
res((key ? values[key] : values) ?? fallback); res((key ? values[key] : values) ?? fallback);
}) })
); );
/**
* persist data to an enhancer store
* @param {string} namespace - the name of the store, e.g. a mod id
* @param {string} key - the key associated with the value
* @param {*} value - the data to save
*/
storage.set = (namespace, key, value) => { storage.set = (namespace, key, value) => {
storage._onChangeListeners.forEach((listener) =>
listener({ type: 'set', namespace, key, value })
);
return new Promise(async (res, rej) => { return new Promise(async (res, rej) => {
const values = await storage.get(namespace, undefined, {}); const values = await storage.get(namespace, undefined, {});
storage._onChangeListeners.forEach((listener) =>
listener({ type: 'set', namespace, key, new: value, old: values[key] })
);
chrome.storage.sync.set({ [namespace]: { ...values, [key]: value } }, res); chrome.storage.sync.set({ [namespace]: { ...values, [key]: value } }, res);
}); });
}; };
/**
* clear data from an enhancer store
* @param {string} namespace - the name of the store, e.g. a mod id
*/
storage.reset = (namespace) => { storage.reset = (namespace) => {
storage._onChangeListeners.forEach((listener) => storage._onChangeListeners.forEach((listener) =>
listener({ type: 'reset', namespace, key: undefined, value: undefined }) listener({ type: 'reset', namespace, key: undefined, new: undefined, old: undefined })
); );
return new Promise((res, rej) => chrome.storage.sync.set({ [namespace]: undefined }, res)); return new Promise((res, rej) => chrome.storage.sync.set({ [namespace]: undefined }, res));
}; };
storage._onChangeListeners = []; storage._onChangeListeners = [];
/**
* add an event listener for changes in storage
* @param {onStorageChangeCallback} listener - called whenever a change in
* storage is initiated from the current process
*/
storage.onChange = (listener) => { storage.onChange = (listener) => {
storage._onChangeListeners.push(listener); storage._onChangeListeners.push(listener);
}; };
/**
* @callback onStorageChangeCallback
* @param {object} event
* @param {string} event.type - 'set' or 'reset'
* @param {string} event.namespace- the name of the store, e.g. a mod id
* @param {string} [event.key] - the key associated with the changed value
* @param {string} [event.new] - the new value being persisted to the store
* @param {string} [event.old] - the previous value associated with the key
*/
/** environment-specific filesystem reading */
export const fs = {};
/**
* fetch and parse a json file's contents
* @param {string} path - a url or within-the-enhancer filepath
* @returns {object} the json value of the requested file as a js object
*/
fs.getJSON = (path) => fs.getJSON = (path) =>
fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) => fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) =>
res.json() res.json()
); );
/**
* fetch a text file's contents
* @param {string} path - a url or within-the-enhancer filepath
* @returns {object} the text content of the requested file
*/
fs.getText = (path) => fs.getText = (path) =>
fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) => fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) =>
res.text() res.text()
); );
/**
* check if a file exists
* @param {string} path - a url or within-the-enhancer filepath
* @returns {boolean} whether or not the file exists
*/
fs.isFile = async (path) => { fs.isFile = async (path) => {
try { try {
await fetch(chrome.runtime.getURL(path)); await fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path));
return true; return true;
} catch { } catch {
return false; return false;
} }
}; };
/** helpers for manipulation of a webpage */
export const web = {};
/**
* wait until a page is loaded and ready for modification
* @param {array} [selectors=[]] - wait for the existence fo elements that match these css selectors
* @returns {Promise} a promise that will resolve when the page is ready
*/
web.whenReady = (selectors = []) => { web.whenReady = (selectors = []) => {
return new Promise((res, rej) => { return new Promise((res, rej) => {
function onLoad() { function onLoad() {
@ -92,12 +152,26 @@ web.whenReady = (selectors = []) => {
} else onLoad(); } else onLoad();
}); });
}; };
/**
* loads/applies a css stylesheet to the page
* @param {string} path - a url or within-the-enhancer filepath
*/
web.loadStyleset = (path) => { web.loadStyleset = (path) => {
document.head.appendChild( document.head.appendChild(
web.createElement(`<link rel="stylesheet" href="${chrome.runtime.getURL(path)}">`) web.createElement(
web.html`<link rel="stylesheet" href="${
path.startsWith('https://') ? path : chrome.runtime.getURL(path)
}">`
)
); );
return true; return true;
}; };
/**
* create a html element from a string instead of separately
* creating the element and then applying attributes and appending children
* @param {string} html - the full html of an element inc. attributes and children
* @returns {Element} the constructed html element
*/
web.createElement = (html) => { web.createElement = (html) => {
const template = document.createElement('template'); const template = document.createElement('template');
template.innerHTML = html.includes('<pre') template.innerHTML = html.includes('<pre')
@ -108,6 +182,11 @@ web.createElement = (html) => {
.join(' '); .join(' ');
return template.content.firstElementChild; return template.content.firstElementChild;
}; };
/**
* replace special html characters with escaped versions
* @param {string} str
* @returns {string} escaped string
*/
web.escapeHtml = (str) => web.escapeHtml = (str) =>
str str
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')
@ -115,20 +194,27 @@ web.escapeHtml = (str) =>
.replace(/>/g, '&gt;') .replace(/>/g, '&gt;')
.replace(/'/g, '&#39;') .replace(/'/g, '&#39;')
.replace(/"/g, '&quot;'); .replace(/"/g, '&quot;');
// 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 {array} keys * a tagged template processor for syntax higlighting purposes
* @param {function} callback * (https://marketplace.visualstudio.com/items?itemName=bierner.lit-html)
* @example
* const el = web.html`<p>hello</p>`; // = '<p>hello</p>'
* document.body.append(web.createElement(el));
*/
web.html = (html, ...templates) => html.map((str) => str + (templates.shift() ?? '')).join('');
/**
* register a hotkey listener to the page
* @param {array} keys - the combination of keys that will trigger the hotkey.
* key codes can be tested at http://keycode.info/.
* available modifiers are 'alt', 'ctrl', 'meta', and 'shift'
* @param {function} callback - called whenever the keys are pressed
*/ */
web.hotkeyListener = (keys, callback) => { web.hotkeyListener = (keys, callback) => {
if (typeof keys === 'string') keys = keys.split('+'); if (typeof keys === 'string') keys = keys.split('+');
if (!web._hotkeyListener) { if (!web._hotkeyListener) {
web._hotkeys = []; web._hotkeys = [];
web._hotkeyListener = document.addEventListener('keyup', (event) => { web._hotkeyListener = document.addEventListener('keyup', (event) => {
for (let hotkey of web._hotkeys) { for (const hotkey of web._hotkeys) {
const matchesEvent = hotkey.keys.every((key) => { const matchesEvent = hotkey.keys.every((key) => {
const modifiers = { const modifiers = {
altKey: 'alt', altKey: 'alt',
@ -136,7 +222,7 @@ web.hotkeyListener = (keys, callback) => {
metaKey: 'meta', metaKey: 'meta',
shiftKey: 'shift', shiftKey: 'shift',
}; };
for (let modifier in modifiers) { for (const modifier in modifiers) {
if (key.toLowerCase() === modifiers[modifier] && event[modifier]) return true; if (key.toLowerCase() === modifiers[modifier] && event[modifier]) return true;
} }
const pressedKeycode = [event.key.toLowerCase(), event.code.toLowerCase()]; const pressedKeycode = [event.key.toLowerCase(), event.code.toLowerCase()];
@ -149,7 +235,10 @@ web.hotkeyListener = (keys, callback) => {
web._hotkeys.push({ keys, callback }); web._hotkeys.push({ keys, callback });
}; };
/** helpers for formatting or parsing text */
export const fmt = {};
import './dep/prism.js'; import './dep/prism.js';
/** syntax highlighting using https://prismjs.com/ */
fmt.Prism = Prism; fmt.Prism = Prism;
fmt.Prism.manual = true; fmt.Prism.manual = true;
fmt.Prism.hooks.add('complete', async (event) => { fmt.Prism.hooks.add('complete', async (event) => {
@ -163,8 +252,8 @@ fmt.Prism.hooks.add('complete', async (event) => {
// } // }
}); });
// delete globalThis['Prism']; // delete globalThis['Prism'];
import './dep/markdown-it.min.js'; import './dep/markdown-it.min.js';
/** markdown -> html using https://github.com/markdown-it/markdown-it/ */
fmt.md = new markdownit({ fmt.md = new markdownit({
linkify: true, linkify: true,
highlight: (str, lang) => highlight: (str, lang) =>
@ -199,7 +288,13 @@ fmt.md.core.ruler.push(
}.bind(null, fmt.md) }.bind(null, fmt.md)
); );
// delete globalThis['markdownit']; // delete globalThis['markdownit'];
/**
* transform a heading into a slug (a lowercase alphanumeric string separated by dashes),
* e.g. for use as an anchor id
* @param {string} heading - the original heading to be slugified
* @param {Set<string>} [slugs] - a list of pre-generated slugs to avoid duplicates
* @returns
*/
fmt.slugger = (heading, slugs = new Set()) => { fmt.slugger = (heading, slugs = new Set()) => {
heading = heading heading = heading
.replace(/\s/g, '-') .replace(/\s/g, '-')
@ -214,50 +309,85 @@ fmt.slugger = (heading, slugs = new Set()) => {
return slug; return slug;
}; };
/** pattern validators */
export const regexers = {};
/**
* check for a valid uuid (8-4-4-4-12 hexadecimal digits)
* @param {string} str - the string to test
* @param {function} err - a callback to execute if the test fails
* @returns {boolean | env.ERROR} true or the env.ERROR constant
*/
regexers.uuid = (str, err = () => {}) => { regexers.uuid = (str, err = () => {}) => {
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); 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; if (match && match.length) return true;
err(`invalid uuid ${str}`); err(`invalid uuid ${str}`);
return ERROR; return env.ERROR;
}; };
/**
* check for a valid semver (MAJOR.MINOR.PATCH)
* @param {string} str - the string to test
* @param {function} err - a callback to execute if the test fails
* @returns {boolean | env.ERROR} true or the env.ERROR constant
*/
regexers.semver = (str, err = () => {}) => { regexers.semver = (str, err = () => {}) => {
const match = str.match( 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 /^(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; if (match && match.length) return true;
err(`invalid semver ${str}`); err(`invalid semver ${str}`);
return ERROR; return env.ERROR;
}; };
/**
* check for a valid email (someone@somewhere.domain)
* @param {string} str - the string to test
* @param {function} err - a callback to execute if the test fails
* @returns {boolean | env.ERROR} true or the env.ERROR constant
*/
regexers.email = (str, err = () => {}) => { regexers.email = (str, err = () => {}) => {
const match = str.match( 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 /^(([^<>()\[\]\\.,;:\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; if (match && match.length) return true;
err(`invalid email ${str}`); err(`invalid email ${str}`);
return ERROR; return env.ERROR;
}; };
/**
* check for a valid url (https://example.com/path)
* @param {string} str - the string to test
* @param {function} err - a callback to execute if the test fails
* @returns {boolean | env.ERROR} true or the env.ERROR constant
*/
regexers.url = (str, err = () => {}) => { regexers.url = (str, err = () => {}) => {
const match = str.match( const match = str.match(
/^[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/i /^[(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; if (match && match.length) return true;
err(`invalid url ${str}`); err(`invalid url ${str}`);
return ERROR; return env.ERROR;
}; };
/** an api for interacting with the enhancer's repository of mods */
export const registry = {};
/** mod ids whitelisted as part of the enhancer's core, permanently enabled */
registry.CORE = [ registry.CORE = [
'a6621988-551d-495a-97d8-3c568bca2e9e', 'a6621988-551d-495a-97d8-3c568bca2e9e',
'0f0bf8b6-eae6-4273-b307-8fc43f2ee082', '0f0bf8b6-eae6-4273-b307-8fc43f2ee082',
]; ];
/**
* internally used to validate mod.json files and provide helpful errors
* @param {object} mod - a mod's mod.json in object form
* @param {*} err - a callback to execute if a test fails
* @param {*} check - a function to test a condition
* @returns {array} the results of the validation
*/
registry.validate = async (mod, err, check) => { registry.validate = async (mod, err, check) => {
let conditions = [ let conditions = [
check('name', mod.name, typeof mod.name === 'string'), check('name', mod.name, typeof mod.name === 'string'),
check('id', mod.id, typeof mod.id === 'string').then((id) => check('id', mod.id, typeof mod.id === 'string').then((id) =>
id === ERROR ? ERROR : regexers.uuid(id, err) id === env.ERROR ? env.ERROR : regexers.uuid(id, err)
), ),
check('version', mod.version, typeof mod.version === 'string').then((version) => check('version', mod.version, typeof mod.version === 'string').then((version) =>
version === ERROR ? ERROR : regexers.semver(version, err) version === env.ERROR ? env.ERROR : regexers.semver(version, err)
), ),
check('description', mod.description, typeof mod.description === 'string'), check('description', mod.description, typeof mod.description === 'string'),
check( check(
@ -265,26 +395,28 @@ registry.validate = async (mod, err, check) => {
mod.preview, mod.preview,
mod.preview === undefined || typeof mod.preview === 'string' mod.preview === undefined || typeof mod.preview === 'string'
).then((preview) => ).then((preview) =>
preview ? (preview === ERROR ? ERROR : regexers.url(preview, err)) : undefined preview ? (preview === env.ERROR ? env.ERROR : regexers.url(preview, err)) : undefined
), ),
check('tags', mod.tags, Array.isArray(mod.tags)).then((tags) => check('tags', mod.tags, Array.isArray(mod.tags)).then((tags) =>
tags === ERROR ? ERROR : tags.map((tag) => check('tag', tag, typeof tag === 'string')) tags === env.ERROR
? env.ERROR
: tags.map((tag) => check('tag', tag, typeof tag === 'string'))
), ),
check('authors', mod.authors, Array.isArray(mod.authors)).then((authors) => check('authors', mod.authors, Array.isArray(mod.authors)).then((authors) =>
authors === ERROR authors === env.ERROR
? ERROR ? env.ERROR
: authors.map((author) => [ : authors.map((author) => [
check('author.name', author.name, typeof author.name === 'string'), check('author.name', author.name, typeof author.name === 'string'),
check( check(
'author.email', 'author.email',
author.email, author.email,
typeof author.email === 'string' typeof author.email === 'string'
).then((email) => (email === ERROR ? ERROR : regexers.email(email, err))), ).then((email) => (email === env.ERROR ? env.ERROR : regexers.email(email, err))),
check('author.url', author.url, typeof author.url === 'string').then((url) => check('author.url', author.url, typeof author.url === 'string').then((url) =>
url === ERROR ? ERROR : regexers.url(url, err) url === env.ERROR ? env.ERROR : regexers.url(url, err)
), ),
check('author.icon', author.icon, typeof author.icon === 'string').then((icon) => check('author.icon', author.icon, typeof author.icon === 'string').then((icon) =>
icon === ERROR ? ERROR : regexers.url(icon, err) icon === env.ERROR ? env.ERROR : regexers.url(icon, err)
), ),
]) ])
), ),
@ -294,8 +426,8 @@ registry.validate = async (mod, err, check) => {
!mod.environments || Array.isArray(mod.environments) !mod.environments || Array.isArray(mod.environments)
).then((environments) => ).then((environments) =>
environments environments
? environments === ERROR ? environments === env.ERROR
? ERROR ? env.ERROR
: environments.map((environment) => : environments.map((environment) =>
check('environment', environment, env.supported.includes(environment)) check('environment', environment, env.supported.includes(environment))
) )
@ -307,14 +439,14 @@ registry.validate = async (mod, err, check) => {
mod.css && typeof mod.css === 'object' && !Array.isArray(mod.css) mod.css && typeof mod.css === 'object' && !Array.isArray(mod.css)
).then((css) => ).then((css) =>
css css
? css === ERROR ? css === env.ERROR
? ERROR ? env.ERROR
: ['frame', 'client', 'menu'] : ['frame', 'client', 'menu']
.filter((dest) => css[dest]) .filter((dest) => css[dest])
.map(async (dest) => .map(async (dest) =>
check(`css.${dest}`, css[dest], Array.isArray(css[dest])).then((files) => check(`css.${dest}`, css[dest], Array.isArray(css[dest])).then((files) =>
files === ERROR files === env.ERROR
? ERROR ? env.ERROR
: files.map(async (file) => : files.map(async (file) =>
check( check(
`css.${dest} file`, `css.${dest} file`,
@ -328,12 +460,12 @@ registry.validate = async (mod, err, check) => {
), ),
check('js', mod.js, mod.js && typeof mod.js === 'object' && !Array.isArray(mod.js)).then( check('js', mod.js, mod.js && typeof mod.js === 'object' && !Array.isArray(mod.js)).then(
async (js) => { async (js) => {
if (js === ERROR) return ERROR; if (js === env.ERROR) return env.ERROR;
if (!js) return undefined; if (!js) return undefined;
return [ return [
check('js.client', js.client, !js.client || Array.isArray(js.client)).then( check('js.client', js.client, !js.client || Array.isArray(js.client)).then(
(client) => { (client) => {
if (client === ERROR) return ERROR; if (client === env.ERROR) return env.ERROR;
if (!client) return undefined; if (!client) return undefined;
return client.map(async (file) => return client.map(async (file) =>
check( check(
@ -346,7 +478,7 @@ registry.validate = async (mod, err, check) => {
), ),
check('js.electron', js.electron, !js.electron || Array.isArray(js.electron)).then( check('js.electron', js.electron, !js.electron || Array.isArray(js.electron)).then(
(electron) => { (electron) => {
if (electron === ERROR) return ERROR; if (electron === env.ERROR) return env.ERROR;
if (!electron) return undefined; if (!electron) return undefined;
return electron.map((file) => return electron.map((file) =>
check( check(
@ -354,8 +486,8 @@ registry.validate = async (mod, err, check) => {
file, file,
file && typeof file === 'object' && !Array.isArray(file) file && typeof file === 'object' && !Array.isArray(file)
).then(async (file) => ).then(async (file) =>
file === ERROR file === env.ERROR
? ERROR ? env.ERROR
: [ : [
check( check(
'js.electron file source', 'js.electron file source',
@ -378,8 +510,8 @@ registry.validate = async (mod, err, check) => {
} }
), ),
check('options', mod.options, Array.isArray(mod.options)).then((options) => check('options', mod.options, Array.isArray(mod.options)).then((options) =>
options === ERROR options === env.ERROR
? ERROR ? env.ERROR
: options.map((option) => { : options.map((option) => {
const conditions = []; const conditions = [];
switch (option.type) { switch (option.type) {
@ -395,8 +527,8 @@ registry.validate = async (mod, err, check) => {
option.values, option.values,
Array.isArray(option.values) Array.isArray(option.values)
).then((value) => ).then((value) =>
value === ERROR value === env.ERROR
? ERROR ? env.ERROR
: value.map((option) => : value.map((option) =>
check('option.values option', option, typeof option === 'string') check('option.values option', option, typeof option === 'string')
) )
@ -421,8 +553,8 @@ registry.validate = async (mod, err, check) => {
!option.extensions || Array.isArray(option.extensions) !option.extensions || Array.isArray(option.extensions)
).then((extensions) => ).then((extensions) =>
extensions extensions
? extensions === ERROR ? extensions === env.ERROR
? ERROR ? env.ERROR
: extensions.map((ext) => : extensions.map((ext) =>
check('option.extension', ext, typeof ext === 'string') check('option.extension', ext, typeof ext === 'string')
) )
@ -452,8 +584,8 @@ registry.validate = async (mod, err, check) => {
!option.environments || Array.isArray(option.environments) !option.environments || Array.isArray(option.environments)
).then((environments) => ).then((environments) =>
environments environments
? environments === ERROR ? environments === env.ERROR
? ERROR ? env.ERROR
: environments.map((environment) => : environments.map((environment) =>
check( check(
'option.environment', 'option.environment',
@ -472,9 +604,14 @@ registry.validate = async (mod, err, check) => {
} while (conditions.some((condition) => Array.isArray(condition))); } while (conditions.some((condition) => Array.isArray(condition)));
return conditions; return conditions;
}; };
/**
* get the default values of a mod's options according to its mod.json
* @param {string} id - the uuid of the mod
* @returns {object} the mod's default values
*/
registry.defaults = async (id) => { registry.defaults = async (id) => {
const mod = const mod =
regexers.uuid(id) !== ERROR regexers.uuid(id) !== env.ERROR
? (await registry.get()).find((mod) => mod.id === id) ? (await registry.get()).find((mod) => mod.id === id)
: undefined; : undefined;
if (!mod || !mod.options) return {}; if (!mod || !mod.options) return {};
@ -500,13 +637,20 @@ registry.defaults = async (id) => {
} }
return defaults; return defaults;
}; };
/**
registry.get = async (filter = (mod) => mod) => { * get all available enhancer mods in the repo
* @param {function} filter - a function to filter out mods
* @returns {array} the filtered and validated list of mod.json objects
* @example
* // will only get mods that are enabled in the current environment
* await registry.get((mod) => registry.enabled(mod.id))
*/
registry.get = async (filter = (mod) => true) => {
if (!registry._errors) registry._errors = []; if (!registry._errors) registry._errors = [];
if (!registry._list || !registry._list.length) { if (!registry._list || !registry._list.length) {
registry._list = []; registry._list = [];
for (const dir of await fs.getJSON('repo/registry.json')) { for (const dir of await fs.getJSON('repo/registry.json')) {
const err = (message) => [registry._errors.push({ source: dir, message }), ERROR][1]; const err = (message) => [registry._errors.push({ source: dir, message }), env.ERROR][1];
try { try {
const mod = await fs.getJSON(`repo/${dir}/mod.json`); const mod = await fs.getJSON(`repo/${dir}/mod.json`);
mod._dir = dir; mod._dir = dir;
@ -519,7 +663,7 @@ registry.get = async (filter = (mod) => mod) => {
condition ? value : err(`invalid ${prop} ${JSON.stringify(value)}`) condition ? value : err(`invalid ${prop} ${JSON.stringify(value)}`)
), ),
validation = await registry.validate(mod, err, check); validation = await registry.validate(mod, err, check);
if (validation.every((condition) => condition !== ERROR)) registry._list.push(mod); if (validation.every((condition) => condition !== env.ERROR)) registry._list.push(mod);
} catch (e) { } catch (e) {
err('invalid mod.json'); err('invalid mod.json');
} }
@ -529,11 +673,22 @@ registry.get = async (filter = (mod) => mod) => {
for (const mod of registry._list) if (await filter(mod)) list.push(mod); for (const mod of registry._list) if (await filter(mod)) list.push(mod);
return list; return list;
}; };
/**
* gets a list of errors encountered while validating the mod.json files
* @returns {object} - {source: directory, message: string }
*/
registry.errors = async () => { registry.errors = async () => {
if (!registry._errors) await registry.get(); if (!registry._errors) await registry.get();
return registry._errors; return registry._errors;
}; };
/**
* checks if a mod is core whitelisted, environment disabled or menu enabled
* @param {string} id - the uuid of the mod
* @returns {boolean} whether or not the mod is enabled
*/
registry.enabled = async (id) => { registry.enabled = async (id) => {
const mod = (await registry.get()).find((mod) => mod.id === id);
if (mod.environments && !mod.environments.includes(env.name)) return false;
if (registry.CORE.includes(id)) return true; if (registry.CORE.includes(id)) return true;
return await storage.get('_enabled', id, false); return await storage.get('_enabled', id, false);
}; };

View File

@ -6,17 +6,13 @@
'use strict'; 'use strict';
import(chrome.runtime.getURL('helpers.js')).then(({ env, web, registry }) => { import(chrome.runtime.getURL('api.js')).then(({ env, web, registry }) => {
web.whenReady().then(async () => { web.whenReady().then(async () => {
for (let mod of await registry.get( for (const mod of await registry.get((mod) => registry.enabled(mod.id))) {
async (mod) => for (const sheet of mod.css?.client || []) {
(await registry.enabled(mod.id)) &&
(!mod.environments || mod.environments.includes(env.name))
)) {
for (let sheet of mod.css?.client || []) {
web.loadStyleset(`repo/${mod._dir}/${sheet}`); web.loadStyleset(`repo/${mod._dir}/${sheet}`);
} }
for (let script of mod.js?.client || []) { for (const script of mod.js?.client || []) {
import(chrome.runtime.getURL(`repo/${mod._dir}/${script}`)); import(chrome.runtime.getURL(`repo/${mod._dir}/${script}`));
} }
} }

View File

@ -17,7 +17,7 @@
}, },
"web_accessible_resources": [ "web_accessible_resources": [
{ {
"resources": ["helpers.js", "repo/*", "icons/*", "dep/*"], "resources": ["api.js", "repo/*", "icons/*", "dep/*"],
"matches": ["https://*.notion.so/*"] "matches": ["https://*.notion.so/*"]
} }
], ],

View File

@ -7,7 +7,7 @@
'use strict'; 'use strict';
const _id = 'b4b0aced-2059-43bf-8d1d-ccd757ee5ebb'; const _id = 'b4b0aced-2059-43bf-8d1d-ccd757ee5ebb';
import { env, storage, web } from '../../helpers.js'; import { env, storage, web } from '../../api.js';
const inserts = { const inserts = {
js: await storage.get(_id, '_file.js'), js: await storage.get(_id, '_file.js'),

View File

@ -7,7 +7,7 @@
'use strict'; 'use strict';
const _id = 'a6621988-551d-495a-97d8-3c568bca2e9e'; const _id = 'a6621988-551d-495a-97d8-3c568bca2e9e';
import { env, storage, web, fs, registry } from '../../helpers.js'; import { env, storage, web, fs, registry } from '../../api.js';
const sidebarSelector = const sidebarSelector =
'#notion-app > div > div.notion-cursor-listener > div.notion-sidebar-container > div > div > div > div:nth-child(4)'; '#notion-app > div > div.notion-cursor-listener > div.notion-sidebar-container > div > div > div > div:nth-child(4)';

View File

@ -7,14 +7,10 @@
'use strict'; 'use strict';
const _id = 'a6621988-551d-495a-97d8-3c568bca2e9e'; const _id = 'a6621988-551d-495a-97d8-3c568bca2e9e';
import { env, storage, web, fmt, fs, registry } from '../../helpers.js'; import { env, storage, web, fmt, fs, registry } from '../../api.js';
for (let mod of await registry.get( for (const mod of await registry.get((mod) => registry.enabled(mod.id))) {
async (mod) => for (const sheet of mod.css?.menu || []) {
(await registry.enabled(mod.id)) &&
(!mod.environments || mod.environments.includes(env.name))
)) {
for (let sheet of mod.css?.menu || []) {
web.loadStyleset(`repo/${mod._dir}/${sheet}`); web.loadStyleset(`repo/${mod._dir}/${sheet}`);
} }
} }
@ -504,7 +500,7 @@ const notifications = {
notifications.waiting = notifications.list.filter( notifications.waiting = notifications.list.filter(
({ id }) => !notifications.dismissed.includes(id) ({ id }) => !notifications.dismissed.includes(id)
); );
for (let notification of notifications.waiting) { for (const notification of notifications.waiting) {
if ( if (
notification.heading && notification.heading &&
notification.appears_on && notification.appears_on &&

View File

@ -7,7 +7,7 @@
'use strict'; 'use strict';
const _id = '5174a483-c88d-4bf8-a95f-35cd330b76e2'; const _id = '5174a483-c88d-4bf8-a95f-35cd330b76e2';
import { env, storage, web } from '../../helpers.js'; import { env, storage, web } from '../../api.js';
web.whenReady().then(async () => { web.whenReady().then(async () => {
if (['linux', 'win32'].includes(env.name)) { if (['linux', 'win32'].includes(env.name)) {