pyDiffTools 0.1.46__tar.gz → 0.1.48__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-0.1.48}/MANIFEST.in +1 -0
- {pydifftools-0.1.46/pyDiffTools.egg-info → pydifftools-0.1.48}/PKG-INFO +1 -1
- {pydifftools-0.1.46 → pydifftools-0.1.48/pyDiffTools.egg-info}/PKG-INFO +1 -1
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pyDiffTools.egg-info/SOURCES.txt +1 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/bibliography.py +20 -6
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/browser_lifecycle.py +13 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/citation_dialog.py +50 -18
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/comment_migration.py +5 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/comment_tags.lua +21 -3
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/comment_tags_margin.lua +21 -3
- pydifftools-0.1.48/pydifftools/comments_author_colors.css +20 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/continuous.py +76 -33
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/wrap_sentences.py +32 -2
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/zotero.py +157 -31
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pyproject.toml +2 -1
- {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_check_single_use.py +32 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_continuous_shutdown.py +68 -11
- {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_wrap_check.py +38 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_zotero.py +116 -7
- {pydifftools-0.1.46 → pydifftools-0.1.48}/LICENSE.md +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/README.rst +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/_quarto.yml +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/example.qmd +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/example.tex +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/index.qmd +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/independent.qmd +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/index.qmd +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/tasks.qmd +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/test_include.qmd +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/test_include.qmd +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pyDiffTools.egg-info/dependency_links.txt +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pyDiffTools.egg-info/entry_points.txt +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pyDiffTools.egg-info/requires.txt +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pyDiffTools.egg-info/top_level.txt +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/__init__.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/check_numbers.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/command_line.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/command_registry.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/comment_functions.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/comment_tags_no_comments.lua +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/comment_toggle.js +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/comments.css +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/copy_files.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/diff-doc.js +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/doc_contents.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/flowchart/__init__.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/flowchart/comparison.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/flowchart/dot_to_yaml.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/flowchart/graph.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/flowchart/watch_graph.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/flowchart/wgrph_view.js +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/forward_search.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/git_gd.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/git_gd_history.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/git_gd_image.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/git_gd_qt.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/html_comments.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/html_uncomments.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/log_example.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/match_spaces.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/notebook/__init__.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/notebook/display.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/notebook/fast_build.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/notebook/tex_to_qmd.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/onewordify.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/onewordify_undo.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/outline.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/rearrange_tex.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/searchacro.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/separate_comments.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/split_conflict.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/unseparate_comments.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/update_check.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/xml2xlsx.vbs +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/setup.cfg +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_bibliography.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_browser_lifecycle.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_command_line_help.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_comment_migration.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_forward_search.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_git_gd_history.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_git_gd_image.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_git_gd_qt.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_rrng.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_tex_to_qmd.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_update_check.py +0 -0
- {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_wrapping.py +0 -0
|
@@ -4,6 +4,7 @@ include LICENSE.md
|
|
|
4
4
|
include pydifftools/diff-doc.js
|
|
5
5
|
include pydifftools/xml2xlsx.vbs
|
|
6
6
|
include pydifftools/comments.css
|
|
7
|
+
include pydifftools/comments_author_colors.css
|
|
7
8
|
include pydifftools/comment_tags.lua
|
|
8
9
|
include pydifftools/comment_tags_margin.lua
|
|
9
10
|
include pydifftools/comment_tags_no_comments.lua
|
|
@@ -58,6 +58,7 @@ pydifftools/comment_tags_margin.lua
|
|
|
58
58
|
pydifftools/comment_tags_no_comments.lua
|
|
59
59
|
pydifftools/comment_toggle.js
|
|
60
60
|
pydifftools/comments.css
|
|
61
|
+
pydifftools/comments_author_colors.css
|
|
61
62
|
pydifftools/continuous.py
|
|
62
63
|
pydifftools/copy_files.py
|
|
63
64
|
pydifftools/diff-doc.js
|
|
@@ -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
|
|
@@ -34,6 +34,24 @@ local OPENERS = {
|
|
|
34
34
|
}
|
|
35
35
|
local AUTHOR_COLORS = {}
|
|
36
36
|
|
|
37
|
+
local function author_color_style(color)
|
|
38
|
+
local red = tonumber(color:sub(2, 3), 16)
|
|
39
|
+
local green = tonumber(color:sub(4, 5), 16)
|
|
40
|
+
local blue = tonumber(color:sub(6, 7), 16)
|
|
41
|
+
local function tint(component)
|
|
42
|
+
return math.floor(component * 0.13 + 255 * 0.87 + 0.5)
|
|
43
|
+
end
|
|
44
|
+
local background = string.format(
|
|
45
|
+
"#%02x%02x%02x", tint(red), tint(green), tint(blue)
|
|
46
|
+
)
|
|
47
|
+
return string.format(
|
|
48
|
+
"--comment-accent-color: %s; "
|
|
49
|
+
.. "--comment-accent-background-color: %s; "
|
|
50
|
+
.. "border-color: %s; background-color: %s;",
|
|
51
|
+
color, background, color, background
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
37
55
|
local comment_id = 0
|
|
38
56
|
local function next_id()
|
|
39
57
|
comment_id = comment_id + 1
|
|
@@ -43,7 +61,7 @@ end
|
|
|
43
61
|
local function make_inline_comment(side, content_inlines, spec)
|
|
44
62
|
local attributes = {}
|
|
45
63
|
if spec and spec.color then
|
|
46
|
-
attributes["style"] =
|
|
64
|
+
attributes["style"] = author_color_style(spec.color)
|
|
47
65
|
attributes["data-comment-author"] = spec.author
|
|
48
66
|
end
|
|
49
67
|
local inner = pandoc.Span(
|
|
@@ -66,7 +84,7 @@ local function make_block_overlay(side, id, content_blocks, spec)
|
|
|
66
84
|
["data-comment-id"] = id,
|
|
67
85
|
["data-comment-author"] = spec and spec.author or nil,
|
|
68
86
|
["style"] = spec and spec.color
|
|
69
|
-
and (
|
|
87
|
+
and author_color_style(spec.color) or nil,
|
|
70
88
|
})
|
|
71
89
|
)
|
|
72
90
|
end
|
|
@@ -402,7 +420,7 @@ function Div(el)
|
|
|
402
420
|
if #style > 0 and not style:match(";%s*$") then
|
|
403
421
|
style = style .. ";"
|
|
404
422
|
end
|
|
405
|
-
el.attributes.style = style ..
|
|
423
|
+
el.attributes.style = style .. author_color_style(color)
|
|
406
424
|
el.attributes["data-comment-author"] = author
|
|
407
425
|
end
|
|
408
426
|
end
|
|
@@ -35,6 +35,24 @@ local OPENERS = {
|
|
|
35
35
|
}
|
|
36
36
|
local AUTHOR_COLORS = {}
|
|
37
37
|
|
|
38
|
+
local function author_color_style(color)
|
|
39
|
+
local red = tonumber(color:sub(2, 3), 16)
|
|
40
|
+
local green = tonumber(color:sub(4, 5), 16)
|
|
41
|
+
local blue = tonumber(color:sub(6, 7), 16)
|
|
42
|
+
local function tint(component)
|
|
43
|
+
return math.floor(component * 0.13 + 255 * 0.87 + 0.5)
|
|
44
|
+
end
|
|
45
|
+
local background = string.format(
|
|
46
|
+
"#%02x%02x%02x", tint(red), tint(green), tint(blue)
|
|
47
|
+
)
|
|
48
|
+
return string.format(
|
|
49
|
+
"--comment-accent-color: %s; "
|
|
50
|
+
.. "--comment-accent-background-color: %s; "
|
|
51
|
+
.. "border-color: %s; background-color: %s;",
|
|
52
|
+
color, background, color, background
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
38
56
|
local comment_id = 0
|
|
39
57
|
local function next_id()
|
|
40
58
|
comment_id = comment_id + 1
|
|
@@ -69,7 +87,7 @@ local function make_inline_comment(side, content_inlines, spec)
|
|
|
69
87
|
["data-comment-author"] = spec and spec.author or nil,
|
|
70
88
|
["style"] = "font-size: 6pt;"
|
|
71
89
|
.. (spec and spec.color
|
|
72
|
-
and ("
|
|
90
|
+
and (" " .. author_color_style(spec.color)) or ""),
|
|
73
91
|
}
|
|
74
92
|
)
|
|
75
93
|
)
|
|
@@ -101,7 +119,7 @@ local function make_block_overlay(side, id, content_blocks, spec)
|
|
|
101
119
|
["data-comment-author"] = spec and spec.author or nil,
|
|
102
120
|
["style"] = "font-size: 6pt;"
|
|
103
121
|
.. (spec and spec.color
|
|
104
|
-
and ("
|
|
122
|
+
and (" " .. author_color_style(spec.color)) or ""),
|
|
105
123
|
}
|
|
106
124
|
)
|
|
107
125
|
)
|
|
@@ -441,7 +459,7 @@ function Div(el)
|
|
|
441
459
|
if #style > 0 and not style:match(";%s*$") then
|
|
442
460
|
style = style .. ";"
|
|
443
461
|
end
|
|
444
|
-
el.attributes.style = style ..
|
|
462
|
+
el.attributes.style = style .. author_color_style(color)
|
|
445
463
|
el.attributes["data-comment-author"] = author
|
|
446
464
|
end
|
|
447
465
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* Keep comment pointer triangles in sync with per-author bubble colors. */
|
|
2
|
+
span.comment-pin > span.comment-right::before,
|
|
3
|
+
.comment-overlay.comment-right::before {
|
|
4
|
+
border-color: transparent var(--comment-accent-color, #5aa0ff) transparent transparent;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
span.comment-pin > span.comment-right::after,
|
|
8
|
+
.comment-overlay.comment-right::after {
|
|
9
|
+
border-color: transparent var(--comment-accent-background-color, #eaf3ff) transparent transparent;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
span.comment-pin > span.comment-left::before,
|
|
13
|
+
.comment-overlay.comment-left::before {
|
|
14
|
+
border-color: transparent transparent transparent var(--comment-accent-color, #5aa0ff);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
span.comment-pin > span.comment-left::after,
|
|
18
|
+
.comment-overlay.comment-left::after {
|
|
19
|
+
border-color: transparent transparent transparent var(--comment-accent-background-color, #eaf3ff);
|
|
20
|
+
}
|
|
@@ -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 = (
|
|
@@ -70,19 +75,24 @@ def confirm_restore_comment_filter(active_mode):
|
|
|
70
75
|
"but you ran without the --no-comments flag.\n\n"
|
|
71
76
|
"Note that you have not locally edited the filter relative to "
|
|
72
77
|
"the library default.\n\n"
|
|
73
|
-
"Do you want to
|
|
78
|
+
"Do you want to update the filter and comment styles to the "
|
|
79
|
+
"library versions and show comments, or keep no comments?"
|
|
74
80
|
)
|
|
75
81
|
default_choice = "restore"
|
|
82
|
+
update_label = "update filter and styles; show comments"
|
|
83
|
+
keep_label = "keep no comments"
|
|
76
84
|
elif active_mode == "custom":
|
|
77
85
|
message = (
|
|
78
|
-
"The current lua filter
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
86
|
+
"The current lua filter differs from the pyDiffTools library "
|
|
87
|
+
"filters, but you ran without the --no-comments flag. It may be "
|
|
88
|
+
"an older distributed filter or a local edit.\n\n"
|
|
89
|
+
"Do you want to update the filter and comment styles to the "
|
|
90
|
+
"library versions and show comments, or keep the current "
|
|
91
|
+
"filter and styles?"
|
|
84
92
|
)
|
|
85
93
|
default_choice = "keep"
|
|
94
|
+
update_label = "update filter and styles; show comments"
|
|
95
|
+
keep_label = "keep current filter"
|
|
86
96
|
else:
|
|
87
97
|
raise ValueError(
|
|
88
98
|
"Comment filter restore prompt is only valid for custom or "
|
|
@@ -97,12 +107,8 @@ box = QMessageBox()
|
|
|
97
107
|
box.setWindowTitle("pydifft cpb")
|
|
98
108
|
box.setIcon(QMessageBox.Icon.Question)
|
|
99
109
|
box.setText(sys.argv[1])
|
|
100
|
-
keep_button = box.addButton(
|
|
101
|
-
|
|
102
|
-
)
|
|
103
|
-
restore_button = box.addButton(
|
|
104
|
-
"show comments", QMessageBox.ButtonRole.AcceptRole
|
|
105
|
-
)
|
|
110
|
+
keep_button = box.addButton(sys.argv[3], QMessageBox.ButtonRole.RejectRole)
|
|
111
|
+
restore_button = box.addButton(sys.argv[4], QMessageBox.ButtonRole.AcceptRole)
|
|
106
112
|
if sys.argv[2] == "restore":
|
|
107
113
|
box.setDefaultButton(restore_button)
|
|
108
114
|
else:
|
|
@@ -112,8 +118,12 @@ if box.clickedButton() is restore_button:
|
|
|
112
118
|
sys.exit(0)
|
|
113
119
|
sys.exit(1)
|
|
114
120
|
"""
|
|
121
|
+
prepare_for_dialog()
|
|
115
122
|
result = subprocess.run(
|
|
116
|
-
[
|
|
123
|
+
[
|
|
124
|
+
sys.executable, "-c", prompt_script, message, default_choice,
|
|
125
|
+
keep_label, update_label,
|
|
126
|
+
],
|
|
117
127
|
capture_output=True,
|
|
118
128
|
text=True,
|
|
119
129
|
)
|
|
@@ -127,6 +137,8 @@ sys.exit(1)
|
|
|
127
137
|
raise RuntimeError("pydifft cpb filter dialog failed.")
|
|
128
138
|
|
|
129
139
|
|
|
140
|
+
# also used by: tests/test_wrap_check.py, which drives this window directly
|
|
141
|
+
# (cpb calls it from run_pandoc)
|
|
130
142
|
def show_markdown_fix_dialog(report):
|
|
131
143
|
"""Show automatic fixes and any source edits that still need a user."""
|
|
132
144
|
from html import escape
|
|
@@ -148,6 +160,7 @@ def show_markdown_fix_dialog(report):
|
|
|
148
160
|
layout = report.get("layout", [])
|
|
149
161
|
if not fixes and not warnings and not layout:
|
|
150
162
|
return
|
|
163
|
+
prepare_for_dialog()
|
|
151
164
|
|
|
152
165
|
# a QApplication must exist before any widget is constructed
|
|
153
166
|
application = QApplication.instance() or QApplication([])
|
|
@@ -362,6 +375,8 @@ def show_markdown_fix_dialog(report):
|
|
|
362
375
|
return user_says_fixed
|
|
363
376
|
|
|
364
377
|
|
|
378
|
+
# also used by: tests/test_wrap_check.py, which drives this window directly
|
|
379
|
+
# (cpb calls it from run_pandoc)
|
|
365
380
|
def show_markdown_reload_dialog():
|
|
366
381
|
"""Ask the user to reload a source file changed by automatic fixes."""
|
|
367
382
|
from PySide6.QtWidgets import (
|
|
@@ -372,6 +387,7 @@ def show_markdown_reload_dialog():
|
|
|
372
387
|
QVBoxLayout,
|
|
373
388
|
)
|
|
374
389
|
|
|
390
|
+
prepare_for_dialog()
|
|
375
391
|
# a QApplication must exist before any widget is constructed
|
|
376
392
|
application = QApplication.instance() or QApplication([])
|
|
377
393
|
dialog = QDialog()
|
|
@@ -421,6 +437,10 @@ def run_pandoc(
|
|
|
421
437
|
break
|
|
422
438
|
|
|
423
439
|
if automatic_fixes_were_made:
|
|
440
|
+
# a corrected comment tag (e.g. <XYcomm> -> <XYcom>) can name an
|
|
441
|
+
# author with no XYcolor yet, so migrate comments again now rather
|
|
442
|
+
# than on the next save
|
|
443
|
+
prepare_comment_source(filename)
|
|
424
444
|
show_markdown_reload_dialog()
|
|
425
445
|
# }}}
|
|
426
446
|
|
|
@@ -514,6 +534,10 @@ def run_pandoc(
|
|
|
514
534
|
shutil.copy2(
|
|
515
535
|
packaged_filters["default"], active_filter
|
|
516
536
|
)
|
|
537
|
+
shutil.copy2(
|
|
538
|
+
os.path.join(package_dir, "comments.css"),
|
|
539
|
+
os.path.join(source_dir, "comments.css"),
|
|
540
|
+
)
|
|
517
541
|
effective_filter_mode = "default"
|
|
518
542
|
else:
|
|
519
543
|
effective_filter_mode = active_mode
|
|
@@ -565,7 +589,10 @@ def run_pandoc(
|
|
|
565
589
|
# }}}
|
|
566
590
|
# Only copy the comment UI assets when comments should be rendered.
|
|
567
591
|
if effective_filter_mode != "none":
|
|
568
|
-
for asset_name in [
|
|
592
|
+
for asset_name in [
|
|
593
|
+
"comments.css", "comments_author_colors.css",
|
|
594
|
+
"comment_toggle.js",
|
|
595
|
+
]:
|
|
569
596
|
target_path = os.path.join(source_dir, asset_name)
|
|
570
597
|
if not os.path.exists(target_path):
|
|
571
598
|
shutil.copy2(
|
|
@@ -909,7 +936,6 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
|
|
|
909
936
|
daemon=True,
|
|
910
937
|
)
|
|
911
938
|
chrome = None
|
|
912
|
-
notice = None
|
|
913
939
|
observer = None
|
|
914
940
|
observer_started = False
|
|
915
941
|
socket_thread_started = False
|
|
@@ -924,6 +950,9 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
|
|
|
924
950
|
# orphaned browser session. Remember the source version from before
|
|
925
951
|
# the build so an edit during a slow Pandoc run remains pending.
|
|
926
952
|
source_missing = object()
|
|
953
|
+
# every dialog is dealt with (OK pressed) before Pandoc runs or
|
|
954
|
+
# Chrome opens
|
|
955
|
+
zotero_notice()
|
|
927
956
|
prepare_comment_source(filename)
|
|
928
957
|
initial_source_stat = os.stat(source_path)
|
|
929
958
|
handled_signature = (
|
|
@@ -940,7 +969,6 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
|
|
|
940
969
|
wrapnumber=wrapnumber,
|
|
941
970
|
)
|
|
942
971
|
append_autorefresh(html_file)
|
|
943
|
-
notice = zotero_notice()
|
|
944
972
|
|
|
945
973
|
# Selenium is deliberately initialized once, after the first
|
|
946
974
|
# successful build. Nothing in recovery constructs a browser.
|
|
@@ -992,6 +1020,16 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
|
|
|
992
1020
|
# immediately after Chrome loads can become polling's initial snapshot
|
|
993
1021
|
# and never be reported as a change.
|
|
994
1022
|
chrome.get("file://" + os.path.abspath(html_file))
|
|
1023
|
+
|
|
1024
|
+
def close_for_dialog():
|
|
1025
|
+
# a rebuild needs the user, so close the preview while the
|
|
1026
|
+
# issues are dealt with; it reopens once the rebuild is done
|
|
1027
|
+
nonlocal chrome
|
|
1028
|
+
if chrome is not None:
|
|
1029
|
+
close_browser_window(chrome)
|
|
1030
|
+
chrome = None
|
|
1031
|
+
|
|
1032
|
+
dialog_callbacks.append(close_for_dialog)
|
|
995
1033
|
rebuild_pending = False
|
|
996
1034
|
stable_signature = None
|
|
997
1035
|
stable_since = None
|
|
@@ -1102,18 +1140,26 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
|
|
|
1102
1140
|
traceback.print_exc()
|
|
1103
1141
|
else:
|
|
1104
1142
|
handled_signature = attempted_signature
|
|
1105
|
-
if not
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1143
|
+
if chrome is not None:
|
|
1144
|
+
if not browser_window_is_alive(chrome):
|
|
1145
|
+
break
|
|
1146
|
+
try:
|
|
1147
|
+
chrome.refresh()
|
|
1148
|
+
except WebDriverException:
|
|
1149
|
+
print(
|
|
1150
|
+
"pydifft cpb: Chrome is no longer "
|
|
1151
|
+
"available; stopping without "
|
|
1152
|
+
"reopening it.",
|
|
1153
|
+
file=sys.stderr,
|
|
1154
|
+
)
|
|
1155
|
+
break
|
|
1156
|
+
if chrome is None:
|
|
1157
|
+
# {{{ reopen the preview a dialog closed
|
|
1158
|
+
chrome = webdriver.Chrome()
|
|
1159
|
+
chrome.get(
|
|
1160
|
+
"file://" + os.path.abspath(html_file)
|
|
1161
|
+
)
|
|
1162
|
+
# }}}
|
|
1117
1163
|
# }}}
|
|
1118
1164
|
time.sleep(MAIN_LOOP_INTERVAL_SECONDS)
|
|
1119
1165
|
except KeyboardInterrupt:
|
|
@@ -1127,10 +1173,7 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
|
|
|
1127
1173
|
if socket_thread_started:
|
|
1128
1174
|
socket_thread.join()
|
|
1129
1175
|
close_browser_window(chrome)
|
|
1130
|
-
|
|
1131
|
-
if notice.poll() is None:
|
|
1132
|
-
notice.terminate()
|
|
1133
|
-
notice.wait()
|
|
1176
|
+
dialog_callbacks.clear()
|
|
1134
1177
|
|
|
1135
1178
|
|
|
1136
1179
|
if __name__ == "__main__":
|