pyDiffTools 0.1.35__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.35/pyDiffTools.egg-info → pydifftools-0.1.36}/PKG-INFO +1 -1
  2. {pydifftools-0.1.35 → pydifftools-0.1.36/pyDiffTools.egg-info}/PKG-INFO +1 -1
  3. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/continuous.py +172 -94
  4. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/wrap_sentences.py +225 -3
  5. {pydifftools-0.1.35 → pydifftools-0.1.36}/pyproject.toml +1 -1
  6. pydifftools-0.1.36/tests/test_wrap_check.py +408 -0
  7. pydifftools-0.1.35/tests/test_wrap_check.py +0 -141
  8. {pydifftools-0.1.35 → pydifftools-0.1.36}/LICENSE.md +0 -0
  9. {pydifftools-0.1.35 → pydifftools-0.1.36}/MANIFEST.in +0 -0
  10. {pydifftools-0.1.35 → pydifftools-0.1.36}/README.rst +0 -0
  11. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/_quarto.yml +0 -0
  12. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  13. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  14. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  15. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  16. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  17. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  18. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  19. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  20. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  21. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  22. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  23. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  24. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  25. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  26. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  27. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  28. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  29. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  30. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  31. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  32. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  33. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  34. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/example.qmd +0 -0
  35. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/example.tex +0 -0
  36. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/index.qmd +0 -0
  37. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  38. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  39. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  40. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  41. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/independent.qmd +0 -0
  42. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/index.qmd +0 -0
  43. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  44. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  45. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  46. {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/test_include.qmd +0 -0
  47. {pydifftools-0.1.35 → pydifftools-0.1.36}/pyDiffTools.egg-info/SOURCES.txt +0 -0
  48. {pydifftools-0.1.35 → pydifftools-0.1.36}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  49. {pydifftools-0.1.35 → pydifftools-0.1.36}/pyDiffTools.egg-info/entry_points.txt +0 -0
  50. {pydifftools-0.1.35 → pydifftools-0.1.36}/pyDiffTools.egg-info/requires.txt +0 -0
  51. {pydifftools-0.1.35 → pydifftools-0.1.36}/pyDiffTools.egg-info/top_level.txt +0 -0
  52. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/__init__.py +0 -0
  53. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/bibliography.py +0 -0
  54. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/browser_lifecycle.py +0 -0
  55. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/check_numbers.py +0 -0
  56. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/citation_dialog.py +0 -0
  57. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/command_line.py +0 -0
  58. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/command_registry.py +0 -0
  59. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/comment_functions.py +0 -0
  60. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/comment_tags.lua +0 -0
  61. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/comment_tags_margin.lua +0 -0
  62. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/comment_tags_no_comments.lua +0 -0
  63. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/comment_toggle.js +0 -0
  64. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/comments.css +0 -0
  65. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/copy_files.py +0 -0
  66. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/diff-doc.js +0 -0
  67. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/doc_contents.py +0 -0
  68. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/flowchart/__init__.py +0 -0
  69. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/flowchart/comparison.py +0 -0
  70. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  71. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/flowchart/graph.py +0 -0
  72. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  73. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/flowchart/watch_graph.py +0 -0
  74. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/flowchart/wgrph_view.js +0 -0
  75. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/forward_search.py +0 -0
  76. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/git_gd.py +0 -0
  77. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/git_gd_history.py +0 -0
  78. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/git_gd_image.py +0 -0
  79. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/git_gd_qt.py +0 -0
  80. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/html_comments.py +0 -0
  81. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/html_uncomments.py +0 -0
  82. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/log_example.py +0 -0
  83. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/match_spaces.py +0 -0
  84. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/notebook/__init__.py +0 -0
  85. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/notebook/display.py +0 -0
  86. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/notebook/fast_build.py +0 -0
  87. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/notebook/tex_to_qmd.py +0 -0
  88. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/onewordify.py +0 -0
  89. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/onewordify_undo.py +0 -0
  90. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/outline.py +0 -0
  91. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/rearrange_tex.py +0 -0
  92. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/searchacro.py +0 -0
  93. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/separate_comments.py +0 -0
  94. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/split_conflict.py +0 -0
  95. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/unseparate_comments.py +0 -0
  96. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/update_check.py +0 -0
  97. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/xml2xlsx.vbs +0 -0
  98. {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/zotero.py +0 -0
  99. {pydifftools-0.1.35 → pydifftools-0.1.36}/setup.cfg +0 -0
  100. {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_bibliography.py +0 -0
  101. {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_browser_lifecycle.py +0 -0
  102. {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_check_single_use.py +0 -0
  103. {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_command_line_help.py +0 -0
  104. {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_continuous_shutdown.py +0 -0
  105. {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_forward_search.py +0 -0
  106. {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_git_gd_history.py +0 -0
  107. {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_git_gd_image.py +0 -0
  108. {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_git_gd_qt.py +0 -0
  109. {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_rrng.py +0 -0
  110. {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_tex_to_qmd.py +0 -0
  111. {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_update_check.py +0 -0
  112. {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_wrapping.py +0 -0
  113. {pydifftools-0.1.35 → 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.35
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.35
3
+ Version: 0.1.36
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -14,7 +14,6 @@ import traceback
14
14
  import tempfile
15
15
  from pathlib import Path
16
16
  import yaml
17
- import textwrap
18
17
  from watchdog.events import FileSystemEventHandler
19
18
  from watchdog.observers import Observer
20
19
  from watchdog.observers.polling import PollingObserver
@@ -127,126 +126,205 @@ sys.exit(1)
127
126
  raise RuntimeError("pydifft cpb filter dialog failed.")
128
127
 
129
128
 
130
- def run_pandoc(
131
- filename,
132
- html_file,
133
- comments_to_margin=False,
134
- no_comments=False,
135
- comment_filter_session=None,
136
- ):
137
- # {{{ review Markdown lint issues in a Qt walkthrough
138
- def show_markdown_lint(filename, issues):
139
- """Walk through cpb's Markdown style issues in a Qt window."""
140
- dialog_script = r"""
141
- import html
142
- import json
143
- import sys
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
+
144
133
  from PySide6.QtCore import Qt
134
+ from PySide6.QtGui import QFontDatabase
145
135
  from PySide6.QtWidgets import (
146
136
  QApplication,
147
137
  QDialog,
138
+ QHBoxLayout,
148
139
  QLabel,
149
140
  QPushButton,
141
+ QTextEdit,
150
142
  QVBoxLayout,
151
143
  )
152
144
 
153
- filename = sys.argv[1]
154
- issues = json.loads(sys.argv[2])
155
- app = QApplication(sys.argv[:1])
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
+
156
157
  dialog = QDialog()
157
- dialog.setWindowTitle("pydifft cpb — Markdown style")
158
- dialog.resize(620, 260)
158
+ dialog._pydifftools_application = (
159
+ QApplication.instance() or QApplication([])
160
+ )
161
+ dialog.setWindowTitle("Markdown source fixes")
162
+ dialog.resize(760, 480)
159
163
  layout = QVBoxLayout(dialog)
160
164
  heading = QLabel()
161
- heading.setTextFormat(Qt.TextFormat.RichText)
165
+ heading.setWordWrap(True)
162
166
  details = QLabel()
163
- details.setTextFormat(Qt.TextFormat.RichText)
164
167
  details.setWordWrap(True)
165
- fix = QLabel()
166
- fix.setTextFormat(Qt.TextFormat.RichText)
167
- fix.setWordWrap(True)
168
168
  layout.addWidget(heading)
169
169
  layout.addWidget(details)
170
- layout.addWidget(fix)
171
- button = QPushButton("Next")
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()
172
190
  layout.addWidget(button)
173
191
  index = 0
174
-
175
- def show_issue():
176
- global index
177
- if index >= len(issues):
178
- heading.setText("<b>All listed issues reviewed.</b>")
179
- details.setText(
180
- "Edit the source if needed, then continue to check it again."
181
- )
182
- fix.setText("")
183
- button.setText("Check again")
184
- return
185
- line, message = issues[index]
186
- heading.setText(
187
- f"<b>{html.escape(filename)}:{line}</b> — issue "
188
- f"{index + 1} of {len(issues)}"
189
- )
190
- details.setText(html.escape(message))
191
- if message.startswith("sentence ends mid-line"):
192
- fix.setText(
193
- "Fix: end the sentence on this line, then continue the next "
194
- "sentence on a new source line.<br>"
195
- "<span style='color:green'>↳ Enter</span> "
196
- "<span style='color:green'><i>soft return: this newline "
197
- "appears only in the source.</i></span>"
198
- )
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")
199
225
  else:
200
- fix.setText(
201
- "Fix: add a source newline within the sentence so the prose "
202
- "wraps at 55 characters or fewer.<br>"
203
- "<span style='color:green'>↳ Enter</span> "
204
- "<span style='color:green'><i>soft return: this newline "
205
- "appears only in the source.</i></span>"
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
+ )
206
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")
207
251
 
208
252
  def advance():
209
- global index
210
- index += 1
211
- show_issue()
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()
212
263
 
213
264
  button.clicked.connect(advance)
214
- show_issue()
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)
215
296
  dialog.exec()
216
- """
217
- result = subprocess.run(
218
- [
219
- sys.executable,
220
- "-c",
221
- textwrap.dedent(dialog_script),
222
- filename,
223
- json.dumps(issues),
224
- ],
225
- capture_output=True,
226
- text=True,
227
- )
228
- if result.returncode != 0:
229
- detail = result.stderr.strip()
230
- if detail:
231
- raise RuntimeError(
232
- f"cpb Markdown lint dialog failed: {detail}"
233
- )
234
- raise RuntimeError("cpb Markdown lint dialog failed.")
235
- # }}}
236
- # Keep cpb's build gate tied to wrchk's Markdown rules. Recheck after each
237
- # walkthrough because users can edit the source in their editor while the
238
- # Qt window is open.
239
- from .wrap_sentences import markdown_lint_issues_from_text
240
297
 
298
+
299
+ def run_pandoc(
300
+ filename,
301
+ html_file,
302
+ comments_to_margin=False,
303
+ no_comments=False,
304
+ comment_filter_session=None,
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
241
311
  while True:
242
- with open(filename, encoding="utf-8") as fp:
243
- markdown_text = fp.read()
244
- issues = markdown_lint_issues_from_text(
245
- markdown_text, wrapnumber=55
246
- )
247
- if not issues:
248
- break
249
- show_markdown_lint(filename, issues)
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
+ # }}}
250
328
 
251
329
  if comments_to_margin:
252
330
  comment_filter_mode = "margin"
@@ -978,14 +978,236 @@ def _iter_block_spans(blocks, wrapnumber, line_start=1):
978
978
  def markdown_lint_issues_from_text(
979
979
  content, wrapnumber=45, punctuation_slop=20
980
980
  ):
981
- """Return lint issues for Markdown text using wrchk's existing rules."""
982
- return [
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 = [
983
989
  issue
984
990
  for line, width, span in _iter_block_spans(
985
- markdown_blocks(content), wrapnumber
991
+ markdown_blocks(prose_content), wrapnumber
986
992
  )
987
993
  for issue in check_prose(span, width, punctuation_slop, line)
988
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)}
989
1211
 
990
1212
 
991
1213
  @register_command(
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyDiffTools"
7
- version = "0.1.35"
7
+ version = "0.1.36"
8
8
  authors = [{ name = "J M Franck" }]
9
9
  description = "Diff tools"
10
10
  readme = "README.rst"