Spaces:
Running
Running
| """Glossary tooltips for hard legal-grading terms in PASS/FAIL questions.""" | |
| from __future__ import annotations | |
| import html | |
| import re | |
| from typing import Iterable | |
| # Longest-first patterns so nested phrases (e.g. prevailing … containing | |
| # "sub-jurisdictional") get a single, more specific tip. | |
| _GLOSSARY_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = ( | |
| ( | |
| re.compile(r"minority or divergent sub-jurisdictional legal treatment", re.I), | |
| ( | |
| "A recognized alternative legal treatment followed by one or more " | |
| "sub-jurisdictions that differs meaningfully from the prevailing treatment." | |
| ), | |
| ), | |
| ( | |
| re.compile(r"prevailing sub-jurisdictional legal treatment", re.I), | |
| ( | |
| "The treatment that can fairly be described as the dominant, usual, or " | |
| "majority legal position across the relevant sub-jurisdictions. It need " | |
| "not be literally universal." | |
| ), | |
| ), | |
| ( | |
| re.compile( | |
| r"require qualification of an otherwise jurisdiction-wide description", | |
| re.I, | |
| ), | |
| ( | |
| "A single unqualified jurisdiction-wide statement would risk being " | |
| "inaccurate or misleading, so the answer should signal the important " | |
| "variation." | |
| ), | |
| ), | |
| ( | |
| re.compile(r"ordinary field of legal operation", re.I), | |
| ( | |
| "The normal situations in which the concept operates. This is different " | |
| "from exceptional cases, competing rules, or genuine edge cases, which " | |
| "belong in a Boundary Conditions block." | |
| ), | |
| ), | |
| ( | |
| re.compile(r"ordinary field of operation", re.I), | |
| ( | |
| "The normal situations in which the concept operates. This is different " | |
| "from exceptional cases, competing rules, or genuine edge cases, which " | |
| "belong in a Boundary Conditions block." | |
| ), | |
| ), | |
| ( | |
| re.compile(r"ordinary application", re.I), | |
| ( | |
| "The normal situations in which the concept operates. This is different " | |
| "from exceptional cases, competing rules, or genuine edge cases, which " | |
| "belong in a Boundary Conditions block." | |
| ), | |
| ), | |
| ( | |
| re.compile(r"boundary situations?", re.I), | |
| ( | |
| "A recognized situation at or near the legal edge of the concept's " | |
| "ordinary operation: an exception, limiting principle, competing rule, " | |
| "or genuine area of legal uncertainty. A simple case in which a basic " | |
| "prerequisite is plainly missing is not necessarily a meaningful " | |
| "boundary situation." | |
| ), | |
| ), | |
| ( | |
| re.compile(r"boundary circumstances?", re.I), | |
| ( | |
| "A recognized situation at or near the legal edge of the concept's " | |
| "ordinary operation: an exception, limiting principle, competing rule, " | |
| "or genuine area of legal uncertainty. A simple case in which a basic " | |
| "prerequisite is plainly missing is not necessarily a meaningful " | |
| "boundary situation." | |
| ), | |
| ), | |
| ( | |
| re.compile(r"substantially uniform", re.I), | |
| ( | |
| "The concept can still be described as materially the same across the " | |
| "jurisdiction despite some recognized local differences." | |
| ), | |
| ), | |
| ( | |
| re.compile(r"sub-jurisdictional", re.I), | |
| ( | |
| "A legally relevant constituent jurisdiction within the jurisdiction " | |
| "being analyzed—for example, a state, province, Land, canton, or other " | |
| "constituent legal unit." | |
| ), | |
| ), | |
| ( | |
| re.compile(r"sub-jurisdictions?", re.I), | |
| ( | |
| "A legally relevant constituent jurisdiction within the jurisdiction " | |
| "being analyzed—for example, a state, province, Land, canton, or other " | |
| "constituent legal unit." | |
| ), | |
| ), | |
| ) | |
| def _parse_emphasis(question_md: str) -> tuple[str, list[tuple[int, int]]]: | |
| """Expand ``:blue[...]`` markers into plain text + emphasis spans.""" | |
| cleaned = question_md | |
| while ":blue[:blue[" in cleaned: | |
| cleaned = re.sub(r":blue\[:blue\[(.*?)\]\s*\]", r":blue[\1]", cleaned) | |
| plain_chars: list[str] = [] | |
| emphasis: list[tuple[int, int]] = [] | |
| pos = 0 | |
| for match in re.finditer(r":blue\[(.*?)\]", cleaned): | |
| plain_chars.extend(cleaned[pos : match.start()]) | |
| inner = match.group(1).strip() | |
| start = len(plain_chars) | |
| plain_chars.extend(inner) | |
| emphasis.append((start, start + len(inner))) | |
| pos = match.end() | |
| plain_chars.extend(cleaned[pos:]) | |
| return "".join(plain_chars), emphasis | |
| def _glossary_spans(plain: str) -> list[tuple[int, int, str]]: | |
| """Non-overlapping glossary matches on plain question text.""" | |
| occupied = [False] * len(plain) | |
| spans: list[tuple[int, int, str]] = [] | |
| for pattern, tip in _GLOSSARY_PATTERNS: | |
| for match in pattern.finditer(plain): | |
| start, end = match.start(), match.end() | |
| if any(occupied[start:end]): | |
| continue | |
| for i in range(start, end): | |
| occupied[i] = True | |
| spans.append((start, end, tip)) | |
| spans.sort(key=lambda item: item[0]) | |
| return spans | |
| def _emphasized(index: int, emphasis: Iterable[tuple[int, int]]) -> bool: | |
| return any(start <= index < end for start, end in emphasis) | |
| def _tip_icon(tip: str) -> str: | |
| return ( | |
| '<span class="glossary-tip" aria-label="Glossary">' | |
| "?" | |
| f'<span class="glossary-bubble">{html.escape(tip)}</span>' | |
| "</span>" | |
| ) | |
| def format_question_html(question_md: str) -> str: | |
| """Render a question with bold emphasis spans and glossary tip icons.""" | |
| plain, emphasis = _parse_emphasis(question_md) | |
| tips = _glossary_spans(plain) | |
| parts: list[str] = [] | |
| # Icon sits at the start of the term (top-left), not after it. | |
| tip_at = {start: tip for start, _, tip in tips} | |
| i = 0 | |
| after_tag = False | |
| while i < len(plain): | |
| if i in tip_at: | |
| parts.append(_tip_icon(tip_at[i])) | |
| after_tag = True | |
| if _emphasized(i, emphasis): | |
| j = i + 1 | |
| while j < len(plain) and _emphasized(j, emphasis): | |
| j += 1 | |
| parts.append(f"<strong>{html.escape(plain[i:j])}</strong>") | |
| i = j | |
| after_tag = True | |
| continue | |
| ch = plain[i] | |
| # Streamlit's HTML markdown path drops ordinary spaces right after | |
| # closing tags, which glues "identify"+"the" into "identifythe". | |
| if after_tag and ch == " ": | |
| parts.append(" ") | |
| else: | |
| parts.append(html.escape(ch)) | |
| i += 1 | |
| after_tag = False | |
| body = "".join(parts) | |
| return ( | |
| "<div class='grading-question' " | |
| "style='font-size:1.15rem;font-weight:400;line-height:1.45;" | |
| f"margin:0.25rem 0 0'>{body}</div>" | |
| ) | |
| GLOSSARY_TIP_CSS = """ | |
| .glossary-tip { | |
| position: relative; | |
| display: inline-block; | |
| margin: 0 0.12rem 0 0.02rem; | |
| padding: 0 0.18rem; | |
| border: 1px solid #9a9a9a; | |
| border-radius: 999px; | |
| font-size: 0.55rem; | |
| font-weight: 600; | |
| line-height: 1.1; | |
| color: #666; | |
| vertical-align: 0.55em; | |
| cursor: help; | |
| } | |
| .glossary-tip .glossary-bubble { | |
| visibility: hidden; | |
| opacity: 0; | |
| position: absolute; | |
| left: 0; | |
| bottom: calc(100% + 0.35rem); | |
| z-index: 1000; | |
| width: max-content; | |
| max-width: min(24rem, 70vw); | |
| padding: 0.85rem 1rem; | |
| border-radius: 0.45rem; | |
| background: #1f1f1f; | |
| color: #f5f5f5; | |
| font-size: 0.95rem; | |
| font-weight: 400; | |
| line-height: 1.45; | |
| text-align: left; | |
| white-space: normal; | |
| box-shadow: 0 4px 14px rgba(0, 0, 0, 0.22); | |
| pointer-events: none; | |
| transition: none; | |
| } | |
| .glossary-tip:hover .glossary-bubble { | |
| visibility: visible; | |
| opacity: 1; | |
| } | |
| """ | |