@abide/abide 0.111.0 → 0.113.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.
- package/CHANGELOG.md +24 -0
- package/docs/REGISTRY.md +14 -14
- package/docs/RULEBOOK.md +35 -6
- package/docs/abide.md +54 -48
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +5 -0
- package/src/compiler/appLayout.ts +16 -1
- package/src/compiler/classifyTemplate.ts +134 -39
- package/src/compiler/emitClient.ts +173 -16
- package/src/compiler/renderMarkup.ts +14 -8
- package/src/server/commands.ts +2 -1
- package/src/server/config.ts +13 -1
- package/src/server/createApp.ts +18 -6
- package/src/server/preload.ts +5 -0
- package/src/server/principal.ts +14 -0
- package/src/server/render.ts +2 -2
- package/src/shared/ASCII_WHITESPACE.ts +6 -0
- package/src/shared/FOREIGN_ATTRIBUTES.ts +24 -0
- package/src/shared/INTEGRATION_POINTS.ts +20 -0
- package/src/shared/IN_PROCESS_ORIGIN.ts +5 -0
- package/src/shared/reactive/Keyed.ts +41 -14
- package/src/shared/reactive/Reactive.ts +34 -2
- package/src/shared/rpc.ts +2 -1
- package/src/ui/Level.ts +43 -95
- package/src/ui/Router.ts +12 -13
- package/src/ui/bindGroup.ts +41 -0
- package/src/ui/bindProperty.ts +1 -21
- package/src/ui/bindSpread.ts +4 -2
- package/src/ui/index.ts +7 -5
- package/src/ui/parseMarkup.ts +20 -25
- package/src/ui/reconcileHead.ts +215 -0
- package/src/ui/setAttribute.ts +25 -1
- package/src/ui/setClassGroup.ts +79 -0
- package/src/ui/setStyleGroup.ts +130 -0
- package/src/ui/applyHead.ts +0 -214
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { ASCII_WHITESPACE } from '#shared/ASCII_WHITESPACE.ts'
|
|
1
2
|
import { escapeAttribute, escapeQuote } from '#shared/escapeText.ts'
|
|
2
3
|
import { isEventName } from '#shared/isEventName.ts'
|
|
3
4
|
import { safeUrl } from '#shared/safeUrl.ts'
|
|
4
5
|
import { URL_ATTRIBUTES } from '#shared/URL_ATTRIBUTES.ts'
|
|
6
|
+
import { decodeCharacterReferences } from './decodeCharacterReferences.ts'
|
|
5
7
|
import {
|
|
6
8
|
type Attribute,
|
|
7
9
|
bodiesOf,
|
|
@@ -90,8 +92,11 @@ export type ClassifiedAttribute = {
|
|
|
90
92
|
kind: 'bound'
|
|
91
93
|
bind: ValueBind
|
|
92
94
|
reflects: 'value' | 'checked' | 'open' | undefined
|
|
93
|
-
/**
|
|
94
|
-
|
|
95
|
+
/**
|
|
96
|
+
* 32.8 — a group member's `value` attribute, which the group is compared against —
|
|
97
|
+
* `undefined` where it has none, and the input's value is then `on`.
|
|
98
|
+
*/
|
|
99
|
+
own: Attribute | undefined
|
|
95
100
|
}
|
|
96
101
|
// 32.9 — the node itself, which a server has none of: written out it was `bind:element=""`,
|
|
97
102
|
// inert and a tell. The client does not set it either; what the value takes is the element.
|
|
@@ -102,24 +107,39 @@ export type ClassifiedAttribute = {
|
|
|
102
107
|
// 32.10, 32.19 — a member of a `class` or `style` merge group (`MergeGroup`). `group` is set on
|
|
103
108
|
// the FIRST member only, where the whole group is written, which keeps the tag in the author's
|
|
104
109
|
// attribute order. `toggles` is the one class or style property a directive's binding writes,
|
|
105
|
-
//
|
|
110
|
+
// or a shared group's whole value at its first member (32.71), named by the directives'
|
|
111
|
+
// classes or properties, and `undefined` for any other member — see where that is decided.
|
|
106
112
|
| {
|
|
107
113
|
kind: 'merged'
|
|
108
114
|
group: MergeGroup | undefined
|
|
109
|
-
toggles:
|
|
115
|
+
toggles:
|
|
116
|
+
| {
|
|
117
|
+
write: 'class' | 'style' | 'classGroup' | 'styleGroup'
|
|
118
|
+
name: string
|
|
119
|
+
}
|
|
120
|
+
| undefined
|
|
110
121
|
}
|
|
111
122
|
)
|
|
112
123
|
|
|
113
124
|
/**
|
|
114
125
|
* Every attribute of an element, classified, in the order the tag carries them.
|
|
115
126
|
*
|
|
116
|
-
* `lowering` answers `live` (`Lowering.isLive`)
|
|
127
|
+
* `lowering` answers `live` (`Lowering.isLive`), and whether a shared `class` group can move
|
|
128
|
+
* (`Lowering.readsAnItem`: a row's item read at its top level is not live, and a row's `update`
|
|
129
|
+
* writes it).
|
|
117
130
|
*/
|
|
118
131
|
export function classifyAttributes(
|
|
119
132
|
node: ElementNode,
|
|
120
|
-
lowering: {
|
|
133
|
+
lowering: {
|
|
134
|
+
isLive(holder: object): boolean
|
|
135
|
+
readsAnItem(holder: object): boolean
|
|
136
|
+
},
|
|
121
137
|
): ClassifiedAttribute[] {
|
|
138
|
+
refuseTwoOwners(node)
|
|
122
139
|
const groups = mergeGroupsOf(node)
|
|
140
|
+
const sharing: ReturnType<typeof sharingOf>[] = []
|
|
141
|
+
for (const group of groups) sharing.push(sharingOf(group, lowering))
|
|
142
|
+
const held = heldClasses(groups)
|
|
123
143
|
const written = new Set<MergeGroup>()
|
|
124
144
|
const classified: ClassifiedAttribute[] = []
|
|
125
145
|
for (const attribute of node.attributes) {
|
|
@@ -130,33 +150,37 @@ export function classifyAttributes(
|
|
|
130
150
|
if (group !== undefined) {
|
|
131
151
|
const leads = !written.has(group)
|
|
132
152
|
written.add(group)
|
|
133
|
-
// 32.
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
// they reran.
|
|
153
|
+
// 32.71 — A SHARED GROUP IS WRITTEN WHOLE AT ITS FIRST MEMBER, through `classList` or
|
|
154
|
+
// the CSSOM, as the server merged it, and moves where any member does — so a directive
|
|
155
|
+
// keeps what the plain attribute still holds without either knowing of the other.
|
|
137
156
|
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
157
|
+
// 32.10 — A STATIC PLAIN ATTRIBUTE IS LEFT UNBOUND, and so is a directive naming one of
|
|
158
|
+
// a static `class`'s classes, which is always on. The static value is never written
|
|
159
|
+
// again, and the directives write through `classList` or the CSSOM beside it.
|
|
160
|
+
const shared = sharing[groups.indexOf(group)]
|
|
161
|
+
if (shared !== undefined) {
|
|
162
|
+
classified.push({
|
|
163
|
+
attribute,
|
|
164
|
+
live: leads ? shared.live : live,
|
|
165
|
+
kind: 'merged',
|
|
166
|
+
group: leads ? group : undefined,
|
|
167
|
+
toggles: leads
|
|
168
|
+
? { write: `${group.name}Group`, name: shared.names }
|
|
169
|
+
: undefined,
|
|
170
|
+
})
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
const name = attribute.name.slice(group.name.length + 1)
|
|
148
174
|
classified.push({
|
|
149
175
|
attribute,
|
|
150
176
|
live,
|
|
151
177
|
kind: 'merged',
|
|
152
178
|
group: leads ? group : undefined,
|
|
153
179
|
toggles:
|
|
154
|
-
attribute === group.plain
|
|
180
|
+
attribute === group.plain ||
|
|
181
|
+
(group.name === 'class' && held.includes(name))
|
|
155
182
|
? undefined
|
|
156
|
-
: {
|
|
157
|
-
write: group.name,
|
|
158
|
-
name: attribute.name.slice(group.name.length + 1),
|
|
159
|
-
},
|
|
183
|
+
: { write: group.name, name },
|
|
160
184
|
})
|
|
161
185
|
continue
|
|
162
186
|
}
|
|
@@ -191,7 +215,7 @@ export function classifyAttributes(
|
|
|
191
215
|
: bind === 'group'
|
|
192
216
|
? 'checked'
|
|
193
217
|
: bind,
|
|
194
|
-
own:
|
|
218
|
+
own: attributeOf(node, 'value'),
|
|
195
219
|
})
|
|
196
220
|
else if (isElementBind(attribute.lowerName))
|
|
197
221
|
classified.push({ attribute, live, kind: 'element' })
|
|
@@ -294,6 +318,85 @@ function mergeGroupsOf(node: ElementNode): MergeGroup[] {
|
|
|
294
318
|
return groups
|
|
295
319
|
}
|
|
296
320
|
|
|
321
|
+
/**
|
|
322
|
+
* 32.71 — for a group whose plain attribute is an expression and where some member can move, so
|
|
323
|
+
* neither half can be written without the other: whether a member is `live`, and the directives'
|
|
324
|
+
* classes or properties, which the group owns before its first write. `undefined` for any other.
|
|
325
|
+
*/
|
|
326
|
+
function sharingOf(
|
|
327
|
+
group: MergeGroup,
|
|
328
|
+
lowering: Parameters<typeof classifyAttributes>[1],
|
|
329
|
+
): { live: boolean; names: string } | undefined {
|
|
330
|
+
if (!group.plain?.expression) return undefined
|
|
331
|
+
let live = false
|
|
332
|
+
let moves = false
|
|
333
|
+
for (const member of group.members) {
|
|
334
|
+
if (!member.expression) continue
|
|
335
|
+
if (lowering.isLive(member)) live = moves = true
|
|
336
|
+
else if (lowering.readsAnItem(member)) moves = true
|
|
337
|
+
}
|
|
338
|
+
if (!moves) return undefined
|
|
339
|
+
const names: string[] = []
|
|
340
|
+
for (const directive of group.directives)
|
|
341
|
+
names.push(directive.name.slice(group.name.length + 1))
|
|
342
|
+
return { live, names: names.join(' ') }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** 32.71 — the classes a static `class` beside a `class:name` holds, each always on. */
|
|
346
|
+
function heldClasses(groups: MergeGroup[]): string[] {
|
|
347
|
+
for (const group of groups)
|
|
348
|
+
if (group.name === 'class' && group.plain && !group.plain.expression)
|
|
349
|
+
return decodeCharacterReferences(
|
|
350
|
+
group.plain.value,
|
|
351
|
+
'attribute',
|
|
352
|
+
).split(ASCII_WHITESPACE)
|
|
353
|
+
return []
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* 32.72 — WHAT A `bind:` WRITES IS THE BIND'S ALONE: `checked` beside `bind:checked` or
|
|
358
|
+
* `bind:group`, `value` beside `bind:value`, any `bind:name` beside `name` — and an
|
|
359
|
+
* `<option selected>` inside a `<select bind:value>`, and a `<textarea bind:value>`'s text. Two
|
|
360
|
+
* owners of one value render whichever the browser keeps and move with whichever wrote last.
|
|
361
|
+
*/
|
|
362
|
+
function refuseTwoOwners(node: ElementNode): void {
|
|
363
|
+
for (const attribute of node.attributes) {
|
|
364
|
+
const lower = attribute.lowerName
|
|
365
|
+
if (!lower.startsWith('bind:') || lower === 'bind:element') continue
|
|
366
|
+
const owned =
|
|
367
|
+
lower === 'bind:group' ? 'checked' : lower.slice('bind:'.length)
|
|
368
|
+
const beside = attributeOf(node, owned)
|
|
369
|
+
if (beside !== undefined)
|
|
370
|
+
throw new Error(
|
|
371
|
+
`abide: <${node.tag}> has ${attribute.name} and ${beside.name} — one value, two owners. ${attribute.name} writes ${owned} already; drop ${beside.name}. RULEBOOK 32.72.`,
|
|
372
|
+
)
|
|
373
|
+
if (owned !== 'value') continue
|
|
374
|
+
if (node.lowerTag === 'textarea' && node.children.length > 0)
|
|
375
|
+
throw new Error(
|
|
376
|
+
`abide: <${node.tag} ${attribute.name}> has text of its own — one value, two owners. The bind writes the text; drop what stands between the tags. RULEBOOK 32.72.`,
|
|
377
|
+
)
|
|
378
|
+
if (node.lowerTag === 'select' && holdsSelectedOption(node.children))
|
|
379
|
+
throw new Error(
|
|
380
|
+
`abide: <${node.tag} ${attribute.name}> has an <option selected> — one value, two owners. The bind selects the option its value holds; drop selected. RULEBOOK 32.72.`,
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Whether an `<option>` with a `selected` attribute stands anywhere among `nodes`, blocks included. */
|
|
386
|
+
function holdsSelectedOption(nodes: readonly TemplateNode[]): boolean {
|
|
387
|
+
for (const node of nodes) {
|
|
388
|
+
if (
|
|
389
|
+
node.kind === 'element' &&
|
|
390
|
+
node.lowerTag === 'option' &&
|
|
391
|
+
attributeOf(node, 'selected') !== undefined
|
|
392
|
+
)
|
|
393
|
+
return true
|
|
394
|
+
for (const body of bodiesOf(node))
|
|
395
|
+
if (holdsSelectedOption(body.children)) return true
|
|
396
|
+
}
|
|
397
|
+
return false
|
|
398
|
+
}
|
|
399
|
+
|
|
297
400
|
/** An element's attribute by its folded name, or `undefined`. */
|
|
298
401
|
export function attributeOf(
|
|
299
402
|
node: ElementNode,
|
|
@@ -304,14 +407,6 @@ export function attributeOf(
|
|
|
304
407
|
return undefined
|
|
305
408
|
}
|
|
306
409
|
|
|
307
|
-
/** An attribute's static value, or `undefined` where it is absent or an expression. */
|
|
308
|
-
function staticAttribute(node: ElementNode, name: string): string | undefined {
|
|
309
|
-
const attribute = attributeOf(node, name)
|
|
310
|
-
return attribute === undefined || attribute.expression
|
|
311
|
-
? undefined
|
|
312
|
-
: attribute.value
|
|
313
|
-
}
|
|
314
|
-
|
|
315
410
|
/**
|
|
316
411
|
* Whether a tag names a component: it starts with an uppercase ASCII letter.
|
|
317
412
|
*
|
|
@@ -437,11 +532,11 @@ export function isSingleRooted(children: TemplateNode[]): boolean {
|
|
|
437
532
|
)
|
|
438
533
|
}
|
|
439
534
|
|
|
440
|
-
// CONTENT
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
535
|
+
// CONTENT A ROW THE BROWSER BUILDS LEAVES TO THE PARSER: an `<svg>` or `<math>` that opens its
|
|
536
|
+
// namespace inside the row (a build only carries one in, off the parent it goes into — 32.70), a
|
|
537
|
+
// raw-text element's contents, which are not decoded the way body text is, and a `<template>`'s
|
|
538
|
+
// children, which go into its `content`. A row that touches any of it is left to the parser — the
|
|
539
|
+
// general path, as the fallback it is.
|
|
445
540
|
const PARSER_OWNED = new Set([
|
|
446
541
|
'svg',
|
|
447
542
|
'math',
|
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
} from '#shared/ANCHORS.ts'
|
|
8
8
|
import { anchorMarkup } from '#shared/anchorMarkup.ts'
|
|
9
9
|
import { CHILDREN } from '#shared/CHILDREN.ts'
|
|
10
|
+
import { FOREIGN_ATTRIBUTES } from '#shared/FOREIGN_ATTRIBUTES.ts'
|
|
11
|
+
import {
|
|
12
|
+
HTML_NAMESPACE,
|
|
13
|
+
INTEGRATION_POINTS,
|
|
14
|
+
} from '#shared/INTEGRATION_POINTS.ts'
|
|
10
15
|
import { RUNTIME_SPECIFIER, SHARED_SPECIFIER } from '#shared/SPECIFIERS.ts'
|
|
11
16
|
import { type CheckerAnswer, UNCHECKED } from './CheckerAnswer.ts'
|
|
12
17
|
import {
|
|
@@ -21,6 +26,7 @@ import {
|
|
|
21
26
|
classifyAttributes,
|
|
22
27
|
isBound,
|
|
23
28
|
isSingleRooted,
|
|
29
|
+
type MergeGroup,
|
|
24
30
|
isSlot,
|
|
25
31
|
} from './classifyTemplate.ts'
|
|
26
32
|
import { decodeCharacterReferences } from './decodeCharacterReferences.ts'
|
|
@@ -220,9 +226,7 @@ export function emitClient(
|
|
|
220
226
|
const registered = heads
|
|
221
227
|
? `if (${ROUTED}) ${ROUTED}.setHead(() => ${headMarkup})\n`
|
|
222
228
|
: ''
|
|
223
|
-
const applied = heads
|
|
224
|
-
? `if (${ROUTED}) ${lowering.use('applyHead')}(${ROUTED}.headMarkup()); `
|
|
225
|
-
: ''
|
|
229
|
+
const applied = heads ? `if (${ROUTED}) ${ROUTED}.reconcile(); ` : ''
|
|
226
230
|
// Applied with the fill either way, so the watch's first run only subscribes: what it would
|
|
227
231
|
// write is already there, from the server or from the line above.
|
|
228
232
|
if (liveHeads) lines.push(`if (${ROUTED}) ${ROUTED}.followHead(${SCOPE})`)
|
|
@@ -767,6 +771,17 @@ function elementUnits(
|
|
|
767
771
|
)
|
|
768
772
|
// 32.5, 32.7, 32.8 — THE PROPERTY AND THE VALUE, IN STEP BOTH WAYS: a watch on this
|
|
769
773
|
// region's scope writes the property, and a listener writes it back.
|
|
774
|
+
// 32.8 — a group compares against the input's own value, read where the value is.
|
|
775
|
+
else if (one.kind === 'bound' && one.bind === 'group')
|
|
776
|
+
binds.push(
|
|
777
|
+
`${lowering.use('bindGroup')}(${element}, ${lowering.reference(one.attribute)}, () => (${
|
|
778
|
+
one.own === undefined
|
|
779
|
+
? "'on'"
|
|
780
|
+
: one.own.expression
|
|
781
|
+
? lowering.binding(one.own)
|
|
782
|
+
: JSON.stringify(one.own.value)
|
|
783
|
+
}), ${SCOPE})`,
|
|
784
|
+
)
|
|
770
785
|
else if (one.kind === 'bound')
|
|
771
786
|
binds.push(
|
|
772
787
|
`${lowering.use('bindProperty')}(${element}, ${JSON.stringify(one.bind)}, ${lowering.reference(one.attribute)}, ${SCOPE})`,
|
|
@@ -798,16 +813,32 @@ if (__r${slot} !== undefined) ${SCOPE}.stops.push(__r${slot})`,
|
|
|
798
813
|
write: 'attribute' as const,
|
|
799
814
|
name: one.attribute.name,
|
|
800
815
|
}
|
|
816
|
+
// 32.71 — A SHARED GROUP IS ONE WATCH over its whole value, which wakes for any member:
|
|
817
|
+
// a directive cannot keep what the plain attribute holds from a watch of its own.
|
|
818
|
+
const group =
|
|
819
|
+
one.kind === 'merged' && writesGroup(write)
|
|
820
|
+
? one.group
|
|
821
|
+
: undefined
|
|
801
822
|
// 32.67 — one reading a row's item is one of the row's item watches, on a name of its
|
|
802
823
|
// own because it is installed apart from the element's other attributes.
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
824
|
+
const asItem =
|
|
825
|
+
group === undefined
|
|
826
|
+
? lowering.readsOwnItem(one.attribute)
|
|
827
|
+
: groupReadsOwnItem(group, lowering)
|
|
828
|
+
const call = writeCall(
|
|
829
|
+
write,
|
|
830
|
+
asItem ? `__i${slot}` : `__a${slot}`,
|
|
831
|
+
name,
|
|
832
|
+
group === undefined
|
|
833
|
+
? lowering.binding(one.attribute)
|
|
834
|
+
: groupArguments(group, (attribute) =>
|
|
835
|
+
lowering.binding(attribute),
|
|
836
|
+
),
|
|
837
|
+
lowering,
|
|
838
|
+
)
|
|
839
|
+
;(asItem ? itemWrites : writes).push(
|
|
840
|
+
`${SCOPE}.${asItem ? 'itemWatch' : 'watch'}(() => ${call})`,
|
|
841
|
+
)
|
|
811
842
|
}
|
|
812
843
|
}
|
|
813
844
|
if (itemWrites.length > 0)
|
|
@@ -825,14 +856,27 @@ if (__r${slot} !== undefined) ${SCOPE}.stops.push(__r${slot})`,
|
|
|
825
856
|
const SCOPE = '__scope'
|
|
826
857
|
|
|
827
858
|
// Which runtime write each kind of attribute binding is handed to. A record rather than a chain
|
|
828
|
-
// of ternaries, so a
|
|
829
|
-
//
|
|
859
|
+
// of ternaries, so a sixth kind is a row here and the call site stays one line — all take the
|
|
860
|
+
// same three arguments, which is what makes them one site at all.
|
|
830
861
|
const WRITES = {
|
|
831
862
|
attribute: 'setAttribute',
|
|
832
863
|
class: 'setClass',
|
|
833
864
|
style: 'setStyleProperty',
|
|
865
|
+
classGroup: 'setClassGroup',
|
|
866
|
+
styleGroup: 'setStyleGroup',
|
|
834
867
|
} as const
|
|
835
868
|
|
|
869
|
+
/** One attribute binding's write: the element, the one name it owns, and the value. */
|
|
870
|
+
function writeCall(
|
|
871
|
+
write: keyof typeof WRITES,
|
|
872
|
+
element: string,
|
|
873
|
+
name: string,
|
|
874
|
+
value: string,
|
|
875
|
+
lowering: Lowering,
|
|
876
|
+
): string {
|
|
877
|
+
return `${lowering.use(WRITES[write])}(${element}, ${JSON.stringify(name)}, ${value})`
|
|
878
|
+
}
|
|
879
|
+
|
|
836
880
|
// The share scope an instance captures for its deferred mounts (10.5), named apart from the one an
|
|
837
881
|
// instance OPENS (`shareScope`): a page that shares nothing still carries this for a child that does.
|
|
838
882
|
const SHARE_AT = '__shareAt'
|
|
@@ -841,6 +885,9 @@ const SHARE_AT = '__shareAt'
|
|
|
841
885
|
const PARENT = '__parent'
|
|
842
886
|
// The document a built region makes its nodes with, read off `PARENT` once a build.
|
|
843
887
|
const DOCUMENT = '__document'
|
|
888
|
+
// The namespace a built region's top-level elements take, read off `PARENT` once a build.
|
|
889
|
+
const SPACE = '__space'
|
|
890
|
+
const HTML_SPACE = JSON.stringify(HTML_NAMESPACE)
|
|
844
891
|
|
|
845
892
|
// A run's anchors while it holds markup (32.30), as the JS string literals a built run parses with.
|
|
846
893
|
const RAW_OPEN_MARKUP = JSON.stringify(anchorMarkup(RAW_OPEN))
|
|
@@ -903,17 +950,29 @@ function emitMaker(
|
|
|
903
950
|
*
|
|
904
951
|
* A NESTED REGION IS AN EMPTY ANCHOR PAIR, filled by its own machinery on the mount's first run
|
|
905
952
|
* (`fillEmpty`), and a component or a slot is the same pair filled by the child.
|
|
953
|
+
*
|
|
954
|
+
* 32.70 — AN ELEMENT IS BUILT IN THE NAMESPACE THE PARSER WOULD GIVE IT, read off the parent it goes
|
|
955
|
+
* into (`namespaceUnder`) rather than decided here: a component's root does not know it is invoked
|
|
956
|
+
* inside an `<svg>`, and a `<g>` made with `createElement` is an HTML element that draws nothing.
|
|
957
|
+
* Read once a build, and again only below an `<annotation-xml>`, which reads its children as HTML by
|
|
958
|
+
* the encoding it declares; below any other integration point (`INTEGRATION_POINTS`) a child is
|
|
959
|
+
* HTML's, and everywhere else it takes its parent's. `createElementNS` with
|
|
960
|
+
* HTML's namespace costs what `createElement` does (Chromium, 2026-10-22), and a build that makes
|
|
961
|
+
* no top-level element reads none.
|
|
906
962
|
*/
|
|
907
963
|
function emitBuild(row: Cursor, anchored: boolean, lowering: Lowering): string {
|
|
908
964
|
const lines: string[] = [
|
|
909
965
|
`const ${DOCUMENT} = ${PARENT}.ownerDocument`,
|
|
910
966
|
'const __built = []',
|
|
911
967
|
]
|
|
968
|
+
let readsSpace = false
|
|
912
969
|
if (anchored)
|
|
913
970
|
lines.push(
|
|
914
971
|
`__built.push(${DOCUMENT}.createComment(${JSON.stringify(ROW_OPEN)}))`,
|
|
915
972
|
)
|
|
916
973
|
const render = buildRender(lowering)
|
|
974
|
+
// The expression each built element's namespace was, for its children to take.
|
|
975
|
+
const spaceOf = new Map<PlacedNode, string>()
|
|
917
976
|
const into = (at: PlacedNode, node: string): string =>
|
|
918
977
|
at.within === null
|
|
919
978
|
? `__built.push(${node})`
|
|
@@ -936,8 +995,19 @@ function emitBuild(row: Cursor, anchored: boolean, lowering: Lowering): string {
|
|
|
936
995
|
for (const line of builtRun(at, lowering, into)) lines.push(line)
|
|
937
996
|
continue
|
|
938
997
|
}
|
|
998
|
+
const within = at.within
|
|
999
|
+
const space =
|
|
1000
|
+
within === null
|
|
1001
|
+
? SPACE
|
|
1002
|
+
: INTEGRATION_POINTS.has(within.tag)
|
|
1003
|
+
? HTML_SPACE
|
|
1004
|
+
: within.tag === 'annotation-xml'
|
|
1005
|
+
? `${lowering.use('namespaceUnder')}(${within.name})`
|
|
1006
|
+
: (spaceOf.get(within) ?? SPACE)
|
|
1007
|
+
if (space === SPACE) readsSpace = true
|
|
1008
|
+
spaceOf.set(at, space)
|
|
939
1009
|
lines.push(
|
|
940
|
-
`const ${at.name} = ${DOCUMENT}.
|
|
1010
|
+
`const ${at.name} = ${DOCUMENT}.createElementNS(${space}, ${JSON.stringify(at.tag)})`,
|
|
941
1011
|
)
|
|
942
1012
|
for (const one of classifyAttributes(
|
|
943
1013
|
at.holder as ElementNode,
|
|
@@ -945,7 +1015,13 @@ function emitBuild(row: Cursor, anchored: boolean, lowering: Lowering): string {
|
|
|
945
1015
|
)) {
|
|
946
1016
|
const value = builtAttribute(one, lowering, render)
|
|
947
1017
|
if (value === null) continue
|
|
948
|
-
const
|
|
1018
|
+
const name =
|
|
1019
|
+
one.kind === 'merged' ? one.group?.name : one.attribute.name
|
|
1020
|
+
// 32.70 — one the parser would put in a namespace is written in it (`addAttribute`).
|
|
1021
|
+
const set =
|
|
1022
|
+
name !== undefined && FOREIGN_ATTRIBUTES.has(name)
|
|
1023
|
+
? `${lowering.use('addAttribute')}(${at.name}, ${JSON.stringify(name)}, `
|
|
1024
|
+
: `${at.name}.setAttribute(${JSON.stringify(name)}, `
|
|
949
1025
|
// 32.37 — a whole-value attribute may be absent, which the server wrote as nothing.
|
|
950
1026
|
lines.push(
|
|
951
1027
|
one.kind === 'dynamic'
|
|
@@ -955,6 +1031,12 @@ function emitBuild(row: Cursor, anchored: boolean, lowering: Lowering): string {
|
|
|
955
1031
|
}
|
|
956
1032
|
lines.push(into(at, at.name))
|
|
957
1033
|
}
|
|
1034
|
+
if (readsSpace)
|
|
1035
|
+
lines.splice(
|
|
1036
|
+
1,
|
|
1037
|
+
0,
|
|
1038
|
+
`const ${SPACE} = ${lowering.use('namespaceUnder')}(${PARENT})`,
|
|
1039
|
+
)
|
|
958
1040
|
lines.push('return __built')
|
|
959
1041
|
return lines.join('\n ')
|
|
960
1042
|
}
|
|
@@ -1059,6 +1141,41 @@ function buildRender(lowering: Lowering): ValueRender {
|
|
|
1059
1141
|
}
|
|
1060
1142
|
}
|
|
1061
1143
|
|
|
1144
|
+
/** 32.71 — whether a write is a shared group's whole value. */
|
|
1145
|
+
function writesGroup(write: keyof typeof WRITES): boolean {
|
|
1146
|
+
return write === 'classGroup' || write === 'styleGroup'
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/** 32.71 — whether a shared group is one of a row's item watches: any member is. */
|
|
1150
|
+
function groupReadsOwnItem(group: MergeGroup, lowering: Lowering): boolean {
|
|
1151
|
+
for (const member of group.members)
|
|
1152
|
+
if (member.expression && lowering.readsOwnItem(member)) return true
|
|
1153
|
+
return false
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* 32.71 — a shared group's write arguments: the plain attribute's value, then every directive's
|
|
1158
|
+
* condition or value in the order the group names them, each read by `read`. A directive with no
|
|
1159
|
+
* expression is a class that is always on, or a property's own bytes.
|
|
1160
|
+
*/
|
|
1161
|
+
function groupArguments(
|
|
1162
|
+
group: MergeGroup,
|
|
1163
|
+
read: (attribute: Attribute) => string,
|
|
1164
|
+
): string {
|
|
1165
|
+
const values: string[] = []
|
|
1166
|
+
for (const directive of group.directives)
|
|
1167
|
+
values.push(
|
|
1168
|
+
directive.expression
|
|
1169
|
+
? read(directive)
|
|
1170
|
+
: group.name === 'class'
|
|
1171
|
+
? 'true'
|
|
1172
|
+
: JSON.stringify(
|
|
1173
|
+
decodeCharacterReferences(directive.value, 'attribute'),
|
|
1174
|
+
),
|
|
1175
|
+
)
|
|
1176
|
+
return `${read(group.plain as Attribute)}, [${values.join(', ')}]`
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1062
1179
|
/**
|
|
1063
1180
|
* Emits a region's `_build` and `_mount` pair into `helpers`, for a region nobody painted.
|
|
1064
1181
|
*
|
|
@@ -1565,6 +1682,40 @@ function rowUpdate(
|
|
|
1565
1682
|
position.holder as ElementNode,
|
|
1566
1683
|
lowering,
|
|
1567
1684
|
)) {
|
|
1685
|
+
// 32.71 — a shared group is written whole where a member moves with the item — and
|
|
1686
|
+
// where none is live, as for any attribute: a live group's watch reads the item too.
|
|
1687
|
+
if (
|
|
1688
|
+
one.kind === 'merged' &&
|
|
1689
|
+
one.group !== undefined &&
|
|
1690
|
+
one.toggles !== undefined &&
|
|
1691
|
+
writesGroup(one.toggles.write)
|
|
1692
|
+
) {
|
|
1693
|
+
if (one.live) continue
|
|
1694
|
+
for (const member of one.group.members)
|
|
1695
|
+
if (
|
|
1696
|
+
member.expression &&
|
|
1697
|
+
!lowering.isLive(member) &&
|
|
1698
|
+
lowering.readsItem(member, block)
|
|
1699
|
+
) {
|
|
1700
|
+
own.push(
|
|
1701
|
+
writeCall(
|
|
1702
|
+
one.toggles.write,
|
|
1703
|
+
`__a${slot}`,
|
|
1704
|
+
one.toggles.name,
|
|
1705
|
+
groupArguments(one.group, (attribute) =>
|
|
1706
|
+
unlandable(
|
|
1707
|
+
lowering,
|
|
1708
|
+
attribute,
|
|
1709
|
+
lowering.expression(attribute),
|
|
1710
|
+
),
|
|
1711
|
+
),
|
|
1712
|
+
lowering,
|
|
1713
|
+
),
|
|
1714
|
+
)
|
|
1715
|
+
break
|
|
1716
|
+
}
|
|
1717
|
+
continue
|
|
1718
|
+
}
|
|
1568
1719
|
if (!one.attribute.expression || one.live) continue
|
|
1569
1720
|
if (!lowering.readsItem(one.attribute, block)) continue
|
|
1570
1721
|
const toggles =
|
|
@@ -1578,7 +1729,13 @@ function rowUpdate(
|
|
|
1578
1729
|
: undefined
|
|
1579
1730
|
if (toggles === undefined) continue
|
|
1580
1731
|
own.push(
|
|
1581
|
-
|
|
1732
|
+
writeCall(
|
|
1733
|
+
toggles.write,
|
|
1734
|
+
`__a${slot}`,
|
|
1735
|
+
toggles.name,
|
|
1736
|
+
lowering.expression(one.attribute),
|
|
1737
|
+
lowering,
|
|
1738
|
+
),
|
|
1582
1739
|
)
|
|
1583
1740
|
}
|
|
1584
1741
|
if (own.length === 0) continue
|
|
@@ -354,13 +354,9 @@ function renderPosition(
|
|
|
354
354
|
// `<br>`, and a close tag written for one is markup no browser accepts.
|
|
355
355
|
if (node.children.length === 0 && VOID_ELEMENTS.has(node.lowerTag))
|
|
356
356
|
return parts.join(' + ')
|
|
357
|
-
// 32.61 — THE VALUE
|
|
358
|
-
//
|
|
359
|
-
if (
|
|
360
|
-
within !== undefined &&
|
|
361
|
-
node.lowerTag === 'textarea' &&
|
|
362
|
-
node.children.length === 0
|
|
363
|
-
)
|
|
357
|
+
// 32.61 — THE VALUE AS THE TEXT, led by the newline the parser drops after `<textarea>` so a
|
|
358
|
+
// value opening on one keeps it. The author wrote none: 32.72 refuses it beside the bind.
|
|
359
|
+
if (within !== undefined && node.lowerTag === 'textarea')
|
|
364
360
|
return [
|
|
365
361
|
...parts,
|
|
366
362
|
`'\\n' + ${lowering.use('escapeAttribute')}(${within})`,
|
|
@@ -614,7 +610,17 @@ function boundMarkup(
|
|
|
614
610
|
return `' value="' + ${lowering.use('escapeAttribute')}(${read}) + '"'`
|
|
615
611
|
const on =
|
|
616
612
|
written.bind === 'group'
|
|
617
|
-
? `${lowering.use('groupHolds')}(${read}, ${
|
|
613
|
+
? `${lowering.use('groupHolds')}(${read}, ${
|
|
614
|
+
written.own === undefined
|
|
615
|
+
? "'on'"
|
|
616
|
+
: written.own.expression
|
|
617
|
+
? unlandable(
|
|
618
|
+
lowering,
|
|
619
|
+
written.own,
|
|
620
|
+
lowering.expression(written.own),
|
|
621
|
+
)
|
|
622
|
+
: JSON.stringify(written.own.value)
|
|
623
|
+
})`
|
|
618
624
|
: read
|
|
619
625
|
return `(${on} ? ' ${written.reflects}' : '')`
|
|
620
626
|
}
|
package/src/server/commands.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
7
7
|
import { parseArgs } from 'node:util' // bun exposes no argv parser of its own yet
|
|
8
8
|
import { MCP_ADDRESS, OPENAPI_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
9
|
+
import { IN_PROCESS_ORIGIN } from '#shared/IN_PROCESS_ORIGIN.ts'
|
|
9
10
|
import { banner } from './banner.ts'
|
|
10
11
|
import { type App, type AppHandle, enterApp } from './createApp.ts'
|
|
11
12
|
import { stopOnSignalOrFault } from './faultPolicy.ts'
|
|
@@ -494,7 +495,7 @@ async function surfaceAt(
|
|
|
494
495
|
const booted = await booting(host, app, given)
|
|
495
496
|
if (typeof booted === 'number') return booted
|
|
496
497
|
return (init) =>
|
|
497
|
-
booted.fetch(new Request(
|
|
498
|
+
booted.fetch(new Request(`${IN_PROCESS_ORIGIN}${address}`, init))
|
|
498
499
|
}
|
|
499
500
|
|
|
500
501
|
/**
|
package/src/server/config.ts
CHANGED
|
@@ -49,6 +49,16 @@ export interface Config {
|
|
|
49
49
|
APP_DATA_DIR: string
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// What a preload derives beside 27.3's three — the test preload's principal secret (38.120). Declared
|
|
53
|
+
// above `config`, which resolves as it is built — and so is invalidated by an addition.
|
|
54
|
+
const ADDED: Record<string, string> = {}
|
|
55
|
+
|
|
56
|
+
/** Adds `values` to the derived layer, so every `onConfig` default and the env still win (27.2). */
|
|
57
|
+
export function addDerived(values: Record<string, string>): void {
|
|
58
|
+
Object.assign(ADDED, values)
|
|
59
|
+
config.invalidate()
|
|
60
|
+
}
|
|
61
|
+
|
|
52
62
|
/** The resolved configuration: abide's derived values, then `onConfig`'s defaults, then the env. */
|
|
53
63
|
export const config = reactive(resolve, { global: true })
|
|
54
64
|
|
|
@@ -100,7 +110,8 @@ function coerce(text: string, standing: unknown): unknown {
|
|
|
100
110
|
}
|
|
101
111
|
|
|
102
112
|
/**
|
|
103
|
-
* The app's name, version and data directory, off its own `package.json` and the platform
|
|
113
|
+
* The app's name, version and data directory, off its own `package.json` and the platform, and
|
|
114
|
+
* what `addDerived` added.
|
|
104
115
|
*
|
|
105
116
|
* 27.3 — a layer under the other two, so a config or an environment that carries one wins.
|
|
106
117
|
*/
|
|
@@ -108,6 +119,7 @@ function derived(): Record<string, string> {
|
|
|
108
119
|
const manifest = APP_PACKAGE.current
|
|
109
120
|
const name = manifest.name ?? 'app'
|
|
110
121
|
return {
|
|
122
|
+
...ADDED,
|
|
111
123
|
APP_NAME: name,
|
|
112
124
|
APP_VERSION: manifest.version ?? '0.0.0',
|
|
113
125
|
// 36.4 — A PATH, and never created: an app that wants the directory makes it.
|