pyDiffTools 0.1.55__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.
Files changed (130) hide show
  1. {pydifftools-0.1.55/pyDiffTools.egg-info → pydifftools-0.1.56}/PKG-INFO +1 -1
  2. {pydifftools-0.1.55 → pydifftools-0.1.56/pyDiffTools.egg-info}/PKG-INFO +1 -1
  3. {pydifftools-0.1.55 → pydifftools-0.1.56}/pyDiffTools.egg-info/SOURCES.txt +1 -0
  4. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/flowchart/source_jump.js +12 -3
  5. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/line_alignment.py +54 -27
  6. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/notebook/fast_build.py +115 -5
  7. {pydifftools-0.1.55 → pydifftools-0.1.56}/pyproject.toml +1 -1
  8. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_forward_search.py +20 -0
  9. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_line_alignment.py +55 -0
  10. pydifftools-0.1.56/tests/test_qt_lifecycle.py +51 -0
  11. {pydifftools-0.1.55 → pydifftools-0.1.56}/LICENSE.md +0 -0
  12. {pydifftools-0.1.55 → pydifftools-0.1.56}/MANIFEST.in +0 -0
  13. {pydifftools-0.1.55 → pydifftools-0.1.56}/README.rst +0 -0
  14. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/_quarto.yml +0 -0
  15. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  16. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  17. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  18. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  19. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  20. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  21. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  22. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  23. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  24. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  25. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  26. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  27. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  28. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  29. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  30. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  31. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  32. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  33. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  34. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  35. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  36. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  37. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/example.qmd +0 -0
  38. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/example.tex +0 -0
  39. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/index.qmd +0 -0
  40. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  41. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  42. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  43. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  44. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/subproject1/independent.qmd +0 -0
  45. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/subproject1/index.qmd +0 -0
  46. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  47. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  48. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  49. {pydifftools-0.1.55 → pydifftools-0.1.56}/example_notebook/project1/test_include.qmd +0 -0
  50. {pydifftools-0.1.55 → pydifftools-0.1.56}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  51. {pydifftools-0.1.55 → pydifftools-0.1.56}/pyDiffTools.egg-info/entry_points.txt +0 -0
  52. {pydifftools-0.1.55 → pydifftools-0.1.56}/pyDiffTools.egg-info/requires.txt +0 -0
  53. {pydifftools-0.1.55 → pydifftools-0.1.56}/pyDiffTools.egg-info/top_level.txt +0 -0
  54. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/__init__.py +0 -0
  55. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/bibliography.py +0 -0
  56. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/browser_lifecycle.py +0 -0
  57. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/check_numbers.py +0 -0
  58. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/citation_dialog.py +0 -0
  59. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/command_line.py +0 -0
  60. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/command_registry.py +0 -0
  61. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/comment_filter_history.json +0 -0
  62. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/comment_functions.py +0 -0
  63. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/comment_migration.py +0 -0
  64. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/comment_tags.lua +0 -0
  65. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/comment_tags_margin.lua +0 -0
  66. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/comment_tags_no_comments.lua +0 -0
  67. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/comment_toggle.js +0 -0
  68. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/comments.css +0 -0
  69. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/comments_author_colors.css +0 -0
  70. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/completions/git-mergein +0 -0
  71. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/continuous.py +0 -0
  72. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/copy_files.py +0 -0
  73. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/diff-doc.js +0 -0
  74. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/doc_contents.py +0 -0
  75. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/flowchart/__init__.py +0 -0
  76. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/flowchart/comparison.py +0 -0
  77. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  78. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/flowchart/graph.py +0 -0
  79. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  80. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/flowchart/watch_graph.py +0 -0
  81. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/flowchart/wgrph_view.js +0 -0
  82. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/forward_search.py +0 -0
  83. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/git_aliases.py +0 -0
  84. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/git_gd.py +0 -0
  85. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/git_gd_history.py +0 -0
  86. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/git_gd_image.py +0 -0
  87. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/git_gd_qt.py +0 -0
  88. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/html_comments.py +0 -0
  89. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/html_uncomments.py +0 -0
  90. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/log_example.py +0 -0
  91. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/match_spaces.py +0 -0
  92. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/notebook/__init__.py +0 -0
  93. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/notebook/display.py +0 -0
  94. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/notebook/tex_to_qmd.py +0 -0
  95. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/onewordify.py +0 -0
  96. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/onewordify_undo.py +0 -0
  97. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/outline.py +0 -0
  98. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/pdf_diff.py +0 -0
  99. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/rearrange_tex.py +0 -0
  100. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/searchacro.py +0 -0
  101. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/separate_comments.py +0 -0
  102. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/source_jump.py +0 -0
  103. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/split_conflict.py +0 -0
  104. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/unseparate_comments.py +0 -0
  105. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/update_check.py +0 -0
  106. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/wrap_sentences.py +0 -0
  107. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/wrapping_options.py +0 -0
  108. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/xml2xlsx.vbs +0 -0
  109. {pydifftools-0.1.55 → pydifftools-0.1.56}/pydifftools/zotero.py +0 -0
  110. {pydifftools-0.1.55 → pydifftools-0.1.56}/setup.cfg +0 -0
  111. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_bibliography.py +0 -0
  112. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_browser_lifecycle.py +0 -0
  113. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_check_single_use.py +0 -0
  114. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_command_line_help.py +0 -0
  115. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_comment_filters.py +0 -0
  116. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_comment_migration.py +0 -0
  117. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_continuous_shutdown.py +0 -0
  118. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_git_gd_history.py +0 -0
  119. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_git_gd_image.py +0 -0
  120. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_git_gd_qt.py +0 -0
  121. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_git_mergein.py +0 -0
  122. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_git_pdf_diff.py +0 -0
  123. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_pdf_diff.py +0 -0
  124. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_rrng.py +0 -0
  125. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_source_jump.py +0 -0
  126. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_tex_to_qmd.py +0 -0
  127. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_update_check.py +0 -0
  128. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_wrap_check.py +0 -0
  129. {pydifftools-0.1.55 → pydifftools-0.1.56}/tests/test_wrapping.py +0 -0
  130. {pydifftools-0.1.55 → 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.55
3
+ Version: 0.1.56
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.55
3
+ Version: 0.1.56
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -118,6 +118,7 @@ tests/test_git_mergein.py
118
118
  tests/test_git_pdf_diff.py
119
119
  tests/test_line_alignment.py
120
120
  tests/test_pdf_diff.py
121
+ tests/test_qt_lifecycle.py
121
122
  tests/test_rrng.py
122
123
  tests/test_source_jump.py
123
124
  tests/test_tex_to_qmd.py
@@ -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
  });
@@ -463,6 +463,58 @@ def align_line_breaks(
463
463
  old_rows = {}
464
464
  for number, (row, _, _) in enumerate(old_tokens):
465
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
+
466
518
  for number in gaps:
467
519
  old_number = matches[hunk].get(number)
468
520
  if old_number is None and number + 1 in matches[hunk]:
@@ -481,16 +533,9 @@ def align_line_breaks(
481
533
  if "\n" in raw and "\n" not in gaps[number] and (
482
534
  not inside_region
483
535
  ):
484
- original_rows = set(render(whitespace).splitlines())
485
536
  proposed = dict(whitespace)
486
537
  proposed[number] = raw
487
- inherited = {line.rstrip() for line in reference_lines}
488
- if any(
489
- line not in original_rows
490
- and line.rstrip() not in inherited
491
- and 0 < len(line.split()) <= 3
492
- for line in render(proposed).splitlines()
493
- ):
538
+ if creates_short_fragment(proposed, (number,)):
494
539
  continue
495
540
  if inside_region or (
496
541
  current_layout[3][j1 + tokens[number][0]]
@@ -502,21 +547,8 @@ def align_line_breaks(
502
547
  ending + continuation[tokens[number][0]]
503
548
  if "\n" in raw else raw
504
549
  )
505
- for row, (first, last) in old_rows.items():
506
- if (
507
- all(number in reverse for number in range(first, last + 1))
508
- and reverse[last] - reverse[first] == last - first
509
- ):
510
- spans.append(
511
- (
512
- reverse[first],
513
- reverse[last],
514
- reference_lines[i1 + row].rstrip(),
515
- )
516
- )
517
550
  # }}}
518
551
  # {{{ additional breaks preserve a line without tiny new fragments
519
- inherited = {line.rstrip() for line in reference_lines}
520
552
  for first, last, old_line in spans:
521
553
  needed = range(
522
554
  max(0, first - 1), min(len(tokens) - 1, last + 1)
@@ -550,12 +582,7 @@ def align_line_breaks(
550
582
  continue
551
583
  if old_line not in {line.rstrip() for line in proposed_rows}:
552
584
  continue
553
- if any(
554
- line not in original_rows
555
- and line.rstrip() not in inherited
556
- and 0 < len(line.split()) <= 3
557
- for line in proposed_rows
558
- ):
585
+ if creates_short_fragment(proposed, needed):
559
586
  continue
560
587
  whitespace = proposed
561
588
  # }}}
@@ -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(html_path.read_text())
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(frag_text)
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
- tree = lxml_html.parse(str(html_path), parser)
2339
- root = tree.getroot()
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(html)
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)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyDiffTools"
7
- version = "0.1.55"
7
+ version = "0.1.56"
8
8
  authors = [{ name = "J M Franck" }]
9
9
  description = "Diff tools"
10
10
  readme = "README.rst"
@@ -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)
@@ -524,6 +524,61 @@ def test_optional_preservation_break_does_not_strand_a_tiny_fragment():
524
524
  assert result["aligned"] == current
525
525
 
526
526
 
527
+ @pytest.mark.parametrize("comma", ["", ","])
528
+ @pytest.mark.parametrize("location", ["elsewhere", "same_hunk"])
529
+ def test_repeated_short_line_does_not_exempt_an_inserted_fragment(
530
+ comma, location,
531
+ ):
532
+ citation = "(@fig:TEMPOOH_vs_TEMPOSO4(b))"
533
+ if location == "elsewhere":
534
+ old_prefix = new_prefix = "which\n\nUnchanged anchor.\n\n"
535
+ else:
536
+ old_prefix = "which\noriginal opening words\n"
537
+ new_prefix = "which changed opening words\n"
538
+ reference = (
539
+ old_prefix + citation + comma + "\nlikely arises from larger\n"
540
+ )
541
+ current = (
542
+ new_prefix + citation + ",\nwhich likely arises from larger\n"
543
+ )
544
+ results = align_line_breaks(reference, current, width=55)
545
+ for result in reversed(results):
546
+ current = (
547
+ current[:result["start"]] + result["aligned"]
548
+ + current[result["stop"]:]
549
+ )
550
+ assert citation + ",\nwhich likely arises from larger\n" in current
551
+
552
+
553
+ def test_matched_short_line_can_still_be_restored():
554
+ reference = (
555
+ "Former opening has four words\nwhich\n"
556
+ "following stable words remain here\n"
557
+ )
558
+ current = (
559
+ "Changed opening has four words which "
560
+ "following stable words remain here\n"
561
+ )
562
+ (result,) = align_line_breaks(reference, current)
563
+ assert result["aligned"] == reference.replace("Former", "Changed")
564
+
565
+
566
+ @pytest.mark.parametrize("fragment,restore", [
567
+ (r"$\alpha\beta\gamma$", False),
568
+ ("link){#one two three four}", True),
569
+ ])
570
+ def test_fragment_word_count_handles_math_and_attribute_tokens(
571
+ fragment, restore,
572
+ ):
573
+ reference = "Former opening\nstable reference words\nformer closing.\n"
574
+ current = (
575
+ fragment
576
+ + " stable reference words rewritten closing has four words.\n"
577
+ )
578
+ (result,) = align_line_breaks(reference, current)
579
+ assert ("\nstable reference words\n" in result["aligned"]) == restore
580
+
581
+
527
582
  @pytest.mark.parametrize("count", [1, 2, 3, 4])
528
583
  @pytest.mark.parametrize("side", ["before", "after"])
529
584
  def test_optional_break_uses_word_count_for_long_fragments(count, side):
@@ -0,0 +1,51 @@
1
+ from pathlib import Path
2
+ import subprocess
3
+ import sys
4
+
5
+
6
+ def test_qt_windows_are_deleted_before_application_shutdown(tmp_path):
7
+ conftest = Path(__file__).with_name("conftest.py").read_text()
8
+ (tmp_path / "conftest.py").write_text(
9
+ conftest
10
+ + """
11
+
12
+ def pytest_sessionfinish(session, exitstatus):
13
+ from PySide6.QtWidgets import QApplication
14
+ assert QApplication.instance() is None
15
+ """
16
+ )
17
+ (tmp_path / "test_windows.py").write_text(
18
+ """
19
+ from PySide6.QtWidgets import QApplication, QPushButton
20
+ from shiboken6 import isValid
21
+
22
+ retained_windows = []
23
+ retained_applications = []
24
+
25
+
26
+ def test_closed_window():
27
+ application = QApplication.instance() or QApplication([])
28
+ retained_applications.append(application)
29
+ window = QPushButton("Close")
30
+ # Keep both a Python reference and a signal closure alive past the test.
31
+ window.clicked.connect(lambda: window.close())
32
+ window.close()
33
+ retained_windows.append(window)
34
+ assert isValid(application)
35
+
36
+
37
+ def test_previous_window_is_deleted():
38
+ assert not isValid(retained_windows[0])
39
+ assert QApplication.instance() is retained_applications[0]
40
+ assert isValid(retained_applications[0])
41
+ """
42
+ )
43
+ completed = subprocess.run(
44
+ [sys.executable, "-m", "pytest", "-q", str(tmp_path)],
45
+ cwd=tmp_path,
46
+ capture_output=True,
47
+ text=True,
48
+ timeout=60,
49
+ )
50
+ assert completed.returncode == 0, completed.stdout + completed.stderr
51
+ assert "2 passed" in completed.stdout
File without changes
File without changes
File without changes
File without changes