253 lines
12 KiB
Python
253 lines
12 KiB
Python
# 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 <StructuredText>, 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
|
|
} |