Repository: rail44/squark Branch: master Commit: 3658acd25258 Files: 47 Total size: 62.5 KB Directory structure: gitextract_c8b3y0t7/ ├── .gitignore ├── Cargo.toml ├── LICENCE ├── README.md ├── examples/ │ ├── counter/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── crate/ │ │ │ ├── .crates.toml │ │ │ ├── .gitignore │ │ │ ├── Cargo.toml │ │ │ └── src/ │ │ │ └── lib.rs │ │ ├── index.html │ │ ├── js/ │ │ │ └── index.js │ │ ├── package.json │ │ └── webpack.config.js │ ├── todomvc/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── crate/ │ │ │ ├── .crates.toml │ │ │ ├── .gitignore │ │ │ ├── Cargo.toml │ │ │ └── src/ │ │ │ └── lib.rs │ │ ├── index.html │ │ ├── js/ │ │ │ └── index.js │ │ ├── package.json │ │ ├── styles.css │ │ └── webpack.config.js │ └── with_task/ │ ├── .gitignore │ ├── README.md │ ├── crate/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ └── src/ │ │ └── lib.rs │ ├── index.html │ ├── js/ │ │ └── index.js │ ├── package.json │ └── webpack.config.js ├── renovate.json ├── squark/ │ ├── .gitignore │ ├── Cargo.toml │ └── src/ │ ├── lib.rs │ └── vdom.rs ├── squark-macros/ │ ├── .gitignore │ ├── Cargo.toml │ ├── src/ │ │ ├── lib.rs │ │ └── view.pest │ └── tests/ │ └── test.rs └── squark-web/ ├── .gitignore ├── Cargo.toml └── src/ └── lib.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /target /Cargo.lock ================================================ FILE: Cargo.toml ================================================ [workspace] members = [ "squark", "squark-macros", "squark-web", ] ================================================ FILE: LICENCE ================================================ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE Version 2, December 2004 Copyright (C) 2004 Sam Hocevar Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. You just DO WHAT THE FUCK YOU WANT TO. ================================================ FILE: README.md ================================================ # squark Rust frontend framework, for web browser and more. **Currently, we depend on `nightly` channel** ## Design * Separating runtime definition and implemention + `squark` crate has no dependency for specific platform * Architecture inspired from [Elm](https://elm-lang.org/) and [HyperApp](https://github.com/hyperapp/hyperapp/) + Simplicy + Elegant * Supporting futures-0.1 + reducer can emit task for async work such as fetch resource ## crates ### squark [![crates.io](https://img.shields.io/crates/v/squark.svg)](https://crates.io/crates/squark) [![docs.rs](https://docs.rs/squark/badge.svg)](https://docs.rs/squark/*/squark/) Core crate. * Pure Rust virtual DOM implemention * Definition of GUI application * Definition of runtime to handle diffirence of virtual DOM ### squark-macros [![crates.io](https://img.shields.io/crates/v/squark-macros.svg)](https://crates.io/crates/squark-macros) [![docs.rs](https://docs.rs/squark-macros/badge.svg)](https://docs.rs/squark-macros/*/squark_macros/) It provides macro like JSX for helping writing view. Very thanks to [pest](https://github.com/pest-parser/pest) parser. #### Syntax ``` view! { } ``` We can generate native Rust expression at compile-time. ### squark-web [![crates.io](https://img.shields.io/crates/v/squark-web.svg)](https://crates.io/crates/squark-web) [![docs.rs](https://docs.rs/squark-web/badge.svg)](https://docs.rs/squark-web/*/squark_web/) Runtime implemention for web browser with usinng [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen/). Here is full example of counter app! ```rust #![feature(proc_macro_hygiene)] extern crate squark; extern crate squark_macros; extern crate squark_web; extern crate wasm_bindgen; extern crate web_sys; use squark::{App, Runtime, View, Task}; use squark_macros::view; use squark_web::WebRuntime; use wasm_bindgen::prelude::*; use web_sys::window; #[derive(Clone, Debug, PartialEq)] struct State { count: isize, } impl State { pub fn new() -> State { State { count: 0 } } } #[derive(Clone, Debug)] enum Action { ChangeCount(isize), } #[derive(Clone, Debug)] struct CounterApp; impl App for CounterApp { type State = State; type Action = Action; fn reducer(&self, mut state: State, action: Action) -> (State, Task) { match action { Action::ChangeCount(c) => { state.count = c; } }; (state, Task::empty()) } fn view(&self, state: State) -> View { let count = state.count; view! {
{ count.to_string() }
} } } impl Default for CounterApp { fn default() -> CounterApp { CounterApp } } #[wasm_bindgen] pub fn run() { WebRuntime::::new( window() .unwrap() .document() .expect("Failed to get document") .query_selector("body") .unwrap() .unwrap(), State::new(), ) .run(); } ``` Project dir is located at [examples/counter](./examples/counter). There are some other examples available on [examples](./examples), most of them use [rust-webpack-template](https://github.com/rustwasm/rust-webpack-template). TodoMVC is working on [https://rail44.github.io/squark/](https://rail44.github.io/squark/). ================================================ FILE: examples/counter/.gitignore ================================================ dist node_modules crate/pkg crate/target crate/wasm-pack.log ================================================ FILE: examples/counter/README.md ================================================ Just run ```bash npm run start ``` dev server will come with hot reloading. ================================================ FILE: examples/counter/crate/.crates.toml ================================================ [v1] "wasm-bindgen-cli 0.2.19 (registry+https://github.com/rust-lang/crates.io-index)" = ["wasm-bindgen", "wasm-bindgen-test-runner", "wasm2es6js"] ================================================ FILE: examples/counter/crate/.gitignore ================================================ /target/ **/*.rs.bk Cargo.lock ================================================ FILE: examples/counter/crate/Cargo.toml ================================================ [package] name = "counter" version = "0.1.0" authors = ["Satoshi Amemiya "] [workspace] [lib] crate-type = ["cdylib"] [dependencies] squark = { "path" = "../../../squark" } squark-macros = { "path" = "../../../squark-macros" } squark-web = { "path" = "../../../squark-web" } serde_json = "1.0.13" wasm-bindgen = "0.2.19" futures = "0.1.25" js-sys = "0.3.6" wasm-bindgen-futures = "0.3.6" [dependencies.web-sys] version = "0.3.2" features = [ 'Document', 'Window', ] ================================================ FILE: examples/counter/crate/src/lib.rs ================================================ #![feature(proc_macro_hygiene)] extern crate squark; extern crate squark_macros; extern crate squark_web; extern crate wasm_bindgen; extern crate web_sys; use squark::{App, Runtime, View, Task}; use squark_macros::view; use squark_web::WebRuntime; use wasm_bindgen::prelude::*; use web_sys::window; #[derive(Clone, Debug, PartialEq)] struct State { count: isize, } impl State { pub fn new() -> State { State { count: 0 } } } #[derive(Clone, Debug)] enum Action { ChangeCount(isize), } #[derive(Clone, Debug)] struct CounterApp; impl App for CounterApp { type State = State; type Action = Action; fn reducer(&self, mut state: State, action: Action) -> (State, Task) { match action { Action::ChangeCount(c) => { state.count = c; } }; (state, Task::empty()) } fn view(&self, state: State) -> View { let count = state.count; view! {
{ count.to_string() }
} } } impl Default for CounterApp { fn default() -> CounterApp { CounterApp } } #[wasm_bindgen] pub fn run() { WebRuntime::::new( window() .unwrap() .document() .expect("Failed to get document") .query_selector("body") .unwrap() .unwrap(), State::new(), ) .run(); } ================================================ FILE: examples/counter/index.html ================================================ ================================================ FILE: examples/counter/js/index.js ================================================ import("../crate/pkg").then(module => { module.run(); }); ================================================ FILE: examples/counter/package.json ================================================ { "scripts": { "start": "webpack-dev-server -d", "build": "webpack --mode none" }, "devDependencies": { "@wasm-tool/wasm-pack-plugin": "^0.2.0", "webpack": "^4.28.1", "webpack-cli": "^3.2.1", "webpack-dev-server": "^3.1.14" } } ================================================ FILE: examples/counter/webpack.config.js ================================================ const path = require("path"); const dist = path.resolve(__dirname, "dist"); const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); module.exports = { entry: "./js/index.js", output: { publicPath: "dist/", path: dist, filename: "bundle.js" }, plugins: [ new WasmPackPlugin({ crateDirectory: path.resolve(__dirname, "crate"), }), ], devServer: { host: '0.0.0.0', }, }; ================================================ FILE: examples/todomvc/.gitignore ================================================ dist node_modules crate/pkg crate/target crate/wasm-pack.log ================================================ FILE: examples/todomvc/README.md ================================================ Just run ```bash npm run start ``` dev server will come with hot reloading. ================================================ FILE: examples/todomvc/crate/.crates.toml ================================================ [v1] "wasm-bindgen-cli 0.2.19 (registry+https://github.com/rust-lang/crates.io-index)" = ["wasm-bindgen", "wasm-bindgen-test-runner", "wasm2es6js"] ================================================ FILE: examples/todomvc/crate/.gitignore ================================================ /target/ **/*.rs.bk Cargo.lock ================================================ FILE: examples/todomvc/crate/Cargo.toml ================================================ [package] name = "todomvc" version = "0.1.0" authors = ["Satoshi Amemiya "] [workspace] [lib] crate-type = ["cdylib"] [dependencies] squark = { "path" = "../../../squark" } squark-macros = { "path" = "../../../squark-macros" } squark-web = { "path" = "../../../squark-web" } serde_json = "1.0.13" wasm-bindgen = "0.2.29" [dependencies.web-sys] version = "0.3.2" features = [ 'Document', 'Window', ] ================================================ FILE: examples/todomvc/crate/src/lib.rs ================================================ #![feature(proc_macro_hygiene)] extern crate serde_json; extern crate squark; extern crate squark_macros; extern crate squark_web; extern crate wasm_bindgen; extern crate web_sys; use squark::{uuid, App, Child, HandlerArg, Runtime, View, Task}; use squark_macros::view; use squark_web::WebRuntime; use std::iter::FromIterator; use wasm_bindgen::prelude::*; use web_sys::window; #[derive(Clone, Hash, Debug, PartialEq)] enum Visibility { All, Active, Completed, } impl ToString for Visibility { fn to_string(&self) -> String { match self { Visibility::All => "All".to_string(), Visibility::Active => "Active".to_string(), Visibility::Completed => "Completed".to_string(), } } } impl Visibility { pub fn view(&self, selected: bool) -> View { let this = self.clone(); let class = if selected { "selected" } else { "" }; view! {
  • { self.to_string() }
  • } } } #[derive(Clone, Debug, PartialEq)] struct Entry { id: String, description: String, completed: bool, } impl Entry { pub fn new(description: String) -> Entry { Entry { id: uuid(), description, completed: false, } } pub fn should_display(&self, visibility: &Visibility) -> bool { match visibility { Visibility::All => true, Visibility::Active => !self.completed, Visibility::Completed => self.completed, } } pub fn view(&self, i: usize, editing: bool) -> View { let completed = self.completed; let mut class = vec![]; if completed { class.push("completed"); } if editing { class.push("editing"); } view! {
  • { if editing { let id = format!("edit-{}", i); view! { Some(Action::UpdateEntry(v)), _ => None, } } onkeydown={ |v| match v { HandlerArg::String(ref v) if v.as_str() == "Enter" => { Some(Action::EndEditing) } _ => None, } } onblur={ move |_| Some(Action::EndEditing) } /> } } else { view! {
    } } }
  • } } } #[derive(Clone, Debug, PartialEq)] struct State { entries: Vec, field: String, editing: Option, visibility: Visibility, } impl State { pub fn new() -> State { State { entries: vec![], field: "".to_string(), editing: None, visibility: Visibility::All, } } pub fn has_completed(&self) -> bool { self.entries.iter().any(|e| e.completed) } pub fn not_completed_count(&self) -> usize { self.entries.len() - self.completed_count() } pub fn completed_count(&self) -> usize { self.entries.iter().filter(|e| e.completed).count() } pub fn is_all_completed(&self) -> bool { self.completed_count() == self.entries.len() } } #[derive(Clone, Debug)] enum Action { UpdateField(String), EditEntry(usize), UpdateEntry(String), EndEditing, Add, Remove(usize), RemoveComplete, Check(usize, bool), CheckAll(bool), ChangeVisibility(Visibility), } fn header_view(state: &State) -> View { view! {

    todos

    Some(Action::UpdateField(v)), _ => None, } } onkeydown={ |v| match v { HandlerArg::String(ref v) if v.as_str() == "Enter" => Some(Action::Add), _ => None, } } />
    } } fn main_view(state: &State) -> View { let is_all_completed = state.is_all_completed(); view! {
    { if state.entries.len() > 0 { view! { } } else { ().into() } }
      { Child::from_iter(state .entries .iter() .enumerate() .filter(|&(_, e)| e.should_display(&state.visibility)) .map(|(i, e)| { let is_editing = state.editing.as_ref().map_or(false, |at| &i == at); e.view(i, is_editing) })) }
    } } fn footer_view(state: &State) -> View { if state.entries.is_empty() { return ().into(); } view! {
    { state.not_completed_count().to_string() } item(s) left
      { Child::from_iter( vec![Visibility::All, Visibility::Active, Visibility::Completed] .into_iter() .map(|v| v.view(v == state.visibility)) ) }
    { if state.has_completed() { view! { } } else { ().into() } }
    } } #[derive(Clone, Debug)] struct TodoApp; impl App for TodoApp { type State = State; type Action = Action; fn reducer(&self, mut state: State, action: Action) -> (State, Task) { match action { Action::Add => { if state.field.as_str() != "" { let entry = Entry::new(state.field); state.entries.push(entry); state.field = "".to_string(); } } Action::UpdateField(s) => { state.field = s; } Action::EndEditing => { if let Some(i) = state.editing { if state.entries[i].description.as_str() == "" { state.entries.remove(i); } state.editing = None; } } Action::UpdateEntry(s) => { if let Some(i) = state.editing { state.entries[i].description = s; } } Action::CheckAll(b) => { for mut entry in &mut state.entries { entry.completed = b; } } Action::Check(at, b) => { state.entries[at].completed = b; } Action::Remove(at) => { state.entries.remove(at); } Action::RemoveComplete => { let entries = state.entries.drain(..).filter(|e| !e.completed).collect(); state.entries = entries; } Action::EditEntry(i) => { state.editing = Some(i); } Action::ChangeVisibility(v) => { state.visibility = v; } }; (state, Task::empty()) } fn view(&self, state: State) -> View { view! {
    { header_view(&state) } { main_view(&state) } { footer_view(&state) }
    } } } impl Default for TodoApp { fn default() -> TodoApp { TodoApp } } #[wasm_bindgen] pub fn run() { WebRuntime::::new( window() .unwrap() .document() .unwrap() .query_selector("#container") .unwrap() .unwrap(), State::new(), ) .run(); } ================================================ FILE: examples/todomvc/index.html ================================================ squark-web • TodoMVC
    ================================================ FILE: examples/todomvc/js/index.js ================================================ import("../crate/pkg").then(module => { module.run(); }); ================================================ FILE: examples/todomvc/package.json ================================================ { "scripts": { "start": "webpack-dev-server -d", "build": "webpack --mode none" }, "devDependencies": { "@wasm-tool/wasm-pack-plugin": "^0.2.0", "webpack": "^4.28.1", "webpack-cli": "^3.2.1", "webpack-dev-server": "^3.1.14" } } ================================================ FILE: examples/todomvc/styles.css ================================================ /* Source: https://github.com/evancz/elm-todomvc/blob/master/style.css */ html, body { margin: 0; padding: 0; } .todomvc-wrapper { visibility: visible !important; } button { margin: 0; padding: 0; border: 0; background: none; font-size: 100%; vertical-align: baseline; font-family: inherit; font-weight: inherit; color: inherit; -webkit-appearance: none; appearance: none; -webkit-font-smoothing: antialiased; -moz-font-smoothing: antialiased; font-smoothing: antialiased; } body { font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.4em; background: #f5f5f5; color: #4d4d4d; min-width: 230px; max-width: 550px; margin: 0 auto; -webkit-font-smoothing: antialiased; -moz-font-smoothing: antialiased; font-smoothing: antialiased; font-weight: 300; } button, input[type="checkbox"] { outline: none; } .hidden { display: none; } .todoapp { background: #fff; margin: 130px 0 40px 0; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); } .todoapp input::-webkit-input-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp input::-moz-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp input::input-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp h1 { position: absolute; top: -155px; width: 100%; font-size: 100px; font-weight: 100; text-align: center; color: rgba(175, 47, 47, 0.15); -webkit-text-rendering: optimizeLegibility; -moz-text-rendering: optimizeLegibility; text-rendering: optimizeLegibility; } .new-todo, .edit { position: relative; margin: 0; width: 100%; font-size: 24px; font-family: inherit; font-weight: inherit; line-height: 1.4em; border: 0; outline: none; color: inherit; padding: 6px; border: 1px solid #999; box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); box-sizing: border-box; -webkit-font-smoothing: antialiased; -moz-font-smoothing: antialiased; font-smoothing: antialiased; } .new-todo { padding: 16px 16px 16px 60px; border: none; background: rgba(0, 0, 0, 0.003); box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); } .main { position: relative; z-index: 2; border-top: 1px solid #e6e6e6; } label[for='toggle-all'] { display: none; } .toggle-all { position: absolute; top: -55px; left: -12px; width: 60px; height: 34px; text-align: center; border: none; /* Mobile Safari */ } .toggle-all:before { content: '❯'; font-size: 22px; color: #e6e6e6; padding: 10px 27px 10px 27px; } .toggle-all:checked:before { color: #737373; } .todo-list { margin: 0; padding: 0; list-style: none; } .todo-list li { position: relative; font-size: 24px; border-bottom: 1px solid #ededed; } .todo-list li:last-child { border-bottom: none; } .todo-list li.editing { border-bottom: none; padding: 0; } .todo-list li.editing .edit { display: block; width: 506px; padding: 13px 17px 12px 17px; margin: 0 0 0 43px; } .todo-list li.editing .view { display: none; } .todo-list li .toggle { text-align: center; width: 40px; /* auto, since non-WebKit browsers doesn't support input styling */ height: auto; position: absolute; top: 0; bottom: 0; margin: auto 0; border: none; /* Mobile Safari */ -webkit-appearance: none; appearance: none; } .todo-list li .toggle:after { content: url('data:image/svg+xml;utf8,'); } .todo-list li .toggle:checked:after { content: url('data:image/svg+xml;utf8,'); } .todo-list li label { white-space: pre-line; word-break: break-all; padding: 15px 60px 15px 15px; margin-left: 45px; display: block; line-height: 1.2; transition: color 0.4s; } .todo-list li.completed label { color: #d9d9d9; text-decoration: line-through; } .todo-list li .destroy { display: none; position: absolute; top: 0; right: 10px; bottom: 0; width: 40px; height: 40px; margin: auto 0; font-size: 30px; color: #cc9a9a; margin-bottom: 11px; transition: color 0.2s ease-out; } .todo-list li .destroy:hover { color: #af5b5e; } .todo-list li .destroy:after { content: '×'; } .todo-list li:hover .destroy { display: block; } .todo-list li .edit { display: none; } .todo-list li.editing:last-child { margin-bottom: -1px; } .footer { color: #777; padding: 10px 15px; height: 20px; text-align: center; border-top: 1px solid #e6e6e6; } .footer:before { content: ''; position: absolute; right: 0; bottom: 0; left: 0; height: 50px; overflow: hidden; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); } .todo-count { float: left; text-align: left; } .todo-count strong { font-weight: 300; } .filters { margin: 0; padding: 0; list-style: none; position: absolute; right: 0; left: 0; } .filters li { display: inline; } .filters li a { color: inherit; margin: 3px; padding: 3px 7px; text-decoration: none; border: 1px solid transparent; border-radius: 3px; } .filters li a.selected, .filters li a:hover { border-color: rgba(175, 47, 47, 0.1); } .filters li a.selected { border-color: rgba(175, 47, 47, 0.2); } .clear-completed, html .clear-completed:active { float: right; position: relative; line-height: 20px; text-decoration: none; cursor: pointer; position: relative; } .clear-completed:hover { text-decoration: underline; } .info { margin: 65px auto 0; color: #bfbfbf; font-size: 10px; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); text-align: center; } .info p { line-height: 1; } .info a { color: inherit; text-decoration: none; font-weight: 400; } .info a:hover { text-decoration: underline; } /* Hack to remove background from Mobile Safari. Can't use it globally since it destroys checkboxes in Firefox */ @media screen and (-webkit-min-device-pixel-ratio:0) { .toggle-all, .todo-list li .toggle { background: none; } .todo-list li .toggle { height: 40px; } .toggle-all { -webkit-transform: rotate(90deg); transform: rotate(90deg); -webkit-appearance: none; appearance: none; } } @media (max-width: 430px) { .footer { height: 50px; } .filters { bottom: 10px; } } ================================================ FILE: examples/todomvc/webpack.config.js ================================================ const path = require("path"); const dist = path.resolve(__dirname, "dist"); const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); module.exports = { entry: "./js/index.js", output: { publicPath: "dist/", path: dist, filename: "bundle.js" }, plugins: [ new WasmPackPlugin({ crateDirectory: path.resolve(__dirname, "crate"), }), ], devServer: { host: '0.0.0.0', }, }; ================================================ FILE: examples/with_task/.gitignore ================================================ dist node_modules crate/pkg crate/target crate/wasm-pack.log ================================================ FILE: examples/with_task/README.md ================================================ Just run ```bash npm run start ``` dev server will come with hot reloading. ================================================ FILE: examples/with_task/crate/.gitignore ================================================ /target/ **/*.rs.bk Cargo.lock ================================================ FILE: examples/with_task/crate/Cargo.toml ================================================ [package] name = "with_task" version = "0.1.0" authors = ["Satoshi Amemiya "] [workspace] [lib] crate-type = ["cdylib"] [dependencies] squark = { "path" = "../../../squark" } squark-macros = { "path" = "../../../squark-macros" } squark-web = { "path" = "../../../squark-web" } serde_json = "1.0.13" wasm-bindgen = "0.2.19" futures = "0.1.25" js-sys = "0.3.6" wasm-bindgen-futures = "0.3.6" [dependencies.web-sys] version = "0.3.2" features = [ 'Document', 'Window', ] ================================================ FILE: examples/with_task/crate/src/lib.rs ================================================ #![feature(proc_macro_hygiene)] extern crate squark; extern crate squark_macros; extern crate squark_web; extern crate wasm_bindgen; extern crate wasm_bindgen_futures; extern crate web_sys; extern crate js_sys; extern crate futures; use squark::{App, Runtime, View, Task}; use squark_macros::view; use squark_web::WebRuntime; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; use web_sys::window; use js_sys::Promise; use futures::prelude::*; #[derive(Clone, Debug, PartialEq)] struct State { count: isize, } impl State { pub fn new() -> State { State { count: 0 } } } #[derive(Clone, Debug)] enum Action { Increment, Decrement, Timeout, } #[derive(Clone, Debug)] struct CounterApp; impl App for CounterApp { type State = State; type Action = Action; fn reducer(&self, mut state: State, action: Action) -> (State, Task) { let mut task = Task::empty(); match action { Action::Increment => { state.count += 1; } Action::Decrement => { state.count -= 1; } Action::Timeout => { let p = Promise::new(&mut move |resolve, _| { let closure = Closure::wrap(Box::new(move |_: JsValue| { resolve.call0(&JsValue::null()).unwrap(); }) as Box); window().unwrap().set_timeout_with_callback_and_timeout_and_arguments_0(closure.as_ref().unchecked_ref(), 1000).unwrap(); closure.forget(); }); let future = JsFuture::from(p) .map(move |_| { Action::Increment }) .map_err(|e| panic!("delay errored; err={:?}", e)); task.push(Box::new(future)); } }; (state, task) } fn view(&self, state: State) -> View { let count = state.count; view! {
    { count.to_string() }
    } } } impl Default for CounterApp { fn default() -> CounterApp { CounterApp } } #[wasm_bindgen] pub fn run() { WebRuntime::::new( window() .unwrap() .document() .expect("Failed to get document") .query_selector("body") .unwrap() .unwrap(), State::new(), ) .run(); } ================================================ FILE: examples/with_task/index.html ================================================ ================================================ FILE: examples/with_task/js/index.js ================================================ import("../crate/pkg").then(module => { module.run(); }); ================================================ FILE: examples/with_task/package.json ================================================ { "scripts": { "start": "webpack-dev-server -d", "build": "webpack --mode none" }, "devDependencies": { "@wasm-tool/wasm-pack-plugin": "^0.2.0", "webpack": "^4.28.1", "webpack-cli": "^3.2.1", "webpack-dev-server": "^3.1.14" } } ================================================ FILE: examples/with_task/webpack.config.js ================================================ const path = require("path"); const dist = path.resolve(__dirname, "dist"); const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); module.exports = { entry: "./js/index.js", output: { publicPath: "dist/", path: dist, filename: "bundle.js" }, plugins: [ new WasmPackPlugin({ crateDirectory: path.resolve(__dirname, "crate"), }), ], devServer: { host: '0.0.0.0', }, }; ================================================ FILE: renovate.json ================================================ { "extends": [ "config:base" ] } ================================================ FILE: squark/.gitignore ================================================ /target/ **/*.rs.bk Cargo.lock ================================================ FILE: squark/Cargo.toml ================================================ [package] name = "squark" version = "0.7.1" authors = ["Satoshi Amemiya "] repository = "https://github.com/rail44/squark" homepage = "https://github.com/rail44/squark" license = "WTFPL" readme = "README.md" keywords = ["web", "asmjs", "webasm", "javascript"] categories = ["gui", "web-programming"] description = "Virtual DOM implemention and application definition inspired from HyperApp" edition = "2018" [dependencies] uuid = "0.7.4" serde_json = "1.0.41" rand = { version = "0.7.2", features = [ "wasm-bindgen" ] } rustc-hash = "1.0.1" futures = "0.1.29" serde = "1.0.101" ================================================ FILE: squark/src/lib.rs ================================================ use rand::prelude::*; use std::cell::{Cell, RefCell}; use std::fmt::Debug; use rustc_hash::FxHashMap; use std::rc::Rc; use futures::Future; use serde::Serialize; mod vdom; pub use crate::vdom::{Node, Element, Diff, View, HandlerArg, AttributeValue, Child}; use crate::vdom::{HandlerFunction, HandlerMap}; thread_local! { static RNG: RefCell = RefCell::new(SmallRng::from_entropy()); } pub trait App: 'static + Clone + Default { type State: Clone + Debug + PartialEq + 'static; type Action: Clone + Debug + 'static; fn reducer(&self, state: Self::State, action: Self::Action) -> (Self::State, Task); fn view(&self, state: Self::State) -> View; } pub fn handler(f: F) -> (String, HandlerFunction) where F: Fn(HandlerArg) -> Option + 'static, { (uuid(), Box::new(f)) } #[derive(Clone)] pub struct Env { app: A, state: Rc>, node: Rc>, handler_map: Rc>>, scheduled: Rc>, } impl Env { pub fn new(state: A::State) -> Env { Env { app: A::default(), state: Rc::new(RefCell::new(state)), node: Rc::new(RefCell::new(Node::Null)), handler_map: Rc::new(RefCell::new(FxHashMap::default())), scheduled: Rc::new(Cell::new(false)), } } fn get_state(&self) -> A::State { self.state.borrow().to_owned() } fn set_state(&self, state: A::State) { *self.state.borrow_mut() = state; } fn get_node(&self) -> Node { self.node.borrow().to_owned() } fn set_node(&self, node: Node) { *self.node.borrow_mut() = node; } fn pop_handler(&self, id: &str) -> Option> { self.handler_map.borrow_mut().remove(id) } } pub struct Task(Vec>>); impl Default for Task { fn default() -> Self { Task(vec![]) } } impl Task { pub fn empty() -> Self { Self::default() } pub fn into_futures(self) -> Vec>> { self.0 } pub fn push(&mut self, future: Box>) { self.0.push(future); } } pub trait Runtime: Clone + 'static { fn get_env<'a>(&'a self) -> &'a Env; fn handle_diff(&self, diff: Diff); fn handle_future(&self, future: Box>); fn schedule_render(&self); fn run(&self) { self.run_with_task(Task::empty()); } fn run_with_task(&self, task: Task) { for future in task.into_futures() { self.emit_future(future); } let env = self.get_env(); env.scheduled.set(false); let mut old_node = env.get_node(); let view = env.app.view(env.get_state()); *env.handler_map.borrow_mut() = view.handler_map; if let Some(diff) = Node::diff(&mut old_node, &view.node, &mut 0) { env.set_node(view.node); self.handle_diff(diff); } } fn on_action(&self, action: A::Action) { let env = self.get_env(); let old_state = env.get_state(); let (new_state, task) = env.app.reducer(old_state.to_owned(), action); for future in task.into_futures() { self.emit_future(future); } self.set_state(new_state); } fn set_state(&self, new_state: A::State) { let env = self.get_env(); let old_state = env.get_state(); if old_state == new_state { return; } env.set_state(new_state); if env.scheduled.get() { return; } env.scheduled.set(true); self.schedule_render(); } fn emit_future(&self, task: Box>) { let this = self.clone(); self.handle_future(Box::new(task.map(move |a| { this.on_action(a); }))); } fn pop_handler(&self, id: &str) -> Option> { let env = self.get_env(); let handler = env.pop_handler(id)?; let this = self.to_owned(); let f = move |arg: HandlerArg| { match handler(arg) { Some(a) => this.on_action(a), None => return, }; }; Some(Box::new(f)) } } pub fn uuid() -> String { RNG.with(|rng| uuid::Builder::from_bytes(rng.borrow_mut().gen())) .set_variant(uuid::Variant::RFC4122) .set_version(uuid::Version::Random) .build() .to_string() } ================================================ FILE: squark/src/vdom.rs ================================================ use rustc_hash::{FxHashMap, FxHashSet}; use std::iter::FromIterator; pub use serde_json::Value as HandlerArg; type Attribute = (String, AttributeValue); fn diff_attributes(a: &mut Vec, b: &[Attribute]) -> Vec { let mut result = vec![]; let mut old_map = FxHashMap::::from_iter(a.drain(..)); for &(ref new_key, ref new_val) in b { match old_map.remove(new_key) { Some(old_val) => { if &old_val != new_val { result.push(Diff::SetAttribute(new_key.to_owned(), new_val.to_owned())) } } None => result.push(Diff::SetAttribute(new_key.to_owned(), new_val.to_owned())), } } for (old_key, _) in old_map.drain() { result.push(Diff::RemoveAttribute(old_key)); } result } pub(crate) type HandlerFunction = Box Option>; type Handler = (String, String); fn diff_handlers(a: &mut Vec, b: &[Handler]) -> Vec { let mut result = vec![]; let mut old_map = FxHashMap::::from_iter(a.drain(..)); for &(ref new_key, ref new_id) in b { old_map.remove(new_key); result.push(Diff::SetHandler(new_key.to_owned(), new_id.to_owned())); } for (old_key, old_id) in old_map.drain() { result.push(Diff::RemoveHandler(old_key, old_id)); } result } #[derive(Clone, Debug)] pub enum Node { Text(String), Element(Element), Null, } impl Node { pub(crate) fn diff(a: &mut Node, b: &Node, i: &mut usize) -> Option { match (a, b) { (&mut Node::Element(ref mut a), &Node::Element(ref b)) => { match Element::diff(a, b, *i) { Some(diff) => Some(diff), None => None, } } (&mut Node::Text(ref mut text_a), &Node::Text(ref text_b)) => { if text_a == text_b { return None; } Some(Diff::ReplaceChild(*i, b.to_owned())) } (&mut Node::Null, &Node::Null) => None, (&mut Node::Null, _) => Some(Diff::AddChild(*i, b.to_owned())), (_, &Node::Null) => Some(Diff::RemoveChild(*i)), _ => Some(Diff::ReplaceChild(*i, b.to_owned())), } } fn get_key(&self) -> Option { match self { Node::Element(ref el) => el.get_key(), _ => None, } } } fn get_nodelist_key_set(nodelist: &[Node]) -> FxHashSet { FxHashSet::from_iter(nodelist.iter().filter_map(|c| c.get_key())) } fn diff_children(a: &mut Vec, b: &[Node], i: &mut usize) -> Vec { let mut result = vec![]; let b_key_set = get_nodelist_key_set(b); let survived = a .drain(..) .filter(|c| match c.get_key() { Some(k) => { let is_survived = b_key_set.contains(&k); if !is_survived { result.push(Diff::RemoveChild(*i)); return false; } *i += 1; true } None => { *i += 1; true } }) .collect(); *a = survived; let mut i = 0; a.reverse(); for new_child in b.iter() { match a.pop() { None => { result.push(Diff::AddChild(i, new_child.to_owned())); i += 1; } Some(mut old_child) => { if let Some(diff) = Node::diff(&mut old_child, new_child, &mut i) { result.push(diff.to_owned()); if let Diff::RemoveChild(_) = diff { continue; } } } } i += 1; } for _ in a.iter() { result.push(Diff::RemoveChild(i)); } result } #[derive(Clone, Debug)] pub struct Element { name: String, attributes: Vec, handlers: Vec, children: Vec, } impl Element { fn new( name: String, attributes: Vec, handlers: Vec, children: Vec, ) -> Element { Element { name, attributes, handlers, children, } } pub fn name(&self) -> &str { &self.name } pub fn attributes(&self) -> &[Attribute] { &self.attributes } pub fn handlers(&self) -> &[Handler] { &self.handlers } pub fn children(&self) -> &[Node] { &self.children } fn diff(a: &mut Element, b: &Element, i: usize) -> Option { if let (Some(a_key), Some(b_key)) = (a.get_key(), b.get_key()) { if a_key != b_key { return Some(Diff::ReplaceChild(i, Node::Element(b.to_owned()))); } } if a.name != b.name { return Some(Diff::ReplaceChild(i, Node::Element(b.to_owned()))); } let mut result = vec![]; result.append(&mut diff_attributes(&mut a.attributes, &b.attributes)); result.append(&mut diff_handlers(&mut a.handlers, &b.handlers)); result.append(&mut diff_children(&mut a.children, &b.children, &mut 0)); if result.is_empty() { return None; } Some(Diff::PatchChild(i, result)) } fn get_key(&self) -> Option { self.attributes .iter() .find(|&&(ref k, _)| k == "key") .and_then(|&(_, ref v)| match v { AttributeValue::String(ref s) => Some(s.to_owned()), AttributeValue::Bool(_) => None, }) } } #[derive(Debug, Clone)] pub enum Diff { SetAttribute(String, AttributeValue), RemoveAttribute(String), AddChild(usize, Node), ReplaceChild(usize, Node), RemoveChild(usize), PatchChild(usize, Vec), SetHandler(String, String), RemoveHandler(String, String), } #[derive(Clone, Debug, PartialEq)] pub enum AttributeValue { String(String), Bool(bool), } impl From for AttributeValue { fn from(s: String) -> AttributeValue { AttributeValue::String(s) } } impl<'a> From<&'a str> for AttributeValue { fn from(s: &'a str) -> AttributeValue { AttributeValue::String(s.to_owned()) } } impl From for AttributeValue { fn from(b: bool) -> AttributeValue { AttributeValue::Bool(b) } } pub(crate) type HandlerMap = FxHashMap>; pub struct View { pub(crate) node: Node, pub(crate) handler_map: HandlerMap, } pub enum Child { View(View), ViewList(Vec>), } impl From for Child where T: Into> + Sized, { fn from(v: T) -> Child { Child::View(v.into()) } } impl FromIterator> for Child { fn from_iter(iter: I) -> Child where I: IntoIterator>, { Child::ViewList(iter.into_iter().collect()) } } impl View { pub fn new( name: String, attributes: Vec, handlers: Vec<(String, (String, HandlerFunction))>, children: Vec>, ) -> View { let mut handler_map = FxHashMap::default(); let handlers = handlers .into_iter() .map(|(kind, (id, f))| { let handler = (kind, id.to_owned()); handler_map.insert(id, f); handler }) .collect(); let mut children_vec = vec![]; for child in children { match child { Child::View(v) => { handler_map.extend(v.handler_map); children_vec.push(v.node); } Child::ViewList(child_vec) => { for v in child_vec { handler_map.extend(v.handler_map); children_vec.push(v.node); } } } } View { node: Node::Element(Element::new(name, attributes, handlers, children_vec)), handler_map, } } pub fn text(s: String) -> View { View { node: Node::Text(s), handler_map: FxHashMap::default(), } } pub fn null() -> View { View { node: Node::Null, handler_map: FxHashMap::default(), } } } impl From<()> for View { fn from(_: ()) -> View { View::null() } } impl From for View { fn from(s: String) -> View { View::text(s) } } impl<'a, A> From<&'a str> for View { fn from(s: &'a str) -> View { View::text(s.to_owned()) } } impl From> for View where T: Into>, { fn from(option: Option) -> View { option.map_or_else(View::null, |v| v.into()) } } ================================================ FILE: squark-macros/.gitignore ================================================ /target/ **/*.rs.bk Cargo.lock ================================================ FILE: squark-macros/Cargo.toml ================================================ [package] name = "squark-macros" version = "0.7.0" authors = ["Satoshi Amemiya "] repository = "https://github.com/rail44/squark" homepage = "https://github.com/rail44/squark" license = "WTFPL" readme = "README.md" keywords = ["web", "asmjs", "webasm", "javascript"] categories = ["gui", "web-programming"] description = "Macros like JSX to help building Squark application" [lib] proc-macro = true [dependencies] pest = "2.1.2" pest_derive = "2.1.0" [dev-dependencies] squark = { path = "../squark", version = "0.7.0" } ================================================ FILE: squark-macros/src/lib.rs ================================================ #![crate_type = "proc-macro"] #![feature(proc_macro_hygiene, proc_macro_quote)] extern crate pest; #[macro_use] extern crate pest_derive; extern crate proc_macro; use parser::{Parser as ViewParser, Rule}; use pest::iterators::{Pair, Pairs}; use pest::Parser; use proc_macro::{quote, Literal, TokenStream, TokenTree}; use std::iter::FromIterator; use std::str::FromStr; mod parser { #[derive(Parser)] #[grammar = "view.pest"] pub struct Parser; } fn get_token_stream(mut tag_pairs: Pairs) -> TokenStream { let name = tag_pairs.next().expect("name").as_str(); let _name = TokenTree::Literal(Literal::string(name)); let mut attributes = vec![]; let mut handlers = vec![]; let vec: Vec> = tag_pairs.next().expect("attributes").into_inner().collect(); for i in 0..(vec.len() / 2) { let j = i * 2; let k = &vec[j].as_str(); let v = &vec[j + 1]; let _v = match v.as_rule() { Rule::embedded => { let mut _embedded = TokenStream::from_str(v.as_str()).unwrap(); quote!($_embedded.into()) } Rule::string => { let _v = TokenTree::Literal(Literal::string(v.as_str())); quote! { $_v.into() } } Rule::bool => { let _v = TokenStream::from_str(v.as_str()).unwrap(); quote! { $_v.into() } } _ => unreachable!(), }; if k.starts_with("on") { let (_, k) = k.split_at(2); let _k = TokenTree::Literal(Literal::string(k)); handlers.push(quote! { ($_k.to_string(), _squark::handler($_v)), }); continue; } let _k = TokenTree::Literal(Literal::string(k)); attributes.push(quote! { ($_k.to_string(), $_v), }); } let _attributes = TokenStream::from_iter(attributes); let _handlers = TokenStream::from_iter(handlers); let mut children = vec![]; if let Some(children_pair) = tag_pairs.next() { for p in children_pair.into_inner() { let token = match p.as_rule() { Rule::tag => { let _tag = get_token_stream(p.into_inner()); quote! { _squark::Child::from($_tag), } } Rule::text => { let _text = TokenTree::Literal(Literal::string(p.as_str())); quote! { $_text.into(), } } Rule::embedded => { let _embedded = TokenStream::from_str(p.as_str()).unwrap(); quote! { {$_embedded}.into(), } } _ => unreachable!(), }; children.push(token); } } let _children = TokenStream::from_iter(children); quote! { _squark::View::new( $_name.to_string(), vec![ $_attributes ], vec![ $_handlers ], vec![ $_children ] ) } } #[proc_macro] pub fn view(arg: TokenStream) -> TokenStream { let s = arg.to_string(); let mut pairs = ViewParser::parse(Rule::view, &s).unwrap(); let _token = get_token_stream(pairs.next().unwrap().into_inner()); quote! { { extern crate squark as _squark; $_token } } } ================================================ FILE: squark-macros/src/view.pest ================================================ view = _{ SOI ~ tag ~ EOI } identifier = _{ ('a'..'z' | 'A'..'Z' | '0'..'9' | "_" | "-")+ } tag_name = @{ identifier } tag = { single_tag | pair_tag } pair_tag = _{ open_tag ~ children ~ close_tag } single_tag = _{ "<" ~ tag_name ~ attributes ~ "/" ~ ">" } children = {(tag | embedded_outer | text)*} open_tag = _{ "<" ~ tag_name ~ attributes ~ ">" } close_tag = _{ "<" ~ "/" ~ (!">" ~ ANY)+ ~ ">" } attributes = { attribute* } attribute = _{ key ~ "=" ~ (string_literal | bool | embedded_outer) } key = @{ identifier } string_literal = _{ "\"" ~ string ~ "\"" } string = @{ (!"\"" ~ ANY)* } embedded_outer = _{ "{" ~ embedded ~ "}" } embedded = { (bracketed | (!"}" ~ ANY))* } bracketed = _{ "{" ~ bracketed_inner ~ "}" } bracketed_inner = _{ (bracketed | (!"}" ~ ANY))* } bool = { "true" | "false" } text = @{ (!("<" | "{") ~ ANY)+ } newline = _{ "\n" | "\r\n" } WHITESPACE = _{ " " | newline } ================================================ FILE: squark-macros/tests/test.rs ================================================ #![feature(test, use_extern_macros, proc_macro_non_items)] extern crate squark; extern crate squark_macros; use squark::View; use squark_macros::view; fn v() -> View<()> { let not_completed_count = 1234; let has_completed = true; view! {

    { not_completed_count.to_string() } item(s) left


    { if has_completed { view! {
    } } #[test] fn it_works() { let v = v(); } ================================================ FILE: squark-web/.gitignore ================================================ /target/ **/*.rs.bk Cargo.lock ================================================ FILE: squark-web/Cargo.toml ================================================ [package] name = "squark-web" version = "0.3.0" authors = ["Satoshi Amemiya "] repository = "https://github.com/rail44/squark" homepage = "https://github.com/rail44/squark" license = "WTFPL" readme = "README.md" categories = ["gui", "web-programming", "wasm"] description = "Squark runtime implemiontion for web browser with using wasm-bindgen" edition = "2018" [dependencies] serde_json = "1.0.41" serde = "1.0.101" squark = { path = "../squark", version = "0.7.0" } wasm-bindgen = { version = "0.2.51", features = [ "nightly", "serde-serialize" ] } js-sys = "0.3.28" futures = "0.1.29" wasm-bindgen-futures = "0.4.1" [dependencies.web-sys] version = "0.3.28" features = [ 'Window', 'Document', 'DomStringMap', 'Element', 'EventTarget', 'HtmlElement', 'HtmlInputElement', 'Node', 'NodeList', 'Text', 'Event', 'InputEvent', 'KeyboardEvent', ] ================================================ FILE: squark-web/src/lib.rs ================================================ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use futures::Future; use wasm_bindgen_futures::future_to_promise; use squark::{ uuid, App, AttributeValue, Diff, Element as SquarkElement, Env, HandlerArg, Node as SquarkNode, Runtime, }; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use web_sys::{window, Document, Element, EventTarget, HtmlElement, Node}; use serde::Serialize; use serde_json::json; trait ToHandlerArg: JsCast { fn to_handler_arg(self) -> HandlerArg; } impl ToHandlerArg for web_sys::Event { fn to_handler_arg(self) -> HandlerArg { json!{null} } } impl ToHandlerArg for web_sys::InputEvent { fn to_handler_arg(self) -> HandlerArg { let ev: web_sys::Event = self.into(); let target = ev.target().unwrap(); let js_val: &JsValue = target.as_ref(); if js_val.is_null() { return json!{""}; } let input_el: &web_sys::HtmlInputElement = target.unchecked_ref(); json!{input_el.value()} } } impl ToHandlerArg for web_sys::KeyboardEvent { fn to_handler_arg(self) -> HandlerArg { json!{self.key()} } } type AttachedMap = HashMap>>; fn document() -> Document { window().unwrap().document().unwrap() } fn get_handler_id(el: &HtmlElement) -> Option { let id = js_sys::Reflect::get(el.dataset().as_ref(), &"handlerId".into()).unwrap(); if id.is_undefined() { return None; } Some(id.as_string().unwrap()) } fn set_handler_id(el: &HtmlElement, id: &str) { js_sys::Reflect::set(el.dataset().as_ref(), &"handlerId".into(), &id.into()).unwrap(); } #[derive(Clone)] pub struct WebRuntime { env: Env
    , root: Rc, attached_map: Rc>, } fn insert_at(parent: &Node, i: usize, node: &Node) { let ref_node = parent.child_nodes().item(i as u32); if ref_node.is_none() { parent.append_child(&node).unwrap(); return; } parent.insert_before(&node, ref_node.as_ref()).unwrap(); } fn set_attribute(el: &Element, name: &str, value: &AttributeValue) { match value { AttributeValue::Bool(b) => { js_sys::Reflect::set(el.as_ref(), &name.into(), &(*b).into()).unwrap(); el.set_attribute(name, &b.to_string()).unwrap(); } AttributeValue::String(s) => { js_sys::Reflect::set(el.as_ref(), &name.into(), &s.into()).unwrap(); el.set_attribute(name, s).unwrap(); } } } impl WebRuntime { pub fn new(root: Element, state: A::State) -> WebRuntime { WebRuntime { env: Env::new(state), root: Rc::new(root), attached_map: Rc::new(RefCell::new(AttachedMap::new())), } } fn handle_diff_inner(&self, el: &Element, diff: Diff) { match diff { Diff::AddChild(i, node) => self.add_child(el, i, node), Diff::PatchChild(i, diffs) => { let as_node: &Node = el.as_ref(); let child = as_node.child_nodes().item(i as u32).unwrap(); for diff in diffs { self.handle_diff_inner(child.unchecked_ref(), diff); } } Diff::ReplaceChild(i, node) => self.replace_child(el, i, node), Diff::SetAttribute(name, value) => set_attribute(el, &name, &value), Diff::RemoveAttribute(name) => { el.remove_attribute(&name).unwrap(); } Diff::RemoveChild(i) => self.remove_child(el.as_ref(), i), Diff::SetHandler(name, id) => self.set_handler(el.unchecked_ref(), &name, &id), Diff::RemoveHandler(name, _) => { let attached = self .attached_map .borrow_mut() .get_mut(&get_handler_id(el.unchecked_ref()).unwrap()) .and_then(|inner| inner.remove(&name)) .unwrap(); let html_el: &EventTarget = el.unchecked_ref(); html_el .remove_event_listener_with_callback(&name, attached.as_ref().unchecked_ref()) .unwrap(); } } } fn replace_at(&self, parent: &Node, i: usize, node: &Node) { let current = parent.child_nodes().item(i as u32).unwrap(); self.remove_attached(¤t); parent.replace_child(&node, ¤t).unwrap(); } fn create_element(&self, el: &SquarkElement) -> Element { let web_el: Element = document().create_element(el.name()).unwrap(); for (ref name, ref value) in el.attributes() { set_attribute(&web_el, name, value); } for (ref name, id) in el.handlers() { self.set_handler(web_el.unchecked_ref(), name, &id); } { let node: &Node = web_el.as_ref(); for child in el.children() { match child { SquarkNode::Element(el) => { let child = self.create_element(el); node.append_child(child.as_ref()).unwrap(); } SquarkNode::Text(s) => { let child = document().create_text_node(s.as_str()); node.append_child(child.as_ref()).unwrap(); } _ => (), }; } } web_el } fn add_child(&self, parent: &Element, i: usize, node: SquarkNode) { match node { SquarkNode::Element(el) => { let child = self.create_element(&el); insert_at(parent.as_ref(), i, child.as_ref()); } SquarkNode::Text(s) => { let child = document().create_text_node(s.as_str()); insert_at(parent.as_ref(), i, child.as_ref()); } _ => (), }; } fn replace_child(&self, parent: &Element, i: usize, node: SquarkNode) { match node { SquarkNode::Element(el) => { let child = self.create_element(&el); self.replace_at(parent.as_ref(), i, child.as_ref()); } SquarkNode::Text(s) => { let child = document().create_text_node(s.as_str()); self.replace_at(parent.as_ref(), i, child.as_ref()); } _ => (), }; } fn remove_child(&self, parent: &Node, i: usize) { let current = parent.child_nodes().item(i as u32).unwrap(); self.remove_attached(current.unchecked_ref()); parent.remove_child(¤t).unwrap(); } fn set_handler(&self, el: &Element, name: &str, id: &str) { let closure = match name { "keydown" => self._set_handler::(el.as_ref(), "keydown", id), "input" => self._set_handler::(el.as_ref(), "input", id), name => self._set_handler::(el.as_ref(), name, id), }; let handler_id = get_handler_id(el.unchecked_ref()).unwrap_or_else(|| { let uuid = uuid(); set_handler_id(el.unchecked_ref(), &uuid); uuid }); let mut map = self.attached_map.borrow_mut(); let inner = map.entry(handler_id).or_insert_with(HashMap::new); if let Some(attached) = inner.remove(name) { let target: &EventTarget = el.as_ref(); target .remove_event_listener_with_callback(&name, attached.as_ref().unchecked_ref()) .unwrap(); } inner.insert(name.to_owned(), closure); } fn _set_handler( &self, el: &EventTarget, name: &str, id: &str, ) -> Closure { let handler = self.pop_handler(id).unwrap(); let closure = Closure::new(move |ev: JsValue| { let ev: T = ev.unchecked_into(); handler(ev.to_handler_arg()); }); el.add_event_listener_with_callback(name, closure.as_ref().unchecked_ref()) .unwrap(); closure } fn remove_attached(&self, el: &Node) { if !el.is_instance_of::() { return; } let el: &Element = el.unchecked_ref(); let mut map = self.attached_map.borrow_mut(); if let Some(id) = get_handler_id(el.unchecked_ref()) { map.remove(&id); } let children = el.query_selector_all("[data-has-handler]").unwrap(); for i in 0..children.length() { let child = children.item(i).unwrap(); if let Some(id) = get_handler_id(child.unchecked_ref()) { map.remove(&id); } } } } fn nop(_: T) {} impl Runtime for WebRuntime { fn get_env<'a>(&'a self) -> &'a Env { &self.env } fn schedule_render(&self) { let this = self.clone(); let closure = Closure::wrap(Box::new(move |_: JsValue| { this.run(); }) as Box); window() .unwrap() .request_animation_frame(closure.as_ref().unchecked_ref()) .unwrap(); closure.forget(); } fn handle_diff(&self, diff: Diff) { self.handle_diff_inner(&self.root, diff); } fn handle_future(&self, future: Box>) { let p = future_to_promise( future .map(|v| JsValue::from_serde(&v).unwrap()) .map_err(|e| JsValue::from_serde(&e).unwrap()) ); let closure = Closure::new(nop); p.then(&closure); closure.forget(); } }