|
| 1 | +import {Parser, TokContext, tokTypes as tt} from "acorn"; |
| 2 | +import {Sourcemap} from "./sourcemap.js"; |
| 3 | + |
| 4 | +const CODE_DOLLAR = 36; |
| 5 | +const CODE_BACKSLASH = 92; |
| 6 | +const CODE_BACKTICK = 96; |
| 7 | +const CODE_BRACEL = 123; |
| 8 | + |
| 9 | +export function transpileTag(input, tag = "", raw = false) { |
| 10 | + const options = {ecmaVersion: 13, sourceType: "module"}; |
| 11 | + const template = TemplateParser.parse(input, options); |
| 12 | + const source = new Sourcemap(input); |
| 13 | + escapeTemplateElements(source, template, raw); |
| 14 | + source.insertLeft(template.start, tag + "`"); |
| 15 | + source.insertRight(template.end, "`"); |
| 16 | + return String(source); |
| 17 | +} |
| 18 | + |
| 19 | +class TemplateParser extends Parser { |
| 20 | + constructor(...args) { |
| 21 | + super(...args); |
| 22 | + // Initialize the type so that we're inside a backQuote |
| 23 | + this.type = tt.backQuote; |
| 24 | + this.exprAllowed = false; |
| 25 | + } |
| 26 | + initialContext() { |
| 27 | + // Provide our custom TokContext |
| 28 | + return [o_tmpl]; |
| 29 | + } |
| 30 | + parseTopLevel(body) { |
| 31 | + // Fix for nextToken calling finishToken(tt.eof) |
| 32 | + if (this.type === tt.eof) this.value = ""; |
| 33 | + // Based on acorn.Parser.parseTemplate |
| 34 | + const isTagged = true; |
| 35 | + body.expressions = []; |
| 36 | + let curElt = this.parseTemplateElement({isTagged}); |
| 37 | + body.quasis = [curElt]; |
| 38 | + while (this.type !== tt.eof) { |
| 39 | + this.expect(tt.dollarBraceL); |
| 40 | + body.expressions.push(this.parseExpression()); |
| 41 | + this.expect(tt.braceR); |
| 42 | + body.quasis.push((curElt = this.parseTemplateElement({isTagged}))); |
| 43 | + } |
| 44 | + curElt.tail = true; |
| 45 | + this.next(); |
| 46 | + this.finishNode(body, "TemplateLiteral"); |
| 47 | + this.expect(tt.eof); |
| 48 | + return body; |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +// Based on acorn’s q_tmpl. We will use this to initialize the |
| 53 | +// parser context so our `readTemplateToken` override is called. |
| 54 | +// `readTemplateToken` is based on acorn's `readTmplToken` which |
| 55 | +// is used inside template literals. Our version allows backQuotes. |
| 56 | +const o_tmpl = new TokContext( |
| 57 | + "`", // token |
| 58 | + true, // isExpr |
| 59 | + true, // preserveSpace |
| 60 | + (parser) => readTemplateToken.call(parser) // override |
| 61 | +); |
| 62 | + |
| 63 | +// This is our custom override for parsing a template that allows backticks. |
| 64 | +// Based on acorn's readInvalidTemplateToken. |
| 65 | +function readTemplateToken() { |
| 66 | + out: for (; this.pos < this.input.length; this.pos++) { |
| 67 | + switch (this.input.charCodeAt(this.pos)) { |
| 68 | + case CODE_BACKSLASH: { |
| 69 | + if (this.pos < this.input.length - 1) ++this.pos; // not a terminal slash |
| 70 | + break; |
| 71 | + } |
| 72 | + case CODE_DOLLAR: { |
| 73 | + if (this.input.charCodeAt(this.pos + 1) === CODE_BRACEL) { |
| 74 | + if (this.pos === this.start && this.type === tt.invalidTemplate) { |
| 75 | + this.pos += 2; |
| 76 | + return this.finishToken(tt.dollarBraceL); |
| 77 | + } |
| 78 | + break out; |
| 79 | + } |
| 80 | + break; |
| 81 | + } |
| 82 | + } |
| 83 | + } |
| 84 | + return this.finishToken(tt.invalidTemplate, this.input.slice(this.start, this.pos)); |
| 85 | +} |
| 86 | + |
| 87 | +function escapeTemplateElements(source, {quasis}, raw) { |
| 88 | + for (const quasi of quasis) { |
| 89 | + if (raw) { |
| 90 | + interpolateBacktick(source, quasi); |
| 91 | + } else { |
| 92 | + escapeBacktick(source, quasi); |
| 93 | + escapeBackslash(source, quasi); |
| 94 | + } |
| 95 | + } |
| 96 | + if (raw) interpolateTerminalBackslash(source); |
| 97 | +} |
| 98 | + |
| 99 | +function escapeBacktick(source, {start, end}) { |
| 100 | + const input = source._input; |
| 101 | + for (let i = start; i < end; ++i) { |
| 102 | + if (input.charCodeAt(i) === CODE_BACKTICK) { |
| 103 | + source.insertRight(i, "\\"); |
| 104 | + } |
| 105 | + } |
| 106 | +} |
| 107 | + |
| 108 | +function interpolateBacktick(source, {start, end}) { |
| 109 | + const input = source._input; |
| 110 | + let oddBackslashes = false; |
| 111 | + for (let i = start; i < end; ++i) { |
| 112 | + switch (input.charCodeAt(i)) { |
| 113 | + case CODE_BACKSLASH: { |
| 114 | + oddBackslashes = !oddBackslashes; |
| 115 | + break; |
| 116 | + } |
| 117 | + case CODE_BACKTICK: { |
| 118 | + if (!oddBackslashes) { |
| 119 | + let j = i + 1; |
| 120 | + while (j < end && input.charCodeAt(j) === CODE_BACKTICK) ++j; |
| 121 | + source.replaceRight(i, j, `\${'${"`".repeat(j - i)}'}`); |
| 122 | + i = j - 1; |
| 123 | + } |
| 124 | + // fall through |
| 125 | + } |
| 126 | + default: { |
| 127 | + oddBackslashes = false; |
| 128 | + break; |
| 129 | + } |
| 130 | + } |
| 131 | + } |
| 132 | +} |
| 133 | + |
| 134 | +function escapeBackslash(source, {start, end}) { |
| 135 | + const input = source._input; |
| 136 | + let afterDollar = false; |
| 137 | + let oddBackslashes = false; |
| 138 | + for (let i = start; i < end; ++i) { |
| 139 | + switch (input.charCodeAt(i)) { |
| 140 | + case CODE_DOLLAR: { |
| 141 | + afterDollar = true; |
| 142 | + oddBackslashes = false; |
| 143 | + break; |
| 144 | + } |
| 145 | + case CODE_BACKSLASH: { |
| 146 | + oddBackslashes = !oddBackslashes; |
| 147 | + if (afterDollar && input.charCodeAt(i + 1) === CODE_BRACEL) continue; |
| 148 | + if (oddBackslashes && input.charCodeAt(i + 1) === CODE_DOLLAR && input.charCodeAt(i + 2) === CODE_BRACEL) |
| 149 | + continue; |
| 150 | + source.insertRight(i, "\\"); |
| 151 | + break; |
| 152 | + } |
| 153 | + default: { |
| 154 | + afterDollar = false; |
| 155 | + oddBackslashes = false; |
| 156 | + break; |
| 157 | + } |
| 158 | + } |
| 159 | + } |
| 160 | +} |
| 161 | + |
| 162 | +function interpolateTerminalBackslash(source) { |
| 163 | + const input = source._input; |
| 164 | + let oddBackslashes = false; |
| 165 | + for (let i = input.length - 1; i >= 0; i--) { |
| 166 | + if (input.charCodeAt(i) === CODE_BACKSLASH) oddBackslashes = !oddBackslashes; |
| 167 | + else break; |
| 168 | + } |
| 169 | + if (oddBackslashes) source.replaceRight(input.length - 1, input.length, "${'\\\\'}"); |
| 170 | +} |
0 commit comments