solrub 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- solrub-0.1.0/LICENSE +21 -0
- solrub-0.1.0/PKG-INFO +75 -0
- solrub-0.1.0/README.md +56 -0
- solrub-0.1.0/pyproject.toml +30 -0
- solrub-0.1.0/setup.cfg +4 -0
- solrub-0.1.0/solrub/__init__.py +193 -0
- solrub-0.1.0/solrub/__main__.py +5 -0
- solrub-0.1.0/solrub.egg-info/PKG-INFO +75 -0
- solrub-0.1.0/solrub.egg-info/SOURCES.txt +11 -0
- solrub-0.1.0/solrub.egg-info/dependency_links.txt +1 -0
- solrub-0.1.0/solrub.egg-info/entry_points.txt +2 -0
- solrub-0.1.0/solrub.egg-info/requires.txt +3 -0
- solrub-0.1.0/solrub.egg-info/top_level.txt +1 -0
solrub-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 higger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
solrub-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: solrub
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Build the student, solution and rubric PDFs of a LaTeX assignment.
|
|
5
|
+
Author-email: higger <matt.higger@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: latex,pdflatex,teaching,exam,rubric,grading
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Education
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Education
|
|
12
|
+
Classifier: Topic :: Text Processing :: Markup :: LaTeX
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Provides-Extra: pts
|
|
17
|
+
Requires-Dist: sum-pts>=0.0.4; extra == "pts"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# solrub
|
|
21
|
+
|
|
22
|
+
Build the student, solution and rubric PDFs of a LaTeX assignment.
|
|
23
|
+
|
|
24
|
+
One `.tex` source carries all three copies. `\sol` marks what only the
|
|
25
|
+
answer key shows and `\rub` what only the grading rubric shows; each copy is
|
|
26
|
+
a `pdflatex` run defining a different subset of them.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install solrub
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Use
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
solrub quiz1a # quiz1a.pdf, quiz1a_sol.pdf, quiz1a_rub.pdf
|
|
36
|
+
solrub quiz1* # every version at once
|
|
37
|
+
solrub quiz1/ # every document in a folder
|
|
38
|
+
solrub --no-sol --no-rub blank
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
All three copies are the default; `--no-sol` and `--no-rub` opt out. Add
|
|
42
|
+
`-p` to sum the points (needs `pip install "solrub[pts]"`).
|
|
43
|
+
|
|
44
|
+
Arguments are files, globs or directories, with the `.tex` suffix optional.
|
|
45
|
+
Globs are expanded by the tool rather than left to the shell, so `quiz1*`
|
|
46
|
+
works the same on Windows. Two filters keep a wide pattern honest: sibling
|
|
47
|
+
builds (`quiz1a.pdf`, `quiz1a_sol.pdf`) collapse onto the `.tex` they came
|
|
48
|
+
from, and a fragment with no `\documentclass` is skipped. A failed document
|
|
49
|
+
does not stop the rest of the batch; the failures are listed at the end.
|
|
50
|
+
|
|
51
|
+
## In the document
|
|
52
|
+
|
|
53
|
+
Three macros divide the source. `\answer` and `\rubric` are hidden by
|
|
54
|
+
default and revealed by their build; `\exam` is the reverse, student-only
|
|
55
|
+
content such as the blank space left for the work, dropped from both keys.
|
|
56
|
+
|
|
57
|
+
```latex
|
|
58
|
+
\newcommand{\answer}[1]{}
|
|
59
|
+
\newcommand{\rubric}[1]{}
|
|
60
|
+
\newcommand{\exam}[1]{#1}
|
|
61
|
+
|
|
62
|
+
\ifdefined\sol
|
|
63
|
+
\renewcommand{\answer}[1]{#1}
|
|
64
|
+
\renewcommand{\exam}[1]{}
|
|
65
|
+
\fi
|
|
66
|
+
|
|
67
|
+
\ifdefined\rub
|
|
68
|
+
\renewcommand{\rubric}[1]{#1}
|
|
69
|
+
\renewcommand{\exam}[1]{}
|
|
70
|
+
\fi
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Requires
|
|
74
|
+
|
|
75
|
+
`pdflatex` on the PATH, and Python 3.9 or newer.
|
solrub-0.1.0/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# solrub
|
|
2
|
+
|
|
3
|
+
Build the student, solution and rubric PDFs of a LaTeX assignment.
|
|
4
|
+
|
|
5
|
+
One `.tex` source carries all three copies. `\sol` marks what only the
|
|
6
|
+
answer key shows and `\rub` what only the grading rubric shows; each copy is
|
|
7
|
+
a `pdflatex` run defining a different subset of them.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install solrub
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Use
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
solrub quiz1a # quiz1a.pdf, quiz1a_sol.pdf, quiz1a_rub.pdf
|
|
17
|
+
solrub quiz1* # every version at once
|
|
18
|
+
solrub quiz1/ # every document in a folder
|
|
19
|
+
solrub --no-sol --no-rub blank
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
All three copies are the default; `--no-sol` and `--no-rub` opt out. Add
|
|
23
|
+
`-p` to sum the points (needs `pip install "solrub[pts]"`).
|
|
24
|
+
|
|
25
|
+
Arguments are files, globs or directories, with the `.tex` suffix optional.
|
|
26
|
+
Globs are expanded by the tool rather than left to the shell, so `quiz1*`
|
|
27
|
+
works the same on Windows. Two filters keep a wide pattern honest: sibling
|
|
28
|
+
builds (`quiz1a.pdf`, `quiz1a_sol.pdf`) collapse onto the `.tex` they came
|
|
29
|
+
from, and a fragment with no `\documentclass` is skipped. A failed document
|
|
30
|
+
does not stop the rest of the batch; the failures are listed at the end.
|
|
31
|
+
|
|
32
|
+
## In the document
|
|
33
|
+
|
|
34
|
+
Three macros divide the source. `\answer` and `\rubric` are hidden by
|
|
35
|
+
default and revealed by their build; `\exam` is the reverse, student-only
|
|
36
|
+
content such as the blank space left for the work, dropped from both keys.
|
|
37
|
+
|
|
38
|
+
```latex
|
|
39
|
+
\newcommand{\answer}[1]{}
|
|
40
|
+
\newcommand{\rubric}[1]{}
|
|
41
|
+
\newcommand{\exam}[1]{#1}
|
|
42
|
+
|
|
43
|
+
\ifdefined\sol
|
|
44
|
+
\renewcommand{\answer}[1]{#1}
|
|
45
|
+
\renewcommand{\exam}[1]{}
|
|
46
|
+
\fi
|
|
47
|
+
|
|
48
|
+
\ifdefined\rub
|
|
49
|
+
\renewcommand{\rubric}[1]{#1}
|
|
50
|
+
\renewcommand{\exam}[1]{}
|
|
51
|
+
\fi
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Requires
|
|
55
|
+
|
|
56
|
+
`pdflatex` on the PATH, and Python 3.9 or newer.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "solrub"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Build the student, solution and rubric PDFs of a LaTeX assignment."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "higger", email = "matt.higger@gmail.com" }]
|
|
10
|
+
keywords = ["latex", "pdflatex", "teaching", "exam", "rubric", "grading"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Education",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Topic :: Education",
|
|
16
|
+
"Topic :: Text Processing :: Markup :: LaTeX",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
pts = ["sum-pts>=0.0.4"]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
solrub = "solrub:main"
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["setuptools>=77"]
|
|
27
|
+
build-backend = "setuptools.build_meta"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools]
|
|
30
|
+
packages = ["solrub"]
|
solrub-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Build the student, solution and rubric PDFs of a LaTeX assignment.
|
|
3
|
+
|
|
4
|
+
One .tex source carries all three copies: the \\sol and \\rub macros mark
|
|
5
|
+
the parts only the answer key and the grading rubric should show, and each
|
|
6
|
+
copy is a pdflatex run that defines a different subset of them. All three
|
|
7
|
+
are built unless --no-sol or --no-rub says otherwise.
|
|
8
|
+
|
|
9
|
+
Arguments are files, globs or directories, resolved here rather than left
|
|
10
|
+
to the shell, so quiz1* reaches quiz1a and quiz1b on every platform. Two
|
|
11
|
+
filters keep a wide pattern honest: sibling builds (quiz1a.pdf,
|
|
12
|
+
quiz1a_sol.pdf) collapse onto the .tex they came from, and a fragment with
|
|
13
|
+
no \\documentclass, e.g. reminder.tex, is skipped.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import glob
|
|
18
|
+
import importlib.util
|
|
19
|
+
import pathlib
|
|
20
|
+
import re
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
from collections.abc import Iterator
|
|
24
|
+
|
|
25
|
+
__version__ = '0.1.0'
|
|
26
|
+
|
|
27
|
+
DOCUMENTCLASS = re.compile(r'^\s*\\documentclass', re.MULTILINE)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_pdf(path: pathlib.Path, jobname: str = None, sol: bool = False,
|
|
31
|
+
rub: bool = False, clean: bool = True) -> None:
|
|
32
|
+
"""Run pdflatex on one document.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
path: the .tex file, suffix optional
|
|
36
|
+
jobname: output basename, defaults to the input's
|
|
37
|
+
sol: define \\sol, revealing the answers
|
|
38
|
+
rub: define \\rub, revealing the rubric (needs sol too)
|
|
39
|
+
clean: delete the aux, out and log files afterwards. A failed
|
|
40
|
+
run keeps them either way, since the log holds the error.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
subprocess.CalledProcessError: pdflatex rejected the document
|
|
44
|
+
"""
|
|
45
|
+
path = pathlib.Path(path).with_suffix('')
|
|
46
|
+
|
|
47
|
+
# nonstopmode so a broken document reports and exits rather than
|
|
48
|
+
# stopping for input, which would strand a whole batch of builds.
|
|
49
|
+
command = ['pdflatex', '-interaction=nonstopmode']
|
|
50
|
+
if jobname:
|
|
51
|
+
command += ['--jobname', jobname]
|
|
52
|
+
if sol and rub:
|
|
53
|
+
command += ['\\def\\sol{1} \\def\\rub{1} \\input{' + path.name + '}']
|
|
54
|
+
elif sol:
|
|
55
|
+
command += ['\\def\\sol{1} \\input{' + path.name + '}']
|
|
56
|
+
else:
|
|
57
|
+
command += [path.name]
|
|
58
|
+
|
|
59
|
+
subprocess.run(command, check=True, cwd=path.parent)
|
|
60
|
+
|
|
61
|
+
if clean:
|
|
62
|
+
clean_up(path)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def sum_points(path: pathlib.Path) -> None:
|
|
66
|
+
"""Print the point total of a document.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
SystemExit: the optional sum-pts dependency is not installed
|
|
70
|
+
"""
|
|
71
|
+
if importlib.util.find_spec('sum_pts') is None:
|
|
72
|
+
sys.exit('summing points needs sum-pts: pip install "solrub[pts]"')
|
|
73
|
+
|
|
74
|
+
command = [
|
|
75
|
+
sys.executable, '-m', 'sum_pts', f"{path}",
|
|
76
|
+
'--left', '\\[', '--right', '\\]',
|
|
77
|
+
'--points', 'pts?', '--prefix', ' *\\\\prob',
|
|
78
|
+
'-r', '\\(\\d+.?\\d* each\\)',
|
|
79
|
+
'-r', '\\((\\d+.?\\d*,? ?)+\\)',
|
|
80
|
+
'-r', '\\{', '-r', '\\}', '-r', ':'
|
|
81
|
+
]
|
|
82
|
+
subprocess.run(command, check=True)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def clean_up(path: pathlib.Path) -> None:
|
|
86
|
+
"""Delete the aux, out and log files of a document's three builds."""
|
|
87
|
+
for stem in [path.stem, f'{path.stem}_sol', f'{path.stem}_rub']:
|
|
88
|
+
for ext in ['aux', 'out', 'log']:
|
|
89
|
+
(path.parent / f'{stem}.{ext}').unlink(missing_ok=True)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def is_document(path: pathlib.Path) -> bool:
|
|
93
|
+
"""Report whether a .tex file is a whole document, not a fragment."""
|
|
94
|
+
return bool(DOCUMENTCLASS.search(path.read_text(errors='ignore')))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def expand(pattern: str) -> Iterator[pathlib.Path]:
|
|
98
|
+
"""Yield the candidate files one command-line argument names.
|
|
99
|
+
|
|
100
|
+
A pattern the shell already expanded arrives as a plain path and passes
|
|
101
|
+
through; one it left alone is globbed here. A directory stands for the
|
|
102
|
+
.tex files directly inside it.
|
|
103
|
+
"""
|
|
104
|
+
for match in sorted(glob.glob(pattern)) or [pattern]:
|
|
105
|
+
path = pathlib.Path(match)
|
|
106
|
+
if path.is_dir():
|
|
107
|
+
yield from sorted(path.glob('*.tex'))
|
|
108
|
+
else:
|
|
109
|
+
yield path
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def resolve(patterns: list[str]) -> list[pathlib.Path]:
|
|
113
|
+
"""Map command-line arguments onto the .tex files to build.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
patterns: paths, globs or directories, .tex suffix optional
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
paths: the .tex files, deduplicated, in the order named. A
|
|
120
|
+
pattern matching several files keeps only the whole documents
|
|
121
|
+
among them; a file named on its own is built whatever it holds.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
FileNotFoundError: a pattern matches nothing on disk
|
|
125
|
+
"""
|
|
126
|
+
paths = []
|
|
127
|
+
for pattern in patterns:
|
|
128
|
+
candidates = list(expand(pattern))
|
|
129
|
+
found = []
|
|
130
|
+
for candidate in candidates:
|
|
131
|
+
tex = candidate.with_suffix('.tex')
|
|
132
|
+
if tex.is_file() and tex not in found:
|
|
133
|
+
found.append(tex)
|
|
134
|
+
if len(found) > 1:
|
|
135
|
+
found = [tex for tex in found if is_document(tex)]
|
|
136
|
+
if not found:
|
|
137
|
+
# An existing file with no source of its own is a build the
|
|
138
|
+
# shell swept in beside it (quiz1a_sol.pdf), so drop it
|
|
139
|
+
# quietly; a name matching nothing at all is a typo.
|
|
140
|
+
if not any(candidate.exists() for candidate in candidates):
|
|
141
|
+
raise FileNotFoundError(
|
|
142
|
+
f'no document to build matches: {pattern}')
|
|
143
|
+
continue
|
|
144
|
+
paths += [tex for tex in found if tex not in paths]
|
|
145
|
+
return paths
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main() -> None:
|
|
149
|
+
"""Run the command line interface."""
|
|
150
|
+
parser = argparse.ArgumentParser(
|
|
151
|
+
prog='solrub',
|
|
152
|
+
description='Build the student, solution and rubric PDFs of a '
|
|
153
|
+
'LaTeX assignment.')
|
|
154
|
+
parser.add_argument('-s', '--sol', action=argparse.BooleanOptionalAction,
|
|
155
|
+
default=True, help='build solution copy')
|
|
156
|
+
parser.add_argument('-r', '--rub', action=argparse.BooleanOptionalAction,
|
|
157
|
+
default=True, help='build rubric copy')
|
|
158
|
+
parser.add_argument('-p', '--pts', action='store_true', help='sum points')
|
|
159
|
+
parser.add_argument('-V', '--version', action='version',
|
|
160
|
+
version=f'%(prog)s {__version__}')
|
|
161
|
+
parser.add_argument('path', nargs='+',
|
|
162
|
+
help='.tex files, globs or directories to build')
|
|
163
|
+
|
|
164
|
+
args = parser.parse_args()
|
|
165
|
+
|
|
166
|
+
failed = []
|
|
167
|
+
for path in resolve(args.path):
|
|
168
|
+
print(f'==> {path}', flush=True)
|
|
169
|
+
|
|
170
|
+
# One unbuildable document must not strand the rest of the batch,
|
|
171
|
+
# so collect the failures and report them together at the end.
|
|
172
|
+
try:
|
|
173
|
+
build_pdf(path)
|
|
174
|
+
|
|
175
|
+
if args.sol:
|
|
176
|
+
build_pdf(path, jobname=f'{path.stem}_sol', sol=True)
|
|
177
|
+
|
|
178
|
+
if args.rub:
|
|
179
|
+
build_pdf(path, jobname=f'{path.stem}_rub', sol=True,
|
|
180
|
+
rub=True)
|
|
181
|
+
|
|
182
|
+
if args.pts:
|
|
183
|
+
sum_points(path)
|
|
184
|
+
except subprocess.CalledProcessError:
|
|
185
|
+
failed.append(path)
|
|
186
|
+
|
|
187
|
+
if failed:
|
|
188
|
+
names = ', '.join(str(path) for path in failed)
|
|
189
|
+
sys.exit(f'failed: {names}\nthe .log beside each holds the error')
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
main()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: solrub
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Build the student, solution and rubric PDFs of a LaTeX assignment.
|
|
5
|
+
Author-email: higger <matt.higger@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: latex,pdflatex,teaching,exam,rubric,grading
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Education
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Education
|
|
12
|
+
Classifier: Topic :: Text Processing :: Markup :: LaTeX
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Provides-Extra: pts
|
|
17
|
+
Requires-Dist: sum-pts>=0.0.4; extra == "pts"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# solrub
|
|
21
|
+
|
|
22
|
+
Build the student, solution and rubric PDFs of a LaTeX assignment.
|
|
23
|
+
|
|
24
|
+
One `.tex` source carries all three copies. `\sol` marks what only the
|
|
25
|
+
answer key shows and `\rub` what only the grading rubric shows; each copy is
|
|
26
|
+
a `pdflatex` run defining a different subset of them.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install solrub
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Use
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
solrub quiz1a # quiz1a.pdf, quiz1a_sol.pdf, quiz1a_rub.pdf
|
|
36
|
+
solrub quiz1* # every version at once
|
|
37
|
+
solrub quiz1/ # every document in a folder
|
|
38
|
+
solrub --no-sol --no-rub blank
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
All three copies are the default; `--no-sol` and `--no-rub` opt out. Add
|
|
42
|
+
`-p` to sum the points (needs `pip install "solrub[pts]"`).
|
|
43
|
+
|
|
44
|
+
Arguments are files, globs or directories, with the `.tex` suffix optional.
|
|
45
|
+
Globs are expanded by the tool rather than left to the shell, so `quiz1*`
|
|
46
|
+
works the same on Windows. Two filters keep a wide pattern honest: sibling
|
|
47
|
+
builds (`quiz1a.pdf`, `quiz1a_sol.pdf`) collapse onto the `.tex` they came
|
|
48
|
+
from, and a fragment with no `\documentclass` is skipped. A failed document
|
|
49
|
+
does not stop the rest of the batch; the failures are listed at the end.
|
|
50
|
+
|
|
51
|
+
## In the document
|
|
52
|
+
|
|
53
|
+
Three macros divide the source. `\answer` and `\rubric` are hidden by
|
|
54
|
+
default and revealed by their build; `\exam` is the reverse, student-only
|
|
55
|
+
content such as the blank space left for the work, dropped from both keys.
|
|
56
|
+
|
|
57
|
+
```latex
|
|
58
|
+
\newcommand{\answer}[1]{}
|
|
59
|
+
\newcommand{\rubric}[1]{}
|
|
60
|
+
\newcommand{\exam}[1]{#1}
|
|
61
|
+
|
|
62
|
+
\ifdefined\sol
|
|
63
|
+
\renewcommand{\answer}[1]{#1}
|
|
64
|
+
\renewcommand{\exam}[1]{}
|
|
65
|
+
\fi
|
|
66
|
+
|
|
67
|
+
\ifdefined\rub
|
|
68
|
+
\renewcommand{\rubric}[1]{#1}
|
|
69
|
+
\renewcommand{\exam}[1]{}
|
|
70
|
+
\fi
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Requires
|
|
74
|
+
|
|
75
|
+
`pdflatex` on the PATH, and Python 3.9 or newer.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
solrub/__init__.py
|
|
5
|
+
solrub/__main__.py
|
|
6
|
+
solrub.egg-info/PKG-INFO
|
|
7
|
+
solrub.egg-info/SOURCES.txt
|
|
8
|
+
solrub.egg-info/dependency_links.txt
|
|
9
|
+
solrub.egg-info/entry_points.txt
|
|
10
|
+
solrub.egg-info/requires.txt
|
|
11
|
+
solrub.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
solrub
|