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:
5
ctstruct/renderers/__init__.py
Normal file
5
ctstruct/renderers/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .base import BaseRenderer
|
||||
from .markdown import MarkdownRenderer
|
||||
from .json_renderer import JSONRenderer
|
||||
|
||||
__all__ = ["BaseRenderer", "MarkdownRenderer", "JSONRenderer"]
|
||||
BIN
ctstruct/renderers/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
ctstruct/renderers/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ctstruct/renderers/__pycache__/base.cpython-313.pyc
Normal file
BIN
ctstruct/renderers/__pycache__/base.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ctstruct/renderers/__pycache__/json_renderer.cpython-313.pyc
Normal file
BIN
ctstruct/renderers/__pycache__/json_renderer.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ctstruct/renderers/__pycache__/markdown.cpython-313.pyc
Normal file
BIN
ctstruct/renderers/__pycache__/markdown.cpython-313.pyc
Normal file
Binary file not shown.
31
ctstruct/renderers/base.py
Normal file
31
ctstruct/renderers/base.py
Normal 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)
|
||||
25
ctstruct/renderers/json_renderer.py
Normal file
25
ctstruct/renderers/json_renderer.py
Normal 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,
|
||||
)
|
||||
88
ctstruct/renderers/markdown.py
Normal file
88
ctstruct/renderers/markdown.py
Normal 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
|
||||
Reference in New Issue
Block a user