-
Notifications
You must be signed in to change notification settings - Fork 20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proc macro for css #17
Conversation
Thank you for another PR! However, I'd like to ask a question: What's the deciding factor of picking this syntax over string literals? (I feel harsh to bring this question up after you are thousands of lines in. That is: let sheet = css! {
background: red;
}; over: let sheet = css!(r#"background: red;"#); The reason why I am asking is because that we will be able to reuse the parser if a string literal is used. For a short period of time, We've got #1 and #12 (and #16 if selector parsing is needed) that needs changes on the parser. If a string literal is used, the procedural macro will become as simple as something like the following: use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use litrs::StringLit;
use proc_macro2::Literal;
use std::convert::TryFrom;
use stylist_core::ast::*;
use quote::quote;
fn attr_to_token_stream(attr: &StyleAttribute) -> TokenStream2 {
let key_token = Literal::string(&attr.key);
let value_token = Literal::string(&attr.value);
quote! {
::stylist::ast::StyleAttribute {
key: #key_token.to_string(),
value: #value_token.to_string(),
}
}
}
fn block_to_token_stream(block: &Block) -> TokenStream2 {
let cond = if let Some(ref m) = block.condition {
let s = Literal::string(m);
quote! { Some(#s.to_string()) }
} else {
quote! { None }
};
let mut attrs = TokenStream2::new();
for attr in block.style_attributes.iter() {
attrs.extend(attr_to_token_stream(attr));
attrs.extend(quote! { ,});
}
quote! {
::stylist::ast::ScopeContent::Block( ::stylist::ast::Block {
condition: #cond,
style_attributes: vec![#attrs]
})
}
}
fn sheet_to_token_stream(sheet: &Sheet) -> TokenStream2 {
let mut output = TokenStream2::new();
for scope in sheet.0.iter() {
match scope {
ScopeContent::Block(ref m) => {
output.extend(block_to_token_stream(m));
}
_ => panic!(),
}
output.extend(quote! { ,});
}
quote! {
::stylist::ast::Sheet(vec![#output])
}
}
#[proc_macro]
pub fn css(input: TokenStream) -> TokenStream {
let mut tokens = input.into_iter();
let first_token = tokens.next().unwrap();
let s_literal = StringLit::try_from(first_token).unwrap();
let sheet: Sheet = s_literal.value().parse().unwrap();
sheet_to_token_stream(&sheet).into()
} (BTW, I did test my code this time.) With that being said, I do not object to this syntax if it provides enough advantage that warrants maintaining a separate parser and a separate AST in the long run. |
The advantage should be, with correct implementation, error highlighting. E.g. forgetting a Further, as you can see you can include arbitrary expressions of |
Example of what you can't do as well when you always css! {
backgroundColor: ${theme.background};
fontSize: ${theme.font_size};
} Converting the above to the style string at runtime should take almost no time, as the syntax tree is statically determined, only the values are injected at runtime. I do not see how a comparable approach could work with just using parse, as the style string is not available at compile time. You'd have to fall back to parsing at runtime: Style::new(format!(r#"
background-color: {bg};
font-size: {sz};
"#, bg = theme.background, sz = theme.font_size)); |
I should also mention, I do value your input on this, you seem level headed and thoughtful :) But don't cry for the time I sink into this, I would do it either way and at the end of the day, I will have to eat my own dogfood code in material-yewi so at least it will be somewhat battle tested. |
How could I deserve such an admiration. You are a humourous person. (I mean, The parser already reports the exact location on errors, when combined with Comparing to Native error: Both are not precisely reporting the location, but I think both are optimisable (and acceptable for now). It's also possible to provide error handling on interpolation values at compile time with format!-style syntax using let color = Color::from_rgb(255, 255, 255); // implements Display
let css = css!(
r#"
color: ${color},
"#,
color = color, // quote_spanned! {} here
); In this form, editor completion should work as well as if it's outside of a proc macro. We just need to expand the existing parser to accept interpolation during compile time and reject interpolation during runtime and limit interpolation to only value of attributes and conditions. Also not related but having a format!-style syntax also has other benefits (does not have to be supported this round): let opacity = 0.1;
let css = css!(
r#"
/* Limit precision to 0.1 */
opacity: ${opactity:.1};
"#,
opacity = opacity,
); Although, I suspect this would also be possible for non-string literal syntax. |
Although, I am still slightly leaning towards a format!-like string literal syntax, purely based on it's less effort to maintain. At the end of the day, I will accept either syntax as I am not the person implementing this PR (unresponsible). I will leave it to you to decide. :) |
Since there's no development on this PR for a while and I want to experiment with #23, I have started to implemented a tentative procedural macro in master and have re-purposed The API that emits a I will still accept this PR if you want to implement this as an alternative syntax. |
I was on vacation for a week, I'll read through what you've done in the mean time, looks promising. I really hope I can switch to this so I can test impacts on code size 👍 |
4e7841d
to
c679e54
Compare
@futursolo if you have 10 minutes time, can you give me a quick code review focused on readability, naming and points where to add internal code documentation? I'm going to work on the missing user-facing documentation now, apart from that, I consider the PR to be feature complete. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current token based parser might be "too complex" (both in amount of code and difficulty to understand) and I would like to see the complexity reduced before merging.
There're a couple ways that this could be done:
- follow rust naming convention (e.g:
stringify
->to_string
) abort!
on first error and move all error handling into parse stage (by usingproc_macro_error
).- Follow
Token
->AST(Css*)
-> AST(stylist::ast::*
) more closely (Parse selectors into selectors in parsing stage other than seeking for commas in output stage) - Remove
Output*
andfold_*
by not doing anything that is not supported bystylist::ast
. - Collect tokens into string literal whenever possible so output stage doesn't have to consider them.
- Document what you are trying to do in the complex code. (Some code were quite difficult to understand for me when first looking at it.)
I recently discovered a crate jss and I think their syntax is elegant and simpler to handle (json::object!
is not even a procedural macro.).
Maybe shifting our syntax a little towards jss
can help reducing the complexity?
(Use some string literals, as there're cases where css tokens cannot be represented properly (like: #000000
) anyways?)
Other than the complexity level of code and some inefficiency in code generation (which I think it's ok to address later), I think this PR looks solid. I do like this syntax as it is simpler than string literals when it comes to interpolations. My rationale about reducing code complexity is based on that it may hinder future contribution from other developers if they cannot easily understand the parser internals. Thank you for another big PR. |
I hear you. I can easily see that |
every 1000 iterations are the same to also get caching behavior to come through
* get rid of convert_case dependency for now
I think I've addressed all review comments thus far. Before I unmark this as Draft, I wanted to check if I should do an interactive rebase and try to rewrite the history a bit so it's cleaner. I think it should be possible to split this into:
And have 4 separate commits that each should be more easy to review and read in the commit history. Let me know what you think @futursolo or if there's anything else to do before that, thank you for your comments and nice discussion. |
In the future, it might be better to separate the changeset into multiple PRs for big changes like this. Thank you. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there're still some trivial things that I didn't cover.
But I think it might be easier to fix them directly after merging this PR than pointing all of them out in the comments.
(I guess I am lazy. :) )
Again, thank you for the PR.
Implements a proc macro for writing css, resulting in an expression of type
Sheet
. The following parts are still missing or up for review already:so this also contains a fix for that.the resolution has been benchedAdditionally fixes failing compilation on master whenmaster has been fixed--target wasm32-unknown-unknown
is active. I suppose at some point I can rebase against a fix on master