Files
CTStruct/ctstruct/parsers/emoji_markup.py
gm daa590117d feat(parser): add EmojiMarkupParser for /🔹/🔸 format
-  → root title, 🔹 → SECTION(level=1), 🔸 → SECTION(level=2)
- (카테고리) 이름들 → LIST with category attribute + LIST_ITEM children
- 카테고리 없는 이름 줄은 빈 줄/새 섹션 전까지 동일 LIST로 묶음
- 22개 테스트 추가 (구조, 명단, 엣지케이스)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 18:14:04 +09:00

112 lines
3.7 KiB
Python

"""
ctstruct/parsers/emoji_markup.py
이모지 마크업 포맷 → CTTree 파서
지원 포맷:
✅제목 → 문서 제목 (root 속성)
🔹1단계 섹션 → SECTION (level=1)
🔸2단계 섹션 → SECTION (level=2)
(카테고리) 이름 → 카테고리 태그가 붙은 LIST + LIST_ITEM들
이름1 이름2 → 카테고리 없는 LIST + LIST_ITEM들 (줄 연속 가능)
"""
from __future__ import annotations
import re
from ..node import CTNode
from ..tree import CTTree
from ..types import NodeType
from .base import BaseParser
class EmojiMarkupParser(BaseParser):
"""
✅ / 🔹 / 🔸 이모지로 계층을 표현하는 텍스트 포맷 파서.
명단은 공백으로 구분된 이름 목록이며, 빈 줄 또는 새로운 섹션/카테고리
마커가 나올 때까지 같은 그룹으로 묶입니다.
Examples:
>>> parser = EmojiMarkupParser()
>>> tree = parser.parse(text)
>>> tree.pretty()
"""
_TITLE = ""
_SEC1 = "🔹"
_SEC2 = "🔸"
_RE_CAT = re.compile(r'^\(([^)]+)\)\s*(.*)')
def parse(self, text: str) -> CTTree:
tree = CTTree()
# 현재 컨텍스트 추적
sec1: CTNode | None = None
sec2: CTNode | None = None
# 아직 트리에 붙이지 않은 이름 버퍼
pending_names: list[str] = []
pending_category: str | None = None
pending_parent: CTNode | None = None # 이름 그룹이 붙을 부모 노드
def flush() -> None:
"""버퍼에 쌓인 이름을 LIST로 만들어 부모에 붙인다."""
nonlocal pending_names, pending_category, pending_parent
if not pending_names:
return
attrs = {"category": pending_category} if pending_category else {}
lst = CTNode(NodeType.LIST, attributes=attrs)
for name in pending_names:
lst.append(CTNode(NodeType.LIST_ITEM, name))
target = pending_parent if pending_parent is not None else tree.root
target.append(lst)
pending_names = []
pending_category = None
for raw in text.splitlines():
line = raw.strip()
if not line:
flush()
continue
# ✅ 문서 제목
if line.startswith(self._TITLE):
flush()
tree.root.set_attr("title", line[len(self._TITLE):].strip())
continue
# 🔹 1단계 섹션
if line.startswith(self._SEC1):
flush()
sec1 = CTNode(NodeType.SECTION, line[len(self._SEC1):].strip(), {"level": 1})
sec2 = None
pending_parent = sec1
tree.root.append(sec1)
continue
# 🔸 2단계 섹션
if line.startswith(self._SEC2):
flush()
sec2 = CTNode(NodeType.SECTION, line[len(self._SEC2):].strip(), {"level": 2})
pending_parent = sec2
parent = sec1 if sec1 is not None else tree.root
parent.append(sec2)
continue
# (카테고리) 이름들 — 새 카테고리이면 기존 버퍼를 먼저 flush
m = self._RE_CAT.match(line)
if m:
flush()
pending_category = m.group(1).strip()
rest = m.group(2).strip()
if rest:
pending_names.extend(rest.split())
continue
# 이름 목록 줄 (카테고리 없이 연속)
pending_names.extend(line.split())
flush()
return tree