Files
CTStruct/ctstruct/tree.py
gm 6e3cd19b55 feat: initial CTStruct framework
- CTNode: 트리 기본 단위 (전위/후위/BFS 순회, 탐색, 분리)
- CTTree: 트리 컨테이너 (빌더 API, TOC, filter/map/merge)
- NodeType: 구조/블록/인라인/메타 노드 타입 Enum
- MarkdownParser: Markdown → CTTree 파서
- MarkdownRenderer: CTTree → Markdown 렌더러
- JSONRenderer: CTTree → JSON 직렬화
- 27개 단위/통합 테스트 전체 통과
2026-05-30 20:40:28 +09:00

198 lines
6.8 KiB
Python

"""
ctstruct/tree.py
CTTree - 트리 전체를 관리하는 컨테이너
"""
from __future__ import annotations
from typing import Any, Callable, Iterator, Optional
from .node import CTNode
from .types import NodeType
class CTTree:
"""
CTStruct 트리 컨테이너.
루트 노드를 통해 전체 트리를 관리하며,
파싱·렌더링·변환 파이프라인의 중심 객체.
Examples:
>>> tree = CTTree()
>>> sec = tree.add(NodeType.SECTION)
>>> sec.append(CTNode(NodeType.HEADING, "제목", {"level": 1}))
>>> sec.append(CTNode(NodeType.PARAGRAPH, "본문 내용입니다."))
>>> print(tree.pretty())
"""
def __init__(self, title: str = "", metadata: Optional[dict[str, Any]] = None) -> None:
self.root = CTNode(NodeType.ROOT)
if title:
self.root.set_attr("title", title)
if metadata:
meta_node = CTNode(NodeType.METADATA, attributes=metadata)
self.root.append(meta_node)
# ── 빌더 API ─────────────────────────────────────────────
def add(self, node_type: NodeType,
content: Optional[str] = None,
**attrs: Any) -> CTNode:
"""루트에 직접 자식 노드를 추가하고 반환."""
node = CTNode(node_type, content, dict(attrs))
self.root.append(node)
return node
@staticmethod
def make_heading(text: str, level: int = 1) -> CTNode:
return CTNode(NodeType.HEADING, text, {"level": level})
@staticmethod
def make_paragraph(text: str) -> CTNode:
return CTNode(NodeType.PARAGRAPH, text)
@staticmethod
def make_code_block(code: str, language: str = "") -> CTNode:
return CTNode(NodeType.CODE_BLOCK, code, {"language": language})
@staticmethod
def make_list(items: list[str], ordered: bool = False) -> CTNode:
from .types import ListStyle
style = ListStyle.ORDERED if ordered else ListStyle.UNORDERED
lst = CTNode(NodeType.LIST, attributes={"style": style.value})
for item in items:
lst.append(CTNode(NodeType.LIST_ITEM, item))
return lst
@staticmethod
def make_table(headers: list[str], rows: list[list[str]]) -> CTNode:
table = CTNode(NodeType.TABLE)
header_row = CTNode(NodeType.TABLE_ROW, attributes={"is_header": True})
for h in headers:
header_row.append(CTNode(NodeType.TABLE_CELL, h, {"is_header": True}))
table.append(header_row)
for row in rows:
tr = CTNode(NodeType.TABLE_ROW)
for cell in row:
tr.append(CTNode(NodeType.TABLE_CELL, cell))
table.append(tr)
return table
# ── 탐색 ─────────────────────────────────────────────────
def find(self, node_type: NodeType) -> list[CTNode]:
"""트리 전체에서 특정 타입 노드 검색."""
return self.root.find(node_type)
def find_by_id(self, node_id: str) -> Optional[CTNode]:
return self.root.find_by_id(node_id)
def walk(self, order: str = "pre") -> Iterator[CTNode]:
return self.root.walk(order)
def headings(self) -> list[CTNode]:
return self.find(NodeType.HEADING)
def toc(self) -> list[dict[str, Any]]:
"""
문서 목차(TOC) 반환.
Returns:
[{"level": int, "text": str, "node_id": str}, ...]
"""
return [
{
"level": h.attr("level", 1),
"text": h.content or "",
"node_id": h.node_id,
}
for h in self.headings()
]
# ── 변환 ─────────────────────────────────────────────────
def filter(self, predicate: Callable[[CTNode], bool]) -> "CTTree":
"""
조건에 맞는 노드만 남긴 **새로운** 트리 반환.
원본 트리는 불변.
"""
import copy
new_tree = copy.deepcopy(self)
nodes_to_remove = [
n for n in new_tree.walk()
if not predicate(n) and not n.is_root
]
for n in nodes_to_remove:
if n.parent:
n.parent.remove(n)
return new_tree
def map(self, transform: Callable[[CTNode], None]) -> "CTTree":
"""
모든 노드에 transform 함수를 적용한 **새로운** 트리 반환.
"""
import copy
new_tree = copy.deepcopy(self)
for node in new_tree.walk():
transform(node)
return new_tree
def merge(self, other: "CTTree") -> "CTTree":
"""다른 트리의 루트 자식들을 현재 트리에 병합 (in-place)."""
import copy
for child in other.root.children:
self.root.append(copy.deepcopy(child))
return self
# ── 직렬화 ───────────────────────────────────────────────
def to_dict(self) -> dict[str, Any]:
"""트리 전체를 Python dict 로 직렬화."""
def _serialize(node: CTNode) -> dict:
return {
"id": node.node_id,
"type": node.node_type.name,
"content": node.content,
"attributes": node.attributes,
"children": [_serialize(c) for c in node.children],
}
return _serialize(self.root)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "CTTree":
"""dict 에서 트리 복원."""
tree = cls()
def _deserialize(d: dict) -> CTNode:
node = CTNode(
node_type=NodeType[d["type"]],
content=d.get("content"),
attributes=d.get("attributes", {}),
node_id=d.get("id", ""),
)
for child_data in d.get("children", []):
node.append(_deserialize(child_data))
return node
tree.root = _deserialize(data)
return tree
# ── 표현 ─────────────────────────────────────────────────
@property
def node_count(self) -> int:
return sum(1 for _ in self.walk())
@property
def depth(self) -> int:
return max((n.depth for n in self.walk()), default=0)
def pretty(self) -> str:
return self.root.pretty()
def __repr__(self) -> str:
return f"CTTree(nodes={self.node_count}, depth={self.depth})"
def __len__(self) -> int:
return len(self.root.children)