pyDiffTools 0.1.53__tar.gz → 0.1.54__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.54}/PKG-INFO +1 -1
  2. {pydifftools-0.1.53 → pydifftools-0.1.54/pyDiffTools.egg-info}/PKG-INFO +1 -1
  3. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/line_alignment.py +50 -3
  4. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/match_spaces.py +0 -1
  5. {pydifftools-0.1.53 → pydifftools-0.1.54}/pyproject.toml +1 -1
  6. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_line_alignment.py +184 -2
  7. {pydifftools-0.1.53 → pydifftools-0.1.54}/LICENSE.md +0 -0
  8. {pydifftools-0.1.53 → pydifftools-0.1.54}/MANIFEST.in +0 -0
  9. {pydifftools-0.1.53 → pydifftools-0.1.54}/README.rst +0 -0
  10. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/_quarto.yml +0 -0
  11. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  12. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  13. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  14. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  15. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  16. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  17. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  18. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  19. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  20. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  21. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  22. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  23. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  24. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  25. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  26. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  27. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  28. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  29. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  30. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  31. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  32. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  33. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/example.qmd +0 -0
  34. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/example.tex +0 -0
  35. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/index.qmd +0 -0
  36. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  37. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  38. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  39. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  40. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/subproject1/independent.qmd +0 -0
  41. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/subproject1/index.qmd +0 -0
  42. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  43. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  44. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  45. {pydifftools-0.1.53 → pydifftools-0.1.54}/example_notebook/project1/test_include.qmd +0 -0
  46. {pydifftools-0.1.53 → pydifftools-0.1.54}/pyDiffTools.egg-info/SOURCES.txt +0 -0
  47. {pydifftools-0.1.53 → pydifftools-0.1.54}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  48. {pydifftools-0.1.53 → pydifftools-0.1.54}/pyDiffTools.egg-info/entry_points.txt +0 -0
  49. {pydifftools-0.1.53 → pydifftools-0.1.54}/pyDiffTools.egg-info/requires.txt +0 -0
  50. {pydifftools-0.1.53 → pydifftools-0.1.54}/pyDiffTools.egg-info/top_level.txt +0 -0
  51. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/__init__.py +0 -0
  52. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/bibliography.py +0 -0
  53. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/browser_lifecycle.py +0 -0
  54. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/check_numbers.py +0 -0
  55. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/citation_dialog.py +0 -0
  56. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/command_line.py +0 -0
  57. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/command_registry.py +0 -0
  58. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/comment_filter_history.json +0 -0
  59. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/comment_functions.py +0 -0
  60. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/comment_migration.py +0 -0
  61. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/comment_tags.lua +0 -0
  62. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/comment_tags_margin.lua +0 -0
  63. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/comment_tags_no_comments.lua +0 -0
  64. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/comment_toggle.js +0 -0
  65. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/comments.css +0 -0
  66. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/comments_author_colors.css +0 -0
  67. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/completions/git-mergein +0 -0
  68. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/continuous.py +0 -0
  69. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/copy_files.py +0 -0
  70. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/diff-doc.js +0 -0
  71. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/doc_contents.py +0 -0
  72. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/flowchart/__init__.py +0 -0
  73. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/flowchart/comparison.py +0 -0
  74. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  75. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/flowchart/graph.py +0 -0
  76. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/flowchart/source_jump.js +0 -0
  77. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  78. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/flowchart/watch_graph.py +0 -0
  79. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/flowchart/wgrph_view.js +0 -0
  80. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/forward_search.py +0 -0
  81. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/git_aliases.py +0 -0
  82. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/git_gd.py +0 -0
  83. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/git_gd_history.py +0 -0
  84. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/git_gd_image.py +0 -0
  85. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/git_gd_qt.py +0 -0
  86. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/html_comments.py +0 -0
  87. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/html_uncomments.py +0 -0
  88. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/log_example.py +0 -0
  89. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/notebook/__init__.py +0 -0
  90. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/notebook/display.py +0 -0
  91. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/notebook/fast_build.py +0 -0
  92. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/notebook/tex_to_qmd.py +0 -0
  93. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/onewordify.py +0 -0
  94. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/onewordify_undo.py +0 -0
  95. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/outline.py +0 -0
  96. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/pdf_diff.py +0 -0
  97. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/rearrange_tex.py +0 -0
  98. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/searchacro.py +0 -0
  99. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/separate_comments.py +0 -0
  100. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/source_jump.py +0 -0
  101. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/split_conflict.py +0 -0
  102. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/unseparate_comments.py +0 -0
  103. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/update_check.py +0 -0
  104. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/wrap_sentences.py +0 -0
  105. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/wrapping_options.py +0 -0
  106. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/xml2xlsx.vbs +0 -0
  107. {pydifftools-0.1.53 → pydifftools-0.1.54}/pydifftools/zotero.py +0 -0
  108. {pydifftools-0.1.53 → pydifftools-0.1.54}/setup.cfg +0 -0
  109. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_bibliography.py +0 -0
  110. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_browser_lifecycle.py +0 -0
  111. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_check_single_use.py +0 -0
  112. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_command_line_help.py +0 -0
  113. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_comment_filters.py +0 -0
  114. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_comment_migration.py +0 -0
  115. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_continuous_shutdown.py +0 -0
  116. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_forward_search.py +0 -0
  117. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_git_gd_history.py +0 -0
  118. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_git_gd_image.py +0 -0
  119. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_git_gd_qt.py +0 -0
  120. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_git_mergein.py +0 -0
  121. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_git_pdf_diff.py +0 -0
  122. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_pdf_diff.py +0 -0
  123. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_rrng.py +0 -0
  124. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_source_jump.py +0 -0
  125. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_tex_to_qmd.py +0 -0
  126. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_update_check.py +0 -0
  127. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_wrap_check.py +0 -0
  128. {pydifftools-0.1.53 → pydifftools-0.1.54}/tests/test_wrapping.py +0 -0
  129. {pydifftools-0.1.53 → pydifftools-0.1.54}/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.54
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.53
3
+ Version: 0.1.54
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -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)
@@ -348,6 +355,39 @@ def align_line_breaks(
348
355
  whitespace = dict(gaps)
349
356
  ending = "\r\n" if "\r\n" in before else "\n"
350
357
 
358
+ def equivalent_gap(number, replacement):
359
+ # {{{ keep rendered separators and explicit Markdown hard breaks
360
+ raw = gaps[number]
361
+ left = start + tokens[number][1]
362
+ right = start + tokens[number + 1][1]
363
+ region = region_at(current_comments, left)
364
+ if region is not None and right < region[1]:
365
+ return True
366
+ region = region_at(current_equations, left)
367
+ if region is not None and right < region[1]:
368
+ # Display delimiters can sit on their own source lines.
369
+ # All other math gaps obey the same rule as prose; no
370
+ # knowledge of TeX commands or text macros is needed.
371
+ if current[region[0]:region[0] + 2] == "$$" and (
372
+ tokens[number][1] + start == region[0]
373
+ or tokens[number + 1][2] + start == region[1]
374
+ ):
375
+ return True
376
+ return bool(raw) == bool(replacement)
377
+ # Collapsing a whitespace run retains one separator. It cannot
378
+ # create a separator from an empty gap or remove the last one.
379
+ # Line margins are handled separately, with structural margins
380
+ # excluded. Two spaces before a newline are a Markdown hard
381
+ # break, and a newline after a backslash is explicit as well.
382
+ if any(re.search(r" {2,}\r?\n", gap)
383
+ for gap in (raw, replacement)) or (
384
+ before[tokens[number][2] - 1:tokens[number][2]] == "\\"
385
+ and ("\n" in raw or "\n" in replacement)
386
+ ):
387
+ return raw == replacement
388
+ return bool(raw) == bool(replacement)
389
+ # }}}
390
+
351
391
  # {{{ restore presentation indentation at matching line starts
352
392
  old_first = {}
353
393
  for number, (row, _, _) in enumerate(old_tokens):
@@ -430,6 +470,8 @@ def align_line_breaks(
430
470
  old_number = candidate
431
471
  if old_number in old_gaps:
432
472
  raw = old_gaps[old_number]
473
+ if not equivalent_gap(number, raw):
474
+ continue
433
475
  region = region_at(regions, start + tokens[number][1])
434
476
  inside_region = (
435
477
  region is not None
@@ -494,6 +536,11 @@ def align_line_breaks(
494
536
  elif "\n" in proposed[number]:
495
537
  proposed[number] = " "
496
538
  proposed_rows = render(proposed).splitlines()
539
+ if any(
540
+ not equivalent_gap(number, proposed[number])
541
+ for number in needed
542
+ ):
543
+ continue
497
544
  if old_line not in {line.rstrip() for line in proposed_rows}:
498
545
  continue
499
546
  if any(
@@ -92,7 +92,6 @@ def match_text(
92
92
  trailing_dependent_phrase=DEFAULT_TRAILING_DEPENDENT_PHRASE,
93
93
  ):
94
94
  """Align reference lines and wrap only overflowing edited prose."""
95
- text2 = text2.replace("\u00a0", " ").replace("\u2004", " ")
96
95
  # {{{ choose the nearby reference width before any line linting
97
96
  reference_lines = classify_lines(text1, filetype)
98
97
  prose_rows = [
@@ -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.54"
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)):
File without changes
File without changes
File without changes
File without changes