| """HTML export for visual inspection of MathVision-like records.""" |
|
|
| from __future__ import annotations |
|
|
| from html import escape |
| from pathlib import Path |
|
|
| from mathvision_explorer.dataset import MathVisionRecord |
|
|
|
|
| def export_html(records: list[MathVisionRecord], output: Path) -> None: |
| """Write a standalone HTML gallery for records.""" |
|
|
| output.parent.mkdir(parents=True, exist_ok=True) |
| cards = "\n".join(_render_card(record, output_dir=output.parent) for record in records) |
| html = f"""<!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1"> |
| <title>MathVision Explorer</title> |
| <style> |
| body {{ |
| margin: 0; |
| font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; |
| color: #172033; |
| background: #f7f8fb; |
| }} |
| header {{ |
| padding: 28px 32px 18px; |
| background: #ffffff; |
| border-bottom: 1px solid #dde3ee; |
| }} |
| h1 {{ |
| margin: 0 0 6px; |
| font-size: 28px; |
| font-weight: 750; |
| }} |
| main {{ |
| display: grid; |
| gap: 18px; |
| grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); |
| padding: 24px 32px 36px; |
| }} |
| article {{ |
| overflow: hidden; |
| border: 1px solid #d8deea; |
| border-radius: 8px; |
| background: #ffffff; |
| }} |
| img {{ |
| display: block; |
| width: 100%; |
| aspect-ratio: 3 / 2; |
| object-fit: contain; |
| background: #eef2f7; |
| }} |
| .body {{ |
| padding: 16px; |
| }} |
| .meta {{ |
| display: flex; |
| flex-wrap: wrap; |
| gap: 8px; |
| margin-bottom: 12px; |
| font-size: 13px; |
| }} |
| .tag {{ |
| padding: 3px 8px; |
| border: 1px solid #cfd7e6; |
| border-radius: 999px; |
| background: #f4f7fb; |
| }} |
| h2 {{ |
| margin: 0 0 12px; |
| font-size: 16px; |
| line-height: 1.35; |
| }} |
| p {{ |
| margin: 8px 0 0; |
| line-height: 1.45; |
| }} |
| .answer {{ |
| font-weight: 700; |
| }} |
| </style> |
| </head> |
| <body> |
| <header> |
| <h1>MathVision Explorer</h1> |
| <div>{len(records)} visual math records</div> |
| </header> |
| <main> |
| {cards} |
| </main> |
| </body> |
| </html> |
| """ |
| output.write_text(html, encoding="utf-8") |
|
|
|
|
| def _render_card(record: MathVisionRecord, *, output_dir: Path) -> str: |
| image_html = "" |
| if record.image_path is not None: |
| image_src = _relative_or_absolute_image(record.image_path, output_dir=output_dir) |
| image_html = f' <img src="{escape(image_src)}" alt="{escape(record.problem_id)}">\n' |
|
|
| meta = [_tag(record.problem_id)] |
| if record.subject is not None: |
| meta.append(_tag(record.subject)) |
| if record.level is not None: |
| meta.append(_tag(f"level {record.level}")) |
|
|
| options = "" |
| if record.options: |
| options = f"<p>Options: {escape(', '.join(record.options))}</p>" |
|
|
| solution = "" |
| if record.solution: |
| solution = f"<p>{escape(record.solution)}</p>" |
|
|
| return f""" <article> |
| {image_html} <div class="body"> |
| <div class="meta">{''.join(meta)}</div> |
| <h2>{escape(record.question)}</h2> |
| <p class="answer">Answer: {escape(record.answer)}</p> |
| {options} |
| {solution} |
| </div> |
| </article>""" |
|
|
|
|
| def _tag(value: str) -> str: |
| return f'<span class="tag">{escape(value)}</span>' |
|
|
|
|
| def _relative_or_absolute_image(image_path: Path, *, output_dir: Path) -> str: |
| try: |
| return image_path.resolve().relative_to(output_dir.resolve()).as_posix() |
| except ValueError: |
| return image_path.resolve().as_uri() |
|
|