atoms-element v5.0.0

#js

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

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


4c11234 — Peter John 2026-08-07T11:52:39+05:30
improve rendering

Generated files not shown: package-lock.json

__snapshots__/index.test.js.snap DELETED
@@ -1,352 +0,0 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
- exports[`createElement styles are scoped and injected via SSR 1`] = `
4
- "@scope (styled-element) {
5
-
6
- :scope {
7
- display: flex;
8
- }
9
- .count {
10
- color: magenta;
11
- }
12
-
13
- }
14
- "
15
- `;
16
-
17
- exports[`createElement with attrs and hooks 1`] = `
18
- "
19
- <div>
20
- <div>
21
- <span>perPage: 5</span> </div> </div> <span>Count: 3</span> <button>Set</button> "
22
- `;
23
-
24
- exports[`createElement without attrs 1`] = `" <div></div>"`;
25
-
26
- exports[`createPage 1`] = `
27
- "
28
- <!DOCTYPE html>
29
- <html lang=\\"en\\">
30
- <head>
31
- <meta charset=\\"utf-8\\" />
32
- <meta http-equiv=\\"x-ua-compatible\\" content=\\"ie=edge\\" />
33
- <meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=utf-8\\">
34
- <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5.0, shrink-to-fit=no\\">
35
- <link rel=\\"sitemap\\" type=\\"application/xml\\" href=\\"/sitemap.xml\\" />
36
- <link rel=\\"icon\\" type=\\"image/png\\" href=\\"/assets/icon.png\\" />
37
-
38
- <title>123</title> <meta name=\\"title\\" content=\\"123\\"/> <meta name=\\"description\\" content=\\"123\\"/>
39
- <style id=\\"global\\">
40
- *, ::before, ::after {
41
-
42
- box-sizing: border-box;
43
- border-width: 0;
44
- border-style: solid;
45
- border-color: #e5e7eb;
46
-
47
- }
48
- hr {
49
-
50
- height: 0;
51
- color: inherit;
52
- border-top-width: 1px;
53
-
54
- }
55
- abbr[title] {
56
-
57
- -webkit-text-decoration: underline dotted;
58
- text-decoration: underline dotted;
59
-
60
- }
61
- b, strong {
62
-
63
- font-weight: bolder;
64
-
65
- }
66
- code, kbd, samp, pre {
67
-
68
- font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
69
- font-size: 1em;
70
-
71
- }
72
- small {
73
-
74
- font-size: 80%;
75
-
76
- }
77
- sub, sup {
78
-
79
- font-size: 75%;
80
- line-height: 0;
81
- position: relative;
82
- vertical-align: baseline;
83
-
84
- }
85
- sub {
86
-
87
- bottom: -0.25em;
88
-
89
- }
90
- sup {
91
-
92
- top: -0.5em;
93
-
94
- }
95
- table {
96
-
97
- text-indent: 0;
98
- border-color: inherit;
99
- border-collapse: collapse;
100
-
101
- }
102
- button, input, optgroup, select, textarea {
103
-
104
- font-size: 100%;
105
- margin: 0;
106
- padding: 0;
107
- line-height: inherit;
108
- color: inherit;
109
-
110
- }
111
- button, select {
112
-
113
-
114
- }
115
- button, [type='button'], [type='reset'], [type='submit'] {
116
-
117
-
118
- }
119
- ::-moz-focus-inner {
120
-
121
- border-style: none;
122
- padding: 0;
123
-
124
- }
125
- :-moz-focusring {
126
-
127
- outline: 1px dotted ButtonText;
128
-
129
- }
130
- :-moz-ui-invalid {
131
-
132
- box-shadow: none;
133
-
134
- }
135
- legend {
136
-
137
- padding: 0;
138
-
139
- }
140
- progress {
141
-
142
- vertical-align: baseline;
143
-
144
- }
145
- ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
146
-
147
- height: auto;
148
-
149
- }
150
- [type='search'] {
151
-
152
- -webkit-appearance: textfield;
153
- outline-offset: -2px;
154
-
155
- }
156
- ::-webkit-search-decoration {
157
-
158
- -webkit-appearance: none;
159
-
160
- }
161
- ::-webkit-file-upload-button {
162
-
163
- -webkit-appearance: button;
164
- font: inherit;
165
-
166
- }
167
- summary {
168
-
169
- display: list-item;
170
-
171
- }
172
- blockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre {
173
-
174
- margin: 0;
175
-
176
- }
177
- button {
178
-
179
- background-image: none;
180
-
181
- outline: 1px dotted, 5px auto -webkit-focus-ring-color;
182
-
183
- }
184
- fieldset {
185
-
186
- margin: 0;
187
- padding: 0;
188
-
189
- }
190
- ol, ul {
191
-
192
- list-style: none;
193
- margin: 0;
194
- padding: 0;
195
-
196
- }
197
- img {
198
-
199
- border-style: solid;
200
-
201
- }
202
- textarea {
203
-
204
- resize: vertical;
205
-
206
- }
207
- input::-moz-placeholder, textarea::-moz-placeholder {
208
-
209
- opacity: 1;
210
- color: #9ca3af;
211
-
212
- }
213
- input:-ms-input-placeholder, textarea:-ms-input-placeholder {
214
-
215
- opacity: 1;
216
- color: #9ca3af;
217
-
218
- }
219
- input::placeholder, textarea::placeholder {
220
-
221
- opacity: 1;
222
- color: #9ca3af;
223
-
224
- }
225
- button, [role='button'] {
226
-
227
- cursor: pointer;
228
-
229
- }
230
- h1, h2, h3, h4, h5, h6 {
231
-
232
- font-size: inherit;
233
- font-weight: inherit;
234
-
235
- }
236
- a {
237
-
238
- color: inherit;
239
- text-decoration: inherit;
240
-
241
- }
242
- pre, code, kbd, samp {
243
-
244
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
245
-
246
- }
247
- img, svg, video, canvas, audio, iframe, embed, object {
248
-
249
- display: block;
250
- vertical-align: middle;
251
-
252
- }
253
- img, video {
254
-
255
- max-width: 100%;
256
- height: auto;
257
-
258
- }
259
- html {
260
-
261
- -moz-tab-size: 4;
262
- -o-tab-size: 4;
263
- tab-size: 4;
264
- line-height: 1.5;
265
- -webkit-text-size-adjust: 100%;
266
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
267
- width: 100%;
268
- height: 100%;
269
-
270
- }
271
- body {
272
-
273
- margin: 0px;
274
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
275
- line-height: 1.4;
276
- background-color: white;
277
- width: 100%;
278
- height: 100%;
279
- display: flex;
280
- flex-direction: column;
281
- flex: 1 1 0%;
282
- min-width: 320px;
283
- min-height: 100vh;
284
- font-weight: 400;
285
- color: rgba(44, 62, 80, 1);
286
- direction: ltr;
287
- font-synthesis: none;
288
- text-rendering: optimizeLegibility;
289
-
290
- }
291
-
292
-
293
- </style>
294
- <script type=\\"module\\"><script>
295
- </head>
296
- <body>
297
-
298
- <div>
299
- <app-header></app-header> <main class=\\"flex flex-1 flex-col mt-20 items-center\\">
300
- <h1 class=\\"text-5xl\\">123</h1> </main> </div>
301
- <script>
302
- window.props = {\\"config\\":{\\"title\\":\\"123\\"}};
303
- </script>
304
- <script type=\\"module\\"><script>
305
- </body>
306
- </html>
307
- "
308
- `;
309
-
310
- exports[`css tagged template 1`] = `
311
- "
312
- :scope {
313
- display: flex;
314
- }
315
- button {
316
- color: magenta;
317
- }
318
- "
319
- `;
320
-
321
- exports[`render attribute keys 1`] = `
322
- "
323
- <div>
324
- <app-counter name=\\"123\\" perPage=\\"1\\"></app-counter> </div>"
325
- `;
326
-
327
- exports[`render attributes within quotes 1`] = `
328
- "
329
- <div>
330
- <app-counter name=\\"123\\" class=\\"high\\" age=\\"1\\" details1=\\"{'name':'123','address':{'street':'1'}}\\" items=\\"[1,2,3]\\"></app-counter> </div>"
331
- `;
332
-
333
- exports[`render multi template 1`] = `
334
- "
335
- <div>
336
-
337
- <app-item meta=\\"{'index':1}\\">
338
- <button>+</button> </app-item> <app-item meta=\\"{'index':2}\\">
339
- <button>+</button> </app-item> </div>"
340
- `;
341
-
342
- exports[`render single template 1`] = `" <div>NoCountry false</div>"`;
343
-
344
- exports[`render unsafeHTML 1`] = `" <div><div><p class=\\"123\\">this is unsafe</p></div></div>"`;
345
-
346
- exports[`renderHtml 1`] = `
347
- "
348
- <div>
349
- <app-counter name=\\"123\\" class=\\"abc high\\" age=\\"1\\" details1=\\"{'name':'123','address':{'street':'1'}}\\" items=\\"[1,2,3]\\"></app-counter> </div>"
350
- `;
351
-
352
- exports[`renderHtml escapes text and attribute values 1`] = `"<div class=\\"a&quot; onmouseover=&quot;alert(1)\\">&lt;script&gt;alert(1)&lt;/script&gt;</div>"`;
examples/e2e.spec.js ADDED
@@ -0,0 +1,68 @@
1
+ // Real-browser test for the example app, run with: npm run test:e2e
2
+ // Requires Chromium once: npx playwright install chromium
3
+ // Named .spec.js (not .test.js) so it's never picked up by a bare `node --test`.
4
+ import { test, before, after } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { spawn } from 'node:child_process';
7
+ import { chromium } from 'playwright';
8
+
9
+ const PORT = 3987;
10
+ const BASE_URL = `http://localhost:${PORT}/`;
11
+
12
+ let serverProcess;
13
+ let browser;
14
+ let page;
15
+ const pageErrors = [];
16
+
17
+ before(async () => {
18
+ serverProcess = spawn(process.execPath, ['examples/server.js', String(PORT)], { stdio: 'ignore' });
19
+
20
+ const deadline = Date.now() + 15000;
21
+ for (;;) {
22
+ try {
23
+ if ((await fetch(BASE_URL)).ok) break;
24
+ } catch {
25
+ // server not up yet
26
+ }
27
+ if (Date.now() > deadline) throw new Error('example server did not start in time');
28
+ await new Promise((resolve) => setTimeout(resolve, 200));
29
+ }
30
+
31
+ browser = await chromium.launch();
32
+ page = await browser.newPage();
33
+ page.on('pageerror', (err) => pageErrors.push(err.message));
34
+ await page.goto(BASE_URL, { waitUntil: 'networkidle' });
35
+ await page.waitForSelector('app-counter');
36
+ });
37
+
38
+ after(async () => {
39
+ await browser?.close();
40
+ serverProcess?.kill();
41
+ });
42
+
43
+ test('renders each counter exactly once, with no leftover duplicate SSR markup', async () => {
44
+ const counters = page.locator('app-counter');
45
+ assert.strictEqual(await counters.count(), 2);
46
+ for (let i = 0; i < 2; i++) {
47
+ assert.strictEqual(await counters.nth(i).locator('h1').count(), 1, `counter ${i} should render exactly one <h1>`);
48
+ }
49
+ });
50
+
51
+ test('clicking +/- updates the counter and the shared total reducer', async () => {
52
+ const first = page.locator('app-counter').nth(0);
53
+ const second = page.locator('app-counter').nth(1);
54
+
55
+ await first.getByRole('button', { name: '+' }).click();
56
+ await first.getByRole('button', { name: '+' }).click();
57
+ await first.getByRole('button', { name: '+' }).click();
58
+ await second.getByRole('button', { name: '+' }).click();
59
+ await first.getByRole('button', { name: '-' }).click();
60
+
61
+ assert.strictEqual((await first.locator('h1').textContent()).trim(), '2');
62
+ assert.strictEqual((await second.locator('h1').textContent()).trim(), '1');
63
+ assert.strictEqual((await page.locator('app-total h1').textContent()).trim(), 'Total of 2 Counters: 3');
64
+ });
65
+
66
+ test('no console/page errors were thrown', () => {
67
+ assert.deepStrictEqual(pageErrors, []);
68
+ });
examples/server.js CHANGED
@@ -1,22 +1,37 @@
1
1
  import http from 'http';
2
2
  import fs from 'fs';
3
+ import path from 'path';
3
4
  import { fileURLToPath } from 'url';
4
5
  import { dirname } from 'path';
5
6
  import renderPage from './pages/index.js';
6
7
 
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = dirname(__filename);
10
+ const rootDir = path.join(__dirname, '..');
9
11
  const port = process.argv[2] || 3000;
10
12
  const elements = ['app-counter.js', 'app-total.js'];
11
13
  const srcMap = {
12
- '/index.js': `${__dirname}/../index.js`,
14
+ '/index.js': `${rootDir}/index.js`,
13
- '/lit-html.js': `${__dirname}/../lit-html.js`,
14
15
  '/store.js': `${__dirname}/store.js`,
15
16
  };
16
17
  elements.forEach((el) => {
17
18
  srcMap['/elements/' + el] = `${__dirname}/elements/${el}`;
18
19
  });
19
20
 
21
+ // lit-html and mutative are real npm dependencies (not vendored), so the
22
+ // browser needs an import map for the bare specifiers index.js uses, plus
23
+ // the packages' files served as-is — their internal imports are all
24
+ // relative, so no other wiring is needed.
25
+ const importMap = {
26
+ imports: {
27
+ 'lit-html': '/node_modules/lit-html/lit-html.js',
28
+ 'lit-html/directive-helpers.js': '/node_modules/lit-html/directive-helpers.js',
29
+ 'lit-html/directives/unsafe-html.js': '/node_modules/lit-html/directives/unsafe-html.js',
30
+ mutative: '/node_modules/mutative/dist/mutative.esm.mjs',
31
+ },
32
+ };
33
+ const nodeModulesDir = path.join(rootDir, 'node_modules');
34
+
20
35
  http
21
36
  .createServer((req, res) => {
22
37
  if (req.url.includes('/api/posts')) {
@@ -40,7 +55,7 @@ http
40
55
  props: {
41
56
  config: { lang: 'en', title: 'Counter App' },
42
57
  },
43
- headScript: '',
58
+ headScript: `<script type="importmap">${JSON.stringify(importMap)}</script>`,
44
59
  bodyScript: `
45
60
  <script type="module">
46
61
  ${elements.map((el) => `import './elements/${el}';`).join('\n')}
@@ -50,12 +65,27 @@ http
50
65
  res.end(html);
51
66
  return;
52
67
  }
68
+ if (req.url.startsWith('/node_modules/')) {
69
+ const relative = req.url.slice('/node_modules/'.length);
70
+ const filename = path.normalize(path.join(nodeModulesDir, relative));
71
+ if (filename.startsWith(nodeModulesDir) && fs.existsSync(filename)) {
72
+ res.setHeader('Content-type', 'application/javascript');
73
+ res.end(fs.readFileSync(filename));
74
+ } else {
75
+ res.statusCode = 404;
76
+ res.end();
77
+ }
78
+ return;
79
+ }
53
80
  const filename = srcMap[req.url];
54
81
  if (filename) {
55
82
  const data = fs.readFileSync(filename);
56
83
  res.setHeader('Content-type', 'application/javascript');
57
84
  res.end(data);
85
+ return;
58
86
  }
87
+ res.statusCode = 404;
88
+ res.end();
59
89
  })
60
90
  .listen(parseInt(port));
61
91
 
index.js CHANGED
@@ -1,4 +1,6 @@
1
- import { html, render as litRender, directive, NodePart, isPrimitive } from './lit-html.js';
1
+ import { html, render as litRender } from 'lit-html';
2
+ import { isTemplateResult } from 'lit-html/directive-helpers.js';
3
+ import { unsafeHTML as litUnsafeHTML } from 'lit-html/directives/unsafe-html.js';
2
4
  import { create } from 'mutative';
3
5
 
4
6
  const isBrowser = typeof window !== 'undefined';
@@ -255,14 +257,14 @@ export const renderHtml = isBrowser
255
257
  } else if (value === null || !(type === 'object' || type === 'function' || type === 'undefined')) {
256
258
  const str = type !== 'string' ? String(value) : value;
257
259
  js += wrapAttribute(attrName, suffix, text, attrName ? escapeAttribute(str) : escapeText(str));
258
- } else if (Array.isArray(value) && value.find((item) => item && item.strings && item.type === 'html')) {
260
+ } else if (Array.isArray(value) && value.find((item) => item && isTemplateResult(item))) {
259
261
  js += text;
260
262
  value.forEach((v) => {
261
263
  js += renderHtml(v);
262
264
  });
263
265
  } else if (type === 'object') {
264
266
  // TemplateResult
265
- if (value.strings && value.type === 'html') {
267
+ if (isTemplateResult(value)) {
266
268
  js += text;
267
269
  js += renderHtml(value);
268
270
  } else {
@@ -434,23 +436,7 @@ const pageStyles = {
434
436
  },
435
437
  };
436
438
 
437
- const previousValues = new WeakMap();
438
- export const unsafeHTML = isBrowser
439
- ? directive((value) => (part) => {
440
- if (!(part instanceof NodePart)) {
441
- throw new Error('unsafeHTML can only be used in text bindings');
442
- }
443
- const previousValue = previousValues.get(part);
444
- if (previousValue !== undefined && isPrimitive(value) && value === previousValue.value && part.value === previousValue.fragment) {
445
- return;
446
- }
447
- const template = document.createElement('template');
448
- template.innerHTML = value; // innerHTML casts to string internally
449
- const fragment = document.importNode(template.content, true);
450
- part.setValue(fragment);
451
- previousValues.set(part, { value, fragment });
452
- })
453
- : (value) => ({ [UNSAFE_HTML]: true, value });
439
+ export const unsafeHTML = isBrowser ? litUnsafeHTML : (value) => ({ [UNSAFE_HTML]: true, value });
454
440
 
455
441
  const fifo = (q) => q.shift();
456
442
  const filo = (q) => q.pop();
@@ -557,6 +543,7 @@ export const createElement = (meta, renderFn) => {
557
543
  super();
558
544
  this._dirty = false;
559
545
  this._connected = false;
546
+ this._hydrated = false;
560
547
  this.attrs = ssrAttrs || {};
561
548
  this.state = {};
562
549
  this.hooks = {
@@ -655,6 +642,13 @@ export const createElement = (meta, renderFn) => {
655
642
  injectedStyleTags.add(name);
656
643
  document.getElementById('global').textContent += styles;
657
644
  }
645
+ if (!this._hydrated) {
646
+ // lit-html's render() appends into the container rather than replacing its
647
+ // content, so any server-rendered light-DOM markup must be discarded before
648
+ // the first client-side render or it's left behind as an inert duplicate.
649
+ this._hydrated = true;
650
+ this.textContent = '';
651
+ }
658
652
  renderHtml(template, this);
659
653
  } else {
660
654
  return renderHtml(template);
index.test.js CHANGED
@@ -1,9 +1,8 @@
1
+ import { test } from 'node:test';
1
- import { expect, test, jest } from '@jest/globals';
2
+ import assert from 'node:assert/strict';
2
3
  import { getElement, createElement, createPage, createReducer, html, renderHtml, unsafeHTML, css, useReducer, useEffect } from './index.js';
3
4
 
4
- global.__DEV = true;
5
-
6
- test('css tagged template', () => {
5
+ test('css tagged template', (t) => {
7
6
  const color = 'magenta';
8
7
  const styles = css`
9
8
  :scope {
@@ -13,10 +12,10 @@ test('css tagged template', () => {
13
12
  color: ${color};
14
13
  }
15
14
  `;
16
- expect(styles).toMatchSnapshot();
15
+ t.assert.snapshot(styles);
17
16
  });
18
17
 
19
- test('createElement styles are scoped and injected via SSR', () => {
18
+ test('createElement styles are scoped and injected via SSR', (t) => {
20
19
  const Styled = () => {
21
20
  return html` <div class="count"></div> `;
22
21
  };
@@ -30,10 +29,10 @@ test('createElement styles are scoped and injected via SSR', () => {
30
29
  `;
31
30
  createElement({ url: '/styled-element.js' }, Styled);
32
31
  const Clazz = getElement('styled-element');
33
- expect(Clazz.styles).toMatchSnapshot();
32
+ t.assert.snapshot(Clazz.styles);
34
33
  });
35
34
 
36
- test('renderHtml', () => {
35
+ test('renderHtml', (t) => {
37
36
  const age = 1;
38
37
  const data = { name: '123', address: { street: '1' } };
39
38
  const items = [1, 2, 3];
@@ -44,20 +43,20 @@ test('renderHtml', () => {
44
43
  </div>
45
44
  `;
46
45
  const res = renderHtml(template);
47
- expect(res).toMatchSnapshot();
46
+ t.assert.snapshot(res);
48
47
  });
49
48
 
50
- test('render attribute keys', () => {
49
+ test('render attribute keys', (t) => {
51
50
  const template = html`
52
51
  <div>
53
52
  <app-counter name="123" perPage="1"></app-counter>
54
53
  </div>
55
54
  `;
56
55
  const res = renderHtml(template);
57
- expect(res).toMatchSnapshot();
56
+ t.assert.snapshot(res);
58
57
  });
59
58
 
60
- test('render attributes within quotes', () => {
59
+ test('render attributes within quotes', (t) => {
61
60
  const age = 1;
62
61
  const data = { name: '123', address: { street: '1' } };
63
62
  const items = [1, 2, 3];
@@ -68,33 +67,33 @@ test('render attributes within quotes', () => {
68
67
  </div>
69
68
  `;
70
69
  const res = renderHtml(template);
71
- expect(res).toMatchSnapshot();
70
+ t.assert.snapshot(res);
72
71
  });
73
72
 
74
- test('render unsafeHTML', () => {
73
+ test('render unsafeHTML', (t) => {
75
74
  const textContent = `<div><p class="123">this is unsafe</p></div>`;
76
75
  const template = html` <div>${unsafeHTML(textContent)}</div> `;
77
76
  const res = renderHtml(template);
78
- expect(res).toMatchSnapshot();
77
+ t.assert.snapshot(res);
79
78
  });
80
79
 
81
- test('renderHtml escapes text and attribute values', () => {
80
+ test('renderHtml escapes text and attribute values', (t) => {
82
81
  const evil = '<script>alert(1)</script>';
83
82
  const quote = 'a" onmouseover="alert(1)';
84
83
  const template = html`<div class="${quote}">${evil}</div>`;
85
84
  const res = renderHtml(template);
86
- expect(res).not.toContain('<script>');
85
+ assert.ok(!res.includes('<script>'));
87
- expect(res).not.toContain('onmouseover="alert(1)"');
86
+ assert.ok(!res.includes('onmouseover="alert(1)"'));
88
- expect(res).toMatchSnapshot();
87
+ t.assert.snapshot(res);
89
88
  });
90
89
 
91
- test('render single template', () => {
90
+ test('render single template', (t) => {
92
91
  const template = html` <div>${html`NoCountry ${false}`}</div> `;
93
92
  const res = renderHtml(template);
94
- expect(res).toMatchSnapshot();
93
+ t.assert.snapshot(res);
95
94
  });
96
95
 
97
- test('render multi template', () => {
96
+ test('render multi template', (t) => {
98
97
  const template = html`
99
98
  <div>
100
99
  ${[1, 2].map(
@@ -107,10 +106,10 @@ test('render multi template', () => {
107
106
  </div>
108
107
  `;
109
108
  const res = renderHtml(template);
110
- expect(res).toMatchSnapshot();
109
+ t.assert.snapshot(res);
111
110
  });
112
111
 
113
- test('createReducer', () => {
112
+ test('createReducer', (t) => {
114
113
  const countReducer = createReducer({
115
114
  initial: {
116
115
  count: 0,
@@ -125,24 +124,24 @@ test('createReducer', () => {
125
124
  },
126
125
  },
127
126
  });
128
- const mock = jest.fn();
127
+ const mock = t.mock.fn();
129
128
  countReducer.subscribe(mock);
130
129
  const before = countReducer.getValue();
131
130
  countReducer.actions.increment(4);
132
- expect(countReducer.getValue().count).toEqual(4);
131
+ assert.strictEqual(countReducer.getValue().count, 4);
133
- expect(mock).toHaveBeenCalledWith({ count: 4, other: { untouched: true } });
132
+ assert.deepStrictEqual(mock.mock.calls[0].arguments, [{ count: 4, other: { untouched: true } }]);
134
133
  countReducer.actions.decrement(1);
135
- expect(countReducer.getValue().count).toEqual(3);
134
+ assert.strictEqual(countReducer.getValue().count, 3);
136
- expect(mock).toHaveBeenCalledWith({ count: 3, other: { untouched: true } });
135
+ assert.deepStrictEqual(mock.mock.calls[1].arguments, [{ count: 3, other: { untouched: true } }]);
137
136
  countReducer.actions.decrement(2);
138
137
  const after = countReducer.getValue();
139
- expect(after.count).toEqual(1);
138
+ assert.strictEqual(after.count, 1);
140
- expect(mock).toHaveBeenCalledWith({ count: 1, other: { untouched: true } });
139
+ assert.deepStrictEqual(mock.mock.calls[2].arguments, [{ count: 1, other: { untouched: true } }]);
141
- expect(after).not.toBe(before);
140
+ assert.notStrictEqual(after, before);
142
- expect(after.other).toBe(before.other);
141
+ assert.strictEqual(after.other, before.other);
143
142
  });
144
143
 
145
- test('useReducer selector only updates when the selected slice changes', () => {
144
+ test('useReducer selector only updates when the selected slice changes', (t) => {
146
145
  const store = createReducer({
147
146
  initial: { count: 0, other: 0 },
148
147
  reducer: {
@@ -161,18 +160,18 @@ test('useReducer selector only updates when the selected slice changes', () => {
161
160
  const Clazz = getElement('count-only');
162
161
  const instance = new Clazz();
163
162
  instance.render();
164
- const updateSpy = jest.fn();
163
+ const updateSpy = t.mock.fn();
165
164
  instance.update = updateSpy;
166
165
 
167
166
  store.actions.incOther();
168
- expect(updateSpy).not.toHaveBeenCalled();
167
+ assert.strictEqual(updateSpy.mock.callCount(), 0);
169
168
 
170
169
  store.actions.incCount();
171
- expect(updateSpy).toHaveBeenCalledTimes(1);
170
+ assert.strictEqual(updateSpy.mock.callCount(), 1);
172
171
  });
173
172
 
174
- test('useEffect cleanup runs on disconnect', () => {
173
+ test('useEffect cleanup runs on disconnect', (t) => {
175
- const cleanup = jest.fn();
174
+ const cleanup = t.mock.fn();
176
175
  createElement({ url: '/cleanup-element.js' }, () => {
177
176
  useEffect(() => cleanup, []);
178
177
  return html` <div></div> `;
@@ -181,23 +180,23 @@ test('useEffect cleanup runs on disconnect', () => {
181
180
  const instance = new Clazz();
182
181
  instance.render();
183
182
  instance._flushEffects();
184
- expect(cleanup).not.toHaveBeenCalled();
183
+ assert.strictEqual(cleanup.mock.callCount(), 0);
185
184
 
186
185
  instance.disconnectedCallback();
187
- expect(cleanup).toHaveBeenCalledTimes(1);
186
+ assert.strictEqual(cleanup.mock.callCount(), 1);
188
187
  });
189
188
 
190
- test('createElement without attrs', () => {
189
+ test('createElement without attrs', (t) => {
191
190
  createElement({ url: '/base-element.js' }, () => {
192
191
  return html` <div></div> `;
193
192
  });
194
193
  const Clazz = getElement('base-element');
195
194
  const instance = new Clazz();
196
195
  const res = instance.render();
197
- expect(res).toMatchSnapshot();
196
+ t.assert.snapshot(res);
198
197
  });
199
198
 
200
- test('createElement with attrs and hooks', () => {
199
+ test('createElement with attrs and hooks', (t) => {
201
200
  createElement({ url: '/base-element.js' }, ({ perPage }) => {
202
201
  const { count, actions } = useReducer({
203
202
  initial: {
@@ -224,11 +223,11 @@ test('createElement with attrs and hooks', () => {
224
223
  const Clazz = getElement('base-element');
225
224
  const instance = new Clazz({ perPage: 5 });
226
225
  const res = instance.render();
227
- expect(Clazz.observedAttributes).toEqual(['perpage']);
226
+ assert.deepStrictEqual(Clazz.observedAttributes, ['perpage']);
228
- expect(res).toMatchSnapshot();
227
+ t.assert.snapshot(res);
229
228
  });
230
229
 
231
- test('createPage', () => {
230
+ test('createPage', (t) => {
232
231
  const route = () => {
233
232
  const langPart = this.config.lang === 'en' ? '' : `/${this.config.lang}`;
234
233
  return `${langPart}`;
@@ -258,5 +257,5 @@ test('createPage', () => {
258
257
  });
259
258
  const scripts = '<script type="module"><script>';
260
259
  const res = renderPage({ lang: 'en', props: { config: { title: '123' } }, headScript: scripts, bodyScript: scripts });
261
- expect(res).toMatchSnapshot();
260
+ t.assert.snapshot(res);
262
261
  });
index.test.js.snapshot ADDED
@@ -0,0 +1,47 @@
1
+ exports[`createElement styles are scoped and injected via SSR 1`] = `
2
+ "@scope (styled-element) {\\n\\n :scope {\\n display: flex;\\n }\\n .count {\\n color: magenta;\\n }\\n \\n}\\n"
3
+ `;
4
+
5
+ exports[`createElement with attrs and hooks 1`] = `
6
+ "\\n <div>\\n <div>\\n <span>perPage: 5</span> </div> </div> <span>Count: 3</span> <button>Set</button> "
7
+ `;
8
+
9
+ exports[`createElement without attrs 1`] = `
10
+ " <div></div>"
11
+ `;
12
+
13
+ exports[`createPage 1`] = `
14
+ "\\n <!DOCTYPE html>\\n <html lang=\\"en\\">\\n <head>\\n <meta charset=\\"utf-8\\" />\\n <meta http-equiv=\\"x-ua-compatible\\" content=\\"ie=edge\\" />\\n <meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=utf-8\\">\\n <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5.0, shrink-to-fit=no\\">\\n <link rel=\\"sitemap\\" type=\\"application/xml\\" href=\\"/sitemap.xml\\" />\\n <link rel=\\"icon\\" type=\\"image/png\\" href=\\"/assets/icon.png\\" />\\n \\n <title>123</title> <meta name=\\"title\\" content=\\"123\\"/> <meta name=\\"description\\" content=\\"123\\"/>\\n <style id=\\"global\\">\\n *, ::before, ::after {\\n\\n box-sizing: border-box;\\n border-width: 0;\\n border-style: solid;\\n border-color: #e5e7eb;\\n\\n}\\nhr {\\n\\n height: 0;\\n color: inherit;\\n border-top-width: 1px;\\n\\n}\\nabbr[title] {\\n\\n -webkit-text-decoration: underline dotted;\\n text-decoration: underline dotted;\\n\\n}\\nb, strong {\\n\\n font-weight: bolder;\\n\\n}\\ncode, kbd, samp, pre {\\n\\n font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;\\n font-size: 1em;\\n\\n}\\nsmall {\\n\\n font-size: 80%;\\n\\n}\\nsub, sup {\\n\\n font-size: 75%;\\n line-height: 0;\\n position: relative;\\n vertical-align: baseline;\\n\\n}\\nsub {\\n\\n bottom: -0.25em;\\n\\n}\\nsup {\\n\\n top: -0.5em;\\n\\n}\\ntable {\\n\\n text-indent: 0;\\n border-color: inherit;\\n border-collapse: collapse;\\n\\n}\\nbutton, input, optgroup, select, textarea {\\n\\n font-size: 100%;\\n margin: 0;\\n padding: 0;\\n line-height: inherit;\\n color: inherit;\\n\\n}\\nbutton, select {\\n\\n\\n}\\nbutton, [type='button'], [type='reset'], [type='submit'] {\\n\\n\\n}\\n::-moz-focus-inner {\\n\\n border-style: none;\\n padding: 0;\\n\\n}\\n:-moz-focusring {\\n\\n outline: 1px dotted ButtonText;\\n\\n}\\n:-moz-ui-invalid {\\n\\n box-shadow: none;\\n\\n}\\nlegend {\\n\\n padding: 0;\\n\\n}\\nprogress {\\n\\n vertical-align: baseline;\\n\\n}\\n::-webkit-inner-spin-button, ::-webkit-outer-spin-button {\\n\\n height: auto;\\n\\n}\\n[type='search'] {\\n\\n -webkit-appearance: textfield;\\n outline-offset: -2px;\\n\\n}\\n::-webkit-search-decoration {\\n\\n -webkit-appearance: none;\\n\\n}\\n::-webkit-file-upload-button {\\n\\n -webkit-appearance: button;\\n font: inherit;\\n\\n}\\nsummary {\\n\\n display: list-item;\\n\\n}\\nblockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre {\\n\\n margin: 0;\\n\\n}\\nbutton {\\n\\n background-image: none;\\n\\n outline: 1px dotted, 5px auto -webkit-focus-ring-color;\\n\\n}\\nfieldset {\\n\\n margin: 0;\\n padding: 0;\\n\\n}\\nol, ul {\\n\\n list-style: none;\\n margin: 0;\\n padding: 0;\\n\\n}\\nimg {\\n\\n border-style: solid;\\n\\n}\\ntextarea {\\n\\n resize: vertical;\\n\\n}\\ninput::-moz-placeholder, textarea::-moz-placeholder {\\n\\n opacity: 1;\\n color: #9ca3af;\\n\\n}\\ninput:-ms-input-placeholder, textarea:-ms-input-placeholder {\\n\\n opacity: 1;\\n color: #9ca3af;\\n\\n}\\ninput::placeholder, textarea::placeholder {\\n\\n opacity: 1;\\n color: #9ca3af;\\n\\n}\\nbutton, [role='button'] {\\n\\n cursor: pointer;\\n\\n}\\nh1, h2, h3, h4, h5, h6 {\\n\\n font-size: inherit;\\n font-weight: inherit;\\n\\n}\\na {\\n\\n color: inherit;\\n text-decoration: inherit;\\n\\n}\\npre, code, kbd, samp {\\n\\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;\\n\\n}\\nimg, svg, video, canvas, audio, iframe, embed, object {\\n\\n display: block;\\n vertical-align: middle;\\n\\n}\\nimg, video {\\n\\n max-width: 100%;\\n height: auto;\\n\\n}\\nhtml {\\n\\n -moz-tab-size: 4;\\n -o-tab-size: 4;\\n tab-size: 4;\\n line-height: 1.5;\\n -webkit-text-size-adjust: 100%;\\n font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';\\n width: 100%;\\n height: 100%;\\n\\n}\\nbody {\\n\\n margin: 0px;\\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\\n line-height: 1.4;\\n background-color: white;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n flex-direction: column;\\n flex: 1 1 0%;\\n min-width: 320px;\\n min-height: 100vh;\\n font-weight: 400;\\n color: rgba(44, 62, 80, 1);\\n direction: ltr;\\n font-synthesis: none;\\n text-rendering: optimizeLegibility;\\n\\n}\\n\\n \\n </style>\\n <script type=\\"module\\"><script>\\n </head>\\n <body>\\n \\n <div>\\n <app-header></app-header> <main class=\\"flex flex-1 flex-col mt-20 items-center\\">\\n <h1 class=\\"text-5xl\\">123</h1> </main> </div>\\n <script>\\n window.props = {\\"config\\":{\\"title\\":\\"123\\"}};\\n </script>\\n <script type=\\"module\\"><script>\\n </body>\\n </html>\\n "
15
+ `;
16
+
17
+ exports[`css tagged template 1`] = `
18
+ "\\n :scope {\\n display: flex;\\n }\\n button {\\n color: magenta;\\n }\\n "
19
+ `;
20
+
21
+ exports[`render attribute keys 1`] = `
22
+ "\\n <div>\\n <app-counter name=\\"123\\" perPage=\\"1\\"></app-counter> </div>"
23
+ `;
24
+
25
+ exports[`render attributes within quotes 1`] = `
26
+ "\\n <div>\\n <app-counter name=\\"123\\" class=\\"high\\" age=\\"1\\" details1=\\"{'name':'123','address':{'street':'1'}}\\" items=\\"[1,2,3]\\"></app-counter> </div>"
27
+ `;
28
+
29
+ exports[`render multi template 1`] = `
30
+ "\\n <div>\\n \\n <app-item meta=\\"{'index':1}\\">\\n <button>+</button> </app-item> <app-item meta=\\"{'index':2}\\">\\n <button>+</button> </app-item> </div>"
31
+ `;
32
+
33
+ exports[`render single template 1`] = `
34
+ " <div>NoCountry false</div>"
35
+ `;
36
+
37
+ exports[`render unsafeHTML 1`] = `
38
+ " <div><div><p class=\\"123\\">this is unsafe</p></div></div>"
39
+ `;
40
+
41
+ exports[`renderHtml 1`] = `
42
+ "\\n <div>\\n <app-counter name=\\"123\\" class=\\"abc high\\" age=\\"1\\" details1=\\"{'name':'123','address':{'street':'1'}}\\" items=\\"[1,2,3]\\"></app-counter> </div>"
43
+ `;
44
+
45
+ exports[`renderHtml escapes text and attribute values 1`] = `
46
+ "<div class=\\"a&quot; onmouseover=&quot;alert(1)\\">&lt;script&gt;alert(1)&lt;/script&gt;</div>"
47
+ `;
lit-html.js DELETED
@@ -1,1244 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
4
- * This code may only be used under the BSD style license found at
5
- * http://polymer.github.io/LICENSE.txt
6
- * The complete set of authors may be found at
7
- * http://polymer.github.io/AUTHORS.txt
8
- * The complete set of contributors may be found at
9
- * http://polymer.github.io/CONTRIBUTORS.txt
10
- * Code distributed by Google as part of the polymer project is also
11
- * subject to an additional IP rights grant found at
12
- * http://polymer.github.io/PATENTS.txt
13
- */
14
- const directives = new WeakMap();
15
- /**
16
- * Brands a function as a directive factory function so that lit-html will call
17
- * the function during template rendering, rather than passing as a value.
18
- *
19
- * A _directive_ is a function that takes a Part as an argument. It has the
20
- * signature: `(part: Part) => void`.
21
- *
22
- * A directive _factory_ is a function that takes arguments for data and
23
- * configuration and returns a directive. Users of directive usually refer to
24
- * the directive factory as the directive. For example, "The repeat directive".
25
- *
26
- * Usually a template author will invoke a directive factory in their template
27
- * with relevant arguments, which will then return a directive function.
28
- *
29
- * Here's an example of using the `repeat()` directive factory that takes an
30
- * array and a function to render an item:
31
- *
32
- * ```js
33
- * html`<ul><${repeat(items, (item) => html`<li>${item}</li>`)}</ul>`
34
- * ```
35
- *
36
- * When `repeat` is invoked, it returns a directive function that closes over
37
- * `items` and the template function. When the outer template is rendered, the
38
- * return directive function is called with the Part for the expression.
39
- * `repeat` then performs it's custom logic to render multiple items.
40
- *
41
- * @param f The directive factory function. Must be a function that returns a
42
- * function of the signature `(part: Part) => void`. The returned function will
43
- * be called with the part object.
44
- *
45
- * @example
46
- *
47
- * import {directive, html} from 'lit-html';
48
- *
49
- * const immutable = directive((v) => (part) => {
50
- * if (part.value !== v) {
51
- * part.setValue(v)
52
- * }
53
- * });
54
- */
55
- const directive =
56
- (f) =>
57
- (...args) => {
58
- const d = f(...args);
59
- directives.set(d, true);
60
- return d;
61
- };
62
- const isDirective = (o) => {
63
- return typeof o === 'function' && directives.has(o);
64
- };
65
-
66
- /**
67
- * @license
68
- * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
69
- * This code may only be used under the BSD style license found at
70
- * http://polymer.github.io/LICENSE.txt
71
- * The complete set of authors may be found at
72
- * http://polymer.github.io/AUTHORS.txt
73
- * The complete set of contributors may be found at
74
- * http://polymer.github.io/CONTRIBUTORS.txt
75
- * Code distributed by Google as part of the polymer project is also
76
- * subject to an additional IP rights grant found at
77
- * http://polymer.github.io/PATENTS.txt
78
- */
79
- /**
80
- * True if the custom elements polyfill is in use.
81
- */
82
- const isCEPolyfill = typeof window !== 'undefined' && window.customElements != null && window.customElements.polyfillWrapFlushCallback !== undefined;
83
- /**
84
- * Reparents nodes, starting from `start` (inclusive) to `end` (exclusive),
85
- * into another container (could be the same container), before `before`. If
86
- * `before` is null, it appends the nodes to the container.
87
- */
88
- const reparentNodes = (container, start, end = null, before = null) => {
89
- while (start !== end) {
90
- const n = start.nextSibling;
91
- container.insertBefore(start, before);
92
- start = n;
93
- }
94
- };
95
- /**
96
- * Removes nodes, starting from `start` (inclusive) to `end` (exclusive), from
97
- * `container`.
98
- */
99
- const removeNodes = (container, start, end = null) => {
100
- while (start !== end) {
101
- const n = start.nextSibling;
102
- container.removeChild(start);
103
- start = n;
104
- }
105
- };
106
-
107
- /**
108
- * @license
109
- * Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
110
- * This code may only be used under the BSD style license found at
111
- * http://polymer.github.io/LICENSE.txt
112
- * The complete set of authors may be found at
113
- * http://polymer.github.io/AUTHORS.txt
114
- * The complete set of contributors may be found at
115
- * http://polymer.github.io/CONTRIBUTORS.txt
116
- * Code distributed by Google as part of the polymer project is also
117
- * subject to an additional IP rights grant found at
118
- * http://polymer.github.io/PATENTS.txt
119
- */
120
- /**
121
- * A sentinel value that signals that a value was handled by a directive and
122
- * should not be written to the DOM.
123
- */
124
- const noChange = {};
125
- /**
126
- * A sentinel value that signals a NodePart to fully clear its content.
127
- */
128
- const nothing = {};
129
-
130
- /**
131
- * @license
132
- * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
133
- * This code may only be used under the BSD style license found at
134
- * http://polymer.github.io/LICENSE.txt
135
- * The complete set of authors may be found at
136
- * http://polymer.github.io/AUTHORS.txt
137
- * The complete set of contributors may be found at
138
- * http://polymer.github.io/CONTRIBUTORS.txt
139
- * Code distributed by Google as part of the polymer project is also
140
- * subject to an additional IP rights grant found at
141
- * http://polymer.github.io/PATENTS.txt
142
- */
143
- /**
144
- * An expression marker with embedded unique key to avoid collision with
145
- * possible text in templates.
146
- */
147
- const marker = `{{lit-${String(Math.random()).slice(2)}}}`;
148
- /**
149
- * An expression marker used text-positions, multi-binding attributes, and
150
- * attributes with markup-like text values.
151
- */
152
- const nodeMarker = `<!--${marker}-->`;
153
- const markerRegex = new RegExp(`${marker}|${nodeMarker}`);
154
- /**
155
- * Suffix appended to all bound attribute names.
156
- */
157
- const boundAttributeSuffix = '$lit$';
158
- /**
159
- * An updatable Template that tracks the location of dynamic parts.
160
- */
161
- class Template {
162
- constructor(result, element) {
163
- this.parts = [];
164
- this.element = element;
165
- const nodesToRemove = [];
166
- const stack = [];
167
- // Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null
168
- const walker = document.createTreeWalker(element.content, 133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, null, false);
169
- // Keeps track of the last index associated with a part. We try to delete
170
- // unnecessary nodes, but we never want to associate two different parts
171
- // to the same index. They must have a constant node between.
172
- let lastPartIndex = 0;
173
- let index = -1;
174
- let partIndex = 0;
175
- const {
176
- strings,
177
- values: { length },
178
- } = result;
179
- while (partIndex < length) {
180
- const node = walker.nextNode();
181
- if (node === null) {
182
- // We've exhausted the content inside a nested template element.
183
- // Because we still have parts (the outer for-loop), we know:
184
- // - There is a template in the stack
185
- // - The walker will find a nextNode outside the template
186
- walker.currentNode = stack.pop();
187
- continue;
188
- }
189
- index++;
190
- if (node.nodeType === 1 /* Node.ELEMENT_NODE */) {
191
- if (node.hasAttributes()) {
192
- const attributes = node.attributes;
193
- const { length } = attributes;
194
- // Per
195
- // https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap,
196
- // attributes are not guaranteed to be returned in document order.
197
- // In particular, Edge/IE can return them out of order, so we cannot
198
- // assume a correspondence between part index and attribute index.
199
- let count = 0;
200
- for (let i = 0; i < length; i++) {
201
- if (endsWith(attributes[i].name, boundAttributeSuffix)) {
202
- count++;
203
- }
204
- }
205
- while (count-- > 0) {
206
- // Get the template literal section leading up to the first
207
- // expression in this attribute
208
- const stringForPart = strings[partIndex];
209
- // Find the attribute name
210
- const name = lastAttributeNameRegex.exec(stringForPart)[2];
211
- // Find the corresponding attribute
212
- // All bound attributes have had a suffix added in
213
- // TemplateResult#getHTML to opt out of special attribute
214
- // handling. To look up the attribute value we also need to add
215
- // the suffix.
216
- const attributeLookupName = name.toLowerCase() + boundAttributeSuffix;
217
- const attributeValue = node.getAttribute(attributeLookupName);
218
- node.removeAttribute(attributeLookupName);
219
- const statics = attributeValue.split(markerRegex);
220
- this.parts.push({ type: 'attribute', index, name, strings: statics });
221
- partIndex += statics.length - 1;
222
- }
223
- }
224
- if (node.tagName === 'TEMPLATE') {
225
- stack.push(node);
226
- walker.currentNode = node.content;
227
- }
228
- } else if (node.nodeType === 3 /* Node.TEXT_NODE */) {
229
- const data = node.data;
230
- if (data.indexOf(marker) >= 0) {
231
- const parent = node.parentNode;
232
- const strings = data.split(markerRegex);
233
- const lastIndex = strings.length - 1;
234
- // Generate a new text node for each literal section
235
- // These nodes are also used as the markers for node parts
236
- for (let i = 0; i < lastIndex; i++) {
237
- let insert;
238
- let s = strings[i];
239
- if (s === '') {
240
- insert = createMarker();
241
- } else {
242
- const match = lastAttributeNameRegex.exec(s);
243
- if (match !== null && endsWith(match[2], boundAttributeSuffix)) {
244
- s = s.slice(0, match.index) + match[1] + match[2].slice(0, -boundAttributeSuffix.length) + match[3];
245
- }
246
- insert = document.createTextNode(s);
247
- }
248
- parent.insertBefore(insert, node);
249
- this.parts.push({ type: 'node', index: ++index });
250
- }
251
- // If there's no text, we must insert a comment to mark our place.
252
- // Else, we can trust it will stick around after cloning.
253
- if (strings[lastIndex] === '') {
254
- parent.insertBefore(createMarker(), node);
255
- nodesToRemove.push(node);
256
- } else {
257
- node.data = strings[lastIndex];
258
- }
259
- // We have a part for each match found
260
- partIndex += lastIndex;
261
- }
262
- } else if (node.nodeType === 8 /* Node.COMMENT_NODE */) {
263
- if (node.data === marker) {
264
- const parent = node.parentNode;
265
- // Add a new marker node to be the startNode of the Part if any of
266
- // the following are true:
267
- // * We don't have a previousSibling
268
- // * The previousSibling is already the start of a previous part
269
- if (node.previousSibling === null || index === lastPartIndex) {
270
- index++;
271
- parent.insertBefore(createMarker(), node);
272
- }
273
- lastPartIndex = index;
274
- this.parts.push({ type: 'node', index });
275
- // If we don't have a nextSibling, keep this node so we have an end.
276
- // Else, we can remove it to save future costs.
277
- if (node.nextSibling === null) {
278
- node.data = '';
279
- } else {
280
- nodesToRemove.push(node);
281
- index--;
282
- }
283
- partIndex++;
284
- } else {
285
- let i = -1;
286
- while ((i = node.data.indexOf(marker, i + 1)) !== -1) {
287
- // Comment node has a binding marker inside, make an inactive part
288
- // The binding won't work, but subsequent bindings will
289
- // TODO (justinfagnani): consider whether it's even worth it to
290
- // make bindings in comments work
291
- this.parts.push({ type: 'node', index: -1 });
292
- partIndex++;
293
- }
294
- }
295
- }
296
- }
297
- // Remove text binding nodes after the walk to not disturb the TreeWalker
298
- for (const n of nodesToRemove) {
299
- n.parentNode.removeChild(n);
300
- }
301
- }
302
- }
303
- const endsWith = (str, suffix) => {
304
- const index = str.length - suffix.length;
305
- return index >= 0 && str.slice(index) === suffix;
306
- };
307
- const isTemplatePartActive = (part) => part.index !== -1;
308
- // Allows `document.createComment('')` to be renamed for a
309
- // small manual size-savings.
310
- const createMarker = () => document.createComment('');
311
- /**
312
- * This regex extracts the attribute name preceding an attribute-position
313
- * expression. It does this by matching the syntax allowed for attributes
314
- * against the string literal directly preceding the expression, assuming that
315
- * the expression is in an attribute-value position.
316
- *
317
- * See attributes in the HTML spec:
318
- * https://www.w3.org/TR/html5/syntax.html#elements-attributes
319
- *
320
- * " \x09\x0a\x0c\x0d" are HTML space characters:
321
- * https://www.w3.org/TR/html5/infrastructure.html#space-characters
322
- *
323
- * "\0-\x1F\x7F-\x9F" are Unicode control characters, which includes every
324
- * space character except " ".
325
- *
326
- * So an attribute is:
327
- * * The name: any character except a control character, space character, ('),
328
- * ("), ">", "=", or "/"
329
- * * Followed by zero or more space characters
330
- * * Followed by "="
331
- * * Followed by zero or more space characters
332
- * * Followed by:
333
- * * Any character except space, ('), ("), "<", ">", "=", (`), or
334
- * * (") then any non-("), or
335
- * * (') then any non-(')
336
- */
337
- const lastAttributeNameRegex =
338
- // eslint-disable-next-line no-control-regex
339
- /([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
340
-
341
- /**
342
- * @license
343
- * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
344
- * This code may only be used under the BSD style license found at
345
- * http://polymer.github.io/LICENSE.txt
346
- * The complete set of authors may be found at
347
- * http://polymer.github.io/AUTHORS.txt
348
- * The complete set of contributors may be found at
349
- * http://polymer.github.io/CONTRIBUTORS.txt
350
- * Code distributed by Google as part of the polymer project is also
351
- * subject to an additional IP rights grant found at
352
- * http://polymer.github.io/PATENTS.txt
353
- */
354
- /**
355
- * An instance of a `Template` that can be attached to the DOM and updated
356
- * with new values.
357
- */
358
- class TemplateInstance {
359
- constructor(template, processor, options) {
360
- this.__parts = [];
361
- this.template = template;
362
- this.processor = processor;
363
- this.options = options;
364
- }
365
- update(values) {
366
- let i = 0;
367
- for (const part of this.__parts) {
368
- if (part !== undefined) {
369
- part.setValue(values[i]);
370
- }
371
- i++;
372
- }
373
- for (const part of this.__parts) {
374
- if (part !== undefined) {
375
- part.commit();
376
- }
377
- }
378
- }
379
- _clone() {
380
- // There are a number of steps in the lifecycle of a template instance's
381
- // DOM fragment:
382
- // 1. Clone - create the instance fragment
383
- // 2. Adopt - adopt into the main document
384
- // 3. Process - find part markers and create parts
385
- // 4. Upgrade - upgrade custom elements
386
- // 5. Update - set node, attribute, property, etc., values
387
- // 6. Connect - connect to the document. Optional and outside of this
388
- // method.
389
- //
390
- // We have a few constraints on the ordering of these steps:
391
- // * We need to upgrade before updating, so that property values will pass
392
- // through any property setters.
393
- // * We would like to process before upgrading so that we're sure that the
394
- // cloned fragment is inert and not disturbed by self-modifying DOM.
395
- // * We want custom elements to upgrade even in disconnected fragments.
396
- //
397
- // Given these constraints, with full custom elements support we would
398
- // prefer the order: Clone, Process, Adopt, Upgrade, Update, Connect
399
- //
400
- // But Safari does not implement CustomElementRegistry#upgrade, so we
401
- // can not implement that order and still have upgrade-before-update and
402
- // upgrade disconnected fragments. So we instead sacrifice the
403
- // process-before-upgrade constraint, since in Custom Elements v1 elements
404
- // must not modify their light DOM in the constructor. We still have issues
405
- // when co-existing with CEv0 elements like Polymer 1, and with polyfills
406
- // that don't strictly adhere to the no-modification rule because shadow
407
- // DOM, which may be created in the constructor, is emulated by being placed
408
- // in the light DOM.
409
- //
410
- // The resulting order is on native is: Clone, Adopt, Upgrade, Process,
411
- // Update, Connect. document.importNode() performs Clone, Adopt, and Upgrade
412
- // in one step.
413
- //
414
- // The Custom Elements v1 polyfill supports upgrade(), so the order when
415
- // polyfilled is the more ideal: Clone, Process, Adopt, Upgrade, Update,
416
- // Connect.
417
- const fragment = isCEPolyfill ? this.template.element.content.cloneNode(true) : document.importNode(this.template.element.content, true);
418
- const stack = [];
419
- const parts = this.template.parts;
420
- // Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null
421
- const walker = document.createTreeWalker(fragment, 133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, null, false);
422
- let partIndex = 0;
423
- let nodeIndex = 0;
424
- let part;
425
- let node = walker.nextNode();
426
- // Loop through all the nodes and parts of a template
427
- while (partIndex < parts.length) {
428
- part = parts[partIndex];
429
- if (!isTemplatePartActive(part)) {
430
- this.__parts.push(undefined);
431
- partIndex++;
432
- continue;
433
- }
434
- // Progress the tree walker until we find our next part's node.
435
- // Note that multiple parts may share the same node (attribute parts
436
- // on a single element), so this loop may not run at all.
437
- while (nodeIndex < part.index) {
438
- nodeIndex++;
439
- if (node.nodeName === 'TEMPLATE') {
440
- stack.push(node);
441
- walker.currentNode = node.content;
442
- }
443
- if ((node = walker.nextNode()) === null) {
444
- // We've exhausted the content inside a nested template element.
445
- // Because we still have parts (the outer for-loop), we know:
446
- // - There is a template in the stack
447
- // - The walker will find a nextNode outside the template
448
- walker.currentNode = stack.pop();
449
- node = walker.nextNode();
450
- }
451
- }
452
- // We've arrived at our part's node.
453
- if (part.type === 'node') {
454
- const part = this.processor.handleTextExpression(this.options);
455
- part.insertAfterNode(node.previousSibling);
456
- this.__parts.push(part);
457
- } else {
458
- this.__parts.push(...this.processor.handleAttributeExpressions(node, part.name, part.strings, this.options));
459
- }
460
- partIndex++;
461
- }
462
- if (isCEPolyfill) {
463
- document.adoptNode(fragment);
464
- customElements.upgrade(fragment);
465
- }
466
- return fragment;
467
- }
468
- }
469
-
470
- /**
471
- * @license
472
- * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
473
- * This code may only be used under the BSD style license found at
474
- * http://polymer.github.io/LICENSE.txt
475
- * The complete set of authors may be found at
476
- * http://polymer.github.io/AUTHORS.txt
477
- * The complete set of contributors may be found at
478
- * http://polymer.github.io/CONTRIBUTORS.txt
479
- * Code distributed by Google as part of the polymer project is also
480
- * subject to an additional IP rights grant found at
481
- * http://polymer.github.io/PATENTS.txt
482
- */
483
- /**
484
- * Our TrustedTypePolicy for HTML which is declared using the html template
485
- * tag function.
486
- *
487
- * That HTML is a developer-authored constant, and is parsed with innerHTML
488
- * before any untrusted expressions have been mixed in. Therefor it is
489
- * considered safe by construction.
490
- */
491
- const policy = typeof window !== 'undefined' && window.trustedTypes && trustedTypes.createPolicy('lit-html', { createHTML: (s) => s });
492
- const commentMarker = ` ${marker} `;
493
- /**
494
- * The return type of `html`, which holds a Template and the values from
495
- * interpolated expressions.
496
- */
497
- class TemplateResult {
498
- constructor(strings, values, type, processor) {
499
- this.strings = strings;
500
- this.values = values;
501
- this.type = type;
502
- this.processor = processor;
503
- }
504
- /**
505
- * Returns a string of HTML used to create a `<template>` element.
506
- */
507
- getHTML() {
508
- const l = this.strings.length - 1;
509
- let html = '';
510
- let isCommentBinding = false;
511
- for (let i = 0; i < l; i++) {
512
- const s = this.strings[i];
513
- // For each binding we want to determine the kind of marker to insert
514
- // into the template source before it's parsed by the browser's HTML
515
- // parser. The marker type is based on whether the expression is in an
516
- // attribute, text, or comment position.
517
- // * For node-position bindings we insert a comment with the marker
518
- // sentinel as its text content, like <!--{{lit-guid}}-->.
519
- // * For attribute bindings we insert just the marker sentinel for the
520
- // first binding, so that we support unquoted attribute bindings.
521
- // Subsequent bindings can use a comment marker because multi-binding
522
- // attributes must be quoted.
523
- // * For comment bindings we insert just the marker sentinel so we don't
524
- // close the comment.
525
- //
526
- // The following code scans the template source, but is *not* an HTML
527
- // parser. We don't need to track the tree structure of the HTML, only
528
- // whether a binding is inside a comment, and if not, if it appears to be
529
- // the first binding in an attribute.
530
- const commentOpen = s.lastIndexOf('<!--');
531
- // We're in comment position if we have a comment open with no following
532
- // comment close. Because <-- can appear in an attribute value there can
533
- // be false positives.
534
- isCommentBinding = (commentOpen > -1 || isCommentBinding) && s.indexOf('-->', commentOpen + 1) === -1;
535
- // Check to see if we have an attribute-like sequence preceding the
536
- // expression. This can match "name=value" like structures in text,
537
- // comments, and attribute values, so there can be false-positives.
538
- const attributeMatch = lastAttributeNameRegex.exec(s);
539
- if (attributeMatch === null) {
540
- // We're only in this branch if we don't have a attribute-like
541
- // preceding sequence. For comments, this guards against unusual
542
- // attribute values like <div foo="<!--${'bar'}">. Cases like
543
- // <!-- foo=${'bar'}--> are handled correctly in the attribute branch
544
- // below.
545
- html += s + (isCommentBinding ? commentMarker : nodeMarker);
546
- } else {
547
- // For attributes we use just a marker sentinel, and also append a
548
- // $lit$ suffix to the name to opt-out of attribute-specific parsing
549
- // that IE and Edge do for style and certain SVG attributes.
550
- html += s.substr(0, attributeMatch.index) + attributeMatch[1] + attributeMatch[2] + boundAttributeSuffix + attributeMatch[3] + marker;
551
- }
552
- }
553
- html += this.strings[l];
554
- return html;
555
- }
556
- getTemplateElement() {
557
- const template = document.createElement('template');
558
- let value = this.getHTML();
559
- if (policy !== undefined) {
560
- // this is secure because `this.strings` is a TemplateStringsArray.
561
- // TODO: validate this when
562
- // https://github.com/tc39/proposal-array-is-template-object is
563
- // implemented.
564
- value = policy.createHTML(value);
565
- }
566
- template.innerHTML = value;
567
- return template;
568
- }
569
- }
570
- /**
571
- * A TemplateResult for SVG fragments.
572
- *
573
- * This class wraps HTML in an `<svg>` tag in order to parse its contents in the
574
- * SVG namespace, then modifies the template to remove the `<svg>` tag so that
575
- * clones only container the original fragment.
576
- */
577
- class SVGTemplateResult extends TemplateResult {
578
- getHTML() {
579
- return `<svg>${super.getHTML()}</svg>`;
580
- }
581
- getTemplateElement() {
582
- const template = super.getTemplateElement();
583
- const content = template.content;
584
- const svgElement = content.firstChild;
585
- content.removeChild(svgElement);
586
- reparentNodes(content, svgElement.firstChild);
587
- return template;
588
- }
589
- }
590
-
591
- /**
592
- * @license
593
- * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
594
- * This code may only be used under the BSD style license found at
595
- * http://polymer.github.io/LICENSE.txt
596
- * The complete set of authors may be found at
597
- * http://polymer.github.io/AUTHORS.txt
598
- * The complete set of contributors may be found at
599
- * http://polymer.github.io/CONTRIBUTORS.txt
600
- * Code distributed by Google as part of the polymer project is also
601
- * subject to an additional IP rights grant found at
602
- * http://polymer.github.io/PATENTS.txt
603
- */
604
- const isPrimitive = (value) => {
605
- return value === null || !(typeof value === 'object' || typeof value === 'function');
606
- };
607
- const isIterable = (value) => {
608
- return (
609
- Array.isArray(value) ||
610
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
611
- !!(value && value[Symbol.iterator])
612
- );
613
- };
614
- /**
615
- * Writes attribute values to the DOM for a group of AttributeParts bound to a
616
- * single attribute. The value is only set once even if there are multiple parts
617
- * for an attribute.
618
- */
619
- class AttributeCommitter {
620
- constructor(element, name, strings) {
621
- this.dirty = true;
622
- this.element = element;
623
- this.name = name;
624
- this.strings = strings;
625
- this.parts = [];
626
- for (let i = 0; i < strings.length - 1; i++) {
627
- this.parts[i] = this._createPart();
628
- }
629
- }
630
- /**
631
- * Creates a single part. Override this to create a differnt type of part.
632
- */
633
- _createPart() {
634
- return new AttributePart(this);
635
- }
636
- _getValue() {
637
- const strings = this.strings;
638
- const l = strings.length - 1;
639
- const parts = this.parts;
640
- // If we're assigning an attribute via syntax like:
641
- // attr="${foo}" or attr=${foo}
642
- // but not
643
- // attr="${foo} ${bar}" or attr="${foo} baz"
644
- // then we don't want to coerce the attribute value into one long
645
- // string. Instead we want to just return the value itself directly,
646
- // so that sanitizeDOMValue can get the actual value rather than
647
- // String(value)
648
- // The exception is if v is an array, in which case we do want to smash
649
- // it together into a string without calling String() on the array.
650
- //
651
- // This also allows trusted values (when using TrustedTypes) being
652
- // assigned to DOM sinks without being stringified in the process.
653
- if (l === 1 && strings[0] === '' && strings[1] === '') {
654
- const v = parts[0].value;
655
- if (typeof v === 'symbol') {
656
- return String(v);
657
- }
658
- if (typeof v === 'string' || !isIterable(v)) {
659
- return v;
660
- }
661
- }
662
- let text = '';
663
- for (let i = 0; i < l; i++) {
664
- text += strings[i];
665
- const part = parts[i];
666
- if (part !== undefined) {
667
- const v = part.value;
668
- if (isPrimitive(v) || !isIterable(v)) {
669
- text += typeof v === 'string' ? v : String(v);
670
- } else {
671
- for (const t of v) {
672
- text += typeof t === 'string' ? t : String(t);
673
- }
674
- }
675
- }
676
- }
677
- text += strings[l];
678
- return text;
679
- }
680
- commit() {
681
- if (this.dirty) {
682
- this.dirty = false;
683
- this.element.setAttribute(this.name, this._getValue());
684
- }
685
- }
686
- }
687
- /**
688
- * A Part that controls all or part of an attribute value.
689
- */
690
- class AttributePart {
691
- constructor(committer) {
692
- this.value = undefined;
693
- this.committer = committer;
694
- }
695
- setValue(value) {
696
- if (value !== noChange && (!isPrimitive(value) || value !== this.value)) {
697
- if (typeof value === 'object') {
698
- this.value = JSON.stringify(value).replace(/"/g, `'`);
699
- } else {
700
- this.value = value;
701
- }
702
- // If the value is a not a directive, dirty the committer so that it'll
703
- // call setAttribute. If the value is a directive, it'll dirty the
704
- // committer if it calls setValue().
705
- if (!isDirective(value)) {
706
- this.committer.dirty = true;
707
- }
708
- }
709
- }
710
- commit() {
711
- while (isDirective(this.value)) {
712
- const directive = this.value;
713
- this.value = noChange;
714
- directive(this);
715
- }
716
- if (this.value === noChange) {
717
- return;
718
- }
719
- this.committer.commit();
720
- }
721
- }
722
- /**
723
- * A Part that controls a location within a Node tree. Like a Range, NodePart
724
- * has start and end locations and can set and update the Nodes between those
725
- * locations.
726
- *
727
- * NodeParts support several value types: primitives, Nodes, TemplateResults,
728
- * as well as arrays and iterables of those types.
729
- */
730
- class NodePart {
731
- constructor(options) {
732
- this.value = undefined;
733
- this.__pendingValue = undefined;
734
- this.options = options;
735
- }
736
- /**
737
- * Appends this part into a container.
738
- *
739
- * This part must be empty, as its contents are not automatically moved.
740
- */
741
- appendInto(container) {
742
- this.startNode = container.appendChild(createMarker());
743
- this.endNode = container.appendChild(createMarker());
744
- }
745
- /**
746
- * Inserts this part after the `ref` node (between `ref` and `ref`'s next
747
- * sibling). Both `ref` and its next sibling must be static, unchanging nodes
748
- * such as those that appear in a literal section of a template.
749
- *
750
- * This part must be empty, as its contents are not automatically moved.
751
- */
752
- insertAfterNode(ref) {
753
- this.startNode = ref;
754
- this.endNode = ref.nextSibling;
755
- }
756
- /**
757
- * Appends this part into a parent part.
758
- *
759
- * This part must be empty, as its contents are not automatically moved.
760
- */
761
- appendIntoPart(part) {
762
- part.__insert((this.startNode = createMarker()));
763
- part.__insert((this.endNode = createMarker()));
764
- }
765
- /**
766
- * Inserts this part after the `ref` part.
767
- *
768
- * This part must be empty, as its contents are not automatically moved.
769
- */
770
- insertAfterPart(ref) {
771
- ref.__insert((this.startNode = createMarker()));
772
- this.endNode = ref.endNode;
773
- ref.endNode = this.startNode;
774
- }
775
- setValue(value) {
776
- this.__pendingValue = value;
777
- }
778
- commit() {
779
- if (this.startNode.parentNode === null) {
780
- return;
781
- }
782
- while (isDirective(this.__pendingValue)) {
783
- const directive = this.__pendingValue;
784
- this.__pendingValue = noChange;
785
- directive(this);
786
- }
787
- const value = this.__pendingValue;
788
- if (value === noChange) {
789
- return;
790
- }
791
- if (isPrimitive(value)) {
792
- if (value !== this.value) {
793
- this.__commitText(value);
794
- }
795
- } else if (value instanceof TemplateResult) {
796
- this.__commitTemplateResult(value);
797
- } else if (value instanceof Node) {
798
- this.__commitNode(value);
799
- } else if (isIterable(value)) {
800
- this.__commitIterable(value);
801
- } else if (value === nothing) {
802
- this.value = nothing;
803
- this.clear();
804
- } else {
805
- // Fallback, will render the string representation
806
- this.__commitText(value);
807
- }
808
- }
809
- __insert(node) {
810
- this.endNode.parentNode.insertBefore(node, this.endNode);
811
- }
812
- __commitNode(value) {
813
- if (this.value === value) {
814
- return;
815
- }
816
- this.clear();
817
- this.__insert(value);
818
- this.value = value;
819
- }
820
- __commitText(value) {
821
- const node = this.startNode.nextSibling;
822
- value = value == null ? '' : value;
823
- // If `value` isn't already a string, we explicitly convert it here in case
824
- // it can't be implicitly converted - i.e. it's a symbol.
825
- const valueAsString = typeof value === 'string' ? value : String(value);
826
- if (node === this.endNode.previousSibling && node.nodeType === 3 /* Node.TEXT_NODE */) {
827
- // If we only have a single text node between the markers, we can just
828
- // set its value, rather than replacing it.
829
- // TODO(justinfagnani): Can we just check if this.value is primitive?
830
- node.data = valueAsString;
831
- } else {
832
- this.__commitNode(document.createTextNode(valueAsString));
833
- }
834
- this.value = value;
835
- }
836
- __commitTemplateResult(value) {
837
- const template = this.options.templateFactory(value);
838
- if (this.value instanceof TemplateInstance && this.value.template === template) {
839
- this.value.update(value.values);
840
- } else {
841
- // Make sure we propagate the template processor from the TemplateResult
842
- // so that we use its syntax extension, etc. The template factory comes
843
- // from the render function options so that it can control template
844
- // caching and preprocessing.
845
- const instance = new TemplateInstance(template, value.processor, this.options);
846
- const fragment = instance._clone();
847
- instance.update(value.values);
848
- this.__commitNode(fragment);
849
- this.value = instance;
850
- }
851
- }
852
- __commitIterable(value) {
853
- // For an Iterable, we create a new InstancePart per item, then set its
854
- // value to the item. This is a little bit of overhead for every item in
855
- // an Iterable, but it lets us recurse easily and efficiently update Arrays
856
- // of TemplateResults that will be commonly returned from expressions like:
857
- // array.map((i) => html`${i}`), by reusing existing TemplateInstances.
858
- // If _value is an array, then the previous render was of an
859
- // iterable and _value will contain the NodeParts from the previous
860
- // render. If _value is not an array, clear this part and make a new
861
- // array for NodeParts.
862
- if (!Array.isArray(this.value)) {
863
- this.value = [];
864
- this.clear();
865
- }
866
- // Lets us keep track of how many items we stamped so we can clear leftover
867
- // items from a previous render
868
- const itemParts = this.value;
869
- let partIndex = 0;
870
- let itemPart;
871
- for (const item of value) {
872
- // Try to reuse an existing part
873
- itemPart = itemParts[partIndex];
874
- // If no existing part, create a new one
875
- if (itemPart === undefined) {
876
- itemPart = new NodePart(this.options);
877
- itemParts.push(itemPart);
878
- if (partIndex === 0) {
879
- itemPart.appendIntoPart(this);
880
- } else {
881
- itemPart.insertAfterPart(itemParts[partIndex - 1]);
882
- }
883
- }
884
- itemPart.setValue(item);
885
- itemPart.commit();
886
- partIndex++;
887
- }
888
- if (partIndex < itemParts.length) {
889
- // Truncate the parts array so _value reflects the current state
890
- itemParts.length = partIndex;
891
- this.clear(itemPart && itemPart.endNode);
892
- }
893
- }
894
- clear(startNode = this.startNode) {
895
- removeNodes(this.startNode.parentNode, startNode.nextSibling, this.endNode);
896
- }
897
- }
898
- /**
899
- * Implements a boolean attribute, roughly as defined in the HTML
900
- * specification.
901
- *
902
- * If the value is truthy, then the attribute is present with a value of
903
- * ''. If the value is falsey, the attribute is removed.
904
- */
905
- class BooleanAttributePart {
906
- constructor(element, name, strings) {
907
- this.value = undefined;
908
- this.__pendingValue = undefined;
909
- if (strings.length !== 2 || strings[0] !== '' || strings[1] !== '') {
910
- throw new Error('Boolean attributes can only contain a single expression');
911
- }
912
- this.element = element;
913
- this.name = name;
914
- this.strings = strings;
915
- }
916
- setValue(value) {
917
- this.__pendingValue = value;
918
- }
919
- commit() {
920
- while (isDirective(this.__pendingValue)) {
921
- const directive = this.__pendingValue;
922
- this.__pendingValue = noChange;
923
- directive(this);
924
- }
925
- if (this.__pendingValue === noChange) {
926
- return;
927
- }
928
- const value = !!this.__pendingValue;
929
- if (this.value !== value) {
930
- if (value) {
931
- this.element.setAttribute(this.name, '');
932
- } else {
933
- this.element.removeAttribute(this.name);
934
- }
935
- this.value = value;
936
- }
937
- this.__pendingValue = noChange;
938
- }
939
- }
940
- /**
941
- * Sets attribute values for PropertyParts, so that the value is only set once
942
- * even if there are multiple parts for a property.
943
- *
944
- * If an expression controls the whole property value, then the value is simply
945
- * assigned to the property under control. If there are string literals or
946
- * multiple expressions, then the strings are expressions are interpolated into
947
- * a string first.
948
- */
949
- class PropertyCommitter extends AttributeCommitter {
950
- constructor(element, name, strings) {
951
- super(element, name, strings);
952
- this.single = strings.length === 2 && strings[0] === '' && strings[1] === '';
953
- }
954
- _createPart() {
955
- return new PropertyPart(this);
956
- }
957
- _getValue() {
958
- if (this.single) {
959
- return this.parts[0].value;
960
- }
961
- return super._getValue();
962
- }
963
- commit() {
964
- if (this.dirty) {
965
- this.dirty = false;
966
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
967
- this.element[this.name] = this._getValue();
968
- }
969
- }
970
- }
971
- class PropertyPart extends AttributePart {}
972
- // Detect event listener options support. If the `capture` property is read
973
- // from the options object, then options are supported. If not, then the third
974
- // argument to add/removeEventListener is interpreted as the boolean capture
975
- // value so we should only pass the `capture` property.
976
- let eventOptionsSupported = false;
977
- // Wrap into an IIFE because MS Edge <= v41 does not support having try/catch
978
- // blocks right into the body of a module
979
- (() => {
980
- try {
981
- const options = {
982
- get capture() {
983
- eventOptionsSupported = true;
984
- return false;
985
- },
986
- };
987
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
988
- window.addEventListener('test', options, options);
989
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
990
- window.removeEventListener('test', options, options);
991
- } catch (_e) {
992
- // event options not supported
993
- }
994
- })();
995
- class EventPart {
996
- constructor(element, eventName, eventContext) {
997
- this.value = undefined;
998
- this.__pendingValue = undefined;
999
- this.element = element;
1000
- this.eventName = eventName;
1001
- this.eventContext = eventContext;
1002
- this.__boundHandleEvent = (e) => this.handleEvent(e);
1003
- }
1004
- setValue(value) {
1005
- this.__pendingValue = value;
1006
- }
1007
- commit() {
1008
- while (isDirective(this.__pendingValue)) {
1009
- const directive = this.__pendingValue;
1010
- this.__pendingValue = noChange;
1011
- directive(this);
1012
- }
1013
- if (this.__pendingValue === noChange) {
1014
- return;
1015
- }
1016
- const newListener = this.__pendingValue;
1017
- const oldListener = this.value;
1018
- const shouldRemoveListener =
1019
- newListener == null ||
1020
- (oldListener != null &&
1021
- (newListener.capture !== oldListener.capture || newListener.once !== oldListener.once || newListener.passive !== oldListener.passive));
1022
- const shouldAddListener = newListener != null && (oldListener == null || shouldRemoveListener);
1023
- if (shouldRemoveListener) {
1024
- this.element.removeEventListener(this.eventName, this.__boundHandleEvent, this.__options);
1025
- }
1026
- if (shouldAddListener) {
1027
- this.__options = getOptions(newListener);
1028
- this.element.addEventListener(this.eventName, this.__boundHandleEvent, this.__options);
1029
- }
1030
- this.value = newListener;
1031
- this.__pendingValue = noChange;
1032
- }
1033
- handleEvent(event) {
1034
- if (typeof this.value === 'function') {
1035
- this.value.call(this.eventContext || this.element, event);
1036
- } else {
1037
- this.value.handleEvent(event);
1038
- }
1039
- }
1040
- }
1041
- // We copy options because of the inconsistent behavior of browsers when reading
1042
- // the third argument of add/removeEventListener. IE11 doesn't support options
1043
- // at all. Chrome 41 only reads `capture` if the argument is an object.
1044
- const getOptions = (o) => o && (eventOptionsSupported ? { capture: o.capture, passive: o.passive, once: o.once } : o.capture);
1045
-
1046
- /**
1047
- * @license
1048
- * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
1049
- * This code may only be used under the BSD style license found at
1050
- * http://polymer.github.io/LICENSE.txt
1051
- * The complete set of authors may be found at
1052
- * http://polymer.github.io/AUTHORS.txt
1053
- * The complete set of contributors may be found at
1054
- * http://polymer.github.io/CONTRIBUTORS.txt
1055
- * Code distributed by Google as part of the polymer project is also
1056
- * subject to an additional IP rights grant found at
1057
- * http://polymer.github.io/PATENTS.txt
1058
- */
1059
- /**
1060
- * Creates Parts when a template is instantiated.
1061
- */
1062
- class DefaultTemplateProcessor {
1063
- /**
1064
- * Create parts for an attribute-position binding, given the event, attribute
1065
- * name, and string literals.
1066
- *
1067
- * @param element The element containing the binding
1068
- * @param name The attribute name
1069
- * @param strings The string literals. There are always at least two strings,
1070
- * event for fully-controlled bindings with a single expression.
1071
- */
1072
- handleAttributeExpressions(element, name, strings, options) {
1073
- const prefix = name[0];
1074
- if (prefix === '.') {
1075
- const committer = new PropertyCommitter(element, name.slice(1), strings);
1076
- return committer.parts;
1077
- }
1078
- if (prefix === '@') {
1079
- return [new EventPart(element, name.slice(1), options.eventContext)];
1080
- }
1081
- if (prefix === '?') {
1082
- return [new BooleanAttributePart(element, name.slice(1), strings)];
1083
- }
1084
- const committer = new AttributeCommitter(element, name, strings);
1085
- return committer.parts;
1086
- }
1087
- /**
1088
- * Create parts for a text-position binding.
1089
- * @param templateFactory
1090
- */
1091
- handleTextExpression(options) {
1092
- return new NodePart(options);
1093
- }
1094
- }
1095
- const defaultTemplateProcessor = new DefaultTemplateProcessor();
1096
-
1097
- /**
1098
- * @license
1099
- * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
1100
- * This code may only be used under the BSD style license found at
1101
- * http://polymer.github.io/LICENSE.txt
1102
- * The complete set of authors may be found at
1103
- * http://polymer.github.io/AUTHORS.txt
1104
- * The complete set of contributors may be found at
1105
- * http://polymer.github.io/CONTRIBUTORS.txt
1106
- * Code distributed by Google as part of the polymer project is also
1107
- * subject to an additional IP rights grant found at
1108
- * http://polymer.github.io/PATENTS.txt
1109
- */
1110
- /**
1111
- * The default TemplateFactory which caches Templates keyed on
1112
- * result.type and result.strings.
1113
- */
1114
- function templateFactory(result) {
1115
- let templateCache = templateCaches.get(result.type);
1116
- if (templateCache === undefined) {
1117
- templateCache = {
1118
- stringsArray: new WeakMap(),
1119
- keyString: new Map(),
1120
- };
1121
- templateCaches.set(result.type, templateCache);
1122
- }
1123
- let template = templateCache.stringsArray.get(result.strings);
1124
- if (template !== undefined) {
1125
- return template;
1126
- }
1127
- // If the TemplateStringsArray is new, generate a key from the strings
1128
- // This key is shared between all templates with identical content
1129
- const key = result.strings.join(marker);
1130
- // Check if we already have a Template for this key
1131
- template = templateCache.keyString.get(key);
1132
- if (template === undefined) {
1133
- // If we have not seen this key before, create a new Template
1134
- template = new Template(result, result.getTemplateElement());
1135
- // Cache the Template for this key
1136
- templateCache.keyString.set(key, template);
1137
- }
1138
- // Cache all future queries for this TemplateStringsArray
1139
- templateCache.stringsArray.set(result.strings, template);
1140
- return template;
1141
- }
1142
- const templateCaches = new Map();
1143
-
1144
- /**
1145
- * @license
1146
- * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
1147
- * This code may only be used under the BSD style license found at
1148
- * http://polymer.github.io/LICENSE.txt
1149
- * The complete set of authors may be found at
1150
- * http://polymer.github.io/AUTHORS.txt
1151
- * The complete set of contributors may be found at
1152
- * http://polymer.github.io/CONTRIBUTORS.txt
1153
- * Code distributed by Google as part of the polymer project is also
1154
- * subject to an additional IP rights grant found at
1155
- * http://polymer.github.io/PATENTS.txt
1156
- */
1157
- const parts = new WeakMap();
1158
- /**
1159
- * Renders a template result or other value to a container.
1160
- *
1161
- * To update a container with new values, reevaluate the template literal and
1162
- * call `render` with the new result.
1163
- *
1164
- * @param result Any value renderable by NodePart - typically a TemplateResult
1165
- * created by evaluating a template tag like `html` or `svg`.
1166
- * @param container A DOM parent to render to. The entire contents are either
1167
- * replaced, or efficiently updated if the same result type was previous
1168
- * rendered there.
1169
- * @param options RenderOptions for the entire render tree rendered to this
1170
- * container. Render options must *not* change between renders to the same
1171
- * container, as those changes will not effect previously rendered DOM.
1172
- */
1173
- const render = (result, container, options) => {
1174
- let part = parts.get(container);
1175
- if (part === undefined) {
1176
- removeNodes(container, container.firstChild);
1177
- parts.set(container, (part = new NodePart(Object.assign({ templateFactory }, options))));
1178
- part.appendInto(container);
1179
- }
1180
- part.setValue(result);
1181
- part.commit();
1182
- };
1183
-
1184
- /**
1185
- * @license
1186
- * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
1187
- * This code may only be used under the BSD style license found at
1188
- * http://polymer.github.io/LICENSE.txt
1189
- * The complete set of authors may be found at
1190
- * http://polymer.github.io/AUTHORS.txt
1191
- * The complete set of contributors may be found at
1192
- * http://polymer.github.io/CONTRIBUTORS.txt
1193
- * Code distributed by Google as part of the polymer project is also
1194
- * subject to an additional IP rights grant found at
1195
- * http://polymer.github.io/PATENTS.txt
1196
- */
1197
- // IMPORTANT: do not change the property name or the assignment expression.
1198
- // This line will be used in regexes to search for lit-html usage.
1199
- // TODO(justinfagnani): inject version number at build time
1200
- if (typeof window !== 'undefined') {
1201
- (window['litHtmlVersions'] || (window['litHtmlVersions'] = [])).push('1.4.1');
1202
- }
1203
- /**
1204
- * Interprets a template literal as an HTML template that can efficiently
1205
- * render to and update a container.
1206
- */
1207
- const html = (strings, ...values) => new TemplateResult(strings, values, 'html', defaultTemplateProcessor);
1208
- /**
1209
- * Interprets a template literal as an SVG template that can efficiently
1210
- * render to and update a container.
1211
- */
1212
- const svg = (strings, ...values) => new SVGTemplateResult(strings, values, 'svg', defaultTemplateProcessor);
1213
-
1214
- export {
1215
- AttributeCommitter,
1216
- AttributePart,
1217
- BooleanAttributePart,
1218
- DefaultTemplateProcessor,
1219
- EventPart,
1220
- NodePart,
1221
- PropertyCommitter,
1222
- PropertyPart,
1223
- SVGTemplateResult,
1224
- Template,
1225
- TemplateInstance,
1226
- TemplateResult,
1227
- createMarker,
1228
- defaultTemplateProcessor,
1229
- directive,
1230
- html,
1231
- isDirective,
1232
- isIterable,
1233
- isPrimitive,
1234
- isTemplatePartActive,
1235
- noChange,
1236
- nothing,
1237
- parts,
1238
- removeNodes,
1239
- render,
1240
- reparentNodes,
1241
- svg,
1242
- templateCaches,
1243
- templateFactory,
1244
- };
package.json CHANGED
@@ -14,7 +14,6 @@
14
14
  "lit-html"
15
15
  ],
16
16
  "files": [
17
- "lit-html.js",
18
17
  "index.js",
19
18
  "index.d.ts"
20
19
  ],
@@ -22,17 +21,16 @@
22
21
  "author": "pyrossh",
23
22
  "type": "module",
24
23
  "engines": {
25
- "node": ">=14.0.0"
24
+ "node": ">=22.3.0"
26
25
  },
27
26
  "scripts": {
28
- "example": "node example/server.js",
27
+ "example": "node examples/server.js",
28
+ "test": "node --test index.test.js",
29
+ "test:update-snapshots": "node --test --test-update-snapshots index.test.js",
29
- "test": "NODE_OPTIONS=--experimental-vm-modules jest"
30
+ "test:e2e": "node --test examples/e2e.spec.js"
30
31
  },
31
32
  "devDependencies": {
32
- "jest": "^27.0.5"
33
+ "playwright": "^1.62.1"
33
- },
34
- "jest": {
35
- "transform": {}
36
34
  },
37
35
  "prettier": {
38
36
  "printWidth": 160,
@@ -41,6 +39,7 @@
41
39
  "trailingComma": "all"
42
40
  },
43
41
  "dependencies": {
42
+ "lit-html": "^3.3.3",
44
43
  "mutative": "^1.3.0"
45
44
  }
46
45
  }