mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-09 15:09:02 +00:00
split up + improve api part 1: all except registry, temp disabled launcher
This commit is contained in:
parent
f98dcb174a
commit
f383e6b401
@ -9,7 +9,6 @@ a complete rework of the enhancer including a port to the browser as a chrome ex
|
|||||||
- new: notifications sourced from an online endpoint for sending global user alerts.
|
- new: notifications sourced from an online endpoint for sending global user alerts.
|
||||||
- improved: a redesigned menu with a better overview of all mods and separate pages for options and documentation.
|
- improved: a redesigned menu with a better overview of all mods and separate pages for options and documentation.
|
||||||
- improved: theming variables that are more specific, less laggy, and less complicated.
|
- improved: theming variables that are more specific, less laggy, and less complicated.
|
||||||
- improved: switched from fontawesome to [feather](https://feathericons.com/) icons.
|
|
||||||
|
|
||||||
// todo
|
// todo
|
||||||
|
|
||||||
|
872
extension/api.js
872
extension/api.js
@ -1,872 +0,0 @@
|
|||||||
/*
|
|
||||||
* notion-enhancer
|
|
||||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
|
||||||
* (https://notion-enhancer.github.io/) under the MIT license
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module notion-enhancer/api
|
|
||||||
* @version 0.11.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* environment-specific methods and constants
|
|
||||||
* @namespace env
|
|
||||||
*/
|
|
||||||
export const env = {};
|
|
||||||
/**
|
|
||||||
* an error constant used in validation, distinct from null or undefined
|
|
||||||
* @constant {Symbol}
|
|
||||||
*/
|
|
||||||
env.ERROR = Symbol();
|
|
||||||
/**
|
|
||||||
* the environment/platform name code is currently being executed in
|
|
||||||
* @constant {string}
|
|
||||||
*/
|
|
||||||
env.name = 'extension';
|
|
||||||
/**
|
|
||||||
* all environments/platforms currently supported by the enhancer
|
|
||||||
* @constant {array<string>}
|
|
||||||
*/
|
|
||||||
env.supported = ['linux', 'win32', 'darwin', 'extension'];
|
|
||||||
/**
|
|
||||||
* the current version of the enhancer
|
|
||||||
* @constant {string}
|
|
||||||
*/
|
|
||||||
env.version = chrome.runtime.getManifest().version;
|
|
||||||
/** open the enhancer's menu */
|
|
||||||
env.openEnhancerMenu = () => chrome.runtime.sendMessage({ action: 'openEnhancerMenu' });
|
|
||||||
/** focus an active notion tab */
|
|
||||||
env.focusNotion = () => chrome.runtime.sendMessage({ action: 'focusNotion' });
|
|
||||||
/** reload all notion and enhancer menu tabs to apply changes */
|
|
||||||
env.reloadTabs = () => chrome.runtime.sendMessage({ action: 'reloadTabs' });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* environment-specific data persistence
|
|
||||||
* @namespace storage
|
|
||||||
*/
|
|
||||||
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) =>
|
|
||||||
new Promise((res, rej) =>
|
|
||||||
chrome.storage.sync.get([namespace], async (values) => {
|
|
||||||
const defaults = await registry.defaults(namespace);
|
|
||||||
values =
|
|
||||||
values[namespace] &&
|
|
||||||
Object.getOwnPropertyNames(values[namespace]).length &&
|
|
||||||
(!key || Object.getOwnPropertyNames(values[namespace]).includes(key))
|
|
||||||
? values[namespace]
|
|
||||||
: defaults;
|
|
||||||
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._queue = [];
|
|
||||||
storage.set = (namespace, key, value) => {
|
|
||||||
const precursor = storage._queue[storage._queue.length - 1] || undefined,
|
|
||||||
interaction = new Promise(async (res, rej) => {
|
|
||||||
if (precursor !== undefined) {
|
|
||||||
await precursor;
|
|
||||||
storage._queue.shift();
|
|
||||||
}
|
|
||||||
const values = await storage.get(namespace, undefined, {});
|
|
||||||
if (values.hasOwnProperty(key)) delete values[key];
|
|
||||||
storage._onChangeListeners.forEach((listener) =>
|
|
||||||
listener({ type: 'set', namespace, key, new: value, old: values[key] })
|
|
||||||
);
|
|
||||||
chrome.storage.sync.set({ [namespace]: { ...values, [key]: value } }, res);
|
|
||||||
});
|
|
||||||
storage._queue.push(interaction);
|
|
||||||
return interaction;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* clear data from an enhancer store
|
|
||||||
* @param {string} namespace - the name of the store, e.g. a mod id
|
|
||||||
*/
|
|
||||||
storage.reset = (namespace) => {
|
|
||||||
storage._onChangeListeners.forEach((listener) =>
|
|
||||||
listener({ type: 'reset', namespace, key: undefined, new: undefined, old: undefined })
|
|
||||||
);
|
|
||||||
return new Promise((res, rej) => chrome.storage.sync.set({ [namespace]: undefined }, res));
|
|
||||||
};
|
|
||||||
storage._onChangeListeners = [];
|
|
||||||
/**
|
|
||||||
* add an event listener for changes in storage
|
|
||||||
* @param {onStorageChangeCallback} callback - called whenever a change in
|
|
||||||
* storage is initiated from the current process
|
|
||||||
*/
|
|
||||||
storage.addChangeListener = (callback) => {
|
|
||||||
storage._onChangeListeners.push(callback);
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* remove a listener added with storage.addChangeListener
|
|
||||||
* @param {onStorageChangeCallback} callback
|
|
||||||
*/
|
|
||||||
storage.removeChangeListener = (callback) => {
|
|
||||||
storage._onChangeListeners = storage._onChangeListeners.filter(
|
|
||||||
(listener) => listener !== callback
|
|
||||||
);
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* @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
|
|
||||||
* @namespace fs
|
|
||||||
*/
|
|
||||||
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) =>
|
|
||||||
fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) =>
|
|
||||||
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) =>
|
|
||||||
fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) =>
|
|
||||||
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) => {
|
|
||||||
try {
|
|
||||||
await fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path));
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* helpers for manipulation of a webpage
|
|
||||||
* @namespace web
|
|
||||||
*/
|
|
||||||
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 = []) => {
|
|
||||||
return new Promise((res, rej) => {
|
|
||||||
function onLoad() {
|
|
||||||
let isReadyInt;
|
|
||||||
isReadyInt = setInterval(isReadyTest, 100);
|
|
||||||
function isReadyTest() {
|
|
||||||
if (selectors.every((selector) => document.querySelector(selector))) {
|
|
||||||
clearInterval(isReadyInt);
|
|
||||||
res(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isReadyTest();
|
|
||||||
}
|
|
||||||
if (document.readyState !== 'complete') {
|
|
||||||
document.addEventListener('readystatechange', (event) => {
|
|
||||||
if (document.readyState === 'complete') onLoad();
|
|
||||||
});
|
|
||||||
} else onLoad();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* loads/applies a css stylesheet to the page
|
|
||||||
* @param {string} path - a url or within-the-enhancer filepath
|
|
||||||
*/
|
|
||||||
web.loadStyleset = (path) => {
|
|
||||||
document.head.appendChild(
|
|
||||||
web.createElement(
|
|
||||||
web.html`<link rel="stylesheet" href="${
|
|
||||||
path.startsWith('https://') ? path : chrome.runtime.getURL(path)
|
|
||||||
}">`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* fetch an icon from the icons folder
|
|
||||||
* @param {string} path - the path to the icon within the folder
|
|
||||||
* @returns {string} the content of an svg file
|
|
||||||
*/
|
|
||||||
web.getIcon = (path) => fs.getText(`icons/${path}.svg`);
|
|
||||||
/** replace all [data-icon] elems with matching svgs from the icons folder */
|
|
||||||
web.loadIcons = () => {
|
|
||||||
document.querySelectorAll('[data-icon]:not(svg:not(:empty))').forEach(async (icon) => {
|
|
||||||
const svg = web.createElement(await web.getIcon(icon.dataset.icon));
|
|
||||||
for (const attr of icon.attributes) {
|
|
||||||
svg.setAttribute(attr.name, attr.value);
|
|
||||||
}
|
|
||||||
icon.replaceWith(svg);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* create a html fragment (collection of nodes) from a string
|
|
||||||
* @param {string} html - a valid html string
|
|
||||||
* @returns {Element} the constructed html fragment
|
|
||||||
*/
|
|
||||||
web.createFragment = (html = '') => {
|
|
||||||
return document.createRange().createContextualFragment(
|
|
||||||
html.includes('<pre')
|
|
||||||
? html.trim()
|
|
||||||
: html
|
|
||||||
.split(/\n/)
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.filter((line) => line.length)
|
|
||||||
.join(' ')
|
|
||||||
);
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* create a single 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) => {
|
|
||||||
return web.createFragment(html).children[0];
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* replace special html characters with escaped versions
|
|
||||||
* @param {string} str
|
|
||||||
* @returns {string} escaped string
|
|
||||||
*/
|
|
||||||
web.escapeHtml = (str) =>
|
|
||||||
str
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/'/g, ''')
|
|
||||||
.replace(/"/g, '"');
|
|
||||||
/**
|
|
||||||
* a tagged template processor for syntax higlighting purposes
|
|
||||||
* (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('');
|
|
||||||
web._hotkeyEventListeners = [];
|
|
||||||
/**
|
|
||||||
* 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.addHotkeyListener = (keys, callback) => {
|
|
||||||
if (typeof keys === 'string') keys = keys.split('+');
|
|
||||||
if (!web._hotkeyEvent) {
|
|
||||||
web._hotkeyEvent = document.addEventListener('keyup', (event) => {
|
|
||||||
for (const hotkey of web._hotkeyEventListeners) {
|
|
||||||
const matchesEvent = hotkey.keys.every((key) => {
|
|
||||||
const modifiers = {
|
|
||||||
altKey: 'alt',
|
|
||||||
ctrlKey: 'ctrl',
|
|
||||||
metaKey: 'meta',
|
|
||||||
shiftKey: 'shift',
|
|
||||||
};
|
|
||||||
for (const modifier in modifiers) {
|
|
||||||
if (key.toLowerCase() === modifiers[modifier] && event[modifier]) return true;
|
|
||||||
}
|
|
||||||
const pressedKeycode = [event.key.toLowerCase(), event.code.toLowerCase()];
|
|
||||||
if (pressedKeycode.includes(key.toLowerCase())) return true;
|
|
||||||
});
|
|
||||||
if (matchesEvent) hotkey.callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
web._hotkeyEventListeners.push({ keys, callback });
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* remove a listener added with web.addHotkeyListener
|
|
||||||
* @param {function} callback
|
|
||||||
*/
|
|
||||||
web.removeHotkeyListener = (callback) => {
|
|
||||||
web._hotkeyEventListeners = web._hotkeyEventListeners.filter(
|
|
||||||
(listener) => listener.callback !== callback
|
|
||||||
);
|
|
||||||
};
|
|
||||||
web._documentObserverListeners = [];
|
|
||||||
web._documentObserverEvents = [];
|
|
||||||
/**
|
|
||||||
* add a listener to watch for changes to the dom
|
|
||||||
* @param {onDocumentObservedCallback} callback
|
|
||||||
* @param {array<string>} [selectors]
|
|
||||||
*/
|
|
||||||
web.addDocumentObserver = (callback, selectors = []) => {
|
|
||||||
if (!web._documentObserver) {
|
|
||||||
const handle = (queue) => {
|
|
||||||
while (queue.length) {
|
|
||||||
const event = queue.shift();
|
|
||||||
for (const listener of web._documentObserverListeners) {
|
|
||||||
if (
|
|
||||||
!listener.selectors.length ||
|
|
||||||
listener.selectors.some(
|
|
||||||
(selector) =>
|
|
||||||
event.target.matches(selector) || event.target.matches(`${selector} *`)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
listener.callback(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
web._documentObserver = new MutationObserver((list, observer) => {
|
|
||||||
if (!web._documentObserverEvents.length)
|
|
||||||
requestIdleCallback(() => handle(web._documentObserverEvents));
|
|
||||||
web._documentObserverEvents.push(...list);
|
|
||||||
});
|
|
||||||
web._documentObserver.observe(document.body, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
attributes: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
web._documentObserverListeners.push({ callback, selectors });
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* remove a listener added with web.addDocumentObserver
|
|
||||||
* @param {onDocumentObservedCallback} callback
|
|
||||||
*/
|
|
||||||
web.removeDocumentObserver = (callback) => {
|
|
||||||
web._documentObserverListeners = web._documentObserverListeners.filter(
|
|
||||||
(listener) => listener.callback !== callback
|
|
||||||
);
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* @callback onDocumentObservedCallback
|
|
||||||
* @param {MutationRecord} event - the observed dom mutation event
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* add a tooltip to show extra information on hover
|
|
||||||
* @param {HTMLElement} $element - the element that will trigger the tooltip when hovered
|
|
||||||
* @param {string} text - the markdown content of the tooltip
|
|
||||||
*/
|
|
||||||
web.addTooltip = ($element, text) => {
|
|
||||||
if (!web._$tooltip) {
|
|
||||||
web._$tooltip = web.createElement(web.html`<div class="enhancer--tooltip"></div>`);
|
|
||||||
document.body.append(web._$tooltip);
|
|
||||||
}
|
|
||||||
text = fmt.md.render(text);
|
|
||||||
$element.addEventListener('mouseover', (event) => {
|
|
||||||
web._$tooltip.innerHTML = text;
|
|
||||||
web._$tooltip.style.display = 'block';
|
|
||||||
});
|
|
||||||
$element.addEventListener('mousemove', (event) => {
|
|
||||||
web._$tooltip.style.top = event.clientY - web._$tooltip.clientHeight + 'px';
|
|
||||||
web._$tooltip.style.left =
|
|
||||||
event.clientX < window.innerWidth / 2 ? event.clientX + 20 + 'px' : '';
|
|
||||||
});
|
|
||||||
$element.addEventListener('mouseout', (event) => {
|
|
||||||
web._$tooltip.style.display = '';
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* helpers for formatting or parsing text
|
|
||||||
* @namespace fmt
|
|
||||||
*/
|
|
||||||
export const fmt = {};
|
|
||||||
import './dep/jscolor.min.js';
|
|
||||||
/** color picker with alpha channel using https://jscolor.com/ */
|
|
||||||
fmt.JSColor = JSColor;
|
|
||||||
import './dep/prism.js';
|
|
||||||
/** syntax highlighting using https://prismjs.com/ */
|
|
||||||
fmt.Prism = Prism;
|
|
||||||
fmt.Prism.manual = true;
|
|
||||||
fmt.Prism.hooks.add('complete', async (event) => {
|
|
||||||
event.element.parentElement.removeAttribute('tabindex');
|
|
||||||
event.element.parentElement.parentElement
|
|
||||||
.querySelector('.copy-to-clipboard-button')
|
|
||||||
.prepend(web.createElement(await web.getIcon('fa/regular/copy')));
|
|
||||||
// if (!fmt.Prism._stylesheetLoaded) {
|
|
||||||
// web.loadStyleset('./dep/prism.css');
|
|
||||||
// fmt.Prism._stylesheetLoaded = true;
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
// delete memberThis['Prism'];
|
|
||||||
import './dep/markdown-it.min.js';
|
|
||||||
/** markdown -> html using https://github.com/markdown-it/markdown-it/ */
|
|
||||||
fmt.md = new markdownit({
|
|
||||||
linkify: true,
|
|
||||||
highlight: (str, lang) =>
|
|
||||||
web.html`<pre class="language-${lang || 'plaintext'} match-braces"><code>${web.escapeHtml(
|
|
||||||
str
|
|
||||||
)}</code></pre>`,
|
|
||||||
});
|
|
||||||
fmt.md.renderer.rules.code_block = (tokens, idx, options, env, slf) => {
|
|
||||||
const attrIdx = tokens[idx].attrIndex('class');
|
|
||||||
if (attrIdx === -1) {
|
|
||||||
tokens[idx].attrPush(['class', 'match-braces language-plaintext']);
|
|
||||||
} else tokens[idx].attrs[attrIdx][1] = 'match-braces language-plaintext';
|
|
||||||
return web.html`<pre${slf.renderAttrs(tokens[idx])}><code>${web.escapeHtml(
|
|
||||||
tokens[idx].content
|
|
||||||
)}</code></pre>\n`;
|
|
||||||
};
|
|
||||||
fmt.md.core.ruler.push(
|
|
||||||
'heading_ids',
|
|
||||||
function (md, state) {
|
|
||||||
const slugs = new Set();
|
|
||||||
state.tokens.forEach(function (token, i) {
|
|
||||||
if (token.type === 'heading_open') {
|
|
||||||
const text = md.renderer.render(state.tokens[i + 1].children, md.options),
|
|
||||||
slug = fmt.slugger(text, slugs);
|
|
||||||
slugs.add(slug);
|
|
||||||
const attrIdx = token.attrIndex('id');
|
|
||||||
if (attrIdx === -1) {
|
|
||||||
token.attrPush(['id', slug]);
|
|
||||||
} else token.attrs[attrIdx][1] = slug;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}.bind(null, fmt.md)
|
|
||||||
);
|
|
||||||
// delete memberThis['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 {string} the generated slug
|
|
||||||
*/
|
|
||||||
fmt.slugger = (heading, slugs = new Set()) => {
|
|
||||||
heading = heading
|
|
||||||
.replace(/\s/g, '-')
|
|
||||||
.replace(/[^A-Za-z0-9-_]/g, '')
|
|
||||||
.toLowerCase();
|
|
||||||
let i = 0,
|
|
||||||
slug = heading;
|
|
||||||
while (slugs.has(slug)) {
|
|
||||||
i++;
|
|
||||||
slug = `${heading}-${i}`;
|
|
||||||
}
|
|
||||||
return slug;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* pattern validators
|
|
||||||
* @namespace regexers
|
|
||||||
*/
|
|
||||||
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 = () => {}) => {
|
|
||||||
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;
|
|
||||||
err(`invalid uuid ${str}`);
|
|
||||||
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 = () => {}) => {
|
|
||||||
const match = str.match(
|
|
||||||
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/i
|
|
||||||
);
|
|
||||||
if (match && match.length) return true;
|
|
||||||
err(`invalid semver ${str}`);
|
|
||||||
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 = () => {}) => {
|
|
||||||
const match = str.match(
|
|
||||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i
|
|
||||||
);
|
|
||||||
if (match && match.length) return true;
|
|
||||||
err(`invalid email ${str}`);
|
|
||||||
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 = () => {}) => {
|
|
||||||
const match = str.match(
|
|
||||||
/^[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/i
|
|
||||||
);
|
|
||||||
if (match && match.length) return true;
|
|
||||||
err(`invalid url ${str}`);
|
|
||||||
return env.ERROR;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* check for a valid color (https://regexr.com/39cgj)
|
|
||||||
* @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.color = (str, err = () => {}) => {
|
|
||||||
const match = str.match(/^(?:#|0x)(?:[a-f0-9]{3}|[a-f0-9]{6})\b|(?:rgb|hsl)a?\([^\)]*\)$/i);
|
|
||||||
if (match && match.length) return true;
|
|
||||||
err(`invalid color ${str}`);
|
|
||||||
return env.ERROR;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* an api for interacting with the enhancer's repository of mods
|
|
||||||
* @namespace registry
|
|
||||||
*/
|
|
||||||
export const registry = {};
|
|
||||||
/** mod ids whitelisted as part of the enhancer's core, permanently enabled */
|
|
||||||
registry.CORE = [
|
|
||||||
'a6621988-551d-495a-97d8-3c568bca2e9e',
|
|
||||||
'0f0bf8b6-eae6-4273-b307-8fc43f2ee082',
|
|
||||||
];
|
|
||||||
/**
|
|
||||||
* internally used to validate mod.json files and provide helpful errors
|
|
||||||
* @private
|
|
||||||
* @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) => {
|
|
||||||
let conditions = [
|
|
||||||
check('name', mod.name, typeof mod.name === 'string'),
|
|
||||||
check('id', mod.id, typeof mod.id === 'string').then((id) =>
|
|
||||||
id === env.ERROR ? env.ERROR : regexers.uuid(id, err)
|
|
||||||
),
|
|
||||||
check('version', mod.version, typeof mod.version === 'string').then((version) =>
|
|
||||||
version === env.ERROR ? env.ERROR : regexers.semver(version, err)
|
|
||||||
),
|
|
||||||
check('description', mod.description, typeof mod.description === 'string'),
|
|
||||||
check(
|
|
||||||
'preview',
|
|
||||||
mod.preview,
|
|
||||||
mod.preview === undefined || typeof mod.preview === 'string'
|
|
||||||
).then((preview) =>
|
|
||||||
preview ? (preview === env.ERROR ? env.ERROR : regexers.url(preview, err)) : undefined
|
|
||||||
),
|
|
||||||
check('tags', mod.tags, Array.isArray(mod.tags)).then((tags) =>
|
|
||||||
tags === env.ERROR
|
|
||||||
? env.ERROR
|
|
||||||
: tags.map((tag) => check('tag', tag, typeof tag === 'string'))
|
|
||||||
),
|
|
||||||
check('authors', mod.authors, Array.isArray(mod.authors)).then((authors) =>
|
|
||||||
authors === env.ERROR
|
|
||||||
? env.ERROR
|
|
||||||
: authors.map((author) => [
|
|
||||||
check('author.name', author.name, typeof author.name === 'string'),
|
|
||||||
check('author.email', author.email, typeof author.email === 'string').then(
|
|
||||||
(email) => (email === env.ERROR ? env.ERROR : regexers.email(email, err))
|
|
||||||
),
|
|
||||||
check('author.url', author.url, typeof author.url === 'string').then((url) =>
|
|
||||||
url === env.ERROR ? env.ERROR : regexers.url(url, err)
|
|
||||||
),
|
|
||||||
check('author.icon', author.icon, typeof author.icon === 'string').then((icon) =>
|
|
||||||
icon === env.ERROR ? env.ERROR : regexers.url(icon, err)
|
|
||||||
),
|
|
||||||
])
|
|
||||||
),
|
|
||||||
check(
|
|
||||||
'environments',
|
|
||||||
mod.environments,
|
|
||||||
!mod.environments || Array.isArray(mod.environments)
|
|
||||||
).then((environments) =>
|
|
||||||
environments
|
|
||||||
? environments === env.ERROR
|
|
||||||
? env.ERROR
|
|
||||||
: environments.map((environment) =>
|
|
||||||
check('environment', environment, env.supported.includes(environment))
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
),
|
|
||||||
check(
|
|
||||||
'css',
|
|
||||||
mod.css,
|
|
||||||
mod.css && typeof mod.css === 'object' && !Array.isArray(mod.css)
|
|
||||||
).then((css) =>
|
|
||||||
css
|
|
||||||
? css === env.ERROR
|
|
||||||
? env.ERROR
|
|
||||||
: ['frame', 'client', 'menu']
|
|
||||||
.filter((dest) => css[dest])
|
|
||||||
.map(async (dest) =>
|
|
||||||
check(`css.${dest}`, css[dest], Array.isArray(css[dest])).then((files) =>
|
|
||||||
files === env.ERROR
|
|
||||||
? env.ERROR
|
|
||||||
: files.map(async (file) =>
|
|
||||||
check(
|
|
||||||
`css.${dest} file`,
|
|
||||||
file,
|
|
||||||
await fs.isFile(`repo/${mod._dir}/${file}`, '.css')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
),
|
|
||||||
check('js', mod.js, mod.js && typeof mod.js === 'object' && !Array.isArray(mod.js)).then(
|
|
||||||
async (js) => {
|
|
||||||
if (js === env.ERROR) return env.ERROR;
|
|
||||||
if (!js) return undefined;
|
|
||||||
return [
|
|
||||||
check('js.client', js.client, !js.client || Array.isArray(js.client)).then(
|
|
||||||
(client) => {
|
|
||||||
if (client === env.ERROR) return env.ERROR;
|
|
||||||
if (!client) return undefined;
|
|
||||||
return client.map(async (file) =>
|
|
||||||
check(
|
|
||||||
'js.client file',
|
|
||||||
file,
|
|
||||||
await fs.isFile(`repo/${mod._dir}/${file}`, '.js')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
check('js.electron', js.electron, !js.electron || Array.isArray(js.electron)).then(
|
|
||||||
(electron) => {
|
|
||||||
if (electron === env.ERROR) return env.ERROR;
|
|
||||||
if (!electron) return undefined;
|
|
||||||
return electron.map((file) =>
|
|
||||||
check(
|
|
||||||
'js.electron file',
|
|
||||||
file,
|
|
||||||
file && typeof file === 'object' && !Array.isArray(file)
|
|
||||||
).then(async (file) =>
|
|
||||||
file === env.ERROR
|
|
||||||
? env.ERROR
|
|
||||||
: [
|
|
||||||
check(
|
|
||||||
'js.electron file source',
|
|
||||||
file.source,
|
|
||||||
await fs.isFile(`repo/${mod._dir}/${file.source}`, '.js')
|
|
||||||
),
|
|
||||||
// referencing the file within the electron app
|
|
||||||
// existence can't be validated, so only format is
|
|
||||||
check(
|
|
||||||
'js.electron file target',
|
|
||||||
file.target,
|
|
||||||
typeof file.target === 'string' && file.target.endsWith('.js')
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
),
|
|
||||||
check('options', mod.options, Array.isArray(mod.options)).then((options) =>
|
|
||||||
options === env.ERROR
|
|
||||||
? env.ERROR
|
|
||||||
: options.map((option) => {
|
|
||||||
const conditions = [];
|
|
||||||
switch (option.type) {
|
|
||||||
case 'toggle':
|
|
||||||
conditions.push(
|
|
||||||
check('option.value', option.value, typeof option.value === 'boolean')
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'select':
|
|
||||||
conditions.push(
|
|
||||||
check('option.values', option.values, Array.isArray(option.values)).then(
|
|
||||||
(value) =>
|
|
||||||
value === env.ERROR
|
|
||||||
? env.ERROR
|
|
||||||
: value.map((option) =>
|
|
||||||
check('option.values option', option, typeof option === 'string')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'text':
|
|
||||||
conditions.push(
|
|
||||||
check('option.value', option.value, typeof option.value === 'string')
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'number':
|
|
||||||
conditions.push(
|
|
||||||
check('option.value', option.value, typeof option.value === 'number')
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'color':
|
|
||||||
conditions.push(
|
|
||||||
check('option.value', option.value, typeof option.value === 'string').then(
|
|
||||||
(color) => (color === env.ERROR ? env.ERROR : regexers.color(color, err))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'file':
|
|
||||||
conditions.push(
|
|
||||||
check(
|
|
||||||
'option.extensions',
|
|
||||||
option.extensions,
|
|
||||||
!option.extensions || Array.isArray(option.extensions)
|
|
||||||
).then((extensions) =>
|
|
||||||
extensions
|
|
||||||
? extensions === env.ERROR
|
|
||||||
? env.ERROR
|
|
||||||
: extensions.map((ext) =>
|
|
||||||
check('option.extension', ext, typeof ext === 'string')
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return check('option.type', option.type, false);
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
conditions,
|
|
||||||
check(
|
|
||||||
'option.key',
|
|
||||||
option.key,
|
|
||||||
typeof option.key === 'string' && !option.key.match(/\s/)
|
|
||||||
),
|
|
||||||
check('option.label', option.label, typeof option.label === 'string'),
|
|
||||||
check(
|
|
||||||
'option.tooltip',
|
|
||||||
option.tooltip,
|
|
||||||
!option.tooltip || typeof option.tooltip === 'string'
|
|
||||||
),
|
|
||||||
check(
|
|
||||||
'option.environments',
|
|
||||||
option.environments,
|
|
||||||
!option.environments || Array.isArray(option.environments)
|
|
||||||
).then((environments) =>
|
|
||||||
environments
|
|
||||||
? environments === env.ERROR
|
|
||||||
? env.ERROR
|
|
||||||
: environments.map((environment) =>
|
|
||||||
check(
|
|
||||||
'option.environment',
|
|
||||||
environment,
|
|
||||||
env.supported.includes(environment)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
),
|
|
||||||
];
|
|
||||||
})
|
|
||||||
),
|
|
||||||
];
|
|
||||||
do {
|
|
||||||
conditions = await Promise.all(conditions.flat(Infinity));
|
|
||||||
} while (conditions.some((condition) => Array.isArray(condition)));
|
|
||||||
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) => {
|
|
||||||
const mod =
|
|
||||||
regexers.uuid(id) !== env.ERROR
|
|
||||||
? (await registry.get()).find((mod) => mod.id === id)
|
|
||||||
: undefined;
|
|
||||||
if (!mod || !mod.options) return {};
|
|
||||||
const defaults = {};
|
|
||||||
for (const opt of mod.options) {
|
|
||||||
switch (opt.type) {
|
|
||||||
case 'toggle':
|
|
||||||
case 'text':
|
|
||||||
case 'number':
|
|
||||||
case 'color':
|
|
||||||
defaults[opt.key] = opt.value;
|
|
||||||
break;
|
|
||||||
case 'select':
|
|
||||||
defaults[opt.key] = opt.values[0];
|
|
||||||
break;
|
|
||||||
case 'file':
|
|
||||||
defaults[opt.key] = undefined;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defaults;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* 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.isEnabled(mod.id))
|
|
||||||
*/
|
|
||||||
registry.get = async (filter = (mod) => true) => {
|
|
||||||
if (!registry._errors) registry._errors = [];
|
|
||||||
if (!registry._list || !registry._list.length) {
|
|
||||||
registry._list = [];
|
|
||||||
for (const dir of await fs.getJSON('repo/registry.json')) {
|
|
||||||
const err = (message) => [registry._errors.push({ source: dir, message }), env.ERROR][1];
|
|
||||||
try {
|
|
||||||
const mod = await fs.getJSON(`repo/${dir}/mod.json`);
|
|
||||||
mod._dir = dir;
|
|
||||||
mod.tags = mod.tags ?? [];
|
|
||||||
mod.css = mod.css ?? {};
|
|
||||||
mod.js = mod.js ?? {};
|
|
||||||
mod.options = mod.options ?? [];
|
|
||||||
const check = (prop, value, condition) =>
|
|
||||||
Promise.resolve(
|
|
||||||
condition ? value : err(`invalid ${prop} ${JSON.stringify(value)}`)
|
|
||||||
),
|
|
||||||
validation = await registry.validate(mod, err, check);
|
|
||||||
if (validation.every((condition) => condition !== env.ERROR)) registry._list.push(mod);
|
|
||||||
} catch {
|
|
||||||
err('invalid mod.json');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const list = [];
|
|
||||||
for (const mod of registry._list) if (await filter(mod)) list.push(mod);
|
|
||||||
return list;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* gets a list of errors encountered while validating the mod.json files
|
|
||||||
* @returns {object} - {source: directory, message: string }
|
|
||||||
*/
|
|
||||||
registry.errors = async () => {
|
|
||||||
if (!registry._errors) await registry.get();
|
|
||||||
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.isEnabled = 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;
|
|
||||||
return await storage.get('_mods', id, false);
|
|
||||||
};
|
|
22
extension/api/_.mjs
Normal file
22
extension/api/_.mjs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* notion-enhancer: api
|
||||||
|
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** @module notion-enhancer/api */
|
||||||
|
|
||||||
|
/** environment-specific methods and constants */
|
||||||
|
export * as env from './env.mjs';
|
||||||
|
/** helpers for formatting or parsing text */
|
||||||
|
export * as fmt from './fmt.mjs';
|
||||||
|
/** environment-specific filesystem reading */
|
||||||
|
export * as fs from './fs.mjs';
|
||||||
|
/** pattern validators */
|
||||||
|
export * as regex from './regex.mjs';
|
||||||
|
/** interactions with the enhancer's repository of mods */
|
||||||
|
// export * as registry from './registry.mjs';
|
||||||
|
/** environment-specific data persistence */
|
||||||
|
export * as storage from './storage.mjs';
|
46
extension/api/env.mjs
Normal file
46
extension/api/env.mjs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* an error constant used in validation, distinct from null or undefined
|
||||||
|
* @constant {Symbol}
|
||||||
|
*/
|
||||||
|
export const ERROR = Symbol();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the environment/platform name code is currently being executed in
|
||||||
|
* @constant {string}
|
||||||
|
*/
|
||||||
|
export const name = 'extension';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* all environments/platforms currently supported by the enhancer
|
||||||
|
* @constant {array<string>}
|
||||||
|
*/
|
||||||
|
export const supported = ['linux', 'win32', 'darwin', 'extension'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the current version of the enhancer
|
||||||
|
* @constant {string}
|
||||||
|
*/
|
||||||
|
export const version = chrome.runtime.getManifest().version;
|
||||||
|
|
||||||
|
/** open the enhancer's menu */
|
||||||
|
export const openEnhancerMenu = () =>
|
||||||
|
chrome.runtime.sendMessage({ action: 'openEnhancerMenu' });
|
||||||
|
|
||||||
|
/** focus an active notion tab */
|
||||||
|
export const focusNotion = () => chrome.runtime.sendMessage({ action: 'focusNotion' });
|
||||||
|
|
||||||
|
/** reload all notion and enhancer menu tabs to apply changes */
|
||||||
|
export const reloadTabs = () => chrome.runtime.sendMessage({ action: 'reloadTabs' });
|
80
extension/api/fmt.mjs
Normal file
80
extension/api/fmt.mjs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* notion-enhancer: api
|
||||||
|
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* helpers for formatting or parsing text
|
||||||
|
* @module notion-enhancer/api/fmt
|
||||||
|
*/
|
||||||
|
|
||||||
|
import '../dep/prism.min.js';
|
||||||
|
/** syntax highlighting using https://prismjs.com/ */
|
||||||
|
export const prism = Prism;
|
||||||
|
Prism.manual = true;
|
||||||
|
Prism.hooks.add('complete', async (event) => {
|
||||||
|
event.element.parentElement.removeAttribute('tabindex');
|
||||||
|
event.element.parentElement.parentElement
|
||||||
|
.querySelector('.copy-to-clipboard-button')
|
||||||
|
.prepend(web.createElement(await web.getIcon('fa/regular/copy')));
|
||||||
|
});
|
||||||
|
|
||||||
|
import '../dep/markdown-it.min.js';
|
||||||
|
/** markdown -> html using https://github.com/markdown-it/markdown-it/ */
|
||||||
|
export const md = new markdownit({
|
||||||
|
linkify: true,
|
||||||
|
highlight: (str, lang) =>
|
||||||
|
web.html`<pre class="language-${lang || 'plaintext'} match-braces"><code>${web.escapeHtml(
|
||||||
|
str
|
||||||
|
)}</code></pre>`,
|
||||||
|
});
|
||||||
|
md.renderer.rules.code_block = (tokens, idx, options, env, slf) => {
|
||||||
|
const attrIdx = tokens[idx].attrIndex('class');
|
||||||
|
if (attrIdx === -1) {
|
||||||
|
tokens[idx].attrPush(['class', 'match-braces language-plaintext']);
|
||||||
|
} else tokens[idx].attrs[attrIdx][1] = 'match-braces language-plaintext';
|
||||||
|
return web.html`<pre${slf.renderAttrs(tokens[idx])}><code>${web.escapeHtml(
|
||||||
|
tokens[idx].content
|
||||||
|
)}</code></pre>\n`;
|
||||||
|
};
|
||||||
|
md.core.ruler.push(
|
||||||
|
'heading_ids',
|
||||||
|
function (md, state) {
|
||||||
|
const slugs = new Set();
|
||||||
|
state.tokens.forEach(function (token, i) {
|
||||||
|
if (token.type === 'heading_open') {
|
||||||
|
const text = md.renderer.render(state.tokens[i + 1].children, md.options),
|
||||||
|
slug = fmt.slugger(text, slugs);
|
||||||
|
slugs.add(slug);
|
||||||
|
const attrIdx = token.attrIndex('id');
|
||||||
|
if (attrIdx === -1) {
|
||||||
|
token.attrPush(['id', slug]);
|
||||||
|
} else token.attrs[attrIdx][1] = slug;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}.bind(null, md)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {string} the generated slug
|
||||||
|
*/
|
||||||
|
export const slugger = (heading, slugs = new Set()) => {
|
||||||
|
heading = heading
|
||||||
|
.replace(/\s/g, '-')
|
||||||
|
.replace(/[^A-Za-z0-9-_]/g, '')
|
||||||
|
.toLowerCase();
|
||||||
|
let i = 0,
|
||||||
|
slug = heading;
|
||||||
|
while (slugs.has(slug)) {
|
||||||
|
i++;
|
||||||
|
slug = `${heading}-${i}`;
|
||||||
|
}
|
||||||
|
return slug;
|
||||||
|
};
|
49
extension/api/fs.mjs
Normal file
49
extension/api/fs.mjs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* 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 filesystem reading
|
||||||
|
* @module notion-enhancer/api/fs
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* transform a path relative to the enhancer root directory into an absolute path
|
||||||
|
* @param {string} path - a url or within-the-enhancer filepath
|
||||||
|
* @returns {string} an absolute filepath
|
||||||
|
*/
|
||||||
|
export const localPath = chrome.runtime.getURL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
export const getJSON = (path) =>
|
||||||
|
fetch(path.startsWith('http') ? path : localPath(path)).then((res) => res.json());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fetch a text file's contents
|
||||||
|
* @param {string} path - a url or within-the-enhancer filepath
|
||||||
|
* @returns {string} the text content of the requested file
|
||||||
|
*/
|
||||||
|
export const getText = (path) =>
|
||||||
|
fetch(path.startsWith('http') ? path : localPath(path)).then((res) => 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
|
||||||
|
*/
|
||||||
|
export const isFile = async (path) => {
|
||||||
|
try {
|
||||||
|
await fetch(path.startsWith('http') ? path : localPath(path));
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
62
extension/api/regex.mjs
Normal file
62
extension/api/regex.mjs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* 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 validators
|
||||||
|
* @module notion-enhancer/api/regex
|
||||||
|
*/
|
||||||
|
|
||||||
|
const patterns = {
|
||||||
|
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 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);
|
342
extension/api/registry.mjs
Normal file
342
extension/api/registry.mjs
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
/*
|
||||||
|
* notion-enhancer: api
|
||||||
|
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* interactions with the enhancer's repository of mods
|
||||||
|
* @module notion-enhancer/api/registry
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as regex from './regex.mjs';
|
||||||
|
|
||||||
|
/** mod ids whitelisted as part of the enhancer's core, permanently enabled */
|
||||||
|
export const CORE = [
|
||||||
|
'a6621988-551d-495a-97d8-3c568bca2e9e',
|
||||||
|
'0f0bf8b6-eae6-4273-b307-8fc43f2ee082',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* internally used to validate mod.json files and provide helpful errors
|
||||||
|
* @private
|
||||||
|
* @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) => {
|
||||||
|
let conditions = [
|
||||||
|
check('name', mod.name, typeof mod.name === 'string'),
|
||||||
|
check('id', mod.id, typeof mod.id === 'string').then((id) =>
|
||||||
|
id === env.ERROR ? env.ERROR : regex.uuid(id, err)
|
||||||
|
),
|
||||||
|
check('version', mod.version, typeof mod.version === 'string').then((version) =>
|
||||||
|
version === env.ERROR ? env.ERROR : regex.semver(version, err)
|
||||||
|
),
|
||||||
|
check('description', mod.description, typeof mod.description === 'string'),
|
||||||
|
check(
|
||||||
|
'preview',
|
||||||
|
mod.preview,
|
||||||
|
mod.preview === undefined || typeof mod.preview === 'string'
|
||||||
|
).then((preview) =>
|
||||||
|
preview ? (preview === env.ERROR ? env.ERROR : regex.url(preview, err)) : undefined
|
||||||
|
),
|
||||||
|
check('tags', mod.tags, Array.isArray(mod.tags)).then((tags) =>
|
||||||
|
tags === env.ERROR
|
||||||
|
? env.ERROR
|
||||||
|
: tags.map((tag) => check('tag', tag, typeof tag === 'string'))
|
||||||
|
),
|
||||||
|
check('authors', mod.authors, Array.isArray(mod.authors)).then((authors) =>
|
||||||
|
authors === env.ERROR
|
||||||
|
? env.ERROR
|
||||||
|
: authors.map((author) => [
|
||||||
|
check('author.name', author.name, typeof author.name === 'string'),
|
||||||
|
check('author.email', author.email, typeof author.email === 'string').then(
|
||||||
|
(email) => (email === env.ERROR ? env.ERROR : regex.email(email, err))
|
||||||
|
),
|
||||||
|
check('author.url', author.url, typeof author.url === 'string').then((url) =>
|
||||||
|
url === env.ERROR ? env.ERROR : regex.url(url, err)
|
||||||
|
),
|
||||||
|
check('author.icon', author.icon, typeof author.icon === 'string').then((icon) =>
|
||||||
|
icon === env.ERROR ? env.ERROR : regex.url(icon, err)
|
||||||
|
),
|
||||||
|
])
|
||||||
|
),
|
||||||
|
check(
|
||||||
|
'environments',
|
||||||
|
mod.environments,
|
||||||
|
!mod.environments || Array.isArray(mod.environments)
|
||||||
|
).then((environments) =>
|
||||||
|
environments
|
||||||
|
? environments === env.ERROR
|
||||||
|
? env.ERROR
|
||||||
|
: environments.map((environment) =>
|
||||||
|
check('environment', environment, env.supported.includes(environment))
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
),
|
||||||
|
check(
|
||||||
|
'css',
|
||||||
|
mod.css,
|
||||||
|
mod.css && typeof mod.css === 'object' && !Array.isArray(mod.css)
|
||||||
|
).then((css) =>
|
||||||
|
css
|
||||||
|
? css === env.ERROR
|
||||||
|
? env.ERROR
|
||||||
|
: ['frame', 'client', 'menu']
|
||||||
|
.filter((dest) => css[dest])
|
||||||
|
.map(async (dest) =>
|
||||||
|
check(`css.${dest}`, css[dest], Array.isArray(css[dest])).then((files) =>
|
||||||
|
files === env.ERROR
|
||||||
|
? env.ERROR
|
||||||
|
: files.map(async (file) =>
|
||||||
|
check(
|
||||||
|
`css.${dest} file`,
|
||||||
|
file,
|
||||||
|
await fs.isFile(`repo/${mod._dir}/${file}`, '.css')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
),
|
||||||
|
check('js', mod.js, mod.js && typeof mod.js === 'object' && !Array.isArray(mod.js)).then(
|
||||||
|
async (js) => {
|
||||||
|
if (js === env.ERROR) return env.ERROR;
|
||||||
|
if (!js) return undefined;
|
||||||
|
return [
|
||||||
|
check('js.client', js.client, !js.client || Array.isArray(js.client)).then(
|
||||||
|
(client) => {
|
||||||
|
if (client === env.ERROR) return env.ERROR;
|
||||||
|
if (!client) return undefined;
|
||||||
|
return client.map(async (file) =>
|
||||||
|
check(
|
||||||
|
'js.client file',
|
||||||
|
file,
|
||||||
|
await fs.isFile(`repo/${mod._dir}/${file}`, '.js')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
check('js.electron', js.electron, !js.electron || Array.isArray(js.electron)).then(
|
||||||
|
(electron) => {
|
||||||
|
if (electron === env.ERROR) return env.ERROR;
|
||||||
|
if (!electron) return undefined;
|
||||||
|
return electron.map((file) =>
|
||||||
|
check(
|
||||||
|
'js.electron file',
|
||||||
|
file,
|
||||||
|
file && typeof file === 'object' && !Array.isArray(file)
|
||||||
|
).then(async (file) =>
|
||||||
|
file === env.ERROR
|
||||||
|
? env.ERROR
|
||||||
|
: [
|
||||||
|
check(
|
||||||
|
'js.electron file source',
|
||||||
|
file.source,
|
||||||
|
await fs.isFile(`repo/${mod._dir}/${file.source}`, '.js')
|
||||||
|
),
|
||||||
|
// referencing the file within the electron app
|
||||||
|
// existence can't be validated, so only format is
|
||||||
|
check(
|
||||||
|
'js.electron file target',
|
||||||
|
file.target,
|
||||||
|
typeof file.target === 'string' && file.target.endsWith('.js')
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
),
|
||||||
|
check('options', mod.options, Array.isArray(mod.options)).then((options) =>
|
||||||
|
options === env.ERROR
|
||||||
|
? env.ERROR
|
||||||
|
: options.map((option) => {
|
||||||
|
const conditions = [];
|
||||||
|
switch (option.type) {
|
||||||
|
case 'toggle':
|
||||||
|
conditions.push(
|
||||||
|
check('option.value', option.value, typeof option.value === 'boolean')
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'select':
|
||||||
|
conditions.push(
|
||||||
|
check('option.values', option.values, Array.isArray(option.values)).then(
|
||||||
|
(value) =>
|
||||||
|
value === env.ERROR
|
||||||
|
? env.ERROR
|
||||||
|
: value.map((option) =>
|
||||||
|
check('option.values option', option, typeof option === 'string')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'text':
|
||||||
|
conditions.push(
|
||||||
|
check('option.value', option.value, typeof option.value === 'string')
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'number':
|
||||||
|
conditions.push(
|
||||||
|
check('option.value', option.value, typeof option.value === 'number')
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'color':
|
||||||
|
conditions.push(
|
||||||
|
check('option.value', option.value, typeof option.value === 'string').then(
|
||||||
|
(color) => (color === env.ERROR ? env.ERROR : regex.color(color, err))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'file':
|
||||||
|
conditions.push(
|
||||||
|
check(
|
||||||
|
'option.extensions',
|
||||||
|
option.extensions,
|
||||||
|
!option.extensions || Array.isArray(option.extensions)
|
||||||
|
).then((extensions) =>
|
||||||
|
extensions
|
||||||
|
? extensions === env.ERROR
|
||||||
|
? env.ERROR
|
||||||
|
: extensions.map((ext) =>
|
||||||
|
check('option.extension', ext, typeof ext === 'string')
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return check('option.type', option.type, false);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
conditions,
|
||||||
|
check(
|
||||||
|
'option.key',
|
||||||
|
option.key,
|
||||||
|
typeof option.key === 'string' && !option.key.match(/\s/)
|
||||||
|
),
|
||||||
|
check('option.label', option.label, typeof option.label === 'string'),
|
||||||
|
check(
|
||||||
|
'option.tooltip',
|
||||||
|
option.tooltip,
|
||||||
|
!option.tooltip || typeof option.tooltip === 'string'
|
||||||
|
),
|
||||||
|
check(
|
||||||
|
'option.environments',
|
||||||
|
option.environments,
|
||||||
|
!option.environments || Array.isArray(option.environments)
|
||||||
|
).then((environments) =>
|
||||||
|
environments
|
||||||
|
? environments === env.ERROR
|
||||||
|
? env.ERROR
|
||||||
|
: environments.map((environment) =>
|
||||||
|
check(
|
||||||
|
'option.environment',
|
||||||
|
environment,
|
||||||
|
env.supported.includes(environment)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
),
|
||||||
|
];
|
||||||
|
do {
|
||||||
|
conditions = await Promise.all(conditions.flat(Infinity));
|
||||||
|
} while (conditions.some((condition) => Array.isArray(condition)));
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
export const defaults = async (id) => {
|
||||||
|
const mod = regex.uuid(id) ? (await registry.get()).find((mod) => mod.id === id) : undefined;
|
||||||
|
if (!mod || !mod.options) return {};
|
||||||
|
const defaults = {};
|
||||||
|
for (const opt of mod.options) {
|
||||||
|
switch (opt.type) {
|
||||||
|
case 'toggle':
|
||||||
|
case 'text':
|
||||||
|
case 'number':
|
||||||
|
case 'color':
|
||||||
|
defaults[opt.key] = opt.value;
|
||||||
|
break;
|
||||||
|
case 'select':
|
||||||
|
defaults[opt.key] = opt.values[0];
|
||||||
|
break;
|
||||||
|
case 'file':
|
||||||
|
defaults[opt.key] = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaults;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get all available 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.isEnabled(mod.id))
|
||||||
|
*/
|
||||||
|
export const get = async (filter = (mod) => true) => {
|
||||||
|
if (!registry._errors) registry._errors = [];
|
||||||
|
if (!registry._list || !registry._list.length) {
|
||||||
|
registry._list = [];
|
||||||
|
for (const dir of await fs.getJSON('repo/registry.json')) {
|
||||||
|
const err = (message) => [registry._errors.push({ source: dir, message }), env.ERROR][1];
|
||||||
|
try {
|
||||||
|
const mod = await fs.getJSON(`repo/${dir}/mod.json`);
|
||||||
|
mod._dir = dir;
|
||||||
|
mod.tags = mod.tags ?? [];
|
||||||
|
mod.css = mod.css ?? {};
|
||||||
|
mod.js = mod.js ?? {};
|
||||||
|
mod.options = mod.options ?? [];
|
||||||
|
const check = (prop, value, condition) =>
|
||||||
|
Promise.resolve(
|
||||||
|
condition ? value : err(`invalid ${prop} ${JSON.stringify(value)}`)
|
||||||
|
),
|
||||||
|
validation = await registry.validate(mod, err, check);
|
||||||
|
if (validation.every((condition) => condition !== env.ERROR)) registry._list.push(mod);
|
||||||
|
} catch {
|
||||||
|
err('invalid mod.json');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const list = [];
|
||||||
|
for (const mod of registry._list) if (await filter(mod)) list.push(mod);
|
||||||
|
return list;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gets a list of errors encountered while validating the mod.json files
|
||||||
|
* @returns {object} - {source: directory, message: string }
|
||||||
|
*/
|
||||||
|
registry.errors = async () => {
|
||||||
|
if (!registry._errors) await registry.get();
|
||||||
|
return registry._errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if a mod is enabled: affected by the core whitelist,
|
||||||
|
* environment and menu configuration
|
||||||
|
* @param {string} id - the uuid of the mod
|
||||||
|
* @returns {boolean} whether or not the mod is enabled
|
||||||
|
*/
|
||||||
|
registry.isEnabled = 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;
|
||||||
|
return await storage.get('_mods', id, false);
|
||||||
|
};
|
106
extension/api/storage.mjs
Normal file
106
extension/api/storage.mjs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
* 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 data persistence
|
||||||
|
* @module notion-enhancer/api/storage
|
||||||
|
*/
|
||||||
|
|
||||||
|
const _queue = [],
|
||||||
|
_onChangeListeners = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get persisted data
|
||||||
|
* @param {array<string>} path - the path of keys to the value being fetched
|
||||||
|
* @param {*} [fallback] - a default value if the path is not matched
|
||||||
|
* @returns {Promise} value ?? fallback
|
||||||
|
*/
|
||||||
|
export const get = (path, fallback = undefined) => {
|
||||||
|
if (!path.length) return fallback;
|
||||||
|
const namespace = path.shift();
|
||||||
|
return new Promise((res, rej) =>
|
||||||
|
chrome.storage.sync.get([namespace], async (values) => {
|
||||||
|
let value = values[namespace];
|
||||||
|
while (path.length) {
|
||||||
|
value = value[path.shift()];
|
||||||
|
if (path.length && !value) {
|
||||||
|
value = fallback;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res(value ?? fallback);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* persist data
|
||||||
|
* @param {array<string>} path - the path of keys to the value being set
|
||||||
|
* @param {*} value - the data to save
|
||||||
|
*/
|
||||||
|
export const set = (path, value) => {
|
||||||
|
if (!path.length) return undefined;
|
||||||
|
const precursor = _queue[_queue.length - 1] || undefined,
|
||||||
|
interaction = new Promise(async (res, rej) => {
|
||||||
|
if (precursor !== undefined) {
|
||||||
|
await precursor;
|
||||||
|
_queue.shift();
|
||||||
|
}
|
||||||
|
const pathClone = [...path],
|
||||||
|
namespace = path.shift();
|
||||||
|
chrome.storage.sync.get([namespace], async (values) => {
|
||||||
|
const update = values[namespace] ?? {};
|
||||||
|
let pointer = update,
|
||||||
|
old;
|
||||||
|
while (true) {
|
||||||
|
const key = path.shift();
|
||||||
|
if (!path.length) {
|
||||||
|
old = pointer[key];
|
||||||
|
pointer[key] = value;
|
||||||
|
break;
|
||||||
|
} else if (!pointer[key]) pointer[key] = {};
|
||||||
|
pointer = pointer[key];
|
||||||
|
}
|
||||||
|
chrome.storage.sync.set({ [namespace]: update }, () => {
|
||||||
|
_onChangeListeners.forEach((listener) =>
|
||||||
|
listener({ type: 'set', path: pathClone, new: value, old })
|
||||||
|
);
|
||||||
|
res(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
_queue.push(interaction);
|
||||||
|
return interaction;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add an event listener for changes in storage
|
||||||
|
* @param {onStorageChangeCallback} callback - called whenever a change in
|
||||||
|
* storage is initiated from the current process
|
||||||
|
*/
|
||||||
|
export const addChangeListener = (callback) => {
|
||||||
|
_onChangeListeners.push(callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remove a listener added with storage.addChangeListener
|
||||||
|
* @param {onStorageChangeCallback} callback
|
||||||
|
*/
|
||||||
|
export const removeChangeListener = (callback) => {
|
||||||
|
_onChangeListeners = _onChangeListeners.filter((listener) => listener !== callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
305
extension/api/web.mjs
Normal file
305
extension/api/web.mjs
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
/*
|
||||||
|
* notion-enhancer: api
|
||||||
|
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* helpers for manipulation of a webpage
|
||||||
|
* @module notion-enhancer/api/env
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { localPath } from './fs.mjs';
|
||||||
|
import { md } from './fmt.mjs';
|
||||||
|
|
||||||
|
const _hotkeyEventListeners = [],
|
||||||
|
_documentObserverListeners = [],
|
||||||
|
_documentObserverEvents = [];
|
||||||
|
|
||||||
|
let _$featherStylesheet, _$tooltip, _$tooltipStylesheet, _hotkeyEvent, _documentObserver;
|
||||||
|
|
||||||
|
import '../dep/jscolor.min.js';
|
||||||
|
/** color picker with alpha channel using https://jscolor.com/ */
|
||||||
|
export const jscolor = JSColor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* wait until a page is loaded and ready for modification
|
||||||
|
* @param {array} [selectors=[]] - wait for the existence of elements that match these css selectors
|
||||||
|
* @returns {Promise} a promise that will resolve when the page is ready
|
||||||
|
*/
|
||||||
|
export const whenReady = (selectors = []) => {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
function onLoad() {
|
||||||
|
let isReadyInt;
|
||||||
|
isReadyInt = setInterval(isReadyTest, 100);
|
||||||
|
function isReadyTest() {
|
||||||
|
if (selectors.every((selector) => document.querySelector(selector))) {
|
||||||
|
clearInterval(isReadyInt);
|
||||||
|
res(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isReadyTest();
|
||||||
|
}
|
||||||
|
if (document.readyState !== 'complete') {
|
||||||
|
document.addEventListener('readystatechange', (event) => {
|
||||||
|
if (document.readyState === 'complete') onLoad();
|
||||||
|
});
|
||||||
|
} else onLoad();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* replace special html characters with escaped versions
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {string} escaped string
|
||||||
|
*/
|
||||||
|
export const escape = (str) =>
|
||||||
|
str
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/\\/g, '\');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a tagged template processor for raw html:
|
||||||
|
* stringifies, minifies, and syntax highlights
|
||||||
|
* @example web.raw`<p>hello</p>`
|
||||||
|
* @returns {string} the processed html
|
||||||
|
*/
|
||||||
|
export const raw = (str, ...templates) => {
|
||||||
|
const html = str
|
||||||
|
.map(
|
||||||
|
(chunk) =>
|
||||||
|
chunk +
|
||||||
|
(['string', 'number'].includes(typeof templates[0])
|
||||||
|
? templates.shift()
|
||||||
|
: escape(JSON.stringify(templates.shift(), null, 2) ?? ''))
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
return html.includes('<pre')
|
||||||
|
? html.trim()
|
||||||
|
: html
|
||||||
|
.split(/\n/)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.length)
|
||||||
|
.join(' ');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a single html element inc. attributes and children from a string
|
||||||
|
* @example web.html`<p>hello</p>`
|
||||||
|
* @returns {Element} the constructed html element
|
||||||
|
*/
|
||||||
|
export const html = (str, ...templates) => {
|
||||||
|
const $fragment = document.createRange().createContextualFragment(raw(str, ...templates));
|
||||||
|
return $fragment.children.length === 1 ? $fragment.children[0] : $fragment.children;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* appends a list of html elements to a parent
|
||||||
|
* @param $container - the parent element
|
||||||
|
* @param $elems - the elements to be appended
|
||||||
|
* @returns {Element} the updated $container
|
||||||
|
*/
|
||||||
|
export const render = ($container, ...$elems) => {
|
||||||
|
$elems = $elems
|
||||||
|
.map(($elem) => ($elem instanceof HTMLCollection ? [...$elem] : $elem))
|
||||||
|
.flat(Infinity)
|
||||||
|
.filter(($elem) => $elem);
|
||||||
|
$container.append(...$elems);
|
||||||
|
return $container;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* removes all children from an element without deleting them/their behaviours
|
||||||
|
* @param $container - the parent element
|
||||||
|
* @returns {Element} the updated $container
|
||||||
|
*/
|
||||||
|
export const empty = ($container) => {
|
||||||
|
while ($container.firstChild && $container.removeChild($container.firstChild));
|
||||||
|
return $container;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loads/applies a css stylesheet to the page
|
||||||
|
* @param {string} path - a url or within-the-enhancer filepath
|
||||||
|
*/
|
||||||
|
export const stylesheet = (path) => {
|
||||||
|
render(
|
||||||
|
document.head,
|
||||||
|
html`<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="${path.startsWith('https://') ? path : localPath(path)}"
|
||||||
|
/>`
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* generate an icon from the feather icons set
|
||||||
|
* @param {string} name - the name/id of the icon
|
||||||
|
* @param {object} attrs - an object of attributes to apply to the icon e.g. classes
|
||||||
|
* @returns {string} an svg string
|
||||||
|
*/
|
||||||
|
export const icon = (name, attrs = {}) => {
|
||||||
|
if (!_$featherStylesheet) {
|
||||||
|
_$featherStylesheet = html`<style data-enhancer-api-style="feather">
|
||||||
|
.enhancer-- {
|
||||||
|
width: 1.25em;
|
||||||
|
height: 1.25em;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
</style>`;
|
||||||
|
render(document.head, _$featherStylesheet);
|
||||||
|
}
|
||||||
|
attrs.class = ((attrs.class || '') + ' enhancer--feather').trim();
|
||||||
|
return `<svg ${Object.entries(attrs).map(
|
||||||
|
([key, val]) => `${escape(key)}="${escape(val)}"`
|
||||||
|
)}><use xlink:href="${localPath('dep/feather-sprite.svg')}#${name}" /></svg>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add a tooltip to show extra information on hover
|
||||||
|
* @param {HTMLElement} $ref - the element that will trigger the tooltip when hovered
|
||||||
|
* @param {string} text - the markdown content of the tooltip
|
||||||
|
*/
|
||||||
|
export const tooltip = ($ref, text) => {
|
||||||
|
if (!_$tooltip) {
|
||||||
|
_$tooltip = html`<div class="enhancer--tooltip"></div>`;
|
||||||
|
_$tooltipStylesheet = html`<style data-enhancer-api-style="tooltip">
|
||||||
|
.enhancer--tooltip {
|
||||||
|
position: absolute;
|
||||||
|
background: var(--theme--ui_tooltip);
|
||||||
|
font-size: 11.5px;
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
|
||||||
|
border-radius: 3px;
|
||||||
|
max-width: 20rem;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.enhancer--tooltip p {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
.enhancer--tooltip p:first-child {
|
||||||
|
color: var(--theme--ui_tooltip-title);
|
||||||
|
}
|
||||||
|
.enhancer--tooltip p:not(:first-child) {
|
||||||
|
color: var(--theme--ui_tooltip-description);
|
||||||
|
}
|
||||||
|
</style>`;
|
||||||
|
render(document.head, _$tooltip, _$tooltipStylesheet);
|
||||||
|
}
|
||||||
|
text = md.render(text);
|
||||||
|
$ref.addEventListener('mouseover', (event) => {
|
||||||
|
_$tooltip.innerHTML = text;
|
||||||
|
_$tooltip.style.display = 'block';
|
||||||
|
});
|
||||||
|
$ref.addEventListener('mousemove', (event) => {
|
||||||
|
_$tooltip.style.top = event.clientY - _$tooltip.clientHeight + 'px';
|
||||||
|
_$tooltip.style.left =
|
||||||
|
event.clientX < window.innerWidth / 2 ? event.clientX + 20 + 'px' : '';
|
||||||
|
});
|
||||||
|
$ref.addEventListener('mouseout', (event) => {
|
||||||
|
_$tooltip.style.display = '';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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/ and are case-insensitive.
|
||||||
|
* available modifiers are 'alt', 'ctrl', 'meta', and 'shift'.
|
||||||
|
* @param {function} callback - called whenever the keys are pressed
|
||||||
|
*/
|
||||||
|
export const addHotkeyListener = (keys, callback) => {
|
||||||
|
if (typeof keys === 'string') keys = keys.split('+');
|
||||||
|
if (!_hotkeyEvent) {
|
||||||
|
_hotkeyEvent = document.addEventListener('keyup', (event) => {
|
||||||
|
for (const hotkey of _hotkeyEventListeners) {
|
||||||
|
const matchesEvent = hotkey.keys.every((key) => {
|
||||||
|
const modifiers = {
|
||||||
|
altKey: 'alt',
|
||||||
|
ctrlKey: 'ctrl',
|
||||||
|
metaKey: 'meta',
|
||||||
|
shiftKey: 'shift',
|
||||||
|
};
|
||||||
|
for (const modifier in modifiers) {
|
||||||
|
if (key.toLowerCase() === modifiers[modifier] && event[modifier]) return true;
|
||||||
|
}
|
||||||
|
if (key.toLowerCase() === event.key.toLowerCase()) return true;
|
||||||
|
});
|
||||||
|
if (matchesEvent) hotkey.callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_hotkeyEventListeners.push({ keys, callback });
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* remove a listener added with web.addHotkeyListener
|
||||||
|
* @param {function} callback
|
||||||
|
*/
|
||||||
|
export const removeHotkeyListener = (callback) => {
|
||||||
|
_hotkeyEventListeners = _hotkeyEventListeners.filter(
|
||||||
|
(listener) => listener.callback !== callback
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add a listener to watch for changes to the dom
|
||||||
|
* @param {onDocumentObservedCallback} callback
|
||||||
|
* @param {array<string>} [selectors]
|
||||||
|
*/
|
||||||
|
export const addDocumentObserver = (callback, selectors = []) => {
|
||||||
|
if (!_documentObserver) {
|
||||||
|
const handle = (queue) => {
|
||||||
|
while (queue.length) {
|
||||||
|
const event = queue.shift();
|
||||||
|
for (const listener of _documentObserverListeners) {
|
||||||
|
if (
|
||||||
|
!listener.selectors.length ||
|
||||||
|
listener.selectors.some(
|
||||||
|
(selector) =>
|
||||||
|
event.target.matches(selector) || event.target.matches(`${selector} *`)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
listener.callback(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_documentObserver = new MutationObserver((list, observer) => {
|
||||||
|
if (!_documentObserverEvents.length)
|
||||||
|
requestIdleCallback(() => handle(_documentObserverEvents));
|
||||||
|
_documentObserverEvents.push(...list);
|
||||||
|
});
|
||||||
|
_documentObserver.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_documentObserverListeners.push({ callback, selectors });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remove a listener added with web.addDocumentObserver
|
||||||
|
* @param {onDocumentObservedCallback} callback
|
||||||
|
*/
|
||||||
|
export const removeDocumentObserver = (callback) => {
|
||||||
|
_documentObserverListeners = _documentObserverListeners.filter(
|
||||||
|
(listener) => listener.callback !== callback
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @callback onDocumentObservedCallback
|
||||||
|
* @param {MutationRecord} event - the observed dom mutation event
|
||||||
|
*/
|
@ -6,24 +6,26 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
import(chrome.runtime.getURL('api/_.mjs'));
|
||||||
|
|
||||||
// only load if user is logged into notion and viewing a page
|
// only load if user is logged into notion and viewing a page
|
||||||
if (
|
// if (
|
||||||
localStorage['LRU:KeyValueStore2:current-user-id'] &&
|
// localStorage['LRU:KeyValueStore2:current-user-id'] &&
|
||||||
location.pathname.split(/[/-]/g).reverse()[0].length === 32
|
// location.pathname.split(/[/-]/g).reverse()[0].length === 32
|
||||||
) {
|
// ) {
|
||||||
import(chrome.runtime.getURL('api.js')).then(async ({ web, registry }) => {
|
// import(chrome.runtime.getURL('api.js')).then(async ({ web, registry }) => {
|
||||||
for (const mod of await registry.get((mod) => registry.isEnabled(mod.id))) {
|
// for (const mod of await registry.get((mod) => registry.isEnabled(mod.id))) {
|
||||||
for (const sheet of mod.css?.client || []) {
|
// for (const sheet of mod.css?.client || []) {
|
||||||
web.loadStyleset(`repo/${mod._dir}/${sheet}`);
|
// web.loadStylesheet(`repo/${mod._dir}/${sheet}`);
|
||||||
}
|
// }
|
||||||
for (const 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}`));
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
const errors = await registry.errors();
|
// const errors = await registry.errors();
|
||||||
if (errors.length) {
|
// if (errors.length) {
|
||||||
console.log('notion-enhancer errors:');
|
// console.log('notion-enhancer errors:');
|
||||||
console.table(errors);
|
// console.table(errors);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "notion-enhancer",
|
"name": "notion-enhancer",
|
||||||
"version": "0.11.0-dev",
|
"version": "0.11.0",
|
||||||
"author": "dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)",
|
"author": "dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)",
|
||||||
"description": "an enhancer/customiser for the all-in-one productivity workspace notion.so",
|
"description": "an enhancer/customiser for the all-in-one productivity workspace notion.so",
|
||||||
"homepage_url": "https://notion-enhancer.github.io",
|
"homepage_url": "https://notion-enhancer.github.io",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icons/colour-x16.png",
|
"16": "icon/colour-x16.png",
|
||||||
"32": "icons/colour-x32.png",
|
"32": "icon/colour-x32.png",
|
||||||
"48": "icons/colour-x48.png",
|
"48": "icon/colour-x48.png",
|
||||||
"128": "icons/colour-x128.png",
|
"128": "icon/colour-x128.png",
|
||||||
"256": "icons/colour-x256.png",
|
"256": "icon/colour-x256.png",
|
||||||
"512": "icons/colour-x512.png"
|
"512": "icon/colour-x512.png"
|
||||||
},
|
},
|
||||||
"action": {},
|
"action": {},
|
||||||
"background": { "service_worker": "worker.js" },
|
"background": { "service_worker": "worker.js" },
|
||||||
"options_page": "repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/menu.html",
|
"options_page": "repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/menu.html",
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
{
|
||||||
"resources": ["api.js", "repo/*", "icons/*", "dep/*"],
|
"resources": ["api/*", "dep/*", "icon/*", "repo/*"],
|
||||||
"matches": ["https://*.notion.so/*"]
|
"matches": ["https://*.notion.so/*"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import { web } from '../../api.js';
|
import { web } from '../../api/_.mjs';
|
||||||
|
|
||||||
web.whenReady().then(async () => {
|
web.whenReady().then(async () => {
|
||||||
const openAsPage = document.querySelector(
|
const openAsPage = document.querySelector(
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import { web } from '../../api.js';
|
import { web } from '../../api/_.mjs';
|
||||||
|
|
||||||
const $button = web.createElement(
|
const $button = web.createElement(
|
||||||
web.html`<button id="calendar-scroll-to-week">Scroll</button>`
|
web.html`<button id="calendar-scroll-to-week">Scroll</button>`
|
||||||
|
@ -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 '../../api.js';
|
import { env, storage, web, fs, registry } from '../../api/_.mjs';
|
||||||
|
|
||||||
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)';
|
||||||
|
@ -7,11 +7,11 @@
|
|||||||
'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, regexers } from '../../api.js';
|
import { env, storage, web, fmt, fs, registry, regexers } from '../../api/_.mjs';
|
||||||
|
|
||||||
for (const mod of await registry.get((mod) => registry.isEnabled(mod.id))) {
|
for (const mod of await registry.get((mod) => registry.isEnabled(mod.id))) {
|
||||||
for (const sheet of mod.css?.menu || []) {
|
for (const sheet of mod.css?.menu || []) {
|
||||||
web.loadStyleset(`repo/${mod._dir}/${sheet}`);
|
web.loadStylesheet(`repo/${mod._dir}/${sheet}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function loadTheme() {
|
async function loadTheme() {
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import { web } from '../../api.js';
|
import { web } from '../../api/_.mjs';
|
||||||
|
|
||||||
export const getSearch = () =>
|
export const getSearch = () =>
|
||||||
new Map(
|
new Map(
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import { web } from '../../api.js';
|
import { web } from '../../api/_.mjs';
|
||||||
|
|
||||||
const $root = document.querySelector(':root');
|
const $root = document.querySelector(':root');
|
||||||
web.addDocumentObserver((mutation) => {
|
web.addDocumentObserver((mutation) => {
|
||||||
|
@ -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 '../../api.js';
|
import { env, storage, web } from '../../api/_.mjs';
|
||||||
|
|
||||||
web.whenReady().then(async () => {
|
web.whenReady().then(async () => {
|
||||||
const cssInsert = await storage.get(_id, '_file.insert.css');
|
const cssInsert = await storage.get(_id, '_file.insert.css');
|
||||||
|
Loading…
Reference in New Issue
Block a user