Skip to main content

vacro_doc_i18n/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use quote::quote;
4use syn::{parse_macro_input, AttrStyle, Attribute, Item, Token};
5
6#[proc_macro_attribute]
7pub fn doc_i18n(_attr: TokenStream, item: TokenStream) -> TokenStream {
8    let mut item_ast = parse_macro_input!(item as Item);
9
10    // 确定语言模式
11    let mode = lang_mode_from_features();
12
13    // 提取所有文档属性
14    let mut attrs = take_attrs_mut(&mut item_ast);
15    let (outer_text, inner_text) = extract_doc_text_split(&attrs);
16
17    // 解析并转换
18    let new_outer = process_doc_syntax(&outer_text, mode);
19    let new_inner = process_doc_syntax(&inner_text, mode);
20
21    // 清理旧属性并写回新属性
22    remove_doc_attrs(&mut attrs);
23
24    push_doc_attrs(&mut attrs, &new_outer, AttrStyle::Outer);
25    push_doc_attrs(
26        &mut attrs,
27        &new_inner,
28        AttrStyle::Inner(Token![!](Span::call_site())),
29    );
30
31    put_attrs_back(&mut item_ast, attrs);
32    TokenStream::from(quote!(#item_ast))
33}
34
35#[derive(Clone, Copy, PartialEq, Debug)]
36enum LangMode {
37    Cn,
38    En,
39    All,
40}
41
42fn lang_mode_from_features() -> LangMode {
43    // 优先级:Doc-All > 指定 CN/EN
44    if cfg!(feature = "doc-all") {
45        LangMode::All
46    } else if cfg!(feature = "doc-cn") {
47        LangMode::Cn
48    } else {
49        LangMode::En
50    }
51}
52
53fn process_doc_syntax(input: &str, mode: LangMode) -> String {
54    let mut out = String::new();
55    let mut current_block_lang: Option<LangMode> = None;
56
57    for line in input.lines() {
58        let trimmed = line.trim();
59
60        // 处理块结束标记 `:::`
61        if trimmed == ":::" {
62            if current_block_lang.is_some() {
63                // 如果是 All 模式,我们需要闭合 div,并强制双换行重置 Markdown 上下文
64                if mode == LangMode::All {
65                    out.push_str("</div>\n\n");
66                }
67                current_block_lang = None;
68            }
69            continue;
70        }
71
72        // 处理块开始标记 `::: @cn` 或 `::: @en`
73        if let Some(lang) = parse_block_start(trimmed) {
74            current_block_lang = Some(lang);
75
76            // 如果是 All 模式,写入开标签,并强制换行
77            if mode == LangMode::All {
78                let class = if lang == LangMode::Cn {
79                    "doc-cn"
80                } else {
81                    "doc-en"
82                };
83                // style="display: block" 配合 \n 确保后续内容被识别为块级
84                out.push_str(&format!(
85                    r#"<div class="{}" style="display: block; margin-bottom: 1em;">{}"#,
86                    class, "\n"
87                ));
88            }
89            continue;
90        }
91
92        // 处理块内内容
93        if let Some(block_lang) = current_block_lang {
94            if mode == LangMode::All || mode == block_lang {
95                out.push_str(line);
96                out.push('\n');
97            }
98            continue;
99        }
100
101        // 处理行内/单行标记 `@cn ...` 或 `@en ...`
102        if let Some((lang, content)) = parse_inline_start(line) {
103            if mode == LangMode::All {
104                // All 模式:包裹 span
105                let class = if lang == LangMode::Cn {
106                    "doc-cn"
107                } else {
108                    "doc-en"
109                };
110                out.push_str(&format!(
111                    r#"<span class="{}">{}</span>{}"#,
112                    class, content, "\n"
113                ));
114            } else if mode == lang {
115                out.push_str(content);
116                out.push('\n');
117            }
118            continue;
119        }
120
121        // 公共内容
122        out.push_str(line);
123        out.push('\n');
124    }
125
126    out
127}
128
129// 解析 `::: @cn` 返回 Some(LangMode::Cn)
130fn parse_block_start(line: &str) -> Option<LangMode> {
131    if !line.starts_with(":::") {
132        return None;
133    }
134    let rest = line[3..].trim();
135    if rest == "@cn" || rest == "@zh" {
136        Some(LangMode::Cn)
137    } else if rest == "@en" {
138        Some(LangMode::En)
139    } else {
140        None
141    }
142}
143
144// 解析 `@cn 这是一段话` -> Some((Cn, "这是一段话"))
145fn parse_inline_start(line: &str) -> Option<(LangMode, &str)> {
146    let trimmed = line.trim_start();
147    if trimmed.starts_with("@cn ") || trimmed.starts_with("@zh ") {
148        let content_start = line.find("@").unwrap() + 3;
149        // 容错处理:如果后面没空格
150        Some((LangMode::Cn, &line[content_start..]))
151    } else if trimmed.starts_with("@en ") {
152        let content_start = line.find("@").unwrap() + 3;
153        Some((LangMode::En, &line[content_start..]))
154    } else {
155        None
156    }
157}
158
159fn take_attrs_mut(item: &mut Item) -> Vec<Attribute> {
160    match item {
161        Item::Const(i) => std::mem::take(&mut i.attrs),
162        Item::Enum(i) => std::mem::take(&mut i.attrs),
163        Item::Fn(i) => std::mem::take(&mut i.attrs),
164        Item::Impl(i) => std::mem::take(&mut i.attrs),
165        Item::Mod(i) => std::mem::take(&mut i.attrs),
166        Item::Struct(i) => std::mem::take(&mut i.attrs),
167        Item::Trait(i) => std::mem::take(&mut i.attrs),
168        Item::Type(i) => std::mem::take(&mut i.attrs),
169        Item::Use(i) => std::mem::take(&mut i.attrs),
170        Item::TraitAlias(i) => std::mem::take(&mut i.attrs),
171        Item::ExternCrate(i) => std::mem::take(&mut i.attrs),
172        Item::ForeignMod(i) => std::mem::take(&mut i.attrs),
173        Item::Union(i) => std::mem::take(&mut i.attrs),
174        Item::Static(i) => std::mem::take(&mut i.attrs),
175        Item::Macro(i) => std::mem::take(&mut i.attrs),
176        _ => vec![],
177    }
178}
179
180fn put_attrs_back(item: &mut Item, attrs: Vec<Attribute>) {
181    match item {
182        Item::Const(i) => i.attrs = attrs,
183        Item::Enum(i) => i.attrs = attrs,
184        Item::Fn(i) => i.attrs = attrs,
185        Item::Impl(i) => i.attrs = attrs,
186        Item::Mod(i) => i.attrs = attrs,
187        Item::Struct(i) => i.attrs = attrs,
188        Item::Trait(i) => i.attrs = attrs,
189        Item::Type(i) => i.attrs = attrs,
190        Item::Use(i) => i.attrs = attrs,
191        Item::TraitAlias(i) => i.attrs = attrs,
192        Item::ExternCrate(i) => i.attrs = attrs,
193        Item::ForeignMod(i) => i.attrs = attrs,
194        Item::Union(i) => i.attrs = attrs,
195        Item::Static(i) => i.attrs = attrs,
196        Item::Macro(i) => i.attrs = attrs,
197        _ => {}
198    }
199}
200
201fn extract_doc_text_split(attrs: &[Attribute]) -> (String, String) {
202    let mut outer = String::new();
203    let mut inner = String::new();
204    for a in attrs {
205        if !a.path().is_ident("doc") {
206            continue;
207        }
208        let mut content = String::new();
209        if let Ok(s) = a.parse_args::<syn::LitStr>() {
210            content = s.value();
211        } else if let syn::Meta::NameValue(nv) = &a.meta {
212            if let syn::Expr::Lit(expr_lit) = &nv.value {
213                if let syn::Lit::Str(litstr) = &expr_lit.lit {
214                    content = litstr.value();
215                }
216            }
217        }
218        // 保留空行,这对 Markdown 很重要
219        match a.style {
220            AttrStyle::Outer => {
221                outer.push_str(&content);
222                outer.push('\n');
223            }
224            AttrStyle::Inner(_) => {
225                inner.push_str(&content);
226                inner.push('\n');
227            }
228        }
229    }
230    (outer, inner)
231}
232
233fn remove_doc_attrs(attrs: &mut Vec<Attribute>) {
234    attrs.retain(|a| !a.path().is_ident("doc"));
235}
236
237fn push_doc_attrs(attrs: &mut Vec<Attribute>, text: &str, style: AttrStyle) {
238    // 允许空行,保留空行
239    for line in text.split('\n') {
240        let lit = syn::LitStr::new(line, proc_macro2::Span::call_site());
241        let attr: Attribute = match style {
242            AttrStyle::Outer => syn::parse_quote!(#[doc = #lit]),
243            AttrStyle::Inner(_) => syn::parse_quote!(#![doc = #lit]),
244        };
245        attrs.push(attr);
246    }
247}