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