diff --git a/extension/icons/blackwhite.svg b/extension/icons/blackwhite.svg
new file mode 100644
index 0000000..12da3b0
--- /dev/null
+++ b/extension/icons/blackwhite.svg
@@ -0,0 +1,30 @@
+
diff --git a/extension/icons/colour.svg b/extension/icons/colour.svg
new file mode 100644
index 0000000..958c24b
--- /dev/null
+++ b/extension/icons/colour.svg
@@ -0,0 +1,64 @@
+
diff --git a/extension/manifest.json b/extension/manifest.json
index 131c7b2..7740325 100644
--- a/extension/manifest.json
+++ b/extension/manifest.json
@@ -11,15 +11,13 @@
"128": "icons/colour-x128.png"
},
"manifest_version": 3,
+ "action": {},
"background": {
"service_worker": "worker.js"
},
- "action": {
- "default_popup": "src/gui.html"
- },
"web_accessible_resources": [
{
- "resources": ["registry.json", "src/*", "repo/*"],
+ "resources": ["icons/*", "src/*", "repo/*"],
"matches": ["https://*.notion.so/*"]
}
],
@@ -29,6 +27,5 @@
"js": ["content-loader.js"]
}
],
- "permissions": ["activeTab"],
"host_permissions": ["https://*.notion.so/*"]
}
diff --git a/extension/registry.json b/extension/registry.json
deleted file mode 100644
index 27c16db..0000000
--- a/extension/registry.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"name":"theming","id":"0f0bf8b6-eae6-4273-b307-8fc43f2ee082","description":"the default theme variables, required by other themes & extensions.","version":"0.11.0","tags":["core","theme"],"authors":[{"name":"dragonwocky","email":"thedragonring.bod@gmail.com","url":"https://dragonwocky.me/","icon":"https://dragonwocky.me/avatar.jpg"}],"css":{"client":["client.css"]},"js":{"client":["test.js"]},"dir":"theming@0f0bf8b6-eae6-4273-b307-8fc43f2ee082"}]
\ No newline at end of file
diff --git a/extension/repo/registry.json b/extension/repo/registry.json
new file mode 100644
index 0000000..8e458ec
--- /dev/null
+++ b/extension/repo/registry.json
@@ -0,0 +1 @@
+["theming@0f0bf8b6-eae6-4273-b307-8fc43f2ee082"]
diff --git a/extension/scan.js b/extension/scan.js
deleted file mode 100644
index 588b155..0000000
--- a/extension/scan.js
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * notion-enhancer
- * (c) 2021 dragonwocky (https://dragonwocky.me/)
- * (https://notion-enhancer.github.io/) under the MIT license
- */
-
-// used to validate mod.json files available in a local repository,
-// the options those files reference, & then generate a registry.json from that
-
-// it also enforces the name@id naming scheme for mod dirs
-
-const fs = require('fs'),
- fsp = fs.promises,
- colour = require('chalk');
-
-let currentFolder = '';
-const errors = [];
-
-const prefix = (status = '') =>
- colour.whiteBright(``);
-function error(msg) {
- const err = `${msg} in ${colour.italic(currentFolder)}`;
- console.error(`${prefix(colour.red('error'))} ${err}`);
- errors.push(err);
-}
-const isFile = (filepath, extension = '') =>
- typeof filepath === 'string' &&
- filepath.endsWith(extension) &&
- fs.existsSync(`./repo/${currentFolder}/${filepath}`, 'file');
-
-const regexers = {
- uuid(str) {
- 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;
- error(`invalid uuid ${str}`);
- return false;
- },
- semver(str) {
- 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;
- error(`invalid semver ${str}`);
- return false;
- },
- email(str) {
- 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;
- error(`invalid email ${str}`);
- return false;
- },
- url(str) {
- 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;
- error(`invalid url ${str}`);
- return false;
- },
-};
-
-async function validate(mod) {
- mod.tags = mod.tags ?? [];
- mod.css = mod.css ?? [];
- mod.js = mod.js ?? {};
- const check = (prop, value, condition) =>
- new Promise((res, rej) =>
- condition ? res(value) : error(`invalid ${prop} ${JSON.stringify(value)}`)
- );
- return Promise.all([
- check('name', mod.name, typeof mod.name === 'string'),
- check('id', mod.id, typeof mod.id === 'string').then((id) => regexers.uuid(id)),
- check('description', mod.description, typeof mod.description === 'string'),
- check('version', mod.version, typeof mod.version === 'string').then((version) =>
- regexers.semver(version)
- ),
- check('tags', mod.tags, Array.isArray(mod.tags)).then((tags) =>
- Promise.all(tags.map((tag) => check('tag', tag, typeof tag === 'string')))
- ),
- check('authors', mod.authors, Array.isArray(mod.authors)).then((authors) =>
- Promise.all(
- authors
- .map((author) => [
- check('author.name', author.name, typeof author.name === 'string'),
- check(
- 'author.email',
- author.email,
- typeof author.email === 'string'
- ).then((email) => regexers.email(email)),
- check('author.url', author.url, typeof author.url === 'string').then((url) =>
- regexers.url(url)
- ),
- check('author.icon', author.icon, typeof author.icon === 'string').then((icon) =>
- regexers.url(icon)
- ),
- ])
- .flat()
- )
- ),
- check(
- 'css',
- mod.css,
- !!mod.css && typeof mod.css === 'object' && !Array.isArray(mod.css)
- ).then(async (css) => {
- for (const dest of ['frame', 'client', 'gui']) {
- const destFiles = css[dest];
- if (destFiles) {
- await check(`css.${dest}`, destFiles, Array.isArray(destFiles)).then((files) =>
- Promise.all(
- files.map(async (file) =>
- check(`css.${dest} file`, file, await isFile(file, '.css'))
- )
- )
- );
- }
- }
- }),
- check('js', mod.js, !!mod.js && typeof mod.js === 'object' && !Array.isArray(mod.js)).then(
- async (js) => {
- const client = js.client;
- if (client) {
- await check('js.client', client, Array.isArray(client)).then((files) =>
- Promise.all(
- files.map(async (file) =>
- check('js.client file', file, await isFile(file, '.js'))
- )
- )
- );
- }
- const electron = js.electron;
- if (electron) {
- await check('js.electron', electron, Array.isArray(electron)).then((files) =>
- Promise.all(
- files.map((file) =>
- check(
- 'js.electron file',
- file,
- !!file && typeof file === 'object' && !Array.isArray(file)
- ).then(async (file) => {
- const source = file.source;
- await check('js.electron file source', source, await isFile(source, '.js'));
- // referencing the file within the electron app
- // existence can't be validated, so only format is
- const target = file.target;
- await check(
- 'js.electron file target',
- target,
- typeof target === 'string' && target.endsWith('.js')
- );
- })
- )
- )
- );
- }
- }
- ),
- check('options', mod.options, !mod.options || (await isFile(mod.options, '.json'))).then(
- async (filepath) => {
- if (!filepath) return;
- let options;
- try {
- options = JSON.parse(await fsp.readFile(`./repo/${currentFolder}/${filepath}`));
- } catch {
- error(`invalid options ${filepath}`);
- }
- // todo: validate options
- }
- ),
- ]);
-}
-
-async function generate() {
- const mods = [];
- for (const folder of await fsp.readdir('./repo')) {
- let mod;
- try {
- mod = JSON.parse(await fsp.readFile(`./repo/${folder}/mod.json`));
- mod.dir = folder;
- currentFolder = folder;
- await validate(mod);
- mods.push(mod);
- } catch {
- error('invalid mod.json');
- }
- }
- if (!errors.length) {
- for (const mod of mods) {
- const oldDir = `./repo/${mod.dir}`;
- mod.dir = `${mod.name.replace(/[^A-Za-z0-9]/, '-')}@${mod.id}`;
- await fsp.rename(oldDir, `./repo/${mod.dir}`);
- }
- await fsp.writeFile('./registry.json', JSON.stringify(mods));
- console.info(
- `${prefix(
- colour.green('success')
- )} all mod configuration valid, registry saved to ./registry.json & folder naming scheme enforced`
- );
- }
-}
-
-if (fs.existsSync('./repo', 'dir')) {
- generate();
-} else {
- console.error(`${prefix(colour.red('error'))} no repo folder found`);
-}
diff --git a/extension/src/client.css b/extension/src/client.css
new file mode 100644
index 0000000..2e4ecc7
--- /dev/null
+++ b/extension/src/client.css
@@ -0,0 +1,46 @@
+/*
+ * notion-enhancer
+ * (c) 2021 dragonwocky (https://dragonwocky.me/)
+ * (https://notion-enhancer.github.io/) under the MIT license
+ */
+
+.enhancer--sidebarMenuTrigger {
+ user-select: none;
+ transition: background 20ms ease-in 0s;
+ cursor: pointer;
+}
+.enhancer--sidebarMenuTrigger:hover {
+ background: var(--theme--button-hover);
+}
+.enhancer--sidebarMenuTrigger svg {
+ width: 16px;
+ height: 16px;
+ margin-left: 2px;
+}
+.enhancer--sidebarMenuTrigger > div {
+ display: flex;
+ align-items: center;
+ min-height: 27px;
+ font-size: 14px;
+ padding: 2px 14px;
+ width: 100%;
+}
+.enhancer--sidebarMenuTrigger > div > div:first-child {
+ flex-shrink: 0;
+ flex-grow: 0;
+ border-radius: 3px;
+ color: var(--theme--text_sidebar);
+ width: 22px;
+ height: 22px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 8px;
+}
+.enhancer--sidebarMenuTrigger > div > div:last-child {
+ flex: 1 1 auto;
+ white-space: nowrap;
+ min-width: 0px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/extension/src/gui.html b/extension/src/gui.html
index 20131c5..9edc684 100644
--- a/extension/src/gui.html
+++ b/extension/src/gui.html
@@ -1,7 +1,7 @@
- Hello, World!
+ notion-enhancer
Hello, World!
diff --git a/extension/src/helpers.js b/extension/src/helpers.js
index fb8809c..32f421c 100644
--- a/extension/src/helpers.js
+++ b/extension/src/helpers.js
@@ -5,25 +5,28 @@
*/
'use strict';
-
-const registry = fetch(chrome.runtime.getURL('/registry.json')).then((response) =>
- response.json()
-);
+const ERROR = Symbol();
const web = {};
-web.whenReady = (func = () => {}) => {
+web.whenReady = (selectors = [], callback = () => {}) => {
return new Promise((res, rej) => {
- if (document.readyState !== 'complete') {
- document.addEventListener('readystatechange', (event) => {
- if (document.readyState === 'complete') {
- func();
+ function onLoad() {
+ let isReadyInt;
+ isReadyInt = setInterval(isReadyTest, 100);
+ function isReadyTest() {
+ if (selectors.every((selector) => document.querySelector(selector))) {
+ clearInterval(isReadyInt);
+ callback();
res(true);
}
- });
- } else {
- func();
- res(true);
+ }
+ isReadyTest();
}
+ if (document.readyState !== 'complete') {
+ document.addEventListener('readystatechange', (event) => {
+ if (document.readyState === 'complete') onLoad();
+ });
+ } else onLoad();
});
};
web.createElement = (html) => {
@@ -38,4 +41,210 @@ web.loadStyleset = (sheet) => {
return true;
};
-export { registry, web };
+//
+
+const fs = {};
+
+fs.getJSON = (path) => fetch(chrome.runtime.getURL(path)).then((res) => res.json());
+fs.getText = (path) => fetch(chrome.runtime.getURL(path)).then((res) => res.text());
+
+fs.isFile = async (path) => {
+ try {
+ await fetch(chrome.runtime.getURL(`/repo/${path}`));
+ return true;
+ } catch {
+ return false;
+ }
+};
+
+//
+
+const regexers = {
+ uuid(str) {
+ 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;
+ error(`invalid uuid ${str}`);
+ return false;
+ },
+ semver(str) {
+ 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;
+ error(`invalid semver ${str}`);
+ return false;
+ },
+ email(str) {
+ 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;
+ error(`invalid email ${str}`);
+ return false;
+ },
+ url(str) {
+ 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;
+ error(`invalid url ${str}`);
+ return false;
+ },
+};
+
+//
+
+const registry = {};
+
+registry.validate = async (mod, err, check) =>
+ Promise.all(
+ [
+ check('name', mod.name, typeof mod.name === 'string'),
+ check('id', mod.id, typeof mod.id === 'string').then((id) =>
+ id === ERROR ? ERROR : regexers.uuid(id)
+ ),
+ check('description', mod.description, typeof mod.description === 'string'),
+ check('version', mod.version, typeof mod.version === 'string').then((version) =>
+ version === ERROR ? ERROR : regexers.semver(version)
+ ),
+ check('tags', mod.tags, Array.isArray(mod.tags)).then((tags) =>
+ tags === ERROR ? ERROR : tags.map((tag) => check('tag', tag, typeof tag === 'string'))
+ ),
+ check('authors', mod.authors, Array.isArray(mod.authors)).then((authors) =>
+ authors === ERROR
+ ? 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 === ERROR ? ERROR : regexers.email(email))),
+ check('author.url', author.url, typeof author.url === 'string').then((url) =>
+ url === ERROR ? ERROR : regexers.url(url)
+ ),
+ check('author.icon', author.icon, typeof author.icon === 'string').then((icon) =>
+ icon === ERROR ? ERROR : regexers.url(icon)
+ ),
+ ])
+ ),
+ check(
+ 'css',
+ mod.css,
+ !!mod.css && typeof mod.css === 'object' && !Array.isArray(mod.css)
+ ).then((css) => {
+ if (css === ERROR) return ERROR;
+ if (!css) return undefined;
+ return ['frame', 'client', 'gui']
+ .filter((dest) => css[dest])
+ .map(async (dest) =>
+ check(`css.${dest}`, css[dest], Array.isArray(css[dest])).then((files) =>
+ files === ERROR
+ ? ERROR
+ : files.map(async (file) =>
+ check(
+ `css.${dest} file`,
+ file,
+ await fs.isFile(`${mod._dir}/${file}`, '.css')
+ )
+ )
+ )
+ );
+ }),
+ check(
+ 'js',
+ mod.js,
+ !!mod.js && typeof mod.js === 'object' && !Array.isArray(mod.js)
+ ).then(async (js) => {
+ if (js === ERROR) return ERROR;
+ if (!js) return undefined;
+ return [
+ check('js.client', js.client, !js.client ?? Array.isArray(js.client)).then(
+ (client) => {
+ if (client === ERROR) return ERROR;
+ if (!client) return undefined;
+ return client.map(async (file) =>
+ check('js.client file', file, await fs.isFile(file, '.js'))
+ );
+ }
+ ),
+ check('js.electron', js.electron, !js.electron ?? Array.isArray(js.electron)).then(
+ (electron) => {
+ if (electron === ERROR) return 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 === ERROR
+ ? ERROR
+ : [
+ check(
+ 'js.electron file source',
+ file.source,
+ await fs.isFile(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,
+ !mod.options ?? (await fs.isFile(mod.options, '.json'))
+ ).then(async (filepath) => {
+ if (filepath === ERROR) return ERROR;
+ if (!filepath) return undefined;
+ try {
+ const options = await fs.getJSON(`/repo/${mod._dir}/${mod.options}`);
+ // todo: validate options
+ } catch {
+ err(`invalid options ${filepath}`);
+ }
+ }),
+ ].flat(Infinity)
+ );
+
+registry.get = async (callback = () => {}) => {
+ registry._list = [];
+ if (!registry._errors) registry._errors = [];
+ for (const dir of await fs.getJSON('/repo/registry.json')) {
+ const err = (message) => [registry._errors.push({ source: dir, message }), 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 ?? {};
+
+ 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 !== ERROR)) registry._list.push(mod);
+ } catch (e) {
+ err('invalid mod.json');
+ }
+ }
+ callback(registry._list);
+ return registry._list;
+};
+
+registry.errors = async (callback = () => {}) => {
+ if (!registry._errors) await registry.get();
+ callback(registry._errors);
+ return registry._errors;
+};
+
+export { web, fs, regexers, registry };
diff --git a/extension/src/launcher.js b/extension/src/launcher.js
index 823042c..ef576ef 100644
--- a/extension/src/launcher.js
+++ b/extension/src/launcher.js
@@ -6,17 +6,31 @@
'use strict';
-import { registry, web } from './helpers.js';
+import { web, fs, registry } from './helpers.js';
-export default async () => {
- web.whenReady().then(async () => {
- for (let mod of await registry) {
+export default () => {
+ web.whenReady([], async () => {
+ web.loadStyleset('/src/client.css');
+ for (let mod of await registry.get()) {
for (let sheet of mod.css?.client || []) {
- web.loadStyleset(`repo/${mod.dir}/${sheet}`);
+ web.loadStyleset(`/repo/${mod._dir}/${sheet}`);
}
for (let script of mod.js?.client || []) {
- import(chrome.runtime.getURL(`repo/${mod.dir}/${script}`));
+ import(chrome.runtime.getURL(`/repo/${mod._dir}/${script}`));
}
}
});
+
+ const sidebarSelector =
+ '#notion-app > div > div.notion-cursor-listener > div.notion-sidebar-container > div > div > div > div:nth-child(4)';
+ web.whenReady([sidebarSelector], async () => {
+ const enhancerIcon = await fs.getText('/icons/colour.svg'),
+ enhancerSidebarElement = web.createElement(
+ ``
+ );
+ enhancerSidebarElement.addEventListener('click', (event) =>
+ chrome.runtime.sendMessage({ type: 'openEnhancerMenu' })
+ );
+ document.querySelector(sidebarSelector).appendChild(enhancerSidebarElement);
+ });
};
diff --git a/extension/worker.js b/extension/worker.js
index 2b07ef6..1dca318 100644
--- a/extension/worker.js
+++ b/extension/worker.js
@@ -3,3 +3,30 @@
* (c) 2021 dragonwocky (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
+
+'use strict';
+
+let _enhancerMenuTab;
+async function openEnhancerMenu() {
+ if (!_enhancerMenuTab) {
+ _enhancerMenuTab = await new Promise((res, rej) => {
+ chrome.tabs.create(
+ {
+ url: chrome.runtime.getURL('/src/gui.html'),
+ },
+ res
+ );
+ });
+ }
+ chrome.tabs.highlight({ 'tabs': _enhancerMenuTab.index }, function () {});
+}
+chrome.action.onClicked.addListener(openEnhancerMenu);
+
+chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
+ switch (request.type) {
+ case 'openEnhancerMenu':
+ openEnhancerMenu();
+ break;
+ }
+ return true;
+});