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:
2026-05-30 20:40:28 +09:00
commit 6e3cd19b55
38 changed files with 1695 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
from .base import BaseRenderer
from .markdown import MarkdownRenderer
from .json_renderer import JSONRenderer
__all__ = ["BaseRenderer", "MarkdownRenderer", "JSONRenderer"]

Binary file not shown.

View File

@@ -0,0 +1,31 @@
"""
ctstruct/renderers/base.py
렌더러 추상 베이스 클래스
"""
from abc import ABC, abstractmethod
from ..tree import CTTree
class BaseRenderer(ABC):
"""
모든 렌더러의 공통 인터페이스.
구현 클래스는 render() 메서드를 오버라이드합니다.
"""
@abstractmethod
def render(self, tree: CTTree) -> str:
"""
CTTree 를 대상 포맷 텍스트로 변환.
Args:
tree: 렌더링할 CTTree
Returns:
변환된 텍스트 문자열
"""
...
def __call__(self, tree: CTTree) -> str:
return self.render(tree)

View File

@@ -0,0 +1,25 @@
"""
ctstruct/renderers/json_renderer.py
CTTree → JSON 렌더러
"""
from __future__ import annotations
import json
from ..tree import CTTree
from .base import BaseRenderer
class JSONRenderer(BaseRenderer):
"""CTTree 를 JSON 문자열로 직렬화."""
def __init__(self, indent: int = 2, ensure_ascii: bool = False) -> None:
self.indent = indent
self.ensure_ascii = ensure_ascii
def render(self, tree: CTTree) -> str:
return json.dumps(
tree.to_dict(),
indent=self.indent,
ensure_ascii=self.ensure_ascii,
)

View File

@@ -0,0 +1,88 @@
"""
ctstruct/renderers/markdown.py
CTTree → Markdown 렌더러
"""
from __future__ import annotations
from ..node import CTNode
from ..tree import CTTree
from ..types import NodeType
from .base import BaseRenderer
class MarkdownRenderer(BaseRenderer):
"""CTTree 를 Markdown 텍스트로 렌더링."""
def render(self, tree: CTTree) -> str:
lines: list[str] = []
# frontmatter
meta = tree.root.attr("metadata")
if meta:
lines.append("---")
for k, v in meta.items():
lines.append(f"{k}: {v}")
lines.append("---")
lines.append("")
for child in tree.root:
lines.extend(self._render_node(child))
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def _render_node(self, node: CTNode) -> list[str]:
t = node.node_type
if t == NodeType.SECTION:
out: list[str] = []
for c in node:
out.extend(self._render_node(c))
out.append("")
return out
if t == NodeType.HEADING:
level = node.attr("level", 1)
return [f"{'#' * level} {node.content or ''}"]
if t == NodeType.PARAGRAPH:
return [node.content or ""]
if t == NodeType.CODE_BLOCK:
lang = node.attr("language", "")
return [f"```{lang}", node.content or "", "```"]
if t == NodeType.BLOCKQUOTE:
return [f"> {line}" for line in (node.content or "").splitlines()]
if t == NodeType.DIVIDER:
return ["---"]
if t == NodeType.LIST:
out = []
style = node.attr("style", "unordered")
for idx, item in enumerate(node, 1):
prefix = f"{idx}." if style == "ordered" else "-"
out.append(f"{prefix} {item.content or ''}")
return out
if t == NodeType.TABLE:
return self._render_table(node)
if t == NodeType.METADATA:
return [] # 메타 노드는 frontmatter로 처리됨
# 그 외 노드는 텍스트 추출
return [node.text_content()]
def _render_table(self, table: CTNode) -> list[str]:
rows = list(table)
if not rows:
return []
out: list[str] = []
for i, row in enumerate(rows):
cells = [c.content or "" for c in row]
out.append("| " + " | ".join(cells) + " |")
if i == 0:
out.append("| " + " | ".join(["---"] * len(cells)) + " |")
return out