student-qmd.yml
Writes the .qmd files a course can hand to students, from assessment sources that keep each answer beside its question.
A homework or exam whose answers sit in .sol divs, or whose included question fragments carry them, can never be given out as it stands. This capability writes a self-contained copy of each source, proves the copy carries nothing it should not, and renders it alone the way a student would.
It is ported from Morrison-Lab/mlg’s tools/make_student_qmd.py and the --student-qmd half of its tools/check_student_copy.py, with the extras Morrison-Lab/epi204’s generator needed.
What it does
For each source, three steps, each able to fail the job:
Write
<output-dir>/<source file name>:- every
{{< include >}}is inlined, recursively, resolved the way Quarto resolves it: a path starting with/from the project root, and any other path from the directory of the top-level document, even inside an included file; - an included file’s own front matter is dropped. Quarto merges it into the document instead, so a key other than
filtersgets a warning; - every answer-key-only div is removed with everything inside it: a div with one of the
hidden-classes, and acontent-visibleorcontent-hiddendiv that Quarto shows only under one of theanswer-profilesor hides under thestudent-profile; - HTML comments outside code, and whole-line
#comments in the front matter, are removed, since they carry instructor notes; - with
drop-render-chunks, fenced code blocks that runquarto renderare removed too, with any div they leave empty; - with
prune-macros, LaTeX macro definitions (\def,\let,\newcommand,\renewcommand,\providecommand) that the source never uses, directly or through another macro, are removed. A macro used only inside an answer counts as used, so a student writing an answer has the answer key’s notation; its definition reaches the student file as written, so never put an answer in a macro’s body. Every definition of a used name is kept, since a later\defreplaces an earlier one.
It fails rather than guess on a missing include, unbalanced divs, an unclosed code fence or comment, or two sources with the same file name.
Write answers with
:::div syntax, not raw HTML. Pandoc reads<div class="sol">as the same div as::: {.sol}, so the assign filter hides it, but the writer removes only:::divs. So a source with a raw<div>carrying one of thehidden-classes, or acontent-visibleorcontent-hiddenone with a profile attribute, fails with its file and line, whether or notcheckis on. Code blocks, code spans and HTML comments are not scanned.- every
Check each written file against its source with Quarto’s own Pandoc (
quarto pandoc), so the check shares no parser for divs and comments with the writer. A file fails when it is missing, still has an include, has an answer-key-only div, an HTML comment or a front-matter comment, or differs in any other way from its source’s AST with those parts taken out. A source div classed.solution,.answeror the like fails too: the assign filter passes such a div through untouched, so an answer written in one would reach every student. So does a source with an answer in a raw HTML<div>, as in the writer.Render each written file alone, in an empty directory outside the repository, so anything it still needs from the repository fails here rather than on a student’s machine. A file with executable cells needs their engine (R, Python, …) set up before this runs, or
render: false.
The defaults match the coatless-quarto/assign filter: it hides .sol and .rubric divs, and shows them under the solution, solutions and rubric profiles.
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
sources |
string | (required) | Source .qmd paths or globs (** allowed), separated by spaces or newlines. |
working-directory |
string | '.' |
Directory to run in; sources and output-dir are relative to it. |
output-dir |
string | '_student-qmd' |
Directory to write the student files to, one per source, named as the source. |
hidden-classes |
string | 'sol rubric' |
Div classes only the answer key shows. |
answer-profiles |
string | 'solution solutions rubric' |
Profiles whose when-profile content is answer-key only. |
student-profile |
string | 'assign' |
The profile the student copy renders under. |
drop-render-chunks |
boolean | false |
Also remove fenced code blocks that run quarto render. |
prune-macros |
boolean | false |
Also remove LaTeX macro definitions the document never uses. A definition inside an answer-key-only div is refused, and so is one whose braces do not close before a blank line or a fence. The check leaves definitions out of its comparison, so a needed one that went missing still fails it, through the math it changes. |
require-answers |
boolean | true |
Fail a source with no answer-key-only div; when false, warn instead. A run in which no source has one still fails. |
check |
boolean | true |
Check each student file against its source. |
render |
boolean | true |
Render each student file alone in an empty directory. |
render-to |
string | 'html' |
Output format for that render. |
index-title |
string | '' |
When set, also write an index.html in output-dir listing the files. |
index-back-href |
string | '' |
A back link for that index page. |
quarto-version |
string | 'release' |
Quarto version to install. |
python-version |
string | '3.x' |
Python version to set up. |
checkout-submodules |
boolean | false |
Initialize the caller’s git submodules first (for includes that live in one). |
artifact-name |
string | '' |
When set, upload the student files as a workflow artifact with this name. |
The composite action (Morrison-Lab/gha/student-qmd@v3) takes the same inputs except the last two, plus install-quarto (default 'true'), and outputs output-dir (an absolute path) and count.
Permissions
Needs only contents: read, the default, so the caller needs no permissions: block.
Example
name: Student .qmd files
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
student-qmd:
uses: Morrison-Lab/gha/.github/workflows/student-qmd.yml@v3
with:
sources: |
Homework/**/hw*.qmd
Exams/**/exam*.qmd
# drop-render-chunks: true # also drop `quarto render` code blocks
# prune-macros: true # drop unused LaTeX macro definitions
# require-answers: false # warn, not fail, on a source with no answers
# render: false # skip rendering (e.g. cells need R)
# artifact-name: student-qmd # upload the filesTo publish the files with a site instead, call the composite action in the job that builds the site, with output-dir inside the rendered output.
What stays with the caller
Checks about a whole project’s render, such as how many pages a render produced or whether every question fragment names the assign filter in its front matter, are specific to each course repository and are not part of this capability.