pyDiffTools 0.1.43__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.43/pyDiffTools.egg-info → pydifftools-0.1.44}/PKG-INFO +1 -1
- {pydifftools-0.1.43 → pydifftools-0.1.44/pyDiffTools.egg-info}/PKG-INFO +1 -1
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/continuous.py +41 -3
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/wrap_sentences.py +363 -3
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pyproject.toml +1 -1
- {pydifftools-0.1.43 → pydifftools-0.1.44}/tests/test_wrap_check.py +249 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/LICENSE.md +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/MANIFEST.in +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/README.rst +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/_quarto.yml +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/example.qmd +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/example.tex +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/index.qmd +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/subproject1/independent.qmd +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/subproject1/index.qmd +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/subproject1/tasks.qmd +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/subproject1/test_include.qmd +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/test_include.qmd +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pyDiffTools.egg-info/SOURCES.txt +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pyDiffTools.egg-info/dependency_links.txt +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pyDiffTools.egg-info/entry_points.txt +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pyDiffTools.egg-info/requires.txt +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pyDiffTools.egg-info/top_level.txt +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/__init__.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/bibliography.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/browser_lifecycle.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/check_numbers.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/citation_dialog.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/command_line.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/command_registry.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/comment_functions.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/comment_migration.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/comment_tags.lua +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/comment_tags_margin.lua +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/comment_tags_no_comments.lua +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/comment_toggle.js +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/comments.css +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/copy_files.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/diff-doc.js +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/doc_contents.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/flowchart/__init__.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/flowchart/comparison.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/flowchart/dot_to_yaml.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/flowchart/graph.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/flowchart/watch_graph.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/flowchart/wgrph_view.js +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/forward_search.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/git_gd.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/git_gd_history.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/git_gd_image.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/git_gd_qt.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/html_comments.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/html_uncomments.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/log_example.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/match_spaces.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/notebook/__init__.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/notebook/display.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/notebook/fast_build.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/notebook/tex_to_qmd.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/onewordify.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/onewordify_undo.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/outline.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/rearrange_tex.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/searchacro.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/separate_comments.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/split_conflict.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/unseparate_comments.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/update_check.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/xml2xlsx.vbs +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/pydifftools/zotero.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/setup.cfg +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/tests/test_bibliography.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/tests/test_browser_lifecycle.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/tests/test_check_single_use.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/tests/test_command_line_help.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/tests/test_comment_migration.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/tests/test_continuous_shutdown.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/tests/test_forward_search.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/tests/test_git_gd_history.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/tests/test_git_gd_image.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/tests/test_git_gd_qt.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/tests/test_rrng.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/tests/test_tex_to_qmd.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/tests/test_update_check.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/tests/test_wrapping.py +0 -0
- {pydifftools-0.1.43 → pydifftools-0.1.44}/tests/test_zotero.py +0 -0
|
@@ -145,7 +145,8 @@ def show_markdown_fix_dialog(report):
|
|
|
145
145
|
|
|
146
146
|
fixes = report["fixes"]
|
|
147
147
|
warnings = report["warnings"]
|
|
148
|
-
|
|
148
|
+
layout = report.get("layout", [])
|
|
149
|
+
if not fixes and not warnings and not layout:
|
|
149
150
|
return
|
|
150
151
|
|
|
151
152
|
dialog = QDialog()
|
|
@@ -196,6 +197,7 @@ def show_markdown_fix_dialog(report):
|
|
|
196
197
|
records.append(
|
|
197
198
|
{"record_type": "fix", "reason": reason, "fixes": page}
|
|
198
199
|
)
|
|
200
|
+
records += [{"record_type": "layout", **notice} for notice in layout]
|
|
199
201
|
records += [
|
|
200
202
|
{"record_type": "warning", **warning} for warning in warnings
|
|
201
203
|
]
|
|
@@ -279,6 +281,40 @@ def show_markdown_fix_dialog(report):
|
|
|
279
281
|
)
|
|
280
282
|
# }}}
|
|
281
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
|
+
# }}}
|
|
317
|
+
button.setText("Next" if index + 1 < len(records) else "Done")
|
|
282
318
|
else:
|
|
283
319
|
line = record["line"]
|
|
284
320
|
heading.setText("This source span is not closed." + page)
|
|
@@ -369,9 +405,11 @@ def run_pandoc(
|
|
|
369
405
|
|
|
370
406
|
automatic_fixes_were_made = False
|
|
371
407
|
while True:
|
|
372
|
-
report = autofix_markdown_file(
|
|
408
|
+
report = autofix_markdown_file(
|
|
409
|
+
filename, wrapnumber=wrapnumber, git_head=True
|
|
410
|
+
)
|
|
373
411
|
automatic_fixes_were_made |= bool(report["fixes"])
|
|
374
|
-
if report["fixes"] or report["warnings"]:
|
|
412
|
+
if report["fixes"] or report["warnings"] or report["layout"]:
|
|
375
413
|
user_says_fixed = show_markdown_fix_dialog(report)
|
|
376
414
|
if report["warnings"]:
|
|
377
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
|
|
|
@@ -1289,11 +1292,364 @@ def apply_markdown_issue_fix(content, line_number, message):
|
|
|
1289
1292
|
return "".join(lines), old_line, after, reason
|
|
1290
1293
|
|
|
1291
1294
|
|
|
1292
|
-
def autofix_markdown_file(
|
|
1293
|
-
|
|
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
|
+
"""
|
|
1294
1305
|
with open(filename, encoding="utf-8", newline="") as fp:
|
|
1295
1306
|
content = fp.read()
|
|
1296
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
|
+
# }}}
|
|
1297
1653
|
# Lint once, fix every flagged line, then lint again, since fixing one
|
|
1298
1654
|
# issue at a time re-lints the whole file for every single fix.
|
|
1299
1655
|
for _ in range(100):
|
|
@@ -1338,7 +1694,11 @@ def autofix_markdown_file(filename, wrapnumber=55, punctuation_slop=20):
|
|
|
1338
1694
|
if fixes:
|
|
1339
1695
|
with open(filename, "w", encoding="utf-8", newline="") as fp:
|
|
1340
1696
|
fp.write(content)
|
|
1341
|
-
return {
|
|
1697
|
+
return {
|
|
1698
|
+
"fixes": fixes,
|
|
1699
|
+
"warnings": unclosed_markdown_spans(content),
|
|
1700
|
+
"layout": layout,
|
|
1701
|
+
}
|
|
1342
1702
|
|
|
1343
1703
|
|
|
1344
1704
|
@register_command(
|
|
@@ -660,3 +660,252 @@ def test_wrchk_latex_document(tmp_path):
|
|
|
660
660
|
wrchk(str(path), wrapnumber=30, punctuation_slop=20)
|
|
661
661
|
wr(str(path), wrapnumber=30)
|
|
662
662
|
wrchk(str(path), wrapnumber=30)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
# {{{ keep line breaks close to git HEAD
|
|
666
|
+
# from RM_ESR, wrapped the way the manuscript already is
|
|
667
|
+
HEAD_PARAGRAPH = (
|
|
668
|
+
"Existing Python libraries enable rapid development of a script\n"
|
|
669
|
+
"that estimates the correlation time of these spectra.\n"
|
|
670
|
+
"This script determines the field positions and amplitudes of the\n"
|
|
671
|
+
"spectral lines for an entire variable-temperature experiment.\n"
|
|
672
|
+
"It implements spline-based smoothing of the spectra in order to\n"
|
|
673
|
+
"minimize the impact of any noise as part of this determination.\n"
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def committed_source(tmp_path, text):
|
|
678
|
+
"""A content.md committed to a fresh git repository."""
|
|
679
|
+
subprocess.run(["git", "init", "-q", str(tmp_path)], check=True)
|
|
680
|
+
path = tmp_path / "content.md"
|
|
681
|
+
path.write_text(text)
|
|
682
|
+
subprocess.run(
|
|
683
|
+
["git", "-C", str(tmp_path), "add", "content.md"], check=True
|
|
684
|
+
)
|
|
685
|
+
subprocess.run(
|
|
686
|
+
[
|
|
687
|
+
"git", "-C", str(tmp_path), "-c", "user.name=t",
|
|
688
|
+
"-c", "user.email=t@example.org", "commit", "-q", "-m", "head",
|
|
689
|
+
],
|
|
690
|
+
check=True,
|
|
691
|
+
)
|
|
692
|
+
return path
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def numstat(path):
|
|
696
|
+
"""(added, removed) lines against git HEAD."""
|
|
697
|
+
output = subprocess.run(
|
|
698
|
+
["git", "-C", str(path.parent), "diff", "--numstat", "HEAD"],
|
|
699
|
+
capture_output=True, text=True, check=True,
|
|
700
|
+
).stdout.split()
|
|
701
|
+
return (int(output[0]), int(output[1])) if output else (0, 0)
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def test_words_appended_past_the_width_become_one_new_line(tmp_path):
|
|
705
|
+
path = committed_source(tmp_path, HEAD_PARAGRAPH)
|
|
706
|
+
path.write_text(
|
|
707
|
+
HEAD_PARAGRAPH.replace(
|
|
708
|
+
"of these spectra.\n", "of these spectra across all samples.\n"
|
|
709
|
+
).replace(
|
|
710
|
+
"rapid development of a script\n",
|
|
711
|
+
"rapid development of a script in a few lines\n",
|
|
712
|
+
)
|
|
713
|
+
)
|
|
714
|
+
# the first edit fits; the second pushes a line past 79 characters
|
|
715
|
+
path.write_text(
|
|
716
|
+
path.read_text().replace(
|
|
717
|
+
"of the\nspectral",
|
|
718
|
+
"of the three hyperfine lines, and of the\nspectral",
|
|
719
|
+
)
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
report = autofix_markdown_file(path, wrapnumber=79, git_head=True)
|
|
723
|
+
|
|
724
|
+
fixed = path.read_text()
|
|
725
|
+
assert report["layout"] == []
|
|
726
|
+
assert markdown_lint_issues_from_text(fixed, 79) == []
|
|
727
|
+
# the HEAD line stays intact and the added words get one new line
|
|
728
|
+
assert (
|
|
729
|
+
"This script determines the field positions and amplitudes of the\n"
|
|
730
|
+
"three hyperfine lines, and of the\n"
|
|
731
|
+
) in fixed
|
|
732
|
+
assert numstat(path) == (3, 2)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def test_stray_line_break_in_an_unchanged_line_is_rejoined(tmp_path):
|
|
736
|
+
path = committed_source(tmp_path, HEAD_PARAGRAPH)
|
|
737
|
+
path.write_text(
|
|
738
|
+
HEAD_PARAGRAPH.replace(
|
|
739
|
+
"development of a script", "development\nof a script"
|
|
740
|
+
)
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
report = autofix_markdown_file(path, wrapnumber=79, git_head=True)
|
|
744
|
+
|
|
745
|
+
assert path.read_text() == HEAD_PARAGRAPH
|
|
746
|
+
assert numstat(path) == (0, 0)
|
|
747
|
+
assert "put it back" in report["fixes"][0]["reason"]
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def test_one_moved_line_break_is_moved_back(tmp_path):
|
|
751
|
+
path = committed_source(tmp_path, HEAD_PARAGRAPH)
|
|
752
|
+
path.write_text(
|
|
753
|
+
HEAD_PARAGRAPH.replace(
|
|
754
|
+
"amplitudes of the\nspectral lines",
|
|
755
|
+
"amplitudes of\nthe spectral lines",
|
|
756
|
+
)
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
autofix_markdown_file(path, wrapnumber=79, git_head=True)
|
|
760
|
+
|
|
761
|
+
assert path.read_text() == HEAD_PARAGRAPH
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def test_joined_head_lines_past_the_width_get_heads_break_back(tmp_path):
|
|
765
|
+
path = committed_source(tmp_path, HEAD_PARAGRAPH)
|
|
766
|
+
path.write_text(
|
|
767
|
+
HEAD_PARAGRAPH.replace("in order to\nminimize", "in order to minimize")
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
autofix_markdown_file(path, wrapnumber=79, git_head=True)
|
|
771
|
+
|
|
772
|
+
assert path.read_text() == HEAD_PARAGRAPH
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def test_two_moved_line_breaks_in_one_hunk_are_only_reported(tmp_path):
|
|
776
|
+
path = committed_source(tmp_path, HEAD_PARAGRAPH)
|
|
777
|
+
edited = HEAD_PARAGRAPH.replace(
|
|
778
|
+
"amplitudes of the\nspectral lines",
|
|
779
|
+
"amplitudes\nof the spectral lines",
|
|
780
|
+
).replace(
|
|
781
|
+
"variable-temperature experiment.\nIt implements",
|
|
782
|
+
"variable-temperature experiment.\nIt\nimplements",
|
|
783
|
+
)
|
|
784
|
+
path.write_text(edited)
|
|
785
|
+
|
|
786
|
+
report = autofix_markdown_file(path, wrapnumber=79, git_head=True)
|
|
787
|
+
|
|
788
|
+
assert path.read_text() == edited
|
|
789
|
+
((notice),) = report["layout"]
|
|
790
|
+
assert notice["moves"] == 2
|
|
791
|
+
assert notice["line"] == 3
|
|
792
|
+
assert notice["suggested"] + "\n" in HEAD_PARAGRAPH
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def test_removed_words_keep_their_line_break(tmp_path):
|
|
796
|
+
path = committed_source(tmp_path, HEAD_PARAGRAPH)
|
|
797
|
+
edited = HEAD_PARAGRAPH.replace(
|
|
798
|
+
"development of a script\nthat", "development\nthat"
|
|
799
|
+
)
|
|
800
|
+
path.write_text(edited)
|
|
801
|
+
|
|
802
|
+
report = autofix_markdown_file(path, wrapnumber=79, git_head=True)
|
|
803
|
+
|
|
804
|
+
assert path.read_text() == edited
|
|
805
|
+
assert report["fixes"] == [] and report["layout"] == []
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def test_new_paragraph_keeps_the_writers_line_breaks(tmp_path):
|
|
809
|
+
path = committed_source(tmp_path, HEAD_PARAGRAPH)
|
|
810
|
+
new_paragraph = (
|
|
811
|
+
"\nThese estimates\n"
|
|
812
|
+
"depend only weakly on the choice of smoothing.\n"
|
|
813
|
+
)
|
|
814
|
+
path.write_text(HEAD_PARAGRAPH + new_paragraph)
|
|
815
|
+
|
|
816
|
+
report = autofix_markdown_file(path, wrapnumber=79, git_head=True)
|
|
817
|
+
|
|
818
|
+
assert path.read_text() == HEAD_PARAGRAPH + new_paragraph
|
|
819
|
+
assert report["fixes"] == [] and report["layout"] == []
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def test_whole_file_is_still_linted_against_head(tmp_path):
|
|
823
|
+
# a problem that git HEAD already has still has to be fixed
|
|
824
|
+
path = committed_source(
|
|
825
|
+
tmp_path, HEAD_PARAGRAPH.replace("spectra.\n", "spectra. \n")
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
autofix_markdown_file(path, wrapnumber=79, git_head=True)
|
|
829
|
+
|
|
830
|
+
assert path.read_text() == HEAD_PARAGRAPH
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def test_file_outside_git_is_linted_without_head(tmp_path):
|
|
834
|
+
path = tmp_path / "content.md"
|
|
835
|
+
path.write_text(
|
|
836
|
+
HEAD_PARAGRAPH.replace(
|
|
837
|
+
"development of a script", "development\nof a script"
|
|
838
|
+
)
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
report = autofix_markdown_file(path, wrapnumber=79, git_head=True)
|
|
842
|
+
|
|
843
|
+
assert report["fixes"] == [] and report["layout"] == []
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
# }}}
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def test_rewritten_sentence_keeps_its_semantic_line_breaks(tmp_path):
|
|
850
|
+
# from RM_ESR: a rewrite that breaks at each phrase shares words with
|
|
851
|
+
# HEAD, but joining its lines would not restore any HEAD line
|
|
852
|
+
head = (
|
|
853
|
+
"Peric *et al.* also observed that\n"
|
|
854
|
+
"even similarly sized nitroxide probes can report\n"
|
|
855
|
+
"different rotational dynamics in bulk water.\n"
|
|
856
|
+
)
|
|
857
|
+
rewrite = (
|
|
858
|
+
"Peric *et al.* also observed that\n"
|
|
859
|
+
"the rotational dynamics\n"
|
|
860
|
+
"of different small nitroxide probes\n"
|
|
861
|
+
"in bulk water\n"
|
|
862
|
+
"differ significantly.\n"
|
|
863
|
+
)
|
|
864
|
+
path = committed_source(tmp_path, head)
|
|
865
|
+
path.write_text(rewrite)
|
|
866
|
+
|
|
867
|
+
report = autofix_markdown_file(path, wrapnumber=79, git_head=True)
|
|
868
|
+
|
|
869
|
+
assert path.read_text() == rewrite
|
|
870
|
+
assert report["fixes"] == [] and report["layout"] == []
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def test_qt_layout_notice_shows_head_now_and_suggested(tmp_path):
|
|
874
|
+
pytest.importorskip("PySide6")
|
|
875
|
+
from PySide6.QtCore import QTimer
|
|
876
|
+
from PySide6.QtWidgets import QApplication, QDialog, QLabel
|
|
877
|
+
from PySide6.QtWidgets import QPushButton, QTextEdit
|
|
878
|
+
|
|
879
|
+
from pydifftools.continuous import show_markdown_fix_dialog
|
|
880
|
+
|
|
881
|
+
app = QApplication.instance() or QApplication([])
|
|
882
|
+
path = committed_source(tmp_path, HEAD_PARAGRAPH)
|
|
883
|
+
path.write_text(
|
|
884
|
+
HEAD_PARAGRAPH.replace(
|
|
885
|
+
"amplitudes of the\nspectral lines",
|
|
886
|
+
"amplitudes\nof the spectral lines",
|
|
887
|
+
).replace(
|
|
888
|
+
"experiment.\nIt implements", "experiment.\nIt\nimplements"
|
|
889
|
+
)
|
|
890
|
+
)
|
|
891
|
+
report = autofix_markdown_file(path, wrapnumber=79, git_head=True)
|
|
892
|
+
seen = {}
|
|
893
|
+
|
|
894
|
+
def inspect():
|
|
895
|
+
dialog = next(
|
|
896
|
+
widget
|
|
897
|
+
for widget in app.topLevelWidgets()
|
|
898
|
+
if isinstance(widget, QDialog) and widget.isVisible()
|
|
899
|
+
)
|
|
900
|
+
seen["heading"] = dialog.findChildren(QLabel)[0].text()
|
|
901
|
+
seen["preview"] = dialog.findChild(QTextEdit).toPlainText()
|
|
902
|
+
dialog.findChild(QPushButton).click()
|
|
903
|
+
|
|
904
|
+
QTimer.singleShot(0, inspect)
|
|
905
|
+
show_markdown_fix_dialog(report)
|
|
906
|
+
|
|
907
|
+
assert "differ from git HEAD in 2 places" in seen["heading"]
|
|
908
|
+
head, now, suggested = seen["preview"].split("\n\n")
|
|
909
|
+
assert head.startswith("git HEAD:")
|
|
910
|
+
assert re.search(r"^ +3 This script determines .* amplitudes$", now, re.M)
|
|
911
|
+
assert re.search(r"^ +4 spectral lines for", suggested, re.M)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/__version__.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/.jupyter_cache/global.db
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg
RENAMED
|
File without changes
|
{pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/subproject1/independent.qmd
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/subproject1/test_include.qmd
RENAMED
|
File without changes
|
{pydifftools-0.1.43 → pydifftools-0.1.44}/example_notebook/project1/subproject1/tryforerror.qmd
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|