From 38e0f36d60e66acc0f8eb87cee0f75ed76e6a08c Mon Sep 17 00:00:00 2001 From: robbykap Date: Mon, 14 Jul 2025 13:28:26 -0600 Subject: [PATCH] Creates result json for inline rendered md in gradescope --- gradescope_md_render/render_md.py | 93 +++++++ gradescope_md_render/sample_content.md | 30 ++ gradescope_md_render/style.css | 369 +++++++++++++++++++++++++ 3 files changed, 492 insertions(+) create mode 100644 gradescope_md_render/render_md.py create mode 100644 gradescope_md_render/sample_content.md create mode 100644 gradescope_md_render/style.css diff --git a/gradescope_md_render/render_md.py b/gradescope_md_render/render_md.py new file mode 100644 index 0000000..9267b69 --- /dev/null +++ b/gradescope_md_render/render_md.py @@ -0,0 +1,93 @@ +import json +import pypandoc +from pathlib import Path +from argparse import ArgumentParser +from bs4 import BeautifulSoup +import cssutils + +def inline_css(html: str, css: str) -> str: + soup = BeautifulSoup(html, "html.parser") + sheet = cssutils.parseString(css) + + for rule in sheet: + if rule.type != rule.STYLE_RULE: + continue + + selector = rule.selectorText + try: + elements = soup.select(selector) + except Exception: + continue + + for element in elements: + existing = element.get("style", "") + inline_style = cssutils.css.CSSStyleDeclaration(cssText=existing) + + for prop in rule.style: + inline_style.setProperty(prop.name, prop.value) + + element["style"] = inline_style.cssText + + for tag in soup.find_all("style"): + tag.decompose() + + return str(soup) + +def convert_md_to_html(file_path: Path, css_path: Path) -> str: + content = file_path.read_text() + css = css_path.read_text() + + # ✅ Critical: use tex_math_dollars here + html_body = pypandoc.convert_text( + content, + to='html', + format='markdown+tex_math_dollars' + ) + + # Inline styles only on the body + inlined_body = inline_css(f"{html_body}", css) + + # ✅ Do NOT run BeautifulSoup on this — keep scripts intact + full_html = f""" + + + + + + + + {inlined_body} + +""" + + return full_html + +def gradescope_results(html_content: str): + with open("results.json", "w") as f: + json.dump({ + "score": 0.0, + "output": html_content, + "output_format": "html" + }, f, indent=2) + +def main(file_path: str, css_path: str): + html = convert_md_to_html(Path(file_path), Path(css_path)) + gradescope_results(html) + +def entry(): + parser = ArgumentParser() + parser.add_argument("--file_path", type=str, required=True) + parser.add_argument("--css_path", type=str, default="style.css") + args = parser.parse_args() + main(args.file_path, args.css_path) + +if __name__ == "__main__": + entry() diff --git a/gradescope_md_render/sample_content.md b/gradescope_md_render/sample_content.md new file mode 100644 index 0000000..e6dcb6c --- /dev/null +++ b/gradescope_md_render/sample_content.md @@ -0,0 +1,30 @@ +# Project 1 + +This project will have a code block + +```python +def hello_world(): + print("Hello, World!") + +def greet(name): + print(f"Hello, {name}!") + +def calculate_sum(a, b): + return a + b +``` + +It will also have a table + +| Name | Age | Occupation | +|----------|-----|-------------| +| Alice | 30 | Engineer | +| Bob | 25 | Designer | +| Charlie | 35 | Teacher | + +It may also have math expressions + +$$ +E = mc^2 +$$ + +Or inline math expressions like $E=mc^2$. diff --git a/gradescope_md_render/style.css b/gradescope_md_render/style.css new file mode 100644 index 0000000..4941fc2 --- /dev/null +++ b/gradescope_md_render/style.css @@ -0,0 +1,369 @@ +/* + * I add this to html files generated with pandoc. + */ + +html { + font-size: 100%; + overflow-y: scroll; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + color: #444; + font-family: Georgia, Palatino, 'Palatino Linotype', Times, 'Times New Roman', serif; + font-size: 12px; + line-height: 1.7; + padding: 1em; + margin: auto; + max-width: 42em; + background: #fefefe; +} + +a { + color: #0645ad; + text-decoration: none; +} + +a:visited { + color: #0b0080; +} + +a:hover { + color: #06e; +} + +a:active { + color: #faa700; +} + +a:focus { + outline: thin dotted; +} + +*::-moz-selection { + background: rgba(255, 255, 0, 0.3); + color: #000; +} + +*::selection { + background: rgba(255, 255, 0, 0.3); + color: #000; +} + +a::-moz-selection { + background: rgba(255, 255, 0, 0.3); + color: #0645ad; +} + +a::selection { + background: rgba(255, 255, 0, 0.3); + color: #0645ad; +} + +p { + margin: 1em 0; +} + +img { + max-width: 100%; +} + +h1, h2, h3, h4, h5, h6 { + color: #111; + line-height: 125%; + margin-top: 2em; + font-weight: normal; +} + +h4, h5, h6 { + font-weight: bold; +} + +h1 { + font-size: 2.5em; +} + +h2 { + font-size: 2em; +} + +h3 { + font-size: 1.5em; +} + +h4 { + font-size: 1.2em; +} + +h5 { + font-size: 1em; +} + +h6 { + font-size: 0.9em; +} + +blockquote { + color: #666666; + margin: 0; + padding-left: 3em; + border-left: 0.5em #EEE solid; +} + +hr { + display: block; + height: 2px; + border: 0; + border-top: 1px solid #aaa; + border-bottom: 1px solid #eee; + margin: 1em 0; + padding: 0; +} + +pre, code, kbd, samp { + color: #000; + font-family: monospace, monospace; + _font-family: 'courier new', monospace; + font-size: 0.98em; +} + +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} + +b, strong { + font-weight: bold; +} + +dfn { + font-style: italic; +} + +ins { + background: #ff9; + color: #000; + text-decoration: none; +} + +mark { + background: #ff0; + color: #000; + font-style: italic; + font-weight: bold; +} + +sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +ul, ol { + margin: 1em 0; + padding: 0 0 0 2em; +} + +li p:last-child { + margin-bottom: 0; +} + +ul ul, ol ol { + margin: .3em 0; +} + +dl { + margin-bottom: 1em; +} + +dt { + font-weight: bold; + margin-bottom: .8em; +} + +dd { + margin: 0 0 .8em 2em; +} + +dd:last-child { + margin-bottom: 0; +} + +img { + border: 0; + -ms-interpolation-mode: bicubic; + vertical-align: middle; +} + +figure { + display: block; + text-align: center; + margin: 1em 0; +} + +figure img { + border: none; + margin: 0 auto; +} + +figcaption { + font-size: 0.8em; + font-style: italic; + margin: 0 0 .8em; +} + +table { + margin-bottom: 2em; + border-bottom: 1px solid #ddd; + border-right: 1px solid #ddd; + border-spacing: 0; + border-collapse: collapse; +} + +table th { + padding: .2em 1em; + background-color: #eee; + border-top: 1px solid #ddd; + border-left: 1px solid #ddd; +} + +table td { + padding: .2em 1em; + border-top: 1px solid #ddd; + border-left: 1px solid #ddd; + vertical-align: top; +} + +.author { + font-size: 1.2em; + text-align: center; +} + +@media only screen and (min-width: 480px) { + body { + font-size: 14px; + } +} +@media only screen and (min-width: 768px) { + body { + font-size: 16px; + } +} +@media print { + * { + background: transparent !important; + color: black !important; + filter: none !important; + -ms-filter: none !important; + } + + body { + font-size: 12pt; + max-width: 100%; + } + + a, a:visited { + text-decoration: underline; + } + + hr { + height: 1px; + border: 0; + border-bottom: 1px solid black; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { + content: ""; + } + + pre, blockquote { + border: 1px solid #999; + padding-right: 1em; + page-break-inside: avoid; + } + + tr, img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + @page :left { + margin: 15mm 20mm 15mm 10mm; +} + + @page :right { + margin: 15mm 10mm 15mm 20mm; +} + + p, h2, h3 { + orphans: 3; + widows: 3; + } + + h2, h3 { + page-break-after: avoid; + } +} + +/* === GitHub Light Code Highlighting === */ +code span.kw { color: #d73a49 !important; font-weight: bold !important; } /* Keywords (def, class) */ +code span.op { color: #d73a49 !important; } /* Operators */ +code span.cf { color: #d73a49 !important; font-weight: bold !important; } /* Control Flow (return, if) */ +code span.dv { color: #005cc5 !important; } /* Literals (numbers) */ +code span.bu { color: #e36209 !important; } /* Built-ins (int, float) */ +code span.str { color: #032f62 !important; } /* Strings */ +code span.com { color: #6a737d !important; font-style: italic !important; } /* Comments */ +code span.typ { color: #005cc5 !important; } /* Type names */ +code span.std { color: #24292e !important; } /* Default identifiers */ + +pre { + background-color: #f6f8fa; + border: 1px solid #d1d5da; + border-radius: 6px; + overflow-x: auto; + margin: 1em 0; +} + +pre code { + background: none; + border: none; + padding: 0; + margin: 0; + display: block; + font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; + font-size: 0.95em; + line-height: 1.5; + white-space: pre; + color: inherit; +} + +code { + background-color: #f6f8fa; + border: 1px solid #d1d5da; + border-radius: 4px; + padding: 0.2em 0.4em; + font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; + font-size: 0.95em; +}