1- import { JSDOM } from 'jsdom' ;
2- const before = global . document ;
3- global . document = new JSDOM ( ) . window . document ;
4- const htm = require ( 'htm' ) ;
5- global . document = before ;
6-
7- // htm() uses the HTML parser, which serializes attribute values.
8- // this is a problem, because composite values here can be made up
9- // of strings and AST nodes, which serialize to [object Object].
10- // Since the handoff from AST node handling to htm() is synchronous,
11- // this global lookup will always reflect the corresponding
12- // AST-derived values for the current htm() invocation.
13- let currentExpressions ;
1+ import htm from 'htm' ;
142
153/**
164 * @param {Babel } babel
175 * @param {object } options
186 * @param {string } [options.pragma=h] JSX/hyperscript pragma.
197 * @param {string } [options.tag=html] The tagged template "tag" function name to process.
208 * @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.
2111 */
2212export default function htmBabelPlugin ( { types : t } , options = { } ) {
2313 const pragma = options . pragma === false ? false : dottedIdentifier ( options . pragma || 'h' ) ;
24-
14+ const useBuiltIns = options . useBuiltIns ;
2515 const inlineVNodes = options . monomorphic || pragma === false ;
2616
2717 function dottedIdentifier ( keypath ) {
@@ -59,6 +49,11 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
5949 }
6050
6151 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+
6257 if ( inlineVNodes ) {
6358 return t . objectExpression ( [
6459 options . monomorphic && t . objectProperty ( propertyName ( 'type' ) , t . numericLiteral ( 1 ) ) ,
@@ -68,93 +63,75 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
6863 options . monomorphic && t . objectProperty ( propertyName ( 'text' ) , t . nullLiteral ( ) )
6964 ] . filter ( Boolean ) ) ;
7065 }
71-
72- return t . callExpression ( pragma , [ tag , props , children ] ) ;
73- }
7466
75- let isVNode = t . isCallExpression ;
76- if ( inlineVNodes ) {
77- isVNode = node => {
78- if ( ! t . isObjectExpression ( node ) ) return false ;
79- return node . properties [ 0 ] . value . value !== 3 ;
80- } ;
81- }
82-
83- function childMapper ( child , index , children ) {
84- // JSX-style whitespace: (@TODO: remove? doesn't match the browser version)
85- if ( typeof child === 'string' && child . trim ( ) . length === 0 || child == null ) {
86- if ( index === 0 || index === children . length - 1 ) return null ;
87- }
88- if ( typeof child === 'string' && isVNode ( children [ index - 1 ] ) && isVNode ( children [ index + 1 ] ) ) {
89- child = child . trim ( ) ;
90- }
91- if ( typeof child === 'string' ) {
92- const matches = child . match ( / \$ \$ \$ _ h _ \[ ( \d + ) \] / ) ;
93- if ( matches ) return currentExpressions [ matches [ 1 ] ] ;
94- 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 ;
9571 }
96- return child ;
97- }
9872
99- function h ( tag , props ) {
100- if ( typeof tag === 'string' ) {
101- const matches = tag . match ( / \$ \$ \$ _ h _ \[ ( \d + ) \] / ) ;
102- if ( matches ) tag = currentExpressions [ matches [ 1 ] ] ;
103- 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 ] ) ;
10480 }
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 ( ) ;
10587
106- //const propsNode = props==null || Object.keys(props).length===0 ? t.nullLiteral() : t.objectExpression(
107- const propsNode = t . objectExpression (
88+ return t . isNode ( props ) ? props : t . objectExpression (
10889 Object . keys ( props ) . map ( key => {
10990 let value = props [ key ] ;
11091 if ( typeof value === 'string' ) {
111- const tokenizer = / \$ \$ \$ _ h _ \[ ( \d + ) \] / g;
112- let token , lhs , root , index = 0 , lastIndex = 0 ;
113- const append = expr => {
114- if ( lhs ) expr = t . binaryExpression ( '+' , lhs , expr ) ;
115- root = lhs = expr ;
116- } ;
117- while ( ( token = tokenizer . exec ( value ) ) ) {
118- append ( t . stringLiteral ( value . substring ( index , token . index ) ) ) ;
119- append ( currentExpressions [ token [ 1 ] ] ) ;
120- index = token . index ;
121- lastIndex = tokenizer . lastIndex ;
122- }
123- if ( lastIndex < value . length ) {
124- append ( t . stringLiteral ( value . substring ( lastIndex ) ) ) ;
125- }
126- value = root ;
92+ value = t . stringLiteral ( value ) ;
12793 }
12894 else if ( typeof value === 'boolean' ) {
12995 value = t . booleanLiteral ( value ) ;
13096 }
13197 return t . objectProperty ( propertyName ( key ) , value ) ;
13298 } )
13399 ) ;
100+ }
134101
135- // recursive iteration of possibly nested arrays of children.
136- let children = [ ] ;
137- if ( arguments . length > 2 ) {
138- const stack = [ ] ;
139- // eslint-disable-next-line prefer-rest-params
140- for ( let i = arguments . length ; i -- > 2 ; ) stack . push ( arguments [ i ] ) ;
141- while ( stack . length ) {
142- const child = stack . pop ( ) ;
143- if ( Array . isArray ( child ) ) {
144- for ( let i = child . length ; i -- ; ) stack . push ( child [ i ] ) ;
145- }
146- else if ( child != null ) {
147- children . push ( child ) ;
148- }
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 ) ;
149110 }
150- children = children . map ( childMapper ) . filter ( Boolean ) ;
111+ return t . isNode ( child ) ? child : transform ( child , state ) ;
151112 }
152- children = t . arrayExpression ( children ) ;
153-
154- 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 ) ;
155117 }
156118
119+ function h ( tag , props , ...children ) {
120+ return { tag, props, children } ;
121+ }
122+
157123 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+ }
158135
159136 // The tagged template tag function name we're looking for.
160137 // This is static because it's generally assigned via htm.bind(h),
@@ -163,13 +140,14 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
163140 return {
164141 name : 'htm' ,
165142 visitor : {
166- TaggedTemplateExpression ( path ) {
143+ TaggedTemplateExpression ( path , state ) {
167144 const tag = path . node . tag . name ;
168145 if ( htmlName [ 0 ] === '/' ? patternStringToRegExp ( htmlName ) . test ( tag ) : tag === htmlName ) {
169146 const statics = path . node . quasi . quasis . map ( e => e . value . raw ) ;
170147 const expr = path . node . quasi . expressions ;
171- currentExpressions = expr ;
172- path . replaceWith ( html ( statics , ...expr . map ( ( p , i ) => `$$$_h_[${ i } ]` ) ) ) ;
148+
149+ const tree = treeify ( statics , expr ) ;
150+ path . replaceWith ( transform ( tree , state ) ) ;
173151 }
174152 }
175153 }
0 commit comments