├── .github └── workflows │ └── gh-pages.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── benches ├── Cargo.toml └── benches │ └── benchmark.rs ├── rscx-macros ├── Cargo.toml └── src │ └── lib.rs ├── rscx ├── Cargo.toml ├── examples │ ├── autogenerated_props.rs │ ├── context.rs │ ├── simple.rs │ └── with_option.rs └── src │ ├── attributes.rs │ ├── axum.rs │ ├── context.rs │ ├── format_wrapper.rs │ ├── lib.rs │ ├── props.rs │ └── render.rs └── website ├── .gitignore ├── Cargo.toml ├── README.md ├── justfile ├── pages ├── index.md └── subfolder │ └── index.md └── src └── main.rs /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Deploy website on GitHub Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Build job 26 | build: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | - name: Setup Pages 32 | uses: actions/configure-pages@v3 33 | - uses: actions-rust-lang/setup-rust-toolchain@v1 34 | - name: Generate HTML 35 | run: cargo run --release 36 | working-directory: ./website 37 | - name: Copy bundle.css 38 | run: cp ./target/csm/bundle.css dist/ 39 | working-directory: ./website 40 | - name: Upload artifact 41 | uses: actions/upload-pages-artifact@v2 42 | with: 43 | path: ./website/dist/ 44 | 45 | # Deployment job 46 | deploy: 47 | environment: 48 | name: github-pages 49 | url: ${{ steps.deployment.outputs.page_url }} 50 | runs-on: ubuntu-latest 51 | needs: build 52 | steps: 53 | - name: Deploy to GitHub Pages 54 | id: deployment 55 | uses: actions/deploy-pages@v2 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "rscx", 5 | "rscx-macros", 6 | "benches", 7 | ] 8 | exclude = [ 9 | "website", 10 | ] 11 | 12 | [workspace.package] 13 | version = "0.1.14" 14 | 15 | [workspace.dependencies] 16 | rscx = { path = "./rscx", version = "0.1.14" } 17 | rscx-macros = { path = "./rscx-macros", version = "0.1.14" } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Antonio Pitasi 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 | ![crates.io](https://img.shields.io/crates/v/rscx.svg) 2 | 3 | # RSCx - Rust Server Components 4 | 5 | RSCx is a server-side HTML rendering engine library with a neat developer 6 | experience and great performance. 7 | 8 | Features: 9 | 10 | - all components are async functions 11 | - JSX-like syntax called RSX parsed with [rstml](https://github.com/rs-tml/rstml) 12 | - contexts, to easily pass values down the components tree ([example](https://github.com/Pitasi/rscx/blob/main/rscx/examples/context.rs)) 13 | - inspired by [Maud](https://maud.lambda.xyz/) and [Leptos](https://leptos.dev/) 14 | 15 | *⚠️ Warning: not production-ready yet. It lacks important features such as HTML 16 | escaping!* 17 | 18 | 19 | ## Usage 20 | 21 | All the examples can be found in [rscx/examples/](https://github.com/Pitasi/rscx/tree/main/rscx/examples). 22 | 23 | ```rs 24 | use rscx::{component, html, props, CollectFragment}; 25 | 26 | #[tokio::main] 27 | async fn main() -> Result<(), Box> { 28 | let app = app().await; 29 | println!("{}", app); 30 | Ok(()) 31 | } 32 | 33 | // simple function returning a String 34 | // it will call the Items() function 35 | async fn app() -> String { 36 | let s = "ul { color: red; }"; 37 | html! { 38 | 39 | 40 | 41 | 42 | 43 | 44 | // call a component with no props 45 |
46 | 47 | // call a component with props and children 48 |
49 | 50 |
51 | 52 | 53 | } 54 | } 55 | 56 | #[component] 57 | /// mark functions with #[component] to use them as components inside html! macro 58 | fn Section( 59 | // you can use `builder` attributes to specify a default value (makes this prop optional) 60 | #[builder(default = "Default title".into(), setter(into))] title: String, 61 | #[builder(default)] children: String, 62 | ) -> String { 63 | html! { 64 |
65 |

{ title }

66 | { children } 67 |
68 | } 69 | } 70 | 71 | #[component] 72 | async fn Items() -> String { 73 | let data = load_data_async().await; 74 | html! { 75 | 83 | } 84 | } 85 | 86 | /// async functions can be easily used in the body of a component, as every component is an async 87 | /// function 88 | async fn load_data_async() -> Vec { 89 | vec!["a".to_string(), "b".to_string(), "c".to_string()] 90 | } 91 | ``` 92 | 93 | 94 | ## Benchmarks 95 | 96 | RSCx is fast. 97 | 98 | *Disclaimer*: RSCx is for servers, as the name suggests. Therefore the following 99 | comparisons with Leptos are unfair. This library contains only a fraction of 100 | Leptos' features. 101 | 102 | *Disclaimer 2*: The benchmarks are pretty basics and should not influence your 103 | decision on whether to use or not this library. Focus on the DX. They are 104 | included as I kept running them to make sure I didn't fall too much behind 105 | alternatives. 106 | 107 | The time in the middle of the three is the average. 108 | 109 | ### Run the benchmarks locally 110 | 111 | ``` 112 | cd bench 113 | # cargo install criterion 114 | cargo criterion 115 | ``` 116 | 117 | 118 | ### Benchmark 1: single element, lots of HTML attributes 119 | 120 | ``` 121 | many_attrs/maud_many_attrs 122 | time: [205.89 ns 208.35 ns 211.53 ns] 123 | many_attrs/horrorshow_many_attrs 124 | time: [37.221 µs 37.304 µs 37.401 µs] 125 | many_attrs/html_node_many_attrs 126 | time: [67.726 µs 67.830 µs 67.939 µs] 127 | many_attrs/leptos_many_attrs 128 | time: [923.31 ns 928.46 ns 935.04 ns] 129 | many_attrs/rscx_many_attrs 130 | time: [207.96 ns 212.82 ns 219.28 ns] 131 | ``` 132 | 133 | RSCx and Maud pretty much are the same as their macros output is effectively a 134 | static string with the result. 135 | 136 | 137 | ### Benchmark 2: little element with props and child 138 | 139 | ``` 140 | small_fragment/maud_small_fragment 141 | time: [107.60 ns 107.71 ns 107.81 ns] 142 | small_fragment/horrorshow_small_fragment 143 | time: [405.98 ns 406.08 ns 406.21 ns] 144 | small_fragment/leptos_small_fragment 145 | time: [1.7641 µs 1.7652 µs 1.7662 µs] 146 | small_fragment/rscx_small_fragment 147 | time: [101.79 ns 101.87 ns 101.97 ns] 148 | ``` 149 | 150 | RSCx offers a better DX than Maud, as the syntax is nicer and values such as i32 151 | can be passed as props/attributes, while in Maud every attribute must be a 152 | static string. 153 | 154 | 155 | ### Benchmark 3: dynamic attributes (read for variable) 156 | 157 | ``` 158 | many_dyn_attrs/horrorshow_many_dyn_attrs 159 | time: [50.445 µs 50.702 µs 50.977 µs] 160 | many_dyn_attrs/leptos_many_dyn_attrs 161 | time: [100.13 µs 100.52 µs 101.00 µs] 162 | many_dyn_attrs/rscx_many_dyn_attrs 163 | time: [33.953 µs 33.990 µs 34.037 µs] 164 | ``` 165 | 166 | 167 | ### Benchmark 4: async component rendering a list of 100 items 168 | 169 | ``` 170 | async_list/maud_async_list 171 | time: [2.3114 µs 2.3241 µs 2.3377 µs] 172 | async_list/leptos_async_list 173 | time: [55.149 µs 55.228 µs 55.315 µs] 174 | async_list/rscx_async_list 175 | time: [5.4809 µs 5.4987 µs 5.5151 µs] 176 | ``` 177 | 178 | I'll reiterate the disclaimer: Leptos is not specifically made for SSR. Going 179 | through its reactivity system (using async resources) adds overhead. 180 | 181 | -------------------------------------------------------------------------------- /benches/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "benches" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dev-dependencies] 7 | criterion = { version = "0.4", features = ["html_reports", "async_tokio"] } 8 | maud = "0.25.0" 9 | rscx = { path = "../rscx" } 10 | 11 | [[bench]] 12 | name = "benchmark" 13 | harness = false 14 | 15 | [dependencies] 16 | futures = "0.3.28" 17 | futures-util = "0.3.28" 18 | horrorshow = "0.8.4" 19 | html-node = "0.2.3" 20 | leptos = { version = "0.5.0-beta2", features = ["ssr"] } 21 | tokio = "1.32.0" 22 | -------------------------------------------------------------------------------- /benches/benches/benchmark.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | use criterion::{criterion_group, criterion_main, Criterion}; 4 | use futures::StreamExt; 5 | use leptos::IntoView; 6 | use rscx::props; 7 | 8 | fn criterion_benchmark(c: &mut Criterion) { 9 | let async_rt = tokio::runtime::Builder::new_current_thread() 10 | .build() 11 | .unwrap(); 12 | 13 | let mut g = c.benchmark_group("many_attrs"); 14 | g.bench_function("maud_many_attrs", |b| b.iter(|| maud_many_attrs())); 15 | g.bench_function("horrorshow_many_attrs", |b| { 16 | b.iter(|| horrorshow_many_attrs()) 17 | }); 18 | g.bench_function("html_node_many_attrs", |b| { 19 | b.iter(|| html_node_many_attrs()) 20 | }); 21 | g.bench_function("leptos_many_attrs", |b| b.iter(|| leptos_many_attrs())); 22 | g.bench_function("rscx_many_attrs", |b| b.iter(|| rscx_many_attrs())); 23 | g.finish(); 24 | 25 | let mut g = c.benchmark_group("many_dyn_attrs"); 26 | g.bench_function("horrorshow_many_dyn_attrs", |b| { 27 | b.iter(|| horrorshow_many_dyn_attrs()) 28 | }); 29 | g.bench_function("leptos_many_dyn_attrs", |b| { 30 | b.iter(|| leptos_many_dyn_attrs()) 31 | }); 32 | g.bench_function("rscx_many_dyn_attrs", |b| b.iter(|| rscx_many_dyn_attrs())); 33 | g.finish(); 34 | 35 | let mut g = c.benchmark_group("small_fragment"); 36 | g.bench_function("maud_small_fragment", |b| b.iter(|| maud_small_fragment())); 37 | g.bench_function("horrorshow_small_fragment", |b| { 38 | b.iter(|| horrorshow_small_fragment()) 39 | }); 40 | g.bench_function("leptos_small_fragment", |b| { 41 | b.iter(|| leptos_small_fragment()) 42 | }); 43 | g.bench_function("rscx_small_fragment", |b| { 44 | b.to_async(&async_rt).iter(|| rscx_small_fragment()) 45 | }); 46 | g.finish(); 47 | 48 | let mut g = c.benchmark_group("async_list"); 49 | g.bench_function("maud_async_list", |b| { 50 | b.to_async(&async_rt).iter(|| maud_async_list_app()) 51 | }); 52 | g.bench_function("leptos_async_list", |b| { 53 | b.to_async(&async_rt).iter(|| leptos_async_list_app()) 54 | }); 55 | g.bench_function("rscx_async_list", |b| { 56 | b.to_async(&async_rt).iter(|| rscx_async_list_app()) 57 | }); 58 | g.finish() 59 | } 60 | 61 | criterion_group!(benches, criterion_benchmark); 62 | criterion_main!(benches); 63 | 64 | fn maud_many_attrs() -> String { 65 | maud::html! { 66 | h1 attr1="1" attr2="2" attr3="3" attr4="4" attr5="5" attr6="6" attr7="7" attr8="8" attr9="9" attr10="10" attr11="11" attr12="12" attr13="13" attr14="14" attr15="15" attr16="16" attr17="17" attr18="18" attr19="19" attr20="20" attr21="21" attr22="22" attr23="23" attr24="24" attr25="25" attr26="26" attr27="27" attr28="28" attr29="29" attr30="30" attr31="31" attr32="32" attr33="33" attr34="34" attr35="35" attr36="36" attr37="37" attr38="38" attr39="39" attr40="40" attr41="41" attr42="42" attr43="43" attr44="44" attr45="45" attr46="46" attr47="47" attr48="48" attr49="49" attr50="50" attr51="51" attr52="52" attr53="53" attr54="54" attr55="55" attr56="56" attr57="57" attr58="58" attr59="59" attr60="60" attr61="61" attr62="62" attr63="63" attr64="64" attr65="65" attr66="66" attr67="67" attr68="68" attr69="69" attr70="70" attr71="71" attr72="72" attr73="73" attr74="74" attr75="75" attr76="76" attr77="77" attr78="78" attr79="79" attr80="80" attr81="81" attr82="82" attr83="83" attr84="84" attr85="85" attr86="86" attr87="87" attr88="88" attr89="89" attr90="90" attr91="91" attr92="92" attr93="93" attr94="94" attr95="95" attr96="96" attr97="97" attr98="98" attr99="99" attr100="100" attr101="101" attr102="102" attr103="103" attr104="104" attr105="105" attr106="106" attr107="107" attr108="108" attr109="109" attr110="110" attr111="111" attr112="112" attr113="113" attr114="114" attr115="115" attr116="116" attr117="117" attr118="118" attr119="119" attr120="120" attr121="121" attr122="122" attr123="123" attr124="124" attr125="125" attr126="126" attr127="127" attr128="128" attr129="129" attr130="130" attr131="131" attr132="132" attr133="133" attr134="134" attr135="135" attr136="136" attr137="137" attr138="138" attr139="139" attr140="140" attr141="141" attr142="142" attr143="143" attr144="144" attr145="145" attr146="146" attr147="147" attr148="148" attr149="149" attr150="150" attr151="151" attr152="152" attr153="153" attr154="154" attr155="155" attr156="156" attr157="157" attr158="158" attr159="159" attr160="160" attr161="161" attr162="162" attr163="163" attr164="164" attr165="165" attr166="166" attr167="167" attr168="168" attr169="169" attr170="170" attr171="171" attr172="172" attr173="173" attr174="174" attr175="175" attr176="176" attr177="177" attr178="178" attr179="179" attr180="180" attr181="181" attr182="182" attr183="183" attr184="184" attr185="185" attr186="186" attr187="187" attr188="188" attr189="189" attr190="190" attr191="191" attr192="192" attr193="193" attr194="194" attr195="195" attr196="196" attr197="197" attr198="198" attr199="199" attr200="200" attr201="201" attr202="202" attr203="203" attr204="204" attr205="205" attr206="206" attr207="207" attr208="208" attr209="209" attr210="210" attr211="211" attr212="212" attr213="213" attr214="214" attr215="215" attr216="216" attr217="217" attr218="218" attr219="219" attr220="220" attr221="221" attr222="222" attr223="223" attr224="224" attr225="225" attr226="226" attr227="227" attr228="228" attr229="229" attr230="230" attr231="231" attr232="232" attr233="233" attr234="234" attr235="235" attr236="236" attr237="237" attr238="238" attr239="239" attr240="240" attr241="241" attr242="242" attr243="243" attr244="244" attr245="245" attr246="246" attr247="247" attr248="248" attr249="249" attr250="250" attr251="251" attr252="252" attr253="253" attr254="254" attr255="255" attr256="256" attr257="257" attr258="258" attr259="259" attr260="260" attr261="261" attr262="262" attr263="263" attr264="264" attr265="265" attr266="266" attr267="267" attr268="268" attr269="269" attr270="270" attr271="271" attr272="272" attr273="273" attr274="274" attr275="275" attr276="276" attr277="277" attr278="278" attr279="279" attr280="280" attr281="281" attr282="282" attr283="283" attr284="284" attr285="285" attr286="286" attr287="287" attr288="288" attr289="289" attr290="290" attr291="291" attr292="292" attr293="293" attr294="294" attr295="295" attr296="296" attr297="297" attr298="298" attr299="299" attr300="300" attr301="301" attr302="302" attr303="303" attr304="304" attr305="305" attr306="306" attr307="307" attr308="308" attr309="309" attr310="310" attr311="311" attr312="312" attr313="313" attr314="314" attr315="315" attr316="316" attr317="317" attr318="318" attr319="319" attr320="320" attr321="321" attr322="322" attr323="323" attr324="324" attr325="325" attr326="326" attr327="327" attr328="328" attr329="329" attr330="330" attr331="331" attr332="332" attr333="333" attr334="334" attr335="335" attr336="336" attr337="337" attr338="338" attr339="339" attr340="340" attr341="341" attr342="342" attr343="343" attr344="344" attr345="345" attr346="346" attr347="347" attr348="348" attr349="349" attr350="350" attr351="351" attr352="352" attr353="353" attr354="354" attr355="355" attr356="356" attr357="357" attr358="358" attr359="359" attr360="360" attr361="361" attr362="362" attr363="363" attr364="364" attr365="365" attr366="366" attr367="367" attr368="368" attr369="369" attr370="370" attr371="371" attr372="372" attr373="373" attr374="374" attr375="375" attr376="376" attr377="377" attr378="378" attr379="379" attr380="380" attr381="381" attr382="382" attr383="383" attr384="384" attr385="385" attr386="386" attr387="387" attr388="388" attr389="389" attr390="390" attr391="391" attr392="392" attr393="393" attr394="394" attr395="395" attr396="396" attr397="397" attr398="398" attr399="399" attr400="400" attr401="401" attr402="402" attr403="403" attr404="404" attr405="405" attr406="406" attr407="407" attr408="408" attr409="409" attr410="410" attr411="411" attr412="412" attr413="413" attr414="414" attr415="415" attr416="416" attr417="417" attr418="418" attr419="419" attr420="420" attr421="421" attr422="422" attr423="423" attr424="424" attr425="425" attr426="426" attr427="427" attr428="428" attr429="429" attr430="430" attr431="431" attr432="432" attr433="433" attr434="434" attr435="435" attr436="436" attr437="437" attr438="438" attr439="439" attr440="440" attr441="441" attr442="442" attr443="443" attr444="444" attr445="445" attr446="446" attr447="447" attr448="448" attr449="449" attr450="450" attr451="451" attr452="452" attr453="453" attr454="454" attr455="455" attr456="456" attr457="457" attr458="458" attr459="459" attr460="460" attr461="461" attr462="462" attr463="463" attr464="464" attr465="465" attr466="466" attr467="467" attr468="468" attr469="469" attr470="470" attr471="471" attr472="472" attr473="473" attr474="474" attr475="475" attr476="476" attr477="477" attr478="478" attr479="479" attr480="480" attr481="481" attr482="482" attr483="483" attr484="484" attr485="485" attr486="486" attr487="487" attr488="488" attr489="489" attr490="490" attr491="491" attr492="492" attr493="493" attr494="494" attr495="495" attr496="496" attr497="497" attr498="498" attr499="499" attr500="500" attr501="501" attr502="502" attr503="503" attr504="504" attr505="505" attr506="506" attr507="507" attr508="508" attr509="509" attr510="510" attr511="511" attr512="512" attr513="513" attr514="514" attr515="515" attr516="516" attr517="517" attr518="518" attr519="519" attr520="520" attr521="521" attr522="522" attr523="523" attr524="524" attr525="525" attr526="526" attr527="527" attr528="528" attr529="529" attr530="530" attr531="531" attr532="532" attr533="533" attr534="534" attr535="535" attr536="536" attr537="537" attr538="538" attr539="539" attr540="540" attr541="541" attr542="542" attr543="543" attr544="544" attr545="545" attr546="546" attr547="547" attr548="548" attr549="549" attr550="550" attr551="551" attr552="552" attr553="553" attr554="554" attr555="555" attr556="556" attr557="557" attr558="558" attr559="559" attr560="560" attr561="561" attr562="562" attr563="563" attr564="564" attr565="565" attr566="566" attr567="567" attr568="568" attr569="569" attr570="570" attr571="571" attr572="572" attr573="573" attr574="574" attr575="575" attr576="576" attr577="577" attr578="578" attr579="579" attr580="580" attr581="581" attr582="582" attr583="583" attr584="584" attr585="585" attr586="586" attr587="587" attr588="588" attr589="589" attr590="590" attr591="591" attr592="592" attr593="593" attr594="594" attr595="595" attr596="596" attr597="597" attr598="598" attr599="599" attr600="600" attr601="601" attr602="602" attr603="603" attr604="604" attr605="605" attr606="606" attr607="607" attr608="608" attr609="609" attr610="610" attr611="611" attr612="612" attr613="613" attr614="614" attr615="615" attr616="616" attr617="617" attr618="618" attr619="619" attr620="620" attr621="621" attr622="622" attr623="623" attr624="624" attr625="625" attr626="626" attr627="627" attr628="628" attr629="629" attr630="630" attr631="631" attr632="632" attr633="633" attr634="634" attr635="635" attr636="636" attr637="637" attr638="638" attr639="639" attr640="640" attr641="641" attr642="642" attr643="643" attr644="644" attr645="645" attr646="646" attr647="647" attr648="648" attr649="649" attr650="650" attr651="651" attr652="652" attr653="653" attr654="654" attr655="655" attr656="656" attr657="657" attr658="658" attr659="659" attr660="660" attr661="661" attr662="662" attr663="663" attr664="664" attr665="665" attr666="666" attr667="667" attr668="668" attr669="669" attr670="670" attr671="671" attr672="672" attr673="673" attr674="674" attr675="675" attr676="676" attr677="677" attr678="678" attr679="679" attr680="680" attr681="681" attr682="682" attr683="683" attr684="684" attr685="685" attr686="686" attr687="687" attr688="688" attr689="689" attr690="690" attr691="691" attr692="692" attr693="693" attr694="694" attr695="695" attr696="696" attr697="697" attr698="698" attr699="699" attr700="700" attr701="701" attr702="702" attr703="703" attr704="704" attr705="705" attr706="706" attr707="707" attr708="708" attr709="709" attr710="710" attr711="711" attr712="712" attr713="713" attr714="714" attr715="715" attr716="716" attr717="717" attr718="718" attr719="719" attr720="720" attr721="721" attr722="722" attr723="723" attr724="724" attr725="725" attr726="726" attr727="727" attr728="728" attr729="729" attr730="730" attr731="731" attr732="732" attr733="733" attr734="734" attr735="735" attr736="736" attr737="737" attr738="738" attr739="739" attr740="740" attr741="741" attr742="742" attr743="743" attr744="744" attr745="745" attr746="746" attr747="747" attr748="748" attr749="749" attr750="750" attr751="751" attr752="752" attr753="753" attr754="754" attr755="755" attr756="756" attr757="757" attr758="758" attr759="759" attr760="760" attr761="761" attr762="762" attr763="763" attr764="764" attr765="765" attr766="766" attr767="767" attr768="768" attr769="769" attr770="770" attr771="771" attr772="772" attr773="773" attr774="774" attr775="775" attr776="776" attr777="777" attr778="778" attr779="779" attr780="780" attr781="781" attr782="782" attr783="783" attr784="784" attr785="785" attr786="786" attr787="787" attr788="788" attr789="789" attr790="790" attr791="791" attr792="792" attr793="793" attr794="794" attr795="795" attr796="796" attr797="797" attr798="798" attr799="799" attr800="800" attr801="801" attr802="802" attr803="803" attr804="804" attr805="805" attr806="806" attr807="807" attr808="808" attr809="809" attr810="810" attr811="811" attr812="812" attr813="813" attr814="814" attr815="815" attr816="816" attr817="817" attr818="818" attr819="819" attr820="820" attr821="821" attr822="822" attr823="823" attr824="824" attr825="825" attr826="826" attr827="827" attr828="828" attr829="829" attr830="830" attr831="831" attr832="832" attr833="833" attr834="834" attr835="835" attr836="836" attr837="837" attr838="838" attr839="839" attr840="840" attr841="841" attr842="842" attr843="843" attr844="844" attr845="845" attr846="846" attr847="847" attr848="848" attr849="849" attr850="850" attr851="851" attr852="852" attr853="853" attr854="854" attr855="855" attr856="856" attr857="857" attr858="858" attr859="859" attr860="860" attr861="861" attr862="862" attr863="863" attr864="864" attr865="865" attr866="866" attr867="867" attr868="868" attr869="869" attr870="870" attr871="871" attr872="872" attr873="873" attr874="874" attr875="875" attr876="876" attr877="877" attr878="878" attr879="879" attr880="880" attr881="881" attr882="882" attr883="883" attr884="884" attr885="885" attr886="886" attr887="887" attr888="888" attr889="889" attr890="890" attr891="891" attr892="892" attr893="893" attr894="894" attr895="895" attr896="896" attr897="897" attr898="898" attr899="899" attr900="900" attr901="901" attr902="902" attr903="903" attr904="904" attr905="905" attr906="906" attr907="907" attr908="908" attr909="909" attr910="910" attr911="911" attr912="912" attr913="913" attr914="914" attr915="915" attr916="916" attr917="917" attr918="918" attr919="919" attr920="920" attr921="921" attr922="922" attr923="923" attr924="924" attr925="925" attr926="926" attr927="927" attr928="928" attr929="929" attr930="930" attr931="931" attr932="932" attr933="933" attr934="934" attr935="935" attr936="936" attr937="937" attr938="938" attr939="939" attr940="940" attr941="941" attr942="942" attr943="943" attr944="944" attr945="945" attr946="946" attr947="947" attr948="948" attr949="949" attr950="950" attr951="951" attr952="952" attr953="953" attr954="954" attr955="955" attr956="956" attr957="957" attr958="958" attr959="959" attr960="960" attr961="961" attr962="962" attr963="963" attr964="964" attr965="965" attr966="966" attr967="967" attr968="968" attr969="969" attr970="970" attr971="971" attr972="972" attr973="973" attr974="974" attr975="975" attr976="976" attr977="977" attr978="978" attr979="979" attr980="980" attr981="981" attr982="982" attr983="983" attr984="984" attr985="985" attr986="986" attr987="987" attr988="988" attr989="989" attr990="990" attr991="991" attr992="992" attr993="993" attr994="994" attr995="995" attr996="996" attr997="997" attr998="998" attr999="999" attr1000="1000" 67 | { "nice" } 68 | }.0 69 | } 70 | 71 | fn horrorshow_many_attrs() -> String { 72 | use horrorshow::html; 73 | format!( 74 | "{}", 75 | html! { 76 | h1(attr1="1", attr2="2", attr3="3", attr4="4", attr5="5", attr6="6", attr7="7", attr8="8", attr9="9", attr10="10", attr11="11", attr12="12", attr13="13", attr14="14", attr15="15", attr16="16", attr17="17", attr18="18", attr19="19", attr20="20", attr21="21", attr22="22", attr23="23", attr24="24", attr25="25", attr26="26", attr27="27", attr28="28", attr29="29", attr30="30", attr31="31", attr32="32", attr33="33", attr34="34", attr35="35", attr36="36", attr37="37", attr38="38", attr39="39", attr40="40", attr41="41", attr42="42", attr43="43", attr44="44", attr45="45", attr46="46", attr47="47", attr48="48", attr49="49", attr50="50", attr51="51", attr52="52", attr53="53", attr54="54", attr55="55", attr56="56", attr57="57", attr58="58", attr59="59", attr60="60", attr61="61", attr62="62", attr63="63", attr64="64", attr65="65", attr66="66", attr67="67", attr68="68", attr69="69", attr70="70", attr71="71", attr72="72", attr73="73", attr74="74", attr75="75", attr76="76", attr77="77", attr78="78", attr79="79", attr80="80", attr81="81", attr82="82", attr83="83", attr84="84", attr85="85", attr86="86", attr87="87", attr88="88", attr89="89", attr90="90", attr91="91", attr92="92", attr93="93", attr94="94", attr95="95", attr96="96", attr97="97", attr98="98", attr99="99", attr100="100", attr101="101", attr102="102", attr103="103", attr104="104", attr105="105", attr106="106", attr107="107", attr108="108", attr109="109", attr110="110", attr111="111", attr112="112", attr113="113", attr114="114", attr115="115", attr116="116", attr117="117", attr118="118", attr119="119", attr120="120", attr121="121", attr122="122", attr123="123", attr124="124", attr125="125", attr126="126", attr127="127", attr128="128", attr129="129", attr130="130", attr131="131", attr132="132", attr133="133", attr134="134", attr135="135", attr136="136", attr137="137", attr138="138", attr139="139", attr140="140", attr141="141", attr142="142", attr143="143", attr144="144", attr145="145", attr146="146", attr147="147", attr148="148", attr149="149", attr150="150", attr151="151", attr152="152", attr153="153", attr154="154", attr155="155", attr156="156", attr157="157", attr158="158", attr159="159", attr160="160", attr161="161", attr162="162", attr163="163", attr164="164", attr165="165", attr166="166", attr167="167", attr168="168", attr169="169", attr170="170", attr171="171", attr172="172", attr173="173", attr174="174", attr175="175", attr176="176", attr177="177", attr178="178", attr179="179", attr180="180", attr181="181", attr182="182", attr183="183", attr184="184", attr185="185", attr186="186", attr187="187", attr188="188", attr189="189", attr190="190", attr191="191", attr192="192", attr193="193", attr194="194", attr195="195", attr196="196", attr197="197", attr198="198", attr199="199", attr200="200", attr201="201", attr202="202", attr203="203", attr204="204", attr205="205", attr206="206", attr207="207", attr208="208", attr209="209", attr210="210", attr211="211", attr212="212", attr213="213", attr214="214", attr215="215", attr216="216", attr217="217", attr218="218", attr219="219", attr220="220", attr221="221", attr222="222", attr223="223", attr224="224", attr225="225", attr226="226", attr227="227", attr228="228", attr229="229", attr230="230", attr231="231", attr232="232", attr233="233", attr234="234", attr235="235", attr236="236", attr237="237", attr238="238", attr239="239", attr240="240", attr241="241", attr242="242", attr243="243", attr244="244", attr245="245", attr246="246", attr247="247", attr248="248", attr249="249", attr250="250", attr251="251", attr252="252", attr253="253", attr254="254", attr255="255", attr256="256", attr257="257", attr258="258", attr259="259", attr260="260", attr261="261", attr262="262", attr263="263", attr264="264", attr265="265", attr266="266", attr267="267", attr268="268", attr269="269", attr270="270", attr271="271", attr272="272", attr273="273", attr274="274", attr275="275", attr276="276", attr277="277", attr278="278", attr279="279", attr280="280", attr281="281", attr282="282", attr283="283", attr284="284", attr285="285", attr286="286", attr287="287", attr288="288", attr289="289", attr290="290", attr291="291", attr292="292", attr293="293", attr294="294", attr295="295", attr296="296", attr297="297", attr298="298", attr299="299", attr300="300", attr301="301", attr302="302", attr303="303", attr304="304", attr305="305", attr306="306", attr307="307", attr308="308", attr309="309", attr310="310", attr311="311", attr312="312", attr313="313", attr314="314", attr315="315", attr316="316", attr317="317", attr318="318", attr319="319", attr320="320", attr321="321", attr322="322", attr323="323", attr324="324", attr325="325", attr326="326", attr327="327", attr328="328", attr329="329", attr330="330", attr331="331", attr332="332", attr333="333", attr334="334", attr335="335", attr336="336", attr337="337", attr338="338", attr339="339", attr340="340", attr341="341", attr342="342", attr343="343", attr344="344", attr345="345", attr346="346", attr347="347", attr348="348", attr349="349", attr350="350", attr351="351", attr352="352", attr353="353", attr354="354", attr355="355", attr356="356", attr357="357", attr358="358", attr359="359", attr360="360", attr361="361", attr362="362", attr363="363", attr364="364", attr365="365", attr366="366", attr367="367", attr368="368", attr369="369", attr370="370", attr371="371", attr372="372", attr373="373", attr374="374", attr375="375", attr376="376", attr377="377", attr378="378", attr379="379", attr380="380", attr381="381", attr382="382", attr383="383", attr384="384", attr385="385", attr386="386", attr387="387", attr388="388", attr389="389", attr390="390", attr391="391", attr392="392", attr393="393", attr394="394", attr395="395", attr396="396", attr397="397", attr398="398", attr399="399", attr400="400", attr401="401", attr402="402", attr403="403", attr404="404", attr405="405", attr406="406", attr407="407", attr408="408", attr409="409", attr410="410", attr411="411", attr412="412", attr413="413", attr414="414", attr415="415", attr416="416", attr417="417", attr418="418", attr419="419", attr420="420", attr421="421", attr422="422", attr423="423", attr424="424", attr425="425", attr426="426", attr427="427", attr428="428", attr429="429", attr430="430", attr431="431", attr432="432", attr433="433", attr434="434", attr435="435", attr436="436", attr437="437", attr438="438", attr439="439", attr440="440", attr441="441", attr442="442", attr443="443", attr444="444", attr445="445", attr446="446", attr447="447", attr448="448", attr449="449", attr450="450", attr451="451", attr452="452", attr453="453", attr454="454", attr455="455", attr456="456", attr457="457", attr458="458", attr459="459", attr460="460", attr461="461", attr462="462", attr463="463", attr464="464", attr465="465", attr466="466", attr467="467", attr468="468", attr469="469", attr470="470", attr471="471", attr472="472", attr473="473", attr474="474", attr475="475", attr476="476", attr477="477", attr478="478", attr479="479", attr480="480", attr481="481", attr482="482", attr483="483", attr484="484", attr485="485", attr486="486", attr487="487", attr488="488", attr489="489", attr490="490", attr491="491", attr492="492", attr493="493", attr494="494", attr495="495", attr496="496", attr497="497", attr498="498", attr499="499", attr500="500", attr501="501", attr502="502", attr503="503", attr504="504", attr505="505", attr506="506", attr507="507", attr508="508", attr509="509", attr510="510", attr511="511", attr512="512", attr513="513", attr514="514", attr515="515", attr516="516", attr517="517", attr518="518", attr519="519", attr520="520", attr521="521", attr522="522", attr523="523", attr524="524", attr525="525", attr526="526", attr527="527", attr528="528", attr529="529", attr530="530", attr531="531", attr532="532", attr533="533", attr534="534", attr535="535", attr536="536", attr537="537", attr538="538", attr539="539", attr540="540", attr541="541", attr542="542", attr543="543", attr544="544", attr545="545", attr546="546", attr547="547", attr548="548", attr549="549", attr550="550", attr551="551", attr552="552", attr553="553", attr554="554", attr555="555", attr556="556", attr557="557", attr558="558", attr559="559", attr560="560", attr561="561", attr562="562", attr563="563", attr564="564", attr565="565", attr566="566", attr567="567", attr568="568", attr569="569", attr570="570", attr571="571", attr572="572", attr573="573", attr574="574", attr575="575", attr576="576", attr577="577", attr578="578", attr579="579", attr580="580", attr581="581", attr582="582", attr583="583", attr584="584", attr585="585", attr586="586", attr587="587", attr588="588", attr589="589", attr590="590", attr591="591", attr592="592", attr593="593", attr594="594", attr595="595", attr596="596", attr597="597", attr598="598", attr599="599", attr600="600", attr601="601", attr602="602", attr603="603", attr604="604", attr605="605", attr606="606", attr607="607", attr608="608", attr609="609", attr610="610", attr611="611", attr612="612", attr613="613", attr614="614", attr615="615", attr616="616", attr617="617", attr618="618", attr619="619", attr620="620", attr621="621", attr622="622", attr623="623", attr624="624", attr625="625", attr626="626", attr627="627", attr628="628", attr629="629", attr630="630", attr631="631", attr632="632", attr633="633", attr634="634", attr635="635", attr636="636", attr637="637", attr638="638", attr639="639", attr640="640", attr641="641", attr642="642", attr643="643", attr644="644", attr645="645", attr646="646", attr647="647", attr648="648", attr649="649", attr650="650", attr651="651", attr652="652", attr653="653", attr654="654", attr655="655", attr656="656", attr657="657", attr658="658", attr659="659", attr660="660", attr661="661", attr662="662", attr663="663", attr664="664", attr665="665", attr666="666", attr667="667", attr668="668", attr669="669", attr670="670", attr671="671", attr672="672", attr673="673", attr674="674", attr675="675", attr676="676", attr677="677", attr678="678", attr679="679", attr680="680", attr681="681", attr682="682", attr683="683", attr684="684", attr685="685", attr686="686", attr687="687", attr688="688", attr689="689", attr690="690", attr691="691", attr692="692", attr693="693", attr694="694", attr695="695", attr696="696", attr697="697", attr698="698", attr699="699", attr700="700", attr701="701", attr702="702", attr703="703", attr704="704", attr705="705", attr706="706", attr707="707", attr708="708", attr709="709", attr710="710", attr711="711", attr712="712", attr713="713", attr714="714", attr715="715", attr716="716", attr717="717", attr718="718", attr719="719", attr720="720", attr721="721", attr722="722", attr723="723", attr724="724", attr725="725", attr726="726", attr727="727", attr728="728", attr729="729", attr730="730", attr731="731", attr732="732", attr733="733", attr734="734", attr735="735", attr736="736", attr737="737", attr738="738", attr739="739", attr740="740", attr741="741", attr742="742", attr743="743", attr744="744", attr745="745", attr746="746", attr747="747", attr748="748", attr749="749", attr750="750", attr751="751", attr752="752", attr753="753", attr754="754", attr755="755", attr756="756", attr757="757", attr758="758", attr759="759", attr760="760", attr761="761", attr762="762", attr763="763", attr764="764", attr765="765", attr766="766", attr767="767", attr768="768", attr769="769", attr770="770", attr771="771", attr772="772", attr773="773", attr774="774", attr775="775", attr776="776", attr777="777", attr778="778", attr779="779", attr780="780", attr781="781", attr782="782", attr783="783", attr784="784", attr785="785", attr786="786", attr787="787", attr788="788", attr789="789", attr790="790", attr791="791", attr792="792", attr793="793", attr794="794", attr795="795", attr796="796", attr797="797", attr798="798", attr799="799", attr800="800", attr801="801", attr802="802", attr803="803", attr804="804", attr805="805", attr806="806", attr807="807", attr808="808", attr809="809", attr810="810", attr811="811", attr812="812", attr813="813", attr814="814", attr815="815", attr816="816", attr817="817", attr818="818", attr819="819", attr820="820", attr821="821", attr822="822", attr823="823", attr824="824", attr825="825", attr826="826", attr827="827", attr828="828", attr829="829", attr830="830", attr831="831", attr832="832", attr833="833", attr834="834", attr835="835", attr836="836", attr837="837", attr838="838", attr839="839", attr840="840", attr841="841", attr842="842", attr843="843", attr844="844", attr845="845", attr846="846", attr847="847", attr848="848", attr849="849", attr850="850", attr851="851", attr852="852", attr853="853", attr854="854", attr855="855", attr856="856", attr857="857", attr858="858", attr859="859", attr860="860", attr861="861", attr862="862", attr863="863", attr864="864", attr865="865", attr866="866", attr867="867", attr868="868", attr869="869", attr870="870", attr871="871", attr872="872", attr873="873", attr874="874", attr875="875", attr876="876", attr877="877", attr878="878", attr879="879", attr880="880", attr881="881", attr882="882", attr883="883", attr884="884", attr885="885", attr886="886", attr887="887", attr888="888", attr889="889", attr890="890", attr891="891", attr892="892", attr893="893", attr894="894", attr895="895", attr896="896", attr897="897", attr898="898", attr899="899", attr900="900", attr901="901", attr902="902", attr903="903", attr904="904", attr905="905", attr906="906", attr907="907", attr908="908", attr909="909", attr910="910", attr911="911", attr912="912", attr913="913", attr914="914", attr915="915", attr916="916", attr917="917", attr918="918", attr919="919", attr920="920", attr921="921", attr922="922", attr923="923", attr924="924", attr925="925", attr926="926", attr927="927", attr928="928", attr929="929", attr930="930", attr931="931", attr932="932", attr933="933", attr934="934", attr935="935", attr936="936", attr937="937", attr938="938", attr939="939", attr940="940", attr941="941", attr942="942", attr943="943", attr944="944", attr945="945", attr946="946", attr947="947", attr948="948", attr949="949", attr950="950", attr951="951", attr952="952", attr953="953", attr954="954", attr955="955", attr956="956", attr957="957", attr958="958", attr959="959", attr960="960", attr961="961", attr962="962", attr963="963", attr964="964", attr965="965", attr966="966", attr967="967", attr968="968", attr969="969", attr970="970", attr971="971", attr972="972", attr973="973", attr974="974", attr975="975", attr976="976", attr977="977", attr978="978", attr979="979", attr980="980", attr981="981", attr982="982", attr983="983", attr984="984", attr985="985", attr986="986", attr987="987", attr988="988", attr989="989", attr990="990", attr991="991", attr992="992", attr993="993", attr994="994", attr995="995", attr996="996", attr997="997", attr998="998", attr999="999", attr1000="1000") : "nice"; 77 | } 78 | ) 79 | } 80 | 81 | fn rscx_many_attrs() -> String { 82 | rscx::html! { 83 |

84 | "nice" 85 |

86 | } 87 | } 88 | 89 | fn leptos_many_attrs() -> leptos::Oco<'static, str> { 90 | leptos::ssr::render_to_string(|| { 91 | leptos::view! { 92 |

93 | "nice" 94 |

95 | } 96 | }) 97 | } 98 | 99 | fn html_node_many_attrs() -> String { 100 | html_node::html! { 101 |

102 | "nice" 103 |

104 | }.to_string() 105 | } 106 | 107 | // many dynamic (non literal) attributes 108 | 109 | fn horrorshow_many_dyn_attrs() -> String { 110 | use horrorshow::html; 111 | let value: i32 = 42; 112 | format!( 113 | "{}", 114 | html! { 115 | h1(attr1=value, attr2=value, attr3=value, attr4=value, attr5=value, attr6=value, attr7=value, attr8=value, attr9=value, attr10=value, attr11=value, attr12=value, attr13=value, attr14=value, attr15=value, attr16=value, attr17=value, attr18=value, attr19=value, attr20=value, attr21=value, attr22=value, attr23=value, attr24=value, attr25=value, attr26=value, attr27=value, attr28=value, attr29=value, attr30=value, attr31=value, attr32=value, attr33=value, attr34=value, attr35=value, attr36=value, attr37=value, attr38=value, attr39=value, attr40=value, attr41=value, attr42=value, attr43=value, attr44=value, attr45=value, attr46=value, attr47=value, attr48=value, attr49=value, attr50=value, attr51=value, attr52=value, attr53=value, attr54=value, attr55=value, attr56=value, attr57=value, attr58=value, attr59=value, attr60=value, attr61=value, attr62=value, attr63=value, attr64=value, attr65=value, attr66=value, attr67=value, attr68=value, attr69=value, attr70=value, attr71=value, attr72=value, attr73=value, attr74=value, attr75=value, attr76=value, attr77=value, attr78=value, attr79=value, attr80=value, attr81=value, attr82=value, attr83=value, attr84=value, attr85=value, attr86=value, attr87=value, attr88=value, attr89=value, attr90=value, attr91=value, attr92=value, attr93=value, attr94=value, attr95=value, attr96=value, attr97=value, attr98=value, attr99=value, attr100=value, attr101=value, attr102=value, attr103=value, attr104=value, attr105=value, attr106=value, attr107=value, attr108=value, attr109=value, attr110=value, attr111=value, attr112=value, attr113=value, attr114=value, attr115=value, attr116=value, attr117=value, attr118=value, attr119=value, attr120=value, attr121=value, attr122=value, attr123=value, attr124=value, attr125=value, attr126=value, attr127=value, attr128=value, attr129=value, attr130=value, attr131=value, attr132=value, attr133=value, attr134=value, attr135=value, attr136=value, attr137=value, attr138=value, attr139=value, attr140=value, attr141=value, attr142=value, attr143=value, attr144=value, attr145=value, attr146=value, attr147=value, attr148=value, attr149=value, attr150=value, attr151=value, attr152=value, attr153=value, attr154=value, attr155=value, attr156=value, attr157=value, attr158=value, attr159=value, attr160=value, attr161=value, attr162=value, attr163=value, attr164=value, attr165=value, attr166=value, attr167=value, attr168=value, attr169=value, attr170=value, attr171=value, attr172=value, attr173=value, attr174=value, attr175=value, attr176=value, attr177=value, attr178=value, attr179=value, attr180=value, attr181=value, attr182=value, attr183=value, attr184=value, attr185=value, attr186=value, attr187=value, attr188=value, attr189=value, attr190=value, attr191=value, attr192=value, attr193=value, attr194=value, attr195=value, attr196=value, attr197=value, attr198=value, attr199=value, attr200=value, attr201=value, attr202=value, attr203=value, attr204=value, attr205=value, attr206=value, attr207=value, attr208=value, attr209=value, attr210=value, attr211=value, attr212=value, attr213=value, attr214=value, attr215=value, attr216=value, attr217=value, attr218=value, attr219=value, attr220=value, attr221=value, attr222=value, attr223=value, attr224=value, attr225=value, attr226=value, attr227=value, attr228=value, attr229=value, attr230=value, attr231=value, attr232=value, attr233=value, attr234=value, attr235=value, attr236=value, attr237=value, attr238=value, attr239=value, attr240=value, attr241=value, attr242=value, attr243=value, attr244=value, attr245=value, attr246=value, attr247=value, attr248=value, attr249=value, attr250=value, attr251=value, attr252=value, attr253=value, attr254=value, attr255=value, attr256=value, attr257=value, attr258=value, attr259=value, attr260=value, attr261=value, attr262=value, attr263=value, attr264=value, attr265=value, attr266=value, attr267=value, attr268=value, attr269=value, attr270=value, attr271=value, attr272=value, attr273=value, attr274=value, attr275=value, attr276=value, attr277=value, attr278=value, attr279=value, attr280=value, attr281=value, attr282=value, attr283=value, attr284=value, attr285=value, attr286=value, attr287=value, attr288=value, attr289=value, attr290=value, attr291=value, attr292=value, attr293=value, attr294=value, attr295=value, attr296=value, attr297=value, attr298=value, attr299=value, attr300=value, attr301=value, attr302=value, attr303=value, attr304=value, attr305=value, attr306=value, attr307=value, attr308=value, attr309=value, attr310=value, attr311=value, attr312=value, attr313=value, attr314=value, attr315=value, attr316=value, attr317=value, attr318=value, attr319=value, attr320=value, attr321=value, attr322=value, attr323=value, attr324=value, attr325=value, attr326=value, attr327=value, attr328=value, attr329=value, attr330=value, attr331=value, attr332=value, attr333=value, attr334=value, attr335=value, attr336=value, attr337=value, attr338=value, attr339=value, attr340=value, attr341=value, attr342=value, attr343=value, attr344=value, attr345=value, attr346=value, attr347=value, attr348=value, attr349=value, attr350=value, attr351=value, attr352=value, attr353=value, attr354=value, attr355=value, attr356=value, attr357=value, attr358=value, attr359=value, attr360=value, attr361=value, attr362=value, attr363=value, attr364=value, attr365=value, attr366=value, attr367=value, attr368=value, attr369=value, attr370=value, attr371=value, attr372=value, attr373=value, attr374=value, attr375=value, attr376=value, attr377=value, attr378=value, attr379=value, attr380=value, attr381=value, attr382=value, attr383=value, attr384=value, attr385=value, attr386=value, attr387=value, attr388=value, attr389=value, attr390=value, attr391=value, attr392=value, attr393=value, attr394=value, attr395=value, attr396=value, attr397=value, attr398=value, attr399=value, attr400=value, attr401=value, attr402=value, attr403=value, attr404=value, attr405=value, attr406=value, attr407=value, attr408=value, attr409=value, attr410=value, attr411=value, attr412=value, attr413=value, attr414=value, attr415=value, attr416=value, attr417=value, attr418=value, attr419=value, attr420=value, attr421=value, attr422=value, attr423=value, attr424=value, attr425=value, attr426=value, attr427=value, attr428=value, attr429=value, attr430=value, attr431=value, attr432=value, attr433=value, attr434=value, attr435=value, attr436=value, attr437=value, attr438=value, attr439=value, attr440=value, attr441=value, attr442=value, attr443=value, attr444=value, attr445=value, attr446=value, attr447=value, attr448=value, attr449=value, attr450=value, attr451=value, attr452=value, attr453=value, attr454=value, attr455=value, attr456=value, attr457=value, attr458=value, attr459=value, attr460=value, attr461=value, attr462=value, attr463=value, attr464=value, attr465=value, attr466=value, attr467=value, attr468=value, attr469=value, attr470=value, attr471=value, attr472=value, attr473=value, attr474=value, attr475=value, attr476=value, attr477=value, attr478=value, attr479=value, attr480=value, attr481=value, attr482=value, attr483=value, attr484=value, attr485=value, attr486=value, attr487=value, attr488=value, attr489=value, attr490=value, attr491=value, attr492=value, attr493=value, attr494=value, attr495=value, attr496=value, attr497=value, attr498=value, attr499=value, attr500=value, attr501=value, attr502=value, attr503=value, attr504=value, attr505=value, attr506=value, attr507=value, attr508=value, attr509=value, attr510=value, attr511=value, attr512=value, attr513=value, attr514=value, attr515=value, attr516=value, attr517=value, attr518=value, attr519=value, attr520=value, attr521=value, attr522=value, attr523=value, attr524=value, attr525=value, attr526=value, attr527=value, attr528=value, attr529=value, attr530=value, attr531=value, attr532=value, attr533=value, attr534=value, attr535=value, attr536=value, attr537=value, attr538=value, attr539=value, attr540=value, attr541=value, attr542=value, attr543=value, attr544=value, attr545=value, attr546=value, attr547=value, attr548=value, attr549=value, attr550=value, attr551=value, attr552=value, attr553=value, attr554=value, attr555=value, attr556=value, attr557=value, attr558=value, attr559=value, attr560=value, attr561=value, attr562=value, attr563=value, attr564=value, attr565=value, attr566=value, attr567=value, attr568=value, attr569=value, attr570=value, attr571=value, attr572=value, attr573=value, attr574=value, attr575=value, attr576=value, attr577=value, attr578=value, attr579=value, attr580=value, attr581=value, attr582=value, attr583=value, attr584=value, attr585=value, attr586=value, attr587=value, attr588=value, attr589=value, attr590=value, attr591=value, attr592=value, attr593=value, attr594=value, attr595=value, attr596=value, attr597=value, attr598=value, attr599=value, attr600=value, attr601=value, attr602=value, attr603=value, attr604=value, attr605=value, attr606=value, attr607=value, attr608=value, attr609=value, attr610=value, attr611=value, attr612=value, attr613=value, attr614=value, attr615=value, attr616=value, attr617=value, attr618=value, attr619=value, attr620=value, attr621=value, attr622=value, attr623=value, attr624=value, attr625=value, attr626=value, attr627=value, attr628=value, attr629=value, attr630=value, attr631=value, attr632=value, attr633=value, attr634=value, attr635=value, attr636=value, attr637=value, attr638=value, attr639=value, attr640=value, attr641=value, attr642=value, attr643=value, attr644=value, attr645=value, attr646=value, attr647=value, attr648=value, attr649=value, attr650=value, attr651=value, attr652=value, attr653=value, attr654=value, attr655=value, attr656=value, attr657=value, attr658=value, attr659=value, attr660=value, attr661=value, attr662=value, attr663=value, attr664=value, attr665=value, attr666=value, attr667=value, attr668=value, attr669=value, attr670=value, attr671=value, attr672=value, attr673=value, attr674=value, attr675=value, attr676=value, attr677=value, attr678=value, attr679=value, attr680=value, attr681=value, attr682=value, attr683=value, attr684=value, attr685=value, attr686=value, attr687=value, attr688=value, attr689=value, attr690=value, attr691=value, attr692=value, attr693=value, attr694=value, attr695=value, attr696=value, attr697=value, attr698=value, attr699=value, attr700=value, attr701=value, attr702=value, attr703=value, attr704=value, attr705=value, attr706=value, attr707=value, attr708=value, attr709=value, attr710=value, attr711=value, attr712=value, attr713=value, attr714=value, attr715=value, attr716=value, attr717=value, attr718=value, attr719=value, attr720=value, attr721=value, attr722=value, attr723=value, attr724=value, attr725=value, attr726=value, attr727=value, attr728=value, attr729=value, attr730=value, attr731=value, attr732=value, attr733=value, attr734=value, attr735=value, attr736=value, attr737=value, attr738=value, attr739=value, attr740=value, attr741=value, attr742=value, attr743=value, attr744=value, attr745=value, attr746=value, attr747=value, attr748=value, attr749=value, attr750=value, attr751=value, attr752=value, attr753=value, attr754=value, attr755=value, attr756=value, attr757=value, attr758=value, attr759=value, attr760=value, attr761=value, attr762=value, attr763=value, attr764=value, attr765=value, attr766=value, attr767=value, attr768=value, attr769=value, attr770=value, attr771=value, attr772=value, attr773=value, attr774=value, attr775=value, attr776=value, attr777=value, attr778=value, attr779=value, attr780=value, attr781=value, attr782=value, attr783=value, attr784=value, attr785=value, attr786=value, attr787=value, attr788=value, attr789=value, attr790=value, attr791=value, attr792=value, attr793=value, attr794=value, attr795=value, attr796=value, attr797=value, attr798=value, attr799=value, attr800=value, attr801=value, attr802=value, attr803=value, attr804=value, attr805=value, attr806=value, attr807=value, attr808=value, attr809=value, attr810=value, attr811=value, attr812=value, attr813=value, attr814=value, attr815=value, attr816=value, attr817=value, attr818=value, attr819=value, attr820=value, attr821=value, attr822=value, attr823=value, attr824=value, attr825=value, attr826=value, attr827=value, attr828=value, attr829=value, attr830=value, attr831=value, attr832=value, attr833=value, attr834=value, attr835=value, attr836=value, attr837=value, attr838=value, attr839=value, attr840=value, attr841=value, attr842=value, attr843=value, attr844=value, attr845=value, attr846=value, attr847=value, attr848=value, attr849=value, attr850=value, attr851=value, attr852=value, attr853=value, attr854=value, attr855=value, attr856=value, attr857=value, attr858=value, attr859=value, attr860=value, attr861=value, attr862=value, attr863=value, attr864=value, attr865=value, attr866=value, attr867=value, attr868=value, attr869=value, attr870=value, attr871=value, attr872=value, attr873=value, attr874=value, attr875=value, attr876=value, attr877=value, attr878=value, attr879=value, attr880=value, attr881=value, attr882=value, attr883=value, attr884=value, attr885=value, attr886=value, attr887=value, attr888=value, attr889=value, attr890=value, attr891=value, attr892=value, attr893=value, attr894=value, attr895=value, attr896=value, attr897=value, attr898=value, attr899=value, attr900=value, attr901=value, attr902=value, attr903=value, attr904=value, attr905=value, attr906=value, attr907=value, attr908=value, attr909=value, attr910=value, attr911=value, attr912=value, attr913=value, attr914=value, attr915=value, attr916=value, attr917=value, attr918=value, attr919=value, attr920=value, attr921=value, attr922=value, attr923=value, attr924=value, attr925=value, attr926=value, attr927=value, attr928=value, attr929=value, attr930=value, attr931=value, attr932=value, attr933=value, attr934=value, attr935=value, attr936=value, attr937=value, attr938=value, attr939=value, attr940=value, attr941=value, attr942=value, attr943=value, attr944=value, attr945=value, attr946=value, attr947=value, attr948=value, attr949=value, attr950=value, attr951=value, attr952=value, attr953=value, attr954=value, attr955=value, attr956=value, attr957=value, attr958=value, attr959=value, attr960=value, attr961=value, attr962=value, attr963=value, attr964=value, attr965=value, attr966=value, attr967=value, attr968=value, attr969=value, attr970=value, attr971=value, attr972=value, attr973=value, attr974=value, attr975=value, attr976=value, attr977=value, attr978=value, attr979=value, attr980=value, attr981=value, attr982=value, attr983=value, attr984=value, attr985=value, attr986=value, attr987=value, attr988=value, attr989=value, attr990=value, attr991=value, attr992=value, attr993=value, attr994=value, attr995=value, attr996=value, attr997=value, attr998=value, attr999=value, attr1000=value) : "nice"; 116 | } 117 | ) 118 | } 119 | 120 | fn rscx_many_dyn_attrs() -> String { 121 | let value: i32 = 42; 122 | rscx::html! { 123 |

124 | "nice" 125 |

126 | } 127 | } 128 | 129 | fn leptos_many_dyn_attrs() -> leptos::Oco<'static, str> { 130 | use leptos::IntoAttribute; 131 | leptos::ssr::render_to_string(|| { 132 | let value: i32 = 42; 133 | leptos::view! { 134 |

135 | "nice" 136 |

137 | } 138 | }) 139 | } 140 | 141 | /// small fragment with some attributes and props 142 | 143 | fn maud_small_fragment() -> String { 144 | maud::html! { 145 | div class="some-class" customattr="42" { 146 | (maud_hero_title("great-stuff".to_string(), 10, "boh".to_string())) 147 | } 148 | } 149 | .0 150 | } 151 | 152 | fn maud_hero_title(class: String, _xxx: i32, _yyy: String) -> maud::Markup { 153 | maud::html! { 154 | h1 { 155 | "my class is " (class) 156 | } 157 | } 158 | } 159 | 160 | fn horrorshow_small_fragment() -> String { 161 | format!( 162 | "{}", 163 | horrorshow::html! { 164 | div(class="some-class", customattr=42) { 165 | |tmpl| { 166 | tmpl << horrorshow_hero_title("great-stuff".to_string(), 10, "boh".to_string()) 167 | } 168 | } 169 | } 170 | ) 171 | } 172 | 173 | fn horrorshow_hero_title( 174 | class: String, 175 | _xxx: i32, 176 | _yyy: String, 177 | ) -> Box { 178 | horrorshow::box_html! { 179 | h1 { 180 | : format!("my class is {}", class) 181 | } 182 | } 183 | } 184 | 185 | async fn rscx_small_fragment() -> String { 186 | rscx::html! { 187 |
188 | 189 | 190 |
191 | } 192 | } 193 | 194 | #[rscx::props] 195 | pub struct RscHeroTitleProps { 196 | class: String, 197 | #[allow(dead_code)] 198 | xxx: i32, 199 | #[allow(dead_code)] 200 | yyy: String, 201 | } 202 | 203 | #[rscx::component] 204 | fn RscHeroTitle(props: RscHeroTitleProps) -> String { 205 | rscx::html! { 206 |

my class is {props.class}

207 | } 208 | } 209 | 210 | fn leptos_small_fragment() -> leptos::Oco<'static, str> { 211 | leptos::ssr::render_to_string(|| { 212 | leptos::view! { 213 |
214 | 215 | 216 |
217 | } 218 | }) 219 | } 220 | 221 | #[leptos::component] 222 | #[allow(unused_variables)] 223 | pub fn LeptosHeroTitle(class: String, xxx: i32, yyy: String) -> impl IntoView { 224 | leptos::view! { 225 |

my class is {class}

226 | } 227 | } 228 | 229 | // async data 230 | async fn rscx_async_list_app() -> String { 231 | rscx::html! { 232 |
233 | 234 |
235 | } 236 | } 237 | 238 | #[rscx::component] 239 | async fn RscAsyncList() -> String { 240 | let data = load_data_async().await; 241 | use rscx::CollectFragment; 242 | rscx::html! { 243 |
    244 | { 245 | data 246 | .into_iter() 247 | .map(|item| rscx::html! {
  • { item }
  • }) 248 | .collect_fragment() 249 | } 250 |
251 | } 252 | } 253 | 254 | async fn leptos_async_list_app() -> String { 255 | let local = tokio::task::LocalSet::new(); 256 | local 257 | .run_until(async move { 258 | let (stream, runtime) = 259 | leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context( 260 | || { 261 | leptos::view! { 262 |
263 | 264 |
265 | } 266 | .into_view() 267 | }, 268 | || "".into(), 269 | || {}, 270 | ); 271 | let mut stream = Box::pin(stream); 272 | let mut buf = String::new(); 273 | while let Some(chunk) = stream.next().await { 274 | buf.push_str(&chunk); 275 | } 276 | runtime.dispose(); 277 | buf 278 | }) 279 | .await 280 | } 281 | 282 | #[leptos::component] 283 | fn LeptosAsyncList() -> impl IntoView { 284 | let once = leptos::create_resource(|| (), move |()| load_data_async()); 285 | 286 | use leptos::{CollectView, SignalGet, Suspense}; 287 | leptos::view! { 288 |
    289 | 290 | { move || once.get() 291 | .map(|items| 292 | items 293 | .into_iter() 294 | .map(|item| leptos::view! {
  • { item }
  • }) 295 | .collect_view() 296 | ) } 297 |
    298 |
299 | } 300 | } 301 | 302 | async fn maud_async_list_app() -> String { 303 | maud::html! { 304 | div { 305 | (maud_async_list().await) 306 | } 307 | } 308 | .0 309 | } 310 | 311 | async fn maud_async_list() -> maud::Markup { 312 | let data = load_data_async().await; 313 | maud::html! { 314 | ul { 315 | @for item in data { 316 | li { (item) } 317 | } 318 | } 319 | } 320 | } 321 | 322 | async fn load_data_async() -> Vec { 323 | vec![ 324 | "1".to_string(), 325 | "2".to_string(), 326 | "3".to_string(), 327 | "4".to_string(), 328 | "5".to_string(), 329 | "6".to_string(), 330 | "7".to_string(), 331 | "8".to_string(), 332 | "9".to_string(), 333 | "10".to_string(), 334 | "11".to_string(), 335 | "12".to_string(), 336 | "13".to_string(), 337 | "14".to_string(), 338 | "15".to_string(), 339 | "16".to_string(), 340 | "17".to_string(), 341 | "18".to_string(), 342 | "19".to_string(), 343 | "20".to_string(), 344 | "21".to_string(), 345 | "22".to_string(), 346 | "23".to_string(), 347 | "24".to_string(), 348 | "25".to_string(), 349 | "26".to_string(), 350 | "27".to_string(), 351 | "28".to_string(), 352 | "29".to_string(), 353 | "30".to_string(), 354 | "31".to_string(), 355 | "32".to_string(), 356 | "33".to_string(), 357 | "34".to_string(), 358 | "35".to_string(), 359 | "36".to_string(), 360 | "37".to_string(), 361 | "38".to_string(), 362 | "39".to_string(), 363 | "40".to_string(), 364 | "41".to_string(), 365 | "42".to_string(), 366 | "43".to_string(), 367 | "44".to_string(), 368 | "45".to_string(), 369 | "46".to_string(), 370 | "47".to_string(), 371 | "48".to_string(), 372 | "49".to_string(), 373 | "50".to_string(), 374 | "51".to_string(), 375 | "52".to_string(), 376 | "53".to_string(), 377 | "54".to_string(), 378 | "55".to_string(), 379 | "56".to_string(), 380 | "57".to_string(), 381 | "58".to_string(), 382 | "59".to_string(), 383 | "60".to_string(), 384 | "61".to_string(), 385 | "62".to_string(), 386 | "63".to_string(), 387 | "64".to_string(), 388 | "65".to_string(), 389 | "66".to_string(), 390 | "67".to_string(), 391 | "68".to_string(), 392 | "69".to_string(), 393 | "70".to_string(), 394 | "71".to_string(), 395 | "72".to_string(), 396 | "73".to_string(), 397 | "74".to_string(), 398 | "75".to_string(), 399 | "76".to_string(), 400 | "77".to_string(), 401 | "78".to_string(), 402 | "79".to_string(), 403 | "80".to_string(), 404 | "81".to_string(), 405 | "82".to_string(), 406 | "83".to_string(), 407 | "84".to_string(), 408 | "85".to_string(), 409 | "86".to_string(), 410 | "87".to_string(), 411 | "88".to_string(), 412 | "89".to_string(), 413 | "90".to_string(), 414 | "91".to_string(), 415 | "92".to_string(), 416 | "93".to_string(), 417 | "94".to_string(), 418 | "95".to_string(), 419 | "96".to_string(), 420 | "97".to_string(), 421 | "98".to_string(), 422 | "99".to_string(), 423 | "100".to_string(), 424 | ] 425 | } 426 | -------------------------------------------------------------------------------- /rscx-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rscx-macros" 3 | version = { workspace = true } 4 | edition = "2021" 5 | authors = ["Antonio Pitasi"] 6 | license = "MIT" 7 | repository = "https://github.com/pitasi/rscx" 8 | description = "proc macros for the rscx crate" 9 | readme = "../README.md" 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | convert_case = "0.6.0" 18 | html-escape = "0.2.13" 19 | proc-macro2 = "1.0.66" 20 | proc-macro2-diagnostics = "0.10.1" 21 | quote = "1.0.33" 22 | rstml = "0.11.2" 23 | syn = { version = "2.0.29", features = ["full"] } 24 | -------------------------------------------------------------------------------- /rscx-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_macro::TokenStream; 4 | use proc_macro2_diagnostics::Diagnostic; 5 | use quote::{quote, quote_spanned, ToTokens}; 6 | use rstml::{ 7 | node::{KeyedAttribute, Node, NodeAttribute, NodeElement, NodeName}, 8 | Parser, ParserConfig, 9 | }; 10 | use syn::punctuated::Punctuated; 11 | use syn::{parse::Parse, parse_quote, spanned::Spanned, Expr, ExprLit, FnArg, ItemStruct, Token}; 12 | 13 | #[proc_macro] 14 | pub fn html(tokens: TokenStream) -> TokenStream { 15 | html_inner(tokens, false) 16 | } 17 | 18 | #[proc_macro] 19 | pub fn html_ide(tokens: TokenStream) -> TokenStream { 20 | html_inner(tokens, true) 21 | } 22 | 23 | fn is_empty_element(name: &str) -> bool { 24 | // https://developer.mozilla.org/en-US/docs/Glossary/Empty_element 25 | match name { 26 | "img" | "input" | "meta" | "link" | "hr" | "br" | "source" | "track" | "wbr" | "area" 27 | | "base" | "col" | "embed" | "param" => true, 28 | _ => false, 29 | } 30 | } 31 | 32 | fn empty_elements_set() -> HashSet<&'static str> { 33 | [ 34 | "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", 35 | "source", "track", "wbr", 36 | ] 37 | .into_iter() 38 | .collect() 39 | } 40 | 41 | fn html_inner(tokens: TokenStream, ide_helper: bool) -> TokenStream { 42 | let config = ParserConfig::new() 43 | .recover_block(true) 44 | .element_close_use_default_wildcard_ident(true) 45 | .always_self_closed_elements(empty_elements_set()); 46 | 47 | let parser = Parser::new(config); 48 | let (nodes, errors) = parser.parse_recoverable(tokens).split_vec(); 49 | process_nodes(ide_helper, &nodes, errors).into() 50 | } 51 | 52 | fn process_nodes<'n>( 53 | ide_helper: bool, 54 | nodes: &'n Vec, 55 | errors: Vec, 56 | ) -> proc_macro2::TokenStream { 57 | let WalkNodesOutput { 58 | static_format: html_string, 59 | values, 60 | collected_elements: elements, 61 | diagnostics, 62 | } = walk_nodes(&nodes); 63 | let docs = if ide_helper { 64 | generate_tags_docs(elements) 65 | } else { 66 | vec![] 67 | }; 68 | let errors = errors 69 | .into_iter() 70 | .map(|e| e.emit_as_expr_tokens()) 71 | .chain(diagnostics); 72 | quote! { 73 | { 74 | // Make sure that "compile_error!(..);" can be used in this context. 75 | #(#errors;)* 76 | // Make sure that "enum x{};" and "let _x = crate::element;" can be used in this context 77 | #(#docs;)* 78 | format!(#html_string, #(rscx::FormatWrapper::new(#values)),*) 79 | } 80 | } 81 | } 82 | 83 | fn generate_tags_docs(elements: Vec<&NodeName>) -> Vec { 84 | // Mark some of elements as type, 85 | // and other as elements as fn in crate::docs, 86 | // to give an example how to link tag with docs. 87 | let elements_as_type: HashSet<&'static str> = 88 | vec!["html", "head", "meta", "link", "body", "div"] 89 | .into_iter() 90 | .collect(); 91 | 92 | elements 93 | .into_iter() 94 | .map(|e| { 95 | if elements_as_type.contains(&*e.to_string()) { 96 | let element = quote_spanned!(e.span() => enum); 97 | quote!({#element X{}}) 98 | } else { 99 | // let _ = crate::docs::element; 100 | let element = quote_spanned!(e.span() => element); 101 | quote!(let _ = crate::docs::#element) 102 | } 103 | }) 104 | .collect() 105 | } 106 | 107 | #[derive(Default)] 108 | struct WalkNodesOutput<'a> { 109 | static_format: String, 110 | // Use proc_macro2::TokenStream instead of syn::Expr 111 | // to provide more errors to the end user. 112 | values: Vec, 113 | // Additional diagnostic messages. 114 | diagnostics: Vec, 115 | // Collect elements to provide semantic highlight based on element tag. 116 | // No differences between open tag and closed tag. 117 | // Also multiple tags with same name can be present, 118 | // because we need to mark each of them. 119 | collected_elements: Vec<&'a NodeName>, 120 | } 121 | impl<'a> WalkNodesOutput<'a> { 122 | fn extend(&mut self, other: WalkNodesOutput<'a>) { 123 | self.static_format.push_str(&other.static_format); 124 | self.values.extend(other.values); 125 | self.diagnostics.extend(other.diagnostics); 126 | self.collected_elements.extend(other.collected_elements); 127 | } 128 | } 129 | 130 | fn walk_nodes<'a>(nodes: &'a Vec) -> WalkNodesOutput<'a> { 131 | let mut out = WalkNodesOutput::default(); 132 | 133 | for node in nodes { 134 | match node { 135 | Node::Doctype(doctype) => { 136 | let value = &doctype.value.to_token_stream_string(); 137 | out.static_format.push_str(&format!("", value)); 138 | } 139 | Node::Element(element) => { 140 | let name = element.name().to_string(); 141 | 142 | if !is_component_tag_name(&name) { 143 | match element.name() { 144 | NodeName::Block(block) => { 145 | out.static_format.push_str("<{}"); 146 | out.values.push(block.to_token_stream()); 147 | } 148 | _ => { 149 | out.static_format.push_str(&format!("<{}", name)); 150 | out.collected_elements.push(&element.open_tag.name); 151 | if let Some(e) = &element.close_tag { 152 | out.collected_elements.push(&e.name) 153 | } 154 | } 155 | } 156 | 157 | // attributes 158 | for attribute in element.attributes() { 159 | match attribute { 160 | NodeAttribute::Block(block) => { 161 | // If the nodes parent is an attribute we prefix with whitespace 162 | out.static_format.push(' '); 163 | out.static_format.push_str("{}"); 164 | out.values.push(block.to_token_stream()); 165 | } 166 | NodeAttribute::Attribute(attribute) => { 167 | let (static_format, value) = walk_attribute(attribute); 168 | out.static_format.push_str(&static_format); 169 | if let Some(value) = value { 170 | out.values.push(value); 171 | } 172 | } 173 | } 174 | } 175 | // Ignore childs of special Empty elements 176 | if is_empty_element(element.open_tag.name.to_string().as_str()) { 177 | out.static_format.push_str(" />"); 178 | if !element.children.is_empty() { 179 | let warning = proc_macro2_diagnostics::Diagnostic::spanned( 180 | element.open_tag.name.span(), 181 | proc_macro2_diagnostics::Level::Warning, 182 | "Element is processed as empty, and cannot have any child", 183 | ); 184 | out.diagnostics.push(warning.emit_as_expr_tokens()) 185 | } 186 | 187 | continue; 188 | } 189 | out.static_format.push('>'); 190 | 191 | // children 192 | let other_output = walk_nodes(&element.children); 193 | out.extend(other_output); 194 | 195 | match element.name() { 196 | NodeName::Block(block) => { 197 | out.static_format.push_str(""); 198 | out.values.push(block.to_token_stream()); 199 | } 200 | _ => { 201 | out.static_format.push_str(&format!("", name)); 202 | } 203 | } 204 | } else { 205 | // custom elements 206 | out.static_format.push_str("{}"); 207 | out.values 208 | .push(CustomElement::new(element).to_token_stream()); 209 | } 210 | } 211 | Node::Text(text) => { 212 | out.static_format.push_str(&text.value_string()); 213 | } 214 | Node::RawText(text) => { 215 | out.static_format.push_str(&text.to_string_best()); 216 | } 217 | Node::Fragment(fragment) => { 218 | let other_output = walk_nodes(&fragment.children); 219 | out.extend(other_output) 220 | } 221 | Node::Comment(comment) => { 222 | out.static_format.push_str(""); 223 | out.values.push(comment.value.to_token_stream()); 224 | } 225 | Node::Block(block) => { 226 | let block = block.try_block().unwrap(); 227 | let stmts = &block.stmts; 228 | out.static_format.push_str("{}"); 229 | out.values.push(quote!(#(#stmts)*)); 230 | } 231 | } 232 | } 233 | 234 | out 235 | } 236 | 237 | fn walk_attribute(attribute: &KeyedAttribute) -> (String, Option) { 238 | let mut static_format = String::new(); 239 | let mut format_value = None; 240 | let key = match attribute.key.to_string().as_str() { 241 | "as_" => "as".to_string(), 242 | _ => attribute.key.to_string(), 243 | }; 244 | static_format.push_str(&format!(" {}", key)); 245 | 246 | match attribute.value() { 247 | Some(Expr::Lit(ExprLit { 248 | lit: syn::Lit::Str(value), 249 | .. 250 | })) => { 251 | static_format.push_str(&format!( 252 | r#"="{}""#, 253 | html_escape::encode_unquoted_attribute(&value.value()) 254 | )); 255 | } 256 | Some(Expr::Lit(ExprLit { 257 | lit: syn::Lit::Bool(value), 258 | .. 259 | })) => { 260 | static_format.push_str(&format!(r#"="{}""#, value.value())); 261 | } 262 | Some(Expr::Lit(ExprLit { 263 | lit: syn::Lit::Int(value), 264 | .. 265 | })) => { 266 | static_format.push_str(&format!(r#"="{}""#, value.token())); 267 | } 268 | Some(Expr::Lit(ExprLit { 269 | lit: syn::Lit::Float(value), 270 | .. 271 | })) => { 272 | static_format.push_str(&format!(r#"="{}""#, value.token())); 273 | } 274 | Some(value) => { 275 | static_format.push_str(r#"="{}""#); 276 | format_value = Some( 277 | quote! {{ 278 | // (#value).escape_attribute() 279 | ::rscx::EscapeAttribute::escape_attribute(&#value) 280 | }} 281 | .into_token_stream(), 282 | ); 283 | } 284 | None => {} 285 | } 286 | 287 | (static_format, format_value) 288 | } 289 | 290 | fn is_component_tag_name(name: &str) -> bool { 291 | name.starts_with(|c: char| c.is_ascii_uppercase()) 292 | } 293 | 294 | struct CustomElement<'e> { 295 | e: &'e NodeElement, 296 | } 297 | 298 | impl<'e> CustomElement<'e> { 299 | fn new(e: &'e NodeElement) -> Self { 300 | CustomElement { e } 301 | } 302 | } 303 | 304 | impl<'e> ToTokens for CustomElement<'_> { 305 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 306 | let name = self.e.name(); 307 | 308 | let mut chain = vec![quote! { 309 | ::rscx::props::props_builder(&#name) 310 | }]; 311 | 312 | let children = &self.e.children; 313 | if !children.is_empty() { 314 | let c = process_nodes(false, children, vec![]); 315 | chain.push(quote! { .children(#c) }); 316 | } 317 | 318 | chain.push({ 319 | self.e 320 | .attributes() 321 | .iter() 322 | .map(|a| match a { 323 | NodeAttribute::Block(block) => { 324 | quote! { 325 | .push_attr( 326 | #[allow(unused_braces)] 327 | #block 328 | ) 329 | } 330 | } 331 | NodeAttribute::Attribute(attribute) => { 332 | let key = &attribute.key; 333 | let value = attribute.value().unwrap(); 334 | quote! { .#key(#value) } 335 | } 336 | }) 337 | .collect::() 338 | }); 339 | 340 | chain.push(quote! { .build() }); 341 | 342 | tokens.extend(quote! { 343 | #name(#(#chain)*).await 344 | }); 345 | } 346 | } 347 | 348 | #[proc_macro_attribute] 349 | pub fn props(_attr: TokenStream, input: TokenStream) -> TokenStream { 350 | let props = syn::parse_macro_input!(input as PropsStruct); 351 | quote! { #props }.to_token_stream().into() 352 | } 353 | 354 | struct PropsStruct { 355 | name: syn::Ident, 356 | item: ItemStruct, 357 | } 358 | 359 | impl Parse for PropsStruct { 360 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 361 | let item = input.parse::()?; 362 | let name = item.ident.clone(); 363 | 364 | Ok(PropsStruct { name, item }) 365 | } 366 | } 367 | 368 | impl ToTokens for PropsStruct { 369 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 370 | let name = &self.name; 371 | let item = &self.item; 372 | 373 | let builder_name = 374 | syn::Ident::new(&format!("{}Builder", name), proc_macro2::Span::call_site()); 375 | 376 | tokens.extend(quote! { 377 | #[derive(::rscx::typed_builder::TypedBuilder)] 378 | #[builder(doc, crate_module_path=::rscx::typed_builder)] 379 | #item 380 | 381 | impl ::rscx::props::Props for #name { 382 | type Builder = #builder_name; 383 | fn builder() -> Self::Builder { 384 | #name::builder() 385 | } 386 | } 387 | }); 388 | 389 | let has_attributes = item 390 | .fields 391 | .iter() 392 | .any(|field| field.ident.as_ref().unwrap().to_string() == "attributes"); 393 | 394 | if has_attributes { 395 | tokens.extend(quote! { 396 | impl #builder_name { 397 | pub fn push_attr(mut self, attr: A) -> Self { 398 | self.props.attributes.push_str(&format!("{} ", attr)); 399 | self 400 | } 401 | } 402 | }); 403 | } 404 | } 405 | } 406 | 407 | #[proc_macro_attribute] 408 | pub fn component(_attr: TokenStream, input: TokenStream) -> TokenStream { 409 | let comp = syn::parse_macro_input!(input as ComponentFn); 410 | quote! { #comp }.to_token_stream().into() 411 | } 412 | 413 | struct ComponentFn { 414 | item: syn::ItemFn, 415 | } 416 | 417 | impl Parse for ComponentFn { 418 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 419 | let item = input.parse::()?; 420 | Ok(ComponentFn { item }) 421 | } 422 | } 423 | 424 | impl ToTokens for ComponentFn { 425 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 426 | let item = &self.item; 427 | let name = &item.sig.ident; 428 | 429 | let (defs, args) = match item.sig.inputs.len() { 430 | 0 => { 431 | // generate empty props 432 | let props_name = 433 | syn::Ident::new(&format!("{}Props", name), proc_macro2::Span::call_site()); 434 | ( 435 | quote! { 436 | #[props] 437 | pub struct #props_name{} 438 | }, 439 | quote! { _props: #props_name }, 440 | ) 441 | } 442 | // match if there is a single arg of type #nameProps 443 | 1 if matches!(item.sig.inputs.first().unwrap(), syn::FnArg::Typed(arg) if matches!(arg.ty.as_ref(), syn::Type::Path(p) if p.path.segments.last().unwrap().ident.to_string() == format!("{}Props", name))) => 444 | { 445 | let props = item.sig.inputs.first().unwrap(); 446 | (quote! {}, props.to_token_stream()) 447 | } 448 | _ => { 449 | let field_defs = &item 450 | .sig 451 | .inputs 452 | .clone() 453 | .into_iter() 454 | .map(|i| match i { 455 | FnArg::Receiver(_) => { 456 | panic!("receiver arguments unsupported"); 457 | } 458 | FnArg::Typed(mut t) => { 459 | if t.attrs.is_empty() { 460 | t.attrs.push(parse_quote! { #[builder(setter(into))] }); 461 | } 462 | 463 | t 464 | } 465 | }) 466 | .collect::>(); 467 | let field_names = item 468 | .sig 469 | .inputs 470 | .iter() 471 | .map(|i| match i { 472 | FnArg::Receiver(_) => { 473 | panic!("receiver arguments unsupported"); 474 | } 475 | FnArg::Typed(t) => &t.pat, 476 | }) 477 | .collect::>(); 478 | let props_name = 479 | syn::Ident::new(&format!("{}Props", name), proc_macro2::Span::call_site()); 480 | 481 | ( 482 | quote! { 483 | #[rscx::props] 484 | pub struct #props_name { 485 | #field_defs 486 | } 487 | }, 488 | quote! { #props_name { #field_names }: #props_name }, 489 | ) 490 | } 491 | }; 492 | 493 | let body = &item.block; 494 | let output = &item.sig.output; 495 | let vis = &item.vis; 496 | 497 | tokens.extend(quote! { 498 | #defs 499 | #[allow(non_snake_case)] 500 | #vis async fn #name(#args) #output { 501 | #body 502 | } 503 | }); 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /rscx/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rscx" 3 | version = { workspace = true } 4 | edition = "2021" 5 | authors = ["Antonio Pitasi"] 6 | license = "MIT" 7 | repository = "https://github.com/pitasi/rscx" 8 | description = "rscx is a HTML templating library for Rust with a JSX-like syntax." 9 | readme = "../README.md" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | async-trait = "0.1.73" 15 | axum = { version = "0.6.20", features = ["macros"], optional = true } 16 | futures = "0.3.28" 17 | html-escape = "0.2.13" 18 | rscx-macros = { workspace = true } 19 | tokio = { version = "1.32.0", features = ["full"] } 20 | tokio-util = { version = "0.7.8", features = ["rt"], optional = true } 21 | typed-builder = "0.16.0" 22 | 23 | [features] 24 | default = [] 25 | axum = ["dep:axum", "dep:tokio-util"] 26 | -------------------------------------------------------------------------------- /rscx/examples/autogenerated_props.rs: -------------------------------------------------------------------------------- 1 | use rscx::{component, html}; 2 | 3 | #[tokio::main] 4 | async fn main() -> Result<(), Box> { 5 | let app = app().await; 6 | println!("{}", app); 7 | Ok(()) 8 | } 9 | 10 | // simple function returning a String 11 | async fn app() -> String { 12 | let s = "ul { color: red; }"; 13 | html! { 14 | 15 | 16 | 17 | 18 | 19 | 20 | // call a component with props and children 21 |
22 |

"I am a paragraph"

23 |
24 | 25 | 26 | } 27 | } 28 | 29 | #[component] 30 | /// mark functions with #[component] to use them as components inside html! macro 31 | fn Section(title: String, children: String) -> String { 32 | html! { 33 |
34 |

{ title }

35 | { children } 36 |
37 | } 38 | } 39 | -------------------------------------------------------------------------------- /rscx/examples/context.rs: -------------------------------------------------------------------------------- 1 | use rscx::{ 2 | component, 3 | context::{provide_context, spawn_local, use_context}, 4 | html, props, 5 | }; 6 | use tokio::task::LocalSet; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<(), Box> { 10 | // Contexts are local to the tokio task that will render the component. 11 | // We need to spawn a local task in LocalSet, such a task will be able to 12 | // use `provide_context` and the children components will be able to use 13 | // `use_context`. 14 | let local = LocalSet::new(); 15 | let res = local 16 | .run_until(async move { 17 | spawn_local(async { 18 | // provide a context of type String 19 | provide_context("John".to_string()); 20 | app().await 21 | }) 22 | .await 23 | }) 24 | .await 25 | .unwrap(); 26 | 27 | println!("{}", res); 28 | Ok(()) 29 | } 30 | 31 | async fn app() -> String { 32 | html! { 33 | 34 |
35 | 36 |
37 | } 38 | } 39 | 40 | #[component] 41 | async fn Greetings() -> String { 42 | // use the context of type String 43 | let current_user = use_context::(); 44 | html! { 45 |

46 | { 47 | if let Some(user) = current_user { 48 | format!("Hello, {}!", user) 49 | } else { 50 | "Log in first!".to_string() 51 | } 52 | } 53 |

54 | } 55 | } 56 | -------------------------------------------------------------------------------- /rscx/examples/simple.rs: -------------------------------------------------------------------------------- 1 | use rscx::{component, html, props, MapFragmentExt}; 2 | 3 | #[tokio::main] 4 | async fn main() -> Result<(), Box> { 5 | let app = app().await; 6 | println!("{}", app); 7 | Ok(()) 8 | } 9 | 10 | // simple function returning a String 11 | // it will call the Items() function 12 | async fn app() -> String { 13 | let s = "ul { color: red; }"; 14 | html! { 15 | 16 | 17 | 18 | 19 | 20 | 21 | // call a component with no props 22 |
23 | 24 | // call a component with props and children 25 |
26 | 27 |
28 | 29 | 30 | } 31 | } 32 | 33 | #[props] 34 | /// mark a struct with #[props] to use it as props in a component. 35 | /// #[builder] can customize single props, marking them as option or setting a default value. 36 | struct SectionProps { 37 | #[builder(setter(into), default = "Default Title".to_string())] 38 | title: String, 39 | #[builder(default)] 40 | children: String, 41 | } 42 | 43 | #[component] 44 | /// mark functions with #[component] to use them as components inside html! macro 45 | fn Section(props: SectionProps) -> String { 46 | html! { 47 |
48 |

{ props.title }

49 | { props.children } 50 |
51 | } 52 | } 53 | 54 | #[component] 55 | async fn Items() -> String { 56 | let data = load_data_async().await; 57 | html! { 58 |
    59 | { 60 | data 61 | .into_iter() 62 | .map_fragment(|item| html! {
  • { item }
  • }) 63 | } 64 |
65 | } 66 | } 67 | 68 | /// async functions can be easily used in the body of a component, as every component is an async 69 | /// function 70 | async fn load_data_async() -> Vec { 71 | vec!["a".to_string(), "b".to_string(), "c".to_string()] 72 | } 73 | -------------------------------------------------------------------------------- /rscx/examples/with_option.rs: -------------------------------------------------------------------------------- 1 | use rscx::html; 2 | 3 | #[tokio::main] 4 | async fn main() -> Result<(), Box> { 5 | let app = app().await; 6 | println!("{}", app); 7 | Ok(()) 8 | } 9 | 10 | // simple function returning a String 11 | // it will call the Items() function 12 | async fn app() -> String { 13 | let s = Some("ul { color: red; }"); 14 | let none: Option<&str> = None; 15 | html! { 16 | 17 | 18 | 19 | 20 | 21 | 22 | { none } 23 | // call a component with no props 24 | 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /rscx/src/attributes.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use html_escape::encode_unquoted_attribute; 4 | 5 | pub trait EscapeAttribute { 6 | fn escape_attribute(&self) -> Cow; 7 | } 8 | 9 | impl EscapeAttribute for &str { 10 | fn escape_attribute(&self) -> Cow { 11 | encode_unquoted_attribute(self) 12 | } 13 | } 14 | 15 | impl EscapeAttribute for str { 16 | fn escape_attribute(&self) -> Cow { 17 | encode_unquoted_attribute(self) 18 | } 19 | } 20 | 21 | impl EscapeAttribute for String { 22 | fn escape_attribute(&self) -> Cow { 23 | encode_unquoted_attribute(self) 24 | } 25 | } 26 | 27 | impl EscapeAttribute for &String { 28 | fn escape_attribute(&self) -> Cow { 29 | encode_unquoted_attribute(self) 30 | } 31 | } 32 | 33 | macro_rules! impl_escape_attribute_literal { 34 | ($($t:ty),*) => { 35 | $( 36 | impl EscapeAttribute for $t { 37 | fn escape_attribute(&self) -> Cow { 38 | Cow::Owned(self.to_string()) 39 | } 40 | } 41 | )* 42 | }; 43 | } 44 | 45 | impl_escape_attribute_literal!( 46 | u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, bool 47 | ); 48 | -------------------------------------------------------------------------------- /rscx/src/axum.rs: -------------------------------------------------------------------------------- 1 | use axum::response::Html; 2 | use std::{future::Future, sync::OnceLock, thread::available_parallelism}; 3 | use tokio_util::task::LocalPoolHandle; 4 | 5 | use crate::context; 6 | 7 | fn get_rendering_pool() -> LocalPoolHandle { 8 | static LOCAL_POOL: OnceLock = OnceLock::new(); 9 | LOCAL_POOL 10 | .get_or_init(|| LocalPoolHandle::new(available_parallelism().map(Into::into).unwrap_or(1))) 11 | .clone() 12 | } 13 | 14 | pub async fn render(f: F) -> Html 15 | where 16 | F: Future + Send + 'static, 17 | O: Send + 'static, 18 | { 19 | get_rendering_pool() 20 | .spawn_pinned(move || async { 21 | let h = context::spawn_local(f).await.unwrap(); 22 | Html(h) 23 | }) 24 | .await 25 | .unwrap() 26 | } 27 | -------------------------------------------------------------------------------- /rscx/src/context.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{Any, TypeId}, 3 | cell::RefCell, 4 | collections::HashMap, 5 | future::Future, 6 | }; 7 | 8 | tokio::task_local! { 9 | pub(crate) static CONTEXT: RefCell>>; 10 | } 11 | 12 | pub fn spawn_local + 'static, O: 'static>( 13 | fut: F, 14 | ) -> tokio::task::JoinHandle { 15 | tokio::task::spawn_local(async move { CONTEXT.scope(RefCell::new(HashMap::new()), fut).await }) 16 | } 17 | 18 | pub fn provide_context(value: T) { 19 | let _ = CONTEXT.with(|context| { 20 | context 21 | .borrow_mut() 22 | .insert(TypeId::of::(), Box::new(value)); 23 | }); 24 | } 25 | 26 | pub fn use_context() -> Option 27 | where 28 | T: Clone + 'static, 29 | { 30 | CONTEXT.with(|context| { 31 | context 32 | .borrow() 33 | .get(&TypeId::of::()) 34 | .map(|any| { 35 | any.downcast_ref::().unwrap_or_else(|| { 36 | panic!( 37 | "Context type mismatch for type: {}", 38 | std::any::type_name::() 39 | ) 40 | }) 41 | }) 42 | .cloned() 43 | }) 44 | } 45 | 46 | pub fn expect_context() -> T 47 | where 48 | T: Clone + 'static, 49 | { 50 | use_context::() 51 | .unwrap_or_else(|| panic!("Context not found for type: {}", std::any::type_name::())) 52 | } 53 | -------------------------------------------------------------------------------- /rscx/src/format_wrapper.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display}; 2 | 3 | use crate::render::Render; 4 | 5 | pub struct FormatWrapper { 6 | inner: T, 7 | } 8 | 9 | impl FormatWrapper { 10 | pub fn new(inner: T) -> Self { 11 | Self { inner } 12 | } 13 | } 14 | 15 | impl Display for FormatWrapper { 16 | #[inline(always)] 17 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 18 | self.inner.render(f) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /rscx/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod attributes; 2 | pub mod props; 3 | pub use attributes::*; 4 | 5 | #[cfg(feature = "axum")] 6 | pub mod axum; 7 | pub mod context; 8 | pub mod format_wrapper; 9 | pub mod render; 10 | 11 | pub extern crate rscx_macros; 12 | use std::future::Future; 13 | 14 | pub use rscx_macros::*; 15 | 16 | pub extern crate typed_builder; 17 | 18 | pub extern crate html_escape; 19 | pub use format_wrapper::FormatWrapper; 20 | 21 | use async_trait::async_trait; 22 | use futures::future::join_all; 23 | 24 | pub trait CollectFragment 25 | where 26 | I: Iterator, 27 | Vec: FromIterator<::Item>, 28 | { 29 | fn collect_fragment(self) -> String; 30 | } 31 | 32 | impl CollectFragment for I 33 | where 34 | I: Iterator, 35 | Vec: FromIterator<::Item>, 36 | { 37 | fn collect_fragment(self) -> String { 38 | self.collect::>().join("") 39 | } 40 | } 41 | 42 | #[async_trait] 43 | pub trait CollectFragmentAsync 44 | where 45 | I: Iterator, 46 | Vec: FromIterator<::Item>, 47 | Fut: Future, 48 | { 49 | async fn collect_fragment_async(self) -> String; 50 | } 51 | 52 | #[async_trait] 53 | impl CollectFragmentAsync for I 54 | where 55 | I: Iterator + Send, 56 | Vec: FromIterator<::Item>, 57 | Fut: Future + Send, 58 | { 59 | async fn collect_fragment_async(self) -> String { 60 | join_all(self.collect::>()).await.join("") 61 | } 62 | } 63 | 64 | pub trait MapFragmentExt: Iterator { 65 | fn map_fragment(self, f: F) -> String 66 | where 67 | Self: Sized, 68 | F: FnMut(Self::Item) -> B, 69 | B: ToString; 70 | } 71 | 72 | impl MapFragmentExt for T 73 | where 74 | T: Iterator, 75 | { 76 | fn map_fragment(self, f: F) -> String 77 | where 78 | Self: Sized, 79 | F: FnMut(Self::Item) -> B, 80 | B: ToString, 81 | { 82 | self.map(f).map(|b| b.to_string()).collect::>().join("") 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /rscx/src/props.rs: -------------------------------------------------------------------------------- 1 | pub trait Component

{} 2 | 3 | impl Component

for F 4 | where 5 | F: FnOnce(P) -> R, 6 | P: Props, 7 | { 8 | } 9 | 10 | pub fn props_builder(_: C) -> P::Builder 11 | where 12 | C: Component

, 13 | P: Props, 14 | { 15 | P::builder() 16 | } 17 | 18 | pub trait Props { 19 | type Builder; 20 | fn builder() -> Self::Builder; 21 | } 22 | -------------------------------------------------------------------------------- /rscx/src/render.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | convert::Infallible, 4 | env::VarError, 5 | fmt, 6 | io::ErrorKind, 7 | net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, 8 | num::{ 9 | NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128, 10 | NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize, 11 | }, 12 | rc::Rc, 13 | sync::{ 14 | mpsc::{RecvTimeoutError, TryRecvError}, 15 | Arc, 16 | }, 17 | }; 18 | 19 | pub trait Render { 20 | fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result; 21 | } 22 | 23 | macro_rules! impl_render_for_basic_types { 24 | ($($t:ty)*) => ($( 25 | impl Render for $t { 26 | #[inline(always)] 27 | fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 28 | fmt::Display::fmt(self, f) 29 | } 30 | } 31 | )*) 32 | } 33 | 34 | impl_render_for_basic_types! { 35 | Infallible 36 | VarError 37 | ErrorKind 38 | IpAddr 39 | SocketAddr 40 | RecvTimeoutError 41 | TryRecvError 42 | bool 43 | char 44 | f32 45 | f64 46 | i8 47 | i16 48 | i32 49 | i64 50 | i128 51 | isize 52 | u8 53 | u16 54 | u32 55 | u64 56 | u128 57 | usize 58 | Ipv4Addr 59 | Ipv6Addr 60 | SocketAddrV4 61 | SocketAddrV6 62 | NonZeroI8 63 | NonZeroI16 64 | NonZeroI32 65 | NonZeroI64 66 | NonZeroI128 67 | NonZeroIsize 68 | NonZeroU8 69 | NonZeroU16 70 | NonZeroU32 71 | NonZeroU64 72 | NonZeroU128 73 | NonZeroUsize 74 | String 75 | str 76 | } 77 | 78 | impl Render for Cow<'_, B> 79 | where 80 | B: Render + ToOwned, 81 | ::Owned: Render, 82 | { 83 | fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 84 | match *self { 85 | Cow::Borrowed(ref b) => Render::render(b, f), 86 | Cow::Owned(ref o) => Render::render(o, f), 87 | } 88 | } 89 | } 90 | 91 | impl Render for Box { 92 | #[inline(always)] 93 | fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 94 | Render::render(&**self, f) 95 | } 96 | } 97 | 98 | impl Render for &T { 99 | #[inline(always)] 100 | fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 101 | Render::render(&**self, f) 102 | } 103 | } 104 | 105 | impl Render for &mut T { 106 | #[inline(always)] 107 | fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 108 | Render::render(&**self, f) 109 | } 110 | } 111 | 112 | impl Render for Rc { 113 | #[inline(always)] 114 | fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 115 | Render::render(&**self, f) 116 | } 117 | } 118 | 119 | impl Render for Arc { 120 | #[inline(always)] 121 | fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 122 | Render::render(&**self, f) 123 | } 124 | } 125 | 126 | impl Render for Option { 127 | #[inline(always)] 128 | fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 129 | if let Some(inner) = self { 130 | inner.render(f) 131 | } else { 132 | Ok(()) 133 | } 134 | } 135 | } 136 | 137 | impl Render for () { 138 | #[inline(always)] 139 | fn render(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result { 140 | Ok(()) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | dist/ 4 | -------------------------------------------------------------------------------- /website/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "website" 3 | edition = "2021" 4 | version = "0.1.0" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | async-recursion = "1.0.5" 10 | csm = { git = "https://github.com/Pitasi/csm", version = "0.1.0" } 11 | rscx = { version = "0.1.11" } 12 | rscx-mdx = { version = "0.1.6" } 13 | tokio = { version = "1.32.0", features = ["full"] } 14 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # website 2 | 3 | Static site generator for https://rscx.dev. 4 | 5 | It uses rscx (duh), [https://github.com/Pitasi/rscx-mdx](rscx-mdx) for writing 6 | markdown and [https://github.com/Pitasi/csm](csm) for scoped CSS styling. 7 | 8 | 9 | ## Build the site 10 | 11 | Install [https://github.com/casey/just](just) and run: 12 | 13 | ```sh 14 | just build 15 | ``` 16 | -------------------------------------------------------------------------------- /website/justfile: -------------------------------------------------------------------------------- 1 | dev: 2 | static-web-server --port 8787 --root ./dist & 3 | open http://localhost:8787 4 | cargo watch -i dist/ -s "cargo run && just copy-bundle-css" 5 | 6 | build-release: build-html-release copy-bundle-css 7 | 8 | build-html-release: 9 | cargo run --release 10 | 11 | copy-bundle-css: 12 | cp ./target/csm/bundle.css ./dist/bundle.css 13 | -------------------------------------------------------------------------------- /website/pages/index.md: -------------------------------------------------------------------------------- 1 | # rscx 🪶 2 | 3 | It looks like React (JSX) but it's executed in a macro, at build time. Your 4 | runtime will only need to send the pre-formatted string back to the client. 5 | 6 | By reducing the number of memory allocations needed, we can say that rscx is 7 | *blazingly fast*. 8 | 9 | 10 | ## Usage 11 | 12 | Import it from crates.io: 13 | 14 | ``` 15 | $ cargo add rscx 16 | ``` 17 | 18 | And, use it in your code: 19 | 20 |

21 | 22 | ```Rust 23 | use rscx::{component, html, props, CollectFragment}; 24 | 25 | #[tokio::main] 26 | async fn main() { 27 | let app = app().await; 28 | println!("{}", app); 29 | } 30 | 31 | // simple function returning a String 32 | // it will call the Items() function 33 | async fn app() -> String { 34 | let s = "ul { color: red; }"; 35 | html! { 36 | 37 | 38 | 39 | 40 | 41 | 42 | // call a component with no props 43 |
44 | 45 | // call a component with props and children 46 |
47 | 48 |
49 | 50 | 51 | } 52 | } 53 | 54 | #[component] 55 | /// mark functions with #[component] to use them as components inside html! macro 56 | fn Section( 57 | // you can use `builder` attributes to specify a default value (makes this prop optional) 58 | #[builder(default = "Default title".into(), setter(into))] title: String, 59 | #[builder(default)] children: String, 60 | ) -> String { 61 | html! { 62 |
63 |

{ title }

64 | { children } 65 |
66 | } 67 | } 68 | 69 | #[component] 70 | async fn Items() -> String { 71 | let data = load_data_async().await; 72 | html! { 73 |
    74 | { 75 | data 76 | .into_iter() 77 | .map(|item| html! {
  • { item }
  • }) 78 | .collect_fragment() // helper method to collect a list of components into a String 79 | } 80 |
81 | } 82 | } 83 | 84 | /// async functions can be easily used in the body of a component, as every component is an async 85 | /// function 86 | async fn load_data_async() -> Vec { 87 | vec!["a".to_string(), "b".to_string(), "c".to_string()] 88 | } 89 | ``` 90 | 91 |
92 | -------------------------------------------------------------------------------- /website/pages/subfolder/index.md: -------------------------------------------------------------------------------- 1 | # testing subfolders :) 2 | -------------------------------------------------------------------------------- /website/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{self, File}, 3 | io::{Read, Write}, 4 | path::Path, 5 | }; 6 | 7 | use csm::csm; 8 | use rscx::{component, html}; 9 | use rscx_mdx::mdx::{Mdx, MdxComponentProps}; 10 | 11 | #[tokio::main] 12 | async fn main() { 13 | let pages_dir = Path::new("pages"); 14 | let dist_dir = Path::new("dist"); 15 | let _ = fs::remove_dir_all(&dist_dir); 16 | render(&pages_dir, &dist_dir).await; 17 | } 18 | 19 | #[async_recursion::async_recursion] 20 | async fn render(dir: &Path, dist_dir: &Path) { 21 | fs::create_dir_all(&dist_dir).unwrap(); 22 | 23 | for entry in dir.read_dir().unwrap() { 24 | let entry = entry.unwrap(); 25 | let path = entry.path(); 26 | 27 | if entry.file_type().unwrap().is_dir() { 28 | render(&path, &dist_dir.join(entry.file_name())).await; 29 | continue; 30 | } 31 | 32 | let file_name = path.file_name().unwrap().to_str().unwrap(); 33 | if !file_name.ends_with(".md") { 34 | continue; 35 | } 36 | 37 | println!("building {}", path.as_path().display()); 38 | let file_name = file_name.trim_end_matches(".md"); 39 | 40 | let mut file = File::open(&path).unwrap(); 41 | let mut contents = String::new(); 42 | file.read_to_string(&mut contents).unwrap(); 43 | let mut file = File::create(dist_dir.join(format!("{}.html", file_name))).unwrap(); 44 | 45 | let html = render_markdown(contents).await; 46 | file.write_all(html.as_bytes()).unwrap(); 47 | } 48 | } 49 | 50 | async fn render_markdown(md: String) -> String { 51 | html! { 52 | 53 | 54 | 55 | } 56 | } 57 | 58 | async fn handler(name: String, _props: MdxComponentProps) -> String { 59 | match name { 60 | _ => html! {}, 61 | } 62 | } 63 | 64 | #[component] 65 | async fn Shell(children: String) -> String { 66 | html! { 67 | 68 | 69 | 70 | 71 | rscx 72 | 73 | 74 | 78 | {children} 79 | 80 | 81 | } 82 | } 83 | --------------------------------------------------------------------------------