pyDiffTools 0.1.49__tar.gz → 0.1.50__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 (122) hide show
  1. {pydifftools-0.1.49/pyDiffTools.egg-info → pydifftools-0.1.50}/PKG-INFO +16 -5
  2. {pydifftools-0.1.49 → pydifftools-0.1.50}/README.rst +15 -4
  3. {pydifftools-0.1.49 → pydifftools-0.1.50/pyDiffTools.egg-info}/PKG-INFO +16 -5
  4. {pydifftools-0.1.49 → pydifftools-0.1.50}/pyDiffTools.egg-info/SOURCES.txt +3 -0
  5. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/browser_lifecycle.py +30 -1
  6. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/command_line.py +35 -2
  7. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/command_registry.py +7 -0
  8. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/comment_tags.lua +10 -3
  9. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/comment_tags_margin.lua +10 -3
  10. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/comment_tags_no_comments.lua +10 -3
  11. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/continuous.py +127 -29
  12. pydifftools-0.1.50/pydifftools/line_alignment.py +391 -0
  13. pydifftools-0.1.50/pydifftools/match_spaces.py +142 -0
  14. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/wrap_sentences.py +80 -340
  15. pydifftools-0.1.50/pydifftools/wrapping_options.py +18 -0
  16. {pydifftools-0.1.49 → pydifftools-0.1.50}/pyproject.toml +1 -1
  17. {pydifftools-0.1.49 → pydifftools-0.1.50}/tests/test_browser_lifecycle.py +56 -0
  18. {pydifftools-0.1.49 → pydifftools-0.1.50}/tests/test_command_line_help.py +126 -0
  19. {pydifftools-0.1.49 → pydifftools-0.1.50}/tests/test_continuous_shutdown.py +47 -17
  20. pydifftools-0.1.50/tests/test_line_alignment.py +131 -0
  21. {pydifftools-0.1.49 → pydifftools-0.1.50}/tests/test_wrap_check.py +305 -23
  22. {pydifftools-0.1.49 → pydifftools-0.1.50}/tests/test_wrapping.py +18 -2
  23. {pydifftools-0.1.49 → pydifftools-0.1.50}/tests/test_zotero.py +1 -1
  24. pydifftools-0.1.49/pydifftools/match_spaces.py +0 -380
  25. {pydifftools-0.1.49 → pydifftools-0.1.50}/LICENSE.md +0 -0
  26. {pydifftools-0.1.49 → pydifftools-0.1.50}/MANIFEST.in +0 -0
  27. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/_quarto.yml +0 -0
  28. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  29. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  30. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  31. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  32. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  33. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  34. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  35. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  36. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  37. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  38. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  39. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  40. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  41. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  42. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  43. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  44. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  45. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  46. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  47. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  48. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  49. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  50. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/example.qmd +0 -0
  51. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/example.tex +0 -0
  52. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/index.qmd +0 -0
  53. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  54. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  55. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  56. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  57. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/subproject1/independent.qmd +0 -0
  58. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/subproject1/index.qmd +0 -0
  59. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  60. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  61. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  62. {pydifftools-0.1.49 → pydifftools-0.1.50}/example_notebook/project1/test_include.qmd +0 -0
  63. {pydifftools-0.1.49 → pydifftools-0.1.50}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  64. {pydifftools-0.1.49 → pydifftools-0.1.50}/pyDiffTools.egg-info/entry_points.txt +0 -0
  65. {pydifftools-0.1.49 → pydifftools-0.1.50}/pyDiffTools.egg-info/requires.txt +0 -0
  66. {pydifftools-0.1.49 → pydifftools-0.1.50}/pyDiffTools.egg-info/top_level.txt +0 -0
  67. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/__init__.py +0 -0
  68. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/bibliography.py +0 -0
  69. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/check_numbers.py +0 -0
  70. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/citation_dialog.py +0 -0
  71. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/comment_functions.py +0 -0
  72. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/comment_migration.py +0 -0
  73. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/comment_toggle.js +0 -0
  74. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/comments.css +0 -0
  75. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/comments_author_colors.css +0 -0
  76. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/copy_files.py +0 -0
  77. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/diff-doc.js +0 -0
  78. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/doc_contents.py +0 -0
  79. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/flowchart/__init__.py +0 -0
  80. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/flowchart/comparison.py +0 -0
  81. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  82. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/flowchart/graph.py +0 -0
  83. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/flowchart/source_jump.js +0 -0
  84. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  85. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/flowchart/watch_graph.py +0 -0
  86. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/flowchart/wgrph_view.js +0 -0
  87. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/forward_search.py +0 -0
  88. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/git_gd.py +0 -0
  89. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/git_gd_history.py +0 -0
  90. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/git_gd_image.py +0 -0
  91. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/git_gd_qt.py +0 -0
  92. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/html_comments.py +0 -0
  93. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/html_uncomments.py +0 -0
  94. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/log_example.py +0 -0
  95. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/notebook/__init__.py +0 -0
  96. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/notebook/display.py +0 -0
  97. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/notebook/fast_build.py +0 -0
  98. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/notebook/tex_to_qmd.py +0 -0
  99. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/onewordify.py +0 -0
  100. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/onewordify_undo.py +0 -0
  101. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/outline.py +0 -0
  102. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/rearrange_tex.py +0 -0
  103. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/searchacro.py +0 -0
  104. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/separate_comments.py +0 -0
  105. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/source_jump.py +0 -0
  106. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/split_conflict.py +0 -0
  107. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/unseparate_comments.py +0 -0
  108. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/update_check.py +0 -0
  109. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/xml2xlsx.vbs +0 -0
  110. {pydifftools-0.1.49 → pydifftools-0.1.50}/pydifftools/zotero.py +0 -0
  111. {pydifftools-0.1.49 → pydifftools-0.1.50}/setup.cfg +0 -0
  112. {pydifftools-0.1.49 → pydifftools-0.1.50}/tests/test_bibliography.py +0 -0
  113. {pydifftools-0.1.49 → pydifftools-0.1.50}/tests/test_check_single_use.py +0 -0
  114. {pydifftools-0.1.49 → pydifftools-0.1.50}/tests/test_comment_migration.py +0 -0
  115. {pydifftools-0.1.49 → pydifftools-0.1.50}/tests/test_forward_search.py +0 -0
  116. {pydifftools-0.1.49 → pydifftools-0.1.50}/tests/test_git_gd_history.py +0 -0
  117. {pydifftools-0.1.49 → pydifftools-0.1.50}/tests/test_git_gd_image.py +0 -0
  118. {pydifftools-0.1.49 → pydifftools-0.1.50}/tests/test_git_gd_qt.py +0 -0
  119. {pydifftools-0.1.49 → pydifftools-0.1.50}/tests/test_rrng.py +0 -0
  120. {pydifftools-0.1.49 → pydifftools-0.1.50}/tests/test_source_jump.py +0 -0
  121. {pydifftools-0.1.49 → pydifftools-0.1.50}/tests/test_tex_to_qmd.py +0 -0
  122. {pydifftools-0.1.49 → pydifftools-0.1.50}/tests/test_update_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.49
3
+ Version: 0.1.50
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -215,10 +215,21 @@ included are (listed in order of fun/utility):
215
215
 
216
216
  * this allows you to use an original file with good whitespace formatting as a "template" that you can match other (e.g. pandoc converted file) onto another
217
217
 
218
- * changed prose lines exceeding 1.5 times the nearby reference width are
219
- wrapped using the same sentence and punctuation rules as ``wr``.
220
- Existing surrounding line breaks are retained; the fallback width is
221
- 80 columns when the reference provides too little prose.
218
+ * ``cpb`` and ``wmatch`` share a matcher that finds changed hunks, aligns
219
+ their words using Git's minimal diff, and restores reference whitespace.
220
+ It then considers extra breaks that preserve an unchanged reference line
221
+ without stranding a tiny edited fragment, and checks wrapping rules.
222
+
223
+ * ``--wrapnumber`` sets the maximum width (79 for ``cpb`` and ``wmatch``).
224
+ Wmatch also uses nearby reference widths for large edits, retaining
225
+ unchanged reference lines and protected Markdown blocks.
226
+
227
+ * ``--trailing-dependent-phrase`` defaults to 20: a clause boundary within
228
+ 20 characters of the maximum width must end the line, even when its
229
+ trailing phrase would still fit. Commas, semicolons, colons, parentheses,
230
+ dashes, and inline-math boundaries are supported. Set it to 0 to disable
231
+ this rule. The option is shared by ``cpb``, ``wmatch``, ``wr`` and
232
+ ``wrchk``; ``--punctuation-slop`` remains an alias.
222
233
 
223
234
  - `pydifft wd` ("word diff"): generate "track changes" word files starting from pandoc markdown in a git history. Assuming that you have copied diff-doc.js (copied + licensed from elsewhere) into your home directory, this will use pandoc to convert the markdown files to MS Word, then use the MS Word comparison tool to generate a document where all relevant changes are shown with "track changes."
224
235
 
@@ -160,10 +160,21 @@ included are (listed in order of fun/utility):
160
160
 
161
161
  * this allows you to use an original file with good whitespace formatting as a "template" that you can match other (e.g. pandoc converted file) onto another
162
162
 
163
- * changed prose lines exceeding 1.5 times the nearby reference width are
164
- wrapped using the same sentence and punctuation rules as ``wr``.
165
- Existing surrounding line breaks are retained; the fallback width is
166
- 80 columns when the reference provides too little prose.
163
+ * ``cpb`` and ``wmatch`` share a matcher that finds changed hunks, aligns
164
+ their words using Git's minimal diff, and restores reference whitespace.
165
+ It then considers extra breaks that preserve an unchanged reference line
166
+ without stranding a tiny edited fragment, and checks wrapping rules.
167
+
168
+ * ``--wrapnumber`` sets the maximum width (79 for ``cpb`` and ``wmatch``).
169
+ Wmatch also uses nearby reference widths for large edits, retaining
170
+ unchanged reference lines and protected Markdown blocks.
171
+
172
+ * ``--trailing-dependent-phrase`` defaults to 20: a clause boundary within
173
+ 20 characters of the maximum width must end the line, even when its
174
+ trailing phrase would still fit. Commas, semicolons, colons, parentheses,
175
+ dashes, and inline-math boundaries are supported. Set it to 0 to disable
176
+ this rule. The option is shared by ``cpb``, ``wmatch``, ``wr`` and
177
+ ``wrchk``; ``--punctuation-slop`` remains an alias.
167
178
 
168
179
  - `pydifft wd` ("word diff"): generate "track changes" word files starting from pandoc markdown in a git history. Assuming that you have copied diff-doc.js (copied + licensed from elsewhere) into your home directory, this will use pandoc to convert the markdown files to MS Word, then use the MS Word comparison tool to generate a document where all relevant changes are shown with "track changes."
169
180
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.49
3
+ Version: 0.1.50
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -215,10 +215,21 @@ included are (listed in order of fun/utility):
215
215
 
216
216
  * this allows you to use an original file with good whitespace formatting as a "template" that you can match other (e.g. pandoc converted file) onto another
217
217
 
218
- * changed prose lines exceeding 1.5 times the nearby reference width are
219
- wrapped using the same sentence and punctuation rules as ``wr``.
220
- Existing surrounding line breaks are retained; the fallback width is
221
- 80 columns when the reference provides too little prose.
218
+ * ``cpb`` and ``wmatch`` share a matcher that finds changed hunks, aligns
219
+ their words using Git's minimal diff, and restores reference whitespace.
220
+ It then considers extra breaks that preserve an unchanged reference line
221
+ without stranding a tiny edited fragment, and checks wrapping rules.
222
+
223
+ * ``--wrapnumber`` sets the maximum width (79 for ``cpb`` and ``wmatch``).
224
+ Wmatch also uses nearby reference widths for large edits, retaining
225
+ unchanged reference lines and protected Markdown blocks.
226
+
227
+ * ``--trailing-dependent-phrase`` defaults to 20: a clause boundary within
228
+ 20 characters of the maximum width must end the line, even when its
229
+ trailing phrase would still fit. Commas, semicolons, colons, parentheses,
230
+ dashes, and inline-math boundaries are supported. Set it to 0 to disable
231
+ this rule. The option is shared by ``cpb``, ``wmatch``, ``wr`` and
232
+ ``wrchk``; ``--punctuation-slop`` remains an alias.
222
233
 
223
234
  - `pydifft wd` ("word diff"): generate "track changes" word files starting from pandoc markdown in a git history. Assuming that you have copied diff-doc.js (copied + licensed from elsewhere) into your home directory, this will use pandoc to convert the markdown files to MS Word, then use the MS Word comparison tool to generate a document where all relevant changes are shown with "track changes."
224
235
 
@@ -70,6 +70,7 @@ pydifftools/git_gd_image.py
70
70
  pydifftools/git_gd_qt.py
71
71
  pydifftools/html_comments.py
72
72
  pydifftools/html_uncomments.py
73
+ pydifftools/line_alignment.py
73
74
  pydifftools/log_example.py
74
75
  pydifftools/match_spaces.py
75
76
  pydifftools/onewordify.py
@@ -83,6 +84,7 @@ pydifftools/split_conflict.py
83
84
  pydifftools/unseparate_comments.py
84
85
  pydifftools/update_check.py
85
86
  pydifftools/wrap_sentences.py
87
+ pydifftools/wrapping_options.py
86
88
  pydifftools/xml2xlsx.vbs
87
89
  pydifftools/zotero.py
88
90
  pydifftools/flowchart/__init__.py
@@ -107,6 +109,7 @@ tests/test_forward_search.py
107
109
  tests/test_git_gd_history.py
108
110
  tests/test_git_gd_image.py
109
111
  tests/test_git_gd_qt.py
112
+ tests/test_line_alignment.py
110
113
  tests/test_rrng.py
111
114
  tests/test_source_jump.py
112
115
  tests/test_tex_to_qmd.py
@@ -33,11 +33,40 @@ def launch_chrome(webdriver, **kwargs):
33
33
  "do not match; retrying with the driver version check disabled.",
34
34
  file=sys.stderr,
35
35
  )
36
- kwargs["service"] = Service(service_args=["--disable-build-check"])
36
+ service = kwargs.get("service")
37
+ kwargs["service"] = Service(
38
+ executable_path=service.path if service else None,
39
+ service_args=[
40
+ *(service.service_args if service else []),
41
+ "--disable-build-check",
42
+ ],
43
+ )
37
44
  return webdriver.Chrome(**kwargs)
38
45
  # }}}
39
46
 
40
47
 
48
+ def start_chrome():
49
+ """Use an installed driver without an online Selenium Manager lookup."""
50
+ # Keep Selenium imports lazy, as in the watch commands.
51
+ from selenium import webdriver
52
+ from selenium.webdriver.chrome.service import Service
53
+
54
+ driver_path = shutil.which("chromedriver")
55
+ if driver_path:
56
+ options = webdriver.ChromeOptions()
57
+ # Debian's driver defaults to Chromium even if Chrome is installed;
58
+ # retain Selenium Manager's preference for Google Chrome.
59
+ browser_path = shutil.which("google-chrome") or shutil.which(
60
+ "google-chrome-stable"
61
+ )
62
+ if browser_path:
63
+ options.binary_location = browser_path
64
+ return launch_chrome(
65
+ webdriver, service=Service(driver_path), options=options
66
+ )
67
+ return launch_chrome(webdriver)
68
+
69
+
41
70
  def prepare_for_dialog():
42
71
  """Let watch commands close their preview before a dialog appears."""
43
72
  for callback in list(dialog_callbacks):
@@ -11,6 +11,7 @@ import nbformat
11
11
  import difflib
12
12
  import shutil
13
13
  import importlib.util
14
+ import importlib.metadata
14
15
  from pathlib import Path
15
16
  from . import (
16
17
  match_spaces,
@@ -46,6 +47,9 @@ from .notebook.fast_build import (
46
47
  )
47
48
 
48
49
  from .command_registry import _COMMAND_SPECS, register_command
50
+ from .wrapping_options import (
51
+ DEFAULT_WIDTH, DEFAULT_TRAILING_DEPENDENT_PHRASE, WRAPPING_ARGUMENTS,
52
+ )
49
53
 
50
54
  _ARGCOMPLETE_SPEC = importlib.util.find_spec("argcomplete")
51
55
  if (
@@ -366,9 +370,19 @@ def gvr(arguments):
366
370
  @register_command(
367
371
  "match whitespace",
368
372
  filename_extensions={"arguments": [".md", ".tex"]},
373
+ argument_options={
374
+ **WRAPPING_ARGUMENTS,
375
+ "arguments": {
376
+ "nargs": 2, "metavar": ("REFERENCE", "FILE"),
377
+ "help": "Reference file and current file to reflow.",
378
+ },
379
+ },
369
380
  )
370
- def wmatch(arguments):
371
- match_spaces.run(arguments)
381
+ def wmatch(
382
+ arguments, wrapnumber=DEFAULT_WIDTH,
383
+ trailing_dependent_phrase=DEFAULT_TRAILING_DEPENDENT_PHRASE,
384
+ ):
385
+ match_spaces.run(arguments, wrapnumber, trailing_dependent_phrase)
372
386
 
373
387
 
374
388
  @register_command("split conflict")
@@ -879,6 +893,12 @@ def build_parser():
879
893
  formatter_class=argparse.RawDescriptionHelpFormatter,
880
894
  is_root_parser=True,
881
895
  )
896
+ parser.add_argument(
897
+ "--ver",
898
+ "--version",
899
+ action="version",
900
+ version=importlib.metadata.version("pyDiffTools"),
901
+ )
882
902
  parser.epilog = _subcommand_help_hint(parser.prog)
883
903
  parser._pydifft_subparsers = {}
884
904
  subparsers = parser.add_subparsers(dest="command")
@@ -913,6 +933,9 @@ def build_parser():
913
933
  def main(argv=None):
914
934
  if argv is None:
915
935
  argv = sys.argv[1:]
936
+ if argv and argv[0] in ("--ver", "--version"):
937
+ build_parser().parse_args(argv)
938
+ return
916
939
  # Run the PyPI update check once per UTC day so users see a notice but
917
940
  # startup stays fast when offline. The date is stored in the
918
941
  # PYDIFFTOOLS_UPDATE_CHECK_LAST_RAN_UTC_DATE environment variable.
@@ -959,6 +982,16 @@ def main(argv=None):
959
982
  parser._pydifft_subparsers[subcommand].print_help()
960
983
  return
961
984
  namespace = parser.parse_args(argv)
985
+ # Shared wrapping arguments are validated identically for each command.
986
+ if hasattr(namespace, "wrapnumber") and namespace.wrapnumber < 1:
987
+ parser._pydifft_subparsers[namespace.command].error(
988
+ "--wrapnumber must be at least 1"
989
+ )
990
+ if getattr(namespace, "trailing_dependent_phrase",
991
+ getattr(namespace, "punctuation_slop", 0)) < 0:
992
+ parser._pydifft_subparsers[namespace.command].error(
993
+ "--trailing-dependent-phrase must be nonnegative"
994
+ )
962
995
  if (
963
996
  namespace.command == "cpb"
964
997
  and namespace.comments_to_margin
@@ -15,6 +15,7 @@ def register_command(
15
15
  description=None,
16
16
  help=None,
17
17
  filename_extensions=None,
18
+ argument_options=None,
18
19
  ):
19
20
  """Register a command handler for the CLI dispatcher."""
20
21
 
@@ -108,6 +109,12 @@ def register_command(
108
109
  kwargs["type"] = type(parameter.default)
109
110
  if parameter.name in argument_help:
110
111
  kwargs["help"] = argument_help[parameter.name].strip()
112
+ if argument_options is not None:
113
+ kwargs.update(argument_options.get(parameter.name, {}))
114
+ override_flags = kwargs.pop("flags", None)
115
+ if override_flags is not None:
116
+ flags = override_flags
117
+ kwargs["dest"] = parameter.name
111
118
  argument_spec = {"flags": flags, "kwargs": kwargs}
112
119
  if parameter.name in completion_allowednames:
113
120
  argument_spec["completion_allowednames"] = (
@@ -237,9 +237,13 @@ local function detect_block_opener(block)
237
237
  end
238
238
 
239
239
  if para_like_t(block) then
240
- for k, opener_spec in pairs(OPENERS) do
241
- local found, before, after = split_inline_block_at_tag(block, k)
242
- if found then
240
+ -- Scan the paragraph once, regardless of the number of authors.
241
+ local content = block.content
242
+ for index = 1, #content do
243
+ local tag = raw_inline_html(content[index])
244
+ local opener_spec = tag and OPENERS[tag] or nil
245
+ if opener_spec then
246
+ local _, before, after = split_inline_block_at_tag(block, tag)
243
247
  return opener_spec, before, after
244
248
  end
245
249
  end
@@ -452,3 +456,6 @@ function Pandoc(doc)
452
456
  end
453
457
  return doc:walk({ Inlines = Inlines, Blocks = Blocks, Div = Div })
454
458
  end
459
+
460
+ -- Initialize authors before walking comments, without an implicit first walk.
461
+ return {{ Pandoc = Pandoc }}
@@ -273,9 +273,13 @@ local function detect_block_opener(block)
273
273
  end
274
274
 
275
275
  if para_like_t(block) then
276
- for k, opener_spec in pairs(OPENERS) do
277
- local found, before, after = split_inline_block_at_tag(block, k)
278
- if found then
276
+ -- Scan the paragraph once, regardless of the number of authors.
277
+ local content = block.content
278
+ for index = 1, #content do
279
+ local tag = raw_inline_html(content[index])
280
+ local opener_spec = tag and OPENERS[tag] or nil
281
+ if opener_spec then
282
+ local _, before, after = split_inline_block_at_tag(block, tag)
279
283
  return opener_spec, before, after
280
284
  end
281
285
  end
@@ -487,3 +491,6 @@ function Pandoc(doc)
487
491
  end
488
492
  return doc:walk({ Inlines = Inlines, Blocks = Blocks, Div = Div })
489
493
  end
494
+
495
+ -- Initialize authors before walking comments, without an implicit first walk.
496
+ return {{ Pandoc = Pandoc }}
@@ -166,9 +166,13 @@ local function detect_block_opener(block)
166
166
  end
167
167
 
168
168
  if para_like_t(block) then
169
- for k, opener_spec in pairs(OPENERS) do
170
- local found, before, after = split_inline_block_at_tag(block, k)
171
- if found then
169
+ -- Scan the paragraph once, regardless of the number of authors.
170
+ local content = block.content
171
+ for index = 1, #content do
172
+ local tag = raw_inline_html(content[index])
173
+ local opener_spec = tag and OPENERS[tag] or nil
174
+ if opener_spec then
175
+ local _, before, after = split_inline_block_at_tag(block, tag)
172
176
  return opener_spec, before, after
173
177
  end
174
178
  end
@@ -348,3 +352,6 @@ function Pandoc(doc)
348
352
  end
349
353
  return doc:walk({ Inlines = Inlines, Blocks = Blocks, Div = Div })
350
354
  end
355
+
356
+ -- Initialize authors before walking comments, without an implicit first walk.
357
+ return {{ Pandoc = Pandoc }}
@@ -18,6 +18,9 @@ from watchdog.events import FileSystemEventHandler
18
18
  from watchdog.observers import Observer
19
19
  from watchdog.observers.polling import PollingObserver
20
20
  from .command_registry import register_command
21
+ from .wrapping_options import (
22
+ DEFAULT_WIDTH, DEFAULT_TRAILING_DEPENDENT_PHRASE, WRAPPING_ARGUMENTS,
23
+ )
21
24
  from .zotero import recover_bibliography, zotero_notice
22
25
  from .comment_migration import prepare_comment_source
23
26
  from .browser_lifecycle import (
@@ -25,8 +28,8 @@ from .browser_lifecycle import (
25
28
  close_browser_window,
26
29
  dialog_callbacks,
27
30
  forward_search_in_browser,
28
- launch_chrome,
29
31
  prepare_for_dialog,
32
+ start_chrome,
30
33
  )
31
34
  from .forward_search import (
32
35
  CPB_FORWARD_SEARCH_PORT,
@@ -170,6 +173,54 @@ def show_markdown_fix_dialog(report):
170
173
  dialog._pydifftools_application = application
171
174
  dialog.setWindowTitle("Markdown source fixes")
172
175
  fixed_font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
176
+
177
+ def annotated_source(before, after, line):
178
+ """Mark changed breaks on their positions in the original source."""
179
+ original_words = list(re.finditer(r"\S+", before))
180
+ updated_words = list(re.finditer(r"\S+", after))
181
+ if [word[0] for word in original_words] != [
182
+ word[0] for word in updated_words
183
+ ]:
184
+ return None
185
+ marks = {}
186
+ for number in range(len(original_words) + 1):
187
+ start = original_words[number - 1].end() if number else 0
188
+ stop = (
189
+ original_words[number].start()
190
+ if number < len(original_words)
191
+ else len(before)
192
+ )
193
+ updated_start = updated_words[number - 1].end() if number else 0
194
+ updated_stop = (
195
+ updated_words[number].start()
196
+ if number < len(updated_words)
197
+ else len(after)
198
+ )
199
+ old_breaks = [
200
+ start + match.start()
201
+ for match in re.finditer("\n", before[start:stop])
202
+ ]
203
+ new_count = after[updated_start:updated_stop].count("\n")
204
+ if new_count > len(old_breaks):
205
+ marks[start] = (
206
+ "<span style='color:#188038; font-weight:bold'>↳</span>"
207
+ * (new_count - len(old_breaks))
208
+ )
209
+ for position in old_breaks[new_count:]:
210
+ marks[position] = (
211
+ "<span style='color:#c62828; font-weight:bold'>↳×</span>"
212
+ )
213
+ if not marks:
214
+ return None
215
+ marked = "".join(
216
+ marks.get(position, "") + escape(char)
217
+ for position, char in enumerate(before)
218
+ ) + marks.get(len(before), "")
219
+ return "<br>".join(
220
+ f"<span style='color:#666'>{line + offset:>6}</span> " + value
221
+ for offset, value in enumerate(marked.split("\n"))
222
+ )
223
+
173
224
  # {{{ group fixes of one kind onto pages that fill the screen
174
225
  screen_height = dialog.screen().availableGeometry().height()
175
226
  dialog.resize(900, int(screen_height * 0.85))
@@ -189,13 +240,15 @@ def show_markdown_fix_dialog(report):
189
240
  page = []
190
241
  rows = 0
191
242
  for fix in sorted(group, key=lambda fix: fix["line"]):
192
- # a blank line separates each fix from the one before it, and a
193
- # fix that only drops trailing spaces shows just its old line
243
+ # A break annotation needs only the original source lines.
194
244
  fix_rows = (
195
245
  len(fix["before"].splitlines() or [""])
196
246
  + (
197
247
  0
198
248
  if fix["after"] == fix["before"].rstrip()
249
+ or annotated_source(
250
+ fix["before"], fix["after"], fix["line"]
251
+ ) is not None
199
252
  else len(fix["after"].splitlines() or [""])
200
253
  )
201
254
  + (1 if page else 0)
@@ -235,6 +288,7 @@ def show_markdown_fix_dialog(report):
235
288
  preview = QTextEdit()
236
289
  preview.setReadOnly(True)
237
290
  preview.setFont(fixed_font)
291
+ preview.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)
238
292
  preview_row.addWidget(line_numbers)
239
293
  preview_row.addWidget(preview, 1)
240
294
  layout.addLayout(preview_row, 1)
@@ -261,9 +315,17 @@ def show_markdown_fix_dialog(report):
261
315
  )
262
316
  details.setText(record["reason"])
263
317
  line_numbers.setVisible(False)
264
- # {{{ show each fix as numbered before (−) and after (+) lines
318
+ # {{{ annotate changed breaks on the original source
265
319
  entries = []
320
+ has_break_annotations = False
266
321
  for fix in record["fixes"]:
322
+ annotated = annotated_source(
323
+ fix["before"], fix["after"], fix["line"]
324
+ )
325
+ if annotated is not None:
326
+ entries.append(annotated)
327
+ has_break_annotations = True
328
+ continue
267
329
  rows = []
268
330
  for offset, value in enumerate(
269
331
  fix["before"].splitlines() or [""]
@@ -289,6 +351,13 @@ def show_markdown_fix_dialog(report):
289
351
  + escape(value)
290
352
  )
291
353
  entries.append("<br>".join(rows))
354
+ if has_break_annotations:
355
+ details.setText(
356
+ record["reason"]
357
+ + "<br>Original source: "
358
+ "<span style='color:#188038'>↳</span> inserted newline; "
359
+ "<span style='color:#c62828'>↳×</span> removed newline."
360
+ )
292
361
  preview.setHtml(
293
362
  "<pre style='font-family:monospace; margin:0'>"
294
363
  + "<br><br>".join(entries)
@@ -308,21 +377,33 @@ def show_markdown_fix_dialog(report):
308
377
  "lay them out as suggested."
309
378
  )
310
379
  line_numbers.setVisible(False)
311
- # {{{ show HEAD, the current source and the suggested layout
380
+ # {{{ show suggested breaks on the original source
312
381
  sections = []
313
- for title, text, numbered in (
314
- ("git HEAD:", record["head"], False),
315
- ("Now:", record["current"], True),
316
- ("Suggested:", record["suggested"], True),
317
- ):
318
- rows = [escape(title)]
319
- for offset, value in enumerate(text.split("\n")):
320
- number = record["line"] + offset if numbered else ""
321
- rows.append(
322
- f"<span style='color:#666'>{number:>6}</span> "
323
- + escape(value)
324
- )
325
- sections.append("<br>".join(rows))
382
+ annotated = annotated_source(
383
+ record["current"], record["suggested"], record["line"]
384
+ )
385
+ if annotated is not None:
386
+ sections.append(annotated)
387
+ details.setText(
388
+ details.text()
389
+ + "<br>Original source, with suggested changes: "
390
+ "<span style='color:#188038'>↳</span> insert newline; "
391
+ "<span style='color:#c62828'>↳×</span> remove newline."
392
+ )
393
+ else:
394
+ for title, text, numbered in (
395
+ ("git HEAD:", record["head"], False),
396
+ ("Now:", record["current"], True),
397
+ ("Suggested:", record["suggested"], True),
398
+ ):
399
+ rows = [escape(title)]
400
+ for offset, value in enumerate(text.split("\n")):
401
+ number = record["line"] + offset if numbered else ""
402
+ rows.append(
403
+ f"<span style='color:#666'>{number:>6}</span> "
404
+ + escape(value)
405
+ )
406
+ sections.append("<br>".join(rows))
326
407
  preview.setHtml(
327
408
  "<pre style='font-family:monospace; margin:0'>"
328
409
  + "<br><br>".join(sections)
@@ -415,7 +496,8 @@ def run_pandoc(
415
496
  comments_to_margin=False,
416
497
  no_comments=False,
417
498
  comment_filter_session=None,
418
- wrapnumber=79,
499
+ wrapnumber=DEFAULT_WIDTH,
500
+ trailing_dependent_phrase=DEFAULT_TRAILING_DEPENDENT_PHRASE,
419
501
  ):
420
502
  # {{{ automatically fix Markdown source and request edits for
421
503
  # unfinished spans
@@ -424,7 +506,8 @@ def run_pandoc(
424
506
  automatic_fixes_were_made = False
425
507
  while True:
426
508
  report = autofix_markdown_file(
427
- filename, wrapnumber=wrapnumber, git_head=True
509
+ filename, wrapnumber=wrapnumber, git_head=True,
510
+ punctuation_slop=trailing_dependent_phrase,
428
511
  )
429
512
  automatic_fixes_were_made |= bool(report["fixes"])
430
513
  if report["fixes"] or report["warnings"] or report["layout"]:
@@ -927,6 +1010,8 @@ class Handler(FileSystemEventHandler):
927
1010
  self._queue_if_source_changed(event.src_path, event.dest_path)
928
1011
 
929
1012
 
1013
+ # also used by: command_line.main through the command registry, and the
1014
+ # CPB runtime tests in tests/test_continuous_shutdown.py and test_zotero.py.
930
1015
  @register_command(
931
1016
  "continuous pandoc build. Like latexmk, but for markdown!",
932
1017
  help={
@@ -938,14 +1023,15 @@ class Handler(FileSystemEventHandler):
938
1023
  "no_comments": (
939
1024
  "Render the HTML without comment tags or comment div blocks."
940
1025
  ),
941
- "wrapnumber": (
942
- "Maximum source line width; the automatic Markdown fixes break "
943
- "only lines longer than this."
944
- ),
945
1026
  },
946
1027
  filename_extensions={"filename": ".md"},
1028
+ argument_options=WRAPPING_ARGUMENTS,
947
1029
  )
948
- def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
1030
+ def cpb(
1031
+ filename, comments_to_margin=False, no_comments=False,
1032
+ wrapnumber=DEFAULT_WIDTH,
1033
+ trailing_dependent_phrase=DEFAULT_TRAILING_DEPENDENT_PHRASE,
1034
+ ):
949
1035
  source_path = os.path.normpath(os.path.abspath(filename))
950
1036
  source_dir = os.path.dirname(source_path)
951
1037
  html_file = filename.rsplit(".", 1)[0] + ".html"
@@ -961,6 +1047,7 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
961
1047
  daemon=True,
962
1048
  )
963
1049
  chrome = None
1050
+ scroll_position = 0
964
1051
  observer = None
965
1052
  observer_started = False
966
1053
  socket_thread_started = False
@@ -993,6 +1080,7 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
993
1080
  no_comments=no_comments,
994
1081
  comment_filter_session=comment_filter_session,
995
1082
  wrapnumber=wrapnumber,
1083
+ trailing_dependent_phrase=trailing_dependent_phrase,
996
1084
  )
997
1085
  source_jump_server = SourceJumpServer(source_path)
998
1086
  source_jump_server.start()
@@ -1000,10 +1088,9 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
1000
1088
 
1001
1089
  # Selenium is deliberately initialized once, after the first
1002
1090
  # successful build. Nothing in recovery constructs a browser.
1003
- from selenium import webdriver
1004
1091
  from selenium.common.exceptions import WebDriverException
1005
1092
 
1006
- chrome = launch_chrome(webdriver)
1093
+ chrome = start_chrome()
1007
1094
  observer = Observer()
1008
1095
  change_queue = queue.Queue()
1009
1096
  event_handler = Handler(filename, change_queue)
@@ -1052,8 +1139,12 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
1052
1139
  def close_for_dialog():
1053
1140
  # a rebuild needs the user, so close the preview while the
1054
1141
  # issues are dealt with; it reopens once the rebuild is done
1055
- nonlocal chrome
1142
+ nonlocal chrome, scroll_position
1056
1143
  if chrome is not None:
1144
+ # sessionStorage does not survive quitting Chrome.
1145
+ scroll_position = chrome.execute_script(
1146
+ "return window.scrollY;"
1147
+ )
1057
1148
  close_browser_window(chrome)
1058
1149
  chrome = None
1059
1150
 
@@ -1140,6 +1231,9 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
1140
1231
  no_comments=no_comments,
1141
1232
  comment_filter_session=comment_filter_session,
1142
1233
  wrapnumber=wrapnumber,
1234
+ trailing_dependent_phrase=(
1235
+ trailing_dependent_phrase
1236
+ ),
1143
1237
  )
1144
1238
  append_autorefresh(
1145
1239
  html_file, source_jump_server.url
@@ -1185,10 +1279,14 @@ def cpb(filename, comments_to_margin=False, no_comments=False, wrapnumber=79):
1185
1279
  break
1186
1280
  if chrome is None:
1187
1281
  # {{{ reopen the preview a dialog closed
1188
- chrome = launch_chrome(webdriver)
1282
+ chrome = start_chrome()
1189
1283
  chrome.get(
1190
1284
  "file://" + os.path.abspath(html_file)
1191
1285
  )
1286
+ chrome.execute_script(
1287
+ "window.scrollTo(0, arguments[0]);",
1288
+ scroll_position,
1289
+ )
1192
1290
  # }}}
1193
1291
  # }}}
1194
1292
  time.sleep(MAIN_LOOP_INTERVAL_SECONDS)