case-forge / core /export.py
nextmarte's picture
Fix py3.10 f-string backslash in export
d5999d0 verified
Raw
History Blame Contribute Delete
4.48 kB
"""Export a forged case+note to downloadable files.
Per the MVP cut-lines, PDF is optional and Markdown is the floor. We ship two
zero-dependency formats: a clean **Markdown** file (the canonical artifact) and a
self-contained **printable HTML** the instructor can open and Print-to-PDF —
covering the PDF use case without a PDF toolchain in the Space.
"""
from __future__ import annotations
import re
import tempfile
from pathlib import Path
def _slug(title: str) -> str:
s = re.sub(r"[^\w\s-]", "", (title or "case").lower()).strip()
s = re.sub(r"[\s_-]+", "-", s)
return (s or "case")[:60]
def _combined_markdown(case_md: str, note_md: str) -> str:
return f"{(case_md or '').strip()}\n\n---\n\n{(note_md or '').strip()}\n"
def _md_inline(text: str) -> str:
"""Minimal inline Markdown → HTML: **bold** and escaping."""
import html
text = html.escape(text)
return re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
def _md_to_html_body(md: str) -> str:
"""Tiny Markdown→HTML for our own controlled output (headings, lists, quotes)."""
lines = md.splitlines()
html_lines: list[str] = []
list_type: str | None = None # 'ul' | 'ol'
def close_list():
nonlocal list_type
if list_type:
html_lines.append(f"</{list_type}>")
list_type = None
for raw in lines:
line = raw.rstrip()
if not line.strip():
close_list()
continue
if line.startswith("# "):
close_list(); html_lines.append(f"<h1>{_md_inline(line[2:])}</h1>")
elif line.startswith("## "):
close_list(); html_lines.append(f"<h2>{_md_inline(line[3:])}</h2>")
elif line.startswith("> "):
close_list(); html_lines.append(f"<blockquote>{_md_inline(line[2:])}</blockquote>")
elif line.strip() == "---":
close_list(); html_lines.append("<hr>")
elif line.startswith("- "):
if list_type != "ul":
close_list(); html_lines.append("<ul>"); list_type = "ul"
html_lines.append(f"<li>{_md_inline(line[2:])}</li>")
elif re.match(r"^\d+\.\s", line):
if list_type != "ol":
close_list(); html_lines.append("<ol>"); list_type = "ol"
item = re.sub(r"^\d+\.\s", "", line) # backslash kept out of the f-string (py3.10)
html_lines.append(f"<li>{_md_inline(item)}</li>")
else:
close_list(); html_lines.append(f"<p>{_md_inline(line)}</p>")
close_list()
return "\n".join(html_lines)
_HTML_SHELL = """<!doctype html>
<html lang="{lang}"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
<style>
body {{ font-family: Georgia, "Times New Roman", serif; max-width: 760px;
margin: 40px auto; padding: 0 24px; color: #1a1a2e; line-height: 1.55; }}
h1 {{ font-size: 1.7rem; border-bottom: 2px solid #6d5dfc; padding-bottom: 6px; }}
h2 {{ font-size: 1.15rem; color: #4a3fb0; margin-top: 1.6em; }}
blockquote {{ border-left: 4px solid #b16cea; margin: 1em 0; padding: .4em 1em;
background: #f5f2ff; font-style: italic; }}
hr {{ border: none; border-top: 1px dashed #ccc; margin: 2.5em 0; }}
ul, ol {{ padding-left: 1.4em; }}
li {{ margin: .25em 0; }}
footer {{ margin-top: 3em; font-size: .8rem; color: #888; text-align: center;
font-family: sans-serif; }}
@media print {{ body {{ margin: 0; max-width: none; }} }}
</style></head>
<body>
{body}
<footer>Forged with Case Forge · Build Small Hackathon</footer>
</body></html>
"""
def to_markdown_file(case_md: str, note_md: str, title: str = "case") -> str:
"""Write the (possibly edited) case+note Markdown to a downloadable file."""
path = Path(tempfile.gettempdir()) / f"{_slug(title)}.md"
path.write_text(_combined_markdown(case_md, note_md), encoding="utf-8")
return str(path)
def to_html_file(case_md: str, note_md: str, title: str = "case",
lang: str = "pt") -> str:
"""Render the (possibly edited) Markdown into a self-contained printable HTML."""
body = _md_to_html_body(_combined_markdown(case_md, note_md))
doc = _HTML_SHELL.format(lang=lang, title=title or "Case", body=body)
path = Path(tempfile.gettempdir()) / f"{_slug(title)}.html"
path.write_text(doc, encoding="utf-8")
return str(path)
__all__ = ["to_markdown_file", "to_html_file"]