basic theming system + module validator tool
104
extension/.gitignore
vendored
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2021 notion-enhancer
|
Copyright (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@ -1,2 +1,13 @@
|
|||||||
# extension
|
# extension
|
||||||
bringing all your favourite notion-enhancer features to the browser
|
|
||||||
|
bringing all your favourite notion-enhancer features to the browser (wip)
|
||||||
|
|
||||||
|
> considering that using notion in the browser is more lightweight & probably preferred for some people since it means they have less apps open, i've always planned to eventually port the enhancer to be a chrome extension.
|
||||||
|
>
|
||||||
|
> since the enhancer focuses on being able to manipulate notion's app files, it's not going to be a very friendly transfer to web - a few features will have to be lost (e.g. frameless mode & tabs, of course) and the extensions system will need to be completely rebuilt.
|
||||||
|
>
|
||||||
|
> to make this work better, i'm splitting things up: an mod repository, a chrome extension, and an app loader.
|
||||||
|
>
|
||||||
|
> i'll be building the enhancer chrome-first from now on, since it'll be more limited there (and it's easier to add extra features when porting than to take features out), and then releasing the app loader a little afterwards. both the app loader and the chrome extension will source the same themes & extensions from the mod repository, and updates & releases of individual mods won't require waiting for the enhancer version anymore.
|
||||||
|
|
||||||
|
- from the #announcements channel of the [notion-enhancer discord](https://discord.gg/sFWPXtA)
|
||||||
|
207
extension/_scan.js
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
/*
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (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(`<notion-enhancer repo scan${status ? `: ${status}` : ''}>`);
|
||||||
|
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`);
|
||||||
|
}
|
7
extension/content-loader.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/*
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import(chrome.runtime.getURL('src/launcher.js')).then((launcher) => launcher.default());
|
BIN
extension/icons/blackwhite-x128.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
extension/icons/blackwhite-x16.png
Normal file
After Width: | Height: | Size: 623 B |
BIN
extension/icons/blackwhite-x32.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
extension/icons/blackwhite-x48.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
extension/icons/colour-x128.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
extension/icons/colour-x16.png
Normal file
After Width: | Height: | Size: 634 B |
BIN
extension/icons/colour-x32.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
extension/icons/colour-x48.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
34
extension/manifest.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "notion-enhancer",
|
||||||
|
"author": "dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)",
|
||||||
|
"description": "an enhancer/customiser for the all-in-one productivity workspace notion.so",
|
||||||
|
"homepage_url": "https://notion-enhancer.github.io",
|
||||||
|
"version": "0.11.0",
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/colour-x16.png",
|
||||||
|
"32": "icons/colour-x32.png",
|
||||||
|
"48": "icons/colour-x48.png",
|
||||||
|
"128": "icons/colour-x128.png"
|
||||||
|
},
|
||||||
|
"manifest_version": 3,
|
||||||
|
"background": {
|
||||||
|
"service_worker": "src/worker.js"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"default_popup": "src/gui.html"
|
||||||
|
},
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": ["registry.json", "src/*", "repo/*"],
|
||||||
|
"matches": ["https://*.notion.so/*"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["https://*.notion.so/*"],
|
||||||
|
"js": ["content-loader.js"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"permissions": ["activeTab"],
|
||||||
|
"host_permissions": ["https://*.notion.so/*"]
|
||||||
|
}
|
1
extension/registry.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
[{"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"]},"dir":"theming@0f0bf8b6-eae6-4273-b307-8fc43f2ee082","js":{}}]
|
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
|
}
|
9
extension/src/gui.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Hello, World!</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Hello, World!</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
41
extension/src/helpers.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const registry = fetch(chrome.runtime.getURL('registry.json')).then((response) =>
|
||||||
|
response.json()
|
||||||
|
);
|
||||||
|
|
||||||
|
const web = {};
|
||||||
|
web.whenReady = (func = () => {}) => {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
if (document.readyState !== 'complete') {
|
||||||
|
document.addEventListener('readystatechange', (event) => {
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
func();
|
||||||
|
res(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
func();
|
||||||
|
res(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
web.createElement = (html) => {
|
||||||
|
const template = document.createElement('template');
|
||||||
|
template.innerHTML = html.trim();
|
||||||
|
return template.content.firstElementChild;
|
||||||
|
};
|
||||||
|
web.loadStyleset = (sheet) => {
|
||||||
|
document.head.appendChild(
|
||||||
|
web.createElement(`<link rel="stylesheet" href="${chrome.runtime.getURL(sheet)}">`)
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { registry, web };
|
19
extension/src/launcher.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { registry, web } from './helpers.js';
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
web.whenReady().then(async () => {
|
||||||
|
for (let mod of await registry) {
|
||||||
|
for (let sheet of mod.css?.client || []) {
|
||||||
|
web.loadStyleset(`repo/${mod.dir}/${sheet}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
7
extension/src/worker.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/*
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|