pyDiffTools 0.1.25__tar.gz → 0.1.26__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.25/pyDiffTools.egg-info → pydifftools-0.1.26}/PKG-INFO +5 -1
- {pydifftools-0.1.25 → pydifftools-0.1.26}/README.rst +4 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26/pyDiffTools.egg-info}/PKG-INFO +5 -1
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pyDiffTools.egg-info/SOURCES.txt +4 -0
- pydifftools-0.1.26/pydifftools/browser_lifecycle.py +68 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/command_line.py +71 -37
- pydifftools-0.1.26/pydifftools/command_registry.py +119 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/continuous.py +47 -56
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/flowchart/graph.py +33 -4
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/flowchart/watch_graph.py +106 -45
- pydifftools-0.1.26/pydifftools/git_gd.py +356 -0
- pydifftools-0.1.26/pydifftools/git_gd_qt.py +334 -0
- pydifftools-0.1.26/pydifftools/log_example.py +28 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/notebook/fast_build.py +228 -68
- pydifftools-0.1.26/pydifftools/update_check.py +73 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pyproject.toml +1 -1
- pydifftools-0.1.26/tests/test_command_line_help.py +56 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/tests/test_continuous_shutdown.py +1 -1
- {pydifftools-0.1.25 → pydifftools-0.1.26}/tests/test_update_check.py +48 -0
- pydifftools-0.1.25/pydifftools/browser_lifecycle.py +0 -25
- pydifftools-0.1.25/pydifftools/command_registry.py +0 -65
- pydifftools-0.1.25/pydifftools/update_check.py +0 -31
- {pydifftools-0.1.25 → pydifftools-0.1.26}/LICENSE.md +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/MANIFEST.in +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/_quarto.yml +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/example.qmd +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/example.tex +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/index.qmd +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/subproject1/independent.qmd +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/subproject1/index.qmd +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/subproject1/tasks.qmd +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/subproject1/test_include.qmd +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/example_notebook/project1/test_include.qmd +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pyDiffTools.egg-info/dependency_links.txt +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pyDiffTools.egg-info/entry_points.txt +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pyDiffTools.egg-info/requires.txt +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pyDiffTools.egg-info/top_level.txt +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/__init__.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/check_numbers.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/comment_functions.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/comment_tags.lua +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/comment_toggle.js +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/comments.css +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/copy_files.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/diff-doc.js +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/doc_contents.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/flowchart/__init__.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/flowchart/dot_to_yaml.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/html_comments.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/html_uncomments.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/match_spaces.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/notebook/__init__.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/notebook/tex_to_qmd.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/onewordify.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/onewordify_undo.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/outline.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/rearrange_tex.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/searchacro.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/separate_comments.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/split_conflict.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/unseparate_comments.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/wrap_sentences.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/pydifftools/xml2xlsx.vbs +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/setup.cfg +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/tests/test_browser_lifecycle.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/tests/test_rrng.py +0 -0
- {pydifftools-0.1.25 → pydifftools-0.1.26}/tests/test_tex_to_qmd.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyDiffTools
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.26
|
|
4
4
|
Summary: Diff tools
|
|
5
5
|
Author: J M Franck
|
|
6
6
|
License: Copyright (c) 2015, jmfranck
|
|
@@ -106,6 +106,10 @@ included are (listed in order of fun/utility):
|
|
|
106
106
|
``_build``/``_display`` directories; with ``--watch`` it starts the HTTP
|
|
107
107
|
server and automatically rebuilds the staged fragments whenever you edit
|
|
108
108
|
a ``.qmd`` file.
|
|
109
|
+
- `pydifft gd [git diff args...]` shows the same Qt review table as the old
|
|
110
|
+
``git_gd_qt.py`` helper before launching ``git difftool`` for a selected
|
|
111
|
+
file. Run ``pydifft gd --install`` to add the matching ``git gd`` alias
|
|
112
|
+
to your global git config.
|
|
109
113
|
- `pydifft qmdinit [directory]` scaffolds a new Quarto-style project using
|
|
110
114
|
the bundled templates and example ``project1`` hierarchy, then downloads
|
|
111
115
|
MathJax into ``_template/mathjax`` so the builder can run immediately.
|
|
@@ -56,6 +56,10 @@ included are (listed in order of fun/utility):
|
|
|
56
56
|
``_build``/``_display`` directories; with ``--watch`` it starts the HTTP
|
|
57
57
|
server and automatically rebuilds the staged fragments whenever you edit
|
|
58
58
|
a ``.qmd`` file.
|
|
59
|
+
- `pydifft gd [git diff args...]` shows the same Qt review table as the old
|
|
60
|
+
``git_gd_qt.py`` helper before launching ``git difftool`` for a selected
|
|
61
|
+
file. Run ``pydifft gd --install`` to add the matching ``git gd`` alias
|
|
62
|
+
to your global git config.
|
|
59
63
|
- `pydifft qmdinit [directory]` scaffolds a new Quarto-style project using
|
|
60
64
|
the bundled templates and example ``project1`` hierarchy, then downloads
|
|
61
65
|
MathJax into ``_template/mathjax`` so the builder can run immediately.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyDiffTools
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.26
|
|
4
4
|
Summary: Diff tools
|
|
5
5
|
Author: J M Franck
|
|
6
6
|
License: Copyright (c) 2015, jmfranck
|
|
@@ -106,6 +106,10 @@ included are (listed in order of fun/utility):
|
|
|
106
106
|
``_build``/``_display`` directories; with ``--watch`` it starts the HTTP
|
|
107
107
|
server and automatically rebuilds the staged fragments whenever you edit
|
|
108
108
|
a ``.qmd`` file.
|
|
109
|
+
- `pydifft gd [git diff args...]` shows the same Qt review table as the old
|
|
110
|
+
``git_gd_qt.py`` helper before launching ``git difftool`` for a selected
|
|
111
|
+
file. Run ``pydifft gd --install`` to add the matching ``git gd`` alias
|
|
112
|
+
to your global git config.
|
|
109
113
|
- `pydifft qmdinit [directory]` scaffolds a new Quarto-style project using
|
|
110
114
|
the bundled templates and example ``project1`` hierarchy, then downloads
|
|
111
115
|
MathJax into ``_template/mathjax`` so the builder can run immediately.
|
|
@@ -57,8 +57,11 @@ pydifftools/continuous.py
|
|
|
57
57
|
pydifftools/copy_files.py
|
|
58
58
|
pydifftools/diff-doc.js
|
|
59
59
|
pydifftools/doc_contents.py
|
|
60
|
+
pydifftools/git_gd.py
|
|
61
|
+
pydifftools/git_gd_qt.py
|
|
60
62
|
pydifftools/html_comments.py
|
|
61
63
|
pydifftools/html_uncomments.py
|
|
64
|
+
pydifftools/log_example.py
|
|
62
65
|
pydifftools/match_spaces.py
|
|
63
66
|
pydifftools/onewordify.py
|
|
64
67
|
pydifftools/onewordify_undo.py
|
|
@@ -79,6 +82,7 @@ pydifftools/notebook/__init__.py
|
|
|
79
82
|
pydifftools/notebook/fast_build.py
|
|
80
83
|
pydifftools/notebook/tex_to_qmd.py
|
|
81
84
|
tests/test_browser_lifecycle.py
|
|
85
|
+
tests/test_command_line_help.py
|
|
82
86
|
tests/test_continuous_shutdown.py
|
|
83
87
|
tests/test_rrng.py
|
|
84
88
|
tests/test_tex_to_qmd.py
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
def browser_window_is_alive(browser):
|
|
6
|
+
# Keep all browser liveness checks in one place so watch commands share
|
|
7
|
+
# the same shutdown behavior when a user closes the browser window.
|
|
8
|
+
# Do not probe with execute_script here: page navigations can briefly
|
|
9
|
+
# interrupt script execution even while the window is still open.
|
|
10
|
+
if browser is None:
|
|
11
|
+
return False
|
|
12
|
+
try:
|
|
13
|
+
handles = browser.window_handles
|
|
14
|
+
if not handles:
|
|
15
|
+
return False
|
|
16
|
+
return True
|
|
17
|
+
except Exception:
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def close_browser_window(browser):
|
|
22
|
+
# Close a Selenium browser session and ignore errors from already-closed
|
|
23
|
+
# windows so cleanup paths stay simple.
|
|
24
|
+
if browser is None:
|
|
25
|
+
return
|
|
26
|
+
try:
|
|
27
|
+
browser.quit()
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def forward_search_in_browser(browser, search_text):
|
|
33
|
+
# Reuse the same browser-side find logic across cpb and qmdb.
|
|
34
|
+
if browser is None or not search_text:
|
|
35
|
+
return False
|
|
36
|
+
found = browser.execute_script(
|
|
37
|
+
"""
|
|
38
|
+
var searchText = arguments[0];
|
|
39
|
+
if (!window.find) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
var didFind = window.find(searchText);
|
|
43
|
+
if (didFind && window.getSelection) {
|
|
44
|
+
var selection = window.getSelection();
|
|
45
|
+
if (selection.rangeCount > 0) {
|
|
46
|
+
var rect = selection.getRangeAt(0).getBoundingClientRect();
|
|
47
|
+
window.scrollBy(0, rect.top - window.innerHeight / 3);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return didFind;
|
|
51
|
+
""",
|
|
52
|
+
search_text,
|
|
53
|
+
)
|
|
54
|
+
if not found:
|
|
55
|
+
print("forward search did not find text:", search_text)
|
|
56
|
+
# Bring the browser window to the foreground in Linux window managers.
|
|
57
|
+
if os.name == "posix" and shutil.which("wmctrl"):
|
|
58
|
+
window_title = browser.execute_script("return document.title;")
|
|
59
|
+
if window_title:
|
|
60
|
+
# Try common Chromium title forms used by desktop environments.
|
|
61
|
+
for title_candidate in [
|
|
62
|
+
window_title,
|
|
63
|
+
window_title + " - Google Chrome",
|
|
64
|
+
window_title + " - Chromium",
|
|
65
|
+
window_title + " - Chrome",
|
|
66
|
+
]:
|
|
67
|
+
subprocess.run(["wmctrl", "-a", title_candidate], check=False)
|
|
68
|
+
return found
|
|
@@ -27,10 +27,16 @@ from .comment_functions import matchingbrackets
|
|
|
27
27
|
from .copy_files import copy_image_files
|
|
28
28
|
from .searchacro import replace_acros
|
|
29
29
|
from .rearrange_tex import run as rearrange_tex_run
|
|
30
|
+
from .git_gd import gd # registers git difftool review command
|
|
30
31
|
from .flowchart.watch_graph import wgrph
|
|
31
32
|
from .flowchart.graph import load_graph_yaml
|
|
32
33
|
from .notebook.tex_to_qmd import tex2qmd
|
|
33
|
-
from .notebook.fast_build import
|
|
34
|
+
from .notebook.fast_build import (
|
|
35
|
+
qmdb,
|
|
36
|
+
qmdinit,
|
|
37
|
+
QMDB_FORWARD_SEARCH_HOST,
|
|
38
|
+
QMDB_FORWARD_SEARCH_PORT,
|
|
39
|
+
)
|
|
34
40
|
|
|
35
41
|
from .command_registry import _COMMAND_SPECS, register_command
|
|
36
42
|
|
|
@@ -518,27 +524,48 @@ def fs(arguments):
|
|
|
518
524
|
"markdown forward search, use with cpb to jump to text in the browser"
|
|
519
525
|
)
|
|
520
526
|
def mfs(text):
|
|
521
|
-
#
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
527
|
+
# Normalize markdown-specific markup so search phrases match rendered text.
|
|
528
|
+
search_text = text
|
|
529
|
+
marker_match = re.search(r"(?i)@[a-z]{2,4}:", search_text)
|
|
530
|
+
if marker_match:
|
|
531
|
+
search_text = search_text[: marker_match.start()]
|
|
532
|
+
search_text = re.sub(r"\[@[^\]]+\]", " ", search_text)
|
|
533
|
+
search_text = search_text.replace("**", " ").replace("*", " ")
|
|
534
|
+
search_text = re.sub(r"\s+", " ", search_text).strip()
|
|
535
|
+
if not search_text:
|
|
536
|
+
search_text = text.strip()
|
|
537
|
+
|
|
538
|
+
# Try existing cpb and qmdb listeners before launching a new cpb process.
|
|
539
|
+
socket_addresses = [
|
|
540
|
+
(FORWARD_SEARCH_HOST, FORWARD_SEARCH_PORT),
|
|
541
|
+
(QMDB_FORWARD_SEARCH_HOST, QMDB_FORWARD_SEARCH_PORT),
|
|
542
|
+
]
|
|
543
|
+
client = None
|
|
544
|
+
for address in socket_addresses:
|
|
545
|
+
candidate = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
546
|
+
try:
|
|
547
|
+
candidate.connect(address)
|
|
548
|
+
client = candidate
|
|
549
|
+
break
|
|
550
|
+
except OSError:
|
|
551
|
+
candidate.close()
|
|
552
|
+
if client is None:
|
|
553
|
+
# If no listener is running yet, choose a markdown file in this
|
|
554
|
+
# directory that contains the requested search text and start cpb for
|
|
555
|
+
# it.
|
|
529
556
|
matching_files = []
|
|
530
557
|
for filename in sorted(os.listdir(".")):
|
|
531
558
|
if not filename.endswith(".md"):
|
|
532
559
|
continue
|
|
533
560
|
with open(filename, encoding="utf-8") as fp:
|
|
534
|
-
if
|
|
561
|
+
if search_text in fp.read():
|
|
535
562
|
matching_files.append(filename)
|
|
536
563
|
if len(matching_files) == 0:
|
|
537
564
|
raise RuntimeError(
|
|
538
|
-
"Could not connect to cpb forward search
|
|
565
|
+
"Could not connect to cpb or qmdb forward search sockets and "
|
|
539
566
|
"could not find the requested text in any .md file in the "
|
|
540
567
|
"current directory."
|
|
541
|
-
)
|
|
568
|
+
)
|
|
542
569
|
if len(matching_files) > 1:
|
|
543
570
|
print(
|
|
544
571
|
"Found search text in multiple markdown files. "
|
|
@@ -554,19 +581,23 @@ def mfs(text):
|
|
|
554
581
|
# Wait up to 20 seconds so the child can bring up the listener,
|
|
555
582
|
# then resend the forward-search text.
|
|
556
583
|
for _ in range(80):
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
584
|
+
for address in socket_addresses:
|
|
585
|
+
candidate = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
586
|
+
try:
|
|
587
|
+
candidate.connect(address)
|
|
588
|
+
client = candidate
|
|
589
|
+
break
|
|
590
|
+
except OSError:
|
|
591
|
+
candidate.close()
|
|
592
|
+
if client is not None:
|
|
560
593
|
break
|
|
561
|
-
|
|
562
|
-
client.close()
|
|
563
|
-
time.sleep(0.25)
|
|
594
|
+
time.sleep(0.25)
|
|
564
595
|
else:
|
|
565
596
|
raise RuntimeError(
|
|
566
597
|
"Started cpb automatically, but the forward search socket "
|
|
567
598
|
"did not come up within 20 seconds."
|
|
568
599
|
)
|
|
569
|
-
client.sendall(
|
|
600
|
+
client.sendall(search_text.encode("utf-8"))
|
|
570
601
|
client.close()
|
|
571
602
|
|
|
572
603
|
|
|
@@ -841,22 +872,11 @@ def build_parser():
|
|
|
841
872
|
flags = argument["flags"]
|
|
842
873
|
kwargs = dict(argument["kwargs"])
|
|
843
874
|
action = subparser.add_argument(*flags, **kwargs)
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
# Provide YAML-only completions for the flowchart watcher.
|
|
850
|
-
action.completer = FilesCompleter(
|
|
851
|
-
allowednames=["*.yaml", "*.yml"]
|
|
852
|
-
)
|
|
853
|
-
if (
|
|
854
|
-
FilesCompleter is not None
|
|
855
|
-
and name == "cpb"
|
|
856
|
-
and action.dest == "filename"
|
|
857
|
-
):
|
|
858
|
-
# Provide Markdown-only completions for continuous pandoc build.
|
|
859
|
-
action.completer = FilesCompleter(allowednames=["*.md"])
|
|
875
|
+
# {{{ attach filename completer
|
|
876
|
+
allowednames = argument.get("completion_allowednames")
|
|
877
|
+
if FilesCompleter is not None and allowednames is not None:
|
|
878
|
+
action.completer = FilesCompleter(allowednames=allowednames)
|
|
879
|
+
# }}}
|
|
860
880
|
if name == "wgrph" and action.dest == "t":
|
|
861
881
|
# Offer case-insensitive completions for incomplete task names.
|
|
862
882
|
action.completer = wgrph_task_completer
|
|
@@ -888,9 +908,23 @@ def main(argv=None):
|
|
|
888
908
|
file=sys.stderr,
|
|
889
909
|
)
|
|
890
910
|
parser = build_parser()
|
|
911
|
+
# {{{ run argcomplete
|
|
891
912
|
if argcomplete is not None:
|
|
892
|
-
#
|
|
893
|
-
|
|
913
|
+
# Only suggest options after the user types '-' so bare
|
|
914
|
+
# `pydifft <tab>` offers subcommands only.
|
|
915
|
+
try:
|
|
916
|
+
argcomplete.autocomplete(
|
|
917
|
+
parser,
|
|
918
|
+
always_complete_options=False,
|
|
919
|
+
exit_method=os._exit,
|
|
920
|
+
output_stream=None,
|
|
921
|
+
)
|
|
922
|
+
except TypeError as exc:
|
|
923
|
+
if "unexpected keyword argument" not in str(exc):
|
|
924
|
+
raise
|
|
925
|
+
# Test stubs may provide a minimal autocomplete(parser) shim only.
|
|
926
|
+
argcomplete.autocomplete(parser)
|
|
927
|
+
# }}}
|
|
894
928
|
if not argv:
|
|
895
929
|
parser.print_help()
|
|
896
930
|
return
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CommandRegistrationError(Exception):
|
|
6
|
+
"""Exception raised when attempting to register a duplicate subcommand."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Registry that stores all subcommands made available to the CLI dispatcher.
|
|
10
|
+
_COMMAND_SPECS = {}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register_command(
|
|
14
|
+
help_text,
|
|
15
|
+
description=None,
|
|
16
|
+
help=None,
|
|
17
|
+
filename_extensions=None,
|
|
18
|
+
):
|
|
19
|
+
"""Register a command handler for the CLI dispatcher."""
|
|
20
|
+
|
|
21
|
+
def decorator(func):
|
|
22
|
+
name = func.__name__.replace("_", "-")
|
|
23
|
+
if name in _COMMAND_SPECS:
|
|
24
|
+
raise CommandRegistrationError(
|
|
25
|
+
f"Command '{name}' already registered"
|
|
26
|
+
)
|
|
27
|
+
signature = inspect.signature(func)
|
|
28
|
+
parameters = [
|
|
29
|
+
parameter
|
|
30
|
+
for parameter in signature.parameters.values()
|
|
31
|
+
if parameter.kind
|
|
32
|
+
not in [
|
|
33
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
34
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
35
|
+
]
|
|
36
|
+
]
|
|
37
|
+
# {{{ normalize filename extensions
|
|
38
|
+
if filename_extensions is None:
|
|
39
|
+
completion_allowednames = {}
|
|
40
|
+
else:
|
|
41
|
+
completion_allowednames = {}
|
|
42
|
+
for argument_name, extensions in filename_extensions.items():
|
|
43
|
+
if isinstance(extensions, str):
|
|
44
|
+
extensions = [extensions]
|
|
45
|
+
completion_allowednames[argument_name] = []
|
|
46
|
+
for extension in extensions:
|
|
47
|
+
if (
|
|
48
|
+
not isinstance(extension, str)
|
|
49
|
+
or len(extension.strip()) == 0
|
|
50
|
+
):
|
|
51
|
+
raise CommandRegistrationError(
|
|
52
|
+
"filename_extensions must contain non-empty"
|
|
53
|
+
" strings"
|
|
54
|
+
)
|
|
55
|
+
extension = extension.strip()
|
|
56
|
+
if extension.startswith("*."):
|
|
57
|
+
completion_allowednames[argument_name].append(
|
|
58
|
+
extension
|
|
59
|
+
)
|
|
60
|
+
elif extension.startswith("."):
|
|
61
|
+
completion_allowednames[argument_name].append(
|
|
62
|
+
"*" + extension
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
completion_allowednames[argument_name].append(
|
|
66
|
+
"*." + extension
|
|
67
|
+
)
|
|
68
|
+
# }}}
|
|
69
|
+
unknown_arguments = sorted(
|
|
70
|
+
set(completion_allowednames)
|
|
71
|
+
- {parameter.name for parameter in parameters}
|
|
72
|
+
)
|
|
73
|
+
if unknown_arguments:
|
|
74
|
+
raise CommandRegistrationError(
|
|
75
|
+
"filename_extensions references unknown arguments: "
|
|
76
|
+
+ ", ".join(unknown_arguments)
|
|
77
|
+
)
|
|
78
|
+
_COMMAND_SPECS[name] = {
|
|
79
|
+
"handler": func,
|
|
80
|
+
"help": help_text.strip(),
|
|
81
|
+
"description": (
|
|
82
|
+
description if description is not None else help_text
|
|
83
|
+
).strip(),
|
|
84
|
+
"arguments": [],
|
|
85
|
+
}
|
|
86
|
+
argument_help = help if help is not None else {}
|
|
87
|
+
for parameter in parameters:
|
|
88
|
+
flags = []
|
|
89
|
+
kwargs = {}
|
|
90
|
+
if parameter.default is inspect._empty:
|
|
91
|
+
flags.append(parameter.name)
|
|
92
|
+
if parameter.name == "arguments":
|
|
93
|
+
# Most commands accept a raw list of trailing arguments.
|
|
94
|
+
kwargs["nargs"] = argparse.REMAINDER
|
|
95
|
+
kwargs["help"] = argparse.SUPPRESS
|
|
96
|
+
else:
|
|
97
|
+
# Single-letter keywords use a short flag; everything else uses
|
|
98
|
+
# the long two-dash style expected by the CLI.
|
|
99
|
+
dash_prefix = "-" if len(parameter.name) == 1 else "--"
|
|
100
|
+
flags.append(dash_prefix + parameter.name.replace("_", "-"))
|
|
101
|
+
kwargs["default"] = parameter.default
|
|
102
|
+
if isinstance(parameter.default, bool):
|
|
103
|
+
# Boolean flags toggle on or off without needing a value.
|
|
104
|
+
kwargs["action"] = (
|
|
105
|
+
"store_false" if parameter.default else "store_true"
|
|
106
|
+
)
|
|
107
|
+
elif parameter.default is not None:
|
|
108
|
+
kwargs["type"] = type(parameter.default)
|
|
109
|
+
if parameter.name in argument_help:
|
|
110
|
+
kwargs["help"] = argument_help[parameter.name].strip()
|
|
111
|
+
argument_spec = {"flags": flags, "kwargs": kwargs}
|
|
112
|
+
if parameter.name in completion_allowednames:
|
|
113
|
+
argument_spec["completion_allowednames"] = (
|
|
114
|
+
completion_allowednames[parameter.name]
|
|
115
|
+
)
|
|
116
|
+
_COMMAND_SPECS[name]["arguments"].append(argument_spec)
|
|
117
|
+
return func
|
|
118
|
+
|
|
119
|
+
return decorator
|
|
@@ -12,7 +12,11 @@ import queue
|
|
|
12
12
|
from watchdog.events import FileSystemEventHandler
|
|
13
13
|
from watchdog.observers import Observer
|
|
14
14
|
from .command_registry import register_command
|
|
15
|
-
from .browser_lifecycle import
|
|
15
|
+
from .browser_lifecycle import (
|
|
16
|
+
browser_window_is_alive,
|
|
17
|
+
close_browser_window,
|
|
18
|
+
forward_search_in_browser,
|
|
19
|
+
)
|
|
16
20
|
|
|
17
21
|
FORWARD_SEARCH_HOST = "127.0.0.1"
|
|
18
22
|
FORWARD_SEARCH_PORT = 51235
|
|
@@ -122,7 +126,11 @@ def run_pandoc(filename, html_file, comments_to_margin=False):
|
|
|
122
126
|
# into the markdown directory before collecting css/lua/js companion files.
|
|
123
127
|
with open(filename, encoding="utf-8") as fp:
|
|
124
128
|
markdown_text = fp.read()
|
|
125
|
-
if
|
|
129
|
+
if (
|
|
130
|
+
"<comment>" in markdown_text
|
|
131
|
+
or "comment-right" in markdown_text
|
|
132
|
+
or "comment-left" in markdown_text
|
|
133
|
+
):
|
|
126
134
|
package_dir = os.path.dirname(os.path.abspath(__file__))
|
|
127
135
|
for asset_name in ["comments.css", "comment_toggle.js"]:
|
|
128
136
|
target_path = os.path.join(source_dir, asset_name)
|
|
@@ -139,9 +147,11 @@ def run_pandoc(filename, html_file, comments_to_margin=False):
|
|
|
139
147
|
]
|
|
140
148
|
if len(localfiles[k]) == 1:
|
|
141
149
|
localfiles[k] = os.path.join(source_dir, localfiles[k][0])
|
|
150
|
+
elif len(localfiles[k]) == 0:
|
|
151
|
+
localfiles[k] = None
|
|
142
152
|
else:
|
|
143
153
|
raise ValueError(
|
|
144
|
-
f"You have more than one
|
|
154
|
+
f"You have more than one {k} file in this directory!"
|
|
145
155
|
" Get rid of all but one! of " + "and".join(localfiles[k])
|
|
146
156
|
)
|
|
147
157
|
# Include any css files next to the markdown source in the pandoc output.
|
|
@@ -150,8 +160,13 @@ def run_pandoc(filename, html_file, comments_to_margin=False):
|
|
|
150
160
|
)
|
|
151
161
|
# Include any lua filters next to the markdown source in the pandoc
|
|
152
162
|
# output by passing repeated --lua-filter arguments.
|
|
163
|
+
lua_priority = {
|
|
164
|
+
"scholarly-metadata.lua": 0,
|
|
165
|
+
"author-info-blocks.lua": 1,
|
|
166
|
+
}
|
|
153
167
|
localfiles["lua"] = sorted(
|
|
154
|
-
[f for f in os.listdir(source_dir) if f.endswith(".lua")]
|
|
168
|
+
[f for f in os.listdir(source_dir) if f.endswith(".lua")],
|
|
169
|
+
key=lambda name: (lua_priority.get(name, 2), name),
|
|
155
170
|
)
|
|
156
171
|
# Include any javascript files next to the markdown source by injecting
|
|
157
172
|
# script tags after pandoc runs. This adds extra javascript and does not
|
|
@@ -161,9 +176,6 @@ def run_pandoc(filename, html_file, comments_to_margin=False):
|
|
|
161
176
|
)
|
|
162
177
|
command = [
|
|
163
178
|
"pandoc",
|
|
164
|
-
"--bibliography",
|
|
165
|
-
localfiles["bib"],
|
|
166
|
-
f"--csl={localfiles['csl']}",
|
|
167
179
|
"--filter",
|
|
168
180
|
"pandoc-crossref",
|
|
169
181
|
"--citeproc",
|
|
@@ -175,16 +187,31 @@ def run_pandoc(filename, html_file, comments_to_margin=False):
|
|
|
175
187
|
html_file,
|
|
176
188
|
filename,
|
|
177
189
|
]
|
|
190
|
+
if localfiles["bib"]:
|
|
191
|
+
command[1:1] = ["--bibliography", localfiles["bib"]]
|
|
192
|
+
if localfiles["csl"]:
|
|
193
|
+
command.insert(1, f"--csl={localfiles['csl']}")
|
|
178
194
|
for css_file in localfiles["css"]:
|
|
179
195
|
command.extend(["--css", os.path.join(source_dir, css_file)])
|
|
180
196
|
for lua_file in localfiles["lua"]:
|
|
181
197
|
command.extend(["--lua-filter", os.path.join(source_dir, lua_file)])
|
|
182
198
|
# command = ['pandoc', '-s', '--mathjax', '-o', html_file, filename]
|
|
183
199
|
print("running:", " ".join(command))
|
|
184
|
-
subprocess.run(
|
|
200
|
+
completed = subprocess.run(
|
|
185
201
|
command,
|
|
186
202
|
)
|
|
203
|
+
if getattr(completed, "returncode", 0) != 0:
|
|
204
|
+
raise RuntimeError(
|
|
205
|
+
f"Pandoc failed with exit code {completed.returncode} while "
|
|
206
|
+
f"building {html_file}.\n"
|
|
207
|
+
f"Command: {' '.join(command)}"
|
|
208
|
+
)
|
|
187
209
|
print("running:\n", command)
|
|
210
|
+
if not os.path.exists(html_file):
|
|
211
|
+
raise RuntimeError(
|
|
212
|
+
"Pandoc completed but did not create the expected HTML file: "
|
|
213
|
+
f"{html_file}"
|
|
214
|
+
)
|
|
188
215
|
if has_local_jax:
|
|
189
216
|
# {{{ for slow internet connection, remove remote files
|
|
190
217
|
with open(html_file, encoding="utf-8") as fp:
|
|
@@ -239,14 +266,14 @@ class Handler(FileSystemEventHandler):
|
|
|
239
266
|
self.filename = filename
|
|
240
267
|
self.comments_to_margin = comments_to_margin
|
|
241
268
|
self.html_file = filename.rsplit(".", 1)[0] + ".html"
|
|
242
|
-
self.
|
|
269
|
+
self.init_chrome()
|
|
243
270
|
|
|
244
|
-
def
|
|
271
|
+
def init_chrome(self):
|
|
245
272
|
# apparently, selenium breaks stdin/out for tests, so it must be
|
|
246
273
|
# imported here
|
|
247
274
|
from selenium import webdriver
|
|
248
275
|
|
|
249
|
-
self.
|
|
276
|
+
self.chrome = webdriver.Chrome()
|
|
250
277
|
run_pandoc(
|
|
251
278
|
self.filename,
|
|
252
279
|
self.html_file,
|
|
@@ -255,7 +282,7 @@ class Handler(FileSystemEventHandler):
|
|
|
255
282
|
if not os.path.exists(self.html_file):
|
|
256
283
|
print("html doesn't exist")
|
|
257
284
|
self.append_autorefresh()
|
|
258
|
-
self.
|
|
285
|
+
self.chrome.get("file://" + os.path.abspath(self.html_file))
|
|
259
286
|
|
|
260
287
|
def on_modified(self, event):
|
|
261
288
|
from selenium.common.exceptions import WebDriverException
|
|
@@ -270,14 +297,14 @@ class Handler(FileSystemEventHandler):
|
|
|
270
297
|
)
|
|
271
298
|
self.append_autorefresh()
|
|
272
299
|
try:
|
|
273
|
-
self.
|
|
300
|
+
self.chrome.refresh()
|
|
274
301
|
except WebDriverException:
|
|
275
302
|
print(
|
|
276
303
|
"I'm quitting!! You probably suspended the computer, which"
|
|
277
304
|
" seems to freak selenium out. Just restart"
|
|
278
305
|
)
|
|
279
|
-
self.
|
|
280
|
-
self.
|
|
306
|
+
self.chrome.quit()
|
|
307
|
+
self.init_chrome()
|
|
281
308
|
|
|
282
309
|
def append_autorefresh(self):
|
|
283
310
|
with open(self.html_file, "r", encoding="utf-8") as fp:
|
|
@@ -348,47 +375,10 @@ position
|
|
|
348
375
|
fp.write(all_data)
|
|
349
376
|
|
|
350
377
|
def forward_search(self, search_text):
|
|
351
|
-
#
|
|
378
|
+
# Reuse shared browser search behavior so cpb and qmdb stay in sync.
|
|
352
379
|
if not search_text:
|
|
353
380
|
return
|
|
354
|
-
|
|
355
|
-
"""
|
|
356
|
-
var searchText = arguments[0];
|
|
357
|
-
if (!window.find) {
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
360
|
-
var didFind = window.find(searchText);
|
|
361
|
-
if (didFind && window.getSelection) {
|
|
362
|
-
var selection = window.getSelection();
|
|
363
|
-
if (selection.rangeCount > 0) {
|
|
364
|
-
var rect = selection.getRangeAt(0).getBoundingClientRect();
|
|
365
|
-
window.scrollBy(0, rect.top - window.innerHeight / 3);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
return didFind;
|
|
369
|
-
""",
|
|
370
|
-
search_text,
|
|
371
|
-
)
|
|
372
|
-
if not found:
|
|
373
|
-
print("forward search did not find text:", search_text)
|
|
374
|
-
# Bring the browser window to the foreground in Linux window managers.
|
|
375
|
-
if os.name == "posix" and shutil.which("wmctrl"):
|
|
376
|
-
window_title = self.firefox.execute_script(
|
|
377
|
-
"return document.title;"
|
|
378
|
-
)
|
|
379
|
-
if window_title:
|
|
380
|
-
# Try common Chromium title forms used by desktop environments.
|
|
381
|
-
for title_candidate in [
|
|
382
|
-
window_title,
|
|
383
|
-
window_title + " - Google Chrome",
|
|
384
|
-
window_title + " - Chromium",
|
|
385
|
-
window_title + " - Chrome",
|
|
386
|
-
]:
|
|
387
|
-
subprocess.run(
|
|
388
|
-
["wmctrl", "-a", title_candidate],
|
|
389
|
-
check=False,
|
|
390
|
-
)
|
|
391
|
-
|
|
381
|
+
forward_search_in_browser(self.chrome, search_text)
|
|
392
382
|
|
|
393
383
|
@register_command(
|
|
394
384
|
"continuous pandoc build. Like latexmk, but for markdown!",
|
|
@@ -399,6 +389,7 @@ position
|
|
|
399
389
|
"comments filter for printing."
|
|
400
390
|
),
|
|
401
391
|
},
|
|
392
|
+
filename_extensions={"filename": ".md"},
|
|
402
393
|
)
|
|
403
394
|
def cpb(filename, comments_to_margin=False):
|
|
404
395
|
observer = Observer()
|
|
@@ -420,7 +411,7 @@ def cpb(filename, comments_to_margin=False):
|
|
|
420
411
|
while True:
|
|
421
412
|
# Exit when the browser window is closed so cpb does not leave a
|
|
422
413
|
# background process running after the user closes Chrome.
|
|
423
|
-
if not browser_window_is_alive(event_handler.
|
|
414
|
+
if not browser_window_is_alive(event_handler.chrome):
|
|
424
415
|
break
|
|
425
416
|
time.sleep(1)
|
|
426
417
|
while not search_queue.empty():
|
|
@@ -434,7 +425,7 @@ def cpb(filename, comments_to_margin=False):
|
|
|
434
425
|
observer.stop()
|
|
435
426
|
observer.join()
|
|
436
427
|
socket_thread.join()
|
|
437
|
-
close_browser_window(event_handler.
|
|
428
|
+
close_browser_window(event_handler.chrome)
|
|
438
429
|
|
|
439
430
|
|
|
440
431
|
if __name__ == "__main__":
|