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:
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