Spaces:
Sleeping
Sleeping
3ssem0
fix: configuration error - aligned entry point to app.py and enforced LF/BOM-less encoding
ec47953 | 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) | |