@webtides/element-js 1.1.3 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -16,6 +16,11 @@ to [Semantic Versioning](http://semver.org/).
16
16
  <!-- ### Removed -->
17
17
  <!-- ### Fixed -->
18
18
 
19
+ ## [1.1.4] - 2024-11-12
20
+
21
+ ### FIXED
22
+
23
+ - Fixed rendering interpolations inside textarea nodes ([#133](https://github.com/webtides/element-js/issues/133))
19
24
 
20
25
  ## [1.1.3] - 2024-11-11
21
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webtides/element-js",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "module": "index.js",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -7,8 +7,6 @@ import { Part } from './Part.js';
7
7
  * @return {(function(*): void)|*}
8
8
  */
9
9
  const processBooleanAttribute = (node, name, oldValue) => {
10
- // TODO: It would be better if the ?boolean= attribute would not be there in the first place...
11
- node.removeAttribute(`?${name}`);
12
10
  return (newValue) => {
13
11
  const value = !!newValue?.valueOf() && newValue !== 'false';
14
12
  if (oldValue !== value) {
@@ -23,8 +21,6 @@ const processBooleanAttribute = (node, name, oldValue) => {
23
21
  * @return {(function(*): void)|*}
24
22
  */
25
23
  const processPropertyAttribute = (node, name) => {
26
- // TODO: It would be better if the .property= attribute would not be there in the first place...
27
- node.removeAttribute(`.${name}`);
28
24
  return (value) => {
29
25
  node[name] = value;
30
26
  };
@@ -36,9 +32,6 @@ const processPropertyAttribute = (node, name) => {
36
32
  * @return {(function(*): void)|*}
37
33
  */
38
34
  const processEventAttribute = (node, name) => {
39
- // TODO: It would be better if the event attribute would not be there in the first place...
40
- node.removeAttribute(name);
41
-
42
35
  let oldValue = undefined;
43
36
  let type = name.startsWith('@') ? name.slice(1) : name.toLowerCase().slice(2);
44
37
 
@@ -0,0 +1,64 @@
1
+ import { Part } from './Part.js';
2
+
3
+ /** @type {Map<Element, RawTextNodePart>} */
4
+ const rawTextNodePartsCache = new WeakMap();
5
+
6
+ /**
7
+ * For updating a node that can only be updated via node.textContent
8
+ * The nodes are: script | style | textarea | title
9
+ */
10
+ export class RawTextNodePart extends Part {
11
+ /** @type {Node} */
12
+ node = undefined;
13
+
14
+ interpolations = 1;
15
+ values = [];
16
+ currentValueIndex = 0;
17
+ initialValue = undefined;
18
+
19
+ /**
20
+ * @param {Node} node
21
+ * @param {string} initialValue
22
+ */
23
+ constructor(node, initialValue) {
24
+ // If we have multiple raw text node parts for the same node, it means we have multiple
25
+ // interpolations inside that node. Instead of creating a new part, we will return the same
26
+ // as before and let it defer the update until the last interpolation gets updated
27
+ const rawTextNodePart = rawTextNodePartsCache.get(node.nextElementSibling);
28
+ if (rawTextNodePart) {
29
+ rawTextNodePart.interpolations++;
30
+ node.__part = rawTextNodePart; // add Part to comment node for debugging in the browser
31
+ return rawTextNodePart;
32
+ }
33
+
34
+ super();
35
+ this.initialValue = initialValue;
36
+ node.__part = this; // add Part to comment node for debugging in the browser
37
+ this.node = node.nextElementSibling;
38
+ rawTextNodePartsCache.set(node.nextElementSibling, this);
39
+ }
40
+
41
+ /**
42
+ * @param {string} value
43
+ */
44
+ update(value) {
45
+ // If we only have one sole interpolation, we can just apply the update
46
+ if (this.interpolations === 1) {
47
+ this.node.textContent = value;
48
+ return;
49
+ }
50
+
51
+ // Instead of applying the update immediately, we check if the part has multiple interpolations
52
+ // and store the values for each interpolation in a list
53
+ this.values[this.currentValueIndex++] = value;
54
+
55
+ // Only the last call to update (for the last interpolation) will actually trigger the update
56
+ // on the DOM processor. Here we can reset everything before the next round of updates
57
+ if (this.interpolations === this.currentValueIndex) {
58
+ this.currentValueIndex = 0;
59
+ let replaceIndex = 0;
60
+ // Note: this will coarse the values into strings, but it's probably ok since we are writing to raw text (only) nodes?!
61
+ this.node.textContent = this.initialValue.replace(/\x03/g, () => this.values[replaceIndex++]);
62
+ }
63
+ }
64
+ }
@@ -4,6 +4,7 @@ import { TemplateResult } from './TemplateResult.js';
4
4
  import { AttributePart } from './AttributePart.js';
5
5
  import { ChildNodePart, processNodePart } from './ChildNodePart.js';
6
6
  import { NodePart } from './NodePart.js';
7
+ import { RawTextNodePart } from './RawTextNodePart.js';
7
8
 
8
9
  /** @type {Map<TemplateStringsArray, Part[]>} */
9
10
  const partsCache = new WeakMap();
@@ -124,6 +125,9 @@ export class TemplatePart extends Part {
124
125
  if (part.type === 'attribute') {
125
126
  return new AttributePart(part.node, part.name, part.initialValue);
126
127
  }
128
+ if (part.type === 'raw-text-node') {
129
+ return new RawTextNodePart(part.node, part.initialValue);
130
+ }
127
131
  if (part.type === 'directive') {
128
132
  return new NodePart(part.node, templateResult.values[index]);
129
133
  }
@@ -3,6 +3,7 @@ import { encodeAttribute, isObjectLike } from '../util/AttributeParser.js';
3
3
  import { TemplatePart } from './TemplatePart.js';
4
4
 
5
5
  const voidElements = /^(?:area|base|br|col|embed|hr|img|input|link|meta|source|track|wbr)$/i;
6
+ const rawTextElements = /<(script|style|textarea|title)([^>]*)>(.*?)<\/\1>/i;
6
7
  const elements = /<([a-z]+[a-z0-9:._-]*)([^>]*?)(\/?)>/g;
7
8
  // TODO: v this will not match any values with escaped quotes like onClick='console.log("\'test")'
8
9
  const attributes = /([^\s]*)=((?:")[^"]*(?:")|(?:')[^']*(?:')|[^\s\/>]*)|([^\s\/>]*)/g;
@@ -12,52 +13,115 @@ const partPositions = /[\x01\x02]/g;
12
13
  // \x03 COMMENT.ATTRIBUTE_TOKEN
13
14
  // \x04 Node.ATTRIBUTE_TOKEN
14
15
 
15
- const interpolation = new RegExp(`(<!--dom-part-(\\d+)--><!--/dom-part-(\\d+)-->|(\\S[-\\w]+)="\x04(")?|\x04)`, 'g');
16
+ const ssrPlaceholder = /{{dom-part\?(.*?)}}/g;
17
+ function makeSSRPlaceholder(type, params) {
18
+ const urlSearchParams = new URLSearchParams({ type, ...params });
19
+ const queryString = urlSearchParams.toString();
20
+ return `{{dom-part?${queryString}}}`;
21
+ }
16
22
 
17
23
  /**
18
24
  * Given a template, find part positions as both nodes and attributes and
19
25
  * return a string with placeholders as either comment nodes or named attributes.
20
26
  * @param {TemplateStringsArray | string[]} templateStrings a template literal tag array
21
- * @param {string} attributePlaceholders replace placeholders inside attribute values with this value
27
+ * @param {boolean} ssr replace interpolations with placeholders
22
28
  * @returns {string} X/HTML with prefixed comments or attributes
23
29
  */
24
- export const createTemplateString = (templateStrings, attributePlaceholders = '') => {
25
- // TODO: make it easier to identify attribute and node parts for SSR and leave the comments at those positions to be replaced in toString()
30
+ export const createTemplateString = (templateStrings, ssr = false) => {
26
31
  let partIndex = 0;
27
32
  // join all interpolations (for values) with a special placeholder and remove any whitespace
28
33
  let template = templateStrings.join('\x01').trim();
29
34
  // find (match) all elements to identify their attributes
30
35
  template = template.replace(elements, (_, name, attributesString, trailingSlash) => {
31
- let elementTagWithAttributes = name + attributesString.replaceAll('\x01', attributePlaceholders).trimEnd();
36
+ let elementTagWithAttributes = name + attributesString;
32
37
  // TODO the closing case is weird.
33
38
  if (trailingSlash.length) elementTagWithAttributes += voidElements.test(name) ? ' /' : '></' + name;
34
39
  // collect all attribute parts so that we can place matching comment nodes
35
40
  const attributeParts = attributesString.replace(attributes, (attribute, name, valueWithQuotes, directive) => {
36
41
  if (directive && directive === '\x01') {
42
+ elementTagWithAttributes = elementTagWithAttributes.replace(
43
+ attribute,
44
+ ssr ? makeSSRPlaceholder('directive') : '',
45
+ );
37
46
  return `<!--\x02$-->`;
38
47
  }
39
48
 
40
49
  // remove quotes from attribute value to normalize the value
41
- const value =
42
- valueWithQuotes?.startsWith('"') || valueWithQuotes?.startsWith("'")
43
- ? valueWithQuotes.slice(1, -1)
44
- : valueWithQuotes;
50
+ const hasQuotes = valueWithQuotes?.startsWith('"') || valueWithQuotes?.startsWith("'");
51
+ const quotes = hasQuotes ? valueWithQuotes.at(0) : '';
52
+ const value = hasQuotes ? valueWithQuotes.slice(1, -1) : valueWithQuotes;
45
53
  const partsCount = (attribute.match(/\x01/g) || []).length;
46
54
  const parts = [];
47
55
  for (let index = 0; index < partsCount; index++) {
56
+ // TODO: maybe we could use url parameters here?! -> ?type=attribute&name=${name}&initialValue=${initialValue}
48
57
  parts.push(`<!--\x02:${name}=${value.replaceAll('\x01', '\x03')}-->`);
49
58
  }
59
+
60
+ if (parts.length > 0) {
61
+ let replacement = '';
62
+ if (ssr) {
63
+ for (let index = 0; index < partsCount - 1; index++) {
64
+ replacement = replacement + makeSSRPlaceholder('noop');
65
+ }
66
+ replacement =
67
+ replacement +
68
+ makeSSRPlaceholder('attribute', {
69
+ name,
70
+ interpolations: partsCount,
71
+ initialValue: value.replaceAll('\x01', '\x03'),
72
+ quotes,
73
+ });
74
+ }
75
+ elementTagWithAttributes = elementTagWithAttributes.replace(attribute, replacement);
76
+ }
77
+
50
78
  return parts.join('');
51
79
  });
52
80
  // TODO create test to check if lf are in place
53
81
  return `
54
82
  ${attributesString.includes('\x01') ? attributeParts : ''}\n
55
- <${elementTagWithAttributes}>\n
83
+ <${elementTagWithAttributes.trimEnd()}>\n
56
84
  `.trim();
57
85
  });
86
+ /**
87
+ * Processes the HTML template content by identifying specific "raw text" nodes (e.g., <script>, <style>, <textarea>, <title>)
88
+ * and inserts comments before each instance of an interpolation within these nodes.
89
+ * The placeholders are represented by a control character (in this case, "\x01").
90
+ *
91
+ * Explanation of logic:
92
+ * - The main regex, `rawTextElements`, identifies and captures specific HTML tags (script, style, textarea, title)
93
+ * along with their attributes and content. This regex captures:
94
+ * 1. `tag`: The tag name (e.g., "textarea").
95
+ * 2. `attributes`: Any attributes within the opening tag (e.g., `foo="bar"`).
96
+ * 3. `content`: The inner content within the tag (which may contain interpolations).
97
+ * - Inside the replace function:
98
+ * - The number of placeholder occurrences (`\x01` instances) in `content` is counted to determine
99
+ * how many parts (comments) should be generated.
100
+ * - An array `parts` is created, where each entry represents a comment to be inserted before the raw text node.
101
+ * Each comment includes a modified version of `content` where each placeholder (`\x01`) is replaced with a
102
+ * different control character (`\x03`) for later differentiation.
103
+ * - The `content` is then modified by removing each interpolation (`\x01` is removed), leaving the raw text
104
+ * content without markers.
105
+ * - The processed comments in `parts` are joined and placed before the modified raw text node, preserving
106
+ * its tag, attributes, and inner content without placeholders.
107
+ * - The function finally returns the modified template string with comments in the correct places.
108
+ */
109
+ template = template.replace(rawTextElements, (match, tag, attributes, content) => {
110
+ const partsCount = (content.match(/\x01/g) || []).length;
111
+ const parts = [];
112
+ for (let index = 0; index < partsCount; index++) {
113
+ // TODO: maybe we could use url parameters here?! -> ?type=raw-text-node&initialValue=${initialValue}
114
+ parts.push(`<!--\x02/raw-text-node=${content.replaceAll('\x01', '\x03')}-->`);
115
+ }
116
+ const parsedContent = content.replaceAll('\x01', ssr ? makeSSRPlaceholder('raw-text-node') : '');
117
+ return `${parts.join('')}<${tag}${attributes}>${parsedContent}</${tag}>`;
118
+ });
58
119
  // replace interpolation placeholders with our indexed markers
59
120
  template = template.replace(partPositions, (partPosition) => {
60
121
  if (partPosition === '\x01') {
122
+ if (ssr) {
123
+ return `<!--dom-part-${partIndex}-->${makeSSRPlaceholder('node')}<!--/dom-part-${partIndex++}-->`;
124
+ }
61
125
  return `<!--dom-part-${partIndex}--><!--/dom-part-${partIndex++}-->`;
62
126
  } else if (partPosition === '\x02') {
63
127
  return `dom-part-${partIndex++}`;
@@ -71,13 +135,11 @@ export const createTemplateString = (templateStrings, attributePlaceholders = ''
71
135
  /**
72
136
  * @param {String} name
73
137
  * @param {any} value
74
- * @param {Boolean} isSingleValue
138
+ * @param {string} quotes
75
139
  * @return {String}
76
140
  */
77
- const attribute = (name, value, isSingleValue = true) => {
78
- return ` ${name}="${encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value)}${
79
- isSingleValue ? '"' : ''
80
- }`;
141
+ const attribute = (name, value, quotes = '"') => {
142
+ return ` ${name}=${quotes}${encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value)}${quotes}`;
81
143
  };
82
144
 
83
145
  /**
@@ -111,11 +173,14 @@ const getValue = (value) => {
111
173
  return value == null ? '' : encodeAttribute(String(value));
112
174
  };
113
175
 
114
- /** @type {Map<TemplateStringsArray, *[]>} */
115
- const parsedUpdates = new WeakMap();
176
+ /** @type {Map<TemplateStringsArray, string>} */
177
+ const ssrTemplateStringsCache = new WeakMap();
178
+
179
+ /** @type {Map<TemplateStringsArray, (() => String)[]>} */
180
+ const ssrUpdatesCache = new WeakMap();
116
181
 
117
182
  /** @type {Map<Element, TemplatePart>} */
118
- const templateParts = new WeakMap();
183
+ const templatePartsCache = new WeakMap();
119
184
 
120
185
  export class TemplateResult {
121
186
  /**
@@ -132,7 +197,7 @@ export class TemplateResult {
132
197
  */
133
198
  renderInto(domNode) {
134
199
  let serverSideRendered = false;
135
- let templatePart = templateParts.get(domNode);
200
+ let templatePart = templatePartsCache.get(domNode);
136
201
  if (!templatePart) {
137
202
  const startNode = Array.from(domNode.childNodes)
138
203
  .filter((node) => node.nodeType === COMMENT_NODE)
@@ -141,7 +206,7 @@ export class TemplateResult {
141
206
  serverSideRendered = startNode !== undefined;
142
207
 
143
208
  templatePart = new TemplatePart(startNode, this);
144
- templateParts.set(domNode, templatePart);
209
+ templatePartsCache.set(domNode, templatePart);
145
210
 
146
211
  if (!serverSideRendered) {
147
212
  domNode.replaceChildren(...templatePart.childNodes);
@@ -152,100 +217,130 @@ export class TemplateResult {
152
217
  }
153
218
 
154
219
  /**
155
- * @param {String[]} strings
220
+ * @param {TemplateStringsArray} strings
156
221
  * @return {(() => String)[]}
157
222
  */
158
- parse(strings) {
159
- const html = createTemplateString(strings, '\x04');
160
- const parts = [];
161
- let i = 0;
162
- let match = null;
163
- while ((match = interpolation.exec(html))) {
164
- const pre = html.slice(i, match.index);
165
- i = match.index + match[0].length;
166
- if (match[2]) {
167
- // ChildNodePart
168
- const index = match[2];
169
- parts.push((value) => `${pre}<!--dom-part-${index}-->${getValue(value)}<!--/dom-part-${index}-->`);
170
- } else if (match[4]) {
171
- // AttributePart with single interpolation or the first interpolation right after the attribute=
172
- const isSingleValue = match[5] !== undefined;
173
- let name = match[4];
174
- switch (true) {
175
- case name[0] === '?':
176
- const booleanName = name.slice(1).toLowerCase();
177
- parts.push((value) => {
178
- if (!value) return '';
179
- return `${pre} ${booleanName}=""`;
180
- });
181
- break;
182
- case name[0] === '.':
183
- const lower = name.slice(1).toLowerCase();
184
- parts.push((value) => {
185
- let result = pre;
186
- // null, undefined, and false are not shown at all
187
- if (value === null || value === undefined || value === '') {
188
- result += attribute(lower, '');
189
- } else {
190
- // in all other cases, just escape it in quotes
191
- result += attribute(lower, value, isSingleValue);
192
- }
193
- return result;
194
- });
195
- break;
196
- case name[0] === '@':
197
- name = 'on' + name.slice(1);
198
- case name[0] === 'o' && name[1] === 'n':
199
- parts.push((value) => {
200
- return pre;
201
- });
202
- break;
203
- default:
204
- parts.push((value) => {
205
- let result = pre;
206
- if (value != null) {
207
- result += attribute(name, value, isSingleValue);
208
- }
209
- return result;
223
+ parseSSRUpdates(strings) {
224
+ let templateString = ssrTemplateStringsCache.get(strings);
225
+
226
+ if (!templateString) {
227
+ templateString = createTemplateString(strings, true);
228
+ ssrTemplateStringsCache.set(strings, templateString);
229
+ }
230
+
231
+ let updates = ssrUpdatesCache.get(strings);
232
+
233
+ if (!updates) {
234
+ const partMatches = [...templateString.matchAll(ssrPlaceholder)].map((match) => match[0]);
235
+ updates = partMatches.flatMap((match) => {
236
+ const placeholderContent = match.replace('{{', '').replace('}}', '');
237
+ const [_, paramsString] = placeholderContent.split('?');
238
+ const searchParams = new URLSearchParams(paramsString);
239
+ const type = searchParams.get('type');
240
+ if (type === 'noop') return [];
241
+ if (type === 'attribute') {
242
+ const name = searchParams.get('name');
243
+ const quotes = searchParams.get('quotes');
244
+
245
+ if (name.startsWith('?')) {
246
+ const booleanName = name.replace('?', '');
247
+ return [
248
+ {
249
+ type: 'attribute',
250
+ processor: (value) => {
251
+ if (!value) return '';
252
+ return `${booleanName}=""`;
253
+ },
254
+ },
255
+ ];
256
+ }
257
+
258
+ if (name.startsWith('.')) {
259
+ const propertyName = name.replace('.', '');
260
+ return [
261
+ {
262
+ type: 'attribute',
263
+ processor: (value) => {
264
+ // null, undefined, and false are not shown at all
265
+ if (value === null || value === undefined || value === '') {
266
+ return '';
267
+ }
268
+ // in all other cases, just escape it in quotes
269
+ return attribute(propertyName, value, quotes);
270
+ },
271
+ },
272
+ ];
273
+ }
274
+
275
+ if (name.startsWith('@') || name.startsWith('on')) {
276
+ return [
277
+ {
278
+ type: 'attribute',
279
+ processor: (value) => {
280
+ // event handlers have nothing to do on the server side
281
+ return '';
282
+ },
283
+ },
284
+ ];
285
+ }
286
+
287
+ const interpolations = searchParams.get('interpolations');
288
+ let values = [];
289
+ const attributeUpdates = [];
290
+ for (let index = 0; index < interpolations - 1; index++) {
291
+ attributeUpdates.push({
292
+ type: 'noop',
293
+ processor: (value) => {
294
+ values.push(value);
295
+ return '';
296
+ },
210
297
  });
211
- break;
212
- }
213
- } else {
214
- // AttributePart in the middle of an attribute value or NodePart
215
- parts.push((value) => {
216
- let result = pre;
217
- // TODO: we currently cannot distinguish between NodeParts and AttributeParts before
218
- if (isObjectLike(value) && value.directiveClass) {
219
- // NodePart
220
- const directive = new value.directiveClass();
221
- result += directive.stringify(...value.values);
222
- } else if (value != null) {
223
- result += encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value);
224
298
  }
225
- return result;
226
- });
227
- }
299
+ attributeUpdates.push({
300
+ type: 'attribute',
301
+ name: searchParams.get('name'),
302
+ interpolations,
303
+ initialValue: searchParams.get('initialValue'),
304
+ processor: (value) => {
305
+ values.push(value);
306
+ const initialValue = searchParams.get('initialValue');
307
+ let replaceIndex = 0;
308
+ // Note: this will coarse the values into strings, but it's probably ok since there can only be multiple values in string attributes?!
309
+ const parsedValue = initialValue.replace(/\x03/g, () => values[replaceIndex++]);
310
+ values = [];
311
+ return attribute(searchParams.get('name'), parsedValue, quotes);
312
+ },
313
+ });
314
+ return attributeUpdates;
315
+ }
316
+ if (type === 'node') return [{ type: 'node', processor: (value) => getValue(value) }];
317
+ if (type === 'raw-text-node') return [{ type: 'raw-text-node', processor: (value) => getValue(value) }];
318
+ if (type === 'directive')
319
+ return [
320
+ {
321
+ type: 'directive',
322
+ processor: (value) => {
323
+ const directive = new value.directiveClass();
324
+ return directive.stringify(...value.values);
325
+ },
326
+ },
327
+ ];
328
+ });
329
+ ssrUpdatesCache.set(strings, updates);
228
330
  }
229
331
 
230
- // We couldn't correctly parse parts from the template
231
- if (parts.length !== strings.length - 1) {
332
+ // We couldn't correctly parse updates from the template
333
+ if (updates.length !== strings.length - 1) {
232
334
  throw {
233
335
  name: 'ParseTemplateError',
234
- message: 'Could not parse parts from template correctly. Parts length has not the expected length.',
336
+ message: 'Could not parse updates from template correctly. Updates length has not the expected length.',
235
337
  strings,
236
- templateString: html,
237
- parts,
338
+ templateString,
339
+ updates,
238
340
  };
239
341
  }
240
342
 
241
- if (parts.length) {
242
- const last = parts[parts.length - 1];
243
- const chunk = html.slice(i);
244
- parts[parts.length - 1] = (value) => last(value) + chunk;
245
- } else {
246
- parts.push(() => html);
247
- }
248
- return parts;
343
+ return updates;
249
344
  }
250
345
 
251
346
  /**
@@ -294,6 +389,17 @@ export class TemplateResult {
294
389
  });
295
390
  continue;
296
391
  }
392
+ if (/^dom-part-\d+\/raw-text-node/.test(node.data)) {
393
+ // For html`<textarea>${'foo'} bar</textarea>` we will get:
394
+ // <!--dom-part-0/raw-text-node=\x03 bar-->
395
+ const [_, ...initialValue] = node.data.split('=');
396
+ parts.push({
397
+ type: 'raw-text-node',
398
+ path: getNodePath(node),
399
+ initialValue: initialValue.join('='),
400
+ });
401
+ continue;
402
+ }
297
403
  if (/^dom-part-\d+\$/.test(node.data)) {
298
404
  parts.push({ type: 'directive', path: getNodePath(node) });
299
405
  }
@@ -328,13 +434,12 @@ export class TemplateResult {
328
434
  }
329
435
 
330
436
  toString() {
331
- let updates = parsedUpdates.get(this.strings);
332
-
333
- if (!updates) {
334
- updates = this.parse(this.strings);
335
- parsedUpdates.set(this.strings, updates);
336
- }
437
+ const updates = this.parseSSRUpdates(this.strings);
438
+ let template = ssrTemplateStringsCache.get(this.strings);
337
439
 
338
- return this.values.length ? this.values.map((value, index) => updates[index](value)).join('') : updates[0]();
440
+ let index = 0;
441
+ return template.replaceAll(ssrPlaceholder, () => {
442
+ return updates[index].processor(this.values[index++]);
443
+ });
339
444
  }
340
445
  }