Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/client/parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
is_keyed,
is_renderable,
single_part_template,
unwrap_html,
unwrap_keyed,
type Displayable,
type Key,
type Renderable,
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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(
Expand Down
9 changes: 6 additions & 3 deletions src/client/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
is_keyed,
is_renderable,
single_part_template,
unwrap_html,
unwrap_keyed,
type Displayable,
type Key,
type Renderable,
Expand Down Expand Up @@ -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 === '?[')
Expand All @@ -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<Node | Span> = []

Expand Down Expand Up @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>, ...children: JsonML[]]
Expand All @@ -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, '`']
},
Expand Down
13 changes: 11 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -153,7 +162,7 @@ function* render_child(value: unknown): Generator<string, void, void> {
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(
Expand Down
54 changes: 32 additions & 22 deletions src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,44 +31,54 @@ 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 {
return html`${part}`
}

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
}