├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE.md ├── README.md ├── SECURITY.md ├── macros ├── Cargo.toml ├── build.rs └── src │ ├── config.rs │ ├── declare.rs │ ├── error.rs │ ├── grammar.lalrpop │ ├── html.rs │ ├── ident.rs │ ├── lexer.rs │ ├── lib.rs │ ├── map.rs │ ├── parser.rs │ └── span.rs ├── tests ├── Cargo.toml ├── cases │ ├── expected-token.rs │ ├── expected-token.stderr │ ├── not-enough-children.rs │ ├── not-enough-children.stderr │ ├── tag-mismatch.rs │ ├── tag-mismatch.stderr │ ├── text-nodes-need-to-be-quoted.rs │ ├── text-nodes-need-to-be-quoted.stderr │ ├── unexpected-end-of-macro.rs │ ├── unexpected-end-of-macro.stderr │ └── update-references.sh └── main.rs └── typed-html ├── Cargo.toml └── src ├── dom.rs ├── elements.rs ├── events.rs ├── lib.rs └── types ├── class.rs ├── id.rs ├── mod.rs ├── spacedlist.rs └── spacedset.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | schedule: 9 | - cron: '11 7 * * 1,4' 10 | 11 | env: 12 | RUSTFLAGS: -Dwarnings 13 | 14 | jobs: 15 | fmt: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Run cargo fmt 20 | run: | 21 | cargo fmt --all -- --check 22 | clippy: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Run cargo clippy 27 | run: | 28 | cargo clippy --workspace --tests --examples 29 | docs: 30 | runs-on: ubuntu-latest 31 | env: 32 | RUSTDOCFLAGS: -Dwarnings 33 | steps: 34 | - uses: actions/checkout@v2 35 | - name: Run cargo clippy 36 | run: | 37 | cargo doc --workspace --no-deps 38 | test: 39 | runs-on: ${{ matrix.os }} 40 | strategy: 41 | matrix: 42 | os: [ubuntu-latest, windows-latest, macOS-latest] 43 | steps: 44 | - uses: actions/checkout@v2 45 | - name: Run cargo test 46 | run: | 47 | cargo test --workspace 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.5.0 - 2023-03-29 4 | 5 | ### 🛠️ Fixes 6 | 7 | - **✨ Fix broken links and naming issues from fork transition- [adrianheine], [pr29]** 8 | 9 | This PR helps clean up a variety of references and links that weren't caught 10 | when the project transitioned from a fork of the `typed-html` crate. 11 | 12 | ### 🌿 Maintenance 13 | 14 | - **✨ Dependency gardening - [ashleygwilliams], [pr32]/[pr33]** 15 | 16 | General dependency maintenance with two notable actions: 17 | 18 | - replace `ansi_term` with `console` to match the rest of the axo toolchain 19 | - update `lalrpop` to 0.19.9 (latest release) to address warning 20 | 21 | [adrianheine]: https://github.com/adrianheine 22 | [pr29]: https://github.com/axodotdev/axohtml/pull/29 23 | [pr32]: https://github.com/axodotdev/axohtml/pull/32 24 | [pr33]: https://github.com/axodotdev/axohtml/pull/33 25 | 26 | ## 0.4.1 - 2023-01-24 27 | 28 | ### 🛠️ Fixes 29 | 30 | - **✨ Fix capitalization for Permissions-Policy meta tag- [ashleygwilliams], [pr26]** 31 | 32 | This PR updates the capitalization of the Permissions Policy header from 33 | `Permissions-policy` to `Permissions-Policy`. 34 | 35 | [pr26]: https://github.com/axodotdev/axohtml/pull/26 36 | [ashleygwilliams]: https://github.com/ashleygwilliams 37 | 38 | ## 0.4.0 - 2023-01-24 39 | 40 | ### 🎁 Features 41 | 42 | - **✨ Add support for Permissions-Policy meta tag- [SaraVieira], [pr23]** 43 | 44 | This pr adds support for using the [`Permissions-Policy` meta tag](https://www.w3.org/TR/permissions-policy-1/) that is used for defining a set of browser APIs you do not wish your website to have. 45 | 46 | [pr23]: https://github.com/axodotdev/axohtml/pull/23 47 | 48 | ## 0.3.0 - 2023-01-02 49 | 50 | ### 🎁 Features 51 | 52 | - **✨ More robust `aria` type checking - [SaraVieira], [i2]/[pr12], [i3]/[pr11]** 53 | 54 | `aria-sort` and `aria-orientation` now offer more robust type checking following the guidelines of MDN you can see in their pages: 55 | 56 | - [`aria-sort`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-sort) 57 | - [`aria-orientation`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-orientation) 58 | 59 | [i2]: https://github.com/axodotdev/axohtml/issues/2 60 | [pr12]: https://github.com/axodotdev/axohtml/pull/12 61 | [i3]: https://github.com/axodotdev/axohtml/issues/3 62 | [pr11]: https://github.com/axodotdev/axohtml/pull/11 63 | 64 | - **✨ Add twitter SEO tag support - [SaraVieira], [pr17]** 65 | 66 | Add support for meta tags used for twitter cards as showed in [their docs](https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/markup) 67 | 68 | [pr17]: https://github.com/axodotdev/axohtml/pull/17 69 | 70 | ### 🛠️ Fixes 71 | 72 | - **✨ Data Attributes now work with more than one hyphen - [SaraVieira], [pr10]** 73 | 74 | Our support for `data` attributes was limited in the way that it only supported one hyphen in said attributes, well, no more, use as many hyphens as your heart pleases 75 | 76 | [pr10]: https://github.com/axodotdev/axohtml/pull/10 77 | 78 | - **✨ Allow `script` tags in HTML - [SaraVieira], [pr10]** 79 | 80 | We now allow you to add script tags in the HTML after the body as the HTML standards also allow 81 | 82 | - **✨ Allow unescaped text in`script`- [SaraVieira], [pr14]** 83 | 84 | Until now we were escaping the text passed down to the `script` tag and in the `script` tag is the only place we are sure we don't want to escape that so that's fixed and you can add `script` tags with unescaped text inside 85 | 86 | [pr14]: https://github.com/axodotdev/axohtml/pull/14 87 | 88 | ## 0.2.0 - 2022-12-19 89 | 90 | ### 🎁 Features 91 | 92 | - **✨ New Attribute - `aria`** - [SaraVieira] 93 | 94 | [`aria` attributes] are critical to making the web more accessible to 95 | everyone, but most importantly, people with disabilities. These were a to-do 96 | item from the original project, and so we to-did them. At least most of 97 | them. There are a [few open issues] if you'd like to help us complete the 98 | implementation. 99 | 100 | [`aria` attributes]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA 101 | [few open issues]: https://github.com/axodotdev/axohtml/issues?q=is%3Aissue+is%3Aopen+aria 102 | 103 | - **✨ New Attribute - `meta:property`** - [SaraVieira] 104 | 105 | If you ask the internet why `meta` tags have a `property` attribute that 106 | isn't in the spec, you won't get great answers. Although not formally 107 | specified in HTML5, `property` attributes in `meta` tags are important for 108 | SEO and [the Open Graph Protocol]. They _are_ documented in [RDFa] which is 109 | a formal W3C recommendation. 110 | 111 | It is outside the scope of this project to standardize standards bodies. We 112 | needed to support the `property` attribute, and so we did. 113 | 114 | [saravieira]: https://github.com/SaraVieira 115 | [the open graph protocol]: https://ogp.me/ 116 | [rdfa]: https://en.wikipedia.org/wiki/RDFa 117 | 118 | ## 0.1.0 - 2022-12-16 119 | 120 | Forked project, removed `dodrio` and `stdweb` features; initial release. 121 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks so much for your interest in contributing to `axohtml`. We are excited 4 | about the building a community of contributors to the project. Here's some 5 | guiding principles for working with us: 6 | 7 | **1. File an issue first!** 8 | 9 | Except for the absolute tiniest of PRs (e.g. a single typo fix), please file an 10 | issue before opening a PR. This can help ensure that the problem you are trying 11 | to solve and the solution you have in mind will be accepted. Where possible, we 12 | don't want folks wasting time on directions we don't want to take the project. 13 | 14 | **2. Write tests, or at least detailed reproduction steps** 15 | 16 | If you find a bug, the best way to prioritize getting it fixed is to open a PR 17 | with a failing test! If you a opening a bug fix PR, please add a test to show 18 | that your fix works. 19 | 20 | **3. Overcommunicate** 21 | 22 | In all scenarios, please provide as much context as possible- you may not think 23 | it's important but it may be! 24 | 25 | **4. Patience** 26 | 27 | Axo is a very small company, it's possible that we may not be able to 28 | immediately prioritize your issue. We are excite to develop a community of 29 | contributors around this project, but it won't always be on the top of our to-do 30 | list, even if we wish it could be. 31 | 32 | If you haven't heard from us in a while and want to check in, feel free to 33 | at-mention @ashleygwilliams- but please be kind while doing so! 34 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "typed-html", 4 | "macros", 5 | "tests", 6 | ] 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | ### 1. Definitions 5 | 6 | **1.1. “Contributor”** 7 | means each individual or legal entity that creates, contributes to 8 | the creation of, or owns Covered Software. 9 | 10 | **1.2. “Contributor Version”** 11 | means the combination of the Contributions of others (if any) used 12 | by a Contributor and that particular Contributor's Contribution. 13 | 14 | **1.3. “Contribution”** 15 | means Covered Software of a particular Contributor. 16 | 17 | **1.4. “Covered Software”** 18 | means Source Code Form to which the initial Contributor has attached 19 | the notice in Exhibit A, the Executable Form of such Source Code 20 | Form, and Modifications of such Source Code Form, in each case 21 | including portions thereof. 22 | 23 | **1.5. “Incompatible With Secondary Licenses”** 24 | means 25 | 26 | * **(a)** that the initial Contributor has attached the notice described 27 | in Exhibit B to the Covered Software; or 28 | * **(b)** that the Covered Software was made available under the terms of 29 | version 1.1 or earlier of the License, but not also under the 30 | terms of a Secondary License. 31 | 32 | **1.6. “Executable Form”** 33 | means any form of the work other than Source Code Form. 34 | 35 | **1.7. “Larger Work”** 36 | means a work that combines Covered Software with other material, in 37 | a separate file or files, that is not Covered Software. 38 | 39 | **1.8. “License”** 40 | means this document. 41 | 42 | **1.9. “Licensable”** 43 | means having the right to grant, to the maximum extent possible, 44 | whether at the time of the initial grant or subsequently, any and 45 | all of the rights conveyed by this License. 46 | 47 | **1.10. “Modifications”** 48 | means any of the following: 49 | 50 | * **(a)** any file in Source Code Form that results from an addition to, 51 | deletion from, or modification of the contents of Covered 52 | Software; or 53 | * **(b)** any new file in Source Code Form that contains any Covered 54 | Software. 55 | 56 | **1.11. “Patent Claims” of a Contributor** 57 | means any patent claim(s), including without limitation, method, 58 | process, and apparatus claims, in any patent Licensable by such 59 | Contributor that would be infringed, but for the grant of the 60 | License, by the making, using, selling, offering for sale, having 61 | made, import, or transfer of either its Contributions or its 62 | Contributor Version. 63 | 64 | **1.12. “Secondary License”** 65 | means either the GNU General Public License, Version 2.0, the GNU 66 | Lesser General Public License, Version 2.1, the GNU Affero General 67 | Public License, Version 3.0, or any later versions of those 68 | licenses. 69 | 70 | **1.13. “Source Code Form”** 71 | means the form of the work preferred for making modifications. 72 | 73 | **1.14. “You” (or “Your”)** 74 | means an individual or a legal entity exercising rights under this 75 | License. For legal entities, “You” includes any entity that 76 | controls, is controlled by, or is under common control with You. For 77 | purposes of this definition, “control” means **(a)** the power, direct 78 | or indirect, to cause the direction or management of such entity, 79 | whether by contract or otherwise, or **(b)** ownership of more than 80 | fifty percent (50%) of the outstanding shares or beneficial 81 | ownership of such entity. 82 | 83 | 84 | ### 2. License Grants and Conditions 85 | 86 | #### 2.1. Grants 87 | 88 | Each Contributor hereby grants You a world-wide, royalty-free, 89 | non-exclusive license: 90 | 91 | * **(a)** under intellectual property rights (other than patent or trademark) 92 | Licensable by such Contributor to use, reproduce, make available, 93 | modify, display, perform, distribute, and otherwise exploit its 94 | Contributions, either on an unmodified basis, with Modifications, or 95 | as part of a Larger Work; and 96 | * **(b)** under Patent Claims of such Contributor to make, use, sell, offer 97 | for sale, have made, import, and otherwise transfer either its 98 | Contributions or its Contributor Version. 99 | 100 | #### 2.2. Effective Date 101 | 102 | The licenses granted in Section 2.1 with respect to any Contribution 103 | become effective for each Contribution on the date the Contributor first 104 | distributes such Contribution. 105 | 106 | #### 2.3. Limitations on Grant Scope 107 | 108 | The licenses granted in this Section 2 are the only rights granted under 109 | this License. No additional rights or licenses will be implied from the 110 | distribution or licensing of Covered Software under this License. 111 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 112 | Contributor: 113 | 114 | * **(a)** for any code that a Contributor has removed from Covered Software; 115 | or 116 | * **(b)** for infringements caused by: **(i)** Your and any other third party's 117 | modifications of Covered Software, or **(ii)** the combination of its 118 | Contributions with other software (except as part of its Contributor 119 | Version); or 120 | * **(c)** under Patent Claims infringed by Covered Software in the absence of 121 | its Contributions. 122 | 123 | This License does not grant any rights in the trademarks, service marks, 124 | or logos of any Contributor (except as may be necessary to comply with 125 | the notice requirements in Section 3.4). 126 | 127 | #### 2.4. Subsequent Licenses 128 | 129 | No Contributor makes additional grants as a result of Your choice to 130 | distribute the Covered Software under a subsequent version of this 131 | License (see Section 10.2) or under the terms of a Secondary License (if 132 | permitted under the terms of Section 3.3). 133 | 134 | #### 2.5. Representation 135 | 136 | Each Contributor represents that the Contributor believes its 137 | Contributions are its original creation(s) or it has sufficient rights 138 | to grant the rights to its Contributions conveyed by this License. 139 | 140 | #### 2.6. Fair Use 141 | 142 | This License is not intended to limit any rights You have under 143 | applicable copyright doctrines of fair use, fair dealing, or other 144 | equivalents. 145 | 146 | #### 2.7. Conditions 147 | 148 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 149 | in Section 2.1. 150 | 151 | 152 | ### 3. Responsibilities 153 | 154 | #### 3.1. Distribution of Source Form 155 | 156 | All distribution of Covered Software in Source Code Form, including any 157 | Modifications that You create or to which You contribute, must be under 158 | the terms of this License. You must inform recipients that the Source 159 | Code Form of the Covered Software is governed by the terms of this 160 | License, and how they can obtain a copy of this License. You may not 161 | attempt to alter or restrict the recipients' rights in the Source Code 162 | Form. 163 | 164 | #### 3.2. Distribution of Executable Form 165 | 166 | If You distribute Covered Software in Executable Form then: 167 | 168 | * **(a)** such Covered Software must also be made available in Source Code 169 | Form, as described in Section 3.1, and You must inform recipients of 170 | the Executable Form how they can obtain a copy of such Source Code 171 | Form by reasonable means in a timely manner, at a charge no more 172 | than the cost of distribution to the recipient; and 173 | 174 | * **(b)** You may distribute such Executable Form under the terms of this 175 | License, or sublicense it under different terms, provided that the 176 | license for the Executable Form does not attempt to limit or alter 177 | the recipients' rights in the Source Code Form under this License. 178 | 179 | #### 3.3. Distribution of a Larger Work 180 | 181 | You may create and distribute a Larger Work under terms of Your choice, 182 | provided that You also comply with the requirements of this License for 183 | the Covered Software. If the Larger Work is a combination of Covered 184 | Software with a work governed by one or more Secondary Licenses, and the 185 | Covered Software is not Incompatible With Secondary Licenses, this 186 | License permits You to additionally distribute such Covered Software 187 | under the terms of such Secondary License(s), so that the recipient of 188 | the Larger Work may, at their option, further distribute the Covered 189 | Software under the terms of either this License or such Secondary 190 | License(s). 191 | 192 | #### 3.4. Notices 193 | 194 | You may not remove or alter the substance of any license notices 195 | (including copyright notices, patent notices, disclaimers of warranty, 196 | or limitations of liability) contained within the Source Code Form of 197 | the Covered Software, except that You may alter any license notices to 198 | the extent required to remedy known factual inaccuracies. 199 | 200 | #### 3.5. Application of Additional Terms 201 | 202 | You may choose to offer, and to charge a fee for, warranty, support, 203 | indemnity or liability obligations to one or more recipients of Covered 204 | Software. However, You may do so only on Your own behalf, and not on 205 | behalf of any Contributor. You must make it absolutely clear that any 206 | such warranty, support, indemnity, or liability obligation is offered by 207 | You alone, and You hereby agree to indemnify every Contributor for any 208 | liability incurred by such Contributor as a result of warranty, support, 209 | indemnity or liability terms You offer. You may include additional 210 | disclaimers of warranty and limitations of liability specific to any 211 | jurisdiction. 212 | 213 | 214 | ### 4. Inability to Comply Due to Statute or Regulation 215 | 216 | If it is impossible for You to comply with any of the terms of this 217 | License with respect to some or all of the Covered Software due to 218 | statute, judicial order, or regulation then You must: **(a)** comply with 219 | the terms of this License to the maximum extent possible; and **(b)** 220 | describe the limitations and the code they affect. Such description must 221 | be placed in a text file included with all distributions of the Covered 222 | Software under this License. Except to the extent prohibited by statute 223 | or regulation, such description must be sufficiently detailed for a 224 | recipient of ordinary skill to be able to understand it. 225 | 226 | 227 | ### 5. Termination 228 | 229 | **5.1.** The rights granted under this License will terminate automatically 230 | if You fail to comply with any of its terms. However, if You become 231 | compliant, then the rights granted under this License from a particular 232 | Contributor are reinstated **(a)** provisionally, unless and until such 233 | Contributor explicitly and finally terminates Your grants, and **(b)** on an 234 | ongoing basis, if such Contributor fails to notify You of the 235 | non-compliance by some reasonable means prior to 60 days after You have 236 | come back into compliance. Moreover, Your grants from a particular 237 | Contributor are reinstated on an ongoing basis if such Contributor 238 | notifies You of the non-compliance by some reasonable means, this is the 239 | first time You have received notice of non-compliance with this License 240 | from such Contributor, and You become compliant prior to 30 days after 241 | Your receipt of the notice. 242 | 243 | **5.2.** If You initiate litigation against any entity by asserting a patent 244 | infringement claim (excluding declaratory judgment actions, 245 | counter-claims, and cross-claims) alleging that a Contributor Version 246 | directly or indirectly infringes any patent, then the rights granted to 247 | You by any and all Contributors for the Covered Software under Section 248 | 2.1 of this License shall terminate. 249 | 250 | **5.3.** In the event of termination under Sections 5.1 or 5.2 above, all 251 | end user license agreements (excluding distributors and resellers) which 252 | have been validly granted by You or Your distributors under this License 253 | prior to termination shall survive termination. 254 | 255 | 256 | ### 6. Disclaimer of Warranty 257 | 258 | > Covered Software is provided under this License on an “as is” 259 | > basis, without warranty of any kind, either expressed, implied, or 260 | > statutory, including, without limitation, warranties that the 261 | > Covered Software is free of defects, merchantable, fit for a 262 | > particular purpose or non-infringing. The entire risk as to the 263 | > quality and performance of the Covered Software is with You. 264 | > Should any Covered Software prove defective in any respect, You 265 | > (not any Contributor) assume the cost of any necessary servicing, 266 | > repair, or correction. This disclaimer of warranty constitutes an 267 | > essential part of this License. No use of any Covered Software is 268 | > authorized under this License except under this disclaimer. 269 | 270 | ### 7. Limitation of Liability 271 | 272 | > Under no circumstances and under no legal theory, whether tort 273 | > (including negligence), contract, or otherwise, shall any 274 | > Contributor, or anyone who distributes Covered Software as 275 | > permitted above, be liable to You for any direct, indirect, 276 | > special, incidental, or consequential damages of any character 277 | > including, without limitation, damages for lost profits, loss of 278 | > goodwill, work stoppage, computer failure or malfunction, or any 279 | > and all other commercial damages or losses, even if such party 280 | > shall have been informed of the possibility of such damages. This 281 | > limitation of liability shall not apply to liability for death or 282 | > personal injury resulting from such party's negligence to the 283 | > extent applicable law prohibits such limitation. Some 284 | > jurisdictions do not allow the exclusion or limitation of 285 | > incidental or consequential damages, so this exclusion and 286 | > limitation may not apply to You. 287 | 288 | 289 | ### 8. Litigation 290 | 291 | Any litigation relating to this License may be brought only in the 292 | courts of a jurisdiction where the defendant maintains its principal 293 | place of business and such litigation shall be governed by laws of that 294 | jurisdiction, without reference to its conflict-of-law provisions. 295 | Nothing in this Section shall prevent a party's ability to bring 296 | cross-claims or counter-claims. 297 | 298 | 299 | ### 9. Miscellaneous 300 | 301 | This License represents the complete agreement concerning the subject 302 | matter hereof. If any provision of this License is held to be 303 | unenforceable, such provision shall be reformed only to the extent 304 | necessary to make it enforceable. Any law or regulation which provides 305 | that the language of a contract shall be construed against the drafter 306 | shall not be used to construe this License against a Contributor. 307 | 308 | 309 | ### 10. Versions of the License 310 | 311 | #### 10.1. New Versions 312 | 313 | Mozilla Foundation is the license steward. Except as provided in Section 314 | 10.3, no one other than the license steward has the right to modify or 315 | publish new versions of this License. Each version will be given a 316 | distinguishing version number. 317 | 318 | #### 10.2. Effect of New Versions 319 | 320 | You may distribute the Covered Software under the terms of the version 321 | of the License under which You originally received the Covered Software, 322 | or under the terms of any subsequent version published by the license 323 | steward. 324 | 325 | #### 10.3. Modified Versions 326 | 327 | If you create software not governed by this License, and you want to 328 | create a new license for such software, you may create and use a 329 | modified version of this License if you rename the license and remove 330 | any references to the name of the license steward (except to note that 331 | such modified license differs from this License). 332 | 333 | #### 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 334 | 335 | If You choose to distribute Source Code Form that is Incompatible With 336 | Secondary Licenses under the terms of this version of the License, the 337 | notice described in Exhibit B of this License must be attached. 338 | 339 | ## Exhibit A - Source Code Form License Notice 340 | 341 | This Source Code Form is subject to the terms of the Mozilla Public 342 | License, v. 2.0. If a copy of the MPL was not distributed with this 343 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 344 | 345 | If it is not possible or desirable to put the notice in a particular 346 | file, then You may include the notice in a location (such as a LICENSE 347 | file in a relevant directory) where a recipient would be likely to look 348 | for such a notice. 349 | 350 | You may add additional accurate notices of copyright ownership. 351 | 352 | ## Exhibit B - “Incompatible With Secondary Licenses” Notice 353 | 354 | This Source Code Form is "Incompatible With Secondary Licenses", as 355 | defined by the Mozilla Public License, v. 2.0. 356 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # axohtml 2 | 3 | [![Github Actions Rust](https://github.com/axodotdev/axohtml/actions/workflows/rust.yml/badge.svg)](https://github.com/axodotdev/axohtml/actions) 4 | [![crates.io](https://img.shields.io/crates/v/axohtml.svg)](https://crates.io/crates/axohtml) 5 | [![License: MPL 2.0](https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0) 6 | 7 | This crate provides the `html!` macro for building fully type checked HTML 8 | documents inside your Rust code using roughly [JSX] compatible syntax. 9 | 10 | This crate is a fork of the great [Bodil Stokke's] [typed-html] crate. Opted 11 | for a fork instead of maintainership because not currently intending to use or 12 | maintain the Wasm compatibility (for now). 13 | 14 | [Bodil Stokke's]: https://github.com/bodil 15 | [typed-html]: https://github.com/bodil/typed-html 16 | 17 | ## Quick Preview 18 | 19 | ```rust 20 | let mut doc: DOMTree = html!( 21 | 22 | 23 | "Hello Axo" 24 | 25 | 26 | 27 |

">o_o<"

28 |

29 | "The tool company for tool companies" 30 |

31 | { (0..3).map(|_| html!( 32 |

33 | ">o_o<" 34 |

35 | )) } 36 |

37 | "Every company should be a developer experience company." 38 |

39 | 40 | 41 | ); 42 | let doc_str = doc.to_string(); 43 | ``` 44 | 45 | ## Syntax 46 | 47 | This macro largely follows [JSX] syntax, but with some differences: 48 | 49 | * Text nodes must be quoted, because there's only so much Rust's tokeniser can 50 | handle outside string literals. So, instead of `

Hello

`, you need to 51 | write `

"Hello"

`. (The parser will throw an error asking you to do this 52 | if you forget.) 53 | * Element attributes will accept simple Rust expressions, but the parser has 54 | its limits, as it's not a full Rust parser. You can use literals, 55 | variables, dotted properties, type constructors and single function or 56 | method calls. If you use something the parser isn't currently capable of 57 | handling, it will complain. You can put braces or parentheses around the 58 | expression if the parser doesn't understand 59 | it. You can use any Rust code inside a brace or parenthesis block. 60 | 61 | ## Valid HTML5 62 | 63 | The macro will only accept valid HTML5 tags, with no tags or attributes marked 64 | experimental or obsolete. If it won't accept something you want it to accept, we 65 | can discuss it over a pull request (experimental tags and attributes, in 66 | particular, are mostly omitted just for brevity, and you're welcome to implement 67 | them). 68 | 69 | The structure validation is simplistic by necessity, as it defers to the type 70 | system: a few elements will have one or more required children, and any element 71 | which accepts children will have a restriction on the type of the children, 72 | usually a broad group as defined by the HTML spec. Many elements have 73 | restrictions on children of children, or require a particular ordering of 74 | optional elements, which isn't currently validated. 75 | 76 | ## Attribute Values 77 | 78 | Brace blocks in the attribute value position should return the expected type for 79 | the attribute. The type checker will complain if you return an unsupported type. 80 | You can also use literals or a few simple Rust expressions as attribute values 81 | (see the Syntax section above). 82 | 83 | The `html!` macro will add an [`.into()`][Into::into] call to the value 84 | expression, so that you can use any type that has an [`Into`][Into] trait 85 | defined for the actual attribute type `A`. 86 | 87 | As a special case, if you use a string literal, the macro will instead use the 88 | [`FromStr`][FromStr] trait to try and parse the string literal into the 89 | expected type. This is extremely useful for eg. CSS classes, letting you type 90 | `class="css-class-1 css-class-2"` instead of going to the trouble of 91 | constructing a [`SpacedSet`][SpacedSet]. The big caveat for this: 92 | currently, the macro is not able to validate the string at compile time, and the 93 | conversion will panic at runtime if the string is invalid. 94 | 95 | ### Example 96 | 97 | ```rust 98 | let classList: SpacedSet = ["foo", "bar", "baz"].try_into()?; 99 | html!( 100 |
101 |
// parses a string literal 102 |
// uses From<[&str, &str, &str]> 103 |
// uses a variable in scope 104 |
107 |
108 | ) 109 | ``` 110 | 111 | ## Generated Nodes 112 | 113 | Brace blocks in the child node position are expected to return an 114 | [`IntoIterator`][IntoIterator] of [`DOMTree`][DOMTree]s. You can return single 115 | elements or text nodes, as they both implement `IntoIterator` for themselves. 116 | The macro will consume this iterator at runtime and insert the generated nodes 117 | as children in the expected position. 118 | 119 | ### Example 120 | 121 | ```rust 122 | html!( 123 |
    124 | { (1..=5).map(|i| html!( 125 |
  • { text!("{}", i) }
  • 126 | )) } 127 |
128 | ) 129 | ``` 130 | 131 | ## Rendering 132 | 133 | You have two options for actually producing something useful from the DOM tree 134 | that comes out of the macro. 135 | 136 | ### Render to a string 137 | 138 | The DOM tree data structure implements [`Display`][Display], so you can call 139 | [`to_string()`][to_string] on it to render it to a [`String`][String]. If you 140 | plan to do this, the type of the tree should be [`DOMTree`][DOMTree] to 141 | ensure you're not using any event handlers that can't be printed. 142 | 143 | ```rust 144 | let doc: DOMTree = html!( 145 |

"Hello Axo"

146 | ); 147 | let doc_str = doc.to_string(); 148 | assert_eq!("

Hello Axo

", doc_str); 149 | ``` 150 | 151 | ### Render to a virtual DOM 152 | 153 | The DOM tree structure also implements a method called `vnode()`, which renders 154 | the tree to a tree of [`VNode`][VNode]s, which is a mirror of the generated tree 155 | with every attribute value rendered into `String`s. You can walk this virtual 156 | DOM tree and pass it on to your favourite virtual DOM system. 157 | 158 | ## License 159 | 160 | 161 | This software is subject to the terms of the Mozilla Public License, v. 2.0. If 162 | a copy of the MPL was not distributed with this file, You can obtain one at 163 | . 164 | 165 | Copyright 2018 Bodil Stokke, 2022 Axo Developer Co. 166 | 167 | [JSX]: https://reactjs.org/docs/introducing-jsx.html 168 | [Display]: https://doc.rust-lang.org/std/fmt/trait.Display.html 169 | [String]: https://doc.rust-lang.org/std/string/struct.String.html 170 | [to_string]: https://doc.rust-lang.org/std/string/trait.ToString.html#tymethod.to_string 171 | [VNode]: https://docs.rs/axohtml/latest/axohtml/dom/enum.VNode.html 172 | [FromStr]: https://doc.rust-lang.org/std/str/trait.FromStr.html 173 | [SpacedSet]: https://docs.rs/axohtml/latest/axohtml/types/struct.SpacedSet.html 174 | [IntoIterator]: https://doc.rust-lang.org/std/iter/trait.IntoIterator.html 175 | [Into]: https://doc.rust-lang.org/std/convert/trait.Into.html 176 | [Into::into]: https://doc.rust-lang.org/std/convert/trait.Into.html#method.into 177 | [DOMTree]: https://docs.rs/axohtml/latest/axohtml/dom/type.DOMTree.html 178 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Axo Developer Co. takes the security of our software products and services seriously. If you believe you have found a security vulnerability in this open source repository, please report the issue to us directly using GitHub private vulnerability reporting or email ashley@axo.dev. If you aren't sure you have found a security vulnerability but have a suspicion or concern, feel free to message anyways; we prefer over-communication :) 2 | 3 | Please do not report security vulnerabilities publicly, such as via GitHub issues, Twitter, or other social media. 4 | 5 | Thanks for helping make software safe for everyone! 6 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axohtml-macros" 3 | version = "0.5.0" 4 | edition = "2018" 5 | authors = ["Axo Developer Co ", "Bodil Stokke "] 6 | build = "build.rs" 7 | license = "MPL-2.0+" 8 | description = "Type checked JSX for Rust (proc_macro crate)" 9 | repository = "https://github.com/axodotdev/axohtml" 10 | documentation = "http://docs.rs/axohtml/" 11 | readme = "../README.md" 12 | categories = ["template-engine", "web-programming"] 13 | keywords = ["jsx", "html"] 14 | 15 | [lib] 16 | proc-macro = true 17 | 18 | [dependencies] 19 | lalrpop-util = "0.19" 20 | proc-macro2 = "1.0.54" 21 | quote = "1.0.26" 22 | console = "0.15.5" 23 | 24 | [build-dependencies] 25 | lalrpop = "0.19.9" 26 | version_check = "0.9.4" 27 | -------------------------------------------------------------------------------- /macros/build.rs: -------------------------------------------------------------------------------- 1 | extern crate lalrpop; 2 | extern crate version_check; 3 | 4 | fn main() { 5 | lalrpop::process_root().unwrap(); 6 | 7 | if version_check::is_feature_flaggable().unwrap_or(false) { 8 | println!("cargo:rustc-cfg=can_join_spans"); 9 | println!("cargo:rustc-cfg=can_show_location_of_runtime_parse_error"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /macros/src/config.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, Span, TokenStream}; 2 | 3 | use crate::map::StringyMap; 4 | 5 | pub fn required_children(element: &str) -> &[&str] { 6 | match element { 7 | "html" => &["head", "body"], 8 | "head" => &["title"], 9 | _ => &[], 10 | } 11 | } 12 | 13 | pub fn global_attrs(span: Span) -> StringyMap { 14 | let mut attrs = StringyMap::new(); 15 | { 16 | let mut insert = 17 | |key, value: &str| attrs.insert(Ident::new(key, span), value.parse().unwrap()); 18 | 19 | insert("id", "crate::types::Id"); 20 | insert("class", "crate::types::ClassList"); 21 | 22 | insert("accesskey", "String"); 23 | insert("autocapitalize", "String"); 24 | insert("contenteditable", "crate::types::Bool"); 25 | insert("contextmenu", "crate::types::Id"); 26 | insert("dir", "crate::types::TextDirection"); 27 | insert("draggable", "crate::types::Bool"); 28 | insert("hidden", "crate::types::Bool"); 29 | insert("is", "String"); 30 | insert("lang", "crate::types::LanguageTag"); 31 | insert("role", "crate::types::Role"); 32 | insert("style", "String"); 33 | insert("tabindex", "isize"); 34 | insert("title", "String"); 35 | 36 | // ARIA 37 | insert("aria_autocomplete", "String"); 38 | insert("aria_checked", "crate::types::Bool"); 39 | insert("aria_disabled", "crate::types::Bool"); 40 | insert("aria_errormessage", "String"); 41 | insert("aria_expanded", "crate::types::Bool"); 42 | insert("aria_haspopup", "crate::types::Bool"); 43 | insert("aria_hidden", "crate::types::Bool"); 44 | insert("aria_invalid", "crate::types::Bool"); 45 | insert("aria_label", "String"); 46 | insert("aria_modal", "crate::types::Bool"); 47 | insert("aria_multiline", "crate::types::Bool"); 48 | insert("aria_multiselectable", "crate::types::Bool"); 49 | insert("aria_orientation", "crate::types::AriaOrientation"); 50 | insert("aria_placeholder", "String"); 51 | insert("aria_pressed", "crate::types::Bool"); 52 | insert("aria_readonly", "crate::types::Bool"); 53 | insert("aria_required", "crate::types::Bool"); 54 | insert("aria_selected", "crate::types::Bool"); 55 | insert("aria_placeholder", "String"); 56 | insert("aria_sort", "crate::types::AriaSort"); // TODO only supports some values 57 | insert("aria_valuemax", "isize"); 58 | insert("aria_valuemin", "isize"); 59 | insert("aria_valuenow", "isize"); 60 | insert("aria_valuetext", "String"); 61 | 62 | // FIXME XML attrs missing 63 | } 64 | attrs 65 | } 66 | 67 | pub static SELF_CLOSING: &[&str] = &[ 68 | "area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", 69 | "meta", "param", "source", "track", "wbr", 70 | ]; 71 | -------------------------------------------------------------------------------- /macros/src/declare.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, Literal, TokenStream, TokenTree}; 2 | use quote::quote; 3 | 4 | use crate::config::{global_attrs, SELF_CLOSING}; 5 | use crate::error::ParseError; 6 | use crate::ident; 7 | use crate::lexer::{Lexer, Token}; 8 | use crate::map::StringyMap; 9 | use crate::parser; 10 | 11 | // State 12 | 13 | pub struct Declare { 14 | pub name: Ident, 15 | pub attrs: StringyMap, 16 | pub req_children: Vec, 17 | pub opt_children: Option, 18 | pub traits: Vec, 19 | } 20 | 21 | impl Declare { 22 | pub fn new(name: Ident) -> Self { 23 | Declare { 24 | attrs: global_attrs(name.span()), 25 | req_children: Vec::new(), 26 | opt_children: None, 27 | traits: Vec::new(), 28 | name, 29 | } 30 | } 31 | 32 | fn elem_name(&self) -> TokenTree { 33 | Ident::new(&self.name.to_string(), self.name.span()).into() 34 | } 35 | 36 | fn attr_type_name(&self) -> TokenTree { 37 | Ident::new(&format!("Attrs_{}", self.name), self.name.span()).into() 38 | } 39 | 40 | fn attrs(&self) -> impl Iterator + '_ { 41 | self.attrs.iter().map(|(key, value)| { 42 | let attr_name: TokenTree = ident::new_raw(&key.to_string(), key.span()).into(); 43 | let attr_type = value.clone(); 44 | let attr_str = Literal::string(&key.to_string()).into(); 45 | (attr_name, attr_type, attr_str) 46 | }) 47 | } 48 | 49 | fn req_children(&self) -> impl Iterator + '_ { 50 | self.req_children.iter().map(|child| { 51 | let child_name: TokenTree = 52 | Ident::new(&format!("child_{}", child), child.span()).into(); 53 | let child_type: TokenTree = Ident::new(&child.to_string(), child.span()).into(); 54 | let child_str = Literal::string(&child.to_string()).into(); 55 | (child_name, child_type, child_str) 56 | }) 57 | } 58 | 59 | pub fn into_token_stream(self) -> TokenStream { 60 | let mut stream = TokenStream::new(); 61 | stream.extend(self.attr_struct()); 62 | stream.extend(self.struct_()); 63 | stream.extend(self.impl_()); 64 | stream.extend(self.impl_node()); 65 | stream.extend(self.impl_element()); 66 | stream.extend(self.impl_marker_traits()); 67 | stream.extend(self.impl_display()); 68 | stream.extend(self.impl_into_iter()); 69 | stream 70 | } 71 | 72 | fn attr_struct(&self) -> TokenStream { 73 | let mut body = TokenStream::new(); 74 | for (attr_name, attr_type, _) in self.attrs() { 75 | body.extend(quote!( pub #attr_name: Option<#attr_type>, )); 76 | } 77 | 78 | let attr_type_name = self.attr_type_name(); 79 | quote!( 80 | pub struct #attr_type_name { 81 | #body 82 | } 83 | ) 84 | } 85 | 86 | fn struct_(&self) -> TokenStream { 87 | let elem_name = self.elem_name(); 88 | let attr_type_name = self.attr_type_name(); 89 | 90 | let mut body = TokenStream::new(); 91 | 92 | for (child_name, child_type, _) in self.req_children() { 93 | body.extend(quote!( pub #child_name: Box<#child_type>, )); 94 | } 95 | 96 | if let Some(child_constraint) = &self.opt_children { 97 | let child_constraint = child_constraint.clone(); 98 | body.extend(quote!(pub children: Vec>>,)); 99 | } 100 | 101 | quote!( 102 | pub struct #elem_name where T: crate::OutputType + Send { 103 | pub attrs: #attr_type_name, 104 | pub data_attributes: Vec<(&'static str, String)>, 105 | pub aria_attributes: Vec<(&'static str, String)>, 106 | pub events: T::Events, 107 | #body 108 | } 109 | ) 110 | } 111 | 112 | fn impl_(&self) -> TokenStream { 113 | let elem_name = self.elem_name(); 114 | let attr_type_name = self.attr_type_name(); 115 | 116 | let mut args = TokenStream::new(); 117 | for (child_name, child_type, _) in self.req_children() { 118 | args.extend(quote!( #child_name: Box<#child_type>, )); 119 | } 120 | 121 | let mut attrs = TokenStream::new(); 122 | for (attr_name, _, _) in self.attrs() { 123 | attrs.extend(quote!( #attr_name: None, )); 124 | } 125 | 126 | let mut body = TokenStream::new(); 127 | body.extend(quote!( 128 | attrs: #attr_type_name { #attrs }, 129 | )); 130 | body.extend(quote!(data_attributes: Vec::new(),)); 131 | body.extend(quote!(aria_attributes: Vec::new(),)); 132 | 133 | for (child_name, _, _) in self.req_children() { 134 | body.extend(quote!( #child_name, )); 135 | } 136 | if self.opt_children.is_some() { 137 | body.extend(quote!(children: Vec::new())); 138 | } 139 | 140 | quote!( 141 | impl #elem_name where T: crate::OutputType + Send { 142 | pub fn new(#args) -> Self { 143 | #elem_name { 144 | events: T::Events::default(), 145 | #body 146 | } 147 | } 148 | } 149 | ) 150 | } 151 | 152 | fn impl_vnode(&self) -> TokenStream { 153 | let elem_name = TokenTree::Literal(Literal::string(self.name.to_string().as_str())); 154 | let mut req_children = TokenStream::new(); 155 | for (child_name, _, _) in self.req_children() { 156 | req_children.extend(quote!( 157 | children.push(self.#child_name.vnode()); 158 | )); 159 | } 160 | let mut opt_children = TokenStream::new(); 161 | if self.opt_children.is_some() { 162 | opt_children.extend(quote!(for child in &mut self.children { 163 | children.push(child.vnode()); 164 | })); 165 | } 166 | 167 | let mut push_attrs = TokenStream::new(); 168 | for (attr_name, _, attr_str) in self.attrs() { 169 | push_attrs.extend(quote!( 170 | if let Some(ref value) = self.attrs.#attr_name { 171 | attributes.push((#attr_str, value.to_string())); 172 | } 173 | )); 174 | } 175 | 176 | quote!( 177 | let mut attributes = Vec::new(); 178 | #push_attrs 179 | attributes.extend(self.data_attributes.clone()); 180 | attributes.extend(self.aria_attributes.clone()); 181 | 182 | let mut children = Vec::new(); 183 | #req_children 184 | #opt_children 185 | 186 | crate::dom::VNode::Element(crate::dom::VElement { 187 | name: #elem_name, 188 | attributes, 189 | events: &mut self.events, 190 | children 191 | }) 192 | ) 193 | } 194 | 195 | fn impl_node(&self) -> TokenStream { 196 | let elem_name = self.elem_name(); 197 | let vnode = self.impl_vnode(); 198 | quote!( 199 | impl crate::dom::Node for #elem_name where T: crate::OutputType + Send { 200 | fn vnode(&'_ mut self) -> crate::dom::VNode<'_, T> { 201 | #vnode 202 | } 203 | } 204 | ) 205 | } 206 | 207 | fn impl_element(&self) -> TokenStream { 208 | let name: TokenTree = Literal::string(&self.name.to_string()).into(); 209 | let elem_name = self.elem_name(); 210 | 211 | let attrs: TokenStream = self.attrs().map(|(_, _, name)| quote!( #name, )).collect(); 212 | let reqs: TokenStream = self 213 | .req_children() 214 | .map(|(_, _, name)| quote!( #name, )) 215 | .collect(); 216 | 217 | let mut push_attrs = TokenStream::new(); 218 | for (attr_name, _, attr_str) in self.attrs() { 219 | push_attrs.extend(quote!( 220 | if let Some(ref value) = self.attrs.#attr_name { 221 | out.push((#attr_str, value.to_string())); 222 | } 223 | )); 224 | } 225 | 226 | quote!( 227 | impl crate::dom::Element for #elem_name where T: crate::OutputType + Send { 228 | fn name() -> &'static str { 229 | #name 230 | } 231 | 232 | fn attribute_names() -> &'static [&'static str] { 233 | &[ #attrs ] 234 | } 235 | 236 | fn required_children() -> &'static [&'static str] { 237 | &[ #reqs ] 238 | } 239 | 240 | fn attributes(&self) -> Vec<(&'static str, String)> { 241 | let mut out = Vec::new(); 242 | #push_attrs 243 | for (key, value) in &self.data_attributes { 244 | out.push((key, value.to_string())); 245 | } 246 | 247 | for (key, value) in &self.aria_attributes { 248 | out.push((key, value.to_string())); 249 | } 250 | out 251 | } 252 | } 253 | ) 254 | } 255 | 256 | fn impl_marker_traits(&self) -> TokenStream { 257 | let elem_name = self.elem_name(); 258 | let mut body = TokenStream::new(); 259 | for t in &self.traits { 260 | let name = t.clone(); 261 | body.extend(quote!( 262 | impl #name for #elem_name where T: crate::OutputType + Send {} 263 | )); 264 | } 265 | body 266 | } 267 | 268 | fn impl_into_iter(&self) -> TokenStream { 269 | let elem_name = self.elem_name(); 270 | quote!( 271 | impl IntoIterator for #elem_name where T: crate::OutputType + Send { 272 | type Item = #elem_name; 273 | type IntoIter = std::vec::IntoIter<#elem_name>; 274 | fn into_iter(self) -> Self::IntoIter { 275 | vec![self].into_iter() 276 | } 277 | } 278 | 279 | impl IntoIterator for Box<#elem_name> where T: crate::OutputType + Send { 280 | type Item = Box<#elem_name>; 281 | type IntoIter = std::vec::IntoIter>>; 282 | fn into_iter(self) -> Self::IntoIter { 283 | vec![self].into_iter() 284 | } 285 | } 286 | ) 287 | } 288 | 289 | fn impl_display(&self) -> TokenStream { 290 | let elem_name = self.elem_name(); 291 | let name: TokenTree = Literal::string(&self.name.to_string()).into(); 292 | 293 | let print_opt_children = if self.opt_children.is_some() { 294 | quote!(for child in &self.children { 295 | child.fmt(f)?; 296 | }) 297 | } else { 298 | TokenStream::new() 299 | }; 300 | 301 | let mut print_req_children = TokenStream::new(); 302 | for (child_name, _, _) in self.req_children() { 303 | print_req_children.extend(quote!( 304 | self.#child_name.fmt(f)?; 305 | )); 306 | } 307 | 308 | let print_children = if self.req_children.is_empty() { 309 | if self.opt_children.is_some() { 310 | if !SELF_CLOSING.contains(&elem_name.to_string().as_str()) { 311 | quote!( 312 | write!(f, ">")?; 313 | #print_opt_children 314 | write!(f, "", #name) 315 | ) 316 | } else { 317 | quote!(if self.children.is_empty() { 318 | write!(f, " />") 319 | } else { 320 | write!(f, ">")?; 321 | #print_opt_children 322 | write!(f, "", #name) 323 | }) 324 | } 325 | } else if !SELF_CLOSING.contains(&elem_name.to_string().as_str()) { 326 | quote!(write!(f, ">", #name)) 327 | } else { 328 | quote!(write!(f, "/>")) 329 | } 330 | } else { 331 | quote!( 332 | write!(f, ">")?; 333 | #print_req_children 334 | #print_opt_children 335 | write!(f, "", #name) 336 | ) 337 | }; 338 | 339 | let mut print_attrs = TokenStream::new(); 340 | for (attr_name, _, attr_str) in self.attrs() { 341 | print_attrs.extend(quote!( 342 | if let Some(ref value) = self.attrs.#attr_name { 343 | let value = crate::escape_html_attribute(value.to_string()); 344 | if !value.is_empty() { 345 | write!(f, " {}=\"{}\"", #attr_str, value)?; 346 | } 347 | } 348 | )); 349 | } 350 | 351 | quote!( 352 | impl std::fmt::Display for #elem_name 353 | where 354 | T: crate::OutputType + Send, 355 | { 356 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 357 | write!(f, "<{}", #name)?; 358 | #print_attrs 359 | for (key, value) in &self.data_attributes { 360 | write!(f, " data-{}=\"{}\"", key, 361 | crate::escape_html_attribute(value.to_string()))?; 362 | } 363 | for (key, value) in &self.aria_attributes { 364 | write!(f, " aria-{}=\"{}\"", key, 365 | crate::escape_html_attribute(value.to_string()))?; 366 | } 367 | write!(f, "{}", self.events)?; 368 | #print_children 369 | } 370 | } 371 | ) 372 | } 373 | } 374 | 375 | pub fn expand_declare(input: &[Token]) -> Result, ParseError> { 376 | parser::grammar::DeclarationsParser::new().parse(Lexer::new(input)) 377 | } 378 | -------------------------------------------------------------------------------- /macros/src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::lexer::Token; 2 | use console::style; 3 | use lalrpop_util::ParseError::*; 4 | use proc_macro2::{Ident, TokenStream}; 5 | use quote::{quote, quote_spanned}; 6 | 7 | pub type ParseError = lalrpop_util::ParseError; 8 | 9 | #[derive(Debug)] 10 | pub enum HtmlParseError { 11 | TagMismatch { open: Ident, close: Ident }, 12 | } 13 | 14 | fn pprint_token(token: &str) -> &str { 15 | match token { 16 | "BraceGroupToken" => "code block", 17 | "LiteralToken" => "literal", 18 | "IdentToken" => "identifier", 19 | a => a, 20 | } 21 | } 22 | 23 | fn pprint_tokens(tokens: &[String]) -> String { 24 | let tokens: Vec<&str> = tokens.iter().map(|s| pprint_token(s)).collect(); 25 | if tokens.len() > 1 { 26 | let start = tokens[..tokens.len() - 1].join(", "); 27 | let end = &tokens[tokens.len() - 1]; 28 | format!("{} or {}", start, end) 29 | } else { 30 | tokens[0].to_string() 31 | } 32 | } 33 | 34 | fn is_in_node_position(tokens: &[String]) -> bool { 35 | use std::collections::HashSet; 36 | let input: HashSet<&str> = tokens.iter().map(String::as_str).collect(); 37 | let output: HashSet<&str> = ["\"<\"", "BraceGroupToken", "LiteralToken"] 38 | .iter() 39 | .cloned() 40 | .collect(); 41 | input == output 42 | } 43 | 44 | pub fn parse_error(input: &[Token], error: &ParseError) -> TokenStream { 45 | match error { 46 | InvalidToken { location } => { 47 | let span = input[*location].span(); 48 | quote_spanned! {span=> 49 | compile_error! { "invalid token" } 50 | } 51 | } 52 | UnrecognizedEOF { expected, .. } => { 53 | let msg = format!( 54 | "unexpected end of macro; missing {}", 55 | pprint_tokens(expected) 56 | ); 57 | quote! { 58 | compile_error! { #msg } 59 | } 60 | } 61 | UnrecognizedToken { 62 | token: (_, token, _), 63 | expected, 64 | } => { 65 | let span = token.span(); 66 | let error_msg = format!("expected {}", pprint_tokens(expected)); 67 | let error = quote_spanned! {span=> 68 | compile_error! { #error_msg } 69 | }; 70 | let help = if is_in_node_position(expected) && token.is_ident() { 71 | // special case: you probably meant to quote that text 72 | let help_msg = format!( 73 | "text nodes need to be quoted, eg. {}", 74 | style("

\"Hello Joe!\"

").bold(), 75 | ); 76 | Some(quote_spanned! {span=> 77 | compile_error! { #help_msg } 78 | }) 79 | } else { 80 | None 81 | }; 82 | quote! {{ 83 | #error 84 | #help 85 | }} 86 | } 87 | ExtraToken { 88 | token: (_, token, _), 89 | } => { 90 | let span = token.span(); 91 | quote_spanned! {span=> 92 | compile_error! { "superfluous token" } 93 | } 94 | } 95 | User { 96 | error: HtmlParseError::TagMismatch { open, close }, 97 | } => { 98 | let close_span = close.span(); 99 | let close_msg = format!("expected closing tag '', found ''", open, close); 100 | let close_error = quote_spanned! {close_span=> 101 | compile_error! { #close_msg } 102 | }; 103 | let open_span = open.span(); 104 | let open_error = quote_spanned! {open_span=> 105 | compile_error! { "unclosed tag" } 106 | }; 107 | quote! {{ 108 | #close_error 109 | #open_error 110 | }} 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /macros/src/grammar.lalrpop: -------------------------------------------------------------------------------- 1 | use crate::lexer::{self, Token, to_stream}; 2 | use crate::error::HtmlParseError; 3 | use crate::html::{Node, Element}; 4 | use crate::declare::Declare; 5 | use crate::map::StringyMap; 6 | use proc_macro2::{Delimiter, Ident, Literal, Group, TokenTree}; 7 | use lalrpop_util::ParseError; 8 | use crate::span; 9 | 10 | grammar; 11 | 12 | /// Match a B separated list of zero or more A, return a list of A. 13 | Separated: Vec
= { 14 | B)*> => match e { 15 | None => v, 16 | Some(e) => { 17 | let mut v = v; 18 | v.push(e); 19 | v 20 | } 21 | } 22 | } 23 | 24 | /// Match a B separated list of one or more A, return a list of tokens, including the Bs. 25 | /// Both A and B must resolve to a Token. 26 | SeparatedInc: Vec = { 27 | => { 28 | let mut out = Vec::new(); 29 | for (a, b) in v { 30 | out.push(a); 31 | out.push(b); 32 | } 33 | out.push(e); 34 | out 35 | } 36 | } 37 | 38 | Ident: Ident = IdentToken => { 39 | match <> { 40 | Token::Ident(ident) => ident, 41 | _ => unreachable!() 42 | } 43 | }; 44 | 45 | Literal: Literal = LiteralToken => { 46 | match <> { 47 | Token::Literal(literal) => literal, 48 | _ => unreachable!() 49 | } 50 | }; 51 | 52 | GroupToken = { 53 | BraceGroupToken, 54 | BracketGroupToken, 55 | ParenGroupToken, 56 | }; 57 | 58 | /// A kebab case HTML ident, converted to a snake case ident. 59 | HtmlIdent: Ident = { 60 | "-")*> => { 61 | let mut init = init; 62 | init.push(last); 63 | let (span, name) = init.into_iter().fold((None, String::new()), |(span, name), token| { 64 | ( 65 | match span { 66 | None => Some(token.span().unstable()), 67 | Some(span) => { 68 | #[cfg(can_join_spans)] 69 | { 70 | span.join(token.span().unstable()) 71 | } 72 | #[cfg(not(can_join_spans))] 73 | { 74 | Some(span) 75 | } 76 | } 77 | }, 78 | if name.is_empty() { 79 | name + &token.to_string() 80 | } else { 81 | name + "_" + &token.to_string() 82 | } 83 | ) 84 | }); 85 | Ident::new(&name, span::from_unstable(span.unwrap())) 86 | } 87 | }; 88 | 89 | 90 | 91 | // The HTML macro 92 | 93 | /// An approximation of a Rust expression. 94 | BareExpression: Token = "&"? (IdentToken ":" ":")* SeparatedInc ParenGroupToken? => { 95 | let (reference, left, right, args) = (<>); 96 | let mut out = Vec::new(); 97 | if let Some(reference) = reference { 98 | out.push(reference); 99 | } 100 | for (ident, c1, c2) in left { 101 | out.push(ident); 102 | out.push(c1); 103 | out.push(c2); 104 | } 105 | out.extend(right); 106 | if let Some(args) = args { 107 | out.push(args); 108 | } 109 | Group::new(Delimiter::Brace, to_stream(out)).into() 110 | }; 111 | 112 | AttrValue: Token = { 113 | LiteralToken, 114 | GroupToken, 115 | BareExpression, 116 | }; 117 | 118 | Attr: (Ident, Token) = "=" => (name, value); 119 | 120 | Attrs: StringyMap = Attr* => <>.into(); 121 | 122 | OpeningTag: (Ident, StringyMap) = "<" ">"; 123 | 124 | ClosingTag: Ident = "<" "/" ">"; 125 | 126 | SingleTag: Element = "<" "/" ">" => { 127 | Element { 128 | name, 129 | attributes, 130 | children: Vec::new(), 131 | } 132 | }; 133 | 134 | ParentTag: Element = =>? { 135 | let (name, attributes) = opening; 136 | let closing_name = closing.to_string(); 137 | if closing_name == name.to_string() { 138 | Ok(Element { 139 | name, 140 | attributes, 141 | children, 142 | }) 143 | } else { 144 | Err(ParseError::User { error: HtmlParseError::TagMismatch { 145 | open: name.into(), 146 | close: closing.into(), 147 | }}) 148 | } 149 | }; 150 | 151 | Element = { 152 | SingleTag, 153 | ParentTag, 154 | }; 155 | 156 | TextNode = Literal; 157 | 158 | CodeBlock: Group = BraceGroupToken => match <> { 159 | Token::Group(_, group) => group, 160 | _ => unreachable!() 161 | }; 162 | 163 | Node: Node = { 164 | Element => Node::Element(<>), 165 | TextNode => Node::Text(<>), 166 | CodeBlock => Node::Block(<>), 167 | }; 168 | 169 | pub NodeWithType: (Node, Option>) = { 170 | Node => (<>, None), 171 | ":" => { 172 | let (node, spec) = (<>); 173 | (node, Some(spec)) 174 | }, 175 | }; 176 | 177 | pub NodeWithBump: (Ident, Node) = { 178 | "," , 179 | }; 180 | 181 | 182 | // The declare macro 183 | 184 | TypePath: Vec = { 185 | IdentToken => vec![<>], 186 | TypePath ":" ":" IdentToken => { 187 | let (mut path, c1, c2, last) = (<>); 188 | path.push(c1); 189 | path.push(c2); 190 | path.push(last); 191 | path 192 | } 193 | }; 194 | 195 | Reference: Vec = "&" ("'" IdentToken)? => { 196 | let (amp, lifetime) = (<>); 197 | let mut out = vec![amp]; 198 | if let Some((tick, ident)) = lifetime { 199 | out.push(tick); 200 | out.push(ident); 201 | } 202 | out 203 | }; 204 | 205 | TypeArgs: Vec = { 206 | TypeSpec, 207 | TypeArgs "," TypeSpec => { 208 | let (mut args, comma, last) = (<>); 209 | args.push(comma); 210 | args.extend(last); 211 | args 212 | } 213 | }; 214 | 215 | TypeArgList: Vec = "<" TypeArgs ">" => { 216 | let (left, mut args, right) = (<>); 217 | args.insert(0, left); 218 | args.push(right); 219 | args 220 | }; 221 | 222 | FnReturnType: Vec = "-" ">" TypeSpec => { 223 | let (dash, right, spec) = (<>); 224 | let mut out = vec![dash, right]; 225 | out.extend(spec); 226 | out 227 | }; 228 | 229 | FnArgList: Vec = ParenGroupToken FnReturnType? => { 230 | let (args, rt) = (<>); 231 | let mut out = vec![args]; 232 | if let Some(rt) = rt { 233 | out.extend(rt); 234 | } 235 | out 236 | }; 237 | 238 | TypeArgSpec = { 239 | TypeArgList, 240 | FnArgList, 241 | }; 242 | 243 | TypeSpec: Vec = Reference? TypePath TypeArgSpec? => { 244 | let (reference, path, args) = (<>); 245 | let mut out = Vec::new(); 246 | if let Some(reference) = reference { 247 | out.extend(reference); 248 | } 249 | out.extend(path); 250 | if let Some(args) = args { 251 | out.extend(args); 252 | } 253 | out 254 | }; 255 | 256 | TypeDecl: (Ident, Vec) = ":" ; 257 | 258 | TypeDecls: Vec<(Ident, Vec)> = { 259 | TypeDecl => vec![<>], 260 | "," => { 261 | let mut decls = decls; 262 | decls.push(decl); 263 | decls 264 | }, 265 | }; 266 | 267 | Attributes = "{" ","? "}"; 268 | 269 | TypePathList = "[" > "]"; 270 | 271 | IdentList = "[" > "]"; 272 | 273 | Groups = "in" ; 274 | 275 | Children: (Vec, Option>) = "with" => { 276 | (req.unwrap_or_else(|| Vec::new()), opt) 277 | }; 278 | 279 | Declaration: Declare = ";" => { 280 | let mut decl = Declare::new(name); 281 | if let Some(attrs) = attrs { 282 | for (key, value) in attrs { 283 | decl.attrs.insert(key, to_stream(value)); 284 | } 285 | } 286 | if let Some(groups) = groups { 287 | for group in groups { 288 | decl.traits.push(to_stream(group)); 289 | } 290 | } 291 | if let Some((req_children, opt_children)) = children { 292 | decl.req_children = req_children; 293 | decl.opt_children = opt_children.map(to_stream); 294 | } 295 | decl 296 | }; 297 | 298 | pub Declarations = Declaration*; 299 | 300 | 301 | 302 | extern { 303 | type Location = usize; 304 | type Error = HtmlParseError; 305 | 306 | enum lexer::Token { 307 | "<" => Token::Punct('<', _), 308 | ">" => Token::Punct('>', _), 309 | "/" => Token::Punct('/', _), 310 | "=" => Token::Punct('=', _), 311 | "-" => Token::Punct('-', _), 312 | ":" => Token::Punct(':', _), 313 | "." => Token::Punct('.', _), 314 | "," => Token::Punct(',', _), 315 | "&" => Token::Punct('&', _), 316 | "'" => Token::Punct('\'', _), 317 | ";" => Token::Punct(';', _), 318 | "{" => Token::GroupOpen(Delimiter::Brace, _), 319 | "}" => Token::GroupClose(Delimiter::Brace, _), 320 | "[" => Token::GroupOpen(Delimiter::Bracket, _), 321 | "]" => Token::GroupClose(Delimiter::Bracket, _), 322 | "in" => Token::Keyword(lexer::Keyword::In, _), 323 | "with" => Token::Keyword(lexer::Keyword::With, _), 324 | IdentToken => Token::Ident(_), 325 | LiteralToken => Token::Literal(_), 326 | ParenGroupToken => Token::Group(Delimiter::Parenthesis, _), 327 | BraceGroupToken => Token::Group(Delimiter::Brace, _), 328 | BracketGroupToken => Token::Group(Delimiter::Bracket, _), 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /macros/src/html.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Delimiter, Group, Ident, Literal, Span, TokenStream, TokenTree}; 2 | use quote::{quote, quote_spanned}; 3 | 4 | use crate::config::required_children; 5 | use crate::error::ParseError; 6 | use crate::ident; 7 | use crate::lexer::{to_stream, Lexer, Token}; 8 | use crate::map::StringyMap; 9 | use crate::parser::grammar; 10 | 11 | use std::iter::FromIterator; 12 | 13 | #[derive(Clone)] 14 | pub enum Node { 15 | Element(Element), 16 | Text(Literal), 17 | Block(Group), 18 | } 19 | 20 | impl Node { 21 | pub fn into_token_stream(self, ty: &Option>) -> Result { 22 | match self { 23 | Node::Element(el) => el.into_token_stream(ty), 24 | Node::Text(text) => { 25 | let text = TokenTree::Literal(text); 26 | Ok(quote!(Box::new(axohtml::dom::TextNode::new(#text.to_string())))) 27 | } 28 | Node::Block(group) => { 29 | let span = group.span(); 30 | let error = 31 | "you cannot use a block as a top level element or a required child element"; 32 | Err(quote_spanned! { span=> 33 | compile_error! { #error } 34 | }) 35 | } 36 | } 37 | } 38 | 39 | fn into_child_stream(self, ty: &Option>) -> Result { 40 | match self { 41 | Node::Element(el) => { 42 | let el = el.into_token_stream(ty)?; 43 | Ok(quote!( 44 | element.children.push(#el); 45 | )) 46 | } 47 | tx @ Node::Text(_) => { 48 | let tx = tx.into_token_stream(ty)?; 49 | Ok(quote!( 50 | element.children.push(#tx); 51 | )) 52 | } 53 | Node::Block(group) => { 54 | let group: TokenTree = group.into(); 55 | Ok(quote!( 56 | for child in #group.into_iter() { 57 | element.children.push(child); 58 | } 59 | )) 60 | } 61 | } 62 | } 63 | } 64 | 65 | #[derive(Clone)] 66 | pub struct Element { 67 | pub name: Ident, 68 | pub attributes: StringyMap, 69 | pub children: Vec, 70 | } 71 | 72 | fn extract_data_attrs(attrs: &mut StringyMap) -> StringyMap { 73 | let mut data = StringyMap::new(); 74 | let keys: Vec = attrs.keys().cloned().collect(); 75 | for key in keys { 76 | let key_name = key.to_string(); 77 | if let Some(key_name) = key_name.strip_prefix("data_") { 78 | let value = attrs.remove(&key).unwrap(); 79 | // makes sure if a data attribute has more than one hyphen 80 | // they all get transformed 81 | let key = str::replace(key_name, "_", "-"); 82 | data.insert(key.to_string(), value); 83 | } 84 | } 85 | data 86 | } 87 | 88 | fn extract_event_handlers( 89 | attrs: &mut StringyMap, 90 | ) -> StringyMap { 91 | let mut events = StringyMap::new(); 92 | let keys: Vec = attrs.keys().cloned().collect(); 93 | for key in keys { 94 | let key_name = key.to_string(); 95 | if let Some(event_name) = key_name.strip_prefix("on") { 96 | let value = attrs.remove(&key).unwrap(); 97 | events.insert(ident::new_raw(event_name, key.span()), value); 98 | } 99 | } 100 | events 101 | } 102 | 103 | fn extract_aria_attributes( 104 | attrs: &mut StringyMap, 105 | ) -> StringyMap { 106 | let mut data = StringyMap::new(); 107 | let keys: Vec = attrs.keys().cloned().collect(); 108 | for key in keys { 109 | let key_name = key.to_string(); 110 | if let Some(key_name) = key_name.strip_prefix("aria_") { 111 | let value = attrs.remove(&key).unwrap(); 112 | data.insert(key_name.to_string(), value); 113 | } 114 | } 115 | data 116 | } 117 | 118 | fn process_value(value: &TokenTree) -> TokenStream { 119 | match value { 120 | TokenTree::Group(g) if g.delimiter() == Delimiter::Bracket => { 121 | let content = g.stream(); 122 | quote!( [ #content ] ) 123 | } 124 | TokenTree::Group(g) if g.delimiter() == Delimiter::Parenthesis => { 125 | let content = g.stream(); 126 | quote!( ( #content ) ) 127 | } 128 | v => TokenStream::from_iter(vec![v.clone()]), 129 | } 130 | } 131 | 132 | fn is_string_literal(literal: &Literal) -> bool { 133 | // This is the worst API 134 | literal.to_string().starts_with('"') 135 | } 136 | 137 | #[allow(dead_code)] 138 | fn stringify_ident(ident: &Ident) -> String { 139 | let s = ident.to_string(); 140 | if let Some(raw_s) = s.strip_prefix("r#") { 141 | raw_s.to_string() 142 | } else { 143 | s 144 | } 145 | } 146 | 147 | impl Element { 148 | fn into_token_stream(mut self, ty: &Option>) -> Result { 149 | let name = self.name; 150 | let name_str = name.to_string(); 151 | let typename: TokenTree = Ident::new(&name_str, name.span()).into(); 152 | let req_names = required_children(&name_str); 153 | if req_names.len() > self.children.len() { 154 | let span = name.span(); 155 | let error = format!( 156 | "<{}> requires {} children but there are only {}", 157 | name_str, 158 | req_names.len(), 159 | self.children.len() 160 | ); 161 | return Err(quote_spanned! {span=> 162 | compile_error! { #error } 163 | }); 164 | } 165 | let events = extract_event_handlers(&mut self.attributes); 166 | let data_attrs = extract_data_attrs(&mut self.attributes); 167 | let aria_attrs = extract_aria_attributes(&mut self.attributes); 168 | let attrs = self.attributes.iter().map(|(key, value)| { 169 | ( 170 | key.to_string(), 171 | TokenTree::Ident(ident::new_raw(&key.to_string(), key.span())), 172 | value, 173 | ) 174 | }); 175 | let opt_children = self 176 | .children 177 | .split_off(req_names.len()) 178 | .into_iter() 179 | .map(|node| node.into_child_stream(ty)) 180 | .collect::, TokenStream>>()?; 181 | let req_children = self 182 | .children 183 | .into_iter() 184 | .map(|node| node.into_token_stream(ty)) 185 | .collect::, TokenStream>>()?; 186 | 187 | let mut body = TokenStream::new(); 188 | 189 | for (attr_str, key, value) in attrs { 190 | match value { 191 | TokenTree::Literal(lit) if is_string_literal(lit) => { 192 | let mut eprintln_msg = "ERROR: ".to_owned(); 193 | #[cfg(can_show_location_of_runtime_parse_error)] 194 | { 195 | let span = lit.span(); 196 | eprintln_msg += &format!( 197 | "{}:{}:{}: ", 198 | span.unstable() 199 | .source_file() 200 | .path() 201 | .to_str() 202 | .unwrap_or("unknown"), 203 | span.unstable().start().line, 204 | span.unstable().start().column 205 | ); 206 | } 207 | eprintln_msg += &format!( 208 | "<{} {}={}> failed to parse attribute value: {{}}", 209 | name_str, attr_str, lit, 210 | ); 211 | #[cfg(not(can_show_location_of_runtime_parse_error))] 212 | { 213 | eprintln_msg += "\nERROR: rebuild with nightly to print source location"; 214 | } 215 | 216 | body.extend(quote!( 217 | element.attrs.#key = Some(#lit.parse().unwrap_or_else(|err| { 218 | eprintln!(#eprintln_msg, err); 219 | panic!("failed to parse string literal"); 220 | })); 221 | )); 222 | } 223 | value => { 224 | let value = process_value(value); 225 | body.extend(quote!( 226 | element.attrs.#key = Some(std::convert::TryInto::try_into(#value).unwrap()); 227 | )); 228 | } 229 | } 230 | } 231 | for (key, value) in data_attrs 232 | .iter() 233 | .map(|(k, v)| (TokenTree::from(Literal::string(k)), v.clone())) 234 | { 235 | body.extend(quote!( 236 | element.data_attributes.push((#key, #value.into())); 237 | )); 238 | } 239 | 240 | for (key, value) in aria_attrs 241 | .iter() 242 | .map(|(k, v)| (TokenTree::from(Literal::string(k)), v.clone())) 243 | { 244 | body.extend(quote!( 245 | element.aria_attributes.push((#key, #value.into())); 246 | )); 247 | } 248 | 249 | body.extend(opt_children); 250 | 251 | for (key, value) in events.iter() { 252 | if ty.is_none() { 253 | let mut err = quote_spanned! { key.span() => 254 | compile_error! { "when using event handlers, you must declare the output type inside the html! macro" } 255 | }; 256 | let hint = quote_spanned! { Span::call_site() => 257 | compile_error! { "for example: change html!(
...
) to html!(
...
: String)" } 258 | }; 259 | err.extend(hint); 260 | return Err(err); 261 | } 262 | let key = TokenTree::Ident(key.clone()); 263 | let value = process_value(value); 264 | body.extend(quote!( 265 | element.events.#key = Some(#value.into()); 266 | )); 267 | } 268 | 269 | let mut args = TokenStream::new(); 270 | for arg in req_children { 271 | args.extend(quote!( #arg, )); 272 | } 273 | 274 | let mut type_annotation = TokenStream::new(); 275 | if let Some(ty) = ty { 276 | let type_var = to_stream(ty.clone()); 277 | type_annotation.extend(quote!(: axohtml::elements::#typename<#type_var>)); 278 | } 279 | 280 | Ok(quote!( 281 | { 282 | let mut element #type_annotation = axohtml::elements::#typename::new(#args); 283 | #body 284 | Box::new(element) 285 | } 286 | )) 287 | } 288 | 289 | #[cfg(feature = "dodrio")] 290 | fn into_dodrio_token_stream( 291 | mut self, 292 | bump: &Ident, 293 | is_req_child: bool, 294 | ) -> Result { 295 | let name = self.name; 296 | let name_str = stringify_ident(&name); 297 | let typename: TokenTree = ident::new_raw(&name_str, name.span()).into(); 298 | let tag_name = TokenTree::from(Literal::string(&name_str)); 299 | let req_names = required_children(&name_str); 300 | if req_names.len() > self.children.len() { 301 | let span = name.span(); 302 | let error = format!( 303 | "<{}> requires {} children but there are only {}", 304 | name_str, 305 | req_names.len(), 306 | self.children.len() 307 | ); 308 | return Err(quote_spanned! {span=> 309 | compile_error! { #error } 310 | }); 311 | } 312 | let events = extract_event_handlers(&mut self.attributes); 313 | let data_attrs = extract_data_attrs(&mut self.attributes); 314 | let attrs = self.attributes.iter().map(|(key, value)| { 315 | ( 316 | key.to_string(), 317 | TokenTree::Ident(ident::new_raw(&key.to_string(), key.span())), 318 | value, 319 | ) 320 | }); 321 | let opt_children = self.children.split_off(req_names.len()); 322 | let req_children = self 323 | .children 324 | .into_iter() 325 | .map(|node| node.into_dodrio_token_stream(bump, true)) 326 | .collect::, TokenStream>>()?; 327 | 328 | let mut set_attrs = TokenStream::new(); 329 | 330 | for (attr_str, key, value) in attrs { 331 | match value { 332 | TokenTree::Literal(lit) if is_string_literal(lit) => { 333 | let mut eprintln_msg = "ERROR: ".to_owned(); 334 | #[cfg(can_show_location_of_runtime_parse_error)] 335 | { 336 | let span = lit.span(); 337 | eprintln_msg += &format!( 338 | "{}:{}:{}: ", 339 | span.unstable() 340 | .source_file() 341 | .path() 342 | .to_str() 343 | .unwrap_or("unknown"), 344 | span.unstable().start().line, 345 | span.unstable().start().column 346 | ); 347 | } 348 | eprintln_msg += &format!( 349 | "<{} {}={}> failed to parse attribute value: {{}}", 350 | name_str, attr_str, lit, 351 | ); 352 | #[cfg(not(can_show_location_of_runtime_parse_error))] 353 | { 354 | eprintln_msg += "\nERROR: rebuild with nightly to print source location"; 355 | } 356 | 357 | set_attrs.extend(quote!( 358 | element.attrs.#key = Some(#lit.parse().unwrap_or_else(|err| { 359 | eprintln!(#eprintln_msg, err); 360 | panic!("failed to parse string literal"); 361 | })); 362 | )); 363 | } 364 | value => { 365 | let value = process_value(value); 366 | set_attrs.extend(quote!( 367 | element.attrs.#key = Some(std::convert::TryInto::try_into(#value).unwrap()); 368 | )); 369 | } 370 | } 371 | } 372 | 373 | let attr_max_len = self.attributes.len() + data_attrs.len(); 374 | let mut builder = quote!( 375 | let mut attr_list = dodrio::bumpalo::collections::Vec::with_capacity_in(#attr_max_len, #bump); 376 | ); 377 | 378 | // Build the attributes. 379 | for (key, _) in self.attributes.iter() { 380 | let key_str = stringify_ident(key); 381 | let key = ident::new_raw(&key_str, key.span()); 382 | let key_str = TokenTree::from(Literal::string(&key_str)); 383 | builder.extend(quote!( 384 | let attr_value = dodrio::bumpalo::format!( 385 | in &#bump, "{}", element.attrs.#key.unwrap()); 386 | if !attr_value.is_empty() { 387 | attr_list.push(dodrio::builder::attr(#key_str, attr_value.into_bump_str())); 388 | } 389 | )); 390 | } 391 | for (key, value) in data_attrs 392 | .iter() 393 | .map(|(k, v)| (TokenTree::from(Literal::string(&k)), v.clone())) 394 | { 395 | builder.extend(quote!( 396 | attr_list.push(dodrio::builder::attr( 397 | #key, 398 | dodrio::bumpalo::format!( 399 | in &#bump, "{}", #value 400 | ).into_bump_str() 401 | )); 402 | )); 403 | } 404 | 405 | builder.extend(quote!( 406 | let mut node = dodrio::builder::ElementBuilder::new(#bump, #tag_name) 407 | .attributes(attr_list) 408 | )); 409 | 410 | // Build an array of event listeners. 411 | let mut event_array = TokenStream::new(); 412 | for (key, value) in events.iter() { 413 | let key = TokenTree::from(Literal::string(&stringify_ident(key))); 414 | let value = process_value(value); 415 | event_array.extend(quote!( 416 | dodrio::builder::on(&#bump, #key, #value), 417 | )); 418 | } 419 | builder.extend(quote!( 420 | .listeners([#event_array]); 421 | )); 422 | 423 | // And finally an array of children, or a stream of builder commands 424 | // if we have a group inside the child list. 425 | let mut child_array = TokenStream::new(); 426 | let mut child_builder = TokenStream::new(); 427 | let mut static_children = true; 428 | 429 | // Walk through required children and build them inline. 430 | let mut make_req_children = TokenStream::new(); 431 | let mut arg_list = Vec::new(); 432 | for (index, child) in req_children.into_iter().enumerate() { 433 | let req_child = TokenTree::from(ident::new_raw( 434 | &format!("req_child_{}", index), 435 | Span::call_site(), 436 | )); 437 | let child_node = TokenTree::from(ident::new_raw( 438 | &format!("child_node_{}", index), 439 | Span::call_site(), 440 | )); 441 | make_req_children.extend(quote!( 442 | let (#req_child, #child_node) = #child; 443 | )); 444 | child_array.extend(quote!( 445 | #child_node, 446 | )); 447 | child_builder.extend(quote!( 448 | node = node.child(#child_node); 449 | )); 450 | arg_list.push(req_child); 451 | } 452 | 453 | // Build optional children, test if we have groups. 454 | for child_node in opt_children { 455 | let child = match child_node { 456 | Node::Text(text) => dodrio_text_node(text), 457 | Node::Element(el) => el.into_dodrio_token_stream(bump, false)?, 458 | Node::Block(group) => { 459 | static_children = false; 460 | let group: TokenTree = group.into(); 461 | child_builder.extend(quote!( 462 | for child in #group.into_iter() { 463 | node = node.child(child); 464 | } 465 | )); 466 | continue; 467 | } 468 | }; 469 | child_array.extend(quote!( 470 | #child, 471 | )); 472 | child_builder.extend(quote!( 473 | node = node.child(#child); 474 | )); 475 | } 476 | 477 | if static_children { 478 | builder.extend(quote!( 479 | let node = node.children([#child_array]); 480 | )); 481 | } else { 482 | builder.extend(child_builder); 483 | } 484 | builder.extend(quote!(node.finish())); 485 | 486 | if is_req_child { 487 | builder = quote!( 488 | (element, {#builder}) 489 | ); 490 | } 491 | 492 | let mut args = TokenStream::new(); 493 | for arg in arg_list { 494 | args.extend(quote!( #arg, )); 495 | } 496 | 497 | Ok(quote!( 498 | { 499 | #make_req_children 500 | let mut element: axohtml::elements::#typename = 501 | axohtml::elements::#typename::new(#args); 502 | #set_attrs 503 | #builder 504 | } 505 | )) 506 | } 507 | } 508 | 509 | // FIXME report a decent error when the macro contains multiple top level elements 510 | pub fn expand_html(input: &[Token]) -> Result<(Node, Option>), ParseError> { 511 | grammar::NodeWithTypeParser::new().parse(Lexer::new(input)) 512 | } 513 | 514 | #[allow(dead_code)] 515 | pub fn expand_dodrio(input: &[Token]) -> Result<(Ident, Node), ParseError> { 516 | grammar::NodeWithBumpParser::new().parse(Lexer::new(input)) 517 | } 518 | -------------------------------------------------------------------------------- /macros/src/ident.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, Span, TokenStream, TokenTree}; 2 | 3 | use std::str::FromStr; 4 | 5 | pub fn new_raw(string: &str, span: Span) -> Ident { 6 | // Validate that it is an ident. 7 | let _ = Ident::new(string, span); 8 | 9 | let s = format!("r#{}", string); 10 | let tts = TokenStream::from_str(&s).unwrap(); 11 | let mut ident = match tts.into_iter().next().unwrap() { 12 | TokenTree::Ident(ident) => ident, 13 | _ => unreachable!(), 14 | }; 15 | ident.set_span(span); 16 | ident 17 | } 18 | -------------------------------------------------------------------------------- /macros/src/lexer.rs: -------------------------------------------------------------------------------- 1 | use crate::error::HtmlParseError; 2 | use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Span, TokenStream, TokenTree}; 3 | 4 | use std::iter::FromIterator; 5 | 6 | pub type Spanned = Result<(Loc, Tok, Loc), Error>; 7 | 8 | #[derive(Clone, Debug)] 9 | pub enum Token { 10 | Ident(Ident), 11 | Literal(Literal), 12 | Punct(char, Punct), 13 | Group(Delimiter, Group), 14 | GroupOpen(Delimiter, Span), 15 | GroupClose(Delimiter, Span), 16 | Keyword(Keyword, Ident), 17 | } 18 | 19 | impl Token { 20 | pub fn span(&self) -> Span { 21 | match self { 22 | Token::Ident(ident) => ident.span(), 23 | Token::Literal(literal) => literal.span(), 24 | Token::Punct(_, punct) => punct.span(), 25 | Token::Group(_, group) => group.span(), 26 | Token::GroupOpen(_, span) => *span, 27 | Token::GroupClose(_, span) => *span, 28 | Token::Keyword(_, ident) => ident.span(), 29 | } 30 | } 31 | 32 | pub fn is_ident(&self) -> bool { 33 | matches!(self, Token::Ident(_)) 34 | } 35 | } 36 | 37 | impl From for TokenTree { 38 | fn from(token: Token) -> Self { 39 | match token { 40 | Token::Ident(ident) => TokenTree::Ident(ident), 41 | Token::Literal(literal) => TokenTree::Literal(literal), 42 | Token::Punct(_, punct) => TokenTree::Punct(punct), 43 | Token::Group(_, group) => TokenTree::Group(group), 44 | Token::GroupOpen(_, _) => panic!("Can't convert a GroupOpen token to a TokenTree"), 45 | Token::GroupClose(_, _) => panic!("Can't convert a GroupClose token to a TokenTree"), 46 | Token::Keyword(_, ident) => TokenTree::Ident(ident), 47 | } 48 | } 49 | } 50 | 51 | impl From for TokenStream { 52 | fn from(token: Token) -> Self { 53 | TokenStream::from_iter(vec![TokenTree::from(token)]) 54 | } 55 | } 56 | 57 | impl From for Token { 58 | fn from(ident: Ident) -> Self { 59 | Token::Ident(ident) 60 | } 61 | } 62 | 63 | impl From for Token { 64 | fn from(literal: Literal) -> Self { 65 | Token::Literal(literal) 66 | } 67 | } 68 | 69 | impl From for Token { 70 | fn from(punct: Punct) -> Self { 71 | Token::Punct(punct.as_char(), punct) 72 | } 73 | } 74 | 75 | impl From for Token { 76 | fn from(group: Group) -> Self { 77 | Token::Group(group.delimiter(), group) 78 | } 79 | } 80 | 81 | #[derive(Debug, Clone)] 82 | pub enum Keyword { 83 | In, 84 | With, 85 | } 86 | 87 | pub fn keywordise(tokens: Vec) -> Vec { 88 | tokens 89 | .into_iter() 90 | .map(|token| match token { 91 | Token::Ident(ident) => { 92 | let name = ident.to_string(); 93 | if name == "in" { 94 | Token::Keyword(Keyword::In, ident) 95 | } else if name == "with" { 96 | Token::Keyword(Keyword::With, ident) 97 | } else { 98 | Token::Ident(ident) 99 | } 100 | } 101 | t => t, 102 | }) 103 | .collect() 104 | } 105 | 106 | pub fn to_stream>(tokens: I) -> TokenStream { 107 | let mut stream = TokenStream::new(); 108 | stream.extend(tokens.into_iter().map(TokenTree::from)); 109 | stream 110 | } 111 | 112 | pub fn unroll_stream(stream: TokenStream, deep: bool) -> Vec { 113 | let mut vec = Vec::new(); 114 | for tt in stream { 115 | match tt { 116 | TokenTree::Ident(ident) => vec.push(ident.into()), 117 | TokenTree::Literal(literal) => vec.push(literal.into()), 118 | TokenTree::Punct(punct) => vec.push(punct.into()), 119 | TokenTree::Group(ref group) if deep && group.delimiter() != Delimiter::Parenthesis => { 120 | vec.push(Token::GroupOpen(group.delimiter(), group.span())); 121 | let sub = unroll_stream(group.stream(), deep); 122 | vec.extend(sub); 123 | vec.push(Token::GroupClose(group.delimiter(), group.span())); 124 | } 125 | TokenTree::Group(group) => vec.push(group.into()), 126 | } 127 | } 128 | vec 129 | } 130 | 131 | pub struct Lexer<'a> { 132 | stream: &'a [Token], 133 | pos: usize, 134 | } 135 | 136 | impl<'a> Lexer<'a> { 137 | pub fn new(stream: &'a [Token]) -> Self { 138 | Lexer { stream, pos: 0 } 139 | } 140 | } 141 | 142 | impl<'a> Iterator for Lexer<'a> { 143 | type Item = Spanned; 144 | 145 | fn next(&mut self) -> Option { 146 | match self.stream.get(self.pos) { 147 | None => None, 148 | Some(token) => { 149 | self.pos += 1; 150 | Some(Ok((self.pos - 1, token.clone(), self.pos))) 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "128"] 2 | #![cfg_attr(can_show_location_of_runtime_parse_error, feature(proc_macro_span))] 3 | 4 | extern crate proc_macro; 5 | 6 | use proc_macro::TokenStream; 7 | 8 | mod config; 9 | mod declare; 10 | mod error; 11 | mod html; 12 | mod ident; 13 | mod lexer; 14 | mod map; 15 | mod parser; 16 | mod span; 17 | 18 | /// Construct a DOM tree. 19 | /// 20 | /// See the crate documentation for [`axohtml`][axohtml]. 21 | /// 22 | /// [axohtml]: https://docs.rs/axohtml/ 23 | #[proc_macro] 24 | pub fn html(input: TokenStream) -> TokenStream { 25 | let stream = lexer::unroll_stream(input.into(), false); 26 | let result = html::expand_html(&stream); 27 | TokenStream::from(match result { 28 | Err(err) => error::parse_error(&stream, &err), 29 | Ok((node, ty)) => match node.into_token_stream(&ty) { 30 | Err(err) => err, 31 | Ok(success) => success, 32 | }, 33 | }) 34 | } 35 | 36 | /// Construct a Dodrio node. 37 | /// 38 | /// See the crate documentation for [`axohtml`][axohtml]. 39 | /// 40 | /// [axohtml]: https://docs.rs/axohtml/ 41 | #[cfg(feature = "dodrio")] 42 | #[proc_macro] 43 | pub fn dodrio(input: TokenStream) -> TokenStream { 44 | let stream = lexer::unroll_stream(input.into(), false); 45 | let result = html::expand_dodrio(&stream); 46 | TokenStream::from(match result { 47 | Err(err) => error::parse_error(&stream, &err), 48 | Ok((bump, node)) => match node.into_dodrio_token_stream(&bump, false) { 49 | Err(err) => err, 50 | // Ok(success) => {println!("{}", success); panic!()}, 51 | Ok(success) => success, 52 | }, 53 | }) 54 | } 55 | 56 | /// This macro is used by `axohtml` internally to generate types and 57 | /// implementations for HTML elements. 58 | #[proc_macro] 59 | pub fn declare_elements(input: TokenStream) -> TokenStream { 60 | let stream = lexer::keywordise(lexer::unroll_stream(input.into(), true)); 61 | let result = declare::expand_declare(&stream); 62 | TokenStream::from(match result { 63 | Err(err) => error::parse_error(&stream, &err), 64 | Ok(decls) => { 65 | let mut out = proc_macro2::TokenStream::new(); 66 | for decl in decls { 67 | out.extend(decl.into_token_stream()); 68 | } 69 | out 70 | } 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /macros/src/map.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | #[derive(Clone)] 4 | pub struct StringyMap(BTreeMap); 5 | 6 | impl StringyMap 7 | where 8 | K: ToString, 9 | { 10 | pub fn new() -> Self { 11 | StringyMap(BTreeMap::new()) 12 | } 13 | 14 | pub fn insert(&mut self, k: K, v: V) -> Option { 15 | let s = k.to_string(); 16 | self.0.insert(s, (k, v)).map(|(_, v)| v) 17 | } 18 | 19 | pub fn remove(&mut self, k: &K) -> Option { 20 | let s = k.to_string(); 21 | self.0.remove(&s).map(|(_, v)| v) 22 | } 23 | 24 | pub fn iter(&self) -> impl Iterator { 25 | self.0.values() 26 | } 27 | 28 | pub fn keys(&self) -> impl Iterator { 29 | self.0.values().map(|(k, _)| k) 30 | } 31 | 32 | #[allow(dead_code)] 33 | pub fn len(&self) -> usize { 34 | self.0.len() 35 | } 36 | } 37 | 38 | impl From> for StringyMap 39 | where 40 | OK: Into, 41 | OV: Into, 42 | K: ToString, 43 | { 44 | fn from(vec: Vec<(OK, OV)>) -> Self { 45 | let mut out = Self::new(); 46 | for (key, value) in vec { 47 | out.insert(key.into(), value.into()); 48 | } 49 | out 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /macros/src/parser.rs: -------------------------------------------------------------------------------- 1 | // We REALLY don't want to lint the generated parser code. 2 | #![allow(clippy::all)] 3 | 4 | use lalrpop_util::lalrpop_mod; 5 | 6 | lalrpop_mod!(pub grammar); 7 | -------------------------------------------------------------------------------- /macros/src/span.rs: -------------------------------------------------------------------------------- 1 | pub fn from_unstable(span: proc_macro::Span) -> proc_macro2::Span { 2 | let ident = proc_macro::Ident::new("_", span); 3 | let tt = proc_macro::TokenTree::Ident(ident); 4 | let tts = proc_macro::TokenStream::from(tt); 5 | let tts2 = proc_macro2::TokenStream::from(tts); 6 | tts2.into_iter().next().unwrap().span() 7 | } 8 | -------------------------------------------------------------------------------- /tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axohtml-tests" 3 | version = "0.0.0" 4 | edition = "2021" 5 | authors = ["Bodil Stokke "] 6 | publish = false 7 | 8 | [[bin]] 9 | name = "axohtml-tests" 10 | path = "main.rs" 11 | 12 | [dev-dependencies] 13 | compiletest_rs = { version = "0.7", features = ["stable"] } 14 | axohtml = { path = "../typed-html" } 15 | axohtml-macros = { path = "../macros" } 16 | version_check = "0.9.1" 17 | -------------------------------------------------------------------------------- /tests/cases/expected-token.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_hygiene)] 2 | 3 | extern crate typed_html; 4 | 5 | use typed_html::html; 6 | use typed_html::dom::DOMTree; 7 | 8 | fn main() { 9 | let _: DOMTree = html!{ 10 | <@> 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /tests/cases/expected-token.stderr: -------------------------------------------------------------------------------- 1 | error: expected identifier 2 | --> $DIR/expected-token.rs:10:10 3 | | 4 | 10 | <@> 5 | | ^ 6 | | 7 | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) 8 | 9 | error: aborting due to previous error 10 | 11 | -------------------------------------------------------------------------------- /tests/cases/not-enough-children.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_hygiene)] 2 | 3 | extern crate typed_html; 4 | 5 | use typed_html::html; 6 | use typed_html::dom::DOMTree; 7 | 8 | fn main() { 9 | let _: DOMTree = html!{ 10 | 11 | 12 | 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /tests/cases/not-enough-children.stderr: -------------------------------------------------------------------------------- 1 | error: requires 2 children but there are only 1 2 | --> $DIR/not-enough-children.rs:10:10 3 | | 4 | 10 | 5 | | ^^^^ 6 | | 7 | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) 8 | 9 | error: aborting due to previous error 10 | 11 | -------------------------------------------------------------------------------- /tests/cases/tag-mismatch.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_hygiene)] 2 | 3 | extern crate typed_html; 4 | 5 | use typed_html::html; 6 | use typed_html::dom::DOMTree; 7 | 8 | fn main() { 9 | let _: DOMTree = html!{ 10 | 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /tests/cases/tag-mismatch.stderr: -------------------------------------------------------------------------------- 1 | error: expected closing tag '', found '' 2 | --> $DIR/tag-mismatch.rs:10:17 3 | | 4 | 10 | 5 | | ^^^^ 6 | | 7 | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) 8 | 9 | error: unclosed tag 10 | --> $DIR/tag-mismatch.rs:10:10 11 | | 12 | 10 | 13 | | ^^^^ 14 | | 15 | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) 16 | 17 | error: aborting due to 2 previous errors 18 | 19 | -------------------------------------------------------------------------------- /tests/cases/text-nodes-need-to-be-quoted.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_hygiene)] 2 | 3 | extern crate typed_html; 4 | 5 | use typed_html::html; 6 | use typed_html::dom::DOMTree; 7 | 8 | fn main() { 9 | let _: DOMTree = html!{ 10 | unquoted 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /tests/cases/text-nodes-need-to-be-quoted.stderr: -------------------------------------------------------------------------------- 1 | error: expected "<", code block or literal 2 | --> $DIR/text-nodes-need-to-be-quoted.rs:10:16 3 | | 4 | 10 | unquoted 5 | | ^^^^^^^^ 6 | | 7 | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) 8 | 9 | error: text nodes need to be quoted, eg. 

"Hello Joe!"

 10 | --> $DIR/text-nodes-need-to-be-quoted.rs:10:16 11 | | 12 | 10 | unquoted 13 | | ^^^^^^^^ 14 | | 15 | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) 16 | 17 | error: aborting due to 2 previous errors 18 | 19 | -------------------------------------------------------------------------------- /tests/cases/unexpected-end-of-macro.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_hygiene)] 2 | 3 | extern crate typed_html; 4 | 5 | use typed_html::html; 6 | use typed_html::dom::DOMTree; 7 | 8 | fn main() { 9 | let _: DOMTree = html!{ 10 | 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /tests/cases/unexpected-end-of-macro.stderr: -------------------------------------------------------------------------------- 1 | error: unexpected end of macro; missing "<", code block or literal 2 | --> $DIR/unexpected-end-of-macro.rs:9:30 3 | | 4 | 9 | let _: DOMTree<String> = html!{ 5 | | ______________________________^ 6 | 10 | | <title> 7 | 11 | | }; 8 | | |_____^ 9 | | 10 | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) 11 | 12 | error: aborting due to previous error 13 | 14 | -------------------------------------------------------------------------------- /tests/cases/update-references.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2015 The Rust Project Developers. See the COPYRIGHT 4 | # file at the top-level directory of this distribution and at 5 | # http://rust-lang.org/COPYRIGHT. 6 | # 7 | # Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or 8 | # http://www.apache.org/licenses/LICENSE-2.0> or the MIT license 9 | # <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your 10 | # option. This file may not be copied, modified, or distributed 11 | # except according to those terms. 12 | 13 | # A script to update the references for particular tests. The idea is 14 | # that you do a run, which will generate files in the build directory 15 | # containing the (normalized) actual output of the compiler. This 16 | # script will then copy that output and replace the "expected output" 17 | # files. You can then commit the changes. 18 | # 19 | # If you find yourself manually editing a foo.stderr file, you're 20 | # doing it wrong. 21 | 22 | if [[ "$1" == "--help" || "$1" == "-h" || "$1" == "" || "$2" == "" ]]; then 23 | echo "usage: $0 <build-directory> <relative-path-to-rs-files>" 24 | echo "" 25 | echo "For example:" 26 | echo " $0 ../../../build/x86_64-apple-darwin/test/ui *.rs */*.rs" 27 | fi 28 | 29 | MYDIR=$(dirname $0) 30 | 31 | BUILD_DIR="$1" 32 | shift 33 | 34 | while [[ "$1" != "" ]]; do 35 | STDERR_NAME="${1/%.rs/.stderr}" 36 | STDOUT_NAME="${1/%.rs/.stdout}" 37 | shift 38 | if [ -f $BUILD_DIR/$STDOUT_NAME ] && \ 39 | ! (diff $BUILD_DIR/$STDOUT_NAME $MYDIR/$STDOUT_NAME >& /dev/null); then 40 | echo updating $MYDIR/$STDOUT_NAME 41 | cp $BUILD_DIR/$STDOUT_NAME $MYDIR/$STDOUT_NAME 42 | fi 43 | if [ -f $BUILD_DIR/$STDERR_NAME ] && \ 44 | ! (diff $BUILD_DIR/$STDERR_NAME $MYDIR/$STDERR_NAME >& /dev/null); then 45 | echo updating $MYDIR/$STDERR_NAME 46 | cp $BUILD_DIR/$STDERR_NAME $MYDIR/$STDERR_NAME 47 | fi 48 | done 49 | -------------------------------------------------------------------------------- /tests/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("This crate is not meant to be run, it only serves as a compilation test."); 3 | } 4 | 5 | #[test] 6 | fn ui() { 7 | extern crate version_check; 8 | 9 | if !version_check::is_feature_flaggable().unwrap_or(false) { 10 | return; 11 | } 12 | 13 | extern crate compiletest_rs as compiletest; 14 | 15 | let mut config = compiletest::Config { 16 | mode: compiletest::common::Mode::Ui, 17 | src_base: std::path::PathBuf::from("cases"), 18 | ..Default::default() 19 | }; 20 | 21 | config.link_deps(); 22 | config.clean_rmeta(); 23 | 24 | compiletest::run_tests(&config); 25 | } 26 | -------------------------------------------------------------------------------- /typed-html/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axohtml" 3 | version = "0.5.0" 4 | edition = "2021" 5 | authors = ["Axo Developer Co <hello@axo.dev>", "Bodil Stokke <bodil@bodil.org>"] 6 | license = "MPL-2.0+" 7 | description = "Type checked JSX for Rust" 8 | repository = "https://github.com/axodotdev/axohtml" 9 | documentation = "http://docs.rs/axohtml/" 10 | readme = "../README.md" 11 | categories = ["template-engine", "web-programming"] 12 | keywords = ["jsx", "html"] 13 | 14 | [dependencies] 15 | axohtml-macros = { path = "../macros" } 16 | strum = "0.24" 17 | strum_macros = "0.24" 18 | mime = "0.3.17" 19 | language-tags = "0.3" 20 | htmlescape = "0.3.1" 21 | proc-macro-nested = "0.1.7" 22 | -------------------------------------------------------------------------------- /typed-html/src/dom.rs: -------------------------------------------------------------------------------- 1 | //! DOM and virtual DOM types. 2 | 3 | use std::fmt::Display; 4 | use std::marker::PhantomData; 5 | 6 | use crate::elements::{FlowContent, PhrasingContent}; 7 | use crate::OutputType; 8 | use htmlescape::encode_minimal; 9 | 10 | /// A boxed DOM tree, as returned from the `html!` macro. 11 | /// 12 | /// # Examples 13 | /// 14 | /// ``` 15 | /// # use axohtml::html; 16 | /// # use axohtml::dom::DOMTree; 17 | /// # fn main() { 18 | /// let tree: DOMTree<String> = html!( 19 | /// <div class="hello"> 20 | /// <p>"Hello Joe!"</p> 21 | /// </div> 22 | /// ); 23 | /// let rendered_tree: String = tree.to_string(); 24 | /// # } 25 | /// ``` 26 | pub type DOMTree<T> = Box<dyn Node<T>>; 27 | 28 | /// An untyped representation of an HTML node. 29 | /// 30 | /// This structure is designed to be easily walked in order to render a DOM tree 31 | /// or diff against an existing tree. It's the stringly typed version of 32 | /// [`Node`]. 33 | /// 34 | /// It can be constructed from any ['Node'][Node]: 35 | /// 36 | /// ```no_compile 37 | /// html!( 38 | /// <p>"But how does she "<em>"eat?"</em></p> 39 | /// ).vnode() 40 | /// ``` 41 | pub enum VNode<'a, T: OutputType + 'a> { 42 | Text(&'a str), 43 | UnsafeText(&'a str), 44 | Element(VElement<'a, T>), 45 | } 46 | 47 | /// An untyped representation of an HTML element. 48 | pub struct VElement<'a, T: OutputType + 'a> { 49 | pub name: &'static str, 50 | pub attributes: Vec<(&'static str, String)>, 51 | pub events: &'a mut T::Events, 52 | pub children: Vec<VNode<'a, T>>, 53 | } 54 | 55 | /// Trait for rendering a typed HTML node. 56 | /// 57 | /// All [HTML elements][crate::elements] implement this, in addition to 58 | /// [`TextNode`]. 59 | /// 60 | /// It implements [`Display`] for rendering to strings, and the 61 | /// [`vnode()`][Self::vnode] method can be used to render a virtual DOM structure. 62 | pub trait Node<T: OutputType + Send>: Display + Send { 63 | /// Render the node into a [`VNode`] tree. 64 | fn vnode(&mut self) -> VNode<T>; 65 | } 66 | 67 | impl<T> IntoIterator for Box<dyn Node<T>> 68 | where 69 | T: OutputType + Send, 70 | { 71 | type Item = Box<dyn Node<T>>; 72 | type IntoIter = std::vec::IntoIter<Box<dyn Node<T>>>; 73 | 74 | fn into_iter(self) -> Self::IntoIter { 75 | vec![self].into_iter() 76 | } 77 | } 78 | 79 | /// Trait for querying a typed HTML element. 80 | /// 81 | /// All [HTML elements][crate::elements] implement this. 82 | pub trait Element<T: OutputType + Send>: Node<T> { 83 | /// Get the name of the element. 84 | fn name() -> &'static str; 85 | /// Get a list of the attribute names for this element. 86 | /// 87 | /// This includes only the typed attributes, not any `data-` attributes 88 | /// defined on this particular element instance. 89 | /// 90 | /// This is probably not useful unless you're the `html!` macro. 91 | fn attribute_names() -> &'static [&'static str]; 92 | /// Get a list of the element names of required children for this element. 93 | /// 94 | /// This is probably not useful unless you're the `html!` macro. 95 | fn required_children() -> &'static [&'static str]; 96 | /// Get a list of the defined attribute pairs for this element. 97 | /// 98 | /// This will convert attribute values into strings and return a vector of 99 | /// key/value pairs. 100 | fn attributes(&self) -> Vec<(&'static str, String)>; 101 | } 102 | 103 | /// An HTML text node. 104 | pub struct TextNode<T: OutputType + Send>(String, PhantomData<T>); 105 | 106 | /// Macro for creating text nodes. 107 | /// 108 | /// Returns a boxed text node of type `Box<TextNode>`. 109 | /// 110 | /// These can be created inside the `html!` macro directly by using string 111 | /// literals. This macro is useful for creating text macros inside code blocks. 112 | /// 113 | /// # Examples 114 | /// 115 | /// ```no_compile 116 | /// html!( 117 | /// <p>{ text!("Hello Joe!") }</p> 118 | /// ) 119 | /// ``` 120 | /// 121 | /// ```no_compile 122 | /// html!( 123 | /// <p>{ text!("Hello {}!", "Robert") }</p> 124 | /// ) 125 | /// ``` 126 | #[macro_export] 127 | macro_rules! text { 128 | ($t:expr) => { 129 | Box::new($crate::dom::TextNode::new($t)) 130 | }; 131 | ($format:tt, $($tail:expr),*) => { 132 | Box::new($crate::dom::TextNode::new(format!($format, $($tail),*))) 133 | }; 134 | } 135 | 136 | /// An unsafe HTML text node. 137 | /// This is like TextNode, but no escaping will be performed when this node is displayed. 138 | pub struct UnsafeTextNode<T: OutputType + Send>(String, PhantomData<T>); 139 | 140 | /// Macro for creating unescaped text nodes. 141 | /// 142 | /// Returns a boxed text node of type `Box<UnsafeTextNode>`. 143 | /// 144 | /// This macro is useful for creating text macros inside code blocks that contain HTML 145 | /// that you do not want to be escaped. For example, if some other process renders Markdown 146 | /// to an HTML string and you want embed that HTML string in an axohtml template, 147 | /// you may want to avoid escaping the tags in that HTML string. 148 | /// 149 | /// # Examples 150 | /// 151 | /// ```no_compile 152 | /// html!( 153 | /// <p>{ unsafe_text!("Hello Joe!") }</p> 154 | /// ) 155 | /// ``` 156 | /// 157 | /// ```no_compile 158 | /// html!( 159 | /// <p>{ unsafe_text!("Hello {}!", "Robert") }</p> 160 | /// ) 161 | /// ``` 162 | /// 163 | /// ```no_compile 164 | /// html!( 165 | /// <p>{ unsafe_text!("<div>this text renders unescaped html</div>") }</p> 166 | /// ) 167 | /// ``` 168 | #[macro_export] 169 | macro_rules! unsafe_text { 170 | ($t:expr) => { 171 | Box::new($crate::dom::UnsafeTextNode::new($t)) 172 | }; 173 | ($format:tt, $($tail:tt),*) => { 174 | Box::new($crate::dom::UnsafeTextNode::new(format!($format, $($tail),*))) 175 | }; 176 | } 177 | 178 | impl<T: OutputType + Send> TextNode<T> { 179 | /// Construct a text node. 180 | /// 181 | /// The preferred way to construct a text node is with the [`text!()`][crate::text] 182 | /// macro. 183 | pub fn new<S: Into<String>>(s: S) -> Self { 184 | TextNode(s.into(), PhantomData) 185 | } 186 | } 187 | 188 | impl<T: OutputType + Send> Display for TextNode<T> { 189 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 190 | f.write_str(&encode_minimal(&self.0)) 191 | } 192 | } 193 | 194 | impl<T: OutputType + Send> Node<T> for TextNode<T> { 195 | fn vnode(&'_ mut self) -> VNode<'_, T> { 196 | VNode::Text(&self.0) 197 | } 198 | } 199 | 200 | impl<T: OutputType + Send> IntoIterator for TextNode<T> { 201 | type Item = TextNode<T>; 202 | type IntoIter = std::vec::IntoIter<TextNode<T>>; 203 | 204 | fn into_iter(self) -> Self::IntoIter { 205 | vec![self].into_iter() 206 | } 207 | } 208 | 209 | impl<T: OutputType + Send> IntoIterator for Box<TextNode<T>> { 210 | type Item = Box<TextNode<T>>; 211 | type IntoIter = std::vec::IntoIter<Box<TextNode<T>>>; 212 | 213 | fn into_iter(self) -> Self::IntoIter { 214 | vec![self].into_iter() 215 | } 216 | } 217 | 218 | impl<T: OutputType + Send> FlowContent<T> for TextNode<T> {} 219 | impl<T: OutputType + Send> PhrasingContent<T> for TextNode<T> {} 220 | 221 | impl<T: OutputType + Send> UnsafeTextNode<T> { 222 | /// Construct a unsafe text node. 223 | /// 224 | /// The preferred way to construct a unsafe text node is with the [`unsafe_text!()`][crate::unsafe_text] 225 | /// macro. 226 | pub fn new<S: Into<String>>(s: S) -> Self { 227 | UnsafeTextNode(s.into(), PhantomData) 228 | } 229 | } 230 | 231 | impl<T: OutputType + Send> Display for UnsafeTextNode<T> { 232 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 233 | f.write_str(&self.0) 234 | } 235 | } 236 | 237 | impl<T: OutputType + Send> Node<T> for UnsafeTextNode<T> { 238 | fn vnode(&'_ mut self) -> VNode<'_, T> { 239 | VNode::UnsafeText(&self.0) 240 | } 241 | } 242 | 243 | impl<T: OutputType + Send> IntoIterator for UnsafeTextNode<T> { 244 | type Item = UnsafeTextNode<T>; 245 | type IntoIter = std::vec::IntoIter<UnsafeTextNode<T>>; 246 | 247 | fn into_iter(self) -> Self::IntoIter { 248 | vec![self].into_iter() 249 | } 250 | } 251 | 252 | impl<T: OutputType + Send> IntoIterator for Box<UnsafeTextNode<T>> { 253 | type Item = Box<UnsafeTextNode<T>>; 254 | type IntoIter = std::vec::IntoIter<Box<UnsafeTextNode<T>>>; 255 | 256 | fn into_iter(self) -> Self::IntoIter { 257 | vec![self].into_iter() 258 | } 259 | } 260 | 261 | impl<T: OutputType + Send> FlowContent<T> for UnsafeTextNode<T> {} 262 | impl<T: OutputType + Send> PhrasingContent<T> for UnsafeTextNode<T> {} 263 | -------------------------------------------------------------------------------- /typed-html/src/elements.rs: -------------------------------------------------------------------------------- 1 | //! Types for all standard HTML5 elements. 2 | 3 | #![allow(non_camel_case_types)] 4 | 5 | use crate::dom::{Node, TextNode, UnsafeTextNode}; 6 | use crate::types::*; 7 | use crate::OutputType; 8 | use axohtml_macros::declare_elements; 9 | 10 | // Marker traits for element content groups 11 | 12 | macro_rules! marker_trait { 13 | ($trait:ident) => { 14 | marker_trait!($trait, Node); 15 | }; 16 | 17 | ($trait:ident, $parent:ident) => { 18 | pub trait $trait<T: OutputType + Send>: $parent<T> {} 19 | 20 | impl<T> IntoIterator for Box<dyn $trait<T>> 21 | where 22 | T: OutputType + Send, 23 | { 24 | type Item = Box<dyn $trait<T>>; 25 | type IntoIter = std::vec::IntoIter<Box<dyn $trait<T>>>; 26 | 27 | fn into_iter(self) -> Self::IntoIter { 28 | vec![self].into_iter() 29 | } 30 | } 31 | }; 32 | } 33 | 34 | marker_trait!(HTMLContent); 35 | marker_trait!(MetadataContent); 36 | marker_trait!(FlowContent); 37 | marker_trait!(SectioningContent); 38 | marker_trait!(HeadingContent); 39 | // Phrasing content seems to be entirely a subclass of FlowContent 40 | marker_trait!(PhrasingContent, FlowContent); 41 | marker_trait!(EmbeddedContent); 42 | marker_trait!(InteractiveContent); 43 | marker_trait!(FormContent); 44 | 45 | // Traits for elements that are more picky about their children 46 | marker_trait!(DescriptionListContent); 47 | marker_trait!(HGroupContent); 48 | marker_trait!(MapContent); 49 | marker_trait!(MediaContent); // <audio> and <video> 50 | marker_trait!(SelectContent); 51 | marker_trait!(TableContent); 52 | marker_trait!(TableColumnContent); 53 | 54 | declare_elements! { 55 | html { 56 | xmlns: Uri, 57 | } with [head, body] HTMLContent; 58 | head with [title] MetadataContent; 59 | body with FlowContent; 60 | 61 | // Metadata 62 | base { 63 | href: Uri, 64 | target: Target, 65 | } in [MetadataContent]; 66 | link { 67 | as: Mime, 68 | crossorigin: CrossOrigin, 69 | href: Uri, 70 | hreflang: LanguageTag, 71 | media: String, // FIXME media query 72 | rel: LinkType, 73 | sizes: String, // FIXME 74 | title: String, // FIXME 75 | type: Mime, 76 | } in [MetadataContent]; 77 | meta { 78 | charset: String, // FIXME IANA standard names 79 | content: String, 80 | http_equiv: HTTPEquiv, 81 | name: Metadata, 82 | // non standard, uses https://en.wikipedia.org/wiki/RDFa 83 | property: MetadataProperties, 84 | } in [MetadataContent]; 85 | style { 86 | type: Mime, 87 | media: String, // FIXME media query 88 | nonce: Nonce, 89 | title: String, // FIXME 90 | } in [MetadataContent] with TextNode; 91 | title in [MetadataContent] with TextNode; 92 | 93 | // Flow 94 | a { 95 | download: String, 96 | href: Uri, 97 | hreflang: LanguageTag, 98 | ping: SpacedList<Uri>, 99 | rel: SpacedList<LinkType>, 100 | target: Target, 101 | type: Mime, 102 | } in [FlowContent, PhrasingContent, InteractiveContent] with FlowContent; 103 | abbr in [FlowContent, PhrasingContent] with PhrasingContent; 104 | address in [FlowContent] with FlowContent; 105 | article in [FlowContent, SectioningContent] with FlowContent; 106 | aside in [FlowContent, SectioningContent] with FlowContent; 107 | audio { 108 | autoplay: Bool, 109 | controls: Bool, 110 | crossorigin: CrossOrigin, 111 | loop: Bool, 112 | muted: Bool, 113 | preload: Preload, 114 | src: Uri, 115 | } in [FlowContent, PhrasingContent, EmbeddedContent] with MediaContent; 116 | b in [FlowContent, PhrasingContent] with PhrasingContent; 117 | bdo in [FlowContent, PhrasingContent] with PhrasingContent; 118 | bdi in [FlowContent, PhrasingContent] with PhrasingContent; 119 | blockquote { 120 | cite: Uri, 121 | } in [FlowContent] with FlowContent; 122 | br in [FlowContent, PhrasingContent]; 123 | button { 124 | autofocus: Bool, 125 | disabled: Bool, 126 | form: Id, 127 | formaction: Uri, 128 | formenctype: FormEncodingType, 129 | formmethod: FormMethod, 130 | formnovalidate: Bool, 131 | formtarget: Target, 132 | name: Id, 133 | type: ButtonType, 134 | value: String, 135 | } in [FlowContent, PhrasingContent, InteractiveContent, FormContent] with PhrasingContent; 136 | canvas { 137 | height: usize, 138 | width: usize, 139 | } in [FlowContent, PhrasingContent, EmbeddedContent] with FlowContent; 140 | cite in [FlowContent, PhrasingContent] with PhrasingContent; 141 | code in [FlowContent, PhrasingContent] with PhrasingContent; 142 | data { 143 | value: String, 144 | } in [FlowContent, PhrasingContent] with PhrasingContent; 145 | datalist in [FlowContent, PhrasingContent] with option; 146 | del { 147 | cite: Uri, 148 | datetime: Datetime, 149 | } in [FlowContent, PhrasingContent] with FlowContent; 150 | details { 151 | open: Bool, 152 | } in [FlowContent, SectioningContent, InteractiveContent] with [summary] FlowContent; 153 | dfn in [FlowContent, PhrasingContent] with PhrasingContent; 154 | div in [FlowContent] with FlowContent; 155 | dl in [FlowContent] with DescriptionListContent; 156 | em in [FlowContent, PhrasingContent] with PhrasingContent; 157 | embed { 158 | height: usize, 159 | src: Uri, 160 | type: Mime, 161 | width: usize, 162 | } in [FlowContent, PhrasingContent, EmbeddedContent, InteractiveContent]; 163 | // FIXME the legend attribute should be optional 164 | fieldset in [FlowContent, SectioningContent, FormContent] with [legend] FlowContent; 165 | // FIXME the figcaption attribute should be optional 166 | figure in [FlowContent, SectioningContent] with [figcaption] FlowContent; 167 | footer in [FlowContent] with FlowContent; 168 | form { 169 | accept-charset: SpacedList<CharacterEncoding>, 170 | action: Uri, 171 | autocomplete: OnOff, 172 | enctype: FormEncodingType, 173 | method: FormMethod, 174 | name: Id, 175 | novalidate: Bool, 176 | target: Target, 177 | } in [FlowContent] with FlowContent; 178 | h1 in [FlowContent, HeadingContent, HGroupContent] with PhrasingContent; 179 | h2 in [FlowContent, HeadingContent, HGroupContent] with PhrasingContent; 180 | h3 in [FlowContent, HeadingContent, HGroupContent] with PhrasingContent; 181 | h4 in [FlowContent, HeadingContent, HGroupContent] with PhrasingContent; 182 | h5 in [FlowContent, HeadingContent, HGroupContent] with PhrasingContent; 183 | h6 in [FlowContent, HeadingContent, HGroupContent] with PhrasingContent; 184 | header in [FlowContent] with FlowContent; 185 | hgroup in [FlowContent, HeadingContent] with HGroupContent; 186 | hr in [FlowContent]; 187 | i in [FlowContent, PhrasingContent] with PhrasingContent; 188 | iframe { 189 | allow: FeaturePolicy, 190 | allowfullscreen: Bool, 191 | allowpaymentrequest: Bool, 192 | height: usize, 193 | name: Id, 194 | referrerpolicy: ReferrerPolicy, 195 | sandbox: SpacedSet<Sandbox>, 196 | src: Uri, 197 | srcdoc: Uri, 198 | width: usize, 199 | } in [FlowContent, PhrasingContent, EmbeddedContent, InteractiveContent] with FlowContent; 200 | img { 201 | alt: String, 202 | crossorigin: CrossOrigin, 203 | decoding: ImageDecoding, 204 | height: usize, 205 | ismap: Bool, 206 | sizes: SpacedList<String>, // FIXME it's not really just a string 207 | src: Uri, 208 | srcset: String, // FIXME this is much more complicated 209 | usemap: String, // FIXME should be a fragment starting with '#' 210 | width: usize, 211 | } in [FlowContent, PhrasingContent, EmbeddedContent]; 212 | input { 213 | accept: String, 214 | alt: String, 215 | autocomplete: String, 216 | autofocus: Bool, 217 | capture: String, 218 | checked: Bool, 219 | disabled: Bool, 220 | form: Id, 221 | formaction: Uri, 222 | formenctype: FormEncodingType, 223 | formmethod: FormDialogMethod, 224 | formnovalidate: Bool, 225 | formtarget: Target, 226 | height: isize, 227 | list: Id, 228 | max: String, 229 | maxlength: usize, 230 | min: String, 231 | minlength: usize, 232 | multiple: Bool, 233 | name: Id, 234 | pattern: String, 235 | placeholder: String, 236 | readonly: Bool, 237 | required: Bool, 238 | size: usize, 239 | spellcheck: Bool, 240 | src: Uri, 241 | step: String, 242 | tabindex: usize, 243 | type: InputType, 244 | value: String, 245 | width: isize, 246 | } in [FlowContent, FormContent, PhrasingContent]; 247 | ins { 248 | cite: Uri, 249 | datetime: Datetime, 250 | } in [FlowContent, PhrasingContent] with FlowContent; 251 | kbd in [FlowContent, PhrasingContent] with PhrasingContent; 252 | label { 253 | for: Id, 254 | form: Id, 255 | } in [FlowContent, PhrasingContent, InteractiveContent, FormContent] with PhrasingContent; 256 | main in [FlowContent] with FlowContent; 257 | map { 258 | name: Id, 259 | } in [FlowContent, PhrasingContent] with MapContent; 260 | mark in [FlowContent, PhrasingContent] with PhrasingContent; 261 | // TODO the <math> element 262 | meter { 263 | value: isize, 264 | min: isize, 265 | max: isize, 266 | low: isize, 267 | high: isize, 268 | optimum: isize, 269 | form: Id, 270 | } in [FlowContent, PhrasingContent] with PhrasingContent; 271 | nav in [FlowContent, SectioningContent] with FlowContent; 272 | noscript in [MetadataContent, FlowContent, PhrasingContent] with Node; 273 | object { 274 | data: Uri, 275 | form: Id, 276 | height: usize, 277 | name: Id, 278 | type: Mime, 279 | typemustmatch: Bool, 280 | usemap: String, // TODO should be a fragment starting with '#' 281 | width: usize, 282 | } in [FlowContent, PhrasingContent, EmbeddedContent, InteractiveContent, FormContent] with param; 283 | ol { 284 | reversed: Bool, 285 | start: isize, 286 | type: OrderedListType, 287 | } in [FlowContent] with li; 288 | output { 289 | for: SpacedSet<Id>, 290 | form: Id, 291 | name: Id, 292 | } in [FlowContent, PhrasingContent, FormContent] with PhrasingContent; 293 | p in [FlowContent] with PhrasingContent; 294 | pre in [FlowContent] with PhrasingContent; 295 | progress { 296 | max: f64, 297 | value: f64, 298 | } in [FlowContent, PhrasingContent] with PhrasingContent; 299 | q { 300 | cite: Uri, 301 | } in [FlowContent, PhrasingContent] with PhrasingContent; 302 | ruby in [FlowContent, PhrasingContent] with PhrasingContent; 303 | s in [FlowContent, PhrasingContent] with PhrasingContent; 304 | samp in [FlowContent, PhrasingContent] with PhrasingContent; 305 | script { 306 | async: Bool, 307 | crossorigin: CrossOrigin, 308 | defer: Bool, 309 | integrity: Integrity, 310 | nomodule: Bool, 311 | nonce: Nonce, 312 | src: Uri, 313 | text: String, 314 | type: String, // TODO could be an enum 315 | } in [MetadataContent, FlowContent, PhrasingContent, TableColumnContent, HTMLContent] with UnsafeTextNode; 316 | section in [FlowContent, SectioningContent] with FlowContent; 317 | select { 318 | autocomplete: String, 319 | autofocus: Bool, 320 | disabled: Bool, 321 | form: Id, 322 | multiple: Bool, 323 | name: Id, 324 | required: Bool, 325 | size: usize, 326 | } in [FlowContent, PhrasingContent, InteractiveContent, FormContent] with SelectContent; 327 | small in [FlowContent, PhrasingContent] with PhrasingContent; 328 | span in [FlowContent, PhrasingContent] with PhrasingContent; 329 | strong in [FlowContent, PhrasingContent] with PhrasingContent; 330 | sub in [FlowContent, PhrasingContent] with PhrasingContent; 331 | sup in [FlowContent, PhrasingContent] with PhrasingContent; 332 | table in [FlowContent] with TableContent; 333 | template in [MetadataContent, FlowContent, PhrasingContent, TableColumnContent] with Node; 334 | textarea { 335 | autocomplete: OnOff, 336 | autofocus: Bool, 337 | cols: usize, 338 | disabled: Bool, 339 | form: Id, 340 | maxlength: usize, 341 | minlength: usize, 342 | name: Id, 343 | placeholder: String, 344 | readonly: Bool, 345 | required: Bool, 346 | rows: usize, 347 | spellcheck: BoolOrDefault, 348 | wrap: Wrap, 349 | } in [FlowContent, PhrasingContent, InteractiveContent, FormContent] with TextNode; 350 | time { 351 | datetime: Datetime, 352 | } in [FlowContent, PhrasingContent] with PhrasingContent; 353 | ul in [FlowContent] with li; 354 | var in [FlowContent, PhrasingContent] with PhrasingContent; 355 | video { 356 | autoplay: Bool, 357 | controls: Bool, 358 | crossorigin: CrossOrigin, 359 | height: usize, 360 | loop: Bool, 361 | muted: Bool, 362 | preload: Preload, 363 | playsinline: Bool, 364 | poster: Uri, 365 | src: Uri, 366 | width: usize, 367 | } in [FlowContent, PhrasingContent, EmbeddedContent] with MediaContent; 368 | wbr in [FlowContent, PhrasingContent]; 369 | 370 | // Non-group elements 371 | area { 372 | alt: String, 373 | coords: String, // TODO could perhaps be validated 374 | download: Bool, 375 | href: Uri, 376 | hreflang: LanguageTag, 377 | ping: SpacedList<Uri>, 378 | rel: SpacedSet<LinkType>, 379 | shape: AreaShape, 380 | target: Target, 381 | } in [MapContent]; 382 | caption in [TableContent] with FlowContent; 383 | col { 384 | span: usize, 385 | }; 386 | colgroup { 387 | span: usize, 388 | } in [TableContent] with col; 389 | dd in [DescriptionListContent] with FlowContent; 390 | dt in [DescriptionListContent] with FlowContent; 391 | figcaption with FlowContent; 392 | legend with PhrasingContent; 393 | li { 394 | value: isize, 395 | } with FlowContent; 396 | option { 397 | disabled: Bool, 398 | label: String, 399 | selected: Bool, 400 | value: String, 401 | } in [SelectContent] with TextNode; 402 | optgroup { 403 | disabled: Bool, 404 | label: String, 405 | } in [SelectContent] with option; 406 | param { 407 | name: String, 408 | value: String, 409 | }; 410 | source { 411 | src: Uri, 412 | type: Mime, 413 | } in [MediaContent]; 414 | summary with PhrasingContent; 415 | tbody in [TableContent] with tr; 416 | td { 417 | colspan: usize, 418 | headers: SpacedSet<Id>, 419 | rowspan: usize, 420 | } in [TableColumnContent] with FlowContent; 421 | tfoot in [TableContent] with tr; 422 | th { 423 | abbr: String, 424 | colspan: usize, 425 | headers: SpacedSet<Id>, 426 | rowspan: usize, 427 | scope: TableHeaderScope, 428 | } in [TableColumnContent] with FlowContent; 429 | thead in [TableContent] with tr; 430 | tr in [TableContent] with TableColumnContent; 431 | track { 432 | default: Bool, 433 | kind: VideoKind, 434 | label: String, 435 | src: Uri, 436 | srclang: LanguageTag, 437 | } in [MediaContent]; 438 | 439 | // Don't @ me 440 | blink in [FlowContent, PhrasingContent] with PhrasingContent; 441 | marquee { 442 | behavior: String, // FIXME enum 443 | bgcolor: String, // FIXME colour 444 | direction: String, // FIXME direction enum 445 | height: String, // FIXME size 446 | hspace: String, // FIXME size 447 | loop: isize, 448 | scrollamount: usize, 449 | scrolldelay: usize, 450 | truespeed: Bool, 451 | vspace: String, // FIXME size 452 | width: String, // FIXME size 453 | } in [FlowContent, PhrasingContent] with PhrasingContent; 454 | } 455 | 456 | #[test] 457 | fn test_data_attributes() { 458 | use crate as axohtml; 459 | use crate::{dom::DOMTree, html}; 460 | 461 | let frag: DOMTree<String> = html!(<div data-id="1234">"Boo!"</div>); 462 | 463 | assert_eq!("<div data-id=\"1234\">Boo!</div>", frag.to_string()); 464 | } 465 | #[test] 466 | fn test_meta_tags() { 467 | use crate as axohtml; 468 | use crate::{dom::DOMTree, html}; 469 | 470 | let frag: DOMTree<String> = html!(<meta property="og:url" content="http://example.com"/> 471 | ); 472 | 473 | assert_eq!( 474 | "<meta content=\"http://example.com\" property=\"og:url\"/>", 475 | frag.to_string() 476 | ); 477 | } 478 | 479 | #[test] 480 | fn test_aria() { 481 | use crate as axohtml; 482 | use crate::{dom::DOMTree, html}; 483 | 484 | let frag: DOMTree<String> = html!(<div aria_hidden="true" aria_label="hello" /> 485 | ); 486 | 487 | assert_eq!( 488 | "<div aria-hidden=\"true\" aria-label=\"hello\"></div>", 489 | frag.to_string() 490 | ); 491 | } 492 | 493 | #[test] 494 | fn test_js() { 495 | use crate as axohtml; 496 | use crate::{dom::DOMTree, html, unsafe_text}; 497 | 498 | let frag: DOMTree<String> = html!(<script>{unsafe_text!("console.log('{}')", "sup")}</script>); 499 | 500 | assert_eq!("<script>console.log('sup')</script>", frag.to_string()); 501 | } 502 | 503 | #[test] 504 | fn test_twitter_cards() { 505 | use crate as axohtml; 506 | use crate::{dom::DOMTree, html}; 507 | 508 | let frag: DOMTree<String> = html!(<meta name="twitter:card" content="summary_large_image"/>); 509 | 510 | assert_eq!( 511 | "<meta content=\"summary_large_image\" name=\"twitter:card\"/>", 512 | frag.to_string() 513 | ); 514 | } 515 | -------------------------------------------------------------------------------- /typed-html/src/events.rs: -------------------------------------------------------------------------------- 1 | //! Event handlers. 2 | 3 | use crate::OutputType; 4 | use htmlescape::encode_attribute; 5 | use std::fmt::{Display, Error, Formatter}; 6 | 7 | /// Trait for event handlers. 8 | pub trait EventHandler<T: OutputType + Send, E: Send> { 9 | /// Build a callback function from this event handler. 10 | /// 11 | /// Returns `None` is this event handler can't be used to build a callback 12 | /// function. This is usually the case if the event handler is a string 13 | /// intended for server side rendering. 14 | // fn build(self) -> Option<Box<FnMut(EventType) + 'static>>; 15 | 16 | fn attach(&mut self, target: &mut T::EventTarget) -> T::EventListenerHandle; 17 | 18 | /// Render this event handler as a string. 19 | /// 20 | /// Returns `None` if this event handler cannot be rendered. Normally, the 21 | /// only event handlers that can be rendered are string values intended for 22 | /// server side rendering. 23 | fn render(&self) -> Option<String>; 24 | } 25 | 26 | macro_rules! declare_events_struct { 27 | ($($name:ident,)*) => { 28 | pub struct Events<T> where T: Send { 29 | $( 30 | pub $name: Option<T>, 31 | )* 32 | } 33 | 34 | impl<T: Send> Events<T> { 35 | pub fn iter(&self) -> impl Iterator<Item = (&'static str, &T)> { 36 | let mut vec = Vec::new(); 37 | $( 38 | if let Some(ref value) = self.$name { 39 | vec.push((stringify!($name), value)); 40 | } 41 | )* 42 | vec.into_iter() 43 | } 44 | 45 | pub fn iter_mut(&mut self) -> impl Iterator<Item = (&'static str, &mut T)> { 46 | let mut vec = Vec::new(); 47 | $( 48 | if let Some(ref mut value) = self.$name { 49 | vec.push((stringify!($name), value)); 50 | } 51 | )* 52 | vec.into_iter() 53 | } 54 | } 55 | 56 | impl<T: 'static + Send> IntoIterator for Events<T> { 57 | type Item = (&'static str, T); 58 | type IntoIter = Box<dyn Iterator<Item = Self::Item>>; 59 | 60 | fn into_iter(self) -> Self::IntoIter { 61 | let mut vec = Vec::new(); 62 | $( 63 | if let Some(value) = self.$name { 64 | vec.push((stringify!($name), value)); 65 | } 66 | )* 67 | Box::new(vec.into_iter()) 68 | } 69 | } 70 | 71 | impl<T: Send> Default for Events<T> { 72 | fn default() -> Self { 73 | Events { 74 | $( 75 | $name: None, 76 | )* 77 | } 78 | } 79 | } 80 | 81 | impl<T: Display + Send> Display for Events<T> { 82 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 83 | $( 84 | if let Some(ref value) = self.$name { 85 | let attribute = encode_attribute(&value.to_string()); 86 | write!(f, " on{}=\"{}\"", stringify!($name), attribute)?; 87 | } 88 | )* 89 | Ok(()) 90 | } 91 | } 92 | } 93 | } 94 | 95 | declare_events_struct! { 96 | abort, 97 | autocomplete, 98 | autocompleteerror, 99 | blur, 100 | cancel, 101 | canplay, 102 | canplaythrough, 103 | change, 104 | click, 105 | close, 106 | contextmenu, 107 | cuechange, 108 | dblclick, 109 | drag, 110 | dragend, 111 | dragenter, 112 | dragexit, 113 | dragleave, 114 | dragover, 115 | dragstart, 116 | drop, 117 | durationchange, 118 | emptied, 119 | ended, 120 | error, 121 | focus, 122 | input, 123 | invalid, 124 | keydown, 125 | keypress, 126 | keyup, 127 | load, 128 | loadeddata, 129 | loadedmetadata, 130 | loadstart, 131 | mousedown, 132 | mouseenter, 133 | mouseleave, 134 | mousemove, 135 | mouseout, 136 | mouseover, 137 | mouseup, 138 | mousewheel, 139 | pause, 140 | play, 141 | playing, 142 | progress, 143 | ratechange, 144 | reset, 145 | resize, 146 | scroll, 147 | seeked, 148 | seeking, 149 | select, 150 | show, 151 | sort, 152 | stalled, 153 | submit, 154 | suspend, 155 | timeupdate, 156 | toggle, 157 | volumechange, 158 | waiting, 159 | } 160 | 161 | #[cfg(test)] 162 | mod tests { 163 | use super::*; 164 | 165 | #[test] 166 | fn test_empty_events_iter() { 167 | let events: Events<&str> = Events::default(); 168 | 169 | let mut iter = events.iter(); 170 | assert_eq!(iter.next(), None); 171 | } 172 | 173 | #[test] 174 | fn test_events_iter() { 175 | let events = Events::<&str> { 176 | abort: Some("abort"), 177 | waiting: Some("waiting"), 178 | ..Default::default() 179 | }; 180 | 181 | let mut iter = events.iter(); 182 | assert_eq!(iter.next(), Some(("abort", &"abort"))); 183 | assert_eq!(iter.next(), Some(("waiting", &"waiting"))); 184 | assert_eq!(iter.next(), None); 185 | } 186 | 187 | #[test] 188 | fn test_events_iter_mut() { 189 | let mut events = Events::<&str> { 190 | abort: Some("abort"), 191 | waiting: Some("waiting"), 192 | ..Default::default() 193 | }; 194 | 195 | let mut iter = events.iter_mut(); 196 | assert_eq!(iter.next(), Some(("abort", &mut "abort"))); 197 | assert_eq!(iter.next(), Some(("waiting", &mut "waiting"))); 198 | assert_eq!(iter.next(), None); 199 | } 200 | 201 | #[test] 202 | fn test_events_into_iter() { 203 | let events = Events::<&str> { 204 | abort: Some("abort"), 205 | waiting: Some("waiting"), 206 | ..Default::default() 207 | }; 208 | 209 | let mut iter = events.into_iter(); 210 | assert_eq!(iter.next(), Some(("abort", "abort"))); 211 | assert_eq!(iter.next(), Some(("waiting", "waiting"))); 212 | assert_eq!(iter.next(), None); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /typed-html/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "128"] 2 | //! This crate provides the `html!` macro for building fully type checked HTML 3 | //! documents inside your Rust code using roughly [JSX] compatible syntax. 4 | //! 5 | //! This crate is a fork of the great [Bodil Stokke's] [typed-html] crate. Opted 6 | //! for a fork instead of maintainership because not currently intending to use or 7 | //! maintain the Wasm compatibility (for now). 8 | //! 9 | //! [Bodil Stokke's]: https://github.com/bodil 10 | //! [typed-html]: https://github.com/bodil/typed-html 11 | //! 12 | //! # Quick Preview 13 | //! 14 | //! ``` 15 | //! # #![recursion_limit = "128"] 16 | //! # use axohtml::html; 17 | //! # use axohtml::dom::{DOMTree, VNode}; 18 | //! # use axohtml::types::Metadata; 19 | //! # fn main() { 20 | //! let mut doc: DOMTree<String> = html!( 21 | //! <html> 22 | //! <head> 23 | //! <title>"Hello Axo" 24 | //! 25 | //! 26 | //! 27 | //!

">o_o<"

28 | //!

29 | //! "The tool company for tool companies" 30 | //!

31 | //! { (0..3).map(|_| html!( 32 | //!

33 | //! ">o_o<" 34 | //!

35 | //! )) } 36 | //!

37 | //! "Every company should be a developer experience company." 38 | //!

39 | //! 40 | //! 41 | //! ); 42 | //! let doc_str = doc.to_string(); 43 | //! # } 44 | //! ``` 45 | //! 46 | //! # Syntax 47 | //! 48 | //! This macro largely follows [JSX] syntax, but with some differences: 49 | //! 50 | //! * Text nodes must be quoted, because there's only so much Rust's tokeniser can 51 | //! handle outside string literals. So, instead of `

Hello

`, you need to 52 | //! write `

"Hello"

`. (The parser will throw an error asking you to do this 53 | //! if you forget.) 54 | //! * Element attributes will accept simple Rust expressions, but the parser has 55 | //! its limits, as it's not a full Rust parser. You can use literals, 56 | //! variables, dotted properties, type constructors and single function or 57 | //! method calls. If you use something the parser isn't currently capable of 58 | //! handling, it will complain. You can put braces or parentheses around the 59 | //! expression if the parser doesn't understand 60 | //! it. You can use any Rust code inside a brace or parenthesis block. 61 | //! 62 | //! # Valid HTML5 63 | //! 64 | //! The macro will only accept valid HTML5 tags, with no tags or attributes marked 65 | //! experimental or obsolete. If it won't accept something you want it to accept, we 66 | //! can discuss it over a pull request (experimental tags and attributes, in 67 | //! particular, are mostly omitted just for brevity, and you're welcome to implement 68 | //! them). 69 | //! 70 | //! The structure validation is simplistic by necessity, as it defers to the type 71 | //! system: a few elements will have one or more required children, and any element 72 | //! which accepts children will have a restriction on the type of the children, 73 | //! usually a broad group as defined by the HTML spec. Many elements have 74 | //! restrictions on children of children, or require a particular ordering of 75 | //! optional elements, which isn't currently validated. 76 | //! 77 | //! # Attribute Values 78 | //! 79 | //! Brace blocks in the attribute value position should return the expected type for 80 | //! the attribute. The type checker will complain if you return an unsupported type. 81 | //! You can also use literals or a few simple Rust expressions as attribute values 82 | //! (see the Syntax section above). 83 | //! 84 | //! The `html!` macro will add an [`.into()`][Into::into] call to the value 85 | //! expression, so that you can use any type that has an [`Into
`] trait 86 | //! defined for the actual attribute type `A`. 87 | //! 88 | //! As a special case, if you use a string literal, the macro will instead use the 89 | //! [`FromStr`][std::str::FromStr] trait to try and parse the string literal into the 90 | //! expected type. This is extremely useful for eg. CSS classes, letting you type 91 | //! `class="css-class-1 css-class-2"` instead of going to the trouble of 92 | //! constructing a [`SpacedSet`][types::SpacedSet]. The big caveat for this: 93 | //! currently, the macro is not able to validate the string at compile time, and the 94 | //! conversion will panic at runtime if the string is invalid. 95 | //! 96 | //! ## Example 97 | //! 98 | //! ``` 99 | //! # use std::convert::{TryFrom, TryInto}; 100 | //! # use axohtml::html; 101 | //! # use axohtml::dom::DOMTree; 102 | //! # use axohtml::types::{Class, SpacedSet}; 103 | //! # fn main() -> Result<(), &'static str> { 104 | //! let classList: SpacedSet = ["foo", "bar", "baz"].try_into()?; 105 | //! # let doc: DOMTree = 106 | //! html!( 107 | //!
108 | //!
// parses a string literal 109 | //!
// uses From<[&str, &str, &str]> 110 | //!
// uses a variable in scope 111 | //!
114 | //!
115 | //! ) 116 | //! # ; Ok(()) } 117 | //! ``` 118 | //! 119 | //! # Generated Nodes 120 | //! 121 | //! Brace blocks in the child node position are expected to return an 122 | //! [`IntoIterator`] of [`DOMTree`][dom::DOMTree]s. You can return single 123 | //! elements or text nodes, as they both implement `IntoIterator` for themselves. 124 | //! The macro will consume this iterator at runtime and insert the generated nodes 125 | //! as children in the expected position. 126 | //! 127 | //! ## Example 128 | //! 129 | //! ``` 130 | //! # use axohtml::{html, text}; 131 | //! # use axohtml::dom::DOMTree; 132 | //! # fn main() { 133 | //! # let doc: DOMTree = 134 | //! html!( 135 | //!
    136 | //! { (1..=5).map(|i| html!( 137 | //!
  • { text!("{}", i) }
  • 138 | //! )) } 139 | //!
140 | //! ) 141 | //! # ;} 142 | //! ``` 143 | //! 144 | //! # Rendering 145 | //! 146 | //! You have two options for actually producing something useful from the DOM tree 147 | //! that comes out of the macro. 148 | //! 149 | //! ## Render to a string 150 | //! 151 | //! The DOM tree data structure implements [`Display`], so you can call 152 | //! [`to_string()`][std::string::ToString::to_string] on it to render it to a [`String`]. If you 153 | //! plan to do this, the type of the tree should be [`DOMTree`][dom::DOMTree] to 154 | //! ensure you're not using any event handlers that can't be printed. 155 | //! 156 | //! ``` 157 | //! # use axohtml::html; 158 | //! # use axohtml::dom::DOMTree; 159 | //! # fn main() { 160 | //! let doc: DOMTree = html!( 161 | //!

"Hello Axo"

162 | //! ); 163 | //! let doc_str = doc.to_string(); 164 | //! assert_eq!("

Hello Axo

", doc_str); 165 | //! # } 166 | //! ``` 167 | //! 168 | //! ## Render to a virtual DOM 169 | //! 170 | //! The DOM tree structure also implements a method called `vnode()`, which renders 171 | //! the tree to a tree of [`VNode`][dom::VNode]s, which is a mirror of the generated tree 172 | //! with every attribute value rendered into `String`s. You can walk this virtual 173 | //! DOM tree and pass it on to your favourite virtual DOM system. 174 | //! 175 | //! # License 176 | //! 177 | //! Copyright 2018 Bodil Stokke, 2022 Axo Developer Co. 178 | //! 179 | //! This software is subject to the terms of the Mozilla Public License, v. 2.0. If 180 | //! a copy of the MPL was not distributed with this file, You can obtain one at 181 | //! . 182 | //! 183 | //! [JSX]: https://reactjs.org/docs/introducing-jsx.html 184 | 185 | pub extern crate htmlescape; 186 | 187 | use std::fmt::Display; 188 | 189 | pub use axohtml_macros::html; 190 | 191 | pub mod dom; 192 | pub mod elements; 193 | pub mod events; 194 | pub mod types; 195 | 196 | /// Marker trait for outputs 197 | pub trait OutputType { 198 | /// The type that contains events for this output. 199 | type Events: Default + Display + Send; 200 | /// The type of event targets for this output. 201 | type EventTarget: Send; 202 | /// The type that's returned from attaching an event listener to a target. 203 | type EventListenerHandle: Send; 204 | } 205 | 206 | /// String output 207 | impl OutputType for String { 208 | type Events = events::Events; 209 | type EventTarget = (); 210 | type EventListenerHandle = (); 211 | } 212 | 213 | pub fn escape_html_attribute(html_attr: String) -> String { 214 | // Even though the code is quoting the variables with a double quote, escape all known quoting chars 215 | html_attr 216 | .replace('\"', """) 217 | .replace('\'', "'") 218 | .replace('`', "`") 219 | } 220 | -------------------------------------------------------------------------------- /typed-html/src/types/class.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | use std::convert::TryFrom; 3 | use std::fmt::{Display, Error, Formatter}; 4 | use std::ops::Deref; 5 | use std::str::FromStr; 6 | 7 | use super::Id; 8 | 9 | /// A valid CSS class. 10 | /// 11 | /// A CSS class is a non-empty string that starts with an alphanumeric character 12 | /// and is followed by any number of alphanumeric characters and the 13 | /// `_`, `-` and `.` characters. 14 | /// 15 | /// See also [`Id`]. 16 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] 17 | pub struct Class(String); 18 | 19 | impl Class { 20 | /// Construct a new class name from a string. 21 | /// 22 | /// Panics if the provided string is invalid. 23 | pub fn new>(s: S) -> Self { 24 | let s = s.borrow(); 25 | Self::from_str(s).unwrap_or_else(|err| { 26 | panic!( 27 | "axohtml::types::Class: {:?} is not a valid class name: {}", 28 | s, err 29 | ) 30 | }) 31 | } 32 | } 33 | 34 | impl FromStr for Class { 35 | type Err = &'static str; 36 | fn from_str(s: &str) -> Result { 37 | let mut chars = s.chars(); 38 | match chars.next() { 39 | None => return Err("class name cannot be empty"), 40 | Some(c) if !c.is_alphabetic() => { 41 | return Err("class name must start with an alphabetic character") 42 | } 43 | _ => (), 44 | } 45 | for c in chars { 46 | if !c.is_alphanumeric() && c != '_' && c != '-' && c != '.' { 47 | return Err("class name can only contain alphanumerics, dash, dot and underscore"); 48 | } 49 | } 50 | Ok(Class(s.to_string())) 51 | } 52 | } 53 | 54 | impl From for Class { 55 | fn from(id: Id) -> Self { 56 | Class(id.to_string()) 57 | } 58 | } 59 | 60 | impl<'a> TryFrom<&'a str> for Class { 61 | type Error = &'static str; 62 | fn try_from(str: &'a str) -> Result { 63 | Class::from_str(str) 64 | } 65 | } 66 | 67 | impl Display for Class { 68 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 69 | Display::fmt(&self.0, f) 70 | } 71 | } 72 | 73 | impl Deref for Class { 74 | type Target = String; 75 | fn deref(&self) -> &Self::Target { 76 | &self.0 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /typed-html/src/types/id.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | use std::convert::TryFrom; 3 | use std::fmt::{Display, Error, Formatter}; 4 | use std::ops::Deref; 5 | use std::str::FromStr; 6 | 7 | use super::Class; 8 | 9 | /// A valid HTML ID. 10 | /// 11 | /// An ID is a non-empty string that starts with an alphanumeric character 12 | /// and is followed by any number of alphanumeric characters and the 13 | /// `_`, `-` and `.` characters. 14 | /// 15 | /// See also [`Class`]. 16 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] 17 | pub struct Id(String); 18 | 19 | impl Id { 20 | /// Construct a new ID from a string. 21 | /// 22 | /// Panics if the provided string is invalid. 23 | pub fn new>(id: S) -> Self { 24 | let id = id.borrow(); 25 | Self::from_str(id) 26 | .unwrap_or_else(|err| panic!("axohtml::types::Id: {:?} is not a valid ID: {}", id, err)) 27 | } 28 | } 29 | 30 | impl FromStr for Id { 31 | type Err = &'static str; 32 | fn from_str(id: &str) -> Result { 33 | let mut chars = id.chars(); 34 | match chars.next() { 35 | None => return Err("ID cannot be empty"), 36 | Some(c) if !c.is_alphabetic() => { 37 | return Err("ID must start with an alphabetic character") 38 | } 39 | _ => (), 40 | } 41 | for c in chars { 42 | if !c.is_alphanumeric() && c != '_' && c != '-' && c != '.' { 43 | return Err("ID can only contain alphanumerics, dash, dot and underscore"); 44 | } 45 | } 46 | Ok(Id(id.to_string())) 47 | } 48 | } 49 | 50 | impl<'a> TryFrom<&'a str> for Id { 51 | type Error = &'static str; 52 | fn try_from(str: &'a str) -> Result { 53 | Id::from_str(str) 54 | } 55 | } 56 | 57 | impl From for Id { 58 | fn from(c: Class) -> Self { 59 | Id(c.to_string()) 60 | } 61 | } 62 | 63 | impl<'a> From<&'a Class> for Id { 64 | fn from(c: &'a Class) -> Self { 65 | Id(c.to_string()) 66 | } 67 | } 68 | 69 | impl Display for Id { 70 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 71 | Display::fmt(&self.0, f) 72 | } 73 | } 74 | 75 | impl Deref for Id { 76 | type Target = String; 77 | fn deref(&self) -> &Self::Target { 78 | &self.0 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /typed-html/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | //! Types for attribute values. 2 | 3 | use strum_macros::*; 4 | 5 | mod class; 6 | pub use self::class::Class; 7 | 8 | mod id; 9 | pub use self::id::Id; 10 | 11 | mod spacedlist; 12 | pub use self::spacedlist::SpacedList; 13 | 14 | mod spacedset; 15 | pub use self::spacedset::SpacedSet; 16 | 17 | pub type ClassList = SpacedSet; 18 | 19 | pub use language_tags::LanguageTag; 20 | pub use mime::Mime; 21 | 22 | // FIXME these all need validating types 23 | pub type Uri = String; 24 | pub type CharacterEncoding = String; 25 | pub type Datetime = String; 26 | pub type FeaturePolicy = String; 27 | pub type Integrity = String; 28 | pub type Nonce = String; 29 | pub type Target = String; 30 | 31 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 32 | pub enum AreaShape { 33 | #[strum(to_string = "rect")] 34 | Rectangle, 35 | #[strum(to_string = "circle")] 36 | Circle, 37 | #[strum(to_string = "poly")] 38 | Polygon, 39 | #[strum(to_string = "default")] 40 | Default, 41 | } 42 | 43 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 44 | pub enum BoolOrDefault { 45 | #[strum(to_string = "true")] 46 | True, 47 | #[strum(to_string = "default")] 48 | Default, 49 | #[strum(to_string = "false")] 50 | False, 51 | } 52 | 53 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 54 | pub enum ButtonType { 55 | #[strum(to_string = "submit")] 56 | Submit, 57 | #[strum(to_string = "reset")] 58 | Reset, 59 | #[strum(to_string = "button")] 60 | Button, 61 | } 62 | 63 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 64 | pub enum Bool { 65 | #[strum(to_string = "true")] 66 | True, 67 | #[strum(to_string = "")] 68 | False, 69 | } 70 | 71 | impl From for Bool { 72 | fn from(v: bool) -> Self { 73 | if v { 74 | Bool::True 75 | } else { 76 | Bool::False 77 | } 78 | } 79 | } 80 | 81 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 82 | pub enum CrossOrigin { 83 | #[strum(to_string = "anonymous")] 84 | Anonymous, 85 | #[strum(to_string = "use-credentials")] 86 | UseCredentials, 87 | } 88 | 89 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 90 | pub enum FormEncodingType { 91 | #[strum(to_string = "application/x-www-form-urlencoded")] 92 | UrlEncoded, 93 | #[strum(to_string = "multipart/form-data")] 94 | FormData, 95 | #[strum(to_string = "text/plain")] 96 | Text, 97 | } 98 | 99 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 100 | pub enum FormMethod { 101 | #[strum(to_string = "post")] 102 | Post, 103 | #[strum(to_string = "get")] 104 | Get, 105 | } 106 | 107 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 108 | pub enum FormDialogMethod { 109 | #[strum(to_string = "post")] 110 | Post, 111 | #[strum(to_string = "get")] 112 | Get, 113 | #[strum(to_string = "dialog")] 114 | Dialog, 115 | } 116 | 117 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 118 | pub enum HTTPEquiv { 119 | #[strum(to_string = "content-security-policy")] 120 | ContentSecurityPolicy, 121 | #[strum(to_string = "Permissions-Policy")] 122 | PermissionsPolicy, 123 | #[strum(to_string = "refresh")] 124 | Refresh, 125 | } 126 | 127 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 128 | pub enum ImageDecoding { 129 | #[strum(to_string = "sync")] 130 | Sync, 131 | #[strum(to_string = "async")] 132 | Async, 133 | #[strum(to_string = "auto")] 134 | Auto, 135 | } 136 | 137 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 138 | pub enum InputType { 139 | #[strum(to_string = "button")] 140 | Button, 141 | #[strum(to_string = "checkbox")] 142 | Checkbox, 143 | #[strum(to_string = "color")] 144 | Color, 145 | #[strum(to_string = "date")] 146 | Date, 147 | #[strum(to_string = "datetime-local")] 148 | DatetimeLocal, 149 | #[strum(to_string = "email")] 150 | Email, 151 | #[strum(to_string = "file")] 152 | File, 153 | #[strum(to_string = "hidden")] 154 | Hidden, 155 | #[strum(to_string = "image")] 156 | Image, 157 | #[strum(to_string = "month")] 158 | Month, 159 | #[strum(to_string = "number")] 160 | Number, 161 | #[strum(to_string = "password")] 162 | Password, 163 | #[strum(to_string = "radio")] 164 | Radio, 165 | #[strum(to_string = "range")] 166 | Range, 167 | #[strum(to_string = "reset")] 168 | Reset, 169 | #[strum(to_string = "search")] 170 | Search, 171 | #[strum(to_string = "submit")] 172 | Submit, 173 | #[strum(to_string = "tel")] 174 | Tel, 175 | #[strum(to_string = "text")] 176 | Text, 177 | #[strum(to_string = "time")] 178 | Time, 179 | #[strum(to_string = "url")] 180 | Url, 181 | #[strum(to_string = "week")] 182 | Week, 183 | } 184 | 185 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 186 | pub enum LinkType { 187 | #[strum(to_string = "alternate")] 188 | Alternate, 189 | #[strum(to_string = "author")] 190 | Author, 191 | #[strum(to_string = "bookmark")] 192 | Bookmark, 193 | #[strum(to_string = "canonical")] 194 | Canonical, 195 | #[strum(to_string = "external")] 196 | External, 197 | #[strum(to_string = "help")] 198 | Help, 199 | #[strum(to_string = "icon")] 200 | Icon, 201 | #[strum(to_string = "license")] 202 | License, 203 | #[strum(to_string = "manifest")] 204 | Manifest, 205 | #[strum(to_string = "modulepreload")] 206 | ModulePreload, 207 | #[strum(to_string = "next")] 208 | Next, 209 | #[strum(to_string = "nofollow")] 210 | NoFollow, 211 | #[strum(to_string = "noopener")] 212 | NoOpener, 213 | #[strum(to_string = "noreferrer")] 214 | NoReferrer, 215 | #[strum(to_string = "pingback")] 216 | PingBack, 217 | #[strum(to_string = "prefetch")] 218 | Prefetch, 219 | #[strum(to_string = "preload")] 220 | Preload, 221 | #[strum(to_string = "prev")] 222 | Prev, 223 | #[strum(to_string = "search")] 224 | Search, 225 | #[strum(to_string = "shortlink")] 226 | ShortLink, 227 | #[strum(to_string = "stylesheet")] 228 | StyleSheet, 229 | #[strum(to_string = "tag")] 230 | Tag, 231 | } 232 | 233 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 234 | pub enum Metadata { 235 | #[strum(to_string = "application-name")] 236 | ApplicationName, 237 | #[strum(to_string = "author")] 238 | Author, 239 | #[strum(to_string = "description")] 240 | Description, 241 | #[strum(to_string = "generator")] 242 | Generator, 243 | #[strum(to_string = "keywords")] 244 | Keywords, 245 | #[strum(to_string = "referrer")] 246 | Referrer, 247 | #[strum(to_string = "creator")] 248 | Creator, 249 | #[strum(to_string = "googlebot")] 250 | Googlebot, 251 | #[strum(to_string = "publisher")] 252 | Publisher, 253 | #[strum(to_string = "robots")] 254 | Robots, 255 | #[strum(to_string = "viewport")] 256 | Viewport, 257 | // Twitter Social meta card tags -> https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/markup 258 | #[strum(to_string = "twitter:card")] 259 | TwitterCard, 260 | #[strum(to_string = "twitter:site:id")] 261 | TwitterSiteId, 262 | #[strum(to_string = "twitter:site")] 263 | TwitterSite, 264 | #[strum(to_string = "twitter:creator:id")] 265 | TwitterCreatorId, 266 | #[strum(to_string = "twitter:creator")] 267 | TwitterCreator, 268 | #[strum(to_string = "twitter:description")] 269 | TwitterDescription, 270 | #[strum(to_string = "twitter:title")] 271 | TwitterTitle, 272 | #[strum(to_string = "twitter:image:alt")] 273 | TwitterImageAlt, 274 | #[strum(to_string = "twitter:image")] 275 | TwitterImage, 276 | #[strum(to_string = "twitter:player:width")] 277 | TwitterPlayerWidth, 278 | #[strum(to_string = "twitter:player:height")] 279 | TwitterPlayerHeight, 280 | #[strum(to_string = "twitter:player:stream")] 281 | TwitterPlayerStream, 282 | #[strum(to_string = "twitter:player")] 283 | TwitterPlayer, 284 | #[strum(to_string = "twitter:app:name:phone")] 285 | TwitterAppNamePhone, 286 | #[strum(to_string = "twitter:app:name:iphone")] 287 | TwitterAppNameIphone, 288 | #[strum(to_string = "twitter:app:id:iphone")] 289 | TwitterAppIdIphone, 290 | #[strum(to_string = "twitter:app:url:iphone")] 291 | TwitterAppUrlIphone, 292 | #[strum(to_string = "twitter:app:name:ipad")] 293 | TwitterAppNameIpad, 294 | #[strum(to_string = "twitter:app:id:ipad")] 295 | TwitterAppIdIpad, 296 | #[strum(to_string = "twitter:app:url:ipad")] 297 | TwitterAppUrlIpad, 298 | #[strum(to_string = "twitter:app:name:googleplay")] 299 | TwitterAppNameGooglePlay, 300 | #[strum(to_string = "twitter:app:id:googleplay")] 301 | TwitterAppIdGooglePlay, 302 | #[strum(to_string = "twitter:app:url:googleplay")] 303 | TwitterAppUrlGooglePlay, 304 | } 305 | 306 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 307 | pub enum MetadataProperties { 308 | #[strum(to_string = "og:title")] 309 | Title, 310 | #[strum(to_string = "og:type")] 311 | Type, 312 | #[strum(to_string = "og:image")] 313 | Image, 314 | #[strum(to_string = "og:image:alt")] 315 | ImageAlt, 316 | #[strum(to_string = "og:image:type")] 317 | ImageType, 318 | #[strum(to_string = "og:image:width")] 319 | ImageWidth, 320 | #[strum(to_string = "og:image:height")] 321 | ImageHeight, 322 | #[strum(to_string = "og:image:secure_url")] 323 | ImageSecureUrl, 324 | #[strum(to_string = "og:url")] 325 | Homepage, 326 | #[strum(to_string = "og:audio")] 327 | Audio, 328 | #[strum(to_string = "og:audio:type")] 329 | AudioType, 330 | #[strum(to_string = "og:audio:secure_url")] 331 | AudioSecureUrl, 332 | #[strum(to_string = "og:description")] 333 | Description, 334 | #[strum(to_string = "og:determiner")] 335 | Determiner, 336 | #[strum(to_string = "og:locale")] 337 | Locale, 338 | #[strum(to_string = "og:site_name")] 339 | ParentSiteName, 340 | #[strum(to_string = "og:video")] 341 | Video, 342 | #[strum(to_string = "og:video:alt")] 343 | VideoAlt, 344 | #[strum(to_string = "og:video:type")] 345 | VideoType, 346 | #[strum(to_string = "og:video:width")] 347 | VideoWidth, 348 | #[strum(to_string = "og:video:height")] 349 | VideoHeight, 350 | #[strum(to_string = "og:video:secure_url")] 351 | VideoSecureUrl, 352 | #[strum(to_string = "og:locale:alternate")] 353 | ExtraLocales, 354 | } 355 | 356 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 357 | pub enum OnOff { 358 | #[strum(to_string = "on")] 359 | On, 360 | #[strum(to_string = "off")] 361 | Off, 362 | } 363 | 364 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 365 | pub enum OrderedListType { 366 | #[strum(to_string = "a")] 367 | LowerCaseLetters, 368 | #[strum(to_string = "A")] 369 | UpperCaseLetters, 370 | #[strum(to_string = "i")] 371 | LowerCaseRomanNumerals, 372 | #[strum(to_string = "I")] 373 | UpperCaseRomanNumerals, 374 | #[strum(to_string = "1")] 375 | Numbers, 376 | } 377 | 378 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 379 | pub enum Preload { 380 | #[strum(to_string = "none")] 381 | None, 382 | #[strum(to_string = "metadata")] 383 | Metadata, 384 | #[strum(to_string = "auto")] 385 | Auto, 386 | } 387 | 388 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 389 | pub enum ReferrerPolicy { 390 | #[strum(to_string = "no-referrer")] 391 | NoReferrer, 392 | #[strum(to_string = "no-referrer-when-downgrade")] 393 | NoReferrerWhenDowngrade, 394 | #[strum(to_string = "origin")] 395 | Origin, 396 | #[strum(to_string = "origin-when-cross-origin")] 397 | OriginWhenCrossOrigin, 398 | #[strum(to_string = "unsafe-url")] 399 | UnsafeUrl, 400 | } 401 | 402 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 403 | pub enum Role { 404 | #[strum(to_string = "any")] 405 | Any, 406 | #[strum(to_string = "alert")] 407 | Alert, 408 | #[strum(to_string = "alertdialog")] 409 | AlertDialog, 410 | #[strum(to_string = "application")] 411 | Application, 412 | #[strum(to_string = "article")] 413 | Article, 414 | #[strum(to_string = "banner")] 415 | Banner, 416 | #[strum(to_string = "checkbox")] 417 | Checkbox, 418 | #[strum(to_string = "cell")] 419 | Cell, 420 | #[strum(to_string = "columnheader")] 421 | ColumnHeader, 422 | #[strum(to_string = "combobox")] 423 | ComboBox, 424 | #[strum(to_string = "complementary")] 425 | Complementary, 426 | #[strum(to_string = "contentinfo")] 427 | ContentInfo, 428 | #[strum(to_string = "definition")] 429 | Definition, 430 | #[strum(to_string = "dialog")] 431 | Dialog, 432 | #[strum(to_string = "directory")] 433 | Directory, 434 | #[strum(to_string = "document")] 435 | Document, 436 | #[strum(to_string = "feed")] 437 | Feed, 438 | #[strum(to_string = "figure")] 439 | Figure, 440 | #[strum(to_string = "form")] 441 | Form, 442 | #[strum(to_string = "grid")] 443 | Grid, 444 | #[strum(to_string = "gridcell")] 445 | GridCell, 446 | #[strum(to_string = "group")] 447 | Group, 448 | #[strum(to_string = "heading")] 449 | Heading, 450 | #[strum(to_string = "img")] 451 | Image, 452 | #[strum(to_string = "link")] 453 | Link, 454 | #[strum(to_string = "list")] 455 | List, 456 | #[strum(to_string = "listbox")] 457 | ListBox, 458 | #[strum(to_string = "listitem")] 459 | ListItem, 460 | #[strum(to_string = "log")] 461 | Log, 462 | #[strum(to_string = "main")] 463 | Main, 464 | #[strum(to_string = "marquee")] 465 | Marquee, 466 | #[strum(to_string = "math")] 467 | Math, 468 | #[strum(to_string = "menu")] 469 | Menu, 470 | #[strum(to_string = "menubar")] 471 | MenuBar, 472 | #[strum(to_string = "menuitem")] 473 | MenuItem, 474 | #[strum(to_string = "menuitemcheckbox")] 475 | MenuItemCheckbox, 476 | #[strum(to_string = "menuitemradio")] 477 | MenuItemRadio, 478 | #[strum(to_string = "navigation")] 479 | Navigation, 480 | #[strum(to_string = "none")] 481 | None, 482 | #[strum(to_string = "note")] 483 | Note, 484 | #[strum(to_string = "option")] 485 | Option, 486 | #[strum(to_string = "presentation")] 487 | Presentation, 488 | #[strum(to_string = "progressbar")] 489 | ProgressBar, 490 | #[strum(to_string = "radio")] 491 | Radio, 492 | #[strum(to_string = "radiogroup")] 493 | RadioGroup, 494 | #[strum(to_string = "region")] 495 | Region, 496 | #[strum(to_string = "row")] 497 | Row, 498 | #[strum(to_string = "rowgroup")] 499 | RowGroup, 500 | #[strum(to_string = "rowheader")] 501 | RowHeader, 502 | #[strum(to_string = "scrollbar")] 503 | ScrollBar, 504 | #[strum(to_string = "search")] 505 | Search, 506 | #[strum(to_string = "searchbox")] 507 | SearchBox, 508 | #[strum(to_string = "separator")] 509 | Separator, 510 | #[strum(to_string = "slider")] 511 | Slider, 512 | #[strum(to_string = "spinbutton")] 513 | SpinButton, 514 | #[strum(to_string = "status")] 515 | Status, 516 | #[strum(to_string = "switch")] 517 | Switch, 518 | #[strum(to_string = "tab")] 519 | Tab, 520 | #[strum(to_string = "table")] 521 | Table, 522 | #[strum(to_string = "tablist")] 523 | TabList, 524 | #[strum(to_string = "tabpanel")] 525 | TabPanel, 526 | #[strum(to_string = "term")] 527 | Term, 528 | #[strum(to_string = "textbox")] 529 | TextBox, 530 | #[strum(to_string = "timer")] 531 | Timer, 532 | #[strum(to_string = "toolbar")] 533 | ToolBar, 534 | #[strum(to_string = "tooltip")] 535 | ToolTip, 536 | #[strum(to_string = "tree")] 537 | Tree, 538 | #[strum(to_string = "treegrid")] 539 | TreeGrid, 540 | } 541 | 542 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 543 | pub enum Sandbox { 544 | #[strum(to_string = "allow-forms")] 545 | AllowForms, 546 | #[strum(to_string = "allow-modals")] 547 | AllowModals, 548 | #[strum(to_string = "allow-orientation-lock")] 549 | AllowOrientationLock, 550 | #[strum(to_string = "allow-pointer-lock")] 551 | AllowPointerLock, 552 | #[strum(to_string = "allow-popups")] 553 | AllowPopups, 554 | #[strum(to_string = "allow-popups-to-escape-sandbox")] 555 | AllowPopupsToEscapeSandbox, 556 | #[strum(to_string = "allow-presentation")] 557 | AllowPresentation, 558 | #[strum(to_string = "allow-same-origin")] 559 | AllowSameOrigin, 560 | #[strum(to_string = "allow-scripts")] 561 | AllowScripts, 562 | #[strum(to_string = "allow-top-navigation")] 563 | AllowTopNavigation, 564 | #[strum(to_string = "allow-top-navigation-by-user-navigation")] 565 | AllowTopNavigationByUserNavigation, 566 | } 567 | 568 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 569 | pub enum TableHeaderScope { 570 | #[strum(to_string = "row")] 571 | Row, 572 | #[strum(to_string = "col")] 573 | Column, 574 | #[strum(to_string = "rowgroup")] 575 | RowGroup, 576 | #[strum(to_string = "colgroup")] 577 | ColGroup, 578 | #[strum(to_string = "auto")] 579 | Auto, 580 | } 581 | 582 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 583 | pub enum TextDirection { 584 | #[strum(to_string = "ltr")] 585 | LeftToRight, 586 | #[strum(to_string = "rtl")] 587 | RightToLeft, 588 | } 589 | 590 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 591 | pub enum VideoKind { 592 | #[strum(to_string = "subtitles")] 593 | Subtitles, 594 | #[strum(to_string = "captions")] 595 | Captions, 596 | #[strum(to_string = "descriptions")] 597 | Descriptions, 598 | #[strum(to_string = "chapters")] 599 | Chapters, 600 | #[strum(to_string = "metadata")] 601 | Metadata, 602 | } 603 | 604 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 605 | pub enum Wrap { 606 | #[strum(to_string = "hard")] 607 | Hard, 608 | #[strum(to_string = "soft")] 609 | Soft, 610 | #[strum(to_string = "off")] 611 | Off, 612 | } 613 | 614 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 615 | pub enum AriaSort { 616 | #[strum(to_string = "ascending")] 617 | Ascending, 618 | #[strum(to_string = "descending")] 619 | Descending, 620 | #[strum(to_string = "none")] 621 | None, 622 | #[strum(to_string = "other")] 623 | Other, 624 | } 625 | 626 | #[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, IntoStaticStr)] 627 | pub enum AriaOrientation { 628 | #[strum(to_string = "horizontal")] 629 | Horizontal, 630 | #[strum(to_string = "vertical")] 631 | Vertical, 632 | } 633 | -------------------------------------------------------------------------------- /typed-html/src/types/spacedlist.rs: -------------------------------------------------------------------------------- 1 | use std::convert::{TryFrom, TryInto}; 2 | use std::fmt::{Debug, Display, Error, Formatter}; 3 | use std::iter::FromIterator; 4 | use std::ops::{Deref, DerefMut}; 5 | use std::str::FromStr; 6 | 7 | /// A space separated list of values. 8 | /// 9 | /// This type represents a list of non-unique values represented as a string of 10 | /// values separated by spaces in HTML attributes. This is rarely used; a 11 | /// [`SpacedSet`][super::SpacedSet] of unique values is much more common. 12 | #[derive(Clone, PartialEq, Eq, Hash)] 13 | pub struct SpacedList
(Vec); 14 | 15 | impl SpacedList { 16 | /// Construct an empty `SpacedList`. 17 | pub fn new() -> Self { 18 | SpacedList(Vec::new()) 19 | } 20 | 21 | /// Add a value to the `SpacedList`, converting it as necessary. 22 | /// 23 | /// Panics if the conversion fails. 24 | pub fn add>(&mut self, value: T) 25 | where 26 | >::Error: Debug, 27 | { 28 | self.0.push(value.try_into().unwrap()) 29 | } 30 | 31 | /// Add a value to the `SpacedList`, converting it as necessary. 32 | /// 33 | /// Returns an error if the conversion fails. 34 | pub fn try_add>(&mut self, value: T) -> Result<(), >::Error> { 35 | self.0.push(value.try_into()?); 36 | Ok(()) 37 | } 38 | } 39 | 40 | impl Default for SpacedList { 41 | fn default() -> Self { 42 | Self::new() 43 | } 44 | } 45 | 46 | impl FromIterator for SpacedList { 47 | fn from_iter(iter: I) -> Self 48 | where 49 | I: IntoIterator, 50 | { 51 | SpacedList(iter.into_iter().collect()) 52 | } 53 | } 54 | 55 | impl<'a, A: 'a + Clone> FromIterator<&'a A> for SpacedList { 56 | fn from_iter(iter: I) -> Self 57 | where 58 | I: IntoIterator, 59 | { 60 | SpacedList(iter.into_iter().cloned().collect()) 61 | } 62 | } 63 | 64 | impl<'a, A> TryFrom<&'a str> for SpacedList 65 | where 66 | A: FromStr, 67 | { 68 | type Error = ::Err; 69 | fn try_from(s: &'a str) -> Result { 70 | s.split_whitespace().map(FromStr::from_str).collect() 71 | } 72 | } 73 | 74 | impl Deref for SpacedList { 75 | type Target = Vec; 76 | fn deref(&self) -> &Self::Target { 77 | &self.0 78 | } 79 | } 80 | 81 | impl DerefMut for SpacedList { 82 | fn deref_mut(&mut self) -> &mut Self::Target { 83 | &mut self.0 84 | } 85 | } 86 | 87 | impl Display for SpacedList { 88 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 89 | let mut it = self.0.iter().peekable(); 90 | while let Some(class) = it.next() { 91 | Display::fmt(class, f)?; 92 | if it.peek().is_some() { 93 | Display::fmt(" ", f)?; 94 | } 95 | } 96 | Ok(()) 97 | } 98 | } 99 | 100 | impl Debug for SpacedList { 101 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 102 | f.debug_list().entries(self.0.iter()).finish() 103 | } 104 | } 105 | 106 | impl<'a, 'b, A> TryFrom<(&'a str, &'b str)> for SpacedList 107 | where 108 | A: FromStr, 109 | { 110 | type Error = ::Err; 111 | fn try_from(s: (&str, &str)) -> Result { 112 | let mut list = Self::new(); 113 | list.push(FromStr::from_str(s.0)?); 114 | list.push(FromStr::from_str(s.1)?); 115 | Ok(list) 116 | } 117 | } 118 | 119 | impl<'a, 'b, 'c, A> TryFrom<(&'a str, &'b str, &'c str)> for SpacedList 120 | where 121 | A: FromStr, 122 | { 123 | type Error = ::Err; 124 | fn try_from(s: (&str, &str, &str)) -> Result { 125 | let mut list = Self::new(); 126 | list.push(FromStr::from_str(s.0)?); 127 | list.push(FromStr::from_str(s.1)?); 128 | list.push(FromStr::from_str(s.2)?); 129 | Ok(list) 130 | } 131 | } 132 | 133 | impl<'a, 'b, 'c, 'd, A> TryFrom<(&'a str, &'b str, &'c str, &'d str)> for SpacedList 134 | where 135 | A: FromStr, 136 | { 137 | type Error = ::Err; 138 | fn try_from(s: (&str, &str, &str, &str)) -> Result { 139 | let mut list = Self::new(); 140 | list.push(FromStr::from_str(s.0)?); 141 | list.push(FromStr::from_str(s.1)?); 142 | list.push(FromStr::from_str(s.2)?); 143 | list.push(FromStr::from_str(s.3)?); 144 | Ok(list) 145 | } 146 | } 147 | 148 | impl<'a, 'b, 'c, 'd, 'e, A> TryFrom<(&'a str, &'b str, &'c str, &'d str, &'e str)> for SpacedList 149 | where 150 | A: FromStr, 151 | { 152 | type Error = ::Err; 153 | fn try_from(s: (&str, &str, &str, &str, &str)) -> Result { 154 | let mut list = Self::new(); 155 | list.push(FromStr::from_str(s.0)?); 156 | list.push(FromStr::from_str(s.1)?); 157 | list.push(FromStr::from_str(s.2)?); 158 | list.push(FromStr::from_str(s.3)?); 159 | list.push(FromStr::from_str(s.4)?); 160 | Ok(list) 161 | } 162 | } 163 | 164 | impl<'a, 'b, 'c, 'd, 'e, 'f, A> TryFrom<(&'a str, &'b str, &'c str, &'d str, &'e str, &'f str)> 165 | for SpacedList 166 | where 167 | A: FromStr, 168 | { 169 | type Error = ::Err; 170 | fn try_from(s: (&str, &str, &str, &str, &str, &str)) -> Result { 171 | let mut list = Self::new(); 172 | list.push(FromStr::from_str(s.0)?); 173 | list.push(FromStr::from_str(s.1)?); 174 | list.push(FromStr::from_str(s.2)?); 175 | list.push(FromStr::from_str(s.3)?); 176 | list.push(FromStr::from_str(s.4)?); 177 | list.push(FromStr::from_str(s.5)?); 178 | Ok(list) 179 | } 180 | } 181 | 182 | impl<'a, 'b, 'c, 'd, 'e, 'f, 'g, A> 183 | TryFrom<( 184 | &'a str, 185 | &'b str, 186 | &'c str, 187 | &'d str, 188 | &'e str, 189 | &'f str, 190 | &'g str, 191 | )> for SpacedList 192 | where 193 | A: FromStr, 194 | { 195 | type Error = ::Err; 196 | fn try_from(s: (&str, &str, &str, &str, &str, &str, &str)) -> Result { 197 | let mut list = Self::new(); 198 | list.push(FromStr::from_str(s.0)?); 199 | list.push(FromStr::from_str(s.1)?); 200 | list.push(FromStr::from_str(s.2)?); 201 | list.push(FromStr::from_str(s.3)?); 202 | list.push(FromStr::from_str(s.4)?); 203 | list.push(FromStr::from_str(s.5)?); 204 | list.push(FromStr::from_str(s.6)?); 205 | Ok(list) 206 | } 207 | } 208 | 209 | impl<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, A> 210 | TryFrom<( 211 | &'a str, 212 | &'b str, 213 | &'c str, 214 | &'d str, 215 | &'e str, 216 | &'f str, 217 | &'g str, 218 | &'h str, 219 | )> for SpacedList 220 | where 221 | A: FromStr, 222 | { 223 | type Error = ::Err; 224 | fn try_from(s: (&str, &str, &str, &str, &str, &str, &str, &str)) -> Result { 225 | let mut list = Self::new(); 226 | list.push(FromStr::from_str(s.0)?); 227 | list.push(FromStr::from_str(s.1)?); 228 | list.push(FromStr::from_str(s.2)?); 229 | list.push(FromStr::from_str(s.3)?); 230 | list.push(FromStr::from_str(s.4)?); 231 | list.push(FromStr::from_str(s.5)?); 232 | list.push(FromStr::from_str(s.6)?); 233 | list.push(FromStr::from_str(s.7)?); 234 | Ok(list) 235 | } 236 | } 237 | 238 | macro_rules! spacedlist_from_array { 239 | ($num:tt) => { 240 | impl<'a, A> TryFrom<[&'a str; $num]> for SpacedList 241 | where 242 | A: FromStr, 243 | { 244 | type Error = ::Err; 245 | fn try_from(s: [&str; $num]) -> Result { 246 | s.iter().map(|s| FromStr::from_str(*s)).collect() 247 | } 248 | } 249 | }; 250 | } 251 | spacedlist_from_array!(1); 252 | spacedlist_from_array!(2); 253 | spacedlist_from_array!(3); 254 | spacedlist_from_array!(4); 255 | spacedlist_from_array!(5); 256 | spacedlist_from_array!(6); 257 | spacedlist_from_array!(7); 258 | spacedlist_from_array!(8); 259 | spacedlist_from_array!(9); 260 | spacedlist_from_array!(10); 261 | spacedlist_from_array!(11); 262 | spacedlist_from_array!(12); 263 | spacedlist_from_array!(13); 264 | spacedlist_from_array!(14); 265 | spacedlist_from_array!(15); 266 | spacedlist_from_array!(16); 267 | spacedlist_from_array!(17); 268 | spacedlist_from_array!(18); 269 | spacedlist_from_array!(19); 270 | spacedlist_from_array!(20); 271 | spacedlist_from_array!(21); 272 | spacedlist_from_array!(22); 273 | spacedlist_from_array!(23); 274 | spacedlist_from_array!(24); 275 | spacedlist_from_array!(25); 276 | spacedlist_from_array!(26); 277 | spacedlist_from_array!(27); 278 | spacedlist_from_array!(28); 279 | spacedlist_from_array!(29); 280 | spacedlist_from_array!(30); 281 | spacedlist_from_array!(31); 282 | spacedlist_from_array!(32); 283 | -------------------------------------------------------------------------------- /typed-html/src/types/spacedset.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | use std::convert::{TryFrom, TryInto}; 3 | use std::fmt::{Debug, Display, Error, Formatter}; 4 | use std::iter::FromIterator; 5 | use std::ops::{Deref, DerefMut}; 6 | use std::str::FromStr; 7 | 8 | /// A space separated set of unique values. 9 | /// 10 | /// This type represents a set of unique values represented as a string of 11 | /// values separated by spaces in HTML attributes. 12 | /// 13 | /// # Examples 14 | /// 15 | /// ``` 16 | /// # use std::convert::{TryFrom, TryInto}; 17 | /// use axohtml::types::{Class, SpacedSet}; 18 | /// 19 | /// # fn main() -> Result<(), &'static str> { 20 | /// let classList: SpacedSet = "foo bar baz".try_into()?; 21 | /// let classList: SpacedSet = ["foo", "bar", "baz"].try_into()?; 22 | /// let classList: SpacedSet = ("foo", "bar", "baz").try_into()?; 23 | /// 24 | /// let classList1: SpacedSet = "foo bar foo".try_into()?; 25 | /// let classList2: SpacedSet = "bar foo bar".try_into()?; 26 | /// assert_eq!(classList1, classList2); 27 | /// # Ok(()) } 28 | /// ``` 29 | #[derive(Clone, PartialEq, Eq, Hash)] 30 | pub struct SpacedSet(BTreeSet); 31 | 32 | impl SpacedSet { 33 | /// Construct an empty `SpacedSet`. 34 | pub fn new() -> Self { 35 | SpacedSet(BTreeSet::new()) 36 | } 37 | 38 | /// Add a value to the `SpacedSet`, converting it as necessary. 39 | /// 40 | /// Panics if the conversion fails. 41 | pub fn add>(&mut self, value: T) -> bool 42 | where 43 | >::Error: Debug, 44 | { 45 | self.0.insert(value.try_into().unwrap()) 46 | } 47 | 48 | /// Add a value to the `SpacedSet`, converting it as necessary. 49 | /// 50 | /// Returns an error if the conversion fails. 51 | pub fn try_add>(&mut self, value: T) -> Result>::Error> { 52 | Ok(self.0.insert(value.try_into()?)) 53 | } 54 | } 55 | 56 | impl Default for SpacedSet { 57 | fn default() -> Self { 58 | Self::new() 59 | } 60 | } 61 | 62 | impl FromIterator for SpacedSet { 63 | fn from_iter(iter: I) -> Self 64 | where 65 | I: IntoIterator, 66 | { 67 | SpacedSet(iter.into_iter().collect()) 68 | } 69 | } 70 | 71 | impl<'a, A: 'a + Ord + Clone> FromIterator<&'a A> for SpacedSet { 72 | fn from_iter(iter: I) -> Self 73 | where 74 | I: IntoIterator, 75 | { 76 | SpacedSet(iter.into_iter().cloned().collect()) 77 | } 78 | } 79 | 80 | impl FromStr for SpacedSet 81 | where 82 | ::Err: Debug, 83 | { 84 | type Err = ::Err; 85 | 86 | fn from_str(s: &str) -> Result { 87 | let result: Result, Self::Err> = 88 | s.split_whitespace().map(|s| FromStr::from_str(s)).collect(); 89 | result.map(Self::from_iter) 90 | } 91 | } 92 | 93 | impl<'a, A> TryFrom<&'a str> for SpacedSet 94 | where 95 | A: Ord + FromStr, 96 | { 97 | type Error = ::Err; 98 | fn try_from(s: &'a str) -> Result { 99 | s.split_whitespace().map(FromStr::from_str).collect() 100 | } 101 | } 102 | 103 | impl Deref for SpacedSet { 104 | type Target = BTreeSet; 105 | fn deref(&self) -> &Self::Target { 106 | &self.0 107 | } 108 | } 109 | 110 | impl DerefMut for SpacedSet { 111 | fn deref_mut(&mut self) -> &mut Self::Target { 112 | &mut self.0 113 | } 114 | } 115 | 116 | impl Display for SpacedSet { 117 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 118 | let mut it = self.0.iter().peekable(); 119 | while let Some(class) = it.next() { 120 | Display::fmt(class, f)?; 121 | if it.peek().is_some() { 122 | Display::fmt(" ", f)?; 123 | } 124 | } 125 | Ok(()) 126 | } 127 | } 128 | 129 | impl Debug for SpacedSet { 130 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 131 | f.debug_list().entries(self.0.iter()).finish() 132 | } 133 | } 134 | 135 | impl<'a, 'b, A> TryFrom<(&'a str, &'b str)> for SpacedSet 136 | where 137 | A: Ord + FromStr, 138 | { 139 | type Error = ::Err; 140 | fn try_from(s: (&str, &str)) -> Result { 141 | let mut list = Self::new(); 142 | list.insert(FromStr::from_str(s.0)?); 143 | list.insert(FromStr::from_str(s.1)?); 144 | Ok(list) 145 | } 146 | } 147 | 148 | impl<'a, 'b, 'c, A> TryFrom<(&'a str, &'b str, &'c str)> for SpacedSet 149 | where 150 | A: Ord + FromStr, 151 | { 152 | type Error = ::Err; 153 | fn try_from(s: (&str, &str, &str)) -> Result { 154 | let mut list = Self::new(); 155 | list.insert(FromStr::from_str(s.0)?); 156 | list.insert(FromStr::from_str(s.1)?); 157 | list.insert(FromStr::from_str(s.2)?); 158 | Ok(list) 159 | } 160 | } 161 | 162 | impl<'a, 'b, 'c, 'd, A> TryFrom<(&'a str, &'b str, &'c str, &'d str)> for SpacedSet 163 | where 164 | A: Ord + FromStr, 165 | { 166 | type Error = ::Err; 167 | fn try_from(s: (&str, &str, &str, &str)) -> Result { 168 | let mut list = Self::new(); 169 | list.insert(FromStr::from_str(s.0)?); 170 | list.insert(FromStr::from_str(s.1)?); 171 | list.insert(FromStr::from_str(s.2)?); 172 | list.insert(FromStr::from_str(s.3)?); 173 | Ok(list) 174 | } 175 | } 176 | 177 | impl<'a, 'b, 'c, 'd, 'e, A> TryFrom<(&'a str, &'b str, &'c str, &'d str, &'e str)> for SpacedSet 178 | where 179 | A: Ord + FromStr, 180 | { 181 | type Error = ::Err; 182 | fn try_from(s: (&str, &str, &str, &str, &str)) -> Result { 183 | let mut list = Self::new(); 184 | list.insert(FromStr::from_str(s.0)?); 185 | list.insert(FromStr::from_str(s.1)?); 186 | list.insert(FromStr::from_str(s.2)?); 187 | list.insert(FromStr::from_str(s.3)?); 188 | list.insert(FromStr::from_str(s.4)?); 189 | Ok(list) 190 | } 191 | } 192 | 193 | impl<'a, 'b, 'c, 'd, 'e, 'f, A> TryFrom<(&'a str, &'b str, &'c str, &'d str, &'e str, &'f str)> 194 | for SpacedSet 195 | where 196 | A: Ord + FromStr, 197 | { 198 | type Error = ::Err; 199 | fn try_from(s: (&str, &str, &str, &str, &str, &str)) -> Result { 200 | let mut list = Self::new(); 201 | list.insert(FromStr::from_str(s.0)?); 202 | list.insert(FromStr::from_str(s.1)?); 203 | list.insert(FromStr::from_str(s.2)?); 204 | list.insert(FromStr::from_str(s.3)?); 205 | list.insert(FromStr::from_str(s.4)?); 206 | list.insert(FromStr::from_str(s.5)?); 207 | Ok(list) 208 | } 209 | } 210 | 211 | impl<'a, 'b, 'c, 'd, 'e, 'f, 'g, A> 212 | TryFrom<( 213 | &'a str, 214 | &'b str, 215 | &'c str, 216 | &'d str, 217 | &'e str, 218 | &'f str, 219 | &'g str, 220 | )> for SpacedSet 221 | where 222 | A: Ord + FromStr, 223 | { 224 | type Error = ::Err; 225 | fn try_from(s: (&str, &str, &str, &str, &str, &str, &str)) -> Result { 226 | let mut list = Self::new(); 227 | list.insert(FromStr::from_str(s.0)?); 228 | list.insert(FromStr::from_str(s.1)?); 229 | list.insert(FromStr::from_str(s.2)?); 230 | list.insert(FromStr::from_str(s.3)?); 231 | list.insert(FromStr::from_str(s.4)?); 232 | list.insert(FromStr::from_str(s.5)?); 233 | list.insert(FromStr::from_str(s.6)?); 234 | Ok(list) 235 | } 236 | } 237 | 238 | impl<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, A> 239 | TryFrom<( 240 | &'a str, 241 | &'b str, 242 | &'c str, 243 | &'d str, 244 | &'e str, 245 | &'f str, 246 | &'g str, 247 | &'h str, 248 | )> for SpacedSet 249 | where 250 | A: Ord + FromStr, 251 | { 252 | type Error = ::Err; 253 | fn try_from(s: (&str, &str, &str, &str, &str, &str, &str, &str)) -> Result { 254 | let mut list = Self::new(); 255 | list.insert(FromStr::from_str(s.0)?); 256 | list.insert(FromStr::from_str(s.1)?); 257 | list.insert(FromStr::from_str(s.2)?); 258 | list.insert(FromStr::from_str(s.3)?); 259 | list.insert(FromStr::from_str(s.4)?); 260 | list.insert(FromStr::from_str(s.5)?); 261 | list.insert(FromStr::from_str(s.6)?); 262 | list.insert(FromStr::from_str(s.7)?); 263 | Ok(list) 264 | } 265 | } 266 | 267 | macro_rules! spacedset_from_array { 268 | ($num:tt) => { 269 | impl<'a, A> TryFrom<[&'a str; $num]> for SpacedSet 270 | where 271 | A: Ord + FromStr, 272 | { 273 | type Error = ::Err; 274 | fn try_from(s: [&str; $num]) -> Result { 275 | s.iter().map(|s| FromStr::from_str(*s)).collect() 276 | } 277 | } 278 | }; 279 | } 280 | spacedset_from_array!(1); 281 | spacedset_from_array!(2); 282 | spacedset_from_array!(3); 283 | spacedset_from_array!(4); 284 | spacedset_from_array!(5); 285 | spacedset_from_array!(6); 286 | spacedset_from_array!(7); 287 | spacedset_from_array!(8); 288 | spacedset_from_array!(9); 289 | spacedset_from_array!(10); 290 | spacedset_from_array!(11); 291 | spacedset_from_array!(12); 292 | spacedset_from_array!(13); 293 | spacedset_from_array!(14); 294 | spacedset_from_array!(15); 295 | spacedset_from_array!(16); 296 | spacedset_from_array!(17); 297 | spacedset_from_array!(18); 298 | spacedset_from_array!(19); 299 | spacedset_from_array!(20); 300 | spacedset_from_array!(21); 301 | spacedset_from_array!(22); 302 | spacedset_from_array!(23); 303 | spacedset_from_array!(24); 304 | spacedset_from_array!(25); 305 | spacedset_from_array!(26); 306 | spacedset_from_array!(27); 307 | spacedset_from_array!(28); 308 | spacedset_from_array!(29); 309 | spacedset_from_array!(30); 310 | spacedset_from_array!(31); 311 | spacedset_from_array!(32); 312 | --------------------------------------------------------------------------------