Merge pull request #4393 from Budibase/fix/feb-fixes

Various February fixes and features
This commit is contained in:
Michael Drury 2022-02-09 14:32:30 +00:00 committed by GitHub
commit b3d4479eec
23 changed files with 294 additions and 140 deletions

View File

@ -1,5 +1,6 @@
<script> <script>
import "@spectrum-css/button/dist/index-vars.css" import "@spectrum-css/button/dist/index-vars.css"
import Tooltip from "../Tooltip/Tooltip.svelte"
export let disabled = false export let disabled = false
export let size = "M" export let size = "M"
@ -11,36 +12,67 @@
export let quiet = false export let quiet = false
export let icon = undefined export let icon = undefined
export let active = false export let active = false
export let tooltip = undefined
let showTooltip = false
</script> </script>
<button <div class:container={!!tooltip}>
class:spectrum-Button--cta={cta} <button
class:spectrum-Button--primary={primary} class:spectrum-Button--cta={cta}
class:spectrum-Button--secondary={secondary} class:spectrum-Button--primary={primary}
class:spectrum-Button--warning={warning} class:spectrum-Button--secondary={secondary}
class:spectrum-Button--overBackground={overBackground} class:spectrum-Button--warning={warning}
class:spectrum-Button--quiet={quiet} class:spectrum-Button--overBackground={overBackground}
class:active class:spectrum-Button--quiet={quiet}
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}" class:active
{disabled} class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
on:click|preventDefault {disabled}
> on:click|preventDefault
{#if icon} on:mouseover={() => (showTooltip = true)}
<svg on:mouseleave={() => (showTooltip = false)}
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}" >
focusable="false" {#if icon}
aria-hidden="true" <svg
aria-label={icon} class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
> focusable="false"
<use xlink:href="#spectrum-icon-18-{icon}" /> aria-hidden="true"
</svg> aria-label={icon}
>
<use xlink:href="#spectrum-icon-18-{icon}" />
</svg>
{/if}
{#if $$slots}
<span class="spectrum-Button-label"><slot /></span>
{/if}
{#if !disabled && tooltip}
<div class="tooltip-icon">
<svg
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
focusable="false"
aria-hidden="true"
aria-label="Info"
>
<use xlink:href="#spectrum-icon-18-InfoOutline" />
</svg>
</div>
{/if}
</button>
{#if showTooltip && tooltip}
<div class="position">
<div class="tooltip">
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
</div>
</div>
{/if} {/if}
{#if $$slots} </div>
<span class="spectrum-Button-label"><slot /></span>
{/if}
</button>
<style> <style>
.container {
display: flex;
align-items: center;
flex-direction: column;
}
.spectrum-Button-label { .spectrum-Button-label {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@ -49,4 +81,23 @@
.active { .active {
color: var(--spectrum-global-color-blue-600) !important; color: var(--spectrum-global-color-blue-600) !important;
} }
.tooltip {
position: absolute;
display: flex;
justify-content: center;
z-index: 100;
width: 160px;
text-align: center;
transform: translateX(-50%);
top: -5px;
}
.position {
position: relative;
width: 0;
height: 0;
}
.tooltip-icon {
padding-left: var(--spacing-m);
line-height: 0;
}
</style> </style>

View File

@ -14,16 +14,20 @@
export let value = null export let value = null
export let placeholder = null export let placeholder = null
export let appendTo = undefined export let appendTo = undefined
export let timeOnly = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const flatpickrId = `${generateID()}-wrapper` const flatpickrId = `${generateID()}-wrapper`
let open = false let open = false
let flatpickr let flatpickr, flatpickrOptions, isTimeOnly
$: isTimeOnly = !timeOnly && value ? !isNaN(new Date(`0-${value}`)) : timeOnly
$: flatpickrOptions = { $: flatpickrOptions = {
element: `#${flatpickrId}`, element: `#${flatpickrId}`,
enableTime: enableTime || false, enableTime: isTimeOnly || enableTime || false,
noCalendar: isTimeOnly || false,
altInput: true, altInput: true,
altFormat: enableTime ? "F j Y, H:i" : "F j, Y", altFormat: isTimeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
wrap: true, wrap: true,
appendTo, appendTo,
disableMobile: "true", disableMobile: "true",
@ -35,6 +39,11 @@
if (newValue) { if (newValue) {
newValue = newValue.toISOString() newValue = newValue.toISOString()
} }
// if time only set date component to today
if (timeOnly) {
const todayDate = new Date().toISOString().split("T")[0]
newValue = `${todayDate}T${newValue.split("T")[1]}`
}
dispatch("change", newValue) dispatch("change", newValue)
} }
@ -67,7 +76,11 @@
return null return null
} }
let date let date
if (val instanceof Date) { let time = new Date(`0-${val}`)
// it is a string like 00:00:00, just time
if (timeOnly || (typeof val === "string" && !isNaN(time))) {
date = time
} else if (val instanceof Date) {
// Use real date obj if already parsed // Use real date obj if already parsed
date = val date = val
} else if (isNaN(val)) { } else if (isNaN(val)) {
@ -77,7 +90,7 @@
// Treat as numerical timestamp // Treat as numerical timestamp
date = new Date(parseInt(val)) date = new Date(parseInt(val))
} }
const time = date.getTime() time = date.getTime()
if (isNaN(time)) { if (isNaN(time)) {
return null return null
} }
@ -88,69 +101,71 @@
} }
</script> </script>
<Flatpickr {#key isTimeOnly}
bind:flatpickr <Flatpickr
value={parseDate(value)} bind:flatpickr
on:open={onOpen} value={parseDate(value)}
on:close={onClose} on:open={onOpen}
options={flatpickrOptions} on:close={onClose}
on:change={handleChange} options={flatpickrOptions}
element={`#${flatpickrId}`} on:change={handleChange}
> element={`#${flatpickrId}`}
<div
id={flatpickrId}
class:is-disabled={disabled}
class:is-invalid={!!error}
class="flatpickr spectrum-InputGroup spectrum-Datepicker"
class:is-focused={open}
aria-readonly="false"
aria-required="false"
aria-haspopup="true"
> >
<div <div
on:click={flatpickr?.open} id={flatpickrId}
class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-invalid={!!error} class:is-invalid={!!error}
class="flatpickr spectrum-InputGroup spectrum-Datepicker"
class:is-focused={open}
aria-readonly="false"
aria-required="false"
aria-haspopup="true"
> >
{#if !!error} <div
on:click={flatpickr?.open}
class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-disabled={disabled}
class:is-invalid={!!error}
>
{#if !!error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<input
data-input
type="text"
{disabled}
class="spectrum-Textfield-input spectrum-InputGroup-input"
{placeholder}
{id}
{value}
/>
</div>
<button
type="button"
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
tabindex="-1"
{disabled}
class:is-invalid={!!error}
on:click={flatpickr?.open}
>
<svg <svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon" class="spectrum-Icon spectrum-Icon--sizeM"
focusable="false" focusable="false"
aria-hidden="true" aria-hidden="true"
aria-label="Calendar"
> >
<use xlink:href="#spectrum-icon-18-Alert" /> <use xlink:href="#spectrum-icon-18-Calendar" />
</svg> </svg>
{/if} </button>
<input
data-input
type="text"
{disabled}
class="spectrum-Textfield-input spectrum-InputGroup-input"
{placeholder}
{id}
{value}
/>
</div> </div>
<button </Flatpickr>
type="button" {/key}
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
tabindex="-1"
{disabled}
class:is-invalid={!!error}
on:click={flatpickr?.open}
>
<svg
class="spectrum-Icon spectrum-Icon--sizeM"
focusable="false"
aria-hidden="true"
aria-label="Calendar"
>
<use xlink:href="#spectrum-icon-18-Calendar" />
</svg>
</button>
</div>
</Flatpickr>
{#if open} {#if open}
<div class="overlay" on:mousedown|self={flatpickr?.close} /> <div class="overlay" on:mousedown|self={flatpickr?.close} />
{/if} {/if}

View File

@ -9,6 +9,7 @@
export let disabled = false export let disabled = false
export let error = null export let error = null
export let enableTime = true export let enableTime = true
export let timeOnly = false
export let placeholder = null export let placeholder = null
export let appendTo = undefined export let appendTo = undefined
@ -27,6 +28,7 @@
{value} {value}
{placeholder} {placeholder}
{enableTime} {enableTime}
{timeOnly}
{appendTo} {appendTo}
on:change={onChange} on:change={onChange}
/> />

View File

@ -2,9 +2,18 @@
import dayjs from "dayjs" import dayjs from "dayjs"
export let value export let value
// adding the 0- will turn a string like 00:00:00 into a valid ISO
// date, but will make actual ISO dates invalid
$: time = new Date(`0-${value}`)
$: isTime = !isNaN(time)
</script> </script>
<div>{dayjs(value).format("MMMM D YYYY, HH:mm")}</div> <div>
{dayjs(isTime ? time : value).format(
isTime ? "HH:mm:ss" : "MMMM D YYYY, HH:mm"
)}
</div>
<style> <style>
div { div {

View File

@ -17,6 +17,7 @@
class:icon-small={size === "M" || size === "S"} class:icon-small={size === "M" || size === "S"}
on:mouseover={() => (showTooltip = true)} on:mouseover={() => (showTooltip = true)}
on:mouseleave={() => (showTooltip = false)} on:mouseleave={() => (showTooltip = false)}
on:focus
> >
<Icon name="InfoOutline" size="S" disabled={true} /> <Icon name="InfoOutline" size="S" disabled={true} />
</div> </div>
@ -47,7 +48,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
top: 15px; top: 15px;
z-index: 1; z-index: 100;
width: 160px; width: 160px;
} }
.icon { .icon {

View File

@ -17,6 +17,10 @@
dispatch("change") dispatch("change")
} }
} }
function save() {
dispatch("save", value)
}
</script> </script>
<div class="parent"> <div class="parent">
@ -39,7 +43,10 @@
name="SaveFloppy" name="SaveFloppy"
hoverable hoverable
size="S" size="S"
on:click={() => setEditing(false)} on:click={() => {
setEditing(false)
save()
}}
/> />
{/if} {/if}
</div> </div>

View File

@ -52,7 +52,7 @@
{/if} {/if}
{#if query?.parameters?.length > 0} {#if query?.parameters?.length > 0}
<div> <div class="params">
<BindingBuilder <BindingBuilder
bind:customParams={parameters.queryParams} bind:customParams={parameters.queryParams}
queryBindings={query.parameters} queryBindings={query.parameters}
@ -70,3 +70,11 @@
{/if} {/if}
{/if} {/if}
</Layout> </Layout>
<style>
.params {
display: flex;
flex-direction: column;
gap: var(--spacing-l);
}
</style>

View File

@ -59,6 +59,9 @@
$: schemaReadOnly = !responseSuccess $: schemaReadOnly = !responseSuccess
$: variablesReadOnly = !responseSuccess $: variablesReadOnly = !responseSuccess
$: showVariablesTab = shouldShowVariables(dynamicVariables, variablesReadOnly) $: showVariablesTab = shouldShowVariables(dynamicVariables, variablesReadOnly)
$: hasSchema =
Object.keys(schema || {}).length !== 0 ||
Object.keys(query?.schema || {}).length !== 0
function getSelectedQuery() { function getSelectedQuery() {
return cloneDeep( return cloneDeep(
@ -294,6 +297,7 @@
bind:value={query.name} bind:value={query.name}
defaultValue="Untitled" defaultValue="Untitled"
on:change={() => (query.flags.urlName = false)} on:change={() => (query.flags.urlName = false)}
on:save={saveQuery}
/> />
<div class="access"> <div class="access">
<Label>Access level</Label> <Label>Access level</Label>
@ -313,7 +317,15 @@
<div class="url"> <div class="url">
<Input bind:value={url} placeholder="http://www.api.com/endpoint" /> <Input bind:value={url} placeholder="http://www.api.com/endpoint" />
</div> </div>
<Button cta disabled={!url} on:click={runQuery}>Send</Button> <Button primary disabled={!url} on:click={runQuery}>Send</Button>
<Button
disabled={!query.name}
cta
on:click={saveQuery}
tooltip={!hasSchema
? "Saving a query before sending will mean no schema is generated"
: null}>Save</Button
>
</div> </div>
<Tabs selected="Bindings" quiet noPadding noHorizPadding onTop> <Tabs selected="Bindings" quiet noPadding noHorizPadding onTop>
<Tab title="Bindings"> <Tab title="Bindings">
@ -527,9 +539,6 @@
>{response?.info.size}</span >{response?.info.size}</span
> >
</Label> </Label>
<Button disabled={!responseSuccess} cta on:click={saveQuery}
>Save query</Button
>
</div> </div>
{/if} {/if}
</Tabs> </Tabs>

View File

@ -2453,6 +2453,12 @@
"key": "enableTime", "key": "enableTime",
"defaultValue": true "defaultValue": true
}, },
{
"type": "boolean",
"label": "Time Only",
"key": "timeOnly",
"defaultValue": false
},
{ {
"type": "text", "type": "text",
"label": "Default value", "label": "Default value",

View File

@ -7,6 +7,7 @@
export let placeholder export let placeholder
export let disabled = false export let disabled = false
export let enableTime = false export let enableTime = false
export let timeOnly = false
export let validation export let validation
export let defaultValue export let defaultValue
@ -33,6 +34,7 @@
id={fieldState.fieldId} id={fieldState.fieldId}
appendTo={document.getElementById("flatpickr-root")} appendTo={document.getElementById("flatpickr-root")}
{enableTime} {enableTime}
{timeOnly}
{placeholder} {placeholder}
/> />
{/if} {/if}

View File

@ -36,13 +36,9 @@ CREATE TABLE people
INSERT products INSERT products
(name, description) (name, description)
VALUES VALUES
('Bananas', 'Fruit thing'); ('Bananas', 'Fruit thing'),
INSERT products
(name, description)
VALUES
('Meat', 'Animal thing'); ('Meat', 'Animal thing');
INSERT tasks INSERT tasks
(taskname, productid) (taskname, productid)
VALUES VALUES

View File

@ -19,6 +19,12 @@ CREATE TABLE Tasks (
FOREIGN KEY(PersonID) FOREIGN KEY(PersonID)
REFERENCES Persons(PersonID) REFERENCES Persons(PersonID)
); );
CREATE TABLE Products (
id serial primary key,
name text,
updated time
);
INSERT INTO Persons (FirstName, LastName, Age, Address, City, CreatedAt) VALUES ('Mike', 'Hughes', 28.2, '123 Fake Street', 'Belfast', '2021-01-19 03:14:07'); INSERT INTO Persons (FirstName, LastName, Age, Address, City, CreatedAt) VALUES ('Mike', 'Hughes', 28.2, '123 Fake Street', 'Belfast', '2021-01-19 03:14:07');
INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'assembling'); INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'assembling');
INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'processing'); INSERT INTO Tasks (PersonID, TaskName) VALUES (1, 'processing');
INSERT INTO Products (name, updated) VALUES ('Meat', '11:00:22'), ('Fruit', '10:00:00');

View File

@ -5,8 +5,11 @@ exports.csv = function (headers, rows) {
csv = `${csv}\n${headers csv = `${csv}\n${headers
.map(header => { .map(header => {
let val = row[header] let val = row[header]
val = typeof val === "object" ? JSON.stringify(val) : val val =
return `"${val}"`.trim() typeof val === "object"
? `"${JSON.stringify(val).replace(/"/g, "'")}"`
: `"${val}"`
return val.trim()
}) })
.join(",")}` .join(",")}`
} }

View File

@ -230,7 +230,6 @@ describe("/queries", () => {
}) })
describe("variables", () => { describe("variables", () => {
async function preview(datasource, fields) { async function preview(datasource, fields) {
return config.previewQuery(request, config, datasource, fields) return config.previewQuery(request, config, datasource, fields)
} }

View File

@ -161,10 +161,16 @@ class QueryRunner {
const responses = await Promise.all(dynamics) const responses = await Promise.all(dynamics)
for (let i = 0; i < foundVars.length; i++) { for (let i = 0; i < foundVars.length; i++) {
const variable = foundVars[i] const variable = foundVars[i]
parameters[variable.name] = processStringSync(variable.value, { parameters[variable.name] = processStringSync(
data: responses[i].rows, variable.value,
info: responses[i].extra, {
}) data: responses[i].rows,
info: responses[i].extra,
},
{
escapeNewlines: true,
}
)
// make sure its known that this uses dynamic variables in case it fails // make sure its known that this uses dynamic variables in case it fails
this.hasDynamicVariables = true this.hasDynamicVariables = true
} }
@ -188,6 +194,7 @@ class QueryRunner {
enrichedQuery[key] = processStringSync(fields[key], parameters, { enrichedQuery[key] = processStringSync(fields[key], parameters, {
noEscaping: true, noEscaping: true,
noHelpers: true, noHelpers: true,
escapeNewlines: true,
}) })
} else { } else {
enrichedQuery[key] = fields[key] enrichedQuery[key] = fields[key]

View File

@ -1163,19 +1163,7 @@
svelte-apexcharts "^1.0.2" svelte-apexcharts "^1.0.2"
svelte-flatpickr "^3.1.0" svelte-flatpickr "^3.1.0"
"@budibase/string-templates@1.0.50-alpha.1": "@budibase/string-templates@^1.0.50", "@budibase/string-templates@^1.0.50-alpha.1":
version "1.0.50-alpha.1"
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.50-alpha.1.tgz#ea05a74a3031850652812be21f44d40249e21d43"
integrity sha512-TM6WNzGBlwV0FhR8luejg8CQGa9IaMrMLrW+gfkl4fqEafoWIeKueVhT/lVuPtUK2NzGxmKfNRoasZu6wM6grA==
dependencies:
"@budibase/handlebars-helpers" "^0.11.7"
dayjs "^1.10.4"
handlebars "^4.7.6"
handlebars-utils "^1.0.6"
lodash "^4.17.20"
vm2 "^3.9.4"
"@budibase/string-templates@^1.0.50":
version "1.0.50" version "1.0.50"
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.50.tgz#53386f3c09891ef21bd47870d25cf7e12f6fac86" resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.50.tgz#53386f3c09891ef21bd47870d25cf7e12f6fac86"
integrity sha512-jmqmikU3Xt0I0hY1S1QI8x5770Z+Tu6zGxb9iQsk3PUCkKCpHz70Rl3Q7x5nYsDfOlhT4YHaHKgYYQYJ6hRafw== integrity sha512-jmqmikU3Xt0I0hY1S1QI8x5770Z+Tu6zGxb9iQsk3PUCkKCpHz70Rl3Q7x5nYsDfOlhT4YHaHKgYYQYJ6hRafw==

View File

@ -21,7 +21,7 @@ const HELPERS = [
// javascript helper // javascript helper
new Helper(HelperFunctionNames.JS, processJS, false), new Helper(HelperFunctionNames.JS, processJS, false),
// this help is applied to all statements // this help is applied to all statements
new Helper(HelperFunctionNames.ALL, value => { new Helper(HelperFunctionNames.ALL, (value, { __opts }) => {
if ( if (
value != null && value != null &&
typeof value === "object" && typeof value === "object" &&
@ -36,7 +36,11 @@ const HELPERS = [
if (value && value.string) { if (value && value.string) {
value = value.string value = value.string
} }
let text = new SafeString(value.replace(/&amp;/g, "&")) let text = value
if (__opts && __opts.escapeNewlines) {
text = value.replace(/\n/g, "\\n")
}
text = new SafeString(text.replace(/&amp;/g, "&"))
if (text == null || typeof text !== "string") { if (text == null || typeof text !== "string") {
return text return text
} }
@ -62,10 +66,14 @@ module.exports.HelperNames = () => {
) )
} }
module.exports.registerAll = handlebars => { module.exports.registerMinimum = handlebars => {
for (let helper of HELPERS) { for (let helper of HELPERS) {
helper.register(handlebars) helper.register(handlebars)
} }
}
module.exports.registerAll = handlebars => {
module.exports.registerMinimum(handlebars)
// register imported helpers // register imported helpers
externalHandlebars.registerAll(handlebars) externalHandlebars.registerAll(handlebars)
} }

View File

@ -1,5 +1,5 @@
const handlebars = require("handlebars") const handlebars = require("handlebars")
const { registerAll } = require("./helpers/index") const { registerAll, registerMinimum } = require("./helpers/index")
const processors = require("./processors") const processors = require("./processors")
const { atob, btoa } = require("./utilities") const { atob, btoa } = require("./utilities")
const manifest = require("../manifest.json") const manifest = require("../manifest.json")
@ -8,6 +8,7 @@ const { FIND_HBS_REGEX, FIND_DOUBLE_HBS_REGEX } = require("./utilities")
const hbsInstance = handlebars.create() const hbsInstance = handlebars.create()
registerAll(hbsInstance) registerAll(hbsInstance)
const hbsInstanceNoHelpers = handlebars.create() const hbsInstanceNoHelpers = handlebars.create()
registerMinimum(hbsInstanceNoHelpers)
const defaultOpts = { noHelpers: false, noEscaping: false } const defaultOpts = { noHelpers: false, noEscaping: false }
/** /**
@ -105,9 +106,7 @@ module.exports.processStringSync = (string, context, opts) => {
throw "Cannot process non-string types." throw "Cannot process non-string types."
} }
try { try {
// finalising adds a helper, can't do this with no helpers string = processors.preprocess(string, opts)
const shouldFinalise = !opts.noHelpers
string = processors.preprocess(string, shouldFinalise)
// this does not throw an error when template can't be fulfilled, have to try correct beforehand // this does not throw an error when template can't be fulfilled, have to try correct beforehand
const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
const templateString = const templateString =
@ -119,8 +118,10 @@ module.exports.processStringSync = (string, context, opts) => {
return processors.postprocess( return processors.postprocess(
template({ template({
now: new Date(now).toISOString(), now: new Date(now).toISOString(),
__opts: opts,
...context, ...context,
}) }),
{ escapeNewlines: opts ? opts.escapeNewlines : false }
) )
} catch (err) { } catch (err) {
return input return input
@ -176,7 +177,9 @@ module.exports.isValid = (string, opts) => {
const context = {} const context = {}
try { try {
const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
instance.compile(processors.preprocess(string, false))(context) instance.compile(processors.preprocess(string, { noFinalise: true }))(
context
)
return true return true
} catch (err) { } catch (err) {
const msg = err && err.message ? err.message : err const msg = err && err.message ? err.message : err

View File

@ -2,7 +2,7 @@ const { FIND_HBS_REGEX } = require("../utilities")
const preprocessor = require("./preprocessor") const preprocessor = require("./preprocessor")
const postprocessor = require("./postprocessor") const postprocessor = require("./postprocessor")
function process(output, processors) { function process(output, processors, opts) {
for (let processor of processors) { for (let processor of processors) {
// if a literal statement has occurred stop // if a literal statement has occurred stop
if (typeof output !== "string") { if (typeof output !== "string") {
@ -15,24 +15,22 @@ function process(output, processors) {
continue continue
} }
for (let match of matches) { for (let match of matches) {
output = processor.process(output, match) output = processor.process(output, match, opts)
} }
} }
return output return output
} }
module.exports.preprocess = (string, finalise = true) => { module.exports.preprocess = (string, opts) => {
let processors = preprocessor.processors let processors = preprocessor.processors
// the pre-processor finalisation stops handlebars from ever throwing an error if (opts.noFinalise) {
// might want to pre-process for other benefits but still want to see errors
if (!finalise) {
processors = processors.filter( processors = processors.filter(
processor => processor.name !== preprocessor.PreprocessorNames.FINALISE processor => processor.name !== preprocessor.PreprocessorNames.FINALISE
) )
} }
return process(string, processors, opts)
}
module.exports.postprocess = string => {
let processors = postprocessor.processors
return process(string, processors) return process(string, processors)
} }
module.exports.postprocess = string => {
return process(string, postprocessor.processors)
}

View File

@ -16,6 +16,8 @@ class Postprocessor {
} }
} }
module.exports.PostProcessorNames = PostProcessorNames
module.exports.processors = [ module.exports.processors = [
new Postprocessor(PostProcessorNames.CONVERT_LITERALS, statement => { new Postprocessor(PostProcessorNames.CONVERT_LITERALS, statement => {
if (typeof statement !== "string" || !statement.includes(LITERAL_MARKER)) { if (typeof statement !== "string" || !statement.includes(LITERAL_MARKER)) {

View File

@ -16,8 +16,8 @@ class Preprocessor {
this.fn = fn this.fn = fn
} }
process(fullString, statement) { process(fullString, statement, opts) {
const output = this.fn(statement) const output = this.fn(statement, opts)
const idx = fullString.indexOf(statement) const idx = fullString.indexOf(statement)
return swapStrings(fullString, idx, statement.length, output) return swapStrings(fullString, idx, statement.length, output)
} }
@ -48,7 +48,8 @@ module.exports.processors = [
return statement return statement
}), }),
new Preprocessor(PreprocessorNames.FINALISE, statement => { new Preprocessor(PreprocessorNames.FINALISE, (statement, opts) => {
const noHelpers = opts && opts.noHelpers
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)
@ -63,7 +64,10 @@ module.exports.processors = [
return statement return statement
} }
} }
if (HelperNames().some(option => option.includes(possibleHelper))) { if (
!noHelpers &&
HelperNames().some(option => option.includes(possibleHelper))
) {
insideStatement = `(${insideStatement})` insideStatement = `(${insideStatement})`
} }
return `{{ all ${insideStatement} }}` return `{{ all ${insideStatement} }}`

View File

@ -59,3 +59,33 @@ describe("attempt some complex problems", () => {
expect(output).toBe("nulltest") expect(output).toBe("nulltest")
}) })
}) })
describe("check behaviour with newlines", () => {
const context = {
binding: `Hello
there`
}
it("should escape new line to \\n with double brace", async () => {
const hbs = JSON.stringify({
body: "{{ binding }}"
})
const output = await processString(hbs, context, { escapeNewlines: true })
expect(JSON.parse(output).body).toBe(context.binding)
})
it("should work the same with triple brace", async () => {
const hbs = JSON.stringify({
body: "{{{ binding }}}"
})
const output = await processString(hbs, context, { escapeNewlines: true })
expect(JSON.parse(output).body).toBe(context.binding)
})
it("should still work with helpers disabled", async () => {
const hbs = JSON.stringify({
body: "{{ binding }}"
})
const output = await processString(hbs, context, { escapeNewlines: true, noHelpers: true })
expect(JSON.parse(output).body).toBe(context.binding)
})
})

View File

@ -20,7 +20,7 @@ describe("test that it can run without helpers", () => {
) )
const valid = await processString("{{ avg 1 1 1 }}", {}) const valid = await processString("{{ avg 1 1 1 }}", {})
expect(valid).toBe("1") expect(valid).toBe("1")
expect(output).toBe("{{ avg 1 1 1 }}") expect(output).toBe("")
}) })
}) })