pyDiffTools 0.1.46__tar.gz → 0.1.47__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.46/pyDiffTools.egg-info → pydifftools-0.1.47}/PKG-INFO +1 -1
- {pydifftools-0.1.46 → pydifftools-0.1.47/pyDiffTools.egg-info}/PKG-INFO +1 -1
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/bibliography.py +20 -6
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/browser_lifecycle.py +13 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/citation_dialog.py +50 -18
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/comment_migration.py +5 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/continuous.py +50 -18
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/wrap_sentences.py +32 -2
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/zotero.py +157 -31
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pyproject.toml +1 -1
- {pydifftools-0.1.46 → pydifftools-0.1.47}/tests/test_check_single_use.py +32 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/tests/test_continuous_shutdown.py +68 -11
- {pydifftools-0.1.46 → pydifftools-0.1.47}/tests/test_wrap_check.py +38 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/tests/test_zotero.py +116 -7
- {pydifftools-0.1.46 → pydifftools-0.1.47}/LICENSE.md +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/MANIFEST.in +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/README.rst +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/_quarto.yml +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/example.qmd +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/example.tex +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/index.qmd +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/subproject1/independent.qmd +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/subproject1/index.qmd +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/subproject1/tasks.qmd +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/subproject1/test_include.qmd +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/example_notebook/project1/test_include.qmd +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pyDiffTools.egg-info/SOURCES.txt +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pyDiffTools.egg-info/dependency_links.txt +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pyDiffTools.egg-info/entry_points.txt +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pyDiffTools.egg-info/requires.txt +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pyDiffTools.egg-info/top_level.txt +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/__init__.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/check_numbers.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/command_line.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/command_registry.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/comment_functions.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/comment_tags.lua +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/comment_tags_margin.lua +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/comment_tags_no_comments.lua +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/comment_toggle.js +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/comments.css +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/copy_files.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/diff-doc.js +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/doc_contents.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/flowchart/__init__.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/flowchart/comparison.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/flowchart/dot_to_yaml.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/flowchart/graph.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/flowchart/watch_graph.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/flowchart/wgrph_view.js +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/forward_search.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/git_gd.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/git_gd_history.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/git_gd_image.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/git_gd_qt.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/html_comments.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/html_uncomments.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/log_example.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/match_spaces.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/notebook/__init__.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/notebook/display.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/notebook/fast_build.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/notebook/tex_to_qmd.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/onewordify.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/onewordify_undo.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/outline.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/rearrange_tex.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/searchacro.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/separate_comments.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/split_conflict.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/unseparate_comments.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/update_check.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/pydifftools/xml2xlsx.vbs +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/setup.cfg +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/tests/test_bibliography.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/tests/test_browser_lifecycle.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/tests/test_command_line_help.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/tests/test_comment_migration.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/tests/test_forward_search.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/tests/test_git_gd_history.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/tests/test_git_gd_image.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/tests/test_git_gd_qt.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/tests/test_rrng.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/tests/test_tex_to_qmd.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/tests/test_update_check.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.47}/tests/test_wrapping.py +0 -0
|
@@ -64,9 +64,9 @@ class BibEntry:
|
|
|
64
64
|
return f"@{self.kind}{{{key or self.key},\n{body}\n}}"
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
def
|
|
68
|
-
"""
|
|
69
|
-
entries =
|
|
67
|
+
def bib_entry_list(text):
|
|
68
|
+
"""Every entry's span and raw fields in order, including repeated keys."""
|
|
69
|
+
entries = []
|
|
70
70
|
position = 0
|
|
71
71
|
while position < len(text):
|
|
72
72
|
position = scan_bibtex(text, position, "@")
|
|
@@ -106,9 +106,17 @@ def bib_entries(text):
|
|
|
106
106
|
raise ValueError(f"duplicate BibTeX field {name} in {key}")
|
|
107
107
|
fields[name] = body[offset:boundary].strip()
|
|
108
108
|
offset = boundary + 1
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
entries.append(BibEntry(start, end + 1, kind, key, fields))
|
|
110
|
+
return entries
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def bib_entries(text):
|
|
114
|
+
"""Index entry spans and raw fields without reserializing them."""
|
|
115
|
+
entries = {}
|
|
116
|
+
for entry in bib_entry_list(text):
|
|
117
|
+
if entry.key in entries:
|
|
118
|
+
raise ValueError(f"duplicate BibTeX key {entry.key}")
|
|
119
|
+
entries[entry.key] = entry
|
|
112
120
|
return entries
|
|
113
121
|
|
|
114
122
|
|
|
@@ -118,6 +126,8 @@ def normalized_text(value):
|
|
|
118
126
|
return "".join(char for char in text if char.isalnum())
|
|
119
127
|
|
|
120
128
|
|
|
129
|
+
# also used by: tests/test_bibliography.py, which checks these duplicate
|
|
130
|
+
# rules directly (cpb calls it from zotero.recover_bibliography)
|
|
121
131
|
def duplicate_reason(existing, incoming):
|
|
122
132
|
"""Identify likely duplicates for human review using Pandoc CSL records.
|
|
123
133
|
|
|
@@ -210,6 +220,8 @@ def markdown_ast(text, renames=None):
|
|
|
210
220
|
return tree
|
|
211
221
|
|
|
212
222
|
|
|
223
|
+
# also used by: tests/test_bibliography.py, which checks citation rewriting
|
|
224
|
+
# directly (cpb calls it from zotero.recover_bibliography)
|
|
213
225
|
def rewrite_citations(markdown, renames):
|
|
214
226
|
"""Replace citation tokens, accepting edits only when Pandoc confirms them.
|
|
215
227
|
|
|
@@ -241,6 +253,8 @@ def rewrite_citations(markdown, renames):
|
|
|
241
253
|
return text.encode("utf-8")
|
|
242
254
|
|
|
243
255
|
|
|
256
|
+
# also used by: tests/test_bibliography.py, which checks the all-or-nothing
|
|
257
|
+
# replacement directly (cpb calls it from zotero.recover_bibliography)
|
|
244
258
|
def replace_bibliography_files(changes):
|
|
245
259
|
"""Stage validated replacements and roll back if a later replacement fails.
|
|
246
260
|
|
|
@@ -2,6 +2,17 @@ import os
|
|
|
2
2
|
import shutil
|
|
3
3
|
import subprocess
|
|
4
4
|
|
|
5
|
+
# callbacks that run before any dialog needs the user; cpb registers one
|
|
6
|
+
# that closes its live preview, so issues are dealt with before it reopens
|
|
7
|
+
dialog_callbacks = []
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def prepare_for_dialog():
|
|
11
|
+
"""Let watch commands close their preview before a dialog appears."""
|
|
12
|
+
for callback in list(dialog_callbacks):
|
|
13
|
+
callback()
|
|
14
|
+
|
|
15
|
+
|
|
5
16
|
def browser_window_is_alive(browser):
|
|
6
17
|
# Keep all browser liveness checks in one place so watch commands share
|
|
7
18
|
# the same shutdown behavior when a user closes the browser window.
|
|
@@ -29,6 +40,8 @@ def close_browser_window(browser):
|
|
|
29
40
|
pass
|
|
30
41
|
|
|
31
42
|
|
|
43
|
+
# also used by: notebook/fast_build.py (qmdb's forward search), besides
|
|
44
|
+
# cpb in continuous.py; tests/test_continuous_shutdown.py monkeypatches it
|
|
32
45
|
def forward_search_in_browser(browser, search_text):
|
|
33
46
|
# Reuse the same browser-side find logic across cpb and qmdb.
|
|
34
47
|
if browser is None or not search_text:
|
|
@@ -21,20 +21,49 @@ from PySide6.QtWidgets import (
|
|
|
21
21
|
class DuplicateDialog(QDialog):
|
|
22
22
|
"""Choose an entry or merge fields; default to existing values and key."""
|
|
23
23
|
|
|
24
|
-
def __init__(self, matches, incoming, source):
|
|
24
|
+
def __init__(self, matches, incoming, source, mode="zotero"):
|
|
25
25
|
super().__init__()
|
|
26
26
|
self.matches = matches
|
|
27
27
|
self.incoming = incoming
|
|
28
28
|
self.decision = None
|
|
29
|
-
self.setWindowTitle("pydifft cpb — Possible duplicate citation")
|
|
30
29
|
self.resize(1000, 650)
|
|
31
30
|
layout = QVBoxLayout(self)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
if mode == "bibliography":
|
|
32
|
+
# {{{ two entries in the bibliography share one citation key
|
|
33
|
+
self.setWindowTitle("pydifft cpb — Duplicate citation key")
|
|
34
|
+
text = (
|
|
35
|
+
f"{source} defines @{incoming['key']} twice, so no missing "
|
|
36
|
+
"citations can be added until one entry is chosen.\n"
|
|
37
|
+
"Keep one entry, or merge them field by field; the other "
|
|
38
|
+
"entry is removed from the bibliography."
|
|
39
|
+
)
|
|
40
|
+
self.labels = ["First entry", "Second entry"]
|
|
41
|
+
actions = [
|
|
42
|
+
("Keep first", "existing"),
|
|
43
|
+
("Keep second", "incoming"),
|
|
44
|
+
("Merge selected fields", "merge"),
|
|
45
|
+
("Leave both (add no citations)", "skip"),
|
|
46
|
+
]
|
|
47
|
+
# }}}
|
|
48
|
+
else:
|
|
49
|
+
self.setWindowTitle("pydifft cpb — Possible duplicate citation")
|
|
50
|
+
text = (
|
|
51
|
+
f"Review the imported citation for {source}.\n"
|
|
52
|
+
"Keeping or merging a duplicate also replaces @UNCHOSEN with "
|
|
53
|
+
"@CHOSEN in this Markdown file. Other documents are not "
|
|
54
|
+
"edited.\n"
|
|
55
|
+
"For a merge, choose each field below, including the citation "
|
|
56
|
+
"key."
|
|
57
|
+
)
|
|
58
|
+
self.labels = ["Existing", "Zotero"]
|
|
59
|
+
actions = [
|
|
60
|
+
("Keep existing", "existing"),
|
|
61
|
+
("Keep Zotero", "incoming"),
|
|
62
|
+
("Merge selected fields", "merge"),
|
|
63
|
+
("Not a duplicate: keep both", "both"),
|
|
64
|
+
("Skip this import", "skip"),
|
|
65
|
+
]
|
|
66
|
+
explanation = QLabel(text)
|
|
38
67
|
explanation.setWordWrap(True)
|
|
39
68
|
layout.addWidget(explanation)
|
|
40
69
|
self.match_selector = QComboBox()
|
|
@@ -44,7 +73,16 @@ class DuplicateDialog(QDialog):
|
|
|
44
73
|
self.table = QTableWidget()
|
|
45
74
|
self.table.setColumnCount(4)
|
|
46
75
|
self.table.setHorizontalHeaderLabels(
|
|
47
|
-
[
|
|
76
|
+
[
|
|
77
|
+
"Field",
|
|
78
|
+
(
|
|
79
|
+
"Existing bibliography"
|
|
80
|
+
if mode == "zotero"
|
|
81
|
+
else self.labels[0]
|
|
82
|
+
),
|
|
83
|
+
self.labels[1],
|
|
84
|
+
"Merge uses",
|
|
85
|
+
]
|
|
48
86
|
)
|
|
49
87
|
self.table.horizontalHeader().setSectionResizeMode(
|
|
50
88
|
QHeaderView.ResizeMode.Stretch
|
|
@@ -52,13 +90,7 @@ class DuplicateDialog(QDialog):
|
|
|
52
90
|
layout.addWidget(self.table)
|
|
53
91
|
buttons = QHBoxLayout()
|
|
54
92
|
self.buttons = {}
|
|
55
|
-
for label, action in
|
|
56
|
-
("Keep existing", "existing"),
|
|
57
|
-
("Keep Zotero", "incoming"),
|
|
58
|
-
("Merge selected fields", "merge"),
|
|
59
|
-
("Not a duplicate: keep both", "both"),
|
|
60
|
-
("Skip this import", "skip"),
|
|
61
|
-
]:
|
|
93
|
+
for label, action in actions:
|
|
62
94
|
button = QPushButton(label)
|
|
63
95
|
button.setAutoDefault(False)
|
|
64
96
|
button.clicked.connect(
|
|
@@ -94,9 +126,9 @@ class DuplicateDialog(QDialog):
|
|
|
94
126
|
self.table.setItem(row, column, cell)
|
|
95
127
|
selector = QComboBox()
|
|
96
128
|
if name in self.left:
|
|
97
|
-
selector.addItem(
|
|
129
|
+
selector.addItem(self.labels[0], self.left[name])
|
|
98
130
|
if name in self.right:
|
|
99
|
-
selector.addItem(
|
|
131
|
+
selector.addItem(self.labels[1], self.right[name])
|
|
100
132
|
self.table.setCellWidget(row, 3, selector)
|
|
101
133
|
self.selectors[name] = selector
|
|
102
134
|
self.table.resizeRowsToContents()
|
|
@@ -11,6 +11,8 @@ import textwrap
|
|
|
11
11
|
|
|
12
12
|
import yaml
|
|
13
13
|
|
|
14
|
+
from .browser_lifecycle import prepare_for_dialog
|
|
15
|
+
|
|
14
16
|
_FRONT_MATTER_RE = re.compile(
|
|
15
17
|
r"\A(\ufeff?---[ \t]*\r?\n)(.*?)(\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$))",
|
|
16
18
|
re.DOTALL,
|
|
@@ -78,6 +80,8 @@ def _most_distant_hue(colors):
|
|
|
78
80
|
return (start + gap_size / 2) % 360
|
|
79
81
|
|
|
80
82
|
|
|
83
|
+
# also used by: tests/test_comment_migration.py, which compiles its Qt
|
|
84
|
+
# script and monkeypatches it (cpb calls it from prepare_comment_source)
|
|
81
85
|
def _migration_dialog(filename, colors, legacy):
|
|
82
86
|
"""Ask Qt for all migrations and return decisions without editing files."""
|
|
83
87
|
script = r"""
|
|
@@ -280,6 +284,7 @@ if result["accepted"]:
|
|
|
280
284
|
|
|
281
285
|
print(json.dumps(result))
|
|
282
286
|
"""
|
|
287
|
+
prepare_for_dialog()
|
|
283
288
|
completed = subprocess.run(
|
|
284
289
|
# the dialog reads the document from its file, since the text of a
|
|
285
290
|
# large document exceeds the operating system's argument limit
|
|
@@ -23,7 +23,9 @@ from .comment_migration import prepare_comment_source
|
|
|
23
23
|
from .browser_lifecycle import (
|
|
24
24
|
browser_window_is_alive,
|
|
25
25
|
close_browser_window,
|
|
26
|
+
dialog_callbacks,
|
|
26
27
|
forward_search_in_browser,
|
|
28
|
+
prepare_for_dialog,
|
|
27
29
|
)
|
|
28
30
|
from .forward_search import (
|
|
29
31
|
CPB_FORWARD_SEARCH_PORT,
|
|
@@ -63,6 +65,9 @@ def _comment_filter_mode(path, packaged_filters=None):
|
|
|
63
65
|
|
|
64
66
|
|
|
65
67
|
# Kept separate so its prompt choices can be unit-tested.
|
|
68
|
+
# also used by: tests/test_wrap_check.py and tests/cli/test_commands.py,
|
|
69
|
+
# which call it directly or monkeypatch it to answer the prompt (cpb calls
|
|
70
|
+
# it from run_pandoc)
|
|
66
71
|
def confirm_restore_comment_filter(active_mode):
|
|
67
72
|
if active_mode == "none":
|
|
68
73
|
message = (
|
|
@@ -112,6 +117,7 @@ if box.clickedButton() is restore_button:
|
|
|
112
117
|
sys.exit(0)
|
|
113
118
|
sys.exit(1)
|
|
114
119
|
"""
|
|
120
|
+
prepare_for_dialog()
|
|
115
121
|
result = subprocess.run(
|
|
116
122
|
[sys.executable, "-c", prompt_script, message, default_choice],
|
|
117
123
|
capture_output=True,
|
|
@@ -127,6 +133,8 @@ sys.exit(1)
|
|
|
127
133
|
raise RuntimeError("pydifft cpb filter dialog failed.")
|
|
128
134
|
|
|
129
135
|
|
|
136
|
+
# also used by: tests/test_wrap_check.py, which drives this window directly
|
|
137
|
+
# (cpb calls it from run_pandoc)
|
|
130
138
|
def show_markdown_fix_dialog(report):
|
|
131
139
|
"""Show automatic fixes and any source edits that still need a user."""
|
|
132
140
|
from html import escape
|
|
@@ -148,6 +156,7 @@ def show_markdown_fix_dialog(report):
|
|
|
148
156
|
layout = report.get("layout", [])
|
|
149
157
|
if not fixes and not warnings and not layout:
|
|
150
158
|
return
|
|
159
|
+
prepare_for_dialog()
|
|
151
160
|
|
|
152
161
|
# a QApplication must exist before any widget is constructed
|
|
153
162
|
application = QApplication.instance() or QApplication([])
|
|
@@ -362,6 +371,8 @@ def show_markdown_fix_dialog(report):
|
|
|
362
371
|
return user_says_fixed
|
|
363
372
|
|
|
364
373
|
|
|
374
|
+
# also used by: tests/test_wrap_check.py, which drives this window directly
|
|
375
|
+
# (cpb calls it from run_pandoc)
|
|
365
376
|
def show_markdown_reload_dialog():
|
|
366
377
|
"""Ask the user to reload a source file changed by automatic fixes."""
|
|
367
378
|
from PySide6.QtWidgets import (
|
|
@@ -372,6 +383,7 @@ def show_markdown_reload_dialog():
|
|
|
372
383
|
QVBoxLayout,
|
|
373
384
|
)
|
|
374
385
|
|
|
386
|
+
prepare_for_dialog()
|
|
375
387
|
# a QApplication must exist before any widget is constructed
|
|
376
388
|
application = QApplication.instance() or QApplication([])
|
|
377
389
|
dialog = QDialog()
|
|
@@ -421,6 +433,10 @@ def run_pandoc(
|
|
|
421
433
|
break
|
|
422
434
|
|
|
423
435
|
if automatic_fixes_were_made:
|
|
436
|
+
# a corrected comment tag (e.g. <XYcomm> -> <XYcom>) can name an
|
|
437
|
+
# author with no XYcolor yet, so migrate comments again now rather
|
|
438
|
+
# than on the next save
|
|
439
|
+
prepare_comment_source(filename)
|
|
424
440
|
show_markdown_reload_dialog()
|
|
425
441
|
# }}}
|
|
426
442
|
|
|
@@ -909,7 +925,6 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
|
|
|
909
925
|
daemon=True,
|
|
910
926
|
)
|
|
911
927
|
chrome = None
|
|
912
|
-
notice = None
|
|
913
928
|
observer = None
|
|
914
929
|
observer_started = False
|
|
915
930
|
socket_thread_started = False
|
|
@@ -924,6 +939,9 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
|
|
|
924
939
|
# orphaned browser session. Remember the source version from before
|
|
925
940
|
# the build so an edit during a slow Pandoc run remains pending.
|
|
926
941
|
source_missing = object()
|
|
942
|
+
# every dialog is dealt with (OK pressed) before Pandoc runs or
|
|
943
|
+
# Chrome opens
|
|
944
|
+
zotero_notice()
|
|
927
945
|
prepare_comment_source(filename)
|
|
928
946
|
initial_source_stat = os.stat(source_path)
|
|
929
947
|
handled_signature = (
|
|
@@ -940,7 +958,6 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
|
|
|
940
958
|
wrapnumber=wrapnumber,
|
|
941
959
|
)
|
|
942
960
|
append_autorefresh(html_file)
|
|
943
|
-
notice = zotero_notice()
|
|
944
961
|
|
|
945
962
|
# Selenium is deliberately initialized once, after the first
|
|
946
963
|
# successful build. Nothing in recovery constructs a browser.
|
|
@@ -992,6 +1009,16 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
|
|
|
992
1009
|
# immediately after Chrome loads can become polling's initial snapshot
|
|
993
1010
|
# and never be reported as a change.
|
|
994
1011
|
chrome.get("file://" + os.path.abspath(html_file))
|
|
1012
|
+
|
|
1013
|
+
def close_for_dialog():
|
|
1014
|
+
# a rebuild needs the user, so close the preview while the
|
|
1015
|
+
# issues are dealt with; it reopens once the rebuild is done
|
|
1016
|
+
nonlocal chrome
|
|
1017
|
+
if chrome is not None:
|
|
1018
|
+
close_browser_window(chrome)
|
|
1019
|
+
chrome = None
|
|
1020
|
+
|
|
1021
|
+
dialog_callbacks.append(close_for_dialog)
|
|
995
1022
|
rebuild_pending = False
|
|
996
1023
|
stable_signature = None
|
|
997
1024
|
stable_since = None
|
|
@@ -1102,18 +1129,26 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
|
|
|
1102
1129
|
traceback.print_exc()
|
|
1103
1130
|
else:
|
|
1104
1131
|
handled_signature = attempted_signature
|
|
1105
|
-
if not
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1132
|
+
if chrome is not None:
|
|
1133
|
+
if not browser_window_is_alive(chrome):
|
|
1134
|
+
break
|
|
1135
|
+
try:
|
|
1136
|
+
chrome.refresh()
|
|
1137
|
+
except WebDriverException:
|
|
1138
|
+
print(
|
|
1139
|
+
"pydifft cpb: Chrome is no longer "
|
|
1140
|
+
"available; stopping without "
|
|
1141
|
+
"reopening it.",
|
|
1142
|
+
file=sys.stderr,
|
|
1143
|
+
)
|
|
1144
|
+
break
|
|
1145
|
+
if chrome is None:
|
|
1146
|
+
# {{{ reopen the preview a dialog closed
|
|
1147
|
+
chrome = webdriver.Chrome()
|
|
1148
|
+
chrome.get(
|
|
1149
|
+
"file://" + os.path.abspath(html_file)
|
|
1150
|
+
)
|
|
1151
|
+
# }}}
|
|
1117
1152
|
# }}}
|
|
1118
1153
|
time.sleep(MAIN_LOOP_INTERVAL_SECONDS)
|
|
1119
1154
|
except KeyboardInterrupt:
|
|
@@ -1127,10 +1162,7 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
|
|
|
1127
1162
|
if socket_thread_started:
|
|
1128
1163
|
socket_thread.join()
|
|
1129
1164
|
close_browser_window(chrome)
|
|
1130
|
-
|
|
1131
|
-
if notice.poll() is None:
|
|
1132
|
-
notice.terminate()
|
|
1133
|
-
notice.wait()
|
|
1165
|
+
dialog_callbacks.clear()
|
|
1134
1166
|
|
|
1135
1167
|
|
|
1136
1168
|
if __name__ == "__main__":
|
|
@@ -29,6 +29,11 @@ DETACHED_CROSSREF = re.compile(
|
|
|
29
29
|
r"(?:(?<!\\)\$\$|\))(?P<gap>[ \t]*\n?[ \t]*)(?=\{#(?:eq|fig):)"
|
|
30
30
|
r"|(?P<marker>\{#(?:eq|fig):[^}\n]*\n[^}]*\})"
|
|
31
31
|
)
|
|
32
|
+
# Comment tags are two initials and "com" (<JFcom>, <JFcom-left>, ...); the
|
|
33
|
+
# comment filter passes a misspelling such as <JFcomm> through as raw HTML.
|
|
34
|
+
MISSPELLED_COMMENT_TAG = re.compile(
|
|
35
|
+
r"<(/?)([A-Za-z]{2})comm(?:ent)?(-left|-right)?>", re.IGNORECASE
|
|
36
|
+
)
|
|
32
37
|
LATEX_MACRO_SPLIT = re.compile(
|
|
33
38
|
r"(\\(?:begin|end|usepackage|newcommand|section"
|
|
34
39
|
r"|subsection|subsubsection|paragraph|input){[^}]*})"
|
|
@@ -1055,18 +1060,29 @@ def markdown_lint_issues_from_text(
|
|
|
1055
1060
|
(issue["line"], issue["message"])
|
|
1056
1061
|
for issue in unclosed_markdown_spans(content)
|
|
1057
1062
|
)
|
|
1058
|
-
# {{{ report single trailing spaces
|
|
1063
|
+
# {{{ report single trailing spaces, misspelled comment tags and
|
|
1064
|
+
# detached crossref markers
|
|
1059
1065
|
fence = None
|
|
1060
1066
|
for number, line in enumerate(content.splitlines(), start=1):
|
|
1061
1067
|
opening = re.match(r"[ \t]*(\x60{3,}|~{3,})", line)
|
|
1062
1068
|
if opening and (fence is None or opening[1].startswith(fence)):
|
|
1063
1069
|
fence = None if fence else opening[1]
|
|
1064
|
-
|
|
1070
|
+
continue
|
|
1071
|
+
if fence is not None:
|
|
1072
|
+
continue
|
|
1073
|
+
if re.search(r"(?<! ) $", line):
|
|
1065
1074
|
issues.append((
|
|
1066
1075
|
number,
|
|
1067
1076
|
"trailing space: remove the single space at the end of this "
|
|
1068
1077
|
"line (two spaces are a deliberate hard line break)",
|
|
1069
1078
|
))
|
|
1079
|
+
misspelled = MISSPELLED_COMMENT_TAG.search(line)
|
|
1080
|
+
if misspelled:
|
|
1081
|
+
issues.append((
|
|
1082
|
+
number,
|
|
1083
|
+
f"misspelled comment tag: {misspelled[0]} should be "
|
|
1084
|
+
f"<{misspelled[1]}{misspelled[2]}com{misspelled[3] or ''}>",
|
|
1085
|
+
))
|
|
1070
1086
|
for found in DETACHED_CROSSREF.finditer(prose_content):
|
|
1071
1087
|
if found["gap"] == "":
|
|
1072
1088
|
continue
|
|
@@ -1233,6 +1249,20 @@ def apply_markdown_issue_fix(content, line_number, message):
|
|
|
1233
1249
|
else:
|
|
1234
1250
|
prefix = re.match(r"^[ \t]*", old_line).group()
|
|
1235
1251
|
# }}}
|
|
1252
|
+
if message.startswith("misspelled comment tag:"):
|
|
1253
|
+
fixed = MISSPELLED_COMMENT_TAG.sub(
|
|
1254
|
+
lambda found: f"<{found[1]}{found[2]}com{found[3] or ''}>",
|
|
1255
|
+
old_line,
|
|
1256
|
+
)
|
|
1257
|
+
lines[index] = fixed + ending
|
|
1258
|
+
return (
|
|
1259
|
+
"".join(lines),
|
|
1260
|
+
old_line,
|
|
1261
|
+
fixed,
|
|
1262
|
+
"One or more comment tags were misspelled, so they would not "
|
|
1263
|
+
"show as comments. A comment tag is two initials and com, as in "
|
|
1264
|
+
"<JFcom>. I corrected them.",
|
|
1265
|
+
)
|
|
1236
1266
|
if message.startswith("trailing space:"):
|
|
1237
1267
|
lines[index] = old_line[:-1] + ending
|
|
1238
1268
|
return (
|