@mekuru/core 0.0.0-stage → 0.1.2
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/README.jp.md +18 -0
- package/README.md +17 -2
- package/package.json +19 -4
- package/src/index.ts +5 -0
- package/src/jsx.ts +138 -0
- package/src/write.ts +35 -0
- package/src/xml.ts +194 -0
package/README.jp.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @mekuru/core
|
|
2
|
+
|
|
3
|
+
[English](README.md)
|
|
4
|
+
|
|
5
|
+
Mekuru Movie Maker の台本を TSX で書き、XML に書き出すパッケージです。組み込み要素の型と、Hono の JSX ランタイムの再エクスポートを提供します。
|
|
6
|
+
|
|
7
|
+
Bun 1.4 以上が必要です。
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bun add -d @mekuru/core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- `tsconfig.json` の `jsx` を `react-jsx`、`jsxImportSource` を `@mekuru/core` に設定します。
|
|
14
|
+
- `writeXml(path, video)` で部品を展開して XML を書き出し、`toXml(video)` で XML 文字列を取得できます。
|
|
15
|
+
- 非同期の読み上げ部品は、先に下書きを書き出し、音声合成が終わると更新できます。
|
|
16
|
+
- XML のプレビューと動画への書き出しには [@mekuru/cli](../cli/README.jp.md) を使います。
|
|
17
|
+
|
|
18
|
+
[TSX の書き方](../../docs/reference/Component.jp.md) と [組み込みの要素](../../docs/reference/Syntax.jp.md)
|
package/README.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mekuru/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[日本語](README.jp.md)
|
|
4
|
+
|
|
5
|
+
TSX authoring and XML output for Mekuru Movie Maker. Provides types for built-in elements and re-exports Hono's JSX runtime.
|
|
6
|
+
|
|
7
|
+
Requires Bun 1.4 or later.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bun add -d @mekuru/core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- Set `jsx` to `react-jsx` and `jsxImportSource` to `@mekuru/core` in `tsconfig.json`.
|
|
14
|
+
- Use `writeXml(path, video)` to expand components and write XML, or `toXml(video)` to get an XML string.
|
|
15
|
+
- Async speech components can write an initial draft and update it after synthesis finishes.
|
|
16
|
+
- Use [@mekuru/cli](../cli/README.md) to preview the XML or render a video.
|
|
17
|
+
|
|
18
|
+
See [TSX authoring](../../docs/reference/Component.md) and [built-in elements](../../docs/reference/Syntax.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mekuru/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.2",
|
|
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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "\n": " ", "\r": " ", "\t": "	" })[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
|
+
};
|