From 8159fc225636680132dfb14bbfa80f474f38200f Mon Sep 17 00:00:00 2001 From: dragonwocky Date: Sun, 3 Oct 2021 19:31:54 +1100 Subject: [PATCH] move api to submodule --- extension/.gitmodules | 3 + extension/api | 1 + extension/api/_.mjs | 25 --- extension/api/components/_.mjs | 36 ---- extension/api/components/feather.mjs | 36 ---- extension/api/components/panel.css | 237 ------------------------ extension/api/components/panel.mjs | 245 ------------------------- extension/api/components/tooltip.css | 25 --- extension/api/components/tooltip.mjs | 38 ---- extension/api/env.mjs | 46 ----- extension/api/fmt.mjs | 132 -------------- extension/api/fs.mjs | 48 ----- extension/api/registry-validation.mjs | 248 -------------------------- extension/api/registry.mjs | 164 ----------------- extension/api/storage.mjs | 67 ------- extension/api/web.mjs | 238 ------------------------ 16 files changed, 4 insertions(+), 1585 deletions(-) create mode 100644 extension/.gitmodules create mode 160000 extension/api delete mode 100644 extension/api/_.mjs delete mode 100644 extension/api/components/_.mjs delete mode 100644 extension/api/components/feather.mjs delete mode 100644 extension/api/components/panel.css delete mode 100644 extension/api/components/panel.mjs delete mode 100644 extension/api/components/tooltip.css delete mode 100644 extension/api/components/tooltip.mjs delete mode 100644 extension/api/env.mjs delete mode 100644 extension/api/fmt.mjs delete mode 100644 extension/api/fs.mjs delete mode 100644 extension/api/registry-validation.mjs delete mode 100644 extension/api/registry.mjs delete mode 100644 extension/api/storage.mjs delete mode 100644 extension/api/web.mjs diff --git a/extension/.gitmodules b/extension/.gitmodules new file mode 100644 index 0000000..65dd035 --- /dev/null +++ b/extension/.gitmodules @@ -0,0 +1,3 @@ +[submodule "api"] + path = api + url = git@github.com:notion-enhancer/api.git diff --git a/extension/api b/extension/api new file mode 160000 index 0000000..5030fe2 --- /dev/null +++ b/extension/api @@ -0,0 +1 @@ +Subproject commit 5030fe2b0fd71b397796b055934ec50f7e909a5c diff --git a/extension/api/_.mjs b/extension/api/_.mjs deleted file mode 100644 index 334d08c..0000000 --- a/extension/api/_.mjs +++ /dev/null @@ -1,25 +0,0 @@ -/* - * notion-enhancer: api - * (c) 2021 dragonwocky (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'; -/** environment-specific filesystem reading */ -export * as fs from './fs.mjs'; -/** environment-specific data persistence */ -export * as storage from './storage.mjs'; - -/** helpers for formatting, validating and parsing values */ -export * as fmt from './fmt.mjs'; -/** interactions with the enhancer's repository of mods */ -export * as registry from './registry.mjs'; -/** helpers for manipulation of a webpage */ -export * as web from './web.mjs'; -/** shared notion-style elements */ -export * as components from './components/_.mjs'; diff --git a/extension/api/components/_.mjs b/extension/api/components/_.mjs deleted file mode 100644 index e1d98e6..0000000 --- a/extension/api/components/_.mjs +++ /dev/null @@ -1,36 +0,0 @@ -/* - * notion-enhancer: api - * (c) 2021 dragonwocky (https://dragonwocky.me/) - * (https://notion-enhancer.github.io/) under the MIT license - */ - -'use strict'; - -/** - * shared notion-style elements - * @module notion-enhancer/api/components - */ - -/** - * 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 { tooltip } from './tooltip.mjs'; - -/** - * 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 { feather } from './feather.mjs'; - -/** - * adds a view to the enhancer's side panel - * @param {string} param0.id - a uuid, used to restore it on reload if it was last open - * @param {string} param0.icon - an svg string - * @param {string} param0.title - the name of the view - * @param {Element} param0.$content - an element containing the content of the view - */ -export { addPanelView } from './panel.mjs'; diff --git a/extension/api/components/feather.mjs b/extension/api/components/feather.mjs deleted file mode 100644 index 3734bda..0000000 --- a/extension/api/components/feather.mjs +++ /dev/null @@ -1,36 +0,0 @@ -/* - * notion-enhancer: api - * (c) 2021 dragonwocky (https://dragonwocky.me/) - * (https://notion-enhancer.github.io/) under the MIT license - */ - -'use strict'; - -/** - * shared notion-style elements - * @module notion-enhancer/api/components/feather - */ - -import { fs, web } from '../_.mjs'; - -let _$iconSheet; - -/** - * 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 feather = async (name, attrs = {}) => { - if (!_$iconSheet) { - _$iconSheet = web.html`${await fs.getText('dep/feather-sprite.svg')}`; - } - attrs.style = ( - (attrs.style ? attrs.style + ';' : '') + - 'stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;fill:none;' - ).trim(); - attrs.viewBox = '0 0 24 24'; - return ` `${web.escape(key)}="${web.escape(val)}"`) - .join(' ')}>${_$iconSheet.getElementById(name)?.innerHTML}`; -}; diff --git a/extension/api/components/panel.css b/extension/api/components/panel.css deleted file mode 100644 index 1294867..0000000 --- a/extension/api/components/panel.css +++ /dev/null @@ -1,237 +0,0 @@ -/* - * notion-enhancer core: components - * (c) 2021 dragonwocky (https://dragonwocky.me/) - * (c) 2021 CloudHill (https://github.com/CloudHill) - * (https://notion-enhancer.github.io/) under the MIT license - */ - -:root { - --component--panel-width: 260px; -} - -#enhancer--panel-hover-trigger { - height: 100vh; - width: 2.5rem; - max-height: 100%; - z-index: 999; - position: absolute; - top: 0; - right: 0; - flex-grow: 0; - flex-shrink: 0; - transition: width 300ms ease-in-out; -} -#enhancer--panel-hover-trigger[data-enhancer-panel-pinned] { - /* taking up the physical space of the panel to move topbar buttons */ - position: relative; - width: var(--component--panel-width); -} - -.notion-cursor-listener > div[style*='flex-end'] { - transition: margin-right 300ms ease-in-out; -} -.notion-cursor-listener > div[style*='flex-end'][data-enhancer-panel-pinned] { - margin-right: var(--component--panel-width); -} -.notion-frame { - transition: padding-right 300ms ease-in-out; -} -.notion-frame[data-enhancer-panel-pinned] { - padding-right: var(--component--panel-width); -} - -#enhancer--panel { - z-index: 999; - position: absolute; - background: var(--theme--bg_secondary); - width: var(--component--panel-width); - right: calc(-1 * var(--component--panel-width)); - opacity: 0; - height: 100vh; - flex-grow: 0; - flex-shrink: 0; - display: flex; - flex-direction: column; - transition: 300ms ease-in; - - margin-top: 5rem; - max-height: calc(100vh - 10rem); -} -#enhancer--panel-hover-trigger:hover + #enhancer--panel:not([data-enhancer-panel-pinned]), -#enhancer--panel:not([data-enhancer-panel-pinned]):hover { - opacity: 1; - transform: translateX(calc(-1 * var(--component--panel-width))); - box-shadow: var(--theme--ui_shadow, rgba(15, 15, 15, 0.05)) 0px 0px 0px 1px, - var(--theme--ui_shadow, rgba(15, 15, 15, 0.1)) 0px 3px 6px, - var(--theme--ui_shadow, rgba(15, 15, 15, 0.2)) 0px 9px 24px !important; -} -#enhancer--panel[data-enhancer-panel-pinned] { - opacity: 1; - max-height: 100%; - margin-top: 0; - transform: translateX(calc(-1 * var(--component--panel-width))); -} - -.enhancer--panel-view-title { - margin: 0; - height: 1em; - display: flex; - align-items: center; - font-size: 1.1rem; - font-weight: 600; -} -.enhancer--panel-view-title svg, -.enhancer--panel-view-title img { - height: 1em; - width: 1em; -} -.enhancer--panel-view-title .enhancer--panel-view-title-text { - font-size: 0.9em; - margin: 0 0 0 0.75em; - padding-bottom: 0.3em; - white-space: nowrap; - overflow: hidden; -} - -#enhancer--panel-header { - font-size: 1.2rem; - font-weight: 600; - display: flex; - align-items: center; - cursor: pointer; - user-select: none; - padding: 0.75rem 0 0.75rem 1rem; - --scoped--bg: var(--theme--bg_secondary); - background: var(--scoped--bg); -} -#enhancer--panel-header-title { - max-width: calc(100% - 4.25rem); -} -#enhancer--panel-header-title .enhancer--panel-view-title { - font-size: 1.2rem; -} -#enhancer--panel-header-title .enhancer--panel-view-title-text { - max-width: calc(100% - 1.75em); - position: relative; -} -.enhancer--panel-view-title-fade-edge { - display: inline-block; - width: 0.75rem; - height: 1em; -} - -#enhancer--panel-switcher-overlay-container { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 999; - overflow: hidden; -} -#enhancer--panel-switcher { - max-width: 320px; - position: relative; - right: 14px; - border-radius: 3px; - padding: 8px 0; - background: var(--theme--bg_popup); - box-shadow: var(--theme--ui_shadow, rgba(15, 15, 15, 0.05)) 0px 0px 0px 1px, - var(--theme--ui_shadow, rgba(15, 15, 15, 0.1)) 0px 3px 6px, - var(--theme--ui_shadow, rgba(15, 15, 15, 0.2)) 0px 9px 24px !important; - overflow: hidden; -} -.enhancer--panel-switcher-item { - display: flex; - align-items: center; - width: 100%; - padding: 8px 14px; - user-select: none; - cursor: pointer; - overflow: hidden; - position: relative; - --scoped--bg: var(--theme--bg_popup); - background: var(--scoped--bg); -} -#enhancer--panel-header:hover, -#enhancer--panel-header:focus-within, -.enhancer--panel-switcher-item:hover, -.enhancer--panel-switcher-item:focus { - --scoped--bg: var(--theme--ui_interactive-hover); -} -#enhancer--panel-header:active, -.enhancer--panel-switcher-item:active { - background: var(--theme--ui_interactive-active); -} -.enhancer--panel-view-title-fade-edge:after { - content: ''; - height: 100%; - position: absolute; - right: 0; - top: 0; - width: 0.75rem; - background: linear-gradient( - to right, - transparent 0%, - var(--scoped--bg) 50%, - var(--scoped--bg) 100% - ); -} - -#enhancer--panel-content { - margin: 0.75rem 1rem; - font-size: 1rem; -} -#enhancer--panel-header-switcher { - padding: 4px; -} -#enhancer--panel-header-toggle { - margin-left: auto; - padding-right: 1rem; - height: 100%; - width: 2.5em; - opacity: 0; - display: flex; -} -#enhancer--panel-header-toggle > div { - margin: auto 0 auto auto; -} -#enhancer--panel-header-switcher, -#enhancer--panel-header-toggle > div { - color: var(--theme--icon_secondary); - height: 1em; - width: 1em; - cursor: pointer; - display: flex; - flex-direction: column; - transition: 300ms ease-in-out; -} -#enhancer--panel #enhancer--panel-header-toggle svg { - transition: 300ms ease-in-out; -} -#enhancer--panel:not([data-enhancer-panel-pinned]) #enhancer--panel-header-toggle svg { - transform: rotateZ(-180deg); -} -#enhancer--panel:hover #enhancer--panel-header-toggle { - opacity: 1; -} - -#enhancer--panel-resize { - position: absolute; - left: -5px; - height: 100%; - width: 10px; -} -#enhancer--panel[data-enhancer-panel-pinned] #enhancer--panel-resize { - cursor: col-resize; -} -#enhancer--panel-resize div { - transition: background 150ms ease-in-out; - background: transparent; - width: 2px; - margin-left: 4px; - height: 100%; -} -#enhancer--panel[data-enhancer-panel-pinned] #enhancer--panel-resize:hover div { - background: var(--theme--ui_divider); -} diff --git a/extension/api/components/panel.mjs b/extension/api/components/panel.mjs deleted file mode 100644 index f4c743b..0000000 --- a/extension/api/components/panel.mjs +++ /dev/null @@ -1,245 +0,0 @@ -/* - * notion-enhancer: api - * (c) 2021 dragonwocky (https://dragonwocky.me/) - * (https://notion-enhancer.github.io/) under the MIT license - */ - -'use strict'; - -/** - * shared notion-style elements - * @module notion-enhancer/api/components/side-panel - */ - -import { web, components, registry } from '../_.mjs'; -const db = await registry.db('36a2ffc9-27ff-480e-84a7-c7700a7d232d'); - -const _views = [], - svgExpand = web.raw` - -`; - -// open + close -let $notionFrame, - $notionRightSidebar, - // resize - dragStartX, - dragStartWidth, - dragEventsFired, - panelWidth, - // render content - $notionApp; - -// open + close -const $panel = web.html`
`, - $pinnedToggle = web.html`
- ${await components.feather('chevrons-right')} -
`, - $hoverTrigger = web.html`
`, - panelPinnedAttr = 'data-enhancer-panel-pinned', - isPinned = () => $panel.hasAttribute(panelPinnedAttr), - togglePanel = () => { - const $elems = [$notionRightSidebar, $notionFrame, $hoverTrigger, $panel]; - if (isPinned()) { - closeSwitcher(); - for (const $elem of $elems) $elem.removeAttribute(panelPinnedAttr); - } else { - for (const $elem of $elems) $elem.setAttribute(panelPinnedAttr, 'true'); - } - db.set(['panel.pinned'], isPinned()); - }, - // resize - $resizeHandle = web.html`
`, - updateWidth = async () => { - document.documentElement.style.setProperty('--component--panel-width', panelWidth + 'px'); - db.set(['panel.width'], panelWidth); - }, - resizeDrag = (event) => { - event.preventDefault(); - dragEventsFired = true; - panelWidth = dragStartWidth + (dragStartX - event.clientX); - if (panelWidth < 190) panelWidth = 190; - if (panelWidth > 480) panelWidth = 480; - $panel.style.width = panelWidth + 'px'; - $hoverTrigger.style.width = panelWidth + 'px'; - $notionFrame.style.paddingRight = panelWidth + 'px'; - $notionRightSidebar.style.right = panelWidth + 'px'; - }, - resizeEnd = (event) => { - $panel.style.width = ''; - $hoverTrigger.style.width = ''; - $notionFrame.style.paddingRight = ''; - $notionRightSidebar.style.right = ''; - updateWidth(); - $resizeHandle.style.cursor = ''; - document.body.removeEventListener('mousemove', resizeDrag); - document.body.removeEventListener('mouseup', resizeEnd); - }, - resizeStart = (event) => { - dragStartX = event.clientX; - dragStartWidth = panelWidth; - $resizeHandle.style.cursor = 'auto'; - document.body.addEventListener('mousemove', resizeDrag); - document.body.addEventListener('mouseup', resizeEnd); - }, - // render content - $panelTitle = web.html`
`, - $header = web.render(web.html`
`, $panelTitle), - $panelContent = web.html`
`, - $switcher = web.html`
`, - $switcherTrigger = web.html`
- ${svgExpand} -
`, - $switcherOverlayContainer = web.html`
`, - isSwitcherOpen = () => document.body.contains($switcher), - openSwitcher = () => { - if (!isPinned()) return togglePanel(); - web.render($notionApp, $switcherOverlayContainer); - web.empty($switcher); - for (const view of _views) { - const open = $panelTitle.contains(view.$title), - $item = web.render( - web.html`
`, - web.render( - web.html``, - view.$icon.cloneNode(true), - view.$title.cloneNode(true) - ) - ); - $item.addEventListener('click', () => { - renderView(view); - db.set(['panel.open'], view.id); - }); - web.render($switcher, $item); - } - const rect = $header.getBoundingClientRect(); - web.render( - web.empty($switcherOverlayContainer), - web.render( - web.html`
`, - web.render( - web.html`
`, - $switcher - ) - ) - ); - $switcher.querySelector('[data-open]').focus(); - $switcher.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 200 }); - document.addEventListener('keydown', switcherKeyListeners); - }, - closeSwitcher = () => { - document.removeEventListener('keydown', switcherKeyListeners); - $switcher.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 200 }).onfinish = () => - $switcherOverlayContainer.remove(); - }, - switcherKeyListeners = (event) => { - if (isSwitcherOpen()) { - switch (event.key) { - case 'Escape': - closeSwitcher(); - event.stopPropagation(); - break; - case 'Enter': - document.activeElement.click(); - event.stopPropagation(); - break; - case 'ArrowUp': - const $prev = event.target.previousElementSibling; - ($prev || event.target.parentElement.lastElementChild).focus(); - event.stopPropagation(); - break; - case 'ArrowDown': - const $next = event.target.nextElementSibling; - ($next || event.target.parentElement.firstElementChild).focus(); - event.stopPropagation(); - break; - } - } - }, - renderView = (view) => { - web.render( - web.empty($panelTitle), - web.render( - web.html``, - view.$icon, - view.$title - ) - ); - web.render(web.empty($panelContent), view.$content); - }; - -async function createPanel() { - const notionRightSidebarSelector = '.notion-cursor-listener > div[style*="flex-end"]'; - await web.whenReady([notionRightSidebarSelector]); - $notionFrame = document.querySelector('.notion-frame'); - $notionRightSidebar = document.querySelector(notionRightSidebarSelector); - if (await db.get(['panel.pinned'])) togglePanel(); - web.addHotkeyListener(await db.get(['panel.hotkey']), togglePanel); - $pinnedToggle.addEventListener('click', (event) => { - event.stopPropagation(); - togglePanel(); - }); - web.render( - $panel, - web.render($header, $panelTitle, $switcherTrigger, $pinnedToggle), - $panelContent, - $resizeHandle - ); - - await enablePanelResize(); - await createViews(); - - $notionRightSidebar.after($hoverTrigger, $panel); -} - -async function enablePanelResize() { - panelWidth = await db.get(['panel.width'], 240); - updateWidth(); - $resizeHandle.addEventListener('mousedown', resizeStart); - $resizeHandle.addEventListener('click', () => { - if (dragEventsFired) { - dragEventsFired = false; - } else togglePanel(); - }); -} - -async function createViews() { - $notionApp = document.querySelector('.notion-app-inner'); - $header.addEventListener('click', openSwitcher); - $switcherTrigger.addEventListener('click', openSwitcher); - $switcherOverlayContainer.addEventListener('click', closeSwitcher); -} - -web.loadStylesheet('api/components/panel.css'); - -/** - * adds a view to the enhancer's side panel - * @param {string} param0.id - a uuid, used to restore the last open view on reload - * @param {string} param0.icon - an svg string - * @param {string} param0.title - the name of the view - * @param {Element} param0.$content - an element containing the content of the view - */ -export const addPanelView = async ({ id, icon, title, $content }) => { - const view = { - id, - $icon: web.html` - ${icon} - `, - $title: web.html` - ${web.escape(title)} - - `, - $content, - }; - _views.push(view); - if (_views.length === 1) await createPanel(); - if (_views.length === 1 || (await db.get(['panel.open'])) === id) renderView(view); -}; diff --git a/extension/api/components/tooltip.css b/extension/api/components/tooltip.css deleted file mode 100644 index b919963..0000000 --- a/extension/api/components/tooltip.css +++ /dev/null @@ -1,25 +0,0 @@ -/* - * notion-enhancer core: components - * (c) 2021 dragonwocky (https://dragonwocky.me/) - * (https://notion-enhancer.github.io/) under the MIT license - */ - -#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); -} diff --git a/extension/api/components/tooltip.mjs b/extension/api/components/tooltip.mjs deleted file mode 100644 index e52d3f0..0000000 --- a/extension/api/components/tooltip.mjs +++ /dev/null @@ -1,38 +0,0 @@ -/* - * notion-enhancer: api - * (c) 2021 dragonwocky (https://dragonwocky.me/) - * (https://notion-enhancer.github.io/) under the MIT license - */ - -'use strict'; - -/** - * shared notion-style elements - * @module notion-enhancer/api/components/tooltip - */ - -import { fmt, web } from '../_.mjs'; - -const _$tooltip = web.html`
`; -web.loadStylesheet('api/components/tooltip.css'); - -/** - * 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) => { - web.render(document.body, _$tooltip); - text = fmt.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 - _$tooltip.clientWidth + 'px'; - }); - $ref.addEventListener('mouseout', (event) => { - _$tooltip.style.display = ''; - }); -}; diff --git a/extension/api/env.mjs b/extension/api/env.mjs deleted file mode 100644 index b2bccdc..0000000 --- a/extension/api/env.mjs +++ /dev/null @@ -1,46 +0,0 @@ -/* - * notion-enhancer: api - * (c) 2021 dragonwocky (https://dragonwocky.me/) - * (https://notion-enhancer.github.io/) under the MIT license - */ - -'use strict'; - -/** - * environment-specific methods and constants - * @module notion-enhancer/api/env - */ - -import * as env from '../env/env.mjs'; - -/** - * the environment/platform name code is currently being executed in - * @constant - * @type {string} - */ -export const name = env.name; - -/** - * the current version of the enhancer - * @constant - * @type {string} - */ -export const version = env.version; - -/** - * open the enhancer's menu - * @type {function} - */ -export const focusMenu = env.focusMenu; - -/** - * focus an active notion tab - * @type {function} - */ -export const focusNotion = env.focusNotion; - -/** - * reload all notion and enhancer menu tabs to apply changes - * @type {function} - */ -export const reload = env.reload; diff --git a/extension/api/fmt.mjs b/extension/api/fmt.mjs deleted file mode 100644 index f905c01..0000000 --- a/extension/api/fmt.mjs +++ /dev/null @@ -1,132 +0,0 @@ -/* - * notion-enhancer: api - * (c) 2021 dragonwocky (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 { web, fs, components } from './_.mjs'; - -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.html`${await components.feather('clipboard')}`); -}); - -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`
${web.escape(
-      str
-    )}
`, -}); -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`${web.escape( - tokens[idx].content - )}\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 = 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} [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; -}; - -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} 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; -}; diff --git a/extension/api/fs.mjs b/extension/api/fs.mjs deleted file mode 100644 index ef05d90..0000000 --- a/extension/api/fs.mjs +++ /dev/null @@ -1,48 +0,0 @@ -/* - * notion-enhancer: api - * (c) 2021 dragonwocky (https://dragonwocky.me/) - * (https://notion-enhancer.github.io/) under the MIT license - */ - -'use strict'; - -/** - * environment-specific filesystem reading - * @module notion-enhancer/api/fs - */ - -import * as fs from '../env/fs.mjs'; - -/** - * transform a path relative to the enhancer root directory into an absolute path - * @type {function} - * @param {string} path - a url or within-the-enhancer filepath - * @returns {string} an absolute filepath - */ -export const localPath = fs.localPath; - -/** - * fetch and parse a json file's contents - * @type {function} - * @param {string} path - a url or within-the-enhancer filepath - * @param {object} [opts] - the second argument of a fetch() request - * @returns {object} the json value of the requested file as a js object - */ -export const getJSON = fs.getJSON; - -/** - * fetch a text file's contents - * @type {function} - * @param {string} path - a url or within-the-enhancer filepath - * @param {object} [opts] - the second argument of a fetch() request - * @returns {string} the text content of the requested file - */ -export const getText = fs.getText; - -/** - * check if a file exists - * @type {function} - * @param {string} path - a url or within-the-enhancer filepath - * @returns {boolean} whether or not the file exists - */ -export const isFile = fs.isFile; diff --git a/extension/api/registry-validation.mjs b/extension/api/registry-validation.mjs deleted file mode 100644 index afe7819..0000000 --- a/extension/api/registry-validation.mjs +++ /dev/null @@ -1,248 +0,0 @@ -/* - * notion-enhancer: api - * (c) 2021 dragonwocky (https://dragonwocky.me/) - * (https://notion-enhancer.github.io/) under the MIT license - */ - -'use strict'; - -import { fmt, registry } from './_.mjs'; - -const check = async ( - mod, - 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; - }, - validateEnvironments = (mod) => { - return check(mod, '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(mod, 'environments.env', tag, registry.supportedEnvs) - ); - } - ); - }, - validateTags = (mod) => { - return check(mod, '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(mod, 'tags.tag', tag, 'string')); - }); - }, - validateAuthors = (mod) => { - return check(mod, 'authors', mod.authors, 'array').then((passed) => { - if (!passed) return false; - return mod.authors.map((author) => [ - check(mod, 'authors.author.name', author.name, 'string'), - check(mod, 'authors.author.email', author.email, 'email'), - check(mod, 'authors.author.homepage', author.homepage, 'url'), - check(mod, 'authors.author.avatar', author.avatar, 'url'), - ]); - }); - }, - validateCSS = (mod) => { - return check(mod, '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(mod, `css.${dest}`, mod.css[dest], 'array'); - test = test.then((passed) => { - if (!passed) return false; - return mod.css[dest].map((file) => - check(mod, `css.${dest}.file`, file, 'file', { extension: '.css' }) - ); - }); - tests.push(test); - } - return tests; - }); - }, - validateJS = (mod) => { - return check(mod, 'js', mod.js, 'object').then((passed) => { - if (!passed) return false; - const tests = []; - if (mod.js.client) { - let test = check(mod, 'js.client', mod.js.client, 'array'); - test = test.then((passed) => { - if (!passed) return false; - return mod.js.client.map((file) => - check(mod, 'js.client.file', file, 'file', { extension: '.mjs' }) - ); - }); - tests.push(test); - } - if (mod.js.electron) { - let test = check(mod, 'js.electron', mod.js.electron, 'array'); - test = test.then((passed) => { - if (!passed) return false; - return mod.js.electron.map((file) => - check(mod, 'js.electron.file', file, 'object').then((passed) => { - if (!passed) return false; - return [ - check(mod, '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(mod, 'js.electron.file.target', file.target, 'string', { - extension: '.js', - }), - ]; - }) - ); - }); - tests.push(test); - } - return tests; - }); - }, - validateOptions = (mod) => { - return check(mod, 'options', mod.options, 'array').then((passed) => { - if (!passed) return false; - return mod.options.map((option) => - check(mod, 'options.option.type', option.type, registry.optionTypes).then((passed) => { - if (!passed) return false; - const tests = [ - check(mod, 'options.option.key', option.key, 'alphanumeric'), - check(mod, 'options.option.label', option.label, 'string'), - check(mod, 'options.option.tooltip', option.tooltip, 'string', { - optional: true, - }), - check(mod, '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( - mod, - 'options.option.environments.env', - environment, - registry.supportedEnvs - ) - ); - }), - ]; - switch (option.type) { - case 'toggle': - tests.push(check(mod, 'options.option.value', option.value, 'boolean')); - break; - case 'select': - tests.push( - check(mod, 'options.option.values', option.values, 'array').then((passed) => { - if (!passed) return false; - return option.values.map((value) => - check(mod, 'options.option.values.value', value, 'string') - ); - }) - ); - break; - case 'text': - case 'hotkey': - tests.push(check(mod, 'options.option.value', option.value, 'string')); - break; - case 'number': - case 'color': - tests.push(check(mod, 'options.option.value', option.value, option.type)); - break; - case 'file': - tests.push( - check(mod, 'options.option.extensions', option.extensions, 'array').then( - (passed) => { - if (!passed) return false; - return option.extensions.map((value) => - check(mod, 'options.option.extensions.extension', value, 'string') - ); - } - ) - ); - } - return tests; - }) - ); - }); - }; - -/** - * 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) { - let conditions = [ - check(mod, 'name', mod.name, 'string'), - check(mod, 'id', mod.id, 'uuid'), - check(mod, 'version', mod.version, 'semver'), - validateEnvironments(mod), - check(mod, 'description', mod.description, 'string'), - check(mod, 'preview', mod.preview, 'file|url', { optional: true }), - validateTags(mod), - validateAuthors(mod), - validateCSS(mod), - validateJS(mod), - validateOptions(mod), - ]; - do { - conditions = await Promise.all(conditions.flat(Infinity)); - } while (conditions.some((condition) => Array.isArray(condition))); - return conditions.every((passed) => passed); -} diff --git a/extension/api/registry.mjs b/extension/api/registry.mjs deleted file mode 100644 index 54ee095..0000000 --- a/extension/api/registry.mjs +++ /dev/null @@ -1,164 +0,0 @@ -/* - * notion-enhancer: api - * (c) 2021 dragonwocky (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 { env, fs, storage } from './_.mjs'; -import { validate } from './registry-validation.mjs'; - -export const _cache = [], - _errors = []; - -/** - * mod ids whitelisted as part of the enhancer's core, permanently enabled - * @constant - * @type {array} - */ -export const core = [ - 'a6621988-551d-495a-97d8-3c568bca2e9e', - '0f0bf8b6-eae6-4273-b307-8fc43f2ee082', - '36a2ffc9-27ff-480e-84a7-c7700a7d232d', -]; - -/** - * all environments/platforms currently supported by the enhancer - * @constant - * @type {array} - */ -export const supportedEnvs = ['linux', 'win32', 'darwin', 'extension']; - -/** - * all available configuration types - * @constant - * @type {array} - */ -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 - * @param {function} filter - a function to filter out mods - * @returns {array} a validated list of mod.json objects - */ -export const list = async (filter = (mod) => true) => { - if (!_cache.length) { - for (const dir of await fs.getJSON('repo/registry.json')) { - try { - const mod = await fs.getJSON(`repo/${dir}/mod.json`); - mod._dir = dir; - if (await validate(mod)) _cache.push(mod); - } catch (e) { - console.log(e); - _errors.push({ source: dir, message: 'invalid mod.json' }); - } - } - } - const list = []; - for (const mod of _cache) if (await filter(mod)) list.push(mod); - return list; -}; - -/** - * list validation errors encountered when loading the repo - * @returns {array} error objects with an error message and a source directory - */ -export const errors = async () => { - if (!_errors.length) await list(); - return _errors; -}; - -/** - * get a single mod from the repo - * @param {string} id - the uuid of the mod - * @returns {object} the mod's mod.json - */ -export const get = async (id) => { - if (!_cache.length) await list(); - return _cache.find((mod) => mod.id === id); -}; - -/** - * 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 - */ -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); -}; - -/** - * get a default value of a mod's option according to its mod.json - * @param {string} id - the uuid of the mod - * @param {string} key - the key of the option - * @returns {string|number|boolean|undefined} the option's default value - */ -export const optionDefault = async (id, key) => { - const mod = await get(id), - opt = mod.options.find((opt) => opt.key === key); - if (!opt) return undefined; - switch (opt.type) { - case 'toggle': - case 'text': - case 'number': - case 'color': - case 'hotkey': - return opt.value; - case 'select': - return opt.values[0]; - case 'file': - return undefined; - } -}; - -/** - * access the storage partition of a mod in the current profile - * @param {string} id - the uuid of the mod - * @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) => { - if (path.length === 2) { - // profiles -> profile -> mod -> option - fallback = (await optionDefault(id, path[1])) ?? fallback; - } - return db.get(path, fallback); - }, - db.set - ); -}; diff --git a/extension/api/storage.mjs b/extension/api/storage.mjs deleted file mode 100644 index 4a3e63b..0000000 --- a/extension/api/storage.mjs +++ /dev/null @@ -1,67 +0,0 @@ -/* - * notion-enhancer: api - * (c) 2021 dragonwocky (https://dragonwocky.me/) - * (https://notion-enhancer.github.io/) under the MIT license - */ - -'use strict'; - -/** - * environment-specific data persistence - * @module notion-enhancer/api/storage - */ - -import * as storage from '../env/storage.mjs'; - -/** - * get persisted data - * @type {function} - * @param {array} 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 = storage.get; - -/** - * persist data - * @type {function} - * @param {array} path - the path of keys to the value being set - * @param {*} value - the data to save - * @returns {Promise} resolves when data has been saved - */ -export const set = storage.set; - -/** - * create a wrapper for accessing a partition of the storage - * @type {function} - * @param {array} namespace - the path of keys to prefix all storage requests with - * @param {function} [get] - the storage get function to be wrapped - * @param {function} [set] - the storage set function to be wrapped - * @returns {object} an object with the wrapped get/set functions - */ -export const db = storage.db; - -/** - * add an event listener for changes in storage - * @type {function} - * @param {onStorageChangeCallback} callback - called whenever a change in - * storage is initiated from the current process - */ -export const addChangeListener = storage.addChangeListener; - -/** - * remove a listener added with storage.addChangeListener - * @type {function} - * @param {onStorageChangeCallback} callback - */ -export const removeChangeListener = storage.removeChangeListener; - -/** - * @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 - */ diff --git a/extension/api/web.mjs b/extension/api/web.mjs deleted file mode 100644 index d7ae5d1..0000000 --- a/extension/api/web.mjs +++ /dev/null @@ -1,238 +0,0 @@ -/* - * notion-enhancer: api - * (c) 2021 dragonwocky (https://dragonwocky.me/) - * (https://notion-enhancer.github.io/) under the MIT license - */ - -'use strict'; - -/** - * helpers for manipulation of a webpage - * @module notion-enhancer/api/web - */ - -import { fs } from './_.mjs'; - -let _hotkeyEventListeners = [], - _documentObserver, - _documentObserverListeners = [], - _documentObserverEvents = []; - -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(); - }); -}; - -/** - * parse the current location search params into a usable form - * @returns {map} a map of the url search params - */ -export const queryParams = () => new URLSearchParams(window.location.search); - -/** - * 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, '\'); - -/** - * a tagged template processor for raw html: - * stringifies, minifies, and syntax highlights - * @example web.raw`

hello

` - * @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(' line.trim()) - .filter((line) => line.length) - .join(' '); -}; - -/** - * create a single html element inc. attributes and children from a string - * @example web.html`

hello

` - * @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 loadStylesheet = (path) => { - render( - document.head, - html`` - ); - return true; -}; - -document.addEventListener('keyup', (event) => { - if (document.activeElement.nodeName === 'INPUT') return; - for (const hotkey of _hotkeyEventListeners) { - const pressed = hotkey.keys.every((key) => { - key = key.toLowerCase(); - const modifiers = { - metaKey: ['meta', 'os', 'win', 'cmd', 'command'], - ctrlKey: ['ctrl', 'control'], - shiftKey: ['shift'], - altKey: ['alt'], - }; - for (const modifier in modifiers) { - const pressed = modifiers[modifier].includes(key) && event[modifier]; - if (pressed) return true; - } - if (key === event.key.toLowerCase()) return true; - }); - if (pressed) hotkey.callback(event); - } -}); - -/** - * 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('+'); - _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} [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 - */