11use crate :: ast_utils:: expand_ts_as_expr;
2+ use crate :: options:: LinguiOptions ;
23use crate :: tokens:: { CaseOrOffset , IcuChoice , MsgToken } ;
34use std:: collections:: HashSet ;
45use swc_core:: {
5- common:: { SyntaxContext , DUMMY_SP } ,
6+ common:: { EqIgnoreSpan , SyntaxContext , DUMMY_SP } ,
67 ecma:: ast:: * ,
78} ;
89
10+ static NUMERIC_REGEX : once_cell:: sync:: Lazy < regex:: Regex > =
11+ once_cell:: sync:: Lazy :: new ( || regex:: Regex :: new ( r"^\d+$" ) . unwrap ( ) ) ;
12+ static VALID_NAME_REGEX : once_cell:: sync:: Lazy < regex:: Regex > =
13+ once_cell:: sync:: Lazy :: new ( || regex:: Regex :: new ( r"^[a-zA-Z_]([\w.-]*\w)?$" ) . unwrap ( ) ) ;
14+
915fn dedup_values ( mut v : Vec < ValueWithPlaceholder > ) -> Vec < ValueWithPlaceholder > {
1016 let mut uniques = HashSet :: new ( ) ;
1117 v. retain ( |e| uniques. insert ( e. placeholder . clone ( ) ) ) ;
@@ -20,10 +26,21 @@ pub struct ValueWithPlaceholder {
2026
2127impl ValueWithPlaceholder {
2228 pub fn into_prop ( self ) -> PropOrSpread {
23- let ident = IdentName :: new ( self . placeholder . into ( ) , DUMMY_SP ) ;
29+ let key = if self . placeholder . contains ( '-' ) || self . placeholder . contains ( '.' ) {
30+ PropName :: Computed ( ComputedPropName {
31+ span : DUMMY_SP ,
32+ expr : Box :: new ( Expr :: Lit ( Lit :: Str ( Str {
33+ span : DUMMY_SP ,
34+ value : self . placeholder . clone ( ) . into ( ) ,
35+ raw : None ,
36+ } ) ) ) ,
37+ } )
38+ } else {
39+ PropName :: Ident ( IdentName :: new ( self . placeholder . into ( ) , DUMMY_SP ) )
40+ } ;
2441
2542 PropOrSpread :: Prop ( Box :: new ( Prop :: KeyValue ( KeyValueProp {
26- key : PropName :: Ident ( ident ) ,
43+ key,
2744 value : self . value ,
2845 } ) ) )
2946 }
@@ -36,24 +53,31 @@ pub struct MessageBuilderResult {
3653 pub components : Option < Box < Expr > > ,
3754}
3855
39- pub struct MessageBuilder {
56+ pub struct MessageBuilder < ' a > {
4057 message : String ,
4158
42- components_stack : Vec < usize > ,
59+ components_stack : Vec < String > ,
4360 components : Vec < ValueWithPlaceholder > ,
4461
4562 values : Vec < ValueWithPlaceholder > ,
4663 values_indexed : Vec < ValueWithPlaceholder > ,
64+
65+ options : & ' a LinguiOptions ,
66+ elements_tracking : Vec < ( String , JSXOpeningElement ) > ,
67+ element_index : usize ,
4768}
4869
49- impl MessageBuilder {
50- pub fn parse ( tokens : Vec < MsgToken > ) -> MessageBuilderResult {
70+ impl < ' a > MessageBuilder < ' a > {
71+ pub fn parse ( tokens : Vec < MsgToken > , options : & ' a LinguiOptions ) -> MessageBuilderResult {
5172 let mut builder = MessageBuilder {
5273 message : String :: new ( ) ,
5374 components_stack : Vec :: new ( ) ,
5475 components : Vec :: new ( ) ,
5576 values : Vec :: new ( ) ,
5677 values_indexed : Vec :: new ( ) ,
78+ options,
79+ elements_tracking : Vec :: new ( ) ,
80+ element_index : 0 ,
5781 } ;
5882
5983 builder. process_tokens ( tokens) ;
@@ -133,30 +157,143 @@ impl MessageBuilder {
133157 self . message . push_str ( val) ;
134158 }
135159
136- fn push_tag_opening ( & mut self , el : JSXOpeningElement , self_closing : bool ) {
137- let current = self . components . len ( ) ;
160+ fn push_tag_opening ( & mut self , mut el : JSXOpeningElement , self_closing : bool ) {
161+ let mut base_name: Option < String > = None ;
162+
163+ if let Some ( attr_name) = & self . options . jsx_placeholder_attribute {
164+ if let Some ( idx) = el. attrs . iter ( ) . position ( |a| {
165+ if let JSXAttrOrSpread :: JSXAttr ( attr) = a {
166+ if let JSXAttrName :: Ident ( ident) = & attr. name {
167+ return & ident. sym == attr_name;
168+ }
169+ }
170+ false
171+ } ) {
172+ let attr = el. attrs . remove ( idx) ;
173+ if let JSXAttrOrSpread :: JSXAttr ( attr) = attr {
174+ let mut is_valid = false ;
175+ if let Some ( JSXAttrValue :: Str ( s) ) = attr. value {
176+ let val = s. value . to_string_lossy ( ) . into_owned ( ) ;
177+ if !val. is_empty ( ) {
178+ base_name = Some ( val) ;
179+ is_valid = true ;
180+ }
181+ }
182+
183+ if !is_valid {
184+ swc_core:: plugin:: errors:: HANDLER . with ( |h| {
185+ h. struct_span_err (
186+ el. span ,
187+ & format ! ( "The `{attr_name}` attribute must be a non-empty string literal." ) ,
188+ ) . emit ( ) ;
189+ } ) ;
190+ }
191+ }
192+ }
193+ }
194+
195+ if base_name. is_none ( ) {
196+ if let Some ( defaults) = & self . options . jsx_placeholder_defaults {
197+ if let JSXElementName :: Ident ( ident) = & el. name {
198+ if let Some ( def) = defaults. get ( & ident. sym . to_string ( ) ) {
199+ base_name = Some ( def. clone ( ) ) ;
200+ }
201+ }
202+ }
203+ }
204+
205+ let name = if let Some ( n) = base_name {
206+ if NUMERIC_REGEX . is_match ( & n) {
207+ swc_core:: plugin:: errors:: HANDLER . with ( |h| {
208+ h. struct_span_err (
209+ el. span ,
210+ & format ! ( "Placeholder name `{n}` is not allowed because it conflicts with auto-generated numeric placeholders. Use a non-numeric name instead." ) ,
211+ ) . emit ( ) ;
212+ } ) ;
213+ } else if !VALID_NAME_REGEX . is_match ( & n) {
214+ swc_core:: plugin:: errors:: HANDLER . with ( |h| {
215+ h. struct_span_err (
216+ el. span ,
217+ & format ! ( "Placeholder name `{n}` is not valid. Names must start and end with a letter/digit/underscore, but may contain `.-` in between." ) ,
218+ ) . emit ( ) ;
219+ } ) ;
220+ }
221+
222+ if let Some ( ( _, orig_el) ) = self . elements_tracking . iter ( ) . find ( |( k, _) | k == & n) {
223+ let has_spreads = orig_el
224+ . attrs
225+ . iter ( )
226+ . any ( |a| matches ! ( a, JSXAttrOrSpread :: SpreadElement ( _) ) ) ;
227+ let attrs_equal = if orig_el. attrs . len ( ) == el. attrs . len ( ) {
228+ if has_spreads {
229+ orig_el
230+ . attrs
231+ . iter ( )
232+ . zip ( el. attrs . iter ( ) )
233+ . all ( |( a, b) | a. eq_ignore_span ( b) )
234+ } else {
235+ orig_el
236+ . attrs
237+ . iter ( )
238+ . all ( |a| el. attrs . iter ( ) . any ( |b| a. eq_ignore_span ( b) ) )
239+ }
240+ } else {
241+ false
242+ } ;
243+
244+ let tags_equal = el. name . eq_ignore_span ( & orig_el. name ) ;
245+
246+ if !tags_equal || !attrs_equal {
247+ swc_core:: plugin:: errors:: HANDLER . with ( |h| {
248+ let attr_name = self . options . jsx_placeholder_attribute . as_deref ( ) . unwrap_or ( "_t" ) ;
249+ let eg = format ! ( "(e.g. `<element {attr_name}=\" newName\" />`)" ) ;
250+ let msg = format ! (
251+ "Multiple distinct JSX elements with the same placeholder name (`{n}`). Differentiate them by {} {eg}." ,
252+ if self . options. jsx_placeholder_attribute. is_some( ) {
253+ format!( "adding/modifying the `{attr_name}` attribute" )
254+ } else {
255+ "setting `macro.jsxPlaceholderAttribute` in the lingui config and then adding the attribute to your JSX elements" . to_string( )
256+ }
257+ ) ;
258+ h. struct_span_err ( el. span , & msg) . emit ( ) ;
259+ } ) ;
260+ }
261+ } else {
262+ self . elements_tracking . push ( ( n. clone ( ) , el. clone ( ) ) ) ;
263+ }
264+
265+ n
266+ } else {
267+ let n = self . element_index . to_string ( ) ;
268+ self . element_index += 1 ;
269+ self . elements_tracking . push ( ( n. clone ( ) , el. clone ( ) ) ) ;
270+ n
271+ } ;
272+
138273 if self_closing {
139- self . push_msg ( & format ! ( "<{current }/>" ) ) ;
274+ self . push_msg ( & format ! ( "<{name }/>" ) ) ;
140275 } else {
141- self . components_stack . push ( current ) ;
142- self . push_msg ( & format ! ( "<{current }>" ) ) ;
276+ self . components_stack . push ( name . clone ( ) ) ;
277+ self . push_msg ( & format ! ( "<{name }>" ) ) ;
143278 }
144279
145- // todo: it looks very dirty and bad to cloning this jsx values
146- self . components . push ( ValueWithPlaceholder {
147- placeholder : self . components . len ( ) . to_string ( ) ,
148- value : Box :: new ( Expr :: JSXElement ( Box :: new ( JSXElement {
149- opening : el,
150- closing : None ,
151- children : vec ! [ ] ,
152- span : DUMMY_SP ,
153- } ) ) ) ,
154- } ) ;
280+ if !self . components . iter ( ) . any ( |c| c. placeholder == name) {
281+ // todo: it looks very dirty and bad to cloning this jsx values
282+ self . components . push ( ValueWithPlaceholder {
283+ placeholder : name. clone ( ) ,
284+ value : Box :: new ( Expr :: JSXElement ( Box :: new ( JSXElement {
285+ opening : el,
286+ closing : None ,
287+ children : vec ! [ ] ,
288+ span : DUMMY_SP ,
289+ } ) ) ) ,
290+ } ) ;
291+ }
155292 }
156293
157294 fn push_tag_closing ( & mut self ) {
158- if let Some ( index ) = self . components_stack . pop ( ) {
159- self . push_msg ( & format ! ( "</{index }>" ) ) ;
295+ if let Some ( name ) = self . components_stack . pop ( ) {
296+ self . push_msg ( & format ! ( "</{name }>" ) ) ;
160297 } else {
161298 // todo JSX tags mismatch. write tests for tags mismatch, swc should not crash in that case
162299 }
0 commit comments