forked from tsai/budibase
committed by
GitHub
34 changed files with 2237 additions and 1079 deletions
@ -1,10 +1,12 @@ |
|||||
import CodeMirror from "codemirror" |
import CodeMirror from "codemirror" |
||||
import "codemirror/lib/codemirror.css" |
import "codemirror/lib/codemirror.css" |
||||
import "codemirror/theme/tomorrow-night-eighties.css" |
import "codemirror/theme/tomorrow-night-eighties.css" |
||||
|
import "codemirror/addon/hint/show-hint.css" |
||||
import "codemirror/theme/neo.css" |
import "codemirror/theme/neo.css" |
||||
import "codemirror/mode/sql/sql" |
import "codemirror/mode/sql/sql" |
||||
import "codemirror/mode/css/css" |
import "codemirror/mode/css/css" |
||||
import "codemirror/mode/handlebars/handlebars" |
import "codemirror/mode/handlebars/handlebars" |
||||
import "codemirror/mode/javascript/javascript" |
import "codemirror/mode/javascript/javascript" |
||||
|
import "codemirror/addon/hint/show-hint" |
||||
|
|
||||
export default CodeMirror |
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 resolve from "rollup-plugin-node-resolve" |
||||
import builtins from "rollup-plugin-node-builtins" |
import builtins from "rollup-plugin-node-builtins" |
||||
import globals from "rollup-plugin-node-globals" |
import globals from "rollup-plugin-node-globals" |
||||
import json from "@rollup/plugin-json" |
import json from "@rollup/plugin-json" |
||||
|
import { terser } from "rollup-plugin-terser" |
||||
|
|
||||
|
const production = !process.env.ROLLUP_WATCH |
||||
export default { |
export default { |
||||
input: "src/index.js", |
input: "src/esIndex.js", |
||||
output: [ |
output: [ |
||||
{ |
{ |
||||
sourcemap: true, |
sourcemap: true, |
||||
format: "umd", |
format: "esm", |
||||
file: "./dist/bundle.js", |
file: "./dist/bundle.js", |
||||
name: "string-templates", |
name: "templates", |
||||
exports: "named", |
exports: "named", |
||||
}, |
}, |
||||
], |
], |
||||
plugins: [ |
plugins: [ |
||||
resolve({ |
resolve({ |
||||
|
mainFields: ["module", "main"], |
||||
preferBuiltins: true, |
preferBuiltins: true, |
||||
browser: true, |
browser: true, |
||||
}), |
}), |
||||
commonjs(), |
commonjs(), |
||||
globals(), |
globals(), |
||||
builtins(), |
builtins(), |
||||
|
production && terser(), |
||||
json(), |
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 */ |
/* eslint-disable no-unused-vars */ |
||||
class Postprocessor { |
class Postprocessor { |
||||
constructor(name, fn) { |
constructor(name, fn) { |
||||
this.name = name |
this.name = name |
||||
this.fn = fn |
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