feat: initial CTStruct framework
- CTNode: 트리 기본 단위 (전위/후위/BFS 순회, 탐색, 분리) - CTTree: 트리 컨테이너 (빌더 API, TOC, filter/map/merge) - NodeType: 구조/블록/인라인/메타 노드 타입 Enum - MarkdownParser: Markdown → CTTree 파서 - MarkdownRenderer: CTTree → Markdown 렌더러 - JSONRenderer: CTTree → JSON 직렬화 - 27개 단위/통합 테스트 전체 통과
This commit is contained in:
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
BIN
tests/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
tests/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_node.cpython-313-pytest-9.0.3.pyc
Normal file
BIN
tests/__pycache__/test_node.cpython-313-pytest-9.0.3.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_parsers.cpython-313-pytest-9.0.3.pyc
Normal file
BIN
tests/__pycache__/test_parsers.cpython-313-pytest-9.0.3.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_tree.cpython-313-pytest-9.0.3.pyc
Normal file
BIN
tests/__pycache__/test_tree.cpython-313-pytest-9.0.3.pyc
Normal file
Binary file not shown.
93
tests/test_node.py
Normal file
93
tests/test_node.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
tests/test_node.py
|
||||
CTNode 단위 테스트
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from ctstruct import CTNode, NodeType
|
||||
|
||||
|
||||
class TestCTNodeBasic:
|
||||
def test_create_node(self):
|
||||
n = CTNode(NodeType.PARAGRAPH, "hello")
|
||||
assert n.node_type == NodeType.PARAGRAPH
|
||||
assert n.content == "hello"
|
||||
assert n.is_leaf
|
||||
assert n.is_root
|
||||
|
||||
def test_append_child(self):
|
||||
parent = CTNode(NodeType.SECTION)
|
||||
child = CTNode(NodeType.PARAGRAPH, "child")
|
||||
parent.append(child)
|
||||
assert len(parent) == 1
|
||||
assert child.parent is parent
|
||||
assert not parent.is_leaf
|
||||
|
||||
def test_depth(self):
|
||||
root = CTNode(NodeType.ROOT)
|
||||
sec = CTNode(NodeType.SECTION)
|
||||
para = CTNode(NodeType.PARAGRAPH, "text")
|
||||
root.append(sec)
|
||||
sec.append(para)
|
||||
assert root.depth == 0
|
||||
assert sec.depth == 1
|
||||
assert para.depth == 2
|
||||
|
||||
def test_detach(self):
|
||||
parent = CTNode(NodeType.SECTION)
|
||||
child = CTNode(NodeType.PARAGRAPH, "bye")
|
||||
parent.append(child)
|
||||
child.detach()
|
||||
assert len(parent) == 0
|
||||
assert child.parent is None
|
||||
|
||||
def test_walk_pre(self):
|
||||
root = CTNode(NodeType.ROOT)
|
||||
a = CTNode(NodeType.SECTION)
|
||||
b = CTNode(NodeType.PARAGRAPH, "b")
|
||||
root.append(a)
|
||||
a.append(b)
|
||||
nodes = list(root.walk("pre"))
|
||||
assert nodes[0] is root
|
||||
assert nodes[1] is a
|
||||
assert nodes[2] is b
|
||||
|
||||
def test_walk_bfs(self):
|
||||
root = CTNode(NodeType.ROOT)
|
||||
a = CTNode(NodeType.SECTION)
|
||||
b = CTNode(NodeType.PARAGRAPH, "b")
|
||||
c = CTNode(NodeType.PARAGRAPH, "c")
|
||||
root.append(a)
|
||||
root.append(c)
|
||||
a.append(b)
|
||||
bfs = list(root.walk("bfs"))
|
||||
assert bfs[0] is root
|
||||
# level 1: a, c
|
||||
assert bfs[1] is a
|
||||
assert bfs[2] is c
|
||||
# level 2: b
|
||||
assert bfs[3] is b
|
||||
|
||||
def test_find(self):
|
||||
root = CTNode(NodeType.ROOT)
|
||||
root.append(CTNode(NodeType.HEADING, "h1", {"level": 1}))
|
||||
root.append(CTNode(NodeType.PARAGRAPH, "para"))
|
||||
root.append(CTNode(NodeType.HEADING, "h2", {"level": 2}))
|
||||
headings = root.find(NodeType.HEADING)
|
||||
assert len(headings) == 2
|
||||
|
||||
def test_text_content(self):
|
||||
root = CTNode(NodeType.ROOT)
|
||||
root.append(CTNode(NodeType.PARAGRAPH, "hello "))
|
||||
root.append(CTNode(NodeType.PARAGRAPH, "world"))
|
||||
assert root.text_content() == "hello world"
|
||||
|
||||
def test_siblings(self):
|
||||
parent = CTNode(NodeType.SECTION)
|
||||
a = CTNode(NodeType.PARAGRAPH, "a")
|
||||
b = CTNode(NodeType.PARAGRAPH, "b")
|
||||
c = CTNode(NodeType.PARAGRAPH, "c")
|
||||
parent.append(a)
|
||||
parent.append(b)
|
||||
parent.append(c)
|
||||
assert b.siblings == [a, c]
|
||||
104
tests/test_parsers.py
Normal file
104
tests/test_parsers.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
tests/test_parsers.py
|
||||
파서 단위 테스트
|
||||
"""
|
||||
|
||||
from ctstruct import NodeType
|
||||
from ctstruct.parsers import MarkdownParser
|
||||
from ctstruct.renderers import MarkdownRenderer, JSONRenderer
|
||||
|
||||
|
||||
SAMPLE_MD = """\
|
||||
---
|
||||
title: 테스트 문서
|
||||
author: gm
|
||||
---
|
||||
|
||||
# 소개
|
||||
|
||||
이 문서는 CTStruct 테스트용 문서입니다.
|
||||
|
||||
## 기능 목록
|
||||
|
||||
- 트리 자료구조
|
||||
- 마크다운 파서
|
||||
- 다양한 렌더러
|
||||
|
||||
## 코드 예제
|
||||
|
||||
```python
|
||||
tree = CTTree()
|
||||
tree.add(NodeType.PARAGRAPH, "hello")
|
||||
```
|
||||
|
||||
> 이것은 인용구입니다.
|
||||
|
||||
---
|
||||
|
||||
# 결론
|
||||
|
||||
CTStruct 는 텍스트를 구조화합니다.
|
||||
"""
|
||||
|
||||
|
||||
class TestMarkdownParser:
|
||||
def setup_method(self):
|
||||
self.parser = MarkdownParser()
|
||||
self.tree = self.parser.parse(SAMPLE_MD)
|
||||
|
||||
def test_parse_returns_tree(self):
|
||||
assert self.tree is not None
|
||||
|
||||
def test_headings_parsed(self):
|
||||
headings = self.tree.find(NodeType.HEADING)
|
||||
assert len(headings) >= 3
|
||||
|
||||
def test_paragraph_parsed(self):
|
||||
paras = self.tree.find(NodeType.PARAGRAPH)
|
||||
assert len(paras) >= 1
|
||||
|
||||
def test_list_parsed(self):
|
||||
lists = self.tree.find(NodeType.LIST)
|
||||
assert len(lists) >= 1
|
||||
items = self.tree.find(NodeType.LIST_ITEM)
|
||||
assert len(items) == 3
|
||||
|
||||
def test_code_block_parsed(self):
|
||||
codes = self.tree.find(NodeType.CODE_BLOCK)
|
||||
assert len(codes) == 1
|
||||
assert codes[0].attr("language") == "python"
|
||||
|
||||
def test_blockquote_parsed(self):
|
||||
quotes = self.tree.find(NodeType.BLOCKQUOTE)
|
||||
assert len(quotes) == 1
|
||||
|
||||
def test_divider_parsed(self):
|
||||
dividers = self.tree.find(NodeType.DIVIDER)
|
||||
assert len(dividers) >= 1
|
||||
|
||||
def test_toc(self):
|
||||
toc = self.tree.toc()
|
||||
assert toc[0]["text"] == "소개"
|
||||
|
||||
|
||||
class TestMarkdownRenderer:
|
||||
def test_roundtrip(self):
|
||||
parser = MarkdownParser()
|
||||
renderer = MarkdownRenderer()
|
||||
tree = parser.parse(SAMPLE_MD)
|
||||
output = renderer.render(tree)
|
||||
# 재파싱 후 헤딩 수 일치 확인
|
||||
tree2 = parser.parse(output)
|
||||
assert len(tree.find(NodeType.HEADING)) == len(tree2.find(NodeType.HEADING))
|
||||
|
||||
|
||||
class TestJSONRenderer:
|
||||
def test_json_output(self):
|
||||
import json
|
||||
parser = MarkdownParser()
|
||||
renderer = JSONRenderer()
|
||||
tree = parser.parse(SAMPLE_MD)
|
||||
output = renderer.render(tree)
|
||||
data = json.loads(output)
|
||||
assert "type" in data
|
||||
assert data["type"] == "ROOT"
|
||||
70
tests/test_tree.py
Normal file
70
tests/test_tree.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
tests/test_tree.py
|
||||
CTTree 단위 테스트
|
||||
"""
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from ctstruct import CTTree, CTNode, NodeType
|
||||
|
||||
|
||||
class TestCTTree:
|
||||
def test_add_node(self):
|
||||
tree = CTTree("My Doc")
|
||||
tree.add(NodeType.PARAGRAPH, "hello")
|
||||
assert len(tree) == 1
|
||||
|
||||
def test_make_list(self):
|
||||
lst = CTTree.make_list(["A", "B", "C"])
|
||||
assert lst.node_type == NodeType.LIST
|
||||
assert len(lst) == 3
|
||||
assert lst[0].content == "A"
|
||||
|
||||
def test_make_table(self):
|
||||
table = CTTree.make_table(
|
||||
headers=["이름", "나이"],
|
||||
rows=[["Alice", "30"], ["Bob", "25"]],
|
||||
)
|
||||
assert table.node_type == NodeType.TABLE
|
||||
assert len(table) == 3 # 헤더행 + 2행
|
||||
|
||||
def test_toc(self):
|
||||
tree = CTTree()
|
||||
tree.add(NodeType.HEADING, "서론", level=1)
|
||||
tree.add(NodeType.HEADING, "본론", level=2)
|
||||
tree.add(NodeType.HEADING, "결론", level=1)
|
||||
toc = tree.toc()
|
||||
assert len(toc) == 3
|
||||
assert toc[0]["text"] == "서론"
|
||||
|
||||
def test_to_dict_and_back(self):
|
||||
tree = CTTree("Test")
|
||||
tree.add(NodeType.PARAGRAPH, "테스트 문단")
|
||||
d = tree.to_dict()
|
||||
restored = CTTree.from_dict(d)
|
||||
paras = restored.find(NodeType.PARAGRAPH)
|
||||
assert len(paras) == 1
|
||||
assert paras[0].content == "테스트 문단"
|
||||
|
||||
def test_filter(self):
|
||||
tree = CTTree()
|
||||
tree.add(NodeType.HEADING, "H", level=1)
|
||||
tree.add(NodeType.PARAGRAPH, "P")
|
||||
filtered = tree.filter(lambda n: n.node_type != NodeType.PARAGRAPH)
|
||||
assert len(filtered.find(NodeType.PARAGRAPH)) == 0
|
||||
assert len(filtered.find(NodeType.HEADING)) == 1
|
||||
|
||||
def test_merge(self):
|
||||
t1 = CTTree()
|
||||
t1.add(NodeType.PARAGRAPH, "t1")
|
||||
t2 = CTTree()
|
||||
t2.add(NodeType.PARAGRAPH, "t2")
|
||||
t1.merge(t2)
|
||||
assert len(t1.find(NodeType.PARAGRAPH)) == 2
|
||||
|
||||
def test_node_count(self):
|
||||
tree = CTTree()
|
||||
tree.add(NodeType.PARAGRAPH, "a")
|
||||
tree.add(NodeType.PARAGRAPH, "b")
|
||||
# root(1) + 2 paragraphs
|
||||
assert tree.node_count == 3
|
||||
Reference in New Issue
Block a user