Package Exports
- lcml
- lcml/dist/index.esm.js
- lcml/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (lcml) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
LCML -- JSON with expressions
Low-Code Markup Language (DSL) presents values with Dynamic Expressions. It is a superset of human readable JSON.
[ 👯 Try it Now | 💻 GitHub | 📓 LCML Syntax | 📓 Integrating LCML ]
Highlights | |
---|---|
🤓 loose mode | 💪 error-tolerant (recover from errors) |
🌲 parse and output AST | 👨🎓 type information is inferred |
🎼 output formatted JavaScript | 🔨 expression processing hooks |
Have a Glimpse
Written in LCML | Output JavaScript | Inferred Type Information |
---|---|---|
3.14159 |
3.14159 |
number |
[1, 2, {{ dice() }}] |
[1, 2, dice()] |
array with 2 numbers + 1 expression |
"hello {{ user.name }}" |
"hello" + toString(user.name) |
string |
{{ foo.bar }} |
foo.bar |
expression |
Here is a loooong LCML example:
// this whole text is written in LCML
// human-readable JSON with {{ expression }} inside:
{
name: "John {{ lib.genLastName() }}", // in string
info: {{ lib.genInfo() }}, // as property value
tags: ["aa", 123, false, {{ location.href }}],
{{ lib.sym }}: "wow", // as property key
}
Compiled with default options:
// output valid JavaScript code
{
name: "John" + toString(lib.genLastName()), // wrapped by "toString"
info: lib.genInfo(),
tags: ["aa", 123, false, location.href],
[lib.sym]: "wow",
}
And every part's type information is inferred:
[] object
[name] string
[info] unknown
[tags] array
[0] string
[1] number
[2] boolean
[3] unknown
[[( lib.sym )]] string
LCML Syntax
LCML syntax is based on JSON syntax, with {{ expression }}
and comments supported.
We support /* block comment */
and // line-comment
You can use {{ expression }}
in many places:
- in string
- as array item
- as property value
- as property key
- as the whole LCML
Loose Mode
When { loose: true }
is passed to parse, these invalid LCML will become valid strings:
LCML | Default Mode | Loose Mode (loose: true ) |
---|---|---|
{{ user.name }}, Welcome |
Error: unexpected remainder | treated as string "{{ user.name }}, Welcome" |
Hello, {{ user.name }} |
Error: invalid input (at "H") | treated as string "Hello, {{ user.name }}" |
/* corrupted */ {{ user |
Error: expect end of expression | treated as string "{{ user" |
Loose Mode Rules:
leading comments are ignored, then the remainder might be treated as string
if the beginning of LCML input looks like a string, array or object, the loose mode will NOT work!
{ ignoreUnparsedRemainder: true }
will not work, unless loose mode is suppressed (see rule #2)due to rule #2, corrupted input like
{ hello:
will cause a Error, not string.- (dangerous) to treat it as string, set
{ onError: 'as-string' }
-- this can be confusing! the parser still outputs a string but it is NOT Loose Mode's credit!
- (dangerous) to treat it as string, set
if Loose Mode actually has functioned, parser will return
{ looseModeEnabled: true }
Some rarely-used notices related to the rule #4, FYI:
- if
{ onError: 'as-string' }
is set, to tell whether it has functioned, you shall check!!parseResult.error && parseResult.ast.type === 'string' && !parseResult.ast.quote
instead ofparseResult.looseModeEnabled
Integrating LCML
import { compile, CompileOptions } from 'lcml';
// compile = parse + toJS
const options: CompileOptions = {
// loose: false,
// onError: 'throw' | 'recover' | 'as-string',
// ignoreUnparsedRemainder: false,
// treatEmptyInput: 'as-undefined',
// compact: false,
// processExpression: (node, parents) => { return node.expression },
// globalToStringMethod: "toString",
};
const result = compile('"Hello, {{ user.name }}"', options);
console.log(result.body);
// => 'Hello, ' + toString(user.name)
console.log(result.ast);
// => { type: "string", start: 0, end: 24 }
console.log(result.expressions);
// => [
// {
// start: 8,
// end: 23,
// expression: " user.name ",
// }
// ]
Global toString
Method
In the generated code, you might see toString(something)
.
This happens when user use {{ expression }}
inside a string literal.
Therefore, to ensure the generated JavaScript runnable, you shall compose your function like this:
function composedGetter() {
// declare the toString method
const toString = x => (typeof x === 'string' ? x : JSON.stringify(x));
// provide other variables that user might reference
const user = getUserInfo();
const state = getState();
// return (/* put the generated code here! */);
return 'Hello, ' + toString(user.name);
}
You can set option globalToStringMethod
in order to use other name instead of toString
.
Process Embedded Expressions
As presented above, option processExpression
can be a callback function receiving node
and its parents
,
returns a JavaScript expression.
You can read node.expression
, transpile it and return new expression.
For example, use Babel to transpile the fashion ESNext syntax like pipeline operator.
The generated JavaScript code will be affected.
Beware: in the received node.expression
, leading and trailing spaces are NOT trimmed.
processExpression: (node, parents) => {
return node.expression;
}