- JSONRenderer(slim=True): null·기본값·빈 attributes 필드 제거 - EmojiMarkupRenderer: CTTree → 이모지 마크업 텍스트 왕복 변환 지원 - title_mark / section_marks 커스터마이징 가능 - names_per_line 옵션으로 줄당 이름 수 제한 - EmojiMarkupParser: 구분자를 생성자 파라미터로 주입 가능 - section_marks 길이 제한 없음 (임의 깊이 섹션 지원) - 긴 마커 우선 매칭으로 prefix 충돌 방지 - 테스트 39개 추가 (커스텀 구분자·렌더러·slim 검증) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
84 lines
3.0 KiB
Python
84 lines
3.0 KiB
Python
"""
|
|
ctstruct/renderers/emoji_markup.py
|
|
CTTree → 이모지 마크업 텍스트 렌더러
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from ..node import CTNode
|
|
from ..tree import CTTree
|
|
from ..types import NodeType
|
|
from .base import BaseRenderer
|
|
|
|
|
|
class EmojiMarkupRenderer(BaseRenderer):
|
|
"""
|
|
CTTree 를 이모지 마크업 텍스트로 렌더링.
|
|
|
|
EmojiMarkupParser 와 같은 구분자를 사용하면 완전한 왕복 변환(roundtrip)이 보장됩니다.
|
|
|
|
Args:
|
|
title_mark: 문서 제목 앞에 붙는 마커 (기본 "✅")
|
|
section_marks: 섹션 레벨별 마커 목록 (기본 ["🔹", "🔸"])
|
|
인덱스 0 → level 1, 인덱스 1 → level 2, …
|
|
names_per_line: 한 줄에 출력할 최대 이름 수 (기본 0 = 제한 없음)
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
title_mark: str = "✅",
|
|
section_marks: list[str] | None = None,
|
|
names_per_line: int = 0,
|
|
) -> None:
|
|
self.title_mark = title_mark
|
|
self.section_marks = section_marks if section_marks is not None else ["🔹", "🔸"]
|
|
self.names_per_line = names_per_line
|
|
|
|
def render(self, tree: CTTree) -> str:
|
|
lines: list[str] = []
|
|
|
|
title = tree.root.attr("title")
|
|
if title:
|
|
lines.append(f"{self.title_mark}{title}")
|
|
lines.append("")
|
|
|
|
for child in tree.root.children:
|
|
lines.extend(self._render_node(child))
|
|
|
|
return "\n".join(lines).rstrip() + "\n"
|
|
|
|
# ── 내부 ─────────────────────────────────────────────────
|
|
|
|
def _render_node(self, node: CTNode) -> list[str]:
|
|
if node.node_type == NodeType.SECTION:
|
|
return self._render_section(node)
|
|
if node.node_type == NodeType.LIST:
|
|
return self._render_list(node)
|
|
return []
|
|
|
|
def _render_section(self, node: CTNode) -> list[str]:
|
|
level = node.attr("level", 1)
|
|
mark = self.section_marks[level - 1] if level <= len(self.section_marks) else ""
|
|
out = [f"{mark}{node.content or ''}"]
|
|
for child in node.children:
|
|
out.extend(self._render_node(child))
|
|
return out
|
|
|
|
def _render_list(self, node: CTNode) -> list[str]:
|
|
names = [item.content or "" for item in node.children
|
|
if item.node_type == NodeType.LIST_ITEM]
|
|
category = node.attr("category")
|
|
prefix = f"({category}) " if category else ""
|
|
|
|
if self.names_per_line > 0:
|
|
chunks = [names[i:i + self.names_per_line]
|
|
for i in range(0, len(names), self.names_per_line)]
|
|
lines = [prefix + " ".join(chunk) for chunk in chunks]
|
|
# prefix는 첫 줄에만
|
|
if len(lines) > 1:
|
|
lines = [lines[0]] + [" ".join(chunk) for chunk in chunks[1:]]
|
|
else:
|
|
lines = [prefix + " ".join(names)] if names else []
|
|
|
|
return lines + [""] # 목록 뒤 빈 줄
|