Files
jsahn 4e943700fc feat: add common utilities and feedback handling scripts
- Introduced common.js for shared functions including API calls, error reporting, and UI helpers.
- Implemented feedback.js for the feedback reporting page, including form handling and displaying feedback status.
- Added layout.js for managing galaxy and section layouts in the visualization.
- Created login.js for user authentication with session management.
- Developed search.js for term searching based on user input and synonyms.
- Implemented signup.js for user registration with validation and immediate login.
2026-10-06 23:14:45 +09:00

140 lines
5.2 KiB
Python

"""목차 페이지에서 장·절·항 트리를 추출."""
import hashlib
import re
import pymupdf
from .pdfutil import page_lines
LEADER = re.compile(r"(?:j{3,}|[·․\.…]{3,}|·\s?·)[\sj·․\.…]*")
TRAIL_PAGE = re.compile(r"(?:_|\s|^)(\d{1,3})$")
ROMAN = "ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩ"
LEVELS = {"chapter": 1, "part": 2, "section": 3, "item": 4, "subitem": 5}
def _split_page(text: str):
t = LEADER.sub(" ", text).strip()
m = TRAIL_PAGE.search(t)
if m and (m.start() > 0 or t.startswith("_")):
return t[: m.start()].strip(" _"), int(m.group(1))
return t, None
def _classify(style: str, text: str, size: float):
"""(kind, number, title) 또는 None."""
if style == "cn2026":
for kind, pat in (
("chapter", r"^(\d+)장\s*(.+)$"),
("section", r"^(\d+)절\s*(.+)$"),
("item", r"^(\d+)\.\s*(.+)$"),
("subitem", r"^(\d+)\)\s*(.+)$"),
):
m = re.match(pat, text)
if m:
return kind, m.group(1), m.group(2).strip()
return None
if style == "pacific":
m = re.match(r"^제(\d+)장\s*(.*)$", text)
if m:
return "chapter", m.group(1), m.group(2).strip()
m = re.match(rf"^([{ROMAN}])\.?\s*(.*)$", text)
if m:
return "part", str(ROMAN.index(m.group(1)) + 1), m.group(2).strip()
for kind, pat in (
("section", r"^제(\d+)절\s*(.+)$"),
("item", r"^(\d+)\.\s*(.+)$"),
("subitem", r"^(\d+)\)\s*(.+)$"),
):
m = re.match(pat, text)
if m:
return kind, m.group(1), m.group(2).strip()
return None
if style == "healthedu":
m = re.match(r"^(\d+)\.\s*(.+)$", text)
if m and size >= 8.7:
return "chapter", m.group(1), re.sub(r"(?<=[가-힣]) (?=[가-힣]$)", "", m.group(2).strip())
m = re.match(r"^\((\d+)\)\s*(.+)$", text)
if m:
return "subitem", m.group(1), m.group(2).strip()
m = re.match(r"^(\d+)\s+(.+)$", text)
if m and size < 8.2:
return "section", m.group(1), m.group(2).strip()
return None
raise ValueError(style)
def extract_toc(doc: pymupdf.Document, book: dict) -> list[dict]:
style = book["toc_style"]
nodes: list[dict] = []
stack: list[dict] = []
pending_title_for = None # 제목이 다음 줄에 오는 장/부
seq = 0
for pi in book["toc_pages"]:
for ln in page_lines(doc, pi):
if ln.size >= 16 or ln.y < 60: # 쪽 제목·머리말
continue
text, page = _split_page(ln.text)
if not text:
continue
if text in ("Contents", "목차", "w"):
continue
c = _classify(style, text, ln.size)
if c is None:
if pending_title_for is not None and not pending_title_for["title"]:
pending_title_for["title"] = text
if page is not None and pending_title_for["page"] is None:
pending_title_for["page"] = page
pending_title_for = None
elif nodes and nodes[-1]["page"] is None:
nodes[-1]["title"] = (nodes[-1]["title"] + " " + text).strip()
if page is not None:
nodes[-1]["page"] = page
continue
kind, num, title = c
level = LEVELS[kind]
while stack and stack[-1]["level"] >= level:
stack.pop()
seq += 1
node = {
# 공개 화면 주소에 교재가 드러나지 않도록 해시 ID
"id": "s_" + hashlib.sha1(f"{book['book_id']}|{seq}".encode()).hexdigest()[:10],
"book_id": book["book_id"],
"kind": kind,
"level": level,
"number": num,
"title": title,
"page": page,
"parent_id": stack[-1]["id"] if stack else None,
}
# 같은 장이 목차 여러 쪽에 걸쳐 반복 표기된 경우 건너뜀
same = [n for n in nodes if n["kind"] == "chapter" and n["number"] == num
and (not title or not n["title"] or n["title"] == title)]
if kind == "chapter" and same:
seq -= 1
stack = [same[0]]
continue
nodes.append(node)
stack.append(node)
pending_title_for = node if not title else None
_fill_pages(nodes)
return nodes
def _fill_pages(nodes: list[dict]) -> None:
"""페이지가 없는 노드는 첫 자식의 페이지를 쓰고, 끝 페이지를 계산."""
by_parent: dict = {}
for n in nodes:
by_parent.setdefault(n["parent_id"], []).append(n)
for n in reversed(nodes):
if n["page"] is None:
kids = [k for k in by_parent.get(n["id"], []) if k["page"] is not None]
if kids:
n["page"] = kids[0]["page"]
# 페이지 역행(목차 파싱 오류)은 직전 값으로 보정
last = 0
for n in nodes:
if n["page"] is None or n["page"] < last - 50:
n["page"] = last
last = max(last, n["page"])