@webtides/element-js 0.7.4 → 1.0.0-alpha.10

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.
Files changed (51) hide show
  1. package/README.md +2 -2
  2. package/docs/README.md +174 -0
  3. package/docs/concepts/classes.md +167 -0
  4. package/docs/concepts/hierarchy.md +61 -0
  5. package/docs/concepts/lifecycle.md +45 -0
  6. package/docs/concepts/reactivity.md +14 -0
  7. package/docs/features/computed-properties.md +28 -0
  8. package/docs/features/dependency-injection-context-protocol.md +132 -0
  9. package/docs/features/events.md +155 -0
  10. package/docs/features/global-styles.md +131 -0
  11. package/docs/features/methods.md +34 -0
  12. package/docs/features/properties.md +56 -0
  13. package/docs/features/refs.md +47 -0
  14. package/docs/features/renderless.md +70 -0
  15. package/docs/features/store.md +143 -0
  16. package/docs/features/styles.md +169 -0
  17. package/docs/features/templates.md +308 -0
  18. package/docs/features/watch.md +54 -0
  19. package/docs/getting-started/upgrade-to-v1.md +243 -0
  20. package/docs/guide/guide.md +112 -0
  21. package/index.js +9 -2
  22. package/package.json +19 -27
  23. package/src/BaseElement.js +176 -86
  24. package/src/StyledElement.js +66 -55
  25. package/src/TemplateElement.js +34 -8
  26. package/src/dom-parts/AttributePart.js +167 -0
  27. package/src/dom-parts/ChildNodePart.js +329 -0
  28. package/src/dom-parts/NodePart.js +38 -0
  29. package/src/dom-parts/Part.js +12 -0
  30. package/src/dom-parts/TemplatePart.js +134 -0
  31. package/src/dom-parts/TemplateResult.js +342 -0
  32. package/src/dom-parts/directives.js +111 -0
  33. package/src/dom-parts/html.js +12 -0
  34. package/src/dom-parts/render.js +10 -0
  35. package/src/util/AttributeParser.js +62 -29
  36. package/src/util/DOMHelper.js +253 -5
  37. package/src/util/Directive.js +42 -0
  38. package/src/util/GlobalStylesStore.js +66 -0
  39. package/src/util/SerializeStateHelper.js +103 -0
  40. package/src/util/Store.js +92 -9
  41. package/src/util/defineElement.js +2 -2
  42. package/src/util/toString.js +2 -2
  43. package/CHANGELOG.md +0 -154
  44. package/dist/index.js +0 -1673
  45. package/dist/index.js.map +0 -7
  46. package/src/renderer/vanilla/TemplateElement.js +0 -50
  47. package/src/renderer/vanilla/index.js +0 -18
  48. package/src/renderer/vanilla/util/directives.js +0 -78
  49. package/src/renderer/vanilla/util/html.js +0 -65
  50. package/src/renderer/vanilla/util/render.js +0 -149
  51. package/src/util/i18n.js +0 -20
@@ -0,0 +1,342 @@
1
+ import { COMMENT_NODE, getNodePath } from '../util/DOMHelper.js';
2
+ import { encodeAttribute, isObjectLike } from '../util/AttributeParser.js';
3
+ import { TemplatePart } from './TemplatePart.js';
4
+
5
+ const voidElements = /^(?:area|base|br|col|embed|hr|img|input|link|meta|source|track|wbr)$/i;
6
+ const elements = /<([a-z]+[a-z0-9:._-]*)([^>]*?)(\/?)>/g;
7
+ // TODO: v this will not match any values with escaped quotes like onClick='console.log("\'test")'
8
+ const attributes = /([^\s]*)=((?:")[^"]*(?:")|(?:')[^']*(?:')|[^\s\/>]*)|([^\s\/>]*)/g;
9
+ const partPositions = /[\x01\x02]/g;
10
+ // \x01 Node.ELEMENT_NODE
11
+ // \x02 Node.ATTRIBUTE_NODE
12
+ // \x03 COMMENT.ATTRIBUTE_TOKEN
13
+ // \x04 Node.ATTRIBUTE_TOKEN
14
+
15
+ const interpolation = new RegExp(`(<!--dom-part-(\\d+)--><!--/dom-part-(\\d+)-->|(\\S[-\\w]+)="\x04(")?|\x04)`, 'g');
16
+
17
+ /**
18
+ * Given a template, find part positions as both nodes and attributes and
19
+ * return a string with placeholders as either comment nodes or named attributes.
20
+ * @param {TemplateStringsArray | string[]} templateStrings a template literal tag array
21
+ * @param {string} attributePlaceholders replace placeholders inside attribute values with this value
22
+ * @returns {string} X/HTML with prefixed comments or attributes
23
+ */
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()
26
+ let partIndex = 0;
27
+ // join all interpolations (for values) with a special placeholder and remove any whitespace
28
+ let template = templateStrings.join('\x01').trim();
29
+ // find (match) all elements to identify their attributes
30
+ template = template.replace(elements, (_, name, attributesString, trailingSlash) => {
31
+ let elementTagWithAttributes = name + attributesString.replaceAll('\x01', attributePlaceholders).trimEnd();
32
+ if (trailingSlash.length) elementTagWithAttributes += voidElements.test(name) ? ' /' : '></' + name;
33
+ // collect all attribute parts so that we can place matching comment nodes
34
+ const attributeParts = attributesString.replace(attributes, (attribute, name, valueWithQuotes, directive) => {
35
+ if (directive && directive === '\x01') {
36
+ return `<!--\x02$-->`;
37
+ }
38
+
39
+ // remove quotes from attribute value to normalize the value
40
+ const value =
41
+ valueWithQuotes?.startsWith('"') || valueWithQuotes?.startsWith("'")
42
+ ? valueWithQuotes.slice(1, -1)
43
+ : valueWithQuotes;
44
+ const partsCount = (attribute.match(/\x01/g) || []).length;
45
+ const parts = [];
46
+ for (let index = 0; index < partsCount; index++) {
47
+ parts.push(`<!--\x02:${name}=${value.replaceAll('\x01', '\x03')}-->`);
48
+ }
49
+ return parts.join('');
50
+ });
51
+ return `
52
+ ${attributesString.includes('\x01') ? attributeParts : ''}
53
+ <${elementTagWithAttributes}>
54
+ `;
55
+ });
56
+ // replace interpolation placeholders with our indexed markers
57
+ template = template.replace(partPositions, (partPosition) => {
58
+ if (partPosition === '\x01') {
59
+ return `<!--dom-part-${partIndex}--><!--/dom-part-${partIndex++}-->`;
60
+ }
61
+ if (partPosition === '\x02') {
62
+ return `dom-part-${partIndex++}`;
63
+ }
64
+ });
65
+ return `
66
+ <!--template-part-->
67
+ ${template}
68
+ <!--/template-part-->
69
+ `;
70
+ // TODO: ^ this is actually important because of the whitespace
71
+ };
72
+
73
+ /**
74
+ * @param {String} name
75
+ * @param {any} value
76
+ * @param {Boolean} isSingleValue
77
+ * @return {String}
78
+ */
79
+ const attribute = (name, value, isSingleValue = true) => {
80
+ return ` ${name}="${encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value)}${
81
+ isSingleValue ? '"' : ''
82
+ }`;
83
+ };
84
+
85
+ /**
86
+ * @param {any} value
87
+ * @return {String}
88
+ */
89
+ const getValue = (value) => {
90
+ switch (typeof value) {
91
+ case 'string':
92
+ return encodeAttribute(value);
93
+ case 'boolean':
94
+ case 'number':
95
+ return String(value);
96
+ case 'object':
97
+ switch (true) {
98
+ case value === null:
99
+ return '';
100
+ case Array.isArray(value):
101
+ return value.map(getValue).join('');
102
+ case value instanceof TemplateResult:
103
+ return value.toString();
104
+ case value.__unsafeHTML:
105
+ return value.string;
106
+ default:
107
+ console.log('Cannot getValue for', value);
108
+ }
109
+ break;
110
+ case 'function':
111
+ return getValue(value());
112
+ }
113
+ return value == null ? '' : encodeAttribute(String(value));
114
+ };
115
+
116
+ /** @type {Map<TemplateStringsArray, *[]>} */
117
+ const parsedUpdates = new WeakMap();
118
+
119
+ /** @type {Map<Element, TemplatePart>} */
120
+ const templateParts = new WeakMap();
121
+
122
+ export class TemplateResult {
123
+ /**
124
+ * @param {TemplateStringsArray} strings
125
+ * @param {any[]} values
126
+ */
127
+ constructor(strings, ...values) {
128
+ this.strings = strings;
129
+ this.values = values;
130
+ }
131
+
132
+ /**
133
+ * @param {Element} domNode
134
+ */
135
+ renderInto(domNode) {
136
+ let serverSideRendered = false;
137
+ let templatePart = templateParts.get(domNode);
138
+ if (!templatePart) {
139
+ const startNode = Array.from(domNode.childNodes)
140
+ .filter((node) => node.nodeType === COMMENT_NODE)
141
+ .find((node) => node.data === 'template-part');
142
+
143
+ serverSideRendered = startNode !== undefined;
144
+
145
+ templatePart = new TemplatePart(startNode, this);
146
+ templateParts.set(domNode, templatePart);
147
+
148
+ if (!serverSideRendered) {
149
+ domNode.replaceChildren(...templatePart.childNodes);
150
+ }
151
+ } else {
152
+ templatePart.update(this);
153
+ }
154
+ }
155
+
156
+ /**
157
+ * @param {String[]} strings
158
+ * @return {(() => String)[]}
159
+ */
160
+ parse(strings) {
161
+ const html = createTemplateString(strings, '\x04');
162
+ const parts = [];
163
+ let i = 0;
164
+ let match = null;
165
+ while ((match = interpolation.exec(html))) {
166
+ const pre = html.slice(i, match.index);
167
+ i = match.index + match[0].length;
168
+ if (match[2]) {
169
+ // ChildNodePart
170
+ const index = match[2];
171
+ parts.push((value) => `${pre}<!--dom-part-${index}-->${getValue(value)}<!--/dom-part-${index}-->`);
172
+ } else if (match[4]) {
173
+ // AttributePart with single interpolation or the first interpolation right after the attribute=
174
+ const isSingleValue = match[5] !== undefined;
175
+ let name = match[4];
176
+ switch (true) {
177
+ case name[0] === '?':
178
+ const booleanName = name.slice(1).toLowerCase();
179
+ parts.push((value) => {
180
+ if (!value) return '';
181
+ return `${pre} ${booleanName}=""`;
182
+ });
183
+ break;
184
+ case name[0] === '.':
185
+ const lower = name.slice(1).toLowerCase();
186
+ parts.push((value) => {
187
+ let result = pre;
188
+ // null, undefined, and false are not shown at all
189
+ if (value === null || value === undefined || value === '') {
190
+ result += attribute(lower, '');
191
+ } else {
192
+ // in all other cases, just escape it in quotes
193
+ result += attribute(lower, value, isSingleValue);
194
+ }
195
+ return result;
196
+ });
197
+ break;
198
+ case name[0] === '@':
199
+ name = 'on' + name.slice(1);
200
+ case name[0] === 'o' && name[1] === 'n':
201
+ parts.push((value) => {
202
+ return pre;
203
+ });
204
+ break;
205
+ default:
206
+ parts.push((value) => {
207
+ let result = pre;
208
+ if (value != null) {
209
+ result += attribute(name, value, isSingleValue);
210
+ }
211
+ return result;
212
+ });
213
+ break;
214
+ }
215
+ } else {
216
+ // AttributePart in the middle of an attribute value or NodePart
217
+ parts.push((value) => {
218
+ let result = pre;
219
+ // TODO: we currently cannot distinguish between NodeParts and AttributeParts before
220
+ if (isObjectLike(value) && value.directiveClass) {
221
+ // NodePart
222
+ const directive = new value.directiveClass();
223
+ result += directive.stringify(...value.values);
224
+ } else if (value != null) {
225
+ result += encodeAttribute(isObjectLike(value) ? JSON.stringify(value) : value);
226
+ }
227
+ return result;
228
+ });
229
+ }
230
+ }
231
+
232
+ // We couldn't correctly parse parts from the template
233
+ if (parts.length !== strings.length - 1) {
234
+ throw {
235
+ name: 'ParseTemplateError',
236
+ message: 'Could not parse parts from template correctly. Parts length has not the expected length.',
237
+ strings,
238
+ templateString: html,
239
+ parts,
240
+ };
241
+ }
242
+
243
+ if (parts.length) {
244
+ const last = parts[parts.length - 1];
245
+ const chunk = html.slice(i);
246
+ parts[parts.length - 1] = (value) => last(value) + chunk;
247
+ } else {
248
+ parts.push(() => html);
249
+ }
250
+ return parts;
251
+ }
252
+
253
+ /**
254
+ * @param {Node[]} childNodes
255
+ * @return {object[]}
256
+ */
257
+ parseParts(childNodes) {
258
+ // we always create a template fragment so that we can start at the root for traversing the node path
259
+ const range = globalThis.document?.createRange();
260
+ range.setStartBefore(childNodes[0]);
261
+ range.setEndAfter(childNodes[childNodes.length - 1]);
262
+ const template = range.cloneContents();
263
+
264
+ const treeWalker = globalThis.document?.createTreeWalker(template, 128);
265
+ let node = treeWalker.nextNode(); //to skip the root template-part
266
+ const parts = [];
267
+
268
+ let nestedLevel = 0;
269
+ // search for parts through numbered comment nodes with placeholders
270
+ while ((node = treeWalker.nextNode())) {
271
+ if (/^template-part$/.test(node.data)) {
272
+ nestedLevel++;
273
+ continue;
274
+ }
275
+ if (/^\/template-part$/.test(node.data)) {
276
+ if (nestedLevel > 0) {
277
+ nestedLevel--;
278
+ }
279
+ continue;
280
+ }
281
+ if (nestedLevel > 0) {
282
+ continue;
283
+ }
284
+ if (/^dom-part-\d+$/.test(node.data)) {
285
+ parts.push({ type: 'node', path: getNodePath(node) });
286
+ continue;
287
+ }
288
+ if (/^dom-part-\d+:/.test(node.data)) {
289
+ const [_, ...attribute] = node.data.split(':');
290
+ const [name, ...initialValue] = attribute.join(':').split('=');
291
+ parts.push({
292
+ type: 'attribute',
293
+ path: getNodePath(node),
294
+ name: name,
295
+ initialValue: initialValue.join('='),
296
+ });
297
+ continue;
298
+ }
299
+ if (/^dom-part-\d+\$/.test(node.data)) {
300
+ parts.push({ type: 'directive', path: getNodePath(node) });
301
+ }
302
+ }
303
+
304
+ // We couldn't correctly parse parts from the template
305
+ if (parts.length !== this.strings.length - 1) {
306
+ throw {
307
+ name: 'ParseTemplateError',
308
+ message:
309
+ 'Could not parse parts from template correctly. Parts length has not the expected length. -> ' +
310
+ JSON.stringify({
311
+ strings: this.strings,
312
+ templateString: this.templateString,
313
+ childNodes,
314
+ template,
315
+ parts,
316
+ }),
317
+ };
318
+ }
319
+
320
+ return parts;
321
+ }
322
+
323
+ /**
324
+ * find interpolations in the given template for nodes and attributes and
325
+ * return a string with placeholders as either comment nodes or named attributes.
326
+ * @returns {string} template with tagged placeholders for values
327
+ */
328
+ get templateString() {
329
+ return createTemplateString(this.strings);
330
+ }
331
+
332
+ toString() {
333
+ let updates = parsedUpdates.get(this.strings);
334
+
335
+ if (!updates) {
336
+ updates = this.parse(this.strings);
337
+ parsedUpdates.set(this.strings, updates);
338
+ }
339
+
340
+ return this.values.length ? this.values.map((value, index) => updates[index](value)).join('') : updates[0]();
341
+ }
342
+ }
@@ -0,0 +1,111 @@
1
+ import { camelToDash, decodeAttribute, encodeAttribute } from '../util/AttributeParser.js';
2
+ import { TemplateResult } from './TemplateResult.js';
3
+ import { convertStringToHTML, convertStringToTemplate } from '../util/DOMHelper.js';
4
+ import { defineDirective, Directive } from '../util/Directive.js';
5
+
6
+ /**
7
+ * Maps a list of classes to an element from an object.
8
+ * The keys will become the classes when the value is truthy.
9
+ * @param {Object.<string, string | boolean | number>} map
10
+ * @returns {string} string of classes joined together
11
+ */
12
+ const classMap = (map) => {
13
+ const classes = [];
14
+ for (const [key, value] of Object.entries(map)) {
15
+ if (value) classes.push(key);
16
+ }
17
+ return classes.join(' ');
18
+ };
19
+
20
+ /**
21
+ * Maps a list of styles to an element from an object.
22
+ * The keys will become the properties and the values will become the values.
23
+ * @param {Object.<string, string | undefined | null>} map
24
+ * @returns {string} string of styles joined together
25
+ */
26
+ const styleMap = (map) => {
27
+ const styles = [];
28
+ for (const [key, value] of Object.entries(map)) {
29
+ if (value) styles.push(`${key}:${value};`);
30
+ }
31
+ return styles.join(' ');
32
+ };
33
+
34
+ /**
35
+ * Renders one of two template parts based on a condition.
36
+ * @param {boolean} condition
37
+ * @param {TemplateResult | string} trueCase
38
+ * @param {TemplateResult | string} [falseCase]
39
+ * @returns {TemplateResult | string}
40
+ */
41
+ const when = (condition, trueCase, falseCase) => {
42
+ return condition ? trueCase : falseCase;
43
+ };
44
+
45
+ /**
46
+ * Chooses and evaluates a template part from a map of cases based on matching the given value to a case.
47
+ * @param {string} value
48
+ * @param {Object.<string, TemplateResult | string>} cases
49
+ * @param {TemplateResult | string} [defaultCase]
50
+ * @returns {TemplateResult | string}
51
+ */
52
+ const choose = (value, cases, defaultCase) => {
53
+ return cases[value] || defaultCase;
54
+ };
55
+
56
+ /**
57
+ * Renders a given string as HTML instead of text or as unescaped string in SSR mode
58
+ * @param {string} string
59
+ * @returns {(function(): Node) | { __unsafeHTML: true, string: string }}
60
+ */
61
+ const unsafeHTML = (string) => {
62
+ if (!globalThis.DOMParser) {
63
+ return { __unsafeHTML: true, string };
64
+ }
65
+ const node = convertStringToHTML(string).firstChild;
66
+ const importedNode = globalThis.document?.importNode(node, true);
67
+ return () => importedNode;
68
+ };
69
+
70
+ /**
71
+ * Renders multiple attributes as key="value" pairs from a map of attributes
72
+ */
73
+ export class SpreadAttributesDirective extends Directive {
74
+ /**
75
+ * @param {Object.<string, any>} attributes
76
+ */
77
+ update(attributes) {
78
+ // TODO: what if the attribute was already there..?!
79
+ // TODO: what if the attribute was there from a previous render/update, but not anymore?!
80
+ for (const name of Object.keys(attributes)) {
81
+ const value = attributes[name];
82
+ this.node.setAttribute(
83
+ camelToDash(name),
84
+ encodeAttribute(typeof value !== 'string' ? JSON.stringify(value) : value),
85
+ );
86
+ }
87
+ }
88
+
89
+ /**
90
+ * @param {Object.<string, any>} attributes
91
+ * @returns {string}
92
+ */
93
+ stringify(attributes) {
94
+ return Object.keys(attributes)
95
+ .map((name) => {
96
+ let value = attributes[name];
97
+ return `${camelToDash(name)}='${encodeAttribute(
98
+ typeof value !== 'string' ? JSON.stringify(value) : value,
99
+ )}'`;
100
+ })
101
+ .join(' ');
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Renders multiple attributes as key="value" pairs from a map of attributes
107
+ * @param {Object.<string, any>} attributes
108
+ */
109
+ const spreadAttributes = defineDirective(SpreadAttributesDirective);
110
+
111
+ export { classMap, styleMap, when, choose, unsafeHTML, spreadAttributes };
@@ -0,0 +1,12 @@
1
+ import { TemplateResult } from './TemplateResult.js';
2
+
3
+ /**
4
+ * @param {TemplateStringsArray} strings
5
+ * @param {any[]} values
6
+ * @return {TemplateResult}
7
+ */
8
+ const html = function (strings, ...values) {
9
+ return new TemplateResult(strings, ...values);
10
+ };
11
+
12
+ export { html };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Render a template string into the given DOM node
3
+ * @param {TemplateResult | string} template
4
+ * @param {Element} domNode
5
+ */
6
+ const render = (template, domNode) => {
7
+ template.renderInto(domNode);
8
+ };
9
+
10
+ export { render };
@@ -1,23 +1,48 @@
1
+ /**
2
+ * Tests if a value is of type `object`
3
+ * @param {any} value
4
+ * @returns {boolean}
5
+ */
1
6
  export function isObjectLike(value) {
2
7
  return typeof value == 'object' && value !== null;
3
8
  }
4
9
 
5
- export function isJSON(str) {
10
+ /**
11
+ * Tests if a value can be parsed as JSON
12
+ * @param {any} value
13
+ * @returns {boolean}
14
+ */
15
+ export function isJSON(value) {
6
16
  try {
7
- return JSON.parse(str) && !!str;
17
+ return JSON.parse(value) && !!value;
8
18
  } catch (e) {
9
19
  return false;
10
20
  }
11
21
  }
12
22
 
23
+ /**
24
+ * Tests if a value is `boolean`
25
+ * @param {any} value
26
+ * @returns {boolean}
27
+ */
13
28
  export function isBoolean(value) {
14
29
  return value === 'true' || value === 'false';
15
30
  }
16
31
 
32
+ /**
33
+ * Parses a value to `boolean`
34
+ * @param {any} value
35
+ * @returns {boolean}
36
+ */
17
37
  export function parseBoolean(value) {
18
38
  return value === 'true';
19
39
  }
20
40
 
41
+ /**
42
+ * Tests if a value is of type `string`
43
+ * @param {any} value
44
+ * @returns {boolean}
45
+ */
21
46
  export function isString(value) {
22
47
  return (
23
48
  typeof value === 'string' ||
@@ -25,26 +50,40 @@ export function isString(value) {
25
50
  );
26
51
  }
27
52
 
53
+ /**
54
+ * Tests if a value is of type `number`
55
+ * @param {any} value
56
+ * @returns {boolean}
57
+ */
28
58
  export function isNumber(value) {
29
59
  return new RegExp('^-?(0|0\\.\\d+|[1-9]\\d*(\\.\\d+)?)$').test(value);
30
60
  }
31
61
 
62
+ /**
63
+ * Tests if a value is of type `NaN`
64
+ * @param {any} value
65
+ * @returns {boolean}
66
+ */
32
67
  export function isNaN(value) {
33
68
  return Number.isNaN(value);
34
69
  }
35
70
 
36
71
  /**
37
- * deep compare two values primitive or complex
38
- *
39
- * @param valueA
40
- * @param valueB
72
+ * Compare two values (deep). It compares primitive or complex values with JSON stringify.
73
+ * @param {any} valueA
74
+ * @param {any} valueB
41
75
  * @returns {boolean}
42
76
  */
43
-
44
77
  export function deepEquals(valueA, valueB) {
45
78
  return JSON.stringify(valueA) === JSON.stringify(valueB);
46
79
  }
47
80
 
81
+ /**
82
+ * Parses an attribute value that comes in as a `string` to its corresponding type
83
+ * @param {string} value
84
+ * @param {PropertyOptions} options
85
+ * @returns {string | number | boolean | object | array}
86
+ */
48
87
  export function parseAttribute(value, options = {}) {
49
88
  if (options.parse === false || !isString(value)) {
50
89
  // no-op
@@ -67,7 +106,8 @@ export function parseAttribute(value, options = {}) {
67
106
 
68
107
  /**
69
108
  * Replaces dashed-expression (i.e. some-value) to a camel-cased expression (i.e. someValue)
70
- * @returns string
109
+ * @param {string} string
110
+ * @returns {string}
71
111
  */
72
112
  export function dashToCamel(string) {
73
113
  if (string.indexOf('-') === -1) return string;
@@ -77,44 +117,37 @@ export function dashToCamel(string) {
77
117
 
78
118
  /**
79
119
  * Replaces camel-cased expression (i.e. someValue) to a dashed-expression (i.e. some-value)
80
- * @returns string
120
+ * @param {string} string
121
+ * @returns {string}
81
122
  */
82
123
  export function camelToDash(string) {
83
124
  return string.replace(/([A-Z])/g, '-$1').toLowerCase();
84
125
  }
85
126
 
86
127
  /**
87
- * Decodes an attribute. Can only be encoded with the `encodeAttribute` method which
88
- * is exported from this file.
89
- *
90
- * @param attribute
128
+ * Decodes an attribute.
129
+ * @param {string} attribute
91
130
  * @returns {string}
92
131
  */
93
132
  export function decodeAttribute(attribute) {
94
133
  return `${attribute}`
95
- .replace(/\$lt;/g, '<')
96
- .replace(/\$gt;/g, '>')
97
- .replace(/\$quot;/g, '"')
98
- .replace(/\$apos;/g, "'");
134
+ .replace(/&lt;/g, '<')
135
+ .replace(/&gt;/g, '>')
136
+ .replace(/&quot;/g, '"')
137
+ .replace(/&apos;/g, "'");
99
138
  }
100
139
 
101
140
  /**
102
- * Encodes an attribute. Can only be decoded with the `decodeAttribute` method which
103
- * is exported from this file.
104
- *
105
- * This does not use the standard way to encode an attribute, as the dom parser
106
- * that renders the html would understand and automatically decode it. Which results
107
- * in non sanitized html output.
108
- *
109
- * @param attribute
141
+ * Encodes an attribute.
142
+ * @param {string} attribute
110
143
  * @returns {string}
111
144
  */
112
145
  export function encodeAttribute(attribute) {
113
146
  return `${attribute}`
114
- .replace(/'/g, '$apos;')
115
- .replace(/"/g, '$quot;')
116
- .replace(/</g, '$lt;')
117
- .replace(/>/g, '$gt;')
147
+ .replace(/'/g, '&apos;')
148
+ .replace(/"/g, '&quot;')
149
+ .replace(/</g, '&lt;')
150
+ .replace(/>/g, '&gt;')
118
151
  .replace(/\r\n/g, '\n')
119
152
  .replace(/[\r\n]/g, '\n');
120
153
  }