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