diff --git a/.gitignore b/.gitignore index 96ef6c0..2c96eb1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -/target +target/ Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index 346a78c..7b11475 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "weblog" -version = "0.2.0" +version = "0.3.0-beta.1" authors = ["Rob Watson "] edition = "2018" description = "weblog is a crate that defines a set of macros for calling `console.log()` and other members of the browser's console API when targeting Wasm." @@ -15,12 +15,11 @@ doctest = true [features] default = ["web_sys"] -web_sys = ["js-sys", "paste", "wasm-bindgen", "web-sys"] +web_sys = ["web-sys", "wasm-bindgen", "weblog-proc-macro"] std_web = ["stdweb"] [dependencies] -js-sys = { version = ">= 0.3", optional = true } -paste = { version = "1.0", optional = true } stdweb = { version = ">= 0.4", optional = true } -wasm-bindgen = { version = ">= 0.2", optional = true } -web-sys = { version = ">= 0.3", optional = true, features = ["console"] } +weblog-proc-macro = { path = "./weblog-proc-macro", version = "0.3.0-beta.1", optional = true } +web-sys = { version = "0.3", features = ["console"], optional = true } +wasm-bindgen = { version = "0.2", optional = true } diff --git a/Makefile b/Makefile index 3f8d698..ed902f6 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,7 @@ +all: build + +build: + cargo build + docs: cargo doc --all --no-deps --target wasm32-unknown-unknown && cargo doc --open diff --git a/README.md b/README.md index 2b55d72..2fd1cca 100644 --- a/README.md +++ b/README.md @@ -54,23 +54,31 @@ No stringification is performed on the Rust side - so objects will be fully intr ```toml # Defaults to web-sys -weblog = "0.2" +weblog = "0.3.0-beta.1" # For stdweb: -weblog = { version = "0.2", default-features = false, features = ["std_web"] } +weblog = { version = "0.3.0-beta.1", default-features = false, features = ["std_web"] } ``` See the documentation for usage examples. The crate currently exposes the following macros: +* `console_assert!` * `console_clear!` +* `console_count!` +* `console_count_reset!` * `console_debug!` * `console_dir!` * `console_dirxml!` * `console_error!` +* `console_exception!` * `console_info!` * `console_log!` +* `console_table!` +* `console_time!` +* `console_time_end!` +* `console_time_stamp!` * `console_trace!` * `console_warn!` diff --git a/examples/web-sys/.gitignore b/examples/web-sys/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/examples/web-sys/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/examples/web-sys/Cargo.toml b/examples/web-sys/Cargo.toml new file mode 100644 index 0000000..e90efb1 --- /dev/null +++ b/examples/web-sys/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "weblog-example-web-sys" +version = "0.3.0-beta.1" +authors = ["Rob Watson "] +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +weblog = { path = "../.." } +wasm-bindgen = "0.2" diff --git a/examples/web-sys/Makefile b/examples/web-sys/Makefile new file mode 100644 index 0000000..62530d0 --- /dev/null +++ b/examples/web-sys/Makefile @@ -0,0 +1,7 @@ +all: build + +build: + wasm-pack build --target web --out-name wasm --out-dir ./static + +run: + simple-http-server -i static -p 3000 --nocache --try-file ./static/index.html diff --git a/examples/web-sys/src/lib.rs b/examples/web-sys/src/lib.rs new file mode 100644 index 0000000..06dfc76 --- /dev/null +++ b/examples/web-sys/src/lib.rs @@ -0,0 +1,76 @@ +use wasm_bindgen::prelude::*; +use weblog::*; + +#[wasm_bindgen(start)] +pub fn main() { + // A simple example. + console_log!("Hello world"); + + // Multiple arguments. + console_log!("Hello", "there", "world"); + + // Various types. + console_log!( + 1.0, + 2f64, + true, + "&str", + String::from("owned string"), + Some("an option"), + None as Option, + ); + + // Various levels. + console_debug!("debug"); + console_info!("informational"); + console_warn!("warning"); + console_error!("an", "error", "occurred"); + console_exception!("an", "exception", "occurred"); + + // Print a stacktrace. + console_trace!(); + + // And include some data. + console_trace!("some", "data", 1, 2, 3); + + // Unlimited arguments. + // + // Note that due to the design of the web-sys library, passing greater + // than 7 variadic arguments (or 8 in the case of console_assert!) + // implies a performance hit. + console_log!(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + // With a trailing comma. + console_log!(1,); + + // With no arguments. + console_log!(); + + // With an assertion. + console_assert!(true); // outputs nothing + console_assert!(false); + console_assert!(false, "Something went wrong"); + console_assert!(false, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, "Done!"); + + // With a simple count. + console_count!(); + console_count_reset!(); + + // A count with a label. + console_count!("myCounter"); + console_count!("myCounter"); + console_count!("myCounter"); + console_count_reset!("myCounter"); + + // A timer. + console_time!(); + console_time_end!(); + console_time!("with a label"); + console_time_log!("with a label", "and", 1, "data"); + console_time_log!("with a label", 1, 2, 3, 4, 5, 6, 7, 8, "data"); + console_time_end!("with a label"); + + // A performance timestamp. + console_time_stamp!(); // outputs nothing to console + console_time_stamp!("with a label"); // outputs nothing to console +} diff --git a/examples/web-sys/static/.gitignore b/examples/web-sys/static/.gitignore new file mode 100644 index 0000000..f59ec20 --- /dev/null +++ b/examples/web-sys/static/.gitignore @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/examples/web-sys/static/index.html b/examples/web-sys/static/index.html new file mode 100644 index 0000000..fa029a2 --- /dev/null +++ b/examples/web-sys/static/index.html @@ -0,0 +1,15 @@ + + + + + Weblog Test + + + + +

Check developer tools console for log messages.

+ + diff --git a/src/console/mod.rs b/src/console/mod.rs index 4bfbcc9..c80f14c 100644 --- a/src/console/mod.rs +++ b/src/console/mod.rs @@ -1,5 +1,2 @@ -#[cfg(feature = "web_sys")] -pub mod web_sys; - #[cfg(feature = "std_web")] pub mod std_web; diff --git a/src/console/std_web.rs b/src/console/std_web.rs index 1b1f17c..2f1d7ee 100644 --- a/src/console/std_web.rs +++ b/src/console/std_web.rs @@ -1,26 +1,50 @@ pub extern crate stdweb; pub use stdweb::js; -#[doc = "Call the browser's `console.debug()` function."] +#[doc = "Call the browser's `console.assert()` function."] #[macro_export] -macro_rules! console_debug { - ($( $item:expr ),* ) => {{ - $crate::stdweb::js! { console.debug($( @{$item} ),*) } +macro_rules! console_assert { + ($($item:expr),* $(,)?) => {{ + $crate::stdweb::js! { console.assert($( @{$item} ),*) } }}; } #[doc = "Call the browser's `console.clear()` function."] #[macro_export] macro_rules! console_clear { - ($item:expr) => {{ + ($($item:expr),* $(,)?) => {{ $crate::stdweb::js! { console.clear() } }}; } +#[doc = "Call the browser's `console.count()` function."] +#[macro_export] +macro_rules! console_count { + ($($item:expr),* $(,)?) => {{ + $crate::stdweb::js! { console.count($( @{$item} ),*) } + }}; +} + +#[doc = "Call the browser's `console.countReset()` function."] +#[macro_export] +macro_rules! console_count_reset { + ($($item:expr),* $(,)?) => {{ + $crate::stdweb::js! { console.countReset($( @{$item} ),*) } + }}; +} + +#[doc = "Call the browser's `console.debug()` function."] +#[macro_export] +macro_rules! console_debug { + ($($item:expr),* $(,)?) => {{ + $crate::stdweb::js! { console.debug($( @{$item} ),*) } + }}; +} + #[doc = "Call the browser's `console.dir()` function."] #[macro_export] macro_rules! console_dir { - ($( $item:expr ),* ) => {{ + ($($item:expr),* $(,)?) => {{ $crate::stdweb::js! { console.dir($( @{$item} ),*) } }}; } @@ -28,7 +52,7 @@ macro_rules! console_dir { #[doc = "Call the browser's `console.dirxml()` function."] #[macro_export] macro_rules! console_dirxml { - ($( $item:expr ),* ) => {{ + ($($item:expr),* $(,)?) => {{ $crate::stdweb::js! { console.dirxml($( @{$item} ),*) } }}; } @@ -36,15 +60,23 @@ macro_rules! console_dirxml { #[doc = "Call the browser's `console.error()` function."] #[macro_export] macro_rules! console_error { - ($( $item:expr ),* ) => {{ + ($($item:expr),* $(,)?) => {{ $crate::stdweb::js! { console.error($( @{$item} ),*) } }}; } +#[doc = "Call the browser's `console.exception()` function."] +#[macro_export] +macro_rules! console_exception { + ($($item:expr),* $(,)?) => {{ + $crate::stdweb::js! { console.exception($( @{$item} ),*) } + }}; +} + #[doc = "Call the browser's `console.info()` function."] #[macro_export] macro_rules! console_info { - ($( $item:expr ),* ) => {{ + ($($item:expr),* $(,)?) => {{ $crate::stdweb::js! { console.info($( @{$item} ),*) } }}; } @@ -52,15 +84,55 @@ macro_rules! console_info { #[doc = "Call the browser's `console.log()` function."] #[macro_export] macro_rules! console_log { - ($( $item:expr ),* ) => {{ + ($($item:expr),* $(,)?) => {{ $crate::stdweb::js! { console.log($( @{$item} ),*) } }}; } +#[doc = "Call the browser's `console.table()` function."] +#[macro_export] +macro_rules! console_table { + ($($item:expr),* $(,)?) => {{ + $crate::stdweb::js! { console.table($( @{$item} ),*) } + }}; +} + +#[doc = "Call the browser's `console.time()` function."] +#[macro_export] +macro_rules! console_time { + ($($item:expr),* $(,)?) => {{ + $crate::stdweb::js! { console.time($( @{$item} ),*) } + }}; +} + +#[doc = "Call the browser's `console.timeEnd()` function."] +#[macro_export] +macro_rules! console_time_end { + ($($item:expr),* $(,)?) => {{ + $crate::stdweb::js! { console.timeEnd($( @{$item} ),*) } + }}; +} + +#[doc = "Call the browser's `console.timeLog()` function."] +#[macro_export] +macro_rules! console_time_log { + ($($item:expr),* $(,)?) => {{ + $crate::stdweb::js! { console.timeLog($( @{$item} ),*) } + }}; +} + +#[doc = "Call the browser's `console.timeStamp()` function."] +#[macro_export] +macro_rules! console_time_stamp { + ($($item:expr),* $(,)?) => {{ + $crate::stdweb::js! { console.timeStamp($( @{$item} ),*) } + }}; +} + #[doc = "Call the browser's `console.trace()` function."] #[macro_export] macro_rules! console_trace { - ($( $item:expr ),* ) => {{ + ($($item:expr),* $(,)?) => {{ $crate::stdweb::js! { console.trace($( @{$item} ),*) } }}; } @@ -68,7 +140,7 @@ macro_rules! console_trace { #[doc = "Call the browser's `console.warn()` function."] #[macro_export] macro_rules! console_warn { - ($( $item:expr ),* ) => {{ + ($($item:expr),* $(,)?) => {{ $crate::stdweb::js! { console.warn($( @{$item} ),*) } }}; } diff --git a/src/console/web_sys.rs b/src/console/web_sys.rs deleted file mode 100644 index e6ffd8b..0000000 --- a/src/console/web_sys.rs +++ /dev/null @@ -1,105 +0,0 @@ -use paste::paste; - -#[doc(hidden)] -pub mod __macro { - pub use js_sys::Array; - pub use wasm_bindgen::JsValue; - pub use web_sys::console; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __jsvalue { - ($value: expr) => { - ::std::convert::Into::<$crate::__macro::JsValue>::into($value) - }; -} - -macro_rules! websys_rules { - // $dollar is a workaround to allow repetition in nested macros. - // It needs to be the token `$`. - ($dollar:tt, $level:ident) => { - paste! { - #[doc = "Call the browser's `console." $level "()` function.\n\n\ - The web-sys crate accepts any amount of arguments, all of which must implement `Into`.\n\n\ - See the [wasm-bindgen documentation](../wasm_bindgen/struct.JsValue.html) for more information."] - #[macro_export] - macro_rules! [] { - () => { - $crate::__macro::console::[<$level _0>]() - }; - ($a:expr $dollar(,)?) => { - $crate::__macro::console::[<$level _1>](&$crate::__jsvalue!($a)) - }; - ($a:expr, $b:expr $dollar(,)?) => { - $crate::__macro::console::[<$level _2>](&$crate::__jsvalue!($a), &$crate::__jsvalue!($b)) - }; - ($a:expr, $b:expr, $c:expr $dollar(,)?) => { - $crate::__macro::console::[<$level _3>](&$crate::__jsvalue!($a), &$crate::__jsvalue!($b), &$crate::__jsvalue!($c)) - }; - ($a:expr, $b:expr, $c:expr, $d:expr $dollar(,)?) => { - $crate::__macro::console::[<$level _4>]( - &$crate::__jsvalue!($a), - &$crate::__jsvalue!($b), - &$crate::__jsvalue!($c), - &$crate::__jsvalue!($d), - ) - }; - ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr $dollar(,)?) => { - $crate::__macro::console::[<$level _5>]( - &$crate::__jsvalue!($a), - &$crate::__jsvalue!($b), - &$crate::__jsvalue!($c), - &$crate::__jsvalue!($d), - &$crate::__jsvalue!($e), - ) - }; - ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr $dollar(,)?) => { - $crate::__macro::console::[<$level _6>]( - &$crate::__jsvalue!($a), - &$crate::__jsvalue!($b), - &$crate::__jsvalue!($c), - &$crate::__jsvalue!($d), - &$crate::__jsvalue!($e), - &$crate::__jsvalue!($f), - ) - }; - ($a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr $dollar(,)?) => { - $crate::__macro::console::[<$level _7>]( - &$crate::__jsvalue!($a), - &$crate::__jsvalue!($b), - &$crate::__jsvalue!($c), - &$crate::__jsvalue!($d), - &$crate::__jsvalue!($e), - &$crate::__jsvalue!($f), - &$crate::__jsvalue!($g), - ) - }; - ($dollar($dollar item:expr),+ $dollar(,)?) => { - { - let args = ::std::vec![$dollar($crate::__jsvalue!($dollar item)),+]; - let args = ::std::iter::IntoIterator::into_iter(args).collect::<$crate::__macro::Array>(); - $crate::__macro::console::$level(&args) - } - }; - } - } - } -} - -websys_rules!($, debug); -websys_rules!($, dir); -websys_rules!($, dirxml); -websys_rules!($, error); -websys_rules!($, info); -websys_rules!($, log); -websys_rules!($, trace); -websys_rules!($, warn); - -#[doc = "Call the browser's `console.clear()` function."] -#[macro_export] -macro_rules! console_clear { - () => { - $crate::__macro::console::clear() - }; -} diff --git a/src/lib.rs b/src/lib.rs index 682b071..1644e59 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,7 +64,7 @@ //! By default, the crate assumes the presence of the `web-sys` crate. //! //! ```toml -//! weblog = "0.1" +//! weblog = "0.3" //! ``` //! //! @@ -72,13 +72,16 @@ //! with `stdweb`, enable the feature in `Cargo.toml`: //! //! ```toml -//! weblog = { version = "0.1", default-features = false, features = ["stdweb"] } +//! weblog = { version = "0.3", default-features = false, features = ["stdweb"] } //! ``` //! mod console; #[cfg(feature = "web_sys")] -pub use self::console::web_sys::*; +pub use weblog_proc_macro::*; + +#[cfg(feature = "web_sys")] +pub use ::web_sys; #[cfg(feature = "std_web")] pub use self::console::std_web::*; diff --git a/weblog-proc-macro/Cargo.toml b/weblog-proc-macro/Cargo.toml new file mode 100644 index 0000000..a0b0d93 --- /dev/null +++ b/weblog-proc-macro/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "weblog-proc-macro" +version = "0.3.0-beta.1" +authors = ["Rob Watson "] +repository = "https://github.com/rfwatson/weblog" +description = "weblog is a crate that defines a set of macros for calling `console.log()` and other members of the browser's console API when targeting Wasm." +edition = "2018" +keywords = ["wasm", "webassembly", "console", "log", "logging"] +license = "MIT OR Apache-2.0" + +[lib] +proc-macro = true + +[dependencies] +syn = "1.0.41" +quote = "1.0.7" +proc-macro2 = "1.0" diff --git a/weblog-proc-macro/src/lib.rs b/weblog-proc-macro/src/lib.rs new file mode 100644 index 0000000..227a8d7 --- /dev/null +++ b/weblog-proc-macro/src/lib.rs @@ -0,0 +1,231 @@ +extern crate proc_macro2; + +mod weblog_impl; + +use proc_macro::TokenStream; +use std::iter; +use weblog_impl::{quote_console_func, ArgMode, ConsoleFunc}; + +/// Call the browser's `console.assert()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/assert) +#[proc_macro] +pub fn console_assert(input: TokenStream) -> TokenStream { + quote_console_func( + ConsoleFunc::variadic("assert_with_condition_and_data", 1), + iter::once(ArgMode::PassThrough).chain(iter::repeat(ArgMode::IntoJsValue)), + input, + ) +} + +/// Call the browser's `console.clear()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/clear) +#[proc_macro] +pub fn console_clear(input: TokenStream) -> TokenStream { + quote_console_func(ConsoleFunc::fixed("clear", 0), iter::empty(), input) +} + +/// Call the browser's `console.count()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/count) +#[proc_macro] +pub fn console_count(input: TokenStream) -> TokenStream { + let name = if input.is_empty() { + "count" + } else { + "count_with_label" + }; + + quote_console_func( + ConsoleFunc::fixed(name, 1), + iter::once(ArgMode::PassThrough), + input, + ) +} + +/// Call the browser's `console.countReset()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/countReset) +#[proc_macro] +pub fn console_count_reset(input: TokenStream) -> TokenStream { + let name = if input.is_empty() { + "count_reset" + } else { + "count_reset_with_label" + }; + + quote_console_func( + ConsoleFunc::fixed(name, 1), + iter::once(ArgMode::PassThrough), + input, + ) +} + +/// Call the browser's `console.debug()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/debug) +#[proc_macro] +pub fn console_debug(input: TokenStream) -> TokenStream { + quote_console_func( + ConsoleFunc::variadic("debug", 0), + iter::repeat(ArgMode::IntoJsValue), + input, + ) +} + +/// Call the browser's `console.dir()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/dir) +#[proc_macro] +pub fn console_dir(input: TokenStream) -> TokenStream { + quote_console_func( + ConsoleFunc::variadic("dir", 0), + iter::repeat(ArgMode::IntoJsValue), + input, + ) +} + +/// Call the browser's `console.dirxml()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/dirxml) +#[proc_macro] +pub fn console_dirxml(input: TokenStream) -> TokenStream { + quote_console_func( + ConsoleFunc::variadic("dirxml", 0), + iter::repeat(ArgMode::IntoJsValue), + input, + ) +} + +/// Call the browser's `console.error()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/error) +#[proc_macro] +pub fn console_error(input: TokenStream) -> TokenStream { + quote_console_func( + ConsoleFunc::variadic("error", 0), + iter::repeat(ArgMode::IntoJsValue), + input, + ) +} + +/// Call the browser's `console.exception()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/exception) +#[proc_macro] +pub fn console_exception(input: TokenStream) -> TokenStream { + quote_console_func( + ConsoleFunc::variadic("exception", 0), + iter::repeat(ArgMode::IntoJsValue), + input, + ) +} + +/// Call the browser's `console.info()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/info) +#[proc_macro] +pub fn console_info(input: TokenStream) -> TokenStream { + quote_console_func( + ConsoleFunc::variadic("info", 0), + iter::repeat(ArgMode::IntoJsValue), + input, + ) +} + +/// Call the browser's `console.log()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/log) +#[proc_macro] +pub fn console_log(input: TokenStream) -> TokenStream { + quote_console_func( + ConsoleFunc::variadic("log", 0), + iter::repeat(ArgMode::IntoJsValue), + input, + ) +} + +/// Call the browser's `console.table()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/table) +#[proc_macro] +pub fn console_table(input: TokenStream) -> TokenStream { + quote_console_func( + ConsoleFunc::variadic("table", 0), + iter::repeat(ArgMode::IntoJsValue), + input, + ) +} + +/// Call the browser's `console.time()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/time) +#[proc_macro] +pub fn console_time(input: TokenStream) -> TokenStream { + let name = if input.is_empty() { + "time" + } else { + "time_with_label" + }; + + quote_console_func( + ConsoleFunc::fixed(name, 1), + iter::once(ArgMode::PassThrough), + input, + ) +} + +/// Call the browser's `console.timeEnd()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/timeEnd) +#[proc_macro] +pub fn console_time_end(input: TokenStream) -> TokenStream { + let name = if input.is_empty() { + "time_end" + } else { + "time_end_with_label" + }; + + quote_console_func( + ConsoleFunc::fixed(name, 1), + iter::once(ArgMode::PassThrough), + input, + ) +} + +/// Call the browser's `console.timeLog()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/timeLog) +#[proc_macro] +pub fn console_time_log(input: TokenStream) -> TokenStream { + quote_console_func( + ConsoleFunc::variadic("time_log_with_label_and_data", 1), + iter::once(ArgMode::PassThrough).chain(iter::repeat(ArgMode::IntoJsValue)), + input, + ) +} + +/// Call the browser's `console.timeStamp()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/timeStamp) +#[proc_macro] +pub fn console_time_stamp(input: TokenStream) -> TokenStream { + let name = if input.is_empty() { + "time_stamp" + } else { + "time_stamp_with_data" + }; + + quote_console_func( + ConsoleFunc::fixed(name, 1), + iter::once(ArgMode::IntoJsValue), + input, + ) +} + +/// Call the browser's `console.trace()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/trace) +#[proc_macro] +pub fn console_trace(input: TokenStream) -> TokenStream { + quote_console_func( + ConsoleFunc::variadic("trace", 0), + iter::repeat(ArgMode::IntoJsValue), + input, + ) +} + +/// Call the browser's `console.warn()` function. +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Console/warn) +#[proc_macro] +pub fn console_warn(input: TokenStream) -> TokenStream { + quote_console_func( + ConsoleFunc::variadic("warn", 0), + iter::repeat(ArgMode::IntoJsValue), + input, + ) +} diff --git a/weblog-proc-macro/src/weblog_impl.rs b/weblog-proc-macro/src/weblog_impl.rs new file mode 100644 index 0000000..1deb450 --- /dev/null +++ b/weblog-proc-macro/src/weblog_impl.rs @@ -0,0 +1,124 @@ +use proc_macro::TokenStream; +use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; +use quote::quote; +use std::cmp; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::{parse_macro_input, Expr, Result, Token}; + +// quote_console_func builds and quotes a call to the browser's console API, based on the +// provided console function and argument types and the input token stream provider by +// the macro caller. +// +// Arguments: +// +// * `func`: an object containing the target function name and parameter configuration. +// * `arg_modes`: an Iterator that will be called once per argument parsed from the input tokens. +// * `input`: the raw input tokens, as provided by the macro caller. +pub fn quote_console_func( + ConsoleFunc { + name, + params_fixed, + is_variadic, + }: ConsoleFunc, + mut arg_modes: impl Iterator, + input: TokenStream, +) -> TokenStream { + let InArgs(in_args) = parse_macro_input!(input as InArgs); + let mut out_args = in_args + .iter() + .map(|arg| quote_arg(arg, arg_modes.next().unwrap_or_default())); + + let num_provided = out_args.len(); + let num_fixed = cmp::min(num_provided, params_fixed); + let num_variadic = cmp::max(num_provided - num_fixed, 0); + + let ident = { + let func_name = if is_variadic && num_variadic <= 7 { + format!("{}_{}", name, num_variadic) + } else { + name + }; + Ident::new(&func_name, Span::call_site()) + }; + + let mut args: Punctuated<_, Token![,]> = Punctuated::new(); + { + for _ in 0..num_fixed { + args.push(out_args.next().unwrap()); + } + if num_variadic > 7 { + let variadic_args = out_args.collect::>(); + let ary = quote! { + &::std::iter::FromIterator::from_iter(::std::iter::IntoIterator::into_iter(::std::vec![#variadic_args])) + }; + args.push(ary); + } else { + for _ in 0..num_variadic { + args.push(out_args.next().unwrap()); + } + } + } + + (quote! { ::weblog::web_sys::console::#ident(#args) }).into() +} + +fn quote_arg(arg: &Expr, arg_mode: ArgMode) -> TokenStream2 { + match arg_mode { + ArgMode::PassThrough => { + quote! { #arg } + } + ArgMode::IntoJsValue => { + quote! { &::std::convert::Into::<::wasm_bindgen::JsValue>::into(#arg) } + } + } +} + +// ConsoleFunc represents the console API function being targeted. +#[derive(Debug)] +pub struct ConsoleFunc { + name: String, + params_fixed: usize, + is_variadic: bool, +} + +impl ConsoleFunc { + pub fn fixed(name: &str, params_fixed: usize) -> Self { + Self { + name: name.into(), + params_fixed, + is_variadic: false, + } + } + + pub fn variadic(name: &str, params_fixed: usize) -> Self { + Self { + name: name.into(), + params_fixed, + is_variadic: true, + } + } +} + +// ArgMode represents an argument being passed to a targeted ConsoleFunc. +#[derive(Debug, Copy, Clone)] +pub enum ArgMode { + PassThrough, + IntoJsValue, +} + +impl Default for ArgMode { + fn default() -> Self { + Self::PassThrough + } +} + +// InArgs is a struct required during parsing of input arguments. +struct InArgs(Punctuated); + +impl Parse for InArgs { + fn parse(input: ParseStream) -> Result { + let in_args: Punctuated = Punctuated::parse_terminated(&input)?; + Ok(Self(in_args)) + } +}