mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-06 21:49:03 +00:00
refactor api ready for submodule
This commit is contained in:
parent
abbfce9af1
commit
ac12b164da
@ -9,17 +9,18 @@
|
||||
/** @module notion-enhancer/api */
|
||||
|
||||
/** environment-specific methods and constants */
|
||||
export * as env from './env.mjs';
|
||||
/** environment-specific filesystem reading */
|
||||
export * as fs from './fs.mjs';
|
||||
/** environment-specific data persistence */
|
||||
export * as storage from './storage.mjs';
|
||||
import * as env from './env.mjs';
|
||||
|
||||
/** helpers for formatting or parsing text */
|
||||
export * as fmt from './fmt.mjs';
|
||||
/** environment-specific filesystem reading */
|
||||
const fs = env.name === 'extension' ? await import('./extension-fs.mjs') : {};
|
||||
/** environment-specific data persistence */
|
||||
const storage = env.name === 'extension' ? await import('./extension-storage.mjs') : {};
|
||||
|
||||
/** helpers for formatting, validating and parsing values */
|
||||
import * as fmt from './fmt.mjs';
|
||||
/** interactions with the enhancer's repository of mods */
|
||||
export * as registry from './registry.mjs';
|
||||
/** pattern and type validators */
|
||||
export * as validation from './validation.mjs';
|
||||
import * as registry from './registry.mjs';
|
||||
/** helpers for manipulation of a webpage */
|
||||
export * as web from './web.mjs';
|
||||
import * as web from './web.mjs';
|
||||
|
||||
export { env, fs, storage, fmt, registry, web };
|
||||
|
@ -11,39 +11,36 @@
|
||||
* @module notion-enhancer/api/env
|
||||
*/
|
||||
|
||||
/**
|
||||
* the environment/platform name code is currently being executed in
|
||||
* @constant {string}
|
||||
*/
|
||||
export const name = 'extension';
|
||||
import env from '../env.mjs';
|
||||
|
||||
/**
|
||||
* all environments/platforms currently supported by the enhancer
|
||||
* @constant {array<string>}
|
||||
* the environment/platform name code is currently being executed in
|
||||
* @constant
|
||||
* @type {string}
|
||||
*/
|
||||
export const supported = ['linux', 'win32', 'darwin', 'extension'];
|
||||
export const name = env.name;
|
||||
|
||||
/**
|
||||
* the current version of the enhancer
|
||||
* @constant {string}
|
||||
* @constant
|
||||
* @type {string}
|
||||
*/
|
||||
export const version = chrome.runtime.getManifest().version;
|
||||
export const version = env.version;
|
||||
|
||||
/** open the enhancer's menu */
|
||||
export const focusMenu = () => chrome.runtime.sendMessage({ action: 'focusMenu' });
|
||||
/**
|
||||
* open the enhancer's menu
|
||||
* @type {function}
|
||||
*/
|
||||
export const focusMenu = env.focusMenu;
|
||||
|
||||
/** focus an active notion tab */
|
||||
export const focusNotion = () => chrome.runtime.sendMessage({ action: 'focusNotion' });
|
||||
/**
|
||||
* focus an active notion tab
|
||||
* @type {function}
|
||||
*/
|
||||
export const focusNotion = env.focusNotion;
|
||||
|
||||
/** reload all notion and enhancer menu tabs to apply changes */
|
||||
export const reload = () => chrome.runtime.sendMessage({ action: 'reload' });
|
||||
|
||||
/** a notification displayed when the menu is opened for the first time */
|
||||
export const welcomeNotification = {
|
||||
id: '84e2d49b-c3dc-44b4-a154-cf589676bfa0',
|
||||
color: 'purple',
|
||||
icon: 'message-circle',
|
||||
message: 'Welcome! Come chat with us on Discord.',
|
||||
link: 'https://discord.gg/sFWPXtA',
|
||||
version,
|
||||
};
|
||||
/**
|
||||
* reload all notion and enhancer menu tabs to apply changes
|
||||
* @type {function}
|
||||
*/
|
||||
export const reload = env.reload;
|
||||
|
@ -11,7 +11,7 @@
|
||||
* @module notion-enhancer/api/fmt
|
||||
*/
|
||||
|
||||
import * as web from './web.mjs';
|
||||
import { web, fs } from './_.mjs';
|
||||
|
||||
import '../dep/prism.min.js';
|
||||
/** syntax highlighting using https://prismjs.com/ */
|
||||
@ -80,3 +80,53 @@ export const slugger = (heading, slugs = new Set()) => {
|
||||
}
|
||||
return slug;
|
||||
};
|
||||
|
||||
const patterns = {
|
||||
alphanumeric: /^[\w\.-]+$/,
|
||||
uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
||||
semver:
|
||||
/^(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,
|
||||
email:
|
||||
/^(([^<>()\[\]\\.,;:\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,
|
||||
url: /^[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/i,
|
||||
color: /^(?:#|0x)(?:[a-f0-9]{3}|[a-f0-9]{6})\b|(?:rgb|hsl)a?\([^\)]*\)$/i,
|
||||
};
|
||||
function test(str, pattern) {
|
||||
const match = str.match(pattern);
|
||||
return !!(match && match.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* test the type of a value. unifies builtin, regex, and environment/api checks
|
||||
* @param {*} value - the value to check
|
||||
* @param {string|array<values>} type - the type the value should be or a list of allowed values
|
||||
* @returns {boolean} whether or not the value matches the type
|
||||
*/
|
||||
export const is = async (value, type, { extension = '' } = {}) => {
|
||||
extension = !value || !value.endsWith || value.endsWith(extension);
|
||||
if (Array.isArray(type)) {
|
||||
return type.includes(value);
|
||||
}
|
||||
switch (type) {
|
||||
case 'array':
|
||||
return Array.isArray(value);
|
||||
case 'object':
|
||||
return value && typeof value === 'object' && !Array.isArray(value);
|
||||
case 'undefined':
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
return typeof value === type && extension;
|
||||
case 'string':
|
||||
return typeof value === type && value.length && extension;
|
||||
case 'alphanumeric':
|
||||
case 'uuid':
|
||||
case 'semver':
|
||||
case 'email':
|
||||
case 'url':
|
||||
case 'color':
|
||||
return typeof value === 'string' && test(value, patterns[type]) && extension;
|
||||
case 'file':
|
||||
return typeof value === 'string' && value && (await fs.isFile(value)) && extension;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
221
extension/api/registry-validation.mjs
Normal file
221
extension/api/registry-validation.mjs
Normal file
@ -0,0 +1,221 @@
|
||||
/*
|
||||
* notion-enhancer: api
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { fmt, registry } from './_.mjs';
|
||||
|
||||
/**
|
||||
* internally used to validate mod.json files and provide helpful errors
|
||||
* @private
|
||||
* @param {object} mod - a mod's mod.json in object form
|
||||
* @returns {boolean} whether or not the mod has passed validation
|
||||
*/
|
||||
export async function validate(mod) {
|
||||
const check = async (
|
||||
key,
|
||||
value,
|
||||
types,
|
||||
{
|
||||
extension = '',
|
||||
error = `invalid ${key} (${extension ? `${extension} ` : ''}${types}): ${JSON.stringify(
|
||||
value
|
||||
)}`,
|
||||
optional = false,
|
||||
} = {}
|
||||
) => {
|
||||
let test;
|
||||
for (const type of Array.isArray(types) ? [types] : types.split('|')) {
|
||||
if (type === 'file') {
|
||||
test =
|
||||
value && !value.startsWith('http')
|
||||
? await fmt.is(`repo/${mod._dir}/${value}`, type, { extension })
|
||||
: false;
|
||||
} else test = await fmt.is(value, type, { extension });
|
||||
if (test) break;
|
||||
}
|
||||
if (!test) {
|
||||
if (optional && (await fmt.is(value, 'undefined'))) return true;
|
||||
if (error) registry._errors.push({ source: mod._dir, message: error });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
let conditions = [
|
||||
check('name', mod.name, 'string'),
|
||||
check('id', mod.id, 'uuid'),
|
||||
check('version', mod.version, 'semver'),
|
||||
check('environments', mod.environments, 'array', { optional: true }).then((passed) => {
|
||||
if (!passed) return false;
|
||||
if (!mod.environments) {
|
||||
mod.environments = registry.supportedEnvs;
|
||||
return true;
|
||||
}
|
||||
return mod.environments.map((tag) =>
|
||||
check('environments.env', tag, registry.supportedEnvs)
|
||||
);
|
||||
}),
|
||||
check('description', mod.description, 'string'),
|
||||
check('preview', mod.preview, 'file|url', { optional: true }),
|
||||
check('tags', mod.tags, 'array').then((passed) => {
|
||||
if (!passed) return false;
|
||||
const containsCategory = mod.tags.filter((tag) =>
|
||||
['core', 'extension', 'theme'].includes(tag)
|
||||
).length;
|
||||
if (!containsCategory) {
|
||||
registry._errors.push({
|
||||
source: mod._dir,
|
||||
message: `invalid tags (must contain at least one of 'core', 'extension', or 'theme'): ${JSON.stringify(
|
||||
mod.tags
|
||||
)}`,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
(mod.tags.includes('theme') &&
|
||||
!(mod.tags.includes('light') || mod.tags.includes('dark'))) ||
|
||||
(mod.tags.includes('light') && mod.tags.includes('dark'))
|
||||
) {
|
||||
registry._errors.push({
|
||||
source: mod._dir,
|
||||
message: `invalid tags (themes must be either 'light' or 'dark', not neither or both): ${JSON.stringify(
|
||||
mod.tags
|
||||
)}`,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return mod.tags.map((tag) => check('tags.tag', tag, 'string'));
|
||||
}),
|
||||
check('authors', mod.authors, 'array').then((passed) => {
|
||||
if (!passed) return false;
|
||||
return mod.authors.map((author) => [
|
||||
check('authors.author.name', author.name, 'string'),
|
||||
check('authors.author.email', author.email, 'email'),
|
||||
check('authors.author.homepage', author.homepage, 'url'),
|
||||
check('authors.author.avatar', author.avatar, 'url'),
|
||||
]);
|
||||
}),
|
||||
check('css', mod.css, 'object').then((passed) => {
|
||||
if (!passed) return false;
|
||||
const tests = [];
|
||||
for (let dest of ['frame', 'client', 'menu']) {
|
||||
if (!mod.css[dest]) continue;
|
||||
let test = check(`css.${dest}`, mod.css[dest], 'array');
|
||||
test = test.then((passed) => {
|
||||
if (!passed) return false;
|
||||
return mod.css[dest].map((file) =>
|
||||
check(`css.${dest}.file`, file, 'file', { extension: '.css' })
|
||||
);
|
||||
});
|
||||
tests.push(test);
|
||||
}
|
||||
return tests;
|
||||
}),
|
||||
check('js', mod.js, 'object').then((passed) => {
|
||||
if (!passed) return false;
|
||||
const tests = [];
|
||||
if (mod.js.client) {
|
||||
let test = check('js.client', mod.js.client, 'array');
|
||||
test = test.then((passed) => {
|
||||
if (!passed) return false;
|
||||
return mod.js.client.map((file) =>
|
||||
check('js.client.file', file, 'file', { extension: '.mjs' })
|
||||
);
|
||||
});
|
||||
tests.push(test);
|
||||
}
|
||||
if (mod.js.electron) {
|
||||
let test = check('js.electron', mod.js.electron, 'array');
|
||||
test = test.then((passed) => {
|
||||
if (!passed) return false;
|
||||
return mod.js.electron.map((file) =>
|
||||
check('js.electron.file', file, 'object').then((passed) => {
|
||||
if (!passed) return false;
|
||||
return [
|
||||
check('js.electron.file.source', file.source, 'file', {
|
||||
extension: '.mjs',
|
||||
}),
|
||||
// referencing the file within the electron app
|
||||
// existence can't be validated, so only format is
|
||||
check('js.electron.file.target', file.target, 'string', {
|
||||
extension: '.js',
|
||||
}),
|
||||
];
|
||||
})
|
||||
);
|
||||
});
|
||||
tests.push(test);
|
||||
}
|
||||
return tests;
|
||||
}),
|
||||
check('options', mod.options, 'array').then((passed) => {
|
||||
if (!passed) return false;
|
||||
return mod.options.map((option) =>
|
||||
check('options.option.type', option.type, registry.optionTypes).then((passed) => {
|
||||
if (!passed) return false;
|
||||
const tests = [
|
||||
check('options.option.key', option.key, 'alphanumeric'),
|
||||
check('options.option.label', option.label, 'string'),
|
||||
check('options.option.tooltip', option.tooltip, 'string', {
|
||||
optional: true,
|
||||
}),
|
||||
check('options.option.environments', option.environments, 'array', {
|
||||
optional: true,
|
||||
}).then((passed) => {
|
||||
if (!passed) return false;
|
||||
if (!option.environments) {
|
||||
option.environments = registry.supportedEnvs;
|
||||
return true;
|
||||
}
|
||||
return option.environments.map((environment) =>
|
||||
check('options.option.environments.env', environment, registry.supportedEnvs)
|
||||
);
|
||||
}),
|
||||
];
|
||||
switch (option.type) {
|
||||
case 'toggle':
|
||||
tests.push(check('options.option.value', option.value, 'boolean'));
|
||||
break;
|
||||
case 'select':
|
||||
tests.push(
|
||||
check('options.option.values', option.values, 'array').then((passed) => {
|
||||
if (!passed) return false;
|
||||
return option.values.map((value) =>
|
||||
check('options.option.values.value', value, 'string')
|
||||
);
|
||||
})
|
||||
);
|
||||
break;
|
||||
case 'text':
|
||||
case 'hotkey':
|
||||
tests.push(check('options.option.value', option.value, 'string'));
|
||||
break;
|
||||
case 'number':
|
||||
case 'color':
|
||||
tests.push(check('options.option.value', option.value, option.type));
|
||||
break;
|
||||
case 'file':
|
||||
tests.push(
|
||||
check('options.option.extensions', option.extensions, 'array').then(
|
||||
(passed) => {
|
||||
if (!passed) return false;
|
||||
return option.extensions.map((value) =>
|
||||
check('options.option.extensions.extension', value, 'string')
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
return tests;
|
||||
})
|
||||
);
|
||||
}),
|
||||
];
|
||||
do {
|
||||
conditions = await Promise.all(conditions.flat(Infinity));
|
||||
} while (conditions.some((condition) => Array.isArray(condition)));
|
||||
return conditions.every((passed) => passed);
|
||||
}
|
@ -11,238 +11,57 @@
|
||||
* @module notion-enhancer/api/registry
|
||||
*/
|
||||
|
||||
import * as env from './env.mjs';
|
||||
import { getJSON } from './fs.mjs';
|
||||
import * as storage from './storage.mjs';
|
||||
import { is } from './validation.mjs';
|
||||
import { env, fs, storage } from './_.mjs';
|
||||
import { validate } from './registry-validation.mjs';
|
||||
|
||||
const _cache = [],
|
||||
export const _cache = [],
|
||||
_errors = [];
|
||||
|
||||
/** mod ids whitelisted as part of the enhancer's core, permanently enabled */
|
||||
/**
|
||||
* mod ids whitelisted as part of the enhancer's core, permanently enabled
|
||||
* @constant
|
||||
* @type {array<string>}
|
||||
*/
|
||||
export const core = [
|
||||
'a6621988-551d-495a-97d8-3c568bca2e9e',
|
||||
'0f0bf8b6-eae6-4273-b307-8fc43f2ee082',
|
||||
];
|
||||
|
||||
/** all available configuration types */
|
||||
export const optionTypes = ['toggle', 'select', 'text', 'number', 'color', 'file', 'hotkey'];
|
||||
|
||||
/** the name of the active configuration profile */
|
||||
export const profileName = await storage.get(['currentprofile'], 'default');
|
||||
|
||||
/** the root database for the current profile */
|
||||
export const profileDB = storage.db(['profiles', profileName]);
|
||||
/**
|
||||
* all environments/platforms currently supported by the enhancer
|
||||
* @constant
|
||||
* @type {array<string>}
|
||||
*/
|
||||
export const supportedEnvs = ['linux', 'win32', 'darwin', 'extension'];
|
||||
|
||||
/**
|
||||
* internally used to validate mod.json files and provide helpful errors
|
||||
* @private
|
||||
* @param {object} mod - a mod's mod.json in object form
|
||||
* @returns {boolean} whether or not the mod has passed validation
|
||||
* all available configuration types
|
||||
* @constant
|
||||
* @type {array<string>}
|
||||
*/
|
||||
async function validate(mod) {
|
||||
const check = async (
|
||||
key,
|
||||
value,
|
||||
types,
|
||||
{
|
||||
extension = '',
|
||||
error = `invalid ${key} (${extension ? `${extension} ` : ''}${types}): ${JSON.stringify(
|
||||
value
|
||||
)}`,
|
||||
optional = false,
|
||||
} = {}
|
||||
) => {
|
||||
let test;
|
||||
for (const type of types.split('|')) {
|
||||
if (type === 'file') {
|
||||
test =
|
||||
value && !value.startsWith('http')
|
||||
? await is(`repo/${mod._dir}/${value}`, type, { extension })
|
||||
: false;
|
||||
} else test = await is(value, type, { extension });
|
||||
if (test) break;
|
||||
}
|
||||
if (!test) {
|
||||
if (optional && (await is(value, 'undefined'))) return true;
|
||||
if (error) _errors.push({ source: mod._dir, message: error });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
let conditions = [
|
||||
check('name', mod.name, 'string'),
|
||||
check('id', mod.id, 'uuid'),
|
||||
check('version', mod.version, 'semver'),
|
||||
check('environments', mod.environments, 'array', { optional: true }).then((passed) => {
|
||||
if (!passed) return false;
|
||||
if (!mod.environments) {
|
||||
mod.environments = env.supported;
|
||||
return true;
|
||||
}
|
||||
return mod.environments.map((tag) => check('environments.env', tag, 'env'));
|
||||
}),
|
||||
check('description', mod.description, 'string'),
|
||||
check('preview', mod.preview, 'file|url', { optional: true }),
|
||||
check('tags', mod.tags, 'array').then((passed) => {
|
||||
if (!passed) return false;
|
||||
const containsCategory = mod.tags.filter((tag) =>
|
||||
['core', 'extension', 'theme'].includes(tag)
|
||||
).length;
|
||||
if (!containsCategory) {
|
||||
_errors.push({
|
||||
source: mod._dir,
|
||||
message: `invalid tags (must contain at least one of 'core', 'extension', or 'theme'): ${JSON.stringify(
|
||||
mod.tags
|
||||
)}`,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
(mod.tags.includes('theme') &&
|
||||
!(mod.tags.includes('light') || mod.tags.includes('dark'))) ||
|
||||
(mod.tags.includes('light') && mod.tags.includes('dark'))
|
||||
) {
|
||||
_errors.push({
|
||||
source: mod._dir,
|
||||
message: `invalid tags (themes must be either 'light' or 'dark', not neither or both): ${JSON.stringify(
|
||||
mod.tags
|
||||
)}`,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return mod.tags.map((tag) => check('tags.tag', tag, 'string'));
|
||||
}),
|
||||
check('authors', mod.authors, 'array').then((passed) => {
|
||||
if (!passed) return false;
|
||||
return mod.authors.map((author) => [
|
||||
check('authors.author.name', author.name, 'string'),
|
||||
check('authors.author.email', author.email, 'email'),
|
||||
check('authors.author.homepage', author.homepage, 'url'),
|
||||
check('authors.author.avatar', author.avatar, 'url'),
|
||||
]);
|
||||
}),
|
||||
check('css', mod.css, 'object').then((passed) => {
|
||||
if (!passed) return false;
|
||||
const tests = [];
|
||||
for (let dest of ['frame', 'client', 'menu']) {
|
||||
if (!mod.css[dest]) continue;
|
||||
let test = check(`css.${dest}`, mod.css[dest], 'array');
|
||||
test = test.then((passed) => {
|
||||
if (!passed) return false;
|
||||
return mod.css[dest].map((file) =>
|
||||
check(`css.${dest}.file`, file, 'file', { extension: '.css' })
|
||||
);
|
||||
});
|
||||
tests.push(test);
|
||||
}
|
||||
return tests;
|
||||
}),
|
||||
check('js', mod.js, 'object').then((passed) => {
|
||||
if (!passed) return false;
|
||||
const tests = [];
|
||||
if (mod.js.client) {
|
||||
let test = check('js.client', mod.js.client, 'array');
|
||||
test = test.then((passed) => {
|
||||
if (!passed) return false;
|
||||
return mod.js.client.map((file) =>
|
||||
check('js.client.file', file, 'file', { extension: '.mjs' })
|
||||
);
|
||||
});
|
||||
tests.push(test);
|
||||
}
|
||||
if (mod.js.electron) {
|
||||
let test = check('js.electron', mod.js.electron, 'array');
|
||||
test = test.then((passed) => {
|
||||
if (!passed) return false;
|
||||
return mod.js.electron.map((file) =>
|
||||
check('js.electron.file', file, 'object').then((passed) => {
|
||||
if (!passed) return false;
|
||||
return [
|
||||
check('js.electron.file.source', file.source, 'file', {
|
||||
extension: '.mjs',
|
||||
}),
|
||||
// referencing the file within the electron app
|
||||
// existence can't be validated, so only format is
|
||||
check('js.electron.file.target', file.target, 'string', {
|
||||
extension: '.js',
|
||||
}),
|
||||
];
|
||||
})
|
||||
);
|
||||
});
|
||||
tests.push(test);
|
||||
}
|
||||
return tests;
|
||||
}),
|
||||
check('options', mod.options, 'array').then((passed) => {
|
||||
if (!passed) return false;
|
||||
return mod.options.map((option) =>
|
||||
check('options.option.type', option.type, 'optionType').then((passed) => {
|
||||
if (!passed) return false;
|
||||
const tests = [
|
||||
check('options.option.key', option.key, 'alphanumeric'),
|
||||
check('options.option.label', option.label, 'string'),
|
||||
check('options.option.tooltip', option.tooltip, 'string', {
|
||||
optional: true,
|
||||
}),
|
||||
check('options.option.environments', option.environments, 'array', {
|
||||
optional: true,
|
||||
}).then((passed) => {
|
||||
if (!passed) return false;
|
||||
if (!option.environments) {
|
||||
option.environments = env.supported;
|
||||
return true;
|
||||
}
|
||||
return option.environments.map((env) =>
|
||||
check('options.option.environments.env', env, 'env')
|
||||
);
|
||||
}),
|
||||
];
|
||||
switch (option.type) {
|
||||
case 'toggle':
|
||||
tests.push(check('options.option.value', option.value, 'boolean'));
|
||||
break;
|
||||
case 'select':
|
||||
tests.push(
|
||||
check('options.option.values', option.values, 'array').then((passed) => {
|
||||
if (!passed) return false;
|
||||
return option.values.map((value) =>
|
||||
check('options.option.values.value', value, 'string')
|
||||
);
|
||||
})
|
||||
);
|
||||
break;
|
||||
case 'text':
|
||||
case 'hotkey':
|
||||
tests.push(check('options.option.value', option.value, 'string'));
|
||||
break;
|
||||
case 'number':
|
||||
case 'color':
|
||||
tests.push(check('options.option.value', option.value, option.type));
|
||||
break;
|
||||
case 'file':
|
||||
tests.push(
|
||||
check('options.option.extensions', option.extensions, 'array').then(
|
||||
(passed) => {
|
||||
if (!passed) return false;
|
||||
return option.extensions.map((value) =>
|
||||
check('options.option.extensions.extension', value, 'string')
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
return tests;
|
||||
})
|
||||
);
|
||||
}),
|
||||
];
|
||||
do {
|
||||
conditions = await Promise.all(conditions.flat(Infinity));
|
||||
} while (conditions.some((condition) => Array.isArray(condition)));
|
||||
return conditions.every((passed) => passed);
|
||||
}
|
||||
export const optionTypes = ['toggle', 'select', 'text', 'number', 'color', 'file', 'hotkey'];
|
||||
|
||||
/**
|
||||
* the name of the active configuration profile
|
||||
* @returns {string}
|
||||
*/
|
||||
export const profileName = async () => storage.get(['currentprofile'], 'default');
|
||||
|
||||
/**
|
||||
* the root database for the current profile
|
||||
* @returns {object} the get/set functions for the profile's storage
|
||||
*/
|
||||
export const profileDB = async () => storage.db(['profiles', await profileName()]);
|
||||
|
||||
/** a notification displayed when the menu is opened for the first time */
|
||||
export const welcomeNotification = {
|
||||
id: '84e2d49b-c3dc-44b4-a154-cf589676bfa0',
|
||||
color: 'purple',
|
||||
icon: 'message-circle',
|
||||
message: 'Welcome! Come chat with us on Discord.',
|
||||
link: 'https://discord.gg/sFWPXtA',
|
||||
version: env.version,
|
||||
};
|
||||
|
||||
/**
|
||||
* list all available mods in the repo
|
||||
@ -251,12 +70,13 @@ async function validate(mod) {
|
||||
*/
|
||||
export const list = async (filter = (mod) => true) => {
|
||||
if (!_cache.length) {
|
||||
for (const dir of await getJSON('repo/registry.json')) {
|
||||
for (const dir of await fs.getJSON('repo/registry.json')) {
|
||||
try {
|
||||
const mod = await getJSON(`repo/${dir}/mod.json`);
|
||||
const mod = await fs.getJSON(`repo/${dir}/mod.json`);
|
||||
mod._dir = dir;
|
||||
if (await validate(mod)) _cache.push(mod);
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
_errors.push({ source: dir, message: 'invalid mod.json' });
|
||||
}
|
||||
}
|
||||
@ -295,7 +115,7 @@ export const enabled = async (id) => {
|
||||
const mod = await get(id);
|
||||
if (!mod.environments.includes(env.name)) return false;
|
||||
if (core.includes(id)) return true;
|
||||
return await profileDB.get(['_mods', id], false);
|
||||
return (await profileDB()).get(['_mods', id], false);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -328,6 +148,7 @@ export const optionDefault = async (id, key) => {
|
||||
* @returns {object} an object with the wrapped get/set functions
|
||||
*/
|
||||
export const db = async (id) => {
|
||||
const db = await profileDB();
|
||||
return storage.db(
|
||||
[id],
|
||||
async (path, fallback = undefined) => {
|
||||
@ -335,8 +156,8 @@ export const db = async (id) => {
|
||||
// profiles -> profile -> mod -> option
|
||||
fallback = (await optionDefault(id, path[1])) ?? fallback;
|
||||
}
|
||||
return profileDB.get(path, fallback);
|
||||
return db.get(path, fallback);
|
||||
},
|
||||
profileDB.set
|
||||
db.set
|
||||
);
|
||||
};
|
||||
|
@ -1,109 +0,0 @@
|
||||
/*
|
||||
* notion-enhancer: api
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* pattern and type validators
|
||||
* @module notion-enhancer/api/validation
|
||||
*/
|
||||
|
||||
import { supported } from './env.mjs';
|
||||
import { optionTypes } from './registry.mjs';
|
||||
import { isFile } from './fs.mjs';
|
||||
|
||||
const patterns = {
|
||||
alphanumeric: /^[\w\.-]+$/,
|
||||
uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
||||
semver:
|
||||
/^(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,
|
||||
email:
|
||||
/^(([^<>()\[\]\\.,;:\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,
|
||||
url: /^[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/i,
|
||||
color: /^(?:#|0x)(?:[a-f0-9]{3}|[a-f0-9]{6})\b|(?:rgb|hsl)a?\([^\)]*\)$/i,
|
||||
};
|
||||
function test(str, pattern) {
|
||||
const match = str.match(pattern);
|
||||
return !!(match && match.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* check is string is alphanumeric-only (letters, numbers, underscores, dots, dashes)
|
||||
* @param {string} str - the string to test
|
||||
* @returns {boolean} whether or not the test passed successfully
|
||||
*/
|
||||
export const alphanumeric = (str) => test(str, patterns.alphanumeric);
|
||||
|
||||
/**
|
||||
* check for a valid uuid (8-4-4-4-12 hexadecimal digits)
|
||||
* @param {string} str - the string to test
|
||||
* @returns {boolean} whether or not the test passed successfully
|
||||
*/
|
||||
export const uuid = (str) => test(str, patterns.uuid);
|
||||
|
||||
/**
|
||||
* check for a valid semver (MAJOR.MINOR.PATCH)
|
||||
* @param {string} str - the string to test
|
||||
* @returns {boolean} whether or not the test passed successfully
|
||||
*/
|
||||
export const semver = (str) => test(str, patterns.semver);
|
||||
|
||||
/**
|
||||
* check for a valid email (e.g. someone@somewhere.domain)
|
||||
* @param {string} str - the string to test
|
||||
* @returns {boolean} whether or not the test passed successfully
|
||||
*/
|
||||
export const email = (str) => test(str, patterns.email);
|
||||
|
||||
/**
|
||||
* check for a valid url (e.g. https://example.com/path)
|
||||
* @param {string} str - the string to test
|
||||
* @returns {boolean} whether or not the test passed successfully
|
||||
*/
|
||||
export const url = (str) => test(str, patterns.url);
|
||||
|
||||
/**
|
||||
* check for a valid color (https://regexr.com/39cgj)
|
||||
* @param {string} str - the string to test
|
||||
* @returns {boolean} whether or not the test passed successfully
|
||||
*/
|
||||
export const color = (str) => test(str, patterns.color);
|
||||
|
||||
/**
|
||||
* test the type of a value. unifies builtin, regex, and environment/api checks.
|
||||
* @param {*} value - the value to check
|
||||
* @param {string} type - the type the value should be
|
||||
* @returns {boolean} whether or not the value matches the type
|
||||
*/
|
||||
export const is = async (value, type, { extension = '' } = {}) => {
|
||||
extension = !value || !value.endsWith || value.endsWith(extension);
|
||||
switch (type) {
|
||||
case 'array':
|
||||
return Array.isArray(value);
|
||||
case 'object':
|
||||
return value && typeof value === 'object' && !Array.isArray(value);
|
||||
case 'undefined':
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
return typeof value === type && extension;
|
||||
case 'string':
|
||||
return typeof value === type && value.length && extension;
|
||||
case 'alphanumeric':
|
||||
case 'uuid':
|
||||
case 'semver':
|
||||
case 'email':
|
||||
case 'url':
|
||||
case 'color':
|
||||
return typeof value === 'string' && test(value, patterns[type]) && extension;
|
||||
case 'file':
|
||||
return typeof value === 'string' && value && (await isFile(value)) && extension;
|
||||
case 'env':
|
||||
return supported.includes(value);
|
||||
case 'optionType':
|
||||
return optionTypes.includes(value);
|
||||
}
|
||||
return false;
|
||||
};
|
@ -11,8 +11,7 @@
|
||||
* @module notion-enhancer/api/web
|
||||
*/
|
||||
|
||||
import { localPath } from './fs.mjs';
|
||||
import { md } from './fmt.mjs';
|
||||
import { fs, fmt } from './_.mjs';
|
||||
|
||||
const _hotkeyEventListeners = [],
|
||||
_documentObserverListeners = [],
|
||||
@ -139,7 +138,7 @@ export const loadStylesheet = (path) => {
|
||||
document.head,
|
||||
html`<link
|
||||
rel="stylesheet"
|
||||
href="${path.startsWith('https://') ? path : localPath(path)}"
|
||||
href="${path.startsWith('https://') ? path : fs.localPath(path)}"
|
||||
/>`
|
||||
);
|
||||
return true;
|
||||
@ -158,7 +157,7 @@ export const icon = (name, attrs = {}) => {
|
||||
).trim();
|
||||
return `<svg ${Object.entries(attrs)
|
||||
.map(([key, val]) => `${escape(key)}="${escape(val)}"`)
|
||||
.join(' ')}><use xlink:href="${localPath('dep/feather-sprite.svg')}#${name}" /></svg>`;
|
||||
.join(' ')}><use xlink:href="${fs.localPath('dep/feather-sprite.svg')}#${name}" /></svg>`;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -193,7 +192,7 @@ export const tooltip = ($ref, text) => {
|
||||
render(document.head, _$tooltipStylesheet);
|
||||
render(document.body, _$tooltip);
|
||||
}
|
||||
text = md.render(text);
|
||||
text = fmt.md.render(text);
|
||||
$ref.addEventListener('mouseover', (event) => {
|
||||
_$tooltip.innerHTML = text;
|
||||
_$tooltip.style.display = 'block';
|
||||
|
24
extension/env.mjs
Normal file
24
extension/env.mjs
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* notion-enhancer: api
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* environment-specific methods and constants
|
||||
* @module notion-enhancer/api/env
|
||||
*/
|
||||
|
||||
const focusMenu = () => chrome.runtime.sendMessage({ action: 'focusMenu' }),
|
||||
focusNotion = () => chrome.runtime.sendMessage({ action: 'focusNotion' }),
|
||||
reload = () => chrome.runtime.sendMessage({ action: 'reload' });
|
||||
|
||||
export default {
|
||||
name: 'extension',
|
||||
version: chrome.runtime.getManifest().version,
|
||||
focusMenu,
|
||||
focusNotion,
|
||||
reload,
|
||||
};
|
@ -33,7 +33,7 @@ export default async function (api, db) {
|
||||
const notifications = {
|
||||
cache: await storage.get(['notifications'], []),
|
||||
provider: [
|
||||
env.welcomeNotification,
|
||||
registry.welcomeNotification,
|
||||
...(await fs.getJSON('https://notion-enhancer.github.io/notifications.json')),
|
||||
],
|
||||
count: (await registry.errors()).length,
|
||||
|
@ -6,9 +6,9 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import { fmt, registry, web } from '../../api/_.mjs';
|
||||
|
||||
import { fmt, web } from '../../api/_.mjs';
|
||||
import { notifications } from './notifications.mjs';
|
||||
import { profileDB } from './menu.mjs';
|
||||
|
||||
export const components = {
|
||||
preview: (url) => web.html`<img
|
||||
@ -56,7 +56,7 @@ export const components = {
|
||||
|
||||
export const options = {
|
||||
toggle: async (mod, opt) => {
|
||||
const checked = await registry.profileDB.get([mod.id, opt.key], opt.value),
|
||||
const checked = await profileDB.get([mod.id, opt.key], opt.value),
|
||||
$toggle = components.toggle(opt.label, checked),
|
||||
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
||||
$label = $toggle.children[0],
|
||||
@ -66,13 +66,13 @@ export const options = {
|
||||
web.tooltip($tooltip, opt.tooltip);
|
||||
}
|
||||
$input.addEventListener('change', async (event) => {
|
||||
await registry.profileDB.set([mod.id, opt.key], $input.checked);
|
||||
await profileDB.set([mod.id, opt.key], $input.checked);
|
||||
notifications.onChange();
|
||||
});
|
||||
return $toggle;
|
||||
},
|
||||
select: async (mod, opt) => {
|
||||
const value = await registry.profileDB.get([mod.id, opt.key], opt.values[0]),
|
||||
const value = await profileDB.get([mod.id, opt.key], opt.values[0]),
|
||||
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
||||
$label = web.render(
|
||||
web.html`<label class="input-label"></label>`,
|
||||
@ -91,13 +91,13 @@ export const options = {
|
||||
$icon = web.html`${web.icon('chevron-down', { class: 'input-icon' })}`;
|
||||
if (opt.tooltip) web.tooltip($tooltip, opt.tooltip);
|
||||
$select.addEventListener('change', async (event) => {
|
||||
await registry.profileDB.set([mod.id, opt.key], $select.value);
|
||||
await profileDB.set([mod.id, opt.key], $select.value);
|
||||
notifications.onChange();
|
||||
});
|
||||
return web.render($label, $select, $icon);
|
||||
},
|
||||
text: async (mod, opt) => {
|
||||
const value = await registry.profileDB.get([mod.id, opt.key], opt.value),
|
||||
const value = await profileDB.get([mod.id, opt.key], opt.value),
|
||||
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
||||
$label = web.render(
|
||||
web.html`<label class="input-label"></label>`,
|
||||
@ -107,13 +107,13 @@ export const options = {
|
||||
$icon = web.html`${web.icon('type', { class: 'input-icon' })}`;
|
||||
if (opt.tooltip) web.tooltip($tooltip, opt.tooltip);
|
||||
$input.addEventListener('change', async (event) => {
|
||||
await registry.profileDB.set([mod.id, opt.key], $input.value);
|
||||
await profileDB.set([mod.id, opt.key], $input.value);
|
||||
notifications.onChange();
|
||||
});
|
||||
return web.render($label, $input, $icon);
|
||||
},
|
||||
number: async (mod, opt) => {
|
||||
const value = await registry.profileDB.get([mod.id, opt.key], opt.value),
|
||||
const value = await profileDB.get([mod.id, opt.key], opt.value),
|
||||
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
||||
$label = web.render(
|
||||
web.html`<label class="input-label"></label>`,
|
||||
@ -123,13 +123,13 @@ export const options = {
|
||||
$icon = web.html`${web.icon('hash', { class: 'input-icon' })}`;
|
||||
if (opt.tooltip) web.tooltip($tooltip, opt.tooltip);
|
||||
$input.addEventListener('change', async (event) => {
|
||||
await registry.profileDB.set([mod.id, opt.key], $input.value);
|
||||
await profileDB.set([mod.id, opt.key], $input.value);
|
||||
notifications.onChange();
|
||||
});
|
||||
return web.render($label, $input, $icon);
|
||||
},
|
||||
color: async (mod, opt) => {
|
||||
const value = await registry.profileDB.get([mod.id, opt.key], opt.value),
|
||||
const value = await profileDB.get([mod.id, opt.key], opt.value),
|
||||
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
||||
$label = web.render(
|
||||
web.html`<label class="input-label"></label>`,
|
||||
@ -155,14 +155,14 @@ export const options = {
|
||||
});
|
||||
if (opt.tooltip) web.tooltip($tooltip, opt.tooltip);
|
||||
$input.addEventListener('change', async (event) => {
|
||||
await registry.profileDB.set([mod.id, opt.key], $input.value);
|
||||
await profileDB.set([mod.id, opt.key], $input.value);
|
||||
notifications.onChange();
|
||||
});
|
||||
paint();
|
||||
return web.render($label, $input, $icon);
|
||||
},
|
||||
file: async (mod, opt) => {
|
||||
const { filename } = (await registry.profileDB.get([mod.id, opt.key], {})) || {},
|
||||
const { filename } = (await profileDB.get([mod.id, opt.key], {})) || {},
|
||||
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
||||
$label = web.render(
|
||||
web.html`<label class="input-label"></label>`,
|
||||
@ -181,7 +181,7 @@ export const options = {
|
||||
reader = new FileReader();
|
||||
reader.onload = async (progress) => {
|
||||
$filename.innerText = file.name;
|
||||
await registry.profileDB.set([mod.id, opt.key], {
|
||||
await profileDB.set([mod.id, opt.key], {
|
||||
filename: file.name,
|
||||
content: progress.currentTarget.result,
|
||||
});
|
||||
@ -191,7 +191,7 @@ export const options = {
|
||||
});
|
||||
$latest.addEventListener('click', (event) => {
|
||||
$filename.innerText = 'none';
|
||||
registry.profileDB.set([mod.id, opt.key], {});
|
||||
profileDB.set([mod.id, opt.key], {});
|
||||
});
|
||||
return web.render(
|
||||
web.html`<div></div>`,
|
||||
@ -200,7 +200,7 @@ export const options = {
|
||||
);
|
||||
},
|
||||
hotkey: async (mod, opt) => {
|
||||
const value = await registry.profileDB.get([mod.id, opt.key], opt.value),
|
||||
const value = await profileDB.get([mod.id, opt.key], opt.value),
|
||||
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
||||
$label = web.render(
|
||||
web.html`<label class="input-label"></label>`,
|
||||
@ -229,7 +229,7 @@ export const options = {
|
||||
pressed.push(key);
|
||||
}
|
||||
$input.value = pressed.join('+');
|
||||
await registry.profileDB.set([mod.id, opt.key], $input.value);
|
||||
await profileDB.set([mod.id, opt.key], $input.value);
|
||||
notifications.onChange();
|
||||
});
|
||||
return web.render($label, $input, $icon);
|
||||
|
@ -7,7 +7,10 @@
|
||||
'use strict';
|
||||
|
||||
import { env, fs, storage, registry, web } from '../../api/_.mjs';
|
||||
const db = await registry.db('a6621988-551d-495a-97d8-3c568bca2e9e');
|
||||
|
||||
export const db = await registry.db('a6621988-551d-495a-97d8-3c568bca2e9e'),
|
||||
profileName = await registry.profileName(),
|
||||
profileDB = await registry.profileDB();
|
||||
|
||||
import './styles.mjs';
|
||||
import { notifications } from './notifications.mjs';
|
||||
@ -35,12 +38,12 @@ window.addEventListener('beforeunload', (event) => {
|
||||
|
||||
const $main = web.html`<main class="main"></main>`,
|
||||
$sidebar = web.html`<article class="sidebar"></article>`,
|
||||
$profile = web.html`<button class="profile-trigger">
|
||||
Profile: ${web.escape(registry.profileName)}
|
||||
</button>`,
|
||||
$options = web.html`<div class="options-container">
|
||||
<p class="options-placeholder">Select a mod to view and configure its options.</p>
|
||||
</div>`;
|
||||
</div>`,
|
||||
$profile = web.html`<button class="profile-trigger">
|
||||
Profile: ${web.escape(profileName)}
|
||||
</button>`;
|
||||
|
||||
let _$profileConfig;
|
||||
$profile.addEventListener('click', async (event) => {
|
||||
@ -51,14 +54,14 @@ $profile.addEventListener('click', async (event) => {
|
||||
const profileNames = [
|
||||
...new Set([
|
||||
...Object.keys(await storage.get(['profiles'], { default: {} })),
|
||||
registry.profileName,
|
||||
profileName,
|
||||
]),
|
||||
],
|
||||
$options = profileNames.map(
|
||||
(profile) => web.raw`<option
|
||||
class="select-option"
|
||||
value="${web.escape(profile)}"
|
||||
${profile === registry.profileName ? 'selected' : ''}
|
||||
${profile === profileName ? 'selected' : ''}
|
||||
>${web.escape(profile)}</option>`
|
||||
),
|
||||
$select = web.html`<select class="input">
|
||||
@ -68,7 +71,7 @@ $profile.addEventListener('click', async (event) => {
|
||||
$edit = web.html`<input
|
||||
type="text"
|
||||
class="input"
|
||||
value="${web.escape(registry.profileName)}"
|
||||
value="${web.escape(profileName)}"
|
||||
pattern="/^[A-Za-z0-9_-]+$/"
|
||||
>`,
|
||||
$export = web.html`<button class="profile-export">
|
||||
@ -214,13 +217,13 @@ const _$modListCache = {},
|
||||
mod.id !== id
|
||||
);
|
||||
for (const mod of mods) {
|
||||
registry.profileDB.set(['_mods', mod.id], false);
|
||||
profileDB.set(['_mods', mod.id], false);
|
||||
document.querySelector(
|
||||
`[data-id="${web.escape(mod.id)}"] .toggle-check`
|
||||
).checked = false;
|
||||
}
|
||||
}
|
||||
registry.profileDB.set(['_mods', mod.id], event.target.checked);
|
||||
profileDB.set(['_mods', mod.id], event.target.checked);
|
||||
notifications.onChange();
|
||||
});
|
||||
$mod.addEventListener('click', async (event) => {
|
||||
|
@ -15,7 +15,7 @@ export const notifications = {
|
||||
$container: web.html`<div class="notifications-container"></div>`,
|
||||
cache: await storage.get(['notifications'], []),
|
||||
provider: [
|
||||
env.welcomeNotification,
|
||||
registry.welcomeNotification,
|
||||
...(await fs.getJSON('https://notion-enhancer.github.io/notifications.json')),
|
||||
],
|
||||
add({ icon, message, id = undefined, color = undefined, link = undefined }) {
|
||||
|
Loading…
Reference in New Issue
Block a user