authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-09-29 23:21:26-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-10-14 02:40:47-07:00
log9365f5f867b8b7f4c55d03ee1dec7151f3b987ce
treea2547a777c3f2b8251f39c7d8ee622762cfff51f
parent27b17df57b1d672f6fee971407b0e4d9916bbf11
signature Commit is signed but in an unrecognized format.

feat(file-viewer): finish scan3, cache layer, and other

feat(paperclover.net): in the summer and waterfalls work feat(lib/log): rework Scope feat(lib/log/stack): do not highlight node_modules in red fix(lib/subprocess): fix spawnAndWait fix(lib/render): lower style object to string chore(lib/render/marko-runtime): upgrade chore: remove `@clo/console` chore: pin all dependencies chore: delete markdown renderer experiment chore: move framework/lib/string to lib/

49 files changed, 1684 insertions(+), 1773 deletions(-)

framework/backend/entry-node.ts-1
......@@ -1,4 +1,3 @@
1import "@paperclover/console/inject";
21import "#debug";
32
43const app = require(globalThis.CLOVER_SERVER_ENTRY ?? process.argv[2]);
framework/backend/entry-passthru.ts-1
......@@ -1,4 +1,3 @@
1import "@paperclover/console/inject";
21export default app;
32
43import app from "#backend";
framework/debug.safe.ts+6
......@@ -1,3 +1,9 @@
1process.on("uncaughtException", (error) => {
2 log.error(error);
3 process.exit(1);
4});
5import * as log from "lib/log.ts";
6
17globalThis.UNWRAP = (t, ...args) => {
28 if (t == null) {
39 throw new Error(
framework/engine/marko-runtime.ts+31-30
......@@ -7,11 +7,11 @@ interface BodyContentObject {
77 content: ServerRenderer;
88}
99
10export const createTemplate = (
11 templateId: string,
12 renderer: ServerRenderer,
10export const _template: typeof marko._template = (
11 templateId,
12 renderer,
1313) => {
14 const { render: renderFn } = marko.createTemplate(templateId, renderer);
14 const { render: renderFn } = marko._template(templateId, renderer);
1515 function wrap(props: Record<string, unknown>, n: number) {
1616 // Marko Custom Tags
1717 const cloverAsyncMarker = { isAsync: false };
......@@ -27,21 +27,22 @@ export const createTemplate = (
2727 return markoResult.then(render.raw);
2828 }
2929 const rr = markoResult.toString();
30 console.log({ rr });
3031 return render.raw(rr);
3132 }
32 wrap.render = render;
33 wrap.render = renderFn;
3334 wrap.unwrapped = renderer;
34 return wrap;
35 return wrap as unknown as Template;
3536};
3637
37export const dynamicTag = (
38 scopeId: number,
39 accessor: Accessor,
40 tag: unknown | string | ServerRenderer | BodyContentObject,
41 inputOrArgs: unknown,
42 content?: (() => void) | 0,
43 inputIsArgs?: 1,
44 serializeReason?: 1 | 0,
38export const _dynamic_tag: typeof marko._dynamic_tag = (
39 scopeId,
40 accessor,
41 tag,
42 inputOrArgs,
43 content,
44 inputIsArgs,
45 serializeReason,
4546) => {
4647 if (typeof tag === "function") {
4748 clover: {
......@@ -73,20 +74,20 @@ export const dynamicTag = (
7374 (r.rejections ??= []).push(...rejections);
7475 return reject(new Error("Render had errors"));
7576 };
76 marko.fork(
77 marko._await(
7778 scopeId,
7879 accessor,
7980 promise,
80 (string: string) => marko.write(string),
81 (string: string) => marko._html(string),
8182 0,
8283 );
8384 } else {
84 marko.write(render.stringifyNode(resolved));
85 marko._html(render.stringifyNode(resolved));
8586 }
8687 return;
8788 }
8889 }
89 return marko.dynamicTag(
90 return marko._dynamic_tag(
9091 scopeId,
9192 accessor,
9293 tag,
......@@ -97,19 +98,19 @@ export const dynamicTag = (
9798 );
9899};
99100
100export function fork(
101 scopeId: number,
102 accessor: Accessor,
103 promise: Promise<unknown>,
104 callback: (data: unknown) => void,
105 serializeMarker?: 0 | 1,
106) {
101export const _await: typeof marko._await = (
102 scopeId,
103 accessor,
104 promise,
105 callback,
106 serializeMarker,
107) => {
107108 const marker = marko.$global().cloverAsyncMarker as Async;
108109 marker.isAsync = true;
109 marko.fork(scopeId, accessor, promise, callback, serializeMarker);
110}
110 marko._await(scopeId, accessor, promise, callback, serializeMarker);
111};
111112
112export function escapeXML(input: unknown) {
113export function _escape(input: unknown) {
113114 // The rationale of this check is that the default toString method
114115 // creating `[object Object]` is universally useless to any end user.
115116 if (
......@@ -126,7 +127,7 @@ export function escapeXML(input: unknown) {
126127 `To emit a literal '${input}', use \${String(value)}`,
127128 );
128129 }
129 return marko.escapeXML(input);
130 return marko._escape(input);
130131}
131132
132133interface Async {
......@@ -135,5 +136,5 @@ interface Async {
135136
136137import * as render from "#engine/render";
137138import type { ServerRenderer } from "marko/html/template";
138import { type Accessor } from "marko/common/types";
139import type { Template } from "marko/common/types";
139140import * as marko from "#marko/html";
framework/engine/render.ts+19-8
......@@ -157,7 +157,7 @@ export function resolveNode(r: State, node: unknown): ResolvedNode {
157157 return resolveNode(r, tag(props));
158158 } catch (e) {
159159 const { 4: src } = node;
160 if (e && typeof e === "object") (e as { src?: string }).src = src;
160 if (e && typeof e === "object") (e as { src?: string }).src ??= src;
161161 throw e;
162162 } finally {
163163 current = null;
......@@ -222,19 +222,30 @@ function stringifyElement(element: ResolvedElement) {
222222 default:
223223 attr = `${prop}=${quoteIfNeeded(escapeAttribute(String(value)))}`;
224224 break;
225 case "className":
226 // Legacy React Compat
227 case "class":
228 attr = `class=${
229 quoteIfNeeded(escapeAttribute(clsx(value as ClsxInput)))
230 }`;
225 case "className": // Legacy React Compat
226 case "class": {
227 const str = clsx(value as ClsxInput);
228 if (!str) continue;
229 attr = `class=${quoteIfNeeded(escapeAttribute(str))}`;
231230 break;
231 }
232 case "style": {
233 const str = typeof value === "string"
234 ? value
235 : Object.entries(value).map((kv) => kv.join(":")).join(";");
236 if (!str) continue;
237 attr = `style=${quoteIfNeeded(escapeAttribute(str))}`;
238 break;
239 }
232240 case "htmlFor":
233241 throw new Error("Do not use the `htmlFor` attribute. Use `for`");
242 case "dangerouslySetInnerHTML":
243 throw new Error(
244 "Do not use the `dangerouslySetInnerHTML` attribute. Use `render.raw()`",
245 );
234246 // Do not process these
235247 case "children":
236248 case "ref":
237 case "dangerouslySetInnerHTML":
238249 case "key":
239250 continue;
240251 }
framework/esbuild-support.ts+1-2
......@@ -112,9 +112,8 @@ export function isIgnoredSource(source: string) {
112112}
113113
114114import * as esbuild from "esbuild";
115import * as string from "#sitegen/string";
115import * as string from "lib/string.ts";
116116import * as path from "node:path";
117117import * as fs from "#sitegen/fs";
118import * as incr from "./incremental.ts";
119118import * as hot from "./hot.ts";
120119import { markoCache } from "./marko.ts";
framework/hot.ts+1
......@@ -153,6 +153,7 @@ export function loadEsbuildCode(
153153 jsxDev: true,
154154 sourcefile: filepath,
155155 sourcemap: "inline",
156 sourceRoot: "/",
156157 }).code;
157158 return module._compile(src, filepath, "commonjs");
158159}
framework/incremental.ts+1-2
......@@ -63,7 +63,6 @@ export function work<I, O>(
6363 const io = new Io(key, p);
6464 jobs += 1;
6565 newKeys += 1;
66 await timers.setTimeout(200);
6766 try {
6867 const value = await job(io, input as I);
6968 validateSerializable(value, "");
......@@ -103,7 +102,7 @@ export async function compile<T>(
103102 p: progress.Ref = progress,
104103) {
105104 ASSERT(!running, `Cannot run twice at the same time.`);
106 const node = p.start("sitegen!");
105 using node = p.start("sitegen!");
107106 try {
108107 running = true;
109108 ASSERT(jobs === 0);
framework/lib/assets.ts+3-8
......@@ -2,7 +2,7 @@
22// This module implements decoding and serving of the asset blobs,
33// but also implements patching of dynamic assets. The `Manifest`
44// is generated by `incremental.ts`
5const debug = console.scoped("assets");
5const console = log.scoped("assets");
66const root = import.meta.dirname;
77let current: Loaded | null = null;
88
......@@ -93,11 +93,6 @@ export async function serveAsset(
9393 );
9494}
9595
96/** @deprecated */
97export function hasAsset(id: string) {
98 return UNWRAP(current).map[id] !== undefined;
99}
100
10196export function etagMatches(etag: string, ifNoneMatch: string) {
10297 return ifNoneMatch === etag || ifNoneMatch.split(/,\s*/).indexOf(etag) > -1;
10398}
......@@ -171,7 +166,7 @@ function respondWithBufferAndViews(
171166 } else {
172167 body = buffer.subarray(...asset.raw);
173168 }
174 debug(
169 console.info(
175170 `${key} encoding=${headers["Content-Encoding"] ?? "raw"} status=${status}`,
176171 );
177172 if (!Object.keys(headers).some((x) => x.toLowerCase() === "cache-control")) {
......@@ -290,4 +285,4 @@ import type { AssetKey as Key } from "../../.clover/ts/asset.d.ts";
290285import * as crypto from "node:crypto";
291286import * as zlib from "node:zlib";
292287import * as util from "node:util";
293import * as console from "@paperclover/console";
288import * as log from "lib/log.ts";
framework/lib/async.ts+2-221
......@@ -1,219 +1,5 @@
11const five_minutes = 5 * 60 * 1000;
22
3interface QueueOptions<T, R> {
4 name: string;
5 fn: (item: T, spin: Spinner) => Promise<R>;
6 getItemText?: (item: T) => string;
7 maxJobs?: number;
8 passive?: boolean;
9}
10
11// Process multiple items in parallel, queue up as many.
12/** @deprecated */
13export class Queue<T, R> {
14 #name: string;
15 #fn: (item: T, spin: Spinner) => Promise<R>;
16 #maxJobs: number;
17 #getItemText: (item: T) => string;
18 #passive: boolean;
19
20 #active: Spinner[] = [];
21 #queue: Array<[T] | [T, (result: R) => void, (err: unknown) => void]> = [];
22
23 #cachedProgress: Progress<{ active: Spinner[] }> | null = null;
24 #done: number = 0;
25 #total: number = 0;
26 #onComplete: (() => void) | null = null;
27 #estimate: number | null = null;
28 #errors: unknown[] = [];
29
30 constructor(options: QueueOptions<T, R>) {
31 this.#name = options.name;
32 this.#fn = options.fn;
33 this.#maxJobs = options.maxJobs ?? 5;
34 this.#getItemText = options.getItemText ?? defaultGetItemText;
35 this.#passive = options.passive ?? false;
36 }
37
38 cancel() {
39 const bar = this.#cachedProgress;
40 bar?.stop();
41 this.#queue = [];
42 }
43
44 get bar() {
45 const cached = this.#cachedProgress;
46 if (!cached) {
47 const bar = (this.#cachedProgress = new Progress({
48 spinner: null,
49 text: ({ active }) => {
50 const now = performance.now();
51 let text = `[${this.#done}/${this.#total}] ${this.#name}`;
52 let n = 0;
53 for (const item of active) {
54 let itemText = "- " + item.format(now);
55 text += `\n` +
56 itemText.slice(0, Math.max(0, process.stdout.columns - 1));
57 if (n > 10) {
58 text += `\n ... + ${active.length - n} more`;
59 break;
60 }
61 n++;
62 }
63 return text;
64 },
65 props: {
66 active: [] as Spinner[],
67 },
68 }));
69 bar.value = 0;
70 return bar;
71 }
72 return cached;
73 }
74
75 addReturn(args: T) {
76 this.#total += 1;
77 this.updateTotal();
78 if (this.#active.length >= this.#maxJobs) {
79 const { promise, resolve, reject } = Promise.withResolvers<R>();
80 this.#queue.push([args, resolve, reject]);
81 return promise;
82 }
83 return this.#run(args);
84 }
85
86 add(args: T) {
87 return this.addReturn(args).then(
88 () => {},
89 () => {},
90 );
91 }
92
93 addMany(items: T[]) {
94 this.#total += items.length;
95 this.updateTotal();
96
97 const runNowCount = this.#maxJobs - this.#active.length;
98 const runNow = items.slice(0, runNowCount);
99 const runLater = items.slice(runNowCount);
100 this.#queue.push(...runLater.reverse().map<[T]>((x) => [x]));
101 runNow.map((item) => this.#run(item).catch(() => {}));
102 }
103
104 async #run(args: T): Promise<R> {
105 const bar = this.bar;
106 const itemText = this.#getItemText(args);
107 const spinner = new Spinner(itemText);
108 spinner.stop();
109 (spinner as any).redraw = () => (bar as any).redraw();
110 const active = this.#active;
111 try {
112 active.unshift(spinner);
113 bar.props = { active };
114 // console.log(this.#name + ": " + itemText);
115 const result = await this.#fn(args, spinner);
116 this.#done++;
117 return result;
118 } catch (err) {
119 if (err && typeof err === "object") {
120 (err as any).job = itemText;
121 }
122 this.#errors.push(err);
123 console.error(util.inspect(err, false, Infinity, true));
124 throw err;
125 } finally {
126 active.splice(active.indexOf(spinner), 1);
127 bar.props = { active };
128 bar.value = this.#done;
129
130 // Process next item
131 const next = this.#queue.shift();
132 if (next) {
133 const args = next[0];
134 this.#run(args)
135 .then((result) => next[1]?.(result))
136 .catch((err) => next[2]?.(err));
137 } else if (this.#active.length === 0) {
138 if (this.#passive) {
139 this.bar.stop();
140 this.#cachedProgress = null;
141 }
142 this.#onComplete?.();
143 }
144 }
145 }
146
147 updateTotal() {
148 const bar = this.bar;
149 bar.total = Math.max(this.#total, this.#estimate ?? 0);
150 }
151
152 set estimate(e: number) {
153 this.#estimate = e;
154 if (this.#cachedProgress) {
155 this.updateTotal();
156 }
157 }
158
159 async done(o?: { method: "success" | "stop" }) {
160 if (this.#active.length === 0) {
161 this.#end(o);
162 return;
163 }
164
165 const { promise, resolve } = Promise.withResolvers<void>();
166 this.#onComplete = resolve;
167 await promise;
168 this.#end(o);
169 }
170
171 #end({
172 method = this.#passive ? "stop" : "success",
173 }: {
174 method?: "success" | "stop";
175 } = {}) {
176 const bar = this.#cachedProgress;
177 if (this.#errors.length > 0) {
178 if (bar) bar.stop();
179 throw new AggregateError(
180 this.#errors,
181 this.#errors.length + " jobs failed in '" + this.#name + "'",
182 );
183 }
184
185 if (bar) bar[method]();
186 }
187
188 get active(): boolean {
189 return this.#active.length !== 0;
190 }
191
192 [Symbol.dispose]() {
193 if (this.active) {
194 this.cancel();
195 }
196 }
197}
198
199const cwd = process.cwd();
200function defaultGetItemText(item: unknown) {
201 let itemText = "";
202 if (typeof item === "string") {
203 itemText = item;
204 } else if (typeof item === "object" && item !== null) {
205 const { path, label, id } = item as any;
206 itemText = label ?? path ?? id ?? JSON.stringify(item);
207 } else {
208 itemText = JSON.stringify(item);
209 }
210
211 if (itemText.startsWith(cwd)) {
212 itemText = path.relative(cwd, itemText);
213 }
214 return itemText;
215}
216
2173/** @deprecated use OnceMap2 */
2184export class OnceMap<T> {
2195 private ongoing = new Map<string, Promise<T>>();
......@@ -415,8 +201,9 @@ export class PromiseAggregator {
415201
416202 async all() {
417203 while (this.promises.size > 0) {
418 await Promise.all(this.promises);
204 const wait = [...this.promises];
419205 this.promises.clear();
206 await Promise.all(wait);
420207 }
421208 if (this.failures.length > 0) {
422209 const agg = new AggregateError(this.failures);
......@@ -429,9 +216,3 @@ export class PromiseAggregator {
429216 return this.all();
430217 }
431218}
432
433import { Progress } from "@paperclover/console/Progress";
434import { Spinner } from "@paperclover/console/Spinner";
435import * as path from "node:path";
436import process from "node:process";
437import * as util from "node:util";
framework/lib/markdown.tsx deleted-200
......@@ -1,200 +0,0 @@
1/* Implementation of [CommonMark] specification for markdown with support
2 * for custom syntax extensions via the parser options. Instead of
3 * returning an AST that has a second conversion pass to JSX, the
4 * returned value of 'parse' is 'engine.Node' which can be stringified
5 * via Clover's SSR engine. This way, generation optimizations, async
6 * components, and other features are gained for free here.
7 *
8 * [CommonMark]: https://spec.commonmark.org/0.31.2/
9 */
10function parse(src: string, options: Partial<ParseOpts> = {}) {}
11
12/* Render markdown content. Same function as 'parse', but JSX components
13 * only take one argument and must start with a capital letter. */
14export function Markdown({
15 src,
16 ...options
17}: { src: string } & Partial<ParseOpts>) {
18 return parse(src, options);
19}
20
21// TODO: This implementation is flawed because it is impossible to sanely handle
22// emphasis and strong emphasis, and all their edge cases. Instead of making these
23// using extensions interface, they should be special cased.
24function parseInline(src: string, options: Partial<InlineOpts> = {}) {
25 const { rules = inlineRules, links = new Map() } = options;
26 const opts: InlineOpts = { rules, links };
27 const parts: render.Node[] = [];
28 const ruleList = Object.values(rules);
29 parse: while (true) {
30 for (const rule of ruleList) {
31 if (!rule.match) continue;
32 const match = src.match(rule.match);
33 if (!match) continue;
34 const index = UNWRAP(match.index);
35 const after = src.slice(index + match[0].length);
36 const parse = rule.parse({ after, match: match[0], opts });
37 if (!parse) continue;
38 // parse before
39 parts.push(src.slice(0, index), parse.result);
40 src = parse.rest ?? after;
41 continue parse;
42 }
43 break;
44 }
45 parts.push(src);
46 return parts;
47}
48
49// -- interfaces --
50interface ParseOpts {
51 gfm: boolean;
52 blockRules: Record<string, BlockRule>;
53 inlineRules: Record<string, InlineRule>;
54}
55interface InlineOpts {
56 rules: Record<string, InlineRule>;
57 links: Map<string, LinkRef>;
58}
59interface InlineRule {
60 match: RegExp;
61 parse(opts: {
62 after: string;
63 match: string;
64 opts: InlineOpts;
65 }): InlineParse | null;
66}
67interface InlineParse {
68 result: engine.Node;
69 rest?: string;
70}
71interface LinkRef {
72 href: string;
73 title: string | null;
74}
75interface BlockRule {
76 match: RegExp;
77 parse(opts: {}): unknown;
78}
79export const inlineRules: Record<string, InlineRule> = {
80 code: {
81 match: /`+/,
82 // 6.1 - code spans
83 parse({ after, match }) {
84 const end = after.indexOf(match);
85 if (end === -1) return null;
86 let inner = after.slice(0, end);
87 const rest = after.slice(end + match.length);
88 // If the resulting string both begins and ends with a space
89 // character, but does not consist entirely of space characters,
90 // a single space character is removed from the front and back.
91 if (inner.match(/^ [^ ]+ $/)) inner = inner.slice(1, -1);
92 return { result: <code>{inner}</code>, rest };
93 },
94 },
95 link: {
96 match: /(?<!!)\[/,
97 // 6.3 - links
98 parse({ after, opts }) {
99 // Match '[' to let the inner-most link win.
100 const splitText = splitFirst(after, /[[\]]/);
101 if (!splitText) return null;
102 if (splitText.delim !== "]") return null;
103 const { first: textSrc, rest: afterText } = splitText;
104 let href: string,
105 title: string | null = null,
106 rest: string;
107 if (afterText[0] === "(") {
108 // Inline link
109 const splitTarget = splitFirst(afterText.slice(1), /\)/);
110 if (!splitTarget) return null;
111 ({ rest } = splitTarget);
112 const target = parseLinkTarget(splitTarget.first);
113 if (!target) return null;
114 ({ href, title } = target);
115 } else if (afterText[0] === "[") {
116 const splitTarget = splitFirst(afterText.slice(1), /]/);
117 if (!splitTarget) return null;
118 const name = splitTarget.first.trim().length === 0
119 // Collapsed reference link
120 ? textSrc.trim()
121 // Full Reference Link
122 : splitTarget.first.trim();
123 const target = opts.links.get(name);
124 if (!target) return null;
125 ({ href, title } = target);
126 ({ rest } = splitTarget);
127 } else {
128 // Shortcut reference link
129 const target = opts.links.get(textSrc);
130 if (!target) return null;
131 ({ href, title } = target);
132 rest = afterText;
133 }
134 return {
135 result: <a {...{ href, title }}>{parseInline(textSrc, opts)}</a>,
136 rest,
137 };
138 },
139 },
140 image: {
141 match: /!\[/,
142 // 6.4 - images
143 parse({ after, opts }) {
144 // Match '[' to let the inner-most link win.
145 const splitText = splitFirst(after, /[[\]]/);
146 if (!splitText) return null;
147 if (splitText.delim !== "]") return null;
148 const { first: textSrc, rest: afterText } = splitText;
149 },
150 },
151 emphasis: {
152 // detect left-flanking delimiter runs, but this expression does not
153 // consider preceding escapes. instead, those are programatically
154 // checked inside the parse function.
155 match: /(?:\*+|(?<!\p{P})_+)(?!\s|\p{P}|$)/u,
156 // 6.2 - emphasis and strong emphasis
157 parse({ before, match, after, opts }) {
158 // find out how long the delim sequence is
159 // look for 'ends'
160 },
161 },
162 autolink: {},
163 html: {},
164 br: {
165 match: / +\n|\\\n/,
166 parse() {
167 return { result: <br /> };
168 },
169 },
170};
171
172function parseLinkTarget(src: string) {
173 let href: string,
174 title: string | null = null;
175 href = src;
176 return { href, title };
177}
178
179/* Find a delimiter while considering backslash escapes. */
180function splitFirst(text: string, match: RegExp) {
181 let first = "",
182 delim: string,
183 escaped: boolean;
184 do {
185 const find = text.match(match);
186 if (!find) return null;
187 delim = find[0];
188 const index = UNWRAP(find.index);
189 let i = index - 1;
190 escaped = false;
191 while (i >= 0 && text[i] === "\\") (escaped = !escaped), (i -= 1);
192 first += text.slice(0, index - +escaped);
193 text = text.slice(index + find[0].length);
194 } while (escaped);
195 return { first, delim, rest: text };
196}
197
198console.log(render.sync(parseInline("meow `bwaa` `` ` `` `` `z``")));
199
200import * as render from "#engine/render";
framework/lib/path.ts+18-2
......@@ -17,6 +17,14 @@ export class Path {
1717 ));
1818 }
1919
20 static createDirSync(...parts: string[]): Path;
21 static createDirSync(base: Path | string, ...more: string[]): Path;
22 static createDirSync(base: Path | string, ...more: string[]): Path {
23 const path = Path.resolve(base, ...more);
24 path.makeDirSync();
25 return path;
26 }
27
2028 /** For convenience, paths are deduplicated. */
2129 constructor(raw: string) {
2230 ASSERT(path.isAbsolute(raw));
......@@ -174,8 +182,15 @@ export class Path {
174182 return fs.readdirSync(this.raw).map((sub) => this.join(sub));
175183 }
176184
177 async readDir(): Promise<Array<Path>> {
178 return (await fs.readdir(this.raw)).map((sub) => this.join(sub));
185 async readDir(
186 { recursive }: { recursive?: boolean } = {},
187 ): Promise<Array<Path>> {
188 return (await fs.readdir(this.raw, { recursive }))
189 .map((sub) => this.join(sub));
190 }
191
192 async writeJsonFile(json: unknown) {
193 await fs.writeFile(this.raw, JSON.stringify(json));
179194 }
180195
181196 static sep = path.sep;
......@@ -194,3 +209,4 @@ import * as fs from "./fs.ts";
194209import * as util from "node:util";
195210import * as error from "./error.ts";
196211import type { RmOptions } from "node:fs";
212import type { Serialized } from "./lru.ts";
framework/lib/string.ts deleted-3
......@@ -1,3 +0,0 @@
1export function escapeRegExp(source: string) {
2 return source.replace(/[\$\\]/g, "\\$&");
3}
framework/lib/subprocess.ts+24-4
......@@ -1,3 +1,6 @@
1// This file is expecting a full rewrite to abstract the Node API away
2// entirely, exposing web streams I/O.
3
14const execFileRaw = util.promisify(child_process.execFile);
25export const exec: typeof execFileRaw = ((
36 ...args: Parameters<typeof execFileRaw>
......@@ -12,6 +15,7 @@ export const exec: typeof execFileRaw = ((
1215 })) as any;
1316
1417const activePids = new Set<number>();
18
1519export interface SpawnOptions extends child_process.SpawnOptions {
1620 cmd: string[];
1721}
......@@ -26,9 +30,28 @@ export function spawn({ cmd, ...rest }: SpawnOptions) {
2630 return process;
2731}
2832
33export interface ForkOptions extends child_process.ForkOptions {
34 cmd: string[];
35}
36
37export function fork({ cmd, ...rest }: ForkOptions) {
38 rest.detached ??= true;
39 const process = child_process.fork(cmd[0]!, cmd.slice(1), rest);
40 const pid = rest.detached && process.pid ? -process.pid : process.pid;
41 if (pid) {
42 activePids.add(pid);
43 process.on("exit", () => void activePids.delete(pid));
44 }
45 return process;
46}
47
2948export async function spawnAndWait(opts: SpawnOptions) {
3049 opts.stdio ??= ["ignore", "inherit", "inherit"];
3150 const process = spawn(opts);
51 const output = Promise.all([
52 process.stdout ? Array.fromAsync(process.stdout).then(Buffer.concat) : null,
53 process.stderr ? Array.fromAsync(process.stderr).then(Buffer.concat) : null,
54 ]);
3255 let [code, signal] = await events.EventEmitter.once(process, "exit") as [
3356 number | null,
3457 string | null,
......@@ -38,10 +61,7 @@ export async function spawnAndWait(opts: SpawnOptions) {
3861 const exit = signal ? `signal ${signal}` : `code ${code}`;
3962 throw new Error(`${opts.cmd[0]} failed with ${exit}`);
4063 }
41 const [stdout, stderr] = await Promise.all([
42 process.stdout ? Array.fromAsync(process.stdout).then(Buffer.concat) : null,
43 process.stderr ? Array.fromAsync(process.stderr).then(Buffer.concat) : null,
44 ]);
64 const [stdout, stderr] = await output;
4565 return { stdout, stderr };
4666}
4767
framework/marko.ts+15-2
......@@ -29,10 +29,22 @@ export function loadMarko(module: NodeJS.Module, filepath: string) {
2929 ) + '\nimport { addScript as CloverScriptInclude } from "#sitegen";\n';
3030 }
3131
32 src = marko.compileSync(src, filepath).code;
33 src = src.replace("marko/debug/html", "#engine/marko-runtime");
32 const compiled = marko.compileSync(src, filepath, {
33 sourceMaps: true,
34 });
35 // workaround an esbuild bug?
36 (compiled.map as unknown as { sourceRoot: string }).sourceRoot = "/";
37 compiled.map.sources[0] = filepath;
38 src = compiled.code.replace("marko/debug/html", "#engine/marko-runtime") +
39 "\n//# sourceMappingURL=data:application/json;charset=utf-8;base64," +
40 Buffer.from(JSON.stringify(compiled.map), "utf-8").toString("base64");
3441 cache = { src, scannedClientRefs: Array.from(scannedClientRefs) };
3542 markoCache.set(filepath, cache);
43 fs.writeMkdirSync(
44 ".clover/marko/" + path.relative(".", filepath).replace(/^src\//, "") +
45 ".js",
46 src,
47 );
3648 }
3749
3850 const { src, scannedClientRefs } = cache;
......@@ -44,3 +56,4 @@ export function loadMarko(module: NodeJS.Module, filepath: string) {
4456import * as marko from "@marko/compiler";
4557import * as hot from "./hot.ts";
4658import * as fs from "#sitegen/fs";
59import * as path from "node:path";
framework/watch.ts+13-16
......@@ -2,9 +2,9 @@
22
33const debounceMilliseconds = 25;
44
5let subprocess: child_process.ChildProcess | null = null;
5let server: child_process.ChildProcess | null = null;
66process.on("beforeExit", () => {
7 subprocess?.removeListener("close", onSubprocessClose);
7 server?.removeListener("close", onSubprocessClose);
88});
99
1010let watch: Watch;
......@@ -17,21 +17,20 @@ export async function main() {
1717}
1818
1919function serve() {
20 if (subprocess) {
21 subprocess.removeListener("close", onSubprocessClose);
22 subprocess.kill();
20 if (server) {
21 server.removeListener("close", onSubprocessClose);
22 server.kill();
2323 }
24 subprocess = child_process.fork(".clover/o/backend.js", [
25 "--development",
26 ], {
24 server = subprocess.fork({
25 cmd: [".clover/o/backend.js", "--development"],
2726 stdio: "inherit",
2827 execArgv: ["--no-warnings"],
2928 });
30 subprocess.on("close", onSubprocessClose);
29 server.on("close", onSubprocessClose);
3130}
3231
3332function onSubprocessClose(code: number | null, signal: string | null) {
34 subprocess = null;
33 server = null;
3534 const status = code != null ? `code ${code}` : `signal ${signal}`;
3635 console.error(`Backend process exited with ${status}`);
3736}
......@@ -50,10 +49,10 @@ async function rebuild(files: string[]) {
5049 for (const file of removeWatch) watch.remove(file);
5150 watch.add(...watchFiles);
5251 // Restart the server if it was changed or not running.
53 if (!subprocess || newOutputs.includes("backend.js")) {
52 if (!server || newOutputs.includes("backend.js")) {
5453 serve();
55 } else if (subprocess && newAssets) {
56 subprocess.send({ type: "clover.assets.reload" });
54 } else if (server && newAssets) {
55 server.send({ type: "clover.assets.reload" });
5756 }
5857 }).catch((err) => {
5958 console.error(util.inspect(err));
......@@ -167,10 +166,8 @@ class Watch {
167166}
168167
169168import * as fs from "node:fs";
170import { withSpinner } from "@paperclover/console/Spinner";
171169import * as generate from "./generate.ts";
172170import * as incr from "./incremental.ts";
173171import * as path from "node:path";
174172import * as util from "node:util";
175import * as hot from "./hot.ts";
176import * as child_process from "node:child_process";
173import * as subprocess from "#sitegen/subprocess";
lib/log.test.ts+3
......@@ -1,6 +1,9 @@
11import { ASSERT, UNWRAP } from "lib/assert.ts";
22import { test } from "node:test";
33import * as headless from "lib/log/headless.ts";
4import * as log from "lib/log.ts";
5
6log satisfies log.Scope; // the namespace import is a valid Scope
47
58class TestWidgetHost implements headless.WidgetHost {
69 columns = 80;
lib/log.ts+75-25
......@@ -1,6 +1,59 @@
1/**
2 * By using `lib/log.ts`, your application gets easy scoped logging as well as
3 * integration with terminal widgets such as `lib/log/progress.ts`. The pattern
4 * for using this module is to shadow the global `console` with a local scope:
5 *
6 * import * as log from "@clo/lib/log";
7 * const console = log.scoped("http");
8 * console("hi!"); // debug
9 *
10 * Or to use the global socpe, import the namespace as `console`.
11 *
12 * import * as console from "@clo/lib/log";
13 *
14 * Now, the code reads familiarly and output is organized.
15 *
16 * This module offers a Node.js integration to have logs go to the standard
17 * output. Custom writers can be used with `lib/log/headless.ts`.
18 *
19 * @module
20 */
21
22/**
23 * Logging scopes implement part of the `Console` API
24 */
25export interface Scope {
26 /** emit an informational message */
27 info(...args: unknown[]): void;
28 /** emit an advisory message */
29 warn(...args: unknown[]): void;
30 /** emit a failure message */
31 error(...args: unknown[]): void;
32
33 /**
34 * emit a debugging message. */
35 log(...args: unknown[]): void;
36 /**
37 * emit a debugging message. this method is in place for compatibility with
38 * the `console` API. prefer calling `.log` directly.
39 */
40 debug(...args: unknown[]): void;
41
42 /** create a nested sub-scope */
43 scoped(name: string): Scope;
44}
45
46/** custom scopes are colored based on the name */
47export function scoped(name: string): Scope {
48 return globalLog.scoped(name);
49}
50
51// These functions implement `Scope`. If a function takes in `Scope`, the
52// namespace import of this file can be passed to it.
53
154/** informational */
255export function info(...args: unknown[]) {
3 globalLog(...args);
56 globalLog.info(...args);
457}
558/** advisory */
659export function warn(...args: unknown[]) {
......@@ -11,28 +64,12 @@ export function error(...args: unknown[]) {
1164 globalLog.error(...args);
1265}
1366/** debugging */
14export function debug(...args: unknown[]) {
15 globalLog(...args);
67export function log(...args: unknown[]) {
68 globalLog.log(...args);
1669}
17
18export interface Scope {
19 /** default call signature is informational scope */
20 (...args: unknown[]): void;
21 /** informational */
22 info(...args: unknown[]): void;
23 /** advisory */
24 warn(...args: unknown[]): void;
25 /** failure */
26 error(...args: unknown[]): void;
27
28 /** prefix */
29 name: string;
30 scoped(name: string): Scope;
31}
32
33/** custom scopes are colored based on the name */
34export function scoped(name: string): Scope {
35 return globalLog.scoped(name);
70/** debugging */
71export function debug(...args: unknown[]) {
72 globalLog.debug(...args);
3673}
3774
3875/**
......@@ -77,9 +114,22 @@ export interface Widget {
77114 // onKey?(key: string): void;
78115}
79116
80const globalWidgetHost = /* @__PURE__ */ (() => {
117const globalWidgetHost = /* @__PURE__ */ ((): headless.WidgetHost => {
118 const { process } = globalThis;
119 if (!process || !process.stderr.isTTY) {
120 return {
121 // TODO: allow passing a level here
122 writeLine: (line) => console["log"](line),
123 getDrawLock: () => ts.defer(() => {}),
124 startWidget: (w) => {
125 const close = w.onChange?.(() => {});
126 return ts.defer(close ?? (() => {}));
127 },
128 cancel: () => {},
129 };
130 }
81131 const widget = headless.widgetHost({
82 // TODO: consider synchronization and make these async
132 // TODO:allow passing a level here to put debug, warn and errors on stderr
83133 writeOutput: (string) => process.stdout.write(string),
84134 writeInteractive: (string) => process.stderr.write(string),
85135 now: () => performance.now(),
......@@ -100,4 +150,4 @@ const globalLog = /* @__PURE__ */ headless.logger({
100150
101151import process from "node:process";
102152import * as headless from "lib/log/headless.ts";
103import type * as ts from "lib/ts.ts";
153import * as ts from "lib/ts.ts";
lib/log/headless.ts+3
......@@ -302,7 +302,10 @@ export function logger(env: LoggerOptions): log.Scope {
302302 function scoped(name: string): log.Scope {
303303 const formatted = name ? name + colon : name;
304304 const fn = logFn.bind(null, formatted, 0) as Partial<log.Scope>;
305 // TODO: repair
305306 fn.info = fn as log.Scope["info"];
307 fn.debug = fn as log.Scope["info"];
308 fn.log = fn as log.Scope["info"];
306309 fn.warn = logFn.bind(null, formatted, 1);
307310 fn.error = logFn.bind(null, formatted, 2);
308311 fn.scoped = createSubScope;
lib/log/progress.ts+37-13
......@@ -45,6 +45,10 @@ export interface Node extends Ref {
4545 * there are no children.
4646 */
4747 passive: boolean;
48 /**
49 * reactive, default `false`. when true, this progress item is hidden
50 */
51 hidden: boolean;
4852 /** reactive. null means decide based on nested depth + terminal height */
4953 maxHeight: null | number;
5054 /** reactive. specify sorting for children */
......@@ -66,6 +70,7 @@ export interface StartOptions {
6670 value?: number | undefined | null;
6771 estimate?: number | undefined | null;
6872 passive?: boolean | undefined | null;
73 hidden?: boolean | undefined | null;
6974 showEstimate?: boolean | undefined | null;
7075 maxHeight?: number | undefined | null;
7176 sortChildren?: Node["sortChildren"] | undefined | null;
......@@ -89,6 +94,7 @@ export type SortNode =
8994 | "value"
9095 | "estimate"
9196 | "passive"
97 | "hidden"
9298 | "showEstimate"
9399 | "sortChildren"
94100 >
......@@ -103,7 +109,7 @@ export interface Internal extends SortNode {
103109
104110type Signal =
105111 | "draw"
106 | { type: "end"; title: string; logs: string[] }
112 | { type: "end"; title: string; logs: string[]; state: Internal }
107113 | { type: "line"; line: string };
108114
109115function newNode(
......@@ -116,6 +122,7 @@ function newNode(
116122 estimate: opts.estimate ?? 0,
117123 showEstimate: opts.showEstimate ?? true,
118124 passive: opts.passive ?? false,
125 hidden: opts.hidden ?? false,
119126 logs: [],
120127 children: [],
121128 parent: null,
......@@ -127,17 +134,18 @@ function newNode(
127134 let detached = false;
128135 function end() {
129136 if (detached) return;
130 if (!state.parent) return;
131 const i = state.parent.children.indexOf(state);
132 if (i === -1) return;
133137 let title = state.text;
134 let p: Internal | null = state;
135 while (p = p.parent) title = p.text + " / " + title;
136 state.signal({ type: "end", title, logs: state.logs });
138 if (state.parent) {
139 const i = state.parent.children.indexOf(state);
140 ASSERT(i !== -1);
141 let p: Internal | null = state;
142 while (p = p.parent) title = p.text + " / " + title;
143 state.parent.children.splice(i, 1);
144 state.parent = null;
145 detached = true;
146 }
147 state.signal({ type: "end", title, logs: state.logs, state });
137148 state.logs = [];
138 state.parent.children.splice(i, 1);
139 state.parent = null;
140 detached = true;
141149 }
142150 function mutate() {
143151 const { parent } = state;
......@@ -199,6 +207,13 @@ function newNode(
199207 state.passive = value;
200208 mutate();
201209 },
210 get hidden() {
211 return state.hidden;
212 },
213 set hidden(value) {
214 state.hidden = value;
215 mutate();
216 },
202217 maxHeight: null,
203218 // get maxHeight() {
204219 // return state.maxHeight;
......@@ -284,7 +299,9 @@ function renderMainLine(state: Internal, now: number, depth: number) {
284299}
285300
286301function hasChildren(states: Internal): boolean {
287 return states.children.some((x) => !x.passive || hasChildren(x));
302 return states.children.some((x) =>
303 !x.hidden && (!x.passive || hasChildren(x))
304 );
288305}
289306
290307function renderChildren(state: Internal, now: number, depth: boolean[]) {
......@@ -292,7 +309,9 @@ function renderChildren(state: Internal, now: number, depth: boolean[]) {
292309 let truncated = 0;
293310 let out = "";
294311 let { children } = state;
295 children = children.filter((x) => !x.passive || hasChildren(x));
312 children = children.filter((x) =>
313 !x.hidden && (!x.passive || hasChildren(x))
314 );
296315 if (children.length === 0) return "";
297316 for (let i = 0, { length } = children; i < length; i += 1) {
298317 const child = UNWRAP(children[i]);
......@@ -376,7 +395,12 @@ export class Headless {
376395 if (msg === "draw") this.#rerender?.();
377396 else if (msg.type === "line") this.env.writeLine(msg.line);
378397 else if (msg.type === "end") {
379 if (msg.logs.length === 0) return;
398 const indexOfActive = this.active.indexOf(msg.state);
399 if (indexOfActive !== -1) this.active.splice(indexOfActive, 1);
400 if (msg.logs.length === 0) {
401 this.#rerender?.();
402 return;
403 }
380404 const { logs } = msg;
381405 const count = `${logs.length} line${logs.length === 1 ? "" : "s"}`;
382406 const header = `[${count} from ${msg.title}]`;
lib/log/stack.ts+7-4
......@@ -201,17 +201,20 @@ export function formatFrame(frame: Frame, colors: boolean): string {
201201 const left = text.slice(0, l);
202202 center = text.slice(l, r);
203203 const right = text.slice(r);
204 const irrelevant = internal || file?.includes("node_modules");
205 const fg = !irrelevant ? ansi.fgRed : "";
206 const bold = !irrelevant ? ansi.bold : ansi.dim + ansi.bold;
204207 out += (left
205 ? ansi.mergeStyles(ansi.fgRed + ansi.dim) + left +
206 ansi.mergeStyles(ansi.resetWeight + ansi.bold)
207 : ansi.mergeStyles(ansi.fgRed + ansi.bold)) +
208 ? ansi.mergeStyles(fg + ansi.dim) + left +
209 ansi.mergeStyles(ansi.resetWeight + bold)
210 : ansi.mergeStyles(fg + bold)) +
208211 center + (right
209212 ? ansi.mergeStyles(ansi.resetWeight + ansi.dim) +
210213 right
211214 : "") +
212215 ansi.mergeStyles(ansi.resetWeight + ansi.fgReset);
213216 } else {
214 out += ansi.fgRed + text + ansi.fgReset;
217 out += text;
215218 }
216219 } else {
217220 out += text;
lib/readme.changes.md+9
......@@ -1,5 +1,14 @@
11# notable changes in clover's typescript library
22
3## v2
4
5- top level readme now explains general usage patterns.
6- rework `log.Scope`, remove self call signature so that the `log` module
7 satisfies `log.Scope`, while adding `log` and `debug` for explicit debugging
8 logs. documentation prescribes doing `const console = log.scope("module");`.
9- add `stack.capture()`
10- add `string.escapeRegExp()`
11
312## v1
413
514- new library `assert`
lib/readme.md+48-12
......@@ -5,11 +5,13 @@ either browsers or node.js. originally, these were helper functions used to
55create my website, <https://paperclover.net>, but i started wanting them across
66projects and at work.
77
8<!--%npm%-->
9
10## contents
11
812all top-level namespaces stand alone as their own projects, and generally do not
913depend on each other. there are no external dependencies; code is easy to audit.
1014
11the public api is simply all of the code. an overview:
12
1315<!-- - `async` -->
1416<!-- - `error` - interop with the `unknown` type in error handlers -->
1517<!-- - `fs` - rebuilt api for `node:fs` -->
......@@ -18,16 +20,48 @@ the public api is simply all of the code. an overview:
1820<!-- - `subprocess` - rebuilt api for `node:child_process` -->
1921<!-- - `testing` -->
2022
21- `assert`
22- `log` - logging, terminal i/o, interactive widgets
23- `log/ansi` - cursor and color constants
24- `log/headless` - use `log` and `log/progress` without a tty
25- `log/progress` - progress bar, spinner, and tree
26- `log/stack` - stack trace parse and formatter
27- `packaging` - workflows used for `lib`'s publications
28- `queue` - priority queue with rescheduling
29- `string`
30- `ts` typescript types and general purpose code
23- `assert.ts`
24- `log.ts` - logging, terminal i/o, interactive widgets
25- `log/ansi.ts` - cursor and color constants
26- `log/headless.ts` - use `log` and `log/progress` without a tty
27- `log/progress.ts` - progress bar, spinner, and tree
28- `log/stack.ts` - stack trace parse and formatter
29- `packaging.ts` - workflows used for `lib`'s publications
30- `queue.ts` - priority queue with rescheduling
31- `string.ts`
32- `ts.ts` typescript types and general purpose code
33
34there is currently no hosted documentation site. please refer to the source
35files' comments.
36
37## usage
38
39unless otherwise noted, it is expected to import modules using a namespace
40import. function names are often concise to avoid repeating redundant words in
41source code.
42
43```ts
44import * as queue from "@clo/lib/queue.ts";
45
46// `run` in the context of `queue` conveys more information
47const result = await queue.run({ ... });
48```
49
50without "barrel" files, using multiple libraries is done by importing each
51library separately:
52
53```ts
54import * as log from "@clo/lib/log.ts";
55import * as progress from "@clo/lib/log/progress.ts";
56```
57
58these patterns developed initially when using a text editor without a language
59server, but i found this pattern to be nicer in general for organizing code. it
60also resulted in fewer lines spent on imports, and making it much easier to
61understand where code comes from. to this extent, all of the library modules are
62named as single words so they can be easily imported.
63
64## versioning
3165
3266these libraries are published as is, **without versioning or maintainance
3367guarantees**. every version is a breaking major release. please upgrade your
......@@ -36,6 +70,8 @@ individual modules may annotate their intended api and usage stability.
3670
3771keep in mind there are [known bugs and missing features] in this release.
3872
73## contributions
74
3975to contribute, please send bug reports or patches to <dev@paperclover.net>. new
4076features may not be accepted if they are complex and don't further my use cases.
4177
lib/readme.npm.md+5-45
......@@ -1,49 +1,9 @@
1# [clover's typescript library]
2
3`lib` is a collection of high-quality typescript libraries for things missing in
4either browsers or node.js. originally, these were helper functions used to
5create my website, <https://paperclover.net>, but i started wanting them across
6projects and at work.
7
81```sh
9# alias install "lib" using "@paperclover/lib"
2# alias install "@paperclover/lib" as "@clo/lib"
103npm install lib@npm:@paperclover/lib
114```
125
13all top-level namespaces stand alone as their own projects, and generally do not
14depend on each other. there are no external dependencies; code is easy to audit.
15
16the public api is simply all of the code. an overview:
17
18<!-- - `async` -->
19<!-- - `error` - interop with the `unknown` type in error handlers -->
20<!-- - `fs` - rebuilt api for `node:fs` -->
21<!-- - `lru` - least recently used cache -->
22<!-- - `mime` - trimmed database of mime types -->
23<!-- - `subprocess` - rebuilt api for `node:child_process` -->
24<!-- - `testing` -->
25
26- `assert`
27- `log` - logging, terminal i/o, interactive widgets
28- `log/ansi` - cursor and color constants
29- `log/headless` - use `log` and `log/progress` without a tty
30- `log/progress` - progress bar, spinner, and tree
31- `log/stack` - stack trace parse and formatter
32- `packaging` - workflows used for `lib`'s publications
33- `queue` - priority queue with rescheduling
34- `string`
35- `ts` typescript types and general purpose code
36
37these libraries are published as is, **without versioning or maintainance
38guarantees**. every version is a breaking major release. please upgrade your
39libraries with intent, considering api changes in [readme.changes.md].
40individual modules may annotate their intended api and usage stability.
41
42keep in mind there are [known bugs and missing features] in this release.
43
44to contribute, please send bug reports or patches to <dev@paperclover.net>. new
45features may not be accepted if they are complex and don't further my use cases.
46
47[readme.changes.md]: ./readme.changes.md
48[known bugs and missing features]: https://git.paperclover.net/clo/sitegen/issues?state=open&labels=4
49[clover's typescript library]: https://git.paperclover.net/clo/sitegen/src/branch/master/lib
6the distribution on npm is transpiled with JSDoc annotations instead of
7publishing the source code directly. this is done because
8[Node.js does not plan to support TypeScript within `node_modules`](https://github.com/nodejs/node/issues/57215),
9which is extremely sad to see.
lib/string.ts+5
......@@ -1,3 +1,8 @@
1/** Escape a value for use in a regular expression. */
2export function escapeRegExp(unsafeText: string) {
3 return unsafeText.replace(/[\$\\]/g, "\\$&");
4}
5
16// TODO: this function is not benchmarked
27export function countNewlines(str: string): number {
38 let count = 0;
package-lock.json+505-465
......@@ -5,45 +5,31 @@
55 "packages": {
66 "": {
77 "dependencies": {
8 "@hono/node-server": "^1.14.3",
9 "@mdx-js/mdx": "^3.1.0",
10 "@paperclover/console": "git+https://git.paperclover.net/clo/console.git",
11 "blurhash": "^2.0.5",
12 "codemirror": "^6.0.1",
13 "devalue": "^5.1.1",
14 "esbuild": "^0.25.5",
15 "hls.js": "^1.6.5",
16 "hono": "^4.7.11",
8 "@hono/node-server": "1.14.3",
9 "@mdx-js/mdx": "3.1.0",
10 "codemirror": "6.0.1",
11 "dashjs": "^5.0.3",
12 "devalue": "^5.3.2",
13 "esbuild": "0.25.5",
14 "hls.js": "1.6.5",
15 "hono": "4.9.9",
1716 "lib": "file:lib",
18 "marko": "^6.0.20",
19 "msgpackr": "^1.11.5",
20 "puppeteer": "^24.10.1",
21 "sharp": "^0.34.2",
22 "source-map-support": "^0.5.21",
23 "ts-to-jsdoc": "^2.4.0",
24 "unique-names-generator": "^4.7.1",
25 "vscode-oniguruma": "^2.0.1",
26 "vscode-textmate": "^9.2.0"
17 "marko": "6.0.82",
18 "msgpackr": "1.11.5",
19 "puppeteer": "24.23.0",
20 "sharp": "0.34.2",
21 "source-map-support": "0.5.21",
22 "ts-to-jsdoc": "2.4.0",
23 "unique-names-generator": "4.7.1",
24 "vscode-oniguruma": "2.0.1",
25 "vscode-textmate": "9.2.0"
2726 },
2827 "devDependencies": {
29 "@types/node": "^24.0.10",
30 "typescript": "^5.8.3"
28 "@types/node": "24.0.10",
29 "typescript": "5.8.3"
3130 }
3231 },
3332 "lib": {},
34 "node_modules/@ampproject/remapping": {
35 "version": "2.3.0",
36 "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
37 "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
38 "license": "Apache-2.0",
39 "dependencies": {
40 "@jridgewell/gen-mapping": "^0.3.5",
41 "@jridgewell/trace-mapping": "^0.3.24"
42 },
43 "engines": {
44 "node": ">=6.0.0"
45 }
46 },
4733 "node_modules/@babel/code-frame": {
4834 "version": "7.27.1",
4935 "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
......@@ -59,30 +45,30 @@
5945 }
6046 },
6147 "node_modules/@babel/compat-data": {
62 "version": "7.28.0",
63 "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
64 "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
48 "version": "7.28.4",
49 "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz",
50 "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==",
6551 "license": "MIT",
6652 "engines": {
6753 "node": ">=6.9.0"
6854 }
6955 },
7056 "node_modules/@babel/core": {
71 "version": "7.28.0",
72 "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
73 "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
57 "version": "7.28.4",
58 "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
59 "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
7460 "license": "MIT",
7561 "dependencies": {
76 "@ampproject/remapping": "^2.2.0",
7762 "@babel/code-frame": "^7.27.1",
78 "@babel/generator": "^7.28.0",
63 "@babel/generator": "^7.28.3",
7964 "@babel/helper-compilation-targets": "^7.27.2",
80 "@babel/helper-module-transforms": "^7.27.3",
81 "@babel/helpers": "^7.27.6",
82 "@babel/parser": "^7.28.0",
65 "@babel/helper-module-transforms": "^7.28.3",
66 "@babel/helpers": "^7.28.4",
67 "@babel/parser": "^7.28.4",
8368 "@babel/template": "^7.27.2",
84 "@babel/traverse": "^7.28.0",
85 "@babel/types": "^7.28.0",
69 "@babel/traverse": "^7.28.4",
70 "@babel/types": "^7.28.4",
71 "@jridgewell/remapping": "^2.3.5",
8672 "convert-source-map": "^2.0.0",
8773 "debug": "^4.1.0",
8874 "gensync": "^1.0.0-beta.2",
......@@ -98,13 +84,13 @@
9884 }
9985 },
10086 "node_modules/@babel/generator": {
101 "version": "7.28.0",
102 "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
103 "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
87 "version": "7.28.3",
88 "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
89 "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
10490 "license": "MIT",
10591 "dependencies": {
106 "@babel/parser": "^7.28.0",
107 "@babel/types": "^7.28.0",
92 "@babel/parser": "^7.28.3",
93 "@babel/types": "^7.28.2",
10894 "@jridgewell/gen-mapping": "^0.3.12",
10995 "@jridgewell/trace-mapping": "^0.3.28",
11096 "jsesc": "^3.0.2"
......@@ -142,17 +128,17 @@
142128 }
143129 },
144130 "node_modules/@babel/helper-create-class-features-plugin": {
145 "version": "7.27.1",
146 "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz",
147 "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==",
131 "version": "7.28.3",
132 "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz",
133 "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==",
148134 "license": "MIT",
149135 "dependencies": {
150 "@babel/helper-annotate-as-pure": "^7.27.1",
136 "@babel/helper-annotate-as-pure": "^7.27.3",
151137 "@babel/helper-member-expression-to-functions": "^7.27.1",
152138 "@babel/helper-optimise-call-expression": "^7.27.1",
153139 "@babel/helper-replace-supers": "^7.27.1",
154140 "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
155 "@babel/traverse": "^7.27.1",
141 "@babel/traverse": "^7.28.3",
156142 "semver": "^6.3.1"
157143 },
158144 "engines": {
......@@ -198,14 +184,14 @@
198184 }
199185 },
200186 "node_modules/@babel/helper-module-transforms": {
201 "version": "7.27.3",
202 "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
203 "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
187 "version": "7.28.3",
188 "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
189 "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
204190 "license": "MIT",
205191 "dependencies": {
206192 "@babel/helper-module-imports": "^7.27.1",
207193 "@babel/helper-validator-identifier": "^7.27.1",
208 "@babel/traverse": "^7.27.3"
194 "@babel/traverse": "^7.28.3"
209195 },
210196 "engines": {
211197 "node": ">=6.9.0"
......@@ -293,25 +279,25 @@
293279 }
294280 },
295281 "node_modules/@babel/helpers": {
296 "version": "7.27.6",
297 "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
298 "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
282 "version": "7.28.4",
283 "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
284 "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
299285 "license": "MIT",
300286 "dependencies": {
301287 "@babel/template": "^7.27.2",
302 "@babel/types": "^7.27.6"
288 "@babel/types": "^7.28.4"
303289 },
304290 "engines": {
305291 "node": ">=6.9.0"
306292 }
307293 },
308294 "node_modules/@babel/parser": {
309 "version": "7.28.0",
310 "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
311 "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
295 "version": "7.28.4",
296 "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
297 "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
312298 "license": "MIT",
313299 "dependencies": {
314 "@babel/types": "^7.28.0"
300 "@babel/types": "^7.28.4"
315301 },
316302 "bin": {
317303 "parser": "bin/babel-parser.js"
......@@ -371,9 +357,9 @@
371357 }
372358 },
373359 "node_modules/@babel/runtime": {
374 "version": "7.27.6",
375 "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
376 "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
360 "version": "7.28.4",
361 "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
362 "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
377363 "license": "MIT",
378364 "engines": {
379365 "node": ">=6.9.0"
......@@ -394,17 +380,17 @@
394380 }
395381 },
396382 "node_modules/@babel/traverse": {
397 "version": "7.28.0",
398 "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
399 "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
383 "version": "7.28.4",
384 "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
385 "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
400386 "license": "MIT",
401387 "dependencies": {
402388 "@babel/code-frame": "^7.27.1",
403 "@babel/generator": "^7.28.0",
389 "@babel/generator": "^7.28.3",
404390 "@babel/helper-globals": "^7.28.0",
405 "@babel/parser": "^7.28.0",
391 "@babel/parser": "^7.28.4",
406392 "@babel/template": "^7.27.2",
407 "@babel/types": "^7.28.0",
393 "@babel/types": "^7.28.4",
408394 "debug": "^4.3.1"
409395 },
410396 "engines": {
......@@ -412,9 +398,9 @@
412398 }
413399 },
414400 "node_modules/@babel/types": {
415 "version": "7.28.0",
416 "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz",
417 "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==",
401 "version": "7.28.4",
402 "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
403 "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
418404 "license": "MIT",
419405 "dependencies": {
420406 "@babel/helper-string-parser": "^7.27.1",
......@@ -516,9 +502,9 @@
516502 }
517503 },
518504 "node_modules/@esbuild/aix-ppc64": {
519 "version": "0.25.6",
520 "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
521 "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==",
505 "version": "0.25.5",
506 "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
507 "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
522508 "cpu": [
523509 "ppc64"
524510 ],
......@@ -532,9 +518,9 @@
532518 }
533519 },
534520 "node_modules/@esbuild/android-arm": {
535 "version": "0.25.6",
536 "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz",
537 "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==",
521 "version": "0.25.5",
522 "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
523 "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
538524 "cpu": [
539525 "arm"
540526 ],
......@@ -548,9 +534,9 @@
548534 }
549535 },
550536 "node_modules/@esbuild/android-arm64": {
551 "version": "0.25.6",
552 "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz",
553 "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==",
537 "version": "0.25.5",
538 "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
539 "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
554540 "cpu": [
555541 "arm64"
556542 ],
......@@ -564,9 +550,9 @@
564550 }
565551 },
566552 "node_modules/@esbuild/android-x64": {
567 "version": "0.25.6",
568 "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz",
569 "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==",
553 "version": "0.25.5",
554 "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
555 "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
570556 "cpu": [
571557 "x64"
572558 ],
......@@ -580,9 +566,9 @@
580566 }
581567 },
582568 "node_modules/@esbuild/darwin-arm64": {
583 "version": "0.25.6",
584 "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz",
585 "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==",
569 "version": "0.25.5",
570 "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
571 "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
586572 "cpu": [
587573 "arm64"
588574 ],
......@@ -596,9 +582,9 @@
596582 }
597583 },
598584 "node_modules/@esbuild/darwin-x64": {
599 "version": "0.25.6",
600 "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz",
601 "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==",
585 "version": "0.25.5",
586 "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
587 "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
602588 "cpu": [
603589 "x64"
604590 ],
......@@ -612,9 +598,9 @@
612598 }
613599 },
614600 "node_modules/@esbuild/freebsd-arm64": {
615 "version": "0.25.6",
616 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz",
617 "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==",
601 "version": "0.25.5",
602 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
603 "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
618604 "cpu": [
619605 "arm64"
620606 ],
......@@ -628,9 +614,9 @@
628614 }
629615 },
630616 "node_modules/@esbuild/freebsd-x64": {
631 "version": "0.25.6",
632 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz",
633 "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==",
617 "version": "0.25.5",
618 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
619 "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
634620 "cpu": [
635621 "x64"
636622 ],
......@@ -644,9 +630,9 @@
644630 }
645631 },
646632 "node_modules/@esbuild/linux-arm": {
647 "version": "0.25.6",
648 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz",
649 "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==",
633 "version": "0.25.5",
634 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
635 "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
650636 "cpu": [
651637 "arm"
652638 ],
......@@ -660,9 +646,9 @@
660646 }
661647 },
662648 "node_modules/@esbuild/linux-arm64": {
663 "version": "0.25.6",
664 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz",
665 "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==",
649 "version": "0.25.5",
650 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
651 "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
666652 "cpu": [
667653 "arm64"
668654 ],
......@@ -676,9 +662,9 @@
676662 }
677663 },
678664 "node_modules/@esbuild/linux-ia32": {
679 "version": "0.25.6",
680 "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz",
681 "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==",
665 "version": "0.25.5",
666 "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
667 "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
682668 "cpu": [
683669 "ia32"
684670 ],
......@@ -692,9 +678,9 @@
692678 }
693679 },
694680 "node_modules/@esbuild/linux-loong64": {
695 "version": "0.25.6",
696 "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz",
697 "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==",
681 "version": "0.25.5",
682 "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
683 "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
698684 "cpu": [
699685 "loong64"
700686 ],
......@@ -708,9 +694,9 @@
708694 }
709695 },
710696 "node_modules/@esbuild/linux-mips64el": {
711 "version": "0.25.6",
712 "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz",
713 "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==",
697 "version": "0.25.5",
698 "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
699 "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
714700 "cpu": [
715701 "mips64el"
716702 ],
......@@ -724,9 +710,9 @@
724710 }
725711 },
726712 "node_modules/@esbuild/linux-ppc64": {
727 "version": "0.25.6",
728 "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz",
729 "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==",
713 "version": "0.25.5",
714 "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
715 "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
730716 "cpu": [
731717 "ppc64"
732718 ],
......@@ -740,9 +726,9 @@
740726 }
741727 },
742728 "node_modules/@esbuild/linux-riscv64": {
743 "version": "0.25.6",
744 "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz",
745 "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==",
729 "version": "0.25.5",
730 "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
731 "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
746732 "cpu": [
747733 "riscv64"
748734 ],
......@@ -756,9 +742,9 @@
756742 }
757743 },
758744 "node_modules/@esbuild/linux-s390x": {
759 "version": "0.25.6",
760 "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz",
761 "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==",
745 "version": "0.25.5",
746 "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
747 "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
762748 "cpu": [
763749 "s390x"
764750 ],
......@@ -772,9 +758,9 @@
772758 }
773759 },
774760 "node_modules/@esbuild/linux-x64": {
775 "version": "0.25.6",
776 "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz",
777 "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==",
761 "version": "0.25.5",
762 "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
763 "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
778764 "cpu": [
779765 "x64"
780766 ],
......@@ -788,9 +774,9 @@
788774 }
789775 },
790776 "node_modules/@esbuild/netbsd-arm64": {
791 "version": "0.25.6",
792 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz",
793 "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==",
777 "version": "0.25.5",
778 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
779 "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
794780 "cpu": [
795781 "arm64"
796782 ],
......@@ -804,9 +790,9 @@
804790 }
805791 },
806792 "node_modules/@esbuild/netbsd-x64": {
807 "version": "0.25.6",
808 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz",
809 "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==",
793 "version": "0.25.5",
794 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
795 "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
810796 "cpu": [
811797 "x64"
812798 ],
......@@ -820,9 +806,9 @@
820806 }
821807 },
822808 "node_modules/@esbuild/openbsd-arm64": {
823 "version": "0.25.6",
824 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz",
825 "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==",
809 "version": "0.25.5",
810 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
811 "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
826812 "cpu": [
827813 "arm64"
828814 ],
......@@ -836,9 +822,9 @@
836822 }
837823 },
838824 "node_modules/@esbuild/openbsd-x64": {
839 "version": "0.25.6",
840 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz",
841 "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==",
825 "version": "0.25.5",
826 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
827 "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
842828 "cpu": [
843829 "x64"
844830 ],
......@@ -851,26 +837,10 @@
851837 "node": ">=18"
852838 }
853839 },
854 "node_modules/@esbuild/openharmony-arm64": {
855 "version": "0.25.6",
856 "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz",
857 "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==",
858 "cpu": [
859 "arm64"
860 ],
861 "license": "MIT",
862 "optional": true,
863 "os": [
864 "openharmony"
865 ],
866 "engines": {
867 "node": ">=18"
868 }
869 },
870840 "node_modules/@esbuild/sunos-x64": {
871 "version": "0.25.6",
872 "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz",
873 "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==",
841 "version": "0.25.5",
842 "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
843 "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
874844 "cpu": [
875845 "x64"
876846 ],
......@@ -884,9 +854,9 @@
884854 }
885855 },
886856 "node_modules/@esbuild/win32-arm64": {
887 "version": "0.25.6",
888 "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz",
889 "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==",
857 "version": "0.25.5",
858 "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
859 "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
890860 "cpu": [
891861 "arm64"
892862 ],
......@@ -900,9 +870,9 @@
900870 }
901871 },
902872 "node_modules/@esbuild/win32-ia32": {
903 "version": "0.25.6",
904 "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz",
905 "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==",
873 "version": "0.25.5",
874 "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
875 "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
906876 "cpu": [
907877 "ia32"
908878 ],
......@@ -916,9 +886,9 @@
916886 }
917887 },
918888 "node_modules/@esbuild/win32-x64": {
919 "version": "0.25.6",
920 "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz",
921 "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==",
889 "version": "0.25.5",
890 "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
891 "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
922892 "cpu": [
923893 "x64"
924894 ],
......@@ -932,9 +902,9 @@
932902 }
933903 },
934904 "node_modules/@hono/node-server": {
935 "version": "1.15.0",
936 "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.15.0.tgz",
937 "integrity": "sha512-MjmK4l5N4dQpZ9OSWN0tCj7ejuc7WvuWMzSKtc89bnknJykAeHxzRigXBTYZk85H6Awrii6RM59iUiUluApu2A==",
905 "version": "1.14.3",
906 "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.14.3.tgz",
907 "integrity": "sha512-KuDMwwghtFYSmIpr4WrKs1VpelTrptvJ+6x6mbUcZnFcc213cumTF5BdqfHyW93B19TNI4Vaev14vOI2a0Ie3w==",
938908 "license": "MIT",
939909 "engines": {
940910 "node": ">=18.14.1"
......@@ -1340,15 +1310,25 @@
13401310 }
13411311 },
13421312 "node_modules/@jridgewell/gen-mapping": {
1343 "version": "0.3.12",
1344 "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
1345 "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
1313 "version": "0.3.13",
1314 "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
1315 "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
13461316 "license": "MIT",
13471317 "dependencies": {
13481318 "@jridgewell/sourcemap-codec": "^1.5.0",
13491319 "@jridgewell/trace-mapping": "^0.3.24"
13501320 }
13511321 },
1322 "node_modules/@jridgewell/remapping": {
1323 "version": "2.3.5",
1324 "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
1325 "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
1326 "license": "MIT",
1327 "dependencies": {
1328 "@jridgewell/gen-mapping": "^0.3.5",
1329 "@jridgewell/trace-mapping": "^0.3.24"
1330 }
1331 },
13521332 "node_modules/@jridgewell/resolve-uri": {
13531333 "version": "3.1.2",
13541334 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
......@@ -1365,9 +1345,9 @@
13651345 "license": "MIT"
13661346 },
13671347 "node_modules/@jridgewell/trace-mapping": {
1368 "version": "0.3.29",
1369 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
1370 "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
1348 "version": "0.3.31",
1349 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
1350 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
13711351 "license": "MIT",
13721352 "dependencies": {
13731353 "@jridgewell/resolve-uri": "^3.1.0",
......@@ -1414,25 +1394,25 @@
14141394 "license": "MIT"
14151395 },
14161396 "node_modules/@marko/compiler": {
1417 "version": "5.39.28",
1418 "resolved": "https://registry.npmjs.org/@marko/compiler/-/compiler-5.39.28.tgz",
1419 "integrity": "sha512-jO/GGNWwyHlCE/fAhiLc1+Xc3apU73wfmjqBFiQmFHyAT2KZRyKJbPz7aVs8uutrPY8Xks9ZCXiXOk0RgvUlvg==",
1420 "license": "MIT",
1421 "dependencies": {
1422 "@babel/code-frame": "^7.26.2",
1423 "@babel/core": "^7.26.10",
1424 "@babel/generator": "^7.27.0",
1425 "@babel/parser": "^7.27.0",
1426 "@babel/plugin-syntax-typescript": "^7.25.9",
1427 "@babel/plugin-transform-modules-commonjs": "^7.26.3",
1428 "@babel/plugin-transform-typescript": "^7.27.0",
1429 "@babel/runtime": "^7.27.0",
1430 "@babel/traverse": "^7.27.0",
1431 "@babel/types": "^7.27.0",
1397 "version": "5.39.40",
1398 "resolved": "https://registry.npmjs.org/@marko/compiler/-/compiler-5.39.40.tgz",
1399 "integrity": "sha512-gQSdfJ5XOy01DtP7ULiqI1onp7Cix1QvnjM0KbfR2HMrWNI14V8V/2JguFAm3BzKEMZmmgvRLk0rvBzSm9FP6Q==",
1400 "license": "MIT",
1401 "dependencies": {
1402 "@babel/code-frame": "^7.27.1",
1403 "@babel/core": "^7.28.0",
1404 "@babel/generator": "^7.28.0",
1405 "@babel/parser": "^7.28.0",
1406 "@babel/plugin-syntax-typescript": "^7.27.1",
1407 "@babel/plugin-transform-modules-commonjs": "^7.27.1",
1408 "@babel/plugin-transform-typescript": "^7.28.0",
1409 "@babel/runtime": "^7.28.2",
1410 "@babel/traverse": "^7.28.0",
1411 "@babel/types": "^7.28.2",
14321412 "@luxass/strip-json-comments": "^1.4.0",
14331413 "complain": "^1.6.1",
14341414 "he": "^1.2.0",
1435 "htmljs-parser": "^5.5.4",
1415 "htmljs-parser": "^5.7.3",
14361416 "jsesc": "^3.1.0",
14371417 "kleur": "^4.1.5",
14381418 "lasso-package-root": "^1.0.1",
......@@ -1596,26 +1576,18 @@
15961576 "node": ">= 8"
15971577 }
15981578 },
1599 "node_modules/@paperclover/console": {
1600 "resolved": "git+https://git.paperclover.net/clo/console.git#1a6ac2b79fdd8a21a1c57d25723975872bc07e3e",
1601 "dependencies": {
1602 "ansi-escapes": "^7.0.0",
1603 "chalk": "^5.4.1",
1604 "strip-ansi": "^7.1.0"
1605 }
1606 },
16071579 "node_modules/@puppeteer/browsers": {
1608 "version": "2.10.5",
1609 "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz",
1610 "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==",
1580 "version": "2.10.10",
1581 "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.10.tgz",
1582 "integrity": "sha512-3ZG500+ZeLql8rE0hjfhkycJjDj0pI/btEh3L9IkWUYcOrgP0xCNRq3HbtbqOPbvDhFaAWD88pDFtlLv8ns8gA==",
16111583 "license": "Apache-2.0",
16121584 "dependencies": {
1613 "debug": "^4.4.1",
1585 "debug": "^4.4.3",
16141586 "extract-zip": "^2.0.1",
16151587 "progress": "^2.0.3",
16161588 "proxy-agent": "^6.5.0",
16171589 "semver": "^7.7.2",
1618 "tar-fs": "^3.0.8",
1590 "tar-fs": "^3.1.0",
16191591 "yargs": "^17.7.2"
16201592 },
16211593 "bin": {
......@@ -1637,6 +1609,12 @@
16371609 "node": ">=10"
16381610 }
16391611 },
1612 "node_modules/@svta/common-media-library": {
1613 "version": "0.12.4",
1614 "resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.12.4.tgz",
1615 "integrity": "sha512-9EuOoaNmz7JrfGwjsrD9SxF9otU5TNMnbLu1yU4BeLK0W5cDxVXXR58Z89q9u2AnHjIctscjMTYdlqQ1gojTuw==",
1616 "license": "Apache-2.0"
1617 },
16401618 "node_modules/@tootallnate/quickjs-emscripten": {
16411619 "version": "0.23.0",
16421620 "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
......@@ -1710,9 +1688,9 @@
17101688 "license": "MIT"
17111689 },
17121690 "node_modules/@types/node": {
1713 "version": "24.0.12",
1714 "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.12.tgz",
1715 "integrity": "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==",
1691 "version": "24.0.10",
1692 "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
1693 "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
17161694 "devOptional": true,
17171695 "license": "MIT",
17181696 "dependencies": {
......@@ -1771,31 +1749,13 @@
17711749 "node": ">= 14"
17721750 }
17731751 },
1774 "node_modules/ansi-escapes": {
1775 "version": "7.0.0",
1776 "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
1777 "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==",
1778 "license": "MIT",
1779 "dependencies": {
1780 "environment": "^1.0.0"
1781 },
1782 "engines": {
1783 "node": ">=18"
1784 },
1785 "funding": {
1786 "url": "https://github.com/sponsors/sindresorhus"
1787 }
1788 },
17891752 "node_modules/ansi-regex": {
1790 "version": "6.1.0",
1791 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
1792 "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
1753 "version": "5.0.1",
1754 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
1755 "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
17931756 "license": "MIT",
17941757 "engines": {
1795 "node": ">=12"
1796 },
1797 "funding": {
1798 "url": "https://github.com/chalk/ansi-regex?sponsor=1"
1758 "node": ">=8"
17991759 }
18001760 },
18011761 "node_modules/ansi-styles": {
......@@ -1847,10 +1807,18 @@
18471807 }
18481808 },
18491809 "node_modules/b4a": {
1850 "version": "1.6.7",
1851 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
1852 "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
1853 "license": "Apache-2.0"
1810 "version": "1.7.3",
1811 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz",
1812 "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==",
1813 "license": "Apache-2.0",
1814 "peerDependencies": {
1815 "react-native-b4a": "*"
1816 },
1817 "peerDependenciesMeta": {
1818 "react-native-b4a": {
1819 "optional": true
1820 }
1821 }
18541822 },
18551823 "node_modules/bail": {
18561824 "version": "2.0.2",
......@@ -1869,22 +1837,23 @@
18691837 "license": "MIT"
18701838 },
18711839 "node_modules/bare-events": {
1872 "version": "2.6.0",
1873 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.0.tgz",
1874 "integrity": "sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==",
1875 "license": "Apache-2.0",
1876 "optional": true
1840 "version": "2.7.0",
1841 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.7.0.tgz",
1842 "integrity": "sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==",
1843 "license": "Apache-2.0"
18771844 },
18781845 "node_modules/bare-fs": {
1879 "version": "4.1.6",
1880 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.6.tgz",
1881 "integrity": "sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==",
1846 "version": "4.4.5",
1847 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.5.tgz",
1848 "integrity": "sha512-TCtu93KGLu6/aiGWzMr12TmSRS6nKdfhAnzTQRbXoSWxkbb9eRd53jQ51jG7g1gYjjtto3hbBrrhzg6djcgiKg==",
18821849 "license": "Apache-2.0",
18831850 "optional": true,
18841851 "dependencies": {
18851852 "bare-events": "^2.5.4",
18861853 "bare-path": "^3.0.0",
1887 "bare-stream": "^2.6.4"
1854 "bare-stream": "^2.6.4",
1855 "bare-url": "^2.2.2",
1856 "fast-fifo": "^1.3.2"
18881857 },
18891858 "engines": {
18901859 "bare": ">=1.16.0"
......@@ -1899,9 +1868,9 @@
18991868 }
19001869 },
19011870 "node_modules/bare-os": {
1902 "version": "3.6.1",
1903 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz",
1904 "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",
1871 "version": "3.6.2",
1872 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
1873 "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
19051874 "license": "Apache-2.0",
19061875 "optional": true,
19071876 "engines": {
......@@ -1919,9 +1888,9 @@
19191888 }
19201889 },
19211890 "node_modules/bare-stream": {
1922 "version": "2.6.5",
1923 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",
1924 "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
1891 "version": "2.7.0",
1892 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
1893 "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
19251894 "license": "Apache-2.0",
19261895 "optional": true,
19271896 "dependencies": {
......@@ -1940,6 +1909,25 @@
19401909 }
19411910 }
19421911 },
1912 "node_modules/bare-url": {
1913 "version": "2.2.2",
1914 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.2.2.tgz",
1915 "integrity": "sha512-g+ueNGKkrjMazDG3elZO1pNs3HY5+mMmOet1jtKyhOaCnkLzitxf26z7hoAEkDNgdNmnc1KIlt/dw6Po6xZMpA==",
1916 "license": "Apache-2.0",
1917 "optional": true,
1918 "dependencies": {
1919 "bare-path": "^3.0.0"
1920 }
1921 },
1922 "node_modules/baseline-browser-mapping": {
1923 "version": "2.8.10",
1924 "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.10.tgz",
1925 "integrity": "sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==",
1926 "license": "Apache-2.0",
1927 "bin": {
1928 "baseline-browser-mapping": "dist/cli.js"
1929 }
1930 },
19431931 "node_modules/basic-ftp": {
19441932 "version": "5.0.5",
19451933 "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
......@@ -1949,11 +1937,44 @@
19491937 "node": ">=10.0.0"
19501938 }
19511939 },
1952 "node_modules/blurhash": {
1953 "version": "2.0.5",
1954 "resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz",
1955 "integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==",
1956 "license": "MIT"
1940 "node_modules/bcp-47": {
1941 "version": "2.1.0",
1942 "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz",
1943 "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==",
1944 "license": "MIT",
1945 "dependencies": {
1946 "is-alphabetical": "^2.0.0",
1947 "is-alphanumerical": "^2.0.0",
1948 "is-decimal": "^2.0.0"
1949 },
1950 "funding": {
1951 "type": "github",
1952 "url": "https://github.com/sponsors/wooorm"
1953 }
1954 },
1955 "node_modules/bcp-47-match": {
1956 "version": "2.0.3",
1957 "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz",
1958 "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==",
1959 "license": "MIT",
1960 "funding": {
1961 "type": "github",
1962 "url": "https://github.com/sponsors/wooorm"
1963 }
1964 },
1965 "node_modules/bcp-47-normalize": {
1966 "version": "2.3.0",
1967 "resolved": "https://registry.npmjs.org/bcp-47-normalize/-/bcp-47-normalize-2.3.0.tgz",
1968 "integrity": "sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==",
1969 "license": "MIT",
1970 "dependencies": {
1971 "bcp-47": "^2.0.0",
1972 "bcp-47-match": "^2.0.0"
1973 },
1974 "funding": {
1975 "type": "github",
1976 "url": "https://github.com/sponsors/wooorm"
1977 }
19571978 },
19581979 "node_modules/brace-expansion": {
19591980 "version": "2.0.2",
......@@ -1977,9 +1998,9 @@
19771998 }
19781999 },
19792000 "node_modules/browserslist": {
1980 "version": "4.25.1",
1981 "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
1982 "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
2001 "version": "4.26.3",
2002 "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
2003 "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
19832004 "funding": [
19842005 {
19852006 "type": "opencollective",
......@@ -1996,9 +2017,10 @@
19962017 ],
19972018 "license": "MIT",
19982019 "dependencies": {
1999 "caniuse-lite": "^1.0.30001726",
2000 "electron-to-chromium": "^1.5.173",
2001 "node-releases": "^2.0.19",
2020 "baseline-browser-mapping": "^2.8.9",
2021 "caniuse-lite": "^1.0.30001746",
2022 "electron-to-chromium": "^1.5.227",
2023 "node-releases": "^2.0.21",
20022024 "update-browserslist-db": "^1.1.3"
20032025 },
20042026 "bin": {
......@@ -2033,9 +2055,9 @@
20332055 }
20342056 },
20352057 "node_modules/caniuse-lite": {
2036 "version": "1.0.30001727",
2037 "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
2038 "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
2058 "version": "1.0.30001747",
2059 "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001747.tgz",
2060 "integrity": "sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg==",
20392061 "funding": [
20402062 {
20412063 "type": "opencollective",
......@@ -2062,18 +2084,6 @@
20622084 "url": "https://github.com/sponsors/wooorm"
20632085 }
20642086 },
2065 "node_modules/chalk": {
2066 "version": "5.4.1",
2067 "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
2068 "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
2069 "license": "MIT",
2070 "engines": {
2071 "node": "^12.17.0 || ^14.13 || >=16.0.0"
2072 },
2073 "funding": {
2074 "url": "https://github.com/chalk/chalk?sponsor=1"
2075 }
2076 },
20772087 "node_modules/character-entities": {
20782088 "version": "2.0.2",
20792089 "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
......@@ -2115,9 +2125,9 @@
21152125 }
21162126 },
21172127 "node_modules/chromium-bidi": {
2118 "version": "5.1.0",
2119 "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz",
2120 "integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==",
2128 "version": "9.1.0",
2129 "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-9.1.0.tgz",
2130 "integrity": "sha512-rlUzQ4WzIAWdIbY/viPShhZU2n21CxDUgazXVbw4Hu1MwaeUSEksSeM6DqPgpRjCLXRk702AVRxJxoOz0dw4OA==",
21212131 "license": "Apache-2.0",
21222132 "dependencies": {
21232133 "mitt": "^3.0.1",
......@@ -2141,37 +2151,22 @@
21412151 "node": ">=12"
21422152 }
21432153 },
2144 "node_modules/cliui/node_modules/ansi-regex": {
2145 "version": "5.0.1",
2146 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
2147 "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
2148 "license": "MIT",
2149 "engines": {
2150 "node": ">=8"
2151 }
2152 },
2153 "node_modules/cliui/node_modules/strip-ansi": {
2154 "version": "6.0.1",
2155 "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
2156 "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
2157 "license": "MIT",
2158 "dependencies": {
2159 "ansi-regex": "^5.0.1"
2160 },
2161 "engines": {
2162 "node": ">=8"
2163 }
2164 },
21652154 "node_modules/code-block-writer": {
21662155 "version": "13.0.3",
21672156 "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
21682157 "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==",
21692158 "license": "MIT"
21702159 },
2160 "node_modules/codem-isoboxer": {
2161 "version": "0.3.10",
2162 "resolved": "https://registry.npmjs.org/codem-isoboxer/-/codem-isoboxer-0.3.10.tgz",
2163 "integrity": "sha512-eNk3TRV+xQMJ1PEj0FQGY8KD4m0GPxT487XJ+Iftm7mVa9WpPFDMWqPt+46buiP5j5Wzqe5oMIhqBcAeKfygSA==",
2164 "license": "MIT"
2165 },
21712166 "node_modules/codemirror": {
2172 "version": "6.0.2",
2173 "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
2174 "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
2167 "version": "6.0.1",
2168 "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
2169 "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
21752170 "license": "MIT",
21762171 "dependencies": {
21772172 "@codemirror/autocomplete": "^6.0.0",
......@@ -2297,6 +2292,24 @@
22972292 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
22982293 "license": "MIT"
22992294 },
2295 "node_modules/dashjs": {
2296 "version": "5.0.3",
2297 "resolved": "https://registry.npmjs.org/dashjs/-/dashjs-5.0.3.tgz",
2298 "integrity": "sha512-TXndNnCUjFjF2nYBxDVba+hWRpVkadkQ8flLp7kHkem+5+wZTfRShJCnVkPUosmjS0YPE9fVNLbYPJxHBeQZvA==",
2299 "license": "BSD-3-Clause",
2300 "dependencies": {
2301 "@svta/common-media-library": "^0.12.4",
2302 "bcp-47-match": "^2.0.3",
2303 "bcp-47-normalize": "^2.3.0",
2304 "codem-isoboxer": "0.3.10",
2305 "fast-deep-equal": "3.1.3",
2306 "html-entities": "^2.5.2",
2307 "imsc": "^1.1.5",
2308 "localforage": "^1.10.0",
2309 "path-browserify": "^1.0.1",
2310 "ua-parser-js": "^1.0.37"
2311 }
2312 },
23002313 "node_modules/data-uri-to-buffer": {
23012314 "version": "6.0.2",
23022315 "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
......@@ -2307,9 +2320,9 @@
23072320 }
23082321 },
23092322 "node_modules/debug": {
2310 "version": "4.4.1",
2311 "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
2312 "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
2323 "version": "4.4.3",
2324 "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
2325 "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
23132326 "license": "MIT",
23142327 "dependencies": {
23152328 "ms": "^2.1.3"
......@@ -2388,15 +2401,15 @@
23882401 }
23892402 },
23902403 "node_modules/devtools-protocol": {
2391 "version": "0.0.1464554",
2392 "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz",
2393 "integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==",
2404 "version": "0.0.1508733",
2405 "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz",
2406 "integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==",
23942407 "license": "BSD-3-Clause"
23952408 },
23962409 "node_modules/electron-to-chromium": {
2397 "version": "1.5.181",
2398 "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.181.tgz",
2399 "integrity": "sha512-+ISMj8OIQ+0qEeDj14Rt8WwcTOiqHyAB+5bnK1K7xNNLjBJ4hRCQfUkw8RWtcLbfBzDwc15ZnKH0c7SNOfwiyA==",
2410 "version": "1.5.229",
2411 "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.229.tgz",
2412 "integrity": "sha512-cwhDcZKGcT/rEthLRJ9eBlMDkh1sorgsuk+6dpsehV0g9CABsIqBxU4rLRjG+d/U6pYU1s37A4lSKrVc5lSQYg==",
24002413 "license": "ISC"
24012414 },
24022415 "node_modules/emoji-regex": {
......@@ -2423,18 +2436,6 @@
24232436 "node": ">=6"
24242437 }
24252438 },
2426 "node_modules/environment": {
2427 "version": "1.1.0",
2428 "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
2429 "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
2430 "license": "MIT",
2431 "engines": {
2432 "node": ">=18"
2433 },
2434 "funding": {
2435 "url": "https://github.com/sponsors/sindresorhus"
2436 }
2437 },
24382439 "node_modules/error-ex": {
24392440 "version": "1.3.2",
24402441 "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
......@@ -2486,9 +2487,9 @@
24862487 }
24872488 },
24882489 "node_modules/esbuild": {
2489 "version": "0.25.6",
2490 "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
2491 "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==",
2490 "version": "0.25.5",
2491 "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
2492 "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
24922493 "hasInstallScript": true,
24932494 "license": "MIT",
24942495 "bin": {
......@@ -2498,32 +2499,31 @@
24982499 "node": ">=18"
24992500 },
25002501 "optionalDependencies": {
2501 "@esbuild/aix-ppc64": "0.25.6",
2502 "@esbuild/android-arm": "0.25.6",
2503 "@esbuild/android-arm64": "0.25.6",
2504 "@esbuild/android-x64": "0.25.6",
2505 "@esbuild/darwin-arm64": "0.25.6",
2506 "@esbuild/darwin-x64": "0.25.6",
2507 "@esbuild/freebsd-arm64": "0.25.6",
2508 "@esbuild/freebsd-x64": "0.25.6",
2509 "@esbuild/linux-arm": "0.25.6",
2510 "@esbuild/linux-arm64": "0.25.6",
2511 "@esbuild/linux-ia32": "0.25.6",
2512 "@esbuild/linux-loong64": "0.25.6",
2513 "@esbuild/linux-mips64el": "0.25.6",
2514 "@esbuild/linux-ppc64": "0.25.6",
2515 "@esbuild/linux-riscv64": "0.25.6",
2516 "@esbuild/linux-s390x": "0.25.6",
2517 "@esbuild/linux-x64": "0.25.6",
2518 "@esbuild/netbsd-arm64": "0.25.6",
2519 "@esbuild/netbsd-x64": "0.25.6",
2520 "@esbuild/openbsd-arm64": "0.25.6",
2521 "@esbuild/openbsd-x64": "0.25.6",
2522 "@esbuild/openharmony-arm64": "0.25.6",
2523 "@esbuild/sunos-x64": "0.25.6",
2524 "@esbuild/win32-arm64": "0.25.6",
2525 "@esbuild/win32-ia32": "0.25.6",
2526 "@esbuild/win32-x64": "0.25.6"
2502 "@esbuild/aix-ppc64": "0.25.5",
2503 "@esbuild/android-arm": "0.25.5",
2504 "@esbuild/android-arm64": "0.25.5",
2505 "@esbuild/android-x64": "0.25.5",
2506 "@esbuild/darwin-arm64": "0.25.5",
2507 "@esbuild/darwin-x64": "0.25.5",
2508 "@esbuild/freebsd-arm64": "0.25.5",
2509 "@esbuild/freebsd-x64": "0.25.5",
2510 "@esbuild/linux-arm": "0.25.5",
2511 "@esbuild/linux-arm64": "0.25.5",
2512 "@esbuild/linux-ia32": "0.25.5",
2513 "@esbuild/linux-loong64": "0.25.5",
2514 "@esbuild/linux-mips64el": "0.25.5",
2515 "@esbuild/linux-ppc64": "0.25.5",
2516 "@esbuild/linux-riscv64": "0.25.5",
2517 "@esbuild/linux-s390x": "0.25.5",
2518 "@esbuild/linux-x64": "0.25.5",
2519 "@esbuild/netbsd-arm64": "0.25.5",
2520 "@esbuild/netbsd-x64": "0.25.5",
2521 "@esbuild/openbsd-arm64": "0.25.5",
2522 "@esbuild/openbsd-x64": "0.25.5",
2523 "@esbuild/sunos-x64": "0.25.5",
2524 "@esbuild/win32-arm64": "0.25.5",
2525 "@esbuild/win32-ia32": "0.25.5",
2526 "@esbuild/win32-x64": "0.25.5"
25272527 }
25282528 },
25292529 "node_modules/escalade": {
......@@ -2688,6 +2688,15 @@
26882688 "node": ">=0.10.0"
26892689 }
26902690 },
2691 "node_modules/events-universal": {
2692 "version": "1.0.1",
2693 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
2694 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
2695 "license": "Apache-2.0",
2696 "dependencies": {
2697 "bare-events": "^2.7.0"
2698 }
2699 },
26912700 "node_modules/extend": {
26922701 "version": "3.0.2",
26932702 "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
......@@ -2714,6 +2723,12 @@
27142723 "@types/yauzl": "^2.9.1"
27152724 }
27162725 },
2726 "node_modules/fast-deep-equal": {
2727 "version": "3.1.3",
2728 "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
2729 "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
2730 "license": "MIT"
2731 },
27172732 "node_modules/fast-fifo": {
27182733 "version": "1.3.2",
27192734 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
......@@ -2903,9 +2918,9 @@
29032918 }
29042919 },
29052920 "node_modules/hls.js": {
2906 "version": "1.6.7",
2907 "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.7.tgz",
2908 "integrity": "sha512-QW2fnwDGKGc9DwQUGLbmMOz8G48UZK7PVNJPcOUql1b8jubKx4/eMHNP5mGqr6tYlJNDG1g10Lx2U/qPzL6zwQ==",
2921 "version": "1.6.5",
2922 "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.5.tgz",
2923 "integrity": "sha512-KMn5n7JBK+olC342740hDPHnGWfE8FiHtGMOdJPfUjRdARTWj9OB+8c13fnsf9sk1VtpuU2fKSgUjHvg4rNbzQ==",
29092924 "license": "Apache-2.0"
29102925 },
29112926 "node_modules/hono": {
......@@ -2917,10 +2932,26 @@
29172932 "node": ">=16.9.0"
29182933 }
29192934 },
2935 "node_modules/html-entities": {
2936 "version": "2.6.0",
2937 "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
2938 "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
2939 "funding": [
2940 {
2941 "type": "github",
2942 "url": "https://github.com/sponsors/mdevils"
2943 },
2944 {
2945 "type": "patreon",
2946 "url": "https://patreon.com/mdevils"
2947 }
2948 ],
2949 "license": "MIT"
2950 },
29202951 "node_modules/htmljs-parser": {
2921 "version": "5.5.4",
2922 "resolved": "https://registry.npmjs.org/htmljs-parser/-/htmljs-parser-5.5.4.tgz",
2923 "integrity": "sha512-qJiir3ViASVhGdNW4kwM4pQWvDtDLtjwaflEQDooiNzbOPIzoaIzmRFWweFl8GomVhJqXqX0jjrhDED3pmAdzg==",
2952 "version": "5.7.4",
2953 "resolved": "https://registry.npmjs.org/htmljs-parser/-/htmljs-parser-5.7.4.tgz",
2954 "integrity": "sha512-PCGI7/Z10oBkpKKqbQ9Wgd0GZjSQ/dV7QlBjOmaqD1FkBsHWk730BW9mJCbnXfRbtZCdLh8M8IA8YeKPu9tGaQ==",
29242955 "license": "MIT"
29252956 },
29262957 "node_modules/http-proxy-agent": {
......@@ -2949,6 +2980,12 @@
29492980 "node": ">= 14"
29502981 }
29512982 },
2983 "node_modules/immediate": {
2984 "version": "3.0.6",
2985 "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
2986 "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
2987 "license": "MIT"
2988 },
29522989 "node_modules/import-fresh": {
29532990 "version": "3.3.1",
29542991 "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
......@@ -2974,6 +3011,15 @@
29743011 "node": ">=4"
29753012 }
29763013 },
3014 "node_modules/imsc": {
3015 "version": "1.1.5",
3016 "resolved": "https://registry.npmjs.org/imsc/-/imsc-1.1.5.tgz",
3017 "integrity": "sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ==",
3018 "license": "BSD-2-Clause",
3019 "dependencies": {
3020 "sax": "1.2.1"
3021 }
3022 },
29773023 "node_modules/inline-style-parser": {
29783024 "version": "0.2.4",
29793025 "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
......@@ -2981,14 +3027,10 @@
29813027 "license": "MIT"
29823028 },
29833029 "node_modules/ip-address": {
2984 "version": "9.0.5",
2985 "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
2986 "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
3030 "version": "10.0.1",
3031 "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
3032 "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
29873033 "license": "MIT",
2988 "dependencies": {
2989 "jsbn": "1.1.0",
2990 "sprintf-js": "^1.1.3"
2991 },
29923034 "engines": {
29933035 "node": ">= 12"
29943036 }
......@@ -3112,12 +3154,6 @@
31123154 "js-yaml": "bin/js-yaml.js"
31133155 }
31143156 },
3115 "node_modules/jsbn": {
3116 "version": "1.1.0",
3117 "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
3118 "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
3119 "license": "MIT"
3120 },
31213157 "node_modules/jsesc": {
31223158 "version": "3.1.0",
31233159 "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
......@@ -3179,12 +3215,30 @@
31793215 "resolved": "lib",
31803216 "link": true
31813217 },
3218 "node_modules/lie": {
3219 "version": "3.1.1",
3220 "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
3221 "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
3222 "license": "MIT",
3223 "dependencies": {
3224 "immediate": "~3.0.5"
3225 }
3226 },
31823227 "node_modules/lines-and-columns": {
31833228 "version": "1.2.4",
31843229 "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
31853230 "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
31863231 "license": "MIT"
31873232 },
3233 "node_modules/localforage": {
3234 "version": "1.10.0",
3235 "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
3236 "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
3237 "license": "Apache-2.0",
3238 "dependencies": {
3239 "lie": "3.1.1"
3240 }
3241 },
31883242 "node_modules/longest-streak": {
31893243 "version": "3.1.0",
31903244 "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
......@@ -3226,12 +3280,12 @@
32263280 }
32273281 },
32283282 "node_modules/marko": {
3229 "version": "6.0.35",
3230 "resolved": "https://registry.npmjs.org/marko/-/marko-6.0.35.tgz",
3231 "integrity": "sha512-1zoMBeMEfltQ8QtmW0WoYYsjC0vUJaomQPpXQSlDw9i1o7YZ6T9v8ZSPcScv4ODRM1+XQ0hfNuUT+u7qijXcsA==",
3283 "version": "6.0.82",
3284 "resolved": "https://registry.npmjs.org/marko/-/marko-6.0.82.tgz",
3285 "integrity": "sha512-xnBm299wmqrK8++12dhGHBUonm2SZgQVPNhHZjfp3Wc7k4U/qPbKrc7H1DJgU7JrYv8sSdxkconisynDqBYC1g==",
32323286 "license": "MIT",
32333287 "dependencies": {
3234 "@marko/compiler": "^5.39.28",
3288 "@marko/compiler": "^5.39.40",
32353289 "csstype": "^3.1.3",
32363290 "magic-string": "^0.30.17"
32373291 },
......@@ -4125,9 +4179,9 @@
41254179 }
41264180 },
41274181 "node_modules/node-releases": {
4128 "version": "2.0.19",
4129 "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
4130 "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
4182 "version": "2.0.21",
4183 "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
4184 "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==",
41314185 "license": "MIT"
41324186 },
41334187 "node_modules/once": {
......@@ -4320,17 +4374,17 @@
43204374 }
43214375 },
43224376 "node_modules/puppeteer": {
4323 "version": "24.12.1",
4324 "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.12.1.tgz",
4325 "integrity": "sha512-+vvwl+Xo4z5uXLLHG+XW8uXnUXQ62oY6KU6bEFZJvHWLutbmv5dw9A/jcMQ0fqpQdLydHmK0Uy7/9Ilj8ufwSQ==",
4377 "version": "24.23.0",
4378 "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.23.0.tgz",
4379 "integrity": "sha512-BVR1Lg8sJGKXY79JARdIssFWK2F6e1j+RyuJP66w4CUmpaXjENicmA3nNpUXA8lcTdDjAndtP+oNdni3T/qQqA==",
43264380 "hasInstallScript": true,
43274381 "license": "Apache-2.0",
43284382 "dependencies": {
4329 "@puppeteer/browsers": "2.10.5",
4330 "chromium-bidi": "5.1.0",
4383 "@puppeteer/browsers": "2.10.10",
4384 "chromium-bidi": "9.1.0",
43314385 "cosmiconfig": "^9.0.0",
4332 "devtools-protocol": "0.0.1464554",
4333 "puppeteer-core": "24.12.1",
4386 "devtools-protocol": "0.0.1508733",
4387 "puppeteer-core": "24.23.0",
43344388 "typed-query-selector": "^2.12.0"
43354389 },
43364390 "bin": {
......@@ -4341,16 +4395,17 @@
43414395 }
43424396 },
43434397 "node_modules/puppeteer-core": {
4344 "version": "24.12.1",
4345 "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.12.1.tgz",
4346 "integrity": "sha512-8odp6d3ERKBa3BAVaYWXn95UxQv3sxvP1reD+xZamaX6ed8nCykhwlOiHSaHR9t/MtmIB+rJmNencI6Zy4Gxvg==",
4398 "version": "24.23.0",
4399 "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.23.0.tgz",
4400 "integrity": "sha512-yl25C59gb14sOdIiSnJ08XiPP+O2RjuyZmEG+RjYmCXO7au0jcLf7fRiyii96dXGUBW7Zwei/mVKfxMx/POeFw==",
43474401 "license": "Apache-2.0",
43484402 "dependencies": {
4349 "@puppeteer/browsers": "2.10.5",
4350 "chromium-bidi": "5.1.0",
4351 "debug": "^4.4.1",
4352 "devtools-protocol": "0.0.1464554",
4403 "@puppeteer/browsers": "2.10.10",
4404 "chromium-bidi": "9.1.0",
4405 "debug": "^4.4.3",
4406 "devtools-protocol": "0.0.1508733",
43534407 "typed-query-selector": "^2.12.0",
4408 "webdriver-bidi-protocol": "0.3.6",
43544409 "ws": "^8.18.3"
43554410 },
43564411 "engines": {
......@@ -4578,6 +4633,12 @@
45784633 "queue-microtask": "^1.2.2"
45794634 }
45804635 },
4636 "node_modules/sax": {
4637 "version": "1.2.1",
4638 "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
4639 "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==",
4640 "license": "ISC"
4641 },
45814642 "node_modules/self-closing-tags": {
45824643 "version": "1.0.1",
45834644 "resolved": "https://registry.npmjs.org/self-closing-tags/-/self-closing-tags-1.0.1.tgz",
......@@ -4675,12 +4736,12 @@
46754736 }
46764737 },
46774738 "node_modules/socks": {
4678 "version": "2.8.5",
4679 "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz",
4680 "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==",
4739 "version": "2.8.7",
4740 "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
4741 "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
46814742 "license": "MIT",
46824743 "dependencies": {
4683 "ip-address": "^9.0.5",
4744 "ip-address": "^10.0.1",
46844745 "smart-buffer": "^4.2.0"
46854746 },
46864747 "engines": {
......@@ -4740,12 +4801,6 @@
47404801 "url": "https://github.com/sponsors/wooorm"
47414802 }
47424803 },
4743 "node_modules/sprintf-js": {
4744 "version": "1.1.3",
4745 "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
4746 "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
4747 "license": "BSD-3-Clause"
4748 },
47494804 "node_modules/stackframe": {
47504805 "version": "1.3.4",
47514806 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
......@@ -4753,16 +4808,14 @@
47534808 "license": "MIT"
47544809 },
47554810 "node_modules/streamx": {
4756 "version": "2.22.1",
4757 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz",
4758 "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==",
4811 "version": "2.23.0",
4812 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
4813 "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
47594814 "license": "MIT",
47604815 "dependencies": {
4816 "events-universal": "^1.0.0",
47614817 "fast-fifo": "^1.3.2",
47624818 "text-decoder": "^1.1.0"
4763 },
4764 "optionalDependencies": {
4765 "bare-events": "^2.2.0"
47664819 }
47674820 },
47684821 "node_modules/string-width": {
......@@ -4779,27 +4832,6 @@
47794832 "node": ">=8"
47804833 }
47814834 },
4782 "node_modules/string-width/node_modules/ansi-regex": {
4783 "version": "5.0.1",
4784 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
4785 "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
4786 "license": "MIT",
4787 "engines": {
4788 "node": ">=8"
4789 }
4790 },
4791 "node_modules/string-width/node_modules/strip-ansi": {
4792 "version": "6.0.1",
4793 "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
4794 "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
4795 "license": "MIT",
4796 "dependencies": {
4797 "ansi-regex": "^5.0.1"
4798 },
4799 "engines": {
4800 "node": ">=8"
4801 }
4802 },
48034835 "node_modules/stringify-entities": {
48044836 "version": "4.0.4",
48054837 "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
......@@ -4815,18 +4847,15 @@
48154847 }
48164848 },
48174849 "node_modules/strip-ansi": {
4818 "version": "7.1.0",
4819 "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
4820 "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
4850 "version": "6.0.1",
4851 "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
4852 "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
48214853 "license": "MIT",
48224854 "dependencies": {
4823 "ansi-regex": "^6.0.1"
4855 "ansi-regex": "^5.0.1"
48244856 },
48254857 "engines": {
4826 "node": ">=12"
4827 },
4828 "funding": {
4829 "url": "https://github.com/chalk/strip-ansi?sponsor=1"
4858 "node": ">=8"
48304859 }
48314860 },
48324861 "node_modules/style-mod": {
......@@ -4854,9 +4883,9 @@
48544883 }
48554884 },
48564885 "node_modules/tar-fs": {
4857 "version": "3.1.0",
4858 "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz",
4859 "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==",
4886 "version": "3.1.1",
4887 "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
4888 "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
48604889 "license": "MIT",
48614890 "dependencies": {
48624891 "pump": "^3.0.0",
......@@ -4971,6 +5000,32 @@
49715000 "node": ">=14.17"
49725001 }
49735002 },
5003 "node_modules/ua-parser-js": {
5004 "version": "1.0.41",
5005 "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz",
5006 "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==",
5007 "funding": [
5008 {
5009 "type": "opencollective",
5010 "url": "https://opencollective.com/ua-parser-js"
5011 },
5012 {
5013 "type": "paypal",
5014 "url": "https://paypal.me/faisalman"
5015 },
5016 {
5017 "type": "github",
5018 "url": "https://github.com/sponsors/faisalman"
5019 }
5020 ],
5021 "license": "MIT",
5022 "bin": {
5023 "ua-parser-js": "script/cli.js"
5024 },
5025 "engines": {
5026 "node": "*"
5027 }
5028 },
49745029 "node_modules/undici-types": {
49755030 "version": "7.8.0",
49765031 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
......@@ -5163,6 +5218,12 @@
51635218 "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
51645219 "license": "MIT"
51655220 },
5221 "node_modules/webdriver-bidi-protocol": {
5222 "version": "0.3.6",
5223 "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.6.tgz",
5224 "integrity": "sha512-mlGndEOA9yK9YAbvtxaPTqdi/kaCWYYfwrZvGzcmkr/3lWM+tQj53BxtpVd6qbC6+E5OnHXgCcAhre6AkXzxjA==",
5225 "license": "Apache-2.0"
5226 },
51665227 "node_modules/wrap-ansi": {
51675228 "version": "7.0.0",
51685229 "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
......@@ -5180,27 +5241,6 @@
51805241 "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
51815242 }
51825243 },
5183 "node_modules/wrap-ansi/node_modules/ansi-regex": {
5184 "version": "5.0.1",
5185 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
5186 "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
5187 "license": "MIT",
5188 "engines": {
5189 "node": ">=8"
5190 }
5191 },
5192 "node_modules/wrap-ansi/node_modules/strip-ansi": {
5193 "version": "6.0.1",
5194 "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
5195 "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
5196 "license": "MIT",
5197 "dependencies": {
5198 "ansi-regex": "^5.0.1"
5199 },
5200 "engines": {
5201 "node": ">=8"
5202 }
5203 },
52045244 "node_modules/wrappy": {
52055245 "version": "1.0.2",
52065246 "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
package.json+19-20
......@@ -1,29 +1,28 @@
11{
22 "type": "module",
33 "dependencies": {
4 "@hono/node-server": "^1.14.3",
5 "@mdx-js/mdx": "^3.1.0",
6 "@paperclover/console": "git+https://git.paperclover.net/clo/console.git",
7 "blurhash": "^2.0.5",
8 "codemirror": "^6.0.1",
9 "devalue": "^5.1.1",
10 "esbuild": "^0.25.5",
11 "hls.js": "^1.6.5",
12 "hono": "^4.7.11",
4 "@hono/node-server": "1.14.3",
5 "@mdx-js/mdx": "3.1.0",
6 "codemirror": "6.0.1",
7 "dashjs": "^5.0.3",
8 "devalue": "^5.3.2",
9 "esbuild": "0.25.5",
10 "hls.js": "1.6.5",
11 "hono": "4.9.9",
1312 "lib": "file:lib",
14 "marko": "^6.0.20",
15 "msgpackr": "^1.11.5",
16 "puppeteer": "^24.10.1",
17 "sharp": "^0.34.2",
18 "source-map-support": "^0.5.21",
19 "ts-to-jsdoc": "^2.4.0",
20 "unique-names-generator": "^4.7.1",
21 "vscode-oniguruma": "^2.0.1",
22 "vscode-textmate": "^9.2.0"
13 "marko": "6.0.82",
14 "msgpackr": "1.11.5",
15 "puppeteer": "24.23.0",
16 "sharp": "0.34.2",
17 "source-map-support": "0.5.21",
18 "ts-to-jsdoc": "2.4.0",
19 "unique-names-generator": "4.7.1",
20 "vscode-oniguruma": "2.0.1",
21 "vscode-textmate": "9.2.0"
2322 },
2423 "devDependencies": {
25 "@types/node": "^24.0.10",
26 "typescript": "^5.8.3"
24 "@types/node": "24.0.10",
25 "typescript": "5.8.3"
2726 },
2827 "imports": {
2928 "#backend": "./src/backend.ts",
repl.js+2-3
......@@ -3,21 +3,20 @@
33// doesn't have to reload the plugin system. This is mostly
44// done since Clover's primary dev-env is a Windows 7 laptop;
55// It takes her machine 600ms to load the plugin file on it's own.
6//
7// TODO: as of 2025-10-03, this file is not maintained
68import process from "node:process";
79import * as util from "node:util";
810const exts = [".tsx", ".ts", ".js", ".marko"];
911
1012process.stderr.write("Loading...");
1113const { hot } = await import("./run.js"); // get plugins ready
12const { errorAllWidgets } = hot.load("@paperclover/console/Widget");
1314process.stderr.write("\r" + " ".repeat("Loading...".length) + "\r");
1415const repl = hot.load("node:repl").start({
1516 prompt: "% ",
1617 eval(code, _global, _id, done) {
1718 evaluate(code)
1819 .catch((err) => {
19 // TODO: improve @paperclover/console's ability to print AggregateError
20 // and errors with extra random properties
2120 console.error(util.inspect(err, false, 10, true));
2221 })
2322 .then((result) => done(null, result));
src/backend.ts+3-3
......@@ -1,11 +1,11 @@
11// This is the main file for paperclover.net's server.
22const app = new Hono();
3const logHttp = console.scoped("http", { color: "magenta" });
3const console = log.scoped("http");
44
55// Middleware
66app.use(trimTrailingSlash());
77app.use(removeDuplicateSlashes);
8app.use(logger((msg) => msg.startsWith("-->") && logHttp(msg.slice(4))));
8app.use(logger((msg) => msg.startsWith("-->") && console.info(msg.slice(4))));
99app.use(admin.middleware);
1010
1111// Backends
......@@ -52,4 +52,4 @@ import { logger } from "hono/logger";
5252import { trimTrailingSlash } from "hono/trailing-slash";
5353import * as assets from "#sitegen/assets";
5454import * as admin from "./admin.ts";
55import * as console from "@paperclover/console";
55import * as log from "lib/log.ts";
src/file-viewer/backend.tsx+72-173
......@@ -18,8 +18,12 @@ app.get("/file/*", async (c, next) => {
1818
1919 let rawFilePath = c.req.path.slice(5) || "/";
2020
21 let derivedKey = "";
22 if (rawFilePath.includes(":/")) {
23 [rawFilePath, derivedKey] = rawFilePath.split(":/") as [string, string];
24 }
2125 // The web file viewer sends urls suffixed with $partial to get HTML partials.
22 if (rawFilePath.endsWith("$partial")) {
26 if (!derivedKey && rawFilePath.endsWith("$partial")) {
2327 return getPartialPage(c, rawFilePath.slice(0, -"$partial".length));
2428 }
2529
......@@ -72,23 +76,25 @@ app.get("/file/*", async (c, next) => {
7276 }
7377 const download = viewMode === "download";
7478
75 const etag = file.hash;
76 // const filePath = file.path;
77 const size = file.size;
78
79 //let encoding = decideEncoding(c.req.header("Accept-Encoding"));
79 // Grab a derived asset if that was requested.
80 let { hash, size, date } = file;
81 if (derivedKey) {
82 const entry = derived.get(file, derivedKey);
83 if (entry) {
84 ({ hash, size, date, path: derivedKey } = entry);
85 } else {
86 return next();
87 }
88 }
89 const etag = `"${hash}"`;
8090
81 // let sizeHeader = encoding === "raw"
82 // ? expectedSize
83 // // Size cannot be known because of compression modes
84 // : undefined;
85 //
8691 const headers = new Headers({
8792 Vary: "Accept-Encoding, Accept",
88 "Content-Type": contentTypeFor(file.path),
93 "Content-Type": contentTypeFor(derivedKey || file.path),
8994 "Content-Length": size.toString(),
90 ETag: file.hash,
91 "Last-Modified": file.date.toUTCString(),
95 ETag: etag,
96 "Last-Modified": date.toUTCString(),
97 "Accept-Ranges": "bytes",
9298 });
9399 if (download) {
94100 headers.set(
......@@ -110,15 +116,8 @@ app.get("/file/*", async (c, next) => {
110116 }
111117 }
112118
113 // Head
114 if (c.req.method === "HEAD") {
115 c.res = new Response(null, { headers });
116 return;
117 }
118
119 // Prevalidate range requests
119 let status = 200;
120120 let rangeHeader = c.req.header("Range") ?? null;
121
122121 const ifRangeHeader = c.req.header("If-Range");
123122 if (ifRangeHeader && ifRangeOutdated(file, ifRangeHeader)) {
124123 // > If the condition is not fulfilled, the full resource is
......@@ -126,71 +125,57 @@ app.get("/file/*", async (c, next) => {
126125 rangeHeader = null;
127126 }
128127
129 const found = await cache.read(filePath);
130 // foundFile = await cache.read(filePath, encoding, {
131 // start: rangeHeader ? rangeHeader[0] : 0,
132 // end: rangeHeader ? rangeHeader[1] : Infinity,
133 // });
134 // const [streamOrBuffer, actualEncoding, src] = foundFile;
135 // encoding = actualEncoding;
136 //
137 // // Range requests
138 // // https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
139 // // Compression is skipped because it's a confusing, but solvable problem.
140 // // See https://stackoverflow.com/questions/33947562/is-it-possible-to-send-http-response-using-gzip-and-byte-ranges-at-the-same-time
141 // if (rangeHeader) {
142 // const ranges = parseRange(rangeHeader, file.size);
143 // // TODO: multiple ranges
144 // if (ranges && ranges.length === 1) {
145 // return (c.res = handleRanges(ranges, file, streamOrBuffer, download));
146 // }
147 // }
148 //
149 // // Respond in a streaming fashion
150 // c.res = new Response(streamOrBuffer, {
151 // headers: {
152 // ...fileHeaders(file, download, sizeHeader),
153 // ...(encoding !== "raw" && {
154 // "Content-Encoding": encoding,
155 // }),
156 // "X-Cache": src,
157 // },
158 // });
159});
128 let start = 0;
129 let end = Infinity;
130
131 // Range requests
132 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
133 if (rangeHeader) {
134 const ranges = parseRange(rangeHeader, file.size);
135 if (ranges?.[0] && ranges.length === 1) {
136 start = ranges[0][0];
137 end = ranges[0][1];
138
139 status = 206;
140 headers.set(
141 "Content-Range",
142 `bytes ${ranges[0][0]}-${ranges[0][1]}/${file.size}`,
143 );
144 headers.delete("Content-Length");
145 } else {
146 // multi-ranges are not supported
147 // it could be done by calling `cache.read` multiple times.
148 }
149 }
150
151 // Head
152 if (c.req.method === "HEAD") {
153 c.res = new Response(null, { headers, status });
154 return;
155 }
160156
161app.get("/canvas/:script", async (c, next) => {
162 const script = c.req.param("script");
163 if (!hasAsset(`/js/canvas/${script}.js`)) {
164 return next();
157 const found = await cache.read({
158 file,
159 hash,
160 derived: derivedKey,
161 start,
162 end,
163 });
164 if (!found) {
165 return Response.json({
166 error: "localOnly is enabled and this file is not available",
167 }, 500);
165168 }
166 return view.serve(c, "file-viewer/canvas", {
167 script,
169 if (status === 200) {
170 headers.set("Content-Length", found.size.toString());
171 }
172 headers.set("Cache-Status", found.src);
173 c.res = new Response(found.stream as ReadableStream, {
174 headers,
175 status,
168176 });
169177});
170178
171function decideEncoding(encodings: string | undefined): CompressionFormat {
172 if (encodings?.includes("zstd")) return "zstd";
173 if (encodings?.includes("gzip")) return "gzip";
174 return "raw";
175}
176
177function fileHeaders(
178 file: MediaFile,
179 download: boolean,
180 size: number | undefined = file.size,
181) {
182 return {
183 Vary: "Accept-Encoding, Accept",
184 "Content-Type": contentTypeFor(file.path),
185 "Content-Length": size.toString(),
186 ETag: file.hash,
187 "Last-Modified": file.date.toUTCString(),
188 ...(download && {
189 "Content-Disposition": `attachment; filename="${file.basename}"`,
190 }),
191 };
192}
193
194179function ifRangeOutdated(file: MediaFile, ifRangeHeader: string) {
195180 // etag
196181 if (ifRangeHeader[0] === '"') {
......@@ -205,6 +190,7 @@ type Ranges = Array<[start: number, end: number]>;
205190
206191function parseRange(rangeHeader: string, fileSize: number): Ranges | null {
207192 const [unit, ranges] = rangeHeader.split("=");
193 if (!ranges) return null;
208194 if (unit !== "bytes") return null;
209195
210196 const result: Array<[start: number, end: number]> = [];
......@@ -212,8 +198,8 @@ function parseRange(rangeHeader: string, fileSize: number): Ranges | null {
212198
213199 for (const range of rangeParts) {
214200 const split = range.split("-");
215 if (split.length !== 2) return null;
216201 const [start, end] = split;
202 if (!start || !end) return null;
217203 if (start === "" && end === "") return null;
218204 const parsedRange: [number, number] = [
219205 start === "" ? fileSize - +end : +start,
......@@ -225,7 +211,7 @@ function parseRange(rangeHeader: string, fileSize: number): Ranges | null {
225211 // Validate that ranges do not intersect
226212 result.sort((a, b) => a[0] - b[0]);
227213 for (let i = 1; i < result.length; i++) {
228 if (result[i][0] <= result[i - 1][1]) {
214 if (UNWRAP(result[i])[0] <= UNWRAP(result[i - 1])[1]) {
229215 return null;
230216 }
231217 }
......@@ -233,94 +219,6 @@ function parseRange(rangeHeader: string, fileSize: number): Ranges | null {
233219 return result;
234220}
235221
236function handleRanges(
237 ranges: Ranges,
238 file: MediaFile,
239 streamOrBuffer: ReadableStream | Buffer,
240 download: boolean,
241): Response {
242 // TODO: multiple ranges
243 const rangeSize = ranges.reduce((a, b) => a + (b[1] - b[0] + 1), 0);
244 const rangeBody = streamOrBuffer instanceof ReadableStream
245 ? applySingleRangeToStream(streamOrBuffer, ranges)
246 : applyRangesToBuffer(streamOrBuffer, ranges, rangeSize);
247 return new Response(rangeBody, {
248 status: 206,
249 headers: {
250 ...fileHeaders(file, download, rangeSize),
251 "Content-Range": `bytes ${ranges[0][0]}-${ranges[0][1]}/${file.size}`,
252 },
253 });
254}
255
256function applyRangesToBuffer(
257 buffer: Buffer,
258 ranges: Ranges,
259 rangeSize: number,
260): Uint8Array {
261 const result = new Uint8Array(rangeSize);
262 let offset = 0;
263 for (const [start, end] of ranges) {
264 result.set(buffer.subarray(start, end + 1), offset);
265 offset += end - start + 1;
266 }
267 return result;
268}
269
270function applySingleRangeToStream(
271 stream: ReadableStream,
272 ranges: Ranges,
273): ReadableStream {
274 let reader: ReadableStreamDefaultReader<Uint8Array>;
275 let position = 0;
276 const [start, end] = ranges[0];
277 return new ReadableStream({
278 async start(controller) {
279 reader = stream.getReader();
280 try {
281 while (position <= end) {
282 const { done, value } = await reader.read();
283
284 if (done) {
285 controller.close();
286 return;
287 }
288
289 const buffer = new Uint8Array(value);
290
291 const bufferStart = position;
292 const bufferEnd = position + buffer.length - 1;
293
294 position += buffer.length;
295
296 if (bufferEnd < start) {
297 continue;
298 }
299
300 if (bufferStart > end) {
301 break;
302 }
303
304 const sendStart = Math.max(0, start - bufferStart);
305 const sendEnd = Math.min(buffer.length - 1, end - bufferStart);
306
307 if (sendStart <= sendEnd) {
308 controller.enqueue(buffer.slice(sendStart, sendEnd + 1));
309 }
310 }
311
312 controller.close();
313 } catch (error) {
314 controller.error(error);
315 } finally {
316 reader.releaseLock();
317 }
318 },
319 cancel() {
320 reader?.releaseLock();
321 },
322 });
323}
324222function checkCotyledonCookie(c: Context) {
325223 const cookie = c.req.header("Cookie");
326224 if (!cookie) return false;
......@@ -383,7 +281,7 @@ function getPartialPage(c: Context, rawFilePath: string) {
383281import { type Context, Hono } from "hono";
384282
385283import * as render from "#engine/render";
386import { etagMatches, hasAsset, serveAsset } from "#sitegen/assets";
284import { etagMatches, serveAsset } from "#sitegen/assets";
387285import * as view from "#sitegen/view";
388286import { contentTypeFor } from "#sitegen/mime";
389287
......@@ -393,3 +291,4 @@ import { FilePermissions } from "@/file-viewer/models/FilePermissions.ts";
393291import { MediaPanel } from "@/file-viewer/views/clofi.tsx";
394292import { Speedbump } from "@/file-viewer/cotyledon.tsx";
395293import * as cache from "@/file-viewer/cache.ts";
294import * as derived from "@/file-viewer/models/derived.ts";
src/file-viewer/bin/scan3.ts+222-151
......@@ -19,6 +19,8 @@ export async function main() {
1919 `paper clover's scan3 [${((now - start) / 1000).toFixed(1)}s]`,
2020 });
2121
22 const promises = new async.PromiseAggregator();
23
2224 const walkQueue = new queue.PriorityQueue(10);
2325
2426 const dirsNode = progress.start("Walk Tree", { estimate: 1 });
......@@ -46,19 +48,15 @@ export async function main() {
4648 node.text = publicPath + " - reading";
4749 const items = (await path.readDir())
4850 .filter((child) => !skipBasename(child.base))
49 .map((child) => (scanDirectory(child), child.base));
51 .map((child) => (promises.push(scanDirectory(child)), child.base));
5052 dirsNode.estimate += items.length;
5153
52 if (mediaFile) {
53 const deleted = mediaFile
54 .getChildren()
55 .filter((child) => !items.includes(child.basename))
56 .flatMap((child) =>
57 child.kind === MediaFileKind.directory
58 ? child.getRecursiveFileChildren()
59 : child
60 );
61 //deleted
54 for (const child of mediaFile?.getChildren() ?? []) {
55 if (items.includes(child.basename)) continue;
56 const recursive = child.kind === MediaFileKind.directory
57 ? [child, ...child.getRecursiveFileChildren()]
58 : [child];
59 for (const deletion of recursive) deletion.delete();
6260 }
6361
6462 return;
......@@ -70,10 +68,10 @@ export async function main() {
7068 stat.size !== mediaFile.size ||
7169 stat.mtime.getTime() !== mediaFile.date.getTime()
7270 ) {
73 updateMetadata({ path, publicPath, stat, mediaFile });
71 promises.push(updateMetadata({ path, publicPath, stat, mediaFile }));
7472 } else {
7573 // If the scanners changed, it may mean more processes should be run.
76 queueProcessors({
74 await queueProcessors({
7775 path,
7876 stat,
7977 mediaFile,
......@@ -144,15 +142,16 @@ export async function main() {
144142 processor,
145143 index,
146144 after,
147 fileNode,
145 fileNode: innerNode,
148146 }: ProcessJob) {
149 using node = fileNode.start(processor.name);
147 using node = innerNode.start(processor.name);
150148 await processor.run({
151149 path,
152150 stat,
153151 mediaFile,
154152 node,
155153 });
154 fileNode.value += 1;
156155 mediaFile.setProcessed(mediaFile.processed | (1 << (16 + index)));
157156 for (const dependantJob of after) {
158157 ASSERT(
......@@ -164,7 +163,7 @@ export async function main() {
164163 queueFileProcessor(dependantJob);
165164 }
166165 }
167 }, (job) => ({ cores: job.processor.cores }));
166 }, (job) => ({ cores: job.processor.cores ?? 0 }));
168167
169168 function decodeProcessors(input: string) {
170169 return input
......@@ -201,7 +200,13 @@ export async function main() {
201200
202201 // If the hash has changed, migrate the bitfield over.
203202 // This also runs when the processor hash is in it's initial 0 state.
204 const order = decodeProcessors(mediaFile.processors);
203 let order: ReturnType<typeof decodeProcessors>;
204 try {
205 order = decodeProcessors(mediaFile.processors);
206 } catch {
207 // this function sucks and this system sucks i hate it.
208 order = [];
209 }
205210 if ((processed & 0xffff) !== hash) {
206211 const previous = order.filter(
207212 (_, i) => (processed & (1 << (16 + i))) !== 0,
......@@ -241,10 +246,11 @@ export async function main() {
241246 fileNode: node,
242247 };
243248 jobs.push(job);
244 if (job.needs === 0) queueFileProcessor(job);
249 if (job.needs === 0) promises.push(queueFileProcessor(job));
245250 }
246251 }
247252 node.estimate = jobs.length;
253 fileNode.estimate += jobs.length;
248254 for (const job of jobs) {
249255 for (const dependId of job.processor.depends) {
250256 const dependJob = jobs.find((j) => j.processor.id === dependId);
......@@ -253,7 +259,7 @@ export async function main() {
253259 } else {
254260 ASSERT(job.needs > 0, `job.needs !== 0, ${job.needs}`);
255261 job.needs -= 1;
256 if (job.needs === 0) queueFileProcessor(job);
262 if (job.needs === 0) promises.push(queueFileProcessor(job));
257263 }
258264 }
259265 }
......@@ -265,16 +271,18 @@ export async function main() {
265271 if (!rootPath.ifExistsSync()) {
266272 throw new Error(`file store ${rootPath} is not mounted`);
267273 }
268 scanDirectory(rootPath);
269 // await qList.done();
270 // await qMeta.done();
271 // await qProcess.done();
272 return;
274 promises.push(scanDirectory(rootPath));
275
276 await promises.all();
277 fileNode.end();
278 dirsNode.end();
273279
274280 // Update directory metadata
281 using dirMetaNode = progress.start("update directory metadata");
275282 const dirs = MediaFile.getDirectoriesToReindex()
276283 .sort((a, b) => b.path.length - a.path.length);
277284 for (const dir of dirs) {
285 using _ = dirMetaNode.start(dir.path);
278286 const children = dir.getChildren();
279287
280288 // readme.txt
......@@ -327,56 +335,58 @@ export async function main() {
327335 dirsort,
328336 });
329337 }
338 dirMetaNode.end();
330339
331340 // Sync to remote
332 // if (
333 // ((await derived.workDir.ifExistsSync()?.readDir())?.length ?? 0) > 0
334 // ) {
335 // await rsync.spawn({
336 // args: [
337 // "--links",
338 // "--recursive",
339 // "--times",
340 // "--partial",
341 // "--progress",
342 // "--remove-source-files",
343 // "--delay-updates",
344 // derived.workDir.toString() + "/",
345 // "clo@zenith:/mnt/storage1/clover/Documents/Config/clover_file/derived/",
346 // ],
347 // title: "Uploading Derived Assets",
348 // cwd: process.cwd(),
349 // });
350 //
351 // await fs.removeEmptyDirectories(derived.workDir.toString());
352 // } else {
353 // console.info("No new derived assets");
354 // }
341 if (
342 ((await derived.workDir.ifExistsSync()?.readDir())?.length ?? 0) > 0
343 ) {
344 await rsync.spawn({
345 args: [
346 "--links",
347 "--recursive",
348 "--times",
349 "--partial",
350 "--progress",
351 // "--remove-source-files",
352 "--delay-updates",
353 "--exclude=tmp.*",
354 derived.workDir.toString() + "/",
355 "clo@zenith:/mnt/storage1/clover/Documents/Config/paperclover/derived/",
356 ],
357 progress: progress.start("upload derived assets"),
358 cwd: process.cwd(),
359 });
360
361 await fs.removeEmptyDirectories(derived.workDir.toString());
362 } else {
363 console.info("No new derived assets");
364 }
355365
356366 MediaFile.db.prepare("VACUUM").run();
357367 MediaFile.db.reload();
358368
359 // await rsync.spawn({
360 // args: [
361 // MediaFile.db.file,
362 // "clo@zenith:/mnt/storage1/clover/Documents/Config/paperclover/cache.sqlite",
363 // ],
364 // title: "Uploading Database",
365 // cwd: process.cwd(),
366 // });
367 // if (sotToken) {
368 // const res = await fetch("https://db.paperclover.net/reload", {
369 // method: "post",
370 // headers: {
371 // Authorization: sotToken,
372 // },
373 // });
374 // if (!res.ok) {
375 // console.warn(
376 // `Failed to reload remote database ${res.status} ${res.statusText}`,
377 // );
378 // }
379 // } else console.warn("Missing SOT token");
369 await rsync.spawn({
370 args: [
371 MediaFile.db.file,
372 "clo@zenith:/mnt/storage1/clover/Documents/Config/paperclover/cache.sqlite",
373 ],
374 progress: progress.start("Uploading database"),
375 cwd: process.cwd(),
376 });
377 if (sotToken) {
378 const res = await fetch("https://db.paperclover.net/reload", {
379 method: "post",
380 headers: {
381 Authorization: sotToken,
382 },
383 });
384 if (!res.ok) {
385 console.warn(
386 `Failed to reload remote database ${res.status} ${res.statusText}`,
387 );
388 }
389 } else console.warn("Missing SOT token");
380390
381391 console.info(
382392 "Updated file viewer index in \x1b[1m" +
......@@ -392,21 +402,30 @@ export async function main() {
392402 from media_files
393403 `)
394404 .getNonNull();
405 const { derivedSize, derivedCount } = MediaFile.db
406 .prepare<[], { derivedCount: number; derivedSize: number }>(`
407 select
408 count(*) as derivedCount,
409 sum(size) as derivedSize
410 from derived_files
411 `)
412 .getNonNull();
413 const canonicalSize = UNWRAP(MediaFile.getByPath("/")).size;
395414
396415 console.info();
397416 console.info(
398417 "Global Stats:\n" +
399418 `- File Count: \x1b[1m${count}\x1b[0m\n` +
400 `- Canonical Size: \x1b[1m${
401 formatSize(MediaFile.getByPath("/")!.size)
402 }\x1b[0m\n` +
403 `- Media Duration: \x1b[1m${formatDurationLong(duration)}\x1b[0m\n`,
419 `- Derived Count: \x1b[1m${derivedCount}\x1b[0m\n` +
420 `- Media Duration: \x1b[1m${formatDurationLong(duration)}\x1b[0m\n` +
421 `- Canonical Size: \x1b[1m${formatSize(canonicalSize)}\x1b[0m\n` +
422 `- Derived Size: \x1b[1m${formatSize(derivedSize)}\x1b[0m\n`,
404423 );
405424}
406425
407426interface Process {
408427 name: string;
409 cores: number;
428 cores?: number;
410429 enable?: boolean;
411430 include?: Set<string>;
412431 exclude?: Set<string>;
......@@ -491,6 +510,7 @@ const procLoadTextContents: Process = {
491510 name: "load text content",
492511 include: rules.extsReadContents,
493512 cores: 1,
513 version: 2,
494514 async run({ path, mediaFile, stat }) {
495515 if (stat.size > 1_000_000) return;
496516 const text = await path.read("utf-8");
......@@ -502,6 +522,7 @@ const procHighlightCode: Process = {
502522 name: "highlight source code",
503523 include: new Set(rules.extsCode.keys()),
504524 cores: 1,
525 version: 2,
505526 async run({ path, mediaFile, stat }) {
506527 const language = UNWRAP(
507528 rules.extsCode.get(path.ext.toLowerCase()),
......@@ -531,7 +552,6 @@ const procImageSubsets: Process = {
531552 include: rules.extsImage,
532553 depends: [procDimensions.name],
533554 version: 2,
534 cores: 2,
535555 async run({ path, mediaFile, node }) {
536556 const { width, height } = UNWRAP(mediaFile.parseDimensions());
537557 const targetSizes = transcodeRules.imageSizes.filter((w) => w < width);
......@@ -544,16 +564,22 @@ const procImageSubsets: Process = {
544564 node.text = baseStatus + ` (${w}x${h}, ${ext.slice(1).toUpperCase()})`;
545565
546566 stack.use(
547 await derived.produce(mediaFile, `${size}${ext}`, async (dir) => {
548 await subprocess.exec(ffmpegBin!, [
549 ...ffmpegOptions,
550 "-i",
551 path.toString(),
552 "-vf",
553 `scale=${w}:${h}:force_original_aspect_ratio=increase,crop=${w}:${h}`,
554 ...args,
555 dir.join(`${size}${ext}`).toString(),
556 ]);
567 await derived.produce({
568 mediaFile,
569 node,
570 cores: 2,
571 subkey: `${size}${ext}`,
572 async producer(dir) {
573 await subprocess.exec(ffmpegBin!, [
574 ...ffmpegOptions,
575 "-i",
576 path.toString(),
577 "-vf",
578 `scale=${w}:${h}:force_original_aspect_ratio=increase,crop=${w}:${h}`,
579 ...args,
580 dir.join(`${size}${ext}`).toString(),
581 ]);
582 },
557583 }),
558584 );
559585 }
......@@ -563,9 +589,7 @@ const procImageSubsets: Process = {
563589 },
564590};
565591
566const videoInputArgsCache = new async.OnceMap2(
567 transcodeRules.getVideoInputArgs,
568);
592const videoArgsCache = new async.OnceMap2(transcodeRules.getVideoInputArgs);
569593
570594const qualityMap: Record<string, string> = {
571595 u: "ultra-high",
......@@ -579,19 +603,32 @@ const procVideos = transcodeRules.videoFormats.map<Process>((preset) => ({
579603 include: rules.extsVideo,
580604 enable: ffmpegBin != null,
581605 depends: [procDuration.name, procDimensions.name],
582 cores: 4,
583606 async run({ path, mediaFile, node }) {
584607 if ((mediaFile.duration ?? 0) < 5) return;
585 await derived.produce(mediaFile, `av1-${preset.id}`, async (dir) => {
586 const input = await videoInputArgsCache.getOrRun(path);
587 const args = transcodeRules.getAv1VideoArgs(preset, input.video, dir);
588
589 await ffmpeg.spawn({
590 ffmpeg: ffmpegBin!,
591 progress: node,
592 args,
593 cwd: dir,
594 });
608 if (!mediaFile.dimensions) return;
609
610 if (mediaFile.path === "/2021/top-10000-bread/output.mp4") return;
611
612 await derived.produce({
613 mediaFile,
614 node,
615 cores: 4,
616 subkey: `av1-${preset.id}`,
617 async producer(dir) {
618 const input = await videoArgsCache.getOrRun(path);
619 const args = transcodeRules.getAv1VideoArgs(
620 preset,
621 UNWRAP(input.video, "frick on " + path + JSON.stringify(input)),
622 dir,
623 );
624
625 await ffmpeg.spawn({
626 ffmpeg: ffmpegBin!,
627 progress: node,
628 args,
629 cwd: dir,
630 });
631 },
595632 });
596633 },
597634}));
......@@ -600,20 +637,29 @@ const procVideoAudios = transcodeRules.audioFormats.map<Process>((preset) => ({
600637 include: rules.extsVideo,
601638 enable: ffmpegBin != null,
602639 depends: [procDuration.name, procDimensions.name],
603 cores: 2,
640 version: 2,
604641 async run({ path, mediaFile, node }) {
605642 if ((mediaFile.duration ?? 0) < 5) return;
606 await derived.produce(mediaFile, `opus-${preset.id}`, async (dir) => {
607 const input = await videoInputArgsCache.getOrRun(path);
608 if (!input.audio) return;
609 const args = transcodeRules.getOpusAudioArgs(preset, input.audio, dir);
610
611 await ffmpeg.spawn({
612 ffmpeg: ffmpegBin!,
613 progress: node,
614 args,
615 cwd: dir,
616 });
643 if (!mediaFile.dimensions) return;
644 if (mediaFile.path === "/2021/top-10000-bread/output.mp4") return;
645 await derived.produce({
646 mediaFile,
647 node,
648 cores: 4,
649 subkey: `opus-${preset.id}`,
650 async producer(dir) {
651 const input = await videoArgsCache.getOrRun(path);
652 ASSERT(input.video);
653 if (!input.audio) return;
654 const args = transcodeRules.getOpusAudioArgs(preset, input.audio, dir);
655
656 await ffmpeg.spawn({
657 ffmpeg: ffmpegBin!,
658 progress: node,
659 args,
660 cwd: dir,
661 });
662 },
617663 });
618664 },
619665}));
......@@ -622,37 +668,50 @@ const procDash: Process = {
622668 include: rules.extsVideo,
623669 enable: ffmpegBin != null,
624670 depends: [...procVideos, ...procVideoAudios].map((x) => x.name),
625 cores: 1,
671 version: 4,
626672 async run({ path, mediaFile, node }) {
627673 if ((mediaFile.duration ?? 0) < 5) return;
628 await derived.produce(mediaFile, `dash-av1`, async (dir) => {
629 const input = await videoInputArgsCache.getOrRun(path);
630 const videos = transcodeRules.videoFormats.map(
631 (preset) =>
632 UNWRAP(dir.parent).join(
633 `av1-${preset.id}`,
634 transcodeRules.av1FileName,
635 ),
636 );
637 const audios = input.audio
638 ? transcodeRules.videoFormats.map(
674 if (!mediaFile.dimensions) return;
675
676 if (mediaFile.path === "/2021/top-10000-bread/output.mp4") return;
677 await derived.produce({
678 mediaFile,
679 node,
680 cores: 1,
681 subkey: `dash-av1`,
682 async producer(dir) {
683 const input = await videoArgsCache.getOrRun(path);
684 const videos = transcodeRules.videoFormats.map(
639685 (preset) =>
640686 UNWRAP(dir.parent).join(
641687 `av1-${preset.id}`,
642688 transcodeRules.av1FileName,
643689 ),
644 )
645 : [];
646 const args = transcodeRules.getMpegDashArgs(videos, audios, dir);
690 );
691 const audios = input.audio
692 ? transcodeRules.audioFormats.map(
693 (preset) =>
694 UNWRAP(dir.parent).join(
695 `opus-${preset.id}`,
696 transcodeRules.opusFileName,
697 ),
698 )
699 : [];
700 const args = transcodeRules.getMpegDashArgs(
701 videos,
702 audios,
703 dir,
704 );
647705
648 await dir.join("d").makeDir();
706 await dir.join("d").makeDir();
649707
650 await ffmpeg.spawn({
651 ffmpeg: ffmpegBin!,
652 progress: node,
653 args,
654 cwd: dir,
655 });
708 await ffmpeg.spawn({
709 ffmpeg: ffmpegBin!,
710 progress: node,
711 args,
712 cwd: dir,
713 });
714 },
656715 });
657716 },
658717};
......@@ -661,19 +720,26 @@ const procH264Hls: Process = {
661720 include: rules.extsVideo,
662721 enable: ffmpegBin != null,
663722 depends: [procDuration.name, procDimensions.name],
664 cores: 8,
665723 async run({ path, mediaFile, node }) {
666724 if ((mediaFile.duration ?? 0) < 5) return;
667 await derived.produce(mediaFile, `hls`, async (dir) => {
668 const input = await videoInputArgsCache.getOrRun(path);
669 const args = transcodeRules.getH264HlsArgs(input, dir);
670
671 await ffmpeg.spawn({
672 ffmpeg: ffmpegBin!,
673 progress: node,
674 args,
675 cwd: dir,
676 });
725 if (!mediaFile.dimensions) return;
726
727 await derived.produce({
728 mediaFile,
729 node,
730 cores: 8,
731 subkey: `hls`,
732 async producer(dir) {
733 const input = await videoArgsCache.getOrRun(path);
734 const args = transcodeRules.getH264HlsArgs(input, dir);
735
736 await ffmpeg.spawn({
737 ffmpeg: ffmpegBin!,
738 progress: node,
739 args,
740 cwd: dir,
741 });
742 },
677743 });
678744 },
679745};
......@@ -685,14 +751,19 @@ const procCompression = [
685751 ({ name, fn }) => ({
686752 name: `compress ${name}`,
687753 exclude: rules.extsPreCompressed,
688 cores: 1,
689 async run({ path, mediaFile }) {
690 await derived.produce(mediaFile, name, async (dir) => {
691 await stream.promises.pipeline(
692 fs.createReadStream(path.toString()),
693 fn(),
694 fs.createWriteStream(dir.join(name).toString()),
695 );
754 async run({ path, mediaFile, node }) {
755 await derived.produce({
756 mediaFile,
757 node,
758 cores: 1,
759 subkey: name,
760 async producer(dir) {
761 await stream.promises.pipeline(
762 fs.createReadStream(path.toString()),
763 fn(),
764 fs.createWriteStream(dir.join(name).toString()),
765 );
766 },
696767 });
697768 },
698769 }),
src/file-viewer/cache.ts+18-19
......@@ -17,7 +17,7 @@
1717// objects given by node:fs/node:http streams -- which are re-allocated for
1818// every read -- are weakly held when written and reused when reading. this
1919// somewhat avoids file reads in concurrent access situations.
20const log = console.scoped("cache");
20const console = log.scoped("cache");
2121
2222export type Source =
2323 | "local" // local mode
......@@ -94,6 +94,7 @@ export interface Options {
9494 file: MediaFile;
9595 /** If set, fetches the derived asset instead. */
9696 derived: string | null;
97 hash: string;
9798 start: number;
9899 /** exclusive. ex: [10, 30] skips ten bytes then grabs 20 */
99100 end: number;
......@@ -106,13 +107,13 @@ export interface Options {
106107 * `null` if a derived asset does not exist
107108 */
108109export async function read(
109 { file, derived, start, end, noWrite, localOnly }: Options,
110 { file, derived, hash, start, end, noWrite, localOnly }: Options,
110111): Promise<{ stream: ReadableStream; src: Source; size: number } | null> {
111112 const rel = derived ? `${file.path}$/${derived}` : file.path;
112113
113114 // Serve from local network share if it is mounted.
114115 const localFile = derived
115 ? derivedFileRoot?.join(file.hash, derived)
116 ? derivedFileRoot?.join(derived)
116117 : rawFileRoot?.join(file.path);
117118 if (localFile) {
118119 local: {
......@@ -128,7 +129,7 @@ export async function read(
128129 }
129130 const fileStream = await readStream(localFile, { start, end });
130131 if (!fileStream) break local;
131 log(`from local: ${rel}`);
132 console.info(`from local: ${rel} ` + localFile);
132133 return {
133134 stream: stream.Readable.toWeb(fileStream),
134135 size,
......@@ -139,10 +140,6 @@ export async function read(
139140
140141 if (!localOnly) return null;
141142
142 const key = derived
143 ? crypto.createHash("sha1").update(file.hash + derived).digest("hex")
144 : file.hash;
145
146143 // Disk cache
147144 const diskPath = diskCacheRoot.join(key);
148145 let diskFile = diskCache.get(diskPath);
......@@ -156,7 +153,7 @@ export async function read(
156153 });
157154 if (diskStream) {
158155 const isPending = diskFile.written.value !== diskFile.size;
159 log(`from ${isPending ? "pending " : ""}disk cache: ${rel}`);
156 console.info(`from ${isPending ? "pending " : ""}disk cache: ${rel}`);
160157 return {
161158 stream: stream.Readable.toWeb(diskStream),
162159 src: isPending ? "pending" : "hit",
......@@ -173,10 +170,10 @@ export async function read(
173170 {
174171 let promise = parallelRequests.get(url);
175172 if (promise) {
176 log(`from other response: ${rel}`);
173 console.info(`from other response: ${rel}`);
177174 response = await promise;
178175 } else {
179 log(`cache miss: ${rel}`);
176 console.info(`cache miss: ${rel}`);
180177 promise = httpGet(url, {
181178 headers: token ? { Authorization: token } : {},
182179 });
......@@ -197,18 +194,18 @@ export async function read(
197194
198195 // Do not cache large assets
199196 if (size > 1_000_000_000) {
200 log(`skipping disk cache due to large asset: ${rel}`);
197 console.info(`skipping disk cache due to large asset: ${rel}`);
201198 return { stream: stream.Readable.toWeb(response), size, src: "ignored" };
202199 }
203200 // Bail out of the disk cache if it is contended
204201 if (diskCache.unlockedCapacity < size) {
205 log(`skipping disk cache due to contention: ${rel}`);
202 console.info(`skipping disk cache due to contention: ${rel}`);
206203 return { stream: stream.Readable.toWeb(response), size, src: "contended" };
207204 }
208205 // IPs which request too many cache misses are temporarily
209206 // banned from writing into the disk cache to avoid abuse.
210207 if (noWrite) {
211 log(`skipping disk cache due to config: ${rel}`);
208 console.info(`skipping disk cache due to config: ${rel}`);
212209 return { stream: stream.Readable.toWeb(response), size, src: "ratelimit" };
213210 }
214211
......@@ -273,7 +270,7 @@ function openDiskCache() {
273270 const index = split.indexOf(".clover");
274271 return index === -1
275272 ? Path.createDirSync(clover, fileCache)
276 : Path.createDirSync(...split.slice(0, index + 1), fileCache);
273 : Path.createDirSync(split.slice(0, index + 1).join(Path.sep), fileCache);
277274}
278275
279276function writeDiskCache() {
......@@ -284,7 +281,9 @@ function writeDiskCache() {
284281 },
285282 });
286283 diskCacheState.writeJsonFile(serialized)
287 .catch(() => {});
284 .catch((err) =>
285 console.error(`Failed to write ${diskCacheState.relative()}:`, err)
286 );
288287}
289288
290289async function getHandle(path: Path, flag: string, unlockOnFail?: Disposable) {
......@@ -404,7 +403,7 @@ async function readStream(path: Path, opts: ReadStreamOptions) {
404403 }
405404
406405 let closed = false;
407 return fs.createReadStream(path.raw, {
406 return fs.createReadStream(path.toString(), {
408407 fd: handle.fd,
409408 start: opts?.start ?? 0,
410409 end: opts?.end ?? Infinity,
......@@ -433,7 +432,7 @@ async function writeStream(
433432 let closed = false;
434433
435434 const { memory, written } = file;
436 const writer = fs.createWriteStream(path.raw, {
435 const writer = fs.createWriteStream(path.toString(), {
437436 start: 0,
438437 fd: handle.fd,
439438 fs: {
......@@ -480,7 +479,6 @@ async function writeStream(
480479 return writer;
481480}
482481
483import * as console from "@paperclover/console";
484482import * as util from "node:util";
485483import * as fsCallbacks from "node:fs";
486484import * as fs from "#sitegen/fs";
......@@ -491,6 +489,7 @@ import * as crypto from "node:crypto";
491489import * as http from "node:http";
492490import * as https from "node:https";
493491import * as paths from "./paths.ts";
492import * as log from "lib/log.ts";
494493import { MediaFile } from "./models/MediaFile.ts";
495494import type { ReadableStream } from "node:stream/web";
496495import { Lru } from "#sitegen/lru";
src/file-viewer/ffmpeg.ts+11-7
......@@ -68,22 +68,26 @@ export async function spawn(options: SpawnOptions) {
6868 }
6969}
7070
71// this is janky
72export async function probeAudioStreams(path: Path) {
71export async function probeStreams(path: Path) {
7372 const { stdout } = await subprocess.spawnAndWait({
7473 cmd: [
7574 "ffprobe",
7675 "-v",
77 "quiet",
78 "-select_streams",
79 "a",
76 "error",
8077 "-show_entries",
81 "stream=index",
78 "stream=codec_type",
8279 path.toString(),
80 "-of",
81 "default=noprint_wrappers=1:nokey=1",
8382 ],
8483 stdio: "pipe",
8584 });
86 return stdout?.byteLength ?? 0 > 0;
85
86 const lines = new TextDecoder().decode(stdout!).trim().split("\n");
87 return {
88 audio: lines.includes("audio"),
89 video: lines.includes("video"),
90 };
8791}
8892
8993export class Parse {
src/file-viewer/models/MediaFile.ts+3-1
......@@ -117,7 +117,9 @@ export class MediaFile {
117117 .filter((file) => !file.basename.startsWith("."));
118118 }
119119 getPublicChildren() {
120 const children = MediaFile.getChildren(this.id);
120 const children = MediaFile.getChildren(this.id).filter((x) =>
121 !x.basename.startsWith("_unlisted")
122 );
121123 if (FilePermissions.getByPrefix(this.path) == 0) {
122124 return children.filter(({ path }) => FilePermissions.getExact(path) == 0);
123125 }
src/file-viewer/models/derived.ts+67-20
......@@ -21,6 +21,7 @@ db.table(
2121 file text not null,
2222 root integer not null,
2323 size integer not null, -- bytes
24 hash text not null, -- sha1
2425 foreign key(root) references derived_roots(id) on delete cascade
2526 );
2627 create table if not exists derived_refs (
......@@ -44,15 +45,19 @@ let ongoing = new Map<string, Promise<number>>();
4445
4546/** produce a derived */
4647export async function produce(
47 file: MediaFile,
48 subkey: string,
49 producer: (root: Path) => Promise<void>,
48 { mediaFile: file, node, subkey, cores, producer }: {
49 mediaFile: MediaFile;
50 node: progress.Node;
51 subkey: string;
52 cores: number;
53 producer: (root: Path) => Promise<void>;
54 },
5055) {
5156 const key = `${file.hash}/${subkey}`;
5257 const current = ongoing.get(key);
5358 let root: number | null = null;
5459 if (current) {
55 await current;
60 root = await current;
5661 } else {brk: {
5762 root = getRootQuery.get({ key })?.id ?? null;
5863 if (root) break brk;
......@@ -60,21 +65,57 @@ export async function produce(
6065 ongoing.set(key, promise);
6166 try {
6267 const tmp = workDir.join(key);
63 await tmp.makeOrEmptyDir();
64 await producer(tmp);
65 const filesWithStats = await Promise.all(
66 (await tmp.readDir())
67 .filter((x) => !x.base.startsWith("tmp."))
68 .map(async (x) => [x, await x.stat()] as const),
69 );
68 node.hidden = true;
69 const filesWithStats = await queue.run({
70 cores,
71 async run() {
72 node.hidden = false;
73 await tmp.delete({ recursive: true, force: true });
74 await tmp.makeOrEmptyDir();
75 await producer(tmp);
76 const sha1queue = new queue.PriorityQueue(cores);
77 return await Promise.all(
78 (await tmp.readDir({ recursive: true }))
79 .filter((path) => !path.base.startsWith("tmp."))
80 .map(async (path) =>
81 sha1queue.run({
82 async run() {
83 using _ = node.start("hash " + path.relative(tmp));
84 const stats = await path.stat();
85 if (stats.isDirectory()) {
86 return [path, stats.size, null] as const;
87 }
88 const hash = await new Promise<string>(
89 (resolve, reject) => {
90 const reader = fs.createReadStream(path.toString());
91 reader.on("error", reject);
92
93 const hasher = crypto.createHash("sha1").setEncoding(
94 "hex",
95 );
96 hasher.on("error", reject);
97 hasher.on("readable", () => resolve(hasher.read()));
98
99 reader.pipe(hasher);
100 },
101 );
102 return [path, stats.size, hash] as const;
103 },
104 })
105 ),
106 );
107 },
108 });
70109 db.node.exec("BEGIN");
71110 try {
72111 root = insertRootQuery.getNonNull({ key, date: Date.now() }).id;
73 for (const [path, stats] of filesWithStats) {
112 for (const [path, size, hash] of filesWithStats) {
113 if (hash === null) continue;
74114 insertFileQuery.run({
75115 root,
76116 file: path.relative(tmp),
77 size: stats.size,
117 size,
118 hash,
78119 });
79120 }
80121 } catch (e) {
......@@ -91,7 +132,7 @@ export async function produce(
91132 ongoing.delete(key);
92133 }
93134 }}
94 ASSERT(root);
135 ASSERT(root != null);
95136 insertRefQuery.run({ root, file: file.id });
96137 return {
97138 [Symbol.dispose]: () => {
......@@ -102,21 +143,23 @@ export async function produce(
102143
103144const getDerivedAssetQuery = db.prepare<
104145 [number, string],
105 { size: number; key: string }
146 { size: number; key: string; hash: string; date: number }
106147>(/* SQL */ `
107 select df.size, rt.key
148 select df.size, df.hash, rt.date, rt.key
108149 from derived_refs dr
109150 join derived_files df on df.root = dr.root
110151 join derived_roots rt on df.root = rt.id
111 where dr.file = ? and df.file like ?
152 where dr.file = ? and df.file = ?
112153`);
113154
114155export function get(file: MediaFile, subPath: string) {
115156 const row = getDerivedAssetQuery.get(file.id, subPath) ?? null;
116157 if (row) {
117158 return {
118 path: Path.resolve(derivedFileRoot).join(row.key, subPath),
159 path: row.key + "/" + subPath,
119160 size: row.size,
161 hash: row.hash,
162 date: new Date(row.date),
120163 };
121164 }
122165 return null;
......@@ -134,9 +177,9 @@ const getRootQuery = db.prepare<[{ key: string }], { id: number }>(/* SQL */ `
134177`);
135178
136179const insertFileQuery = db.prepare<
137 [{ file: string; root: number; size: number }]
180 [{ file: string; root: number; size: number; hash: string }]
138181>(/* SQL */ `
139 insert into derived_files (file, root, size) values ($file, $root, $size);
182 insert into derived_files (file, root, size, hash) values ($file, $root, $size, $hash);
140183`);
141184
142185const insertRefQuery = db.prepare<[{ root: number; file: number }]>(/* SQL */ `
......@@ -154,4 +197,8 @@ const deleteRefQuery = db.prepare<[{ root: number; file: number }]>(/* SQL */ `
154197import { getDb } from "#sitegen/sqlite";
155198import type { MediaFile } from "./MediaFile.ts";
156199import { Path } from "#sitegen/path";
200import * as fs from "node:fs";
201import * as crypto from "node:crypto";
202import * as queue from "lib/queue.ts";
203import * as progress from "lib/log/progress.ts";
157204import { derivedFileRoot } from "../paths.ts";
src/file-viewer/rsync.ts+12-13
......@@ -23,34 +23,35 @@ export const defaultExtraOptions = [
2323
2424export interface SpawnOptions {
2525 args: string[];
26 title: string;
2726 rsync?: string;
28 progress?: Progress;
27 progress: progress.Node;
2928 cwd: string;
3029}
3130
3231export async function spawn(options: SpawnOptions) {
33 const { rsync = "rsync", args, title, cwd } = options;
32 const { rsync = "rsync", args, cwd } = options;
33 using node = options.progress;
3434 const proc = child_process.spawn(rsync, [...defaultExtraOptions, ...args], {
3535 stdio: ["ignore", "pipe", "pipe"],
3636 cwd,
3737 });
3838 const parser = new Parse();
39 const bar = options.progress ?? new Progress({ text: title });
4039 let running = true;
4140
4241 const stdoutSplitter = readline.createInterface({ input: proc.stdout });
4342 const stderrSplitter = readline.createInterface({ input: proc.stderr });
4443
44 const title = node.text;
45
4546 const handleLine = (line: string) => {
4647 const result = parser.onLine(line);
4748 if (result.kind === "ignore") {
4849 return;
4950 } else if (result.kind === "log") {
50 console[result.level](result.message);
51 node.log[result.level](result.message);
5152 } else if (result.kind === "count") {
5253 if (!running) return;
53 bar.text = `${result.files} files...`;
54 node.text = `${title} (${result.files} files...)`;
5455 } else if (result.kind === "progress") {
5556 if (!running) return;
5657 const {
......@@ -61,8 +62,8 @@ export async function spawn(options: SpawnOptions) {
6162 currentFile,
6263 speed,
6364 } = result;
64 bar.value = transferNumber;
65 bar.total = totalFiles;
65 node.value = transferNumber;
66 node.estimate = totalFiles;
6667 const extras = [
6768 formatSize(bytesTransferred),
6869 (totalFiles > filesToCheck)
......@@ -73,8 +74,8 @@ export async function spawn(options: SpawnOptions) {
7374 const fileName = currentFile.length > 20
7475 ? `${currentFile.slice(0, 3)}..${currentFile.slice(-15)}`
7576 : currentFile;
76 bar.text = `[${transferNumber}/${totalFiles}] ${fileName} ${
77 extras.length > 0 ? `(${extras})` : ""
77 node.text = `${title} - ${fileName}${
78 extras.length > 0 ? `( ${extras})` : ""
7879 }`;
7980 } else result satisfies never;
8081 };
......@@ -90,10 +91,8 @@ export async function spawn(options: SpawnOptions) {
9091 e.args = [rsync, ...args].join(" ");
9192 e.code = code;
9293 e.signal = signal;
93 bar.error(e.message);
9494 return e;
9595 }
96 bar.success(title);
9796}
9897
9998export class Parse {
......@@ -184,5 +183,5 @@ export class Parse {
184183import * as child_process from "node:child_process";
185184import * as readline from "node:readline";
186185import events from "node:events";
187import { Progress } from "@paperclover/console/Progress";
186import * as progress from "lib/log/progress.ts";
188187import { formatSize } from "@/file-viewer/format.ts";
src/file-viewer/transcode-rules.ts+65-58
......@@ -116,24 +116,23 @@ export async function getVideoInputArgs(path: Path): Promise<InputArgs> {
116116 ],
117117 audio: audioSrc
118118 ? ["-i", String(audioSrc)]
119 : (await ffmpeg.probeAudioStreams(path))
119 : (await ffmpeg.probeStreams(path)).audio
120120 ? ["-i", path.toString()]
121121 : null,
122122 };
123123 }
124124 }
125 const streams = await ffmpeg.probeStreams(path);
125126 return {
126127 both: ["-i", path.toString()],
127 video: ["-i", path.toString()],
128 audio: (await ffmpeg.probeAudioStreams(path))
129 ? ["-i", path.toString()]
130 : null,
128 video: streams.video ? ["-i", path.toString()] : null,
129 audio: streams.audio ? ["-i", path.toString()] : null,
131130 };
132131}
133132
134133export interface InputArgs {
135 video: string[];
136134 both: string[];
135 video: string[] | null;
137136 audio: string[] | null;
138137}
139138
......@@ -181,6 +180,8 @@ export function getOpusAudioArgs(
181180 const { audioKbit } = opus;
182181 const cmd = [...audioInputArgs];
183182
183 cmd.push("-vn");
184
184185 cmd.push("-c:a", "libopus");
185186 cmd.push("-b:a", audioKbit + "k");
186187
......@@ -191,27 +192,21 @@ export function getOpusAudioArgs(
191192}
192193
193194export function getMpegDashArgs(
194 videos: Array<Path>,
195 audios: Array<Path>,
195 videos: Path[],
196 audios: Path[],
196197 outDir: Path,
197198) {
198199 const cmd = [];
199200
200201 // inputs
201 videos.forEach((file) => cmd.push("-i", file.toString()));
202 audios.forEach((file) => cmd.push("-i", file.toString()));
202 videos.forEach((v) => cmd.push("-i", v.toString()));
203 audios.forEach((a) => cmd.push("-i", a.toString()));
203204
204205 // map streams
205 videos.forEach((_, i) => {
206 cmd.push("-map", `${i}:0`);
207 });
208 audios.forEach((_, i) => {
209 cmd.push("-map", `${videos.length + i}:0`);
210 });
211
212 // copy codecs
213 cmd.push("-c", "copy");
206 videos.forEach((_, i) => cmd.push("-map", `${i}:0`));
207 audios.forEach((_, i) => cmd.push("-map", `${videos.length + i}:0`));
214208
209 cmd.push("-c", "copy");
215210 cmd.push("-f", "dash");
216211 cmd.push("-seg_duration", "4");
217212 cmd.push("-init_seg_name", "d/$RepresentationID$.init.$ext$");
......@@ -219,11 +214,12 @@ export function getMpegDashArgs(
219214 cmd.push("-use_timeline", "1");
220215 cmd.push("-use_template", "1");
221216
222 // build adaptation sets string
223 const videoAdaptationSets = videos.map((_, i) => `id=${i},streams=${i}`)
217 // calculate actual dimensions per video
218 const videoAdaptationSets = videos
219 .map((_, i) => `id=${i},streams=${i}`)
224220 .join(" ");
225 let adaptationSets = videoAdaptationSets;
226221
222 let adaptationSets = videoAdaptationSets;
227223 if (audios.length > 0) {
228224 const audioId = videos.length;
229225 const audioIndices = audios.map((_, i) => videos.length + i).join(",");
......@@ -231,7 +227,6 @@ export function getMpegDashArgs(
231227 }
232228
233229 cmd.push("-adaptation_sets", adaptationSets);
234
235230 cmd.push("-y");
236231 cmd.push(outDir.join("dash.mpd").toString());
237232
......@@ -257,53 +252,65 @@ export function getH264HlsArgs(
257252) {
258253 const cmd = [...inputArgs.both];
259254
260 // map streams
261 hlsPresets.forEach(() => {
262 cmd.push("-map", "0:v:0");
263 if (inputArgs.audio) {
264 cmd.push("-map", "0:a:0");
265 }
266 });
255 if (inputArgs.audio) {
256 // with audio: create v+a pairs for each preset
257 hlsPresets.forEach(() => {
258 cmd.push("-map", "0:v:0", "-map", "0:a:0");
259 });
267260
268 // encode each variant
269 hlsPresets.forEach((preset, i) => {
270 if (inputArgs.audio) {
261 hlsPresets.forEach((preset, i) => {
271262 const vIdx = i * 2;
272263 const aIdx = i * 2 + 1;
273 cmd.push(`-c:v:${vIdx}`, "libx264");
274 cmd.push(`-b:v:${vIdx}`, preset.videoBitrate);
275 cmd.push(`-c:a:${aIdx}`, "aac");
276 cmd.push(`-b:a:${aIdx}`, preset.audioBitrate);
277 cmd.push(`-filter:v:${vIdx}`, `scale=-2:min(${preset.maxHeight}\\,ih)`);
278 } else {
279 cmd.push(`-c:v:${i}`, "libx264");
280 cmd.push(`-b:v:${i}`, preset.videoBitrate);
281 cmd.push(`-filter:v:${i}`, `scale=-2:min(${preset.maxHeight}\\,ih)`);
282 }
283 });
284
285 cmd.push("-f", "hls");
286 cmd.push("-hls_time", "2");
287 cmd.push("-hls_list_size", "0");
288 cmd.push("-hls_segment_type", "fmp4");
289 cmd.push("-hls_allow_cache", "1");
264 // deno-fmt-ignore
265 cmd.push(
266 `-c:${vIdx}`, "libx264",
267 `-b:v:${vIdx}`, preset.videoBitrate,
268 `-filter:${vIdx}`, `scale=-2:min(${preset.maxHeight}\\,ih)`,
269 `-c:${aIdx}`, "aac",
270 `-b:a:${aIdx}`, preset.audioBitrate,
271 );
272 });
290273
291 if (inputArgs.audio) {
292274 cmd.push(
293275 "-var_stream_map",
294 hlsPresets.map((_, i) => `v:${i * 2},a:${i * 2 + 1}`).join(" "),
276 hlsPresets.map((_, i) => `v:${i},a:${i}`).join(" "),
295277 );
296278 } else {
297 cmd.push("-var_stream_map", hlsPresets.map((_, i) => `v:${i}`).join(" "));
298 }
279 // video only: single stream per preset
280 hlsPresets.forEach(() => {
281 cmd.push("-map", "0:v:0");
282 });
283
284 hlsPresets.forEach((preset, i) => {
285 // deno-fmt-ignore
286 cmd.push(
287 `-c:v:${i}`, "libx264",
288 `-b:v:${i}`, preset.videoBitrate,
289 `-filter:v:${i}`, `scale=-2:min(${preset.maxHeight}\\,ih)`,
290 );
291 });
299292
300 cmd.push("-master_pl_name", "master.m3u8");
301 cmd.push("-hls_segment_filename", "hls.%v.%03d.ts");
293 cmd.push(
294 "-var_stream_map",
295 hlsPresets.map((_, i) => `v:${i}`).join(" "),
296 );
297 }
302298
303 cmd.push("-y");
304 cmd.push(outDir.join("hls.%v.m3u8").toString());
299 // deno-fmt-ignore
300 cmd.push(
301 "-f", "hls",
302 "-hls_time", "2",
303 "-hls_list_size", "0",
304 "-hls_segment_type", "fmp4",
305 "-hls_allow_cache", "1",
306 "-master_pl_name", "master.m3u8",
307 "-hls_segment_filename", "hls.%v.%03d.ts",
308 "-y",
309 outDir.join("hls.%v.m3u8").toString(),
310 );
305311
306312 return cmd;
307313}
314
308315import type { Path } from "#sitegen/path";
309316import * as ffmpeg from "./ffmpeg.ts";
src/file-viewer/views/clofi.tsx+9-30
......@@ -71,7 +71,6 @@ export default function MediaList({
7171 hasCotyledonCookie: boolean;
7272}) {
7373 addScript("./clofi.client.ts");
74 addScript("@/tags/hls-polyfill.client.ts");
7574
7675 const dirs: MediaFile[] = [];
7776 let dir: MediaFile | null = file;
......@@ -356,10 +355,8 @@ function DirView({
356355 />
357356 );
358357 }
359 const isCotyledon = parseInt(dir.path.split("/")?.[1]) < 2025;
360 const unsortedFiles = dir
361 .getPublicChildren()
362 .filter((f) => !f.basenameWithoutExt.startsWith("_unlisted"));
358 const isCotyledon = parseInt(UNWRAP(dir.path.split("/")[1])) < 2025;
359 const unsortedFiles = dir.getPublicChildren();
363360 const sorter = dir.dirsort
364361 ? sorterFromDirSort(dir.dirsort)
365362 : isNumericallyOrdered(unsortedFiles)
......@@ -639,31 +636,19 @@ function TextView({
639636 siblingFiles?: MediaFile[];
640637}) {
641638 const contents = file.contents;
642
643639 if (file.path.startsWith("/2021/phoenix-write/maps")) {
644640 const basename = file.basename;
645641 if (basename.includes("map") && basename.endsWith(".txt")) {
646642 return (
647 <pre
648 style="white-space:normal;max-width:73ch"
649 dangerouslySetInnerHTML={{
650 __html: highlightHashComments(contents),
651 }}
652 ></pre>
643 <pre style="white-space:normal;max-width:73ch">{render.raw(highlightHashComments(contents))}</pre>
653644 );
654645 }
655646 }
656647
657648 return (
658 <pre
659 style="white-space:pre-wrap;max-width:70ch"
660 dangerouslySetInnerHTML={{
661 __html: highlightLinksInTextView(
662 contents,
663 siblingFiles.filter((f) => f.kind === MediaFileKind.file),
664 ),
665 }}
666 ></pre>
649 <pre style={{ "white-space": "pre-wrap", "max-width": "70ch" }}>
650 {render.raw(highlightLinksInTextView( contents, siblingFiles.filter((f) => f.kind === MediaFileKind.file)))}
651 </pre>
667652 );
668653}
669654
......@@ -673,10 +658,8 @@ function ChatView({ file }: { file: MediaFile }) {
673658 <div
674659 style="max-width:70ch"
675660 class="convo"
676 dangerouslySetInnerHTML={{
677 __html: highlightConvo(contents),
678 }}
679661 >
662 {render.raw(highlightConvo(contents))}
680663 </div>
681664 );
682665}
......@@ -690,12 +673,7 @@ function CodeView({ file }: { file: MediaFile }) {
690673 return <DownloadViewCodeNotComputed file={file} />;
691674 }
692675 return (
693 <pre
694 style="white-space:pre-wrap"
695 dangerouslySetInnerHTML={{
696 __html: highlightedContents,
697 }}
698 />
676 <pre style="white-space:pre-wrap">{render.raw(highlightedContents)}</pre>
699677 );
700678}
701679
......@@ -957,3 +935,4 @@ import {
957935import { ForEveryone } from "@/file-viewer/cotyledon.tsx";
958936import * as mime from "#sitegen/mime";
959937import * as sort from "../sort.ts";
938import * as render from "#engine/render";
src/pages/in-the-summer.css created+11
......@@ -0,0 +1,11 @@
1body {
2 background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9IiM3NEM3ODQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik00My43NSAyMDcuODEyQzQzLjc1IDE5OC4zMiAzNi4wNTQ5IDE5MC42MjUgMjYuNTYyNSAxOTAuNjI1QzE3LjA3MDEgMTkwLjYyNSA5LjM3NSAxOTguMzIgOS4zNzUgMjA3LjgxMkM5LjM3NSAyMTcuMzA1IDE3LjA3MDEgMjI1IDI2LjU2MjUgMjI1VjIyOC4xMjVDMTUuMzQ0MiAyMjguMTI1IDYuMjUgMjE5LjAzMSA2LjI1IDIwNy44MTJDNi4yNSAxOTYuNTk0IDE1LjM0NDIgMTg3LjUgMjYuNTYyNSAxODcuNUMzNy43ODA4IDE4Ny41IDQ2Ljg3NSAxOTYuNTk0IDQ2Ljg3NSAyMDcuODEyQzQ2Ljg3NSAyMTkuMDMxIDM3Ljc4MDggMjI4LjEyNSAyNi41NjI1IDIyOC4xMjVWMjI1QzM2LjA1NDkgMjI1IDQzLjc1IDIxNy4zMDUgNDMuNzUgMjA3LjgxMlpNMTQzLjc1IDE4NS45MzhDMTQzLjc1IDE3Ni40NDUgMTM2LjA1NSAxNjguNzUgMTI2LjU2MiAxNjguNzVDMTE3LjA3IDE2OC43NSAxMDkuMzc1IDE3Ni40NDUgMTA5LjM3NSAxODUuOTM4QzEwOS4zNzUgMTk1LjQzIDExNy4wNyAyMDMuMTI1IDEyNi41NjIgMjAzLjEyNVYyMDYuMjVDMTE1LjM0NCAyMDYuMjUgMTA2LjI1IDE5Ny4xNTYgMTA2LjI1IDE4NS45MzhDMTA2LjI1IDE3NC43MTkgMTE1LjM0NCAxNjUuNjI1IDEyNi41NjIgMTY1LjYyNUMxMzcuNzgxIDE2NS42MjUgMTQ2Ljg3NSAxNzQuNzE5IDE0Ni44NzUgMTg1LjkzOEMxNDYuODc1IDE5Ny4xNTYgMTM3Ljc4MSAyMDYuMjUgMTI2LjU2MiAyMDYuMjVWMjAzLjEyNUMxMzYuMDU1IDIwMy4xMjUgMTQzLjc1IDE5NS40MyAxNDMuNzUgMTg1LjkzOFpNMTI4LjEyNSAxMDQuNjg4QzEyOC4xMjUgOTUuMTk1MSAxMjAuNDMgODcuNSAxMTAuOTM4IDg3LjVDMTAxLjQ0NSA4Ny41IDkzLjc1IDk1LjE5NTEgOTMuNzUgMTA0LjY4OEM5My43NSAxMTQuMTggMTAxLjQ0NSAxMjEuODc1IDExMC45MzggMTIxLjg3NVYxMjVDOTkuNzE5MiAxMjUgOTAuNjI1IDExNS45MDYgOTAuNjI1IDEwNC42ODhDOTAuNjI1IDkzLjQ2OTIgOTkuNzE5MiA4NC4zNzUgMTEwLjkzOCA4NC4zNzVDMTIyLjE1NiA4NC4zNzUgMTMxLjI1IDkzLjQ2OTIgMTMxLjI1IDEwNC42ODhDMTMxLjI1IDExNS45MDYgMTIyLjE1NiAxMjUgMTEwLjkzOCAxMjVWMTIxLjg3NUMxMjAuNDMgMTIxLjg3NSAxMjguMTI1IDExNC4xOCAxMjguMTI1IDEwNC42ODhaTTU2LjI1IDg5LjA2MjVDNTYuMjUgNzkuNTcwMSA0OC41NTQ5IDcxLjg3NSAzOS4wNjI1IDcxLjg3NUMyOS41NzAxIDcxLjg3NSAyMS44NzUgNzkuNTcwMSAyMS44NzUgODkuMDYyNUMyMS44NzUgOTguNTU0OSAyOS41NzAxIDEwNi4yNSAzOS4wNjI1IDEwNi4yNVYxMDkuMzc1QzI3Ljg0NDIgMTA5LjM3NSAxOC43NSAxMDAuMjgxIDE4Ljc1IDg5LjA2MjVDMTguNzUgNzcuODQ0MiAyNy44NDQyIDY4Ljc1IDM5LjA2MjUgNjguNzVDNTAuMjgwOCA2OC43NSA1OS4zNzUgNzcuODQ0MiA1OS4zNzUgODkuMDYyNUM1OS4zNzUgMTAwLjI4MSA1MC4yODA4IDEwOS4zNzUgMzkuMDYyNSAxMDkuMzc1VjEwNi4yNUM0OC41NTQ5IDEwNi4yNSA1Ni4yNSA5OC41NTQ5IDU2LjI1IDg5LjA2MjVaTTE5MC42MjUgMjMuNDM3NUMxOTAuNjI1IDEzLjk0NTEgMTgyLjkzIDYuMjUgMTczLjQzOCA2LjI1QzE2My45NDUgNi4yNSAxNTYuMjUgMTMuOTQ1MSAxNTYuMjUgMjMuNDM3NUMxNTYuMjUgMzIuOTI5OSAxNjMuOTQ1IDQwLjYyNSAxNzMuNDM4IDQwLjYyNVY0My43NUMxNjIuMjE5IDQzLjc1IDE1My4xMjUgMzQuNjU1OCAxNTMuMTI1IDIzLjQzNzVDMTUzLjEyNSAxMi4yMTkyIDE2Mi4yMTkgMy4xMjUgMTczLjQzOCAzLjEyNUMxODQuNjU2IDMuMTI1IDE5My43NSAxMi4yMTkyIDE5My43NSAyMy40Mzc1QzE5My43NSAzNC42NTU4IDE4NC42NTYgNDMuNzUgMTczLjQzOCA0My43NVY0MC42MjVDMTgyLjkzIDQwLjYyNSAxOTAuNjI1IDMyLjkyOTkgMTkwLjYyNSAyMy40Mzc1Wk00My43NSA3LjgxMjVDNDMuNzUgLTEuNjc5ODkgMzYuMDU0OSAtOS4zNzUgMjYuNTYyNSAtOS4zNzVDMTcuMDcwMSAtOS4zNzUgOS4zNzUgLTEuNjc5ODkgOS4zNzUgNy44MTI1QzkuMzc1IDE3LjMwNDkgMTcuMDcwMSAyNSAyNi41NjI1IDI1VjI4LjEyNUMxNS4zNDQyIDI4LjEyNSA2LjI1IDE5LjAzMDggNi4yNSA3LjgxMjVDNi4yNSAtMy40MDU3OCAxNS4zNDQyIC0xMi41IDI2LjU2MjUgLTEyLjVDMzcuNzgwOCAtMTIuNSA0Ni44NzUgLTMuNDA1NzggNDYuODc1IDcuODEyNUM0Ni44NzUgMTkuMDMwOCAzNy43ODA4IDI4LjEyNSAyNi41NjI1IDI4LjEyNVYyNUMzNi4wNTQ5IDI1IDQzLjc1IDE3LjMwNDkgNDMuNzUgNy44MTI1Wk0xNDMuNzUgLTE0LjA2MjVDMTQzLjc1IC0yMy41NTQ5IDEzNi4wNTUgLTMxLjI1IDEyNi41NjIgLTMxLjI1QzExNy4wNyAtMzEuMjUgMTA5LjM3NSAtMjMuNTU0OSAxMDkuMzc1IC0xNC4wNjI1QzEwOS4zNzUgLTQuNTcwMTEgMTE3LjA3IDMuMTI1IDEyNi41NjIgMy4xMjVWNi4yNUMxMTUuMzQ0IDYuMjUgMTA2LjI1IC0yLjg0NDIyIDEwNi4yNSAtMTQuMDYyNUMxMDYuMjUgLTI1LjI4MDggMTE1LjM0NCAtMzQuMzc1IDEyNi41NjIgLTM0LjM3NUMxMzcuNzgxIC0zNC4zNzUgMTQ2Ljg3NSAtMjUuMjgwOCAxNDYuODc1IC0xNC4wNjI1QzE0Ni44NzUgLTIuODQ0MjIgMTM3Ljc4MSA2LjI1IDEyNi41NjIgNi4yNVYzLjEyNUMxMzYuMDU1IDMuMTI1IDE0My43NSAtNC41NzAxMSAxNDMuNzUgLTE0LjA2MjVaIi8+CjxwYXRoIGQ9Ik0xMzMuOTE3IDI5LjIyOTdMNzQuNTQyMiA4OC42MDQ3TDcyLjMzMjggODYuMzk1M0wxMzEuNzA4IDI3LjAyMDNMMTMzLjkxNyAyOS4yMjk3WiIvPgo8cGF0aCBkPSJNNDEuNzI5NyAxMzguNjA1TC0xNy42NDUzIDE5Ny45OEwtMTkuODU0NyAxOTUuNzdMMzkuNTIwMyAxMzYuMzk1TDQxLjcyOTcgMTM4LjYwNVoiLz4KPHBhdGggZD0iTTE1MS4xMDUgMTEwLjQ4TDkxLjcyOTcgMTY5Ljg1NUw4OS41MjAzIDE2Ny42NDVMMTQ4Ljg5NSAxMDguMjdMMTUxLjEwNSAxMTAuNDhaIi8+CjxwYXRoIGQ9Ik0yMTAuNDggMTEwLjQ4TDE1MS4xMDUgMTY5Ljg1NUwxNDguODk1IDE2Ny42NDVMMjA4LjI3IDEwOC4yN0wyMTAuNDggMTEwLjQ4WiIvPgo8cGF0aCBkPSJNMTAuNDc5NyAxMTAuNDhMLTQ4Ljg5NTMgMTY5Ljg1NUwtNTEuMTA0NyAxNjcuNjQ1TDguMjcwMjYgMTA4LjI3TDEwLjQ3OTcgMTEwLjQ4WiIvPgo8cGF0aCBkPSJNOTcuOTc5NyAtMTEuMzk1M0wzOC42MDQ3IDQ3Ljk3OTdMMzYuMzk1MyA0NS43NzAzTDk1Ljc3MDMgLTEzLjYwNDdMOTcuOTc5NyAtMTEuMzk1M1oiLz4KPHBhdGggZD0iTTk3Ljk3OTcgMTg4LjYwNUwzOC42MDQ3IDI0Ny45OEwzNi4zOTUzIDI0NS43N0w5NS43NzAzIDE4Ni4zOTVMOTcuOTc5NyAxODguNjA1WiIvPgo8cGF0aCBkPSJNMTMuNjA0NyAzOC42MDQ3TC00NS43NzAzIDk3Ljk3OTdMLTQ3Ljk3OTcgOTUuNzcwM0wxMS4zOTUzIDM2LjM5NTNMMTMuNjA0NyAzOC42MDQ3WiIvPgo8cGF0aCBkPSJNMjQxLjczIDEzOC42MDVMMTgyLjM1NSAxOTcuOThMMTgwLjE0NSAxOTUuNzdMMjM5LjUyIDEzNi4zOTVMMjQxLjczIDEzOC42MDVaIi8+CjxwYXRoIGQ9Ik0yMTMuNjA1IDM4LjYwNDdMMTU0LjIzIDk3Ljk3OTdMMTUyLjAyIDk1Ljc3MDNMMjExLjM5NSAzNi4zOTUzTDIxMy42MDUgMzguNjA0N1oiLz4KPHBhdGggZD0iTTgxLjY5NzQgMTE0LjM0NUw1MS41NDg2IDEyMS40MDdMNzIuMTc0IDE0NC4xMDhMODEuNjk3NCAxMTQuMzQ1WiIvPgo8cGF0aCBkPSJNNjMuOTUyOCAxNTYuNzczTDQyLjEwNjggMTc4LjcxOEw3MS42MjAyIDE4Ny4wNjhMNjMuOTUyOCAxNTYuNzczWiIvPgo8cGF0aCBkPSJNODAuOTQyIDE1LjYyNUw4Ni44MzMzIDQ2LjAyNDNMMTEwLjMxNSAyNi4yOTIxTDgwLjk0MiAxNS42MjVaIi8+CjxwYXRoIGQ9Ik0xMjUuNDE0IDU5LjYzOTVMMTQ4LjM0OSA4MC40NDM1TDE1NS4zMiA1MC41NzQ1TDEyNS40MTQgNTkuNjM5NVoiLz4KPHBhdGggZD0iTS0yMC43MzI4IDEwMi42Nkw5LjU3NTM0IDk2LjMxNjRMLTEwLjUwNDkgNzMuMTMxNUwtMjAuNzMyOCAxMDIuNjZaIi8+CjxwYXRoIGQ9Ik0xNzkuMjY4IDEwMi42NkwyMDkuNTc2IDk2LjMxNjRMMTg5LjQ5NiA3My4xMzE1TDE3OS4yNjggMTAyLjY2WiIvPgo8cGF0aCBkPSJNMTcyLjE1IDE5OC40MDJMMTg4Ljc4OCAxNzIuMjg3TDE1OC4xNzEgMTcwLjQ1M0wxNzIuMTUgMTk4LjQwMloiLz4KPC9zdmc+);
3}
4h1 {
5 margin-bottom: 0;
6 margin-top: 1rem;
7}
8.contain {
9 max-width: 80ch;
10 margin: auto;
11}
src/pages/in-the-summer.marko created+40
......@@ -0,0 +1,40 @@
1export const meta = { title: 'in the summer' };
2export const theme = {
3 bg: '#7DCC8C',
4 fg: '#000',
5 primary: '#9b3706',
6}
7
8import "./in-the-summer.css";
9
10<main style={ 'max-width': '110ch' }>
11<nav><a href="/">back to home</a></nav>
12<div.contain>
13 <header>
14 <h1>in the summer</h1>
15 <time style="margin-left:.5rem;opacity:.5">(2025-08-01)</time>
16 </header>
17
18 <p>
19 this is a song about the summer, and related feelings. written in 2022,
20 produced in 2025. i had a lot of fun playing with a much larger instrument
21 selection, and new 3d workflows. enjoy!
22 </p>
23</div>
24
25<clover-video file="/2025/in the summer/in the summer.mp4">
26 <@header><strong>music video</>: in the summer</>
27</>
28
29<div.contain>
30 <h2>downloads</h2>
31 <ul>
32 <li><a href="/file/2025/in%20the%20summer/in%20the%20summer.mp4?view=dl">in the summer.mp4</a> (AV1, requires modern player)</li>
33 <li><a href="/file/2025/in%20the%20summer/in%20the%20summer.mp3?view=dl">in the summer.mp3</a> (Music)</li>
34 <li><a href="/file/2025/in%20the%20summer/fragments">fragments/</a> (behind the scenes)</li>
35 </ul>
36 <h2>mentions on the q&a</h2>
37 // <QuestionsFor artifact='in-the-summer'/>
38</div>
39</main>
40
src/pages/waterfalls.marko created+47
......@@ -0,0 +1,47 @@
1import "./waterfalls.css";
2
3export const meta = { title: "waterfalls by paper clover" };
4export const theme = {
5 fg: "#fff",
6 bg: "#000",
7};
8
9<main class="waterfalls">
10 <div class="waterfalls_bg1" />
11 <main class="waterfalls">
12 <div class="contain">
13 <h1>waterfalls and gender identity update</h1>
14 <p>
15 this is a song about identity, jealousness, and the seasons. more
16 specifically, it's a personal account of how it felt realizing i was a
17 transgender woman. enjoy.
18 </p>
19 </div>
20 <clover-video
21 file="/2025/waterfalls/waterfalls.mp4"
22 poster="/2025/waterfalls/thumbnail.jpeg"
23 >
24 <@header><strong>music video</strong>: waterfalls</>
25 </>
26 <div class="contain">
27 <p>
28 it's nice to be <em>free</em>.
29 </p>
30 <p>like paper airplanes in the wind.</p>
31 <p>the stars shine in the sky. it's beautiful.</p>
32 <p>
33 love
34 <br />
35 –clover caruso
36 </p>
37 <br />
38
39 <p>
40 <strong>more</strong>: <br />
41 <a href="/file/2025/waterfalls/fragments">fragments</a>
42 <br />
43 <a href="/q+a">q&a page</a>
44 </p>
45 </div>
46 </main>
47</main>
src/pages/waterfalls.tsx deleted-66
......@@ -1,66 +0,0 @@
1import { Video } from "@/tags/Video.tsx";
2
3export const meta = { title: "waterfalls by paper clover" };
4
5export default () => (
6 <main class="waterfalls">
7 <div class="waterfalls_bg1" />
8 <main class="waterfalls">
9 <div class="contain">
10 <h1>waterfalls and gender identity update</h1>
11 <p>
12 this is a song about identity, jealousness, and the seasons. more
13 specifically, it's a personal account of how it felt realizing i was a
14 transgender woman. enjoy.
15 </p>
16 </div>
17 <Video
18 title={
19 <>
20 <strong>music video</strong>: waterfalls
21 </>
22 }
23 width={1920}
24 height={1080}
25 poster="/file/2025/waterfalls/thumbnail.jpeg"
26 posterHash="hMM%+pl^]eOvxCXU$$Ew4TB~LceBsAacSiOq3*F;MevmthFf+]$*WM]%E2KybarEt7NIJ3K$J8r=igxbTERP"
27 sources={[
28 "/file/2025/waterfalls/stream/hls.m3u8",
29 "/file/2025/waterfalls/waterfalls.webm",
30 ]}
31 downloads={[
32 "/file/2025/waterfalls/waterfalls.mp3",
33 "/file/2025/waterfalls/waterfalls.mp4",
34 "/file/2025/waterfalls/waterfalls.webm",
35 "/file/2025/waterfalls/lyrics.txt",
36 "/file/2025/waterfalls/",
37 ]}
38 />
39 <div class="contain">
40 <p>
41 it's nice to be <em>free</em>.
42 </p>
43 <p>like paper airplanes in the wind.</p>
44 <p>the stars shine in the sky. it's beautiful.</p>
45 <p>
46 love
47 <br />
48 –clover caruso
49 </p>
50 <br />
51
52 <p>
53 <strong>more</strong>: <br />
54 <a href="/file/2025/waterfalls/fragments">fragments</a>
55 <br />
56 <a href="/q+a">q&a page</a>
57 </p>
58 </div>
59 </main>
60 </main>
61);
62
63export const theme = {
64 fg: "#fff",
65 bg: "#000",
66};
src/tags/Video.tsx deleted-58
......@@ -1,58 +0,0 @@
1import "./video.css";
2import * as path from "node:path";
3import { addScript } from "#sitegen";
4import { PrecomputedBlurhash } from "./blurhash.tsx";
5
6export namespace Video {
7 export interface Props {
8 title: string;
9 width: number;
10 height: number;
11 sources: string[];
12 downloads: string[];
13 poster?: string;
14 posterHash?: string;
15 borderless?: boolean;
16 }
17}
18
19export function Video(
20 { title, sources, height, poster, posterHash, width, borderless }:
21 Video.Props,
22) {
23 addScript("./hls-polyfill.client.ts");
24 return (
25 <figure class={`video ${borderless ? "borderless" : ""}`}>
26 <figcaption>{title}</figcaption>
27 {posterHash && <PrecomputedBlurhash hash={posterHash} />}
28 {poster && <img src={poster} alt="waterfalls" />}
29 <video
30 controls
31 preload="none"
32 style={`width:100%;background:transparent;aspect-ratio:${
33 simplifyFraction(width, height)
34 }`}
35 poster="data:null"
36 >
37 {sources.map((src) => (
38 <source
39 src={src}
40 type={contentTypeFromExt(src)}
41 />
42 ))}
43 </video>
44 </figure>
45 );
46}
47export function contentTypeFromExt(src: string) {
48 if (src.endsWith(".m3u8")) return "application/x-mpegURL";
49 if (src.endsWith(".webm")) return "video/webm";
50 if (src.endsWith(".mp4")) return "video/mp4";
51 if (src.endsWith(".ogg")) return "video/ogg";
52 throw new Error("Unknown video extension: " + path.extname(src));
53}
54const gcd = (a: number, b: number): number => b ? gcd(b, a % b) : a;
55function simplifyFraction(n: number, d: number) {
56 const divisor = gcd(n, d);
57 return `${n / divisor}/${d / divisor}`;
58}
src/tags/blurhash.tsx deleted-34
......@@ -1,34 +0,0 @@
1import * as blurhash from "blurhash";
2import sharp from "sharp";
3function decode83(str: string) {
4 return [...str].reduce(
5 (value, char) =>
6 value * 83 +
7 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" //
8 .indexOf(char),
9 0,
10 );
11}
12function getSizeFromHash(hash: string) {
13 const sizeFlag = decode83(hash[0]);
14 const height = ((sizeFlag / 9) | 0) + 1;
15 const width = (sizeFlag % 9) + 1;
16 return { width, height };
17}
18export async function encodeBlurhashPng(hash: string) {
19 const size = getSizeFromHash(hash);
20 const data = blurhash.decode(hash, size.width, size.height);
21 return (await sharp(data, {
22 raw: { width: size.width, height: size.height, channels: 4 },
23 }).png({ colors: size.width * size.height }).toBuffer());
24}
25export async function PrecomputedBlurhash({ hash }: { hash: string }) {
26 const data = await encodeBlurhashPng(hash);
27 return (
28 <span
29 style={`background:url("data:image/png;base64,${
30 data.toString("base64url")
31 }")0px 0px/100% auto;`}
32 />
33 );
34}
src/tags/clover-video.client.ts created+155
......@@ -0,0 +1,155 @@
1// Theoretical minimum browser support: Safari v6, Chrome v23, Firefox v42
2
3// Polyfill for old browsers.
4type PlaybackMode = "av1" | "hls-polyfill" | "hls-native" | "none";
5let hls: Promise<typeof import("hls.js")> | null = null;
6
7Promise.allSettled ??= <T>(promises: Iterable<T | PromiseLike<T>>) =>
8 Promise.all(Array.from(promises, (p) =>
9 Promise.resolve(p)
10 .then(
11 (value) => ({ status: "fulfilled" as const, value }),
12 (reason) => ({ status: "rejected" as const, reason }),
13 )));
14
15decidePlaybackMode().then((mode) =>
16 Array.from(
17 document.querySelectorAll<HTMLElement>(".clover-video"),
18 (container) => hydrateVideo(container, mode),
19 )
20);
21
22function decidePlaybackMode(): Promise<PlaybackMode> {
23 return shouldPlayAv1().catch(() => ({ hardware: false, supported: false }))
24 .then((av1): Awaitable<PlaybackMode> => {
25 // Native AV1 with DASH Polyfill
26 // (Chrome v70, Safari v17 on M3/A17 chip, Firefox v67)
27 if (av1.hardware) return "av1";
28 if (av1.supported) {
29 // Software decoding is too slow to use, unfortunately.
30 // Consider a device like a 2014 two core laptop.
31 console.warn(`implements non-accelerated av1+opus`);
32 } else {
33 console.warn(`does not implement av1+opus`);
34 }
35
36 // Native HLS support (Safari v6)
37 const video = document.querySelector("video")!;
38 if (video.canPlayType("application/x-mpegURL")) {
39 return "hls-native";
40 }
41
42 return (hls ??= import("hls.js"))
43 .then((hls): PlaybackMode => {
44 // Polyfill HLS (Chrome 23, Firefox 42, IE11 on Win8)
45 // TODO: ES modules have a minimum of Chrome 63 or Firefox 60
46 if (hls.default.isSupported()) return "hls-polyfill";
47 // Other browsers
48 return "none";
49 });
50 })
51 .catch((): PlaybackMode => "none")
52 .then((mode) => {
53 console.info(`selected playback mode: ${mode}`);
54 return mode;
55 });
56}
57
58function hydrateVideo(container: HTMLElement, mode: PlaybackMode) {
59 const video = container.querySelector("video")!;
60 let src = video.getAttribute("data-src");
61 if (!src) return;
62 src = `/file${src}`;
63
64 const dashFile = `${src}:/dash.mpd`;
65 const hlsFile = `${src}:/master.m3u8`;
66
67 if (mode === "av1") {
68 console.info(`Playback: av1, opus, mpeg-dash`);
69 import("dashjs").then((dashjs) => {
70 const player = dashjs.MediaPlayer().create();
71 // This proxying is for ManagedMediaSource, exposing the HLS stream for AirPlay
72 let mainSource: HTMLSourceElement | null = null;
73 Object.defineProperty(video, "src", {
74 get: () => false,
75 set: (v) => {
76 if (!mainSource) {
77 mainSource = document.createElement("source");
78 mainSource.src = v;
79 video.appendChild(mainSource);
80 } else {
81 mainSource.src = v;
82 }
83 },
84 });
85 Object.defineProperty(video, "disableRemotePlayback", {
86 get: () => false,
87 set: () => {
88 const source = document.createElement("source");
89 source.src = hlsFile;
90 video.appendChild(source);
91 },
92 });
93 player.initialize(
94 video,
95 dashFile,
96 false, // autoplay
97 0, // start
98 );
99 video.controls = true;
100 });
101 } else if (mode === "hls-native") {
102 const hlsSource = document.createElement("source");
103 hlsSource.src = `${src}:/hls.m3u8`;
104 video.appendChild(hlsSource);
105 video.controls = true;
106 } else if (mode === "hls-polyfill") {
107 hls!.then((hls) => {
108 const player = new hls.default();
109 player.loadSource(hlsFile);
110 player.attachMedia(video);
111 video.controls = true;
112 });
113 } else {
114 video.src = src;
115 video.controls = true;
116 }
117}
118
119declare global {
120 var ManagedMediaSource: typeof MediaSource | null;
121}
122
123function shouldPlayAv1(): Promise<{ hardware: boolean; supported: boolean }> {
124 const MediaSource = globalThis.ManagedMediaSource ?? globalThis.MediaSource;
125 if (!MediaSource.isTypeSupported('audio/webm;codecs="opus"')) {
126 return Promise.resolve({
127 hardware: false,
128 supported: false,
129 });
130 }
131
132 if (navigator.vendor?.includes("Apple")) {
133 const supported = !!MediaSource?.isTypeSupported(
134 'video/mp4;codecs="av01.0.01M.08"',
135 );
136 return Promise.resolve({
137 hardware: supported,
138 supported,
139 });
140 } else {
141 return navigator.mediaCapabilities.decodingInfo({
142 type: "media-source",
143 video: {
144 contentType: 'video/mp4;codecs="av01.0.01M.08"',
145 bitrate: 128_000,
146 width: 1920,
147 height: 1080,
148 framerate: 30,
149 },
150 }).then((info) => ({
151 hardware: !!info.powerEfficient,
152 supported: !!info.supported,
153 }));
154 }
155}
src/tags/clover-video.css created+40
......@@ -0,0 +1,40 @@
1.clover-video {
2 border: 4px solid var(--fg);
3 display: flex;
4 flex-direction: column;
5}
6.clover-video video {
7 background-color: transparent;
8}
9.clover-video .stack {
10 position: relative;
11 overflow: hidden;
12 display: flex;
13 flex-direction: column;
14}
15.clover-video video {
16 width: 100%;
17}
18.clover-video h2 {
19 margin-top: 0;
20}
21.clover-video .fallback {
22 background-color: #222b;
23 color: white;
24 padding: 2rem;
25 position: absolute;
26 left: 0;
27 top: 0;
28 width: 100%;
29 height: 100%;
30}
31.clover-video video:has(~ .fallback > *) {
32 filter: blur(20px);
33}
34.clover-video figcaption {
35 background-color: var(--fg);
36 color: white;
37 width: 100%;
38 margin-top: -1px;
39 padding-bottom: 2px;
40}
src/tags/clover-video.marko created+57
......@@ -0,0 +1,57 @@
1import "./clover-video.css";
2
3export interface Input {
4 file: MediaFile | string;
5 poster?: MediaFile | string;
6 header?: Marko.AttrTag<{ content: Marko.Body }>;
7}
8
9<const/{ header } = input />
10<const/file = typeof UNWRAP(input.file) === 'object'
11 ? input.file
12 : UNWRAP(MediaFile.getByPath(input.file), `No file ${JSON.stringify(input.file)}`) />
13<const/poster = input.poster
14 ? typeof input.poster === 'object'
15 ? input.poster
16 : UNWRAP(MediaFile.getByPath(input.poster), `No file ${JSON.stringify(input.poster)}`)
17 : null />
18
19<const/{ width, height } = file.parseDimensions() />
20
21<figure.clover-video>
22 <figcaption>
23 <if=header><${header.content}/></><else>
24 ${file.basename}
25 </else>
26 </figcaption>
27 <div.stack style=`aspect-ratio:${width}/${height}`>
28 <video
29 data-src=file.path
30 width=1920
31 height=1080
32 poster=poster ? `/file${poster.path}` : null
33 />
34 <noscript.fallback>
35 <h2>scripting disabled!</h2>
36 <p>
37 to play this video, JavaScript must be enabled in your
38 browser. the default video playback behavior is not enabled here
39 as it results in a worse experience on slower internet
40 connections, and <a href="http://caniuse.com/mpeg-dash">MPEG-DASH
41 streaming is not natively supported in any browser</a> without JavaScript.
42 </p>
43 <p>
44 since disabling JavaScript is not the default in any browser,
45 you likely wanted to download the raw video file anyways.
46 those links are available below, but note that you will need
47 an AV1-compatible video player, such as VLC Media Player or
48 QuickTime Player (M3 chip or newer)
49 </p>
50 </noscript>
51 </div>
52</figure>
53
54client import "./clover-video.client.ts";
55
56import { MediaFile } from "@/file-viewer/models/MediaFile.ts";
57
src/tags/hls-polyfill.client.ts deleted-29
......@@ -1,29 +0,0 @@
1import type Hls from "hls.js";
2
3let cachedHls: null | Promise<typeof Hls | false> | typeof Hls | false = null;
4
5const loadHls = () =>
6 cachedHls ?? (cachedHls = (
7 // @ts-ignore
8 +"HLS POLYFILL", import("hls.js")
9 ).then((m, y = m.default) => cachedHls = y.isSupported() && y));
10
11const createVideoPlayer = async (videoElement: HTMLVideoElement) => {
12 const sources = videoElement.querySelectorAll("source");
13 for (let i = 0; i < sources.length; i++) {
14 const { src, type } = sources[i];
15 if (videoElement.canPlayType(type)) {
16 videoElement.src = src;
17 return;
18 }
19
20 if (type === "application/x-mpegURL" && await loadHls()) {
21 const hls = new (cachedHls as typeof Hls)();
22 hls.loadSource(src);
23 hls.attachMedia(videoElement);
24 return;
25 }
26 }
27};
28
29document.querySelectorAll("video").forEach(createVideoPlayer);
src/tags/video.css deleted-24
......@@ -1,24 +0,0 @@
1.video {
2 border: 4px solid var(--fg);
3 display: flex;
4 flex-direction: column;
5 position: relative;
6}
7
8.video > img,
9.video > span {
10 position: absolute;
11 left: 0;
12 top: 0;
13 width: 100%;
14 height: 100%;
15 z-index: -1;
16}
17
18.video figcaption {
19 background-color: var(--fg);
20 color: var(--bg);
21 width: 100%;
22 margin-top: -1px;
23 padding-bottom: 2px;
24}