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