feat: initial CTStruct framework
- CTNode: 트리 기본 단위 (전위/후위/BFS 순회, 탐색, 분리) - CTTree: 트리 컨테이너 (빌더 API, TOC, filter/map/merge) - NodeType: 구조/블록/인라인/메타 노드 타입 Enum - MarkdownParser: Markdown → CTTree 파서 - MarkdownRenderer: CTTree → Markdown 렌더러 - JSONRenderer: CTTree → JSON 직렬화 - 27개 단위/통합 테스트 전체 통과
This commit is contained in:
191
ctstruct/node.py
Normal file
191
ctstruct/node.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
@dataclass
|
||||
class CTNode:
|
||||
"""
|
||||
CTStruct 트리의 기본 노드.
|
||||
|
||||
Attributes:
|
||||
node_type : 노드의 의미적 타입
|
||||
content : 노드가 직접 보유하는 텍스트 값 (없으면 None)
|
||||
attributes : 노드 메타정보 (level, lang, href, …)
|
||||
node_id : 고유 식별자 (자동 생성)
|
||||
parent : 부모 노드 (루트는 None)
|
||||
children : 자식 노드 목록
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
# ── 자식 관리 ────────────────────────────────────────────
|
||||
|
||||
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 text_content(self) -> str:
|
||||
"""
|
||||
자신과 모든 하위 노드의 텍스트를 이어 붙여 반환.
|
||||
렌더러 없이 순수 텍스트 추출에 사용.
|
||||
"""
|
||||
parts: list[str] = []
|
||||
for node in self.walk("pre"):
|
||||
if node.content:
|
||||
parts.append(node.content)
|
||||
return "".join(parts)
|
||||
|
||||
def pretty(self, indent: int = 2) -> str:
|
||||
"""트리 구조를 사람이 읽기 좋은 형태로 출력."""
|
||||
pad = " " * (self.depth * indent)
|
||||
tag = self.node_type.name
|
||||
preview = repr(self.content[:20] + "…") if self.content and len(self.content) > 20 else repr(self.content)
|
||||
attrs = ""
|
||||
if self.attributes:
|
||||
attrs = " " + " ".join(f"{k}={v!r}" for k, v in self.attributes.items())
|
||||
line = f"{pad}[{self.node_id}] {tag}{attrs}: {preview}"
|
||||
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"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]
|
||||
Reference in New Issue
Block a user