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'
'
+
+ 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'')
+ 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)