|
1 | | -"""Compares two sets question by question, naming every place they say something else. |
| 1 | +r"""Compares two sets question by question, naming every place they say something else. |
2 | 2 |
|
3 | 3 | `in2lambda convert` writes a set, `in2lambda build` writes a set from a draft, and Lambda |
4 | 4 | Feedback exports a set. :func:`differences` compares any two of them in question and part |
5 | 5 | order - each question's main text, and each part's text and worked solution - and returns |
6 | 6 | one line per difference, naming the question, the part and the field as |
7 | 7 | `in2lambda.validation` names them. |
8 | 8 |
|
9 | | -Three differences in wording are not differences in what a question says, and are taken |
| 9 | +These differences in wording are not differences in what a question says, and are taken |
10 | 10 | off both sides before comparing: |
11 | 11 |
|
12 | 12 | - **Whitespace.** Every run of whitespace is compared as one space, because a draft |
13 | 13 | quotes the lines pandoc wrapped where `in2lambda convert` writes a paragraph on one |
14 | 14 | line. |
| 15 | +- **Separator lines.** A line holding nothing but three or more hyphens is dropped, |
| 16 | + because Lambda Feedback writes one around a display maths block where a document |
| 17 | + writes nothing. |
| 18 | +- **Quotes.** ``‘`` and ``’`` are compared as ``'``, and ``“`` and ``”`` as ``"``, |
| 19 | + because pandoc's LaTeX reader writes the curly quote where its commonmark_x writer |
| 20 | + writes the straight one. |
| 21 | +- **Maths notation.** Inside every ``$ ... $`` and ``$$ ... $$``, ``\left`` and |
| 22 | + ``\right`` are removed, ``~``, ``\,`` and ``\space`` are compared as a space, and |
| 23 | + every run of whitespace is dropped, except that a run between a control word and a |
| 24 | + following letter is compared as one space. LaTeX renders ``$z=2+3 i$`` and |
| 25 | + ``$z=2+3i$`` the same, and ``\mathrm{~m}`` and ``\mathrm{m}`` the same, where |
| 26 | + ``\alpha x`` and ``\alphax`` are two different expressions. An export writes |
| 27 | + ``$z = 2+3 i$`` where `in2lambda convert` writes ``$z=2+3i$``. |
15 | 28 | - **Image references.** An image is compared by the file's name, because |
16 | 29 | `in2lambda convert` writes every image as ```` where a draft keeps |
17 | 30 | the alt text the document wrote, and an export names each file as ``media/`` holds it |
|
25 | 38 | after `` # ``. |
26 | 39 | """ |
27 | 40 |
|
| 41 | +import re |
28 | 42 | from itertools import zip_longest |
29 | 43 | from pathlib import Path |
30 | 44 | from typing import Any, Optional |
31 | 45 |
|
32 | 46 | from in2lambda.api.question import Question |
33 | 47 | from in2lambda.api.set import Set |
34 | 48 | from in2lambda.json_convert.json_convert import _IMAGE |
35 | | -from in2lambda.validation import _location |
| 49 | +from in2lambda.validation import _COMMAND, _MATHS, _location |
36 | 50 |
|
37 | 51 | _TICKET = " # " |
38 | 52 | """What a line of a differs.txt names the ticket closing it after.""" |
39 | 53 |
|
| 54 | +_RULE = re.compile(r"(?m)^[ \t]*(?:-{3,}|\*{3,}|_{3,})[ \t]*$") |
| 55 | +"""A line holding nothing but three or more hyphens, asterisks or underscores: a markdown |
| 56 | +separator, which Lambda Feedback writes around a display maths as `---` or `***`.""" |
| 57 | + |
| 58 | +_QUOTES = str.maketrans({"‘": "'", "’": "'", "“": '"', "”": '"'}) |
| 59 | +"""Each curly quote and the straight quote it is compared as.""" |
| 60 | + |
| 61 | +_SIZE = re.compile(r"\\(?:left|right)(?![a-zA-Z])") |
| 62 | +r"""``\left`` and ``\right``, which size a delimiter without changing which it is.""" |
| 63 | + |
| 64 | +_LATEX_SPACE = re.compile(r"~|\\,|\\space(?![a-zA-Z])") |
| 65 | +"""The three ways of writing a space inside maths.""" |
| 66 | + |
| 67 | +_SPACING = re.compile(rf"({_COMMAND.pattern})\s+(?=[a-zA-Z])|\s+") |
| 68 | +"""A run of whitespace inside maths, with the control word it ends where one precedes it |
| 69 | +and a letter follows it.""" |
| 70 | + |
| 71 | + |
| 72 | +def _spacing(whitespace: re.Match[str]) -> str: |
| 73 | + r"""One space where a run of whitespace ends a control word, and nothing elsewhere. |
| 74 | +
|
| 75 | + ``\alpha x`` is two symbols and ``\alphax`` is a control word nothing defines, so |
| 76 | + the space between a control word and a letter is the only whitespace LaTeX renders. |
| 77 | + """ |
| 78 | + return f"{whitespace[1]} " if whitespace[1] else "" |
| 79 | + |
| 80 | + |
| 81 | +def _maths(expression: re.Match[str]) -> str: |
| 82 | + """One ``$ ... $`` or ``$$ ... $$`` with the notation that is not the maths folded. |
| 83 | +
|
| 84 | + Args: |
| 85 | + expression: A match of `in2lambda.validation._MATHS`, holding the display maths |
| 86 | + it found in its first group and the inline maths in its second. |
| 87 | + """ |
| 88 | + display = expression[1] is not None |
| 89 | + tex = expression[1] if display else expression[2] |
| 90 | + delimiter = "$$" if display else "$" |
| 91 | + folded = _LATEX_SPACE.sub(" ", _SIZE.sub("", tex)) |
| 92 | + return f"{delimiter}{_SPACING.sub(_spacing, folded)}{delimiter}" |
| 93 | + |
40 | 94 |
|
41 | 95 | def _text(markdown: str) -> str: |
42 | 96 | """A field with the differences in wording that are not differences taken off. |
43 | 97 |
|
44 | | - Every run of whitespace becomes one space, and every image reference is written as |
45 | | - the file's name alone. The module docstring says why. |
| 98 | + A separator line is dropped, each curly quote becomes a straight quote, the maths |
| 99 | + notation inside every ``$ ... $`` and ``$$ ... $$`` is folded, every image reference |
| 100 | + is written as the file's name alone, and every run of whitespace becomes one space. |
| 101 | + The module docstring says why. |
46 | 102 | """ |
47 | | - named = _IMAGE.sub(lambda reference: f".name})", markdown) |
| 103 | + # Before the whitespace collapse below, which writes the field on one line and |
| 104 | + # leaves no line for _RULE to match. |
| 105 | + # The platform writes a space as the entity ` ` (and a hard one as ` `). |
| 106 | + without_rules = _RULE.sub("", markdown.replace(" ", " ").replace(" ", " ")) |
| 107 | + folded = _MATHS.sub(_maths, without_rules.translate(_QUOTES)) |
| 108 | + # A space touching a maths delimiter from outside renders the same either way: |
| 109 | + # `of $y$` and `of$y$` are one expression, so the whitespace beside `$` is dropped. |
| 110 | + folded = re.sub(r"\s*(\${1,2})\s*", r"\1", folded) |
| 111 | + named = _IMAGE.sub(lambda reference: f".name})", folded) |
48 | 112 | return " ".join(named.split()) |
49 | 113 |
|
50 | 114 |
|
|
0 commit comments