Merge pull request #1 from lorennorman/lines-format

new shorthand format
This commit is contained in:
Loren Norman 2024-01-04 16:04:33 -05:00 committed by GitHub
commit 8ed2ac0ca4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 480 additions and 580 deletions

View file

@ -1,10 +1,10 @@
export default {
type: 'controls_if',
toolbox: {
category: 'Logic',
},
commonType: 'controls_if',
generators: {
json: (block, generator) => {
// If/elseif/else condition.

View file

@ -1,10 +1,10 @@
export default {
type: 'logic_boolean',
toolbox: {
category: 'Values',
},
commonType: 'logic_boolean',
generators: {
json: (block, generator) => {
const bool = block.getFieldValue('BOOL') === 'TRUE'

View file

@ -1,10 +1,10 @@
export default {
type: 'logic_compare',
toolbox: {
category: 'Logic',
},
commonType: 'logic_compare',
generators: {
json: (block, generator) => {
const

View file

@ -1,10 +1,10 @@
export default {
type: 'logic_negate',
toolbox: {
category: 'Logic',
},
commonType: 'logic_negate',
generators: {
json: (block, generator) => {
const

View file

@ -1,10 +1,10 @@
export default {
type: 'logic_null',
toolbox: {
category: 'Values',
},
commonType: 'logic_null',
generators: {
json: (block, generator) => {
return [ null, 0]

View file

@ -1,10 +1,10 @@
export default {
type: 'logic_operation',
toolbox: {
category: 'Logic',
},
commonType: 'logic_operation',
generators: {
json: (block, generator) => {
const

View file

@ -1,10 +1,10 @@
export default {
type: 'math_arithmetic',
toolbox: {
category: 'Math',
},
commonType: 'math_arithmetic',
generators: {
json: (block, generator) => {
const

View file

@ -1,7 +1,7 @@
export default {
toolbox: { },
type: 'math_change',
commonType: 'math_change',
toolbox: { },
generators: {
json: (block, generator) => {

View file

@ -1,10 +1,10 @@
export default {
type: 'math_number',
toolbox: {
category: 'Values',
},
commonType: 'math_number',
generators: {
json: (block, generator) => {
return [Number(block.getFieldValue('NUM')) || '0', 0]

View file

@ -1,10 +1,10 @@
export default {
type: 'text',
toolbox: {
category: 'Values',
},
commonType: 'text',
generators: {
json: (block, generator) => {
const text = block.getFieldValue('TEXT')

View file

@ -1,10 +1,10 @@
export default {
type: 'text_multiline',
toolbox: {
category: 'Values',
},
commonType: 'text_multiline',
generators: {
json: (block, generator) => {
const text = block.getFieldValue('TEXT')

View file

@ -1,7 +1,7 @@
export default {
toolbox: { },
type: 'variables_get',
commonType: 'variables_get',
toolbox: { },
generators: {
json: (block, generator) => {

View file

@ -1,7 +1,7 @@
export default {
toolbox: { },
type: 'variables_set',
commonType: 'variables_set',
toolbox: { },
generators: {
json: (block, generator) => {

View file

@ -1,74 +1,40 @@
export default {
type: "action_post_webhook",
toolbox: {
category: 'Actions',
},
json: {
"type": "action_post_webhook",
"message0": "🔗 POST a Webhook %1",
"args0": [
{
"type": "input_dummy",
"align": "CENTRE"
}
],
"message1": "Web URL: %1",
"args1": [
{
"type": "input_value",
"name": "URL",
"check": "String",
"align": "RIGHT"
}
],
"message2": "Select a Feed: %1",
"args2": [
{
"type": "input_value",
"name": "FEED",
"check": "feed",
"align": "RIGHT"
}
],
"message3": "Body: %1",
"args3": [
{
"type": "input_value",
"name": "BODY",
"check": "String",
"align": "RIGHT"
}
],
"message4": "Form Encode? %1 %2",
"args4": [
{
"type": "field_checkbox",
"name": "FORM_ENCODE",
"checked": false
}, {
"type": "input_dummy",
"align": "RIGHT"
}
],
"output": "action",
"colour": 345,
"tooltip": "",
"helpUrl": ""
visualization: {
colour: 345,
},
inputs: {
URL: {
connections: {
mode: "value",
output: "action",
},
lines: [
[ "🔗 POST a Webhook", "CENTER" ],
[ "Web URL:", {
inputValue: "URL",
check: "String",
shadow: {
type: 'text',
fields: { TEXT: 'https://...' }
}
},
FEED: {
shadow: {
type: 'selector_feed'
}
},
BODY: {
}],
[ "Select a Feed:", {
inputValue: "FEED",
check: "feed",
shadow: 'selector_feed'
}],
[ "Body:", {
inputValue: "BODY",
check: "String",
shadow: {
type: 'text_multiline',
fields: {
@ -92,8 +58,13 @@ export default {
]`
}
}
}
},
}],
[ "Form Encode?", {
field: "FORM_ENCODE",
checked: false
}],
],
generators: {
json: (block, generator) => {

View file

@ -1,52 +1,35 @@
export default {
type: "action_publish_to_feed",
toolbox: {
category: 'Actions',
},
json: {
"type": "action_publish_to_feed",
"message0": "Publish to Feed %1 Feed: %2 Value: %3",
"args0": [
{
"type": "input_dummy",
"align": "CENTRE"
},
{
"type": "input_value",
"name": "FEED",
"check": "feed",
"align": "RIGHT"
},
{
"type": "input_value",
"name": "VALUE",
"check": [
"Boolean",
"String",
"Number"
],
"align": "RIGHT"
}
],
"output": "action",
"colour": 345,
"tooltip": "",
"helpUrl": ""
visualization: {
colour: 345,
},
inputs: {
FEED: {
shadow: {
type: 'selector_feed'
}
},
VALUE: {
shadow: {
type: 'text'
}
},
connections: {
mode: "value",
output: "action",
},
lines: [
[ "Publish to Feed", "CENTER" ],
[ "Feed:", {
inputValue: "FEED",
check: "feed",
shadow: 'selector_feed'
}],
[ "Value:", {
inputValue: "VALUE",
check: [ "Boolean", "String", "Number" ],
shadow: 'text'
}]
],
generators: {
json: (block, generator) => {
const

View file

@ -14,34 +14,32 @@ export default {
output: "action",
},
data: {
inputValues: {
FEED: {
check: 'feed',
shadow: 'selector_feed',
},
SUBJECT: {
check: 'String',
shadow: {
type: 'text',
fields: { TEXT: '{{feed_name}} feed has a new value: {{value}}' }
}
},
BODY: {
check: 'String',
shadow: {
type: 'text_multiline',
fields: { TEXT: 'The {{feed_name}} feed has a new value: {{value}} at {{created_at}}' }
}
},
}
},
lines: [
{ center: "📧 Send an Email" },
"Select a Feed: %FEED",
"Subject: %SUBJECT",
"Body: %BODY",
[ "📧 Send an Email", 'CENTER'],
[ "Select a Feed:", {
inputValue: "FEED",
check: 'feed',
shadow: 'selector_feed',
}],
[ "Subject:", {
inputValue: "SUBJECT",
check: 'String',
shadow: {
type: 'text',
fields: { TEXT: '{{feed_name}} feed has a new value: {{value}}' }
}
}],
[ "Body:", {
inputValue: "BODY",
check: 'String',
shadow: {
type: 'text_multiline',
fields: { TEXT: 'The {{feed_name}} feed has a new value: {{value}} at {{created_at}}' }
}
}],
],
generators: {

View file

@ -1,56 +1,39 @@
export default {
type: "action_send_sms",
toolbox: {
category: 'Actions',
},
json: {
"type": "action_send_sms",
"message0": "📲 Send an SMS %1",
"args0": [
{
"type": "input_dummy",
"align": "CENTRE"
}
],
"message1": "Select a Feed: %1",
"args1": [
{
"type": "input_value",
"name": "FEED",
"check": "feed",
"align": "RIGHT"
}
],
"message2": "Body: %1",
"args2": [
{
"type": "input_value",
"name": "BODY",
"check": "String",
"align": "RIGHT"
}
],
"output": "action",
"colour": 345,
"tooltip": "",
"helpUrl": ""
visualization: {
colour: 345
},
inputs: {
FEED: {
shadow: {
type: 'selector_feed'
}
},
BODY: {
connections: {
mode: "value",
output: "action",
},
lines: [
[ "📲 Send an SMS", "CENTER" ],
[ "Select a Feed:", {
inputValue: "FEED",
check: "feed",
shadow: 'selector_feed'
}],
[ "Body:", {
inputValue: "BODY",
check: "String",
shadow: {
type: 'text_multiline',
fields: {
TEXT: 'The {{feed_name}} feed has a new value: {{value}} at {{created_at}}'
}
}
},
},
}],
],
generators: {
json: (block, generator) => {

View file

@ -21,6 +21,7 @@ export default {
colour: 230
},
// TODO: consider something simpler, maybe leftOutput, topOutput, and bottomCheck?
// Block connections
connections: {
// mode options:
@ -34,49 +35,42 @@ export default {
output: "trigger",
},
// specify all data this block contains
data: {
// inline form elements
fields: {
// 'options' key makes a dropdown
FIELD_A: { options: [
[ 'user sees', 'internal id' ], // n times
]},
// 'checked' key makes a checkbox
FIELD_B: { checked: true }
},
// single block attachments
inputValues: {
INPUT_A: {
check: 'other_block_output', // validate connected block is this type
shadow: 'other_block_type' // generate this kind of shadow from toolbox
},
FEED_B: {
check: [ "other", "block", "outputs" ], // validate connected block is in this collection of types
shadow: 'other_block_type'
},
},
// list of block attachments (not implemented yet)
inputStatements: {}
},
// describes each line of the block, from top to bottom
lines: [
"line contents", // bare string: simple line text
{ // bare object:
// - text is line text
// - input refers to any input collection key
text: 'line contents with input appended',
input: 'INPUT_A',
},
// alignment key: creates an input dummy for alignment
{ center: "centered text" }, // bare string value: becomes line text
{ right: { // object value: text with input options
text: 'right-aligned text with an input appended',
input: 'INPUT_B',
}}
// STRING LINES
"line contents", // simple text line, default alignment,
// ARRAY LINES
// [ string, string ]: [ text, alignment ]
[ "line contents", "alignment" ],
// [ string, object ]: [ text, { inputValue, inputStatement, field, fields } ]
[ "line contents", {
// for a single block input
inputValue: 'INPUT_VALUE_NAME',
check: 'input_block_output',
shadow: 'block_type_to_shadow', // -> { shadow: { type: 'block_type_to_shadow' }}
shadow: {
type: 'block_type_to_shadow',
inputs: {}, // fill in the inputs on the shadowed block
fields: {}, // fill in the fields on the shadowed block
},
// TODO: for multiple block inputs
// inputStatement: 'INPUT_STATEMENT_NAME', ...
// for a single field input
field: 'FIELD_NAME',
text: 'whatever', // makes a text field
spellcheck: true, // text field option
checked: true, // makes a checkbox field
options: [ // makes a dropdown field
['user text', 'computer id'],
// ...
],
// TODO: for multiple field inputs: {}
}],
],
// generators for this block type

View file

@ -1,33 +1,27 @@
export default {
type: "root_block",
toolbox: { },
json: {
"type": "root_block",
"message0": "Action Root %1 Trigger: %2 Action: %3",
"args0": [
{
"type": "input_dummy",
"align": "CENTRE"
},
{
"type": "input_value",
"name": "TRIGGER",
"check": "trigger",
"align": "RIGHT"
},
{
"type": "input_value",
"name": "ACTION",
"check": "action",
"align": "RIGHT"
}
],
"colour": 120,
"tooltip": "",
"helpUrl": ""
visualization: {
colour: 120,
},
lines: [
[ "Action Root", "CENTER" ],
[ "Trigger:", {
inputValue: "TRIGGER",
check: "trigger",
}],
[ "Action:", {
inputValue: "ACTION",
check: "action",
}],
],
generators: {
json: (block, generator) => {
const

View file

@ -1,33 +1,35 @@
export default {
type: "selector_comparison",
toolbox: {
category: 'Comparisons',
},
json: {
"type": "selector_comparison",
"message0": "%1",
"args0": [
{
"type": "field_dropdown",
"name": "OPERATOR",
"options": [
[ "any", "any" ],
[ ">", "gt" ],
[ ">=", "gte" ],
[ "<", "lt" ],
[ "<=", "lte" ],
[ "=", "equal_to" ],
[ "≠", "not_equal_to" ],
[ "includes", "inc" ]
]
}
],
"output": "comparison_operator",
visualization: {
"colour": 230,
"tooltip": "",
"helpUrl": ""
},
connections: {
mode: "value",
output: "comparison_operator"
},
lines: [
[ "", {
field: "OPERATOR",
options: [
[ "any", "any" ],
[ ">", "gt" ],
[ ">=", "gte" ],
[ "<", "lt" ],
[ "<=", "lte" ],
[ "=", "equal_to" ],
[ "≠", "not_equal_to" ],
[ "includes", "inc" ]
]
}],
],
generators: {
json: block => [ block.getFieldValue('OPERATOR'), 0 ]
}

View file

@ -1,29 +1,32 @@
export default {
type: "selector_feed",
toolbox: {
category: 'Feeds',
},
json: {
"type": "selector_feed",
"message0": "Feed: %1",
"args0": [
{
"type": "field_dropdown",
"name": "FEED_ID",
"options": [
[ "Feed_A", "1001" ],
[ "Feed_B", "1002" ],
[ "Feed_C", "1003" ],
]
}
],
"extensions": [ "populate_feeds_dropdown" ],
"output": "feed",
"colour": 30,
"tooltip": "",
"helpUrl": ""
visualization: {
colour: 30,
// TODO: proper extension handling
extensions: [ "populate_feeds_dropdown" ],
},
connections: {
mode: "value",
output: "feed"
},
lines: [
[ "Feed:", {
field: "FEED_ID",
options: [
[ "Feed_A", "1001" ],
[ "Feed_B", "1002" ],
[ "Feed_C", "1003" ],
]
}],
],
generators: {
json: block => [ block.getFieldValue('FEED_ID'), 0 ]
}

View file

@ -1,61 +1,45 @@
export default {
type: "trigger_reactive",
toolbox: {
category: 'Triggers'
},
json: {
"type": "trigger_reactive",
"message0": "📥 Reactive %1",
"args0": [
{
"type": "input_dummy",
"align": "CENTRE"
}
],
"message1": "Compare Feeds %1",
"args1": [
{
"type": "input_dummy",
"align": "CENTRE"
}
],
"message2": "Feed: %1",
"args2": [
{
"type": "input_value",
"name": "FEED_A",
"check": "feed",
"align": "RIGHT"
}
],
"message3": "Operator: %1",
"args3": [
{
"type": "input_value",
"name": "COMPARATOR",
"check": "comparison_operator",
"align": "RIGHT"
}
],
"message4": "Feed or Value: %1",
"args4": [
{
"type": "input_value",
"name": "FEED_B",
"check": [
"feed",
"Number",
"String"
],
"align": "RIGHT"
}
],
"message5": "Limit Every: %1 %2",
"args5": [ {
"type": "field_dropdown",
"name": "NOTIFY_LIMIT",
"options": [
visualization: {
"colour": 230,
},
connections: {
mode: "value",
output: "trigger",
},
lines: [
[ "📥 Reactive", "CENTER"],
[ "Compare Feeds", "CENTER" ],
[ "Feed:", {
inputValue: "FEED_A",
check: "feed",
shadow: 'selector_feed'
}],
[ "Operator:", {
inputValue: "COMPARATOR",
check: "comparison_operator",
shadow: 'selector_comparison'
}],
[ "Feed or Value:", {
inputValue: "FEED_B",
check: [ "feed", "Number", "String" ],
shadow: 'selector_feed'
}],
[ "Limit Every:", {
field: "NOTIFY_LIMIT",
options: [
[ '10 sec', '0' ],
[ '1 min', '1' ],
[ '15 min', '15' ],
@ -64,45 +48,13 @@ export default {
[ '6 hrs', '360' ],
[ '1 day', '1440' ]
]
}, {
"type": "input_dummy",
"align": "RIGHT"
}
],
"message6": "Notify on Reset? %1 %2",
"args6": [
{
"type": "field_checkbox",
"name": "NOTIFY_ON_RESET",
"checked": true
}, {
"type": "input_dummy",
"align": "RIGHT"
}
],
"output": "trigger",
"colour": 230,
"tooltip": "",
"helpUrl": ""
},
}],
inputs: {
FEED_A: {
shadow: {
type: 'selector_feed'
}
},
COMPARATOR: {
shadow: {
type: 'selector_comparison'
}
},
FEED_B: {
shadow: {
type: 'selector_feed'
}
},
},
[ "Notify on Reset?", {
field: "NOTIFY_ON_RESET",
checked: true
}],
],
generators: {
json: (block, generator) => {

View file

@ -1,37 +1,28 @@
export default {
type: "trigger_schedule",
toolbox: {
category: 'Triggers'
},
json: {
"type": "trigger_schedule",
"message0": "📅 Scheduled %1",
"args0": [
{
"type": "input_dummy",
"align": "CENTRE"
}
],
"message1": "Schedule %1 %2",
"args1": [
{
"type": "field_input",
"name": "CRONTAB",
"text": "* * * * *",
"spellcheck": false
},
{
"type": "input_dummy",
"align": "CENTRE"
}
],
"output": "trigger",
visualization: {
"colour": 230,
"tooltip": "",
"helpUrl": ""
},
inputs: { },
connections: {
mode: 'value',
output: 'trigger'
},
lines: [
[ "📅 Scheduled", 'CENTER' ],
[ "Schedule", {
field: 'CRONTAB',
text: "* * * * *",
spellcheck: false,
align: 'CENTER'
}]
],
generators: {
json: (block, generator) => {

View file

@ -1,7 +1,6 @@
export default {
type: "trigger_timer",
// where and how this block appears in the toolbox
toolbox: {
category: 'Triggers'
},
@ -15,9 +14,33 @@ export default {
output: "trigger",
},
data: {
fields: {
RUN_AFTER: { options: [
lines: [
[ "⏲️ Timer", "center" ], // alignment shorthand
[ "Compare Feeds", "center" ],
[ 'Feed:', {
inputValue: "FEED_A", // input value
check: 'feed',
shadow: 'selector_feed'
}],
[ "Compare Feeds:", {
inputValue: "COMPARATOR",
check: 'comparison_operator',
shadow: {
type: 'selector_comparison'
}
}],
[ "Feed or Value:", {
inputValue: "FEED_B",
check: 'feed',
shadow: 'selector_feed'
}],
[ "Run After:", {
field: "RUN_AFTER", // field
options: [
[ '10 sec', '10' ],
[ '1 min', '60' ],
[ '15 min', '900' ],
@ -25,34 +48,13 @@ export default {
[ '1 hr', '3600' ],
[ '6 hrs', '21600' ],
[ '1 day', '86400' ]
]},
EXTEND_TIMER: { checked: true }
},
]
}],
inputValues: {
FEED_A: {
check: 'feed',
shadow: 'selector_feed'
},
COMPARATOR: {
check: 'comparison_operator',
shadow: 'selector_comparison'
},
FEED_B: {
check: [ "feed", "Number", "String" ],
shadow: 'selector_feed'
},
},
},
lines: [
{ center: "⏲️ Timer" },
{ center: "Compare % Feeds" },
'Feed: %FEED_A',
"Compare Feeds: %COMPARATOR",
"Feed or Value: %FEED_B",
"Run After: %RUN_AFTER",
"Extend Timer? %EXTEND_TIMER"
[ "Extend Timer?", {
field: "EXTEND_TIMER",
checked: true
}]
],
generators: {
@ -65,6 +67,8 @@ export default {
comparisonTargetValue = generator.valueToCode(block, 'FEED_B', 0) || null,
comparisonPayload = { [comparisonTargetKey]: comparisonTargetValue },
// pair with api -> blockly diagram code in io-rails
// imagine an expressive, generative middle layer...
payload = {
trigger_type: 'timer',
feed_id: generator.valueToCode(block, 'FEED_A', 0) || null,

View file

@ -1,10 +1,9 @@
import { map } from 'lodash-es'
import Blockly from 'blockly'
import { keys, map, without } from 'lodash-es'
import Blockly, { common } from 'blockly'
import blockDefaults from './defaults.js'
import ALL_BLOCKS from './all.js'
import { toBlockJSON } from '../tools/tools.js'
import { getBlockType } from '../tools/util.js'
export const
@ -12,30 +11,52 @@ export const
customBlocksJson = [],
allBlocksByCategory = {}
const processCommonBlock = commonBlock => {
const { commonType } = commonBlock
const
COMMON_KEYS = [
"type",
"toolbox",
"generators"
],
if(!Blockly.Blocks[commonType]) {
throw new Error(`Common Block not found for type: ${commonType}`)
}
}
CUSTOM_KEYS = COMMON_KEYS.concat([
"toolbox",
"help",
"visualization",
"connections",
"lines"
]),
const processCustomBlock = customBlock => {
customBlocksJson.push({
isCommonBlockType = type => !!Blockly.Blocks[type],
processCommonBlock = commonBlock => {
// ensure only supported keys are present
const extraKeys = without(keys(commonBlock), ...COMMON_KEYS)
if(extraKeys.length) {
throw new Error(`Common Block definition has unrecognized keys: "${extraKeys.join('", "')}"\nBlock: ${JSON.stringify(commonBlock, null, 2)}`)
}
},
processCustomBlock = customBlock => {
// ensure only supported keys are present
const extraKeys = without(keys(customBlock), ...CUSTOM_KEYS)
if(extraKeys.length) {
throw new Error(`Common Block definition has unrecognized keys: "${extraKeys.join('", "')}"\nBlock: ${JSON.stringify(customBlock, null, 2)}`)
}
customBlocksJson.push({
...blockDefaults,
...customBlock.type
? toBlockJSON(customBlock)
: customBlock.json
...toBlockJSON(customBlock)
})
}
}
// walk the blocks and process them into the exported collections
map(ALL_BLOCKS, (block, key) => {
const hasType = getBlockType(block)
if(!block.type) { throw new Error(`No "type" declared for block: ${key}`) }
if(!hasType) { throw new Error(`No "type" declared for block: ${key}`) }
block.commonType
isCommonBlockType(block.type)
? processCommonBlock(block) // built-in blocks
: processCustomBlock(block) // our blocks
})

View file

@ -1,8 +1,7 @@
// build toolbox from a config and blocks that reference it
import { includes, isString, map, filter, mapValues } from 'lodash-es'
import { chain, includes, isString, map, filter } from 'lodash-es'
import { allBlockDefinitions } from '../blocks/index.js'
import { getBlockType } from '../tools/util.js'
const
@ -16,34 +15,6 @@ const
{ name: 'Comparisons', colour: 208 },
],
blockToInputs = block =>
block.data?.inputValues
// shadow processing
? mapValues(block.data?.inputValues, ({ shadow }) =>
isString(shadow) // shorthand
? { shadow: { type: shadow }}
: shadow
)
: block.inputs,
blockToFields = block => {
return block.fields
},
selectBlocksByCategoryName = name =>
filter(allBlockDefinitions, def =>
def.toolbox.category === name || includes(def.toolbox.categories, name)
),
generateCategoryContents = ({ name }) =>
map(selectBlocksByCategoryName(name), block => ({
kind: 'block',
type: getBlockType(block),
inputs: blockToInputs(block),
fields: blockToFields(block)
})
),
generateToolboxContents = () => map(TOOLBOX_CONFIG, category =>
// inject other kinds of toolbox objects here
category === SEP
@ -55,7 +26,33 @@ const
...category.extras,
contents: generateCategoryContents(category)
}
)
),
generateCategoryContents = ({ name }) =>
map(selectBlocksByCategoryName(name), block => ({
kind: 'block',
type: block.type,
inputs: blockToInputs(block),
// fields: {}
})),
selectBlocksByCategoryName = name =>
filter(allBlockDefinitions, def =>
def.toolbox.category === name || includes(def.toolbox.categories, name)
),
blockToInputs = ({ lines }) =>
chain(lines)
.map('[1]')
.filter("inputValue")
.keyBy("inputValue")
.mapValues(shadowPropertyToInput)
.value(),
shadowPropertyToInput = ({ shadow }) =>
isString(shadow) // is shorthand?
? { shadow: { type: shadow }} // expand to full object
: { shadow } // set as shadow value
export const toolbox = {
kind: 'categoryToolbox',

View file

@ -1,4 +1,4 @@
import { flatMap, forEach, includes, isObject, isString, keys, reduce, values, without } from 'lodash-es'
import { flatMap, forEach, includes, isArray, isObject, isString, keys, reduce, values, without } from 'lodash-es'
import { defaultAlignment } from '../blocks/defaults.js'
@ -82,22 +82,24 @@ const processConnections = block => {
const processLines = block => {
// grab block settings
// process each line
const { lines, data } = block
const { lines } = block
return reduce(lines, (allLines, line, lineNumber) => {
const { message, args } = processLine(line, data)
const { message, args } = processLine(line)
allLines[`message${lineNumber}`] = message
allLines[`args${lineNumber}`] = args
return allLines
}, {})
}
const processLine = (line, data={}) => {
const processLine = (line) => {
if(!isString(line) && !isObject(line)) {
throw new Error(`Given non-object, non-string line: ${line}`)
}
const { alignment, lineValue } = splitAlignment(line)
const { alignment, lineValue, lineData={} } = parseLine(line)
// always expect alignment
if(!alignment) { throw new Error(`Alignment not detected for line: ${JSON.stringify(line, null, 2)}`) }
// expect 'text' key
if(!isString(lineValue.text)) { throw new Error(`No text given for line: ${JSON.stringify(line, null, 2)}`)}
// expect optional 'input' key
@ -105,98 +107,104 @@ const processLine = (line, data={}) => {
// expect no other keys
if(otherKeys.length) { throw new Error(`Expected no keys other than "text" and "input", got:\n${otherKeys}\nfor line: ${JSON.stringify(line, null, 2)}`)}
const
args = [],
allDataKeys = flatMap([ keys(data.fields), keys(data.inputValues), keys(data.inputStatements)])
// build up the args
const args = []
let { message, inputKey } = lineToMessageAndInput(lineValue, allDataKeys)
// append inputValues to args
if(lineData.inputValue) {
args.push({
type: "input_value",
name: lineData.inputValue,
check: lineData.check
})
// process alignment and input into args
if(inputKey) {
// lookup in values, fields, statements
// TODO: ensure only one found
const fieldInput = data.fields?.[inputKey]
if(fieldInput) {
args.push({
type: (fieldInput.options && "field_dropdown")
|| (includes(keys(fieldInput), "checked") && "field_checkbox"),
name: inputKey,
check: fieldInput.check,
options: fieldInput.options
})
}
const valueInput = data.inputValues?.[inputKey]
if(valueInput) {
args.push({
type: "input_value",
name: inputKey,
check: valueInput.check
})
}
const statementInput = data.inputStatements?.[inputKey]
if(statementInput){
throw new Error(`Not implemented: statement inputs`)
}
// append fields to args
} else if(lineData.field) {
args.push({
type: (lineData.options && "field_dropdown")
|| (includes(keys(lineData), "checked") && "field_checkbox")
|| (includes(keys(lineData), "text") && "field_input"),
name: lineData.field,
checked: lineData.checked,
options: lineData.options,
text: lineData.text,
spellcheck: lineData.spellcheck,
})
}
if(alignment) {
// append alignment to an existing input
if(args[0]?.type === "input_value") {
args[0].align = alignment.toUpperCase()
// if an input exists, append alignment to it
if(args[0]?.type === "input_value") {
args[0].align = alignment
// make an input just for alignment
} else {
args.push({
"type": "input_dummy",
"align": alignment.toUpperCase()
})
}
// otherwise make an input for alignment
} else {
args.push({
"type": "input_dummy",
"align": alignment
})
}
// quick sanity check on args length
if(args.length > 2) {
throw new Error(`args array longer than 2: ${JSON.stringify(args, null, 2)}`)
}
// process text into message
if(args.length === 1) {
message = message.concat(" %1")
} else if(args.length === 2) {
message = message.concat(" %1 %2")
}
const
argsIndices = (args.length === 1) ? " %1"
: (args.length === 2) ? " %1 %2"
: '',
message = lineValue.text.concat(argsIndices)
return { args, message }
}
const validAlignments = ['center', 'centre', 'right', 'left']
const splitAlignment = line => {
if(isString(line)) {
return { alignment: defaultAlignment, lineValue: { text: line } }
}
const parseLine = line => {
if(isString(line)) { return parseStringLine(line) }
// look for alignment keys
const lineKeys = keys(line)
let alignment
forEach(lineKeys, key => {
const lowerKey = key?.toLowerCase()
if(includes(validAlignments, lowerKey)) {
alignment = (lowerKey === 'center') ? 'centre' : lowerKey
if(isArray(line)) { return parseArrayLine(line) }
if(lineKeys.length > 1) {
const lineKeysMessage = without(lineKeys, key).join('", "')
throw new Error(`Alignment key ("${key}") should not have sibling keys, but has:\n"${lineKeysMessage}"`)
}
}
})
const lineValue = alignment
? values(line)[0]
: line
// if found, alignment must be only key
return {
alignment: alignment || defaultAlignment,
lineValue: isString(lineValue)
? { text: lineValue }
: lineValue
}
throw new Error(`Line not valid: ${JSON.stringify(line, null, 2)}`)
}
const parseStringLine = text =>
({ alignment: defaultAlignment, lineValue: { text } })
const parseArrayLine = line => {
const [ text, second ] = line
if(isString(second)) {
return { alignment: second, lineValue: { text } }
}
if(isObject(second)) {
const alignment = parseAlignment(second.align)
return { alignment, lineValue: { text }, lineData: second }
}
throw new Error(`second index invalid for line: ${JSON.stringify(line, null, 2)}`)
}
const validAlignments = [ 'CENTER', 'CENTRE', 'RIGHT', 'LEFT' ]
const parseAlignment = alignmentString => {
// if the input is falsy, return default alignment
if( !alignmentString ) { return defaultAlignment }
alignmentString = alignmentString.toUpperCase()
// throw if the downcased string is not in the valid list
if( !includes(validAlignments, alignmentString) ) {
throw new Error(`Alignment not valid: ${alignmentString}`)
}
// anglocize and return
return alignmentString === 'CENTER'
? 'CENTRE'
: alignmentString
}
// TODO: re-use this when we want to do inline fields like:
// "Field 1: %FIELD_NAME_1 more text then %FIELD_NAME_2 fields not at end"
const DATA_REGEX = / %\w+/ // a space, then %, then one or more word characters
const lineToMessageAndInput = ({ text, input }, dataKeys) => {
// check if this text contains a data string

View file

@ -1 +0,0 @@
export const getBlockType = block => block.type || block.commonType || block.json.type