@brust/runtime-dom 2.0.0-alpha.13 → 2.0.0-alpha.14
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/package.json +1 -1
- package/src/attr-kinds.ts +6 -0
- package/src/directives/bind.ts +6 -3
- package/src/instance.ts +4 -3
- package/src/registry.ts +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// GENERATED from crates/brust-jinja/src/lib.rs (BOOLEAN_ATTRS, OVERLOADED_ATTRS, STRINGIFY_ATTRS); do not edit.
|
|
2
|
+
// Regenerate: bun scripts/gen-attr-kinds.ts. test/attr-kinds.test.ts fails when this drifts.
|
|
3
|
+
const set = (s: string): Set<string> => new Set(s.split(' '))
|
|
4
|
+
export const BOOLEAN = set('allowfullscreen async autofocus autoplay checked controls default defer disabled disablepictureinpicture disableremoteplayback formnovalidate hidden inert itemscope loop multiple muted nomodule novalidate open playsinline readonly required reversed scoped seamless selected')
|
|
5
|
+
export const OVERLOADED = set('capture download')
|
|
6
|
+
export const STRINGIFY = set('autoreverse contenteditable draggable externalresourcesrequired focusable preservealpha spellcheck value')
|
package/src/directives/bind.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { BINDERS, parsed, read, warnOnce } from '../core-api'
|
|
2
|
+
import { BOOLEAN, OVERLOADED, STRINGIFY } from '../attr-kinds'
|
|
2
3
|
|
|
3
4
|
// Attributes that execute or load code from a string; a bound value must never reach them as-is.
|
|
4
5
|
const URL_ATTRS = new Set(['href', 'src', 'action', 'formaction', 'poster', 'data', 'xlink:href', 'ping'])
|
|
5
6
|
// Scheme allowlist on the parsed URL (relative URLs resolve to http): new URL() applies the browser's tab/newline/control-char stripping.
|
|
6
7
|
const safeUrl = (v: string): boolean => { try { return ['http:', 'https:', 'mailto:', 'tel:'].includes(new URL(v, 'http://x').protocol) } catch { return false } }
|
|
7
|
-
const BOOLEAN = new Set(['disabled', 'checked', 'selected', 'readonly', 'required', 'hidden', 'open', 'multiple'])
|
|
8
8
|
BINDERS.push({
|
|
9
9
|
match: (a) => a.startsWith('x-bind-'),
|
|
10
10
|
bind: ({ inst, el, attr, raw, scope }) => {
|
|
@@ -23,9 +23,12 @@ BINDERS.push({
|
|
|
23
23
|
if (on) el.setAttribute(name, '') ; else el.removeAttribute(name)
|
|
24
24
|
return
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
// aria-*, data-* and the booleanish strings print `true`/`false`; download/capture print `true` as ``; on any other attribute React omits a boolean.
|
|
27
|
+
if (name.startsWith('aria-') || name.startsWith('data-') || STRINGIFY.has(name)) { if (v == null) el.removeAttribute(name); else el.setAttribute(name, String(v)); return }
|
|
28
|
+
if (OVERLOADED.has(name) && typeof v === 'boolean') { if (v) el.setAttribute(name, ''); else el.removeAttribute(name); return }
|
|
29
|
+
if (v == null || typeof v === 'boolean') { el.removeAttribute(name); return }
|
|
27
30
|
// React writes no `src=""` / `href=""` (it would load the page again); an anchor keeps its empty href.
|
|
28
|
-
if (v === '' && (name === 'src' || name === 'href') && el.
|
|
31
|
+
if (v === '' && (name === 'src' || name === 'href') && el.localName !== 'a') { el.removeAttribute(name); return }
|
|
29
32
|
if (URL_ATTRS.has(name) && !safeUrl(String(v))) { warnOnce(`b:${inst.name}:${name}`, `${attr} in ${inst.name}: unsafe URL refused`); return }
|
|
30
33
|
el.setAttribute(name, String(v))
|
|
31
34
|
})
|
package/src/instance.ts
CHANGED
|
@@ -58,13 +58,14 @@ export class Instance {
|
|
|
58
58
|
return true
|
|
59
59
|
}
|
|
60
60
|
onCleanup(fn: () => void): void { this.cleanups.push(fn) }
|
|
61
|
-
ref<E extends Element = HTMLElement>(name: string): { current: E | null } {
|
|
61
|
+
ref<E extends Element = HTMLElement>(name: string, init?: unknown): { current: E | null } {
|
|
62
62
|
let r = this.refs.get(name)
|
|
63
|
-
|
|
63
|
+
// `useRef(init)`: the cell starts at `init`; an `x-ref` element replaces it when it binds (React attaches the same way).
|
|
64
|
+
if (!r) { r = { current: (init ?? null) as Element | null }; this.refs.set(name, r) }
|
|
64
65
|
return r as { current: E | null }
|
|
65
66
|
}
|
|
66
67
|
ctx(): BehaviorCtx {
|
|
67
|
-
return { el: this.host, props: this.props, effect: (fn) => this.componentEffect(fn), onCleanup: (fn) => this.onCleanup(fn), ref: (n) => this.ref(n) }
|
|
68
|
+
return { el: this.host, props: this.props, effect: (fn) => this.componentEffect(fn), onCleanup: (fn) => this.onCleanup(fn), ref: (n, init) => this.ref(n, init) }
|
|
68
69
|
}
|
|
69
70
|
init(factory: BehaviorFactory): void {
|
|
70
71
|
const m = factory(this.ctx())
|
package/src/registry.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface BehaviorCtx {
|
|
|
5
5
|
props: Signal<Record<string, unknown>>
|
|
6
6
|
effect: (fn: () => void | (() => void)) => Dispose
|
|
7
7
|
onCleanup: (fn: () => void) => void
|
|
8
|
-
ref: <E extends Element = HTMLElement>(name: string) => { current: E | null }
|
|
8
|
+
ref: <E extends Element = HTMLElement>(name: string, init?: unknown) => { current: E | null }
|
|
9
9
|
}
|
|
10
10
|
export type BehaviorInstance = Record<string, unknown>
|
|
11
11
|
export type BehaviorFactory = (ctx: BehaviorCtx) => BehaviorInstance | void
|