Allow to output either raw or transformed AST, fix several transformation bugs with...
[jst.git] / visitors.js
1 /*
2  * Copyright (C) 2018 Nick Downing <nick@ndcode.org>
3  * SPDX-License-Identifier: MIT
4  * 
5  * Permission is hereby granted, free of charge, to any person obtaining a copy
6  * of this software and associated documentation files (the "Software"), to
7  * deal in the Software without restriction, including without limitation the
8  * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9  * sell copies of the Software, and to permit persons to whom the Software is
10  * furnished to do so, subject to the following conditions:
11  * 
12  * The above copyright notice and this permission notice shall be included in
13  * all copies or substantial portions of the Software.
14  * 
15  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21  * IN THE SOFTWARE.
22  */
23
24 let assert = require('assert')
25 let astring = require('astring')
26 let transform = require('./transform')
27 let uglify_js = require('@ndcode/uglify-js')
28
29 let expr_to_tag = (node, context, html_allowed, call_allowed) => {
30   if (node.type === 'Identifier')
31     context.name.push(node.name)
32   else if (node.type === 'Literal')
33     context.name.push(node.value.toString())
34   else if (node.type === 'BinaryExpression' && node.operator === '-') {
35     if (!expr_to_tag(node.left, context, false, false))
36       return false;
37     if (!expr_to_tag(node.right, context, html_allowed, call_allowed))
38       return false;
39   }
40   else if (node.type === 'MemberExpression' && !node.computed) {
41     if (!expr_to_tag(node.object, context, false, false))
42       return false;
43     context.tag[context.tag_type].push(context.name.join('-'))
44     context.name = []
45     context.tag_type = 1
46     if (!expr_to_tag(node.property, context, html_allowed, call_allowed))
47       return false;
48   }
49   else if (node.type === 'MemberExpressionHash') {
50     if (!expr_to_tag(node.object, context, false, false))
51       return false;
52     context.tag[context.tag_type].push(context.name.join('-'))
53     context.name = []
54     context.tag_type = 2
55     if (!expr_to_tag(node.property, context, html_allowed, call_allowed))
56       return false;
57   }
58   else if (html_allowed && node.type === 'HTMLExpression') {
59     if (!expr_to_tag(node.tag, context, false, true))
60       return false;
61     context.tag[context.tag_type].push(context.name.join('-'))
62     context.body = node.body
63   }
64   else if (call_allowed && node.type === 'CallExpression') {
65     if (!expr_to_tag(node.callee, context, false, false))
66       return false;
67     context.arguments = node.arguments
68   }
69   else
70     return false;
71   return true;
72
73
74 let expr_to_name = (node, name) => {
75   if (node.type === 'Identifier')
76     name.push(node.name)
77   else if (node.type === 'Literal')
78     name.push(node.value.toString())
79   else if (node.type === 'BinaryExpression' && node.operator === '-') {
80     expr_to_name(node.left, name)
81     expr_to_name(node.right, name)
82   }
83   else
84     assert(false)
85 }
86
87 let html_body = (context, st, c) => {
88   assert(context.tag[0].length == 1)
89   let tag = context.tag[0][0]
90   let prefix = '<' + tag
91
92   if (context.tag[1].length)
93     prefix += ' class="' + context.tag[1].join(' ') + '"'
94
95   if (context.tag[2].length)
96     prefix += ' id="' + context.tag[2].join(' ') + '"'
97
98   let expr = undefined
99   for (var i = 0; i < context.arguments.length; ++i) {
100     let argument = context.arguments[i]
101
102     let name_expr, value_expr
103     if (
104       argument.type === 'AssignmentExpression' &&
105       argument.operator === '='
106     ) {
107       name_expr = argument.left
108       value_expr = c(argument.right, st, 'Expression')
109     }
110     else {
111       name_expr = argument
112       value_expr = undefined
113     }
114
115     name = []
116     expr_to_name(name_expr, name)
117     prefix += ' ' + name.join('-')
118
119     if (value_expr !== undefined) {
120       prefix += '="'
121       if (value_expr.type === 'Literal')
122         prefix +=
123           value_expr.value
124             .toString()
125             .replaceAll('&', '&amp;')
126             .replaceAll('"', '&quot;')
127       else {
128         let expr1 = {
129           type: 'Literal',
130           value: prefix
131         }
132         expr = {
133           type: 'BinaryExpression',
134           left: expr === undefined ? expr1 : {
135             type: 'BinaryExpression',
136             left: expr,
137             operator: '+',
138             right: expr1
139           },
140           operator: '+',
141           right: {
142             type: 'CallExpression',
143             callee: {
144               type: 'MemberExpression',
145               object: {
146                 type: 'CallExpression',
147                 callee: {
148                   type: 'MemberExpression',
149                   object: {
150                     type: 'CallExpression',
151                     callee: {
152                       type: 'MemberExpression',
153                       object: value_expr,
154                       property: {
155                         type: 'Identifier',
156                         name: 'toString'
157                       },
158                       computed: false
159                     },
160                     arguments: [
161                     ]
162                   },
163                   property: {
164                     type: 'Identifier',
165                     name: 'replaceAll'
166                   },
167                   computed: false
168                 },
169                 arguments: [
170                   {
171                     type: 'Literal',
172                     value: '&'
173                   },
174                   {
175                     type: 'Literal',
176                     value: '&amp;'
177                   }
178                 ]
179               },
180               property: {
181                 type: 'Identifier',
182                 name: 'replaceAll'
183               },
184               computed: false
185             },
186             arguments: [
187               {
188                 type: 'Literal',
189                 value: '"'
190               },
191               {
192                 type: 'Literal',
193                 value: '&quot;'
194               }
195             ]
196           }
197         }
198         prefix = ''
199       }
200       prefix += '"'
201     }
202   }
203   prefix += '>'
204
205   let result = []
206   let body = c(context.body, st, 'Statement').body
207   let prefix_is_template = false
208   if (tag === 'script') {
209     let program = {type: 'Program', body, sourceType: 'script'}
210     // simple way
211     //prefix += astring.generate(program, {indent: ''})
212     // uglified way
213     let render = uglify_js.minify(
214       uglify_js.AST_Node.from_mozilla_ast(program),
215       {
216         compress: true,
217         mangle: true,
218         output: {interpolate: true}
219       }
220     )
221     if (render.error)
222       throw render.error
223     let code = render.code
224     if (render.interpolated) {
225       prefix = prefix.replaceAll('\\', '\\\\').replaceAll('${', '\\${')
226       prefix_is_template = true
227     }
228     else
229       code = code.replaceAll('\\${', '${').replaceAll('\\\\', '\\')
230     prefix += code
231   }
232   else if (body.length !== 0) {
233     let expr1 = {
234       type: 'Literal',
235       value: prefix
236     }
237     expr = expr === undefined ? expr1 : {
238       type: 'BinaryExpression',
239       left: expr,
240       operator: '+',
241       right: expr1,
242     }
243     result.push(
244       {
245         type: 'ExpressionStatement',
246         expression: {
247           type: 'CallExpression',
248           callee: {
249             type: 'MemberExpression',
250             object: {
251               type: 'Identifier',
252               name: '_out'
253             },
254             property: {
255               type: 'Identifier',
256               name: 'push'
257             },
258             computed: false
259           },
260           arguments: [
261             expr
262           ]
263         }
264       }
265     )
266     prefix = ''
267     expr = undefined
268
269     result = result.concat(body)
270   }
271
272   if (
273     tag !== 'br' &&
274     tag !== 'img' &&
275     tag !== 'input' &&
276     tag !== 'link' &&
277     tag !== 'meta'
278   )
279     prefix += '</' + tag + '>'
280   if (prefix.length !== 0) {
281     let expr1 = prefix_is_template ?
282       // note: we are cheating a bit here because prefix contains some (${...})
283       // substitutions to be done by the server, and the contents of those are
284       // meant to be given in AST form in expressions list, but we can get away
285       // with it because UglifyJS uses the raw form of the quasis string which
286       // can distinguish between the (${...)} for server and \${...} for client
287       {
288         type: 'TemplateLiteral',
289         expressions: [],
290         quasis: [
291           {
292             type: 'TemplateExpression',
293             value: {
294               raw: prefix.replaceAll('`', '\\`')
295             },
296             tail: true
297           }
298         ]
299       } :
300       {
301         type: 'Literal',
302         value: prefix
303       }
304     expr = expr === undefined ? expr1 : {
305       type: 'BinaryExpression',
306       left: expr,
307       operator: '+',
308       right: expr1,
309     }
310     result.push(
311       {
312         type: 'ExpressionStatement',
313         expression: {
314           type: 'CallExpression',
315           callee: {
316             type: 'MemberExpression',
317             object: {
318               type: 'Identifier',
319               name: '_out'
320             },
321             property: {
322               type: 'Identifier',
323               name: 'push'
324             },
325             computed: false
326           },
327           arguments: [
328             expr
329           ]
330         }
331       }
332     )
333   }
334   return result
335 }
336
337 let visitors = Object.assign({}, transform.visitors)
338 let visitors_ExpressionStatement = visitors.ExpressionStatement
339 visitors.ExpressionStatement = (node, st, c) => {
340   if (node.expression.type === 'Literal')
341     return {
342       type: 'ExpressionStatement',
343       expression: {
344         type: 'CallExpression',
345         callee: {
346           type: 'MemberExpression',
347           object: {
348             type: 'Identifier',
349             name: '_out'
350           },
351           property: {
352             type: 'Identifier',
353             name: 'push'
354           },
355           computed: false
356         },
357         arguments: [
358           {
359             type: 'Literal',
360             value:
361               node.expression.value
362                 .toString()
363                 .replaceAll('&', '&amp;')
364                 .replaceAll('<', '&lt;')
365           }
366         ]
367       }
368     }
369   if (
370     node.expression.type === 'TemplateLiteral' ||
371     node.expression.type === 'TaggedTemplateLiteral'
372   )
373     return {
374       type: 'ExpressionStatement',
375       expression: {
376         type: 'CallExpression',
377         callee: {
378           type: 'MemberExpression',
379           object: {
380             type: 'Identifier',
381             name: '_out'
382           },
383           property: {
384             type: 'Identifier',
385             name: 'push'
386           },
387           computed: false
388         },
389         arguments: [
390           {
391             type: 'CallExpression',
392             callee: {
393               type: 'MemberExpression',
394               object: {
395                 type: 'CallExpression',
396                 callee: {
397                   type: 'MemberExpression',
398                   object: c(node.expression, st, 'Expression'),
399                   property: {
400                     type: 'Identifier',
401                     name: 'replaceAll'
402                   },
403                   computed: false
404                 },
405                 arguments: [
406                   {
407                     type: 'Literal',
408                     value: '&'
409                   },
410                   {
411                     type: 'Literal',
412                     value: '&amp;'
413                   }
414                 ]
415               },
416               property: {
417                 type: 'Identifier',
418                 name: 'replaceAll'
419               },
420               computed: false
421             },
422             arguments: [
423               {
424                 type: 'Literal',
425                 value: '<'
426               },
427               {
428                 type: 'Literal',
429                 value: '&lt;'
430               }
431             ]
432           }
433         ]
434       }
435     }
436   if (
437     node.expression.type === 'BinaryExpression' ||
438     node.expression.type === 'MemberExpression' ||
439     node.expression.type === 'HTMLExpression'
440   ) {
441     context = {name: [], tag: [[], [], []], tag_type: 0, arguments: []}
442     if (
443       expr_to_tag(node.expression, context, true, false) &&
444       context.body !== undefined
445     ) {
446       let body = html_body(context, st, c)
447       return body.length === 1 ? body[0] : {
448         type: 'BlockStatement',
449         body: body
450       }
451     }
452   }
453   return visitors_ExpressionStatement(node, st, c)
454 }
455 let visitors_Expression = visitors.Expression
456 visitors.Expression = (node, st, c) => {
457   if (
458     node.type === 'BinaryExpression' ||
459     node.type === 'MemberExpression' ||
460     node.type === 'HTMLExpression'
461   ) {
462     context = {name: [], tag: [[], [], []], tag_type: 0, arguments: []}
463     if (
464       expr_to_tag(node, context, true, false) &&
465       context.body !== undefined
466     )
467       return {
468         type: 'CallExpression',
469         callee: {
470           type: 'ArrowFunctionExpression',
471           id: null,
472           expression: false,
473           generator: false,
474           async: false,
475           params: [],
476           body: {
477             type: 'BlockStatement',
478             body: [
479               {
480                 type: 'VariableDeclaration',
481                 declarations: [
482                   {
483                     type: 'VariableDeclarator',
484                     id: {
485                       type: 'Identifier',
486                       name: '_out'
487                     },
488                     init: {
489                       type: 'ArrayExpression',
490                       elements: []
491                     }
492                   }
493                 ],
494                 kind: 'let'
495               }
496             ]
497             .concat(html_body(context, st, c))
498             .concat(
499               [
500                 {
501                   type: 'ReturnStatement',
502                   argument: {
503                     type: 'CallExpression',
504                     callee: {
505                       type: 'MemberExpression',
506                       object: {
507                         type: 'Identifier',
508                         name: '_out'
509                       },
510                       property: {
511                         type: 'Identifier',
512                         name: 'join'
513                       },
514                       computed: false
515                     },
516                     arguments: [
517                       {
518                         type: 'Literal',
519                         value: '',
520                         raw: '\'\''
521                       }
522                     ]
523                   }
524                 }
525               ]
526             )
527           }
528         },
529         arguments: []
530       }
531   }
532   return visitors_Expression(node, st, c)
533 }
534
535 module.exports = visitors