pyDiffTools 0.1.41__tar.gz → 0.1.43__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.41/pyDiffTools.egg-info → pydifftools-0.1.43}/PKG-INFO +1 -1
  2. {pydifftools-0.1.41 → pydifftools-0.1.43/pyDiffTools.egg-info}/PKG-INFO +1 -1
  3. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/continuous.py +110 -45
  4. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/wrap_sentences.py +56 -69
  5. {pydifftools-0.1.41 → pydifftools-0.1.43}/pyproject.toml +1 -1
  6. {pydifftools-0.1.41 → pydifftools-0.1.43}/tests/test_wrap_check.py +110 -34
  7. {pydifftools-0.1.41 → pydifftools-0.1.43}/tests/test_wrapping.py +2 -1
  8. {pydifftools-0.1.41 → pydifftools-0.1.43}/LICENSE.md +0 -0
  9. {pydifftools-0.1.41 → pydifftools-0.1.43}/MANIFEST.in +0 -0
  10. {pydifftools-0.1.41 → pydifftools-0.1.43}/README.rst +0 -0
  11. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/_quarto.yml +0 -0
  12. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  13. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  14. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  15. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  16. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  17. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  18. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  19. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  20. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  21. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  22. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  23. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  24. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  25. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  26. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  27. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  28. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  29. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  30. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  31. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  32. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  33. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  34. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/example.qmd +0 -0
  35. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/example.tex +0 -0
  36. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/index.qmd +0 -0
  37. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  38. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  39. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  40. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  41. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/subproject1/independent.qmd +0 -0
  42. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/subproject1/index.qmd +0 -0
  43. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  44. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  45. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  46. {pydifftools-0.1.41 → pydifftools-0.1.43}/example_notebook/project1/test_include.qmd +0 -0
  47. {pydifftools-0.1.41 → pydifftools-0.1.43}/pyDiffTools.egg-info/SOURCES.txt +0 -0
  48. {pydifftools-0.1.41 → pydifftools-0.1.43}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  49. {pydifftools-0.1.41 → pydifftools-0.1.43}/pyDiffTools.egg-info/entry_points.txt +0 -0
  50. {pydifftools-0.1.41 → pydifftools-0.1.43}/pyDiffTools.egg-info/requires.txt +0 -0
  51. {pydifftools-0.1.41 → pydifftools-0.1.43}/pyDiffTools.egg-info/top_level.txt +0 -0
  52. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/__init__.py +0 -0
  53. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/bibliography.py +0 -0
  54. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/browser_lifecycle.py +0 -0
  55. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/check_numbers.py +0 -0
  56. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/citation_dialog.py +0 -0
  57. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/command_line.py +0 -0
  58. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/command_registry.py +0 -0
  59. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/comment_functions.py +0 -0
  60. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/comment_migration.py +0 -0
  61. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/comment_tags.lua +0 -0
  62. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/comment_tags_margin.lua +0 -0
  63. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/comment_tags_no_comments.lua +0 -0
  64. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/comment_toggle.js +0 -0
  65. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/comments.css +0 -0
  66. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/copy_files.py +0 -0
  67. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/diff-doc.js +0 -0
  68. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/doc_contents.py +0 -0
  69. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/flowchart/__init__.py +0 -0
  70. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/flowchart/comparison.py +0 -0
  71. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  72. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/flowchart/graph.py +0 -0
  73. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  74. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/flowchart/watch_graph.py +0 -0
  75. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/flowchart/wgrph_view.js +0 -0
  76. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/forward_search.py +0 -0
  77. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/git_gd.py +0 -0
  78. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/git_gd_history.py +0 -0
  79. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/git_gd_image.py +0 -0
  80. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/git_gd_qt.py +0 -0
  81. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/html_comments.py +0 -0
  82. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/html_uncomments.py +0 -0
  83. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/log_example.py +0 -0
  84. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/match_spaces.py +0 -0
  85. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/notebook/__init__.py +0 -0
  86. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/notebook/display.py +0 -0
  87. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/notebook/fast_build.py +0 -0
  88. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/notebook/tex_to_qmd.py +0 -0
  89. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/onewordify.py +0 -0
  90. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/onewordify_undo.py +0 -0
  91. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/outline.py +0 -0
  92. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/rearrange_tex.py +0 -0
  93. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/searchacro.py +0 -0
  94. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/separate_comments.py +0 -0
  95. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/split_conflict.py +0 -0
  96. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/unseparate_comments.py +0 -0
  97. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/update_check.py +0 -0
  98. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/xml2xlsx.vbs +0 -0
  99. {pydifftools-0.1.41 → pydifftools-0.1.43}/pydifftools/zotero.py +0 -0
  100. {pydifftools-0.1.41 → pydifftools-0.1.43}/setup.cfg +0 -0
  101. {pydifftools-0.1.41 → pydifftools-0.1.43}/tests/test_bibliography.py +0 -0
  102. {pydifftools-0.1.41 → pydifftools-0.1.43}/tests/test_browser_lifecycle.py +0 -0
  103. {pydifftools-0.1.41 → pydifftools-0.1.43}/tests/test_check_single_use.py +0 -0
  104. {pydifftools-0.1.41 → pydifftools-0.1.43}/tests/test_command_line_help.py +0 -0
  105. {pydifftools-0.1.41 → pydifftools-0.1.43}/tests/test_comment_migration.py +0 -0
  106. {pydifftools-0.1.41 → pydifftools-0.1.43}/tests/test_continuous_shutdown.py +0 -0
  107. {pydifftools-0.1.41 → pydifftools-0.1.43}/tests/test_forward_search.py +0 -0
  108. {pydifftools-0.1.41 → pydifftools-0.1.43}/tests/test_git_gd_history.py +0 -0
  109. {pydifftools-0.1.41 → pydifftools-0.1.43}/tests/test_git_gd_image.py +0 -0
  110. {pydifftools-0.1.41 → pydifftools-0.1.43}/tests/test_git_gd_qt.py +0 -0
  111. {pydifftools-0.1.41 → pydifftools-0.1.43}/tests/test_rrng.py +0 -0
  112. {pydifftools-0.1.41 → pydifftools-0.1.43}/tests/test_tex_to_qmd.py +0 -0
  113. {pydifftools-0.1.41 → pydifftools-0.1.43}/tests/test_update_check.py +0 -0
  114. {pydifftools-0.1.41 → pydifftools-0.1.43}/tests/test_zotero.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.41
3
+ Version: 0.1.43
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.41
3
+ Version: 0.1.43
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -132,7 +132,7 @@ def show_markdown_fix_dialog(report):
132
132
  from html import escape
133
133
 
134
134
  from PySide6.QtCore import Qt
135
- from PySide6.QtGui import QFontDatabase
135
+ from PySide6.QtGui import QFontDatabase, QFontMetrics
136
136
  from PySide6.QtWidgets import (
137
137
  QApplication,
138
138
  QDialog,
@@ -145,14 +145,7 @@ def show_markdown_fix_dialog(report):
145
145
 
146
146
  fixes = report["fixes"]
147
147
  warnings = report["warnings"]
148
- records = [
149
- {"record_type": "fix", **fix}
150
- for fix in fixes
151
- ] + [
152
- {"record_type": "warning", **warning}
153
- for warning in warnings
154
- ]
155
- if not records:
148
+ if not fixes and not warnings:
156
149
  return
157
150
 
158
151
  dialog = QDialog()
@@ -160,7 +153,53 @@ def show_markdown_fix_dialog(report):
160
153
  QApplication.instance() or QApplication([])
161
154
  )
162
155
  dialog.setWindowTitle("Markdown source fixes")
163
- dialog.resize(760, 480)
156
+ fixed_font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
157
+ # {{{ group fixes of one kind onto pages that fill the screen
158
+ screen_height = dialog.screen().availableGeometry().height()
159
+ dialog.resize(900, int(screen_height * 0.85))
160
+ # leave room for the heading, explanation, button and margins
161
+ page_rows = max(
162
+ 3,
163
+ int(
164
+ (screen_height * 0.85 - 120)
165
+ / QFontMetrics(fixed_font).lineSpacing()
166
+ ),
167
+ )
168
+ groups = {}
169
+ for fix in fixes:
170
+ groups.setdefault(fix["reason"], []).append(fix)
171
+ records = []
172
+ for reason, group in groups.items():
173
+ page = []
174
+ rows = 0
175
+ for fix in sorted(group, key=lambda fix: fix["line"]):
176
+ # a blank line separates each fix from the one before it, and a
177
+ # fix that only drops trailing spaces shows just its old line
178
+ fix_rows = (
179
+ len(fix["before"].splitlines() or [""])
180
+ + (
181
+ 0
182
+ if fix["after"] == fix["before"].rstrip()
183
+ else len(fix["after"].splitlines() or [""])
184
+ )
185
+ + (1 if page else 0)
186
+ )
187
+ if page and rows + fix_rows > page_rows:
188
+ records.append(
189
+ {"record_type": "fix", "reason": reason, "fixes": page}
190
+ )
191
+ page = []
192
+ rows = 0
193
+ fix_rows -= 1
194
+ page.append(fix)
195
+ rows += fix_rows
196
+ records.append(
197
+ {"record_type": "fix", "reason": reason, "fixes": page}
198
+ )
199
+ records += [
200
+ {"record_type": "warning", **warning} for warning in warnings
201
+ ]
202
+ # }}}
164
203
  layout = QVBoxLayout(dialog)
165
204
  heading = QLabel()
166
205
  heading.setWordWrap(True)
@@ -175,14 +214,10 @@ def show_markdown_fix_dialog(report):
175
214
  Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight
176
215
  )
177
216
  line_numbers.setStyleSheet("color: #666; padding-right: 8px;")
178
- line_numbers.setFont(
179
- QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
180
- )
217
+ line_numbers.setFont(fixed_font)
181
218
  preview = QTextEdit()
182
219
  preview.setReadOnly(True)
183
- preview.setFont(
184
- QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
185
- )
220
+ preview.setFont(fixed_font)
186
221
  preview_row.addWidget(line_numbers)
187
222
  preview_row.addWidget(preview, 1)
188
223
  layout.addLayout(preview_row, 1)
@@ -194,37 +229,60 @@ def show_markdown_fix_dialog(report):
194
229
 
195
230
  def show_record():
196
231
  record = records[index]
197
- line = record["line"]
232
+ page = (
233
+ f" (page {index + 1} of {len(records)})"
234
+ if len(records) > 1
235
+ else ""
236
+ )
198
237
  if record["record_type"] == "fix":
199
- heading.setText("I fixed this source line automatically.")
238
+ count = len(record["fixes"])
239
+ heading.setText(
240
+ "I fixed this source line automatically." + page
241
+ if count == 1
242
+ else f"I fixed these {count} source lines automatically."
243
+ + page
244
+ )
200
245
  details.setText(record["reason"])
201
- before_lines = record["before"].splitlines() or [""]
202
- after_lines = record["after"].splitlines() or [""]
203
- source_html = ""
204
- numbers = []
205
- if "\n" in record["after"]:
206
- after_html = "<br>↳ ".join(
207
- escape(value)
208
- for value in record["after"].split("\n")
209
- )
210
- source_html = (
211
- "<pre style='font-family:monospace; margin:0'>"
212
- + "Before:<br>"
213
- + "<br>".join(escape(value) for value in before_lines)
214
- + "<br><br>After:<br>"
215
- + after_html
216
- + "</pre>"
217
- )
218
- numbers = [""] + list(
219
- range(line, line + len(before_lines))
220
- )
221
- numbers += ["", ""]
222
- numbers += list(range(line, line + len(after_lines)))
223
- line_numbers.setText("<br>".join(str(value) for value in numbers))
224
- preview.setHtml(source_html)
246
+ line_numbers.setVisible(False)
247
+ # {{{ show each fix as numbered before (−) and after (+) lines
248
+ entries = []
249
+ for fix in record["fixes"]:
250
+ rows = []
251
+ for offset, value in enumerate(
252
+ fix["before"].splitlines() or [""]
253
+ ):
254
+ stripped = value.rstrip(" ")
255
+ rows.append(
256
+ f"<span style='color:#666'>{fix['line'] + offset:>6}"
257
+ "</span> − "
258
+ + escape(stripped)
259
+ # make stray trailing spaces visible
260
+ + "<span style='background:#fbb'>·</span>"
261
+ * (len(value) - len(stripped))
262
+ )
263
+ for offset, value in enumerate(
264
+ []
265
+ if fix["after"] == fix["before"].rstrip()
266
+ else fix["after"].split("\n")
267
+ ):
268
+ rows.append(
269
+ f"<span style='color:#666'>{fix['line'] + offset:>6}"
270
+ "</span> "
271
+ + ("+ " if offset == 0 else "↳ ")
272
+ + escape(value)
273
+ )
274
+ entries.append("<br>".join(rows))
275
+ preview.setHtml(
276
+ "<pre style='font-family:monospace; margin:0'>"
277
+ + "<br><br>".join(entries)
278
+ + "</pre>"
279
+ )
280
+ # }}}
225
281
  button.setText("Next" if index + 1 < len(records) else "Done")
226
282
  else:
227
- heading.setText("This source span is not closed.")
283
+ line = record["line"]
284
+ heading.setText("This source span is not closed." + page)
285
+ line_numbers.setVisible(True)
228
286
  if record["kind"] == "math":
229
287
  delimiter = record["delimiter"]
230
288
  details.setText(
@@ -303,6 +361,7 @@ def run_pandoc(
303
361
  comments_to_margin=False,
304
362
  no_comments=False,
305
363
  comment_filter_session=None,
364
+ wrapnumber=79,
306
365
  ):
307
366
  # {{{ automatically fix Markdown source and request edits for
308
367
  # unfinished spans
@@ -310,7 +369,7 @@ def run_pandoc(
310
369
 
311
370
  automatic_fixes_were_made = False
312
371
  while True:
313
- report = autofix_markdown_file(filename, max_line_length=79)
372
+ report = autofix_markdown_file(filename, wrapnumber=wrapnumber)
314
373
  automatic_fixes_were_made |= bool(report["fixes"])
315
374
  if report["fixes"] or report["warnings"]:
316
375
  user_says_fixed = show_markdown_fix_dialog(report)
@@ -789,10 +848,14 @@ class Handler(FileSystemEventHandler):
789
848
  "no_comments": (
790
849
  "Render the HTML without comment tags or comment div blocks."
791
850
  ),
851
+ "wrapnumber": (
852
+ "Maximum source line width; the automatic Markdown fixes break "
853
+ "only lines longer than this."
854
+ ),
792
855
  },
793
856
  filename_extensions={"filename": ".md"},
794
857
  )
795
- def cpb(filename, comments_to_margin=False, no_comments=False):
858
+ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
796
859
  source_path = os.path.normpath(os.path.abspath(filename))
797
860
  source_dir = os.path.dirname(source_path)
798
861
  html_file = filename.rsplit(".", 1)[0] + ".html"
@@ -836,6 +899,7 @@ def cpb(filename, comments_to_margin=False, no_comments=False):
836
899
  comments_to_margin=comments_to_margin,
837
900
  no_comments=no_comments,
838
901
  comment_filter_session=comment_filter_session,
902
+ wrapnumber=wrapnumber,
839
903
  )
840
904
  append_autorefresh(html_file)
841
905
  notice = zotero_notice()
@@ -971,6 +1035,7 @@ def cpb(filename, comments_to_margin=False, no_comments=False):
971
1035
  comments_to_margin=comments_to_margin,
972
1036
  no_comments=no_comments,
973
1037
  comment_filter_session=comment_filter_session,
1038
+ wrapnumber=wrapnumber,
974
1039
  )
975
1040
  append_autorefresh(html_file)
976
1041
  except Exception as exc:
@@ -658,18 +658,29 @@ def _next_break(words, wrapnumber, punctuation_slop, boundaries, offset):
658
658
  Shared by wrap_prose (which then emits that line) and check_prose
659
659
  (which replays the same choice to see if a source line agrees).
660
660
  """
661
+ # counts[j] - 1 is the length of the line holding words[: j + 1]
661
662
  counts = list(itertools.accumulate(len(word) + 1 for word in words))
662
- upto = min(range(len(counts)), key=lambda j: abs(counts[j] - wrapnumber))
663
+ if counts[-1] - 1 <= wrapnumber:
664
+ # the rest fits, so there is nothing to break
665
+ return len(words) - 1
666
+ # the last word that fits within the width (or the first word, if even
667
+ # that is too long)
668
+ upto = max(
669
+ [j for j, count in enumerate(counts) if count - 1 <= wrapnumber]
670
+ or [0]
671
+ )
672
+ # prefer to end the line on a clause (comma, paren, dash, ...) or at an
673
+ # inline-math boundary within punctuation_slop characters of that break
663
674
  candidates = [
664
675
  j
665
- for j, word in enumerate(words)
666
- if (len(word) > 1 and word[-1] in ",;:)-") or offset + j in boundaries
676
+ for j, word in enumerate(words[: upto + 1])
677
+ if (
678
+ (len(word) > 1 and word[-1] in ",;:)-")
679
+ or offset + j in boundaries
680
+ )
681
+ and counts[upto] - counts[j] < punctuation_slop
667
682
  ]
668
- if candidates:
669
- punct = min(candidates, key=lambda j: abs(counts[j] - wrapnumber))
670
- if punct < upto and upto - punct < punctuation_slop:
671
- upto = punct
672
- return upto
683
+ return max(candidates) if candidates else upto
673
684
 
674
685
 
675
686
  def wrap_prose(
@@ -805,6 +816,11 @@ def _prepare(filename, cleanoo=False, i=-1):
805
816
  "filename": "Input file to wrap. Use '-' to read from stdin.",
806
817
  "cleanoo": "Strip LibreOffice markup before wrapping.",
807
818
  "i": "Indentation level for wrapped lines.",
819
+ "wrapnumber": "Maximum line width in characters.",
820
+ "punctuation_slop": (
821
+ "End a line early on a clause (comma, paren, dash, ...) when "
822
+ "that is within this many characters of the widest break."
823
+ ),
808
824
  },
809
825
  filename_extensions={"filename": [".md", ".tex"]},
810
826
  )
@@ -920,7 +936,16 @@ def check_prose(content, wrapnumber, punctuation_slop, line_start=1):
920
936
  and not breaks_after[actual_end]
921
937
  ):
922
938
  actual_end += 1
923
- if actual_end > wr_end:
939
+ line_length = (
940
+ positions[actual_end]
941
+ + len(words[actual_end])
942
+ - positions[fragment_offset]
943
+ )
944
+ # only a line that exceeds the width is too long
945
+ if (
946
+ actual_end > wr_end
947
+ and line_length > wrapnumber
948
+ ):
924
949
  overflow = words[wr_end + 1]
925
950
  issue_line = para_line + stripped.count(
926
951
  "\n", 0, positions[actual_end]
@@ -1004,13 +1029,9 @@ def _iter_block_spans(blocks, wrapnumber, line_start=1):
1004
1029
 
1005
1030
 
1006
1031
  def markdown_lint_issues_from_text(
1007
- content, wrapnumber=45, punctuation_slop=20, max_line_length=None
1032
+ content, wrapnumber=45, punctuation_slop=20
1008
1033
  ):
1009
- """Return Markdown writing issues, including unfinished source spans.
1010
-
1011
- With `max_line_length`, a prose line is only too long when it holds
1012
- more than that many characters, instead of wr's greedy line choice.
1013
- """
1034
+ """Return Markdown writing issues, including unfinished source spans."""
1014
1035
  def mask_comment(match):
1015
1036
  return "".join("\n" if char == "\n" else " " for char in match[0])
1016
1037
 
@@ -1020,30 +1041,13 @@ def markdown_lint_issues_from_text(
1020
1041
  # trailing spaces are reported separately below, and a double space
1021
1042
  # (a hard line break) must not look like a sentence ending mid-line
1022
1043
  prose_content = re.sub(r"[ \t]+$", "", prose_content, flags=re.M)
1023
- issues = []
1024
- prose_lines = prose_content.splitlines()
1025
- for line, width, span in _iter_block_spans(
1026
- markdown_blocks(prose_content), wrapnumber
1027
- ):
1028
- for issue in check_prose(span, width, punctuation_slop, line):
1029
- if max_line_length is None or not issue[1].startswith(
1030
- "line too long:"
1031
- ):
1032
- issues.append(issue)
1033
- if max_line_length is not None:
1034
- # {{{ flag prose lines longer than the character limit
1035
- for number in range(line, line + len(span.splitlines())):
1036
- text = prose_lines[number - 1]
1037
- # a single unbreakable word cannot be shortened
1038
- if len(text) > max_line_length and len(
1039
- WORD.findall(text)
1040
- ) > 1:
1041
- issues.append((
1042
- number,
1043
- f"line over {max_line_length} characters: break "
1044
- "it at a word boundary",
1045
- ))
1046
- # }}}
1044
+ issues = [
1045
+ issue
1046
+ for line, width, span in _iter_block_spans(
1047
+ markdown_blocks(prose_content), wrapnumber
1048
+ )
1049
+ for issue in check_prose(span, width, punctuation_slop, line)
1050
+ ]
1047
1051
  issues.extend(
1048
1052
  (issue["line"], issue["message"])
1049
1053
  for issue in unclosed_markdown_spans(content)
@@ -1186,10 +1190,15 @@ def apply_markdown_issue_fix(content, line_number, message):
1186
1190
  else:
1187
1191
  span = found.span("gap")
1188
1192
  joined = ""
1189
- before = content[found.start() : found.end()]
1190
1193
  updated = content[: span[0]] + joined + content[span[1] :]
1194
+ # report the whole source lines around the join
1195
+ line_start = content.rfind("\n", 0, found.start()) + 1
1196
+ line_end = content.find("\n", max(found.end(), span[1]))
1197
+ if line_end == -1:
1198
+ line_end = len(content)
1199
+ before = content[line_start:line_end]
1191
1200
  after = updated[
1192
- found.start() : span[0] + len(joined) + found.end() - span[1]
1201
+ line_start : line_end - (span[1] - span[0]) + len(joined)
1193
1202
  ]
1194
1203
  return (
1195
1204
  updated,
@@ -1229,25 +1238,7 @@ def apply_markdown_issue_fix(content, line_number, message):
1229
1238
  old_line[:-1],
1230
1239
  "A line ended with a single stray space. I removed it.",
1231
1240
  )
1232
- if message.startswith("line over "):
1233
- # {{{ break at the last word boundary that fits the limit
1234
- limit = int(re.match(r"line over (\d+)", message)[1])
1235
- gaps = [
1236
- found.end()
1237
- for found in WORD.finditer(old_line)
1238
- if container.end() < found.end() < len(old_line.rstrip())
1239
- ]
1240
- fitting = [gap for gap in gaps if gap <= limit]
1241
- # when even the first word is too long, break right after it
1242
- split_at = fitting[-1] if fitting else gaps[0]
1243
- first = old_line[:split_at].rstrip()
1244
- second = old_line[split_at:].lstrip(" \t")
1245
- reason = (
1246
- f"This line was longer than {limit} characters. "
1247
- "I moved the last words onto a new line."
1248
- )
1249
- # }}}
1250
- elif message.startswith("line too long:"):
1241
+ if message.startswith("line too long:"):
1251
1242
  match = re.search(r"after '(.+?)' \(before '(.+?)'\); move", message)
1252
1243
  if match is None:
1253
1244
  raise ValueError(
@@ -1298,9 +1289,7 @@ def apply_markdown_issue_fix(content, line_number, message):
1298
1289
  return "".join(lines), old_line, after, reason
1299
1290
 
1300
1291
 
1301
- def autofix_markdown_file(
1302
- filename, wrapnumber=55, punctuation_slop=20, max_line_length=None
1303
- ):
1292
+ def autofix_markdown_file(filename, wrapnumber=55, punctuation_slop=20):
1304
1293
  """Apply safe source line breaks and report unfinished source spans."""
1305
1294
  with open(filename, encoding="utf-8", newline="") as fp:
1306
1295
  content = fp.read()
@@ -1309,10 +1298,7 @@ def autofix_markdown_file(
1309
1298
  # issue at a time re-lints the whole file for every single fix.
1310
1299
  for _ in range(100):
1311
1300
  issues = markdown_lint_issues_from_text(
1312
- content,
1313
- wrapnumber=wrapnumber,
1314
- punctuation_slop=punctuation_slop,
1315
- max_line_length=max_line_length,
1301
+ content, wrapnumber=wrapnumber, punctuation_slop=punctuation_slop
1316
1302
  )
1317
1303
  fixable = {}
1318
1304
  for line_number, message in issues:
@@ -1358,8 +1344,9 @@ def autofix_markdown_file(
1358
1344
  @register_command(
1359
1345
  "check wrapping and sentence-break rules (for markdown or latex).",
1360
1346
  "check that a file already obeys wr's wrapping rules -- without\n"
1361
- "rewriting anything. Reports a source line as too long if it holds\n"
1362
- "more words than wr's own greedy choice would put there, and\n"
1347
+ "rewriting anything. Reports a source line as too long if it is\n"
1348
+ "longer than --wrapnumber characters (and suggests the break wr would\n"
1349
+ "use, which favors ending on a clause a few words early), and\n"
1363
1350
  "reports a sentence that ends in the middle of a source line\n"
1364
1351
  "instead of at a line break. Prints 'file:line: message' for each\n"
1365
1352
  "violation and exits non-zero if any are found. Takes the same\n"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyDiffTools"
7
- version = "0.1.41"
7
+ version = "0.1.43"
8
8
  authors = [{ name = "J M Franck" }]
9
9
  description = "Diff tools"
10
10
  readme = "README.rst"
@@ -253,49 +253,66 @@ def test_autofix_fixes_every_line_in_one_pass_from_the_end(tmp_path):
253
253
  assert markdown_lint_issues_from_text(fixed, wrapnumber=55) == []
254
254
 
255
255
 
256
- def test_character_limit_ignores_lines_wr_would_break_sooner():
257
- # from RM_ESR: 74 characters, which wr's width-55 rule would break
256
+ def test_only_lines_longer_than_the_width_are_too_long():
257
+ # from RM_ESR: short lines ending on a clause used to be flagged, since
258
+ # wr broke at a comma or paren even when the rest of the line fit
258
259
  source = (
259
- "temperatures not amenable to other methods, as in "
260
- "[@Banerjee2009ESREviCoe].\n"
260
+ "(see @fig:CaldararuTau) indicates that\n"
261
+ "This is especially true, given that a comparison\n"
261
262
  )
262
263
 
263
- assert markdown_lint_issues_from_text(source, 55) != []
264
- assert markdown_lint_issues_from_text(source, max_line_length=79) == []
264
+ assert markdown_lint_issues_from_text(source, 55) == []
265
+ assert markdown_lint_issues_from_text(source, 79) == []
266
+ assert [
267
+ message.split(":")[0]
268
+ for _, message in markdown_lint_issues_from_text(source, 40)
269
+ ] == ["line too long"]
265
270
 
266
271
 
267
- @pytest.mark.parametrize("prefix", ["", "- ", "> "])
268
- def test_character_limit_breaks_at_last_fitting_word(tmp_path, prefix):
269
- # from RM_ESR: a sentence that grew past 79 characters
272
+ def test_long_line_breaks_early_on_a_nearby_clause(tmp_path):
273
+ # from RM_ESR: several clauses on one line that grew past 79
270
274
  line = (
271
- prefix + "This script determines the field positions and amplitudes "
272
- "of the spectral lines for an entire variable-temperature "
273
- "experiment.\n"
275
+ "Finally, it estimates the rotational correlation time, "
276
+ "*via* the classic Kivelson equation, as a function of temperature.\n"
274
277
  )
275
278
  path = tmp_path / "source.md"
276
279
  path.write_text(line)
277
280
 
278
- issues = markdown_lint_issues_from_text(line, max_line_length=79)
279
- report = autofix_markdown_file(path, max_line_length=79)
281
+ report = autofix_markdown_file(path, wrapnumber=79)
280
282
 
281
283
  fixed = path.read_text()
282
- assert [message.split(":")[0] for _, message in issues] == [
283
- "line over 79 characters"
284
- ]
285
- assert "79 characters" in report["fixes"][0]["reason"]
284
+ assert fixed.split() == line.split()
286
285
  assert all(len(text) <= 79 for text in fixed.splitlines())
287
- assert fixed.replace("\n> ", "\n").split() == line.split()
288
- # the first line is filled as far as the limit allows
289
- first = fixed.splitlines()[0]
290
- next_word = line.split()[len(first.split())]
291
- assert len(first) + 1 + len(next_word) > 79
292
- assert markdown_lint_issues_from_text(fixed, max_line_length=79) == []
286
+ # the widest break is after "classic" (72 characters); "time," is 18
287
+ # characters before that, within the slop, so the line ends on it
288
+ assert fixed.splitlines()[0].endswith("rotational correlation time,")
289
+ assert len(report["fixes"]) == 1
290
+
291
+
292
+ def test_clause_far_before_the_width_does_not_force_an_early_break():
293
+ line = (
294
+ "Finally, it estimates the rotational correlation time of the "
295
+ "probe and many other ordinary words here.\n"
296
+ )
297
+
298
+ first = wrap_prose(line, 79).splitlines()[0]
293
299
 
300
+ assert first.startswith("Finally, it estimates")
301
+ assert len(first) > 70
294
302
 
295
- def test_character_limit_skips_a_single_unbreakable_word():
296
- source = "https://example.org/" + "x" * 90 + "\n"
297
303
 
298
- assert markdown_lint_issues_from_text(source, max_line_length=79) == []
304
+ @pytest.mark.parametrize("width", [20, 45, 55, 79])
305
+ def test_wr_never_runs_past_the_width(width):
306
+ text = (
307
+ "This script determines the field positions, and amplitudes, of the "
308
+ "spectral lines (for an entire variable-temperature experiment) -- "
309
+ "and it implements spline-based smoothing of the spectra.\n"
310
+ )
311
+
312
+ wrapped = wrap_prose(text, width)
313
+
314
+ assert all(len(line) <= width for line in wrapped.splitlines())
315
+ assert markdown_lint_issues_from_text(wrapped, width) == []
299
316
 
300
317
 
301
318
  def test_unclosed_math_is_reported_after_fifty_lines():
@@ -406,19 +423,14 @@ def test_qt_fix_and_reload_windows_show_source_changes(tmp_path):
406
423
  )
407
424
  seen["preview"] = dialog.findChild(QTextEdit).toPlainText()
408
425
  seen["button"] = dialog.findChild(QPushButton).text()
409
- seen["line_gutter"] = next(
410
- label
411
- for label in dialog.findChildren(QLabel)
412
- if "#666" in label.styleSheet()
413
- ).text()
414
426
  seen["heading"] = dialog.findChildren(QLabel)[0].text()
415
427
  dialog.findChild(QPushButton).click()
416
428
 
417
429
  QTimer.singleShot(0, inspect_fix_window)
418
430
  show_markdown_fix_dialog(report)
419
- assert "↳ of words" in seen["preview"]
431
+ assert "↳ collection of words" in seen["preview"]
420
432
  assert seen["button"] == "Done"
421
- assert "1" in seen["line_gutter"]
433
+ assert re.search(r"^ +1 − This deliberately", seen["preview"], re.M)
422
434
  assert "line 1" not in seen["heading"].lower()
423
435
 
424
436
  def inspect_reload_window():
@@ -437,6 +449,70 @@ def test_qt_fix_and_reload_windows_show_source_changes(tmp_path):
437
449
  assert seen["reload_button"] == "I've reloaded"
438
450
 
439
451
 
452
+ def test_qt_fix_window_groups_fixes_of_one_kind_onto_full_pages(tmp_path):
453
+ pytest.importorskip("PySide6")
454
+ from PySide6.QtCore import QTimer
455
+ from PySide6.QtWidgets import QApplication, QDialog, QPushButton
456
+ from PySide6.QtWidgets import QLabel, QTextEdit
457
+
458
+ from pydifftools.continuous import show_markdown_fix_dialog
459
+
460
+ app = QApplication.instance() or QApplication([])
461
+ # from RM_ESR: many stray trailing spaces plus one other kind of fix
462
+ source_path = tmp_path / "content.md"
463
+ source_path.write_text(
464
+ "".join(f"Sentence number {j} ends here. \n" for j in range(60))
465
+ + "\n$$\nx = y\n$$ {#eq:tauFromLinewidth}\n"
466
+ )
467
+ report = autofix_markdown_file(source_path)
468
+ assert len(report["fixes"]) == 61
469
+ pages = []
470
+
471
+ def inspect_page():
472
+ dialog = next(
473
+ widget
474
+ for widget in app.topLevelWidgets()
475
+ if isinstance(widget, QDialog)
476
+ and widget.isVisible()
477
+ and widget.windowTitle() == "Markdown source fixes"
478
+ )
479
+ preview = dialog.findChild(QTextEdit).toPlainText()
480
+ pages.append(
481
+ {
482
+ "heading": dialog.findChildren(QLabel)[0].text(),
483
+ "details": dialog.findChildren(QLabel)[1].text(),
484
+ "entries": preview.split("\n\n"),
485
+ }
486
+ )
487
+ button = dialog.findChild(QPushButton)
488
+ if button.text() == "Next":
489
+ QTimer.singleShot(0, inspect_page)
490
+ button.click()
491
+
492
+ QTimer.singleShot(0, inspect_page)
493
+ show_markdown_fix_dialog(report)
494
+
495
+ trailing = [page for page in pages if "stray space" in page["details"]]
496
+ crossref = [page for page in pages if "crossref" in page["details"]]
497
+ # one kind per page, many fixes per page, and every fix shown once
498
+ assert len(trailing) + len(crossref) == len(pages)
499
+ assert len(crossref) == 1
500
+ assert 1 < len(trailing) < 60
501
+ assert all(len(page["entries"]) > 5 for page in trailing[:-1])
502
+ assert sum(len(page["entries"]) for page in trailing) == 60
503
+ assert f"(page 1 of {len(pages)})" in pages[0]["heading"]
504
+ # a trailing-space entry is just the numbered line with its stray space
505
+ # made visible, while other fixes also show the line after the fix
506
+ assert re.fullmatch(
507
+ r" +1 − Sentence number 0 ends here\.·", trailing[0]["entries"][0]
508
+ )
509
+ assert re.fullmatch(
510
+ r" +64 − \$\$ \{#eq:tauFromLinewidth\}\n +64 \+ "
511
+ r"\$\$\{#eq:tauFromLinewidth\}",
512
+ crossref[0]["entries"][0],
513
+ )
514
+
515
+
440
516
  def test_qt_unclosed_math_window_bolds_source_and_requests_confirmation():
441
517
  from PySide6.QtCore import QTimer
442
518
  from PySide6.QtWidgets import (
@@ -423,7 +423,8 @@ def test_nested_containers_keep_terms_items_and_paragraphs_separate():
423
423
  result = wrap_blocks(markdown_blocks(source), 25)
424
424
  assert result.startswith("Term\n: first definition\n")
425
425
  assert "\n - a nested item with\n" in result
426
- assert "\n > a nested quotation\n" in result
426
+ # the quotation is 17 wide, so wr never lets it run past that
427
+ assert "\n > a nested\n" in result
427
428
  assert "\n: a second definition\n" in result
428
429
  assert "\n\nOutside prose" in result
429
430
  assert wrap_blocks(markdown_blocks(result), 25) == result
File without changes
File without changes
File without changes
File without changes