From cb3ea100392775314e263b3e9c89b91b4db6088d Mon Sep 17 00:00:00 2001 From: matistjati Date: Thu, 8 Aug 2024 00:54:41 +0200 Subject: [PATCH 01/25] Add markdown support --- examples/README.md | 5 + examples/bplusa/data/sample/1.ans | 1 + examples/bplusa/data/sample/1.in | 1 + examples/bplusa/data/secret/1.ans | 1 + examples/bplusa/data/secret/1.in | 1 + examples/bplusa/data/secret/2.ans | 1 + examples/bplusa/data/secret/2.in | 1 + examples/bplusa/data/secret/3.ans | 1 + examples/bplusa/data/secret/3.in | 1 + .../input_validators/validator/validator.cpp | 8 + .../input_validators}/validator/validator.h | 0 .../output_validators/validator/validate.cc | 64 ++++ .../output_validators/validator/validate.h | 153 ++++++++ examples/bplusa/problem.yaml | 4 + .../bplusa/problem_statement/problem.en.md | 8 + .../bplusa/submissions/accepted/cplus1.cpp | 10 + examples/bplusa/submissions/accepted/zero.cpp | 10 + .../validator/validator.cpp | 0 .../input_validators/validator/validator.h | 356 ++++++++++++++++++ problemtools/md2html.py | 162 ++++++++ problemtools/problem2html.py | 98 +++-- problemtools/problem2pdf.py | 6 +- .../templates/markdown/default-layout.html | 35 ++ problemtools/templates/markdown/problem.css | 90 +++++ problemtools/tex2html.py | 67 ++++ problemtools/verifyproblem.py | 53 +-- 26 files changed, 1056 insertions(+), 81 deletions(-) create mode 100644 examples/bplusa/data/sample/1.ans create mode 100644 examples/bplusa/data/sample/1.in create mode 100644 examples/bplusa/data/secret/1.ans create mode 100644 examples/bplusa/data/secret/1.in create mode 100644 examples/bplusa/data/secret/2.ans create mode 100644 examples/bplusa/data/secret/2.in create mode 100644 examples/bplusa/data/secret/3.ans create mode 100644 examples/bplusa/data/secret/3.in create mode 100644 examples/bplusa/input_validators/validator/validator.cpp rename examples/{oddecho/input_format_validators => bplusa/input_validators}/validator/validator.h (100%) create mode 100644 examples/bplusa/output_validators/validator/validate.cc create mode 100644 examples/bplusa/output_validators/validator/validate.h create mode 100644 examples/bplusa/problem.yaml create mode 100644 examples/bplusa/problem_statement/problem.en.md create mode 100644 examples/bplusa/submissions/accepted/cplus1.cpp create mode 100644 examples/bplusa/submissions/accepted/zero.cpp rename examples/oddecho/{input_format_validators => input_validators}/validator/validator.cpp (100%) create mode 100644 examples/oddecho/input_validators/validator/validator.h create mode 100644 problemtools/md2html.py create mode 100644 problemtools/templates/markdown/default-layout.html create mode 100644 problemtools/templates/markdown/problem.css create mode 100644 problemtools/tex2html.py diff --git a/examples/README.md b/examples/README.md index 2f6107a3..9665f8a7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -25,3 +25,8 @@ more than one language. This is an example of a *scoring* problem where submissions can get different scores depending on which test groups they solve. It also demonstrates how an input validator might check different constraints for different test groups. + +# bplusa + +This is an example of a problem using an output validator, showcasing different things to keep in mind +when using output validator. It also demonstrates using Markdown as a statement language. diff --git a/examples/bplusa/data/sample/1.ans b/examples/bplusa/data/sample/1.ans new file mode 100644 index 00000000..654d5269 --- /dev/null +++ b/examples/bplusa/data/sample/1.ans @@ -0,0 +1 @@ +2 3 diff --git a/examples/bplusa/data/sample/1.in b/examples/bplusa/data/sample/1.in new file mode 100644 index 00000000..7ed6ff82 --- /dev/null +++ b/examples/bplusa/data/sample/1.in @@ -0,0 +1 @@ +5 diff --git a/examples/bplusa/data/secret/1.ans b/examples/bplusa/data/secret/1.ans new file mode 100644 index 00000000..1790e253 --- /dev/null +++ b/examples/bplusa/data/secret/1.ans @@ -0,0 +1 @@ +123 0 diff --git a/examples/bplusa/data/secret/1.in b/examples/bplusa/data/secret/1.in new file mode 100644 index 00000000..190a1803 --- /dev/null +++ b/examples/bplusa/data/secret/1.in @@ -0,0 +1 @@ +123 diff --git a/examples/bplusa/data/secret/2.ans b/examples/bplusa/data/secret/2.ans new file mode 100644 index 00000000..93fd4034 --- /dev/null +++ b/examples/bplusa/data/secret/2.ans @@ -0,0 +1 @@ +992 0 diff --git a/examples/bplusa/data/secret/2.in b/examples/bplusa/data/secret/2.in new file mode 100644 index 00000000..7f9d7e97 --- /dev/null +++ b/examples/bplusa/data/secret/2.in @@ -0,0 +1 @@ +992 diff --git a/examples/bplusa/data/secret/3.ans b/examples/bplusa/data/secret/3.ans new file mode 100644 index 00000000..80c0cc79 --- /dev/null +++ b/examples/bplusa/data/secret/3.ans @@ -0,0 +1 @@ +1 0 diff --git a/examples/bplusa/data/secret/3.in b/examples/bplusa/data/secret/3.in new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/examples/bplusa/data/secret/3.in @@ -0,0 +1 @@ +1 diff --git a/examples/bplusa/input_validators/validator/validator.cpp b/examples/bplusa/input_validators/validator/validator.cpp new file mode 100644 index 00000000..0ecff521 --- /dev/null +++ b/examples/bplusa/input_validators/validator/validator.cpp @@ -0,0 +1,8 @@ +#include "validator.h" + + +void run() { + Int(1, 1000); + Endl(); + Eof(); +} diff --git a/examples/oddecho/input_format_validators/validator/validator.h b/examples/bplusa/input_validators/validator/validator.h similarity index 100% rename from examples/oddecho/input_format_validators/validator/validator.h rename to examples/bplusa/input_validators/validator/validator.h diff --git a/examples/bplusa/output_validators/validator/validate.cc b/examples/bplusa/output_validators/validator/validate.cc new file mode 100644 index 00000000..61eabfc2 --- /dev/null +++ b/examples/bplusa/output_validators/validator/validate.cc @@ -0,0 +1,64 @@ +#include "validate.h" + +#include +using namespace std; + +#define rep(i, a, b) for(int i = a; i < (b); ++i) +#define all(x) begin(x), end(x) +#define sz(x) (int)(x).size() +typedef long long ll; +typedef pair pii; +typedef vector vi; +typedef vector vvi; +typedef long double ld; + +#define repe(i, container) for (auto& i : container) + +void check_isvalid(int a, int b, int c, feedback_function feedback) +{ + if (a==b) feedback("a is equal to b"); + if (a+b!=c) feedback("b+a!=c"); +} + +const int HUNDRED_THOUSAND = int(1e5); +int main(int argc, char **argv) { + init_io(argc, argv); + + // Read the testcase input + int c; + judge_in >> c; + + auto check = [&](istream& sol, feedback_function feedback) { + int a, b; + // Don't get stuck waiting for output from solution + if(!(sol >> a >> b)) feedback("Expected more output"); + // Validate constraints + if (a < -HUNDRED_THOUSAND || a > HUNDRED_THOUSAND) feedback("a is too big or large"); + if (b < -HUNDRED_THOUSAND || b > HUNDRED_THOUSAND) feedback("b is too big or large"); + + // Check that they actually solved the task + check_isvalid(a, b, c, feedback); + + // Disallow trailing output + string trailing; + if(sol >> trailing) feedback("Trailing output"); + return true; + }; + + // Check both the judge's and contestants' output + // It is good practice to not assume that the judge is correct/optimal + bool judge_found_sol = check(judge_ans, judge_error); + bool author_found_sol = check(author_out, wrong_answer); + + // In this problem, having a return value from check is unnecessary + // However, if there isn't always a solution, we will get a nice + // judge error if the judge solution claims no solution exists, while + // a contestant finds one + if(!judge_found_sol) + judge_error("NO! Judge did not find valid solution"); + + if(!author_found_sol) + wrong_answer("Contestant did not find valid solution"); + + accept(); +} diff --git a/examples/bplusa/output_validators/validator/validate.h b/examples/bplusa/output_validators/validator/validate.h new file mode 100644 index 00000000..c59c5fdb --- /dev/null +++ b/examples/bplusa/output_validators/validator/validate.h @@ -0,0 +1,153 @@ +/* Utility functions for writing output validators for the Kattis + * problem format. + * + * The primary functions and variables available are the following. + * In many cases, the only functions needed are "init_io", + * "wrong_answer", and "accept". + * + * - init_io(argc, argv): + * initialization + * + * - judge_in, judge_ans, author_out: + * std::istream objects for judge input file, judge answer + * file, and submission output file. + * + * - accept(): + * exit and give Accepted! + * + * - accept_with_score(double score): + * exit with Accepted and give a score (for scoring problems) + * + * - judge_message(std::string msg, ...): + * printf-style function for emitting a judge message (a + * message that gets displayed to a privileged user with access + * to secret data etc). + * + * - wrong_answer(std::string msg, ...): + * printf-style function for exitting and giving Wrong Answer, + * and emitting a judge message (which would typically explain + * the cause of the Wrong Answer) + * + * - judge_error(std::string msg, ...): + * printf-style function for exitting and giving Judge Error, + * and emitting a judge message (which would typically explain + * the cause of the Judge Error) + * + * - author_message(std::string msg, ...): + * printf-style function for emitting an author message (a + * message that gets displayed to the author of the + * submission). (Use with caution, and be careful not to let + * it leak information!) + * + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +typedef void (*feedback_function)(const char*, ...); + +const int EXITCODE_AC = 42; +const int EXITCODE_WA = 43; +const char* FILENAME_AUTHOR_MESSAGE = "teammessage.txt"; +const char* FILENAME_JUDGE_MESSAGE = "judgemessage.txt"; +const char* FILENAME_JUDGE_ERROR = "judgeerror.txt"; +const char* FILENAME_SCORE = "score.txt"; + +#define USAGE "%s: judge_in judge_ans feedback_dir < author_out\n" + +std::ifstream judge_in, judge_ans; +std::istream author_out(std::cin.rdbuf()); + +char *feedbackdir = NULL; + +void vreport_feedback(const char* category, + const char* msg, + va_list pvar) { + std::ostringstream fname; + if (feedbackdir) + fname << feedbackdir << '/'; + fname << category; + FILE *f = fopen(fname.str().c_str(), "a"); + assert(f); + vfprintf(f, msg, pvar); + fclose(f); +} + +void report_feedback(const char* category, const char* msg, ...) { + va_list pvar; + va_start(pvar, msg); + vreport_feedback(category, msg, pvar); +} + +void author_message(const char* msg, ...) { + va_list pvar; + va_start(pvar, msg); + vreport_feedback(FILENAME_AUTHOR_MESSAGE, msg, pvar); +} + +void judge_message(const char* msg, ...) { + va_list pvar; + va_start(pvar, msg); + vreport_feedback(FILENAME_JUDGE_MESSAGE, msg, pvar); +} + +void wrong_answer(const char* msg, ...) { + va_list pvar; + va_start(pvar, msg); + vreport_feedback(FILENAME_JUDGE_MESSAGE, msg, pvar); + exit(EXITCODE_WA); +} + +void judge_error(const char* msg, ...) { + va_list pvar; + va_start(pvar, msg); + vreport_feedback(FILENAME_JUDGE_ERROR, msg, pvar); + assert(0); +} + +void accept() { + exit(EXITCODE_AC); +} + +void accept_with_score(double scorevalue) { + report_feedback(FILENAME_SCORE, "%.9le", scorevalue); + exit(EXITCODE_AC); +} + + +bool is_directory(const char *path) { + struct stat entry; + return stat(path, &entry) == 0 && S_ISDIR(entry.st_mode); +} + +void init_io(int argc, char **argv) { + if(argc < 4) { + fprintf(stderr, USAGE, argv[0]); + judge_error("Usage: %s judgein judgeans feedbackdir [opts] < userout", argv[0]); + } + + // Set up feedbackdir first, as that allows us to produce feedback + // files for errors in the other parameters. + if (!is_directory(argv[3])) { + judge_error("%s: %s is not a directory\n", argv[0], argv[3]); + } + feedbackdir = argv[3]; + + judge_in.open(argv[1], std::ios_base::in); + if (judge_in.fail()) { + judge_error("%s: failed to open %s\n", argv[0], argv[1]); + } + + judge_ans.open(argv[2], std::ios_base::in); + if (judge_ans.fail()) { + judge_error("%s: failed to open %s\n", argv[0], argv[2]); + } + + author_out.rdbuf(std::cin.rdbuf()); +} diff --git a/examples/bplusa/problem.yaml b/examples/bplusa/problem.yaml new file mode 100644 index 00000000..d59b82ec --- /dev/null +++ b/examples/bplusa/problem.yaml @@ -0,0 +1,4 @@ +source: Kattis +license: public domain +name: B plus A +validation: custom diff --git a/examples/bplusa/problem_statement/problem.en.md b/examples/bplusa/problem_statement/problem.en.md new file mode 100644 index 00000000..d5060a86 --- /dev/null +++ b/examples/bplusa/problem_statement/problem.en.md @@ -0,0 +1,8 @@ +Given the integer $c$, find any pair of integers $b$ and $a$ satisfying $b+a=c$ and $a \neq b$. + +## Input +Input consists of the integer $C$ ($1 \le C \le 1000$). + +## Output +Output $b$ and $a$, separated by a space. Any $b$, $a$ satisfying above constraints and $-10^5 \leq a,b \leq 10^5$ +will be accepted. diff --git a/examples/bplusa/submissions/accepted/cplus1.cpp b/examples/bplusa/submissions/accepted/cplus1.cpp new file mode 100644 index 00000000..946facb7 --- /dev/null +++ b/examples/bplusa/submissions/accepted/cplus1.cpp @@ -0,0 +1,10 @@ +#include +using namespace std; + +int main() +{ + int c; + cin >> c; + cout << c+1 << " " << -1 << endl; + return 0; +} diff --git a/examples/bplusa/submissions/accepted/zero.cpp b/examples/bplusa/submissions/accepted/zero.cpp new file mode 100644 index 00000000..2f4c748a --- /dev/null +++ b/examples/bplusa/submissions/accepted/zero.cpp @@ -0,0 +1,10 @@ +#include +using namespace std; + +int main() +{ + int c; + cin >> c; + cout << c << " " << 0 << endl; + return 0; +} diff --git a/examples/oddecho/input_format_validators/validator/validator.cpp b/examples/oddecho/input_validators/validator/validator.cpp similarity index 100% rename from examples/oddecho/input_format_validators/validator/validator.cpp rename to examples/oddecho/input_validators/validator/validator.cpp diff --git a/examples/oddecho/input_validators/validator/validator.h b/examples/oddecho/input_validators/validator/validator.h new file mode 100644 index 00000000..f42bc2d7 --- /dev/null +++ b/examples/oddecho/input_validators/validator/validator.h @@ -0,0 +1,356 @@ +#ifdef NDEBUG +#error Asserts must be enabled! Do not set NDEBUG. +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace std; + +// Implemented by you! +void run(); + +// PUBLIC API +// (extend if you need to) + +[[noreturn]] +void die(const string& msg); +[[noreturn]] +void die_line(const string& msg); + +struct ArgType { + string _name, _x; + ArgType(const string& name, const string& x) : _name(name), _x(x) {} + operator string() const { return _x; } + operator long long() const; + operator bool() const; + operator int() const; +}; + +struct IntType { + long long _x; + IntType(long long x) : _x(x) {} + operator long long() const { return _x; } + operator int() const; + operator bool() const; +}; + +ArgType Arg(const string& name); + +ArgType Arg(const string& name, long long _default); + +string Arg(const string& name, const string& _default); + +template +void AssertUnique(const Vec& v); + +namespace IO { + IntType Int(long long lo, long long hi); + double Float(double lo, double hi, bool strict = true); + template + vector SpacedInts(long long count, T lo, T hi); + vector SpacedFloats(long long count, double lo, double hi); + void Char(char expected); + char Char(); + string Line(); + void Endl() { Char('\n'); } + void Space() { Char(' '); } + void Eof() { Char(-1); } +}; +using namespace IO; + +// INTERNALS + +bool _validator_initialized; +struct _validator { + map params; + set used_params; + + void construct(int argc, char** argv) { + _validator_initialized = true; + for (int i = 1; i < argc; i++) { + string s = argv[i]; + size_t ind = s.find('='); + if (ind == string::npos) continue; + auto before = s.substr(0, ind), after = s.substr(ind + 1); + if (params.count(before)) + die("Duplicate parameter " + before); + params[before] = after; + } + } + + void destroy() { + assert(_validator_initialized); + if (!params.empty()) { + string name = params.begin()->first; + die("Unused parameter " + name); + } + IO::Eof(); + _Exit(42); + } + + bool has_var(const string& name) { + if (!_validator_initialized) die("Must not read variables before main"); + return params.count(name) || used_params.count(name); + } + + string get_var(const string& name) { + if (!_validator_initialized) die("Must not read variables before main"); + if (used_params.count(name)) die("Must not read parameter " + name + " twice (either typo or slow)"); + if (!params.count(name)) die("No parameter " + name); + string res = params.at(name); + params.erase(name); + used_params.insert(name); + return res; + } +} _validator_inst; + +void die(const string& msg) { + cerr << msg << endl; + ofstream fout("/tmp/input_validator_msg", ios::app); + fout << msg << endl; + fout.close(); + _Exit(43); +} + +ArgType::operator long long() const { + string dummy; + { + long long num; + istringstream iss(_x); + iss >> num; + if (iss && !(iss >> dummy)) return num; + } + { + // We also allow scientific notation, for clarity + long double num; + istringstream iss(_x); + iss >> num; + if (iss && !(iss >> dummy)) return (long long)num; + } + die("Unable to parse value " + _x + " for parameter " + _name); +} + +ArgType::operator int() const { + long long val = (long long)*this; + if (val < INT_MIN || val > INT_MAX) + die("number " + to_string(val) + " is too large for an int for parameter " + _name); + return (int)val; +} + +ArgType::operator bool() const { + long long val = (long long)*this; + if (val < 0 || val > 1) + die("number " + to_string(val) + " is not boolean (0/1), for parameter " + _name); + return (bool)val; +} + +IntType::operator int() const { + long long val = (long long)*this; + if (val < INT_MIN || val > INT_MAX) + die_line("number " + to_string(val) + " is too large for an int"); + return (int)val; +} + +IntType::operator bool() const { + long long val = (long long)*this; + if (val < 0 || val > 1) + die_line("number " + to_string(val) + " is not boolean (0/1)"); + return (bool)val; +} + +ArgType Arg(const string& name) { + return {name, _validator_inst.get_var(name)}; +} + +ArgType Arg(const string& name, long long _default) { + if (!_validator_inst.has_var(name)) + return {name, to_string(_default)}; + ArgType ret = Arg(name); + (void)(long long)ret; + return ret; +} + +string Arg(const string& name, const string& _default) { + if (!_validator_inst.has_var(name)) + return _default; + return (string)Arg(name); +} + +static int _lineno = 1, _consumed_lineno = -1, _hit_char_error = 0; +char _peek1(); +void die_line(const string& msg) { + if (!_hit_char_error && _peek1() == -1) die(msg); + else if (_consumed_lineno == -1) die(msg + " (before reading any input)"); + else die(msg + " on line " + to_string(_consumed_lineno)); +} + +static char _buffer = -2; // -2 = none, -1 = eof, other = that char +char _peek1() { + if (_buffer != -2) return _buffer; + int val = getchar_unlocked(); + static_assert(EOF == -1, ""); + static_assert(CHAR_MIN == -128, ""); + if (val == -2 || val < CHAR_MIN || val >= CHAR_MAX) { + _hit_char_error = 1; + die_line("Unable to process byte " + to_string(val)); + } + _buffer = (char)val; + return _buffer; +} +void _use_peek(char ch) { + _buffer = -2; + if (ch == '\n') _lineno++; + else _consumed_lineno = _lineno; +} +char _read1() { + char ret = _peek1(); + _use_peek(ret); + return ret; +} +string _token() { + string ret; + for (;;) { + char ch = _peek1(); + if (ch == ' ' || ch == '\n' || ch == -1) { + break; + } + _use_peek(ch); + ret += ch; + } + return ret; +} +string _describe(char ch) { + assert(ch != -2); + if (ch == -1) return "EOF"; + if (ch == ' ') return "SPACE"; + if (ch == '\r') return "CARRIAGE RETURN"; + if (ch == '\n') return "NEWLINE"; + if (ch == '\t') return "TAB"; + if (ch == '\'') return "\"'\""; + return string("'") + ch + "'"; +} + +IntType IO::Int(long long lo, long long hi) { + string s = _token(); + if (s.empty()) die_line("Expected number, saw " + _describe(_peek1())); + try { + long long mul = 1; + int ind = 0; + if (s[0] == '-') { + mul = -1; + ind = 1; + } + if (ind == (int)s.size()) throw false; + char ch = s[ind++]; + if (ch < '0' || ch > '9') throw false; + if (ch == '0' && ind != (int)s.size()) throw false; + long long ret = ch - '0'; + while (ind < (int)s.size()) { + if (ret > LLONG_MAX / 10 - 20 || ret < LLONG_MIN / 10 + 20) + throw false; + ret *= 10; + ch = s[ind++]; + if (ch < '0' || ch > '9') throw false; + ret += ch - '0'; + } + ret *= mul; + if (ret < lo || ret > hi) die_line("Number " + s + " is out of range [" + to_string(lo) + ", " + to_string(hi) + "]"); + return {ret}; + } catch (bool) { + die_line("Unable to parse \"" + s + "\" as integer"); + } +} + +template +vector IO::SpacedInts(long long count, T lo, T hi) { + vector res; + res.reserve(count); + for (int i = 0; i < count; i++) { + if (i != 0) IO::Space(); + res.emplace_back((T)IO::Int(lo, hi)); + } + IO::Endl(); + return res; +} + +vector IO::SpacedFloats(long long count, double lo, double hi) { + vector res; + res.reserve(count); + for (int i = 0; i < count; i++) { + if (i != 0) IO::Space(); + res.emplace_back(IO::Float(lo, hi)); + } + IO::Endl(); + return res; +} + +double IO::Float(double lo, double hi, bool strict) { + string s = _token(); + if (s.empty()) die_line("Expected floating point number, saw " + _describe(_peek1())); + istringstream iss(s); + double res; + string dummy; + iss >> res; + if (!iss || iss >> dummy) die_line("Unable to parse " + s + " as a float"); + if (res < lo || res > hi) die_line("Floating-point number " + s + " is out of range [" + to_string(lo) + ", " + to_string(hi) + "]"); + if (res != res) die_line("Floating-point number " + s + " is NaN"); + if (strict) { + if (s.find('.') != string::npos && s.back() == '0' && s.substr(s.size() - 2) != ".0") + die_line("Number " + s + " has unnecessary trailing zeroes"); + if (s[0] == '0' && s.size() > 1 && s[1] == '0') + die_line("Number " + s + " has unnecessary leading zeroes"); + } + return res; +} + +char IO::Char() { + char ret = _read1(); + if (ret == -1) die_line("Expected character, saw EOF"); + return ret; +} + +void IO::Char(char expected) { + char ret = _peek1(); + if (ret != expected) die_line("Expected " + _describe(expected) + ", saw " + _describe(ret)); + _use_peek(ret); +} + +string IO::Line() { + string ret; + for (;;) { + char ch = IO::Char(); + if (ch == '\n') break; + ret += ch; + } + return ret; +} + +template +void AssertUnique(const Vec& v_) { + Vec v = v_; + auto beg = v.begin(), end = v.end(); + sort(beg, end); + int size = (int)(end - beg); + for (int i = 0; i < size - 1; i++) { + if (v[i] == v[i+1]) { + ostringstream oss; + oss << "Vector contains duplicate value " << v[i]; + die_line(oss.str()); + } + } +} + +int main(int argc, char** argv) { + _validator_inst.construct(argc, argv); + run(); + _validator_inst.destroy(); +} + diff --git a/problemtools/md2html.py b/problemtools/md2html.py new file mode 100644 index 00000000..63b1f6f4 --- /dev/null +++ b/problemtools/md2html.py @@ -0,0 +1,162 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +import html +import os.path +import string +import argparse +from typing import Optional + +import markdown +from markdown.inlinepatterns import InlineProcessor +from markdown.extensions import Extension +import xml.etree.ElementTree as etree + +from . import verifyproblem +from . import problem2html + +def _substitute_template(templatepath: str, templatefile: str, **params) -> str: + """Read the markdown template and substitute in things such as problem name, + statement etc using python's format syntax. + """ + with open(os.path.join(templatepath, templatefile), "r", encoding="utf-8") as template_file: + html_template = template_file.read() % params + return html_template + + +def _get_problem_name(problem: str, language: str = "en") -> Optional[str]: + """Load problem.yaml to get problem name""" + with verifyproblem.Problem(problem) as prob: + config = verifyproblem.ProblemConfig(prob) + if not config.check(None): + print("Please add problem name to problem.yaml when using markdown") + return None + names = config.get("name") + # If there is only one language, per the spec that is the one we want + if len(names) == 1: + return next(iter(names.values())) + + if language not in names: + raise Exception(f"No problem name defined for language {language}") + return names[language] + + +def _samples_to_html(problem: str) -> str: + """Read all samples from the problem directory and convert them to HTML""" + samples_html = "" + sample_path = os.path.join(problem, "data", "sample") + interactive_samples = [] + samples = [] + casenum = 1 + for sample in sorted(os.listdir(sample_path)): + if sample.endswith(".interaction"): + lines = [""" + + + + + +
ReadSample Interaction {}Write
""".format(casenum)] + with open(os.path.join(sample_path, sample), "r", encoding="utf-8") as infile: + sample_interaction = infile.readlines() + for interaction in sample_interaction: + data = interaction[1:] + line_type = "" + if interaction[0] == '>': + line_type = "sampleinteractionwrite" + elif interaction[0] == '<': + line_type = "sampleinteractionread" + else: + print(f"Warning: Interaction had unknown prefix {interaction[0]}") + lines.append(f"""
{data}
""") + + interactive_samples.append(''.join(lines)) + casenum += 1 + continue + if not sample.endswith(".in"): + continue + sample_name = sample[:-3] + outpath = os.path.join(sample_path, sample_name + ".ans") + if not os.path.isfile(outpath): + continue + with open(os.path.join(sample_path, sample), "r", encoding="utf-8") as infile: + sample_input = infile.read() + with open(outpath, "r", encoding="utf-8") as outfile: + sample_output = outfile.read() + + samples.append(f""" + + Sample Input %(case)d + Sample Output %(case)d + + +
%(input)s
+
%(output)s
+ """ % ({"case": casenum, "input": html.escape(sample_input), "output": html.escape(sample_output)})) + casenum += 1 + + if interactive_samples: + samples_html += ''.join(interactive_samples) + if samples: + samples_html += """ + + + %(samples)s + +
+ """ % {"samples": ''.join(samples)} + return samples_html + + +def convert(problem: str, options: argparse.Namespace) -> None: + problembase = os.path.splitext(os.path.basename(problem))[0] + destfile = string.Template(options.destfile).safe_substitute(problem=problembase) + + statement_path = problem2html._find_statement(problem, extension="md", language=options.language) + + if statement_path is None: + raise Exception('No markdown statement found') + + with open(statement_path, "r", encoding="utf-8") as input_file: + text = input_file.read() + statement_html = markdown.markdown(text, extensions=[InlineMathExtension(), "tables"]) + + templatepaths = [os.path.join(os.path.dirname(__file__), 'templates/markdown'), + os.path.join(os.path.dirname(__file__), '../templates/markdown'), + '/usr/lib/problemtools/templates/markdown'] + templatepath = next((p for p in templatepaths + if os.path.isdir(p) and os.path.isfile(os.path.join(p, "default-layout.html"))), + None) + + if templatepath is None: + raise Exception('Could not find directory with markdown templates') + + problem_name = _get_problem_name(problem) + + html_template = _substitute_template(templatepath, "default-layout.html", + statement_html=statement_html, + language=options.language, + title=problem_name or "Missing problem name", + problemid=problembase) + + html_template += _samples_to_html(problem) + + with open(destfile, "w", encoding="utf-8", errors="xmlcharrefreplace") as output_file: + output_file.write(html_template) + + if options.css: + with open("problem.css", "w") as output_file: + with open(os.path.join(templatepath, "problem.css"), "r") as input_file: + output_file.write(input_file.read()) + + +class InlineMathProcessor(InlineProcessor): + def handleMatch(self, m, data): + el = etree.Element('span') + el.attrib['class'] = 'tex2jax_process' + el.text = "$" + m.group(1) + "$" + return el, m.start(0), m.end(0) + +class InlineMathExtension(Extension): + def extendMarkdown(self, md): + MATH_PATTERN = r'\$(.*?)\$' # like $1 + 2$ + md.inlinePatterns.register(InlineMathProcessor(MATH_PATTERN, md), 'inline-math', 200) \ No newline at end of file diff --git a/problemtools/problem2html.py b/problemtools/problem2html.py index 6bf56192..f0224e89 100644 --- a/problemtools/problem2html.py +++ b/problemtools/problem2html.py @@ -6,65 +6,50 @@ import argparse import logging import subprocess +from typing import Optional + +from . import tex2html +from . import md2html + +SUPPORTED_EXTENSIONS = ("tex", "md") + +def _find_statement(problem: str, extension: str, language: Optional[str]) -> Optional[str]: + """Finds the "best" statement for given language and extension""" + if language is None: + statement_path = os.path.join(problem, f"problem_statement/problem.en.{extension}") + if os.path.isfile(statement_path): + return statement_path + statement_path = os.path.join(problem, f"problem_statement/problem.{extension}") + if os.path.isfile(statement_path): + return statement_path + return None + statement_path = os.path.join(problem, f"problem_statement/problem.{language}.{extension}") + if os.path.isfile(statement_path): + return statement_path + return None + + +def _find_statement_extension(problem: str, language: Optional[str]) -> str: + """Given a language, find whether the extension is tex or md""" + extensions = [] + for ext in SUPPORTED_EXTENSIONS: + if _find_statement(problem, ext, language) is not None: + extensions.append(ext) + # At most one extension per language to avoid arbitrary/hidden priorities + if len(extensions) > 1: + raise Exception(f"""Found more than one type of statement ({' and '.join(extensions)}) + for language {language or 'en'}""") + if len(extensions) == 1: + return extensions[0] + raise Exception(f"No statement found for language {language or 'en'}") -from . import template def convert(options: argparse.Namespace) -> None: - # PlasTeX.Logging statically overwrites logging and formatting, so delay loading - import plasTeX.TeX - import plasTeX.Logging - from .ProblemPlasTeX import ProblemRenderer - from .ProblemPlasTeX import ProblemsetMacros - problem = os.path.realpath(options.problem) problembase = os.path.splitext(os.path.basename(problem))[0] destdir = string.Template(options.destdir).safe_substitute(problem=problembase) destfile = string.Template(options.destfile).safe_substitute(problem=problembase) - imgbasedir = string.Template(options.imgbasedir).safe_substitute(problem=problembase) - - if options.quiet: - plasTeX.Logging.disableLogging() - else: - plasTeX.Logging.getLogger().setLevel(getattr(logging, options.loglevel.upper())) - plasTeX.Logging.getLogger('status').setLevel(getattr(logging, options.loglevel.upper())) - - texfile = problem - # Set up template if necessary - with template.Template(problem, language=options.language) as templ: - texfile = open(templ.get_file_name(), 'r') - - origcwd = os.getcwd() - - # Setup parser and renderer etc - - # plasTeX version 3 changed the name of this argument (and guarding against this - # by checking plasTeX.__version__ fails on plastex v3.0 which failed to update - # __version__) - try: - tex = plasTeX.TeX.TeX(myfile=texfile) - except Exception: - tex = plasTeX.TeX.TeX(file=texfile) - - ProblemsetMacros.init(tex) - - tex.ownerDocument.config['general']['copy-theme-extras'] = options.css - if not options.headers: - tex.ownerDocument.userdata['noheaders'] = True - tex.ownerDocument.config['files']['filename'] = destfile - tex.ownerDocument.config['images']['filenames'] = 'img-$num(4)' - tex.ownerDocument.config['images']['enabled'] = False - tex.ownerDocument.config['images']['imager'] = 'none' - tex.ownerDocument.config['images']['base-url'] = imgbasedir - # tell plasTeX where to search for problemtools' built-in packages - tex.ownerDocument.config['general']['packages-dirs'] = [os.path.join(os.path.dirname(__file__), 'ProblemPlasTeX')] - - renderer = ProblemRenderer() - - if not options.quiet: - print('Parsing TeX source...') - doc = tex.parse() - texfile.close() # Go to destdir if destdir: @@ -75,12 +60,13 @@ def convert(options: argparse.Namespace) -> None: try: if not options.quiet: print('Rendering!') - renderer.render(doc) - # Annoying: I have not figured out any way of stopping the plasTeX - # renderer from generating a .paux file - if os.path.isfile('.paux'): - os.remove('.paux') + origcwd = os.getcwd() + + if _find_statement_extension(problem, options.language) == "tex": + tex2html.convert(problem, options) + else: + md2html.convert(problem, options) if options.tidy: with open(os.devnull, 'w') as devnull: diff --git a/problemtools/problem2pdf.py b/problemtools/problem2pdf.py index 0f6fc452..ac119d05 100644 --- a/problemtools/problem2pdf.py +++ b/problemtools/problem2pdf.py @@ -8,11 +8,15 @@ from . import template -def convert(options: argparse.Namespace) -> bool: +def convert(options: argparse.Namespace, ignore_markdown: bool = False) -> bool: problem = os.path.realpath(options.problem) problembase = os.path.splitext(os.path.basename(problem))[0] destfile = string.Template(options.destfile).safe_substitute(problem=problembase) + # We skip PDF check when verifying problems with markdown statements + if os.path.isfile(os.path.join(problem, "problem_statement", "problem.%s.md" % options.language)) and ignore_markdown: + return True + # Set up template if necessary with template.Template(problem, language=options.language) as templ: texfile = templ.get_file_name() diff --git a/problemtools/templates/markdown/default-layout.html b/problemtools/templates/markdown/default-layout.html new file mode 100644 index 00000000..a7177fc3 --- /dev/null +++ b/problemtools/templates/markdown/default-layout.html @@ -0,0 +1,35 @@ + + + + +%(title)s + + + + + + + + +
+

%(title)s

+

Problem ID: %(problemid)s

+
+
+ %(statement_html)s +
+ + + \ No newline at end of file diff --git a/problemtools/templates/markdown/problem.css b/problemtools/templates/markdown/problem.css new file mode 100644 index 00000000..20448219 --- /dev/null +++ b/problemtools/templates/markdown/problem.css @@ -0,0 +1,90 @@ +.problemheader { + text-align: center; +} + +.problembody { + font-family: 'Times New Roman', Georgia, serif; + font-size: 1.1em; + text-align: justify; + padding-top: 1.5em; +} + +.problembody h2, .problembody h3, .problembody table.sample th { + font-family: Arial, Helvetica, sans-serif; +} + +div.minipage { + display: inline-block; +} + +div.illustration { + float: right; + padding-left: 20px; +} + +img.illustration { + width: 100%; +} + +div.figure { + display: block; + float: none; + margin-left: auto; + margin-right: auto; +} + +.illustration div.description { + font-size: 8pt; + text-align: right; +} + +.problembody p { + text-align: justify; +} + +td { + vertical-align:top; +} + +table, table td { + border: 0; +} + +table.tabular p { + margin: 0; +} + +table.sample { + width: 100%; +} + +table.sample th { + text-align: left; + width: 50%; +} + +table.sample td { + border: 1px solid black; +} + +div.sampleinteractionread { + border: 1px solid black; + width: 60%; + float: left; + margin: 3px 0px 3px 0px; +} + +.sampleinteractionread pre { + margin: 1px 5px 1px 5px; +} + +div.sampleinteractionwrite { + border: 1px solid black; + width: 60%; + float: right; + margin: 3px 0px 3px 0px; +} + +.sampleinteractionwrite pre { + margin: 1px 5px 1px 5px; +} \ No newline at end of file diff --git a/problemtools/tex2html.py b/problemtools/tex2html.py new file mode 100644 index 00000000..8281f804 --- /dev/null +++ b/problemtools/tex2html.py @@ -0,0 +1,67 @@ +import os +import logging +import string +import argparse + +from . import template + + +def convert(problem: str, options: argparse.Namespace) -> None: + # PlasTeX.Logging statically overwrites logging and formatting, so delay loading + import plasTeX.TeX + import plasTeX.Logging + from .ProblemPlasTeX import ProblemRenderer + from .ProblemPlasTeX import ProblemsetMacros + + problembase = os.path.splitext(os.path.basename(problem))[0] + if options.quiet: + plasTeX.Logging.disableLogging() + else: + plasTeX.Logging.getLogger().setLevel(getattr(logging, options.loglevel.upper())) + plasTeX.Logging.getLogger('status').setLevel(getattr(logging, options.loglevel.upper())) + + destfile = string.Template(options.destfile).safe_substitute(problem=problembase) + imgbasedir = string.Template(options.imgbasedir).safe_substitute(problem=problembase) + + texfile = problem + # Set up template if necessary + with template.Template(problem, language=options.language) as templ: + texfile = open(templ.get_file_name(), 'r') + + # Setup parser and renderer etc + + # plasTeX version 3 changed the name of this argument (and guarding against this + # by checking plasTeX.__version__ fails on plastex v3.0 which failed to update + # __version__) + try: + tex = plasTeX.TeX.TeX(myfile=texfile) + except Exception: + tex = plasTeX.TeX.TeX(file=texfile) + + ProblemsetMacros.init(tex) + + tex.ownerDocument.config['general']['copy-theme-extras'] = options.css + if not options.headers: + tex.ownerDocument.userdata['noheaders'] = True + tex.ownerDocument.config['files']['filename'] = destfile + tex.ownerDocument.config['images']['filenames'] = 'img-$num(4)' + tex.ownerDocument.config['images']['enabled'] = False + tex.ownerDocument.config['images']['imager'] = 'none' + tex.ownerDocument.config['images']['base-url'] = imgbasedir + # tell plasTeX where to search for problemtools' built-in packages + tex.ownerDocument.config['general']['packages-dirs'] = [os.path.join(os.path.dirname(__file__), 'ProblemPlasTeX')] + + renderer = ProblemRenderer() + + if not options.quiet: + print('Parsing TeX source...') + doc = tex.parse() + texfile.close() + + + renderer.render(doc) + + # Annoying: I have not figured out any way of stopping the plasTeX + # renderer from generating a .paux file + if os.path.isfile('.paux'): + os.remove('.paux') diff --git a/problemtools/verifyproblem.py b/problemtools/verifyproblem.py index a45cbf9d..1db4a6e6 100644 --- a/problemtools/verifyproblem.py +++ b/problemtools/verifyproblem.py @@ -1119,12 +1119,14 @@ def __init__(self, problem: Problem): self._problem = problem self.languages = [] glob_path = os.path.join(problem.probdir, 'problem_statement', 'problem.') - if glob.glob(glob_path + 'tex'): - self.languages.append('') - for f in glob.glob(glob_path + '[a-z][a-z].tex'): - m = re.search("problem.([a-z][a-z]).tex$", f) - assert m - self.languages.append(m.group(1)) + for extension in problem2html.SUPPORTED_EXTENSIONS: + if glob.glob(glob_path + extension): + self.languages.append('') + for f in glob.glob(glob_path + '[a-z][a-z].%s' % extension): + lang = re.search("problem.([a-z][a-z]).%s$" % extension, f).group(1) + if lang in self.languages: + self.error('Language %s has several statement formats' % lang) + self.languages.append(lang) def check(self, context: Context) -> bool: if self._check_res is not None: @@ -1132,9 +1134,9 @@ def check(self, context: Context) -> bool: self._check_res = True if not self.languages: - self.error('No problem statements found (expected problem.tex or problem.[a-z][a-z].tex in problem_statement directory)') + self.error('No problem statements found (expected problem.{tex,md} or problem.[a-z][a-z].{tex,md} in problem_statement directory)') if '' in self.languages and 'en' in self.languages: - self.error("Can't supply both problem.tex and problem.en.tex") + self.error("Can't supply both problem.{tex,md} and problem.en.{tex,md}") for lang in self.languages: try: @@ -1143,7 +1145,7 @@ def check(self, context: Context) -> bool: options.language = lang options.nopdf = True options.quiet = True - if not problem2pdf.convert(options): + if not problem2pdf.convert(options, ignore_markdown=True): langparam = f' --language {lang}' if lang != '' else '' self.error(f'Could not compile problem statement for language "{lang}". Run problem2pdf{langparam} on the problem to diagnose.') except Exception as e: @@ -1165,21 +1167,24 @@ def __str__(self) -> str: def get_config(self) -> dict[str, dict[str, str]]: ret: dict[str, dict[str, str]] = {} - for lang in self.languages: - filename = f'problem.{lang}.tex' if lang != '' else 'problem.tex' - stmt = open(os.path.join(self._problem.probdir, 'problem_statement', filename)).read() - patterns = [ - (r'\\problemname{(.*)}', 'name'), - (r'^%%\s*plainproblemname:(.*)$', 'name'), - ] - for tup in patterns: - pattern = tup[0] - dest = tup[1] - hit = re.search(pattern, stmt, re.MULTILINE) - if hit: - if not dest in ret: - ret[dest] = {} - ret[dest][lang] = hit.group(1).strip() + for extension in problem2html.SUPPORTED_EXTENSIONS: + for lang in self.languages: + filename = f'problem.{lang}.{extension}' if lang != '' else 'problem.{extension}' + if not os.path.isfile(filename): + continue + stmt = open(os.path.join(self._problem.probdir, 'problem_statement', filename)).read() + patterns = [ + (r'\\problemname{(.*)}', 'name'), + (r'^%%\s*plainproblemname:(.*)$', 'name'), + ] + for tup in patterns: + pattern = tup[0] + dest = tup[1] + hit = re.search(pattern, stmt, re.MULTILINE) + if hit: + if not dest in ret: + ret[dest] = {} + ret[dest][lang] = hit.group(1).strip() return ret From 868eb39d990a6ebeba8cebbfc22cd587a38ce978 Mon Sep 17 00:00:00 2001 From: matistjati Date: Thu, 8 Aug 2024 01:10:58 +0200 Subject: [PATCH 02/25] Added display math --- examples/README.md | 5 +++-- problemtools/md2html.py | 19 +++++++++++++++---- .../templates/markdown/default-layout.html | 3 ++- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/examples/README.md b/examples/README.md index 9665f8a7..d646a86d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -28,5 +28,6 @@ different scores depending on which test groups they solve. It also demonstrates # bplusa -This is an example of a problem using an output validator, showcasing different things to keep in mind -when using output validator. It also demonstrates using Markdown as a statement language. +This is an example of a problem using an output validator, where there are multiple valid answers. +The output validator is written pretty generally, guarding against the most common mistakes when using +output validators. It also demonstrates using Markdown as a statement language. diff --git a/problemtools/md2html.py b/problemtools/md2html.py index 63b1f6f4..1349b206 100644 --- a/problemtools/md2html.py +++ b/problemtools/md2html.py @@ -118,7 +118,7 @@ def convert(problem: str, options: argparse.Namespace) -> None: with open(statement_path, "r", encoding="utf-8") as input_file: text = input_file.read() - statement_html = markdown.markdown(text, extensions=[InlineMathExtension(), "tables"]) + statement_html = markdown.markdown(text, extensions=[MathExtension(), "tables"]) templatepaths = [os.path.join(os.path.dirname(__file__), 'templates/markdown'), os.path.join(os.path.dirname(__file__), '../templates/markdown'), @@ -156,7 +156,18 @@ def handleMatch(self, m, data): el.text = "$" + m.group(1) + "$" return el, m.start(0), m.end(0) -class InlineMathExtension(Extension): +class DisplayMathProcessor(InlineProcessor): + def handleMatch(self, m, data): + el = etree.Element('div') + el.attrib['class'] = 'tex2jax_process' + el.text = "$$" + m.group(1) + "$$" + return el, m.start(0), m.end(0) + +class MathExtension(Extension): def extendMarkdown(self, md): - MATH_PATTERN = r'\$(.*?)\$' # like $1 + 2$ - md.inlinePatterns.register(InlineMathProcessor(MATH_PATTERN, md), 'inline-math', 200) \ No newline at end of file + # Regex magic so that both $ $ and $$ $$ can coexist + INLINE_MATH_PATTERN = r'(? Date: Thu, 8 Aug 2024 01:32:01 +0200 Subject: [PATCH 03/25] Add dependencies for markdown --- Dockerfile | 1 + README.md | 4 ++-- admin/docker/Dockerfile.minimal | 1 + debian/control | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index e9787418..daa50dde 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,7 @@ RUN apt-get update && \ python3-minimal \ python3-pip \ python3-plastex \ + python3-markdown \ python3-yaml \ sudo \ texlive-fonts-recommended \ diff --git a/README.md b/README.md index 601de517..499fe610 100644 --- a/README.md +++ b/README.md @@ -207,13 +207,13 @@ The dependencies needed to *build/install* problemtools can be installed with: And the dependencies needed to *run* problemtools can be installed with: - sudo apt install ghostscript libgmpxx4ldbl python3-minimal python-pkg-resources python3-plastex python3-yaml texlive-fonts-recommended texlive-lang-cyrillic texlive-latex-extra texlive-plain-generic tidy + sudo apt install ghostscript libgmpxx4ldbl python3-minimal python-pkg-resources python3-plastex python3-markdown python3-yaml texlive-fonts-recommended texlive-lang-cyrillic texlive-latex-extra texlive-plain-generic tidy ### Fedora On Fedora, these dependencies can be installed with: - sudo dnf install boost-regex gcc gmp-devel gmp-c++ python3 python3-pyyaml texlive-latex texlive-collection-fontsrecommended texlive-fancyhdr texlive-subfigure texlive-wrapfig texlive-import texlive-ulem texlive-xifthen texlive-overpic texlive-pbox tidy ghostscript + sudo dnf install boost-regex gcc gmp-devel gmp-c++ python3 python3-pyyaml python3-markdown texlive-latex texlive-collection-fontsrecommended texlive-fancyhdr texlive-subfigure texlive-wrapfig texlive-import texlive-ulem texlive-xifthen texlive-overpic texlive-pbox tidy ghostscript Followed by: diff --git a/admin/docker/Dockerfile.minimal b/admin/docker/Dockerfile.minimal index 534e661f..a44811f5 100644 --- a/admin/docker/Dockerfile.minimal +++ b/admin/docker/Dockerfile.minimal @@ -24,6 +24,7 @@ RUN apt update && \ python3-minimal \ python3-yaml \ python3-plastex \ + python3-markdown \ texlive-fonts-recommended \ texlive-lang-cyrillic \ texlive-latex-extra \ diff --git a/debian/control b/debian/control index 42797c8b..43410292 100644 --- a/debian/control +++ b/debian/control @@ -8,7 +8,7 @@ Homepage: https://github.com/Kattis/problemtools Package: kattis-problemtools Architecture: any -Depends: ${shlibs:Depends}, ${python3:Depends}, ${misc:Depends}, python3-plastex, python3-pkg-resources, texlive-plain-generic, texlive-fonts-recommended, texlive-latex-extra, texlive-lang-cyrillic, tidy, ghostscript, dvisvgm +Depends: ${shlibs:Depends}, ${python3:Depends}, ${misc:Depends}, python3-plastex, python3-markdown, python3-pkg-resources, texlive-plain-generic, texlive-fonts-recommended, texlive-latex-extra, texlive-lang-cyrillic, tidy, ghostscript, dvisvgm Recommends: gcc, g++ Description: Kattis Problem Tools These are tools to manage and verify problem packages in the From 05f6372cf850ef128e52b746687e1cc98c2b8ae0 Mon Sep 17 00:00:00 2001 From: matistjati Date: Thu, 8 Aug 2024 02:12:18 +0200 Subject: [PATCH 04/25] Style markdown tables --- examples/guess/problem.yaml | 2 + .../guess/problem_statement/problem.sv.md | 20 ++ examples/oddecho/problem.yaml | 4 +- .../oddecho/problem_statement/problem.sv.md | 27 ++ examples/problemset.cls | 257 ++++++++++++++++++ examples/tmpe5kbz3qn.tex | 6 + oddecho_html/index.html | 136 +++++++++ oddecho_html/problem.css | 105 +++++++ problemtools/md2html.py | 19 +- .../templates/markdown/default-layout.html | 2 +- problemtools/templates/markdown/problem.css | 15 + 11 files changed, 589 insertions(+), 4 deletions(-) create mode 100644 examples/guess/problem_statement/problem.sv.md create mode 100644 examples/oddecho/problem_statement/problem.sv.md create mode 100644 examples/problemset.cls create mode 100644 examples/tmpe5kbz3qn.tex create mode 100644 oddecho_html/index.html create mode 100644 oddecho_html/problem.css diff --git a/examples/guess/problem.yaml b/examples/guess/problem.yaml index fcb51934..c1e29500 100644 --- a/examples/guess/problem.yaml +++ b/examples/guess/problem.yaml @@ -2,6 +2,8 @@ source: Kattis license: cc by-sa validation: custom interactive +name: + sv: Gissa talet # Override standard limits: say that the TLE solutions provided should # be at least 4 times above the time limit in order for us to be diff --git a/examples/guess/problem_statement/problem.sv.md b/examples/guess/problem_statement/problem.sv.md new file mode 100644 index 00000000..c1edbd67 --- /dev/null +++ b/examples/guess/problem_statement/problem.sv.md @@ -0,0 +1,20 @@ +Jag tänker på ett hemligt tal mellan $1$ and $100$, kan du gissa vilket? +Givet en gissning kommer jag att berätta om din gissning +var för stor, för liten eller rätt. Du får bara $10$ gissningar, använd +dem klokt! + + +## Interaktion +Ditt program ska skriva ut gissningar om talet. +En gissning är en rad som enbart innehåller ett heltal mellan $1$ och $1000$. +Efter varje gissning måste du flusha standard out. + +Efter varje gissning kan du läs svaret på standard in. +Detta svar är ett av tre ord: + +- `lower` om talet jag tänker på är lägre än din gissning, +- `higher` om talet jag tänker på är högre än din gissning, eller +- `correct` om din gissning är korrekt. + +Efter att ha gissat rätt ska du avsluta ditt program. +Om du gissar fel $10$ gånger får du inga fler chanser och ditt program kommer avbrytas. diff --git a/examples/oddecho/problem.yaml b/examples/oddecho/problem.yaml index 1fcd5e21..f213fbd9 100644 --- a/examples/oddecho/problem.yaml +++ b/examples/oddecho/problem.yaml @@ -2,6 +2,8 @@ license: cc by-sa author: Johan Sannemo source: Principles of Algorithmic Problem Solving type: scoring -name: Echo +name: + en: Echo + sv: Eko grading: show_test_data_groups: true diff --git a/examples/oddecho/problem_statement/problem.sv.md b/examples/oddecho/problem_statement/problem.sv.md new file mode 100644 index 00000000..e0af2eea --- /dev/null +++ b/examples/oddecho/problem_statement/problem.sv.md @@ -0,0 +1,27 @@ +**ECHO! Echo! Ech...** + +Du älskar att skrika i grottor för att höra dina ord ekade tillbaka till dig. Tyvärr, som en hårt arbetande mjukvaruingenjör, är du inte tillräckligt lycklig för att komma ut och skrika i grottor så ofta. Istället skulle du vilja implementera ett program som fungerar som en ersättning för en grotta. + +Ibland vill du mata in några ord i programmet och få dem ekade tillbaka till dig. Men, som det är välkänt, om du skriker för snabbt i en grotta kan ekot störa de nya ord du säger. Mer specifikt, varje annat ord du säger kommer att störa ekot av ditt tidigare ord. Därför kommer endast det första, tredje, femte och så vidare ordet faktiskt att producera ett eko. + +Din uppgift är att skriva ett program som simulerar detta beteende. + +## Inmatning + +Den första raden av inmatningen innehåller ett heltal $N$ ($1 \le N \le 10$). + +De följande $N$ raderna innehåller vardera ett ord. Varje ord är högst $100$ bokstäver långt och innehåller endast bokstäverna `a-z`. + +## Utmatning + +Skriv ut de ord som har udda index (dvs. första, tredje, femte och så vidare) i inmatningen. + +## Bedömning + +Din lösning kommer att testas på en uppsättning testgrupper, där varje grupp är värd ett antal poäng. För att få poängen för en testgrupp måste du lösa alla testfall i den testgruppen. + +| Grupp | Poäng | Begränsningar | +|-------|-------|--------------------------| +| 1 | 1 | $N$ är alltid $5$ | +| 2 | 1 | Inga ytterligare begränsningar | + diff --git a/examples/problemset.cls b/examples/problemset.cls new file mode 100644 index 00000000..8501dea1 --- /dev/null +++ b/examples/problemset.cls @@ -0,0 +1,257 @@ +\NeedsTeXFormat{LaTeX2e} +\ProvidesClass{problemset}[2011/08/26 Problem Set For ACM-Style Programming Contests] + + +% Options to add: +% noproblemnumbers +% nosamplenumbers +% nopagenumbers +% nofooter +% noheader + +\newif\ifplastex +\plastexfalse + +\newif\if@footer\@footertrue +\DeclareOption{nofooter}{\@footerfalse} + +\newif\if@problemnumbers\@problemnumberstrue +\DeclareOption{noproblemnumbers}{\@problemnumbersfalse} + +\newif\if@clearevenpages\@clearevenpagestrue + +\DeclareOption{plainproblems}{ + \@footerfalse + \@problemnumbersfalse + \@clearevenpagesfalse +} + +%\DeclareOption{noproblemnumbers}{...} + +\DeclareOption*{\PassOptionsToClass{\CurrentOption}{article}} +\ProcessOptions\relax + +\LoadClass{article} + +\RequirePackage{times} % Font choice +\RequirePackage{amsmath} % AMS +\RequirePackage{amssymb} % AMS +\RequirePackage[OT2,T1]{fontenc} % Cyrillic and standard % TODO: make alphabet options more general +\RequirePackage[utf8]{inputenc} % UTF-8 support +\RequirePackage{fancyhdr} % Headers +\RequirePackage{graphicx} % Graphics +\RequirePackage{subfigure} % Subfigures +\RequirePackage{wrapfig} % Illustrations +\RequirePackage{import} % Proper file inclusion +\RequirePackage{verbatim} % For samples +\RequirePackage{fullpage} % Set up margins for full page +\RequirePackage{url} % Urls +\RequirePackage[colorlinks=true]{hyperref} +\RequirePackage{ulem} % \sout + + +%% Commands used to set name, logo, etc of contest +\newcommand*{\contestname}[1]{\def\@contestname{#1}} +\newcommand*{\contestshortname}[1]{\def\@contestshortname{#1}} +\newcommand*{\contestlogo}[1]{\def\@contestlogo{#1}} +\newcommand*{\headerlogo}[1]{\def\@headerlogo{#1}} +\newcommand*{\location}[1]{\def\@location{#1}} +\newcommand*{\licenseblurb}[1]{\def\@licenseblurb{#1}} +\newcommand*{\problemlanguage}[1]{\def\@problemlanguage{#1}} +\contestname{} +\contestshortname{} +\contestlogo{} +\headerlogo{} +\location{} +\licenseblurb{} +\problemlanguage{} + + + +% Typesetting sections in a problem + +\renewcommand\section{\@startsection{section}{1}{\z@}% + {-3.5ex \@plus -1ex \@minus -.2ex}% + {2.3ex \@plus.2ex}% + {\normalfont\large\sf\bfseries}} + +\renewcommand\subsection{\@startsection{subsection}{2}{\z@}% + {-3.25ex\@plus -1ex \@minus -.2ex}% + {1.5ex \@plus .2ex}% + {\normalfont\normalsize\sf\bfseries}} + +\renewcommand{\contentsname}{Problems} + + +% TODO: make last command of illustration/figure optional + +\newcommand{\illustration}[3]{ + \begin{wrapfigure}{r}{#1\textwidth} + \includegraphics[width=#1\textwidth]{#2} + \begin{flushright} + \vspace{-9pt} + \tiny #3 + \end{flushright} + \vspace{-15pt} + \end{wrapfigure} + \par + \noindent +} + + +%% Redefine cleardoublepage to put a text on even-numbered empty +%% pages. +\newcommand{\makeemptypage}{ + ~\thispagestyle{empty} + \vfill + \centerline{\Large \textsf{ This page is intentionally left (almost) blank.}} + \vfill + \clearpage +} +\renewcommand{\cleardoublepage}{ + \clearpage% + \ifodd\value{page}\else\makeemptypage\fi% +} + +\newcommand{\clearproblemsetpage}{ + \if@clearevenpages + \cleardoublepage + \else + \clearpage + \fi +} + + +%% Set up a problem counter and number problems A B C ... +\newcounter{problemcount} +\setcounter{problemcount}{0} +\newcommand{\problemnumber}{\Alph{problemcount}} + +%% Number figures as A.1 A.2... B.1 B.2... (except if we're converting to HTML) +\ifplastex\else +\renewcommand{\thefigure}{\problemnumber.\arabic{figure}} +\fi + + +%% Command for starting new problem + +%% Problem inclusion +\newcommand{\includeproblem}[3]{ + \startproblem{#1}{#2}{#3} + \import{#1/problem_statement/}{problem\@problemlanguage.tex} +} + +\newcommand{\startproblem}[3]{ + \clearproblemsetpage + \refstepcounter{problemcount} + \setcounter{samplenum}{0} + \setcounter{figure}{0}% + \def\@problemid{#1} + \def\@problemname{#2} + \def\@timelimit{#3} + \problemheader{\@problemname}{\@problemid} +} + +\newcommand{\problemheader}[2]{ + \begin{center} + \textsf{ + {\huge #1\\} + {\Large Problem ID: #2\\} + } + \end{center} +} + +%% Commands related to sample data + +%% Sample counter +\newcounter{samplenum} +\newcommand{\sampleid}{\arabic{samplenum}} + +%% Define the command used to give sample data +%% Takes filename as parameter + +\newcommand{\includesample}[1]{ + \displaysample{\@problemid/data/sample/#1} +} + +\newcommand{\displaysample}[1]{ + \IfFileExists{#1.in}{}{\ClassError{problemset}{Can't find file '#1.in'}{}} + \IfFileExists{#1.ans}{}{\ClassError{problemset}{Can't find file '#1.ans'}{}} + \refstepcounter{samplenum} + \par + \vspace{0.4cm} + \noindent + \sampletable + {Sample Input \sampleid}{#1.in} + {Sample Output \sampleid}{#1.ans} +} + +\newcommand{\sampletable}[4]{ + \begin{tabular}{|l|l|} + \multicolumn{1}{l}{\textsf{\textbf{#1}}} & + \multicolumn{1}{l}{\textsf{\textbf{#3}}} \\ + \hline + \parbox[t]{0.475\textwidth}{\vspace{-0.5cm}\verbatiminput{#2}} + & + \parbox[t]{0.475\textwidth}{\vspace{-0.5cm}\verbatiminput{#4}} + \\ + \hline + \end{tabular} +} + + +% Remaining part of file is headers and toc, not tested with plasTeX +% and should not be used in plastex mode +\ifplastex\else + + +%% Set up headers +\fancypagestyle{problem}{ + \fancyhf{} % Clear old junk +% \ifx \@headerlogo \@empty\relax \else +% \fancyhead[C]{ +% \includegraphics[scale=0.3]{\@headerlogo} +% } +% \fi + \if@footer + \fancyhead[L]{ + \emph{ + \@contestshortname{} + \if@problemnumbers Problem \problemnumber:{} \fi + \@problemname + } + } + \fancyhead[R]{\thepage} + \fancyfoot[L]{ + \emph{\@licenseblurb} + } +% \fancyfoot[R]{\includegraphics[scale=0.5]{cc-by-sa} } + \fi +} +\renewcommand{\headrulewidth}{0pt} +\pagestyle{problem} + +\AtBeginDocument{ + % FIXME: Figure out how to do this in a header-indep. way. +% \ifx \@headerlogo \@empty \relax\else + \addtolength{\headheight}{12pt} + \addtolength{\topmargin}{-30pt} + \addtolength{\textheight}{18pt} +% \fi + \setlength{\headsep}{25pt} +} + + +% Set up table of contents for cover page +\AtBeginDocument{ + \addtocontents{toc}{\protect\begin{tabular}{cl}} +} +\AtEndDocument{ + \clearproblemsetpage + % Annoyingly enough addtocontents won't work at end of doc + \immediate\write\@auxout{% + \string\@writefile{toc}{\string\end{tabular}}% + } +} + +\fi diff --git a/examples/tmpe5kbz3qn.tex b/examples/tmpe5kbz3qn.tex new file mode 100644 index 00000000..ba1c2e5b --- /dev/null +++ b/examples/tmpe5kbz3qn.tex @@ -0,0 +1,6 @@ +\documentclass[plainproblems]{problemset} + +\problemlanguage{.sv} + +\begin{document} + diff --git a/oddecho_html/index.html b/oddecho_html/index.html new file mode 100644 index 00000000..3e88327f --- /dev/null +++ b/oddecho_html/index.html @@ -0,0 +1,136 @@ + + + + + Echo + + + + + +
+

Echo

+

Problem ID: oddecho

+
+
+

ECHO! Echo! Ech...

+

Du älskar att skrika i grottor för att höra dina ord ekade + tillbaka till dig. Tyvärr, som en hårt arbetande + mjukvaruingenjör, är du inte tillräckligt lycklig för att komma + ut och skrika i grottor så ofta. Istället skulle du vilja + implementera ett program som fungerar som en ersättning för en + grotta.

+

Ibland vill du mata in några ord i programmet och få dem + ekade tillbaka till dig. Men, som det är välkänt, om du skriker + för snabbt i en grotta kan ekot störa de nya ord du säger. Mer + specifikt, varje annat ord du säger kommer att störa ekot av + ditt tidigare ord. Därför kommer endast det första, tredje, + femte och så vidare ordet faktiskt att producera ett eko.

+

Din uppgift är att skriva ett program som simulerar detta + beteende.

+

Inmatning

+

Den första raden av inmatningen innehåller ett heltal + $N$ ($1 \le N \le 10$).

+

De följande $N$ raderna + innehåller vardera ett ord. Varje ord är högst $100$ bokstäver långt och innehåller + endast bokstäverna a-z.

+

Utmatning

+

Skriv ut de ord som har udda index (dvs. första, tredje, + femte och så vidare) i inmatningen.

+

Bedömning

+

Din lösning kommer att testas på en uppsättning testgrupper, + där varje grupp är värd ett antal poäng. För att få poängen för + en testgrupp måste du lösa alla testfall i den testgruppen.

+ + + + + + + + + + + + + + + + + + + + +
GruppPoängBegränsningar
11$N$ är alltid + $5$
21Inga ytterligare begränsningar
+
+ + + + + + + + + + + + + + + + + + + +
Sample Input 1Sample Output 1
+
5
+hello
+i
+am
+an
+echo
+
+
+
hello
+am
+echo
+
+
Sample Input 2Sample Output 2
+
10
+only
+if
+these
+oddindexed
+words
+appear
+are
+you
+correct
+output
+
+
+
only
+these
+words
+are
+correct
+
+
+ + diff --git a/oddecho_html/problem.css b/oddecho_html/problem.css new file mode 100644 index 00000000..0b5be150 --- /dev/null +++ b/oddecho_html/problem.css @@ -0,0 +1,105 @@ +.problemheader { + text-align: center; +} + +.problembody { + font-family: 'Times New Roman', Georgia, serif; + font-size: 1.1em; + text-align: justify; + padding-top: 1.5em; +} + +.problembody h2, .problembody h3, .problembody table.sample th { + font-family: Arial, Helvetica, sans-serif; +} + +.markdown-table { + border-collapse: collapse; + width: 100%; +} + +.markdown-table th, .markdown-table td { + border: 1px solid black; + padding: 8px; + text-align: left; +} + +.markdown-table th { + background-color: #f2f2f2; +} + +div.minipage { + display: inline-block; +} + +div.illustration { + float: right; + padding-left: 20px; +} + +img.illustration { + width: 100%; +} + +div.figure { + display: block; + float: none; + margin-left: auto; + margin-right: auto; +} + +.illustration div.description { + font-size: 8pt; + text-align: right; +} + +.problembody p { + text-align: justify; +} + +td { + vertical-align:top; +} + +table, table td { + border: 0; +} + +table.tabular p { + margin: 0; +} + +table.sample { + width: 100%; +} + +table.sample th { + text-align: left; + width: 50%; +} + +table.sample td { + border: 1px solid black; +} + +div.sampleinteractionread { + border: 1px solid black; + width: 60%; + float: left; + margin: 3px 0px 3px 0px; +} + +.sampleinteractionread pre { + margin: 1px 5px 1px 5px; +} + +div.sampleinteractionwrite { + border: 1px solid black; + width: 60%; + float: right; + margin: 3px 0px 3px 0px; +} + +.sampleinteractionwrite pre { + margin: 1px 5px 1px 5px; +} \ No newline at end of file diff --git a/problemtools/md2html.py b/problemtools/md2html.py index 1349b206..6140d607 100644 --- a/problemtools/md2html.py +++ b/problemtools/md2html.py @@ -7,6 +7,7 @@ from typing import Optional import markdown +from markdown.treeprocessors import Treeprocessor from markdown.inlinepatterns import InlineProcessor from markdown.extensions import Extension import xml.etree.ElementTree as etree @@ -118,7 +119,7 @@ def convert(problem: str, options: argparse.Namespace) -> None: with open(statement_path, "r", encoding="utf-8") as input_file: text = input_file.read() - statement_html = markdown.markdown(text, extensions=[MathExtension(), "tables"]) + statement_html = markdown.markdown(text, extensions=[MathExtension(), AddClassExtension(), "tables"]) templatepaths = [os.path.join(os.path.dirname(__file__), 'templates/markdown'), os.path.join(os.path.dirname(__file__), '../templates/markdown'), @@ -148,7 +149,7 @@ def convert(problem: str, options: argparse.Namespace) -> None: with open(os.path.join(templatepath, "problem.css"), "r") as input_file: output_file.write(input_file.read()) - +# Parse inline math $a+b$ class InlineMathProcessor(InlineProcessor): def handleMatch(self, m, data): el = etree.Element('span') @@ -156,6 +157,7 @@ def handleMatch(self, m, data): el.text = "$" + m.group(1) + "$" return el, m.start(0), m.end(0) +# Parse display math $$a+b$$ class DisplayMathProcessor(InlineProcessor): def handleMatch(self, m, data): el = etree.Element('div') @@ -163,6 +165,7 @@ def handleMatch(self, m, data): el.text = "$$" + m.group(1) + "$$" return el, m.start(0), m.end(0) +# Add the display+inline math class MathExtension(Extension): def extendMarkdown(self, md): # Regex magic so that both $ $ and $$ $$ can coexist @@ -171,3 +174,15 @@ def extendMarkdown(self, md): md.inlinePatterns.register(DisplayMathProcessor(DISPLAY_MATH_PATTERN, md), 'display-math', 200) md.inlinePatterns.register(InlineMathProcessor(INLINE_MATH_PATTERN, md), 'inline-math', 201) + +# Add class markdown-table to all tables for easier styling +# (Otherwise, we will end up styling sample tables) +class AddClassTreeprocessor(Treeprocessor): + def run(self, root): + for table in root.findall(".//table"): + if 'class' not in table.attrib: + table.set('class', 'markdown-table') # Replace 'my-custom-class' with your desired class name + +class AddClassExtension(Extension): + def extendMarkdown(self, md): + md.treeprocessors.register(AddClassTreeprocessor(md), 'add_class', 15) diff --git a/problemtools/templates/markdown/default-layout.html b/problemtools/templates/markdown/default-layout.html index 0285e9d4..814324c1 100644 --- a/problemtools/templates/markdown/default-layout.html +++ b/problemtools/templates/markdown/default-layout.html @@ -33,4 +33,4 @@

Problem ID: %(problemid)s

- \ No newline at end of file + diff --git a/problemtools/templates/markdown/problem.css b/problemtools/templates/markdown/problem.css index 20448219..0b5be150 100644 --- a/problemtools/templates/markdown/problem.css +++ b/problemtools/templates/markdown/problem.css @@ -13,6 +13,21 @@ font-family: Arial, Helvetica, sans-serif; } +.markdown-table { + border-collapse: collapse; + width: 100%; +} + +.markdown-table th, .markdown-table td { + border: 1px solid black; + padding: 8px; + text-align: left; +} + +.markdown-table th { + background-color: #f2f2f2; +} + div.minipage { display: inline-block; } From 673773e4363d40b80bbf62feb149093d2ffaacfa Mon Sep 17 00:00:00 2001 From: matistjati Date: Thu, 8 Aug 2024 02:13:41 +0200 Subject: [PATCH 05/25] Remove temp files --- examples/problemset.cls | 257 --------------------------------------- examples/tmpe5kbz3qn.tex | 6 - oddecho_html/index.html | 136 --------------------- oddecho_html/problem.css | 105 ---------------- 4 files changed, 504 deletions(-) delete mode 100644 examples/problemset.cls delete mode 100644 examples/tmpe5kbz3qn.tex delete mode 100644 oddecho_html/index.html delete mode 100644 oddecho_html/problem.css diff --git a/examples/problemset.cls b/examples/problemset.cls deleted file mode 100644 index 8501dea1..00000000 --- a/examples/problemset.cls +++ /dev/null @@ -1,257 +0,0 @@ -\NeedsTeXFormat{LaTeX2e} -\ProvidesClass{problemset}[2011/08/26 Problem Set For ACM-Style Programming Contests] - - -% Options to add: -% noproblemnumbers -% nosamplenumbers -% nopagenumbers -% nofooter -% noheader - -\newif\ifplastex -\plastexfalse - -\newif\if@footer\@footertrue -\DeclareOption{nofooter}{\@footerfalse} - -\newif\if@problemnumbers\@problemnumberstrue -\DeclareOption{noproblemnumbers}{\@problemnumbersfalse} - -\newif\if@clearevenpages\@clearevenpagestrue - -\DeclareOption{plainproblems}{ - \@footerfalse - \@problemnumbersfalse - \@clearevenpagesfalse -} - -%\DeclareOption{noproblemnumbers}{...} - -\DeclareOption*{\PassOptionsToClass{\CurrentOption}{article}} -\ProcessOptions\relax - -\LoadClass{article} - -\RequirePackage{times} % Font choice -\RequirePackage{amsmath} % AMS -\RequirePackage{amssymb} % AMS -\RequirePackage[OT2,T1]{fontenc} % Cyrillic and standard % TODO: make alphabet options more general -\RequirePackage[utf8]{inputenc} % UTF-8 support -\RequirePackage{fancyhdr} % Headers -\RequirePackage{graphicx} % Graphics -\RequirePackage{subfigure} % Subfigures -\RequirePackage{wrapfig} % Illustrations -\RequirePackage{import} % Proper file inclusion -\RequirePackage{verbatim} % For samples -\RequirePackage{fullpage} % Set up margins for full page -\RequirePackage{url} % Urls -\RequirePackage[colorlinks=true]{hyperref} -\RequirePackage{ulem} % \sout - - -%% Commands used to set name, logo, etc of contest -\newcommand*{\contestname}[1]{\def\@contestname{#1}} -\newcommand*{\contestshortname}[1]{\def\@contestshortname{#1}} -\newcommand*{\contestlogo}[1]{\def\@contestlogo{#1}} -\newcommand*{\headerlogo}[1]{\def\@headerlogo{#1}} -\newcommand*{\location}[1]{\def\@location{#1}} -\newcommand*{\licenseblurb}[1]{\def\@licenseblurb{#1}} -\newcommand*{\problemlanguage}[1]{\def\@problemlanguage{#1}} -\contestname{} -\contestshortname{} -\contestlogo{} -\headerlogo{} -\location{} -\licenseblurb{} -\problemlanguage{} - - - -% Typesetting sections in a problem - -\renewcommand\section{\@startsection{section}{1}{\z@}% - {-3.5ex \@plus -1ex \@minus -.2ex}% - {2.3ex \@plus.2ex}% - {\normalfont\large\sf\bfseries}} - -\renewcommand\subsection{\@startsection{subsection}{2}{\z@}% - {-3.25ex\@plus -1ex \@minus -.2ex}% - {1.5ex \@plus .2ex}% - {\normalfont\normalsize\sf\bfseries}} - -\renewcommand{\contentsname}{Problems} - - -% TODO: make last command of illustration/figure optional - -\newcommand{\illustration}[3]{ - \begin{wrapfigure}{r}{#1\textwidth} - \includegraphics[width=#1\textwidth]{#2} - \begin{flushright} - \vspace{-9pt} - \tiny #3 - \end{flushright} - \vspace{-15pt} - \end{wrapfigure} - \par - \noindent -} - - -%% Redefine cleardoublepage to put a text on even-numbered empty -%% pages. -\newcommand{\makeemptypage}{ - ~\thispagestyle{empty} - \vfill - \centerline{\Large \textsf{ This page is intentionally left (almost) blank.}} - \vfill - \clearpage -} -\renewcommand{\cleardoublepage}{ - \clearpage% - \ifodd\value{page}\else\makeemptypage\fi% -} - -\newcommand{\clearproblemsetpage}{ - \if@clearevenpages - \cleardoublepage - \else - \clearpage - \fi -} - - -%% Set up a problem counter and number problems A B C ... -\newcounter{problemcount} -\setcounter{problemcount}{0} -\newcommand{\problemnumber}{\Alph{problemcount}} - -%% Number figures as A.1 A.2... B.1 B.2... (except if we're converting to HTML) -\ifplastex\else -\renewcommand{\thefigure}{\problemnumber.\arabic{figure}} -\fi - - -%% Command for starting new problem - -%% Problem inclusion -\newcommand{\includeproblem}[3]{ - \startproblem{#1}{#2}{#3} - \import{#1/problem_statement/}{problem\@problemlanguage.tex} -} - -\newcommand{\startproblem}[3]{ - \clearproblemsetpage - \refstepcounter{problemcount} - \setcounter{samplenum}{0} - \setcounter{figure}{0}% - \def\@problemid{#1} - \def\@problemname{#2} - \def\@timelimit{#3} - \problemheader{\@problemname}{\@problemid} -} - -\newcommand{\problemheader}[2]{ - \begin{center} - \textsf{ - {\huge #1\\} - {\Large Problem ID: #2\\} - } - \end{center} -} - -%% Commands related to sample data - -%% Sample counter -\newcounter{samplenum} -\newcommand{\sampleid}{\arabic{samplenum}} - -%% Define the command used to give sample data -%% Takes filename as parameter - -\newcommand{\includesample}[1]{ - \displaysample{\@problemid/data/sample/#1} -} - -\newcommand{\displaysample}[1]{ - \IfFileExists{#1.in}{}{\ClassError{problemset}{Can't find file '#1.in'}{}} - \IfFileExists{#1.ans}{}{\ClassError{problemset}{Can't find file '#1.ans'}{}} - \refstepcounter{samplenum} - \par - \vspace{0.4cm} - \noindent - \sampletable - {Sample Input \sampleid}{#1.in} - {Sample Output \sampleid}{#1.ans} -} - -\newcommand{\sampletable}[4]{ - \begin{tabular}{|l|l|} - \multicolumn{1}{l}{\textsf{\textbf{#1}}} & - \multicolumn{1}{l}{\textsf{\textbf{#3}}} \\ - \hline - \parbox[t]{0.475\textwidth}{\vspace{-0.5cm}\verbatiminput{#2}} - & - \parbox[t]{0.475\textwidth}{\vspace{-0.5cm}\verbatiminput{#4}} - \\ - \hline - \end{tabular} -} - - -% Remaining part of file is headers and toc, not tested with plasTeX -% and should not be used in plastex mode -\ifplastex\else - - -%% Set up headers -\fancypagestyle{problem}{ - \fancyhf{} % Clear old junk -% \ifx \@headerlogo \@empty\relax \else -% \fancyhead[C]{ -% \includegraphics[scale=0.3]{\@headerlogo} -% } -% \fi - \if@footer - \fancyhead[L]{ - \emph{ - \@contestshortname{} - \if@problemnumbers Problem \problemnumber:{} \fi - \@problemname - } - } - \fancyhead[R]{\thepage} - \fancyfoot[L]{ - \emph{\@licenseblurb} - } -% \fancyfoot[R]{\includegraphics[scale=0.5]{cc-by-sa} } - \fi -} -\renewcommand{\headrulewidth}{0pt} -\pagestyle{problem} - -\AtBeginDocument{ - % FIXME: Figure out how to do this in a header-indep. way. -% \ifx \@headerlogo \@empty \relax\else - \addtolength{\headheight}{12pt} - \addtolength{\topmargin}{-30pt} - \addtolength{\textheight}{18pt} -% \fi - \setlength{\headsep}{25pt} -} - - -% Set up table of contents for cover page -\AtBeginDocument{ - \addtocontents{toc}{\protect\begin{tabular}{cl}} -} -\AtEndDocument{ - \clearproblemsetpage - % Annoyingly enough addtocontents won't work at end of doc - \immediate\write\@auxout{% - \string\@writefile{toc}{\string\end{tabular}}% - } -} - -\fi diff --git a/examples/tmpe5kbz3qn.tex b/examples/tmpe5kbz3qn.tex deleted file mode 100644 index ba1c2e5b..00000000 --- a/examples/tmpe5kbz3qn.tex +++ /dev/null @@ -1,6 +0,0 @@ -\documentclass[plainproblems]{problemset} - -\problemlanguage{.sv} - -\begin{document} - diff --git a/oddecho_html/index.html b/oddecho_html/index.html deleted file mode 100644 index 3e88327f..00000000 --- a/oddecho_html/index.html +++ /dev/null @@ -1,136 +0,0 @@ - - - - - Echo - - - - - -
-

Echo

-

Problem ID: oddecho

-
-
-

ECHO! Echo! Ech...

-

Du älskar att skrika i grottor för att höra dina ord ekade - tillbaka till dig. Tyvärr, som en hårt arbetande - mjukvaruingenjör, är du inte tillräckligt lycklig för att komma - ut och skrika i grottor så ofta. Istället skulle du vilja - implementera ett program som fungerar som en ersättning för en - grotta.

-

Ibland vill du mata in några ord i programmet och få dem - ekade tillbaka till dig. Men, som det är välkänt, om du skriker - för snabbt i en grotta kan ekot störa de nya ord du säger. Mer - specifikt, varje annat ord du säger kommer att störa ekot av - ditt tidigare ord. Därför kommer endast det första, tredje, - femte och så vidare ordet faktiskt att producera ett eko.

-

Din uppgift är att skriva ett program som simulerar detta - beteende.

-

Inmatning

-

Den första raden av inmatningen innehåller ett heltal - $N$ ($1 \le N \le 10$).

-

De följande $N$ raderna - innehåller vardera ett ord. Varje ord är högst $100$ bokstäver långt och innehåller - endast bokstäverna a-z.

-

Utmatning

-

Skriv ut de ord som har udda index (dvs. första, tredje, - femte och så vidare) i inmatningen.

-

Bedömning

-

Din lösning kommer att testas på en uppsättning testgrupper, - där varje grupp är värd ett antal poäng. För att få poängen för - en testgrupp måste du lösa alla testfall i den testgruppen.

- - - - - - - - - - - - - - - - - - - - -
GruppPoängBegränsningar
11$N$ är alltid - $5$
21Inga ytterligare begränsningar
-
- - - - - - - - - - - - - - - - - - - -
Sample Input 1Sample Output 1
-
5
-hello
-i
-am
-an
-echo
-
-
-
hello
-am
-echo
-
-
Sample Input 2Sample Output 2
-
10
-only
-if
-these
-oddindexed
-words
-appear
-are
-you
-correct
-output
-
-
-
only
-these
-words
-are
-correct
-
-
- - diff --git a/oddecho_html/problem.css b/oddecho_html/problem.css deleted file mode 100644 index 0b5be150..00000000 --- a/oddecho_html/problem.css +++ /dev/null @@ -1,105 +0,0 @@ -.problemheader { - text-align: center; -} - -.problembody { - font-family: 'Times New Roman', Georgia, serif; - font-size: 1.1em; - text-align: justify; - padding-top: 1.5em; -} - -.problembody h2, .problembody h3, .problembody table.sample th { - font-family: Arial, Helvetica, sans-serif; -} - -.markdown-table { - border-collapse: collapse; - width: 100%; -} - -.markdown-table th, .markdown-table td { - border: 1px solid black; - padding: 8px; - text-align: left; -} - -.markdown-table th { - background-color: #f2f2f2; -} - -div.minipage { - display: inline-block; -} - -div.illustration { - float: right; - padding-left: 20px; -} - -img.illustration { - width: 100%; -} - -div.figure { - display: block; - float: none; - margin-left: auto; - margin-right: auto; -} - -.illustration div.description { - font-size: 8pt; - text-align: right; -} - -.problembody p { - text-align: justify; -} - -td { - vertical-align:top; -} - -table, table td { - border: 0; -} - -table.tabular p { - margin: 0; -} - -table.sample { - width: 100%; -} - -table.sample th { - text-align: left; - width: 50%; -} - -table.sample td { - border: 1px solid black; -} - -div.sampleinteractionread { - border: 1px solid black; - width: 60%; - float: left; - margin: 3px 0px 3px 0px; -} - -.sampleinteractionread pre { - margin: 1px 5px 1px 5px; -} - -div.sampleinteractionwrite { - border: 1px solid black; - width: 60%; - float: right; - margin: 3px 0px 3px 0px; -} - -.sampleinteractionwrite pre { - margin: 1px 5px 1px 5px; -} \ No newline at end of file From 1c64085aafc4b5b35e1d24d4685e17ba15720917 Mon Sep 17 00:00:00 2001 From: matistjati Date: Thu, 8 Aug 2024 02:37:33 +0200 Subject: [PATCH 06/25] Statement fix --- .../oddecho/problem_statement/problem.sv.md | 17 ++++++++++------- problemtools/md2html.py | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/examples/oddecho/problem_statement/problem.sv.md b/examples/oddecho/problem_statement/problem.sv.md index e0af2eea..52ebedf7 100644 --- a/examples/oddecho/problem_statement/problem.sv.md +++ b/examples/oddecho/problem_statement/problem.sv.md @@ -1,24 +1,27 @@ **ECHO! Echo! Ech...** -Du älskar att skrika i grottor för att höra dina ord ekade tillbaka till dig. Tyvärr, som en hårt arbetande mjukvaruingenjör, är du inte tillräckligt lycklig för att komma ut och skrika i grottor så ofta. Istället skulle du vilja implementera ett program som fungerar som en ersättning för en grotta. +Du älskar att skrika i grottor för att höra dina ord ekade tillbaka till dig. Tyvärr, som en hårt arbetande mjukvaruingenjör, har du +inte tid för att komma ut och skrika i grottor så ofta. Istället skulle du vilja implementera ett program som fungerar som en ersättning för en grotta. -Ibland vill du mata in några ord i programmet och få dem ekade tillbaka till dig. Men, som det är välkänt, om du skriker för snabbt i en grotta kan ekot störa de nya ord du säger. Mer specifikt, varje annat ord du säger kommer att störa ekot av ditt tidigare ord. Därför kommer endast det första, tredje, femte och så vidare ordet faktiskt att producera ett eko. +Ibland vill du mata in några ord i programmet och få dem ekade tillbaka till dig. Men, som det är välkänt, om du skriker för snabbt i en grotta kan ekot störa de nya ord du säger. Mer specifikt, vartannat ord du säger kommer att störa ekot av ditt tidigare ord. Därför kommer endast det första, tredje, femte och så vidare ordet faktiskt att producera ett eko. Din uppgift är att skriva ett program som simulerar detta beteende. -## Inmatning +## Indata -Den första raden av inmatningen innehåller ett heltal $N$ ($1 \le N \le 10$). +Den första raden av indata innehåller ett heltal $N$ ($1 \le N \le 10$). De följande $N$ raderna innehåller vardera ett ord. Varje ord är högst $100$ bokstäver långt och innehåller endast bokstäverna `a-z`. -## Utmatning +## Utdata Skriv ut de ord som har udda index (dvs. första, tredje, femte och så vidare) i inmatningen. -## Bedömning -Din lösning kommer att testas på en uppsättning testgrupper, där varje grupp är värd ett antal poäng. För att få poängen för en testgrupp måste du lösa alla testfall i den testgruppen. +## Poängsättning + +Din lösning kommer att testas på en mängd testfallsgrupper. +För att få poäng för en grupp så måste du klara alla testfall i gruppen. | Grupp | Poäng | Begränsningar | |-------|-------|--------------------------| diff --git a/problemtools/md2html.py b/problemtools/md2html.py index 6140d607..bc7ccc5c 100644 --- a/problemtools/md2html.py +++ b/problemtools/md2html.py @@ -168,7 +168,7 @@ def handleMatch(self, m, data): # Add the display+inline math class MathExtension(Extension): def extendMarkdown(self, md): - # Regex magic so that both $ $ and $$ $$ can coexist + # Regex magic so that both $ $ and $$ $$ can coexist. Written by a wizard (ChatGPT) INLINE_MATH_PATTERN = r'(? Date: Thu, 8 Aug 2024 02:59:22 +0200 Subject: [PATCH 07/25] Some refactoring --- problemtools/md2html.py | 125 +++++++++++++++++++---------------- problemtools/problem2html.py | 1 - problemtools/tex2html.py | 2 +- 3 files changed, 70 insertions(+), 58 deletions(-) diff --git a/problemtools/md2html.py b/problemtools/md2html.py index bc7ccc5c..5db0d727 100644 --- a/problemtools/md2html.py +++ b/problemtools/md2html.py @@ -6,18 +6,67 @@ import argparse from typing import Optional +import xml.etree.ElementTree as etree import markdown from markdown.treeprocessors import Treeprocessor from markdown.inlinepatterns import InlineProcessor from markdown.extensions import Extension -import xml.etree.ElementTree as etree from . import verifyproblem from . import problem2html + +def convert(problem: str, options: argparse.Namespace) -> None: + """Convert a Markdown statement to HTML + + Args: + problem: path to problem directory + options: command-line arguments. See problem2html.py + """ + problembase = os.path.splitext(os.path.basename(problem))[0] + destfile = string.Template(options.destfile).safe_substitute(problem=problembase) + + statement_path = problem2html._find_statement(problem, extension="md", language=options.language) + + if statement_path is None: + raise Exception('No markdown statement found') + + with open(statement_path, "r", encoding="utf-8") as input_file: + text = input_file.read() + statement_html = markdown.markdown(text, extensions=[MathExtension(), AddClassExtension(), "tables"]) + + templatepaths = [os.path.join(os.path.dirname(__file__), 'templates/markdown'), + os.path.join(os.path.dirname(__file__), '../templates/markdown'), + '/usr/lib/problemtools/templates/markdown'] + templatepath = next((p for p in templatepaths + if os.path.isdir(p) and os.path.isfile(os.path.join(p, "default-layout.html"))), + None) + + if templatepath is None: + raise Exception('Could not find directory with markdown templates') + + problem_name = _get_problem_name(problem) + + html_template = _substitute_template(templatepath, "default-layout.html", + statement_html=statement_html, + language=options.language, + title=problem_name or "Missing problem name", + problemid=problembase) + + html_template += _samples_to_html(problem) + + with open(destfile, "w", encoding="utf-8", errors="xmlcharrefreplace") as output_file: + output_file.write(html_template) + + if options.css: + with open("problem.css", "w") as output_file: + with open(os.path.join(templatepath, "problem.css"), "r") as input_file: + output_file.write(input_file.read()) + + def _substitute_template(templatepath: str, templatefile: str, **params) -> str: """Read the markdown template and substitute in things such as problem name, - statement etc using python's format syntax. + statement etc using python's format syntax. """ with open(os.path.join(templatepath, templatefile), "r", encoding="utf-8") as template_file: html_template = template_file.read() % params @@ -35,7 +84,7 @@ def _get_problem_name(problem: str, language: str = "en") -> Optional[str]: # If there is only one language, per the spec that is the one we want if len(names) == 1: return next(iter(names.values())) - + if language not in names: raise Exception(f"No problem name defined for language {language}") return names[language] @@ -50,13 +99,13 @@ def _samples_to_html(problem: str) -> str: casenum = 1 for sample in sorted(os.listdir(sample_path)): if sample.endswith(".interaction"): - lines = [""" + lines = [f"""
- + -
ReadSample Interaction {}Sample Interaction {casenum} Write
""".format(casenum)] + """] with open(os.path.join(sample_path, sample), "r", encoding="utf-8") as infile: sample_interaction = infile.readlines() for interaction in sample_interaction: @@ -84,7 +133,7 @@ def _samples_to_html(problem: str) -> str: with open(outpath, "r", encoding="utf-8") as outfile: sample_output = outfile.read() - samples.append(f""" + samples.append(""" Sample Input %(case)d Sample Output %(case)d @@ -92,63 +141,23 @@ def _samples_to_html(problem: str) -> str:
%(input)s
%(output)s
- """ % ({"case": casenum, "input": html.escape(sample_input), "output": html.escape(sample_output)})) + """ + % ({"case": casenum, "input": html.escape(sample_input), "output": html.escape(sample_output)})) casenum += 1 if interactive_samples: samples_html += ''.join(interactive_samples) if samples: - samples_html += """ + samples_html += f""" - %(samples)s + {''.join(samples)}
- """ % {"samples": ''.join(samples)} + """ return samples_html -def convert(problem: str, options: argparse.Namespace) -> None: - problembase = os.path.splitext(os.path.basename(problem))[0] - destfile = string.Template(options.destfile).safe_substitute(problem=problembase) - - statement_path = problem2html._find_statement(problem, extension="md", language=options.language) - - if statement_path is None: - raise Exception('No markdown statement found') - - with open(statement_path, "r", encoding="utf-8") as input_file: - text = input_file.read() - statement_html = markdown.markdown(text, extensions=[MathExtension(), AddClassExtension(), "tables"]) - - templatepaths = [os.path.join(os.path.dirname(__file__), 'templates/markdown'), - os.path.join(os.path.dirname(__file__), '../templates/markdown'), - '/usr/lib/problemtools/templates/markdown'] - templatepath = next((p for p in templatepaths - if os.path.isdir(p) and os.path.isfile(os.path.join(p, "default-layout.html"))), - None) - - if templatepath is None: - raise Exception('Could not find directory with markdown templates') - - problem_name = _get_problem_name(problem) - - html_template = _substitute_template(templatepath, "default-layout.html", - statement_html=statement_html, - language=options.language, - title=problem_name or "Missing problem name", - problemid=problembase) - - html_template += _samples_to_html(problem) - - with open(destfile, "w", encoding="utf-8", errors="xmlcharrefreplace") as output_file: - output_file.write(html_template) - - if options.css: - with open("problem.css", "w") as output_file: - with open(os.path.join(templatepath, "problem.css"), "r") as input_file: - output_file.write(input_file.read()) - # Parse inline math $a+b$ class InlineMathProcessor(InlineProcessor): def handleMatch(self, m, data): @@ -157,6 +166,7 @@ def handleMatch(self, m, data): el.text = "$" + m.group(1) + "$" return el, m.start(0), m.end(0) + # Parse display math $$a+b$$ class DisplayMathProcessor(InlineProcessor): def handleMatch(self, m, data): @@ -165,15 +175,17 @@ def handleMatch(self, m, data): el.text = "$$" + m.group(1) + "$$" return el, m.start(0), m.end(0) + # Add the display+inline math class MathExtension(Extension): def extendMarkdown(self, md): # Regex magic so that both $ $ and $$ $$ can coexist. Written by a wizard (ChatGPT) - INLINE_MATH_PATTERN = r'(? None: doc = tex.parse() texfile.close() - + renderer.render(doc) # Annoying: I have not figured out any way of stopping the plasTeX From 08645f58cacf8c80d9d994aa3b18d6863cc80230 Mon Sep 17 00:00:00 2001 From: matistjati Date: Fri, 9 Aug 2024 15:02:30 +0200 Subject: [PATCH 08/25] Added image support in markdown --- examples/README.md | 2 +- .../oddecho/problem_statement/echo_cave.webp | Bin 0 -> 19340 bytes .../oddecho/problem_statement/problem.en.tex | 2 + .../oddecho/problem_statement/problem.sv.md | 7 ++ problemtools/md2html.py | 89 ++++++++++++++++-- 5 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 examples/oddecho/problem_statement/echo_cave.webp diff --git a/examples/README.md b/examples/README.md index d646a86d..1aa6f8a2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -24,7 +24,7 @@ more than one language. ## oddecho This is an example of a *scoring* problem where submissions can get -different scores depending on which test groups they solve. It also demonstrates how an input validator might check different constraints for different test groups. +different scores depending on which test groups they solve. It also demonstrates how an input validator might check different constraints for different test groups. The swedish statement showcses how to use images in Markdown. # bplusa diff --git a/examples/oddecho/problem_statement/echo_cave.webp b/examples/oddecho/problem_statement/echo_cave.webp new file mode 100644 index 0000000000000000000000000000000000000000..8e79d2bc1596bf2cfcb1c50cf1c8b731beb99aea GIT binary patch literal 19340 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 LA=f6700000Kr61t literal 0 HcmV?d00001 diff --git a/examples/oddecho/problem_statement/problem.en.tex b/examples/oddecho/problem_statement/problem.en.tex index e4b03ab8..2505bb0e 100644 --- a/examples/oddecho/problem_statement/problem.en.tex +++ b/examples/oddecho/problem_statement/problem.en.tex @@ -12,6 +12,8 @@ Your task is to write a program that simulates this behavior. +\includegraphics[]{image.jpg} + \section*{Input} The first line of the input contains an integer \(N\) (\(1 \le N \le 10\)). diff --git a/examples/oddecho/problem_statement/problem.sv.md b/examples/oddecho/problem_statement/problem.sv.md index 52ebedf7..d51c5e13 100644 --- a/examples/oddecho/problem_statement/problem.sv.md +++ b/examples/oddecho/problem_statement/problem.sv.md @@ -1,5 +1,12 @@ **ECHO! Echo! Ech...** + +A cave + + Du älskar att skrika i grottor för att höra dina ord ekade tillbaka till dig. Tyvärr, som en hårt arbetande mjukvaruingenjör, har du inte tid för att komma ut och skrika i grottor så ofta. Istället skulle du vilja implementera ett program som fungerar som en ersättning för en grotta. diff --git a/problemtools/md2html.py b/problemtools/md2html.py index 5db0d727..b73ff4d9 100644 --- a/problemtools/md2html.py +++ b/problemtools/md2html.py @@ -4,6 +4,7 @@ import os.path import string import argparse +import re from typing import Optional import xml.etree.ElementTree as etree @@ -31,9 +32,14 @@ def convert(problem: str, options: argparse.Namespace) -> None: if statement_path is None: raise Exception('No markdown statement found') + seen_images = set() + call_handle = lambda src: _handle_image(os.path.join(problem, "problem_statement", src), seen_images) with open(statement_path, "r", encoding="utf-8") as input_file: text = input_file.read() - statement_html = markdown.markdown(text, extensions=[MathExtension(), AddClassExtension(), "tables"]) + statement_html = markdown.markdown(text, extensions=[MathExtension(), AddClassExtension(), "tables", + FixImageLinksExtension(call_handle)]) + + templatepaths = [os.path.join(os.path.dirname(__file__), 'templates/markdown'), os.path.join(os.path.dirname(__file__), '../templates/markdown'), @@ -64,6 +70,17 @@ def convert(problem: str, options: argparse.Namespace) -> None: output_file.write(input_file.read()) +def _handle_image(src, seen_images): + if src in seen_images: + return + if not os.path.isfile(src): + raise Exception(f"Could not find image {src} in problem_statement folder") + file_name = os.path.basename(src) + with open(src, "rb") as img: + with open(file_name, "wb") as out: + out.write(img.read()) + + def _substitute_template(templatepath: str, templatefile: str, **params) -> str: """Read the markdown template and substitute in things such as problem name, statement etc using python's format syntax. @@ -158,8 +175,8 @@ def _samples_to_html(problem: str) -> str: return samples_html -# Parse inline math $a+b$ class InlineMathProcessor(InlineProcessor): + """Tell mathjax to process all $a+b$""" def handleMatch(self, m, data): el = etree.Element('span') el.attrib['class'] = 'tex2jax_process' @@ -167,8 +184,8 @@ def handleMatch(self, m, data): return el, m.start(0), m.end(0) -# Parse display math $$a+b$$ class DisplayMathProcessor(InlineProcessor): + """Tell mathjax to process all $$a+b$$""" def handleMatch(self, m, data): el = etree.Element('div') el.attrib['class'] = 'tex2jax_process' @@ -176,8 +193,8 @@ def handleMatch(self, m, data): return el, m.start(0), m.end(0) -# Add the display+inline math class MathExtension(Extension): + """Add $a+b$ and $$a+b$$""" def extendMarkdown(self, md): # Regex magic so that both $ $ and $$ $$ can coexist. Written by a wizard (ChatGPT) inline_math_pattern = r'(? + + Implementation details: python-markdown seems to put both of these inside + html nodes' text, not as their own nodes. Therefore, we do a dfs and + use regex to extract them. + + """ + def __init__(self, md, callback): + super().__init__(md) + self.callback = callback + + def find_images(self, text: str) -> None: + """Find all images in a string and call the callback on each""" + if not text: + return + + # Find html-style images + html_img_pattern = re.compile(r']*?)>', re.IGNORECASE) + + html_src_pattern = re.compile(r'src\s*=\s*["\']([^"\']+)["\']', re.IGNORECASE) + for match in html_img_pattern.finditer(text): + img_attrs = match.group(1) + + src_match = html_src_pattern.search(img_attrs) + if src_match: + src_value = src_match.group(1) + self.callback(src_value) + + # Find markdown-style images + markdown_pattern = re.compile(r'!\[(.*?)\]\((.*?)\s*(?:"(.*?)")?\)') + + for match in markdown_pattern.finditer(text): + alt_text, src, title = match.groups() + self.callback(src) + + def dfs(self, element): + """Visit every html node and find any images contained in it""" + self.find_images(element.text) + for child in element: + self.dfs(child) + + def run(self, root): + self.dfs(root) + +class FixImageLinksExtension(Extension): + """Add FixImageLinks extension""" + def __init__(self, callback): + super().__init__() + self.callback = callback + + def extendMarkdown(self, md): + md.treeprocessors.register(FixImageLinks(md, self.callback), 'find_images', 200) From a6a19330156ca6ad100d71d55d841dfe02225957 Mon Sep 17 00:00:00 2001 From: matistjati Date: Fri, 9 Aug 2024 15:16:26 +0200 Subject: [PATCH 09/25] Added footnote support --- examples/README.md | 3 ++- examples/oddecho/problem_statement/problem.sv.md | 3 ++- problemtools/md2html.py | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/README.md b/examples/README.md index 1aa6f8a2..d1076a7e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -24,7 +24,8 @@ more than one language. ## oddecho This is an example of a *scoring* problem where submissions can get -different scores depending on which test groups they solve. It also demonstrates how an input validator might check different constraints for different test groups. The swedish statement showcses how to use images in Markdown. +different scores depending on which test groups they solve. It also demonstrates how an input validator might check different constraints for different test groups. The swedish statement showcases how to use images, footnotes +and tables in Markdown. # bplusa diff --git a/examples/oddecho/problem_statement/problem.sv.md b/examples/oddecho/problem_statement/problem.sv.md index d51c5e13..09d4cea0 100644 --- a/examples/oddecho/problem_statement/problem.sv.md +++ b/examples/oddecho/problem_statement/problem.sv.md @@ -10,7 +10,7 @@ Alternatively, Du älskar att skrika i grottor för att höra dina ord ekade tillbaka till dig. Tyvärr, som en hårt arbetande mjukvaruingenjör, har du inte tid för att komma ut och skrika i grottor så ofta. Istället skulle du vilja implementera ett program som fungerar som en ersättning för en grotta. -Ibland vill du mata in några ord i programmet och få dem ekade tillbaka till dig. Men, som det är välkänt, om du skriker för snabbt i en grotta kan ekot störa de nya ord du säger. Mer specifikt, vartannat ord du säger kommer att störa ekot av ditt tidigare ord. Därför kommer endast det första, tredje, femte och så vidare ordet faktiskt att producera ett eko. +Ibland vill du mata in några ord i programmet och få dem ekade tillbaka till dig. Men, som det är välkänt, om du skriker för snabbt i en grotta kan ekot störa de nya ord du säger. [^1] Mer specifikt, vartannat ord du säger kommer att störa ekot av ditt tidigare ord. Därför kommer endast det första, tredje, femte och så vidare ordet faktiskt att producera ett eko. Din uppgift är att skriva ett program som simulerar detta beteende. @@ -35,3 +35,4 @@ För att få poäng för en grupp så måste du klara alla testfall i gruppen. | 1 | 1 | $N$ är alltid $5$ | | 2 | 1 | Inga ytterligare begränsningar | +[^1]: [https://sv.wikipedia.org/wiki/Interferens_(v%C3%A5gr%C3%B6relse)](https://sv.wikipedia.org/wiki/Interferens_(v%C3%A5gr%C3%B6relse)) diff --git a/problemtools/md2html.py b/problemtools/md2html.py index b73ff4d9..b7aad19a 100644 --- a/problemtools/md2html.py +++ b/problemtools/md2html.py @@ -36,8 +36,9 @@ def convert(problem: str, options: argparse.Namespace) -> None: call_handle = lambda src: _handle_image(os.path.join(problem, "problem_statement", src), seen_images) with open(statement_path, "r", encoding="utf-8") as input_file: text = input_file.read() - statement_html = markdown.markdown(text, extensions=[MathExtension(), AddClassExtension(), "tables", - FixImageLinksExtension(call_handle)]) + statement_html = markdown.markdown(text, extensions=[MathExtension(), AddClassExtension(), + FixImageLinksExtension(call_handle), + 'footnotes', "tables"]) From 7627c58bc98f3e606065f9d30ba751718dc2b660 Mon Sep 17 00:00:00 2001 From: matistjati Date: Fri, 9 Aug 2024 15:26:52 +0200 Subject: [PATCH 10/25] Code cleanup --- problemtools/md2html.py | 35 +++++++++++++++++++++-------------- problemtools/problem2html.py | 4 ++-- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/problemtools/md2html.py b/problemtools/md2html.py index b7aad19a..334bbea1 100644 --- a/problemtools/md2html.py +++ b/problemtools/md2html.py @@ -27,21 +27,20 @@ def convert(problem: str, options: argparse.Namespace) -> None: problembase = os.path.splitext(os.path.basename(problem))[0] destfile = string.Template(options.destfile).safe_substitute(problem=problembase) - statement_path = problem2html._find_statement(problem, extension="md", language=options.language) + statement_path = problem2html.find_statement(problem, extension="md", language=options.language) if statement_path is None: raise Exception('No markdown statement found') - seen_images = set() - call_handle = lambda src: _handle_image(os.path.join(problem, "problem_statement", src), seen_images) + # The extension will only call _handle_image with the image name. We also need the path + # to the statement folder. We capture that with this lambda + call_handle = lambda src: _copy_image(os.path.join(problem, "problem_statement", src)) with open(statement_path, "r", encoding="utf-8") as input_file: text = input_file.read() statement_html = markdown.markdown(text, extensions=[MathExtension(), AddClassExtension(), FixImageLinksExtension(call_handle), 'footnotes', "tables"]) - - templatepaths = [os.path.join(os.path.dirname(__file__), 'templates/markdown'), os.path.join(os.path.dirname(__file__), '../templates/markdown'), '/usr/lib/problemtools/templates/markdown'] @@ -71,12 +70,20 @@ def convert(problem: str, options: argparse.Namespace) -> None: output_file.write(input_file.read()) -def _handle_image(src, seen_images): - if src in seen_images: - return +def _copy_image(src: str) -> None: + """This is called for every image in the statement + Copies the image to the output directory from the statement + + Args: + src: full file path to the image + """ + if not os.path.isfile(src): raise Exception(f"Could not find image {src} in problem_statement folder") file_name = os.path.basename(src) + # No point in copying it twice + if os.path.isfile(file_name): + return with open(src, "rb") as img: with open(file_name, "wb") as out: out.write(img.read()) @@ -226,11 +233,11 @@ class FixImageLinks(Treeprocessor): If your image name is image.jpg, we consider the following to be reasonable ![Alt](image.jpg) - + Implementation details: python-markdown seems to put both of these inside html nodes' text, not as their own nodes. Therefore, we do a dfs and use regex to extract them. - + """ def __init__(self, md, callback): super().__init__(md) @@ -240,24 +247,24 @@ def find_images(self, text: str) -> None: """Find all images in a string and call the callback on each""" if not text: return - + # Find html-style images html_img_pattern = re.compile(r']*?)>', re.IGNORECASE) html_src_pattern = re.compile(r'src\s*=\s*["\']([^"\']+)["\']', re.IGNORECASE) for match in html_img_pattern.finditer(text): img_attrs = match.group(1) - + src_match = html_src_pattern.search(img_attrs) if src_match: src_value = src_match.group(1) self.callback(src_value) - + # Find markdown-style images markdown_pattern = re.compile(r'!\[(.*?)\]\((.*?)\s*(?:"(.*?)")?\)') for match in markdown_pattern.finditer(text): - alt_text, src, title = match.groups() + _, src, __ = match.groups() self.callback(src) def dfs(self, element): diff --git a/problemtools/problem2html.py b/problemtools/problem2html.py index e1380e64..4c084613 100644 --- a/problemtools/problem2html.py +++ b/problemtools/problem2html.py @@ -12,7 +12,7 @@ SUPPORTED_EXTENSIONS = ("tex", "md") -def _find_statement(problem: str, extension: str, language: Optional[str]) -> Optional[str]: +def find_statement(problem: str, extension: str, language: Optional[str]) -> Optional[str]: """Finds the "best" statement for given language and extension""" if language is None: statement_path = os.path.join(problem, f"problem_statement/problem.en.{extension}") @@ -32,7 +32,7 @@ def _find_statement_extension(problem: str, language: Optional[str]) -> str: """Given a language, find whether the extension is tex or md""" extensions = [] for ext in SUPPORTED_EXTENSIONS: - if _find_statement(problem, ext, language) is not None: + if find_statement(problem, ext, language) is not None: extensions.append(ext) # At most one extension per language to avoid arbitrary/hidden priorities if len(extensions) > 1: From 1b222ac332d1f4be06a6b2821a3dae950689808b Mon Sep 17 00:00:00 2001 From: matistjati Date: Tue, 13 Aug 2024 13:19:37 +0200 Subject: [PATCH 11/25] md -> html works --- .../oddecho/problem_statement/echo_cave.jpg | Bin 0 -> 35667 bytes .../oddecho/problem_statement/echo_cave.webp | Bin 19340 -> 0 bytes .../oddecho/problem_statement/problem.en.tex | 2 - .../oddecho/problem_statement/problem.sv.md | 11 +- problemtools/md2html.py | 187 ++++++------------ problemtools/templates/markdown/problem.css | 54 ++--- 6 files changed, 101 insertions(+), 153 deletions(-) create mode 100644 examples/oddecho/problem_statement/echo_cave.jpg delete mode 100644 examples/oddecho/problem_statement/echo_cave.webp diff --git a/examples/oddecho/problem_statement/echo_cave.jpg b/examples/oddecho/problem_statement/echo_cave.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e197bf1b4a2e9d2d2782e29680869536a27684b1 GIT binary patch literal 35667 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 literal 0 HcmV?d00001 diff --git a/examples/oddecho/problem_statement/echo_cave.webp b/examples/oddecho/problem_statement/echo_cave.webp deleted file mode 100644 index 8e79d2bc1596bf2cfcb1c50cf1c8b731beb99aea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19340 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 LA=f6700000Kr61t diff --git a/examples/oddecho/problem_statement/problem.en.tex b/examples/oddecho/problem_statement/problem.en.tex index 2505bb0e..e4b03ab8 100644 --- a/examples/oddecho/problem_statement/problem.en.tex +++ b/examples/oddecho/problem_statement/problem.en.tex @@ -12,8 +12,6 @@ Your task is to write a program that simulates this behavior. -\includegraphics[]{image.jpg} - \section*{Input} The first line of the input contains an integer \(N\) (\(1 \le N \le 10\)). diff --git a/examples/oddecho/problem_statement/problem.sv.md b/examples/oddecho/problem_statement/problem.sv.md index 09d4cea0..9d2e236c 100644 --- a/examples/oddecho/problem_statement/problem.sv.md +++ b/examples/oddecho/problem_statement/problem.sv.md @@ -1,10 +1,9 @@ **ECHO! Echo! Ech...** - -A cave - Du älskar att skrika i grottor för att höra dina ord ekade tillbaka till dig. Tyvärr, som en hårt arbetande mjukvaruingenjör, har du @@ -35,4 +34,4 @@ För att få poäng för en grupp så måste du klara alla testfall i gruppen. | 1 | 1 | $N$ är alltid $5$ | | 2 | 1 | Inga ytterligare begränsningar | -[^1]: [https://sv.wikipedia.org/wiki/Interferens_(v%C3%A5gr%C3%B6relse)](https://sv.wikipedia.org/wiki/Interferens_(v%C3%A5gr%C3%B6relse)) +[^1]: [https://sv.wikipedia.org/wiki/Interferens](https://sv.wikipedia.org/wiki/Interferens) diff --git a/problemtools/md2html.py b/problemtools/md2html.py index 334bbea1..8505704c 100644 --- a/problemtools/md2html.py +++ b/problemtools/md2html.py @@ -5,18 +5,15 @@ import string import argparse import re +import json from typing import Optional -import xml.etree.ElementTree as etree -import markdown -from markdown.treeprocessors import Treeprocessor -from markdown.inlinepatterns import InlineProcessor -from markdown.extensions import Extension - from . import verifyproblem from . import problem2html +FOOTNOTES_STRING = '
' + def convert(problem: str, options: argparse.Namespace) -> None: """Convert a Markdown statement to HTML @@ -32,14 +29,13 @@ def convert(problem: str, options: argparse.Namespace) -> None: if statement_path is None: raise Exception('No markdown statement found') - # The extension will only call _handle_image with the image name. We also need the path - # to the statement folder. We capture that with this lambda - call_handle = lambda src: _copy_image(os.path.join(problem, "problem_statement", src)) - with open(statement_path, "r", encoding="utf-8") as input_file: - text = input_file.read() - statement_html = markdown.markdown(text, extensions=[MathExtension(), AddClassExtension(), - FixImageLinksExtension(call_handle), - 'footnotes', "tables"]) + if not os.path.isfile(statement_path): + raise Exception(f"Error! {statement_path} is not a file") + + + _copy_images(statement_path, + lambda img_name: handle_image(os.path.join(problem, "problem_statement", img_name))) + statement_html = os.popen(f"pandoc {statement_path} -t html").read() templatepaths = [os.path.join(os.path.dirname(__file__), 'templates/markdown'), os.path.join(os.path.dirname(__file__), '../templates/markdown'), @@ -59,7 +55,10 @@ def convert(problem: str, options: argparse.Namespace) -> None: title=problem_name or "Missing problem name", problemid=problembase) - html_template += _samples_to_html(problem) + samples = _samples_to_html(problem) + + html_template = inject_samples(html_template, samples) + html_template = replace_hr_in_footnotes(html_template) with open(destfile, "w", encoding="utf-8", errors="xmlcharrefreplace") as output_file: output_file.write(html_template) @@ -70,18 +69,17 @@ def convert(problem: str, options: argparse.Namespace) -> None: output_file.write(input_file.read()) -def _copy_image(src: str) -> None: +def handle_image(src: str) -> None: """This is called for every image in the statement - Copies the image to the output directory from the statement + Copies the image from the statement to the output directory Args: src: full file path to the image """ + file_name = os.path.basename(src) if not os.path.isfile(src): - raise Exception(f"Could not find image {src} in problem_statement folder") - file_name = os.path.basename(src) - # No point in copying it twice + raise Exception(f"File {file_name} not found in problem_statement") if os.path.isfile(file_name): return with open(src, "rb") as img: @@ -89,6 +87,53 @@ def _copy_image(src: str) -> None: out.write(img.read()) +def json_dfs(data, callback) -> None: + if isinstance(data, dict): + for key, value in data.items(): + # Markdown-style images + if key == 't' and value == 'Image': + callback(data['c'][2][0]) + else: + json_dfs(value, callback) + + # HTML-style images + if key == "t" and value == "RawInline": + image_string = data["c"][1] + src = re.search(r'src=["\'](.*?)["\']', image_string) + if src: + callback(src.group(1)) + + elif isinstance(data, list): + for item in data: + json_dfs(item, callback) + + +def _copy_images(statement_path, callback): + statement_json = os.popen(f"pandoc {statement_path} -t json").read() + json_dfs(json.loads(statement_json), callback) + + +def inject_samples(html, samples): + if FOOTNOTES_STRING in html: + pos = html.find(FOOTNOTES_STRING) + else: + pos = html.find("") + html = html[:pos] + samples + html[pos:] + return html + + +def replace_hr_in_footnotes(html_content): + if not FOOTNOTES_STRING in html_content: + return html_content + footnotes = html_content.find(FOOTNOTES_STRING) + hr_pos = html_content.find("
", footnotes) + return html_content[:hr_pos] + """ +

+ Footnotes +

+""" + html_content[6 + hr_pos:] + + def _substitute_template(templatepath: str, templatefile: str, **params) -> str: """Read the markdown template and substitute in things such as problem name, statement etc using python's format syntax. @@ -182,105 +227,3 @@ def _samples_to_html(problem: str) -> str: """ return samples_html - -class InlineMathProcessor(InlineProcessor): - """Tell mathjax to process all $a+b$""" - def handleMatch(self, m, data): - el = etree.Element('span') - el.attrib['class'] = 'tex2jax_process' - el.text = "$" + m.group(1) + "$" - return el, m.start(0), m.end(0) - - -class DisplayMathProcessor(InlineProcessor): - """Tell mathjax to process all $$a+b$$""" - def handleMatch(self, m, data): - el = etree.Element('div') - el.attrib['class'] = 'tex2jax_process' - el.text = "$$" + m.group(1) + "$$" - return el, m.start(0), m.end(0) - - -class MathExtension(Extension): - """Add $a+b$ and $$a+b$$""" - def extendMarkdown(self, md): - # Regex magic so that both $ $ and $$ $$ can coexist. Written by a wizard (ChatGPT) - inline_math_pattern = r'(? - - Implementation details: python-markdown seems to put both of these inside - html nodes' text, not as their own nodes. Therefore, we do a dfs and - use regex to extract them. - - """ - def __init__(self, md, callback): - super().__init__(md) - self.callback = callback - - def find_images(self, text: str) -> None: - """Find all images in a string and call the callback on each""" - if not text: - return - - # Find html-style images - html_img_pattern = re.compile(r']*?)>', re.IGNORECASE) - - html_src_pattern = re.compile(r'src\s*=\s*["\']([^"\']+)["\']', re.IGNORECASE) - for match in html_img_pattern.finditer(text): - img_attrs = match.group(1) - - src_match = html_src_pattern.search(img_attrs) - if src_match: - src_value = src_match.group(1) - self.callback(src_value) - - # Find markdown-style images - markdown_pattern = re.compile(r'!\[(.*?)\]\((.*?)\s*(?:"(.*?)")?\)') - - for match in markdown_pattern.finditer(text): - _, src, __ = match.groups() - self.callback(src) - - def dfs(self, element): - """Visit every html node and find any images contained in it""" - self.find_images(element.text) - for child in element: - self.dfs(child) - - def run(self, root): - self.dfs(root) - -class FixImageLinksExtension(Extension): - """Add FixImageLinks extension""" - def __init__(self, callback): - super().__init__() - self.callback = callback - - def extendMarkdown(self, md): - md.treeprocessors.register(FixImageLinks(md, self.callback), 'find_images', 200) diff --git a/problemtools/templates/markdown/problem.css b/problemtools/templates/markdown/problem.css index 0b5be150..66c5dc95 100644 --- a/problemtools/templates/markdown/problem.css +++ b/problemtools/templates/markdown/problem.css @@ -13,23 +13,50 @@ font-family: Arial, Helvetica, sans-serif; } -.markdown-table { +table { border-collapse: collapse; width: 100%; } -.markdown-table th, .markdown-table td { +table th, table td { border: 1px solid black; padding: 8px; text-align: left; } -.markdown-table th { +table th { background-color: #f2f2f2; } +.sample { + font-family: Arial, Helvetica, sans-serif; +} + +.sample td { + font-size: 13px; +} + +.sample { + border-collapse: separate; + width: 100%; +} + +.sample th { + padding: 0px; + border: 0px; + background-color: #ffffff; + text-align: left; + width: 50%; + font-size: 16px; + font-family: Arial, Helvetica, sans-serif; +} + +.sample td { + border: 1px solid black; +} + div.minipage { - display: inline-block; + display: inline-block; } div.illustration { @@ -61,26 +88,7 @@ td { vertical-align:top; } -table, table td { - border: 0; -} -table.tabular p { - margin: 0; -} - -table.sample { - width: 100%; -} - -table.sample th { - text-align: left; - width: 50%; -} - -table.sample td { - border: 1px solid black; -} div.sampleinteractionread { border: 1px solid black; From 712ce3edec968a25ec3196f6f84ef19eea1626f4 Mon Sep 17 00:00:00 2001 From: matistjati Date: Sat, 17 Aug 2024 01:18:41 +0200 Subject: [PATCH 12/25] Make md styling more constistent with latex --- problemtools/templates/markdown/problem.css | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/problemtools/templates/markdown/problem.css b/problemtools/templates/markdown/problem.css index 66c5dc95..8d354eca 100644 --- a/problemtools/templates/markdown/problem.css +++ b/problemtools/templates/markdown/problem.css @@ -13,31 +13,31 @@ font-family: Arial, Helvetica, sans-serif; } -table { +/*Style all tables except sample*/ +table:not(.sample) { border-collapse: collapse; - width: 100%; } -table th, table td { - border: 1px solid black; - padding: 8px; +table:not(.sample) td, table:not(.sample) th { + border-top-style: solid; + border-top-color: black; + border-top-width: 1px; text-align: left; + border-right: 1px solid black; + border-left: 1px solid black; + border-bottom: 1px solid black; } -table th { - background-color: #f2f2f2; +table:not(.sample) td { + margin: 0px; } +/*Style sample in its own way*/ .sample { font-family: Arial, Helvetica, sans-serif; } -.sample td { - font-size: 13px; -} - .sample { - border-collapse: separate; width: 100%; } From 11a2e4c1cafe6911cffd9275e27a8decd41a1c79 Mon Sep 17 00:00:00 2001 From: matistjati Date: Sat, 17 Aug 2024 17:12:02 +0200 Subject: [PATCH 13/25] md->pdf and Reorganize code --- .../oddecho/problem_statement/problem.sv.md | 6 +- problemtools/md2html.py | 102 +------------- problemtools/problem2html.py | 37 +---- problemtools/problem2pdf.py | 85 +++++++----- problemtools/statement_common.py | 130 ++++++++++++++++++ problemtools/verifyproblem.py | 7 +- 6 files changed, 196 insertions(+), 171 deletions(-) create mode 100644 problemtools/statement_common.py diff --git a/examples/oddecho/problem_statement/problem.sv.md b/examples/oddecho/problem_statement/problem.sv.md index 9d2e236c..4ffd89cf 100644 --- a/examples/oddecho/problem_statement/problem.sv.md +++ b/examples/oddecho/problem_statement/problem.sv.md @@ -1,10 +1,6 @@ -**ECHO! Echo! Ech...** +**EKO! Eko! Ek...** ![](echo_cave.jpg) - Du älskar att skrika i grottor för att höra dina ord ekade tillbaka till dig. Tyvärr, som en hårt arbetande mjukvaruingenjör, har du inte tid för att komma ut och skrika i grottor så ofta. Istället skulle du vilja implementera ett program som fungerar som en ersättning för en grotta. diff --git a/problemtools/md2html.py b/problemtools/md2html.py index 8505704c..68b967ae 100644 --- a/problemtools/md2html.py +++ b/problemtools/md2html.py @@ -8,8 +8,7 @@ import json from typing import Optional -from . import verifyproblem -from . import problem2html +from . import statement_common FOOTNOTES_STRING = '
' @@ -24,7 +23,7 @@ def convert(problem: str, options: argparse.Namespace) -> None: problembase = os.path.splitext(os.path.basename(problem))[0] destfile = string.Template(options.destfile).safe_substitute(problem=problembase) - statement_path = problem2html.find_statement(problem, extension="md", language=options.language) + statement_path = statement_common.find_statement(problem, extension="md", language=options.language) if statement_path is None: raise Exception('No markdown statement found') @@ -47,7 +46,7 @@ def convert(problem: str, options: argparse.Namespace) -> None: if templatepath is None: raise Exception('Could not find directory with markdown templates') - problem_name = _get_problem_name(problem) + problem_name = statement_common.get_problem_name(problem, options.language) html_template = _substitute_template(templatepath, "default-layout.html", statement_html=statement_html, @@ -55,7 +54,7 @@ def convert(problem: str, options: argparse.Namespace) -> None: title=problem_name or "Missing problem name", problemid=problembase) - samples = _samples_to_html(problem) + samples = statement_common.samples_to_html(problem) html_template = inject_samples(html_template, samples) html_template = replace_hr_in_footnotes(html_template) @@ -88,6 +87,7 @@ def handle_image(src: str) -> None: def json_dfs(data, callback) -> None: + """Traverse all items in a JSON tree, find all images, and call callback for each one""" if isinstance(data, dict): for key, value in data.items(): # Markdown-style images @@ -96,13 +96,6 @@ def json_dfs(data, callback) -> None: else: json_dfs(value, callback) - # HTML-style images - if key == "t" and value == "RawInline": - image_string = data["c"][1] - src = re.search(r'src=["\'](.*?)["\']', image_string) - if src: - callback(src.group(1)) - elif isinstance(data, list): for item in data: json_dfs(item, callback) @@ -142,88 +135,3 @@ def _substitute_template(templatepath: str, templatefile: str, **params) -> str: html_template = template_file.read() % params return html_template - -def _get_problem_name(problem: str, language: str = "en") -> Optional[str]: - """Load problem.yaml to get problem name""" - with verifyproblem.Problem(problem) as prob: - config = verifyproblem.ProblemConfig(prob) - if not config.check(None): - print("Please add problem name to problem.yaml when using markdown") - return None - names = config.get("name") - # If there is only one language, per the spec that is the one we want - if len(names) == 1: - return next(iter(names.values())) - - if language not in names: - raise Exception(f"No problem name defined for language {language}") - return names[language] - - -def _samples_to_html(problem: str) -> str: - """Read all samples from the problem directory and convert them to HTML""" - samples_html = "" - sample_path = os.path.join(problem, "data", "sample") - interactive_samples = [] - samples = [] - casenum = 1 - for sample in sorted(os.listdir(sample_path)): - if sample.endswith(".interaction"): - lines = [f""" - - - - - -
ReadSample Interaction {casenum}Write
"""] - with open(os.path.join(sample_path, sample), "r", encoding="utf-8") as infile: - sample_interaction = infile.readlines() - for interaction in sample_interaction: - data = interaction[1:] - line_type = "" - if interaction[0] == '>': - line_type = "sampleinteractionwrite" - elif interaction[0] == '<': - line_type = "sampleinteractionread" - else: - print(f"Warning: Interaction had unknown prefix {interaction[0]}") - lines.append(f"""
{data}
""") - - interactive_samples.append(''.join(lines)) - casenum += 1 - continue - if not sample.endswith(".in"): - continue - sample_name = sample[:-3] - outpath = os.path.join(sample_path, sample_name + ".ans") - if not os.path.isfile(outpath): - continue - with open(os.path.join(sample_path, sample), "r", encoding="utf-8") as infile: - sample_input = infile.read() - with open(outpath, "r", encoding="utf-8") as outfile: - sample_output = outfile.read() - - samples.append(""" - - Sample Input %(case)d - Sample Output %(case)d - - -
%(input)s
-
%(output)s
- """ - % ({"case": casenum, "input": html.escape(sample_input), "output": html.escape(sample_output)})) - casenum += 1 - - if interactive_samples: - samples_html += ''.join(interactive_samples) - if samples: - samples_html += f""" - - - {''.join(samples)} - -
- """ - return samples_html - diff --git a/problemtools/problem2html.py b/problemtools/problem2html.py index 4c084613..9536da38 100644 --- a/problemtools/problem2html.py +++ b/problemtools/problem2html.py @@ -5,43 +5,10 @@ import string import argparse import subprocess -from typing import Optional from . import tex2html from . import md2html - -SUPPORTED_EXTENSIONS = ("tex", "md") - -def find_statement(problem: str, extension: str, language: Optional[str]) -> Optional[str]: - """Finds the "best" statement for given language and extension""" - if language is None: - statement_path = os.path.join(problem, f"problem_statement/problem.en.{extension}") - if os.path.isfile(statement_path): - return statement_path - statement_path = os.path.join(problem, f"problem_statement/problem.{extension}") - if os.path.isfile(statement_path): - return statement_path - return None - statement_path = os.path.join(problem, f"problem_statement/problem.{language}.{extension}") - if os.path.isfile(statement_path): - return statement_path - return None - - -def _find_statement_extension(problem: str, language: Optional[str]) -> str: - """Given a language, find whether the extension is tex or md""" - extensions = [] - for ext in SUPPORTED_EXTENSIONS: - if find_statement(problem, ext, language) is not None: - extensions.append(ext) - # At most one extension per language to avoid arbitrary/hidden priorities - if len(extensions) > 1: - raise Exception(f"""Found more than one type of statement ({' and '.join(extensions)}) - for language {language or 'en'}""") - if len(extensions) == 1: - return extensions[0] - raise Exception(f"No statement found for language {language or 'en'}") - +from . import statement_common def convert(options: argparse.Namespace) -> None: problem = os.path.realpath(options.problem) @@ -62,7 +29,7 @@ def convert(options: argparse.Namespace) -> None: origcwd = os.getcwd() - if _find_statement_extension(problem, options.language) == "tex": + if statement_common.find_statement_extension(problem, options.language) == "tex": tex2html.convert(problem, options) else: md2html.convert(problem, options) diff --git a/problemtools/problem2pdf.py b/problemtools/problem2pdf.py index ac119d05..ef1784d4 100644 --- a/problemtools/problem2pdf.py +++ b/problemtools/problem2pdf.py @@ -5,47 +5,70 @@ import string import argparse import subprocess -from . import template +import tempfile +from . import template +from . import statement_common -def convert(options: argparse.Namespace, ignore_markdown: bool = False) -> bool: - problem = os.path.realpath(options.problem) - problembase = os.path.splitext(os.path.basename(problem))[0] +def convert(options: argparse.Namespace) -> bool: + problem_root = os.path.realpath(options.problem) + problembase = os.path.splitext(os.path.basename(problem_root))[0] destfile = string.Template(options.destfile).safe_substitute(problem=problembase) - # We skip PDF check when verifying problems with markdown statements - if os.path.isfile(os.path.join(problem, "problem_statement", "problem.%s.md" % options.language)) and ignore_markdown: - return True - - # Set up template if necessary - with template.Template(problem, language=options.language) as templ: - texfile = templ.get_file_name() - - origcwd = os.getcwd() - - os.chdir(os.path.dirname(texfile)) - params = ['pdflatex', '-interaction=nonstopmode'] - output = None - if options.quiet: - output = open(os.devnull, 'w') - if options.nopdf: - params.append('-draftmode') - - params.append(texfile) + if statement_common.find_statement_extension(problem_root, language=options.language) == "md": + statement_path = statement_common.find_statement(problem_root, extension="md", language=options.language) + + if not os.path.isfile(statement_path): + raise Exception(f"Error! {statement_path} is not a file") + + statement_dir = os.path.join(problem_root, "problem_statement") + with open(statement_path, "r") as f: + statement_md = f.read() + + # Hacky: html samples -> md. Then we append to the markdown document + samples = statement_common._samples_to_html(problem_root) + with tempfile.NamedTemporaryFile(mode='w', suffix=".html") as temp_file: + temp_file.write(samples) + temp_file.flush() + samples_md = os.popen(f"pandoc {temp_file.name} -t markdown").read() + + statement_md += samples_md + with tempfile.NamedTemporaryFile(mode='w', suffix=".md") as temp_file: + temp_file.write(statement_md) + temp_file.flush() + # Do .read so that the file isn't deleted until pandoc is done + os.popen(f"pandoc {temp_file.name} -o {problembase}.pdf --resource-path={statement_dir}").read() + + else: + # Set up template if necessary + with template.Template(problem_root, language=options.language) as templ: + texfile = templ.get_file_name() + + origcwd = os.getcwd() + + os.chdir(os.path.dirname(texfile)) + params = ['pdflatex', '-interaction=nonstopmode'] + output = None + if options.quiet: + output = open(os.devnull, 'w') + if options.nopdf: + params.append('-draftmode') + + params.append(texfile) - status = subprocess.call(params, stdout=output) - if status == 0: status = subprocess.call(params, stdout=output) + if status == 0: + status = subprocess.call(params, stdout=output) - if output is not None: - output.close() + if output is not None: + output.close() - os.chdir(origcwd) + os.chdir(origcwd) - if status == 0 and not options.nopdf: - shutil.move(os.path.splitext(texfile)[0] + '.pdf', destfile) + if status == 0 and not options.nopdf: + shutil.move(os.path.splitext(texfile)[0] + '.pdf', destfile) - return status == 0 + return status == 0 def get_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) diff --git a/problemtools/statement_common.py b/problemtools/statement_common.py new file mode 100644 index 00000000..e8130c93 --- /dev/null +++ b/problemtools/statement_common.py @@ -0,0 +1,130 @@ +import os +from typing import Optional +import html + +from . import verifyproblem + +SUPPORTED_EXTENSIONS = ("tex", "md") + +def find_statement(problem_root: str, extension: str, language: Optional[str]) -> Optional[str]: + """Finds the "best" statement for given language and extension""" + if language is None: + statement_path = os.path.join(problem_root, f"problem_statement/problem.en.{extension}") + if os.path.isfile(statement_path): + return statement_path + statement_path = os.path.join(problem_root, f"problem_statement/problem.{extension}") + if os.path.isfile(statement_path): + return statement_path + return None + statement_path = os.path.join(problem_root, f"problem_statement/problem.{language}.{extension}") + if os.path.isfile(statement_path): + return statement_path + return None + + +def find_statement_extension(problem_root: str, language: Optional[str]) -> str: + """Given a language, find whether the extension is tex or md + + Args: + problem_root: path to problem root + """ + extensions = [] + for ext in SUPPORTED_EXTENSIONS: + if find_statement(problem_root, ext, language) is not None: + extensions.append(ext) + # At most one extension per language to avoid arbitrary/hidden priorities + if len(extensions) > 1: + raise Exception(f"""Found more than one type of statement ({' and '.join(extensions)}) + for language {language or 'en'}""") + if len(extensions) == 1: + return extensions[0] + raise Exception(f"No statement found for language {language or 'en'}") + + + +def get_problem_name(problem: str, language: Optional[str]) -> Optional[str]: + """Load problem.yaml to get problem name""" + if language is None: + language = "en" + with verifyproblem.Problem(problem) as prob: + config = verifyproblem.ProblemConfig(prob) + if not config.check(None): + print("Please add problem name to problem.yaml when using markdown") + return None + names = config.get("name") + # If there is only one language, per the spec that is the one we want + if len(names) == 1: + return next(iter(names.values())) + + if language not in names: + raise Exception(f"No problem name defined for language {language or 'en'}") + return names[language] + + +def _samples_to_html(problem: str) -> str: + """Read all samples from the problem directory and convert them to HTML""" + samples_html = "" + sample_path = os.path.join(problem, "data", "sample") + interactive_samples = [] + samples = [] + casenum = 1 + for sample in sorted(os.listdir(sample_path)): + if sample.endswith(".interaction"): + lines = [f""" + + + + + +
ReadSample Interaction {casenum}Write
"""] + with open(os.path.join(sample_path, sample), "r", encoding="utf-8") as infile: + sample_interaction = infile.readlines() + for interaction in sample_interaction: + data = interaction[1:] + line_type = "" + if interaction[0] == '>': + line_type = "sampleinteractionwrite" + elif interaction[0] == '<': + line_type = "sampleinteractionread" + else: + print(f"Warning: Interaction had unknown prefix {interaction[0]}") + lines.append(f"""
{data}
""") + + interactive_samples.append(''.join(lines)) + casenum += 1 + continue + if not sample.endswith(".in"): + continue + sample_name = sample[:-3] + outpath = os.path.join(sample_path, sample_name + ".ans") + if not os.path.isfile(outpath): + continue + with open(os.path.join(sample_path, sample), "r", encoding="utf-8") as infile: + sample_input = infile.read() + with open(outpath, "r", encoding="utf-8") as outfile: + sample_output = outfile.read() + + samples.append(""" + + Sample Input %(case)d + Sample Output %(case)d + + +
%(input)s
+
%(output)s
+ """ + % ({"case": casenum, "input": html.escape(sample_input), "output": html.escape(sample_output)})) + casenum += 1 + + if interactive_samples: + samples_html += ''.join(interactive_samples) + if samples: + samples_html += f""" + + + {''.join(samples)} + +
+ """ + return samples_html + diff --git a/problemtools/verifyproblem.py b/problemtools/verifyproblem.py index 1db4a6e6..8be12f67 100644 --- a/problemtools/verifyproblem.py +++ b/problemtools/verifyproblem.py @@ -28,6 +28,7 @@ from . import problem2pdf from . import problem2html +from . import statement_common from . import config from . import languages @@ -1119,7 +1120,7 @@ def __init__(self, problem: Problem): self._problem = problem self.languages = [] glob_path = os.path.join(problem.probdir, 'problem_statement', 'problem.') - for extension in problem2html.SUPPORTED_EXTENSIONS: + for extension in statement_common.SUPPORTED_EXTENSIONS: if glob.glob(glob_path + extension): self.languages.append('') for f in glob.glob(glob_path + '[a-z][a-z].%s' % extension): @@ -1145,7 +1146,7 @@ def check(self, context: Context) -> bool: options.language = lang options.nopdf = True options.quiet = True - if not problem2pdf.convert(options, ignore_markdown=True): + if not problem2pdf.convert(options): langparam = f' --language {lang}' if lang != '' else '' self.error(f'Could not compile problem statement for language "{lang}". Run problem2pdf{langparam} on the problem to diagnose.') except Exception as e: @@ -1167,7 +1168,7 @@ def __str__(self) -> str: def get_config(self) -> dict[str, dict[str, str]]: ret: dict[str, dict[str, str]] = {} - for extension in problem2html.SUPPORTED_EXTENSIONS: + for extension in statement_common.SUPPORTED_EXTENSIONS: for lang in self.languages: filename = f'problem.{lang}.{extension}' if lang != '' else 'problem.{extension}' if not os.path.isfile(filename): From 480e0ea9885b6e6d871656fa1b9ca51669fd7e5c Mon Sep 17 00:00:00 2001 From: matistjati Date: Sat, 17 Aug 2024 19:39:39 +0200 Subject: [PATCH 14/25] Better md->pdf tables --- problemtools/md2html.py | 2 +- problemtools/problem2pdf.py | 24 +++++++-- problemtools/statement_common.py | 52 +++++++++---------- .../templates/markdown_pdf/fix_tables.md | 12 +++++ 4 files changed, 59 insertions(+), 31 deletions(-) create mode 100644 problemtools/templates/markdown_pdf/fix_tables.md diff --git a/problemtools/md2html.py b/problemtools/md2html.py index 68b967ae..f9190ffe 100644 --- a/problemtools/md2html.py +++ b/problemtools/md2html.py @@ -54,7 +54,7 @@ def convert(problem: str, options: argparse.Namespace) -> None: title=problem_name or "Missing problem name", problemid=problembase) - samples = statement_common.samples_to_html(problem) + samples = "".join(statement_common.samples_to_html(problem)) html_template = inject_samples(html_template, samples) html_template = replace_hr_in_footnotes(html_template) diff --git a/problemtools/problem2pdf.py b/problemtools/problem2pdf.py index ef1784d4..d65ea432 100644 --- a/problemtools/problem2pdf.py +++ b/problemtools/problem2pdf.py @@ -21,23 +21,39 @@ def convert(options: argparse.Namespace) -> bool: if not os.path.isfile(statement_path): raise Exception(f"Error! {statement_path} is not a file") + templatepaths = [os.path.join(os.path.dirname(__file__), 'templates/markdown_pdf'), + os.path.join(os.path.dirname(__file__), '../templates/markdown_pdf'), + '/usr/lib/problemtools/templates/markdown_pdf'] + templatepath = next((p for p in templatepaths + if os.path.isdir(p) and os.path.isfile(os.path.join(p, "fix_tables.md"))), + None) + table_fix_path = os.path.join(templatepath, "fix_tables.md") + if not os.path.isfile(table_fix_path): + raise Exception("Could not find markdown pdf template") + + with open(table_fix_path, "r") as f: + table_fix = f.read() + statement_dir = os.path.join(problem_root, "problem_statement") with open(statement_path, "r") as f: statement_md = f.read() + statement_md = table_fix + statement_md + # Hacky: html samples -> md. Then we append to the markdown document - samples = statement_common._samples_to_html(problem_root) + samples = "".join(statement_common.samples_to_html(problem_root)) with tempfile.NamedTemporaryFile(mode='w', suffix=".html") as temp_file: temp_file.write(samples) temp_file.flush() - samples_md = os.popen(f"pandoc {temp_file.name} -t markdown").read() - + samples_md = os.popen(f"pandoc {temp_file.name} -t latex").read() statement_md += samples_md + + #statement_md += samples_md with tempfile.NamedTemporaryFile(mode='w', suffix=".md") as temp_file: temp_file.write(statement_md) temp_file.flush() # Do .read so that the file isn't deleted until pandoc is done - os.popen(f"pandoc {temp_file.name} -o {problembase}.pdf --resource-path={statement_dir}").read() + os.popen(f"pandoc --verbose {temp_file.name} -o {problembase}.pdf --resource-path={statement_dir}").read() else: # Set up template if necessary diff --git a/problemtools/statement_common.py b/problemtools/statement_common.py index e8130c93..0667e928 100644 --- a/problemtools/statement_common.py +++ b/problemtools/statement_common.py @@ -1,5 +1,5 @@ import os -from typing import Optional +from typing import Optional, List import html from . import verifyproblem @@ -61,11 +61,17 @@ def get_problem_name(problem: str, language: Optional[str]) -> Optional[str]: return names[language] -def _samples_to_html(problem: str) -> str: - """Read all samples from the problem directory and convert them to HTML""" - samples_html = "" - sample_path = os.path.join(problem, "data", "sample") - interactive_samples = [] +def samples_to_html(problem_root: str) -> List[str]: + """Read all samples from the problem directory and convert them to HTML + + Args: + problem_root: path to root of problem + + Returns: + List[str]: All samples, converted to html. Ordered lexicographically by file names + """ + + sample_path = os.path.join(problem_root, "data", "sample") samples = [] casenum = 1 for sample in sorted(os.listdir(sample_path)): @@ -90,7 +96,7 @@ def _samples_to_html(problem: str) -> str: print(f"Warning: Interaction had unknown prefix {interaction[0]}") lines.append(f"""
{data}
""") - interactive_samples.append(''.join(lines)) + samples.append(''.join(lines)) casenum += 1 continue if not sample.endswith(".in"): @@ -105,26 +111,20 @@ def _samples_to_html(problem: str) -> str: sample_output = outfile.read() samples.append(""" - - Sample Input %(case)d - Sample Output %(case)d - - -
%(input)s
-
%(output)s
- """ + + + + + + + + + + + +
Sample Input %(case)dSample Output %(case)d
%(input)s
%(output)s
""" % ({"case": casenum, "input": html.escape(sample_input), "output": html.escape(sample_output)})) casenum += 1 - if interactive_samples: - samples_html += ''.join(interactive_samples) - if samples: - samples_html += f""" - - - {''.join(samples)} - -
- """ - return samples_html + return samples diff --git a/problemtools/templates/markdown_pdf/fix_tables.md b/problemtools/templates/markdown_pdf/fix_tables.md new file mode 100644 index 00000000..fd597724 --- /dev/null +++ b/problemtools/templates/markdown_pdf/fix_tables.md @@ -0,0 +1,12 @@ +--- +header-includes: + - '\usepackage{xstring}' + - '\setlength{\aboverulesep}{0pt}' + - '\setlength{\belowrulesep}{0pt}' + - '\renewcommand{\arraystretch}{1.3}' + - '\makeatletter' + - '\patchcmd{\LT@array}{\@mkpream{#2}}{\StrGobbleLeft{#2}{2}[\pream]\StrGobbleRight{\pream}{2}[\pream]\StrSubstitute{\pream}{l}{|l}[\pream]\@mkpream{@{}\pream|@{}}}{}{}' + - '\def\midrule{}' + - '\apptocmd{\LT@tabularcr}{\hline}{}{}' + - '\makeatother' +--- From e9b3f8ed43faeed909cac3f95338232bb0bf30c7 Mon Sep 17 00:00:00 2001 From: matistjati Date: Sun, 18 Aug 2024 02:00:47 +0200 Subject: [PATCH 15/25] Interactive samples for pdf --- problemtools/md2html.py | 2 +- problemtools/problem2pdf.py | 16 ++-- problemtools/statement_common.py | 74 +++++++++++++++---- .../templates/markdown_pdf/fix_tables.md | 2 + 4 files changed, 68 insertions(+), 26 deletions(-) diff --git a/problemtools/md2html.py b/problemtools/md2html.py index f9190ffe..d764b4f4 100644 --- a/problemtools/md2html.py +++ b/problemtools/md2html.py @@ -54,7 +54,7 @@ def convert(problem: str, options: argparse.Namespace) -> None: title=problem_name or "Missing problem name", problemid=problembase) - samples = "".join(statement_common.samples_to_html(problem)) + samples = "".join(statement_common.format_samples(problem, to_pdf=False)) html_template = inject_samples(html_template, samples) html_template = replace_hr_in_footnotes(html_template) diff --git a/problemtools/problem2pdf.py b/problemtools/problem2pdf.py index d65ea432..77081f8a 100644 --- a/problemtools/problem2pdf.py +++ b/problemtools/problem2pdf.py @@ -38,22 +38,20 @@ def convert(options: argparse.Namespace) -> bool: with open(statement_path, "r") as f: statement_md = f.read() + # Add code that adds vertical and horizontal lines to all tables statement_md = table_fix + statement_md # Hacky: html samples -> md. Then we append to the markdown document - samples = "".join(statement_common.samples_to_html(problem_root)) - with tempfile.NamedTemporaryFile(mode='w', suffix=".html") as temp_file: - temp_file.write(samples) - temp_file.flush() - samples_md = os.popen(f"pandoc {temp_file.name} -t latex").read() - statement_md += samples_md + samples = "\n".join(statement_common.format_samples(problem_root, to_pdf=True)) + + # If we don't add newline, the table might get attached to a footnote + statement_md += "\n" + samples - #statement_md += samples_md with tempfile.NamedTemporaryFile(mode='w', suffix=".md") as temp_file: temp_file.write(statement_md) temp_file.flush() - # Do .read so that the file isn't deleted until pandoc is done - os.popen(f"pandoc --verbose {temp_file.name} -o {problembase}.pdf --resource-path={statement_dir}").read() + # Do .read so that the temp file isn't deleted until pandoc is done + os.popen(f"pandoc {temp_file.name} -o {problembase}.pdf --resource-path={statement_dir}").read() else: # Set up template if necessary diff --git a/problemtools/statement_common.py b/problemtools/statement_common.py index 0667e928..cb6960f0 100644 --- a/problemtools/statement_common.py +++ b/problemtools/statement_common.py @@ -1,6 +1,7 @@ import os from typing import Optional, List import html +import tempfile from . import verifyproblem @@ -49,8 +50,7 @@ def get_problem_name(problem: str, language: Optional[str]) -> Optional[str]: with verifyproblem.Problem(problem) as prob: config = verifyproblem.ProblemConfig(prob) if not config.check(None): - print("Please add problem name to problem.yaml when using markdown") - return None + raise Exception(f"Invalid problem.yaml") names = config.get("name") # If there is only one language, per the spec that is the one we want if len(names) == 1: @@ -61,44 +61,78 @@ def get_problem_name(problem: str, language: Optional[str]) -> Optional[str]: return names[language] -def samples_to_html(problem_root: str) -> List[str]: - """Read all samples from the problem directory and convert them to HTML +def format_samples(problem_root: str, to_pdf: bool = False) -> List[str]: + """Read all samples from the problem directory and convert them to pandoc-valid markdown Args: problem_root: path to root of problem + to_pdf: whether the outputted samples should be valid for for html or pdf Returns: - List[str]: All samples, converted to html. Ordered lexicographically by file names + List[str]: All samples, converted to a format appropriate to be pasted into + a markdown file. Ordered lexicographically by file names """ sample_path = os.path.join(problem_root, "data", "sample") + if not os.path.isdir(sample_path): + print("WARNING!! no sample folder") + return [] samples = [] casenum = 1 for sample in sorted(os.listdir(sample_path)): if sample.endswith(".interaction"): - lines = [f""" + if to_pdf: + line = r"""\begin{tabular}{p{0.3\textwidth} p{0.5\textwidth} p{0.0\textwidth}} +\textbf{Read} & \textbf{Sample Interaction %i} & \textbf{Write} \\ +\end{tabular}""" % casenum + else: + line = f""" +
-
Read Sample Interaction {casenum} Write
"""] + """ + with open(os.path.join(sample_path, sample), "r", encoding="utf-8") as infile: sample_interaction = infile.readlines() + lines = [] for interaction in sample_interaction: data = interaction[1:] - line_type = "" - if interaction[0] == '>': - line_type = "sampleinteractionwrite" - elif interaction[0] == '<': - line_type = "sampleinteractionread" + if to_pdf: + if interaction[0] == '>': + left = True + elif interaction[0] == '<': + left = False + else: + print(f"Warning: Interaction had unknown prefix {interaction[0]}") + lines.append(r""" + \begin{table}[H] + %(justify)s\begin{tabular}{|p{0.6\textwidth}|} + \hline + %(text)s \\ + \hline + \end{tabular} + \end{table}""" % {"justify": "" if left else "\\hspace*{\\fill}\n", + "text": data}) else: - print(f"Warning: Interaction had unknown prefix {interaction[0]}") - lines.append(f"""
{data}
""") - - samples.append(''.join(lines)) + line_type = "" + if interaction[0] == '>': + line_type = "sampleinteractionwrite" + elif interaction[0] == '<': + line_type = "sampleinteractionread" + else: + print(f"Warning: Interaction had unknown prefix {interaction[0]}") + lines.append(f"""
{data}
""") + + if to_pdf: + samples.append(line + '\\vspace{-15pt}'.join(lines)) + else: + samples.append(line + ''.join(lines)) casenum += 1 continue + if not sample.endswith(".in"): continue sample_name = sample[:-3] @@ -124,6 +158,14 @@ def samples_to_html(problem_root: str) -> List[str]: """ % ({"case": casenum, "input": html.escape(sample_input), "output": html.escape(sample_output)})) + + if to_pdf: + # If pdf, convert to markdown + with tempfile.NamedTemporaryFile(mode='w', suffix=".html") as temp_file: + temp_file.write(samples[-1]) + temp_file.flush() + samples[-1] = os.popen(f"pandoc {temp_file.name} -t markdown").read() + casenum += 1 return samples diff --git a/problemtools/templates/markdown_pdf/fix_tables.md b/problemtools/templates/markdown_pdf/fix_tables.md index fd597724..1b04614f 100644 --- a/problemtools/templates/markdown_pdf/fix_tables.md +++ b/problemtools/templates/markdown_pdf/fix_tables.md @@ -1,5 +1,7 @@ --- header-includes: + - '\usepackage{float}' + - '\usepackage{booktabs}' - '\usepackage{xstring}' - '\setlength{\aboverulesep}{0pt}' - '\setlength{\belowrulesep}{0pt}' From ad3e801c453a19bd174750b4e3107f4f50d18f62 Mon Sep 17 00:00:00 2001 From: matistjati Date: Sun, 18 Aug 2024 02:14:57 +0200 Subject: [PATCH 16/25] Remove bplusa --- examples/bplusa/data/sample/1.ans | 1 - examples/bplusa/data/sample/1.in | 1 - examples/bplusa/data/secret/1.ans | 1 - examples/bplusa/data/secret/1.in | 1 - examples/bplusa/data/secret/2.ans | 1 - examples/bplusa/data/secret/2.in | 1 - examples/bplusa/data/secret/3.ans | 1 - examples/bplusa/data/secret/3.in | 1 - .../input_validators/validator/validator.cpp | 8 - .../input_validators/validator/validator.h | 356 ------------------ .../output_validators/validator/validate.cc | 64 ---- .../output_validators/validator/validate.h | 153 -------- examples/bplusa/problem.yaml | 4 - .../bplusa/problem_statement/problem.en.md | 8 - .../bplusa/submissions/accepted/cplus1.cpp | 10 - examples/bplusa/submissions/accepted/zero.cpp | 10 - 16 files changed, 621 deletions(-) delete mode 100644 examples/bplusa/data/sample/1.ans delete mode 100644 examples/bplusa/data/sample/1.in delete mode 100644 examples/bplusa/data/secret/1.ans delete mode 100644 examples/bplusa/data/secret/1.in delete mode 100644 examples/bplusa/data/secret/2.ans delete mode 100644 examples/bplusa/data/secret/2.in delete mode 100644 examples/bplusa/data/secret/3.ans delete mode 100644 examples/bplusa/data/secret/3.in delete mode 100644 examples/bplusa/input_validators/validator/validator.cpp delete mode 100644 examples/bplusa/input_validators/validator/validator.h delete mode 100644 examples/bplusa/output_validators/validator/validate.cc delete mode 100644 examples/bplusa/output_validators/validator/validate.h delete mode 100644 examples/bplusa/problem.yaml delete mode 100644 examples/bplusa/problem_statement/problem.en.md delete mode 100644 examples/bplusa/submissions/accepted/cplus1.cpp delete mode 100644 examples/bplusa/submissions/accepted/zero.cpp diff --git a/examples/bplusa/data/sample/1.ans b/examples/bplusa/data/sample/1.ans deleted file mode 100644 index 654d5269..00000000 --- a/examples/bplusa/data/sample/1.ans +++ /dev/null @@ -1 +0,0 @@ -2 3 diff --git a/examples/bplusa/data/sample/1.in b/examples/bplusa/data/sample/1.in deleted file mode 100644 index 7ed6ff82..00000000 --- a/examples/bplusa/data/sample/1.in +++ /dev/null @@ -1 +0,0 @@ -5 diff --git a/examples/bplusa/data/secret/1.ans b/examples/bplusa/data/secret/1.ans deleted file mode 100644 index 1790e253..00000000 --- a/examples/bplusa/data/secret/1.ans +++ /dev/null @@ -1 +0,0 @@ -123 0 diff --git a/examples/bplusa/data/secret/1.in b/examples/bplusa/data/secret/1.in deleted file mode 100644 index 190a1803..00000000 --- a/examples/bplusa/data/secret/1.in +++ /dev/null @@ -1 +0,0 @@ -123 diff --git a/examples/bplusa/data/secret/2.ans b/examples/bplusa/data/secret/2.ans deleted file mode 100644 index 93fd4034..00000000 --- a/examples/bplusa/data/secret/2.ans +++ /dev/null @@ -1 +0,0 @@ -992 0 diff --git a/examples/bplusa/data/secret/2.in b/examples/bplusa/data/secret/2.in deleted file mode 100644 index 7f9d7e97..00000000 --- a/examples/bplusa/data/secret/2.in +++ /dev/null @@ -1 +0,0 @@ -992 diff --git a/examples/bplusa/data/secret/3.ans b/examples/bplusa/data/secret/3.ans deleted file mode 100644 index 80c0cc79..00000000 --- a/examples/bplusa/data/secret/3.ans +++ /dev/null @@ -1 +0,0 @@ -1 0 diff --git a/examples/bplusa/data/secret/3.in b/examples/bplusa/data/secret/3.in deleted file mode 100644 index d00491fd..00000000 --- a/examples/bplusa/data/secret/3.in +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/examples/bplusa/input_validators/validator/validator.cpp b/examples/bplusa/input_validators/validator/validator.cpp deleted file mode 100644 index 0ecff521..00000000 --- a/examples/bplusa/input_validators/validator/validator.cpp +++ /dev/null @@ -1,8 +0,0 @@ -#include "validator.h" - - -void run() { - Int(1, 1000); - Endl(); - Eof(); -} diff --git a/examples/bplusa/input_validators/validator/validator.h b/examples/bplusa/input_validators/validator/validator.h deleted file mode 100644 index f42bc2d7..00000000 --- a/examples/bplusa/input_validators/validator/validator.h +++ /dev/null @@ -1,356 +0,0 @@ -#ifdef NDEBUG -#error Asserts must be enabled! Do not set NDEBUG. -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -using namespace std; - -// Implemented by you! -void run(); - -// PUBLIC API -// (extend if you need to) - -[[noreturn]] -void die(const string& msg); -[[noreturn]] -void die_line(const string& msg); - -struct ArgType { - string _name, _x; - ArgType(const string& name, const string& x) : _name(name), _x(x) {} - operator string() const { return _x; } - operator long long() const; - operator bool() const; - operator int() const; -}; - -struct IntType { - long long _x; - IntType(long long x) : _x(x) {} - operator long long() const { return _x; } - operator int() const; - operator bool() const; -}; - -ArgType Arg(const string& name); - -ArgType Arg(const string& name, long long _default); - -string Arg(const string& name, const string& _default); - -template -void AssertUnique(const Vec& v); - -namespace IO { - IntType Int(long long lo, long long hi); - double Float(double lo, double hi, bool strict = true); - template - vector SpacedInts(long long count, T lo, T hi); - vector SpacedFloats(long long count, double lo, double hi); - void Char(char expected); - char Char(); - string Line(); - void Endl() { Char('\n'); } - void Space() { Char(' '); } - void Eof() { Char(-1); } -}; -using namespace IO; - -// INTERNALS - -bool _validator_initialized; -struct _validator { - map params; - set used_params; - - void construct(int argc, char** argv) { - _validator_initialized = true; - for (int i = 1; i < argc; i++) { - string s = argv[i]; - size_t ind = s.find('='); - if (ind == string::npos) continue; - auto before = s.substr(0, ind), after = s.substr(ind + 1); - if (params.count(before)) - die("Duplicate parameter " + before); - params[before] = after; - } - } - - void destroy() { - assert(_validator_initialized); - if (!params.empty()) { - string name = params.begin()->first; - die("Unused parameter " + name); - } - IO::Eof(); - _Exit(42); - } - - bool has_var(const string& name) { - if (!_validator_initialized) die("Must not read variables before main"); - return params.count(name) || used_params.count(name); - } - - string get_var(const string& name) { - if (!_validator_initialized) die("Must not read variables before main"); - if (used_params.count(name)) die("Must not read parameter " + name + " twice (either typo or slow)"); - if (!params.count(name)) die("No parameter " + name); - string res = params.at(name); - params.erase(name); - used_params.insert(name); - return res; - } -} _validator_inst; - -void die(const string& msg) { - cerr << msg << endl; - ofstream fout("/tmp/input_validator_msg", ios::app); - fout << msg << endl; - fout.close(); - _Exit(43); -} - -ArgType::operator long long() const { - string dummy; - { - long long num; - istringstream iss(_x); - iss >> num; - if (iss && !(iss >> dummy)) return num; - } - { - // We also allow scientific notation, for clarity - long double num; - istringstream iss(_x); - iss >> num; - if (iss && !(iss >> dummy)) return (long long)num; - } - die("Unable to parse value " + _x + " for parameter " + _name); -} - -ArgType::operator int() const { - long long val = (long long)*this; - if (val < INT_MIN || val > INT_MAX) - die("number " + to_string(val) + " is too large for an int for parameter " + _name); - return (int)val; -} - -ArgType::operator bool() const { - long long val = (long long)*this; - if (val < 0 || val > 1) - die("number " + to_string(val) + " is not boolean (0/1), for parameter " + _name); - return (bool)val; -} - -IntType::operator int() const { - long long val = (long long)*this; - if (val < INT_MIN || val > INT_MAX) - die_line("number " + to_string(val) + " is too large for an int"); - return (int)val; -} - -IntType::operator bool() const { - long long val = (long long)*this; - if (val < 0 || val > 1) - die_line("number " + to_string(val) + " is not boolean (0/1)"); - return (bool)val; -} - -ArgType Arg(const string& name) { - return {name, _validator_inst.get_var(name)}; -} - -ArgType Arg(const string& name, long long _default) { - if (!_validator_inst.has_var(name)) - return {name, to_string(_default)}; - ArgType ret = Arg(name); - (void)(long long)ret; - return ret; -} - -string Arg(const string& name, const string& _default) { - if (!_validator_inst.has_var(name)) - return _default; - return (string)Arg(name); -} - -static int _lineno = 1, _consumed_lineno = -1, _hit_char_error = 0; -char _peek1(); -void die_line(const string& msg) { - if (!_hit_char_error && _peek1() == -1) die(msg); - else if (_consumed_lineno == -1) die(msg + " (before reading any input)"); - else die(msg + " on line " + to_string(_consumed_lineno)); -} - -static char _buffer = -2; // -2 = none, -1 = eof, other = that char -char _peek1() { - if (_buffer != -2) return _buffer; - int val = getchar_unlocked(); - static_assert(EOF == -1, ""); - static_assert(CHAR_MIN == -128, ""); - if (val == -2 || val < CHAR_MIN || val >= CHAR_MAX) { - _hit_char_error = 1; - die_line("Unable to process byte " + to_string(val)); - } - _buffer = (char)val; - return _buffer; -} -void _use_peek(char ch) { - _buffer = -2; - if (ch == '\n') _lineno++; - else _consumed_lineno = _lineno; -} -char _read1() { - char ret = _peek1(); - _use_peek(ret); - return ret; -} -string _token() { - string ret; - for (;;) { - char ch = _peek1(); - if (ch == ' ' || ch == '\n' || ch == -1) { - break; - } - _use_peek(ch); - ret += ch; - } - return ret; -} -string _describe(char ch) { - assert(ch != -2); - if (ch == -1) return "EOF"; - if (ch == ' ') return "SPACE"; - if (ch == '\r') return "CARRIAGE RETURN"; - if (ch == '\n') return "NEWLINE"; - if (ch == '\t') return "TAB"; - if (ch == '\'') return "\"'\""; - return string("'") + ch + "'"; -} - -IntType IO::Int(long long lo, long long hi) { - string s = _token(); - if (s.empty()) die_line("Expected number, saw " + _describe(_peek1())); - try { - long long mul = 1; - int ind = 0; - if (s[0] == '-') { - mul = -1; - ind = 1; - } - if (ind == (int)s.size()) throw false; - char ch = s[ind++]; - if (ch < '0' || ch > '9') throw false; - if (ch == '0' && ind != (int)s.size()) throw false; - long long ret = ch - '0'; - while (ind < (int)s.size()) { - if (ret > LLONG_MAX / 10 - 20 || ret < LLONG_MIN / 10 + 20) - throw false; - ret *= 10; - ch = s[ind++]; - if (ch < '0' || ch > '9') throw false; - ret += ch - '0'; - } - ret *= mul; - if (ret < lo || ret > hi) die_line("Number " + s + " is out of range [" + to_string(lo) + ", " + to_string(hi) + "]"); - return {ret}; - } catch (bool) { - die_line("Unable to parse \"" + s + "\" as integer"); - } -} - -template -vector IO::SpacedInts(long long count, T lo, T hi) { - vector res; - res.reserve(count); - for (int i = 0; i < count; i++) { - if (i != 0) IO::Space(); - res.emplace_back((T)IO::Int(lo, hi)); - } - IO::Endl(); - return res; -} - -vector IO::SpacedFloats(long long count, double lo, double hi) { - vector res; - res.reserve(count); - for (int i = 0; i < count; i++) { - if (i != 0) IO::Space(); - res.emplace_back(IO::Float(lo, hi)); - } - IO::Endl(); - return res; -} - -double IO::Float(double lo, double hi, bool strict) { - string s = _token(); - if (s.empty()) die_line("Expected floating point number, saw " + _describe(_peek1())); - istringstream iss(s); - double res; - string dummy; - iss >> res; - if (!iss || iss >> dummy) die_line("Unable to parse " + s + " as a float"); - if (res < lo || res > hi) die_line("Floating-point number " + s + " is out of range [" + to_string(lo) + ", " + to_string(hi) + "]"); - if (res != res) die_line("Floating-point number " + s + " is NaN"); - if (strict) { - if (s.find('.') != string::npos && s.back() == '0' && s.substr(s.size() - 2) != ".0") - die_line("Number " + s + " has unnecessary trailing zeroes"); - if (s[0] == '0' && s.size() > 1 && s[1] == '0') - die_line("Number " + s + " has unnecessary leading zeroes"); - } - return res; -} - -char IO::Char() { - char ret = _read1(); - if (ret == -1) die_line("Expected character, saw EOF"); - return ret; -} - -void IO::Char(char expected) { - char ret = _peek1(); - if (ret != expected) die_line("Expected " + _describe(expected) + ", saw " + _describe(ret)); - _use_peek(ret); -} - -string IO::Line() { - string ret; - for (;;) { - char ch = IO::Char(); - if (ch == '\n') break; - ret += ch; - } - return ret; -} - -template -void AssertUnique(const Vec& v_) { - Vec v = v_; - auto beg = v.begin(), end = v.end(); - sort(beg, end); - int size = (int)(end - beg); - for (int i = 0; i < size - 1; i++) { - if (v[i] == v[i+1]) { - ostringstream oss; - oss << "Vector contains duplicate value " << v[i]; - die_line(oss.str()); - } - } -} - -int main(int argc, char** argv) { - _validator_inst.construct(argc, argv); - run(); - _validator_inst.destroy(); -} - diff --git a/examples/bplusa/output_validators/validator/validate.cc b/examples/bplusa/output_validators/validator/validate.cc deleted file mode 100644 index 61eabfc2..00000000 --- a/examples/bplusa/output_validators/validator/validate.cc +++ /dev/null @@ -1,64 +0,0 @@ -#include "validate.h" - -#include -using namespace std; - -#define rep(i, a, b) for(int i = a; i < (b); ++i) -#define all(x) begin(x), end(x) -#define sz(x) (int)(x).size() -typedef long long ll; -typedef pair pii; -typedef vector vi; -typedef vector vvi; -typedef long double ld; - -#define repe(i, container) for (auto& i : container) - -void check_isvalid(int a, int b, int c, feedback_function feedback) -{ - if (a==b) feedback("a is equal to b"); - if (a+b!=c) feedback("b+a!=c"); -} - -const int HUNDRED_THOUSAND = int(1e5); -int main(int argc, char **argv) { - init_io(argc, argv); - - // Read the testcase input - int c; - judge_in >> c; - - auto check = [&](istream& sol, feedback_function feedback) { - int a, b; - // Don't get stuck waiting for output from solution - if(!(sol >> a >> b)) feedback("Expected more output"); - // Validate constraints - if (a < -HUNDRED_THOUSAND || a > HUNDRED_THOUSAND) feedback("a is too big or large"); - if (b < -HUNDRED_THOUSAND || b > HUNDRED_THOUSAND) feedback("b is too big or large"); - - // Check that they actually solved the task - check_isvalid(a, b, c, feedback); - - // Disallow trailing output - string trailing; - if(sol >> trailing) feedback("Trailing output"); - return true; - }; - - // Check both the judge's and contestants' output - // It is good practice to not assume that the judge is correct/optimal - bool judge_found_sol = check(judge_ans, judge_error); - bool author_found_sol = check(author_out, wrong_answer); - - // In this problem, having a return value from check is unnecessary - // However, if there isn't always a solution, we will get a nice - // judge error if the judge solution claims no solution exists, while - // a contestant finds one - if(!judge_found_sol) - judge_error("NO! Judge did not find valid solution"); - - if(!author_found_sol) - wrong_answer("Contestant did not find valid solution"); - - accept(); -} diff --git a/examples/bplusa/output_validators/validator/validate.h b/examples/bplusa/output_validators/validator/validate.h deleted file mode 100644 index c59c5fdb..00000000 --- a/examples/bplusa/output_validators/validator/validate.h +++ /dev/null @@ -1,153 +0,0 @@ -/* Utility functions for writing output validators for the Kattis - * problem format. - * - * The primary functions and variables available are the following. - * In many cases, the only functions needed are "init_io", - * "wrong_answer", and "accept". - * - * - init_io(argc, argv): - * initialization - * - * - judge_in, judge_ans, author_out: - * std::istream objects for judge input file, judge answer - * file, and submission output file. - * - * - accept(): - * exit and give Accepted! - * - * - accept_with_score(double score): - * exit with Accepted and give a score (for scoring problems) - * - * - judge_message(std::string msg, ...): - * printf-style function for emitting a judge message (a - * message that gets displayed to a privileged user with access - * to secret data etc). - * - * - wrong_answer(std::string msg, ...): - * printf-style function for exitting and giving Wrong Answer, - * and emitting a judge message (which would typically explain - * the cause of the Wrong Answer) - * - * - judge_error(std::string msg, ...): - * printf-style function for exitting and giving Judge Error, - * and emitting a judge message (which would typically explain - * the cause of the Judge Error) - * - * - author_message(std::string msg, ...): - * printf-style function for emitting an author message (a - * message that gets displayed to the author of the - * submission). (Use with caution, and be careful not to let - * it leak information!) - * - */ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -typedef void (*feedback_function)(const char*, ...); - -const int EXITCODE_AC = 42; -const int EXITCODE_WA = 43; -const char* FILENAME_AUTHOR_MESSAGE = "teammessage.txt"; -const char* FILENAME_JUDGE_MESSAGE = "judgemessage.txt"; -const char* FILENAME_JUDGE_ERROR = "judgeerror.txt"; -const char* FILENAME_SCORE = "score.txt"; - -#define USAGE "%s: judge_in judge_ans feedback_dir < author_out\n" - -std::ifstream judge_in, judge_ans; -std::istream author_out(std::cin.rdbuf()); - -char *feedbackdir = NULL; - -void vreport_feedback(const char* category, - const char* msg, - va_list pvar) { - std::ostringstream fname; - if (feedbackdir) - fname << feedbackdir << '/'; - fname << category; - FILE *f = fopen(fname.str().c_str(), "a"); - assert(f); - vfprintf(f, msg, pvar); - fclose(f); -} - -void report_feedback(const char* category, const char* msg, ...) { - va_list pvar; - va_start(pvar, msg); - vreport_feedback(category, msg, pvar); -} - -void author_message(const char* msg, ...) { - va_list pvar; - va_start(pvar, msg); - vreport_feedback(FILENAME_AUTHOR_MESSAGE, msg, pvar); -} - -void judge_message(const char* msg, ...) { - va_list pvar; - va_start(pvar, msg); - vreport_feedback(FILENAME_JUDGE_MESSAGE, msg, pvar); -} - -void wrong_answer(const char* msg, ...) { - va_list pvar; - va_start(pvar, msg); - vreport_feedback(FILENAME_JUDGE_MESSAGE, msg, pvar); - exit(EXITCODE_WA); -} - -void judge_error(const char* msg, ...) { - va_list pvar; - va_start(pvar, msg); - vreport_feedback(FILENAME_JUDGE_ERROR, msg, pvar); - assert(0); -} - -void accept() { - exit(EXITCODE_AC); -} - -void accept_with_score(double scorevalue) { - report_feedback(FILENAME_SCORE, "%.9le", scorevalue); - exit(EXITCODE_AC); -} - - -bool is_directory(const char *path) { - struct stat entry; - return stat(path, &entry) == 0 && S_ISDIR(entry.st_mode); -} - -void init_io(int argc, char **argv) { - if(argc < 4) { - fprintf(stderr, USAGE, argv[0]); - judge_error("Usage: %s judgein judgeans feedbackdir [opts] < userout", argv[0]); - } - - // Set up feedbackdir first, as that allows us to produce feedback - // files for errors in the other parameters. - if (!is_directory(argv[3])) { - judge_error("%s: %s is not a directory\n", argv[0], argv[3]); - } - feedbackdir = argv[3]; - - judge_in.open(argv[1], std::ios_base::in); - if (judge_in.fail()) { - judge_error("%s: failed to open %s\n", argv[0], argv[1]); - } - - judge_ans.open(argv[2], std::ios_base::in); - if (judge_ans.fail()) { - judge_error("%s: failed to open %s\n", argv[0], argv[2]); - } - - author_out.rdbuf(std::cin.rdbuf()); -} diff --git a/examples/bplusa/problem.yaml b/examples/bplusa/problem.yaml deleted file mode 100644 index d59b82ec..00000000 --- a/examples/bplusa/problem.yaml +++ /dev/null @@ -1,4 +0,0 @@ -source: Kattis -license: public domain -name: B plus A -validation: custom diff --git a/examples/bplusa/problem_statement/problem.en.md b/examples/bplusa/problem_statement/problem.en.md deleted file mode 100644 index d5060a86..00000000 --- a/examples/bplusa/problem_statement/problem.en.md +++ /dev/null @@ -1,8 +0,0 @@ -Given the integer $c$, find any pair of integers $b$ and $a$ satisfying $b+a=c$ and $a \neq b$. - -## Input -Input consists of the integer $C$ ($1 \le C \le 1000$). - -## Output -Output $b$ and $a$, separated by a space. Any $b$, $a$ satisfying above constraints and $-10^5 \leq a,b \leq 10^5$ -will be accepted. diff --git a/examples/bplusa/submissions/accepted/cplus1.cpp b/examples/bplusa/submissions/accepted/cplus1.cpp deleted file mode 100644 index 946facb7..00000000 --- a/examples/bplusa/submissions/accepted/cplus1.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include -using namespace std; - -int main() -{ - int c; - cin >> c; - cout << c+1 << " " << -1 << endl; - return 0; -} diff --git a/examples/bplusa/submissions/accepted/zero.cpp b/examples/bplusa/submissions/accepted/zero.cpp deleted file mode 100644 index 2f4c748a..00000000 --- a/examples/bplusa/submissions/accepted/zero.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include -using namespace std; - -int main() -{ - int c; - cin >> c; - cout << c << " " << 0 << endl; - return 0; -} From 30d9603e1d7aa505cb8ebce05021ce4a5329235e Mon Sep 17 00:00:00 2001 From: matistjati Date: Sun, 18 Aug 2024 02:47:41 +0200 Subject: [PATCH 17/25] PDF problem name --- problemtools/problem2html.py | 3 +++ problemtools/problem2pdf.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/problemtools/problem2html.py b/problemtools/problem2html.py index 9536da38..c9ffe221 100644 --- a/problemtools/problem2html.py +++ b/problemtools/problem2html.py @@ -13,6 +13,9 @@ def convert(options: argparse.Namespace) -> None: problem = os.path.realpath(options.problem) + if not os.path.isdir(problem): + raise Exception(f"Problem does not exist: {problem}") + problembase = os.path.splitext(os.path.basename(problem))[0] destdir = string.Template(options.destdir).safe_substitute(problem=problembase) destfile = string.Template(options.destfile).safe_substitute(problem=problembase) diff --git a/problemtools/problem2pdf.py b/problemtools/problem2pdf.py index 77081f8a..63c1a1cd 100644 --- a/problemtools/problem2pdf.py +++ b/problemtools/problem2pdf.py @@ -38,9 +38,12 @@ def convert(options: argparse.Namespace) -> bool: with open(statement_path, "r") as f: statement_md = f.read() + problem_name = statement_common.get_problem_name(problem_root, options.language) + # Add code that adds vertical and horizontal lines to all tables + statement_md = r'\centerline{\huge %s}' % problem_name + statement_md statement_md = table_fix + statement_md - + # Hacky: html samples -> md. Then we append to the markdown document samples = "\n".join(statement_common.format_samples(problem_root, to_pdf=True)) From efc5c9e6b666839e9ec96502a079115dbf6446d9 Mon Sep 17 00:00:00 2001 From: matistjati Date: Sun, 18 Aug 2024 10:46:43 +0200 Subject: [PATCH 18/25] Add dependencies --- Dockerfile | 3 ++- README.md | 4 ++-- admin/docker/Dockerfile.minimal | 3 ++- debian/control | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index daa50dde..cff647c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,11 +14,12 @@ RUN apt-get update && \ libgmp10 \ libgmpxx4ldbl \ openjdk-8-jdk \ + pandoc \ python3-minimal \ python3-pip \ python3-plastex \ - python3-markdown \ python3-yaml \ + rsvg-convert \ sudo \ texlive-fonts-recommended \ texlive-lang-cyrillic \ diff --git a/README.md b/README.md index 499fe610..4e708b1a 100644 --- a/README.md +++ b/README.md @@ -207,13 +207,13 @@ The dependencies needed to *build/install* problemtools can be installed with: And the dependencies needed to *run* problemtools can be installed with: - sudo apt install ghostscript libgmpxx4ldbl python3-minimal python-pkg-resources python3-plastex python3-markdown python3-yaml texlive-fonts-recommended texlive-lang-cyrillic texlive-latex-extra texlive-plain-generic tidy + sudo apt install ghostscript libgmpxx4ldbl pandoc python3-minimal python-pkg-resources python3-plastex python3-markdown python3-yaml rsvg-convert texlive-fonts-recommended texlive-lang-cyrillic texlive-latex-extra texlive-plain-generic tidy ### Fedora On Fedora, these dependencies can be installed with: - sudo dnf install boost-regex gcc gmp-devel gmp-c++ python3 python3-pyyaml python3-markdown texlive-latex texlive-collection-fontsrecommended texlive-fancyhdr texlive-subfigure texlive-wrapfig texlive-import texlive-ulem texlive-xifthen texlive-overpic texlive-pbox tidy ghostscript + sudo dnf install boost-regex gcc gmp-devel gmp-c++ pandoc python3 python3-pyyaml rsvg-convert texlive-latex texlive-collection-fontsrecommended texlive-fancyhdr texlive-subfigure texlive-wrapfig texlive-import texlive-ulem texlive-xifthen texlive-overpic texlive-pbox tidy ghostscript Followed by: diff --git a/admin/docker/Dockerfile.minimal b/admin/docker/Dockerfile.minimal index a44811f5..340f0b20 100644 --- a/admin/docker/Dockerfile.minimal +++ b/admin/docker/Dockerfile.minimal @@ -20,11 +20,12 @@ RUN apt update && \ apt install -y \ ghostscript \ libgmpxx4ldbl \ + pandoc \ python-pkg-resources \ python3-minimal \ python3-yaml \ python3-plastex \ - python3-markdown \ + rsvg-convert \ texlive-fonts-recommended \ texlive-lang-cyrillic \ texlive-latex-extra \ diff --git a/debian/control b/debian/control index 43410292..d1bf4179 100644 --- a/debian/control +++ b/debian/control @@ -8,7 +8,7 @@ Homepage: https://github.com/Kattis/problemtools Package: kattis-problemtools Architecture: any -Depends: ${shlibs:Depends}, ${python3:Depends}, ${misc:Depends}, python3-plastex, python3-markdown, python3-pkg-resources, texlive-plain-generic, texlive-fonts-recommended, texlive-latex-extra, texlive-lang-cyrillic, tidy, ghostscript, dvisvgm +Depends: ${shlibs:Depends}, ${python3:Depends}, ${misc:Depends}, pandoc, python3-plastex, python3-pkg-resources, rsvg-convert, texlive-plain-generic, texlive-fonts-recommended, texlive-latex-extra, texlive-lang-cyrillic, tidy, ghostscript, dvisvgm Recommends: gcc, g++ Description: Kattis Problem Tools These are tools to manage and verify problem packages in the From 762599f9f70364787257b61825a19e3134da30ce Mon Sep 17 00:00:00 2001 From: matistjati Date: Sun, 18 Aug 2024 10:57:15 +0200 Subject: [PATCH 19/25] Add problem names --- examples/different/problem.yaml | 5 +++++ examples/guess/problem.yaml | 1 + examples/hello/problem.yaml | 3 +++ examples/oddecho/problem.yaml | 4 ++-- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/examples/different/problem.yaml b/examples/different/problem.yaml index 279a8acb..a7652c2e 100644 --- a/examples/different/problem.yaml +++ b/examples/different/problem.yaml @@ -5,6 +5,11 @@ ## Author of the problem (default: null) # author: +# The problem name +# En may be omitted, as there is only one language +name: + en: A Different Problem + ## Where the problem was first used (default: null) source: Kattis # source_url: diff --git a/examples/guess/problem.yaml b/examples/guess/problem.yaml index c1e29500..bf832bb2 100644 --- a/examples/guess/problem.yaml +++ b/examples/guess/problem.yaml @@ -4,6 +4,7 @@ license: cc by-sa validation: custom interactive name: sv: Gissa talet + en: Guess the Number # Override standard limits: say that the TLE solutions provided should # be at least 4 times above the time limit in order for us to be diff --git a/examples/hello/problem.yaml b/examples/hello/problem.yaml index 194b060f..6c6f791e 100644 --- a/examples/hello/problem.yaml +++ b/examples/hello/problem.yaml @@ -1,5 +1,8 @@ source: Kattis license: public domain +name: + sv: Hej Världen! + en: Hello World! # Fix memory limit at 512 MB. (Note that for most problems, this # should not be done. It is only done in this case because we include diff --git a/examples/oddecho/problem.yaml b/examples/oddecho/problem.yaml index f213fbd9..3a918455 100644 --- a/examples/oddecho/problem.yaml +++ b/examples/oddecho/problem.yaml @@ -3,7 +3,7 @@ author: Johan Sannemo source: Principles of Algorithmic Problem Solving type: scoring name: - en: Echo - sv: Eko + en: Odd Echo + sv: Udda Eko grading: show_test_data_groups: true From 2bba9d4c935c6a51690bcdf9fbff9f731a2f2002 Mon Sep 17 00:00:00 2001 From: matistjati Date: Sun, 18 Aug 2024 11:04:22 +0200 Subject: [PATCH 20/25] Added problem name to test hello package --- problemtools/tests/hello/problem.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/problemtools/tests/hello/problem.yaml b/problemtools/tests/hello/problem.yaml index 194b060f..6c6f791e 100644 --- a/problemtools/tests/hello/problem.yaml +++ b/problemtools/tests/hello/problem.yaml @@ -1,5 +1,8 @@ source: Kattis license: public domain +name: + sv: Hej Världen! + en: Hello World! # Fix memory limit at 512 MB. (Note that for most problems, this # should not be done. It is only done in this case because we include From cdd1804a06605db2c823e288834c48742486127a Mon Sep 17 00:00:00 2001 From: matistjati Date: Sun, 18 Aug 2024 11:21:34 +0200 Subject: [PATCH 21/25] Improve security by running pandoc without shell capabilities --- problemtools/md2html.py | 9 +++++---- problemtools/problem2pdf.py | 4 ++-- problemtools/statement_common.py | 4 +++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/problemtools/md2html.py b/problemtools/md2html.py index d764b4f4..76ccd4e4 100644 --- a/problemtools/md2html.py +++ b/problemtools/md2html.py @@ -4,9 +4,8 @@ import os.path import string import argparse -import re import json -from typing import Optional +import subprocess from . import statement_common @@ -34,7 +33,8 @@ def convert(problem: str, options: argparse.Namespace) -> None: _copy_images(statement_path, lambda img_name: handle_image(os.path.join(problem, "problem_statement", img_name))) - statement_html = os.popen(f"pandoc {statement_path} -t html").read() + command = ["pandoc", statement_path, "-t" , "html"] + statement_html = subprocess.run(command, capture_output=True, text=True, shell=False).stdout templatepaths = [os.path.join(os.path.dirname(__file__), 'templates/markdown'), os.path.join(os.path.dirname(__file__), '../templates/markdown'), @@ -102,7 +102,8 @@ def json_dfs(data, callback) -> None: def _copy_images(statement_path, callback): - statement_json = os.popen(f"pandoc {statement_path} -t json").read() + command = ["pandoc", statement_path, "-t" , "json"] + statement_json = subprocess.run(command, capture_output=True, text=True, shell=False).stdout json_dfs(json.loads(statement_json), callback) diff --git a/problemtools/problem2pdf.py b/problemtools/problem2pdf.py index 63c1a1cd..4eeeea0f 100644 --- a/problemtools/problem2pdf.py +++ b/problemtools/problem2pdf.py @@ -53,8 +53,8 @@ def convert(options: argparse.Namespace) -> bool: with tempfile.NamedTemporaryFile(mode='w', suffix=".md") as temp_file: temp_file.write(statement_md) temp_file.flush() - # Do .read so that the temp file isn't deleted until pandoc is done - os.popen(f"pandoc {temp_file.name} -o {problembase}.pdf --resource-path={statement_dir}").read() + command = ["pandoc", temp_file.name, "-o", f"{problembase}.pdf", f"--resource-path={statement_dir}"] + subprocess.run(command, capture_output=True, text=True, shell=False) else: # Set up template if necessary diff --git a/problemtools/statement_common.py b/problemtools/statement_common.py index cb6960f0..5b2bd29f 100644 --- a/problemtools/statement_common.py +++ b/problemtools/statement_common.py @@ -2,6 +2,7 @@ from typing import Optional, List import html import tempfile +import subprocess from . import verifyproblem @@ -164,7 +165,8 @@ def format_samples(problem_root: str, to_pdf: bool = False) -> List[str]: with tempfile.NamedTemporaryFile(mode='w', suffix=".html") as temp_file: temp_file.write(samples[-1]) temp_file.flush() - samples[-1] = os.popen(f"pandoc {temp_file.name} -t markdown").read() + command = ["pandoc", temp_file.name, "-t" , "markdown"] + samples[-1] = subprocess.run(command, capture_output=True, text=True, shell=False).stdout casenum += 1 From 194c7b1a11e1af2bbe16b95232c8b7c3c8ea6727 Mon Sep 17 00:00:00 2001 From: matistjati Date: Sun, 18 Aug 2024 11:33:09 +0200 Subject: [PATCH 22/25] Refactoring --- problemtools/md2html.py | 16 +++++++++------- problemtools/problem2pdf.py | 18 +++++++++--------- problemtools/statement_common.py | 16 ++++++++-------- problemtools/templates/markdown/problem.css | 13 +++++-------- 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/problemtools/md2html.py b/problemtools/md2html.py index 76ccd4e4..62a8e153 100644 --- a/problemtools/md2html.py +++ b/problemtools/md2html.py @@ -1,6 +1,5 @@ #! /usr/bin/env python3 # -*- coding: utf-8 -*- -import html import os.path import string import argparse @@ -12,9 +11,9 @@ FOOTNOTES_STRING = '
' -def convert(problem: str, options: argparse.Namespace) -> None: +def convert(problem: str, options: argparse.Namespace) -> bool: """Convert a Markdown statement to HTML - + Args: problem: path to problem directory options: command-line arguments. See problem2html.py @@ -34,7 +33,8 @@ def convert(problem: str, options: argparse.Namespace) -> None: _copy_images(statement_path, lambda img_name: handle_image(os.path.join(problem, "problem_statement", img_name))) command = ["pandoc", statement_path, "-t" , "html"] - statement_html = subprocess.run(command, capture_output=True, text=True, shell=False).stdout + statement_html = subprocess.run(command, capture_output=True, text=True, + shell=False, check=True).stdout templatepaths = [os.path.join(os.path.dirname(__file__), 'templates/markdown'), os.path.join(os.path.dirname(__file__), '../templates/markdown'), @@ -66,11 +66,13 @@ def convert(problem: str, options: argparse.Namespace) -> None: with open("problem.css", "w") as output_file: with open(os.path.join(templatepath, "problem.css"), "r") as input_file: output_file.write(input_file.read()) + + return True def handle_image(src: str) -> None: """This is called for every image in the statement - Copies the image from the statement to the output directory + Copies the image from the statement to the output directory Args: src: full file path to the image @@ -103,7 +105,8 @@ def json_dfs(data, callback) -> None: def _copy_images(statement_path, callback): command = ["pandoc", statement_path, "-t" , "json"] - statement_json = subprocess.run(command, capture_output=True, text=True, shell=False).stdout + statement_json = subprocess.run(command, capture_output=True, + text=True, shell=False, check=True).stdout json_dfs(json.loads(statement_json), callback) @@ -135,4 +138,3 @@ def _substitute_template(templatepath: str, templatefile: str, **params) -> str: with open(os.path.join(templatepath, templatefile), "r", encoding="utf-8") as template_file: html_template = template_file.read() % params return html_template - diff --git a/problemtools/problem2pdf.py b/problemtools/problem2pdf.py index 4eeeea0f..911c5cb6 100644 --- a/problemtools/problem2pdf.py +++ b/problemtools/problem2pdf.py @@ -20,7 +20,7 @@ def convert(options: argparse.Namespace) -> bool: if not os.path.isfile(statement_path): raise Exception(f"Error! {statement_path} is not a file") - + templatepaths = [os.path.join(os.path.dirname(__file__), 'templates/markdown_pdf'), os.path.join(os.path.dirname(__file__), '../templates/markdown_pdf'), '/usr/lib/problemtools/templates/markdown_pdf'] @@ -30,20 +30,20 @@ def convert(options: argparse.Namespace) -> bool: table_fix_path = os.path.join(templatepath, "fix_tables.md") if not os.path.isfile(table_fix_path): raise Exception("Could not find markdown pdf template") - - with open(table_fix_path, "r") as f: - table_fix = f.read() + + with open(table_fix_path, "r") as file: + table_fix = file.read() statement_dir = os.path.join(problem_root, "problem_statement") - with open(statement_path, "r") as f: - statement_md = f.read() - + with open(statement_path, "r") as file: + statement_md = file.read() + problem_name = statement_common.get_problem_name(problem_root, options.language) # Add code that adds vertical and horizontal lines to all tables statement_md = r'\centerline{\huge %s}' % problem_name + statement_md statement_md = table_fix + statement_md - + # Hacky: html samples -> md. Then we append to the markdown document samples = "\n".join(statement_common.format_samples(problem_root, to_pdf=True)) @@ -54,7 +54,7 @@ def convert(options: argparse.Namespace) -> bool: temp_file.write(statement_md) temp_file.flush() command = ["pandoc", temp_file.name, "-o", f"{problembase}.pdf", f"--resource-path={statement_dir}"] - subprocess.run(command, capture_output=True, text=True, shell=False) + return subprocess.run(command, capture_output=True, text=True, shell=False, check=True) else: # Set up template if necessary diff --git a/problemtools/statement_common.py b/problemtools/statement_common.py index 5b2bd29f..97b71170 100644 --- a/problemtools/statement_common.py +++ b/problemtools/statement_common.py @@ -26,7 +26,7 @@ def find_statement(problem_root: str, extension: str, language: Optional[str]) - def find_statement_extension(problem_root: str, language: Optional[str]) -> str: """Given a language, find whether the extension is tex or md - + Args: problem_root: path to problem root """ @@ -51,7 +51,7 @@ def get_problem_name(problem: str, language: Optional[str]) -> Optional[str]: with verifyproblem.Problem(problem) as prob: config = verifyproblem.ProblemConfig(prob) if not config.check(None): - raise Exception(f"Invalid problem.yaml") + raise Exception("Invalid problem.yaml") names = config.get("name") # If there is only one language, per the spec that is the one we want if len(names) == 1: @@ -64,7 +64,7 @@ def get_problem_name(problem: str, language: Optional[str]) -> Optional[str]: def format_samples(problem_root: str, to_pdf: bool = False) -> List[str]: """Read all samples from the problem directory and convert them to pandoc-valid markdown - + Args: problem_root: path to root of problem to_pdf: whether the outputted samples should be valid for for html or pdf @@ -73,7 +73,7 @@ def format_samples(problem_root: str, to_pdf: bool = False) -> List[str]: List[str]: All samples, converted to a format appropriate to be pasted into a markdown file. Ordered lexicographically by file names """ - + sample_path = os.path.join(problem_root, "data", "sample") if not os.path.isdir(sample_path): print("WARNING!! no sample folder") @@ -95,7 +95,7 @@ def format_samples(problem_root: str, to_pdf: bool = False) -> List[str]: Write """ - + with open(os.path.join(sample_path, sample), "r", encoding="utf-8") as infile: sample_interaction = infile.readlines() lines = [] @@ -159,16 +159,16 @@ def format_samples(problem_root: str, to_pdf: bool = False) -> List[str]: """ % ({"case": casenum, "input": html.escape(sample_input), "output": html.escape(sample_output)})) - + if to_pdf: # If pdf, convert to markdown with tempfile.NamedTemporaryFile(mode='w', suffix=".html") as temp_file: temp_file.write(samples[-1]) temp_file.flush() command = ["pandoc", temp_file.name, "-t" , "markdown"] - samples[-1] = subprocess.run(command, capture_output=True, text=True, shell=False).stdout + samples[-1] = subprocess.run(command, capture_output=True, text=True, + shell=False, check=True).stdout casenum += 1 return samples - diff --git a/problemtools/templates/markdown/problem.css b/problemtools/templates/markdown/problem.css index 8d354eca..ca6e72ed 100644 --- a/problemtools/templates/markdown/problem.css +++ b/problemtools/templates/markdown/problem.css @@ -35,9 +35,6 @@ table:not(.sample) td { /*Style sample in its own way*/ .sample { font-family: Arial, Helvetica, sans-serif; -} - -.sample { width: 100%; } @@ -94,20 +91,20 @@ div.sampleinteractionread { border: 1px solid black; width: 60%; float: left; - margin: 3px 0px 3px 0px; + margin: 3px 0px; } .sampleinteractionread pre { - margin: 1px 5px 1px 5px; + margin: 1px 5px; } div.sampleinteractionwrite { border: 1px solid black; width: 60%; float: right; - margin: 3px 0px 3px 0px; + margin: 3px 0px; } .sampleinteractionwrite pre { - margin: 1px 5px 1px 5px; -} \ No newline at end of file + margin: 1px 5px; +} From 554892a122a7e84516486d78888d18a92f8f81ee Mon Sep 17 00:00:00 2001 From: matistjati Date: Sun, 18 Aug 2024 11:59:18 +0200 Subject: [PATCH 23/25] Even more refactoring --- problemtools/md2html.py | 2 +- problemtools/problem2pdf.py | 117 +++++++++++--------- problemtools/statement_common.py | 181 ++++++++++++++++++------------- 3 files changed, 174 insertions(+), 126 deletions(-) diff --git a/problemtools/md2html.py b/problemtools/md2html.py index 62a8e153..7b834d22 100644 --- a/problemtools/md2html.py +++ b/problemtools/md2html.py @@ -66,7 +66,7 @@ def convert(problem: str, options: argparse.Namespace) -> bool: with open("problem.css", "w") as output_file: with open(os.path.join(templatepath, "problem.css"), "r") as input_file: output_file.write(input_file.read()) - + return True diff --git a/problemtools/problem2pdf.py b/problemtools/problem2pdf.py index 911c5cb6..09ed5962 100644 --- a/problemtools/problem2pdf.py +++ b/problemtools/problem2pdf.py @@ -12,80 +12,93 @@ def convert(options: argparse.Namespace) -> bool: problem_root = os.path.realpath(options.problem) + + if statement_common.find_statement_extension(problem_root, language=options.language) == "md": + return md2pdf(options) + else: + return latex2pdf(options) + + +def md2pdf(options: argparse.Namespace) -> bool: + problem_root = os.path.realpath(options.problem) problembase = os.path.splitext(os.path.basename(problem_root))[0] destfile = string.Template(options.destfile).safe_substitute(problem=problembase) - if statement_common.find_statement_extension(problem_root, language=options.language) == "md": - statement_path = statement_common.find_statement(problem_root, extension="md", language=options.language) + statement_path = statement_common.find_statement(problem_root, extension="md", language=options.language) - if not os.path.isfile(statement_path): - raise Exception(f"Error! {statement_path} is not a file") + if not os.path.isfile(statement_path): + raise Exception(f"Error! {statement_path} is not a file") - templatepaths = [os.path.join(os.path.dirname(__file__), 'templates/markdown_pdf'), - os.path.join(os.path.dirname(__file__), '../templates/markdown_pdf'), - '/usr/lib/problemtools/templates/markdown_pdf'] - templatepath = next((p for p in templatepaths - if os.path.isdir(p) and os.path.isfile(os.path.join(p, "fix_tables.md"))), - None) - table_fix_path = os.path.join(templatepath, "fix_tables.md") - if not os.path.isfile(table_fix_path): - raise Exception("Could not find markdown pdf template") + templatepaths = [os.path.join(os.path.dirname(__file__), 'templates/markdown_pdf'), + os.path.join(os.path.dirname(__file__), '../templates/markdown_pdf'), + '/usr/lib/problemtools/templates/markdown_pdf'] + templatepath = next((p for p in templatepaths + if os.path.isdir(p) and os.path.isfile(os.path.join(p, "fix_tables.md"))), + None) + table_fix_path = os.path.join(templatepath, "fix_tables.md") + if not os.path.isfile(table_fix_path): + raise Exception("Could not find markdown pdf template") - with open(table_fix_path, "r") as file: - table_fix = file.read() + with open(table_fix_path, "r") as file: + table_fix = file.read() - statement_dir = os.path.join(problem_root, "problem_statement") - with open(statement_path, "r") as file: - statement_md = file.read() + statement_dir = os.path.join(problem_root, "problem_statement") + with open(statement_path, "r") as file: + statement_md = file.read() - problem_name = statement_common.get_problem_name(problem_root, options.language) + problem_name = statement_common.get_problem_name(problem_root, options.language) - # Add code that adds vertical and horizontal lines to all tables - statement_md = r'\centerline{\huge %s}' % problem_name + statement_md - statement_md = table_fix + statement_md + # Add code that adds vertical and horizontal lines to all tables + statement_md = r'\centerline{\huge %s}' % problem_name + statement_md + statement_md = table_fix + statement_md - # Hacky: html samples -> md. Then we append to the markdown document - samples = "\n".join(statement_common.format_samples(problem_root, to_pdf=True)) + samples = "\n".join(statement_common.format_samples(problem_root, to_pdf=True)) - # If we don't add newline, the table might get attached to a footnote - statement_md += "\n" + samples + # If we don't add newline, the topmost table might get attached to a footnote + statement_md += "\n" + samples - with tempfile.NamedTemporaryFile(mode='w', suffix=".md") as temp_file: - temp_file.write(statement_md) - temp_file.flush() - command = ["pandoc", temp_file.name, "-o", f"{problembase}.pdf", f"--resource-path={statement_dir}"] - return subprocess.run(command, capture_output=True, text=True, shell=False, check=True) + with tempfile.NamedTemporaryFile(mode='w', suffix=".md") as temp_file: + temp_file.write(statement_md) + temp_file.flush() + command = ["pandoc", temp_file.name, "-o", destfile, f"--resource-path={statement_dir}"] + return subprocess.run(command, capture_output=True, text=True, shell=False, check=True) - else: - # Set up template if necessary - with template.Template(problem_root, language=options.language) as templ: - texfile = templ.get_file_name() - origcwd = os.getcwd() +def latex2pdf(options: argparse.Namespace) -> bool: + problem_root = os.path.realpath(options.problem) + problembase = os.path.splitext(os.path.basename(problem_root))[0] + destfile = string.Template(options.destfile).safe_substitute(problem=problembase) - os.chdir(os.path.dirname(texfile)) - params = ['pdflatex', '-interaction=nonstopmode'] - output = None - if options.quiet: - output = open(os.devnull, 'w') - if options.nopdf: - params.append('-draftmode') + # Set up template if necessary + with template.Template(problem_root, language=options.language) as templ: + texfile = templ.get_file_name() - params.append(texfile) + origcwd = os.getcwd() + os.chdir(os.path.dirname(texfile)) + params = ['pdflatex', '-interaction=nonstopmode'] + output = None + if options.quiet: + output = open(os.devnull, 'w') + if options.nopdf: + params.append('-draftmode') + + params.append(texfile) + + status = subprocess.call(params, stdout=output) + if status == 0: status = subprocess.call(params, stdout=output) - if status == 0: - status = subprocess.call(params, stdout=output) - if output is not None: - output.close() + if output is not None: + output.close() + + os.chdir(origcwd) - os.chdir(origcwd) + if status == 0 and not options.nopdf: + shutil.move(os.path.splitext(texfile)[0] + '.pdf', destfile) - if status == 0 and not options.nopdf: - shutil.move(os.path.splitext(texfile)[0] + '.pdf', destfile) + return status == 0 - return status == 0 def get_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) diff --git a/problemtools/statement_common.py b/problemtools/statement_common.py index 97b71170..66a6c673 100644 --- a/problemtools/statement_common.py +++ b/problemtools/statement_common.py @@ -82,55 +82,7 @@ def format_samples(problem_root: str, to_pdf: bool = False) -> List[str]: casenum = 1 for sample in sorted(os.listdir(sample_path)): if sample.endswith(".interaction"): - if to_pdf: - line = r"""\begin{tabular}{p{0.3\textwidth} p{0.5\textwidth} p{0.0\textwidth}} -\textbf{Read} & \textbf{Sample Interaction %i} & \textbf{Write} \\ -\end{tabular}""" % casenum - else: - line = f""" - - - - - - -
ReadSample Interaction {casenum}Write
""" - - with open(os.path.join(sample_path, sample), "r", encoding="utf-8") as infile: - sample_interaction = infile.readlines() - lines = [] - for interaction in sample_interaction: - data = interaction[1:] - if to_pdf: - if interaction[0] == '>': - left = True - elif interaction[0] == '<': - left = False - else: - print(f"Warning: Interaction had unknown prefix {interaction[0]}") - lines.append(r""" - \begin{table}[H] - %(justify)s\begin{tabular}{|p{0.6\textwidth}|} - \hline - %(text)s \\ - \hline - \end{tabular} - \end{table}""" % {"justify": "" if left else "\\hspace*{\\fill}\n", - "text": data}) - else: - line_type = "" - if interaction[0] == '>': - line_type = "sampleinteractionwrite" - elif interaction[0] == '<': - line_type = "sampleinteractionread" - else: - print(f"Warning: Interaction had unknown prefix {interaction[0]}") - lines.append(f"""
{data}
""") - - if to_pdf: - samples.append(line + '\\vspace{-15pt}'.join(lines)) - else: - samples.append(line + ''.join(lines)) + samples.append(format_interactive_sample(sample_path, sample, casenum, to_pdf)) casenum += 1 continue @@ -140,35 +92,118 @@ def format_samples(problem_root: str, to_pdf: bool = False) -> List[str]: outpath = os.path.join(sample_path, sample_name + ".ans") if not os.path.isfile(outpath): continue - with open(os.path.join(sample_path, sample), "r", encoding="utf-8") as infile: - sample_input = infile.read() - with open(outpath, "r", encoding="utf-8") as outfile: - sample_output = outfile.read() - samples.append(""" + samples.append(format_normal_sample(sample_path, sample, casenum, to_pdf)) + casenum += 1 + + return samples + + +def format_normal_sample(sample_root: str, sample: str, casenum: int, to_pdf: bool) -> str: + """ + + Args: + sample_root: root of the sample folder + sample: file name of the sample + casenum: which sample is this? (1, 2, 3...) + to_pdf: do we target pdf or html output + + Returns: + str: the sample, ready to be pasted into a markdown doc and fed to pandoc + """ + + with open(os.path.join(sample_root, sample), "r", encoding="utf-8") as infile: + sample_input = infile.read() + sample_name = sample[:-3] + outpath = os.path.join(sample_root, sample_name + ".ans") + with open(outpath, "r", encoding="utf-8") as outfile: + sample_output = outfile.read() + + sample = """ + + + + + + + + + + + +
Sample Input %(case)dSample Output %(case)d
%(input)s
%(output)s
""" % ({"case": casenum, "input": html.escape(sample_input), + "output": html.escape(sample_output)}) + + if to_pdf: + # If pdf, convert to markdown + with tempfile.NamedTemporaryFile(mode='w', suffix=".html") as temp_file: + temp_file.write(sample) + temp_file.flush() + command = ["pandoc", temp_file.name, "-t" , "markdown"] + return subprocess.run(command, capture_output=True, text=True, + shell=False, check=True).stdout + else: + return sample + + +def format_interactive_sample(sample_root: str, sample: str, casenum: int, to_pdf: bool) -> str: + """ + + Args: + sample_root: root of the sample folder + sample: file name of the sample + casenum: which sample is this? (1, 2, 3...) + to_pdf: do we target pdf or html output + + Returns: + str: the sample, ready to be pasted into a markdown doc and fed to pandoc + """ + if to_pdf: + line = r"""\begin{tabular}{p{0.3\textwidth} p{0.5\textwidth} p{0.0\textwidth}} +\textbf{Read} & \textbf{Sample Interaction %i} & \textbf{Write} \\ +\end{tabular}""" % casenum + else: + line = f""" - - - + + + - - - - -
Sample Input %(case)dSample Output %(case)dReadSample Interaction {casenum}Write
%(input)s
%(output)s
""" - % ({"case": casenum, "input": html.escape(sample_input), "output": html.escape(sample_output)})) + with open(os.path.join(sample_root, sample), "r", encoding="utf-8") as infile: + sample_interaction = infile.readlines() + lines = [] + for interaction in sample_interaction: + data = interaction[1:] if to_pdf: - # If pdf, convert to markdown - with tempfile.NamedTemporaryFile(mode='w', suffix=".html") as temp_file: - temp_file.write(samples[-1]) - temp_file.flush() - command = ["pandoc", temp_file.name, "-t" , "markdown"] - samples[-1] = subprocess.run(command, capture_output=True, text=True, - shell=False, check=True).stdout - - casenum += 1 + if interaction[0] == '>': + left = True + elif interaction[0] == '<': + left = False + else: + print(f"Warning: Interaction had unknown prefix {interaction[0]}") + lines.append(r""" + \begin{table}[H] + %(justify)s\begin{tabular}{|p{0.6\textwidth}|} + \hline + %(text)s \\ + \hline + \end{tabular} + \end{table}""" % {"justify": "" if left else "\\hspace*{\\fill}\n", + "text": data}) + else: + line_type = "" + if interaction[0] == '>': + line_type = "sampleinteractionwrite" + elif interaction[0] == '<': + line_type = "sampleinteractionread" + else: + print(f"Warning: Interaction had unknown prefix {interaction[0]}") + lines.append(f"""
{data}
""") - return samples + if to_pdf: + return line + '\\vspace{-15pt}'.join(lines) + else: + return line + ''.join(lines) From d8a4c3e79c91776c924edcee560271fcc94cd8f3 Mon Sep 17 00:00:00 2001 From: matistjati Date: Sun, 18 Aug 2024 12:12:25 +0200 Subject: [PATCH 24/25] Remove python3-markdown dependency --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4e708b1a..96758f52 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ The dependencies needed to *build/install* problemtools can be installed with: And the dependencies needed to *run* problemtools can be installed with: - sudo apt install ghostscript libgmpxx4ldbl pandoc python3-minimal python-pkg-resources python3-plastex python3-markdown python3-yaml rsvg-convert texlive-fonts-recommended texlive-lang-cyrillic texlive-latex-extra texlive-plain-generic tidy + sudo apt install ghostscript libgmpxx4ldbl pandoc python3-minimal python-pkg-resources python3-plastex python3-yaml rsvg-convert texlive-fonts-recommended texlive-lang-cyrillic texlive-latex-extra texlive-plain-generic tidy ### Fedora From 7390fb815d4cc72fe7a3a847389d37b1c3e31434 Mon Sep 17 00:00:00 2001 From: matistjati Date: Sun, 18 Aug 2024 12:55:15 +0200 Subject: [PATCH 25/25] Add problem id to pdf and small fixes --- examples/README.md | 6 ------ problemtools/md2html.py | 6 +++--- problemtools/problem2pdf.py | 2 ++ .../{markdown => markdown_html}/default-layout.html | 0 .../templates/{markdown => markdown_html}/problem.css | 2 -- 5 files changed, 5 insertions(+), 11 deletions(-) rename problemtools/templates/{markdown => markdown_html}/default-layout.html (100%) rename problemtools/templates/{markdown => markdown_html}/problem.css (99%) diff --git a/examples/README.md b/examples/README.md index d1076a7e..9d7f9ee5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -26,9 +26,3 @@ more than one language. This is an example of a *scoring* problem where submissions can get different scores depending on which test groups they solve. It also demonstrates how an input validator might check different constraints for different test groups. The swedish statement showcases how to use images, footnotes and tables in Markdown. - -# bplusa - -This is an example of a problem using an output validator, where there are multiple valid answers. -The output validator is written pretty generally, guarding against the most common mistakes when using -output validators. It also demonstrates using Markdown as a statement language. diff --git a/problemtools/md2html.py b/problemtools/md2html.py index 7b834d22..3d729e72 100644 --- a/problemtools/md2html.py +++ b/problemtools/md2html.py @@ -36,9 +36,9 @@ def convert(problem: str, options: argparse.Namespace) -> bool: statement_html = subprocess.run(command, capture_output=True, text=True, shell=False, check=True).stdout - templatepaths = [os.path.join(os.path.dirname(__file__), 'templates/markdown'), - os.path.join(os.path.dirname(__file__), '../templates/markdown'), - '/usr/lib/problemtools/templates/markdown'] + templatepaths = [os.path.join(os.path.dirname(__file__), 'templates/markdown_html'), + os.path.join(os.path.dirname(__file__), '../templates/markdown_html'), + '/usr/lib/problemtools/templates/markdown_html'] templatepath = next((p for p in templatepaths if os.path.isdir(p) and os.path.isfile(os.path.join(p, "default-layout.html"))), None) diff --git a/problemtools/problem2pdf.py b/problemtools/problem2pdf.py index 09ed5962..62d40dbe 100644 --- a/problemtools/problem2pdf.py +++ b/problemtools/problem2pdf.py @@ -48,7 +48,9 @@ def md2pdf(options: argparse.Namespace) -> bool: problem_name = statement_common.get_problem_name(problem_root, options.language) + problem_id = os.path.basename(problem_root) # Add code that adds vertical and horizontal lines to all tables + statement_md = r'\centerline{\large %s}' % f"Problem id: {problem_id}" + statement_md statement_md = r'\centerline{\huge %s}' % problem_name + statement_md statement_md = table_fix + statement_md diff --git a/problemtools/templates/markdown/default-layout.html b/problemtools/templates/markdown_html/default-layout.html similarity index 100% rename from problemtools/templates/markdown/default-layout.html rename to problemtools/templates/markdown_html/default-layout.html diff --git a/problemtools/templates/markdown/problem.css b/problemtools/templates/markdown_html/problem.css similarity index 99% rename from problemtools/templates/markdown/problem.css rename to problemtools/templates/markdown_html/problem.css index ca6e72ed..c38a4d97 100644 --- a/problemtools/templates/markdown/problem.css +++ b/problemtools/templates/markdown_html/problem.css @@ -85,8 +85,6 @@ td { vertical-align:top; } - - div.sampleinteractionread { border: 1px solid black; width: 60%;