diff --git a/problemtools/ProblemPlasTeX/ProblemsetMacros.py b/problemtools/ProblemPlasTeX/ProblemsetMacros.py index b5bd4f41..ce73b29f 100644 --- a/problemtools/ProblemPlasTeX/ProblemsetMacros.py +++ b/problemtools/ProblemPlasTeX/ProblemsetMacros.py @@ -1,11 +1,14 @@ import sys import os import os.path +from pathlib import Path from plasTeX.DOM import Node from plasTeX.Base import Command from plasTeX.Base import DimenCommand from plasTeX.Logging import getLogger +from problemtools import metadata + log = getLogger() status = getLogger('status') @@ -64,35 +67,82 @@ def invoke(self, tex): class sampletableinteractive(Command): args = 'header read write file:str' - def read_sample_interaction(self, filename): - data = open(filename, 'r', encoding='utf-8').read() - messages = [] - cur_msg: list[str] = [] - cur_mode = None - for line in data.split('\n'): - if not line: - continue - if line[0] == '<': - mode = 'read' - elif line[0] == '>': - mode = 'write' + def split_multipass(self, lines: list[str]) -> list[list[str]]: + multipass_passes = [] + curr_pass: list[str] = [] + for line in lines: + if line.startswith('---'): + multipass_passes.append(curr_pass) + curr_pass = [] else: - continue - line = line[1:] - if mode != cur_mode: - if cur_mode: - messages.append({'mode': cur_mode, 'data': '\n'.join(cur_msg)}) - cur_msg = [] - cur_msg.append(line) - cur_mode = mode - if cur_mode: - messages.append({'mode': cur_mode, 'data': '\n'.join(cur_msg)}) + curr_pass.append(line) + + if curr_pass: + multipass_passes.append(curr_pass) + return multipass_passes + + def format_pass_content(self, block: list[str]) -> list[dict]: + sections = [] + + if self.attributes['is_interactive']: + cur_msg: list[str] = [] + cur_mode = None + + def format_message(cur_mode: str, cur_msg: list[str]) -> dict: + return {'mode': cur_mode, 'data': ''.join(cur_msg)} + + for line in block: + if not line: + continue + if line[0] not in ('<', '>'): + log.warning(f'Interaction had unknown prefix {line[0]}') + continue + + if line[0] == '<': + mode = 'read' + elif line[0] == '>': + mode = 'write' + + if mode != cur_mode: + if cur_mode: + sections.append(format_message(cur_mode, cur_msg)) + cur_msg = [] + cur_mode = mode + cur_msg.append(line[1:]) + if cur_mode: + sections.append(format_message(cur_mode, cur_msg)) + else: + in_data = ''.join(line[1:] for line in block if line[0] == '>') + out_data = ''.join(line[1:] for line in block if line[0] == '<') + sections.append({'mode': 'batch_sample', 'in_data': in_data, 'out_data': out_data}) + return sections + + def read_sample_interaction(self, filename: Path) -> list[dict]: + with open(filename, 'r', encoding='utf-8') as f: + data = self.split_multipass(f.readlines()) + + messages = [] + for index, block in enumerate(data): + if self.attributes['is_multi_pass']: + messages.append({'mode': 'newpass', 'data': str(index + 1)}) + messages.extend(self.format_pass_content(block)) return messages def invoke(self, tex): super().invoke(tex) dir = os.path.dirname(tex.filename) - file = os.path.join(dir, self.attributes['file']) + file = Path(dir) / self.attributes['file'] + # A slightly messy way of finding out whether we're multipass and/or interactive + problem_root = file.parent.parent.parent + problem_metadata, _ = metadata.load_metadata(problem_root) + self.attributes['is_multi_pass'] = problem_metadata.is_multi_pass() + self.attributes['is_interactive'] = problem_metadata.is_interactive() + + if not self.attributes['is_interactive']: + self.attributes['read'] = 'Sample Input' + self.attributes['write'] = 'Sample Output' + self.attributes['header'] = f'Sample Case {self.attributes["header"][2]}' + try: status.info(' ( sampletableinteractive %s ' % file) self.attributes['messages'] = self.read_sample_interaction(file) diff --git a/problemtools/statement_util.py b/problemtools/statement_util.py index 9eeb1d27..46eda650 100644 --- a/problemtools/statement_util.py +++ b/problemtools/statement_util.py @@ -282,17 +282,33 @@ def make_pass_header(curr_pass: int) -> str: def format_pass_content(content: list[str]) -> str: block = [] if is_interactive: - for interaction in content: - line_type = '' - if interaction[0] == '>': + message_type = '$' + message: list[str] = [] + + def format_message(message_type: str, message: list[str]) -> str: + if message_type == '>': line_type = 'sampleinteractionwrite' - elif interaction[0] == '<': + elif message_type == '<': line_type = 'sampleinteractionread' - else: + return f'
{"".join(message)}
' + + for interaction in content: + if len(interaction) == 0: + continue + if interaction[0] not in ('<', '>'): log.warning(f'Interaction had unknown prefix {interaction[0]}') + continue + + if interaction[0] != message_type and message_type != '$': + block.append(format_message(message_type, message)) + message = [] + + message_type = interaction[0] data = html.escape(interaction[1:]) + message.append(data) - block.append(f'
{data}
') + if message: + block.append(format_message(message_type, message)) else: input_lines = [html.escape(line[1:]) for line in content if line.startswith('<')] output_lines = [html.escape(line[1:]) for line in content if line.startswith('>')] @@ -315,8 +331,8 @@ def format_pass_content(content: list[str]) -> str: if interaction.startswith('---'): passes.append(curr_pass) curr_pass = [] - continue - curr_pass.append(interaction) + else: + curr_pass.append(interaction) if len(curr_pass): passes.append(curr_pass) diff --git a/problemtools/templates/html/Themes/default/problem.css b/problemtools/templates/html/Themes/default/problem.css index 3ce6869d..81b60664 100644 --- a/problemtools/templates/html/Themes/default/problem.css +++ b/problemtools/templates/html/Themes/default/problem.css @@ -63,8 +63,13 @@ table.sample th { width: 50%; } +table.sample pre { + margin: 0; +} + table.sample td { border: 1px solid black; + width: 50%; } div.sampleinteractionread { diff --git a/problemtools/templates/html/problemsetmacros.zpts b/problemtools/templates/html/problemsetmacros.zpts index 9eaee400..2a059d95 100644 --- a/problemtools/templates/html/problemsetmacros.zpts +++ b/problemtools/templates/html/problemsetmacros.zpts @@ -24,17 +24,47 @@ name: sampletable name: sampletableinteractive - + - +
+ + -
+ + + + + + + +
+
+ +

    
+ +
+ + + + + + +
+

+            
+

+            
+
+ + name: illustration
diff --git a/problemtools/templates/markdown_html/problem.css b/problemtools/templates/markdown_html/problem.css index 8fa307b5..0cadfe83 100644 --- a/problemtools/templates/markdown_html/problem.css +++ b/problemtools/templates/markdown_html/problem.css @@ -55,6 +55,7 @@ table:not(.sample) td { .sample td { vertical-align: top; border: 1px solid black; + width: 50%; } .sample pre { diff --git a/problemtools/verifyproblem.py b/problemtools/verifyproblem.py index 99b66b2e..0e40a5f2 100644 --- a/problemtools/verifyproblem.py +++ b/problemtools/verifyproblem.py @@ -1444,13 +1444,7 @@ def _parse_validator_results(self, val, status: int, feedbackdir, testcase: Test except Exception as e: return SubmissionResult('JE', reason=f'failed to parse validator score: {e}') else: - # If we're running multipass, we do not need to output a score after every pass - # We accept the small risk of allowing a non-multipass output validator to not output score.txt - # if it produces a file called nextpass.in - if (Path(feedbackdir) / 'nextpass.in').exists(): - score = 0 - else: - return SubmissionResult('JE', reason='problem has custom scoring but validator did not produce "score.txt"') + return SubmissionResult('JE', reason='problem has custom scoring but validator did not produce "score.txt"') return SubmissionResult('AC', score=score)