Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ pnpm install
pnpm playground # returns an example selector
```

## Projects

[`@bomb.sh/dom`](packages/dom/README.md) is a headless interactive DOM.

## Support

Having trouble? Get help in the official [Bombshell Discord][discord].
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"format": "bsh format",
"format:check": "bsh format --check",
"lint": "bsh lint .",
"test": "bsh test --exclude 'experiments/ghostwright/**'"
"test": "pnpm --filter @bomb.sh/dom build && bsh test --exclude 'experiments/ghostwright/**' --exclude '**/node_modules/**' --exclude '**/dist/**'"
},
"devDependencies": {
"@bomb.sh/args": "catalog:",
Expand Down
21 changes: 21 additions & 0 deletions packages/dom/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License Copyright (c) 2025-Present [Bombshell contributors](https://bomb.sh/team)

Permission is hereby granted, free of
charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the
following conditions:

The above copyright notice and this permission notice
(including the next paragraph) shall be included in all copies or substantial
portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
121 changes: 121 additions & 0 deletions packages/dom/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# @bomb.sh/dom

A lightweight, headless, in-memory DOM for interactive applications. Documents own their trees, focus, selection, and custom-element registries. Importing the package installs no globals. Terminal rendering, layout, input decoding, frame scheduling, and output backpressure belong to consumers.

This is an independently maintained implementation with a tested subset of DOM and HTML behavior. See the [capability matrix](docs/capabilities.md) for supported scope and unfinished work, and [provenance](docs/provenance.md) for the pinned references, licenses, and baseline differences.

## Construct and observe a document

```ts
import { Document, MutationObserver } from '@bomb.sh/dom';

const document = new Document({
onInvalidate(change) {
// Ask your renderer for a frame; coalesce and handle backpressure there.
console.info(change.kind); // mutation, focus, or selection
},
reportError(error) {
console.error(error); // Listener / custom-element callback exceptions
},
});
const root = document.createElement('box');
const label = document.createElement('text');
label.textContent = 'Ready';
root.append(label); // Detached preparation requests no frame.
document.append(root); // Explicit shape: use "document" here for a Clack adapter.

const observer = new MutationObserver((records) => {
console.info(records.map((record) => record.type));
});
observer.observe(label, { childList: true, characterData: true, subtree: true });
label.firstChild!.textContent = 'Running';
// Observer records are delivered in a microtask, independently of repainting.
// Explicitly observing a detached node works too.
```

Documents start empty. `documentElement` is nullable. Append one root element; document fragments allow detached preparation. Nodes move on insertion and are adopted when moved across documents. Child, element, and attribute collections are live read-only array views; mutation goes through node/attribute methods.

## Events and host input

```ts
import { Event, Dispatch } from '@bomb.sh/dom';

root.addEventListener(
'activate',
(event) => {
event.preventDefault();
},
{ capture: true },
);
const accepted = label.dispatchEvent(
new Event('activate', {
bubbles: true,
cancelable: true,
}),
); // false

const input = document.createElement('input');
const next = document.createElement('button');
root.append(input, next);
input.focus();
const dispatch = new Dispatch(document); // document.dispatch is also available.
dispatch.keydown({ key: 'Tab' }); // Route to focus, then navigate if uncanceled.
dispatch.keyup({ key: 'Tab' });
```

Event names preserve case. Capture, target, bubbling, cancellation, listener snapshots, once/passive/signal options, listener objects, and shadow retargeting are exercised by unit tests and selected upstream WPT cases. Listener exceptions are reported without escaping dispatch. The default error reporter queues an uncaught exception; hosts can provide `reportError`.

`element.dispatchEvent()` delivers a synthetic event. It does not move focus, edit text, or perform browser default actions. `Dispatch` is the explicit host-input boundary and implements logical Tab navigation; its dispatched events are still untrusted. See [focus and controls](docs/focus-and-controls.md).

## Two independent selection systems

```ts
input.value = 'a😀b';
input.setSelectionRange(1, 3, 'forward'); // UTF-16 code units
input.setRangeText('x', 1, 3, 'select');

const range = document.createRange();
range.selectNodeContents(label);
document.getSelection().addRange(range);
console.info(document.getSelection().toString());
```

Ranges adjust when text or the tree changes. Element offsets count child nodes. These APIs do not measure graphemes, terminal columns, or coordinates. See [selection and observation](docs/selection-observation.md) for the exact API subset.

## Custom elements and opt-in parsing

```ts
import { installParsing } from '@bomb.sh/dom/parsing';
installParsing(document);

class PromptLabel extends document.HTMLElement {
connectedCallback(): void {
if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' }).innerHTML = '<text><slot></slot></text>';
}
}
}
document.customElements.define('prompt-label', PromptLabel);
const prompt = document.createElement('prompt-label');
prompt.append('Continue?');
root.append(prompt);
```

Each environment has an explicit `document.HTMLElement` base and registry. Autonomous custom elements support direct construction and identity-preserving upgrades, observed attributes, connection/disconnection/adoption callbacks, and reentrancy. Parsing is opt-in; scripts remain inert. Template contents are separate, inert trees. Shadow roots and slots compose events and focus without a rendering engine.

`parse5` is the only production dependency. The separate `@bomb.sh/dom/parsing` entry uses its HTML tokenizer/tree builder, then creates nodes in this implementation. The core entry does not load it. No LinkeDOM runtime, second DOM hierarchy, framework, browser process, or worker is required.

## Development

From the playground root:

```sh
pnpm install
pnpm --filter @bomb.sh/dom build
pnpm --filter @bomb.sh/dom typecheck
pnpm --filter @bomb.sh/dom exec vitest run
pnpm --filter @bomb.sh/dom format:check
pnpm --filter @bomb.sh/dom lint
```

The package emits ESM JavaScript, declaration files, and maps through the playground's TypeScript compiler. Core checking uses strict TypeScript without the browser DOM library. JSX and upstream browser declaration requirements stay local to examples. No publishing is performed by these commands.
35 changes: 35 additions & 0 deletions packages/dom/docs/capabilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Capability matrix

Statuses describe this implementation and its checked-in tests. “Implemented and tested” means the scope in that row, not complete conformance to an entire specification. The full DOM/HTML/Web Components WPT suites have not been run.

| Area | Status | Supported scope / remaining boundary |
| ---------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Packaging | Implemented and tested | Strict TypeScript, ESM-only exports and declarations; independent documents; no process globals, browser or worker dependency. |
| Tree and mutation | Implemented and tested | Element, Text, Comment, Attr, Document, DocumentFragment; arbitrary element names; moves, prevalidation, siblings, ownership, connectivity, cloning/import/adoption, textContent, HTML/SVG/MathML namespaces. Live read-only array views protect collections. |
| Typed element names | Implemented and tested | `Elements` declaration merging; input/textarea/template/slot return actual specialized classes. Application declarations must match registered classes. |
| Host invalidation | Implemented and tested | Connected changes only; detach invalidates the old document; focus and selection changes observable; listeners/dispatch alone do not invalidate; host owns scheduling. |
| MutationObserver | Implemented and tested | Target/subtree options, detached targets, old values, filters, transient removed-subtree observation, microtask delivery, takeRecords/disconnect, aggregate fragment/replacement records. |
| Events | Implemented and tested | Capture/target/bubble, stable paths, stop boundaries, cancellation, dedup/removal/snapshots, reuse/nesting, once/passive/AbortSignal, listener objects/error reporting, case preservation, extensible objects. 20 unchanged pinned event WPT cases pass without skips. |
| Focus and host dispatch | Implemented and tested | Eligibility, focus/blur transitions and related targets, removal/reentrancy, logical tabindex navigation, shadow focus/delegation, separate host keydown/keyup entry. |
| Text controls | Implemented and tested | Input/textarea values and UTF-16 selection, directions/clamping, select/setSelectionRange/setRangeText, dirty values, value cloning, and observable selection. |
| Document ranges/selection | Implemented and tested | Live boundaries, comparison, collapse/extend, direction, traversal/stringification, clone/extract/delete contents; insertion/removal/text/split updates. |
| Autonomous custom elements | Implemented and tested | Per-document base/registry, construction, define/get/whenDefined/upgrade, preserved existing identity/listeners/user state, lifecycle and observed attributes, adoption, callback errors/reentrancy. |
| Templates and parsing | Implemented and tested | Opt-in parse5 HTML fragments, inert nested templates, namespaces/comments, cloning/importing, basic serialization, TreeWalker nextNode/parentNode and filtering. |
| Shadow trees and slots | Implemented and tested | Open/closed roots, named/default/fallback slot distribution, slotchange, shadow-including connectivity/adoption, event retargeting/closed composed paths, focus interactions. |
| Remaining Range/Selection APIs | Planned but unfinished | Range.insertNode/surroundContents/createContextualFragment, StaticRange, normalize-related updates, composed/shadow document selection and additional full-suite coverage. APIs absent from declarations until implemented. |
| Further custom-element/shadow APIs | Planned but unfinished | Unicode custom-element names, customized built-ins, scoped registries, form-associated elements/ElementInternals, declarative shadow DOM, manual slot assignment, cloneable/serializable roots, complete specification edge-case coverage. |
| Further DOM facilities | Planned but unfinished | CSS selector matching, complete traversal API, NodeList/NamedNodeMap legacy methods, DOMTokenList/style declaration model, DocumentType/processing instructions/XML documents and XML parsing, complete HTML serialization (including raw-text script/style serialization). |
| Further interaction | Planned but unfinished | Full browser focus-navigation/radio-group behavior, editing/activation defaults, broader form values/validation and input sanitization. See focus documentation. |
| Rendering and terminal I/O | Intentionally unsupported | CSS layout, glyph/caret painting, ANSI, keyboard-byte decoding, hit testing, coordinates, frame coalescing/output backpressure. Consumer responsibilities. |
| Browser subsystems | Intentionally unsupported | Canvas, media, navigation, networking, browser script execution, trusted user input, browser windows or rendering processes. |
| Legacy distribution/global setup | Intentionally unsupported | CommonJS builds, worker bundles/infrastructure, implicit global document/window/constructor installation, decorators as a requirement. |

## Milestone record

1. The pinned DOM/event baseline established the initial tree and event regression coverage.
2. Independent host invalidation, MutationObserver, focus/Dispatch, and text controls followed.
3. Live Range/Selection tests cover both ordinary edits and detached-tree mutations.
4. Custom elements, inert templates, parsing, shadow roots, and slots have focused unit tests and custom-element browser differential cases.
5. Framework integrations and their acceptance tests are maintained separately from the core package.

Further work should expand this matrix only after adding meaningful tests and accurate declarations. No unsupported browser API is advertised through structural casts or global interface replacement.
58 changes: 58 additions & 0 deletions packages/dom/docs/events-provenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Events: references and boundaries

The event implementation is independently authored. No LinkeDOM implementation is
transplanted. Its regression requirements were checked against the read-only
[`@clack/ui` baseline](https://github.com/bombshell-dev/ui/blob/606675f16131c6ae7c4553a67ab477bdf661f772/packages/ui/src/dom.ts)
at `606675f16131c6ae7c4553a67ab477bdf661f772`, including `dom.test.ts`. The pinned
source was inspected through `git show` in the existing local checkout.

The [WHATWG DOM event dispatch and invoke algorithms](https://dom.spec.whatwg.org/#concept-event-dispatch)
govern phase order, callback receivers, cancellation, registration removal,
per-invocation snapshots, cleanup, and listener exception handling. In particular,
target capture and target non-capture are separate invocations: a propagation
stop during target capture prevents the non-capture invocation. Additions can
join a later invocation but cannot join the current snapshot. Cancellation
persists on reuse; stop flags do not.

Relevant WPT cases inspected:

- [`Event-dispatch-order.html`](https://github.com/web-platform-tests/wpt/blob/master/dom/events/Event-dispatch-order.html): ancestor capture precedes target delivery and ancestor bubbling.
- [`EventTarget-dispatchEvent.html`](https://github.com/web-platform-tests/wpt/blob/master/dom/events/EventTarget-dispatchEvent.html): active events reject redispatch; listener exceptions are reported while delivery continues.
- [`event-composed-path.html`](https://github.com/web-platform-tests/wpt/blob/master/shadow-dom/event-composed-path.html), [`event-composed.html`](https://github.com/web-platform-tests/wpt/blob/master/shadow-dom/event-composed.html), and [`event-with-related-target.html`](https://github.com/web-platform-tests/wpt/blob/master/shadow-dom/event-with-related-target.html): open/closed visibility, shadow boundaries, and related-target retargeting.

The local tests independently exercise these cases and additional registration,
abort, passive, object-listener, mutation-during-dispatch, and reuse scenarios.

An additional [Vitest adapter](../test/wpt-events.test.ts) executes all 20 cases
from three unchanged upstream WPT files: `AddEventListenerOptions-once.any.js`,
`AddEventListenerOptions-passive.any.js`, and
`AddEventListenerOptions-signal.any.js`. They are pinned to commit
`b4b303c8dca22989c43e2d76fce199c36d4b77a7` with the upstream BSD license and
[provenance](../test/wpt/README.md). Every case passes; none are skipped.
These tests found and corrected options-dictionary conversion occurring too
late for null listeners and acceptance of invalid null abort signals. This is
a selected upstream run, not a run of the complete WPT browser harness.

## Deliberate corrections to the baseline

The baseline allows listener exceptions to escape `dispatchEvent`, terminating
delivery. This package reports them through the document/target error reporter,
continues delivery, and returns the cancellation result. The standalone default
reports an uncaught exception in a microtask; hosts can supply `reportError`.
`stopImmediatePropagation()` sets both stop flags. Event dispatch has its own
active flag, including before listener invocation, rather than inferring it from
`eventPhase`. Neither listener registration nor dispatch requests a host frame.

## Current scope

`Event`, `CustomEvent`, `FocusEvent`, and `KeyboardEvent` are synthetic event
classes; `isTrusted` is always false. Keyboard events expose key/code, location,
the four primary modifiers, repeat, and composition status. They do not execute
browser default actions. Legacy `initEvent`, UIEvent view/detail, legacy keyCode,
mouse/pointer events, activation behavior, and window-global `event` are not
implemented. Tree hooks capture retargeting and closed-root information with
the event path. Targeted tests cover open/closed and nested closed roots,
non-bubbling composed host delivery, related-target trimming, slotted light-tree
events, and mutation-stable retargeting. This is not a claim that all shadow WPT
cases pass. Native `AbortSignal.any()` supplies a private dependent signal so
listener removal survives propagation stops on the source signal's abort event.
Loading
Loading