From 9c159ad96dff2463257fbd27d8324a2350d12040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 20 May 2026 02:36:58 -0400 Subject: [PATCH] fix: escape href in compiler link dedup + add font-link extraction tests Escape href values in querySelector calls for link dedup in both htmlBundler.ts and htmlCompiler.ts to match the runtime path (which uses CSS.escape). Prevents SyntaxError on hrefs containing quotes. Add two tests for inlineSubCompositions font-link extraction: - Verifies elements are extracted with original rel + crossorigin - Verifies dedup across multiple sub-compositions sharing the same font --- packages/core/src/compiler/htmlBundler.ts | 3 +- .../compiler/inlineSubCompositions.test.ts | 66 +++++++++++++++++++ .../producer/src/services/htmlCompiler.ts | 3 +- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/packages/core/src/compiler/htmlBundler.ts b/packages/core/src/compiler/htmlBundler.ts index 3512b4b17..1bcbfbbf6 100644 --- a/packages/core/src/compiler/htmlBundler.ts +++ b/packages/core/src/compiler/htmlBundler.ts @@ -813,7 +813,8 @@ export async function bundleToSingleHtml( } for (const link of compExternalLinks) { - if (!document.querySelector(`link[href="${link.href}"]`)) { + const escapedHref = link.href.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); + if (!document.querySelector(`link[href="${escapedHref}"]`)) { const linkEl = document.createElement("link"); linkEl.setAttribute("rel", link.rel); linkEl.setAttribute("href", link.href); diff --git a/packages/core/src/compiler/inlineSubCompositions.test.ts b/packages/core/src/compiler/inlineSubCompositions.test.ts index 7e91f165e..da71c2afe 100644 --- a/packages/core/src/compiler/inlineSubCompositions.test.ts +++ b/packages/core/src/compiler/inlineSubCompositions.test.ts @@ -131,6 +131,72 @@ describe("inlineSubCompositions – #ID selector scoping divergence", () => { expect(scopedCss).toContain('[data-hf-authored-id="intro"]'); }); + it("extracts elements from sub-composition with original rel and crossorigin", () => { + const subCompWithLinks = ` + + + + + +
+ Hello +
+`; + + const document = makeHostDocument("captions"); + const host = document.querySelector('[data-composition-src="intro.html"]')!; + + const result = inlineSubCompositions(document, [host], { + resolveHtml: () => subCompWithLinks, + parseHtml: (html) => parseHTML(html).document, + }); + + expect(result.externalLinks).toHaveLength(3); + expect(result.externalLinks[0]).toEqual({ + href: "https://fonts.googleapis.com", + rel: "preconnect", + crossorigin: undefined, + }); + expect(result.externalLinks[1]).toEqual({ + href: "https://fonts.gstatic.com", + rel: "preconnect", + crossorigin: "", + }); + expect(result.externalLinks[2]).toEqual({ + href: "https://fonts.googleapis.com/css2?family=Montserrat:wght@800&display=swap", + rel: "stylesheet", + crossorigin: undefined, + }); + }); + + it("deduplicates link hrefs across multiple sub-compositions", () => { + const subComp = ` + + + +
A
+`; + + const { document } = parseHTML(` + +
+
+
+
+`); + const hosts = Array.from(document.querySelectorAll("[data-composition-src]")); + + const result = inlineSubCompositions(document, hosts, { + resolveHtml: () => subComp, + parseHtml: (html) => parseHTML(html).document, + }); + + expect(result.externalLinks).toHaveLength(1); + expect(result.externalLinks[0]!.href).toBe( + "https://fonts.googleapis.com/css2?family=Montserrat:wght@800", + ); + }); + it("producer path propagates data-hf-authored-id to host when inner root has id", () => { const document = makeHostDocument("intro"); const host = document.querySelector('[data-composition-src="intro.html"]')!; diff --git a/packages/producer/src/services/htmlCompiler.ts b/packages/producer/src/services/htmlCompiler.ts index 06dab2ebc..8108eab59 100644 --- a/packages/producer/src/services/htmlCompiler.ts +++ b/packages/producer/src/services/htmlCompiler.ts @@ -614,7 +614,8 @@ function inlineSubCompositions( if (result.externalLinks.length && head) { for (const link of result.externalLinks) { - if (document.querySelector(`link[href="${link.href}"]`)) continue; + const escapedHref = link.href.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); + if (document.querySelector(`link[href="${escapedHref}"]`)) continue; const el = document.createElement("link"); el.setAttribute("rel", link.rel); el.setAttribute("href", link.href);