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), }, }; },