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>
This commit is contained in:
gm
2026-06-01 18:14:04 +09:00
parent fd98a4c705
commit daa590117d
3 changed files with 277 additions and 1 deletions

View File

@@ -0,0 +1,164 @@
"""
tests/test_emoji_markup_parser.py
EmojiMarkupParser 단위 테스트
"""
import pytest
from ctstruct import NodeType
from ctstruct.parsers import EmojiMarkupParser
SAMPLE = """\
✅예시 양식
🔹1번 항목
홍길동 홍길순 홍길영 홍길준
홍길호 홍길윤
🔹2번 항목
🔸2-1 항목
(건강) 홍길찬 홍길만
🔸2-2 항목
(직장) 홍길주
"""
@pytest.fixture
def tree():
return EmojiMarkupParser().parse(SAMPLE)
# ── 문서 제목 ─────────────────────────────────────────────────────────────────
class TestTitle:
def test_title_set(self, tree):
assert tree.root.attr("title") == "예시 양식"
# ── 섹션 구조 ─────────────────────────────────────────────────────────────────
class TestSections:
def test_two_top_sections(self, tree):
secs = [n for n in tree.root.children if n.node_type == NodeType.SECTION]
assert len(secs) == 2
def test_section1_name(self, tree):
sec = tree.root.children[0]
assert sec.content == "1번 항목"
assert sec.attr("level") == 1
def test_section2_name(self, tree):
sec = tree.root.children[1]
assert sec.content == "2번 항목"
assert sec.attr("level") == 1
def test_subsections_under_section2(self, tree):
sec2 = tree.root.children[1]
subsecs = [n for n in sec2.children if n.node_type == NodeType.SECTION]
assert len(subsecs) == 2
assert subsecs[0].content == "2-1 항목"
assert subsecs[1].content == "2-2 항목"
def test_subsection_level(self, tree):
sec2 = tree.root.children[1]
for sub in sec2.children:
assert sub.attr("level") == 2
# ── 명단 ─────────────────────────────────────────────────────────────────────
class TestNameLists:
def test_section1_has_one_list(self, tree):
sec1 = tree.root.children[0]
lists = [n for n in sec1.children if n.node_type == NodeType.LIST]
assert len(lists) == 1
def test_section1_names_across_lines(self, tree):
sec1 = tree.root.children[0]
lst = next(n for n in sec1.children if n.node_type == NodeType.LIST)
names = [item.content for item in lst.children]
assert names == ["홍길동", "홍길순", "홍길영", "홍길준", "홍길호", "홍길윤"]
def test_section1_no_category(self, tree):
sec1 = tree.root.children[0]
lst = next(n for n in sec1.children if n.node_type == NodeType.LIST)
assert lst.attr("category") is None
def test_subsec_21_category(self, tree):
sub21 = tree.root.children[1].children[0]
lst = next(n for n in sub21.children if n.node_type == NodeType.LIST)
assert lst.attr("category") == "건강"
def test_subsec_21_names(self, tree):
sub21 = tree.root.children[1].children[0]
lst = next(n for n in sub21.children if n.node_type == NodeType.LIST)
names = [item.content for item in lst.children]
assert names == ["홍길찬", "홍길만"]
def test_subsec_22_category(self, tree):
sub22 = tree.root.children[1].children[1]
lst = next(n for n in sub22.children if n.node_type == NodeType.LIST)
assert lst.attr("category") == "직장"
def test_subsec_22_names(self, tree):
sub22 = tree.root.children[1].children[1]
lst = next(n for n in sub22.children if n.node_type == NodeType.LIST)
names = [item.content for item in lst.children]
assert names == ["홍길주"]
def test_all_list_items_are_leaf(self, tree):
for item in tree.find(NodeType.LIST_ITEM):
assert item.is_leaf
# ── 전체 통계 ─────────────────────────────────────────────────────────────────
class TestStats:
def test_total_names(self, tree):
assert len(tree.find(NodeType.LIST_ITEM)) == 9
def test_total_lists(self, tree):
assert len(tree.find(NodeType.LIST)) == 3
def test_total_sections(self, tree):
secs = tree.find(NodeType.SECTION)
assert len(secs) == 4 # 🔹2 + 🔸2
# ── 엣지 케이스 ───────────────────────────────────────────────────────────────
class TestEdgeCases:
def test_empty_string(self):
tree = EmojiMarkupParser().parse("")
assert tree.root.attr("title") is None
assert len(tree.root.children) == 0
def test_title_only(self):
tree = EmojiMarkupParser().parse("✅제목만")
assert tree.root.attr("title") == "제목만"
assert len(tree.root.children) == 0
def test_multiple_categories_in_section(self):
text = "🔹항목\n(A) 홍길동\n\n(B) 홍길순\n"
tree = EmojiMarkupParser().parse(text)
sec = tree.root.children[0]
lists = [n for n in sec.children if n.node_type == NodeType.LIST]
assert len(lists) == 2
assert lists[0].attr("category") == "A"
assert lists[1].attr("category") == "B"
def test_names_without_section(self):
tree = EmojiMarkupParser().parse("홍길동 홍길순\n홍길영\n")
lst = tree.find(NodeType.LIST)
assert len(lst) == 1
assert len(lst[0].children) == 3
def test_category_names_on_separate_line(self):
# 카테고리 줄 다음에 이름이 없고 다음 줄에 이름이 있는 경우
text = "🔹항목\n(건강)\n홍길동 홍길순\n"
tree = EmojiMarkupParser().parse(text)
sec = tree.root.children[0]
lst = next(n for n in sec.children if n.node_type == NodeType.LIST)
assert lst.attr("category") == "건강"
assert len(lst.children) == 2