Browse Source

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

gh-pages
Andrew Kingston 6 years ago
parent
commit
4cdca6f196
  1. 4
      packages/builder/src/components/common/Dropdowns/DropdownItem.svelte
  2. 16
      packages/builder/src/components/design/PropertiesPanel/BindingPanel.svelte
  3. 10
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/IconSelect/IconSelect.svelte
  4. 1
      packages/server/src/api/controllers/static/index.js
  5. 4
      packages/server/src/api/controllers/static/templates/BudibaseApp.svelte
  6. 4
      packages/standard-components/src/Icon.svelte
  7. 2
      packages/string-templates/jest.config.js
  8. 2
      packages/string-templates/rollup.config.js
  9. 130
      packages/string-templates/src/custom/preprocessor.js
  10. 10
      packages/string-templates/src/helpers/index.js
  11. 4
      packages/string-templates/src/utilities.js
  12. 7
      packages/string-templates/test/basic.spec.js
  13. 16
      packages/string-templates/test/escapes.spec.js
  14. 33
      packages/string-templates/test/renderApp.spec.js

4
packages/builder/src/components/common/Dropdowns/DropdownItem.svelte

@ -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}

16
packages/builder/src/components/design/PropertiesPanel/BindingPanel.svelte

@ -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}

10
packages/builder/src/components/design/PropertiesPanel/PropertyControls/IconSelect/IconSelect.svelte

@ -1,9 +1,11 @@
<script context="module"> <script context="module">
import iconData from "./icons.js" import iconData from "./icons.js"
const categories = Object.keys(iconData)
const icons = Object.keys(iconData).reduce((acc, cat) => [...acc, ...Object.keys(iconData[cat])], [])
const categories = Object.keys(iconData)
const icons = Object.keys(iconData).reduce(
(acc, cat) => [...acc, ...Object.keys(iconData[cat])],
[]
)
</script> </script>
<script> <script>
@ -285,4 +287,4 @@
.page-btn:hover { .page-btn:hover {
cursor: pointer; cursor: pointer;
} }
</style> </style>

1
packages/server/src/api/controllers/static/index.js

@ -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,

4
packages/server/src/api/controllers/static/templates/BudibaseApp.svelte

@ -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 {

4
packages/standard-components/src/Icon.svelte

@ -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}`} />

2
packages/string-templates/jest.config.js

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

2
packages/string-templates/rollup.config.js

@ -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"],

130
packages/string-templates/src/custom/preprocessor.js

@ -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
} }
function swapToDotNotation(statement) { process(fullString, statement) {
let startBraceIdx = statement.indexOf("[") const output = this.fn(statement)
let lastIdx = 0 const idx = fullString.indexOf(statement)
while (startBraceIdx !== -1) { return swapStrings(fullString, idx, statement.length, output)
// if the character previous to the literal specifier is alpha-numeric this should happen
if (isAlphaNumeric(statement.charAt(startBraceIdx - 1))) {
statement = swapStrings(statement, startBraceIdx + lastIdx, 1, ".[")
}
lastIdx = startBraceIdx + 1
startBraceIdx = statement.substring(lastIdx + 1).indexOf("[")
} }
return statement
} }
function handleSpacesInProperties(statement) { const PROCESSORS = [
// exclude helpers and brackets, regex will only find double brackets new Preprocessor("swap-to-dot-notation", statement => {
const exclusions = HelperFunctions.concat(["{{", "}}"]) let startBraceIdx = statement.indexOf("[")
// find all the parts split by spaces let lastIdx = 0
const splitBySpaces = statement.split(" ") while (startBraceIdx !== -1) {
// remove the excluded elements // if the character previous to the literal specifier is alpha-numeric this should happen
const propertyParts = splitBySpaces.filter(part => exclusions.indexOf(part) === -1) if (isAlphaNumeric(statement.charAt(startBraceIdx - 1))) {
// rebuild to get the full property statement = swapStrings(statement, startBraceIdx + lastIdx, 1, ".[")
const fullProperty = propertyParts.join(" ") }
// now work out the dot notation layers and split them up lastIdx = startBraceIdx + 1
const propertyLayers = fullProperty.split(".") startBraceIdx = statement.substring(lastIdx + 1).indexOf("[")
// find the layers which need to be wrapped and wrap them
for (let layer of propertyLayers) {
if (layer.indexOf(" ") !== -1) {
statement = swapStrings(statement, statement.indexOf(layer), layer.length, `[${layer}]`)
} }
} return statement
// remove the edge case of double brackets being entered (in-case user already has specified) }),
return statement.replace(/\[\[/g, "[").replace(/]]/g, "]")
}
function finalise(statement) { new Preprocessor("handle-spaces-in-properties", statement => {
let insideStatement = statement.slice(2, statement.length - 2) // exclude helpers and brackets, regex will only find double brackets
if (insideStatement.charAt(0) === " ") { const exclusions = HelperFunctions.concat(["{{", "}}"])
insideStatement = insideStatement.slice(1) // find all the parts split by spaces
} const splitBySpaces = statement.split(" ")
if (insideStatement.charAt(insideStatement.length - 1) === " ") { // remove the excluded elements
insideStatement = insideStatement.slice(0, insideStatement.length - 1) const propertyParts = splitBySpaces.filter(
} part => exclusions.indexOf(part) === -1
if (includesAny(insideStatement, HelperFunctions)) { )
insideStatement = `(${insideStatement})` // rebuild to get the full property
} const fullProperty = propertyParts.join(" ")
return `{{ all ${insideStatement} }}` // now work out the dot notation layers and split them up
} const propertyLayers = fullProperty.split(".")
// find the layers which need to be wrapped and wrap them
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)
return statement.replace(/\[\[/g, "[").replace(/]]/g, "]")
}),
new Preprocessor("finalise", statement => {
let insideStatement = statement.slice(2, statement.length - 2)
if (insideStatement.charAt(0) === " ") {
insideStatement = insideStatement.slice(1)
}
if (insideStatement.charAt(insideStatement.length - 1) === " ") {
insideStatement = insideStatement.slice(0, insideStatement.length - 1)
}
if (includesAny(insideStatement, HelperFunctions)) {
insideStatement = `(${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,18 +88,17 @@ 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
} }

10
packages/string-templates/src/helpers/index.js

@ -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) {

4
packages/string-templates/src/utilities.js

@ -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)
} }

7
packages/string-templates/test/basic.spec.js

@ -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 {

16
packages/string-templates/test/escapes.spec.js

@ -32,4 +32,20 @@ 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")
})
}) })

33
packages/string-templates/test/renderApp.spec.js

@ -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>`)
})
})
Loading…
Cancel
Save