@reticlehq/browser 2.13.1 → 3.1.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/dist/actions/actions-dom.d.ts +8 -20
- package/dist/actions/actions-dom.js +70 -69
- package/dist/actions/actions-press.d.ts +14 -0
- package/dist/actions/actions-press.js +123 -0
- package/dist/actions/actions.d.ts +1 -89
- package/dist/actions/actions.js +64 -365
- package/dist/actions/appeared-text.d.ts +0 -20
- package/dist/actions/appeared-text.js +6 -36
- package/dist/actions/danger-context.d.ts +3 -0
- package/dist/actions/danger-context.js +39 -0
- package/dist/actions/scroll.d.ts +8 -10
- package/dist/actions/scroll.js +28 -17
- package/dist/actions/synthetic/synthetic-input.d.ts +2 -0
- package/dist/actions/synthetic/synthetic-input.js +13 -0
- package/dist/actions/value-input.d.ts +0 -30
- package/dist/actions/value-input.js +0 -33
- package/dist/commands/commands.d.ts +0 -2
- package/dist/commands/commands.js +10 -70
- package/dist/connect-options.d.ts +2 -96
- package/dist/dom/a11y.d.ts +0 -29
- package/dist/dom/a11y.js +3 -90
- package/dist/dom/addressing/auto-anchor.d.ts +25 -0
- package/dist/dom/addressing/auto-anchor.js +41 -0
- package/dist/dom/addressing/refs.d.ts +14 -0
- package/dist/dom/addressing/refs.js +114 -0
- package/dist/dom/addressing/source.d.ts +9 -0
- package/dist/dom/addressing/source.js +29 -0
- package/dist/dom/chart.d.ts +0 -61
- package/dist/dom/chart.js +0 -60
- package/dist/dom/desktop-capture.d.ts +0 -30
- package/dist/dom/desktop-capture.js +1 -73
- package/dist/dom/dom-ignore.d.ts +0 -30
- package/dist/dom/dom-ignore.js +1 -48
- package/dist/dom/occlusion.d.ts +0 -7
- package/dist/dom/occlusion.js +31 -12
- package/dist/dom/paint-context.d.ts +6 -0
- package/dist/dom/paint-context.js +38 -0
- package/dist/dom/query.d.ts +0 -14
- package/dist/dom/query.js +50 -216
- package/dist/dom/realm.d.ts +0 -20
- package/dist/dom/realm.js +0 -28
- package/dist/dom/shadow-registry.d.ts +0 -33
- package/dist/dom/shadow-registry.js +2 -72
- package/dist/dom/snapshot.d.ts +2 -43
- package/dist/dom/snapshot.js +27 -152
- package/dist/dom/theme.d.ts +0 -37
- package/dist/dom/theme.js +2 -52
- package/dist/dom/virtualized.d.ts +0 -17
- package/dist/dom/virtualized.js +0 -27
- package/dist/edit-epoch.d.ts +1 -23
- package/dist/edit-epoch.js +1 -53
- package/dist/index.d.ts +4 -5
- package/dist/index.js +4 -28
- package/dist/observers/animation.d.ts +0 -1
- package/dist/observers/animation.js +1 -4
- package/dist/observers/blind-spots.d.ts +0 -26
- package/dist/observers/blind-spots.js +3 -62
- package/dist/observers/console.d.ts +1 -2
- package/dist/observers/console.js +41 -30
- package/dist/observers/context-open.d.ts +1 -11
- package/dist/observers/context-open.js +10 -14
- package/dist/observers/departure.d.ts +3 -0
- package/dist/observers/departure.js +52 -0
- package/dist/observers/dialogs.d.ts +2 -0
- package/dist/observers/dialogs.js +39 -0
- package/dist/observers/dom.js +28 -52
- package/dist/observers/download.d.ts +1 -2
- package/dist/observers/download.js +39 -56
- package/dist/observers/focus.d.ts +0 -6
- package/dist/observers/focus.js +0 -7
- package/dist/observers/frames.d.ts +0 -9
- package/dist/observers/frames.js +1 -28
- package/dist/observers/health.d.ts +0 -5
- package/dist/observers/health.js +2 -34
- package/dist/observers/install-all.d.ts +2 -0
- package/dist/observers/install-all.js +26 -27
- package/dist/observers/ipc.d.ts +0 -25
- package/dist/observers/ipc.js +0 -65
- package/dist/observers/navigation.d.ts +2 -0
- package/dist/observers/navigation.js +33 -0
- package/dist/observers/net-detail/network-body.d.ts +12 -0
- package/dist/observers/net-detail/network-body.js +71 -0
- package/dist/observers/net-detail/network-redact.d.ts +1 -0
- package/dist/observers/net-detail/network-redact.js +1 -0
- package/dist/observers/network-stream.d.ts +0 -21
- package/dist/observers/network-stream.js +3 -46
- package/dist/observers/network.d.ts +3 -27
- package/dist/observers/network.js +178 -295
- package/dist/observers/perf.d.ts +0 -10
- package/dist/observers/perf.js +0 -16
- package/dist/observers/route.d.ts +1 -2
- package/dist/observers/route.js +13 -19
- package/dist/observers/scroll.d.ts +0 -1
- package/dist/observers/scroll.js +2 -11
- package/dist/observers/sdk-failure.d.ts +1 -9
- package/dist/observers/sdk-failure.js +1 -30
- package/dist/observers/state.d.ts +1 -11
- package/dist/observers/state.js +13 -41
- package/dist/observers/storage.d.ts +0 -14
- package/dist/observers/storage.js +10 -34
- package/dist/observers/types.d.ts +0 -15
- package/dist/observers/types.js +0 -14
- package/dist/patching/capture-method.d.ts +0 -24
- package/dist/patching/capture-method.js +0 -26
- package/dist/presenter/chrome/layers.d.ts +2 -0
- package/dist/presenter/chrome/layers.js +2 -0
- package/dist/presenter/chrome/log-kinds.d.ts +12 -0
- package/dist/presenter/chrome/log-kinds.js +7 -0
- package/dist/presenter/{overlay.d.ts → chrome/overlay.d.ts} +0 -1
- package/dist/presenter/{overlay.js → chrome/overlay.js} +0 -1
- package/dist/presenter/chrome/presenter-brand.d.ts +3 -0
- package/dist/presenter/{presenter-brand.js → chrome/presenter-brand.js} +0 -21
- package/dist/presenter/chrome/presenter-effects.d.ts +4 -0
- package/dist/presenter/{presenter-effects.js → chrome/presenter-effects.js} +1 -10
- package/dist/presenter/chrome/presenter-hud-chrome.d.ts +7 -0
- package/dist/presenter/{presenter-hud-chrome.js → chrome/presenter-hud-chrome.js} +0 -10
- package/dist/presenter/chrome/presenter-log.d.ts +17 -0
- package/dist/presenter/{presenter-log.js → chrome/presenter-log.js} +5 -54
- package/dist/presenter/chrome/presenter-report-copy.d.ts +51 -0
- package/dist/presenter/chrome/presenter-report-copy.js +95 -0
- package/dist/presenter/chrome/presenter-safe-html.d.ts +2 -0
- package/dist/presenter/chrome/presenter-safe-html.js +16 -0
- package/dist/presenter/{presenter-tally.d.ts → chrome/presenter-tally.d.ts} +0 -5
- package/dist/presenter/{presenter-tally.js → chrome/presenter-tally.js} +1 -5
- package/dist/presenter/{presenter-verbs.js → chrome/presenter-verbs.js} +0 -7
- package/dist/presenter/{presenter-heroicons-data.d.ts → icons/presenter-heroicons-data.d.ts} +1 -8
- package/dist/presenter/{presenter-heroicons-data.js → icons/presenter-heroicons-data.js} +1 -8
- package/dist/presenter/{presenter-icons.d.ts → icons/presenter-icons.d.ts} +1 -6
- package/dist/presenter/{presenter-icons.js → icons/presenter-icons.js} +1 -6
- package/dist/presenter/presenter-account-styles.d.ts +1 -0
- package/dist/presenter/presenter-account-styles.js +99 -0
- package/dist/presenter/presenter-account.d.ts +40 -0
- package/dist/presenter/presenter-account.js +178 -0
- package/dist/presenter/presenter-config.d.ts +10 -107
- package/dist/presenter/presenter-config.js +5 -92
- package/dist/presenter/presenter-controls.d.ts +1 -35
- package/dist/presenter/presenter-controls.js +10 -133
- package/dist/presenter/presenter-dock-layout.d.ts +0 -3
- package/dist/presenter/presenter-dock-layout.js +1 -5
- package/dist/presenter/presenter-drag.d.ts +2 -42
- package/dist/presenter/presenter-drag.js +3 -76
- package/dist/presenter/presenter-glow.d.ts +0 -21
- package/dist/presenter/presenter-glow.js +2 -28
- package/dist/presenter/presenter-hud-position.d.ts +18 -0
- package/dist/presenter/presenter-hud-position.js +50 -0
- package/dist/presenter/presenter-report-styles.d.ts +0 -6
- package/dist/presenter/presenter-report-styles.js +22 -7
- package/dist/presenter/presenter-report.d.ts +5 -7
- package/dist/presenter/presenter-report.js +44 -95
- package/dist/presenter/presenter-run-state.d.ts +1 -8
- package/dist/presenter/presenter-run-state.js +1 -6
- package/dist/presenter/presenter-settings-styles.d.ts +0 -1
- package/dist/presenter/presenter-settings-styles.js +2 -2
- package/dist/presenter/presenter-settings.d.ts +5 -23
- package/dist/presenter/presenter-settings.js +21 -33
- package/dist/presenter/presenter-shell-styles.js +23 -20
- package/dist/presenter/presenter-shell.d.ts +6 -18
- package/dist/presenter/presenter-shell.js +26 -77
- package/dist/presenter/presenter-styles.d.ts +2 -10
- package/dist/presenter/presenter-styles.js +6 -12
- package/dist/presenter/presenter-workspace.d.ts +4 -4
- package/dist/presenter/presenter-workspace.js +30 -10
- package/dist/presenter/presenter.d.ts +3 -63
- package/dist/presenter/presenter.js +29 -132
- package/dist/presenter/tour/tour-view.d.ts +33 -0
- package/dist/presenter/tour/tour-view.js +188 -0
- package/dist/presenter/tour/tour.d.ts +25 -0
- package/dist/presenter/tour/tour.js +245 -0
- package/dist/recorder/recorder-styles.d.ts +0 -6
- package/dist/recorder/recorder-styles.js +0 -6
- package/dist/recorder/recorder.d.ts +0 -34
- package/dist/recorder/recorder.js +6 -49
- package/dist/registry/auto-testids.d.ts +0 -19
- package/dist/registry/auto-testids.js +1 -21
- package/dist/registry/capabilities.d.ts +0 -31
- package/dist/registry/capabilities.js +1 -46
- package/dist/registry/commit-and-signal.d.ts +0 -6
- package/dist/registry/commit-and-signal.js +1 -7
- package/dist/registry/declared-channels.d.ts +2 -0
- package/dist/registry/declared-channels.js +14 -0
- package/dist/registry/declared-platform.d.ts +4 -0
- package/dist/registry/declared-platform.js +34 -0
- package/dist/registry/domains.d.ts +0 -8
- package/dist/registry/domains.js +0 -7
- package/dist/registry/emitter.d.ts +0 -14
- package/dist/registry/emitter.js +1 -11
- package/dist/registry/store-adapters.d.ts +0 -186
- package/dist/registry/store-adapters.js +1 -164
- package/dist/registry/{adapters.d.ts → stores/adapters.d.ts} +0 -10
- package/dist/registry/{adapters.js → stores/adapters.js} +0 -10
- package/dist/registry/stores.d.ts +0 -33
- package/dist/registry/stores.js +1 -60
- package/dist/reticle-presenter-helpers.d.ts +0 -13
- package/dist/reticle-presenter-helpers.js +10 -20
- package/dist/reticle-singleton.d.ts +2 -0
- package/dist/reticle-singleton.js +3 -0
- package/dist/reticle.d.ts +1 -52
- package/dist/reticle.js +115 -180
- package/dist/review/annotator-styles.js +1 -8
- package/dist/review/annotator.d.ts +2 -13
- package/dist/review/annotator.js +2 -42
- package/dist/review/mark-anchor.d.ts +0 -11
- package/dist/review/mark-anchor.js +3 -11
- package/dist/security/serialization.d.ts +0 -20
- package/dist/security/serialization.js +1 -66
- package/dist/session-continuity.d.ts +0 -16
- package/dist/session-continuity.js +1 -20
- package/dist/timers/clock.d.ts +1 -14
- package/dist/timers/clock.js +59 -60
- package/dist/timers/console/native-console.js +5 -0
- package/dist/timers/native/native-timers.d.ts +11 -0
- package/dist/timers/native/native-timers.js +48 -0
- package/dist/transport/transport.d.ts +0 -51
- package/dist/transport/transport.js +5 -131
- package/dist/transport/unreachable-message.d.ts +0 -17
- package/dist/transport/unreachable-message.js +0 -17
- package/dist/util/captured-method.d.ts +0 -35
- package/dist/util/captured-method.js +0 -35
- package/package.json +5 -4
- package/dist/actions/synthetic-input.d.ts +0 -9
- package/dist/actions/synthetic-input.js +0 -40
- package/dist/dom/auto-anchor.d.ts +0 -65
- package/dist/dom/auto-anchor.js +0 -74
- package/dist/dom/refs.d.ts +0 -90
- package/dist/dom/refs.js +0 -245
- package/dist/dom/source.d.ts +0 -53
- package/dist/dom/source.js +0 -74
- package/dist/observers/network-body.d.ts +0 -35
- package/dist/observers/network-body.js +0 -168
- package/dist/observers/network-redact.d.ts +0 -20
- package/dist/observers/network-redact.js +0 -80
- package/dist/presenter/presenter-brand.d.ts +0 -22
- package/dist/presenter/presenter-effects.d.ts +0 -13
- package/dist/presenter/presenter-hud-chrome.d.ts +0 -17
- package/dist/presenter/presenter-log.d.ts +0 -64
- package/dist/presenter/presenter-report-copy.d.ts +0 -112
- package/dist/presenter/presenter-report-copy.js +0 -156
- package/dist/presenter/presenter-test-helpers.d.ts +0 -16
- package/dist/presenter/presenter-test-helpers.js +0 -41
- package/dist/timers/native-console.js +0 -9
- package/dist/timers/native-timers.d.ts +0 -36
- package/dist/timers/native-timers.js +0 -78
- /package/dist/presenter/{presenter-verbs.d.ts → chrome/presenter-verbs.d.ts} +0 -0
- /package/dist/timers/{native-console.d.ts → console/native-console.d.ts} +0 -0
|
@@ -1,120 +1,26 @@
|
|
|
1
1
|
import type { RedactionConfig } from '@reticlehq/core';
|
|
2
|
-
/**
|
|
3
|
-
* What `reticle.connect()` accepts.
|
|
4
|
-
*
|
|
5
|
-
* Split out of reticle.ts when that file crossed the 600-line cap - the same move `session-info.ts`
|
|
6
|
-
* made for the server's Session. A pure description of the SDK's public entry contract has no reason
|
|
7
|
-
* to live inside the class that happens to consume it, and separating them keeps the surface every
|
|
8
|
-
* integrating app actually reads reviewable on its own.
|
|
9
|
-
*/
|
|
10
2
|
export interface ReticleConnectOptions {
|
|
11
|
-
/** WS endpoint of the local bridge. Defaults to ws://localhost:<port><path>. */
|
|
12
3
|
url?: string;
|
|
13
|
-
/** Human-friendly session label so the agent can target the right tab. */
|
|
14
4
|
session?: string;
|
|
15
|
-
/**
|
|
16
|
-
* Stable project identity, normally stamped by the build plugin (e.g. "acme-web-9f3c1d"). Lets the
|
|
17
|
-
* agent scope to the right app even when its dev server boots on an unexpected port. Optional.
|
|
18
|
-
*/
|
|
19
5
|
projectId?: string;
|
|
20
|
-
/**
|
|
21
|
-
* Browser/bridge pairing token. Required whenever the daemon has one, which is the ordinary case:
|
|
22
|
-
* it auto-provisions `~/.reticle/pairing-token` on first start and then refuses a hello without it,
|
|
23
|
-
* including on localhost. Non-localhost additionally needs `allowNonLocalhost`.
|
|
24
|
-
*/
|
|
25
6
|
token?: string;
|
|
26
|
-
/** Explicitly allow Reticle on a non-localhost page or bridge. Requires token. */
|
|
27
7
|
allowNonLocalhost?: boolean;
|
|
28
|
-
/**
|
|
29
|
-
* Escape hatch for the production backstop. Reticle is dev-only and refuses to connect when the
|
|
30
|
-
* build reports NODE_ENV=production (an SSR healthcheck or a prod bundle opened locally would
|
|
31
|
-
* otherwise activate). The real fix is to gate the import behind `import.meta.env.DEV` so it's
|
|
32
|
-
* tree-shaken out; this flag only exists for the rare intentional prod diagnostic.
|
|
33
|
-
*/
|
|
34
8
|
allowInProduction?: boolean;
|
|
35
|
-
/** Show a small in-page status chip (connection + event count). */
|
|
36
9
|
overlay?: boolean;
|
|
37
|
-
/**
|
|
38
|
-
* Capture request/response bodies on net.request events (dev-only; text-like content only,
|
|
39
|
-
* sensitive keys redacted, per-body capped). Off by default - bodies cost tokens and can carry PII.
|
|
40
|
-
*/
|
|
41
10
|
captureNetworkBodies?: boolean;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*
|
|
45
|
-
* The presenter is hidden from every tool by design, for a good reason: an agent that can drive
|
|
46
|
-
* Reticle's own interface can fabricate its own impact report, and Reticle chrome in a snapshot is
|
|
47
|
-
* noise in every other app on earth.
|
|
48
|
-
*
|
|
49
|
-
* The cost of that rule is that a HUD change is the only kind of change Reticle cannot be used to
|
|
50
|
-
* check — the panel rendering it is invisible to everything that could look at it. This hatch
|
|
51
|
-
* exists for exactly that case: the app under test IS Reticle. It reports itself in the app's
|
|
52
|
-
* capabilities, so a verdict drawn with it open can never be mistaken for an ordinary one.
|
|
53
|
-
*/
|
|
11
|
+
sourceMapping?: boolean;
|
|
12
|
+
captureErrorBodies?: boolean;
|
|
54
13
|
exposePresenter?: boolean;
|
|
55
|
-
/**
|
|
56
|
-
* The project root, so source paths report repo-relative instead of absolute.
|
|
57
|
-
*
|
|
58
|
-
* React's `_debugSource.fileName` is absolute; the babel stamp is repo-relative. Build plugins that
|
|
59
|
-
* can `define` a global set it themselves; frameworks that cannot (Next passes it through env,
|
|
60
|
-
* Astro through its own config) hand it here instead. Taking it as a connect OPTION is what keeps
|
|
61
|
-
* the generated app-side code plain JavaScript - the previous shape made the caller assign a global,
|
|
62
|
-
* and the TypeScript cast that needed shipped into a `.jsx` file and broke the build.
|
|
63
|
-
*/
|
|
64
14
|
root?: string;
|
|
65
|
-
/**
|
|
66
|
-
* The SDK's own package version, so a version-skewed pair against the daemon can name itself
|
|
67
|
-
* rather than surfacing as a bare `-32000`. Build plugins supply it automatically; pass it here
|
|
68
|
-
* only for a hand-wired connect that has no plugin.
|
|
69
|
-
*/
|
|
70
15
|
sdkVersion?: string;
|
|
71
|
-
/** Presenter mode: glow border, animated cursor, click/hover effects, narration HUD. */
|
|
72
16
|
present?: boolean;
|
|
73
|
-
/** Per-action pacing (ms) in presenter mode so a human can follow. Default 450. */
|
|
74
17
|
pace?: number;
|
|
75
|
-
/** Min ms each narration line stays visible before the next replaces it (presenter). Default 3000. */
|
|
76
18
|
narrationDwellMs?: number;
|
|
77
|
-
/**
|
|
78
|
-
* Border behavior in presenter mode: 'session' (default) persists the border for the whole
|
|
79
|
-
* session; 'busy' restores the fade-after-idle behavior.
|
|
80
|
-
*/
|
|
81
19
|
border?: 'session' | 'busy';
|
|
82
|
-
/** Max accumulated activity-log rows before the oldest are pruned (presenter). Default 50. */
|
|
83
20
|
logMax?: number;
|
|
84
|
-
/**
|
|
85
|
-
* Mount the floating human-recorder toolbar (Record/Stop/Annotate).
|
|
86
|
-
* Default off - purely additive, dev-only.
|
|
87
|
-
*/
|
|
88
21
|
recorder?: boolean;
|
|
89
|
-
/**
|
|
90
|
-
* Mount the page annotator: expanding the HUD enters click-to-annotate mode, and Reticle emits
|
|
91
|
-
* a HUMAN_MARK the agent drains via reticle_review. Defaults to ON with the presenter; pass
|
|
92
|
-
* `annotate: false` to suppress.
|
|
93
|
-
*/
|
|
94
22
|
annotate?: boolean;
|
|
95
|
-
/** Live-control: overridable ended-border fade delay (native timer). Default 4000. */
|
|
96
23
|
endedFadeMs?: number;
|
|
97
|
-
/** Session auto-end after this much agent idle (presenter). Default 5min; agent-tunable via reticle_session. */
|
|
98
24
|
idleEndMs?: number;
|
|
99
|
-
/**
|
|
100
|
-
* Extend the redaction rules with your app's own vocabulary.
|
|
101
|
-
*
|
|
102
|
-
* ```ts
|
|
103
|
-
* reticle.connect({
|
|
104
|
-
* redact: {
|
|
105
|
-
* keys: ['deviceSecretRef', /^partner[-_]?code$/i], // also redact these
|
|
106
|
-
* allow: ['designToken'], // stop redacting this false positive
|
|
107
|
-
* },
|
|
108
|
-
* });
|
|
109
|
-
* ```
|
|
110
|
-
*
|
|
111
|
-
* Additive only - there is no way to replace the built-in rule, because a config that could would
|
|
112
|
-
* eventually ship an app that leaks. `keys` strings match a key name exactly (case-insensitively);
|
|
113
|
-
* a RegExp is tested against it. `allow` exempts a key from the DEFAULT rule and loses to `keys`.
|
|
114
|
-
*
|
|
115
|
-
* Literal `keys` strings also cross the bridge, so the daemon redacts them on the driven path,
|
|
116
|
-
* where request bodies are captured raw from the network stack and never pass through this SDK.
|
|
117
|
-
* RegExp entries and `allow` do NOT cross - see docs/usage.md ("Extending the redaction rules") for why, and what that means.
|
|
118
|
-
*/
|
|
119
25
|
redact?: RedactionConfig;
|
|
120
26
|
}
|
package/dist/dom/a11y.d.ts
CHANGED
|
@@ -1,38 +1,9 @@
|
|
|
1
1
|
import { ElementState, type ElementDescriptor } from '@reticlehq/core';
|
|
2
|
-
/** Compute the ARIA role (explicit wins, else implicit from the tag). */
|
|
3
2
|
export declare function getRole(el: Element): string;
|
|
4
3
|
export declare function getAccessibleName(el: Element): string;
|
|
5
|
-
/**
|
|
6
|
-
* The set of states relevant to assertions. `visible` is an O(depth) forced-style walk; callers that
|
|
7
|
-
* already computed it (describe) pass it in so it isn't resolved twice per element.
|
|
8
|
-
*/
|
|
9
4
|
export declare function getStates(el: Element, visible?: boolean): ElementState[];
|
|
10
|
-
/**
|
|
11
|
-
* True when a form field holds a secret the SDK must never capture verbatim — a password input, a
|
|
12
|
-
* sensitive `autocomplete` (cc-number, one-time-code, …), or a name/id/testid/aria-label that trips
|
|
13
|
-
* `isSensitiveKey`. The single source of truth for both live-snapshot redaction (`getValue`) and the
|
|
14
|
-
* flow recorder's fill-value redaction, so recorded flows never persist typed passwords/OTPs/keys.
|
|
15
|
-
*/
|
|
16
5
|
export declare function isSensitiveField(el: Element): boolean;
|
|
17
6
|
export declare function getValue(el: Element): string | undefined;
|
|
18
|
-
/**
|
|
19
|
-
* Whether the element is actually visible (not display:none/hidden/aria-hidden/opacity:0), walking to
|
|
20
|
-
* root. This is an O(depth) forced-style walk PER node; `memo` (optional, scoped to ONE synchronous
|
|
21
|
-
* query pass) caches the full inherited result per element so a broad state-filtered query stops
|
|
22
|
-
* re-resolving getComputedStyle up the same ancestor chain for every sibling. Sound because the DOM is
|
|
23
|
-
* static for the pass's duration — the cache MUST be a per-call Map, never module-level (that would go
|
|
24
|
-
* stale the instant the app mutates, the same trap the shadow-root note in query.ts documents).
|
|
25
|
-
*/
|
|
26
|
-
/**
|
|
27
|
-
* True when the element is inside the viewport right now: visible AND its bounding box intersects
|
|
28
|
-
* the window. This is what makes a scroll assertable (#398) — content below the fold of a scrolling
|
|
29
|
-
* container is `visible`/`present` before any scroll, so only a viewport-intersection check can tell
|
|
30
|
-
* "scrolled into view" from "was always in the DOM". Uses getBoundingClientRect (viewport-relative,
|
|
31
|
-
* already accounts for scroll position), not an IntersectionObserver, so it stays synchronous inside
|
|
32
|
-
* the predicate pass.
|
|
33
|
-
*/
|
|
34
7
|
export declare function isInViewport(el: Element, memo?: Map<Element, boolean>): boolean;
|
|
35
8
|
export declare function isVisible(el: Element, memo?: Map<Element, boolean>): boolean;
|
|
36
|
-
/** Build the compact descriptor surfaced to the agent. `memo` (optional) shares the per-call
|
|
37
|
-
* visibility cache with the query's state filter so ancestors aren't re-walked per element. */
|
|
38
9
|
export declare function describe(el: Element, memo?: Map<Element, boolean>): ElementDescriptor;
|
package/dist/dom/a11y.js
CHANGED
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
import { ElementState, REDACTED_VALUE } from '@reticlehq/core';
|
|
2
2
|
import { isButton, isHtmlElement, isImage, isInput, isSelect, isTextArea } from './realm.js';
|
|
3
|
-
import { refs } from './refs.js';
|
|
3
|
+
import { refs } from './addressing/refs.js';
|
|
4
4
|
import { inspectChart } from './chart.js';
|
|
5
5
|
import { isSensitiveKey } from '../security/serialization.js';
|
|
6
|
-
import { formatSource, sourceFromDom } from './source.js';
|
|
7
|
-
/**
|
|
8
|
-
* Roles whose accessible name comes from their text content (ARIA's `nameFrom: author content`).
|
|
9
|
-
*
|
|
10
|
-
* `radio`, `checkbox`, `row` and `tooltip` were missing, and the gap is not cosmetic: a segmented
|
|
11
|
-
* filter written as `<button role="radio">held</button>` — an extremely ordinary design-system
|
|
12
|
-
* control — reported as a nameless `radio`, so `by: role` + name could not address it at all and an
|
|
13
|
-
* agent had to fall back to a testid the app has no reason to carry. Measured on a shipments console:
|
|
14
|
-
* six filters, six nameless radios, none reachable by name.
|
|
15
|
-
*
|
|
16
|
-
* `listitem`, `status` and `alert` are NOT in the spec's list. They are kept as a deliberate
|
|
17
|
-
* over-approximation — a name computed from content is more useful than no name — and removing them
|
|
18
|
-
* has no measured symptom to justify the risk.
|
|
19
|
-
*/
|
|
6
|
+
import { formatSource, sourceFromDom } from './addressing/source.js';
|
|
20
7
|
const NAME_FROM_CONTENT = new Set([
|
|
21
8
|
'button',
|
|
22
9
|
'link',
|
|
@@ -57,16 +44,6 @@ function inputRole(input) {
|
|
|
57
44
|
return 'button';
|
|
58
45
|
return 'textbox';
|
|
59
46
|
}
|
|
60
|
-
/**
|
|
61
|
-
* Cheap author-supplied-naming probe: an explicit `aria-label`, any `aria-labelledby`, or a
|
|
62
|
-
* `title`. Attribute reads only - no name computation - so `getRole` can consult it without
|
|
63
|
-
* recursion into `getAccessibleName`.
|
|
64
|
-
*
|
|
65
|
-
* This decides `section` -> `region`, which the implicit-role table makes CONDITIONAL: an unnamed
|
|
66
|
-
* `<section>` is a plain `generic` container, while one carrying an accessible name is exposed as
|
|
67
|
-
* `region`. Getting that backwards either floods every page with phantom regions or hides real
|
|
68
|
-
* ones, so both halves are pinned by tests.
|
|
69
|
-
*/
|
|
70
47
|
function hasAuthorNaming(el) {
|
|
71
48
|
const label = el.getAttribute('aria-label');
|
|
72
49
|
if (label !== null && label.trim().length > 0)
|
|
@@ -77,7 +54,6 @@ function hasAuthorNaming(el) {
|
|
|
77
54
|
const title = el.getAttribute('title');
|
|
78
55
|
return title !== null && title.trim().length > 0;
|
|
79
56
|
}
|
|
80
|
-
/** Compute the ARIA role (explicit wins, else implicit from the tag). */
|
|
81
57
|
export function getRole(el) {
|
|
82
58
|
const explicit = el.getAttribute('role');
|
|
83
59
|
if (explicit !== null && explicit.trim().length > 0)
|
|
@@ -124,10 +100,6 @@ export function getRole(el) {
|
|
|
124
100
|
case 'thead':
|
|
125
101
|
case 'tfoot':
|
|
126
102
|
return 'rowgroup';
|
|
127
|
-
// A cell's role follows its grid context: plain tables expose `cell`/`columnheader`,
|
|
128
|
-
// while inside an explicit `role="grid"`/`role="treegrid"` the same markup is exposed as
|
|
129
|
-
// `gridcell` - the pair data-grid queries actually reach for (`{ role: "cell" }` against a
|
|
130
|
-
// CSS grid pretending to be a table would otherwise answer zero).
|
|
131
103
|
case 'td':
|
|
132
104
|
return el.closest('[role~="grid"], [role~="treegrid"]') !== null ? 'gridcell' : 'cell';
|
|
133
105
|
case 'th': {
|
|
@@ -186,22 +158,6 @@ function labelledByText(el) {
|
|
|
186
158
|
const joined = parts.join(' ').trim();
|
|
187
159
|
return joined.length > 0 ? joined : null;
|
|
188
160
|
}
|
|
189
|
-
/** Accessible name via a practical subset of the accname algorithm. */
|
|
190
|
-
/** Accessible name via a practical subset of the accname algorithm. */
|
|
191
|
-
/**
|
|
192
|
-
* Text content with `aria-hidden` subtrees removed.
|
|
193
|
-
*
|
|
194
|
-
* The accessible-name spec excludes them, and so does the matcher, because THIS function is what
|
|
195
|
-
* `by: role` + name matches through. Reading raw `textContent` here made the name we REPORT differ
|
|
196
|
-
* from the name that can be SELECTED: Material UI renders its required-field marker as
|
|
197
|
-
* `<span aria-hidden="true"> *</span>`, so a login field was reported as `"Username *"` and
|
|
198
|
-
* addressable only as `"Username"`. Reporting a name the agent cannot use defeats the purpose of
|
|
199
|
-
* reporting it at all, so both come from the same rule.
|
|
200
|
-
*
|
|
201
|
-
* An `<img alt>` contributes its alt text the way the spec's subtree step treats embedded
|
|
202
|
-
* alternatives: `<button><img alt="Close"></button>` is named "Close", not nameless. Pieces are
|
|
203
|
-
* joined with spaces so an icon followed by a word never fuses into one unmatchable token.
|
|
204
|
-
*/
|
|
205
161
|
function textWithoutHidden(node) {
|
|
206
162
|
if (node.nodeType === Node.TEXT_NODE)
|
|
207
163
|
return node.textContent ?? '';
|
|
@@ -244,10 +200,6 @@ export function getAccessibleName(el) {
|
|
|
244
200
|
if (text.length > 0)
|
|
245
201
|
return text;
|
|
246
202
|
}
|
|
247
|
-
// Submit-like inputs carry their name on `value`, exactly where the visible caption comes
|
|
248
|
-
// from: `<input type="submit" value="Send">` renders a button reading Send. Without this the
|
|
249
|
-
// descriptor printed `button ""` while `by: text` found the very same input by "Send", so the
|
|
250
|
-
// two locators disagreed about one element - the disagreement this engine exists to prevent.
|
|
251
203
|
if (isInput(el)) {
|
|
252
204
|
const type = el.type.toLowerCase();
|
|
253
205
|
if ('submit' === type || 'button' === type || 'reset' === type) {
|
|
@@ -278,10 +230,6 @@ function ariaBool(el, attr) {
|
|
|
278
230
|
return undefined;
|
|
279
231
|
return 'true' === value;
|
|
280
232
|
}
|
|
281
|
-
/**
|
|
282
|
-
* The set of states relevant to assertions. `visible` is an O(depth) forced-style walk; callers that
|
|
283
|
-
* already computed it (describe) pass it in so it isn't resolved twice per element.
|
|
284
|
-
*/
|
|
285
233
|
export function getStates(el, visible = isVisible(el)) {
|
|
286
234
|
const states = [ElementState.PRESENT];
|
|
287
235
|
states.push(visible ? ElementState.VISIBLE : ElementState.HIDDEN);
|
|
@@ -297,12 +245,6 @@ export function getStates(el, visible = isVisible(el)) {
|
|
|
297
245
|
states.push(ElementState.FOCUSED);
|
|
298
246
|
return states;
|
|
299
247
|
}
|
|
300
|
-
/**
|
|
301
|
-
* True when a form field holds a secret the SDK must never capture verbatim — a password input, a
|
|
302
|
-
* sensitive `autocomplete` (cc-number, one-time-code, …), or a name/id/testid/aria-label that trips
|
|
303
|
-
* `isSensitiveKey`. The single source of truth for both live-snapshot redaction (`getValue`) and the
|
|
304
|
-
* flow recorder's fill-value redaction, so recorded flows never persist typed passwords/OTPs/keys.
|
|
305
|
-
*/
|
|
306
248
|
export function isSensitiveField(el) {
|
|
307
249
|
if (!isInput(el) && !isTextArea(el) && !isSelect(el)) {
|
|
308
250
|
return false;
|
|
@@ -328,7 +270,6 @@ export function getValue(el) {
|
|
|
328
270
|
const valueNow = el.getAttribute('aria-valuenow');
|
|
329
271
|
return valueNow ?? undefined;
|
|
330
272
|
}
|
|
331
|
-
/** Whether the element's OWN box hides it — one forced-style resolution, no ancestor walk. */
|
|
332
273
|
function selfHidden(el) {
|
|
333
274
|
if ('true' === el.getAttribute('aria-hidden'))
|
|
334
275
|
return true;
|
|
@@ -347,22 +288,6 @@ function selfHidden(el) {
|
|
|
347
288
|
}
|
|
348
289
|
return false;
|
|
349
290
|
}
|
|
350
|
-
/**
|
|
351
|
-
* Whether the element is actually visible (not display:none/hidden/aria-hidden/opacity:0), walking to
|
|
352
|
-
* root. This is an O(depth) forced-style walk PER node; `memo` (optional, scoped to ONE synchronous
|
|
353
|
-
* query pass) caches the full inherited result per element so a broad state-filtered query stops
|
|
354
|
-
* re-resolving getComputedStyle up the same ancestor chain for every sibling. Sound because the DOM is
|
|
355
|
-
* static for the pass's duration — the cache MUST be a per-call Map, never module-level (that would go
|
|
356
|
-
* stale the instant the app mutates, the same trap the shadow-root note in query.ts documents).
|
|
357
|
-
*/
|
|
358
|
-
/**
|
|
359
|
-
* True when the element is inside the viewport right now: visible AND its bounding box intersects
|
|
360
|
-
* the window. This is what makes a scroll assertable (#398) — content below the fold of a scrolling
|
|
361
|
-
* container is `visible`/`present` before any scroll, so only a viewport-intersection check can tell
|
|
362
|
-
* "scrolled into view" from "was always in the DOM". Uses getBoundingClientRect (viewport-relative,
|
|
363
|
-
* already accounts for scroll position), not an IntersectionObserver, so it stays synchronous inside
|
|
364
|
-
* the predicate pass.
|
|
365
|
-
*/
|
|
366
291
|
export function isInViewport(el, memo) {
|
|
367
292
|
if (!isVisible(el, memo))
|
|
368
293
|
return false;
|
|
@@ -381,9 +306,6 @@ export function isVisible(el, memo) {
|
|
|
381
306
|
if (cached !== undefined)
|
|
382
307
|
return cached;
|
|
383
308
|
const parent = el.parentElement;
|
|
384
|
-
// Each cached boolean already folds in that node's own aria-hidden/[hidden]/display/visibility/opacity,
|
|
385
|
-
// so inherited visibility composes by AND up the chain and a sibling short-circuits at the first
|
|
386
|
-
// cached ancestor.
|
|
387
309
|
const result = !selfHidden(el) && (null === parent || isVisible(parent, memo));
|
|
388
310
|
if (memo !== undefined)
|
|
389
311
|
memo.set(el, result);
|
|
@@ -394,13 +316,11 @@ function getVisibleText(el) {
|
|
|
394
316
|
const text = collapse(el.textContent ?? '');
|
|
395
317
|
return text.length > MAX_TEXT ? `${text.slice(0, MAX_TEXT)}…` : text;
|
|
396
318
|
}
|
|
397
|
-
/** Build the compact descriptor surfaced to the agent. `memo` (optional) shares the per-call
|
|
398
|
-
* visibility cache with the query's state filter so ancestors aren't re-walked per element. */
|
|
399
319
|
export function describe(el, memo) {
|
|
400
320
|
const value = getValue(el);
|
|
401
321
|
const text = getVisibleText(el);
|
|
402
322
|
const name = getAccessibleName(el);
|
|
403
|
-
const visible = isVisible(el, memo);
|
|
323
|
+
const visible = isVisible(el, memo);
|
|
404
324
|
const base = {
|
|
405
325
|
ref: refs.refFor(el),
|
|
406
326
|
role: getRole(el),
|
|
@@ -412,16 +332,9 @@ export function describe(el, memo) {
|
|
|
412
332
|
base.value = value;
|
|
413
333
|
if (text.length > 0 && text !== name)
|
|
414
334
|
base.text = text;
|
|
415
|
-
// DOM-only lookup on purpose: describe() runs once per matched element, so the adapter's fiber walk
|
|
416
|
-
// would turn a broad query into thousands of tree traversals. The stamped attribute answers the
|
|
417
|
-
// same question for a fraction of the cost, and single-element paths that can afford the better
|
|
418
|
-
// answer (inspect, act, review) use sourceFor() instead.
|
|
419
335
|
const source = formatSource(sourceFromDom(el));
|
|
420
336
|
if (source !== undefined)
|
|
421
337
|
base.source = source;
|
|
422
|
-
// Chart faults, only when there are any. Gated on the element actually containing plot geometry so
|
|
423
|
-
// the common case — every non-chart element on the page — pays one querySelector miss and nothing
|
|
424
|
-
// else, and a HEALTHY chart adds no bytes to the wire either.
|
|
425
338
|
if (el.querySelector('path, polyline, polygon') !== null) {
|
|
426
339
|
const faults = inspectChart(el).findings;
|
|
427
340
|
if (faults.length > 0)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const AnchorStrategy: {
|
|
2
|
+
readonly TESTID: "testid";
|
|
3
|
+
readonly COMPONENT: "component";
|
|
4
|
+
readonly ROLE: "role";
|
|
5
|
+
readonly POSITION: "position";
|
|
6
|
+
};
|
|
7
|
+
export type AnchorStrategy = (typeof AnchorStrategy)[keyof typeof AnchorStrategy];
|
|
8
|
+
export interface AnchorInput {
|
|
9
|
+
testid?: string;
|
|
10
|
+
component?: string;
|
|
11
|
+
source?: {
|
|
12
|
+
file: string;
|
|
13
|
+
line: number;
|
|
14
|
+
};
|
|
15
|
+
role?: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
nth?: number;
|
|
18
|
+
}
|
|
19
|
+
interface SynthesizedAnchor {
|
|
20
|
+
strategy: AnchorStrategy;
|
|
21
|
+
value: string;
|
|
22
|
+
stable: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function synthesizeAnchor(input: AnchorInput): SynthesizedAnchor;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const AnchorStrategy = {
|
|
2
|
+
TESTID: 'testid',
|
|
3
|
+
COMPONENT: 'component',
|
|
4
|
+
ROLE: 'role',
|
|
5
|
+
POSITION: 'position',
|
|
6
|
+
};
|
|
7
|
+
function nonEmpty(s) {
|
|
8
|
+
return 'string' === typeof s && s.length > 0;
|
|
9
|
+
}
|
|
10
|
+
function sourceTag(source) {
|
|
11
|
+
const base = source.file.split('/').pop() ?? source.file;
|
|
12
|
+
return `${base}:${source.line}`;
|
|
13
|
+
}
|
|
14
|
+
export function synthesizeAnchor(input) {
|
|
15
|
+
if (nonEmpty(input.testid)) {
|
|
16
|
+
return { strategy: AnchorStrategy.TESTID, value: input.testid, stable: true };
|
|
17
|
+
}
|
|
18
|
+
if (nonEmpty(input.component) && input.source !== undefined) {
|
|
19
|
+
return {
|
|
20
|
+
strategy: AnchorStrategy.COMPONENT,
|
|
21
|
+
value: `${input.component}@${sourceTag(input.source)}`,
|
|
22
|
+
stable: true,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (nonEmpty(input.component) && (nonEmpty(input.role) || nonEmpty(input.name))) {
|
|
26
|
+
const qualifier = nonEmpty(input.name) ? input.name : input.role;
|
|
27
|
+
return {
|
|
28
|
+
strategy: AnchorStrategy.COMPONENT,
|
|
29
|
+
value: `${input.component}[${qualifier ?? ''}]`,
|
|
30
|
+
stable: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (nonEmpty(input.role) && nonEmpty(input.name)) {
|
|
34
|
+
return { strategy: AnchorStrategy.ROLE, value: `${input.role}:${input.name}`, stable: false };
|
|
35
|
+
}
|
|
36
|
+
if (nonEmpty(input.role)) {
|
|
37
|
+
const suffix = input.nth !== undefined ? `#${input.nth}` : '';
|
|
38
|
+
return { strategy: AnchorStrategy.ROLE, value: `${input.role}${suffix}`, stable: false };
|
|
39
|
+
}
|
|
40
|
+
return { strategy: AnchorStrategy.POSITION, value: `el#${input.nth ?? 0}`, stable: false };
|
|
41
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Ref } from '@reticlehq/core';
|
|
2
|
+
export declare const MAX_TRACKED_REFS = 10000;
|
|
3
|
+
export declare function echoRef(ref: string): string;
|
|
4
|
+
export declare const REF_BLOCK = 100;
|
|
5
|
+
export declare class RefRegistry {
|
|
6
|
+
#private;
|
|
7
|
+
constructor(storage?: () => Storage | undefined);
|
|
8
|
+
get size(): number;
|
|
9
|
+
markEdited(): void;
|
|
10
|
+
mintedBeforeLastEdit(ref: string): boolean;
|
|
11
|
+
refFor(el: Element): Ref;
|
|
12
|
+
resolve(ref: string): Element | null;
|
|
13
|
+
}
|
|
14
|
+
export declare const refs: RefRegistry;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { TRANSPORT_LIMITS, asRef } from '@reticlehq/core';
|
|
2
|
+
export const MAX_TRACKED_REFS = 10000;
|
|
3
|
+
const REF_ELISION = '…';
|
|
4
|
+
const REF_PREFIX = 'e';
|
|
5
|
+
export function echoRef(ref) {
|
|
6
|
+
const max = TRANSPORT_LIMITS.MAX_REF_LENGTH;
|
|
7
|
+
return max >= ref.length ? ref : `${ref.slice(0, max)}${REF_ELISION}`;
|
|
8
|
+
}
|
|
9
|
+
const SWEEP_EVERY_MINTS = 1000;
|
|
10
|
+
const REF_BASE_KEY = '__reticle_ref_base';
|
|
11
|
+
export const REF_BLOCK = 100;
|
|
12
|
+
function readBase(store) {
|
|
13
|
+
try {
|
|
14
|
+
const raw = store?.getItem(REF_BASE_KEY);
|
|
15
|
+
const value = null === raw || undefined === raw ? Number.NaN : Number(raw);
|
|
16
|
+
return Number.isSafeInteger(value) && value > 0 ? value : 0;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class RefRegistry {
|
|
23
|
+
#toRef = new WeakMap();
|
|
24
|
+
#fromRef = new Map();
|
|
25
|
+
#storage;
|
|
26
|
+
#seq = 0;
|
|
27
|
+
#reserved = 0;
|
|
28
|
+
#mintsSinceSweep = 0;
|
|
29
|
+
#editedAtSeq = 0;
|
|
30
|
+
constructor(storage = () => globalThis.sessionStorage) {
|
|
31
|
+
this.#storage = storage;
|
|
32
|
+
}
|
|
33
|
+
#reserve() {
|
|
34
|
+
let store;
|
|
35
|
+
try {
|
|
36
|
+
store = this.#storage();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
store = undefined;
|
|
40
|
+
}
|
|
41
|
+
const base = Math.max(readBase(store), this.#seq);
|
|
42
|
+
this.#reserved = base + REF_BLOCK;
|
|
43
|
+
try {
|
|
44
|
+
store?.setItem(REF_BASE_KEY, String(this.#reserved));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
}
|
|
48
|
+
this.#seq = base;
|
|
49
|
+
}
|
|
50
|
+
get size() {
|
|
51
|
+
return this.#fromRef.size;
|
|
52
|
+
}
|
|
53
|
+
#evictOldest() {
|
|
54
|
+
const oldest = this.#fromRef.keys().next();
|
|
55
|
+
if (oldest.done !== true)
|
|
56
|
+
this.#fromRef.delete(oldest.value);
|
|
57
|
+
}
|
|
58
|
+
#sweep() {
|
|
59
|
+
for (const [ref, weak] of this.#fromRef) {
|
|
60
|
+
const el = weak.deref();
|
|
61
|
+
if (el === undefined || !el.isConnected)
|
|
62
|
+
this.#fromRef.delete(ref);
|
|
63
|
+
}
|
|
64
|
+
while (this.#fromRef.size > MAX_TRACKED_REFS)
|
|
65
|
+
this.#evictOldest();
|
|
66
|
+
}
|
|
67
|
+
markEdited() {
|
|
68
|
+
this.#editedAtSeq = this.#seq;
|
|
69
|
+
}
|
|
70
|
+
mintedBeforeLastEdit(ref) {
|
|
71
|
+
if (0 === this.#editedAtSeq || !ref.startsWith(REF_PREFIX))
|
|
72
|
+
return false;
|
|
73
|
+
const n = Number(ref.slice(REF_PREFIX.length));
|
|
74
|
+
return Number.isSafeInteger(n) && n > 0 && n <= this.#editedAtSeq;
|
|
75
|
+
}
|
|
76
|
+
refFor(el) {
|
|
77
|
+
const existing = this.#toRef.get(el);
|
|
78
|
+
if (existing !== undefined) {
|
|
79
|
+
const weak = this.#fromRef.get(existing);
|
|
80
|
+
this.#fromRef.delete(existing);
|
|
81
|
+
this.#fromRef.set(existing, weak ?? new WeakRef(el));
|
|
82
|
+
return existing;
|
|
83
|
+
}
|
|
84
|
+
if (this.#seq >= this.#reserved)
|
|
85
|
+
this.#reserve();
|
|
86
|
+
this.#seq += 1;
|
|
87
|
+
const ref = asRef(`${REF_PREFIX}${String(this.#seq)}`);
|
|
88
|
+
this.#toRef.set(el, ref);
|
|
89
|
+
this.#fromRef.set(ref, new WeakRef(el));
|
|
90
|
+
this.#mintsSinceSweep += 1;
|
|
91
|
+
if (this.#mintsSinceSweep >= SWEEP_EVERY_MINTS) {
|
|
92
|
+
this.#mintsSinceSweep = 0;
|
|
93
|
+
this.#sweep();
|
|
94
|
+
}
|
|
95
|
+
else if (this.#fromRef.size > MAX_TRACKED_REFS) {
|
|
96
|
+
this.#evictOldest();
|
|
97
|
+
}
|
|
98
|
+
return ref;
|
|
99
|
+
}
|
|
100
|
+
resolve(ref) {
|
|
101
|
+
const weak = this.#fromRef.get(ref);
|
|
102
|
+
if (weak === undefined)
|
|
103
|
+
return null;
|
|
104
|
+
const el = weak.deref();
|
|
105
|
+
if (el === undefined || !el.isConnected) {
|
|
106
|
+
this.#fromRef.delete(ref);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
this.#fromRef.delete(ref);
|
|
110
|
+
this.#fromRef.set(ref, weak);
|
|
111
|
+
return el;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export const refs = new RefRegistry();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface SourceLocation {
|
|
2
|
+
file: string;
|
|
3
|
+
line: number;
|
|
4
|
+
}
|
|
5
|
+
export declare function parseSourceAttr(value: string | null): SourceLocation | undefined;
|
|
6
|
+
export declare function sourceFromDom(el: Element): SourceLocation | undefined;
|
|
7
|
+
export declare function sourceFor(el: Element, adapterSource: SourceLocation | undefined): SourceLocation | undefined;
|
|
8
|
+
export declare function documentHasSourceStamps(doc: Document): boolean;
|
|
9
|
+
export declare function formatSource(source: SourceLocation | undefined): string | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DATA_RETICLE_SOURCE_ATTR } from '@reticlehq/core';
|
|
2
|
+
const SOURCE_ATTR = DATA_RETICLE_SOURCE_ATTR;
|
|
3
|
+
export function parseSourceAttr(value) {
|
|
4
|
+
if (null === value)
|
|
5
|
+
return undefined;
|
|
6
|
+
const m = /^(.*):(\d+):(\d+)$/.exec(value);
|
|
7
|
+
if (null === m)
|
|
8
|
+
return undefined;
|
|
9
|
+
const file = m[1];
|
|
10
|
+
const line = Number(m[2]);
|
|
11
|
+
if (file === undefined || 0 === file.length || !Number.isFinite(line))
|
|
12
|
+
return undefined;
|
|
13
|
+
return { file, line };
|
|
14
|
+
}
|
|
15
|
+
export function sourceFromDom(el) {
|
|
16
|
+
const host = el.closest(`[${SOURCE_ATTR}]`);
|
|
17
|
+
return host !== null ? parseSourceAttr(host.getAttribute(SOURCE_ATTR)) : undefined;
|
|
18
|
+
}
|
|
19
|
+
export function sourceFor(el, adapterSource) {
|
|
20
|
+
if (adapterSource !== undefined)
|
|
21
|
+
return { file: adapterSource.file, line: adapterSource.line };
|
|
22
|
+
return sourceFromDom(el);
|
|
23
|
+
}
|
|
24
|
+
export function documentHasSourceStamps(doc) {
|
|
25
|
+
return doc.querySelector(`[${SOURCE_ATTR}]`) !== null;
|
|
26
|
+
}
|
|
27
|
+
export function formatSource(source) {
|
|
28
|
+
return source === undefined ? undefined : `${source.file}:${String(source.line)}`;
|
|
29
|
+
}
|