1+ import htm from './index.mjs' ;
2+
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+
11+ /**
12+ * @param {Babel } babel
13+ * @param {object } options
14+ * @param {string } [options.pragma=h] JSX/hyperscript pragma.
15+ * @param {string } [options.tag=html] The tagged template "tag" function name to process.
16+ * @param {boolean } [options.monomorphic=false] Output monomorphic inline objects instead of using String literals.
17+ */
18+ export default function htmBabelPlugin ( { types : t } , options = { } ) {
19+ const pragma = options . pragma === false ? false : dottedIdentifier ( options . pragma || 'h' ) ;
20+
21+ const inlineVNodes = options . monomorphic || pragma === false ;
22+
23+ function dottedIdentifier ( keypath ) {
24+ const path = keypath . split ( '.' ) ;
25+ let out ;
26+ for ( let i = 0 ; i < path . length ; i ++ ) {
27+ const ident = propertyName ( path [ i ] ) ;
28+ out = i === 0 ? ident : t . memberExpression ( out , ident ) ;
29+ }
30+ return out ;
31+ }
32+
33+ function patternStringToRegExp ( str ) {
34+ const parts = str . split ( '/' ) . slice ( 1 ) ;
35+ const end = parts . pop ( ) || '' ;
36+ return new RegExp ( parts . join ( '/' ) , end ) ;
37+ }
38+
39+ function propertyName ( key ) {
40+ if ( key . match ( / ( ^ \d | [ ^ a - z 0 - 9 _ $ ] ) / i) ) return t . stringLiteral ( key ) ;
41+ return t . identifier ( key ) ;
42+ }
43+
44+ function stringValue ( str ) {
45+ if ( options . monomorphic ) {
46+ return t . objectExpression ( [
47+ t . objectProperty ( propertyName ( 'type' ) , t . numericLiteral ( 3 ) ) ,
48+ t . objectProperty ( propertyName ( 'tag' ) , t . nullLiteral ( ) ) ,
49+ t . objectProperty ( propertyName ( 'props' ) , t . nullLiteral ( ) ) ,
50+ t . objectProperty ( propertyName ( 'children' ) , t . nullLiteral ( ) ) ,
51+ t . objectProperty ( propertyName ( 'text' ) , t . stringLiteral ( str ) )
52+ ] ) ;
53+ }
54+ return t . stringLiteral ( str ) ;
55+ }
56+
57+ function createVNode ( tag , props , children ) {
58+ if ( inlineVNodes ) {
59+ return t . objectExpression ( [
60+ options . monomorphic && t . objectProperty ( propertyName ( 'type' ) , t . numericLiteral ( 1 ) ) ,
61+ t . objectProperty ( propertyName ( 'tag' ) , tag ) ,
62+ t . objectProperty ( propertyName ( 'props' ) , props ) ,
63+ t . objectProperty ( propertyName ( 'children' ) , children ) ,
64+ options . monomorphic && t . objectProperty ( propertyName ( 'text' ) , t . nullLiteral ( ) )
65+ ] . filter ( Boolean ) ) ;
66+ }
67+
68+ return t . callExpression ( pragma , [ tag , props , children ] ) ;
69+ }
70+
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 mapChildren ( 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+ return stringValue ( child ) ;
89+ }
90+ return child ;
91+ }
92+
93+ function h ( tag , props , ...children ) {
94+ if ( typeof tag === 'string' ) {
95+ const matches = tag . match ( / \$ \$ \$ _ h _ \[ ( \d + ) \] / ) ;
96+ if ( matches ) tag = currentExpressions [ matches [ 1 ] ] ;
97+ else tag = t . stringLiteral ( tag ) ;
98+ }
99+ //const propsNode = props==null || Object.keys(props).length===0 ? t.nullLiteral() : t.objectExpression(
100+ const propsNode = t . objectExpression (
101+ Object . keys ( props ) . map ( key => {
102+ let value = props [ key ] ;
103+ if ( typeof value === 'string' ) {
104+ const tokenizer = / \$ \$ \$ _ h _ \[ ( \d + ) \] / g;
105+ let token , lhs , root , index = 0 , lastIndex = 0 ;
106+ const append = expr => {
107+ if ( lhs ) expr = t . binaryExpression ( '+' , lhs , expr ) ;
108+ root = lhs = expr ;
109+ } ;
110+ while ( ( token = tokenizer . exec ( value ) ) ) {
111+ append ( t . stringLiteral ( value . substring ( index , token . index ) ) ) ;
112+ append ( currentExpressions [ token [ 1 ] ] ) ;
113+ index = token . index ;
114+ lastIndex = tokenizer . lastIndex ;
115+ }
116+ if ( lastIndex < value . length ) {
117+ append ( t . stringLiteral ( value . substring ( lastIndex ) ) ) ;
118+ }
119+ value = root ;
120+ }
121+ else if ( typeof value === 'boolean' ) {
122+ value = t . booleanLiteral ( value ) ;
123+ }
124+ return t . objectProperty ( propertyName ( key ) , value ) ;
125+ } )
126+ ) ;
127+
128+ if ( Array . isArray ( children ) ) {
129+ children = t . arrayExpression ( children . map ( mapChildren ) . filter ( Boolean ) ) ;
130+ }
131+ return createVNode ( tag , propsNode , children ) ;
132+ }
133+
134+ const html = htm . bind ( h ) ;
135+
136+ // The tagged template tag function name we're looking for.
137+ // This is static because it's generally assigned via htm.bind(h),
138+ // which could be imported from elsewhere, making tracking impossible.
139+ const htmlName = options . tag || 'html' ;
140+ return {
141+ name : 'htm' ,
142+ visitor : {
143+ TaggedTemplateExpression ( path ) {
144+ const tag = path . node . tag . name ;
145+ if ( htmlName [ 0 ] === '/' ? patternStringToRegExp ( htmlName ) . test ( tag ) : tag === htmlName ) {
146+ const statics = path . node . quasi . quasis . map ( e => e . value . raw ) ;
147+ const expr = path . node . quasi . expressions ;
148+ currentExpressions = expr ;
149+ path . replaceWith ( html ( statics , ...expr . map ( ( p , i ) => `$$$_h_[${ i } ]` ) ) ) ;
150+ }
151+ }
152+ }
153+ } ;
154+ }
0 commit comments