pyDiffTools 0.1.56__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.56/pyDiffTools.egg-info → pydifftools-0.1.58}/PKG-INFO +12 -1
  2. {pydifftools-0.1.56 → pydifftools-0.1.58}/README.rst +11 -0
  3. {pydifftools-0.1.56 → pydifftools-0.1.58/pyDiffTools.egg-info}/PKG-INFO +12 -1
  4. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/browser_lifecycle.py +92 -12
  5. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/command_line.py +11 -21
  6. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/continuous.py +63 -47
  7. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/wrap_sentences.py +11 -2
  8. {pydifftools-0.1.56 → pydifftools-0.1.58}/pyproject.toml +1 -1
  9. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_browser_lifecycle.py +82 -0
  10. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_continuous_shutdown.py +89 -72
  11. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_forward_search.py +1 -1
  12. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_wrap_check.py +40 -0
  13. {pydifftools-0.1.56 → pydifftools-0.1.58}/LICENSE.md +0 -0
  14. {pydifftools-0.1.56 → pydifftools-0.1.58}/MANIFEST.in +0 -0
  15. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/_quarto.yml +0 -0
  16. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  17. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  18. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  19. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  20. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  21. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  22. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  23. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  24. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  25. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  26. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  27. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  28. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  29. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  30. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  31. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  32. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  33. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  34. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  35. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  36. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  37. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  38. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/example.qmd +0 -0
  39. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/example.tex +0 -0
  40. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/index.qmd +0 -0
  41. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  42. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  43. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  44. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  45. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/subproject1/independent.qmd +0 -0
  46. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/subproject1/index.qmd +0 -0
  47. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  48. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  49. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  50. {pydifftools-0.1.56 → pydifftools-0.1.58}/example_notebook/project1/test_include.qmd +0 -0
  51. {pydifftools-0.1.56 → pydifftools-0.1.58}/pyDiffTools.egg-info/SOURCES.txt +0 -0
  52. {pydifftools-0.1.56 → pydifftools-0.1.58}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  53. {pydifftools-0.1.56 → pydifftools-0.1.58}/pyDiffTools.egg-info/entry_points.txt +0 -0
  54. {pydifftools-0.1.56 → pydifftools-0.1.58}/pyDiffTools.egg-info/requires.txt +0 -0
  55. {pydifftools-0.1.56 → pydifftools-0.1.58}/pyDiffTools.egg-info/top_level.txt +0 -0
  56. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/__init__.py +0 -0
  57. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/bibliography.py +0 -0
  58. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/check_numbers.py +0 -0
  59. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/citation_dialog.py +0 -0
  60. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/command_registry.py +0 -0
  61. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/comment_filter_history.json +0 -0
  62. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/comment_functions.py +0 -0
  63. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/comment_migration.py +0 -0
  64. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/comment_tags.lua +0 -0
  65. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/comment_tags_margin.lua +0 -0
  66. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/comment_tags_no_comments.lua +0 -0
  67. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/comment_toggle.js +0 -0
  68. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/comments.css +0 -0
  69. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/comments_author_colors.css +0 -0
  70. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/completions/git-mergein +0 -0
  71. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/copy_files.py +0 -0
  72. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/diff-doc.js +0 -0
  73. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/doc_contents.py +0 -0
  74. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/flowchart/__init__.py +0 -0
  75. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/flowchart/comparison.py +0 -0
  76. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  77. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/flowchart/graph.py +0 -0
  78. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/flowchart/source_jump.js +0 -0
  79. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  80. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/flowchart/watch_graph.py +0 -0
  81. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/flowchart/wgrph_view.js +0 -0
  82. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/forward_search.py +0 -0
  83. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/git_aliases.py +0 -0
  84. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/git_gd.py +0 -0
  85. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/git_gd_history.py +0 -0
  86. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/git_gd_image.py +0 -0
  87. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/git_gd_qt.py +0 -0
  88. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/html_comments.py +0 -0
  89. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/html_uncomments.py +0 -0
  90. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/line_alignment.py +0 -0
  91. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/log_example.py +0 -0
  92. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/match_spaces.py +0 -0
  93. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/notebook/__init__.py +0 -0
  94. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/notebook/display.py +0 -0
  95. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/notebook/fast_build.py +0 -0
  96. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/notebook/tex_to_qmd.py +0 -0
  97. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/onewordify.py +0 -0
  98. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/onewordify_undo.py +0 -0
  99. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/outline.py +0 -0
  100. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/pdf_diff.py +0 -0
  101. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/rearrange_tex.py +0 -0
  102. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/searchacro.py +0 -0
  103. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/separate_comments.py +0 -0
  104. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/source_jump.py +0 -0
  105. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/split_conflict.py +0 -0
  106. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/unseparate_comments.py +0 -0
  107. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/update_check.py +0 -0
  108. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/wrapping_options.py +0 -0
  109. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/xml2xlsx.vbs +0 -0
  110. {pydifftools-0.1.56 → pydifftools-0.1.58}/pydifftools/zotero.py +0 -0
  111. {pydifftools-0.1.56 → pydifftools-0.1.58}/setup.cfg +0 -0
  112. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_bibliography.py +0 -0
  113. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_check_single_use.py +0 -0
  114. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_command_line_help.py +0 -0
  115. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_comment_filters.py +0 -0
  116. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_comment_migration.py +0 -0
  117. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_git_gd_history.py +0 -0
  118. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_git_gd_image.py +0 -0
  119. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_git_gd_qt.py +0 -0
  120. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_git_mergein.py +0 -0
  121. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_git_pdf_diff.py +0 -0
  122. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_line_alignment.py +0 -0
  123. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_pdf_diff.py +0 -0
  124. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_qt_lifecycle.py +0 -0
  125. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_rrng.py +0 -0
  126. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_source_jump.py +0 -0
  127. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_tex_to_qmd.py +0 -0
  128. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_update_check.py +0 -0
  129. {pydifftools-0.1.56 → pydifftools-0.1.58}/tests/test_wrapping.py +0 -0
  130. {pydifftools-0.1.56 → 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.56
3
+ Version: 0.1.58
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -163,6 +163,17 @@ included are (listed in order of fun/utility):
163
163
  ``python .github/scripts/update_comment_filter_history.py``; the release
164
164
  workflow also refreshes it before building distributions.
165
165
 
166
+ To run Lua filters before pandoc-crossref, declare an ordered list in
167
+ the Markdown YAML header::
168
+
169
+ filters-before-crossref:
170
+ - filters/panel-references.lua
171
+
172
+ Paths are resolved relative to the Markdown file. Each declared filter
173
+ runs once, in listed order, and is excluded from the adjacent Lua filters
174
+ that normally run afterward. Missing files produce a build error.
175
+ Documents without this setting retain the existing filter order.
176
+
166
177
  Declare a single local ``.bib`` file with ``bibliography: references.bib``
167
178
  in the Markdown YAML header; relative paths are resolved beside the
168
179
  Markdown file. Without that declaration, ``cpb`` uses a single adjacent
@@ -108,6 +108,17 @@ included are (listed in order of fun/utility):
108
108
  ``python .github/scripts/update_comment_filter_history.py``; the release
109
109
  workflow also refreshes it before building distributions.
110
110
 
111
+ To run Lua filters before pandoc-crossref, declare an ordered list in
112
+ the Markdown YAML header::
113
+
114
+ filters-before-crossref:
115
+ - filters/panel-references.lua
116
+
117
+ Paths are resolved relative to the Markdown file. Each declared filter
118
+ runs once, in listed order, and is excluded from the adjacent Lua filters
119
+ that normally run afterward. Missing files produce a build error.
120
+ Documents without this setting retain the existing filter order.
121
+
111
122
  Declare a single local ``.bib`` file with ``bibliography: references.bib``
112
123
  in the Markdown YAML header; relative paths are resolved beside the
113
124
  Markdown file. Without that declaration, ``cpb`` uses a single adjacent
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.56
3
+ Version: 0.1.58
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -163,6 +163,17 @@ included are (listed in order of fun/utility):
163
163
  ``python .github/scripts/update_comment_filter_history.py``; the release
164
164
  workflow also refreshes it before building distributions.
165
165
 
166
+ To run Lua filters before pandoc-crossref, declare an ordered list in
167
+ the Markdown YAML header::
168
+
169
+ filters-before-crossref:
170
+ - filters/panel-references.lua
171
+
172
+ Paths are resolved relative to the Markdown file. Each declared filter
173
+ runs once, in listed order, and is excluded from the adjacent Lua filters
174
+ that normally run afterward. Missing files produce a build error.
175
+ Documents without this setting retain the existing filter order.
176
+
166
177
  Declare a single local ``.bib`` file with ``bibliography: references.bib``
167
178
  in the Markdown YAML header; relative paths are resolved beside the
168
179
  Markdown file. Without that declaration, ``cpb`` uses a single adjacent
@@ -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,
@@ -826,6 +826,7 @@ def run_pandoc(
826
826
  # }}}
827
827
  # {{{ select the declared bibliography before adjacent-file discovery
828
828
  bibliography_declared = False
829
+ metadata = None
829
830
  bibliography_paths = []
830
831
  front_matter = re.match(
831
832
  r"\A\ufeff?---[^\S\n]*\n(.*?)\n(?:---|\.\.\.)[^\S\n]*(?:\n|$)",
@@ -851,6 +852,27 @@ def run_pandoc(
851
852
  for value in declared
852
853
  ]
853
854
  # }}}
855
+ # {{{ resolve explicitly ordered filters before cross-reference processing
856
+ filters_before_crossref = []
857
+ if isinstance(metadata, dict) and "filters-before-crossref" in metadata:
858
+ declared_filters = metadata["filters-before-crossref"]
859
+ if not isinstance(declared_filters, list) or not all(
860
+ isinstance(value, str) and value for value in declared_filters
861
+ ):
862
+ raise ValueError(
863
+ "cpb: filters-before-crossref must be a list of file paths "
864
+ f"in {filename}"
865
+ )
866
+ for value in declared_filters:
867
+ filter_path = Path(source_dir, value).resolve()
868
+ if not filter_path.is_file():
869
+ raise FileNotFoundError(
870
+ "cpb: filters-before-crossref file not found: "
871
+ f"{value!r} (resolved to {filter_path})"
872
+ )
873
+ if str(filter_path) not in filters_before_crossref:
874
+ filters_before_crossref.append(str(filter_path))
875
+ # }}}
854
876
  localfiles = {}
855
877
  for k in ["csl", "bib"]:
856
878
  if k == "bib" and bibliography_declared:
@@ -886,7 +908,12 @@ def run_pandoc(
886
908
  "author-info-blocks.lua": 1,
887
909
  }
888
910
  localfiles["lua"] = sorted(
889
- [f for f in os.listdir(source_dir) if f.endswith(".lua")],
911
+ [
912
+ f for f in os.listdir(source_dir)
913
+ if f.endswith(".lua")
914
+ and str(Path(source_dir, f).resolve())
915
+ not in filters_before_crossref
916
+ ],
890
917
  key=lambda name: (lua_priority.get(name, 2), name),
891
918
  )
892
919
  # Include any javascript files next to the markdown source by injecting
@@ -908,6 +935,10 @@ def run_pandoc(
908
935
  html_file,
909
936
  filename,
910
937
  ]
938
+ before_crossref_args = []
939
+ for filter_path in filters_before_crossref:
940
+ before_crossref_args.extend(["--lua-filter", filter_path])
941
+ command[1:1] = before_crossref_args
911
942
  if not bibliography_declared and localfiles["bib"]:
912
943
  bibliography_paths = [localfiles["bib"]]
913
944
  for bibliography_path in bibliography_paths:
@@ -1200,11 +1231,16 @@ def cpb(
1200
1231
  daemon=True,
1201
1232
  )
1202
1233
  chrome = None
1203
- scroll_position = 0
1204
1234
  observer = None
1205
1235
  observer_started = False
1206
1236
  socket_thread_started = False
1207
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)
1208
1244
 
1209
1245
  try:
1210
1246
  # Bind and serve before any build or browser work. This makes the fixed
@@ -1290,29 +1326,18 @@ def cpb(
1290
1326
  # and never be reported as a change.
1291
1327
  chrome.get("file://" + os.path.abspath(html_file))
1292
1328
 
1293
- def close_for_dialog():
1294
- # a rebuild needs the user, so close the preview while the
1295
- # issues are dealt with; it reopens once the rebuild is done
1296
- nonlocal chrome, scroll_position
1297
- if chrome is not None:
1298
- # sessionStorage does not survive quitting the browser.
1299
- scroll_position = chrome.execute_script(
1300
- "return window.scrollY;"
1301
- )
1302
- close_browser_window(chrome)
1303
- chrome = None
1304
-
1305
- dialog_callbacks.append(close_for_dialog)
1306
1329
  rebuild_pending = False
1307
1330
  stable_signature = None
1308
1331
  stable_since = None
1332
+ listener_warning_shown = False
1309
1333
  while True:
1310
- if not socket_thread.is_alive():
1311
- raise RuntimeError(
1312
- "The cpb forward-search listener stopped unexpectedly; "
1313
- "closing the preview instead of leaving an "
1314
- "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,
1315
1339
  )
1340
+ listener_warning_shown = True
1316
1341
  # Browser closure is terminal. Source-file and build failures do
1317
1342
  # not affect browser ownership or observer lifetime.
1318
1343
  if not browser_window_is_alive(chrome):
@@ -1321,7 +1346,11 @@ def cpb(
1321
1346
  for queued_search in drain_forward_search_queue(search_queue):
1322
1347
  search_text = queued_search.strip()
1323
1348
  if search_text:
1324
- 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)
1325
1354
 
1326
1355
  saw_source_event = False
1327
1356
  while not change_queue.empty():
@@ -1419,30 +1448,16 @@ def cpb(
1419
1448
  traceback.print_exc()
1420
1449
  else:
1421
1450
  handled_signature = attempted_signature
1422
- if chrome is not None:
1423
- if not browser_window_is_alive(chrome):
1424
- break
1425
- try:
1426
- chrome.refresh()
1427
- except WebDriverException:
1428
- print(
1429
- "pydifft cpb: Browser is no longer "
1430
- "available; stopping without "
1431
- "reopening it.",
1432
- file=sys.stderr,
1433
- )
1434
- break
1435
- if chrome is None:
1436
- # {{{ reopen the preview a dialog closed
1437
- chrome = start_browser()
1438
- chrome.get(
1439
- "file://" + os.path.abspath(html_file)
1440
- )
1441
- chrome.execute_script(
1442
- "window.scrollTo(0, arguments[0]);",
1443
- scroll_position,
1444
- )
1445
- # }}}
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
+ )
1446
1461
  # }}}
1447
1462
  time.sleep(MAIN_LOOP_INTERVAL_SECONDS)
1448
1463
  except KeyboardInterrupt:
@@ -1458,7 +1473,8 @@ def cpb(
1458
1473
  if socket_thread_started:
1459
1474
  socket_thread.join()
1460
1475
  close_browser_window(chrome)
1461
- dialog_callbacks.clear()
1476
+ if previous_sigterm is not None:
1477
+ signal.signal(signal.SIGTERM, previous_sigterm)
1462
1478
 
1463
1479
 
1464
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.56"
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