basic theming system + module validator tool

This commit is contained in:
dragonwocky 2021-04-11 23:54:28 +10:00
parent 5b516c21c6
commit f1dcc694de
22 changed files with 3436 additions and 2 deletions

104
extension/.gitignore vendored Normal file
View 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

View File

@ -1,6 +1,6 @@
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
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,2 +1,13 @@
# 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
View 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`);
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

34
extension/manifest.json Normal file
View 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
View 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":{}}]

File diff suppressed because it is too large Load Diff

View File

@ -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"]
}
}

File diff suppressed because it is too large Load Diff

9
extension/src/gui.html Normal file
View 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
View 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
View 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
View 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';