#!/usr/bin/env python3 """Parse Cognito form definition JS and output structured YAML for Healthie migration.""" import re import sys import json def parse_form_data(filepath): """Extract field definitions from the Cognito form JS bundle.""" with open(filepath) as f: content = f.read() # Extract the options object options_match = re.search(r"var options\s*=\s*", content) if not options_match: print("Could not find options object", file=sys.stderr) sys.exit(1) result = { "form_name": "MetaFlow Enrollment Assessment", "form_version": "cognito_v1", "pages": {}, "fields": {}, "conditional_sections": {}, "section_definitions": {} } # Extract page index page_idx_match = re.search(r"Page_Index:[^{]*constant:\s*({[^}]+})", content) if page_idx_match: page_obj = {} for m in re.finditer(r'(\w+):\s*(\d+)', page_idx_match.group(1)): field_name = m.group(1) page_num = int(m.group(2)) page_key = f"page_{page_num}" if page_key not in page_obj: page_obj[page_key] = [] page_obj[page_key].append(field_name) result["pages"] = page_obj # Extract all section type definitions section_pattern = re.compile( r"'(Forms\.FormEntry\.MetaFlowHealth1\.MetaFlowEnrollmentAssessment\.(\w+))':\s*\{", re.MULTILINE ) sections = {} for m in section_pattern.finditer(content): full_type = m.group(1) section_name = m.group(2) # Find the closing brace for this section by counting braces start = m.end() depth = 1 pos = start while pos < len(content) and depth > 0: if content[pos] == '{': depth += 1 elif content[pos] == '}': depth -= 1 pos += 1 section_body = content[start:pos-1] sections[section_name] = section_body result["section_definitions"][section_name] = section_body # Extract field definitions from main form entry main_type = "'Forms.FormEntry.MetaFlowHealth1.MetaFlowEnrollmentAssessment':" main_idx = content.find(main_type) if main_idx >= 0: # Find the $storageProperties to know which fields are actual fields storage_match = re.search(r"\$storageProperties:\s*\{([^}]+)\}", content[main_idx:]) if storage_match: storage_props = {} for m in re.finditer(r'x(\d+):\s*"(\w+)"', storage_match.group(1)): storage_props[int(m.group(1))] = m.group(2) result["_storage_props"] = storage_props # Now parse fields from each section all_fields = {} def extract_field_info(section_body, section_name=None): """Extract field info from a section body.""" fields = {} # Match field definitions: FieldName: { label: "...", type: ... } # Handle both simple and complex field definitions field_pattern = re.compile( r"(\w+(?!\s*\(?\s*\)|\s*\$)):\s*\{", re.MULTILINE ) for fm in field_pattern.finditer(section_body): field_name = fm.group(1) # Skip non-field properties if field_name.startswith('$') or field_name in ('Form', 'init', 'set', 'get', 'function', 'meta'): continue # Extract the field's body (brace counting) start = fm.end() depth = 1 pos = start while pos < len(section_body) and depth > 0: if section_body[pos] == '{': depth += 1 elif section_body[pos] == '}': depth -= 1 pos += 1 field_body = section_body[start:pos-1] # Extract label label_match = re.search(r'label:\s*"([^"]*)"', field_body) label = label_match.group(1) if label_match else field_name # Extract type type_match = re.search(r'type:\s*\'?(\w+(?:\[\])?)\'?', field_body) ftype = type_match.group(1) if type_match else 'String' # Skip Choice[] helper arrays (they end with _Choices) if field_name.endswith('_Choices') or ftype == 'Choice[]': continue # Skip transient computed properties if '$transient' in field_body: continue field_info = { "cognito_field_id": field_name, "label": label, "type": ftype, } # Extract helptext helptext_match = re.search(r'helptext:\s*"(<[^"]*>)"', field_body) if helptext_match: # Strip HTML tags for clean helptext raw_ht = helptext_match.group(1) clean_ht = re.sub(r'<[^>]+>', '', raw_ht) clean_ht = clean_ht.strip() if clean_ht: field_info["helptext"] = clean_ht # Extract allowedValues (choices) av_match = re.search(r'allowedValues:\s*\{[^}]*function:\s*function\(\)\s*\{\s*return\s*\[([^\]]+)\]', field_body) if av_match: choices_str = av_match.group(1) choices = [c.strip().strip('"').strip("'") for c in choices_str.split('", "')] # Clean up any trailing garbage choices = [c for c in choices if c and not c.startswith(']')] field_info["choices"] = choices field_info["type"] = "choice" if ftype == 'String' else "multi_choice" # Check if multi-select if ftype == 'String[]': field_info["type"] = "multi_choice" # Extract required if 'required:' in field_body: field_info["required"] = True # Extract default default_match = re.search(r'default:\s*"(.*?)"(?=\s*[,}])', field_body) if default_match: field_info["default"] = default_match.group(1) # Extract length max length_match = re.search(r'max:\s*(\d+)', field_body) if length_match and ftype in ('String',): field_info["max_length"] = int(length_match.group(1)) if section_name: field_info["section"] = section_name fields[field_name] = field_info return fields # Parse top-level fields from the main form entry type main_start = content.find(main_type) if main_start >= 0: # Find body of main type brace_start = content.find('{', main_start) depth = 1 pos = brace_start + 1 while pos < len(content) and depth > 0: if content[pos] == '{': depth += 1 elif content[pos] == '}': depth -= 1 pos += 1 main_body = content[brace_start+1:pos-1] # Extract top-level fields (not in nested sections) for field_name in storage_props.values() if storage_props else []: if field_name.startswith('IHave') or field_name.startswith('Name') or \ field_name.startswith('DateOfBirth') or field_name.startswith('Email') or \ field_name.startswith('Phone') or field_name.startswith('Address') or \ field_name.startswith('SexOn') or field_name.startswith('DoYouHave') or \ field_name.startswith('PleaseList') or field_name.startswith('Form') or \ field_name.startswith('Entry') or field_name.startswith('Id'): # Find field definition in main body field_pattern = re.compile( rf'{re.escape(field_name)}:\s*\{{', re.MULTILINE ) fm = field_pattern.search(main_body) if fm: start = fm.end() depth = 1 pos = start while pos < len(main_body) and depth > 0: if main_body[pos] == '{': depth += 1 elif main_body[pos] == '}': depth -= 1 pos += 1 field_body = main_body[start:pos-1] label_match = re.search(r'label:\s*"([^"]*)"', field_body) label = label_match.group(1) if label_match else field_name type_match = re.search(r'type:\s*\'?(\w+(?:\[\])?)\'?', field_body) ftype = type_match.group(1) if type_match else 'String' if field_name in ('Id', 'Entry', 'Form', 'Form_Theme', 'Form_Available', 'Allow_Workflow_Links', 'Entry_Statuses', 'Form_Available', '_p1_Visible', 'Page3Visible', 'Female_Visible', 'Male_Visible', 'VascularPlaqueAtherosclerosisVascularNarrowing_Visible', 'FattyLiver_Visible', 'KidneyUrineRenalFunctionReports_Visible'): continue field_info = { "cognito_field_id": field_name, "label": label, "type": ftype, } helptext_match = re.search(r'helptext:\s*"(<[^"]*>)"', field_body) if helptext_match: raw_ht = helptext_match.group(1) clean_ht = re.sub(r'<[^>]+>', '', raw_ht).strip() if clean_ht: field_info["helptext"] = clean_ht av_match = re.search(r'allowedValues:\s*\{[^}]*function:\s*function\(\)\s*\{\s*return\s*\[([^\]]+)\]', field_body) if av_match: choices_str = av_match.group(1) choices = [c.strip().strip('"').strip("'") for c in choices_str.split('", "')] choices = [c for c in choices if c and not c.startswith(']')] field_info["choices"] = choices field_info["type"] = "choice" if ftype == 'String' else "multi_choice" if ftype == 'Boolean': field_info["type"] = "boolean" elif ftype == 'Date': field_info["type"] = "date" elif ftype == 'String[]': field_info["type"] = "multi_choice" if 'required:' in field_body: field_info["required"] = True all_fields[field_name] = field_info # Parse section fields for section_name, section_body in sections.items(): section_fields = extract_field_info(section_body, section_name) all_fields.update(section_fields) # Extract conditional visibility rules conditional_rules = {} cond_pattern = re.compile( r'(\w+_Visible):\s*\{[^}]*\$transient[^}]*dependsOn:\s*"([^"]+)"[^}]*function[^}]*return\s*\(([^)]+)\)', re.DOTALL ) for cm in cond_pattern.finditer(content): section_or_page = cm.group(1) depends_on = cm.group(2) condition_expr = cm.group(3).strip() # Determine which section this applies to section = section_or_page.replace('_Visible', '') conditional_rules[section] = { "depends_on": depends_on, "condition": condition_expr } result["fields"] = all_fields result["conditional_rules"] = conditional_rules return result def main(): data = parse_form_data("sample_data/cognito_form_data.js") # Output as JSON for now print(json.dumps(data, indent=2, default=str)) if __name__ == "__main__": main()