Add a bookmarklet maker and improve node deployment (from today and last night)

This commit is contained in:
Jade Ellis
2024-04-10 15:13:16 +01:00
parent 255767c10b
commit e3c4eec7cf
20 changed files with 1057 additions and 252 deletions
@@ -0,0 +1,82 @@
<script lang="ts">
import Editor, { LanguageConfig } from "$lib/Editor.svelte";
import SvelteSeo from "svelte-seo";
import { bookmarkify, parseMeta } from "./bookmarklets";
import type { Config } from "./config";
/** @type {import('./$types').Snapshot<string>} */
export const snapshot = {
capture: () => value,
restore: (v: string) => (value = v),
};
let value = "";
let output = "";
let options: Config = {};
async function process(str: string) {
options = await parseMeta(str);
let res = await bookmarkify(str, options);
if (typeof res == "string") {
output = res;
}
}
$: progress = process(value);
</script>
<SvelteSeo
title="Bookmarklet Maker"
description="Make booklets in your browser with this tool. Make handy shortcuts to save time."
canonical="https://jade.ellis.link/bookmarklets"
/>
<h1>Bookmarklet Maker</h1>
<Editor
{value}
on:change={(e) => (value = e.detail)}
lang={LanguageConfig.JavaScript}
>
<div slot="header" class="code-header">Input</div>
</Editor>
<h2>Output</h2>
{#await progress}
<p>...waiting</p>
{:catch error}
<p style="color: red">{error.message}</p>
{/await}
<textarea name="output" class="output card" rows="1" value={output} readonly
></textarea>
<!-- <Editor readonly={true} /> -->
<p>
Bookmark this link: <a href={output}>{options.name || "My Bookmarklet"}</a>
</p>
<p>
Either drag the link to your bookmarlets bar or, on FireFox, right click and
select "Bookmark Link"
</p>
<style>
.code-header {
padding: 0.25em 0.5em;
}
.output {
position: relative;
z-index: 1;
background-color: var(--input-background-color);
color: var(--input-color);
border: none;
font-family: monospace;
line-height: 1.4;
display: block;
/* white-space: pre; */
/* word-wrap: normal; */
resize: vertical;
width: 100%;
height: 8ex;
padding: 4px 2px 4px 6px;
font-size: 1rem;
scrollbar-gutter: stable;
user-select: all;
}
</style>
@@ -0,0 +1 @@
export const prerender = true;
@@ -0,0 +1,223 @@
import MagicString from "magic-string";
import { Parser } from "acorn";
import { minify } from "terser";
let sourceMap = false;
import { configSchema } from "./config.schema";
import type { Config } from "./config";
// console.log(configSchema)
export async function bookmarkify(code: string, options: Config) {
// try {
if (options.script) {
options.script = options.script.reverse();
options.script.forEach(s => {
let { path, opts } = extractOptions(s);
code = loadScript(code, path, opts.loadOnce);
});
}
if (options.style) {
options.style.forEach(s => {
let { path, opts } = extractOptions(s);
code = loadStyle(path, opts.loadOnce) + code;
});
}
const result = await minify(code, { sourceMap });
// return result.code;
if (typeof result.code == "string") {
// const intermediate = new MagicString(result.code);
return `javascript:${encodeURIComponent("(function(){" + result.code + "})()")}`;
}
// } catch (e) {
// console.log("Error occurred", e);
// }
}
export async function parseMeta(str: string): Promise<Config> {
enum MetaState {
PreOpen,
Opened,
Closed
}
let state: MetaState = MetaState.PreOpen
const openMetadata = /==bookmarklet==/gim;
const closeMetadata = /==\/bookmarklet==/gim;
const metaLine = /^[\s]*@([^\s]+)\s+(.*)$/gim;
let options: Config = {};
Parser.parse(str, {
ecmaVersion: "latest",
onComment(isBlock, text, start, end, startLoc, endLoc) {
openMetadata.lastIndex = 0;
closeMetadata.lastIndex = 0;
metaLine.lastIndex = 0;
if (state == MetaState.PreOpen) {
let res = openMetadata.exec(text)
if (res !== null) {
state = MetaState.Opened
closeMetadata.lastIndex = openMetadata.lastIndex;
metaLine.lastIndex = openMetadata.lastIndex;
// console.log("Meta opened at", start + openMetadata.lastIndex)
}
}
// console.log(text, closeMetadata.lastIndex)
let res
while (state == MetaState.Opened && (res = metaLine.exec(text)) !== null) {
closeMetadata.lastIndex = metaLine.lastIndex;
// console.log(str.slice(start + 2 + (metaLine.lastIndex - res[0].length), start + 2 + metaLine.lastIndex ))
let k = res[1];
let v = res[2];
if (k) {
if (configSchema.properties[k]?.type == "array") {
options[k] = options[k] || [];
options[k].push(v);
} else if (configSchema.properties[k]?.type == "boolean") {
options[k] = v.toLowerCase() == 'true';
} else {
options[k] = v;
}
}
}
if (state == MetaState.Opened) {
let endRes = closeMetadata.exec(text)
if (endRes !== null) {
state = MetaState.Closed;
// console.log("Meta closed at", start + closeMetadata.lastIndex)
}
}
},
});
// @ts-ignore
if (state == MetaState.Opened) {
throw new Error("Missing metadata close block. Add '==/Bookmarklet==' to your comment block");
}
return options;
}
function loadScript(code: string, path: string, loadOnce: boolean) {
loadOnce = !!loadOnce;
let id = `bookmarklet__script_${cyrb53(path).toString(36).substring(0, 7)}`;
return `
function callback(){
${code}
}
if (!${loadOnce} || !document.getElementById("${id}")) {
var s = document.createElement("script");
if (s.addEventListener) {
s.addEventListener("load", callback, false)
} else if (s.readyState) {
s.onreadystatechange = callback
}
if (${loadOnce}) {
s.id = "${id}";
}
s.src = "${quoteEscape(path)}";
document.body.appendChild(s);
} else {
callback();
}
`;
}
function loadStyle(path: string, loadOnce: boolean) {
loadOnce = !!loadOnce;
let id = `bookmarklet__style_${cyrb53(path).toString(36).substring(0, 7)}`;
return `
if (!${loadOnce} || !document.getElementById("${id}")) {
var link = document.createElement("link");
if (${loadOnce}) {
link.id = "${id}";
}
link.rel="stylesheet";
link.href = "${quoteEscape(path)}";
document.body.appendChild(link);
}
`;
}
const cyrb53 = (str: string, seed = 0) => {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
};
function extractOptions(path: string) {
// Returns {
// path: the updated path string (minus any options)
// opts: plain object of options
// }
//
// You can prefix a path with options in the form of:
//
// ```
// @style !loadOnce !foo=false https://example.com/foo.css
// ```
//
// If there is no `=`, then the value of the option defaults to `true`.
// Values get converted via JSON.parse if possible, o/w they're a string.
//
let opts: { [x: string]: any } = {};
let matcher = /^(\![^\s]+)\s+/g
let m
let splitAfter = 0;
while ((m = matcher.exec(path)) !== null) {
splitAfter = matcher.lastIndex;
let opt = m[1].substring(1).split('=');
opts[opt[0]] = opt[1] === undefined ? true : _fuzzyParse(opt[1]);
// break
}
return { path: path.substring(splitAfter), opts };
}
const _fuzzyParse = (val: string) => {
try {
return JSON.parse(val);
} catch (e) {
return val;
}
};
// function result() {
// return minification(value)
// .then((result) => {
// errorMessage = "";
// if (result === "") {
// errorMessage = "Put some code in there!";
// } else {
// codeOutput = "javascript:(function(){" + result + "}());";
// }
// return;
// })
// .catch((err) => {
// codeOutput = "";
// return (errorMessage = err);
// });
// }
function quoteEscape(x) {
return x.replace('"', '\\"').replace("'", "\\'");
}
@@ -0,0 +1,13 @@
export interface Config {
author?: string;
description?: string;
email?: string;
license?: string;
name?: string;
repository?: string;
script?: string[];
style?: string[];
url?: string;
version?: string;
[x: string]: any;
}