pyDiffTools 0.1.45__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.
Files changed (114) hide show
  1. {pydifftools-0.1.45/pyDiffTools.egg-info → pydifftools-0.1.47}/PKG-INFO +1 -1
  2. {pydifftools-0.1.45 → pydifftools-0.1.47/pyDiffTools.egg-info}/PKG-INFO +1 -1
  3. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/bibliography.py +20 -6
  4. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/browser_lifecycle.py +13 -0
  5. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/citation_dialog.py +50 -18
  6. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/comment_migration.py +5 -0
  7. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/continuous.py +50 -18
  8. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/wrap_sentences.py +49 -15
  9. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/zotero.py +157 -31
  10. {pydifftools-0.1.45 → pydifftools-0.1.47}/pyproject.toml +1 -1
  11. {pydifftools-0.1.45 → pydifftools-0.1.47}/tests/test_check_single_use.py +32 -0
  12. {pydifftools-0.1.45 → pydifftools-0.1.47}/tests/test_continuous_shutdown.py +68 -11
  13. {pydifftools-0.1.45 → pydifftools-0.1.47}/tests/test_wrap_check.py +61 -3
  14. {pydifftools-0.1.45 → pydifftools-0.1.47}/tests/test_zotero.py +116 -7
  15. {pydifftools-0.1.45 → pydifftools-0.1.47}/LICENSE.md +0 -0
  16. {pydifftools-0.1.45 → pydifftools-0.1.47}/MANIFEST.in +0 -0
  17. {pydifftools-0.1.45 → pydifftools-0.1.47}/README.rst +0 -0
  18. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/_quarto.yml +0 -0
  19. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  20. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  21. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  22. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  23. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  24. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  25. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  26. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  27. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  28. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  29. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  30. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  31. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  32. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  33. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  34. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  35. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  36. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  37. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  38. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  39. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  40. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  41. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/example.qmd +0 -0
  42. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/example.tex +0 -0
  43. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/index.qmd +0 -0
  44. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  45. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  46. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  47. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  48. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/subproject1/independent.qmd +0 -0
  49. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/subproject1/index.qmd +0 -0
  50. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  51. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  52. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  53. {pydifftools-0.1.45 → pydifftools-0.1.47}/example_notebook/project1/test_include.qmd +0 -0
  54. {pydifftools-0.1.45 → pydifftools-0.1.47}/pyDiffTools.egg-info/SOURCES.txt +0 -0
  55. {pydifftools-0.1.45 → pydifftools-0.1.47}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  56. {pydifftools-0.1.45 → pydifftools-0.1.47}/pyDiffTools.egg-info/entry_points.txt +0 -0
  57. {pydifftools-0.1.45 → pydifftools-0.1.47}/pyDiffTools.egg-info/requires.txt +0 -0
  58. {pydifftools-0.1.45 → pydifftools-0.1.47}/pyDiffTools.egg-info/top_level.txt +0 -0
  59. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/__init__.py +0 -0
  60. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/check_numbers.py +0 -0
  61. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/command_line.py +0 -0
  62. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/command_registry.py +0 -0
  63. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/comment_functions.py +0 -0
  64. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/comment_tags.lua +0 -0
  65. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/comment_tags_margin.lua +0 -0
  66. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/comment_tags_no_comments.lua +0 -0
  67. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/comment_toggle.js +0 -0
  68. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/comments.css +0 -0
  69. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/copy_files.py +0 -0
  70. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/diff-doc.js +0 -0
  71. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/doc_contents.py +0 -0
  72. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/flowchart/__init__.py +0 -0
  73. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/flowchart/comparison.py +0 -0
  74. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  75. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/flowchart/graph.py +0 -0
  76. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  77. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/flowchart/watch_graph.py +0 -0
  78. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/flowchart/wgrph_view.js +0 -0
  79. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/forward_search.py +0 -0
  80. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/git_gd.py +0 -0
  81. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/git_gd_history.py +0 -0
  82. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/git_gd_image.py +0 -0
  83. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/git_gd_qt.py +0 -0
  84. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/html_comments.py +0 -0
  85. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/html_uncomments.py +0 -0
  86. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/log_example.py +0 -0
  87. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/match_spaces.py +0 -0
  88. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/notebook/__init__.py +0 -0
  89. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/notebook/display.py +0 -0
  90. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/notebook/fast_build.py +0 -0
  91. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/notebook/tex_to_qmd.py +0 -0
  92. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/onewordify.py +0 -0
  93. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/onewordify_undo.py +0 -0
  94. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/outline.py +0 -0
  95. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/rearrange_tex.py +0 -0
  96. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/searchacro.py +0 -0
  97. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/separate_comments.py +0 -0
  98. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/split_conflict.py +0 -0
  99. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/unseparate_comments.py +0 -0
  100. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/update_check.py +0 -0
  101. {pydifftools-0.1.45 → pydifftools-0.1.47}/pydifftools/xml2xlsx.vbs +0 -0
  102. {pydifftools-0.1.45 → pydifftools-0.1.47}/setup.cfg +0 -0
  103. {pydifftools-0.1.45 → pydifftools-0.1.47}/tests/test_bibliography.py +0 -0
  104. {pydifftools-0.1.45 → pydifftools-0.1.47}/tests/test_browser_lifecycle.py +0 -0
  105. {pydifftools-0.1.45 → pydifftools-0.1.47}/tests/test_command_line_help.py +0 -0
  106. {pydifftools-0.1.45 → pydifftools-0.1.47}/tests/test_comment_migration.py +0 -0
  107. {pydifftools-0.1.45 → pydifftools-0.1.47}/tests/test_forward_search.py +0 -0
  108. {pydifftools-0.1.45 → pydifftools-0.1.47}/tests/test_git_gd_history.py +0 -0
  109. {pydifftools-0.1.45 → pydifftools-0.1.47}/tests/test_git_gd_image.py +0 -0
  110. {pydifftools-0.1.45 → pydifftools-0.1.47}/tests/test_git_gd_qt.py +0 -0
  111. {pydifftools-0.1.45 → pydifftools-0.1.47}/tests/test_rrng.py +0 -0
  112. {pydifftools-0.1.45 → pydifftools-0.1.47}/tests/test_tex_to_qmd.py +0 -0
  113. {pydifftools-0.1.45 → pydifftools-0.1.47}/tests/test_update_check.py +0 -0
  114. {pydifftools-0.1.45 → pydifftools-0.1.47}/tests/test_wrapping.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.45
3
+ Version: 0.1.47
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.45
3
+ Version: 0.1.47
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -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
@@ -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 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
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
- if notice is not None:
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 and detached crossref markers
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
- elif fence is None and re.search(r"(?<! ) $", line):
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
@@ -1207,8 +1223,8 @@ def apply_markdown_issue_fix(content, line_number, message):
1207
1223
  updated,
1208
1224
  before,
1209
1225
  after,
1210
- "A crossref marker was separated from its equation or figure. "
1211
- "I joined it back on.",
1226
+ "One or more crossref markers were separated from their "
1227
+ "equation or figure. I joined them back on.",
1212
1228
  )
1213
1229
  # }}}
1214
1230
  old_line = lines[index]
@@ -1233,13 +1249,28 @@ 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
+ "&lt;JFcom&gt;. I corrected them.",
1265
+ )
1236
1266
  if message.startswith("trailing space:"):
1237
1267
  lines[index] = old_line[:-1] + ending
1238
1268
  return (
1239
1269
  "".join(lines),
1240
1270
  old_line,
1241
1271
  old_line[:-1],
1242
- "A line ended with a single stray space. I removed it.",
1272
+ "One or more lines ended with a single stray space. I removed "
1273
+ "those spaces.",
1243
1274
  )
1244
1275
  if message.startswith("line too long:"):
1245
1276
  match = re.search(r"after '(.+?)' \(before '(.+?)'\); move", message)
@@ -1263,8 +1294,8 @@ def apply_markdown_issue_fix(content, line_number, message):
1263
1294
  first = old_line[:split_at].rstrip()
1264
1295
  second = old_line[split_at:].lstrip(" \t")
1265
1296
  reason = (
1266
- "This line was hard to read because it held too many words. "
1267
- "I moved the next words onto a new line."
1297
+ "One or more lines were run-on lines, longer than the line "
1298
+ "width. I moved the extra words onto new lines."
1268
1299
  )
1269
1300
  elif message.startswith("sentence ends mid-line"):
1270
1301
  boundary = SENTENCE_BOUNDARY.search(old_line)
@@ -1276,8 +1307,11 @@ def apply_markdown_issue_fix(content, line_number, message):
1276
1307
  first = old_line[: boundary.end(1)].rstrip()
1277
1308
  second = old_line[boundary.end(2) :].lstrip(" \t")
1278
1309
  reason = (
1279
- "A sentence ended, but the next sentence continued on the same "
1280
- "source line. I started it on a new line."
1310
+ "One or more sentences started in the middle of a source line. "
1311
+ "Since you can line-break the source wherever you want (only "
1312
+ "double breaks give a new paragraph), you should <b>always "
1313
+ "start sentences on new lines</b>. I started each one on a new "
1314
+ "line."
1281
1315
  )
1282
1316
  else:
1283
1317
  raise ValueError(f"No automatic fix is available for {message!r}")
@@ -1614,15 +1648,15 @@ def autofix_markdown_file(
1614
1648
  reasons = []
1615
1649
  if result["linted"]:
1616
1650
  reasons.append(
1617
- "This edit made a line too long or ended a sentence "
1618
- "mid-line. I broke it where it keeps the most lines "
1619
- "unchanged from git HEAD."
1651
+ "One or more edits made a line too long or ended a "
1652
+ "sentence mid-line. I broke those lines where they "
1653
+ "keep the most lines unchanged from git HEAD."
1620
1654
  )
1621
1655
  if result["moved"]:
1622
1656
  reasons.append(
1623
- "This edit moved a line break away from where git "
1624
- "HEAD has it. I put it back so the unchanged lines "
1625
- "stay unchanged."
1657
+ "One or more edits moved a line break away from "
1658
+ "where git HEAD has it. I put those breaks back so "
1659
+ "the unchanged lines stay unchanged."
1626
1660
  )
1627
1661
  fixes.append(
1628
1662
  {