Initial attempt at transpiling HBS to JS.
This commit is contained in:
parent
8d9794df5c
commit
3d13030aa1
|
@ -8,6 +8,7 @@
|
||||||
Tab,
|
Tab,
|
||||||
Body,
|
Body,
|
||||||
Layout,
|
Layout,
|
||||||
|
Button,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
import {
|
import {
|
||||||
|
@ -15,10 +16,14 @@
|
||||||
decodeJSBinding,
|
decodeJSBinding,
|
||||||
encodeJSBinding,
|
encodeJSBinding,
|
||||||
} from "@budibase/string-templates"
|
} from "@budibase/string-templates"
|
||||||
import { readableToRuntimeBinding } from "builderStore/dataBinding"
|
import {
|
||||||
|
readableToRuntimeBinding,
|
||||||
|
runtimeToReadableBinding,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
import { handlebarsCompletions } from "constants/completions"
|
import { handlebarsCompletions } from "constants/completions"
|
||||||
import { addHBSBinding, addJSBinding } from "./utils"
|
import { addHBSBinding, addJSBinding } from "./utils"
|
||||||
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
||||||
|
import { convertToJS } from "@budibase/string-templates"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -57,6 +62,7 @@
|
||||||
|
|
||||||
const updateValue = val => {
|
const updateValue = val => {
|
||||||
valid = isValid(readableToRuntimeBinding(bindings, val))
|
valid = isValid(readableToRuntimeBinding(bindings, val))
|
||||||
|
console.log(decodeJSBinding(readableToRuntimeBinding(bindings, val)))
|
||||||
if (valid) {
|
if (valid) {
|
||||||
dispatch("change", val)
|
dispatch("change", val)
|
||||||
}
|
}
|
||||||
|
@ -69,8 +75,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a data binding to the expression
|
// Adds a data binding to the expression
|
||||||
const addBinding = binding => {
|
const addBinding = (binding, { forceJS } = {}) => {
|
||||||
if (usingJS) {
|
if (usingJS || forceJS) {
|
||||||
let js = decodeJSBinding(jsValue)
|
let js = decodeJSBinding(jsValue)
|
||||||
js = addJSBinding(js, getCaretPosition(), binding.readableBinding)
|
js = addJSBinding(js, getCaretPosition(), binding.readableBinding)
|
||||||
jsValue = encodeJSBinding(js)
|
jsValue = encodeJSBinding(js)
|
||||||
|
@ -100,6 +106,16 @@
|
||||||
updateValue(jsValue)
|
updateValue(jsValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const convert = () => {
|
||||||
|
const runtime = readableToRuntimeBinding(bindings, hbsValue)
|
||||||
|
console.log(runtime)
|
||||||
|
const runtimeJs = encodeJSBinding(convertToJS(runtime))
|
||||||
|
jsValue = runtimeToReadableBinding(bindings, runtimeJs)
|
||||||
|
hbsValue = null
|
||||||
|
mode = "JavaScript"
|
||||||
|
addBinding("", { forceJS: true })
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
valid = isValid(readableToRuntimeBinding(bindings, value))
|
valid = isValid(readableToRuntimeBinding(bindings, value))
|
||||||
})
|
})
|
||||||
|
@ -172,6 +188,9 @@
|
||||||
for more details.
|
for more details.
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="convert">
|
||||||
|
<Button secondary on:click={convert}>Convert to JS</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
{#if allowJS}
|
{#if allowJS}
|
||||||
|
@ -306,4 +325,8 @@
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.convert {
|
||||||
|
padding-top: var(--spacing-m);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
const { getHelperList } = require("../helpers")
|
||||||
|
|
||||||
|
function getLayers(fullBlock) {
|
||||||
|
let layers = []
|
||||||
|
while (fullBlock.length) {
|
||||||
|
const start = fullBlock.lastIndexOf("("),
|
||||||
|
end = fullBlock.indexOf(")")
|
||||||
|
let layer
|
||||||
|
if (start === -1 || end === -1) {
|
||||||
|
layer = fullBlock.trim()
|
||||||
|
fullBlock = ""
|
||||||
|
} else {
|
||||||
|
const untrimmed = fullBlock.substring(start, end + 1)
|
||||||
|
layer = untrimmed.substring(1, untrimmed.length - 1).trim()
|
||||||
|
fullBlock =
|
||||||
|
fullBlock.slice(0, start) +
|
||||||
|
fullBlock.slice(start + untrimmed.length + 1, fullBlock.length)
|
||||||
|
}
|
||||||
|
layers.push(layer)
|
||||||
|
}
|
||||||
|
return layers
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVariable(variableName) {
|
||||||
|
return isNaN(parseFloat(variableName)) ? `$("${variableName}")` : variableName
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildList(parts, value) {
|
||||||
|
function build() {
|
||||||
|
return parts
|
||||||
|
.map(part => (part.startsWith("helper") ? part : getVariable(part)))
|
||||||
|
.join(", ")
|
||||||
|
}
|
||||||
|
if (!value) {
|
||||||
|
return parts.length > 1 ? `...[${build()}]` : build()
|
||||||
|
} else {
|
||||||
|
return parts.length === 0 ? value : `...[${value}, ${build()}]`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitBySpace(layer) {
|
||||||
|
const parts = []
|
||||||
|
let started = null,
|
||||||
|
last = 0
|
||||||
|
for (let index = 0; index < layer.length; index++) {
|
||||||
|
const char = layer[index]
|
||||||
|
if (char === "[" && started == null) {
|
||||||
|
started = index
|
||||||
|
} else if (char === "]" && started != null && layer[index + 1] !== ".") {
|
||||||
|
parts.push(layer.substring(started, index + 1).trim())
|
||||||
|
started = null
|
||||||
|
last = index
|
||||||
|
} else if (started == null && char === " ") {
|
||||||
|
parts.push(layer.substring(last, index).trim())
|
||||||
|
last = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!layer.startsWith("[")) {
|
||||||
|
parts.push(layer.substring(last, layer.length).trim())
|
||||||
|
}
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.convertHBSBlock = (block, blockNumber) => {
|
||||||
|
const braceLength = block[2] === "{" ? 3 : 2
|
||||||
|
block = block.substring(braceLength, block.length - braceLength).trim()
|
||||||
|
const layers = getLayers(block)
|
||||||
|
|
||||||
|
let value = null
|
||||||
|
const list = getHelperList()
|
||||||
|
for (let layer of layers) {
|
||||||
|
const parts = splitBySpace(layer)
|
||||||
|
if (value || parts.length > 1) {
|
||||||
|
// first of layer should always be the helper
|
||||||
|
const helper = parts.splice(0, 1)
|
||||||
|
if (list[helper]) {
|
||||||
|
value = `helpers.${helper}(${buildList(parts, value)})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// no helpers
|
||||||
|
else {
|
||||||
|
value = getVariable(parts[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// split by space will remove square brackets
|
||||||
|
return { variable: `var${blockNumber}`, value }
|
||||||
|
}
|
|
@ -23,6 +23,9 @@ const ADDED_HELPERS = {
|
||||||
duration: duration,
|
duration: duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.externalCollections = EXTERNAL_FUNCTION_COLLECTIONS
|
||||||
|
exports.addedHelpers = ADDED_HELPERS
|
||||||
|
|
||||||
exports.registerAll = handlebars => {
|
exports.registerAll = handlebars => {
|
||||||
for (let [name, helper] of Object.entries(ADDED_HELPERS)) {
|
for (let [name, helper] of Object.entries(ADDED_HELPERS)) {
|
||||||
handlebars.registerHelper(name, helper)
|
handlebars.registerHelper(name, helper)
|
||||||
|
|
|
@ -7,6 +7,7 @@ const {
|
||||||
HelperFunctionBuiltin,
|
HelperFunctionBuiltin,
|
||||||
LITERAL_MARKER,
|
LITERAL_MARKER,
|
||||||
} = require("./constants")
|
} = require("./constants")
|
||||||
|
const { getHelperList } = require("./list")
|
||||||
|
|
||||||
const HTML_SWAPS = {
|
const HTML_SWAPS = {
|
||||||
"<": "<",
|
"<": "<",
|
||||||
|
@ -91,3 +92,5 @@ module.exports.unregisterAll = handlebars => {
|
||||||
// unregister all imported helpers
|
// unregister all imported helpers
|
||||||
externalHandlebars.unregisterAll(handlebars)
|
externalHandlebars.unregisterAll(handlebars)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports.getHelperList = getHelperList
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const { atob } = require("../utilities")
|
const { atob } = require("../utilities")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
const { LITERAL_MARKER } = require("../helpers/constants")
|
const { LITERAL_MARKER } = require("../helpers/constants")
|
||||||
|
const { getHelperList } = require("./list")
|
||||||
|
|
||||||
// The method of executing JS scripts depends on the bundle being built.
|
// The method of executing JS scripts depends on the bundle being built.
|
||||||
// This setter is used in the entrypoint (either index.cjs or index.mjs).
|
// This setter is used in the entrypoint (either index.cjs or index.mjs).
|
||||||
|
@ -45,6 +46,7 @@ module.exports.processJS = (handlebars, context) => {
|
||||||
// app context.
|
// app context.
|
||||||
const sandboxContext = {
|
const sandboxContext = {
|
||||||
$: path => getContextValue(path, cloneDeep(context)),
|
$: path => getContextValue(path, cloneDeep(context)),
|
||||||
|
helpers: getHelperList(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a sandbox with our context and run the JS
|
// Create a sandbox with our context and run the JS
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
const externalHandlebars = require("./external")
|
||||||
|
const helperList = require("@budibase/handlebars-helpers")
|
||||||
|
|
||||||
|
module.exports.getHelperList = () => {
|
||||||
|
let constructed = []
|
||||||
|
for (let collection of externalHandlebars.externalCollections) {
|
||||||
|
constructed.push(helperList[collection]())
|
||||||
|
}
|
||||||
|
const fullMap = {}
|
||||||
|
for (let collection of constructed) {
|
||||||
|
for (let [key, func] of Object.entries(collection)) {
|
||||||
|
fullMap[key] = func
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let key of Object.keys(externalHandlebars.addedHelpers)) {
|
||||||
|
fullMap[key] = externalHandlebars.addedHelpers[key]
|
||||||
|
}
|
||||||
|
return fullMap
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ module.exports.doesContainStrings = templates.doesContainStrings
|
||||||
module.exports.doesContainString = templates.doesContainString
|
module.exports.doesContainString = templates.doesContainString
|
||||||
module.exports.disableEscaping = templates.disableEscaping
|
module.exports.disableEscaping = templates.disableEscaping
|
||||||
module.exports.findHBSBlocks = templates.findHBSBlocks
|
module.exports.findHBSBlocks = templates.findHBSBlocks
|
||||||
|
module.exports.convertToJS = templates.convertToJS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use vm2 to run JS scripts in a node env
|
* Use vm2 to run JS scripts in a node env
|
||||||
|
|
|
@ -8,6 +8,7 @@ const {
|
||||||
FIND_ANY_HBS_REGEX,
|
FIND_ANY_HBS_REGEX,
|
||||||
findDoubleHbsInstances,
|
findDoubleHbsInstances,
|
||||||
} = require("./utilities")
|
} = require("./utilities")
|
||||||
|
const { convertHBSBlock } = require("./conversion")
|
||||||
|
|
||||||
const hbsInstance = handlebars.create()
|
const hbsInstance = handlebars.create()
|
||||||
registerAll(hbsInstance)
|
registerAll(hbsInstance)
|
||||||
|
@ -342,3 +343,31 @@ module.exports.findHBSBlocks = string => {
|
||||||
module.exports.doesContainString = (template, string) => {
|
module.exports.doesContainString = (template, string) => {
|
||||||
return exports.doesContainStrings(template, [string])
|
return exports.doesContainStrings(template, [string])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports.convertToJS = hbs => {
|
||||||
|
const blocks = exports.findHBSBlocks(hbs)
|
||||||
|
let js = "return `",
|
||||||
|
prevBlock = null
|
||||||
|
const variables = {}
|
||||||
|
if (blocks.length === 0) {
|
||||||
|
js += hbs
|
||||||
|
}
|
||||||
|
let count = 1
|
||||||
|
for (let block of blocks) {
|
||||||
|
let stringPart = hbs
|
||||||
|
if (prevBlock) {
|
||||||
|
stringPart = stringPart.split(prevBlock)[1]
|
||||||
|
}
|
||||||
|
stringPart = stringPart.split(block)[0]
|
||||||
|
prevBlock = block
|
||||||
|
const { variable, value } = convertHBSBlock(block, count++)
|
||||||
|
variables[variable] = value
|
||||||
|
js += `${stringPart.split()}\${${variable}}`
|
||||||
|
}
|
||||||
|
let varBlock = ""
|
||||||
|
for (let [variable, value] of Object.entries(variables)) {
|
||||||
|
varBlock += `const ${variable} = ${value};\n`
|
||||||
|
}
|
||||||
|
js += "`;"
|
||||||
|
return `${varBlock}${js}`
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ export const doesContainStrings = templates.doesContainStrings
|
||||||
export const doesContainString = templates.doesContainString
|
export const doesContainString = templates.doesContainString
|
||||||
export const disableEscaping = templates.disableEscaping
|
export const disableEscaping = templates.disableEscaping
|
||||||
export const findHBSBlocks = templates.findHBSBlocks
|
export const findHBSBlocks = templates.findHBSBlocks
|
||||||
|
export const convertToJS = templates.convertToJS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use polyfilled vm to run JS scripts in a browser Env
|
* Use polyfilled vm to run JS scripts in a browser Env
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
const {
|
||||||
|
convertToJS
|
||||||
|
} = require("../src/index.cjs")
|
||||||
|
|
||||||
|
function checkLines(response, lines) {
|
||||||
|
const toCheck = response.split("\n")
|
||||||
|
let count = 0
|
||||||
|
for (let line of lines) {
|
||||||
|
expect(toCheck[count++]).toBe(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Test that the string processing works correctly", () => {
|
||||||
|
it("should convert string without HBS", () => {
|
||||||
|
const response = convertToJS("Hello my name is Michael")
|
||||||
|
expect(response).toBe("return `Hello my name is Michael`;")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("basic example with square brackets", () => {
|
||||||
|
const response = convertToJS("{{ [query] }}")
|
||||||
|
checkLines(response, [
|
||||||
|
"const var1 = $(\"[query]\");",
|
||||||
|
"return `${var1}`;",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should convert some basic HBS strings", () => {
|
||||||
|
const response = convertToJS("Hello {{ name }}, welcome to {{ company }}!")
|
||||||
|
checkLines(response, [
|
||||||
|
"const var1 = $(\"name\");",
|
||||||
|
"const var2 = $(\"company\");",
|
||||||
|
"return `Hello ${var1}, welcome to ${var2}`;",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle a helper block", () => {
|
||||||
|
const response = convertToJS("This is the average: {{ avg array }}")
|
||||||
|
checkLines(response, [
|
||||||
|
"const var1 = helpers.avg($(\"array\"));",
|
||||||
|
"return `This is the average: ${var1}`;",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle multi-variable helper", () => {
|
||||||
|
const response = convertToJS("This is the average: {{ join ( avg val1 val2 val3 ) }}")
|
||||||
|
checkLines(response, [
|
||||||
|
"const var1 = helpers.join(helpers.avg(...[$(\"val1\"), $(\"val2\"), $(\"val3\")]));",
|
||||||
|
"return `This is the average: ${var1}`;",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle a complex statement", () => {
|
||||||
|
const response = convertToJS("This is the average: {{ join ( avg val1 val2 val3 ) val4 }}")
|
||||||
|
checkLines(response, [
|
||||||
|
"const var1 = helpers.join(...[helpers.avg(...[$(\"val1\"), $(\"val2\"), $(\"val3\")]), $(\"val4\")]);",
|
||||||
|
"return `This is the average: ${var1}`;",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle square brackets", () => {
|
||||||
|
const response = convertToJS("This is: {{ [val thing] }}")
|
||||||
|
checkLines(response, [
|
||||||
|
"const var1 = $(\"[val thing]\");",
|
||||||
|
"return `This is: ${var1}`;",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle square brackets with properties", () => {
|
||||||
|
const response = convertToJS("{{ [user].[_id] }}")
|
||||||
|
checkLines(response, [
|
||||||
|
"const var1 = $(\"[user].[_id]\");",
|
||||||
|
"return `${var1}`;",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle multiple complex statements", () => {
|
||||||
|
const response = convertToJS("average: {{ avg ( abs val1 ) val2 }} add: {{ add 1 2 }}")
|
||||||
|
checkLines(response, [
|
||||||
|
"const var1 = helpers.avg(...[helpers.abs($(\"val1\")), $(\"val2\")]);",
|
||||||
|
"const var2 = helpers.add(...[1, 2]);",
|
||||||
|
"return `average: ${var1} add: ${var2}`;",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue