refactor api ready for submodule

This commit is contained in:
dragonwocky 2021-10-01 17:39:45 +10:00
parent abbfce9af1
commit ac12b164da
14 changed files with 415 additions and 408 deletions

View File

@ -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 };

View File

@ -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;

View File

@ -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;
};

View 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);
}

View File

@ -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
);
};

View File

@ -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;
};

View File

@ -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
View 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,
};

View File

@ -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,

View File

@ -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);

View File

@ -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) => {

View File

@ -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 }) {