Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: multiline expressions #2871

Merged
merged 6 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions companion/lib/Internal/Controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const CHOICES_DYNAMIC_LOCATION = [
useVariables: {
local: true,
},
isExpression: true,
}),
]

Expand Down Expand Up @@ -87,6 +88,7 @@ const CHOICES_STEP_WITH_VARIABLES = [
useVariables: {
local: true,
},
isExpression: true,
}),
]

Expand Down Expand Up @@ -278,6 +280,7 @@ export default class Controls {
useVariables: {
local: true,
},
isExpression: true,
},

...CHOICES_DYNAMIC_LOCATION,
Expand Down Expand Up @@ -523,6 +526,7 @@ export default class Controls {
useVariables: {
local: true,
},
isExpression: true,
}),
{
type: 'checkbox',
Expand Down Expand Up @@ -606,6 +610,7 @@ export default class Controls {
useVariables: {
local: true,
},
isExpression: true,
},
...CHOICES_DYNAMIC_LOCATION,
...CHOICES_STEP_WITH_VARIABLES,
Expand Down
1 change: 1 addition & 0 deletions companion/lib/Internal/CustomVariables.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export default class CustomVariables {
useVariables: {
local: true,
},
isExpression: true,
},
],
},
Expand Down
1 change: 1 addition & 0 deletions companion/lib/Internal/Surface.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const CHOICES_PAGE_WITH_VARIABLES = [
useVariables: {
local: true,
},
isExpression: true,
}),
]

Expand Down
1 change: 1 addition & 0 deletions companion/lib/Internal/Variables.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export default class Variables {
useVariables: {
local: true,
},
isExpression: true,
},
],
},
Expand Down
10 changes: 10 additions & 0 deletions docs/4_secondary_admin_controls/expressions/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,14 @@ There are various functions that you can use. These can be used in the usual way

Strings can be formed using `` `${$(internal:a)}dB` ``. You can use anything instead of `$(internal:a)`, even other templates and conditional logic.

You can split your expression over multiple lines or statements, and create intermediary values too
```
myval = $(internal:a) + $(internal:b)
myval / 2
```
Note: the parser is looser than js in how statements have to be written, it is valid for multiple to be on one line (eg `10 20 30`).
The value of the last statement will be taken as the output of the expression.

And you can add either `/* block comments */` or `// end of line comments` to document your expressions.

All of these features can be combined into long and complex expressions, and more is sure to be possible in the future. We look forward to seeing what you come up with!
17 changes: 17 additions & 0 deletions docs/4_secondary_admin_controls/expressions/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,22 @@ Supported operators include:
- Define an object: `{ a: 1 }`
- Define an array: `[1, 2]`
- Object/array lookup: `$(my:var)['some-prop']`
- Assignment of temporary variables:
- Assignment: `a = 1`
- Addition assignment: `a += 1`
- Subtraction assignment: `a -= 1`
- Multiplication assignment: `a *= 1`
- Division assignment: `a /= 1`
- Modulous assignment: `a %= 1`
- Increment: `a++` or `++a`
- Decrement: `a--` or `--a`
- Logical OR assignment: `a ||= 1`
- Logical AND assignment: `a &&= 1`
- Left shift assignment: `a <<= 1`
- Right shift assignment: `a >>= 1`
- Bitwise XOR assignment: `a ^= 1`
- Bitwise AND assignment: `a &= 1`
- Bitwise OR assignment: `a |= 1`
- Exponent assignment: `a **= 2`

> **Note:** In the examples able `a` and `b` should be replaced with custom variables, module variables or number literals. They are only used here for brevity.
145 changes: 97 additions & 48 deletions shared-lib/lib/Expression/ExpressionParse.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,22 @@ import jsep from 'jsep'
import jsepNumbers from '@jsep-plugin/numbers'
import jsepObject from '@jsep-plugin/object'
import jsepTemplateLiteral from '@jsep-plugin/template'
import jsepComments from '@jsep-plugin/comment'
import { CompanionVariablesPlugin } from './Plugins/CompanionVariables.js'
import { AssignmentPlugin } from './Plugins/Assignment.js'

// setup plugins
jsep.plugins.register(jsepNumbers)
jsep.plugins.register(jsepObject)
jsep.plugins.register(jsepTemplateLiteral)
jsep.plugins.register(jsepComments)
jsep.plugins.register(AssignmentPlugin)
jsep.plugins.register(CompanionVariablesPlugin)

// remove some unwanted operators
jsep.removeBinaryOp('<<<')
jsep.removeBinaryOp('>>>')

/** @type{jsep.IPlugin} */
const companionVariablesPlugin = {
name: 'companion variables plugin',
init(/** @type {any} */ jsep) {
// jsep.addIdentifierChar('$(')
jsep.hooks.add(
'gobble-token',
/**
* TODO: this is bad, but necessary for now
* @this {any}
* @param {any} env
*/
function myPlugin(env) {
const tokenStart = this.expr.slice(this.index, this.index + 2)
if (tokenStart == '$(') {
const end = this.expr.indexOf(')', this.index + 2)

if (end !== -1) {
env.node = {
type: 'CompanionVariable',
name: this.expr.slice(this.index + 2, end),
}

this.index = end + 1
}
}
}
)
},
}
jsep.plugins.register(companionVariablesPlugin)

/**
* Parse an expression into executable nodes
* @param {string} expression
Expand Down Expand Up @@ -153,6 +127,21 @@ function visitElements(node, visitor) {
// @ts-ignore
visitElements(node.value, visitor)

break
case 'ReturnStatement':
case 'UpdateExpression':
// @ts-ignore
visitElements(node.argument, visitor)
break
case 'AssignmentExpression':
// @ts-ignore
visitElements(node.left, visitor)
// @ts-ignore
visitElements(node.right, visitor)
break
case 'Identifier':
// console.log(node)
// Nothing to do
break
default:
throw new Error(`Unknown node "${node.type}"`)
Expand All @@ -161,11 +150,32 @@ function visitElements(node, visitor) {
}

/**
*
* @param {jsep.Expression} node
*/
function fixupExpression(node) {
visitElements(node, (node) => {
function fixReturnDetectedAsFunction(node) {
if (
node.type === 'CallExpression' &&
// @ts-ignore
node.callee.name === 'return' &&
// @ts-ignore
node.arguments.length === 1
) {
node.type = 'ReturnStatement'

// @ts-ignore
node.argument = node.arguments[0]

delete node.arguments
delete node.callee
}
}

/**
*
* @param {jsep.Expression} rootNode
*/
function fixupExpression(rootNode) {
visitElements(rootNode, (node) => {
// Accept undefined
if (node.type === 'Identifier' && node.name === 'undefined') {
node.type = 'Literal'
Expand All @@ -176,6 +186,11 @@ function fixupExpression(node) {
return
}

if (rootNode === node) {
// Fixup return statements detected as a function, if at the root
fixReturnDetectedAsFunction(node)
}

// Fix up object properties being defined as 'Identifier'
if (node.type === 'ObjectExpression') {
// @ts-ignore
Expand All @@ -192,26 +207,60 @@ function fixupExpression(node) {
return
}

// Fix up a $(my:var)[1]
if (node.type === 'Compound' && node.body) {
/** @type {jsep.Expression[]} */
// @ts-ignore
const body = node.body

if (
body.length === 2 &&
body[0].type === 'CompanionVariable' &&
body[1].type === 'ArrayExpression' &&
// @ts-ignore
body[1].elements.length === 1
) {
node.computed = true
node.type = 'MemberExpression'
node.object = body[0]
// @ts-ignore
node.property = body[1].elements[0]
// Fixup return statements detected as a function
for (const expr of body) {
fixReturnDetectedAsFunction(expr)
}

// Fix up a $(my:var)[1]
for (let i = 0; i + 1 < body.length; i++) {
const exprA = body[i]
const exprB = body[i + 1]

if (
exprA.type === 'CompanionVariable' &&
exprB.type === 'ArrayExpression' &&
// @ts-ignore
exprB.elements.length === 1
) {
body[i] = {
computed: true,
type: 'MemberExpression',
object: exprA,
// @ts-ignore
property: exprB.elements[0],
}

// delete node.body

body.splice(i + 1, 1)
}
}

// Combine a return identifier with the expression that follows
for (let i = 0; i + 1 < body.length; i++) {
const exprA = body[i]
const exprB = body[i + 1]

if (exprA.type === 'Identifier' && exprA.name === 'return') {
exprA.type = 'ReturnStatement'
exprA.argument = exprB

delete exprA.name

body.splice(i + 1, 1)
}
}

// If the compound node contains just a single node now, flatten it
if (body.length === 1) {
delete node.body
Object.assign(node, body[0])
}
}
})
Expand Down
Loading