pyDiffTools 0.1.54__tar.gz → 0.1.56__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.54/pyDiffTools.egg-info → pydifftools-0.1.56}/PKG-INFO +4 -2
- {pydifftools-0.1.54 → pydifftools-0.1.56}/README.rst +3 -1
- {pydifftools-0.1.54 → pydifftools-0.1.56/pyDiffTools.egg-info}/PKG-INFO +4 -2
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pyDiffTools.egg-info/SOURCES.txt +1 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/flowchart/source_jump.js +12 -3
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/line_alignment.py +60 -26
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/match_spaces.py +7 -1
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/notebook/fast_build.py +115 -5
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/wrap_sentences.py +24 -5
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pyproject.toml +1 -1
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_forward_search.py +20 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_line_alignment.py +83 -8
- pydifftools-0.1.56/tests/test_qt_lifecycle.py +51 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_wrap_check.py +50 -16
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_wrapping.py +8 -2
- {pydifftools-0.1.54 → pydifftools-0.1.56}/LICENSE.md +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/MANIFEST.in +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/_quarto.yml +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/example.qmd +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/example.tex +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/index.qmd +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/subproject1/independent.qmd +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/subproject1/index.qmd +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/subproject1/tasks.qmd +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/subproject1/test_include.qmd +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/example_notebook/project1/test_include.qmd +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pyDiffTools.egg-info/dependency_links.txt +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pyDiffTools.egg-info/entry_points.txt +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pyDiffTools.egg-info/requires.txt +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pyDiffTools.egg-info/top_level.txt +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/__init__.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/bibliography.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/browser_lifecycle.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/check_numbers.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/citation_dialog.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/command_line.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/command_registry.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/comment_filter_history.json +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/comment_functions.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/comment_migration.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/comment_tags.lua +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/comment_tags_margin.lua +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/comment_tags_no_comments.lua +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/comment_toggle.js +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/comments.css +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/comments_author_colors.css +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/completions/git-mergein +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/continuous.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/copy_files.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/diff-doc.js +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/doc_contents.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/flowchart/__init__.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/flowchart/comparison.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/flowchart/dot_to_yaml.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/flowchart/graph.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/flowchart/watch_graph.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/flowchart/wgrph_view.js +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/forward_search.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/git_aliases.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/git_gd.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/git_gd_history.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/git_gd_image.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/git_gd_qt.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/html_comments.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/html_uncomments.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/log_example.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/notebook/__init__.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/notebook/display.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/notebook/tex_to_qmd.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/onewordify.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/onewordify_undo.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/outline.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/pdf_diff.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/rearrange_tex.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/searchacro.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/separate_comments.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/source_jump.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/split_conflict.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/unseparate_comments.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/update_check.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/wrapping_options.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/xml2xlsx.vbs +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/pydifftools/zotero.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/setup.cfg +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_bibliography.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_browser_lifecycle.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_check_single_use.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_command_line_help.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_comment_filters.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_comment_migration.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_continuous_shutdown.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_git_gd_history.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_git_gd_image.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_git_gd_qt.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_git_mergein.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_git_pdf_diff.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_pdf_diff.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_rrng.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_source_jump.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_tex_to_qmd.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_update_check.py +0 -0
- {pydifftools-0.1.54 → pydifftools-0.1.56}/tests/test_zotero.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.56
|
|
4
4
|
Summary: Diff tools
|
|
5
5
|
Author: J M Franck
|
|
6
6
|
License: Copyright (c) 2015, jmfranck
|
|
@@ -306,7 +306,9 @@ included are (listed in order of fun/utility):
|
|
|
306
306
|
* ``cpb`` and ``wmatch`` share a matcher that finds changed hunks, aligns
|
|
307
307
|
their words using Git's minimal diff, and restores reference whitespace.
|
|
308
308
|
It then considers extra breaks that preserve an unchanged reference line
|
|
309
|
-
without stranding
|
|
309
|
+
without stranding an edited fragment of three words or fewer, and checks
|
|
310
|
+
wrapping rules. When matching a baseline, edited lines may exceed the
|
|
311
|
+
character width by at most four words before wrapping is required.
|
|
310
312
|
|
|
311
313
|
* ``--wrapnumber`` sets the maximum width (79 for ``cpb`` and ``wmatch``).
|
|
312
314
|
Wmatch also uses nearby reference widths for large edits, retaining
|
|
@@ -251,7 +251,9 @@ included are (listed in order of fun/utility):
|
|
|
251
251
|
* ``cpb`` and ``wmatch`` share a matcher that finds changed hunks, aligns
|
|
252
252
|
their words using Git's minimal diff, and restores reference whitespace.
|
|
253
253
|
It then considers extra breaks that preserve an unchanged reference line
|
|
254
|
-
without stranding
|
|
254
|
+
without stranding an edited fragment of three words or fewer, and checks
|
|
255
|
+
wrapping rules. When matching a baseline, edited lines may exceed the
|
|
256
|
+
character width by at most four words before wrapping is required.
|
|
255
257
|
|
|
256
258
|
* ``--wrapnumber`` sets the maximum width (79 for ``cpb`` and ``wmatch``).
|
|
257
259
|
Wmatch also uses nearby reference widths for large edits, retaining
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyDiffTools
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.56
|
|
4
4
|
Summary: Diff tools
|
|
5
5
|
Author: J M Franck
|
|
6
6
|
License: Copyright (c) 2015, jmfranck
|
|
@@ -306,7 +306,9 @@ included are (listed in order of fun/utility):
|
|
|
306
306
|
* ``cpb`` and ``wmatch`` share a matcher that finds changed hunks, aligns
|
|
307
307
|
their words using Git's minimal diff, and restores reference whitespace.
|
|
308
308
|
It then considers extra breaks that preserve an unchanged reference line
|
|
309
|
-
without stranding
|
|
309
|
+
without stranding an edited fragment of three words or fewer, and checks
|
|
310
|
+
wrapping rules. When matching a baseline, edited lines may exceed the
|
|
311
|
+
character width by at most four words before wrapping is required.
|
|
310
312
|
|
|
311
313
|
* ``--wrapnumber`` sets the maximum width (79 for ``cpb`` and ``wmatch``).
|
|
312
314
|
Wmatch also uses nearby reference widths for large edits, retaining
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// Add a small local-only source jump action to cpb and flowchart previews.
|
|
1
|
+
// Add a small local-only source jump action to cpb, qmdb and flowchart previews.
|
|
2
2
|
(function () {
|
|
3
3
|
const endpoint = window.pydifftSourceJumpEndpoint;
|
|
4
4
|
if (!endpoint) {
|
|
5
5
|
return;
|
|
6
6
|
}
|
|
7
7
|
const excludedSourceElements =
|
|
8
|
-
'.math,.MathJax,mjx-container,.katex,math,' +
|
|
8
|
+
'.pydifft-obs-prefix,.math,.MathJax,mjx-container,.katex,math,' +
|
|
9
9
|
'script[type^="math/tex"],' +
|
|
10
10
|
'a[href*="#fig:"],a[href*="#fig-"],a[href*="#fig%3A"]';
|
|
11
11
|
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
let phrase = '';
|
|
30
|
+
let source = null;
|
|
30
31
|
|
|
31
32
|
// Preserve HTML tag boundaries without adding spaces to words such as
|
|
32
33
|
// H<sub>2</sub>O. Python treats this reserved marker as a formatting gap.
|
|
@@ -43,6 +44,9 @@
|
|
|
43
44
|
|
|
44
45
|
function showMenu(event, text) {
|
|
45
46
|
phrase = normalizeSourceText(text || '');
|
|
47
|
+
const sourceElement = event.target && event.target.closest
|
|
48
|
+
? event.target.closest('[data-pydifft-source]') : null;
|
|
49
|
+
source = sourceElement ? sourceElement.dataset.pydifftSource : null;
|
|
46
50
|
if (!phrase) {
|
|
47
51
|
hideMenu();
|
|
48
52
|
return;
|
|
@@ -102,6 +106,11 @@
|
|
|
102
106
|
text += ' ';
|
|
103
107
|
return;
|
|
104
108
|
}
|
|
109
|
+
if (node.nodeType === Node.ELEMENT_NODE &&
|
|
110
|
+
(node.tagName === 'BR' || node.tagName === 'IMG')) {
|
|
111
|
+
text += ' ';
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
105
114
|
const tagBoundary = node !== block && node.nodeType === Node.ELEMENT_NODE;
|
|
106
115
|
if (tagBoundary) {
|
|
107
116
|
text += '\uE000';
|
|
@@ -144,7 +153,7 @@
|
|
|
144
153
|
fetch(endpoint, {
|
|
145
154
|
method: 'POST',
|
|
146
155
|
mode: 'no-cors',
|
|
147
|
-
body: phrase,
|
|
156
|
+
body: source ? JSON.stringify({source: source, phrase: phrase}) : phrase,
|
|
148
157
|
}).catch(function () {
|
|
149
158
|
// The preview may be closing while the local editor request is sent.
|
|
150
159
|
});
|
|
@@ -256,6 +256,7 @@ def align_line_breaks(
|
|
|
256
256
|
if allowed:
|
|
257
257
|
for line, message in check_prose(
|
|
258
258
|
span, width, punctuation_slop, offset,
|
|
259
|
+
overflow_words=4,
|
|
259
260
|
):
|
|
260
261
|
issues.setdefault(line, message)
|
|
261
262
|
offset += span.count("\n")
|
|
@@ -462,6 +463,58 @@ def align_line_breaks(
|
|
|
462
463
|
old_rows = {}
|
|
463
464
|
for number, (row, _, _) in enumerate(old_tokens):
|
|
464
465
|
old_rows.setdefault(row, [number, number])[1] = number
|
|
466
|
+
for row, (first, last) in old_rows.items():
|
|
467
|
+
if (
|
|
468
|
+
all(number in reverse for number in range(first, last + 1))
|
|
469
|
+
and reverse[last] - reverse[first] == last - first
|
|
470
|
+
):
|
|
471
|
+
spans.append((
|
|
472
|
+
reverse[first], reverse[last],
|
|
473
|
+
reference_lines[i1 + row].rstrip(),
|
|
474
|
+
))
|
|
475
|
+
inherited = {(first, last) for first, last, _ in spans}
|
|
476
|
+
token_gaps = [
|
|
477
|
+
before[left[2]:right[1]]
|
|
478
|
+
for left, right in zip(tokens, tokens[1:])
|
|
479
|
+
]
|
|
480
|
+
|
|
481
|
+
def creates_short_fragment(proposed, changed):
|
|
482
|
+
# Inspect only rows beside newly inserted breaks. Token
|
|
483
|
+
# ranges identify the actual inherited line, even when its
|
|
484
|
+
# text occurs elsewhere. Stop counting after four words.
|
|
485
|
+
for number in changed:
|
|
486
|
+
if (
|
|
487
|
+
"\n" not in proposed[number]
|
|
488
|
+
or "\n" in whitespace[number]
|
|
489
|
+
):
|
|
490
|
+
continue
|
|
491
|
+
for direction, first in ((-1, number), (1, number + 1)):
|
|
492
|
+
last = first
|
|
493
|
+
words = len(before[
|
|
494
|
+
tokens[first][1]:tokens[first][2]
|
|
495
|
+
].split())
|
|
496
|
+
while words <= 3 and (
|
|
497
|
+
(direction < 0 and first > 0)
|
|
498
|
+
or (direction > 0 and last < len(tokens) - 1)
|
|
499
|
+
):
|
|
500
|
+
gap = first - 1 if direction < 0 else last
|
|
501
|
+
raw = proposed.get(gap, token_gaps[gap])
|
|
502
|
+
if "\n" in raw:
|
|
503
|
+
break
|
|
504
|
+
adjacent = first - 1 if direction < 0 else last + 1
|
|
505
|
+
words += len(before[
|
|
506
|
+
tokens[adjacent][1]:tokens[adjacent][2]
|
|
507
|
+
].split()) - (not raw)
|
|
508
|
+
if words > 3:
|
|
509
|
+
break
|
|
510
|
+
if direction < 0:
|
|
511
|
+
first -= 1
|
|
512
|
+
else:
|
|
513
|
+
last += 1
|
|
514
|
+
if words <= 3 and (first, last) not in inherited:
|
|
515
|
+
return True
|
|
516
|
+
return False
|
|
517
|
+
|
|
465
518
|
for number in gaps:
|
|
466
519
|
old_number = matches[hunk].get(number)
|
|
467
520
|
if old_number is None and number + 1 in matches[hunk]:
|
|
@@ -477,14 +530,13 @@ def align_line_breaks(
|
|
|
477
530
|
region is not None
|
|
478
531
|
and start + tokens[number + 1][1] < region[1]
|
|
479
532
|
)
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
and "\n" not in gaps[number] and not inside_region
|
|
483
|
-
and len("".join(
|
|
484
|
-
before[:tokens[number][2]].split("\n")[-1].split()
|
|
485
|
-
)) < 10
|
|
533
|
+
if "\n" in raw and "\n" not in gaps[number] and (
|
|
534
|
+
not inside_region
|
|
486
535
|
):
|
|
487
|
-
|
|
536
|
+
proposed = dict(whitespace)
|
|
537
|
+
proposed[number] = raw
|
|
538
|
+
if creates_short_fragment(proposed, (number,)):
|
|
539
|
+
continue
|
|
488
540
|
if inside_region or (
|
|
489
541
|
current_layout[3][j1 + tokens[number][0]]
|
|
490
542
|
and current_layout[3][j1 + tokens[number + 1][0]]
|
|
@@ -495,21 +547,8 @@ def align_line_breaks(
|
|
|
495
547
|
ending + continuation[tokens[number][0]]
|
|
496
548
|
if "\n" in raw else raw
|
|
497
549
|
)
|
|
498
|
-
for row, (first, last) in old_rows.items():
|
|
499
|
-
if (
|
|
500
|
-
all(number in reverse for number in range(first, last + 1))
|
|
501
|
-
and reverse[last] - reverse[first] == last - first
|
|
502
|
-
):
|
|
503
|
-
spans.append(
|
|
504
|
-
(
|
|
505
|
-
reverse[first],
|
|
506
|
-
reverse[last],
|
|
507
|
-
reference_lines[i1 + row].rstrip(),
|
|
508
|
-
)
|
|
509
|
-
)
|
|
510
550
|
# }}}
|
|
511
551
|
# {{{ additional breaks preserve a line without tiny new fragments
|
|
512
|
-
inherited = {line.rstrip() for line in reference_lines}
|
|
513
552
|
for first, last, old_line in spans:
|
|
514
553
|
needed = range(
|
|
515
554
|
max(0, first - 1), min(len(tokens) - 1, last + 1)
|
|
@@ -543,12 +582,7 @@ def align_line_breaks(
|
|
|
543
582
|
continue
|
|
544
583
|
if old_line not in {line.rstrip() for line in proposed_rows}:
|
|
545
584
|
continue
|
|
546
|
-
if
|
|
547
|
-
line not in original_rows
|
|
548
|
-
and line.rstrip() not in inherited
|
|
549
|
-
and 0 < len("".join(line.split())) < 10
|
|
550
|
-
for line in proposed_rows
|
|
551
|
-
):
|
|
585
|
+
if creates_short_fragment(proposed, needed):
|
|
552
586
|
continue
|
|
553
587
|
whitespace = proposed
|
|
554
588
|
# }}}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from difflib import SequenceMatcher
|
|
2
2
|
from bisect import bisect_left
|
|
3
3
|
import math
|
|
4
|
+
import re
|
|
4
5
|
from .line_alignment import align_line_breaks
|
|
5
6
|
from .wrapping_options import (
|
|
6
7
|
DEFAULT_WIDTH,
|
|
@@ -117,7 +118,12 @@ def match_text(
|
|
|
117
118
|
return (
|
|
118
119
|
local
|
|
119
120
|
if any(
|
|
120
|
-
len(line) >
|
|
121
|
+
len(line) > local and (
|
|
122
|
+
joined or sum(
|
|
123
|
+
match.end() > local
|
|
124
|
+
for match in re.finditer(r"\S+", line)
|
|
125
|
+
) > 4
|
|
126
|
+
)
|
|
121
127
|
for line in restored.splitlines()
|
|
122
128
|
)
|
|
123
129
|
else fallback_width
|
|
@@ -21,6 +21,7 @@ import shutil
|
|
|
21
21
|
import queue
|
|
22
22
|
import yaml
|
|
23
23
|
from pydifftools.command_registry import register_command
|
|
24
|
+
from pydifftools.source_jump import jump_to_source, MAX_SOURCE_JUMP_BYTES
|
|
24
25
|
from pydifftools.browser_lifecycle import (
|
|
25
26
|
browser_window_is_alive,
|
|
26
27
|
close_browser_window,
|
|
@@ -61,6 +62,7 @@ CODE_DISPLAY_MODES = {
|
|
|
61
62
|
}
|
|
62
63
|
# {{{ Rebuild endpoint URL (changeable parameters)
|
|
63
64
|
RERUN_ENDPOINT = "/__pydifft/rerun"
|
|
65
|
+
SOURCE_JUMP_ENDPOINT = "/__pydifft/jump"
|
|
64
66
|
# }}}
|
|
65
67
|
# {{{ Right-click "SOURCE" to rebuild a notebook through the qmdb server
|
|
66
68
|
RERUN_SCRIPT = """
|
|
@@ -80,6 +82,7 @@ RERUN_SCRIPT = """
|
|
|
80
82
|
return;
|
|
81
83
|
}
|
|
82
84
|
e.preventDefault();
|
|
85
|
+
e.stopImmediatePropagation();
|
|
83
86
|
menu = document.createElement("div");
|
|
84
87
|
menu.textContent = "Rebuild";
|
|
85
88
|
menu.style.cssText = "position:fixed;z-index:2147483647;"
|
|
@@ -2210,11 +2213,32 @@ def _ensure_rerun_script(root):
|
|
|
2210
2213
|
head.append(script)
|
|
2211
2214
|
|
|
2212
2215
|
|
|
2216
|
+
def _normalize_observation_html(text):
|
|
2217
|
+
"""Keep raw observation boundaries intact across paragraphs and figures."""
|
|
2218
|
+
# HTML parsers close unknown inline tags at </p>. A block-capable
|
|
2219
|
+
# container keeps the closing tag bounding all of the observed prose.
|
|
2220
|
+
text = re.sub(
|
|
2221
|
+
r"<obs\b([^>]*)>",
|
|
2222
|
+
r'<div class="pydifft-observation"\1>',
|
|
2223
|
+
text,
|
|
2224
|
+
flags=re.IGNORECASE,
|
|
2225
|
+
)
|
|
2226
|
+
text = re.sub(r"</obs\s*>", "</div>", text, flags=re.IGNORECASE)
|
|
2227
|
+
return text
|
|
2228
|
+
|
|
2229
|
+
|
|
2213
2230
|
# also used by: tests/notebook/test_fast_build.py, which calls or
|
|
2214
2231
|
# monkeypatches this notebook-builder entry point.
|
|
2215
2232
|
def postprocess_html(html_path: Path, include_root: Path, resource_root: Path):
|
|
2216
2233
|
"""Replace placeholder nodes with referenced HTML bodies."""
|
|
2217
|
-
root = lxml_html.fromstring(
|
|
2234
|
+
root = lxml_html.fromstring(
|
|
2235
|
+
_normalize_observation_html(html_path.read_text())
|
|
2236
|
+
)
|
|
2237
|
+
# Mark fragments before expansion to retain nested include source paths.
|
|
2238
|
+
source = Path(os.path.relpath(html_path, DISPLAY_DIR)).with_suffix(".qmd")
|
|
2239
|
+
for node in root.xpath("//body") or [root]:
|
|
2240
|
+
if not node.get("data-pydifft-source"):
|
|
2241
|
+
node.set("data-pydifft-source", source.as_posix())
|
|
2218
2242
|
# keep processing until no include placeholders remain so nested includes
|
|
2219
2243
|
# are fully expanded in the served HTML
|
|
2220
2244
|
while True:
|
|
@@ -2229,7 +2253,9 @@ def postprocess_html(html_path: Path, include_root: Path, resource_root: Path):
|
|
|
2229
2253
|
target = (include_root / target_rel).resolve()
|
|
2230
2254
|
if target.exists():
|
|
2231
2255
|
frag_text = target.read_text()
|
|
2232
|
-
frag = lxml_html.fromstring(
|
|
2256
|
+
frag = lxml_html.fromstring(
|
|
2257
|
+
_normalize_observation_html(frag_text)
|
|
2258
|
+
)
|
|
2233
2259
|
body = frag.xpath("body")
|
|
2234
2260
|
if body:
|
|
2235
2261
|
elems = list(body[0])
|
|
@@ -2244,6 +2270,12 @@ def postprocess_html(html_path: Path, include_root: Path, resource_root: Path):
|
|
|
2244
2270
|
start_c = lxml_html.HtmlComment(f"BEGIN include {target_rel}")
|
|
2245
2271
|
parent.insert(idx, end_c)
|
|
2246
2272
|
for elem in reversed(elems):
|
|
2273
|
+
if isinstance(elem.tag, str):
|
|
2274
|
+
elem.set(
|
|
2275
|
+
"data-pydifft-source",
|
|
2276
|
+
elem.get("data-pydifft-source")
|
|
2277
|
+
or Path(target_rel).with_suffix(".qmd").as_posix(),
|
|
2278
|
+
)
|
|
2247
2279
|
parent.insert(idx, elem)
|
|
2248
2280
|
parent.insert(idx, start_c)
|
|
2249
2281
|
progress = True
|
|
@@ -2318,6 +2350,45 @@ def postprocess_html(html_path: Path, include_root: Path, resource_root: Path):
|
|
|
2318
2350
|
# Includes contribute their bodies, so their head scripts are lost.
|
|
2319
2351
|
# Install the handler once on the final page after expanding includes.
|
|
2320
2352
|
_ensure_rerun_script(root)
|
|
2353
|
+
# {{{ Style observations left raw when images split Pandoc inline lists
|
|
2354
|
+
for obs in root.xpath('//div[@class="pydifft-observation"]'):
|
|
2355
|
+
# Inline display preserves observations embedded in running prose.
|
|
2356
|
+
obs.set("style", obs.get("style", "") + ";display:inline;color:blue;")
|
|
2357
|
+
if not obs.xpath('./span[@class="pydifft-obs-prefix"]'):
|
|
2358
|
+
metadata = " ".join(filter(None, (
|
|
2359
|
+
obs.get("time") or obs.get("date"), obs.get("author")
|
|
2360
|
+
)))
|
|
2361
|
+
if metadata:
|
|
2362
|
+
prefix = lxml_html.Element("span")
|
|
2363
|
+
prefix.set("class", "pydifft-obs-prefix")
|
|
2364
|
+
prefix.set("style", "font-size:85%;color:green;")
|
|
2365
|
+
prefix.text = metadata + ":"
|
|
2366
|
+
prefix.tail = " " + (obs.text or "")
|
|
2367
|
+
obs.text = None
|
|
2368
|
+
obs.insert(0, prefix)
|
|
2369
|
+
# }}}
|
|
2370
|
+
# {{{ Reuse CPB's context menu on every final QMDB page
|
|
2371
|
+
head = root.find("head")
|
|
2372
|
+
if head is None:
|
|
2373
|
+
head = lxml_html.Element("head")
|
|
2374
|
+
root.insert(0, head)
|
|
2375
|
+
for old in root.xpath(
|
|
2376
|
+
'//script[@id="pydifft-source-jump-config" or '
|
|
2377
|
+
'@id="pydifft-source-jump"]'
|
|
2378
|
+
):
|
|
2379
|
+
old.getparent().remove(old)
|
|
2380
|
+
config = lxml_html.Element("script", id="pydifft-source-jump-config")
|
|
2381
|
+
config.text = (
|
|
2382
|
+
"window.pydifftSourceJumpEndpoint = "
|
|
2383
|
+
+ json.dumps(SOURCE_JUMP_ENDPOINT) + ";"
|
|
2384
|
+
)
|
|
2385
|
+
head.append(config)
|
|
2386
|
+
script = lxml_html.Element("script", id="pydifft-source-jump")
|
|
2387
|
+
script.text = (
|
|
2388
|
+
Path(__file__).parents[1] / "flowchart" / "source_jump.js"
|
|
2389
|
+
).read_text(encoding="utf-8")
|
|
2390
|
+
head.append(script)
|
|
2391
|
+
# }}}
|
|
2321
2392
|
html_path.write_text(lxml_html.tostring(root, encoding="unicode"))
|
|
2322
2393
|
|
|
2323
2394
|
|
|
@@ -2335,8 +2406,10 @@ def substitute_code_placeholders(
|
|
|
2335
2406
|
if code_display not in CODE_DISPLAY_MODES:
|
|
2336
2407
|
raise ValueError(f"unknown code display mode: {code_display}")
|
|
2337
2408
|
parser = lxml_html.HTMLParser(encoding="utf-8")
|
|
2338
|
-
|
|
2339
|
-
|
|
2409
|
+
root = lxml_html.document_fromstring(
|
|
2410
|
+
_normalize_observation_html(html_path.read_text()), parser=parser
|
|
2411
|
+
)
|
|
2412
|
+
tree = root.getroottree()
|
|
2340
2413
|
formatter = HtmlFormatter()
|
|
2341
2414
|
head = root.xpath("//head")
|
|
2342
2415
|
if head and not root.xpath('//style[@id="pygments-style"]'):
|
|
@@ -2379,7 +2452,9 @@ def substitute_code_placeholders(
|
|
|
2379
2452
|
frags = [details]
|
|
2380
2453
|
# }}}
|
|
2381
2454
|
if not missing_output and html:
|
|
2382
|
-
frags += lxml_html.fragments_fromstring(
|
|
2455
|
+
frags += lxml_html.fragments_fromstring(
|
|
2456
|
+
_normalize_observation_html(html)
|
|
2457
|
+
)
|
|
2383
2458
|
elif missing_output:
|
|
2384
2459
|
# Only show the placeholder when the notebook output entry is
|
|
2385
2460
|
# absent so executed cells that intentionally produce no output
|
|
@@ -2499,6 +2574,41 @@ def watch_and_serve(
|
|
|
2499
2574
|
super().__init__(*args, directory=str(DISPLAY_DIR), **kwargs)
|
|
2500
2575
|
|
|
2501
2576
|
def do_POST(self):
|
|
2577
|
+
if self.path == SOURCE_JUMP_ENDPOINT:
|
|
2578
|
+
# {{{ Dispatch the shared menu to the fragment's QMD source
|
|
2579
|
+
origin = self.headers.get("Origin")
|
|
2580
|
+
if origin and origin.split("://", 1)[-1] != self.headers.get(
|
|
2581
|
+
"Host"
|
|
2582
|
+
):
|
|
2583
|
+
self.send_error(403)
|
|
2584
|
+
return
|
|
2585
|
+
try:
|
|
2586
|
+
length = int(self.headers.get("Content-Length", "0"))
|
|
2587
|
+
if not 0 < length <= MAX_SOURCE_JUMP_BYTES:
|
|
2588
|
+
raise ValueError(length)
|
|
2589
|
+
request = json.loads(self.rfile.read(length))
|
|
2590
|
+
source = (PROJECT_ROOT / request["source"]).resolve()
|
|
2591
|
+
source.relative_to(PROJECT_ROOT.resolve())
|
|
2592
|
+
if source.suffix != ".qmd":
|
|
2593
|
+
raise ValueError("Expected a QMD source")
|
|
2594
|
+
phrase = request["phrase"]
|
|
2595
|
+
if not isinstance(phrase, str):
|
|
2596
|
+
raise ValueError("Expected source text")
|
|
2597
|
+
except (ValueError, KeyError, TypeError):
|
|
2598
|
+
self.send_error(400)
|
|
2599
|
+
return
|
|
2600
|
+
try:
|
|
2601
|
+
jump_to_source(source, phrase)
|
|
2602
|
+
except (
|
|
2603
|
+
OSError, subprocess.SubprocessError, ValueError
|
|
2604
|
+
) as exc:
|
|
2605
|
+
print(f"pydifft source jump failed: {exc}", flush=True)
|
|
2606
|
+
self.send_error(500)
|
|
2607
|
+
return
|
|
2608
|
+
self.send_response(204)
|
|
2609
|
+
self.end_headers()
|
|
2610
|
+
return
|
|
2611
|
+
# }}}
|
|
2502
2612
|
# {{{ Re-run the notebook behind a right-clicked SOURCE label
|
|
2503
2613
|
if self.path != RERUN_ENDPOINT:
|
|
2504
2614
|
self.send_error(404)
|
|
@@ -880,9 +880,14 @@ def wr(
|
|
|
880
880
|
fp.write(result)
|
|
881
881
|
|
|
882
882
|
|
|
883
|
-
def check_prose(
|
|
883
|
+
def check_prose(
|
|
884
|
+
content, wrapnumber, punctuation_slop, line_start=1, overflow_words=0,
|
|
885
|
+
):
|
|
884
886
|
"""Report source lines in `content` that break wr's rules.
|
|
885
887
|
|
|
888
|
+
Diff alignment can set overflow_words to tolerate that many words ending
|
|
889
|
+
beyond the character width. Ordinary wrapping checks use a strict limit.
|
|
890
|
+
|
|
886
891
|
Checks mirror wrap_prose's own paragraph/sentence/macro
|
|
887
892
|
splitting so the two commands never disagree about what counts as a
|
|
888
893
|
sentence: lines must not hold more words than wr's own greedy choice
|
|
@@ -953,7 +958,18 @@ def check_prose(content, wrapnumber, punctuation_slop, line_start=1):
|
|
|
953
958
|
+ len(words[actual_end])
|
|
954
959
|
- positions[fragment_offset]
|
|
955
960
|
)
|
|
956
|
-
|
|
961
|
+
# Diff alignment can keep a few extra words to
|
|
962
|
+
# avoid moving the sentence tail onto a new line.
|
|
963
|
+
# Count against the character limit, independently
|
|
964
|
+
# of an earlier preferred punctuation break.
|
|
965
|
+
excess = sum(
|
|
966
|
+
positions[j] + len(words[j])
|
|
967
|
+
- positions[fragment_offset] > wrapnumber
|
|
968
|
+
for j in range(fragment_offset, actual_end + 1)
|
|
969
|
+
)
|
|
970
|
+
if actual_end > wr_end and not (
|
|
971
|
+
overflow_words and 0 < excess <= overflow_words
|
|
972
|
+
):
|
|
957
973
|
overflow = words[wr_end + 1]
|
|
958
974
|
rule = (
|
|
959
975
|
"line too long"
|
|
@@ -1134,7 +1150,7 @@ def _mask_html_comments(content, initially_open=False):
|
|
|
1134
1150
|
|
|
1135
1151
|
|
|
1136
1152
|
def markdown_lint_issues_from_text(
|
|
1137
|
-
content, wrapnumber=45, punctuation_slop=20
|
|
1153
|
+
content, wrapnumber=45, punctuation_slop=20, overflow_words=0,
|
|
1138
1154
|
):
|
|
1139
1155
|
"""Return Markdown writing issues, including unfinished source spans."""
|
|
1140
1156
|
masked_content = _mask_html_comments(content)
|
|
@@ -1149,7 +1165,9 @@ def markdown_lint_issues_from_text(
|
|
|
1149
1165
|
for line, width, span in _iter_block_spans(
|
|
1150
1166
|
markdown_blocks(prose_content), wrapnumber
|
|
1151
1167
|
)
|
|
1152
|
-
for issue in check_prose(
|
|
1168
|
+
for issue in check_prose(
|
|
1169
|
+
span, width, punctuation_slop, line, overflow_words,
|
|
1170
|
+
)
|
|
1153
1171
|
]
|
|
1154
1172
|
issues.extend(
|
|
1155
1173
|
(issue["line"], issue["message"])
|
|
@@ -1523,7 +1541,8 @@ def autofix_markdown_file(
|
|
|
1523
1541
|
# issue at a time re-lints the whole file for every single fix.
|
|
1524
1542
|
for _ in range(100):
|
|
1525
1543
|
issues = markdown_lint_issues_from_text(
|
|
1526
|
-
content, wrapnumber=wrapnumber, punctuation_slop=punctuation_slop
|
|
1544
|
+
content, wrapnumber=wrapnumber, punctuation_slop=punctuation_slop,
|
|
1545
|
+
overflow_words=4 if baseline is not None else 0,
|
|
1527
1546
|
)
|
|
1528
1547
|
fixable = {}
|
|
1529
1548
|
for line_number, message in issues:
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import io
|
|
2
3
|
import queue
|
|
3
4
|
import socket
|
|
@@ -380,6 +381,7 @@ def test_second_qmdb_takes_next_ports_and_accepts_rebuild(
|
|
|
380
381
|
build_calls = []
|
|
381
382
|
http_ports = []
|
|
382
383
|
handlers = []
|
|
384
|
+
jumps = []
|
|
383
385
|
urls = []
|
|
384
386
|
httpd = FakeHttpServer()
|
|
385
387
|
|
|
@@ -424,6 +426,20 @@ def test_second_qmdb_takes_next_ports_and_accepts_rebuild(
|
|
|
424
426
|
handler.do_POST()
|
|
425
427
|
assert responses == [202]
|
|
426
428
|
assert built.wait(timeout=2)
|
|
429
|
+
handler.path = fast_build.SOURCE_JUMP_ENDPOINT
|
|
430
|
+
for source, status in [
|
|
431
|
+
("doc.qmd", 204), ("../outside.qmd", 400),
|
|
432
|
+
("doc.html", 400),
|
|
433
|
+
]:
|
|
434
|
+
body = json.dumps({
|
|
435
|
+
"source": source, "phrase": "Source text."
|
|
436
|
+
}).encode()
|
|
437
|
+
handler.headers["Content-Length"] = str(len(body))
|
|
438
|
+
handler.rfile = io.BytesIO(body)
|
|
439
|
+
handler.send_error = responses.append
|
|
440
|
+
handler.do_POST()
|
|
441
|
+
assert responses[-1] == status
|
|
442
|
+
assert jumps == [(tmp_path / "doc.qmd", "Source text.")]
|
|
427
443
|
return False
|
|
428
444
|
|
|
429
445
|
class Observer:
|
|
@@ -445,6 +461,10 @@ def test_second_qmdb_takes_next_ports_and_accepts_rebuild(
|
|
|
445
461
|
classmethod(lambda cls, **_k: Machine()),
|
|
446
462
|
)
|
|
447
463
|
monkeypatch.setattr(fast_build, "load_rendered_files", lambda: [])
|
|
464
|
+
monkeypatch.setattr(
|
|
465
|
+
fast_build, "jump_to_source",
|
|
466
|
+
lambda source, phrase: jumps.append((source, phrase)),
|
|
467
|
+
)
|
|
448
468
|
monkeypatch.setattr(fast_build, "ThreadingHTTPServer", fake_http_server)
|
|
449
469
|
monkeypatch.setattr(fast_build, "_serve_forever", lambda _httpd: None)
|
|
450
470
|
monkeypatch.setattr(fast_build, "BrowserReloader", Reloader)
|