fix: styles with semicolons

svelte-custom-renderer
paoloricciuti 5 days ago
parent 1e5dc328cd
commit be0640be87

@ -514,7 +514,73 @@ export function set_node_value(node, value) {
// --- Helpers for style attribute string manipulation (custom renderer) --- // --- Helpers for style attribute string manipulation (custom renderer) ---
// TODO: check if this can be improved? /**
* Split only at top-level semicolons, preserving CSS strings, functions and comments.
* @param {string} style_string
* @returns {string[]} trimmed, non-empty declarations (without trailing `;`)
*/
function split_style_declarations(style_string) {
/** @type {string[]} */
var declarations = [];
/** @type {false | '"' | "'"} */
var in_str = false;
var depth = 0;
var in_comment = false;
var start = 0;
for (var i = 0; i < style_string.length; i++) {
var c = style_string[i];
if (in_comment) {
if (c === '/' && style_string[i - 1] === '*') {
in_comment = false;
}
continue;
}
if (c === '\\') {
i++;
continue;
}
if (in_str) {
if (c === in_str) {
in_str = false;
}
continue;
}
if (c === '/' && style_string[i + 1] === '*') {
in_comment = true;
} else if (c === '"' || c === "'") {
in_str = c;
} else if (c === '(') {
depth++;
} else if (c === ')') {
if (depth > 0) depth--;
} else if (c === ';' && depth === 0) {
var declaration = style_string.slice(start, i).trim();
if (declaration) declarations.push(declaration);
start = i + 1;
}
}
var last = style_string.slice(start).trim();
if (last) declarations.push(last);
return declarations;
}
/**
* @param {string} declaration
* @returns {string | null}
*/
function declaration_property(declaration) {
var colon_index = declaration.indexOf(':');
if (colon_index === -1) return null;
return declaration.slice(0, colon_index).trim();
}
/** /**
* @param {string} style_string * @param {string} style_string
@ -525,26 +591,22 @@ export function set_node_value(node, value) {
*/ */
function set_style_property_in_string(style_string, property, value, priority) { function set_style_property_in_string(style_string, property, value, priority) {
var declaration = property + ': ' + value + (priority ? ' !' + priority : ''); var declaration = property + ': ' + value + (priority ? ' !' + priority : '');
var parts = style_string.split(';'); var parts = split_style_declarations(style_string);
var found = false; var found = false;
for (var i = 0; i < parts.length; i++) { for (var i = 0; i < parts.length; i++) {
var colon_index = parts[i].indexOf(':'); if (declaration_property(parts[i]) === property) {
if (colon_index !== -1 && parts[i].substring(0, colon_index).trim() === property) { parts[i] = declaration;
parts[i] = ' ' + declaration;
found = true; found = true;
break; break;
} }
} }
if (!found) { if (!found) {
parts.push(' ' + declaration); parts.push(declaration);
} }
return parts return parts.join('; ');
.map((p) => p.trim())
.filter(Boolean)
.join('; ');
} }
/** /**
@ -553,15 +615,8 @@ function set_style_property_in_string(style_string, property, value, priority) {
* @returns {string} * @returns {string}
*/ */
function remove_style_property_in_string(style_string, property) { function remove_style_property_in_string(style_string, property) {
return style_string return split_style_declarations(style_string)
.split(';') .filter((part) => declaration_property(part) !== property)
.filter((part) => {
var colon_index = part.indexOf(':');
if (colon_index === -1) return false;
return part.substring(0, colon_index).trim() !== property;
})
.map((p) => p.trim())
.filter(Boolean)
.join('; '); .join('; ');
} }

@ -0,0 +1,135 @@
import { afterEach, assert, beforeEach, describe, it } from 'vitest';
import {
style_set_property,
style_remove_property
} from '../../src/internal/client/dom/operations.js';
import { set_renderer } from '../../src/internal/client/custom-renderer/state.js';
function make_element(style = '') {
return { attributes: { style } as Record<string, string> };
}
const fake_renderer = {
getAttribute(element: any, name: string) {
return element.attributes[name] ?? null;
},
setAttribute(element: any, name: string, value: string) {
element.attributes[name] = String(value);
}
} as any;
function style_of(element: any) {
return element.attributes.style;
}
describe('custom renderer style string manipulation', () => {
beforeEach(() => {
set_renderer(fake_renderer);
});
afterEach(() => {
set_renderer(null);
});
it('sets a property on an empty style', () => {
const el = make_element();
style_set_property(el as any, 'color', 'red');
assert.equal(style_of(el), 'color: red');
});
it('updates an existing property, preserving unrelated ones', () => {
const el = make_element('color: red; background: blue');
style_set_property(el as any, 'color', 'green');
assert.equal(style_of(el), 'color: green; background: blue');
});
it('appends a new property, preserving unrelated ones', () => {
const el = make_element('color: red');
style_set_property(el as any, 'background', 'blue');
assert.equal(style_of(el), 'color: red; background: blue');
});
it('supports the important priority', () => {
const el = make_element('color: red');
style_set_property(el as any, 'color', 'green', 'important');
assert.equal(style_of(el), 'color: green !important');
});
it('removes a property, preserving unrelated ones', () => {
const el = make_element('color: red; background: blue');
style_remove_property(el as any, 'color');
assert.equal(style_of(el), 'background: blue');
});
it('removing a non-existent property is a no-op', () => {
const el = make_element('color: red');
style_remove_property(el as any, 'background');
assert.equal(style_of(el), 'color: red');
});
it('removing the only property yields an empty style', () => {
const el = make_element('color: red');
style_remove_property(el as any, 'color');
assert.equal(style_of(el), '');
});
describe('preserves quoted semicolons', () => {
it('when updating an unrelated property', () => {
const el = make_element('content: "a;b"; color: red');
style_set_property(el as any, 'color', 'green');
assert.equal(style_of(el), 'content: "a;b"; color: green');
});
it('when removing an unrelated property', () => {
const el = make_element('content: "a;b"; color: red');
style_remove_property(el as any, 'color');
assert.equal(style_of(el), 'content: "a;b"');
});
it('with single-quoted values', () => {
const el = make_element("content: 'x;y;z'; color: red");
style_set_property(el as any, 'color', 'green');
assert.equal(style_of(el), "content: 'x;y;z'; color: green");
});
});
describe('preserves data URLs', () => {
it('with semicolons inside url(...)', () => {
const el = make_element('background: url(data:image/png;base64,AAAA); color: red');
style_set_property(el as any, 'color', 'green');
assert.equal(style_of(el), 'background: url(data:image/png;base64,AAAA); color: green');
});
it('when removing the data URL property', () => {
const el = make_element('background: url(data:image/png;base64,AAAA); color: red');
style_remove_property(el as any, 'background');
assert.equal(style_of(el), 'color: red');
});
});
describe('preserves escaped quotes', () => {
it('escaped quote inside a quoted value does not close the string', () => {
const el = make_element('content: "a\\";b"; color: red');
style_set_property(el as any, 'color', 'green');
assert.equal(style_of(el), 'content: "a\\";b"; color: green');
});
it('escaped backslash before a quote', () => {
const el = make_element('content: "a\\\\"; color: red');
style_remove_property(el as any, 'color');
assert.equal(style_of(el), 'content: "a\\\\"');
});
});
it('ignores semicolons inside comments', () => {
const el = make_element('color: red /* a;b */; background: blue');
style_set_property(el as any, 'background', 'green');
assert.equal(style_of(el), 'color: red /* a;b */; background: green');
});
it('preserves escaped semicolons outside strings', () => {
const el = make_element('--text: a\\;b; color: red');
style_remove_property(el as any, 'color');
assert.equal(style_of(el), '--text: a\\;b');
});
});
Loading…
Cancel
Save