# ----------------------------------------------------------------------------- # Description: Layout YAML file filtering, parsing, and staging helpers. # Inside functions: is_layout_cell_filename, parse_layout_cell_content, read_layout_cell_file, load_layout_cell_files, layout_cell_filename, write_layout_cells_to_directory # Developer : Qin Yue @ 2026 # Organization : OptiHK Limited # ----------------------------------------------------------------------------- import os import re import yaml LAYOUT_YAML_EXTENSIONS = (".yml", ".yaml") ROUTE_SIDECAR_SUFFIXES = (".routes.yml", ".routes.yaml") class LayoutFileError(ValueError): """Raised when a saved layout YAML file cannot be used as a cell.""" def is_layout_cell_filename(filename): """Return True for user layout cell YAML files, excluding sidecars/manifests.""" lower = (filename or "").lower() if lower == "technology.yml": return False if lower.endswith(ROUTE_SIDECAR_SUFFIXES): return False return lower.endswith(LAYOUT_YAML_EXTENSIONS) def is_layout_cell_document(data): """Return True when parsed YAML looks like an mxPIC saved cell/project.""" if not isinstance(data, dict): return False kind = data.get("kind") if kind and str(kind).strip().lower() != "cell": return False layout_keys = { "schema_version", "canvas_size", "canvasSize", "instances", "elements", "pins", "ports", "bundles", } return any(key in data for key in layout_keys) def parse_layout_cell_content(content, source="layout YAML"): """Parse and validate saved layout YAML content.""" try: data = yaml.safe_load(content or "") or {} except yaml.YAMLError as exc: raise LayoutFileError(f"{source} is not valid YAML: {exc}") from exc if not is_layout_cell_document(data): raise LayoutFileError(f"{source} is not a saved mxPIC layout cell") return data def read_layout_cell_file(path): """Read a saved layout cell file and return parsed data plus raw content.""" try: with open(path, "r", encoding="utf-8") as file: content = file.read() except OSError as exc: raise LayoutFileError(f"{os.path.basename(path)} could not be read: {exc}") from exc return parse_layout_cell_content(content, os.path.basename(path)), content def load_layout_cell_files(project_dir): """Load valid layout cell files from a project directory and collect warnings.""" cells = [] warnings = [] if not os.path.isdir(project_dir): return cells, warnings for filename in sorted(os.listdir(project_dir)): if not is_layout_cell_filename(filename): continue path = os.path.join(project_dir, filename) if not os.path.isfile(path): continue try: data, content = read_layout_cell_file(path) except LayoutFileError as exc: warnings.append(f"Skipped {filename}: {exc}") continue cells.append({ "filename": filename, "name": str(data.get("name") or os.path.splitext(filename)[0]), "data": data, "content": content, }) return cells, warnings def layout_cell_filename(cell_name, fallback="canvas_1.yml"): """Build a safe filename for staging a parsed layout cell.""" name = re.sub(r"[^A-Za-z0-9_.-]+", "_", str(cell_name or "")).strip("._") if not name: return fallback return f"{name}.yml" def write_layout_cells_to_directory(cells, output_dir): """Write valid layout cell contents into a clean staging directory.""" os.makedirs(output_dir, exist_ok=True) used_names = set() for index, cell in enumerate(cells, start=1): filename = layout_cell_filename(cell.get("name"), cell.get("filename") or f"cell_{index}.yml") base, ext = os.path.splitext(filename) unique_filename = filename counter = 1 while unique_filename.lower() in used_names: counter += 1 unique_filename = f"{base}_{counter}{ext}" used_names.add(unique_filename.lower()) with open(os.path.join(output_dir, unique_filename), "w", encoding="utf-8") as file: file.write(cell["content"])