pyDiffTools 0.1.37__tar.gz → 0.1.39__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 (114) hide show
  1. {pydifftools-0.1.37/pyDiffTools.egg-info → pydifftools-0.1.39}/PKG-INFO +1 -1
  2. {pydifftools-0.1.37 → pydifftools-0.1.39/pyDiffTools.egg-info}/PKG-INFO +1 -1
  3. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/comment_migration.py +11 -7
  4. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/wrap_sentences.py +63 -38
  5. {pydifftools-0.1.37 → pydifftools-0.1.39}/pyproject.toml +1 -1
  6. {pydifftools-0.1.37 → pydifftools-0.1.39}/tests/test_comment_migration.py +11 -2
  7. {pydifftools-0.1.37 → pydifftools-0.1.39}/tests/test_wrap_check.py +55 -0
  8. {pydifftools-0.1.37 → pydifftools-0.1.39}/LICENSE.md +0 -0
  9. {pydifftools-0.1.37 → pydifftools-0.1.39}/MANIFEST.in +0 -0
  10. {pydifftools-0.1.37 → pydifftools-0.1.39}/README.rst +0 -0
  11. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/_quarto.yml +0 -0
  12. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  13. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  14. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  15. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  16. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  17. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  18. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  19. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  20. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  21. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  22. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  23. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  24. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  25. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  26. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  27. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  28. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  29. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  30. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  31. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  32. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  33. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  34. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/example.qmd +0 -0
  35. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/example.tex +0 -0
  36. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/index.qmd +0 -0
  37. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  38. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  39. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  40. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  41. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/subproject1/independent.qmd +0 -0
  42. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/subproject1/index.qmd +0 -0
  43. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  44. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  45. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  46. {pydifftools-0.1.37 → pydifftools-0.1.39}/example_notebook/project1/test_include.qmd +0 -0
  47. {pydifftools-0.1.37 → pydifftools-0.1.39}/pyDiffTools.egg-info/SOURCES.txt +0 -0
  48. {pydifftools-0.1.37 → pydifftools-0.1.39}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  49. {pydifftools-0.1.37 → pydifftools-0.1.39}/pyDiffTools.egg-info/entry_points.txt +0 -0
  50. {pydifftools-0.1.37 → pydifftools-0.1.39}/pyDiffTools.egg-info/requires.txt +0 -0
  51. {pydifftools-0.1.37 → pydifftools-0.1.39}/pyDiffTools.egg-info/top_level.txt +0 -0
  52. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/__init__.py +0 -0
  53. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/bibliography.py +0 -0
  54. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/browser_lifecycle.py +0 -0
  55. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/check_numbers.py +0 -0
  56. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/citation_dialog.py +0 -0
  57. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/command_line.py +0 -0
  58. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/command_registry.py +0 -0
  59. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/comment_functions.py +0 -0
  60. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/comment_tags.lua +0 -0
  61. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/comment_tags_margin.lua +0 -0
  62. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/comment_tags_no_comments.lua +0 -0
  63. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/comment_toggle.js +0 -0
  64. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/comments.css +0 -0
  65. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/continuous.py +0 -0
  66. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/copy_files.py +0 -0
  67. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/diff-doc.js +0 -0
  68. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/doc_contents.py +0 -0
  69. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/flowchart/__init__.py +0 -0
  70. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/flowchart/comparison.py +0 -0
  71. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  72. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/flowchart/graph.py +0 -0
  73. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  74. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/flowchart/watch_graph.py +0 -0
  75. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/flowchart/wgrph_view.js +0 -0
  76. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/forward_search.py +0 -0
  77. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/git_gd.py +0 -0
  78. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/git_gd_history.py +0 -0
  79. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/git_gd_image.py +0 -0
  80. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/git_gd_qt.py +0 -0
  81. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/html_comments.py +0 -0
  82. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/html_uncomments.py +0 -0
  83. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/log_example.py +0 -0
  84. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/match_spaces.py +0 -0
  85. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/notebook/__init__.py +0 -0
  86. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/notebook/display.py +0 -0
  87. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/notebook/fast_build.py +0 -0
  88. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/notebook/tex_to_qmd.py +0 -0
  89. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/onewordify.py +0 -0
  90. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/onewordify_undo.py +0 -0
  91. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/outline.py +0 -0
  92. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/rearrange_tex.py +0 -0
  93. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/searchacro.py +0 -0
  94. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/separate_comments.py +0 -0
  95. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/split_conflict.py +0 -0
  96. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/unseparate_comments.py +0 -0
  97. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/update_check.py +0 -0
  98. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/xml2xlsx.vbs +0 -0
  99. {pydifftools-0.1.37 → pydifftools-0.1.39}/pydifftools/zotero.py +0 -0
  100. {pydifftools-0.1.37 → pydifftools-0.1.39}/setup.cfg +0 -0
  101. {pydifftools-0.1.37 → pydifftools-0.1.39}/tests/test_bibliography.py +0 -0
  102. {pydifftools-0.1.37 → pydifftools-0.1.39}/tests/test_browser_lifecycle.py +0 -0
  103. {pydifftools-0.1.37 → pydifftools-0.1.39}/tests/test_check_single_use.py +0 -0
  104. {pydifftools-0.1.37 → pydifftools-0.1.39}/tests/test_command_line_help.py +0 -0
  105. {pydifftools-0.1.37 → pydifftools-0.1.39}/tests/test_continuous_shutdown.py +0 -0
  106. {pydifftools-0.1.37 → pydifftools-0.1.39}/tests/test_forward_search.py +0 -0
  107. {pydifftools-0.1.37 → pydifftools-0.1.39}/tests/test_git_gd_history.py +0 -0
  108. {pydifftools-0.1.37 → pydifftools-0.1.39}/tests/test_git_gd_image.py +0 -0
  109. {pydifftools-0.1.37 → pydifftools-0.1.39}/tests/test_git_gd_qt.py +0 -0
  110. {pydifftools-0.1.37 → pydifftools-0.1.39}/tests/test_rrng.py +0 -0
  111. {pydifftools-0.1.37 → pydifftools-0.1.39}/tests/test_tex_to_qmd.py +0 -0
  112. {pydifftools-0.1.37 → pydifftools-0.1.39}/tests/test_update_check.py +0 -0
  113. {pydifftools-0.1.37 → pydifftools-0.1.39}/tests/test_wrapping.py +0 -0
  114. {pydifftools-0.1.37 → pydifftools-0.1.39}/tests/test_zotero.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.37
3
+ Version: 0.1.39
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.37
3
+ Version: 0.1.39
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -78,7 +78,7 @@ def _most_distant_hue(colors):
78
78
  return (start + gap_size / 2) % 360
79
79
 
80
80
 
81
- def _migration_dialog(text, colors, legacy):
81
+ def _migration_dialog(filename, colors, legacy):
82
82
  """Ask Qt for all migrations and return decisions without editing files."""
83
83
  script = r"""
84
84
  import colorsys
@@ -95,6 +95,8 @@ from PySide6.QtWidgets import (
95
95
  )
96
96
 
97
97
  payload = json.loads(sys.argv[1])
98
+ with open(sys.argv[2], encoding="utf-8", newline="") as fp:
99
+ text = fp.read()
98
100
  colors = payload["colors"]
99
101
  app = QApplication(sys.argv[:1])
100
102
  result = {"accepted": True, "replacements": {}, "colors": {}}
@@ -201,9 +203,9 @@ legacy = payload["legacy"]
201
203
  if legacy:
202
204
  initials = ask_legacy_initials()
203
205
  legacy_occurrences = len(re.findall(
204
- r"</?comment(?:-(?:left|right))?>", payload["text"], re.I
206
+ r"</?comment(?:-(?:left|right))?>", text, re.I
205
207
  )) + len(re.findall(
206
- r"\.comment-(?:left|right)(?=[\s}])", payload["text"], re.I
208
+ r"\.comment-(?:left|right)(?=[\s}])", text, re.I
207
209
  ))
208
210
  if initials is None:
209
211
  result["accepted"] = False
@@ -222,12 +224,12 @@ if legacy:
222
224
  if result["accepted"]:
223
225
  authors = sorted(set(
224
226
  initials.upper() for initials in re.findall(
225
- r"</?([A-Za-z]{2})com(?:-(?:left|right))?>", payload["text"], re.I
227
+ r"</?([A-Za-z]{2})com(?:-(?:left|right))?>", text, re.I
226
228
  )
227
229
  ) | set(
228
230
  initials.upper() for initials in re.findall(
229
231
  r"\.([A-Za-z]{2})com(?:-(?:left|right))?(?=[\s}])",
230
- payload["text"], re.I
232
+ text, re.I
231
233
  )
232
234
  ))
233
235
  if result["replacements"].get("legacy"):
@@ -279,8 +281,10 @@ if result["accepted"]:
279
281
  print(json.dumps(result))
280
282
  """
281
283
  completed = subprocess.run(
284
+ # the dialog reads the document from its file, since the text of a
285
+ # large document exceeds the operating system's argument limit
282
286
  [sys.executable, "-c", textwrap.dedent(script),
283
- json.dumps({"text": text, "colors": colors, "legacy": legacy})],
287
+ json.dumps({"colors": colors, "legacy": legacy}), filename],
284
288
  capture_output=True, text=True,
285
289
  )
286
290
  if completed.returncode:
@@ -319,7 +323,7 @@ def prepare_comment_source(filename):
319
323
  }
320
324
  if not has_legacy and authors.issubset(colors):
321
325
  return original
322
- decisions = _migration_dialog(original, colors, has_legacy)
326
+ decisions = _migration_dialog(filename, colors, has_legacy)
323
327
  if not decisions.get("accepted"):
324
328
  raise RuntimeError("cpb comment migration was canceled")
325
329
  updated = original
@@ -942,7 +942,9 @@ def _iter_classified_spans(text, filetype, line_start=1):
942
942
  leaf prose blocks -- both just need the same protected-region
943
943
  exclusions wr itself uses.
944
944
  """
945
- classified = classify_lines(text, filetype, normalize_math=True)
945
+ # normalizing math can add lines (e.g. splitting "$${#eq:x}"), which
946
+ # would shift every reported line number after it
947
+ classified = classify_lines(text, filetype)
946
948
  line = line_start
947
949
  for wrappable, group in itertools.groupby(classified, key=lambda x: x[0]):
948
950
  content = "".join(l for _, l in group)
@@ -1122,19 +1124,24 @@ def apply_markdown_issue_fix(content, line_number, message):
1122
1124
  prefix = re.match(r"^[ \t]*", old_line).group()
1123
1125
  # }}}
1124
1126
  if message.startswith("line too long:"):
1125
- match = re.search(r"before '([^']+)'", message)
1127
+ match = re.search(r"after '(.+?)' \(before '(.+?)'\); move", message)
1126
1128
  if match is None:
1127
1129
  raise ValueError(
1128
1130
  f"Cannot find the suggested word in {message!r}"
1129
1131
  )
1130
- word = re.escape(match.group(1))
1131
- position = re.search(r"(?<!\S)" + word + r"(?!\S)", old_line)
1132
+ # split at the suggested word that follows wr's break word, since the
1133
+ # same word can appear earlier on the line (e.g. "for ... ($3x$ for")
1134
+ position = re.search(
1135
+ r"(?<!\S)" + re.escape(match.group(1)) + r"[ \t]+("
1136
+ + re.escape(match.group(2)) + r")(?!\S)",
1137
+ old_line,
1138
+ )
1132
1139
  if position is None:
1133
1140
  raise ValueError(
1134
- f"Cannot find {match.group(1)!r} on Markdown line "
1135
- f"{line_number}"
1141
+ f"Cannot find {match.group(2)!r} after {match.group(1)!r} on "
1142
+ f"Markdown line {line_number}"
1136
1143
  )
1137
- split_at = position.start()
1144
+ split_at = position.start(1)
1138
1145
  first = old_line[:split_at].rstrip()
1139
1146
  second = old_line[split_at:].lstrip(" \t")
1140
1147
  reason = (
@@ -1157,9 +1164,22 @@ def apply_markdown_issue_fix(content, line_number, message):
1157
1164
  else:
1158
1165
  raise ValueError(f"No automatic fix is available for {message!r}")
1159
1166
 
1160
- fixed_line = first + ending + prefix + second + ending
1161
- lines[index] = fixed_line
1162
- after = first + "\n" + prefix + second
1167
+ if not first.strip():
1168
+ # a break here would insert a blank line, i.e. a paragraph break
1169
+ raise ValueError(
1170
+ f"Automatic fix would leave an empty line at {line_number}"
1171
+ )
1172
+ if second.strip():
1173
+ lines[index] = first + ending + prefix + second + ending
1174
+ after = first + "\n" + prefix + second
1175
+ else:
1176
+ # only trailing whitespace followed the break, so drop it rather
1177
+ # than adding a blank line
1178
+ lines[index] = first + ending
1179
+ after = first
1180
+ reason = (
1181
+ "A sentence ended with a trailing space. I removed the space."
1182
+ )
1163
1183
  return "".join(lines), old_line, after, reason
1164
1184
 
1165
1185
 
@@ -1168,39 +1188,44 @@ def autofix_markdown_file(filename, wrapnumber=55, punctuation_slop=20):
1168
1188
  with open(filename, encoding="utf-8", newline="") as fp:
1169
1189
  content = fp.read()
1170
1190
  fixes = []
1171
- for _ in range(1000):
1191
+ # Lint once, fix every flagged line, then lint again, since fixing one
1192
+ # issue at a time re-lints the whole file for every single fix.
1193
+ for _ in range(100):
1172
1194
  issues = markdown_lint_issues_from_text(
1173
1195
  content, wrapnumber=wrapnumber, punctuation_slop=punctuation_slop
1174
1196
  )
1175
- fixable = next(
1176
- (
1177
- issue
1178
- for issue in issues
1179
- if not issue[1].startswith(
1180
- ("unclosed math:", "unclosed HTML comment:")
1181
- )
1182
- ),
1183
- None,
1184
- )
1185
- if fixable is None:
1197
+ fixable = {}
1198
+ for line_number, message in issues:
1199
+ if not message.startswith(
1200
+ ("unclosed math:", "unclosed HTML comment:")
1201
+ ):
1202
+ # a fix only rewrites its own line, so fix one issue per line
1203
+ # per pass and let the next lint catch whatever remains
1204
+ fixable.setdefault(line_number, message)
1205
+ if not fixable:
1186
1206
  break
1187
- line_number, message = fixable
1188
- updated, before, after, reason = apply_markdown_issue_fix(
1189
- content, line_number, message
1190
- )
1191
- if updated == content:
1192
- raise RuntimeError(
1193
- f"Automatic Markdown fix made no change at line {line_number}"
1207
+ # work from the bottom up so each inserted line break leaves the
1208
+ # line numbers of the remaining issues unchanged
1209
+ pass_fixes = []
1210
+ for line_number in sorted(fixable, reverse=True):
1211
+ updated, before, after, reason = apply_markdown_issue_fix(
1212
+ content, line_number, fixable[line_number]
1194
1213
  )
1195
- fixes.append(
1196
- {
1197
- "line": line_number,
1198
- "reason": reason,
1199
- "before": before,
1200
- "after": after,
1201
- }
1202
- )
1203
- content = updated
1214
+ if updated == content:
1215
+ raise RuntimeError(
1216
+ "Automatic Markdown fix made no change at line "
1217
+ f"{line_number}"
1218
+ )
1219
+ pass_fixes.append(
1220
+ {
1221
+ "line": line_number,
1222
+ "reason": reason,
1223
+ "before": before,
1224
+ "after": after,
1225
+ }
1226
+ )
1227
+ content = updated
1228
+ fixes.extend(reversed(pass_fixes))
1204
1229
  else:
1205
1230
  raise RuntimeError("Too many automatic Markdown fixes were needed.")
1206
1231
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyDiffTools"
7
- version = "0.1.37"
7
+ version = "0.1.39"
8
8
  authors = [{ name = "J M Franck" }]
9
9
  description = "Diff tools"
10
10
  readme = "README.rst"
@@ -13,11 +13,14 @@ def test_hue_picker_starts_at_seed_and_fills_largest_hue_gap():
13
13
  assert _most_distant_hue(["#ff0000", "#00ff00"]) == pytest.approx(240)
14
14
 
15
15
 
16
- def test_migration_dialog_builds_qt_script_and_decodes_result(monkeypatch):
16
+ def test_migration_dialog_builds_qt_script_and_decodes_result(
17
+ tmp_path, monkeypatch
18
+ ):
17
19
  captured = {}
18
20
 
19
21
  def fake_run(command, **_kwargs):
20
22
  captured["script"] = command[2]
23
+ captured["command"] = command
21
24
  compile(command[2], "<comment migration dialog>", "exec")
22
25
  return type(
23
26
  "Completed",
@@ -27,11 +30,17 @@ def test_migration_dialog_builds_qt_script_and_decodes_result(monkeypatch):
27
30
 
28
31
  monkeypatch.setattr(comment_migration.subprocess, "run", fake_run)
29
32
 
30
- result = _migration_dialog("<ABcom>note</ABcom>", {}, False)
33
+ path = tmp_path / "notes.md"
34
+ path.write_text("<ABcom>note</ABcom>", encoding="utf-8")
35
+ result = _migration_dialog(str(path), {}, False)
31
36
 
32
37
  assert result == {"accepted": True}
33
38
  assert "QColor.fromHsv" in captured["script"]
34
39
  assert "QComboBox" in captured["script"]
40
+ # the document is passed by filename, since large files exceed the argv
41
+ # limit
42
+ assert captured["command"][4] == str(path)
43
+ assert "<ABcom>" not in captured["command"][3]
35
44
 
36
45
 
37
46
  def test_prepare_migrates_legacy_tags_and_div_classes(tmp_path, monkeypatch):
@@ -120,6 +120,61 @@ def test_one_reported_long_line_is_fixed_at_the_suggested_word():
120
120
  assert "hard to read" in reason
121
121
 
122
122
 
123
+ def test_lint_line_numbers_survive_labeled_display_math():
124
+ # normalizing "$${#eq:x}" for wrapping splits it onto two lines, which
125
+ # must not shift the line numbers reported for later prose
126
+ source = (
127
+ "Intro.\n$$\nx = y\n$${#eq:x}\nwhere this sentence ends. "
128
+ "Another starts.\n"
129
+ )
130
+
131
+ ((line, message),) = markdown_lint_issues_from_text(source, 55)
132
+
133
+ assert line == 5
134
+ assert message.startswith("sentence ends mid-line")
135
+
136
+
137
+ def test_long_line_fix_splits_at_the_word_after_the_break():
138
+ # "for" also starts the line, but wr breaks before the second one
139
+ source = (
140
+ "for $30\\;\\text{s}$ ($3\\times$ for AOT) with some more words "
141
+ "added.\n"
142
+ )
143
+ message = (
144
+ "line too long: wr would break after '($3\\times$' (before 'for'); "
145
+ "move 'for' onward to the next line, or shorten this sentence"
146
+ )
147
+
148
+ fixed, _, _, _ = apply_markdown_issue_fix(source, 1, message)
149
+
150
+ assert fixed.startswith("for $30\\;\\text{s}$ ($3\\times$\nfor AOT)")
151
+
152
+
153
+ def test_trailing_space_after_sentence_is_removed_not_a_blank_line(tmp_path):
154
+ path = tmp_path / "source.md"
155
+ path.write_text("It melts near 238&nbsp;K. \nIn contrast, it stays.\n")
156
+
157
+ autofix_markdown_file(path)
158
+
159
+ fixed = path.read_text()
160
+ assert fixed.startswith("It melts near 238&nbsp;K.\nIn contrast,")
161
+ assert "\n\n" not in fixed
162
+
163
+
164
+ def test_autofix_fixes_every_line_in_one_pass_from_the_end(tmp_path):
165
+ path = tmp_path / "source.md"
166
+ paragraph = "First sentence ends here. A second sentence begins.\n"
167
+ path.write_text("\n".join([paragraph] * 30))
168
+
169
+ report = autofix_markdown_file(path)
170
+
171
+ fixed = path.read_text()
172
+ assert len(report["fixes"]) == 30
173
+ assert [fix["line"] for fix in report["fixes"]] == list(range(1, 60, 2))
174
+ assert fixed.count("\n\n") == 29
175
+ assert markdown_lint_issues_from_text(fixed, wrapnumber=55) == []
176
+
177
+
123
178
  def test_unclosed_math_is_reported_after_fifty_lines():
124
179
  short_math = "$$\n" + ("x = y\n" * 48)
125
180
  long_math = "$$\n" + ("x = y\n" * 49)
File without changes
File without changes
File without changes
File without changes