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,4 +9,5 @@
- A draft now holds a `log` of every command that changed it and a `fields` map of what those commands wrote, each field recording which layer wrote it (1 a spec, 2 a predicate, 3 a line range, 4 a literal), the source ranges it was copied from, whether it has been edited and by whom. `in2lambda draft mark ignore BLOCK` is the first such command, and `in2lambda draft replay` rebuilds the draft from the frozen markdown and the log, refusing unless what it builds is the `draft.json` that is there, byte for byte. A `draft.json` written before this has no `log` in it and is refused as one nothing here wrote; `in2lambda source add --start-over` freezes the document again.
- 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 naming both fields.
- `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.
- The Python API is unchanged: `in2lambda.main.runner` and everything under `in2lambda.api` take the same arguments and return the same objects.
1 change: 1 addition & 0 deletions docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ A fully type-annotated extensively documented Python library is available for th
quickstart
question-format
filters/index
spec
```

```{toctree}
Expand Down
103 changes: 103 additions & 0 deletions docs/source/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# 📐 Specs

A spec is a small YAML file saying which blocks of a document are questions, which are parts and
which are solutions. Running one fills in the draft beside the document, so that the wording of
every question comes out of the source rather than being retyped:

```bash
$ in2lambda source add questions.docx
$ in2lambda spec run spec.yaml
b6 is in no field.
```

The last line is the point of it: a spec run reports every block it made nothing of, so what is
left to account for is in front of you rather than quietly missing.

The fields a draft holds belong to the spec that wrote them, so a spec is run over a draft once.
Running an edited one again is refused; freeze the document afresh and run it, which is two
commands:

```bash
$ in2lambda source add --start-over questions.docx
$ in2lambda spec run spec.yaml
```

## What a spec says

```yaml
question: Header level=2 text~'^Question'
part: ListItem
solution: after Header text=Solutions, label~'^\d+(\([a-z]\))?$'
strip: ['^#+ ', '^\([a-z]\) ', '^\d+(\([a-z]\))? ']
ignore: Header level=1
layout: PartsSepSol
```

`question` and `layout` have to be there; `part`, `solution`, `strip` and `ignore` need not be.

- **`question`, `part`, `solution`** select the blocks that are each of those things.
- **`ignore`** selects the blocks that are none of them - a running header, a page of
instructions - and marks them as `in2lambda draft mark ignore` would, so they are not reported
as left out.
- **`strip`** is a list of patterns taken off the front of every value: the `(a) ` or `1. ` that
labels a part in the document, but not in the question.
- **`layout`** is one of the [filters](filters/index), and says which solution answers which
question or part. See below.

A block is whatever the first of `ignore`, `question`, `part`, `solution` to match it says it is.
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.

## Selectors

A selector is a block type, then any number of constraints:

```
[after SELECTOR,] [Type] name=value name~'regex' ...
```

The type is a pandoc element - `Header`, `Para`, `ListItem` - and may be left out to match any
block. A constraint is about one of three things:

| Attribute | What it is |
|-----------|------------|
| `level` | A heading's level: `level=2` is `##`. |
| `text` | The whole block as text, with the markup taken off. |
| `label` | The first word of that text, which is usually what numbers a question. |

`=` asks for exactly that; `~` for a regular expression anywhere in it. `after SELECTOR,` says
the block has to come after the first block that selector matches, which is how the solutions at
the end of a problem sheet are told apart from the questions at the front.

A regular expression goes in single quotes. YAML reads `\(` inside double quotes as an escape
and complains, and `'^\([a-z]\)'` is the same string without the argument.

A selector matches what **pandoc** makes of the document, while a field holds the **markdown** of
the lines it came from. That is worth knowing in two places: a part written `(a) Find the load.`
is a `ListItem`, because pandoc reads `(a)` as a list marker, and `strip` still has to take the
`(a) ` off the front of the value, because the line it was copied from still has it.

## Layouts

The layout is the one thing that differs between problem sheets that are otherwise alike: where
the solutions are, and what each of them answers.

| Layout | Which solution answers what |
|--------|-----------------------------|
| `PartsOneSol` | One solution to the whole question, however many parts it has. |
| `PartSolPartSol` | Each solution answers the part just before it, or the question if it has no parts yet. |
| `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. |

## What it writes

Each question is `q1`, `q2` and so on in the order they appear, and each of its parts `q1.a`,
`q1.b`. So a spec fills in `q1.text`, `q1.a.text`, `q1.a.solution` and, for a question answered
as a whole, `q1.solution`. Every one of them records the lines it was copied from, and that a
spec wrote it.

The spec is recorded in the draft's log with its hash, so `in2lambda draft replay` rebuilds the
same draft from the same spec - and refuses if the spec has been edited since, because then it
would be checking the draft against something else. That is why running an edited spec over a
draft it has already filled in is refused too: the draft would be left holding fields no spec on
disk wrote, and no replay could ever check it again.
Loading
Loading