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.
155 changes: 155 additions & 0 deletions packages/dom/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# @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. Synthetic keyboard and focus events do not run host input actions. Following HTML activation semantics, an uncanceled `MouseEvent("click")` can activate controls; `element.click()` provides that operation. `Dispatch` is the explicit host-input boundary for logical navigation, text editing, keyboard activation and option selection; all generated events remain 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.

## Interactive HTML and terminal elements

Forms, buttons, labels, fieldsets, inputs, textareas, selects/options, output,
progress/meter, details/summary and dialogs have specialized classes and tested
state transitions. See [forms and element extensions](docs/forms-and-elements.md)
for examples and limits, and [interactive standards](docs/interactive-standards.md)
for browser comparisons and known differences.

Arbitrary names still work without registration. To give a terminal element
application behavior, register its factory explicitly on each document:

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

class Box extends Element {
constructor(document: Document) {
super('box', document);
}
status = 'ready';
}
declare module '@bomb.sh/dom' {
interface Elements {
box: Box;
}
}
document.registerElement('box', (owner) => new Box(owner));
document.createElement('box').status; // string, with a real Box instance
```

Declaration merging is a TypeScript contract: install the factory in every
application document that relies on that declaration. Registration affects future
creation, cloning and parsing; it does not upgrade existing nodes. Hyphenated
custom elements use `document.customElements` and its lifecycle model.

## 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.
40 changes: 40 additions & 0 deletions packages/dom/docs/capabilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 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; specialized control, form, interactive, template and slot classes. Document-local `registerElement` factories support terminal names. Declarations must match each document’s registered factories. |
| 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 transitions, logical tabindex/shadow/radio navigation, autofocus, modal isolation, host keydown/keyup/text/selection entry. Cancellable activation and plain-text editing are explicit host defaults; see focus documentation. |
| Text controls | Implemented and tested | Values/defaults/dirty state, type-dependent sanitization, checkedness/radio groups, UTF-16 selection, select/setSelectionRange/setRangeText, numeric/date conversion/stepping, validity and observable state. Host editing supports selection-capable input types and textarea. |
| 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 fragments, inert templates, namespaces/comments, cloning/importing, explicit declarative shadow parsing via setHTMLUnsafe, getHTML shadow serialization, TreeWalker nextNode/parentNode and filtering. |
| Shadow trees and slots | Implemented and tested | Open/closed roots, named/manual/fallback distribution, slot.assign/slotchange, clonable/serializable roots, declarative hydration, connectivity/adoption, event retargeting/composed paths and focus. |
| Forms and activation | Implemented and tested | Form/button/label/fieldset/legend associations, disabled first-legend exception, submit/reset/validation, FormData entry collection and events, cancelable mouse-click activation, label forwarding and implicit host submission. No network submission. |
| Choice and value elements | Implemented and tested | Select/option/optgroup/datalist selection, live protected collections, selectedcontent snapshots, output reset/defaults, progress/meter numeric state and label associations; three unchanged selectedness WPT cases. |
| Disclosure and dialog | Implemented and tested | Details/summary open state and named exclusivity; dialog show/showModal/requestClose/close, cancel/toggle/close events, return values, focus restoration and modal removal cleanup. |
| Shared HTML state | Implemented and tested | Hidden/inert/autofocus, inherited contentEditable/plaintext editing, classList and output.htmlFor token methods with live attributes. Visibility has no CSS/layout component. |
| 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 custom elements/ElementInternals, and complete specification edge-case coverage. |
| Further DOM facilities | Planned but unfinished | CSS selector matching, complete traversal, NodeList/NamedNodeMap and DOMTokenList numeric/legacy properties, style declaration model, DocumentType/processing instructions/XML, complete serialization (including raw-text script/style). |
| Further interaction | Planned but unfinished | IME/clipboard/rich-text editing, editing non-selection input types, shadow editable selection, browser-specific focus/CloseWatcher policy, popovers, file input values/files, localized validation UI and complete control algorithms. See focus and form 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. Interactive HTML adds forms, choice/value controls, details/dialog, host editing, manual slots, declarative shadows and document-local terminal factories. Browser differential observations and explicit divergences are recorded in [interactive standards](interactive-standards.md).
6. 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.
Loading
Loading