Merge remote-tracking branch 'origin/dev' into sub_laptop_2
This commit is contained in:
@@ -104,7 +104,16 @@ def sections(book: str, division: str = "", chapter: str = "", q: str = "", limi
|
||||
|
||||
|
||||
# ── 로직 원문(출처 글 → 품셈 본문 절) ──────────────────────────────────
|
||||
_NUMBER = re.compile(r"(?<![\d-])\d+(?:-\d+)+(?![\d-])")
|
||||
_NUMBER = re.compile(r"(?<![\d-])(\d+(?:-\d+)+)(?![\d-])(?:\s+(주[①-⑳]?|\d{1,2})(?![\d-]))?")
|
||||
_ESCAPE = re.compile(r"\\([.\-*_()#\[\]>~`])") # 마크다운 이스케이프 `\.` `\-` …
|
||||
_COMMENT = re.compile(r"<!--.*?-->") # 글 안 주석(글자층 표시) — 글자만 남김
|
||||
_CIRCLED = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳"
|
||||
|
||||
|
||||
def _plain(text: str) -> str:
|
||||
"""원문 md 기호 벗김 — 주석 · 이스케이프 · 인용 머리 `> `."""
|
||||
text = _ESCAPE.sub(r"\1", _COMMENT.sub("", text)).strip()
|
||||
return text[1:].strip() if text.startswith(">") else text
|
||||
|
||||
|
||||
def _blocks(lines: list[str]) -> list[dict]:
|
||||
@@ -117,10 +126,15 @@ def _blocks(lines: list[str]) -> list[dict]:
|
||||
if not text or text.startswith("<!--"):
|
||||
joinable = False
|
||||
continue
|
||||
if not text.startswith(("#", "|")):
|
||||
text = _plain(text)
|
||||
if not text:
|
||||
joinable = False
|
||||
continue
|
||||
if text.startswith("#"):
|
||||
out.append({"h": len(text) - len(text.lstrip("#")), "text": text.lstrip("#").strip()})
|
||||
elif text.startswith("|"):
|
||||
cells = [c.strip() for c in text.strip("|").split("|")]
|
||||
cells = [_plain(c) for c in text.strip("|").split("|")]
|
||||
if all(re.fullmatch(r":?-{3,}:?", c) for c in cells):
|
||||
continue # 머리 · 몸 가름 줄
|
||||
if not joinable:
|
||||
@@ -134,6 +148,44 @@ def _blocks(lines: list[str]) -> list[dict]:
|
||||
return out
|
||||
|
||||
|
||||
def _mark(blocks: list[dict], item: str) -> bool:
|
||||
"""출처 항 · 주 번호(「1」 · 「주③」 · 「주」)에 드는 조각에 hit 표시 — 못 찾으면 False(절 통째만)."""
|
||||
texts = [b.get("p") or b.get("text") or "" for b in blocks]
|
||||
if item.startswith("주"):
|
||||
note = next((i for i, t in enumerate(texts) if t.startswith("[주]")), None)
|
||||
if note is None:
|
||||
return False
|
||||
start, stop = note, len(blocks)
|
||||
if len(item) > 1: # 「주③」 — [주] 아래 ③ 줄 하나(다음 ① ~ ⑳ 줄 앞까지)
|
||||
start = next(
|
||||
(i for i in range(note + 1, len(texts)) if texts[i].startswith(item[1])), None
|
||||
)
|
||||
if start is None:
|
||||
return False
|
||||
stop = next(
|
||||
(i for i in range(start + 1, len(texts)) if texts[i][:1] in _CIRCLED), len(blocks)
|
||||
)
|
||||
else: # 「1」 — 「1. …」 제목 · 줄 ~ 다음 제목 · 「2. …」 앞(줄이면 [주] 앞도)
|
||||
head = re.compile(rf"{item}[.)]\s")
|
||||
start = next((i for i, t in enumerate(texts) if head.match(t)), None)
|
||||
if start is None:
|
||||
return False
|
||||
under = "h" in blocks[start]
|
||||
stop = next(
|
||||
(
|
||||
i
|
||||
for i in range(start + 1, len(blocks))
|
||||
if "h" in blocks[i]
|
||||
or (not under and re.match(r"\d+[.)]\s|\[주\]", texts[i]))
|
||||
or (under and re.match(r"\d+[.)]\s", texts[i]) and "p" in blocks[i])
|
||||
),
|
||||
len(blocks),
|
||||
)
|
||||
for b in blocks[start:stop]:
|
||||
b["hit"] = True
|
||||
return True
|
||||
|
||||
|
||||
def _section(book: str, division: str, number: str) -> dict | None:
|
||||
"""번호 절을 품셈 본문에서 찾음 — 없으면 끝 마디를 하나씩 떼며 위 절로(두 마디까지)."""
|
||||
parts = number.split("-")
|
||||
@@ -178,8 +230,14 @@ def source(src: str) -> dict:
|
||||
if book == "건설품셈"
|
||||
else ""
|
||||
)
|
||||
found = [_section(book, division, n) for n in dict.fromkeys(_NUMBER.findall(src))]
|
||||
return {"source": src, "found": [x for x in found if x]}
|
||||
found = []
|
||||
for number, item in dict.fromkeys(_NUMBER.findall(src)):
|
||||
one = _section(book, division, number)
|
||||
if one:
|
||||
if item and _mark(one["blocks"], item):
|
||||
one["item"] = item
|
||||
found.append(one)
|
||||
return {"source": src, "found": found}
|
||||
|
||||
|
||||
# ── 표 조건 값 ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -55,3 +55,9 @@
|
||||
.m01lab-src__table td {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* 출처가 가리킨 항 · 주 — 옅은 바탕 + 왼쪽 막대 */
|
||||
.m01lab-src__hit {
|
||||
background: color-mix(in srgb, var(--color-accent) 14%, transparent);
|
||||
box-shadow: -3px 0 0 var(--color-accent);
|
||||
}
|
||||
|
||||
@@ -26,15 +26,24 @@ interface Block {
|
||||
text?: string;
|
||||
p?: string;
|
||||
table?: string[][];
|
||||
/** 출처가 가리킨 항 · 주 — 강조 */
|
||||
hit?: boolean;
|
||||
}
|
||||
interface Found {
|
||||
number: string;
|
||||
matched: string;
|
||||
file: string;
|
||||
blocks: Block[];
|
||||
item?: string;
|
||||
}
|
||||
|
||||
function blockNode(b: Block): HTMLElement {
|
||||
const node = rawNode(b);
|
||||
if (b.hit) node.classList.add("m01lab-src__hit");
|
||||
return node;
|
||||
}
|
||||
|
||||
function rawNode(b: Block): HTMLElement {
|
||||
if (b.table)
|
||||
return el("div", {
|
||||
className: "m01-logic__scroll",
|
||||
@@ -89,11 +98,20 @@ export function createSourceView(): SourceView {
|
||||
...found.flatMap((f) => [
|
||||
el("p", {
|
||||
className: "m01-logic__muted m01lab-src__file",
|
||||
text: f.matched === f.number ? f.file : `${f.file} · ${ts("Near", f.number)}`,
|
||||
text: [
|
||||
f.file,
|
||||
f.matched === f.number ? "" : ts("Near", f.number),
|
||||
f.item ? (/^\d+$/.test(f.item) ? `${f.item}항` : f.item) : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
}),
|
||||
...f.blocks.map(blockNode),
|
||||
]),
|
||||
);
|
||||
requestAnimationFrame(() =>
|
||||
body.querySelector(".m01lab-src__hit")?.scrollIntoView({ block: "center" }),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user