@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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
boundValue,
|
|
3
|
+
groupHolds,
|
|
4
|
+
heldValue,
|
|
5
|
+
writeBound,
|
|
6
|
+
} from '#shared/boundValue.ts'
|
|
7
|
+
import type { Scope } from './Scope.ts'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Keeps a radio or checkbox's `checked` in step with a `bind:group` value, both ways.
|
|
11
|
+
*
|
|
12
|
+
* 32.8 — COMPARED AGAINST THE INPUT'S OWN VALUE, read through `own` — the `value` attribute's
|
|
13
|
+
* expression, or its static text, or `on` where it has none — rather than off the element: a
|
|
14
|
+
* `value={option.id}` that moves reruns the compare, and a number stays a number in the group
|
|
15
|
+
* where the element's `value` property would have handed back its text.
|
|
16
|
+
*/
|
|
17
|
+
export function bindGroup(
|
|
18
|
+
element: Element,
|
|
19
|
+
operand: unknown,
|
|
20
|
+
own: () => unknown,
|
|
21
|
+
scope: Scope,
|
|
22
|
+
): void {
|
|
23
|
+
const bound = element as HTMLInputElement
|
|
24
|
+
scope.watch(() => {
|
|
25
|
+
const checked = groupHolds(boundValue(operand), own())
|
|
26
|
+
bound.checked = checked
|
|
27
|
+
bound.toggleAttribute('checked', checked)
|
|
28
|
+
})
|
|
29
|
+
bound.addEventListener('change', () => {
|
|
30
|
+
const value = own()
|
|
31
|
+
const standing = heldValue(operand)
|
|
32
|
+
// A checkbox group holds a list, and keeps its order; a radio group holds the one value.
|
|
33
|
+
if (Array.isArray(standing)) {
|
|
34
|
+
const next: unknown[] = []
|
|
35
|
+
for (const member of standing)
|
|
36
|
+
if (member !== value) next.push(member)
|
|
37
|
+
if (bound.checked) next.push(value)
|
|
38
|
+
writeBound(operand, next)
|
|
39
|
+
} else if (bound.checked) writeBound(operand, value)
|
|
40
|
+
})
|
|
41
|
+
}
|
package/src/ui/bindProperty.ts
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
boundValue,
|
|
11
|
-
groupHolds,
|
|
12
11
|
heldValue,
|
|
13
12
|
selectHolds,
|
|
14
13
|
writeBound,
|
|
@@ -20,7 +19,7 @@ type Bound = HTMLInputElement & HTMLDetailsElement
|
|
|
20
19
|
|
|
21
20
|
export function bindProperty(
|
|
22
21
|
element: Element,
|
|
23
|
-
kind: 'value' | 'checked' | 'open'
|
|
22
|
+
kind: 'value' | 'checked' | 'open',
|
|
24
23
|
operand: unknown,
|
|
25
24
|
scope: Scope,
|
|
26
25
|
): void {
|
|
@@ -29,25 +28,6 @@ export function bindProperty(
|
|
|
29
28
|
bindValue(bound, operand, scope)
|
|
30
29
|
return
|
|
31
30
|
}
|
|
32
|
-
if (kind === 'group') {
|
|
33
|
-
scope.watch(() => {
|
|
34
|
-
const checked = groupHolds(boundValue(operand), bound.value)
|
|
35
|
-
bound.checked = checked
|
|
36
|
-
bound.toggleAttribute('checked', checked)
|
|
37
|
-
})
|
|
38
|
-
bound.addEventListener('change', () => {
|
|
39
|
-
const standing = heldValue(operand)
|
|
40
|
-
// A checkbox group holds a list, and keeps its order; a radio group holds the one value.
|
|
41
|
-
if (Array.isArray(standing)) {
|
|
42
|
-
const next: unknown[] = []
|
|
43
|
-
for (const member of standing)
|
|
44
|
-
if (member !== bound.value) next.push(member)
|
|
45
|
-
if (bound.checked) next.push(bound.value)
|
|
46
|
-
writeBound(operand, next)
|
|
47
|
-
} else if (bound.checked) writeBound(operand, bound.value)
|
|
48
|
-
})
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
31
|
// `checked` on an input, `open` on a `<details>`.
|
|
52
32
|
const property = kind
|
|
53
33
|
scope.watch(() => {
|
package/src/ui/bindSpread.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { spreadEntries } from '#shared/spread.ts'
|
|
6
6
|
import type { Scope } from './Scope.ts'
|
|
7
|
+
import { addAttribute } from './setAttribute.ts'
|
|
7
8
|
|
|
8
9
|
/** Keeps an element's spread attributes and `on<event>` listeners written as the value changes. */
|
|
9
10
|
export function bindSpread(
|
|
@@ -19,8 +20,9 @@ export function bindSpread(
|
|
|
19
20
|
const next = new Set<string>()
|
|
20
21
|
for (const [name, text] of spreadEntries(read(), explicit, wanted)) {
|
|
21
22
|
next.add(name)
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
const standing = element.getAttribute(name)
|
|
24
|
+
if (standing === null) addAttribute(element, name, text)
|
|
25
|
+
else if (standing !== text) element.setAttribute(name, text)
|
|
24
26
|
}
|
|
25
27
|
for (const name of written)
|
|
26
28
|
if (!next.has(name)) element.removeAttribute(name)
|
package/src/ui/index.ts
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
// and every `class:name` on an element is merged by the server into the single `style` or `class`
|
|
16
16
|
// attribute beside the author's own, so neither of those two attributes can be written back whole
|
|
17
17
|
// — the CSSOM and `classList` are what touch the one declaration or the one token a binding owns.
|
|
18
|
+
// A `class={expr}` or `style={expr}` beside its directives is the same answer once more
|
|
19
|
+
// (`setClassGroup`, `setStyleGroup`, 32.71): the group writes what it owns, and nothing else.
|
|
18
20
|
// A list is reached through `listAt` and `updateList` alone, so its rows' shape is no caller's.
|
|
19
21
|
// Nothing that finds a node by something added to the document, because a compiler may not invent
|
|
20
22
|
// structure an author's CSS or queries can see.
|
|
@@ -37,10 +39,8 @@ export {
|
|
|
37
39
|
runHere,
|
|
38
40
|
textHere,
|
|
39
41
|
} from './adoptWalk.ts'
|
|
40
|
-
// 39.6 — a module mounted into an empty region applies its own head, from inside its instance
|
|
41
|
-
// (`emitClient`); only a module that carries its own markup and a `<head>` imports it.
|
|
42
|
-
export { applyHead } from './applyHead.ts'
|
|
43
42
|
export { bindElement } from './bindElement.ts'
|
|
43
|
+
export { bindGroup } from './bindGroup.ts'
|
|
44
44
|
export { bindProperty } from './bindProperty.ts'
|
|
45
45
|
export { bindSpread } from './bindSpread.ts'
|
|
46
46
|
export { escapeless } from './escapeless.ts'
|
|
@@ -48,7 +48,7 @@ export { escapeless } from './escapeless.ts'
|
|
|
48
48
|
export { contributeHead } from './Level.ts'
|
|
49
49
|
export { listAt, updateList } from './List.ts'
|
|
50
50
|
export { warnMismatch } from './Mismatch.ts'
|
|
51
|
-
export { markupNodes } from './parseMarkup.ts'
|
|
51
|
+
export { markupNodes, namespaceUnder } from './parseMarkup.ts'
|
|
52
52
|
export {
|
|
53
53
|
fillEmpty,
|
|
54
54
|
fillRegion,
|
|
@@ -58,9 +58,11 @@ export {
|
|
|
58
58
|
export { rowKeyOf } from './rowKeyOf.ts'
|
|
59
59
|
export { itemOf, noUpdate, replaceRow, rowCell } from './rows.ts'
|
|
60
60
|
export { EMPTY_SCOPE, Scope } from './Scope.ts'
|
|
61
|
-
export { setAttribute } from './setAttribute.ts'
|
|
61
|
+
export { addAttribute, setAttribute } from './setAttribute.ts'
|
|
62
62
|
export { setClass } from './setClass.ts'
|
|
63
|
+
export { setClassGroup } from './setClassGroup.ts'
|
|
63
64
|
export { setMarkup } from './setMarkup.ts'
|
|
65
|
+
export { setStyleGroup } from './setStyleGroup.ts'
|
|
64
66
|
export { setStyleProperty } from './setStyleProperty.ts'
|
|
65
67
|
export { setText } from './setText.ts'
|
|
66
68
|
export { TryRegion } from './TryRegion.ts'
|
package/src/ui/parseMarkup.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HTML_NAMESPACE,
|
|
3
|
+
INTEGRATION_POINTS,
|
|
4
|
+
} from '#shared/INTEGRATION_POINTS.ts'
|
|
5
|
+
|
|
1
6
|
// MARKUP PARSED IN THE DOCUMENT `parent` BELONGS TO, not the global one. A page mounted into a second
|
|
2
7
|
// document (an example `<iframe>`) parsed its `{#if}` arms in the right one and its `{#for}` rows in
|
|
3
8
|
// the page's while the two were separate copies of this; one copy is one answer.
|
|
@@ -9,26 +14,22 @@ const TEMPLATES = new WeakMap<Document, HTMLTemplateElement>()
|
|
|
9
14
|
const SVG = 'http://www.w3.org/2000/svg'
|
|
10
15
|
const MATHML = 'http://www.w3.org/1998/Math/MathML'
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'mtext',
|
|
23
|
-
])
|
|
24
|
-
|
|
25
|
-
/** Whether the parser reads `element`'s children as HTML again, though it is SVG or MathML. */
|
|
26
|
-
function isIntegrationPoint(element: Element): boolean {
|
|
27
|
-
if (INTEGRATION_POINTS.has(element.localName)) return true
|
|
17
|
+
/**
|
|
18
|
+
* The namespace an element the parser makes inside `parent` takes: SVG's or MathML's under an
|
|
19
|
+
* element of either, and HTML's under anything else — an HTML element, a document, a fragment, and
|
|
20
|
+
* an integration point (`INTEGRATION_POINTS`), whose children the parser reads as HTML again.
|
|
21
|
+
*/
|
|
22
|
+
export function namespaceUnder(parent: Node): string {
|
|
23
|
+
const element = parent as Element
|
|
24
|
+
const space = element.namespaceURI
|
|
25
|
+
if (space !== SVG && space !== MATHML) return HTML_NAMESPACE
|
|
26
|
+
if (INTEGRATION_POINTS.has(element.localName)) return HTML_NAMESPACE
|
|
28
27
|
// The HTML standard makes an `<annotation-xml>` one only by the encoding it declares.
|
|
29
|
-
if (element.localName !== 'annotation-xml') return
|
|
28
|
+
if (element.localName !== 'annotation-xml') return space
|
|
30
29
|
const encoding = element.getAttribute('encoding')?.toLowerCase()
|
|
31
30
|
return encoding === 'text/html' || encoding === 'application/xhtml+xml'
|
|
31
|
+
? HTML_NAMESPACE
|
|
32
|
+
: space
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/**
|
|
@@ -48,14 +49,8 @@ export function parseMarkup(parent: Node, markup: string): DocumentFragment {
|
|
|
48
49
|
template = document.createElement('template')
|
|
49
50
|
TEMPLATES.set(document, template)
|
|
50
51
|
}
|
|
51
|
-
const space = (parent
|
|
52
|
-
const wrapper =
|
|
53
|
-
(space !== SVG && space !== MATHML) ||
|
|
54
|
-
isIntegrationPoint(parent as Element)
|
|
55
|
-
? undefined
|
|
56
|
-
: space === SVG
|
|
57
|
-
? 'svg'
|
|
58
|
-
: 'math'
|
|
52
|
+
const space = namespaceUnder(parent)
|
|
53
|
+
const wrapper = space === HTML_NAMESPACE ? undefined : space === SVG ? 'svg' : 'math'
|
|
59
54
|
if (wrapper === undefined) {
|
|
60
55
|
template.innerHTML = markup
|
|
61
56
|
return template.content
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// 39.4–39.6, 39.13–39.17 — THE DOCUMENT HEAD HAS ONE WRITER. Every routed level and every component
|
|
2
|
+
// with a `<head>` is a contributor, and the head is reconciled from all of them at once, in the
|
|
3
|
+
// chain's order — each level's own head, then its components' in the order they mounted — rather
|
|
4
|
+
// than patched by each with what it can learn of the others.
|
|
5
|
+
//
|
|
6
|
+
// A KEY IS WON BY THE LAST CONTRIBUTOR HOLDING IT (39.4), and one nobody holds any more leaves or
|
|
7
|
+
// goes back to whoever still does (39.14) because that is what the chain now says. A KEYLESS NODE
|
|
8
|
+
// IS ITS CONTRIBUTOR'S, held by reference (39.5, 39.15, 39.16): two contributors writing the same
|
|
9
|
+
// `<style>` each own their own, and one leaving takes its own with it — told apart by where they
|
|
10
|
+
// stand, never by being equal.
|
|
11
|
+
//
|
|
12
|
+
// NOTHING IS ADDED TO MARK OWNERSHIP, and nothing a contributor did not write is touched: a node the
|
|
13
|
+
// server painted is ADOPTED the first time its contributor is reconciled, matched in chain order.
|
|
14
|
+
|
|
15
|
+
import { headKeyOf } from '#shared/headKeyOf.ts'
|
|
16
|
+
import { markupNodes } from './parseMarkup.ts'
|
|
17
|
+
|
|
18
|
+
/** One contributor's head, parsed once per text it reads. */
|
|
19
|
+
type Parsed = { text: string; keyed: Map<string, Element>; keyless: Node[] }
|
|
20
|
+
|
|
21
|
+
const parsedBy = new WeakMap<object, Parsed>()
|
|
22
|
+
/** The keyless nodes standing in the head for each contributor, in its order. */
|
|
23
|
+
const ownedBy = new Map<object, Node[]>()
|
|
24
|
+
/**
|
|
25
|
+
* The element standing for each key the chain held at the last reconcile, so one it drops is
|
|
26
|
+
* withdrawn — that element, and never another holding the same key that nothing here wrote.
|
|
27
|
+
*/
|
|
28
|
+
const keyedNodes = new Map<string, Element>()
|
|
29
|
+
|
|
30
|
+
/** Whether `who` has been reconciled — adopted or written — since it joined. */
|
|
31
|
+
export function reconciled(who: object): boolean {
|
|
32
|
+
return ownedBy.has(who)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Writes the head the chain contributes: `chain` is every contributor and the head it reads now,
|
|
37
|
+
* outermost first.
|
|
38
|
+
*
|
|
39
|
+
* Only what moved is written. A key standing as it is wanted, and a keyless node equal to the one
|
|
40
|
+
* its contributor wants there, are left standing — a `<link rel=stylesheet>` put back as itself is
|
|
41
|
+
* refetched and flashes the page unstyled, and a `<script>` would run again.
|
|
42
|
+
*/
|
|
43
|
+
export function reconcileHead(
|
|
44
|
+
chain: readonly (readonly [who: object, text: string])[],
|
|
45
|
+
): void {
|
|
46
|
+
const head = document.head
|
|
47
|
+
const present = new Set<object>()
|
|
48
|
+
const wanted = new Map<string, Element>()
|
|
49
|
+
for (const [who, text] of chain) {
|
|
50
|
+
present.add(who)
|
|
51
|
+
for (const [key, element] of parsedOf(head, who, text).keyed)
|
|
52
|
+
wanted.set(key, element)
|
|
53
|
+
}
|
|
54
|
+
const standing = standingKeys(head)
|
|
55
|
+
for (const [key, node] of keyedNodes)
|
|
56
|
+
if (!wanted.has(key)) {
|
|
57
|
+
node.remove()
|
|
58
|
+
keyedNodes.delete(key)
|
|
59
|
+
}
|
|
60
|
+
for (const [key, element] of wanted) {
|
|
61
|
+
let at = standing.get(key)
|
|
62
|
+
if (at === undefined) {
|
|
63
|
+
at = element.cloneNode(true) as Element
|
|
64
|
+
head.appendChild(at)
|
|
65
|
+
} else if (at.outerHTML !== element.outerHTML) {
|
|
66
|
+
const next = element.cloneNode(true) as Element
|
|
67
|
+
head.replaceChild(next, at)
|
|
68
|
+
at = next
|
|
69
|
+
}
|
|
70
|
+
keyedNodes.set(key, at)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const [who, nodes] of ownedBy)
|
|
74
|
+
if (!present.has(who)) {
|
|
75
|
+
for (const node of nodes) node.parentNode?.removeChild(node)
|
|
76
|
+
ownedBy.delete(who)
|
|
77
|
+
}
|
|
78
|
+
const claimed = new Set<Node>()
|
|
79
|
+
for (const nodes of ownedBy.values())
|
|
80
|
+
for (const node of nodes) claimed.add(node)
|
|
81
|
+
let previous: Node | null = null
|
|
82
|
+
for (let at = 0; at < chain.length; at += 1) {
|
|
83
|
+
const who = (chain[at] as readonly [object, string])[0]
|
|
84
|
+
const wants = (parsedBy.get(who) as Parsed).keyless
|
|
85
|
+
const owned = ownedBy.get(who) ?? adopt(head, wants, claimed)
|
|
86
|
+
const next: Node[] = []
|
|
87
|
+
for (let index = 0; index < wants.length; index += 1) {
|
|
88
|
+
const want = wants[index] as Node
|
|
89
|
+
const have = owned[index]
|
|
90
|
+
let node: Node
|
|
91
|
+
if (have?.isEqualNode(want) === true) node = have as Node
|
|
92
|
+
else {
|
|
93
|
+
node = runnable(want.cloneNode(true))
|
|
94
|
+
if (have !== undefined) head.replaceChild(node, have)
|
|
95
|
+
else
|
|
96
|
+
head.insertBefore(
|
|
97
|
+
node,
|
|
98
|
+
previous === null
|
|
99
|
+
? firstOwnedAfter(chain, at)
|
|
100
|
+
: previous.nextSibling,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
next.push(node)
|
|
104
|
+
previous = node
|
|
105
|
+
}
|
|
106
|
+
for (let index = wants.length; index < owned.length; index += 1)
|
|
107
|
+
(owned[index] as Node).parentNode?.removeChild(owned[index] as Node)
|
|
108
|
+
ownedBy.set(who, next)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** `who`'s head parsed, from the cache while it reads the same text. */
|
|
113
|
+
function parsedOf(head: HTMLHeadElement, who: object, text: string): Parsed {
|
|
114
|
+
const cached = parsedBy.get(who)
|
|
115
|
+
if (cached !== undefined && cached.text === text) return cached
|
|
116
|
+
const parsed: Parsed = { text, keyed: new Map(), keyless: [] }
|
|
117
|
+
for (const node of markupNodes(head, text)) {
|
|
118
|
+
const key = node.nodeType === 1 ? headKey(node as Element) : undefined
|
|
119
|
+
if (key === undefined) parsed.keyless.push(node)
|
|
120
|
+
else parsed.keyed.set(key, node as Element)
|
|
121
|
+
}
|
|
122
|
+
parsedBy.set(who, parsed)
|
|
123
|
+
return parsed
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The nodes the server painted for a contributor reconciled for the first time: each one it wants,
|
|
128
|
+
* the first equal node nobody has claimed, in order. Contributors are adopted in the chain's
|
|
129
|
+
* order, which is the order the server painted them in, so an earlier one claims the earlier of
|
|
130
|
+
* two equal nodes.
|
|
131
|
+
*/
|
|
132
|
+
function adopt(
|
|
133
|
+
head: HTMLHeadElement,
|
|
134
|
+
wants: Node[],
|
|
135
|
+
claimed: Set<Node>,
|
|
136
|
+
): Node[] {
|
|
137
|
+
const adopted: Node[] = []
|
|
138
|
+
let from = head.firstChild
|
|
139
|
+
for (const want of wants) {
|
|
140
|
+
let found: Node | null = null
|
|
141
|
+
for (let at = from; at !== null; at = at.nextSibling)
|
|
142
|
+
if (!claimed.has(at) && at.isEqualNode(want)) {
|
|
143
|
+
found = at
|
|
144
|
+
break
|
|
145
|
+
}
|
|
146
|
+
if (found === null) break
|
|
147
|
+
claimed.add(found)
|
|
148
|
+
adopted.push(found)
|
|
149
|
+
from = found.nextSibling
|
|
150
|
+
}
|
|
151
|
+
return adopted
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The first keyless node a contributor after `at` owns, which a new one goes before. */
|
|
155
|
+
function firstOwnedAfter(
|
|
156
|
+
chain: readonly (readonly [object, string])[],
|
|
157
|
+
at: number,
|
|
158
|
+
): Node | null {
|
|
159
|
+
for (let later = at + 1; later < chain.length; later += 1) {
|
|
160
|
+
const owned = ownedBy.get(
|
|
161
|
+
(chain[later] as readonly [object, string])[0],
|
|
162
|
+
)
|
|
163
|
+
if (owned !== undefined && owned.length > 0) return owned[0] as Node
|
|
164
|
+
}
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* `node`, or — for a `<script>` — the same script made by the document rather than the parser.
|
|
170
|
+
*
|
|
171
|
+
* 39.17 — A PARSED SCRIPT NEVER RUNS. The fragment parser marks every `<script>` it makes "already
|
|
172
|
+
* started", and a clone keeps the mark, so a script it made and then moved into the head is inert
|
|
173
|
+
* where the server's, made by the document's own parser, ran. One made by `createElement` starts
|
|
174
|
+
* clear and runs on insertion. Keyless always (39.11 keys no `<script>`).
|
|
175
|
+
*/
|
|
176
|
+
function runnable(node: Node): Node {
|
|
177
|
+
if (node.nodeName !== 'SCRIPT') return node
|
|
178
|
+
const parsed = node as HTMLScriptElement
|
|
179
|
+
const script = document.createElement('script')
|
|
180
|
+
const attributes = parsed.attributes
|
|
181
|
+
for (let index = 0; index < attributes.length; index += 1) {
|
|
182
|
+
const attribute = attributes[index] as Attr
|
|
183
|
+
script.setAttribute(attribute.name, attribute.value)
|
|
184
|
+
}
|
|
185
|
+
script.text = parsed.text
|
|
186
|
+
return script
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The head's keyed elements by key, the first holding each — 39.12 within the standing head: the
|
|
191
|
+
* FIRST element holding a key is the one a contribution replaces.
|
|
192
|
+
*/
|
|
193
|
+
function standingKeys(head: HTMLHeadElement): Map<string, Element> {
|
|
194
|
+
const standingByKey = new Map<string, Element>()
|
|
195
|
+
for (
|
|
196
|
+
let node = head.firstElementChild;
|
|
197
|
+
node !== null;
|
|
198
|
+
node = node.nextElementSibling
|
|
199
|
+
) {
|
|
200
|
+
const key = headKey(node)
|
|
201
|
+
if (key !== undefined && !standingByKey.has(key))
|
|
202
|
+
standingByKey.set(key, node)
|
|
203
|
+
}
|
|
204
|
+
return standingByKey
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The key a head element replaces by, or `undefined` for one that appends — 39.11 through the one
|
|
209
|
+
* grammar (`headKeyOf`), asked with a resolved attribute.
|
|
210
|
+
*/
|
|
211
|
+
function headKey(element: Element): string | undefined {
|
|
212
|
+
return headKeyOf(element.tagName.toLowerCase(), (name) =>
|
|
213
|
+
element.getAttribute(name),
|
|
214
|
+
)
|
|
215
|
+
}
|
package/src/ui/setAttribute.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { attributeText } from '#shared/attributeText.ts'
|
|
2
|
+
import { FOREIGN_ATTRIBUTES } from '#shared/FOREIGN_ATTRIBUTES.ts'
|
|
3
|
+
import { HTML_NAMESPACE } from '#shared/INTEGRATION_POINTS.ts'
|
|
2
4
|
import { WORK } from '#shared/WORK.ts'
|
|
3
5
|
|
|
4
6
|
/**
|
|
@@ -33,5 +35,27 @@ export function setAttribute(
|
|
|
33
35
|
if (element.hasAttribute(name)) element.removeAttribute(name)
|
|
34
36
|
return
|
|
35
37
|
}
|
|
36
|
-
|
|
38
|
+
const standing = element.getAttribute(name)
|
|
39
|
+
if (standing === next) return
|
|
40
|
+
if (standing === null) addAttribute(element, name, next)
|
|
41
|
+
else element.setAttribute(name, next)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Writes an attribute `element` does not hold yet, in the namespace the parser gives it there.
|
|
46
|
+
*
|
|
47
|
+
* 32.70 — an `xlink:href` on an SVG element is XLink's (`FOREIGN_ATTRIBUTES`), and one
|
|
48
|
+
* `setAttribute` makes has none, so a `<use>` it names draws nothing. Only an attribute's CREATION
|
|
49
|
+
* decides its namespace: `setAttribute` and `getAttribute` find a standing one by its qualified
|
|
50
|
+
* name, whatever namespace it is in.
|
|
51
|
+
*/
|
|
52
|
+
export function addAttribute(
|
|
53
|
+
element: Element,
|
|
54
|
+
name: string,
|
|
55
|
+
text: string,
|
|
56
|
+
): void {
|
|
57
|
+
const space = FOREIGN_ATTRIBUTES.get(name)
|
|
58
|
+
if (space === undefined || element.namespaceURI === HTML_NAMESPACE)
|
|
59
|
+
element.setAttribute(name, text)
|
|
60
|
+
else element.setAttributeNS(space, name, text)
|
|
37
61
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ASCII_WHITESPACE } from '#shared/ASCII_WHITESPACE.ts'
|
|
2
|
+
import { textOf } from '#shared/Raw.ts'
|
|
3
|
+
import { WORK } from '#shared/WORK.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 32.71 — what a `class={expr}` and the `class:name` directives beside it last wrote, as one
|
|
7
|
+
* group: the attribute's text and tokens, and the directives' conditions in the order `names`
|
|
8
|
+
* gives them.
|
|
9
|
+
*
|
|
10
|
+
* KEYED BY ELEMENT rather than declared by the mount, because a row's `update` writes the same
|
|
11
|
+
* element from a function of its own and both have to read one record.
|
|
12
|
+
*/
|
|
13
|
+
const owned = new WeakMap<
|
|
14
|
+
Element,
|
|
15
|
+
{
|
|
16
|
+
text: string | undefined
|
|
17
|
+
tokens: ReadonlySet<string>
|
|
18
|
+
conditions: readonly unknown[] | undefined
|
|
19
|
+
names: string[]
|
|
20
|
+
}
|
|
21
|
+
>()
|
|
22
|
+
|
|
23
|
+
// Every record's tokens until its `class` is first written, which replaces the set.
|
|
24
|
+
const NO_TOKENS: ReadonlySet<string> = new Set()
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Writes a `class={expr}` and the `class:name` directives beside it as one group, through
|
|
28
|
+
* `classList`: `plain` is the attribute's value, and `conditions` each directive's, in the order
|
|
29
|
+
* of `names`, its classes space-separated.
|
|
30
|
+
*
|
|
31
|
+
* 32.71 — ONLY ITS OWN CLASSES, and only what moved: the attribute's tokens are re-split only when
|
|
32
|
+
* its text changed, a token it dropped comes off unless a directive holds it, and a directive is
|
|
33
|
+
* written only when its condition flipped, kept on while the attribute holds its class. Every
|
|
34
|
+
* write is guarded by `contains`, so the first run over a served page writes nothing.
|
|
35
|
+
*/
|
|
36
|
+
export function setClassGroup(
|
|
37
|
+
element: Element,
|
|
38
|
+
names: string,
|
|
39
|
+
plain: unknown,
|
|
40
|
+
conditions: readonly unknown[],
|
|
41
|
+
): void {
|
|
42
|
+
WORK.bindingRuns += 1
|
|
43
|
+
let held = owned.get(element)
|
|
44
|
+
if (held === undefined) {
|
|
45
|
+
held = {
|
|
46
|
+
text: undefined,
|
|
47
|
+
tokens: NO_TOKENS,
|
|
48
|
+
conditions: undefined,
|
|
49
|
+
names: names.split(' '),
|
|
50
|
+
}
|
|
51
|
+
owned.set(element, held)
|
|
52
|
+
}
|
|
53
|
+
const classes = element.classList
|
|
54
|
+
const text = textOf(plain)
|
|
55
|
+
const was = held.conditions
|
|
56
|
+
if (text !== held.text) {
|
|
57
|
+
const next = new Set<string>()
|
|
58
|
+
for (const token of text.split(ASCII_WHITESPACE))
|
|
59
|
+
if (token !== '') next.add(token)
|
|
60
|
+
for (const token of held.tokens) {
|
|
61
|
+
if (next.has(token)) continue
|
|
62
|
+
const at = held.names.indexOf(token)
|
|
63
|
+
if (at !== -1 && conditions[at]) continue
|
|
64
|
+
if (classes.contains(token)) classes.remove(token)
|
|
65
|
+
}
|
|
66
|
+
for (const token of next)
|
|
67
|
+
if (!classes.contains(token)) classes.add(token)
|
|
68
|
+
held.text = text
|
|
69
|
+
held.tokens = next
|
|
70
|
+
}
|
|
71
|
+
for (let at = 0; at < held.names.length; at += 1) {
|
|
72
|
+
const on = Boolean(conditions[at])
|
|
73
|
+
if (was !== undefined && Boolean(was[at]) === on) continue
|
|
74
|
+
const name = held.names[at] as string
|
|
75
|
+
const wanted = on || held.tokens.has(name)
|
|
76
|
+
if (classes.contains(name) !== wanted) classes.toggle(name, wanted)
|
|
77
|
+
}
|
|
78
|
+
held.conditions = conditions
|
|
79
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { oneStyleValue } from '#shared/escapeText.ts'
|
|
2
|
+
import { textOf } from '#shared/Raw.ts'
|
|
3
|
+
import { WORK } from '#shared/WORK.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 32.71 — what a `style={expr}` and the `style:prop` directives beside it last wrote, as one
|
|
7
|
+
* group: the attribute's text, its declarations once they have been needed, and the directives'
|
|
8
|
+
* values in the order `names` gives them.
|
|
9
|
+
*
|
|
10
|
+
* KEYED BY ELEMENT rather than declared by the mount, because a row's `update` writes the same
|
|
11
|
+
* element from a function of its own and both have to read one record.
|
|
12
|
+
*/
|
|
13
|
+
const owned = new WeakMap<
|
|
14
|
+
Element,
|
|
15
|
+
{
|
|
16
|
+
text: string | undefined
|
|
17
|
+
declared: Map<string, [value: string, priority: string]> | undefined
|
|
18
|
+
values: readonly unknown[] | undefined
|
|
19
|
+
names: string[]
|
|
20
|
+
}
|
|
21
|
+
>()
|
|
22
|
+
|
|
23
|
+
// THE PARSE IS THE BROWSER'S OWN: a declaration list is not split on `;` by hand, since one can sit
|
|
24
|
+
// inside `url()` or a string. A detached element's style is parsed and never laid out.
|
|
25
|
+
let parsed: CSSStyleDeclaration | undefined
|
|
26
|
+
|
|
27
|
+
/** A declaration list's properties, values and priorities, as the browser parses it. */
|
|
28
|
+
function declarationsOf(
|
|
29
|
+
text: string,
|
|
30
|
+
): Map<string, [value: string, priority: string]> {
|
|
31
|
+
parsed ??= document.createElement('div').style
|
|
32
|
+
parsed.cssText = text
|
|
33
|
+
const declared = new Map<string, [value: string, priority: string]>()
|
|
34
|
+
for (let index = 0; index < parsed.length; index += 1) {
|
|
35
|
+
const property = parsed.item(index)
|
|
36
|
+
declared.set(property, [
|
|
37
|
+
parsed.getPropertyValue(property),
|
|
38
|
+
parsed.getPropertyPriority(property),
|
|
39
|
+
])
|
|
40
|
+
}
|
|
41
|
+
return declared
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Writes a `style={expr}` and the `style:prop` directives beside it as one group, through the
|
|
46
|
+
* CSSOM: `plain` is the attribute's value, and `values` each directive's, in the order of `names`,
|
|
47
|
+
* its properties space-separated.
|
|
48
|
+
*
|
|
49
|
+
* 32.71 — ONLY ITS OWN PROPERTIES, in the server's cascade: the attribute's declarations, then
|
|
50
|
+
* each directive's in the tag's order (`styleValue`), a later one winning and one with no value
|
|
51
|
+
* declaring nothing. Only what moved is written. The attribute is parsed only when its text
|
|
52
|
+
* changed — never on the first run, whose value the served page or built row already carries —
|
|
53
|
+
* and every write is guarded by a compare. Where the attribute changed or a directive emptied,
|
|
54
|
+
* the cascade is laid again from the attribute, which is how a shorthand on one side and a
|
|
55
|
+
* longhand on the other resolve as they did in the server's list.
|
|
56
|
+
*/
|
|
57
|
+
export function setStyleGroup(
|
|
58
|
+
element: Element,
|
|
59
|
+
names: string,
|
|
60
|
+
plain: unknown,
|
|
61
|
+
values: readonly unknown[],
|
|
62
|
+
): void {
|
|
63
|
+
WORK.bindingRuns += 1
|
|
64
|
+
let held = owned.get(element)
|
|
65
|
+
if (held === undefined) {
|
|
66
|
+
held = {
|
|
67
|
+
text: undefined,
|
|
68
|
+
declared: undefined,
|
|
69
|
+
values: undefined,
|
|
70
|
+
names: names.split(' '),
|
|
71
|
+
}
|
|
72
|
+
owned.set(element, held)
|
|
73
|
+
}
|
|
74
|
+
const style = (element as HTMLElement).style
|
|
75
|
+
const text = textOf(plain)
|
|
76
|
+
const was = held.values
|
|
77
|
+
// The first directive to write again, every one after it too: a later one wins.
|
|
78
|
+
let from = -1
|
|
79
|
+
if (held.text === undefined) held.text = text
|
|
80
|
+
else if (text !== held.text) {
|
|
81
|
+
const before = held.declared ?? declarationsOf(held.text)
|
|
82
|
+
const after = declarationsOf(text)
|
|
83
|
+
for (const property of before.keys())
|
|
84
|
+
if (!after.has(property) && style.getPropertyValue(property) !== '')
|
|
85
|
+
style.removeProperty(property)
|
|
86
|
+
lay(style, after)
|
|
87
|
+
held.text = text
|
|
88
|
+
held.declared = after
|
|
89
|
+
from = 0
|
|
90
|
+
}
|
|
91
|
+
for (let at = 0; at < held.names.length && from !== 0; at += 1) {
|
|
92
|
+
const value = directiveValue(values[at])
|
|
93
|
+
if (was !== undefined && directiveValue(was[at]) === value) continue
|
|
94
|
+
if (value !== '' || was === undefined) {
|
|
95
|
+
if (from === -1 && value !== '') from = at
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
const name = held.names[at] as string
|
|
99
|
+
if (style.getPropertyValue(name) !== '') style.removeProperty(name)
|
|
100
|
+
held.declared ??= declarationsOf(held.text)
|
|
101
|
+
lay(style, held.declared)
|
|
102
|
+
from = 0
|
|
103
|
+
}
|
|
104
|
+
if (from !== -1)
|
|
105
|
+
for (let at = from; at < held.names.length; at += 1) {
|
|
106
|
+
const value = directiveValue(values[at])
|
|
107
|
+
const name = held.names[at] as string
|
|
108
|
+
if (value !== '' && style.getPropertyValue(name) !== value)
|
|
109
|
+
style.setProperty(name, value)
|
|
110
|
+
}
|
|
111
|
+
held.values = values
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A directive's value, cut as the server cuts it; `''` declares nothing. */
|
|
115
|
+
function directiveValue(value: unknown): string {
|
|
116
|
+
return oneStyleValue(textOf(value))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Writes each declaration the element does not already carry. */
|
|
120
|
+
function lay(
|
|
121
|
+
style: CSSStyleDeclaration,
|
|
122
|
+
declared: Map<string, [value: string, priority: string]>,
|
|
123
|
+
): void {
|
|
124
|
+
for (const [property, [value, priority]] of declared)
|
|
125
|
+
if (
|
|
126
|
+
style.getPropertyValue(property) !== value ||
|
|
127
|
+
style.getPropertyPriority(property) !== priority
|
|
128
|
+
)
|
|
129
|
+
style.setProperty(property, value, priority)
|
|
130
|
+
}
|