/
jsx-lexer.js
258 lines (223 loc) · 7.79 KB
/
jsx-lexer.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
258
import JavascriptLexer from './javascript-lexer.js'
import ts from 'typescript'
export default class JsxLexer extends JavascriptLexer {
constructor(options = {}) {
super(options)
this.transSupportBasicHtmlNodes =
options.transSupportBasicHtmlNodes || false
this.transKeepBasicHtmlNodesFor = options.transKeepBasicHtmlNodesFor || [
'br',
'strong',
'i',
'p',
]
this.omitAttributes = [this.attr, 'ns', 'defaults']
}
extract(content, filename = '__default.jsx') {
const keys = []
const parseCommentNode = this.createCommentNodeParser()
const parseTree = (node) => {
let entry
parseCommentNode(keys, node, content)
switch (node.kind) {
case ts.SyntaxKind.CallExpression:
entry = this.expressionExtractor.call(this, node)
break
case ts.SyntaxKind.TaggedTemplateExpression:
entry = this.taggedTemplateExpressionExtractor.call(this, node)
break
case ts.SyntaxKind.JsxElement:
entry = this.jsxExtractor.call(this, node, content)
break
case ts.SyntaxKind.JsxSelfClosingElement:
entry = this.jsxExtractor.call(this, node, content)
break
}
if (entry) {
keys.push(entry)
}
node.forEachChild(parseTree)
}
const sourceFile = ts.createSourceFile(
filename,
content,
ts.ScriptTarget.Latest
)
parseTree(sourceFile)
const keysWithNamespace = this.setNamespaces(keys)
const keysWithPrefixes = this.setKeyPrefixes(keysWithNamespace)
return keysWithPrefixes
}
jsxExtractor(node, sourceText) {
const tagNode = node.openingElement || node
const getPropValue = (node, tagName) => {
const attribute = node.attributes.properties.find(
(attr) => attr.name.text === tagName
)
if (!attribute) {
return undefined
}
if (attribute.initializer.expression?.kind === ts.SyntaxKind.Identifier) {
this.emit(
'warning',
`Namespace is not a string literal: ${attribute.initializer.expression.text}`
)
return undefined
}
return attribute.initializer.expression
? attribute.initializer.expression.text
: attribute.initializer.text
}
const getKey = (node) => getPropValue(node, this.attr)
if (tagNode.tagName.text === 'Trans') {
const entry = {}
entry.key = getKey(tagNode)
const defaultsProp = getPropValue(tagNode, 'defaults')
const defaultValue =
defaultsProp || this.nodeToString.call(this, node, sourceText)
if (defaultValue !== '') {
entry.defaultValue = defaultValue
if (!entry.key) {
entry.key = entry.defaultValue
}
}
const namespace = getPropValue(tagNode, 'ns')
if (namespace) {
entry.namespace = namespace
}
tagNode.attributes.properties.forEach((property) => {
if (this.omitAttributes.includes(property.name.text)) {
return
}
if (property.initializer) {
if (property.initializer.expression) {
if (
property.initializer.expression.kind === ts.SyntaxKind.TrueKeyword
) {
entry[property.name.text] = true
} else if (
property.initializer.expression.kind ===
ts.SyntaxKind.FalseKeyword
) {
entry[property.name.text] = false
} else {
entry[
property.name.text
] = `{${property.initializer.expression.text}}`
}
} else {
entry[property.name.text] = property.initializer.text
}
} else entry[property.name.text] = true
})
return entry.key ? entry : null
} else if (tagNode.tagName.text === 'Interpolate') {
const entry = {}
entry.key = getKey(tagNode)
return entry.key ? entry : null
}
}
nodeToString(node, sourceText) {
const children = this.parseChildren.call(this, node.children, sourceText)
const elemsToString = (children) =>
children
.map((child, index) => {
switch (child.type) {
case 'js':
case 'text':
return child.content
case 'tag':
const useTagName =
child.isBasic &&
this.transSupportBasicHtmlNodes &&
this.transKeepBasicHtmlNodesFor.includes(child.name)
const elementName = useTagName ? child.name : index
const childrenString = elemsToString(child.children)
return childrenString || !(useTagName && child.selfClosing)
? `<${elementName}>${childrenString}</${elementName}>`
: `<${elementName} />`
default:
throw new Error('Unknown parsed content: ' + child.type)
}
})
.join('')
return elemsToString(children)
}
parseChildren(children = [], sourceText) {
return children
.map((child) => {
if (child.kind === ts.SyntaxKind.JsxText) {
return {
type: 'text',
content: child.text
.replace(/(^(\n|\r)\s*)|((\n|\r)\s*$)/g, '')
.replace(/(\n|\r)\s*/g, ' '),
}
} else if (
child.kind === ts.SyntaxKind.JsxElement ||
child.kind === ts.SyntaxKind.JsxSelfClosingElement
) {
const element = child.openingElement || child
const name = element.tagName.escapedText
const isBasic = !element.attributes.properties.length
return {
type: 'tag',
children: this.parseChildren(child.children, sourceText),
name,
isBasic,
selfClosing: child.kind === ts.SyntaxKind.JsxSelfClosingElement,
}
} else if (child.kind === ts.SyntaxKind.JsxExpression) {
// strip empty expressions
if (!child.expression) {
return {
type: 'text',
content: '',
}
} else if (child.expression.kind === ts.SyntaxKind.StringLiteral) {
return {
type: 'text',
content: child.expression.text,
}
}
// strip properties from ObjectExpressions
// annoying (and who knows how many other exceptions we'll need to write) but necessary
else if (
child.expression.kind === ts.SyntaxKind.ObjectLiteralExpression
) {
// i18next-react only accepts two props, any random single prop, and a format prop
// for our purposes, format prop is always ignored
let nonFormatProperties = child.expression.properties.filter(
(prop) => prop.name.text !== 'format'
)
// more than one property throw a warning in i18next-react, but still works as a key
if (nonFormatProperties.length > 1) {
this.emit(
'warning',
`The passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.`
)
return {
type: 'text',
content: '',
}
}
return {
type: 'js',
content: `{{${nonFormatProperties[0].name.text}}}`,
}
}
// slice on the expression so that we ignore comments around it
return {
type: 'js',
content: `{${sourceText.slice(
child.expression.pos,
child.expression.end
)}}`,
}
} else {
throw new Error('Unknown ast element when parsing jsx: ' + child.kind)
}
})
.filter((child) => child.type !== 'text' || child.content)
}
}