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.
Files changed (115) hide show
  1. {pydifftools-0.1.46 → pydifftools-0.1.48}/MANIFEST.in +1 -0
  2. {pydifftools-0.1.46/pyDiffTools.egg-info → pydifftools-0.1.48}/PKG-INFO +1 -1
  3. {pydifftools-0.1.46 → pydifftools-0.1.48/pyDiffTools.egg-info}/PKG-INFO +1 -1
  4. {pydifftools-0.1.46 → pydifftools-0.1.48}/pyDiffTools.egg-info/SOURCES.txt +1 -0
  5. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/bibliography.py +20 -6
  6. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/browser_lifecycle.py +13 -0
  7. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/citation_dialog.py +50 -18
  8. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/comment_migration.py +5 -0
  9. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/comment_tags.lua +21 -3
  10. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/comment_tags_margin.lua +21 -3
  11. pydifftools-0.1.48/pydifftools/comments_author_colors.css +20 -0
  12. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/continuous.py +76 -33
  13. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/wrap_sentences.py +32 -2
  14. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/zotero.py +157 -31
  15. {pydifftools-0.1.46 → pydifftools-0.1.48}/pyproject.toml +2 -1
  16. {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_check_single_use.py +32 -0
  17. {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_continuous_shutdown.py +68 -11
  18. {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_wrap_check.py +38 -0
  19. {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_zotero.py +116 -7
  20. {pydifftools-0.1.46 → pydifftools-0.1.48}/LICENSE.md +0 -0
  21. {pydifftools-0.1.46 → pydifftools-0.1.48}/README.rst +0 -0
  22. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/_quarto.yml +0 -0
  23. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  24. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  25. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  26. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  27. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  28. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  29. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  30. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  31. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  32. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  33. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  34. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  35. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  36. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  37. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  38. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  39. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  40. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  41. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  42. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  43. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  44. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  45. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/example.qmd +0 -0
  46. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/example.tex +0 -0
  47. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/index.qmd +0 -0
  48. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  49. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  50. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  51. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  52. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/independent.qmd +0 -0
  53. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/index.qmd +0 -0
  54. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  55. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  56. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  57. {pydifftools-0.1.46 → pydifftools-0.1.48}/example_notebook/project1/test_include.qmd +0 -0
  58. {pydifftools-0.1.46 → pydifftools-0.1.48}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  59. {pydifftools-0.1.46 → pydifftools-0.1.48}/pyDiffTools.egg-info/entry_points.txt +0 -0
  60. {pydifftools-0.1.46 → pydifftools-0.1.48}/pyDiffTools.egg-info/requires.txt +0 -0
  61. {pydifftools-0.1.46 → pydifftools-0.1.48}/pyDiffTools.egg-info/top_level.txt +0 -0
  62. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/__init__.py +0 -0
  63. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/check_numbers.py +0 -0
  64. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/command_line.py +0 -0
  65. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/command_registry.py +0 -0
  66. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/comment_functions.py +0 -0
  67. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/comment_tags_no_comments.lua +0 -0
  68. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/comment_toggle.js +0 -0
  69. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/comments.css +0 -0
  70. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/copy_files.py +0 -0
  71. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/diff-doc.js +0 -0
  72. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/doc_contents.py +0 -0
  73. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/flowchart/__init__.py +0 -0
  74. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/flowchart/comparison.py +0 -0
  75. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  76. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/flowchart/graph.py +0 -0
  77. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  78. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/flowchart/watch_graph.py +0 -0
  79. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/flowchart/wgrph_view.js +0 -0
  80. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/forward_search.py +0 -0
  81. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/git_gd.py +0 -0
  82. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/git_gd_history.py +0 -0
  83. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/git_gd_image.py +0 -0
  84. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/git_gd_qt.py +0 -0
  85. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/html_comments.py +0 -0
  86. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/html_uncomments.py +0 -0
  87. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/log_example.py +0 -0
  88. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/match_spaces.py +0 -0
  89. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/notebook/__init__.py +0 -0
  90. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/notebook/display.py +0 -0
  91. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/notebook/fast_build.py +0 -0
  92. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/notebook/tex_to_qmd.py +0 -0
  93. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/onewordify.py +0 -0
  94. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/onewordify_undo.py +0 -0
  95. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/outline.py +0 -0
  96. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/rearrange_tex.py +0 -0
  97. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/searchacro.py +0 -0
  98. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/separate_comments.py +0 -0
  99. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/split_conflict.py +0 -0
  100. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/unseparate_comments.py +0 -0
  101. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/update_check.py +0 -0
  102. {pydifftools-0.1.46 → pydifftools-0.1.48}/pydifftools/xml2xlsx.vbs +0 -0
  103. {pydifftools-0.1.46 → pydifftools-0.1.48}/setup.cfg +0 -0
  104. {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_bibliography.py +0 -0
  105. {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_browser_lifecycle.py +0 -0
  106. {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_command_line_help.py +0 -0
  107. {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_comment_migration.py +0 -0
  108. {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_forward_search.py +0 -0
  109. {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_git_gd_history.py +0 -0
  110. {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_git_gd_image.py +0 -0
  111. {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_git_gd_qt.py +0 -0
  112. {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_rrng.py +0 -0
  113. {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_tex_to_qmd.py +0 -0
  114. {pydifftools-0.1.46 → pydifftools-0.1.48}/tests/test_update_check.py +0 -0
  115. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.46
3
+ Version: 0.1.48
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.46
3
+ Version: 0.1.48
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -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 bib_entries(text):
68
- """Index entry spans and raw fields without reserializing them."""
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
- if key in entries:
110
- raise ValueError(f"duplicate BibTeX key {key}")
111
- entries[key] = BibEntry(start, end + 1, kind, key, fields)
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
- explanation = QLabel(
33
- f"Review the imported citation for {source}.\n"
34
- "Keeping or merging a duplicate also replaces @UNCHOSEN with "
35
- "@CHOSEN in this Markdown file. Other documents are not edited.\n"
36
- "For a merge, choose each field below, including the citation key."
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
- ["Field", "Existing bibliography", "Zotero", "Merge uses"]
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("Existing", self.left[name])
129
+ selector.addItem(self.labels[0], self.left[name])
98
130
  if name in self.right:
99
- selector.addItem("Zotero", self.right[name])
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"] = "--comment-accent-color: " .. spec.color .. ";"
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 ("--comment-accent-color: " .. spec.color .. ";") or nil,
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 .. "--comment-accent-color: " .. color .. ";"
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 (" --comment-accent-color: " .. spec.color .. ";") or ""),
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 (" --comment-accent-color: " .. spec.color .. ";") or ""),
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 .. "--comment-accent-color: " .. color .. ";"
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 show comments, or continue with no comments?"
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 does not match the pyDiffTools library "
79
- "default or the no-comments filter, but you ran without the "
80
- "--no-comments flag.\n\n"
81
- "Note that this looks like a locally edited filter.\n\n"
82
- "Do you want to show comments using the library default, or "
83
- "continue with no comments?"
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
- "no comments", QMessageBox.ButtonRole.RejectRole
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
- [sys.executable, "-c", prompt_script, message, default_choice],
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 ["comments.css", "comment_toggle.js"]:
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 browser_window_is_alive(chrome):
1106
- break
1107
- try:
1108
- chrome.refresh()
1109
- except WebDriverException:
1110
- print(
1111
- "pydifft cpb: Chrome is no longer "
1112
- "available; stopping without reopening "
1113
- "it.",
1114
- file=sys.stderr,
1115
- )
1116
- break
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
- if notice is not None:
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__":