Spaces:
Sleeping
Sleeping
Download client/fn-codegen-test.cjs from incognitolm/RealBlocks: direct link, hf CLI and curl.
- Browser
- Download file 11 kB
-
https://huggingface.co/spaces/incognitolm/RealBlocks/resolve/main/client/fn-codegen-test.cjs
- Command line
-
hf download hf://spaces/incognitolm/RealBlocks/client/fn-codegen-test.cjs
-
curl -L -o fn-codegen-test.cjs https://huggingface.co/spaces/incognitolm/RealBlocks/resolve/main/client/fn-codegen-test.cjs
11 kB
| // Headless test: verify custom function blocks generate JS correctly. | |
| const Blockly = require('blockly'); | |
| const { javascriptGenerator, Order } = require('blockly/javascript'); | |
| const FN_COLOR = '#E91E63'; | |
| const OVAL_COLOR = '#FF8C1A'; | |
| function escXml(s) { | |
| return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); | |
| } | |
| // --- Simulated app blocks (statement blocks return strings; value blocks tuples) --- | |
| javascriptGenerator.forBlock['rb_js_set_var'] = function (block) { | |
| const name = block.getFieldValue('name') || 'myVar'; | |
| let value = ''; | |
| const t = block.getInputTargetBlock('value'); | |
| if (t) { | |
| const tuple = javascriptGenerator.blockToCode(t, Order.ATOMIC); | |
| value = Array.isArray(tuple) ? tuple[0] : tuple || ''; | |
| } | |
| const code = `myVar${''} = ${value || 'undefined'};`; | |
| return block.outputConnection ? [code, Order.ATOMIC] : code; // statement -> string | |
| }; | |
| javascriptGenerator.forBlock['rb_js_const_number'] = function (block) { | |
| return [String(block.getFieldValue('value') ?? 0), Order.ATOMIC]; | |
| }; | |
| javascriptGenerator.forBlock['rb_js_console'] = function (block) { | |
| let value = ''; | |
| const t = block.getInputTargetBlock('value'); | |
| if (t) { | |
| const tuple = javascriptGenerator.blockToCode(t, Order.ATOMIC); | |
| value = Array.isArray(tuple) ? tuple[0] : tuple || ''; | |
| } | |
| const code = `console.log(${value || "''"});`; | |
| return block.outputConnection ? [code, Order.ATOMIC] : code; | |
| }; | |
| // --- Minimal app block definitions --- | |
| Blockly.Blocks['rb_js_set_var'] = { | |
| init() { | |
| this.appendDummyInput().appendField('set var').appendField('myVar', 'name'); | |
| this.appendValueInput('value'); | |
| this.setPreviousStatement(true); | |
| this.setNextStatement(true); | |
| }, | |
| }; | |
| Blockly.Blocks['rb_js_const_number'] = { | |
| init() { | |
| this.appendDummyInput().appendField('value').appendField(new Blockly.FieldNumber(0), 'value'); | |
| this.setOutput(true); | |
| }, | |
| }; | |
| Blockly.Blocks['rb_js_console'] = { | |
| init() { | |
| this.appendDummyInput().appendField('print'); | |
| this.appendValueInput('value'); | |
| this.setPreviousStatement(true); | |
| this.setNextStatement(true); | |
| }, | |
| }; | |
| // --- Register the custom function block types (mirrors customFunctionBlocks.ts) --- | |
| function parseFnData(block) { | |
| try { | |
| const d = JSON.parse(String(block.data || '{}')); | |
| if (d && typeof d === 'object' && typeof d.name === 'string') return d; | |
| } catch {} | |
| return null; | |
| } | |
| function paramSegsOf(segments) { | |
| return (segments || []).filter((s) => s.kind === 'param'); | |
| } | |
| function encodeName(name) { | |
| return String(name).replace(/%/g, '\\u0025').replace(/\s+/g, '_').replace(/\W/g, (ch) => { | |
| return '%' + ch.codePointAt(0).toString(16).toUpperCase(); | |
| }); | |
| } | |
| function fnParamNames(data) { | |
| return paramSegsOf(data.segments).map((s) => encodeName(s.name)); | |
| } | |
| Blockly.Blocks['rb_custom_fn'] = { | |
| init() { | |
| this.setColour(FN_COLOR); | |
| this.appendDummyInput(); | |
| this.setInputsInline(true); | |
| }, | |
| domToMutation(m) { | |
| try { | |
| const data = { | |
| name: m.getAttribute('name') || '', | |
| segments: JSON.parse(m.getAttribute('segments') || '[]'), | |
| asyncFn: m.getAttribute('async') === 'true', | |
| returns: m.getAttribute('returns') === 'true', | |
| }; | |
| this.data = JSON.stringify(data); | |
| // rebuild layout: define + param value inputs + body statement | |
| for (const input of [...this.inputList]) this.removeInput(input.name); | |
| const row = this.appendDummyInput(); | |
| row.appendField('define'); | |
| row.appendField(data.name, 'name'); | |
| let pendingText = ''; | |
| let idx = 0; | |
| for (const seg of data.segments) { | |
| if (seg.kind === 'text') pendingText += seg.text; | |
| else { | |
| const vi = this.appendValueInput('PARAM' + idx); | |
| if (pendingText) vi.appendField(pendingText); | |
| pendingText = ''; | |
| if (seg.type === 'boolean') vi.setCheck('Boolean'); | |
| idx++; | |
| } | |
| } | |
| const props = this.appendDummyInput(); | |
| if (pendingText) props.appendField(pendingText); | |
| props.appendField(data.asyncFn ? 'TRUE' : 'FALSE', 'asyncFn'); | |
| props.appendField('async'); | |
| props.appendField(data.returns ? 'TRUE' : 'FALSE', 'returns'); | |
| props.appendField('return value'); | |
| this.appendStatementInput('body'); | |
| this.setInputsInline(true); | |
| } catch (e) { | |
| throw e; | |
| } | |
| }, | |
| mutationToDom() { | |
| const m = Blockly.utils.xml.createElement('mutation'); | |
| const d = parseFnData(this) || { name: '', segments: [] }; | |
| m.setAttribute('name', String(d.name || '')); | |
| m.setAttribute('segments', JSON.stringify(d.segments || [])); | |
| m.setAttribute('async', d.asyncFn ? 'true' : 'false'); | |
| m.setAttribute('returns', d.returns ? 'true' : 'false'); | |
| return m; | |
| }, | |
| }; | |
| Blockly.Blocks['rb_custom_call'] = { | |
| init() { | |
| this.setColour(FN_COLOR); | |
| this.setPreviousStatement(true); | |
| this.setNextStatement(true); | |
| this.appendDummyInput(); | |
| this.setInputsInline(true); | |
| }, | |
| domToMutation(m) { | |
| const data = { | |
| name: m.getAttribute('name') || '', | |
| segments: JSON.parse(m.getAttribute('segments') || '[]'), | |
| returns: m.getAttribute('returns') === 'true', | |
| }; | |
| this.data = JSON.stringify(data); | |
| for (const input of [...this.inputList]) this.removeInput(input.name); | |
| const head = this.appendDummyInput(); | |
| head.appendField(data.name, 'fnName'); | |
| let pendingText = ''; | |
| let idx = 0; | |
| for (const seg of data.segments) { | |
| if (seg.kind === 'text') pendingText += seg.text; | |
| else { | |
| const vi = this.appendValueInput('PARAM' + idx); | |
| if (pendingText) vi.appendField(pendingText); | |
| pendingText = ''; | |
| if (seg.type === 'boolean') vi.setCheck('Boolean'); | |
| idx++; | |
| } | |
| } | |
| const trail = this.appendDummyInput(); | |
| if (pendingText) trail.appendField(pendingText); | |
| this.setInputsInline(true); | |
| }, | |
| mutationToDom() { | |
| const m = Blockly.utils.xml.createElement('mutation'); | |
| const d = parseFnData(this) || { name: '', segments: [] }; | |
| m.setAttribute('name', String(d.name || '')); | |
| m.setAttribute('segments', JSON.stringify(d.segments || [])); | |
| return m; | |
| }, | |
| }; | |
| Blockly.Blocks['rb_custom_param_var'] = { | |
| init() { | |
| this.setColour(OVAL_COLOR); | |
| this.setOutput(true); | |
| this.appendDummyInput().appendField('param', 'name'); | |
| this.setInputsInline(true); | |
| }, | |
| }; | |
| // generators use second param `gen` (mirrors the fix) | |
| javascriptGenerator.forBlock['rb_custom_fn'] = function (block, gen) { | |
| const data = parseFnData(block); | |
| const name = encodeName(block.getFieldValue('name') || (data && data.name) || 'myFunction'); | |
| const asyncFn = block.getFieldValue('asyncFn') === 'TRUE'; | |
| const params = (data ? fnParamNames(data) : []).join(', '); | |
| const body = gen.statementToCode(block, 'body'); | |
| return `${asyncFn ? 'async ' : ''}function ${name}(${params}) {\n${body}\n}`; | |
| }; | |
| function callCode(block, gen, isValue) { | |
| const data = parseFnData(block); | |
| const name = encodeName((data && data.name) || 'myFunction'); | |
| const params = paramSegsOf((data && data.segments) || []); | |
| const args = params | |
| .map((_, i) => { | |
| try { | |
| return gen.valueToCode(block, 'PARAM' + i, Order.ATOMIC) || 'undefined'; | |
| } catch { | |
| return 'undefined'; | |
| } | |
| }) | |
| .join(', '); | |
| return isValue ? `${name}(${args})` : `${name}(${args});`; | |
| } | |
| javascriptGenerator.forBlock['rb_custom_call'] = function (block, gen) { | |
| return callCode(block, gen, false); | |
| }; | |
| javascriptGenerator.forBlock['rb_custom_call_value'] = function (block, gen) { | |
| return [callCode(block, gen, true), Order.ATOMIC]; | |
| }; | |
| javascriptGenerator.forBlock['rb_custom_param_var'] = function (block) { | |
| return [encodeName(String(block.getFieldValue('name') || '')), Order.ATOMIC]; | |
| }; | |
| // --- Build a workspace with a function hat containing body blocks + a caller --- | |
| const xml = ` | |
| <xml> | |
| <block type="rb_custom_fn"> | |
| <mutation name="greet" segments='[{"kind":"text","text":"Say hi to "},{"kind":"param","name":"who","type":"variable"}]' async="false" returns="false"></mutation> | |
| <field name="name">greet</field> | |
| <field name="asyncFn">FALSE</field> | |
| <field name="returns">FALSE</field> | |
| <statement name="body"> | |
| <block type="rb_js_console"> | |
| <value name="value"> | |
| <block type="rb_custom_param_var"> | |
| <field name="name">who</field> | |
| <data>greet</data> | |
| </block> | |
| </value> | |
| </block> | |
| </statement> | |
| <value name="PARAM0"> | |
| <block type="rb_custom_param_var"> | |
| <field name="name">who</field> | |
| <data>greet</data> | |
| </block> | |
| </value> | |
| </block> | |
| <block type="rb_custom_call"> | |
| <mutation name="greet" segments='[{"kind":"text","text":"Say hi to "},{"kind":"param","name":"who","type":"variable"}]' returns="false"></mutation> | |
| <value name="PARAM0"> | |
| <block type="rb_js_const_number"><field name="value">42</field></block> | |
| </value> | |
| </block> | |
| </xml>`; | |
| const ws = new Blockly.Workspace(); | |
| Blockly.Xml.domToWorkspace(Blockly.utils.xml.textToDom(xml), ws); | |
| let ok = true; | |
| function check(label, actual, expected) { | |
| const pass = actual === expected; | |
| if (!pass) ok = false; | |
| console.log((pass ? 'PASS' : 'FAIL') + ' ' + label); | |
| if (!pass) { | |
| console.log(' expected: ' + JSON.stringify(expected)); | |
| console.log(' actual: ' + JSON.stringify(actual)); | |
| } | |
| } | |
| // Generate from the whole workspace | |
| javascriptGenerator.init(ws); | |
| const whole = javascriptGenerator.workspaceToCode(ws); | |
| const expectedWhole = `function greet(who) { | |
| console.log(who); | |
| } | |
| greet(42);`; | |
| check('workspaceToCode', whole, expectedWhole); | |
| // Generate the fn hat alone via blockToCode (as generateCodeFromBlock does) | |
| const fnBlock = ws.getTopBlocks(true).find((b) => b.type === 'rb_custom_fn'); | |
| const fnTuple = javascriptGenerator.blockToCode(fnBlock); | |
| check('fn blockToCode is string', typeof fnTuple === 'string', true); | |
| check('fn blockToCode', fnTuple, `function greet(who) { | |
| console.log(who); | |
| }`); | |
| // Caller alone | |
| const callBlock = ws.getTopBlocks(true).find((b) => b.type === 'rb_custom_call'); | |
| const callStr = javascriptGenerator.blockToCode(callBlock); | |
| check('call blockToCode is string', typeof callStr === 'string', true); | |
| check('call blockToCode', callStr, 'greet(42);'); | |
| // Caller with an empty param socket must not emit `greet(, )` (syntax error); | |
| // the empty arg falls back to `undefined`. | |
| const emptyXml = `<xml><block type="rb_custom_call">` + | |
| `<mutation name="greet" segments='[{"kind":"text","text":"Say hi to "},{"kind":"param","name":"who","type":"variable"}]' returns="false"></mutation>` + | |
| `</block></xml>`; | |
| const ws2 = new Blockly.Workspace(); | |
| Blockly.Xml.domToWorkspace(Blockly.utils.xml.textToDom(emptyXml), ws2); | |
| javascriptGenerator.init(ws2); | |
| const emptyCall = javascriptGenerator.workspaceToCode(ws2); | |
| check('empty-arg caller -> undefined', emptyCall, 'greet(undefined);'); | |
| process.exit(ok ? 0 : 1); | |