pyDiffTools 0.1.34__tar.gz → 0.1.36__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 (113) hide show
  1. {pydifftools-0.1.34/pyDiffTools.egg-info → pydifftools-0.1.36}/PKG-INFO +1 -1
  2. {pydifftools-0.1.34 → pydifftools-0.1.36/pyDiffTools.egg-info}/PKG-INFO +1 -1
  3. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/continuous.py +200 -3
  4. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/wrap_sentences.py +292 -66
  5. {pydifftools-0.1.34 → pydifftools-0.1.36}/pyproject.toml +1 -1
  6. pydifftools-0.1.36/tests/test_wrap_check.py +408 -0
  7. pydifftools-0.1.34/tests/test_wrap_check.py +0 -141
  8. {pydifftools-0.1.34 → pydifftools-0.1.36}/LICENSE.md +0 -0
  9. {pydifftools-0.1.34 → pydifftools-0.1.36}/MANIFEST.in +0 -0
  10. {pydifftools-0.1.34 → pydifftools-0.1.36}/README.rst +0 -0
  11. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/_quarto.yml +0 -0
  12. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  13. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  14. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  15. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  16. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  17. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  18. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  19. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  20. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  21. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  22. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  23. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  24. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  25. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  26. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  27. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  28. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  29. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  30. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  31. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  32. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  33. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  34. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/example.qmd +0 -0
  35. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/example.tex +0 -0
  36. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/index.qmd +0 -0
  37. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  38. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  39. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  40. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  41. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/independent.qmd +0 -0
  42. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/index.qmd +0 -0
  43. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  44. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  45. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  46. {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/test_include.qmd +0 -0
  47. {pydifftools-0.1.34 → pydifftools-0.1.36}/pyDiffTools.egg-info/SOURCES.txt +0 -0
  48. {pydifftools-0.1.34 → pydifftools-0.1.36}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  49. {pydifftools-0.1.34 → pydifftools-0.1.36}/pyDiffTools.egg-info/entry_points.txt +0 -0
  50. {pydifftools-0.1.34 → pydifftools-0.1.36}/pyDiffTools.egg-info/requires.txt +0 -0
  51. {pydifftools-0.1.34 → pydifftools-0.1.36}/pyDiffTools.egg-info/top_level.txt +0 -0
  52. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/__init__.py +0 -0
  53. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/bibliography.py +0 -0
  54. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/browser_lifecycle.py +0 -0
  55. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/check_numbers.py +0 -0
  56. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/citation_dialog.py +0 -0
  57. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/command_line.py +0 -0
  58. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/command_registry.py +0 -0
  59. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/comment_functions.py +0 -0
  60. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/comment_tags.lua +0 -0
  61. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/comment_tags_margin.lua +0 -0
  62. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/comment_tags_no_comments.lua +0 -0
  63. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/comment_toggle.js +0 -0
  64. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/comments.css +0 -0
  65. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/copy_files.py +0 -0
  66. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/diff-doc.js +0 -0
  67. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/doc_contents.py +0 -0
  68. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/flowchart/__init__.py +0 -0
  69. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/flowchart/comparison.py +0 -0
  70. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  71. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/flowchart/graph.py +0 -0
  72. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  73. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/flowchart/watch_graph.py +0 -0
  74. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/flowchart/wgrph_view.js +0 -0
  75. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/forward_search.py +0 -0
  76. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/git_gd.py +0 -0
  77. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/git_gd_history.py +0 -0
  78. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/git_gd_image.py +0 -0
  79. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/git_gd_qt.py +0 -0
  80. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/html_comments.py +0 -0
  81. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/html_uncomments.py +0 -0
  82. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/log_example.py +0 -0
  83. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/match_spaces.py +0 -0
  84. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/notebook/__init__.py +0 -0
  85. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/notebook/display.py +0 -0
  86. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/notebook/fast_build.py +0 -0
  87. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/notebook/tex_to_qmd.py +0 -0
  88. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/onewordify.py +0 -0
  89. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/onewordify_undo.py +0 -0
  90. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/outline.py +0 -0
  91. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/rearrange_tex.py +0 -0
  92. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/searchacro.py +0 -0
  93. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/separate_comments.py +0 -0
  94. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/split_conflict.py +0 -0
  95. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/unseparate_comments.py +0 -0
  96. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/update_check.py +0 -0
  97. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/xml2xlsx.vbs +0 -0
  98. {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/zotero.py +0 -0
  99. {pydifftools-0.1.34 → pydifftools-0.1.36}/setup.cfg +0 -0
  100. {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_bibliography.py +0 -0
  101. {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_browser_lifecycle.py +0 -0
  102. {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_check_single_use.py +0 -0
  103. {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_command_line_help.py +0 -0
  104. {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_continuous_shutdown.py +0 -0
  105. {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_forward_search.py +0 -0
  106. {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_git_gd_history.py +0 -0
  107. {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_git_gd_image.py +0 -0
  108. {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_git_gd_qt.py +0 -0
  109. {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_rrng.py +0 -0
  110. {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_tex_to_qmd.py +0 -0
  111. {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_update_check.py +0 -0
  112. {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_wrapping.py +0 -0
  113. {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_zotero.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.34
3
+ Version: 0.1.36
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.34
3
+ Version: 0.1.36
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -1,5 +1,6 @@
1
1
  """Continuous Pandoc build utility that requires geckodriver."""
2
2
 
3
+ import json
3
4
  import time
4
5
  import subprocess
5
6
  import sys
@@ -10,7 +11,6 @@ import shutil
10
11
  import threading
11
12
  import queue
12
13
  import traceback
13
- import json
14
14
  import tempfile
15
15
  from pathlib import Path
16
16
  import yaml
@@ -61,7 +61,8 @@ def _comment_filter_mode(path, packaged_filters=None):
61
61
  return "custom"
62
62
 
63
63
 
64
- def _confirm_restore_comment_filter(active_mode):
64
+ # Kept separate so its prompt choices can be unit-tested.
65
+ def confirm_restore_comment_filter(active_mode):
65
66
  if active_mode == "none":
66
67
  message = (
67
68
  "The current lua filter is the one that does not show comments, "
@@ -125,6 +126,176 @@ sys.exit(1)
125
126
  raise RuntimeError("pydifft cpb filter dialog failed.")
126
127
 
127
128
 
129
+ def show_markdown_fix_dialog(report):
130
+ """Show automatic fixes and any source edits that still need a user."""
131
+ from html import escape
132
+
133
+ from PySide6.QtCore import Qt
134
+ from PySide6.QtGui import QFontDatabase
135
+ from PySide6.QtWidgets import (
136
+ QApplication,
137
+ QDialog,
138
+ QHBoxLayout,
139
+ QLabel,
140
+ QPushButton,
141
+ QTextEdit,
142
+ QVBoxLayout,
143
+ )
144
+
145
+ fixes = report["fixes"]
146
+ warnings = report["warnings"]
147
+ records = [
148
+ {"record_type": "fix", **fix}
149
+ for fix in fixes
150
+ ] + [
151
+ {"record_type": "warning", **warning}
152
+ for warning in warnings
153
+ ]
154
+ if not records:
155
+ return
156
+
157
+ dialog = QDialog()
158
+ dialog._pydifftools_application = (
159
+ QApplication.instance() or QApplication([])
160
+ )
161
+ dialog.setWindowTitle("Markdown source fixes")
162
+ dialog.resize(760, 480)
163
+ layout = QVBoxLayout(dialog)
164
+ heading = QLabel()
165
+ heading.setWordWrap(True)
166
+ details = QLabel()
167
+ details.setWordWrap(True)
168
+ layout.addWidget(heading)
169
+ layout.addWidget(details)
170
+
171
+ preview_row = QHBoxLayout()
172
+ line_numbers = QLabel()
173
+ line_numbers.setAlignment(
174
+ Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight
175
+ )
176
+ line_numbers.setStyleSheet("color: #666; padding-right: 8px;")
177
+ line_numbers.setFont(
178
+ QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
179
+ )
180
+ preview = QTextEdit()
181
+ preview.setReadOnly(True)
182
+ preview.setFont(
183
+ QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
184
+ )
185
+ preview_row.addWidget(line_numbers)
186
+ preview_row.addWidget(preview, 1)
187
+ layout.addLayout(preview_row, 1)
188
+
189
+ button = QPushButton()
190
+ layout.addWidget(button)
191
+ index = 0
192
+ user_says_fixed = False
193
+
194
+ def show_record():
195
+ record = records[index]
196
+ line = record["line"]
197
+ if record["record_type"] == "fix":
198
+ heading.setText("I fixed this source line automatically.")
199
+ details.setText(record["reason"])
200
+ before_lines = record["before"].splitlines() or [""]
201
+ after_lines = record["after"].splitlines() or [""]
202
+ source_html = ""
203
+ numbers = []
204
+ if "\n" in record["after"]:
205
+ after_html = "<br>↳ ".join(
206
+ escape(value)
207
+ for value in record["after"].split("\n")
208
+ )
209
+ source_html = (
210
+ "<pre style='font-family:monospace; margin:0'>"
211
+ + "Before:<br>"
212
+ + "<br>".join(escape(value) for value in before_lines)
213
+ + "<br><br>After:<br>"
214
+ + after_html
215
+ + "</pre>"
216
+ )
217
+ numbers = [""] + list(
218
+ range(line, line + len(before_lines))
219
+ )
220
+ numbers += ["", ""]
221
+ numbers += list(range(line, line + len(after_lines)))
222
+ line_numbers.setText("<br>".join(str(value) for value in numbers))
223
+ preview.setHtml(source_html)
224
+ button.setText("Next" if index + 1 < len(records) else "Done")
225
+ else:
226
+ heading.setText("This source span is not closed.")
227
+ if record["kind"] == "math":
228
+ delimiter = record["delimiter"]
229
+ details.setText(
230
+ f"This math has no closing {delimiter}. Add {delimiter} "
231
+ "where the expression ends, then choose “I've fixed it”."
232
+ )
233
+ else:
234
+ details.setText(
235
+ "This HTML comment has no closing -->. Add --> where "
236
+ "the comment ends, then choose “I've fixed it”."
237
+ )
238
+ offending_lines = record["offending"].splitlines() or [""]
239
+ line_numbers.setText(
240
+ "<br>".join(
241
+ str(number)
242
+ for number in range(line, line + len(offending_lines))
243
+ )
244
+ )
245
+ preview.setHtml(
246
+ "<pre style='font-family:monospace; margin:0'><b>"
247
+ + "<br>".join(escape(value) for value in offending_lines)
248
+ + "</b></pre>"
249
+ )
250
+ button.setText("I've fixed it")
251
+
252
+ def advance():
253
+ nonlocal index, user_says_fixed
254
+ if records[index]["record_type"] == "warning":
255
+ user_says_fixed = True
256
+ dialog.accept()
257
+ return
258
+ if index + 1 < len(records):
259
+ index += 1
260
+ show_record()
261
+ else:
262
+ dialog.accept()
263
+
264
+ button.clicked.connect(advance)
265
+ show_record()
266
+ dialog.exec()
267
+ return user_says_fixed
268
+
269
+
270
+ def show_markdown_reload_dialog():
271
+ """Ask the user to reload a source file changed by automatic fixes."""
272
+ from PySide6.QtWidgets import (
273
+ QApplication,
274
+ QDialog,
275
+ QLabel,
276
+ QPushButton,
277
+ QVBoxLayout,
278
+ )
279
+
280
+ dialog = QDialog()
281
+ dialog._pydifftools_application = (
282
+ QApplication.instance() or QApplication([])
283
+ )
284
+ dialog.setWindowTitle("Reload the Markdown source")
285
+ dialog.resize(440, 150)
286
+ layout = QVBoxLayout(dialog)
287
+ message = QLabel(
288
+ "I fixed the Markdown source. Reload it in your editor to see the "
289
+ "changes."
290
+ )
291
+ message.setWordWrap(True)
292
+ layout.addWidget(message)
293
+ button = QPushButton("I've reloaded")
294
+ button.clicked.connect(dialog.accept)
295
+ layout.addWidget(button)
296
+ dialog.exec()
297
+
298
+
128
299
  def run_pandoc(
129
300
  filename,
130
301
  html_file,
@@ -132,6 +303,29 @@ def run_pandoc(
132
303
  no_comments=False,
133
304
  comment_filter_session=None,
134
305
  ):
306
+ # {{{ automatically fix Markdown source and request edits for
307
+ # unfinished spans
308
+ from .wrap_sentences import autofix_markdown_file
309
+
310
+ automatic_fixes_were_made = False
311
+ while True:
312
+ report = autofix_markdown_file(filename, wrapnumber=55)
313
+ automatic_fixes_were_made |= bool(report["fixes"])
314
+ if report["fixes"] or report["warnings"]:
315
+ user_says_fixed = show_markdown_fix_dialog(report)
316
+ if report["warnings"]:
317
+ if not user_says_fixed:
318
+ raise RuntimeError(
319
+ "The Markdown source still has an unclosed math "
320
+ "expression or HTML comment."
321
+ )
322
+ continue
323
+ break
324
+
325
+ if automatic_fixes_were_made:
326
+ show_markdown_reload_dialog()
327
+ # }}}
328
+
135
329
  if comments_to_margin:
136
330
  comment_filter_mode = "margin"
137
331
  elif no_comments:
@@ -198,9 +392,12 @@ def run_pandoc(
198
392
  ):
199
393
  show_comments = comment_filter_session["show_comments"]
200
394
  else:
201
- show_comments = _confirm_restore_comment_filter(
395
+ # {{{ ask whether to restore comments after detecting a
396
+ # locally changed comment filter
397
+ show_comments = confirm_restore_comment_filter(
202
398
  active_mode
203
399
  )
400
+ # }}}
204
401
  if comment_filter_session is not None:
205
402
  comment_filter_session["show_comments"] = (
206
403
  show_comments
@@ -826,62 +826,13 @@ def wr(filename, wrapnumber=45, punctuation_slop=20, cleanoo=False, i=-1):
826
826
  fp.write(result)
827
827
 
828
828
 
829
- def _check_fragment(
830
- fragment, frag_start, wrapnumber, punctuation_slop, base_line, stripped
831
- ):
832
- """Report line-length violations within one wrap unit (a sentence, or a
833
- piece of one split off around a LaTeX structural macro).
834
-
835
- Replays wr's own greedy word-fitting choice (_next_break) but always
836
- resumes from the *real* line break already present in the source
837
- (never from wr's own suggested break), so a shorter user break, or a
838
- word nudged onto the next line, is never flagged -- only a line that
839
- holds more words than wr's choice would allow is.
840
- """
841
- words, positions = [], []
842
- for match in re.finditer(r"\S+", fragment):
843
- words.append(match.group())
844
- positions.append(frag_start + match.start())
845
- if not words:
846
- return []
847
- ends = positions[1:] + [frag_start + len(fragment)]
848
- breaks_after = [
849
- "\n" in stripped[positions[j] + len(words[j]) : ends[j]]
850
- for j in range(len(words))
851
- ]
852
- boundaries = _math_boundaries(words)
853
- issues = []
854
- offset = 0
855
- while offset < len(words):
856
- upto = _next_break(
857
- words[offset:], wrapnumber, punctuation_slop, boundaries, offset
858
- )
859
- wr_end = offset + upto
860
- actual_end = offset
861
- while actual_end < len(words) - 1 and not breaks_after[actual_end]:
862
- actual_end += 1
863
- if actual_end > wr_end:
864
- overflow = words[wr_end + 1]
865
- line = base_line + stripped.count("\n", 0, positions[actual_end])
866
- issues.append(
867
- (
868
- line,
869
- f"line too long: wr would break after '{words[wr_end]}' "
870
- f"(before '{overflow}'); move '{overflow}' onward to "
871
- "the next line, or shorten this sentence",
872
- )
873
- )
874
- offset = actual_end + 1
875
- return issues
876
-
877
-
878
829
  def check_prose(content, wrapnumber, punctuation_slop, line_start=1):
879
830
  """Report source lines in `content` that break wr's rules.
880
831
 
881
832
  Two checks, mirroring wrap_prose's own paragraph/sentence/macro
882
833
  splitting so the two commands never disagree about what counts as a
883
834
  sentence: lines must not hold more words than wr's own greedy choice
884
- would put there (see _check_fragment), and a sentence must not end
835
+ would put there (see the fragment check below), and a sentence must not end
885
836
  in the middle of a source line. A sentence-ending period followed by
886
837
  a backslash-space (`\\ `) rather than a plain space -- the standard
887
838
  LaTeX/Pandoc way to mark an abbreviation -- is never mistaken for a
@@ -912,16 +863,54 @@ def check_prose(content, wrapnumber, punctuation_slop, line_start=1):
912
863
  frag_offset = sentence_start
913
864
  for fragment in LATEX_MACRO_SPLIT.split(sentence):
914
865
  if fragment:
915
- issues.extend(
916
- _check_fragment(
917
- fragment,
918
- frag_offset,
919
- wrapnumber,
920
- punctuation_slop,
921
- para_line,
922
- stripped,
923
- )
924
- )
866
+ # {{{ check line lengths within this sentence fragment
867
+ words, positions = [], []
868
+ for match in re.finditer(r"\S+", fragment):
869
+ words.append(match.group())
870
+ positions.append(frag_offset + match.start())
871
+ if words:
872
+ ends = positions[1:] + [frag_offset + len(fragment)]
873
+ breaks_after = [
874
+ "\n"
875
+ in stripped[
876
+ positions[j] + len(words[j]) : ends[j]
877
+ ]
878
+ for j in range(len(words))
879
+ ]
880
+ boundaries = _math_boundaries(words)
881
+ fragment_offset = 0
882
+ while fragment_offset < len(words):
883
+ upto = _next_break(
884
+ words[fragment_offset:],
885
+ wrapnumber,
886
+ punctuation_slop,
887
+ boundaries,
888
+ fragment_offset,
889
+ )
890
+ wr_end = fragment_offset + upto
891
+ actual_end = fragment_offset
892
+ while (
893
+ actual_end < len(words) - 1
894
+ and not breaks_after[actual_end]
895
+ ):
896
+ actual_end += 1
897
+ if actual_end > wr_end:
898
+ overflow = words[wr_end + 1]
899
+ issue_line = para_line + stripped.count(
900
+ "\n", 0, positions[actual_end]
901
+ )
902
+ issues.append(
903
+ (
904
+ issue_line,
905
+ "line too long: wr would break after "
906
+ f"'{words[wr_end]}' (before "
907
+ f"'{overflow}'); move '{overflow}' "
908
+ "onward to the next line, or shorten "
909
+ "this sentence",
910
+ )
911
+ )
912
+ fragment_offset = actual_end + 1
913
+ # }}}
925
914
  frag_offset += len(fragment)
926
915
  offset += len(sentence)
927
916
  if k < len(separators):
@@ -986,6 +975,241 @@ def _iter_block_spans(blocks, wrapnumber, line_start=1):
986
975
  line += span
987
976
 
988
977
 
978
+ def markdown_lint_issues_from_text(
979
+ content, wrapnumber=45, punctuation_slop=20
980
+ ):
981
+ """Return Markdown writing issues, including unfinished source spans."""
982
+ def mask_comment(match):
983
+ return "".join("\n" if char == "\n" else " " for char in match[0])
984
+
985
+ prose_content = re.sub(
986
+ r"<!--.*?(?:-->|\Z)", mask_comment, content, flags=re.DOTALL
987
+ )
988
+ issues = [
989
+ issue
990
+ for line, width, span in _iter_block_spans(
991
+ markdown_blocks(prose_content), wrapnumber
992
+ )
993
+ for issue in check_prose(span, width, punctuation_slop, line)
994
+ ]
995
+ issues.extend(
996
+ (issue["line"], issue["message"])
997
+ for issue in unclosed_markdown_spans(content)
998
+ )
999
+ return sorted(issues)
1000
+
1001
+
1002
+ def unclosed_markdown_spans(content, math_line_limit=50):
1003
+ """Find unfinished math and HTML comments in Markdown source."""
1004
+ lines = content.splitlines(keepends=True)
1005
+ spans = []
1006
+ fence_char = None
1007
+ fence_size = 0
1008
+ math_start = None
1009
+ math_delimiter = None
1010
+ math_content = []
1011
+ comment_start = None
1012
+ comment_content = []
1013
+ inline_code_delimiter = None
1014
+ token_pattern = re.compile(
1015
+ r"<!--|-->|(?<!\\)\$\$|(?<!\\)\$|`+"
1016
+ )
1017
+
1018
+ for line_number, line in enumerate(lines, 1):
1019
+ fence = re.match(r"^[ \t]*(?P<marker>`{3,}|~{3,})", line)
1020
+ if fence_char is not None:
1021
+ if (
1022
+ fence
1023
+ and fence.group("marker")[0] == fence_char
1024
+ and len(fence.group("marker")) >= fence_size
1025
+ ):
1026
+ fence_char = None
1027
+ continue
1028
+ if fence:
1029
+ fence_char = fence.group("marker")[0]
1030
+ fence_size = len(fence.group("marker"))
1031
+ continue
1032
+
1033
+ for token in token_pattern.finditer(line):
1034
+ value = token.group()
1035
+ if comment_start is not None:
1036
+ if value == "-->":
1037
+ comment_start = None
1038
+ comment_content = []
1039
+ continue
1040
+ if math_start is not None:
1041
+ if value == math_delimiter:
1042
+ math_start = None
1043
+ math_delimiter = None
1044
+ math_content = []
1045
+ continue
1046
+ if inline_code_delimiter is not None:
1047
+ if value.startswith("`") and value == inline_code_delimiter:
1048
+ inline_code_delimiter = None
1049
+ continue
1050
+ if value.startswith("`"):
1051
+ inline_code_delimiter = value
1052
+ continue
1053
+ if value == "<!--":
1054
+ comment_start = line_number
1055
+ comment_content = [line[token.start() :]]
1056
+ elif value in {"$", "$$"}:
1057
+ math_start = line_number
1058
+ math_delimiter = value
1059
+ math_content = [line[token.start() :]]
1060
+ if comment_start is not None and line_number > comment_start:
1061
+ comment_content.append(line)
1062
+ if math_start is not None and line_number > math_start:
1063
+ math_content.append(line)
1064
+
1065
+ if comment_start is not None:
1066
+ spans.append(
1067
+ {
1068
+ "kind": "comment",
1069
+ "line": comment_start,
1070
+ "message": "unclosed HTML comment: add '-->' to close it",
1071
+ "offending": "".join(comment_content).rstrip("\r\n"),
1072
+ }
1073
+ )
1074
+ if (
1075
+ math_start is not None
1076
+ and len(lines) - math_start + 1 >= math_line_limit
1077
+ ):
1078
+ spans.append(
1079
+ {
1080
+ "kind": "math",
1081
+ "line": math_start,
1082
+ "message": (
1083
+ "unclosed math: add the matching "
1084
+ f"'{math_delimiter}' to close it"
1085
+ ),
1086
+ "offending": "".join(math_content).rstrip("\r\n"),
1087
+ "delimiter": math_delimiter,
1088
+ }
1089
+ )
1090
+ return spans
1091
+
1092
+
1093
+ # Kept public so the automatic source edits can be unit-tested directly.
1094
+ def apply_markdown_issue_fix(content, line_number, message):
1095
+ """Insert one source line break for a wrchk Markdown issue."""
1096
+ lines = content.splitlines(keepends=True)
1097
+ index = line_number - 1
1098
+ if index < 0 or index >= len(lines):
1099
+ raise ValueError(
1100
+ f"Markdown issue refers to missing line {line_number}"
1101
+ )
1102
+ old_line = lines[index]
1103
+ ending = ""
1104
+ if old_line.endswith("\r\n"):
1105
+ old_line, ending = old_line[:-2], "\r\n"
1106
+ elif old_line.endswith(("\n", "\r")):
1107
+ old_line, ending = old_line[:-1], old_line[-1:]
1108
+ if not ending:
1109
+ ending = "\n"
1110
+
1111
+ # {{{ keep continuation lines inside their Markdown container
1112
+ container = re.match(
1113
+ r"^((?:[ \t]*>[ \t]?)*)(?:([-+*]|[0-9]+[.)])([ \t]+))?",
1114
+ old_line,
1115
+ )
1116
+ quote_prefix, marker, marker_space = container.groups()
1117
+ if marker:
1118
+ prefix = quote_prefix + " " * (len(marker) + len(marker_space))
1119
+ elif quote_prefix:
1120
+ prefix = quote_prefix
1121
+ else:
1122
+ prefix = re.match(r"^[ \t]*", old_line).group()
1123
+ # }}}
1124
+ if message.startswith("line too long:"):
1125
+ match = re.search(r"before '([^']+)'", message)
1126
+ if match is None:
1127
+ raise ValueError(
1128
+ f"Cannot find the suggested word in {message!r}"
1129
+ )
1130
+ word = re.escape(match.group(1))
1131
+ position = re.search(r"(?<!\S)" + word + r"(?!\S)", old_line)
1132
+ if position is None:
1133
+ raise ValueError(
1134
+ f"Cannot find {match.group(1)!r} on Markdown line "
1135
+ f"{line_number}"
1136
+ )
1137
+ split_at = position.start()
1138
+ first = old_line[:split_at].rstrip()
1139
+ second = old_line[split_at:].lstrip(" \t")
1140
+ reason = (
1141
+ "This line was hard to read because it held too many words. "
1142
+ "I moved the next words onto a new line."
1143
+ )
1144
+ elif message.startswith("sentence ends mid-line"):
1145
+ boundary = SENTENCE_BOUNDARY.search(old_line)
1146
+ if boundary is None or boundary.group(2) != " ":
1147
+ raise ValueError(
1148
+ "Cannot find the sentence break on Markdown line "
1149
+ f"{line_number}"
1150
+ )
1151
+ first = old_line[: boundary.end(1)].rstrip()
1152
+ second = old_line[boundary.end(2) :].lstrip(" \t")
1153
+ reason = (
1154
+ "A sentence ended, but the next sentence continued on the same "
1155
+ "source line. I started it on a new line."
1156
+ )
1157
+ else:
1158
+ raise ValueError(f"No automatic fix is available for {message!r}")
1159
+
1160
+ fixed_line = first + ending + prefix + second + ending
1161
+ lines[index] = fixed_line
1162
+ after = first + "\n" + prefix + second
1163
+ return "".join(lines), old_line, after, reason
1164
+
1165
+
1166
+ def autofix_markdown_file(filename, wrapnumber=55, punctuation_slop=20):
1167
+ """Apply safe source line breaks and report unfinished source spans."""
1168
+ with open(filename, encoding="utf-8", newline="") as fp:
1169
+ content = fp.read()
1170
+ fixes = []
1171
+ for _ in range(1000):
1172
+ issues = markdown_lint_issues_from_text(
1173
+ content, wrapnumber=wrapnumber, punctuation_slop=punctuation_slop
1174
+ )
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:
1186
+ 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}"
1194
+ )
1195
+ fixes.append(
1196
+ {
1197
+ "line": line_number,
1198
+ "reason": reason,
1199
+ "before": before,
1200
+ "after": after,
1201
+ }
1202
+ )
1203
+ content = updated
1204
+ else:
1205
+ raise RuntimeError("Too many automatic Markdown fixes were needed.")
1206
+
1207
+ if fixes:
1208
+ with open(filename, "w", encoding="utf-8", newline="") as fp:
1209
+ fp.write(content)
1210
+ return {"fixes": fixes, "warnings": unclosed_markdown_spans(content)}
1211
+
1212
+
989
1213
  @register_command(
990
1214
  "check wrapping and sentence-break rules (for markdown or latex).",
991
1215
  "check that a file already obeys wr's wrapping rules -- without\n"
@@ -1005,17 +1229,19 @@ def _iter_block_spans(blocks, wrapnumber, line_start=1):
1005
1229
  def wrchk(filename, wrapnumber=45, punctuation_slop=20, cleanoo=False, i=-1):
1006
1230
  alltext, filetype, _, display_name = _prepare(filename, cleanoo, i)
1007
1231
  if filetype == "markdown":
1008
- spans = _iter_block_spans(markdown_blocks(alltext), wrapnumber)
1232
+ issues = markdown_lint_issues_from_text(
1233
+ alltext, wrapnumber, punctuation_slop
1234
+ )
1009
1235
  else:
1010
1236
  spans = (
1011
1237
  (line, wrapnumber, content)
1012
1238
  for line, content in _iter_classified_spans(alltext, filetype)
1013
1239
  )
1014
- issues = [
1015
- issue
1016
- for line, width, content in spans
1017
- for issue in check_prose(content, width, punctuation_slop, line)
1018
- ]
1240
+ issues = [
1241
+ issue
1242
+ for line, width, content in spans
1243
+ for issue in check_prose(content, width, punctuation_slop, line)
1244
+ ]
1019
1245
  for line, message in issues:
1020
1246
  print(f"{display_name}:{line}: {message}")
1021
1247
  if issues:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyDiffTools"
7
- version = "0.1.34"
7
+ version = "0.1.36"
8
8
  authors = [{ name = "J M Franck" }]
9
9
  description = "Diff tools"
10
10
  readme = "README.rst"