Merge branch 'feature/handlebars-migration' of github.com:Budibase/budibase into form-builder

This commit is contained in:
Andrew Kingston 2021-01-21 12:14:09 +00:00
commit 4cdca6f196
14 changed files with 160 additions and 83 deletions

View File

@ -11,9 +11,7 @@
on:click on:click
class:big={subtitle != null} class:big={subtitle != null}
{...$$restProps}> {...$$restProps}>
{#if icon} {#if icon}<i class={icon} />{/if}
<i class={icon} />
{/if}
<div class="content"> <div class="content">
<div class="title">{title}</div> <div class="title">{title}</div>
{#if subtitle != null} {#if subtitle != null}

View File

@ -1,12 +1,6 @@
<script> <script>
import groupBy from "lodash/fp/groupBy" import groupBy from "lodash/fp/groupBy"
import { import { Button, TextArea, Drawer, Heading, Spacer } from "@budibase/bbui"
Button,
TextArea,
Drawer,
Heading,
Spacer,
} from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -38,7 +32,9 @@
<Heading extraSmall>Tables</Heading> <Heading extraSmall>Tables</Heading>
<ul> <ul>
{#each context as { readableBinding }} {#each context as { readableBinding }}
<li on:click={() => addToText(readableBinding)}>{readableBinding}</li> <li on:click={() => addToText(readableBinding)}>
{readableBinding}
</li>
{/each} {/each}
</ul> </ul>
{/if} {/if}
@ -46,7 +42,9 @@
<Heading extraSmall>Components</Heading> <Heading extraSmall>Components</Heading>
<ul> <ul>
{#each instance as { readableBinding }} {#each instance as { readableBinding }}
<li on:click={() => addToText(readableBinding)}>{readableBinding}</li> <li on:click={() => addToText(readableBinding)}>
{readableBinding}
</li>
{/each} {/each}
</ul> </ul>
{/if} {/if}

View File

@ -2,8 +2,10 @@
import iconData from "./icons.js" import iconData from "./icons.js"
const categories = Object.keys(iconData) const categories = Object.keys(iconData)
const icons = Object.keys(iconData).reduce((acc, cat) => [...acc, ...Object.keys(iconData[cat])], []) const icons = Object.keys(iconData).reduce(
(acc, cat) => [...acc, ...Object.keys(iconData[cat])],
[]
)
</script> </script>
<script> <script>

View File

@ -177,6 +177,7 @@ exports.serveApp = async function(ctx) {
}) })
const appHbs = fs.readFileSync(`${__dirname}/templates/app.hbs`, "utf8") const appHbs = fs.readFileSync(`${__dirname}/templates/app.hbs`, "utf8")
console.log(appHbs)
ctx.body = await processString(appHbs, { ctx.body = await processString(appHbs, {
head, head,
body: html, body: html,

View File

@ -22,7 +22,9 @@
<title>{title}</title> <title>{title}</title>
<link rel="icon" type="image/png" href={favicon} /> <link rel="icon" type="image/png" href={favicon} />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" /> <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
<link href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css" rel="stylesheet"> <link
href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css"
rel="stylesheet" />
<style> <style>
html, html,
body { body {

View File

@ -10,6 +10,4 @@
export let color = "#000" export let color = "#000"
</script> </script>
<i <i style={`color: ${color};`} class={`${icon} ${size}`} />
style={`color: ${color};`}
class={`${icon} ${size}`} />

View File

@ -191,4 +191,4 @@ module.exports = {
// Whether to use watchman for file crawling // Whether to use watchman for file crawling
// watchman: true, // watchman: true,
}; }

View File

@ -11,7 +11,7 @@ export default {
name: "string-templates", name: "string-templates",
exports: "named", exports: "named",
globals: { globals: {
"fs": "fs", fs: "fs",
}, },
}, },
external: ["fs"], external: ["fs"],

View File

@ -1,60 +1,81 @@
const { HelperFunctions } = require("../helpers") const { HelperFunctions } = require("../helpers")
const { swapStrings, isAlphaNumeric, FIND_HBS_REGEX, includesAny } = require("../utilities") const {
swapStrings,
isAlphaNumeric,
FIND_HBS_REGEX,
includesAny,
} = require("../utilities")
function handleProcessor(string, match, fn) { class Preprocessor {
const output = fn(match) constructor(name, fn) {
const idx = string.indexOf(match) this.name = name
return swapStrings(string, idx, match.length, output) this.fn = fn
}
process(fullString, statement) {
const output = this.fn(statement)
const idx = fullString.indexOf(statement)
return swapStrings(fullString, idx, statement.length, output)
}
} }
function swapToDotNotation(statement) { const PROCESSORS = [
let startBraceIdx = statement.indexOf("[") new Preprocessor("swap-to-dot-notation", statement => {
let lastIdx = 0 let startBraceIdx = statement.indexOf("[")
while (startBraceIdx !== -1) { let lastIdx = 0
// if the character previous to the literal specifier is alpha-numeric this should happen while (startBraceIdx !== -1) {
if (isAlphaNumeric(statement.charAt(startBraceIdx - 1))) { // if the character previous to the literal specifier is alpha-numeric this should happen
statement = swapStrings(statement, startBraceIdx + lastIdx, 1, ".[") if (isAlphaNumeric(statement.charAt(startBraceIdx - 1))) {
statement = swapStrings(statement, startBraceIdx + lastIdx, 1, ".[")
}
lastIdx = startBraceIdx + 1
startBraceIdx = statement.substring(lastIdx + 1).indexOf("[")
} }
lastIdx = startBraceIdx + 1 return statement
startBraceIdx = statement.substring(lastIdx + 1).indexOf("[") }),
}
return statement
}
function handleSpacesInProperties(statement) { new Preprocessor("handle-spaces-in-properties", statement => {
// exclude helpers and brackets, regex will only find double brackets // exclude helpers and brackets, regex will only find double brackets
const exclusions = HelperFunctions.concat(["{{", "}}"]) const exclusions = HelperFunctions.concat(["{{", "}}"])
// find all the parts split by spaces // find all the parts split by spaces
const splitBySpaces = statement.split(" ") const splitBySpaces = statement.split(" ")
// remove the excluded elements // remove the excluded elements
const propertyParts = splitBySpaces.filter(part => exclusions.indexOf(part) === -1) const propertyParts = splitBySpaces.filter(
// rebuild to get the full property part => exclusions.indexOf(part) === -1
const fullProperty = propertyParts.join(" ") )
// now work out the dot notation layers and split them up // rebuild to get the full property
const propertyLayers = fullProperty.split(".") const fullProperty = propertyParts.join(" ")
// find the layers which need to be wrapped and wrap them // now work out the dot notation layers and split them up
for (let layer of propertyLayers) { const propertyLayers = fullProperty.split(".")
if (layer.indexOf(" ") !== -1) { // find the layers which need to be wrapped and wrap them
statement = swapStrings(statement, statement.indexOf(layer), layer.length, `[${layer}]`) for (let layer of propertyLayers) {
if (layer.indexOf(" ") !== -1) {
statement = swapStrings(
statement,
statement.indexOf(layer),
layer.length,
`[${layer}]`
)
}
} }
} // remove the edge case of double brackets being entered (in-case user already has specified)
// remove the edge case of double brackets being entered (in-case user already has specified) return statement.replace(/\[\[/g, "[").replace(/]]/g, "]")
return statement.replace(/\[\[/g, "[").replace(/]]/g, "]") }),
}
function finalise(statement) { new Preprocessor("finalise", statement => {
let insideStatement = statement.slice(2, statement.length - 2) let insideStatement = statement.slice(2, statement.length - 2)
if (insideStatement.charAt(0) === " ") { if (insideStatement.charAt(0) === " ") {
insideStatement = insideStatement.slice(1) insideStatement = insideStatement.slice(1)
} }
if (insideStatement.charAt(insideStatement.length - 1) === " ") { if (insideStatement.charAt(insideStatement.length - 1) === " ") {
insideStatement = insideStatement.slice(0, insideStatement.length - 1) insideStatement = insideStatement.slice(0, insideStatement.length - 1)
} }
if (includesAny(insideStatement, HelperFunctions)) { if (includesAny(insideStatement, HelperFunctions)) {
insideStatement = `(${insideStatement})` insideStatement = `(${insideStatement})`
} }
return `{{ all ${insideStatement} }}` return `{{ all ${insideStatement} }}`
} })
]
/** /**
* When running handlebars statements to execute on the context of the automation it possible user's may input handlebars * When running handlebars statements to execute on the context of the automation it possible user's may input handlebars
@ -67,17 +88,16 @@ function finalise(statement) {
* @param {string} string The string which *may* contain handlebars statements, it is OK if it does not contain any. * @param {string} string The string which *may* contain handlebars statements, it is OK if it does not contain any.
* @returns {string} The string that was input with processed up handlebars statements as required. * @returns {string} The string that was input with processed up handlebars statements as required.
*/ */
module.exports.preprocess = (string) => { module.exports.preprocess = string => {
let preprocessors = [swapToDotNotation, handleSpacesInProperties, finalise] for (let processor of PROCESSORS) {
for (let processor of preprocessors) { // re-run search each time incase previous processor updated/removed a match
// re-run search each time incase previous cleaner update/removed a match
let regex = new RegExp(FIND_HBS_REGEX) let regex = new RegExp(FIND_HBS_REGEX)
let matches = string.match(regex) let matches = string.match(regex)
if (matches == null) { if (matches == null) {
continue continue
} }
for (let match of matches) { for (let match of matches) {
string = handleProcessor(string, match, processor) string = processor.process(string, match)
} }
} }
return string return string

View File

@ -12,7 +12,7 @@ const HelperFunctionBuiltin = [
"#each", "#each",
"#with", "#with",
"lookup", "lookup",
"log" "log",
] ]
const HelperFunctionNames = { const HelperFunctionNames = {
@ -27,17 +27,19 @@ const HELPERS = [
}), }),
// this help is applied to all statements // this help is applied to all statements
new Helper(HelperFunctionNames.ALL, value => { new Helper(HelperFunctionNames.ALL, value => {
let text = new SafeString(unescape(value).replace(/&amp;/g, '&')) let text = new SafeString(unescape(value).replace(/&amp;/g, "&"))
if (text == null || typeof text !== "string") { if (text == null || typeof text !== "string") {
return text return text
} }
return text.replace(/[<>]/g, tag => { return text.replace(/[<>]/g, tag => {
return HTML_SWAPS[tag] || tag return HTML_SWAPS[tag] || tag
}) })
}) }),
] ]
module.exports.HelperFunctions = Object.values(HelperFunctionNames).concat(HelperFunctionBuiltin) module.exports.HelperFunctions = Object.values(HelperFunctionNames).concat(
HelperFunctionBuiltin
)
module.exports.registerAll = handlebars => { module.exports.registerAll = handlebars => {
for (let helper of HELPERS) { for (let helper of HELPERS) {

View File

@ -1,8 +1,8 @@
const ALPHA_NUMERIC_REGEX = /^[A-Za-z0-9]+$/g const ALPHA_NUMERIC_REGEX = /^[A-Za-z0-9]+$/g
module.exports.FIND_HBS_REGEX = /{{[^}}]*}}/g module.exports.FIND_HBS_REGEX = /{{([^{}])+}}/g
module.exports.isAlphaNumeric = (char) => { module.exports.isAlphaNumeric = char => {
return char.match(ALPHA_NUMERIC_REGEX) return char.match(ALPHA_NUMERIC_REGEX)
} }

View File

@ -11,6 +11,13 @@ describe("Test that the string processing works correctly", () => {
expect(output).toBe("templating is easy") expect(output).toBe("templating is easy")
}) })
it("should process a literal template", async () => {
const output = await processString("derp is {{{ adjective }}}", {
adjective: "derp"
})
expect(output).toBe("derp is derp")
})
it("should fail gracefully when wrong type passed in", async () => { it("should fail gracefully when wrong type passed in", async () => {
let error = null let error = null
try { try {

View File

@ -33,3 +33,19 @@ describe("Handling context properties with spaces in their name", () => {
expect(output).toBe("testcase 1") expect(output).toBe("testcase 1")
}) })
}) })
describe("attempt some complex problems", () => {
it("should be able to handle a very complex handlebars statement", async () => {
const context = {
"New Repeater": {
"Get Actors": {
"first_name": "Bob",
"last_name": "Bobert"
},
},
}
const hbs = "{{ New Repeater.Get Actors.first_name }} {{ New Repeater.Get Actors.last_name }}"
const output = await processString(hbs, context)
expect(output).toBe("Bob Bobert")
})
})

View File

@ -0,0 +1,33 @@
const { processString } = require("../src/index")
describe("specific test case for whether or not full app template can still be rendered", () => {
it("should be able to render the app template", async () => {
const template =
`<!doctype html>
<html>
<head>
{{{head}}}
</head>
<script>
window["##BUDIBASE_APP_ID##"] = "{{appId}}"
</script>
{{{body}}}
</html>`
const context = {
appId: "App1",
head: "<title>App</title>",
body: "<body><p>App things</p></body>"
}
const output = await processString(template, context)
expect(output).toBe(`<!doctype html>
<html>
<head>
<title>App</title>
</head>
<script>
window["##BUDIBASE_APP_ID##"] = "App1"
</script>
<body><p>App things</p></body>
</html>`)
})
})