pyDiffTools 0.1.42__tar.gz → 0.1.44__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.42/pyDiffTools.egg-info → pydifftools-0.1.44}/PKG-INFO +1 -1
- {pydifftools-0.1.42 → pydifftools-0.1.44/pyDiffTools.egg-info}/PKG-INFO +1 -1
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/continuous.py +141 -45
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/wrap_sentences.py +370 -5
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pyproject.toml +1 -1
- {pydifftools-0.1.42 → pydifftools-0.1.44}/tests/test_wrap_check.py +314 -6
- {pydifftools-0.1.42 → pydifftools-0.1.44}/LICENSE.md +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/MANIFEST.in +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/README.rst +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/_quarto.yml +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/example.qmd +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/example.tex +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/index.qmd +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/subproject1/independent.qmd +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/subproject1/index.qmd +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/subproject1/tasks.qmd +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/subproject1/test_include.qmd +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/example_notebook/project1/test_include.qmd +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pyDiffTools.egg-info/SOURCES.txt +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pyDiffTools.egg-info/dependency_links.txt +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pyDiffTools.egg-info/entry_points.txt +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pyDiffTools.egg-info/requires.txt +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pyDiffTools.egg-info/top_level.txt +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/__init__.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/bibliography.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/browser_lifecycle.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/check_numbers.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/citation_dialog.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/command_line.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/command_registry.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/comment_functions.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/comment_migration.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/comment_tags.lua +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/comment_tags_margin.lua +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/comment_tags_no_comments.lua +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/comment_toggle.js +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/comments.css +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/copy_files.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/diff-doc.js +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/doc_contents.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/flowchart/__init__.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/flowchart/comparison.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/flowchart/dot_to_yaml.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/flowchart/graph.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/flowchart/watch_graph.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/flowchart/wgrph_view.js +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/forward_search.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/git_gd.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/git_gd_history.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/git_gd_image.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/git_gd_qt.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/html_comments.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/html_uncomments.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/log_example.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/match_spaces.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/notebook/__init__.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/notebook/display.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/notebook/fast_build.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/notebook/tex_to_qmd.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/onewordify.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/onewordify_undo.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/outline.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/rearrange_tex.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/searchacro.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/separate_comments.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/split_conflict.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/unseparate_comments.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/update_check.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/xml2xlsx.vbs +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/pydifftools/zotero.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/setup.cfg +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/tests/test_bibliography.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/tests/test_browser_lifecycle.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/tests/test_check_single_use.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/tests/test_command_line_help.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/tests/test_comment_migration.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/tests/test_continuous_shutdown.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/tests/test_forward_search.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/tests/test_git_gd_history.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/tests/test_git_gd_image.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/tests/test_git_gd_qt.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/tests/test_rrng.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/tests/test_tex_to_qmd.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/tests/test_update_check.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/tests/test_wrapping.py +0 -0
- {pydifftools-0.1.42 → pydifftools-0.1.44}/tests/test_zotero.py +0 -0
|
@@ -132,7 +132,7 @@ def show_markdown_fix_dialog(report):
|
|
|
132
132
|
from html import escape
|
|
133
133
|
|
|
134
134
|
from PySide6.QtCore import Qt
|
|
135
|
-
from PySide6.QtGui import QFontDatabase
|
|
135
|
+
from PySide6.QtGui import QFontDatabase, QFontMetrics
|
|
136
136
|
from PySide6.QtWidgets import (
|
|
137
137
|
QApplication,
|
|
138
138
|
QDialog,
|
|
@@ -145,14 +145,8 @@ def show_markdown_fix_dialog(report):
|
|
|
145
145
|
|
|
146
146
|
fixes = report["fixes"]
|
|
147
147
|
warnings = report["warnings"]
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
for fix in fixes
|
|
151
|
-
] + [
|
|
152
|
-
{"record_type": "warning", **warning}
|
|
153
|
-
for warning in warnings
|
|
154
|
-
]
|
|
155
|
-
if not records:
|
|
148
|
+
layout = report.get("layout", [])
|
|
149
|
+
if not fixes and not warnings and not layout:
|
|
156
150
|
return
|
|
157
151
|
|
|
158
152
|
dialog = QDialog()
|
|
@@ -160,7 +154,54 @@ def show_markdown_fix_dialog(report):
|
|
|
160
154
|
QApplication.instance() or QApplication([])
|
|
161
155
|
)
|
|
162
156
|
dialog.setWindowTitle("Markdown source fixes")
|
|
163
|
-
|
|
157
|
+
fixed_font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
|
|
158
|
+
# {{{ group fixes of one kind onto pages that fill the screen
|
|
159
|
+
screen_height = dialog.screen().availableGeometry().height()
|
|
160
|
+
dialog.resize(900, int(screen_height * 0.85))
|
|
161
|
+
# leave room for the heading, explanation, button and margins
|
|
162
|
+
page_rows = max(
|
|
163
|
+
3,
|
|
164
|
+
int(
|
|
165
|
+
(screen_height * 0.85 - 120)
|
|
166
|
+
/ QFontMetrics(fixed_font).lineSpacing()
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
groups = {}
|
|
170
|
+
for fix in fixes:
|
|
171
|
+
groups.setdefault(fix["reason"], []).append(fix)
|
|
172
|
+
records = []
|
|
173
|
+
for reason, group in groups.items():
|
|
174
|
+
page = []
|
|
175
|
+
rows = 0
|
|
176
|
+
for fix in sorted(group, key=lambda fix: fix["line"]):
|
|
177
|
+
# a blank line separates each fix from the one before it, and a
|
|
178
|
+
# fix that only drops trailing spaces shows just its old line
|
|
179
|
+
fix_rows = (
|
|
180
|
+
len(fix["before"].splitlines() or [""])
|
|
181
|
+
+ (
|
|
182
|
+
0
|
|
183
|
+
if fix["after"] == fix["before"].rstrip()
|
|
184
|
+
else len(fix["after"].splitlines() or [""])
|
|
185
|
+
)
|
|
186
|
+
+ (1 if page else 0)
|
|
187
|
+
)
|
|
188
|
+
if page and rows + fix_rows > page_rows:
|
|
189
|
+
records.append(
|
|
190
|
+
{"record_type": "fix", "reason": reason, "fixes": page}
|
|
191
|
+
)
|
|
192
|
+
page = []
|
|
193
|
+
rows = 0
|
|
194
|
+
fix_rows -= 1
|
|
195
|
+
page.append(fix)
|
|
196
|
+
rows += fix_rows
|
|
197
|
+
records.append(
|
|
198
|
+
{"record_type": "fix", "reason": reason, "fixes": page}
|
|
199
|
+
)
|
|
200
|
+
records += [{"record_type": "layout", **notice} for notice in layout]
|
|
201
|
+
records += [
|
|
202
|
+
{"record_type": "warning", **warning} for warning in warnings
|
|
203
|
+
]
|
|
204
|
+
# }}}
|
|
164
205
|
layout = QVBoxLayout(dialog)
|
|
165
206
|
heading = QLabel()
|
|
166
207
|
heading.setWordWrap(True)
|
|
@@ -175,14 +216,10 @@ def show_markdown_fix_dialog(report):
|
|
|
175
216
|
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight
|
|
176
217
|
)
|
|
177
218
|
line_numbers.setStyleSheet("color: #666; padding-right: 8px;")
|
|
178
|
-
line_numbers.setFont(
|
|
179
|
-
QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
|
|
180
|
-
)
|
|
219
|
+
line_numbers.setFont(fixed_font)
|
|
181
220
|
preview = QTextEdit()
|
|
182
221
|
preview.setReadOnly(True)
|
|
183
|
-
preview.setFont(
|
|
184
|
-
QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
|
|
185
|
-
)
|
|
222
|
+
preview.setFont(fixed_font)
|
|
186
223
|
preview_row.addWidget(line_numbers)
|
|
187
224
|
preview_row.addWidget(preview, 1)
|
|
188
225
|
layout.addLayout(preview_row, 1)
|
|
@@ -194,37 +231,94 @@ def show_markdown_fix_dialog(report):
|
|
|
194
231
|
|
|
195
232
|
def show_record():
|
|
196
233
|
record = records[index]
|
|
197
|
-
|
|
234
|
+
page = (
|
|
235
|
+
f" (page {index + 1} of {len(records)})"
|
|
236
|
+
if len(records) > 1
|
|
237
|
+
else ""
|
|
238
|
+
)
|
|
198
239
|
if record["record_type"] == "fix":
|
|
199
|
-
|
|
240
|
+
count = len(record["fixes"])
|
|
241
|
+
heading.setText(
|
|
242
|
+
"I fixed this source line automatically." + page
|
|
243
|
+
if count == 1
|
|
244
|
+
else f"I fixed these {count} source lines automatically."
|
|
245
|
+
+ page
|
|
246
|
+
)
|
|
200
247
|
details.setText(record["reason"])
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
248
|
+
line_numbers.setVisible(False)
|
|
249
|
+
# {{{ show each fix as numbered before (−) and after (+) lines
|
|
250
|
+
entries = []
|
|
251
|
+
for fix in record["fixes"]:
|
|
252
|
+
rows = []
|
|
253
|
+
for offset, value in enumerate(
|
|
254
|
+
fix["before"].splitlines() or [""]
|
|
255
|
+
):
|
|
256
|
+
stripped = value.rstrip(" ")
|
|
257
|
+
rows.append(
|
|
258
|
+
f"<span style='color:#666'>{fix['line'] + offset:>6}"
|
|
259
|
+
"</span> − "
|
|
260
|
+
+ escape(stripped)
|
|
261
|
+
# make stray trailing spaces visible
|
|
262
|
+
+ "<span style='background:#fbb'>·</span>"
|
|
263
|
+
* (len(value) - len(stripped))
|
|
264
|
+
)
|
|
265
|
+
for offset, value in enumerate(
|
|
266
|
+
[]
|
|
267
|
+
if fix["after"] == fix["before"].rstrip()
|
|
268
|
+
else fix["after"].split("\n")
|
|
269
|
+
):
|
|
270
|
+
rows.append(
|
|
271
|
+
f"<span style='color:#666'>{fix['line'] + offset:>6}"
|
|
272
|
+
"</span> "
|
|
273
|
+
+ ("+ " if offset == 0 else "↳ ")
|
|
274
|
+
+ escape(value)
|
|
275
|
+
)
|
|
276
|
+
entries.append("<br>".join(rows))
|
|
277
|
+
preview.setHtml(
|
|
278
|
+
"<pre style='font-family:monospace; margin:0'>"
|
|
279
|
+
+ "<br><br>".join(entries)
|
|
280
|
+
+ "</pre>"
|
|
281
|
+
)
|
|
282
|
+
# }}}
|
|
283
|
+
button.setText("Next" if index + 1 < len(records) else "Done")
|
|
284
|
+
elif record["record_type"] == "layout":
|
|
285
|
+
heading.setText(
|
|
286
|
+
"These line breaks differ from git HEAD in "
|
|
287
|
+
f"{record['moves']} places." + page
|
|
288
|
+
)
|
|
289
|
+
details.setText(
|
|
290
|
+
"Keeping these lines as they are in git HEAD would need "
|
|
291
|
+
f"{record['moves']} line breaks moved, so I left them as you "
|
|
292
|
+
"wrote them. If you did not mean to rearrange these lines, "
|
|
293
|
+
"lay them out as suggested."
|
|
294
|
+
)
|
|
295
|
+
line_numbers.setVisible(False)
|
|
296
|
+
# {{{ show HEAD, the current source and the suggested layout
|
|
297
|
+
sections = []
|
|
298
|
+
for title, text, numbered in (
|
|
299
|
+
("git HEAD:", record["head"], False),
|
|
300
|
+
("Now:", record["current"], True),
|
|
301
|
+
("Suggested:", record["suggested"], True),
|
|
302
|
+
):
|
|
303
|
+
rows = [escape(title)]
|
|
304
|
+
for offset, value in enumerate(text.split("\n")):
|
|
305
|
+
number = record["line"] + offset if numbered else ""
|
|
306
|
+
rows.append(
|
|
307
|
+
f"<span style='color:#666'>{number:>6}</span> "
|
|
308
|
+
+ escape(value)
|
|
309
|
+
)
|
|
310
|
+
sections.append("<br>".join(rows))
|
|
311
|
+
preview.setHtml(
|
|
312
|
+
"<pre style='font-family:monospace; margin:0'>"
|
|
313
|
+
+ "<br><br>".join(sections)
|
|
314
|
+
+ "</pre>"
|
|
315
|
+
)
|
|
316
|
+
# }}}
|
|
225
317
|
button.setText("Next" if index + 1 < len(records) else "Done")
|
|
226
318
|
else:
|
|
227
|
-
|
|
319
|
+
line = record["line"]
|
|
320
|
+
heading.setText("This source span is not closed." + page)
|
|
321
|
+
line_numbers.setVisible(True)
|
|
228
322
|
if record["kind"] == "math":
|
|
229
323
|
delimiter = record["delimiter"]
|
|
230
324
|
details.setText(
|
|
@@ -311,9 +405,11 @@ def run_pandoc(
|
|
|
311
405
|
|
|
312
406
|
automatic_fixes_were_made = False
|
|
313
407
|
while True:
|
|
314
|
-
report = autofix_markdown_file(
|
|
408
|
+
report = autofix_markdown_file(
|
|
409
|
+
filename, wrapnumber=wrapnumber, git_head=True
|
|
410
|
+
)
|
|
315
411
|
automatic_fixes_were_made |= bool(report["fixes"])
|
|
316
|
-
if report["fixes"] or report["warnings"]:
|
|
412
|
+
if report["fixes"] or report["warnings"] or report["layout"]:
|
|
317
413
|
user_says_fixed = show_markdown_fix_dialog(report)
|
|
318
414
|
if report["warnings"]:
|
|
319
415
|
if not user_says_fixed:
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import re
|
|
3
|
+
import subprocess
|
|
2
4
|
import sys
|
|
3
5
|
import itertools
|
|
4
6
|
from dataclasses import dataclass, field
|
|
7
|
+
from difflib import SequenceMatcher
|
|
5
8
|
|
|
6
9
|
from .command_registry import register_command
|
|
7
10
|
|
|
@@ -1190,10 +1193,15 @@ def apply_markdown_issue_fix(content, line_number, message):
|
|
|
1190
1193
|
else:
|
|
1191
1194
|
span = found.span("gap")
|
|
1192
1195
|
joined = ""
|
|
1193
|
-
before = content[found.start() : found.end()]
|
|
1194
1196
|
updated = content[: span[0]] + joined + content[span[1] :]
|
|
1197
|
+
# report the whole source lines around the join
|
|
1198
|
+
line_start = content.rfind("\n", 0, found.start()) + 1
|
|
1199
|
+
line_end = content.find("\n", max(found.end(), span[1]))
|
|
1200
|
+
if line_end == -1:
|
|
1201
|
+
line_end = len(content)
|
|
1202
|
+
before = content[line_start:line_end]
|
|
1195
1203
|
after = updated[
|
|
1196
|
-
|
|
1204
|
+
line_start : line_end - (span[1] - span[0]) + len(joined)
|
|
1197
1205
|
]
|
|
1198
1206
|
return (
|
|
1199
1207
|
updated,
|
|
@@ -1284,11 +1292,364 @@ def apply_markdown_issue_fix(content, line_number, message):
|
|
|
1284
1292
|
return "".join(lines), old_line, after, reason
|
|
1285
1293
|
|
|
1286
1294
|
|
|
1287
|
-
def autofix_markdown_file(
|
|
1288
|
-
|
|
1295
|
+
def autofix_markdown_file(
|
|
1296
|
+
filename, wrapnumber=55, punctuation_slop=20, git_head=False
|
|
1297
|
+
):
|
|
1298
|
+
"""Apply safe source line breaks and report unfinished source spans.
|
|
1299
|
+
|
|
1300
|
+
With `git_head`, line breaks in the hunks that differ from the file's
|
|
1301
|
+
git HEAD version are first placed to keep that diff small (see
|
|
1302
|
+
"layout" in the returned report for hunks that would need more than
|
|
1303
|
+
one line break moved).
|
|
1304
|
+
"""
|
|
1289
1305
|
with open(filename, encoding="utf-8", newline="") as fp:
|
|
1290
1306
|
content = fp.read()
|
|
1291
1307
|
fixes = []
|
|
1308
|
+
layout = []
|
|
1309
|
+
baseline = None
|
|
1310
|
+
if git_head:
|
|
1311
|
+
directory, basename = os.path.split(os.path.abspath(filename))
|
|
1312
|
+
try:
|
|
1313
|
+
with subprocess.Popen(
|
|
1314
|
+
["git", "-C", directory, "show", "HEAD:./" + basename],
|
|
1315
|
+
stdout=subprocess.PIPE,
|
|
1316
|
+
stderr=subprocess.DEVNULL,
|
|
1317
|
+
) as shown:
|
|
1318
|
+
output = shown.communicate()[0]
|
|
1319
|
+
if shown.returncode == 0:
|
|
1320
|
+
baseline = output.decode("utf-8")
|
|
1321
|
+
except FileNotFoundError:
|
|
1322
|
+
# git is not installed
|
|
1323
|
+
pass
|
|
1324
|
+
if baseline is not None:
|
|
1325
|
+
# {{{ place line breaks in changed hunks to keep the HEAD diff small
|
|
1326
|
+
|
|
1327
|
+
def mask_comment(match):
|
|
1328
|
+
return "".join("\n" if char == "\n" else " " for char in match[0])
|
|
1329
|
+
|
|
1330
|
+
prefix_pattern = re.compile(
|
|
1331
|
+
r"^(?P<quote>(?:[ \t]*>[ \t]?)*)(?P<indent>[ \t]*)"
|
|
1332
|
+
r"(?:(?P<marker>[-+*]|[0-9]+[.)])(?P<space>[ \t]+))?"
|
|
1333
|
+
)
|
|
1334
|
+
|
|
1335
|
+
def line_tokens(text):
|
|
1336
|
+
"""Words of each line, after its list or quote prefix."""
|
|
1337
|
+
masked = re.sub(
|
|
1338
|
+
r"<!--.*?(?:-->|\Z)", mask_comment, text, flags=re.DOTALL
|
|
1339
|
+
)
|
|
1340
|
+
result = []
|
|
1341
|
+
start = 0
|
|
1342
|
+
for number, line in enumerate(masked.splitlines(keepends=True)):
|
|
1343
|
+
prefix = prefix_pattern.match(line)
|
|
1344
|
+
result.extend(
|
|
1345
|
+
(number, start + found.start(), start + found.end())
|
|
1346
|
+
for found in WORD.finditer(line, prefix.end())
|
|
1347
|
+
)
|
|
1348
|
+
start += len(line)
|
|
1349
|
+
return masked, result
|
|
1350
|
+
|
|
1351
|
+
head_lines = baseline.splitlines(keepends=True)
|
|
1352
|
+
lines = content.splitlines(keepends=True)
|
|
1353
|
+
line_starts = [0]
|
|
1354
|
+
for line in lines:
|
|
1355
|
+
line_starts.append(line_starts[-1] + len(line))
|
|
1356
|
+
masked, tokens = line_tokens(content)
|
|
1357
|
+
prose = [
|
|
1358
|
+
allowed and line.strip() != ""
|
|
1359
|
+
for allowed, line in classify_lines(masked, strict=False)
|
|
1360
|
+
]
|
|
1361
|
+
prefixes = [prefix_pattern.match(line) for line in lines]
|
|
1362
|
+
continuations = [
|
|
1363
|
+
found["quote"]
|
|
1364
|
+
+ found["indent"]
|
|
1365
|
+
+ (
|
|
1366
|
+
" " * (len(found["marker"]) + len(found["space"]))
|
|
1367
|
+
if found["marker"]
|
|
1368
|
+
else ""
|
|
1369
|
+
)
|
|
1370
|
+
for found in prefixes
|
|
1371
|
+
]
|
|
1372
|
+
results = []
|
|
1373
|
+
for operation, i1, i2, j1, j2 in SequenceMatcher(
|
|
1374
|
+
None, head_lines, lines, autojunk=False
|
|
1375
|
+
).get_opcodes():
|
|
1376
|
+
if operation == "equal" or j1 == j2:
|
|
1377
|
+
continue
|
|
1378
|
+
hunk_start, hunk_end = line_starts[j1], line_starts[j2]
|
|
1379
|
+
hunk_tokens = [token for token in tokens if j1 <= token[0] < j2]
|
|
1380
|
+
head_text = "".join(head_lines[i1:i2])
|
|
1381
|
+
head_masked, head_tokens = line_tokens(head_text)
|
|
1382
|
+
matched = set()
|
|
1383
|
+
for block in SequenceMatcher(
|
|
1384
|
+
None,
|
|
1385
|
+
[head_masked[a:b] for _, a, b in head_tokens],
|
|
1386
|
+
[masked[a:b] for _, a, b in hunk_tokens],
|
|
1387
|
+
autojunk=False,
|
|
1388
|
+
).get_matching_blocks():
|
|
1389
|
+
matched.update(range(block.b, block.b + block.size))
|
|
1390
|
+
# {{{ find the gaps between prose words where a break may move
|
|
1391
|
+
gaps = []
|
|
1392
|
+
for k in range(len(hunk_tokens) - 1):
|
|
1393
|
+
left_line, _, left_end = hunk_tokens[k]
|
|
1394
|
+
right_line, right_start, _ = hunk_tokens[k + 1]
|
|
1395
|
+
raw = content[left_end:right_start]
|
|
1396
|
+
if not (prose[left_line] and prose[right_line]):
|
|
1397
|
+
continue
|
|
1398
|
+
if left_line == right_line:
|
|
1399
|
+
if raw.strip(" \t"):
|
|
1400
|
+
continue
|
|
1401
|
+
elif (
|
|
1402
|
+
right_line != left_line + 1
|
|
1403
|
+
or prefixes[right_line]["marker"]
|
|
1404
|
+
or prefixes[right_line].group(0)
|
|
1405
|
+
!= continuations[left_line]
|
|
1406
|
+
or raw.strip(" \t\r\n") != prefixes[right_line][
|
|
1407
|
+
"quote"
|
|
1408
|
+
].strip(" \t")
|
|
1409
|
+
):
|
|
1410
|
+
continue
|
|
1411
|
+
gaps.append(
|
|
1412
|
+
{
|
|
1413
|
+
"start": left_end,
|
|
1414
|
+
"stop": right_start,
|
|
1415
|
+
"newline": "\n" in raw,
|
|
1416
|
+
"line": left_line,
|
|
1417
|
+
# breaks between two newly written words are the
|
|
1418
|
+
# writer's choice; only lint rules move those
|
|
1419
|
+
"free": k not in matched and k + 1 not in matched,
|
|
1420
|
+
"sentence": bool(
|
|
1421
|
+
re.search(
|
|
1422
|
+
"(" + SENTENCE_END + r")$",
|
|
1423
|
+
content[max(0, left_end - 4) : left_end],
|
|
1424
|
+
)
|
|
1425
|
+
),
|
|
1426
|
+
}
|
|
1427
|
+
)
|
|
1428
|
+
# }}}
|
|
1429
|
+
# a hunk this large is a wholesale reflow, not a stray line
|
|
1430
|
+
# break, so leave it to the lint rules alone
|
|
1431
|
+
if not gaps or len(gaps) > 400:
|
|
1432
|
+
continue
|
|
1433
|
+
ending = "\r\n" if "\r\n" in content[hunk_start:hunk_end] else "\n"
|
|
1434
|
+
rendered = {}
|
|
1435
|
+
scores = {}
|
|
1436
|
+
|
|
1437
|
+
def render(flags):
|
|
1438
|
+
"""The hunk with each gap a space or a line break, and the
|
|
1439
|
+
(row, column) where each gap starts."""
|
|
1440
|
+
key = tuple(flags)
|
|
1441
|
+
if key not in rendered:
|
|
1442
|
+
output = []
|
|
1443
|
+
columns = []
|
|
1444
|
+
where = [0, 0] # row and column of the output's end
|
|
1445
|
+
|
|
1446
|
+
def append(piece):
|
|
1447
|
+
output.append(piece)
|
|
1448
|
+
if "\n" in piece:
|
|
1449
|
+
where[0] += piece.count("\n")
|
|
1450
|
+
where[1] = len(piece) - piece.rfind("\n") - 1
|
|
1451
|
+
else:
|
|
1452
|
+
where[1] += len(piece)
|
|
1453
|
+
|
|
1454
|
+
position = hunk_start
|
|
1455
|
+
for gap, flag in zip(gaps, flags):
|
|
1456
|
+
append(content[position : gap["start"]])
|
|
1457
|
+
columns.append(tuple(where))
|
|
1458
|
+
if flag == gap["newline"]:
|
|
1459
|
+
append(content[gap["start"] : gap["stop"]])
|
|
1460
|
+
elif flag:
|
|
1461
|
+
append(ending + continuations[gap["line"]])
|
|
1462
|
+
else:
|
|
1463
|
+
append(" ")
|
|
1464
|
+
position = gap["stop"]
|
|
1465
|
+
append(content[position:hunk_end])
|
|
1466
|
+
rendered[key] = ("".join(output), columns)
|
|
1467
|
+
return rendered[key]
|
|
1468
|
+
|
|
1469
|
+
def kept(text):
|
|
1470
|
+
"""How many HEAD lines the hunk keeps unchanged.
|
|
1471
|
+
|
|
1472
|
+
Fewer lines alone does not count as closer to HEAD, so
|
|
1473
|
+
rewritten text keeps the writer's line breaks.
|
|
1474
|
+
"""
|
|
1475
|
+
if text not in scores:
|
|
1476
|
+
scores[text] = sum(
|
|
1477
|
+
block.size
|
|
1478
|
+
for block in SequenceMatcher(
|
|
1479
|
+
None,
|
|
1480
|
+
[line.rstrip() for line in head_text.splitlines()],
|
|
1481
|
+
[line.rstrip() for line in text.splitlines()],
|
|
1482
|
+
autojunk=False,
|
|
1483
|
+
).get_matching_blocks()
|
|
1484
|
+
)
|
|
1485
|
+
return scores[text]
|
|
1486
|
+
|
|
1487
|
+
def too_long(flags):
|
|
1488
|
+
"""Rendered lines past the width that a gap could break."""
|
|
1489
|
+
text, columns = render(flags)
|
|
1490
|
+
rendered = text.splitlines()
|
|
1491
|
+
return [
|
|
1492
|
+
number
|
|
1493
|
+
for number in sorted(
|
|
1494
|
+
{row for (row, _), flag in zip(columns, flags)
|
|
1495
|
+
if not flag}
|
|
1496
|
+
)
|
|
1497
|
+
if len(rendered[number].rstrip()) > wrapnumber
|
|
1498
|
+
]
|
|
1499
|
+
|
|
1500
|
+
original = [gap["newline"] for gap in gaps]
|
|
1501
|
+
# {{{ lint first: a break after each sentence, and a break in
|
|
1502
|
+
# each overlong line wherever it keeps the most HEAD lines
|
|
1503
|
+
base = [
|
|
1504
|
+
flag or gap["sentence"] for flag, gap in zip(original, gaps)
|
|
1505
|
+
]
|
|
1506
|
+
for _ in range(len(gaps)):
|
|
1507
|
+
overlong = too_long(base)
|
|
1508
|
+
if not overlong:
|
|
1509
|
+
break
|
|
1510
|
+
text, columns = render(base)
|
|
1511
|
+
row = overlong[0]
|
|
1512
|
+
line_text = text.splitlines()[row]
|
|
1513
|
+
prefix = prefix_pattern.match(line_text).group(0)
|
|
1514
|
+
words = WORD.findall(line_text[len(prefix) :])
|
|
1515
|
+
upto = _next_break(
|
|
1516
|
+
words,
|
|
1517
|
+
wrapnumber - len(prefix),
|
|
1518
|
+
punctuation_slop,
|
|
1519
|
+
_math_boundaries(words),
|
|
1520
|
+
0,
|
|
1521
|
+
)
|
|
1522
|
+
wr_column = len(prefix) + len(" ".join(words[: upto + 1]))
|
|
1523
|
+
candidates = [
|
|
1524
|
+
number
|
|
1525
|
+
for number, ((gap_row, column), flag) in enumerate(
|
|
1526
|
+
zip(columns, base)
|
|
1527
|
+
)
|
|
1528
|
+
if gap_row == row and not flag
|
|
1529
|
+
]
|
|
1530
|
+
fitting = [
|
|
1531
|
+
number
|
|
1532
|
+
for number in candidates
|
|
1533
|
+
if columns[number][1] <= wrapnumber
|
|
1534
|
+
] or candidates[:1]
|
|
1535
|
+
|
|
1536
|
+
def with_break(number):
|
|
1537
|
+
return [
|
|
1538
|
+
flag or other == number
|
|
1539
|
+
for other, flag in enumerate(base)
|
|
1540
|
+
]
|
|
1541
|
+
|
|
1542
|
+
base = with_break(
|
|
1543
|
+
min(
|
|
1544
|
+
fitting,
|
|
1545
|
+
key=lambda number: (
|
|
1546
|
+
-kept(render(with_break(number))[0]),
|
|
1547
|
+
abs(columns[number][1] - wr_column),
|
|
1548
|
+
),
|
|
1549
|
+
)
|
|
1550
|
+
)
|
|
1551
|
+
# }}}
|
|
1552
|
+
# {{{ then join or move breaks next to HEAD's words while that
|
|
1553
|
+
# restores HEAD lines; only the lint rules above may add a break
|
|
1554
|
+
best = list(base)
|
|
1555
|
+
best_kept = kept(render(best)[0])
|
|
1556
|
+
moves = 0
|
|
1557
|
+
for _ in range(len(gaps)):
|
|
1558
|
+
trials = []
|
|
1559
|
+
for number, gap in enumerate(gaps):
|
|
1560
|
+
if (
|
|
1561
|
+
not best[number]
|
|
1562
|
+
or gap["free"]
|
|
1563
|
+
or gap["sentence"]
|
|
1564
|
+
):
|
|
1565
|
+
continue
|
|
1566
|
+
joined = list(best)
|
|
1567
|
+
joined[number] = False
|
|
1568
|
+
trials.append(joined)
|
|
1569
|
+
# or slide the break to another gap of the joined line
|
|
1570
|
+
columns = render(joined)[1]
|
|
1571
|
+
for other, flag in enumerate(joined):
|
|
1572
|
+
if (
|
|
1573
|
+
not flag
|
|
1574
|
+
and other != number
|
|
1575
|
+
and columns[other][0] == columns[number][0]
|
|
1576
|
+
):
|
|
1577
|
+
moved = list(joined)
|
|
1578
|
+
moved[other] = True
|
|
1579
|
+
trials.append(moved)
|
|
1580
|
+
limit = len(too_long(best))
|
|
1581
|
+
scored = [
|
|
1582
|
+
(kept(render(trial)[0]), trial)
|
|
1583
|
+
for trial in trials
|
|
1584
|
+
if len(too_long(trial)) <= limit
|
|
1585
|
+
]
|
|
1586
|
+
if not scored:
|
|
1587
|
+
break
|
|
1588
|
+
trial_kept, trial = max(scored, key=lambda item: item[0])
|
|
1589
|
+
if trial_kept <= best_kept:
|
|
1590
|
+
break
|
|
1591
|
+
best_kept, best = trial_kept, trial
|
|
1592
|
+
moves += 1
|
|
1593
|
+
# }}}
|
|
1594
|
+
chosen = best if moves == 1 else base
|
|
1595
|
+
results.append(
|
|
1596
|
+
{
|
|
1597
|
+
"j1": j1,
|
|
1598
|
+
"j2": j2,
|
|
1599
|
+
"start": hunk_start,
|
|
1600
|
+
"stop": hunk_end,
|
|
1601
|
+
"old": content[hunk_start:hunk_end],
|
|
1602
|
+
"new": render(chosen)[0],
|
|
1603
|
+
"linted": base != original,
|
|
1604
|
+
"moved": moves == 1,
|
|
1605
|
+
"head": head_text,
|
|
1606
|
+
"suggested": render(best)[0] if moves > 1 else None,
|
|
1607
|
+
"moves": moves,
|
|
1608
|
+
}
|
|
1609
|
+
)
|
|
1610
|
+
shift = 0
|
|
1611
|
+
for result in results:
|
|
1612
|
+
line = result["j1"] + 1 + shift
|
|
1613
|
+
if result["new"] != result["old"]:
|
|
1614
|
+
reasons = []
|
|
1615
|
+
if result["linted"]:
|
|
1616
|
+
reasons.append(
|
|
1617
|
+
"This edit made a line too long or ended a sentence "
|
|
1618
|
+
"mid-line. I broke it where it keeps the most lines "
|
|
1619
|
+
"unchanged from git HEAD."
|
|
1620
|
+
)
|
|
1621
|
+
if result["moved"]:
|
|
1622
|
+
reasons.append(
|
|
1623
|
+
"This edit moved a line break away from where git "
|
|
1624
|
+
"HEAD has it. I put it back so the unchanged lines "
|
|
1625
|
+
"stay unchanged."
|
|
1626
|
+
)
|
|
1627
|
+
fixes.append(
|
|
1628
|
+
{
|
|
1629
|
+
"line": line,
|
|
1630
|
+
"reason": " ".join(reasons),
|
|
1631
|
+
"before": result["old"].rstrip("\r\n"),
|
|
1632
|
+
"after": result["new"].rstrip("\r\n"),
|
|
1633
|
+
}
|
|
1634
|
+
)
|
|
1635
|
+
if result["suggested"] is not None:
|
|
1636
|
+
layout.append(
|
|
1637
|
+
{
|
|
1638
|
+
"line": line,
|
|
1639
|
+
"moves": result["moves"],
|
|
1640
|
+
"head": result["head"].rstrip("\r\n"),
|
|
1641
|
+
"current": result["new"].rstrip("\r\n"),
|
|
1642
|
+
"suggested": result["suggested"].rstrip("\r\n"),
|
|
1643
|
+
}
|
|
1644
|
+
)
|
|
1645
|
+
shift += result["new"].count("\n") - result["old"].count("\n")
|
|
1646
|
+
for result in reversed(results):
|
|
1647
|
+
content = (
|
|
1648
|
+
content[: result["start"]]
|
|
1649
|
+
+ result["new"]
|
|
1650
|
+
+ content[result["stop"] :]
|
|
1651
|
+
)
|
|
1652
|
+
# }}}
|
|
1292
1653
|
# Lint once, fix every flagged line, then lint again, since fixing one
|
|
1293
1654
|
# issue at a time re-lints the whole file for every single fix.
|
|
1294
1655
|
for _ in range(100):
|
|
@@ -1333,7 +1694,11 @@ def autofix_markdown_file(filename, wrapnumber=55, punctuation_slop=20):
|
|
|
1333
1694
|
if fixes:
|
|
1334
1695
|
with open(filename, "w", encoding="utf-8", newline="") as fp:
|
|
1335
1696
|
fp.write(content)
|
|
1336
|
-
return {
|
|
1697
|
+
return {
|
|
1698
|
+
"fixes": fixes,
|
|
1699
|
+
"warnings": unclosed_markdown_spans(content),
|
|
1700
|
+
"layout": layout,
|
|
1701
|
+
}
|
|
1337
1702
|
|
|
1338
1703
|
|
|
1339
1704
|
@register_command(
|