@mekuru/core 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,21 @@
1
1
  {
2
2
  "name": "@mekuru/core",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "The types of the XML of Mekuru Movie Maker, for writing videos in TSX with jsxImportSource @mekuru/core (the JSX runtime is Hono's); the viewer and the compiler are in @mekuru/cli",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts",
8
+ "./jsx-runtime": "./src/index.ts",
9
+ "./jsx-dev-runtime": "./src/index.ts"
10
+ },
11
+ "files": [
12
+ "src"
13
+ ],
14
+ "sideEffects": false,
15
+ "dependencies": {
16
+ "hono": "^4.13.0"
17
+ },
18
+ "engines": {
19
+ "bun": ">=1.4"
20
+ }
21
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { Fragment, jsx, jsx as jsxs, jsx as jsxDEV } from "hono/jsx/jsx-runtime";
2
+
3
+ export type { Anchor, At, Color, IntrinsicElements, JSX, Length, Node, Time } from "./jsx.ts";
4
+ export { isDraft, waitFor, writeXml } from "./write.ts";
5
+ export { fps, toXml } from "./xml.ts";
package/src/jsx.ts ADDED
@@ -0,0 +1,138 @@
1
+ import type { Child } from "hono/jsx";
2
+ import type { JSX as HonoJSX } from "hono/jsx/jsx-runtime";
3
+
4
+ export type Node = Child;
5
+
6
+ export type Length = number | `${number}%` | "auto";
7
+ type Ratio = number | `${number}%`;
8
+ export type Time = number;
9
+ export type At = number | string;
10
+ export type Color = string;
11
+ export type Anchor = "tl" | "t" | "tr" | "l" | "c" | "r" | "bl" | "b" | "br" | "center" | "top" | "bottom" | "left" | "right";
12
+
13
+ interface Base {
14
+ key?: string | number;
15
+ }
16
+
17
+ interface WithChildren {
18
+ children?: Node;
19
+ }
20
+
21
+ interface Timed {
22
+ id?: string;
23
+ at?: At;
24
+ fadein?: Time;
25
+ fadeout?: Time;
26
+ slot?: string;
27
+ }
28
+
29
+ interface Look {
30
+ z?: number;
31
+ opacity?: Ratio;
32
+ }
33
+
34
+ interface Box {
35
+ x?: Length;
36
+ y?: Length;
37
+ anchor?: Anchor;
38
+ w?: Length;
39
+ h?: Length;
40
+ "min-w"?: Length;
41
+ "max-w"?: Length;
42
+ "min-h"?: Length;
43
+ "max-h"?: Length;
44
+ grow?: boolean | number;
45
+ shrink?: number;
46
+ rotate?: number;
47
+ }
48
+
49
+ interface Deco {
50
+ fill?: Color;
51
+ stroke?: Color;
52
+ "stroke-width"?: number;
53
+ radius?: number;
54
+ }
55
+
56
+ type Align = "start" | "center" | "end" | "stretch";
57
+
58
+ interface Overlay extends Base, Timed, Look, Box, Deco, WithChildren {
59
+ padding?: Length | readonly Length[];
60
+ align?: Align;
61
+ }
62
+
63
+ interface Flow extends Overlay {
64
+ gap?: Length;
65
+ justify?: "start" | "center" | "end" | "between" | "around" | "evenly";
66
+ wrap?: boolean;
67
+ }
68
+
69
+ interface TextStyle {
70
+ size?: number;
71
+ color?: Color;
72
+ font?: string;
73
+ stroke?: Color;
74
+ "stroke-width"?: number;
75
+ "line-height"?: number;
76
+ background?: Color;
77
+ radius?: number;
78
+ padding?: Length | readonly Length[];
79
+ }
80
+
81
+ interface Media {
82
+ src: string;
83
+ from?: Time;
84
+ to?: Time;
85
+ volume?: Ratio;
86
+ children?: Node;
87
+ }
88
+
89
+ export interface IntrinsicElements {
90
+ mekuru: Base &
91
+ WithChildren & {
92
+ size?: `${number}x${number}`;
93
+ fps?: number;
94
+ font?: string;
95
+ preset?: string;
96
+ crf?: number;
97
+ "audio-bitrate"?: string;
98
+ };
99
+ seq: Overlay;
100
+ stack: Overlay;
101
+ row: Flow;
102
+ column: Flow;
103
+ repeat: Base & Timed & Look & WithChildren;
104
+ dur: Base & Timed & Look & WithChildren & { len: Time };
105
+
106
+ text: Base & Timed & Look & Box & TextStyle & WithChildren & { weight?: "normal" | "bold"; align?: "left" | "center" | "right"; pre?: boolean };
107
+ image: Base & Timed & Look & Box & { src: string };
108
+ video: Base & Timed & Look & Omit<Box, "rotate"> & Media;
109
+ audio: Base & Omit<Timed, "slot"> & Omit<Media, "src"> & { src?: string; label?: string; draft?: boolean };
110
+ hold: Base & WithChildren & Look & { at: Time; id?: string; fadein?: Time; fadeout?: Time; slot?: string };
111
+ speed: Base & { from?: Time; to?: Time; rate: number };
112
+ cut: Base & { from?: Time; to?: Time };
113
+ rect: Base & Timed & Look & Box & Deco;
114
+ circle: Base & Timed & Look & Box & Deco;
115
+ line: Base &
116
+ Timed &
117
+ Look & {
118
+ p: string;
119
+ closed?: boolean;
120
+ fill?: Color;
121
+ stroke?: Color;
122
+ "stroke-width"?: number;
123
+ head?: "none" | "start" | "end" | "both";
124
+ dashed?: boolean;
125
+ dotted?: boolean;
126
+ };
127
+ br: Base;
128
+ }
129
+
130
+ export namespace JSX {
131
+ export type Element = HonoJSX.Element;
132
+ export type ElementType = HonoJSX.ElementType;
133
+ export interface ElementChildrenAttribute extends HonoJSX.ElementChildrenAttribute {}
134
+ export interface IntrinsicAttributes extends HonoJSX.IntrinsicAttributes {}
135
+ export interface IntrinsicElements extends IntrinsicElementsBase {}
136
+ }
137
+
138
+ type IntrinsicElementsBase = IntrinsicElements;
package/src/write.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { rename, rm } from "node:fs/promises";
3
+ import { toXml } from "./xml.ts";
4
+
5
+ const output = new AsyncLocalStorage<{ draft: boolean; waits: Set<Promise<unknown>> }>();
6
+
7
+ export const isDraft = (): boolean => output.getStore()?.draft ?? false;
8
+
9
+ export const waitFor = (work: Promise<unknown>): void => {
10
+ work.catch(() => {});
11
+ output.getStore()?.waits.add(work);
12
+ };
13
+
14
+ export const writeXml = async (path: string, video: unknown): Promise<void> => {
15
+ const context = { draft: true, waits: new Set<Promise<unknown>>() };
16
+ await output.run(context, async () => {
17
+ const tmp = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
18
+ const save = async () => {
19
+ const xml = await toXml(video);
20
+ await Bun.write(tmp, xml);
21
+ await rename(tmp, path);
22
+ };
23
+ try {
24
+ await save();
25
+ context.draft = false;
26
+ if (context.waits.size === 0) return;
27
+ await Promise.all(context.waits);
28
+ await save();
29
+ } finally {
30
+ context.draft = false;
31
+ context.waits.clear();
32
+ await rm(tmp, { force: true });
33
+ }
34
+ });
35
+ };
package/src/xml.ts ADDED
@@ -0,0 +1,194 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ type Props = Record<string, unknown>;
4
+
5
+ interface Element {
6
+ tag: string;
7
+ props: Props;
8
+ children: Item[];
9
+ }
10
+ type Item = Element | string;
11
+
12
+ interface JsxNode {
13
+ tag: unknown;
14
+ props: Props;
15
+ children: unknown[];
16
+ }
17
+
18
+ const isJsxNode = (v: unknown): v is JsxNode => typeof v === "object" && v !== null && "tag" in v && "props" in v && Array.isArray((v as JsxNode).children);
19
+
20
+ const describe = (v: unknown): string => (typeof v === "object" && v !== null ? (v.constructor?.name ?? "an object") : typeof v);
21
+
22
+ const rate = new AsyncLocalStorage<number>();
23
+
24
+ export const fps = (): number => {
25
+ const f = rate.getStore();
26
+ if (f === undefined) throw new Error("fps() is known only inside `<mekuru>` while writeXml writes it: call it in a component");
27
+ return f;
28
+ };
29
+
30
+ const frameRate = (v: unknown): number => {
31
+ const f = v === undefined ? 30 : Number(v);
32
+ if (!(f > 0 && f <= 240)) throw new Error(`\`<mekuru fps>\` is frames per second, a number above 0 (not ${String(v)})`);
33
+ return f;
34
+ };
35
+
36
+ const expand = async (node: unknown, out: Item[]): Promise<void> => {
37
+ node = await node;
38
+ if (node === null || node === undefined || typeof node === "boolean") return;
39
+ if (typeof node === "string" || typeof node === "number" || typeof node === "bigint" || node instanceof String) {
40
+ out.push(String(node));
41
+ return;
42
+ }
43
+ if (Array.isArray(node)) {
44
+ for (const n of node) await expand(n, out);
45
+ return;
46
+ }
47
+ if (isJsxNode(node)) {
48
+ const { tag, props, children } = node;
49
+ if (typeof tag === "function") {
50
+ const p = { ...props };
51
+ if (children.length) p.children = children.length === 1 ? children[0] : children;
52
+ return expand(await tag(p), out);
53
+ }
54
+ if (tag === "") {
55
+ for (const c of children) await expand(c, out);
56
+ return;
57
+ }
58
+ if (typeof tag === "string") {
59
+ const el: Element = { tag, props, children: [] };
60
+ const inside = async () => {
61
+ for (const c of children) await expand(c, el.children);
62
+ };
63
+ await (tag === "mekuru" ? rate.run(frameRate(props.fps), inside) : inside());
64
+ out.push(el);
65
+ return;
66
+ }
67
+ }
68
+ throw new Error(`a video is made of elements and text, not ${describe(node)}`);
69
+ };
70
+
71
+ interface X {
72
+ tag: string;
73
+ attrs: [string, string][];
74
+ children: (X | string)[];
75
+ }
76
+
77
+ const TIMES = new Set(["len", "fadein", "fadeout", "from", "to", "delay"]);
78
+ const LENGTHS = new Set(["x", "y", "w", "h", "min-w", "max-w", "min-h", "max-h", "gap"]);
79
+ const RATIOS = new Set(["opacity", "volume"]);
80
+ const COLORS = new Set(["fill", "stroke", "color", "background"]);
81
+
82
+ const NAMED: Record<string, string> = {
83
+ black: "#000000",
84
+ white: "#FFFFFF",
85
+ red: "#FF0000",
86
+ lime: "#00FF00",
87
+ green: "#008000",
88
+ blue: "#0000FF",
89
+ yellow: "#FFFF00",
90
+ cyan: "#00FFFF",
91
+ aqua: "#00FFFF",
92
+ magenta: "#FF00FF",
93
+ fuchsia: "#FF00FF",
94
+ gray: "#808080",
95
+ grey: "#808080",
96
+ silver: "#C0C0C0",
97
+ maroon: "#800000",
98
+ olive: "#808000",
99
+ purple: "#800080",
100
+ teal: "#008080",
101
+ navy: "#000080",
102
+ orange: "#FFA500",
103
+ transparent: "#00000000",
104
+ none: "#00000000",
105
+ };
106
+
107
+ const ANCHORS: Record<string, string> = { center: "c", top: "t", bottom: "b", left: "l", right: "r" };
108
+
109
+ const num = (n: number): string => {
110
+ const s = String(n);
111
+ return /e/i.test(s) && Math.abs(n) < 1 ? n.toFixed(20).replace(/\.?0+$/, "") : s;
112
+ };
113
+
114
+ const frames = (v: unknown): string => (typeof v === "number" ? num(v) : String(v).trim());
115
+
116
+ const length = (v: unknown): string => (typeof v === "number" ? num(v) : String(v));
117
+
118
+ const ratio = (v: unknown): string => {
119
+ if (typeof v === "number") return num(v);
120
+ const s = String(v).trim();
121
+ const pct = /^(-?[\d.]+)%$/.exec(s);
122
+ return pct ? num(Number(pct[1]) / 100) : s;
123
+ };
124
+
125
+ const color = (v: unknown): string => NAMED[String(v).trim().toLowerCase()] ?? String(v).trim();
126
+
127
+ const at = (v: unknown): [string, string] => {
128
+ const s = typeof v === "string" ? v.trim() : v;
129
+ if (typeof s !== "string" || !/^[\p{L}_]/u.test(s)) return ["delay", frames(s)];
130
+ const [id, q] = s.includes(":") ? [s.slice(0, s.indexOf(":")), s.slice(s.indexOf(":") + 1)] : [s, "0"];
131
+ return ["at", `${id.trim()}:${frames(q)}`];
132
+ };
133
+
134
+ const attr = (tag: string, key: string, v: unknown): [string, string] | undefined => {
135
+ if (key === "at") return tag === "hold" ? ["at", frames(v)] : at(v);
136
+ if (typeof v === "boolean") return key === "grow" ? [key, v ? "1" : "0"] : v ? [key, "true"] : undefined;
137
+ if (TIMES.has(key)) return [key, frames(v)];
138
+ if (LENGTHS.has(key)) return [key, length(v)];
139
+ if (RATIOS.has(key)) return [key, ratio(v)];
140
+ if (COLORS.has(key)) return [key, color(v)];
141
+ if (key === "padding") return [key, (Array.isArray(v) ? v : [v]).map(length).join(" ")];
142
+ if (key === "anchor") return [key, ANCHORS[String(v)] ?? String(v)];
143
+ return [key, typeof v === "number" ? num(v) : String(v)];
144
+ };
145
+
146
+ const normalize = (el: Element): X => {
147
+ const x: X = { tag: el.tag, attrs: [], children: [] };
148
+ for (const c of el.children) {
149
+ if (typeof c !== "string") x.children.push(normalize(c));
150
+ else if (el.tag === "text" || c.trim()) x.children.push(c);
151
+ }
152
+ for (const [k, v] of Object.entries(el.props)) {
153
+ if (k === "children" || k === "key" || v === undefined || v === null) continue;
154
+ const a = attr(el.tag, k, v);
155
+ if (a) x.attrs.push(a);
156
+ }
157
+ return x;
158
+ };
159
+
160
+ const escapeXml = (s: string, quote: boolean) =>
161
+ s.replace(
162
+ quote ? /[&<>"\n\r\t]/g : /[&<>]/g,
163
+ (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "\n": "&#10;", "\r": "&#13;", "\t": "&#9;" })[c] ?? c,
164
+ );
165
+
166
+ const head = (x: X) => `<${x.tag}${x.attrs.map(([k, v]) => ` ${k}="${escapeXml(v, true)}"`).join("")}`;
167
+
168
+ const inline = (c: X | string): string =>
169
+ typeof c === "string" ? escapeXml(c, false) : c.children.length ? `${head(c)}>${c.children.map(inline).join("")}</${c.tag}>` : `${head(c)}/>`;
170
+
171
+ const write = (x: X, indent: string, out: string[]) => {
172
+ if (x.children.length === 0 || x.children.some((c) => typeof c === "string")) {
173
+ out.push(`${indent}${inline(x)}`);
174
+ } else {
175
+ out.push(`${indent}${head(x)}>`);
176
+ for (const c of x.children) write(c as X, `${indent} `, out);
177
+ out.push(`${indent}</${x.tag}>`);
178
+ }
179
+ };
180
+
181
+ export const toXml = async (video: unknown): Promise<string> => {
182
+ const top: Item[] = [];
183
+ await expand(video, top);
184
+ const root = top.find((i): i is Element => typeof i !== "string");
185
+ if (top.length !== 1 || !root || root.tag !== "mekuru") throw new Error("writeXml takes a video: one `<mekuru>` element");
186
+ const x: X = { tag: "mekuru", attrs: [], children: [] };
187
+ for (const [k, v] of Object.entries(root.props)) {
188
+ if (k !== "children" && k !== "key" && v !== undefined && v !== null) x.attrs.push([k, typeof v === "number" ? num(v) : String(v)]);
189
+ }
190
+ x.children.push(normalize({ tag: "seq", props: {}, children: root.children }));
191
+ const out: string[] = [];
192
+ write(x, "", out);
193
+ return `${out.join("\n")}\n`;
194
+ };
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.