feat(node): add header, footer, prefix, suffix, data_type, value fields

- 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 통과
This commit is contained in:
2026-05-30 21:13:22 +09:00
parent 3f26cbb687
commit fd98a4c705
9 changed files with 341 additions and 32 deletions

View File

@@ -4,7 +4,7 @@ CTStruct - 텍스트 양식을 구조화하는 트리 자료형 라이브러리
from .node import CTNode
from .tree import CTTree
from .types import NodeType, ListStyle, Alignment
from .types import NodeType, ListStyle, Alignment, DataType
__version__ = "0.1.0"
__all__ = ["CTNode", "CTTree", "NodeType", "ListStyle", "Alignment"]
__all__ = ["CTNode", "CTTree", "NodeType", "ListStyle", "Alignment", "DataType"]

View File

@@ -9,7 +9,7 @@ import uuid
from typing import Any, Iterator, Optional
from dataclasses import dataclass, field
from .types import NodeType
from .types import NodeType, DataType
@dataclass
@@ -17,15 +17,33 @@ class CTNode:
"""
CTStruct 트리의 기본 노드.
┌─────────────────────────────────────────────────────────┐
│ [header] │
│ prefix ←── content / value ──► suffix │
│ [footer] │
└─────────────────────────────────────────────────────────┘
Attributes:
node_type : 노드의 의미적 타입
content : 노드가 직접 보유하는 텍스트 (없으면 None)
node_type : 노드의 의미적 타입 (NodeType)
content : 노드가 직접 보유하는 원문 텍스트 (없으면 None)
attributes : 노드 메타정보 (level, lang, href, …)
node_id : 고유 식별자 (자동 생성)
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)
@@ -33,7 +51,17 @@ class CTNode:
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 반환 (체이닝 지원)."""
@@ -64,7 +92,7 @@ class CTNode:
self.parent.remove(self)
return self
# ── 탐색 ─────────────────────────────────────────────────
# ── 탐색 ─────────────────────────────────────────────────
@property
def depth(self) -> int:
@@ -134,13 +162,13 @@ class CTNode:
return None
def find_by_id(self, node_id: str) -> Optional["CTNode"]:
"""node_id로 노드 탐색."""
"""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)
@@ -149,34 +177,64 @@ class 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() 를 이어 붙여 반환.
렌더러 없이 순수 텍스트 추출에 사용.
"""
parts: list[str] = []
for node in self.walk("pre"):
if node.content:
parts.append(node.content)
return "".join(parts)
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
preview = repr(self.content[:20] + "") if self.content and len(self.content) > 20 else repr(self.content)
attrs = ""
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:
attrs = " " + " ".join(f"{k}={v!r}" for k, v in self.attributes.items())
line = f"{pad}[{self.node_id}] {tag}{attrs}: {preview}"
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})"
)

View File

@@ -8,7 +8,7 @@ from __future__ import annotations
from typing import Any, Callable, Iterator, Optional
from .node import CTNode
from .types import NodeType
from .types import NodeType, DataType
class CTTree:
@@ -150,11 +150,19 @@ class CTTree:
"""트리 전체를 Python dict 로 직렬화."""
def _serialize(node: CTNode) -> dict:
return {
"id": node.node_id,
"type": node.node_type.name,
"content": node.content,
"id": node.node_id,
"type": node.node_type.name,
"content": node.content,
"attributes": node.attributes,
"children": [_serialize(c) for c in node.children],
# ── 텍스트 장식 ──
"header": node.header,
"footer": node.footer,
"prefix": node.prefix,
"suffix": node.suffix,
# ── 타입 값 ──
"data_type": node.data_type.value,
"value": node.value,
"children": [_serialize(c) for c in node.children],
}
return _serialize(self.root)
@@ -164,11 +172,23 @@ class CTTree:
tree = cls()
def _deserialize(d: dict) -> CTNode:
raw_dtype = d.get("data_type", DataType.UNSET.value)
try:
dtype = DataType(raw_dtype)
except ValueError:
dtype = DataType.CUSTOM
node = CTNode(
node_type=NodeType[d["type"]],
content=d.get("content"),
attributes=d.get("attributes", {}),
node_id=d.get("id", ""),
node_type = NodeType[d["type"]],
content = d.get("content"),
attributes = d.get("attributes", {}),
node_id = d.get("id", ""),
header = d.get("header"),
footer = d.get("footer"),
prefix = d.get("prefix"),
suffix = d.get("suffix"),
data_type = dtype,
value = d.get("value"),
)
for child_data in d.get("children", []):
node.append(_deserialize(child_data))

View File

@@ -53,3 +53,40 @@ class Alignment(Enum):
CENTER = "center"
RIGHT = "right"
NONE = "none"
class DataType(Enum):
"""
CTNode.value 의 자료형 힌트.
파서·렌더러가 값을 해석할 때 참조하는 타입 태그입니다.
추후 타입이 추가될 수 있으며, 미지정 시 UNSET 을 사용합니다.
"""
UNSET = "unset" # 자료형 미지정 (기본값)
# ── 스칼라 ────────────────────────────────────────────────
STRING = "string" # 문자열
INTEGER = "integer" # 정수
FLOAT = "float" # 부동소수점
BOOLEAN = "boolean" # 참/거짓
NULL = "null" # 빈 값
# ── 날짜/시간 ─────────────────────────────────────────────
DATE = "date" # 날짜 (YYYY-MM-DD)
DATETIME = "datetime" # 날짜+시간 (ISO 8601)
TIME = "time" # 시간 (HH:MM:SS)
# ── 컬렉션 ────────────────────────────────────────────────
LIST = "list" # 배열/목록
DICT = "dict" # 키-값 맵
SET = "set" # 집합
# ── 텍스트 특화 ───────────────────────────────────────────
MARKDOWN = "markdown" # Markdown 서식 문자열
HTML = "html" # HTML 서식 문자열
JSON = "json" # JSON 직렬화 문자열
CODE = "code" # 소스 코드 문자열
# ── 확장 ──────────────────────────────────────────────────
CUSTOM = "custom" # 사용자 정의 타입