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.
- {pydifftools-0.1.35/pyDiffTools.egg-info → pydifftools-0.1.36}/PKG-INFO +1 -1
- {pydifftools-0.1.35 → pydifftools-0.1.36/pyDiffTools.egg-info}/PKG-INFO +1 -1
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/continuous.py +172 -94
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/wrap_sentences.py +225 -3
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pyproject.toml +1 -1
- pydifftools-0.1.36/tests/test_wrap_check.py +408 -0
- pydifftools-0.1.35/tests/test_wrap_check.py +0 -141
- {pydifftools-0.1.35 → pydifftools-0.1.36}/LICENSE.md +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/MANIFEST.in +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/README.rst +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/_quarto.yml +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/example.qmd +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/example.tex +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/index.qmd +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/independent.qmd +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/index.qmd +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/tasks.qmd +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/test_include.qmd +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/example_notebook/project1/test_include.qmd +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pyDiffTools.egg-info/SOURCES.txt +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pyDiffTools.egg-info/dependency_links.txt +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pyDiffTools.egg-info/entry_points.txt +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pyDiffTools.egg-info/requires.txt +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pyDiffTools.egg-info/top_level.txt +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/__init__.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/bibliography.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/browser_lifecycle.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/check_numbers.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/citation_dialog.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/command_line.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/command_registry.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/comment_functions.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/comment_tags.lua +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/comment_tags_margin.lua +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/comment_tags_no_comments.lua +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/comment_toggle.js +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/comments.css +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/copy_files.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/diff-doc.js +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/doc_contents.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/flowchart/__init__.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/flowchart/comparison.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/flowchart/dot_to_yaml.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/flowchart/graph.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/flowchart/watch_graph.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/flowchart/wgrph_view.js +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/forward_search.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/git_gd.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/git_gd_history.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/git_gd_image.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/git_gd_qt.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/html_comments.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/html_uncomments.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/log_example.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/match_spaces.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/notebook/__init__.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/notebook/display.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/notebook/fast_build.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/notebook/tex_to_qmd.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/onewordify.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/onewordify_undo.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/outline.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/rearrange_tex.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/searchacro.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/separate_comments.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/split_conflict.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/unseparate_comments.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/update_check.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/xml2xlsx.vbs +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/pydifftools/zotero.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/setup.cfg +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_bibliography.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_browser_lifecycle.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_check_single_use.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_command_line_help.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_continuous_shutdown.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_forward_search.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_git_gd_history.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_git_gd_image.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_git_gd_qt.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_rrng.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_tex_to_qmd.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_update_check.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_wrapping.py +0 -0
- {pydifftools-0.1.35 → pydifftools-0.1.36}/tests/test_zotero.py +0 -0
|
@@ -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
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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.
|
|
158
|
-
|
|
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.
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
210
|
-
index
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
982
|
-
|
|
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(
|
|
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(
|