mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
34 changed files with 2237 additions and 1079 deletions
@ -1,10 +1,12 @@ |
|||
import CodeMirror from "codemirror" |
|||
import "codemirror/lib/codemirror.css" |
|||
import "codemirror/theme/tomorrow-night-eighties.css" |
|||
import "codemirror/addon/hint/show-hint.css" |
|||
import "codemirror/theme/neo.css" |
|||
import "codemirror/mode/sql/sql" |
|||
import "codemirror/mode/css/css" |
|||
import "codemirror/mode/handlebars/handlebars" |
|||
import "codemirror/mode/javascript/javascript" |
|||
import "codemirror/addon/hint/show-hint" |
|||
|
|||
export default CodeMirror |
|||
|
|||
@ -0,0 +1,15 @@ |
|||
import { getManifest } from "@budibase/string-templates" |
|||
|
|||
export function handlebarsCompletions() { |
|||
const manifest = getManifest() |
|||
|
|||
return Object.keys(manifest).flatMap(key => |
|||
Object.entries(manifest[key]).map(([helperName, helperConfig]) => ({ |
|||
text: helperName, |
|||
path: helperName, |
|||
label: helperName, |
|||
displayText: helperName, |
|||
description: helperConfig.description, |
|||
})) |
|||
) |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,92 @@ |
|||
# String templating |
|||
This package provides a common system for string templating across the Budibase Builder, client and server. |
|||
The templating is provided through the use of [Handlebars](https://handlebarsjs.com/) an extension of Mustache |
|||
which is capable of carrying out logic. We have also extended the base Handlebars functionality through the use |
|||
of a set of helpers provided through the [handlebars-helpers](https://github.com/helpers/handlebars-helpers) package. |
|||
|
|||
We have not implemented all the helpers provided by the helpers package as some of them provide functionality |
|||
we felt would not be beneficial. The following collections of helpers have been implemented: |
|||
1. [Math](https://github.com/helpers/handlebars-helpers/tree/master#math) - a set of useful helpers for |
|||
carrying out logic pertaining to numbers e.g. `avg`, `add`, `abs` and so on. |
|||
2. [Array](https://github.com/helpers/handlebars-helpers/tree/master#array) - some very specific helpers |
|||
for use with arrays, useful for example in automations. Helpers like `first`, `last`, `after` and `join` |
|||
can be useful for getting particular portions of arrays or turning them into strings. |
|||
3. [Number](https://github.com/helpers/handlebars-helpers/tree/master#number) - unlike the math helpers these |
|||
are useful for converting numbers into useful formats for display, e.g. `bytes`, `addCommas` and `toPrecision`. |
|||
4. [URL](https://github.com/helpers/handlebars-helpers/tree/master#url) - very specific helpers for dealing with URLs, |
|||
such as `encodeURI`, `escape`, `stripQueryString` and `stripProtocol`. These are primarily useful |
|||
for building up particular URLs to hit as say part of an automation. |
|||
5. [String](https://github.com/helpers/handlebars-helpers/tree/master#string) - these helpers are useful for building |
|||
strings and preparing them for display, e.g. `append`, `camelcase`, `capitalize` and `ellipsis`. |
|||
6. [Comparison](https://github.com/helpers/handlebars-helpers/tree/master#comparison) - these helpers are mainly for |
|||
building strings when particular conditions are met, for example `and`, `or`, `gt`, `lt`, `not` and so on. This is a very |
|||
extensive set of helpers but is mostly as would be expected from a set of logical operators. |
|||
7. [Date](https://github.com/helpers/helper-date) - last but certainly not least is a moment based date helper, which can |
|||
format ISO/timestamp based dates into something human-readable. An example of this would be `{{date dateProperty "DD-MM-YYYY"}}`. |
|||
|
|||
## Date formatting |
|||
This package uses the standard method for formatting date times, using the following syntax: |
|||
| Input | Example | Description | |
|||
| ----- | ------- | ----------- | |
|||
| YYYY | 2014 | 4 or 2 digit year. Note: Only 4 digit can be parsed on strict mode | |
|||
| YY | 14 | 2 digit year | |
|||
| Y | -25 | Year with any number of digits and sign | |
|||
| Q | 1..4 | Quarter of year. Sets month to first month in quarter. | |
|||
| M MM | 1..12 | Month number | |
|||
| MMM MMMM | Jan..December | Month name in locale set by moment.locale() | |
|||
| D DD | 1..31 | Day of month | |
|||
| Do | 1st..31st | Day of month with ordinal | |
|||
| DDD DDDD | 1..365 | Day of year | |
|||
| X | 1410715640.579 | Unix timestamp | |
|||
| x | 1410715640579 | Unix ms timestamp | |
|||
|
|||
## Template format |
|||
There are two main ways that the templating system can be used, the first is very similar to that which |
|||
would be produced by Mustache - a single statement: |
|||
``` |
|||
Hello I'm building a {{uppercase adjective}} string with Handlebars! |
|||
``` |
|||
In the statement above provided a context of `{adjective: "cool"}` will produce a string of `Hello I'm building a COOL string with Handlebars!`. |
|||
Here we can see an example of how string helpers can be used to make a string exactly as we need it. These statements are relatively |
|||
simple; we can also stack helpers as such: `{{ uppercase (remove string "bad") }}` with the use of parenthesis. |
|||
|
|||
The other type of statement that can be made with the templating system is conditional ones, that appear as such: |
|||
``` |
|||
Hello I'm building a {{ #gte score "50" }}Great{{ else }}Bad{{ /gte }} string with Handlebars! |
|||
``` |
|||
In this string we can see that the string `Great` or `Bad` will be inserted depending on the state of the |
|||
`score` context variable. The comparison, string and array helpers all provide some conditional operators which can be used |
|||
in this way. There will also be some operators which will be built with a very similar syntax but will produce an |
|||
iterative operation, like a for each - an example of this would be the `forEach` array helper. |
|||
|
|||
## Usage |
|||
Usage of this templating package is through one of the primary functions provided by the package - these functions are |
|||
as follows: |
|||
1. `processString` - `async (string, object)` - given a template string and a context object this will build a string |
|||
using our pre-processors, post-processors and handlebars. |
|||
2. `processObject` - `async (object, object)` - carries out the functionality provided by `processString` for any string |
|||
inside the given object. This will recurse deeply into the provided object so for very large objects this could be slow. |
|||
3. `processStringSync` - `(string, object)` - a reduced functionality processing of strings which is synchronous, like |
|||
functions provided by Node (e.g. `readdirSync`) |
|||
4. `processObjectSync` - `(object, object)` - as with the sync'd string, recurses an object to process it synchronously. |
|||
5. `makePropSafe` - `(string)` - some properties cannot be handled by Handlebars, for example `Table 1` is not valid due |
|||
to spaces found in the property name. This will update the property name to `[Table 1]` wrapping it in literal |
|||
specifiers so that it is safe for use in Handlebars. Ideally this function should be called for every level of an object |
|||
being accessed, for example `[Table 1].[property name]` is the syntax that is required for Handlebars. |
|||
6. `isValid` - `(string)` - checks the given string for any templates and provides a boolean stating whether it is a valid |
|||
template. |
|||
7. `getManifest` - returns the manifest JSON which has been generated for the helpers, describing them and their params. |
|||
|
|||
## Development |
|||
This library is built with [Rollup](https://rollupjs.org/guide/en/) as many of the packages built by Budibase are. We have |
|||
built the string templating package as a UMD so that it can be used by Node and Browser based applications. This package also |
|||
builds Typescript stubs which when making use of the library will be used by your IDE to provide code completion. The following |
|||
commands are provided for development purposes: |
|||
1. `yarn build` - will build the Typescript stubs and the bundle into the `dist` directory. |
|||
2. `yarn test` - runs the test suite which will check various helpers are still functioning as |
|||
expected and a few expected use cases. |
|||
3. `yarn dev:builder` - an internal command which is used by lerna to watch and build any changes |
|||
to the package as part of the main `yarn dev` of the repo. |
|||
|
|||
It is also important to note this package is managed in the same manner as all other in the mono-repo, |
|||
through lerna. |
|||
File diff suppressed because it is too large
@ -1,28 +1,32 @@ |
|||
import commonjs from "rollup-plugin-commonjs" |
|||
import commonjs from "@rollup/plugin-commonjs" |
|||
import resolve from "rollup-plugin-node-resolve" |
|||
import builtins from "rollup-plugin-node-builtins" |
|||
import globals from "rollup-plugin-node-globals" |
|||
import json from "@rollup/plugin-json" |
|||
import { terser } from "rollup-plugin-terser" |
|||
|
|||
const production = !process.env.ROLLUP_WATCH |
|||
export default { |
|||
input: "src/index.js", |
|||
input: "src/esIndex.js", |
|||
output: [ |
|||
{ |
|||
sourcemap: true, |
|||
format: "umd", |
|||
format: "esm", |
|||
file: "./dist/bundle.js", |
|||
name: "string-templates", |
|||
name: "templates", |
|||
exports: "named", |
|||
}, |
|||
], |
|||
plugins: [ |
|||
resolve({ |
|||
mainFields: ["module", "main"], |
|||
preferBuiltins: true, |
|||
browser: true, |
|||
}), |
|||
commonjs(), |
|||
globals(), |
|||
builtins(), |
|||
production && terser(), |
|||
json(), |
|||
], |
|||
} |
|||
|
|||
@ -0,0 +1,152 @@ |
|||
const HELPER_LIBRARY = "@budibase/handlebars-helpers" |
|||
const helpers = require(HELPER_LIBRARY) |
|||
const { HelperFunctionBuiltin } = require("../src/helpers/constants") |
|||
const fs = require("fs") |
|||
const doctrine = require("doctrine") |
|||
const marked = require("marked") |
|||
|
|||
const DIRECTORY = fs.existsSync("node_modules") ? "." : ".." |
|||
|
|||
const FILENAME = `${DIRECTORY}/manifest.json` |
|||
|
|||
/** |
|||
* full list of supported helpers can be found here: |
|||
* https://github.com/helpers/handlebars-helpers
|
|||
*/ |
|||
|
|||
const COLLECTIONS = ["math", "array", "number", "url", "string", "comparison"] |
|||
|
|||
const outputJSON = {} |
|||
|
|||
function fixSpecialCases(name, obj) { |
|||
const args = obj.args |
|||
if (name === "ifNth") { |
|||
args[0] = "a" |
|||
args[1] = "b" |
|||
} |
|||
if (name === "eachIndex") { |
|||
obj.description = "Iterates the array, listing an item and the index of it." |
|||
} |
|||
if (name === "toFloat") { |
|||
obj.description = "Convert input to a float." |
|||
} |
|||
if (name === "toInt") { |
|||
obj.description = "Convert input to an integer." |
|||
} |
|||
return obj |
|||
} |
|||
|
|||
function lookForward(lines, funcLines, idx) { |
|||
const funcLen = funcLines.length |
|||
for (let i = idx, j = 0; i < idx + funcLen; ++i, j++) { |
|||
if (!lines[i].includes(funcLines[j])) { |
|||
return false |
|||
} |
|||
} |
|||
return true |
|||
} |
|||
|
|||
function getCommentInfo(file, func) { |
|||
const lines = file.split("\n") |
|||
const funcLines = func.split("\n") |
|||
let comment = null |
|||
for (let idx = 0; idx < lines.length; ++idx) { |
|||
// from here work back until we have the comment
|
|||
if (lookForward(lines, funcLines, idx)) { |
|||
let fromIdx = idx |
|||
let start = 0, |
|||
end = 0 |
|||
do { |
|||
if (lines[fromIdx].includes("*/")) { |
|||
end = fromIdx |
|||
} else if (lines[fromIdx].includes("/*")) { |
|||
start = fromIdx |
|||
} |
|||
if (start && end) { |
|||
break |
|||
} |
|||
fromIdx-- |
|||
} while (fromIdx > 0) |
|||
comment = lines.slice(start, end + 1).join("\n") |
|||
} |
|||
} |
|||
if (comment == null) { |
|||
return { description: "" } |
|||
} |
|||
const docs = doctrine.parse(comment, { unwrap: true }) |
|||
// some hacky fixes
|
|||
docs.description = docs.description.replace(/\n/g, " ") |
|||
docs.description = docs.description.replace(/[ ]{2,}/g, " ") |
|||
docs.description = docs.description.replace(/is is/g, "is") |
|||
const example = docs.description.split("```") |
|||
if (example.length > 1) { |
|||
docs.example = example[1] |
|||
} |
|||
docs.description = example[0].trim() |
|||
return docs |
|||
} |
|||
|
|||
/** |
|||
* This script is very specific to purpose, parsing the handlebars-helpers files to attempt to get information about them. |
|||
*/ |
|||
function run() { |
|||
const foundNames = [] |
|||
for (let collection of COLLECTIONS) { |
|||
const collectionFile = fs.readFileSync( |
|||
`${DIRECTORY}/node_modules/${HELPER_LIBRARY}/lib/${collection}.js`, |
|||
"utf8" |
|||
) |
|||
const collectionInfo = {} |
|||
// collect information about helper
|
|||
let hbsHelperInfo = helpers[collection]() |
|||
for (let entry of Object.entries(hbsHelperInfo)) { |
|||
const name = entry[0] |
|||
// skip built in functions and ones seen already
|
|||
if ( |
|||
HelperFunctionBuiltin.indexOf(name) !== -1 || |
|||
foundNames.indexOf(name) !== -1 |
|||
) { |
|||
continue |
|||
} |
|||
foundNames.push(name) |
|||
// this is ridiculous, but it parse the function header
|
|||
const fnc = entry[1].toString() |
|||
const jsDocInfo = getCommentInfo(collectionFile, fnc) |
|||
let args = jsDocInfo.tags |
|||
.filter(tag => tag.title === "param") |
|||
.map( |
|||
tag => |
|||
tag.description && |
|||
tag.description |
|||
.replace(/`/g, "") |
|||
.split(" ")[0] |
|||
.trim() |
|||
) |
|||
collectionInfo[name] = fixSpecialCases(name, { |
|||
args, |
|||
numArgs: args.length, |
|||
example: jsDocInfo.example || undefined, |
|||
description: jsDocInfo.description, |
|||
}) |
|||
} |
|||
outputJSON[collection] = collectionInfo |
|||
} |
|||
// add the date helper
|
|||
outputJSON["date"] = { |
|||
date: { |
|||
args: ["datetime", "format"], |
|||
numArgs: 2, |
|||
example: '{{date now "YYYY"}}', |
|||
description: "Format a date using moment.js data formatting.", |
|||
}, |
|||
} |
|||
// convert all markdown to HTML
|
|||
for (let collection of Object.values(outputJSON)) { |
|||
for (let helper of Object.values(collection)) { |
|||
helper.description = marked(helper.description) |
|||
} |
|||
} |
|||
fs.writeFileSync(FILENAME, JSON.stringify(outputJSON, null, 2)) |
|||
} |
|||
|
|||
run() |
|||
@ -0,0 +1,12 @@ |
|||
import templates from "./index" |
|||
|
|||
/** |
|||
* This file is simply an entrypoint for rollup - makes a lot of cjs problems go away |
|||
*/ |
|||
export const isValid = templates.isValid |
|||
export const makePropSafe = templates.makePropSafe |
|||
export const getManifest = templates.getManifest |
|||
export const processStringSync = templates.processStringSync |
|||
export const processObjectSync = templates.processObjectSync |
|||
export const processString = templates.processString |
|||
export const processObject = templates.processObject |
|||
@ -1,9 +1,43 @@ |
|||
const { LITERAL_MARKER } = require("../helpers/constants") |
|||
|
|||
const PostProcessorNames = { |
|||
CONVERT_LITERALS: "convert-literals", |
|||
} |
|||
|
|||
/* eslint-disable no-unused-vars */ |
|||
class Postprocessor { |
|||
constructor(name, fn) { |
|||
this.name = name |
|||
this.fn = fn |
|||
} |
|||
|
|||
process(statement) { |
|||
return this.fn(statement) |
|||
} |
|||
} |
|||
|
|||
module.exports.processors = [] |
|||
module.exports.processors = [ |
|||
new Postprocessor(PostProcessorNames.CONVERT_LITERALS, statement => { |
|||
if (!statement.includes(LITERAL_MARKER)) { |
|||
return statement |
|||
} |
|||
|
|||
const components = statement.split("-") |
|||
// pop and shift remove the empty array elements from the first and last dash
|
|||
components.pop() |
|||
components.shift() |
|||
const type = components[1] |
|||
const value = components[2] |
|||
switch (type) { |
|||
case "string": |
|||
return value |
|||
case "number": |
|||
return parseFloat(value) |
|||
case "boolean": |
|||
return value === "true" |
|||
case "object": |
|||
return JSON.parse(value) |
|||
} |
|||
return value |
|||
}), |
|||
] |
|||
|
|||
Loading…
Reference in new issue