texdiff 0.2.3__tar.gz → 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {texdiff-0.2.3/src/texdiff.egg-info → texdiff-0.2.5}/PKG-INFO +1 -1
  2. {texdiff-0.2.3 → texdiff-0.2.5}/pyproject.toml +1 -1
  3. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff/__init__.py +1 -1
  4. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff/align.py +5 -1
  5. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff/emit.py +15 -2
  6. {texdiff-0.2.3 → texdiff-0.2.5/src/texdiff.egg-info}/PKG-INFO +1 -1
  7. {texdiff-0.2.3 → texdiff-0.2.5}/tests/test_align.py +38 -0
  8. {texdiff-0.2.3 → texdiff-0.2.5}/tests/test_tables_render.py +58 -0
  9. {texdiff-0.2.3 → texdiff-0.2.5}/LICENSE +0 -0
  10. {texdiff-0.2.3 → texdiff-0.2.5}/README.md +0 -0
  11. {texdiff-0.2.3 → texdiff-0.2.5}/setup.cfg +0 -0
  12. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff/api.py +0 -0
  13. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff/check.py +0 -0
  14. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff/cli.py +0 -0
  15. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff/flatten.py +0 -0
  16. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff/newlines.py +0 -0
  17. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff/nodes.py +0 -0
  18. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff/oldlines.py +0 -0
  19. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff/parse.py +0 -0
  20. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff/preamble.py +0 -0
  21. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff/tables.py +0 -0
  22. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff/textdiff.py +0 -0
  23. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff.egg-info/SOURCES.txt +0 -0
  24. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff.egg-info/dependency_links.txt +0 -0
  25. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff.egg-info/entry_points.txt +0 -0
  26. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff.egg-info/requires.txt +0 -0
  27. {texdiff-0.2.3 → texdiff-0.2.5}/src/texdiff.egg-info/top_level.txt +0 -0
  28. {texdiff-0.2.3 → texdiff-0.2.5}/tests/test_alignment.py +0 -0
  29. {texdiff-0.2.3 → texdiff-0.2.5}/tests/test_api.py +0 -0
  30. {texdiff-0.2.3 → texdiff-0.2.5}/tests/test_check.py +0 -0
  31. {texdiff-0.2.3 → texdiff-0.2.5}/tests/test_cli.py +0 -0
  32. {texdiff-0.2.3 → texdiff-0.2.5}/tests/test_corpus.py +0 -0
  33. {texdiff-0.2.3 → texdiff-0.2.5}/tests/test_emit.py +0 -0
  34. {texdiff-0.2.3 → texdiff-0.2.5}/tests/test_flatten.py +0 -0
  35. {texdiff-0.2.3 → texdiff-0.2.5}/tests/test_parse.py +0 -0
  36. {texdiff-0.2.3 → texdiff-0.2.5}/tests/test_preamble.py +0 -0
  37. {texdiff-0.2.3 → texdiff-0.2.5}/tests/test_table_head_order.py +0 -0
  38. {texdiff-0.2.3 → texdiff-0.2.5}/tests/test_tables.py +0 -0
  39. {texdiff-0.2.3 → texdiff-0.2.5}/tests/test_textdiff.py +0 -0
  40. {texdiff-0.2.3 → texdiff-0.2.5}/tests/test_verbatim.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: texdiff
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: AST-driven semantic diff for LaTeX documents
5
5
  Author: GoBobr
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "texdiff"
7
- version = "0.2.3"
7
+ version = "0.2.5"
8
8
  description = "AST-driven semantic diff for LaTeX documents"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -42,4 +42,4 @@ __all__ = [
42
42
  "word_diff",
43
43
  ]
44
44
 
45
- __version__ = "0.2.2"
45
+ __version__ = "0.2.5"
@@ -690,8 +690,12 @@ def _sibling_swap_rescue(edits: list[Edit], _pair) -> list[Edit]:
690
690
  for i, j in swaps.items():
691
691
  swap_new[i] = edits[j].new
692
692
  edits[j] = Insert(new=modifies[i].new)
693
+ # pair through _pair, NOT raw Modify: the swapped-in sibling can
694
+ # be verbatim-equal to the old node (the rescue often trades a
695
+ # mis-pairing for the node's TRUE counterpart). A raw Modify then
696
+ # renders an unchanged table as wholesale retired + reinserted.
693
697
  return [
694
- Modify(old=modifies[idx].old, new=swap_new[idx])
698
+ _pair(modifies[idx].old, swap_new[idx])
695
699
  if idx in swaps
696
700
  else e
697
701
  for idx, e in enumerate(edits)
@@ -1556,7 +1556,13 @@ def _is_safe_inline(text: str) -> bool:
1556
1556
  return False
1557
1557
  if "\n" in text:
1558
1558
  return False
1559
- if _ARG_MACRO_RE.search(text) or _ARG_MACRO_BR_RE.search(text):
1559
+ # decoration macros (\\emph, \\textbf, ...) with pure-text
1560
+ # arguments are LR-safe inside \\DIFdel/\\DIFadd - font commands
1561
+ # survive \\sout/\\uwave. Mask them before the macro-argument
1562
+ # probe or cells like ``\\emph{valid\\_min} to \\emph{valid\\_max}``
1563
+ # degrade to the colour-only fallback: red but never struck.
1564
+ probe = _SAFE_DECOR_RE.sub(lambda m: m.group(2), text)
1565
+ if _ARG_MACRO_RE.search(probe) or _ARG_MACRO_BR_RE.search(probe):
1560
1566
  return False
1561
1567
  if _LIST_ITEM_RE.search(text):
1562
1568
  # \item / \par inside a strikeout/wave is LR-mode illegal
@@ -2239,6 +2245,11 @@ def _escape_cell_pair(old_cell: str, new_cell: str) -> str:
2239
2245
  if _is_safe_inline(new_cell):
2240
2246
  return f"\\DIFadd{{{new_cell}}}"
2241
2247
  return f"\\DIFaddbegin{{}} {new_cell} \\DIFaddend{{}}"
2248
+ if not new_cell:
2249
+ # text -> empty: strike the old cell (blue adds nothing)
2250
+ if _is_safe_inline(old_cell):
2251
+ return f"\\DIFdel{{{old_cell}}}"
2252
+ return f"\\DIFdelbegin{{}} {old_cell} \\DIFdelend{{}}"
2242
2253
  # underscore-escape-only difference: the OLD ADS generator emits
2243
2254
  # raw ``_`` in variable paths while the NEW one escapes it as
2244
2255
  # ``\_`` - both render identically in LaTeX. A cell pair that
@@ -2459,7 +2470,9 @@ def _merge_keyed_rows(
2459
2470
  parts: list[str] = []
2460
2471
  for oc, nc in zip(old_cells, new_cells):
2461
2472
  oc_s, nc_s = oc.strip(), nc.strip()
2462
- if oc_s == nc_s or not nc_s:
2473
+ if oc_s == nc_s:
2474
+ parts.append(oc)
2475
+ elif not nc_s and not oc_s:
2463
2476
  parts.append(oc)
2464
2477
  else:
2465
2478
  parts.append(_escape_cell_pair(oc_s, nc_s))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: texdiff
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: AST-driven semantic diff for LaTeX documents
5
5
  Author: GoBobr
6
6
  License: MIT License
@@ -89,3 +89,41 @@ class TestSameSignatureDifferentText:
89
89
  new = [group(text_node("schemas")), group(text_node("handlers")), text_node("tail\n\n")]
90
90
  edits = align(old, new)
91
91
  assert [type(e).__name__ for e in edits] == ["Insert", "Match", "Match"]
92
+
93
+
94
+ def test_sibling_swap_rescue_pairs_verbatim_equal_swap_as_match():
95
+ """A swap that lands on a verbatim-equal sibling must Match.
96
+
97
+ The sibling-swap rescue re-pairs a Modify with the same-signature
98
+ Insert the old side actually resembles. When the swapped-in
99
+ sibling is byte-identical to the old node - an unchanged table
100
+ re-inserted next to a new sibling table (PGS "consumed
101
+ variables" band table) - the rescue used to construct a raw
102
+ Modify. Emit then rendered the IDENTICAL table as a wholesale
103
+ retired (red, unstruck) copy followed by a blue re-inserted
104
+ copy. The swap must go through _pair so equal text degrades to
105
+ a plain Match.
106
+ """
107
+ from texdiff.align import _sibling_swap_rescue
108
+ from texdiff.parse import parse
109
+
110
+ def _env(body: str):
111
+ return parse(f"\\begin{{savenotes}}\n{body}\n\\end{{savenotes}}\n")[0]
112
+
113
+ def _pair(o, n):
114
+ if o.text == n.text:
115
+ return Match(node=o)
116
+ return Modify(old=o, new=n)
117
+
118
+ old_node = _env("alpha beta gamma delta epsilon zeta")
119
+ wrong_new = _env("an unrelated climatology tail with other words")
120
+ right_new = _env("alpha beta gamma delta epsilon zeta")
121
+
122
+ edits = [Modify(old=old_node, new=wrong_new), Insert(new=right_new)]
123
+ out = _sibling_swap_rescue(edits, _pair)
124
+ # the identical counterpart matches: renders black, not retired
125
+ assert type(out[0]).__name__ == "Match"
126
+ assert out[0].node is old_node
127
+ # the previously (wrongly) chosen sibling stays visible as insert
128
+ assert type(out[1]).__name__ == "Insert"
129
+ assert out[1].new is wrong_new
@@ -1018,3 +1018,61 @@ class TestGroupedPairingRegressions:
1018
1018
  assert name in out[nearest : nearest + 12], (
1019
1019
  f"{unit} merged into the wrong group"
1020
1020
  )
1021
+
1022
+ def test_decorated_macro_cell_strikes_not_just_colours(self):
1023
+ r"""Range-cell case: \emph{..} cells must strike, not just colour.
1024
+
1025
+ A changed cell containing only decoration macros with
1026
+ pure-text arguments (``\emph{valid\_min} to
1027
+ \emph{valid\_max}``) is LR-safe inside \DIFdel/\DIFadd:
1028
+ font commands survive \sout/\uwave. It must render with the
1029
+ inline strike markup, not degrade to the colour-switch-only
1030
+ fallback (\DIFdelbegin..\DIFdelend), which paints the old
1031
+ text red without striking it.
1032
+ """
1033
+ old_src = (
1034
+ "\\documentclass{article}\\begin{document}\n"
1035
+ "\\begin{longtable}{ll}\n\\caption{vars}\\\\\n"
1036
+ "\\rowcolor{lightcyan} \\textbf{lat} & Latitude & float & "
1037
+ "\\emph{valid\\_min} to \\emph{valid\\_max} & d1\\\\\n"
1038
+ "\\end{longtable}\\end{document}\n"
1039
+ )
1040
+ new_src = old_src.replace(
1041
+ "\\emph{valid\\_min} to \\emph{valid\\_max} & d1",
1042
+ "defined by \\emph{valid\\_range} & lat",
1043
+ )
1044
+ out = diff_documents(old_src, new_src, inject_preamble=False).marked_up
1045
+ # the old range cell is STRUCK inside \DIFdel{..}, not merely
1046
+ # coloured by the block fallback
1047
+ assert "\\DIFdel{\\emph{valid\\_min} to \\emph{valid\\_max}}" in out
1048
+ # no colour-only degradation for the pair
1049
+ assert "\\DIFdelbegin{} \\emph{valid" not in out
1050
+ # new side waves its markup too
1051
+ assert "\\DIFadd{defined by \\emph{valid\\_range}}" in out
1052
+
1053
+ def test_text_to_empty_cell_change_is_marked(self):
1054
+ r"""Text->empty cell change must strike the old cell.
1055
+
1056
+ When a cell's content is removed entirely (the regenerated
1057
+ spec leaves the Range column blank), the per-cell merge used
1058
+ to emit the OLD cell verbatim with no markup at all: the
1059
+ retired value silently appeared as unchanged text. It must
1060
+ render struck red (and its counterpart, empty->text, wavy
1061
+ blue).
1062
+ """
1063
+ old_src = (
1064
+ "\\documentclass{article}\\begin{document}\n"
1065
+ "\\begin{longtable}{lll}\n"
1066
+ "z0 & 0 & 0 \\\\\n"
1067
+ "z1 & 1 & 1 \\\\\n"
1068
+ "\\rowcolor{lightcyan} \\textbf{day} & Date of Day & "
1069
+ "\\emph{valid\\_min} to \\emph{valid\\_max} \\\\\n"
1070
+ "w & 5 & 6 \\\\\n"
1071
+ "\\end{longtable}\\end{document}\n"
1072
+ )
1073
+ new_src = old_src.replace(
1074
+ "\\emph{valid\\_min} to \\emph{valid\\_max} \\\\",
1075
+ " \\\\",
1076
+ )
1077
+ out = diff_documents(old_src, new_src, inject_preamble=False).marked_up
1078
+ assert "\\DIFdel{\\emph{valid\\_min} to \\emph{valid\\_max}}" in out
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes