Skip to content
Merged
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
201 changes: 109 additions & 92 deletions examples/SafeDOMExample.affine
Original file line number Diff line number Diff line change
@@ -1,112 +1,129 @@
// SPDX-License-Identifier: MPL-2.0
// SafeDOMExample.affine β€” formally-verified DOM mounting (aspirational).
//
// SafeDOMExample.affine β€” Using SafeDOM for formally verified DOM mounting.
// This example shows the *shape* of SafeDOM consumer code in current
// AffineScript syntax. The `SafeDOM` stdlib surface it references
// (`mount_safe`, `mount_when_ready`, `mount_batch`,
// `proven_selector_validate`, `proven_html_validate`, `mount`) is the
// target of `affinescript#56` (DOM+Pixi binding survey) and does not
// yet exist in the published stdlib. The file is therefore
// parse-checked but not type-checked end-to-end until #56 lands the
// bindings; `affinescript check` reports `Resolve.UndefinedModule
// SafeDOM` which is expected.
//
// AffineScript migration of SafeDOMExample.res
// Previous versions of this file (estate-wide, 5 dialect variants)
// pre-dated ADR-014 (qualified paths), ADR-016 (effect rows), and the
// `#{`-record-literal sigil (ADR-215). They were retired in favour of
// this canonical via the gitbot-fleet#208 sweep (2026-05-26).

open SafeDOM
module SafeDOMExample;

// Example 1: Basic mounting with error handling
let mountApp = () => {
mountSafe(
"#app",
"<div><h1>Hello, World!</h1><p>Mounted safely with proofs.</p></div>",
~onSuccess=el => {
Console.log("βœ“ App mounted successfully!")
Console.log("Element:", el)
},
~onError=err => {
Console.error("βœ— Mount failed:", err)
}
)
}
use prelude::{Option, Some, None, Result, Ok, Err};

// Example 2: Wait for DOM ready before mounting
let mountWhenDOMReady = () => {
mountWhenReady(
"#app",
"<div class='container'><h1>App Title</h1></div>",
~onSuccess=_ => Console.log("βœ“ Mounted after DOM ready"),
~onError=err => Console.error("βœ— Failed:", err)
)
// `Element` and friends are nominal extern types for now β€” the real
// shape lands with affinescript#56.
extern type Element;
extern type Selector;
extern type ValidHTML;

// Single-mount status, lifted from the host into a typed tag union.
enum MountStatus {
Mounted(Element),
MountPointNotFound(String),
InvalidSelector(String),
InvalidHTML(String)
}

// Example 3: Batch mounting (atomic - all or nothing)
let mountMultiple = () => {
let specs = [
{selector: "#header", html: "<header><h1>Site Title</h1></header>"},
{selector: "#nav", html: "<nav><a href='/'>Home</a></nav>"},
{selector: "#main", html: "<main><p>Content here</p></main>"},
{selector: "#footer", html: "<footer>Β© 2026</footer>"}
]

switch mountBatch(specs) {
| Ok(elements) => {
Console.log(`βœ“ Successfully mounted ${Array.length(elements)} elements`)
elements->Array.forEach(el => Console.log(" -", el))
}
| Error(err) => {
Console.error("βœ— Batch mount failed:", err)
Console.error(" (None were mounted - atomic operation)")
}
}
// Batch-mount result.
enum MountResult {
Mounted([Element]),
Failed(String)
}

// Example 4: Explicit validation before mounting
let mountWithValidation = () => {
// Validate selector first
switch ProvenSelector.validate("#my-app") {
| Error(e) => Console.error(`Invalid selector: ${e}`)
| Ok(validSelector) => {
// Validate HTML
switch ProvenHTML.validate("<div>Content</div>") {
| Error(e) => Console.error(`Invalid HTML: ${e}`)
| Ok(validHtml) => {
// Now mount with proven safety
switch mount(validSelector, validHtml) {
| Mounted(el) => Console.log("βœ“ Mounted with validated inputs:", el)
| MountPointNotFound(s) => Console.error(`βœ— Element not found: ${s}`)
| InvalidSelector(_) => Console.error("Impossible - already validated")
| InvalidHTML(_) => Console.error("Impossible - already validated")
}
}
}
}
// Spec for one element in a batch mount.
struct MountSpec {
selector: String,
html: String
}

// Example 5: Integration with TEA
module MyApp = {
type model = {message: string}
type msg = NoOp
// SafeDOM's host-side surface, all IO-effecting. Callbacks are passed
// as separate parameters (rather than a `MountCallbacks` record)
// because fn-typed struct fields are not currently parser-supported.
extern fn mount_safe(
selector: ref String,
html: ref String,
on_success: fn(Element) -> (),
on_error: fn(String) -> (),
) -{IO}-> ();

let init = () => {message: "Hello from TEA"}
let update = (model, _msg) => model
let view = model => `<div><h1>${model.message}</h1></div>`
}
extern fn mount_when_ready(
selector: ref String,
html: ref String,
on_success: fn(Element) -> (),
on_error: fn(String) -> (),
) -{IO}-> ();

let mountTEAApp = () => {
let model = MyApp.init()
let html = MyApp.view(model)
extern fn mount_batch(specs: ref [MountSpec]) -{IO}-> MountResult;

mountWhenReady(
"#tea-app",
html,
~onSuccess=el => {
Console.log("βœ“ TEA app mounted")
// Set up event handlers, subscriptions here
},
~onError=err => Console.error(`βœ— TEA mount failed: ${err}`)
)
extern fn proven_selector_validate(s: ref String) -{IO}-> Result<Selector, String>;
extern fn proven_html_validate(s: ref String) -{IO}-> Result<ValidHTML, String>;
extern fn mount(sel: ref Selector, html: ref ValidHTML) -{IO}-> MountStatus;

extern fn array_for_each(xs: ref [Element], f: fn(Element) -> ()) -{IO}-> ();
extern fn array_len(xs: ref [Element]) -> Int;

// Example 1 β€” basic mount with success/error branches.
pub fn mount_app() -{IO}-> () {
mount_safe(
"#app",
"<div><h1>Hello, World!</h1><p>Mounted safely with proofs.</p></div>",
fn(el) -> () { Console::log("App mounted successfully"); },
fn(err) -> () { Console::error("Mount failed: " ++ err); },
);
}

// Entry point
let main = () => {
Console.log("SafeDOM Examples")
Console.log("================\n")
// Example 2 β€” defer until DOM ready.
pub fn mount_when_dom_ready() -{IO}-> () {
mount_when_ready(
"#app",
"<div class='container'><h1>App Title</h1></div>",
fn(_el) -> () { Console::log("Mounted after DOM ready"); },
fn(err) -> () { Console::error("Failed: " ++ err); },
);
}

// Choose which example to run
mountWhenDOMReady() // Run on DOM ready
// Example 3 β€” atomic batch mount.
pub fn mount_multiple() -{IO}-> () {
let specs = [
MountSpec #{ selector: "#header", html: "<header><h1>Site Title</h1></header>" },
MountSpec #{ selector: "#nav", html: "<nav><a href='/'>Home</a></nav>" },
MountSpec #{ selector: "#main", html: "<main><p>Content here</p></main>" },
MountSpec #{ selector: "#footer", html: "<footer>2026</footer>" },
];

match mount_batch(specs) {
Mounted(elements) => {
Console::log("Batch mount succeeded");
array_for_each(elements, fn(_el) -> () { Console::log(" element"); });
},
Failed(err) => {
Console::error("Batch mount failed (atomic β€” none mounted): " ++ err);
}
}
}

// Auto-execute when module loads
main()
// Example 4 β€” explicit two-stage validation before mounting.
pub fn mount_with_validation() -{IO}-> () {
match proven_selector_validate("#my-app") {
Err(e) => Console::error("Invalid selector: " ++ e),
Ok(valid_selector) => match proven_html_validate("<div>Content</div>") {
Err(e) => Console::error("Invalid HTML: " ++ e),
Ok(valid_html) => match mount(valid_selector, valid_html) {
Mounted(_el) => Console::log("Mounted with validated inputs"),
MountPointNotFound(s) => Console::error("Element not found: " ++ s),
InvalidSelector(_) => Console::error("impossible β€” already validated"),
InvalidHTML(_) => Console::error("impossible β€” already validated"),
},
},
}
}
Loading