1#![doc = include_str!("README.md")]
7#![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")]
8
9extern crate proc_macro;
10
11use i_slint_compiler::diagnostics::BuildDiagnostics;
12use i_slint_compiler::parser::SyntaxKind;
13use i_slint_compiler::*;
14use proc_macro::{Spacing, TokenStream, TokenTree};
15use quote::quote;
16
17fn are_token_touching(token1: proc_macro::Span, token2: proc_macro::Span) -> Option<bool> {
22 are_token_touching_impl(&format!("{token1:?}"), &format!("{token2:?}"))
25}
26
27fn are_token_touching_impl(token1_debug: &str, token2_debug: &str) -> Option<bool> {
28 let is_byte_char = |c: char| c.is_numeric() || c == ':';
32 let not_is_byte_char = |c: char| !is_byte_char(c);
33 let end_of_token1 = token1_debug
34 .trim_end_matches(not_is_byte_char)
35 .rsplit(not_is_byte_char)
36 .next()?
37 .trim_matches(':');
38 let begin_of_token2 = token2_debug
39 .trim_end_matches(not_is_byte_char)
40 .strip_suffix(is_byte_char)?
41 .trim_end_matches(is_byte_char)
42 .trim_end_matches(not_is_byte_char)
43 .rsplit(not_is_byte_char)
44 .next()?
45 .trim_matches(':');
46 (!begin_of_token2.is_empty()).then_some(end_of_token1 == begin_of_token2)
47}
48
49#[test]
50fn are_token_touching_impl_test() {
51 assert!(are_token_touching_impl("#0 bytes(6662788..6662789)", "#0 bytes(6662789..6662794)")
52 .unwrap());
53 assert!(!are_token_touching_impl("#0 bytes(6662788..6662789)", "#0 bytes(6662790..6662794)")
54 .unwrap());
55 assert!(!are_token_touching_impl("#0 bytes(6662789..6662794)", "#0 bytes(6662788..6662789)")
56 .unwrap());
57 assert!(
58 !are_token_touching_impl("#0 bytes(6662788..6662789)", "#0 bytes(662789..662794)").unwrap()
59 );
60 assert!(are_token_touching_impl("#0 bytes(123..456)", "#0 bytes(456..789)").unwrap());
61
62 assert!(are_token_touching_impl("/foo/bar.rs:12:7: 12:18", "/foo/bar.rs:12:18: 12:19").unwrap());
64 assert!(are_token_touching_impl("/foo/bar.rs:2:7: 13:18", "/foo/bar.rs:13:18: 14:29").unwrap());
65 assert!(!are_token_touching_impl("/foo/bar.rs:2:7: 13:18", "/foo/bar.rs:14:18: 14:29").unwrap());
66 assert!(!are_token_touching_impl("/foo/bar.rs:2:7: 2:8", "/foo/bar.rs:2:18: 2:29").unwrap());
67
68 assert!(are_token_touching_impl("hello", "hello").is_none());
70 assert!(are_token_touching_impl("hello42", "hello42").is_none());
71
72 assert!(are_token_touching_impl("55", "56").is_none());
74}
75
76fn fill_token_vec(stream: impl Iterator<Item = TokenTree>, vec: &mut Vec<parser::Token>) {
77 let mut prev_spacing = Spacing::Alone;
78 let mut prev_span = proc_macro::Span::call_site();
79 for t in stream {
80 let span = t.span();
81 match t {
82 TokenTree::Ident(i) => {
83 if let Some(last) = vec.last_mut() {
84 if (last.kind == SyntaxKind::ColorLiteral && last.text.len() == 1)
85 || (last.kind == SyntaxKind::Identifier
86 && are_token_touching(prev_span, span)
87 .unwrap_or_else(|| last.text.ends_with('-')))
88 {
89 last.text = format!("{}{}", last.text, i).into();
90 prev_span = span;
91 continue;
92 }
93 }
94 vec.push(parser::Token {
95 kind: SyntaxKind::Identifier,
96 text: i.to_string().into(),
97 span: Some(i.span()),
98 ..Default::default()
99 });
100 }
101 TokenTree::Punct(p) => {
102 let kind = match p.as_char() {
103 ':' => SyntaxKind::Colon,
104 '=' => {
105 if let Some(last) = vec.last_mut() {
106 let kt = match last.kind {
107 SyntaxKind::Star => Some((SyntaxKind::StarEqual, "*=")),
108 SyntaxKind::Colon => Some((SyntaxKind::ColonEqual, ":=")),
109 SyntaxKind::Plus => Some((SyntaxKind::PlusEqual, "+=")),
110 SyntaxKind::Minus => Some((SyntaxKind::MinusEqual, "-=")),
111 SyntaxKind::Div => Some((SyntaxKind::DivEqual, "/=")),
112 SyntaxKind::LAngle => Some((SyntaxKind::LessEqual, "<=")),
113 SyntaxKind::RAngle => Some((SyntaxKind::GreaterEqual, ">=")),
114 SyntaxKind::Equal => Some((SyntaxKind::EqualEqual, "==")),
115 SyntaxKind::Bang => Some((SyntaxKind::NotEqual, "!=")),
116 _ => None,
117 };
118 if let Some((k, t)) = kt {
119 if prev_spacing == Spacing::Joint {
120 last.kind = k;
121 last.text = t.into();
122 continue;
123 }
124 }
125 }
126 SyntaxKind::Equal
127 }
128 ';' => SyntaxKind::Semicolon,
129 '!' => SyntaxKind::Bang,
130 '.' => {
131 if let Some(last) = vec.last_mut() {
133 if last.kind == SyntaxKind::NumberLiteral
134 && are_token_touching(prev_span, p.span()).unwrap_or(false)
135 && !last.text.contains('.')
136 && !last.text.ends_with(char::is_alphabetic)
137 {
138 last.text = format!("{}.", last.text).into();
139 prev_span = span;
140 continue;
141 }
142 }
143 SyntaxKind::Dot
144 }
145 '+' => SyntaxKind::Plus,
146 '-' => {
147 if let Some(last) = vec.last_mut() {
148 if last.kind == SyntaxKind::Identifier
149 && are_token_touching(prev_span, p.span()).unwrap_or(true)
150 {
151 last.text = format!("{}-", last.text).into();
152 prev_span = span;
153 continue;
154 }
155 }
156 SyntaxKind::Minus
157 }
158 '*' => SyntaxKind::Star,
159 '/' => SyntaxKind::Div,
160 '<' => SyntaxKind::LAngle,
161 '>' => {
162 if let Some(last) = vec.last_mut() {
163 if last.kind == SyntaxKind::LessEqual && prev_spacing == Spacing::Joint
164 {
165 last.kind = SyntaxKind::DoubleArrow;
166 last.text = "<=>".into();
167 continue;
168 } else if last.kind == SyntaxKind::Equal
169 && prev_spacing == Spacing::Joint
170 {
171 last.kind = SyntaxKind::FatArrow;
172 last.text = "=>".into();
173 continue;
174 } else if last.kind == SyntaxKind::Minus
175 && prev_spacing == Spacing::Joint
176 {
177 last.kind = SyntaxKind::Arrow;
178 last.text = "->".into();
179 continue;
180 }
181 }
182 SyntaxKind::RAngle
183 }
184 '#' => SyntaxKind::ColorLiteral,
185 '?' => SyntaxKind::Question,
186 ',' => SyntaxKind::Comma,
187 '&' => {
188 if let Some(last) = vec.last_mut() {
191 if last.kind == SyntaxKind::AndAnd && prev_spacing == Spacing::Joint {
192 continue;
193 }
194 }
195 SyntaxKind::AndAnd
196 }
197 '|' => {
198 if let Some(last) = vec.last_mut() {
201 if last.kind == SyntaxKind::Pipe && prev_spacing == Spacing::Joint {
202 last.kind = SyntaxKind::OrOr;
203 continue;
204 }
205 }
206 SyntaxKind::Pipe
207 }
208 '%' => {
209 if let Some(last) = vec.last_mut() {
211 if last.kind == SyntaxKind::NumberLiteral {
212 last.text = format!("{}%", last.text).into();
213 continue;
214 }
215 }
216 SyntaxKind::Percent
217 }
218 '$' => SyntaxKind::Dollar,
219 '@' => SyntaxKind::At,
220 _ => SyntaxKind::Error,
221 };
222 prev_spacing = p.spacing();
223 vec.push(parser::Token {
224 kind,
225 text: p.to_string().into(),
226 span: Some(p.span()),
227 ..Default::default()
228 });
229 }
230 TokenTree::Literal(l) => {
231 let s = l.to_string();
232 let f = s.chars().next().unwrap();
234 let kind = if f == '"' {
235 SyntaxKind::StringLiteral
236 } else if f.is_ascii_digit() {
237 if let Some(last) = vec.last_mut() {
238 if (last.kind == SyntaxKind::ColorLiteral && last.text.len() == 1)
239 || (last.kind == SyntaxKind::Identifier
240 && are_token_touching(prev_span, span)
241 .unwrap_or_else(|| last.text.ends_with('-')))
242 {
243 last.text = format!("{}{}", last.text, s).into();
244 prev_span = span;
245 continue;
246 }
247 }
248 SyntaxKind::NumberLiteral
249 } else {
250 SyntaxKind::Error
251 };
252 vec.push(parser::Token {
253 kind,
254 text: s.into(),
255 span: Some(l.span()),
256 ..Default::default()
257 });
258 }
259 TokenTree::Group(g) => {
260 use proc_macro::Delimiter::*;
261 use SyntaxKind::*;
262 let (l, r, sl, sr) = match g.delimiter() {
263 Parenthesis => (LParent, RParent, "(", ")"),
264 Brace => (LBrace, RBrace, "{", "}"),
265 Bracket => (LBracket, RBracket, "[", "]"),
266 None => todo!(),
267 };
268 vec.push(parser::Token {
269 kind: l,
270 text: sl.into(),
271 span: Some(g.span()), ..Default::default()
273 });
274 fill_token_vec(g.stream().into_iter(), vec);
275 vec.push(parser::Token {
276 kind: r,
277 text: sr.into(),
278 span: Some(g.span()), ..Default::default()
280 });
281 }
282 }
283 prev_span = span;
284 }
285}
286
287fn extract_path(literal: proc_macro::Literal) -> std::path::PathBuf {
288 let path_with_quotes = literal.to_string();
289 let path_with_quotes_stripped = if let Some(p) = path_with_quotes.strip_prefix('r') {
290 let hash_removed = p.trim_matches('#');
291 hash_removed.strip_prefix('\"').unwrap().strip_suffix('\"').unwrap()
292 } else {
293 path_with_quotes.trim_matches('\"')
295 };
296 path_with_quotes_stripped.into()
297}
298
299fn extract_compiler_config(
300 mut stream: proc_macro::token_stream::IntoIter,
301 compiler_config: &mut CompilerConfiguration,
302) -> impl Iterator<Item = TokenTree> {
303 let mut remaining_stream;
304 loop {
305 remaining_stream = stream.clone();
306 match (stream.next(), stream.next()) {
307 (Some(TokenTree::Punct(p)), Some(TokenTree::Group(group)))
308 if p.as_char() == '#' && group.delimiter() == proc_macro::Delimiter::Bracket =>
309 {
310 let mut attr_stream = group.stream().into_iter();
311 match attr_stream.next() {
312 Some(TokenTree::Ident(include_ident))
313 if include_ident.to_string() == "include_path" =>
314 {
315 match (attr_stream.next(), attr_stream.next()) {
316 (
317 Some(TokenTree::Punct(equal_punct)),
318 Some(TokenTree::Literal(path)),
319 ) if equal_punct.as_char() == '=' => {
320 compiler_config.include_paths.push(extract_path(path));
321 }
322 _ => break,
323 }
324 }
325 Some(TokenTree::Ident(library_ident))
326 if library_ident.to_string() == "library_path" =>
327 {
328 match (attr_stream.next(), attr_stream.next(), attr_stream.next()) {
329 (
330 Some(TokenTree::Group(group)),
331 Some(TokenTree::Punct(equal_punct)),
332 Some(TokenTree::Literal(path)),
333 ) if group.delimiter() == proc_macro::Delimiter::Parenthesis
334 && equal_punct.as_char() == '=' =>
335 {
336 let library_name = group.stream().into_iter().next().unwrap();
337 compiler_config
338 .library_paths
339 .insert(library_name.to_string(), extract_path(path));
340 }
341 _ => break,
342 }
343 }
344 Some(TokenTree::Ident(style_ident)) if style_ident.to_string() == "style" => {
345 match (attr_stream.next(), attr_stream.next()) {
346 (
347 Some(TokenTree::Punct(equal_punct)),
348 Some(TokenTree::Literal(requested_style)),
349 ) if equal_punct.as_char() == '=' => {
350 compiler_config.style = requested_style
351 .to_string()
352 .strip_prefix('\"')
353 .unwrap()
354 .strip_suffix('\"')
355 .unwrap()
356 .to_string()
357 .into();
358 }
359 _ => break,
360 }
361 }
362 _ => break,
363 }
364 }
365 _ => break,
366 }
367 }
368 remaining_stream
369}
370
371#[doc = concat!("[The Slint Language Documentation](https://slint.dev/releases/", env!("CARGO_PKG_VERSION"), "/docs/slint)")]
376#[proc_macro]
385pub fn slint(stream: TokenStream) -> TokenStream {
386 let token_iter = stream.into_iter();
387
388 let mut compiler_config =
389 CompilerConfiguration::new(i_slint_compiler::generator::OutputFormat::Rust);
390
391 let token_iter = extract_compiler_config(token_iter, &mut compiler_config);
392
393 let mut tokens = vec![];
394 fill_token_vec(token_iter, &mut tokens);
395
396 let source_file = if let Some(cargo_manifest) = std::env::var_os("CARGO_MANIFEST_DIR") {
397 let mut path: std::path::PathBuf = cargo_manifest.into();
398 path.push("Cargo.toml");
399 diagnostics::SourceFileInner::from_path_only(path)
400 } else {
401 diagnostics::SourceFileInner::from_path_only(Default::default())
402 };
403 let mut diag = BuildDiagnostics::default();
404 let syntax_node = parser::parse_tokens(tokens.clone(), source_file, &mut diag);
405 if diag.has_errors() {
406 return diag.report_macro_diagnostic(&tokens);
407 }
408
409 compiler_config.translation_domain = std::env::var("CARGO_PKG_NAME").ok();
411 let (root_component, diag, loader) =
412 spin_on::spin_on(compile_syntax_node(syntax_node, diag, compiler_config));
413 if diag.has_errors() {
415 return diag.report_macro_diagnostic(&tokens);
416 }
417
418 let mut result = generator::rust::generate(&root_component, &loader.compiler_config)
419 .unwrap_or_else(|e| {
420 let e_str = e.to_string();
421 quote!(compile_error!(#e_str))
422 });
423
424 let reload = diag
426 .all_loaded_files
427 .iter()
428 .filter(|path| path.is_absolute() && !path.ends_with("Cargo.toml"))
429 .filter_map(|p| p.to_str())
430 .map(|p| quote! {const _ : &'static [u8] = ::core::include_bytes!(#p);});
431
432 result.extend(reload);
433 result.extend(quote! {const _ : ::core::option::Option<&'static str> = ::core::option_env!("SLINT_STYLE");});
434
435 let mut result = TokenStream::from(result);
436 if !diag.is_empty() {
437 result.extend(diag.report_macro_diagnostic(&tokens));
438 }
439 result
440}