Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[latex] Add command problem_slides #413

Merged
merged 6 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ def build_contest_zip(problems, zipfiles, outfile, statement_language):
]
+ list(Path('.').glob(f'contest*.{statement_language}.pdf'))
+ list(Path('.').glob(f'solutions*.{statement_language}.pdf'))
+ list(Path('.').glob(f'problem-slides*.{statement_language}.pdf'))
):
if Path(fname).is_file():
zf.write(
Expand Down
124 changes: 76 additions & 48 deletions bin/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
import re
import shutil
import sys
from enum import Enum
from pathlib import Path
from typing import Optional

from colorama import Fore, Style

import config
from contest import contest_yaml
from contest import contest_yaml, problems_yaml
import problem
from util import (
copy_and_substitute,
Expand All @@ -26,6 +27,12 @@
)


class PdfType(str, Enum):
PROBLEM = 'problem'
PROBLEM_SLIDE = 'problem-slide'
SOLUTION = 'solution'


def latex_builddir(problem: "problem.Problem", language: str) -> Path:
builddir = problem.tmpdir / 'latex' / language
builddir.mkdir(parents=True, exist_ok=True)
Expand Down Expand Up @@ -175,6 +182,33 @@ def get_tl(problem: "problem.Problem"):
return tl if print_tl else ''


def problem_data(problem: "problem.Problem", language: str):
background = next(
(p['rgb'][1:] for p in problems_yaml() if p['id'] == str(problem.path) and 'rgb' in p),
'ffffff',
)
# Source: https://github.com/DOMjudge/domjudge/blob/095854650facda41dbb40966e70199840b887e33/webapp/src/Twig/TwigExtension.php#L1056
foreground = (
'000000' if sum(int(background[i : i + 2], 16) for i in range(0, 6, 2)) > 450 else 'ffffff'
)
border = "".join(
("00" + hex(max(0, int(background[i : i + 2], 16) - 64))[2:])[-2:] for i in range(0, 6, 2)
)

return {
'problemlabel': problem.label,
'problemyamlname': problem.settings.name[language].replace('_', ' '),
'problemauthor': problem.settings.author,
'problembackground': background,
'problemforeground': foreground,
'problemborder': border,
'timelimit': get_tl(problem),
'problemdir': problem.path.absolute().as_posix(),
'problemdirname': problem.name,
'builddir': latex_builddir(problem, language).as_posix(),
}


def make_environment() -> dict[str, str]:
env = os.environ.copy()
# Search the contest directory and the latex directory.
Expand Down Expand Up @@ -312,13 +346,15 @@ def run_latexmk(stdout, stderr):
# 1. Copy the latex/problem.tex file to tmpdir/<problem>/latex/<language>/problem.tex,
# substituting variables.
# 2. Create tmpdir/<problem>/latex/<language>/samples.tex.
# 3. Run latexmk and link the resulting problem.<language>.pdf into the problem directory.
def build_problem_pdf(problem: "problem.Problem", language: str, solution=False, web=False):
# 3. Run latexmk and link the resulting <build_type>.<language>.pdf into the problem directory.
def build_problem_pdf(
problem: "problem.Problem", language: str, build_type=PdfType.PROBLEM, web=False
):
"""
Arguments:
-- language: str, the two-letter language code appearing the file name, such as problem.en.tex
"""
main_file = 'solution' if solution else 'problem'
main_file = build_type.value
main_file += '-web.tex' if web else '.tex'

bar = PrintBar(f'{main_file[:-3]}{language}.pdf')
Expand All @@ -332,21 +368,13 @@ def build_problem_pdf(problem: "problem.Problem", language: str, solution=False,
copy_and_substitute(
local_data if local_data.is_file() else config.tools_root / 'latex' / main_file,
builddir / main_file,
{
'problemlabel': problem.label,
'problemyamlname': problem.settings.name[language].replace('_', ' '),
'problemauthor': problem.settings.author,
'timelimit': get_tl(problem),
'problemdir': problem.path.absolute().as_posix(),
'problemdirname': problem.name,
'builddir': builddir.as_posix(),
},
problem_data(problem, language),
)

return build_latex_pdf(builddir, builddir / main_file, language, bar, problem.path)


def build_problem_pdfs(problem, solutions=False, web=False):
def build_problem_pdfs(problem: "problem.Problem", build_type=PdfType.PROBLEM, web=False):
"""Build PDFs for various languages. If list of languages is specified,
(either via config files or --language arguments), build those. Otherwise
build all languages for which there is a statement latex source.
Expand All @@ -362,20 +390,22 @@ def build_problem_pdfs(problem, solutions=False, web=False):
languages = config.args.languages
else:
languages = problem.statement_languages
# For solutions, filter for `solution.xy.tex` files that exist.
if solutions:
# For solutions or problem slides, filter for `<build_type>.<language>.tex` files that exist.
if build_type != PdfType.PROBLEM:
filtered_languages = []
for lang in languages:
if (problem.path / 'problem_statement' / f'solution.{lang}.tex').exists():
if (problem.path / 'problem_statement' / f'{build_type.value}.{lang}.tex').exists():
filtered_languages.append(lang)
else:
message(
f'solution.{lang}.tex not found', problem.name, color_type=MessageType.WARN
f'{build_type.value}.{lang}.tex not found',
problem.name,
color_type=MessageType.WARN,
)
languages = filtered_languages
if config.args.watch and len(languages) > 1:
fatal('--watch does not work with multiple languages. Please use --language')
return all([build_problem_pdf(problem, lang, solutions, web) for lang in languages])
return all([build_problem_pdf(problem, lang, build_type, web) for lang in languages])


def find_logo() -> Path:
Expand All @@ -392,14 +422,16 @@ def build_contest_pdf(
problems: list["problem.Problem"],
tmpdir: Path,
language: str,
solutions=False,
build_type=PdfType.PROBLEM,
web=False,
) -> bool:
builddir = tmpdir / contest / 'latex' / language
builddir.mkdir(parents=True, exist_ok=True)
build_type = 'solution' if solutions else 'problem'

main_file = 'solutions' if solutions else 'contest'
problem_slides = build_type == PdfType.PROBLEM_SLIDE
solutions = build_type == PdfType.SOLUTION

main_file = 'problem-slides' if problem_slides else 'solutions' if solutions else 'contest'
main_file += '-web.tex' if web else '.tex'

bar = PrintBar(f'{main_file[:-3]}{language}.pdf')
Expand Down Expand Up @@ -441,41 +473,37 @@ def build_contest_pdf(
elif headertex.exists():
problems_data += f'\\input{{{headertex}}}\n'

local_per_problem_data = Path(f'contest-{build_type}.tex')
per_problem_data = (
local_per_problem_data = Path(f'contest-{build_type.value}.tex')
per_problem_data_tex = (
local_per_problem_data
if local_per_problem_data.is_file()
else config.tools_root / 'latex' / f'contest-{build_type}.tex'
else config.tools_root / 'latex' / f'contest-{build_type.value}.tex'
).read_text()

for problem in problems:
if build_type == 'problem':
if build_type == PdfType.PROBLEM:
prepare_problem(problem, language)

if solutions:
solutiontex = problem.path / 'problem_statement' / 'solution.tex'
solutionlangtex = problem.path / 'problem_statement' / f'solution.{language}.tex'
if solutionlangtex.is_file():
else: # i.e. for SOLUTION and PROBLEM_SLIDE
tex_no_lang = problem.path / 'problem_statement' / f'{build_type.value}.tex'
tex_with_lang = (
problem.path / 'problem_statement' / f'{build_type.value}.{language}.tex'
)
if tex_with_lang.is_file():
# All is good
pass
elif solutiontex.is_file():
bar.warn(f'Rename solution.tex to solution.{language}.tex', problem.name)
elif tex_no_lang.is_file():
bar.warn(
f'Rename {build_type.value}.tex to {build_type.value}.{language}.tex',
problem.name,
)
continue
else:
bar.warn(f'solution.{language}.tex not found', problem.name)
bar.warn(f'{build_type.value}.{language}.tex not found', problem.name)
continue

problems_data += substitute(
per_problem_data,
{
'problemlabel': problem.label,
'problemyamlname': problem.settings.name[language].replace('_', ' '),
'problemauthor': problem.settings.author,
'timelimit': get_tl(problem),
'problemdir': problem.path.absolute().as_posix(),
'problemdirname': problem.name,
'builddir': latex_builddir(problem, language).as_posix(),
},
per_problem_data_tex,
problem_data(problem, language),
)

if solutions:
Expand All @@ -487,14 +515,14 @@ def build_contest_pdf(
elif footertex.exists():
problems_data += f'\\input{{{footertex}}}\n'

(builddir / f'contest-{build_type}s.tex').write_text(problems_data)
(builddir / f'contest-{build_type.value}s.tex').write_text(problems_data)

return build_latex_pdf(builddir, Path(main_file), language, bar)


def build_contest_pdfs(contest, problems, tmpdir, lang=None, solutions=False, web=False):
def build_contest_pdfs(contest, problems, tmpdir, lang=None, build_type=PdfType.PROBLEM, web=False):
if lang:
return build_contest_pdf(contest, problems, tmpdir, lang, solutions, web)
return build_contest_pdf(contest, problems, tmpdir, lang, build_type, web)

"""Build contest PDFs for all available languages"""
statement_languages = set.intersection(*(set(p.statement_languages) for p in problems))
Expand All @@ -519,7 +547,7 @@ def build_contest_pdfs(contest, problems, tmpdir, lang=None, solutions=False, we
color_type=MessageType.FATAL,
)
return all(
[build_contest_pdf(contest, problems, tmpdir, lang, solutions, web) for lang in languages]
build_contest_pdf(contest, problems, tmpdir, lang, build_type, web) for lang in languages
)


Expand Down
79 changes: 69 additions & 10 deletions bin/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ def build_parser():
'--watch',
'-w',
action='store_true',
help='Continuously compile the pdf whenever a `problem_statement.tex` changes. Note that this does not pick up changes to `*.yaml` configuration files. Further Note that this implies `--cp`.',
help='Continuously compile the pdf whenever a `problem.*.tex` changes. Note that this does not pick up changes to `*.yaml` configuration files. Further Note that this implies `--cp`.',
)
pdfparser.add_argument(
'--open',
Expand All @@ -420,6 +420,27 @@ def build_parser():
pdfparser.add_argument('--web', action='store_true', help='Create a web version of the pdf.')
pdfparser.add_argument('-1', action='store_true', help='Only run the LaTeX compiler once.')

# Problem slides
pdfparser = subparsers.add_parser(
'problem_slides', parents=[global_parser], help='Build the problem slides pdf.'
)
pdfparser.add_argument('--no-timelimit', action='store_true', help='Do not print timelimits.')
pdfparser.add_argument(
'--watch',
'-w',
action='store_true',
help='Continuously compile the pdf whenever a `problem-slide.*.tex` changes. Note that this does not pick up changes to `*.yaml` configuration files.',
)
pdfparser.add_argument(
'--open',
'-o',
nargs='?',
const=True,
type=Path,
help='Open the continuously compiled pdf (with a specified program).',
)
pdfparser.add_argument('-1', action='store_true', help='Only run the LaTeX compiler once.')

# Solution slides
solparser = subparsers.add_parser(
'solutions', parents=[global_parser], help='Build the solution slides pdf.'
Expand All @@ -442,7 +463,7 @@ def build_parser():
'--watch',
'-w',
action='store_true',
help='Continuously compile the pdf whenever a `solution.tex` changes. Note that this does not pick up changes to `*.yaml` configuration files. Further Note that this implies `--cp`.',
help='Continuously compile the pdf whenever a `solution.*.tex` changes. Note that this does not pick up changes to `*.yaml` configuration files. Further Note that this implies `--cp`.',
)
solparser.add_argument(
'--open',
Expand Down Expand Up @@ -994,9 +1015,15 @@ def run_parsed_arguments(args):
# --all is passed.
if level == 'problem' or (level == 'problemset' and config.args.all):
success &= latex.build_problem_pdfs(problem)
if action in ['solutions']:
if level == 'problem':
success &= latex.build_problem_pdfs(problem, solutions=True, web=config.args.web)
if level == 'problem':
if action in ['solutions']:
success &= latex.build_problem_pdfs(
problem, build_type=latex.PdfType.SOLUTION, web=config.args.web
)
if action in ['problem_slides']:
success &= latex.build_problem_pdfs(
problem, build_type=latex.PdfType.PROBLEM_SLIDE, web=config.args.web
)
if action in ['validate', 'all']:
if not (action == 'validate' and (config.args.input or config.args.answer)):
success &= problem.validate_data(validate.Mode.INVALID)
Expand Down Expand Up @@ -1060,7 +1087,16 @@ def run_parsed_arguments(args):

if action in ['solutions']:
success &= latex.build_contest_pdfs(
contest, problems, tmpdir, solutions=True, web=config.args.web
contest, problems, tmpdir, build_type=latex.PdfType.SOLUTION, web=config.args.web
)

if action in ['problem_slides']:
success &= latex.build_contest_pdfs(
contest,
problems,
tmpdir,
build_type=latex.PdfType.PROBLEM_SLIDE,
web=config.args.web,
)

if action in ['zip']:
Expand All @@ -1074,12 +1110,35 @@ def run_parsed_arguments(args):
contest, problems, tmpdir, statement_language, web=True
)
if not config.args.no_solutions:
success &= latex.build_contest_pdfs(
contest, problems, tmpdir, statement_language, solutions=True
success &= latex.build_contest_pdf(
contest,
problems,
tmpdir,
statement_language,
build_type=latex.PdfType.SOLUTION,
)
success &= latex.build_contest_pdf(
contest,
problems,
tmpdir,
statement_language,
build_type=latex.PdfType.SOLUTION,
web=True,
)
success &= latex.build_contest_pdfs(
contest, problems, tmpdir, statement_language, solutions=True, web=True
# Only build the problem slides if at least one problem has the TeX for it
if any(
glob(problem.path / "problem_statement", "problem-slide.*.tex")
for problem in problems
):
success &= latex.build_contest_pdf(
Copy link
Owner

@RagnarGrootKoerkamp RagnarGrootKoerkamp Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this (the no_solutions flag) be more granular? It's annoying if things fail because no problem slides are present.

Or maybe better is to never make it an error if problem slides are missing? (But that's kinda tricky when you want slides for all problems?)

Also I checked an doing bt problem_slides at the contest level with some problem-slide.en.tex missing crashes in latex (because file not found). We should probably detect when there's not a single problem-slide present and then just Log that (without error). When only some files are present, it's fine to error IMO.

At the problem level it gives a nice message problem-slide.en.tex not found, so there all is good.

Otherwise LGTM! Thanks for making the PR :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point! Will reduce some errors to warnings today or tomorrow 😄

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some extra checks 🙂 The behaviour at the problem level hasn't changed, this is the new behaviour at the contest level:

  • When no problem-slide.*.tex are present:
    • bt problem_slides will warn for all problems, followed by a failing LaTeX compilation (because the user is explicitly trying to compile them)
    • bt zip will log "No problem has problem-slide.*.tex, skipping problem slides"
  • When at least one problem-slide.*.tex is present:
    • Both bt problem_slides and bt zip will warn for the problems that do not have it, but will compile the rest

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good. Thanks! Merging now.

contest,
problems,
tmpdir,
statement_language,
build_type=latex.PdfType.PROBLEM_SLIDE,
)
else:
log("No problem has problem-slide.*.tex, skipping problem slides")

outfile = contest + '.zip'
if config.args.kattis:
Expand Down
Loading
Loading