pyDiffTools 0.1.53__tar.gz → 0.1.55__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 (129) hide show
  1. {pydifftools-0.1.53/pyDiffTools.egg-info → pydifftools-0.1.55}/PKG-INFO +4 -2
  2. {pydifftools-0.1.53 → pydifftools-0.1.55}/README.rst +3 -1
  3. {pydifftools-0.1.53 → pydifftools-0.1.55/pyDiffTools.egg-info}/PKG-INFO +4 -2
  4. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/line_alignment.py +65 -11
  5. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/match_spaces.py +7 -2
  6. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/wrap_sentences.py +24 -5
  7. {pydifftools-0.1.53 → pydifftools-0.1.55}/pyproject.toml +1 -1
  8. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_line_alignment.py +212 -10
  9. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_wrap_check.py +50 -16
  10. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_wrapping.py +8 -2
  11. {pydifftools-0.1.53 → pydifftools-0.1.55}/LICENSE.md +0 -0
  12. {pydifftools-0.1.53 → pydifftools-0.1.55}/MANIFEST.in +0 -0
  13. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/_quarto.yml +0 -0
  14. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  15. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  16. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  17. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  18. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  19. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  20. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  21. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  22. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  23. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  24. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  25. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  26. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  27. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  28. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  29. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  30. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  31. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  32. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  33. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  34. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  35. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  36. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/example.qmd +0 -0
  37. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/example.tex +0 -0
  38. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/index.qmd +0 -0
  39. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  40. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  41. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  42. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  43. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/subproject1/independent.qmd +0 -0
  44. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/subproject1/index.qmd +0 -0
  45. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  46. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  47. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  48. {pydifftools-0.1.53 → pydifftools-0.1.55}/example_notebook/project1/test_include.qmd +0 -0
  49. {pydifftools-0.1.53 → pydifftools-0.1.55}/pyDiffTools.egg-info/SOURCES.txt +0 -0
  50. {pydifftools-0.1.53 → pydifftools-0.1.55}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  51. {pydifftools-0.1.53 → pydifftools-0.1.55}/pyDiffTools.egg-info/entry_points.txt +0 -0
  52. {pydifftools-0.1.53 → pydifftools-0.1.55}/pyDiffTools.egg-info/requires.txt +0 -0
  53. {pydifftools-0.1.53 → pydifftools-0.1.55}/pyDiffTools.egg-info/top_level.txt +0 -0
  54. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/__init__.py +0 -0
  55. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/bibliography.py +0 -0
  56. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/browser_lifecycle.py +0 -0
  57. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/check_numbers.py +0 -0
  58. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/citation_dialog.py +0 -0
  59. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/command_line.py +0 -0
  60. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/command_registry.py +0 -0
  61. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/comment_filter_history.json +0 -0
  62. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/comment_functions.py +0 -0
  63. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/comment_migration.py +0 -0
  64. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/comment_tags.lua +0 -0
  65. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/comment_tags_margin.lua +0 -0
  66. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/comment_tags_no_comments.lua +0 -0
  67. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/comment_toggle.js +0 -0
  68. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/comments.css +0 -0
  69. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/comments_author_colors.css +0 -0
  70. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/completions/git-mergein +0 -0
  71. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/continuous.py +0 -0
  72. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/copy_files.py +0 -0
  73. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/diff-doc.js +0 -0
  74. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/doc_contents.py +0 -0
  75. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/flowchart/__init__.py +0 -0
  76. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/flowchart/comparison.py +0 -0
  77. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  78. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/flowchart/graph.py +0 -0
  79. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/flowchart/source_jump.js +0 -0
  80. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  81. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/flowchart/watch_graph.py +0 -0
  82. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/flowchart/wgrph_view.js +0 -0
  83. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/forward_search.py +0 -0
  84. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/git_aliases.py +0 -0
  85. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/git_gd.py +0 -0
  86. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/git_gd_history.py +0 -0
  87. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/git_gd_image.py +0 -0
  88. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/git_gd_qt.py +0 -0
  89. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/html_comments.py +0 -0
  90. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/html_uncomments.py +0 -0
  91. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/log_example.py +0 -0
  92. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/notebook/__init__.py +0 -0
  93. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/notebook/display.py +0 -0
  94. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/notebook/fast_build.py +0 -0
  95. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/notebook/tex_to_qmd.py +0 -0
  96. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/onewordify.py +0 -0
  97. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/onewordify_undo.py +0 -0
  98. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/outline.py +0 -0
  99. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/pdf_diff.py +0 -0
  100. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/rearrange_tex.py +0 -0
  101. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/searchacro.py +0 -0
  102. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/separate_comments.py +0 -0
  103. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/source_jump.py +0 -0
  104. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/split_conflict.py +0 -0
  105. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/unseparate_comments.py +0 -0
  106. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/update_check.py +0 -0
  107. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/wrapping_options.py +0 -0
  108. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/xml2xlsx.vbs +0 -0
  109. {pydifftools-0.1.53 → pydifftools-0.1.55}/pydifftools/zotero.py +0 -0
  110. {pydifftools-0.1.53 → pydifftools-0.1.55}/setup.cfg +0 -0
  111. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_bibliography.py +0 -0
  112. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_browser_lifecycle.py +0 -0
  113. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_check_single_use.py +0 -0
  114. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_command_line_help.py +0 -0
  115. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_comment_filters.py +0 -0
  116. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_comment_migration.py +0 -0
  117. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_continuous_shutdown.py +0 -0
  118. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_forward_search.py +0 -0
  119. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_git_gd_history.py +0 -0
  120. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_git_gd_image.py +0 -0
  121. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_git_gd_qt.py +0 -0
  122. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_git_mergein.py +0 -0
  123. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_git_pdf_diff.py +0 -0
  124. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_pdf_diff.py +0 -0
  125. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_rrng.py +0 -0
  126. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_source_jump.py +0 -0
  127. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_tex_to_qmd.py +0 -0
  128. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_update_check.py +0 -0
  129. {pydifftools-0.1.53 → pydifftools-0.1.55}/tests/test_zotero.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.53
3
+ Version: 0.1.55
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -306,7 +306,9 @@ included are (listed in order of fun/utility):
306
306
  * ``cpb`` and ``wmatch`` share a matcher that finds changed hunks, aligns
307
307
  their words using Git's minimal diff, and restores reference whitespace.
308
308
  It then considers extra breaks that preserve an unchanged reference line
309
- without stranding a tiny edited fragment, and checks wrapping rules.
309
+ without stranding an edited fragment of three words or fewer, and checks
310
+ wrapping rules. When matching a baseline, edited lines may exceed the
311
+ character width by at most four words before wrapping is required.
310
312
 
311
313
  * ``--wrapnumber`` sets the maximum width (79 for ``cpb`` and ``wmatch``).
312
314
  Wmatch also uses nearby reference widths for large edits, retaining
@@ -251,7 +251,9 @@ included are (listed in order of fun/utility):
251
251
  * ``cpb`` and ``wmatch`` share a matcher that finds changed hunks, aligns
252
252
  their words using Git's minimal diff, and restores reference whitespace.
253
253
  It then considers extra breaks that preserve an unchanged reference line
254
- without stranding a tiny edited fragment, and checks wrapping rules.
254
+ without stranding an edited fragment of three words or fewer, and checks
255
+ wrapping rules. When matching a baseline, edited lines may exceed the
256
+ character width by at most four words before wrapping is required.
255
257
 
256
258
  * ``--wrapnumber`` sets the maximum width (79 for ``cpb`` and ``wmatch``).
257
259
  Wmatch also uses nearby reference widths for large edits, retaining
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.53
3
+ Version: 0.1.55
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -306,7 +306,9 @@ included are (listed in order of fun/utility):
306
306
  * ``cpb`` and ``wmatch`` share a matcher that finds changed hunks, aligns
307
307
  their words using Git's minimal diff, and restores reference whitespace.
308
308
  It then considers extra breaks that preserve an unchanged reference line
309
- without stranding a tiny edited fragment, and checks wrapping rules.
309
+ without stranding an edited fragment of three words or fewer, and checks
310
+ wrapping rules. When matching a baseline, edited lines may exceed the
311
+ character width by at most four words before wrapping is required.
310
312
 
311
313
  * ``--wrapnumber`` sets the maximum width (79 for ``cpb`` and ``wmatch``).
312
314
  Wmatch also uses nearby reference widths for large edits, retaining
@@ -88,7 +88,7 @@ def align_line_breaks(
88
88
  ):
89
89
  """Return changed hunks with original, linted and proposed layouts.
90
90
 
91
- Align words with minimal diff, reuse whitespace after matching words,
91
+ Align words with minimal diff, reuse equivalent source whitespace,
92
92
  then preserve complete reference lines and enforce line linting. CPB
93
93
  and wmatch apply the aligned layout automatically.
94
94
  Width can be a number or a function choosing a limit after restoration.
@@ -148,6 +148,9 @@ def align_line_breaks(
148
148
  margins[row] = allowed[row] and (
149
149
  inside or (
150
150
  margins[row] and "\t" not in indent and len(indent) < 4
151
+ and not (
152
+ row and not rows[row - 1].strip() and len(indent) >= 2
153
+ )
151
154
  )
152
155
  )
153
156
  position += len(line)
@@ -174,7 +177,7 @@ def align_line_breaks(
174
177
  if p["marker"] else "")
175
178
  for p in prefixes
176
179
  ]
177
- math_word = re.compile(
180
+ span_word = re.compile(
178
181
  r"\$\$|\$|\\[A-Za-z]+|\\[^\s]|[A-Za-z]+|[0-9]+(?:\.[0-9]+)?|[^\s]"
179
182
  )
180
183
  tokens = []
@@ -186,7 +189,11 @@ def align_line_breaks(
186
189
  end]
187
190
  for left, right in zip(boundaries, boundaries[1:]):
188
191
  in_math = region_at(equations, base + left) is not None
189
- pattern = math_word if in_math else WORD
192
+ hidden = region_at(comments, base + left) is not None
193
+ # Hidden source has no rendered word boundaries. Separate
194
+ # punctuation there too, so layout changes do not turn
195
+ # "$$R_c" into unmatched "$$" and "R_c" tokens.
196
+ pattern = span_word if in_math or hidden else WORD
190
197
  tokens.extend(
191
198
  (row, match.start(), match.end())
192
199
  for match in pattern.finditer(text, left, right)
@@ -249,6 +256,7 @@ def align_line_breaks(
249
256
  if allowed:
250
257
  for line, message in check_prose(
251
258
  span, width, punctuation_slop, offset,
259
+ overflow_words=4,
252
260
  ):
253
261
  issues.setdefault(line, message)
254
262
  offset += span.count("\n")
@@ -348,6 +356,39 @@ def align_line_breaks(
348
356
  whitespace = dict(gaps)
349
357
  ending = "\r\n" if "\r\n" in before else "\n"
350
358
 
359
+ def equivalent_gap(number, replacement):
360
+ # {{{ keep rendered separators and explicit Markdown hard breaks
361
+ raw = gaps[number]
362
+ left = start + tokens[number][1]
363
+ right = start + tokens[number + 1][1]
364
+ region = region_at(current_comments, left)
365
+ if region is not None and right < region[1]:
366
+ return True
367
+ region = region_at(current_equations, left)
368
+ if region is not None and right < region[1]:
369
+ # Display delimiters can sit on their own source lines.
370
+ # All other math gaps obey the same rule as prose; no
371
+ # knowledge of TeX commands or text macros is needed.
372
+ if current[region[0]:region[0] + 2] == "$$" and (
373
+ tokens[number][1] + start == region[0]
374
+ or tokens[number + 1][2] + start == region[1]
375
+ ):
376
+ return True
377
+ return bool(raw) == bool(replacement)
378
+ # Collapsing a whitespace run retains one separator. It cannot
379
+ # create a separator from an empty gap or remove the last one.
380
+ # Line margins are handled separately, with structural margins
381
+ # excluded. Two spaces before a newline are a Markdown hard
382
+ # break, and a newline after a backslash is explicit as well.
383
+ if any(re.search(r" {2,}\r?\n", gap)
384
+ for gap in (raw, replacement)) or (
385
+ before[tokens[number][2] - 1:tokens[number][2]] == "\\"
386
+ and ("\n" in raw or "\n" in replacement)
387
+ ):
388
+ return raw == replacement
389
+ return bool(raw) == bool(replacement)
390
+ # }}}
391
+
351
392
  # {{{ restore presentation indentation at matching line starts
352
393
  old_first = {}
353
394
  for number, (row, _, _) in enumerate(old_tokens):
@@ -430,19 +471,27 @@ def align_line_breaks(
430
471
  old_number = candidate
431
472
  if old_number in old_gaps:
432
473
  raw = old_gaps[old_number]
474
+ if not equivalent_gap(number, raw):
475
+ continue
433
476
  region = region_at(regions, start + tokens[number][1])
434
477
  inside_region = (
435
478
  region is not None
436
479
  and start + tokens[number + 1][1] < region[1]
437
480
  )
438
- if (
439
- number not in matches[hunk] and "\n" in raw
440
- and "\n" not in gaps[number] and not inside_region
441
- and len("".join(
442
- before[:tokens[number][2]].split("\n")[-1].split()
443
- )) < 10
481
+ if "\n" in raw and "\n" not in gaps[number] and (
482
+ not inside_region
444
483
  ):
445
- continue
484
+ original_rows = set(render(whitespace).splitlines())
485
+ proposed = dict(whitespace)
486
+ proposed[number] = raw
487
+ inherited = {line.rstrip() for line in reference_lines}
488
+ if any(
489
+ line not in original_rows
490
+ and line.rstrip() not in inherited
491
+ and 0 < len(line.split()) <= 3
492
+ for line in render(proposed).splitlines()
493
+ ):
494
+ continue
446
495
  if inside_region or (
447
496
  current_layout[3][j1 + tokens[number][0]]
448
497
  and current_layout[3][j1 + tokens[number + 1][0]]
@@ -494,12 +543,17 @@ def align_line_breaks(
494
543
  elif "\n" in proposed[number]:
495
544
  proposed[number] = " "
496
545
  proposed_rows = render(proposed).splitlines()
546
+ if any(
547
+ not equivalent_gap(number, proposed[number])
548
+ for number in needed
549
+ ):
550
+ continue
497
551
  if old_line not in {line.rstrip() for line in proposed_rows}:
498
552
  continue
499
553
  if any(
500
554
  line not in original_rows
501
555
  and line.rstrip() not in inherited
502
- and 0 < len("".join(line.split())) < 10
556
+ and 0 < len(line.split()) <= 3
503
557
  for line in proposed_rows
504
558
  ):
505
559
  continue
@@ -1,6 +1,7 @@
1
1
  from difflib import SequenceMatcher
2
2
  from bisect import bisect_left
3
3
  import math
4
+ import re
4
5
  from .line_alignment import align_line_breaks
5
6
  from .wrapping_options import (
6
7
  DEFAULT_WIDTH,
@@ -92,7 +93,6 @@ def match_text(
92
93
  trailing_dependent_phrase=DEFAULT_TRAILING_DEPENDENT_PHRASE,
93
94
  ):
94
95
  """Align reference lines and wrap only overflowing edited prose."""
95
- text2 = text2.replace("\u00a0", " ").replace("\u2004", " ")
96
96
  # {{{ choose the nearby reference width before any line linting
97
97
  reference_lines = classify_lines(text1, filetype)
98
98
  prose_rows = [
@@ -118,7 +118,12 @@ def match_text(
118
118
  return (
119
119
  local
120
120
  if any(
121
- len(line) > (local if joined else 1.5 * local)
121
+ len(line) > local and (
122
+ joined or sum(
123
+ match.end() > local
124
+ for match in re.finditer(r"\S+", line)
125
+ ) > 4
126
+ )
122
127
  for line in restored.splitlines()
123
128
  )
124
129
  else fallback_width
@@ -880,9 +880,14 @@ def wr(
880
880
  fp.write(result)
881
881
 
882
882
 
883
- def check_prose(content, wrapnumber, punctuation_slop, line_start=1):
883
+ def check_prose(
884
+ content, wrapnumber, punctuation_slop, line_start=1, overflow_words=0,
885
+ ):
884
886
  """Report source lines in `content` that break wr's rules.
885
887
 
888
+ Diff alignment can set overflow_words to tolerate that many words ending
889
+ beyond the character width. Ordinary wrapping checks use a strict limit.
890
+
886
891
  Checks mirror wrap_prose's own paragraph/sentence/macro
887
892
  splitting so the two commands never disagree about what counts as a
888
893
  sentence: lines must not hold more words than wr's own greedy choice
@@ -953,7 +958,18 @@ def check_prose(content, wrapnumber, punctuation_slop, line_start=1):
953
958
  + len(words[actual_end])
954
959
  - positions[fragment_offset]
955
960
  )
956
- if actual_end > wr_end:
961
+ # Diff alignment can keep a few extra words to
962
+ # avoid moving the sentence tail onto a new line.
963
+ # Count against the character limit, independently
964
+ # of an earlier preferred punctuation break.
965
+ excess = sum(
966
+ positions[j] + len(words[j])
967
+ - positions[fragment_offset] > wrapnumber
968
+ for j in range(fragment_offset, actual_end + 1)
969
+ )
970
+ if actual_end > wr_end and not (
971
+ overflow_words and 0 < excess <= overflow_words
972
+ ):
957
973
  overflow = words[wr_end + 1]
958
974
  rule = (
959
975
  "line too long"
@@ -1134,7 +1150,7 @@ def _mask_html_comments(content, initially_open=False):
1134
1150
 
1135
1151
 
1136
1152
  def markdown_lint_issues_from_text(
1137
- content, wrapnumber=45, punctuation_slop=20
1153
+ content, wrapnumber=45, punctuation_slop=20, overflow_words=0,
1138
1154
  ):
1139
1155
  """Return Markdown writing issues, including unfinished source spans."""
1140
1156
  masked_content = _mask_html_comments(content)
@@ -1149,7 +1165,9 @@ def markdown_lint_issues_from_text(
1149
1165
  for line, width, span in _iter_block_spans(
1150
1166
  markdown_blocks(prose_content), wrapnumber
1151
1167
  )
1152
- for issue in check_prose(span, width, punctuation_slop, line)
1168
+ for issue in check_prose(
1169
+ span, width, punctuation_slop, line, overflow_words,
1170
+ )
1153
1171
  ]
1154
1172
  issues.extend(
1155
1173
  (issue["line"], issue["message"])
@@ -1523,7 +1541,8 @@ def autofix_markdown_file(
1523
1541
  # issue at a time re-lints the whole file for every single fix.
1524
1542
  for _ in range(100):
1525
1543
  issues = markdown_lint_issues_from_text(
1526
- content, wrapnumber=wrapnumber, punctuation_slop=punctuation_slop
1544
+ content, wrapnumber=wrapnumber, punctuation_slop=punctuation_slop,
1545
+ overflow_words=4 if baseline is not None else 0,
1527
1546
  )
1528
1547
  fixable = {}
1529
1548
  for line_number, message in issues:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyDiffTools"
7
- version = "0.1.53"
7
+ version = "0.1.55"
8
8
  authors = [{ name = "J M Franck" }]
9
9
  description = "Diff tools"
10
10
  readme = "README.rst"
@@ -47,6 +47,129 @@ def test_partial_matching_lines_restore_whitespace_first(tmp_path):
47
47
  assert new.read_text() == expected
48
48
 
49
49
 
50
+ @pytest.mark.parametrize("reference,current", [
51
+ (
52
+ r"the TEMPO-SO~4~ probe ($32.9;\text{kJ}/\text{mol}$"
53
+ "(@fig:DodecaTSO4TempCycle, blue),\nwhich\n",
54
+ r"the TEMPO-SO~4~ probe ($32.9;\text{kJ}/\text{mol}$"
55
+ " (@fig:DodecaTSO4TempCycle, blue),\nwhich\n",
56
+ ),
57
+ (
58
+ r"the TEMPO-SO~4~ probe inside small RMs dispersed in"
59
+ "\ndodecane ($32.9;\\text{kJ}/\\text{mol}$ : "
60
+ "@fig:DodecaTSO4TempCycle, blue),\nwhich\n",
61
+ r"the TEMPO-SO~4~ probe inside small RMs dispersed in"
62
+ "\ndodecane ($32.9;\\text{kJ}/\\text{mol}$: "
63
+ "@fig:DodecaTSO4TempCycle, blue),\nwhich\n",
64
+ ),
65
+ ("First $x$ (reference).\n", "First $x$(reference).\n"),
66
+ ("First $x$: reference.\n", "First $x$ : reference.\n"),
67
+ ("First words continue here.\n", "First words \ncontinue here.\n"),
68
+ ("A long unchanged introductory line stays here.\n"
69
+ "First words\\\ncontinue here.\n",
70
+ "A long unchanged introductory line stays here.\n"
71
+ "First words\\ continue here.\n"),
72
+ ("Keep ordinary spaces.\n", "Keep\u00a0ordinary\u2004spaces.\n"),
73
+ (r"Use $\text{a,b}$ here." + "\n",
74
+ r"Use $\text{a, b}$ here." + "\n"),
75
+ (r"Use $\text{a, b}$ here." + "\n",
76
+ r"Use $\text{a,b}$ here." + "\n"),
77
+ (r"Use $\text{a,{b,c}}$ here." + "\n",
78
+ r"Use $\text{a, {b, c}}$ here." + "\n"),
79
+ (r"Use $\text{a \text{b} c,d}$ here." + "\n",
80
+ r"Use $\text{a \text{b} c, d}$ here." + "\n"),
81
+ (r"Use $\alpha{}b$ here." + "\n",
82
+ r"Use $\alpha b$ here." + "\n"),
83
+ (r"Use $\custom{a,b}$ here." + "\n",
84
+ r"Use $\custom{a, b}$ here." + "\n"),
85
+ ("Use $<!-- hidden words -->$ here.\n",
86
+ "Use $<!--hidden words-->$ here.\n"),
87
+ ])
88
+ def test_rendered_spacing_survives_shared_alignment_and_wmatch(
89
+ tmp_path, reference, current,
90
+ ):
91
+ for result in align_line_breaks(reference, current, width=1000):
92
+ assert result["aligned"] == result["before"]
93
+ old, new = tmp_path / "old.md", tmp_path / "new.md"
94
+ old.write_text(reference)
95
+ new.write_text(current)
96
+ run([str(old), str(new)], wrapnumber=1000)
97
+ assert new.read_text() == current
98
+
99
+
100
+ def test_removed_hard_break_is_not_restored_by_alignment(tmp_path):
101
+ reference = "First words \ncontinue here.\n"
102
+ current = "First words continue here.\n"
103
+ (result,) = align_line_breaks(reference, current)
104
+ assert " \n" not in result["aligned"]
105
+ old, new = tmp_path / "old.md", tmp_path / "new.md"
106
+ old.write_text(reference)
107
+ new.write_text(current)
108
+ run([str(old), str(new)])
109
+ assert " \n" not in new.read_text()
110
+
111
+
112
+ @pytest.mark.parametrize("context", ["{}", "Use ${}$ here."])
113
+ @pytest.mark.parametrize("before,after", [
114
+ ("alpha,beta", "alpha, beta"),
115
+ ("alpha, beta", "alpha,beta"),
116
+ ("alpha(beta)", "alpha (beta)"),
117
+ ("alpha (beta)", "alpha(beta)"),
118
+ ("alpha:beta", "alpha :beta"),
119
+ ("alpha :beta", "alpha:beta"),
120
+ ])
121
+ def test_alignment_preserves_last_space_everywhere(
122
+ tmp_path, context, before, after,
123
+ ):
124
+ reference = context.format(before) + "\n"
125
+ current = context.format(after) + "\n"
126
+ for result in align_line_breaks(reference, current):
127
+ assert result["aligned"] == result["before"]
128
+ old, new = tmp_path / "old.md", tmp_path / "new.md"
129
+ old.write_text(reference)
130
+ new.write_text(current)
131
+ run([str(old), str(new)])
132
+ assert new.read_text() == current
133
+
134
+
135
+ def test_extra_spaces_and_presentation_margins_can_be_cleaned(tmp_path):
136
+ reference = "Keep one separator here.\n"
137
+ current = " Keep one \t separator here.\n"
138
+ (result,) = align_line_breaks(reference, current, width=79)
139
+ assert result["aligned"] == reference
140
+ old, new = tmp_path / "old.md", tmp_path / "new.md"
141
+ old.write_text(reference)
142
+ new.write_text(current)
143
+ run([str(old), str(new)])
144
+ assert new.read_text() == reference
145
+ new.write_text(reference.rstrip("\n") + " \n")
146
+ autofix_markdown_file(new, wrapnumber=79)
147
+ assert new.read_text() == reference
148
+
149
+
150
+ def test_cpb_preserves_rendered_spacing_while_restoring_source_breaks(
151
+ tmp_path,
152
+ ):
153
+ import subprocess
154
+
155
+ reference = (
156
+ "A probe inside small RMs\ndispersed in dodecane "
157
+ "($32.9;\\text{kJ}/\\text{mol}$ : @fig:cycle, blue),\nwhich\n"
158
+ )
159
+ current = reference.replace("RMs\ndispersed", "RMs dispersed").replace(
160
+ "$ :", "$:"
161
+ )
162
+ path = tmp_path / "source.md"
163
+ subprocess.run(["git", "init", str(tmp_path)], check=True,
164
+ capture_output=True)
165
+ path.write_text(reference)
166
+ subprocess.run(["git", "-C", str(tmp_path), "add", "source.md"],
167
+ check=True, capture_output=True)
168
+ path.write_text(current)
169
+ autofix_markdown_file(path, wrapnumber=1000, git_index=True)
170
+ assert path.read_text() == reference.replace("$ :", "$:")
171
+
172
+
50
173
  @pytest.mark.parametrize("old_prefix,new_prefix", [
51
174
  ("", " "), (" ", ""), (" ", " "),
52
175
  ])
@@ -80,6 +203,10 @@ def test_leading_indentation_edits_do_not_overlap_whitespace_restoration():
80
203
  ("> Quote.\n", " > Quote.\n"),
81
204
  ("> First.\n continuation.\n", "> First.\n continuation.\n"),
82
205
  ("```\n Code.\n```\n", "```\n Code.\n```\n"),
206
+ ("Introduction.\n\n Indented paragraph.\n",
207
+ "Introduction.\n\n Indented paragraph.\n"),
208
+ ("Introduction.\n\n Indented paragraph.\n",
209
+ "Introduction.\n\n Indented paragraph.\n"),
83
210
  ])
84
211
  def test_structural_indentation_is_preserved(reference, current):
85
212
  assert all(
@@ -177,6 +304,63 @@ def test_comment_alignment_preserves_reference_whitespace():
177
304
  assert restored == reference
178
305
 
179
306
 
307
+ @pytest.mark.parametrize("ending", ["\n", "\r\n"])
308
+ @pytest.mark.parametrize("body", [
309
+ r"$$R_c \approx (100\,\text{nm·K})/\Delta T$$",
310
+ r"$$R_c \approx 100/28 \approx 3.6\,\text{nm}$$",
311
+ "Attached(punctuation),without spaces.",
312
+ ])
313
+ def test_hidden_source_layout_matches_across_attached_punctuation(
314
+ tmp_path, body, ending,
315
+ ):
316
+ import subprocess
317
+
318
+ reference = (
319
+ "Visible prose.\n<!--\nRS: Calculation:\n" + body + "\n-->\n"
320
+ "Visible $x$ (reference): preserve this space.\n"
321
+ ).replace("\n", ending)
322
+ changed_body = body.replace("$$", "$$\n", 1)
323
+ if body.endswith("$$"):
324
+ changed_body = changed_body[:-2] + "\n$$"
325
+ else:
326
+ changed_body = body.replace("(", " ( ").replace(
327
+ "),", " ) ,\n"
328
+ )
329
+ current = reference.replace(body, changed_body.replace("\n", ending))
330
+ restored = current
331
+ for result in reversed(align_line_breaks(reference, current, width=79)):
332
+ restored = (
333
+ restored[:result["start"]] + result["aligned"]
334
+ + restored[result["stop"]:]
335
+ )
336
+ assert restored == reference
337
+ old, new = tmp_path / "old.md", tmp_path / "new.md"
338
+ old.write_bytes(reference.encode())
339
+ new.write_bytes(current.encode())
340
+ run([str(old), str(new)])
341
+ # wmatch reads text with universal newline handling.
342
+ assert new.read_text() == reference.replace(ending, "\n")
343
+ subprocess.run(["git", "init", str(tmp_path)], check=True,
344
+ capture_output=True)
345
+ new.write_bytes(reference.encode())
346
+ subprocess.run(["git", "-C", str(tmp_path), "add", "new.md"],
347
+ check=True, capture_output=True)
348
+ subprocess.run(["git", "-C", str(tmp_path), "config", "user.name",
349
+ "Test"], check=True)
350
+ subprocess.run(["git", "-C", str(tmp_path), "config", "user.email",
351
+ "test@example.com"], check=True)
352
+ subprocess.run(["git", "-C", str(tmp_path), "commit", "-m", "Base"],
353
+ check=True, capture_output=True)
354
+ subprocess.run(["git", "-C", str(tmp_path), "tag", "jf_last"],
355
+ check=True)
356
+ new.write_bytes(current.encode())
357
+ autofix_markdown_file(new, wrapnumber=79, git_ref="jf_last")
358
+ assert new.read_bytes() == reference.encode()
359
+ assert not autofix_markdown_file(
360
+ new, wrapnumber=79, git_ref="jf_last",
361
+ )["fixes"]
362
+
363
+
180
364
  def test_literal_comment_delimiters_remain_opaque():
181
365
  delimiter = "`"
182
366
  reference = f"Use {delimiter}<!-- hidden words -->{delimiter} here.\n"
@@ -203,8 +387,6 @@ def test_literal_comment_delimiters_remain_opaque():
203
387
  "$$\n" + r"r(w_0) = \begin{cases}" + "\n"
204
388
  + r"m w_0 + \delta & w_0 < 20 \\[8pt]" + "\n"
205
389
  + r"c & w_0 \geq 20\end{cases}" + "\n$$\n"),
206
- ("Use $<!-- hidden words -->$ here.\n",
207
- "Use $<!--hidden words-->$ here.\n"),
208
390
  ])
209
391
  def test_math_layout_restores_baseline_without_prose_lint(reference, current):
210
392
  for result in reversed(align_line_breaks(reference, current, width=79)):
@@ -307,13 +489,17 @@ def test_batched_word_diff_keeps_hunks_isolated(monkeypatch):
307
489
  "Other introduction\nsame reference words\nother closing.\n"
308
490
  )
309
491
  current = (
310
- "Changed introduction same reference words changed closing.\n"
492
+ "Changed words in introduction same reference words "
493
+ "changed words in closing.\n"
311
494
  "\nUnchanged anchor.\n\n"
312
- "Updated introduction same reference words updated closing.\n"
495
+ "Updated words in introduction same reference words "
496
+ "updated words in closing.\n"
313
497
  )
314
498
  expected = [
315
- "Changed introduction\nsame reference words\nchanged closing.\n",
316
- "Updated introduction\nsame reference words\nupdated closing.\n",
499
+ "Changed words in introduction\nsame reference words\n"
500
+ "changed words in closing.\n",
501
+ "Updated words in introduction\nsame reference words\n"
502
+ "updated words in closing.\n",
317
503
  ]
318
504
  calls = []
319
505
  original_run = line_alignment.subprocess.run
@@ -334,10 +520,26 @@ def test_optional_preservation_break_does_not_strand_a_tiny_fragment():
334
520
  )
335
521
  current = "X stable reference words rewritten closing.\n"
336
522
  (result,) = align_line_breaks(reference, current)
337
- # Reference whitespace is restored after "words", but separating "X"
338
- # just to preserve the next reference line would make a tiny new row.
339
- assert result["aligned"] == (
340
- "X stable reference words\nrewritten closing.\n"
523
+ # Restoring either boundary would strand a short changed fragment.
524
+ assert result["aligned"] == current
525
+
526
+
527
+ @pytest.mark.parametrize("count", [1, 2, 3, 4])
528
+ @pytest.mark.parametrize("side", ["before", "after"])
529
+ def test_optional_break_uses_word_count_for_long_fragments(count, side):
530
+ reference = "Former opening\nstable reference words\nformer closing.\n"
531
+ fragment = " ".join(["substantial"] * count)
532
+ current = (
533
+ fragment + " stable reference words with a rewritten closing.\n"
534
+ if side == "before" else
535
+ "Here is a rewritten opening stable reference words " + fragment
536
+ + ".\n"
537
+ )
538
+ (result,) = align_line_breaks(reference, current)
539
+ # Four words justify an optional break to preserve a complete old line.
540
+ # One to three long words must stay with the adjacent reference words.
541
+ assert ("\nstable reference words\n" in result["aligned"]) == (
542
+ count > 3
341
543
  )
342
544
 
343
545
 
@@ -754,6 +754,42 @@ def test_stray_line_break_in_an_unchanged_line_is_rejoined(tmp_path):
754
754
  assert "put those breaks back" in report["fixes"][0]["reason"]
755
755
 
756
756
 
757
+ @pytest.mark.parametrize("count", [3, 4, 5])
758
+ @pytest.mark.parametrize("word", ["a", "substantial"])
759
+ @pytest.mark.parametrize("command", ["cpb", "wmatch"])
760
+ def test_diff_wrap_allows_only_four_words_beyond_width(
761
+ tmp_path, count, word, command,
762
+ ):
763
+ from pydifftools.match_spaces import run
764
+
765
+ first = "Stable reference words fill the first line"
766
+ width = len(first)
767
+ reference = first + "\nand the reference sentence finishes here.\n"
768
+ path = committed_source(tmp_path, reference)
769
+ current = reference.replace(
770
+ first, first.replace("Stable", "Edited") + " " + " ".join(
771
+ [word] * count
772
+ ),
773
+ )
774
+ path.write_text(current)
775
+ old = tmp_path / "reference.md"
776
+ old.write_text(reference)
777
+ for _ in range(2):
778
+ if command == "cpb":
779
+ autofix_markdown_file(
780
+ path, wrapnumber=width, punctuation_slop=0, git_head=True,
781
+ )
782
+ else:
783
+ run([str(old), str(path)], width, 0)
784
+ result = path.read_text()
785
+ assert result.split() == current.split()
786
+ if count <= 4:
787
+ assert result == current
788
+ else:
789
+ assert result != current
790
+ assert result.splitlines()[0] == first.replace("Stable", "Edited")
791
+
792
+
757
793
  def test_one_moved_line_break_is_moved_back(tmp_path):
758
794
  path = committed_source(tmp_path, HEAD_PARAGRAPH)
759
795
  path.write_text(
@@ -843,7 +879,7 @@ def test_rm_esr_restores_full_head_lines_in_rewritten_sentence(
843
879
 
844
880
  expected = edited.replace(
845
881
  "the ESR measurements presented",
846
- "the\nESR measurements\npresented",
882
+ "the ESR measurements\npresented",
847
883
  ).replace("tube (1.50", "tube\n(1.50")
848
884
  assert path.read_text() == expected
849
885
  assert report["fixes"] and report["layout"] == []
@@ -855,25 +891,21 @@ def test_rm_esr_restores_full_head_lines_in_rewritten_sentence(
855
891
  matched.write_text(edited)
856
892
  run([str(reference), str(matched)])
857
893
  assert matched.read_text() == expected
858
- # Automatic alignment preserves both unchanged HEAD lines.
859
- assert numstat(path) == (8, 8)
894
+ # Avoid a new two-word row just to preserve the ESR reference line.
895
+ assert numstat(path) == (8, 9)
860
896
 
861
897
 
862
- def test_embedded_head_line_restores_both_boundaries_together(tmp_path):
898
+ def test_embedded_head_line_does_not_strand_short_changed_lines(tmp_path):
863
899
  path = committed_source(tmp_path, "Before\nESR measurements\nAfter.\n")
864
900
  edited = "Changed before ESR measurements changed after.\n"
865
901
  path.write_text(edited)
866
902
 
867
903
  report = autofix_markdown_file(path, wrapnumber=79, git_head=True)
868
904
 
869
- expected = (
870
- "Changed before\nESR measurements\nchanged after."
871
- )
905
+ expected = edited.rstrip()
872
906
  assert path.read_text() == expected + "\n"
873
907
  assert report["layout"] == []
874
- (fix,) = report["fixes"]
875
- assert fix["before"] == edited.rstrip()
876
- assert fix["after"] == expected
908
+ assert report["fixes"] == []
877
909
 
878
910
 
879
911
  @pytest.mark.parametrize("count", [1, 5, 30])
@@ -886,13 +918,13 @@ def test_cpb_and_wmatch_restore_scattered_reference_lines(tmp_path, count):
886
918
  for number in range(count)
887
919
  ) + "\n"
888
920
  current = "\n\n".join(
889
- f"Changed intro {number} Stable component {number} "
890
- f"changed outro {number}."
921
+ f"Changed opening intro {number} Stable component {number} "
922
+ f"changed closing outro {number}."
891
923
  for number in range(count)
892
924
  ) + "\n"
893
925
  expected = "\n\n".join(
894
- f"Changed intro {number}\nStable component {number}\n"
895
- f"changed outro {number}."
926
+ f"Changed opening intro {number}\nStable component {number}\n"
927
+ f"changed closing outro {number}."
896
928
  for number in range(count)
897
929
  ) + "\n"
898
930
  path = committed_source(tmp_path, head)
@@ -1288,14 +1320,16 @@ def test_qt_diff_fixes_fill_pages_with_double_line_breaks(tmp_path):
1288
1320
  for j in range(count)
1289
1321
  ) + "\n"
1290
1322
  current = "\n\n".join(
1291
- f"Changed intro {j} Stable component {j} changed outro {j}."
1323
+ f"Changed opening intro {j} Stable component {j} "
1324
+ f"changed closing outro {j}."
1292
1325
  for j in range(count)
1293
1326
  ) + "\n"
1294
1327
  path = committed_source(tmp_path, head)
1295
1328
  path.write_text(current)
1296
1329
  report = autofix_markdown_file(path, wrapnumber=79, git_index=True)
1297
1330
  expected = "\n\n".join(
1298
- f"Changed intro {j}\nStable component {j}\nchanged outro {j}."
1331
+ f"Changed opening intro {j}\nStable component {j}\n"
1332
+ f"changed closing outro {j}."
1299
1333
  for j in range(count)
1300
1334
  ) + "\n"
1301
1335
  # Every hunk must already be applied before the notice opens.
@@ -247,7 +247,9 @@ def test_wmatch_reuses_wr_rules_and_preserves_other_lines(tmp_path, operation):
247
247
  )
248
248
  new.write_text(changed + "\n" + rest)
249
249
  run([str(old), str(new)])
250
- expected = wrap_prose(changed + "\n", 40) + rest
250
+ expected = wrap_prose(changed + "\n", 40).replace(
251
+ "has enough\nwords\n", "has enough words\n",
252
+ ) + rest
251
253
  assert new.read_text() == expected
252
254
  run([str(old), str(new)])
253
255
  assert new.read_text() == expected
@@ -304,7 +306,11 @@ def test_wmatch_fallback_width_and_long_word(tmp_path):
304
306
  text = "Start " + "extra words " * 20 + long_word + " end.\n"
305
307
  new.write_text(text)
306
308
  run([str(old), str(new)])
307
- assert new.read_text() == wrap_prose(text, 80)
309
+ expected = wrap_prose(text, 79).replace(
310
+ "extra words\nextra words\n" + long_word + "\nend.\n",
311
+ "extra words extra words " + long_word + " end.\n",
312
+ )
313
+ assert new.read_text() == expected
308
314
  assert long_word in new.read_text()
309
315
 
310
316
 
File without changes
File without changes
File without changes