├── clippy.toml ├── book ├── src │ ├── 99-dev │ │ ├── 00-index.md │ │ └── 01-releases.md │ ├── 01-targets │ │ ├── 00-index.md │ │ ├── 01-web.md │ │ └── 99-new.md │ ├── 01-values.md │ ├── SUMMARY.md │ └── 00-intro.md └── book.toml ├── dom ├── raf │ ├── CHANGELOG.md │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── CHANGELOG.md ├── augdom │ ├── CHANGELOG.md │ └── Cargo.toml ├── examples │ ├── todo │ │ ├── e2e │ │ │ ├── cypress │ │ │ │ ├── fixtures │ │ │ │ │ └── example.json │ │ │ │ └── support │ │ │ │ │ ├── index.js │ │ │ │ │ └── commands.js │ │ │ ├── cypress.json │ │ │ └── package.json │ │ ├── Cargo.toml │ │ ├── src │ │ │ ├── header.rs │ │ │ ├── footer.rs │ │ │ ├── input.rs │ │ │ ├── filter.rs │ │ │ ├── lib.rs │ │ │ ├── item.rs │ │ │ ├── integration_tests.rs │ │ │ └── main_section.rs │ │ ├── index.html │ │ └── README.md │ ├── hacking │ │ ├── README.md │ │ ├── index.html │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── ssr │ │ ├── README.md │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── dom_builder │ │ ├── Cargo.toml │ │ ├── src │ │ │ └── lib.rs │ │ └── index.html │ ├── counter_fn │ │ ├── Cargo.toml │ │ ├── src │ │ │ └── lib.rs │ │ └── index.html │ └── drivertest │ │ ├── Cargo.toml │ │ └── src │ │ └── lib.rs ├── local-wasm-pack │ ├── wasm-pack.rs │ └── Cargo.toml ├── src │ ├── interfaces │ │ ├── mod.rs │ │ ├── event_target.rs │ │ ├── element.rs │ │ ├── security.rs │ │ ├── node.rs │ │ ├── content_categories.rs │ │ ├── html_element.rs │ │ └── global_events.rs │ ├── text.rs │ ├── elements │ │ └── interactive.rs │ ├── embed.rs │ ├── lib.rs │ ├── cached_node.rs │ └── elements.rs ├── prettiest │ ├── Cargo.toml │ └── CHANGELOG.md ├── tests │ ├── dom_builder.rs │ └── custom_component.rs └── Cargo.toml ├── .netlify ├── config.json └── state.json ├── favicon.ico ├── assets └── logo.png ├── rustfmt.toml ├── netlify.toml ├── illicit ├── src │ ├── snapshots │ │ ├── illicit__tests__layer_debug_impl.snap │ │ └── illicit__tests__failure_error.snap │ └── anon_rc.rs ├── benches │ └── basic_env.rs ├── macro │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── Cargo.toml └── CHANGELOG.md ├── .gitignore ├── .github └── dependabot.yml ├── topo ├── tests │ └── simple.rs ├── macro │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── Cargo.toml ├── benches │ └── simple_calls.rs ├── src │ └── slot.rs └── CHANGELOG.md ├── index.css ├── mox ├── Cargo.toml ├── tests │ ├── simple_builder.rs │ ├── dashes.rs │ └── derive_builder.rs └── CHANGELOG.md ├── ofl ├── reloadOnChanges.js ├── Cargo.toml └── src │ ├── format.rs │ ├── main.rs │ ├── server │ ├── run.rs │ ├── session.rs │ └── inject.rs │ ├── published.rs │ ├── workspace.rs │ └── website.rs ├── dyn-cache ├── Cargo.toml ├── CHANGELOG.md └── src │ └── cache_cell.rs ├── README.md ├── LICENSE-MIT ├── benches └── core.rs ├── src ├── testing.rs └── runtime │ ├── var.rs │ ├── runloop.rs │ └── context.rs ├── Cargo.toml ├── tests └── issue_238.rs ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── .cargo └── config.toml ├── CONTRIBUTING.md └── .vscode └── tasks.json /clippy.toml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /book/src/99-dev/00-index.md: -------------------------------------------------------------------------------- 1 | TODO -------------------------------------------------------------------------------- /book/src/01-targets/00-index.md: -------------------------------------------------------------------------------- 1 | TODO -------------------------------------------------------------------------------- /book/src/99-dev/01-releases.md: -------------------------------------------------------------------------------- 1 | TODO -------------------------------------------------------------------------------- /dom/raf/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # raf releases -------------------------------------------------------------------------------- /dom/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # moxie-dom releases 2 | 3 | TODO -------------------------------------------------------------------------------- /dom/augdom/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # augdom releases 2 | 3 | TODO -------------------------------------------------------------------------------- /.netlify/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "telemetryDisabled": true 3 | } -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp/moxie/HEAD/favicon.ico -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp/moxie/HEAD/assets/logo.png -------------------------------------------------------------------------------- /.netlify/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "siteId": "3ad3f9c1-495b-4558-987c-ab0363f47651" 3 | } -------------------------------------------------------------------------------- /dom/examples/todo/e2e/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "example": "fixture" 3 | } -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | newline_style = "Unix" 2 | use_field_init_shorthand = true 3 | use_small_heuristics = "Max" 4 | -------------------------------------------------------------------------------- /book/src/01-values.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | * project values 4 | * empathy 5 | * respect 6 | * sharing 7 | * dialogue 8 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "target/website" 3 | 4 | ## more info on configuring this file: https://www.netlify.com/docs/netlify-toml-reference/ 5 | -------------------------------------------------------------------------------- /dom/examples/todo/e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectId": "9vrkgj", 3 | "baseUrl": "http://localhost:8000/dom/examples/todo", 4 | "fixturesFolder": false, 5 | "pluginsFile": false 6 | } -------------------------------------------------------------------------------- /illicit/src/snapshots/illicit__tests__layer_debug_impl.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: illicit/src/lib.rs 3 | expression: "format!(\"{:?}\", snapshot)" 4 | --- 5 | Snapshot { current: Layer { u8: 1 } } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | target/ 3 | **/*.rs.bk 4 | /Cargo.lock 5 | *.log 6 | *.new 7 | node_modules/ 8 | .idea/ 9 | pkg/ 10 | cargo-timing*.html 11 | **/cypress/screenshots 12 | **/cypress/videos -------------------------------------------------------------------------------- /dom/examples/hacking/README.md: -------------------------------------------------------------------------------- 1 | # dom hacking example 2 | 3 | scratch space for standing up web/dom bindings 4 | 5 | in two terminals: 6 | 7 | ``` 8 | $ cargo dom-flow 9 | $ cargo server 10 | ``` 11 | -------------------------------------------------------------------------------- /book/src/01-targets/01-web.md: -------------------------------------------------------------------------------- 1 | 2 | * moxie-dom 3 | * getting started, examples 4 | * creating elements 5 | * mounting elements 6 | * element attributes 7 | * element event handling 8 | * use of call slots 9 | -------------------------------------------------------------------------------- /illicit/src/snapshots/illicit__tests__failure_error.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: illicit/src/lib.rs 3 | expression: e 4 | --- 5 | expected a `u8` from the environment, did not find it in current env: Snapshot { current: Layer } 6 | -------------------------------------------------------------------------------- /dom/local-wasm-pack/wasm-pack.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | 3 | fn main() { 4 | pretty_env_logger::formatted_timed_builder().init(); 5 | wasm_pack::command::run_wasm_pack(wasm_pack::Cli::from_args().cmd).unwrap(); 6 | } 7 | -------------------------------------------------------------------------------- /dom/src/interfaces/mod.rs: -------------------------------------------------------------------------------- 1 | //! Traits which correspond to the web platform's class interfaces. 2 | 3 | pub mod content_categories; 4 | pub mod element; 5 | pub mod event_target; 6 | pub mod global_events; 7 | pub mod html_element; 8 | pub mod node; 9 | pub mod security; 10 | -------------------------------------------------------------------------------- /book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | [Introduction](00-intro.md) 2 | [Project Values](01-values.md) 3 | 4 | * [Targets](01-targets/00-index.md) 5 | * [The web](01-targets/01-web.md) 6 | * [Adding new targets](01-targets/99-new.md) 7 | * [Contributing](99-dev/00-index.md) 8 | * [Releases](99-dev/01-releases.md) 9 | -------------------------------------------------------------------------------- /dom/examples/todo/e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "cypress-example-todomvc-e2e-tests", 4 | "version": "0.0.0-development", 5 | "devDependencies": { 6 | "axe-core": "4.0.2", 7 | "cypress": "^5.2.0", 8 | "cypress-axe": "0.8.1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | title = "the moxie project" 3 | author = "the moxie developers" 4 | description = "prose related to the moxie project, for publication to the website" 5 | 6 | [build] 7 | build-dir = "pkg" 8 | create-missing = false 9 | 10 | [preprocessor.index] 11 | 12 | [preprocessor.links] 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "13:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: quick-xml 11 | versions: 12 | - 0.21.0 13 | - dependency-name: hashbrown 14 | versions: 15 | - 0.10.0 16 | -------------------------------------------------------------------------------- /illicit/benches/basic_env.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate criterion; 3 | 4 | use criterion::Criterion; 5 | 6 | fn enter_small_env(c: &mut Criterion) { 7 | c.bench_function("enter a small illicit env", |b| { 8 | b.iter(|| illicit::Layer::new().offer(10u128).enter(|| ())); 9 | }); 10 | } 11 | 12 | criterion::criterion_group!(benches, enter_small_env,); 13 | criterion::criterion_main!(benches); 14 | -------------------------------------------------------------------------------- /dom/examples/ssr/README.md: -------------------------------------------------------------------------------- 1 | # moxie server-side rendering example 2 | 3 | A proof-of-concept implementation of rendering HTML in gotham using moxie-dom without any browser 4 | dependencies. 5 | 6 | `cargo run` starts a server that listens on `127.0.0.1:7878`, serving HTML based on the URL after 7 | `/paths/*`. 8 | 9 | `cargo test` uses gotham's (very nice) test server tool to verify the behavior matches what we 10 | expect. 11 | -------------------------------------------------------------------------------- /topo/tests/simple.rs: -------------------------------------------------------------------------------- 1 | use topo::*; 2 | 3 | #[test] 4 | fn invoke_test_topo() { 5 | #[topo::nested] 6 | fn unique_id() -> CallId { 7 | CallId::current() 8 | } 9 | 10 | topo::call(|| { 11 | let mut prev = unique_id(); 12 | for _ in 0..10 { 13 | let current = unique_id(); 14 | assert_ne!(prev, current, "each CallId must be unique"); 15 | prev = current; 16 | } 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /topo/macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "topo-macro" 3 | version = "0.10.0" 4 | description = "procedural macros for the topo crate" 5 | readme = "../CHANGELOG.md" 6 | 7 | # update here, update everywhere! 8 | license = "MIT/Apache-2.0" 9 | homepage = "https://moxie.rs" 10 | repository = "https://github.com/anp/moxie.git" 11 | authors = ["Adam Perry "] 12 | edition = "2018" 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [dependencies] 18 | quote = "1.0" 19 | syn = { version = "1.0", features = ["full"] } 20 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | .content { 2 | margin: 0 auto; 3 | padding: 0 2em; 4 | max-width: 800px; 5 | margin-bottom: 50px; 6 | line-height: 1.6em; 7 | } 8 | 9 | .header { 10 | margin: 0; 11 | color: #333; 12 | text-align: center; 13 | padding: 2.5em 2em 0; 14 | border-bottom: 1px solid #eee; 15 | } 16 | 17 | .header h1 { 18 | margin: 0.2em 0; 19 | font-size: 3em; 20 | font-weight: 300; 21 | } 22 | 23 | .header h2 { 24 | font-weight: 300; 25 | color: #ccc; 26 | padding: 0; 27 | margin-top: 0; 28 | } -------------------------------------------------------------------------------- /dom/examples/todo/e2e/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your other test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/guides/configuration#section-global 10 | // *********************************************************** 11 | 12 | require('./commands') 13 | require('cypress-axe') 14 | -------------------------------------------------------------------------------- /dom/examples/dom_builder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dom-builder-moxie-dom-fn" 3 | version = "0.1.0" 4 | publish = false 5 | description = "an example counter for moxie-dom using the DOM builder API" 6 | edition = "2018" 7 | license-file = "../../../../LICENSE-MIT" 8 | repository = "https://github.com/anp/moxie.git" 9 | 10 | [package.metadata.wasm-pack.profile.release] 11 | wasm-opt = false 12 | 13 | [lib] 14 | crate-type = [ "cdylib" ] 15 | 16 | [dependencies] 17 | moxie = { path = "../../.." } 18 | moxie-dom = { path = "../../" } 19 | wasm-bindgen = "0.2" 20 | -------------------------------------------------------------------------------- /illicit/macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "illicit-macro" 3 | version = "1.0.0" 4 | description = "procedural macros for the illicit crate" 5 | readme = "../CHANGELOG.md" 6 | 7 | # update here, update everywhere! 8 | license = "MIT/Apache-2.0" 9 | homepage = "https://moxie.rs" 10 | repository = "https://github.com/anp/moxie.git" 11 | authors = ["Adam Perry "] 12 | edition = "2018" 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [dependencies] 18 | proc-macro-error = "1.0.0" 19 | quote = "1.0" 20 | syn = { version = "1.0", features = ["full"] } 21 | -------------------------------------------------------------------------------- /dom/examples/counter_fn/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "counter-moxie-dom-fn" 3 | version = "0.1.0" 4 | publish = false 5 | description = "an example counter for moxie-dom" 6 | edition = "2018" 7 | license-file = "../../../../LICENSE-MIT" 8 | repository = "https://github.com/anp/moxie.git" 9 | 10 | [package.metadata.wasm-pack.profile.release] 11 | wasm-opt = false 12 | 13 | [lib] 14 | crate-type = [ "cdylib" ] 15 | 16 | [dependencies] 17 | mox = { path = "../../../mox" } 18 | moxie = { path = "../../.." } 19 | moxie-dom = { path = "../../" } 20 | wasm-bindgen = "0.2" 21 | -------------------------------------------------------------------------------- /dom/examples/hacking/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | dom hacking 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /dom/examples/ssr/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ssr-poc" 3 | version = "0.1.0" 4 | publish = false 5 | authors = ["Adam Perry "] 6 | edition = "2018" 7 | description = "proof of concept for server-side rendered HTML with moxie-dom" 8 | 9 | [dependencies] 10 | augdom = { path = "../../augdom" } 11 | gotham = "0.6.0" 12 | gotham_derive = "0.6.0" 13 | hyper = "0.14" 14 | mox = { path = "../../../mox" } 15 | moxie = { path = "../../../" } 16 | serde = "1" 17 | serde_derive = "1" 18 | topo = { path = "../../../topo" } 19 | 20 | [dependencies.moxie-dom] 21 | path = "../../" 22 | default-features = false 23 | features = [ "rsdom" ] -------------------------------------------------------------------------------- /dom/local-wasm-pack/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "local-wasm-pack" 3 | version = "0.1.0" 4 | authors = ["Adam Perry "] 5 | edition = "2018" 6 | publish = false 7 | description = "uses wasm-pack as a library to have cargo install it for us" 8 | 9 | [[bin]] 10 | path = "wasm-pack.rs" 11 | name = "wasm-pack" 12 | 13 | [dependencies] 14 | pretty_env_logger = "0.3" 15 | structopt = "0.2" 16 | 17 | [dependencies.wasm-pack] 18 | version = "0.9.1" 19 | # TODO https://github.com/rustwasm/wasm-pack/issues/954 go back to upstream 20 | git = "https://github.com/anp/wasm-pack.git" 21 | branch = "apple-silicon-rosetta" 22 | 23 | [workspace] 24 | -------------------------------------------------------------------------------- /dom/examples/dom_builder/src/lib.rs: -------------------------------------------------------------------------------- 1 | use moxie_dom::{elements::html::*, prelude::*}; 2 | use wasm_bindgen::prelude::*; 3 | 4 | /// The counter_fn example, but using the DOM builder API. 5 | #[wasm_bindgen] 6 | pub fn boot(root: moxie_dom::raw::sys::Node) { 7 | moxie_dom::boot(root, || { 8 | let (count, incrementer) = state(|| 0); 9 | let decrementer = incrementer.clone(); 10 | 11 | div() 12 | .child(button().onclick(move |_| decrementer.mutate(|count| *count -= 1)).child("-")) 13 | .child(count) 14 | .child(button().onclick(move |_| incrementer.mutate(|count| *count += 1)).child("+")) 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /dom/examples/counter_fn/src/lib.rs: -------------------------------------------------------------------------------- 1 | use mox::mox; 2 | use moxie_dom::{elements::html::*, prelude::*}; 3 | use wasm_bindgen::prelude::*; 4 | 5 | #[wasm_bindgen] 6 | pub fn boot(root: moxie_dom::raw::sys::Node) { 7 | moxie_dom::boot(root, || { 8 | let (count, incrementer) = state(|| 0); 9 | let decrementer = incrementer.clone(); 10 | mox! { 11 |
12 | 13 | { count } 14 | 15 |
16 | } 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /dom/raf/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "raf" 3 | version = "0.2.0-pre" 4 | description = "browser event loop scheduler using requestAnimationFrame" 5 | categories = ["asynchronous", "concurrency", "gui", "wasm", "web-programming"] 6 | keywords = ["scheduler", "events", "requestAnimationFrame"] 7 | readme = "CHANGELOG.md" 8 | 9 | # update here, update everywhere! 10 | license = "MIT/Apache-2.0" 11 | homepage = "https://moxie.rs" 12 | repository = "https://github.com/anp/moxie.git" 13 | authors = ["Adam Perry "] 14 | edition = "2018" 15 | 16 | [dependencies] 17 | futures = "0.3.5" 18 | wasm-bindgen = "0.2.48" 19 | web-sys = { version = "0.3.28", features = ["Window"] } 20 | -------------------------------------------------------------------------------- /mox/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mox" 3 | version = "0.12.0" 4 | description = "Mockery Of X(ML): a JSX-alike syntax for the builder pattern." 5 | categories = ["gui", "rust-patterns"] 6 | keywords = ["jsx", "xml", "builder"] 7 | readme = "CHANGELOG.md" 8 | 9 | # update here, update everywhere! 10 | license = "MIT/Apache-2.0" 11 | homepage = "https://moxie.rs" 12 | repository = "https://github.com/anp/moxie.git" 13 | authors = ["Adam Perry "] 14 | edition = "2018" 15 | 16 | [lib] 17 | proc-macro = true 18 | 19 | [dependencies] 20 | proc-macro2 = "1" 21 | quote = "1" 22 | syn-rsx = "0.8.0-beta.2" 23 | syn = "^1" 24 | 25 | [dev-dependencies] 26 | derive_builder = "0.10" 27 | -------------------------------------------------------------------------------- /dom/examples/drivertest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "drivertest" 3 | publish = false 4 | version = "0.1.0" 5 | authors = ["Adam Perry "] 6 | edition = "2018" 7 | 8 | [package.metadata.wasm-pack.profile.release] 9 | wasm-opt = false 10 | 11 | [dependencies] 12 | augdom = { path = "../../augdom", features = ["rsdom"] } 13 | mox = { path = "../../../mox" } 14 | moxie = { path = "../../../" } 15 | moxie-dom = { path = "../../", features = ["rsdom"] } 16 | topo = { path = "../../../topo" } 17 | wasm-bindgen = "0.2.51" 18 | wasm-bindgen-test = "0.3" 19 | 20 | [dependencies.web-sys] 21 | version = "0.3.28" 22 | features = [ 23 | "Element", 24 | "Node", 25 | "HtmlElement", 26 | ] 27 | -------------------------------------------------------------------------------- /dom/examples/counter_fn/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | moxie-dom • counter 10 | 11 | 12 | 13 |
14 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /dom/examples/dom_builder/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | moxie-dom • builder 10 | 11 | 12 | 13 |
14 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /dom/examples/hacking/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dom-hacking" 3 | description = "simple project for hacking on new moxie-dom api" 4 | version = "0.1.0-pre" 5 | publish = false 6 | edition = "2018" 7 | license-file = "../../../../LICENSE-MIT" 8 | repository = "https://github.com/anp/moxie.git" 9 | 10 | [package.metadata.wasm-pack.profile.release] 11 | wasm-opt = false 12 | 13 | [lib] 14 | crate-type = [ "cdylib" ] 15 | 16 | [dependencies] 17 | console_log = "0.2.0" 18 | mox = { path = "../../../mox" } 19 | moxie-dom = { path = "../../" } 20 | topo = { path = "../../../topo" } 21 | tracing = { version = "^0.1", features = ["log"] } 22 | wasm-bindgen = "0.2.48" 23 | 24 | [dev-dependencies] 25 | augdom = { path = "../../augdom" } 26 | wasm-bindgen-test = "0.3" 27 | -------------------------------------------------------------------------------- /illicit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "illicit" 3 | version = "1.1.2" 4 | description = "An implicit thread-local environment which is indexed by type." 5 | categories = ["rust-patterns"] 6 | keywords = ["context", "environment", "global", "singleton"] 7 | readme = "CHANGELOG.md" 8 | 9 | # update here, update everywhere! 10 | license = "MIT/Apache-2.0" 11 | homepage = "https://moxie.rs" 12 | repository = "https://github.com/anp/moxie.git" 13 | authors = ["Adam Perry "] 14 | edition = "2018" 15 | 16 | [dependencies] 17 | illicit-macro = { path = "macro", version = "1.0.0"} 18 | owning_ref = "0.4" 19 | scopeguard = "1" 20 | 21 | [dev-dependencies] 22 | criterion = "0.3" 23 | insta = "1.0.0" 24 | 25 | [[bench]] 26 | name = "basic_env" 27 | harness = false 28 | -------------------------------------------------------------------------------- /dom/examples/todo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "todomvc-moxie" 3 | description = "TodoMVC clone with moxie-dom" 4 | version = "0.1.0" 5 | publish = false 6 | edition = "2018" 7 | license-file = "../../../../LICENSE-MIT" 8 | repository = "https://github.com/anp/moxie.git" 9 | 10 | [package.metadata.wasm-pack.profile.release] 11 | wasm-opt = false 12 | 13 | [lib] 14 | crate-type = [ "cdylib" ] 15 | 16 | [dependencies] 17 | console_error_panic_hook = "0.1.6" 18 | illicit = { path = "../../../illicit" } 19 | mox = { path = "../../../mox" } 20 | moxie-dom = { path = "../../" } 21 | topo = { path = "../../../topo" } 22 | tracing = { version = "^0.1", features = [ "log" ] } 23 | tracing-wasm = "0.2.0" 24 | wasm-bindgen = "0.2" 25 | 26 | [dev-dependencies] 27 | pretty_assertions = "1.0" 28 | wasm-bindgen-test = "0.3" 29 | -------------------------------------------------------------------------------- /ofl/reloadOnChanges.js: -------------------------------------------------------------------------------- 1 | let timeoutExp = 6; 2 | let changes = null; 3 | 4 | function listenToChangeEvents() { 5 | try { 6 | changes = new WebSocket(`ws://${(location.host || "[::1]:8000")}/ch-ch-ch-changes`); 7 | } catch (e) { 8 | changes.error(e); 9 | } 10 | 11 | changes.onclose = () => { 12 | let timeout = Math.pow(2, timeoutExp); 13 | timeoutExp += 1; 14 | console.log('livereload socket closed, scheduling reconnect in', timeout, 'ms'); 15 | setTimeout(listenToChangeEvents, timeout); 16 | }; 17 | changes.onerror = ({ data: error }) => { 18 | console.error('livereload error', error); 19 | changes.close(); 20 | }; 21 | changes.onmessage = ({ data }) => { 22 | location.reload(); 23 | }; 24 | } 25 | 26 | listenToChangeEvents(); -------------------------------------------------------------------------------- /dyn-cache/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dyn-cache" 3 | version = "0.12.2" 4 | description = "Query cache indexed by type." 5 | categories = ["caching", "rust-patterns"] 6 | keywords = ["incremental", "memoize", "intern", "cache"] 7 | readme = "CHANGELOG.md" 8 | 9 | # update here, update everywhere! 10 | license = "MIT/Apache-2.0" 11 | homepage = "https://moxie.rs" 12 | repository = "https://github.com/anp/moxie.git" 13 | authors = ["Adam Perry "] 14 | edition = "2018" 15 | 16 | [features] 17 | default = [] 18 | wasm-bindgen = [ "parking_lot/wasm-bindgen" ] 19 | 20 | [dependencies] 21 | downcast-rs = "1.1.1" 22 | hash_hasher = "2.0.3" 23 | hashbrown = "0.12.0" 24 | illicit = { path = "../illicit", version = "1.1.2"} 25 | parking_lot = "0.11.0" 26 | paste = "1.0.0" 27 | 28 | [dev-dependencies] 29 | scopeguard = "1" 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | moxie logo 2 | 3 | # moxie 4 | 5 | ![crates.io](https://img.shields.io/crates/v/moxie) 6 | ![License](https://img.shields.io/crates/l/moxie.svg) 7 | [![codecov](https://codecov.io/gh/anp/moxie/branch/main/graph/badge.svg)](https://codecov.io/gh/anp/moxie) 8 | 9 | ## More Information 10 | 11 | For more information about the moxie project, see the [website](https://moxie.rs). 12 | 13 | ## Contributing and Code of Conduct 14 | 15 | See [CONTRIBUTING.md](CONTRIBUTING.md) for overall contributing info and [CONDUCT.md](CODE_OF_CONDUCT.md) 16 | for the project's Code of Conduct. The project is still early in its lifecycle but we welcome 17 | anyone interested in getting involved. 18 | 19 | ## License 20 | 21 | Licensed under either of 22 | 23 | * [Apache License, Version 2.0](LICENSE-APACHE) 24 | * [MIT license](LICENSE-MIT) 25 | 26 | at your option. 27 | -------------------------------------------------------------------------------- /dom/prettiest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "prettiest" 3 | version = "0.2.1" 4 | description = "Pretty-printer for JS values from wasm-bindgen." 5 | categories = ["web"] 6 | keywords = ["debug", "pretty", "javascript"] 7 | readme = "CHANGELOG.md" 8 | 9 | # update here, update everywhere! 10 | license = "MIT/Apache-2.0" 11 | homepage = "https://moxie.rs" 12 | repository = "https://github.com/anp/moxie.git" 13 | authors = ["Adam Perry "] 14 | edition = "2018" 15 | 16 | [dependencies] 17 | js-sys = "0.3.25" 18 | scopeguard = "1.1.0" 19 | wasm-bindgen = "0.2.48" 20 | 21 | [dependencies.web-sys] 22 | version = "0.3.28" 23 | features = [ 24 | "Document", 25 | "Element", 26 | "Event", 27 | "EventTarget", 28 | "HtmlElement", 29 | "KeyboardEvent", 30 | "KeyboardEventInit", 31 | "Window", 32 | ] 33 | 34 | [dev-dependencies] 35 | futures = "0.3.5" 36 | wasm-bindgen-test = "0.3" 37 | -------------------------------------------------------------------------------- /dom/examples/todo/src/header.rs: -------------------------------------------------------------------------------- 1 | use crate::{input::text_input, Todo}; 2 | use mox::mox; 3 | use moxie_dom::{ 4 | elements::sectioning::{h1, header, Header}, 5 | prelude::*, 6 | }; 7 | use tracing::info; 8 | 9 | #[topo::nested] 10 | #[illicit::from_env(todos: &Key>)] 11 | pub fn input_header() -> Header { 12 | let todos = todos.clone(); 13 | mox! { 14 |
15 |

"todos"

16 | { text_input( 17 | "What needs to be done?", 18 | false, 19 | move |value: String| { 20 | todos.update(|prev| { 21 | let mut todos: Vec = prev.to_vec(); 22 | todos.push(Todo::new(value)); 23 | info!({ ?todos }, "added new todo"); 24 | Some(todos) 25 | }); 26 | }, 27 | )} 28 |
29 | } 30 | } 31 | -------------------------------------------------------------------------------- /topo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "topo" 3 | version = "0.13.2" 4 | description = "Tools for incrementally computing repeated callgraphs." 5 | categories = ["caching", "data-structures", "gui", "memory-management", "rust-patterns"] 6 | keywords = ["cache", "memoize", "intern", "topology", "incremental"] 7 | readme = "CHANGELOG.md" 8 | 9 | # update here, update everywhere! 10 | license = "MIT/Apache-2.0" 11 | homepage = "https://moxie.rs" 12 | repository = "https://github.com/anp/moxie.git" 13 | authors = ["Adam Perry "] 14 | edition = "2018" 15 | 16 | [features] 17 | default = [] 18 | wasm-bindgen = [ "dyn-cache/wasm-bindgen", "parking_lot/wasm-bindgen" ] 19 | 20 | [dependencies] 21 | dyn-cache = { path = "../dyn-cache", version = "0.12.2"} 22 | illicit = { path = "../illicit", version = "1.1.2"} 23 | once_cell = "1.4.0" 24 | parking_lot = "0.11.0" 25 | topo-macro = { path = "macro", version = "0.10.0"} 26 | 27 | [dev-dependencies] 28 | criterion = "0.3" 29 | 30 | [[bench]] 31 | name = "simple_calls" 32 | harness = false 33 | -------------------------------------------------------------------------------- /dom/examples/todo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | moxie-dom • TodoMVC 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 |

Double-click to edit a todo

19 | 20 |

Created by anp

21 |

Part of moxie-dom

22 |
23 | 24 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Adam Perry 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /dom/src/text.rs: -------------------------------------------------------------------------------- 1 | //! Text nodes in the DOM. 2 | 3 | use crate::{ 4 | cached_node::CachedNode, 5 | interfaces::{ 6 | content_categories::{FlowContent, PhrasingContent}, 7 | node::NodeBuilder, 8 | }, 9 | prelude::*, 10 | }; 11 | use moxie::cache; 12 | 13 | /// Create a [DOM text node](https://developer.mozilla.org/en-US/docs/Web/API/Text). 14 | /// This is normally called by the `moxie::mox!` macro. 15 | #[topo::nested] 16 | pub fn text(s: impl AsRef) -> Text { 17 | let text_node = cache(s.as_ref(), |s| document().create_text_node(s)); 18 | Text(CachedNode::new(text_node)) 19 | } 20 | 21 | /// A text node in the DOM. 22 | #[must_use = "needs to be bound to a parent"] 23 | pub struct Text(CachedNode); 24 | 25 | impl NodeBuilder for Text { 26 | type Output = Self; 27 | 28 | fn build(self) -> Self::Output { 29 | self 30 | } 31 | } 32 | 33 | impl crate::interfaces::node::sealed::Memoized for Text { 34 | fn node(&self) -> &CachedNode { 35 | &self.0 36 | } 37 | } 38 | impl crate::interfaces::node::NodeWrapper for Text {} 39 | 40 | impl FlowContent for Text {} 41 | impl PhrasingContent for Text {} 42 | -------------------------------------------------------------------------------- /mox/tests/simple_builder.rs: -------------------------------------------------------------------------------- 1 | use mox::mox; 2 | 3 | #[derive(Debug, PartialEq)] 4 | struct Tag { 5 | name: String, 6 | children: Vec, 7 | } 8 | 9 | fn built() -> TagBuilder { 10 | TagBuilder::default() 11 | } 12 | 13 | #[derive(Default)] 14 | struct TagBuilder { 15 | name: Option, 16 | children: Vec, 17 | } 18 | 19 | impl TagBuilder { 20 | fn name(mut self, name: impl Into) -> Self { 21 | self.name = Some(name.into()); 22 | self 23 | } 24 | 25 | fn child(mut self, child: TagBuilder) -> Self { 26 | self.children.push(child.build()); 27 | self 28 | } 29 | 30 | fn build(self) -> Tag { 31 | Tag { name: self.name.unwrap(), children: self.children } 32 | } 33 | } 34 | 35 | #[test] 36 | fn simple() { 37 | let expected = Tag { 38 | name: String::from("alice"), 39 | children: vec![Tag { name: String::from("bob"), children: vec![] }], 40 | }; 41 | 42 | assert_eq!( 43 | mox! { 44 | 45 | 46 | 47 | }, 48 | expected 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /dom/tests/dom_builder.rs: -------------------------------------------------------------------------------- 1 | use mox::mox; 2 | use moxie_dom::{ 3 | elements::html::div, 4 | interfaces::node::{Child, NodeBuilder}, 5 | prelude::*, 6 | }; 7 | use wasm_bindgen_test::*; 8 | 9 | wasm_bindgen_test_configure!(run_in_browser); 10 | 11 | pub async fn render_test(render_child: impl FnMut() -> Root + 'static, expected: &str) 12 | where 13 | Root: Child + 'static, 14 | { 15 | let test_root = document().create_element("div"); 16 | moxie_dom::boot(test_root.clone(), render_child); 17 | 18 | assert_eq!(test_root.first_child().unwrap().to_string(), expected,); 19 | } 20 | 21 | #[wasm_bindgen_test] 22 | pub async fn simple_mox() { 23 | render_test(|| mox!(
"test text"
), "
test text
").await; 24 | } 25 | 26 | #[wasm_bindgen_test] 27 | pub async fn block_mox() { 28 | render_test(|| mox!(
{"test text"}
), "
test text
").await; 29 | } 30 | 31 | #[wasm_bindgen_test] 32 | pub async fn simple_builder() { 33 | render_test(|| div().child(text("test text")), "
test text
").await; 34 | } 35 | 36 | #[wasm_bindgen_test] 37 | pub async fn built_builder() { 38 | render_test(|| div().child(text("test text").build()).build(), "
test text
").await; 39 | } 40 | -------------------------------------------------------------------------------- /benches/core.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate criterion; 3 | 4 | use criterion::{BenchmarkId, Criterion}; 5 | use moxie::{ 6 | once, 7 | runtime::{Revision, RunLoop}, 8 | }; 9 | use std::rc::Rc; 10 | 11 | criterion::criterion_group!(runtime, once_from_store, run_empty, run_repeated); 12 | criterion::criterion_main!(runtime); 13 | 14 | fn once_from_store(c: &mut Criterion) { 15 | let mut rt = RunLoop::new(|| once(|| Rc::new(vec![0; 1_000_000]))); 16 | rt.run_once(); 17 | c.bench_function("1mb vec cached", |b| b.iter(|| rt.run_once())); 18 | } 19 | 20 | fn run_empty(c: &mut Criterion) { 21 | let mut rt = RunLoop::new(Revision::current); 22 | c.bench_function("run_empty", |b| b.iter(|| rt.run_once())); 23 | } 24 | 25 | fn run_n_times_empty(b: &mut criterion::Bencher, n: &usize) { 26 | let mut rt = RunLoop::new(Revision::current); 27 | b.iter(|| { 28 | for _ in 0..*n { 29 | rt.run_once(); 30 | } 31 | }); 32 | } 33 | 34 | fn run_repeated(c: &mut Criterion) { 35 | let mut group = c.benchmark_group("run_repeated"); 36 | for input in &[2, 7, 23] { 37 | group.bench_with_input(BenchmarkId::from_parameter(input), input, run_n_times_empty); 38 | } 39 | group.finish(); 40 | } 41 | -------------------------------------------------------------------------------- /book/src/01-targets/99-new.md: -------------------------------------------------------------------------------- 1 | # Adding new targets 2 | 3 | Expressing an interactive system "in terms of moxie's tools" requires making some decisions. 4 | 5 | ## A core choice in moxie: Builders 6 | 7 | Existing UI systems tend to come with complex objects that require nuanced initialization, often 8 | with many parameters, some of which are optional and some which are not. Rust has one main tool 9 | for describing those initializations: the [builder pattern]. It is possible to describe complex 10 | "mixed-optionality" initialization *without* the builder pattern in Rust, but it's so prevalent 11 | in Rust that it's [officially recommended][builder pattern]. 12 | 13 | The [`mox!`][mox] macro ("**M**ockery **O**f **X**ML") is essentially an XML syntax for Rust 14 | builders. See [its documentation][mox] in the moxie crate for information about exactly how it 15 | expands. 16 | 17 | ## Finding Event Loops 18 | 19 | TODO 20 | 21 | ## Memoization 22 | 23 | TODO 24 | 25 | ## Persistence 26 | 27 | TODO 28 | 29 | ## Parent/child relationships 30 | 31 | TODO 32 | 33 | [mox]: https://docs.rs/moxie/latest/moxie/macro.mox.html 34 | [builder pattern]: https://rust-lang.github.io/api-guidelines/type-safety.html#builders-enable-construction-of-complex-values-c-builder -------------------------------------------------------------------------------- /mox/tests/dashes.rs: -------------------------------------------------------------------------------- 1 | //! These are compile time tests, but they shouldn't take any time to run, so we don't ignore them. 2 | use mox::mox; 3 | 4 | struct Tag(); 5 | 6 | impl Tag { 7 | fn single_dash(self, _value: &str) -> Self { 8 | self 9 | } 10 | 11 | fn build(self) {} 12 | } 13 | 14 | fn unused(_x: T) {} 15 | 16 | #[test] 17 | pub fn single_dash() { 18 | let custom_name = || Tag(); 19 | unused(|| mox!()); 20 | } 21 | 22 | #[test] 23 | pub fn single_dash_attr() { 24 | let tag = || Tag(); 25 | unused(|| mox!()); 26 | } 27 | 28 | #[test] 29 | pub fn multi_dash() { 30 | let custom_multi_dash_name = || Tag(); 31 | unused(|| mox!()); 32 | } 33 | 34 | #[test] 35 | pub fn trailing_dash() { 36 | let custom_trailing_dash_ = || Tag(); 37 | unused(|| mox!()); 38 | } 39 | 40 | #[test] 41 | pub fn keywords() { 42 | let custom_for_loop_in_self = || Tag(); 43 | unused(|| mox!()); 44 | } 45 | 46 | #[test] 47 | pub fn leading_keyword() { 48 | let for_loop_in_self = || Tag(); 49 | unused(|| mox!()); 50 | } 51 | -------------------------------------------------------------------------------- /ofl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ofl" 3 | version = "0.2.0-alpha.0-pre" 4 | authors = ["Adam Perry "] 5 | edition = "2018" 6 | license = "MIT/Apache-2.0" 7 | description = "outcome-oriented project task tool, as in 'all task runners are...'" 8 | repository = "https://github.com/anp/moxie/tree/main/ofl" 9 | 10 | [dependencies] 11 | actix = "0.10.0-alpha.3" 12 | actix-files = "0.3.0-alpha.1" 13 | actix-rt = "1.1.1" 14 | actix-web = "3.0.0-alpha.3" 15 | actix-web-actors = "3.0.0-alpha.1" 16 | anyhow = "1" 17 | cargo_metadata = "0.12.0" 18 | crates-index = "0.16.0" 19 | crossbeam = "0.8" 20 | dialoguer = "0.6" 21 | futures = "0.3" 22 | git2 = "0.13.15" 23 | grcov = "0.6.1" 24 | gumdrop = { version = "0.8", features = ["default_expr"] } 25 | http = "0.2" 26 | lol_html = "0.2.0" 27 | mdbook = "0.3.1" 28 | notify = "5.0.0-pre.2" 29 | opener = "0.4" 30 | pathfinding = "2.0.4" 31 | pin-utils = "0.1.0" 32 | rustc-hash = "1.1.0" 33 | semver = "0.11" 34 | serde_json = "1.0.53" 35 | spongedown = "0.4.1" 36 | tempfile = "3.1.0" 37 | toml_edit = "0.1.5" 38 | tracing = { version = "^0.1", features = [ "log" ] } 39 | tracing-subscriber = { version = "0.2.5", features = [ "fmt" ] } 40 | walkdir = "2.2.9" 41 | which = "4.0.2" 42 | 43 | [dependencies.reqwest] # force cargo to vendor native-tls (see issue #35) 44 | version = "0.10.6" 45 | features = ["blocking", "native-tls-vendored"] 46 | -------------------------------------------------------------------------------- /dom/examples/todo/README.md: -------------------------------------------------------------------------------- 1 | # TodoMVC Example 2 | 3 | Commands all assume the working directory is the repository root. 4 | 5 | ## Serving 6 | 7 | Build the example and start the project's local HTTP server: 8 | 9 | ``` 10 | $ cargo build-dom-todo # for live-watching rebuilds use `cargo dom-flow` 11 | $ cargo ofl serve 12 | ``` 13 | 14 | In VSCode the same can be accomplished by running the `dom crates` and `project server` tasks. 15 | 16 | ## Using 17 | 18 | To use the example locally, follow the directions for [serving](#serving) the project and 19 | navigate to `http://[::1]:8000/dom/examples/todo/index.html` in your browser. 20 | 21 | ## Tests 22 | 23 | Unit & integration tests can be run with `cargo test-dom-todo`. 24 | 25 | ### End-to-end 26 | 27 | End-to-end tests are run with [Cypress](https://cypress.io) which requires 28 | [Node.js](https://nodejs.org) to run. 29 | 30 | If you've already followed the [serving](#serving) instructions the e2e tests can be run from the 31 | Cypress UI directly. Start the test runner with the `cypress` VSCode task or run the following: 32 | 33 | ``` 34 | $ cd dom/examples/todo/e2e; npx cypress run 35 | ``` 36 | 37 | #### One-off 38 | 39 | The tests require a running HTTP server and a current build. The `test-dom-todo-e2e` cargo command 40 | runs a build, and starts an HTTP server for the test before running it: 41 | 42 | ``` 43 | $ cargo test-dom-todo-e2e 44 | ``` 45 | -------------------------------------------------------------------------------- /src/testing.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for testing moxie-based programs. 2 | 3 | use futures::task::ArcWake; 4 | use std::sync::{ 5 | atomic::{AtomicBool, AtomicU64, Ordering}, 6 | Arc, 7 | }; 8 | 9 | /// A value which keeps track of how many times it's been cloned. Useful for 10 | /// testing caching behaviors. 11 | #[derive(Default)] 12 | pub struct CountsClones(Arc); 13 | 14 | impl CountsClones { 15 | /// Returns the number of times this value has been cloned. 16 | pub fn clone_count(&self) -> u64 { 17 | self.0.load(Ordering::Relaxed) 18 | } 19 | } 20 | 21 | impl Clone for CountsClones { 22 | fn clone(&self) -> Self { 23 | self.0.fetch_add(1, Ordering::Relaxed); 24 | Self(self.0.clone()) 25 | } 26 | } 27 | 28 | /// A waker which keeps track of whether it's been woken or not. Useful for 29 | /// testing state update behaviors. 30 | pub struct BoolWaker(AtomicBool); 31 | 32 | impl BoolWaker { 33 | /// Returns a new instance. 34 | pub fn new() -> Arc { 35 | Arc::new(Self(AtomicBool::new(false))) 36 | } 37 | 38 | /// Returns true if woken since the last call to this method. 39 | pub fn is_woken(&self) -> bool { 40 | self.0.swap(false, Ordering::Relaxed) 41 | } 42 | } 43 | 44 | impl ArcWake for BoolWaker { 45 | fn wake_by_ref(arc_self: &Arc) { 46 | arc_self.0.store(true, Ordering::Relaxed); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ofl/src/format.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Context, Error}; 2 | use gumdrop::Options; 3 | use std::{path::Path, process::Command}; 4 | use tracing::*; 5 | 6 | use crate::workspace::Workspace; 7 | 8 | #[derive(Debug, Options)] 9 | pub struct Format { 10 | /// Pass the `--check` flag to rustfmt. 11 | check: bool, 12 | } 13 | 14 | impl Format { 15 | pub fn run(&self, project_root: impl AsRef) -> Result<(), Error> { 16 | let workspace = Workspace::get(project_root)?; 17 | 18 | let mut command = Command::new("rustfmt"); 19 | command.arg("--edition").arg("2018"); 20 | 21 | if self.check { 22 | command.arg("--check"); 23 | } 24 | 25 | for member in workspace.local_members() { 26 | let targets = &workspace.metadata[&member].targets; 27 | command.args(targets.iter().map(|t| &t.src_path)); 28 | } 29 | 30 | for ofl_member in workspace.ofl_members() { 31 | let ofl_targets = &workspace.ofl_metadata[&ofl_member].targets; 32 | command.args(ofl_targets.iter().map(|t| &t.src_path)); 33 | } 34 | 35 | debug!({ ?command }, "running rustfmt"); 36 | 37 | let status = command.status().context("running rustfmt command")?; 38 | if !status.success() { 39 | error!({ ?status }, "rustfmt failed"); 40 | bail!("rustfmt failed! {:?}", status); 41 | } 42 | 43 | Ok(()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /dom/examples/todo/src/footer.rs: -------------------------------------------------------------------------------- 1 | use crate::{filter::filter, Todo}; 2 | use mox::mox; 3 | use moxie_dom::{ 4 | elements::{forms::Button, html::*, sectioning::Footer, text_semantics::Span}, 5 | prelude::*, 6 | }; 7 | 8 | #[topo::nested] 9 | pub fn items_remaining(num_active: usize) -> Span { 10 | let bolded = if num_active == 0 { text("No") } else { text(num_active.to_string()) }; 11 | mox! { 12 | 13 | {bolded} 14 | {% " {} left", if num_active == 1 { "item" } else { "items" } } 15 | 16 | } 17 | } 18 | 19 | #[topo::nested] 20 | #[illicit::from_env(todos: &Key>)] 21 | pub fn clear_completed_button(num_complete: usize) -> Button { 22 | let todos = todos.to_owned(); 23 | let remove_completed = 24 | move |_| todos.update(|t| Some(t.iter().filter(|t| !t.completed).cloned().collect())); 25 | mox! { 26 | 31 | } 32 | } 33 | 34 | #[topo::nested] 35 | pub fn filter_footer(num_complete: usize, num_active: usize) -> Footer { 36 | let mut footer = footer().class("footer").child(items_remaining(num_active)).child(filter()); 37 | 38 | if num_complete > 0 { 39 | footer = footer.child(clear_completed_button(num_complete)); 40 | } 41 | 42 | footer.build() 43 | } 44 | -------------------------------------------------------------------------------- /mox/tests/derive_builder.rs: -------------------------------------------------------------------------------- 1 | use derive_builder::Builder; 2 | use mox::mox; 3 | use std::error::Error; 4 | 5 | #[derive(Builder, Debug, PartialEq)] 6 | #[builder(pattern = "owned", setter(into))] 7 | struct ToBuild { 8 | // TODO(colin-kiegel/rust-derive-builder#168) cleanup 9 | #[builder(setter(name = "name"))] 10 | name: String, 11 | #[builder(setter(name = "height"))] 12 | height: i32, 13 | #[builder(private, default)] 14 | children: Vec, 15 | } 16 | 17 | impl ToBuildBuilder { 18 | fn child(mut self, child: ToBuild) -> Self { 19 | let children = if let Some(c) = self.children.as_mut() { 20 | c 21 | } else { 22 | self.children = Some(Vec::new()); 23 | self.children.as_mut().unwrap() 24 | }; 25 | 26 | children.push(child); 27 | self 28 | } 29 | } 30 | 31 | fn built() -> ToBuildBuilder { 32 | ToBuildBuilder::default() 33 | } 34 | 35 | #[test] 36 | fn simple() -> Result<(), Box> { 37 | let expected = ToBuild { 38 | name: String::from("alice"), 39 | height: 3, 40 | children: vec![ToBuild { name: String::from("bob"), height: 1, children: vec![] }], 41 | }; 42 | 43 | assert_eq!( 44 | mox! { 45 | 46 | // TODO clean up with fallible tags 47 | { mox!()? } 48 | 49 | }?, 50 | expected 51 | ); 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /dom/examples/todo/src/input.rs: -------------------------------------------------------------------------------- 1 | use mox::mox; 2 | use moxie_dom::{ 3 | elements::forms::{input, Input}, 4 | prelude::*, 5 | }; 6 | use wasm_bindgen::JsCast; 7 | 8 | #[topo::nested] 9 | pub fn text_input( 10 | placeholder: &str, 11 | editing: bool, 12 | mut on_save: impl FnMut(String) + 'static, 13 | ) -> Input { 14 | let (text, set_text) = state(|| if editing { placeholder.to_string() } else { String::new() }); 15 | let clear_text = set_text.clone(); 16 | 17 | fn input_value(ev: impl AsRef) -> String { 18 | let event: &sys::Event = ev.as_ref(); 19 | let target = event.target().unwrap(); 20 | let input: sys::HtmlInputElement = target.dyn_into().unwrap(); 21 | let val = input.value(); 22 | input.set_value(""); // it's a little weird to clear the text every time, TODO clean up 23 | val 24 | } 25 | 26 | mox! { 27 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moxie" 3 | version = "0.7.1" 4 | description = "Incremental runtime for interactive software." 5 | categories = ["asynchronous", "caching", "concurrency", "gui", "rust-patterns"] 6 | keywords = ["incremental", "memoize", "intern", "reactive"] 7 | readme = "CHANGELOG.md" 8 | 9 | # update here, update everywhere! 10 | license = "MIT/Apache-2.0" 11 | homepage = "https://moxie.rs" 12 | repository = "https://github.com/anp/moxie.git" 13 | authors = ["Adam Perry "] 14 | edition = "2018" 15 | 16 | [features] 17 | default = [] 18 | wasm-bindgen = [ "dyn-cache/wasm-bindgen", "parking_lot/wasm-bindgen", "topo/wasm-bindgen" ] 19 | 20 | [dependencies] 21 | dyn-cache = { path = "dyn-cache", version = "0.12.2"} 22 | futures = "0.3.5" 23 | illicit = { path = "illicit", version = "1.1.2"} 24 | parking_lot = "0.11" 25 | scopeguard = "1" 26 | topo = { path = "topo", version = "0.13.2"} 27 | tracing = "^0.1" 28 | 29 | [dev-dependencies] 30 | criterion = "0.3" 31 | tracing-subscriber = { version = "0.3.1", features = [ "env-filter" ] } 32 | 33 | [workspace] 34 | members = [ 35 | "dom", 36 | "dom/augdom", 37 | "dom/examples/counter_fn", 38 | "dom/examples/dom_builder", 39 | "dom/examples/drivertest", 40 | "dom/examples/hacking", 41 | "dom/examples/ssr", 42 | "dom/examples/todo", 43 | "dom/prettiest", 44 | "dom/raf", 45 | "dyn-cache", 46 | "illicit", 47 | "illicit/macro", 48 | "mox", 49 | "topo", 50 | "topo/macro", 51 | ] 52 | exclude = [ 53 | "ofl", 54 | ] 55 | 56 | [[bench]] 57 | name = "core" 58 | harness = false 59 | -------------------------------------------------------------------------------- /dyn-cache/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # dyn-cache 2 | 3 | The [dyn-cache](https://docs.rs/dyn-cache) crate provides incremental caching for Rust function 4 | invocations. 5 | 6 | 7 | 8 | ## [0.12.2] - 2021-04-25 9 | 10 | ### Fixed 11 | 12 | - Cached values no longer inherit liveness from their dependents if those dependents were 13 | initialized in the current GC revision. This prevented some values from being GC'd at the correct 14 | time. See [#238](https://github.com/anp/moxie/issues/238). 15 | 16 | ## [0.12.1] - 2020-12-28 17 | 18 | ### Added 19 | 20 | - `wasm-bindgen` cargo feature which enables correct usage of parking_lot on wasm32 targets. 21 | 22 | ## [0.12.0] - 2020-08-09 23 | 24 | ### Changed 25 | 26 | - `CacheMiss` handles initialization of borrowed inputs for storage, this removes arguments from 27 | some lower-level functions. 28 | 29 | ## [0.11.0] - 2020-08-08 30 | 31 | ### Fixed 32 | 33 | - Nested queries to `SharedLocalCache`/``SharedSendCache` have their intermediate dependencies 34 | retained as long as a transitive dependent is used in a revision. 35 | 36 | ## [0.10.0] - 2020-07-19 37 | 38 | ### Added 39 | 40 | - Crate extracted from `topo::cache` module. 41 | - `{LocalCache,SendCache}::cache` wraps `cache_with` for types that impl `Clone`. 42 | - `{LocalCache,SendCache}::hold` wraps `cache_with` for queries that don't need returns. 43 | - `CacheMiss` struct is used to ensure storage happens where the failed lookup happened. 44 | 45 | ### Changed 46 | 47 | - Rename `Cache`/`SharedCache` to `SendCache`/`SharedSendCache`. 48 | -------------------------------------------------------------------------------- /dom/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moxie-dom" 3 | version = "0.3.0-pre" 4 | description = "Incrementally interactive HTML applications." 5 | categories = ["asynchronous", "concurrency", "gui", "wasm", "web-programming"] 6 | keywords = ["dom", "web", "incremental", "interactive"] 7 | readme = "CHANGELOG.md" 8 | 9 | # update here, update everywhere! 10 | license = "MIT/Apache-2.0" 11 | homepage = "https://moxie.rs" 12 | repository = "https://github.com/anp/moxie.git" 13 | authors = ["Adam Perry "] 14 | edition = "2018" 15 | 16 | [package.metadata.docs.rs] 17 | default-target = "wasm32-unknown-unknown" 18 | all-features = true 19 | 20 | [lib] 21 | crate-type = [ "cdylib", "rlib", ] 22 | 23 | [features] 24 | default = ["webdom"] 25 | rsdom = ["augdom/rsdom"] 26 | webdom = [ 27 | "augdom/webdom", 28 | "moxie/wasm-bindgen", 29 | "raf", 30 | "topo/wasm-bindgen", 31 | "wasm-bindgen", 32 | "wasm-bindgen-futures", 33 | ] 34 | 35 | [dependencies] 36 | augdom = { path = "augdom", version = "0.2.0-pre", default-features = false } 37 | futures = "0.3.5" 38 | illicit = { path = "../illicit", version = "1.1.2"} 39 | moxie = { path = "../", version = "0.7.1-pre"} 40 | paste = "1.0.0" 41 | scopeguard = "1" 42 | topo = { path = "../topo", version = "0.13.2"} 43 | 44 | # web-only 45 | raf = { path = "raf", version = "0.2.0-pre", optional = true } 46 | wasm-bindgen = { version = "0.2.68", optional = true } 47 | wasm-bindgen-futures = { version = "0.4.13", optional = true } 48 | 49 | [dev-dependencies] 50 | mox = { path = "../mox", version = "0.12.0"} 51 | pretty_assertions = "1.0" 52 | wasm-bindgen-test = "0.3" 53 | -------------------------------------------------------------------------------- /dom/prettiest/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # prettiest 2 | 3 | [prettiest](https://docs.rs/prettiest) provides pretty-printing `Debug` and `Display` impls 4 | for Javascript values in the [wasm-bindgen](https://docs.rs/wasm-bindgen) crate. 5 | 6 | 7 | 8 | ## [0.2.1] - 2020-10-18 9 | 10 | ### 11 | 12 | - Output should be (more) stable across browser versions. 13 | 14 | ### Changed 15 | 16 | - `Prettified` sorts object properties for each prototype before printing. 17 | - `Prettified` prints functions for each prototype after the properties from that prototype and 18 | before the properties of the preceding prototype. 19 | 20 | ## [0.2.0] - 2020-08-22 21 | 22 | ### Added 23 | 24 | - `Pretty` trait offers a `.pretty()` method to anything `AsRef`. 25 | - `Prettified` implements `Display`. 26 | - `Prettified::skip_property` allows deleting properties that aren't useful to print, like 27 | `timeStamp`. 28 | 29 | ### Fixed 30 | 31 | - Cycles in objects are broken correctly. 32 | - Null and undefined values are handled correctly. 33 | - Values not explicitly handled are represented by `Pretty::Unknown`. 34 | - Objects print properties from their prototype chain. 35 | 36 | ### Changed 37 | 38 | - `Pretty` enum renamed to `Prettified` to allow trait to be named `Pretty`. 39 | - Objects print non-function properties before function properties. 40 | - Objects print their properites in prototype-order. 41 | - HTML elements, window and document all have abbreviated output. 42 | 43 | ## [0.1.0] - 2020-08-20 44 | 45 | Initial release. Only sort of works -- not recommended for use. 46 | -------------------------------------------------------------------------------- /topo/macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Procedural macro support crate for the `topo` crate. 2 | 3 | use proc_macro::TokenStream; 4 | use syn::{ 5 | parse_macro_input, parse_quote, spanned::Spanned, AttributeArgs, Expr, ItemFn, Lit, Meta, 6 | NestedMeta, 7 | }; 8 | 9 | #[proc_macro_attribute] 10 | pub fn nested(args: TokenStream, input: TokenStream) -> TokenStream { 11 | let args: AttributeArgs = parse_macro_input!(args); 12 | let mut input_fn: ItemFn = syn::parse(input).unwrap(); 13 | 14 | let inner_block = input_fn.block; 15 | input_fn.block = if let Some(slot_expr) = slot_from_args(&args) { 16 | parse_quote! {{ 17 | topo::call_in_slot(#slot_expr, move || #inner_block) 18 | }} 19 | } else { 20 | parse_quote! {{ topo::call(move || #inner_block) }} 21 | }; 22 | 23 | quote::quote_spanned!(input_fn.span()=> 24 | #[track_caller] 25 | #input_fn 26 | ) 27 | .into() 28 | } 29 | 30 | /// parse the attribute arguments, retrieving an an expression to use as part of 31 | /// the slot 32 | fn slot_from_args(args: &[NestedMeta]) -> Option { 33 | assert!(args.len() <= 1); 34 | 35 | args.get(0).map(|arg| match arg { 36 | NestedMeta::Meta(Meta::NameValue(kv)) => { 37 | assert!( 38 | kv.path.is_ident("slot"), 39 | "only `slot = \"...\" argument is supported by #[nested]" 40 | ); 41 | 42 | match &kv.lit { 43 | Lit::Str(l) => l.parse().unwrap(), 44 | _ => panic!("`slot` argument accepts a string literal"), 45 | } 46 | } 47 | _ => panic!("only `slot = \"...\" argument is supported by #[nested]"), 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /dom/examples/todo/src/filter.rs: -------------------------------------------------------------------------------- 1 | use crate::Todo; 2 | use mox::mox; 3 | use moxie_dom::{ 4 | elements::{ 5 | html::*, 6 | text_content::{Li, Ul}, 7 | }, 8 | prelude::*, 9 | }; 10 | use Visibility::{Active, All, Completed}; 11 | 12 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 13 | pub enum Visibility { 14 | All, 15 | Active, 16 | Completed, 17 | } 18 | 19 | impl Default for Visibility { 20 | fn default() -> Self { 21 | All 22 | } 23 | } 24 | 25 | impl std::fmt::Display for Visibility { 26 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 27 | f.write_str(match self { 28 | All => "All", 29 | Active => "Active", 30 | Completed => "Completed", 31 | }) 32 | } 33 | } 34 | 35 | impl Visibility { 36 | pub fn should_show(self, todo: &Todo) -> bool { 37 | match self { 38 | All => true, 39 | Active => !todo.completed, 40 | Completed => todo.completed, 41 | } 42 | } 43 | } 44 | 45 | #[topo::nested] 46 | #[illicit::from_env(visibility: &Key)] 47 | pub fn filter_link(to_set: Visibility) -> Li { 48 | let visibility = visibility.clone(); 49 | mox! { 50 |
  • 51 | 54 | { to_set } 55 | 56 |
  • 57 | } 58 | } 59 | 60 | #[topo::nested] 61 | pub fn filter() -> Ul { 62 | let mut list = ul(); 63 | list = list.class("filters"); 64 | for &to_set in &[All, Active, Completed] { 65 | list = list.child(filter_link(to_set)); 66 | } 67 | 68 | list.build() 69 | } 70 | -------------------------------------------------------------------------------- /src/runtime/var.rs: -------------------------------------------------------------------------------- 1 | use crate::{Commit, Key}; 2 | use parking_lot::Mutex; 3 | use std::{sync::Arc, task::Waker}; 4 | 5 | /// The underlying container of state variables. Vends copies of the latest 6 | /// [`Commit`] for [`Key`]s. 7 | pub(crate) struct Var { 8 | current: Commit, 9 | id: topo::CallId, 10 | pending: Option>, 11 | waker: Waker, 12 | } 13 | 14 | impl Var { 15 | pub fn new(id: topo::CallId, waker: Waker, inner: State) -> Arc> { 16 | let current = Commit { id, inner: Arc::new(inner) }; 17 | Arc::new(Mutex::new(Var { id, current, waker, pending: None })) 18 | } 19 | 20 | /// Attach this `Var` to its callsite, performing any pending commit and 21 | /// returning the resulting latest commit. 22 | pub fn root(var: Arc>) -> (Commit, Key) { 23 | let (id, commit_at_root) = { 24 | let mut var = var.lock(); 25 | if let Some(pending) = var.pending.take() { 26 | var.current = pending; 27 | } 28 | (var.id, var.current.clone()) 29 | }; 30 | 31 | (commit_at_root.clone(), Key { id, commit_at_root, var }) 32 | } 33 | 34 | /// Returns a reference to the latest value, pending or committed. 35 | pub fn latest(&self) -> &State { 36 | self.pending.as_ref().unwrap_or(&self.current) 37 | } 38 | 39 | /// Initiate a commit to the state variable. The commit will actually 40 | /// complete asynchronously when the state variable is next rooted in a 41 | /// topological function, flushing the pending commit. 42 | pub fn enqueue_commit(&mut self, state: State) { 43 | self.pending = Some(Commit { inner: Arc::new(state), id: self.id }); 44 | self.waker.wake_by_ref(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /dom/src/interfaces/event_target.rs: -------------------------------------------------------------------------------- 1 | //! EventTarget is a DOM interface implemented by objects that can receive 2 | //! events and may have listeners for them. 3 | 4 | use crate::interfaces::node::NodeWrapper; 5 | use augdom::event::{Event, EventHandle}; 6 | use moxie::cache_with; 7 | 8 | /// EventTarget is a DOM interface implemented by objects that can receive 9 | /// events and may have listeners for them. 10 | /// 11 | /// Element, Document, and Window are the most common event targets, but other 12 | /// objects can be event targets, too. For example XMLHttpRequest, AudioNode, 13 | /// AudioContext, and others. 14 | /// 15 | /// Many event targets (including elements, documents, and windows) also support 16 | /// setting event handlers via onevent properties and attributes. 17 | /// 18 | /// Note: this trait cannot be implemented outside of this crate. 19 | pub trait EventTarget: NodeWrapper 20 | where 21 | Ev: 'static + Event, 22 | { 23 | /// Declare an event handler on the element. 24 | /// 25 | /// A guard value is stored as a resulting "effect" of the mutation, and 26 | /// removes the attribute when `drop`ped, to ensure that the attribute 27 | /// is removed when this declaration is no longer referenced in the most 28 | /// recent (`moxie::Revision`). 29 | /// 30 | /// Currently this is performed on every Revision, as changes to event 31 | /// handlers don't typically affect the debugging experience and have 32 | /// not yet shown up in performance profiles. 33 | #[topo::nested] 34 | fn on(self, callback: impl FnMut(Ev) + 'static) -> Self { 35 | cache_with( 36 | &moxie::runtime::Revision::current(), 37 | |_| EventHandle::new(self.raw_node_that_has_sharp_edges_please_be_careful(), callback), 38 | |_| {}, 39 | ); 40 | self 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /mox/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # mox 2 | 3 | [mox] implements the `mox!` macro: "a Mockery Of X(ML)". A [JSX]-like Rust DSL for calling builders. 4 | 5 | [mox]: https://docs.rs/moxie 6 | [JSX]: https://reactjs.org/docs/introducing-jsx.html 7 | 8 | 9 | 10 | ## [0.12.0] - 2021-02-01 11 | 12 | ### Changed 13 | 14 | - `mox-impl` is no longer being published and its contents are included directly in this crate. 15 | 16 | ### Removed 17 | 18 | - Dependency on `topo`. 19 | 20 | ## [0.11.0] - 2021-01-10 21 | 22 | ### Added 23 | 24 | - "Attribute init shorthand" allows pulling an attribute from an identically-named binding in the 25 | local scope: 26 | 27 | ```rust 28 | let onclick = |_| { ... }; 29 | mox!() 30 | ``` 31 | 32 | - Module-nested tag names: `mox!("foo")`. 33 | - Attributes support single-expression values without braces: ` 33 | }); 34 | 35 | for t in &["first", "second", "third"] { 36 | root = root.child(mox! {
    { t }
    }); 37 | } 38 | 39 | root.build() 40 | } 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use super::*; 45 | use augdom::testing::{Query, TargetExt}; 46 | use wasm_bindgen_test::*; 47 | wasm_bindgen_test_configure!(run_in_browser); 48 | 49 | #[wasm_bindgen_test] 50 | pub async fn hello_browser() { 51 | let test_root = document().create_element("div"); 52 | moxie_dom::boot(test_root.clone(), root); 53 | 54 | let button = test_root.find().by_text("increment").until().one().await.unwrap(); 55 | assert_eq!( 56 | test_root.first_child().unwrap().to_string(), 57 | r#"
    58 |
    hello world from moxie! (0)
    59 | 60 |
    first
    61 |
    second
    62 |
    third
    63 |
    "# 64 | ); 65 | 66 | button.click(); 67 | test_root.find().by_text("hello world from moxie! (1)").until().one().await.unwrap(); 68 | button.click(); 69 | test_root.find().by_text("hello world from moxie! (2)").until().one().await.unwrap(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /ofl/src/server/run.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Error}; 2 | use gumdrop::Options; 3 | use std::{ 4 | path::PathBuf, 5 | process::Command, 6 | thread::{sleep, spawn}, 7 | time::Duration, 8 | }; 9 | use tracing::*; 10 | use which::which; 11 | 12 | #[derive(Debug, Options)] 13 | pub struct RunOpts { 14 | /// the port to use for the ephemeral http server 15 | #[options(default = "8000")] 16 | port: u16, 17 | /// run once the http server is up 18 | #[options(free)] 19 | cmd: String, 20 | /// working directory for the command 21 | cwd: PathBuf, 22 | /// cargo command to run before launching tests 23 | cargo_before: Option, 24 | /// args to pass the command 25 | #[options(free)] 26 | args: Vec, 27 | } 28 | 29 | impl RunOpts { 30 | pub fn run(self, root_path: PathBuf) -> Result<(), Error> { 31 | let mut server = super::ServerOpts { port: self.port, ..Default::default() }; 32 | server.watch_changes = false; 33 | 34 | let _server = spawn(move || { 35 | if let Err(error) = server.run_server(root_path) { 36 | error!(?error, "server failed, exiting"); 37 | std::process::abort(); 38 | } 39 | }); 40 | 41 | if let Some(cargo_cmd) = self.cargo_before { 42 | let status = Command::new("cargo").arg(&cargo_cmd).status()?; 43 | if !status.success() { 44 | bail!("`cargo {}` failed with status {:?}", cargo_cmd, status); 45 | } 46 | } 47 | 48 | info!("checking server..."); 49 | let url = format!("http://[::1]:{}", self.port); 50 | while reqwest::blocking::get(&url).is_err() { 51 | info!("server not yet ready, trying again..."); 52 | sleep(Duration::from_secs(1)); 53 | } 54 | 55 | let mut command = Command::new(which(self.cmd)?); 56 | command.args(self.args).current_dir(self.cwd); 57 | 58 | info!(?command, "running"); 59 | let status = command.status()?; 60 | info!(%status, "finished"); 61 | 62 | if !status.success() { 63 | bail!("command failed"); 64 | } 65 | 66 | Ok(()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/issue_238.rs: -------------------------------------------------------------------------------- 1 | use moxie::{ 2 | cache, once, 3 | runtime::{Revision, RunLoop}, 4 | }; 5 | use std::sync::{ 6 | atomic::{AtomicU32, Ordering}, 7 | Arc, 8 | }; 9 | 10 | struct CountDrops { 11 | num_drops: Arc, 12 | } 13 | 14 | impl Drop for CountDrops { 15 | fn drop(&mut self) { 16 | self.num_drops.fetch_add(1, Ordering::SeqCst); 17 | } 18 | } 19 | 20 | #[test] 21 | fn issue_238_cache_call_incorrectly_preserves_once_guard() { 22 | // counts the number of times the once() call below is made 23 | let once_calls = Arc::new(AtomicU32::new(0)); 24 | 25 | // counts the number of times the value returned from once() below is GC'd 26 | let once_drops = Arc::new(AtomicU32::new(0)); 27 | 28 | let (adder, drop_adder) = (once_calls.clone(), once_drops.clone()); 29 | let mut rt = RunLoop::new(move || { 30 | println!("\n\nrunning loop again"); 31 | let (commit, key) = moxie::state(|| false); 32 | 33 | // caching with the current revision should be a no-op 34 | println!("calling cache"); 35 | cache(&Revision::current(), |_| { 36 | println!("entered cache"); 37 | if *commit { 38 | // this call should be GC'd if commit is false 39 | println!("calling once"); 40 | once(|| { 41 | println!("entered once"); 42 | adder.fetch_add(1, Ordering::SeqCst); 43 | Arc::new(CountDrops { num_drops: drop_adder.clone() }) 44 | }); 45 | } 46 | }); 47 | 48 | key 49 | }); 50 | 51 | // first execution, key is `false` here, no once() call 52 | let key = rt.run_once(); 53 | assert_eq!(once_calls.load(Ordering::SeqCst), 0); 54 | assert_eq!(once_drops.load(Ordering::SeqCst), 0); 55 | 56 | // set key to true, once() should execute but not drop 57 | key.set(true); 58 | rt.run_once(); 59 | assert_eq!(once_calls.load(Ordering::SeqCst), 1); 60 | assert_eq!(once_drops.load(Ordering::SeqCst), 0); 61 | 62 | // set key to false, once() should not execute and should get GC'd 63 | key.set(false); 64 | rt.run_once(); 65 | assert_eq!(once_calls.load(Ordering::SeqCst), 1); 66 | assert_eq!(once_drops.load(Ordering::SeqCst), 1); 67 | } 68 | -------------------------------------------------------------------------------- /dyn-cache/src/cache_cell.rs: -------------------------------------------------------------------------------- 1 | use super::dep_node::{DepNode, Dependent}; 2 | use std::{ 3 | any::type_name, 4 | borrow::Borrow, 5 | fmt::{Debug, Formatter, Result as FmtResult}, 6 | }; 7 | 8 | /// A CacheCell represents the storage used for a particular input/output pair 9 | /// on the heap. 10 | #[derive(Clone, Default, Hash, Eq, PartialEq)] 11 | pub(crate) struct CacheCell { 12 | dep: DepNode, 13 | input: Input, 14 | output: Output, 15 | } 16 | 17 | impl CacheCell { 18 | pub fn new(input: Input, output: Output, dep: DepNode) -> Self { 19 | Self { dep, input, output } 20 | } 21 | 22 | /// Return a reference to the output if the input is equal, marking it live 23 | /// in the process. If get fails, returns its own `Dependent` to be used as 24 | /// a dependency of any queries which are invoked to re-initialize this 25 | /// cell. 26 | pub fn get(&self, input: &Arg, dependent: Dependent) -> Result<&Output, Dependent> 27 | where 28 | Arg: PartialEq + ?Sized, 29 | Input: Borrow, 30 | { 31 | self.dep.root_read(dependent); 32 | if input == &self.input { 33 | Ok(&self.output) 34 | } else { 35 | Err(self.dep.as_dependent()) 36 | } 37 | } 38 | 39 | /// Store a new input/output and mark the storage live. 40 | pub fn store(&mut self, input: Input, output: Output, dependent: Dependent, revision: u64) { 41 | self.dep.root_write(dependent, revision); 42 | self.input = input; 43 | self.output = output; 44 | } 45 | 46 | pub fn is_live(&self) -> bool { 47 | self.dep.is_known_live() 48 | } 49 | 50 | pub fn update_liveness(&mut self, current_revision: u64) { 51 | self.dep.update_liveness(current_revision); 52 | } 53 | 54 | pub fn mark_dead(&mut self) { 55 | self.dep.mark_dead(); 56 | } 57 | } 58 | 59 | impl Debug for CacheCell 60 | where 61 | Input: 'static, 62 | Output: 'static, 63 | { 64 | // someday specialization might save us from these lame debug impls? 65 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 66 | f.debug_map() 67 | .entry(&"input", &type_name::()) 68 | .entry(&"output", &type_name::()) 69 | .finish() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /dom/src/elements/interactive.rs: -------------------------------------------------------------------------------- 1 | //! HTML offers a selection of elements which help to create interactive user 2 | //! interface objects. 3 | 4 | html_element! { 5 | /// The [HTML Details Element (`
    `)][mdn] creates a disclosure widget in which 6 | /// information is visible only when the widget is toggled into an "open" state. 7 | /// 8 | /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details 9 |
    10 | 11 | categories { 12 | Flow, Sectioning, Interactive, Palpable 13 | } 14 | 15 | children { 16 | tags { 17 | 18 | } 19 | categories { 20 | Flow 21 | } 22 | } 23 | 24 | attributes { 25 | /// Indicates whether the details will be shown on page load. 26 | open(bool) 27 | } 28 | } 29 | 30 | html_element! { 31 | /// The [HTML `` element][mdn] represents a dialog box or other interactive component, 32 | /// such as an inspector or window. 33 | /// 34 | /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog 35 | 36 | 37 | categories { 38 | Flow, Sectioning 39 | } 40 | 41 | children { 42 | categories { 43 | Flow 44 | } 45 | } 46 | 47 | attributes { 48 | /// Indicates that the dialog is active and can be interacted with. When the open attribute 49 | /// is not set, the dialog shouldn't be shown to the user. 50 | open(bool) 51 | } 52 | } 53 | 54 | html_element! { 55 | /// The [HTML `` element][mdn] represents a group of commands that a user can perform or 56 | /// activate. This includes both list menus, which might appear across the top of a screen, as 57 | /// well as context menus, such as those that might appear underneath a button after it has been 58 | /// clicked. 59 | /// 60 | /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/menu 61 | 62 | 63 | categories { 64 | Flow, 65 | Palpable // if the element's children include at least one
  • element 66 | } 67 | 68 | children { 69 | categories { 70 | Flow 71 | } 72 | } 73 | } 74 | 75 | html_element! { 76 | /// The [HTML Disclosure Summary element (``)][mdn] element specifies a summary, 77 | /// caption, or legend for a [`
    `][details] element's disclosure box. 78 | /// 79 | /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary 80 | /// [details]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details 81 | 82 | 83 | children { 84 | tags { 85 |

    ,

    ,

    ,

    ,

    ,
    ,
    86 | } 87 | categories { 88 | Phrasing 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # moxie 2 | 3 | moxie supports incremental "declarative" Rust code for interactive systems. 4 | It comes with a lightweight event loop runtime that supports granular 5 | reuse of arbitrary work, state change notifications, and async loaders. 6 | 7 | 8 | 9 | ## [0.7.1] - 2021-05-05 10 | 11 | ### Added 12 | 13 | - `Key::mutate` allows naive clone-update-compare access to a state variable. 14 | - `#[moxie::updater(...)]` attribute macro supports creating a `Key` wrapper with shorthand for 15 | mutating methods. 16 | - `wasm-bindgen` cargo feature which enables correct usage of parking_lot on wasm32 targets. 17 | 18 | ### Fixed 19 | 20 | - Some new clippy lints. 21 | 22 | ### Changed 23 | 24 | - No longer requires a nightly cargo to build. 25 | 26 | ## [0.7.0] - 2020-09-27 27 | 28 | ### Added 29 | 30 | - Support borrowed arguments to cache functions to avoid cloning on every revision. 31 | - Common trait implementations for `Key`. 32 | - Updated crate & module docs. 33 | - Testing utilities in `testing` module. 34 | 35 | ### Removed 36 | 37 | - Futures loading is no longer feature flagged. 38 | - moxie's cache (previously MemoStorage) is moved to dyn-cache, a new dependency. 39 | - Built-in executor which was only used for testing. 40 | - No longer depends on nightly Rust for `#[track_caller]` -- it's stabilized. 41 | - `mox!` macro is now published separately. 42 | 43 | ### Changed 44 | 45 | - "Memoization" renamed to "caching" in all APIs. 46 | - `Runtime::run_once` allows passing an argument to the root function. 47 | - `Runtime` no longer owns the root function. 48 | - `embed` module renamed to `runtime`. 49 | - State functions return a tuple `(Commit, Key)` instead of just a `Key`. 50 | 51 | ## [0.2.3] - 2019-12-27 52 | 53 | ### Fixed 54 | 55 | - Incorrect version numbers which prevented 0.2.2 from working from crates.io. 56 | 57 | ## [0.2.2] - 2019-12-27 58 | 59 | ### Added 60 | 61 | - Depends on nightly Rust for `#[track_caller]` feature. 62 | 63 | ### Changed 64 | 65 | - Update to topo version that produces functions instead of macros from `#[topo::nested]`. No more 66 | macros! Makes use of `#[track_caller]`. 67 | 68 | ## [0.2.1] - 2019-11-22 69 | 70 | ### Added 71 | 72 | - Async executor integration w/ futures loading (`load`, `load_once`, ...). Under feature flag. 73 | - `#![forbid(unsafe_code)]` 74 | - `Runtime::run_once` returns the root closure's return value. 75 | - `memo_with`, `once_with` functions that allows non-`Clone` types in storage 76 | - `Key` tracks its callsite. 77 | - `mox!` re-exported. 78 | 79 | ### Removed 80 | 81 | - Attempts at memoizing all components. 82 | - Unnecessary revision bookkeeping for state variables. 83 | 84 | ### Fixed 85 | 86 | - Passing illicit env values to a root function. 87 | 88 | ## [0.1.1-alpha.0] - 2019-08-17 89 | 90 | Initial release in support of moxie-dom. 91 | 92 | ## [0.1.0] - 2018-11-10 93 | 94 | Initial name reservation. 95 | -------------------------------------------------------------------------------- /dom/tests/custom_component.rs: -------------------------------------------------------------------------------- 1 | use augdom::testing::{Query, TargetExt}; 2 | use mox::mox; 3 | use moxie_dom::{ 4 | elements::html::{button, div}, 5 | interfaces::node::NodeBuilder, 6 | prelude::*, 7 | }; 8 | use wasm_bindgen_test::*; 9 | 10 | struct Counter { 11 | button: moxie_dom::elements::forms::Button, 12 | } 13 | 14 | impl moxie_dom::interfaces::content_categories::FlowContent for Counter {} 15 | 16 | impl moxie_dom::interfaces::node::Child for Counter { 17 | fn to_bind(&self) -> &augdom::Node { 18 | self.button.to_bind() 19 | } 20 | } 21 | 22 | fn custom_counter() -> CounterBuilder { 23 | CounterBuilder::default() 24 | } 25 | 26 | #[derive(Default)] 27 | struct CounterBuilder { 28 | default_value: Option, 29 | text: Option, 30 | } 31 | 32 | impl CounterBuilder { 33 | pub fn value(mut self, value: i32) -> Self { 34 | self.default_value = Some(value); 35 | self 36 | } 37 | 38 | pub fn button_text(mut self, text: impl ToString) -> Self { 39 | self.text = Some(text.to_string()); 40 | self 41 | } 42 | } 43 | 44 | impl NodeBuilder for CounterBuilder { 45 | type Output = Counter; 46 | 47 | #[topo::nested] 48 | fn build(self) -> Counter { 49 | let Self { text, default_value } = self; 50 | let (value, set_value) = state(|| default_value.unwrap_or(0)); 51 | let text = text.unwrap_or_default(); 52 | 53 | let button = mox! { 54 | 57 | }; 58 | 59 | Counter { button } 60 | } 61 | } 62 | 63 | wasm_bindgen_test_configure!(run_in_browser); 64 | 65 | #[wasm_bindgen_test] 66 | pub async fn binds_to_div() { 67 | let render_counter_as_child = || mox!(
    ); 68 | let test_root = document().create_element("div"); 69 | moxie_dom::boot(test_root.clone(), render_counter_as_child); 70 | 71 | assert_eq!( 72 | test_root.first_child().unwrap().to_string(), 73 | "
    74 | 75 |
    ", 76 | ); 77 | } 78 | 79 | #[wasm_bindgen_test] 80 | pub async fn renders_and_interacts() { 81 | let render_counter = || mox!(); 82 | let test_root = document().create_element("div"); 83 | moxie_dom::boot(test_root.clone(), render_counter); 84 | 85 | assert_eq!(test_root.first_child().unwrap().to_string(), "",); 86 | let button = test_root.find().by_text("foo (0)").until().one().await.unwrap(); 87 | 88 | button.click(); 89 | test_root.find().by_text("foo (1)").until().one().await.unwrap(); 90 | assert_eq!(test_root.first_child().unwrap().to_string(), "",); 91 | 92 | button.click(); 93 | test_root.find().by_text("foo (2)").until().one().await.unwrap(); 94 | assert_eq!(test_root.first_child().unwrap().to_string(), "",); 95 | } 96 | -------------------------------------------------------------------------------- /ofl/src/server/session.rs: -------------------------------------------------------------------------------- 1 | use actix::prelude::*; 2 | use actix_web_actors::ws; 3 | use crossbeam::channel::Sender; 4 | use std::time::{Duration, Instant}; 5 | use tracing::*; 6 | 7 | const TIMEOUT: Duration = Duration::from_secs(10); 8 | const FREQUENCY: Duration = Duration::from_secs(1); 9 | 10 | pub struct Changed(pub String); 11 | 12 | impl Message for Changed { 13 | type Result = (); 14 | } 15 | 16 | pub struct ChangeWatchSession { 17 | last_heartbeat: Instant, 18 | last_change: Option, 19 | session_tx: Sender>, 20 | } 21 | 22 | impl ChangeWatchSession { 23 | pub fn new(session_tx: Sender>) -> Self { 24 | ChangeWatchSession { last_heartbeat: Instant::now(), last_change: None, session_tx } 25 | } 26 | 27 | fn tick_heartbeat(&mut self) { 28 | self.last_heartbeat = Instant::now(); 29 | } 30 | } 31 | 32 | impl Handler for ChangeWatchSession { 33 | type Result = (); 34 | 35 | fn handle(&mut self, changed: Changed, _cx: &mut ::Context) -> Self::Result { 36 | self.last_change = Some(changed); 37 | } 38 | } 39 | 40 | impl StreamHandler> for ChangeWatchSession { 41 | fn handle(&mut self, msg: Result, cx: &mut Self::Context) { 42 | match msg { 43 | Ok(ws::Message::Ping(msg)) => { 44 | self.tick_heartbeat(); 45 | cx.pong(&msg); 46 | } 47 | Ok(ws::Message::Pong(_)) => { 48 | self.tick_heartbeat(); 49 | } 50 | Ok(ws::Message::Close(_)) => { 51 | cx.stop(); 52 | } 53 | Ok(ws::Message::Nop) => (), 54 | Ok(ws::Message::Text(text)) => { 55 | self.tick_heartbeat(); 56 | debug!("ignoring text ws message {:?}", text); 57 | } 58 | Ok(ws::Message::Binary(_bin)) => { 59 | self.tick_heartbeat(); 60 | debug!("ignoring binary ws message"); 61 | } 62 | Ok(ws::Message::Continuation(_)) => self.tick_heartbeat(), 63 | Err(e) => warn!({ %e }, "websocket protocol error"), 64 | } 65 | } 66 | } 67 | 68 | impl Actor for ChangeWatchSession { 69 | type Context = ws::WebsocketContext; 70 | 71 | fn started(&mut self, cx: &mut Self::Context) { 72 | self.session_tx.send(cx.address()).unwrap(); 73 | cx.run_interval(FREQUENCY, |session, cx| { 74 | if Instant::now().duration_since(session.last_heartbeat) > TIMEOUT { 75 | info!("ws change event client timed out, disconnecting"); 76 | cx.stop(); 77 | return; 78 | } 79 | 80 | if let Some(Changed(change)) = session.last_change.take() { 81 | info!(%change, "notifying client"); 82 | cx.text(change); 83 | } else { 84 | cx.ping(b""); 85 | } 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /topo/benches/simple_calls.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate criterion; 3 | 4 | use criterion::{BenchmarkId, Criterion}; 5 | 6 | fn get_id(c: &mut Criterion) { 7 | c.bench_function("id from env", |b| { 8 | topo::call(|| b.iter(|| topo::CallId::current())); 9 | }); 10 | } 11 | 12 | fn call_and_get_id(c: &mut Criterion) { 13 | c.bench_function("call and get id", |b| b.iter(|| topo::call(|| topo::CallId::current()))); 14 | } 15 | 16 | fn call(c: &mut Criterion) { 17 | c.bench_function("just a call", |b| b.iter(|| topo::call(|| ()))); 18 | } 19 | 20 | fn call_topo_fns_to_depth(b: &mut criterion::Bencher, depth: &usize) { 21 | macro_rules! mk { 22 | (go $depth_spec:ident) => { 23 | b.iter(|| topo::call(|| mk!(pass $depth_spec 0))) 24 | }; 25 | (pass $depth_spec:ident $call_depth:expr) => { 26 | topo::call(|| { 27 | mk!(cur $depth_spec ($call_depth + 1)); 28 | }) 29 | }; 30 | (cur twelve $depth:expr) => { 31 | mk!(pass eleven $depth); 32 | }; 33 | (cur eleven $depth:expr) => { 34 | mk!(pass ten $depth); 35 | }; 36 | (cur ten $depth:expr) => { 37 | mk!(pass nine $depth); 38 | }; 39 | (cur nine $depth:expr) => { 40 | mk!(pass eight $depth); 41 | }; 42 | (cur eight $depth:expr) => { 43 | mk!(pass seven $depth); 44 | }; 45 | (cur seven $depth:expr) => { 46 | mk!(pass six $depth); 47 | }; 48 | (cur six $depth:expr) => { 49 | mk!(pass five $depth); 50 | }; 51 | (cur five $depth:expr) => { 52 | mk!(pass four $depth); 53 | }; 54 | (cur four $depth:expr) => { 55 | mk!(pass three $depth); 56 | }; 57 | (cur three $depth:expr) => { 58 | mk!(pass two $depth); 59 | }; 60 | (cur two $depth:expr) => { 61 | mk!(pass one $depth); 62 | }; 63 | (cur one $depth:expr) => { 64 | mk!(pass zero ($depth + 1)); // lol what a cheater! 65 | }; 66 | (cur zero $depth:expr) => { 67 | assert_eq!($depth - 2, *depth); 68 | }; 69 | } 70 | match depth { 71 | 12 => mk!(go twelve), 72 | 11 => mk!(go eleven), 73 | 10 => mk!(go ten), 74 | 9 => mk!(go nine), 75 | 8 => mk!(go eight), 76 | 7 => mk!(go seven), 77 | 6 => mk!(go six), 78 | 5 => mk!(go five), 79 | 4 => mk!(go four), 80 | 3 => mk!(go three), 81 | 2 => mk!(go two), 82 | 1 => mk!(go one), 83 | _ => unimplemented!(), 84 | } 85 | } 86 | 87 | fn call_depths(c: &mut Criterion) { 88 | let mut group = c.benchmark_group("call_depths"); 89 | for input in &[1, 3, 9, 12] { 90 | group.bench_with_input(BenchmarkId::from_parameter(input), input, call_topo_fns_to_depth); 91 | } 92 | group.finish(); 93 | } 94 | 95 | criterion::criterion_group!(benches, get_id, call_and_get_id, call, call_depths,); 96 | criterion::criterion_main!(benches); 97 | -------------------------------------------------------------------------------- /src/runtime/runloop.rs: -------------------------------------------------------------------------------- 1 | use super::{Revision, Runtime}; 2 | use futures::{ 3 | stream::{Stream, StreamExt}, 4 | task::LocalSpawn, 5 | }; 6 | use std::{ 7 | pin::Pin, 8 | task::{Context as FutContext, Poll, Waker}, 9 | }; 10 | 11 | /// A [`Runtime`] that is bound with a particular root function. 12 | /// 13 | /// If running in a context with an async executor, can be consumed as a 14 | /// [`futures::Stream`] of [`crate::runtime::Revision`]s in order to provide 15 | /// the [`super::Runtime`] with a [`std::task::Waker`]. 16 | pub struct RunLoop { 17 | inner: Runtime, 18 | root: Root, 19 | } 20 | 21 | impl super::Runtime { 22 | /// Returns this runtime bound with a specific root function it will run in 23 | /// a loop. 24 | pub fn looped(self, root: Root) -> RunLoop 25 | where 26 | Root: FnMut() -> Out, 27 | { 28 | RunLoop { inner: self, root } 29 | } 30 | } 31 | 32 | impl RunLoop 33 | where 34 | Root: FnMut() -> Out + Unpin, 35 | { 36 | /// Creates a new `Runtime` attached to the provided root function. 37 | pub fn new(root: Root) -> RunLoop { 38 | RunLoop { root, inner: Runtime::new() } 39 | } 40 | 41 | /// Returns the runtime's current Revision. 42 | pub fn revision(&self) -> Revision { 43 | self.inner.revision() 44 | } 45 | 46 | /// Sets the [`std::task::Waker`] which will be called when state variables 47 | /// change. 48 | pub fn set_state_change_waker(&mut self, wk: Waker) { 49 | self.inner.set_state_change_waker(wk); 50 | } 51 | 52 | /// Sets the executor that will be used to spawn normal priority tasks. 53 | pub fn set_task_executor(&mut self, sp: impl LocalSpawn + 'static) { 54 | self.inner.set_task_executor(sp); 55 | } 56 | 57 | /// Run the root function once within this runtime's context, returning the 58 | /// result. 59 | pub fn run_once(&mut self) -> Out { 60 | self.inner.run_once(&mut self.root) 61 | } 62 | 63 | /// Poll this runtime without exiting. Discards any value returned from the 64 | /// root function. The future yields in between revisions and is woken on 65 | /// state changes. 66 | pub async fn run_on_state_changes(mut self) { 67 | loop { 68 | self.next().await; 69 | } 70 | } 71 | 72 | /// Unbinds the runtime from its current root function, returning both. 73 | pub fn unloop(self) -> (Runtime, Root) { 74 | (self.inner, self.root) 75 | } 76 | } 77 | 78 | impl Stream for RunLoop 79 | where 80 | Root: FnMut() -> Out + Unpin, 81 | { 82 | type Item = (Revision, Out); 83 | 84 | /// This `Stream` implementation runs a single revision for each call to 85 | /// `poll_next`, always returning `Poll::Ready(Some(...))`. 86 | fn poll_next(self: Pin<&mut Self>, cx: &mut FutContext<'_>) -> Poll> { 87 | let this = self.get_mut(); 88 | this.inner.set_state_change_waker(cx.waker().clone()); 89 | let out = this.run_once(); 90 | Poll::Ready(Some((this.inner.revision, out))) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /dom/examples/todo/e2e/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create the custom commands: 'createDefaultTodos' 4 | // and 'createTodo'. 5 | // 6 | // The commands.js file is a great place to 7 | // modify existing commands and create custom 8 | // commands for use throughout your tests. 9 | // 10 | // You can read more about custom commands here: 11 | // https://on.cypress.io/commands 12 | // *********************************************** 13 | 14 | Cypress.Commands.add('createDefaultTodos', function () { 15 | 16 | let TODO_ITEM_ONE = 'buy some cheese' 17 | let TODO_ITEM_TWO = 'feed the cat' 18 | let TODO_ITEM_THREE = 'book a doctors appointment' 19 | 20 | // begin the command here, which by will display 21 | // as a 'spinning blue state' in the UI to indicate 22 | // the command is running 23 | let cmd = Cypress.log({ 24 | name: 'create default todos', 25 | message: [], 26 | consoleProps () { 27 | // we're creating our own custom message here 28 | // which will print out to our browsers console 29 | // whenever we click on this command 30 | return { 31 | 'Inserted Todos': [TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE], 32 | } 33 | }, 34 | }) 35 | 36 | // additionally we pass {log: false} to all of our 37 | // sub-commands so none of them will output to 38 | // our command log 39 | 40 | cy.get('.new-todo', { log: false }) 41 | .type(`${TODO_ITEM_ONE}{enter}`, { log: false }) 42 | .type(`${TODO_ITEM_TWO}{enter}`, { log: false }) 43 | .type(`${TODO_ITEM_THREE}{enter}`, { log: false }) 44 | 45 | cy.get('.todo-list li', { log: false }) 46 | .then(function ($listItems) { 47 | // once we're done inserting each of the todos 48 | // above we want to return the .todo-list li's 49 | // to allow for further chaining and then 50 | // we want to snapshot the state of the DOM 51 | // and end the command so it goes from that 52 | // 'spinning blue state' to the 'finished state' 53 | cmd.set({ $el: $listItems }).snapshot().end() 54 | }) 55 | }) 56 | 57 | Cypress.Commands.add('createTodo', function (todo) { 58 | 59 | let cmd = Cypress.log({ 60 | name: 'create todo', 61 | message: todo, 62 | consoleProps () { 63 | return { 64 | 'Inserted Todo': todo, 65 | } 66 | }, 67 | }) 68 | 69 | // create the todo 70 | cy.get('.new-todo', { log: false }).type(`${todo}{enter}`, { log: false }) 71 | 72 | // now go find the actual todo 73 | // in the todo list so we can 74 | // easily alias this in our tests 75 | // and set the $el so its highlighted 76 | cy.get('.todo-list', { log: false }) 77 | .contains('li', todo.trim(), { log: false }) 78 | .then(function ($li) { 79 | // set the $el for the command so 80 | // it highlights when we hover over 81 | // our command 82 | cmd.set({ $el: $li }).snapshot().end() 83 | }) 84 | }) 85 | 86 | Cypress.Commands.add('addAxeCode', () => { 87 | cy.window({ log: false }).then((win) => { 88 | return new Promise((resolve) => { 89 | const script = win.document.createElement('script') 90 | 91 | script.src = '/node_modules/axe-core/axe.min.js' 92 | script.addEventListener('load', resolve) 93 | 94 | win.document.head.appendChild(script) 95 | }) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /dom/src/interfaces/node.rs: -------------------------------------------------------------------------------- 1 | //! Traits for nodes in the DOM tree. 2 | 3 | use std::fmt::Display; 4 | 5 | use crate::text::{text, Text}; 6 | 7 | /// This module is pub(crate) to ensure only the correct wrapper types access 8 | /// untyped nodes via the traits defined here. 9 | pub(crate) mod sealed { 10 | /// Implemented by types in this crate which wrap CachedNode. 11 | pub trait Memoized { 12 | /// Return a reference to the inner value. 13 | fn node(&self) -> &crate::cached_node::CachedNode; 14 | } 15 | } 16 | 17 | /// Node is an interface from which various types of DOM API objects inherit, 18 | /// allowing those types to be treated similarly; for example, inheriting the 19 | /// same set of methods, or being testable in the same way. 20 | /// 21 | /// Note: this trait cannot be implemented outside of this crate and is not 22 | /// intended for direct use in most cases. See the 23 | /// [`crate::interfaces::element`], module and its siblings, as well as the 24 | /// [`Parent`] and [`Child`] traits in this module. 25 | pub trait NodeWrapper: sealed::Memoized + Sized { 26 | /// Retrieves access to the raw HTML element underlying the (CachedNode). 27 | /// 28 | /// Because this offers an escape hatch around the memoized mutations, it 29 | /// should be used with caution. Also because of this, it has a silly 30 | /// name intended to loudly announce that care must be taken. 31 | /// 32 | /// Code called by the root function of your application will be run quite 33 | /// frequently and so the tools for memoization are important for 34 | /// keeping your application responsive. If you have legitimate needs 35 | /// for this API, please consider filing an issue with your use case so 36 | /// the maintainers of this crate can consider "official" ways to support 37 | /// it. 38 | fn raw_node_that_has_sharp_edges_please_be_careful(&self) -> &augdom::Node { 39 | self.node().raw_node() 40 | } 41 | } 42 | 43 | /// A value which can be bound as a child to a DOM node. 44 | pub trait Child: Sized { 45 | /// Returns the "raw" node for this child to bind to its parent. 46 | fn to_bind(&self) -> &augdom::Node; 47 | } 48 | 49 | /// A builder for DOM nodes 50 | pub trait NodeBuilder { 51 | /// The type of the DOM node 52 | type Output; 53 | 54 | /// Build, returning the output. 55 | fn build(self) -> Self::Output; 56 | } 57 | 58 | impl NodeBuilder for T 59 | where 60 | T: Display, 61 | { 62 | type Output = Text; 63 | 64 | fn build(self) -> Self::Output { 65 | // TODO rely on format_args, see [`(fmt_as_str #74442)`](https://github.com/rust-lang/rust/issues/74442) 66 | text(format!("{}", self)) 67 | } 68 | } 69 | 70 | impl Child for N 71 | where 72 | N: NodeWrapper, 73 | { 74 | fn to_bind(&self) -> &augdom::Node { 75 | self.raw_node_that_has_sharp_edges_please_be_careful() 76 | } 77 | } 78 | 79 | /// A node which accepts children. 80 | /// 81 | /// > Note: `C` is constrained by `Child` rather than `NodeWrapper` to allow 82 | /// custom components to be bound directly to DOM types. 83 | pub trait Parent: NodeWrapper { 84 | /// Add a child to this node. 85 | fn child>(self, child: T) -> Self { 86 | self.node().ensure_child_attached(child.build().to_bind()); 87 | self 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /illicit/macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Procedural macro support crate for the `illicit` crate. 2 | 3 | extern crate proc_macro; 4 | use proc_macro::TokenStream; 5 | use proc_macro_error::{abort, abort_call_site, proc_macro_error}; 6 | use quote::quote; 7 | use syn::{ 8 | parse::Parser, parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, 9 | Attribute, FnArg, ItemFn, Local, PatType, Stmt, Token, Type, TypeReference, 10 | }; 11 | 12 | #[proc_macro_attribute] 13 | #[proc_macro_error] 14 | pub fn from_env(args: TokenStream, input: TokenStream) -> TokenStream { 15 | let mut input_fn: ItemFn = parse_macro_input!(input); 16 | 17 | let args = Punctuated::::parse_terminated.parse(args).unwrap(); 18 | if args.is_empty() { 19 | abort_call_site!("must specify >=1 one argument"); 20 | } 21 | 22 | let doc_prelude = " 23 | # Environment Expectations 24 | 25 | This function requires the following types to be visible to [`illicit::get`] and will 26 | panic otherwise: 27 | "; 28 | 29 | for line in doc_prelude.lines() { 30 | input_fn.attrs.push(parse_quote!(#[doc = #line])); 31 | } 32 | 33 | for arg in args { 34 | let arg = match arg { 35 | FnArg::Receiver(rec) => abort!(rec.span(), "can't receive self by-environment"), 36 | FnArg::Typed(pt) => pt, 37 | }; 38 | let ([first_stmt, second_stmt], doc_attr) = bind_env_reference(&arg); 39 | input_fn.block.stmts.insert(0, Stmt::Local(first_stmt)); 40 | input_fn.block.stmts.insert(1, Stmt::Local(second_stmt)); 41 | input_fn.attrs.push(doc_attr); 42 | } 43 | 44 | quote::quote!(#input_fn).into() 45 | } 46 | 47 | /// Create a pair of local assignment expressions from the `pattern: &type` 48 | /// pair which is passed. 49 | fn bind_env_reference(arg: &PatType) -> ([Local; 2], Attribute) { 50 | let arg_span = arg.span(); 51 | 52 | let ty = match &*arg.ty { 53 | Type::Reference(TypeReference { lifetime, mutability, elem, .. }) => { 54 | if mutability.is_some() { 55 | abort!(mutability.span(), "mutable references cannot be passed by environment"); 56 | } 57 | 58 | if lifetime.is_some() { 59 | abort!( 60 | lifetime.span(), 61 | "cannot bind to concrete lifetimes for environment references" 62 | ); 63 | } 64 | 65 | quote!(#elem) 66 | } 67 | 68 | ty => abort!(ty.span(), "arguments must be references"), 69 | }; 70 | 71 | let name = *arg.pat.clone(); 72 | let init_expr = parse_quote!(illicit::expect::<#ty>()); 73 | let deref_expr = parse_quote!(&*#name); 74 | 75 | let shadowed = Local { 76 | attrs: vec![], 77 | let_token: Token![let](arg_span), 78 | pat: name.clone(), 79 | init: Some((Token![=](arg_span), Box::new(init_expr))), 80 | semi_token: Token![;](arg_span), 81 | }; 82 | let derefd = Local { 83 | attrs: vec![], 84 | let_token: Token![let](arg_span), 85 | pat: name, 86 | init: Some((Token![=](arg_span), Box::new(deref_expr))), 87 | semi_token: Token![;](arg_span), 88 | }; 89 | 90 | let ty_bullet = format!("* `{}`", ty); 91 | let doc_attr = parse_quote!(#[doc = #ty_bullet]); 92 | 93 | ([shadowed, derefd], doc_attr) 94 | } 95 | -------------------------------------------------------------------------------- /dom/examples/todo/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | use filter::Visibility; 4 | use header::input_header; 5 | use main_section::main_section; 6 | 7 | use illicit::AsContext; 8 | use mox::mox; 9 | use moxie_dom::{ 10 | elements::sectioning::{section, Section}, 11 | interfaces::element::Element, 12 | prelude::*, 13 | }; 14 | use std::sync::atomic::{AtomicU32, Ordering}; 15 | use tracing::*; 16 | use wasm_bindgen::prelude::*; 17 | 18 | pub mod filter; 19 | pub mod footer; 20 | pub mod header; 21 | pub mod input; 22 | pub mod item; 23 | pub mod main_section; 24 | 25 | #[topo::nested] 26 | fn todo_app() -> Section { 27 | mox! { 28 |
    29 | { input_header() } 30 | { main_section() } 31 |
    32 | } 33 | } 34 | 35 | pub(crate) struct App { 36 | pub todos: Key>, 37 | pub visibility: Key, 38 | } 39 | 40 | impl App { 41 | #[topo::nested] 42 | pub fn current() -> Self { 43 | let (_, visibility) = state(Visibility::default); 44 | let (_, todos) = 45 | // we allow the default empty to be overridden for testing 46 | // TODO support localStorage 47 | state(|| illicit::get::>().map(|d| d.clone()).unwrap_or_default()); 48 | 49 | Self { todos, visibility } 50 | } 51 | 52 | pub fn enter(self, f: impl FnMut() -> T) -> T { 53 | illicit::Layer::new().offer(self.todos).offer(self.visibility).enter(f) 54 | } 55 | 56 | pub fn boot(node: impl Into) { 57 | Self::boot_fn(&[], node, todo_app) 58 | } 59 | 60 | fn boot_fn( 61 | default_todos: &[Todo], 62 | node: impl Into, 63 | mut root: impl FnMut() -> Root + 'static, 64 | ) { 65 | let defaults = default_todos.to_vec(); 66 | moxie_dom::boot(node, move || defaults.clone().offer(|| App::current().enter(&mut root))); 67 | info!("running"); 68 | } 69 | } 70 | 71 | #[derive(Clone, Debug)] 72 | pub struct Todo { 73 | id: u32, 74 | title: String, 75 | completed: bool, 76 | } 77 | 78 | impl Todo { 79 | fn new(s: impl Into) -> Self { 80 | static NEXT_ID: AtomicU32 = AtomicU32::new(0); 81 | Self { id: NEXT_ID.fetch_add(1, Ordering::SeqCst), title: s.into(), completed: false } 82 | } 83 | } 84 | 85 | #[wasm_bindgen(start)] 86 | pub fn setup_tracing() { 87 | static SETUP: std::sync::Once = std::sync::Once::new(); 88 | SETUP.call_once(|| { 89 | let config = tracing_wasm::WASMLayerConfigBuilder::new() 90 | .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) 91 | .build(); 92 | tracing_wasm::set_as_global_default_with_config(config); 93 | std::panic::set_hook(Box::new(|info| { 94 | error!(?info, "crashed"); 95 | })); 96 | info!("tracing initialized"); 97 | }); 98 | console_error_panic_hook::set_once(); 99 | } 100 | 101 | #[wasm_bindgen] 102 | pub fn boot(root: moxie_dom::raw::sys::Node) { 103 | App::boot(root); 104 | } 105 | 106 | /// Included as a module within the crate rather than a separate file because 107 | /// cargo is grumpy about resolving the crate-under-test. 108 | #[cfg(test)] 109 | mod integration_tests; 110 | 111 | #[cfg(test)] 112 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 113 | -------------------------------------------------------------------------------- /dom/src/embed.rs: -------------------------------------------------------------------------------- 1 | //! Embedding APIs offering finer-grained control over execution of the runtime. 2 | 3 | use crate::{cached_node::CachedNode, interfaces::node::Child}; 4 | use futures::{ 5 | future::LocalFutureObj, 6 | task::{LocalSpawn, SpawnError}, 7 | }; 8 | use moxie::runtime::RunLoop; 9 | 10 | /// Wrapper around `moxie::runtime::RunLoop` and a root function which returns a 11 | /// DOM node. After each call to `run_once` the node returned from the root 12 | /// function is re-attached to the provided parent, removing other children. 13 | #[must_use] 14 | pub struct DomLoop { 15 | inner: RunLoop>, 16 | } 17 | 18 | impl DomLoop { 19 | /// Construct a new `DomLoop` which will maintain the children of the 20 | /// provided `parent`. 21 | /// 22 | /// On its own, a `WebRuntime` is inert and must either have its `run_once` 23 | /// method called when a re-render is needed, or be scheduled with 24 | /// [`DomLoop::animation_frame_scheduler`]. 25 | pub fn new( 26 | parent: impl Into, 27 | mut root: impl (FnMut() -> Root) + 'static, 28 | ) -> Self { 29 | let parent = parent.into(); 30 | 31 | let mut inner = RunLoop::new(Box::new(move || { 32 | let parent = CachedNode::new(parent.clone()); 33 | #[allow(clippy::redundant_closure)] // removing the closure syntax makes this FnOnce 34 | let new_root = topo::call(|| root()); 35 | 36 | parent.ensure_child_attached(new_root.to_bind()); 37 | parent.remove_trailing_children(); 38 | }) as Box); 39 | 40 | #[cfg(feature = "webdom")] 41 | { 42 | inner.set_task_executor(WebSpawner); 43 | } 44 | 45 | Self { inner } 46 | } 47 | 48 | /// Run the root function in a fresh `moxie::Revision` and bind the returned 49 | /// node as the child of the loop's parent. 50 | pub fn run_once(&mut self) { 51 | self.inner.run_once(); 52 | } 53 | } 54 | 55 | #[cfg(feature = "rsdom")] 56 | impl DomLoop { 57 | /// Construct a new `DomLoop` which will maintain the children of a virtual 58 | /// `
    `. 59 | /// 60 | /// Internally calls [`DomLoop::new`]. 61 | #[cfg(feature = "rsdom")] 62 | pub fn new_virtual( 63 | parent: impl Into, 64 | root: impl (FnMut() -> Root) + 'static, 65 | ) -> Self { 66 | Self::new(parent, augdom::in_virtual_document(root)) 67 | } 68 | } 69 | 70 | #[cfg(feature = "webdom")] 71 | impl DomLoop { 72 | /// Pass ownership of this runtime to a "loop" which runs with 73 | /// `requestAnimationFrame`. 74 | pub fn animation_frame_scheduler(self) -> raf::AnimationFrameScheduler { 75 | raf::AnimationFrameScheduler::new(self) 76 | } 77 | } 78 | 79 | #[cfg(feature = "webdom")] 80 | impl raf::Tick for DomLoop { 81 | fn tick(&mut self) { 82 | self.inner.run_once(); 83 | } 84 | } 85 | 86 | #[cfg(feature = "webdom")] 87 | impl raf::Waking for DomLoop { 88 | fn set_waker(&mut self, wk: std::task::Waker) { 89 | self.inner.set_state_change_waker(wk); 90 | } 91 | } 92 | 93 | #[cfg(feature = "webdom")] 94 | struct WebSpawner; 95 | 96 | #[cfg(feature = "webdom")] 97 | impl LocalSpawn for WebSpawner { 98 | fn spawn_local_obj(&self, fut: LocalFutureObj<'static, ()>) -> Result<(), SpawnError> { 99 | wasm_bindgen_futures::spawn_local(fut); 100 | Ok(()) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /ofl/src/server/inject.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | dev::{MessageBody, ResponseBody, ServiceResponse}, 3 | error::Error as WebError, 4 | web, 5 | }; 6 | use futures::stream::{Stream, StreamExt}; 7 | use lol_html::{element, html_content::ContentType, RewriteStrSettings}; 8 | use tracing::*; 9 | 10 | const RELOAD_ON_CHANGES: &str = 11 | include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/reloadOnChanges.js")); 12 | const CHANGES_URL: &str = "/ch-ch-ch-changes"; 13 | 14 | pub async fn reload_on_changes_into_html( 15 | mut response: ServiceResponse, 16 | ) -> Result, actix_web::error::Error> 17 | where 18 | B: MessageBody + Unpin, 19 | { 20 | if let Some(content_type) = response.headers().get("content-type") { 21 | if content_type != "text/html" { 22 | return Ok(response); 23 | } 24 | } 25 | 26 | let path = response.request().path(); 27 | if path == CHANGES_URL { 28 | return Ok(response); 29 | } 30 | info!({ %path }, "serving html page"); 31 | 32 | let mut body = collect_body(response.take_body()).await; 33 | 34 | if let Some(rewritten) = inject_script_tag(&body) { 35 | body = rewritten; 36 | } 37 | 38 | Ok(response.map_body(|_, _| ResponseBody::Other(body.into()))) 39 | } 40 | 41 | fn inject_script_tag(body: &str) -> Option { 42 | lol_html::rewrite_str( 43 | body, 44 | RewriteStrSettings { 45 | element_content_handlers: vec![element!("head", |head| { 46 | info!("inserting script tag"); 47 | head.append("", ContentType::Html); 50 | Ok(()) 51 | })], 52 | ..Default::default() 53 | }, 54 | ) 55 | .ok() 56 | } 57 | 58 | async fn collect_body( 59 | mut body: impl Stream> + Unpin, 60 | ) -> String { 61 | let mut buf = Vec::new(); 62 | 63 | while let Some(Ok(bytes)) = body.next().await { 64 | buf.extend(bytes); 65 | } 66 | 67 | String::from_utf8(buf).unwrap() 68 | } 69 | 70 | #[cfg(test)] 71 | mod tests { 72 | use super::*; 73 | 74 | macro_rules! assert_injected { 75 | ($s:expr) => {{ 76 | let base = $s; 77 | let injected = inject_script_tag(base).unwrap(); 78 | assert!(!base.contains(CHANGES_URL), "original shouldn't have reload URL"); 79 | assert!(injected.contains(CHANGES_URL), "must have injected reload URL"); 80 | assert!(injected.contains("=>"), "must preserve lambda notation without escaping") 81 | }}; 82 | } 83 | 84 | macro_rules! assert_untouched { 85 | ($s:expr) => {{ 86 | let base = $s; 87 | assert_eq!(inject_script_tag(base).unwrap(), base, "contents shouldn't change"); 88 | }}; 89 | } 90 | 91 | #[test] 92 | fn inject() { 93 | assert_injected!(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../index.html"))); 94 | } 95 | 96 | #[test] 97 | fn inject_weird_whitespace() { 98 | assert_injected!(" "); 99 | } 100 | 101 | #[test] 102 | fn inject_toml_nop() { 103 | assert_untouched!(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../Cargo.toml"))); 104 | } 105 | 106 | #[test] 107 | fn inject_parse_fail_nop() { 108 | assert_untouched!(" "); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at adam.n.perry+conduct@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /dom/examples/todo/src/item.rs: -------------------------------------------------------------------------------- 1 | use crate::{input::text_input, Todo}; 2 | use mox::mox; 3 | use moxie_dom::{ 4 | elements::{ 5 | forms::Input, 6 | html::*, 7 | text_content::{Div, Li}, 8 | }, 9 | prelude::*, 10 | }; 11 | 12 | #[illicit::from_env(todos: &Key>)] 13 | fn item_edit_input(todo: Todo, editing: Key) -> Input { 14 | let todos = todos.clone(); 15 | let text = todo.title.clone(); 16 | text_input(&text, true, move |value: String| { 17 | editing.set(false); 18 | todos.update(|todos| { 19 | let mut todos = todos.to_vec(); 20 | if let Some(mut todo) = todos.iter_mut().find(|t| t.id == todo.id) { 21 | todo.title = value; 22 | } 23 | Some(todos) 24 | }); 25 | }) 26 | } 27 | 28 | #[illicit::from_env(todos: &Key>)] 29 | fn item_with_buttons(todo: Todo, editing: Key) -> Div { 30 | let todos = todos.clone(); 31 | let id = todo.id; 32 | let toggle_todos = todos.clone(); 33 | 34 | let toggle_completion = move |_| { 35 | toggle_todos.update(|t| { 36 | Some( 37 | t.iter() 38 | .cloned() 39 | .map(move |mut t| { 40 | if t.id == id { 41 | t.completed = !t.completed; 42 | t 43 | } else { 44 | t 45 | } 46 | }) 47 | .collect(), 48 | ) 49 | }) 50 | }; 51 | 52 | mox! { 53 |
    54 | 55 | 56 | 59 | 60 |
    64 | } 65 | } 66 | 67 | #[topo::nested(slot = "&todo.id")] 68 | pub fn todo_item(todo: &Todo) -> Li { 69 | let (editing, set_editing) = state(|| false); 70 | 71 | let mut classes = String::new(); 72 | if todo.completed { 73 | classes.push_str("completed "); 74 | } 75 | if *editing { 76 | classes.push_str("editing"); 77 | } 78 | 79 | let mut item = li(); 80 | item = item.class(classes); 81 | 82 | if *editing { 83 | item = item.child(item_edit_input(todo.clone(), set_editing)); 84 | } else { 85 | item = item.child(item_with_buttons(todo.clone(), set_editing)); 86 | } 87 | 88 | item.build() 89 | } 90 | 91 | #[cfg(test)] 92 | mod tests { 93 | use super::*; 94 | use pretty_assertions::assert_eq; 95 | 96 | #[wasm_bindgen_test::wasm_bindgen_test] 97 | pub async fn single_item() { 98 | let root = document().create_element("div"); 99 | crate::App::boot_fn(&[Todo::new("weeeee")], root.clone(), || { 100 | let todo = &illicit::expect::>>()[0]; 101 | todo_item(todo) 102 | }); 103 | 104 | assert_eq!( 105 | root.pretty_outer_html(2), 106 | r#"
    107 |
  • 108 |
    109 | 110 | 111 | 112 | 114 |
    115 |
  • 116 |
    "# 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /ofl/src/published.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Context, Error}; 2 | use cargo_metadata::{Package, PackageId}; 3 | use crates_index::Index; 4 | use gumdrop::Options; 5 | use std::path::PathBuf; 6 | use tracing::*; 7 | 8 | use crate::workspace::Workspace; 9 | 10 | #[derive(Debug, Options)] 11 | pub struct EnsurePublished { 12 | help: bool, 13 | /// Disables publishing to crates.io. 14 | #[options(no_short)] 15 | dry_run: bool, 16 | } 17 | 18 | impl EnsurePublished { 19 | pub fn run(self, root: PathBuf) -> Result<(), Error> { 20 | let workspace = Workspace::get(&root)?; 21 | info!(workspace = %root.display(), "identifying packages to publish"); 22 | let to_publish = packages_to_publish(&workspace)?; 23 | for id in to_publish { 24 | let package = &workspace.metadata[&id]; 25 | publish(package, self.dry_run)?; 26 | 27 | let tag = format!("{}-v{}", package.name, package.version); 28 | let message = format!("Published {}.", &tag); 29 | workspace.tag_head(&tag, &message)?; 30 | 31 | if !self.dry_run { 32 | info!("sleeping a bit"); 33 | std::thread::sleep(std::time::Duration::from_secs(30)); 34 | } 35 | } 36 | Ok(()) 37 | } 38 | } 39 | 40 | fn packages_to_publish(workspace: &Workspace) -> Result, Error> { 41 | let index = Index::new_cargo_default(); 42 | info!("updating cargo index"); 43 | index.retrieve_or_update()?; 44 | 45 | let members = workspace.local_members(); 46 | let mut to_publish_ids = vec![]; 47 | 48 | for member in members { 49 | let package = &workspace.metadata[&member]; 50 | let version_str = package.version.to_string(); 51 | 52 | let manifest = std::fs::read_to_string(&package.manifest_path)?; 53 | if manifest.contains("publish = false") { 54 | info!({ %package.name }, "skipping `publish = false`"); 55 | continue; 56 | } 57 | 58 | if package.version.is_prerelease() { 59 | info!({ %package.name, %package.version }, "skipping pre-release version"); 60 | } else if index 61 | .crate_(&package.name) 62 | .map(|c| c.versions().iter().any(|v| v.version() == version_str)) 63 | .unwrap_or_default() 64 | { 65 | info!({ %package.name, %package.version }, "skipping already-published version"); 66 | } else { 67 | to_publish_ids.push(member); 68 | } 69 | } 70 | 71 | let to_publish = 72 | to_publish_ids.iter().map(|id| &workspace.metadata[id].name).collect::>(); 73 | 74 | info!("will publish: {:#?}", &to_publish); 75 | Ok(to_publish_ids) 76 | } 77 | 78 | fn publish(package: &Package, dry_run: bool) -> Result<(), Error> { 79 | info!({ %package.name, %package.version }, "publishing"); 80 | 81 | let subcommand = if dry_run { "package" } else { "publish" }; 82 | 83 | let output = std::process::Command::new("cargo") 84 | .arg(subcommand) 85 | .arg("--manifest-path") 86 | .arg(&package.manifest_path) 87 | .output()?; 88 | 89 | if !output.status.success() { 90 | let stderr = String::from_utf8(output.stderr).context("output as utf8")?; 91 | let stdout = String::from_utf8(output.stdout).context("output as utf8")?; 92 | error!( 93 | "failed to package {} 94 | stderr: 95 | {} 96 | stdout: 97 | {}", 98 | package.manifest_path.display(), 99 | stderr, 100 | stdout, 101 | ); 102 | bail!("cargo failure"); 103 | } 104 | 105 | Ok(()) 106 | } 107 | -------------------------------------------------------------------------------- /dom/src/interfaces/content_categories.rs: -------------------------------------------------------------------------------- 1 | //! Every HTML element is a member of one or more content categories — these 2 | //! categories group elements that share common characteristics. This is a loose 3 | //! grouping (it doesn't actually create a relationship among elements of these 4 | //! categories), but they help define and describe the categories' shared 5 | //! behavior and their associated rules, especially when you come upon their 6 | //! intricate details. It's also possible for elements to not be a member of any 7 | //! of these categories. 8 | //! 9 | //! There are three types of content categories: 10 | //! 11 | //! 1. Main content categories, which describe common rules shared by many 12 | //! elements. 13 | //! 2. Form-related content categories, which describe rules common to 14 | //! form-related elements. 15 | //! 3. Specific content categories, which describe rare categories shared 16 | //! only by a few elements, sometimes only in a specific context. 17 | 18 | use crate::interfaces::node::Child; 19 | 20 | /// Elements belonging to the metadata content category modify the presentation 21 | /// or the behavior of the rest of the document, set up links to other 22 | /// documents, or convey other out of band information. 23 | pub trait MetadataContent: Child {} 24 | 25 | /// Elements belonging to the flow content category typically contain text or 26 | /// embedded content. 27 | pub trait FlowContent: Child {} 28 | 29 | /// Elements belonging to the sectioning content model create a section in the 30 | /// current outline that defines the scope of
    elements,
    31 | /// elements, and heading content. 32 | pub trait SectioningContent: Child {} 33 | 34 | /// Heading content defines the title of a section, whether marked by an 35 | /// explicit sectioning content element, or implicitly defined by the heading 36 | /// content itself. 37 | pub trait HeadingContent: Child {} 38 | 39 | /// Phrasing content defines the text and the mark-up it contains. Runs of 40 | /// phrasing content make up paragraphs. 41 | pub trait PhrasingContent: Child {} 42 | 43 | /// Embedded content imports another resource or inserts content from another 44 | /// mark-up language or namespace into the document. 45 | pub trait EmbeddedContent: Child {} 46 | 47 | /// Interactive content includes elements that are specifically designed for 48 | /// user interaction. 49 | pub trait InteractiveContent: Child {} 50 | 51 | /// Form-associated content comprises elements that have a form owner, exposed 52 | /// by a form attribute. A form owner is either the containing
    element or 53 | /// the element whose id is specified in the form attribute. 54 | pub trait FormAssociatedContent: Child {} 55 | 56 | /// Elements that are listed in the form.elements and fieldset.elements IDL 57 | /// collections. 58 | pub trait ListedContent: Child {} 59 | 60 | /// Elements that can be associated with
    98 |
  • 99 |
  • 100 |
    101 | 102 | 103 | 104 | 106 |
    107 |
  • 108 |
  • 109 |
    110 | 111 | 112 | 113 | 115 |
    116 |
  • 117 | 118 |
    119 | 120 | 3 items left 121 | 132 |
    133 | 134 | "# 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /dom/src/interfaces/html_element.rs: -------------------------------------------------------------------------------- 1 | //! Trait for the base class of HTML elements. 2 | 3 | use crate::interfaces::element::ElementBuilder; 4 | 5 | /// The HTMLElement interface represents any HTML element. 6 | /// 7 | /// Note: this trait cannot be implemented outside of this crate. 8 | pub trait HtmlElementBuilder: ElementBuilder { 9 | attr_method! { 10 | /// Keyboard shortcut to activate or add focus to the element. 11 | accesskey 12 | } 13 | 14 | attr_method! { 15 | /// Sets whether input is automatically capitalized when entered by user. It can have the 16 | /// following values: 17 | /// 18 | /// * `off` or `none`, no autocapitalization is applied (all letters default to lowercase) 19 | /// * `on` or `sentences`, the first letter of each sentence defaults to a capital letter; 20 | /// all other letters default to lowercase 21 | /// * `words`, the first letter of each word defaults to a capital letter; all other letters 22 | /// default to lowercase 23 | /// * `characters`, all letters should default to uppercase 24 | autocapitalize 25 | } 26 | 27 | attr_method! { 28 | /// A value of "true" means the element is editable and a value of "false" means it isn't. 29 | contenteditable(bool) 30 | } 31 | 32 | // TODO(#240) intern the data attribute strings so we can have 'static lifetime attribute names 33 | // /// Forms a class of attributes, called custom data attributes, that allow 34 | // /// proprietary information to be exchanged between the HTML and its DOM 35 | // /// representation that may be used by scripts. 36 | // fn data(self, key: impl ToString, value: impl AsRef) -> Self { 37 | // self.attribute(&format!("data-{}", key.to_string()), value.as_ref()) 38 | // } 39 | 40 | attr_method! { 41 | /// The directionality of the element. It can have the following values: 42 | /// 43 | /// * `ltr`, which means left to right and is to be used for languages that are written from 44 | /// the left to the right (like English); 45 | /// * `rtl`, which means right to left and is to be used for languages that are written from 46 | /// the right to the left (like Arabic); 47 | /// * `auto`, which lets the user agent decide. It uses a basic algorithm as it parses the 48 | /// characters inside the element until it finds a character with a strong directionality, 49 | /// then it applies that directionality to the whole element. 50 | dir 51 | } 52 | 53 | attr_method! { 54 | /// Defines whether the element can be dragged. 55 | draggable(bool) 56 | } 57 | 58 | attr_method! { 59 | /// Indicates if the element is hidden or not. 60 | hidden(bool) 61 | } 62 | 63 | attr_method! { 64 | /// Indicates whether the user agent must act as though the given node is absent 65 | /// for the purposes of user interaction events, in-page text searches ("find in page"), and 66 | /// text selection. 67 | inert(bool) 68 | } 69 | 70 | attr_method! { 71 | /// Provides a hint as to the type of data that might be entered by the user 72 | /// while editing the element or its contents. The attribute can be used with 73 | /// form controls (such as the value of textarea elements), or in elements in 74 | /// an editing host (e.g., using contenteditable attribute). 75 | inputmode 76 | } 77 | 78 | attr_method! { 79 | /// The language of an element's attributes, text, and element contents. 80 | lang 81 | } 82 | 83 | attr_method! { 84 | /// Assigns a slot in a shadow DOM shadow tree to an element. 85 | slot 86 | } 87 | 88 | attr_method! { 89 | /// Indicates whether spell checking is allowed for the element. 90 | spellcheck 91 | } 92 | 93 | attr_method! { 94 | /// Overrides the browser's default tab order and follows the one specified 95 | /// instead. 96 | tabindex 97 | } 98 | 99 | attr_method! { 100 | /// The text that appears in a popup box when mouse is over the element. 101 | title 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /dom/raf/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Provides a scheduled loop in the browser via `requestAnimationFrame`. 2 | 3 | #![deny(missing_docs)] 4 | 5 | use futures::task::{waker, ArcWake}; 6 | use std::{ 7 | cell::{Cell, RefCell}, 8 | rc::Rc, 9 | sync::Arc, 10 | task::Waker, 11 | }; 12 | use wasm_bindgen::{prelude::*, JsCast}; 13 | use web_sys::window; 14 | 15 | /// A value which can be mutably called by the scheduler. 16 | pub trait Tick: 'static { 17 | /// Tick this value, indicating a new frame request is being fulfilled. 18 | fn tick(&mut self); 19 | } 20 | 21 | /// A value which can receive a waker from the scheduler that will request a new 22 | /// frame when woken. 23 | pub trait Waking { 24 | /// Receive a waker from the scheduler that calls `requestAnimationFrame` 25 | /// when woken. 26 | fn set_waker(&mut self, wk: Waker); 27 | } 28 | 29 | /// Owns a `WebRuntime` and schedules its execution using 30 | /// `requestAnimationFrame`. 31 | #[must_use] 32 | pub struct AnimationFrameScheduler(Rc>); 33 | 34 | struct AnimationFrameState { 35 | ticker: RefCell, 36 | handle: Cell>, 37 | } 38 | 39 | impl ArcWake for AnimationFrameScheduler { 40 | fn wake_by_ref(arc_self: &Arc>) { 41 | arc_self.ensure_scheduled(false); 42 | } 43 | } 44 | 45 | impl AnimationFrameScheduler { 46 | /// Construct a new scheduler with the provided callback. `ticker.tick()` 47 | /// will be called once per fulfilled animation frame request. 48 | pub fn new(ticker: T) -> Self { 49 | AnimationFrameScheduler(Rc::new(AnimationFrameState { 50 | ticker: RefCell::new(ticker), 51 | handle: Cell::new(None), 52 | })) 53 | } 54 | 55 | fn ensure_scheduled(&self, immediately_again: bool) { 56 | let existing = self.0.handle.replace(None); 57 | let handle = existing.unwrap_or_else(|| { 58 | let self2 = AnimationFrameScheduler(Rc::clone(&self.0)); 59 | let callback = Closure::once(Box::new(move || { 60 | self2.0.handle.set(None); 61 | 62 | self2.0.ticker.borrow_mut().tick(); 63 | 64 | if immediately_again { 65 | self2.ensure_scheduled(true); 66 | } 67 | })); 68 | 69 | AnimationFrameHandle::request(callback) 70 | }); 71 | self.0.handle.set(Some(handle)); 72 | } 73 | 74 | /// Consumes the scheduler to initiate a `requestAnimationFrame` callback 75 | /// loop where new animation frames are requested immmediately after the 76 | /// last `moxie::Revision` is completed. `WebRuntime::run_once` is 77 | /// called once per requested animation frame. 78 | pub fn run_on_every_frame(self) { 79 | self.ensure_scheduled(true); 80 | } 81 | } 82 | 83 | impl AnimationFrameScheduler { 84 | /// Consumes the scheduler to initiate a `requestAnimationFrame` callback 85 | /// loop where new animation frames are requested whenever the waker 86 | /// passed to the provided closure is woken. 87 | pub fn run_on_wake(self) { 88 | let state = Rc::clone(&self.0); 89 | let waker = waker(Arc::new(self)); 90 | state.ticker.borrow_mut().set_waker(waker); 91 | state.ticker.borrow_mut().tick(); 92 | } 93 | } 94 | 95 | // don't send these to workers until have a fix :P 96 | unsafe impl Send for AnimationFrameScheduler {} 97 | unsafe impl Sync for AnimationFrameScheduler {} 98 | 99 | struct AnimationFrameHandle { 100 | raw: i32, 101 | /// Prefixed with an underscore because it is only read by JS, otherwise 102 | /// we'll get a warning. 103 | _callback: Closure, 104 | } 105 | 106 | impl AnimationFrameHandle { 107 | fn request(callback: Closure) -> Self { 108 | let raw = 109 | window().unwrap().request_animation_frame(callback.as_ref().unchecked_ref()).unwrap(); 110 | 111 | Self { raw, _callback: callback } 112 | } 113 | } 114 | 115 | impl Drop for AnimationFrameHandle { 116 | fn drop(&mut self) { 117 | window().unwrap().cancel_animation_frame(self.raw).ok(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /dom/src/interfaces/global_events.rs: -------------------------------------------------------------------------------- 1 | //! Trait for defining the methods related to the event handlers shared by all 2 | //! HTML elements. 3 | 4 | use crate::interfaces::{event_target::EventTarget, html_element::HtmlElementBuilder}; 5 | use augdom::event::*; 6 | 7 | /// An event which can be handled on any element. 8 | pub trait GlobalEvent: Event + 'static {} 9 | 10 | macro_rules! global_events { 11 | ($($property:ident <- $event:ident,)+) => { 12 | 13 | $(impl GlobalEvent for $event {})+ 14 | 15 | /// These event handlers are defined on the [GlobalEventHandlers][mdn] mixin, 16 | /// and implemented by HTMLElement, Document, Window, as well as by 17 | /// WorkerGlobalScope for Web Workers. 18 | /// 19 | /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers 20 | pub trait GlobalEventHandler: 21 | HtmlElementBuilder 22 | $(+ EventTarget<$event>)+ 23 | {$( 24 | /// Set an event handler. 25 | fn $property(self, callback: impl FnMut(augdom::event::$event) + 'static) -> Self { 26 | self.on(callback) 27 | } 28 | )+} 29 | 30 | }; 31 | } 32 | 33 | global_events! { 34 | onabort <- Abort, 35 | onblur <- Blur, 36 | oncancel <- Cancel, 37 | oncanplay <- CanPlay, 38 | oncanplaythrough <- CanPlayThrough, 39 | onchange <- Change, 40 | onclick <- Click, 41 | onclose <- CloseWebsocket, 42 | oncontextmenu <- ContextMenu, 43 | oncuechange <- CueChange, 44 | ondblclick <- DoubleClick, 45 | ondrag <- Drag, 46 | ondragend <- DragEnd, 47 | ondragenter <- DragEnter, 48 | ondragexit <- DragExit, 49 | ondragleave <- DragLeave, 50 | ondragover <- DragOver, 51 | ondragstart <- DragStart, 52 | ondrop <- Dropped, 53 | ondurationchange <- DurationChange, 54 | onemptied <- Emptied, 55 | onended <- PlaybackEnded, 56 | onerror <- ErrorEvent, 57 | onfocus <- Focus, 58 | ongotpointercapture <- GotPointerCapture, 59 | oninput <- Input, 60 | oninvalid <- Invalid, 61 | onkeydown <- KeyDown, 62 | onkeypress <- KeyPress, 63 | onkeyup <- KeyUp, 64 | onload <- ResourceLoad, 65 | onloadeddata <- DataLoaded, 66 | onloadedmetadata <- MetadataLoaded, 67 | onloadend <- LoadEnd, 68 | onloadstart <- LoadStart, 69 | onlostpointercapture <- LostPointerCapture, 70 | onmouseenter <- MouseEnter, 71 | onmouseleave <- MouseLeave, 72 | onmousemove <- MouseMove, 73 | onmouseout <- MouseOut, 74 | onmouseover <- MouseOver, 75 | onmouseup <- MouseUp, 76 | onpause <- Pause, 77 | onplay <- Play, 78 | onplaying <- Playing, 79 | onpointercancel <- PointerCancel, 80 | onpointerdown <- PointerDown, 81 | onpointerenter <- PointerEnter, 82 | onpointerleave <- PointerLeave, 83 | onpointermove <- PointerMove, 84 | onpointerout <- PointerOut, 85 | onpointerover <- PointerOver, 86 | onpointerup <- PointerUp, 87 | onprogress <- Progress, 88 | onratechange <- PlaybackRateChange, 89 | onreset <- FormReset, 90 | onresize <- ViewResize, 91 | onscroll <- Scroll, 92 | onseeked <- Seeked, 93 | onseeking <- Seeking, 94 | onselect <- Select, 95 | onselectionchange <- SelectionChange, 96 | onselectstart <- SelectionStart, 97 | onshow <- ContextMenuShow, 98 | onstalled <- Stalled, 99 | onsubmit <- Submit, 100 | onsuspend <- Suspend, 101 | ontimeupdate <- TimeUpdate, 102 | onvolumechange <- VolumeChange, 103 | onwaiting <- Waiting, 104 | onwheel <- Wheel, 105 | } 106 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "core crates", 8 | "type": "shell", 9 | "presentation": { 10 | "group": "main", 11 | "panel": "dedicated" 12 | }, 13 | "runOptions": { 14 | "runOn": "folderOpen" 15 | }, 16 | "isBackground": true, 17 | "command": "cargo", 18 | "args": [ 19 | "core-flow" 20 | ], 21 | "problemMatcher": [ 22 | "$rustc-watch" 23 | ] 24 | }, 25 | { 26 | "label": "dom crates", 27 | "type": "shell", 28 | "presentation": { 29 | "group": "main", 30 | "panel": "dedicated" 31 | }, 32 | "runOptions": { 33 | "runOn": "folderOpen" 34 | }, 35 | "isBackground": true, 36 | "command": "cargo", 37 | "args": [ 38 | "dom-flow" 39 | ], 40 | "problemMatcher": [ 41 | "$rustc-watch" 42 | ] 43 | }, 44 | { 45 | "label": "project website", 46 | "type": "shell", 47 | "presentation": { 48 | "group": "tools", 49 | "panel": "dedicated" 50 | }, 51 | "runOptions": { 52 | "runOn": "folderOpen" 53 | }, 54 | "isBackground": true, 55 | "command": "cargo", 56 | "args": [ 57 | "site-flow" 58 | ], 59 | "problemMatcher": [ 60 | "$rustc-watch" 61 | ] 62 | }, 63 | { 64 | "label": "project server", 65 | "type": "shell", 66 | "presentation": { 67 | "group": "tools", 68 | "panel": "dedicated" 69 | }, 70 | "runOptions": { 71 | "runOn": "folderOpen" 72 | }, 73 | "isBackground": true, 74 | "command": "cargo", 75 | "args": [ 76 | "server" 77 | ], 78 | "problemMatcher": [] 79 | }, 80 | { 81 | "label": "ofl crates", 82 | "type": "shell", 83 | "presentation": { 84 | "group": "tools", 85 | "panel": "dedicated" 86 | }, 87 | "runOptions": { 88 | "runOn": "folderOpen" 89 | }, 90 | "isBackground": true, 91 | "command": "cargo", 92 | "args": [ 93 | "ofl-flow" 94 | ], 95 | "problemMatcher": [ 96 | "$rustc-watch" 97 | ] 98 | }, 99 | { 100 | "label": "docs/fmt", 101 | "type": "shell", 102 | "presentation": { 103 | "group": "tools", 104 | "panel": "dedicated" 105 | }, 106 | "runOptions": { 107 | "runOn": "folderOpen" 108 | }, 109 | "isBackground": true, 110 | "command": "cargo", 111 | "args": [ 112 | "watch", 113 | "-x", 114 | "ofl-fmt-project", 115 | "-x", 116 | "docs-all", 117 | ], 118 | "problemMatcher": [ 119 | "$rustc-watch" 120 | ] 121 | }, 122 | { 123 | "label": "cypress", 124 | "type": "shell", 125 | "presentation": { 126 | "group": "tools", 127 | "panel": "dedicated" 128 | }, 129 | "runOptions": { 130 | "runOn": "folderOpen" 131 | }, 132 | "isBackground": true, 133 | "command": "npx", 134 | "args": [ 135 | "cypress", 136 | "open" 137 | ], 138 | "options": { 139 | "cwd": "${workspaceFolder}/dom/examples/todo/e2e" 140 | }, 141 | "problemMatcher": [] 142 | } 143 | ] 144 | } -------------------------------------------------------------------------------- /dom/examples/ssr/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate gotham_derive; 3 | #[macro_use] 4 | extern crate serde_derive; 5 | 6 | use augdom::Dom; 7 | use gotham::{ 8 | router::{builder::*, Router}, 9 | state::{FromState, State}, 10 | }; 11 | use mox::mox; 12 | use moxie_dom::{ 13 | elements::text_content::{li, ul, Ul}, 14 | embed::DomLoop, 15 | prelude::*, 16 | }; 17 | 18 | fn main() { 19 | let addr = "127.0.0.1:7878"; 20 | println!("Listening for requests at http://{}", addr); 21 | gotham::start(addr, router()) 22 | } 23 | 24 | #[derive(Deserialize, StateData, StaticResponseExtender)] 25 | struct PathExtractor { 26 | #[serde(rename = "*")] 27 | parts: Vec, 28 | } 29 | 30 | #[topo::nested] 31 | fn simple_list(items: &[String]) -> Ul { 32 | let mut list = ul(); 33 | for item in items { 34 | list = list.child(mox!(
  • { item }
  • )); 35 | } 36 | list.build() 37 | } 38 | 39 | fn parts_handler(state: State) -> (State, String) { 40 | let parts = { 41 | let path = PathExtractor::borrow_from(&state); 42 | path.parts.to_owned() 43 | }; 44 | let web_div = augdom::create_virtual_element("div"); 45 | let mut renderer = DomLoop::new_virtual(web_div.clone(), move || simple_list(&parts)); 46 | renderer.run_once(); 47 | (state, web_div.pretty_outer_html(2)) 48 | } 49 | 50 | fn router() -> Router { 51 | build_simple_router(|route| { 52 | route.get("/parts/*").with_path_extractor::().to(parts_handler); 53 | }) 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use super::*; 59 | use gotham::test::TestServer; 60 | use hyper::StatusCode; 61 | 62 | #[test] 63 | fn extracts_one_component() { 64 | let test_server = TestServer::new(router()).unwrap(); 65 | let response = test_server.client().get("http://localhost/parts/head").perform().unwrap(); 66 | 67 | assert_eq!(response.status(), StatusCode::OK); 68 | 69 | let body = String::from_utf8(response.read_body().unwrap()).unwrap(); 70 | assert_eq!( 71 | &body, 72 | r#"
    73 |
      74 |
    • head
    • 75 |
    76 |
    "#, 77 | ); 78 | } 79 | 80 | #[test] 81 | fn extracts_multiple_components() { 82 | let test_server = TestServer::new(router()).unwrap(); 83 | let response = test_server 84 | .client() 85 | .get("http://localhost/parts/head/shoulders/knees/toes") 86 | .perform() 87 | .unwrap(); 88 | 89 | assert_eq!(response.status(), StatusCode::OK); 90 | 91 | let body = String::from_utf8(response.read_body().unwrap()).unwrap(); 92 | assert_eq!( 93 | &body, 94 | &r#"
    95 |
      96 |
    • head
    • 97 |
    • shoulders
    • 98 |
    • knees
    • 99 |
    • toes
    • 100 |
    101 |
    "#, 102 | ); 103 | } 104 | 105 | #[test] 106 | fn basic_list_prerender() { 107 | let root = augdom::create_virtual_element("div"); 108 | let mut tester = DomLoop::new_virtual(root.clone(), move || { 109 | mox! { 110 |
      111 |
    • "first"
    • 112 |
    • "second"
    • 113 |
    • "third"
    • 114 |
    115 | } 116 | }); 117 | 118 | tester.run_once(); 119 | 120 | assert_eq!( 121 | &root.outer_html(), 122 | r#"
    • first
    • second
    • third
    "#, 123 | "concisely-rendered string output must have no newlines or indentation" 124 | ); 125 | 126 | assert_eq!( 127 | // this newline lets the below string output seem legible 128 | format!("\n{:#?}", &root), 129 | r#" 130 |
    131 |
      132 |
    • first
    • 133 |
    • second
    • 134 |
    • third
    • 135 |
    136 |
    "#, 137 | "pretty debug output must be 4-space-indented" 138 | ); 139 | 140 | assert_eq!( 141 | // this newline lets the below string output seem legible 142 | format!("\n{}", &root), 143 | r#" 144 |
    145 |
      146 |
    • first
    • 147 |
    • second
    • 148 |
    • third
    • 149 |
    150 |
    "#, 151 | "Display output must be 2-space-indented" 152 | ); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /dom/src/cached_node.rs: -------------------------------------------------------------------------------- 1 | //! Nodes which cache mutations. 2 | 3 | use augdom::{Dom, Node}; 4 | use moxie::cache_with; 5 | use std::{ 6 | cell::Cell, 7 | fmt::{Debug, Formatter, Result as FmtResult}, 8 | }; 9 | 10 | /// A topologically-nested "incremental smart pointer" for an HTML element. 11 | /// 12 | /// Created during execution of the (element) macro and the element-specific 13 | /// wrappers. Offers a "stringly-typed" API for mutating the contained DOM 14 | /// nodes, adhering fairly closely to the upstream web specs. 15 | pub struct CachedNode { 16 | id: topo::CallId, 17 | last_child: Cell>, 18 | node: Node, 19 | } 20 | 21 | impl CachedNode { 22 | #[topo::nested] 23 | pub(crate) fn new(node: Node) -> Self { 24 | Self { node, last_child: Cell::new(None), id: topo::CallId::current() } 25 | } 26 | 27 | pub(crate) fn raw_node(&self) -> &Node { 28 | &self.node 29 | } 30 | 31 | // TODO accept PartialEq+ToString implementors 32 | #[topo::nested(slot = "&(self.id, name)")] 33 | pub(crate) fn set_attribute(&self, name: &'static str, value: &str) { 34 | let mut should_set = false; 35 | cache_with( 36 | value, 37 | |_| { 38 | // when this isn't the first time the attribute is being set for this element, 39 | // this closure executes while the previous attribute's guard is still live. 40 | // if we actually set the attribute here, it will be removed when this closure exits 41 | // which we definitely don't want. easiest fix is to set the attribute after our 42 | // hypothetical cleanup has completed 43 | should_set = true; 44 | let name = name.to_owned(); 45 | // TODO find a way to reuse the guard if we're replacing a previous value 46 | scopeguard::guard(self.node.clone(), move |node| { 47 | node.remove_attribute(&name); 48 | }) 49 | }, 50 | |_| {}, 51 | ); 52 | 53 | if should_set { 54 | self.node.set_attribute(name, value); 55 | } 56 | } 57 | 58 | pub(crate) fn ensure_child_attached(&self, new_child: &Node) { 59 | let prev_sibling = self.last_child.replace(Some(new_child.clone())); 60 | 61 | let existing = if prev_sibling.is_none() { 62 | self.node.first_child() 63 | } else { 64 | prev_sibling.and_then(|p| p.next_sibling()) 65 | }; 66 | 67 | if let Some(ref existing) = existing { 68 | if existing != new_child { 69 | self.node.replace_child(new_child, existing); 70 | } 71 | } else { 72 | self.node.append_child(new_child); 73 | } 74 | } 75 | 76 | pub(crate) fn remove_trailing_children(&self) { 77 | let last_desired_child = self.last_child.replace(None); 78 | 79 | // if there weren't any children declared this revision, we need to 80 | // make sure we clean up any from the last revision 81 | let mut next_to_remove = if let Some(c) = last_desired_child { 82 | // put back the last node we found this revision so this can be called multiple 83 | // times 84 | self.last_child.set(Some(c.clone())); 85 | c.next_sibling() 86 | } else { 87 | self.node.first_child() 88 | }; 89 | 90 | while let Some(to_remove) = next_to_remove { 91 | next_to_remove = to_remove.next_sibling(); 92 | self.node.remove_child(&to_remove).unwrap(); 93 | } 94 | } 95 | } 96 | 97 | impl Debug for CachedNode { 98 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 99 | f.debug_struct("CachedNode").field("node", &self.node).finish() 100 | } 101 | } 102 | 103 | #[cfg(test)] 104 | mod tests { 105 | use crate::{elements::just_all_of_it_ok::div, prelude::*}; 106 | use mox::mox; 107 | use moxie::{runtime::RunLoop, state}; 108 | 109 | #[wasm_bindgen_test::wasm_bindgen_test] 110 | pub fn attributes_change() { 111 | let mut rt = RunLoop::new(|| { 112 | let (value, key) = state(|| String::from("boo")); 113 | (key, mox!(
    )) 114 | }); 115 | let (key, node) = rt.run_once(); 116 | assert_eq!( 117 | node.raw_node_that_has_sharp_edges_please_be_careful().to_string(), 118 | "
    \n
    " 119 | ); 120 | 121 | key.set(String::from("aha")); 122 | let (_, node) = rt.run_once(); 123 | assert_eq!( 124 | node.raw_node_that_has_sharp_edges_please_be_careful().to_string(), 125 | "
    \n
    " 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /ofl/src/website.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Error}; 2 | use gumdrop::Options; 3 | use mdbook::MDBook; 4 | use std::{ 5 | fs::File, 6 | io::{BufReader, BufWriter}, 7 | path::{Path, PathBuf}, 8 | sync::Mutex, 9 | }; 10 | use tracing::*; 11 | 12 | #[derive(Debug, Options)] 13 | pub struct Website { 14 | help: bool, 15 | #[options(command)] 16 | op: Option, 17 | } 18 | 19 | impl Website { 20 | pub fn run(self, root_path: PathBuf) -> Result<(), Error> { 21 | let operation = self.op.unwrap_or_else(|| Operation::Build(DistOpts::default(&root_path))); 22 | match operation { 23 | Operation::Build(opts) => opts.build_website_dist(&root_path), 24 | } 25 | } 26 | } 27 | 28 | #[derive(Debug, Options)] 29 | enum Operation { 30 | Build(DistOpts), 31 | } 32 | 33 | #[derive(Debug, Options)] 34 | struct DistOpts { 35 | help: bool, 36 | #[options(free, required)] 37 | output_dir: PathBuf, 38 | } 39 | 40 | impl DistOpts { 41 | fn default(root_path: &Path) -> Self { 42 | Self { help: false, output_dir: root_path.join("target").join("website") } 43 | } 44 | 45 | fn build_website_dist(self, root_path: &Path) -> Result<(), Error> { 46 | let md = MDBook::load(&root_path.join("book")).map_err(SyncFailure::new)?; 47 | md.build().map_err(SyncFailure::new)?; 48 | self.copy_to_target_dir(root_path) 49 | } 50 | 51 | fn copy_to_target_dir(self, root_path: &Path) -> Result<(), Error> { 52 | let _ = std::fs::remove_dir_all(&self.output_dir); 53 | std::fs::create_dir_all(&self.output_dir)?; 54 | let output_path = self.output_dir.canonicalize()?; 55 | 56 | let to_copy = self.files_to_copy(root_path, &output_path)?; 57 | info!({ num_files = to_copy.len() }, "discovered"); 58 | 59 | for path in to_copy { 60 | let relative = path.strip_prefix(root_path)?; 61 | let rel_path = relative.display(); 62 | debug!({ %rel_path }, "copying path"); 63 | let destination = output_path.join(relative); 64 | let parent = destination.parent().unwrap(); 65 | std::fs::create_dir_all(&parent) 66 | .with_context(|| format!("creating {}", parent.display()))?; 67 | 68 | // note: can't use std::fs::copy here because it generates filesystem notifs 69 | // see https://github.com/notify-rs/notify/issues/259 70 | let mut src = BufReader::new( 71 | File::open(&path).with_context(|| format!("opening {}", path.display()))?, 72 | ); 73 | let mut dst = BufWriter::new( 74 | File::create(&destination) 75 | .with_context(|| format!("creating {}", destination.display()))?, 76 | ); 77 | std::io::copy(&mut src, &mut dst).with_context(|| { 78 | format!("copying {} to {}", path.display(), destination.display()) 79 | })?; 80 | } 81 | 82 | Ok(()) 83 | } 84 | 85 | fn files_to_copy(&self, root_path: &Path, output_path: &Path) -> Result, Error> { 86 | let skip_prefixes = 87 | vec![output_path.to_path_buf(), root_path.join(".vscode"), root_path.join("ofl")]; 88 | 89 | let exts = 90 | vec!["css", "html", "ico", "js", "json", "map", "png", "svg", "txt", "wasm", "woff"]; 91 | 92 | let output = output_path.display(); 93 | info!({ %output }, "cleaning"); 94 | 95 | info!("discovering files to copy"); 96 | let mut to_copy = vec![]; 97 | for entry in walkdir::WalkDir::new(root_path) { 98 | let path = entry?.path().to_owned(); 99 | 100 | match path.extension() { 101 | Some(ext) if exts.contains(&ext.to_str().unwrap()) => (), 102 | _ => continue, 103 | }; 104 | 105 | for prefix in &skip_prefixes { 106 | if path.starts_with(prefix) { 107 | continue; 108 | } 109 | } 110 | 111 | if path.components().any(|c| c.as_os_str() == "node_modules") { 112 | continue; 113 | } 114 | 115 | to_copy.push(path); 116 | } 117 | Ok(to_copy) 118 | } 119 | } 120 | 121 | #[derive(Debug)] 122 | struct SyncFailure(Mutex); 123 | 124 | impl SyncFailure 125 | where 126 | E: std::error::Error, 127 | { 128 | fn new(e: E) -> Self { 129 | Self(Mutex::new(e)) 130 | } 131 | } 132 | 133 | impl std::fmt::Display for SyncFailure 134 | where 135 | E: std::error::Error, 136 | { 137 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 138 | self.0.lock().unwrap().fmt(f) 139 | } 140 | } 141 | 142 | impl std::error::Error for SyncFailure where E: std::error::Error {} 143 | -------------------------------------------------------------------------------- /topo/src/slot.rs: -------------------------------------------------------------------------------- 1 | use dyn_cache::sync::SendCache; 2 | use once_cell::sync::Lazy; 3 | use parking_lot::Mutex; 4 | use std::{ 5 | any::{type_name, TypeId}, 6 | borrow::Borrow, 7 | collections::HashMap, 8 | fmt::{Debug, Formatter, Result as FmtResult}, 9 | hash::{Hash, Hasher}, 10 | marker::PhantomData, 11 | }; 12 | 13 | static TOKENS: Lazy> = Lazy::new(|| Mutex::new(SendCache::default())); 14 | 15 | /// A unique identifer in the global cache. Each type can have 16 | /// [`std::u32::MAX`] unique values cached. Constructed with [`Token::make`], 17 | /// which will always produce the same value for the same input. 18 | /// 19 | /// # Memory Usage 20 | /// 21 | /// Token inputs are not yet dropped and care should be taken when creating 22 | /// large numbers of them, as the memory used over time will grow with 23 | /// proportion to the number of unique tokens created. 24 | /// 25 | /// See [issue #141](https://github.com/anp/moxie/issues/141) for future work. 26 | /// 27 | /// A typed token can be converted into an [`OpaqueToken`] to allow 28 | /// differentiating between unique values of different types. 29 | pub(crate) struct Slot { 30 | index: u32, 31 | ty: PhantomData, 32 | } 33 | 34 | impl Slot 35 | where 36 | T: Eq + Hash + Send + 'static, 37 | { 38 | /// Makes a unique token from the provided value, interning it in the global 39 | /// cache. Later calls with the same input will return the same token. 40 | pub fn make(value: &Q) -> Slot 41 | where 42 | Q: Eq + Hash + ToOwned + ?Sized, 43 | T: Borrow, 44 | { 45 | static INDICES: Lazy>> = 46 | Lazy::new(|| Mutex::new(HashMap::new())); 47 | let mut existing_tokens = TOKENS.lock(); 48 | 49 | match existing_tokens.get(value, &()) { 50 | Ok(token) => *token, 51 | Err(miss) => { 52 | let (to_store, new_token) = miss.init(|_| { 53 | let mut indices = INDICES.lock(); 54 | let count = indices.entry(TypeId::of::()).or_default(); 55 | *count += 1; 56 | let new_token = Self { index: *count, ty: PhantomData }; 57 | (new_token, new_token) 58 | }); 59 | existing_tokens.store(to_store); 60 | new_token 61 | } 62 | } 63 | } 64 | 65 | /// Fabricate a token. Used for e.g. creating a root `crate::CallId`. 66 | pub(crate) fn fake() -> Self { 67 | Self { index: 0, ty: PhantomData } 68 | } 69 | } 70 | 71 | impl Clone for Slot { 72 | fn clone(&self) -> Self { 73 | Self { index: self.index, ty: PhantomData } 74 | } 75 | } 76 | 77 | impl Copy for Slot {} 78 | 79 | impl Debug for Slot { 80 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 81 | f.debug_struct("Token").field("index", &self.index).field("ty", &type_name::()).finish() 82 | } 83 | } 84 | 85 | impl From<&Q> for Slot 86 | where 87 | Q: Eq + Hash + ToOwned + ?Sized, 88 | T: Borrow + Eq + Hash + Send + 'static, 89 | { 90 | fn from(query: &Q) -> Self { 91 | Slot::make(query) 92 | } 93 | } 94 | 95 | impl Hash for Slot { 96 | fn hash(&self, hasher: &mut H) { 97 | self.index.hash(hasher) 98 | } 99 | } 100 | 101 | impl PartialEq for Slot { 102 | fn eq(&self, other: &Self) -> bool { 103 | self.index == other.index 104 | } 105 | } 106 | impl Eq for Slot {} 107 | 108 | impl PartialOrd for Slot { 109 | fn partial_cmp(&self, other: &Self) -> Option { 110 | self.index.partial_cmp(&other.index) 111 | } 112 | } 113 | impl Ord for Slot { 114 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 115 | self.index.cmp(&other.index) 116 | } 117 | } 118 | 119 | /// A unique type-erased identifier for a cached value. 120 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] 121 | pub(crate) struct OpaqueSlot { 122 | ty: TypeId, 123 | index: u32, 124 | } 125 | 126 | impl From> for OpaqueSlot { 127 | fn from(token: Slot) -> Self { 128 | OpaqueSlot { index: token.index, ty: TypeId::of::() } 129 | } 130 | } 131 | 132 | #[cfg(test)] 133 | mod tests { 134 | use super::*; 135 | 136 | #[test] 137 | fn make_tokens() { 138 | let foo: Slot = Slot::make("foo"); 139 | assert_eq!(foo, Slot::make("foo")); 140 | assert_ne!(foo, Slot::make("bar")); 141 | } 142 | 143 | #[test] 144 | fn make_opaque() { 145 | let first: OpaqueSlot = Slot::make(&10u8).into(); 146 | let second: OpaqueSlot = Slot::make(&10u16).into(); 147 | assert_ne!(first, second); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /topo/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # topo 2 | 3 | The [topo](https://docs.rs/topo) crate provides incremental caching and identifiers for 4 | repeated function invocations. Together with a change notification mechanism it can be used 5 | to implement a form of [incremental computing](https://en.wikipedia.org/wiki/Incremental_computing). 6 | 7 | 8 | 9 | ## [0.13.2] - 2021-02-01 10 | 11 | ### Changed 12 | 13 | - Internal refactors to reduce code size. 14 | 15 | ## [0.13.1] - 2020-12-28 16 | 17 | ### Added 18 | 19 | - `wasm-bindgen` cargo feature which enables correct usage of parking_lot on wasm32 targets. 20 | 21 | ## [0.13.0] - 2020-07-19 22 | 23 | ### Removed 24 | 25 | - `cache` module extracted to the `dyn-cache` crate. 26 | 27 | ## [0.12.0] - 2020-07-06 28 | 29 | ### Changed 30 | 31 | - Return type of `cache::{Cache, LocalCache}::get_if_arg_eq_prev_input` also returns hash of the 32 | query type, and `cache::{Cache, LocalCache}::store` requires it. 33 | 34 | ## [0.11.0] - 2020-07-06 35 | 36 | ### Added 37 | 38 | - `cache::Hashed` holds a query key and its hash for later storage. 39 | 40 | ### Changed 41 | 42 | - `cache::{Cache, LocalCache}::get_if_arg_eq_prev_input` now returns `Err(Hashed)` rather than 43 | `None` when a lookup fails. The `store` function on both types now requires that `Hashed` to be 44 | passed when inserting new input & output. 45 | - Cache contents are GC'd in a single loop, previously there were two iterations over each 46 | namespace. 47 | 48 | ### Fixed 49 | 50 | - This release removes the last "known redundant" work in the cache API. 51 | 52 | ## [0.10.0] - 2020-07-05 53 | 54 | ### Fixed 55 | 56 | - `Id` generation is no longer vulnerable to hashing collisions. 57 | 58 | ### Added 59 | 60 | - #[nested] allows specifying a `slot`. 61 | - `cache::{Cache, GlobalCache}` types for storing interned and memoized values. 62 | - `cache::{SharedCache, SharedGlobalCache}` types for safe multiple-owner access to caches, 63 | implementing `cache_with` with careful locking to allow nested calls in the future. 64 | - `root` free function for allowing one to re-root a call topology (i.e. if running inside of a 65 | broader one). 66 | 67 | ### Removed 68 | 69 | - `Callsite` and `Point` are no longer `pub`. 70 | - `#![feature(track_caller)]` is no longer needed, although until 1.46 hits beta/stable an MSRV of 71 | nightly-2020-07-02 applies. 72 | 73 | ### Changed 74 | 75 | - `call_in_slot` accepts borrowed slots. 76 | - `Id` renamed to `CallId`. 77 | - `illicit` dependency updated to 1.0. 78 | - `impl Trait` has been removed from public APIs where it may cause accidental `Send`/`!Send` 79 | contracts. 80 | 81 | ## [0.9.4] - 2019-12-26 82 | 83 | ### Changed 84 | 85 | - Updated `illicit` dependency to `0.9.0`. 86 | 87 | ## [0.9.3] - 2019-12-25 88 | 89 | ### Changed 90 | 91 | - `#[track_caller]` is used to generate `Id`s, replacing macros. Requires nightly for now. 92 | - Use `DefaultHasher` instead of `FnvHasher`. 93 | 94 | ### Added 95 | 96 | - `call`, `call_in_slot` functions. 97 | 98 | ### Removed 99 | 100 | - `call!` and `unstable_make_topo_macro!` macros. 101 | 102 | ## [0.9.2] - 2019-11-23 103 | 104 | ### Changed 105 | 106 | - Using `fnv` crate for hashing `Id`s. 107 | 108 | ## [0.9.1] - 2019-11-21 109 | 110 | ### Removed 111 | 112 | - `#![warn(intra_doc_resolution_failure)]` was causing docs.rs issues due to root_html_url. 113 | 114 | ## [0.9.0] - 2019-11-19 115 | 116 | ### Added 117 | 118 | - `#![forbid(unsafe_code)]` 119 | - `call!` accepts a "slot" other than the number of times a callsite has been seen. The callsite 120 | count is still the default. 121 | - Invoking `call!` when no `Point` has already been entered will now create a new root and enter it 122 | before executing the block. 123 | 124 | ### Changed 125 | 126 | - Rename `#[bound]` to `#[nested]`. 127 | - Rename `current_callsite_count` to `Callsite::current_count`. 128 | 129 | ### Removed 130 | 131 | - `env!`, `Env`, `#[from_env]` moved to `illicit` crate. 132 | - `root!` removed in favor of creating a new root whenever `call!` is invoked outside of a `Point`. 133 | 134 | ## [0.8.2] - 2019-08-20 135 | 136 | ### Fixed 137 | 138 | - `root!` no longer hides the outer environment from the called block. 139 | 140 | ## [0.8.1] - 2019-08-17 141 | 142 | ### Changed 143 | 144 | - `Id`'s `Debug` impl uses hex. 145 | 146 | ### Fixed 147 | 148 | - Incorrect line endings. 149 | 150 | ## [0.8.0] - 2019-06-23 151 | 152 | ### Added 153 | 154 | - `#[topo::bound]` attaches a function to the topology. 155 | - `root!` and `call!` macros attach arbitrary blocks to a new or the current topology respectively, 156 | entering new `Point`s for each call, each of which has a (mostly) unique `Id`. 157 | - `env!` macro allows declaring type-indexed implicit variables, produces `Env` instances. 158 | 159 | ## [0.1.0] - 2019-05-26 160 | 161 | Published to reserve name on crates.io. 162 | -------------------------------------------------------------------------------- /dom/src/elements.rs: -------------------------------------------------------------------------------- 1 | //! Element definitions generated from the listing on [MDN]. 2 | //! 3 | //! [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element 4 | 5 | /// A module for glob-importing all element creation functions, similar to the 6 | /// global HTML namespace. 7 | pub mod html { 8 | pub use super::{ 9 | body, 10 | embedding::{embed, iframe, object, param, picture, source}, 11 | forms::{ 12 | button, datalist, fieldset, form, input, label, legend, meter, optgroup, option, 13 | output, progress, select, textarea, 14 | }, 15 | html, 16 | interactive::{details, dialog, menu, summary}, 17 | media::{area, audio, img, map, track, video}, 18 | metadata::{base, head, link, meta, style, title}, 19 | scripting::{canvas, noscript, script}, 20 | sectioning::{ 21 | address, article, aside, footer, h1, h2, h3, h4, h5, h6, header, hgroup, main, nav, 22 | section, 23 | }, 24 | table::{caption, col, colgroup, table, tbody, td, tfoot, th, thead, tr}, 25 | text_content::{blockquote, dd, div, dl, dt, figcaption, figure, hr, li, ol, p, pre, ul}, 26 | text_semantics::{ 27 | a, abbr, b, bdi, bdo, br, cite, code, data, del, dfn, em, i, ins, kbd, mark, q, rb, rp, 28 | rt, rtc, ruby, s, samp, small, span, strong, sub, sup, time, u, var, wbr, 29 | }, 30 | }; 31 | } 32 | 33 | pub(crate) mod just_all_of_it_ok { 34 | pub use super::{ 35 | embedding::*, forms::*, interactive::*, media::*, metadata::*, scripting::*, sectioning::*, 36 | table::*, text_content::*, text_semantics::*, *, 37 | }; 38 | } 39 | 40 | pub mod embedding; 41 | pub mod forms; 42 | pub mod interactive; 43 | pub mod media; 44 | pub mod metadata; 45 | pub mod scripting; 46 | pub mod sectioning; 47 | pub mod table; 48 | pub mod text_content; 49 | pub mod text_semantics; 50 | 51 | html_element! { 52 | /// The [`` element][mdn] represents the root (top-level element) of an HTML document, 53 | /// so it is also referred to as the *root element*. All other elements must be descendants of 54 | /// this element. 55 | /// 56 | /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/html 57 | 58 | 59 | children { 60 | tags { 61 | , 62 | } 63 | } 64 | 65 | attributes { 66 | /// Specifies the XML Namespace of the document. Default value is 67 | /// `http://www.w3.org/1999/xhtml`. This is required in documents parsed with XML parsers, 68 | /// and optional in text/html documents. 69 | xmlns 70 | } 71 | } 72 | 73 | html_element! { 74 | /// The [HTML `` element][mdn] represents the content of an HTML document. There can be 75 | /// only one `` element in a document. 76 | /// 77 | /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/body 78 | 79 | 80 | categories { 81 | Sectioning 82 | } 83 | children { 84 | categories { 85 | Flow 86 | } 87 | } 88 | } 89 | 90 | macro_rules! body_events { 91 | ($($property:ident <- $event:ident,)+) => { 92 | $( 93 | impl crate::interfaces::event_target::EventTarget for BodyBuilder 94 | {} 95 | )+ 96 | 97 | impl BodyBuilder {$( 98 | /// Set an event handler. 99 | pub fn $property( 100 | self, 101 | callback: impl FnMut(augdom::event::$event) + 'static, 102 | ) -> Self { 103 | use crate::interfaces::event_target::EventTarget; 104 | self.on(callback) 105 | } 106 | )+} 107 | }; 108 | } 109 | 110 | body_events! { 111 | onafterprint <- AfterPrint, 112 | onbeforeprint <- BeforePrint, 113 | onhashchange <- HashChange, 114 | onmessage <- WebsocketMessage, 115 | onoffline <- Offline, 116 | ononline <- Online, 117 | onstorage <- Storage, 118 | onunload <- Unload, 119 | } 120 | 121 | html_element! { 122 | /// The [HTML `` element][mdn]—part of the [Web Components][wc] technology suite—is a 123 | /// placeholder inside a web component that you can fill with your own markup, which lets you 124 | /// create separate DOM trees and present them together. 125 | /// 126 | /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot 127 | /// [wc]: https://developer.mozilla.org/en-US/docs/Web/Web_Components 128 | 129 | 130 | categories { 131 | Flow, Phrasing 132 | } 133 | 134 | attributes { 135 | /// The slot's name. 136 | name 137 | } 138 | } 139 | 140 | html_element! { 141 | /// The [HTML Content Template (`