diff --git a/resources/knowledge/original/_pipeline/pum_md_tool.py b/resources/knowledge/original/_pipeline/pum_md_tool.py index ed755f49..42b5d5f3 100644 --- a/resources/knowledge/original/_pipeline/pum_md_tool.py +++ b/resources/knowledge/original/_pipeline/pum_md_tool.py @@ -19,6 +19,7 @@ import difflib import hashlib import re import sys +import unicodedata from dataclasses import dataclass from pathlib import Path @@ -425,6 +426,17 @@ _TABLE_RULE = re.compile(r"^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$") #: 막음표 — 원문 글자가 md 문법으로 읽히지 않게 앞에 `\` 를 붙인 것(`\*` `\-` `\#` `\>` `\|` …) _ESCAPED = re.compile(r"\\([\\`*_{}\[\]()#+\-.!|>~^])") _HOLD = "\x00" +#: 글자층 밖 — PDF 글자층의 사용자 영역 글리프(U+E000–F8FF)는 뜻 없는 코드(식의 「(m³)」·분수선). +#: ①② 에서 빼고 개수·쪽을 기계검사 칸에 드러냄(③ 이 그림으로 봄). md 에서 그 자리를 받아쓴 글자는 +#: `` 로 감싸면 ①② 에서 빼고 「받아씀 N자」로 드러냄. +_PUA = re.compile("[\ue000-\uf8ff]") +_OFF_OPEN, _OFF_CLOSE = "\x01", "\x02" +_OFF_MARK = re.compile(r"") + + +def _same_han(ch: str) -> str: + """호환 한자(U+F900–FAFF) → 통합 한자(率 U+F9DB → 率 U+7387). ⚠ NFKC 통째로는 안 씀 — ㎥·①·㎡ 가 풀림.""" + return unicodedata.normalize("NFKC", ch) if "\uf900" <= ch <= "\ufaff" else ch def book_of(path: Path) -> dict: @@ -448,6 +460,7 @@ def md_lines(body: str) -> list[tuple[int, str]]: `` 는 셈 — 쪽을 넘은 표가 PDF 에서 되풀이한 머리 줄(md 표에서는 뺌 · 규칙 4-5). """ body = _REPEATED.sub(r"\1", body) + body = _OFF_MARK.sub(lambda m: _OFF_CLOSE if m.group(1) else _OFF_OPEN, body) body = _COMMENT.sub(lambda m: "\n" * m.group(0).count("\n"), body) out = [] for number, line in enumerate(body.split("\n"), start=1): @@ -490,12 +503,30 @@ def pdf_lines(doc, head: dict[str, str], footer: re.Pattern) -> list[tuple[int, return out +def _split_off(lines: list[tuple[int, str]]) -> tuple[list[tuple[int, str]], list[tuple[int, str]]]: + """(셀 줄, 글자층 밖 줄) — PDF 는 사용자 영역 글리프 · md 는 `글자층 밖` 으로 감싼 글자를 떼어 냄.""" + kept, off, inside = [], [], False + for at, line in lines: + keep, gone = [], [] + for ch in line: + if ch in (_OFF_OPEN, _OFF_CLOSE): + inside = ch == _OFF_OPEN + elif inside or _PUA.match(ch): + gone.append(ch) + else: + keep.append(ch) + kept.append((at, "".join(keep))) + if "".join(gone).strip(): + off.append((at, "".join(gone))) + return kept, off + + def _tight(lines: list[tuple[int, str]]) -> tuple[str, list[int]]: - """공백 뗀 글자 줄 + 글자마다 어디서 왔나(쪽 또는 md 줄).""" + """공백 뗀 글자 줄 + 글자마다 어디서 왔나(쪽 또는 md 줄) · 호환 한자는 통합 한자로.""" text, where = [], [] for at, line in lines: for ch in re.sub(r"\s", "", line): - text.append(ch) + text.append(_same_han(ch)) where.append(at) return "".join(text), where @@ -510,7 +541,9 @@ def _numbers(lines: list[tuple[int, str]]) -> tuple[collections.Counter, dict[st def compare(pdf: list[tuple[int, str]], md: list[tuple[int, str]]) -> dict: - """① 수(갯수까지) ② 글자(공백 뗌 · 갯수까지) — 양방향.""" + """① 수(갯수까지) ② 글자(공백 뗌 · 갯수까지) — 양방향. 글자층 밖(사용자 영역 글리프 · 감싼 받아쓰기)은 빼고 따로 셈.""" + pdf, pdf_off = _split_off(pdf) + md, md_off = _split_off(md) pdf_n, pdf_at = _numbers(pdf) md_n, md_at = _numbers(md) pdf_t, pdf_where = _tight(pdf) @@ -539,6 +572,11 @@ def compare(pdf: list[tuple[int, str]], md: list[tuple[int, str]]) -> dict: "char_missing": missing_c, "char_extra": extra_c, "spots": spots, + "off_pdf": ( + sum(len(re.sub(r"\s", "", t)) for _, t in pdf_off), + sorted({a for a, _ in pdf_off}), + ), + "off_md": sum(len(re.sub(r"\s", "", t)) for _, t in md_off), } @@ -550,9 +588,14 @@ def verdict(result: dict) -> str: sum(result["char_extra"].values()), ) word = "통과" if not any(counts) else "불통" - return ( + line = ( f"{word} · ①수 결손 {counts[0]} 허구 {counts[1]} · ②글자 빠짐 {counts[2]} 더함 {counts[3]}" ) + glyphs, pages = result.get("off_pdf", (0, [])) + if glyphs or result.get("off_md"): # ③ 이 그림으로 그 식을 꼭 보게 + where = f"({', '.join(map(str, pages))}쪽)" if pages else "" + line += f" · 글자층 밖 글리프 {glyphs}개{where} · 받아씀 {result.get('off_md', 0)}자" + return line def fingerprint(text: str) -> str: diff --git a/resources/knowledge/original/원가계산/_품셈md_작성규칙.md b/resources/knowledge/original/원가계산/_품셈md_작성규칙.md index 38320209..22dc2f45 100644 --- a/resources/knowledge/original/원가계산/_품셈md_작성규칙.md +++ b/resources/knowledge/original/원가계산/_품셈md_작성규칙.md @@ -81,6 +81,8 @@ PDF쪽: 135~136 | ③ 자리 대조 | **작성하지 않은 다른 창** | 쪽 그림을 보며 표마다 수가 제 줄·제 칸에 있는지 · 병합 · 머리 · 밑수 · [주] | - ①② 대조에서 빼는 것 — PDF 쪽: 쪽 맨 아래 꼬리(「- 51 -」 · 「30 공통부문」 · 「제1장 적용기준 31」) · 쪽 끝 이어짐 표지 「→」 / md 쪽: 머리 · `` · 그림 링크 · 표 문법(`|` · 구분 줄 · `
`) · 제목 `#` · 인용 `>` · 첨자 표시 `^` `_` (막음표 `\` 뒤 글자는 원문으로 셈). +- 글자층 밖 — PDF 글자층의 사용자 영역 글리프(U+E000–F8FF · 식의 「(m³)」·분수선)는 ①② 에서 빼고 기계검사 칸에 「글자층 밖 글리프 N개(쪽)」로 적음. 그 자리를 그림 보고 받아쓴 md 글자는 `(m^3)` 로 감쌈(①② 에서 빠지고 「받아씀 N자」로 드러남) — **이 칸이 뜬 파일은 ③ 이 그림으로 그 식을 꼭 봄.** 사용자 영역 글리프 자리 말고는 감싸지 않음. +- 호환 한자(U+F900–FAFF · 率 U+F9DB)는 ② 에서 통합 한자(率 U+7387)와 같게 봄. 다른 글자(㎥·①·㎡ 등)는 그대로 견줌. - ①②가 0 이어도 칸이 뒤바뀐 것은 못 잡음 → ③ 없이는 `확정` 아님. - ③에서 고칠 곳이 나오면 검증 창이 고치지 않고 **작성 창에 돌려보냄**(줄 번호 + PDF 쪽 + 무엇이 다른지). - `확정` 뒤 파일이 바뀌면 시험이 빨강. 고치려면 상태를 `작성중` 으로 되돌리고 ①②③ 다시. diff --git a/resources/tester/test_pum_md_tool.py b/resources/tester/test_pum_md_tool.py index c3e39efc..2cbbc60a 100644 --- a/resources/tester/test_pum_md_tool.py +++ b/resources/tester/test_pum_md_tool.py @@ -170,3 +170,16 @@ def test_repeated_head_comment_counts_and_footer_dropped(): ] ) assert tool.verdict(tool.compare(pdf, tool.md_lines(md))).startswith("통과") + + +def test_off_layer_glyphs_and_compat_han(): + """사용자 영역 글리프는 ①② 에서 빼고 드러냄 · md 는 감싼 받아쓰기를 뺌 · 호환 한자는 통합 한자로(NFKC 통째 아님).""" + pdf = [(21, "체적 V \ue001\ue002 = 1.5"), (21, "공극\uf9db ㎥ ①")] + md = tool.md_lines("체적 V (m^3) = 1.5\n공극率 ㎥ ①") + result = tool.compare(pdf, md) + line = tool.verdict(result) + assert line.startswith("통과"), line + assert "글자층 밖 글리프 2개(21쪽) · 받아씀 4자" in line # 「(m3)」 네 자 + # 감싸지 않으면 받아쓴 글자가 더함으로 뜸 + bare = tool.compare(pdf, tool.md_lines("체적 V (m^3) = 1.5\n공극率 ㎥ ①")) + assert tool.verdict(bare).startswith("불통")