forked from pugjs/babel-plugin-transform-react-pug
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Tag.js
257 lines (220 loc) · 6.82 KB
/
Tag.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
// @flow
import type Context from '../context';
import parseExpression from '../utils/parse-expression';
import t from '../babel-types';
import {visitJsx, visitJsxExpressions} from '../visitors';
import {getInterpolationRefs} from '../utils/interpolation';
type PugAttribute = {
name: string,
val: string,
mustEscape: boolean,
};
type Attribute = JSXAttribute | JSXSpreadAttribute;
/**
* Get children nodes from the node, passing the node's
* context to the children and generating JSX values.
* @param {Object} node - The node
* @param {Context} context - The context to apply to the children
* nodes
* @returns {Array<JSXValue>}
*/
function getChildren(node: Object, context: Context): Array<JSXValue> {
return context.noKey(childContext =>
(node.code ? [visitJsx(node.code, childContext)] : []).concat(
visitJsxExpressions(node.block.nodes, childContext),
),
);
}
/**
* Iterate through the node's attributes and convert
* them into JSX attributes.
* @param {Object} node - The node
* @param {Context} context - The context
* @returns {Array<Attribute>}
*/
function getAttributes(node: Object, context: Context): Array<Attribute> {
const classes: Array<Object> = [];
const attrs: Array<Attribute> = node.attrs
.map((node: PugAttribute): PugAttribute => {
if (node.val === true) {
return {
...node,
mustEscape: false,
};
}
return node;
})
.map(({name, val, mustEscape}: PugAttribute): Attribute | null => {
if (/\.\.\./.test(name) && val === true) {
return t.jSXSpreadAttribute(parseExpression(name.substr(3), context));
}
switch (name) {
case 'for':
name = 'htmlFor';
break;
case 'maxlength':
name = 'maxLength';
break;
case 'class':
name = 'className';
break;
}
const expr = parseExpression(val === true ? 'true' : val, context);
if (!mustEscape) {
const isStringViaAliases: boolean =
t.isStringLiteral(expr) && !['className', 'id'].includes(name);
const isNotStringOrBoolean: boolean =
!t.isStringLiteral(expr) && !t.isBooleanLiteral(expr);
if (isStringViaAliases || isNotStringOrBoolean) {
throw context.error(
'INVALID_EXPRESSION',
'Unescaped attributes are not supported in react-pug',
);
}
}
if (expr == null) {
return null;
}
if (name === 'className') {
classes.push(expr);
return null;
}
const jsxValue =
t.asStringLiteral(expr) ||
t.asJSXElement(expr) ||
t.jSXExpressionContainer(expr);
if (/\.\.\./.test(name)) {
throw new Error('spread attributes must not have a value');
}
return t.jSXAttribute(t.jSXIdentifier(name), jsxValue);
})
.filter(Boolean);
if (classes.length) {
const value = classes.every(cls => t.isStringLiteral(cls))
? t.stringLiteral(classes.map(cls => (cls: any).value).join(' '))
: t.jSXExpressionContainer(
t.callExpression(
t.memberExpression(
t.arrayExpression(classes),
t.identifier('join'),
),
[t.stringLiteral(' ')],
),
);
attrs.push(t.jSXAttribute(t.jSXIdentifier('className'), value));
}
return attrs;
}
/**
* Retrieve attributes and children of the passed node.
* @param {Object} node - The node
* @param {Context} context - The context
* @returns {Object} Contains the attributes and children
* of the node.
*/
function getAttributesAndChildren(
node: Object,
context: Context,
): {
attrs: Array<JSXAttribute | JSXSpreadAttribute>,
children: Array<JSXValue>,
} {
const children = getChildren(node, context);
if (node.attributeBlocks.length) {
throw new Error('Attribute blocks are not yet supported in react-pug');
}
const attrs = getAttributes(node, context);
context.key.handleAttributes(attrs);
return {attrs, children};
}
/**
* Generate a JSX element.
* @param { string } name - The name of the JSX element
* @param { Array<JSXAttribute|JSXSpreadAttribute> } attrs -
* The attributes for the JSX element
* @param { Array<JSXValue> } children - The children for
* the JSX element
* @returns { JSXElement } The JSX element.
*/
function buildJSXElement(
name: string,
attrs: Array<JSXAttribute | JSXSpreadAttribute>,
children,
): JSXElement {
const tagName = t.jSXIdentifier(name);
const noChildren = children.length === 0;
const open = t.jSXOpeningElement(
tagName,
attrs, // Array<JSXAttribute | JSXSpreadAttribute>
noChildren,
);
const close = noChildren ? null : t.jSXClosingElement(tagName);
return t.jSXElement(open, close, children, noChildren);
}
/**
* Check whether an interpolation exists, if so, check whether
* the interpolation is a react component and return either
* the component as a JSX element or the interpolation.
* @param {string} name - The interpolation reference
* @param {Context} context - The current context to retrieve
* the interpolation from
* @param {Array<JSXValue>} children - Whether the element has
* attributes or children
* @returns {?Object} The context's interpolation or a JSX element.
*/
function getInterpolationByContext(
name: string,
context: Context,
attrs: Array<JSXAttribute | JSXSpreadAttribute>,
children: Array<JSXValue>,
): ?Expression {
if (!getInterpolationRefs(name)) {
return null;
}
const interpolation = (context.getInterpolationByRef(name): any);
const isReactComponent =
t.isIdentifier(interpolation) &&
interpolation.name.charAt(0) === interpolation.name.charAt(0).toUpperCase();
if (attrs.length || children.length) {
if (isReactComponent) {
return buildJSXElement(interpolation.name, attrs, children);
} else {
throw context.error(
'INVALID_EXPRESSION',
`Only components can have children and attributes`,
);
}
}
return interpolation;
}
const TagVisitor = {
jsx(node: Object, context: Context): JSXValue {
const {attrs, children} = getAttributesAndChildren(node, context);
const interpolation = getInterpolationByContext(
node.name,
context,
attrs,
children,
);
if (interpolation != null) {
return (
t.asJSXElement(interpolation) || t.jSXExpressionContainer(interpolation)
);
}
return buildJSXElement(node.name, attrs, children);
},
expression(node: Object, context: Context): Expression {
const {attrs, children} = getAttributesAndChildren(node, context);
const interpolation = getInterpolationByContext(
node.name,
context,
attrs,
children,
);
if (interpolation != null) {
return interpolation;
}
return buildJSXElement(node.name, attrs, children);
},
};
export default TagVisitor;