diff --git a/src/compiler/compile/render_ssr/handlers/shared/get_attribute_value.ts b/src/compiler/compile/render_ssr/handlers/shared/get_attribute_value.ts index 5c89a7a008..9efb086854 100644 --- a/src/compiler/compile/render_ssr/handlers/shared/get_attribute_value.ts +++ b/src/compiler/compile/render_ssr/handlers/shared/get_attribute_value.ts @@ -9,7 +9,7 @@ export function get_class_attribute_value(attribute: Attribute): ESTreeExpressio // handle special case — `class={possiblyUndefined}` with scoped CSS if (attribute.chunks.length === 2 && (attribute.chunks[1] as Text).synthetic) { const value = (attribute.chunks[0] as Expression).node; - return x`@escape(@null_to_empty(${value})) + "${(attribute.chunks[1] as Text).data}"`; + return x`@escape(@null_to_empty(${value}), 1) + "${(attribute.chunks[1] as Text).data}"`; } return get_attribute_value(attribute); @@ -22,7 +22,7 @@ export function get_attribute_value(attribute: Attribute): ESTreeExpression { .map((chunk) => { return chunk.type === 'Text' ? string_literal(chunk.data.replace(/"/g, '"')) as ESTreeExpression - : x`@escape(${chunk.node})`; + : x`@escape(${chunk.node}, 1)`; }) .reduce((lhs, rhs) => x`${lhs} + ${rhs}`); } diff --git a/src/runtime/internal/ssr.ts b/src/runtime/internal/ssr.ts index 2d843abb2f..022fb67ddb 100644 --- a/src/runtime/internal/ssr.ts +++ b/src/runtime/internal/ssr.ts @@ -32,16 +32,45 @@ export function spread(args, classes_to_add) { return str; } -export const escaped = { - '"': '"', - "'": ''', - '&': '&', - '<': '<', - '>': '>' -}; +const ATTR_REGEX = /[&<"]/; +const CONTENT_REGEX = /[&<]/; + +export function escape(html: string, attr: 0 | 1 = 0) { + if (typeof html !== 'string') return html; + + const match = (attr ? ATTR_REGEX : CONTENT_REGEX).exec(html); + if (!match) return html; + + let index = 0; + let lastIndex = 0; + let out = ''; + let escape = ''; + + for (index = match.index; index < html.length; index++) { + switch (html.charCodeAt(index)) { + case 34: // " + if (!attr) continue; + escape = '"'; + break; + case 38: // & + escape = '&'; + break; + case 60: // < + escape = '<'; + break; + default: + continue; + } + + if (lastIndex !== index) { + out += html.substring(lastIndex, index); + } + + lastIndex = index + 1; + out += escape; + } -export function escape(html) { - return String(html).replace(/["'&<>]/g, match => escaped[match]); + return lastIndex !== index ? out + html.substring(lastIndex, index) : out; } export function each(items, fn) { @@ -129,7 +158,7 @@ export function create_ssr_component(fn) { export function add_attribute(name, value, boolean) { if (value == null || (boolean && !value)) return ''; - return ` ${name}${value === true ? '' : `=${typeof value === 'string' ? JSON.stringify(escape(value)) : `"${value}"`}`}`; + return ` ${name}${value === true ? '' : `=${typeof value === 'string' ? JSON.stringify(escape(value, 1)) : `"${value}"`}`}`; } export function add_classes(classes) {