From baead62ca82058512a8c9ff578837a4caf5edc98 Mon Sep 17 00:00:00 2001 From: "Peter B. Johnson" Date: Mon, 21 Sep 2026 10:00:55 +0100 Subject: [PATCH 1/2] implement: Freeze a docx without the bracketed spans the PDF generator cannot compile (t58) --- CHANGELOG.md | 1 + in2lambda/source/__init__.py | 88 +++++++++++++++++- tests/fixtures/sources/README.md | 9 ++ .../sources/bracketed_spans/expected.json | 20 ++++ .../sources/bracketed_spans/make_source.py | 33 +++++++ .../sources/bracketed_spans/source.docx | Bin 0 -> 36744 bytes tests/test_source.py | 36 ++++++- 7 files changed, 180 insertions(+), 7 deletions(-) create mode 100644 tests/fixtures/sources/bracketed_spans/expected.json create mode 100644 tests/fixtures/sources/bracketed_spans/make_source.py create mode 100644 tests/fixtures/sources/bracketed_spans/source.docx diff --git a/CHANGELOG.md b/CHANGELOG.md index c379426..e47c09a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - beartype is now `^0.22`. At 0.20.0 and below, the beartype import hook leaves `cli` a plain function instead of a group, so the command line either fails to import or runs `convert` whatever the arguments are. 0.20.1 is the first version that works. - `in2lambda source add FILE` freezes a document: `in2lambda source add` converts .docx and .tex to markdown beside the file, and writes `FILE.draft.json` beside the file, holding the markdown's hash and every block in the markdown with the lines it spans, so that another tool can quote the source by line range. The draft is named after the source, so a folder holding a term's sheets holds one draft per sheet. `in2lambda source show` prints that markdown numbered with the block ids. Freezing a file that has changed since is refused unless `--start-over` discards the draft. Showing a file that has changed is refused as well, because its block ids would name lines they were not taken from. `in2lambda source add` needs pandoc and the `convert` extra, as `in2lambda convert` does. `in2lambda source show` reads the markdown the draft was frozen from, and needs neither. - `in2lambda source add` writes the markdown of a converted .docx or .tex unwrapped, so that a paragraph is one line however long the paragraph is, and an inline `$ ... $` is never broken across two lines. `in2lambda source add` also moves every `$$ ... $$` that pandoc wrote on one line onto lines of its own, in a paragraph and in a list item, indented to the item's width where the maths is written in a list item. A `$$` that opens or closes in a table cell, in a block quote or in a code block is left unchanged, as is the maths that an unpaired `$$` elsewhere in the document, such as one in inline code, pairs with. `in2lambda validate` reports the maths left unchanged. pandoc's writer produces both forms, and Lambda Feedback renders neither, so `in2lambda validate` reported them against every converted sheet. The markdown beside a document frozen before this release differs, and so do its line ranges. `in2lambda source add --start-over` freezes the document again. +- `in2lambda source add` now drops the underline, highlight and small capitals a converted .docx holds, keeping the text each marked, so a field reads `**Question 2:**` in place of `**[Question 2:]{.underline}**`. Pandoc's docx reader writes those three formats as the bracketed spans `[text]{.underline}`, `[text]{.mark}` and `[text]{.smallcaps}`, which the PDF generator's pandoc turns into LaTeX commands its template does not define, so a set built from such a field failed to compile. Lambda Feedback's markdown renders none of the three. A bracketed span in a code block or in inline code is left unchanged. The markdown beside a .docx frozen before this release differs, and so do its line ranges where a span stood; `in2lambda source add --start-over` freezes the document again. - A draft now holds a `log` of every command that changed it, and a `fields` map of the fields those commands wrote. Each field records the layer that 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. `in2lambda draft replay` rebuilds the draft from the frozen markdown and the log, and refuses unless the draft it builds matches the draft on disk byte for byte. A draft written before this release holds no `log` and is refused as a draft in2lambda did not write; `in2lambda source add --start-over` freezes the document again. - `in2lambda draft question add`, `in2lambda draft part add QUESTION` and `in2lambda draft question solution QUESTION` fill a draft in. 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`. Each takes `--literal TEXT` for wording the source does not hold in a form the field can take, which records the field as edited and written by layer 4 in place of layer 3. in2lambda works the question and part numbers out from the fields already written, 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 is refused, naming the field. A command quoting lines another field was taken from is refused, naming both fields. - `in2lambda draft part solution PART --text RANGE` gives one part of a question its worked solution, for a sheet that writes a solution under each part rather than one solution answering the whole question. PART is written `q1.p2`. The command writes `q1.p2.solution` from the lines named, or from `--literal TEXT`, as `in2lambda draft question solution` writes a question's solution. A PART naming a question, or naming a part the draft has not written, is refused, naming the part. A sheet laid out this way can now be answered part by part by commands, which only a spec's `PartPartSolSol` and `PartSolPartSol` layouts could do before, so the warning `in2lambda validate` prints about a part nothing answers can be acted on. diff --git a/in2lambda/source/__init__.py b/in2lambda/source/__init__.py index f493c17..659d5d2 100644 --- a/in2lambda/source/__init__.py +++ b/in2lambda/source/__init__.py @@ -97,6 +97,12 @@ def _field_fault(field: Any) -> str: The writer and the reader must agree. Pandoc's ``markdown`` writer emits fenced divs and bracketed spans that a commonmark reader reads as ordinary text. ``commonmark_x`` also covers the ``$...$`` maths and the ``{width=...}`` attributes a converted document holds. + +``commonmark_x`` does write the bracketed spans a .docx holds, which +:func:`_spans_unwrapped` unwraps afterwards. The ``-bracketed_spans`` the writer takes +does not help: with ``raw_html`` on, which ``commonmark_x`` keeps, the writer falls back +to ``Question 2:`` and ``oil``, and turning ``raw_html`` +off as well changes how a figure is written. """ _POSITION = re.compile(r"(?:[^@;]*@)?(\d+):\d+-(\d+):(\d+)") @@ -422,6 +428,73 @@ def blocked(position: int) -> bool: return "".join(written) +_SPAN = re.compile(r"\[([^\[\]\n]*)\]\{[^{}\n]*\}") +"""A bracketed span as ``commonmark_x`` writes one, opened and closed on the one line. + +The ``]{`` is what tells one from a link's ``](`` and from an image's ``){width=...}``. +""" + + +def _spans_unwrapped(markdown: str) -> str: + r"""Markdown pandoc wrote, with the attributes of its bracketed spans dropped. + + Pandoc's docx reader turns Word's underline into ``[Question 2:]{.underline}``, its + highlight into ``[oil]{.mark}`` and its small capitals into ``[Note:]{.smallcaps}``. + A field quoted out of markdown holding one of those is read back by the PDF + generator's pandoc as underline, highlight or small capitals, and written to LaTeX as + a command the generator's template does not define, so the set fails to compile. + Lambda Feedback's markdown renders none of the three, so the attribute is dropped and + the text it marked is kept. The reader emits a ``[text]{custom-style=...}`` span only + with its ``+styles`` extension, which the freeze does not enable; a document holding + one is unwrapped the same way. + + Every rewrite stays within the line it began on, so no line range moves. A span + pandoc broke over two lines is left as written, and ``in2lambda validate`` reports the + field quoting it. A span in a code block or in inline code is left as written as well: + those are characters the document shows. + + Examples: + >>> from in2lambda.source import _spans_unwrapped + >>> _spans_unwrapped("# [Hydraulic scale]{.underline}\n") + '# Hydraulic scale\n' + >>> _spans_unwrapped("**[Question 2:]{.underline}** joined by [oil]{.mark}.\n") + '**Question 2:** joined by oil.\n' + >>> _spans_unwrapped("[**[a]{.mark}**]{.underline}\n") + '**a**\n' + >>> _spans_unwrapped("[Note:]{.smallcaps} the oil is incompressible.\r\n") + 'Note: the oil is incompressible.\r\n' + >>> _spans_unwrapped("Type `[a]{.mark}` first.\n") + 'Type `[a]{.mark}` first.\n' + >>> _spans_unwrapped("Type this:\n\n [a]{.mark}\n") + 'Type this:\n\n [a]{.mark}\n' + >>> _spans_unwrapped("::: {.solution}\nThe load is $F = pA$.\n:::\n") + '::: {.solution}\nThe load is $F = pA$.\n:::\n' + >>> _spans_unwrapped('![](figure.png){width="1in"}\n') + '![](figure.png){width="1in"}\n' + """ + if "\r\n" in markdown: + # Pandoc writes the line endings of whoever is running it, and the file on disk + # is hashed as it is written, so a Windows freeze stays a Windows file. + return _spans_unwrapped(markdown.replace("\r\n", "\n")).replace("\n", "\r\n") + + verbatim = _verbatim_lines(markdown) + + def unwrapped(match: re.Match[str]) -> str: + before = markdown[markdown.rfind("\n", 0, match.start()) + 1 : match.start()] + if markdown.count("\n", 0, match.start()) + 1 in verbatim or ( + before.count("`") % 2 + ): + return match.group() + return match.group(1) + + rewritten = _SPAN.sub(unwrapped, markdown) + if rewritten == markdown: + return markdown + # A span holding a span - `[**[a]{.mark}**]{.underline}` - unwraps from the inside, + # because the brackets of the outer one hold the brackets of the inner one. + return _spans_unwrapped(rewritten) + + def _digest(data: bytes) -> str: """How a frozen markdown is named in its draft, so that a change to it is reported. @@ -870,7 +943,9 @@ def add( are written to ``FILE.draft.json``, so that code quoting a source by line range can check that those lines still hold the text they held. A converted file is written unwrapped: a paragraph is one line, however long, and each ``$$ ... $$`` is written - on lines of its own. Lambda Feedback renders display maths written that way. + on lines of its own. Lambda Feedback renders display maths written that way. The + underline, highlight and small capitals a .docx holds are dropped and the text they + marked is kept, because Lambda Feedback renders none of the three. The files are numbered in the order they are given. A file already frozen into the draft beside them is checked against the hash it was frozen at, and is not frozen @@ -929,11 +1004,14 @@ def add( raw, markdown = _source(path) frozen_path = path else: - # Unwrapped, and with the display maths blocked out, before anything is - # hashed: both are habits of pandoc's writer rather than anything the author - # did, and both are what a field quoting these lines would have to render. + # Unwrapped, with the display maths blocked out and the bracketed spans + # dropped, before anything is hashed: all three are habits of pandoc's writer + # rather than anything the author did, and all three are what a field quoting + # these lines would have to render. markdown = _display_maths_blocked( - _pandoc(str(path), _MARKDOWN, "--wrap=none").decode("utf-8") + _spans_unwrapped( + _pandoc(str(path), _MARKDOWN, "--wrap=none").decode("utf-8") + ) ) raw = markdown.encode("utf-8") frozen_path = path.with_suffix(".md") diff --git a/tests/fixtures/sources/README.md b/tests/fixtures/sources/README.md index 677bcad..5605d78 100644 --- a/tests/fixtures/sources/README.md +++ b/tests/fixtures/sources/README.md @@ -62,3 +62,12 @@ image usually has no alt text, so this is also the ordinary case. The image the docx embeds is referenced as `media/rId9.png`, which is not extracted - nothing here reads the image, only the lines around it. + +`bracketed_spans/source.docx` holds an underlined heading, a bold and underlined `Question 2:` +beside a highlighted word, and a run in small capitals. `make_source.py` beside it wrote that +document with python-docx, which is not a dependency of in2lambda: the document is committed and +the script records what it holds. Pandoc's docx reader turns the three formats into the bracketed +spans `[Hydraulic scale]{.underline}`, `[oil]{.mark}` and `[Note:]{.smallcaps}`, which the freeze +unwraps to the text alone. The test asserting that no fixture's frozen markdown holds a `]{` +looks for that rather than for `{.`, because `fenced_div` freezes to `::: {.solution}`, which is a +div the freeze keeps. diff --git a/tests/fixtures/sources/bracketed_spans/expected.json b/tests/fixtures/sources/bracketed_spans/expected.json new file mode 100644 index 0000000..1cc3329 --- /dev/null +++ b/tests/fixtures/sources/bracketed_spans/expected.json @@ -0,0 +1,20 @@ +[ + { + "end": 1, + "id": "b1", + "start": 1, + "type": "heading" + }, + { + "end": 3, + "id": "b2", + "start": 3, + "type": "paragraph" + }, + { + "end": 5, + "id": "b3", + "start": 5, + "type": "paragraph" + } +] diff --git a/tests/fixtures/sources/bracketed_spans/make_source.py b/tests/fixtures/sources/bracketed_spans/make_source.py new file mode 100644 index 0000000..36286ad --- /dev/null +++ b/tests/fixtures/sources/bracketed_spans/make_source.py @@ -0,0 +1,33 @@ +"""Writes the ``source.docx`` beside this file, which was run by hand. + +python-docx is not a dependency of in2lambda and is not added as one: the document is +committed, and this script records what it holds so that anyone can write it again. The +import stands inside the main block because the test suite imports every module under +``tests`` to collect its doctests. + +Word's underline, highlight and small capitals are the three formats pandoc's docx +reader turns into a bracketed span, which `in2lambda source add` unwraps. +""" + +if __name__ == "__main__": + from docx import Document + from docx.enum.text import WD_COLOR_INDEX + + document = Document() + + heading = document.add_heading("", level=1) + heading.add_run("Hydraulic scale").underline = True + + paragraph = document.add_paragraph() + label = paragraph.add_run("Question 2:") + label.bold = True + label.underline = True + paragraph.add_run(" A hydraulic scale has two pistons joined by ") + paragraph.add_run("oil").font.highlight_color = WD_COLOR_INDEX.YELLOW + paragraph.add_run(".") + + note = document.add_paragraph() + note.add_run("Note: ").font.small_caps = True + note.add_run("the oil is incompressible.") + + document.save("source.docx") diff --git a/tests/fixtures/sources/bracketed_spans/source.docx b/tests/fixtures/sources/bracketed_spans/source.docx new file mode 100644 index 0000000000000000000000000000000000000000..e44c9ac9087e445db9417e836833286e306cfa6a GIT binary patch literal 36744 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 Path: """The document a folder holds, whatever format it is in.""" @@ -53,6 +56,35 @@ def test_source_add_finds_the_expected_blocks(folder: Path, tmp_path: Path) -> N quoted = _quoted(draft, markdown, 1, block["start"], block["end"]) assert math_delimiter_checker(quoted) is MathDelimiterError.PASSED, quoted + # A bracketed span is underline, highlight or small capitals, which the PDF + # generator's pandoc writes as a LaTeX command its template does not define, so no + # freeze may leave one. A fenced div's `::: {.solution}` is not one of these. + assert not SPAN_ATTRIBUTE.search(markdown) + + +@needs_compiler +def test_a_frozen_docx_compiles_as_the_pdf_generator_does(tmp_path: Path) -> None: + """A .docx holding underline, highlight and small capitals is what faulted at build. + + The three sheets that reported it are private, so the fixture stands for them: every + block of it is quoted as a field would be and compiled as Lambda Feedback compiles a + set. + """ + shutil.copytree(SOURCES_DIR / "bracketed_spans", tmp_path, dirs_exist_ok=True) + + result = CliRunner().invoke(cli, ["source", "add", str(tmp_path / "source.docx")]) + + assert result.exit_code == 0, result.output + draft = json.loads((tmp_path / "source.draft.json").read_text()) + (source,) = draft["sources"] + markdown = (tmp_path / source["source"]).read_text() + fields = [ + (block["id"], _quoted(draft, markdown, 1, block["start"], block["end"])) + for block in source["blocks"] + ] + + assert pdf.problems(fields, []) == [] + def test_freezing_again_is_refused_once_the_source_has_changed( tmp_path: Path, monkeypatch From aa3e31da225bb951a3854534d4121cf38fd6c678 Mon Sep 17 00:00:00 2001 From: "Peter B. Johnson" Date: Mon, 21 Sep 2026 10:17:52 +0100 Subject: [PATCH 2/2] implement: Freeze a docx without the bracketed spans the PDF generator cannot compile (t58) --- in2lambda/source/__init__.py | 10 +++++++++- tests/fixtures/sources/bracketed_spans/make_source.py | 5 ++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/in2lambda/source/__init__.py b/in2lambda/source/__init__.py index 659d5d2..53dd491 100644 --- a/in2lambda/source/__init__.py +++ b/in2lambda/source/__init__.py @@ -428,10 +428,16 @@ def blocked(position: int) -> bool: return "".join(written) -_SPAN = re.compile(r"\[([^\[\]\n]*)\]\{[^{}\n]*\}") +_ATTRIBUTE = r"""[.#][^\s{}]+|[\w-]+=(?:"[^"\n]*"|[^\s{}]+)""" +"""One attribute of a pandoc attribute list: a class, an id, or a key and its value.""" + +_SPAN = re.compile(rf"\[([^\[\]\n]*)\]\{{(?:{_ATTRIBUTE})(?: +(?:{_ATTRIBUTE}))*\}}") """A bracketed span as ``commonmark_x`` writes one, opened and closed on the one line. The ``]{`` is what tells one from a link's ``](`` and from an image's ``){width=...}``. +The braces must hold an attribute list, because LaTeX writes brackets before braces as +well: ``$\\sqrt[3]{x + 1}$`` is a cube root, and dropping its braces would leave +``$\\sqrt3$``, which KaTeX renders and no check reports. """ @@ -471,6 +477,8 @@ def _spans_unwrapped(markdown: str) -> str: '::: {.solution}\nThe load is $F = pA$.\n:::\n' >>> _spans_unwrapped('![](figure.png){width="1in"}\n') '![](figure.png){width="1in"}\n' + >>> _spans_unwrapped("The root is $\\sqrt[3]{x + 1}$.\n") + 'The root is $\\sqrt[3]{x + 1}$.\n' """ if "\r\n" in markdown: # Pandoc writes the line endings of whoever is running it, and the file on disk diff --git a/tests/fixtures/sources/bracketed_spans/make_source.py b/tests/fixtures/sources/bracketed_spans/make_source.py index 36286ad..e5978fe 100644 --- a/tests/fixtures/sources/bracketed_spans/make_source.py +++ b/tests/fixtures/sources/bracketed_spans/make_source.py @@ -10,6 +10,8 @@ """ if __name__ == "__main__": + from pathlib import Path + from docx import Document from docx.enum.text import WD_COLOR_INDEX @@ -30,4 +32,5 @@ note.add_run("Note: ").font.small_caps = True note.add_run("the oil is incompressible.") - document.save("source.docx") + # Beside this file, so that the fixture is written wherever the script is run from. + document.save(Path(__file__).with_name("source.docx"))