@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 +6 -0
- package/package.json +1 -1
- package/src/dom-parts/AttributePart.js +5 -13
- package/src/dom-parts/TemplateResult.js +52 -28
- package/src/util/AttributeParser.js +4 -10
- package/src/util/DOMHelper.js +30 -0
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
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
orphan = true;
|
|
62
|
-
}
|
|
56
|
+
oldValue = value;
|
|
57
|
+
if (value == null) {
|
|
58
|
+
node.removeAttribute(name);
|
|
63
59
|
} else {
|
|
64
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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 ?
|
|
61
|
+
ssr ? makeSSRPlaceholderParams('directive') : '',
|
|
45
62
|
);
|
|
46
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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 +
|
|
85
|
+
replacement = replacement + makeSSRPlaceholderParams('noop');
|
|
65
86
|
}
|
|
66
87
|
replacement =
|
|
67
88
|
replacement +
|
|
68
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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 ?
|
|
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
|
-
|
|
123
|
-
|
|
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
|
|
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 ? '' :
|
|
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
|
|
382
|
-
|
|
383
|
-
|
|
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:
|
|
410
|
+
name: searchParams.get('name'),
|
|
411
|
+
initialValue: searchParams.get('initialValue'),
|
|
389
412
|
});
|
|
390
413
|
continue;
|
|
391
414
|
}
|
|
392
|
-
if (/^dom-part-\d
|
|
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
|
|
395
|
-
const [_,
|
|
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:
|
|
423
|
+
initialValue: searchParams.get('initialValue'),
|
|
400
424
|
});
|
|
401
425
|
continue;
|
|
402
426
|
}
|
|
403
|
-
if (/^dom-part-\d
|
|
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(/</g, '<')
|
|
135
|
-
.replace(/>/g, '>')
|
|
136
|
-
.replace(/"/g, '"')
|
|
137
|
-
.replace(/'/g, "'");
|
|
133
|
+
return `${attribute}`.replace(/"/g, '"').replace(/&/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(
|
|
143
|
+
.replace(/&/g, '&')
|
|
148
144
|
.replace(/"/g, '"')
|
|
149
|
-
.replace(/</g, '<')
|
|
150
|
-
.replace(/>/g, '>')
|
|
151
145
|
.replace(/\r\n/g, '\n')
|
|
152
146
|
.replace(/[\r\n]/g, '\n');
|
|
153
147
|
}
|
package/src/util/DOMHelper.js
CHANGED
|
@@ -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(/&/g, '&')
|
|
317
|
+
.replace(/</g, '<')
|
|
318
|
+
.replace(/>/g, '>')
|
|
319
|
+
.replace(/"/g, '"')
|
|
320
|
+
.replace(/'/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, '&')
|
|
331
|
+
.replace(/</g, '<')
|
|
332
|
+
.replace(/>/g, '>')
|
|
333
|
+
.replace(/"/g, '"')
|
|
334
|
+
.replace(/'/g, ''')
|
|
335
|
+
.replace(/\r\n/g, '\n')
|
|
336
|
+
.replace(/[\r\n]/g, '\n');
|
|
337
|
+
}
|