mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(studio): match style attributes with explicit quote boundaries (#3712)
* fix(studio): match style attributes with explicit quote boundaries * fix(studio): apply quote boundaries to active source writers
This commit is contained in:
@@ -2,6 +2,26 @@ import { describe, expect, it } from "vitest";
|
||||
import { patchMediaColorGradingInHtml } from "./colorGradingScopePatch";
|
||||
|
||||
describe("patchMediaColorGradingInHtml", () => {
|
||||
it.each(['"', "'"])("replaces and removes long grading values quoted with %s", (quote) => {
|
||||
const otherQuote = quote === '"' ? "'" : '"';
|
||||
for (const value of [
|
||||
"",
|
||||
"line1\nline2",
|
||||
"a".repeat(100_000),
|
||||
`data-color-grading=${otherQuote}a`.repeat(10_000),
|
||||
]) {
|
||||
const html = `<img alt="kept"\tDATA-COLOR-GRADING=${quote}${value}${quote} />`;
|
||||
expect(patchMediaColorGradingInHtml(html, "new")).toEqual({
|
||||
html: '<img alt="kept" data-color-grading="new" />',
|
||||
count: 1,
|
||||
});
|
||||
expect(patchMediaColorGradingInHtml(html, null)).toEqual({
|
||||
html: '<img alt="kept" />',
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("adds color grading to video and image tags only", () => {
|
||||
const { html, count } = patchMediaColorGradingInHtml(
|
||||
`<div><video id="v"></video><img id="i" /><audio id="a"></audio></div>`,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const MEDIA_TAG_RE = /<\s*(video|img)\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi;
|
||||
const COLOR_GRADING_ATTR_RE = /\sdata-color-grading=(["'])([\s\S]*?)\1/i;
|
||||
const COLOR_GRADING_ATTR_RE = /\sdata-color-grading=(?:"[^"]*"|'[^']*')/i;
|
||||
const IGNORED_HTML_RANGE_RE = /<!--[\s\S]*?-->|<(script|style)\b[\s\S]*?<\/\1\s*>/gi;
|
||||
|
||||
interface TextRange {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { mergeStyleIntoTag } from "./htmlEditor";
|
||||
|
||||
describe("mergeStyleIntoTag", () => {
|
||||
it.each([
|
||||
['<div style="color: red; opacity: 1">', '<div style="color: red; opacity: 0.5">'],
|
||||
["<div style='color: red; opacity: 1'>", "<div style='color: red; opacity: 0.5'>"],
|
||||
['<div style="">', '<div style="opacity: 0.5">'],
|
||||
["<div style=''>", "<div style='opacity: 0.5'>"],
|
||||
['<div style="color:\nred">', '<div style="color: red; opacity: 0.5">'],
|
||||
["<div style='font-family: \"a\"'>", "<div style='font-family: \"a\"; opacity: 0.5'>"],
|
||||
["<div style=\"font-family: 'a'\">", "<div style=\"font-family: 'a'; opacity: 0.5\">"],
|
||||
[
|
||||
"<div style=\"color:red\" style='color:blue'>",
|
||||
"<div style=\"color: red; opacity: 0.5\" style='color:blue'>",
|
||||
],
|
||||
['<div STYLE="color:red">', '<div STYLE="color:red" style="opacity: 0.5">'],
|
||||
['<div style = "color:red">', '<div style = "color:red" style="opacity: 0.5">'],
|
||||
['<div data-style="color:red">', '<div data-style="color: red; opacity: 0.5">'],
|
||||
["<img/>", '<img style="opacity: 0.5"/>'],
|
||||
['<div style="color:red>', '<div style="color:red style="opacity: 0.5">'],
|
||||
])("preserves quote and source behavior (case %#)", (tag, expected) => {
|
||||
expect(mergeStyleIntoTag(tag, "opacity: 0.5")).toBe(expected);
|
||||
expect(mergeStyleIntoTag(tag, " \n")).toBe(tag);
|
||||
});
|
||||
|
||||
it.each(['"', "'"])("handles long values delimited by %s", (quote) => {
|
||||
const value = "a".repeat(100_000);
|
||||
const tag = `<div style=${quote}--label:${value}${quote}>`;
|
||||
expect(mergeStyleIntoTag(tag, "opacity: 0.5")).toBe(
|
||||
`<div style=${quote}--label: ${value}; opacity: 0.5${quote}>`,
|
||||
);
|
||||
const opener = `style=${quote}a`;
|
||||
expect(mergeStyleIntoTag(`<div style=${quote}${opener.repeat(10_000)}>`, "opacity: 0.5")).toBe(
|
||||
`<div style=${quote}opacity: 0.5${quote}a${opener.repeat(9_999)}>`,
|
||||
);
|
||||
const unterminated = `<div style=${quote}${value}>`;
|
||||
expect(mergeStyleIntoTag(unterminated, "opacity: 0.5")).toBe(
|
||||
`<div style=${quote}${value} style="opacity: 0.5">`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -29,13 +29,13 @@ export function mergeStyleIntoTag(tag: string, newStyles: string): string {
|
||||
|
||||
const incoming = parseStyleString(newStyles);
|
||||
|
||||
// Match style="..." or style='...' — handle multi-line attrs via dotall-like trick
|
||||
const styleAttrRe = /style=(["'])([\s\S]*?)\1/;
|
||||
// Match each quote-delimited form explicitly, including multi-line values.
|
||||
const styleAttrRe = /style=(")([^"]*)"|style=(')([^']*)'/;
|
||||
const match = tag.match(styleAttrRe);
|
||||
|
||||
if (match) {
|
||||
const quote = match[1];
|
||||
const existing = parseStyleString(match[2]);
|
||||
const quote = match[1] ?? match[3];
|
||||
const existing = parseStyleString(match[2] ?? match[4]);
|
||||
const merged = { ...existing, ...incoming };
|
||||
const serialized = Object.entries(merged)
|
||||
.map(([k, v]) => `${k}: ${v}`)
|
||||
|
||||
@@ -8,6 +8,22 @@ import {
|
||||
} from "./sourcePatcher";
|
||||
|
||||
describe("applyPatchByTarget", () => {
|
||||
it.each(['"', "'"])("patches long and multiline styles quoted with %s", (quote) => {
|
||||
const otherQuote = quote === '"' ? "'" : '"';
|
||||
const op: PatchOperation = { type: "inline-style", property: "opacity", value: "0.5" };
|
||||
for (const value of [
|
||||
"",
|
||||
"line1\nline2",
|
||||
"a".repeat(100_000),
|
||||
`style=${otherQuote}a`.repeat(10_000),
|
||||
]) {
|
||||
const html = `<img id="hero" class="hero" style=${quote}--label:${value}${quote} />`;
|
||||
const expected = `<img id="hero" class="hero" style=${quote}--label: ${value}; opacity: 0.5${quote} />`;
|
||||
expect(applyPatch(html, "hero", op)).toBe(expected);
|
||||
expect(applyPatchByTarget(html, { selector: ".hero" }, op)).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("updates a composition host by data-composition-id selector", () => {
|
||||
const html = `<div data-composition-id="intro" data-start="0" data-track-index="1"></div>`;
|
||||
const op: PatchOperation = { type: "attribute", property: "start", value: "2.5" };
|
||||
|
||||
@@ -184,10 +184,10 @@ function patchInlineStyleInTag(
|
||||
if (!tag) return html;
|
||||
|
||||
// Check if there's an existing style attribute
|
||||
const styleMatch = /\bstyle=(["'])([\s\S]*?)\1/.exec(tag);
|
||||
const styleMatch = /\bstyle=(")([^"]*)"|\bstyle=(')([^']*)'/.exec(tag);
|
||||
if (styleMatch) {
|
||||
const existingStyle = styleMatch[2];
|
||||
const quote = styleMatch[1];
|
||||
const existingStyle = styleMatch[2] ?? styleMatch[4];
|
||||
const quote = styleMatch[1] ?? styleMatch[3];
|
||||
// Parse existing properties
|
||||
const props = new Map<string, string>();
|
||||
for (const part of splitInlineStyleDeclarations(existingStyle)) {
|
||||
|
||||
Reference in New Issue
Block a user