ratatui-kit-macros 0.6.0

ratatui-kit macros
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
use proc_macro2::Span;
use quote::{ToTokens, quote};
use syn::{
    Expr, FieldValue, Ident, Member, Pat, Token, TypePath, braced, parse::Parse,
    parse::ParseStream, punctuated::Punctuated, spanned::Spanned, token::Comma,
};
use uuid::Uuid;

use crate::adapter::ParsedAdapter;

// 单个子节点:嵌套元素 / adapter / 任意表达式、或一等控制流(if/for/match)。
//
// `pub(crate)`:`ParsedElementHead::to_element_expr` 以 `&[ParsedElementChild]` 接收
// children 参数,该方法对 `router.rs` 可见(`pub(crate)`),故本类型也需 crate 级可见。
pub(crate) enum ParsedElementChild {
    Element(ElementOrAdapter),
    Expr(Expr),
    // ControlFlow 装箱:If/For 内联持有 syn 的 Expr/Pat,是本枚举最大的变体,
    // 不装箱会触发 clippy::large_enum_variant(parse AST,装箱成本可忽略)。
    ControlFlow(Box<ControlFlow>),
}

// element! 子节点块内的一等控制流。分支体本身又是一组子节点。
//
// 相比把条件渲染塞进表达式插槽,一等控制流让每个分支独立把自己的
// 子节点 `extend` 进 children——故各分支可返回不同元素类型,无需 `.into_any()` 统一类型。
//
// `pub(crate)`:随 [`ParsedElementChild`] 经 `to_element_expr` 的 crate 级签名传染而来。
pub(crate) enum ControlFlow {
    If {
        cond: Expr,
        then_branch: Vec<ParsedElementChild>,
        else_branch: Option<Box<ElseBranch>>,
    },
    For {
        pat: Pat,
        expr: Expr,
        body: Vec<ParsedElementChild>,
    },
    Match {
        expr: Expr,
        arms: Vec<MatchArm>,
    },
}

// `else if ...` 或 `else { ... }`。
pub(crate) enum ElseBranch {
    If(Box<ControlFlow>),
    Block(Vec<ParsedElementChild>),
}

pub(crate) struct MatchArm {
    pat: Pat,
    guard: Option<Expr>,
    body: Vec<ParsedElementChild>,
}

// 解析一段子节点序列(用于元素的 `{}` 块,以及控制流的各分支体)。
fn parse_children(input: ParseStream) -> syn::Result<Vec<ParsedElementChild>> {
    let mut children = Vec::new();
    while !input.is_empty() {
        if input.peek(Token![if]) {
            children.push(ParsedElementChild::ControlFlow(Box::new(parse_if(input)?)));
        } else if input.peek(Token![for]) {
            children.push(ParsedElementChild::ControlFlow(Box::new(parse_for(input)?)));
        } else if input.peek(Token![match]) {
            children.push(ParsedElementChild::ControlFlow(Box::new(parse_match(
                input,
            )?)));
        } else if input.peek(syn::token::Brace) {
            // `{ expr }`:把子节点位置交还给任意 Rust 表达式(返回 Option/Vec/Iterator/Element)。
            children.push(ParsedElementChild::Expr(input.parse()?));
        } else {
            // 嵌套元素 `Comp(..){..}` 或 `widget(...)` / `stateful(...)` 适配器。
            children.push(ParsedElementChild::Element(input.parse()?));
        }
    }
    Ok(children)
}

fn parse_if(input: ParseStream) -> syn::Result<ControlFlow> {
    input.parse::<Token![if]>()?;
    // parse_without_eager_brace:把后续 `{` 当作分支体起始而非条件表达式的一部分;
    // 同时支持 `if let PAT = EXPR` 这类 let 条件。
    let cond = Expr::parse_without_eager_brace(input)?;
    let content;
    braced!(content in input);
    let then_branch = parse_children(&content)?;

    let else_branch = if input.peek(Token![else]) {
        input.parse::<Token![else]>()?;
        if input.peek(Token![if]) {
            Some(Box::new(ElseBranch::If(Box::new(parse_if(input)?))))
        } else {
            let content;
            braced!(content in input);
            Some(Box::new(ElseBranch::Block(parse_children(&content)?)))
        }
    } else {
        None
    };

    Ok(ControlFlow::If {
        cond,
        then_branch,
        else_branch,
    })
}

fn parse_for(input: ParseStream) -> syn::Result<ControlFlow> {
    input.parse::<Token![for]>()?;
    let pat = Pat::parse_single(input)?;
    input.parse::<Token![in]>()?;
    let expr = Expr::parse_without_eager_brace(input)?;
    let content;
    braced!(content in input);
    let body = parse_children(&content)?;
    Ok(ControlFlow::For { pat, expr, body })
}

fn parse_match(input: ParseStream) -> syn::Result<ControlFlow> {
    input.parse::<Token![match]>()?;
    let expr = Expr::parse_without_eager_brace(input)?;
    let content;
    braced!(content in input);
    let mut arms = Vec::new();
    while !content.is_empty() {
        // 分支模式支持 `A | B`,故用 parse_multi。
        let pat = Pat::parse_multi(&content)?;
        let guard = if content.peek(Token![if]) {
            content.parse::<Token![if]>()?;
            Some(content.parse::<Expr>()?)
        } else {
            None
        };
        content.parse::<Token![=>]>()?;
        // 分支体要求用 `{}` 包裹(里面是一组子节点)。
        let body_content;
        braced!(body_content in content);
        let body = parse_children(&body_content)?;
        if content.peek(Token![,]) {
            content.parse::<Token![,]>()?;
        }
        arms.push(MatchArm { pat, guard, body });
    }
    Ok(ControlFlow::Match { expr, arms })
}

impl ParsedElementChild {
    // 生成「把本子节点 extend 进 `dest`」的语句。控制流会把内层 extend 包进 if/for/match。
    fn to_extend(&self, dest: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
        match self {
            ParsedElementChild::Element(element) => {
                quote!(::ratatui_kit::extend_with_elements(&mut #dest, #element);)
            }
            // Expr 形如块 `{ ... }`:先绑定到局部再 extend——避免把 `{ expr }` 直接做实参
            // 触发 clippy::unnecessary_braces,同时允许块内写多条语句。
            ParsedElementChild::Expr(expr) => quote!({
                let _child = #expr;
                ::ratatui_kit::extend_with_elements(&mut #dest, _child);
            }),
            // ControlFlow 递归把内层 extend 包进 if/for/match 外壳。
            ParsedElementChild::ControlFlow(cf) => cf.to_extend(dest),
        }
    }
}

impl ControlFlow {
    fn to_extend(&self, dest: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
        match self {
            ControlFlow::If {
                cond,
                then_branch,
                else_branch,
            } => {
                let then_stmts = then_branch.iter().map(|c| c.to_extend(dest));
                let else_tokens = match else_branch {
                    None => quote!(),
                    Some(b) => match &**b {
                        ElseBranch::Block(children) => {
                            let stmts = children.iter().map(|c| c.to_extend(dest));
                            quote!(else { #(#stmts)* })
                        }
                        // 递归:内层 If 的 to_extend 以 `if ...` 起始,前缀 `else ` 即得 `else if ...`。
                        ElseBranch::If(inner) => {
                            let inner_tokens = inner.to_extend(dest);
                            quote!(else #inner_tokens)
                        }
                    },
                };
                quote!(if #cond { #(#then_stmts)* } #else_tokens)
            }
            ControlFlow::For { pat, expr, body } => {
                let stmts = body.iter().map(|c| c.to_extend(dest));
                quote!(for #pat in #expr { #(#stmts)* })
            }
            ControlFlow::Match { expr, arms } => {
                let arm_tokens = arms.iter().map(|arm| {
                    let MatchArm { pat, guard, body } = arm;
                    let stmts = body.iter().map(|c| c.to_extend(dest));
                    let guard_tokens = match guard {
                        Some(g) => quote!(if #g),
                        None => quote!(),
                    };
                    quote!(#pat #guard_tokens => { #(#stmts)* })
                });
                quote!(match #expr { #(#arm_tokens)* })
            }
        }
    }
}

pub enum PropsItem {
    FieldValue(FieldValue),
    Rest(Expr),
}

impl Parse for PropsItem {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        if input.peek(Token![..]) {
            input.parse::<Token![..]>()?;
            let rest_expr: Expr = input.parse()?;
            Ok(PropsItem::Rest(rest_expr))
        } else {
            let field_value: FieldValue = input.parse()?;
            Ok(PropsItem::FieldValue(field_value))
        }
    }
}

impl ToTokens for PropsItem {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        match self {
            PropsItem::FieldValue(field_value) => {
                let mut field_value = field_value.clone();
                let expr = &field_value.expr;
                field_value.expr = syn::parse2(quote!((#expr).into())).unwrap();
                tokens.extend(quote!(#field_value))
            }
            PropsItem::Rest(expr) => {
                tokens.extend(quote!(..#expr));
            }
        }
    }
}

impl PropsItem {
    pub fn span(&self) -> Span {
        match self {
            PropsItem::FieldValue(field_value) => field_value.span(),
            PropsItem::Rest(expr) => expr.span(),
        }
    }

    // 若本项是保留的 `key:` 字段(元素身份键),返回其 `FieldValue`。
    //
    // 单一真源:`ToTokens` 的 key 构造与 props 过滤、`ParsedElement::key_span` 都经此查找——
    // 避免「`Member::Named("key")` 匹配 + 魔法串 `"key"`」散落多处、改名时需多处同步。
    fn as_key_field(&self) -> Option<&FieldValue> {
        match self {
            PropsItem::FieldValue(fv) if matches!(&fv.member, Member::Named(ident) if ident == "key") => {
                Some(fv)
            }
            _ => None,
        }
    }
}

// element 的「头部」:类型路径 + 可选 `(props)`,**不含 children**。
//
// 把「头部解析 + element codegen」与「children」在类型上分离——`element!` 与 `routes!`
// 都基于 head 构建,但 `{}` 的归属由各自决定(`element!` 当子节点、`routes!` 当子路由)。
// head 没有 children 字段,故「解析阶段触及 `{}`」在类型层面无法表达,无需注释约定护栏。
pub struct ParsedElementHead {
    ty: TypePath,
    props: Punctuated<PropsItem, Comma>,
}

impl Parse for ParsedElementHead {
    // 只解析类型路径 + 可选 `(props)`。**不 peek/消费 `Brace`**——`{}` 留给调用方。
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let ty: TypePath = input.parse()?;
        let props = if input.peek(syn::token::Paren) {
            let props_input;
            syn::parenthesized!(props_input in input);
            Punctuated::parse_terminated(&props_input)?
        } else {
            Punctuated::new()
        };

        let rest_position = props
            .iter()
            .position(|item| matches!(item, PropsItem::Rest(_)));

        if let Some(pos) = rest_position
            && pos != props.len() - 1
        {
            return Err(syn::Error::new(
                props[pos].span(),
                "the rest property must be the last item",
            ));
        }

        Ok(Self { ty, props })
    }
}

impl ParsedElementHead {
    // 返回 `key:` 字段的 span(若存在)。`routes!` 借此拒绝路由元素上的 `key:`——
    // 路由身份由 path 决定,元素 key 在路由场景下无意义(详见 `router.rs`)。
    // 仅 `routes!`(router 特性)调用,故随 router 特性门控,避免无特性时的 dead_code 警告。
    #[cfg(feature = "router")]
    pub fn key_span(&self) -> Option<Span> {
        self.props
            .iter()
            .find_map(PropsItem::as_key_field)
            .map(|fv| fv.member.span())
    }

    // 生成构造 `Element<Ty>` 的表达式 token——element codegen 的**单一真源**。
    //
    // `children` 作为参数注入(而非读取持有状态):`element!` 传实际子节点切片,
    // `routes!` 传空切片。输出**带外层括号**的块表达式 `({ … _element })`,使调用方
    // 可直接 `.into_any()` 或作为实参,无需自己补括号、无需知道内部是块——token 形状
    // 知识收归本模块,`router.rs` 不再依赖它。
    pub(crate) fn to_element_expr(
        &self,
        children: &[ParsedElementChild],
    ) -> proc_macro2::TokenStream {
        let ty = &self.ty;
        let decl_key = Uuid::new_v4().as_u128();
        let has_rest = self
            .props
            .iter()
            .any(|item| matches!(item, PropsItem::Rest(_)));
        // 有用户 `key:` → User((decl_key, expr)) 单次堆分配;否则 Decl(decl_key) 零堆分配。
        let key = self
            .props
            .iter()
            .find_map(PropsItem::as_key_field)
            .map(|fv| {
                let expr = &fv.expr;
                quote!(::ratatui_kit::ElementKey::user((#decl_key, #expr)))
            })
            .unwrap_or_else(|| quote!(::ratatui_kit::ElementKey::decl(#decl_key)));

        let props_assignments = self
            .props
            .iter()
            .filter(|item| item.as_key_field().is_none())
            .map(|props_item| quote!(#props_item))
            .collect::<Vec<_>>();

        let set_children = if !children.is_empty() {
            let dest = quote!(_element.props.children);
            let stmts = children.iter().map(|child| child.to_extend(&dest));
            Some(quote! {
                #(#stmts)*
            })
        } else {
            None
        };

        let has_props_assignments = !props_assignments.is_empty();

        let default_rest = if has_rest {
            quote! {
                #(#props_assignments),*
            }
        } else {
            quote! {
                #(#props_assignments,)*
                ..Default::default()
            }
        };

        let element_binding = if set_children.is_some() {
            quote!(let mut _element=::ratatui_kit::Element::<#ty>{
                key: #key,
                props: _props,
            };)
        } else {
            quote!(let _element=::ratatui_kit::Element::<#ty>{
                key: #key,
                props: _props,
            };)
        };

        // 外层括号 load-bearing:块表达式 `{ … }` 须加括号方能在实参位继续 `.into_any()`。
        if has_props_assignments {
            quote! {
                ({
                    type Props<'a>= <#ty as ::ratatui_kit::ElementType>::Props<'a>;
                    // 用户填满全部字段时,兜底的 `..Default::default()` 会触发 needless_update;
                    // element! 统一以 Default 补未填字段,此处多余属预期(宏无从得知字段总数),显式 allow。
                    #[allow(clippy::needless_update)]
                    let _props = Props{
                        #default_rest
                    };

                    #element_binding
                    #set_children
                    _element
                })
            }
        } else {
            quote! {
                ({
                    type Props<'a>= <#ty as ::ratatui_kit::ElementType>::Props<'a>;
                    let _props = Props::default();
                    #element_binding
                    #set_children
                    _element
                })
            }
        }
    }
}

// 完整的声明式元素:头部 + 子节点。`element!` 用,`ToTokens` 委托 head 的 codegen。
pub struct ParsedElement {
    head: ParsedElementHead,
    children: Vec<ParsedElementChild>,
}

impl Parse for ParsedElement {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let head = input.parse::<ParsedElementHead>()?;
        let children = if input.peek(syn::token::Brace) {
            let children_input;
            braced!(children_input in input);
            parse_children(&children_input)?
        } else {
            Vec::new()
        };
        Ok(Self { head, children })
    }
}

impl ToTokens for ParsedElement {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        tokens.extend(self.head.to_element_expr(&self.children));
    }
}

pub enum ElementOrAdapter {
    Element(ParsedElement),
    // Adapter 装箱:ParsedAdapter 含两个 syn::Expr(stateful 的 widget+state),内联较大,
    // 不装箱会使本枚举因变体大小失衡触发 clippy::large_enum_variant。
    Adapter(Box<ParsedAdapter>),
}

impl Parse for ElementOrAdapter {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        if input.peek(Ident) {
            let fork = input.fork();
            let ident: Ident = fork.parse()?;
            let ident = ident.to_string();
            if matches!(ident.as_str(), "widget" | "stateful") && fork.peek(syn::token::Paren) {
                let adapter: ParsedAdapter = input.parse()?;
                return Ok(ElementOrAdapter::Adapter(Box::new(adapter)));
            }
        }

        if input.peek(Token![$]) {
            return Err(input.error(
                "`$` adapter syntax was removed; use `widget(...)` or `stateful(widget, state)`",
            ));
        }

        if input.peek(Token![#]) {
            return Err(input.error("`#(expr)` child syntax was removed; use `{ expr }`"));
        }

        let element: ParsedElement = input.parse()?;
        Ok(ElementOrAdapter::Element(element))
    }
}

impl ToTokens for ElementOrAdapter {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        match self {
            ElementOrAdapter::Element(element) => element.to_tokens(tokens),
            ElementOrAdapter::Adapter(adapter) => adapter.to_tokens(tokens),
        }
    }
}