Change String.replace() to replaceAll() everywhere (this was a nasty bug), implement...
authorNick Downing <nick@ndcode.org>
Wed, 12 Jan 2022 13:47:14 +0000 (00:47 +1100)
committerNick Downing <nick@ndcode.org>
Wed, 12 Jan 2022 13:47:14 +0000 (00:47 +1100)
cli.js
jst.js
link.sh
package.json
src/expression.js
test_interpolate.jst [new file with mode: 0644]
transform.js
visitors.js

diff --git a/cli.js b/cli.js
index 2111408..0e2c3ce 100755 (executable)
--- a/cli.js
+++ b/cli.js
@@ -32,7 +32,8 @@ fs.writeSync(
       },
       uglify_options: {
         compress: commander.compress,
-        mangle: commander.mangle
+        mangle: commander.mangle,
+        output: {inline_script: false}
       }
     }
   ),
diff --git a/jst.js b/jst.js
index 4ef8997..156dfe5 100644 (file)
--- a/jst.js
+++ b/jst.js
@@ -25,7 +25,7 @@ let acorn = require('./dist/acorn')
 let astring = require('astring')
 let transform = require('./transform')
 let visitors = require('./visitors')
-let uglify_js = require('uglify-js')
+let uglify_js = require('@ndcode/uglify-js')
 
 let jst = (text, options) => {
   options = Object.assign(
@@ -48,14 +48,15 @@ let jst = (text, options) => {
       }
     )
   )
-  return (
-    options.output === 'astring' ?
-      astring.generate(ast, options.astring_options) :
-      uglify_js.minify(
-        uglify_js.AST_Node.from_mozilla_ast(ast),
-        options.uglify_options
-      ).code
+  if (options.output === 'astring')
+    return astring.generate(ast, options.astring_options)
+  let render = uglify_js.minify(
+    uglify_js.AST_Node.from_mozilla_ast(ast),
+    options.uglify_options
   )
+  if (render.error)
+    throw render.error
+  return render.code
 }
 
 module.exports = jst
diff --git a/link.sh b/link.sh
index a37337a..e2f51ce 100755 (executable)
--- a/link.sh
+++ b/link.sh
@@ -1,5 +1,5 @@
 #!/bin/sh
 rm -rf node_modules package-lock.json
-npm link @ndcode/build_cache @ndcode/clean-css @ndcode/disk_build
+npm link @ndcode/build_cache @ndcode/clean-css @ndcode/disk_build @ndcode/uglify-js
 npm install
 npm link
index f9989da..e45b1d5 100644 (file)
     "@ndcode/build_cache": "^0.1.0",
     "@ndcode/clean-css": "^0.1.0",
     "@ndcode/disk_build": "^0.1.1",
+    "@ndcode/uglify-js": "^3.14.5",
     "@rollup/plugin-buble": "^0.21.3",
     "@unicode/unicode-14.0.0": "^1.2.1",
     "assert": "^1.4.1",
     "astring": "^1.8.1",
     "commander": "^2.17.0",
-    "rollup": "^2.60.2",
-    "uglify-js": "^3.14.5"
+    "rollup": "^2.60.2"
   },
   "scripts": {
     "prepare": "rollup -c rollup.config.js"
index b70cf7d..bbd7be1 100644 (file)
@@ -294,6 +294,20 @@ function isPrivateFieldAccess(node) {
 pp.parseExprSubscripts = function(refDestructuringErrors, forInit, allowHTML) { // Nick allowHTML
   let startPos = this.start, startLoc = this.startLoc
   let expr = this.parseExprAtom(refDestructuringErrors, forInit)
+
+  // Nick
+  if (
+    allowHTML &&
+    expr.type === "Identifier" &&
+    expr.name === "$" &&
+    this.eat(tt.braceL)
+  ) {
+    let node = this.startNodeAt(startPos, startLoc)
+    node.argument = this.parseExpression()
+    this.expect(tt.braceR)
+    expr = this.finishNode(node, "InterpolateExpression")
+  }
+
   if (expr.type === "ArrowFunctionExpression" && this.input.slice(this.lastTokStart, this.lastTokEnd) !== ")")
     return expr
   let result = this.parseSubscripts(expr, startPos, startLoc, false, forInit, allowHTML) // Nick allowHTML
diff --git a/test_interpolate.jst b/test_interpolate.jst
new file mode 100644 (file)
index 0000000..15e519e
--- /dev/null
@@ -0,0 +1,32 @@
+_out = []
+_out.push('<!doctype html>')
+html(lang="en") {
+  head {
+    _out.push('<!-- Required meta tags -->')
+    meta(charset="utf-8") {}
+    meta(name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no") {}
+    _out.push('<!-- Bootstrap CSS -->')
+    link(rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous") {}
+    title {
+      'Hello, world!'
+    }
+  }
+  body {
+    h1 {
+      'Hello, world!'
+    }
+    _out.push('<!-- Optional JavaScript -->')
+    _out.push('<!-- jQuery first, then Popper.js, then Bootstrap JS -->')
+    script(src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous") {}
+    script(src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous") {}
+    script(src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous") {}
+    let hello1 = 'HELLO'
+    let world1 = 'WORLD'
+    script {
+      let hello = ${JSON.stringify(hello1)}
+      let world = ${JSON.stringify(world1)}
+      console.log(`oh, ${hello}, \${world}`)
+    }
+  }
+}
+console.log(_out.join(''))
index fc87ae5..d633565 100644 (file)
@@ -88,7 +88,8 @@ visitors.SwitchCase = (node, st, c) => {
 }
 visitors.ReturnStatement =
 visitors.YieldExpression =
-visitors.AwaitExpression = (node, st, c) => {
+visitors.AwaitExpression =
+visitors.InterpolateExpression = (node, st, c) => {
   if (node.argument) node.argument = c(node.argument, st, 'Expression')
   return node
 }
index 3b62c17..83cf467 100644 (file)
@@ -24,7 +24,7 @@
 let assert = require('assert')
 let astring = require('astring')
 let transform = require('./transform')
-let uglify_js = require('uglify-js')
+let uglify_js = require('@ndcode/uglify-js')
 
 let expr_to_tag = (node, context, html_allowed, call_allowed) => {
   if (node.type === 'Identifier')
@@ -122,8 +122,8 @@ let html_body = (context, st, c) => {
         prefix +=
           value_expr.value
             .toString()
-            .replace('&', '&amp;')
-            .replace('"', '&quot;')
+            .replaceAll('&', '&amp;')
+            .replaceAll('"', '&quot;')
       else {
         let expr1 = {
           type: 'Literal',
@@ -162,7 +162,7 @@ let html_body = (context, st, c) => {
                   },
                   property: {
                     type: 'Identifier',
-                    name: 'replace'
+                    name: 'replaceAll'
                   },
                   computed: false
                 },
@@ -179,7 +179,7 @@ let html_body = (context, st, c) => {
               },
               property: {
                 type: 'Identifier',
-                name: 'replace'
+                name: 'replaceAll'
               },
               computed: false
             },
@@ -204,6 +204,7 @@ let html_body = (context, st, c) => {
 
   let result = []
   let body = c(context.body, st, 'Statement').body
+  let prefix_is_template = false
   if (tag === 'script') {
     let program = {type: 'Program', body, sourceType: 'script'}
     // simple way
@@ -211,11 +212,22 @@ let html_body = (context, st, c) => {
     // uglified way
     let render = uglify_js.minify(
       uglify_js.AST_Node.from_mozilla_ast(program),
-      {compress: true, mangle: true}
+      {
+        compress: true,
+        mangle: true,
+        output: {interpolate: true}
+      }
     )
     if (render.error)
       throw render.error
-    prefix += render.code // we simply assume it does not contain </script> tags, should HTML escape it? 
+    let code = render.code
+    if (code.includes('(${')) {
+      prefix = prefix.replaceAll('\\', '\\\\').replaceAll('${', '\\${')
+      prefix_is_template = true
+    }
+    else
+      code = code.replaceAll('\\${', '${').replaceAll('\\\\', '\\')
+    prefix += code
   }
   else if (body.length !== 0) {
     let expr1 = {
@@ -266,10 +278,29 @@ let html_body = (context, st, c) => {
   )
     prefix += '</' + tag + '>'
   if (prefix.length !== 0) {
-    let expr1 = {
-      type: 'Literal',
-      value: prefix
-    }
+    let expr1 = prefix_is_template ?
+      // note: we are cheating a bit here because prefix contains some (${...})
+      // substitutions to be done by the server, and the contents of those are
+      // meant to be given in AST form in expressions list, but we can get away
+      // with it because UglifyJS uses the raw form of the quasis string which
+      // can distinguish between the (${...)} for server and \${...} for client
+      {
+        type: 'TemplateLiteral',
+        expressions: [],
+        quasis: [
+          {
+            type: 'TemplateExpression',
+            value: {
+              raw: prefix.replaceAll('`', '\\`')
+            },
+            tail: true
+          }
+        ]
+      } :
+      {
+        type: 'Literal',
+        value: prefix
+      }
     expr = expr === undefined ? expr1 : {
       type: 'BinaryExpression',
       left: expr,
@@ -329,8 +360,8 @@ visitors.ExpressionStatement = (node, st, c) => {
             value:
               node.expression.value
                 .toString()
-                .replace('&', '&amp;')
-                .replace('<', '&lt;')
+                .replaceAll('&', '&amp;')
+                .replaceAll('<', '&lt;')
           }
         ]
       }
@@ -367,7 +398,7 @@ visitors.ExpressionStatement = (node, st, c) => {
                   object: c(node.expression, st, 'Expression'),
                   property: {
                     type: 'Identifier',
-                    name: 'replace'
+                    name: 'replaceAll'
                   },
                   computed: false
                 },
@@ -384,7 +415,7 @@ visitors.ExpressionStatement = (node, st, c) => {
               },
               property: {
                 type: 'Identifier',
-                name: 'replace'
+                name: 'replaceAll'
               },
               computed: false
             },