""" ctstruct/node.py CTNode - 트리의 기본 단위 노드 """ from __future__ import annotations import uuid from typing import Any, Iterator, Optional from dataclasses import dataclass, field from .types import NodeType, DataType @dataclass class CTNode: """ CTStruct 트리의 기본 노드. ┌─────────────────────────────────────────────────────────┐ │ [header] │ │ prefix ←── content / value ──► suffix │ │ [footer] │ └─────────────────────────────────────────────────────────┘ Attributes: node_type : 노드의 의미적 타입 (NodeType) content : 노드가 직접 보유하는 원문 텍스트 (없으면 None) attributes : 노드 메타정보 (level, lang, href, …) node_id : 고유 식별자 (자동 생성, 8자리 hex) parent : 부모 노드 (루트는 None) children : 자식 노드 목록 ── 텍스트 장식 ────────────────────────────────────── header : 머리글 — 노드 블록 앞에 오는 텍스트/레이블 footer : 꼬리글 — 노드 블록 뒤에 오는 텍스트/레이블 prefix : 접두사 — content/value 직전 인라인 텍스트 suffix : 접미사 — content/value 직후 인라인 텍스트 ── 타입 값 ────────────────────────────────────────── data_type : 값의 자료형 힌트 (DataType enum) value : 구조화된 값 (str·int·float·bool·list·dict 등) content 는 원문 텍스트, value 는 해석된 값 """ # ── 기본 필드 ───────────────────────────────────────────── node_type : NodeType content : Optional[str] = None attributes : dict[str, Any] = field(default_factory=dict) node_id : str = field(default_factory=lambda: uuid.uuid4().hex[:8]) parent : Optional["CTNode"] = field(default=None, repr=False, compare=False) children : list["CTNode"] = field(default_factory=list) # ── 텍스트 장식 필드 ────────────────────────────────────── header : Optional[str] = None # 머리글 footer : Optional[str] = None # 꼬리글 prefix : Optional[str] = None # 접두사 suffix : Optional[str] = None # 접미사 # ── 타입 값 필드 ────────────────────────────────────────── data_type : DataType = field(default=DataType.UNSET) value : Any = None # 해석된 값 # ── 자식 관리 ───────────────────────────────────────────── def append(self, child: "CTNode") -> "CTNode": """자식 노드를 추가하고 self 반환 (체이닝 지원).""" child.parent = self self.children.append(child) return self def prepend(self, child: "CTNode") -> "CTNode": """자식 노드를 맨 앞에 삽입.""" child.parent = self self.children.insert(0, child) return self def insert(self, index: int, child: "CTNode") -> "CTNode": """지정 위치에 자식 노드 삽입.""" child.parent = self self.children.insert(index, child) return self def remove(self, child: "CTNode") -> None: """자식 노드를 제거.""" self.children.remove(child) child.parent = None def detach(self) -> "CTNode": """자신을 부모에서 분리하고 self 반환.""" if self.parent: self.parent.remove(self) return self # ── 탐색 ────────────────────────────────────────────────── @property def depth(self) -> int: """루트로부터의 깊이 (루트 = 0).""" return 0 if self.parent is None else self.parent.depth + 1 @property def is_leaf(self) -> bool: return len(self.children) == 0 @property def is_root(self) -> bool: return self.parent is None @property def root(self) -> "CTNode": """트리 최상위 루트 노드 반환.""" return self if self.is_root else self.parent.root # type: ignore[union-attr] @property def siblings(self) -> list["CTNode"]: """같은 부모를 가진 형제 노드 목록.""" if self.parent is None: return [] return [n for n in self.parent.children if n is not self] def ancestors(self) -> Iterator["CTNode"]: """부모 → 루트 방향 이터레이터.""" node = self.parent while node is not None: yield node node = node.parent def walk(self, order: str = "pre") -> Iterator["CTNode"]: """ 트리 전체 순회. Args: order: "pre" (전위) | "post" (후위) | "bfs" (너비 우선) """ if order == "pre": yield self for child in self.children: yield from child.walk("pre") elif order == "post": for child in self.children: yield from child.walk("post") yield self elif order == "bfs": queue = [self] while queue: node = queue.pop(0) yield node queue.extend(node.children) else: raise ValueError(f"Unknown traversal order: {order!r}") def find(self, node_type: NodeType) -> list["CTNode"]: """특정 타입의 모든 하위 노드 반환.""" return [n for n in self.walk() if n.node_type == node_type] def find_one(self, node_type: NodeType) -> Optional["CTNode"]: """특정 타입의 첫 번째 하위 노드 반환.""" for n in self.walk(): if n.node_type == node_type: return n return None def find_by_id(self, node_id: str) -> Optional["CTNode"]: """node_id 로 노드 탐색.""" for n in self.walk(): if n.node_id == node_id: return n return None # ── 속성 접근 헬퍼 ──────────────────────────────────────── def attr(self, key: str, default: Any = None) -> Any: return self.attributes.get(key, default) def set_attr(self, key: str, value: Any) -> "CTNode": self.attributes[key] = value return self # ── 텍스트 조합 ─────────────────────────────────────────── def full_text(self) -> str: """ header + prefix + content + suffix + footer 를 순서대로 이어 반환. 렌더러 없이 노드의 완전한 텍스트 표현을 빠르게 얻을 때 사용. """ parts: list[str] = [] if self.header is not None: parts.append(self.header) if self.prefix is not None: parts.append(self.prefix) if self.content is not None: parts.append(self.content) if self.suffix is not None: parts.append(self.suffix) if self.footer is not None: parts.append(self.footer) return "".join(parts) def text_content(self) -> str: """ 자신과 모든 하위 노드의 full_text() 를 이어 붙여 반환. 렌더러 없이 순수 텍스트 추출에 사용. """ return "".join(n.full_text() for n in self.walk("pre")) # ── 표현 ────────────────────────────────────────────────── def pretty(self, indent: int = 2) -> str: """트리 구조를 사람이 읽기 좋은 형태로 출력.""" pad = " " * (self.depth * indent) tag = self.node_type.name dtype = f" dtype={self.data_type.value}" if self.data_type != DataType.UNSET else "" def _clip(s: Optional[str]) -> str: if s is None: return "None" return repr(s[:20] + "…") if len(s) > 20 else repr(s) # 핵심 필드만 한 줄로 표시 core = f"content={_clip(self.content)}" extras: list[str] = [] if self.header is not None: extras.append(f"header={_clip(self.header)}") if self.footer is not None: extras.append(f"footer={_clip(self.footer)}") if self.prefix is not None: extras.append(f"prefix={_clip(self.prefix)}") if self.suffix is not None: extras.append(f"suffix={_clip(self.suffix)}") if self.value is not None: extras.append(f"value={self.value!r}") if self.attributes: for k, v in self.attributes.items(): extras.append(f"{k}={v!r}") extra_str = (" | " + ", ".join(extras)) if extras else "" line = f"{pad}[{self.node_id}] {tag}{dtype}: {core}{extra_str}" child_lines = "\n".join(c.pretty(indent) for c in self.children) return f"{line}\n{child_lines}" if child_lines else line def __repr__(self) -> str: return ( f"CTNode(id={self.node_id!r}, type={self.node_type.name}, " f"dtype={self.data_type.value}, " f"children={len(self.children)}, content={self.content!r})" ) def __len__(self) -> int: """자식 수 반환.""" return len(self.children) def __iter__(self) -> Iterator["CTNode"]: return iter(self.children) def __getitem__(self, index: int) -> "CTNode": return self.children[index]