diff --git a/src/client/parts.ts b/src/client/parts.ts index 8e561a3..7fac99b 100644 --- a/src/client/parts.ts +++ b/src/client/parts.ts @@ -5,6 +5,8 @@ import { is_keyed, is_renderable, single_part_template, + unwrap_html, + unwrap_keyed, type Displayable, type Key, type Renderable, @@ -119,7 +121,7 @@ export function create_child_part( let i = 0 let end = span._start for (const item of value) { - const key = is_keyed(item) ? item._key : (item as Key) + const key = is_keyed(item) ? unwrap_keyed(item) : (item as Key) if (entries.length <= i) { const span = create_span_after(end) entries[i] = { _span: span, _part: create_child_part(span), _key: key } @@ -172,7 +174,7 @@ export function create_child_part( } if (is_html(value)) { - const { _dynamics: dynamics, _statics: statics } = value + const { _statics: statics, _dynamics: dynamics } = unwrap_html(value) const template = compile_template(statics) assert( diff --git a/src/client/root.ts b/src/client/root.ts index ce69b9d..31dd32e 100644 --- a/src/client/root.ts +++ b/src/client/root.ts @@ -5,6 +5,8 @@ import { is_keyed, is_renderable, single_part_template, + unwrap_html, + unwrap_keyed, type Displayable, type Key, type Renderable, @@ -96,7 +98,7 @@ function hydrate_child_part(span: Span, value: unknown) { let end = span._start for (const item of value) { - const key = is_keyed(item) ? item._key : (item as Key) + const key = is_keyed(item) ? unwrap_keyed(item) : (item as Key) const start = end.nextSibling assert(start && is_comment(start) && start.data === '?[') @@ -111,7 +113,8 @@ function hydrate_child_part(span: Span, value: unknown) { } if (is_html(value)) { - template = compile_template(value._statics) + const { _statics: statics, _dynamics: dynamics } = unwrap_html(value) + template = compile_template(statics) const node_by_part: Array = [] @@ -180,7 +183,7 @@ function hydrate_child_part(span: Span, value: unknown) { _start: child.previousSibling, _end: end, }, - value._dynamics[dynamic_index], + dynamics[dynamic_index], ), ] case PART_DIRECTIVE: diff --git a/src/index.ts b/src/index.ts index 6ec1677..0a80cd2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ export { html, keyed, type Displayable, type HTML, type Renderable } from './shared.ts' -import { is_html } from './shared.ts' +import { is_html, unwrap_html } from './shared.ts' if (__DEV__) { type JsonML = string | readonly [tag: string, attrs?: Record, ...children: JsonML[]] @@ -13,11 +13,11 @@ if (__DEV__) { ;((globalThis as { devtoolsFormatters?: Formatter[] }).devtoolsFormatters ??= []).push({ header(value) { if (!is_html(value)) return null + const { _statics: statics, _dynamics: dynamics } = unwrap_html(value) const children: JsonML[] = [] - for (let i = 0; i < value._dynamics.length; i++) - children.push(value._statics[i], ['object', { object: value._dynamics[i] }]) - children.push(value._statics[value._statics.length - 1]) + for (let i = 0; i < dynamics.length; i++) children.push(statics[i], ['object', { object: dynamics[i] }]) + children.push(statics[statics.length - 1]) return ['span', {}, 'html`', ...children, '`'] }, diff --git a/src/server.ts b/src/server.ts index e808e81..fd9dae2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,13 @@ -import { assert, is_html, is_iterable, is_renderable, lexer, single_part_template, type Displayable } from './shared.ts' +import { + assert, + is_html, + is_iterable, + is_renderable, + lexer, + single_part_template, + unwrap_html, + type Displayable, +} from './shared.ts' interface PartRenderer { replace_start: number @@ -153,7 +162,7 @@ function* render_child(value: unknown): Generator { if (is_iterable(value)) { for (const item of value) yield* render_child(item) } else if (is_html(value)) { - const { _statics: statics, _dynamics: dynamics } = value + const { _statics: statics, _dynamics: dynamics } = unwrap_html(value) const template = compile_template(statics) assert( diff --git a/src/shared.ts b/src/shared.ts index 507bd0e..a89120c 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -31,23 +31,27 @@ export function assert(value: unknown, message?: string): asserts value { } } -export interface HTML { - [html_tag]: true - /* @internal */ _statics: TemplateStringsArray - /* @internal */ _dynamics: unknown[] +export let unwrap_html: (value: HTML) => { _statics: TemplateStringsArray; _dynamics: unknown[] } +export let unwrap_keyed: (value: Keyed) => Key + +export class HTML { + #statics: TemplateStringsArray + #dynamics: unknown[] + constructor(statics: TemplateStringsArray, dynamics: unknown[]) { + this.#statics = statics + this.#dynamics = dynamics + } + static { + unwrap_html = value => ({ _statics: value.#statics, _dynamics: value.#dynamics }) + } } -const html_tag: unique symbol = Symbol() export function html(statics: TemplateStringsArray, ...dynamics: unknown[]): HTML { - return { - [html_tag]: true, - _dynamics: dynamics, - _statics: statics, - } + return new HTML(statics, dynamics) } export function is_html(value: unknown): value is HTML { - return typeof value === 'object' && value !== null && html_tag in value + return value instanceof HTML } export function single_part_template(part: Displayable): HTML { @@ -55,20 +59,26 @@ export function single_part_template(part: Displayable): HTML { } export type Key = string | number | bigint | boolean | symbol | object | null -export interface Keyed extends Renderable { - [keyed_tag]: true - /** @internal */ _key: Key + +export class Keyed implements Renderable { + #key: Key + #displayable: Displayable + constructor(displayable: Displayable, key: Key) { + this.#key = key + this.#displayable = displayable + } + render(): Displayable { + return this.#displayable + } + static { + unwrap_keyed = value => value.#key + } } -const keyed_tag: unique symbol = Symbol() export function keyed(displayable: Displayable, key: Key): Keyed { - return { - [keyed_tag]: true, - _key: key, - render: () => displayable, - } + return new Keyed(displayable, key) } -export function is_keyed(value: any): value is Keyed { - return typeof value === 'object' && value !== null && keyed_tag in value +export function is_keyed(value: unknown): value is Keyed { + return value instanceof Keyed }