webcraft-ai-backend / deploy_tmp /schema_generator.py
3ssem0
fix: configuration error - aligned entry point to app.py and enforced LF/BOM-less encoding
ec47953
Raw
History Blame Contribute Delete
15.3 kB
import os
import re
import json
# Configuration
# Configuration
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
# Check if we are in 'Ai model part' or root
if os.path.basename(SCRIPT_DIR) == "Ai model part":
BLOCKS_DIR = os.path.join(SCRIPT_DIR, "../src/lib/blocks")
else:
# Fallback or assume we are in root and script is in Ai model part
BLOCKS_DIR = os.path.join(SCRIPT_DIR, "src/lib/blocks")
OUTPUT_FILE = os.path.join(SCRIPT_DIR, "block_schema.py")
# Regex Patterns
BLOCK_START_RE = re.compile(r"Blockly\.Blocks\s*\[\s*[\"'](.*?)[\"']\s*\]\s*=\s*\{", re.DOTALL)
INPUT_STATEMENT_RE = re.compile(r"\.appendStatementInput\s*\(\s*[\"'](.*?)[\"']\s*\)", re.DOTALL)
INPUT_VALUE_RE = re.compile(r"\.appendValueInput\s*\(\s*[\"'](.*?)[\"']\s*\)", re.DOTALL)
INPUT_DUMMY_RE = re.compile(r"\.appendDummyInput\s*\(\s*\)")
CHECK_RE = re.compile(r"\.setCheck\s*\(\s*(?:\[(.*?)\]|[\"'](.*?)\"'])\s*\)", re.DOTALL)
FIELD_RE = re.compile(r"Field(?:Dropdown|TextInput|Checkbox|Colour|Number)\s*\(.*?\)\s*,\s*[\"'](.*?)[\"']", re.DOTALL)
PREV_STATEMENT_RE = re.compile(r"\.setPreviousStatement\s*\(\s*true(?:,\s*(?:\[(.*?)\]|[\"'](.*?)[\"']))?\s*\)", re.DOTALL)
NEXT_STATEMENT_RE = re.compile(r"\.setNextStatement\s*\(\s*true(?:,\s*(?:\[(.*?)\]|[\"'](.*?)[\"']))?\s*\)", re.DOTALL)
TOOLTIP_RE = re.compile(r"\.setTooltip\s*\(\s*[\"'](.*?)[\"']\s*\)", re.DOTALL)
COLOR_RE = re.compile(r"\.setColour\s*\((.*?)\)", re.DOTALL)
OUTPUT_RE = re.compile(r"\.setOutput\s*\(\s*true(?:,\s*(?:\[(.*?)\]|[\"'](.*?)[\"']))?\s*\)", re.DOTALL)
def parse_blocks():
blocks = {}
# helper to clean check strings
def clean_checks(check_str):
if not check_str: return None
# remove quotes and split
return [c.strip().strip("'\"") for c in check_str.split(',')]
print(f"Scanning blocks in {BLOCKS_DIR}...")
for filename in os.listdir(BLOCKS_DIR):
if not filename.endswith(".js"): continue
if filename == "index.js": continue
filepath = os.path.join(BLOCKS_DIR, filename)
category_name = filename.replace("-blocks.js", "").title()
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# SPECIAL HANDLING: EXCLUDE NON-PRO BLOCKS
if filename in ["layout-blocks.js", "element-blocks.js", "ui-blocks.js"]:
print(f"Skipping {filename} (excluded from Pro Mode schema)")
continue
# Extract blocks using standard Blockly.Blocks pattern
parts = re.split(r"Blockly\.Blocks\s*\[\s*[\"`']", content)
for part in parts[1:]:
# Extract block ID
block_id_match = re.match(r"(.*?)[\"`']\s*\]", part)
if not block_id_match: continue
block_id = block_id_match.group(1)
if block_id in blocks:
continue
props = []
inputs = {}
prev_checks = []
# Scan fields
for field_match in FIELD_RE.finditer(part):
props.append(field_match.group(1))
# Scan Inputs
input_parts = re.split(r"\.append", part)
for input_part in input_parts[1:]:
input_name = None
input_type = "dummy"
checks = None
if input_part.startswith("StatementInput"):
match = re.search(r"Input\s*\(\s*[\"'](.*?)[\"']", input_part)
if match:
input_name = match.group(1)
input_type = "statement"
elif input_part.startswith("ValueInput"):
match = re.search(r"Input\s*\(\s*[\"'](.*?)[\"']", input_part)
if match:
input_name = match.group(1)
input_type = "value"
if input_name:
check_match = re.search(r"\.setCheck\s*\(\s*(?:\[(.*?)\]|[\"'](.*?)[\"'])\s*\)", input_part)
if check_match:
raw_check = check_match.group(1) or check_match.group(2)
checks = clean_checks(raw_check)
inputs[input_name] = {"type": input_type, "check": checks}
# Scan Previous Statement
prev_match = PREV_STATEMENT_RE.search(part)
if prev_match:
raw_check = prev_match.group(1) or prev_match.group(2)
if raw_check:
prev_checks = clean_checks(raw_check)
else:
prev_checks = ["*"]
# Scan Next Statement
next_match = NEXT_STATEMENT_RE.search(part)
has_next = bool(next_match)
# Scan Output
output_checks = []
output_match = OUTPUT_RE.search(part)
if output_match:
raw_check = output_match.group(1) or output_match.group(2)
if raw_check:
output_checks = clean_checks(raw_check)
else:
output_checks = ["*"]
if block_id in ["html_attr_class", "html_attr_id"]:
if "VALUE" in inputs:
if "String" not in (inputs["VALUE"]["check"] or []):
if inputs["VALUE"]["check"] is None: inputs["VALUE"]["check"] = []
inputs["VALUE"]["check"].append("String")
blocks[block_id] = {
"type": block_id,
"category": category_name,
"props": props,
"inputs": inputs,
"prev_checks": prev_checks,
"output_checks": output_checks,
"has_next": has_next
}
# SPECIAL HANDLING: Dynamic HTML blocks in html-blocks.js
if filename == "html-blocks.js":
tags_match = re.search(r"const htmlElementTags\s*=\s*\[(.*?)\];", content, re.DOTALL)
void_match = re.search(r"const voidHtmlTags\s*=\s*new Set\(\[(.*?)\]\)", content, re.DOTALL)
combined_match = re.search(r"const combinedTags\s*=\s*new Set\(\[(.*?)\]\)", content, re.DOTALL)
if tags_match:
tags_str = tags_match.group(1)
tags = [t.strip().strip("'\"") for t in tags_str.split(',') if t.strip()]
void_tags = set()
if void_match:
void_str = void_match.group(1)
void_tags = {t.strip().strip("'\"") for t in void_str.split(',') if t.strip()}
combined_tags = set()
if combined_match:
combined_str = combined_match.group(1)
combined_tags = {t.strip().strip("'\"") for t in combined_str.split(',') if t.strip()}
for tag in tags:
if tag in combined_tags: continue
block_id = f"html_{tag}"
if block_id in blocks: continue
is_void = tag in void_tags
inputs = {
"ATTRS": {"check": ["html_attr_bundle", "html_attr_entry", "css_selector"], "type": "value"}
}
if not is_void:
inputs["CONTENT"] = {"check": None, "type": "statement"}
blocks[block_id] = {
"type": block_id,
"category": "Structure",
"props": [],
"inputs": inputs,
"prev_checks": ["*"],
"output_checks": [],
"has_next": True
}
# INJECT MISSING/CUSTOM BLOCKS
# 1. html_script
if "html_script" not in blocks:
print("Injecting html_script block...")
blocks["html_script"] = {
"type": "html_script",
"category": "Structure",
"props": [],
"inputs": {
"ATTRS": {"check": ["html_attr_bundle", "html_attr_entry", "css_selector"], "type": "value"},
"CONTENT": {"check": ["content"], "type": "statement"}
},
"prev_checks": ["*"],
"output_checks": [],
"has_next": True
}
# 2. html_style
if "html_style" not in blocks:
print("Injecting html_style block...")
blocks["html_style"] = {
"type": "html_style",
"category": "Structure",
"props": [],
"inputs": {
"ATTRS": {"check": ["html_attr_bundle", "html_attr_entry", "css_selector"], "type": "value"},
"CONTENT": {"check": ["content"], "type": "statement"}
},
"prev_checks": ["*"],
"output_checks": [],
"has_next": True
}
# 3. html_raw (Custom block for raw HTML injection if frontend supports it, otherwise useful for AI to 'hold' code)
if "html_raw" not in blocks:
print("Injecting html_raw block...")
blocks["html_raw"] = {
"type": "html_raw",
"category": "Structure",
"props": [], # Maybe needs a 'CODE' prop?
"inputs": {
"CONTENT": {"check": ["String"], "type": "value"} # Takes a string value? Or statement?
},
# Let's assume it works like text_content but raw
"optional_props": ["CODE"],
"prev_checks": ["*"],
"output_checks": [],
"has_next": True
}
# Note: 'inputs' usually map to block inputs. 'props' to fields.
# Making it consistent with text_content
blocks["html_raw"]["inputs"] = {}
blocks["html_raw"]["props"] = ["CODE"] # Field for the raw code
return blocks
def generate_schema_file(blocks):
# 1. Build Block Definitions
schema_blocks = {}
for b_id, b_data in blocks.items():
children = {}
for name, info in b_data["inputs"].items():
children[name] = {"check": info["check"], "required": False}
schema_blocks[b_id] = {
"type": b_id,
"category": b_data["category"],
"description": f"Auto-generated {b_id}",
"required_props": [], # We assume all props optional for now or need logic
"optional_props": b_data["props"],
"children": children,
"color": "#3b82f6" # Placeholder
}
# 2. Build Allowed Connections
# Map CheckType -> List of BlockIDs that are that type
statement_providers = {}
value_providers = {}
for b_id, b_data in blocks.items():
# Blocks that provide a statement connection
for check in b_data["prev_checks"]:
if check not in statement_providers: statement_providers[check] = []
statement_providers[check].append(b_id)
# Blocks that provide a value connection (Output)
for check in b_data.get("output_checks", []):
if check not in value_providers: value_providers[check] = []
value_providers[check].append(b_id)
allowed_connections = {}
for b_id, b_data in blocks.items():
conns = {}
# Input Connections
for input_name, input_info in b_data["inputs"].items():
checks = input_info["check"]
input_type = input_info["type"]
# Select the correct provider map
providers = value_providers if input_type == "value" else statement_providers
allowed = []
if checks and "*" not in checks:
for c in checks:
# Specific checks
allowed.extend(providers.get(c, []))
# If no specific allowed blocks found OR if input is generic/untyped statement
if not allowed or (input_type == "statement" and (not checks or "*" in checks)):
# Fallback: include all generic statement providers (*)
# And also any block that doesn't have a specific type requirement if it's a statement
allowed.extend(providers.get("*", []))
# Special fix: if it's a statement input, we often want to allow ANY statement
# unless it's strictly typed (like metadata). For most HTML tags, we want this.
if input_type == "statement" and (not checks or "*" in checks):
for name, ids in providers.items():
if name == "*" or name is None or name == "content": # "content" is common in some blocks
allowed.extend(ids)
# Uniquify
allowed = list(set(allowed))
conns[input_name] = allowed if allowed else []
# Next Connection
if b_data["has_next"]:
conns["NEXT"] = "*" # Allow all valid next blocks for now
allowed_connections[b_id] = conns
# 3. Build Categories List
categories = {}
for b_id, b_data in blocks.items():
cat = b_data["category"]
if cat not in categories: categories[cat] = []
categories[cat].append(b_id)
# 4. Write Output
output_content = '"""\nAuto-generated Block Schema\n"""\n\n'
import pprint
schema_dict = {
"blocks": schema_blocks,
"allowed_connections": allowed_connections,
"block_categories": categories
}
# Use pprint to generate valid Python syntax (True/False/None instead of true/false/null)
# width=200 keeps it reasonably compact
formatted_dict = pprint.pformat(schema_dict, indent=4, width=120, sort_dicts=False)
output_content += "BLOCKLY_SCHEMA = " + formatted_dict + "\n\n"
# 5. Helper Functions
output_content += """
# Helper functions
def get_block_schema(block_type: str):
return BLOCKLY_SCHEMA["blocks"].get(block_type)
def get_allowed_children(block_type: str, input_name: str = None):
connections = BLOCKLY_SCHEMA["allowed_connections"].get(block_type, {})
if input_name:
allowed = connections.get(input_name, [])
return allowed if allowed != "*" else None
return connections
def get_blocks_by_category(category: str):
return BLOCKLY_SCHEMA["block_categories"].get(category, [])
"""
with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
f.write(output_content)
print(f"Generated schema with {len(blocks)} blocks in {OUTPUT_FILE}")
if __name__ == "__main__":
blocks = parse_blocks()
generate_schema_file(blocks)