pyDiffTools 0.1.48__tar.gz → 0.1.49__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pydifftools-0.1.48/pyDiffTools.egg-info → pydifftools-0.1.49}/PKG-INFO +1 -1
- {pydifftools-0.1.48 → pydifftools-0.1.49/pyDiffTools.egg-info}/PKG-INFO +1 -1
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pyDiffTools.egg-info/SOURCES.txt +3 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/browser_lifecycle.py +31 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/comment_toggle.js +1 -1
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/comments.css +5 -4
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/continuous.py +37 -5
- pydifftools-0.1.49/pydifftools/flowchart/source_jump.js +158 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/flowchart/watch_graph.py +53 -3
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/flowchart/wgrph_view.js +23 -0
- pydifftools-0.1.49/pydifftools/source_jump.py +128 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pyproject.toml +2 -1
- pydifftools-0.1.49/tests/test_browser_lifecycle.py +115 -0
- pydifftools-0.1.49/tests/test_source_jump.py +85 -0
- pydifftools-0.1.48/tests/test_browser_lifecycle.py +0 -30
- {pydifftools-0.1.48 → pydifftools-0.1.49}/LICENSE.md +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/MANIFEST.in +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/README.rst +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/_quarto.yml +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/example.qmd +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/example.tex +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/index.qmd +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/subproject1/independent.qmd +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/subproject1/index.qmd +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/subproject1/tasks.qmd +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/subproject1/test_include.qmd +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/example_notebook/project1/test_include.qmd +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pyDiffTools.egg-info/dependency_links.txt +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pyDiffTools.egg-info/entry_points.txt +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pyDiffTools.egg-info/requires.txt +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pyDiffTools.egg-info/top_level.txt +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/__init__.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/bibliography.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/check_numbers.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/citation_dialog.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/command_line.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/command_registry.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/comment_functions.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/comment_migration.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/comment_tags.lua +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/comment_tags_margin.lua +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/comment_tags_no_comments.lua +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/comments_author_colors.css +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/copy_files.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/diff-doc.js +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/doc_contents.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/flowchart/__init__.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/flowchart/comparison.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/flowchart/dot_to_yaml.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/flowchart/graph.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/forward_search.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/git_gd.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/git_gd_history.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/git_gd_image.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/git_gd_qt.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/html_comments.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/html_uncomments.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/log_example.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/match_spaces.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/notebook/__init__.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/notebook/display.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/notebook/fast_build.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/notebook/tex_to_qmd.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/onewordify.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/onewordify_undo.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/outline.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/rearrange_tex.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/searchacro.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/separate_comments.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/split_conflict.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/unseparate_comments.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/update_check.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/wrap_sentences.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/xml2xlsx.vbs +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/pydifftools/zotero.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/setup.cfg +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/tests/test_bibliography.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/tests/test_check_single_use.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/tests/test_command_line_help.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/tests/test_comment_migration.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/tests/test_continuous_shutdown.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/tests/test_forward_search.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/tests/test_git_gd_history.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/tests/test_git_gd_image.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/tests/test_git_gd_qt.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/tests/test_rrng.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/tests/test_tex_to_qmd.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/tests/test_update_check.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/tests/test_wrap_check.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/tests/test_wrapping.py +0 -0
- {pydifftools-0.1.48 → pydifftools-0.1.49}/tests/test_zotero.py +0 -0
|
@@ -78,6 +78,7 @@ pydifftools/outline.py
|
|
|
78
78
|
pydifftools/rearrange_tex.py
|
|
79
79
|
pydifftools/searchacro.py
|
|
80
80
|
pydifftools/separate_comments.py
|
|
81
|
+
pydifftools/source_jump.py
|
|
81
82
|
pydifftools/split_conflict.py
|
|
82
83
|
pydifftools/unseparate_comments.py
|
|
83
84
|
pydifftools/update_check.py
|
|
@@ -88,6 +89,7 @@ pydifftools/flowchart/__init__.py
|
|
|
88
89
|
pydifftools/flowchart/comparison.py
|
|
89
90
|
pydifftools/flowchart/dot_to_yaml.py
|
|
90
91
|
pydifftools/flowchart/graph.py
|
|
92
|
+
pydifftools/flowchart/source_jump.js
|
|
91
93
|
pydifftools/flowchart/svg-pan-zoom.min.js
|
|
92
94
|
pydifftools/flowchart/watch_graph.py
|
|
93
95
|
pydifftools/flowchart/wgrph_view.js
|
|
@@ -106,6 +108,7 @@ tests/test_git_gd_history.py
|
|
|
106
108
|
tests/test_git_gd_image.py
|
|
107
109
|
tests/test_git_gd_qt.py
|
|
108
110
|
tests/test_rrng.py
|
|
111
|
+
tests/test_source_jump.py
|
|
109
112
|
tests/test_tex_to_qmd.py
|
|
110
113
|
tests/test_update_check.py
|
|
111
114
|
tests/test_wrap_check.py
|
|
@@ -1,12 +1,43 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import re
|
|
2
3
|
import shutil
|
|
3
4
|
import subprocess
|
|
5
|
+
import sys
|
|
4
6
|
|
|
5
7
|
# callbacks that run before any dialog needs the user; cpb registers one
|
|
6
8
|
# that closes its live preview, so issues are dealt with before it reopens
|
|
7
9
|
dialog_callbacks = []
|
|
8
10
|
|
|
9
11
|
|
|
12
|
+
def launch_chrome(webdriver, **kwargs):
|
|
13
|
+
"""Start Chrome, retrying version mismatches with a warning."""
|
|
14
|
+
from selenium.common.exceptions import SessionNotCreatedException
|
|
15
|
+
from selenium.webdriver.chrome.service import Service
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
return webdriver.Chrome(**kwargs)
|
|
19
|
+
except SessionNotCreatedException as exc:
|
|
20
|
+
# {{{ Retry only ChromeDriver's explicit browser-version rejection
|
|
21
|
+
details = str(exc)
|
|
22
|
+
driver_match = re.search(
|
|
23
|
+
r"ChromeDriver only supports Chrome version (\d+)", details
|
|
24
|
+
)
|
|
25
|
+
browser_match = re.search(
|
|
26
|
+
r"Current browser version is (\d+)", details
|
|
27
|
+
)
|
|
28
|
+
if driver_match is None or browser_match is None:
|
|
29
|
+
raise
|
|
30
|
+
print(
|
|
31
|
+
"pydifft: warning: ChromeDriver "
|
|
32
|
+
f"{driver_match.group(1)} and Chrome {browser_match.group(1)} "
|
|
33
|
+
"do not match; retrying with the driver version check disabled.",
|
|
34
|
+
file=sys.stderr,
|
|
35
|
+
)
|
|
36
|
+
kwargs["service"] = Service(service_args=["--disable-build-check"])
|
|
37
|
+
return webdriver.Chrome(**kwargs)
|
|
38
|
+
# }}}
|
|
39
|
+
|
|
40
|
+
|
|
10
41
|
def prepare_for_dialog():
|
|
11
42
|
"""Let watch commands close their preview before a dialog appears."""
|
|
12
43
|
for callback in list(dialog_callbacks):
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function positionComments() {
|
|
37
|
-
const useMobileFlow = window.matchMedia("(max-width:
|
|
37
|
+
const useMobileFlow = window.matchMedia("(max-width: 600px)").matches;
|
|
38
38
|
// Inline comment bubbles are absolutely positioned relative to a zero-width
|
|
39
39
|
// pin. We nudge overlapping bubbles so adjacent <comment> tags visibly
|
|
40
40
|
// separate, and we raise the bubbles so the pointer aims at the source point.
|
|
@@ -4,9 +4,10 @@ html {
|
|
|
4
4
|
}
|
|
5
5
|
body {
|
|
6
6
|
margin: 0 auto;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
padding-
|
|
7
|
+
/* Keep 75% of the previous side margins and give the rest to the text. */
|
|
8
|
+
max-width: calc(27em + 25%);
|
|
9
|
+
padding-left: 37.5px;
|
|
10
|
+
padding-right: 37.5px;
|
|
10
11
|
padding-top: 50px;
|
|
11
12
|
padding-bottom: 50px;
|
|
12
13
|
hyphens: auto;
|
|
@@ -311,7 +312,7 @@ span.comment-pin > span.comment-left::after {
|
|
|
311
312
|
}
|
|
312
313
|
|
|
313
314
|
/* ---------- Mobile fallback ---------- */
|
|
314
|
-
@media (max-width:
|
|
315
|
+
@media (max-width: 600px) {
|
|
315
316
|
div.comment-right,
|
|
316
317
|
div.comment-left,
|
|
317
318
|
span.comment-pin > span.comment-right,
|
|
@@ -25,6 +25,7 @@ from .browser_lifecycle import (
|
|
|
25
25
|
close_browser_window,
|
|
26
26
|
dialog_callbacks,
|
|
27
27
|
forward_search_in_browser,
|
|
28
|
+
launch_chrome,
|
|
28
29
|
prepare_for_dialog,
|
|
29
30
|
)
|
|
30
31
|
from .forward_search import (
|
|
@@ -34,6 +35,7 @@ from .forward_search import (
|
|
|
34
35
|
drain_forward_search_queue,
|
|
35
36
|
serve_forward_search,
|
|
36
37
|
)
|
|
38
|
+
from .source_jump import SourceJumpServer
|
|
37
39
|
|
|
38
40
|
FORWARD_SEARCH_PORT = CPB_FORWARD_SEARCH_PORT
|
|
39
41
|
POLL_INTERVAL_SECONDS = 0.1
|
|
@@ -802,7 +804,7 @@ def run_pandoc(
|
|
|
802
804
|
return
|
|
803
805
|
|
|
804
806
|
|
|
805
|
-
def append_autorefresh(html_file):
|
|
807
|
+
def append_autorefresh(html_file, source_jump_url=None):
|
|
806
808
|
with open(html_file, "r", encoding="utf-8") as fp:
|
|
807
809
|
all_data = fp.read()
|
|
808
810
|
all_data = all_data.replace(
|
|
@@ -867,6 +869,29 @@ position
|
|
|
867
869
|
</head>
|
|
868
870
|
""",
|
|
869
871
|
)
|
|
872
|
+
if source_jump_url:
|
|
873
|
+
all_data = re.sub(
|
|
874
|
+
r'\s*<script id="pydifft-source-jump-config">.*?</script>'
|
|
875
|
+
r'\s*<script id="pydifft-source-jump">.*?</script>',
|
|
876
|
+
"",
|
|
877
|
+
all_data,
|
|
878
|
+
flags=re.DOTALL,
|
|
879
|
+
)
|
|
880
|
+
source_jump_js = (
|
|
881
|
+
Path(__file__).parent / "flowchart" / "source_jump.js"
|
|
882
|
+
).read_text(encoding="utf-8")
|
|
883
|
+
source_jump_scripts = (
|
|
884
|
+
'<script id="pydifft-source-jump-config">'
|
|
885
|
+
"window.pydifftSourceJumpEndpoint = "
|
|
886
|
+
+ json.dumps(source_jump_url)
|
|
887
|
+
+ ";</script>"
|
|
888
|
+
'<script id="pydifft-source-jump">'
|
|
889
|
+
+ source_jump_js
|
|
890
|
+
+ "</script>"
|
|
891
|
+
)
|
|
892
|
+
all_data = all_data.replace(
|
|
893
|
+
"</head>", source_jump_scripts + "</head>", 1
|
|
894
|
+
)
|
|
870
895
|
with open(html_file, "w", encoding="utf-8") as fp:
|
|
871
896
|
fp.write(all_data)
|
|
872
897
|
|
|
@@ -939,6 +964,7 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
|
|
|
939
964
|
observer = None
|
|
940
965
|
observer_started = False
|
|
941
966
|
socket_thread_started = False
|
|
967
|
+
source_jump_server = None
|
|
942
968
|
|
|
943
969
|
try:
|
|
944
970
|
# Bind and serve before any build or browser work. This makes the fixed
|
|
@@ -968,14 +994,16 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
|
|
|
968
994
|
comment_filter_session=comment_filter_session,
|
|
969
995
|
wrapnumber=wrapnumber,
|
|
970
996
|
)
|
|
971
|
-
|
|
997
|
+
source_jump_server = SourceJumpServer(source_path)
|
|
998
|
+
source_jump_server.start()
|
|
999
|
+
append_autorefresh(html_file, source_jump_server.url)
|
|
972
1000
|
|
|
973
1001
|
# Selenium is deliberately initialized once, after the first
|
|
974
1002
|
# successful build. Nothing in recovery constructs a browser.
|
|
975
1003
|
from selenium import webdriver
|
|
976
1004
|
from selenium.common.exceptions import WebDriverException
|
|
977
1005
|
|
|
978
|
-
chrome = webdriver
|
|
1006
|
+
chrome = launch_chrome(webdriver)
|
|
979
1007
|
observer = Observer()
|
|
980
1008
|
change_queue = queue.Queue()
|
|
981
1009
|
event_handler = Handler(filename, change_queue)
|
|
@@ -1113,7 +1141,9 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
|
|
|
1113
1141
|
comment_filter_session=comment_filter_session,
|
|
1114
1142
|
wrapnumber=wrapnumber,
|
|
1115
1143
|
)
|
|
1116
|
-
append_autorefresh(
|
|
1144
|
+
append_autorefresh(
|
|
1145
|
+
html_file, source_jump_server.url
|
|
1146
|
+
)
|
|
1117
1147
|
except Exception as exc:
|
|
1118
1148
|
exception_filename = getattr(exc, "filename", None)
|
|
1119
1149
|
missing_source = isinstance(
|
|
@@ -1155,7 +1185,7 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
|
|
|
1155
1185
|
break
|
|
1156
1186
|
if chrome is None:
|
|
1157
1187
|
# {{{ reopen the preview a dialog closed
|
|
1158
|
-
chrome = webdriver
|
|
1188
|
+
chrome = launch_chrome(webdriver)
|
|
1159
1189
|
chrome.get(
|
|
1160
1190
|
"file://" + os.path.abspath(html_file)
|
|
1161
1191
|
)
|
|
@@ -1167,6 +1197,8 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
|
|
|
1167
1197
|
finally:
|
|
1168
1198
|
stop_event.set()
|
|
1169
1199
|
forward_search_server.close()
|
|
1200
|
+
if source_jump_server is not None:
|
|
1201
|
+
source_jump_server.stop()
|
|
1170
1202
|
if observer_started:
|
|
1171
1203
|
observer.stop()
|
|
1172
1204
|
observer.join()
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Add a small local-only source jump action to cpb and flowchart previews.
|
|
2
|
+
(function () {
|
|
3
|
+
const endpoint = window.pydifftSourceJumpEndpoint;
|
|
4
|
+
if (!endpoint) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
const excludedSourceElements =
|
|
8
|
+
'.math,.MathJax,mjx-container,.katex,math,' +
|
|
9
|
+
'script[type^="math/tex"],' +
|
|
10
|
+
'a[href*="#fig:"],a[href*="#fig-"],a[href*="#fig%3A"]';
|
|
11
|
+
|
|
12
|
+
const menu = document.createElement('div');
|
|
13
|
+
menu.id = 'pydifft-source-jump-menu';
|
|
14
|
+
menu.style.cssText =
|
|
15
|
+
'display:none;position:fixed;z-index:2147483647;background:#fff;' +
|
|
16
|
+
'border:1px solid #777;border-radius:3px;padding:3px;' +
|
|
17
|
+
'box-shadow:0 2px 6px #555;font:14px sans-serif;';
|
|
18
|
+
const button = document.createElement('button');
|
|
19
|
+
button.type = 'button';
|
|
20
|
+
button.textContent = 'Jump to source';
|
|
21
|
+
button.style.cssText =
|
|
22
|
+
'border:0;background:transparent;padding:6px 10px;cursor:pointer;' +
|
|
23
|
+
'font:inherit;';
|
|
24
|
+
menu.appendChild(button);
|
|
25
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
26
|
+
document.body.appendChild(menu);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
let phrase = '';
|
|
30
|
+
|
|
31
|
+
function hideMenu() {
|
|
32
|
+
menu.style.display = 'none';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function showMenu(event, text) {
|
|
36
|
+
phrase = (text || '').replace(/\s+/g, ' ').trim();
|
|
37
|
+
if (!phrase) {
|
|
38
|
+
hideMenu();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (menu.parentNode !== document.body && document.body) {
|
|
42
|
+
document.body.appendChild(menu);
|
|
43
|
+
}
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
menu.style.left = Math.min(event.clientX, window.innerWidth - 170) + 'px';
|
|
46
|
+
menu.style.top = Math.min(event.clientY, window.innerHeight - 42) + 'px';
|
|
47
|
+
menu.style.display = 'block';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sentenceAtPoint(event) {
|
|
51
|
+
let range = null;
|
|
52
|
+
if (document.caretRangeFromPoint) {
|
|
53
|
+
range = document.caretRangeFromPoint(event.clientX, event.clientY);
|
|
54
|
+
} else if (document.caretPositionFromPoint) {
|
|
55
|
+
const position = document.caretPositionFromPoint(
|
|
56
|
+
event.clientX,
|
|
57
|
+
event.clientY
|
|
58
|
+
);
|
|
59
|
+
if (position) {
|
|
60
|
+
range = document.createRange();
|
|
61
|
+
range.setStart(position.offsetNode, position.offset);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const target = event.target.nodeType === Node.TEXT_NODE
|
|
65
|
+
? event.target.parentElement
|
|
66
|
+
: event.target;
|
|
67
|
+
const targetElement = target && target.closest ? target : document.body;
|
|
68
|
+
if (targetElement.closest(excludedSourceElements)) {
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
const block = targetElement.closest(
|
|
72
|
+
'p,li,blockquote,h1,h2,h3,h4,h5,h6,dt,dd,td,th'
|
|
73
|
+
) || targetElement;
|
|
74
|
+
// Drop rendered math and cross-reference links so the phrase resembles
|
|
75
|
+
// the searchable Markdown source, where mfs removes those constructs.
|
|
76
|
+
let text = '';
|
|
77
|
+
let clickedOffset = 0;
|
|
78
|
+
let found = false;
|
|
79
|
+
function collectText(node) {
|
|
80
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
81
|
+
if (range && node === range.startContainer) {
|
|
82
|
+
clickedOffset = text.length + range.startOffset;
|
|
83
|
+
found = true;
|
|
84
|
+
}
|
|
85
|
+
text += node.textContent;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (
|
|
89
|
+
node.nodeType === Node.ELEMENT_NODE &&
|
|
90
|
+
node !== block &&
|
|
91
|
+
node.matches(excludedSourceElements)
|
|
92
|
+
) {
|
|
93
|
+
text += ' ';
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
node.childNodes.forEach(collectText);
|
|
97
|
+
}
|
|
98
|
+
collectText(block);
|
|
99
|
+
const rawText = text;
|
|
100
|
+
text = rawText.replace(/\s+/g, ' ').trim();
|
|
101
|
+
if (!text) {
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
if (!range || !found) {
|
|
105
|
+
return text;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The raw DOM offset is used to find sentence bounds before whitespace is
|
|
109
|
+
// collapsed. Recompute its visible offset after normalization.
|
|
110
|
+
const normalizedPrefix = rawText
|
|
111
|
+
.slice(0, clickedOffset)
|
|
112
|
+
.replace(/\s+/g, ' ')
|
|
113
|
+
.replace(/^\s+/, '');
|
|
114
|
+
clickedOffset = normalizedPrefix.length;
|
|
115
|
+
const before = text.slice(0, clickedOffset);
|
|
116
|
+
const after = text.slice(clickedOffset);
|
|
117
|
+
const start = Math.max(
|
|
118
|
+
before.lastIndexOf('.'),
|
|
119
|
+
before.lastIndexOf('!'),
|
|
120
|
+
before.lastIndexOf('?')
|
|
121
|
+
) + 1;
|
|
122
|
+
const endMatch = after.match(/[.!?](?:\s|$)/);
|
|
123
|
+
const end = endMatch ? clickedOffset + endMatch.index + 1 : text.length;
|
|
124
|
+
return text.slice(start, end).trim() || text;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
button.addEventListener('click', function () {
|
|
128
|
+
hideMenu();
|
|
129
|
+
fetch(endpoint, {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
mode: 'no-cors',
|
|
132
|
+
body: phrase,
|
|
133
|
+
}).catch(function () {
|
|
134
|
+
// The preview may be closing while the local editor request is sent.
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
document.addEventListener('click', hideMenu);
|
|
138
|
+
document.addEventListener('keydown', function (event) {
|
|
139
|
+
if (event.key === 'Escape') {
|
|
140
|
+
hideMenu();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
document.addEventListener('contextmenu', function (event) {
|
|
144
|
+
showMenu(event, sentenceAtPoint(event));
|
|
145
|
+
});
|
|
146
|
+
document.addEventListener('pydifft-source-context', function (event) {
|
|
147
|
+
showMenu(
|
|
148
|
+
{
|
|
149
|
+
clientX: event.detail.clientX,
|
|
150
|
+
clientY: event.detail.clientY,
|
|
151
|
+
preventDefault: function () {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
event.detail.phrase
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
})();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import subprocess
|
|
2
2
|
import html
|
|
3
|
+
import json
|
|
3
4
|
import sys
|
|
4
5
|
import time
|
|
5
6
|
import shutil
|
|
@@ -26,7 +27,9 @@ from pydifftools.command_registry import register_command
|
|
|
26
27
|
from pydifftools.browser_lifecycle import (
|
|
27
28
|
browser_window_is_alive,
|
|
28
29
|
close_browser_window,
|
|
30
|
+
launch_chrome,
|
|
29
31
|
)
|
|
32
|
+
from pydifftools.source_jump import SourceJumpServer
|
|
30
33
|
from .comparison import PlanComparison
|
|
31
34
|
from .graph import EmptyGraphYamlError, endpoint_projects, write_dot_from_yaml
|
|
32
35
|
|
|
@@ -58,7 +61,7 @@ def _reload_svg(driver, svg_src) -> None:
|
|
|
58
61
|
|
|
59
62
|
def start_chrome(webdriver, options, preview_url):
|
|
60
63
|
# Launch Chrome and display the local SVG preview page from the server.
|
|
61
|
-
driver = webdriver
|
|
64
|
+
driver = launch_chrome(webdriver, options=options)
|
|
62
65
|
driver.get(preview_url)
|
|
63
66
|
return driver
|
|
64
67
|
|
|
@@ -117,6 +120,8 @@ def _svg_set_stroke(shape, color, stroke_width=None):
|
|
|
117
120
|
)
|
|
118
121
|
|
|
119
122
|
|
|
123
|
+
# also used by: tests/flowchart/test_watch_reload.py, which verifies
|
|
124
|
+
# query modes
|
|
120
125
|
def _watch_view_state_from_params(params):
|
|
121
126
|
# Treat each GET query as the full requested mode so navigation links can
|
|
122
127
|
# reliably switch back to the overview without depending on prior state.
|
|
@@ -138,12 +143,15 @@ def _watch_view_state_from_params(params):
|
|
|
138
143
|
return order_by_date, target_task, filter_completed
|
|
139
144
|
|
|
140
145
|
|
|
146
|
+
# also used by: tests/flowchart/test_watch_reload.py, which verifies
|
|
147
|
+
# preview HTML
|
|
141
148
|
def _watch_html(
|
|
142
149
|
svg_url,
|
|
143
150
|
order_by_date,
|
|
144
151
|
target_task=None,
|
|
145
152
|
filter_completed=False,
|
|
146
153
|
comparison=None,
|
|
154
|
+
source_jump_url=None,
|
|
147
155
|
):
|
|
148
156
|
# {{{ Build links for the other preview views
|
|
149
157
|
links = []
|
|
@@ -213,7 +221,14 @@ def _watch_html(
|
|
|
213
221
|
"</p>"
|
|
214
222
|
"<script src='/svg-pan-zoom.min.js'></script>"
|
|
215
223
|
"<script src='/wgrph_view.js'></script>"
|
|
216
|
-
|
|
224
|
+
+ (
|
|
225
|
+
"<script>window.pydifftSourceJumpEndpoint = "
|
|
226
|
+
+ json.dumps(source_jump_url)
|
|
227
|
+
+ ";</script><script src='/source_jump.js'></script>"
|
|
228
|
+
if source_jump_url
|
|
229
|
+
else ""
|
|
230
|
+
)
|
|
231
|
+
+ "</body></html>"
|
|
217
232
|
)
|
|
218
233
|
|
|
219
234
|
|
|
@@ -322,6 +337,17 @@ def build_graph(
|
|
|
322
337
|
ET.register_namespace("xlink", xlink_ns)
|
|
323
338
|
link_marker = "__WGRPH_TASK_LINK__:"
|
|
324
339
|
for group in svg_root.iter(f"{namespace}g"):
|
|
340
|
+
if group.attrib.get("class") == "node":
|
|
341
|
+
title = next(
|
|
342
|
+
(
|
|
343
|
+
child.text.strip()
|
|
344
|
+
for child in group
|
|
345
|
+
if child.tag == f"{namespace}title" and child.text
|
|
346
|
+
),
|
|
347
|
+
None,
|
|
348
|
+
)
|
|
349
|
+
if title:
|
|
350
|
+
group.set("data-source-name", title)
|
|
325
351
|
if "class" not in group.attrib or group.attrib["class"] != "node":
|
|
326
352
|
continue
|
|
327
353
|
for index, child in enumerate(list(group)):
|
|
@@ -712,6 +738,7 @@ class FlowchartPreviewServer:
|
|
|
712
738
|
self.host = host
|
|
713
739
|
self.httpd = None
|
|
714
740
|
self.server_thread = None
|
|
741
|
+
self.source_jump_server = None
|
|
715
742
|
self.base_url = None
|
|
716
743
|
self.svg_url = None
|
|
717
744
|
|
|
@@ -736,6 +763,13 @@ class FlowchartPreviewServer:
|
|
|
736
763
|
"text/javascript; charset=utf-8",
|
|
737
764
|
)
|
|
738
765
|
return
|
|
766
|
+
if parsed.path == "/source_jump.js":
|
|
767
|
+
_send_preview_response(
|
|
768
|
+
self,
|
|
769
|
+
(Path(__file__).parent / parsed.path[1:]).read_bytes(),
|
|
770
|
+
"text/javascript; charset=utf-8",
|
|
771
|
+
)
|
|
772
|
+
return
|
|
739
773
|
|
|
740
774
|
if parsed.path != "/" and parsed.path != "/index.html":
|
|
741
775
|
self.send_error(404)
|
|
@@ -786,6 +820,7 @@ class FlowchartPreviewServer:
|
|
|
786
820
|
event_handler.state["target_task"],
|
|
787
821
|
event_handler.state["filter_completed"],
|
|
788
822
|
event_handler.comparison,
|
|
823
|
+
self_outer.source_jump_server.url,
|
|
789
824
|
)
|
|
790
825
|
body_bytes = body.encode("utf-8")
|
|
791
826
|
_send_preview_response(
|
|
@@ -797,7 +832,17 @@ class FlowchartPreviewServer:
|
|
|
797
832
|
def log_message(self, format, *args):
|
|
798
833
|
return
|
|
799
834
|
|
|
800
|
-
self.
|
|
835
|
+
self.source_jump_server = SourceJumpServer(event_handler.yaml_file)
|
|
836
|
+
self.source_jump_server.start()
|
|
837
|
+
self_outer = self
|
|
838
|
+
try:
|
|
839
|
+
self.httpd = http.server.ThreadingHTTPServer(
|
|
840
|
+
(self.host, 0), Handler
|
|
841
|
+
)
|
|
842
|
+
except Exception:
|
|
843
|
+
self.source_jump_server.stop()
|
|
844
|
+
self.source_jump_server = None
|
|
845
|
+
raise
|
|
801
846
|
self.httpd.daemon_threads = True
|
|
802
847
|
port = self.httpd.server_address[1]
|
|
803
848
|
self.base_url = f"http://{self.host}:{port}/"
|
|
@@ -825,8 +870,13 @@ class FlowchartPreviewServer:
|
|
|
825
870
|
self.httpd.server_close()
|
|
826
871
|
if self.server_thread is not None:
|
|
827
872
|
self.server_thread.join(timeout=1.0)
|
|
873
|
+
if self.source_jump_server is not None:
|
|
874
|
+
self.source_jump_server.stop()
|
|
875
|
+
self.source_jump_server = None
|
|
828
876
|
|
|
829
877
|
|
|
878
|
+
# also used by: pydifftools.command_registry, which dispatches registered
|
|
879
|
+
# commands
|
|
830
880
|
@register_command(
|
|
831
881
|
"Watch a flowchart YAML file, rebuild DOT/SVG output, and open the"
|
|
832
882
|
" preview",
|
|
@@ -120,6 +120,29 @@
|
|
|
120
120
|
},
|
|
121
121
|
true
|
|
122
122
|
);
|
|
123
|
+
svgDoc.addEventListener(
|
|
124
|
+
'contextmenu',
|
|
125
|
+
function (evt) {
|
|
126
|
+
const node = evt.target.closest
|
|
127
|
+
? evt.target.closest('g.node[data-source-name]')
|
|
128
|
+
: null;
|
|
129
|
+
if (!node) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
evt.preventDefault();
|
|
133
|
+
const bounds = embed.getBoundingClientRect();
|
|
134
|
+
window.parent.document.dispatchEvent(
|
|
135
|
+
new window.parent.CustomEvent('pydifft-source-context', {
|
|
136
|
+
detail: {
|
|
137
|
+
phrase: node.getAttribute('data-source-name'),
|
|
138
|
+
clientX: bounds.left + evt.clientX,
|
|
139
|
+
clientY: bounds.top + evt.clientY,
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
},
|
|
144
|
+
true
|
|
145
|
+
);
|
|
123
146
|
// }}}
|
|
124
147
|
|
|
125
148
|
// {{{ Zoom-to-rectangle mode
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Local browser-to-editor requests for source jumps."""
|
|
2
|
+
|
|
3
|
+
import http.server
|
|
4
|
+
import re
|
|
5
|
+
import secrets
|
|
6
|
+
import subprocess
|
|
7
|
+
import threading
|
|
8
|
+
import urllib.parse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
MAX_SOURCE_JUMP_BYTES = 8192
|
|
12
|
+
SOURCE_JUMP_WORDS = re.compile(r"[\w]+", re.UNICODE)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _run_gvim_shell_command(*arguments):
|
|
16
|
+
"""Run the user's interactive Bash ``gvim`` alias with safe arguments."""
|
|
17
|
+
|
|
18
|
+
subprocess.run(
|
|
19
|
+
["bash", "-ic", 'gvim "$@"', "pydifft", *arguments],
|
|
20
|
+
check=True,
|
|
21
|
+
stdout=subprocess.DEVNULL,
|
|
22
|
+
stderr=subprocess.DEVNULL,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# also used by: SourceJumpServer's HTTP request handler and
|
|
27
|
+
# tests/test_source_jump.py
|
|
28
|
+
def jump_to_source(source_path, phrase):
|
|
29
|
+
"""Open *source_path* through the user's gvim alias and run ``SW!``."""
|
|
30
|
+
|
|
31
|
+
words = [word.lower() for word in SOURCE_JUMP_WORDS.findall(phrase)]
|
|
32
|
+
if not words:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
"The source-search phrase contains no searchable words"
|
|
35
|
+
)
|
|
36
|
+
if len(words) > 120:
|
|
37
|
+
words = words[:120]
|
|
38
|
+
|
|
39
|
+
_run_gvim_shell_command(str(source_path))
|
|
40
|
+
remote_keys = "<C-\\><C-N>:SW! " + " ".join(words) + "<CR>"
|
|
41
|
+
_run_gvim_shell_command(
|
|
42
|
+
"--servername", "GVIM", "--remote-send", remote_keys
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SourceJumpServer:
|
|
47
|
+
"""An ephemeral loopback HTTP endpoint scoped to one preview session."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, source_path):
|
|
50
|
+
self.source_path = str(source_path)
|
|
51
|
+
self.token = secrets.token_urlsafe(24)
|
|
52
|
+
self.httpd = None
|
|
53
|
+
self.thread = None
|
|
54
|
+
self.url = None
|
|
55
|
+
|
|
56
|
+
def start(self):
|
|
57
|
+
source_path = self.source_path
|
|
58
|
+
token = self.token
|
|
59
|
+
|
|
60
|
+
class Handler(http.server.BaseHTTPRequestHandler):
|
|
61
|
+
def do_POST(self):
|
|
62
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
63
|
+
query = urllib.parse.parse_qs(parsed.query)
|
|
64
|
+
if (
|
|
65
|
+
parsed.path != "/jump"
|
|
66
|
+
or query.get("token", [None])[-1] != token
|
|
67
|
+
):
|
|
68
|
+
self.send_error(404)
|
|
69
|
+
return
|
|
70
|
+
try:
|
|
71
|
+
content_length = int(
|
|
72
|
+
self.headers.get("Content-Length", "0")
|
|
73
|
+
)
|
|
74
|
+
except ValueError:
|
|
75
|
+
self.send_error(400)
|
|
76
|
+
return
|
|
77
|
+
if (
|
|
78
|
+
content_length <= 0
|
|
79
|
+
or content_length > MAX_SOURCE_JUMP_BYTES
|
|
80
|
+
):
|
|
81
|
+
self.send_error(413)
|
|
82
|
+
return
|
|
83
|
+
try:
|
|
84
|
+
phrase = self.rfile.read(content_length).decode("utf-8")
|
|
85
|
+
except UnicodeDecodeError:
|
|
86
|
+
self.send_error(400)
|
|
87
|
+
return
|
|
88
|
+
try:
|
|
89
|
+
jump_to_source(source_path, phrase)
|
|
90
|
+
except (
|
|
91
|
+
OSError,
|
|
92
|
+
subprocess.SubprocessError,
|
|
93
|
+
ValueError,
|
|
94
|
+
) as exc:
|
|
95
|
+
print(f"pydifft source jump failed: {exc}", flush=True)
|
|
96
|
+
self.send_error(500)
|
|
97
|
+
return
|
|
98
|
+
self.send_response(204)
|
|
99
|
+
self.send_header("Cache-Control", "no-store")
|
|
100
|
+
self.end_headers()
|
|
101
|
+
|
|
102
|
+
def log_message(self, format, *args):
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
self.httpd = http.server.ThreadingHTTPServer(
|
|
106
|
+
("127.0.0.1", 0), Handler
|
|
107
|
+
)
|
|
108
|
+
self.httpd.daemon_threads = True
|
|
109
|
+
host, port = self.httpd.server_address
|
|
110
|
+
self.url = (
|
|
111
|
+
f"http://{host}:{port}/jump?token="
|
|
112
|
+
+ urllib.parse.quote(self.token, safe="")
|
|
113
|
+
)
|
|
114
|
+
self.thread = threading.Thread(
|
|
115
|
+
target=self.httpd.serve_forever,
|
|
116
|
+
daemon=True,
|
|
117
|
+
)
|
|
118
|
+
self.thread.start()
|
|
119
|
+
return self.url
|
|
120
|
+
|
|
121
|
+
def stop(self):
|
|
122
|
+
if self.httpd is not None:
|
|
123
|
+
self.httpd.shutdown()
|
|
124
|
+
self.httpd.server_close()
|
|
125
|
+
self.httpd = None
|
|
126
|
+
if self.thread is not None:
|
|
127
|
+
self.thread.join(timeout=1.0)
|
|
128
|
+
self.thread = None
|