feat: slim JSON, EmojiMarkupRenderer, configurable delimiters
- 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>
This commit is contained in:
83
ctstruct/renderers/emoji_markup.py
Normal file
83
ctstruct/renderers/emoji_markup.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
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 + [""] # 목록 뒤 빈 줄
|
||||
Reference in New Issue
Block a user