@@ -7,6 +7,170 @@ const floatRegex = /^-?\d+(\.\d+)?$/;
77
88const rescriptKeywords = new Set ( [ "type" , "open" , "as" , "in" ] ) ;
99
10+ type Rule < T extends Node = Node > = {
11+ match : ( node : T , parent : Node | null ) => boolean ;
12+ transform : ( node : T , parent : Node | null , magicString : MagicString ) => void ;
13+ stopAfterMatch ?: boolean ; // If true, stop applying further rules after this one matches
14+ } ;
15+
16+ // Single quotes to double quotes
17+ const singleQuotesToDouble : Rule < Node > = {
18+ match : ( node ) =>
19+ node . type === "JSXAttribute" &&
20+ node . value ?. type === "Literal" &&
21+ typeof node . value . raw === "string" &&
22+ node . value . raw . startsWith ( "'" ) ,
23+ transform : ( node , _ , magicString ) => {
24+ const attr = node as Extract < Node , { type : "JSXAttribute" } > ;
25+ const value = attr . value as Extract < typeof attr . value , { type : "Literal" } > ;
26+ magicString . update ( value . start , value . end , `"${ value . raw ! . slice ( 1 , - 1 ) } "` ) ;
27+ } ,
28+ } ;
29+
30+ // SVG width/height numeric to string
31+ const svgWidthHeightToString : Rule < Node > = {
32+ match : ( node , parent ) =>
33+ node . type === "JSXAttribute" &&
34+ parent ?. type === "JSXOpeningElement" &&
35+ parent . name . type === "JSXIdentifier" &&
36+ parent . name . name . toLowerCase ( ) === "svg" &&
37+ node . name . type === "JSXIdentifier" &&
38+ ( node . name . name === "width" || node . name . name === "height" ) &&
39+ node . value ?. type === "JSXExpressionContainer" &&
40+ node . value . expression ?. type === "Literal" &&
41+ typeof node . value . expression . value === "number" ,
42+ transform : ( node , _ , magicString ) => {
43+ const attr = node as Extract < Node , { type : "JSXAttribute" } > ;
44+ const value = attr . value as Extract <
45+ typeof attr . value ,
46+ { type : "JSXExpressionContainer" }
47+ > ;
48+ const expression = value . expression as Extract <
49+ typeof value . expression ,
50+ { type : "Literal" }
51+ > ;
52+ const numericValue = String ( expression . value ) ;
53+ magicString . update ( value . start , value . end , `"${ numericValue } "` ) ;
54+ } ,
55+ } ;
56+
57+ // Rescript keywords get underscore suffix
58+ const rescriptKeywordUnderscore : Rule < Node > = {
59+ match : ( node ) =>
60+ node . type === "JSXAttribute" &&
61+ node . name . type === "JSXIdentifier" &&
62+ rescriptKeywords . has ( node . name . name ) ,
63+ transform : ( node , _ , magicString ) => {
64+ const attr = node as Extract < Node , { type : "JSXAttribute" } > ;
65+ magicString . appendRight ( attr . name . end , "_" ) ;
66+ } ,
67+ } ;
68+
69+ // aria- attributes to camelCase
70+ const ariaToCamelCase : Rule < Node > = {
71+ match : ( node ) =>
72+ node . type === "JSXAttribute" &&
73+ node . name . type === "JSXIdentifier" &&
74+ typeof node . name . name === "string" &&
75+ node . name . name . startsWith ( "aria-" ) ,
76+ transform : ( node , _ , magicString ) => {
77+ const attr = node as Extract < Node , { type : "JSXAttribute" } > ;
78+ const name = attr . name . name as string ;
79+ magicString . update (
80+ attr . name . start + 4 ,
81+ attr . name . start + 6 ,
82+ name [ 5 ] ?. toUpperCase ( ) || "" ,
83+ ) ;
84+ } ,
85+ } ;
86+
87+ // data-testid to dataTestId
88+ const dataTestIdToCamelCase : Rule < Node > = {
89+ match : ( node ) =>
90+ node . type === "JSXAttribute" &&
91+ node . name . type === "JSXIdentifier" &&
92+ node . name . name === "data-testid" ,
93+ transform : ( node , _ , magicString ) => {
94+ const attr = node as Extract < Node , { type : "JSXAttribute" } > ;
95+ magicString . update ( attr . name . start , attr . name . end , "dataTestId" ) ;
96+ } ,
97+ } ;
98+
99+ // Null values become =true
100+ const nullValueToTrue : Rule < Node > = {
101+ match : ( node ) => node . type === "JSXAttribute" && node . value === null ,
102+ transform : ( node , _ , magicString ) => {
103+ magicString . appendRight ( node . end , "=true" ) ;
104+ } ,
105+ } ;
106+
107+ // Integer text nodes
108+ const integerTextNode : Rule < Node > = {
109+ match : ( node ) =>
110+ node . type === "JSXText" &&
111+ typeof node . raw === "string" &&
112+ integerRegex . test ( node . raw . trim ( ) ) ,
113+ transform : ( node , _ , magicString ) => {
114+ magicString . prependLeft ( node . start , "{React.int(" ) ;
115+ magicString . appendRight ( node . end , ")}" ) ;
116+ } ,
117+ stopAfterMatch : true ,
118+ } ;
119+
120+ // Float text nodes
121+ const floatTextNode : Rule < Node > = {
122+ match : ( node ) =>
123+ node . type === "JSXText" &&
124+ typeof node . raw === "string" &&
125+ floatRegex . test ( node . raw . trim ( ) ) ,
126+ transform : ( node , _ , magicString ) => {
127+ magicString . prependLeft ( node . start , "{React.float(" ) ;
128+ magicString . appendRight ( node . end , ")}" ) ;
129+ } ,
130+ stopAfterMatch : true ,
131+ } ;
132+
133+ // String text nodes
134+ const stringTextNode : Rule < Node > = {
135+ match : ( node ) =>
136+ node . type === "JSXText" &&
137+ typeof node . value === "string" &&
138+ node . value . trim ( ) !== "" ,
139+ transform : ( node , _ , magicString ) => {
140+ magicString . prependLeft ( node . start , '{React.string("' ) ;
141+ magicString . appendRight ( node . end , '")}' ) ;
142+ } ,
143+ stopAfterMatch : true ,
144+ } ;
145+
146+ const rules : Rule < Node > [ ] = [
147+ singleQuotesToDouble ,
148+ svgWidthHeightToString ,
149+ rescriptKeywordUnderscore ,
150+ ariaToCamelCase ,
151+ dataTestIdToCamelCase ,
152+ nullValueToTrue ,
153+ integerTextNode ,
154+ floatTextNode ,
155+ stringTextNode ,
156+ ] ;
157+
158+ function applyRules (
159+ node : Node ,
160+ parent : Node | null ,
161+ rules : Rule < Node > [ ] ,
162+ magicString : MagicString ,
163+ ) : void {
164+ for ( const rule of rules ) {
165+ if ( rule . match ( node , parent ) ) {
166+ rule . transform ( node , parent , magicString ) ;
167+ if ( rule . stopAfterMatch ) {
168+ break ;
169+ }
170+ }
171+ }
172+ }
173+
10174export function transformJsx ( input : string ) : string {
11175 const magicString = new MagicString ( input ) ;
12176 const parseResult = parseSync ( "clipboard-input.tsx" , input , {
@@ -15,53 +179,8 @@ export function transformJsx(input: string): string {
15179 } ) ;
16180
17181 walk ( parseResult . program , {
18- enter : ( node : Node ) => {
19- if ( node . type === "JSXAttribute" ) {
20- if ( node . value ?. type === "Literal" && node . value . raw ?. startsWith ( "'" ) ) {
21- magicString . update (
22- node . value . start ,
23- node . value . end ,
24- `"${ node . value . raw . slice ( 1 , - 1 ) } "` ,
25- ) ;
26- }
27-
28- if (
29- typeof node . name . name === "string" &&
30- rescriptKeywords . has ( node . name . name )
31- ) {
32- magicString . appendRight ( node . name . end , "_" ) ;
33- }
34-
35- if (
36- typeof node . name . name === "string" &&
37- node . name . name . startsWith ( "aria-" )
38- ) {
39- magicString . update (
40- node . name . start + 4 ,
41- node . name . start + 6 ,
42- node . name . name [ 5 ] ?. toUpperCase ( ) || "" ,
43- ) ;
44- }
45-
46- if ( node . name . name === "data-testid" ) {
47- magicString . update ( node . name . start , node . name . end , "dataTestId" ) ;
48- }
49-
50- if ( node . value === null ) {
51- magicString . appendRight ( node . end , "=true" ) ;
52- }
53- } else if ( node . type === "JSXText" ) {
54- if ( node . raw && integerRegex . test ( node . raw . trim ( ) ) ) {
55- magicString . prependLeft ( node . start , "{React.int(" ) ;
56- magicString . appendRight ( node . end , ")}" ) ;
57- } else if ( node . raw && floatRegex . test ( node . raw . trim ( ) ) ) {
58- magicString . prependLeft ( node . start , "{React.float(" ) ;
59- magicString . appendRight ( node . end , ")}" ) ;
60- } else if ( node . value . trim ( ) ) {
61- magicString . prependLeft ( node . start , '{React.string("' ) ;
62- magicString . appendRight ( node . end , '")}' ) ;
63- }
64- }
182+ enter : ( node : Node , parent : Node | null ) => {
183+ applyRules ( node , parent , rules , magicString ) ;
65184 } ,
66185 } ) ;
67186
0 commit comments