~repos /atoms-element

#js

git clone https://pyrossh.dev/repos/atoms-element.git

A simple web component library for defining your custom elements. It works on both client and server.


6102973a Peter John

4 years ago
fix remaining issues in static generation
Files changed (3) hide show
  1. src/index.js +26 -14
  2. src/lit-html-server.js +0 -1084
  3. test/index.test.js +44 -2
src/index.js CHANGED
@@ -5,22 +5,36 @@ const isBrowser = typeof window !== 'undefined';
5
5
  export { html, isBrowser };
6
6
 
7
7
  const lastAttributeNameRegex =
8
- // eslint-disable-next-line no-control-regex
9
8
  /([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
10
9
 
10
+ const wrapAttribute = (attrName, suffix, text, v) => {
11
+ let buffer = text;
12
+ const hasQuote = suffix && suffix.includes(`="`);
13
+ if (attrName && !hasQuote) {
14
+ buffer += `"`;
15
+ }
16
+ buffer += v;
17
+ if (attrName && !hasQuote) {
18
+ buffer += `"`;
19
+ }
20
+ return buffer;
21
+ };
22
+
11
23
  export const render = isBrowser
12
24
  ? litRender
13
25
  : (template) => {
14
26
  let js = '';
15
27
  template.strings.forEach((text, i) => {
16
28
  const value = template.values[i];
17
- // TODO: remove @click @mouseleave= .handleClick props
18
- // either here or in lit-html
19
- // console.log('text', text);
20
29
  const type = typeof value;
30
+ let attrName, suffix;
31
+ const matchName = lastAttributeNameRegex.exec(text);
32
+ if (matchName) {
33
+ attrName = matchName[2];
34
+ suffix = matchName[3];
35
+ }
21
36
  if (value === null || !(type === 'object' || type === 'function' || type === 'undefined')) {
22
- js += text;
23
- js += type !== 'string' ? String(value) : value;
37
+ js += wrapAttribute(attrName, suffix, text, type !== 'string' ? String(value) : value);
24
38
  } else if (Array.isArray(value)) {
25
39
  js += text;
26
40
  // Array of TemplateResult
@@ -28,21 +42,19 @@ export const render = isBrowser
28
42
  js += render(v);
29
43
  });
30
44
  } else if (type === 'object') {
31
- js += text;
32
45
  // TemplateResult
33
46
  if (value.strings && value.type === 'html') {
47
+ js += text;
34
48
  js += render(value);
35
49
  } else {
36
- js += JSON.stringify(value).replace(/"/g, `'`);
50
+ js += wrapAttribute(attrName, suffix, text, JSON.stringify(value).replace(/"/g, `'`));
37
51
  }
38
52
  } else if (type == 'function') {
39
- const matchName = lastAttributeNameRegex.exec(text);
40
- if (matchName) {
53
+ if (attrName) {
41
- let [, prefix, name, suffix] = matchName;
42
- js += text.replace(' ' + name + '=', '');
54
+ js += text.replace(' ' + attrName + '=', '');
43
55
  } else {
44
- js += text;
56
+ // js += text;
45
- js += value();
57
+ // js += value();
46
58
  }
47
59
  } else if (type !== 'undefined') {
48
60
  js += text;
src/lit-html-server.js DELETED
@@ -1,1084 +0,0 @@
1
- /**
2
- * Collection of shared utilities used by directives.
3
- * Manually added to ensure that directives can be used by both index.js and browser.js
4
- */
5
-
6
- /**
7
- * A value for parts that signals a Part to clear its content
8
- */
9
- const nothing = '__nothing-lit-html-server-string__';
10
-
11
- /**
12
- * A prefix value for strings that should not be escaped
13
- */
14
- const unsafePrefixString = '__unsafe-lit-html-server-string__';
15
-
16
- /**
17
- * Determine if "part" is a NodePart
18
- *
19
- * @param { unknown } part
20
- * @returns { part is NodePart }
21
- */
22
- function isNodePart(part) {
23
- // @ts-ignore
24
- return part && part.getValue !== undefined && !('name' in part);
25
- }
26
-
27
- /**
28
- * Determine if "part" is an AttributePart
29
- *
30
- * @param { unknown } part
31
- * @returns { part is AttributePart }
32
- */
33
- function isAttributePart(part) {
34
- // @ts-ignore
35
- return part && part.getValue !== undefined && 'name' in part;
36
- }
37
-
38
- /**
39
- * Determine if "value" is a primitive
40
- *
41
- * @param { unknown } value
42
- * @returns { value is null|string|boolean|number }
43
- */
44
- function isPrimitive(value) {
45
- const type = typeof value;
46
-
47
- return value === null || !(type === 'object' || type === 'function');
48
- }
49
-
50
- /**
51
- * Determine if "obj" is a directive function
52
- *
53
- * @param { unknown } fn
54
- * @returns { fn is Function }
55
- */
56
- function isDirective(fn) {
57
- // @ts-ignore
58
- return typeof fn === 'function' && fn.isDirective;
59
- }
60
-
61
- /**
62
- * Determine whether "result" is a TemplateResult
63
- *
64
- * @param { unknown } result
65
- * @returns { result is TemplateResult }
66
- */
67
- function isTemplateResult(result) {
68
- // @ts-ignore
69
- return result && typeof result.template !== 'undefined' && typeof result.values !== 'undefined';
70
- }
71
-
72
- /**
73
- * Determine if "iterator" is an synchronous iterator
74
- *
75
- * @param { unknown } iterator
76
- * @returns { iterator is IterableIterator<unknown> }
77
- */
78
- function isSyncIterator(iterator) {
79
- return (
80
- iterator != null &&
81
- // Ignore strings (which are also iterable)
82
- typeof iterator !== 'string' &&
83
- // @ts-ignore
84
- typeof iterator[Symbol.iterator] === 'function'
85
- );
86
- }
87
-
88
- /**
89
- * Determine if "value" is an object
90
- *
91
- * @param { unknown } value
92
- * @returns { value is object }
93
- */
94
- function isObject(value) {
95
- return Object.prototype.toString.call(value) === '[object Object]';
96
- }
97
-
98
- /**
99
- * Determine if "value" is a Buffer
100
- *
101
- * @param { unknown } value
102
- * @returns { value is Buffer }
103
- */
104
- function isBuffer(value) {
105
- return Buffer.isBuffer(value);
106
- }
107
-
108
- /**
109
- * Determine if "value" is an Array
110
- *
111
- * @param { unknown } value
112
- * @returns { value is Array }
113
- */
114
- function isArray(value) {
115
- return Array.isArray(value);
116
- }
117
-
118
- // https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#output-encoding-rules-summary
119
- // https://github.com/mathiasbynens/jsesc/blob/master/jsesc.js
120
-
121
- /** @type { { [name: string]: string } } */
122
- const HTML_ESCAPES = {
123
- '"': '&quot;',
124
- "'": '&#x27;',
125
- '&': '&amp;',
126
- '<': '&lt;',
127
- '>': '&gt;',
128
- };
129
- const RE_HTML = /["'&<>]/g;
130
- const RE_SCRIPT_STYLE_TAG = /<\/(script|style)/gi;
131
-
132
- /**
133
- * Safely escape "string" for inlining
134
- *
135
- * @param { string } string
136
- * @param { string } context - one of text|attribute|script|style
137
- * @returns { string }
138
- */
139
- function escape(string, context = 'text') {
140
- switch (context) {
141
- case 'script':
142
- case 'style':
143
- return string.replace(RE_SCRIPT_STYLE_TAG, '<\\/$1').replace(/<!--/g, '\\x3C!--');
144
- case 'attribute':
145
- case 'text':
146
- default:
147
- return string.replace(RE_HTML, (match) => HTML_ESCAPES[match]);
148
- }
149
- }
150
-
151
- const EMPTY_STRING_BUFFER = Buffer.from('');
152
-
153
- /**
154
- * Base class interface for Node/Attribute parts
155
- */
156
- class Part {
157
- /**
158
- * Constructor
159
- *
160
- * @param { string } tagName
161
- */
162
- constructor(tagName) {
163
- this.tagName = tagName;
164
- this._value;
165
- }
166
-
167
- /**
168
- * Store the current value.
169
- * Used by directives to temporarily transfer value
170
- * (value will be deleted after reading).
171
- *
172
- * @param { any } value
173
- */
174
- setValue(value) {
175
- this._value = value;
176
- }
177
-
178
- /**
179
- * Retrieve resolved string from passed "value"
180
- *
181
- * @param { any } value
182
- * @param { RenderOptions } [options]
183
- * @returns { any }
184
- */
185
- getValue(value, options) {
186
- return value;
187
- }
188
-
189
- /**
190
- * No-op
191
- */
192
- commit() {}
193
- }
194
-
195
- /**
196
- * A dynamic template part for text nodes
197
- */
198
- class NodePart extends Part {
199
- /**
200
- * Retrieve resolved value given passed "value"
201
- *
202
- * @param { any } value
203
- * @param { RenderOptions } [options]
204
- * @returns { any }
205
- */
206
- getValue(value, options) {
207
- return resolveNodeValue(value, this);
208
- }
209
- }
210
-
211
- /**
212
- * A dynamic template part for attributes.
213
- * Unlike text nodes, attributes may contain multiple strings and parts.
214
- */
215
- class AttributePart extends Part {
216
- /**
217
- * Constructor
218
- *
219
- * @param { string } name
220
- * @param { Array<Buffer> } strings
221
- * @param { string } tagName
222
- */
223
- constructor(name, strings, tagName) {
224
- super(tagName);
225
- this.name = name;
226
- this.strings = strings;
227
- this.length = strings.length - 1;
228
- this.prefix = Buffer.from(`${this.name}="`);
229
- this.suffix = Buffer.from(`${this.strings[this.length]}"`);
230
- }
231
-
232
- /**
233
- * Retrieve resolved string Buffer from passed "values".
234
- * Resolves to a single string, or Promise for a single string,
235
- * even when responsible for multiple values.
236
- *
237
- * @param { Array<unknown> } values
238
- * @param { RenderOptions } [options]
239
- * @returns { Buffer | Promise<Buffer> }
240
- */
241
- getValue(values, options) {
242
- let chunks = [this.prefix];
243
- let chunkLength = this.prefix.length;
244
- let pendingChunks;
245
-
246
- for (let i = 0; i < this.length; i++) {
247
- const string = this.strings[i];
248
- let value = resolveAttributeValue(values[i], this, options !== undefined ? options.serializePropertyAttributes : false);
249
-
250
- // Bail if 'nothing'
251
- if (value === nothing) {
252
- return EMPTY_STRING_BUFFER;
253
- }
254
-
255
- chunks.push(string);
256
- chunkLength += string.length;
257
-
258
- if (isBuffer(value)) {
259
- chunks.push(value);
260
- chunkLength += value.length;
261
- } else if (isArray(value)) {
262
- for (const chunk of value) {
263
- chunks.push(chunk);
264
- chunkLength += chunk.length;
265
- }
266
- }
267
- }
268
-
269
- chunks.push(this.suffix);
270
- chunkLength += this.suffix.length;
271
- if (pendingChunks !== undefined) {
272
- return Promise.all(pendingChunks).then(() => Buffer.concat(chunks, chunkLength));
273
- }
274
- return Buffer.concat(chunks, chunkLength);
275
- }
276
- }
277
-
278
- /**
279
- * A dynamic template part for boolean attributes.
280
- * Boolean attributes are prefixed with "?"
281
- */
282
- class BooleanAttributePart extends AttributePart {
283
- /**
284
- * Constructor
285
- *
286
- * @param { string } name
287
- * @param { Array<Buffer> } strings
288
- * @param { string } tagName
289
- * @throws error when multiple expressions
290
- */
291
- constructor(name, strings, tagName) {
292
- super(name, strings, tagName);
293
-
294
- this.nameAsBuffer = Buffer.from(this.name);
295
-
296
- if (strings.length !== 2 || strings[0] === EMPTY_STRING_BUFFER || strings[1] === EMPTY_STRING_BUFFER) {
297
- throw Error('Boolean attributes can only contain a single expression');
298
- }
299
- }
300
-
301
- /**
302
- * Retrieve resolved string Buffer from passed "values".
303
- *
304
- * @param { Array<unknown> } values
305
- * @param { RenderOptions } [options]
306
- * @returns { Buffer | Promise<Buffer> }
307
- */
308
- getValue(values, options) {
309
- let value = values[0];
310
-
311
- if (isDirective(value)) {
312
- value = resolveDirectiveValue(value, this);
313
- }
314
-
315
- return value ? this.nameAsBuffer : EMPTY_STRING_BUFFER;
316
- }
317
- }
318
-
319
- /**
320
- * A dynamic template part for property attributes.
321
- * Property attributes are prefixed with "."
322
- */
323
- class PropertyAttributePart extends AttributePart {
324
- /**
325
- * Retrieve resolved string Buffer from passed "values".
326
- * Returns an empty string unless "options.serializePropertyAttributes=true"
327
- *
328
- * @param { Array<unknown> } values
329
- * @param { RenderOptions } [options]
330
- * @returns { Buffer | Promise<Buffer> }
331
- */
332
- getValue(values, options) {
333
- if (options !== undefined && options.serializePropertyAttributes) {
334
- const value = super.getValue(values, options);
335
- const prefix = Buffer.from('.');
336
-
337
- return Buffer.concat([prefix, value]);
338
- }
339
-
340
- return EMPTY_STRING_BUFFER;
341
- }
342
- }
343
-
344
- /**
345
- * A dynamic template part for event attributes.
346
- * Event attributes are prefixed with "@"
347
- */
348
- class EventAttributePart extends AttributePart {
349
- /**
350
- * Retrieve resolved string Buffer from passed "values".
351
- * Event bindings have no server-side representation,
352
- * so always returns an empty string.
353
- *
354
- * @param { Array<unknown> } values
355
- * @param { RenderOptions } [options]
356
- * @returns { Buffer }
357
- */
358
- getValue(values, options) {
359
- return EMPTY_STRING_BUFFER;
360
- }
361
- }
362
-
363
- /**
364
- * Resolve "value" to string if possible
365
- *
366
- * @param { unknown } value
367
- * @param { AttributePart } part
368
- * @param { boolean } [serialiseObjectsAndArrays]
369
- * @returns { any }
370
- */
371
- function resolveAttributeValue(value, part, serialiseObjectsAndArrays = false) {
372
- if (isDirective(value)) {
373
- value = resolveDirectiveValue(value, part);
374
- }
375
-
376
- if (value === nothing) {
377
- return value;
378
- }
379
-
380
- if (isPrimitive(value)) {
381
- const string = typeof value !== 'string' ? String(value) : value;
382
- // Escape if not prefixed with unsafePrefixString, otherwise strip prefix
383
- return Buffer.from(string.indexOf(unsafePrefixString) === 0 ? string.slice(33) : escape(string, 'attribute'));
384
- } else if (typeof value === 'object') {
385
- return Buffer.from(JSON.stringify(value).replace(/"/g, `'`));
386
- } else if (isBuffer(value)) {
387
- return value;
388
- } else if (serialiseObjectsAndArrays && (isObject(value) || isArray(value))) {
389
- return Buffer.from(escape(JSON.stringify(value), 'attribute'));
390
- } else if (isSyncIterator(value)) {
391
- if (!isArray(value)) {
392
- value = Array.from(value);
393
- }
394
- return Buffer.concat(
395
- // @ts-ignore: already converted to Array
396
- value.reduce((values, value) => {
397
- value = resolveAttributeValue(value, part, serialiseObjectsAndArrays);
398
- // Flatten
399
- if (isArray(value)) {
400
- return values.concat(value);
401
- }
402
- values.push(value);
403
- return values;
404
- }, []),
405
- );
406
- } else {
407
- return Buffer.from(String(value));
408
- }
409
- }
410
-
411
- /**
412
- * Resolve "value" to string Buffer if possible
413
- *
414
- * @param { unknown } value
415
- * @param { NodePart } part
416
- * @returns { any }
417
- */
418
- function resolveNodeValue(value, part) {
419
- if (isDirective(value)) {
420
- value = resolveDirectiveValue(value, part);
421
- }
422
-
423
- if (value === nothing || value === undefined) {
424
- return EMPTY_STRING_BUFFER;
425
- }
426
-
427
- if (isPrimitive(value)) {
428
- const string = typeof value !== 'string' ? String(value) : value;
429
- // Escape if not prefixed with unsafePrefixString, otherwise strip prefix
430
- return Buffer.from(
431
- string.indexOf(unsafePrefixString) === 0
432
- ? string.slice(33)
433
- : escape(string, part.tagName === 'script' || part.tagName === 'style' ? part.tagName : 'text'),
434
- );
435
- } else if (isTemplateResult(value) || isBuffer(value)) {
436
- return value;
437
- } else if (isSyncIterator(value)) {
438
- if (!isArray(value)) {
439
- value = Array.from(value);
440
- }
441
- // @ts-ignore: already converted to Array
442
- return value.reduce((values, value) => {
443
- value = resolveNodeValue(value, part);
444
- // Flatten
445
- if (isArray(value)) {
446
- return values.concat(value);
447
- }
448
- values.push(value);
449
- return values;
450
- }, []);
451
- } else {
452
- throw Error(`unknown NodePart value: ${value}`);
453
- }
454
- }
455
-
456
- /**
457
- * Resolve values of async "iterator"
458
- *
459
- * @param { AsyncIterable<unknown> } iterator
460
- * @param { NodePart } part
461
- * @returns { AsyncGenerator }
462
- */
463
- async function* resolveAsyncIteratorValue(iterator, part) {
464
- for await (const value of iterator) {
465
- yield resolveNodeValue(value, part);
466
- }
467
- }
468
-
469
- /**
470
- * Resolve value of "directive"
471
- *
472
- * @param { function } directive
473
- * @param { Part } part
474
- * @returns { unknown }
475
- */
476
- function resolveDirectiveValue(directive, part) {
477
- // Directives are synchronous, so it's safe to read and delete value
478
- directive(part);
479
- const value = part._value;
480
- part._value = undefined;
481
- return value;
482
- }
483
-
484
- /**
485
- * Class representing the default Template processor.
486
- * Exposes factory functions for generating Part instances to use for
487
- * resolving a template's dynamic values.
488
- */
489
- class DefaultTemplateProcessor {
490
- /**
491
- * Create part instance for dynamic attribute values
492
- *
493
- * @param { string } name
494
- * @param { Array<Buffer> } strings
495
- * @param { string } tagName
496
- * @returns { AttributePart }
497
- */
498
- handleAttributeExpressions(name, strings = [], tagName) {
499
- const prefix = name[0];
500
-
501
- if (prefix === '.') {
502
- return new PropertyAttributePart(name.slice(1), strings, tagName);
503
- } else if (prefix === '@') {
504
- return new EventAttributePart(name.slice(1), strings, tagName);
505
- } else if (prefix === '?') {
506
- return new BooleanAttributePart(name.slice(1), strings, tagName);
507
- }
508
-
509
- return new AttributePart(name, strings, tagName);
510
- }
511
-
512
- /**
513
- * Create part instance for dynamic text values
514
- *
515
- * @param { string } tagName
516
- * @returns { NodePart }
517
- */
518
- handleTextExpression(tagName) {
519
- return new NodePart(tagName);
520
- }
521
- }
522
-
523
- /* eslint no-constant-condition:0 */
524
-
525
- /**
526
- * Class for the default TemplateResult processor
527
- * used by Promise/Stream TemplateRenderers.
528
- *
529
- * @implements TemplateResultProcessor
530
- */
531
- class DefaultTemplateResultProcessor {
532
- /**
533
- * Process "stack" and push chunks to "renderer"
534
- *
535
- * @param { TemplateResultRenderer } renderer
536
- * @param { Array<unknown> } stack
537
- * @param { number } [highWaterMark] - byte length to buffer before pushing data
538
- * @param { RenderOptions } [options]
539
- * @returns { () => void }
540
- */
541
- getProcessor(renderer, stack, highWaterMark = 0, options) {
542
- /** @type { Array<Buffer> } */
543
- const buffer = [];
544
- let bufferLength = 0;
545
- let processing = false;
546
-
547
- function flushBuffer() {
548
- if (buffer.length > 0) {
549
- const keepPushing = renderer.push(Buffer.concat(buffer, bufferLength));
550
-
551
- bufferLength = buffer.length = 0;
552
- return keepPushing;
553
- }
554
- }
555
-
556
- return function process() {
557
- if (processing) {
558
- return;
559
- }
560
-
561
- while (true) {
562
- processing = true;
563
- let chunk = stack[0];
564
- let breakLoop = false;
565
- let popStack = true;
566
-
567
- // Done
568
- if (chunk === undefined) {
569
- flushBuffer();
570
- return renderer.push(null);
571
- }
572
-
573
- if (isTemplateResult(chunk)) {
574
- popStack = false;
575
- chunk = getTemplateResultChunk(chunk, stack, options);
576
- }
577
-
578
- // Skip if finished reading TemplateResult (null)
579
- if (chunk !== null) {
580
- if (isBuffer(chunk)) {
581
- buffer.push(chunk);
582
- bufferLength += chunk.length;
583
- // Flush buffered data if over highWaterMark
584
- if (bufferLength > highWaterMark) {
585
- // Break if backpressure triggered
586
- breakLoop = !flushBuffer();
587
- processing = !breakLoop;
588
- }
589
- } else if (isArray(chunk)) {
590
- // First remove existing Array if at top of stack (not added by pending TemplateResult)
591
- if (stack[0] === chunk) {
592
- popStack = false;
593
- stack.shift();
594
- }
595
- stack.unshift(...chunk);
596
- } else {
597
- stack.length = 0;
598
- return renderer.destroy(Error(`unknown chunk type: ${chunk}`));
599
- }
600
- }
601
-
602
- if (popStack) {
603
- stack.shift();
604
- }
605
-
606
- if (breakLoop) {
607
- break;
608
- }
609
- }
610
- };
611
- }
612
- }
613
-
614
- /**
615
- * Retrieve next chunk from "result".
616
- * Adds nested TemplateResults to the stack if necessary.
617
- *
618
- * @param { TemplateResult } result
619
- * @param { Array<unknown> } stack
620
- * @param { RenderOptions } [options]
621
- */
622
- function getTemplateResultChunk(result, stack, options) {
623
- let chunk = result.readChunk(options);
624
-
625
- // Skip empty strings
626
- if (isBuffer(chunk) && chunk.length === 0) {
627
- chunk = result.readChunk(options);
628
- }
629
-
630
- // Finished reading, dispose
631
- if (chunk === null) {
632
- stack.shift();
633
- } else if (isTemplateResult(chunk)) {
634
- // Add to top of stack
635
- stack.unshift(chunk);
636
- chunk = getTemplateResultChunk(chunk, stack, options);
637
- }
638
-
639
- return chunk;
640
- }
641
-
642
- /**
643
- * A factory for rendering a template result to a string resolving Promise
644
- *
645
- * @param { TemplateResult } result
646
- * @param { TemplateResultProcessor } processor
647
- * @param { boolean } [asBuffer]
648
- * @param { RenderOptions } [options]
649
- */
650
- function promiseTemplateRenderer(result, processor, asBuffer = false, options) {
651
- return new Promise((resolve, reject) => {
652
- let stack = [result];
653
- /** @type { Array<Buffer> } */
654
- let buffer = [];
655
- let bufferLength = 0;
656
-
657
- processor.getProcessor(
658
- {
659
- push(chunk) {
660
- if (chunk === null) {
661
- const concatBuffer = Buffer.concat(buffer, bufferLength);
662
- resolve(asBuffer ? concatBuffer : concatBuffer.toString());
663
- } else {
664
- buffer.push(chunk);
665
- bufferLength += chunk.length;
666
- }
667
- return true;
668
- },
669
- destroy(err) {
670
- buffer.length = stack.length = bufferLength = 0;
671
- // @ts-ignore
672
- buffer = undefined;
673
- // @ts-ignore
674
- stack = undefined;
675
- reject(err);
676
- },
677
- },
678
- stack,
679
- 0,
680
- options,
681
- )();
682
- });
683
- }
684
-
685
- /**
686
- * This regex extracts the attribute name preceding an attribute-position
687
- * expression. It does this by matching the syntax allowed for attributes
688
- * against the string literal directly preceding the expression, assuming that
689
- * the expression is in an attribute-value position.
690
- *
691
- * See attributes in the HTML spec:
692
- * https://www.w3.org/TR/html5/syntax.html#elements-attributes
693
- *
694
- * " \x09\x0a\x0c\x0d" are HTML space characters:
695
- * https://www.w3.org/TR/html5/infrastructure.html#space-characters
696
- *
697
- * "\0-\x1F\x7F-\x9F" are Unicode control characters, which includes every
698
- * space character except " ".
699
- *
700
- * So an attribute is:
701
- * * The name: any character except a control character, space character, ('),
702
- * ("), ">", "=", or "/"
703
- * * Followed by zero or more space characters
704
- * * Followed by "="
705
- * * Followed by zero or more space characters
706
- * * Followed by:
707
- * * Any character except space, ('), ("), "<", ">", "=", (`), or
708
- * * (") then any non-("), or
709
- * * (') then any non-(')
710
- */
711
- const lastAttributeNameRegex =
712
- // eslint-disable-next-line no-control-regex
713
- /([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
714
-
715
- const EMPTY_STRING_BUFFER$1 = Buffer.from('');
716
- const RE_QUOTE = /"[^"]*|'[^']*$/;
717
- /* eslint no-control-regex: 0 */
718
- const RE_TAG_NAME = /[a-zA-Z0-9._-]/;
719
- const TAG_OPEN = 1;
720
- const TAG_CLOSED = 0;
721
- const TAG_NONE = -1;
722
-
723
- /**
724
- * A cacheable Template that stores the "strings" and "parts" associated with a
725
- * tagged template literal invoked with "html`...`".
726
- */
727
- class Template {
728
- /**
729
- * Create Template instance
730
- *
731
- * @param { TemplateStringsArray } strings
732
- * @param { TemplateProcessor } processor
733
- */
734
- constructor(strings, processor) {
735
- this.strings = [];
736
- this.parts = [];
737
- this._prepare(strings, processor);
738
- }
739
-
740
- /**
741
- * Prepare the template's static strings,
742
- * and create Part instances for the dynamic values,
743
- * based on lit-html syntax.
744
- *
745
- * @param { TemplateStringsArray } strings
746
- * @param { TemplateProcessor } processor
747
- */
748
- _prepare(strings, processor) {
749
- const endIndex = strings.length - 1;
750
- let attributeMode = false;
751
- let nextString = strings[0];
752
- let tagName = '';
753
-
754
- for (let i = 0; i < endIndex; i++) {
755
- let string = nextString;
756
- nextString = strings[i + 1];
757
- const [tagState, tagStateIndex] = getTagState(string);
758
- let skip = 0;
759
- let part;
760
-
761
- // Open/close tag found at end of string
762
- if (tagState !== TAG_NONE) {
763
- attributeMode = tagState !== TAG_CLOSED;
764
- // Find tag name if open, or if closed and no existing tag name
765
- if (tagState === TAG_OPEN || tagName === '') {
766
- tagName = getTagName(string, tagState, tagStateIndex);
767
- }
768
- }
769
-
770
- if (attributeMode) {
771
- const matchName = lastAttributeNameRegex.exec(string);
772
-
773
- if (matchName) {
774
- let [, prefix, name, suffix] = matchName;
775
-
776
- // Since attributes are conditional, remove "name" and "suffix" from static string
777
- string = string.slice(0, matchName.index + prefix.length);
778
-
779
- const matchQuote = RE_QUOTE.exec(suffix);
780
-
781
- // If attribute is quoted, handle potential multiple values
782
- if (matchQuote) {
783
- const quoteCharacter = matchQuote[0].charAt(0);
784
- // Store any text between quote character and value
785
- const attributeStrings = [Buffer.from(suffix.slice(matchQuote.index + 1))];
786
- let open = true;
787
- skip = 0;
788
- let attributeString;
789
-
790
- // Scan ahead and gather all strings for this attribute
791
- while (open) {
792
- attributeString = strings[i + skip + 1];
793
- const closingQuoteIndex = attributeString.indexOf(quoteCharacter);
794
-
795
- if (closingQuoteIndex === -1) {
796
- attributeStrings.push(Buffer.from(attributeString));
797
- skip++;
798
- } else {
799
- attributeStrings.push(Buffer.from(attributeString.slice(0, closingQuoteIndex)));
800
- nextString = attributeString.slice(closingQuoteIndex + 1);
801
- i += skip;
802
- open = false;
803
- }
804
- }
805
-
806
- part = processor.handleAttributeExpressions(name, attributeStrings, tagName);
807
- } else {
808
- part = processor.handleAttributeExpressions(name, [EMPTY_STRING_BUFFER$1, EMPTY_STRING_BUFFER$1], tagName);
809
- }
810
- }
811
- } else {
812
- part = processor.handleTextExpression(tagName);
813
- }
814
-
815
- this.strings.push(Buffer.from(string));
816
- // @ts-ignore: part will never be undefined here
817
- this.parts.push(part);
818
- // Add placehholders for strings/parts that wil be skipped due to multple values in a single AttributePart
819
- if (skip > 0) {
820
- this.strings.push(null);
821
- this.parts.push(null);
822
- skip = 0;
823
- }
824
- }
825
-
826
- this.strings.push(Buffer.from(nextString));
827
- }
828
- }
829
-
830
- /**
831
- * Determine if 'string' terminates with an opened or closed tag.
832
- *
833
- * Iterating through all characters has at worst a time complexity of O(n),
834
- * and is better than the alternative (using "indexOf/lastIndexOf") which is potentially O(2n).
835
- *
836
- * @param { string } string
837
- * @returns { Array<number> } - returns tuple "[-1, -1]" if no tag, "[0, i]" if closed tag, or "[1, i]" if open tag
838
- */
839
- function getTagState(string) {
840
- for (let i = string.length - 1; i >= 0; i--) {
841
- const char = string[i];
842
-
843
- if (char === '>') {
844
- return [TAG_CLOSED, i];
845
- } else if (char === '<') {
846
- return [TAG_OPEN, i];
847
- }
848
- }
849
-
850
- return [TAG_NONE, -1];
851
- }
852
-
853
- /**
854
- * Retrieve tag name from "string" starting at "tagStateIndex" position
855
- * Walks forward or backward based on "tagState" open or closed
856
- *
857
- * @param { string } string
858
- * @param { number } tagState
859
- * @param { number } tagStateIndex
860
- * @returns { string }
861
- */
862
- function getTagName(string, tagState, tagStateIndex) {
863
- let tagName = '';
864
-
865
- if (tagState === TAG_CLOSED) {
866
- // Walk backwards until open tag
867
- for (let i = tagStateIndex - 1; i >= 0; i--) {
868
- const char = string[i];
869
-
870
- if (char === '<') {
871
- return getTagName(string, TAG_OPEN, i);
872
- }
873
- }
874
- } else {
875
- for (let i = tagStateIndex + 1; i < string.length; i++) {
876
- const char = string[i];
877
-
878
- if (!RE_TAG_NAME.test(char)) {
879
- break;
880
- }
881
-
882
- tagName += char;
883
- }
884
- }
885
-
886
- return tagName;
887
- }
888
-
889
- const EMPTY_STRING_BUFFER$2 = Buffer.from('');
890
-
891
- let id = 0;
892
-
893
- /**
894
- * A class for consuming the combined static and dynamic parts of a lit-html Template.
895
- * TemplateResults
896
- */
897
- class TemplateResult {
898
- /**
899
- * Constructor
900
- *
901
- * @param { Template } template
902
- * @param { Array<unknown> } values
903
- */
904
- constructor(template, values) {
905
- this.template = template;
906
- this.values = values;
907
- this.id = id++;
908
- this.index = 0;
909
- }
910
-
911
- /**
912
- * Consume template result content.
913
- *
914
- * @param { RenderOptions } [options]
915
- * @returns { unknown }
916
- */
917
- read(options) {
918
- let buffer = EMPTY_STRING_BUFFER$2;
919
- let chunk;
920
- /** @type { Array<Buffer> | undefined } */
921
- let chunks;
922
-
923
- while ((chunk = this.readChunk(options)) !== null) {
924
- if (isBuffer(chunk)) {
925
- buffer = Buffer.concat([buffer, chunk], buffer.length + chunk.length);
926
- } else {
927
- if (chunks === undefined) {
928
- chunks = [];
929
- }
930
- buffer = reduce(buffer, chunks, chunk) || EMPTY_STRING_BUFFER$2;
931
- }
932
- }
933
-
934
- if (chunks !== undefined) {
935
- chunks.push(buffer);
936
- return chunks.length > 1 ? chunks : chunks[0];
937
- }
938
-
939
- return buffer;
940
- }
941
-
942
- /**
943
- * Consume template result content one chunk at a time.
944
- * @param { RenderOptions } [options]
945
- * @returns { unknown }
946
- */
947
- readChunk(options) {
948
- const isString = this.index % 2 === 0;
949
- const index = (this.index / 2) | 0;
950
-
951
- // Finished
952
- if (!isString && index >= this.template.strings.length - 1) {
953
- // Reset
954
- this.index = 0;
955
- return null;
956
- }
957
-
958
- this.index++;
959
-
960
- if (isString) {
961
- return this.template.strings[index];
962
- }
963
-
964
- const part = this.template.parts[index];
965
- let value;
966
-
967
- if (isAttributePart(part)) {
968
- // AttributeParts can have multiple values, so slice based on length
969
- // (strings in-between values are already handled the instance)
970
- if (part.length > 1) {
971
- value = part.getValue(this.values.slice(index, index + part.length), options);
972
- this.index += part.length;
973
- } else {
974
- value = part.getValue([this.values[index]], options);
975
- }
976
- } else {
977
- value = part && part.getValue(this.values[index], options);
978
- }
979
-
980
- return value;
981
- }
982
- }
983
-
984
- /**
985
- * Commit "chunk" to string "buffer".
986
- * Returns new "buffer" value.
987
- *
988
- * @param { Buffer } buffer
989
- * @param { Array<unknown> } chunks
990
- * @param { unknown } chunk
991
- * @returns { Buffer | undefined }
992
- */
993
- function reduce(buffer, chunks, chunk) {
994
- if (isBuffer(chunk)) {
995
- return Buffer.concat([buffer, chunk], buffer.length + chunk.length);
996
- } else if (isTemplateResult(chunk)) {
997
- chunks.push(buffer, chunk);
998
- return EMPTY_STRING_BUFFER$2;
999
- } else if (isArray(chunk)) {
1000
- return chunk.reduce((buffer, chunk) => reduce(buffer, chunks, chunk), buffer);
1001
- }
1002
- }
1003
-
1004
- /**
1005
- * Default templateResult factory
1006
- *
1007
- * @param { unknown } value
1008
- * @returns { TemplateResult }
1009
- */
1010
- // prettier-ignore
1011
- const DEFAULT_TEMPLATE_FN = (value) => html`${value}`;
1012
-
1013
- const defaultTemplateProcessor = new DefaultTemplateProcessor();
1014
- const defaultTemplateResultProcessor = new DefaultTemplateResultProcessor();
1015
- const templateCache = new Map();
1016
-
1017
- /**
1018
- * Interprets a template literal as an HTML template that can be
1019
- * rendered as a Readable stream or String
1020
- *
1021
- * @param { TemplateStringsArray } strings
1022
- * @param { ...unknown } values
1023
- * @returns { TemplateResult }
1024
- */
1025
- function html(strings, ...values) {
1026
- let template = templateCache.get(strings);
1027
-
1028
- if (template === undefined) {
1029
- template = new Template(strings, defaultTemplateProcessor);
1030
- templateCache.set(strings, template);
1031
- }
1032
-
1033
- return new TemplateResult(template, values);
1034
- }
1035
-
1036
- /**
1037
- * Render a template result to a string resolving Promise.
1038
- *
1039
- * @param { unknown } result - a template result returned from call to "html`...`"
1040
- * @param { RenderOptions } [options]
1041
- * @returns { Promise<string> }
1042
- */
1043
- function renderToString(result, options) {
1044
- return promiseTemplateRenderer(getRenderResult(result), defaultTemplateResultProcessor, false, options);
1045
- }
1046
-
1047
- /**
1048
- * Retrieve TemplateResult for render
1049
- *
1050
- * @param { unknown} result
1051
- * @returns { TemplateResult }
1052
- */
1053
- function getRenderResult(result) {
1054
- // @ts-ignore
1055
- return !isTemplateResult(result) ? DEFAULT_TEMPLATE_FN(result) : result;
1056
- }
1057
-
1058
- export {
1059
- AttributePart,
1060
- BooleanAttributePart,
1061
- DefaultTemplateProcessor,
1062
- DefaultTemplateResultProcessor,
1063
- EventAttributePart,
1064
- NodePart,
1065
- Part,
1066
- PropertyAttributePart,
1067
- Template,
1068
- TemplateResult,
1069
- defaultTemplateProcessor,
1070
- defaultTemplateResultProcessor,
1071
- directive,
1072
- html,
1073
- isAttributePart,
1074
- isDirective,
1075
- isNodePart,
1076
- isTemplateResult,
1077
- nothing,
1078
- renderToBuffer,
1079
- renderToStream,
1080
- renderToString,
1081
- html as svg,
1082
- templateCache,
1083
- unsafePrefixString,
1084
- };
test/index.test.js CHANGED
@@ -13,6 +13,8 @@ import {
13
13
  useConfig,
14
14
  useLocation,
15
15
  useState,
16
+ unsafeHTML,
17
+ classMap,
16
18
  } from '../src/index.js';
17
19
 
18
20
  const logMock = jest.fn();
@@ -149,20 +151,60 @@ test('useLocation', async () => {
149
151
  });
150
152
 
151
153
  test('render', async () => {
154
+ const age = 1;
152
155
  const data = { name: '123', address: { street: '1' } };
156
+ const highlight = 'high';
153
157
  const template = html`
154
158
  <div>
155
- <app-counter name="123" details="${data}"></app-counter>
159
+ <app-counter name="123" class="abc ${highlight}" age=${age} details1=${data}></app-counter>
156
160
  </div>
157
161
  `;
158
162
  const res = await render(template);
159
163
  expect(res).toEqual(`
160
164
  <div>
161
- <app-counter name=\"123\" details=\"{'name':'123','address':{'street':'1'}}\"></app-counter>
165
+ <app-counter name="123" class="abc high" age="1" details1="{'name':'123','address':{'street':'1'}}"></app-counter>
162
166
  </div>
163
167
  `);
164
168
  });
165
169
 
170
+ test('render attributes within quotes', async () => {
171
+ const age = 1;
172
+ const data = { name: '123', address: { street: '1' } };
173
+ const classes = 'high';
174
+ const template = html`
175
+ <div>
176
+ <app-counter name="123" class=${classes} age="${age}" details1="${data}"></app-counter>
177
+ </div>
178
+ `;
179
+ const res = await render(template);
180
+ expect(res).toEqual(`
181
+ <div>
182
+ <app-counter name="123" class="high" age="1" details1="{'name':'123','address':{'street':'1'}}"></app-counter>
183
+ </div>
184
+ `);
185
+ });
186
+
187
+ test('render unsafeHTML', async () => {
188
+ const textContent = `<div><p class="123">this is unsafe</p></div>`;
189
+ const template = html` <div>${unsafeHTML(textContent)}</div> `;
190
+ const res = await render(template);
191
+ expect(res).toEqual(` <div><div><p class="123">this is unsafe</p></div></div> `);
192
+ });
193
+
194
+ test('render classMap show', async () => {
195
+ const hide = false;
196
+ const template = html` <div class="abc ${classMap({ show: !hide })}"></div> `;
197
+ const res = await render(template);
198
+ expect(res).toEqual(` <div class="abc show"></div> `);
199
+ });
200
+
201
+ test('render classMap hide', async () => {
202
+ const hide = true;
203
+ const template = html` <div class="abc ${classMap({ show: !hide })}"></div> `;
204
+ const res = await render(template);
205
+ expect(res).toEqual(` <div class="abc "></div> `);
206
+ });
207
+
166
208
  test('render single template', async () => {
167
209
  const template = html` <div>${html`NoCountry ${false}`}</div> `;
168
210
  const res = await render(template);