diff --git a/packages/editor-ext/src/lib/html-embed/html-embed-codec.spec.ts b/packages/editor-ext/src/lib/html-embed/html-embed-codec.spec.ts
index 917f1d51..f50bec0f 100644
--- a/packages/editor-ext/src/lib/html-embed/html-embed-codec.spec.ts
+++ b/packages/editor-ext/src/lib/html-embed/html-embed-codec.spec.ts
@@ -2,6 +2,8 @@ import { afterEach, describe, expect, it } from "vitest";
import {
encodeHtmlEmbedSource,
decodeHtmlEmbedSource,
+ parseHtmlEmbedHeight,
+ renderHtmlEmbedHeight,
} from "./html-embed";
// Unit coverage for the base64 codec used by the htmlEmbed node's
@@ -118,6 +120,45 @@ describe("html-embed codec — encode failure fallback", () => {
});
});
+describe("html-embed height — parseHtmlEmbedHeight (data-height -> px | null)", () => {
+ it('parses a numeric string ("300" -> 300)', () => {
+ expect(parseHtmlEmbedHeight("300")).toBe(300);
+ });
+
+ it("parses an absent value (null -> null = auto-resize)", () => {
+ expect(parseHtmlEmbedHeight(null)).toBeNull();
+ expect(parseHtmlEmbedHeight("")).toBeNull();
+ });
+
+ it('rejects a non-numeric value ("abc" -> null) — pins the NaN guard (BUG-2)', () => {
+ // Without Number.isFinite this would be NaN (typeof "number"), disabling
+ // auto-resize and yielding an unclamped iframe height downstream.
+ expect(parseHtmlEmbedHeight("abc")).toBeNull();
+ });
+
+ it('parses a trailing-unit value ("120px" -> 120) via parseInt', () => {
+ expect(parseHtmlEmbedHeight("120px")).toBe(120);
+ });
+});
+
+describe("html-embed height — renderHtmlEmbedHeight (px -> data-height | {})", () => {
+ it("renders a fixed height (120 -> { data-height: '120' })", () => {
+ expect(renderHtmlEmbedHeight(120)).toEqual({ "data-height": "120" });
+ });
+
+ it("renders auto-resize as no attribute (null -> {})", () => {
+ expect(renderHtmlEmbedHeight(null)).toEqual({});
+ });
+
+ it("renders 0 as no attribute (0 is auto -> {})", () => {
+ expect(renderHtmlEmbedHeight(0)).toEqual({});
+ });
+
+ it("renders undefined as no attribute (absent -> {})", () => {
+ expect(renderHtmlEmbedHeight(undefined)).toEqual({});
+ });
+});
+
describe("html-embed codec — decode of malformed input (browser branch)", () => {
it("returns '' for input atob rejects (catch branch)", () => {
// atob throws on characters outside the base64 alphabet; the codec catches
diff --git a/packages/editor-ext/src/lib/html-embed/html-embed.ts b/packages/editor-ext/src/lib/html-embed/html-embed.ts
index baa396e1..c0bbfe81 100644
--- a/packages/editor-ext/src/lib/html-embed/html-embed.ts
+++ b/packages/editor-ext/src/lib/html-embed/html-embed.ts
@@ -69,6 +69,30 @@ export function decodeHtmlEmbedSource(encoded: string): string {
}
}
+/**
+ * Parse the `data-height` attribute value into a fixed iframe height in px.
+ *
+ * Returns null (auto-resize) when the value is absent, empty, or non-numeric.
+ * A non-numeric `data-height` (e.g. a crafted/corrupted import) must NOT become
+ * NaN: NaN is typeof "number" and would disable auto-resize and yield an
+ * unclamped iframe height downstream. The Number.isFinite guard pins that fix.
+ */
+export function parseHtmlEmbedHeight(value: string | null): number | null {
+ if (!value) return null;
+ const n = parseInt(value, 10);
+ return Number.isFinite(n) ? n : null;
+}
+
+/**
+ * Render a fixed height back to a `data-height` attribute. A null/0/absent
+ * height means auto-resize, so no attribute is emitted.
+ */
+export function renderHtmlEmbedHeight(
+ height: number | null | undefined,
+): { "data-height": string } | Record {
+ return height ? { "data-height": String(height) } : {};
+}
+
export const HtmlEmbed = Node.create({
name: "htmlEmbed",
inline: false,
@@ -103,17 +127,9 @@ export const HtmlEmbed = Node.create({
// Fixed iframe height in px. null/absent => auto-resize on the client.
height: {
default: null,
- parseHTML: (el) => {
- const v = el.getAttribute("data-height");
- if (!v) return null;
- const n = parseInt(v, 10);
- // A non-numeric data-height (e.g. crafted/corrupted import) must not
- // become NaN: NaN is typeof "number" and would disable auto-resize and
- // yield an unclamped iframe height downstream. Treat it as auto (null).
- return Number.isFinite(n) ? n : null;
- },
+ parseHTML: (el) => parseHtmlEmbedHeight(el.getAttribute("data-height")),
renderHTML: (attrs: HtmlEmbedAttributes) =>
- attrs.height ? { "data-height": String(attrs.height) } : {},
+ renderHtmlEmbedHeight(attrs.height),
},
};
},