pyDiffTools 0.1.57__tar.gz → 0.1.58__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 (130) hide show
  1. {pydifftools-0.1.57/pyDiffTools.egg-info → pydifftools-0.1.58}/PKG-INFO +1 -1
  2. {pydifftools-0.1.57 → pydifftools-0.1.58/pyDiffTools.egg-info}/PKG-INFO +1 -1
  3. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/browser_lifecycle.py +92 -12
  4. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/command_line.py +11 -21
  5. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/continuous.py +31 -46
  6. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/wrap_sentences.py +11 -2
  7. {pydifftools-0.1.57 → pydifftools-0.1.58}/pyproject.toml +1 -1
  8. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_browser_lifecycle.py +82 -0
  9. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_continuous_shutdown.py +89 -72
  10. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_forward_search.py +1 -1
  11. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_wrap_check.py +40 -0
  12. {pydifftools-0.1.57 → pydifftools-0.1.58}/LICENSE.md +0 -0
  13. {pydifftools-0.1.57 → pydifftools-0.1.58}/MANIFEST.in +0 -0
  14. {pydifftools-0.1.57 → pydifftools-0.1.58}/README.rst +0 -0
  15. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/_quarto.yml +0 -0
  16. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  17. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  18. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  19. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  20. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  21. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  22. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  23. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  24. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  25. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  26. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  27. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  28. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  29. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  30. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  31. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  32. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  33. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  34. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  35. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  36. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  37. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  38. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/example.qmd +0 -0
  39. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/example.tex +0 -0
  40. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/index.qmd +0 -0
  41. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  42. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  43. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  44. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  45. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/subproject1/independent.qmd +0 -0
  46. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/subproject1/index.qmd +0 -0
  47. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  48. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  49. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  50. {pydifftools-0.1.57 → pydifftools-0.1.58}/example_notebook/project1/test_include.qmd +0 -0
  51. {pydifftools-0.1.57 → pydifftools-0.1.58}/pyDiffTools.egg-info/SOURCES.txt +0 -0
  52. {pydifftools-0.1.57 → pydifftools-0.1.58}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  53. {pydifftools-0.1.57 → pydifftools-0.1.58}/pyDiffTools.egg-info/entry_points.txt +0 -0
  54. {pydifftools-0.1.57 → pydifftools-0.1.58}/pyDiffTools.egg-info/requires.txt +0 -0
  55. {pydifftools-0.1.57 → pydifftools-0.1.58}/pyDiffTools.egg-info/top_level.txt +0 -0
  56. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/__init__.py +0 -0
  57. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/bibliography.py +0 -0
  58. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/check_numbers.py +0 -0
  59. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/citation_dialog.py +0 -0
  60. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/command_registry.py +0 -0
  61. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/comment_filter_history.json +0 -0
  62. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/comment_functions.py +0 -0
  63. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/comment_migration.py +0 -0
  64. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/comment_tags.lua +0 -0
  65. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/comment_tags_margin.lua +0 -0
  66. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/comment_tags_no_comments.lua +0 -0
  67. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/comment_toggle.js +0 -0
  68. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/comments.css +0 -0
  69. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/comments_author_colors.css +0 -0
  70. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/completions/git-mergein +0 -0
  71. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/copy_files.py +0 -0
  72. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/diff-doc.js +0 -0
  73. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/doc_contents.py +0 -0
  74. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/flowchart/__init__.py +0 -0
  75. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/flowchart/comparison.py +0 -0
  76. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  77. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/flowchart/graph.py +0 -0
  78. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/flowchart/source_jump.js +0 -0
  79. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  80. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/flowchart/watch_graph.py +0 -0
  81. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/flowchart/wgrph_view.js +0 -0
  82. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/forward_search.py +0 -0
  83. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/git_aliases.py +0 -0
  84. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/git_gd.py +0 -0
  85. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/git_gd_history.py +0 -0
  86. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/git_gd_image.py +0 -0
  87. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/git_gd_qt.py +0 -0
  88. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/html_comments.py +0 -0
  89. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/html_uncomments.py +0 -0
  90. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/line_alignment.py +0 -0
  91. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/log_example.py +0 -0
  92. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/match_spaces.py +0 -0
  93. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/notebook/__init__.py +0 -0
  94. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/notebook/display.py +0 -0
  95. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/notebook/fast_build.py +0 -0
  96. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/notebook/tex_to_qmd.py +0 -0
  97. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/onewordify.py +0 -0
  98. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/onewordify_undo.py +0 -0
  99. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/outline.py +0 -0
  100. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/pdf_diff.py +0 -0
  101. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/rearrange_tex.py +0 -0
  102. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/searchacro.py +0 -0
  103. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/separate_comments.py +0 -0
  104. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/source_jump.py +0 -0
  105. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/split_conflict.py +0 -0
  106. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/unseparate_comments.py +0 -0
  107. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/update_check.py +0 -0
  108. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/wrapping_options.py +0 -0
  109. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/xml2xlsx.vbs +0 -0
  110. {pydifftools-0.1.57 → pydifftools-0.1.58}/pydifftools/zotero.py +0 -0
  111. {pydifftools-0.1.57 → pydifftools-0.1.58}/setup.cfg +0 -0
  112. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_bibliography.py +0 -0
  113. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_check_single_use.py +0 -0
  114. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_command_line_help.py +0 -0
  115. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_comment_filters.py +0 -0
  116. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_comment_migration.py +0 -0
  117. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_git_gd_history.py +0 -0
  118. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_git_gd_image.py +0 -0
  119. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_git_gd_qt.py +0 -0
  120. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_git_mergein.py +0 -0
  121. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_git_pdf_diff.py +0 -0
  122. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_line_alignment.py +0 -0
  123. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_pdf_diff.py +0 -0
  124. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_qt_lifecycle.py +0 -0
  125. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_rrng.py +0 -0
  126. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_source_jump.py +0 -0
  127. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_tex_to_qmd.py +0 -0
  128. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_update_check.py +0 -0
  129. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_wrapping.py +0 -0
  130. {pydifftools-0.1.57 → pydifftools-0.1.58}/tests/test_zotero.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.57
3
+ Version: 0.1.58
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.57
3
+ Version: 0.1.58
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -4,8 +4,7 @@ import shutil
4
4
  import subprocess
5
5
  import sys
6
6
 
7
- # callbacks that run before any dialog needs the user; cpb registers one
8
- # that closes its live preview, so issues are dealt with before it reopens
7
+ # Optional callbacks for callers that need to prepare for a dialog.
9
8
  dialog_callbacks = []
10
9
 
11
10
 
@@ -97,7 +96,7 @@ def start_browser():
97
96
 
98
97
 
99
98
  def prepare_for_dialog():
100
- """Let watch commands close their preview before a dialog appears."""
99
+ """Notify registered callers before a dialog appears."""
101
100
  for callback in list(dialog_callbacks):
102
101
  callback()
103
102
 
@@ -108,6 +107,10 @@ def browser_window_is_alive(browser):
108
107
  # the same shutdown behavior when a user closes the browser window.
109
108
  # Do not probe with execute_script here: page navigations can briefly
110
109
  # interrupt script execution even while the window is still open.
110
+ from selenium.common.exceptions import (
111
+ InvalidSessionIdException, NoSuchWindowException, WebDriverException,
112
+ )
113
+
111
114
  if browser is None:
112
115
  return False
113
116
  try:
@@ -115,10 +118,14 @@ def browser_window_is_alive(browser):
115
118
  if not handles:
116
119
  return False
117
120
  return True
118
- except Exception:
121
+ except (InvalidSessionIdException, NoSuchWindowException):
119
122
  return False
123
+ except WebDriverException:
124
+ # A transient command failure is not evidence of a closed window.
125
+ return True
120
126
 
121
127
 
128
+ # also used by: continuous.py, notebook/fast_build.py, flowchart/watch_graph.py
122
129
  def close_browser_window(browser):
123
130
  # Close a Selenium browser session and ignore errors from already-closed
124
131
  # windows so cleanup paths stay simple.
@@ -137,20 +144,93 @@ def forward_search_in_browser(browser, search_text):
137
144
  if browser is None or not search_text:
138
145
  return False
139
146
  found = browser.execute_script(
140
- """
141
- var searchText = arguments[0];
142
- if (!window.find) {
143
- return false;
147
+ r"""
148
+ const searchText = arguments[0];
149
+ let didFind = window.find && window.find(searchText);
150
+ // Match across inline elements, entities, and rendered references.
151
+ if (!didFind) {
152
+ const escape = text => text.replace(
153
+ /[.*+?^${}()|[\]\\]/g, '\\$&'
154
+ ).replace(/\s+/g, '\\s+');
155
+ const reference =
156
+ /\[?@([a-z]{2,4}):(\w(?:[\w:./-]*[\w/-])?)\]?/gi;
157
+ let pattern = '', cursor = 0, match;
158
+ let valid = true;
159
+ while ((match = reference.exec(searchText))) {
160
+ pattern += escape(searchText.slice(cursor, match.index));
161
+ const id = match[1].toLowerCase() + ':' + match[2];
162
+ const link = Array.from(
163
+ document.querySelectorAll('a[href]')
164
+ ).find(a => a.getAttribute('href') === '#' + id);
165
+ const target = document.getElementById(id);
166
+ if (!link && !target) {
167
+ valid = false;
168
+ break;
169
+ }
170
+ // Unlinked crossrefs still render a numeric label. For a
171
+ // reference alone, select its target rather than guessing
172
+ // which occurrence of that number the user intended.
173
+ if (!link && searchText.trim() === match[0]) {
174
+ const range = document.createRange();
175
+ range.selectNodeContents(target);
176
+ const selection = window.getSelection();
177
+ selection.removeAllRanges();
178
+ selection.addRange(range);
179
+ didFind = true;
180
+ valid = false;
181
+ break;
182
+ }
183
+ pattern += '(?:(?:eq(?:uation)?|fig(?:ure)?|tab(?:le)?|' +
184
+ 'sec(?:tion)?|ch(?:apter)?|lst|listing)s?\\.?\\s*)?' +
185
+ (link ? escape(link.textContent.trim()) :
186
+ '\\(?\\d+(?:[.–-]\\d+)*[a-z]?\\)?');
187
+ cursor = reference.lastIndex;
188
+ }
189
+ pattern += escape(searchText.slice(cursor));
190
+ if (valid) {
191
+ const walker = document.createTreeWalker(
192
+ document.body, NodeFilter.SHOW_TEXT
193
+ );
194
+ const nodes = [], starts = [];
195
+ let text = '', node;
196
+ while ((node = walker.nextNode())) {
197
+ if (node.parentElement.closest(
198
+ 'script, style, noscript, [hidden]'
199
+ ) || !node.parentElement.getClientRects().length) {
200
+ continue;
201
+ }
202
+ starts.push(text.length);
203
+ nodes.push(node);
204
+ text += node.textContent;
205
+ }
206
+ const result = new RegExp(pattern, 'i').exec(text);
207
+ if (result && result[0].length) {
208
+ const start = result.index;
209
+ const end = start + result[0].length;
210
+ const first = starts.findIndex((offset, i) =>
211
+ offset + nodes[i].length > start
212
+ );
213
+ const last = starts.findIndex((offset, i) =>
214
+ offset + nodes[i].length >= end
215
+ );
216
+ const range = document.createRange();
217
+ range.setStart(nodes[first], start - starts[first]);
218
+ range.setEnd(nodes[last], end - starts[last]);
219
+ const selection = window.getSelection();
220
+ selection.removeAllRanges();
221
+ selection.addRange(range);
222
+ didFind = true;
223
+ }
224
+ }
144
225
  }
145
- var didFind = window.find(searchText);
146
226
  if (didFind && window.getSelection) {
147
- var selection = window.getSelection();
227
+ const selection = window.getSelection();
148
228
  if (selection.rangeCount > 0) {
149
- var rect = selection.getRangeAt(0).getBoundingClientRect();
229
+ const rect = selection.getRangeAt(0).getBoundingClientRect();
150
230
  window.scrollBy(0, rect.top - window.innerHeight / 3);
151
231
  }
152
232
  }
153
- return didFind;
233
+ return Boolean(didFind);
154
234
  """,
155
235
  search_text,
156
236
  )
@@ -1,6 +1,7 @@
1
1
  # PYTHON_ARGCOMPLETE_OK
2
2
 
3
3
  import argparse
4
+ import html
4
5
  import sys
5
6
  import os
6
7
  import gzip
@@ -554,26 +555,14 @@ def fs(arguments):
554
555
  )
555
556
  def mfs(text):
556
557
  # Normalize markdown-specific markup so search phrases match rendered text.
557
- search_text = text
558
- marker_match = re.search(r"(?i)@[a-z]{2,4}:", search_text)
559
- if marker_match:
560
- search_text = search_text[: marker_match.start()]
561
- search_text = re.sub(r"\[@[^\]]+\]", " ", search_text)
562
- # Strip paired markup delimiters (bold/italic, strikethrough,
563
- # superscript/subscript). These delimiters hug the text they wrap with no
564
- # space in the rendered output (e.g. "x^2^" renders as "x2"), so they are
565
- # dropped rather than replaced with a space. Plain str.replace is used
566
- # (not regex) so this is linear time regardless of how many delimiter
567
- # characters appear.
568
- search_text = (
569
- search_text.replace("**", "")
570
- .replace("__", "")
571
- .replace("~~", "")
572
- .replace("*", "")
573
- .replace("_", "")
574
- .replace("~", "")
575
- .replace("^", "")
576
- )
558
+ search_text = html.unescape(text)
559
+ search_text = re.sub(r"\[@(?![a-z]{2,4}:)[^\]]+\]", " ", search_text,
560
+ flags=re.IGNORECASE)
561
+ # Keep reference IDs intact while removing surrounding Markdown markup.
562
+ parts = re.split(r"(?i)(@[a-z]{2,4}:\w(?:[\w:./-]*[\w/-])?)", search_text)
563
+ for index in range(0, len(parts), 2):
564
+ parts[index] = re.sub(r"[*_~^]", "", parts[index])
565
+ search_text = "".join(parts)
577
566
  search_text = re.sub(r"\s+", " ", search_text).strip()
578
567
  if not search_text:
579
568
  search_text = text.strip()
@@ -620,7 +609,8 @@ def mfs(text):
620
609
  if not filename.endswith(".md"):
621
610
  continue
622
611
  with open(filename, encoding="utf-8") as fp:
623
- if search_text in fp.read():
612
+ source_text = fp.read()
613
+ if text.strip() in source_text or search_text in source_text:
624
614
  matching_files.append(filename)
625
615
  if len(matching_files) == 0:
626
616
  raise RuntimeError(
@@ -11,6 +11,7 @@ import re
11
11
  import shutil
12
12
  import threading
13
13
  import queue
14
+ import signal
14
15
  import traceback
15
16
  import tempfile
16
17
  from pathlib import Path
@@ -27,7 +28,6 @@ from .comment_migration import prepare_comment_source
27
28
  from .browser_lifecycle import (
28
29
  browser_window_is_alive,
29
30
  close_browser_window,
30
- dialog_callbacks,
31
31
  forward_search_in_browser,
32
32
  prepare_for_dialog,
33
33
  start_browser,
@@ -1231,11 +1231,16 @@ def cpb(
1231
1231
  daemon=True,
1232
1232
  )
1233
1233
  chrome = None
1234
- scroll_position = 0
1235
1234
  observer = None
1236
1235
  observer_started = False
1237
1236
  socket_thread_started = False
1238
1237
  source_jump_server = None
1238
+ previous_sigterm = None
1239
+ if threading.current_thread() is threading.main_thread():
1240
+ def stop_cpb(_signum, _frame):
1241
+ raise KeyboardInterrupt
1242
+
1243
+ previous_sigterm = signal.signal(signal.SIGTERM, stop_cpb)
1239
1244
 
1240
1245
  try:
1241
1246
  # Bind and serve before any build or browser work. This makes the fixed
@@ -1321,29 +1326,18 @@ def cpb(
1321
1326
  # and never be reported as a change.
1322
1327
  chrome.get("file://" + os.path.abspath(html_file))
1323
1328
 
1324
- def close_for_dialog():
1325
- # a rebuild needs the user, so close the preview while the
1326
- # issues are dealt with; it reopens once the rebuild is done
1327
- nonlocal chrome, scroll_position
1328
- if chrome is not None:
1329
- # sessionStorage does not survive quitting the browser.
1330
- scroll_position = chrome.execute_script(
1331
- "return window.scrollY;"
1332
- )
1333
- close_browser_window(chrome)
1334
- chrome = None
1335
-
1336
- dialog_callbacks.append(close_for_dialog)
1337
1329
  rebuild_pending = False
1338
1330
  stable_signature = None
1339
1331
  stable_since = None
1332
+ listener_warning_shown = False
1340
1333
  while True:
1341
- if not socket_thread.is_alive():
1342
- raise RuntimeError(
1343
- "The cpb forward-search listener stopped unexpectedly; "
1344
- "closing the preview instead of leaving an "
1345
- "undiscoverable session."
1334
+ if not socket_thread.is_alive() and not listener_warning_shown:
1335
+ print(
1336
+ "pydifft cpb: forward-search listener stopped; "
1337
+ "forward search is unavailable. The preview remains open.",
1338
+ file=sys.stderr,
1346
1339
  )
1340
+ listener_warning_shown = True
1347
1341
  # Browser closure is terminal. Source-file and build failures do
1348
1342
  # not affect browser ownership or observer lifetime.
1349
1343
  if not browser_window_is_alive(chrome):
@@ -1352,7 +1346,11 @@ def cpb(
1352
1346
  for queued_search in drain_forward_search_queue(search_queue):
1353
1347
  search_text = queued_search.strip()
1354
1348
  if search_text:
1355
- forward_search_in_browser(chrome, search_text)
1349
+ try:
1350
+ forward_search_in_browser(chrome, search_text)
1351
+ except WebDriverException as exc:
1352
+ print(f"pydifft cpb: forward search failed: {exc}",
1353
+ file=sys.stderr)
1356
1354
 
1357
1355
  saw_source_event = False
1358
1356
  while not change_queue.empty():
@@ -1450,30 +1448,16 @@ def cpb(
1450
1448
  traceback.print_exc()
1451
1449
  else:
1452
1450
  handled_signature = attempted_signature
1453
- if chrome is not None:
1454
- if not browser_window_is_alive(chrome):
1455
- break
1456
- try:
1457
- chrome.refresh()
1458
- except WebDriverException:
1459
- print(
1460
- "pydifft cpb: Browser is no longer "
1461
- "available; stopping without "
1462
- "reopening it.",
1463
- file=sys.stderr,
1464
- )
1465
- break
1466
- if chrome is None:
1467
- # {{{ reopen the preview a dialog closed
1468
- chrome = start_browser()
1469
- chrome.get(
1470
- "file://" + os.path.abspath(html_file)
1471
- )
1472
- chrome.execute_script(
1473
- "window.scrollTo(0, arguments[0]);",
1474
- scroll_position,
1475
- )
1476
- # }}}
1451
+ if not browser_window_is_alive(chrome):
1452
+ break
1453
+ try:
1454
+ chrome.refresh()
1455
+ except WebDriverException as exc:
1456
+ print(
1457
+ "pydifft cpb: preview refresh failed: "
1458
+ f"{exc}",
1459
+ file=sys.stderr,
1460
+ )
1477
1461
  # }}}
1478
1462
  time.sleep(MAIN_LOOP_INTERVAL_SECONDS)
1479
1463
  except KeyboardInterrupt:
@@ -1489,7 +1473,8 @@ def cpb(
1489
1473
  if socket_thread_started:
1490
1474
  socket_thread.join()
1491
1475
  close_browser_window(chrome)
1492
- dialog_callbacks.clear()
1476
+ if previous_sigterm is not None:
1477
+ signal.signal(signal.SIGTERM, previous_sigterm)
1493
1478
 
1494
1479
 
1495
1480
  if __name__ == "__main__":
@@ -703,7 +703,14 @@ def wrap_prose(
703
703
  """Wrap prose with sentence and punctuation rules for both commands."""
704
704
  output = []
705
705
  # Keep paragraph separators and the boundary newlines next to exclusions.
706
- for paragraph in PARAGRAPH_SPLIT.split(text):
706
+ paragraphs = PARAGRAPH_SPLIT.split(text)
707
+ if filetype == "markdown":
708
+ # Hard line breaks are source boundaries, not wrappable whitespace.
709
+ paragraphs = [
710
+ part for paragraph in paragraphs
711
+ for part in re.split(r"( {2,}\r?\n)", paragraph)
712
+ ]
713
+ for paragraph in paragraphs:
707
714
  if not paragraph.strip():
708
715
  output.append(paragraph)
709
716
  continue
@@ -1001,7 +1008,9 @@ def check_prose(
1001
1008
  if k < len(separators):
1002
1009
  sep = separators[k]
1003
1010
  offset += len(sep)
1004
- if sep == " " and (
1011
+ if sep == " " and not re.match(
1012
+ r"[ \t]*\r?\n", stripped[offset:]
1013
+ ) and (
1005
1014
  k + 1 >= len(sentences) or sentences[k + 1].strip()
1006
1015
  ):
1007
1016
  issue_line = para_line + stripped.count(
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyDiffTools"
7
- version = "0.1.57"
7
+ version = "0.1.58"
8
8
  authors = [{ name = "J M Franck" }]
9
9
  description = "Diff tools"
10
10
  readme = "README.rst"
@@ -271,3 +271,85 @@ def test_start_browser_does_not_hide_programming_errors(monkeypatch):
271
271
  monkeypatch.setattr(webdriver, "Firefox", firefox)
272
272
  with pytest.raises(TypeError, match="unexpected argument"):
273
273
  browser_lifecycle.start_browser()
274
+
275
+
276
+ def test_forward_search_matches_entities_and_rendered_crossrefs(
277
+ monkeypatch, tmp_path
278
+ ):
279
+ from selenium.webdriver.chrome.service import Service
280
+ from pydifftools import command_line
281
+
282
+ options = webdriver.ChromeOptions()
283
+ options.add_argument("--headless=new")
284
+ browser = webdriver.Chrome(
285
+ service=Service("/usr/bin/chromedriver"), options=options
286
+ )
287
+ monkeypatch.setattr(browser_lifecycle.shutil, "which", lambda _name: None)
288
+ searches = []
289
+ monkeypatch.setattr(
290
+ command_line, "send_forward_search",
291
+ lambda _address, text: searches.append(text),
292
+ )
293
+ try:
294
+ preview = tmp_path / "preview.html"
295
+ preview.write_text(
296
+ "<p>It melts near 238&nbsp;K &amp; stays solid.</p>"
297
+ '<p>See eq.&nbsp;<a href="#eq:my_equation">(1)</a> for '
298
+ "<strong>the result</strong>.</p>"
299
+ '<p><a href="#fig:overview">Figure 2</a> shows the sample.</p>'
300
+ '<p>See <a href="#sec:methods">section 3</a>.</p>'
301
+ '<h2 id="sec:unlinked">4 Unlinked section</h2>'
302
+ "<p>As described in sec.&nbsp;4, it works.</p>"
303
+ )
304
+ browser.get(preview.as_uri())
305
+ examples = [
306
+ ("It melts near 238&nbsp;K &amp; stays solid.",
307
+ "It melts near 238\xa0K & stays solid."),
308
+ ("238&#160;K", "238\xa0K"),
309
+ ("238 K", "238\xa0K"),
310
+ ("See @eq:my_equation for **the result**.",
311
+ "See eq.\xa0(1) for the result."),
312
+ ("@eq:my_equation for the result.",
313
+ "eq.\xa0(1) for the result."),
314
+ ("See [@eq:my_equation]", "See eq.\xa0(1)"),
315
+ ("@fig:overview shows the sample.",
316
+ "Figure 2 shows the sample."),
317
+ ("See @sec:methods.", "See section 3."),
318
+ ("As described in @sec:unlinked, it works.",
319
+ "As described in sec.\xa04, it works."),
320
+ ("@sec:unlinked", "4 Unlinked section"),
321
+ ]
322
+ for source, rendered in examples:
323
+ browser.execute_script("window.getSelection().removeAllRanges()")
324
+ command_line.mfs(source)
325
+ assert browser_lifecycle.forward_search_in_browser(
326
+ browser, searches[-1]
327
+ ), source
328
+ assert browser.execute_script(
329
+ "return window.getSelection().toString()"
330
+ ) == rendered
331
+ assert not browser_lifecycle.forward_search_in_browser(
332
+ browser, "See @eq:missing for the result."
333
+ )
334
+ finally:
335
+ browser.quit()
336
+
337
+
338
+ @pytest.mark.parametrize("closed", [False, True])
339
+ def test_browser_probe_distinguishes_transient_error_from_closed_session(
340
+ closed,
341
+ ):
342
+ class InterruptedBrowser:
343
+ @property
344
+ def window_handles(self):
345
+ if closed:
346
+ from selenium.common.exceptions import (
347
+ InvalidSessionIdException,
348
+ )
349
+
350
+ raise InvalidSessionIdException("window closed")
351
+ raise WebDriverException("page navigation interrupted command")
352
+
353
+ assert browser_lifecycle.browser_window_is_alive(
354
+ InterruptedBrowser()
355
+ ) is not closed