From 3886551b528ff235f52e16745dad660ec35b5ef5 Mon Sep 17 00:00:00 2001 From: William Henderson Date: Wed, 24 Jan 2024 21:34:40 +0000 Subject: [PATCH] feat: introduce JavaScript plugin API using V8 (#11) --- Cargo.lock | 186 +++++++++++++-- README.md | 44 +++- stuart-core/Cargo.toml | 2 +- stuart-core/src/functions/mod.rs | 12 +- stuart-core/src/parse/function.rs | 1 + stuart-core/src/parse/mod.rs | 2 +- stuart/Cargo.toml | 9 +- stuart/example/stuart.toml | 3 - stuart/src/main.rs | 13 +- stuart/src/new.rs | 4 +- stuart/src/plugins/js/context.rs | 87 +++++++ stuart/src/plugins/js/json.rs | 104 ++++++++ stuart/src/plugins/js/mod.rs | 223 ++++++++++++++++++ stuart/src/plugins/mod.rs | 50 +++- stuart/src/test.rs | 80 +++++++ .../basic}/content/index.html | 0 .../basic}/content/root.html | 0 .../basic}/static/lightning.png | Bin .../{example => tests/basic}/static/style.css | 0 stuart/tests/basic/stuart.toml | 3 + stuart/tests/js/content/data.json | 4 + stuart/tests/js/content/index.html | 8 + stuart/tests/js/content/root.html | 1 + stuart/tests/js/plugin.mjs | 72 ++++++ stuart/tests/js/stuart.toml | 6 + 25 files changed, 868 insertions(+), 46 deletions(-) delete mode 100644 stuart/example/stuart.toml create mode 100644 stuart/src/plugins/js/context.rs create mode 100644 stuart/src/plugins/js/json.rs create mode 100644 stuart/src/plugins/js/mod.rs create mode 100644 stuart/src/test.rs rename stuart/{example => tests/basic}/content/index.html (100%) rename stuart/{example => tests/basic}/content/root.html (100%) rename stuart/{example => tests/basic}/static/lightning.png (100%) rename stuart/{example => tests/basic}/static/style.css (100%) create mode 100644 stuart/tests/basic/stuart.toml create mode 100644 stuart/tests/js/content/data.json create mode 100644 stuart/tests/js/content/index.html create mode 100644 stuart/tests/js/content/root.html create mode 100644 stuart/tests/js/plugin.mjs create mode 100644 stuart/tests/js/stuart.toml diff --git a/Cargo.lock b/Cargo.lock index 7360979..20e74a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,6 +61,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + [[package]] name = "bumpalo" version = "3.11.0" @@ -110,7 +116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15f2ea93df33549dbe2e8eecd1ca55269d63ae0b3ba1f55db030817d1c2867f" dependencies = [ "atty", - "bitflags", + "bitflags 1.3.2", "clap_lex", "indexmap", "strsim", @@ -155,6 +161,22 @@ dependencies = [ "regex", ] +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "filetime" version = "0.2.17" @@ -173,7 +195,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6" dependencies = [ - "bitflags", + "bitflags 1.3.2", "fsevent-sys", ] @@ -186,13 +208,23 @@ dependencies = [ "libc", ] +[[package]] +name = "fslock" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57eafdd0c16f57161105ae1b98a1238f97645f2f588438b2949c99a2af9616bf" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "fuchsia-zircon" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" dependencies = [ - "bitflags", + "bitflags 1.3.2", "fuchsia-zircon-sys", ] @@ -217,6 +249,15 @@ dependencies = [ "libc", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "humphrey" version = "0.7.0" @@ -291,7 +332,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" dependencies = [ - "bitflags", + "bitflags 1.3.2", "inotify-sys", "libc", ] @@ -347,9 +388,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.132" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libloading" @@ -361,6 +402,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + [[package]] name = "log" version = "0.4.17" @@ -436,7 +483,7 @@ version = "4.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257" dependencies = [ - "bitflags", + "bitflags 1.3.2", "filetime", "fsevent", "fsevent-sys", @@ -469,9 +516,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl-probe" @@ -500,7 +547,7 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63" dependencies = [ - "bitflags", + "bitflags 1.3.2", "memchr", "unicase", ] @@ -520,7 +567,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -555,6 +602,19 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rustix" +version = "0.38.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustls" version = "0.21.7" @@ -640,7 +700,7 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c4437699b6d34972de58652c68b98cb5b53a4199ab126db8e20ec8ded29a721" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -697,10 +757,11 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "stuart" -version = "0.2.7" +version = "0.3.0" dependencies = [ "clap", "humphrey", + "humphrey_json", "humphrey_ws", "include_dir", "libloading", @@ -711,11 +772,12 @@ dependencies = [ "stuart_core", "termcolor", "toml", + "v8", ] [[package]] name = "stuart_core" -version = "0.2.7" +version = "0.3.0" dependencies = [ "chrono", "dateparser", @@ -791,6 +853,18 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "v8" +version = "0.82.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f53dfb242f4c0c39ed3fc7064378a342e57b5c9bd774636ad34ffe405b808121" +dependencies = [ + "bitflags 1.3.2", + "fslock", + "once_cell", + "which", +] + [[package]] name = "version_check" version = "0.9.4" @@ -878,6 +952,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "winapi" version = "0.2.8" @@ -940,7 +1026,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -949,21 +1044,42 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", + "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm", + "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.36.1" @@ -976,6 +1092,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.36.1" @@ -988,6 +1110,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.36.1" @@ -1000,6 +1128,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.36.1" @@ -1012,12 +1146,24 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.36.1" @@ -1030,6 +1176,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "ws2_32-sys" version = "0.2.1" diff --git a/README.md b/README.md index 2060431..0b9b130 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@

-Stuart is a very fast and flexible static site generator, with build times as low as 0.1ms per page. It is written in Rust, and is designed be easier to use than other SSGs, while still beating them in the benchmarks. Stuart's simple yet powerful templating system allows you to define complex logic for your site, sourcing data from Markdown and JSON files as well as the template files, before rendering it all to static HTML. For even more complex projects, you can augment Stuart with custom build scripts in any language that integrate with the core build system, as well as custom plugins. +Stuart is a very fast and flexible static site generator, with build times as low as 0.1ms per page. It is written in Rust, and is designed be easier to use than other SSGs, while still beating them in the benchmarks. Stuart's simple yet powerful templating system allows you to define complex logic for your site, sourcing data from Markdown and JSON files as well as the template files, before rendering it all to static HTML. For even more complex projects, you can augment Stuart with custom build scripts in any language that integrate with the core build system, as well as custom plugins in Rust or JavaScript. **Note:** Stuart is an extremely new project, so functionality and documentation are still being added. For the time being, documentation is limited to this README. @@ -44,7 +44,7 @@ Stuart is a very fast and flexible static site generator, with build times as lo ### Installation -Stuart is available as a pre-built binary for Windows and Linux. You can download the latest release from the [releases page](https://github.com/w-henderson/Stuart/releases). Alternatively, you can build the code from scratch using Rust's package manager, Cargo. To do this, clone the repository and run `cargo build --release`. +Stuart is available as a pre-built binary for Windows and Linux. You can download the latest release from the [releases page](https://github.com/w-henderson/Stuart/releases). Alternatively, you can build the code from scratch using Rust's package manager, Cargo. To do this, clone the repository and run `cargo build --release`. Support for JavaScript plugins is disabled by default, so to enable it, enable the `js` feature. Stuart requires Git to be installed for many of its features to work. @@ -74,11 +74,17 @@ You can declare plugin dependencies in the `[dependencies]` section using a simi [dependencies] my_plugin = "/path/to/plugin.so" my_other_plugin = "/path/to/cargo/project/" -my_remote_plugin = "https://github.com/username/plugin" +my_git_plugin = "https://github.com/username/plugin" +my_remote_plugin = "https://example.com/plugin.so" ``` Stuart will automatically detect whether the plugin needs to be cloned from a Git repository and whether it needs to be compiled. If the plugin does require compilation, Stuart requires the Rust toolchain to be installed. +You can separate plugin sources with a semicolon to specify fallbacks, for example: +```toml +my_plugin = "/lib/plugin.so;https://example.com/plugin.so" +``` + ## Project Structure A Stuart project contains a number of folders, each of which has a specific purpose. Additionally, some file names have special meanings too. All content should go in the `content` directory, as this is the only one that will be processed by the build system. @@ -237,9 +243,11 @@ Stuart supports dynamically-loaded plugins, which are Rust libraries that provid {{ my_plugin::my_function() }} ``` -### Plugin API +If the function name doesn't clash with another function, the plugin name can be omitted. If it does, built-in functions take priority over plugin functions, but the order of plugin functions is undefined. So don't do that. + +### Native Plugin API -Plugins are defined using the `define_plugin!` macro in the core crate. They can add functions to Stuart, which are implemented in exactly the same way as the built-in functions, and can also add parsers for new file types. Please refer to the [built-in functions](https://github.com/w-henderson/Stuart/tree/master/stuart-core/src/functions/parsers) for function implementation examples, and to the [image optimization plugin source code](https://github.com/w-henderson/Stuart/tree/master/plugins/imgopt) to see how parsers for new file types can be used. An example of calling the macro is as follows: +Native plugins are defined using the `define_plugin!` macro in the core crate. They can add functions to Stuart, which are implemented in exactly the same way as the built-in functions, and can also add parsers for new file types. Please refer to the [built-in functions](https://github.com/w-henderson/Stuart/tree/master/stuart-core/src/functions/parsers) for function implementation examples, and to the [image optimization plugin source code](https://github.com/w-henderson/Stuart/tree/master/plugins/imgopt) to see how parsers for new file types can be used. An example of calling the macro is as follows: ```rs declare_plugin! { @@ -268,3 +276,29 @@ The project must also have `stuart_core` as a dependency in order to use the `de [dependencies] stuart_core = { version = "*", default-features = false } ``` + +### JavaScript Plugin API + +When compiled with the `js` feature, Stuart can also load plugins written in JavaScript using V8. These can only add functions to Stuart. A plugin is simply a JavaScript module which exports a default object containing the plugin metadata, analogous to the `declare_plugin!` macro in Rust: + +```js +export default { + name: "my_plugin", + version: "1.0.0", + functions: [ + { + name: "add", + fn: (a, b) => a + b + } + ] +} +``` + +Stuart variables will automatically be converted to JavaScript objects when passed as arguments, but changes to them will not propagate back to the Rust end automatically. For that, you can use the `STUART` global object to access the plugin API. + +```js +const self = STUART.get("self"); +STUART.set("my_var", `Title: ${self.title}`); +``` + +For an example of a more complex JavaScript plugin, see [stuart-math](https://github.com/w-henderson/stuart-math), which uses MathJax to render LaTeX. \ No newline at end of file diff --git a/stuart-core/Cargo.toml b/stuart-core/Cargo.toml index 0cf85f4..929832f 100644 --- a/stuart-core/Cargo.toml +++ b/stuart-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "stuart_core" -version = "0.2.7" +version = "0.3.0" edition = "2021" license = "MIT" homepage = "https://github.com/w-henderson/Stuart" diff --git a/stuart-core/src/functions/mod.rs b/stuart-core/src/functions/mod.rs index dafe3f0..f203228 100644 --- a/stuart-core/src/functions/mod.rs +++ b/stuart-core/src/functions/mod.rs @@ -53,16 +53,18 @@ use std::fmt::Debug; /// the inner workings of which are hidden from Stuart through the [`Function`] trait. The parser should also /// define a name, which is used to identify the function when parsing a file. The name of the function parser /// **must** be the same as that of the returned function. -pub trait FunctionParser: Send + Sync { +pub trait FunctionParser { /// Returns the name of the function which the parser can parse. /// /// This **must** return the same value as the `name` method of the returned function. - fn name(&self) -> &'static str; + fn name(&self) -> &str; /// Attempts to parse the raw function into an executable function object. fn parse(&self, raw: RawFunction) -> Result, ParseError>; /// Returns `true` if the raw function can be parsed by this function parser. + /// + /// Not used for plugin functions! fn can_parse(&self, raw: &RawFunction) -> bool { raw.name == self.name() } @@ -72,9 +74,9 @@ pub trait FunctionParser: Send + Sync { /// /// When the function is executed, it is given a [`Scope`] object, which contains information about the current state /// of the program, including variables, stack frames and more. -pub trait Function: Debug + Send + Sync { +pub trait Function: Debug { /// Returns the name of the function. - fn name(&self) -> &'static str; + fn name(&self) -> &str; /// Executes the function in the given scope. fn execute(&self, scope: &mut Scope) -> Result<(), TracebackError>; @@ -151,7 +153,7 @@ macro_rules! define_functions { const FUNCTION_COUNT: usize = count!($($name)*); ::lazy_static::lazy_static! { - static ref FUNCTION_PARSERS: [Box; FUNCTION_COUNT] = [ + static ref FUNCTION_PARSERS: [Box; FUNCTION_COUNT] = [ $(Box::new($name)),* ]; } diff --git a/stuart-core/src/parse/function.rs b/stuart-core/src/parse/function.rs index a8e87fe..eebbff2 100644 --- a/stuart-core/src/parse/function.rs +++ b/stuart-core/src/parse/function.rs @@ -18,6 +18,7 @@ pub struct RawFunction { } /// Represents a raw argument. +#[derive(Debug, Clone, PartialEq, Eq)] pub enum RawArgument { /// A variable name. Variable(String), diff --git a/stuart-core/src/parse/mod.rs b/stuart-core/src/parse/mod.rs index 22cffb4..cfc358a 100644 --- a/stuart-core/src/parse/mod.rs +++ b/stuart-core/src/parse/mod.rs @@ -257,7 +257,7 @@ fn parse_function( for plugin in plugins.plugins() { for function in &plugin.functions { if function_name == function.name() - || function_name == format!("{}::{}", &plugin.name, function.name()) + || function_name == format!("{}::{}", plugin.name, function.name()) { return Ok(Token::Function(Rc::new( function diff --git a/stuart/Cargo.toml b/stuart/Cargo.toml index 6876641..e5ea28c 100644 --- a/stuart/Cargo.toml +++ b/stuart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "stuart" -version = "0.2.7" +version = "0.3.0" edition = "2021" license = "MIT" homepage = "https://github.com/w-henderson/Stuart" @@ -15,7 +15,7 @@ name = "stuart" path = "src/main.rs" [dependencies] -stuart_core = { version = "^0.2.6", path = "../stuart-core" } +stuart_core = { version = "^0.3.0", path = "../stuart-core" } clap = "^3.2" toml = "^0.5" @@ -25,6 +25,11 @@ termcolor = "^1.1.0" once_cell = "^1.13.1" include_dir = "^0.7.2" humphrey = { version = "^0.7.0", features = ["tls"] } +humphrey_json = { version = "^0.2.0", default-features = false } humphrey_ws = "^0.5.1" notify = "^4.0.17" libloading = "^0.7.3" +v8 = { version = "^0.82.0", optional = true } + +[features] +js = ["v8"] diff --git a/stuart/example/stuart.toml b/stuart/example/stuart.toml deleted file mode 100644 index 096f757..0000000 --- a/stuart/example/stuart.toml +++ /dev/null @@ -1,3 +0,0 @@ -[site] -name = "example" -author = "William Henderson " \ No newline at end of file diff --git a/stuart/src/main.rs b/stuart/src/main.rs index 4c3f555..1f277e1 100644 --- a/stuart/src/main.rs +++ b/stuart/src/main.rs @@ -14,6 +14,9 @@ mod plugins; mod scripts; mod serve; +#[cfg(test)] +mod test; + use crate::build::StuartContext; use crate::error::StuartError; use crate::logger::{LogLevel, Logger, Progress, LOGGER}; @@ -24,8 +27,9 @@ use std::fs::{remove_dir_all, remove_file}; use std::path::PathBuf; use std::sync::atomic::Ordering; -fn main() { - let matches = App::new("Stuart") +/// Returns the CLI application. +fn app() -> App<'static> { + App::new("Stuart") .version(env!("CARGO_PKG_VERSION")) .author("William Henderson ") .about("A Blazingly-Fast Static Site Generator") @@ -102,7 +106,10 @@ fn main() { Command::new("clean").about("Removes the output directory and generated metadata"), ) .subcommand_required(true) - .get_matches(); +} + +fn main() { + let matches = app().get_matches(); let log_level = if matches.is_present("quiet") { LogLevel::Quiet diff --git a/stuart/src/new.rs b/stuart/src/new.rs index 51a3829..0c56aac 100644 --- a/stuart/src/new.rs +++ b/stuart/src/new.rs @@ -13,12 +13,12 @@ use std::io::Write; use std::path::{Path, PathBuf}; /// The directory containing the default site template, built into the binary when compiled. -static DEFAULT_PROJECT: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/example"); +static DEFAULT_PROJECT: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/tests/basic"); /// Creates a new site with the given arguments. pub fn new(args: &ArgMatches) -> Result<(), Box> { let name = args.value_of("name").unwrap(); - let path = PathBuf::try_from(name).map_err(|_| FsError::Write)?; + let path = PathBuf::from(name); let no_git = args.is_present("no-git"); let mut manifest: Vec = format!("[site]\nname = \"{}\"", name).as_bytes().to_vec(); diff --git a/stuart/src/plugins/js/context.rs b/stuart/src/plugins/js/context.rs new file mode 100644 index 0000000..c1e77e6 --- /dev/null +++ b/stuart/src/plugins/js/context.rs @@ -0,0 +1,87 @@ +//! Enables access to Stuart's execution context from JavaScript running within V8. + +use stuart_core::process::Scope; + +/// Makes the Stuart scope accessible to `set_variable` and `get_variable` when they're called from JavaScript code. +pub fn set_stuart_context(scope: &mut v8::HandleScope, context: &mut Scope) { + let stuart_context = v8::Object::new(scope); + + let k_context = v8::String::new(scope, "STUART").unwrap(); + let k_set_variable = v8::String::new(scope, "set").unwrap(); + let k_get_variable = v8::String::new(scope, "get").unwrap(); + let k_external = v8::String::new(scope, "_ptr").unwrap(); + + let set_variable = v8::FunctionTemplate::new(scope, set_variable) + .get_function(scope) + .unwrap(); + let get_variable = v8::FunctionTemplate::new(scope, get_variable) + .get_function(scope) + .unwrap(); + let external = v8::External::new(scope, context as *mut _ as *mut std::ffi::c_void); + + stuart_context.set(scope, k_set_variable.into(), set_variable.into()); + stuart_context.set(scope, k_get_variable.into(), get_variable.into()); + stuart_context.set(scope, k_external.into(), external.into()); + + scope + .get_current_context() + .global(scope) + .set(scope, k_context.into(), stuart_context.into()); +} + +/// Gets the Stuart context from V8. +/// +/// # Safety +/// This function is only safe if `set_stuart_context` has previously been called with the same `scope`. +unsafe fn get_stuart_context<'s>( + scope: &mut v8::HandleScope, + obj: v8::Local<'_, v8::Object>, +) -> &'s mut Scope<'s> { + let k_external = v8::String::new(scope, "_ptr").unwrap(); + + (v8::Local::::try_from(obj.get(scope, k_external.into()).unwrap()) + .unwrap() + .value() as *mut Scope) + .as_mut() + .unwrap() +} + +/// Sets a variable in the current Stuart scope. +pub fn set_variable<'s>( + scope: &mut v8::HandleScope<'s>, + args: v8::FunctionCallbackArguments<'s>, + _ret: v8::ReturnValue, +) { + let stuart_scope = unsafe { get_stuart_context(scope, args.this()) }; + let key = args.get(0).to_rust_string_lossy(scope); + let value = args.get(1); + let json_value = super::json::js_to_json(value, scope); + + if json_value.is_none() { + println!( + "warning(js): attempted to set variable `{}` to `undefined`", + key + ); + return; + } + + stuart_scope + .stack + .last_mut() + .unwrap() + .add_variable(key, json_value.unwrap()); +} + +/// Gets a variable from the current Stuart scope. +pub fn get_variable<'s>( + scope: &mut v8::HandleScope<'s>, + args: v8::FunctionCallbackArguments<'s>, + mut ret: v8::ReturnValue, +) { + let stuart_scope = unsafe { get_stuart_context(scope, args.this()) }; + let key = args.get(0).to_rust_string_lossy(scope); + let value = stuart_scope.get_variable(&key); + let v8_value = super::json::json_to_js(value, scope); + + ret.set(v8_value); +} diff --git a/stuart/src/plugins/js/json.rs b/stuart/src/plugins/js/json.rs new file mode 100644 index 0000000..163cdac --- /dev/null +++ b/stuart/src/plugins/js/json.rs @@ -0,0 +1,104 @@ +//! Provides functionality for converting between JSON and JavaScript objects within V8. + +use humphrey_json::Value; + +/// Converts a JSON value to a JavaScript object. +/// +/// Returns `undefined` if `value` is `None`. +pub fn json_to_js<'a>( + value: Option, + scope: &mut v8::HandleScope<'a>, +) -> v8::Local<'a, v8::Value> { + match value { + Some(v) => match v { + Value::Null => v8::null(scope).into(), + Value::Bool(boolean) => v8::Boolean::new(scope, boolean).into(), + Value::Number(number) => v8::Number::new(scope, number).into(), + Value::String(string) => v8::String::new(scope, &string).unwrap().into(), + Value::Array(array) => { + let v8_array = v8::Array::new(scope, array.len() as i32); + for (i, value) in array.into_iter().enumerate() { + let v8_value = json_to_js(Some(value), scope); + v8_array.set_index(scope, i as u32, v8_value); + } + v8_array.into() + } + Value::Object(object) => { + let v8_object = v8::Object::new(scope); + for (key, value) in object.into_iter() { + let v8_key = v8::String::new(scope, &key).unwrap().into(); + let v8_value = json_to_js(Some(value), scope); + v8_object.set(scope, v8_key, v8_value); + } + v8_object.into() + } + }, + None => v8::undefined(scope).into(), + } +} + +/// Converts a JavaScript object to a JSON value. +/// +/// Returns `None` if `value` is `undefined`. +pub fn js_to_json<'a>( + value: v8::Local<'a, v8::Value>, + scope: &mut v8::HandleScope<'a>, +) -> Option { + if value.is_undefined() { + return None; + } + + if value.is_null() { + return Some(Value::Null); + } + + if value.is_boolean() { + return Some(Value::Bool(value.boolean_value(scope))); + } + + if value.is_number() { + return Some(Value::Number(value.number_value(scope).unwrap())); + } + + if value.is_string() { + return Some(Value::String(value.to_rust_string_lossy(scope).to_string())); + } + + if value.is_array() { + let v8_array = value.to_object(scope).unwrap(); + let k_length = v8::String::new(scope, "length").unwrap(); + let length = v8_array + .get(scope, k_length.into()) + .unwrap() + .uint32_value(scope) + .unwrap(); + let mut array = Vec::with_capacity(length as usize); + for i in 0..length { + let v8_value = v8_array.get_index(scope, i).unwrap(); + let value = js_to_json(v8_value, scope); + array.push(value.unwrap()); + } + return Some(Value::Array(array)); + } + + if value.is_object() { + let v8_object = value.to_object(scope).unwrap(); + let keys = v8_object + .get_own_property_names(scope, v8::GetPropertyNamesArgs::default()) + .unwrap(); + let length = keys.length(); + let mut object = Vec::with_capacity(length as usize); + + for i in 0..length { + let v8_key = keys.get_index(scope, i).unwrap(); + let key = v8_key.to_rust_string_lossy(scope).to_string(); + let v8_value = v8_object.get(scope, v8_key).unwrap(); + let value = js_to_json(v8_value, scope); + object.push((key, value.unwrap())); + } + + return Some(Value::Object(object)); + } + + None +} diff --git a/stuart/src/plugins/js/mod.rs b/stuart/src/plugins/js/mod.rs new file mode 100644 index 0000000..213b7d9 --- /dev/null +++ b/stuart/src/plugins/js/mod.rs @@ -0,0 +1,223 @@ +//! Implements V8-based JavaScript plugins. + +mod context; +mod json; + +use stuart_core::functions::{Function, FunctionParser}; +use stuart_core::parse::{ParseError, RawArgument, RawFunction}; +use stuart_core::plugins::Plugin; +use stuart_core::process::{ProcessError, Scope}; +use stuart_core::TracebackError; + +use std::path::Path; +use std::rc::Rc; +use std::sync::{Mutex, Once}; + +/// Ensures that V8 is initialised exactly once. +static INITIALISED: Once = Once::new(); + +/// A parser for JavaScript functions. +/// +/// Since JavaScript functions can take a variable number of arguments of different types, this function +/// just passes the function's arguments to `JSFunction` as-is. +pub struct JSFunctionParser { + /// The name of the function. + name: String, + /// A reference to the V8 isolate. This is within a `Mutex` to enable shared ownership, despite the fact that + /// V8 isolates are not `Send` or `Sync`, so we can't share it between threads. + isolate: Rc>, + /// The V8 context for this plugin. + context: v8::Global, +} + +/// A Stuart function that executes JavaScript code. +#[derive(Debug)] +pub struct JSFunction { + /// The name of the function. + name: String, + /// A reference to the V8 isolate. This is within a `Mutex` to enable shared ownership, despite the fact that + /// V8 isolates are not `Send` or `Sync`, so we can't share it between threads. + isolate: Rc>, + /// The V8 context for this plugin. + context: v8::Global, + /// The function's arguments. + args: Vec, +} + +/// Attempts to load a JavaScript plugin from the given path, spinning up a new V8 isolate. +pub fn load_js_plugin(path: impl AsRef) -> Result { + INITIALISED.call_once(|| { + v8::V8::initialize_platform(v8::new_default_platform(0, false).make_shared()); + v8::V8::initialize(); + }); + + let mut isolate = v8::Isolate::new(Default::default()); + let global_context; + + let (name, version, functions) = { + let handle_scope = &mut v8::HandleScope::new(&mut isolate); + let context = v8::Context::new(handle_scope); + global_context = v8::Global::new(handle_scope, context); + let scope = &mut v8::ContextScope::new(handle_scope, context); + + let name: v8::Local<'_, v8::Value> = + v8::String::new(scope, &path.as_ref().to_string_lossy()) + .unwrap() + .into(); + let origin = v8::ScriptOrigin::new(scope, name, 0, 0, false, 0, name, false, false, true); + let source_string = std::fs::read_to_string(path).map_err(|e| e.to_string())?; + let source = v8::String::new(scope, &source_string).unwrap(); + let compile_source = v8::script_compiler::Source::new(source, Some(&origin)); + let module = v8::script_compiler::compile_module(scope, compile_source) + .ok_or("failed to compile module")?; + + module + .instantiate_module(scope, |_, _, _, m| Some(m)) + .ok_or("failed to instantiate module")?; + module.evaluate(scope).ok_or("failed to evaluate module")?; + + let key = v8::String::new(scope, "default").unwrap(); + let default = module + .get_module_namespace() + .to_object(scope) + .unwrap() + .get(scope, key.into()) + .ok_or("failed to get default export")? + .to_object(scope) + .ok_or("failed to get default export")?; + + let key = v8::String::new(scope, "name").unwrap(); + let plugin_name = default + .get(scope, key.into()) + .ok_or("missing plugin name")? + .to_rust_string_lossy(scope); + + let key = v8::String::new(scope, "version").unwrap(); + let plugin_version = default + .get(scope, key.into()) + .ok_or("missing plugin version")? + .to_rust_string_lossy(scope); + + let key = v8::String::new(scope, "functions").unwrap(); + let functions = default + .get(scope, key.into()) + .ok_or("missing plugin functions")? + .to_object(scope) + .ok_or("missing plugin functions")?; + let key = v8::String::new(scope, "length").unwrap(); + let length = functions + .get(scope, key.into()) + .ok_or("missing plugin functions")? + .uint32_value(scope) + .unwrap(); + + let mut functions_vec = Vec::with_capacity(length as usize); + + for i in 0..length { + let function_object = functions + .get_index(scope, i) + .ok_or_else(|| format!("missing function at index {}", i))? + .to_object(scope) + .ok_or_else(|| format!("missing function at index {}", i))?; + + let key = v8::String::new(scope, "name").unwrap(); + let function_name = function_object + .get(scope, key.into()) + .ok_or_else(|| format!("invalid function at index {}", i))? + .to_rust_string_lossy(scope); + + let key = v8::String::new(scope, "fn").unwrap(); + let function_fn = function_object + .get(scope, key.into()) + .ok_or_else(|| format!("invalid function at index {}", i))?; + + let key = v8::String::new(scope, &format!("_stuart_{}", function_name)).unwrap(); + context.global(scope).set(scope, key.into(), function_fn); + + functions_vec.push(function_name); + } + + (plugin_name, plugin_version, functions_vec) + }; + + let isolate = Rc::new(Mutex::new(isolate)); + let mut function_parsers = Vec::with_capacity(functions.len()); + for function in &functions { + function_parsers.push(Box::new(JSFunctionParser { + name: function.clone(), + isolate: isolate.clone(), + context: global_context.clone(), + }) as Box); + } + + Ok(Plugin { + name, + version, + functions: function_parsers, + parsers: Vec::new(), + }) +} + +impl FunctionParser for JSFunctionParser { + fn name(&self) -> &str { + &self.name + } + + fn parse(&self, raw: RawFunction) -> Result, ParseError> { + Ok(Box::new(JSFunction { + name: self.name.clone(), + isolate: self.isolate.clone(), + context: self.context.clone(), + args: raw.positional_args, + })) + } +} + +impl Function for JSFunction { + fn name(&self) -> &str { + todo!() + } + + fn execute(&self, stuart_scope: &mut Scope) -> Result<(), TracebackError> { + let self_token = stuart_scope.tokens.current().unwrap().clone(); + + let mut isolate = self.isolate.lock().unwrap(); + let handle_scope = &mut v8::HandleScope::new(&mut *isolate); + let context = v8::Local::new(handle_scope, &self.context); + let scope = &mut v8::ContextScope::new(handle_scope, context); + + let evaluated_args = self + .args + .iter() + .map(|a| match a { + RawArgument::Variable(name) => match stuart_scope.get_variable(name) { + Some(v) => Ok(json::json_to_js(Some(v), scope)), + None => { + Err(self_token.traceback(ProcessError::UndefinedVariable(name.to_string()))) + } + }, + RawArgument::String(s) => Ok(v8::String::new(scope, s).unwrap().into()), + RawArgument::Integer(i) => Ok(v8::Integer::new(scope, *i).into()), + _ => Err(self_token.traceback(ProcessError::StackError)), + }) + .collect::, _>>()?; + + let key = v8::String::new(scope, &format!("_stuart_{}", self.name)).unwrap(); + let function_obj = context.global(scope).get(scope, key.into()).unwrap(); + let function = v8::Local::::try_from(function_obj).unwrap(); + + // Make the `stuart_scope`` variable accessible from JavaScript calls back into Rust. + // If I've done this right (which is a big if), this should be safe because the V8 scope is dropped/GC'd as soon as `execute` returns. + context::set_stuart_context(scope, stuart_scope); + + if let Some(result) = function.call(scope, function_obj, &evaluated_args) { + if !result.is_undefined() { + stuart_scope + .output(result.to_rust_string_lossy(scope)) + .unwrap(); + } + } + + Ok(()) + } +} diff --git a/stuart/src/plugins/mod.rs b/stuart/src/plugins/mod.rs index 688d21f..c1e7b11 100644 --- a/stuart/src/plugins/mod.rs +++ b/stuart/src/plugins/mod.rs @@ -2,6 +2,9 @@ mod source; +#[cfg(feature = "js")] +mod js; + use crate::config::git; use crate::error::StuartError; @@ -12,7 +15,7 @@ use libloading::Library; use std::collections::HashMap; use std::fs::create_dir_all; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::time::Instant; /// Represents an external function that initializes a plugin. @@ -76,6 +79,16 @@ pub fn load( continue; } + #[cfg(not(feature = "js"))] + if source.ends_with(".js") || source.ends_with(".mjs") { + log!( + "Skipping", + "plugin file `{}` (JavaScript support is not enabled)", + source + ); + continue; + } + if let Err(err) = load_from_source(&mut manager, name, source, root) { if e.is_none() { err.print(); @@ -111,12 +124,12 @@ fn load_from_source( src: &str, root: &Path, ) -> Result<(), Box> { - let source = PathBuf::from(src); + let source = root.join(src); if source.exists() && source.is_file() { log!("Loading", "plugin `{}` from `{}`", name, src); - unsafe { manager.load(source)? }; + manager.load(source)?; Ok(()) } else if source.join("Cargo.toml").exists() { @@ -124,7 +137,7 @@ fn load_from_source( let path = source::build_cargo_project(&source)?; - unsafe { manager.load(path)? }; + unsafe { manager.load_binary(path)? }; Ok(()) } else if git::exists(src) { @@ -164,7 +177,7 @@ fn load_from_source( let path = source::build_cargo_project(project)?; - unsafe { manager.load(path)? }; + unsafe { manager.load_binary(path)? }; Ok(()) } else if let Some(plugin) = source::download_plugin(src) { @@ -179,7 +192,7 @@ fn load_from_source( std::fs::write(&plugin_path, plugin).map_err(|_| Error::Fs(FsError::Write))?; - unsafe { manager.load(plugin_path)? }; + manager.load(plugin_path)?; Ok(()) } else { @@ -194,11 +207,25 @@ impl DynamicPluginManager { } /// Attempts to load a plugin from the given path. + pub fn load(&mut self, path: impl AsRef) -> Result<(), String> { + let path = path.as_ref(); + let ext = path.extension().unwrap_or_default().to_string_lossy(); + + if ext == "js" || ext == "mjs" { + #[cfg(feature = "js")] + self.load_js(path)?; + Ok(()) + } else { + unsafe { self.load_binary(path) } + } + } + + /// Attempts to load a binary plugin from the given path. /// /// # Safety /// /// Calls foreign code. The safety of this function is dependent on the safety of the foreign code. - pub unsafe fn load(&mut self, path: impl AsRef) -> Result<(), String> { + pub unsafe fn load_binary(&mut self, path: impl AsRef) -> Result<(), String> { let library = Library::new(path.as_ref()).map_err(|e| e.to_string())?; self.libraries.push(library); @@ -213,6 +240,15 @@ impl DynamicPluginManager { Ok(()) } + + /// Attempts to load a JavaScript plugin from the given path. + #[cfg(feature = "js")] + pub fn load_js(&mut self, path: impl AsRef) -> Result<(), String> { + let plugin = js::load_js_plugin(path)?; + self.plugins.push(plugin); + + Ok(()) + } } impl Manager for DynamicPluginManager { diff --git a/stuart/src/test.rs b/stuart/src/test.rs new file mode 100644 index 0000000..d34e26c --- /dev/null +++ b/stuart/src/test.rs @@ -0,0 +1,80 @@ +#![allow(clippy::redundant_closure_call)] + +use crate::{app, build}; + +use std::fs::{remove_dir_all, remove_file}; +use std::path::Path; +use std::process::exit; + +macro_rules! test { + ($name:ident, $manifest_path:expr, $post_build_checks:expr) => { + #[test] + fn $name() { + let result = full_build(concat!( + env!("CARGO_MANIFEST_DIR"), + $manifest_path, + "/stuart.toml" + )); + + if !result { + cleanup(concat!( + env!("CARGO_MANIFEST_DIR"), + $manifest_path, + "/stuart.toml" + )); + + exit(1); + } + + let index = std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + $manifest_path, + "/dist/index.html" + )) + .unwrap(); + + cleanup(concat!( + env!("CARGO_MANIFEST_DIR"), + $manifest_path, + "/stuart.toml" + )); + + $post_build_checks(index.trim()); + } + }; +} + +test!(basic, "/tests/basic", |_| ()); + +#[cfg(feature = "js")] +test!(js, "/tests/js", |index: &str| { + let mut lines = index.lines().map(|s| s.trim()); + assert_eq!(lines.next().unwrap(), "5"); // add(2, 3) + assert_eq!(lines.next().unwrap(), "1,3,4,5,6,8"); // sort(1, 5, 3, 6, 8, 4) + assert_eq!(lines.next().unwrap(), "0 1 2"); // inc() inc() inc() + assert_eq!(lines.next().unwrap(), "5"); // magnitude({ x: 3, y: 4 }) + assert_eq!(lines.next().unwrap(), "set by JavaScript!"); // set() + assert_eq!(lines.next().unwrap(), "set by JavaScript!"); // get() +}); + +fn full_build(manifest_path: &str) -> bool { + let args = app().get_matches_from(vec!["stuart", "build", "--manifest-path", manifest_path]); + let result = match args.subcommand() { + Some(("build", args)) => build(args), + _ => unreachable!(), + }; + + if let Err(e) = result { + e.print(); + false + } else { + true + } +} + +fn cleanup(manifest_path: &str) { + let path = Path::new(manifest_path); + let dist = path.parent().unwrap().join("dist"); + let _ = remove_dir_all(dist); + let _ = remove_file(path.parent().unwrap().join("metadata.json")); +} diff --git a/stuart/example/content/index.html b/stuart/tests/basic/content/index.html similarity index 100% rename from stuart/example/content/index.html rename to stuart/tests/basic/content/index.html diff --git a/stuart/example/content/root.html b/stuart/tests/basic/content/root.html similarity index 100% rename from stuart/example/content/root.html rename to stuart/tests/basic/content/root.html diff --git a/stuart/example/static/lightning.png b/stuart/tests/basic/static/lightning.png similarity index 100% rename from stuart/example/static/lightning.png rename to stuart/tests/basic/static/lightning.png diff --git a/stuart/example/static/style.css b/stuart/tests/basic/static/style.css similarity index 100% rename from stuart/example/static/style.css rename to stuart/tests/basic/static/style.css diff --git a/stuart/tests/basic/stuart.toml b/stuart/tests/basic/stuart.toml new file mode 100644 index 0000000..372dd13 --- /dev/null +++ b/stuart/tests/basic/stuart.toml @@ -0,0 +1,3 @@ +[site] +name = "example" +author = "William Henderson " diff --git a/stuart/tests/js/content/data.json b/stuart/tests/js/content/data.json new file mode 100644 index 0000000..5ab6436 --- /dev/null +++ b/stuart/tests/js/content/data.json @@ -0,0 +1,4 @@ +{ + "x": 3, + "y": 4 +} \ No newline at end of file diff --git a/stuart/tests/js/content/index.html b/stuart/tests/js/content/index.html new file mode 100644 index 0000000..99e6559 --- /dev/null +++ b/stuart/tests/js/content/index.html @@ -0,0 +1,8 @@ +{{ begin("body") }} + {{ my_js_plugin::add(2, 3) }} + {{ my_js_plugin::sort(1, 5, 3, 6, 8, 4) }} + {{ my_js_plugin::inc() }} {{ my_js_plugin::inc() }} {{ my_js_plugin::inc() }} + {{ import($data, "./data.json") }} {{ my_js_plugin::magnitude($data) }} + {{ my_js_plugin::set() }} {{ $my_var }} + {{ my_js_plugin::get() }} +{{ end("body") }} \ No newline at end of file diff --git a/stuart/tests/js/content/root.html b/stuart/tests/js/content/root.html new file mode 100644 index 0000000..0d28d39 --- /dev/null +++ b/stuart/tests/js/content/root.html @@ -0,0 +1 @@ +{{ insert("body") }} \ No newline at end of file diff --git a/stuart/tests/js/plugin.mjs b/stuart/tests/js/plugin.mjs new file mode 100644 index 0000000..16fdb7f --- /dev/null +++ b/stuart/tests/js/plugin.mjs @@ -0,0 +1,72 @@ +/* Basic add function */ +function add(a, b) { + return a + b; +} + +/* Test function with variable number of arguments */ +function bubbleSort(...items) { + let arr = [...items]; + let swapped = true; + for (let i = 0; swapped && i < arr.length; i++) { + swapped = false; + for (let j = 0; j < arr.length - i - 1; j++) { + if (arr[j + 1] < arr[j]) { + [arr[j + 1], arr[j]] = [arr[j], arr[j + 1]]; + swapped = true; + } + } + } + return arr; +} + +/* Test function with internal (to the plugin) state */ +let state = 0; +function inc() { + return state++; +} + +/* Test function which takes in a more complex type */ +function magnitude(v) { + return Math.sqrt(v.x * v.x + v.y * v.y); +} + +/* Test function which sets a variable in Rust */ +function setVariable() { + STUART.set("my_var", "set by JavaScript!"); +} + +/* Test function which gets a variable from Rust */ +function getVariable() { + return STUART.get("my_var"); +} + +export default { + name: "my_js_plugin", + version: "0.0.1", + functions: [ + { + name: "add", + fn: add + }, + { + name: "sort", + fn: bubbleSort + }, + { + name: "inc", + fn: inc + }, + { + name: "magnitude", + fn: magnitude + }, + { + name: "set", + fn: setVariable + }, + { + name: "get", + fn: getVariable + } + ] +} \ No newline at end of file diff --git a/stuart/tests/js/stuart.toml b/stuart/tests/js/stuart.toml new file mode 100644 index 0000000..f943824 --- /dev/null +++ b/stuart/tests/js/stuart.toml @@ -0,0 +1,6 @@ +[site] +name = "example" +author = "William Henderson " + +[dependencies] +my_js_plugin = "./plugin.mjs"