├── .github └── workflows │ └── build-and-test.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── dom-bench ├── keyed-comp │ ├── Cargo.toml │ ├── index.html │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── comp.rs │ │ ├── data.rs │ │ └── lib.rs └── keyed │ ├── Cargo.toml │ ├── index.html │ ├── package-lock.json │ ├── package.json │ └── src │ ├── data.rs │ └── lib.rs ├── icon.svg ├── icon_160.png ├── maomi-dom-macro ├── Cargo.toml └── src │ ├── css │ ├── media_cond.rs │ ├── mod.rs │ └── property.rs │ ├── element │ └── mod.rs │ └── lib.rs ├── maomi-dom-template ├── .gitignore ├── Cargo.toml ├── build.rs ├── i18n │ └── zh_CN.toml ├── index.html └── src │ ├── lib.mcss │ └── lib.rs ├── maomi-dom ├── Cargo.toml ├── src │ ├── base_element.rs │ ├── class_list.rs │ ├── composing.rs │ ├── custom_attr.rs │ ├── dynamic_style.rs │ ├── element │ │ ├── content_sectioning.rs │ │ ├── demarcating_edits.rs │ │ ├── forms.rs │ │ ├── inline_text.rs │ │ ├── mod.rs │ │ ├── multimedia.rs │ │ ├── table_content.rs │ │ └── text_content.rs │ ├── event │ │ ├── animation.rs │ │ ├── form.rs │ │ ├── mod.rs │ │ ├── mouse.rs │ │ ├── scroll.rs │ │ ├── tap.rs │ │ ├── touch.rs │ │ ├── transition.rs │ │ └── utils.rs │ ├── lib.rs │ ├── text_node.rs │ └── virtual_element.rs └── tests │ ├── web.rs │ └── web_tests │ ├── component.rs │ ├── event.rs │ ├── mod.rs │ ├── prerendering.rs │ ├── skin.rs │ └── template.rs ├── maomi-macro ├── Cargo.toml └── src │ ├── component.rs │ ├── i18n.rs │ ├── lib.rs │ └── template.rs ├── maomi-skin ├── Cargo.toml └── src │ ├── css_token.rs │ ├── lib.rs │ ├── module.rs │ ├── pseudo.rs │ ├── style_sheet.rs │ └── write_css.rs ├── maomi-tools ├── Cargo.toml └── src │ ├── config.rs │ ├── i18n_format.rs │ └── lib.rs ├── maomi-tree ├── Cargo.toml └── src │ ├── lib.rs │ └── mem.rs └── maomi ├── Cargo.toml └── src ├── backend ├── context.rs └── mod.rs ├── component.rs ├── diff ├── key.rs ├── keyless.rs └── mod.rs ├── error.rs ├── event.rs ├── lib.rs ├── locale_string.rs ├── mount_point.rs ├── node.rs ├── prop.rs ├── template.rs └── text_node.rs /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: build-and-test 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /target 3 | Cargo.lock 4 | **/*.rs.bk 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "maomi", 4 | "maomi-macro", 5 | "maomi-tree", 6 | "maomi-skin", 7 | "maomi-dom", 8 | "maomi-dom-macro", 9 | "maomi-dom-template", 10 | "maomi-tools", 11 | "dom-bench/keyed", 12 | "dom-bench/keyed-comp", 13 | ] 14 | resolver = "2" 15 | 16 | [profile.release] 17 | opt-level = "z" 18 | lto = true 19 | codegen-units = 1 20 | 21 | [patch.crates-io] 22 | "maomi" = { path = "./maomi" } 23 | "maomi-dom" = { path = "./maomi-dom" } 24 | "maomi-dom-macro" = { path = "./maomi-dom-macro" } 25 | "maomi-macro" = { path = "./maomi-macro" } 26 | "maomi-skin" = { path = "./maomi-skin" } 27 | "maomi-tools" = { path = "./maomi-tools" } 28 | "maomi-tree" = { path = "./maomi-tree" } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 LastLeaf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![maomi](icon_160.png) 2 | 3 | # maomi 4 | 5 | Strict and Performant Web Application Programming 6 | 7 | ![crates.io](https://img.shields.io/crates/v/maomi?style=flat-square) ![docs.rs](https://img.shields.io/docsrs/maomi?style=flat-square) ![build-status](https://img.shields.io/github/actions/workflow/status/lastleaf/maomi/build-and-test.yml?style=flat-square) 8 | 9 | ```rust 10 | #[component] 11 | struct HelloWorld { 12 | template: template! { 13 | "Hello world!" 14 | } 15 | } 16 | ``` 17 | 18 | ## Key Features 19 | 20 | * Write rust code, compile to WebAssembly, and run in browser. 21 | * Great overall performance and no common performance pitfalls. 22 | * Reports mistakes while compilation. 23 | * With rust-analyzer installed, easier to investigate elements, properties, and even style classes. 24 | * Based on templates and data bindings. 25 | * Limited stylesheet syntax, easier to investigate. 26 | * High performance server side rendering. 27 | * I18n in the core design. 28 | 29 | Checkout the [website](http://lastleaf.cn/maomi/en_US) for details. 30 | 31 | 去 [中文版站点](http://lastleaf.cn/maomi/zh_CN) 了解详情。 32 | 33 | ## Examples 34 | 35 | See [dom-template](./maomi-dom-template/) for the basic example. Compile with: 36 | 37 | ```sh 38 | wasm-pack build maomi-dom-template --target no-modules 39 | ``` 40 | 41 | ## Run Tests 42 | 43 | General rust tests and wasm-pack tests are both needed. 44 | 45 | ```sh 46 | cargo test 47 | wasm-pack test --firefox maomi-dom # or --chrome 48 | ``` 49 | -------------------------------------------------------------------------------- /dom-bench/keyed-comp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maomi-dom-bench-keyed-comp" 3 | version = "0.5.0" 4 | authors = ["LastLeaf "] 5 | license = "MIT" 6 | description = "Strict and Performant Web Application Programming" 7 | homepage = "https://github.com/LastLeaf/maomi" 8 | documentation = "https://github.com/LastLeaf/maomi" 9 | repository = "https://github.com/LastLeaf/maomi" 10 | edition = "2021" 11 | 12 | [lib] 13 | crate-type = ["cdylib", "rlib"] 14 | 15 | [dependencies] 16 | maomi = "=0.5.0" 17 | maomi-dom = "=0.5.0" 18 | log = "0.4" 19 | env_logger = "0.9" 20 | console_log = { version = "0.2", features = ["color"] } 21 | console_error_panic_hook = "0.1" 22 | wasm-bindgen = "0.2" 23 | getrandom = { version = "0.2.7", features = ["js"] } 24 | 25 | [package.metadata.wasm-pack.profile.release] 26 | wasm-opt = ['-O4', '-g'] 27 | -------------------------------------------------------------------------------- /dom-bench/keyed-comp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /dom-bench/keyed-comp/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-framework-benchmark-maomi", 3 | "version": "0.0.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "js-framework-benchmark-maomi", 9 | "version": "0.0.1" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /dom-bench/keyed-comp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "js-framework-benchmark-maomi", 4 | "version": "0.0.1", 5 | "main": "index.js", 6 | "js-framework-benchmark": { 7 | "frameworkVersion": "" 8 | }, 9 | "scripts": { 10 | "build-prod": "wasm-pack build --target no-modules" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /dom-bench/keyed-comp/src/comp.rs: -------------------------------------------------------------------------------- 1 | use maomi::prelude::*; 2 | use maomi_dom::class_list::DomExternalClasses; 3 | use maomi_dom::{async_task, dom_define_attribute, element::*, event::*, DomBackend}; 4 | 5 | dom_define_attribute!(aria_hidden); 6 | 7 | #[component(Backend = DomBackend)] 8 | pub(crate) struct Div { 9 | template: template! { 10 |
13 | }, 14 | pub(crate) class: DomExternalClasses, 15 | } 16 | 17 | impl Component for Div { 18 | fn new() -> Self { 19 | Self { 20 | template: Default::default(), 21 | class: DomExternalClasses::new(), 22 | } 23 | } 24 | } 25 | 26 | #[component(Backend = DomBackend)] 27 | pub(crate) struct Span { 28 | template: template! { 29 | 34 | }, 35 | pub(crate) class: DomExternalClasses, 36 | pub(crate) aria_hidden: Prop, 37 | pub(crate) click: Event, 38 | } 39 | 40 | impl Component for Span { 41 | fn new() -> Self { 42 | Self { 43 | template: Default::default(), 44 | class: DomExternalClasses::new(), 45 | aria_hidden: Prop::new(String::new()), 46 | click: Default::default(), 47 | } 48 | } 49 | } 50 | 51 | impl Span { 52 | fn click(this: ComponentEvent) { 53 | let mut detail = this.clone_detail(); 54 | let this = this.rc(); 55 | async_task(async move { 56 | this.get(move |this| { 57 | this.click.trigger(&mut detail); 58 | }) 59 | .await; 60 | }); 61 | } 62 | } 63 | 64 | #[component(Backend = DomBackend)] 65 | pub(crate) struct H1 { 66 | template: template! { 67 |

70 | }, 71 | pub(crate) class: DomExternalClasses, 72 | } 73 | 74 | impl Component for H1 { 75 | fn new() -> Self { 76 | Self { 77 | template: Default::default(), 78 | class: DomExternalClasses::new(), 79 | } 80 | } 81 | } 82 | 83 | #[component(Backend = DomBackend)] 84 | pub(crate) struct Button { 85 | template: template! { 86 | 92 | }, 93 | pub(crate) class: DomExternalClasses, 94 | pub(crate) r#type: Prop, 95 | pub(crate) id: Prop, 96 | pub(crate) tap: Event, 97 | } 98 | 99 | impl Component for Button { 100 | fn new() -> Self { 101 | Self { 102 | template: Default::default(), 103 | class: DomExternalClasses::new(), 104 | r#type: Prop::new(String::new()), 105 | id: Prop::new(String::new()), 106 | tap: Default::default(), 107 | } 108 | } 109 | } 110 | 111 | impl Button { 112 | fn tap(this: ComponentEvent) { 113 | let mut detail = this.clone_detail(); 114 | let this = this.rc(); 115 | async_task(async move { 116 | this.get(move |this| { 117 | this.tap.trigger(&mut detail); 118 | }) 119 | .await; 120 | }); 121 | } 122 | } 123 | 124 | #[component(Backend = DomBackend)] 125 | pub(crate) struct A { 126 | template: template! { 127 | 131 | }, 132 | pub(crate) class: DomExternalClasses, 133 | pub(crate) tap: Event, 134 | } 135 | 136 | impl Component for A { 137 | fn new() -> Self { 138 | Self { 139 | template: Default::default(), 140 | class: DomExternalClasses::new(), 141 | tap: Default::default(), 142 | } 143 | } 144 | } 145 | 146 | impl A { 147 | fn tap(this: ComponentEvent) { 148 | let mut detail = this.clone_detail(); 149 | let this = this.rc(); 150 | async_task(async move { 151 | this.get(move |this| { 152 | this.tap.trigger(&mut detail); 153 | }) 154 | .await; 155 | }); 156 | } 157 | } 158 | 159 | #[component(Backend = DomBackend)] 160 | pub(crate) struct Table { 161 | template: template! { 162 |
165 | }, 166 | pub(crate) class: DomExternalClasses, 167 | } 168 | 169 | impl Component for Table { 170 | fn new() -> Self { 171 | Self { 172 | template: Default::default(), 173 | class: DomExternalClasses::new(), 174 | } 175 | } 176 | } 177 | 178 | #[component(Backend = DomBackend)] 179 | pub(crate) struct Tbody { 180 | template: template! { 181 | 184 | }, 185 | pub(crate) class: DomExternalClasses, 186 | } 187 | 188 | impl Component for Tbody { 189 | fn new() -> Self { 190 | Self { 191 | template: Default::default(), 192 | class: DomExternalClasses::new(), 193 | } 194 | } 195 | } 196 | 197 | #[component(Backend = DomBackend)] 198 | pub(crate) struct Tr { 199 | template: template! { 200 | 203 | }, 204 | pub(crate) class: DomExternalClasses, 205 | } 206 | 207 | impl Component for Tr { 208 | fn new() -> Self { 209 | Self { 210 | template: Default::default(), 211 | class: DomExternalClasses::new(), 212 | } 213 | } 214 | } 215 | 216 | #[component(Backend = DomBackend)] 217 | pub(crate) struct Td { 218 | template: template! { 219 | 222 | }, 223 | pub(crate) class: DomExternalClasses, 224 | } 225 | 226 | impl Component for Td { 227 | fn new() -> Self { 228 | Self { 229 | template: Default::default(), 230 | class: DomExternalClasses::new(), 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /dom-bench/keyed-comp/src/data.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | 3 | use super::TableRow; 4 | 5 | thread_local! { 6 | static ID: Cell = Cell::new(0); 7 | } 8 | 9 | fn gen_id() -> usize { 10 | ID.with(|x| { 11 | let ret = x.get() + 1; 12 | x.set(ret); 13 | ret 14 | }) 15 | } 16 | 17 | fn random_member<'a>(list: &'a [&'static str]) -> &'a str { 18 | let mut n: [u8; 1] = [0]; 19 | getrandom::getrandom(&mut n).unwrap(); 20 | list[(n[0] as usize) % list.len()] 21 | } 22 | 23 | pub(crate) fn build(count: usize) -> Vec { 24 | let adjectives = [ 25 | "pretty", 26 | "large", 27 | "big", 28 | "small", 29 | "tall", 30 | "short", 31 | "long", 32 | "handsome", 33 | "plain", 34 | "quaint", 35 | "clean", 36 | "elegant", 37 | "easy", 38 | "angry", 39 | "crazy", 40 | "helpful", 41 | "mushy", 42 | "odd", 43 | "unsightly", 44 | "adorable", 45 | "important", 46 | "inexpensive", 47 | "cheap", 48 | "expensive", 49 | "fancy", 50 | ]; 51 | let colours = [ 52 | "red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", 53 | "orange", 54 | ]; 55 | let nouns = [ 56 | "table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", 57 | "pizza", "mouse", "keyboard", 58 | ]; 59 | let mut ret = Vec::with_capacity(count); 60 | for _ in 0..count { 61 | ret.push(TableRow { 62 | id: gen_id(), 63 | label: format!( 64 | "{} {} {}", 65 | random_member(&adjectives), 66 | random_member(&colours), 67 | random_member(&nouns) 68 | ), 69 | }); 70 | } 71 | ret 72 | } 73 | -------------------------------------------------------------------------------- /dom-bench/keyed-comp/src/lib.rs: -------------------------------------------------------------------------------- 1 | use maomi::{prelude::*, BackendContext}; 2 | use maomi_dom::{event::*, prelude::*, DomBackend}; 3 | use wasm_bindgen::prelude::*; 4 | 5 | mod comp; 6 | mod data; 7 | use comp::*; 8 | 9 | stylesheet! { 10 | #[css_name("jumbotron")] 11 | class jumbotron {} 12 | #[css_name("row")] 13 | class row {} 14 | #[css_name("col-md-1")] 15 | class col_md_1 {} 16 | #[css_name("col-md-4")] 17 | class col_md_4 {} 18 | #[css_name("col-md-6")] 19 | class col_md_6 {} 20 | #[css_name("col-sm-6")] 21 | class col_sm_6 {} 22 | #[css_name("smallpad")] 23 | class smallpad {} 24 | #[css_name("btn")] 25 | class btn {} 26 | #[css_name("btn-primary")] 27 | class btn_primary {} 28 | #[css_name("btn-block")] 29 | class btn_block {} 30 | #[css_name("table")] 31 | class table {} 32 | #[css_name("table-hover")] 33 | class table_hover {} 34 | #[css_name("table-striped")] 35 | class table_striped {} 36 | #[css_name("test-data")] 37 | class test_data {} 38 | #[css_name("danger")] 39 | class danger {} 40 | #[css_name("glyphicon")] 41 | class glyphicon {} 42 | #[css_name("glyphicon-remove")] 43 | class glyphicon_remove {} 44 | #[css_name("preloadicon")] 45 | class preloadicon {} 46 | } 47 | 48 | #[component(Backend = DomBackend)] 49 | struct HelloWorld { 50 | template: template! { 51 |
52 |
53 |
54 |

"maomi (keyed, wrapped)"

55 |
56 |
57 |
58 |
59 | 69 |
70 |
71 | 81 |
82 |
83 | 93 |
94 |
95 | 105 |
106 |
107 | 117 |
118 |
119 | 129 |
130 |
131 |
132 |
133 |
134 | 135 | 136 | for item in self.rows.iter() use usize { 137 | 140 | 141 | 144 | 149 | 150 | 151 | } 152 | 153 |
{ &format!("{}", item.id) } 142 | { &item.label } 143 | 145 | 146 | 147 | 148 |
154 | 158 | }, 159 | rows: Vec, 160 | selected: usize, 161 | } 162 | 163 | #[derive(Debug, Clone)] 164 | struct TableRow { 165 | id: usize, 166 | label: String, 167 | } 168 | 169 | impl AsListKey for TableRow { 170 | type ListKey = usize; 171 | 172 | fn as_list_key(&self) -> &Self::ListKey { 173 | &self.id 174 | } 175 | } 176 | 177 | // implement basic component interfaces 178 | impl Component for HelloWorld { 179 | fn new() -> Self { 180 | Self { 181 | template: Default::default(), 182 | rows: vec![], 183 | selected: std::usize::MAX, 184 | } 185 | } 186 | } 187 | 188 | impl HelloWorld { 189 | fn add(this: ComponentEvent) { 190 | this.task(|this| { 191 | this.rows.append(&mut data::build(1000)); 192 | }); 193 | } 194 | 195 | fn remove(this: ComponentEvent, id: &usize) { 196 | let id = *id; 197 | this.task(move |this| { 198 | let index = this.rows.iter().position(|x| x.id == id).unwrap(); 199 | this.rows.remove(index); 200 | }); 201 | } 202 | 203 | fn select(this: ComponentEvent, id: &usize) { 204 | let id = *id; 205 | this.task(move |this| { 206 | this.selected = id; 207 | }); 208 | } 209 | 210 | fn run(this: ComponentEvent) { 211 | this.task(|this| { 212 | this.rows = data::build(1000); 213 | this.selected = usize::MAX; 214 | }); 215 | } 216 | 217 | fn update(this: ComponentEvent) { 218 | this.task(|this| { 219 | let mut i = 0; 220 | while i < this.rows.len() { 221 | this.rows[i].label += " !!!"; 222 | i += 10; 223 | } 224 | }); 225 | } 226 | 227 | fn run_lots(this: ComponentEvent) { 228 | this.task(|this| { 229 | this.rows = data::build(10000); 230 | this.selected = usize::MAX; 231 | }); 232 | } 233 | 234 | fn clear(this: ComponentEvent) { 235 | this.task(|this| { 236 | this.rows = Vec::with_capacity(0); 237 | this.selected = usize::MAX; 238 | }); 239 | } 240 | 241 | fn swap_rows(this: ComponentEvent) { 242 | this.task(|this| { 243 | let rows = &mut this.rows; 244 | if rows.len() > 998 { 245 | let r998 = rows[998].clone(); 246 | rows[998] = std::mem::replace(&mut rows[1], r998); 247 | } 248 | }); 249 | } 250 | } 251 | 252 | #[wasm_bindgen(start)] 253 | pub fn wasm_main() { 254 | // init logger 255 | console_error_panic_hook::set_once(); 256 | console_log::init_with_level(log::Level::Warn).unwrap(); 257 | 258 | // init a backend context 259 | let dom_backend = DomBackend::new_with_element_id("main").unwrap(); 260 | let backend_context = BackendContext::new(dom_backend); 261 | 262 | // create a mount point 263 | backend_context 264 | .enter_sync(move |ctx| { 265 | let mount_point = ctx.attach(|_: &mut HelloWorld| {}).unwrap(); 266 | // leak the mount point, so that event callbacks still work 267 | Box::leak(Box::new(mount_point)); 268 | }) 269 | .map_err(|_| "Cannot init mount point") 270 | .unwrap(); 271 | 272 | // leak the backend context, so that event callbacks still work 273 | Box::leak(Box::new(backend_context)); 274 | } 275 | -------------------------------------------------------------------------------- /dom-bench/keyed/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maomi-dom-bench-keyed" 3 | version = "0.5.0" 4 | authors = ["LastLeaf "] 5 | license = "MIT" 6 | description = "Strict and Performant Web Application Programming" 7 | homepage = "https://github.com/LastLeaf/maomi" 8 | documentation = "https://github.com/LastLeaf/maomi" 9 | repository = "https://github.com/LastLeaf/maomi" 10 | edition = "2021" 11 | 12 | [lib] 13 | crate-type = ["cdylib", "rlib"] 14 | 15 | [dependencies] 16 | maomi = "=0.5.0" 17 | maomi-dom = "=0.5.0" 18 | log = "0.4" 19 | env_logger = "0.9" 20 | console_log = { version = "0.2", features = ["color"] } 21 | console_error_panic_hook = "0.1" 22 | wasm-bindgen = "0.2" 23 | getrandom = { version = "0.2.7", features = ["js"] } 24 | 25 | [package.metadata.wasm-pack.profile.release] 26 | wasm-opt = ['-O4', '-g'] 27 | -------------------------------------------------------------------------------- /dom-bench/keyed/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /dom-bench/keyed/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-framework-benchmark-maomi", 3 | "version": "0.0.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "js-framework-benchmark-maomi", 9 | "version": "0.0.1" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /dom-bench/keyed/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "js-framework-benchmark-maomi", 4 | "version": "0.0.1", 5 | "main": "index.js", 6 | "js-framework-benchmark": { 7 | "frameworkVersion": "" 8 | }, 9 | "scripts": { 10 | "build-prod": "wasm-pack build --target no-modules" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /dom-bench/keyed/src/data.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | 3 | use super::TableRow; 4 | 5 | thread_local! { 6 | static ID: Cell = Cell::new(0); 7 | } 8 | 9 | fn gen_id() -> usize { 10 | ID.with(|x| { 11 | let ret = x.get() + 1; 12 | x.set(ret); 13 | ret 14 | }) 15 | } 16 | 17 | fn random_member<'a>(list: &'a [&'static str]) -> &'a str { 18 | let mut n: [u8; 1] = [0]; 19 | getrandom::getrandom(&mut n).unwrap(); 20 | list[(n[0] as usize) % list.len()] 21 | } 22 | 23 | pub(crate) fn build(count: usize) -> Vec { 24 | let adjectives = [ 25 | "pretty", 26 | "large", 27 | "big", 28 | "small", 29 | "tall", 30 | "short", 31 | "long", 32 | "handsome", 33 | "plain", 34 | "quaint", 35 | "clean", 36 | "elegant", 37 | "easy", 38 | "angry", 39 | "crazy", 40 | "helpful", 41 | "mushy", 42 | "odd", 43 | "unsightly", 44 | "adorable", 45 | "important", 46 | "inexpensive", 47 | "cheap", 48 | "expensive", 49 | "fancy", 50 | ]; 51 | let colours = [ 52 | "red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", 53 | "orange", 54 | ]; 55 | let nouns = [ 56 | "table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", 57 | "pizza", "mouse", "keyboard", 58 | ]; 59 | let mut ret = Vec::with_capacity(count); 60 | for _ in 0..count { 61 | ret.push(TableRow { 62 | id: gen_id(), 63 | label: format!( 64 | "{} {} {}", 65 | random_member(&adjectives), 66 | random_member(&colours), 67 | random_member(&nouns) 68 | ), 69 | }); 70 | } 71 | ret 72 | } 73 | -------------------------------------------------------------------------------- /dom-bench/keyed/src/lib.rs: -------------------------------------------------------------------------------- 1 | use maomi::{prelude::*, BackendContext}; 2 | use maomi_dom::element::table as table_elem; 3 | use maomi_dom::{element::*, event::*, prelude::*, DomBackend}; 4 | use wasm_bindgen::prelude::*; 5 | 6 | mod data; 7 | 8 | maomi_dom::dom_define_attribute!(aria_hidden); 9 | 10 | stylesheet! { 11 | #[css_name("jumbotron")] 12 | class jumbotron {} 13 | #[css_name("row")] 14 | class row {} 15 | #[css_name("col-md-1")] 16 | class col_md_1 {} 17 | #[css_name("col-md-4")] 18 | class col_md_4 {} 19 | #[css_name("col-md-6")] 20 | class col_md_6 {} 21 | #[css_name("col-sm-6")] 22 | class col_sm_6 {} 23 | #[css_name("smallpad")] 24 | class smallpad {} 25 | #[css_name("btn")] 26 | class btn {} 27 | #[css_name("btn-primary")] 28 | class btn_primary {} 29 | #[css_name("btn-block")] 30 | class btn_block {} 31 | #[css_name("table")] 32 | class table {} 33 | #[css_name("table-hover")] 34 | class table_hover {} 35 | #[css_name("table-striped")] 36 | class table_striped {} 37 | #[css_name("test-data")] 38 | class test_data {} 39 | #[css_name("danger")] 40 | class danger {} 41 | #[css_name("glyphicon")] 42 | class glyphicon {} 43 | #[css_name("glyphicon-remove")] 44 | class glyphicon_remove {} 45 | #[css_name("preloadicon")] 46 | class preloadicon {} 47 | } 48 | 49 | #[component(Backend = DomBackend)] 50 | struct HelloWorld { 51 | template: template! { 52 |
53 |
54 |
55 |

"maomi (keyed)"

56 |
57 |
58 |
59 |
60 | 70 |
71 |
72 | 82 |
83 |
84 | 94 |
95 |
96 | 106 |
107 |
108 | 118 |
119 |
120 | 130 |
131 |
132 |
133 |
134 |
135 | 136 | 137 | for item in self.rows.iter() use usize { 138 | 141 | { &format!("{}", item.id) } 142 | 143 | { &item.label } 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | } 153 | 154 | 155 | 159 | }, 160 | rows: Vec, 161 | selected: usize, 162 | } 163 | 164 | #[derive(Debug, Clone)] 165 | struct TableRow { 166 | id: usize, 167 | label: String, 168 | } 169 | 170 | impl AsListKey for TableRow { 171 | type ListKey = usize; 172 | 173 | fn as_list_key(&self) -> &Self::ListKey { 174 | &self.id 175 | } 176 | } 177 | 178 | // implement basic component interfaces 179 | impl Component for HelloWorld { 180 | fn new() -> Self { 181 | Self { 182 | template: Default::default(), 183 | rows: vec![], 184 | selected: std::usize::MAX, 185 | } 186 | } 187 | } 188 | 189 | impl HelloWorld { 190 | fn add(this: ComponentEvent) { 191 | this.task(|this| { 192 | this.rows.append(&mut data::build(1000)); 193 | }); 194 | } 195 | 196 | fn remove(this: ComponentEvent, id: &usize) { 197 | let id = *id; 198 | this.task(move |this| { 199 | let index = this.rows.iter().position(|x| x.id == id).unwrap(); 200 | this.rows.remove(index); 201 | }); 202 | } 203 | 204 | fn select(this: ComponentEvent, id: &usize) { 205 | let id = *id; 206 | this.task(move |this| { 207 | this.selected = id; 208 | }); 209 | } 210 | 211 | fn run(this: ComponentEvent) { 212 | this.task(|this| { 213 | this.rows = data::build(1000); 214 | this.selected = usize::MAX; 215 | }); 216 | } 217 | 218 | fn update(this: ComponentEvent) { 219 | this.task(|this| { 220 | let mut i = 0; 221 | while i < this.rows.len() { 222 | this.rows[i].label += " !!!"; 223 | i += 10; 224 | } 225 | }); 226 | } 227 | 228 | fn run_lots(this: ComponentEvent) { 229 | this.task(|this| { 230 | this.rows = data::build(10000); 231 | this.selected = usize::MAX; 232 | }); 233 | } 234 | 235 | fn clear(this: ComponentEvent) { 236 | this.task(|this| { 237 | this.rows = Vec::with_capacity(0); 238 | this.selected = usize::MAX; 239 | }); 240 | } 241 | 242 | fn swap_rows(this: ComponentEvent) { 243 | this.task(|this| { 244 | let rows = &mut this.rows; 245 | if rows.len() > 998 { 246 | let r998 = rows[998].clone(); 247 | rows[998] = std::mem::replace(&mut rows[1], r998); 248 | } 249 | }); 250 | } 251 | } 252 | 253 | #[wasm_bindgen(start)] 254 | pub fn wasm_main() { 255 | // init logger 256 | console_error_panic_hook::set_once(); 257 | console_log::init_with_level(log::Level::Warn).unwrap(); 258 | 259 | // init a backend context 260 | let dom_backend = DomBackend::new_with_element_id("main").unwrap(); 261 | let backend_context = BackendContext::new(dom_backend); 262 | 263 | // create a mount point 264 | backend_context 265 | .enter_sync(move |ctx| { 266 | let mount_point = ctx.attach(|_: &mut HelloWorld| {}).unwrap(); 267 | // leak the mount point, so that event callbacks still work 268 | Box::leak(Box::new(mount_point)); 269 | }) 270 | .map_err(|_| "Cannot init mount point") 271 | .unwrap(); 272 | 273 | // leak the backend context, so that event callbacks still work 274 | Box::leak(Box::new(backend_context)); 275 | } 276 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 48 | 50 | 54 | 61 | 68 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /icon_160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LastLeaf/maomi/1b2ad020cf549fdadaddade1c042784706d2c7d3/icon_160.png -------------------------------------------------------------------------------- /maomi-dom-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maomi-dom-macro" 3 | version = "0.5.0" 4 | authors = ["LastLeaf "] 5 | license = "MIT" 6 | description = "Strict and Performant Web Application Programming" 7 | homepage = "https://github.com/LastLeaf/maomi" 8 | documentation = "https://github.com/LastLeaf/maomi" 9 | repository = "https://github.com/LastLeaf/maomi" 10 | edition = "2021" 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | maomi-tools = "=0.5.0" 17 | maomi-skin = "=0.5.0" 18 | proc-macro2 = { version = "1.0", features = ["span-locations"] } 19 | syn = { version = "1.0", features = ["parsing"] } 20 | quote = "1.0" 21 | once_cell = "1.13" 22 | 23 | [dev-dependencies] 24 | serial_test = "0.9" 25 | -------------------------------------------------------------------------------- /maomi-dom-macro/src/css/property.rs: -------------------------------------------------------------------------------- 1 | use maomi_skin::css_token::*; 2 | use maomi_skin::style_sheet::*; 3 | use maomi_skin::write_css::*; 4 | use maomi_skin::ParseError; 5 | use maomi_skin::VarDynValue; 6 | 7 | pub(crate) struct DomCssProperty { 8 | // TODO really parse the value 9 | inner: Vec, 10 | } 11 | 12 | impl ParseStyleSheetValue for DomCssProperty { 13 | fn parse_value(_: &CssIdent, tokens: &mut CssTokenStream) -> Result { 14 | let mut v = vec![]; 15 | while tokens.peek().is_ok() { 16 | v.push(tokens.next().unwrap()) 17 | } 18 | Ok(Self { inner: v }) 19 | } 20 | } 21 | 22 | impl WriteCss for DomCssProperty { 23 | fn write_css_with_args( 24 | &self, 25 | cssw: &mut CssWriter, 26 | values: &[VarDynValue], 27 | ) -> std::fmt::Result { 28 | for token in &self.inner { 29 | token.write_css_with_args(cssw, values)?; 30 | } 31 | Ok(()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /maomi-dom-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "128"] 2 | 3 | use maomi_skin::style_sheet::StyleSheet; 4 | use proc_macro::TokenStream; 5 | 6 | mod css; 7 | use css::DomStyleSheet; 8 | mod element; 9 | use element::{DomDefineAttribute, DomElementDefinition, DomElementDefinitionAttribute}; 10 | 11 | #[proc_macro] 12 | pub fn stylesheet(item: TokenStream) -> TokenStream { 13 | let ss = syn::parse_macro_input!(item as StyleSheet); 14 | quote::quote! { 15 | #ss 16 | } 17 | .into() 18 | } 19 | 20 | #[proc_macro_attribute] 21 | pub fn dom_element_definition(attr: TokenStream, item: TokenStream) -> TokenStream { 22 | let _ = syn::parse_macro_input!(attr as DomElementDefinitionAttribute); 23 | let def = syn::parse_macro_input!(item as DomElementDefinition); 24 | quote::quote! { 25 | #def 26 | } 27 | .into() 28 | } 29 | 30 | /// Define a custom DOM attribute. 31 | /// 32 | /// It can be used in `attr:xxx=""` syntax. 33 | #[proc_macro] 34 | pub fn dom_define_attribute(item: TokenStream) -> TokenStream { 35 | let def = syn::parse_macro_input!(item as DomDefineAttribute); 36 | quote::quote! { 37 | #def 38 | } 39 | .into() 40 | } 41 | -------------------------------------------------------------------------------- /maomi-dom-template/.gitignore: -------------------------------------------------------------------------------- 1 | /pkg 2 | -------------------------------------------------------------------------------- /maomi-dom-template/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maomi-dom-template" 3 | version = "0.5.0" 4 | authors = ["LastLeaf "] 5 | license = "MIT" 6 | description = "Strict and Performant Web Application Programming" 7 | homepage = "https://github.com/LastLeaf/maomi" 8 | documentation = "https://github.com/LastLeaf/maomi" 9 | repository = "https://github.com/LastLeaf/maomi" 10 | edition = "2021" 11 | 12 | [lib] 13 | crate-type = ["cdylib", "rlib"] 14 | 15 | [dependencies] 16 | maomi = "=0.5.0" 17 | maomi-dom = "=0.5.0" 18 | log = "0.4" 19 | env_logger = "0.9" 20 | console_log = "0.2" 21 | console_error_panic_hook = "0.1" 22 | wasm-bindgen = "0.2" 23 | 24 | [package.metadata.wasm-pack.profile.release] 25 | wasm-opt = ["-Oz"] 26 | 27 | [package.metadata.maomi] 28 | css-out-dir = "pkg" # the location of CSS output (can be overrided by `MAOMI_CSS_OUT_DIR` environment variable) 29 | css-out-mode = "debug" # the location of CSS output (can be overrided by `MAOMI_CSS_OUT_MODE` environment variable) 30 | stylesheet-mod-root = "src/lib.mcss" 31 | i18n-dir = "i18n" 32 | -------------------------------------------------------------------------------- /maomi-dom-template/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // rerun if locale changes 3 | println!("cargo:rerun-if-env-changed=MAOMI_I18N_LOCALE"); 4 | } 5 | -------------------------------------------------------------------------------- /maomi-dom-template/i18n/zh_CN.toml: -------------------------------------------------------------------------------- 1 | [translation] 2 | "Hello world!" = "你好,世界!" 3 | "WARN" = "警告" 4 | "transparent text" = "半透明文本" 5 | "Click me!" = "点击我!" 6 | "Hello world again!" = "你好啊,世界!" 7 | -------------------------------------------------------------------------------- /maomi-dom-template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /maomi-dom-template/src/lib.mcss: -------------------------------------------------------------------------------- 1 | // the crate root stylesheet 2 | 3 | pub(crate) const A: value = 1; 4 | -------------------------------------------------------------------------------- /maomi-dom-template/src/lib.rs: -------------------------------------------------------------------------------- 1 | // import WASM support 2 | use wasm_bindgen::prelude::*; 3 | // import maomi core module 4 | use maomi::{ 5 | locale_string::{LocaleString, ToLocaleStr}, 6 | prelude::*, 7 | BackendContext, 8 | }; 9 | // using DOM backend 10 | use maomi_dom::{element::*, event::*, prelude::*, DomBackend}; 11 | 12 | stylesheet! { 13 | use crate::*; 14 | 15 | // declare a class 16 | class warn { 17 | color = orange; 18 | } 19 | 20 | // declare a dynamic style 21 | style opacity(alpha: f32) { 22 | opacity = alpha; 23 | } 24 | } 25 | 26 | stylesheet! { 27 | class error {} 28 | } 29 | 30 | #[component(Backend = DomBackend)] 31 | struct MyComponent { 32 | template: template! { 33 | 34 |
35 | }, 36 | } 37 | 38 | // declare a component 39 | #[component(Backend = DomBackend)] 40 | struct HelloWorld { 41 | // a component should have a template field 42 | template: template! { 43 | // the template is XML-like 44 |
45 | // text in the template must be quoted 46 | "Hello world!" 47 |
48 | // use { ... } bindings in the template 49 |
50 | { &self.hello } 51 |
52 | // use classes in `class:xxx` form 53 |
"WARN"
54 | // use dynamic style in `style:xxx` form 55 |
"transparent text"
56 | // bind event with `@xxx()` 57 | if !self.clicked { 58 |
"Click me!"
59 | } 60 | }, 61 | hello: LocaleString, 62 | clicked: bool, 63 | } 64 | 65 | // implement basic component interfaces 66 | impl Component for HelloWorld { 67 | fn new() -> Self { 68 | Self { 69 | template: Default::default(), 70 | hello: i18n!("Hello world again!").to_locale_string(), 71 | clicked: false, 72 | } 73 | } 74 | } 75 | 76 | impl HelloWorld { 77 | // an event handler 78 | fn handle_tap(this: ComponentEvent) { 79 | log::info!("Clicked!"); 80 | this.task(|this| { 81 | this.clicked = true; 82 | }); 83 | } 84 | } 85 | 86 | #[wasm_bindgen(start)] 87 | pub fn wasm_main() { 88 | // init logger 89 | console_error_panic_hook::set_once(); 90 | console_log::init_with_level(log::Level::Trace).unwrap(); 91 | 92 | // init a backend context 93 | let dom_backend = DomBackend::new_with_document_body().unwrap(); 94 | let backend_context = BackendContext::new(dom_backend); 95 | 96 | // create a mount point 97 | let mount_point = backend_context 98 | .enter_sync(move |ctx| ctx.attach(|_: &mut HelloWorld| {})) 99 | .map_err(|_| "Cannot init mount point") 100 | .unwrap(); 101 | 102 | // leak the backend context, so that event callbacks still work 103 | std::mem::forget(mount_point); 104 | std::mem::forget(backend_context); 105 | } 106 | -------------------------------------------------------------------------------- /maomi-dom/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maomi-dom" 3 | version = "0.5.0" 4 | authors = ["LastLeaf "] 5 | license = "MIT" 6 | description = "Strict and Performant Web Application Programming" 7 | homepage = "https://github.com/LastLeaf/maomi" 8 | documentation = "https://github.com/LastLeaf/maomi" 9 | repository = "https://github.com/LastLeaf/maomi" 10 | edition = "2021" 11 | 12 | [features] 13 | default = [] 14 | prerendering = ["maomi/prerendering", "html-escape"] 15 | prerendering-apply = ["maomi/prerendering-apply"] 16 | all = ["prerendering", "prerendering-apply"] 17 | 18 | [dependencies] 19 | maomi = "=0.5.0" 20 | maomi-dom-macro = "=0.5.0" 21 | log = "0.4" 22 | js-sys = "0.3" 23 | wasm-bindgen = "0.2" 24 | wasm-bindgen-futures = "0.4" 25 | html-escape = { version = "0.2", optional = true } 26 | 27 | [dependencies.web-sys] 28 | version = "0.3" 29 | features = [ 30 | "Window", 31 | "Document", 32 | "HtmlElement", 33 | "Node", 34 | "NodeList", 35 | "DocumentFragment", 36 | "Element", 37 | "Text", 38 | "DomTokenList", 39 | "CssStyleDeclaration", 40 | "EventListener", 41 | "EventTarget", 42 | "Event", 43 | "EventInit", 44 | "MouseEvent", 45 | "TouchEvent", 46 | "TouchList", 47 | "Touch", 48 | "KeyboardEvent", 49 | "InputEvent", 50 | "AnimationEvent", 51 | "TransitionEvent", 52 | "HtmlAnchorElement", 53 | "HtmlDataElement", 54 | "HtmlQuoteElement", 55 | "HtmlTimeElement", 56 | "HtmlTableColElement", 57 | "HtmlTableCellElement", 58 | "HtmlInputElement", 59 | "HtmlFormElement", 60 | "HtmlMeterElement", 61 | "HtmlOptionElement", 62 | "HtmlTextAreaElement", 63 | "HtmlImageElement", 64 | "HtmlMediaElement", 65 | "HtmlVideoElement", 66 | "HtmlTrackElement", 67 | "HtmlAreaElement", 68 | "SubmitEvent", 69 | ] 70 | 71 | [dev-dependencies] 72 | wasm-bindgen-test = "0.3" 73 | console_log = "0.2" 74 | console_error_panic_hook = "0.1" 75 | wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } 76 | once_cell = "1.13" 77 | -------------------------------------------------------------------------------- /maomi-dom/src/composing.rs: -------------------------------------------------------------------------------- 1 | use maomi::backend::tree::*; 2 | 3 | use crate::DomGeneralElement; 4 | 5 | pub(crate) enum ChildFrag { 6 | None, 7 | Single(web_sys::Node), 8 | Multi(web_sys::DocumentFragment), 9 | } 10 | 11 | impl ChildFrag { 12 | fn new() -> Self { 13 | Self::None 14 | } 15 | 16 | fn add(&mut self, n: &web_sys::Node) { 17 | match self { 18 | Self::None => { 19 | *self = Self::Single(n.clone()); 20 | } 21 | Self::Single(prev) => { 22 | let frag = crate::DOCUMENT.with(|document| document.create_document_fragment()); 23 | frag.append_child(prev).map(|_| ()).unwrap_or_else(|x| { 24 | crate::log_js_error(&x); 25 | }); 26 | frag.append_child(&n).map(|_| ()).unwrap_or_else(|x| { 27 | crate::log_js_error(&x); 28 | }); 29 | *self = Self::Multi(frag); 30 | } 31 | Self::Multi(frag) => { 32 | frag.append_child(&n).map(|_| ()).unwrap_or_else(|x| { 33 | crate::log_js_error(&x); 34 | }); 35 | } 36 | } 37 | } 38 | 39 | pub(crate) fn dom(&self) -> Option<&web_sys::Node> { 40 | match self { 41 | Self::None => None, 42 | Self::Single(x) => Some(x), 43 | Self::Multi(x) => Some(&x), 44 | } 45 | } 46 | } 47 | 48 | pub(crate) fn remove_all_children<'a>( 49 | parent: &web_sys::Node, 50 | n: ForestNode<'a, DomGeneralElement>, 51 | ) { 52 | fn rec<'a>(parent: &web_sys::Node, n: &ForestNode<'a, DomGeneralElement>) { 53 | match &**n { 54 | DomGeneralElement::Element(x) => { 55 | parent 56 | .remove_child(x.composing_dom()) 57 | .map(|_| ()) 58 | .unwrap_or_else(|x| { 59 | crate::log_js_error(&x); 60 | }); 61 | } 62 | DomGeneralElement::Text(x) => { 63 | parent 64 | .remove_child(x.composing_dom()) 65 | .map(|_| ()) 66 | .unwrap_or_else(|x| { 67 | crate::log_js_error(&x); 68 | }); 69 | } 70 | DomGeneralElement::Virtual(_) => { 71 | let mut cur_option = n.first_child(); 72 | while let Some(cur) = cur_option { 73 | rec(parent, &cur); 74 | cur_option = cur.next_sibling(); 75 | } 76 | } 77 | } 78 | } 79 | rec(parent, &n); 80 | } 81 | 82 | pub(crate) fn collect_child_frag<'a>(n: ForestNode<'a, DomGeneralElement>) -> ChildFrag { 83 | fn rec<'a>(n: ForestNode<'a, DomGeneralElement>, ret: &mut ChildFrag) { 84 | match &*n { 85 | DomGeneralElement::Element(x) => { 86 | return ret.add(&x.composing_dom()); 87 | } 88 | DomGeneralElement::Text(x) => { 89 | return ret.add(&x.composing_dom()); 90 | } 91 | DomGeneralElement::Virtual(_) => { 92 | let mut cur_option = n.first_child(); 93 | while let Some(cur) = cur_option { 94 | rec(cur.clone(), ret); 95 | cur_option = cur.next_sibling(); 96 | } 97 | } 98 | } 99 | } 100 | let mut ret = ChildFrag::new(); 101 | rec(n, &mut ret); 102 | ret 103 | } 104 | 105 | pub(crate) fn find_nearest_dom_ancestor<'a>( 106 | n: ForestNode<'a, DomGeneralElement>, 107 | ) -> Option { 108 | let mut cur_rc = n.rc(); 109 | loop { 110 | let next = { 111 | let cur = n.borrow(&cur_rc); 112 | match &*cur { 113 | DomGeneralElement::Element(x) => { 114 | return Some(x.composing_dom().clone()); 115 | } 116 | DomGeneralElement::Text(x) => { 117 | return Some(x.composing_dom().clone()); 118 | } 119 | DomGeneralElement::Virtual(_) => { 120 | if let Some(x) = cur.parent_rc() { 121 | x 122 | } else { 123 | break; 124 | } 125 | } 126 | } 127 | }; 128 | cur_rc = next; 129 | } 130 | return None; 131 | } 132 | 133 | fn find_first_dom_child<'a>(parent: ForestNode<'a, DomGeneralElement>) -> Option { 134 | match &*parent { 135 | DomGeneralElement::Element(x) => { 136 | return Some(x.composing_dom().clone()); 137 | } 138 | DomGeneralElement::Text(x) => { 139 | return Some(x.composing_dom().clone()); 140 | } 141 | DomGeneralElement::Virtual(_) => { 142 | let mut cur_option = parent.first_child(); 143 | while let Some(cur) = cur_option { 144 | if let Some(x) = find_first_dom_child(cur.clone()) { 145 | return Some(x); 146 | } 147 | cur_option = cur.next_sibling(); 148 | } 149 | } 150 | } 151 | return None; 152 | } 153 | 154 | pub(crate) fn find_next_dom_sibling<'a>( 155 | n: ForestNode<'a, DomGeneralElement>, 156 | ) -> Option { 157 | let mut cur_rc = n.rc(); 158 | loop { 159 | let next = { 160 | let cur = n.borrow(&cur_rc); 161 | if let Some(next) = cur.next_sibling_rc() { 162 | if let Some(x) = find_first_dom_child(n.borrow(&next)) { 163 | return Some(x); 164 | } 165 | next 166 | } else if let Some(parent) = cur.parent_rc() { 167 | match &*cur.borrow(&parent) { 168 | DomGeneralElement::Element(_) | DomGeneralElement::Text(_) => { 169 | break; 170 | } 171 | DomGeneralElement::Virtual(_) => {} 172 | } 173 | parent 174 | } else { 175 | break; 176 | } 177 | }; 178 | cur_rc = next; 179 | } 180 | return None; 181 | } 182 | -------------------------------------------------------------------------------- /maomi-dom/src/custom_attr.rs: -------------------------------------------------------------------------------- 1 | //! Some utilities to define custom DOM attributes. 2 | //! 3 | //! Sometimes it is needed to add some custom attribute to DOM elements. 4 | //! In these cases, `maomi_dom::dom_define_attribute!` can be used to define a custom attribute. 5 | //! Then the attribute can be used with `attr:xxx` template syntax. 6 | //! 7 | //! ```no_run 8 | //! // define a new attribute `role` 9 | //! maomi_dom::dom_define_attribute!(aria_hidden); 10 | //! // use in template like this 11 | //! //
12 | //! 13 | 14 | use maomi::prop::{ListPropertyInit, ListPropertyItem, ListPropertyUpdate}; 15 | 16 | use crate::{base_element::DomElement, DomState}; 17 | 18 | /// The custom DOM attributes. 19 | /// 20 | /// Can be used in template with `attr:name="value"` syntax, e.g. `attr:aria-hidden="true"`. 21 | /// Caution! This bypass type checks and directly write to DOM. 22 | /// Do not use this if there are other proper attributes. 23 | pub struct DomCustomAttrs { 24 | pub(crate) inner: Vec>, 25 | } 26 | 27 | impl DomCustomAttrs { 28 | pub(crate) fn new() -> Self { 29 | Self { 30 | inner: Vec::with_capacity(0), 31 | } 32 | } 33 | } 34 | 35 | impl ListPropertyInit for DomCustomAttrs { 36 | type UpdateContext = DomElement; 37 | 38 | #[inline] 39 | fn init_list(dest: &mut Self, count: usize, _ctx: &mut Self::UpdateContext) { 40 | dest.inner = Vec::with_capacity(count); 41 | dest.inner.resize(count, None); 42 | } 43 | } 44 | 45 | impl ListPropertyUpdate for DomCustomAttrs { 46 | type ItemValue = &'static str; 47 | 48 | #[inline] 49 | fn compare_and_set_item_ref>( 50 | dest: &mut Self, 51 | index: usize, 52 | src: &str, 53 | ctx: &mut Self::UpdateContext, 54 | ) where 55 | Self: Sized, 56 | { 57 | let attr_name = U::item_value(dest, index, src, ctx); 58 | if dest.inner[index].as_ref().map(|x| x.as_str()) != Some(src) { 59 | dest.inner[index] = Some(src.to_string()); 60 | match &mut ctx.elem { 61 | DomState::Normal(x) => { 62 | let _ = x.set_attribute(attr_name, src); 63 | } 64 | #[cfg(feature = "prerendering")] 65 | DomState::Prerendering(x) => { 66 | x.set_attribute(attr_name, src.to_string()); 67 | } 68 | #[cfg(feature = "prerendering-apply")] 69 | DomState::PrerenderingApply(_) => {} 70 | } 71 | } 72 | } 73 | } 74 | 75 | impl ListPropertyUpdate for DomCustomAttrs { 76 | type ItemValue = &'static str; 77 | 78 | #[inline] 79 | fn compare_and_set_item_ref>( 80 | dest: &mut Self, 81 | index: usize, 82 | src: &bool, 83 | ctx: &mut Self::UpdateContext, 84 | ) where 85 | Self: Sized, 86 | { 87 | let attr_name = U::item_value(dest, index, src, ctx); 88 | let v = if *src { Some("") } else { None }; 89 | if dest.inner[index].as_ref().map(|x| x.as_str()) != v { 90 | dest.inner[index] = v.map(|x| x.to_string()); 91 | match &mut ctx.elem { 92 | DomState::Normal(x) => { 93 | if v.is_some() { 94 | let _ = x.set_attribute(attr_name, ""); 95 | } else { 96 | let _ = x.remove_attribute(attr_name); 97 | } 98 | } 99 | #[cfg(feature = "prerendering")] 100 | DomState::Prerendering(x) => { 101 | if v.is_some() { 102 | x.set_attribute(attr_name, "".to_string()); 103 | } else { 104 | x.remove_attribute(attr_name); 105 | } 106 | } 107 | #[cfg(feature = "prerendering-apply")] 108 | DomState::PrerenderingApply(_) => {} 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /maomi-dom/src/dynamic_style.rs: -------------------------------------------------------------------------------- 1 | //! The utilities for DOM `ClassList` handling. 2 | 3 | use maomi::prop::{ListPropertyInit, ListPropertyItem, ListPropertyUpdate}; 4 | 5 | use crate::{base_element::DomElement, DomState}; 6 | 7 | #[doc(hidden)] 8 | pub fn set_style(name: &'static str, value: &str, ctx: &mut DomElement) { 9 | match &mut ctx.elem { 10 | DomState::Normal(x) => { 11 | use wasm_bindgen::JsCast; 12 | if let Some(x) = x.dyn_ref::() { 13 | x.style().set_property(name, value).unwrap(); 14 | } 15 | } 16 | #[cfg(feature = "prerendering")] 17 | DomState::Prerendering(x) => { 18 | x.set_style(name, value); 19 | } 20 | #[cfg(feature = "prerendering-apply")] 21 | DomState::PrerenderingApply(_) => {} 22 | } 23 | } 24 | 25 | /// The manager for DOM `ClassList` . 26 | pub struct DomStyleList { 27 | values: Box<[DomStyleItemValue]>, 28 | } 29 | 30 | #[derive(Debug, Clone, PartialEq)] 31 | enum DomStyleItemValue { 32 | None, 33 | Str(String), 34 | I32(i32), 35 | F32(f32), 36 | } 37 | 38 | impl DomStyleList { 39 | pub(crate) fn new() -> Self { 40 | Self { 41 | values: Box::new([]), 42 | } 43 | } 44 | } 45 | 46 | impl ListPropertyInit for DomStyleList { 47 | type UpdateContext = DomElement; 48 | 49 | #[inline] 50 | fn init_list(dest: &mut Self, count: usize, _ctx: &mut Self::UpdateContext) { 51 | let mut v = Vec::with_capacity(count); 52 | v.resize_with(count, || DomStyleItemValue::None); 53 | dest.values = v.into_boxed_slice(); 54 | } 55 | } 56 | 57 | impl ListPropertyUpdate for DomStyleList { 58 | type ItemValue = (); 59 | 60 | #[inline] 61 | fn compare_and_set_item_ref>( 62 | dest: &mut Self, 63 | index: usize, 64 | src: &i32, 65 | ctx: &mut Self::UpdateContext, 66 | ) where 67 | Self: Sized, 68 | { 69 | if dest.values[index] == DomStyleItemValue::I32(*src) { 70 | return; 71 | } 72 | dest.values[index] = DomStyleItemValue::I32(*src); 73 | U::item_value(dest, index, src, ctx); 74 | } 75 | } 76 | 77 | impl ListPropertyUpdate for DomStyleList { 78 | type ItemValue = (); 79 | 80 | #[inline] 81 | fn compare_and_set_item_ref>( 82 | dest: &mut Self, 83 | index: usize, 84 | src: &f32, 85 | ctx: &mut Self::UpdateContext, 86 | ) where 87 | Self: Sized, 88 | { 89 | if dest.values[index] == DomStyleItemValue::F32(*src) { 90 | return; 91 | } 92 | dest.values[index] = DomStyleItemValue::F32(*src); 93 | U::item_value(dest, index, src, ctx); 94 | } 95 | } 96 | 97 | impl ListPropertyUpdate for DomStyleList { 98 | type ItemValue = (); 99 | 100 | #[inline] 101 | fn compare_and_set_item_ref>( 102 | dest: &mut Self, 103 | index: usize, 104 | src: &str, 105 | ctx: &mut Self::UpdateContext, 106 | ) where 107 | Self: Sized, 108 | { 109 | if let DomStyleItemValue::Str(x) = &dest.values[index] { 110 | if x.as_str() == src { 111 | return; 112 | } 113 | } 114 | dest.values[index] = DomStyleItemValue::Str(src.to_string()); 115 | U::item_value(dest, index, src, ctx); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /maomi-dom/src/element/content_sectioning.rs: -------------------------------------------------------------------------------- 1 | //! The DOM elements about content sectioning. 2 | 3 | use maomi_dom_macro::dom_element_definition; 4 | 5 | use super::*; 6 | 7 | #[dom_element_definition] 8 | pub struct address {} 9 | 10 | #[dom_element_definition] 11 | pub struct article {} 12 | 13 | #[dom_element_definition] 14 | pub struct aside {} 15 | 16 | #[dom_element_definition] 17 | pub struct footer {} 18 | 19 | #[dom_element_definition] 20 | pub struct header {} 21 | 22 | #[dom_element_definition] 23 | pub struct h1 {} 24 | 25 | #[dom_element_definition] 26 | pub struct h2 {} 27 | 28 | #[dom_element_definition] 29 | pub struct h3 {} 30 | 31 | #[dom_element_definition] 32 | pub struct h4 {} 33 | 34 | #[dom_element_definition] 35 | pub struct h5 {} 36 | 37 | #[dom_element_definition] 38 | pub struct h6 {} 39 | 40 | #[dom_element_definition] 41 | pub struct main {} 42 | 43 | #[dom_element_definition] 44 | pub struct nav {} 45 | 46 | #[dom_element_definition] 47 | pub struct section {} 48 | -------------------------------------------------------------------------------- /maomi-dom/src/element/demarcating_edits.rs: -------------------------------------------------------------------------------- 1 | //! The DOM elements about demarcating edits. 2 | 3 | use super::*; 4 | 5 | #[dom_element_definition] 6 | pub struct del { 7 | pub cite: attribute!(&str in web_sys::HtmlQuoteElement), 8 | pub date_time: attribute!(&str in web_sys::HtmlTimeElement), 9 | } 10 | 11 | #[dom_element_definition] 12 | pub struct ins { 13 | pub cite: attribute!(&str in web_sys::HtmlQuoteElement), 14 | pub date_time: attribute!(&str in web_sys::HtmlTimeElement), 15 | } 16 | -------------------------------------------------------------------------------- /maomi-dom/src/element/forms.rs: -------------------------------------------------------------------------------- 1 | //! The DOM elements about forms. 2 | 3 | use maomi::prop::BindingValue; 4 | use web_sys::{HtmlFormElement, HtmlInputElement}; 5 | 6 | use super::*; 7 | 8 | #[dom_element_definition] 9 | pub struct button { 10 | pub autocomplete: attribute!(&str in HtmlInputElement), 11 | pub disabled: attribute!(bool in HtmlInputElement), 12 | pub form_action: attribute!(&str in HtmlInputElement), 13 | pub form_enctype: attribute!(&str in HtmlInputElement), 14 | pub form_method: attribute!(&str in HtmlInputElement), 15 | pub form_no_validate: attribute!(bool in HtmlInputElement), 16 | pub form_target: attribute!(&str in HtmlInputElement), 17 | pub name: attribute!(&str in HtmlInputElement), 18 | pub r#type: attribute!(&str in HtmlInputElement), 19 | pub value: attribute!(&str in HtmlInputElement), 20 | } 21 | 22 | #[dom_element_definition] 23 | pub struct datalist {} 24 | 25 | #[dom_element_definition] 26 | pub struct fieldset { 27 | pub disabled: attribute!(bool in HtmlInputElement), 28 | pub name: attribute!(&str in HtmlInputElement), 29 | } 30 | 31 | #[dom_element_definition] 32 | pub struct form { 33 | pub autocomplete: attribute!(&str in HtmlFormElement), 34 | pub name: attribute!(&str in HtmlFormElement), 35 | pub rel: attribute!(&str in web_sys::HtmlAnchorElement), 36 | pub action: attribute!(&str in HtmlFormElement), 37 | pub enctype: attribute!(&str in HtmlFormElement), 38 | pub method: attribute!(&str in HtmlFormElement), 39 | pub no_validate: attribute!(bool in HtmlFormElement), 40 | pub target: attribute!(&str in HtmlFormElement), 41 | pub submit: event!(crate::event::form::Submit), 42 | } 43 | 44 | #[dom_element_definition] 45 | pub struct input { 46 | pub accept: attribute!(&str in HtmlInputElement), 47 | pub alt: attribute!(&str in HtmlInputElement), 48 | pub autocomplete: attribute!(&str in HtmlInputElement), 49 | pub checked: attribute!(bool in HtmlInputElement while "change" 50 | |binding_value: &mut BindingValue, _ev: &web_sys::Event, target: &web_sys::HtmlInputElement| { 51 | binding_value.set(target.checked()); 52 | } 53 | ), 54 | pub disabled: attribute!(bool in HtmlInputElement), 55 | pub form_action: attribute!(&str in HtmlInputElement), 56 | pub form_enctype: attribute!(&str in HtmlInputElement), 57 | pub form_method: attribute!(&str in HtmlInputElement), 58 | pub form_no_validate: attribute!(bool in HtmlInputElement), 59 | pub form_target: attribute!(&str in HtmlInputElement), 60 | pub height: attribute!(u32 in HtmlInputElement), 61 | pub max: attribute!(&str in HtmlInputElement), 62 | pub max_length: attribute!(i32 in HtmlInputElement), 63 | pub min: attribute!(&str in HtmlInputElement), 64 | pub min_length: attribute!(i32 in HtmlInputElement), 65 | pub multiple: attribute!(bool in HtmlInputElement), 66 | pub name: attribute!(&str in HtmlInputElement), 67 | pub pattern: attribute!(&str in HtmlInputElement), 68 | pub placeholder: attribute!(&str in HtmlInputElement), 69 | pub read_only: attribute!(bool in HtmlInputElement), 70 | pub required: attribute!(bool in HtmlInputElement), 71 | pub size: attribute!(u32 in HtmlInputElement), 72 | pub src: attribute!(&str in HtmlInputElement), 73 | pub step: attribute!(&str in HtmlInputElement), 74 | pub r#type: attribute!(&str in HtmlInputElement), 75 | pub spellcheck: attribute!(bool in web_sys::HtmlElement), 76 | pub value: attribute!(&str in HtmlInputElement while "input" 77 | |binding_value: &mut BindingValue, _ev: &web_sys::Event, target: &web_sys::HtmlInputElement| { 78 | binding_value.set(target.value()); 79 | } 80 | ), 81 | pub width: attribute!(u32 in HtmlInputElement), 82 | pub change: event!(crate::event::form::Change), 83 | pub input: event!(crate::event::form::Input), 84 | } 85 | 86 | #[dom_element_definition] 87 | pub struct label { 88 | pub r#for: attribute!(&str), 89 | } 90 | 91 | #[dom_element_definition] 92 | pub struct legend {} 93 | 94 | #[dom_element_definition] 95 | pub struct meter { 96 | pub value: attribute!(f64 in web_sys::HtmlMeterElement while "change" 97 | |binding_value: &mut BindingValue, _ev: &web_sys::Event, target: &web_sys::HtmlMeterElement| { 98 | binding_value.set(target.value()); 99 | } 100 | ), 101 | pub min: attribute!(f64 in web_sys::HtmlMeterElement), 102 | pub max: attribute!(f64 in web_sys::HtmlMeterElement), 103 | pub low: attribute!(f64 in web_sys::HtmlMeterElement), 104 | pub high: attribute!(f64 in web_sys::HtmlMeterElement), 105 | pub optimum: attribute!(f64 in web_sys::HtmlMeterElement), 106 | pub change: event!(crate::event::form::Change), 107 | } 108 | 109 | #[dom_element_definition] 110 | pub struct optgroup { 111 | pub disabled: attribute!(bool in web_sys::HtmlOptionElement), 112 | pub label: attribute!(&str in web_sys::HtmlOptionElement), 113 | } 114 | 115 | #[dom_element_definition] 116 | pub struct option { 117 | pub disabled: attribute!(bool in web_sys::HtmlOptionElement), 118 | pub label: attribute!(&str in web_sys::HtmlOptionElement), 119 | pub selected: attribute!(bool in web_sys::HtmlOptionElement), 120 | pub value: attribute!(&str in web_sys::HtmlOptionElement), 121 | pub change: event!(crate::event::form::Change), 122 | } 123 | 124 | #[dom_element_definition] 125 | pub struct output { 126 | pub r#for: attribute!(&str), 127 | pub name: attribute!(&str in web_sys::HtmlInputElement), 128 | } 129 | 130 | #[dom_element_definition] 131 | pub struct progress { 132 | pub max: attribute!(f64 in web_sys::HtmlMeterElement), 133 | pub value: attribute!(f64 in web_sys::HtmlMeterElement while "change" 134 | |binding_value: &mut BindingValue, _ev: &web_sys::Event, target: &web_sys::HtmlMeterElement| { 135 | binding_value.set(target.value()); 136 | } 137 | ), 138 | pub change: event!(crate::event::form::Change), 139 | } 140 | 141 | #[dom_element_definition] 142 | pub struct select { 143 | pub autocomplete: attribute!(&str in HtmlInputElement), 144 | pub disabled: attribute!(bool in HtmlInputElement), 145 | pub multiple: attribute!(bool in HtmlInputElement), 146 | pub name: attribute!(&str in HtmlInputElement), 147 | pub required: attribute!(bool in HtmlInputElement), 148 | pub size: attribute!(u32 in HtmlInputElement), 149 | } 150 | 151 | #[dom_element_definition] 152 | pub struct textarea { 153 | pub autocomplete: attribute!(&str in web_sys::HtmlTextAreaElement), 154 | pub cols: attribute!(u32 in web_sys::HtmlTextAreaElement), 155 | pub disabled: attribute!(bool in web_sys::HtmlTextAreaElement), 156 | pub max_length: attribute!(i32 in web_sys::HtmlTextAreaElement), 157 | pub min_length: attribute!(i32 in web_sys::HtmlTextAreaElement), 158 | pub name: attribute!(&str in web_sys::HtmlTextAreaElement), 159 | pub placeholder: attribute!(&str in web_sys::HtmlTextAreaElement), 160 | pub read_only: attribute!(bool in web_sys::HtmlTextAreaElement), 161 | pub required: attribute!(bool in web_sys::HtmlTextAreaElement), 162 | pub rows: attribute!(u32 in web_sys::HtmlTextAreaElement), 163 | pub spellcheck: attribute!(bool in web_sys::HtmlElement), 164 | pub value: attribute!(&str in web_sys::HtmlTextAreaElement while "input" 165 | |binding_value: &mut BindingValue, _ev: &web_sys::Event, target: &web_sys::HtmlTextAreaElement| { 166 | binding_value.set(target.value()); 167 | } 168 | ), 169 | pub wrap: attribute!(&str in web_sys::HtmlTextAreaElement), 170 | } 171 | -------------------------------------------------------------------------------- /maomi-dom/src/element/inline_text.rs: -------------------------------------------------------------------------------- 1 | //! The DOM elements about inline texts. 2 | 3 | use web_sys::HtmlAnchorElement; 4 | 5 | use super::*; 6 | 7 | #[dom_element_definition] 8 | pub struct a { 9 | pub download: attribute!(&str in HtmlAnchorElement), 10 | pub href: attribute!(&str in HtmlAnchorElement), 11 | pub hreflang: attribute!(&str in HtmlAnchorElement), 12 | pub ping: attribute!(&str in HtmlAnchorElement), 13 | pub referrer_policy: attribute!(&str in HtmlAnchorElement), 14 | pub rel: attribute!(&str in HtmlAnchorElement), 15 | pub target: attribute!(&str in HtmlAnchorElement), 16 | pub r#type: attribute!(&str in HtmlAnchorElement), 17 | } 18 | 19 | #[dom_element_definition] 20 | pub struct abbr {} 21 | 22 | #[dom_element_definition] 23 | pub struct b {} 24 | 25 | #[dom_element_definition] 26 | pub struct bdi {} 27 | 28 | #[dom_element_definition] 29 | pub struct bdo {} 30 | 31 | #[dom_element_definition] 32 | pub struct br {} 33 | 34 | #[dom_element_definition] 35 | pub struct site {} 36 | 37 | #[dom_element_definition] 38 | pub struct code {} 39 | 40 | #[dom_element_definition] 41 | pub struct data { 42 | pub value: attribute!(&str in web_sys::HtmlDataElement), 43 | } 44 | 45 | #[dom_element_definition] 46 | pub struct em {} 47 | 48 | #[dom_element_definition] 49 | pub struct i {} 50 | 51 | #[dom_element_definition] 52 | pub struct kbd {} 53 | 54 | #[dom_element_definition] 55 | pub struct mark {} 56 | 57 | #[dom_element_definition] 58 | pub struct q { 59 | pub cite: attribute!(&str in web_sys::HtmlQuoteElement), 60 | } 61 | 62 | #[dom_element_definition] 63 | pub struct rp {} 64 | 65 | #[dom_element_definition] 66 | pub struct rt {} 67 | 68 | #[dom_element_definition] 69 | pub struct ruby {} 70 | 71 | #[dom_element_definition] 72 | pub struct s {} 73 | 74 | #[dom_element_definition] 75 | pub struct samp {} 76 | 77 | #[dom_element_definition] 78 | pub struct small {} 79 | 80 | #[dom_element_definition] 81 | pub struct span {} 82 | 83 | #[dom_element_definition] 84 | pub struct strong {} 85 | 86 | #[dom_element_definition] 87 | pub struct sub {} 88 | 89 | #[dom_element_definition] 90 | pub struct sup {} 91 | 92 | #[dom_element_definition] 93 | pub struct time { 94 | pub date_time: attribute!(&str in web_sys::HtmlTimeElement), 95 | } 96 | 97 | #[dom_element_definition] 98 | pub struct u {} 99 | 100 | #[dom_element_definition] 101 | pub struct var {} 102 | 103 | #[dom_element_definition] 104 | pub struct wbr {} 105 | -------------------------------------------------------------------------------- /maomi-dom/src/element/mod.rs: -------------------------------------------------------------------------------- 1 | //! The element definition. 2 | //! 3 | //! The element list is found in [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element) . 4 | 5 | use crate::MaybeJsStr; 6 | #[allow(unused_imports)] 7 | use maomi::locale_string::LocaleString; 8 | use maomi::{ 9 | backend::{AsElementTag, BackendComponent}, 10 | error::Error, 11 | node::{OwnerWeak, SlotChange, StaticSingleSlot}, 12 | BackendContext, 13 | }; 14 | use maomi_dom_macro::dom_element_definition; 15 | use wasm_bindgen::JsCast; 16 | 17 | use crate::{ 18 | base_element::*, class_list::DomClassList, custom_attr::DomCustomAttrs, 19 | dynamic_style::DomStyleList, event, event::DomEvent, tree::*, DomBackend, DomGeneralElement, 20 | DomState, 21 | }; 22 | 23 | // TODO add embedded content, svg, MathML support 24 | 25 | pub mod content_sectioning; 26 | pub use content_sectioning::*; 27 | pub mod text_content; 28 | pub use text_content::*; 29 | pub mod inline_text; 30 | pub use inline_text::*; 31 | pub mod multimedia; 32 | pub use multimedia::*; 33 | pub mod demarcating_edits; 34 | pub use demarcating_edits::*; 35 | pub mod table_content; 36 | pub use table_content::*; 37 | pub mod forms; 38 | pub use forms::*; 39 | -------------------------------------------------------------------------------- /maomi-dom/src/element/multimedia.rs: -------------------------------------------------------------------------------- 1 | //! The DOM elements about multimedia. 2 | 3 | use super::*; 4 | 5 | #[dom_element_definition] 6 | pub struct canvas { 7 | pub width: attribute!(u32 in web_sys::HtmlInputElement), 8 | pub height: attribute!(u32 in web_sys::HtmlInputElement), 9 | } 10 | 11 | #[dom_element_definition] 12 | pub struct img { 13 | pub alt: attribute!(&str in web_sys::HtmlImageElement), 14 | pub decoding: attribute!(&str in web_sys::HtmlImageElement), 15 | pub height: attribute!(u32 in web_sys::HtmlImageElement), 16 | pub is_map: attribute!(bool in web_sys::HtmlImageElement), 17 | pub referrer_policy: attribute!(&str in web_sys::HtmlImageElement), 18 | pub sizes: attribute!(&str in web_sys::HtmlImageElement), 19 | pub src: attribute!(&str in web_sys::HtmlImageElement), 20 | pub srcset: attribute!(&str in web_sys::HtmlImageElement), 21 | pub width: attribute!(u32 in web_sys::HtmlImageElement), 22 | pub use_map: attribute!(&str in web_sys::HtmlImageElement), 23 | } 24 | 25 | #[dom_element_definition] 26 | pub struct audio { 27 | pub autoplay: attribute!(bool in web_sys::HtmlMediaElement), 28 | pub controls: attribute!(bool in web_sys::HtmlMediaElement), 29 | pub r#loop: attribute!(bool in web_sys::HtmlMediaElement), 30 | pub muted: attribute!(bool in web_sys::HtmlMediaElement), 31 | pub preload: attribute!(&str in web_sys::HtmlMediaElement), 32 | pub src: attribute!(&str in web_sys::HtmlMediaElement), 33 | } 34 | 35 | #[dom_element_definition] 36 | pub struct video { 37 | pub autoplay: attribute!(bool in web_sys::HtmlMediaElement), 38 | pub controls: attribute!(bool in web_sys::HtmlMediaElement), 39 | pub height: attribute!(u32 in web_sys::HtmlVideoElement), 40 | pub r#loop: attribute!(bool in web_sys::HtmlMediaElement), 41 | pub muted: attribute!(bool in web_sys::HtmlMediaElement), 42 | pub poster: attribute!(&str in web_sys::HtmlVideoElement), 43 | pub preload: attribute!(&str in web_sys::HtmlMediaElement), 44 | pub src: attribute!(&str in web_sys::HtmlMediaElement), 45 | pub width: attribute!(u32 in web_sys::HtmlVideoElement), 46 | } 47 | 48 | #[dom_element_definition] 49 | pub struct track { 50 | pub default: attribute!(bool in web_sys::HtmlTrackElement), 51 | pub kind: attribute!(&str in web_sys::HtmlTrackElement), 52 | pub label: attribute!(&str in web_sys::HtmlTrackElement), 53 | pub src: attribute!(&str in web_sys::HtmlTrackElement), 54 | pub srclang: attribute!(&str in web_sys::HtmlTrackElement), 55 | } 56 | 57 | #[dom_element_definition] 58 | pub struct map { 59 | pub name: attribute!(&str in web_sys::HtmlInputElement), 60 | } 61 | 62 | #[dom_element_definition] 63 | pub struct area { 64 | pub name: attribute!(&str in web_sys::HtmlInputElement), 65 | pub alt: attribute!(&str in web_sys::HtmlAreaElement), 66 | pub coords: attribute!(&str in web_sys::HtmlAreaElement), 67 | pub download: attribute!(&str in web_sys::HtmlAreaElement), 68 | pub href: attribute!(&str in web_sys::HtmlAreaElement), 69 | pub hreflang: attribute!(&str in web_sys::HtmlAnchorElement), 70 | pub ping: attribute!(&str in web_sys::HtmlAreaElement), 71 | pub rel: attribute!(&str in web_sys::HtmlAreaElement), 72 | pub shape: attribute!(&str in web_sys::HtmlAreaElement), 73 | pub target: attribute!(&str in web_sys::HtmlAreaElement), 74 | } 75 | -------------------------------------------------------------------------------- /maomi-dom/src/element/table_content.rs: -------------------------------------------------------------------------------- 1 | //! The DOM elements about table contents. 2 | 3 | use super::*; 4 | 5 | #[dom_element_definition] 6 | pub struct caption {} 7 | 8 | #[dom_element_definition] 9 | pub struct col { 10 | pub span: attribute!(u32 in web_sys::HtmlTableColElement), 11 | } 12 | 13 | #[dom_element_definition] 14 | pub struct colgroup { 15 | pub span: attribute!(u32 in web_sys::HtmlTableColElement), 16 | } 17 | 18 | #[dom_element_definition] 19 | pub struct table {} 20 | 21 | #[dom_element_definition] 22 | pub struct tbody {} 23 | 24 | #[dom_element_definition] 25 | pub struct td { 26 | pub col_span: attribute!(u32 in web_sys::HtmlTableCellElement), 27 | pub row_span: attribute!(u32 in web_sys::HtmlTableCellElement), 28 | pub headers: attribute!(&str in web_sys::HtmlTableCellElement), 29 | } 30 | 31 | #[dom_element_definition] 32 | pub struct tfoot {} 33 | 34 | #[dom_element_definition] 35 | pub struct th { 36 | pub col_span: attribute!(u32 in web_sys::HtmlTableCellElement), 37 | pub row_span: attribute!(u32 in web_sys::HtmlTableCellElement), 38 | pub headers: attribute!(&str in web_sys::HtmlTableCellElement), 39 | } 40 | 41 | #[dom_element_definition] 42 | pub struct thead {} 43 | 44 | #[dom_element_definition] 45 | pub struct tr {} 46 | -------------------------------------------------------------------------------- /maomi-dom/src/element/text_content.rs: -------------------------------------------------------------------------------- 1 | //! The DOM elements about text content. 2 | 3 | use super::*; 4 | 5 | #[dom_element_definition] 6 | pub struct blockquote { 7 | pub cite: attribute!(&str in web_sys::HtmlQuoteElement), 8 | } 9 | 10 | #[dom_element_definition] 11 | pub struct dd {} 12 | 13 | #[dom_element_definition] 14 | pub struct div {} 15 | 16 | #[dom_element_definition] 17 | pub struct dl {} 18 | 19 | #[dom_element_definition] 20 | pub struct dt {} 21 | 22 | #[dom_element_definition] 23 | pub struct figcaption {} 24 | 25 | #[dom_element_definition] 26 | pub struct figure {} 27 | 28 | #[dom_element_definition] 29 | pub struct hr {} 30 | 31 | #[dom_element_definition] 32 | pub struct li { 33 | pub value: attribute!(&str in web_sys::HtmlDataElement), 34 | } 35 | 36 | #[dom_element_definition] 37 | pub struct menu {} 38 | 39 | #[dom_element_definition] 40 | pub struct ol {} 41 | 42 | #[dom_element_definition] 43 | pub struct p {} 44 | 45 | #[dom_element_definition] 46 | pub struct pre {} 47 | 48 | #[dom_element_definition] 49 | pub struct ul {} 50 | -------------------------------------------------------------------------------- /maomi-dom/src/event/animation.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::{prelude::*, JsCast}; 2 | 3 | use super::{ColdEventItem, DomEventRegister}; 4 | use crate::DomGeneralElement; 5 | 6 | /// The animation event detail. 7 | #[derive(Debug, Clone, PartialEq)] 8 | pub struct AnimationEvent { 9 | dom_event: web_sys::AnimationEvent, 10 | } 11 | 12 | impl AnimationEvent { 13 | /// Get the elapsed time of the animation. 14 | #[inline] 15 | pub fn elapsed_time(&self) -> f32 { 16 | self.dom_event.elapsed_time() 17 | } 18 | } 19 | 20 | fn trigger_ev>(dom_event: web_sys::AnimationEvent) { 21 | let target = dom_event 22 | .target() 23 | .and_then(|x| crate::DomElement::from_event_dom_elem(x.unchecked_ref(), false)); 24 | if let Some(n) = target { 25 | if let DomGeneralElement::Element(x) = &mut *n.borrow_mut() { 26 | T::trigger(x, &mut AnimationEvent { dom_event }); 27 | } 28 | } 29 | } 30 | 31 | cold_event!( 32 | AnimationStart, 33 | AnimationEvent, 34 | Closure::new(move |dom_event: web_sys::AnimationEvent| { 35 | trigger_ev::(dom_event); 36 | }) 37 | ); 38 | 39 | cold_event!( 40 | AnimationIteration, 41 | AnimationEvent, 42 | Closure::new(move |dom_event: web_sys::AnimationEvent| { 43 | trigger_ev::(dom_event); 44 | }) 45 | ); 46 | 47 | cold_event!( 48 | AnimationEnd, 49 | AnimationEvent, 50 | Closure::new(move |dom_event: web_sys::AnimationEvent| { 51 | trigger_ev::(dom_event); 52 | }) 53 | ); 54 | 55 | cold_event!( 56 | AnimationCancel, 57 | AnimationEvent, 58 | Closure::new(move |dom_event: web_sys::AnimationEvent| { 59 | trigger_ev::(dom_event); 60 | }) 61 | ); 62 | -------------------------------------------------------------------------------- /maomi-dom/src/event/form.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::{closure::Closure, JsCast}; 2 | 3 | use super::{ColdEventItem, DomEventRegister}; 4 | use crate::DomGeneralElement; 5 | 6 | fn trigger_ev_submit>(dom_event: web_sys::SubmitEvent) { 7 | let target = dom_event 8 | .target() 9 | .and_then(|x| crate::DomElement::from_event_dom_elem(x.unchecked_ref(), false)); 10 | if let Some(n) = target { 11 | if let DomGeneralElement::Element(x) = &mut *n.borrow_mut() { 12 | T::trigger(x, &mut SubmitEvent { dom_event }); 13 | } 14 | } 15 | } 16 | 17 | /// The mouse-related event detail. 18 | #[derive(Debug, Clone, PartialEq)] 19 | pub struct SubmitEvent { 20 | dom_event: web_sys::SubmitEvent, 21 | } 22 | 23 | cold_event!( 24 | Submit, 25 | SubmitEvent, 26 | Closure::new(move |dom_event: web_sys::SubmitEvent| { 27 | trigger_ev_submit::(dom_event); 28 | }) 29 | ); 30 | 31 | fn trigger_ev_change>(dom_event: web_sys::Event) { 32 | let target = dom_event 33 | .target() 34 | .and_then(|x| crate::DomElement::from_event_dom_elem(x.unchecked_ref(), false)); 35 | if let Some(n) = target { 36 | if let DomGeneralElement::Element(x) = &mut *n.borrow_mut() { 37 | T::trigger(x, &mut ChangeEvent { dom_event }); 38 | } 39 | } 40 | } 41 | 42 | /// The mouse-related event detail. 43 | #[derive(Debug, Clone, PartialEq)] 44 | pub struct ChangeEvent { 45 | dom_event: web_sys::Event, 46 | } 47 | 48 | cold_event!( 49 | Change, 50 | ChangeEvent, 51 | Closure::new(move |dom_event: web_sys::Event| { 52 | trigger_ev_change::(dom_event); 53 | }) 54 | ); 55 | 56 | fn trigger_ev_input>(dom_event: web_sys::InputEvent) { 57 | let target = dom_event 58 | .target() 59 | .and_then(|x| crate::DomElement::from_event_dom_elem(x.unchecked_ref(), false)); 60 | if let Some(n) = target { 61 | if let DomGeneralElement::Element(x) = &mut *n.borrow_mut() { 62 | T::trigger(x, &mut InputEvent { dom_event }); 63 | } 64 | } 65 | } 66 | 67 | /// The input event detail. 68 | #[derive(Debug, Clone, PartialEq)] 69 | pub struct InputEvent { 70 | dom_event: web_sys::InputEvent, 71 | } 72 | 73 | #[derive(Debug, Clone, Copy, PartialEq)] 74 | pub enum InputEventType {} 75 | 76 | impl InputEvent { 77 | /// Get the inserted characters. 78 | #[inline] 79 | pub fn data(&self) -> Option { 80 | self.dom_event.data() 81 | } 82 | 83 | /// Get whether action is during the composition progress, a.k.a. input with IME. 84 | #[inline] 85 | pub fn is_composing(&self) -> bool { 86 | self.dom_event.is_composing() 87 | } 88 | } 89 | 90 | cold_event!( 91 | Input, 92 | InputEvent, 93 | Closure::new(move |dom_event: web_sys::InputEvent| { 94 | trigger_ev_input::(dom_event); 95 | }) 96 | ); 97 | -------------------------------------------------------------------------------- /maomi-dom/src/event/mod.rs: -------------------------------------------------------------------------------- 1 | //! The event definition. 2 | //! 3 | //! Besides DOM events, this module also provides `tap` events, 4 | //! which are generated from mouse or touch events. 5 | //! * `tap` event refers to a finger tap (or mouse click); 6 | //! * `cancal_tap` event refers to a finger (or mouse) that moved; 7 | //! * `long_tap` event refers to a long finger tap (or mouse hold). 8 | //! Calling `detail.preventDefault()` in the `long_tap` handler will prevent the `tap` event. 9 | 10 | use maomi::event::EventHandler; 11 | use std::marker::PhantomData; 12 | use wasm_bindgen::{prelude::*, JsCast}; 13 | 14 | use crate::base_element::DomElement; 15 | 16 | #[macro_use] 17 | mod utils; 18 | pub(crate) mod tap; 19 | pub use tap::TapEvent; 20 | pub(crate) mod touch; 21 | pub use touch::TouchEvent; 22 | pub(crate) mod mouse; 23 | pub use mouse::{MouseButton, MouseEvent}; 24 | pub(crate) mod scroll; 25 | pub use scroll::ScrollEvent; 26 | pub(crate) mod animation; 27 | pub use animation::AnimationEvent; 28 | pub(crate) mod transition; 29 | pub use transition::TransitionEvent; 30 | pub(crate) mod form; 31 | pub use form::{ChangeEvent, InputEvent, SubmitEvent}; 32 | 33 | pub(crate) struct DomListeners { 34 | #[allow(dead_code)] 35 | touch: touch::TouchEventCbs, 36 | } 37 | 38 | impl DomListeners { 39 | pub(crate) fn new(element: &web_sys::Element) -> Self { 40 | Self { 41 | touch: touch::init_dom_listeners(element), 42 | } 43 | } 44 | } 45 | 46 | // hot event list is usually used to store popular events and bubble events 47 | #[derive(Default)] 48 | pub(crate) struct HotEventList { 49 | touch_start: Option>, 50 | touch_move: Option>, 51 | touch_end: Option>, 52 | touch_cancel: Option>, 53 | tap: Option>, 54 | long_tap: Option>, 55 | cancel_tap: Option>, 56 | } 57 | 58 | // code event list is slow to visit but memory-efficient 59 | pub(crate) type ColdEventList = Vec; 60 | 61 | pub(crate) enum ColdEventItem { 62 | BindingEventListener(&'static str, Closure), 63 | MouseDown( 64 | Box, 65 | Closure, 66 | ), 67 | MouseUp( 68 | Box, 69 | Closure, 70 | ), 71 | MouseMove( 72 | Box, 73 | Closure, 74 | ), 75 | MouseEnter( 76 | Box, 77 | Closure, 78 | ), 79 | MouseLeave( 80 | Box, 81 | Closure, 82 | ), 83 | Click( 84 | Box, 85 | Closure, 86 | ), 87 | Scroll( 88 | Box, 89 | Closure, 90 | ), 91 | AnimationStart( 92 | Box, 93 | Closure, 94 | ), 95 | AnimationIteration( 96 | Box, 97 | Closure, 98 | ), 99 | AnimationEnd( 100 | Box, 101 | Closure, 102 | ), 103 | AnimationCancel( 104 | Box, 105 | Closure, 106 | ), 107 | TransitionRun( 108 | Box, 109 | Closure, 110 | ), 111 | TransitionStart( 112 | Box, 113 | Closure, 114 | ), 115 | TransitionEnd( 116 | Box, 117 | Closure, 118 | ), 119 | TransitionCancel( 120 | Box, 121 | Closure, 122 | ), 123 | Submit( 124 | Box, 125 | Closure, 126 | ), 127 | Change( 128 | Box, 129 | Closure, 130 | ), 131 | Input( 132 | Box, 133 | Closure, 134 | ), 135 | } 136 | 137 | impl ColdEventItem { 138 | pub(crate) fn apply(&self, elem: &web_sys::Element) { 139 | let (ev_name, cb): (&str, &JsValue) = match self { 140 | Self::BindingEventListener(name, cb) => (name, cb.as_ref()), 141 | Self::MouseDown(_, cb) => ("mousedown", cb.as_ref()), 142 | Self::MouseUp(_, cb) => ("mouseup", cb.as_ref()), 143 | Self::MouseMove(_, cb) => ("mousemove", cb.as_ref()), 144 | Self::MouseEnter(_, cb) => ("mouseenter", cb.as_ref()), 145 | Self::MouseLeave(_, cb) => ("mouseleave", cb.as_ref()), 146 | Self::Click(_, cb) => ("click", cb.as_ref()), 147 | Self::Scroll(_, cb) => ("scroll", cb.as_ref()), 148 | Self::AnimationStart(_, cb) => ("animationstart", cb.as_ref()), 149 | Self::AnimationIteration(_, cb) => ("animationiteration", cb.as_ref()), 150 | Self::AnimationEnd(_, cb) => ("animationend", cb.as_ref()), 151 | Self::AnimationCancel(_, cb) => ("animationcancel", cb.as_ref()), 152 | Self::TransitionRun(_, cb) => ("transitionrun", cb.as_ref()), 153 | Self::TransitionStart(_, cb) => ("transitionstart", cb.as_ref()), 154 | Self::TransitionEnd(_, cb) => ("transitionend", cb.as_ref()), 155 | Self::TransitionCancel(_, cb) => ("transitioncancel", cb.as_ref()), 156 | Self::Submit(_, cb) => ("submit", cb.as_ref()), 157 | Self::Change(_, cb) => ("change", cb.as_ref()), 158 | Self::Input(_, cb) => ("input", cb.as_ref()), 159 | }; 160 | // Seriously, there should be a removal on the element dropped, 161 | // otherwise the closure is lost and a js error is displayed in console. 162 | // However, most events do not trigger after element removal, 163 | // so here just do no removal. 164 | if let Err(err) = elem.add_event_listener_with_callback(ev_name, cb.unchecked_ref()) { 165 | crate::log_js_error(&err); 166 | log::error!( 167 | "Failed adding listener for event {:?}. This event will not be triggered.", 168 | ev_name 169 | ); 170 | } 171 | } 172 | } 173 | 174 | /// A DOM event that can be binded. 175 | pub trait DomEventRegister { 176 | /// The event detailed type. 177 | type Detail; 178 | 179 | /// Bind the event. 180 | /// 181 | /// It is auto-managed by the `#[component]` . 182 | /// Do not touch unless you know how it works exactly. 183 | fn bind(target: &mut DomElement, f: Box); 184 | 185 | /// Trigger the event. 186 | fn trigger(target: &mut DomElement, detail: &mut Self::Detail); 187 | } 188 | 189 | /// A DOM event 190 | pub struct DomEvent { 191 | _phantom: PhantomData, 192 | } 193 | 194 | impl Default for DomEvent { 195 | #[inline] 196 | fn default() -> Self { 197 | Self { 198 | _phantom: PhantomData, 199 | } 200 | } 201 | } 202 | 203 | impl EventHandler for DomEvent { 204 | type UpdateContext = DomElement; 205 | 206 | #[inline] 207 | fn set_handler_fn( 208 | _dest: &mut Self, 209 | handler_fn: Box, 210 | ctx: &mut DomElement, 211 | ) { 212 | M::bind(ctx, handler_fn); 213 | } 214 | } 215 | 216 | /// A DOM event that bubbles. 217 | pub trait BubbleEvent { 218 | /// Stop bubbling. 219 | fn stop_propagation(&mut self); 220 | /// Get whether the bubbling is stopped. 221 | fn propagation_stopped(&self) -> bool; 222 | /// Prevent the default DOM operation. 223 | fn prevent_default(&mut self); 224 | /// Get whether the default DOM operation is prevented. 225 | fn default_prevented(&self) -> bool; 226 | } 227 | -------------------------------------------------------------------------------- /maomi-dom/src/event/mouse.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::{prelude::*, JsCast}; 2 | 3 | use super::{BubbleEvent, ColdEventItem, DomEventRegister}; 4 | use crate::DomGeneralElement; 5 | 6 | /// The mouse-related event detail. 7 | #[derive(Debug, Clone, PartialEq)] 8 | pub struct MouseEvent { 9 | propagation_stopped: bool, 10 | default_prevented: bool, 11 | dom_event: web_sys::MouseEvent, 12 | } 13 | 14 | /// A mouse button. 15 | #[derive(Debug, Clone, Copy, PartialEq)] 16 | pub enum MouseButton { 17 | /// The main button, i.e. left button. 18 | Main, 19 | /// The auxiliary button, i.e. middle button or wheel button. 20 | Auxiliary, 21 | /// The secondary button, i.e. right button. 22 | Secondary, 23 | /// The fourth button, i.e. history-back button. 24 | Fourth, 25 | /// The fifth button, i.e. history-forward button. 26 | Fifth, 27 | /// Other unknwon button. 28 | Unknown(i16), 29 | } 30 | 31 | impl MouseEvent { 32 | /// Get the button that triggers `mouse_down` or `mouse_up` . 33 | #[inline] 34 | pub fn button(&self) -> MouseButton { 35 | match self.dom_event.button() { 36 | 0 => MouseButton::Main, 37 | 1 => MouseButton::Auxiliary, 38 | 2 => MouseButton::Secondary, 39 | 3 => MouseButton::Fourth, 40 | 4 => MouseButton::Fifth, 41 | x => MouseButton::Unknown(x), 42 | } 43 | } 44 | 45 | /// Check whether keyboard alt key is pressed. 46 | #[inline] 47 | pub fn alt_key(&self) -> bool { 48 | self.dom_event.alt_key() 49 | } 50 | 51 | /// Check whether keyboard ctrl key is pressed. 52 | #[inline] 53 | pub fn ctrl_key(&self) -> bool { 54 | self.dom_event.ctrl_key() 55 | } 56 | 57 | /// Check whether keyboard meta key is pressed. 58 | #[inline] 59 | pub fn meta_key(&self) -> bool { 60 | self.dom_event.meta_key() 61 | } 62 | 63 | /// Check whether keyboard shift key is pressed. 64 | #[inline] 65 | pub fn shift_key(&self) -> bool { 66 | self.dom_event.shift_key() 67 | } 68 | 69 | /// Get the x-position reletive to the viewport. 70 | #[inline] 71 | pub fn client_x(&self) -> i32 { 72 | self.dom_event.client_x() 73 | } 74 | 75 | /// Get the y-position reletive to the viewport. 76 | #[inline] 77 | pub fn client_y(&self) -> i32 { 78 | self.dom_event.client_y() 79 | } 80 | } 81 | 82 | impl BubbleEvent for MouseEvent { 83 | #[inline] 84 | fn stop_propagation(&mut self) { 85 | if self.propagation_stopped { 86 | return; 87 | }; 88 | self.propagation_stopped = true; 89 | self.dom_event.stop_propagation() 90 | } 91 | 92 | #[inline] 93 | fn propagation_stopped(&self) -> bool { 94 | self.propagation_stopped 95 | } 96 | 97 | #[inline] 98 | fn prevent_default(&mut self) { 99 | if self.default_prevented { 100 | return; 101 | }; 102 | self.default_prevented = true; 103 | self.dom_event.prevent_default() 104 | } 105 | 106 | #[inline] 107 | fn default_prevented(&self) -> bool { 108 | self.default_prevented 109 | } 110 | } 111 | 112 | fn trigger_ev>(dom_event: web_sys::MouseEvent) { 113 | let target = dom_event 114 | .target() 115 | .and_then(|x| crate::DomElement::from_event_dom_elem(x.unchecked_ref(), false)); 116 | if let Some(n) = target { 117 | if let DomGeneralElement::Element(x) = &mut *n.borrow_mut() { 118 | T::trigger( 119 | x, 120 | &mut MouseEvent { 121 | propagation_stopped: false, 122 | default_prevented: false, 123 | dom_event, 124 | }, 125 | ); 126 | } 127 | } 128 | } 129 | 130 | cold_event!( 131 | MouseDown, 132 | MouseEvent, 133 | Closure::new(move |dom_event: web_sys::MouseEvent| { 134 | trigger_ev::(dom_event); 135 | }) 136 | ); 137 | 138 | cold_event!( 139 | MouseUp, 140 | MouseEvent, 141 | Closure::new(move |dom_event: web_sys::MouseEvent| { 142 | trigger_ev::(dom_event); 143 | }) 144 | ); 145 | 146 | cold_event!( 147 | MouseMove, 148 | MouseEvent, 149 | Closure::new(move |dom_event: web_sys::MouseEvent| { 150 | trigger_ev::(dom_event); 151 | }) 152 | ); 153 | 154 | cold_event!( 155 | MouseEnter, 156 | MouseEvent, 157 | Closure::new(move |dom_event: web_sys::MouseEvent| { 158 | trigger_ev::(dom_event); 159 | }) 160 | ); 161 | 162 | cold_event!( 163 | MouseLeave, 164 | MouseEvent, 165 | Closure::new(move |dom_event: web_sys::MouseEvent| { 166 | trigger_ev::(dom_event); 167 | }) 168 | ); 169 | 170 | cold_event!( 171 | Click, 172 | MouseEvent, 173 | Closure::new(move |dom_event: web_sys::MouseEvent| { 174 | trigger_ev::(dom_event); 175 | }) 176 | ); 177 | -------------------------------------------------------------------------------- /maomi-dom/src/event/scroll.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::{prelude::*, JsCast}; 2 | 3 | use super::{BubbleEvent, ColdEventItem, DomEventRegister}; 4 | use crate::DomGeneralElement; 5 | 6 | /// The scroll event detail. 7 | #[derive(Debug, Clone, PartialEq)] 8 | pub struct ScrollEvent { 9 | propagation_stopped: bool, 10 | default_prevented: bool, 11 | dom_event: web_sys::Event, 12 | } 13 | 14 | impl BubbleEvent for ScrollEvent { 15 | #[inline] 16 | fn stop_propagation(&mut self) { 17 | if self.propagation_stopped { 18 | return; 19 | }; 20 | self.propagation_stopped = true; 21 | self.dom_event.stop_propagation() 22 | } 23 | 24 | #[inline] 25 | fn propagation_stopped(&self) -> bool { 26 | self.propagation_stopped 27 | } 28 | 29 | #[inline] 30 | fn prevent_default(&mut self) { 31 | if self.default_prevented { 32 | return; 33 | }; 34 | self.default_prevented = true; 35 | self.dom_event.prevent_default() 36 | } 37 | 38 | #[inline] 39 | fn default_prevented(&self) -> bool { 40 | self.default_prevented 41 | } 42 | } 43 | 44 | fn trigger_ev>(dom_event: web_sys::Event) { 45 | let target = dom_event 46 | .target() 47 | .and_then(|x| crate::DomElement::from_event_dom_elem(x.unchecked_ref(), false)); 48 | if let Some(n) = target { 49 | if let DomGeneralElement::Element(x) = &mut *n.borrow_mut() { 50 | T::trigger( 51 | x, 52 | &mut ScrollEvent { 53 | propagation_stopped: false, 54 | default_prevented: false, 55 | dom_event, 56 | }, 57 | ); 58 | } 59 | } 60 | } 61 | 62 | cold_event!( 63 | Scroll, 64 | ScrollEvent, 65 | Closure::new(move |dom_event: web_sys::Event| { 66 | trigger_ev::(dom_event); 67 | }) 68 | ); 69 | -------------------------------------------------------------------------------- /maomi-dom/src/event/tap.rs: -------------------------------------------------------------------------------- 1 | use maomi::backend::tree::{ForestNodeRc, ForestToken}; 2 | use wasm_bindgen::{prelude::*, JsCast}; 3 | 4 | use super::{touch::TouchIdentifier, utils, BubbleEvent, DomEventRegister}; 5 | use crate::DomGeneralElement; 6 | 7 | const CANCEL_TAP_DIST: i32 = 5; 8 | const LONG_TAP_TIME_MS: i32 = 500; 9 | 10 | thread_local! { 11 | pub(super) static TOUCH_TRACKER: std::cell::RefCell = Default::default(); 12 | } 13 | 14 | pub(crate) fn remove_element_touch_state(target: &ForestToken) { 15 | TOUCH_TRACKER.with(|this| this.borrow_mut().interrupt_by_elem(target)) 16 | } 17 | 18 | #[derive(Default)] 19 | pub(super) struct TouchTracker { 20 | cur: Vec, 21 | touch_mode: bool, 22 | } 23 | 24 | struct CurrentTouch { 25 | identifier: TouchIdentifier, 26 | target: ForestToken, 27 | client_x: i32, 28 | client_y: i32, 29 | #[allow(dead_code)] 30 | long_tap_cb: Option>, 31 | long_tap_cb_id: i32, 32 | } 33 | 34 | impl TouchTracker { 35 | pub(super) fn touch_mode(&self) -> bool { 36 | self.touch_mode 37 | } 38 | 39 | pub(super) fn add( 40 | &mut self, 41 | identifier: TouchIdentifier, 42 | target: ForestNodeRc, 43 | client_x: i32, 44 | client_y: i32, 45 | touch_mode: bool, 46 | ) { 47 | if touch_mode { 48 | self.touch_mode = true; 49 | } 50 | let (long_tap_cb, long_tap_cb_id) = crate::WINDOW.with(move |window| { 51 | let cb = Closure::new(move || { 52 | TOUCH_TRACKER.with(|this| { 53 | this.borrow_mut().trigger_long_tap(identifier); 54 | }) 55 | }); 56 | let cb_id = window.set_timeout_with_callback_and_timeout_and_arguments_0( 57 | cb.as_ref().unchecked_ref(), 58 | LONG_TAP_TIME_MS, 59 | ); 60 | match cb_id { 61 | Err(err) => { 62 | crate::log_js_error(&err); 63 | log::error!("Setup long tap handler failed."); 64 | (None, 0) 65 | } 66 | Ok(cb_id) => (Some(cb), cb_id), 67 | } 68 | }); 69 | self.cur.push(CurrentTouch { 70 | identifier, 71 | target: target.token(), 72 | client_x, 73 | client_y, 74 | long_tap_cb, 75 | long_tap_cb_id, 76 | }); 77 | } 78 | 79 | fn trigger_long_tap(&mut self, identifier: TouchIdentifier) { 80 | if let Some((i, t)) = self 81 | .cur 82 | .iter_mut() 83 | .enumerate() 84 | .find(|(_, x)| x.identifier == identifier) 85 | { 86 | t.long_tap_cb = None; 87 | // generate long_tap event 88 | let mut ev = TapEvent { 89 | propagation_stopped: false, 90 | default_prevented: false, 91 | client_x: t.client_x, 92 | client_y: t.client_y, 93 | }; 94 | if let Some(target) = unsafe { t.target.unsafe_resolve_token() } { 95 | utils::bubble_event::(target, &mut ev); 96 | } 97 | if ev.default_prevented { 98 | self.cur.swap_remove(i); 99 | } 100 | } 101 | } 102 | 103 | pub(super) fn update(&mut self, identifier: TouchIdentifier, client_x: i32, client_y: i32) { 104 | if let Some((i, t)) = self 105 | .cur 106 | .iter_mut() 107 | .enumerate() 108 | .find(|(_, x)| x.identifier == identifier) 109 | { 110 | if (t.client_x - client_x).abs() > CANCEL_TAP_DIST 111 | || (t.client_y - client_y).abs() > CANCEL_TAP_DIST 112 | { 113 | if t.long_tap_cb.is_some() { 114 | crate::WINDOW.with(|window| { 115 | window.clear_timeout_with_handle(t.long_tap_cb_id); 116 | }); 117 | } 118 | // generate cancel_tap event 119 | let mut ev = TapEvent { 120 | propagation_stopped: false, 121 | default_prevented: false, 122 | client_x: t.client_x, 123 | client_y: t.client_y, 124 | }; 125 | if let Some(target) = unsafe { t.target.unsafe_resolve_token() } { 126 | utils::bubble_event::(target, &mut ev); 127 | } 128 | self.cur.swap_remove(i); 129 | } 130 | } 131 | } 132 | 133 | pub(super) fn remove(&mut self, identifier: TouchIdentifier) { 134 | if let Some((i, t)) = self 135 | .cur 136 | .iter_mut() 137 | .enumerate() 138 | .find(|(_, x)| x.identifier == identifier) 139 | { 140 | if t.long_tap_cb.is_some() { 141 | crate::WINDOW.with(|window| { 142 | window.clear_timeout_with_handle(t.long_tap_cb_id); 143 | }); 144 | } 145 | // generate tap event 146 | let mut ev = TapEvent { 147 | propagation_stopped: false, 148 | default_prevented: false, 149 | client_x: t.client_x, 150 | client_y: t.client_y, 151 | }; 152 | if let Some(target) = unsafe { t.target.unsafe_resolve_token() } { 153 | utils::bubble_event::(target, &mut ev); 154 | } 155 | self.cur.swap_remove(i); 156 | } 157 | if self.cur.len() == 0 { 158 | self.touch_mode = false; 159 | } 160 | } 161 | 162 | pub(super) fn interrupt(&mut self, identifier: TouchIdentifier) { 163 | if let Some((i, t)) = self 164 | .cur 165 | .iter_mut() 166 | .enumerate() 167 | .find(|(_, x)| x.identifier == identifier) 168 | { 169 | if t.long_tap_cb.is_some() { 170 | crate::WINDOW.with(|window| { 171 | window.clear_timeout_with_handle(t.long_tap_cb_id); 172 | }); 173 | } 174 | self.cur.swap_remove(i); 175 | } 176 | if self.cur.len() == 0 { 177 | self.touch_mode = false; 178 | } 179 | } 180 | 181 | pub fn interrupt_by_elem(&mut self, forest_token: &ForestToken) { 182 | if let Some((i, t)) = self 183 | .cur 184 | .iter_mut() 185 | .enumerate() 186 | .find(|(_, x)| x.target.stable_addr() == forest_token.stable_addr()) 187 | { 188 | if t.long_tap_cb.is_some() { 189 | crate::WINDOW.with(|window| { 190 | window.clear_timeout_with_handle(t.long_tap_cb_id); 191 | }); 192 | } 193 | self.cur.swap_remove(i); 194 | } 195 | if self.cur.len() == 0 { 196 | self.touch_mode = false; 197 | } 198 | } 199 | } 200 | 201 | /// The tap event detail. 202 | /// 203 | /// Tap events are generated from DOM `touch*` or `mouse*` events automatically. 204 | #[derive(Debug, Clone, PartialEq)] 205 | pub struct TapEvent { 206 | propagation_stopped: bool, 207 | default_prevented: bool, 208 | client_x: i32, 209 | client_y: i32, 210 | } 211 | 212 | impl TapEvent { 213 | /// Get the x-position reletive to the viewport. 214 | #[inline] 215 | pub fn client_x(&self) -> i32 { 216 | self.client_x 217 | } 218 | 219 | /// Get the y-position reletive to the viewport. 220 | #[inline] 221 | pub fn client_y(&self) -> i32 { 222 | self.client_y 223 | } 224 | } 225 | 226 | impl BubbleEvent for TapEvent { 227 | #[inline] 228 | fn stop_propagation(&mut self) { 229 | self.propagation_stopped = true; 230 | } 231 | 232 | #[inline] 233 | fn propagation_stopped(&self) -> bool { 234 | self.propagation_stopped 235 | } 236 | 237 | #[inline] 238 | fn prevent_default(&mut self) { 239 | self.default_prevented = true; 240 | } 241 | 242 | #[inline] 243 | fn default_prevented(&self) -> bool { 244 | self.default_prevented 245 | } 246 | } 247 | 248 | hot_event!(Tap, tap, TapEvent); 249 | hot_event!(LongTap, long_tap, TapEvent); 250 | hot_event!(CancelTap, cancel_tap, TapEvent); 251 | -------------------------------------------------------------------------------- /maomi-dom/src/event/transition.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::{prelude::*, JsCast}; 2 | 3 | use super::{ColdEventItem, DomEventRegister}; 4 | use crate::DomGeneralElement; 5 | 6 | /// The transition event detail. 7 | #[derive(Debug, Clone, PartialEq)] 8 | pub struct TransitionEvent { 9 | dom_event: web_sys::TransitionEvent, 10 | } 11 | 12 | impl TransitionEvent { 13 | /// Get the property name that the transition runs on. 14 | #[inline] 15 | pub fn property_name(&self) -> String { 16 | self.dom_event.property_name() 17 | } 18 | 19 | /// Get the elapsed time of the transition. 20 | #[inline] 21 | pub fn elapsed_time(&self) -> f32 { 22 | self.dom_event.elapsed_time() 23 | } 24 | } 25 | 26 | fn trigger_ev>(dom_event: web_sys::TransitionEvent) { 27 | let target = dom_event 28 | .target() 29 | .and_then(|x| crate::DomElement::from_event_dom_elem(x.unchecked_ref(), false)); 30 | if let Some(n) = target { 31 | if let DomGeneralElement::Element(x) = &mut *n.borrow_mut() { 32 | T::trigger(x, &mut TransitionEvent { dom_event }); 33 | } 34 | } 35 | } 36 | 37 | cold_event!( 38 | TransitionRun, 39 | TransitionEvent, 40 | Closure::new(move |dom_event: web_sys::TransitionEvent| { 41 | trigger_ev::(dom_event); 42 | }) 43 | ); 44 | 45 | cold_event!( 46 | TransitionStart, 47 | TransitionEvent, 48 | Closure::new(move |dom_event: web_sys::TransitionEvent| { 49 | trigger_ev::(dom_event); 50 | }) 51 | ); 52 | 53 | cold_event!( 54 | TransitionEnd, 55 | TransitionEvent, 56 | Closure::new(move |dom_event: web_sys::TransitionEvent| { 57 | trigger_ev::(dom_event); 58 | }) 59 | ); 60 | 61 | cold_event!( 62 | TransitionCancel, 63 | TransitionEvent, 64 | Closure::new(move |dom_event: web_sys::TransitionEvent| { 65 | trigger_ev::(dom_event); 66 | }) 67 | ); 68 | -------------------------------------------------------------------------------- /maomi-dom/src/event/utils.rs: -------------------------------------------------------------------------------- 1 | use maomi::backend::tree::ForestNodeRc; 2 | 3 | use super::{BubbleEvent, DomEventRegister}; 4 | use crate::DomGeneralElement; 5 | 6 | macro_rules! hot_event { 7 | ($t:ident, $field:ident, $detail:ty) => { 8 | pub struct $t {} 9 | 10 | impl DomEventRegister for $t { 11 | type Detail = $detail; 12 | 13 | fn bind( 14 | target: &mut crate::base_element::DomElement, 15 | f: Box, 16 | ) { 17 | let list = target.hot_event_list_mut(); 18 | list.$field = Some(f); 19 | } 20 | 21 | fn trigger(target: &mut crate::base_element::DomElement, detail: &mut Self::Detail) { 22 | if let Some(list) = target.hot_event_list() { 23 | if let Some(f) = &list.$field { 24 | f(detail); 25 | } 26 | } 27 | } 28 | } 29 | }; 30 | } 31 | 32 | macro_rules! cold_event { 33 | ($arm:ident, $detail:ty, $listen:expr) => { 34 | pub struct $arm {} 35 | 36 | impl DomEventRegister for $arm { 37 | type Detail = $detail; 38 | 39 | #[inline] 40 | fn bind( 41 | target: &mut crate::base_element::DomElement, 42 | f: Box, 43 | ) { 44 | for item in target.cold_event_list_mut() { 45 | if let ColdEventItem::$arm(x, _) = item { 46 | *x = f; 47 | return; 48 | } 49 | } 50 | #[cfg(feature = "prerendering")] 51 | if let crate::DomState::Prerendering(_) = &target.elem { 52 | return; 53 | } 54 | let cb = $listen; 55 | let item = ColdEventItem::$arm(f, cb); 56 | match &target.elem { 57 | crate::DomState::Normal(x) => item.apply(x), 58 | #[cfg(feature = "prerendering")] 59 | crate::DomState::Prerendering(_) => unreachable!(), 60 | #[cfg(feature = "prerendering-apply")] 61 | crate::DomState::PrerenderingApply(_) => {} 62 | } 63 | target.cold_event_list_mut().push(item); 64 | } 65 | 66 | #[inline] 67 | fn trigger(target: &mut crate::base_element::DomElement, detail: &mut Self::Detail) { 68 | if let Some(list) = target.cold_event_list() { 69 | let f = list.iter().find_map(|x| { 70 | if let ColdEventItem::$arm(x, _) = x { 71 | Some(x) 72 | } else { 73 | None 74 | } 75 | }); 76 | if let Some(f) = f { 77 | f(detail); 78 | } 79 | } 80 | } 81 | } 82 | }; 83 | } 84 | 85 | pub(super) fn bubble_event(target: ForestNodeRc, detail: &mut T::Detail) 86 | where 87 | T: DomEventRegister, 88 | T::Detail: BubbleEvent, 89 | { 90 | let mut cur = target.clone(); 91 | loop { 92 | let next = { 93 | let mut n = cur.borrow_mut(); 94 | if let DomGeneralElement::Element(x) = &mut *n { 95 | T::trigger(x, detail); 96 | if detail.propagation_stopped() { 97 | break; 98 | } 99 | } 100 | n.parent_rc() 101 | }; 102 | if let Some(next) = next { 103 | cur = next; 104 | } else { 105 | break; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /maomi-dom/src/text_node.rs: -------------------------------------------------------------------------------- 1 | use maomi::backend::*; 2 | 3 | use crate::{DomGeneralElement, DomState, WriteHtmlState}; 4 | 5 | #[doc(hidden)] 6 | pub struct DomTextNode { 7 | dom_elem: dom_state_ty!(web_sys::Text, (), ()), 8 | content: String, 9 | } 10 | 11 | impl DomTextNode { 12 | pub(crate) fn text_content(&self) -> &str { 13 | &self.content 14 | } 15 | 16 | pub(crate) fn composing_dom(&self) -> &web_sys::Node { 17 | match &self.dom_elem { 18 | DomState::Normal(x) => &x, 19 | #[cfg(feature = "prerendering")] 20 | DomState::Prerendering(_) => unreachable!(), 21 | #[cfg(feature = "prerendering-apply")] 22 | DomState::PrerenderingApply(_) => unreachable!(), 23 | } 24 | } 25 | 26 | #[cfg(feature = "prerendering-apply")] 27 | pub(crate) fn rematch_dom(&mut self, e: web_sys::Node) { 28 | use wasm_bindgen::JsCast; 29 | let mut e = e; 30 | if self.content.len() == 0 { 31 | let text_node = crate::DOCUMENT.with(|document| document.create_text_node("")); 32 | e = e 33 | .parent_node() 34 | .unwrap() 35 | .replace_child(&text_node, &e) 36 | .unwrap(); 37 | } 38 | self.dom_elem = DomState::Normal(e.unchecked_into()); 39 | } 40 | 41 | pub(crate) fn new(this: &mut tree::ForestNodeMut, content: &str) -> Self { 42 | let dom_elem = match this.is_prerendering() { 43 | DomState::Normal(_) => DomState::Normal( 44 | crate::DOCUMENT.with(|document| document.create_text_node(content)), 45 | ), 46 | #[cfg(feature = "prerendering")] 47 | DomState::Prerendering(_) => DomState::Prerendering(()), 48 | #[cfg(feature = "prerendering-apply")] 49 | DomState::PrerenderingApply(_) => DomState::PrerenderingApply(()), 50 | }; 51 | Self { 52 | dom_elem, 53 | content: content.to_string(), 54 | } 55 | } 56 | 57 | pub(crate) fn is_prerendering(&self) -> dom_state_ty!((), (), ()) { 58 | match &self.dom_elem { 59 | DomState::Normal(_) => DomState::Normal(()), 60 | #[cfg(feature = "prerendering")] 61 | DomState::Prerendering(_) => DomState::Prerendering(()), 62 | #[cfg(feature = "prerendering-apply")] 63 | DomState::PrerenderingApply(_) => DomState::PrerenderingApply(()), 64 | } 65 | } 66 | 67 | pub(crate) fn write_inner_html( 68 | &self, 69 | w: &mut impl std::io::Write, 70 | _state: &mut WriteHtmlState, 71 | ) -> std::io::Result<()> { 72 | match &self.dom_elem { 73 | DomState::Normal(x) => { 74 | let s = x.text_content().unwrap_or_default(); 75 | write!(w, "{}", s)?; 76 | } 77 | #[cfg(feature = "prerendering")] 78 | DomState::Prerendering(_) => { 79 | if _state.prev_is_text_node { 80 | write!(w, "")?; 81 | } else { 82 | _state.prev_is_text_node = true; 83 | } 84 | match self.content.as_str() { 85 | "" => { 86 | write!(w, "")?; 87 | } 88 | x => { 89 | html_escape::encode_text_minimal_to_writer(x, w)?; 90 | } 91 | } 92 | } 93 | #[cfg(feature = "prerendering-apply")] 94 | DomState::PrerenderingApply(_) => {} 95 | } 96 | Ok(()) 97 | } 98 | } 99 | 100 | impl BackendTextNode for DomTextNode { 101 | type BaseBackend = crate::DomBackend; 102 | 103 | #[inline] 104 | fn set_text(&mut self, content: &str) { 105 | if self.content.as_str() != content { 106 | self.content = content.to_string(); 107 | match &self.dom_elem { 108 | DomState::Normal(x) => x.set_text_content(Some(content)), 109 | #[cfg(feature = "prerendering")] 110 | DomState::Prerendering(_) => {} 111 | #[cfg(feature = "prerendering-apply")] 112 | DomState::PrerenderingApply(_) => {} 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /maomi-dom/src/virtual_element.rs: -------------------------------------------------------------------------------- 1 | use maomi::backend::*; 2 | 3 | use crate::{DomGeneralElement, DomState}; 4 | 5 | #[doc(hidden)] 6 | pub struct DomVirtualElement { 7 | dom_elem: dom_state_ty!((), (), ()), 8 | } 9 | 10 | impl DomVirtualElement { 11 | #[inline] 12 | pub(crate) fn new(this: &mut tree::ForestNodeMut) -> Self { 13 | let dom_elem = match this.is_prerendering() { 14 | DomState::Normal(_) => DomState::Normal(()), 15 | #[cfg(feature = "prerendering")] 16 | DomState::Prerendering(_) => DomState::Prerendering(()), 17 | #[cfg(feature = "prerendering-apply")] 18 | DomState::PrerenderingApply(_) => DomState::PrerenderingApply(()), 19 | }; 20 | Self { dom_elem } 21 | } 22 | 23 | #[cfg(feature = "prerendering-apply")] 24 | pub(crate) fn rematch_dom(&mut self) { 25 | if self.dom_elem == DomState::PrerenderingApply(()) { 26 | self.dom_elem = DomState::Normal(()); 27 | } 28 | } 29 | 30 | pub(crate) fn is_prerendering(&self) -> dom_state_ty!((), (), ()) { 31 | match &self.dom_elem { 32 | DomState::Normal(_) => DomState::Normal(()), 33 | #[cfg(feature = "prerendering")] 34 | DomState::Prerendering(_) => DomState::Prerendering(()), 35 | #[cfg(feature = "prerendering-apply")] 36 | DomState::PrerenderingApply(_) => DomState::PrerenderingApply(()), 37 | } 38 | } 39 | } 40 | 41 | impl BackendVirtualElement for DomVirtualElement { 42 | type BaseBackend = crate::DomBackend; 43 | } 44 | -------------------------------------------------------------------------------- /maomi-dom/tests/web.rs: -------------------------------------------------------------------------------- 1 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 2 | 3 | pub mod web_tests; 4 | -------------------------------------------------------------------------------- /maomi-dom/tests/web_tests/mod.rs: -------------------------------------------------------------------------------- 1 | use js_sys::Reflect; 2 | use std::sync::Once; 3 | use wasm_bindgen::{JsCast, JsValue}; 4 | 5 | use maomi::{prelude::*, template::ComponentTemplate, AsyncCallback}; 6 | use maomi_dom::{async_task, prelude::*}; 7 | 8 | macro_rules! first_dom { 9 | ($this:expr, $elem:ty) => { 10 | ($this.template_structure().unwrap()[0] 11 | .as_ref::>() 12 | .tag 13 | .dom_element()) 14 | }; 15 | } 16 | 17 | pub mod component; 18 | pub mod event; 19 | pub mod prerendering; 20 | pub mod skin; 21 | pub mod template; 22 | 23 | static INIT: Once = Once::new(); 24 | 25 | fn init() { 26 | INIT.call_once(|| { 27 | console_error_panic_hook::set_once(); 28 | console_log::init_with_level(log::Level::Trace).unwrap(); 29 | }); 30 | } 31 | 32 | pub type ComponentTestCb = Box; 33 | 34 | pub trait ComponentTest { 35 | fn set_callback(&mut self, callback: ComponentTestCb); 36 | } 37 | 38 | pub async fn test_component + ComponentTest>() { 39 | init(); 40 | let elem = web_sys::window() 41 | .unwrap() 42 | .document() 43 | .unwrap() 44 | .create_element("div") 45 | .unwrap(); 46 | // web_sys::window() 47 | // .unwrap() 48 | // .document() 49 | // .unwrap() 50 | // .document_element() 51 | // .unwrap() 52 | // .append_child(&elem) 53 | // .unwrap(); 54 | let dom_backend = DomBackend::new_with_element(elem).unwrap(); 55 | let backend_context = maomi::BackendContext::new(dom_backend); 56 | let (fut, _mount_point) = backend_context 57 | .enter_sync(move |ctx| { 58 | let (fut, cb) = AsyncCallback::new(); 59 | let mount_point = ctx 60 | .attach(move |comp: &mut T| { 61 | comp.set_callback(Box::new(|| cb(()))); 62 | }) 63 | .unwrap(); 64 | (fut, mount_point) 65 | }) 66 | .map_err(|_| "Cannot init mount point") 67 | .unwrap(); 68 | fut.await 69 | } 70 | 71 | #[cfg(feature = "prerendering")] 72 | pub async fn test_component_prerendering< 73 | T: PrerenderableComponent + ComponentTemplate + ComponentTest, 74 | >( 75 | query_data: &T::QueryData, 76 | ) -> (String, T::PrerenderingData) 77 | where 78 | T::PrerenderingData: Clone, 79 | { 80 | init(); 81 | let dom_backend = DomBackend::prerendering(); 82 | let backend_context = maomi::BackendContext::new(dom_backend); 83 | let prerendering_data = 84 | maomi::BackendContext::::prerendering_data::(query_data).await; 85 | let prerendering_data_cloned = prerendering_data.get().clone(); 86 | let (_mount_point, ret) = backend_context 87 | .enter_sync(move |ctx| { 88 | let mount_point = ctx.prerendering_attach(prerendering_data).unwrap(); 89 | let mut ret = vec![]; 90 | ctx.write_prerendering_html(&mut ret).unwrap(); 91 | (mount_point, ret) 92 | }) 93 | .map_err(|_| "Cannot init mount point") 94 | .unwrap(); 95 | (String::from_utf8(ret).unwrap(), prerendering_data_cloned) 96 | } 97 | 98 | #[cfg(feature = "prerendering-apply")] 99 | pub async fn test_component_prerendering_apply< 100 | T: PrerenderableComponent + ComponentTemplate + ComponentTest, 101 | >( 102 | html: &str, 103 | prerendering_data: T::PrerenderingData, 104 | ) { 105 | init(); 106 | let prerendering_data = maomi::PrerenderingData::::new(prerendering_data); 107 | let elem = web_sys::window() 108 | .unwrap() 109 | .document() 110 | .unwrap() 111 | .create_element("div") 112 | .unwrap(); 113 | elem.set_inner_html(html); 114 | let dom_backend = DomBackend::new_prerendered(); 115 | let backend_context = maomi::BackendContext::new(dom_backend); 116 | let (fut, _mount_point) = backend_context 117 | .enter_sync(move |ctx| { 118 | let (fut, cb) = AsyncCallback::new(); 119 | let mount_point = ctx.prerendering_attach(prerendering_data).unwrap(); 120 | ctx.apply_prerendered_element(elem).unwrap(); 121 | let root_rc = mount_point.root_component().rc(); 122 | maomi_dom::async_task(async move { 123 | root_rc 124 | .update(|comp| { 125 | comp.set_callback(Box::new(|| cb(()))); 126 | }) 127 | .await 128 | .unwrap(); 129 | }); 130 | (fut, mount_point) 131 | }) 132 | .map_err(|_| "Cannot init mount point") 133 | .unwrap(); 134 | fut.await 135 | } 136 | 137 | fn simulate_event( 138 | target: &web_sys::EventTarget, 139 | ty: &str, 140 | bubbles: bool, 141 | props: impl IntoIterator, 142 | ) { 143 | let mut event_init = web_sys::EventInit::new(); 144 | event_init.bubbles(bubbles); 145 | let ev = web_sys::Event::new_with_event_init_dict(ty, &event_init).unwrap(); 146 | for (k, v) in props { 147 | Reflect::set(&ev, &JsValue::from_str(k), &v).unwrap(); 148 | } 149 | let target = target.clone(); 150 | async_task(async move { 151 | target.dispatch_event(&ev).unwrap(); 152 | }); 153 | } 154 | 155 | fn generate_fake_touch( 156 | target: &web_sys::Element, 157 | identifier: u32, 158 | client_x: i32, 159 | client_y: i32, 160 | ) -> JsValue { 161 | let v = js_sys::Object::new(); 162 | Reflect::set( 163 | &v, 164 | &JsValue::from_str("identifier"), 165 | &JsValue::from_f64(identifier as f64), 166 | ) 167 | .unwrap(); 168 | Reflect::set( 169 | &v, 170 | &JsValue::from_str("clientX"), 171 | &JsValue::from_f64(client_x as f64), 172 | ) 173 | .unwrap(); 174 | Reflect::set( 175 | &v, 176 | &JsValue::from_str("clientY"), 177 | &JsValue::from_f64(client_y as f64), 178 | ) 179 | .unwrap(); 180 | Reflect::set(&v, &JsValue::from_str("target"), target).unwrap(); 181 | let arr = js_sys::Array::new(); 182 | arr.push(&v); 183 | arr.dyn_into().unwrap() 184 | } 185 | -------------------------------------------------------------------------------- /maomi-dom/tests/web_tests/prerendering.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(feature = "prerendering", feature = "prerendering-apply"))] 2 | 3 | use wasm_bindgen_test::*; 4 | 5 | use maomi::prelude::*; 6 | use maomi_dom::{async_task, element::*, event::*, prelude::*}; 7 | 8 | use super::*; 9 | 10 | #[wasm_bindgen_test] 11 | async fn generate_prerendering_html() { 12 | #[component(Backend = DomBackend)] 13 | struct Child { 14 | template: template! { 15 |
16 | { &self.text } 17 | }, 18 | text: Prop, 19 | title: Prop, 20 | } 21 | 22 | impl Component for Child { 23 | fn new() -> Self { 24 | Self { 25 | template: Default::default(), 26 | text: Prop::new("".into()), 27 | title: Prop::new("".into()), 28 | } 29 | } 30 | } 31 | 32 | stylesheet! { 33 | #[css_name("abc")] 34 | class abc {} 35 | #[css_name("def")] 36 | class def {} 37 | style g(v: f32) { 38 | opacity = v; 39 | } 40 | style h(v: f32) { 41 | height = Px(v); 42 | } 43 | } 44 | 45 | #[component(Backend = DomBackend)] 46 | struct Parent { 47 | callback: Option, 48 | template: template! { 49 |
50 | 51 | { &self.text } 52 | 53 |
54 | }, 55 | def_class: bool, 56 | g_style: f32, 57 | child_text: String, 58 | child_title: String, 59 | text: String, 60 | } 61 | 62 | impl Component for Parent { 63 | fn new() -> Self { 64 | Self { 65 | callback: None, 66 | template: Default::default(), 67 | def_class: true, 68 | g_style: 0.5, 69 | child_text: "456<".into(), 70 | child_title: "".into(), 71 | text: "123".into(), 72 | } 73 | } 74 | 75 | fn created(&self) { 76 | let this = self.rc(); 77 | async_task(async move { 78 | this.update(|this| { 79 | assert_eq!( 80 | first_dom!(this, div).inner_html(), 81 | r#"
456<123"#, 82 | ); 83 | this.def_class = false; 84 | this.g_style = 1.; 85 | this.child_text = "456".into(); 86 | this.child_title = "789".into(); 87 | this.text = "+123".into(); 88 | }) 89 | .await 90 | .unwrap(); 91 | async_task(async move { 92 | this.update_with(|this, _| { 93 | assert_eq!( 94 | first_dom!(this, div) 95 | .outer_html(), 96 | r#"
456+123
"#, 97 | ); 98 | (this.callback.take().unwrap())(); 99 | }) 100 | .await 101 | .unwrap(); 102 | }) 103 | }) 104 | } 105 | } 106 | 107 | #[async_trait] 108 | impl PrerenderableComponent for Parent { 109 | type QueryData = &'static str; 110 | type PrerenderingData = String; 111 | 112 | async fn prerendering_data(query_data: &Self::QueryData) -> Self::PrerenderingData { 113 | query_data.to_string() 114 | } 115 | 116 | fn apply_prerendering_data(&mut self, data: Self::PrerenderingData) { 117 | self.child_title = data; 118 | } 119 | } 120 | 121 | impl ComponentTest for Parent { 122 | fn set_callback(&mut self, callback: ComponentTestCb) { 123 | self.callback = Some(callback); 124 | } 125 | } 126 | 127 | let (html, prerendering_data) = test_component_prerendering::(&"789\"").await; 128 | assert_eq!( 129 | &html, 130 | r#"
456<123
"#, 131 | ); 132 | 133 | test_component_prerendering_apply::(&html, prerendering_data).await; 134 | } 135 | 136 | #[wasm_bindgen_test] 137 | async fn cold_event_in_prerendered() { 138 | #[component(Backend = DomBackend)] 139 | struct MyComp { 140 | callback: Option, 141 | template: template! { 142 |
143 | }, 144 | } 145 | 146 | impl Component for MyComp { 147 | fn new() -> Self { 148 | Self { 149 | callback: None, 150 | template: Default::default(), 151 | } 152 | } 153 | 154 | fn created(&self) { 155 | let this = self.rc(); 156 | this.task_with(|this, _| { 157 | let dom_elem = first_dom!(this, div).clone(); 158 | simulate_event(&dom_elem, "scroll", false, []); 159 | }); 160 | } 161 | } 162 | 163 | impl MyComp { 164 | fn scroll_fn(this: ComponentEvent) { 165 | let this = this.rc(); 166 | async_task(async move { 167 | this.update_with(|this, _| { 168 | (this.callback.take().unwrap())(); 169 | }) 170 | .await 171 | .unwrap(); 172 | }); 173 | } 174 | } 175 | 176 | #[async_trait] 177 | impl PrerenderableComponent for MyComp { 178 | type QueryData = (); 179 | type PrerenderingData = (); 180 | 181 | async fn prerendering_data(_query_data: &Self::QueryData) -> Self::PrerenderingData { 182 | () 183 | } 184 | 185 | fn apply_prerendering_data(&mut self, _data: Self::PrerenderingData) { 186 | // empty 187 | } 188 | } 189 | 190 | impl ComponentTest for MyComp { 191 | fn set_callback(&mut self, callback: ComponentTestCb) { 192 | self.callback = Some(callback); 193 | } 194 | } 195 | 196 | let (html, prerendering_data) = test_component_prerendering::(&()).await; 197 | test_component_prerendering_apply::(&html, prerendering_data).await; 198 | } 199 | 200 | #[wasm_bindgen_test] 201 | async fn hot_event_in_prerendered() { 202 | #[component(Backend = DomBackend)] 203 | struct MyComp { 204 | callback: Option, 205 | template: template! { 206 |
207 | }, 208 | } 209 | 210 | impl Component for MyComp { 211 | fn new() -> Self { 212 | Self { 213 | callback: None, 214 | template: Default::default(), 215 | } 216 | } 217 | 218 | fn created(&self) { 219 | let this = self.rc(); 220 | async_task(async move { 221 | this.get(|this| { 222 | let dom_elem = first_dom!(this, div).clone(); 223 | simulate_event( 224 | &dom_elem, 225 | "touchstart", 226 | true, 227 | [("changedTouches", generate_fake_touch(&dom_elem, 1, 12, 34))], 228 | ); 229 | }) 230 | .await; 231 | }); 232 | } 233 | } 234 | 235 | impl MyComp { 236 | fn handler(this: ComponentEvent) { 237 | let ev = this.detail(); 238 | assert_eq!(ev.client_x(), 12); 239 | assert_eq!(ev.client_y(), 34); 240 | this.task_with(|this, _| { 241 | (this.callback.take().unwrap())(); 242 | }); 243 | } 244 | } 245 | 246 | #[async_trait] 247 | impl PrerenderableComponent for MyComp { 248 | type QueryData = (); 249 | type PrerenderingData = (); 250 | 251 | async fn prerendering_data(_query_data: &Self::QueryData) -> Self::PrerenderingData { 252 | () 253 | } 254 | 255 | fn apply_prerendering_data(&mut self, _data: Self::PrerenderingData) { 256 | // empty 257 | } 258 | } 259 | 260 | impl ComponentTest for MyComp { 261 | fn set_callback(&mut self, callback: ComponentTestCb) { 262 | self.callback = Some(callback); 263 | } 264 | } 265 | 266 | let (html, prerendering_data) = test_component_prerendering::(&()).await; 267 | test_component_prerendering_apply::(&html, prerendering_data).await; 268 | } 269 | -------------------------------------------------------------------------------- /maomi-dom/tests/web_tests/skin.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen_test::*; 2 | 3 | use maomi::prelude::*; 4 | use maomi_dom::{element::*, prelude::*}; 5 | 6 | use super::*; 7 | 8 | #[wasm_bindgen_test] 9 | async fn skin_class() { 10 | stylesheet! { 11 | #[css_name("a-class")] 12 | class a_class {} 13 | } 14 | 15 | #[component(Backend = DomBackend)] 16 | struct MyComp { 17 | callback: Option, 18 | template: template! { 19 |
20 | }, 21 | } 22 | 23 | impl Component for MyComp { 24 | fn new() -> Self { 25 | Self { 26 | callback: None, 27 | template: Default::default(), 28 | } 29 | } 30 | 31 | fn created(&self) { 32 | self.rc().task_with(|this, _| { 33 | assert_eq!( 34 | first_dom!(this, div).outer_html(), 35 | r#"
"#, 36 | ); 37 | (this.callback.take().unwrap())(); 38 | }); 39 | } 40 | } 41 | 42 | impl ComponentTest for MyComp { 43 | fn set_callback(&mut self, callback: ComponentTestCb) { 44 | self.callback = Some(callback); 45 | } 46 | } 47 | 48 | test_component::().await; 49 | } 50 | 51 | #[wasm_bindgen_test] 52 | async fn skin_style() { 53 | stylesheet! { 54 | style opacity(v: f32) { 55 | opacity = v; 56 | } 57 | style text_color(v: &str) { 58 | color = Color(v); 59 | } 60 | style url(v: &str) { 61 | background_image = url(v); 62 | } 63 | } 64 | 65 | #[component(Backend = DomBackend)] 66 | struct MyComp { 67 | callback: Option, 68 | template: template! { 69 |
70 | }, 71 | } 72 | 73 | impl Component for MyComp { 74 | fn new() -> Self { 75 | Self { 76 | callback: None, 77 | template: Default::default(), 78 | } 79 | } 80 | 81 | fn created(&self) { 82 | self.rc().task_with(|this, _| { 83 | let style = first_dom!(this, div) 84 | .dyn_ref::() 85 | .unwrap() 86 | .style(); 87 | assert_eq!(style.get_property_value("opacity"), Ok("0".into()),); 88 | assert_eq!( 89 | style.get_property_value("color"), 90 | Ok("rgb(170, 187, 204)".into()), 91 | ); 92 | assert_eq!( 93 | style.get_property_value("background-image"), 94 | Ok(r#"url("a.png")"#.into()), 95 | ); 96 | (this.callback.take().unwrap())(); 97 | }); 98 | } 99 | } 100 | 101 | impl ComponentTest for MyComp { 102 | fn set_callback(&mut self, callback: ComponentTestCb) { 103 | self.callback = Some(callback); 104 | } 105 | } 106 | 107 | test_component::().await; 108 | } 109 | -------------------------------------------------------------------------------- /maomi-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maomi-macro" 3 | version = "0.5.0" 4 | authors = ["LastLeaf "] 5 | license = "MIT" 6 | description = "Strict and Performant Web Application Programming" 7 | homepage = "https://github.com/LastLeaf/maomi" 8 | documentation = "https://github.com/LastLeaf/maomi" 9 | repository = "https://github.com/LastLeaf/maomi" 10 | edition = "2021" 11 | 12 | [lib] 13 | proc-macro = true 14 | doctest = false 15 | 16 | [dependencies] 17 | maomi-tools = "=0.5.0" 18 | maomi-skin = "=0.5.0" 19 | proc-macro2 = "1.0" 20 | syn = { version = "1.0", features = ["parsing"] } 21 | quote = "1.0" 22 | toml = "0.7" 23 | rustc-hash = "1.1" 24 | once_cell = "1.13" 25 | 26 | [dev-dependencies] 27 | serial_test = "0.9" 28 | -------------------------------------------------------------------------------- /maomi-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "128"] 2 | 3 | use proc_macro::TokenStream; 4 | 5 | mod component; 6 | mod i18n; 7 | mod template; 8 | 9 | /// Define a component struct. 10 | #[proc_macro_attribute] 11 | pub fn component(attr: TokenStream, item: TokenStream) -> TokenStream { 12 | component::component(attr.into(), item.into()).into() 13 | } 14 | 15 | /// Translate a string with default translation group. 16 | /// 17 | /// The basic usage: 18 | /// 19 | /// ```rust 20 | /// i18n!("The string to translate."); 21 | /// ``` 22 | /// 23 | /// Furthermore, this macro works like `println!` and `format!` . 24 | /// However, the dynamic components must also be translated. 25 | /// 26 | /// ```rust 27 | /// let my_name = LocaleString::translated("Alice"); 28 | /// i18n!("My name is {}.", my_name); 29 | /// ``` 30 | /// 31 | #[proc_macro] 32 | pub fn i18n(item: TokenStream) -> TokenStream { 33 | let content = syn::parse_macro_input!(item as i18n::mac::I18nArgs); 34 | quote::quote!(#content).into() 35 | } 36 | 37 | /// Define translation group. 38 | /// 39 | /// This will define another macro similar to `i18n!` but use another translation group. 40 | /// Usage: 41 | /// 42 | /// ```rust 43 | /// i18n_group!(my_group_name as my_macro_name); 44 | /// my_macro_name!("The string to translate with group `my_group_name`."); 45 | /// ``` 46 | /// 47 | #[proc_macro] 48 | pub fn i18n_group(item: TokenStream) -> TokenStream { 49 | let content = syn::parse_macro_input!(item as i18n::mac::I18nGroupArgs); 50 | quote::quote!(#content).into() 51 | } 52 | 53 | #[doc(hidden)] 54 | #[proc_macro] 55 | pub fn i18n_group_format(item: TokenStream) -> TokenStream { 56 | let content = syn::parse_macro_input!(item as i18n::mac::I18nGroupFormatArgs); 57 | quote::quote!(#content).into() 58 | } 59 | -------------------------------------------------------------------------------- /maomi-skin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maomi-skin" 3 | version = "0.5.0" 4 | authors = ["LastLeaf "] 5 | license = "MIT" 6 | description = "Strict and Performant Web Application Programming" 7 | homepage = "https://github.com/LastLeaf/maomi" 8 | documentation = "https://github.com/LastLeaf/maomi" 9 | repository = "https://github.com/LastLeaf/maomi" 10 | edition = "2021" 11 | 12 | [dependencies] 13 | maomi-tools = "=0.5.0" 14 | proc-macro2 = { version = "1.0", features = ["span-locations"] } 15 | syn = { version = "1.0", features = ["full"] } 16 | quote = "1.0" 17 | rustc-hash = "1.1" 18 | once_cell = "1.13" 19 | -------------------------------------------------------------------------------- /maomi-skin/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "128"] 2 | 3 | use proc_macro2::{Span, TokenStream}; 4 | use rustc_hash::FxHashMap; 5 | 6 | // pub mod parser; 7 | pub mod css_token; 8 | use css_token::*; 9 | pub mod module; 10 | pub mod pseudo; 11 | pub mod style_sheet; 12 | pub mod write_css; 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct ParseError { 16 | err: syn::Error, 17 | } 18 | 19 | impl ParseError { 20 | pub fn new(span: Span, message: impl ToString) -> Self { 21 | Self { 22 | err: syn::Error::new(span, message.to_string()), 23 | } 24 | } 25 | 26 | pub fn into_syn_error(self) -> syn::Error { 27 | self.err 28 | } 29 | } 30 | 31 | impl std::fmt::Display for ParseError { 32 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 33 | f.write_str(&self.err.to_string()) 34 | } 35 | } 36 | 37 | impl From for ParseError { 38 | fn from(err: syn::Error) -> Self { 39 | Self { err } 40 | } 41 | } 42 | 43 | pub trait ParseWithVars: Sized { 44 | fn parse_with_vars( 45 | input: syn::parse::ParseStream, 46 | scope: &mut ScopeVars, 47 | ) -> Result; 48 | } 49 | 50 | #[derive(Debug)] 51 | pub struct ScopeVars { 52 | cur_mod: Option, 53 | vars: FxHashMap, 54 | var_refs: Vec, 55 | } 56 | 57 | impl ScopeVars { 58 | fn insert_var(&mut self, var_name: &VarName, value: ScopeVarValue) -> Result<(), syn::Error> { 59 | let mut inserted = false; 60 | let span = var_name.span(); 61 | self.vars.entry(var_name.to_string()).or_insert_with(|| { 62 | inserted = true; 63 | value 64 | }); 65 | if inserted { 66 | Ok(()) 67 | } else { 68 | Err(syn::Error::new(span, "duplicated identifier")) 69 | } 70 | } 71 | } 72 | 73 | #[derive(Debug, Clone)] 74 | pub enum ScopeVarValue { 75 | Token(CssToken), 76 | DynStr(VarDynRef), 77 | DynNum(VarDynRef), 78 | StyleDefinition(Vec<(VarName, ArgType)>), 79 | } 80 | 81 | impl ScopeVarValue { 82 | fn type_name(&self) -> &'static str { 83 | match self { 84 | Self::Token(_) => "value", 85 | Self::DynStr(_) => "&str", 86 | Self::DynNum(_) => "{number}", 87 | Self::StyleDefinition(_) => "StyleDefinition", 88 | } 89 | } 90 | } 91 | 92 | #[derive(Debug, Clone, Copy)] 93 | pub enum ArgType { 94 | Str(Span), 95 | Num(Span), 96 | } 97 | 98 | impl ArgType { 99 | pub fn type_tokens(self) -> TokenStream { 100 | match self { 101 | Self::Str(span) => quote::quote_spanned!(span=> &str ), 102 | Self::Num(span) => quote::quote_spanned!(span=> f32 ), 103 | } 104 | } 105 | } 106 | 107 | #[derive(Debug, Clone)] 108 | pub struct VarDynRef { 109 | pub span: Span, 110 | pub index: usize, 111 | } 112 | 113 | impl PartialEq for VarDynRef { 114 | fn eq(&self, other: &Self) -> bool { 115 | self.index == other.index 116 | } 117 | } 118 | 119 | #[derive(Debug, Clone)] 120 | pub struct VarDynValue { 121 | pub span: Span, 122 | pub kind: VarDynValueKind, 123 | } 124 | 125 | #[derive(Debug, Clone)] 126 | pub enum VarDynValueKind { 127 | Placeholder, 128 | Str(String), 129 | Num(Number), 130 | } 131 | 132 | impl VarDynValue { 133 | pub fn placeholder(span: Span) -> Self { 134 | Self { 135 | span, 136 | kind: VarDynValueKind::Placeholder, 137 | } 138 | } 139 | 140 | fn type_name(&self) -> &'static str { 141 | match &self.kind { 142 | VarDynValueKind::Placeholder => "{unknown}", 143 | VarDynValueKind::Str(_) => "&str", 144 | VarDynValueKind::Num(_) => "{number}", 145 | } 146 | } 147 | } 148 | 149 | #[derive(Debug, Clone, PartialEq)] 150 | pub enum MaybeDyn { 151 | Static(T), 152 | Dyn(VarDynRef), 153 | } 154 | 155 | impl ParseWithVars for MaybeDyn { 156 | fn parse_with_vars( 157 | input: syn::parse::ParseStream, 158 | scope: &mut ScopeVars, 159 | ) -> Result { 160 | use syn::*; 161 | let la = input.lookahead1(); 162 | let value = if la.peek(LitStr) { 163 | let s: LitStr = input.parse()?; 164 | MaybeDyn::Static(s.value()) 165 | } else if la.peek(Ident) { 166 | let var_name: VarName = input.parse()?; 167 | if let Some(v) = scope.vars.get(&var_name.to_string()) { 168 | match v { 169 | ScopeVarValue::DynStr(x) => { 170 | scope.var_refs.push(var_name.into_ref()); 171 | MaybeDyn::Dyn(x.clone()) 172 | } 173 | x => { 174 | return Err(syn::Error::new( 175 | var_name.span(), 176 | format!("expected &str, found {}", x.type_name()), 177 | )); 178 | } 179 | } 180 | } else { 181 | return Err(syn::Error::new(var_name.span(), "variable not declared")); 182 | } 183 | } else { 184 | return Err(la.error()); 185 | }; 186 | Ok(value) 187 | } 188 | } 189 | 190 | impl MaybeDyn { 191 | fn value<'a>(&'a self, values: &'a [VarDynValue]) -> Result<&'a str, syn::Error> { 192 | match self { 193 | Self::Static(x) => Ok(x), 194 | Self::Dyn(x) => { 195 | let v = values.get(x.index).unwrap(); 196 | match &v.kind { 197 | VarDynValueKind::Str(x) => Ok(x), 198 | _ => Err(syn::Error::new( 199 | x.span, 200 | format!("expected &str, found {}", v.type_name()), 201 | )), 202 | } 203 | } 204 | } 205 | } 206 | } 207 | 208 | impl ParseWithVars for MaybeDyn { 209 | fn parse_with_vars( 210 | input: syn::parse::ParseStream, 211 | scope: &mut ScopeVars, 212 | ) -> Result { 213 | use syn::*; 214 | let la = input.lookahead1(); 215 | let value = if la.peek(LitInt) { 216 | let v: LitInt = input.parse()?; 217 | let value = v.base10_parse()?; 218 | MaybeDyn::Static(Number::I32(value)) 219 | } else if la.peek(LitFloat) { 220 | let v: LitFloat = input.parse()?; 221 | let value = v.base10_parse()?; 222 | MaybeDyn::Static(Number::F32(value)) 223 | } else if la.peek(Ident) { 224 | let var_name: VarName = input.parse()?; 225 | if let Some(v) = scope.vars.get(&var_name.to_string()) { 226 | match v { 227 | ScopeVarValue::DynNum(x) => { 228 | scope.var_refs.push(var_name.into_ref()); 229 | MaybeDyn::Dyn(x.clone()) 230 | } 231 | x => { 232 | return Err(syn::Error::new( 233 | var_name.span(), 234 | format!("expected i32 or f32, found {}", x.type_name()), 235 | )); 236 | } 237 | } 238 | } else { 239 | return Err(syn::Error::new(var_name.span(), "variable not declared")); 240 | } 241 | } else { 242 | return Err(la.error()); 243 | }; 244 | Ok(value) 245 | } 246 | } 247 | 248 | impl MaybeDyn { 249 | fn value(&self, values: &[VarDynValue]) -> Result { 250 | match self { 251 | Self::Static(x) => Ok(x.clone()), 252 | Self::Dyn(x) => { 253 | let v = values.get(x.index).unwrap(); 254 | match &v.kind { 255 | VarDynValueKind::Num(x) => Ok(x.clone()), 256 | _ => Err(syn::Error::new( 257 | x.span, 258 | format!("expected {{number}}, found {}", v.type_name()), 259 | )), 260 | } 261 | } 262 | } 263 | } 264 | } 265 | 266 | #[derive(Debug, Clone, PartialEq)] 267 | pub enum Number { 268 | I32(i32), 269 | F32(f32), 270 | } 271 | 272 | #[derive(Debug, Clone, Default)] 273 | pub struct ModPath { 274 | segs: Vec, 275 | } 276 | 277 | impl ModPath { 278 | fn visible_in(&self, src: &Self) -> bool { 279 | for (index, seg) in self.segs.iter().enumerate() { 280 | if Some(seg) != src.segs.get(index) { 281 | return false; 282 | } 283 | } 284 | true 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /maomi-skin/src/module.rs: -------------------------------------------------------------------------------- 1 | use rustc_hash::FxHashMap; 2 | use std::{ 3 | any::{Any, TypeId}, 4 | cell::RefCell, 5 | path::Path, 6 | rc::Rc, 7 | }; 8 | 9 | use crate::{css_token::VarName, style_sheet::*, ModPath}; 10 | 11 | thread_local! { 12 | static ROOT_MODULE_MAP: RefCell>>> = RefCell::new(FxHashMap::default()); 13 | } 14 | 15 | fn parse_mod_file( 16 | mod_path: ModPath, 17 | p: &Path, 18 | ) -> Option>> { 19 | use syn::parse::Parser; 20 | let s = std::fs::read_to_string(p).ok()?; 21 | let style_sheet = StyleSheet::::parse_mod_fn(mod_path) 22 | .parse_str(&s) 23 | .unwrap_or_else(|err| StyleSheet::new_err(err)); 24 | Some(Rc::new(style_sheet)) 25 | } 26 | 27 | pub(crate) fn parse_mod_path( 28 | cur_mod_path: &crate::ModPath, 29 | mod_name: &VarName, 30 | ) -> Option>> { 31 | maomi_tools::config::crate_config(|crate_config| { 32 | let mod_root: &Path = crate_config.stylesheet_mod_root.as_ref()?; 33 | let mut cur_dir = mod_root.parent()?.to_path_buf(); 34 | for seg in cur_mod_path.segs.iter() { 35 | cur_dir.push(seg.to_string()); 36 | } 37 | let mut p1 = cur_dir.clone(); 38 | p1.push(&format!("{}.mcss", mod_name.ident.to_string())); 39 | let mut p2 = cur_dir; 40 | p2.push(mod_name.ident.to_string()); 41 | p2.push("mod.mcss"); 42 | let mut full_mod_path = cur_mod_path.clone(); 43 | full_mod_path.segs.push(mod_name.ident.clone()); 44 | parse_mod_file(full_mod_path.clone(), &p2).or_else(|| parse_mod_file(full_mod_path, &p1)) 45 | }) 46 | } 47 | 48 | fn init_root_module() -> Option>> { 49 | maomi_tools::config::crate_config(|crate_config| { 50 | let mod_root: &Path = crate_config.stylesheet_mod_root.as_ref()?; 51 | parse_mod_file(Default::default(), mod_root) 52 | }) 53 | } 54 | 55 | pub fn root_module() -> Option>> { 56 | let ret = ROOT_MODULE_MAP.with(|map| { 57 | let map = &mut *map.borrow_mut(); 58 | map.entry(TypeId::of::()) 59 | .or_insert_with(|| { 60 | init_root_module::().map(|x| { 61 | let x: Rc = x; 62 | x 63 | }) 64 | }) 65 | .clone() 66 | })?; 67 | Some(ret.downcast::>().unwrap()) 68 | } 69 | -------------------------------------------------------------------------------- /maomi-skin/src/pseudo.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | css_token::*, 3 | write_css::{CssWriter, WriteCss}, 4 | ParseWithVars, VarDynValue, 5 | }; 6 | 7 | /// The supported pseudo classes 8 | /// 9 | /// The list is found in [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes) . 10 | /// Tree-structural pseudo-classes is not included, 11 | /// since it can be handled correctly though template. 12 | pub enum Pseudo { 13 | Fullscreen, 14 | Modal, 15 | PictureInPicture, 16 | Autofill, 17 | Enabled, 18 | Disabled, 19 | ReadOnly, 20 | ReadWrite, 21 | PlaceholderShown, 22 | Default, 23 | Checked, 24 | Blank, 25 | Valid, 26 | Invalid, 27 | InRange, 28 | OutOfRange, 29 | Required, 30 | Optional, 31 | UserInvalid, 32 | Dir(PseudoDir), 33 | Lang(CssIdent), 34 | AnyLink, 35 | Link, 36 | Visited, 37 | LocalLink, 38 | Target, 39 | TargetWithin, 40 | Scope, 41 | Playing, 42 | Paused, 43 | Current, 44 | Past, 45 | Future, 46 | Hover, 47 | Active, 48 | Focus, 49 | FocusVisible, 50 | FocusWithin, 51 | } 52 | 53 | pub enum PseudoDir { 54 | Ltr, 55 | Rtl, 56 | } 57 | 58 | impl ParseWithVars for Pseudo { 59 | fn parse_with_vars( 60 | input: syn::parse::ParseStream, 61 | _scope: &mut crate::ScopeVars, 62 | ) -> Result { 63 | let ident: syn::Ident = input.parse()?; 64 | let ret = match ident.to_string().as_str() { 65 | "fullscreen" => Self::Fullscreen, 66 | "modal" => Self::Modal, 67 | "picture_in_picture" => Self::PictureInPicture, 68 | "autofill" => Self::Autofill, 69 | "enabled" => Self::Enabled, 70 | "disabled" => Self::Disabled, 71 | "read_only" => Self::ReadOnly, 72 | "read_write" => Self::ReadWrite, 73 | "placeholder_shown" => Self::PlaceholderShown, 74 | "default" => Self::Default, 75 | "checked" => Self::Checked, 76 | "blank" => Self::Blank, 77 | "valid" => Self::Valid, 78 | "invalid" => Self::Invalid, 79 | "in_range" => Self::InRange, 80 | "out_of_range" => Self::OutOfRange, 81 | "required" => Self::Required, 82 | "optional" => Self::Optional, 83 | "user_invalid" => Self::UserInvalid, 84 | "dir" => { 85 | let content; 86 | syn::parenthesized!(content in input); 87 | let input = content; 88 | let s: syn::Ident = input.parse()?; 89 | let ret = match s.to_string().as_str() { 90 | "ltr" => PseudoDir::Ltr, 91 | "rtl" => PseudoDir::Rtl, 92 | _ => return Err(syn::Error::new(s.span(), "unknown dir")), 93 | }; 94 | Self::Dir(ret) 95 | } 96 | "lang" => { 97 | let content; 98 | syn::parenthesized!(content in input); 99 | let input = content; 100 | Self::Lang(input.parse()?) 101 | } 102 | "any_link" => Self::AnyLink, 103 | "link" => Self::Link, 104 | "visited" => Self::Visited, 105 | "local_link" => Self::LocalLink, 106 | "target" => Self::Target, 107 | "target_within" => Self::TargetWithin, 108 | "scope" => Self::Scope, 109 | "playing" => Self::Playing, 110 | "paused" => Self::Paused, 111 | "current" => Self::Current, 112 | "past" => Self::Past, 113 | "future" => Self::Future, 114 | "hover" => Self::Hover, 115 | "active" => Self::Active, 116 | "focus" => Self::Focus, 117 | "focus_visible" => Self::FocusVisible, 118 | "focus_within" => Self::FocusWithin, 119 | _ => return Err(syn::Error::new(ident.span(), "unknown pseudo class")), 120 | }; 121 | Ok(ret) 122 | } 123 | } 124 | 125 | impl WriteCss for Pseudo { 126 | fn write_css_with_args( 127 | &self, 128 | cssw: &mut CssWriter, 129 | _values: &[VarDynValue], 130 | ) -> std::fmt::Result { 131 | match self { 132 | Self::Fullscreen => cssw.write_ident("fullscreen", false), 133 | Self::Modal => cssw.write_ident("modal", false), 134 | Self::PictureInPicture => cssw.write_ident("picture-in-picture", false), 135 | Self::Autofill => cssw.write_ident("autofill", false), 136 | Self::Enabled => cssw.write_ident("enabled", false), 137 | Self::Disabled => cssw.write_ident("disabled", false), 138 | Self::ReadOnly => cssw.write_ident("read-only", false), 139 | Self::ReadWrite => cssw.write_ident("read-write", false), 140 | Self::PlaceholderShown => cssw.write_ident("placeholder-shown", false), 141 | Self::Default => cssw.write_ident("default", false), 142 | Self::Checked => cssw.write_ident("checked", false), 143 | Self::Blank => cssw.write_ident("blank", false), 144 | Self::Valid => cssw.write_ident("valid", false), 145 | Self::Invalid => cssw.write_ident("invalid", false), 146 | Self::InRange => cssw.write_ident("in-range", false), 147 | Self::OutOfRange => cssw.write_ident("out-of-range", false), 148 | Self::Required => cssw.write_ident("required", false), 149 | Self::Optional => cssw.write_ident("optional", false), 150 | Self::UserInvalid => cssw.write_ident("user-invalid", false), 151 | Self::Dir(dir) => cssw.write_function_block(false, "dir", |cssw| match &dir { 152 | PseudoDir::Ltr => cssw.write_ident("ltr", true), 153 | PseudoDir::Rtl => cssw.write_ident("rtl", true), 154 | }), 155 | Self::Lang(lang) => cssw.write_function_block(false, "lang", |cssw| { 156 | cssw.write_ident(lang.css_name().as_str(), true) 157 | }), 158 | Self::AnyLink => cssw.write_ident("any-link", false), 159 | Self::Link => cssw.write_ident("link", false), 160 | Self::Visited => cssw.write_ident("visited", false), 161 | Self::LocalLink => cssw.write_ident("local-link", false), 162 | Self::Target => cssw.write_ident("target", false), 163 | Self::TargetWithin => cssw.write_ident("target-within", false), 164 | Self::Scope => cssw.write_ident("scope", false), 165 | Self::Playing => cssw.write_ident("playing", false), 166 | Self::Paused => cssw.write_ident("paused", false), 167 | Self::Current => cssw.write_ident("current", false), 168 | Self::Past => cssw.write_ident("past", false), 169 | Self::Future => cssw.write_ident("future", false), 170 | Self::Hover => cssw.write_ident("hover", false), 171 | Self::Active => cssw.write_ident("active", false), 172 | Self::Focus => cssw.write_ident("focus", false), 173 | Self::FocusVisible => cssw.write_ident("focus-visible", false), 174 | Self::FocusWithin => cssw.write_ident("focus-within", false), 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /maomi-tools/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maomi-tools" 3 | version = "0.5.0" 4 | authors = ["LastLeaf "] 5 | license = "MIT" 6 | description = "Strict and Performant Web Application Programming" 7 | homepage = "https://github.com/LastLeaf/maomi" 8 | documentation = "https://github.com/LastLeaf/maomi" 9 | repository = "https://github.com/LastLeaf/maomi" 10 | edition = "2021" 11 | 12 | [[bin]] 13 | name = "maomi-i18n-format" 14 | path = "src/i18n_format.rs" 15 | 16 | [dependencies] 17 | log = "0.4" 18 | once_cell = "1.13" 19 | serde = { version = "1.0", features = ["derive"] } 20 | toml = "0.7" 21 | rustc-hash = "1.1" 22 | clap = { version = "4.1", features = ["derive"] } 23 | -------------------------------------------------------------------------------- /maomi-tools/src/config.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use std::{env, path::PathBuf}; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct CrateConfig { 6 | pub crate_name: Option, 7 | pub css_out_dir: Option, 8 | pub css_out_mode: CssOutMode, 9 | pub stylesheet_mod_root: Option, 10 | pub i18n_locale: Option, 11 | pub i18n_dir: Option, 12 | pub i18n_format_metadata: bool, 13 | pub rust_analyzer_env: bool, 14 | } 15 | 16 | #[derive(Debug, Clone, Copy, PartialEq)] 17 | pub enum CssOutMode { 18 | Release, 19 | Debug, 20 | } 21 | 22 | #[derive(serde::Deserialize, Debug)] 23 | struct MaomiManifestCargo { 24 | package: MaomiManifestPackage, 25 | } 26 | 27 | #[derive(serde::Deserialize, Debug)] 28 | struct MaomiManifestPackage { 29 | metadata: MaomiManifestMetadata, 30 | } 31 | 32 | #[derive(serde::Deserialize, Debug)] 33 | struct MaomiManifestMetadata { 34 | maomi: MaomiManifest, 35 | } 36 | 37 | #[derive(serde::Deserialize, Debug, Default)] 38 | struct MaomiManifest { 39 | #[serde(default, rename = "css-out-dir")] 40 | css_out_dir: Option, 41 | #[serde(default, rename = "css-out-mode")] 42 | css_out_mode: Option, 43 | #[serde(default, rename = "stylesheet-mod-root")] 44 | stylesheet_mod_root: Option, 45 | #[serde(default, rename = "i18n-dir")] 46 | i18n_dir: Option, 47 | } 48 | 49 | static CRATE_CONFIG: Lazy = Lazy::new(|| { 50 | let crate_name = env::var("CARGO_PKG_NAME").ok(); 51 | let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok(); 52 | let rel_path = PathBuf::from( 53 | manifest_dir 54 | .as_ref() 55 | .map(|x| x.as_str()) 56 | .unwrap_or_default(), 57 | ); 58 | 59 | // read manifest 60 | let manifest = manifest_dir 61 | .as_ref() 62 | .and_then(|x| { 63 | let mut p = PathBuf::from(x); 64 | p.push("Cargo.toml"); 65 | let content = std::fs::read_to_string(&p).ok()?; 66 | let config: MaomiManifestCargo = toml::from_str(&content).ok()?; 67 | Some(config.package.metadata.maomi) 68 | }) 69 | .unwrap_or_default(); 70 | let MaomiManifest { 71 | css_out_dir, 72 | css_out_mode, 73 | stylesheet_mod_root, 74 | i18n_dir, 75 | } = manifest; 76 | 77 | // check env vars 78 | let css_out_dir = env::var("MAOMI_CSS_OUT_DIR").ok().or(css_out_dir).map(|x| { 79 | let p = rel_path.join(x); 80 | std::fs::create_dir_all(&p).unwrap(); 81 | p 82 | }); 83 | let css_out_mode = env::var("MAOMI_CSS_OUT_MODE") 84 | .ok() 85 | .or(css_out_mode) 86 | .map(|x| match x.as_str() { 87 | "debug" => CssOutMode::Debug, 88 | _ => CssOutMode::Release, 89 | }) 90 | .unwrap_or(CssOutMode::Release); 91 | let stylesheet_mod_root = std::env::var("MAOMI_STYLESHEET_MOD_ROOT") 92 | .ok() 93 | .or(stylesheet_mod_root) 94 | .map(|s| rel_path.join(&s)) 95 | .or_else(|| { 96 | manifest_dir 97 | .as_ref() 98 | .map(|s| rel_path.join(&s).join("src").join("lib.mcss")) 99 | }); 100 | let i18n_locale = 101 | std::env::var("MAOMI_I18N_LOCALE") 102 | .ok() 103 | .and_then(|x| if x.len() > 0 { Some(x) } else { None }); 104 | let i18n_dir = std::env::var("MAOMI_I18N_DIR") 105 | .ok() 106 | .or(i18n_dir) 107 | .map(|s| rel_path.join(&s)) 108 | .or_else(|| { 109 | manifest_dir 110 | .as_ref() 111 | .map(|s| rel_path.join(&s).join("i18n")) 112 | }); 113 | let i18n_format_metadata = match std::env::var("MAOMI_I18N_FORMAT_METADATA") 114 | .unwrap_or_default() 115 | .as_str() 116 | { 117 | "on" => true, 118 | _ => false, 119 | }; 120 | let rust_analyzer_env = match std::env::var("MAOMI_RUST_ANALYZER") 121 | .unwrap_or_default() 122 | .as_str() 123 | { 124 | "on" => true, 125 | _ => false, 126 | }; 127 | 128 | CrateConfig { 129 | crate_name, 130 | css_out_dir, 131 | css_out_mode, 132 | stylesheet_mod_root, 133 | i18n_locale, 134 | i18n_dir, 135 | i18n_format_metadata, 136 | rust_analyzer_env, 137 | } 138 | }); 139 | 140 | pub fn crate_config(f: impl FnOnce(&CrateConfig) -> R) -> R { 141 | f(&CRATE_CONFIG) 142 | } 143 | -------------------------------------------------------------------------------- /maomi-tools/src/i18n_format.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use rustc_hash::{FxHashMap, FxHashSet}; 3 | use std::path::PathBuf; 4 | 5 | use maomi_tools::i18n::{Locale, METADATA_VERSION}; 6 | 7 | #[derive(serde::Deserialize)] 8 | struct FormatMetadataOwned { 9 | version: u32, 10 | item: Vec, 11 | } 12 | 13 | #[derive(serde::Deserialize)] 14 | struct FormatMetadataItemOwned { 15 | namespace: String, 16 | src: String, 17 | translated: Option, 18 | } 19 | 20 | fn toml_str_escape(s: &str) -> String { 21 | s.chars() 22 | .map(|x| match x { 23 | '\u{0008}' => "\\b".to_string(), 24 | '\t' => "\\t".to_string(), 25 | '\n' => "\\n".to_string(), 26 | '\u{000C}' => "\\f".to_string(), 27 | '\r' => "\\r".to_string(), 28 | '\"' => "\\\"".to_string(), 29 | '\\' => "\\\\".to_string(), 30 | x => x.to_string(), 31 | }) 32 | .collect() 33 | } 34 | 35 | fn do_format( 36 | w: &mut impl std::fmt::Write, 37 | format_metadata: FormatMetadataOwned, 38 | mut src: Locale, 39 | missing_sign: Option<&str>, 40 | ) -> Result<(), std::fmt::Error> { 41 | struct TransItem<'a> { 42 | src: &'a str, 43 | translated: &'a str, 44 | missing: bool, 45 | unused: bool, 46 | } 47 | 48 | // group by namespaces 49 | let mut namespaces = vec!["translation"]; 50 | let mut map: FxHashMap<&str, (FxHashSet<&str>, Vec)> = FxHashMap::default(); 51 | map.insert("translation", (FxHashSet::default(), vec![])); 52 | for item in &format_metadata.item { 53 | let (set, arr) = map.entry(&item.namespace).or_insert_with(|| { 54 | namespaces.push(&item.namespace); 55 | (FxHashSet::default(), vec![]) 56 | }); 57 | if !set.insert(&item.src) { 58 | continue; 59 | }; 60 | arr.push(TransItem { 61 | src: &item.src, 62 | translated: item 63 | .translated 64 | .as_ref() 65 | .map(|x| x.as_str()) 66 | .unwrap_or_default(), 67 | missing: item.translated.is_none(), 68 | unused: false, 69 | }); 70 | if let Some(x) = src.get_mut(&item.namespace) { 71 | x.remove(&item.src); 72 | } 73 | } 74 | 75 | // add unused translations 76 | for (ns, trans) in &src { 77 | if trans.is_empty() { 78 | continue; 79 | }; 80 | for (src, translated) in trans { 81 | let (_, arr) = map.entry(&ns).or_insert_with(|| { 82 | namespaces.push(&ns); 83 | (FxHashSet::default(), vec![]) 84 | }); 85 | arr.push(TransItem { 86 | src, 87 | translated, 88 | missing: false, 89 | unused: true, 90 | }); 91 | } 92 | } 93 | 94 | // write translation 95 | writeln!(w, "# formatted by maomi-i18n-format")?; 96 | for ns in namespaces { 97 | let (_, trans_items) = map.get(ns).unwrap(); 98 | writeln!(w, "\n[{}]", ns)?; 99 | for item in trans_items { 100 | if item.unused { 101 | writeln!(w, "# (unused)")?; 102 | } else if item.missing { 103 | if let Some(sign) = missing_sign.clone() { 104 | write!(w, "# {} # ", sign)?; 105 | } else { 106 | write!(w, "# ")?; 107 | } 108 | } 109 | writeln!( 110 | w, 111 | r#""{}" = "{}""#, 112 | toml_str_escape(item.src), 113 | toml_str_escape(item.translated) 114 | )?; 115 | } 116 | } 117 | 118 | Ok(()) 119 | } 120 | 121 | #[derive(Parser, Debug)] 122 | #[command(author, version, about = "Format a translation file for maomi")] 123 | struct CmdArgs { 124 | /// The locale to format 125 | #[arg(short, long)] 126 | locale: Option, 127 | /// Add a comment for missing translation 128 | #[arg(short, long)] 129 | missing: Option, 130 | /// Output to stdout instead of the translation file 131 | #[arg(long)] 132 | print: bool, 133 | /// The crate path (default to working directory) 134 | dir: Option, 135 | } 136 | 137 | fn main() { 138 | let cmd_args = CmdArgs::parse(); 139 | 140 | // locate the crate by Cargo.toml 141 | let mut cur_dir = std::env::current_dir().unwrap_or_default(); 142 | if let Some(p) = cmd_args.dir.as_ref() { 143 | cur_dir.push(p); 144 | } 145 | if !cur_dir.join("Cargo.toml").exists() { 146 | panic!("Cargo.toml not found at {:?}", cur_dir); 147 | } 148 | std::env::set_var("CARGO_MANIFEST_DIR", cur_dir); 149 | 150 | maomi_tools::config::crate_config(|crate_config| { 151 | // read config and do format 152 | let locale = { 153 | cmd_args.locale.as_ref().unwrap_or_else(|| { 154 | crate_config.i18n_locale.as_ref().expect( 155 | "locale not specified (try specify `MAOMI_I18N_LOCALE` environment variable)", 156 | ) 157 | }) 158 | }; 159 | let i18n_dir = crate_config 160 | .i18n_dir 161 | .as_ref() 162 | .expect("no proper i18n directory found"); 163 | 164 | // read metadata and original translation file 165 | let src_path = i18n_dir.join(format!("{}.toml", locale)); 166 | let format_metadata_path = i18n_dir 167 | .join("format-metadata") 168 | .join(format!("{}.toml", locale)); 169 | let format_metadata = std::fs::read_to_string(&format_metadata_path).expect("no format metadata found (try build this crate with environment variable `MAOMI_I18N_FORMAT_METADATA=on`)"); 170 | let format_metadata: FormatMetadataOwned = 171 | toml::from_str(&format_metadata).expect("illegal format metadata"); 172 | if format_metadata.version != METADATA_VERSION { 173 | panic!("the format metadata is generated by a different version of maomi"); 174 | } 175 | let src = std::fs::read_to_string(&src_path).unwrap_or_default(); 176 | let src: Locale = toml::from_str(&src).unwrap_or_default(); 177 | 178 | // do the formatting 179 | let mut r = String::new(); 180 | do_format( 181 | &mut r, 182 | format_metadata, 183 | src, 184 | cmd_args.missing.as_ref().map(|x| x.as_str()), 185 | ) 186 | .unwrap(); 187 | let formatted = r; 188 | 189 | // output 190 | if cmd_args.print { 191 | println!("{}", formatted); 192 | } else { 193 | std::fs::write(&src_path, &formatted).expect("Failed to write formatted content"); 194 | } 195 | }); 196 | } 197 | -------------------------------------------------------------------------------- /maomi-tools/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | 3 | pub mod i18n { 4 | use rustc_hash::FxHashMap; 5 | 6 | pub type Locale = FxHashMap>; 7 | 8 | pub const METADATA_VERSION: u32 = 1; 9 | 10 | #[derive(serde::Serialize)] 11 | pub struct FormatMetadata<'a> { 12 | pub item: Vec>, 13 | } 14 | 15 | #[derive(serde::Serialize)] 16 | pub struct FormatMetadataItem<'a> { 17 | pub namespace: &'a str, 18 | pub src: &'a str, 19 | pub translated: Option<&'a str>, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /maomi-tree/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maomi-tree" 3 | version = "0.5.0" 4 | authors = ["LastLeaf "] 5 | license = "MIT" 6 | description = "Strict and Performant Web Application Programming" 7 | homepage = "https://github.com/LastLeaf/maomi" 8 | documentation = "https://github.com/LastLeaf/maomi" 9 | repository = "https://github.com/LastLeaf/maomi" 10 | edition = "2021" 11 | 12 | [dependencies] 13 | log = "0.4" 14 | -------------------------------------------------------------------------------- /maomi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maomi" 3 | version = "0.5.0" 4 | authors = ["LastLeaf "] 5 | license = "MIT" 6 | description = "Strict and Performant Web Application Programming" 7 | homepage = "https://github.com/LastLeaf/maomi" 8 | documentation = "https://github.com/LastLeaf/maomi" 9 | repository = "https://github.com/LastLeaf/maomi" 10 | edition = "2021" 11 | 12 | [features] 13 | default = [] 14 | prerendering = [] 15 | prerendering-apply = [] 16 | all = ["prerendering", "prerendering-apply"] 17 | 18 | [dependencies] 19 | maomi-macro = "=0.5.0" 20 | maomi-tree = "=0.5.0" 21 | log = "0.4" 22 | async-trait = "0.1" 23 | -------------------------------------------------------------------------------- /maomi/src/backend/mod.rs: -------------------------------------------------------------------------------- 1 | //! The backend-related interface. 2 | //! 3 | //! This module contains some basic types that a backend implementor will use. 4 | 5 | pub use maomi_tree as tree; 6 | use tree::*; 7 | 8 | use crate::{ 9 | error::Error, 10 | node::{DynNodeList, OwnerWeak, SlotChange, SlotKindTrait}, 11 | }; 12 | pub mod context; 13 | use context::BackendContext; 14 | 15 | /// The backend stage. 16 | /// 17 | /// This is meaningful only when prerendering is used. 18 | #[derive(Debug, Clone, Copy, PartialEq)] 19 | pub enum BackendStage { 20 | /// The normal backend stage. 21 | Normal, 22 | /// The backend is in prerendering stage. 23 | #[cfg(feature = "prerendering")] 24 | Prerendering, 25 | /// The backend is applying prerendering result. 26 | #[cfg(feature = "prerendering-apply")] 27 | PrerenderingApply, 28 | } 29 | 30 | /// The interface that a backend should implement. 31 | /// 32 | /// This is used by the backend implementor. 33 | /// *In most cases, it should not be used in component implementors.* 34 | pub trait Backend: 'static { 35 | /// The general type for a backend element. 36 | type GeneralElement: BackendGeneralElement; 37 | 38 | /// The type for a virtual element. 39 | type VirtualElement: BackendVirtualElement; 40 | 41 | /// The type for a text node. 42 | type TextNode: BackendTextNode; 43 | 44 | /// Generate an async task. 45 | fn async_task(fut: impl 'static + std::future::Future) 46 | where 47 | Self: Sized; 48 | 49 | /// Whether the backend is in prerendering stage. 50 | fn backend_stage(&self) -> BackendStage; 51 | 52 | /// Get the root element. 53 | fn root(&self) -> ForestNode; 54 | 55 | /// Get the root element. 56 | fn root_mut(&mut self) -> ForestNodeMut; 57 | } 58 | 59 | /// The general type of the elements. 60 | /// 61 | /// This is used by the backend implementor. 62 | /// *In most cases, it should not be used in component implementors.* 63 | /// 64 | /// The backend can contain several types of elements. 65 | /// * A `VirtualElement` is an element which should not layout in backend. 66 | /// * A `TextNode` is a text node. 67 | /// * The backend can define other types of elements. 68 | pub trait BackendGeneralElement: 'static { 69 | /// The related backend type. 70 | type BaseBackend: Backend; 71 | 72 | /// Cast to a virtual element. 73 | fn as_virtual_element_mut<'b>( 74 | this: &'b mut ForestNodeMut, 75 | ) -> Option< 76 | ForestValueMut< 77 | 'b, 78 | <::BaseBackend as Backend>::VirtualElement, 79 | >, 80 | > 81 | where 82 | Self: Sized; 83 | 84 | /// Cast to a text node. 85 | fn as_text_node_mut<'b>( 86 | this: &'b mut ForestNodeMut, 87 | ) -> Option< 88 | ForestValueMut<'b, <::BaseBackend as Backend>::TextNode>, 89 | > 90 | where 91 | Self: Sized; 92 | 93 | /// Create a virtual element. 94 | fn create_virtual_element<'b>( 95 | this: &'b mut ForestNodeMut, 96 | ) -> Result::GeneralElement>, Error> 97 | where 98 | Self: Sized; 99 | 100 | /// Create a text node. 101 | fn create_text_node( 102 | this: &mut ForestNodeMut, 103 | content: &str, 104 | ) -> Result::GeneralElement>, Error> 105 | where 106 | Self: Sized; 107 | 108 | /// Append a child element. 109 | fn append<'b>( 110 | this: &'b mut ForestNodeMut, 111 | child: &'b ForestNodeRc< 112 | <::BaseBackend as Backend>::GeneralElement, 113 | >, 114 | ) where 115 | Self: Sized; 116 | 117 | /// Insert an element before this element. 118 | fn insert<'b>( 119 | this: &'b mut ForestNodeMut, 120 | target: &'b ForestNodeRc< 121 | <::BaseBackend as Backend>::GeneralElement, 122 | >, 123 | ) where 124 | Self: Sized; 125 | 126 | /// Detach this element temporarily. 127 | fn temp_detach( 128 | this: ForestNodeMut, 129 | ) -> ForestNodeRc<<::BaseBackend as Backend>::GeneralElement> 130 | where 131 | Self: Sized; 132 | 133 | /// Remove this element. 134 | fn detach( 135 | this: ForestNodeMut, 136 | ) -> ForestNodeRc<<::BaseBackend as Backend>::GeneralElement> 137 | where 138 | Self: Sized; 139 | 140 | /// Replace an element. 141 | fn replace_with( 142 | mut this: ForestNodeMut, 143 | replacer: ForestNodeRc< 144 | <::BaseBackend as Backend>::GeneralElement, 145 | >, 146 | ) -> ForestNodeRc<<::BaseBackend as Backend>::GeneralElement> 147 | where 148 | Self: Sized, 149 | { 150 | Self::insert(&mut this, &replacer); 151 | Self::detach(this) 152 | } 153 | } 154 | 155 | /// The virtual element in the backend. 156 | /// 157 | /// This is used by the backend implementor. 158 | /// *In most cases, it should not be used in component implementors.* 159 | pub trait BackendVirtualElement { 160 | /// The related backend type. 161 | type BaseBackend: Backend; 162 | } 163 | 164 | /// The text node in the backend. 165 | /// 166 | /// This is used by the backend implementor. 167 | /// *In most cases, it should not be used in component implementors.* 168 | pub trait BackendTextNode { 169 | /// The related backend type. 170 | type BaseBackend: Backend; 171 | 172 | /// Set the text content. 173 | fn set_text(&mut self, content: &str); 174 | } 175 | 176 | /// A trait that indicates a component or a backend-implemented element for the backend. 177 | /// 178 | /// This is used by the backend implementor. 179 | /// *In most cases, it should not be used in component implementors.* 180 | pub trait BackendComponent { 181 | /// The slot data type. 182 | type SlotData; 183 | /// The type of the updated comopnent or element. 184 | type UpdateTarget; 185 | /// The update-related data of the component or element. 186 | /// 187 | /// Should be `bool` for components. 188 | type UpdateContext; 189 | 190 | /// Create with a backend element. 191 | fn init<'b>( 192 | backend_context: &'b BackendContext, 193 | owner: &'b mut ForestNodeMut, 194 | owner_weak: &Box, 195 | ) -> Result<(Self, ForestNodeRc), Error> 196 | where 197 | Self: Sized; 198 | 199 | /// Indicate that the create process should be finished. 200 | fn create<'b>( 201 | &'b mut self, 202 | backend_context: &'b BackendContext, 203 | owner: &'b mut ForestNodeMut, 204 | update_fn: Box, 205 | slot_fn: &mut dyn FnMut( 206 | &mut tree::ForestNodeMut, 207 | &ForestToken, 208 | &Self::SlotData, 209 | ) -> Result<(), Error>, 210 | ) -> Result<(), Error>; 211 | 212 | /// Indicate that the pending updates should be applied. 213 | fn apply_updates<'b>( 214 | &'b mut self, 215 | backend_context: &'b BackendContext, 216 | owner: &'b mut ForestNodeMut, 217 | update_fn: Box, 218 | slot_fn: &mut dyn FnMut( 219 | SlotChange<&mut tree::ForestNodeMut, &ForestToken, &Self::SlotData>, 220 | ) -> Result<(), Error>, 221 | ) -> Result<(), Error>; 222 | } 223 | 224 | /// A trait that indicates a component that can be converted into a `BackendComponent` . 225 | /// 226 | /// It usually refers to a `#[component]` or a backend supported component. 227 | /// For manually usages, this should be used by the backend implementor. 228 | /// *In most cases, it should not be used in component implementors.* 229 | pub trait AsElementTag { 230 | /// The converted `BackendComponent` type. 231 | type Target: 'static; 232 | /// The slot list type. 233 | type SlotChildren: SlotKindTrait; 234 | } 235 | -------------------------------------------------------------------------------- /maomi/src/diff/keyless.rs: -------------------------------------------------------------------------------- 1 | //! The keyless list algorithm module. 2 | //! 3 | //! This is one of the list compare algorithm. 4 | //! See [diff](../) module documentation for details. 5 | //! 6 | 7 | use std::marker::PhantomData; 8 | 9 | use super::*; 10 | use crate::backend::BackendGeneralElement; 11 | 12 | /// The repeated list storing the list state. 13 | /// 14 | /// It is auto-managed by the `#[component]` . 15 | /// Do not touch unless you know how it works exactly. 16 | pub struct KeylessList { 17 | list: Vec<(C, ForestToken)>, 18 | } 19 | 20 | impl KeylessList { 21 | #[doc(hidden)] 22 | #[inline] 23 | pub fn list_diff_new<'a, 'b, B: Backend>( 24 | backend_element: &'a mut ForestNodeMut<'b, B::GeneralElement>, 25 | size_hint: usize, 26 | ) -> ListAlgo, ListKeylessAlgoUpdate<'a, 'b, B, C>> { 27 | ListAlgo::New(ListKeylessAlgoNew { 28 | list: Vec::with_capacity(size_hint), 29 | backend_element, 30 | _phantom: PhantomData, 31 | }) 32 | } 33 | 34 | #[doc(hidden)] 35 | #[inline] 36 | pub fn list_diff_update<'a, 'b, B: Backend>( 37 | &'a mut self, 38 | backend_element: &'a mut ForestNodeMut<'b, B::GeneralElement>, 39 | size_hint: usize, 40 | ) -> ListAlgo, ListKeylessAlgoUpdate<'a, 'b, B, C>> { 41 | if size_hint > self.list.len() { 42 | self.list.reserve_exact(size_hint - self.list.len()); 43 | } 44 | ListAlgo::Update(ListKeylessAlgoUpdate { 45 | cur_index: 0, 46 | list: &mut self.list, 47 | backend_element, 48 | _phantom: PhantomData, 49 | }) 50 | } 51 | } 52 | 53 | #[doc(hidden)] 54 | pub struct ListKeylessAlgoNew<'a, 'b, B: Backend, C> { 55 | list: Vec<(C, ForestToken)>, 56 | backend_element: &'a mut ForestNodeMut<'b, B::GeneralElement>, 57 | _phantom: PhantomData, 58 | } 59 | 60 | impl<'a, 'b, B: Backend, C> ListKeylessAlgoNew<'a, 'b, B, C> { 61 | #[doc(hidden)] 62 | pub fn next( 63 | &mut self, 64 | create_fn: impl FnOnce(&mut ForestNodeMut) -> Result, 65 | ) -> Result<(), Error> { 66 | let backend_element = ::create_virtual_element( 67 | self.backend_element, 68 | )?; 69 | let c = create_fn(&mut self.backend_element.borrow_mut(&backend_element))?; 70 | self.list.push((c, backend_element.token())); 71 | ::append( 72 | self.backend_element, 73 | &backend_element, 74 | ); 75 | Ok(()) 76 | } 77 | 78 | #[doc(hidden)] 79 | #[inline] 80 | pub fn end(self) -> KeylessList { 81 | KeylessList { list: self.list } 82 | } 83 | } 84 | 85 | #[doc(hidden)] 86 | pub struct ListKeylessAlgoUpdate<'a, 'b, B: Backend, C> { 87 | cur_index: usize, 88 | list: &'a mut Vec<(C, ForestToken)>, 89 | backend_element: &'a mut ForestNodeMut<'b, B::GeneralElement>, 90 | _phantom: PhantomData, 91 | } 92 | 93 | impl<'a, 'b, B: Backend, C> ListKeylessAlgoUpdate<'a, 'b, B, C> { 94 | #[doc(hidden)] 95 | pub fn next( 96 | &mut self, 97 | create_or_update_fn: impl FnOnce( 98 | Option<&mut C>, 99 | &mut ForestNodeMut, 100 | ) -> Result, Error>, 101 | ) -> Result<(), Error> { 102 | if let Some((ref mut c, forest_token)) = self.list.get_mut(self.cur_index) { 103 | if let Some(n) = &mut self.backend_element.borrow_mut_token(&forest_token) { 104 | create_or_update_fn(Some(c), n)?; 105 | } 106 | } else { 107 | let backend_element = 108 | ::create_virtual_element( 109 | self.backend_element, 110 | )?; 111 | let c = 112 | create_or_update_fn(None, &mut self.backend_element.borrow_mut(&backend_element))? 113 | .ok_or(Error::ListChangeWrong)?; 114 | self.list.push((c, backend_element.token())); 115 | ::append( 116 | self.backend_element, 117 | &backend_element, 118 | ); 119 | } 120 | self.cur_index += 1; 121 | Ok(()) 122 | } 123 | 124 | #[doc(hidden)] 125 | pub fn end(self) -> Result<(), Error> { 126 | for (_c, forest_token) in self.list.drain(self.cur_index..) { 127 | if let Some(n) = self.backend_element.borrow_mut_token(&forest_token) { 128 | ::detach(n); 129 | } 130 | } 131 | Ok(()) 132 | } 133 | } 134 | 135 | /// The iterator for a `KeylessList` 136 | pub struct KeylessListIter<'a, C> { 137 | children: std::slice::Iter<'a, (C, ForestToken)>, 138 | } 139 | 140 | impl<'a, C> Iterator for KeylessListIter<'a, C> { 141 | type Item = &'a C; 142 | 143 | fn next(&mut self) -> Option { 144 | self.children.next().map(|(x, _)| x) 145 | } 146 | 147 | fn size_hint(&self) -> (usize, Option) { 148 | self.children.size_hint() 149 | } 150 | } 151 | 152 | impl<'a, C> IntoIterator for &'a KeylessList { 153 | type Item = &'a C; 154 | type IntoIter = KeylessListIter<'a, C>; 155 | 156 | fn into_iter(self) -> Self::IntoIter { 157 | KeylessListIter { 158 | children: self.list.iter(), 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /maomi/src/diff/mod.rs: -------------------------------------------------------------------------------- 1 | //! The diff algorithm utilities. 2 | //! 3 | //! When applying list update, 4 | //! the framework tries to figure out which items should be added, removed, or moved. 5 | //! For example, in the following component: 6 | //! 7 | //! ```rust 8 | //! use maomi::prelude::*; 9 | //! 10 | //! #[component] 11 | //! struct MyComponent { 12 | //! template: template! { 13 | //! for item in self.list.iter() { 14 | //! /* ... */ 15 | //! } 16 | //! }, 17 | //! list: Vec, 18 | //! } 19 | //! ``` 20 | //! 21 | //! This requires an algorithm to compare the current `list` and the `list` that used to do previous update, 22 | //! and decide which items should be added, removed, or moved. 23 | //! 24 | //! By default, the [keyless](./keyless) algorithm is used. 25 | //! This algorithm compares items one by one, 26 | //! and adds or removes items at the ends of the list. 27 | //! For example: 28 | //! * if the `list` in the previous update is `[30, 40, 50]` while the current `list` is `[30, 40, 50, 60]` , 29 | //! then the forth item with item data `60` is added; 30 | //! * if the `list` in the previous update is `[30, 40, 50]` while the current `list` is `[30, 50]` , 31 | //! then the second item is updated with item data `50` , and the third item is removed. 32 | //! This algorithm is very fast if the items at the start and the middle of the list will not be removed or inserted, 33 | //! but it is pretty slow if that happens. 34 | //! 35 | //! For lists that often randomly changes, the [key](./key) algorithm is a better option. 36 | //! To use this algorithm, the `AsListKey` trait must be implemented for the item data, 37 | //! and the `use` instruction should be added in the template `for` expression. 38 | //! The example code above should be changed: 39 | //! 40 | //! ```rust 41 | //! use maomi::prelude::*; 42 | //! 43 | //! struct ListData { 44 | //! id: usize, 45 | //! } 46 | //! 47 | //! impl AsListKey for ListData { 48 | //! type ListKey = usize; 49 | //! 50 | //! fn as_list_key(&self) -> &usize { 51 | //! &self.id 52 | //! } 53 | //! } 54 | //! 55 | //! #[component] 56 | //! struct MyComponent { 57 | //! template: template! { 58 | //! // add a `use` list key 59 | //! for item in self.list.iter() use usize { 60 | //! /* ... */ 61 | //! } 62 | //! }, 63 | //! list: Vec, 64 | //! } 65 | //! ``` 66 | //! 67 | //! The `ListKey` is used for list comparison. 68 | //! * if the `ListKey` list in the previous update is `[30, 40, 50]` while the current is `[30, 50]` , 69 | //! then the second item is removed. 70 | //! * if the `ListKey` list in the previous update is `[30, 40, 50]` while the current is `[30, 40, 60, 50]` , 71 | //! then the third item with item data 60 is inserted; 72 | //! This algorithm has a balanced performance on lists that dynamically changes, 73 | //! while it has a small overhead no matter the list is changed or not. 74 | 75 | use crate::{ 76 | backend::{tree::*, Backend}, 77 | error::Error, 78 | }; 79 | pub mod key; 80 | pub mod keyless; 81 | 82 | #[doc(hidden)] 83 | pub enum ListAlgo { 84 | New(N), 85 | Update(U), 86 | } 87 | 88 | impl ListAlgo { 89 | #[doc(hidden)] 90 | #[inline] 91 | pub fn as_new(&mut self) -> &mut N { 92 | match self { 93 | Self::New(x) => x, 94 | Self::Update(_) => panic!("illegal list update"), 95 | } 96 | } 97 | 98 | #[doc(hidden)] 99 | #[inline] 100 | pub fn as_update(&mut self) -> &mut U { 101 | match self { 102 | Self::Update(x) => x, 103 | Self::New(_) => panic!("illegal list update"), 104 | } 105 | } 106 | 107 | #[doc(hidden)] 108 | #[inline] 109 | pub fn into_new(self) -> N { 110 | match self { 111 | Self::New(x) => x, 112 | Self::Update(_) => panic!("illegal list update"), 113 | } 114 | } 115 | 116 | #[doc(hidden)] 117 | #[inline] 118 | pub fn into_update(self) -> U { 119 | match self { 120 | Self::Update(x) => x, 121 | Self::New(_) => panic!("illegal list update"), 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /maomi/src/error.rs: -------------------------------------------------------------------------------- 1 | //! The error utilities. 2 | 3 | /// An framework error. 4 | #[derive(Debug)] 5 | pub enum Error { 6 | /// A custom error. 7 | /// 8 | /// Generally refers to errors generated by other crates. 9 | Custom(Box), 10 | /// The operation is invalid before component created. 11 | TreeNotCreated, 12 | /// A wrong node tree is visited. 13 | /// 14 | /// Generally refers to some bad operation had being done directly in the node tree. 15 | TreeNodeTypeWrong, 16 | /// The backend tree node has been released. 17 | /// 18 | /// Generally refers to some bad operation had being done directly in the node tree. 19 | TreeNodeReleased, 20 | /// A list update failed due to wrong changes list. 21 | /// 22 | /// Generally refers to some bad operation had being done directly in the node tree. 23 | ListChangeWrong, 24 | /// A recursive update is detected. 25 | /// 26 | /// An element cannot be updated while it is still in another update process. 27 | /// This will not happen while calling async update methods like `ComponentRc::task` or `ComponentRc::update` . 28 | /// Generally refers to some manually update process being incorrectly triggered. 29 | RecursiveUpdate, 30 | /// The backend context has already entered. 31 | AlreadyEntered, 32 | /// A general backend failure. 33 | BackendError { 34 | /// The message from backend. 35 | msg: String, 36 | /// The detailed backend error object. 37 | err: Option>, 38 | }, 39 | } 40 | 41 | impl std::fmt::Display for Error { 42 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 43 | match self { 44 | Error::Custom(err) => { 45 | write!(f, "{}", err)?; 46 | } 47 | Error::TreeNotCreated => { 48 | write!(f, "Component template has not been initialized")?; 49 | } 50 | Error::TreeNodeTypeWrong => { 51 | write!(f, "The node type in backend element tree is incorrect")?; 52 | } 53 | Error::TreeNodeReleased => { 54 | write!(f, "The node in backend element tree has been released")?; 55 | } 56 | Error::ListChangeWrong => { 57 | write!(f, "The list change is incorrect")?; 58 | } 59 | Error::RecursiveUpdate => { 60 | write!(f, "A recursive update is detected")?; 61 | } 62 | Error::AlreadyEntered => { 63 | write!(f, "The backend context is already entered")?; 64 | } 65 | Error::BackendError { msg, err } => { 66 | if let Some(err) = err { 67 | write!(f, "{}: {}", msg, err.to_string())?; 68 | } else { 69 | write!(f, "{}", msg)?; 70 | } 71 | } 72 | } 73 | Ok(()) 74 | } 75 | } 76 | 77 | impl std::error::Error for Error {} 78 | -------------------------------------------------------------------------------- /maomi/src/event.rs: -------------------------------------------------------------------------------- 1 | //! The event handling utilities. 2 | //! 3 | //! The event fields in components can be binded by component users, 4 | //! and triggered by the component itself. 5 | //! 6 | //! The following example shows the basic usage of events. 7 | //! 8 | //! ```rust 9 | //! use maomi::prelude::*; 10 | //! 11 | //! #[component] 12 | //! struct MyComponent { 13 | //! template: template! { 14 | //! /* ... */ 15 | //! }, 16 | //! // define an event with the detailed type 17 | //! my_event: Event, 18 | //! } 19 | //! 20 | //! impl Component for MyComponent { 21 | //! fn new() -> Self { 22 | //! Self { 23 | //! template: Default::default(), 24 | //! my_event: Event::new(), 25 | //! } 26 | //! } 27 | //! 28 | //! fn created(&self) { 29 | //! // trigger the event 30 | //! self.my_event.trigger(&mut 123); 31 | //! } 32 | //! } 33 | //! 34 | //! #[component] 35 | //! struct MyComponentUser { 36 | //! template: template! { 37 | //! // set the event listener 38 | //! 39 | //! // extra arguments can be added in the listener 40 | //! // (arguments should implement `Clone` or `ToOwned`) 41 | //! 42 | //! }, 43 | //! } 44 | //! 45 | //! impl MyComponentUser { 46 | //! // the event listener has two preset arguments: `this` and the event detailed type 47 | //! fn my_ev(this: ComponentEvent) { 48 | //! assert_eq!(this.detail().clone(), 123); 49 | //! } 50 | //! 51 | //! // with extra arguments 52 | //! fn my_ev_with_data(this: ComponentEvent, data: &str) { 53 | //! assert_eq!(this.detail().clone(), 123); 54 | //! assert_eq!(data, "abc"); 55 | //! } 56 | //! } 57 | //! ``` 58 | 59 | use crate::component::ComponentRc; 60 | 61 | /// The event handler setter. 62 | /// 63 | /// This trait is implemented by `Event` . 64 | /// Custom event types that implements this trait can also be used in templates with `=@` syntax. 65 | pub trait EventHandler { 66 | /// Must be `bool` if used in components 67 | type UpdateContext; 68 | 69 | /// Set the handler fn 70 | fn set_handler_fn( 71 | dest: &mut Self, 72 | handler_fn: Box, 73 | ctx: &mut Self::UpdateContext, 74 | ); 75 | } 76 | 77 | /// An event that can be binded and triggered. 78 | pub struct Event { 79 | handler: Option>, 80 | } 81 | 82 | impl Default for Event { 83 | fn default() -> Self { 84 | Self::new() 85 | } 86 | } 87 | 88 | impl Event { 89 | /// Initialize the event. 90 | pub fn new() -> Self { 91 | Self { handler: None } 92 | } 93 | 94 | /// Trigger the event. 95 | /// 96 | /// Binded handler will be called immediately. 97 | pub fn trigger(&self, detail: &mut D) { 98 | if let Some(f) = &self.handler { 99 | f(detail) 100 | } 101 | } 102 | } 103 | 104 | impl EventHandler for Event { 105 | type UpdateContext = bool; 106 | 107 | fn set_handler_fn( 108 | dest: &mut Self, 109 | handler_fn: Box, 110 | _ctx: &mut Self::UpdateContext, 111 | ) { 112 | dest.handler = Some(handler_fn); 113 | } 114 | } 115 | 116 | /// A helper type that contains the event target and the event detail. 117 | /// 118 | /// It implements `Deref>` so that any associated function in `ComponentRc` can be visited. 119 | pub struct ComponentEvent<'d, C: 'static, D> { 120 | rc: ComponentRc, 121 | detail: &'d mut D, 122 | } 123 | 124 | impl<'d, C: 'static, D> std::ops::Deref for ComponentEvent<'d, C, D> { 125 | type Target = ComponentRc; 126 | 127 | fn deref(&self) -> &Self::Target { 128 | &self.rc 129 | } 130 | } 131 | 132 | impl<'d, C: 'static, D> ComponentEvent<'d, C, D> { 133 | /// Create with event target and detail. 134 | pub fn new(rc: ComponentRc, detail: &'d mut D) -> Self { 135 | Self { rc, detail } 136 | } 137 | 138 | /// Get the target component. 139 | pub fn rc(&self) -> ComponentRc { 140 | self.rc.clone() 141 | } 142 | 143 | /// Clone the event detail. 144 | pub fn clone_detail(&self) -> D 145 | where 146 | D: Clone, 147 | { 148 | self.detail.clone() 149 | } 150 | 151 | /// Get the event detail. 152 | pub fn detail(&self) -> &D { 153 | &self.detail 154 | } 155 | 156 | /// Get the mutable reference of the event detail. 157 | pub fn detail_mut(&mut self) -> &mut D { 158 | &mut self.detail 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /maomi/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! maomi: a rust framework for building pages with components 2 | //! 3 | //! `maomi` is a framework for building (web) application user interface. 4 | //! It has strict compile-time check and generates fast code. 5 | //! 6 | //! This is the *core* module of the framework. 7 | //! In browsers, the `maomi-dom` crate is also needed. 8 | //! See the [`maomi_dom`](../maomi-dom) crate document for a quick start. 9 | 10 | #![warn(missing_docs)] 11 | 12 | pub mod backend; 13 | pub mod component; 14 | pub mod diff; 15 | pub mod error; 16 | pub mod event; 17 | pub mod locale_string; 18 | pub mod mount_point; 19 | pub mod node; 20 | pub mod prop; 21 | pub mod template; 22 | pub mod text_node; 23 | pub use backend::context::PrerenderingData; 24 | pub use backend::context::{AsyncCallback, BackendContext}; 25 | 26 | /// The types that should usually be imported. 27 | /// 28 | /// Usually, `use maomi::prelude::*;` should be added in component files for convinience. 29 | pub mod prelude { 30 | pub use super::component::PrerenderableComponent; 31 | pub use super::component::{Component, ComponentExt, ComponentRc}; 32 | pub use super::diff::key::AsListKey; 33 | pub use super::event::{ComponentEvent, Event}; 34 | pub use super::prop::Prop; 35 | pub use async_trait::async_trait; 36 | pub use maomi_macro::*; 37 | } 38 | -------------------------------------------------------------------------------- /maomi/src/locale_string.rs: -------------------------------------------------------------------------------- 1 | //! The translated string types, used in i18n. 2 | 3 | use std::{fmt::Display, ops::Deref}; 4 | 5 | /// Marker for a type that is i18n friendly. 6 | /// 7 | /// When i18n support is enabled, 8 | /// only types which implemented this trait can be used in text node. 9 | pub trait ToLocaleStr { 10 | /// Get the translated text. 11 | fn to_locale_str(&self) -> &str; 12 | 13 | /// Get the owned string of the translated text. 14 | fn to_locale_string(&self) -> LocaleString { 15 | LocaleString::translated(self.to_locale_str()) 16 | } 17 | } 18 | 19 | /// A translated static str. 20 | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] 21 | pub struct LocaleStaticStr(&'static str); 22 | 23 | impl LocaleStaticStr { 24 | /// Wraps a translated str. 25 | /// 26 | /// Make sure the string is translated! 27 | pub const fn translated(s: &'static str) -> Self { 28 | Self(s) 29 | } 30 | } 31 | 32 | impl ToLocaleStr for LocaleStaticStr { 33 | fn to_locale_str(&self) -> &str { 34 | self.0 35 | } 36 | } 37 | 38 | impl Display for LocaleStaticStr { 39 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 40 | self.0.fmt(f) 41 | } 42 | } 43 | 44 | impl Deref for LocaleStaticStr { 45 | type Target = str; 46 | 47 | fn deref(&self) -> &Self::Target { 48 | self.0 49 | } 50 | } 51 | 52 | impl AsRef for LocaleStaticStr { 53 | fn as_ref(&self) -> &str { 54 | self.0 55 | } 56 | } 57 | 58 | impl<'a> Into<&'a str> for &'a LocaleStaticStr { 59 | fn into(self) -> &'a str { 60 | self.0 61 | } 62 | } 63 | 64 | /// A translated string. 65 | #[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] 66 | pub struct LocaleString(String); 67 | 68 | impl LocaleString { 69 | /// Wraps a translated string. 70 | /// 71 | /// Make sure the string is translated! 72 | pub fn translated(s: impl ToString) -> Self { 73 | Self(s.to_string()) 74 | } 75 | } 76 | 77 | impl ToLocaleStr for LocaleString { 78 | fn to_locale_str(&self) -> &str { 79 | self.0.as_str() 80 | } 81 | } 82 | 83 | impl ToLocaleStr for &T { 84 | fn to_locale_str(&self) -> &str { 85 | (*self).to_locale_str() 86 | } 87 | } 88 | 89 | impl Display for LocaleString { 90 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 91 | self.0.fmt(f) 92 | } 93 | } 94 | 95 | impl Deref for LocaleString { 96 | type Target = str; 97 | 98 | fn deref(&self) -> &Self::Target { 99 | self.0.as_str() 100 | } 101 | } 102 | 103 | impl AsRef for LocaleString { 104 | fn as_ref(&self) -> &str { 105 | self.0.as_str() 106 | } 107 | } 108 | 109 | impl<'a> Into<&'a str> for &'a LocaleString { 110 | fn into(self) -> &'a str { 111 | self.0.as_str() 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /maomi/src/mount_point.rs: -------------------------------------------------------------------------------- 1 | //! The mount point utilities. 2 | 3 | use crate::backend::{tree, Backend, BackendComponent, BackendGeneralElement}; 4 | use crate::component::{Component, ComponentNode}; 5 | use crate::error::Error; 6 | use crate::node::OwnerWeak; 7 | use crate::template::ComponentTemplate; 8 | use crate::BackendContext; 9 | 10 | struct DanglingOwner(); 11 | 12 | impl OwnerWeak for DanglingOwner { 13 | fn apply_updates(&self) -> Result<(), Error> { 14 | Ok(()) 15 | } 16 | 17 | fn clone_owner_weak(&self) -> Box { 18 | Box::new(DanglingOwner()) 19 | } 20 | } 21 | 22 | /// A mount point which contains a root component. 23 | /// 24 | /// A mount point can be created through `BackendContext::attach` . 25 | pub struct MountPoint + 'static> { 26 | component_node: ComponentNode, 27 | backend_element: tree::ForestNodeRc, 28 | } 29 | 30 | impl> MountPoint { 31 | pub(crate) fn attach( 32 | backend_context: &BackendContext, 33 | parent: &mut tree::ForestNodeMut, 34 | init: impl FnOnce(&mut C), 35 | ) -> Result { 36 | let owner_weak: Box = Box::new(DanglingOwner()); 37 | let (mut component_node, backend_element) = 38 | as BackendComponent>::init(backend_context, parent, &owner_weak)?; 39 | as BackendComponent>::create( 40 | &mut component_node, 41 | backend_context, 42 | parent, 43 | Box::new(|comp, _| init(comp)), 44 | &mut |_, _, _| Ok(()), 45 | )?; 46 | let this = Self { 47 | component_node, 48 | backend_element, 49 | }; 50 | ::append(parent, &this.backend_element); 51 | Ok(this) 52 | } 53 | 54 | pub(crate) fn detach(&mut self, parent: &mut tree::ForestNodeMut) { 55 | let elem = parent.borrow_mut(&self.backend_element); 56 | ::detach(elem); 57 | } 58 | 59 | /// Get the root component node. 60 | pub fn root_component(&self) -> &ComponentNode { 61 | &self.component_node 62 | } 63 | 64 | /// Get the `dyn` form of the mount point 65 | /// 66 | /// This is useful for storing a mount point without its exact component type. 67 | pub fn into_dyn(self) -> DynMountPoint { 68 | DynMountPoint { 69 | _component_node: Box::new(self.component_node), 70 | backend_element: self.backend_element, 71 | } 72 | } 73 | } 74 | 75 | /// The `dyn` form of the mount point. 76 | /// 77 | /// This form does not contain the root component type. 78 | pub struct DynMountPoint { 79 | _component_node: Box, 80 | backend_element: tree::ForestNodeRc, 81 | } 82 | 83 | impl DynMountPoint { 84 | pub(crate) fn detach(&mut self, parent: &mut tree::ForestNodeMut) { 85 | let elem = parent.borrow_mut(&self.backend_element); 86 | ::detach(elem); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /maomi/src/template.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for template management. 2 | //! 3 | //! Most utilities in this module is used by `#[component]` . 4 | 5 | use std::cell::{Cell, Ref, RefCell}; 6 | 7 | use crate::{ 8 | backend::{tree::*, Backend}, 9 | component::*, 10 | error::Error, 11 | node::{OwnerWeak, SlotChange, SlotKindTrait}, 12 | prop::Prop, 13 | BackendContext, 14 | }; 15 | 16 | /// An init object for the template. 17 | /// 18 | /// It is auto-managed by the `#[component]` . 19 | /// Do not touch unless you know how it works exactly. 20 | pub struct TemplateInit { 21 | pub(crate) updater: ComponentWeak, 22 | } 23 | 24 | /// Some helper functions for the template type. 25 | pub trait TemplateHelper: Default { 26 | /// Mark the template that update is needed. 27 | /// 28 | /// It is auto-managed by the `#[component]` . 29 | /// Do not touch unless you know how it works exactly. 30 | fn mark_dirty(&self) 31 | where 32 | C: 'static; 33 | 34 | /// Clear the mark. 35 | /// 36 | /// It is auto-managed by the `#[component]` . 37 | /// Do not touch unless you know how it works exactly. 38 | fn clear_dirty(&self) -> bool 39 | where 40 | C: 'static; 41 | 42 | /// Returns whether the template has been initialized. 43 | fn is_initialized(&self) -> bool; 44 | 45 | /// Get the template inner node tree. 46 | fn structure(&self) -> Option>; 47 | 48 | /// Get the template inner node tree. 49 | fn slot_scopes(&self) -> Ref; 50 | 51 | /// Get the corresponding `ComponentRc` . 52 | fn component_rc(&self) -> Result, Error> 53 | where 54 | C: 'static + Sized; 55 | 56 | /// Get the corresponding `ComponentWeak` . 57 | fn component_weak(&self) -> Result, Error> 58 | where 59 | C: 'static + Sized; 60 | 61 | #[doc(hidden)] 62 | fn extract_pending_slot_changes( 63 | &self, 64 | new_changes: Vec>, 65 | ) -> Vec>; 66 | 67 | /// Get the `OwnerWeak` of the current component. 68 | fn self_owner_weak(&self) -> &Box; 69 | } 70 | 71 | /// The template type. 72 | /// 73 | /// It is auto-managed by the `#[component]` . 74 | /// Do not touch unless you know how it works exactly. 75 | pub struct Template { 76 | #[doc(hidden)] 77 | pub __m_self_owner_weak: Option>, 78 | updater: Option>, 79 | dirty: Cell, 80 | #[doc(hidden)] 81 | pub __m_structure: Option>, 82 | #[doc(hidden)] 83 | pub __m_slot_scopes: RefCell, 84 | #[doc(hidden)] 85 | pub __m_pending_slot_changes: Cell>>, 86 | } 87 | 88 | impl Default for Template { 89 | fn default() -> Self { 90 | Self { 91 | __m_self_owner_weak: None, 92 | updater: None, 93 | dirty: Cell::new(false), 94 | __m_structure: None, 95 | __m_slot_scopes: RefCell::new(L::default()), 96 | __m_pending_slot_changes: Cell::new(Vec::with_capacity(0)), 97 | } 98 | } 99 | } 100 | 101 | impl Template { 102 | #[doc(hidden)] 103 | #[inline] 104 | pub fn init(&mut self, init: TemplateInit) { 105 | self.__m_self_owner_weak = Some(init.updater.to_owner_weak()); 106 | self.updater = Some(init.updater); 107 | } 108 | } 109 | 110 | impl TemplateHelper for Template { 111 | #[inline] 112 | fn mark_dirty(&self) 113 | where 114 | C: 'static, 115 | { 116 | if self.__m_structure.is_some() { 117 | self.dirty.set(true); 118 | } 119 | } 120 | 121 | #[inline] 122 | fn clear_dirty(&self) -> bool 123 | where 124 | C: 'static, 125 | { 126 | self.dirty.replace(false) 127 | } 128 | 129 | #[inline] 130 | fn is_initialized(&self) -> bool { 131 | self.__m_structure.is_some() 132 | } 133 | 134 | #[inline] 135 | fn structure(&self) -> Option> { 136 | self.__m_structure 137 | .as_ref() 138 | .and_then(|x| x.try_borrow().ok()) 139 | } 140 | 141 | #[inline] 142 | fn slot_scopes(&self) -> Ref { 143 | self.__m_slot_scopes.borrow() 144 | } 145 | 146 | #[inline] 147 | fn component_rc(&self) -> Result, Error> 148 | where 149 | C: 'static, 150 | { 151 | self.updater 152 | .as_ref() 153 | .and_then(|x| x.upgrade()) 154 | .ok_or(Error::TreeNotCreated) 155 | } 156 | 157 | #[inline] 158 | fn component_weak(&self) -> Result, Error> 159 | where 160 | C: 'static, 161 | { 162 | self.updater 163 | .as_ref() 164 | .map(|x| x.clone()) 165 | .ok_or(Error::TreeNotCreated) 166 | } 167 | 168 | #[inline] 169 | fn extract_pending_slot_changes( 170 | &self, 171 | new_changes: Vec>, 172 | ) -> Vec> { 173 | self.__m_pending_slot_changes.replace(new_changes) 174 | } 175 | 176 | #[inline] 177 | fn self_owner_weak(&self) -> &Box { 178 | self.__m_self_owner_weak.as_ref().unwrap() 179 | } 180 | } 181 | 182 | /// The slot types which is associated with the template. 183 | /// 184 | /// It is auto-managed by the `#[component]` . 185 | /// Do not touch unless you know how it works exactly. 186 | pub trait ComponentSlotKind { 187 | /// The slot list type. 188 | type SlotChildren: SlotKindTrait; 189 | 190 | /// The type of the slot data, specified through `#[component(SlotData = ...)]`. 191 | type SlotData: 'static; 192 | } 193 | 194 | /// A component template 195 | /// 196 | /// It is auto-managed by the `#[component]` . 197 | /// Do not touch unless you know how it works exactly. 198 | pub trait ComponentTemplate: ComponentSlotKind { 199 | /// The type of the template field. 200 | type TemplateField: TemplateHelper< 201 | Self, 202 | Self::TemplateStructure, 203 | Self::SlotChildren<(ForestToken, Prop)>, 204 | >; 205 | 206 | /// The type of the template inner structure. 207 | type TemplateStructure; 208 | 209 | /// Get a reference of the template field of the component. 210 | fn template(&self) -> &Self::TemplateField; 211 | 212 | /// Initialize a template. 213 | fn template_init(&mut self, init: TemplateInit) 214 | where 215 | Self: Sized; 216 | 217 | /// Create a component within the specified shadow root. 218 | fn template_create_or_update<'b>( 219 | &'b mut self, 220 | backend_context: &'b BackendContext, 221 | backend_element: &'b mut ForestNodeMut, 222 | slot_fn: &mut dyn FnMut( 223 | SlotChange<&mut ForestNodeMut, &ForestToken, &Self::SlotData>, 224 | ) -> Result<(), Error>, 225 | ) -> Result<(), Error> 226 | where 227 | Self: Sized; 228 | 229 | /// Update a component and store the slot changes. 230 | #[inline(never)] 231 | fn template_update_store_slot_changes<'b>( 232 | &'b mut self, 233 | backend_context: &'b BackendContext, 234 | backend_element: &'b mut ForestNodeMut, 235 | ) -> Result 236 | where 237 | Self: Sized, 238 | { 239 | let mut slot_changes: Vec> = Vec::with_capacity(0); 240 | self.template_create_or_update(backend_context, backend_element, &mut |slot_change| { 241 | match slot_change { 242 | SlotChange::Unchanged(..) => {} 243 | SlotChange::DataChanged(_, n, _) => { 244 | slot_changes.push(SlotChange::DataChanged((), n.clone(), ())) 245 | } 246 | SlotChange::Added(_, n, _) => { 247 | slot_changes.push(SlotChange::Added((), n.clone(), ())) 248 | } 249 | SlotChange::Removed(n) => slot_changes.push(SlotChange::Removed(n.clone())), 250 | } 251 | Ok(()) 252 | })?; 253 | if slot_changes.len() > 0 { 254 | if self 255 | .template() 256 | .extract_pending_slot_changes(slot_changes) 257 | .len() 258 | > 0 259 | { 260 | Err(Error::ListChangeWrong)?; 261 | } 262 | Ok(true) 263 | } else { 264 | Ok(false) 265 | } 266 | } 267 | 268 | /// Iterate over slots. 269 | #[inline(never)] 270 | fn for_each_slot_scope<'b>( 271 | &'b mut self, 272 | backend_element: &'b mut ForestNodeMut, 273 | slot_fn: &mut dyn FnMut( 274 | SlotChange<&mut ForestNodeMut, &ForestToken, &Self::SlotData>, 275 | ) -> Result<(), Error>, 276 | ) -> Result<(), Error> { 277 | for (t, d) in self.template().slot_scopes().iter() { 278 | let n = &mut backend_element 279 | .borrow_mut_token(t) 280 | .ok_or(Error::TreeNodeReleased)?; 281 | slot_fn(SlotChange::Unchanged(n, t, d))?; 282 | } 283 | Ok(()) 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /maomi/src/text_node.rs: -------------------------------------------------------------------------------- 1 | //! Helper types for text nodes. 2 | 3 | use crate::{ 4 | backend::{tree, Backend, BackendGeneralElement, BackendTextNode}, 5 | error::Error, 6 | locale_string::ToLocaleStr, 7 | }; 8 | 9 | /// A text node 10 | pub struct TextNode { 11 | backend_element_token: tree::ForestToken, 12 | } 13 | 14 | impl TextNode { 15 | /// Create a text node. 16 | #[inline] 17 | pub fn create( 18 | owner: &mut tree::ForestNodeMut, 19 | content: impl ToLocaleStr, 20 | ) -> Result<(Self, tree::ForestNodeRc), Error> 21 | where 22 | Self: Sized, 23 | { 24 | let elem = B::GeneralElement::create_text_node(owner, content.to_locale_str())?; 25 | let this = Self { 26 | backend_element_token: elem.token(), 27 | }; 28 | Ok((this, elem)) 29 | } 30 | 31 | /// Get the backend element. 32 | #[inline] 33 | pub fn backend_element_rc<'b, B: Backend>( 34 | &'b mut self, 35 | owner: &'b mut tree::ForestNodeMut, 36 | ) -> Result, Error> { 37 | owner 38 | .resolve_token(&self.backend_element_token) 39 | .ok_or(Error::TreeNodeReleased) 40 | } 41 | 42 | /// Set the text content. 43 | #[inline] 44 | pub fn set_text( 45 | &mut self, 46 | owner: &mut tree::ForestNodeMut, 47 | content: impl ToLocaleStr, 48 | ) -> Result<(), Error> { 49 | if let Some(mut text_node) = owner.borrow_mut_token(&self.backend_element_token) { 50 | let mut text_node = B::GeneralElement::as_text_node_mut(&mut text_node) 51 | .ok_or(Error::TreeNodeTypeWrong)?; 52 | text_node.set_text(content.to_locale_str()); 53 | } 54 | Ok(()) 55 | } 56 | } 57 | --------------------------------------------------------------------------------