Skip to content
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- A draft is filled in by `in2lambda draft question add`, `in2lambda draft part add QUESTION` and `in2lambda draft question solution QUESTION`. Each takes `--text` to copy the wording out of the frozen source, as a block id such as `b3` or as lines such as `s10:14`, or `--literal TEXT` where the source does not say it in a form the field can take, which records the field as edited and written by layer 4 rather than 3. Question and part numbers are worked out from the fields already written rather than given, so a replay arrives at the same ids. `in2lambda draft split block BLOCK AT` cuts a block the parser made one of two things into `b3a` and `b3b`, so that each half can be quoted on its own. A command writing a field that is already written, or quoting lines another field was taken from, is refused: the first naming the field, the second naming both.
- `in2lambda draft field replace FIELD OLD NEW` changes the wording inside a field that is already written, for the faults only an edit can fix - a brace the OCR dropped out of some maths, which no range of the source says correctly. OLD has to occur in the field exactly once, or the command is refused saying how many times it occurs; `--regex` reads it as a regular expression and NEW as what to replace it with. The field is left quoting the lines it was taken from, at the layer that wrote it, but recorded as edited and by whoever replaced the wording, so the change can be shown against the source.
- `in2lambda spec run SPEC` runs a YAML file of selectors over the frozen source: it says which blocks are questions, parts and solutions, which to ignore, what to strip off the front of each one, and which of the four filters lays the solutions out. It fills in the draft's fields with the markdown of the lines each was taken from, records the spec's name and hash in the log so a replay runs the same file, and reports every block it made nothing of. Running an edited spec over a draft it has already filled in is refused, as freezing a document that has changed is: `in2lambda source add --start-over` begins the draft again. Reading a spec needs pyyaml, which the `convert` extra now installs alongside panflute. See [the spec page](https://lambda-feedback.github.io/in2lambda/spec.html) for the selectors and layouts.
- A spec's `question`, `part`, `solution` and `ignore` now each take a list of selectors as well as one selector, written as a YAML list under the key, and a block has that role where any one of them matches it. A run that would write a field twice - a document of nothing but solutions has more solutions than the layout has questions to answer - now leaves the second block in no field and reports it, naming the field and the block that holds it, instead of refusing the whole run. In the Python API, `in2lambda.spec.Spec` holds a list of selectors per role rather than one or None, and `in2lambda.spec.fields` returns a third list, of `in2lambda.spec.Doubled`, beside its fields and its ignored blocks.
- A field quoted out of a list item is now dedented as commonmark reads the item: the marker comes off the first line and as much of the same width off every line under it. So a question written `1. ` no longer carries its number, a continuation line no longer arrives indented far enough to be rendered as a code block, and a spec's `strip` is left with what pandoc does not read as a marker. Values written by `in2lambda spec run`, `in2lambda draft question add`, `in2lambda draft part add` and `in2lambda draft question solution` change accordingly; the ranges behind them still name the same source lines.
- `in2lambda validate` checks a draft over as a whole and writes what it finds into it as a `report`: source blocks in no field and not marked ignore, two fields taken from the same lines, gaps in the numbering of the questions or their parts, and fields holding nothing, each at level `error`; a part, or a question written without parts, that nothing in the draft answers is reported at level `warning` instead. Each finding names the level, the field and the lines it is about, so it can be acted on without reading the draft. Finding something is not a failure and the command still exits 0; the report is replaced by the next run of the checks and dropped by the next command that changes the draft, since it describes the draft as it stood. It also checks over the set the draft describes, as a converted document is checked at export - maths delimiters, what KaTeX will not render, images the export would not carry, and the compile Lambda Feedback's PDF generator does where pandoc and xelatex are installed, with a warning saying what to install where they are not - and reports each of those against the draft field the text is written in, at level `error`, so that `in2lambda build` refuses them as it refuses anything else at that level.
- `in2lambda build` writes a draft out as a Lambda Feedback set: one question per `qN.text` field, holding the parts written for it and the worked solutions, with the images those fields refer to under `media/`, as `in2lambda convert` writes a set - a field naming an image that is not beside the draft is refused saying which file is missing, since the checks read the draft and not the folder it is in, and a question's own solution written beside a solution for every part it has becomes a part of its own holding just that solution, as `convert` pairs them up. It is refused unless `in2lambda validate` has been run since the draft last changed - every command that changes one drops its report - and found nothing at level `error`, and the refusal prints those findings so they can be acted on without opening the draft. A finding at level `warning` - a part or question nothing in the draft answers - does not stop it: half the sheets there are keep their solutions in another file or have none at all, so the warning is printed and the set written all the same, rather than a solution having to be invented to quiet it. `in2lambda render` writes each question as a PDF instead, compiled as Lambda Feedback's own PDF generator compiles it, which needs pandoc and xelatex; it is gated on nothing, since looking at a draft is how what the checks found gets fixed. Both take `-o/--out`, as `convert` does.
Expand Down
28 changes: 24 additions & 4 deletions docs/source/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ A block is whatever the first of `ignore`, `question`, `part`, `solution` to mat
That order is fixed, whatever order the keys are written in, so a spec whose selectors overlap
has to tell them apart by what they match rather than by where they are in the file.

`question`, `part`, `solution` and `ignore` each take one selector, or a list of them written
under the key. A block has that role where any one of the selectors in the list matches it, so
one spec selects the questions of a document that writes them two different ways:

```yaml
ignore:
- Header level=1
- Para text~'^Marks'
```

## Selectors

A selector is a block type, then any number of constraints:
Expand Down Expand Up @@ -134,6 +144,10 @@ the solutions are, and what each of them answers.
| `PartPartSolSol` | The parts come together and their solutions come after, in the same order. |
| `PartsSepSol` | Every solution is at the end: the first answers the first part of the first question, and so on. |

A sheet holding more solutions than the layout has questions and parts to answer sends two of
them to the one field. The second is left in no field and reported, naming the field and the
block that holds it. The section below says the same of a document of solutions.

## A separate solutions document

Many sheets come as two files: the questions, and the solutions written separately from them.
Expand Down Expand Up @@ -166,10 +180,16 @@ changes is what the selectors mean in a document of solutions, which is what `in
the sheet itself is laid out as.

A solution past the last slot is reported as being in no field, like any other block the spec
made nothing of; one landing on a question the solutions before it have answered is refused,
saying that the field - `q2.solution`, say - is already written and that no command here writes
a field twice. `in2lambda draft field replace` changes the wording of one, and `in2lambda source
add --start-over` begins the draft again.
made nothing of. So is one landing on a question the solutions before it have answered, with a
second line naming the field it would have gone in and the block that holds it:

```
b7 (lines 14-15) is in no field and not marked ignore.
b7 (lines 14-15) would be q2.solution, which b5 (lines 10-11) already holds.
```

The run writes every other field, so a spec that sends two solutions to one field still fills
the draft in and names the block to look at.

## What it writes

Expand Down
16 changes: 12 additions & 4 deletions in2lambda/draft/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from typing import Any

import in2lambda.spec
from in2lambda.draft.report import _order, checks, overlapping, uncovered
from in2lambda.draft.report import _order, _where, checks, overlapping, uncovered
from in2lambda.source import (
SourceError,
_digest,
Expand Down Expand Up @@ -889,7 +889,7 @@ def _spec_run(
(_elements(markdown, number), markdown)
for number, markdown in enumerate(sources, start=1)
]
fields, ignored = in2lambda.spec.fields(spec, documents, functions)
fields, ignored, doubled = in2lambda.spec.fields(spec, documents, functions)
for found in fields:
record(
draft,
Expand Down Expand Up @@ -920,6 +920,14 @@ def _spec_run(
# A spec writes a draft's worth of fields, so what it hands back is the other way
# round: what it made nothing of, which is what is left for anyone to act on. Said
# in the words `in2lambda validate` says it in, since it is the same check.
if left_out := uncovered(draft):
return "\n".join(finding["message"] for finding in left_out)
reported = [finding["message"] for finding in uncovered(draft)]
# A doubled block is one of those, and this names the field it would have gone in
# and the block that holds it, which the coverage report cannot say.
reported += [
f"{block}{_where(ranges)} would be {key}, which {by_block}"
f"{_where(by_ranges)} already holds."
for block, ranges, key, by_block, by_ranges in doubled
]
if reported:
return "\n".join(reported)
return "Every block is in a field or ignored."
Loading
Loading