├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── script.zh.md ├── slides.pdf ├── src └── lib.rs ├── style_derive ├── Cargo.toml ├── lib.rs └── to_css.rs └── style_traits ├── Cargo.toml └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "style" 3 | version = "0.1.0" 4 | authors = ["Xidorn Quan "] 5 | license = "MPL-2.0" 6 | edition = "2018" 7 | publish = false 8 | 9 | [dependencies] 10 | style_derive = { path = "style_derive" } 11 | style_traits = { path = "style_traits" } 12 | 13 | [workspace] 14 | members = ["style_traits", "style_derive"] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | 375 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # (Re:) Writing A Custm Derive From Zero 2 | 3 | * [Slides](./slides.pdf) 4 | * Post: [中文](./script.zh.md) 5 | * Video: 6 | * [Youtube](https://www.youtube.com/watch?v=Crz_h3egAPM) 7 | * [Bilibili](https://www.bilibili.com/video/av51061344) 8 | -------------------------------------------------------------------------------- /script.zh.md: -------------------------------------------------------------------------------- 1 | # (Re:) 从零开始编写一个Custom Derive 2 | 3 | (根据我在RustCon Asia 2019上的演讲所备的原稿整理而成) 4 | 5 | Servo的样式系统中对custom derive有着广泛的使用。我想从Servo样式系统中的实例出发,介绍一下custom derive的应用,以及如何从零开始开发一个custom derive。 6 | 7 | ## Custom Derive的应用 8 | 9 | 我们可以看这么一个例子,这是Servo的类型系统中一个比较有代表性的类型,可以看到它有13个derive,除了Rust自己的`Clone`、`Copy`、`Debug`、`PartialEq`以及依赖中提供的`MallocSizeOf`以外, 剩下8个都是样式系统中专用的。 10 | 11 | ```rust 12 | #[derive( 13 | Animate, 14 | Clone, 15 | ComputeSquaredDistance, 16 | Copy, 17 | Debug, 18 | MallocSizeOf, 19 | PartialEq, 20 | Parse, 21 | SpecifiedValueInfo, 22 | ToAnimatedValue, 23 | ToAnimatedZero, 24 | ToComputedValue, 25 | ToCss, 26 | )] 27 | /// Either `` or `auto`. 28 | pub enum ColorOrAuto { 29 | /// A ` 30 | Color(C), 31 | /// `auto` 32 | Auto, 33 | } 34 | ``` 35 | 36 | 如果我们这里不用custom derive而是手写出这8个的代码,它大概长这样: 37 | 38 |
39 | 40 | ```rust 41 | use cssparser::{Parser, Token}; 42 | use parser::{Parse, ParserContext}; 43 | use std::fmt::{self, Write}; 44 | use style_traits::{CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, ToCss}; 45 | use values::animated::{Animate, Procedure, ToAnimatedValue, ToAnimatedZero}; 46 | use values::computed::{Context, ToComputedValue}; 47 | use values::distance::{ComputeSquaredDistance, SquaredDistance}; 48 | 49 | impl Animate for ColorOrAuto { 50 | fn animate(&self, other: &Self, procedure: Procedure) -> Result { 51 | match (self, other) { 52 | (&ColorOrAuto::Color(ref this), &ColorOrAuto::Color(ref other)) => { 53 | this.animate(other, procedure).map(ColorOrAuto::Color) 54 | } 55 | (&ColorOrAuto::Auto, &ColorOrAuto::Auto) => { 56 | Ok(ColorOrAuto::Auto) 57 | } 58 | _ => Err(()) 59 | } 60 | } 61 | } 62 | 63 | impl ComputeSquaredDistance for ColorOrAuto { 64 | fn compute_squared_distance(&self, other: &Self) -> Result { 65 | match (self, other) { 66 | (&ColorOrAuto::Color(ref this), &ColorOrAuto::Color(ref other)) => { 67 | this.compute_squared_distance(other) 68 | } 69 | (&ColorOrAuto::Auto, &ColorOrAuto::Auto) => { 70 | Ok(SquaredDistance::from_sqrt(0.)) 71 | } 72 | _ => Err(()) 73 | } 74 | } 75 | } 76 | 77 | impl Parse for ColorOrAuto { 78 | fn parse<'i, 't>( 79 | context: &ParserContext, 80 | input: &mut Parser<'i, 't>, 81 | ) -> Result> { 82 | if let Ok(v) = input.try(|i| C::parse(context, i)) { 83 | return Ok(ColorOrAuto::Color(v)); 84 | } 85 | let location = input.current_source_location(); 86 | let ident = input.expect_ident()?; 87 | match_ignore_ascii_case! { &ident, 88 | "auto" => Ok(ColorOrAuto::Auto), 89 | _ => Err(location.new_unexpected_token_error(Token::Ident(ident.clone()))), 90 | } 91 | } 92 | } 93 | 94 | impl SpecifiedValueInfo for ColorOrAuto { 95 | const SUPPORTED_TYPES: u8 = C::SUPPORTED_TYPES; 96 | fn collect_completion_keywords(f: KeywordsCollectFn) { 97 | C::collect_completion_keywords(f); 98 | f(&["auto"]); 99 | } 100 | } 101 | 102 | impl ToAnimatedValue for ColorOrAuto { 103 | type AnimatedValue = ColorOrAuto; 104 | fn to_animated_value(self) -> Self::AnimatedValue { 105 | match self { 106 | ColorOrAuto::Color(c) => ColorOrAuto::Color(c.to_animated_value()), 107 | ColorOrAuto::Auto => ColorOrAuto::Auto, 108 | } 109 | } 110 | fn from_animated_value(animated: Self::AnimatedValue) -> Self { 111 | match animated { 112 | ColorOrAuto::Color(c) => ColorOrAuto::Color(C::from_animated_value(c)), 113 | ColorOrAuto::Auto => ColorOrAuto::Auto, 114 | } 115 | } 116 | } 117 | 118 | impl ToAnimatedZero for ColorOrAuto { 119 | fn to_animated_zero(&self) -> Result { 120 | match self { 121 | ColorOrAuto::Color(c) => c.to_animated_zero().map(ColorOrAuto::Color), 122 | ColorOrAuto::Auto => Ok(ColorOrAuto::Auto), 123 | } 124 | } 125 | } 126 | 127 | impl ToComputedValue for ColorOrAuto { 128 | type ComputedValue = ColorOrAuto; 129 | fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { 130 | match self { 131 | ColorOrAuto::Color(c) => ColorOrAuto::Color(c.to_computed_value(context)), 132 | ColorOrAuto::Auto => ColorOrAuto::Auto, 133 | } 134 | } 135 | fn from_computed_value(computed: &Self::ComputedValue) -> Self { 136 | match computed { 137 | ColorOrAuto::Color(c) => ColorOrAuto::Color(C::from_computed_value(c)), 138 | ColorOrAuto::Auto => ColorOrAuto::Auto, 139 | } 140 | } 141 | } 142 | 143 | impl ToCss for ColorOrAuto { 144 | fn to_css(&self, dest: &mut CssWriter) -> fmt::Result { 145 | match self { 146 | ColorOrAuto::Color(c) => c.to_css(dest), 147 | ColorOrAuto::Auto => dest.write_str("auto"), 148 | } 149 | } 150 | } 151 | ``` 152 | 153 |
154 | 155 | 为什么样式系统中需要用到这么多custom derive呢?原因很简单,因为CSS很复杂。 156 | 157 | 举例来说,CSS里面一个值在不同的阶段需要不同的表达方式,开发者编写CSS的时候写的是文本形式,我们需要把它解析成指定值(specified value),而后指定值要按照一定的规则计算并应用到各个元素上成为计算值(computed value),除此之外因为值在动画过渡的过程中可能需要不同的精度以及不同的取值范围,因而还有一个单独的动画值(animated value)。有这么多不同的形式,我们自然也需要在这些形式之间进行转换。 158 | 159 | 将数据在不同的形式之间转换也许是custom derive最合适的应用场景了。Rust生态圈里非常有名的[Serde](https://crates.io/crates/serde)便是custom derive在这个场景下的一个经典应用:在结构化数据和一些通用的序列化格式之间进行转换。 160 | 161 | 在Stylo中除了数据转换以外,custom derive还被用在一些递归计算,如计算两个值之间的距离,还有某些简单的编译期反射上。 162 | 163 | ## 如何编写Custom derive 164 | 165 | 那么应该如何编写custom derive呢? 166 | 167 | 我在第一次写custom derive之前,觉得那些代码非常抽象,难以理解。那时候别人让我review这些代码,我都是拒绝的。我想custom derive的代码看起来抽象是有原因的。正常的函数,你给一个输入,它给一个输出,你可以很直观地对比它的输出结果与你的期望,也可以很方便地查看中间过程。而对于custom derive来说,它输出的是一段程序,而你能对比的往往只有它所输出的程序的运行结果。增加了一层间接性,自然会增加理解代码的难度。因此我认为,要编写和理解custom derive,最重要的就是你要对你想生成的程序有一个清晰的概念。 168 | 169 | ### 基础代码 170 | 171 | 举例来说,Servo里面有一个trait叫`ToCss`,是用来将某种类型转换到CSS文本形式的。这个trait的声明大体是这样: 172 | 173 | ```rust 174 | pub trait ToCss { 175 | fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write; 176 | } 177 | ``` 178 | 179 | 需要注意的是custom derive需要放在一个独立的crate里,并且需要在crate的`Cargo.toml`里写上 180 | 181 | ```toml 182 | [lib] 183 | proc-macro = true 184 | ``` 185 | 186 | 然后我们就先在它的`lib.rs`里写下这个custom derive的最外层代码: 187 | 188 | ```rust 189 | extern crate proc_macro; 190 | use proc_macro::TokenStream; 191 | #[proc_macro_derive(ToCss)] 192 | pub fn derive_to_css(input: TokenStream) -> TokenStream { 193 | unimplemented!() 194 | } 195 | ``` 196 | 197 | 可以看到custom derive输入的是一个`TokenStream`输出也是一个`TokenStream`,这没什么特别有趣的地方。需要注意的是即使在Rust 2018里,那个`extern crate`目前也依然是必须的,关于解决掉它的方式也[还在讨论之中](https://github.com/rust-lang/rust/issues/57288)。 198 | 199 | CSS中最常见的一类值是关键字,比如说`white-space`可以有`normal`、`nowrap`、`pre`、`pre-wrap`、`pre-line`等值,在Rust里面我们显然应该以枚举的形式来表示,像是这样: 200 | 201 | ```rust 202 | pub enum WhiteSpace { 203 | Normal, 204 | Nowrap, 205 | Pre, 206 | PreWrap, 207 | PreLine, 208 | } 209 | ``` 210 | 211 | 如果我们要为它手动实现`ToCss`,大概应该长这样: 212 | 213 | ```rust 214 | impl ToCss for WhiteSpace { 215 | fn to_css(&self, dest: &mut W) -> fmt::Result 216 | where 217 | W: fmt::Write, 218 | { 219 | match self { 220 | WhiteSpace::Normal => dest.write_str("normal"), 221 | WhiteSpace::Nowrap => dest.write_str("nowrap"), 222 | WhiteSpace::Pre => dest.write_str("pre"), 223 | WhiteSpace::PreWrap => dest.write_str("pre-wrap"), 224 | WhiteSpace::PreLine => dest.write_str("pre-line"), 225 | } 226 | } 227 | } 228 | ``` 229 | 230 | 观察这个实现的代码,我们会发现它有一些比较固定的与类型本身无关的结构,我们可以先把这些结构写下来 231 | 232 | ```rust 233 | #[recursion_limit = "128"] 234 | 235 | use proc_macro::TokenStream; 236 | use quote::quote; 237 | 238 | TokenStream::from(quote! { 239 | impl style_traits::ToCss for /* ??? */ { 240 | fn to_css(&self, dest: &mut W) -> std::fmt::Result 241 | where 242 | W: std::fmt::Write, 243 | { 244 | match self { 245 | /* ??? */ 246 | } 247 | } 248 | } 249 | }) 250 | ``` 251 | 252 | 这里我们用到了一个宏 `quote`,这个宏是来自[同名的crate](https://crates.io/crates/quote),是开发custom derive中使用的两个最重要的crate之一,它可以让你像这样直接写出你要生成的代码,省去很多麻烦。但因为`quote`可能产生很深的宏递归,所以很多时候需要在文件的开头加上`#![recursion_limit = "128"]`。 253 | 254 | 可以看到这里我们在`quote!`之外又包了一层`TokenStream::from`。这是因为`quote`产生的虽然也是`TokenStream`,但却是来自[`proc-marco2`](https://crates.io/crates/proc-macro2)这个crate的。`proc-macro2`的`TokenStream`可以`Into`到`proc_macro`的`TokenStream`中去。为什么有了自带的`proc_macro`还需要`proc-macro2`呢?我也没有能完全说清楚的自信,应该有一些历史和实践上的原因,有兴趣的可以自己研究ww 255 | 256 | 此外你们或许注意到,这里所有的类型和trait我们用的都是完整路径,因为你不知道这个宏会被放在什么地方,所以保险起见用上完整路径比较好。 257 | 258 | ### 解析和代码生成 259 | 260 | `TokenStream`如其名字所述,就是一串符号而已,最好要有什么东西能帮我们将这串符号解析成方便实用的形式。这里就要用到[`syn`](https://crates.io/crates/syn)这个crate了。这个crate实现了一个Rust代码的解析器,用来把一串符号按照Rust的语法解析成语法树。 261 | 262 | 参照文档,我们可以写出下面这样的代码: 263 | 264 | ```rust 265 | use heck::KebabCase; 266 | use proc_macro::TokenStream; 267 | use proc_macro2::TokenStream as TokenStream2; 268 | use quote::quote; 269 | use syn::{parse_macro_input, Data, DeriveInput, Fields}; 270 | 271 | let input = parse_macro_input!(input as DeriveInput); 272 | let name = input.ident; 273 | let match_body = match input.data { 274 | Data::Enum(data) => { 275 | data.variants.into_iter().flat_map(|variant| { 276 | match variant.fields { 277 | Fields::Unit => { 278 | let ident = variant.ident; 279 | let value = ident.to_string().to_kebab_case(); 280 | quote! { 281 | #name::#ident => std::fmt::Write::write_str(dest, #value), 282 | } 283 | } 284 | _ => panic!("unsupported variant fields"), 285 | } 286 | }).collect::() 287 | } 288 | _ => panic!("unsupported data structure"), 289 | }; 290 | TokenStream::from(quote! { 291 | impl style_traits::ToCss for #name { 292 | fn to_css(&self, dest: &mut W) -> std::fmt::Result 293 | where 294 | W: std::fmt::Write, 295 | { 296 | match self { 297 | #match_body 298 | } 299 | } 300 | } 301 | }) 302 | ``` 303 | 304 | 这里我们先将原来代码中的占位内容替换成两个变量,在`quote`里直接用井号加上变量名就可以了。接下来要做的就只是在前面计算出这两个变量的内容。 305 | 306 | 为此我们先将输入数据解析成`DeriveInput`的格式,然后假定其为简单的枚举类型,将枚举每个分支转换成一条`match`分支的代码。 307 | 308 | 这里我们用到了`heck`这个crate来转换大小写写法,但这不是特别重要。 309 | 310 | 另外可以看到里面的`write_str`我们也用了展开的写法,原因和之前一样,因为不知道生成代码的位置,所以要用最保险的写法。这样我们就写出了一个最基本的custom derive了。对于类似`white-space`这样使用关键字的CSS属性就可以直接derive出`ToCss`的实现了。 311 | 312 | ### 代码的拆分 313 | 314 | 这里用了两个不同的`TokenStream`看起来让人不是很舒服。结合这个问题,加上很多时候你会编写不止一个custom derive,通常custom derive的主要逻辑部分都会拆到另外的模块里,比如上面的例子通常会这样拆,在`lib.rs`里写上包装代码: 315 | 316 | ```rust 317 | extern crate proc_macro; 318 | use proc_macro::TokenStream; 319 | 320 | mod to_css; 321 | 322 | #[proc_macro_derive(ToCss)] 323 | pub fn derive_to_css(input: TokenStream) -> TokenStream { 324 | to_css::derive(syn::parse_macro_input!(input)).into() 325 | } 326 | ``` 327 | 328 | 然后在独立的模块`to_css.rs`里写上: 329 | 330 | ```rust 331 | use heck::KebabCase; 332 | use proc_macro2::TokenStream; 333 | use quote::quote; 334 | use syn::{Data, DeriveInput, Fields, Ident, Variant}; 335 | 336 | pub fn derive(input: DeriveInput) -> TokenStream { 337 | let name = input.ident; 338 | let match_body: TokenStream = match input.data { 339 | Data::Enum(data) => data 340 | .variants 341 | .into_iter() 342 | .flat_map(|variant| derive_variant(&name, variant)) 343 | .collect(), 344 | _ => panic!("unsupported data structure"), 345 | }; 346 | quote! { 347 | impl style_traits::ToCss for #name { 348 | fn to_css(&self, dest: &mut W) -> std::fmt::Result 349 | where 350 | W: std::fmt::Write, 351 | { 352 | match self { 353 | #match_body 354 | } 355 | } 356 | } 357 | } 358 | } 359 | 360 | fn derive_variant(name: &Ident, variant: Variant) -> TokenStream { 361 | match variant.fields { 362 | Fields::Unit => { 363 | let ident = variant.ident; 364 | let value = ident.to_string().to_kebab_case(); 365 | quote! { 366 | #name::#ident => std::fmt::Write::write_str(dest, #value), 367 | } 368 | } 369 | _ => panic!("unsupported variant fields"), 370 | } 371 | } 372 | ``` 373 | 374 | 注意这个模块里我们就只接触`proc-macro2`的`TokenStream`了,所有和`proc_macro::TokenStream`的交互都交给了`lib.rs`,这样我们也就可以直接返回`quote!`的结果而不需要另外包装了。 375 | 376 | ### 带字段的枚举 377 | 378 | 我们上面只处理了不带任何字段的枚举,但在现实中有很多带字段的。 379 | 380 | 还是CSS的例子,比如`initial-letters`这个属性就支持`normal`或者一个数字和一个整数作为它的值,我们可能会将其定义为这种形式: 381 | 382 | ```rust 383 | pub enum InitialLetters { 384 | Normal, 385 | Values(f32, i32), 386 | } 387 | ``` 388 | 389 | 这时候我们手写的`ToCss`大概是这样 390 | 391 | ```rust 392 | match self { 393 | InitialLetters::Normal => dest.write_str("normal"), 394 | InitialLetters::Values(v1, v2) => write!(dest, "{} {}", v1, v2), 395 | } 396 | ``` 397 | 398 | 但这种写法显然是对我们这里的状况非常特化的,即`ToCss`输出字符串,而且这里的两个字段正好都实现了`Display`。如果我们不是要输出字符串,或者我们要处理不是`Display`的类型要怎么办? 399 | 400 | 通常我们的策略是,假定一个类型里每个字段的类型都实现了同一个trait,然后我们在custom derive生成的代码里递归调用trait的相应方法就可以了。所以我们要先给`f32`和`i32`实现`ToCss`,这样我们可以把上面实现中的对两个类型特异的部分换成通用的递归调用: 401 | 402 | ```rust 403 | match self { 404 | InitialLetters::Normal => dest.write_str("normal"), 405 | InitialLetters::Values(v1, v2) => { 406 | v1.to_css(dest)?; 407 | dest.write_char(' ')?; 408 | v2.to_css(dest)?; 409 | Ok(()) 410 | } 411 | } 412 | ``` 413 | 414 | 于是我们就对这个部分应该怎么写有了概念,剩下的就简单了,我们在之前`Fields::Unit`的分支下面加上一个新的分支 415 | 416 | ```rust 417 | Fields::Unnamed(fields) => { 418 | let bindings = &(0..fields.unnamed.len()) 419 | .map(|i| Ident::new(&format!("v{}", i), Span::call_site())) 420 | .collect::>(); 421 | let is_first = iter::once(true).chain(iter::repeat(false)); 422 | quote! { 423 | #name::#ident(#(#bindings),*) => { 424 | #( 425 | if !#is_first { 426 | std::fmt::Write::write_char(dest, ' ')?; 427 | } 428 | style_traits::ToCss::to_css(#bindings, dest)?; 429 | )* 430 | Ok(()) 431 | } 432 | } 433 | } 434 | ``` 435 | 436 | 这里首先生成了与无名字段数量同样多的绑定变量的变量名。我们用了`v`加数字的形式作为名称然后新建一个`Ident`表示其为一个标识符。注意我们不能直接用字符串,因为字符串转换为token以后会变成字符串字面量的token,前面我们在`Unit`的分支就有利用过这一点。如果我们需要标识符的话就要用上`Ident`。 437 | 438 | 另外还可以看到`quote`里面也可以像`macro_rules`的宏定义里面那样支持重复,只不过用的也是井号。这里我们用了一个小技巧,结合标准库自带的几个迭代器用来插入中间的空格。 439 | 440 | 支持了无名字段,其实命名字段也是类似的了,无名字段和命名字段之间的差异,无非一个用小括号一个用大括号,以及一个没有名字所以需要我们生成绑定变量名,另一个直接用字段名就好了。我们可以把共通的中间部分提取出来 441 | 442 | ```rust 443 | fn derive_fields_body(bindings: &[Ident]) -> TokenStream { 444 | let is_first = iter::once(true).chain(iter::repeat(false)); 445 | quote! { 446 | #( 447 | if !#is_first { 448 | std::fmt::Write::write_char(dest, ' ')?; 449 | } 450 | style_traits::ToCss::to_css(#bindings, dest)?; 451 | )* 452 | Ok(()) 453 | } 454 | } 455 | ``` 456 | 457 | 然后就简单了 458 | 459 | ```rust 460 | Fields::Named(fields) => { 461 | let field_names = fields 462 | .named 463 | .into_iter() 464 | .map(|field| field.ident.unwrap()) 465 | .collect::>(); 466 | let body = derive_fields_body(&field_names); 467 | quote! { 468 | #name::#ident { #(#field_names),* } => { 469 | #body 470 | } 471 | } 472 | } 473 | ``` 474 | 475 | 这样我们就可以把那个`unsupported variant fields`的panic分支给删掉了,因为我们支持了所有可能的枚举分支类型。 476 | 477 | ### 结构体 478 | 479 | 也许你很早就发现了,其实枚举的三种分支类型也是结构体的三种类型,那么我们是不是可以直接复用这些代码给结构体呢? 480 | 481 | 我们来看这么一个结构体类型: 482 | 483 | ```rust 484 | pub struct CounterPair { 485 | name: CustomIdent, 486 | value: i32, 487 | } 488 | ``` 489 | 490 | 要实现这个结构体的`ToCss`我们可能会这么写 491 | 492 | ```rust 493 | self.name.to_css(dest)?; 494 | dest.write_char(' ')?; 495 | self.value.to_css(dest)?; 496 | Ok(()) 497 | ``` 498 | 499 | 但这么写的话上面的代码似乎复用起来会稍微麻烦一点?其实虽然可能不常用,但对于结构体我们也可以用`match`来匹配,于是我们可以把其中的代码写成这样的形式: 500 | 501 | ```rust 502 | match self { 503 | CounterPair { name, value } => { 504 | name.to_css(dest)?; 505 | dest.write_char(' ')?; 506 | value.to_css(dest)?; 507 | Ok(()) 508 | } 509 | } 510 | ``` 511 | 512 | 这种形式是不是很熟悉了?跟前面枚举的写法几乎如出一辙。于是我们可以将`derive_variant`改名叫`derive_fields`并做一点简单的修改: 513 | 514 | ```rust 515 | fn derive_fields(ident: &Ident, pat: TokenStream, fields: Fields) -> TokenStream { 516 | // ... 517 | } 518 | ``` 519 | 520 | 然后就可以将其同时用于枚举和结构体了 521 | 522 | ```rust 523 | let match_body = match input.data { 524 | Data::Struct(data) => derive_fields(&name, quote!(#name), data.fields), 525 | Data::Enum(data) => data 526 | .variants 527 | .into_iter() 528 | .flat_map(|variant| { 529 | let ident = variant.ident; 530 | derive_fields(&ident, quote!(#name::#ident), variant.fields) 531 | }) 532 | .collect(), 533 | _ => panic!("unsupported data structure"), 534 | }; 535 | ``` 536 | 537 | ### 泛型 538 | 539 | 在更复杂的情况下我们还会遇到泛型类型,比如说这个枚举 540 | 541 | ```rust 542 | pub enum ColorOrAuto { 543 | Color(C), 544 | Auto, 545 | } 546 | ``` 547 | 548 | 这里的泛型参数`C`是某个代表颜色的类型。 549 | 550 | 与处理字段的策略类似,对于泛型我们一般也假定所有类型参数都有实现同一个trait。这里手动为其实现`ToCss`我们大概要这么写: 551 | 552 | ```rust 553 | impl ToCss for ColorOrAuto 554 | where 555 | C: ToCss, 556 | { 557 | fn to_css(&self, dest: &mut W) -> fmt::Result 558 | where 559 | W: fmt::Write, 560 | { 561 | match self { 562 | ColorOrAuto::Color(c) => c.to_css(dest), 563 | ColorOrAuto::Auto => dest.write_str("auto"), 564 | } 565 | } 566 | } 567 | ``` 568 | 569 | 可以看到函数内部的代码没有差别,主要的差别是`impl`块的声明部分了。显然这里我们需要做的是在`quote`的代码里也加上泛型的部分,我们可以这么做: 570 | 571 | ```rust 572 | if !input.generics.params.is_empty() { 573 | let mut where_clause = input.generics.where_clause.take(); 574 | let predicates = &mut where_clause.get_or_insert(parse_quote!(where)).predicates; 575 | for param in input.generics.type_params() { 576 | let ident = ¶m.ident; 577 | predicates.push(parse_quote!(#ident: style_traits::ToCss)); 578 | } 579 | input.generics.where_clause = where_clause; 580 | } 581 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 582 | quote! { 583 | impl#impl_generics style_traits::ToCss for #name#ty_generics 584 | #where_clause 585 | { 586 | // same as before 587 | } 588 | } 589 | ``` 590 | 591 | 这段代码看过去很多东西,但其实解析起来也并不复杂。 592 | 593 | 我们首先把`where`子句给拆出来,然后为每一个类型参数加上一个实现`ToCss`的约束,再把添加好的`where`子句装回去。为什么还要装回去呢?往下看可以发现`syn`非常贴心地为我们准备了一个`split_for_impl`的方法可以轻松地生成我们需要的部分,最后直接把这些部分加到`quote`里就可以了。 594 | 595 | 从这个代码可以看到我们保留了原来类型声明所带有的所有类型约束,这在目前的Rust依然是必须的。在不远的将来如果隐含约束实现了,这部分的代码也可以简化一些吧。 596 | 597 | ### 属性 598 | 599 | 有的时候我们可能会需要微调trait的一些行为,这时候就可以用到属性。 600 | 601 | 比如说CSS的`transform-style`属性有一个值是`preserve-3d`,但`heck`并不能将`Preserve3d`转换到我们需要的形式,这时候我们就希望能用一个属性来解决,比如这样 602 | 603 | ```rust 604 | #[derive(ToCss)] 605 | pub enum TransformStyle { 606 | Flat, 607 | #[css(keyword = "preserve-3d")] 608 | Preserve3d, 609 | } 610 | ``` 611 | 612 | 为了实现这样的功能,我们可以使用一个叫[`darling`](https://crates.io/crates/darling)的crate,可以将属性解析存放到一个结构体中。 613 | 614 | 对于这里的需求,我们可以写这样一个结构体 615 | 616 | ```rust 617 | #[derive(Default, FromVariant)] 618 | #[darling(attributes(css), default)] 619 | struct CssVariantAttrs { 620 | keyword: Option, 621 | } 622 | ``` 623 | 624 | 这里的derive的`FromVariant`表示可以从枚举的分支数据中解析属性信息。与此类似的还有`FromField`、`FromDeriveInput`等可以解析字段或者整个类型的属性信息。`attributes(css)`表示这个结构体对应的是`css`这个属性。这里应该还是比较直观的。 625 | 626 | 然后就在之前得到枚举分支的地方调用`from_variant`来解析属性信息: 627 | 628 | ```rust 629 | let attrs = CssVariantAttrs::from_variant(&variant) 630 | .expect("failed to parse variant attributes"); 631 | let ident = variant.ident; 632 | derive_fields(&ident, quote!(#name::#ident), variant.fields, Some(attrs)) 633 | ``` 634 | 635 | 并在生成代码的地方根据这个属性的值生成出对应的代码即可 636 | 637 | ```rust 638 | let value = attrs 639 | .and_then(|attrs| attrs.keyword) 640 | .unwrap_or_else(|| ident.to_string().to_kebab_case()); 641 | quote! { 642 | #pat => std::fmt::Write::write_str(dest, #value), 643 | } 644 | ``` 645 | 646 | 最后我们需要在`lib.rs`里告诉Rust这个custom derive可以解析`css`这个属性,只要这样: 647 | 648 | ```rust 649 | #[proc_macro_derive(ToCss, attributes(css))] 650 | pub fn derive_to_css(input: TokenStream) -> TokenStream { 651 | to_css::derive(syn::parse_macro_input!(input)).into() 652 | } 653 | ``` 654 | 655 | ### synstructure 656 | 657 | 前面我们一步步分析了这个custom derive的实现,但你或许发现了在这么多代码里其实大多数代码都在处理枚举、结构体、分支、泛型之类的东西,这些代码看起来应该在不同的custom derive实现里都可以通用的。那么是否有什么现成的工具可以替我们处理这些东西呢?答案是肯定的,那就是[`synstructure`](https://crates.io/crates/synstructure)。 658 | 659 | `synstructure`基本上是一个custom derive的工具包和框架。我们上面的代码如果用它的话只要这么简单就可以了: 660 | 661 | ```rust 662 | fn derive_to_css(input: Structure) -> TokenStream { 663 | let body = input.each_variant(|vi| { 664 | let bindings = vi.bindings(); 665 | if bindings.is_empty() { 666 | let value = vi.ast().ident.to_string().to_kebab_case(); 667 | return quote! { 668 | std::fmt::Write::write_str(dest, #value) 669 | }; 670 | } 671 | let is_first = iter::once(true).chain(iter::repeat(false)); 672 | quote! { 673 | #( 674 | if !#is_first { 675 | std::fmt::Write::write_char(dest, ' ')?; 676 | } 677 | style_traits::ToCss::to_css(#bindings, dest)?; 678 | )* 679 | Ok(()) 680 | } 681 | }); 682 | input.gen_impl(quote! { 683 | gen impl style_traits::ToCss for @Self { 684 | fn to_css(&self, dest: &mut W) -> std::fmt::Result 685 | where 686 | W: std::fmt::Write, 687 | { 688 | match self { 689 | #body 690 | } 691 | } 692 | } 693 | }) 694 | } 695 | 696 | decl_derive!([ToCss] => derive_to_css); 697 | ``` 698 | 699 | `synstructure`将结构体和枚举分支都抽象成了一个分支信息的类型,我们只需要对每个分支生成其执行的代码,其他外围代码就都由`synstructure`的`each_variant`来代劳了,泛型之类的处理也交给了它的`gen_impl`,我们只需要专注写最核心的代码就可以了。 700 | 701 | 既然`synstructure`这么方便为什么我不一开始就介绍呢?因为我之前也提到了,写custom derive最重要的是你要对你生成的代码有概念。`synstructure`虽然好用,但我觉得它隐藏了太多代码生成的细节,对一开始的理解和编写custom derive并不是很好。但当你理解了custom derive以后,`synstructure`会成为一个非常趁手的工具。 702 | 703 | 眼尖的人可能注意到了,在用`synstructure`的代码里面对属性的支持消失了。这是因为`synstructure`和`darling`在枚举分支属性的支持上交互性不太好,这一点我想未来还有改进的空间。 704 | 705 | ## 总结 706 | 707 | 完整的代码可以在我相应的[代码仓库](https://github.com/upsuper/custom-derive-2019)中找到,有兴趣的话可以进一步查看。 708 | 709 | 我想声明的是,这里看到的代码都是我为这个演讲从头写出来的,货真价实的从零开始。Servo里相应的代码跟这里有许多区别,区别的一个主要原因当然是实际应用中的需求更复杂,毕竟一开始也说了,CSS很复杂。除此之外,Servo里的那些代码写成的时间也要早许多,特别是过去一年Rust的快速发展和生态圈的完善让很多代码可以有更简化的写法,但已经写成的代码没有特别的必要当然也就没人去改写了。 -------------------------------------------------------------------------------- /slides.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upsuper/custom-derive-2019/1b5e6c2730ef713f73f7f96bd21fecacc39780da/slides.pdf -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use style_derive::ToCss; 3 | use style_traits::ToCss; 4 | 5 | #[derive(ToCss)] 6 | pub enum WhiteSpace { 7 | Normal, 8 | Nowrap, 9 | Pre, 10 | PreWrap, 11 | PreLine, 12 | } 13 | 14 | /* 15 | impl ToCss for WhiteSpace { 16 | fn to_css(&self, dest: &mut W) -> fmt::Result 17 | where 18 | W: fmt::Write, 19 | { 20 | match self { 21 | WhiteSpace::Normal => dest.write_str("normal"), 22 | WhiteSpace::Nowrap => dest.write_str("nowrap"), 23 | WhiteSpace::Pre => dest.write_str("pre"), 24 | WhiteSpace::PreWrap => dest.write_str("pre-wrap"), 25 | WhiteSpace::PreLine => dest.write_str("pre-line"), 26 | } 27 | } 28 | } 29 | */ 30 | 31 | #[derive(ToCss)] 32 | pub enum InitialLetters { 33 | Normal, 34 | Values(f32, i32), 35 | } 36 | 37 | /* 38 | impl ToCss for InitialLetters { 39 | fn to_css(&self, dest: &mut W) -> fmt::Result 40 | where 41 | W: fmt::Write, 42 | { 43 | match self { 44 | InitialLetters::Normal => dest.write_str("normal"), 45 | InitialLetters::Values(v1, v2) => { 46 | v1.to_css(dest)?; 47 | dest.write_char(' ')?; 48 | v2.to_css(dest)?; 49 | Ok(()) 50 | } 51 | } 52 | } 53 | } 54 | */ 55 | 56 | pub struct CustomIdent(String); 57 | 58 | impl ToCss for CustomIdent { 59 | fn to_css(&self, dest: &mut W) -> fmt::Result 60 | where 61 | W: fmt::Write, 62 | { 63 | dest.write_str(&self.0) 64 | } 65 | } 66 | 67 | #[derive(ToCss)] 68 | pub struct CounterPair { 69 | name: CustomIdent, 70 | value: i32, 71 | } 72 | 73 | /* 74 | impl ToCss for CounterPair { 75 | fn to_css(&self, dest: &mut W) -> fmt::Result 76 | where 77 | W: fmt::Write, 78 | { 79 | match self { 80 | CounterPair { name, value } => { 81 | name.to_css(dest)?; 82 | dest.write_char(' ')?; 83 | value.to_css(dest)?; 84 | Ok(()) 85 | } 86 | } 87 | } 88 | } 89 | */ 90 | 91 | #[derive(ToCss)] 92 | pub enum ColorOrAuto { 93 | Color(C), 94 | Auto, 95 | } 96 | 97 | /* 98 | impl ToCss for ColorOrAuto 99 | where 100 | C: ToCss, 101 | { 102 | fn to_css(&self, dest: &mut W) -> fmt::Result 103 | where 104 | W: fmt::Write, 105 | { 106 | match self { 107 | ColorOrAuto::Color(c) => c.to_css(dest), 108 | ColorOrAuto::Auto => dest.write_str("auto"), 109 | } 110 | } 111 | } 112 | */ 113 | 114 | #[derive(ToCss)] 115 | pub enum TransformStyle { 116 | Flat, 117 | #[css(keyword = "preserve-3d")] 118 | Preserve3d, 119 | } 120 | 121 | #[cfg(test)] 122 | mod tests { 123 | use super::*; 124 | use style_traits::ToCss; 125 | 126 | fn to_css(value: T) -> String { 127 | let mut result = String::new(); 128 | value.to_css(&mut result).unwrap(); 129 | result 130 | } 131 | 132 | #[test] 133 | fn test_white_space() { 134 | assert_eq!(to_css(WhiteSpace::Normal), "normal"); 135 | assert_eq!(to_css(WhiteSpace::Nowrap), "nowrap"); 136 | assert_eq!(to_css(WhiteSpace::Pre), "pre"); 137 | assert_eq!(to_css(WhiteSpace::PreWrap), "pre-wrap"); 138 | assert_eq!(to_css(WhiteSpace::PreLine), "pre-line"); 139 | } 140 | 141 | #[test] 142 | fn test_initial_letters() { 143 | assert_eq!(to_css(InitialLetters::Normal), "normal"); 144 | assert_eq!(to_css(InitialLetters::Values(3.0, 2)), "3 2"); 145 | assert_eq!(to_css(InitialLetters::Values(2.51, 3)), "2.51 3"); 146 | } 147 | 148 | #[test] 149 | fn test_counter_pair() { 150 | assert_eq!( 151 | to_css(CounterPair { 152 | name: CustomIdent("a".to_string()), 153 | value: 1 154 | }), 155 | "a 1", 156 | ); 157 | } 158 | 159 | #[test] 160 | fn test_color_or_auto() { 161 | // `i32` is not a color, but for simplification we use it as a fake type. 162 | assert_eq!(to_css(ColorOrAuto::Color(0i32)), "0"); 163 | assert_eq!(to_css(ColorOrAuto::::Auto), "auto"); 164 | } 165 | 166 | #[test] 167 | fn test_transform_style() { 168 | assert_eq!(to_css(TransformStyle::Flat), "flat"); 169 | assert_eq!(to_css(TransformStyle::Preserve3d), "preserve-3d"); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /style_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "style_derive" 3 | version = "0.0.1" 4 | authors = ["Xidorn Quan "] 5 | license = "MPL-2.0" 6 | edition = "2018" 7 | publish = false 8 | 9 | [lib] 10 | path = "lib.rs" 11 | proc-macro = true 12 | 13 | [dependencies] 14 | darling = "0.9.0" 15 | heck = "0.3.1" 16 | proc-macro2 = "0.4.27" 17 | quote = "0.6.11" 18 | syn = "0.15.29" 19 | synstructure = "0.10.1" 20 | -------------------------------------------------------------------------------- /style_derive/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "128"] 2 | 3 | extern crate proc_macro; 4 | use proc_macro::TokenStream; 5 | 6 | mod to_css; 7 | 8 | #[proc_macro_derive(ToCss, attributes(css))] 9 | pub fn derive_to_css(input: TokenStream) -> TokenStream { 10 | to_css::derive(syn::parse_macro_input!(input)).into() 11 | } 12 | 13 | /* 14 | use heck::KebabCase; 15 | use proc_macro2::TokenStream; 16 | use quote::quote; 17 | use std::iter; 18 | use synstructure::{decl_derive, Structure}; 19 | 20 | fn derive_to_css(input: Structure) -> TokenStream { 21 | let body = input.each_variant(|vi| { 22 | let bindings = vi.bindings(); 23 | if bindings.is_empty() { 24 | let value = vi.ast().ident.to_string().to_kebab_case(); 25 | return quote! { 26 | std::fmt::Write::write_str(dest, #value) 27 | }; 28 | } 29 | let is_first = iter::once(true).chain(iter::repeat(false)); 30 | quote! { 31 | #( 32 | if !#is_first { 33 | std::fmt::Write::write_char(dest, ' ')?; 34 | } 35 | style_traits::ToCss::to_css(#bindings, dest)?; 36 | )* 37 | Ok(()) 38 | } 39 | }); 40 | input.gen_impl(quote! { 41 | gen impl style_traits::ToCss for @Self { 42 | fn to_css(&self, dest: &mut W) -> std::fmt::Result 43 | where 44 | W: std::fmt::Write, 45 | { 46 | match self { 47 | #body 48 | } 49 | } 50 | } 51 | }) 52 | } 53 | 54 | decl_derive!([ToCss, attributes(css)] => derive_to_css); 55 | */ 56 | -------------------------------------------------------------------------------- /style_derive/to_css.rs: -------------------------------------------------------------------------------- 1 | use darling::FromVariant; 2 | use heck::KebabCase; 3 | use proc_macro2::{Span, TokenStream}; 4 | use quote::quote; 5 | use std::iter; 6 | use syn::{parse_quote, Data, DeriveInput, Fields, Ident}; 7 | 8 | pub fn derive(mut input: DeriveInput) -> TokenStream { 9 | let name = input.ident; 10 | let match_body = match input.data { 11 | Data::Struct(data) => derive_fields(&name, quote!(#name), data.fields, None), 12 | Data::Enum(data) => data 13 | .variants 14 | .into_iter() 15 | .flat_map(|variant| { 16 | let attrs = CssVariantAttrs::from_variant(&variant) 17 | .expect("failed to parse variant attributes"); 18 | let ident = variant.ident; 19 | derive_fields(&ident, quote!(#name::#ident), variant.fields, Some(attrs)) 20 | }) 21 | .collect(), 22 | _ => panic!("unsupported data structure"), 23 | }; 24 | 25 | if !input.generics.params.is_empty() { 26 | let mut where_clause = input.generics.where_clause.take(); 27 | let predicates = &mut where_clause.get_or_insert(parse_quote!(where)).predicates; 28 | for param in input.generics.type_params() { 29 | let ident = ¶m.ident; 30 | predicates.push(parse_quote!(#ident: style_traits::ToCss)); 31 | } 32 | input.generics.where_clause = where_clause; 33 | } 34 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 35 | quote! { 36 | impl#impl_generics style_traits::ToCss for #name#ty_generics 37 | #where_clause 38 | { 39 | fn to_css(&self, dest: &mut W) -> std::fmt::Result 40 | where 41 | W: std::fmt::Write, 42 | { 43 | match self { 44 | #match_body 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | fn derive_fields( 52 | ident: &Ident, 53 | pat: TokenStream, 54 | fields: Fields, 55 | attrs: Option, 56 | ) -> TokenStream { 57 | match fields { 58 | Fields::Unit => { 59 | let value = attrs 60 | .and_then(|attrs| attrs.keyword) 61 | .unwrap_or_else(|| ident.to_string().to_kebab_case()); 62 | quote! { 63 | #pat => std::fmt::Write::write_str(dest, #value), 64 | } 65 | } 66 | Fields::Unnamed(fields) => { 67 | let bindings = (0..fields.unnamed.len()) 68 | .map(|i| Ident::new(&format!("v{}", i), Span::call_site())) 69 | .collect::>(); 70 | let body = derive_fields_body(&bindings); 71 | quote! { 72 | #pat(#(#bindings),*) => { #body } 73 | } 74 | } 75 | Fields::Named(fields) => { 76 | let field_names = fields 77 | .named 78 | .into_iter() 79 | .map(|field| field.ident.unwrap()) 80 | .collect::>(); 81 | let body = derive_fields_body(&field_names); 82 | quote! { 83 | #pat { #(#field_names),* } => { #body } 84 | } 85 | } 86 | } 87 | } 88 | 89 | fn derive_fields_body(bindings: &[Ident]) -> TokenStream { 90 | let is_first = iter::once(true).chain(iter::repeat(false)); 91 | quote! { 92 | #( 93 | if !#is_first { 94 | std::fmt::Write::write_char(dest, ' ')?; 95 | } 96 | style_traits::ToCss::to_css(#bindings, dest)?; 97 | )* 98 | Ok(()) 99 | } 100 | } 101 | 102 | #[derive(Default, FromVariant)] 103 | #[darling(attributes(css), default)] 104 | struct CssVariantAttrs { 105 | keyword: Option, 106 | } 107 | -------------------------------------------------------------------------------- /style_traits/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "style_traits" 3 | version = "0.1.0" 4 | authors = ["Xidorn Quan "] 5 | license = "MPL-2.0" 6 | edition = "2018" 7 | publish = false 8 | 9 | [lib] 10 | path = "lib.rs" 11 | 12 | [dependencies] 13 | -------------------------------------------------------------------------------- /style_traits/lib.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | pub trait ToCss { 4 | fn to_css(&self, dest: &mut W) -> fmt::Result 5 | where 6 | W: fmt::Write; 7 | } 8 | 9 | impl ToCss for i32 { 10 | fn to_css(&self, dest: &mut W) -> fmt::Result 11 | where 12 | W: fmt::Write, 13 | { 14 | write!(dest, "{}", self) 15 | } 16 | } 17 | 18 | impl ToCss for f32 { 19 | fn to_css(&self, dest: &mut W) -> fmt::Result 20 | where 21 | W: fmt::Write, 22 | { 23 | write!(dest, "{}", self) 24 | } 25 | } 26 | --------------------------------------------------------------------------------