@webjsdev/core 0.7.65 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +5 -0
  2. package/dist/webjs-core-browser.js +6 -5
  3. package/dist/webjs-core-browser.js.map +3 -3
  4. package/dist/webjs-core-testing.js +22 -21
  5. package/dist/webjs-core-testing.js.map +3 -3
  6. package/dist/webjs-core.js +7 -6
  7. package/dist/webjs-core.js.map +3 -3
  8. package/index-browser.js +18 -9
  9. package/index.d.ts +26 -10
  10. package/index.js +21 -9
  11. package/package.json +15 -2
  12. package/src/action-effects-client.d.ts +5 -0
  13. package/src/action-effects-client.js +84 -0
  14. package/src/action-seed-client.js +5 -5
  15. package/src/asset-url.js +12 -13
  16. package/src/async-directive.d.ts +18 -0
  17. package/src/async-directive.js +88 -0
  18. package/src/attribute-directives.d.ts +6 -0
  19. package/src/attribute-directives.js +49 -0
  20. package/src/attribute-reader.js +213 -25
  21. package/src/binding-prefixes.js +10 -53
  22. package/src/component/lifecycle.js +921 -252
  23. package/src/component/reactive.js +7 -14
  24. package/src/component/server-element.js +23 -1
  25. package/src/component.d.ts +160 -44
  26. package/src/context.d.ts +94 -18
  27. package/src/context.js +341 -217
  28. package/src/converters.d.ts +7 -0
  29. package/src/converters.js +38 -0
  30. package/src/css.d.ts +32 -6
  31. package/src/css.js +178 -14
  32. package/src/directive.d.ts +58 -0
  33. package/src/directive.js +119 -0
  34. package/src/directives.d.ts +40 -1
  35. package/src/directives.js +160 -1
  36. package/src/form-action.d.ts +13 -0
  37. package/src/form-action.js +139 -4
  38. package/src/hot.js +23 -0
  39. package/src/html.d.ts +8 -0
  40. package/src/html.js +55 -2
  41. package/src/is-server.d.ts +2 -0
  42. package/src/is-server.js +18 -0
  43. package/src/jsx-guard.js +130 -0
  44. package/src/metadata.d.ts +22 -3
  45. package/src/nav.d.ts +4 -1
  46. package/src/nav.js +44 -11
  47. package/src/optimistic.js +2 -2
  48. package/src/refresh-header.js +52 -0
  49. package/src/registry.js +10 -1
  50. package/src/render-client/adopt-slots.js +250 -0
  51. package/src/render-client/parts.js +790 -42
  52. package/src/render-client/reconciler.js +84 -23
  53. package/src/render-client/template-compiler.js +152 -43
  54. package/src/render-client.d.ts +6 -1
  55. package/src/render-server/attrs.js +2 -2
  56. package/src/render-server/compile.js +50 -2
  57. package/src/render-server/errors.js +37 -0
  58. package/src/render-server/expand.js +100 -20
  59. package/src/render-server/served-attrs.js +78 -0
  60. package/src/render-server/walk.js +177 -5
  61. package/src/render-server.d.ts +3 -0
  62. package/src/render-server.js +2 -0
  63. package/src/repeat.d.ts +7 -2
  64. package/src/repeat.js +20 -6
  65. package/src/router-client/anchors.js +3 -1
  66. package/src/router-client/boundaries.js +15 -1
  67. package/src/router-client/constants.js +30 -0
  68. package/src/router-client/dom-differ.js +17 -0
  69. package/src/router-client/fetch-apply.js +107 -5
  70. package/src/router-client/frames.js +6 -3
  71. package/src/router-client/head-merge.js +58 -10
  72. package/src/router-client/navigator.js +49 -9
  73. package/src/router-client/owned-styles.js +372 -0
  74. package/src/router-client/prefetch.js +6 -4
  75. package/src/router-client/root-attrs.js +43 -0
  76. package/src/router-client/stream.js +7 -0
  77. package/src/router-client/stylesheets.js +191 -0
  78. package/src/router-client/swap.js +53 -10
  79. package/src/routes.d.ts +75 -18
  80. package/src/sentinels.d.ts +2 -0
  81. package/src/sentinels.js +4 -0
  82. package/src/serialize.d.ts +2 -0
  83. package/src/serialize.js +35 -4
  84. package/src/signal.d.ts +7 -0
  85. package/src/signal.js +26 -6
  86. package/src/slot/polyfills.js +3 -3
  87. package/src/slot.js +4 -8
  88. package/src/static-html.d.ts +22 -0
  89. package/src/static-html.js +144 -0
  90. package/src/task.d.ts +63 -20
  91. package/src/task.js +326 -293
  92. package/src/webjs-config.d.ts +2 -9
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Lit's `defaultConverter` and `notEqual` (#1686), exported under the same
3
+ * names so a component written from Lit's docs (a custom converter that
4
+ * delegates to the default, or `hasChanged: (v, old) => notEqual(v, old)`)
5
+ * imports them from `@webjsdev/core` as it would from `lit`.
6
+ *
7
+ * The semantics are Lit's, line for line: a Boolean attribute is on when
8
+ * present, a Number parses with `Number()`, an Object or Array round-trips
9
+ * through JSON (an unparseable value reads `null`, as in Lit), and anything
10
+ * else is the string.
11
+ *
12
+ * @module
13
+ */
14
+
15
+ /** @type {{ toAttribute(value: unknown, type?: unknown): unknown, fromAttribute(value: string | null, type?: unknown): unknown }} */
16
+ export const defaultConverter = {
17
+ toAttribute(value, type) {
18
+ if (type === Boolean) return value ? '' : null;
19
+ if (type === Object || type === Array) return value == null ? value : JSON.stringify(value);
20
+ return value;
21
+ },
22
+ fromAttribute(value, type) {
23
+ if (type === Boolean) return value !== null;
24
+ if (type === Number) return value === null ? null : Number(value);
25
+ if (type === Object || type === Array) {
26
+ try { return JSON.parse(/** @type {string} */ (value)); } catch { return null; }
27
+ }
28
+ return value;
29
+ },
30
+ };
31
+
32
+ /**
33
+ * Lit's default change check: true when `value` differs from `old`, by
34
+ * `Object.is` (so `NaN` equals `NaN`).
35
+ * @param {unknown} value @param {unknown} old
36
+ * @returns {boolean}
37
+ */
38
+ export const notEqual = (value, old) => !Object.is(value, old);
package/src/css.d.ts CHANGED
@@ -1,9 +1,35 @@
1
- export interface CSSResult {
2
- _$webjsCss: true;
3
- text: string;
1
+ /** The value `css` and `unsafeCSS` return (Lit's `CSSResult`). */
2
+ export declare class CSSResult {
3
+ private constructor(cssText: string);
4
+ readonly _$webjsCss: true;
5
+ /** The style text. */
6
+ readonly cssText: string;
7
+ /** The style text, an alias of `cssText`. */
8
+ readonly text: string;
9
+ /** A shared constructed stylesheet in the browser; undefined on the server. */
10
+ readonly styleSheet: CSSStyleSheet | undefined;
11
+ toString(): string;
4
12
  }
5
13
 
6
- export function css(strings: TemplateStringsArray | string[], ...values: unknown[]): CSSResult;
14
+ /** A `css` result or a constructed stylesheet (Lit's `CSSResultOrNative`). */
15
+ export type CSSResultOrNative = CSSResult | CSSStyleSheet;
16
+
17
+ /** Arbitrarily nested arrays of styles (Lit's `CSSResultArray`). */
18
+ export interface CSSResultArray extends Array<CSSResultOrNative | CSSResultArray> {}
19
+
20
+ /** What `static styles` accepts (Lit's `CSSResultGroup`). */
21
+ export type CSSResultGroup = CSSResultOrNative | CSSResultArray;
22
+
23
+ /**
24
+ * Tagged template producing a `CSSResult`. A hole may hold another `css`
25
+ * result or a number; anything else throws (wrap a trusted string in
26
+ * `unsafeCSS()`).
27
+ */
28
+ export function css(strings: TemplateStringsArray | string[], ...values: (CSSResult | number)[]): CSSResult;
29
+ /** Wrap a trusted string as a `CSSResult`. Never pass user input. */
30
+ export function unsafeCSS(value: unknown): CSSResult;
7
31
  export function isCSS(x: unknown): x is CSSResult;
8
- export function adoptStyles(root: ShadowRoot | Document, styles: CSSResult[]): void;
9
- export function stylesToString(styles: CSSResult[]): string;
32
+ /** Flatten `static styles` into the list both renderers apply (Lit's `finalizeStyles`). */
33
+ export function finalizeStyles(styles?: CSSResultGroup | null): CSSResultOrNative[];
34
+ export function adoptStyles(root: ShadowRoot | Document, styles: CSSResultOrNative[]): void;
35
+ export function stylesToString(styles: CSSResultOrNative[]): string;
package/src/css.js CHANGED
@@ -1,7 +1,88 @@
1
1
  /**
2
- * Tagged template literal producing a {@link CSSResult}.
2
+ * The `css` tag and the style plumbing both renderers share.
3
3
  *
4
- * @typedef {{ _$webjsCss: true, text: string }} CSSResult
4
+ * Portions derived from Lit (`@lit/reactive-element` css-tag.ts and
5
+ * reactive-element.ts `finalizeStyles`), Copyright Google LLC,
6
+ * BSD-3-Clause.
7
+ */
8
+
9
+ /**
10
+ * The value `css` and `unsafeCSS` return, Lit's `CSSResult`. `cssText` is the
11
+ * style text; `styleSheet` is a constructed `CSSStyleSheet` built on first
12
+ * read in the browser (undefined on the server), cached per result so every
13
+ * element adopting it shares one sheet.
14
+ *
15
+ * The `_$webjsCss` brand, not `instanceof`, is what identifies a result:
16
+ * `@webjsdev/core` resolves to a bundle while a subpath resolves to source, so
17
+ * two copies of this class can meet in one page.
18
+ */
19
+
20
+ /** Held by `css` and `unsafeCSS` only, Lit's `constructionToken`. */
21
+ const constructionToken = Symbol();
22
+
23
+ /**
24
+ * The shared sheet per hole-free `css` template, keyed by its strings array.
25
+ * @type {WeakMap<readonly string[], CSSStyleSheet>}
26
+ */
27
+ const sheetCache = new WeakMap();
28
+
29
+ export class CSSResult {
30
+ /**
31
+ * Not constructable from outside this module, as in Lit: `css` and
32
+ * `unsafeCSS` are the only ways to make one, so arbitrary text cannot pass
33
+ * for a checked `css` result without going through `unsafeCSS` by name.
34
+ * @param {string} cssText
35
+ * @param {readonly string[] | undefined} strings the `css` template, keying the shared sheet
36
+ * @param {symbol} safeToken
37
+ */
38
+ constructor(cssText, strings, safeToken) {
39
+ if (safeToken !== constructionToken) {
40
+ throw new Error('[webjs] CSSResult is not constructable. Use `css` or `unsafeCSS` instead.');
41
+ }
42
+ /** @type {true} */
43
+ this._$webjsCss = true;
44
+ /** @type {string} */
45
+ this.cssText = cssText;
46
+ /** @type {CSSStyleSheet | undefined} */
47
+ this._styleSheet = undefined;
48
+ this._strings = strings;
49
+ }
50
+
51
+ /** The style text, also readable as `cssText` (Lit's name). */
52
+ get text() {
53
+ return this.cssText;
54
+ }
55
+
56
+ /**
57
+ * A constructed stylesheet for `adoptedStyleSheets`, or undefined where
58
+ * there is none (the server). A `css` template with no holes always yields
59
+ * the same text, so every result from it shares one sheet, as in Lit.
60
+ */
61
+ get styleSheet() {
62
+ if (this._styleSheet === undefined && typeof CSSStyleSheet !== 'undefined') {
63
+ const strings = this._strings;
64
+ const cacheable = strings !== undefined && strings.length === 1;
65
+ let sheet = cacheable ? sheetCache.get(strings) : undefined;
66
+ if (sheet === undefined) {
67
+ sheet = new CSSStyleSheet();
68
+ sheet.replaceSync(this.cssText);
69
+ if (cacheable) sheetCache.set(strings, sheet);
70
+ }
71
+ this._styleSheet = sheet;
72
+ }
73
+ return this._styleSheet;
74
+ }
75
+
76
+ toString() {
77
+ return this.cssText;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Tagged template literal producing a {@link CSSResult}, Lit's `css`. A hole
83
+ * may hold another `css` result (composed in as text) or a number. Anything
84
+ * else throws: a string in a hole is how untrusted text reaches a stylesheet,
85
+ * so it has to go through `unsafeCSS()` on purpose, as in Lit.
5
86
  *
6
87
  * @param {TemplateStringsArray | string[]} strings
7
88
  * @param {...unknown} values
@@ -10,9 +91,39 @@
10
91
  export function css(strings, ...values) {
11
92
  let text = strings[0];
12
93
  for (let i = 1; i < strings.length; i++) {
13
- text += String(values[i - 1] ?? '') + strings[i];
94
+ text += cssValue(values[i - 1]) + strings[i];
14
95
  }
15
- return { _$webjsCss: true, text };
96
+ return new CSSResult(text, strings, constructionToken);
97
+ }
98
+
99
+ /** @param {unknown} value */
100
+ function cssValue(value) {
101
+ if (isCSS(value)) return /** @type {any} */ (value).cssText ?? /** @type {any} */ (value).text;
102
+ if (typeof value === 'number') return String(value);
103
+ throw new Error(
104
+ `[webjs] Value passed to 'css' must be a 'css' result or a number, got ${describe(value)}. ` +
105
+ `Compose another css\`...\` result, or wrap a trusted string in unsafeCSS() from '@webjsdev/core'. ` +
106
+ `Never pass user input through unsafeCSS().`,
107
+ );
108
+ }
109
+
110
+ /** @param {unknown} value */
111
+ function describe(value) {
112
+ if (value === null) return 'null';
113
+ if (typeof value === 'string') return JSON.stringify(value.length > 40 ? value.slice(0, 40) + '...' : value);
114
+ return typeof value;
115
+ }
116
+
117
+ /**
118
+ * Wrap a string (or any value, stringified) as a {@link CSSResult}, Lit's
119
+ * `unsafeCSS`. The text is used as is, so only pass trusted values: CSS from
120
+ * user input can exfiltrate data through `url()` and attribute selectors.
121
+ *
122
+ * @param {unknown} value
123
+ * @returns {CSSResult}
124
+ */
125
+ export function unsafeCSS(value) {
126
+ return new CSSResult(typeof value === 'string' ? value : String(value), undefined, constructionToken);
16
127
  }
17
128
 
18
129
  /** @param {unknown} x @returns {x is CSSResult} */
@@ -21,34 +132,87 @@ export function isCSS(x) {
21
132
  }
22
133
 
23
134
  /**
24
- * Apply an array of CSSResults to a shadow root (client-side only).
25
- * Uses adoptedStyleSheets when available, falls back to <style>.
135
+ * Lit's `finalizeStyles`: turn a class's `static styles` (a result, a sheet,
136
+ * or arbitrarily nested arrays of them) into the flat list both renderers
137
+ * apply. A style included twice keeps its LAST position, so a later override
138
+ * still wins the cascade, as in Lit.
139
+ *
140
+ * @param {unknown} styles
141
+ * @returns {(CSSResult | CSSStyleSheet)[]}
142
+ */
143
+ export function finalizeStyles(styles) {
144
+ /** @type {(CSSResult | CSSStyleSheet)[]} */
145
+ const out = [];
146
+ if (Array.isArray(styles)) {
147
+ const unique = new Set(styles.flat(Infinity).reverse());
148
+ for (const s of unique) if (isStyle(s)) out.unshift(/** @type {any} */ (s));
149
+ } else if (isStyle(styles)) {
150
+ out.push(/** @type {any} */ (styles));
151
+ }
152
+ return out;
153
+ }
154
+
155
+ /** @param {unknown} s */
156
+ function isStyle(s) {
157
+ return isCSS(s) || (typeof CSSStyleSheet !== 'undefined' && s instanceof CSSStyleSheet);
158
+ }
159
+
160
+ /**
161
+ * Apply styles to a shadow root (client-side only). Uses adoptedStyleSheets
162
+ * when available, falls back to a <style> element.
26
163
  *
27
164
  * @param {ShadowRoot | Document} root
28
- * @param {CSSResult[]} styles
165
+ * @param {(CSSResult | CSSStyleSheet)[]} styles
29
166
  */
30
167
  export function adoptStyles(root, styles) {
31
168
  if (!styles || !styles.length) return;
32
169
  if ('adoptedStyleSheets' in root && typeof CSSStyleSheet !== 'undefined') {
33
- const sheets = styles.map((s) => {
170
+ /** @type {any} */ (root).adoptedStyleSheets = styles.map((s) => {
171
+ if (s instanceof CSSStyleSheet) return s;
172
+ const shared = /** @type {any} */ (s).styleSheet;
173
+ if (shared) return shared;
34
174
  const sheet = new CSSStyleSheet();
35
- sheet.replaceSync(s.text);
175
+ sheet.replaceSync(styleText(s));
36
176
  return sheet;
37
177
  });
38
- /** @type {any} */ (root).adoptedStyleSheets = sheets;
39
178
  } else {
40
179
  const host = /** @type {any} */ (root);
41
180
  const el = document.createElement('style');
42
- el.textContent = styles.map((s) => s.text).join('\n');
181
+ el.textContent = styles.map(styleText).join('\n');
43
182
  host.appendChild(el);
44
183
  }
45
184
  }
46
185
 
186
+ /** @param {any} s @returns {string} */
187
+ function styleText(s) {
188
+ if (isCSS(s)) return s.cssText ?? s.text;
189
+ if (s && s.cssRules) return Array.from(s.cssRules, (r) => /** @type {any} */ (r).cssText).join('\n');
190
+ return '';
191
+ }
192
+
47
193
  /**
48
- * Serialise styles to a <style> tag string (server-side).
49
- * @param {CSSResult[]} styles
194
+ * Serialise styles to a <style> tag string (server-side). A constructed
195
+ * `CSSStyleSheet` has no server equivalent, so only `css` results render.
196
+ * @param {(CSSResult | CSSStyleSheet)[]} styles
50
197
  */
51
198
  export function stylesToString(styles) {
52
199
  if (!styles || !styles.length) return '';
53
- return `<style>${styles.map((s) => s.text).join('\n')}</style>`;
200
+ const text = styles.filter(isCSS).map(styleText);
201
+ return text.length ? `<style>${text.join('\n')}</style>` : '';
202
+ }
203
+
204
+ /**
205
+ * The error a light-DOM component with `static styles` gets: the styles are
206
+ * adopted into a shadow root, and the component has none. One string, used by
207
+ * the server renderer (logged during SSR) and by the browser element, so the
208
+ * two cannot drift. Internal: not re-exported from the package.
209
+ * @param {string} tag
210
+ * @returns {string}
211
+ */
212
+ export function lightDomStylesMessage(tag) {
213
+ return (
214
+ `[webjs] <${tag}> declares static styles but renders into light DOM, so the styles are never applied. ` +
215
+ `Add \`static shadow = true\` to scope them in a shadow root, or style the light-DOM markup with Tailwind ` +
216
+ `utilities or a global stylesheet (prefix custom class selectors with the tag name).`
217
+ );
54
218
  }
@@ -0,0 +1,58 @@
1
+ /** lit's `PartType`: the kind of part a directive is bound to. */
2
+ export const PartType: {
3
+ readonly ATTRIBUTE: 1;
4
+ readonly CHILD: 2;
5
+ readonly PROPERTY: 3;
6
+ readonly BOOLEAN_ATTRIBUTE: 4;
7
+ readonly EVENT: 5;
8
+ readonly ELEMENT: 6;
9
+ };
10
+ export type PartType = (typeof PartType)[keyof typeof PartType];
11
+
12
+ /** lit's `PartInfo`: where a directive instance is used. */
13
+ export interface PartInfo {
14
+ readonly type: PartType;
15
+ readonly name?: string;
16
+ readonly tagName?: string;
17
+ readonly strings?: readonly string[];
18
+ }
19
+
20
+ /** The part handed to `update()` on the client (a lit-shaped facade). */
21
+ export interface Part {
22
+ readonly type: PartType;
23
+ readonly options: { readonly host: unknown };
24
+ /** Element, attribute, property, boolean and event parts. */
25
+ readonly element?: Element;
26
+ readonly name?: string;
27
+ readonly strings?: readonly string[];
28
+ /** Child parts. */
29
+ readonly parentNode?: Node | null;
30
+ readonly startNode?: Node | null;
31
+ readonly endNode?: Node | null;
32
+ }
33
+
34
+ /** lit's `Directive`: subclass it, implement `render()`, optionally `update()`. */
35
+ export abstract class Directive {
36
+ constructor(partInfo: PartInfo);
37
+ abstract render(...props: unknown[]): unknown;
38
+ update(part: Part, props: unknown[]): unknown;
39
+ }
40
+
41
+ /** The value a `directive()` function returns; rendered by both renderers. */
42
+ export interface DirectiveResult<C extends abstract new (...args: any[]) => Directive = abstract new (...args: any[]) => Directive> {
43
+ readonly _$webjs: 'directive';
44
+ readonly ctor: C;
45
+ readonly values: unknown[];
46
+ }
47
+
48
+ /** The arguments of a directive class's `render()`. */
49
+ export type DirectiveParameters<C extends Directive> = Parameters<C['render']>;
50
+
51
+ /** lit's `directive()`: the template-facing function for a directive class. */
52
+ export function directive<C extends new (partInfo: PartInfo) => Directive>(
53
+ c: C,
54
+ ): (...values: DirectiveParameters<InstanceType<C>>) => DirectiveResult<C>;
55
+
56
+ export function isDirectiveResult(x: unknown): x is DirectiveResult;
57
+ /** lit's `getDirectiveClass`. */
58
+ export function getDirectiveClass(x: unknown): (abstract new (...args: any[]) => Directive) | undefined;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Custom directive authoring, lit's `lit/directive.js` under a WebJs path:
3
+ * `@webjsdev/core/directive`.
4
+ *
5
+ * Portions derived from Lit, Copyright Google LLC, BSD-3-Clause.
6
+ *
7
+ * ```js
8
+ * import { directive, Directive, PartType } from '@webjsdev/core/directive';
9
+ *
10
+ * class Uppercase extends Directive {
11
+ * constructor(partInfo) {
12
+ * super(partInfo);
13
+ * if (partInfo.type !== PartType.CHILD) throw new Error('uppercase() is a child directive');
14
+ * }
15
+ * render(text) { return String(text).toUpperCase(); }
16
+ * }
17
+ * export const uppercase = directive(Uppercase);
18
+ *
19
+ * html`<p>${uppercase(name)}</p>`;
20
+ * ```
21
+ *
22
+ * The contract is lit's. One directive INSTANCE lives per template part (per
23
+ * piece of an interpolated attribute) for as long as the same directive class
24
+ * renders there; a different class (or a plain value) replaces it. On the
25
+ * client every render calls `update(part, values)`, whose default forwards to
26
+ * `render(...values)`; on the server only `render(...values)` runs, because
27
+ * there is no DOM to hand `update`. Whatever either returns is rendered at the
28
+ * part like any other value (`noChange` keeps the last commit, `nothing`
29
+ * clears it), so a directive may return a template or another directive.
30
+ *
31
+ * The result is a plain object branded `_$webjs: 'directive'`. Both renderers
32
+ * recognise it by that brand, never by `instanceof`, so a directive built
33
+ * against one loaded copy of this module renders through another (this
34
+ * subpath is source while the root import may be the built bundle).
35
+ *
36
+ * @module directive
37
+ */
38
+
39
+ /**
40
+ * The kind of template part a directive instance is bound to, lit's values.
41
+ * @type {Readonly<{ ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 }>}
42
+ */
43
+ export const PartType = Object.freeze({
44
+ ATTRIBUTE: 1,
45
+ CHILD: 2,
46
+ PROPERTY: 3,
47
+ BOOLEAN_ATTRIBUTE: 4,
48
+ EVENT: 5,
49
+ ELEMENT: 6,
50
+ });
51
+
52
+ /**
53
+ * Base class for a custom directive. Subclass it, implement `render()`, and
54
+ * optionally override `update()` for DOM access on the client.
55
+ */
56
+ export class Directive {
57
+ /**
58
+ * @param {{ type: number, name?: string, tagName?: string, strings?: readonly string[] }} _partInfo
59
+ * Where the directive is used: `type` is a {@link PartType}; an attribute
60
+ * part also carries `name`, `tagName` and, when the attribute is
61
+ * interpolated, its static `strings`.
62
+ */
63
+ constructor(_partInfo) {}
64
+
65
+ /**
66
+ * Produce the value to render. Called on the server, and on the client by
67
+ * the default `update()`.
68
+ * @param {...unknown} _values the arguments the directive function was called with
69
+ * @returns {unknown}
70
+ */
71
+ render(..._values) {
72
+ return undefined;
73
+ }
74
+
75
+ /**
76
+ * Called on the client on every render with the part (a lit-shaped facade:
77
+ * `type`, `options.host`, and `element` / `name` / `strings` for an element
78
+ * or attribute part, `parentNode` / `startNode` / `endNode` for a child
79
+ * part) and the arguments as an array. The default calls `render()`.
80
+ * @param {unknown} _part
81
+ * @param {unknown[]} values
82
+ * @returns {unknown}
83
+ */
84
+ update(_part, values) {
85
+ return this.render(...values);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Turn a {@link Directive} subclass into the function templates call, lit's
91
+ * `directive()`. Each call returns a marker carrying the class and its
92
+ * arguments; the renderer owns the instance.
93
+ *
94
+ * @template {new (partInfo: any) => Directive} C
95
+ * @param {C} c
96
+ * @returns {(...values: unknown[]) => { _$webjs: 'directive', ctor: C, values: unknown[] }}
97
+ */
98
+ export function directive(c) {
99
+ return (...values) => ({ _$webjs: 'directive', ctor: c, values });
100
+ }
101
+
102
+ /**
103
+ * Whether `x` is the result of calling a `directive()` function.
104
+ * @param {unknown} x
105
+ * @returns {x is { _$webjs: 'directive', ctor: any, values: unknown[] }}
106
+ */
107
+ export function isDirectiveResult(x) {
108
+ return !!x && typeof x === 'object' && /** @type {any} */ (x)._$webjs === 'directive';
109
+ }
110
+
111
+ /**
112
+ * The directive class a directive result was made from, lit's
113
+ * `getDirectiveClass`.
114
+ * @param {unknown} x
115
+ * @returns {Function | undefined}
116
+ */
117
+ export function getDirectiveClass(x) {
118
+ return isDirectiveResult(x) ? x.ctor : undefined;
119
+ }
@@ -1,8 +1,12 @@
1
+ import type { nothing, noChange } from './sentinels.js';
1
2
  export { repeat, isRepeat } from './repeat.js';
3
+ export type { RepeatDirective } from './repeat.js';
2
4
 
3
5
  export interface UnsafeHTMLDirective {
4
6
  _$webjs: 'unsafe-html';
5
7
  value: string;
8
+ /** Set by `unsafeSVG` / `unsafeMathML`: the namespace the client parses the markup in. */
9
+ ns?: 'svg' | 'mathml';
6
10
  }
7
11
 
8
12
  export interface LiveDirective<T = unknown> {
@@ -63,8 +67,12 @@ export interface WatchDirective<T = unknown> {
63
67
  signal: { get(): T; __isSignal: true };
64
68
  }
65
69
 
66
- export function unsafeHTML(htmlString: string | null | undefined): UnsafeHTMLDirective;
70
+ export function unsafeHTML(htmlString: string | null | undefined | typeof nothing | typeof noChange): UnsafeHTMLDirective;
67
71
  export function isUnsafeHTML(x: unknown): x is UnsafeHTMLDirective;
72
+ /** lit's `unsafeSVG`: trusted SVG markup, parsed in the SVG namespace on the client. */
73
+ export function unsafeSVG(svgString: string | null | undefined | typeof nothing | typeof noChange): UnsafeHTMLDirective;
74
+ /** lit's `unsafeMathML`: trusted MathML markup, parsed in the MathML namespace on the client. */
75
+ export function unsafeMathML(mathString: string | null | undefined | typeof nothing | typeof noChange): UnsafeHTMLDirective;
68
76
  export function live<T>(value: T): LiveDirective<T>;
69
77
  export function isLive(x: unknown): x is LiveDirective;
70
78
  export function keyed<T>(key: unknown, template: T): KeyedDirective<T>;
@@ -96,3 +104,34 @@ export function asyncReplace<T>(
96
104
  export function isAsyncReplace(x: unknown): x is AsyncReplaceDirective;
97
105
  export function watch<T>(sig: { get(): T; __isSignal: true }): WatchDirective<T>;
98
106
  export function isWatch(x: unknown): x is WatchDirective;
107
+
108
+ type Falsy = null | undefined | false | 0 | -0 | 0n | '';
109
+
110
+ /** lit's `when`: the case functions receive the condition value. */
111
+ export function when<C, T, F = undefined>(
112
+ condition: C,
113
+ trueCase: (c: Exclude<C, Falsy>) => T,
114
+ falseCase?: (c: Extract<C, Falsy>) => F,
115
+ ): C extends Falsy ? F : T;
116
+ /** lit's `choose`: the case whose key is `===` to `value`, else the default. */
117
+ export function choose<T, V, K extends T = T>(
118
+ value: T,
119
+ cases: Array<[K, () => V]>,
120
+ defaultCase?: () => V,
121
+ ): V | undefined;
122
+ /** lit's `map`: a lazy generator over `items`, renderable anywhere an array is. */
123
+ export function map<T, U>(
124
+ items: Iterable<T> | undefined,
125
+ f: (value: T, index: number) => U,
126
+ ): Generator<U, void, unknown>;
127
+ /** lit's `join`: `items` with `joiner` (or `joiner(index)`) between each pair. */
128
+ export function join<I, J>(
129
+ items: Iterable<I> | undefined,
130
+ joiner: J | ((index: number) => J),
131
+ ): Generator<I | J, void, unknown>;
132
+ /** lit's `range`: `range(end)`, `range(start, end)` or `range(start, end, step)`. */
133
+ export function range(end: number): Generator<number, void, unknown>;
134
+ export function range(start: number, end: number, step?: number): Generator<number, void, unknown>;
135
+
136
+ export { classMap, styleMap, ifDefined } from './attribute-directives.js';
137
+ export type { ClassMapDirective, StyleMapDirective } from './attribute-directives.js';