From aa15e9a6da253ec112ed16121e703ff69957e646 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 19 Nov 2020 20:30:35 -0500 Subject: [PATCH 01/10] faster SSR, modelled on ryansolid/dom-expressions#27 --- .../handlers/shared/get_attribute_value.ts | 4 +- src/runtime/internal/ssr.ts | 49 +++++++++++++++---- 2 files changed, 41 insertions(+), 12 deletions(-) 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 5c89a7a0087f..9efb0868543f 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 2d843abb2f3a..022fb67ddbb5 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) { From e58d65822a18a7d04b4b7a3171e51f6f9a6732d7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 19 Nov 2020 23:04:40 -0500 Subject: [PATCH 02/10] simplify --- src/runtime/internal/ssr.ts | 48 +++++++++++++------------------------ 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/src/runtime/internal/ssr.ts b/src/runtime/internal/ssr.ts index 022fb67ddbb5..51e8fc6f4cd6 100644 --- a/src/runtime/internal/ssr.ts +++ b/src/runtime/internal/ssr.ts @@ -32,45 +32,31 @@ export function spread(args, classes_to_add) { return str; } -const ATTR_REGEX = /[&<"]/; -const CONTENT_REGEX = /[&<]/; +const ATTR_REGEX = /[&"]/g; +const CONTENT_REGEX = /[&<]/g; + +const escapes = { + '"': '"', + '&': '&', + '<': '<' +}; 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; - } + const pattern = (attr ? ATTR_REGEX : CONTENT_REGEX); + pattern.lastIndex = 0; - if (lastIndex !== index) { - out += html.substring(lastIndex, index); - } + let escaped = ''; + let last = 0; - lastIndex = index + 1; - out += escape; + while (pattern.test(html)) { + const i = pattern.lastIndex - 1; + escaped += html.slice(last, i) + escapes[html[i]]; + last = i + 1; } - return lastIndex !== index ? out + html.substring(lastIndex, index) : out; + return escaped + html.slice(last); } export function each(items, fn) { From 57bb045cd1947feefe42783ec0ea53278b5ab0f7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 19 Nov 2020 23:26:07 -0500 Subject: [PATCH 03/10] remove redundant parens --- src/runtime/internal/ssr.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/internal/ssr.ts b/src/runtime/internal/ssr.ts index 51e8fc6f4cd6..92e6a923b818 100644 --- a/src/runtime/internal/ssr.ts +++ b/src/runtime/internal/ssr.ts @@ -44,7 +44,7 @@ const escapes = { export function escape(html: string, attr: 0 | 1 = 0) { if (typeof html !== 'string') return html; - const pattern = (attr ? ATTR_REGEX : CONTENT_REGEX); + const pattern = attr ? ATTR_REGEX : CONTENT_REGEX; pattern.lastIndex = 0; let escaped = ''; From 29d4695fc769e995e98bc538d4325e5679ec0298 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 20 Nov 2020 08:43:05 -0500 Subject: [PATCH 04/10] boolean argument --- .../compile/render_ssr/handlers/shared/get_attribute_value.ts | 4 ++-- src/runtime/internal/ssr.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 9efb0868543f..d62b2d14e9c8 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}), 1) + "${(attribute.chunks[1] as Text).data}"`; + return x`@escape(@null_to_empty(${value}), true) + "${(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}, 1)`; + : x`@escape(${chunk.node}, true)`; }) .reduce((lhs, rhs) => x`${lhs} + ${rhs}`); } diff --git a/src/runtime/internal/ssr.ts b/src/runtime/internal/ssr.ts index 92e6a923b818..5d321abee40b 100644 --- a/src/runtime/internal/ssr.ts +++ b/src/runtime/internal/ssr.ts @@ -41,10 +41,10 @@ const escapes = { '<': '<' }; -export function escape(html: string, attr: 0 | 1 = 0) { +export function escape(html: string, is_attr = false) { if (typeof html !== 'string') return html; - const pattern = attr ? ATTR_REGEX : CONTENT_REGEX; + const pattern = is_attr ? ATTR_REGEX : CONTENT_REGEX; pattern.lastIndex = 0; let escaped = ''; From 3a94f2aca56391bd64b37ac0ff71b3aa385dafda Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 20 Nov 2020 10:21:06 -0500 Subject: [PATCH 05/10] Update src/runtime/internal/ssr.ts Co-authored-by: Conduitry --- src/runtime/internal/ssr.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/internal/ssr.ts b/src/runtime/internal/ssr.ts index 5d321abee40b..bc333d000b5b 100644 --- a/src/runtime/internal/ssr.ts +++ b/src/runtime/internal/ssr.ts @@ -144,7 +144,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, 1)) : `"${value}"`}`}`; + return ` ${name}${value === true ? '' : `=${typeof value === 'string' ? JSON.stringify(escape(value, true)) : `"${value}"`}`}`; } export function add_classes(classes) { From 30df740729e695eb270701642c1f6da34cfcea8d Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Fri, 6 May 2022 09:38:44 -0700 Subject: [PATCH 06/10] fix merge --- src/runtime/internal/ssr.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/internal/ssr.ts b/src/runtime/internal/ssr.ts index deb1ed836b4b..416267fa03f7 100644 --- a/src/runtime/internal/ssr.ts +++ b/src/runtime/internal/ssr.ts @@ -97,7 +97,7 @@ export function escape(html: string, is_attr = false) { } export function escape_attribute_value(value) { - return typeof value === 'string' ? escape(value) : value; + return typeof value === 'string' ? escape(value, true) : value; } export function escape_object(obj) { From 88e2146701668bd4cd9f8052eb0e050488dcef3b Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Fri, 6 May 2022 10:59:24 -0700 Subject: [PATCH 07/10] use ternary --- src/runtime/internal/ssr.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/runtime/internal/ssr.ts b/src/runtime/internal/ssr.ts index 416267fa03f7..ecffc82fa47d 100644 --- a/src/runtime/internal/ssr.ts +++ b/src/runtime/internal/ssr.ts @@ -72,12 +72,6 @@ export function merge_ssr_styles(style_attribute, style_directive) { const ATTR_REGEX = /[&"]/g; const CONTENT_REGEX = /[&<]/g; -const escapes = { - '"': '"', - '&': '&', - '<': '<' -}; - export function escape(html: string, is_attr = false) { if (typeof html !== 'string') return html; @@ -87,11 +81,12 @@ export function escape(html: string, is_attr = false) { let escaped = ''; let last = 0; - while (pattern.test(html)) { - const i = pattern.lastIndex - 1; - escaped += html.slice(last, i) + escapes[html[i]]; - last = i + 1; - } + while (pattern.test(html)) { + const i = pattern.lastIndex - 1; + const ch = html[i]; + escaped += html.slice(last, i) + (ch === '&' ? '&' : (ch === '"' ? '"' : '<')); + last = i + 1; + } return escaped + html.slice(last); } From 1b0e836e95bbc57008529ed858d90150620b5573 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Fri, 6 May 2022 11:03:54 -0700 Subject: [PATCH 08/10] slice -> substring --- src/runtime/internal/ssr.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/runtime/internal/ssr.ts b/src/runtime/internal/ssr.ts index ecffc82fa47d..f9cd27bcbcba 100644 --- a/src/runtime/internal/ssr.ts +++ b/src/runtime/internal/ssr.ts @@ -84,11 +84,11 @@ export function escape(html: string, is_attr = false) { while (pattern.test(html)) { const i = pattern.lastIndex - 1; const ch = html[i]; - escaped += html.slice(last, i) + (ch === '&' ? '&' : (ch === '"' ? '"' : '<')); + escaped += html.substring(last, i) + (ch === '&' ? '&' : (ch === '"' ? '"' : '<')); last = i + 1; } - return escaped + html.slice(last); + return escaped + html.substring(last); } export function escape_attribute_value(value) { From 61e180b2b9d9dcb4ca930375081786e47caa1918 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Fri, 6 May 2022 11:07:10 -0700 Subject: [PATCH 09/10] add comment --- src/runtime/internal/ssr.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/runtime/internal/ssr.ts b/src/runtime/internal/ssr.ts index f9cd27bcbcba..7fee80a0ecbd 100644 --- a/src/runtime/internal/ssr.ts +++ b/src/runtime/internal/ssr.ts @@ -72,6 +72,10 @@ export function merge_ssr_styles(style_attribute, style_directive) { const ATTR_REGEX = /[&"]/g; const CONTENT_REGEX = /[&<]/g; +/** + * Note: this method is performance sensitive and has been optimized + * https://github.com/sveltejs/svelte/pull/5701 + */ export function escape(html: string, is_attr = false) { if (typeof html !== 'string') return html; From b32049bfdf40089e341c2fa2c8a200545488a59c Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Fri, 6 May 2022 16:12:24 -0700 Subject: [PATCH 10/10] restore String conversion --- src/runtime/internal/ssr.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/runtime/internal/ssr.ts b/src/runtime/internal/ssr.ts index 7fee80a0ecbd..2c7d24a2cd61 100644 --- a/src/runtime/internal/ssr.ts +++ b/src/runtime/internal/ssr.ts @@ -76,8 +76,8 @@ const CONTENT_REGEX = /[&<]/g; * Note: this method is performance sensitive and has been optimized * https://github.com/sveltejs/svelte/pull/5701 */ -export function escape(html: string, is_attr = false) { - if (typeof html !== 'string') return html; +export function escape(value: unknown, is_attr = false) { + const str = String(value); const pattern = is_attr ? ATTR_REGEX : CONTENT_REGEX; pattern.lastIndex = 0; @@ -85,14 +85,14 @@ export function escape(html: string, is_attr = false) { let escaped = ''; let last = 0; - while (pattern.test(html)) { + while (pattern.test(str)) { const i = pattern.lastIndex - 1; - const ch = html[i]; - escaped += html.substring(last, i) + (ch === '&' ? '&' : (ch === '"' ? '"' : '<')); + const ch = str[i]; + escaped += str.substring(last, i) + (ch === '&' ? '&' : (ch === '"' ? '"' : '<')); last = i + 1; } - return escaped + html.substring(last); + return escaped + str.substring(last); } export function escape_attribute_value(value) {