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 let mode = lang_mode_from_features();
12
13 let mut attrs = take_attrs_mut(&mut item_ast);
15 let (outer_text, inner_text) = extract_doc_text_split(&attrs);
16
17 let new_outer = process_doc_syntax(&outer_text, mode);
19 let new_inner = process_doc_syntax(&inner_text, mode);
20
21 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)),
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 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 if trimmed == ":::" {
62 if current_block_lang.is_some() {
63 if mode == LangMode::All {
65 out.push_str("</div>\n\n");
66 }
67 current_block_lang = None;
68 }
69 continue;
70 }
71
72 if let Some(lang) = parse_block_start(trimmed) {
74 current_block_lang = Some(lang);
75
76 if mode == LangMode::All {
78 let class = if lang == LangMode::Cn {
79 "doc-cn"
80 } else {
81 "doc-en"
82 };
83 out.push_str(&format!(
85 r#"<div class="{}" style="display: block; margin-bottom: 1em;">{}"#,
86 class, "\n"
87 ));
88 }
89 continue;
90 }
91
92 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 if let Some((lang, content)) = parse_inline_start(line) {
103 if mode == LangMode::All {
104 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 out.push_str(line);
123 out.push('\n');
124 }
125
126 out
127}
128
129fn 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
144fn 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 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 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 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}