@abide/abide 0.112.0 → 0.114.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 +20 -0
- package/docs/REGISTRY.md +10 -10
- package/docs/RULEBOOK.md +30 -3
- package/docs/abide.md +52 -44
- package/package.json +1 -1
- package/src/compiler/appLayout.ts +16 -1
- package/src/compiler/classifyTemplate.ts +5 -5
- package/src/compiler/emitClient.ts +45 -2
- package/src/server/bridgeRelay.ts +93 -31
- package/src/server/claudeCode.ts +42 -9
- 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 +60 -7
- package/src/server/requestScope.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/bindSpread.ts +4 -2
- package/src/ui/index.ts +2 -2
- package/src/ui/parseMarkup.ts +20 -25
- package/src/ui/setAttribute.ts +25 -1
|
@@ -22,6 +22,9 @@ import { IDLE_TIMEOUT_SECONDS } from './IDLE_TIMEOUT_SECONDS.ts'
|
|
|
22
22
|
/** The three framings a stream is answered in. 17.6. */
|
|
23
23
|
export type Framing = Framed | 'bytes'
|
|
24
24
|
|
|
25
|
+
/** A principal cookie's claims, `exp` and `iat` aside, and when it lapses. 26.11. */
|
|
26
|
+
export type Sealed = { claims: Record<string, unknown>; expires: number }
|
|
27
|
+
|
|
25
28
|
/** What one request, or one `App.run`, carries across every `await` its work makes. */
|
|
26
29
|
export type RequestScope = Scope & {
|
|
27
30
|
/**
|
|
@@ -38,6 +41,8 @@ export type RequestScope = Scope & {
|
|
|
38
41
|
caller: string | undefined
|
|
39
42
|
/** This request's one resolve of the principal. 26.14. */
|
|
40
43
|
principal: Promise<Principal> | undefined
|
|
44
|
+
/** What the principal cookie opened to once read, or `null` where it held no seal. */
|
|
45
|
+
opened: Sealed | null | undefined
|
|
41
46
|
/** 16.23 — whether the received request's idle timeout is off until the answer is sent. */
|
|
42
47
|
unreaped: boolean
|
|
43
48
|
/** This response's nonce, minted by the `csp` rung or on the first ask. 29.3. */
|
|
@@ -168,6 +173,7 @@ export function scopeFor(
|
|
|
168
173
|
unreaped: false,
|
|
169
174
|
caller: undefined,
|
|
170
175
|
principal: undefined,
|
|
176
|
+
opened: undefined,
|
|
171
177
|
applied: false,
|
|
172
178
|
request: given,
|
|
173
179
|
route: undefined,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const XLINK = 'http://www.w3.org/1999/xlink'
|
|
2
|
+
const XML = 'http://www.w3.org/XML/1998/namespace'
|
|
3
|
+
const XMLNS = 'http://www.w3.org/2000/xmlns/'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The attributes the HTML parser puts in a namespace on an SVG or MathML element — its "adjust
|
|
7
|
+
* foreign attributes" table — each to that namespace. Every other attribute it writes has none.
|
|
8
|
+
*
|
|
9
|
+
* A leaf, because the compiler reads it to know which built attribute needs the client's
|
|
10
|
+
* `addAttribute`, and the client reads it to write one.
|
|
11
|
+
*/
|
|
12
|
+
export const FOREIGN_ATTRIBUTES: ReadonlyMap<string, string> = new Map([
|
|
13
|
+
['xlink:actuate', XLINK],
|
|
14
|
+
['xlink:arcrole', XLINK],
|
|
15
|
+
['xlink:href', XLINK],
|
|
16
|
+
['xlink:role', XLINK],
|
|
17
|
+
['xlink:show', XLINK],
|
|
18
|
+
['xlink:title', XLINK],
|
|
19
|
+
['xlink:type', XLINK],
|
|
20
|
+
['xml:lang', XML],
|
|
21
|
+
['xml:space', XML],
|
|
22
|
+
['xmlns', XMLNS],
|
|
23
|
+
['xmlns:xlink', XMLNS],
|
|
24
|
+
])
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SVG and MathML elements whose children the HTML parser reads as HTML again — the
|
|
3
|
+
* integration points — less `annotation-xml`, which is one only by the encoding it declares.
|
|
4
|
+
*
|
|
5
|
+
* A leaf, because the client's parse and build read it (`namespaceUnder`) and the compiler reads
|
|
6
|
+
* it to know a built element below one is HTML's, whatever its parent's namespace.
|
|
7
|
+
*/
|
|
8
|
+
export const INTEGRATION_POINTS: ReadonlySet<string> = new Set([
|
|
9
|
+
'foreignObject',
|
|
10
|
+
'desc',
|
|
11
|
+
'title',
|
|
12
|
+
'mi',
|
|
13
|
+
'mo',
|
|
14
|
+
'mn',
|
|
15
|
+
'ms',
|
|
16
|
+
'mtext',
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
/** HTML's namespace: what the parser gives an element outside SVG and MathML, and below an integration point. */
|
|
20
|
+
export const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The origin a request made in process is addressed to, where no listener names a real one: an
|
|
2
|
+
// rpc's `raw` answered by the app this process holds, a CLI command asking the app it booted, and
|
|
3
|
+
// an `App.run` given a principal and no request (42.19). A leaf, because the server and the shared
|
|
4
|
+
// rpc both build one.
|
|
5
|
+
export const IN_PROCESS_ORIGIN = 'http://app.internal'
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
refuseBothCaps,
|
|
35
35
|
signalMoved,
|
|
36
36
|
startDeferred,
|
|
37
|
+
unwatchStore,
|
|
37
38
|
} from './Reactive.ts'
|
|
38
39
|
import {
|
|
39
40
|
detach,
|
|
@@ -46,6 +47,7 @@ import {
|
|
|
46
47
|
type Tracked,
|
|
47
48
|
trackProbe,
|
|
48
49
|
} from './tracking.ts'
|
|
50
|
+
import { owned, owning } from './watch.ts'
|
|
49
51
|
|
|
50
52
|
/**
|
|
51
53
|
* The keyed face: one `Reactive` per `Args` key, plus the probes and triggers over its entries.
|
|
@@ -262,27 +264,22 @@ class StartWindow {
|
|
|
262
264
|
}
|
|
263
265
|
|
|
264
266
|
/**
|
|
265
|
-
* `body`
|
|
266
|
-
*
|
|
267
|
-
* 11.27's LAST TWO STEPS as one body. The gate runs on every run of the entry — a refresh
|
|
268
|
-
* re-validates — and throws the refusal, which is what a body failing already does: the entry's
|
|
269
|
-
* `error` holds it and its read throws it (2.2), and the body the gate stands in front of is never
|
|
270
|
-
* called. What it accepts is what the body is handed (11.68).
|
|
267
|
+
* `body` handed what the `args` gate accepted for one entry (11.68), rather than the args it was
|
|
268
|
+
* reached with.
|
|
271
269
|
*
|
|
272
270
|
* A STREAM STAYS A STREAM FROM CONSTRUCTION. 9.6 reads the body's KIND, so the wrapper around an
|
|
273
271
|
* async generator is an async generator, or a gated keyed stream would be one only from its first
|
|
274
272
|
* run.
|
|
275
273
|
*/
|
|
276
|
-
function
|
|
274
|
+
function handed<Args>(
|
|
277
275
|
body: (args: Args) => unknown,
|
|
278
|
-
|
|
276
|
+
accepted: Args,
|
|
279
277
|
): (args: Args) => unknown {
|
|
280
|
-
const check = (args: Args): Args => checkArgs(gate, args)
|
|
281
278
|
if (isStreamBody(body))
|
|
282
|
-
return async function* (
|
|
283
|
-
yield* (body as (args: Args) => AsyncIterable<unknown>)(
|
|
279
|
+
return async function* () {
|
|
280
|
+
yield* (body as (args: Args) => AsyncIterable<unknown>)(accepted)
|
|
284
281
|
}
|
|
285
|
-
return (
|
|
282
|
+
return (): unknown => body(accepted)
|
|
286
283
|
}
|
|
287
284
|
|
|
288
285
|
/**
|
|
@@ -348,7 +345,7 @@ export function createKeyed<
|
|
|
348
345
|
(throttle ?? debounce) as number,
|
|
349
346
|
)
|
|
350
347
|
const lifetime = ttl === undefined ? null : new Lifetime(ttl)
|
|
351
|
-
const entryBody = heldOnce(
|
|
348
|
+
const entryBody = heldOnce(body)
|
|
352
349
|
// 11.27, 11.29 — the defaults are applied before the key is taken, so the entry is one for a
|
|
353
350
|
// call that wrote them and one that left them to the parameter. Read once, here: a body with
|
|
354
351
|
// none pays a `null` test per reach.
|
|
@@ -358,6 +355,12 @@ export function createKeyed<
|
|
|
358
355
|
// there were, so the key that starts loading next — search-as-you-type — never woke it. One
|
|
359
356
|
// signal per `Keyed`, moved where an entry is reached and where one is released.
|
|
360
357
|
const membership = newTracked()
|
|
358
|
+
// 8.21 — A `Keyed` BUILT IN A SETUP OR A REQUEST ends with it, and every store its entries
|
|
359
|
+
// watch is stopped then: the entries are the cache's, so no owner of their own stops them.
|
|
360
|
+
if (options?.store !== undefined && owning())
|
|
361
|
+
owned(() => {
|
|
362
|
+
for (const entry of entries.values()) unwatchStore(entry)
|
|
363
|
+
})
|
|
361
364
|
|
|
362
365
|
function askMembership(): void {
|
|
363
366
|
const reader = TRACKING.reader
|
|
@@ -372,6 +375,7 @@ export function createKeyed<
|
|
|
372
375
|
): void {
|
|
373
376
|
entries.delete(key)
|
|
374
377
|
leaveSelections(created)
|
|
378
|
+
unwatchStore(created)
|
|
375
379
|
signalMoved(membership)
|
|
376
380
|
}
|
|
377
381
|
|
|
@@ -385,9 +389,32 @@ export function createKeyed<
|
|
|
385
389
|
if (failedTransiently(standing)) standing.refresh()
|
|
386
390
|
return standing
|
|
387
391
|
}
|
|
392
|
+
// 11.27's VALIDATE, ONCE PER ENTRY AND BEFORE IT IS FILED: a refused `Args` is answered by a
|
|
393
|
+
// value holding the refusal (2.2) that no map keeps, so args nobody should have sent never
|
|
394
|
+
// take a place among the entries — on a `global` value, a place nothing but `invalidate`
|
|
395
|
+
// would take back (11.35). Built with none of the options: a `store` would restore a value
|
|
396
|
+
// over the refusal and watch for an entry nothing releases.
|
|
397
|
+
let run: (args: Args) => unknown = entryBody
|
|
398
|
+
if (gate !== undefined) {
|
|
399
|
+
let accepted: Args
|
|
400
|
+
try {
|
|
401
|
+
accepted = checkArgs(gate, args)
|
|
402
|
+
} catch (refusal) {
|
|
403
|
+
return createReactive<Stored, Accepted, Failures>(
|
|
404
|
+
`${label}(${key})`,
|
|
405
|
+
() => {
|
|
406
|
+
throw refusal
|
|
407
|
+
},
|
|
408
|
+
args,
|
|
409
|
+
undefined,
|
|
410
|
+
undefined,
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
run = handed(entryBody, accepted)
|
|
414
|
+
}
|
|
388
415
|
const created = createReactive<Stored, Accepted, Failures>(
|
|
389
416
|
`${label}(${key})`,
|
|
390
|
-
|
|
417
|
+
run as (args: unknown) => unknown,
|
|
391
418
|
args,
|
|
392
419
|
undefined,
|
|
393
420
|
entryOptions as ReactiveOptions<Accepted, Stored, Failures>,
|
|
@@ -75,6 +75,7 @@ export type Transformer<Accepted, Stored, Failures = never> = (
|
|
|
75
75
|
export type Store<Stored> = {
|
|
76
76
|
get: () => Stored | undefined | Promise<Stored | undefined>
|
|
77
77
|
set: (value: Stored, retention: { ttl: number }) => void | Promise<void>
|
|
78
|
+
watch?: (changed: () => void) => () => void
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
/**
|
|
@@ -366,6 +367,8 @@ type Persisted = {
|
|
|
366
367
|
* what `pending` sends next, and what 8.18 serves a re-read in place of calling `get`.
|
|
367
368
|
*/
|
|
368
369
|
unconfirmed: unknown
|
|
370
|
+
/** What stops the store's `watch`, where it has one (8.21). */
|
|
371
|
+
unwatch: (() => void) | null
|
|
369
372
|
}
|
|
370
373
|
|
|
371
374
|
/**
|
|
@@ -891,6 +894,7 @@ class Cell {
|
|
|
891
894
|
writing: false,
|
|
892
895
|
pending: false,
|
|
893
896
|
unconfirmed: undefined,
|
|
897
|
+
unwatch: null,
|
|
894
898
|
}
|
|
895
899
|
this.by = (options?.by ?? null) as
|
|
896
900
|
| string
|
|
@@ -3801,6 +3805,32 @@ function rereadStore(cell: Cell, persisted: Persisted): void {
|
|
|
3801
3805
|
)
|
|
3802
3806
|
}
|
|
3803
3807
|
|
|
3808
|
+
/**
|
|
3809
|
+
* Follows what the store says changed outside this process, re-reading it each time.
|
|
3810
|
+
*
|
|
3811
|
+
* 8.20 — A RE-READ, NEVER THE BODY: an edit made where the value lives is the store's news, so it
|
|
3812
|
+
* is read back as a refresh of a value whose only producer is its store would read it, and 8.18
|
|
3813
|
+
* still has a write the app made stand over it. 8.21 — stopped with the value: by its owner here,
|
|
3814
|
+
* and by its `Keyed` through `unwatchStore` where an entry is evicted.
|
|
3815
|
+
*/
|
|
3816
|
+
function watchStore(cell: Cell, persisted: Persisted, cached: boolean): void {
|
|
3817
|
+
const watch = persisted.store.watch
|
|
3818
|
+
if (watch === undefined) return
|
|
3819
|
+
persisted.unwatch = watch(() => {
|
|
3820
|
+
rereadStore(cell, persisted)
|
|
3821
|
+
})
|
|
3822
|
+
if (!cached && owning()) owned(() => unwatchStore(cell.callable))
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
/** Stops the store's `watch` on `value`, once (8.21). */
|
|
3826
|
+
export function unwatchStore(value: unknown): void {
|
|
3827
|
+
const persisted = (value as Callable)[CELL].store
|
|
3828
|
+
if (persisted === null || persisted.unwatch === null) return
|
|
3829
|
+
const unwatch = persisted.unwatch
|
|
3830
|
+
persisted.unwatch = null
|
|
3831
|
+
unwatch()
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3804
3834
|
/**
|
|
3805
3835
|
* Lands a store re-read, letting an unconfirmed write win.
|
|
3806
3836
|
*
|
|
@@ -4693,8 +4723,10 @@ export function createReactive<
|
|
|
4693
4723
|
// (2.2, 4.14) rather than serving an `undefined` its own type says is a `Stored`.
|
|
4694
4724
|
// 8.3 AND 8.14: a store answers first, where the value is constructed, and whatever it misses
|
|
4695
4725
|
// falls back to what the value would have done without one.
|
|
4696
|
-
if (cell.store !== null)
|
|
4697
|
-
|
|
4726
|
+
if (cell.store !== null) {
|
|
4727
|
+
restoreFirst(cell, cell.store, initial)
|
|
4728
|
+
watchStore(cell, cell.store, cached)
|
|
4729
|
+
} else if (body === null) {
|
|
4698
4730
|
if (initial === undefined) landOpening(cell)
|
|
4699
4731
|
else if (initial !== NO_INITIAL) open(cell, initial)
|
|
4700
4732
|
} else if (deferred) cell.loading = true
|
package/src/shared/rpc.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { type Failed, isFailedNamed } from './Failed.ts'
|
|
17
17
|
import { GLOBAL_GETS } from './GLOBAL_GETS.ts'
|
|
18
18
|
import { HandlerRoute } from './HandlerRoute.ts'
|
|
19
|
+
import { IN_PROCESS_ORIGIN } from './IN_PROCESS_ORIGIN.ts'
|
|
19
20
|
import { compose, type Middleware } from './Middleware.ts'
|
|
20
21
|
import { principalRotated } from './principalRotated.ts'
|
|
21
22
|
import { RPC_SERVER } from './RPC_SERVER.ts'
|
|
@@ -442,7 +443,7 @@ export function declare(method: Method): Declare {
|
|
|
442
443
|
if (RPC_SERVER.raw !== null)
|
|
443
444
|
return RPC_SERVER.raw(
|
|
444
445
|
new Request(
|
|
445
|
-
new URL(asked.url,
|
|
446
|
+
new URL(asked.url, IN_PROCESS_ORIGIN),
|
|
446
447
|
given,
|
|
447
448
|
),
|
|
448
449
|
)
|
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
|
@@ -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,7 +58,7 @@ 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
63
|
export { setClassGroup } from './setClassGroup.ts'
|
|
64
64
|
export { setMarkup } from './setMarkup.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
|
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
|
}
|