- CTNode에 6개 신규 필드 추가 - header : 머리글 (노드 블록 앞 텍스트) - footer : 꼬리글 (노드 블록 뒤 텍스트) - prefix : 접두사 (content 직전 인라인) - suffix : 접미사 (content 직후 인라인) - data_type : 값의 자료형 힌트 (DataType enum) - value : 해석된 값 (Any — str/int/float/bool/list/dict 등) - types.py: DataType enum 추가 (UNSET, STRING~CODE, CUSTOM) - node.py: full_text() 메서드 추가 (6개 필드 순서대로 조합) - tree.py: to_dict / from_dict 직렬화에 신규 필드 포함 - 테스트: test_node_fields.py 신규 추가 (15개 케이스) - 전체 테스트 42/42 통과
250 lines
10 KiB
Python
250 lines
10 KiB
Python
"""
|
|
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]
|