├── .github └── workflows │ └── check.yml ├── .gitignore ├── LICENSE ├── README.md ├── _typos.toml ├── moon.mod.json └── src ├── clipboard ├── aliases.mbt ├── clipboard.mbt ├── clipboard.mbti └── moon.pkg.json ├── cmd ├── README.md ├── cmd.mbti ├── command.mbt └── moon.pkg.json ├── dialog ├── aliases.mbt ├── dialog.mbt ├── dialog.mbti └── moon.pkg.json ├── dom ├── canvas.mbt ├── clipboard.mbt ├── data_transfer.mbt ├── dom.mbti ├── dom_cast.mbt ├── dom_document.mbt ├── dom_element.mbt ├── dom_event.mbt ├── dom_event_target.mbt ├── dom_fragment.mbt ├── dom_node.mbt ├── dom_rect.mbt ├── dom_text.mbt ├── dom_window.mbt ├── moon.pkg.json └── navigator.mbt ├── example ├── async │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── main │ │ ├── async.mbti │ │ ├── main.mbt │ │ └── moon.pkg.json │ ├── moon.mod.json │ ├── package-lock.json │ ├── package.json │ └── vite.config.js ├── canvas │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── main │ │ ├── canvas.mbti │ │ ├── main.mbt │ │ └── moon.pkg.json │ ├── moon.mod.json │ ├── package-lock.json │ ├── package.json │ └── vite.config.js ├── clipboard │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── main │ │ ├── main.mbt │ │ ├── main.mbti │ │ └── moon.pkg.json │ ├── moon.mod.json │ ├── package-lock.json │ ├── package.json │ ├── vite.config.js │ └── vite.config.js.timestamp-1745560557839-1da781a9e4714.mjs ├── counter │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── main │ │ ├── counter.mbti │ │ ├── main.mbt │ │ └── moon.pkg.json │ ├── moon.mod.json │ ├── package-lock.json │ ├── package.json │ └── vite.config.js ├── custom_command │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── main │ │ ├── custom_command.mbti │ │ ├── main.mbt │ │ └── moon.pkg.json │ ├── moon.mod.json │ ├── package-lock.json │ ├── package.json │ └── vite.config.js ├── dialog │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── main │ │ ├── dialog.mbti │ │ ├── main.mbt │ │ └── moon.pkg.json │ ├── moon.mod.json │ ├── package-lock.json │ ├── package.json │ └── vite.config.js ├── form │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── main │ │ ├── form.mbti │ │ ├── main.mbt │ │ └── moon.pkg.json │ ├── moon.mod.json │ ├── package-lock.json │ ├── package.json │ └── vite.config.js └── todoMVC │ ├── .gitignore │ ├── README.md │ ├── editor │ ├── editor.mbt │ ├── editor.mbti │ └── moon.pkg.json │ ├── home │ ├── card.mbt │ ├── home.mbt │ ├── home.mbti │ ├── list.mbt │ └── moon.pkg.json │ ├── index.html │ ├── main │ ├── main.mbt │ ├── main.mbti │ └── moon.pkg.json │ ├── moon.mod.json │ ├── package-lock.json │ ├── package.json │ ├── server.js │ ├── views │ ├── button.mbt │ ├── moon.pkg.json │ └── views.mbti │ └── vite.config.js ├── html ├── README.md ├── attributes.mbt ├── canvas │ ├── canvas.mbt │ ├── canvas.mbti │ ├── context2d.mbt │ └── moon.pkg.json ├── event.mbt ├── html.mbt ├── html.mbti └── moon.pkg.json ├── http ├── http.mbt ├── http.mbti └── moon.pkg.json ├── internal ├── browser │ ├── README.md │ ├── browser.mbti │ ├── command.mbt │ ├── moon.pkg.json │ └── sandbox.mbt ├── ffi │ ├── ffi.mbti │ ├── moon.pkg.json │ └── utils.mbt └── vdom │ ├── dom.mbt │ ├── moon.pkg.json │ ├── show.mbt │ ├── test.mbt │ └── vdom.mbti ├── moon.pkg.json ├── nav ├── README.md ├── aliases.mbt ├── moon.pkg.json ├── nav.mbti ├── navigation.mbt └── scroll.mbt ├── rabbit-tea.mbti ├── top.mbt ├── url ├── moon.pkg.json ├── url.mbt ├── url.mbti └── url_test.mbt └── variant ├── moon.pkg.json ├── variant.mbt └── variant.mbti /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | stable-build: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: 15 | - name: ubuntu-latest 16 | path: ubuntu_x86_64_moon_setup 17 | 18 | runs-on: ${{ matrix.os.name }} 19 | continue-on-error: true 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: install 24 | run: | 25 | curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash 26 | echo "$HOME/.moon/bin" >> $GITHUB_PATH 27 | 28 | - name: moon version 29 | run: | 30 | moon version --all 31 | moonrun --version 32 | 33 | - name: moon test 34 | run: | 35 | moon update 36 | moon install 37 | moon test --target js --serial --release 38 | moon test --target js --serial 39 | 40 | - name: moon check 41 | run: moon check --target js --deny-warn 42 | 43 | - name: moon info 44 | run: | 45 | moon info --target js 46 | git diff --exit-code 47 | 48 | - name: format diff 49 | run: | 50 | moon fmt 51 | git diff --exit-code 52 | 53 | typo-check: 54 | runs-on: ubuntu-latest 55 | timeout-minutes: 10 56 | env: 57 | FORCE_COLOR: 1 58 | TYPOS_VERSION: v1.19.0 59 | steps: 60 | - name: download typos 61 | run: curl -LsSf https://github.com/crate-ci/typos/releases/download/$TYPOS_VERSION/typos-$TYPOS_VERSION-x86_64-unknown-linux-musl.tar.gz | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin 62 | 63 | - name: Checkout repository 64 | uses: actions/checkout@v4 65 | with: 66 | ref: ${{ github.event.pull_request.head.sha }} 67 | 68 | - name: check typos 69 | run: typos 70 | 71 | 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .mooncakes/ 3 | node_modules/ 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rabbit-TEA 2 | 3 | A declarative and functional web UI framework inspired by The Elm Architecture. 4 | 5 | ## Features 6 | 7 | - Refactor Safety 8 | 9 | By leveraging *pattern matching* and *tagged union*, exhaustive checks provide 10 | a better refactoring experience. MoonBit helps prevent common runtime errors 11 | ensuring more robust and reliable code. 12 | 13 | - Maintainable State 14 | 15 | The state is globally managed, making it easier to maintain the entire application. 16 | By utilizing persistent data structures from `moonbitlang/core/immut`, 17 | you can implement advanced features such as undo/redo functionality with ease. 18 | 19 | - Lightweight Runtime 20 | 21 | The generated JavaScript file is 33KB after minified for a project like 22 | `src/example/counter`, including the virtual DOM. 23 | 24 | ## Basic Example 25 | 26 | ```moonbit 27 | typealias Model = Int 28 | let model = 0 29 | 30 | enum Msg { 31 | Increment 32 | Decrement 33 | } 34 | 35 | fn update(msg : Msg, model : Model) -> (Command[Msg], Model) { 36 | match msg { 37 | Increment => (none(), model + 1) 38 | Decrement => (none(), model - 1) 39 | } 40 | } 41 | 42 | fn view(model : Model) -> Html[Msg] { 43 | div([ 44 | h1([text(model.to_string())]), 45 | button(click=Msg::Increment, [text("+")]), 46 | button(click=Msg::Decrement, [text("-")]), 47 | ]) 48 | } 49 | 50 | fn main { 51 | @tea.startup(model~, update~, view~) 52 | } 53 | ``` 54 | 55 | For more examples, see the `src/example` directory. 56 | 57 | # Getting started 58 | 59 | To get started, you can use the [Rabbit-TEA template](https://github.com/Yoorkin/rabbit-tea-tailwind). 60 | 61 | It also includes instructions for debugging your code with Rabbit-TEA. 62 | -------------------------------------------------------------------------------- /_typos.toml: -------------------------------------------------------------------------------- 1 | [default.extend-words] 2 | childs = "childs" 3 | -------------------------------------------------------------------------------- /moon.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Yoorkin/rabbit-tea", 3 | "version": "0.7.11", 4 | "deps": { 5 | "rami3l/js-ffi": "0.2.4" 6 | }, 7 | "readme": "README.md", 8 | "repository": "https://github.com/Yoorkin/rabbit-tea", 9 | "license": "Apache-2.0", 10 | "keywords": [ 11 | "TEA", 12 | "html", 13 | "UI", 14 | "web" 15 | ], 16 | "description": "TEA web UI framework for MoonBit", 17 | "source": "src" 18 | } -------------------------------------------------------------------------------- /src/clipboard/aliases.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | typealias @cmd.Cmd 3 | -------------------------------------------------------------------------------- /src/clipboard/clipboard.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | pub(all) enum Item { 3 | Text(String) 4 | } 5 | 6 | ///| Returns a command that copies the given item to the clipboard. 7 | /// 8 | /// # Message 9 | /// 10 | /// - `copied` is triggered if the copy operation is successful. 11 | /// - `failed` is triggered with the error message if the copy operation fails. 12 | pub fn[M] copy(item : Item, copied? : M, failed? : (String) -> M) -> Cmd[M] { 13 | fn(dispatcher) { 14 | @js.async_run(fn() { 15 | try { 16 | guard item is Text(text) //TODO: support other formats 17 | @dom.window().navigator().clipboard().write_text(text).wait!() |> ignore 18 | match copied { 19 | Some(msg) => dispatcher.trigger_update(msg) 20 | None => () 21 | } 22 | } catch { 23 | e => 24 | match failed { 25 | Some(to_msg) => dispatcher.trigger_update(to_msg(e.to_string())) 26 | None => () 27 | } 28 | } 29 | }) 30 | } 31 | } 32 | 33 | ///| Returns a command that pastes the clipboard content and returns it as an item. 34 | /// 35 | /// # Message 36 | /// - `pasted` is triggered with the pasted item if the paste operation is successful. 37 | /// - `failed` is triggered with the error message if the paste operation fails. 38 | pub fn[M] paste(pasted~ : (Item) -> M, failed? : (String) -> M) -> Cmd[M] { 39 | fn(dispatcher) { 40 | @js.async_run(fn() { 41 | try { 42 | // TODO: support other formats 43 | let str : String = @dom.window() 44 | .navigator() 45 | .clipboard() 46 | .read_text() 47 | .wait!() 48 | |> @js.Value::cast() 49 | dispatcher.trigger_update(pasted(Text(str))) 50 | } catch { 51 | e => 52 | match failed { 53 | Some(to_msg) => dispatcher.trigger_update(to_msg(e.to_string())) 54 | None => () 55 | } 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/clipboard/clipboard.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/clipboard" 2 | 3 | import( 4 | "Yoorkin/rabbit-tea/cmd" 5 | ) 6 | 7 | // Values 8 | fn[M] copy(Item, copied? : M, failed? : (String) -> M) -> @cmd.Cmd[M] 9 | 10 | fn[M] paste(pasted~ : (Item) -> M, failed? : (String) -> M) -> @cmd.Cmd[M] 11 | 12 | // Types and methods 13 | pub(all) enum Item { 14 | Text(String) 15 | } 16 | 17 | // Type aliases 18 | 19 | // Traits 20 | 21 | -------------------------------------------------------------------------------- /src/clipboard/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "Yoorkin/rabbit-tea/cmd", 4 | "Yoorkin/rabbit-tea/dom", 5 | "rami3l/js-ffi/js" 6 | ] 7 | } -------------------------------------------------------------------------------- /src/cmd/README.md: -------------------------------------------------------------------------------- 1 | # Cmd 2 | 3 | The `Cmd[Msg]` type represent a task that has not yet been executed. 4 | It's a side effect managed by the runtime. The only way to run the command 5 | is return it from the `update` function: 6 | 7 | ```mbt 8 | fn update(msg : Msg, model : Model) -> (Cmd[Msg], Model) { 9 | match msg { 10 | Msg::Click(id) => { 11 | // create a command to tell rabbit-tea scroll to the element with the given id 12 | let cmd = @nav.scroll_to(id) 13 | let updated_model = {...} 14 | (cmd, updated_model) 15 | } 16 | } 17 | } 18 | ``` 19 | 20 | The `scroll_to` function creates a `cmd` value that is returned with the 21 | `updated_model`. This means the scrolling action does not occur immediately. 22 | Instead, it will scroll to the element with the specified `id` after the 23 | `updated_model` has been rendered. 24 | 25 | # Custom Command 26 | 27 | You can encapsulate a command to interoperate Rabbit-Tea with the external 28 | JavaScript world. 29 | 30 | The `Cmd` type acts as a wrapper for a callback function that will be executed 31 | by the runtime at a later point: 32 | 33 | ```mbt 34 | type Cmd[M] (Events[M]) -> Unit 35 | ``` 36 | 37 | It accepts the `Events[M]` type as a parameter, which is a collection of events 38 | that can be triggered by your command. The most common usage is to trigger 39 | another update with a message: 40 | 41 | ```mbt 42 | fn delay[M](msg : M, ms : Int) -> Cmd[M] { 43 | Cmd(fn(events){ 44 | set_timeout(fn(){ events.trigger_update(msg) }, ms) 45 | }) 46 | } 47 | 48 | extern "js" fn set_timeout(f : () -> Unit, ms : Int) = "(f,ms) => setTimeout(f, ms)" 49 | ``` 50 | 51 | The `@http` package is also implemented in a similar manner. 52 | 53 | For a complete example, refer to `src/example/custom_command`. 54 | 55 | # Design Considerations 56 | 57 | Why do we wrap tasks in `Cmd` instead of running them immediately? 58 | Here are some reasons: 59 | 60 | 1. **Tasks Need to Run After the New Model Is Rendered** 61 | 62 | - As shown in the example above, the `scroll_to` function must execute after 63 | the new model is rendered. If it runs immediately, the scroll action may not 64 | work as expected. 65 | 66 | - For instance, you might need to update the UI to a loading state before 67 | fetching data. If the HTTP task is executed immediately, the app could lose 68 | responsiveness. 69 | 70 | You might ask: "Why not make the `update` function asynchronous so tasks can 71 | run asynchronously, allowing the UI to update between those tasks?" Here's 72 | another reason: 73 | 74 | 2. **Encourages users follows the *Single Source of Truth* Principle** 75 | 76 | The *single source of truth* principle ensures that the new model and view 77 | are computed based on a single, consistent model. 78 | 79 | If the `update` function were asynchronous, updates could overlap, leading 80 | to situations where one update process occurs before another is completed. 81 | This could result in inconsistent states or unexpected behavior. 82 | 83 | ``` 84 | +---> update(msg1, old_model) ---> new_model1 85 | | 86 | old_model--+ 87 | | 88 | +---> update(msg2, old_model) ---> new_model2 89 | ``` 90 | 91 | Now you have two models, `new_model1` and `new_model2`. Which model should 92 | be used in the view? 93 | 94 | In some other UI frameworks, this issue could occur. In rabbit-tea, it can 95 | be avoided by using the `Cmd` pattern. If you doesn't use asynchronous 96 | functions in update, e.g. `@cmd.attempt` and `@cmd.perform`, you will never 97 | met this problem. 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/cmd/cmd.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/cmd" 2 | 3 | import( 4 | "Yoorkin/rabbit-tea/url" 5 | ) 6 | 7 | // Values 8 | fn[A, E : Error, M] attempt((Result[A, E]) -> M, () -> A!E) -> Cmd[M] 9 | 10 | fn[M] batch(Array[Cmd[M]]) -> Cmd[M] 11 | 12 | fn[A, B] map(Cmd[A], (A) -> B) -> Cmd[B] 13 | 14 | fn[M] none() -> Cmd[M] 15 | 16 | fn[A, M] perform((A) -> M, () -> A) -> Cmd[M] 17 | 18 | fn[M] task(M) -> Cmd[M] 19 | 20 | // Types and methods 21 | pub(all) type Cmd[M] (Events[M]) -> Unit 22 | fn[A, B] Cmd::map(Self[A], (A) -> B) -> Self[B] 23 | 24 | type Events[M] 25 | fn[M] Events::new((@url.Url) -> Unit, (@url.UrlRequest) -> Unit, (M) -> Unit) -> Self[M] 26 | fn[M] Events::trigger_update(Self[M], M) -> Unit 27 | fn[M] Events::trigger_url_changed(Self[M], @url.Url) -> Unit 28 | fn[M] Events::trigger_url_request(Self[M], @url.UrlRequest) -> Unit 29 | 30 | // Type aliases 31 | pub typealias Command[M] = Cmd[M] 32 | 33 | // Traits 34 | 35 | -------------------------------------------------------------------------------- /src/cmd/command.mbt: -------------------------------------------------------------------------------- 1 | ///| Store the events that can be triggered by the command. 2 | struct Events[M] { 3 | on_url_changed : (@url.Url) -> Unit 4 | on_url_request : (@url.UrlRequest) -> Unit 5 | on_update : (M) -> Unit 6 | } 7 | 8 | ///| Used by the runtime. 9 | pub fn[M] Events::new( 10 | on_url_changed : (@url.Url) -> Unit, 11 | on_url_request : (@url.UrlRequest) -> Unit, 12 | on_update : (M) -> Unit 13 | ) -> Events[M] { 14 | { on_url_changed, on_url_request, on_update } 15 | } 16 | 17 | ///| Trigger the update function with `url_changed` message config by the user. 18 | pub fn[M] Events::trigger_url_changed(self : Events[M], url : @url.Url) -> Unit { 19 | (self.on_url_changed)(url) 20 | } 21 | 22 | ///| Trigger the update function with `url_request` message config by the user. 23 | pub fn[M] Events::trigger_url_request( 24 | self : Events[M], 25 | url : @url.UrlRequest 26 | ) -> Unit { 27 | (self.on_url_request)(url) 28 | } 29 | 30 | ///| Trigger the update function with message `msg`. 31 | pub fn[M] Events::trigger_update(self : Events[M], msg : M) -> Unit { 32 | (self.on_update)(msg) 33 | } 34 | 35 | ///| The command type, represents a task that can be executed. 36 | /// 37 | /// You can define your own command to interoperate Rabbit-Tea with the outside JS world. 38 | /// 39 | /// Before implementing your own command, check the existing commands in the `nav` and `http` packages. 40 | /// 41 | /// # Example 42 | /// 43 | /// ``` 44 | /// fn delay[M](msg : M, ms : Int) -> Cmd[M] { 45 | /// Cmd(fn(events){ 46 | /// set_timeout(fn(){ events.trigger_update(msg) }, ms) 47 | /// }) 48 | /// } 49 | /// 50 | /// extern "js" fn set_timeout(f : () -> Unit, ms : Int) = "(f,ms) => setTimeout(f, ms)" 51 | /// ``` 52 | pub(all) type Cmd[M] (Events[M]) -> Unit 53 | 54 | ///| 55 | pub typealias Command[M] = Cmd[M] 56 | 57 | ///| Map the messages in the command to another type. 58 | pub fn[A, B] map(self : Cmd[A], f : (A) -> B) -> Cmd[B] { 59 | Cmd(fn(events) { 60 | let predef = { 61 | on_url_changed: events.on_url_changed, 62 | on_url_request: events.on_url_request, 63 | on_update: fn(msg) { (events.on_update)(f(msg)) }, 64 | } 65 | let Cmd(f) = self 66 | f(predef) 67 | }) 68 | } 69 | 70 | ///| Create a command that does nothing. 71 | pub fn[M] none() -> Cmd[M] { 72 | Cmd(fn { _ => () }) 73 | } 74 | 75 | ///| Create a command that runs multiple commands. 76 | pub fn[M] batch(xs : Array[Cmd[M]]) -> Cmd[M] { 77 | Cmd(fn(events) { xs.each(fn { Cmd(f) => f(events) }) }) 78 | } 79 | 80 | ///| Create a command that trigger another update for the given message. 81 | pub fn[M] task(message : M) -> Cmd[M] { 82 | Cmd(fn(events) { events.trigger_update(message) }) 83 | } 84 | 85 | ///| Create a command that runs an async function. 86 | /// 87 | /// The async function `f` will be called, and the result will be wrapped in a 88 | /// message `msg`, then trigger another update with this message. 89 | pub fn[A, M] perform(msg : (A) -> M, f : async () -> A) -> Cmd[M] { 90 | Cmd(fn(events) { @js.async_run(fn() { events.trigger_update(msg(f!())) }) }) 91 | } 92 | 93 | ///| Create a command that runs an async function and handles errors. 94 | /// 95 | /// This is similar to `perform`, but it converts the returned value 96 | /// or thrown error into a `Result`. 97 | pub fn[A, E : Error, M] attempt( 98 | msg : (Result[A, E]) -> M, 99 | f : async () -> A!E 100 | ) -> Cmd[M] { 101 | Cmd(fn(events) { 102 | @js.async_run(fn() { 103 | let msg = try f() catch { 104 | e => msg(Err(e)) 105 | } else { 106 | r => msg(Ok(r)) 107 | } 108 | events.trigger_update(msg) 109 | }) 110 | }) 111 | } 112 | -------------------------------------------------------------------------------- /src/cmd/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "Yoorkin/rabbit-tea/url", 4 | "rami3l/js-ffi/js" 5 | ] 6 | } -------------------------------------------------------------------------------- /src/dialog/aliases.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | typealias @cmd.Cmd 3 | -------------------------------------------------------------------------------- /src/dialog/dialog.mbt: -------------------------------------------------------------------------------- 1 | ///| Create a command to show a dialog. 2 | /// 3 | /// The `modal` argument determines whether the dialog is modal or not. 4 | pub fn[M] show(id : String, modal~ : Bool = true) -> Cmd[M] { 5 | fn(_) { 6 | let dialog = @dom.document() 7 | .get_element_by_id(id) 8 | .to_option() 9 | .unwrap() 10 | .to_html_element() 11 | .to_option() 12 | .unwrap() 13 | .to_html_dialog_element() 14 | .to_option() 15 | .unwrap() 16 | if modal { 17 | dialog.show_modal() 18 | } else { 19 | dialog.show() 20 | } 21 | } 22 | } 23 | 24 | ///| Create a command to close a dialog. 25 | /// 26 | /// The `return_value` argument is the value that will be filled in the `close` message. 27 | pub fn[M] close(id : String, return_value? : String) -> Cmd[M] { 28 | fn(_) { 29 | let dialog = @dom.document() 30 | .get_element_by_id(id) 31 | .to_option() 32 | .unwrap() 33 | .to_html_element() 34 | .to_option() 35 | .unwrap() 36 | .to_html_dialog_element() 37 | .to_option() 38 | .unwrap() 39 | dialog.close(return_value=@js.Optional::from_option(return_value)) 40 | } 41 | } 42 | 43 | ///| Create a command to request closing a dialog. 44 | /// 45 | /// This command will not close the dialog immediately; it will trigger the 46 | /// `cancel` message. 47 | /// 48 | /// The `return_value` argument is the value that will be filled in the `close` message. 49 | pub fn[M] request_close(id : String, return_value? : String) -> Cmd[M] { 50 | fn(_) { 51 | let dialog = @dom.document() 52 | .get_element_by_id(id) 53 | .to_option() 54 | .unwrap() 55 | .to_html_element() 56 | .to_option() 57 | .unwrap() 58 | .to_html_dialog_element() 59 | .to_option() 60 | .unwrap() 61 | dialog.request_close(return_value=@js.Optional::from_option(return_value)) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/dialog/dialog.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/dialog" 2 | 3 | import( 4 | "Yoorkin/rabbit-tea/cmd" 5 | ) 6 | 7 | // Values 8 | fn[M] close(String, return_value? : String) -> @cmd.Cmd[M] 9 | 10 | fn[M] request_close(String, return_value? : String) -> @cmd.Cmd[M] 11 | 12 | fn[M] show(String, modal~ : Bool = ..) -> @cmd.Cmd[M] 13 | 14 | // Types and methods 15 | 16 | // Type aliases 17 | 18 | // Traits 19 | 20 | -------------------------------------------------------------------------------- /src/dialog/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "Yoorkin/rabbit-tea/cmd", 4 | "Yoorkin/rabbit-tea/dom", 5 | "rami3l/js-ffi/js" 6 | ] 7 | } -------------------------------------------------------------------------------- /src/dom/canvas.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | extern type HTMLCanvasElement 3 | 4 | ///| 5 | pub extern "js" fn HTMLCanvasElement::to_html_element( 6 | self : HTMLCanvasElement 7 | ) -> HTMLElement = "(x) => x" 8 | 9 | ///| 10 | pub extern "js" fn HTMLCanvasElement::get_width( 11 | self : HTMLCanvasElement 12 | ) -> Int = "(x) => x.width" 13 | 14 | ///| 15 | pub extern "js" fn HTMLCanvasElement::get_height( 16 | self : HTMLCanvasElement 17 | ) -> Int = "(x) => x.height" 18 | 19 | ///| 20 | pub extern "js" fn HTMLCanvasElement::get_context( 21 | self : HTMLCanvasElement, 22 | context_id : String 23 | ) -> 24 | @js.Union5[ 25 | CanvasRenderingContext2D, 26 | ImageBitmapRenderingContext, 27 | WebGLRenderingContext, 28 | WebGL2RenderingContext, 29 | GPUCanvasContext, 30 | ] = "(x, id) => x.getContext(id)" 31 | 32 | ///| 33 | extern type CanvasRenderingContext2D 34 | 35 | ///| 36 | extern "js" fn checked_to_canvas_rendering_context_2d( 37 | value : @js.Value 38 | ) -> @js.Nullable[CanvasRenderingContext2D] = 39 | #| (value) => value instanceof CanvasRenderingContext2D ? value : null 40 | 41 | ///| 42 | pub impl @js.Cast for CanvasRenderingContext2D with into(value) { 43 | checked_to_canvas_rendering_context_2d(value).to_option() 44 | } 45 | 46 | ///| 47 | pub impl @js.Cast for CanvasRenderingContext2D with from(value) { 48 | @js.Value::cast_from(value) 49 | } 50 | 51 | ///| 52 | extern type ImageBitmapRenderingContext 53 | 54 | ///| 55 | extern type WebGLRenderingContext 56 | 57 | ///| 58 | extern type WebGL2RenderingContext 59 | 60 | ///| 61 | extern type GPUCanvasContext 62 | 63 | ///| 64 | pub extern "js" fn CanvasRenderingContext2D::save( 65 | self : CanvasRenderingContext2D 66 | ) = "(x) => x.save()" 67 | 68 | ///| 69 | pub extern "js" fn CanvasRenderingContext2D::restore( 70 | self : CanvasRenderingContext2D 71 | ) = "(x) => x.restore()" 72 | 73 | ///| 74 | pub extern "js" fn CanvasRenderingContext2D::reset( 75 | self : CanvasRenderingContext2D 76 | ) = "(x) => x.reset()" 77 | 78 | ///| 79 | pub extern "js" fn CanvasRenderingContext2D::is_context_lost( 80 | self : CanvasRenderingContext2D 81 | ) -> Bool = "() => x.isContextLost()" 82 | 83 | ///| 84 | pub extern "js" fn CanvasRenderingContext2D::scale( 85 | self : CanvasRenderingContext2D, 86 | x : Double, 87 | y : Double 88 | ) = "(x,y) => x.scale(x,y)" 89 | 90 | ///| 91 | pub extern "js" fn CanvasRenderingContext2D::rotate( 92 | self : CanvasRenderingContext2D, 93 | angle : Double 94 | ) = "(x,angle) => x.rotate(angle)" 95 | 96 | ///| 97 | pub extern "js" fn CanvasRenderingContext2D::translate( 98 | self : CanvasRenderingContext2D, 99 | x : Double, 100 | y : Double 101 | ) = "(x,y) => x.translate(x,y)" 102 | 103 | ///| 104 | pub extern "js" fn CanvasRenderingContext2D::transform( 105 | self : CanvasRenderingContext2D, 106 | a : Double, 107 | b : Double, 108 | c : Double, 109 | d : Double, 110 | e : Double, 111 | f : Double 112 | ) = "(x,a,b,c,d,e,f) => x.transform(a,b,c,d,e,f)" 113 | 114 | ///| 115 | pub extern "js" fn CanvasRenderingContext2D::set_transform( 116 | self : CanvasRenderingContext2D, 117 | a : Double, 118 | b : Double, 119 | c : Double, 120 | d : Double, 121 | e : Double, 122 | f : Double 123 | ) = "(x,a,b,c,d,e,f) => x.setTransform(a,b,c,d,e,f)" 124 | 125 | ///| 126 | pub extern "js" fn CanvasRenderingContext2D::reset_transform( 127 | self : CanvasRenderingContext2D 128 | ) = "(x) => x.resetTransform()" 129 | 130 | // TODO: add interface CanvasCompositing 131 | ///| 132 | pub extern "js" fn CanvasRenderingContext2D::image_smoothing_enabled( 133 | self : CanvasRenderingContext2D 134 | ) -> Bool = "(x) => x.imageSmoothingEnabled" 135 | 136 | ///| 137 | pub extern "js" fn CanvasRenderingContext2D::get_image_smoothing_quality( 138 | self : CanvasRenderingContext2D 139 | ) -> String = "(x) => x.imageSmoothingQuality" 140 | 141 | ///| Possible values are "low", "medium", and "high". 142 | pub extern "js" fn CanvasRenderingContext2D::set_image_smoothing_quality( 143 | self : CanvasRenderingContext2D, 144 | value : String 145 | ) = "(x,value) => x.imageSmoothingQuality = value" 146 | 147 | ///| 148 | extern "js" fn checked_to_canvas_gradient( 149 | value : @js.Value 150 | ) -> @js.Nullable[CanvasGradient] = 151 | #| (value) => value instanceof CanvasGradient ? value : null 152 | 153 | ///| 154 | pub impl @js.Cast for CanvasGradient with into(value) { 155 | checked_to_canvas_gradient(value).to_option() 156 | } 157 | 158 | ///| 159 | pub impl @js.Cast for CanvasGradient with from(value) { 160 | @js.Value::cast_from(value) 161 | } 162 | 163 | ///| 164 | extern "js" fn checked_to_canvas_pattern( 165 | value : @js.Value 166 | ) -> @js.Nullable[CanvasPattern] = 167 | #| (value) => value instanceof CanvasPattern ? value : null 168 | 169 | ///| 170 | pub impl @js.Cast for CanvasPattern with into(value) { 171 | checked_to_canvas_pattern(value).to_option() 172 | } 173 | 174 | ///| 175 | pub impl @js.Cast for CanvasPattern with from(value) { 176 | @js.Value::cast_from(value) 177 | } 178 | 179 | ///| 180 | pub extern "js" fn CanvasRenderingContext2D::get_stroke_style( 181 | self : CanvasRenderingContext2D 182 | ) -> @js.Union3[String, CanvasGradient, CanvasPattern] = 183 | #| (x) => x.strokeStyle 184 | 185 | ///| 186 | pub extern "js" fn CanvasRenderingContext2D::set_stroke_style( 187 | self : CanvasRenderingContext2D, 188 | value : @js.Union3[String, CanvasGradient, CanvasPattern] 189 | ) = 190 | #| (x,value) => x.strokeStyle = value 191 | 192 | ///| 193 | pub extern "js" fn CanvasRenderingContext2D::get_fill_style( 194 | self : CanvasRenderingContext2D 195 | ) -> @js.Union3[String, CanvasGradient, CanvasPattern] = 196 | #| (x) => x.fillStyle 197 | 198 | ///| 199 | pub extern "js" fn CanvasRenderingContext2D::set_fill_style( 200 | self : CanvasRenderingContext2D, 201 | value : @js.Union3[String, CanvasGradient, CanvasPattern] 202 | ) = 203 | #| (x,value) => x.fillStyle = value 204 | 205 | ///| 206 | pub extern "js" fn CanvasRenderingContext2D::create_linear_gradient( 207 | self : CanvasRenderingContext2D, 208 | x0 : Double, 209 | y0 : Double, 210 | x1 : Double, 211 | y1 : Double 212 | ) -> CanvasGradient = "(x,x0,y0,x1,y1) => x.createLinearGradient(x0,y0,x1,y1)" 213 | 214 | ///| 215 | pub extern "js" fn CanvasRenderingContext2D::create_radial_gradient( 216 | self : CanvasRenderingContext2D, 217 | x0 : Double, 218 | y0 : Double, 219 | r0 : Double, 220 | x1 : Double, 221 | y1 : Double, 222 | r1 : Double 223 | ) -> CanvasGradient = "(x,x0,y0,r0,x1,y1,r1) => x.createRadialGradient(x0,y0,r0,x1,y1,r1)" 224 | 225 | // TODO: add interface CanvasShadowStyles 226 | // TODO: add interface CanvasFilters 227 | 228 | ///| 229 | pub extern "js" fn CanvasRenderingContext2D::clear_rect( 230 | self : CanvasRenderingContext2D, 231 | x : Double, 232 | y : Double, 233 | w : Double, 234 | h : Double 235 | ) = "(self,x,y,w,h) => self.clearRect(x,y,w,h)" 236 | 237 | ///| 238 | pub extern "js" fn CanvasRenderingContext2D::fill_rect( 239 | self : CanvasRenderingContext2D, 240 | x : Double, 241 | y : Double, 242 | w : Double, 243 | h : Double 244 | ) = "(self,x,y,w,h) => self.fillRect(x,y,w,h)" 245 | 246 | ///| 247 | pub extern "js" fn CanvasRenderingContext2D::stroke_rect( 248 | self : CanvasRenderingContext2D, 249 | x : Double, 250 | y : Double, 251 | w : Double, 252 | h : Double 253 | ) = "(self,x,y,w,h) => self.stroke_rect(x,y,w,h)" 254 | 255 | // TODO: add interface CanvasDrawPath 256 | 257 | ///| 258 | pub extern "js" fn CanvasRenderingContext2D::begin_path( 259 | self : CanvasRenderingContext2D 260 | ) = "(self) => self.beginPath()" 261 | 262 | ///| 263 | pub extern "js" fn CanvasRenderingContext2D::fill( 264 | self : CanvasRenderingContext2D, 265 | fill_rule~ : String = "nonzero" 266 | ) = "(self, fillRule) => self.fill(fillRule)" 267 | 268 | ///| 269 | pub extern "js" fn CanvasRenderingContext2D::fill_path( 270 | self : CanvasRenderingContext2D, 271 | path : Path2D, 272 | fill_rule~ : String = "nonzero" 273 | ) = "(self,path,fillRule) => self.stroke(path,fillRule)" 274 | 275 | ///| 276 | pub extern "js" fn CanvasRenderingContext2D::stroke( 277 | self : CanvasRenderingContext2D 278 | ) = "(self) => self.stroke()" 279 | 280 | ///| 281 | pub extern "js" fn CanvasRenderingContext2D::stroke_with_path( 282 | self : CanvasRenderingContext2D, 283 | path : Path2D 284 | ) = "(self,path) => self.stroke(path)" 285 | 286 | ///| 287 | pub extern "js" fn CanvasRenderingContext2D::clip( 288 | self : CanvasRenderingContext2D, 289 | fill_rule~ : String = "nonzero" 290 | ) = "(self, fillRule) => self.clip(fillRule)" 291 | 292 | ///| 293 | pub extern "js" fn CanvasRenderingContext2D::clip_with_path( 294 | self : CanvasRenderingContext2D, 295 | path : Path2D, 296 | fill_rule~ : String = "nonzero" 297 | ) = "(self,path,fillRule) => self.clip(path,fillRule)" 298 | 299 | ///| 300 | pub extern "js" fn CanvasRenderingContext2D::is_point_in_path( 301 | self : CanvasRenderingContext2D, 302 | x : Double, 303 | y : Double, 304 | fill_rule~ : String = "nonzero" 305 | ) -> Bool = "(self,x,y,fillRule) => self.isPointInPath(x,y,fillRule)" 306 | 307 | ///| 308 | pub extern "js" fn CanvasRenderingContext2D::is_point_in_path_with_path( 309 | self : CanvasRenderingContext2D, 310 | path : Path2D, 311 | x : Double, 312 | y : Double, 313 | fill_rule~ : String = "nonzero" 314 | ) -> Bool = "(self,path,x,y,fillRule) => self.isPointInPath(path,x,y,fillRule)" 315 | 316 | ///| 317 | pub extern "js" fn CanvasRenderingContext2D::is_point_in_stroke( 318 | self : CanvasRenderingContext2D, 319 | x : Double, 320 | y : Double 321 | ) -> Bool = "(self,x,y) => self.isPointInStroke(x,y)" 322 | 323 | ///| 324 | pub extern "js" fn CanvasRenderingContext2D::is_point_in_stroke_with_path( 325 | self : CanvasRenderingContext2D, 326 | path : Path2D, 327 | x : Double, 328 | y : Double 329 | ) -> Bool = "(self,path,x,y) => self.isPointInStroke(path,x,y)" 330 | 331 | // TODO: add interface CanvasUserInterface 332 | 333 | ///| 334 | pub extern "js" fn CanvasRenderingContext2D::fill_text( 335 | self : CanvasRenderingContext2D, 336 | text : String, 337 | x : Double, 338 | y : Double, 339 | max_width~ : @js.Optional[Double] = @js.Optional::undefined() 340 | ) = "(self,text,x,y,maxWidth) => self.fill_text(text,x,y,maxWidth)" 341 | 342 | ///| 343 | pub extern "js" fn CanvasRenderingContext2D::stroke_text( 344 | self : CanvasRenderingContext2D, 345 | text : String, 346 | x : Double, 347 | y : Double, 348 | max_width~ : @js.Optional[Double] = @js.Optional::undefined() 349 | ) = "(self,text,x,y,maxWidth) => self.stroke_text(text,x,y,maxWidth)" 350 | 351 | // TODO: add ConvasText.measureText 352 | 353 | ///| 354 | pub extern "js" fn CanvasRenderingContext2D::draw_image( 355 | self : CanvasRenderingContext2D, 356 | dx : Double, 357 | dy : Double 358 | ) = "(self,image,dx,dy) => self.drawImage(image,dx,dy)" 359 | 360 | ///| 361 | pub extern "js" fn CanvasRenderingContext2D::draw_image_with_size( 362 | self : CanvasRenderingContext2D, 363 | dx : Double, 364 | dy : Double, 365 | dw : Double, 366 | dh : Double 367 | ) = "(self,image,dx,dy,dw,dh) => self.drawImage(image,dx,dy,dw,dh)" 368 | 369 | ///| 370 | pub extern "js" fn CanvasRenderingContext2D::draw_image_with_src_and_dst_size( 371 | self : CanvasRenderingContext2D, 372 | sx : Double, 373 | sy : Double, 374 | sw : Double, 375 | sh : Double, 376 | dx : Double, 377 | dy : Double, 378 | dw : Double, 379 | dh : Double 380 | ) = "(self,image,sx,sy,sw,sh,dx,dy,dw,dh) => self.drawImage(image,sx,sy,sw,sh,dx,dy,dw,dh)" 381 | 382 | ///| 383 | pub extern "js" fn CanvasRenderingContext2D::create_image_data( 384 | self : CanvasRenderingContext2D, 385 | sw : Double, 386 | sh : Double, 387 | settings~ : @js.Optional[ImageDataSettings] = @js.Optional::undefined() 388 | ) -> ImageData = "(self,sw,sh,settings) => self.createImageData(sw,sh,settings)" 389 | 390 | ///| 391 | pub extern "js" fn CanvasRenderingContext2D::create_image_data_with_data( 392 | self : CanvasRenderingContext2D, 393 | data : ImageData 394 | ) -> ImageData = "(self,data) => self.createImageData(data)" 395 | 396 | ///| 397 | pub extern "js" fn CanvasRenderingContext2D::get_image_data( 398 | self : CanvasRenderingContext2D, 399 | sx : Double, 400 | sy : Double, 401 | sw : Double, 402 | sh : Double, 403 | settings~ : @js.Optional[ImageDataSettings] = @js.Optional::undefined() 404 | ) -> ImageData = "(self,sx,sy,sw,sh) => self.getImageData(sx,sy,sw,sh)" 405 | 406 | ///| 407 | pub extern "js" fn CanvasRenderingContext2D::put_image_data( 408 | self : CanvasRenderingContext2D, 409 | image_data : ImageData, 410 | dx : Int, 411 | dy : Int 412 | ) = "(self,imageData,dx,dy) => self.putImageData(imageData,dx,dy)" 413 | 414 | ///| 415 | pub extern "js" fn CanvasRenderingContext2D::put_image_data_with_dirty( 416 | self : CanvasRenderingContext2D, 417 | image_data : ImageData, 418 | dx : Int, 419 | dy : Int, 420 | dirty_x : Int, 421 | dirty_y : Int, 422 | dirty_width : Int, 423 | dirty_height : Int 424 | ) = 425 | #| (self,imageData,dx,dy,dirtyX,dirtyY,dirtyWidth,dirtyHeight) => 426 | #| self.putImageData(imageData,dx,dy,dirtyX,dirtyY,dirtyWidth,dirtyHeight) 427 | 428 | ///| 429 | pub extern "js" fn CanvasRenderingContext2D::get_line_with( 430 | self : CanvasRenderingContext2D 431 | ) -> Double = "(self) => self.lineWidth" 432 | 433 | ///| 434 | pub extern "js" fn CanvasRenderingContext2D::set_line_width( 435 | self : CanvasRenderingContext2D, 436 | value : Double 437 | ) = "(self,value) => self.lineWidth = value" 438 | 439 | ///| 440 | extern type CanvasLineCap 441 | 442 | ///| 443 | extern type CanvasLineJoin 444 | 445 | ///| 446 | pub extern "js" fn CanvasRenderingContext2D::get_line_cap( 447 | self : CanvasRenderingContext2D 448 | ) -> CanvasLineCap = "(self) => self.lineCap" 449 | 450 | ///| 451 | pub extern "js" fn CanvasRenderingContext2D::set_line_cap( 452 | self : CanvasRenderingContext2D, 453 | value : CanvasLineCap 454 | ) = "(self,value) => self.lineCap = value" 455 | 456 | ///| 457 | pub extern "js" fn CanvasRenderingContext2D::get_line_join( 458 | self : CanvasRenderingContext2D 459 | ) -> CanvasLineJoin = "(self) => self.lineJoin" 460 | 461 | ///| 462 | pub extern "js" fn CanvasRenderingContext2D::set_line_join( 463 | self : CanvasRenderingContext2D, 464 | value : CanvasLineJoin 465 | ) = "(self,value) => self.lineJoin = value" 466 | 467 | // TODO: add CanvasPathDrawingStyles.setLineDash, CanvasPathDrawingStyles.getLineDash, CanvasPathDrawingStyles.getLineDashOffset 468 | // TODO: add CanvasTextDrawingStyles 469 | 470 | ///| 471 | pub extern "js" fn CanvasRenderingContext2D::close_path( 472 | self : CanvasRenderingContext2D 473 | ) = "(self) => self.closePath()" 474 | 475 | ///| 476 | pub extern "js" fn CanvasRenderingContext2D::move_to( 477 | self : CanvasRenderingContext2D, 478 | x : Double, 479 | y : Double 480 | ) = "(self,x,y) => self.moveTo(x,y)" 481 | 482 | ///| 483 | pub extern "js" fn CanvasRenderingContext2D::line_to( 484 | self : CanvasRenderingContext2D, 485 | x : Double, 486 | y : Double 487 | ) = "(self,x,y) => self.lineTo(x,y)" 488 | 489 | ///| 490 | pub extern "js" fn CanvasRenderingContext2D::quardratic_curve_to( 491 | self : CanvasRenderingContext2D, 492 | cpx : Double, 493 | cpy : Double, 494 | x : Double, 495 | y : Double 496 | ) = "(self,cpx,cpy,x,y) => self.quadraticCurveTo(cpx,cpy,x,y)" 497 | 498 | ///| 499 | pub extern "js" fn CanvasRenderingContext2D::bezier_curve_to( 500 | self : CanvasRenderingContext2D, 501 | cp1x : Double, 502 | cp1y : Double, 503 | cp2x : Double, 504 | cp2y : Double, 505 | x : Double, 506 | y : Double 507 | ) = "(self,cp1x,cp1y,cp2x,cp2y,x,y) => self.bezierCurveTo(cp1x,cp1y,cp2x,cp2y,x,y)" 508 | 509 | ///| 510 | pub extern "js" fn CanvasRenderingContext2D::arc_to( 511 | self : CanvasRenderingContext2D, 512 | x1 : Double, 513 | y1 : Double, 514 | x2 : Double, 515 | y2 : Double, 516 | radius : Double 517 | ) = "(self,x1,y1,x2,y2,radius) => self.arcTo(x1,y1,x2,y2,radius)" 518 | 519 | ///| 520 | pub extern "js" fn CanvasRenderingContext2D::rect( 521 | self : CanvasRenderingContext2D, 522 | x : Double, 523 | y : Double, 524 | w : Double, 525 | h : Double 526 | ) = "(self,x,y,w,h) => self.rect(x,y,w,h)" 527 | 528 | ///| 529 | pub extern "js" fn CanvasRenderingContext2D::round_rect( 530 | self : CanvasRenderingContext2D, 531 | x : Double, 532 | y : Double, 533 | w : Double, 534 | h : Double, 535 | // FIXME: support other types like DOMPointInit or sequence 536 | radius : Double 537 | ) = "(self,x,y,w,h,radius) => self.roundRect(x,y,w,h,radius)" 538 | 539 | ///| 540 | pub extern "js" fn CanvasRenderingContext2D::arc( 541 | self : CanvasRenderingContext2D, 542 | x : Double, 543 | y : Double, 544 | radius : Double, 545 | start_angle : Double, 546 | end_angle : Double, 547 | anticlockwise~ : Bool = false 548 | ) = "(self,x,y,radius,startAngle,endAngle,anticlockwise) => self.arc(x,y,radius,startAngle,endAngle,anticlockwise)" 549 | 550 | ///| 551 | pub extern "js" fn CanvasRenderingContext2D::ellipse( 552 | self : CanvasRenderingContext2D, 553 | x : Double, 554 | y : Double, 555 | radius_x : Double, 556 | radius_y : Double, 557 | rotation : Double, 558 | start_angle : Double, 559 | end_angle : Double, 560 | anticlockwise~ : Bool = false 561 | ) = "(self,x,y,radiusX,radiusY,rotation,startAngle,endAngle,anticlockwise) => self.ellipse(x,y,radiusX,radiusY,rotation,startAngle,endAngle,anticlockwise)" 562 | 563 | ///| 564 | extern type CanvasGradient 565 | 566 | ///| 567 | pub extern "js" fn CanvasGradient::add_color_stop( 568 | self : CanvasGradient, 569 | offset : Double, 570 | color : String 571 | ) = "(self,offset,color) => self.addColorStop(offset,color)" 572 | 573 | ///| 574 | extern type CanvasPattern 575 | 576 | ///| 577 | pub extern "js" fn CanvasPattern::set_transform( 578 | self : CanvasPattern, 579 | transform : @js.Optional[DOMMatrix2DInit] 580 | ) = "(self,transform) => self.setTransform(transform)" 581 | 582 | ///| 583 | extern type DOMMatrix2DInit 584 | 585 | // TODO: add TextMetrics 586 | 587 | ///| 588 | extern type ImageDataSettings 589 | 590 | ///| Possible values are "srgb" and "display-p3". 591 | pub typealias PredefinedColorSpace = String 592 | 593 | ///| 594 | pub extern "js" fn ImageDataSettings::new( 595 | color_space : PredefinedColorSpace 596 | ) -> ImageDataSettings = "(colorSpace) => ({colorSpace: colorSpace})" 597 | 598 | ///| 599 | extern type ImageData 600 | 601 | ///| 602 | pub extern "js" fn ImageData::new( 603 | sw : Int, 604 | sh : Int, 605 | settings : @js.Optional[ImageDataSettings] 606 | ) -> ImageData = "(sw,sh,settings) => new ImageData(sw,sh,settings)" 607 | 608 | ///| 609 | pub extern "js" fn ImageData::new_with_data( 610 | data : Uint8ClampedArray, 611 | sw : Int, 612 | sh : Int, 613 | settings~ : @js.Optional[ImageDataSettings] = @js.Optional::undefined() 614 | ) -> ImageData = "(data,sw,sh,settings) => new ImageData(data,sw,sh,settings)" 615 | 616 | ///| 617 | pub extern "js" fn ImageData::get_width(self : ImageData) -> Int = "(self) => self.width" 618 | 619 | ///| 620 | pub extern "js" fn ImageData::get_height(self : ImageData) -> Int = "(self) => self.height" 621 | 622 | ///| 623 | pub extern "js" fn ImageData::get_data(self : ImageData) -> Uint8ClampedArray = "(self) => self.data" 624 | 625 | // TODO: support ImageData.data 626 | 627 | ///| 628 | pub extern "js" fn ImageData::get_color_space( 629 | self : ImageData 630 | ) -> PredefinedColorSpace = "(self) => self.colorSpace" 631 | 632 | ///| 633 | extern type Path2D 634 | 635 | ///| 636 | pub extern "js" fn Path2D::new( 637 | path~ : @js.Optional[@js.Union2[Path2D, String]] = @js.Optional::undefined() 638 | ) -> Path2D = "(path) => new Path2D(path)" 639 | 640 | ///| 641 | pub extern "js" fn Path2D::add_path( 642 | self : Path2D, 643 | path : Path2D, 644 | transform~ : @js.Optional[DOMMatrix2DInit] = @js.Optional::undefined() 645 | ) = "(self,path,transform) => self.addPath(path,transform)" 646 | 647 | ///| 648 | extern type Uint8ClampedArray 649 | -------------------------------------------------------------------------------- /src/dom/clipboard.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | pub extern type Clipboard 3 | 4 | // TODO: support ClipboardItem 5 | // 6 | // ///| 7 | // pub extern type ClipboardItem 8 | 9 | // ///| 10 | // pub extern "js" fn Clipboard::write( 11 | // self : Clipboard, 12 | // data : Array[ClipboardItem] 13 | // ) = 14 | // #| (self,data) => self.write(data) 15 | 16 | // ///| 17 | // pub extern "js" fn Clipboard::read(self : Clipboard) -> @js.Promise = 18 | // #| (self) => self.read() 19 | 20 | ///| 21 | pub extern "js" fn Clipboard::write_text( 22 | self : Clipboard, 23 | text : String 24 | ) -> @js.Promise = 25 | #| (self,text) => self.writeText(text) 26 | 27 | ///| 28 | pub extern "js" fn Clipboard::read_text(self : Clipboard) -> @js.Promise = 29 | #| (self) => self.readText() 30 | -------------------------------------------------------------------------------- /src/dom/data_transfer.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | extern type DataTransfer 3 | 4 | ///| 5 | pub extern "js" fn DataTransfer::drop_effect(self : DataTransfer) -> String = "(t) => t.dropEffect" 6 | 7 | ///| 8 | pub extern "js" fn DataTransfer::effect_allowed(self : DataTransfer) -> String = "(t) => t.effectAllowed" 9 | 10 | ///| 11 | pub extern "js" fn DataTransfer::items( 12 | self : DataTransfer 13 | ) -> DataTransferItemList = "(t) => t.items" 14 | 15 | ///| 16 | pub extern "js" fn DataTransfer::set_drog_image( 17 | self : DataTransfer, 18 | image : Element, 19 | x : Int, 20 | y : Int 21 | ) = "(t, image, x, y) => t.setDragImage(image, x, y)" 22 | 23 | ///| 24 | extern type DataTransferItemList 25 | 26 | ///| 27 | pub extern "js" fn DataTransferItemList::length( 28 | self : DataTransferItemList 29 | ) -> Int = "(l) => l.length" 30 | 31 | ///| 32 | pub extern "js" fn DataTransferItemList::op_get( 33 | self : DataTransferItemList, 34 | index : Int 35 | ) = "(l, i) => l[i]" 36 | 37 | ///| 38 | pub extern "js" fn DataTransferItemList::add( 39 | self : DataTransferItemList, 40 | data : String, 41 | type_ : String 42 | ) = "(l, d, t) => l.add(d, t)" 43 | 44 | ///| 45 | pub extern "js" fn DataTransferItemList::remove( 46 | self : DataTransferItemList, 47 | index : Int 48 | ) = "(l, i) => l.remove(i)" 49 | 50 | ///| 51 | pub extern "js" fn DataTransferItemList::clear(self : DataTransferItemList) = "(l) => l.clear()" 52 | -------------------------------------------------------------------------------- /src/dom/dom.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/dom" 2 | 3 | import( 4 | "rami3l/js-ffi/js" 5 | ) 6 | 7 | // Values 8 | const DOM_KEY_LOCATION_LEFT : Int = 0x01 9 | 10 | const DOM_KEY_LOCATION_NUMPAD : Int = 0x03 11 | 12 | const DOM_KEY_LOCATION_RIGHT : Int = 0x02 13 | 14 | const DOM_KEY_LOCATION_STANDARD : Int = 0x00 15 | 16 | fn add_event_listener(EventTarget, String, (Event) -> Unit) -> Unit 17 | 18 | fn append_child(Node, Node) -> Unit 19 | 20 | fn count_child(Node) -> Int 21 | 22 | fn create_document_fragment(Document) -> DocumentFragment 23 | 24 | fn create_element(Document, String) -> Element 25 | 26 | fn create_text_node(Document, String) -> Element 27 | 28 | fn current_url(Window) -> String 29 | 30 | fn dispatch_event(EventTarget, Event) -> Unit 31 | 32 | fn document() -> Document 33 | 34 | fn first_child(Node) -> Node 35 | 36 | fn get_element_by_id(Document, String) -> @js.Nullable[Element] 37 | 38 | fn history_go_back(Window) -> Unit 39 | 40 | fn history_go_forward(Window) -> Unit 41 | 42 | fn insert_before(Node, Node, Node) -> Unit 43 | 44 | fn last_child(Node) -> Node 45 | 46 | fn load_url(Window, String) -> Unit 47 | 48 | fn next_sibling(Node) -> Node 49 | 50 | fn node_name(Node) -> String 51 | 52 | fn node_type(Node) -> Int 53 | 54 | fn node_value(Node) -> String 55 | 56 | fn nth_child(Node, Int) -> Node 57 | 58 | fn parent_node(Node) -> Node 59 | 60 | fn previous_sibling(Node) -> Node 61 | 62 | fn push_url(Window, String) -> Unit 63 | 64 | fn reload_url(Window) -> Unit 65 | 66 | fn remove_child(Node, Node) -> Unit 67 | 68 | fn remove_event_listener(EventTarget, String, (Event) -> Unit) -> Unit 69 | 70 | fn replace_child(Node, Node, Node) -> Unit 71 | 72 | fn replace_url(Window, String) -> Unit 73 | 74 | fn request_animination_frame(Window, () -> Unit) -> Unit 75 | 76 | fn scroll_by(Window, Int, Int) -> Unit 77 | 78 | fn scroll_to(Window, Int, Int) -> Unit 79 | 80 | fn scroll_to_bottom(Window) -> Unit 81 | 82 | fn scroll_to_top(Window) -> Unit 83 | 84 | fn to_clipboard(EventTarget) -> @js.Nullable[Clipboard] 85 | 86 | fn to_document(Node) -> @js.Nullable[Document] 87 | 88 | fn to_document_fragment(Node) -> @js.Nullable[DocumentFragment] 89 | 90 | fn to_element(Node) -> @js.Nullable[Element] 91 | 92 | fn to_event_target(Node) -> EventTarget 93 | 94 | fn to_node(EventTarget) -> @js.Nullable[Node] 95 | 96 | fn to_text(Node) -> @js.Nullable[Text] 97 | 98 | fn window() -> Window 99 | 100 | // Types and methods 101 | type CanvasGradient 102 | fn CanvasGradient::add_color_stop(Self, Double, String) -> Unit 103 | impl @js.Cast for CanvasGradient 104 | 105 | type CanvasLineCap 106 | 107 | type CanvasLineJoin 108 | 109 | type CanvasPattern 110 | fn CanvasPattern::set_transform(Self, @js.Optional[DOMMatrix2DInit]) -> Unit 111 | impl @js.Cast for CanvasPattern 112 | 113 | type CanvasRenderingContext2D 114 | fn CanvasRenderingContext2D::arc(Self, Double, Double, Double, Double, Double, anticlockwise~ : Bool = ..) -> Unit 115 | fn CanvasRenderingContext2D::arc_to(Self, Double, Double, Double, Double, Double) -> Unit 116 | fn CanvasRenderingContext2D::begin_path(Self) -> Unit 117 | fn CanvasRenderingContext2D::bezier_curve_to(Self, Double, Double, Double, Double, Double, Double) -> Unit 118 | fn CanvasRenderingContext2D::clear_rect(Self, Double, Double, Double, Double) -> Unit 119 | fn CanvasRenderingContext2D::clip(Self, fill_rule~ : String = ..) -> Unit 120 | fn CanvasRenderingContext2D::clip_with_path(Self, Path2D, fill_rule~ : String = ..) -> Unit 121 | fn CanvasRenderingContext2D::close_path(Self) -> Unit 122 | fn CanvasRenderingContext2D::create_image_data(Self, Double, Double, settings~ : @js.Optional[ImageDataSettings] = ..) -> ImageData 123 | fn CanvasRenderingContext2D::create_image_data_with_data(Self, ImageData) -> ImageData 124 | fn CanvasRenderingContext2D::create_linear_gradient(Self, Double, Double, Double, Double) -> CanvasGradient 125 | fn CanvasRenderingContext2D::create_radial_gradient(Self, Double, Double, Double, Double, Double, Double) -> CanvasGradient 126 | fn CanvasRenderingContext2D::draw_image(Self, Double, Double) -> Unit 127 | fn CanvasRenderingContext2D::draw_image_with_size(Self, Double, Double, Double, Double) -> Unit 128 | fn CanvasRenderingContext2D::draw_image_with_src_and_dst_size(Self, Double, Double, Double, Double, Double, Double, Double, Double) -> Unit 129 | fn CanvasRenderingContext2D::ellipse(Self, Double, Double, Double, Double, Double, Double, Double, anticlockwise~ : Bool = ..) -> Unit 130 | fn CanvasRenderingContext2D::fill(Self, fill_rule~ : String = ..) -> Unit 131 | fn CanvasRenderingContext2D::fill_path(Self, Path2D, fill_rule~ : String = ..) -> Unit 132 | fn CanvasRenderingContext2D::fill_rect(Self, Double, Double, Double, Double) -> Unit 133 | fn CanvasRenderingContext2D::fill_text(Self, String, Double, Double, max_width~ : @js.Optional[Double] = ..) -> Unit 134 | fn CanvasRenderingContext2D::get_fill_style(Self) -> @js.Union3[String, CanvasGradient, CanvasPattern] 135 | fn CanvasRenderingContext2D::get_image_data(Self, Double, Double, Double, Double, settings~ : @js.Optional[ImageDataSettings] = ..) -> ImageData 136 | fn CanvasRenderingContext2D::get_image_smoothing_quality(Self) -> String 137 | fn CanvasRenderingContext2D::get_line_cap(Self) -> CanvasLineCap 138 | fn CanvasRenderingContext2D::get_line_join(Self) -> CanvasLineJoin 139 | fn CanvasRenderingContext2D::get_line_with(Self) -> Double 140 | fn CanvasRenderingContext2D::get_stroke_style(Self) -> @js.Union3[String, CanvasGradient, CanvasPattern] 141 | fn CanvasRenderingContext2D::image_smoothing_enabled(Self) -> Bool 142 | fn CanvasRenderingContext2D::is_context_lost(Self) -> Bool 143 | fn CanvasRenderingContext2D::is_point_in_path(Self, Double, Double, fill_rule~ : String = ..) -> Bool 144 | fn CanvasRenderingContext2D::is_point_in_path_with_path(Self, Path2D, Double, Double, fill_rule~ : String = ..) -> Bool 145 | fn CanvasRenderingContext2D::is_point_in_stroke(Self, Double, Double) -> Bool 146 | fn CanvasRenderingContext2D::is_point_in_stroke_with_path(Self, Path2D, Double, Double) -> Bool 147 | fn CanvasRenderingContext2D::line_to(Self, Double, Double) -> Unit 148 | fn CanvasRenderingContext2D::move_to(Self, Double, Double) -> Unit 149 | fn CanvasRenderingContext2D::put_image_data(Self, ImageData, Int, Int) -> Unit 150 | fn CanvasRenderingContext2D::put_image_data_with_dirty(Self, ImageData, Int, Int, Int, Int, Int, Int) -> Unit 151 | fn CanvasRenderingContext2D::quardratic_curve_to(Self, Double, Double, Double, Double) -> Unit 152 | fn CanvasRenderingContext2D::rect(Self, Double, Double, Double, Double) -> Unit 153 | fn CanvasRenderingContext2D::reset(Self) -> Unit 154 | fn CanvasRenderingContext2D::reset_transform(Self) -> Unit 155 | fn CanvasRenderingContext2D::restore(Self) -> Unit 156 | fn CanvasRenderingContext2D::rotate(Self, Double) -> Unit 157 | fn CanvasRenderingContext2D::round_rect(Self, Double, Double, Double, Double, Double) -> Unit 158 | fn CanvasRenderingContext2D::save(Self) -> Unit 159 | fn CanvasRenderingContext2D::scale(Self, Double, Double) -> Unit 160 | fn CanvasRenderingContext2D::set_fill_style(Self, @js.Union3[String, CanvasGradient, CanvasPattern]) -> Unit 161 | fn CanvasRenderingContext2D::set_image_smoothing_quality(Self, String) -> Unit 162 | fn CanvasRenderingContext2D::set_line_cap(Self, CanvasLineCap) -> Unit 163 | fn CanvasRenderingContext2D::set_line_join(Self, CanvasLineJoin) -> Unit 164 | fn CanvasRenderingContext2D::set_line_width(Self, Double) -> Unit 165 | fn CanvasRenderingContext2D::set_stroke_style(Self, @js.Union3[String, CanvasGradient, CanvasPattern]) -> Unit 166 | fn CanvasRenderingContext2D::set_transform(Self, Double, Double, Double, Double, Double, Double) -> Unit 167 | fn CanvasRenderingContext2D::stroke(Self) -> Unit 168 | fn CanvasRenderingContext2D::stroke_rect(Self, Double, Double, Double, Double) -> Unit 169 | fn CanvasRenderingContext2D::stroke_text(Self, String, Double, Double, max_width~ : @js.Optional[Double] = ..) -> Unit 170 | fn CanvasRenderingContext2D::stroke_with_path(Self, Path2D) -> Unit 171 | fn CanvasRenderingContext2D::transform(Self, Double, Double, Double, Double, Double, Double) -> Unit 172 | fn CanvasRenderingContext2D::translate(Self, Double, Double) -> Unit 173 | impl @js.Cast for CanvasRenderingContext2D 174 | 175 | pub extern type Clipboard 176 | fn Clipboard::read_text(Self) -> @js.Promise 177 | fn Clipboard::write_text(Self, String) -> @js.Promise 178 | 179 | type ClipboardEvent 180 | fn ClipboardEvent::clipboard_data(Self) -> DataTransfer 181 | 182 | type DOMMatrix2DInit 183 | 184 | type DOMRect 185 | fn DOMRect::get_bottom(Self) -> Double 186 | fn DOMRect::get_height(Self) -> Double 187 | fn DOMRect::get_left(Self) -> Double 188 | fn DOMRect::get_right(Self) -> Double 189 | fn DOMRect::get_top(Self) -> Double 190 | fn DOMRect::get_width(Self) -> Double 191 | fn DOMRect::get_x(Self) -> Double 192 | fn DOMRect::get_y(Self) -> Double 193 | 194 | type DataTransfer 195 | fn DataTransfer::drop_effect(Self) -> String 196 | fn DataTransfer::effect_allowed(Self) -> String 197 | fn DataTransfer::items(Self) -> DataTransferItemList 198 | fn DataTransfer::set_drog_image(Self, Element, Int, Int) -> Unit 199 | 200 | type DataTransferItemList 201 | fn DataTransferItemList::add(Self, String, String) -> Unit 202 | fn DataTransferItemList::clear(Self) -> Unit 203 | fn DataTransferItemList::length(Self) -> Int 204 | fn DataTransferItemList::op_get(Self, Int) -> Unit 205 | fn DataTransferItemList::remove(Self, Int) -> Unit 206 | 207 | type Document 208 | fn Document::create_document_fragment(Self) -> DocumentFragment 209 | fn Document::create_element(Self, String) -> Element 210 | fn Document::create_text_node(Self, String) -> Element 211 | fn Document::get_element_by_id(Self, String) -> @js.Nullable[Element] 212 | fn Document::to_node(Self) -> Node 213 | 214 | type DocumentFragment 215 | fn DocumentFragment::to_node(Self) -> Node 216 | 217 | type Element 218 | fn Element::append_children(Self, Self) -> Unit 219 | fn Element::children(Self) -> Array[Self] 220 | fn Element::get_attribute(Self, String) -> String 221 | fn Element::get_bounding_client_rect(Self) -> DOMRect 222 | fn Element::get_property(Self, String) -> String 223 | fn Element::remove_attribute(Self, String) -> Unit 224 | fn Element::remove_children(Self, Self) -> Unit 225 | fn Element::remove_property(Self, String) -> Unit 226 | fn Element::scroll_into_view(Self) -> Unit 227 | fn Element::set_attribute(Self, String, String) -> Unit 228 | fn Element::set_inner_html(Self, String) -> Unit 229 | fn Element::set_property(Self, String, @js.Value) -> Unit 230 | fn Element::to_html_element(Self) -> @js.Nullable[HTMLElement] 231 | fn Element::to_node(Self) -> Node 232 | 233 | type Event 234 | fn Event::prevent_default(Self) -> Unit 235 | fn Event::stop_propagation(Self) -> Unit 236 | fn Event::target(Self) -> EventTarget 237 | fn Event::to_clipboard_event(Self) -> @js.Nullable[KeyboardEvent] 238 | fn Event::to_ui_event(Self) -> @js.Nullable[UIEvent] 239 | 240 | type EventTarget 241 | fn EventTarget::add_event_listener(Self, String, (Event) -> Unit) -> Unit 242 | fn EventTarget::dispatch_event(Self, Event) -> Unit 243 | fn EventTarget::remove_event_listener(Self, String, (Event) -> Unit) -> Unit 244 | fn EventTarget::to_clipboard(Self) -> @js.Nullable[Clipboard] 245 | fn EventTarget::to_node(Self) -> @js.Nullable[Node] 246 | 247 | type FocusEvent 248 | fn FocusEvent::related_target(Self) -> EventTarget 249 | 250 | type GPUCanvasContext 251 | 252 | type HTMLCanvasElement 253 | fn HTMLCanvasElement::get_context(Self, String) -> @js.Union5[CanvasRenderingContext2D, ImageBitmapRenderingContext, WebGLRenderingContext, WebGL2RenderingContext, GPUCanvasContext] 254 | fn HTMLCanvasElement::get_height(Self) -> Int 255 | fn HTMLCanvasElement::get_width(Self) -> Int 256 | fn HTMLCanvasElement::to_html_element(Self) -> HTMLElement 257 | 258 | type HTMLDialogElement 259 | fn HTMLDialogElement::close(Self, return_value~ : @js.Optional[String] = ..) -> Unit 260 | fn HTMLDialogElement::open(Self) -> Bool 261 | fn HTMLDialogElement::request_close(Self, return_value~ : @js.Optional[String] = ..) -> Unit 262 | fn HTMLDialogElement::return_value(Self) -> String 263 | fn HTMLDialogElement::show(Self) -> Unit 264 | fn HTMLDialogElement::show_modal(Self) -> Unit 265 | 266 | type HTMLElement 267 | fn HTMLElement::remove_style(Self, String) -> Unit 268 | fn HTMLElement::set_style(Self, String, String) -> Unit 269 | fn HTMLElement::to_element(Self) -> Element 270 | fn HTMLElement::to_html_canvas_element(Self) -> @js.Nullable[HTMLCanvasElement] 271 | fn HTMLElement::to_html_dialog_element(Self) -> @js.Nullable[HTMLDialogElement] 272 | fn HTMLElement::to_html_input_element(Self) -> @js.Nullable[HTMLInputElement] 273 | fn HTMLElement::to_html_select_element(Self) -> @js.Nullable[HTMLSelectElement] 274 | 275 | type HTMLInputElement 276 | fn HTMLInputElement::value(Self) -> String 277 | 278 | type HTMLSelectElement 279 | fn HTMLSelectElement::value(Self) -> String 280 | 281 | type ImageBitmapRenderingContext 282 | 283 | type ImageData 284 | fn ImageData::get_color_space(Self) -> String 285 | fn ImageData::get_data(Self) -> Uint8ClampedArray 286 | fn ImageData::get_height(Self) -> Int 287 | fn ImageData::get_width(Self) -> Int 288 | fn ImageData::new(Int, Int, @js.Optional[ImageDataSettings]) -> Self 289 | fn ImageData::new_with_data(Uint8ClampedArray, Int, Int, settings~ : @js.Optional[ImageDataSettings] = ..) -> Self 290 | 291 | type ImageDataSettings 292 | fn ImageDataSettings::new(String) -> Self 293 | 294 | type InputEvent 295 | fn InputEvent::data(Self) -> String 296 | fn InputEvent::input_type(Self) -> String 297 | fn InputEvent::is_composing(Self) -> Bool 298 | 299 | type KeyboardEvent 300 | fn KeyboardEvent::alt_key(Self) -> Bool 301 | fn KeyboardEvent::code(Self) -> String 302 | fn KeyboardEvent::ctrl_key(Self) -> Bool 303 | fn KeyboardEvent::is_composing(Self) -> Bool 304 | fn KeyboardEvent::key(Self) -> String 305 | fn KeyboardEvent::location(Self) -> Int 306 | fn KeyboardEvent::meta_key(Self) -> Bool 307 | fn KeyboardEvent::repeat(Self) -> Bool 308 | fn KeyboardEvent::shift_key(Self) -> Bool 309 | 310 | type MouseEvent 311 | fn MouseEvent::alt_key(Self) -> Bool 312 | fn MouseEvent::client_x(Self) -> Int 313 | fn MouseEvent::client_y(Self) -> Int 314 | fn MouseEvent::ctrl_key(Self) -> Bool 315 | fn MouseEvent::meta_key(Self) -> Bool 316 | fn MouseEvent::offset_x(Self) -> Int 317 | fn MouseEvent::offset_y(Self) -> Int 318 | fn MouseEvent::screen_x(Self) -> Int 319 | fn MouseEvent::screen_y(Self) -> Int 320 | fn MouseEvent::shift_key(Self) -> Bool 321 | 322 | pub extern type Navigator 323 | fn Navigator::clipboard(Self) -> Clipboard 324 | 325 | type Node 326 | fn Node::append_child(Self, Self) -> Unit 327 | fn Node::count_child(Self) -> Int 328 | fn Node::first_child(Self) -> Self 329 | fn Node::insert_before(Self, Self, Self) -> Unit 330 | fn Node::last_child(Self) -> Self 331 | fn Node::next_sibling(Self) -> Self 332 | fn Node::node_name(Self) -> String 333 | fn Node::node_type(Self) -> Int 334 | fn Node::node_value(Self) -> String 335 | fn Node::nth_child(Self, Int) -> Self 336 | fn Node::parent_node(Self) -> Self 337 | fn Node::previous_sibling(Self) -> Self 338 | fn Node::remove_child(Self, Self) -> Unit 339 | fn Node::replace_child(Self, Self, Self) -> Unit 340 | fn Node::to_document(Self) -> @js.Nullable[Document] 341 | fn Node::to_document_fragment(Self) -> @js.Nullable[DocumentFragment] 342 | fn Node::to_element(Self) -> @js.Nullable[Element] 343 | fn Node::to_event_target(Self) -> EventTarget 344 | fn Node::to_text(Self) -> @js.Nullable[Text] 345 | 346 | type Path2D 347 | fn Path2D::add_path(Self, Self, transform~ : @js.Optional[DOMMatrix2DInit] = ..) -> Unit 348 | fn Path2D::new(path~ : @js.Optional[@js.Union2[Self, String]] = ..) -> Self 349 | 350 | type Text 351 | fn Text::to_node(Self) -> Node 352 | 353 | type UIEvent 354 | fn UIEvent::to_event(Self) -> Event 355 | fn UIEvent::to_focus_event(Self) -> @js.Nullable[MouseEvent] 356 | fn UIEvent::to_input_event(Self) -> @js.Nullable[MouseEvent] 357 | fn UIEvent::to_keyboard_event(Self) -> @js.Nullable[KeyboardEvent] 358 | fn UIEvent::to_mouse_event(Self) -> @js.Nullable[MouseEvent] 359 | 360 | type Uint8ClampedArray 361 | 362 | type WebGL2RenderingContext 363 | 364 | type WebGLRenderingContext 365 | 366 | type Window 367 | fn Window::alert(Self, String) -> Unit 368 | fn Window::confirm(Self, String) -> Bool 369 | fn Window::current_url(Self) -> String 370 | fn Window::history_go_back(Self) -> Unit 371 | fn Window::history_go_forward(Self) -> Unit 372 | fn Window::load_url(Self, String) -> Unit 373 | fn Window::navigator(Self) -> Navigator 374 | fn Window::push_url(Self, String) -> Unit 375 | fn Window::reload_url(Self) -> Unit 376 | fn Window::replace_url(Self, String) -> Unit 377 | fn Window::request_animination_frame(Self, () -> Unit) -> Unit 378 | fn Window::scroll_by(Self, Int, Int) -> Unit 379 | fn Window::scroll_to(Self, Int, Int) -> Unit 380 | fn Window::scroll_to_bottom(Self) -> Unit 381 | fn Window::scroll_to_top(Self) -> Unit 382 | fn Window::to_event_target(Self) -> EventTarget 383 | 384 | // Type aliases 385 | pub typealias Listener = (Event) -> Unit 386 | 387 | pub typealias PredefinedColorSpace = String 388 | 389 | // Traits 390 | 391 | -------------------------------------------------------------------------------- /src/dom/dom_cast.mbt: -------------------------------------------------------------------------------- 1 | // ------------- cast node to other types ----------------- 2 | ///| 3 | pub extern "js" fn to_document(self : Node) -> @js.Nullable[Document] = 4 | #| (x) => x.nodeType===9 ? x : null 5 | 6 | ///| 7 | pub extern "js" fn to_document_fragment( 8 | self : Node 9 | ) -> @js.Nullable[DocumentFragment] = 10 | #| (x) => x.nodeType===11 ? x : null 11 | 12 | ///| 13 | pub extern "js" fn to_element(self : Node) -> @js.Nullable[Element] = 14 | #| (x) => x.nodeType===1 ? x : null 15 | 16 | ///| 17 | pub extern "js" fn to_text(self : Node) -> @js.Nullable[Text] = 18 | #| (x) => x.nodeType===3 ? x : null 19 | 20 | ///| 21 | pub extern "js" fn to_event_target(self : Node) -> EventTarget = "(x) => x" 22 | 23 | // -------------- convert types back to node ----------------- 24 | ///| 25 | pub extern "js" fn Document::to_node(self : Document) -> Node = "(x) => x" 26 | 27 | ///| 28 | pub extern "js" fn DocumentFragment::to_node(self : DocumentFragment) -> Node = "(x) => x" 29 | 30 | ///| 31 | pub extern "js" fn Element::to_node(self : Element) -> Node = "(x) => x" 32 | 33 | ///| 34 | pub extern "js" fn Text::to_node(self : Text) -> Node = "(x) => x" 35 | -------------------------------------------------------------------------------- /src/dom/dom_document.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | extern type Document 3 | 4 | ///| 5 | pub extern "js" fn document() -> Document = "() => document" 6 | 7 | ///| 8 | pub extern "js" fn get_element_by_id( 9 | self : Document, 10 | id : String 11 | ) -> @js.Nullable[Element] = "(doc,id) => doc.getElementById(id)" 12 | 13 | ///| 14 | pub extern "js" fn create_element(self : Document, tag : String) -> Element = "(doc,tag) => doc.createElement(tag)" 15 | 16 | ///| 17 | pub extern "js" fn create_text_node(self : Document, str : String) -> Element = "(doc,str) => doc.createTextNode(str)" 18 | 19 | ///| 20 | pub extern "js" fn create_document_fragment( 21 | self : Document 22 | ) -> DocumentFragment = "(doc) => doc.createDocumentFragment()" 23 | -------------------------------------------------------------------------------- /src/dom/dom_element.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | extern type Element 3 | 4 | ///| 5 | pub extern "js" fn Element::to_html_element( 6 | self : Element 7 | ) -> @js.Nullable[HTMLElement] = 8 | #| (x) => x instanceof HTMLElement ? x : null 9 | 10 | ///| 11 | pub extern "js" fn Element::children(self : Element) -> Array[Element] = 12 | #| (self) => self.children 13 | 14 | ///| 15 | pub extern "js" fn Element::append_children( 16 | self : Element, 17 | child : Element 18 | ) -> Unit = "(self, child) => self.append(child)" 19 | 20 | ///| 21 | pub extern "js" fn Element::remove_children( 22 | self : Element, 23 | child : Element 24 | ) -> Unit = " (self,child) => self.remove(child) " 25 | 26 | // NOTE: The DOM tree also includes the TEXT_NODE, which is not an element. 27 | // Therefore, a vdom patch algorithm should not be based on the ffi above. 28 | // https://dom.spec.whatwg.org/#ref-for-dom-node-nodetype%E2%91%A0 29 | 30 | ///| 31 | pub extern "js" fn Element::set_attribute( 32 | self : Element, 33 | attr : String, 34 | value : String 35 | ) -> Unit = "(self,attr,value) => self.setAttribute(attr, value)" 36 | 37 | ///| 38 | pub extern "js" fn Element::get_attribute( 39 | self : Element, 40 | attr : String 41 | ) -> String = "(self,attr) => self.getAttribute(attr)" 42 | 43 | ///| 44 | pub extern "js" fn Element::remove_attribute( 45 | self : Element, 46 | attr : String 47 | ) -> Unit = "(self,attr) => self.removeAttribute(attr)" 48 | 49 | ///| 50 | pub extern "js" fn Element::set_property( 51 | self : Element, 52 | prop : String, 53 | value : @js.Value 54 | ) = "(self,prop,value) => self[prop] = value" 55 | 56 | ///| 57 | pub extern "js" fn Element::remove_property( 58 | self : Element, 59 | prop : String 60 | ) -> Unit = "(self,prop) => delete self[prop]" 61 | 62 | ///| 63 | pub extern "js" fn Element::get_property( 64 | self : Element, 65 | prop : String 66 | ) -> String = "(self,prop) => self[prop]" 67 | 68 | ///| 69 | pub extern "js" fn Element::scroll_into_view(self : Element) -> Unit = 70 | #| (self) => self.scrollIntoView() 71 | 72 | ///| 73 | pub extern "js" fn Element::set_inner_html( 74 | self : Element, 75 | html : String 76 | ) -> Unit = "(self,html) => self.innerHTML = html" 77 | 78 | ///| 79 | pub extern "js" fn Element::get_bounding_client_rect(self : Element) -> DOMRect = 80 | #| (self) => self.getBoundingClientRect() 81 | 82 | ///| 83 | extern type HTMLElement 84 | 85 | ///| 86 | pub extern "js" fn HTMLElement::to_element(self : HTMLElement) -> Element = "(x) => x" 87 | 88 | ///| 89 | pub extern "js" fn HTMLElement::to_html_canvas_element( 90 | self : HTMLElement 91 | ) -> @js.Nullable[HTMLCanvasElement] = 92 | #| (x) => x instanceof HTMLCanvasElement ? x : null 93 | 94 | ///| 95 | pub extern "js" fn HTMLElement::to_html_input_element( 96 | self : HTMLElement 97 | ) -> @js.Nullable[HTMLInputElement] = 98 | #| (x) => x instanceof HTMLInputElement ? x : null 99 | 100 | ///| 101 | pub extern "js" fn HTMLElement::to_html_select_element( 102 | self : HTMLElement 103 | ) -> @js.Nullable[HTMLSelectElement] = 104 | #| (x) => x instanceof HTMLSelectElement ? x : null 105 | 106 | ///| 107 | pub extern "js" fn HTMLElement::to_html_dialog_element( 108 | self : HTMLElement 109 | ) -> @js.Nullable[HTMLDialogElement] = 110 | #| (x) => x instanceof HTMLDialogElement ? x : null 111 | 112 | ///| 113 | pub extern "js" fn HTMLElement::set_style( 114 | self : HTMLElement, 115 | key : String, 116 | value : String 117 | ) -> Unit = 118 | #| (self,key,value) => self.style[key] = value 119 | 120 | ///| 121 | pub extern "js" fn HTMLElement::remove_style(self : HTMLElement, key : String) = 122 | #| (self,key) => self.style[key] = '' 123 | 124 | ///| 125 | extern type HTMLInputElement 126 | 127 | ///| 128 | pub extern "js" fn HTMLInputElement::value(self : HTMLInputElement) -> String = "(self) => self.value" 129 | 130 | ///| 131 | extern type HTMLSelectElement 132 | 133 | ///| 134 | pub extern "js" fn HTMLSelectElement::value(self : HTMLSelectElement) -> String = "(self) => self.value" 135 | 136 | ///| 137 | extern type HTMLDialogElement 138 | 139 | ///| 140 | pub extern "js" fn HTMLDialogElement::open(self : HTMLDialogElement) -> Bool = "(self) => self.open" 141 | 142 | ///| 143 | pub extern "js" fn HTMLDialogElement::return_value( 144 | self : HTMLDialogElement 145 | ) -> String = "(self) => self.returnValue" 146 | 147 | ///| 148 | pub extern "js" fn HTMLDialogElement::close( 149 | self : HTMLDialogElement, 150 | return_value~ : @js.Optional[String] = @js.Optional::undefined() 151 | ) = "(self,returnValue) => self.close(returnValue)" 152 | 153 | ///| 154 | pub extern "js" fn HTMLDialogElement::request_close( 155 | self : HTMLDialogElement, 156 | return_value~ : @js.Optional[String] = @js.Optional::undefined() 157 | ) = "(self, returnValue) => self.requestClose(returnValue)" 158 | 159 | ///| 160 | pub extern "js" fn HTMLDialogElement::show(self : HTMLDialogElement) = "(self) => self.show()" 161 | 162 | ///| 163 | pub extern "js" fn HTMLDialogElement::show_modal(self : HTMLDialogElement) = "(self) => self.showModal()" 164 | -------------------------------------------------------------------------------- /src/dom/dom_event.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// 3 | /// # Hierarchy of DOM Events 4 | /// 5 | /// ``` 6 | /// Event <-+-- ClipboardEvent 7 | /// +-- UIEvent <--+-- MouseEvent 8 | /// +-- InputEvent 9 | /// +-- FocusEvent 10 | /// +-- KeyboardEvent 11 | /// ``` 12 | extern type Event 13 | 14 | ///| 15 | pub extern "js" fn Event::target(self : Event) -> EventTarget = "(self) => self.target" 16 | 17 | ///| 18 | pub extern "js" fn Event::to_ui_event(self : Event) -> @js.Nullable[UIEvent] = 19 | #| (x) => x instanceof UIEvent ? x : null 20 | 21 | ///| 22 | pub extern "js" fn Event::to_clipboard_event( 23 | self : Event 24 | ) -> @js.Nullable[KeyboardEvent] = 25 | #| (x) => x instanceof ClipboardEvent ? x : null 26 | 27 | ///| 28 | pub extern "js" fn Event::prevent_default(self : Event) = "(self) => self.preventDefault()" 29 | 30 | ///| 31 | pub extern "js" fn Event::stop_propagation(self : Event) = "(self) => self.stopPropagation()" 32 | 33 | ///| 34 | extern type UIEvent 35 | 36 | ///| 37 | pub extern "js" fn UIEvent::to_event(self : UIEvent) -> Event = "(x) => x" 38 | 39 | ///| 40 | pub extern "js" fn UIEvent::to_mouse_event( 41 | self : UIEvent 42 | ) -> @js.Nullable[MouseEvent] = 43 | #| (e) => e instanceof MouseEvent ? e : null 44 | 45 | ///| 46 | pub extern "js" fn UIEvent::to_input_event( 47 | self : UIEvent 48 | ) -> @js.Nullable[MouseEvent] = 49 | #| (e) => e instanceof InputEvent ? e : null 50 | 51 | ///| 52 | pub extern "js" fn UIEvent::to_focus_event( 53 | self : UIEvent 54 | ) -> @js.Nullable[MouseEvent] = 55 | #| (e) => e instanceof FocusEvent ? e : null 56 | 57 | ///| 58 | pub extern "js" fn UIEvent::to_keyboard_event( 59 | self : UIEvent 60 | ) -> @js.Nullable[KeyboardEvent] = 61 | #| (e) => e instanceof KeyboardEvent ? e : null 62 | 63 | ///| 64 | extern type MouseEvent 65 | 66 | ///| 67 | pub extern "js" fn MouseEvent::screen_x(self : MouseEvent) -> Int = "(e) => e.screenX" 68 | 69 | ///| 70 | pub extern "js" fn MouseEvent::screen_y(self : MouseEvent) -> Int = "(e) => e.screenY" 71 | 72 | ///| 73 | pub extern "js" fn MouseEvent::client_x(self : MouseEvent) -> Int = "(e) => e.clientX" 74 | 75 | ///| 76 | pub extern "js" fn MouseEvent::client_y(self : MouseEvent) -> Int = "(e) => e.clientY" 77 | 78 | ///| 79 | pub extern "js" fn MouseEvent::offset_x(self : MouseEvent) -> Int = "(e) => e.offsetX" 80 | 81 | ///| 82 | pub extern "js" fn MouseEvent::offset_y(self : MouseEvent) -> Int = "(e) => e.offsetY" 83 | 84 | ///| 85 | pub extern "js" fn MouseEvent::ctrl_key(self : MouseEvent) -> Bool = "(e) => e.ctrlKey" 86 | 87 | ///| 88 | pub extern "js" fn MouseEvent::shift_key(self : MouseEvent) -> Bool = "(e) => e.shiftKey" 89 | 90 | ///| 91 | pub extern "js" fn MouseEvent::alt_key(self : MouseEvent) -> Bool = "(e) => e.altKey" 92 | 93 | ///| 94 | pub extern "js" fn MouseEvent::meta_key(self : MouseEvent) -> Bool = "(e) => e.metaKey" 95 | 96 | ///| 97 | extern type InputEvent 98 | 99 | ///| 100 | pub extern "js" fn InputEvent::data(self : InputEvent) -> String = "(e) => e.data" 101 | 102 | ///| 103 | pub extern "js" fn InputEvent::is_composing(self : InputEvent) -> Bool = "(e) => e.isComposing" 104 | 105 | ///| 106 | pub extern "js" fn InputEvent::input_type(self : InputEvent) -> String = "(e) => e.inputType" 107 | 108 | ///| 109 | extern type FocusEvent 110 | 111 | ///| 112 | pub extern "js" fn FocusEvent::related_target(self : FocusEvent) -> EventTarget = "(e) => e.relatedTarget" 113 | 114 | ///| 115 | extern type KeyboardEvent 116 | 117 | ///| 118 | pub extern "js" fn KeyboardEvent::key(self : KeyboardEvent) -> String = "(e) => e.key" 119 | 120 | ///| 121 | pub extern "js" fn KeyboardEvent::code(self : KeyboardEvent) -> String = "(e) => e.code" 122 | 123 | ///| 124 | pub extern "js" fn KeyboardEvent::alt_key(self : KeyboardEvent) -> Bool = "(e) => e.altKey" 125 | 126 | ///| 127 | pub extern "js" fn KeyboardEvent::ctrl_key(self : KeyboardEvent) -> Bool = "(e) => e.ctrlKey" 128 | 129 | ///| 130 | pub extern "js" fn KeyboardEvent::shift_key(self : KeyboardEvent) -> Bool = "(e) => e.shiftKey" 131 | 132 | ///| 133 | pub extern "js" fn KeyboardEvent::meta_key(self : KeyboardEvent) -> Bool = "(e) => e.metaKey" 134 | 135 | ///| 136 | pub extern "js" fn KeyboardEvent::is_composing(self : KeyboardEvent) -> Bool = "(e) => e.isComposing" 137 | 138 | ///| 139 | pub extern "js" fn KeyboardEvent::repeat(self : KeyboardEvent) -> Bool = "(e) => e.repeat" 140 | 141 | ///| 142 | pub extern "js" fn KeyboardEvent::location(self : KeyboardEvent) -> Int = "(e) => e.location" 143 | 144 | ///| 145 | pub const DOM_KEY_LOCATION_STANDARD : Int = 0x00 146 | 147 | ///| 148 | pub const DOM_KEY_LOCATION_LEFT : Int = 0x01 149 | 150 | ///| 151 | pub const DOM_KEY_LOCATION_RIGHT : Int = 0x02 152 | 153 | ///| 154 | pub const DOM_KEY_LOCATION_NUMPAD : Int = 0x03 155 | 156 | ///| 157 | extern type ClipboardEvent 158 | 159 | ///| 160 | pub extern "js" fn ClipboardEvent::clipboard_data( 161 | self : ClipboardEvent 162 | ) -> DataTransfer = "(e) => e.clipboardData" 163 | -------------------------------------------------------------------------------- /src/dom/dom_event_target.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// # Hireharchy related to the DOM EventTarget 3 | /// 4 | /// ``` 5 | /// EventTarget <---+--Clipboard 6 | /// +--Node <--+-- Document 7 | /// +-- DocumentFragment 8 | /// +-- Element <-- HTMLElement <--+-- HTMLCanvasElement 9 | /// +-- HTMLInputElement 10 | /// +-- HTMLDialogElement 11 | /// ``` 12 | extern type EventTarget 13 | 14 | ///| 15 | pub extern "js" fn to_node(self : EventTarget) -> @js.Nullable[Node] = 16 | #| (x) => x instanceof Node ? x : null 17 | 18 | ///| 19 | pub extern "js" fn to_clipboard(self : EventTarget) -> @js.Nullable[Clipboard] = 20 | #| (x) => x instanceof Clipboard ? x : null 21 | 22 | ///| 23 | pub extern "js" fn add_event_listener( 24 | self : EventTarget, 25 | type_ : String, 26 | callback : Listener 27 | ) -> Unit = 28 | #| (target, type, listener) => target.addEventListener(type, listener) 29 | 30 | ///| 31 | pub extern "js" fn remove_event_listener( 32 | self : EventTarget, 33 | type_ : String, 34 | callback : Listener 35 | ) -> Unit = 36 | #| (target, type, listener) => target.removeEventListener(type, listener) 37 | 38 | ///| 39 | pub extern "js" fn dispatch_event(self : EventTarget, event : Event) -> Unit = 40 | #| (target, event) => target.dispatchEvent(event) 41 | 42 | ///| 43 | pub typealias Listener = (Event) -> Unit 44 | -------------------------------------------------------------------------------- /src/dom/dom_fragment.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | extern type DocumentFragment 3 | -------------------------------------------------------------------------------- /src/dom/dom_node.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | extern type Node 3 | 4 | // ---------- node API -------------- 5 | ///| 6 | pub extern "js" fn node_type(self : Node) -> Int = "(x) => x.nodeType" 7 | 8 | ///| 9 | pub extern "js" fn node_name(self : Node) -> String = "(x) => x.nodeName" 10 | 11 | ///| 12 | pub extern "js" fn node_value(self : Node) -> String = "(x) => x.nodeValue" 13 | 14 | ///| 15 | pub extern "js" fn first_child(self : Node) -> Node = "(x) => x.firstChild" 16 | 17 | ///| 18 | pub extern "js" fn last_child(self : Node) -> Node = "(x) => x.lastChild" 19 | 20 | ///| 21 | pub extern "js" fn next_sibling(self : Node) -> Node = "(x) => x.nextSibling" 22 | 23 | ///| 24 | pub extern "js" fn previous_sibling(self : Node) -> Node = "(x) => x.previousSibling" 25 | 26 | ///| 27 | pub extern "js" fn parent_node(self : Node) -> Node = "(x) => x.parentNode" 28 | 29 | ///| 30 | pub extern "js" fn append_child(self : Node, child : Node) = "(p,c) => p.appendChild(c)" 31 | 32 | ///| 33 | pub extern "js" fn remove_child(self : Node, child : Node) = "(p,c) => p.removeChild(c)" 34 | 35 | ///| 36 | pub extern "js" fn replace_child(self : Node, new : Node, old : Node) = "(p,n,o) => p.replaceChild(n,o)" 37 | 38 | ///| 39 | pub extern "js" fn insert_before(self : Node, value : Node, before : Node) = "(p,value,before) => p.insertBefore(value,before)" 40 | 41 | // ---------- some specific node API -------------- 42 | // Note: the childNodes property is a NodeList, not an array, so we can't use the Array type. 43 | ///| 44 | pub extern "js" fn nth_child(self : Node, index : Int) -> Node = 45 | #| (x,i) => { 46 | #| const r = x.childNodes[i]; 47 | #| if (r === undefined) throw new Error(`nth_child: index ${i} out of bounds, length=${x.childNodes.length}`); 48 | #| return r; 49 | #| } 50 | 51 | ///| 52 | pub extern "js" fn count_child(self : Node) -> Int = "(x) => x.childNodes.length" 53 | -------------------------------------------------------------------------------- /src/dom/dom_rect.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | extern type DOMRect 3 | 4 | ///| 5 | pub extern "js" fn DOMRect::get_x(self : DOMRect) -> Double = "(self) => self.x" 6 | 7 | ///| 8 | pub extern "js" fn DOMRect::get_y(self : DOMRect) -> Double = "(self) => self.y" 9 | 10 | ///| 11 | pub extern "js" fn DOMRect::get_width(self : DOMRect) -> Double = "(self) => self.width" 12 | 13 | ///| 14 | pub extern "js" fn DOMRect::get_height(self : DOMRect) -> Double = "(self) => self.height" 15 | 16 | ///| 17 | pub extern "js" fn DOMRect::get_top(self : DOMRect) -> Double = "(self) => self.top" 18 | 19 | ///| 20 | pub extern "js" fn DOMRect::get_right(self : DOMRect) -> Double = "(self) => self.right" 21 | 22 | ///| 23 | pub extern "js" fn DOMRect::get_bottom(self : DOMRect) -> Double = "(self) => self.bottom" 24 | 25 | ///| 26 | pub extern "js" fn DOMRect::get_left(self : DOMRect) -> Double = "(self) => self.left" 27 | -------------------------------------------------------------------------------- /src/dom/dom_text.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | extern type Text 3 | -------------------------------------------------------------------------------- /src/dom/dom_window.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | extern type Window 3 | 4 | ///| 5 | pub extern "js" fn scroll_to(self : Window, x : Int, y : Int) = 6 | #| (self, x, y) => { self.scrollTo(x, y); } 7 | 8 | ///| 9 | pub extern "js" fn scroll_by(self : Window, x : Int, y : Int) = 10 | #| (self, x, y) => { self.scrollBy(x, y); } 11 | 12 | ///| 13 | pub extern "js" fn scroll_to_top(self : Window) = 14 | #| (self) => { self.scrollTo(0, 0); } 15 | 16 | ///| 17 | pub extern "js" fn scroll_to_bottom(self : Window) = 18 | #| (self) => { self.scrollTo(0, document.body.scrollHeight); } 19 | 20 | ///| 21 | pub extern "js" fn history_go_back(self : Window) = 22 | #| (self) => { self.history.back(); } 23 | 24 | ///| 25 | pub extern "js" fn history_go_forward(self : Window) = 26 | #| (self) => { self.history.forward(); } 27 | 28 | ///| 29 | pub extern "js" fn load_url(self : Window, url : String) = 30 | #| (self,url) => { self.location.href = url; } 31 | 32 | ///| 33 | pub extern "js" fn reload_url(self : Window) = 34 | #| (self) => { self.location.reload(); } 35 | 36 | ///| 37 | pub extern "js" fn push_url(self : Window, url : String) = 38 | #| (self,url) => { self.history.pushState(null, '', url); } 39 | 40 | ///| 41 | pub extern "js" fn current_url(self : Window) -> String = 42 | #| (self) => { return self.location.href; } 43 | 44 | ///| 45 | pub extern "js" fn replace_url(self : Window, url : String) = 46 | #| (self,url) => { self.history.replaceState(null, '', url); } 47 | 48 | ///| 49 | pub extern "js" fn request_animination_frame(self : Window, f : () -> Unit) = "(self,f) => { self.requestAnimationFrame(f); }" 50 | 51 | ///| 52 | pub extern "js" fn window() -> Window = "() => window" 53 | 54 | ///| 55 | pub extern "js" fn Window::to_event_target(self : Window) -> EventTarget = "(x) => x" 56 | 57 | ///| 58 | pub extern "js" fn Window::alert(self : Window, msg : String) = "(self,msg) => self.alert(msg)" 59 | 60 | ///| 61 | pub extern "js" fn Window::confirm(self : Window, msg : String) -> Bool = "(self,msg) => self.confirm(msg)" 62 | 63 | ///| 64 | pub extern "js" fn Window::navigator(self : Window) -> Navigator = 65 | #| (self) => self.navigator 66 | -------------------------------------------------------------------------------- /src/dom/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "rami3l/js-ffi/js" 4 | ] 5 | } -------------------------------------------------------------------------------- /src/dom/navigator.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | pub extern type Navigator 3 | 4 | ///| 5 | pub extern "js" fn Navigator::clipboard(self : Navigator) -> Clipboard = 6 | #| (self) => self.clipboard 7 | -------------------------------------------------------------------------------- /src/example/async/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .mooncakes 3 | target 4 | main.js 5 | main.js.map 6 | -------------------------------------------------------------------------------- /src/example/async/README.md: -------------------------------------------------------------------------------- 1 | # Async Example 2 | 3 | This example demonstrates how to manually wrap an async JS FFI in Moonbit. 4 | 5 | It's particularly useful when the functionality is not 6 | yet available in the rabbit-tea library. Ideally, the Core library should 7 | provide basic async utilities, but in this early stage, we need to implement 8 | the wrapping ourselves. 9 | 10 | If you are looking for HTTP request functionality, you can use the `rabbit-tea/http` package. 11 | 12 | Run the example with the following command: 13 | 14 | ```bash 15 | npm i 16 | npm run dev 17 | ``` 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/example/async/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | async 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/example/async/main/async.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/example/async" 2 | 3 | // Values 4 | fn suspend[T](((T) -> Unit) -> Unit) -> T 5 | 6 | // Types and methods 7 | type Message 8 | 9 | type Model 10 | 11 | // Type aliases 12 | 13 | // Traits 14 | 15 | -------------------------------------------------------------------------------- /src/example/async/main/main.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | fnalias @html.(text, button, div) 3 | 4 | ///| 5 | enum Message { 6 | TimeUp(String) 7 | Clear 8 | SetTime 9 | } 10 | 11 | ///| 12 | struct Model { 13 | text : String 14 | } 15 | 16 | ///| 17 | pub async fn suspend[T](f : ((T) -> Unit) -> Unit) -> T = "%async.suspend" 18 | 19 | ///| 20 | extern "js" fn set_timeout(f : () -> Unit, ms : Int) = "(f,ms) => setTimeout(f, ms)" 21 | 22 | ///| 23 | fn update(msg : Message, model : Model) -> (@tea.Cmd[Message], Model) { 24 | match msg { 25 | TimeUp(text) => (@tea.none(), { text: "TimeUp \{text}" }) 26 | Clear => (@tea.none(), { text: "" }) 27 | SetTime => { 28 | let f = async fn() { 29 | suspend!(fn(resolve) { set_timeout(fn() { resolve(()) }, 5000) }) 30 | "5000" 31 | } 32 | (@tea.perform(TimeUp(_), f), model) 33 | } 34 | } 35 | } 36 | 37 | ///| 38 | fn view(model : Model) -> @html.Html[Message] { 39 | div([ 40 | button(click=Clear, [text("Clear")]), 41 | button(click=SetTime, [text("show text after 5s")]), 42 | text(model.text), 43 | ]) 44 | } 45 | 46 | ///| NOTE: This program is only available in the js backend, 47 | /// see README.md to getting started. 48 | fn main { 49 | let model = { text: "" } 50 | @tea.startup(model~, update~, view~) 51 | } 52 | -------------------------------------------------------------------------------- /src/example/async/main/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "is-main": true, 3 | "import": [ 4 | { 5 | "path": "Yoorkin/rabbit-tea", 6 | "alias": "tea" 7 | }, 8 | { 9 | "path":"Yoorkin/rabbit-tea/html", 10 | "alias": "html" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /src/example/async/moon.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Yoorkin/example-todoMVC", 3 | "version": "0.0.1", 4 | "deps": { 5 | "Yoorkin/rabbit-tea": { 6 | "path": "../../../." 7 | } 8 | }, 9 | "readme": "README.md", 10 | "license": "Apache-2.0" 11 | } -------------------------------------------------------------------------------- /src/example/async/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "body-parser": "^1.20.3", 4 | "express": "^4.21.2", 5 | "@tailwindcss/vite": "^4.0.17", 6 | "rabbit-tea-vite": "^1.0.0", 7 | "vite": "^5.4.18" 8 | }, 9 | "scripts": { 10 | "dev": "vite", 11 | "build": "vite build", 12 | "preview": "vite preview" 13 | }, 14 | "type": "module" 15 | } 16 | -------------------------------------------------------------------------------- /src/example/async/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import rabbitTEA from 'rabbit-tea-vite' 3 | import tailwindcss from '@tailwindcss/vite' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | rabbitTEA(), 8 | tailwindcss() 9 | ] 10 | }) 11 | 12 | -------------------------------------------------------------------------------- /src/example/canvas/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .mooncakes 3 | target 4 | main.js 5 | main.js.map 6 | -------------------------------------------------------------------------------- /src/example/canvas/README.md: -------------------------------------------------------------------------------- 1 | # Canvas Example 2 | 3 | Run the example with the following command: 4 | 5 | 6 | ```bash 7 | npm i 8 | npm run dev 9 | ``` 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/example/canvas/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | canvas 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/example/canvas/main/canvas.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/example/canvas" 2 | 3 | // Values 4 | 5 | // Types and methods 6 | type Drawing 7 | 8 | type Message 9 | 10 | type Model 11 | 12 | // Type aliases 13 | 14 | // Traits 15 | 16 | -------------------------------------------------------------------------------- /src/example/canvas/main/main.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | fnalias @html.(div, text, button) 3 | 4 | ///| 5 | enum Message { 6 | Clear 7 | DrawPoint(@html.Mouse) 8 | MouseDown(@html.Mouse) 9 | MouseUp 10 | ChooseColor(@canvas.Color) 11 | } 12 | 13 | ///| 14 | struct Model { 15 | drawing : Drawing 16 | color : @canvas.Color 17 | canvas : @canvas.Model[Message] 18 | } 19 | 20 | ///| 21 | enum Drawing { 22 | Disabled 23 | Enabled(@html.Pos) 24 | } 25 | 26 | ///| 27 | fn update(msg : Message, model : Model) -> (@tea.Cmd[Message], Model) { 28 | match msg { 29 | Clear => { 30 | model.canvas.context2d().clear_rect(0.0, 0.0, 500.0, 500.0) 31 | (@tea.none(), model) 32 | } 33 | ChooseColor(color) => { 34 | model.canvas.context2d().set_stroke_style(color) 35 | (@tea.none(), { ..model, color, }) 36 | } 37 | MouseDown(mouse) => 38 | (@tea.none(), { ..model, drawing: Enabled(mouse.offset_pos()) }) 39 | MouseUp(_) => (@tea.none(), { ..model, drawing: Disabled }) 40 | DrawPoint(mouse) => 41 | match model.drawing { 42 | Enabled({ x: lx, y: ly }) => { 43 | model.canvas 44 | .context2d() 45 | ..begin_path() 46 | ..move_to(lx.to_double(), ly.to_double()) 47 | ..line_to( 48 | mouse.offset_pos().x.to_double(), 49 | mouse.offset_pos().y.to_double(), 50 | ) 51 | .stroke() 52 | (@tea.none(), { ..model, drawing: Enabled(mouse.offset_pos()) }) 53 | } 54 | Disabled => (@tea.none(), model) 55 | } 56 | } 57 | } 58 | 59 | ///| 60 | fn view(model : Model) -> @html.Html[Message] { 61 | div(style=["border: solid 1px gray; width: 500px; height: 500px"], [ 62 | model.canvas.to_html(), 63 | div(style=["width:30px; height:30px", "background: \{model.color}"], []), 64 | button(click=Clear, [text("Clear")]), 65 | button(click=ChooseColor(RGB(255, 0, 0)), [text("Red")]), 66 | button(click=ChooseColor(RGB(0, 255, 0)), [text("Green")]), 67 | button(click=ChooseColor(RGB(0, 0, 255)), [text("Blue")]), 68 | ]) 69 | } 70 | 71 | ///| NOTE: This program is only available in the js backend, 72 | /// see README.md to getting started. 73 | fn main { 74 | let model = { 75 | drawing: Disabled, 76 | color: RGB(0, 0, 0), 77 | canvas: @canvas.new( 78 | width=500, 79 | height=500, 80 | mousemove=DrawPoint(_), 81 | mousedown=MouseDown(_), 82 | mouseup=fn(_) { Message::MouseUp }, 83 | ), 84 | } 85 | @tea.startup(model~, update~, view~) 86 | } 87 | -------------------------------------------------------------------------------- /src/example/canvas/main/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "is-main": true, 3 | "import": [ 4 | { 5 | "path": "Yoorkin/rabbit-tea", 6 | "alias": "tea" 7 | }, 8 | { 9 | "path":"Yoorkin/rabbit-tea/html", 10 | "alias": "html" 11 | }, 12 | "Yoorkin/rabbit-tea/html/canvas" 13 | ] 14 | } -------------------------------------------------------------------------------- /src/example/canvas/moon.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Yoorkin/example-todoMVC", 3 | "version": "0.0.1", 4 | "deps": { 5 | "Yoorkin/rabbit-tea": { 6 | "path": "../../../." 7 | } 8 | }, 9 | "readme": "README.md", 10 | "license": "Apache-2.0" 11 | } -------------------------------------------------------------------------------- /src/example/canvas/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "body-parser": "^1.20.3", 4 | "express": "^4.21.2", 5 | "@tailwindcss/vite": "^4.0.17", 6 | "rabbit-tea-vite": "^1.0.0", 7 | "vite": "^5.4.18" 8 | }, 9 | "scripts": { 10 | "dev": "vite", 11 | "build": "vite build", 12 | "preview": "vite preview" 13 | }, 14 | "type": "module" 15 | } 16 | -------------------------------------------------------------------------------- /src/example/canvas/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import rabbitTEA from 'rabbit-tea-vite' 3 | import tailwindcss from '@tailwindcss/vite' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | rabbitTEA(), 8 | tailwindcss() 9 | ] 10 | }) 11 | 12 | -------------------------------------------------------------------------------- /src/example/clipboard/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .mooncakes 3 | target 4 | main.js 5 | main.js.map 6 | -------------------------------------------------------------------------------- /src/example/clipboard/README.md: -------------------------------------------------------------------------------- 1 | # Clipboard Example 2 | 3 | Run the example with the following command: 4 | 5 | ```bash 6 | npm i 7 | npm run dev 8 | ``` 9 | -------------------------------------------------------------------------------- /src/example/clipboard/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | clipboard 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/example/clipboard/main/main.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | fnalias @tea.none 3 | 4 | ///| 5 | fnalias @html.(p, div, text, button) 6 | 7 | ///| 8 | typealias @tea.Cmd 9 | 10 | ///| 11 | typealias @html.Html 12 | 13 | ///| 14 | struct Model { 15 | tips : String 16 | copied : Bool 17 | } 18 | 19 | ///| 20 | enum Msg { 21 | Copy(String) 22 | Paste 23 | GotPasteData(@clipboard.Item) 24 | OnError(String) 25 | OnCopySucceed 26 | } 27 | 28 | ///| 29 | fn update(msg : Msg, model : Model) -> (Cmd[Msg], Model) { 30 | match msg { 31 | Copy(text) => { 32 | let cmd = @clipboard.copy( 33 | Text(text), 34 | copied=OnCopySucceed, 35 | failed=OnError(_), 36 | ) 37 | (cmd, { ..model, copied: false }) 38 | } 39 | Paste => 40 | ( 41 | @clipboard.paste(pasted=GotPasteData(_), failed=OnError(_)), 42 | { ..model, copied: false }, 43 | ) 44 | GotPasteData(Text(tips)) => (none(), { ..model, tips, }) 45 | OnError(err) => (none(), { ..model, tips: err }) 46 | OnCopySucceed => (none(), { ..model, copied: true }) 47 | } 48 | } 49 | 50 | ///| 51 | fn view(model : Model) -> Html[Msg] { 52 | let { tips, copied } = model 53 | div([ 54 | p([text(tips.to_string())]), 55 | button(click=Msg::Copy(tips), [ 56 | text(if copied { "copied!" } else { "copy" }), 57 | ]), 58 | button(click=Msg::Paste, [text("paste")]), 59 | ]) 60 | } 61 | 62 | ///| NOTE: This program is only available in the js backend, 63 | /// see README.md to getting started. 64 | fn main { 65 | let model = { tips: "moon add Yoorkin/rabbit-tea", copied: false } 66 | @tea.startup(model~, update~, view~) 67 | } 68 | -------------------------------------------------------------------------------- /src/example/clipboard/main/main.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/example-todoMVC/main" 2 | 3 | // Values 4 | 5 | // Types and methods 6 | type Model 7 | 8 | type Msg 9 | 10 | // Type aliases 11 | 12 | // Traits 13 | 14 | -------------------------------------------------------------------------------- /src/example/clipboard/main/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "is-main": true, 3 | "import": [ 4 | { 5 | "path": "Yoorkin/rabbit-tea", 6 | "alias": "tea" 7 | }, 8 | "Yoorkin/rabbit-tea/cmd", 9 | "Yoorkin/rabbit-tea/clipboard", 10 | { 11 | "path": "Yoorkin/rabbit-tea/html", 12 | "alias": "html" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /src/example/clipboard/moon.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Yoorkin/example-todoMVC", 3 | "version": "0.0.1", 4 | "deps": { 5 | "Yoorkin/rabbit-tea": { 6 | "path": "../../../." 7 | } 8 | }, 9 | "readme": "README.md", 10 | "license": "Apache-2.0" 11 | } -------------------------------------------------------------------------------- /src/example/clipboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "body-parser": "^1.20.3", 4 | "express": "^4.21.2", 5 | "@tailwindcss/vite": "^4.0.17", 6 | "rabbit-tea-vite": "^1.0.0", 7 | "vite": "^5.4.18" 8 | }, 9 | "scripts": { 10 | "dev": "vite", 11 | "build": "vite build", 12 | "preview": "vite preview" 13 | }, 14 | "type": "module" 15 | } 16 | -------------------------------------------------------------------------------- /src/example/clipboard/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import rabbitTEA from 'rabbit-tea-vite' 3 | import tailwindcss from '@tailwindcss/vite' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | rabbitTEA(), 8 | tailwindcss() 9 | ] 10 | }) 11 | 12 | -------------------------------------------------------------------------------- /src/example/clipboard/vite.config.js.timestamp-1745560557839-1da781a9e4714.mjs: -------------------------------------------------------------------------------- 1 | // vite.config.js 2 | import { defineConfig } from "file:///Users/yorkin/source/moonbit/moonbit-frontend/moonbit-tea/src/example/clipboard/node_modules/vite/dist/node/index.js"; 3 | import rabbitTEA from "file:///Users/yorkin/source/moonbit/moonbit-frontend/moonbit-tea/src/example/clipboard/node_modules/rabbit-tea-vite/dist/index.js"; 4 | import tailwindcss from "file:///Users/yorkin/source/moonbit/moonbit-frontend/moonbit-tea/src/example/clipboard/node_modules/@tailwindcss/vite/dist/index.mjs"; 5 | var vite_config_default = defineConfig({ 6 | plugins: [ 7 | rabbitTEA(), 8 | tailwindcss() 9 | ] 10 | }); 11 | export { 12 | vite_config_default as default 13 | }; 14 | //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMveW9ya2luL3NvdXJjZS9tb29uYml0L21vb25iaXQtZnJvbnRlbmQvbW9vbmJpdC10ZWEvc3JjL2V4YW1wbGUvY2xpcGJvYXJkXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMveW9ya2luL3NvdXJjZS9tb29uYml0L21vb25iaXQtZnJvbnRlbmQvbW9vbmJpdC10ZWEvc3JjL2V4YW1wbGUvY2xpcGJvYXJkL3ZpdGUuY29uZmlnLmpzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy95b3JraW4vc291cmNlL21vb25iaXQvbW9vbmJpdC1mcm9udGVuZC9tb29uYml0LXRlYS9zcmMvZXhhbXBsZS9jbGlwYm9hcmQvdml0ZS5jb25maWcuanNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHJhYmJpdFRFQSBmcm9tICdyYWJiaXQtdGVhLXZpdGUnXG5pbXBvcnQgdGFpbHdpbmRjc3MgZnJvbSAnQHRhaWx3aW5kY3NzL3ZpdGUnXG5cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gICAgcGx1Z2luczogW1xuICAgICAgICByYWJiaXRURUEoKSxcbiAgICAgICAgdGFpbHdpbmRjc3MoKVxuICAgIF1cbn0pXG5cbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBK1osU0FBUyxvQkFBb0I7QUFDNWIsT0FBTyxlQUFlO0FBQ3RCLE9BQU8saUJBQWlCO0FBRXhCLElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQ3hCLFNBQVM7QUFBQSxJQUNMLFVBQVU7QUFBQSxJQUNWLFlBQVk7QUFBQSxFQUNoQjtBQUNKLENBQUM7IiwKICAibmFtZXMiOiBbXQp9Cg== 15 | -------------------------------------------------------------------------------- /src/example/counter/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .mooncakes 3 | target 4 | main.js 5 | main.js.map 6 | -------------------------------------------------------------------------------- /src/example/counter/README.md: -------------------------------------------------------------------------------- 1 | # Counter Example 2 | 3 | Run the example with the following command: 4 | 5 | ```bash 6 | npm i 7 | npm run dev 8 | ``` 9 | -------------------------------------------------------------------------------- /src/example/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | counter 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/example/counter/main/counter.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/example/counter" 2 | 3 | // Values 4 | 5 | // Types and methods 6 | type Msg 7 | 8 | // Type aliases 9 | 10 | // Traits 11 | 12 | -------------------------------------------------------------------------------- /src/example/counter/main/main.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | fnalias @tea.none 3 | 4 | ///| 5 | fnalias @html.(h1, div, text, button) 6 | 7 | ///| 8 | typealias @tea.Cmd 9 | 10 | ///| 11 | typealias @html.Html 12 | 13 | ///| 14 | typealias Model = Int 15 | 16 | ///| 17 | enum Msg { 18 | Increment 19 | Decrement 20 | } 21 | 22 | ///| 23 | fn update(msg : Msg, model : Model) -> (Cmd[Msg], Model) { 24 | match msg { 25 | Increment => (none(), model + 1) 26 | Decrement => (none(), model - 1) 27 | } 28 | } 29 | 30 | ///| 31 | fn view(model : Model) -> Html[Msg] { 32 | div([ 33 | h1([text(model.to_string())]), 34 | button(click=Increment, [text("+")]), 35 | button(click=Decrement, [text("-")]), 36 | ]) 37 | } 38 | 39 | ///| NOTE: This program is only available in the js backend, 40 | /// see README.md to getting started. 41 | fn main { 42 | let model = 0 43 | @tea.startup(model~, update~, view~) 44 | } 45 | -------------------------------------------------------------------------------- /src/example/counter/main/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "is-main": true, 3 | "import": [ 4 | { 5 | "path": "Yoorkin/rabbit-tea", 6 | "alias": "tea" 7 | }, 8 | "Yoorkin/rabbit-tea/cmd", 9 | { 10 | "path": "Yoorkin/rabbit-tea/html", 11 | "alias": "html" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /src/example/counter/moon.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Yoorkin/example-todoMVC", 3 | "version": "0.0.1", 4 | "deps": { 5 | "Yoorkin/rabbit-tea": { 6 | "path": "../../../." 7 | } 8 | }, 9 | "readme": "README.md", 10 | "license": "Apache-2.0" 11 | } -------------------------------------------------------------------------------- /src/example/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "body-parser": "^1.20.3", 4 | "express": "^4.21.2", 5 | "@tailwindcss/vite": "^4.0.17", 6 | "rabbit-tea-vite": "^1.0.0", 7 | "vite": "^5.4.18" 8 | }, 9 | "scripts": { 10 | "dev": "vite", 11 | "build": "vite build", 12 | "preview": "vite preview" 13 | }, 14 | "type": "module" 15 | } 16 | -------------------------------------------------------------------------------- /src/example/counter/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import rabbitTEA from 'rabbit-tea-vite' 3 | import tailwindcss from '@tailwindcss/vite' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | rabbitTEA(), 8 | tailwindcss() 9 | ] 10 | }) 11 | 12 | -------------------------------------------------------------------------------- /src/example/custom_command/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .mooncakes 3 | target 4 | main.js 5 | main.js.map 6 | -------------------------------------------------------------------------------- /src/example/custom_command/README.md: -------------------------------------------------------------------------------- 1 | # Custom Command Example 2 | 3 | Run the example with the following command: 4 | 5 | ```bash 6 | npm i 7 | npm run dev 8 | ``` 9 | -------------------------------------------------------------------------------- /src/example/custom_command/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | command 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/example/custom_command/main/custom_command.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/example/custom_command" 2 | 3 | // Values 4 | 5 | // Types and methods 6 | type Message 7 | 8 | type Model 9 | 10 | // Type aliases 11 | 12 | // Traits 13 | 14 | -------------------------------------------------------------------------------- /src/example/custom_command/main/main.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | fnalias @html.(text, button, div) 3 | 4 | ///| 5 | enum Message { 6 | TimeUp(String) 7 | Clear 8 | SetTime 9 | } 10 | 11 | ///| 12 | struct Model { 13 | text : String 14 | } 15 | 16 | ///| 17 | extern "js" fn set_timeout(f : () -> Unit, ms : Int) = "(f,ms) => setTimeout(f, ms)" 18 | 19 | ///| Custom command to delay the message `msg` for `ms` milliseconds. 20 | fn delay[M](msg : M, ms : Int) -> @cmd.Cmd[M] { 21 | @cmd.Cmd(fn(events) { set_timeout(fn() { events.trigger_update(msg) }, ms) }) 22 | } 23 | 24 | ///| 25 | fn update(msg : Message, model : Model) -> (@cmd.Cmd[Message], Model) { 26 | match msg { 27 | TimeUp(text) => (@cmd.none(), { text: "TimeUp: \{text}" }) 28 | Clear => (@cmd.none(), { text: "" }) 29 | SetTime => (delay(TimeUp("delayed 5000ms"), 5000), model) 30 | } 31 | } 32 | 33 | ///| 34 | fn view(model : Model) -> @html.Html[Message] { 35 | div([ 36 | button(click=Message::Clear, [text("Clear")]), 37 | button(click=Message::SetTime, [text("show text after 5s")]), 38 | text(model.text), 39 | ]) 40 | } 41 | 42 | ///| NOTE: This program is only available in the js backend, 43 | /// see README.md to getting started. 44 | fn main { 45 | let model = { text: "" } 46 | @tea.startup(model~, update~, view~) 47 | } 48 | -------------------------------------------------------------------------------- /src/example/custom_command/main/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "is-main": true, 3 | "import": [ 4 | { 5 | "path": "Yoorkin/rabbit-tea", 6 | "alias": "tea" 7 | }, 8 | "Yoorkin/rabbit-tea/cmd", 9 | { 10 | "path": "Yoorkin/rabbit-tea/html", 11 | "alias": "html" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /src/example/custom_command/moon.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Yoorkin/example-todoMVC", 3 | "version": "0.0.1", 4 | "deps": { 5 | "Yoorkin/rabbit-tea": { 6 | "path": "../../../." 7 | } 8 | }, 9 | "readme": "README.md", 10 | "license": "Apache-2.0" 11 | } -------------------------------------------------------------------------------- /src/example/custom_command/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "body-parser": "^1.20.3", 4 | "express": "^4.21.2", 5 | "@tailwindcss/vite": "^4.0.17", 6 | "rabbit-tea-vite": "^1.0.0", 7 | "vite": "^5.4.18" 8 | }, 9 | "scripts": { 10 | "dev": "vite", 11 | "build": "vite build", 12 | "preview": "vite preview" 13 | }, 14 | "type": "module" 15 | } 16 | -------------------------------------------------------------------------------- /src/example/custom_command/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import rabbitTEA from 'rabbit-tea-vite' 3 | import tailwindcss from '@tailwindcss/vite' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | rabbitTEA(), 8 | tailwindcss() 9 | ] 10 | }) 11 | 12 | -------------------------------------------------------------------------------- /src/example/dialog/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .mooncakes 3 | target 4 | main.js 5 | main.js.map 6 | -------------------------------------------------------------------------------- /src/example/dialog/README.md: -------------------------------------------------------------------------------- 1 | # Dialog Example 2 | 3 | Run the example with the following command: 4 | 5 | ```bash 6 | npm i 7 | npm run dev 8 | ``` 9 | -------------------------------------------------------------------------------- /src/example/dialog/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | dialog 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/example/dialog/main/dialog.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/example/dialog" 2 | 3 | // Values 4 | 5 | // Types and methods 6 | type Msg 7 | 8 | // Type aliases 9 | 10 | // Traits 11 | 12 | -------------------------------------------------------------------------------- /src/example/dialog/main/main.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | fnalias @tea.none 3 | 4 | ///| 5 | fnalias @html.(h1, div, text, button, dialog, input) 6 | 7 | ///| 8 | typealias @tea.Cmd 9 | 10 | ///| 11 | typealias @html.Html 12 | 13 | ///| 14 | priv struct Model { 15 | title : String 16 | input : String 17 | } derive(Show) 18 | 19 | ///| 20 | enum Msg { 21 | Edit 22 | Save 23 | Discard 24 | AboutToClose 25 | DialogClosed(String) 26 | InputChanged(String) 27 | } 28 | 29 | ///| 30 | fn update(msg : Msg, model : Model) -> (Cmd[Msg], Model) { 31 | println(model) 32 | match msg { 33 | Edit => (@dialog.show("menu", modal=true), { ..model, input: model.title }) 34 | Save => (@dialog.close("menu", return_value=model.input), model) 35 | Discard => (@dialog.request_close("menu", return_value=model.title), model) 36 | AboutToClose => { 37 | let cmd = if model.input == model.title || 38 | @dom.window().confirm("Are you sure?") { 39 | @dialog.close("menu", return_value=model.title) 40 | } else { 41 | none() 42 | } 43 | (cmd, model) 44 | } 45 | DialogClosed(title) => (none(), { ..model, title, }) 46 | InputChanged(input) => (none(), { ..model, input, }) 47 | } 48 | } 49 | 50 | ///| 51 | fn view(model : Model) -> Html[Msg] { 52 | div([ 53 | h1([text(model.title)]), 54 | button(click=Msg::Edit, [text("edit")]), 55 | dialog(id="menu", cancel=Msg::AboutToClose, close=DialogClosed(_), [ 56 | h1([text("edit the title")]), 57 | input(input_type=Text, value=model.input, input=InputChanged(_)), 58 | button(click=Msg::Discard, [text("discard")]), 59 | button(click=Msg::Save, [text("save")]), 60 | ]), 61 | ]) 62 | } 63 | 64 | ///| NOTE: This program is only available in the js backend, 65 | /// see README.md to getting started. 66 | fn main { 67 | let model = { title: "Hello World", input: "" } 68 | @tea.startup(model~, update~, view~) 69 | } 70 | -------------------------------------------------------------------------------- /src/example/dialog/main/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "is-main": true, 3 | "import": [ 4 | { 5 | "path": "Yoorkin/rabbit-tea", 6 | "alias": "tea" 7 | }, 8 | "Yoorkin/rabbit-tea/html", 9 | "Yoorkin/rabbit-tea/dom", 10 | "Yoorkin/rabbit-tea/dialog" 11 | ] 12 | } -------------------------------------------------------------------------------- /src/example/dialog/moon.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Yoorkin/example-todoMVC", 3 | "version": "0.0.1", 4 | "deps": { 5 | "Yoorkin/rabbit-tea": { 6 | "path": "../../../." 7 | } 8 | }, 9 | "readme": "README.md", 10 | "license": "Apache-2.0" 11 | } -------------------------------------------------------------------------------- /src/example/dialog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "body-parser": "^1.20.3", 4 | "express": "^4.21.2", 5 | "@tailwindcss/vite": "^4.0.17", 6 | "rabbit-tea-vite": "^1.0.0", 7 | "vite": "^5.4.18" 8 | }, 9 | "scripts": { 10 | "dev": "vite", 11 | "build": "vite build", 12 | "preview": "vite preview" 13 | }, 14 | "type": "module" 15 | } 16 | -------------------------------------------------------------------------------- /src/example/dialog/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import rabbitTEA from 'rabbit-tea-vite' 3 | import tailwindcss from '@tailwindcss/vite' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | rabbitTEA(), 8 | tailwindcss() 9 | ] 10 | }) 11 | 12 | -------------------------------------------------------------------------------- /src/example/form/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .mooncakes 3 | target 4 | main.js 5 | main.js.map 6 | -------------------------------------------------------------------------------- /src/example/form/README.md: -------------------------------------------------------------------------------- 1 | # Form Example 2 | 3 | Run the example with the following command: 4 | 5 | ```bash 6 | npm i 7 | npm run dev 8 | ``` 9 | -------------------------------------------------------------------------------- /src/example/form/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | form 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/example/form/main/form.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/example/form" 2 | 3 | // Values 4 | 5 | // Types and methods 6 | type Msg 7 | 8 | // Type aliases 9 | 10 | // Traits 11 | 12 | -------------------------------------------------------------------------------- /src/example/form/main/main.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | fnalias @tea.none 3 | 4 | ///| 5 | fnalias @html.(h1, div, text, select, option) 6 | 7 | ///| 8 | typealias @tea.Cmd 9 | 10 | ///| 11 | typealias @html.Html 12 | 13 | ///| 14 | typealias Model = String 15 | 16 | ///| 17 | enum Msg { 18 | SelectChange(String) 19 | } 20 | 21 | ///| 22 | fn update(msg : Msg, _ : Model) -> (Cmd[Msg], Model) { 23 | match msg { 24 | SelectChange(selected) => (none(), selected) 25 | } 26 | } 27 | 28 | ///| 29 | fn view(model : Model) -> Html[Msg] { 30 | div([ 31 | h1([text(model)]), 32 | select(change=SelectChange(_), disabled=false, [ 33 | option(value="plan A", selected=false, [text("plan A")]), 34 | option(value="plan B", [text("plan B")]), 35 | option(value="plan C", [text("plan C")]), 36 | ]), 37 | ]) 38 | } 39 | 40 | ///| NOTE: This program is only available in the js backend, 41 | /// see README.md to getting started. 42 | fn main { 43 | let model = "plan A" 44 | @tea.startup(model~, update~, view~) 45 | } 46 | -------------------------------------------------------------------------------- /src/example/form/main/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "is-main": true, 3 | "import": [ 4 | { 5 | "path": "Yoorkin/rabbit-tea", 6 | "alias": "tea" 7 | }, 8 | "Yoorkin/rabbit-tea/html" 9 | ] 10 | } -------------------------------------------------------------------------------- /src/example/form/moon.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Yoorkin/example-todoMVC", 3 | "version": "0.0.1", 4 | "deps": { 5 | "Yoorkin/rabbit-tea": { 6 | "path": "../../../." 7 | } 8 | }, 9 | "readme": "README.md", 10 | "license": "Apache-2.0" 11 | } -------------------------------------------------------------------------------- /src/example/form/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "body-parser": "^1.20.3", 4 | "express": "^4.21.2", 5 | "@tailwindcss/vite": "^4.0.17", 6 | "rabbit-tea-vite": "^1.0.0", 7 | "vite": "^5.4.18" 8 | }, 9 | "scripts": { 10 | "dev": "vite", 11 | "build": "vite build", 12 | "preview": "vite preview" 13 | }, 14 | "type": "module" 15 | } 16 | -------------------------------------------------------------------------------- /src/example/form/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import rabbitTEA from 'rabbit-tea-vite' 3 | import tailwindcss from '@tailwindcss/vite' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | rabbitTEA(), 8 | tailwindcss() 9 | ] 10 | }) 11 | 12 | -------------------------------------------------------------------------------- /src/example/todoMVC/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .mooncakes 3 | target 4 | main.js 5 | main.js.map 6 | -------------------------------------------------------------------------------- /src/example/todoMVC/README.md: -------------------------------------------------------------------------------- 1 | # Todo App Example 2 | 3 | This is a simple todo app example. 4 | It uses the [nested-TEA pattern](https://sporto.github.io/elm-patterns/architecture/nested-tea.html) to demonstrate 5 | how to build a complex app with multiple pages. It doesn't mean the pattern is encouraged to be used in a small app like this. 6 | 7 | ## Setup 8 | 9 | Run the following command to start the example: 10 | 11 | ```bash 12 | npm i 13 | npm run dev 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /src/example/todoMVC/editor/editor.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | enum Model { 3 | Loading 4 | Failed 5 | Editing(Card) 6 | } 7 | 8 | ///| 9 | struct Card { 10 | title : String 11 | content : String 12 | id : Int? 13 | } derive(Show) 14 | 15 | ///| 16 | enum Msg { 17 | TitleChanged(String) 18 | ContentChanged(String) 19 | GotCardData(Result[Card, String]) 20 | SaveCardAndExit(Card) 21 | ReqResult(Result[Json, String]) 22 | } derive(Show) 23 | 24 | ///| 25 | pub fn load(index : Int?) -> (@tea.Cmd[Msg], Model) { 26 | match index { 27 | None => (@tea.none(), Editing({ title: "", content: "", id: None })) 28 | Some(index) => 29 | ( 30 | @http.get( 31 | "/api/cards/\{index}", 32 | expect=Json(GotCardData(_), decode_card), 33 | ), 34 | Loading, 35 | ) 36 | } 37 | } 38 | 39 | ///| 40 | pub fn decode_card(data : Json) -> Result[Card, String] { 41 | match data { 42 | { "title": String(title), "content": String(content), "id": Number(id), .. } => 43 | Ok({ title, content, id: Some(id.to_int()) }) 44 | _ => Err("Invalid card data") 45 | } 46 | } 47 | 48 | ///| 49 | pub fn update(msg : Msg, model : Model) -> (@tea.Cmd[Msg], Model) { 50 | match (msg, model) { 51 | (GotCardData(card), _) => 52 | match card { 53 | Ok(card) => (@tea.none(), Editing(card)) 54 | Err(_) => (@tea.none(), Failed) 55 | } 56 | (TitleChanged(title), Editing(card)) => 57 | (@tea.none(), Editing({ ..card, title, })) 58 | (ContentChanged(content), Editing(card)) => 59 | (@tea.none(), Editing({ ..card, content, })) 60 | (SaveCardAndExit(card), _) => { 61 | let save = match card.id { 62 | Some(id) => 63 | if card.title == "" && card.content == "" { 64 | @http.delete("/api/cards/\{id}", expect=Json(ReqResult(_), Ok(_))) 65 | } else { 66 | @http.patch( 67 | "/api/cards/\{id}", 68 | Json({ 69 | "title": card.title.to_json(), 70 | "content": card.content.to_json(), 71 | "id": id.to_json(), 72 | }), 73 | expect=Json(ReqResult(_), Ok(_)), 74 | ) 75 | } 76 | None => 77 | @http.post( 78 | "/api/cards", 79 | Json({ 80 | "title": Json::string(card.title), 81 | "content": Json::string(card.content), 82 | }), 83 | expect=Json(ReqResult(_), Ok(_)), 84 | ) 85 | } 86 | let exit = @nav.push_url("/home") 87 | (@tea.batch([save, exit]), model) 88 | } 89 | _ => (@tea.none(), model) 90 | } 91 | } 92 | 93 | ///| 94 | pub fn view(model : Model) -> @html.Html[Msg] { 95 | match model { 96 | Loading => @html.div([@html.text("Loading card...")]) 97 | Failed => @html.div([@html.text("Failed to load card")]) 98 | Editing({ title, content, id } as card) => { 99 | let title_text = match id { 100 | Some(_) => @html.text("Edit Todo") 101 | None => @html.text("New Todo") 102 | } 103 | @html.div( 104 | style=[ 105 | "display: flex", "flex-direction: column", "width: 500px", "height: 100%", 106 | ], 107 | [ 108 | @html.h1([title_text]), 109 | view_text_input("title", title, input=TitleChanged(_)), 110 | view_text_input("content", content, input=ContentChanged(_)), 111 | @views.button("close", click=SaveCardAndExit(card)), 112 | ], 113 | ) 114 | } 115 | } 116 | } 117 | 118 | ///| 119 | fn view_text_input[M]( 120 | label : String, 121 | value : String, 122 | input~ : (String) -> M 123 | ) -> @html.Html[M] { 124 | @html.div([ 125 | @html.label([ 126 | @html.text(label), 127 | @html.input( 128 | style=[ 129 | "border: 1px solid #ccc", "border-radius: 5px", "padding: 5px", "margin: 10px", 130 | "width: 500px", 131 | ], 132 | value~, 133 | input_type=Text, 134 | input~, 135 | ), 136 | ]), 137 | ]) 138 | } 139 | -------------------------------------------------------------------------------- /src/example/todoMVC/editor/editor.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/example-todoMVC/editor" 2 | 3 | import( 4 | "Yoorkin/rabbit-tea/cmd" 5 | "Yoorkin/rabbit-tea/html" 6 | ) 7 | 8 | // Values 9 | fn decode_card(Json) -> Result[Card, String] 10 | 11 | fn load(Int?) -> (@cmd.Cmd[Msg], Model) 12 | 13 | fn update(Msg, Model) -> (@cmd.Cmd[Msg], Model) 14 | 15 | fn view(Model) -> @html.T[Msg] 16 | 17 | // Types and methods 18 | type Card 19 | impl Show for Card 20 | 21 | type Model 22 | 23 | type Msg 24 | impl Show for Msg 25 | 26 | // Type aliases 27 | 28 | // Traits 29 | 30 | -------------------------------------------------------------------------------- /src/example/todoMVC/editor/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | { 4 | "path": "Yoorkin/rabbit-tea", 5 | "alias": "tea" 6 | }, 7 | "Yoorkin/rabbit-tea/html", 8 | "Yoorkin/rabbit-tea/http", 9 | "Yoorkin/rabbit-tea/nav", 10 | "Yoorkin/example-todoMVC/views" 11 | ] 12 | } -------------------------------------------------------------------------------- /src/example/todoMVC/home/card.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | enum CardModel { 3 | TitleCard(title~ : String, content~ : String, id~ : Int) 4 | Card(String, id~ : Int) 5 | } derive(Show) 6 | 7 | ///| 8 | fn id(self : CardModel) -> Int { 9 | match self { 10 | TitleCard(id~, ..) => id 11 | Card(_, id~) => id 12 | } 13 | } 14 | 15 | ///| 16 | fn view_card[Msg](model : CardModel) -> @html.Html[Msg] { 17 | let (content, id) = match model { 18 | TitleCard(title~, content~, id~) => 19 | ([@html.h1([@html.text(title)]), @html.p([@html.text(content)])], id) 20 | Card(content, id~) => ([@html.p([@html.text(content)])], id) 21 | } 22 | @html.a( 23 | href="cards/\{id}", 24 | style=[ 25 | "box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05)", "border-style: solid", "border-width: 1px", 26 | "border-color: #e5e7eb", "padding: 1.25rem", "border-radius: 0.5rem", "margin: 0.5rem", 27 | "background-color: #fff", 28 | ], 29 | content, 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/example/todoMVC/home/home.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | pub enum Msg { 3 | DeleteAll 4 | GotCardsData(Result[Array[CardModel], String]) 5 | ReqResult(Result[Json, String]) 6 | } derive(Show) 7 | 8 | ///| 9 | pub struct Model { 10 | cards : Array[CardModel] 11 | } derive(Show) 12 | 13 | ///| 14 | pub fn load() -> (@tea.Cmd[Msg], Model) { 15 | ( 16 | @http.get("/api/cards", expect=Json(GotCardsData(_), decode_cards)), 17 | { cards: [] }, 18 | ) 19 | } 20 | 21 | ///| 22 | pub fn view(model : Model) -> @html.Html[Msg] { 23 | @html.div(style=["p-0", "display:flex", "flex-direction:column"], [ 24 | @html.h1([@html.text("MoonBit TodoMVC")]), 25 | @html.div(style=["display: flex", "width: 500px", "height: 100%"], [ 26 | @views.button("clear", click=DeleteAll), 27 | @views.button("+", href="/new"), 28 | ]), 29 | @html.p([@html.text("Click cards below to edit, click + to add a new card")]), 30 | view_list(model.cards), 31 | ]) 32 | } 33 | 34 | ///| 35 | pub fn update(msg : Msg, model : Model) -> (@tea.Cmd[Msg], Model) { 36 | match msg { 37 | DeleteAll => { 38 | let cmds = @tea.batch( 39 | [ 40 | ..model.cards.map(fn(card) { 41 | @http.delete( 42 | "/api/cards/\{card.id()}", 43 | expect=Json(ReqResult(_), Ok(_)), 44 | ) 45 | }), 46 | @nav.push_url("/home"), 47 | ], 48 | ) 49 | (cmds, model) 50 | } 51 | GotCardsData(res) => 52 | match res { 53 | Ok(cards) => (@tea.none(), { cards, }) 54 | Err(_) => (@tea.none(), { cards: [] }) 55 | } 56 | ReqResult(_) => (@tea.none(), model) 57 | } 58 | } 59 | 60 | ///| 61 | fn decode_cards(json : Json) -> Result[Array[CardModel], String] { 62 | match json { 63 | { "cards": Array(elems), .. } => { 64 | let cards = elems.filter_map(fn { 65 | { 66 | "title"? : Some(String("")) 67 | | None, 68 | "content": String(content), 69 | "id": Number(id), 70 | .. 71 | } => Some(Card(content, id=id.to_int())) 72 | { 73 | "title": String(title), 74 | "content": String(content), 75 | "id": Number(id), 76 | .. 77 | } => Some(TitleCard(title~, content~, id=id.to_int())) 78 | _ => None 79 | }) 80 | Ok(cards) 81 | } 82 | _ => Err("Invalid cards data") 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/example/todoMVC/home/home.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/example-todoMVC/home" 2 | 3 | import( 4 | "Yoorkin/rabbit-tea/cmd" 5 | "Yoorkin/rabbit-tea/html" 6 | ) 7 | 8 | // Values 9 | fn id2() -> Int 10 | 11 | fn load() -> (@cmd.Cmd[Msg], Model) 12 | 13 | fn update(Msg, Model) -> (@cmd.Cmd[Msg], Model) 14 | 15 | fn view(Model) -> @html.T[Msg] 16 | 17 | // Types and methods 18 | type CardModel 19 | impl Show for CardModel 20 | 21 | pub struct Model { 22 | cards : Array[CardModel] 23 | } 24 | impl Show for Model 25 | 26 | pub enum Msg { 27 | DeleteAll 28 | GotCardsData(Result[Array[CardModel], String]) 29 | ReqResult(Result[Json, String]) 30 | } 31 | impl Show for Msg 32 | 33 | // Type aliases 34 | 35 | // Traits 36 | 37 | -------------------------------------------------------------------------------- /src/example/todoMVC/home/list.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | typealias ListModel = Array[CardModel] 3 | 4 | ///| 5 | fn view_list[A](model : ListModel) -> @html.Html[A] { 6 | @html.div( 7 | style=[ 8 | "display: flex", "flex-direction: column", "width: 500px", "height: 100%", 9 | ], 10 | model.map(view_card), 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/example/todoMVC/home/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | { 4 | "path": "Yoorkin/rabbit-tea", 5 | "alias": "tea" 6 | }, 7 | "Yoorkin/rabbit-tea/html", 8 | "Yoorkin/rabbit-tea/http", 9 | "Yoorkin/rabbit-tea/nav", 10 | "Yoorkin/example-todoMVC/views" 11 | ] 12 | } -------------------------------------------------------------------------------- /src/example/todoMVC/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | todoMVC 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/example/todoMVC/main/main.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | enum Message { 3 | GotHomeMsg(@home.Msg) 4 | GotEditorMsg(@editor.Msg) 5 | LinkClicked(@url.UrlRequest) 6 | UrlChanged(@url.Url) 7 | } derive(Show) 8 | 9 | ///| 10 | enum Model { 11 | NotFound 12 | Home(@home.Model) 13 | Editor(@editor.Model) 14 | } 15 | 16 | ///| 17 | fn view(model : Model) -> @html.Html[Message] { 18 | match model { 19 | Home(model) => @home.view(model).map(GotHomeMsg(_)) 20 | Editor(model) => @editor.view(model).map(GotEditorMsg(_)) 21 | NotFound => @html.div([@html.text("Not Found")]) 22 | } 23 | } 24 | 25 | ///| 26 | fn update_with[SubModel, SubMsg]( 27 | pair : (@tea.Cmd[SubMsg], SubModel), 28 | to_model : (SubModel) -> Model, 29 | to_msg : (SubMsg) -> Message 30 | ) -> (@tea.Cmd[Message], Model) { 31 | let (cmd, model) = pair 32 | (cmd.map(to_msg), to_model(model)) 33 | } 34 | 35 | ///| 36 | fn update(msg : Message, model : Model) -> (@tea.Cmd[Message], Model) { 37 | match (msg, model) { 38 | (GotHomeMsg(msg), Home(model)) => 39 | @home.update(msg, model) |> update_with(Home(_), GotHomeMsg(_)) 40 | (GotEditorMsg(msg), Editor(model)) => 41 | @editor.update(msg, model) |> update_with(Editor(_), GotEditorMsg(_)) 42 | (LinkClicked(request), _) => 43 | match request { 44 | Internal(url) => (@nav.push_url(url.to_string()), model) 45 | External(url) => (@nav.load(url), model) 46 | } 47 | (UrlChanged(url), _) => route(url) 48 | _ => (@tea.none(), model) 49 | } 50 | } 51 | 52 | ///| 53 | fn route(url : @url.Url) -> (@tea.Cmd[Message], Model) { 54 | let paths = url.path.split("/").collect() 55 | println("routing to \{url.to_string()}, path \{paths}") 56 | match paths { 57 | ["home" | "/" | ""] => @home.load() |> update_with(Home(_), GotHomeMsg(_)) 58 | ["new"] => @editor.load(None) |> update_with(Editor(_), GotEditorMsg(_)) 59 | ["cards", id] => 60 | match @strconv.parse_int?(id.to_string()) { 61 | Ok(id) => 62 | @editor.load(Some(id)) |> update_with(Editor(_), GotEditorMsg(_)) 63 | Err(err) => { 64 | println("error parsing id \{err}") 65 | (@tea.none(), NotFound) 66 | } 67 | } 68 | _ => (@cmd.none(), NotFound) 69 | } 70 | } 71 | 72 | ///| 73 | fn main { 74 | @tea.application( 75 | initialize=route, 76 | update~, 77 | view~, 78 | url_request=LinkClicked(_), 79 | url_changed=UrlChanged(_), 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /src/example/todoMVC/main/main.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/example-todoMVC/main" 2 | 3 | // Values 4 | 5 | // Types and methods 6 | type Message 7 | impl Show for Message 8 | 9 | type Model 10 | 11 | // Type aliases 12 | 13 | // Traits 14 | 15 | -------------------------------------------------------------------------------- /src/example/todoMVC/main/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "is-main": true, 3 | "import": [ 4 | { 5 | "path": "Yoorkin/rabbit-tea", 6 | "alias": "tea" 7 | }, 8 | "Yoorkin/rabbit-tea/cmd", 9 | "Yoorkin/rabbit-tea/html", 10 | "Yoorkin/rabbit-tea/nav", 11 | "Yoorkin/rabbit-tea/url", 12 | "Yoorkin/example-todoMVC/home", 13 | "Yoorkin/example-todoMVC/editor" 14 | ] 15 | } -------------------------------------------------------------------------------- /src/example/todoMVC/moon.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Yoorkin/example-todoMVC", 3 | "version": "0.0.1", 4 | "deps": { 5 | "Yoorkin/rabbit-tea": { 6 | "path": "../../../." 7 | } 8 | }, 9 | "readme": "README.md", 10 | "license": "Apache-2.0" 11 | } -------------------------------------------------------------------------------- /src/example/todoMVC/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "body-parser": "^1.20.3", 4 | "express": "^4.21.2", 5 | "@tailwindcss/vite": "^4.0.17", 6 | "rabbit-tea-vite": "^1.0.0", 7 | "vite": "^5.4.18" 8 | }, 9 | "scripts": { 10 | "dev": "vite", 11 | "build": "vite build", 12 | "preview": "vite preview" 13 | }, 14 | "type": "module" 15 | } 16 | -------------------------------------------------------------------------------- /src/example/todoMVC/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | 4 | export default function () { 5 | 6 | const app = express(); 7 | const port = 3000; 8 | 9 | // Middleware to parse JSON bodies 10 | app.use(bodyParser.json()); 11 | 12 | // In-memory storage for cards 13 | let cards = [ 14 | { 15 | "title": "", 16 | "content": "This is a note. Click on the edit button to start editing.", 17 | "id": 1 18 | }, 19 | { 20 | "title": "", 21 | "content": "This is a note.", 22 | "id": 2 23 | }, 24 | { 25 | "title": "Todo", 26 | "content": "delete this note", 27 | "id": 3 28 | } 29 | ] 30 | 31 | // GET /api/cards - Retrieve all cards 32 | app.get('/api/cards', (req, res) => { 33 | res.json({ cards: cards }); 34 | console.log("GET /api/cards", cards); 35 | }); 36 | 37 | // GET /api/cards/:id - Retrieve all cards 38 | app.get('/api/cards/:id', (req, res) => { 39 | const cardId = parseInt(req.params.id, 10); 40 | const cardIndex = cards.findIndex(card => card.id === cardId); 41 | res.json(cards[cardIndex]); 42 | console.log(`GET /api/cards/${cardId}`, cards[cardIndex]); 43 | }); 44 | 45 | // POST /api/cards - Create a new card 46 | app.post('/api/cards', (req, res) => { 47 | const newCard = req.body; 48 | newCard.id = cards.length + 1; // Simple ID assignment 49 | cards.push(newCard); 50 | res.status(201).json(newCard); 51 | console.log("POST /api/cards", newCard); 52 | }); 53 | 54 | // PATCH /api/cards/:id - Update a card 55 | app.patch('/api/cards/:id', (req, res) => { 56 | const cardId = parseInt(req.params.id, 10); 57 | const cardIndex = cards.findIndex(card => card.id === cardId); 58 | 59 | if (cardIndex === -1) { 60 | return res.status(404).json({ error: 'Card not found' }); 61 | } 62 | 63 | const updatedCard = { ...cards[cardIndex], ...req.body }; 64 | cards[cardIndex] = updatedCard; 65 | res.json(updatedCard); 66 | console.log(`PATCH /api/cards/${cardId}`, updatedCard); 67 | }); 68 | 69 | // DELETE /api/cards/:id - Delete a card 70 | app.delete('/api/cards/:id', (req, res) => { 71 | const cardId = parseInt(req.params.id, 10); 72 | const cardIndex = cards.findIndex(card => card.id === cardId); 73 | 74 | if (cardIndex === -1) { 75 | return res.status(404).json({ error: 'Card not found' }); 76 | } 77 | 78 | cards.splice(cardIndex, 1); 79 | res.status(204).send(); 80 | console.log(`DELETE /api/cards/${cardId}`); 81 | }); 82 | 83 | // Start the server 84 | app.listen(port, () => { 85 | console.log(`Server is listening on http://localhost:${port}`); 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /src/example/todoMVC/views/button.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | pub fn button[M](text : String, href? : String, click? : M) -> @html.Html[M] { 3 | let button_style = [ 4 | "background: pink", "border-radius: 10px", "width:80px", "padding:20px", "margin:10px", 5 | ] 6 | let content = [@html.text(text)] 7 | match (href, click) { 8 | (Some(href), _) => @html.a(style=button_style, href~, content) 9 | (_, Some(click)) => @html.div(style=button_style, click~, content) 10 | _ => @html.div(style=button_style, content) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/example/todoMVC/views/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "Yoorkin/rabbit-tea/html" 4 | ] 5 | } -------------------------------------------------------------------------------- /src/example/todoMVC/views/views.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/example-todoMVC/views" 2 | 3 | import( 4 | "Yoorkin/rabbit-tea/html" 5 | ) 6 | 7 | // Values 8 | fn button[M](String, href? : String, click? : M) -> @html.T[M] 9 | 10 | // Types and methods 11 | 12 | // Type aliases 13 | 14 | // Traits 15 | 16 | -------------------------------------------------------------------------------- /src/example/todoMVC/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import rabbitTEA from 'rabbit-tea-vite' 3 | import tailwindcss from '@tailwindcss/vite' 4 | import { exec } from 'child_process'; 5 | import server from './server.js'; 6 | 7 | server(); 8 | 9 | export default defineConfig({ 10 | plugins: [ 11 | rabbitTEA(), 12 | tailwindcss() 13 | ], 14 | server: { 15 | proxy: { 16 | '/api': 'http://localhost:3000' 17 | } 18 | } 19 | }) 20 | 21 | -------------------------------------------------------------------------------- /src/html/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Html 3 | 4 | This package provides the helper functions to build Html. 5 | 6 | Here are two design choices: 7 | 8 | - Guide Users with Type Definitions 9 | 10 | This package defines specific types for arguments instead of relying on a potentially confusing `String` type. These types serve as documentation, helping users understand how to correctly provide arguments. 11 | 12 | - Seamless HTML EDSL 13 | 14 | Instead of relying on a precompiler to process JSX-like syntax, this EDSL allows users to leverage the full power of Moonbit's syntax and toolchain. 15 | 16 | You can embed expressions in views without needing escape characters like `property={expression}` or `:property=expression`. For cases where the property name matches the variable name, you can use the convenient name-punning syntax, such as `property?` or `property~`. 17 | 18 | In this early stage, we need to focus on improving the functionality of Rabbit-TEA. Language extensions like JSX may be considered after the 1.0 release. 19 | 20 | # Using the Html EDSL 21 | 22 | We are trying to define wrapper functions for each HTML element. They all follow a form like this: 23 | 24 | ```mbt 25 | pub fn div[M]( 26 | style~ : Array[String] = [], 27 | id? : String, 28 | class? : String, 29 | click? : M, 30 | childrens : Array[Html[M]] 31 | ) -> Html[M] 32 | ``` 33 | 34 | ## The `text` element 35 | 36 | To represent text in HTML, use the `text` function. 37 | 38 | ```mbt 39 | let html = p([text("hello world")]) 40 | ``` 41 | 42 | ## The special `nothing` element 43 | 44 | There is a special `nothing` element that does not represent an actual HTML element, it simply represents "nothing". This is particularly useful for handling multiple `Option` types in your model: 45 | 46 | ```mbt 47 | fn bar(path : Path, tag : Option[Tag]){ 48 | let path = foo(path) 49 | let tag = match tag { 50 | None => [] 51 | Some(x) => [view(x)] // Yuck! 52 | } 53 | div([path] + tag) // Don't do this 54 | } 55 | ``` 56 | 57 | ```mbt 58 | fn bar(path : Path, tag : Option[Tag]){ 59 | let path = foo(path) 60 | let tag = match tag { 61 | None => nothing() 62 | Some(x) => view(x) 63 | } 64 | div([path, tag]) // Use @html.nothing 65 | } 66 | ``` 67 | 68 | ## Advanced Usage 69 | 70 | The wrapper functions and properties provided here may not cover all possible use cases. If you encounter missing functionality, feel free to file an issue or use the `node()` function as a workaround. The `node()` function allows you to manually specify the tag name, attributes, and children for your HTML element, offering flexibility for advanced or uncommon scenarios. 71 | 72 | ```mbt 73 | let html = node( 74 | "div", 75 | [style("key","value"), property("id","key")], 76 | [child1, child2], 77 | ) 78 | ``` 79 | 80 | Contributions to help us finish the missing wrappers or arguments are also welcome. 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/html/attributes.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | type Attribute[Msg] @vdom.Attribute[Msg] 3 | 4 | ///| Specify an style 5 | pub fn[Msg] style(key : String, value : String) -> Attribute[Msg] { 6 | @vdom.style(key, value) 7 | } 8 | 9 | ///| 10 | pub fn[Msg] attribute(key : String, value : String) -> Attribute[Msg] { 11 | @vdom.attribute(key, value) 12 | } 13 | 14 | ///| 15 | pub fn[M] property(key : String, value : @variant.Variant) -> Attribute[M] { 16 | @vdom.property(key, value) 17 | } 18 | 19 | ///| 20 | pub fn[Msg] href(value : String) -> Attribute[Msg] { 21 | // Note: `href` will be the value you specified, but by property, the value 22 | // will be resolved to an absolute URL. 23 | @vdom.property("href", @variant.String(value)) 24 | } 25 | 26 | ///| 27 | pub(all) enum Target { 28 | Self 29 | Blank 30 | } 31 | 32 | ///| 33 | fn to_string(self : Target) -> String { 34 | match self { 35 | Self => "_self" 36 | Blank => "_blank" 37 | } 38 | } 39 | 40 | ///| 41 | pub fn[Msg] target(value : Target) -> Attribute[Msg] { 42 | @vdom.attribute("target", value.to_string()) 43 | } 44 | -------------------------------------------------------------------------------- /src/html/canvas/canvas.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | struct Model[Msg] { 3 | element : @dom.HTMLCanvasElement 4 | wrapper : @html.T[Msg] 5 | } 6 | 7 | ///| 8 | pub fn[Msg] context2d(self : Model[Msg]) -> Context2D { 9 | self.element.get_context("2d").to0().unwrap() 10 | } 11 | 12 | ///| 13 | pub fn[Msg] new( 14 | width~ : Double, 15 | height~ : Double, 16 | click? : (@html.Mouse) -> Msg, 17 | mousemove? : (@html.Mouse) -> Msg, 18 | mousedown? : (@html.Mouse) -> Msg, 19 | mouseup? : (@html.Mouse) -> Msg, 20 | mouseenter? : (@html.Mouse) -> Msg, 21 | mouseleave? : (@html.Mouse) -> Msg 22 | ) -> Model[Msg] { 23 | let canvas = @dom.document().create_element("canvas") 24 | let attributes = { 25 | let attrs = [ 26 | @html.attribute("width", width.to_string()), 27 | @html.attribute("height", height.to_string()), 28 | ] 29 | [ 30 | click.map(@html.on_click), 31 | mousemove.map(@html.on_mouse_move), 32 | mousedown.map(@html.on_mouse_down), 33 | mouseup.map(@html.on_mouse_up), 34 | mouseenter.map(@html.on_mouse_enter), 35 | mouseleave.map(@html.on_mouse_leave), 36 | ].each(fn { 37 | None => () 38 | Some(attr) => attrs.push(attr) 39 | }) 40 | @ref.new(Some(attrs)) 41 | } 42 | let element = canvas 43 | .to_html_element() 44 | .to_option() 45 | .unwrap() 46 | .to_html_canvas_element() 47 | .to_option() 48 | .unwrap() 49 | let node = canvas.to_node() 50 | { 51 | element, 52 | wrapper: @html.external( 53 | node, 54 | attributes, 55 | width=width.to_int(), 56 | height=height.to_int(), 57 | ), 58 | } 59 | } 60 | 61 | ///| 62 | pub fn[Msg] to_html(self : Model[Msg]) -> @html.T[Msg] { 63 | self.wrapper 64 | } 65 | -------------------------------------------------------------------------------- /src/html/canvas/canvas.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/html/canvas" 2 | 3 | import( 4 | "Yoorkin/rabbit-tea/dom" 5 | "Yoorkin/rabbit-tea/html" 6 | ) 7 | 8 | // Values 9 | fn begin_path(Context2D) -> Unit 10 | 11 | fn clear_rect(Context2D, Double, Double, Double, Double) -> Unit 12 | 13 | fn[Msg] context2d(Model[Msg]) -> Context2D 14 | 15 | fn fill_rect(Context2D, Double, Double, Double, Double) -> Unit 16 | 17 | fn fill_text(Context2D, String, Double, Double) -> Unit 18 | 19 | fn line_to(Context2D, Double, Double) -> Unit 20 | 21 | fn move_to(Context2D, Double, Double) -> Unit 22 | 23 | fn[Msg] new(width~ : Double, height~ : Double, click? : (@html.Mouse) -> Msg, mousemove? : (@html.Mouse) -> Msg, mousedown? : (@html.Mouse) -> Msg, mouseup? : (@html.Mouse) -> Msg, mouseenter? : (@html.Mouse) -> Msg, mouseleave? : (@html.Mouse) -> Msg) -> Model[Msg] 24 | 25 | fn set_fill_style(Context2D, Color) -> Unit 26 | 27 | fn set_line_width(Context2D, Double) -> Unit 28 | 29 | fn set_stroke_style(Context2D, Color) -> Unit 30 | 31 | fn stroke(Context2D) -> Unit 32 | 33 | fn[Msg] to_html(Model[Msg]) -> @html.T[Msg] 34 | 35 | fn to_string(Color) -> String 36 | 37 | fn to_unsafe_js(Context2D) -> @dom.CanvasRenderingContext2D 38 | 39 | // Types and methods 40 | pub(all) enum Color { 41 | RGB(Int, Int, Int) 42 | RGBA(Int, Int, Int, Int) 43 | Black 44 | White 45 | } 46 | fn Color::to_string(Self) -> String 47 | impl Show for Color 48 | 49 | type Context2D 50 | fn Context2D::begin_path(Self) -> Unit 51 | fn Context2D::clear_rect(Self, Double, Double, Double, Double) -> Unit 52 | fn Context2D::fill_rect(Self, Double, Double, Double, Double) -> Unit 53 | fn Context2D::fill_text(Self, String, Double, Double) -> Unit 54 | fn Context2D::line_to(Self, Double, Double) -> Unit 55 | fn Context2D::move_to(Self, Double, Double) -> Unit 56 | fn Context2D::set_fill_style(Self, Color) -> Unit 57 | fn Context2D::set_line_width(Self, Double) -> Unit 58 | fn Context2D::set_stroke_style(Self, Color) -> Unit 59 | fn Context2D::stroke(Self) -> Unit 60 | fn Context2D::to_unsafe_js(Self) -> @dom.CanvasRenderingContext2D 61 | 62 | type Model[Msg] 63 | fn[Msg] Model::context2d(Self[Msg]) -> Context2D 64 | fn[Msg] Model::to_html(Self[Msg]) -> @html.T[Msg] 65 | 66 | // Type aliases 67 | 68 | // Traits 69 | 70 | -------------------------------------------------------------------------------- /src/html/canvas/context2d.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | type Context2D @dom.CanvasRenderingContext2D 3 | 4 | ///| Get the underlying `CanvasRenderingContext2D` js object. 5 | /// 6 | /// This function is unsafe and should be used rarely. 7 | pub fn to_unsafe_js(self : Context2D) -> @dom.CanvasRenderingContext2D { 8 | self._ 9 | } 10 | 11 | ///| 12 | pub fn begin_path(self : Context2D) -> Unit { 13 | self._.begin_path() 14 | } 15 | 16 | ///| 17 | pub fn move_to(self : Context2D, x : Double, y : Double) -> Unit { 18 | self._.move_to(x, y) 19 | } 20 | 21 | ///| 22 | pub fn line_to(self : Context2D, x : Double, y : Double) -> Unit { 23 | self._.line_to(x, y) 24 | } 25 | 26 | ///| 27 | pub fn set_line_width(self : Context2D, width : Double) -> Unit { 28 | self._.set_line_width(width) 29 | } 30 | 31 | ///| 32 | pub fn stroke(self : Context2D) -> Unit { 33 | self._.stroke() 34 | } 35 | 36 | ///| 37 | pub(all) enum Color { 38 | RGB(Int, Int, Int) 39 | RGBA(Int, Int, Int, Int) 40 | Black 41 | White 42 | } derive(Show) 43 | 44 | ///| 45 | pub fn to_string(self : Color) -> String { 46 | match self { 47 | RGB(r, g, b) => "rgb(\{r}, \{g}, \{b})" 48 | RGBA(r, g, b, a) => "rgba(\{r}, \{g}, \{b}, \{a})" 49 | Black => "black" 50 | White => "white" 51 | } 52 | } 53 | 54 | ///| 55 | pub fn set_stroke_style(self : Context2D, color : Color) -> Unit { 56 | self._.set_stroke_style(@js.Union3::from0(color.to_string())) 57 | } 58 | 59 | ///| 60 | pub fn set_fill_style(self : Context2D, color : Color) -> Unit { 61 | self._.set_fill_style(@js.Union3::from0(color.to_string())) 62 | } 63 | 64 | ///| 65 | pub fn fill_rect( 66 | self : Context2D, 67 | x : Double, 68 | y : Double, 69 | width : Double, 70 | height : Double 71 | ) -> Unit { 72 | self._.fill_rect(x, y, width, height) 73 | } 74 | 75 | ///| 76 | pub fn clear_rect( 77 | self : Context2D, 78 | x : Double, 79 | y : Double, 80 | width : Double, 81 | height : Double 82 | ) -> Unit { 83 | self._.clear_rect(x, y, width, height) 84 | } 85 | 86 | ///| 87 | pub fn fill_text( 88 | self : Context2D, 89 | text : String, 90 | x : Double, 91 | y : Double 92 | ) -> Unit { 93 | self._.fill_text(text, x, y) 94 | } 95 | -------------------------------------------------------------------------------- /src/html/canvas/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "Yoorkin/rabbit-tea/dom", 4 | "rami3l/js-ffi/js", 5 | "Yoorkin/rabbit-tea/html" 6 | ], 7 | "link": { 8 | "js": { 9 | "format": "iife" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/html/event.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | type Mouse @dom.MouseEvent 3 | 4 | ///| 5 | pub(all) struct Pos { 6 | x : Int 7 | y : Int 8 | } 9 | 10 | ///| 11 | pub fn Mouse::screen_pos(self : Mouse) -> Pos { 12 | { x: self._.screen_x(), y: self._.screen_y() } 13 | } 14 | 15 | ///| 16 | pub fn Mouse::offset_pos(self : Mouse) -> Pos { 17 | { x: self._.offset_x(), y: self._.offset_y() } 18 | } 19 | 20 | ///| 21 | pub fn Mouse::client_pos(self : Mouse) -> Pos { 22 | { x: self._.client_x(), y: self._.client_y() } 23 | } 24 | 25 | ///| 26 | fn[Msg] on_mouse(event : String, msg : (Mouse) -> Msg) -> Attribute[Msg] { 27 | @vdom.on( 28 | event, 29 | HandleEvent(fn(event) { 30 | msg( 31 | event 32 | .to_ui_event() 33 | .to_option() 34 | .unwrap() 35 | .to_mouse_event() 36 | .to_option() 37 | .unwrap(), 38 | ) 39 | }), 40 | ) 41 | } 42 | 43 | ///| 44 | pub fn[Msg] on_click(msg : (Mouse) -> Msg) -> Attribute[Msg] { 45 | on_mouse("click", msg) 46 | } 47 | 48 | ///| 49 | pub fn[Msg] on_input(msg : (String) -> Msg) -> Attribute[Msg] { 50 | @vdom.on( 51 | "input", 52 | HandleEvent(fn(event) { 53 | // TODO: eliminate to_option 54 | let value : String = event 55 | .target() 56 | .to_node() 57 | .to_option() 58 | .unwrap() 59 | .to_element() 60 | .to_option() 61 | .unwrap() 62 | .to_html_element() 63 | .to_option() 64 | .unwrap() 65 | .to_html_input_element() 66 | .to_option() 67 | .unwrap() 68 | .value() 69 | msg(value) 70 | }), 71 | ) 72 | } 73 | 74 | ///| 75 | pub fn[Msg] on_change(msg : (String) -> Msg) -> Attribute[Msg] { 76 | @vdom.on( 77 | "change", 78 | HandleEvent(fn(event) { 79 | // TODO: eliminate to_option 80 | let html_element = event 81 | .target() 82 | .to_node() 83 | .to_option() 84 | .unwrap() 85 | .to_element() 86 | .to_option() 87 | .unwrap() 88 | .to_html_element() 89 | .to_option() 90 | .unwrap() 91 | let value = if html_element.to_html_input_element().to_option() is Some(x) { 92 | x.value() 93 | } else if html_element.to_html_select_element().to_option() is Some(x) { 94 | x.value() 95 | } else { 96 | panic() // TODO: check for other HTMLElement types 97 | } 98 | msg(value) 99 | }), 100 | ) 101 | } 102 | 103 | ///| 104 | pub fn[Msg] on_double_click(msg : (Mouse) -> Msg) -> Attribute[Msg] { 105 | on_mouse("dblclick", msg) 106 | } 107 | 108 | ///| 109 | pub fn[Msg] on_mouse_down(msg : (Mouse) -> Msg) -> Attribute[Msg] { 110 | on_mouse("mousedown", msg) 111 | } 112 | 113 | ///| 114 | pub fn[Msg] on_mouse_up(msg : (Mouse) -> Msg) -> Attribute[Msg] { 115 | on_mouse("mouseup", msg) 116 | } 117 | 118 | ///| 119 | pub fn[Msg] on_mouse_move(msg : (Mouse) -> Msg) -> Attribute[Msg] { 120 | on_mouse("mousemove", msg) 121 | } 122 | 123 | ///| 124 | pub fn[Msg] on_mouse_enter(msg : (Mouse) -> Msg) -> Attribute[Msg] { 125 | on_mouse("mouseenter", msg) 126 | } 127 | 128 | ///| 129 | pub fn[Msg] on_mouse_over(msg : (Mouse) -> Msg) -> Attribute[Msg] { 130 | on_mouse("mouseover", msg) 131 | } 132 | 133 | ///| 134 | pub fn[Msg] on_mouse_leave(msg : (Mouse) -> Msg) -> Attribute[Msg] { 135 | on_mouse("mouseleave", msg) 136 | } 137 | 138 | ///| 139 | pub fn[Msg] on_mouse_out(msg : (Mouse) -> Msg) -> Attribute[Msg] { 140 | on_mouse("mouseout", msg) 141 | } 142 | -------------------------------------------------------------------------------- /src/html/html.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/html" 2 | 3 | import( 4 | "Yoorkin/rabbit-tea/dom" 5 | "Yoorkin/rabbit-tea/internal/vdom" 6 | "Yoorkin/rabbit-tea/variant" 7 | ) 8 | 9 | // Values 10 | fn[M] a(style~ : Array[String] = .., id? : String, class? : String, href~ : String, target~ : Target = .., Array[T[M]], escape~ : Bool = ..) -> T[M] 11 | 12 | fn[Msg] attribute(String, String) -> Attribute[Msg] 13 | 14 | fn[M] b(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 15 | 16 | fn[M] blockquote(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 17 | 18 | fn[M] br(style~ : Array[String] = .., id? : String, class? : String) -> T[M] 19 | 20 | fn[M] button(style~ : Array[String] = .., id? : String, class? : String, click? : M, Array[T[M]]) -> T[M] 21 | 22 | fn[M] caption(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 23 | 24 | fn[M] code(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 25 | 26 | fn[M] col(style~ : Array[String] = .., id? : String, span? : Int, class? : String, Array[T[M]]) -> T[M] 27 | 28 | fn[M] colgroup(style~ : Array[String] = .., id? : String, span? : Int, class? : String, Array[T[M]]) -> T[M] 29 | 30 | fn[M] dd(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 31 | 32 | fn[M] dialog(style~ : Array[String] = .., id? : String, class? : String, open? : Bool, close? : (String) -> M, cancel? : M, Array[T[M]]) -> T[M] 33 | 34 | fn[M] div(style~ : Array[String] = .., id? : String, class? : String, click? : M, Array[T[M]]) -> T[M] 35 | 36 | fn[M] dl(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 37 | 38 | fn[M] dt(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 39 | 40 | fn[M] em(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 41 | 42 | fn[Msg] external(@dom.Node, Ref[Array[Attribute[Msg]]?], width~ : Int, height~ : Int) -> T[Msg] 43 | 44 | fn[M] form(style~ : Array[String] = .., id? : String, class? : String, action? : String, name? : String, Array[T[M]]) -> T[M] 45 | 46 | fn[M] h1(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 47 | 48 | fn[M] h2(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 49 | 50 | fn[M] h3(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 51 | 52 | fn[M] h4(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 53 | 54 | fn[M] h5(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 55 | 56 | fn[M] h6(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 57 | 58 | fn[M] hr(style~ : Array[String] = .., id? : String, class? : String, childrens~ : Array[T[M]] = ..) -> T[M] 59 | 60 | fn[Msg] href(String) -> Attribute[Msg] 61 | 62 | fn[M] i(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 63 | 64 | fn[M] iframe(style~ : Array[String] = .., id? : String, class? : String, src? : String, title? : String, width? : Int, height? : Int) -> T[M] 65 | 66 | fn[M] img(style~ : Array[String] = .., id? : String, class? : String, src? : String, alt? : String, title? : String, width? : Int, height? : Int, border? : Int, Array[T[M]]) -> T[M] 67 | 68 | fn[M] input(input_type~ : InputType = .., name? : String, value? : String, checked? : Bool, read_only? : Bool, multiple? : Bool, accept? : String, placeholder? : String, auto_complete? : AutoComplete, style~ : Array[String] = .., max? : Int, min? : Int, step? : Int, maxlength? : Int, minlength? : Int, pattern? : String, size? : Int, width? : Int, height? : Int, id? : String, class? : String, childrens~ : Array[T[M]] = .., change? : (String) -> M, input? : (String) -> M) -> T[M] 69 | 70 | fn[M] label(style~ : Array[String] = .., id? : String, class? : String, for_? : String, Array[T[M]]) -> T[M] 71 | 72 | fn[M] li(style~ : Array[String] = .., value? : Int, id? : String, class? : String, click? : M, Array[T[M]]) -> T[M] 73 | 74 | fn[A, B] map(T[A], (A) -> B) -> T[B] 75 | 76 | fn[Msg] node(String, Array[Attribute[Msg]], Array[T[Msg]]) -> T[Msg] 77 | 78 | fn[M] nothing() -> T[M] 79 | 80 | fn[M] ol(style~ : Array[String] = .., reversed? : Bool, start? : Int, id? : String, class? : String, Array[T[M]]) -> T[M] 81 | 82 | fn[Msg] on_change((String) -> Msg) -> Attribute[Msg] 83 | 84 | fn[Msg] on_click((Mouse) -> Msg) -> Attribute[Msg] 85 | 86 | fn[Msg] on_double_click((Mouse) -> Msg) -> Attribute[Msg] 87 | 88 | fn[Msg] on_input((String) -> Msg) -> Attribute[Msg] 89 | 90 | fn[Msg] on_mouse_down((Mouse) -> Msg) -> Attribute[Msg] 91 | 92 | fn[Msg] on_mouse_enter((Mouse) -> Msg) -> Attribute[Msg] 93 | 94 | fn[Msg] on_mouse_leave((Mouse) -> Msg) -> Attribute[Msg] 95 | 96 | fn[Msg] on_mouse_move((Mouse) -> Msg) -> Attribute[Msg] 97 | 98 | fn[Msg] on_mouse_out((Mouse) -> Msg) -> Attribute[Msg] 99 | 100 | fn[Msg] on_mouse_over((Mouse) -> Msg) -> Attribute[Msg] 101 | 102 | fn[Msg] on_mouse_up((Mouse) -> Msg) -> Attribute[Msg] 103 | 104 | fn[M] option(style~ : Array[String] = .., id? : String, class? : String, disabled? : Bool, value? : String, selected~ : Bool = .., Array[T[M]]) -> T[M] 105 | 106 | fn[M] p(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 107 | 108 | fn[M] pre(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 109 | 110 | fn[M] property(String, @variant.Variant) -> Attribute[M] 111 | 112 | fn[M] section(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 113 | 114 | fn[M] select(style~ : Array[String] = .., id? : String, class? : String, disabled? : Bool, name? : String, change? : (String) -> M, Array[T[M]]) -> T[M] 115 | 116 | fn[M] span(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 117 | 118 | fn[M] strong(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 119 | 120 | fn[Msg] style(String, String) -> Attribute[Msg] 121 | 122 | fn[M] sub(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 123 | 124 | fn[M] sup(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 125 | 126 | fn[M] table(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 127 | 128 | fn[Msg] target(Target) -> Attribute[Msg] 129 | 130 | fn[M] tbody(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 131 | 132 | fn[M] td(style~ : Array[String] = .., id? : String, colspan? : Int, rowspan? : Int, headers? : String, class? : String, Array[T[M]]) -> T[M] 133 | 134 | fn[Msg] text(String) -> T[Msg] 135 | 136 | fn[M] tfoot(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 137 | 138 | fn[M] th(style~ : Array[String] = .., id? : String, abbr? : String, colspan? : Int, rowspan? : Int, headers? : String, scope? : Scope, class? : String, Array[T[M]]) -> T[M] 139 | 140 | fn[M] thead(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 141 | 142 | fn[Msg] to_virtual_dom(T[Msg]) -> @vdom.Node[Msg] 143 | 144 | fn[M] tr(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 145 | 146 | fn[M] u(style~ : Array[String] = .., id? : String, class? : String, Array[T[M]]) -> T[M] 147 | 148 | fn[M] ul(style~ : Array[String] = .., id? : String, class? : String, click? : M, Array[T[M]]) -> T[M] 149 | 150 | // Types and methods 151 | type Attribute[Msg] 152 | 153 | pub(all) enum AutoComplete { 154 | On 155 | Off 156 | } 157 | 158 | pub(all) enum InputType { 159 | Button 160 | Checkbox 161 | Color 162 | Date 163 | DateTimeLocal 164 | Email 165 | File 166 | Hidden 167 | Image 168 | Month 169 | Number 170 | Password 171 | Radio 172 | Range 173 | Reset 174 | Search 175 | Submit 176 | Tel 177 | Text 178 | Time 179 | Url 180 | Week 181 | } 182 | 183 | type Mouse 184 | fn Mouse::client_pos(Self) -> Pos 185 | fn Mouse::offset_pos(Self) -> Pos 186 | fn Mouse::screen_pos(Self) -> Pos 187 | 188 | pub(all) struct Pos { 189 | x : Int 190 | y : Int 191 | } 192 | 193 | pub(all) enum Scope { 194 | Row 195 | Col 196 | RowGroup 197 | ColGroup 198 | } 199 | 200 | type T[Msg] 201 | fn[A, B] T::map(Self[A], (A) -> B) -> Self[B] 202 | fn[Msg] T::to_virtual_dom(Self[Msg]) -> @vdom.Node[Msg] 203 | 204 | pub(all) enum Target { 205 | Self 206 | Blank 207 | } 208 | 209 | // Type aliases 210 | pub typealias Html[Msg] = T[Msg] 211 | 212 | // Traits 213 | 214 | -------------------------------------------------------------------------------- /src/html/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | { 4 | "path": "Yoorkin/rabbit-tea/internal/vdom", 5 | "alias": "vdom" 6 | }, 7 | "Yoorkin/rabbit-tea/dom", 8 | "rami3l/js-ffi/js", 9 | "Yoorkin/rabbit-tea/variant" 10 | ], 11 | "link": { 12 | "js": { 13 | "format": "iife" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/http/http.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | pub typealias Decoder[Model] = (Json) -> Result[Model, String] 3 | 4 | ///| 5 | pub(all) enum Expecting[Msg, Model] { 6 | Json((Result[Model, String]) -> Msg, Decoder[Model]) 7 | Text((Result[String, String]) -> Msg) 8 | } 9 | 10 | ///| 11 | pub(all) enum Body { 12 | Json(Json) 13 | Text(String) 14 | Empty 15 | } 16 | 17 | ///| 18 | fn content_type(self : Body) -> String { 19 | match self { 20 | Json(_) => "application/json" 21 | Text(_) => "text/plain" 22 | Empty(_) => "text/plain" 23 | } 24 | } 25 | 26 | ///| 27 | fn stringfiy(self : Body) -> String { 28 | match self { 29 | Json(json) => json.stringify() 30 | Text(text) => text 31 | Empty => "" 32 | } 33 | } 34 | 35 | ///| 36 | extern "js" fn js_request( 37 | url : String, 38 | http_method : String, 39 | content_type~ : String, 40 | body~ : String, 41 | has_body~ : Bool, 42 | succeed~ : (String) -> Unit, 43 | failed~ : (String) -> Unit 44 | ) = 45 | #| (url,method,contentType,body,hasBody,succeed,failed) => { 46 | #| var config = { method: method, headers: { 'Content-Type': contentType } }; 47 | #| if (hasBody) { config.body = body }; 48 | #| console.log("Requesting: ",url,config); 49 | #| fetch(url, config) 50 | #| .then(response => response.text()) 51 | #| .then(json => succeed(json)) 52 | #| .catch(error => { 53 | #| console.log(error); 54 | #| failed(error.toString()) 55 | #| }) 56 | #| } 57 | 58 | ///| 59 | fn[Msg, Model] request( 60 | url : String, 61 | http_method : String, 62 | expect~ : Expecting[Msg, Model], 63 | body~ : Body 64 | ) -> @cmd.Cmd[Msg] { 65 | let launch = fn(events : @cmd.Events[Msg]) { 66 | let has_body = match body { 67 | Empty => false 68 | _ => true 69 | } 70 | let content_type = body.content_type() 71 | let body = body.stringfiy() 72 | match expect { 73 | Json(f, decoder) => 74 | js_request( 75 | url, 76 | http_method, 77 | has_body~, 78 | body~, 79 | content_type~, 80 | succeed=fn(str) { 81 | let result = try Ok(@json.parse!(str)) catch { 82 | _ => Err("Json parse error") 83 | } 84 | events.trigger_update(f(result.bind(decoder))) 85 | }, 86 | failed=fn(msg) { 87 | events.trigger_update(f(Err("Http request failed:\{msg}"))) 88 | }, 89 | ) 90 | Text(f) => 91 | js_request( 92 | url, 93 | http_method, 94 | has_body~, 95 | body~, 96 | content_type~, 97 | succeed=fn(str) { events.trigger_update(f(Ok(str))) }, 98 | failed=fn(msg) { 99 | events.trigger_update(f(Err("Http request failed:\{msg}"))) 100 | }, 101 | ) 102 | } 103 | } 104 | Cmd(launch) 105 | } 106 | 107 | ///| Create a command to send a GET request. 108 | pub fn[Msg, Model] get( 109 | url : String, 110 | expect~ : Expecting[Msg, Model] 111 | ) -> @cmd.Cmd[Msg] { 112 | request(url, "GET", expect~, body=Empty) 113 | } 114 | 115 | ///| Create a command to send a PUT request. 116 | pub fn[Msg, Model] delete( 117 | url : String, 118 | expect~ : Expecting[Msg, Model] 119 | ) -> @cmd.Cmd[Msg] { 120 | request(url, "DELETE", expect~, body=Empty) 121 | } 122 | 123 | ///| Create a command to send a POST request. 124 | pub fn[Msg, Model] post( 125 | url : String, 126 | body : Body, 127 | expect~ : Expecting[Msg, Model] 128 | ) -> @cmd.Cmd[Msg] { 129 | request(url, "POST", body~, expect~) 130 | } 131 | 132 | ///| Create a command to send a PATCH request. 133 | pub fn[Msg, Model] patch( 134 | url : String, 135 | body : Body, 136 | expect~ : Expecting[Msg, Model] 137 | ) -> @cmd.Cmd[Msg] { 138 | request(url, "PATCH", body~, expect~) 139 | } 140 | -------------------------------------------------------------------------------- /src/http/http.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/http" 2 | 3 | import( 4 | "Yoorkin/rabbit-tea/cmd" 5 | ) 6 | 7 | // Values 8 | fn[Msg, Model] delete(String, expect~ : Expecting[Msg, Model]) -> @cmd.Cmd[Msg] 9 | 10 | fn[Msg, Model] get(String, expect~ : Expecting[Msg, Model]) -> @cmd.Cmd[Msg] 11 | 12 | fn[Msg, Model] patch(String, Body, expect~ : Expecting[Msg, Model]) -> @cmd.Cmd[Msg] 13 | 14 | fn[Msg, Model] post(String, Body, expect~ : Expecting[Msg, Model]) -> @cmd.Cmd[Msg] 15 | 16 | // Types and methods 17 | pub(all) enum Body { 18 | Json(Json) 19 | Text(String) 20 | Empty 21 | } 22 | 23 | pub(all) enum Expecting[Msg, Model] { 24 | Json((Result[Model, String]) -> Msg, (Json) -> Result[Model, String]) 25 | Text((Result[String, String]) -> Msg) 26 | } 27 | 28 | // Type aliases 29 | pub typealias Decoder[Model] = (Json) -> Result[Model, String] 30 | 31 | // Traits 32 | 33 | -------------------------------------------------------------------------------- /src/http/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "Yoorkin/rabbit-tea/cmd" 4 | ] 5 | } -------------------------------------------------------------------------------- /src/internal/browser/README.md: -------------------------------------------------------------------------------- 1 | The `browser` package is deprecated! Please use `@cmd` or `@nav` instead 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/internal/browser/browser.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/internal/browser" 2 | 3 | import( 4 | "Yoorkin/rabbit-tea/cmd" 5 | "Yoorkin/rabbit-tea/url" 6 | ) 7 | 8 | // Values 9 | fn[Msg, Model, View] get_on_url_changed(Sandbox[Msg, Model, View]) -> ((@url.Url) -> Msg)? 10 | 11 | fn[Msg, Model, View] get_on_url_request(Sandbox[Msg, Model, View]) -> ((@url.UrlRequest) -> Msg)? 12 | 13 | fn[Msg, Model, View] get_predefined_events(Sandbox[Msg, Model, View]) -> @cmd.Events[Msg] 14 | 15 | fn[M, Model, View] launch(Sandbox[M, Model, View], @cmd.Cmd[M]) -> Unit 16 | 17 | fn[Msg, Model, View] refresh(Sandbox[Msg, Model, View]) -> Unit 18 | 19 | fn[Msg, Model, View] update(Sandbox[Msg, Model, View], Msg) -> Unit 20 | 21 | // Types and methods 22 | type Sandbox[Msg, Model, View] 23 | fn[Msg, Model, View] Sandbox::get_on_url_changed(Self[Msg, Model, View]) -> ((@url.Url) -> Msg)? 24 | fn[Msg, Model, View] Sandbox::get_on_url_request(Self[Msg, Model, View]) -> ((@url.UrlRequest) -> Msg)? 25 | fn[Msg, Model, View] Sandbox::get_predefined_events(Self[Msg, Model, View]) -> @cmd.Events[Msg] 26 | fn[M, Model, View] Sandbox::launch(Self[M, Model, View], @cmd.Cmd[M]) -> Unit 27 | fn[Model, Msg, View] Sandbox::new(Model, (Msg, Model) -> (@cmd.Cmd[Msg], Model), (Model) -> View, after_update~ : (View) -> Unit, url_changed? : (@url.Url) -> Msg, url_request? : (@url.UrlRequest) -> Msg) -> Self[Msg, Model, View] 28 | fn[Msg, Model, View] Sandbox::refresh(Self[Msg, Model, View]) -> Unit 29 | fn[Msg, Model, View] Sandbox::update(Self[Msg, Model, View], Msg) -> Unit 30 | 31 | // Type aliases 32 | 33 | // Traits 34 | 35 | -------------------------------------------------------------------------------- /src/internal/browser/command.mbt: -------------------------------------------------------------------------------- 1 | ///| Launch commands. It may trigger the update function. 2 | pub fn[M, Model, View] launch( 3 | self : Sandbox[M, Model, View], 4 | cmd : @cmd.Cmd[M] 5 | ) -> Unit { 6 | let f = cmd._ 7 | f(self.predefined.unwrap()) 8 | } 9 | -------------------------------------------------------------------------------- /src/internal/browser/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "link": { 3 | "js": { 4 | "format": "iife" 5 | } 6 | }, 7 | "import": [ 8 | "Yoorkin/rabbit-tea/url", 9 | "Yoorkin/rabbit-tea/dom", 10 | "Yoorkin/rabbit-tea/cmd" 11 | ] 12 | 13 | } -------------------------------------------------------------------------------- /src/internal/browser/sandbox.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | struct Sandbox[Msg, Model, View] { 3 | mut model : Model 4 | update : (Msg, Model) -> (@cmd.Cmd[Msg], Model) 5 | view : (Model) -> View 6 | after_update : (View) -> Unit 7 | on_url_changed : ((@url.Url) -> Msg)? 8 | on_url_request : ((@url.UrlRequest) -> Msg)? 9 | mut predefined : @cmd.Events[Msg]? 10 | } 11 | 12 | ///| 13 | // used by vdom 14 | pub fn[Msg, Model, View] get_predefined_events( 15 | self : Sandbox[Msg, Model, View] 16 | ) -> @cmd.Events[Msg] { 17 | self.predefined.unwrap() 18 | } 19 | 20 | ///| 21 | // used by vdom 22 | pub fn[Msg, Model, View] get_on_url_changed( 23 | self : Sandbox[Msg, Model, View] 24 | ) -> ((@url.Url) -> Msg)? { 25 | self.on_url_changed 26 | } 27 | 28 | ///| 29 | pub fn[Msg, Model, View] get_on_url_request( 30 | self : Sandbox[Msg, Model, View] 31 | ) -> ((@url.UrlRequest) -> Msg)? { 32 | self.on_url_request 33 | } 34 | 35 | ///| Update the model and launch commands. 36 | pub fn[Msg, Model, View] update( 37 | self : Sandbox[Msg, Model, View], 38 | message : Msg 39 | ) -> Unit { 40 | let (cmd, model) = (self.update)(message, self.model) 41 | self.model = model 42 | let view = (self.view)(self.model) 43 | (self.after_update)(view) 44 | // TODO: 45 | // The command may trigger another message immediately, causing the VDOM to be generated twice. 46 | // We need to optimize this. 47 | self.launch(cmd) 48 | } 49 | 50 | ///| Refresh the view. 51 | /// This function will call the view function and patch the result to the DOM. 52 | pub fn[Msg, Model, View] refresh(self : Sandbox[Msg, Model, View]) -> Unit { 53 | let view = (self.view)(self.model) 54 | (self.after_update)(view) 55 | } 56 | 57 | ///| 58 | pub fn[Model, Msg, View] Sandbox::new( 59 | model : Model, 60 | update : (Msg, Model) -> (@cmd.Cmd[Msg], Model), 61 | view : (Model) -> View, 62 | after_update~ : (View) -> Unit, 63 | url_changed? : (@url.Url) -> Msg, 64 | url_request? : (@url.UrlRequest) -> Msg 65 | ) -> Sandbox[Msg, Model, View] { 66 | let sandbox = { 67 | model, 68 | update, 69 | view, 70 | after_update, 71 | on_url_changed: url_changed, 72 | on_url_request: url_request, 73 | predefined: None, 74 | } 75 | let on_url_changed = match url_changed { 76 | Some(f) => fn(url) { sandbox.update(f(url)) } 77 | None => ignore 78 | } 79 | let on_url_request = match url_request { 80 | Some(f) => fn(url) { sandbox.update(f(url)) } 81 | None => ignore 82 | } 83 | @dom.window() 84 | .to_event_target() 85 | .add_event_listener("popstate", fn(_event) { 86 | guard @url.parse?(@dom.window().current_url()) is Ok(url) 87 | on_url_changed(url) 88 | }) 89 | sandbox.predefined = Some( 90 | @cmd.Events::new(on_url_changed, on_url_request, fn(msg) { 91 | sandbox.update(msg) 92 | }), 93 | ) 94 | sandbox 95 | } 96 | -------------------------------------------------------------------------------- /src/internal/ffi/ffi.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/internal/ffi" 2 | 3 | // Values 4 | 5 | // Types and methods 6 | 7 | // Type aliases 8 | 9 | // Traits 10 | 11 | -------------------------------------------------------------------------------- /src/internal/ffi/moon.pkg.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/internal/ffi/utils.mbt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/internal/vdom/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | { "path" : "Yoorkin/rabbit-tea/internal/browser", "alias": "adapter" }, 4 | "Yoorkin/rabbit-tea/dom", 5 | "Yoorkin/rabbit-tea/url", 6 | "Yoorkin/rabbit-tea/variant", 7 | "rami3l/js-ffi/js" 8 | ], 9 | "link": { 10 | "js": { 11 | "format": "iife" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/internal/vdom/show.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | impl[Msg] Show for AttrsUpdate[Msg] with to_string(self : AttrsUpdate[Msg]) -> String { 3 | match self { 4 | AttrRemove(key) => "AttrRemove(\{key})" 5 | AttrAdd(key, value) => "AttrAdd(\{key}, \{value})" 6 | StyleAdd(key, value) => "StyleAdd(\{key}, \{value})" 7 | StyleRemove(key) => "StyleRemove(\{key})" 8 | PropertyAdd(key, value) => "PropertyAdd(\{key}, \{value})" 9 | PropertyRemove(key) => "PropertyRemove(\{key})" 10 | EventRemove(key, _) => "EventRemove(\{key})" 11 | EventAdd(key, _) => "EventAdd(\{key})" 12 | } 13 | } 14 | 15 | ///| 16 | impl[Msg] Show for AttrsUpdate[Msg] with output(self, buf) { 17 | buf.write_string(self.to_string()) 18 | } 19 | 20 | ///| 21 | impl[Msg] Show for AttrValue[Msg] with to_string(self) { 22 | match self { 23 | AttrEvent(_) => "AttrEvent" 24 | AttrStyle(value) => "AttrStyle(\{value})" 25 | AttrString(value) => "AttrString(\{value})" 26 | AttrProperty(value) => "AttrProperty(\{value})" 27 | } 28 | } 29 | 30 | ///| 31 | impl[Msg] Show for AttrValue[Msg] with output(self, buf) { 32 | buf.write_string(self.to_string()) 33 | } 34 | 35 | ///| 36 | impl[Msg] Show for Node[Msg] with to_string(self) { 37 | match self { 38 | Node(tag, attrs~, childrens~, listeners~) => 39 | "Node(\{tag}, \{attrs}, \{childrens}, Listeners(\{listeners.length()}))" 40 | ExternalNode(_, attrs, width~, height~) => 41 | "ExternalNode(\{attrs},\{width},\{height})" 42 | Text(value) => "Text(\{value})" 43 | Nothing => "Nothing" 44 | } 45 | } 46 | 47 | ///| 48 | impl[Msg] Show for Node[Msg] with output(self, buf) { 49 | buf.write_string(self.to_string()) 50 | } 51 | 52 | ///| 53 | impl[Msg] Show for Patch[Msg] with to_string(self) { 54 | match self { 55 | Drop(index, length) => "Drop(\{index}, \{length})" 56 | Remove(index) => "Remove(\{index})" 57 | Replace(index, node) => "Replace(\{index}, \{node})" 58 | InsertBefore(index, node) => "InsertBefore(\{index}, \{node})" 59 | Append(nodes) => "Append(\{nodes})" 60 | Update(update) => "Update(\{update})" 61 | } 62 | } 63 | 64 | ///| 65 | impl[Msg] Show for Patch[Msg] with output(self, buf) { 66 | buf.write_string(self.to_string()) 67 | } 68 | 69 | ///| 70 | impl[Msg] Show for Update[Msg] with to_string(self) { 71 | match self { 72 | UpdateNode(index, attrs, childrens, listeners) => 73 | "UpdateNode(\{index}, \{attrs}, \{childrens}, Listeners(\{listeners.length()}))" 74 | UpdateText(index, value) => "UpdateText(\{index}, \{value})" 75 | } 76 | } 77 | 78 | ///| 79 | impl[Msg] Show for Update[Msg] with output(self, buf) { 80 | buf.write_string(self.to_string()) 81 | } 82 | 83 | ///| 84 | impl[Msg] Show for Attribute[Msg] with to_string(self) { 85 | let Attribute((key, value)) = self 86 | "Attribute(\{key}, \{value})" 87 | } 88 | 89 | ///| 90 | impl[Msg] Show for Attribute[Msg] with output(self, buf) { 91 | buf.write_string(self.to_string()) 92 | } 93 | -------------------------------------------------------------------------------- /src/internal/vdom/test.mbt: -------------------------------------------------------------------------------- 1 | // test { 2 | // let node1 = { 3 | // tag: "div", 4 | // attrs: { "id": "1", "class": "container" }, 5 | // childrens: [ 6 | // { 7 | // tag: "h1", 8 | // attrs: {}, 9 | // childrens: [ 10 | // { 11 | // tag: "a", 12 | // attrs: { "href": "https://example.com", "target": "_blank" }, 13 | // childrens: [ 14 | // { 15 | // tag: "img", 16 | // attrs: { 17 | // "src": "https://example.com/image.png", 18 | // "alt": "image", 19 | // "title": "image", 20 | // "width": "100", 21 | // "height": "100", 22 | // "border": "0", 23 | // }, 24 | // childrens: [ 25 | // { 26 | // tag: "img", 27 | // attrs: { 28 | // "src": "https://example.com/image.png", 29 | // "alt": "image", 30 | // "title": "image", 31 | // "width": "100", 32 | // "height": "100", 33 | // "border": "0", 34 | // }, 35 | // childrens: [], 36 | // }, 37 | // { 38 | // tag: "img", 39 | // attrs: { 40 | // "src": "https://example.com/image.png", 41 | // "alt": "image", 42 | // "title": "image", 43 | // "width": "100", 44 | // "height": "100", 45 | // "border": "0", 46 | // }, 47 | // childrens: [], 48 | // }, 49 | // ], 50 | // }, 51 | // ], 52 | // }, 53 | // ], 54 | // }, 55 | // ], 56 | // } 57 | // let node2 = { 58 | // tag: "div", 59 | // attrs: { "id": "1", "class": "container" }, 60 | // childrens: [ 61 | // { 62 | // tag: "h1", 63 | // attrs: {}, 64 | // childrens: [ 65 | // { 66 | // tag: "a", 67 | // attrs: { "href": "https://example.com", "target": "_blank" }, 68 | // childrens: [ 69 | // { 70 | // tag: "img", 71 | // attrs: { 72 | // "src": "https://example.com/image.png", 73 | // "alt": "image", 74 | // "title": "image", 75 | // "width": "100", 76 | // "height": "10", 77 | // "border": "0", 78 | // }, 79 | // childrens: [ 80 | // { 81 | // tag: "img", 82 | // attrs: { 83 | // "src": "https://example.com/image.png", 84 | // "alt": "image", 85 | // "title": "image", 86 | // "width": "100", 87 | // "height": "100", 88 | // "border": "0", 89 | // }, 90 | // childrens: [], 91 | // }, 92 | // ], 93 | // }, 94 | // ], 95 | // }, 96 | // { 97 | // tag: "b", 98 | // attrs: { "href": "https://example.com", "target": "_blank" }, 99 | // childrens: [ 100 | // { 101 | // tag: "img", 102 | // attrs: { 103 | // "src": "https://example.com/image.png", 104 | // "alt": "image", 105 | // "title": "image", 106 | // "width": "100", 107 | // "height": "100", 108 | // "border": "0", 109 | // }, 110 | // childrens: [], 111 | // }, 112 | // ], 113 | // }, 114 | // ], 115 | // }, 116 | // ], 117 | // } 118 | // let patches = diff(node1, node2) 119 | // inspect!( 120 | // @pp.render(patches), 121 | // content= 122 | // #|[ 123 | // #| MutateAttrs(3, {"src": "https://example.com/image.png", "alt": "image", "title": "image", "width": "100", "height": "10", "border": "0"}), 124 | // #| Remove(5), 125 | // #| InsertAfter(6, {tagName: "b", attrs: {"href": "https://example.com", "target": "_blank"}, childrens: [{tagName: "img", attrs: {"src": "https://example.com/image.png", "alt": "image", "title": "image", "width": "100", "height": "100", "border": "0"}, childrens: []}]}) 126 | // #|] 127 | // , 128 | // ) 129 | // } 130 | -------------------------------------------------------------------------------- /src/internal/vdom/vdom.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/internal/vdom" 2 | 3 | import( 4 | "Yoorkin/rabbit-tea/dom" 5 | "Yoorkin/rabbit-tea/internal/browser" 6 | "Yoorkin/rabbit-tea/variant" 7 | "rami3l/js-ffi/js" 8 | ) 9 | 10 | // Values 11 | fn[Msg] attribute(String, String) -> Attribute[Msg] 12 | 13 | fn[Msg] diff(Node[Msg], Node[Msg]) -> Patch[Msg] 14 | 15 | fn[Msg] diff_without_key(Array[Node[Msg]], Array[Node[Msg]]) -> Array[Patch[Msg]] 16 | 17 | fn[Msg] do_diff(Node[Msg], Node[Msg]) -> Array[Patch[Msg]] 18 | 19 | fn[Msg] external(@dom.Node, Ref[Array[Attribute[Msg]]?], width~ : Int, height~ : Int) -> Node[Msg] 20 | 21 | fn[Msg] link(Array[Attribute[Msg]], Array[Node[Msg]], escape~ : Bool = ..) -> Node[Msg] 22 | 23 | fn[A, B] map(Node[A], (A) -> B) -> Node[B] 24 | 25 | fn[Msg] node(String, Array[Attribute[Msg]], Array[Node[Msg]]) -> Node[Msg] 26 | 27 | fn[Msg] nothing() -> Node[Msg] 28 | 29 | fn[Msg] on(String, Handler[Msg]) -> Attribute[Msg] 30 | 31 | fn[Msg, Model, View] patch(Node[Msg], Node[Msg], @browser.Sandbox[Msg, Model, View], mount~ : String) -> Unit 32 | 33 | fn[Msg] property(String, @variant.Variant) -> Attribute[Msg] 34 | 35 | fn[Msg] style(String, String) -> Attribute[Msg] 36 | 37 | fn[Msg] text(String) -> Node[Msg] 38 | 39 | fn variant_to_js_value(@variant.Variant) -> @js.Value 40 | 41 | // Types and methods 42 | type Attribute[Msg] 43 | fn[A, B] Attribute::map(Self[A], (A) -> B) -> Self[B] 44 | 45 | pub(all) enum Handler[Msg] { 46 | Normal(Msg) 47 | HandleEvent((@dom.Event) -> Msg) 48 | Custom(Msg, stop_propagation~ : Bool, prevent_default~ : Bool) 49 | } 50 | fn[A, B] Handler::map(Self[A], (A) -> B) -> Self[B] 51 | 52 | type Node[Msg] 53 | fn[A, B] Node::map(Self[A], (A) -> B) -> Self[B] 54 | fn[Msg, Model, View] Node::patch(Self[Msg], Self[Msg], @browser.Sandbox[Msg, Model, View], mount~ : String) -> Unit 55 | 56 | type Patch[Msg] 57 | 58 | // Type aliases 59 | 60 | // Traits 61 | 62 | -------------------------------------------------------------------------------- /src/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "Yoorkin/rabbit-tea/html", 4 | "Yoorkin/rabbit-tea/internal/vdom", 5 | "Yoorkin/rabbit-tea/internal/browser", 6 | "Yoorkin/rabbit-tea/url", 7 | "Yoorkin/rabbit-tea/dom", 8 | "Yoorkin/rabbit-tea/cmd", 9 | "rami3l/js-ffi/js" 10 | ] 11 | } -------------------------------------------------------------------------------- /src/nav/README.md: -------------------------------------------------------------------------------- 1 | # Navigation 2 | 3 | This package provides a collection of functions to navigate, scroll the viewport, 4 | and manage the URL history in a web application. Each function returns a `Cmd[Msg]` 5 | type, representing an action that has yet to be executed. 6 | 7 | For more details on how commands work, see the documentation in the 8 | `rabbit-tea/cmd` package. 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/nav/aliases.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | typealias @cmd.Cmd 3 | -------------------------------------------------------------------------------- /src/nav/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "Yoorkin/rabbit-tea/cmd", 4 | "Yoorkin/rabbit-tea/url", 5 | "Yoorkin/rabbit-tea/dom", 6 | "rami3l/js-ffi/js" 7 | ] 8 | } -------------------------------------------------------------------------------- /src/nav/nav.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/nav" 2 | 3 | import( 4 | "Yoorkin/rabbit-tea/cmd" 5 | ) 6 | 7 | // Values 8 | fn[M] back() -> @cmd.Cmd[M] 9 | 10 | fn[M] forward() -> @cmd.Cmd[M] 11 | 12 | fn[M] load(String) -> @cmd.Cmd[M] 13 | 14 | fn[M] push_url(String) -> @cmd.Cmd[M] 15 | 16 | fn[M] reload() -> @cmd.Cmd[M] 17 | 18 | fn[M] replace_url(String) -> @cmd.Cmd[M] 19 | 20 | fn[M] scroll_by_pos(Int, Int) -> @cmd.Cmd[M] 21 | 22 | fn[M] scroll_to(String) -> @cmd.Cmd[M] 23 | 24 | fn[M] scroll_to_bottom() -> @cmd.Cmd[M] 25 | 26 | fn[M] scroll_to_pos(Int, Int) -> @cmd.Cmd[M] 27 | 28 | fn[M] scroll_to_top() -> @cmd.Cmd[M] 29 | 30 | // Types and methods 31 | 32 | // Type aliases 33 | 34 | // Traits 35 | 36 | -------------------------------------------------------------------------------- /src/nav/navigation.mbt: -------------------------------------------------------------------------------- 1 | ///| Create a command to go back in history. 2 | /// This will cause the page to reload. 3 | pub fn[M] back() -> Cmd[M] { 4 | fn(_) { @dom.window().history_go_back() } 5 | } 6 | 7 | ///| Create a command to go forward in history. 8 | /// This will cause the page to reload. 9 | pub fn[M] forward() -> Cmd[M] { 10 | fn(_) { @dom.window().history_go_forward() } 11 | } 12 | 13 | ///| Create a command to load a new URL. 14 | /// This will cause the page to reload. 15 | pub fn[M] load(url : String) -> Cmd[M] { 16 | fn(_) { @dom.window().load_url(url) } 17 | } 18 | 19 | ///| Create a command to reload the current url. 20 | pub fn[M] reload() -> Cmd[M] { 21 | fn(_) { @dom.window().reload_url() } 22 | } 23 | 24 | ///| Create a command to push a new URL to history but not trigger a page load. 25 | /// 26 | /// This will trigger the `url_changed` message. 27 | pub fn[M] push_url(url : String) -> Cmd[M] { 28 | fn(events : @cmd.Events[M]) { 29 | @dom.window().push_url(url) 30 | guard @url.parse?(@dom.window().current_url()) is Ok(url) 31 | events.trigger_url_changed(url) 32 | } 33 | } 34 | 35 | ///| Create a command to change the URL but not trigger a page load. 36 | /// 37 | /// This will trigger the `url_changed` message. 38 | pub fn[M] replace_url(url : String) -> Cmd[M] { 39 | fn(events : @cmd.Events[M]) { 40 | @dom.window().replace_url(url) 41 | guard @url.parse?(@dom.window().current_url()) is Ok(url) 42 | events.trigger_url_changed(url) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/nav/scroll.mbt: -------------------------------------------------------------------------------- 1 | ///| Create a command to scroll the window to a specific position. 2 | pub fn[M] scroll_to_pos(x : Int, y : Int) -> Cmd[M] { 3 | fn(_) { @dom.window().scroll_to(x, y) } 4 | } 5 | 6 | ///| Create a command to scroll the window by a specific amount. 7 | pub fn[M] scroll_by_pos(x : Int, y : Int) -> Cmd[M] { 8 | fn(_) { @dom.window().scroll_by(x, y) } 9 | } 10 | 11 | ///| Create a command to scroll the window to a specific element. 12 | /// The element is specified by its id. 13 | pub fn[M] scroll_to(element : String) -> Cmd[M] { 14 | fn(_) { 15 | match @dom.document().get_element_by_id(element).to_option() { 16 | Some(e) => e.scroll_into_view() 17 | None => () 18 | } 19 | } 20 | } 21 | 22 | ///| Create a command to scroll the window to the top. 23 | pub fn[M] scroll_to_top() -> Cmd[M] { 24 | fn(_) { @dom.window().scroll_to_top() } 25 | } 26 | 27 | ///| Create a command to scroll the window to the bottom. 28 | pub fn[M] scroll_to_bottom() -> Cmd[M] { 29 | fn(_) { @dom.window().scroll_to_bottom() } 30 | } 31 | -------------------------------------------------------------------------------- /src/rabbit-tea.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea" 2 | 3 | import( 4 | "Yoorkin/rabbit-tea/cmd" 5 | "Yoorkin/rabbit-tea/html" 6 | "Yoorkin/rabbit-tea/url" 7 | ) 8 | 9 | // Values 10 | fn[Model, Msg] application(initialize~ : (@url.Url) -> (@cmd.Cmd[Msg], Model), update~ : (Msg, Model) -> (@cmd.Cmd[Msg], Model), view~ : (Model) -> @html.T[Msg], url_changed? : (@url.Url) -> Msg, url_request? : (@url.UrlRequest) -> Msg, mount~ : String = ..) -> Unit 11 | 12 | fn[A, E : Error, M] attempt((Result[A, E]) -> M, () -> A!E) -> @cmd.Cmd[M] 13 | 14 | fn[M] batch(Array[@cmd.Cmd[M]]) -> @cmd.Cmd[M] 15 | 16 | fn[M] none() -> @cmd.Cmd[M] 17 | 18 | fn[A, M] perform((A) -> M, () -> A) -> @cmd.Cmd[M] 19 | 20 | fn[Model, Message] startup(model~ : Model, update~ : (Message, Model) -> (@cmd.Cmd[Message], Model), view~ : (Model) -> @html.T[Message], mount~ : String = ..) -> Unit 21 | 22 | fn[M] task(M) -> @cmd.Cmd[M] 23 | 24 | // Types and methods 25 | 26 | // Type aliases 27 | pub typealias Cmd[M] = @cmd.Cmd[M] 28 | 29 | pub typealias Command[M] = @cmd.Cmd[M] 30 | 31 | // Traits 32 | 33 | -------------------------------------------------------------------------------- /src/top.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | pub typealias @cmd.(Cmd, Command) 3 | 4 | ///| 5 | pub fnalias @cmd.(none, batch, task, perform, attempt) 6 | 7 | ///| Start the application. 8 | /// 9 | /// - `model` is the state of your application. 10 | /// - `view` is a way to turn your model into HTML. 11 | /// - `update` a way to update your state based on messages. 12 | /// 13 | /// These three are the core of the TEA. Rabbit-TEA is highly unstable at this time, 14 | /// but it follows the same pattern as Elm. You can visit https://guide.elm-lang.org/ 15 | /// to get more intuition! 16 | /// 17 | /// To start the application with router, you can use the `application` function. 18 | pub fn[Model, Message] startup( 19 | model~ : Model, 20 | update~ : (Message, Model) -> (Cmd[Message], Model), 21 | view~ : (Model) -> @html.Html[Message], 22 | mount~ : String = "app" 23 | ) -> Unit { 24 | @dom.document() 25 | .get_element_by_id(mount) 26 | .get_exn() 27 | .set_inner_html("
") 28 | let mut sandbox = None 29 | let mut curr_dom = @vdom.node("div", [], []) 30 | fn after_update(html : @html.Html[Message]) { 31 | guard sandbox is Some(sandbox) 32 | let new_dom = html.to_virtual_dom() 33 | new_dom.patch(curr_dom, sandbox, mount~) 34 | curr_dom = new_dom 35 | } 36 | 37 | sandbox = Some(@browser.Sandbox::new(model, update, view, after_update~)) 38 | sandbox.unwrap().refresh() 39 | } 40 | 41 | ///| Start the application with initial URL. 42 | /// 43 | /// - `url_changed` is a message that will be passed when the URL is changed by the navigation API in the `@browser` package. 44 | /// - `url_request` is a message that will be passed when an `` tag is clicked. 45 | /// - `initialize` will be called when the application is started. 46 | /// 47 | pub fn[Model, Msg] application( 48 | initialize~ : (@url.Url) -> (Cmd[Msg], Model), 49 | update~ : (Msg, Model) -> (Cmd[Msg], Model), 50 | view~ : (Model) -> @html.Html[Msg], 51 | url_changed? : (@url.Url) -> Msg, 52 | url_request? : (@url.UrlRequest) -> Msg, 53 | mount~ : String = "app" 54 | ) -> Unit { 55 | @dom.document() 56 | .get_element_by_id(mount) 57 | .get_exn() 58 | .set_inner_html("
") 59 | let mut sandbox_ref = None 60 | let mut curr_dom = @vdom.node("div", [], []) 61 | fn after_update(html : @html.Html[Msg]) { 62 | guard sandbox_ref is Some(sandbox) 63 | let new_dom = html.to_virtual_dom() 64 | new_dom.patch(curr_dom, sandbox, mount~) 65 | curr_dom = new_dom 66 | } 67 | 68 | guard @url.parse?(@dom.window().current_url()) is Ok(url) 69 | let (cmd, model) = initialize(url) 70 | let sandbox = @browser.Sandbox::new( 71 | model, 72 | update, 73 | view, 74 | after_update~, 75 | url_request?, 76 | url_changed?, 77 | ) 78 | sandbox_ref = Some(sandbox) 79 | sandbox..launch(cmd)..refresh() 80 | } 81 | -------------------------------------------------------------------------------- /src/url/moon.pkg.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/url/url.mbt: -------------------------------------------------------------------------------- 1 | ///| Represent the URL change request from the user, trigger by `@html.a` tag. 2 | /// The `Internal` means the target URL is in the same domain. 3 | /// The `External` means the target URL is in another site. 4 | pub(all) enum UrlRequest { 5 | Internal(Url) 6 | External(String) 7 | } derive(Show, Eq, Compare) 8 | 9 | ///| Url 10 | /// 11 | /// ```text 12 | /// https://example.com:8042/over/there?name=ferret#nose 13 | /// \___/ \______________/\_________/ \_________/ \__/ 14 | /// | | | | | 15 | /// scheme authority path query fragment 16 | /// ``` 17 | /// 18 | /// This diagram is from https://package.elm-lang.org/packages/elm/url/latest/Url 19 | pub(all) struct Url { 20 | protocol : Protocol 21 | host : String 22 | port : Int? 23 | path : String 24 | query : String? 25 | fragment : String? 26 | } derive(Show, Eq, Compare) 27 | 28 | ///| 29 | pub(all) enum Protocol { 30 | Http 31 | Https 32 | Other(String) 33 | } derive(Show, Eq, Compare) 34 | 35 | ///| 36 | pub fn to_string(self : Url) -> String { 37 | let protocol = match self.protocol { 38 | Http => "http" 39 | Https => "https" 40 | _ => panic() // TODO: fix this 41 | } 42 | let port = match self.port { 43 | Some(p) => ":\{p}" 44 | None => "" 45 | } 46 | let query = match self.query { 47 | Some(q) => "?\{q}" 48 | None => "" 49 | } 50 | let fragment = match self.fragment { 51 | Some(f) => "#\{f}" 52 | None => "" 53 | } 54 | "\{protocol}://\{self.host}\{port}/\{self.path}\{query}\{fragment}" 55 | } 56 | 57 | ///| 58 | pub fn parse(url : String) -> Url!Error { 59 | let (protocol, remain) = match url.split("://").collect() { 60 | ["http", remain] => (Http, remain) 61 | ["https", remain] => (Https, remain) 62 | [x, remain] => (Other(x.to_string()), remain) 63 | [remain] => (Other(""), remain) 64 | _ => fail!("Invalid protocol") 65 | } 66 | let (mid, query_and_fragment) = match remain.split("?").collect() { 67 | [mid, remain] => (mid, remain) 68 | [mid] => (mid, "") 69 | _ => fail!("Invalid host") 70 | } 71 | let (mid_part, fragment1) = match mid.split("#").collect() { 72 | [mid, fragment] => (mid, Some(fragment)) 73 | [mid] => (mid, None) 74 | _ => fail!("Invalid fragment") 75 | } 76 | let (mid, path) = match mid_part.split("/").collect() { 77 | [mid] => (mid, "") 78 | [mid, .. paths] => 79 | (mid, paths.iter().map(@string.View::to_string).join("/")) 80 | _ => fail!("Invalid host") 81 | } 82 | let (host, port) = match mid.split(":").collect() { 83 | [host, port] => { 84 | let port = try @strconv.parse_int!(port.to_string()) catch { 85 | _ => Option::None 86 | } else { 87 | number => Some(number) 88 | } 89 | (host.to_string(), port) 90 | } 91 | [host] => (host.to_string(), None) 92 | _ => fail!("Invalid host") 93 | } 94 | let (query, fragment2) = match query_and_fragment.split("#").collect() { 95 | [query, fragment] => (Some(query.to_string()), Some(fragment)) 96 | [query] => 97 | if query.is_empty() { 98 | (None, None) 99 | } else { 100 | (Some(query.to_string()), None) 101 | } 102 | [] => (None, None) 103 | _ => fail!("Invalid query") 104 | } 105 | let fragment = match (fragment1, fragment2) { 106 | (Some(f1), Some(f2)) => Some("\{f1}#\{f2}") 107 | (Some(f), None) => Some(f.to_string()) 108 | (None, Some(f)) => Some(f.to_string()) 109 | (None, None) => None 110 | } 111 | { protocol, host, port, path, query, fragment } 112 | } 113 | -------------------------------------------------------------------------------- /src/url/url.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/url" 2 | 3 | // Values 4 | fn parse(String) -> Url! 5 | 6 | fn to_string(Url) -> String 7 | 8 | // Types and methods 9 | pub(all) enum Protocol { 10 | Http 11 | Https 12 | Other(String) 13 | } 14 | impl Compare for Protocol 15 | impl Eq for Protocol 16 | impl Show for Protocol 17 | 18 | pub(all) struct Url { 19 | protocol : Protocol 20 | host : String 21 | port : Int? 22 | path : String 23 | query : String? 24 | fragment : String? 25 | } 26 | fn Url::to_string(Self) -> String 27 | impl Compare for Url 28 | impl Eq for Url 29 | impl Show for Url 30 | 31 | pub(all) enum UrlRequest { 32 | Internal(Url) 33 | External(String) 34 | } 35 | impl Compare for UrlRequest 36 | impl Eq for UrlRequest 37 | impl Show for UrlRequest 38 | 39 | // Type aliases 40 | 41 | // Traits 42 | 43 | -------------------------------------------------------------------------------- /src/url/url_test.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | test "basic_url" { 3 | let a = "http://localhost:5173/abcd" 4 | inspect!( 5 | @url.parse?(a), 6 | content= 7 | #|Ok({protocol: Http, host: "localhost", port: Some(5173), path: "abcd", query: None, fragment: None}) 8 | , 9 | ) 10 | inspect!( 11 | @url.parse?("https://example.com"), 12 | content= 13 | #|Ok({protocol: Https, host: "example.com", port: None, path: "", query: None, fragment: None}) 14 | , 15 | ) 16 | inspect!( 17 | @url.parse?("https://example.com/"), 18 | content= 19 | #|Ok({protocol: Https, host: "example.com", port: None, path: "", query: None, fragment: None}) 20 | , 21 | ) 22 | inspect!( 23 | @url.parse?("https://example.com/path1/path2"), 24 | content= 25 | #|Ok({protocol: Https, host: "example.com", port: None, path: "path1/path2", query: None, fragment: None}) 26 | , 27 | ) 28 | inspect!( 29 | @url.parse?("https://example.com:8080/path/to/somewhere?query#fragment"), 30 | content= 31 | #|Ok({protocol: Https, host: "example.com", port: Some(8080), path: "path/to/somewhere", query: Some("query"), fragment: Some("fragment")}) 32 | , 33 | ) 34 | inspect!( 35 | @url.parse?( 36 | "https://example.com:8080/path/to/somewhere?key=value&key2=value2#fragment", 37 | ), 38 | content= 39 | #|Ok({protocol: Https, host: "example.com", port: Some(8080), path: "path/to/somewhere", query: Some("key=value&key2=value2"), fragment: Some("fragment")}) 40 | , 41 | ) 42 | } 43 | 44 | ///| 45 | test "special url" { 46 | inspect!( 47 | @url.parse?("localhost:8080/abcd"), 48 | content= 49 | #|Ok({protocol: Other(""), host: "localhost", port: Some(8080), path: "abcd", query: None, fragment: None}) 50 | , 51 | ) 52 | } 53 | 54 | ///| 55 | test "no_port" { 56 | inspect!( 57 | @url.parse?("https://example.com/path/to/somewhere?query#fragment"), 58 | content= 59 | #|Ok({protocol: Https, host: "example.com", port: None, path: "path/to/somewhere", query: Some("query"), fragment: Some("fragment")}) 60 | , 61 | ) 62 | } 63 | 64 | ///| 65 | test "no_query" { 66 | inspect!( 67 | @url.parse?("https://example.com:8080/path/to/somewhere#fragment"), 68 | content= 69 | #|Ok({protocol: Https, host: "example.com", port: Some(8080), path: "path/to/somewhere", query: None, fragment: Some("fragment")}) 70 | , 71 | ) 72 | } 73 | 74 | ///| 75 | test "no_fragment" { 76 | inspect!( 77 | @url.parse?("https://example.com:8080/path/to/somewhere?query"), 78 | content= 79 | #|Ok({protocol: Https, host: "example.com", port: Some(8080), path: "path/to/somewhere", query: Some("query"), fragment: None}) 80 | , 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /src/variant/moon.pkg.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/variant/variant.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | pub(all) enum Variant { 3 | Boolean(Bool) 4 | Integer(Int) 5 | Floating(Double) 6 | String(String) 7 | } derive(Show, Eq, Compare, Hash) 8 | -------------------------------------------------------------------------------- /src/variant/variant.mbti: -------------------------------------------------------------------------------- 1 | package "Yoorkin/rabbit-tea/variant" 2 | 3 | // Values 4 | 5 | // Types and methods 6 | pub(all) enum Variant { 7 | Boolean(Bool) 8 | Integer(Int) 9 | Floating(Double) 10 | String(String) 11 | } 12 | impl Compare for Variant 13 | impl Eq for Variant 14 | impl Hash for Variant 15 | impl Show for Variant 16 | 17 | // Type aliases 18 | 19 | // Traits 20 | 21 | --------------------------------------------------------------------------------