mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
Add a bookmarklet maker and improve node deployment (from today and last night)
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
<script lang="ts" context="module">
|
||||
|
||||
export enum LanguageConfig {
|
||||
None,
|
||||
JavaScript
|
||||
}
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
import CodeMirror from "svelte-codemirror-editor";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { theme } from "$lib/theme";
|
||||
import { githubLight, githubDark } from "$lib/themes/github";
|
||||
import type { Extension } from "@codemirror/state";
|
||||
|
||||
|
||||
export let value = "";
|
||||
export let readonly = false;
|
||||
export let lang: LanguageConfig = LanguageConfig.None;
|
||||
let langPlugin = null
|
||||
switch (lang) {
|
||||
case LanguageConfig.None:
|
||||
langPlugin = null
|
||||
break;
|
||||
case LanguageConfig.JavaScript:
|
||||
langPlugin = javascript()
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
let extensions :Extension[] = [];
|
||||
if (langPlugin) extensions.push(langPlugin)
|
||||
// $: console.log(value)
|
||||
|
||||
// import { linter, lintGutter } from "@codemirror/lint";
|
||||
// import * as eslint from "eslint-linter-browserify";
|
||||
|
||||
// lintGutter(),
|
||||
// linter(esLint(new eslint.Linter(), config)),
|
||||
|
||||
</script>
|
||||
|
||||
<div class="editor-wrapper card "
|
||||
class:no-header={!$$slots.header}>
|
||||
|
||||
{#if $$slots.header}
|
||||
<div class="header">
|
||||
<slot name="header"/>
|
||||
</div>
|
||||
{/if}
|
||||
<CodeMirror
|
||||
{value}
|
||||
|
||||
class="editor"
|
||||
theme={$theme == "dark" ? githubDark : githubLight}
|
||||
{extensions}
|
||||
{readonly}
|
||||
on:change
|
||||
/>
|
||||
</div>
|
||||
<style>
|
||||
|
||||
.editor-wrapper {
|
||||
min-height: 200px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-color: var(--surface-secondary-color);
|
||||
}
|
||||
:global(.editor-wrapper .cm-scroller, .editor-wrapper .cm-editor) {
|
||||
min-height: 200px;
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
/* box-shadow: var(--shadow);
|
||||
background-color: var(--surface-color); */
|
||||
}
|
||||
:global(.editor-wrapper.no-header .cm-scroller, .editor-wrapper.no-header .cm-editor) {
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
:global(pre.cm-editor) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -4,6 +4,10 @@
|
||||
--theme: #242424;
|
||||
--background-color: #f8f8f8;
|
||||
--surface-color: #fff;
|
||||
--surface-secondary-color: #ededed;
|
||||
|
||||
--input-background-color: #fff;
|
||||
--input-color: #24292e;
|
||||
--backdrop-color: rgba(247, 247, 247, .54);
|
||||
--shadow-color: rgba(0, 0, 0, .12);
|
||||
--font-color: rgba(0, 0, 0, .87);
|
||||
@@ -23,6 +27,10 @@
|
||||
--backdrop-color: rgba(20, 20, 20, .54);
|
||||
--shadow-color: rgba(255, 255, 255, .12);
|
||||
--surface-color: #242424;
|
||||
--surface-secondary-color: #222222;
|
||||
|
||||
--input-background-color: #161616;
|
||||
--input-color: #d8d8d8;
|
||||
--font-color: rgba(255, 255, 255, .87);
|
||||
--font-color-contrast: rgba(0, 0, 0, .87);
|
||||
--font-color-secondary: rgba(255, 255, 255, .6)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
let query = typeof window != "undefined" ? window?.matchMedia('(prefers-color-scheme: dark)') : undefined
|
||||
|
||||
export const theme = writable(query?.matches ? 'dark' : 'light')
|
||||
|
||||
query?.addEventListener('change', e => {
|
||||
theme.set(e.matches ? 'dark' : 'light')
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
|
||||
// NOTE: This requires enabling unsafe-inline styles in the CSP
|
||||
// From thememirror
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import {
|
||||
HighlightStyle,
|
||||
type TagStyle,
|
||||
syntaxHighlighting,
|
||||
} from '@codemirror/language';
|
||||
|
||||
interface Options {
|
||||
/**
|
||||
* Theme variant. Determines which styles CodeMirror will apply by default.
|
||||
*/
|
||||
variant: Variant;
|
||||
|
||||
/**
|
||||
* Settings to customize the look of the editor, like background, gutter, selection and others.
|
||||
*/
|
||||
settings: Settings;
|
||||
|
||||
/**
|
||||
* Syntax highlighting styles.
|
||||
*/
|
||||
styles: TagStyle[];
|
||||
}
|
||||
|
||||
type Variant = 'light' | 'dark';
|
||||
|
||||
interface Settings {
|
||||
/**
|
||||
* Editor background.
|
||||
*/
|
||||
background: string;
|
||||
|
||||
/**
|
||||
* Default text color.
|
||||
*/
|
||||
foreground: string;
|
||||
|
||||
/**
|
||||
* Caret color.
|
||||
*/
|
||||
caret: string;
|
||||
|
||||
/**
|
||||
* Selection background.
|
||||
*/
|
||||
selection: string;
|
||||
|
||||
/**
|
||||
* Background of highlighted lines.
|
||||
*/
|
||||
lineHighlight: string;
|
||||
|
||||
/**
|
||||
* Gutter background.
|
||||
*/
|
||||
gutterBackground: string;
|
||||
|
||||
/**
|
||||
* Text color inside gutter.
|
||||
*/
|
||||
gutterForeground: string;
|
||||
}
|
||||
|
||||
const createTheme = ({ variant, settings, styles }: Options): Extension => {
|
||||
const theme = EditorView.theme(
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'&': {
|
||||
backgroundColor: settings.background,
|
||||
color: settings.foreground,
|
||||
},
|
||||
'.cm-content': {
|
||||
caretColor: settings.caret,
|
||||
},
|
||||
'.cm-cursor, .cm-dropCursor': {
|
||||
borderLeftColor: settings.caret,
|
||||
},
|
||||
'&.cm-focused .cm-selectionLayer .cm-selectionBackground, .cm-content ::selection':
|
||||
{
|
||||
backgroundColor: settings.selection,
|
||||
},
|
||||
'.cm-activeLine': {
|
||||
backgroundColor: settings.lineHighlight,
|
||||
},
|
||||
'.cm-gutters': {
|
||||
backgroundColor: settings.gutterBackground,
|
||||
color: settings.gutterForeground,
|
||||
},
|
||||
'.cm-activeLineGutter': {
|
||||
backgroundColor: settings.lineHighlight,
|
||||
},
|
||||
},
|
||||
{
|
||||
dark: variant === 'dark',
|
||||
},
|
||||
);
|
||||
|
||||
const highlightStyle = HighlightStyle.define(styles);
|
||||
const extension = [theme, syntaxHighlighting(highlightStyle)];
|
||||
|
||||
return extension;
|
||||
};
|
||||
|
||||
export default createTheme;
|
||||
|
||||
export const githubLight = createTheme({
|
||||
variant: 'light',
|
||||
settings: {
|
||||
background: '#fff',
|
||||
foreground: '#24292e',
|
||||
selection: '#BBDFFF',
|
||||
// selectionMatch: '#BBDFFF',
|
||||
gutterBackground: '#fff',
|
||||
gutterForeground: '#6e7781',
|
||||
caret: '#7c3aed',
|
||||
lineHighlight: '#8a91991a',
|
||||
},
|
||||
styles: [
|
||||
{ tag: [t.standard(t.tagName), t.tagName], color: '#116329' },
|
||||
{ tag: [t.comment, t.bracket], color: '#6a737d' },
|
||||
{ tag: [t.className, t.propertyName], color: '#6f42c1' },
|
||||
{ tag: [t.variableName, t.attributeName, t.number, t.operator], color: '#005cc5' },
|
||||
{ tag: [t.keyword, t.typeName, t.typeOperator, t.typeName], color: '#d73a49' },
|
||||
{ tag: [t.string, t.meta, t.regexp], color: '#032f62' },
|
||||
{ tag: [t.name, t.quote], color: '#22863a' },
|
||||
{ tag: [t.heading, t.strong], color: '#24292e', fontWeight: 'bold' },
|
||||
{ tag: [t.emphasis], color: '#24292e', fontStyle: 'italic' },
|
||||
{ tag: [t.deleted], color: '#b31d28', backgroundColor: 'ffeef0' },
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#e36209' },
|
||||
{ tag: [t.url, t.escape, t.regexp, t.link], color: '#032f62' },
|
||||
{ tag: t.link, textDecoration: 'underline' },
|
||||
{ tag: t.strikethrough, textDecoration: 'line-through' },
|
||||
{ tag: t.invalid, color: '#cb2431' }
|
||||
],
|
||||
});
|
||||
|
||||
export
|
||||
const githubDark = createTheme({
|
||||
variant: 'dark',
|
||||
settings: {
|
||||
background: '#161616',
|
||||
foreground: '#d8d8d8',
|
||||
caret: '#c9d1d9',
|
||||
selection: '#003d73',
|
||||
// selectionMatch: '#003d73',\
|
||||
lineHighlight: '#1e1e1e',
|
||||
gutterBackground: '#1c1c1c',
|
||||
gutterForeground: '#fff',
|
||||
},
|
||||
styles: [
|
||||
{ tag: [t.standard(t.tagName), t.tagName], color: '#7ee787' },
|
||||
{ tag: [t.comment, t.bracket], color: '#8b949e' },
|
||||
{ tag: [t.className, t.propertyName], color: '#d2a8ff' },
|
||||
{ tag: [t.variableName, t.attributeName, t.number, t.operator], color: '#79c0ff' },
|
||||
{ tag: [t.keyword, t.typeName, t.typeOperator, t.typeName], color: '#ff7b72' },
|
||||
{ tag: [t.string, t.meta, t.regexp], color: '#a5d6ff' },
|
||||
{ tag: [t.name, t.quote], color: '#7ee787' },
|
||||
{ tag: [t.heading, t.strong], color: '#d2a8ff', fontWeight: 'bold' },
|
||||
{ tag: [t.emphasis], color: '#d2a8ff', fontStyle: 'italic' },
|
||||
{ tag: [t.deleted], color: '#ffdcd7', backgroundColor: 'ffeef0' },
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#ffab70' },
|
||||
{ tag: t.link, textDecoration: 'underline' },
|
||||
{ tag: t.strikethrough, textDecoration: 'line-through' },
|
||||
{ tag: t.invalid, color: '#f97583' },
|
||||
],
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user