# ToUpload/parsers/parse_scl.py # -*- coding: utf-8 -*- from lxml import etree import re # Importar desde las utilidades del parser from .parser_utils import ns, get_multilingual_text def reconstruct_scl_from_tokens(st_node): """ Reconstruye SCL desde , mejorando el manejo de variables, constantes literales, tokens básicos, espacios y saltos de línea. """ if st_node is None: return "// Error: StructuredText node not found.\n" scl_parts = [] # Usar st:* para obtener todos los elementos hijos dentro del namespace st children = st_node.xpath("./st:*", namespaces=ns) for elem in children: tag = etree.QName(elem.tag).localname if tag == "Token": scl_parts.append(elem.get("Text", "")) elif tag == "Blank": # Añadir espacios solo si es necesario o más de uno num_spaces = int(elem.get("Num", 1)) if not scl_parts or not scl_parts[-1].endswith(" "): scl_parts.append(" " * num_spaces) elif num_spaces > 1: scl_parts.append(" " * (num_spaces -1)) elif tag == "NewLine": # Quitar espacios finales antes del salto de línea if scl_parts: scl_parts[-1] = scl_parts[-1].rstrip() scl_parts.append("\n") elif tag == "Access": scope = elem.get("Scope") access_str = f"/*_ERR_Scope_{scope}_*/" # Placeholder # --- Variables --- if scope in [ "GlobalVariable", "LocalVariable", "TempVariable", "InOutVariable", "InputVariable", "OutputVariable", "ConstantVariable", "GlobalConstant", "LocalConstant" # Añadir constantes simbólicas ]: symbol_elem = elem.xpath("./st:Symbol", namespaces=ns) if symbol_elem: components = symbol_elem[0].xpath("./st:Component", namespaces=ns) symbol_text_parts = [] for i, comp in enumerate(components): name = comp.get("Name", "_ERR_COMP_") if i > 0: symbol_text_parts.append(".") # Check for HasQuotes attribute (adjust namespace if needed) # El atributo está en el Component o en el Access padre? Probar ambos has_quotes_comp = comp.get("HasQuotes", "false").lower() == "true" # Check directly on Component has_quotes_access = False access_parent = comp.xpath("ancestor::st:Access[1]", namespaces=ns) # Get immediate Access parent if access_parent: has_quotes_attr = access_parent[0].xpath("./st:BooleanAttribute[@Name='HasQuotes']/text()", namespaces=ns) has_quotes_access = has_quotes_attr and has_quotes_attr[0].lower() == 'true' has_quotes = has_quotes_comp or has_quotes_access is_temp = name.startswith("#") # Apply quotes based on HasQuotes or if it's the first component and not temp if has_quotes or (i == 0 and not is_temp and '"' not in name): # Avoid double quotes symbol_text_parts.append(f'"{name}"') else: symbol_text_parts.append(name) # --- Array Index Access --- index_access_nodes = comp.xpath("./st:Access", namespaces=ns) if index_access_nodes: # Llamada recursiva para cada índice indices_text = [reconstruct_scl_from_tokens(idx_node) for idx_node in index_access_nodes] # Limpiar saltos de línea dentro de los corchetes indices_cleaned = [idx.replace('\n', '').strip() for idx in indices_text] symbol_text_parts.append(f"[{','.join(indices_cleaned)}]") access_str = "".join(symbol_text_parts) else: access_str = f"/*_ERR_NO_SYMBOL_IN_{scope}_*/" # --- Constantes Literales --- elif scope == "LiteralConstant": constant_elem = elem.xpath("./st:Constant", namespaces=ns) if constant_elem: val_elem = constant_elem[0].xpath("./st:ConstantValue/text()", namespaces=ns) type_elem = constant_elem[0].xpath("./st:ConstantType/text()", namespaces=ns) const_type = type_elem[0].strip().lower() if type_elem and type_elem[0] is not None else "" const_val = val_elem[0].strip() if val_elem and val_elem[0] is not None else "_ERR_CONSTVAL_" # Formatear según tipo if const_type == "bool": access_str = const_val.upper() elif const_type.lower() == "string": replaced_val = const_val.replace("'", "''") access_str = f"'{replaced_val}'" elif const_type.lower() == "char": replaced_val = const_val.replace("'", "''") access_str = f"'{replaced_val}'" elif const_type == "wstring": replaced_val = const_val.replace("'", "''") access_str = f"WSTRING#'{replaced_val}'" elif const_type == "wchar": replaced_val = const_val.replace("'", "''") access_str = f"WCHAR#'{replaced_val}'" elif const_type == "time": access_str = f"T#{const_val}" elif const_type == "ltime": access_str = f"LT#{const_val}" elif const_type == "s5time": access_str = f"S5T#{const_val}" elif const_type == "date": access_str = f"D#{const_val}" elif const_type == "dtl": access_str = f"DTL#{const_val}" elif const_type == "dt": access_str = f"DT#{const_val}" elif const_type == "tod": access_str = f"TOD#{const_val}" elif const_type in ["int", "dint", "sint", "usint", "uint", "udint", "real", "lreal", "word", "dword", "byte"]: # Añadir .0 para reales si no tienen decimal if const_type in ["real", "lreal"] and '.' not in const_val and 'e' not in const_val.lower(): access_str = f"{const_val}.0" else: access_str = const_val else: # Otros tipos (LWORD, etc.) o desconocidos access_str = const_val else: access_str = "/*_ERR_NOCONST_*/" # --- Llamadas a Funciones/Bloques (Scope=Call) --- elif scope == "Call": call_info_node = elem.xpath("./st:CallInfo", namespaces=ns) if call_info_node: ci = call_info_node[0] call_name = ci.get("Name", "_ERR_CALLNAME_") call_type = ci.get("BlockType") # FB, FC, etc. # Parámetros (están como Access o Token dentro de CallInfo/Parameter) params = ci.xpath("./st:Parameter", namespaces=ns) param_parts = [] for p in params: p_name = p.get("Name", "_ERR_PARAMNAME_") # El valor del parámetro está dentro del nodo Parameter p_value_node = p.xpath("./st:Access | ./st:Token", namespaces=ns) # Buscar Access o Token p_value_scl = "" if p_value_node: p_value_scl = reconstruct_scl_from_tokens(p) # Parsear el contenido del parámetro p_value_scl = p_value_scl.replace('\n', '').strip() # Limpiar SCL resultante param_parts.append(f"{p_name} := {p_value_scl}") # Manejar FB vs FC if call_type == "FB": instance_node = ci.xpath("./st:Instance/st:Component/@Name", namespaces=ns) if instance_node: instance_name = f'"{instance_node[0]}"' access_str = f"{instance_name}({', '.join(param_parts)})" else: # FB sin instancia? Podría ser STAT access_str = f'"{call_name}"({", ".join(param_parts)}) (* FB sin instancia explícita? *)' elif call_type == "FC": access_str = f'"{call_name}"({", ".join(param_parts)})' else: # Otros tipos de llamada access_str = f'"{call_name}"({", ".join(param_parts)}) (* Tipo: {call_type} *)' else: access_str = "/*_ERR_NO_CALLINFO_*/" # Añadir más scopes si son necesarios (e.g., Address, Label, Reference) scl_parts.append(access_str) elif tag == "Comment" or tag == "LineComment": # Usar get_multilingual_text del parser_utils comment_text = get_multilingual_text(elem) if tag == "Comment": scl_parts.append(f"(* {comment_text} *)") else: scl_parts.append(f"// {comment_text}") # Ignorar otros tipos de nodos si no son relevantes para el SCL full_scl = "".join(scl_parts) # --- Re-indentación Simple --- output_lines = [] indent_level = 0 indent_str = " " # Dos espacios for line in full_scl.splitlines(): trimmed_line = line.strip() if not trimmed_line: # Mantener líneas vacías? Opcional. # output_lines.append("") continue # Reducir indentación ANTES de imprimir para END, ELSE, etc. if trimmed_line.upper().startswith(("END_", "UNTIL", "}")) or \ trimmed_line.upper() in ["ELSE", "ELSIF"]: indent_level = max(0, indent_level - 1) output_lines.append(indent_str * indent_level + trimmed_line) # Aumentar indentación DESPUÉS de imprimir para IF, FOR, etc. # Ser más específico con las palabras clave que aumentan indentación # Usar .upper() para ignorar mayúsculas/minúsculas line_upper = trimmed_line.upper() if line_upper.endswith(("THEN", "DO", "OF", "{")) or \ line_upper.startswith(("IF ", "FOR ", "WHILE ", "CASE ", "REPEAT", "STRUCT")) or \ line_upper == "ELSE": # Excepción: No indentar después de ELSE IF if not (line_upper == "ELSE" and "IF" in output_lines[-1].upper()): indent_level += 1 return "\n".join(output_lines) def parse_scl_network(network_element): """ Parsea una red SCL extrayendo el código fuente reconstruido. Devuelve un diccionario representando la red para el JSON. """ network_id = network_element.get("ID", "UnknownSCL_ID") network_lang = "SCL" # Sabemos que es SCL # Buscar NetworkSource y luego StructuredText network_source_node = network_element.xpath(".//flg:NetworkSource", namespaces=ns) structured_text_node = None if network_source_node: structured_text_node_list = network_source_node[0].xpath("./st:StructuredText", namespaces=ns) if structured_text_node_list: structured_text_node = structured_text_node_list[0] reconstructed_scl = "// SCL extraction failed: StructuredText node not found.\n" if structured_text_node is not None: reconstructed_scl = reconstruct_scl_from_tokens(structured_text_node) # Crear la estructura de datos para la red parsed_network_data = { "id": network_id, "language": network_lang, "logic": [ # SCL se guarda como un único bloque lógico { "instruction_uid": f"SCL_{network_id}", # UID sintético "type": "RAW_SCL_CHUNK", # Tipo especial para SCL crudo "scl": reconstructed_scl, # El código SCL reconstruido } ], # No añadimos error aquí, reconstruct_scl_from_tokens ya incluye comentarios de error } return parsed_network_data # --- Función de Información del Parser --- def get_parser_info(): """Devuelve la información para este parser.""" return { 'language': ['SCL'], # Lista de lenguajes soportados 'parser_func': parse_scl_network # Función a llamar }