mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
fix(cli): read info metadata from the root composition (#5299)
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { orientation, durationFromHtml } from "./info.js";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import info, { orientation, durationFromHtml } from "./info.js";
|
||||
|
||||
describe("orientation", () => {
|
||||
it("is landscape when width > height", () => {
|
||||
@@ -31,3 +34,76 @@ describe("durationFromHtml", () => {
|
||||
expect(durationFromHtml(html, 5)).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("info", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "quoted attributes",
|
||||
html: '<div data-composition-id="main" data-width="64" data-height="96" data-duration=".2"></div>',
|
||||
},
|
||||
{
|
||||
name: "whitespace around attribute assignments",
|
||||
html: '<div data-composition-id = "main" data-width = "64" data-height\n= "96" data-duration = ".2"></div>',
|
||||
},
|
||||
{
|
||||
name: "unquoted attributes",
|
||||
html: "<div data-composition-id=main data-width=64 data-height=96 data-duration=.2></div>",
|
||||
},
|
||||
{
|
||||
name: "commented composition before the root",
|
||||
html: `<!-- <div data-composition-id="example" data-width="1920" data-height="1080" data-duration="8"></div> -->
|
||||
<div data-composition-id="main" data-width="64" data-height="96" data-duration=".2"></div>`,
|
||||
},
|
||||
{
|
||||
name: "composition markup inside a script string",
|
||||
html: `<script>const example = '<div data-composition-id="example" data-width="1920" data-height="1080" data-duration="8"></div>';</script>
|
||||
<div data-composition-id="main" data-width="64" data-height="96" data-duration=".2"></div>`,
|
||||
},
|
||||
{
|
||||
name: "explicit root after another composition",
|
||||
html: `<div data-composition-id="other" data-width="1920" data-height="1080" data-duration="8"></div>
|
||||
<div data-composition-id="main" data-root="true" data-width="64" data-height="96" data-duration=".2"></div>`,
|
||||
},
|
||||
])("reports the root metadata with $name", async ({ html }) => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hf-info-"));
|
||||
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
try {
|
||||
writeFileSync(join(dir, "index.html"), html);
|
||||
await info.run?.({ args: { _: [], dir, json: true }, rawArgs: [], cmd: info });
|
||||
|
||||
expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toMatchObject({
|
||||
resolution: "portrait",
|
||||
width: 64,
|
||||
height: 96,
|
||||
duration: 0.2,
|
||||
});
|
||||
} finally {
|
||||
log.mockRestore();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the inferred resolution and timeline duration when root metadata is absent", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hf-info-"));
|
||||
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
try {
|
||||
writeFileSync(
|
||||
join(dir, "index.html"),
|
||||
`<html data-resolution="landscape"><body><div data-composition-id="main">
|
||||
<div class="clip" data-start="1" data-duration="2"></div>
|
||||
</div></body></html>`,
|
||||
);
|
||||
await info.run?.({ args: { _: [], dir, json: true }, rawArgs: [], cmd: info });
|
||||
|
||||
expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toMatchObject({
|
||||
resolution: "landscape",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
duration: 3,
|
||||
});
|
||||
} finally {
|
||||
log.mockRestore();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,11 @@ export const examples: Example[] = [
|
||||
];
|
||||
import { join } from "node:path";
|
||||
import { parseHtml, CANVAS_DIMENSIONS } from "@hyperframes/core";
|
||||
import {
|
||||
findRootCompositionElement,
|
||||
readCompositionSize,
|
||||
} from "@hyperframes/core/runtime/composition-length";
|
||||
import { readDataDurationSeconds } from "@hyperframes/parsers/media-duration";
|
||||
import { c } from "../ui/colors.js";
|
||||
import { formatBytes, label } from "../ui/format.js";
|
||||
import { ensureDOMParser } from "../utils/dom.js";
|
||||
@@ -26,11 +31,16 @@ export function orientation(width: number, height: number): "landscape" | "portr
|
||||
* fall back to the computed timeline end.
|
||||
*/
|
||||
export function durationFromHtml(html: string, fallback: number): number {
|
||||
const match =
|
||||
html.match(/data-composition-id[^>]*data-duration=["']([\d.]+)["']/) ||
|
||||
html.match(/data-duration=["']([\d.]+)["'][^>]*data-composition-id/);
|
||||
const value = match?.[1] ? parseFloat(match[1]) : NaN;
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
return durationFromRoot(rootFromHtml(html), fallback);
|
||||
}
|
||||
|
||||
function rootFromHtml(html: string): HTMLElement | null {
|
||||
ensureDOMParser();
|
||||
return findRootCompositionElement(new DOMParser().parseFromString(html, "text/html"));
|
||||
}
|
||||
|
||||
function durationFromRoot(root: Element | null, fallback: number): number {
|
||||
return readDataDurationSeconds((name) => root?.getAttribute(name)) ?? fallback;
|
||||
}
|
||||
|
||||
function totalSize(dir: string): number {
|
||||
@@ -56,7 +66,7 @@ export default defineCommand({
|
||||
const project = resolveProject(args.dir);
|
||||
const html = readFileSync(project.indexPath, "utf-8");
|
||||
|
||||
ensureDOMParser();
|
||||
const root = rootFromHtml(html);
|
||||
const parsed = parseHtml(html);
|
||||
|
||||
const tracks = new Set(parsed.elements.map((el) => el.zIndex));
|
||||
@@ -64,18 +74,12 @@ export default defineCommand({
|
||||
(max, el) => Math.max(max, el.startTime + el.duration),
|
||||
0,
|
||||
);
|
||||
// Read actual dimensions from root composition element
|
||||
const widthMatch =
|
||||
html.match(/data-composition-id[^>]*data-width=["'](\d+)["']/) ||
|
||||
html.match(/data-width=["'](\d+)["'][^>]*data-composition-id/);
|
||||
const heightMatch =
|
||||
html.match(/data-composition-id[^>]*data-height=["'](\d+)["']/) ||
|
||||
html.match(/data-height=["'](\d+)["'][^>]*data-composition-id/);
|
||||
const dimensions = root ? readCompositionSize(root) : null;
|
||||
const fallback = CANVAS_DIMENSIONS[parsed.resolution];
|
||||
const width = widthMatch?.[1] ? parseInt(widthMatch[1], 10) : fallback.width;
|
||||
const height = heightMatch?.[1] ? parseInt(heightMatch[1], 10) : fallback.height;
|
||||
const width = dimensions?.width ?? fallback.width;
|
||||
const height = dimensions?.height ?? fallback.height;
|
||||
const resolution = `${width}x${height}`;
|
||||
const duration = durationFromHtml(html, maxEnd);
|
||||
const duration = durationFromRoot(root, maxEnd);
|
||||
const size = totalSize(project.dir);
|
||||
|
||||
const typeCounts: Record<string, number> = {};
|
||||
|
||||
Reference in New Issue
Block a user