From 76f6ca9474be6d02b7277984fed8f7f00b65cac3 Mon Sep 17 00:00:00 2001 From: Jan Bujak Date: Sun, 23 Apr 2017 13:53:38 +0200 Subject: [PATCH] Initial commit --- .gitignore | 4 + Cargo.toml | 25 + LICENSE-APACHE | 201 +++ LICENSE-MIT | 25 + README.md | 140 ++ examples/todomvc/Cargo.toml | 10 + examples/todomvc/README.md | 0 examples/todomvc/src/main.rs | 241 +++ .../static/css/todomvc-app-css/index.css | 378 +++++ .../static/css/todomvc-common/base.css | 141 ++ examples/todomvc/static/index.html | 43 + info/logo.png | Bin 0 -> 31936 bytes src/ecosystem/mod.rs | 5 + src/ecosystem/serde.rs | 1462 +++++++++++++++++ src/ecosystem/serde_json.rs | 47 + src/lib.rs | 209 +++ src/webapi/date.rs | 22 + src/webapi/document.rs | 97 ++ src/webapi/element.rs | 62 + src/webapi/event.rs | 307 ++++ src/webapi/event_target.rs | 73 + src/webapi/global.rs | 12 + src/webapi/html_element.rs | 59 + src/webapi/html_elements/input.rs | 52 + src/webapi/html_elements/mod.rs | 3 + src/webapi/location.rs | 37 + src/webapi/mod.rs | 17 + src/webapi/node.rs | 197 +++ src/webapi/node_list.rs | 91 + src/webapi/storage.rs | 69 + src/webapi/string_map.rs | 39 + src/webapi/text_node.rs | 23 + src/webapi/token_list.rs | 47 + src/webapi/window.rs | 89 + src/webapi/window_or_worker.rs | 27 + src/webcore/callfn.rs | 73 + src/webcore/ffi.rs | 9 + src/webcore/initialization.rs | 261 +++ src/webcore/macros.rs | 542 ++++++ src/webcore/mod.rs | 11 + src/webcore/newtype.rs | 77 + src/webcore/number.rs | 650 ++++++++ src/webcore/serialization.rs | 1016 ++++++++++++ src/webcore/try_from.rs | 29 + src/webcore/value.rs | 1137 +++++++++++++ src/webcore/void.rs | 25 + 46 files changed, 8084 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 examples/todomvc/Cargo.toml create mode 100644 examples/todomvc/README.md create mode 100644 examples/todomvc/src/main.rs create mode 100644 examples/todomvc/static/css/todomvc-app-css/index.css create mode 100644 examples/todomvc/static/css/todomvc-common/base.css create mode 100644 examples/todomvc/static/index.html create mode 100644 info/logo.png create mode 100644 src/ecosystem/mod.rs create mode 100644 src/ecosystem/serde.rs create mode 100644 src/ecosystem/serde_json.rs create mode 100644 src/lib.rs create mode 100644 src/webapi/date.rs create mode 100644 src/webapi/document.rs create mode 100644 src/webapi/element.rs create mode 100644 src/webapi/event.rs create mode 100644 src/webapi/event_target.rs create mode 100644 src/webapi/global.rs create mode 100644 src/webapi/html_element.rs create mode 100644 src/webapi/html_elements/input.rs create mode 100644 src/webapi/html_elements/mod.rs create mode 100644 src/webapi/location.rs create mode 100644 src/webapi/mod.rs create mode 100644 src/webapi/node.rs create mode 100644 src/webapi/node_list.rs create mode 100644 src/webapi/storage.rs create mode 100644 src/webapi/string_map.rs create mode 100644 src/webapi/text_node.rs create mode 100644 src/webapi/token_list.rs create mode 100644 src/webapi/window.rs create mode 100644 src/webapi/window_or_worker.rs create mode 100644 src/webcore/callfn.rs create mode 100644 src/webcore/ffi.rs create mode 100644 src/webcore/initialization.rs create mode 100644 src/webcore/macros.rs create mode 100644 src/webcore/mod.rs create mode 100644 src/webcore/newtype.rs create mode 100644 src/webcore/number.rs create mode 100644 src/webcore/serialization.rs create mode 100644 src/webcore/try_from.rs create mode 100644 src/webcore/value.rs create mode 100644 src/webcore/void.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..89af8c2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +target +Cargo.lock +examples/todomvc/target +examples/todomvc/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..b451523e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "stdweb" +version = "0.1.0" +authors = ["Jan Bujak "] +repository = "https://github.com/koute/stdweb" +homepage = "https://github.com/koute/stdweb" +documentation = "https://docs.rs/stdweb/*/stdweb/" +license = "MIT/Apache-2.0" +readme = "README.md" +keywords = ["web", "asmjs", "webasm", "javascript"] +description = "A standard library for the client-side Web" + +[dependencies] +serde = { version = "1", optional = true } +serde_json = { version = "1", optional = true } +clippy = { version = "0.0", optional = true } + +[dev-dependencies] +serde_json = "1" +serde_derive = "1" + +[features] +default = ["serde", "serde_json"] +dev = ["serde", "serde_json", "clippy"] +serde-support = ["serde", "serde_json"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 00000000..cc176fb7 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2017 Jan Bujak + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 00000000..3cb2325e --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2017 Jan Bujak + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..3c56ed46 --- /dev/null +++ b/README.md @@ -0,0 +1,140 @@ +

+ +

+ +[![Build Status](https://api.travis-ci.org/koute/stdweb.svg)](https://travis-ci.org/koute/stdweb) + +# A standard library for the client-side Web + +[![Documentation](https://docs.rs/stdweb/badge.svg)](https://docs.rs/stdweb/*/stdweb/) + +The goal of this crate is to provide Rust bindings to the Web APIs and to allow +a high degree of interoperability between Rust and JavaScript. + +## Examples + +You can directly embed JavaScript code into Rust: + +```rust +let message = "Hello, 世界!"; +let result = js! { + alert!( @{message} ); + return 2 + 2 * 2; +}; + +println!( "2 + 2 * 2 = {:?}", result ); +``` + +Even closures are are supported: + +```rust +let print_hello = |name: String| { + println!( "Hello, {}!", name ); +}; + +js! { + var print_hello = @{print_hello}; + print_hello( "Bob" ); + print_hello.drop(); // Necessary to clean up the closure on Rust's side. +} +``` + +You can also pass arbitrary structures thanks to [serde]: + +```rust +#[derive(Serialize)] +struct Person { + name: String, + age: i32 +} + +js_serializable!( Person ); + +js! { + var person = @{person}; + console.log( person.name + " is " + person.age + " years old." ); +}; +``` + +[serde]: https://serde.rs/ + +This crate also exposes a number of Web APIs, for example: + +```rust +let button = document().query_selector( "#hide-button" ).unwrap(); +button.add_event_listener( move |_: ClickEvent| { + for anchor in document().query_selector_all( "#main a" ) { + js!( @{anchor}.style = "display: none;"; ); + } +}); +``` + +## Design goals + + * Expose a full suite of Web APIs as exposed by web browsers. + * Try to follow the original JavaScript conventions and structure as much as possible, + except in cases where doing otherwise results in a clearly superior design. + * Be a building block from which higher level frameworks and libraries + can be built. + * Make it convenient and easy to embed JavaScript code directly into Rust + and to marshal data between the two. + * Integrate with the wider Rust ecosystem, e.g. support marshaling of structs + which implement serde's Serializable. + * Put Rust in the driver's seat where a non-trivial Web application can be + written without touching JavaScript at all. + * Allow Rust to take part in the upcoming WebAssembly (re)volution. + +## Getting started + +WARNING: This crate is still a work-in-progress. Things might not work. +Things might break. The APIs are in flux. Please do not use it in production. + +1. Add an asmjs target with rustup: + + $ rustup target add asmjs-unknown-emscripten + +2. Install emscripten. If you're on Arch Linux then you can just + run `sudo pacman -S emscripten`; other distributions might also + have recent enough emscripten packages in their repositories. + + Alternatively you can install it like this: + + $ curl -O https://s3.amazonaws.com/mozilla-games/emscripten/releases/emsdk-portable.tar.gz + $ tar -xzf emsdk-portable.tar.gz + $ source emsdk_portable/emsdk_env.sh + $ emsdk update + $ emsdk install sdk-incoming-64bit + $ emsdk activate sdk-incoming-64bit + +3. Install [cargo-web]; it's not strictly necessary but it makes things + more convenient: + + $ cargo install cargo-web + +4. Go into `examples/todomvc` and type: + + $ cargo web start + +5. Visit `http://localhost:8000` with your browser. + +[cargo-web]: https://github.com/koute/cargo-web + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +Snippets of documentation which come from [Mozilla Developer Network] are covered under the [CC-BY-SA, version 2.5] or later. + +[Mozilla Developer Network]: https://developer.mozilla.org/en-US/ +[CC-BY-SA, version 2.5]: https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. diff --git a/examples/todomvc/Cargo.toml b/examples/todomvc/Cargo.toml new file mode 100644 index 00000000..fe01ede2 --- /dev/null +++ b/examples/todomvc/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "todomvc" +version = "0.1.0" +authors = ["Jan Bujak "] + +[dependencies] +serde = "1" +serde_json = "1" +serde_derive = "1" +stdweb = { path = "../.." } diff --git a/examples/todomvc/README.md b/examples/todomvc/README.md new file mode 100644 index 00000000..e69de29b diff --git a/examples/todomvc/src/main.rs b/examples/todomvc/src/main.rs new file mode 100644 index 00000000..25c9d88b --- /dev/null +++ b/examples/todomvc/src/main.rs @@ -0,0 +1,241 @@ +#[macro_use] +extern crate stdweb; + +#[macro_use] +extern crate serde_derive; +extern crate serde_json; + +use std::cell::RefCell; +use std::rc::Rc; + +use stdweb::unstable::TryInto; +use stdweb::web::{ + IEventTarget, + IElement, + IHtmlElement, + INode, + HtmlElement, + Element, + document, + window +}; + +use stdweb::web::event::{ + IEvent, + IKeyboardEvent, + DoubleClickEvent, + ClickEvent, + KeypressEvent, + ChangeEvent, + BlurEvent, + HashChangeEvent +}; + +use stdweb::web::html_element::InputElement; + +// Shamelessly stolen from webplatform's TodoMVC example. +macro_rules! enclose { + ( ($( $x:ident ),*) $y:expr ) => { + { + $(let $x = $x.clone();)* + $y + } + }; +} + +#[derive(Clone, Serialize, Deserialize)] +struct Todo { + title: String, + completed: bool +} + +#[derive(Serialize, Deserialize)] +struct State { + todo_list: Vec< Todo > +} + +impl State { + fn new() -> Self { + State { + todo_list: Vec::new() + } + } +} + +type StateRef = Rc< RefCell< State > >; + +fn start_editing( state: &StateRef, index: usize, li: &HtmlElement, label: &Element ) { + li.class_list().add( "editing" ); + + let edit: InputElement = document().create_element( "input" ).try_into().unwrap(); + edit.class_list().add( "edit" ); + edit.set_value( label.inner_text() ); + edit.add_event_listener( enclose!( (edit) move |event: KeypressEvent| { + if event.key() == "Enter" { + edit.blur(); + } + })); + + edit.add_event_listener( enclose!( (state, li, edit) move |_: BlurEvent| { + li.class_list().remove( "editing" ); + li.remove_child( &edit ).unwrap(); + state.borrow_mut().todo_list[ index ].title = edit.value().into_string().unwrap(); + update_dom( &state ); + })); + + li.append_child( &edit ); + edit.focus(); +} + +fn create_entry( state: &StateRef, index: usize, text: &str ) -> HtmlElement { + let li: HtmlElement = document().create_element( "li" ).try_into().unwrap(); + let div = document().create_element( "div" ); + let checkbox: InputElement = document().create_element( "input" ).try_into().unwrap(); + let label = document().create_element( "label" ); + let button = document().create_element( "button" ); + + div.class_list().add( "view" ); + + checkbox.class_list().add( "toggle" ); + checkbox.set_kind( "checkbox" ); + checkbox.add_event_listener( enclose!( (state, checkbox) move |_: ChangeEvent| { + let checked: bool = js!( return @{&checkbox}.checked; ).try_into().unwrap(); + state.borrow_mut().todo_list[ index ].completed = checked; + update_dom( &state ); + })); + + label.append_child( &document().create_text_node( text ) ); + label.add_event_listener( enclose!( (state, li, label) move |_: DoubleClickEvent| { + start_editing( &state, index, &li, &label ); + })); + + button.class_list().add( "destroy" ); + button.add_event_listener( enclose!( (state) move |_: ClickEvent| { + state.borrow_mut().todo_list.remove( index ); + update_dom( &state ); + })); + + li.append_child( &div ); + div.append_child( &checkbox ); + div.append_child( &label ); + div.append_child( &button ); + + li +} + +fn update_dom( state: &StateRef ) { + // Ideally you'd use some kind of DOM diffing here; + // since it's supposed to be a simple example we opt + // for the nuclear option and just rebuild everything + // from scratch. + + fn only_active( todo: &Todo ) -> bool { todo.completed == false } + fn only_completed( todo: &Todo ) -> bool { todo.completed == true } + fn all( _: &Todo ) -> bool { true } + + // See which filter we're supposed to use based on the URL. + let hash = document().location().unwrap().hash(); + let filter = match hash.as_str() { + "#/active" => only_active, + "#/completed" => only_completed, + _ => all + }; + + let filter_anchor_selector = match hash.as_str() { + "#/active" | "#/completed" => hash.as_str(), + _ => "#/" + }; + + // Select the filter "button". + let filter_anchors = document().query_selector_all( ".filters a" ); + for anchor in &filter_anchors { + let anchor: Element = anchor.try_into().unwrap(); + anchor.class_list().remove( "selected" ); + } + + let filter_anchor_selector = format!( ".filters a[href='{}']", filter_anchor_selector ); + let selected_anchor: Element = document().query_selector( filter_anchor_selector.as_str() ).unwrap().try_into().unwrap(); + selected_anchor.class_list().add( "selected" ); + + // Clear previous entries in the list. + let list = document().query_selector( ".todo-list" ).unwrap(); + while let Some( child ) = list.first_child() { + list.remove_child( &child ).unwrap(); + } + + // Fill out the list. + let state_borrow = state.borrow(); + for (index, todo) in state_borrow.todo_list.iter().enumerate().filter( |&(_, todo)| filter( todo ) ) { + let entry_node = create_entry( state, index, todo.title.as_str() ); + if todo.completed { + entry_node.class_list().add( "completed" ); + let checkbox = entry_node.query_selector( "input[type='checkbox']" ).unwrap(); + js!( @{checkbox}.checked = true; ); + } + list.append_child( &entry_node ); + } + + // Display the amount of active TODOs lefs. + let items_left = state_borrow.todo_list.iter().filter( |todo| { + todo.completed == false + }).count(); + + let counter_display = document().query_selector( ".todo-count" ).unwrap(); + if items_left == 1 { + counter_display.set_text_content( "1 item left" ); + } else { + counter_display.set_text_content( format!( "{} items left", items_left ).as_str() ); + } + + // Hide the list if we don't have any TODOs. + let main = document().query_selector( ".main" ).unwrap(); + if state_borrow.todo_list.is_empty() { + js!( @{main}.style = "display: none;" ); + } else { + js!( @{main}.style = "display: block;" ); + } + + // Save the state into local storage. + let state_json = serde_json::to_string( &*state_borrow ).unwrap(); + window().local_storage().insert( "state", state_json.as_str() ); +} + +fn main() { + stdweb::initialize(); + + let state = window().local_storage().get( "state" ).and_then( |state_json| { + serde_json::from_str( state_json.as_str() ).ok() + }).unwrap_or_else( State::new ); + let state = Rc::new( RefCell::new( state ) ); + + let title_entry: InputElement = document().query_selector( ".new-todo" ).unwrap().try_into().unwrap(); + title_entry.add_event_listener( enclose!( (state, title_entry) move |event: KeypressEvent| { + if event.key() == "Enter" { + event.prevent_default(); + + let title: String = title_entry.value().try_into().unwrap(); + if title.is_empty() == false { + state.borrow_mut().todo_list.push( Todo { + title: title, + completed: false + }); + + title_entry.set_value( "" ); + update_dom( &state ); + } + } + })); + + let clear_completed = document().query_selector( ".clear-completed" ).unwrap(); + clear_completed.add_event_listener( enclose!( (state) move |_: ClickEvent| { + state.borrow_mut().todo_list.retain( |todo| todo.completed == false ); + update_dom( &state ); + })); + + window().add_event_listener( enclose!( (state) move |_: HashChangeEvent| { + update_dom( &state ); + })); + + update_dom( &state ); + stdweb::event_loop(); +} diff --git a/examples/todomvc/static/css/todomvc-app-css/index.css b/examples/todomvc/static/css/todomvc-app-css/index.css new file mode 100644 index 00000000..e6e089cb --- /dev/null +++ b/examples/todomvc/static/css/todomvc-app-css/index.css @@ -0,0 +1,378 @@ +html, +body { + margin: 0; + padding: 0; +} + +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; + } +} diff --git a/examples/todomvc/static/css/todomvc-common/base.css b/examples/todomvc/static/css/todomvc-common/base.css new file mode 100644 index 00000000..da65968a --- /dev/null +++ b/examples/todomvc/static/css/todomvc-common/base.css @@ -0,0 +1,141 @@ +hr { + margin: 20px 0; + border: 0; + border-top: 1px dashed #c5c5c5; + border-bottom: 1px dashed #f7f7f7; +} + +.learn a { + font-weight: normal; + text-decoration: none; + color: #b83f45; +} + +.learn a:hover { + text-decoration: underline; + color: #787e7e; +} + +.learn h3, +.learn h4, +.learn h5 { + margin: 10px 0; + font-weight: 500; + line-height: 1.2; + color: #000; +} + +.learn h3 { + font-size: 24px; +} + +.learn h4 { + font-size: 18px; +} + +.learn h5 { + margin-bottom: 0; + font-size: 14px; +} + +.learn ul { + padding: 0; + margin: 0 0 30px 25px; +} + +.learn li { + line-height: 20px; +} + +.learn p { + font-size: 15px; + font-weight: 300; + line-height: 1.3; + margin-top: 0; + margin-bottom: 0; +} + +#issue-count { + display: none; +} + +.quote { + border: none; + margin: 20px 0 60px 0; +} + +.quote p { + font-style: italic; +} + +.quote p:before { + content: '“'; + font-size: 50px; + opacity: .15; + position: absolute; + top: -20px; + left: 3px; +} + +.quote p:after { + content: '”'; + font-size: 50px; + opacity: .15; + position: absolute; + bottom: -42px; + right: 3px; +} + +.quote footer { + position: absolute; + bottom: -40px; + right: 0; +} + +.quote footer img { + border-radius: 3px; +} + +.quote footer a { + margin-left: 5px; + vertical-align: middle; +} + +.speech-bubble { + position: relative; + padding: 10px; + background: rgba(0, 0, 0, .04); + border-radius: 5px; +} + +.speech-bubble:after { + content: ''; + position: absolute; + top: 100%; + right: 30px; + border: 13px solid transparent; + border-top-color: rgba(0, 0, 0, .04); +} + +.learn-bar > .learn { + position: absolute; + width: 272px; + top: 8px; + left: -300px; + padding: 10px; + border-radius: 5px; + background-color: rgba(255, 255, 255, .6); + transition-property: left; + transition-duration: 500ms; +} + +@media (min-width: 899px) { + .learn-bar { + width: auto; + padding-left: 300px; + } + + .learn-bar > .learn { + left: 8px; + } +} diff --git a/examples/todomvc/static/index.html b/examples/todomvc/static/index.html new file mode 100644 index 00000000..85433955 --- /dev/null +++ b/examples/todomvc/static/index.html @@ -0,0 +1,43 @@ + + + + + stdweb • TodoMVC + + + + +
+
+

todos

+ +
+
+ + +
    + +
    +
    +
    +

    Double-click to edit a todo

    +

    Part of the stdweb project

    +

    Based on TodoMVC

    +
    + + + diff --git a/info/logo.png b/info/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c08bd5e48b8409abf4e340aea972c56d14a32534 GIT binary patch literal 31936 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 &self, serializer: S ) -> Result< S::Ok, S::Error > { + serializer.serialize_unit_struct( "undefined" ) + } +} + +impl< 'de > Deserialize< 'de > for Undefined { + #[inline] + fn deserialize< D: de::Deserializer< 'de > >( deserializer: D ) -> Result< Self, D::Error > { + struct UndefinedVisitor; + impl< 'de > Visitor< 'de > for UndefinedVisitor { + type Value = Undefined; + + fn expecting( &self, formatter: &mut fmt::Formatter ) -> fmt::Result { + formatter.write_str( "undefined" ) + } + + fn visit_unit< E: de::Error >( self ) -> Result< Self::Value, E > { + Ok( Undefined ) + } + } + + deserializer.deserialize_unit_struct( "undefined", UndefinedVisitor ) + } +} + +impl Serialize for Null { + #[inline] + fn serialize< S: ser::Serializer >( &self, serializer: S ) -> Result< S::Ok, S::Error > { + serializer.serialize_unit_struct( "null" ) + } +} + +impl< 'de > Deserialize< 'de > for Null { + #[inline] + fn deserialize< D: de::Deserializer< 'de > >( deserializer: D ) -> Result< Self, D::Error > { + struct NullVisitor; + impl< 'de > Visitor< 'de > for NullVisitor { + type Value = Null; + + fn expecting( &self, formatter: &mut fmt::Formatter ) -> fmt::Result { + formatter.write_str( "null" ) + } + + fn visit_unit< E: de::Error >( self ) -> Result< Self::Value, E > { + Ok( Null ) + } + } + + deserializer.deserialize_unit_struct( "null", NullVisitor ) + } +} + +impl Serialize for Number { + #[inline] + fn serialize< S: ser::Serializer >( &self, serializer: S ) -> Result< S::Ok, S::Error > { + match *get_storage( self ) { + Storage::I32( value ) => serializer.serialize_i32( value ), + Storage::F64( value ) => serializer.serialize_f64( value ) + } + } +} + +impl< 'de > Deserialize< 'de > for Number { + #[inline] + fn deserialize< D: de::Deserializer< 'de > >( deserializer: D ) -> Result< Self, D::Error > { + struct NumberVisitor; + impl< 'de > Visitor< 'de > for NumberVisitor { + type Value = Number; + + fn expecting( &self, formatter: &mut fmt::Formatter ) -> fmt::Result { + formatter.write_str( "a number" ) + } + + fn visit_i8< E: de::Error >( self, value: i8 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_i16< E: de::Error >( self, value: i16 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_i32< E: de::Error >( self, value: i32 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_i64< E: de::Error >( self, value: i64 ) -> Result< Self::Value, E > { + value.try_into().map_err( E::custom ) + } + + fn visit_u8< E: de::Error >( self, value: u8 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_u16< E: de::Error >( self, value: u16 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_u32< E: de::Error >( self, value: u32 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_u64< E: de::Error >( self, value: u64 ) -> Result< Self::Value, E > { + value.try_into().map_err( E::custom ) + } + + fn visit_f32< E: de::Error >( self, value: f32 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_f64< E: de::Error >( self, value: f64 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + } + + deserializer.deserialize_f64( NumberVisitor ) + } +} + +impl Serialize for Value { + #[inline] + fn serialize< S: ser::Serializer >( &self, serializer: S ) -> Result< S::Ok, S::Error > { + use serde_crate::ser::SerializeMap; + match *self { + Value::Undefined => serializer.serialize_unit_struct( "undefined" ), + Value::Null => serializer.serialize_unit_struct( "null" ), + Value::Bool( value ) => serializer.serialize_bool( value ), + Value::Number( ref value ) => value.serialize( serializer ), + Value::String( ref value ) => serializer.serialize_str( value ), + Value::Array( ref value ) => value.serialize( serializer ), + Value::Object( ref value ) => { + let mut map = try!( serializer.serialize_map( Some( value.len() ) ) ); + for (key, value) in value { + try!( map.serialize_key( key ) ); + try!( map.serialize_value( value ) ); + } + + map.end() + }, + Value::Reference( _ ) => { + let map = try!( serializer.serialize_map( None ) ); + map.end() + } + } + } +} + +impl< 'de > Deserialize< 'de > for Value { + #[inline] + fn deserialize< D: de::Deserializer< 'de > >( deserializer: D ) -> Result< Self, D::Error > { + struct ValueVisitor; + impl< 'de > Visitor< 'de > for ValueVisitor { + type Value = Value; + + fn expecting( &self, formatter: &mut fmt::Formatter ) -> fmt::Result { + formatter.write_str( "a value which is convertible into a JavaScript value" ) + } + + fn visit_bool< E: de::Error >( self, value: bool ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_i8< E: de::Error >( self, value: i8 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_i16< E: de::Error >( self, value: i16 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_i32< E: de::Error >( self, value: i32 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_i64< E: de::Error >( self, value: i64 ) -> Result< Self::Value, E > { + value.try_into().map_err( E::custom ) + } + + fn visit_u8< E: de::Error >( self, value: u8 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_u16< E: de::Error >( self, value: u16 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_u32< E: de::Error >( self, value: u32 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_u64< E: de::Error >( self, value: u64 ) -> Result< Self::Value, E > { + value.try_into().map_err( E::custom ) + } + + fn visit_f32< E: de::Error >( self, value: f32 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_f64< E: de::Error >( self, value: f64 ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_char< E: de::Error >( self, value: char ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_str< E: de::Error >( self, value: &str ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_string< E: de::Error >( self, value: String ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_unit< E: de::Error >( self ) -> Result< Self::Value, E > { + Ok( Null.into() ) + } + + fn visit_none< E: de::Error >( self ) -> Result< Self::Value, E > { + Ok( Null.into() ) + } + + fn visit_some< D: de::Deserializer< 'de > >( self, deserializer: D ) -> Result< Self::Value, D::Error > { + deserializer.deserialize_any( self ) + } + + fn visit_seq< V: de::SeqAccess< 'de > >( self, mut visitor: V ) -> Result< Self::Value, V::Error > { + let mut output: Vec< Value > = Vec::with_capacity( visitor.size_hint().unwrap_or( 0 ) ); + while let Some( element ) = visitor.next_element()? { + output.push( element ); + } + + Ok( output.into() ) + } + + fn visit_map< V: de::MapAccess< 'de > >( self, mut visitor: V ) -> Result< Self::Value, V::Error > { + let mut output: BTreeMap< String, Value > = BTreeMap::new(); + while let Some( (key, value) ) = visitor.next_entry()? { + output.insert( key, value ); + } + + Ok( output.into() ) + } + + fn visit_bytes< E: de::Error >( self, value: &[u8] ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + fn visit_byte_buf< E: de::Error >( self, value: Vec< u8 > ) -> Result< Self::Value, E > { + Ok( value.into() ) + } + + // Not really sure how (if?) to implement these at this point: + + // fn visit_newtype_struct< D: de::Deserializer< 'de > >( self, deserializer: D ) -> Result< Self::Value, D::Error > { + // unimplemented!(); + // } + + // fn visit_enum< V: de::EnumAccess >( self, visitor: V ) -> Result< Self::Value, V::Error > { + // unimplemented!(); + // } + } + + deserializer.deserialize_any( ValueVisitor ) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +enum ConversionErrorKind { + InvalidKey, + NumberConversionError( number::ConversionError ), + Custom( String ) +} + +/// A structure denoting a conversion error encountered during +/// serialization or deserialization. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct ConversionError { + kind: ConversionErrorKind +} + +impl ConversionError { + fn invalid_key() -> Self { + ConversionError { + kind: ConversionErrorKind::InvalidKey + } + } +} + +impl fmt::Display for ConversionError { + fn fmt( &self, formatter: &mut fmt::Formatter ) -> Result< (), fmt::Error > { + let message = error::Error::description( self ); + write!( formatter, "{}", message ) + } +} + +impl error::Error for ConversionError { + fn description( &self ) -> &str { + match self.kind { + ConversionErrorKind::InvalidKey => "key must be either a string or an integer", + ConversionErrorKind::NumberConversionError( ref error ) => error.description(), + ConversionErrorKind::Custom( ref message ) => message.as_str() + } + } +} + +impl ser::Error for ConversionError { + fn custom< T: fmt::Display >( message: T ) -> Self { + ConversionError { + kind: ConversionErrorKind::Custom( message.to_string() ) + } + } +} + +impl de::Error for ConversionError { + fn custom< T: fmt::Display >( message: T ) -> Self { + ConversionError { + kind: ConversionErrorKind::Custom( message.to_string() ) + } + } +} + +impl From< number::ConversionError > for ConversionError { + fn from( error: number::ConversionError ) -> Self { + ConversionError { + kind: ConversionErrorKind::NumberConversionError( error ) + } + } +} + +#[derive(Debug)] +pub struct Serializer { +} + +impl Serializer { + pub fn new() -> Self { + Serializer {} + } +} + +impl< 'a > ser::Serializer for &'a mut Serializer { + type Ok = Value; + type Error = ConversionError; + type SerializeSeq = SerializeVec; + type SerializeTuple = SerializeVec; + type SerializeTupleStruct = SerializeVec; + type SerializeTupleVariant = SerializeTupleVariant; + type SerializeMap = SerializeMap; + type SerializeStruct = SerializeMap; + type SerializeStructVariant = SerializeStructVariant; + + fn serialize_bool( self, value: bool ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_i8( self, value: i8 ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_i16( self, value: i16 ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_i32( self, value: i32 ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_i64( self, value: i64 ) -> Result< Self::Ok, Self::Error > { + Ok( value.try_into()? ) + } + + fn serialize_u8( self, value: u8 ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_u16( self, value: u16 ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_u32( self, value: u32 ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_u64( self, value: u64 ) -> Result< Self::Ok, Self::Error > { + Ok( value.try_into()? ) + } + + fn serialize_f32( self, value: f32 ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_f64( self, value: f64 ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_char( self, value: char ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_str( self, value: &str ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_bytes( self, value: &[u8] ) -> Result< Self::Ok, Self::Error > { + Ok( value.into() ) + } + + fn serialize_none( self ) -> Result< Self::Ok, Self::Error > { + self.serialize_unit() + } + + fn serialize_some< T: ?Sized + Serialize >( self, value: &T ) -> Result< Self::Ok, Self::Error > { + value.serialize( self ) + } + + fn serialize_unit( self ) -> Result< Self::Ok, Self::Error > { + Ok( Null.into() ) + } + + fn serialize_unit_struct( self, _name: &'static str ) -> Result< Self::Ok, Self::Error > { + self.serialize_unit() + } + + fn serialize_unit_variant( self, _name: &'static str, _variant_index: u32, variant: &'static str ) -> Result< Self::Ok, Self::Error > { + self.serialize_str( variant ) + } + + fn serialize_newtype_struct< T: ?Sized + Serialize >( self, _name: &'static str, value: &T ) -> Result< Self::Ok, Self::Error > { + value.serialize( self ) + } + + fn serialize_newtype_variant< T: ?Sized + Serialize >( self, _name: &'static str, _variant_index: u32, variant: &'static str, value: &T ) -> Result< Self::Ok, Self::Error > { + let mut object = BTreeMap::new(); + object.insert( String::from( variant ), to_value( &value )? ); + Ok( Value::Object( object ) ) + } + + fn serialize_seq( self, length: Option< usize > ) -> Result< Self::SerializeSeq, Self::Error > { + Ok( SerializeVec { + elements: Vec::with_capacity( length.unwrap_or( 0 ) ) + }) + } + + fn serialize_tuple( self, length: usize ) -> Result< Self::SerializeTuple, Self::Error > { + self.serialize_seq( Some( length ) ) + } + + fn serialize_tuple_struct( self, _name: &'static str, length: usize ) -> Result< Self::SerializeTupleStruct, Self::Error > { + self.serialize_seq( Some( length ) ) + } + + fn serialize_tuple_variant( self, _name: &'static str, _variant_index: u32, variant: &'static str, length: usize ) -> Result< Self::SerializeTupleVariant, Self::Error > { + Ok( SerializeTupleVariant { + name: String::from( variant ), + elements: Vec::with_capacity( length ), + }) + } + + fn serialize_map( self, _length: Option< usize > ) -> Result< Self::SerializeMap, Self::Error > { + Ok( SerializeMap { + map: BTreeMap::new(), + next_key: None, + }) + } + + fn serialize_struct( self, _name: &'static str, length: usize ) -> Result< Self::SerializeStruct, Self::Error > { + self.serialize_map( Some( length ) ) + } + + fn serialize_struct_variant( self, _name: &'static str, _variant_index: u32, variant: &'static str, _length: usize ) -> Result< Self::SerializeStructVariant, Self::Error > { + Ok( SerializeStructVariant { + name: String::from( variant ), + map: BTreeMap::new(), + }) + } +} + +#[doc(hidden)] +#[cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))] +#[inline] +pub fn to_value< T: Serialize >( value: T ) -> Result< Value, ConversionError > { + let mut serializer = Serializer {}; + value.serialize( &mut serializer ) +} + +#[doc(hidden)] +#[inline] +pub fn from_value< 'de, T: Deserialize< 'de > >( value: Value ) -> Result< T, ConversionError > { + Deserialize::deserialize( value ) +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct SerializeVec { + elements: Vec< Value >, +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct SerializeTupleVariant { + name: String, + elements: Vec< Value >, +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct SerializeMap { + map: BTreeMap< String, Value >, + next_key: Option< String >, +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct SerializeStructVariant { + name: String, + map: BTreeMap< String, Value >, +} + +impl ser::SerializeSeq for SerializeVec { + type Ok = Value; + type Error = ConversionError; + + #[inline] + fn serialize_element< T: ?Sized + Serialize >( &mut self, value: &T ) -> Result< (), Self::Error > { + self.elements.push( to_value( &value )? ); + Ok(()) + } + + #[inline] + fn end( self ) -> Result< Self::Ok, Self::Error > { + Ok( self.elements.into() ) + } +} + +impl ser::SerializeTuple for SerializeVec { + type Ok = Value; + type Error = ConversionError; + + #[inline] + fn serialize_element< T: ?Sized + Serialize >( &mut self, value: &T ) -> Result< (), Self::Error > { + ser::SerializeSeq::serialize_element( self, value ) + } + + #[inline] + fn end( self ) -> Result< Self::Ok, Self::Error > { + ser::SerializeSeq::end( self ) + } +} + +impl ser::SerializeTupleStruct for SerializeVec { + type Ok = Value; + type Error = ConversionError; + + fn serialize_field< T: ?Sized + Serialize >( &mut self, value: &T ) -> Result< (), Self::Error > { + ser::SerializeSeq::serialize_element( self, value ) + } + + fn end( self ) -> Result< Self::Ok, Self::Error > { + ser::SerializeSeq::end( self ) + } +} + +impl ser::SerializeTupleVariant for SerializeTupleVariant { + type Ok = Value; + type Error = ConversionError; + + fn serialize_field< T: ?Sized + Serialize >( &mut self, value: &T ) -> Result< (), Self::Error > { + self.elements.push( to_value( &value )? ); + Ok(()) + } + + fn end( self ) -> Result< Self::Ok, Self::Error > { + let mut object: BTreeMap< String, Value > = BTreeMap::new(); + object.insert( self.name, Value::Array( self.elements ) ); + Ok( Value::Object( object ) ) + } +} + +impl ser::SerializeMap for SerializeMap { + type Ok = Value; + type Error = ConversionError; + + fn serialize_key< T: ?Sized + Serialize >( &mut self, key: &T ) -> Result< (), Self::Error > { + match to_value( &key )? { + Value::String( string ) => self.next_key = Some( string ), + Value::Number( number ) => { + if let Ok( value ) = number.try_into() { + let value: u64 = value; + self.next_key = Some( value.to_string() ); + } else if let Ok( value ) = number.try_into() { + let value: i64 = value; + self.next_key = Some( value.to_string() ); + } else { + return Err( ConversionError::invalid_key() ) + } + }, + _ => return Err( ConversionError::invalid_key() ) + } + + Ok(()) + } + + fn serialize_value< T: ?Sized + Serialize >( &mut self, value: &T ) -> Result< (), Self::Error > { + let key = self.next_key.take(); + // Panic because this indicates a bug in the program rather than an + // expected failure. + let key = key.expect( "serialize_value called before serialize_key" ); + self.map.insert( key, to_value( &value )? ); + Ok(()) + } + + fn end( self ) -> Result< Self::Ok, Self::Error > { + Ok( Value::Object( self.map ) ) + } +} + +impl ser::SerializeStruct for SerializeMap { + type Ok = Value; + type Error = ConversionError; + + fn serialize_field< T: ?Sized + Serialize >( &mut self, key: &'static str, value: &T ) -> Result< (), Self::Error > { + ser::SerializeMap::serialize_key( self, key )?; + ser::SerializeMap::serialize_value( self, value ) + } + + fn end( self ) -> Result< Self::Ok, Self::Error > { + ser::SerializeMap::end( self ) + } +} + +impl ser::SerializeStructVariant for SerializeStructVariant { + type Ok = Value; + type Error = ConversionError; + + fn serialize_field< T: ?Sized + Serialize >( &mut self, key: &'static str, value: &T ) -> Result< (), Self::Error > { + self.map.insert( String::from( key ), to_value( &value )? ); + Ok(()) + } + + fn end( self ) -> Result< Self::Ok, Self::Error > { + let mut object = BTreeMap::new(); + object.insert( self.name, Value::Object( self.map ) ); + Ok( Value::Object( object ) ) + } +} + +impl< 'de > de::Deserializer< 'de > for Number { + type Error = ConversionError; + + #[inline] + fn deserialize_any< V: Visitor< 'de > >( self, visitor: V ) -> Result< V::Value, Self::Error > { + // TODO: Consider dispatching the visitor based on the actual value? + match *get_storage( &self ) { + number::Storage::I32( value ) => visitor.visit_i32( value ), + number::Storage::F64( value ) => visitor.visit_f64( value ) + } + } + + forward_to_deserialize_any! { + bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit option + seq bytes byte_buf map unit_struct newtype_struct + tuple_struct struct identifier tuple enum ignored_any + } +} + +// TODO: Impl for `&'a Number` and `&'a mut Number`. + +impl Value { + fn unexpected( &self ) -> de::Unexpected { + match *self { + Value::Undefined => de::Unexpected::Other( "undefined" ), + Value::Null => de::Unexpected::Other( "null" ), + Value::Bool( value ) => de::Unexpected::Bool( value ), + Value::Number( ref value ) => { + match *get_storage( value ) { + number::Storage::I32( value ) => de::Unexpected::Signed( value as i64 ), + number::Storage::F64( value ) => de::Unexpected::Float( value ) + } + }, + Value::String( ref value ) => de::Unexpected::Str( value ), + Value::Array( _ ) => de::Unexpected::Seq, + Value::Object( _ ) => de::Unexpected::Map, + Value::Reference( _ ) => de::Unexpected::Other( "reference to a JavaScript value" ) + } + } +} + +impl< 'de > de::Deserializer< 'de > for Value { + type Error = ConversionError; + + #[inline] + fn deserialize_any< V: Visitor< 'de > >( self, visitor: V ) -> Result< V::Value, Self::Error > { + match self { + Value::Undefined => visitor.visit_unit(), + Value::Null => visitor.visit_unit(), + Value::Bool( value ) => visitor.visit_bool( value ), + Value::Number( value ) => de::Deserializer::deserialize_any( value, visitor ), + Value::String( value ) => visitor.visit_string( value ), + Value::Array( value ) => { + let length = value.len(); + let mut deserializer = SeqDeserializer::new( value ); + let seq = visitor.visit_seq( &mut deserializer )?; + let remaining = deserializer.iter.len(); + if remaining == 0 { + Ok( seq ) + } else { + Err( de::Error::invalid_length( length, &"fewer elements in the array" ) ) + } + }, + Value::Object( value ) => { + let length = value.len(); + let mut deserializer = MapDeserializer::new( value ); + let map = visitor.visit_map( &mut deserializer )?; + let remaining = deserializer.iter.len(); + if remaining == 0 { + Ok( map ) + } else { + Err( de::Error::invalid_length( length, &"fewer elements in the object" ) ) + } + }, + Value::Reference( _ ) => { + unimplemented!(); // TODO: ? + } + } + } + + #[inline] + fn deserialize_option< V: Visitor< 'de > >( self, visitor: V ) -> Result< V::Value, Self::Error > { + match self { + Value::Undefined => visitor.visit_none(), + Value::Null => visitor.visit_none(), + _ => visitor.visit_some( self ) + } + } + + #[inline] + fn deserialize_enum< V: Visitor< 'de > >( self, _name: &str, _variants: &'static [&'static str], visitor: V ) -> Result< V::Value, Self::Error > { + let (variant, value) = match self { + Value::Object( value ) => { + let mut iter = value.into_iter(); + let (variant, value) = match iter.next() { + Some( value ) => value, + None => { + return Err( de::Error::invalid_value( de::Unexpected::Map, &"map with a single key" ) ); + } + }; + + // Enums are encoded as objects with a single key:value pair. + if iter.next().is_some() { + return Err( de::Error::invalid_value( de::Unexpected::Map, &"map with a single key" ) ); + } + + (variant, Some( value )) + }, + Value::String( variant ) => (variant, None), + other => { + return Err( de::Error::invalid_type( other.unexpected(), &"string or map" ) ); + } + }; + + visitor.visit_enum( EnumDeserializer { + variant: variant, + value: value, + }) + } + + #[inline] + fn deserialize_newtype_struct< V: Visitor< 'de > >( self, _name: &'static str, visitor: V ) -> Result< V::Value, Self::Error > { + visitor.visit_newtype_struct( self ) + } + + forward_to_deserialize_any! { + bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit seq + bytes byte_buf map unit_struct tuple_struct struct + identifier tuple ignored_any + } +} + +struct EnumDeserializer { + variant: String, + value: Option< Value >, +} + +impl< 'de > de::EnumAccess< 'de > for EnumDeserializer { + type Error = ConversionError; + type Variant = VariantDeserializer; + + fn variant_seed< V: de::DeserializeSeed< 'de > >( self, seed: V ) -> Result< (V::Value, VariantDeserializer), Self::Error > { + let variant = self.variant.into_deserializer(); + let visitor = VariantDeserializer { value: self.value }; + seed.deserialize( variant ).map( |v| (v, visitor) ) + } +} + +struct VariantDeserializer { + value: Option< Value >, +} + +impl< 'de > de::VariantAccess< 'de > for VariantDeserializer { + type Error = ConversionError; + + fn unit_variant( self ) -> Result< (), Self::Error > { + match self.value { + Some( value ) => de::Deserialize::deserialize( value ), + None => Ok(()), + } + } + + fn newtype_variant_seed< T: de::DeserializeSeed< 'de > >( self, seed: T ) -> Result< T::Value, Self::Error > { + match self.value { + Some( value ) => seed.deserialize( value ), + None => Err( de::Error::invalid_type( de::Unexpected::UnitVariant, &"newtype variant" ) ), + } + } + + fn tuple_variant< V: Visitor< 'de > >( self, _length: usize, visitor: V ) -> Result< V::Value, Self::Error > { + match self.value { + Some( Value::Array( value ) ) => { + de::Deserializer::deserialize_any( SeqDeserializer::new( value ), visitor ) + }, + Some( other ) => Err( de::Error::invalid_type( other.unexpected(), &"tuple variant" ) ), + None => Err( de::Error::invalid_type( de::Unexpected::UnitVariant, &"tuple variant" ) ) + } + } + + fn struct_variant< V: Visitor< 'de > >( self, _fields: &'static [&'static str], visitor: V ) -> Result< V::Value, Self::Error > { + match self.value { + Some( Value::Object( value ) ) => { + de::Deserializer::deserialize_any( MapDeserializer::new( value ), visitor ) + }, + Some( other ) => Err( de::Error::invalid_type( other.unexpected(), &"struct variant" ) ), + _ => Err( de::Error::invalid_type( de::Unexpected::UnitVariant, &"struct variant" ) ) + } + } +} + +struct SeqDeserializer { + iter: vec::IntoIter< Value >, +} + +impl SeqDeserializer { + fn new( vec: Vec< Value >) -> Self { + SeqDeserializer { + iter: vec.into_iter(), + } + } +} + +impl< 'de > de::Deserializer< 'de > for SeqDeserializer { + type Error = ConversionError; + + #[inline] + fn deserialize_any< V: Visitor< 'de > >( mut self, visitor: V ) -> Result< V::Value, Self::Error > { + let length = self.iter.len(); + if length == 0 { + visitor.visit_unit() + } else { + let ret = visitor.visit_seq( &mut self )?; + let remaining = self.iter.len(); + if remaining == 0 { + Ok( ret ) + } else { + Err( de::Error::invalid_length( length, &"fewer elements in array" ) ) + } + } + } + + forward_to_deserialize_any! { + bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit option + seq bytes byte_buf map unit_struct newtype_struct + tuple_struct struct identifier tuple enum ignored_any + } +} + +impl< 'de > de::SeqAccess< 'de > for SeqDeserializer { + type Error = ConversionError; + + fn next_element_seed< T: de::DeserializeSeed< 'de > >( &mut self, seed: T ) -> Result< Option< T::Value >, Self::Error > { + match self.iter.next() { + Some( value ) => seed.deserialize( value ).map( Some ), + None => Ok( None ), + } + } + + fn size_hint( &self ) -> Option< usize > { + match self.iter.size_hint() { + (lower, Some( upper )) if lower == upper => Some( upper ), + _ => None + } + } +} + +struct MapDeserializer { + iter: as IntoIterator>::IntoIter, + value: Option< Value >, +} + +impl MapDeserializer { + fn new( map: BTreeMap< String, Value > ) -> Self { + MapDeserializer { + iter: map.into_iter(), + value: None, + } + } +} + +impl< 'de > de::MapAccess< 'de > for MapDeserializer { + type Error = ConversionError; + + fn next_key_seed< T: de::DeserializeSeed< 'de > >( &mut self, seed: T ) -> Result< Option< T::Value >, Self::Error> { + match self.iter.next() { + Some( (key, value) ) => { + self.value = Some( value ); + seed.deserialize( key.into_deserializer() ).map( Some ) + } + None => Ok( None ) + } + } + + fn next_value_seed< T: de::DeserializeSeed< 'de > >( &mut self, seed: T ) -> Result< T::Value, Self::Error > { + match self.value.take() { + Some( value ) => seed.deserialize( value ), + None => Err( de::Error::custom( "value is missing" ) ), + } + } + + fn size_hint( &self ) -> Option< usize > { + match self.iter.size_hint() { + (lower, Some( upper )) if lower == upper => Some( upper ), + _ => None + } + } +} + +impl< 'de > de::Deserializer< 'de > for MapDeserializer { + type Error = ConversionError; + + #[inline] + fn deserialize_any< V: Visitor< 'de > >( self, visitor: V ) -> Result< V::Value, Self::Error > { + visitor.visit_map( self ) + } + + forward_to_deserialize_any! { + bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit option + seq bytes byte_buf map unit_struct newtype_struct + tuple_struct struct identifier tuple enum ignored_any + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __js_serializable_serde_boilerplate { + (($($impl_arg:tt)*) ($($kind_arg:tt)*) ($($bounds:tt)*)) => { + __js_serializable_boilerplate!( ($($impl_arg)*) ($($kind_arg)*) ($($bounds)*) ); + + impl< $($impl_arg),* > $crate::private::JsSerializable for $($kind_arg)* where $($bounds)* { + #[inline] + fn into_js< 'x >( &'x self, arena: &'x $crate::private::PreallocatedArena ) -> $crate::private::SerializedValue< 'x > { + let value = $crate::private::to_value( self ).unwrap(); + let value = arena.save( value ); + $crate::private::JsSerializable::into_js( value, arena ) + } + + #[inline] + fn memory_required( &self ) -> usize { + // TODO: This is very inefficient. The actual conversion into + // the Value should be only done once. + let value = to_value( self ).unwrap(); + $crate::private::JsSerializable::memory_required( &value ) + } + } + + impl< $($impl_arg),* > $crate::unstable::TryFrom< $($kind_arg)* > for $crate::Value where $($bounds)* { + type Error = $crate::serde::ConversionError; + #[inline] + fn try_from( value: $($kind_arg)* ) -> Result< Self, Self::Error > { + $crate::private::to_value( value ) + } + } + + impl< '_a, $($impl_arg),* > $crate::unstable::TryFrom< &'_a $($kind_arg)* > for $crate::Value where $($bounds)* { + type Error = $crate::serde::ConversionError; + #[inline] + fn try_from( value: &'_a $($kind_arg)* ) -> Result< Self, Self::Error > { + $crate::private::to_value( value ) + } + } + + impl< '_a, $($impl_arg),* > $crate::unstable::TryFrom< &'_a mut $($kind_arg)* > for $crate::Value where $($bounds)* { + type Error = $crate::serde::ConversionError; + #[inline] + fn try_from( value: &'_a mut $($kind_arg)* ) -> Result< Self, Self::Error > { + $crate::private::to_value( value ) + } + } + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __js_deserializable_serde_boilerplate { + (($($impl_arg:tt)*) ($($kind_arg:tt)*) ($($bounds:tt)*)) => { + impl< $($impl_arg),* > TryFrom< $crate::Value > for $($kind_arg)* where $($bounds)* { + type Error = $crate::serde::ConversionError; + #[inline] + fn try_from( value: $crate::Value ) -> Result< Self, Self::Error > { + $crate::private::from_value( value ) + } + } + } +} + +/// A macro which makes it possible to pass an instance of a given type +/// implementing Serde's `Serialize` into the [js!](macro.js.html) macro. +/// +/// For types defined outside of your crate you can also use the [Serde](struct.Serde.html) +/// newtype to make them serializable indirectly. +/// +/// # Examples +/// +/// ``` +/// #[derive(Serialize, Debug)] +/// struct Person { +/// name: String, +/// age: i32 +/// } +/// +/// js_serializable!( Person ); +/// +/// let person = Person { +/// name: "Bob".to_owned(), +/// age: 33 +/// }; +/// +/// js! { +/// var person = @{person}; +/// console.log( person.name + " is " + person.age + " years old." ); +/// }; +/// ``` +/// +/// This macro also accepts generics: +/// +/// ``` +/// trait Foobar {} +/// +/// #[derive(Serialize)] +/// struct Wrapper< 'a, T: Serialize + 'a >( &'a T ); +/// +/// js_serializable!( impl< 'a, T > for Wrapper< 'a, T > where T: Serialize + Foobar ); +/// ``` +#[macro_export] +macro_rules! js_serializable { + ($kind:tt) => { + __js_serializable_serde_boilerplate!( () ($kind) () ); + }; + + (impl< $($impl_arg:tt),* > for $kind:ty where $($bounds:tt)*) => { + __js_serializable_serde_boilerplate!( ($($impl_arg),*) ($kind) ($($bounds)*) ); + }; + + (impl< $($impl_arg:tt),* > for $kind:ty) => { + __js_serializable_serde_boilerplate!( ($($impl_arg),*) ($kind) () ); + }; +} + +/// A macro which makes it possible to convert an instance of a given type +/// implementing Serde's `Deserialize` into a [Value](enum.Value.html) using +/// [TryInto](unstable/trait.TryInto.html). +/// +/// For types defined outside of your crate you can also use the [Serde](serde/struct.Serde.html) +/// newtype to make them deserializable indirectly. +/// +/// # Examples +/// +/// ``` +/// #[derive(Deserialize, Debug)] +/// struct Person { +/// name: String, +/// age: i32 +/// } +/// +/// js_deserializable!( Person ); +/// +/// let value = js! { +/// return { +/// number: 123, +/// string: "Hello!" +/// }; +/// }; +/// +/// let structure: StructureSerializable = value.try_into().unwrap(); +/// assert_eq!( structure.number, 123 ); +/// assert_eq!( structure.string, "Hello!" ); +/// ``` +/// +/// This macro also accepts generics just as the [js_serializable!](macro.js_serializable.html) does. +#[macro_export] +macro_rules! js_deserializable { + ($kind:tt) => { + __js_deserializable_serde_boilerplate!( () ($kind) () ); + }; + + (impl< $($impl_arg:tt),* > for $kind:ty where $($bounds:tt)*) => { + __js_deserializable_serde_boilerplate!( ($($impl_arg),*) ($kind) ($($bounds)*) ); + }; + + (impl< $($impl_arg:tt),* > for $kind:ty) => { + __js_deserializable_serde_boilerplate!( ($($impl_arg),*) ($kind) () ); + }; +} + +/// A newtype which makes it possible to pass a value which implements +/// Serde's `Serializable` into the [js!](macro.js.html) macro. +/// +/// For types defined in your crate you can also use the [js_serializable!](macro.js_serializable.html) +/// macro to make them serializable directly. +/// +/// # Examples +/// +/// ``` +/// #[derive(Serialize, Debug)] +/// struct Person { +/// name: String, +/// age: i32 +/// } +/// +/// let person = Person { +/// name: "Bob".to_owned(), +/// age: 33 +/// }; +/// +/// js! { +/// var person = @{Serde( person )}; +/// console.log( person.name + " is " + person.age + " years old." ); +/// }; +/// ``` +pub struct Serde< T >( pub T ); + +impl< T: fmt::Debug > fmt::Debug for Serde< T > { + #[inline] + fn fmt( &self, formatter: &mut fmt::Formatter ) -> Result< (), fmt::Error > { + self.0.fmt( formatter ) + } +} + +impl< T: Serialize > JsSerializable for Serde< T > { + #[inline] + fn into_js< 'a >( &'a self, arena: &'a PreallocatedArena ) -> SerializedValue< 'a > { + let value = to_value( &self.0 ).unwrap(); + let value = arena.save( value ); + value.into_js( arena ) + } + + #[inline] + fn memory_required( &self ) -> usize { + let value = to_value( &self.0 ).unwrap(); + value.memory_required() + } +} + +impl< T: Serialize > TryFrom< Serde< T > > for Value { + type Error = ConversionError; + #[inline] + fn try_from( value: Serde< T > ) -> Result< Self, Self::Error > { + to_value( &value.0 ) + } +} + +impl< 'a, T: Serialize > TryFrom< &'a Serde< T > > for Value { + type Error = ConversionError; + #[inline] + fn try_from( value: &'a Serde< T > ) -> Result< Self, Self::Error > { + to_value( &value.0 ) + } +} + +impl< 'a, T: Serialize > TryFrom< &'a mut Serde< T > > for Value { + type Error = ConversionError; + #[inline] + fn try_from( value: &'a mut Serde< T > ) -> Result< Self, Self::Error > { + to_value( &value.0 ) + } +} + +impl< 'de, T: Deserialize< 'de > > TryFrom< Value > for Serde< T > { + type Error = ConversionError; + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + Ok( Serde( from_value( value )? ) ) + } +} + +__js_serializable_boilerplate!( impl< T > for Serde< T > where T: Serialize ); + +#[cfg(test)] +mod tests { + use super::*; + use serde_json; + + #[test] + fn serialize_undefined() { + // This is technically incorrect as `undefined` is not serializable into JSON, + // but serde is generic so it serialized it anyway. + assert_eq!( serde_json::to_string( &Undefined ).unwrap(), "null" ); + } + + #[test] + fn serialize_null() { + assert_eq!( serde_json::to_string( &Null ).unwrap(), "null" ); + } + + #[test] + fn serialize_number_negative() { + let value: Number = (-123_i32).into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "-123" ); + } + + #[test] + fn serialize_number_positive() { + let value: Number = 123_i32.into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "123" ); + } + + #[test] + fn serialize_number_float() { + let value: Number = 3.33_f64.into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "3.33" ); + } + + #[test] + fn serialize_value_undefined() { + let value: Value = Undefined.into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "null" ); + } + + #[test] + fn serialize_value_null() { + let value: Value = Null.into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "null" ); + } + + #[test] + fn serialize_value_bool_true() { + let value: Value = true.into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "true" ); + } + + #[test] + fn serialize_value_bool_false() { + let value: Value = false.into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "false" ); + } + + #[test] + fn serialize_value_number() { + let value: Value = (123_i32).into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "123" ); + } + + #[test] + fn serialize_value_string() { + let value: Value = "死神はりんごしか食べない".into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "\"死神はりんごしか食べない\"" ); + } + + #[test] + fn serialize_value_array() { + let value: Value = (&[true, false][..]).into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "[true,false]" ); + } + + #[test] + fn serialize_value_object() { + use std::collections::BTreeMap; + let mut map = BTreeMap::new(); + map.insert( "1", "one" ); + map.insert( "2", "two" ); + + let value: Value = map.into(); + assert_eq!( serde_json::to_string( &value ).unwrap(), "{\"1\":\"one\",\"2\":\"two\"}" ); + } + + #[test] + fn deserialize_value_null() { + let value: Value = serde_json::from_str( "null" ).unwrap(); + assert_eq!( value, Value::Null ); + } + + #[test] + fn deserialize_value_bool_false() { + let value: Value = serde_json::from_str( "false" ).unwrap(); + assert_eq!( value, Value::Bool( false ) ); + } + + #[test] + fn deserialize_value_bool_true() { + let value: Value = serde_json::from_str( "true" ).unwrap(); + assert_eq!( value, Value::Bool( true ) ); + } + + #[test] + fn deserialize_value_number_integer() { + let value: Value = serde_json::from_str( "33" ).unwrap(); + assert_eq!( value, Value::Number( 33.into() ) ); + } + + #[test] + fn deserialize_value_number_float() { + let value: Value = serde_json::from_str( "33.33" ).unwrap(); + assert_eq!( value, Value::Number( 33.33.into() ) ); + } + + #[test] + fn deserialize_value_string() { + let value: Value = serde_json::from_str( "\"Bob\"" ).unwrap(); + assert_eq!( value, Value::String( "Bob".to_owned() ) ); + } + + #[test] + fn deserialize_value_array() { + let value: Value = serde_json::from_str( "[true, false]" ).unwrap(); + assert_eq!( value, Value::Array( vec![ Value::Bool( true ), Value::Bool( false ) ] ) ); + } + + #[test] + fn deserialize_value_object() { + let value: Value = serde_json::from_str( "{\"1\":\"one\",\"2\":\"two\"}" ).unwrap(); + let mut map: BTreeMap< String, Value > = BTreeMap::new(); + map.insert( "1".to_owned(), Value::String( "one".to_owned() ) ); + map.insert( "2".to_owned(), Value::String( "two".to_owned() ) ); + assert_eq!( value, Value::Object( map ) ); + } + + #[derive(Serialize, Deserialize, Debug)] + struct Structure { + number: i32, + string: String + } + + #[derive(Serialize, Deserialize, Debug)] + struct StructureSerializable { + number: i32, + string: String + } + + js_serializable!( StructureSerializable ); + js_deserializable!( StructureSerializable ); + + #[test] + fn serialization_into_value_through_macro() { + let structure = StructureSerializable { + number: 123, + string: "Hello!".to_owned() + }; + + let value: Value = structure.try_into().unwrap(); + let mut map = BTreeMap::new(); + map.insert( "number".to_owned(), Value::Number( 123.into() ) ); + map.insert( "string".to_owned(), Value::String( "Hello!".to_owned() ) ); + assert_eq!( value, Value::Object( map ) ); + } + + #[test] + fn serialization_into_javascript_through_macro() { + let structure = StructureSerializable { + number: 123, + string: "Hello!".to_owned() + }; + + let result = js! { + var object = @{structure}; + return object.number === 123 && object.string === "Hello!" && Object.keys( object ).length == 2; + }; + + assert_eq!( result, true ); + } + + #[test] + fn serialization_into_value_through_newtype() { + let structure = Structure { + number: 123, + string: "Hello!".to_owned() + }; + + let value: Value = Serde( structure ).try_into().unwrap(); + let mut map = BTreeMap::new(); + map.insert( "number".to_owned(), Value::Number( 123.into() ) ); + map.insert( "string".to_owned(), Value::String( "Hello!".to_owned() ) ); + assert_eq!( value, Value::Object( map ) ); + } + + #[test] + fn serialization_into_javascript_through_newtype() { + let structure = Structure { + number: 123, + string: "Hello!".to_owned() + }; + + let result = js! { + var object = @{Serde( structure )}; + return object.number === 123 && object.string === "Hello!" && Object.keys( object ).length == 2; + }; + + assert_eq!( result, true ); + } + + #[test] + fn deserialization_into_value_through_macro() { + let value = js! { + return { + number: 123, + string: "Hello!" + }; + }; + + let structure: StructureSerializable = value.try_into().unwrap(); + assert_eq!( structure.number, 123 ); + assert_eq!( structure.string, "Hello!" ); + } + + #[test] + fn deserialization_into_value_through_newtype() { + let value = js! { + return { + number: 123, + string: "Hello!" + }; + }; + + let structure: Serde< Structure > = value.try_into().unwrap(); + assert_eq!( structure.0.number, 123 ); + assert_eq!( structure.0.string, "Hello!" ); + } +} diff --git a/src/ecosystem/serde_json.rs b/src/ecosystem/serde_json.rs new file mode 100644 index 00000000..d66b21b3 --- /dev/null +++ b/src/ecosystem/serde_json.rs @@ -0,0 +1,47 @@ +use std::collections::BTreeMap; +use serde_json::value::Value as JsonValue; +use webcore::value::Value; +use webcore::try_from::{TryFrom, TryInto}; +use webcore::number::ConversionError; + +impl TryFrom< JsonValue > for Value { + type Error = ConversionError; + + #[inline] + fn try_from( value: JsonValue ) -> Result< Self, Self::Error > { + let result = match value { + JsonValue::Null => Value::Null, + JsonValue::Bool( value ) => Value::Bool( value ), + JsonValue::Number( value ) => { + if let Some( value ) = value.as_u64() { + Value::Number( value.try_into()? ) + } else if let Some( value ) = value.as_i64() { + Value::Number( value.try_into()? ) + } else { + Value::Number( value.as_f64().unwrap().into() ) + } + }, + JsonValue::String( value ) => Value::String( value ), + JsonValue::Array( value ) => { + let mut vector = Vec::new(); + + vector.reserve( value.len() ); + for element in value.into_iter() { + vector.push( element.try_into()? ); + } + + Value::Array( vector ) + }, + JsonValue::Object( value ) => { + let mut map = BTreeMap::new(); + for (key, value) in value.into_iter() { + map.insert( key.into(), value.try_into()? ); + } + + Value::Object( map ) + } + }; + + Ok( result ) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..d47a00ad --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,209 @@ +//! The goal of this crate is to provide Rust bindings to the Web APIs and to allow +//! a high degree of interoperability between Rust and JavaScript. +//! +//! ## Examples +//! +//! You can directly embed JavaScript code into Rust: +//! +//! ```rust +//! let message = "Hello, 世界!"; +//! let result = js! { +//! alert!( @{message} ); +//! return 2 + 2 * 2; +//! }; +//! +//! println!( "2 + 2 * 2 = {:?}", result ); +//! ``` +//! +//! Even closures are are supported: +//! +//! ```rust +//! let print_hello = |name: String| { +//! println!( "Hello, {}!", name ); +//! }; +//! +//! js! { +//! var print_hello = @{print_hello}; +//! print_hello( "Bob" ); +//! print_hello.drop(); // Necessary to clean up the closure on Rust's side. +//! } +//! ``` +//! +//! You can also pass arbitrary structures thanks to [serde]: +//! +//! ```rust +//! #[derive(Serialize)] +//! struct Person { +//! name: String, +//! age: i32 +//! } +//! +//! js_serializable!( Person ); +//! +//! js! { +//! var person = @{person}; +//! console.log( person.name + " is " + person.age + " years old." ); +//! }; +//! ``` +//! +//! [serde]: https://serde.rs/ +//! +//! This crate also exposes a number of Web APIs, for example: +//! +//! ```rust +//! let button = document().query_selector( "#hide-button" ).unwrap(); +//! button.add_event_listener( move |_: ClickEvent| { +//! for anchor in document().query_selector_all( "#main a" ) { +//! js!( @{anchor}.style = "display: none;"; ); +//! } +//! }); +//! ``` + +#![deny( + missing_docs, + missing_debug_implementations, + trivial_numeric_casts, + unstable_features, + unused_import_braces, + unused_qualifications +)] +#![cfg_attr(feature = "dev", allow(unstable_features))] +#![cfg_attr(feature = "dev", feature(plugin))] +#![cfg_attr(feature = "dev", plugin(clippy))] +#![recursion_limit="750"] + +#[cfg(feature = "serde")] +#[macro_use] +extern crate serde as serde_crate; + +#[cfg(any(test, feature = "serde_json"))] +extern crate serde_json; + +#[cfg(all(test, feature = "serde"))] +#[macro_use] +extern crate serde_derive; + +#[macro_use] +mod webcore; +mod webapi; +mod ecosystem; + +pub use webcore::initialization::{ + initialize, + event_loop +}; +pub use webcore::value::{ + Undefined, + Null, + Value, + Reference +}; +pub use webcore::number::Number; + +#[cfg(feature = "serde")] +/// A module with serde-related APIs. +pub mod serde { + pub use ecosystem::serde::{ + ConversionError, + Serde + }; +} + +/// A module with bindings to the Web APIs. +pub mod web { + pub use webapi::window::{ + Window, + window + }; + pub use webapi::document::{ + Document, + document + }; + pub use webapi::global::{ + set_timeout, + alert + }; + pub use webapi::date::Date; + pub use webapi::event_target::{IEventTarget, EventTarget}; + pub use webapi::node::{INode, Node, CloneKind}; + pub use webapi::element::{IElement, Element}; + pub use webapi::html_element::{IHtmlElement, HtmlElement}; + pub use webapi::window_or_worker::IWindowOrWorker; + pub use webapi::token_list::TokenList; + pub use webapi::node_list::NodeList; + pub use webapi::string_map::StringMap; + pub use webapi::storage::Storage; + pub use webapi::location::Location; + + /// A module containing error types. + pub mod error { + pub use webapi::node::NotFoundError; + } + + /// A module containing HTML DOM elements. + pub mod html_element { + pub use webapi::html_elements::InputElement; + } + + /// A module containing JavaScript DOM events. + pub mod event { + pub use webapi::event::{ + IEvent, + IKeyboardEvent, + IUiEvent, + IMouseEvent, + IFocusEvent, + + ChangeEvent, + KeypressEvent, + ClickEvent, + DoubleClickEvent, + FocusEvent, + BlurEvent, + HashChangeEvent + }; + } +} + +/// A module containing stable counterparts to currently +/// unstable Rust features. +pub mod unstable { + pub use webcore::try_from::{ + TryFrom, + TryInto + }; + + pub use webcore::void::Void; +} + +#[doc(hidden)] +pub mod private { + pub use webcore::ffi::emscripten_asm_const_int; + pub use webcore::serialization::{ + JsSerializable, + JsSerializableOwned, + PreallocatedArena, + SerializedValue + }; + + pub use webcore::newtype::{ + IntoNewtype, + Newtype + }; + + pub use webcore::value::{ + FromReference, + FromReferenceUnchecked + }; + + #[cfg(feature = "serde")] + pub use ecosystem::serde::{ + to_value, + from_value + }; + + // This is to prevent an unused_mut warnings in macros, because an `allow` doesn't work apparently? + #[allow(dead_code)] + #[inline(always)] + pub fn noop< T >( _: &mut T ) {} +} diff --git a/src/webapi/date.rs b/src/webapi/date.rs new file mode 100644 index 00000000..0d9c795a --- /dev/null +++ b/src/webapi/date.rs @@ -0,0 +1,22 @@ +use std::marker::PhantomData; + +/// [(JavaScript docs)](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Date) +#[derive(Debug)] +pub struct Date { + dummy: PhantomData< () > +} + +impl Date { + /// The Date.now() method returns the number of milliseconds elapsed since 1 January 1970 00:00:00 UTC. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now) + pub fn now() -> f64 { + em_asm_double!( "return Date.now();" ) + } +} + +#[test] +fn test_date_now() { + let now = Date::now(); + assert!( now > 0.0 ); +} diff --git a/src/webapi/document.rs b/src/webapi/document.rs new file mode 100644 index 00000000..84d9ceae --- /dev/null +++ b/src/webapi/document.rs @@ -0,0 +1,97 @@ +use webcore::value::Reference; +use webapi::event_target::{IEventTarget, EventTarget}; +use webapi::node::{INode, Node}; +use webapi::element::Element; +use webapi::text_node::TextNode; +use webapi::node_list::NodeList; +use webapi::location::Location; + +/// The `Document` interface represents any web page loaded in the browser and +/// serves as an entry point into the web page's content, which is the DOM tree. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Document) +pub struct Document( Reference ); + +impl IEventTarget for Document {} +impl INode for Document {} + +reference_boilerplate! { + Document, + instanceof Document + convertible to EventTarget + convertible to Node +} + +/// A global instance of [Document](struct.Document.html). +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Document) +pub fn document() -> Document { + unsafe { js!( return document; ).into_reference_unchecked() }.unwrap() +} + +impl Document { + /// Returns the first [Element](struct.Element.html) within the document that matches the specified selector, or group of selectors. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector) + pub fn query_selector( &self, selector: &str ) -> Option< Element > { + // TODO: This can throw an exception in case of an invalid selector; + // convert the return type to a Result. + unsafe { + js!( return @{self}.querySelector( @{selector} ); ).into_reference_unchecked() + } + } + + /// Returns a list of the elements within the document (using depth-first + /// pre-order traversal of the document's nodes) that match the + /// specified group of selectors. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll) + pub fn query_selector_all( &self, selector: &str ) -> NodeList { + unsafe { + js!( return @{self}.querySelectorAll( @{selector} ); ).into_reference_unchecked().unwrap() + } + } + + /// Returns a reference to the element by its ID; the ID is a string which can + /// be used to uniquely identify the element, found in the HTML `id` attribute. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById) + pub fn get_element_by_id( &self, id: &str ) -> Option< Element > { + unsafe { + js!( return @{self}.getElementById( @{id} ); ).into_reference_unchecked() + } + } + + /// In an HTML document, the Document.createElement() method creates the HTML + /// element specified by `tag`, or an HTMLUnknownElement if `tag` isn't + /// recognized. In other documents, it creates an element with a null namespace URI. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement) + pub fn create_element( &self, tag: &str ) -> Element { + unsafe { + js!( return @{self}.createElement( @{tag} ); ).into_reference_unchecked().unwrap() + } + } + + /// Creates a new text node. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Document/createTextNode) + pub fn create_text_node( &self, text: &str ) -> TextNode { + unsafe { + js!( return @{self}.createTextNode( @{text} ); ).into_reference_unchecked().unwrap() + } + } + + /// Returns a [Location](struct.Location.html) object which contains + /// information about the URL of the document and provides methods + /// for changing that URL and loading another URL. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Document/location) + pub fn location( &self ) -> Option< Location > { + unsafe { + js!( + return @{self}.location; + ).into_reference_unchecked() + } + } +} diff --git a/src/webapi/element.rs b/src/webapi/element.rs new file mode 100644 index 00000000..fb8416ca --- /dev/null +++ b/src/webapi/element.rs @@ -0,0 +1,62 @@ +use webcore::value::Reference; +use webapi::event_target::{IEventTarget, EventTarget}; +use webapi::node::{INode, Node}; +use webapi::token_list::TokenList; +use webapi::node_list::NodeList; + +/// The `IElement` interface represents an object of a [Document](struct.Document.html). +/// This interface describes methods and properties common to all +/// kinds of elements. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Element) +pub trait IElement: IEventTarget { + /// The Element.classList is a read-only property which returns a live + /// [TokenList](struct.TokenList.html) collection of the class attributes + /// of the element. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) + fn class_list( &self ) -> TokenList { + unsafe { + js!( return @{self.as_ref()}.classList; ).into_reference_unchecked().unwrap() + } + } + + /// Returns the first element that is a descendant of the element on which it is + /// invoked that matches the specified group of selectors. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelector) + fn query_selector( &self, selector: &str ) -> Option< Element > { + // TODO: This can throw an exception in case of an invalid selector; + // convert the return type to a Result. + unsafe { + js!( return @{self.as_ref()}.querySelector( @{selector} ); ).into_reference_unchecked() + } + } + + /// Returns a non-live [NodeList](struct.NodeList.html) of all elements descended + /// from the element on which it is invoked that matches the specified group of CSS selectors. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelectorAll) + fn query_selector_all( &self, selector: &str ) -> NodeList { + unsafe { + js!( return @{self.as_ref()}.querySelectorAll( @{selector} ); ).into_reference_unchecked().unwrap() + } + } +} + +/// A reference to a JavaScript object which implements the [IElement](trait.IElement.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Element) +pub struct Element( Reference ); + +impl IEventTarget for Element {} +impl INode for Element {} +impl IElement for Element {} + +reference_boilerplate! { + Element, + instanceof Element + convertible to EventTarget + convertible to Node +} diff --git a/src/webapi/event.rs b/src/webapi/event.rs new file mode 100644 index 00000000..3f05dceb --- /dev/null +++ b/src/webapi/event.rs @@ -0,0 +1,307 @@ +use webcore::value::{Reference, Value}; +use webcore::try_from::{TryFrom, TryInto}; + +/// The `IEvent` interface represents any event which takes place in the DOM; some +/// are user-generated (such as mouse or keyboard events), while others are +/// generated by APIs (such as events that indicate an animation has finished +/// running, a video has been paused, and so forth). There are many types of event, +/// some of which use other interfaces based on the main `IEvent` interface. `IEvent` +/// itself contains the properties and methods which are common to all events. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Event) +pub trait IEvent: AsRef< Reference > + TryFrom< Value > { + /// Returns a string containing the type of event. It is set when + /// the event is constructed and is the name commonly used to refer + /// to the specific event. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Event/type) + fn event_type( &self ) -> String { + js!( return @{self.as_ref()}.type; ).try_into().unwrap() + } + + /// Cancels the event if it is cancelable, without + /// stopping further propagation of the event. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault) + fn prevent_default( &self ) { + js! { @(no_return) + @{self.as_ref()}.preventDefault(); + } + } +} + +pub trait ConcreteEvent: IEvent { + // TODO: Switch to an associated constant for `event_type` once they stabilize. + + /// Returns a string representing the event type. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Event/type) + fn static_event_type() -> &'static str; +} + +/// A reference to a JavaScript object which implements the [IEvent](trait.IEvent.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Event) +pub struct Event( Reference ); + +impl IEvent for Event {} + +reference_boilerplate! { + Event, + instanceof Event +} + +/// The `ChangeEvent` is fired for input, select, and textarea +/// elements when a change to the element's value is committed +/// by the user. Unlike the input event, the change event is not +/// necessarily fired for each change to an element's value. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/Events/change) +pub struct ChangeEvent( Reference ); + +impl IEvent for ChangeEvent {} +impl ConcreteEvent for ChangeEvent { + #[inline] + fn static_event_type() -> &'static str { + "change" + } +} + +reference_boilerplate! { + ChangeEvent, + instanceof Event + convertible to Event +} + +/// The `IUiEvent` interface represents simple user interface events. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent) +pub trait IUiEvent: IEvent { +} + +/// A reference to a JavaScript object which implements the [IUiEvent](trait.IUiEvent.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent) +pub struct UiEvent( Reference ); + +impl IEvent for UiEvent {} +impl IUiEvent for UiEvent {} + +reference_boilerplate! { + UiEvent, + instanceof UiEvent + convertible to Event +} + +/// The `IMouseEvent` interface represents events that occur due to the user +/// interacting with a pointing device (such as a mouse). +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) +pub trait IMouseEvent: IUiEvent { +} + +/// A reference to a JavaScript object which implements the [IMouseEvent](trait.IMouseEvent.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) +pub struct MouseEvent( Reference ); + +impl IEvent for MouseEvent {} +impl IUiEvent for MouseEvent {} +impl IMouseEvent for MouseEvent {} + +reference_boilerplate! { + MouseEvent, + instanceof MouseEvent + convertible to Event + convertible to UiEvent +} + +/// The `ClickEvent` is fired when a pointing device button (usually a +/// mouse's primary button) is pressed and released on a single element. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/Events/click) +pub struct ClickEvent( Reference ); + +impl IEvent for ClickEvent {} +impl IUiEvent for ClickEvent {} +impl IMouseEvent for ClickEvent {} +impl ConcreteEvent for ClickEvent { + #[inline] + fn static_event_type() -> &'static str { + "click" + } +} + +reference_boilerplate! { + ClickEvent, + instanceof MouseEvent + convertible to Event + convertible to UiEvent + convertible to MouseEvent +} + +/// The `DoubleClickEvent` is fired when a pointing device button +/// (usually a mouse's primary button) is clicked twice on a single +/// element. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/Events/dblclick) +pub struct DoubleClickEvent( Reference ); + +impl IEvent for DoubleClickEvent {} +impl IUiEvent for DoubleClickEvent {} +impl IMouseEvent for DoubleClickEvent {} +impl ConcreteEvent for DoubleClickEvent { + #[inline] + fn static_event_type() -> &'static str { + "dblclick" + } +} + +reference_boilerplate! { + DoubleClickEvent, + instanceof MouseEvent + convertible to Event + convertible to UiEvent + convertible to MouseEvent +} + +/// `IKeyboardEvent` objects describe a user interaction with the +/// keyboard. Each event describes a key; the event type identifies +/// what kind of activity was performed. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent) +pub trait IKeyboardEvent: IEvent { + /// Returns the value of a key or keys pressed by the user. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key) + fn key( &self ) -> String { + js!( return @{self.as_ref()}.key; ).into_string().unwrap() + } +} + +/// A reference to a JavaScript object which implements the [IKeyboardEvent](trait.IKeyboardEvent.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent) +pub struct KeyboardEvent( Reference ); + +impl IEvent for KeyboardEvent {} +impl IKeyboardEvent for KeyboardEvent {} + +reference_boilerplate! { + KeyboardEvent, + instanceof KeyboardEvent + convertible to Event +} + +/// The `KeypressEvent` is fired when a key is pressed down, and that +/// key normally produces a character value. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/Events/keypress) +pub struct KeypressEvent( Reference ); + +impl IEvent for KeypressEvent {} +impl IKeyboardEvent for KeypressEvent {} +impl ConcreteEvent for KeypressEvent { + #[inline] + fn static_event_type() -> &'static str { + "keypress" + } +} + +reference_boilerplate! { + KeypressEvent, + instanceof KeyboardEvent + convertible to Event + convertible to KeyboardEvent +} + +/// The `IFocusEvent` interface represents focus-related +/// events. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent) +pub trait IFocusEvent: IEvent { +} + +/// A reference to a JavaScript object which implements the [IFocusEvent](trait.IFocusEvent.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent) +pub struct FocusRelatedEvent( Reference ); + +impl IEvent for FocusRelatedEvent {} +impl IFocusEvent for FocusRelatedEvent {} + +reference_boilerplate! { + FocusRelatedEvent, + instanceof FocusEvent + convertible to Event +} + +/// The `FocusEvent` is fired when an element has received focus. The main +/// difference between this event and focusin is that only the latter bubbles. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/Events/focus) +pub struct FocusEvent( Reference ); + +impl IEvent for FocusEvent {} +impl IFocusEvent for FocusEvent {} +impl ConcreteEvent for FocusEvent { + #[inline] + fn static_event_type() -> &'static str { + "focus" + } +} + +reference_boilerplate! { + FocusEvent, + instanceof FocusEvent + convertible to Event + convertible to FocusRelatedEvent +} + +/// The `BlurEvent` is fired when an element has lost focus. The main difference +/// between this event and focusout is that only the latter bubbles. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/Events/blur) +pub struct BlurEvent( Reference ); + +impl IEvent for BlurEvent {} +impl IFocusEvent for BlurEvent {} +impl ConcreteEvent for BlurEvent { + #[inline] + fn static_event_type() -> &'static str { + "blur" + } +} + +reference_boilerplate! { + BlurEvent, + instanceof FocusEvent + convertible to Event + convertible to FocusRelatedEvent +} + +/// The `HashChangeEvent` is fired when the fragment +/// identifier of the URL has changed (the part of the URL +/// that follows the # symbol, including the # symbol). +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/Events/hashchange) +pub struct HashChangeEvent( Reference ); + +impl IEvent for HashChangeEvent {} +impl ConcreteEvent for HashChangeEvent { + #[inline] + fn static_event_type() -> &'static str { + "hashchange" + } +} + +reference_boilerplate! { + HashChangeEvent, + instanceof HashChangeEvent + convertible to Event +} diff --git a/src/webapi/event_target.rs b/src/webapi/event_target.rs new file mode 100644 index 00000000..9e5d7822 --- /dev/null +++ b/src/webapi/event_target.rs @@ -0,0 +1,73 @@ +use std::fmt; + +use webcore::value::Reference; +use webcore::try_from::TryInto; +use webapi::event::ConcreteEvent; + +pub struct EventListenerHandle { + event_type: &'static str, + reference: Reference, + listener_reference: Reference +} + +impl fmt::Debug for EventListenerHandle { + fn fmt( &self, formatter: &mut fmt::Formatter ) -> fmt::Result { + write!( formatter, "EventListenerHandle {{ event_type: {}, reference: {:?} }}", self.event_type, self.reference ) + } +} + +impl EventListenerHandle { + /// Removes the handler from the [IEventTarget](trait.IEventTarget.html) on + /// which it was previously registered. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener) + pub fn remove( self ) { + js! { @(no_return) + var self = @{self.reference}; + var event_type = @{self.event_type}; + var listener = @{self.listener_reference}; + listener.drop(); + self.removeEventListener( event_type, listener ); + } + } +} + +/// `IEventTarget` is an interface implemented by objects that +/// can receive events and may have listeners for them. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) +pub trait IEventTarget: AsRef< Reference > { + /// Adds given event handler to the list the list of event listeners for + /// the specified `EventTarget` on which it's called. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) + fn add_event_listener< T, F >( &self, listener: F ) -> EventListenerHandle + where T: ConcreteEvent, F: FnMut( T ) + 'static + { + let reference = self.as_ref(); + let listener_reference = js! { + var listener = @{listener}; + @{reference}.addEventListener( @{T::static_event_type()}, listener ); + return listener; + }.try_into().unwrap(); + + EventListenerHandle { + event_type: T::static_event_type(), + reference: reference.clone(), + listener_reference: listener_reference + } + } +} + +/// A reference to a JavaScript object which implements the [IEventTarget](trait.IEventTarget.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) +pub struct EventTarget( Reference ); + +impl IEventTarget for EventTarget {} + +reference_boilerplate! { + EventTarget, + instanceof EventTarget +} diff --git a/src/webapi/global.rs b/src/webapi/global.rs new file mode 100644 index 00000000..6c63a1a9 --- /dev/null +++ b/src/webapi/global.rs @@ -0,0 +1,12 @@ +use webapi::window::window; +use webapi::window_or_worker::IWindowOrWorker; + +/// An alias for [window.set_timeout](struct.Window.html#method.set_timeout). +pub fn set_timeout< F: FnOnce() >( callback: F, timeout: u32 ) { + window().set_timeout( callback, timeout ); +} + +/// An alias for [window.alert](struct.Window.html#method.alert). +pub fn alert( message: &str ) { + window().alert( message ); +} diff --git a/src/webapi/html_element.rs b/src/webapi/html_element.rs new file mode 100644 index 00000000..0f0fefb8 --- /dev/null +++ b/src/webapi/html_element.rs @@ -0,0 +1,59 @@ +use webcore::value::Reference; +use webapi::event_target::{IEventTarget, EventTarget}; +use webapi::node::{INode, Node}; +use webapi::element::{IElement, Element}; +use webapi::string_map::StringMap; + +/// The `IHtmlElement` interface represents any HTML element. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) +pub trait IHtmlElement: IElement { + /// Sets focus on the specified element, if it can be focused. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) + fn focus( &self ) { + js! { @(no_return) + @{self.as_ref()}.focus(); + } + } + + /// Removes keyboard focus from the current element. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur) + fn blur( &self ) { + js! { @(no_return) + @{self.as_ref()}.blur(); + } + } + + /// Allows access, both in reading and writing, to all of the custom data attributes (data-*) + /// set on the element, either in HTML or in the DOM. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) + fn dataset( &self ) -> StringMap { + unsafe { + js!( + return @{self.as_ref()}.dataset; + ).into_reference_unchecked().unwrap() + } + } +} + +/// A reference to a JavaScript object which implements the [IHtmlElement](trait.IHtmlElement.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) +pub struct HtmlElement( Reference ); + +impl IEventTarget for HtmlElement {} +impl INode for HtmlElement {} +impl IElement for HtmlElement {} +impl IHtmlElement for HtmlElement {} + +reference_boilerplate! { + HtmlElement, + instanceof HTMLElement + convertible to EventTarget + convertible to Node + convertible to Element +} diff --git a/src/webapi/html_elements/input.rs b/src/webapi/html_elements/input.rs new file mode 100644 index 00000000..49c3cb08 --- /dev/null +++ b/src/webapi/html_elements/input.rs @@ -0,0 +1,52 @@ +use webcore::value::{Value, Reference}; +use webapi::event_target::{IEventTarget, EventTarget}; +use webapi::node::{INode, Node}; +use webapi::element::{IElement, Element}; +use webapi::html_element::{IHtmlElement, HtmlElement}; + +/// The HTML input element is used to create interactive controls +/// for web-based forms in order to accept data from the user. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en/docs/Web/HTML/Element/input) +pub struct InputElement( Reference ); + +impl IEventTarget for InputElement {} +impl INode for InputElement {} +impl IElement for InputElement {} +impl IHtmlElement for InputElement {} + +reference_boilerplate! { + InputElement, + instanceof HTMLInputElement + convertible to EventTarget + convertible to Node + convertible to Element + convertible to HtmlElement +} + +impl InputElement { + /// The value of the control. This attribute is optional except when the input is a radio button or a checkbox. + #[inline] + pub fn value( &self ) -> Value { + js! ( + return @{self}.value; + ) + } + + /// Sets the value of the control. + #[inline] + pub fn set_value< T: Into< Value > >( &self, value: T ) { + js! { @(no_return) + @{self}.value = @{value.into()}; + } + } + + /// The type of control to render. See [Form types](https://developer.mozilla.org/en/docs/Web/HTML/Element/input#Form__types) + /// for the individual types, with links to more information about each. + #[inline] + pub fn set_kind( &self, kind: &str ) { + js! { @(no_return) + @{self}.type = @{kind}; + } + } +} diff --git a/src/webapi/html_elements/mod.rs b/src/webapi/html_elements/mod.rs new file mode 100644 index 00000000..e41ee3cb --- /dev/null +++ b/src/webapi/html_elements/mod.rs @@ -0,0 +1,3 @@ +mod input; + +pub use self::input::InputElement; diff --git a/src/webapi/location.rs b/src/webapi/location.rs new file mode 100644 index 00000000..a4e17e38 --- /dev/null +++ b/src/webapi/location.rs @@ -0,0 +1,37 @@ +use webcore::value::Reference; +use webcore::try_from::TryInto; + +/// The `Location` interface represents the location (URL) of the object it +/// is linked to. Changes done on it are reflected on the object it relates +/// to. Both the [Document](struct.Document.html) and [Window](struct.Window.html) +/// interface have such a linked `Location`, accessible via [Document::location](struct.Document.html#method.location) +/// and [Window::location](struct.Window.html#method.location) respectively. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Location) +pub struct Location( Reference ); + +reference_boilerplate! { + Location, + instanceof Location +} + +impl Location { + /// The entire URL. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Location/href) + pub fn href( &self ) -> String { + js!( + return @{self}.href; + ).try_into().unwrap() + } + + /// Returns a `String` containing a '#' followed by the fragment + /// identifier of the URL. The fragment is not percent-decoded. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Location/hash) + pub fn hash( &self ) -> String { + js!( + return @{self}.hash; + ).try_into().unwrap() + } +} diff --git a/src/webapi/mod.rs b/src/webapi/mod.rs new file mode 100644 index 00000000..0457fb54 --- /dev/null +++ b/src/webapi/mod.rs @@ -0,0 +1,17 @@ +pub mod global; +pub mod date; +pub mod document; +pub mod window; +pub mod event; +pub mod event_target; +pub mod node; +pub mod element; +pub mod html_element; +pub mod html_elements; +pub mod window_or_worker; +pub mod token_list; +pub mod text_node; +pub mod node_list; +pub mod string_map; +pub mod location; +pub mod storage; diff --git a/src/webapi/node.rs b/src/webapi/node.rs new file mode 100644 index 00000000..8c77a86e --- /dev/null +++ b/src/webapi/node.rs @@ -0,0 +1,197 @@ +use std::fmt; +use std::error; + +use webcore::value::{Reference, FromReference}; +use webcore::try_from::TryInto; +use webapi::event_target::{IEventTarget, EventTarget}; +use webapi::node_list::NodeList; + +/// A structure denoting that the specified DOM [Node](trait.INode.html) was not found. +#[derive(Debug)] +pub struct NotFoundError( String ); +impl error::Error for NotFoundError { + fn description( &self ) -> &str { + self.0.as_str() + } +} + +impl fmt::Display for NotFoundError { + fn fmt( &self, formatter: &mut fmt::Formatter ) -> fmt::Result { + write!( formatter, "{}", self.0 ) + } +} + +/// An enum which determines whenever the DOM [Node](trait.INode.html)'s children will also be cloned or not. +/// +/// Mainly used in [INode::clone_node](trait.INode.html#method.clone_node). +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum CloneKind { + /// Will not clone the children. + Shallow, + /// Will clone the children. + Deep +} + +/// `INode` is an interface from which a number of DOM API object types inherit. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node) +pub trait INode: IEventTarget + FromReference { + /// Adds a node to the end of the list of children of a specified parent node. + /// + /// If the given child is a reference to an existing node in the document then + /// it is moved from its current position to the new position (there is no requirement + /// to remove the node from its parent node before appending it to some other node). + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild) + fn append_child< T: INode >( &self, child: &T ) { + js! { @(no_return) + @{self.as_ref()}.appendChild( @{child.as_ref()} ); + } + } + + /// Removes a child node from the DOM. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild) + fn remove_child< T: INode >( &self, child: &T ) -> Result< (), NotFoundError > { + // TODO: Return the removed node. + let status = js! { + try { + @{self.as_ref()}.removeChild( @{child.as_ref()} ); + return true; + } catch( exception ) { + if( exception instanceof NotFoundError ) { + return false; + } else { + throw exception; + } + } + }; + + if status == true { + Ok(()) + } else { + Err( NotFoundError( "The node to be removed is not a child of this node.".to_owned() ) ) + } + } + + /// Returns a duplicate of the node on which this method was called. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/cloneNode) + fn clone_node( &self, kind: CloneKind ) -> Self { + let is_deep = match kind { + CloneKind::Deep => true, + CloneKind::Shallow => false + }; + + let cloned = js! { + return @{self.as_ref()}.cloneNode( @{is_deep} ); + }; + + cloned.into_reference().unwrap().downcast::< Self >().unwrap() + } + + /// Checks whenever a given node is a descendant of this one or not. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/contains) + fn contains< T: INode >( &self, node: &T ) -> bool { + js!( + return @{self.as_ref()}.contains( @{node.as_ref()} ); + ).try_into().unwrap() + } + + /// Inserts the specified node before the reference node as a child of the current node. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore) + fn insert_before< T: INode, U: INode >( &self, new_node: &T, reference_node: &U ) { + js! { @(no_return) + @{self.as_ref()}.insertBefore( @{new_node.as_ref()}, @{reference_node.as_ref()} ); + } + } + + /// Replaces one hild node of the specified node with another. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/replaceChild) + fn replace_child< T: INode, U: INode >( &self, new_child: &T, old_child: &U ) { + js! { @(no_return) + @{self.as_ref()}.replaceChild( @{new_child.as_ref()}, @{old_child.as_ref()} ); + } + } + + /// Returns the parent of this node in the DOM tree. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/parentNode) + fn parent_node( &self ) -> Option< Node > { + js!( + return @{self.as_ref()}.parentNode; + ).try_into().ok() + } + + /// Returns the node's first child in the tree, or `None` if the node is childless. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en/docs/Web/API/Node/firstChild) + fn first_child( &self ) -> Option< Node > { + js!( + return @{self.as_ref()}.firstChild; + ).try_into().ok() + } + + /// A property which represents the "rendered" text content of a node and its descendants. + /// It approximates the text the user would get if they highlighted the contents of the element + /// with the cursor and then copied to the clipboard. + /// + /// This feature was originally introduced by Internet Explorer, and was formally specified in the HTML + /// standard in 2016 after being adopted by all major browser vendors. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/innerText) + fn inner_text( &self ) -> String { + js!( + return @{self.as_ref()}.innerText; + ).try_into().unwrap() + } + + /// A property which represents the text content of a node and its descendants. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) + fn text_content( &self ) -> Option< String > { + js!( + return @{self.as_ref()}.textContent; + ).try_into().unwrap() + } + + /// Sets the text content of this node; calling thil removes all + /// of node's children and replaces them with a single text node + /// with the given value. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) + fn set_text_content( &self, text: &str ) { + js! { @(no_return) + @{self.as_ref()}.textContent = @{text}; + } + } + + /// Returns a live collection of child nodes of this node. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node/childNodes) + fn child_nodes( &self ) -> NodeList { + unsafe { + js!( + return @{self.as_ref()}.childNodes; + ).into_reference_unchecked().unwrap() + } + } +} + +/// A reference to a JavaScript object which implements the [INode](trait.INode.html) +/// interface. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Node) +pub struct Node( Reference ); + +impl IEventTarget for Node {} +impl INode for Node {} + +reference_boilerplate! { + Node, + instanceof Node + convertible to EventTarget +} \ No newline at end of file diff --git a/src/webapi/node_list.rs b/src/webapi/node_list.rs new file mode 100644 index 00000000..3da31b0e --- /dev/null +++ b/src/webapi/node_list.rs @@ -0,0 +1,91 @@ +use webcore::value::{Value, Reference, FromReferenceUnchecked}; +use webcore::try_from::TryInto; +use webapi::node::Node; + +/// `NodeList` objects are collections of nodes such as those returned by properties +/// such as [INode::child_nodes](trait.INode.html#method.child_nodes) and the +/// [Document::query_selector_all](struct.Document.html#method.query_selector_all) method. +/// +/// In some cases, the `NodeList` is a live collection, which means that changes in the DOM +/// are reflected in the collection - for example [INode::child_nodes](trait.INode.html#method.child_nodes) is live. +/// +/// In other cases, the `NodeList` is a static collection, meaning any subsequent change +/// in the DOM does not affect the content of the collection - for example +/// [Document::query_selector_all](struct.Document.html#method.query_selector_all) returns +/// a static `NodeList`. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) +pub struct NodeList( Reference ); + +reference_boilerplate! { + NodeList, + instanceof NodeList +} + +impl NodeList { + /// Returns the number of [Node](struct.Node.html)s contained in this list. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/NodeList/length) + pub fn len( &self ) -> usize { + let length: i32 = js!( return @{self}.length; ).try_into().unwrap(); + length as usize + } + + /// Returns an iterator over the list. + pub fn iter( &self ) -> NodeIter { + NodeIter { + list: self.clone(), + index: 0 + } + } +} + +impl IntoIterator for NodeList { + type Item = Node; + type IntoIter = NodeIter; + + #[inline] + fn into_iter( self ) -> Self::IntoIter { + NodeIter { + list: self, + index: 0 + } + } +} + +impl< 'a > IntoIterator for &'a NodeList { + type Item = Node; + type IntoIter = NodeIter; + + #[inline] + fn into_iter( self ) -> Self::IntoIter { + NodeIter { + list: self.clone(), + index: 0 + } + } +} + +#[derive(Debug)] +pub struct NodeIter { + list: NodeList, + index: i32 +} + +impl Iterator for NodeIter { + type Item = Node; + fn next( &mut self ) -> Option< Self::Item > { + let value = js!( + return @{&self.list}[ @{self.index} ]; + ); + + let node = match value { + Value::Undefined => return None, + Value::Reference( reference ) => unsafe { Node::from_reference_unchecked( reference ) }, + _ => unreachable!() + }; + + self.index += 1; + Some( node ) + } +} diff --git a/src/webapi/storage.rs b/src/webapi/storage.rs new file mode 100644 index 00000000..70fa5620 --- /dev/null +++ b/src/webapi/storage.rs @@ -0,0 +1,69 @@ +use webcore::value::Reference; +use webcore::try_from::TryInto; + +/// The `Storage` interface of the Web Storage API provides access to +/// the session storage or local storage for a particular domain. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Storage) +pub struct Storage( Reference ); + +reference_boilerplate! { + Storage, + instanceof Storage +} + +impl Storage { + /// Gets the number of data items stored in the `Storage` object. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Storage/length) + pub fn len( &self ) -> usize { + let length: i32 = js!( return @{self}.length; ).try_into().unwrap(); + length as usize + } + + /// Returns a value corresponding to the key. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem) + pub fn get( &self, key: &str ) -> Option< String > { + js!( return @{self}.getItem( @{key} ); ).try_into().ok() + } + + /// Inserts a key-value pair into the storage. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem) + pub fn insert( &self, key: &str, value: &str ) { + js!( @(no_return) + @{self}.setItem( @{key}, @{value} ); + ); + } + + /// Removes a key from the storage. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Storage/removeItem) + pub fn remove( &self, key: &str ) { + js!( @(no_return) + @{self}.removeItem( @{key} ); + ); + } + + /// When invoked, will empty all keys out of the storage. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Storage/clear) + pub fn clear( &self ) { + js!( @(no_return) + @{self}.clear(); + ); + } + + /// Return the name of the nth key in the storage. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Storage/key) + pub fn key( &self, nth: usize ) -> Option< String > { + js!( return @{self}.key( @{nth as u32} ); ).try_into().ok() + } + + /// Returns true if the storage contains a value for the specified key. + pub fn contains_key( &self, key: &str ) -> bool { + js!( return !!@{self}.getItem( @{key} ); ).try_into().unwrap() + } +} \ No newline at end of file diff --git a/src/webapi/string_map.rs b/src/webapi/string_map.rs new file mode 100644 index 00000000..aee24402 --- /dev/null +++ b/src/webapi/string_map.rs @@ -0,0 +1,39 @@ +use webcore::value::Reference; +use webcore::try_from::TryInto; + +/// Used by the `dataset` HTML attribute to represent data for custom attributes added to elements. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/DOMStringMap) +pub struct StringMap( Reference ); + +reference_boilerplate! { + StringMap, + instanceof DOMStringMap +} + +// The methods here are deliberately named exactly as those from Rust's HashMap. +impl StringMap { + /// Returns a value corresponding to the key. + pub fn get( &self, key: &str ) -> Option< String > { + js!( return @{self}[ @{key} ]; ).try_into().ok() + } + + /// Inserts a key-value pair into the map. + pub fn insert( &self, key: &str, value: &str ) { + js!( @(no_return) + @{self}[ @{key} ] = @{value}; + ); + } + + /// Removes a key from the map. + pub fn remove( &self, key: &str ) { + js!( @(no_return) + delete @{self}[ @{key} ]; + ); + } + + /// Returns true if the map contains a value for the specified key. + pub fn contains_key( &self, key: &str ) -> bool { + js!( return @{key} in @{self}; ).try_into().unwrap() + } +} diff --git a/src/webapi/text_node.rs b/src/webapi/text_node.rs new file mode 100644 index 00000000..3fd2bc31 --- /dev/null +++ b/src/webapi/text_node.rs @@ -0,0 +1,23 @@ +use webcore::value::Reference; +use webapi::event_target::{IEventTarget, EventTarget}; +use webapi::node::{INode, Node}; + +/// The `TextNode` represents the textual content of an [IElement](trait.IElement.html) +/// +/// If an element has no markup within its content, it has +/// a single child `TextNode` that contains the element's +/// text. However, if the element contains markup, it is parsed +/// into information items and `TextNode`s that form its children. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Text) +pub struct TextNode( Reference ); + +impl IEventTarget for TextNode {} +impl INode for TextNode {} + +reference_boilerplate! { + TextNode, + instanceof Text + convertible to EventTarget + convertible to Node +} diff --git a/src/webapi/token_list.rs b/src/webapi/token_list.rs new file mode 100644 index 00000000..7356ee33 --- /dev/null +++ b/src/webapi/token_list.rs @@ -0,0 +1,47 @@ +use webcore::value::Reference; +use webcore::try_from::TryInto; + +/// The `TokenList` represents a set of space-separated tokens. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList) +pub struct TokenList( Reference ); + +reference_boilerplate! { + TokenList, + instanceof DOMTokenList +} + +impl TokenList { + /// Gets the number of tokens in the list. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList/length) + pub fn len( &self ) -> usize { + let length: i32 = js!( return @{self}.length; ).try_into().unwrap(); + length as usize + } + + /// Adds token to the underlying string. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList/add) + pub fn add( &self, token: &str ) { + js! { @(no_return) + @{self}.add( @{token} ); + } + } + + /// Removes token from the underlying string. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList/remove) + pub fn remove( &self, token: &str ) { + js! { @(no_return) + @{self}.remove( @{token} ); + } + } + + /// Returns `true` if the underlying string contains token, otherwise `false`. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList/contains) + pub fn contains( &self, token: &str ) -> bool { + js!( return @{self}.contains( @{token} ); ).try_into().unwrap() + } +} \ No newline at end of file diff --git a/src/webapi/window.rs b/src/webapi/window.rs new file mode 100644 index 00000000..cc41a4b2 --- /dev/null +++ b/src/webapi/window.rs @@ -0,0 +1,89 @@ +use webcore::value::Reference; +use webapi::event_target::{IEventTarget, EventTarget}; +use webapi::window_or_worker::IWindowOrWorker; +use webapi::storage::Storage; +use webapi::location::Location; + +/// The `Window` object represents a window containing a DOM document. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Window) +pub struct Window( Reference ); + +impl IEventTarget for Window {} +impl IWindowOrWorker for Window {} + +reference_boilerplate! { + Window, + instanceof Window + convertible to EventTarget +} + +/// A global instance of [Window](struct.Window.html). +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Window) +pub fn window() -> Window { + unsafe { js!( return window; ).into_reference_unchecked() }.unwrap() +} + +impl Window { + /// The Window.alert() method displays an alert dialog + /// with the optional specified content and an OK button. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Window/alert) + pub fn alert( &self, message: &str ) { + js!( @(no_return) + @{self}.alert( @{message} ); + ); + } + + /// The `local_storage` property allows you to access a local [Storage](struct.Storage.html) + /// object. + /// + /// It is similar to the [Window::session_storage](struct.Window.html#method.session_storage). + /// The only difference is that, while data stored in `local_storage` has + /// no expiration time, data stored in `session_storage` gets cleared when + /// the browsing session ends - that is, when the browser is closed. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) + pub fn local_storage( &self ) -> Storage { + unsafe { + js!( + return @{self.as_ref()}.localStorage; + ).into_reference_unchecked().unwrap() + } + } + + /// The `session_storage` property allows you to access a session [Storage](struct.Storage.html) + /// object for the current origin. + /// + /// It is similar to the [Window::local_storage](struct.Window.html#method.local_storage), + /// The only difference is that, while data stored in `local_storage` has + /// no expiration time, data stored in `session_storage` gets cleared when + /// the browsing session ends. + /// + /// A page session lasts for as long as the browser is open and survives over + /// page reloads and restores. Opening a page in a new tab or window will cause + /// a new session to be initiated, which differs from how session cookies work. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) + pub fn session_storage( &self ) -> Storage { + unsafe { + js!( + return @{self.as_ref()}.sessionStorage; + ).into_reference_unchecked().unwrap() + } + } + + /// Returns a [Location](struct.Location.html) object which contains + /// information about the URL of the document and provides methods + /// for changing that URL and loading another URL. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Window/location) + pub fn location( &self ) -> Option< Location > { + unsafe { + js!( + return @{self}.location; + ).into_reference_unchecked() + } + } +} diff --git a/src/webapi/window_or_worker.rs b/src/webapi/window_or_worker.rs new file mode 100644 index 00000000..24d631d3 --- /dev/null +++ b/src/webapi/window_or_worker.rs @@ -0,0 +1,27 @@ +use webcore::value::Reference; + +extern fn funcall_adapter< F: FnOnce() >( callback: *mut F ) { + let callback = unsafe { + Box::from_raw( callback ) + }; + + callback(); +} + +/// The `IWindowOrWorker` mixin describes several features common to +/// the `Window` and the global scope of web workers. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope) +pub trait IWindowOrWorker: AsRef< Reference > { + /// Sets a timer which executes a function once after the timer expires. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout) + fn set_timeout< F: FnOnce() >( &self, callback: F, timeout: u32 ) { + let callback = Box::into_raw( Box::new( callback ) ); + em_asm_int!( "\ + Module.STDWEB.acquire_js_reference( $0 ).setTimeout( function() {\ + Runtime.dynCall( 'vi', $1, [$2] );\ + }, $3 );\ + ", self.as_ref().as_raw(), funcall_adapter::< F > as extern fn( *mut F ), callback, timeout ); + } +} diff --git a/src/webcore/callfn.rs b/src/webcore/callfn.rs new file mode 100644 index 00000000..7eb1925a --- /dev/null +++ b/src/webcore/callfn.rs @@ -0,0 +1,73 @@ +// Since the {Fn, FnMut, FnOnce} traits are unstable we need +// to define our own versions. +// +// I'd put this in a separate crate, but we need to keep it +// here to allow Rust to do a better job of checking impl exhaustiveness, +// otherwise if we pull these traits from another crate we'll have +// `conflicting implementations of trait` errors. +pub trait CallOnce< Args > { + type Output; + fn call_once( self, args: Args ) -> Self::Output; + fn expected_argument_count() -> usize; +} + +pub trait CallMut< Args >: CallOnce< Args > { + fn call_mut( &mut self, args: Args ) -> Self::Output; +} + +pub trait Call< Args >: CallMut< Args > { + fn call( &self, args: Args ) -> Self::Output; +} + +macro_rules! noop { + ($token:tt) => {} +} + +macro_rules! define { + ($next:tt => $($kind:ident),*) => { + impl< R, $($kind,)* F: FnOnce( $($kind,)* ) -> R > CallOnce< ($($kind,)*) > for F { + type Output = R; + #[inline] + fn call_once( self, args: ($($kind,)*) ) -> Self::Output { + #[allow(non_snake_case)] + let ($($kind,)*) = args; + self( $($kind),* ) + } + + #[inline] + fn expected_argument_count() -> usize { + let mut count = 0; + $( + // I'm too lazy to make a separate macro to count the tokens so we just do this. + count += 1; + noop!( $kind ); + )* + + $crate::private::noop( &mut count ); + count + } + } + + impl< R, $($kind,)* F: FnMut( $($kind,)* ) -> R > CallMut< ($($kind,)*) > for F { + #[inline] + fn call_mut( &mut self, args: ($($kind,)*) ) -> Self::Output { + #[allow(non_snake_case)] + let ($($kind,)*) = args; + self( $($kind),* ) + } + } + + impl< R, $($kind,)* F: Fn( $($kind,)* ) -> R > Call< ($($kind,)*) > for F { + #[inline] + fn call( &self, args: ($($kind,)*) ) -> Self::Output { + #[allow(non_snake_case)] + let ($($kind,)*) = args; + self( $($kind),* ) + } + } + + next! { $next } + } +} + +loop_through_identifiers!( define ); diff --git a/src/webcore/ffi.rs b/src/webcore/ffi.rs new file mode 100644 index 00000000..ac7c9698 --- /dev/null +++ b/src/webcore/ffi.rs @@ -0,0 +1,9 @@ +pub type CallbackFn = Option< unsafe extern "C" fn() >; + +extern "C" { + pub fn free( pointer: *const u8 ); + pub fn emscripten_asm_const_int( code: *const u8, ... ) -> i32; + pub fn emscripten_asm_const_double( code: *const u8, ... ) -> f64; + pub fn emscripten_pause_main_loop(); + pub fn emscripten_set_main_loop( callback: CallbackFn, fps: i32, simulate_infinite_loop: i32 ); +} diff --git a/src/webcore/initialization.rs b/src/webcore/initialization.rs new file mode 100644 index 00000000..be0460c6 --- /dev/null +++ b/src/webcore/initialization.rs @@ -0,0 +1,261 @@ +use std::panic; +use webcore::ffi; + +/// Initializes the library. +/// +/// Calling this is required for anything to work. +pub fn initialize() { + static mut INITIALIZED: bool = false; + unsafe { + if INITIALIZED { + return; + } + + INITIALIZED = true; + } + + js! { @(no_return) + Module.STDWEB = {}; + Module.STDWEB.to_js = function to_js( address ) { + var kind = HEAPU8[ address + 12 ]; + if( kind === 0 ) { + return undefined; + } else if( kind === 1 ) { + return null; + } else if( kind === 2 ) { + return HEAP32[ address / 4 ]; + } else if( kind === 3 ) { + return HEAPF64[ address / 8 ]; + } else if( kind === 4 ) { + var pointer = HEAPU32[ address / 4 ]; + var length = HEAPU32[ (address + 4) / 4 ]; + return Module.STDWEB.to_js_string( pointer, length ); + } else if( kind === 5 ) { + return false; + } else if( kind === 6 ) { + return true; + } else if( kind === 7 ) { + var pointer = HEAPU32[ address / 4 ]; + var length = HEAPU32[ (address + 4) / 4 ]; + var output = []; + for( var i = 0; i < length; ++i ) { + output.push( Module.STDWEB.to_js( pointer + i * 16 ) ); + } + return output; + } else if( kind === 8 ) { + var value_array_pointer = HEAPU32[ address / 4 ]; + var length = HEAPU32[ (address + 4) / 4 ]; + var key_array_pointer = HEAPU32[ (address + 8) / 4 ]; + var output = {}; + for( var i = 0; i < length; ++i ) { + var key_pointer = HEAPU32[ (key_array_pointer + i * 8) / 4 ]; + var key_length = HEAPU32[ (key_array_pointer + 4 + i * 8) / 4 ]; + var key = Module.STDWEB.to_js_string( key_pointer, key_length ); + var value = Module.STDWEB.to_js( value_array_pointer + i * 16 ); + output[ key ] = value; + } + return output; + } else if( kind === 9 ) { + return Module.STDWEB.acquire_js_reference( HEAP32[ address / 4 ] ); + } else if( kind === 10 ) { + var adapter_pointer = HEAPU32[ address / 4 ]; + var pointer = HEAPU32[ (address + 4) / 4 ]; + var deallocator_pointer = HEAPU32[ (address + 8) / 4 ]; + var output = function() { + var args = _malloc( 16 ); + Module.STDWEB.from_js( args, arguments ); + Runtime.dynCall( "vii", adapter_pointer, [pointer, args] ); + var result = Module.STDWEB.tmp; + Module.STDWEB.tmp = null; + + return result; + }; + + output.drop = function() { + output.drop = null; + Runtime.dynCall( "vi", deallocator_pointer, [pointer] ); + }; + + return output; + } + }; + }; + + js! { @(no_return) + Module.STDWEB.from_js = function from_js( address, value ) { + var kind = Object.prototype.toString.call( value ); + if( kind === "[object String]" ) { + var length = lengthBytesUTF8( value ); + var pointer = _malloc( length + 1 ); + stringToUTF8( value, pointer, length + 1 ); + HEAPU8[ address + 12 ] = 4; + HEAPU32[ address / 4 ] = pointer; + HEAPU32[ (address + 4) / 4 ] = length; + } else if( kind === "[object Number]" ) { + if( value === (value|0) ) { + HEAPU8[ address + 12 ] = 2; + HEAP32[ address / 4 ] = value; + } else { + HEAPU8[ address + 12 ] = 3; + HEAPF64[ address / 8 ] = value; + } + } else if( value === null ) { + HEAPU8[ address + 12 ] = 1; + } else if( value === undefined ) { + HEAPU8[ address + 12 ] = 0; + } else if( value === false ) { + HEAPU8[ address + 12 ] = 5; + } else if( value === true ) { + HEAPU8[ address + 12 ] = 6; + } else if( kind === "[object Array]" || kind === "[object Arguments]" ) { + var length = value.length; + var pointer = _malloc( length * 16 ); + HEAPU8[ address + 12 ] = 7; + HEAPU32[ address / 4 ] = pointer; + HEAPU32[ (address + 4) / 4 ] = length; + for( var i = 0; i < length; ++i ) { + Module.STDWEB.from_js( pointer + i * 16, value[ i ] ); + } + } else if( kind === "[object Object]" ) { + var keys = Object.keys( value ); + var length = keys.length; + var key_array_pointer = _malloc( length * 8 ); + var value_array_pointer = _malloc( length * 16 ); + HEAPU8[ address + 12 ] = 8; + HEAPU32[ address / 4 ] = value_array_pointer; + HEAPU32[ (address + 4) / 4 ] = length; + HEAPU32[ (address + 8) / 4 ] = key_array_pointer; + for( var i = 0; i < length; ++i ) { + var key = keys[ i ]; + var key_length = lengthBytesUTF8( key ); + var key_pointer = _malloc( key_length + 1 ); + stringToUTF8( key, key_pointer, key_length + 1 ); + + var key_address = key_array_pointer + i * 8; + HEAPU32[ key_address / 4 ] = key_pointer; + HEAPU32[ (key_address + 4) / 4 ] = key_length; + + Module.STDWEB.from_js( value_array_pointer + i * 16, value[ key ] ); + } + } else { + var refid = Module.STDWEB.acquire_rust_reference( value ); + HEAPU8[ address + 12 ] = 9; + HEAP32[ address / 4 ] = refid; + } + }; + }; + + js! { @(no_return) + // This is ported from Rust's stdlib; it's faster than + // the string conversion from Emscripten. + Module.STDWEB.to_js_string = function to_js_string( index, length ) { + index = index|0; + length = length|0; + var end = (index|0) + (length|0); + var output = ""; + while( index < end ) { + var x = HEAPU8[ index++ ]; + if( x < 128 ) { + output += String.fromCharCode( x ); + continue; + } + var init = (x & (0x7F >> 2)); + var y = 0; + if( index < end ) { + y = HEAPU8[ index++ ]; + } + var ch = (init << 6) | (y & 63); + if( x >= 0xE0 ) { + let z = 0; + if( index < end ) { + z = HEAPU8[ index++ ]; + } + let y_z = ((y & 63) << 6) | (z & 63); + ch = init << 12 | y_z; + if( x >= 0xF0 ) { + let w = 0; + if( index < end ) { + w = HEAPU8[ index++ ]; + } + ch = (init & 7) << 18 | ((y_z << 6) | (w & 63)); + } + } + output += String.fromCharCode( ch ); + continue; + } + return output; + }; + }; + + js! { @(no_return) + var id_to_ref_map = {}; + var id_to_refcount_map = {}; + var ref_to_id_map = new WeakMap(); + var last_refid = 1; + + Module.STDWEB.acquire_rust_reference = function( reference ) { + if( reference === undefined || reference === null ) { + return 0; + } + + var refid = ref_to_id_map.get( reference ); + if( refid === undefined ) { + refid = last_refid++; + ref_to_id_map.set( reference, refid ); + id_to_ref_map[ refid ] = reference; + id_to_refcount_map[ refid ] = 1; + } else { + id_to_refcount_map[ refid ]++; + } + + return refid; + }; + + Module.STDWEB.acquire_js_reference = function( refid ) { + return id_to_ref_map[ refid ]; + }; + + Module.STDWEB.increment_refcount = function( refid ) { + id_to_refcount_map[ refid ]++; + }; + + Module.STDWEB.decrement_refcount = function( refid ) { + id_to_refcount_map[ refid ]--; + if( id_to_refcount_map[ refid ] === 0 ) { + var reference = id_to_ref_map[ refid ]; + delete id_to_ref_map[ refid ]; + delete id_to_refcount_map[ refid ]; + ref_to_id_map.delete( reference ); + } + }; + } + + if cfg!( test ) == false { + panic::set_hook( Box::new( |info| { + em_asm_int!( "console.error( 'Encountered a panic!' );" ); + if let Some( value ) = info.payload().downcast_ref::< String >() { + em_asm_int!( "\ + console.error( 'Panic error message:', Module.STDWEB.to_js_string( $0, $1 ) );\ + ", value.as_ptr(), value.len() ); + } + if let Some( location ) = info.location() { + let file = location.file(); + em_asm_int!( "\ + console.error( 'Panic location:', Module.STDWEB.to_js_string( $0, $1 ) + ':' + $2 );\ + ", file.as_ptr(), file.len(), location.line() ); + } + })); + } +} + +/// Runs the event loop. +/// +/// You should call this before returning from `main()`, +/// otherwise bad things will happen. +pub fn event_loop() -> ! { + unsafe { + ffi::emscripten_set_main_loop( Some( ffi::emscripten_pause_main_loop ), 0, 1 ); + } + + unreachable!(); +} diff --git a/src/webcore/macros.rs b/src/webcore/macros.rs new file mode 100644 index 00000000..600a0166 --- /dev/null +++ b/src/webcore/macros.rs @@ -0,0 +1,542 @@ +macro_rules! next { + (empty) => {}; + + ((peel, $callback:tt, ($value:tt))) => { + $callback!( empty => ); + }; + + ((peel, $callback:tt, ($value:tt, $($other:tt),+))) => { + $callback!( (peel, $callback, ($($other),+)) => $($other),+ ); + }; +} + +macro_rules! foreach { + ($callback:tt => $($values:tt),*) => { + $callback!( (peel, $callback, ($($values),*)) => $($values),* ); + }; +} + +macro_rules! loop_through_identifiers { + ($callback:tt) => { + foreach!( $callback => A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12 ); + }; +} + +macro_rules! em_asm_int { + ($code:expr, $($arg:expr),*) => { + { + const CODE: &'static str = concat!( $code, "\0" ); + + #[allow(unused_unsafe)] + unsafe { + use webcore::ffi; + ffi::emscripten_asm_const_int( + CODE as *const _ as *const u8, + $($arg),* + ) + } + } + }; + ($code:expr) => { + { + const CODE: &'static str = concat!( $code, "\0" ); + unsafe { + use webcore::ffi; + ffi::emscripten_asm_const_int( + CODE as *const _ as *const u8 + ) + } + } + }; +} + +macro_rules! em_asm_double { + ($code:expr, $($arg:expr),*) => { + { + const CODE: &'static str = concat!( $code, "\0" ); + + #[allow(unused_unsafe)] + unsafe { + use webcore::ffi; + ffi::emscripten_asm_const_double( + CODE as *const _ as *const u8, + $($arg),* + ) + } + } + }; + ($code:expr) => { + { + const CODE: &'static str = concat!( $code, "\0" ); + unsafe { + use webcore::ffi; + ffi::emscripten_asm_const_double( + CODE as *const _ as *const u8 + ) + } + } + }; +} + +// Abandon all hope, ye who enter here! +// +// If there was a contest for the ugliest and most hacky macro ever written, +// I would enter this one. +// +// There is probably a more clever way to write this macro, but oh well. +#[doc(hidden)] +#[macro_export] +macro_rules! _js_impl { + (@_inc @_stringify "0" $($rest:tt)*) => { _js_impl!( @_stringify "1" $($rest)* ) }; + (@_inc @_stringify "1" $($rest:tt)*) => { _js_impl!( @_stringify "2" $($rest)* ) }; + (@_inc @_stringify "2" $($rest:tt)*) => { _js_impl!( @_stringify "3" $($rest)* ) }; + (@_inc @_stringify "3" $($rest:tt)*) => { _js_impl!( @_stringify "4" $($rest)* ) }; + (@_inc @_stringify "4" $($rest:tt)*) => { _js_impl!( @_stringify "5" $($rest)* ) }; + (@_inc @_stringify "5" $($rest:tt)*) => { _js_impl!( @_stringify "6" $($rest)* ) }; + (@_inc @_stringify "6" $($rest:tt)*) => { _js_impl!( @_stringify "7" $($rest)* ) }; + (@_inc @_stringify "7" $($rest:tt)*) => { _js_impl!( @_stringify "8" $($rest)* ) }; + (@_inc @_stringify "8" $($rest:tt)*) => { _js_impl!( @_stringify "9" $($rest)* ) }; + (@_inc @_stringify "9" $($rest:tt)*) => { _js_impl!( @_stringify "10" $($rest)* ) }; + + (@_stringify $arg_counter:tt [$terminator:tt $($terminator_rest:tt)*] [$($out:tt)*] -> [] $next:tt $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [$($terminator_rest)*] [$($out)* ($terminator)] -> $next $($rest)* ) + }; + + (@_stringify $arg_counter:tt [] [$($out:tt)*] -> []) => { + concat!( $(concat! $out),* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [($($inner:tt)*) $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [")" $($terminator)*] [$($out)* ("(")] -> [$($inner)*] [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [[$($inner:tt)*] $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter ["]" $($terminator)*] [$($out)* ("[")] -> [$($inner)*] [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [{$($inner:tt)*} $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter ["}" $($terminator)*] [$($out)* ("{")] -> [$($inner)*] [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [@{$arg:expr} $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_inc @_stringify $arg_counter [$($terminator)*] [$($out)* ("Module.STDWEB.to_js($") ($arg_counter) (")")] -> [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [++ $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [$($terminator)*] [$($out)* ("++")] -> [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [-- $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [$($terminator)*] [$($out)* ("--")] -> [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [=== $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [$($terminator)*] [$($out)* ("===")] -> [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [!== $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [$($terminator)*] [$($out)* ("!==")] -> [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [$token:tt . $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [$($terminator)*] [$($out)* (stringify!( $token )) (".")] -> [$($remaining)*] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [$token:tt] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [$($terminator)*] [$($out)* (stringify!( $token ))] -> [] $($rest)* ) + }; + + (@_stringify $arg_counter:tt [$($terminator:tt)*] [$($out:tt)*] -> [$token:tt $($remaining:tt)*] $($rest:tt)*) => { + _js_impl!( @_stringify $arg_counter [$($terminator)*] [$($out)* (stringify!( $token )) (" ")] -> [$($remaining)*] $($rest)* ) + }; + + (@stringify [$($flags:tt)*] -> $($rest:tt)*) => { + _js_impl!( @if no_return in [$($flags)*] { + _js_impl!( @_stringify "0" [] [] -> [$($rest)*] ) + } else { + _js_impl!( @_stringify "1" [] [] -> [$($rest)*] ) + }) + }; + + (@if no_return in [no_return $($rest:tt)*] {$($true_case:tt)*} else {$($false_case:tt)*}) => { + $($true_case)* + }; + + (@if $condition:tt in [] {$($true_case:tt)*} else {$($false_case:tt)*}) => { + $($false_case)* + }; + + (@if $condition:tt in [$token:tt $($rest:tt)*] {$($true_case:tt)*} else {$($false_case:tt)*}) => { + _js_impl!( @if $condition in [$($rest)*] {$($true_case)*} else {$($false_case)*} ); + }; + + (@prepare $memory_required:ident [] [$($names:tt)*]) => {}; + (@prepare $memory_required:ident [$arg:tt $($rest_args:tt)*] [$name:tt $($rest_names:tt)*]) => { + let $name = $arg; + let $name = $crate::private::IntoNewtype::into_newtype( $name ); + $memory_required += $crate::private::JsSerializableOwned::memory_required_owned( &$name ); + _js_impl!( @prepare $memory_required [$($rest_args)*] [$($rest_names)*] ); + }; + + (@serialize $arena:ident [] [$($names:tt)*]) => {}; + (@serialize $arena:ident [$arg:tt $($rest_args:tt)*] [$name:tt $($rest_names:tt)*]) => { + let mut $name = Some( $name ); + let $name = $crate::private::JsSerializableOwned::into_js_owned( &mut $name, &$arena ); + let $name = &$name as *const _; + _js_impl!( @serialize $arena [$($rest_args)*] [$($rest_names)*] ); + }; + + (@call_emscripten [$a0:tt] [$a0_name:tt $($arg_names:tt)*]) => { + $crate::private::emscripten_asm_const_int( $a0_name ); + }; + + (@call_emscripten [$a0:tt $a1:tt] [$a0_name:tt $a1_name:tt $($arg_names:tt)*]) => { + $crate::private::emscripten_asm_const_int( $a0_name, $a1_name ); + }; + + (@call_emscripten [$a0:tt $a1:tt $a2:tt] [$a0_name:tt $a1_name:tt $a2_name:tt $($arg_names:tt)*]) => { + $crate::private::emscripten_asm_const_int( $a0_name, $a1_name, $a2_name ); + }; + + (@call_emscripten [$a0:tt $a1:tt $a2:tt $a3:tt] [$a0_name:tt $a1_name:tt $a2_name:tt $a3_name:tt $($arg_names:tt)*]) => { + $crate::private::emscripten_asm_const_int( $a0_name, $a1_name, $a2_name, $a3_name ); + }; + + (@call_emscripten [$a0:tt $a1:tt $a2:tt $a3:tt $a4:tt] [$a0_name:tt $a1_name:tt $a2_name:tt $a3_name:tt $a4_name:tt $($arg_names:tt)*]) => { + $crate::private::emscripten_asm_const_int( $a0_name, $a1_name, $a2_name, $a3_name, $a4_name ); + }; + + (@call_emscripten [$a0:tt $a1:tt $a2:tt $a3:tt $a4:tt $a5:tt] [$a0_name:tt $a1_name:tt $a2_name:tt $a3_name:tt $a4_name:tt $a5_name:tt $($arg_names:tt)*]) => { + $crate::private::emscripten_asm_const_int( $a0_name, $a1_name, $a2_name, $a3_name, $a4_name, $a5_name ); + }; + + (@call [$code:expr, [$($flags:tt)*]] [$($args:tt)*] ->) => { + // It'd be nice to put at least some of this inside a function inside the crate, + // but then it wouldn't work (I tried!) as the string with the code wouldn't be + // passed as a direct reference to a constant, and Emscripten needs that to actually + // use the JavaScript code we're passing to it. + { + if cfg!( test ) { + $crate::initialize(); + } + + const CODE_STR: &'static str = _js_impl!( + @if no_return in [$($flags)*] { + concat!( $code, "\0" ) + } else { + concat!( "Module.STDWEB.from_js($0, (function(){", $code, "})());\0" ) + } + ); + + #[allow(dead_code)] + const CODE: *const u8 = CODE_STR as *const _ as *const u8; + + let mut memory_required = 0; + _js_impl!( @prepare memory_required [$($args)*] [a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10] ); + + #[allow(unused_variables)] + let arena = $crate::private::PreallocatedArena::new( memory_required ); + + _js_impl!( @serialize arena [$($args)*] [a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10] ); + arena.assert_no_free_space_left(); + + $crate::private::noop( &mut memory_required ); + + #[allow(unused_unsafe)] + unsafe { + _js_impl!( + @if no_return in [$($flags)*] { + _js_impl!( @call_emscripten [CODE $($args)*] [CODE a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10] ); + } else {{ + let mut result: $crate::private::SerializedValue = Default::default(); + _js_impl!( @call_emscripten [CODE RESULT $($args)*] [CODE (&mut result as *mut _) a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10] ); + result.deserialize() + }} + ) + } + } + }; + + (@call [$code:expr, [$($flags:tt)*]] [$($args:tt)*] -> { $($inner:tt)* } $($rest:tt)*) => { + _js_impl!( @call [$code, [$($flags)*]] [$($args)*] -> $($inner)* $($rest)* ); + }; + + (@call [$code:expr, [$($flags:tt)*]] [$($args:tt)*] -> ( $($inner:tt)* ) $($rest:tt)*) => { + _js_impl!( @call [$code, [$($flags)*]] [$($args)*] -> $($inner)* $($rest)* ); + }; + + (@call [$code:expr, [$($flags:tt)*]] [$($args:tt)*] -> [ $($inner:tt)* ] $($rest:tt)*) => { + _js_impl!( @call [$code, [$($flags)*]] [$($args)*] -> $($inner)* $($rest)* ); + }; + + (@call [$code:expr, [$($flags:tt)*]] [$($args:tt)*] -> @{$arg:expr} $($rest:tt)*) => { + _js_impl!( @call [$code, [$($flags)*]] [$($args)* $arg] -> $($rest)* ); + }; + + (@call [$code:expr, [$($flags:tt)*]] [$($args:tt)*] -> $token:tt $($rest:tt)*) => { + _js_impl!( @call [$code, [$($flags)*]] [$($args)*] -> $($rest)* ); + }; +} + +/// Embeds JavaScript code into your Rust program. +/// +/// This macro supports normal JavaScript syntax, albeit with a few limitations: +/// +/// * String literals delimited with `'` are not supported. +/// * Semicolons are always required. +/// * The macro will hit the default recursion limit pretty fast, so you'll +/// probably want to increase it with `#![recursion_limit="500"]`. +/// (This is planned to be fixed once procedural macros land in stable Rust.) +/// * Any callbacks passed into JavaScript will **leak memory** by default! +/// You need to call `.drop()` on the callback from the JavaScript side to free it. +/// +/// You can pass Rust expressions into the JavaScript code with `@{...expr...}`. +/// The value returned by this macro is an instance of [Value](enum.Value.html). +/// +/// # Examples +/// +/// ``` +/// let name = "Bob"; +/// let result = js! { +/// console.log( "Hello " + @{name} + "!" ); +/// return 2 + 2; +/// }; +/// +/// println!( "2 + 2 = {:?}", result ); +/// ``` +#[macro_export] +macro_rules! js { + (@($($flags:tt),*) $($token:tt)*) => { + _js_impl!( @call [_js_impl!( @stringify [$($flags)*] -> $($token)* ), [$($flags)*]] [] -> $($token)* ) + }; + + ($($token:tt)*) => { + _js_impl!( @call [_js_impl!( @stringify [] -> $($token)* ), []] [] -> $($token)* ) + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __js_serializable_boilerplate { + ($kind:tt) => { + __js_serializable_boilerplate!( () ($kind) () ); + }; + + (impl< $($impl_arg:tt),* > for $kind:ty where $($bounds:tt)*) => { + __js_serializable_boilerplate!( ($($impl_arg),*) ($kind) ($($bounds)*) ); + }; + + (impl< $($impl_arg:tt),* > for $kind:ty) => { + __js_serializable_boilerplate!( ($($impl_arg),*) ($kind) () ); + }; + + (($($impl_arg:tt)*) ($($kind_arg:tt)*) ($($bounds:tt)*)) => { + impl< $($impl_arg)* > $crate::private::JsSerializableOwned for $crate::private::Newtype< (), $($kind_arg)* > where $($bounds)* { + #[inline] + fn into_js_owned< 'x >( value: &'x mut Option< Self >, arena: &'x $crate::private::PreallocatedArena ) -> $crate::private::SerializedValue< 'x > { + $crate::private::JsSerializable::into_js( value.as_ref().unwrap().as_ref(), arena ) + } + + #[inline] + fn memory_required_owned( &self ) -> usize { + $crate::private::JsSerializable::memory_required( &**self ) + } + } + + impl< '_r, $($impl_arg)* > $crate::private::JsSerializableOwned for $crate::private::Newtype< (), &'_r $($kind_arg)* > where $($bounds)* { + #[inline] + fn into_js_owned< '_a >( value: &'_a mut Option< Self >, arena: &'_a $crate::private::PreallocatedArena ) -> $crate::private::SerializedValue< '_a > { + $crate::private::JsSerializable::into_js( value.as_ref().unwrap().as_ref(), arena ) + } + + #[inline] + fn memory_required_owned( &self ) -> usize { + $crate::private::JsSerializable::memory_required( &**self ) + } + } + + impl< $($impl_arg)* > $crate::private::JsSerializableOwned for $($kind_arg)* where $($bounds)* { + #[inline] + fn into_js_owned< '_a >( value: &'_a mut Option< Self >, arena: &'_a $crate::private::PreallocatedArena ) -> $crate::private::SerializedValue< '_a > { + $crate::private::JsSerializable::into_js( value.as_ref().unwrap(), arena ) + } + + #[inline] + fn memory_required_owned( &self ) -> usize { + $crate::private::JsSerializable::memory_required( self ) + } + } + + impl< '_r, $($impl_arg)* > $crate::private::JsSerializableOwned for &'_r $($kind_arg)* where $($bounds)* { + #[inline] + fn into_js_owned< '_a >( value: &'_a mut Option< Self >, arena: &'_a $crate::private::PreallocatedArena ) -> $crate::private::SerializedValue< '_a > { + $crate::private::JsSerializable::into_js( value.unwrap(), arena ) + } + + #[inline] + fn memory_required_owned( &self ) -> usize { + $crate::private::JsSerializable::memory_required( *self ) + } + } + }; +} + +macro_rules! __reference_boilerplate { + ($kind:ident, instanceof $js_name:ident $($rest:tt)*) => { + impl $crate::private::FromReference for $kind { + #[inline] + fn from_reference( reference: Reference ) -> Option< Self > { + if instanceof!( reference, $js_name ) { + Some( $kind( reference ) ) + } else { + None + } + } + } + + __reference_boilerplate!( $kind, $($rest)* ); + }; + + ($kind:ident, convertible to $base_kind:ident $($rest:tt)*) => { + impl From< $kind > for $base_kind { + #[inline] + fn from( value: $kind ) -> Self { + use $crate::private::FromReferenceUnchecked; + let reference: $crate::Reference = value.into(); + unsafe { + $base_kind::from_reference_unchecked( reference ) + } + } + } + + __reference_boilerplate!( $kind, $($rest)* ); + }; + + ($kind:ident,) => { + impl ::std::fmt::Debug for $kind { + fn fmt( &self, formatter: &mut ::std::fmt::Formatter ) -> ::std::fmt::Result { + write!( formatter, concat!( "<", stringify!( $kind ), ":{}>" ), self.0.as_raw() ) + } + } + + impl Clone for $kind { + #[inline] + fn clone( &self ) -> Self { + $kind( self.0.clone() ) + } + } + + impl AsRef< $crate::Reference > for $kind { + #[inline] + fn as_ref( &self ) -> &$crate::Reference { + &self.0 + } + } + + impl $crate::private::FromReferenceUnchecked for $kind { + #[inline] + unsafe fn from_reference_unchecked( reference: $crate::Reference ) -> Self { + $kind( reference ) + } + } + + impl From< $kind > for $crate::Reference { + #[inline] + fn from( value: $kind ) -> Self { + value.0 + } + } + + impl $crate::unstable::TryFrom< $kind > for $crate::Reference { + type Error = $crate::unstable::Void; + + #[inline] + fn try_from( value: $kind ) -> Result< Self, Self::Error > { + Ok( value.0 ) + } + } + + impl< T: $crate::unstable::TryInto< $crate::Reference > > $crate::unstable::TryFrom< T > for $kind + where >::Error: Into< Box< ::std::error::Error > > + { + type Error = Box< ::std::error::Error >; // TODO + + #[inline] + fn try_from( value: T ) -> Result< Self, Self::Error > { + value.try_into() + .map_err( |error| error.into() ) + .and_then( |reference: Reference| reference.downcast().ok_or_else( || "reference is of a different type".into() ) ) + } + } + + impl $crate::private::JsSerializable for $kind { + #[inline] + fn into_js< 'a >( &'a self, arena: &'a $crate::private::PreallocatedArena ) -> $crate::private::SerializedValue< 'a > { + self.0.into_js( arena ) + } + + #[inline] + fn memory_required( &self ) -> usize { + Reference::memory_required( &self.0 ) + } + } + + __js_serializable_boilerplate!( $kind ); + }; +} + +macro_rules! reference_boilerplate { + ($kind:ident, $($rest:tt)*) => { + __reference_boilerplate!( $kind, $($rest)* ); + } +} + +macro_rules! instanceof { + ($value:expr, $kind:ident) => {{ + use $crate::unstable::TryInto; + let reference: Option< &$crate::Reference > = (&$value).try_into().ok(); + reference.map( |reference| { + em_asm_int!( + concat!( "return (Module.STDWEB.acquire_js_reference( $0 ) instanceof ", stringify!( $kind ), ") | 0;" ), + reference.as_raw() + ) == 1 + }).unwrap_or( false ) + }}; +} + +#[cfg(test)] +mod tests { + macro_rules! stringify_js { + ($($token:tt)*) => { + _js_impl!( @stringify [] -> $($token)* ) + }; + } + + #[test] + fn stringify() { + assert_eq!( stringify_js! { console.log }, "console.log" ); + assert_eq!( stringify_js! { 1.0 }, "1.0" ); + assert_eq!( stringify_js! { [ 1.0 ] }, "[1.0]" ); + assert_eq!( stringify_js! { { 1.0 } }, "{1.0}" ); + assert_eq!( stringify_js! { ( 1.0 ) }, "(1.0)" ); + assert_eq!( stringify_js! { a b }, "a b" ); + assert_eq!( stringify_js! { === }, "===" ); + assert_eq!( stringify_js! { ++i }, "++i" ); + assert_eq!( stringify_js! { i++ }, "i ++" ); + assert_eq!( stringify_js! { --i }, "--i" ); + assert_eq!( stringify_js! { i-- }, "i --" ); + assert_eq!( stringify_js! { ( @{1} ); }.replace( " ", "" ), "(Module.STDWEB.to_js($1));" ); + assert_eq!( + stringify_js! { + console.log( "Hello!", @{1234i32} ); + }.replace( " ", "" ), + "console.log(\"Hello!\",Module.STDWEB.to_js($1));" + ); + assert_eq!( + stringify_js! { + @{a}.fn( @{b} ); + }.replace( " ", "" ), + "Module.STDWEB.to_js($1).fn(Module.STDWEB.to_js($2));" + ); + } +} diff --git a/src/webcore/mod.rs b/src/webcore/mod.rs new file mode 100644 index 00000000..66b1c14c --- /dev/null +++ b/src/webcore/mod.rs @@ -0,0 +1,11 @@ +#[macro_use] +pub mod macros; +pub mod initialization; +pub mod value; +pub mod number; +pub mod serialization; +pub mod ffi; +pub mod callfn; +pub mod newtype; +pub mod try_from; +pub mod void; diff --git a/src/webcore/newtype.rs b/src/webcore/newtype.rs new file mode 100644 index 00000000..7c49cee8 --- /dev/null +++ b/src/webcore/newtype.rs @@ -0,0 +1,77 @@ +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::fmt; + +// Unfortunately Rust doesn't allow something like this: +// impl< A, T: Trait< A > > AnotherTrait for T {} +// It will simply return `unconstrained type parameter` error. +// +// To work around this we create a dummy newtype which will +// artificially constraint the extra parameter. +#[doc(hidden)] +pub struct Newtype< A, T >( T, PhantomData< A > ); + +impl< A, T > Newtype< A, T > { + #[doc(hidden)] + #[inline] + pub fn unwrap_newtype( self ) -> T { + self.0 + } +} + +#[doc(hidden)] +pub trait IntoNewtype< A >: Sized { + fn into_newtype( self ) -> Newtype< A, Self >; +} + +impl< A, T > IntoNewtype< A > for T { + #[inline] + fn into_newtype( self ) -> Newtype< A, Self > { + Newtype( self, PhantomData ) + } +} + +impl< A, T > Deref for Newtype< A, T > { + type Target = T; + + #[inline] + fn deref( &self ) -> &Self::Target { + &self.0 + } +} + +impl< A, T > DerefMut for Newtype< A, T > { + #[inline] + fn deref_mut( &mut self ) -> &mut Self::Target { + &mut self.0 + } +} + +impl< A, T: Copy > Copy for Newtype< A, T > {} +impl< A, T: Clone > Clone for Newtype< A, T > { + #[inline] + fn clone( &self ) -> Self { + Newtype( self.0.clone(), PhantomData ) + } +} + +impl< A, T > AsRef< T > for Newtype< A, T > { + #[inline] + fn as_ref( &self ) -> &T { + &self.0 + } +} + +impl< A, T > AsMut< T > for Newtype< A, T > { + #[inline] + fn as_mut( &mut self ) -> &mut T { + &mut self.0 + } +} + +impl< A, T: fmt::Debug > fmt::Debug for Newtype< A, T > { + #[inline] + fn fmt( &self, formatter: &mut fmt::Formatter ) -> Result< (), fmt::Error > { + self.0.fmt( formatter ) + } +} diff --git a/src/webcore/number.rs b/src/webcore/number.rs new file mode 100644 index 00000000..f6cbefd9 --- /dev/null +++ b/src/webcore/number.rs @@ -0,0 +1,650 @@ +// TODO: Handle NaN and Infinity. + +use std::{i8, i16, i32, i64, u8, u16, u32, u64, f32, f64}; +use std::error; +use std::fmt; +use webcore::try_from::TryFrom; + +// 2^53 - 1 +const MAX_SAFE_INTEGER_F64: i64 = 9007199254740991; +const MIN_SAFE_INTEGER_F64: i64 = -9007199254740991; + +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum Storage { + // Technically JavaScript only has f64 numbers, however at least the V8 + // treats numbers which can be represented as 31-bit integers more optimally. + // + // Now, I have absolutely no idea if doing this is worth it as opposed to + // just sticking with always using f64; it's definitely worth investigating + // in the future. + I32( i32 ), + F64( f64 ) +} + +/// A type representing a JavaScript number. +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct Number( Storage ); + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum ConversionError { + OutOfRange, + NotAnInteger +} + +impl fmt::Display for ConversionError { + fn fmt( &self, formatter: &mut fmt::Formatter ) -> Result< (), fmt::Error > { + let message = error::Error::description( self ); + write!( formatter, "{}", message ) + } +} + +impl error::Error for ConversionError { + fn description( &self ) -> &str { + match *self { + ConversionError::OutOfRange => "number out of range", + ConversionError::NotAnInteger => "number not an integer" + } + } +} + +// We don't want to make the inner value public, hence this accessor. +#[inline] +pub fn get_storage( number: &Number ) -> &Storage { + &number.0 +} + +impl AsRef< Number > for Number { + #[inline] + fn as_ref( &self ) -> &Self { + self + } +} + +impl From< i8 > for Number { + #[inline] + fn from( value: i8 ) -> Self { + Number( Storage::I32( value as i32 ) ) + } +} + +impl From< i16 > for Number { + #[inline] + fn from( value: i16 ) -> Self { + Number( Storage::I32( value as i32 ) ) + } +} + +impl From< i32 > for Number { + #[inline] + fn from( value: i32 ) -> Self { + Number( Storage::I32( value ) ) + } +} + +impl TryFrom< i64 > for Number { + type Error = ConversionError; + + fn try_from( value: i64 ) -> Result< Self, Self::Error > { + if value >= i32::MIN as i64 && value <= i32::MAX as i64 { + Ok( Number( Storage::I32( value as i32 ) ) ) + } else if value >= MIN_SAFE_INTEGER_F64 && value <= MAX_SAFE_INTEGER_F64 { + Ok( Number( Storage::F64( value as f64 ) ) ) + } else { + Err( ConversionError::OutOfRange ) + } + } +} + +impl From< u8 > for Number { + #[inline] + fn from( value: u8 ) -> Self { + Number( Storage::I32( value as i32 ) ) + } +} + +impl From< u16 > for Number { + #[inline] + fn from( value: u16 ) -> Self { + Number( Storage::I32( value as i32 ) ) + } +} + +impl From< u32 > for Number { + #[inline] + fn from( value: u32 ) -> Self { + if value <= i32::MAX as u32 { + Number( Storage::I32( value as i32 ) ) + } else { + Number( Storage::F64( value as f64 ) ) + } + } +} + +impl TryFrom< u64 > for Number { + type Error = ConversionError; + + fn try_from( value: u64 ) -> Result< Self, Self::Error > { + if value <= i32::MAX as u64 { + Ok( Number( Storage::I32( value as i32 ) ) ) + } else if value <= MAX_SAFE_INTEGER_F64 as u64 { + Ok( Number( Storage::F64( value as f64 ) ) ) + } else { + Err( ConversionError::OutOfRange ) + } + } +} + +impl From< f32 > for Number { + #[inline] + fn from( value: f32 ) -> Self { + Number( Storage::F64( value as f64 ) ) + } +} + +impl From< f64 > for Number { + #[inline] + fn from( value: f64 ) -> Self { + Number( Storage::F64( value ) ) + } +} + +macro_rules! impl_trivial_try_from { + ($($kind:ty),+) => { + $( + impl TryFrom< $kind > for Number { + type Error = $crate::unstable::Void; + + #[inline] + fn try_from( value: $kind ) -> Result< Self, Self::Error > { + Ok( value.into() ) + } + } + )+ + } +} + +impl_trivial_try_from!( i8, i16, i32, u8, u16, u32, f32, f64 ); + +macro_rules! impl_conversion_into_rust_types { + ($(into $($kind:tt),+: { from i32: $integer_callback:ident, from f64: $float_callback:ident }),+) => { + $( + $( + impl TryFrom< Number > for $kind { + type Error = ConversionError; + + #[allow(trivial_numeric_casts)] + fn try_from( number: Number ) -> Result< Self, Self::Error > { + match number.0 { + Storage::I32( value ) => { + $integer_callback!( value, $kind ) + }, + Storage::F64( value ) => { + $float_callback!( value, $kind ) + } + } + } + } + )+ + )+ + } +} + +macro_rules! i32_to_small_integer { + ($value:expr, $kind:tt) => { + if $value <= $kind::MAX as i32 && $value >= $kind::MIN as i32 { + Ok( $value as $kind ) + } else { + Err( ConversionError::OutOfRange ) + } + } +} + +macro_rules! direct_cast { + ($value:expr, $kind:tt) => { + Ok( $value as $kind ) + } +} + +macro_rules! i32_to_big_unsigned_integer { + ($value:expr, $kind:tt) => { + if $value >= 0 { + Ok( $value as $kind ) + } else { + Err( ConversionError::OutOfRange ) + } + } +} + +macro_rules! f64_to_integer { + ($value:expr, $kind:tt) => {{ + if $value.floor() != $value { + return Err( ConversionError::NotAnInteger ); + } + + if $value <= $kind::MAX as f64 && $value >= $kind::MIN as f64 { + Ok( $value as $kind ) + } else { + Err( ConversionError::OutOfRange ) + } + }} +} + +impl_conversion_into_rust_types! { + into i8, i16, i32, u8, u16: { + from i32: i32_to_small_integer, + from f64: f64_to_integer + }, + into i64: { + from i32: direct_cast, + from f64: f64_to_integer + }, + into u32, u64: { + from i32: i32_to_big_unsigned_integer, + from f64: f64_to_integer + }, + into f64: { + from i32: direct_cast, + from f64: direct_cast + } +} + +impl PartialEq< i8 > for Number { + #[inline] + fn eq( &self, right: &i8 ) -> bool { + match self.0 { + Storage::I32( left ) => left == *right as i32, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< i16 > for Number { + #[inline] + fn eq( &self, right: &i16 ) -> bool { + match self.0 { + Storage::I32( left ) => left == *right as i32, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< i32 > for Number { + #[inline] + fn eq( &self, right: &i32 ) -> bool { + match self.0 { + Storage::I32( left ) => left == *right, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< i64 > for Number { + #[inline] + fn eq( &self, right: &i64 ) -> bool { + match self.0 { + Storage::I32( left ) => left as i64 == *right, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< u8 > for Number { + #[inline] + fn eq( &self, right: &u8 ) -> bool { + match self.0 { + Storage::I32( left ) => left == *right as i32, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< u16 > for Number { + #[inline] + fn eq( &self, right: &u16 ) -> bool { + match self.0 { + Storage::I32( left ) => left == *right as i32, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< u32 > for Number { + #[inline] + fn eq( &self, right: &u32 ) -> bool { + match self.0 { + Storage::I32( left ) => left as i64 == *right as i64, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< u64 > for Number { + #[inline] + fn eq( &self, right: &u64 ) -> bool { + match self.0 { + Storage::I32( left ) => { + if *right >= i32::MAX as u64 { + // The right side is not convertible to an i32. + return false; + } + left == *right as i32 + }, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< f32 > for Number { + #[inline] + fn eq( &self, right: &f32 ) -> bool { + match self.0 { + Storage::I32( left ) => left as f64 == *right as f64, + Storage::F64( left ) => left == *right as f64 + } + } +} + +impl PartialEq< f64 > for Number { + #[inline] + fn eq( &self, right: &f64 ) -> bool { + match self.0 { + Storage::I32( left ) => left as f64 == *right, + Storage::F64( left ) => left == *right + } + } +} + +macro_rules! impl_symmetric_partial_eq { + ( $($kind:tt),+ ) => { + $( + impl PartialEq< Number > for $kind { + #[inline] + fn eq( &self, right: &Number ) -> bool { + right == self + } + } + )+ + } +} + +impl_symmetric_partial_eq! { + u8, u16, u32, u64, + i8, i16, i32, i64, + f32, f64 +} + +#[cfg(test)] +mod tests { + use super::*; + use std::mem; + use std::u8; + + // 2^23 - 1 + const MAX_SAFE_INTEGER_F32: i32 = 8388607; + const MIN_SAFE_INTEGER_F32: i32 = -8388607; + + trait ExampleValues: Sized { + fn example_values() -> Vec< Self >; + } + + macro_rules! example_values { + ( + $($kind:tt => [$($value:expr),+]),+ + ) => { + $( + impl ExampleValues for $kind { + fn example_values() -> Vec< Self > { vec![ $($value),+ ] } + } + )+ + } + } + + example_values! { + u8 => [ 0, 1, u8::MAX - 1, u8::MAX ], + u16 => [ 0, 1, u16::MAX - 1, u16::MAX ], + u32 => [ 0, 1, u32::MAX - 1, u32::MAX ], + u64 => [ 0, 1, u64::MAX - 1, u64::MAX ], + i8 => [ i8::MIN, i8::MIN + 1, -1, 0, 1, i8::MAX - 1, i8::MAX ], + i16 => [ i16::MIN, i16::MIN + 1, -1, 0, 1, i16::MAX - 1, i16::MAX ], + i32 => [ i32::MIN, i32::MIN + 1, -1, 0, 1, i32::MAX - 1, i32::MAX ], + i64 => [ i64::MIN, i64::MIN + 1, -1, 0, 1, i64::MAX - 1, i64::MAX ], + f32 => [ + f32::MIN, f32::MIN + 1.0, -1.0, 0.0, 1.0, f32::MAX - 1.0, f32::MAX, + -0.33, 0.33, -3.33, 3.33, + MIN_SAFE_INTEGER_F32 as f32, + MIN_SAFE_INTEGER_F32 as f32 - 100.0, + MAX_SAFE_INTEGER_F32 as f32, + MAX_SAFE_INTEGER_F32 as f32 + 100.0 + ], + f64 => [ + f64::MIN, f64::MIN + 1.0, -1.0, 0.0, 1.0, f64::MAX - 1.0, f64::MAX, + -0.33, 0.33, -3.33, 3.33, + MIN_SAFE_INTEGER_F32 as f64, + MIN_SAFE_INTEGER_F32 as f64 - 100.0, + MAX_SAFE_INTEGER_F32 as f64, + MAX_SAFE_INTEGER_F32 as f64 + 100.0, + MIN_SAFE_INTEGER_F64 as f64, + MIN_SAFE_INTEGER_F64 as f64 - 100.0, + MAX_SAFE_INTEGER_F64 as f64, + MAX_SAFE_INTEGER_F64 as f64 + 100.0 + ] + } + + #[derive(Copy, Clone, PartialEq, Eq, Debug)] + enum Kind { + U, + I, + F + } + + macro_rules! type_kind { + (u8) => (U); + (u16) => (U); + (u32) => (U); + (u64) => (U); + (i8) => (I); + (i16) => (I); + (i32) => (I); + (i64) => (I); + (f32) => (F); + (f64) => (F); + } + + macro_rules! is_convertible { + ($value:expr, $src_type:tt => $dst_type:tt) => {{ + let src_size = mem::size_of::< $src_type >(); + let dst_size = mem::size_of::< $dst_type >(); + let src_kind = type_kind!( $src_type ); + let dst_kind = type_kind!( $dst_type ); + + use self::Kind::*; + match (src_kind, dst_kind) { + (F, F) => { + if dst_size >= src_size { + true + } else /* dst_size < src_size */ { + let value = $value as f64; + let in_range = match dst_size { + 4 => value <= MAX_SAFE_INTEGER_F32 as f64 && value >= MIN_SAFE_INTEGER_F32 as f64, + _ => unreachable!() + }; + + in_range && ($value as $dst_type) as $src_type == $value + } + }, + (U, U) | (I, I) => { + if dst_size >= src_size { + true + } else /* dst_size < src_size */ { + $value >= ($dst_type::MIN as $src_type) && + $value <= ($dst_type::MAX as $src_type) + } + }, + (U, I) => { + if dst_size > src_size { + true + } else /* dst_size <= src_size */ { + $value <= ($dst_type::MAX as $src_type) + } + }, + (I, U) => { + if $value < (0 as $src_type) { + false + } else if dst_size >= src_size { + true + } else /* dst_size < src_size */ { + $value <= ($dst_type::MAX as $src_type) + } + }, + (F, U) => { + ($value as f64).floor() == ($value as f64) && + ($value as f64) >= ($dst_type::MIN as f64) && + ($value as f64) <= ($dst_type::MAX as f64) + }, + (F, I) => { + ($value as f64).floor() == ($value as f64) && + ($value as f64) >= ($dst_type::MIN as f64) && + ($value as f64) <= ($dst_type::MAX as f64) + }, + (I, F) => { + let value = $value as i64; + match dst_size { + 4 => value <= MAX_SAFE_INTEGER_F32 as i64 && value >= MIN_SAFE_INTEGER_F32 as i64, + 8 => value <= MAX_SAFE_INTEGER_F64 as i64 && value >= MIN_SAFE_INTEGER_F64 as i64, + _ => unreachable!() + } + }, + (U, F) => { + let value = $value as u64; + match dst_size { + 4 => value <= MAX_SAFE_INTEGER_F32 as u64, + 8 => value <= MAX_SAFE_INTEGER_F64 as u64, + _ => unreachable!() + } + } + } + }} + } + + macro_rules! conversion_test_body { + ($value:expr, $src_type:tt, $dst_type:tt) => {{ + let is_convertible_into_number = is_convertible!( $value, $src_type => f64 ); + let number: Result< Number, _ > = $value.try_into(); + let number = match number { + Ok( number ) => { + if !is_convertible_into_number { + panic!( "Type {} should NOT be convertible into Number yet it is: {:?}", stringify!( $src_type ), $value ); + } + number + }, + Err( _ ) => { + if is_convertible_into_number { + panic!( "Type {} should be convertible into Number yet it isn't: {:?}", stringify!( $src_type ), $value ); + } + return; + } + }; + + let is_convertible = is_convertible!( $value, $src_type => $dst_type ); + let output = number.try_into(); + if is_convertible { + let result = output == Ok( $value as $dst_type ); + assert!( result, "Conversion should succeed yet it didn't for {:?}", $value ); + } else { + let result = output.is_err(); + assert!( result, "Conversion should NOT succeed yet it did for {:?}", $value ); + }; + }} + } + + macro_rules! conversion_test { + ($src_type:tt, $(($dst_type:tt, $test_name:ident)),+) => { + $( + #[allow(trivial_numeric_casts, const_err)] + #[test] + fn $test_name() { + for value in $src_type::example_values() { + conversion_test_body!( value, $src_type, $dst_type ); + } + } + )+ + } + } + + macro_rules! generate_conversion_tests { + ($raw_type:tt) => { + conversion_test! { + $raw_type, + (u8, into_u8), + (u16, into_u16), + (u32, into_u32), + (u64, into_u64), + (i8, into_i8), + (i16, into_i16), + (i32, into_i32), + (i64, into_i64), + // No conversion to f32. + (f64, into_f64) + } + } + } + + macro_rules! generate_number_tests { + ($(($raw_type:tt, $name:ident)),+) => { + $( + mod $name { + use super::*; + use webcore::try_from::TryInto; + + #[test] + fn round_trip() { + for left in $raw_type::example_values() { + let number: Number = left.into(); + let right: $raw_type = number.try_into().unwrap(); + assert!( left == right, "Failed for: {:?}", left ); + } + } + + #[test] + fn equality() { + for value in $raw_type::example_values() { + let number: Number = value.into(); + assert!( number == value, "Failed for: {:?}", value ); + assert!( value == number, "Failed for: {:?}", value ); + } + } + + generate_conversion_tests! { $raw_type } + } + )+ + } + } + + generate_number_tests! { + (u8, for_u8), + (u16, for_u16), + (u32, for_u32), + (i8, for_i8), + (i16, for_i16), + (i32, for_i32), + (f64, for_f64) + } + + mod for_f32 { + use super::*; + use webcore::try_from::TryInto; + generate_conversion_tests! { f32 } + } + + mod for_u64 { + use super::*; + use webcore::try_from::TryInto; + generate_conversion_tests! { u64 } + } + + mod for_i64 { + use super::*; + use webcore::try_from::TryInto; + generate_conversion_tests! { u64 } + } +} diff --git a/src/webcore/serialization.rs b/src/webcore/serialization.rs new file mode 100644 index 00000000..719039eb --- /dev/null +++ b/src/webcore/serialization.rs @@ -0,0 +1,1016 @@ +use std::mem; +use std::slice; +use std::i32; +use std::collections::BTreeMap; +use std::marker::PhantomData; +use std::cell::{Cell, UnsafeCell}; + +use webcore::ffi; +use webcore::callfn::CallMut; +use webcore::newtype::Newtype; +use webcore::try_from::{TryFrom, TryInto}; +use webcore::number::Number; + +use webcore::value::{ + Null, + Undefined, + Reference, + Value +}; + +#[repr(u8)] +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum Tag { + Undefined = 0, + Null = 1, + I32 = 2, + F64 = 3, + Str = 4, + False = 5, + True = 6, + Array = 7, + Object = 8, + Reference = 9, + Function = 10 +} + +impl Default for Tag { + #[inline] + fn default() -> Self { + Tag::Undefined + } +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct PreallocatedArena { + saved: UnsafeCell< Vec< Value > >, + buffer: UnsafeCell< Vec< u8 > >, + index: Cell< usize > +} + +impl PreallocatedArena { + #[doc(hidden)] + #[inline] + pub fn new( memory_required: usize ) -> Self { + let mut buffer = Vec::new(); + buffer.reserve( memory_required ); + unsafe { + buffer.set_len( memory_required ); + } + + PreallocatedArena { + saved: UnsafeCell::new( Vec::new() ), + buffer: UnsafeCell::new( buffer ), + index: Cell::new( 0 ) + } + } + + #[doc(hidden)] + #[inline] + pub fn reserve< T >( &self, count: usize ) -> &mut [T] { + let bytes = mem::size_of::< T >() * count; + let slice = unsafe { + let mut buffer = &mut *self.buffer.get(); + debug_assert!( self.index.get() + bytes <= buffer.len() ); + + slice::from_raw_parts_mut( + buffer.as_mut_ptr().offset( self.index.get() as isize ) as *mut T, + count + ) + }; + + self.index.set( self.index.get() + bytes ); + slice + } + + #[doc(hidden)] + #[inline] + pub fn save( &self, value: Value ) -> &Value { + unsafe { + let mut saved = &mut *self.saved.get(); + saved.push( value ); + &*(saved.last().unwrap() as *const Value) + } + } + + #[doc(hidden)] + #[inline] + pub fn assert_no_free_space_left( &self ) { + debug_assert!( self.index.get() == unsafe { &*self.buffer.get() }.len() ); + } +} + +#[doc(hidden)] +pub trait JsSerializableOwned: Sized { + fn into_js_owned< 'a >( value: &'a mut Option< Self >, arena: &'a PreallocatedArena ) -> SerializedValue< 'a >; + fn memory_required_owned( &self ) -> usize; +} + +#[doc(hidden)] +pub trait JsSerializable { + fn into_js< 'a >( &'a self, arena: &'a PreallocatedArena ) -> SerializedValue< 'a >; + fn memory_required( &self ) -> usize; +} + +// This is a generic structure for serializing every JavaScript value. +#[doc(hidden)] +#[repr(C)] +#[derive(Default, Debug)] +pub struct SerializedValue< 'a > { + data_1: u64, + data_2: u32, + tag: Tag, + phantom: PhantomData< &'a () > +} + +#[test] +fn test_serialized_value_size() { + assert_eq!( mem::size_of::< SerializedValue< 'static > >(), 16 ); +} + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedUndefined; + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedNull; + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedI32 { + value: i32 +} + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedF64 { + value: f64 +} + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedTrue {} + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedFalse {} + +#[repr(C)] +#[derive(Clone, Debug)] +struct SerializedUntaggedString { + pointer: u32, + length: u32 +} + +#[repr(C)] +#[derive(Clone, Debug)] +struct SerializedUntaggedArray { + pointer: u32, + length: u32 +} + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedObject { + value_pointer: u32, + length: u32, + key_pointer: u32 +} + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedReference { + refid: i32 +} + +#[repr(C)] +#[derive(Debug)] +struct SerializedUntaggedFunction { + adapter_pointer: u32, + pointer: u32, + deallocator_pointer: u32 +} + +impl SerializedUntaggedString { + #[inline] + fn deserialize( &self ) -> String { + let pointer = self.pointer as *mut u8; + let length = self.length as usize; + + unsafe { + let vector = Vec::from_raw_parts( pointer, length, length + 1 ); + String::from_utf8_unchecked( vector ) + } + } +} + +impl SerializedUntaggedArray { + #[inline] + fn deserialize( &self ) -> Vec< Value > { + let pointer = self.pointer as *const SerializedValue; + let length = self.length as usize; + let slice = unsafe { + slice::from_raw_parts( pointer, length ) + }; + + let vector = slice.iter().map( |value| value.deserialize() ).collect(); + unsafe { + ffi::free( pointer as *const u8 ); + } + + vector + } +} + +impl SerializedUntaggedObject { + #[inline] + fn deserialize( &self ) -> BTreeMap< String, Value > { + let length = self.length as usize; + let key_pointer = self.key_pointer as *const SerializedUntaggedString; + let value_pointer = self.value_pointer as *const SerializedValue; + + let key_slice = unsafe { slice::from_raw_parts( key_pointer, length ) }; + let value_slice = unsafe { slice::from_raw_parts( value_pointer, length ) }; + + let map = key_slice.iter().zip( value_slice.iter() ).map( |(key, value)| { + let key = key.deserialize(); + let value = value.deserialize(); + (key, value) + }).collect(); + + unsafe { + ffi::free( key_pointer as *const u8 ); + ffi::free( value_pointer as *const u8 ); + } + + map + } +} + +impl SerializedUntaggedReference { + #[inline] + fn deserialize( &self ) -> Reference { + unsafe { Reference::from_raw_unchecked( self.refid ) } + } +} + +macro_rules! untagged_boilerplate { + ($tests_namespace:ident, $reader_name:ident, $tag:expr, $untagged_type:ident) => { + impl< 'a > SerializedValue< 'a > { + #[allow(dead_code)] + #[inline] + fn $reader_name( &self ) -> &$untagged_type { + debug_assert_eq!( self.tag, $tag ); + unsafe { + &*(self as *const _ as *const $untagged_type) + } + } + } + + impl< 'a > From< $untagged_type > for SerializedValue< 'a > { + #[inline] + fn from( untagged: $untagged_type ) -> Self { + unsafe { + let mut value: SerializedValue = mem::uninitialized(); + *(&mut value as *mut SerializedValue as *mut $untagged_type) = untagged; + value.tag = $tag; + value + } + } + } + + #[cfg(test)] + mod $tests_namespace { + use super::*; + + #[test] + fn does_not_overlap_with_the_tag() { + let size = mem::size_of::< $untagged_type >(); + let tag_offset = unsafe { &(&*(0 as *const SerializedValue< 'static >)).tag as *const _ as usize }; + assert!( size <= tag_offset ); + } + } + } +} + +untagged_boilerplate!( test_undefined, as_undefined, Tag::Undefined, SerializedUntaggedUndefined ); +untagged_boilerplate!( test_null, as_null, Tag::Null, SerializedUntaggedNull ); +untagged_boilerplate!( test_i32, as_i32, Tag::I32, SerializedUntaggedI32 ); +untagged_boilerplate!( test_f64, as_f64, Tag::F64, SerializedUntaggedF64 ); +untagged_boilerplate!( test_true, as_true, Tag::True, SerializedUntaggedTrue ); +untagged_boilerplate!( test_false, as_false, Tag::False, SerializedUntaggedFalse ); +untagged_boilerplate!( test_object, as_object, Tag::Object, SerializedUntaggedObject ); +untagged_boilerplate!( test_string, as_string, Tag::Str, SerializedUntaggedString ); +untagged_boilerplate!( test_array, as_array, Tag::Array, SerializedUntaggedArray ); +untagged_boilerplate!( test_reference, as_reference, Tag::Reference, SerializedUntaggedReference ); +untagged_boilerplate!( test_function, as_function, Tag::Function, SerializedUntaggedFunction ); + +impl< 'a > SerializedValue< 'a > { + #[doc(hidden)] + #[inline] + pub fn deserialize( &self ) -> Value { + match self.tag { + Tag::Undefined => Value::Undefined, + Tag::Null => Value::Null, + Tag::I32 => self.as_i32().value.into(), + Tag::F64 => self.as_f64().value.into(), + Tag::Str => Value::String( self.as_string().deserialize() ), + Tag::False => Value::Bool( false ), + Tag::True => Value::Bool( true ), + Tag::Array => Value::Array( self.as_array().deserialize() ), + Tag::Object => Value::Object( self.as_object().deserialize() ), + Tag::Reference => self.as_reference().deserialize().into(), + Tag::Function => unreachable!() + } + } +} + +impl JsSerializable for () { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedUndefined.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( () ); + +impl JsSerializable for Undefined { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedUndefined.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( Undefined ); + +impl JsSerializable for Null { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedNull.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( Null ); + +impl JsSerializable for Reference { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedReference { + refid: self.as_raw() + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( Reference ); + +impl JsSerializable for bool { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + if *self { + SerializedUntaggedTrue {}.into() + } else { + SerializedUntaggedFalse {}.into() + } + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( bool ); + +impl JsSerializable for str { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedString { + pointer: self.as_ptr() as u32, + length: self.len() as u32 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( impl< 'a > for &'a str ); + +impl JsSerializable for String { + #[inline] + fn into_js< 'a >( &'a self, arena: &'a PreallocatedArena ) -> SerializedValue< 'a > { + self.as_str().into_js( arena ) + } + + #[inline] + fn memory_required( &self ) -> usize { + self.as_str().memory_required() + } +} + +__js_serializable_boilerplate!( String ); + +impl JsSerializable for i8 { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedI32 { + value: *self as i32 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + (*self as i32).memory_required() + } +} + +__js_serializable_boilerplate!( i8 ); + +impl JsSerializable for i16 { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedI32 { + value: *self as i32 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + (*self as i32).memory_required() + } +} + +__js_serializable_boilerplate!( i16 ); + +impl JsSerializable for i32 { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedI32 { + value: *self + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( i32 ); + +impl JsSerializable for u8 { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedI32 { + value: *self as i32 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + (*self as i32).memory_required() + } +} + +__js_serializable_boilerplate!( u8 ); + +impl JsSerializable for u16 { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedI32 { + value: *self as i32 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + (*self as i32).memory_required() + } +} + +__js_serializable_boilerplate!( u16 ); + +impl JsSerializable for u32 { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedF64 { + value: *self as f64 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + (*self as f64).memory_required() + } +} + +__js_serializable_boilerplate!( u32 ); + +impl JsSerializable for f32 { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedF64 { + value: *self as f64 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + (*self as f64).memory_required() + } +} + +__js_serializable_boilerplate!( f32 ); + +impl JsSerializable for f64 { + #[inline] + fn into_js< 'a >( &'a self, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + SerializedUntaggedF64 { + value: *self + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( f64 ); + +impl JsSerializable for Number { + #[inline] + fn into_js< 'a >( &'a self, arena: &'a PreallocatedArena ) -> SerializedValue< 'a > { + use webcore::number::{Storage, get_storage}; + match *get_storage( self ) { + Storage::I32( ref value ) => value.into_js( arena ), + Storage::F64( ref value ) => value.into_js( arena ) + } + } + + #[inline] + fn memory_required( &self ) -> usize { + 0 + } +} + +__js_serializable_boilerplate!( Number ); + +impl< T: JsSerializable > JsSerializable for Option< T > { + #[inline] + fn into_js< 'a >( &'a self, arena: &'a PreallocatedArena ) -> SerializedValue< 'a > { + if let Some( value ) = self.as_ref() { + value.into_js( arena ) + } else { + SerializedUntaggedNull.into() + } + } + + #[inline] + fn memory_required( &self ) -> usize { + if let Some( value ) = self.as_ref() { + value.memory_required() + } else { + 0 + } + } +} + +__js_serializable_boilerplate!( impl< T > for Option< T > where T: JsSerializable ); + +impl< T: JsSerializable > JsSerializable for [T] { + #[inline] + fn into_js< 'a >( &'a self, arena: &'a PreallocatedArena ) -> SerializedValue< 'a > { + let output = arena.reserve( self.len() ); + for (value, output_value) in self.iter().zip( output.iter_mut() ) { + *output_value = value.into_js( arena ); + } + + SerializedUntaggedArray { + pointer: output.as_ptr() as u32, + length: output.len() as u32 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + mem::size_of::< SerializedValue >() * self.len() + + self.iter().fold( 0, |sum, value| sum + value.memory_required() ) + } +} + +__js_serializable_boilerplate!( impl< 'a, T > for &'a [T] where T: JsSerializable ); + +impl< T: JsSerializable > JsSerializable for BTreeMap< String, T > { + #[inline] + fn into_js< 'a >( &'a self, arena: &'a PreallocatedArena ) -> SerializedValue< 'a > { + let keys = arena.reserve( self.len() ); + let values = arena.reserve( self.len() ); + for (((key, value), output_key), output_value) in self.iter().zip( keys.iter_mut() ).zip( values.iter_mut() ) { + *output_key = key.into_js( arena ).as_string().clone(); + *output_value = value.into_js( arena ); + } + + SerializedUntaggedObject { + key_pointer: keys.as_ptr() as u32, + value_pointer: values.as_ptr() as u32, + length: keys.len() as u32 + }.into() + } + + #[inline] + fn memory_required( &self ) -> usize { + mem::size_of::< SerializedValue >() * self.len() + + mem::size_of::< SerializedUntaggedString >() * self.len() + + self.iter().fold( 0, |sum, (key, value)| sum + key.memory_required() + value.memory_required() ) + } +} + +__js_serializable_boilerplate!( impl< T > for BTreeMap< String, T > where T: JsSerializable ); + +impl JsSerializable for Value { + fn into_js< 'a >( &'a self, arena: &'a PreallocatedArena ) -> SerializedValue< 'a > { + match *self { + Value::Undefined => SerializedUntaggedUndefined.into(), + Value::Null => SerializedUntaggedNull.into(), + Value::Bool( ref value ) => value.into_js( arena ), + Value::Number( ref value ) => value.into_js( arena ), + Value::String( ref value ) => value.into_js( arena ), + Value::Array( ref value ) => value.as_slice().into_js( arena ), + Value::Object( ref value ) => value.into_js( arena ), + Value::Reference( ref value ) => value.into_js( arena ) + } + } + + fn memory_required( &self ) -> usize { + match *self { + Value::Undefined => Undefined.memory_required(), + Value::Null => Null.memory_required(), + Value::Bool( value ) => value.memory_required(), + Value::Number( value ) => value.memory_required(), + Value::String( ref value ) => value.memory_required(), + Value::Array( ref value ) => value.as_slice().memory_required(), + Value::Object( ref value ) => value.memory_required(), + Value::Reference( ref value ) => value.memory_required() + } + } +} + +__js_serializable_boilerplate!( Value ); + +macro_rules! impl_for_fn { + ($next:tt => $($kind:ident),*) => { + + impl< $($kind: TryFrom< Value >,)* F > JsSerializableOwned for Newtype< ($($kind,)*), F > + where F: CallMut< ($($kind,)*) > + 'static, F::Output: JsSerializableOwned + { + #[inline] + #[allow(unused_mut, unused_variables, non_snake_case)] + fn into_js_owned< 'a >( value: &'a mut Option< Self >, _: &'a PreallocatedArena ) -> SerializedValue< 'a > { + let callback: *mut F = Box::into_raw( Box::new( value.take().unwrap().unwrap_newtype() ) ); + + extern fn funcall_adapter< $($kind: TryFrom< Value >,)* F: CallMut< ($($kind,)*) > >( + callback: *mut F, + raw_arguments: *mut SerializedUntaggedArray + ) where F::Output: JsSerializableOwned + { + let callback = unsafe { &mut *callback }; + let mut arguments = unsafe { &*raw_arguments }.deserialize(); + + unsafe { + ffi::free( raw_arguments as *const u8 ); + } + + if arguments.len() != F::expected_argument_count() { + // TODO: Should probably throw an exception into the JS world or something like that. + panic!( "Expected {} arguments, got {}", F::expected_argument_count(), arguments.len() ); + } + + let mut arguments = arguments.drain( .. ); + let mut nth_argument = 0; + $( + let $kind = match arguments.next().unwrap().try_into() { + Ok( value ) => value, + Err( _ ) => { + panic!( "Argument #{} is not convertible", nth_argument + 1 ); + } + }; + + nth_argument += 1; + )* + + $crate::private::noop( &mut nth_argument ); + + let result = callback.call_mut( ($($kind,)*) ); + let mut arena = PreallocatedArena::new( result.memory_required_owned() ); + + let mut result = Some( result ); + let result = JsSerializableOwned::into_js_owned( &mut result, &mut arena ); + let result = &result as *const _; + + // This is kinda hacky but I'm not sure how else to do it at the moment. + em_asm_int!( "Module.STDWEB.tmp = Module.STDWEB.to_js( $0 );", result ); + } + + extern fn deallocator< $($kind: TryFrom< Value >,)* F: CallMut< ($($kind,)*) > >( callback: *mut F ) { + let callback = unsafe { + Box::from_raw( callback ) + }; + + drop( callback ); + } + + SerializedUntaggedFunction { + adapter_pointer: funcall_adapter::< $($kind,)* F > as u32, + pointer: callback as u32, + deallocator_pointer: deallocator::< $($kind,)* F > as u32 + }.into() + } + + #[inline] + fn memory_required_owned( &self ) -> usize { + 0 + } + } + + next! { $next } + } +} + +loop_through_identifiers!( impl_for_fn ); + +impl< 'a, T: ?Sized + JsSerializable > JsSerializable for &'a T { + #[inline] + fn into_js< 'x >( &'x self, arena: &'x PreallocatedArena ) -> SerializedValue< 'x > { + T::into_js( *self, arena ) + } + + #[inline] + fn memory_required( &self ) -> usize { + T::memory_required( *self ) + } +} + +#[cfg(test)] +mod test_deserialization { + use std::rc::Rc; + use std::cell::Cell; + use super::*; + + #[test] + fn i32() { + assert_eq!( js! { return 100; }, Value::Number( 100_i32.into() ) ); + } + + #[test] + fn f64() { + assert_eq!( js! { return 100.5; }, Value::Number( 100.5_f64.into() ) ); + } + + #[test] + fn bool_true() { + assert_eq!( js! { return true; }, Value::Bool( true ) ); + } + + #[test] + fn bool_false() { + assert_eq!( js! { return false; }, Value::Bool( false ) ); + } + + #[test] + fn undefined() { + assert_eq!( js! { return undefined; }, Value::Undefined ); + } + + #[test] + fn null() { + assert_eq!( js! { return null; }, Value::Null ); + } + + #[test] + fn string() { + assert_eq!( js! { return "Dog"; }, Value::String( "Dog".to_string() ) ); + } + + #[test] + fn array() { + assert_eq!( js! { return [1, 2]; }, Value::Array( vec![ Value::Number( 1.into() ), Value::Number( 2.into() ) ] ) ); + } + + #[test] + fn object() { + assert_eq!( js! { return {"one": 1, "two": 2}; }, Value::Object( [("one".to_string(), Value::Number(1.into())), ("two".to_string(), Value::Number(2.into()))].iter().cloned().collect() ) ); + } + + #[test] + fn reference() { + assert_eq!( js! { return new Date(); }.is_reference(), true ); + } + + #[test] + fn arguments() { + let value = js! { + return (function() { + return arguments; + })( 1, 2 ); + }; + + assert_eq!( value, Value::Array( vec![ Value::Number( 1.into() ), Value::Number( 2.into() ) ] ) ); + } + + #[test] + fn function() { + let value = Rc::new( Cell::new( 0 ) ); + let fn_value = value.clone(); + + js! { + var callback = @{move || { fn_value.set( 1 ); }}; + callback(); + callback.drop(); + }; + + assert_eq!( value.get(), 1 ); + } + + #[test] + fn function_returning_bool() { + let result = js! { + var callback = @{move || { return true }}; + var result = callback(); + callback.drop(); + + return result; + }; + + assert_eq!( result, Value::Bool( true ) ); + } + + #[test] + fn function_with_single_bool_argument() { + let value = Rc::new( Cell::new( false ) ); + let fn_value = value.clone(); + + js! { + var callback = @{move |value: bool| { fn_value.set( value ); }}; + callback( true ); + callback.drop(); + }; + + assert_eq!( value.get(), true ); + } +} + +#[cfg(test)] +mod test_serialization { + use super::*; + + #[test] + fn object() { + let object: BTreeMap< _, _ > = [ + ("number".to_string(), Value::Number( 123.into() )), + ("string".to_string(), Value::String( "Hello!".into() )) + ].iter().cloned().collect(); + + let result = js! { + var object = @{object}; + return object.number === 123 && object.string === "Hello!" && Object.keys( object ).length == 2; + }; + assert_eq!( result, Value::Bool( true ) ); + } + + #[test] + fn multiple() { + let reference: Reference = js! { + return new Date(); + }.try_into().unwrap(); + + let result = js! { + var callback = @{|| {}}; + var reference = @{&reference}; + var string = @{"Hello!"}; + return Object.prototype.toString.call( callback ) == "[object Function]" && + Object.prototype.toString.call( reference ) == "[object Date]" && + Object.prototype.toString.call( string ) == "[object String]" + }; + assert_eq!( result, Value::Bool( true ) ); + } +} + +#[cfg(test)] +mod test_reserialization { + use super::*; + + #[test] + fn i32() { + assert_eq!( js! { return @{100}; }, Value::Number( 100_i32.into() ) ); + } + + #[test] + fn f64() { + assert_eq!( js! { return @{100.5}; }, Value::Number( 100.5_f64.into() ) ); + } + + #[test] + fn bool_true() { + assert_eq!( js! { return @{true}; }, Value::Bool( true ) ); + } + + #[test] + fn bool_false() { + assert_eq!( js! { return @{false}; }, Value::Bool( false ) ); + } + + #[test] + fn undefined() { + assert_eq!( js! { return @{Undefined}; }, Value::Undefined ); + } + + #[test] + fn null() { + assert_eq!( js! { return @{Null}; }, Value::Null ); + } + + #[test] + fn string() { + assert_eq!( js! { return @{"Dog"}; }, Value::String( "Dog".to_string() ) ); + } + + #[test] + fn array() { + let array = vec![ Value::Number( 1.into() ), Value::Number( 2.into() ) ]; + assert_eq!( js! { return @{&[1, 2][..]}; }, Value::Array( array ) ); + } + + #[test] + fn object() { + let object = [("one".to_string(), Value::Number(1.into())), ("two".to_string(), Value::Number(2.into()))].iter().cloned().collect(); + assert_eq!( js! { return @{&object}; }, Value::Object( object ) ); + } + + #[test] + fn reference() { + let date = js! { return new Date(); }; + assert_eq!( js! { return Object.prototype.toString.call( @{date} ) }, "[object Date]" ); + } + + #[test] + fn reference_by_ref() { + let date = js! { return new Date(); }; + assert_eq!( js! { return Object.prototype.toString.call( @{&date} ) }, "[object Date]" ); + } + + #[test] + fn option_some() { + assert_eq!( js! { return @{Some( true )}; }, Value::Bool( true ) ); + } + + #[test] + fn option_none() { + let boolean_none: Option< bool > = None; + assert_eq!( js! { return @{boolean_none}; }, Value::Null ); + } + + #[test] + fn value() { + assert_eq!( js! { return @{Value::String( "Dog".to_string() )}; }, Value::String( "Dog".to_string() ) ); + } + + #[test] + fn closure_context() { + let constant: u32 = 0x12345678; + let callback = move || { + let value: Value = constant.into(); + value + }; + + let value = js! { + return @{callback}(); + }; + + assert_eq!( value, Value::Number( 0x12345678_i32.into() ) ); + } +} diff --git a/src/webcore/try_from.rs b/src/webcore/try_from.rs new file mode 100644 index 00000000..b0278525 --- /dev/null +++ b/src/webcore/try_from.rs @@ -0,0 +1,29 @@ +/// Attempt to construct Self via a conversion. +/// +/// This definition is only temporary until Rust's `TryFrom` is stabilized. +pub trait TryFrom< T >: Sized { + /// The type returned in the event of a conversion error. + type Error; + + /// Performs the conversion. + fn try_from( T ) -> Result< Self, Self::Error >; +} + +/// An attempted conversion that consumes self, which may or may not be expensive. +/// +/// his definition is only temporary until Rust's `TryInto` is stabilized. +pub trait TryInto< T >: Sized { + /// The type returned in the event of a conversion error. + type Error; + + /// Performs the conversion. + fn try_into( self ) -> Result< T, Self::Error >; +} + +impl< T, U > TryInto< U > for T where U: TryFrom< T > { + type Error = U::Error; + #[inline] + fn try_into( self ) -> Result< U, U::Error > { + U::try_from( self ) + } +} diff --git a/src/webcore/value.rs b/src/webcore/value.rs new file mode 100644 index 00000000..4ce44b88 --- /dev/null +++ b/src/webcore/value.rs @@ -0,0 +1,1137 @@ +use std::collections::{BTreeMap, HashMap}; +use std::hash::Hash; +use std::fmt; +use std::error; +use webcore::try_from::{TryFrom, TryInto}; +use webcore::number::{self, Number}; + +/// A unit type representing JavaScript's `undefined`. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub struct Undefined; + +/// A unit type representing JavaScript's `null`. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub struct Null; + +/// A type representing a reference to a JavaScript value. +#[repr(C)] +#[derive(PartialEq, Eq, Debug)] +pub struct Reference( i32 ); + +impl Reference { + #[doc(hidden)] + #[inline] + pub unsafe fn from_raw_unchecked( refid: i32 ) -> Reference { + em_asm_int!( "Module.STDWEB.increment_refcount( $0 );", refid ); + Reference( refid ) + } + + #[doc(hidden)] + #[inline] + pub fn as_raw( &self ) -> i32 { + self.0 + } + + /// Converts this reference into the given type `T`; checks whenever the reference + /// is really of type `T` and returns `None` if it's not. + #[inline] + pub fn downcast< T: FromReference >( self ) -> Option< T > { + T::from_reference( self ) + } +} + +impl Clone for Reference { + #[inline] + fn clone( &self ) -> Self { + unsafe { + Reference::from_raw_unchecked( self.as_raw() ) + } + } +} + +impl Drop for Reference { + #[inline] + fn drop( &mut self ) { + em_asm_int!( "Module.STDWEB.decrement_refcount( $0 );", self.0 ); + } +} + +impl AsRef< Reference > for Reference { + #[inline] + fn as_ref( &self ) -> &Self { + self + } +} + +macro_rules! __impl_infallible_try_from { + (($($impl_arg:tt)*) ($($src_arg:tt)*) ($($dst_arg:tt)*) ($($bounds:tt)*)) => { + impl< $($impl_arg)* > TryFrom< $($src_arg)* > for $($dst_arg)* where $($bounds)* { + type Error = $crate::unstable::Void; + + #[inline] + fn try_from( source: $($src_arg)* ) -> Result< Self, Self::Error > { + Ok( source.into() ) + } + } + }; +} + +macro_rules! impl_infallible_try_from { + (impl< $($impl_arg:tt),* > for $src:ty => $dst:ty where ($($bounds:tt)*); $($rest:tt)*) => { + __impl_infallible_try_from!( ($($impl_arg),*) ($src) ($dst) ($($bounds)*) ); + impl_infallible_try_from!( $($rest)* ); + }; + + (impl< $($impl_arg:tt),* > for $src:ty => $dst:ty; $($rest:tt)*) => { + __impl_infallible_try_from!( ($($impl_arg),*) ($src) ($dst) () ); + impl_infallible_try_from!( $($rest)* ); + }; + + ($src:ty => $dst:ty; $($rest:tt)*) => { + __impl_infallible_try_from!( () ($src) ($dst) () ); + impl_infallible_try_from!( $($rest)* ); + + }; + + () => {}; +} + +impl_infallible_try_from! { + Reference => Reference; + impl< 'a > for &'a Reference => &'a Reference; +} + +#[doc(hidden)] +pub trait FromReferenceUnchecked: Sized { + unsafe fn from_reference_unchecked( reference: Reference ) -> Self; + + #[inline] + unsafe fn from_value_unchecked( value: Value ) -> Option< Self > { + let reference: Option< Reference > = value.try_into().ok(); + reference.map( |reference| Self::from_reference_unchecked( reference ) ) + } +} + +#[doc(hidden)] +pub trait FromReference: FromReferenceUnchecked { + fn from_reference( reference: Reference ) -> Option< Self >; +} + +impl FromReferenceUnchecked for Reference { + #[inline] + unsafe fn from_reference_unchecked( reference: Reference ) -> Self { + reference + } +} + +/// A type representing a JavaScript value. +/// +/// This type implements a rich set of conversions +/// from and into standard Rust types, for example: +/// +/// ```rust +/// let v1: Value = "Hello world!".into(); +/// let v2: Value = true.into(); +/// let v3: Value = vec![ 1, 2, 3 ].into(); +/// let v4: Value = Null.into(); +/// let v5: Value = 123_u64.try_into().unwrap(); +/// +/// let v1_r: String = v1.try_into().unwrap(); +/// let v2_r: bool = v2.try_into().unwrap(); +/// let v3_r: Vec< i32 > = v3.try_into().unwrap(); +/// let v4_r: Option< String > = v4.try_into().unwrap(); // Will be `None`. +/// let v5_r: u64 = v5.try_into().unwrap(); +/// ``` +#[allow(missing_docs)] +#[derive(Clone, PartialEq, Debug)] +pub enum Value { + Undefined, + Null, + Bool( bool ), + Number( Number ), + String( String ), + Array( Vec< Value > ), + Object( BTreeMap< String, Value > ), // TODO: Use our own type instead of using BTreeMap directly. + Reference( Reference ) +} + +impl Value { + /// Checks whenever the Value is of the Reference variant. + #[inline] + pub fn is_reference( &self ) -> bool { + if let Value::Reference( _ ) = *self { + true + } else { + false + } + } + + /// Gets a reference to the [Reference](struct.Reference.html) inside this `Value`. + #[inline] + pub fn as_reference( &self ) -> Option< &Reference > { + match *self { + Value::Reference( ref reference ) => Some( reference ), + _ => None + } + } + + /// Returns the [Reference](struct.Reference.html) inside this `Value`. + #[inline] + pub fn into_reference( self ) -> Option< Reference > { + match self { + Value::Reference( reference ) => Some( reference ), + _ => None + } + } + + /// Converts a [Reference](struct.Reference.html) inside this `Value` into + /// the given type `T`; doesn't check whenever the reference is really of type `T`. + /// + /// In cases where the value is not a `Reference` a `None` is returned. + #[inline] + pub unsafe fn into_reference_unchecked< T: FromReferenceUnchecked >( self ) -> Option< T > { + T::from_value_unchecked( self ) + } + + /// Returns the `String` inside this `Value`. + #[inline] + pub fn into_string( self ) -> Option< String > { + match self { + Value::String( string ) => Some( string ), + _ => None + } + } + + /// Returns a borrow of the string inside this `Value`. + #[inline] + pub fn as_str( &self ) -> Option< &str > { + match *self { + Value::String( ref string ) => Some( string.as_str() ), + _ => None + } + } +} + +impl AsRef< Value > for Value { + #[inline] + fn as_ref( &self ) -> &Self { + self + } +} + +impl From< Undefined > for Value { + #[inline] + fn from( _: Undefined ) -> Self { + Value::Undefined + } +} + +impl< 'a > From< &'a Undefined > for Value { + #[inline] + fn from( _: &'a Undefined ) -> Self { + Value::Undefined + } +} + +impl< 'a > From< &'a mut Undefined > for Value { + #[inline] + fn from( _: &'a mut Undefined ) -> Self { + Value::Undefined + } +} + +impl From< Null > for Value { + #[inline] + fn from( _: Null ) -> Self { + Value::Null + } +} + +impl< 'a > From< &'a Null > for Value { + #[inline] + fn from( _: &'a Null ) -> Self { + Value::Null + } +} + +impl< 'a > From< &'a mut Null > for Value { + #[inline] + fn from( _: &'a mut Null ) -> Self { + Value::Null + } +} + +impl From< bool > for Value { + #[inline] + fn from( value: bool ) -> Self { + Value::Bool( value ) + } +} + +impl< 'a > From< &'a bool > for Value { + #[inline] + fn from( value: &'a bool ) -> Self { + Value::Bool( *value ) + } +} + +impl< 'a > From< &'a mut bool > for Value { + #[inline] + fn from( value: &'a mut bool ) -> Self { + (value as &bool).into() + } +} + +impl< 'a > From< &'a str > for Value { + #[inline] + fn from( value: &'a str ) -> Self { + Value::String( value.to_string() ) + } +} + +impl< 'a > From< &'a mut str > for Value { + #[inline] + fn from( value: &'a mut str ) -> Self { + (value as &str).into() + } +} + +impl From< String > for Value { + #[inline] + fn from( value: String ) -> Self { + Value::String( value ) + } +} + +impl< 'a > From< &'a String > for Value { + #[inline] + fn from( value: &'a String ) -> Self { + Value::String( value.clone() ) + } +} + +impl< 'a > From< &'a mut String > for Value { + #[inline] + fn from( value: &'a mut String ) -> Self { + (value as &String).into() + } +} + +impl From< char > for Value { + #[inline] + fn from( value: char ) -> Self { + let mut buffer: [u8; 4] = [0; 4]; + let string = value.encode_utf8( &mut buffer ); + string.to_owned().into() + } +} + +impl< 'a > From< &'a char > for Value { + #[inline] + fn from( value: &'a char ) -> Self { + (*value).into() + } +} + +impl< 'a > From< &'a mut char > for Value { + #[inline] + fn from( value: &'a mut char ) -> Self { + (*value).into() + } +} + +impl< T: Into< Value > > From< Vec< T > > for Value { + #[inline] + fn from( value: Vec< T > ) -> Self { + Value::Array( value.into_iter().map( |element| element.into() ).collect() ) + } +} + +impl< 'a, T > From< &'a Vec< T > > for Value where &'a T: Into< Value > { + #[inline] + fn from( value: &'a Vec< T > ) -> Self { + value[..].into() + } +} + +impl< 'a, T > From< &'a mut Vec< T > > for Value where &'a T: Into< Value > { + #[inline] + fn from( value: &'a mut Vec< T > ) -> Self { + value[..].into() + } +} + +impl< 'a, T > From< &'a [T] > for Value where &'a T: Into< Value > { + #[inline] + fn from( value: &'a [T] ) -> Self { + Value::Array( value.iter().map( |element| { + element.into() + }).collect() ) + } +} + +impl< 'a, T > From< &'a mut [T] > for Value where &'a T: Into< Value > { + #[inline] + fn from( value: &'a mut [T] ) -> Self { + (value as &[T]).into() + } +} + +// TODO: It would be nice to specialize this for values which are already of type Value. +impl< K: Into< String >, V: Into< Value > > From< BTreeMap< K, V > > for Value { + #[inline] + fn from( value: BTreeMap< K, V > ) -> Self { + let value = value.into_iter().map( |(key, value)| (key.into(), value.into()) ).collect(); + Value::Object( value ) + } +} + +impl< 'a, K, V > From< &'a BTreeMap< K, V > > for Value where &'a K: Into< String >, &'a V: Into< Value > { + #[inline] + fn from( value: &'a BTreeMap< K, V > ) -> Self { + let value = value.iter().map( |(key, value)| (key.into(), value.into()) ).collect(); + Value::Object( value ) + } +} + +impl< 'a, K, V > From< &'a mut BTreeMap< K, V > > for Value where &'a K: Into< String >, &'a V: Into< Value > { + #[inline] + fn from( value: &'a mut BTreeMap< K, V > ) -> Self { + let value: &BTreeMap< K, V > = value; + value.into() + } +} + +impl< K: Into< String > + Hash + Eq, V: Into< Value > > From< HashMap< K, V > > for Value { + #[inline] + fn from( value: HashMap< K, V > ) -> Self { + let value = value.into_iter().map( |(key, value)| (key.into(), value.into()) ).collect(); + Value::Object( value ) + } +} + +impl< 'a, K: Hash + Eq, V > From< &'a HashMap< K, V > > for Value where &'a K: Into< String >, &'a V: Into< Value > { + #[inline] + fn from( value: &'a HashMap< K, V > ) -> Self { + let value = value.iter().map( |(key, value)| (key.into(), value.into()) ).collect(); + Value::Object( value ) + } +} + +impl< 'a, K: Hash + Eq, V > From< &'a mut HashMap< K, V > > for Value where &'a K: Into< String >, &'a V: Into< Value > { + #[inline] + fn from( value: &'a mut HashMap< K, V > ) -> Self { + let value: &HashMap< K, V > = value; + value.into() + } +} + +impl From< Reference > for Value { + #[inline] + fn from( value: Reference ) -> Self { + Value::Reference( value ) + } +} + +impl< 'a > From< &'a Reference > for Value { + #[inline] + fn from( value: &'a Reference ) -> Self { + Value::Reference( value.clone() ) + } +} + +impl< 'a > From< &'a mut Reference > for Value { + #[inline] + fn from( value: &'a mut Reference ) -> Self { + (value as &Reference).into() + } +} + +macro_rules! impl_from_number { + ($($kind:ty)+) => { + $( + impl From< $kind > for Value { + #[inline] + fn from( value: $kind ) -> Self { + Value::Number( value.into() ) + } + } + + impl< 'a > From< &'a $kind > for Value { + #[inline] + fn from( value: &'a $kind ) -> Self { + Value::Number( (*value).into() ) + } + } + + impl< 'a > From< &'a mut $kind > for Value { + #[inline] + fn from( value: &'a mut $kind ) -> Self { + (value as &$kind).into() + } + } + + impl_infallible_try_from!( $kind => Value; ); + )+ + }; +} + +impl_from_number!( i8 i16 i32 u8 u16 u32 f32 f64 ); +impl_infallible_try_from! { + Undefined => Value; + impl< 'a > for &'a Undefined => Value; + impl< 'a > for &'a mut Undefined => Value; + Null => Value; + impl< 'a > for &'a Null => Value; + impl< 'a > for &'a mut Null => Value; + bool => Value; + impl< 'a > for &'a bool => Value; + impl< 'a > for &'a mut bool => Value; + impl< 'a > for &'a str => Value; + impl< 'a > for &'a mut str => Value; + String => Value; + impl< 'a > for &'a String => Value; + impl< 'a > for &'a mut String => Value; + char => Value; + impl< 'a > for &'a char => Value; + impl< 'a > for &'a mut char => Value; + impl< T > for Vec< T > => Value where (T: Into< Value >); + impl< 'a, T > for &'a Vec< T > => Value where (&'a T: Into< Value >); + impl< 'a, T > for &'a mut Vec< T > => Value where (&'a T: Into< Value >); + impl< 'a, T > for &'a [T] => Value where (&'a T: Into< Value >); + impl< 'a, T > for &'a mut [T] => Value where (&'a T: Into< Value >); + impl< K, V > for BTreeMap< K, V > => Value where (K: Into< String >, V: Into< Value >); + impl< 'a, K, V > for &'a BTreeMap< K, V > => Value where (&'a K: Into< String >, &'a V: Into< Value >); + impl< 'a, K, V > for &'a mut BTreeMap< K, V > => Value where (&'a K: Into< String >, &'a V: Into< Value >); + impl< K, V > for HashMap< K, V > => Value where (K: Into< String > + Hash + Eq, V: Into< Value >); + impl< 'a, K, V > for &'a HashMap< K, V > => Value where (K: Hash + Eq, &'a K: Into< String >, &'a V: Into< Value >); + impl< 'a, K, V > for &'a mut HashMap< K, V > => Value where (K: Hash + Eq, &'a K: Into< String >, &'a V: Into< Value >); + Reference => Value; +} + +macro_rules! impl_try_from_number { + ($($kind:ty)+) => { + $( + impl TryFrom< $kind > for Value { + type Error = >::Error; + + #[inline] + fn try_from( value: $kind ) -> Result< Self, Self::Error > { + Ok( Value::Number( value.try_into()? ) ) + } + } + )+ + }; +} + +impl_try_from_number!( i64 u64 ); + +impl PartialEq< Undefined > for Value { + #[inline] + fn eq( &self, _: &Undefined ) -> bool { + match *self { + Value::Undefined => true, + _ => false + } + } +} + +impl PartialEq< Null > for Value { + #[inline] + fn eq( &self, _: &Null ) -> bool { + match *self { + Value::Null => true, + _ => false + } + } +} + +impl PartialEq< bool > for Value { + #[inline] + fn eq( &self, right: &bool ) -> bool { + match *self { + Value::Bool( left ) => left == *right, + _ => false + } + } +} + +impl PartialEq< str > for Value { + #[inline] + fn eq( &self, right: &str ) -> bool { + match *self { + Value::String( ref left ) => left == right, + _ => false + } + } +} + +impl PartialEq< String > for Value { + #[inline] + fn eq( &self, right: &String ) -> bool { + match *self { + Value::String( ref left ) => left == right, + _ => false + } + } +} + +impl< T > PartialEq< [T] > for Value where Value: PartialEq< T > { + #[inline] + fn eq( &self, right: &[T] ) -> bool { + match *self { + Value::Array( ref left ) => left.iter().zip( right.iter() ).all( |(left, right)| left == right ), + _ => false + } + } +} + +impl< 'a, T > PartialEq< &'a [T] > for Value where Value: PartialEq< T > { + #[inline] + fn eq( &self, right: &&'a [T] ) -> bool { + >::eq( self, right ) + } +} + +impl PartialEq< Number > for Value { + #[inline] + fn eq( &self, right: &Number ) -> bool { + match *self { + Value::Number( left ) => left == *right, + _ => false + } + } +} + +impl< T: AsRef< Reference > > PartialEq< T > for Value { + #[inline] + fn eq( &self, right: &T ) -> bool { + match *self { + Value::Reference( ref left ) => left == right.as_ref(), + _ => false + } + } +} + +impl< 'a > PartialEq< Reference > for &'a Value { + #[inline] + fn eq( &self, right: &Reference ) -> bool { + (*self).eq( right ) + } +} + +impl PartialEq< Value > for Reference { + #[inline] + fn eq( &self, right: &Value ) -> bool { + right.eq( self ) + } +} + +impl< 'a > PartialEq< &'a Value > for Reference { + #[inline] + fn eq( &self, right: &&'a Value ) -> bool { + let right: &'a Value = right; + right.eq( self ) + } +} + +impl< 'a > PartialEq< Value > for &'a Reference { + #[inline] + fn eq( &self, right: &Value ) -> bool { + (*self).eq( right ) + } +} + +macro_rules! impl_partial_eq_boilerplate { + ( $( $kind:ty ),+ ) => { + $( + impl< 'a > PartialEq< &'a $kind > for Value { + #[inline] + fn eq( &self, right: &&'a $kind ) -> bool { + let right: &'a $kind = right; + self.eq( right ) + } + } + + impl< 'a > PartialEq< $kind > for &'a Value { + #[inline] + fn eq( &self, right: &$kind ) -> bool { + (*self).eq( right ) + } + } + + impl PartialEq< Value > for $kind { + #[inline] + fn eq( &self, right: &Value ) -> bool { + right == self + } + } + + impl< 'a > PartialEq< &'a Value > for $kind { + #[inline] + fn eq( &self, right: &&'a Value ) -> bool { + let right: &'a Value = right; + right == self + } + } + + impl< 'a > PartialEq< Value > for &'a $kind { + #[inline] + fn eq( &self, right: &Value ) -> bool { + (*self).eq( right ) + } + } + )+ + } +} + +macro_rules! impl_partial_eq_to_number { + ($($kind:ty)+) => { + $( + impl PartialEq< $kind > for Value { + #[inline] + fn eq( &self, right: &$kind ) -> bool { + match *self { + Value::Number( left ) => left == *right, + _ => false + } + } + } + + impl_partial_eq_boilerplate!( $kind ); + )+ + }; +} + +impl_partial_eq_to_number!( i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 ); + +impl_partial_eq_boilerplate! { + Undefined, + Null, + bool, + str, + String, + Number +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum ConversionError { + TypeMismatch { + actual_type: &'static str + }, + NumericConversionError( number::ConversionError ), + ValueConversionError( Box< ConversionError > ) +} + +fn value_type_name( value: &Value ) -> &'static str { + match *value { + Value::Undefined => "Undefined", + Value::Null => "Null", + Value::Bool( _ ) => "Bool", + Value::Number( _ ) => "Number", + Value::String( _ ) => "String", + Value::Array( _ ) => "Array", + Value::Object( _ ) => "Object", + Value::Reference( _ ) => "Reference" + } +} + +impl fmt::Display for ConversionError { + fn fmt( &self, formatter: &mut fmt::Formatter ) -> Result< (), fmt::Error > { + match *self { + ConversionError::TypeMismatch { actual_type } => write!( formatter, "type mismatch; actual type is {}", actual_type ), + ConversionError::NumericConversionError( ref inner ) => write!( formatter, "{}", inner ), + ConversionError::ValueConversionError( ref inner ) => write!( formatter, "value conversion error: {}", inner ) + } + } +} + +impl error::Error for ConversionError { + fn description( &self ) -> &str { + match *self { + ConversionError::TypeMismatch { .. } => "type mismatch", + ConversionError::NumericConversionError( ref inner ) => inner.description(), + ConversionError::ValueConversionError( _ ) => "value conversion error" + } + } +} + +impl From< number::ConversionError > for ConversionError { + fn from( inner: number::ConversionError ) -> Self { + ConversionError::NumericConversionError( inner ) + } +} + +impl ConversionError { + #[inline] + fn type_mismatch( actual_value: &Value ) -> Self { + ConversionError::TypeMismatch { + actual_type: value_type_name( actual_value ) + } + } + + #[inline] + fn value_conversion_error( inner: ConversionError ) -> Self { + ConversionError::ValueConversionError( Box::new( inner ) ) + } +} + +impl TryFrom< Value > for Undefined { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Undefined => Ok( Undefined ), + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +impl TryFrom< Value > for Null { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Null => Ok( Null ), + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +impl TryFrom< Value > for bool { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Bool( value ) => Ok( value ), + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +macro_rules! impl_try_into_number { + ($($kind:ty)+) => { + $( + impl TryFrom< Value > for $kind { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Number( value ) => { + let result: Result< Self, _ > = value.try_into(); + result.map_err( |error| error.into() ) + }, + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } + } + )+ + }; +} + +impl_try_into_number!( u8 u16 u32 u64 i8 i16 i32 i64 f64 ); + +impl< V: TryFrom< Value, Error = ConversionError > > TryFrom< Value > for BTreeMap< String, V > { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Object( object ) => { + let mut output = BTreeMap::new(); + for (key, value) in object { + let value = match value.try_into() { + Ok( value ) => value, + Err( error ) => return Err( ConversionError::value_conversion_error( error ) ) + }; + output.insert( key, value ); + } + Ok( output ) + }, + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +impl< V: TryFrom< Value, Error = ConversionError > > TryFrom< Value > for HashMap< String, V > { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Object( object ) => { + let mut output = HashMap::with_capacity( object.len() ); + for (key, value) in object { + let value = match value.try_into() { + Ok( value ) => value, + Err( error ) => return Err( ConversionError::value_conversion_error( error ) ) + }; + output.insert( key, value ); + } + Ok( output ) + }, + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +impl< T: TryFrom< Value, Error = ConversionError > > TryFrom< Value > for Vec< T > { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Array( array ) => { + let mut output = Vec::with_capacity( array.len() ); + for value in array { + let value = match value.try_into() { + Ok( value ) => value, + Err( error ) => return Err( ConversionError::value_conversion_error( error ) ) + }; + output.push( value ); + } + Ok( output ) + }, + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +impl TryFrom< Value > for String { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::String( value ) => Ok( value ), + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +impl TryFrom< Value > for Reference { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Reference( value ) => Ok( value ), + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +impl< 'a > TryFrom< &'a Value > for &'a str { + type Error = ConversionError; + + #[inline] + fn try_from( value: &'a Value ) -> Result< Self, Self::Error > { + match *value { + Value::String( ref value ) => Ok( value ), + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +impl< 'a > TryFrom< &'a Value > for &'a Reference { + type Error = ConversionError; + + #[inline] + fn try_from( value: &'a Value ) -> Result< Self, Self::Error > { + match *value { + Value::Reference( ref value ) => Ok( value ), + _ => Err( ConversionError::type_mismatch( &value ) ) + } + } +} + +macro_rules! __impl_nullable_try_from_value { + (($($impl_arg:tt)*) ($($dst_arg:tt)*) ($($bounds:tt)*)) => { + impl< $($impl_arg)* > TryFrom< Value > for Option< $($dst_arg)* > where $($bounds)* { + type Error = ConversionError; + + #[inline] + fn try_from( value: Value ) -> Result< Self, Self::Error > { + match value { + Value::Undefined | Value::Null => Ok( None ), + value => value.try_into().map( Some ) + } + } + } + }; +} + +macro_rules! impl_nullable_try_from_value { + (impl< $($impl_arg:tt),* > $dst:ty where ($($bounds:tt)*); $($rest:tt)*) => { + __impl_nullable_try_from_value!( ($($impl_arg),*) ($dst) ($($bounds)*) ); + impl_nullable_try_from_value!( $($rest)* ); + }; + + (impl< $($impl_arg:tt),* > $dst:ty; $($rest:tt)*) => { + __impl_nullable_try_from_value!( ($($impl_arg),*) ($dst) () ); + impl_nullable_try_from_value!( $($rest)* ); + }; + + ($dst:ty; $($rest:tt)*) => { + __impl_nullable_try_from_value!( () ($dst) () ); + impl_nullable_try_from_value!( $($rest)* ); + + }; + + () => {}; +} + +impl_nullable_try_from_value! { + bool; + u8; + u16; + u32; + u64; + i8; + i16; + i32; + i64; + f64; + impl< V > BTreeMap< String, V > where (V: TryFrom< Value, Error = ConversionError >); + impl< V > HashMap< String, V > where (V: TryFrom< Value, Error = ConversionError >); + impl< T > Vec< T > where (T: TryFrom< Value, Error = ConversionError >); + String; + Reference; +} + +impl< 'a > TryFrom< &'a Value > for Option< &'a str > { + type Error = ConversionError; + + #[inline] + fn try_from( value: &'a Value ) -> Result< Self, Self::Error > { + match *value { + Value::String( ref value ) => Ok( Some( value ) ), + ref value => value.try_into().map( Some ) + } + } +} + +impl< 'a > TryFrom< &'a Value > for Option< &'a Reference > { + type Error = ConversionError; + + #[inline] + fn try_from( value: &'a Value ) -> Result< Self, Self::Error > { + match *value { + Value::Reference( ref value ) => Ok( Some( value ) ), + ref value => value.try_into().map( Some ) + } + } +} + +#[cfg(test)] +mod tests { + use super::{Value, Reference}; + use webcore::try_from::TryInto; + + #[test] + fn string_equality() { + let value = Value::String( "Hello!".to_owned() ); + assert!( value == "Hello!" ); + assert!( &value == "Hello!" ); + assert!( value == "Hello!".to_owned() ); + assert!( &value == "Hello!".to_owned() ); + assert!( value == &"Hello!".to_owned() ); + assert!( &value == &"Hello!".to_owned() ); + assert!( "Hello!" == value ); + assert!( "Hello!" == &value ); + assert!( "Hello!".to_owned() == value ); + assert!( "Hello!".to_owned() == &value ); + assert!( &"Hello!".to_owned() == value ); + assert!( &"Hello!".to_owned() == &value ); + + assert!( value != "Bob" ); + } + + #[test] + fn array_equality() { + let value = Value::Array( vec![ Value::Bool( true ), Value::Bool( false ) ] ); + assert!( value == &[true, false][..] ); + assert!( value != &[true, true][..] ); + // Looks like it's not possible to define a symmetric PartialEq for arrays. ); + } + + #[test] + fn reference_equality() { + let value = js! { return new Date() }; + let reference: Reference = value.clone().try_into().unwrap(); + + assert!( value == reference ); + assert!( &value == reference ); + assert!( value == &reference ); + assert!( &value == &reference ); + assert!( reference == value ); + assert!( &reference == value ); + assert!( reference == &value ); + assert!( &reference == &value ); + } + + pub struct Error( Reference ); + reference_boilerplate! { + Error, + instanceof Error + } + + pub struct ReferenceError( Reference ); + reference_boilerplate! { + ReferenceError, + instanceof ReferenceError + convertible to Error + } + + pub struct TypeError( Reference ); + reference_boilerplate! { + TypeError, + instanceof TypeError + convertible to Error + } + + #[test] + fn reference_downcast() { + let reference = js! { return new ReferenceError(); }.into_reference().unwrap(); + assert!( reference.clone().downcast::< Error >().is_some() ); + assert!( reference.clone().downcast::< ReferenceError >().is_some() ); + assert!( reference.clone().downcast::< TypeError >().is_none() ); + } + + #[test] + fn reference_try_into_downcast_from_reference() { + let reference = js! { return new ReferenceError(); }.into_reference().unwrap(); + let typed_reference: Result< Error, _ > = reference.clone().try_into(); + assert!( typed_reference.is_ok() ); + + let typed_reference: Result< ReferenceError, _ > = reference.clone().try_into(); + assert!( typed_reference.is_ok() ); + + let typed_reference: Result< TypeError, _ > = reference.clone().try_into(); + assert!( typed_reference.is_err() ); + } + + #[test] + fn reference_try_into_downcast_from_value() { + let value = js! { return new ReferenceError(); }; + let typed_reference: Result< Error, _ > = value.clone().try_into(); + assert!( typed_reference.is_ok() ); + + let typed_reference: Result< ReferenceError, _ > = value.clone().try_into(); + assert!( typed_reference.is_ok() ); + + let typed_reference: Result< TypeError, _ > = value.clone().try_into(); + assert!( typed_reference.is_err() ); + } + + #[test] + fn reference_into_upcast() { + let reference: ReferenceError = js! { return new ReferenceError(); }.into_reference().unwrap().downcast().unwrap(); + let _: Error = reference.clone().into(); + let _: Reference = reference.clone().into(); + } +} diff --git a/src/webcore/void.rs b/src/webcore/void.rs new file mode 100644 index 00000000..7c868621 --- /dev/null +++ b/src/webcore/void.rs @@ -0,0 +1,25 @@ +use std::fmt; +use std::error; + +/// An uninhabited type for use in statically impossible cases. +/// +/// Will be replaced by Rust's `!` type once that stabilizes. +pub enum Void {} + +impl fmt::Debug for Void { + fn fmt( &self, _: &mut fmt::Formatter ) -> Result< (), fmt::Error > { + unreachable!(); + } +} + +impl fmt::Display for Void { + fn fmt( &self, _: &mut fmt::Formatter ) -> Result< (), fmt::Error > { + unreachable!(); + } +} + +impl error::Error for Void { + fn description( &self ) -> &str { + unreachable!(); + } +}