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.
- {pydifftools-0.1.34/pyDiffTools.egg-info → pydifftools-0.1.36}/PKG-INFO +1 -1
- {pydifftools-0.1.34 → pydifftools-0.1.36/pyDiffTools.egg-info}/PKG-INFO +1 -1
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/continuous.py +200 -3
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/wrap_sentences.py +292 -66
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pyproject.toml +1 -1
- pydifftools-0.1.36/tests/test_wrap_check.py +408 -0
- pydifftools-0.1.34/tests/test_wrap_check.py +0 -141
- {pydifftools-0.1.34 → pydifftools-0.1.36}/LICENSE.md +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/MANIFEST.in +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/README.rst +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/_quarto.yml +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/example.qmd +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/example.tex +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/index.qmd +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/independent.qmd +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/index.qmd +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/tasks.qmd +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/test_include.qmd +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/example_notebook/project1/test_include.qmd +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pyDiffTools.egg-info/SOURCES.txt +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pyDiffTools.egg-info/dependency_links.txt +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pyDiffTools.egg-info/entry_points.txt +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pyDiffTools.egg-info/requires.txt +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pyDiffTools.egg-info/top_level.txt +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/__init__.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/bibliography.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/browser_lifecycle.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/check_numbers.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/citation_dialog.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/command_line.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/command_registry.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/comment_functions.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/comment_tags.lua +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/comment_tags_margin.lua +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/comment_tags_no_comments.lua +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/comment_toggle.js +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/comments.css +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/copy_files.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/diff-doc.js +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/doc_contents.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/flowchart/__init__.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/flowchart/comparison.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/flowchart/dot_to_yaml.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/flowchart/graph.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/flowchart/watch_graph.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/flowchart/wgrph_view.js +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/forward_search.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/git_gd.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/git_gd_history.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/git_gd_image.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/git_gd_qt.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/html_comments.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/html_uncomments.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/log_example.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/match_spaces.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/notebook/__init__.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/notebook/display.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/notebook/fast_build.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/notebook/tex_to_qmd.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/onewordify.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/onewordify_undo.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/outline.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/rearrange_tex.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/searchacro.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/separate_comments.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/split_conflict.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/unseparate_comments.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/update_check.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/xml2xlsx.vbs +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/pydifftools/zotero.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/setup.cfg +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_bibliography.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_browser_lifecycle.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_check_single_use.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_command_line_help.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_continuous_shutdown.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_forward_search.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_git_gd_history.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_git_gd_image.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_git_gd_qt.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_rrng.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_tex_to_qmd.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_update_check.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_wrapping.py +0 -0
- {pydifftools-0.1.34 → pydifftools-0.1.36}/tests/test_zotero.py +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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:
|