@webtides/element-js 1.1.4 → 1.1.5

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,12 @@ to [Semantic Versioning](http://semver.org/).
16
16
  <!-- ### Removed -->
17
17
  <!-- ### Fixed -->
18
18
 
19
+ ## [1.1.5] - 2024-11-14
20
+
21
+ ### FIXED
22
+
23
+ - Fixed using special characters in attribute static strings ([#134](https://github.com/webtides/element-js/issues/134))
24
+
19
25
  ## [1.1.4] - 2024-11-12
20
26
 
21
27
  ### FIXED
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webtides/element-js",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "module": "index.js",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -49,23 +49,15 @@ const processEventAttribute = (node, name) => {
49
49
  * @return {(function(*): void)|*}
50
50
  */
51
51
  const processAttribute = (node, name) => {
52
- let oldValue,
53
- orphan = true;
54
- const attributeNode = globalThis.document?.createAttributeNS(null, name);
52
+ let oldValue;
55
53
  return (newValue) => {
56
54
  const value = newValue?.valueOf();
57
55
  if (oldValue !== value) {
58
- if ((oldValue = value) == null) {
59
- if (!orphan) {
60
- node.removeAttributeNode(attributeNode);
61
- orphan = true;
62
- }
56
+ oldValue = value;
57
+ if (value == null) {
58
+ node.removeAttribute(name);
63
59
  } else {
64
- attributeNode.value = value;
65
- if (orphan) {
66
- node.setAttributeNodeNS(attributeNode);
67
- orphan = false;
68
- }
60
+ node.setAttribute(name, value);
69
61
  }
70
62
  }
71
63
  };
@@ -1,4 +1,4 @@
1
- import { COMMENT_NODE, getNodePath } from '../util/DOMHelper.js';
1
+ import { COMMENT_NODE, getNodePath, encodeHTML } from '../util/DOMHelper.js';
2
2
  import { encodeAttribute, isObjectLike } from '../util/AttributeParser.js';
3
3
  import { TemplatePart } from './TemplatePart.js';
4
4
 
@@ -8,18 +8,35 @@ const elements = /<([a-z]+[a-z0-9:._-]*)([^>]*?)(\/?)>/g;
8
8
  // TODO: v this will not match any values with escaped quotes like onClick='console.log("\'test")'
9
9
  const attributes = /([^\s]*)=((?:")[^"]*(?:")|(?:')[^']*(?:')|[^\s\/>]*)|([^\s\/>]*)/g;
10
10
  const partPositions = /[\x01\x02]/g;
11
+ const ssrPlaceholder = /{{dom-part\?(.*?)}}/g;
11
12
  // \x01 Node.ELEMENT_NODE
12
13
  // \x02 Node.ATTRIBUTE_NODE
13
14
  // \x03 COMMENT.ATTRIBUTE_TOKEN
14
15
  // \x04 Node.ATTRIBUTE_TOKEN
15
16
 
16
- const ssrPlaceholder = /{{dom-part\?(.*?)}}/g;
17
- function makeSSRPlaceholder(type, params) {
17
+ /**
18
+ * Creates a URL query string for meta information to pass along ssr part placeholders
19
+ * @param type
20
+ * @param params
21
+ * @return {string}
22
+ */
23
+ function makeSSRPlaceholderParams(type, params) {
18
24
  const urlSearchParams = new URLSearchParams({ type, ...params });
19
25
  const queryString = urlSearchParams.toString();
20
26
  return `{{dom-part?${queryString}}}`;
21
27
  }
22
28
 
29
+ /**
30
+ * Creates a URL query string for meta information to pass along part comments
31
+ * @param {string} type
32
+ * @param {{}} params
33
+ * @return {string}
34
+ */
35
+ function makePartParams(type, params) {
36
+ const urlSearchParams = new URLSearchParams({ type, ...params });
37
+ return urlSearchParams.toString();
38
+ }
39
+
23
40
  /**
24
41
  * Given a template, find part positions as both nodes and attributes and
25
42
  * return a string with placeholders as either comment nodes or named attributes.
@@ -41,9 +58,10 @@ export const createTemplateString = (templateStrings, ssr = false) => {
41
58
  if (directive && directive === '\x01') {
42
59
  elementTagWithAttributes = elementTagWithAttributes.replace(
43
60
  attribute,
44
- ssr ? makeSSRPlaceholder('directive') : '',
61
+ ssr ? makeSSRPlaceholderParams('directive') : '',
45
62
  );
46
- return `<!--\x02$-->`;
63
+ const params = makePartParams('directive');
64
+ return `<!--\x02?${params}-->`;
47
65
  }
48
66
 
49
67
  // remove quotes from attribute value to normalize the value
@@ -53,19 +71,22 @@ export const createTemplateString = (templateStrings, ssr = false) => {
53
71
  const partsCount = (attribute.match(/\x01/g) || []).length;
54
72
  const parts = [];
55
73
  for (let index = 0; index < partsCount; index++) {
56
- // TODO: maybe we could use url parameters here?! -> ?type=attribute&name=${name}&initialValue=${initialValue}
57
- parts.push(`<!--\x02:${name}=${value.replaceAll('\x01', '\x03')}-->`);
74
+ const params = makePartParams('attribute', {
75
+ name,
76
+ initialValue: value.replaceAll('\x01', '\x03'),
77
+ });
78
+ parts.push(`<!--\x02?${params}-->`);
58
79
  }
59
80
 
60
81
  if (parts.length > 0) {
61
82
  let replacement = '';
62
83
  if (ssr) {
63
84
  for (let index = 0; index < partsCount - 1; index++) {
64
- replacement = replacement + makeSSRPlaceholder('noop');
85
+ replacement = replacement + makeSSRPlaceholderParams('noop');
65
86
  }
66
87
  replacement =
67
88
  replacement +
68
- makeSSRPlaceholder('attribute', {
89
+ makeSSRPlaceholderParams('attribute', {
69
90
  name,
70
91
  interpolations: partsCount,
71
92
  initialValue: value.replaceAll('\x01', '\x03'),
@@ -110,19 +131,19 @@ export const createTemplateString = (templateStrings, ssr = false) => {
110
131
  const partsCount = (content.match(/\x01/g) || []).length;
111
132
  const parts = [];
112
133
  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')}-->`);
134
+ const params = makePartParams('raw-text-node', {
135
+ initialValue: content.replaceAll('\x01', '\x03'),
136
+ });
137
+ parts.push(`<!--\x02?${params}-->`);
115
138
  }
116
- const parsedContent = content.replaceAll('\x01', ssr ? makeSSRPlaceholder('raw-text-node') : '');
139
+ const parsedContent = content.replaceAll('\x01', ssr ? makeSSRPlaceholderParams('raw-text-node') : '');
117
140
  return `${parts.join('')}<${tag}${attributes}>${parsedContent}</${tag}>`;
118
141
  });
119
142
  // replace interpolation placeholders with our indexed markers
120
143
  template = template.replace(partPositions, (partPosition) => {
121
144
  if (partPosition === '\x01') {
122
- if (ssr) {
123
- return `<!--dom-part-${partIndex}-->${makeSSRPlaceholder('node')}<!--/dom-part-${partIndex++}-->`;
124
- }
125
- return `<!--dom-part-${partIndex}--><!--/dom-part-${partIndex++}-->`;
145
+ const placeholder = ssr ? makeSSRPlaceholderParams('node') : '';
146
+ return `<!--dom-part-${partIndex}-->${placeholder}<!--/dom-part-${partIndex++}-->`;
126
147
  } else if (partPosition === '\x02') {
127
148
  return `dom-part-${partIndex++}`;
128
149
  }
@@ -149,7 +170,7 @@ const attribute = (name, value, quotes = '"') => {
149
170
  const getValue = (value) => {
150
171
  switch (typeof value) {
151
172
  case 'string':
152
- return encodeAttribute(value);
173
+ return encodeHTML(value);
153
174
  case 'boolean':
154
175
  case 'number':
155
176
  return String(value);
@@ -170,7 +191,7 @@ const getValue = (value) => {
170
191
  case 'function':
171
192
  return getValue(value());
172
193
  }
173
- return value == null ? '' : encodeAttribute(String(value));
194
+ return value == null ? '' : encodeHTML(String(value));
174
195
  };
175
196
 
176
197
  /** @type {Map<TemplateStringsArray, string>} */
@@ -378,29 +399,32 @@ export class TemplateResult {
378
399
  parts.push({ type: 'node', path: getNodePath(node) });
379
400
  continue;
380
401
  }
381
- if (/^dom-part-\d+:/.test(node.data)) {
382
- const [_, ...attribute] = node.data.split(':');
383
- const [name, ...initialValue] = attribute.join(':').split('=');
402
+ if (/^dom-part-\d+\?type=attribute&(.*?)$/.test(node.data)) {
403
+ // For html`<div id="${'foo'}" class="${'bar'}"></div>` we will get:
404
+ // <!--dom-part-0?type=attribute&name=id&initialValue=\x03--><!--dom-part-1?type=attribute&name=class&initialValue=\x03-->
405
+ const [_, paramsString] = node.data.split('?');
406
+ const searchParams = new URLSearchParams(paramsString);
384
407
  parts.push({
385
408
  type: 'attribute',
386
409
  path: getNodePath(node),
387
- name: name,
388
- initialValue: initialValue.join('='),
410
+ name: searchParams.get('name'),
411
+ initialValue: searchParams.get('initialValue'),
389
412
  });
390
413
  continue;
391
414
  }
392
- if (/^dom-part-\d+\/raw-text-node/.test(node.data)) {
415
+ if (/^dom-part-\d+\?type=raw-text-node&(.*?)$/.test(node.data)) {
393
416
  // For html`<textarea>${'foo'} bar</textarea>` we will get:
394
- // <!--dom-part-0/raw-text-node=\x03 bar-->
395
- const [_, ...initialValue] = node.data.split('=');
417
+ // <!--dom-part-0?type=raw-text-node&initialValue=\x03 bar-->
418
+ const [_, paramsString] = node.data.split('?');
419
+ const searchParams = new URLSearchParams(paramsString);
396
420
  parts.push({
397
421
  type: 'raw-text-node',
398
422
  path: getNodePath(node),
399
- initialValue: initialValue.join('='),
423
+ initialValue: searchParams.get('initialValue'),
400
424
  });
401
425
  continue;
402
426
  }
403
- if (/^dom-part-\d+\$/.test(node.data)) {
427
+ if (/^dom-part-\d+\?type=directive$/.test(node.data)) {
404
428
  parts.push({ type: 'directive', path: getNodePath(node) });
405
429
  }
406
430
  }
@@ -125,29 +125,23 @@ export function camelToDash(string) {
125
125
  }
126
126
 
127
127
  /**
128
- * Decodes an attribute.
128
+ * Decodes an attribute according to {@link https://html.spec.whatwg.org/multipage/syntax.html#attributes-2}
129
129
  * @param {string} attribute
130
130
  * @returns {string}
131
131
  */
132
132
  export function decodeAttribute(attribute) {
133
- return `${attribute}`
134
- .replace(/&lt;/g, '<')
135
- .replace(/&gt;/g, '>')
136
- .replace(/&quot;/g, '"')
137
- .replace(/&apos;/g, "'");
133
+ return `${attribute}`.replace(/&quot;/g, '"').replace(/&amp;/g, '&');
138
134
  }
139
135
 
140
136
  /**
141
- * Encodes an attribute.
137
+ * Encodes an attribute according to {@link https://html.spec.whatwg.org/multipage/syntax.html#attributes-2}
142
138
  * @param {string} attribute
143
139
  * @returns {string}
144
140
  */
145
141
  export function encodeAttribute(attribute) {
146
142
  return `${attribute}`
147
- .replace(/'/g, '&apos;')
143
+ .replace(/&/g, '&amp;')
148
144
  .replace(/"/g, '&quot;')
149
- .replace(/</g, '&lt;')
150
- .replace(/>/g, '&gt;')
151
145
  .replace(/\r\n/g, '\n')
152
146
  .replace(/[\r\n]/g, '\n');
153
147
  }
@@ -305,3 +305,33 @@ export const diffNodes = function (templateNode, domNode, shouldDiffChildNodes =
305
305
  }
306
306
  }
307
307
  };
308
+
309
+ /**
310
+ * Decodes a html string
311
+ * @param {string} html
312
+ * @returns {string}
313
+ */
314
+ export function decodeHTML(html) {
315
+ return `${html}`
316
+ .replace(/&amp;/g, '&')
317
+ .replace(/&lt;/g, '<')
318
+ .replace(/&gt;/g, '>')
319
+ .replace(/&quot;/g, '"')
320
+ .replace(/&apos;/g, "'");
321
+ }
322
+
323
+ /**
324
+ * Encodes a html string
325
+ * @param {string} html
326
+ * @returns {string}
327
+ */
328
+ export function encodeHTML(html) {
329
+ return `${html}`
330
+ .replace(/&/g, '&amp;')
331
+ .replace(/</g, '&lt;')
332
+ .replace(/>/g, '&gt;')
333
+ .replace(/"/g, '&quot;')
334
+ .replace(/'/g, '&apos;')
335
+ .replace(/\r\n/g, '\n')
336
+ .replace(/[\r\n]/g, '\n');
337
+ }