@@ -6,10 +6,11 @@ import htm from 'htm';
66 * @param {string } [options.pragma=h] JSX/hyperscript pragma.
77 * @param {string } [options.tag=html] The tagged template "tag" function name to process.
88 * @param {boolean } [options.monomorphic=false] Output monomorphic inline objects instead of using String literals.
9+ * @param {boolean } [options.useBuiltIns=false] Use the native Object.assign instead of trying to polyfill it.
910 */
1011export default function htmBabelPlugin ( { types : t } , options = { } ) {
1112 const pragma = options . pragma === false ? false : dottedIdentifier ( options . pragma || 'h' ) ;
12-
13+ const useBuiltIns = options . useBuiltIns ;
1314 const inlineVNodes = options . monomorphic || pragma === false ;
1415
1516 function dottedIdentifier ( keypath ) {
@@ -59,91 +60,91 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
5960
6061 return t . callExpression ( pragma , [ tag , props , children ] ) ;
6162 }
62-
63- let isVNode = t . isCallExpression ;
64- if ( inlineVNodes ) {
65- isVNode = node => {
66- if ( ! t . isObjectExpression ( node ) ) return false ;
67- return node . properties [ 0 ] . value . value !== 3 ;
68- } ;
69- }
70-
71- function childMapper ( child , index , children ) {
72- // JSX-style whitespace: (@TODO: remove? doesn't match the browser version)
73- if ( typeof child === 'string' && child . trim ( ) . length === 0 || child == null ) {
74- if ( index === 0 || index === children . length - 1 ) return null ;
75- }
76- if ( typeof child === 'string' && isVNode ( children [ index - 1 ] ) && isVNode ( children [ index + 1 ] ) ) {
77- child = child . trim ( ) ;
78- }
79- if ( typeof child === 'string' ) {
80- return stringValue ( child ) ;
63+
64+ function findNextNode ( args , start ) {
65+ for ( let i = start ; i < args . length ; i ++ ) {
66+ if ( t . isNode ( args [ i ] ) ) {
67+ return i ;
68+ }
8169 }
82- return child ;
70+ return args . length ;
8371 }
84-
85- function h ( tag , props ) {
86- if ( typeof tag === 'string' ) {
87- tag = t . stringLiteral ( tag ) ;
72+
73+ function flattenSpread ( args ) {
74+ const flattened = [ ] ;
75+ for ( let i = 0 ; i < args . length ; i ++ ) {
76+ if ( t . isNode ( args [ i ] ) ) {
77+ flattened . push ( args [ i ] ) ;
78+ }
79+ else {
80+ const start = i ;
81+ const end = findNextNode ( args , start + 1 ) ;
82+ flattened . push ( Object . assign ( ...args . slice ( start , end ) ) ) ;
83+ i = end - 1 ;
84+ }
8885 }
86+ return flattened ;
87+ }
88+
89+ function spreadNode ( args , state ) {
90+ args = flattenSpread ( args ) ;
8991
90- let propsNode ;
91-
92- if ( t . isObjectExpression ( props ) ) {
93- propsNode = props ;
94- for ( let i in props ) {
95- if ( props . hasOwnProperty ( i ) && props [ i ] && props [ i ] . type ) {
96- for ( let j = 0 ; j < props . properties . length ; j ++ ) {
97- if ( props . properties [ j ] . start > props [ i ] . start ) {
98- props . properties . splice ( j , 0 , t . objectProperty ( propertyName ( i ) , props [ i ] ) ) ;
99- break ;
100- }
101- }
102- delete props [ i ] ;
103- }
104- }
92+ // Case 'Object.assign(x)' can be collapsed to 'x'.
93+ if ( args . length === 1 ) {
94+ return propsNode ( args [ 0 ] ) ;
10595 }
106- else {
107- propsNode = t . objectExpression (
108- Object . keys ( props ) . map ( key => {
109- let value = props [ key ] ;
110- if ( typeof value === 'string' ) {
111- value = t . stringLiteral ( value ) ;
112- }
113- else if ( typeof value === 'boolean' ) {
114- value = t . booleanLiteral ( value ) ;
115- }
116- else if ( typeof value === 'number' ) {
117- value = t . stringLiteral ( value + '' ) ;
118- }
119- return t . objectProperty ( propertyName ( key ) , value ) ;
120- } )
121- ) ;
96+ // Case 'Object.assign({}, x)', can be collapsed to 'x'.
97+ if ( args . length === 2 && ! t . isNode ( args [ 0 ] ) && Object . keys ( args [ 0 ] ) . length === 0 ) {
98+ return propsNode ( args [ 1 ] ) ;
12299 }
123-
124- // recursive iteration of possibly nested arrays of children.
125- let children = [ ] ;
126- if ( arguments . length > 2 ) {
127- const stack = [ ] ;
128- // eslint-disable-next-line prefer-rest-params
129- for ( let i = arguments . length ; i -- > 2 ; ) stack . push ( arguments [ i ] ) ;
130- while ( stack . length ) {
131- const child = stack . pop ( ) ;
132- if ( Array . isArray ( child ) ) {
133- for ( let i = child . length ; i -- ; ) stack . push ( child [ i ] ) ;
100+ const helper = useBuiltIns ? dottedIdentifier ( 'Object.assign' ) : state . addHelper ( 'extends' ) ;
101+ return t . callExpression ( helper , args . map ( propsNode ) ) ;
102+ }
103+
104+ function propsNode ( props ) {
105+ return t . isNode ( props ) ? props : t . objectExpression (
106+ Object . keys ( props ) . map ( key => {
107+ let value = props [ key ] ;
108+ if ( typeof value === 'string' ) {
109+ value = t . stringLiteral ( value ) ;
134110 }
135- else if ( child != null ) {
136- children . push ( child ) ;
111+ else if ( typeof value === 'boolean' ) {
112+ value = t . booleanLiteral ( value ) ;
137113 }
114+ return t . objectProperty ( propertyName ( key ) , value ) ;
115+ } )
116+ ) ;
117+ }
118+
119+ function transform ( { tag, props, children } , state ) {
120+ function childMapper ( child ) {
121+ if ( typeof child === 'string' ) {
122+ return stringValue ( child ) ;
138123 }
139- children = children . map ( childMapper ) . filter ( Boolean ) ;
124+ return t . isNode ( child ) ? child : transform ( child , state ) ;
140125 }
141- children = t . arrayExpression ( children ) ;
142-
143- return createVNode ( tag , propsNode , children ) ;
126+ const newTag = typeof tag === 'string' ? t . stringLiteral ( tag ) : tag ;
127+ const newProps = ! Array . isArray ( props ) ? propsNode ( props ) : spreadNode ( props , state ) ;
128+ const newChildren = t . arrayExpression ( children . map ( childMapper ) ) ;
129+ return createVNode ( newTag , newProps , newChildren ) ;
144130 }
145131
132+ function h ( tag , props , ...children ) {
133+ return { tag, props, children } ;
134+ }
135+
146136 const html = htm . bind ( h ) ;
137+
138+ function treeify ( statics , expr ) {
139+ const assign = Object . assign ;
140+ try {
141+ Object . assign = function ( ...objs ) { return objs ; } ;
142+ return html ( statics , ...expr ) ;
143+ }
144+ finally {
145+ Object . assign = assign ;
146+ }
147+ }
147148
148149 // The tagged template tag function name we're looking for.
149150 // This is static because it's generally assigned via htm.bind(h),
@@ -152,12 +153,14 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
152153 return {
153154 name : 'htm' ,
154155 visitor : {
155- TaggedTemplateExpression ( path ) {
156+ TaggedTemplateExpression ( path , state ) {
156157 const tag = path . node . tag . name ;
157158 if ( htmlName [ 0 ] === '/' ? patternStringToRegExp ( htmlName ) . test ( tag ) : tag === htmlName ) {
158159 const statics = path . node . quasi . quasis . map ( e => e . value . raw ) ;
159160 const expr = path . node . quasi . expressions ;
160- path . replaceWith ( html ( statics , ...expr ) ) ;
161+
162+ const tree = treeify ( statics , expr ) ;
163+ path . replaceWith ( transform ( tree , state ) ) ;
161164 }
162165 }
163166 }
0 commit comments