11import htm from 'htm' ;
22
3- // htm() uses the HTML parser, which serializes attribute values.
4- // this is a problem, because composite values here can be made up
5- // of strings and AST nodes, which serialize to [object Object].
6- // Since the handoff from AST node handling to htm() is synchronous,
7- // this global lookup will always reflect the corresponding
8- // AST-derived values for the current htm() invocation.
9- let currentExpressions ;
10-
113/**
124 * @param {Babel } babel
135 * @param {object } options
146 * @param {string } [options.pragma=h] JSX/hyperscript pragma.
157 * @param {string } [options.tag=html] The tagged template "tag" function name to process.
168 * @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.
10+ * @param {boolean } [options.variableArity=true] If `false`, always passes exactly 3 arguments to the pragma function.
1711 */
1812export default function htmBabelPlugin ( { types : t } , options = { } ) {
1913 const pragma = options . pragma === false ? false : dottedIdentifier ( options . pragma || 'h' ) ;
20-
14+ const useBuiltIns = options . useBuiltIns ;
2115 const inlineVNodes = options . monomorphic || pragma === false ;
2216
2317 function dottedIdentifier ( keypath ) {
@@ -55,6 +49,11 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
5549 }
5650
5751 function createVNode ( tag , props , children ) {
52+ // Never pass children=[[]].
53+ if ( children . elements . length === 1 && t . isArrayExpression ( children . elements [ 0 ] ) && children . elements [ 0 ] . elements . length === 0 ) {
54+ children = children . elements [ 0 ] ;
55+ }
56+
5857 if ( inlineVNodes ) {
5958 return t . objectExpression ( [
6059 options . monomorphic && t . objectProperty ( propertyName ( 'type' ) , t . numericLiteral ( 1 ) ) ,
@@ -64,93 +63,75 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
6463 options . monomorphic && t . objectProperty ( propertyName ( 'text' ) , t . nullLiteral ( ) )
6564 ] . filter ( Boolean ) ) ;
6665 }
67-
68- return t . callExpression ( pragma , [ tag , props , children ] ) ;
69- }
7066
71- let isVNode = t . isCallExpression ;
72- if ( inlineVNodes ) {
73- isVNode = node => {
74- if ( ! t . isObjectExpression ( node ) ) return false ;
75- return node . properties [ 0 ] . value . value !== 3 ;
76- } ;
77- }
78-
79- function childMapper ( child , index , children ) {
80- // JSX-style whitespace: (@TODO: remove? doesn't match the browser version)
81- if ( typeof child === 'string' && child . trim ( ) . length === 0 || child == null ) {
82- if ( index === 0 || index === children . length - 1 ) return null ;
83- }
84- if ( typeof child === 'string' && isVNode ( children [ index - 1 ] ) && isVNode ( children [ index + 1 ] ) ) {
85- child = child . trim ( ) ;
86- }
87- if ( typeof child === 'string' ) {
88- const matches = child . match ( / \$ \$ \$ _ h _ \[ ( \d + ) \] / ) ;
89- if ( matches ) return currentExpressions [ matches [ 1 ] ] ;
90- return stringValue ( child ) ;
67+ // Passing `{variableArity:false}` always produces `h(tag, props, children)` - where `children` is always an Array.
68+ // Otherwise, the default is `h(tag, props, ...children)`.
69+ if ( options . variableArity !== false ) {
70+ children = children . elements ;
9171 }
92- return child ;
93- }
9472
95- function h ( tag , props ) {
96- if ( typeof tag === 'string' ) {
97- const matches = tag . match ( / \$ \$ \$ _ h _ \[ ( \d + ) \] / ) ;
98- if ( matches ) tag = currentExpressions [ matches [ 1 ] ] ;
99- else tag = t . stringLiteral ( tag ) ;
73+ return t . callExpression ( pragma , [ tag , props ] . concat ( children ) ) ;
74+ }
75+
76+ function spreadNode ( args , state ) {
77+ // 'Object.assign({}, x)', can be collapsed to 'x'.
78+ if ( args . length === 2 && ! t . isNode ( args [ 0 ] ) && Object . keys ( args [ 0 ] ) . length === 0 ) {
79+ return propsNode ( args [ 1 ] ) ;
10080 }
81+ const helper = useBuiltIns ? dottedIdentifier ( 'Object.assign' ) : state . addHelper ( 'extends' ) ;
82+ return t . callExpression ( helper , args . map ( propsNode ) ) ;
83+ }
84+
85+ function propsNode ( props ) {
86+ if ( props == null ) return t . nullLiteral ( ) ;
10187
102- //const propsNode = props==null || Object.keys(props).length===0 ? t.nullLiteral() : t.objectExpression(
103- const propsNode = t . objectExpression (
88+ return t . isNode ( props ) ? props : t . objectExpression (
10489 Object . keys ( props ) . map ( key => {
10590 let value = props [ key ] ;
10691 if ( typeof value === 'string' ) {
107- const tokenizer = / \$ \$ \$ _ h _ \[ ( \d + ) \] / g;
108- let token , lhs , root , index = 0 , lastIndex = 0 ;
109- const append = expr => {
110- if ( lhs ) expr = t . binaryExpression ( '+' , lhs , expr ) ;
111- root = lhs = expr ;
112- } ;
113- while ( ( token = tokenizer . exec ( value ) ) ) {
114- append ( t . stringLiteral ( value . substring ( index , token . index ) ) ) ;
115- append ( currentExpressions [ token [ 1 ] ] ) ;
116- index = token . index ;
117- lastIndex = tokenizer . lastIndex ;
118- }
119- if ( lastIndex < value . length ) {
120- append ( t . stringLiteral ( value . substring ( lastIndex ) ) ) ;
121- }
122- value = root ;
92+ value = t . stringLiteral ( value ) ;
12393 }
12494 else if ( typeof value === 'boolean' ) {
12595 value = t . booleanLiteral ( value ) ;
12696 }
12797 return t . objectProperty ( propertyName ( key ) , value ) ;
12898 } )
12999 ) ;
100+ }
130101
131- // recursive iteration of possibly nested arrays of children.
132- let children = [ ] ;
133- if ( arguments . length > 2 ) {
134- const stack = [ ] ;
135- // eslint-disable-next-line prefer-rest-params
136- for ( let i = arguments . length ; i -- > 2 ; ) stack . push ( arguments [ i ] ) ;
137- while ( stack . length ) {
138- const child = stack . pop ( ) ;
139- if ( Array . isArray ( child ) ) {
140- for ( let i = child . length ; i -- ; ) stack . push ( child [ i ] ) ;
141- }
142- else if ( child != null ) {
143- children . push ( child ) ;
144- }
102+ function transform ( node , state ) {
103+ if ( node === undefined ) return t . identifier ( 'undefined' ) ;
104+ if ( node == null ) return t . nullLiteral ( ) ;
105+
106+ const { tag, props, children } = node ;
107+ function childMapper ( child ) {
108+ if ( typeof child === 'string' ) {
109+ return stringValue ( child ) ;
145110 }
146- children = children . map ( childMapper ) . filter ( Boolean ) ;
111+ return t . isNode ( child ) ? child : transform ( child , state ) ;
147112 }
148- children = t . arrayExpression ( children ) ;
149-
150- return createVNode ( tag , propsNode , children ) ;
113+ const newTag = typeof tag === 'string' ? t . stringLiteral ( tag ) : tag ;
114+ const newProps = ! Array . isArray ( props ) ? propsNode ( props ) : spreadNode ( props , state ) ;
115+ const newChildren = t . arrayExpression ( children . map ( childMapper ) ) ;
116+ return createVNode ( newTag , newProps , newChildren ) ;
151117 }
152118
119+ function h ( tag , props , ...children ) {
120+ return { tag, props, children } ;
121+ }
122+
153123 const html = htm . bind ( h ) ;
124+
125+ function treeify ( statics , expr ) {
126+ const assign = Object . assign ;
127+ try {
128+ Object . assign = function ( ...objs ) { return objs ; } ;
129+ return html ( statics , ...expr ) ;
130+ }
131+ finally {
132+ Object . assign = assign ;
133+ }
134+ }
154135
155136 // The tagged template tag function name we're looking for.
156137 // This is static because it's generally assigned via htm.bind(h),
@@ -159,13 +140,14 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
159140 return {
160141 name : 'htm' ,
161142 visitor : {
162- TaggedTemplateExpression ( path ) {
143+ TaggedTemplateExpression ( path , state ) {
163144 const tag = path . node . tag . name ;
164145 if ( htmlName [ 0 ] === '/' ? patternStringToRegExp ( htmlName ) . test ( tag ) : tag === htmlName ) {
165146 const statics = path . node . quasi . quasis . map ( e => e . value . raw ) ;
166147 const expr = path . node . quasi . expressions ;
167- currentExpressions = expr ;
168- path . replaceWith ( html ( statics , ...expr . map ( ( p , i ) => `$$$_h_[${ i } ]` ) ) ) ;
148+
149+ const tree = treeify ( statics , expr ) ;
150+ path . replaceWith ( transform ( tree , state ) ) ;
169151 }
170152 }
171153 }
0 commit comments