pyDiffTools 0.1.39__tar.gz → 0.1.40__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 (114) hide show
  1. {pydifftools-0.1.39/pyDiffTools.egg-info → pydifftools-0.1.40}/PKG-INFO +1 -1
  2. {pydifftools-0.1.39 → pydifftools-0.1.40/pyDiffTools.egg-info}/PKG-INFO +1 -1
  3. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/wrap_sentences.py +90 -14
  4. {pydifftools-0.1.39 → pydifftools-0.1.40}/pyproject.toml +1 -1
  5. {pydifftools-0.1.39 → pydifftools-0.1.40}/tests/test_wrap_check.py +80 -2
  6. {pydifftools-0.1.39 → pydifftools-0.1.40}/LICENSE.md +0 -0
  7. {pydifftools-0.1.39 → pydifftools-0.1.40}/MANIFEST.in +0 -0
  8. {pydifftools-0.1.39 → pydifftools-0.1.40}/README.rst +0 -0
  9. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/_quarto.yml +0 -0
  10. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  11. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  12. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  13. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  14. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  15. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  16. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  17. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  18. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  19. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  20. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  21. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  22. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  23. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  24. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  25. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  26. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  27. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  28. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  29. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  30. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  31. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  32. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/example.qmd +0 -0
  33. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/example.tex +0 -0
  34. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/index.qmd +0 -0
  35. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  36. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  37. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  38. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  39. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/subproject1/independent.qmd +0 -0
  40. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/subproject1/index.qmd +0 -0
  41. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  42. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  43. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  44. {pydifftools-0.1.39 → pydifftools-0.1.40}/example_notebook/project1/test_include.qmd +0 -0
  45. {pydifftools-0.1.39 → pydifftools-0.1.40}/pyDiffTools.egg-info/SOURCES.txt +0 -0
  46. {pydifftools-0.1.39 → pydifftools-0.1.40}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  47. {pydifftools-0.1.39 → pydifftools-0.1.40}/pyDiffTools.egg-info/entry_points.txt +0 -0
  48. {pydifftools-0.1.39 → pydifftools-0.1.40}/pyDiffTools.egg-info/requires.txt +0 -0
  49. {pydifftools-0.1.39 → pydifftools-0.1.40}/pyDiffTools.egg-info/top_level.txt +0 -0
  50. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/__init__.py +0 -0
  51. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/bibliography.py +0 -0
  52. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/browser_lifecycle.py +0 -0
  53. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/check_numbers.py +0 -0
  54. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/citation_dialog.py +0 -0
  55. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/command_line.py +0 -0
  56. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/command_registry.py +0 -0
  57. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/comment_functions.py +0 -0
  58. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/comment_migration.py +0 -0
  59. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/comment_tags.lua +0 -0
  60. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/comment_tags_margin.lua +0 -0
  61. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/comment_tags_no_comments.lua +0 -0
  62. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/comment_toggle.js +0 -0
  63. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/comments.css +0 -0
  64. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/continuous.py +0 -0
  65. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/copy_files.py +0 -0
  66. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/diff-doc.js +0 -0
  67. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/doc_contents.py +0 -0
  68. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/flowchart/__init__.py +0 -0
  69. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/flowchart/comparison.py +0 -0
  70. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  71. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/flowchart/graph.py +0 -0
  72. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  73. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/flowchart/watch_graph.py +0 -0
  74. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/flowchart/wgrph_view.js +0 -0
  75. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/forward_search.py +0 -0
  76. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/git_gd.py +0 -0
  77. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/git_gd_history.py +0 -0
  78. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/git_gd_image.py +0 -0
  79. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/git_gd_qt.py +0 -0
  80. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/html_comments.py +0 -0
  81. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/html_uncomments.py +0 -0
  82. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/log_example.py +0 -0
  83. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/match_spaces.py +0 -0
  84. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/notebook/__init__.py +0 -0
  85. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/notebook/display.py +0 -0
  86. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/notebook/fast_build.py +0 -0
  87. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/notebook/tex_to_qmd.py +0 -0
  88. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/onewordify.py +0 -0
  89. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/onewordify_undo.py +0 -0
  90. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/outline.py +0 -0
  91. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/rearrange_tex.py +0 -0
  92. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/searchacro.py +0 -0
  93. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/separate_comments.py +0 -0
  94. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/split_conflict.py +0 -0
  95. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/unseparate_comments.py +0 -0
  96. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/update_check.py +0 -0
  97. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/xml2xlsx.vbs +0 -0
  98. {pydifftools-0.1.39 → pydifftools-0.1.40}/pydifftools/zotero.py +0 -0
  99. {pydifftools-0.1.39 → pydifftools-0.1.40}/setup.cfg +0 -0
  100. {pydifftools-0.1.39 → pydifftools-0.1.40}/tests/test_bibliography.py +0 -0
  101. {pydifftools-0.1.39 → pydifftools-0.1.40}/tests/test_browser_lifecycle.py +0 -0
  102. {pydifftools-0.1.39 → pydifftools-0.1.40}/tests/test_check_single_use.py +0 -0
  103. {pydifftools-0.1.39 → pydifftools-0.1.40}/tests/test_command_line_help.py +0 -0
  104. {pydifftools-0.1.39 → pydifftools-0.1.40}/tests/test_comment_migration.py +0 -0
  105. {pydifftools-0.1.39 → pydifftools-0.1.40}/tests/test_continuous_shutdown.py +0 -0
  106. {pydifftools-0.1.39 → pydifftools-0.1.40}/tests/test_forward_search.py +0 -0
  107. {pydifftools-0.1.39 → pydifftools-0.1.40}/tests/test_git_gd_history.py +0 -0
  108. {pydifftools-0.1.39 → pydifftools-0.1.40}/tests/test_git_gd_image.py +0 -0
  109. {pydifftools-0.1.39 → pydifftools-0.1.40}/tests/test_git_gd_qt.py +0 -0
  110. {pydifftools-0.1.39 → pydifftools-0.1.40}/tests/test_rrng.py +0 -0
  111. {pydifftools-0.1.39 → pydifftools-0.1.40}/tests/test_tex_to_qmd.py +0 -0
  112. {pydifftools-0.1.39 → pydifftools-0.1.40}/tests/test_update_check.py +0 -0
  113. {pydifftools-0.1.39 → pydifftools-0.1.40}/tests/test_wrapping.py +0 -0
  114. {pydifftools-0.1.39 → pydifftools-0.1.40}/tests/test_zotero.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.39
3
+ Version: 0.1.40
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.39
3
+ Version: 0.1.40
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -16,6 +16,16 @@ PARAGRAPH_SPLIT = re.compile(r"(\n(?:[ \t]*\n)+)")
16
16
  SENTENCE_END = r"[^.!?]{3}[.!?]"
17
17
  SENTENCE_SPLIT = re.compile("(" + SENTENCE_END + r")[ \n]")
18
18
  SENTENCE_BOUNDARY = re.compile("(" + SENTENCE_END + r")([ \n])")
19
+ # A word is a run of non-space characters, except that an attribute block
20
+ # attached to a figure's closing ")" (or any "{#id ...}" block) stays inside
21
+ # the word, so a break never lands inside a pandoc-crossref marker.
22
+ WORD = re.compile(r"(?:(?<=\))\{[^}]*\}|\{#[^}]*\}|\S)+")
23
+ # A pandoc-crossref marker must directly follow the closing "$$" of its
24
+ # equation or the closing ")" of its figure, and must stay on one line.
25
+ DETACHED_CROSSREF = re.compile(
26
+ r"(?:(?<!\\)\$\$|\))(?P<gap>[ \t]*\n?[ \t]*)(?=\{#(?:eq|fig):)"
27
+ r"|(?P<marker>\{#(?:eq|fig):[^}\n]*\n[^}]*\})"
28
+ )
19
29
  LATEX_MACRO_SPLIT = re.compile(
20
30
  r"(\\(?:begin|end|usepackage|newcommand|section"
21
31
  r"|subsection|subsubsection|paragraph|input){[^}]*})"
@@ -584,6 +594,11 @@ def classify_lines(
584
594
  if not equation.endswith("\n"):
585
595
  equation += "\n"
586
596
  equation += "$$"
597
+ # keep a pandoc-crossref marker on its closing "$$"
598
+ marker = re.match(r"\{#eq:[^}]*\}", content[stop:])
599
+ if marker:
600
+ equation += marker[0]
601
+ stop += marker.end()
587
602
  if stop < len(content):
588
603
  equation += "\n"
589
604
  if content[stop] == "\n":
@@ -686,7 +701,9 @@ def wrap_prose(
686
701
  lines = []
687
702
  indentation = 0
688
703
  for sentence in sentences:
689
- words = [word for word in re.split("[ \n]+", sentence) if word]
704
+ words = [
705
+ re.sub(r"\s+", " ", word) for word in WORD.findall(sentence)
706
+ ]
690
707
  boundaries = _math_boundaries(words)
691
708
  if filetype == "latex":
692
709
  indentation = 0
@@ -794,6 +811,15 @@ def _prepare(filename, cleanoo=False, i=-1):
794
811
  def wr(filename, wrapnumber=45, punctuation_slop=20, cleanoo=False, i=-1):
795
812
  alltext, filetype, indent_amount, _ = _prepare(filename, cleanoo, i)
796
813
  if filetype == "markdown":
814
+ # reattach detached pandoc-crossref markers before wrapping
815
+ alltext = DETACHED_CROSSREF.sub(
816
+ lambda found: (
817
+ re.sub(r"\s+", " ", found["marker"])
818
+ if found["marker"]
819
+ else found[0][: len(found[0]) - len(found["gap"])]
820
+ ),
821
+ alltext,
822
+ )
797
823
  result = wrap_blocks(
798
824
  markdown_blocks(alltext),
799
825
  wrapnumber,
@@ -865,7 +891,7 @@ def check_prose(content, wrapnumber, punctuation_slop, line_start=1):
865
891
  if fragment:
866
892
  # {{{ check line lengths within this sentence fragment
867
893
  words, positions = [], []
868
- for match in re.finditer(r"\S+", fragment):
894
+ for match in WORD.finditer(fragment):
869
895
  words.append(match.group())
870
896
  positions.append(frag_offset + match.start())
871
897
  if words:
@@ -987,6 +1013,9 @@ def markdown_lint_issues_from_text(
987
1013
  prose_content = re.sub(
988
1014
  r"<!--.*?(?:-->|\Z)", mask_comment, content, flags=re.DOTALL
989
1015
  )
1016
+ # trailing spaces are reported separately below, and a double space
1017
+ # (a hard line break) must not look like a sentence ending mid-line
1018
+ prose_content = re.sub(r"[ \t]+$", "", prose_content, flags=re.M)
990
1019
  issues = [
991
1020
  issue
992
1021
  for line, width, span in _iter_block_spans(
@@ -998,6 +1027,27 @@ def markdown_lint_issues_from_text(
998
1027
  (issue["line"], issue["message"])
999
1028
  for issue in unclosed_markdown_spans(content)
1000
1029
  )
1030
+ # {{{ report single trailing spaces and detached crossref markers
1031
+ fence = None
1032
+ for number, line in enumerate(content.splitlines(), start=1):
1033
+ opening = re.match(r"[ \t]*(\x60{3,}|~{3,})", line)
1034
+ if opening and (fence is None or opening[1].startswith(fence)):
1035
+ fence = None if fence else opening[1]
1036
+ elif fence is None and re.search(r"(?<! ) $", line):
1037
+ issues.append((
1038
+ number,
1039
+ "trailing space: remove the single space at the end of this "
1040
+ "line (two spaces are a deliberate hard line break)",
1041
+ ))
1042
+ for found in DETACHED_CROSSREF.finditer(prose_content):
1043
+ if found["gap"] == "":
1044
+ continue
1045
+ issues.append((
1046
+ prose_content.count("\n", 0, found.start()) + 1,
1047
+ "detached crossref marker: keep the {#eq:...} or {#fig:...} "
1048
+ "marker on one line, directly after the closing $$ or )",
1049
+ ))
1050
+ # }}}
1001
1051
  return sorted(issues)
1002
1052
 
1003
1053
 
@@ -1101,6 +1151,33 @@ def apply_markdown_issue_fix(content, line_number, message):
1101
1151
  raise ValueError(
1102
1152
  f"Markdown issue refers to missing line {line_number}"
1103
1153
  )
1154
+ if message.startswith("detached crossref marker"):
1155
+ # {{{ join the marker onto its "$$" or ")" (this can remove a line)
1156
+ start = sum(len(line) for line in lines[:index])
1157
+ found = next(
1158
+ found
1159
+ for found in DETACHED_CROSSREF.finditer(content, start)
1160
+ if found["gap"] != ""
1161
+ )
1162
+ if found["gap"] is None:
1163
+ span = found.span("marker")
1164
+ joined = re.sub(r"\s+", " ", found["marker"])
1165
+ else:
1166
+ span = found.span("gap")
1167
+ joined = ""
1168
+ before = content[found.start() : found.end()]
1169
+ updated = content[: span[0]] + joined + content[span[1] :]
1170
+ after = updated[
1171
+ found.start() : span[0] + len(joined) + found.end() - span[1]
1172
+ ]
1173
+ return (
1174
+ updated,
1175
+ before,
1176
+ after,
1177
+ "A crossref marker was separated from its equation or figure. "
1178
+ "I joined it back on.",
1179
+ )
1180
+ # }}}
1104
1181
  old_line = lines[index]
1105
1182
  ending = ""
1106
1183
  if old_line.endswith("\r\n"):
@@ -1123,6 +1200,14 @@ def apply_markdown_issue_fix(content, line_number, message):
1123
1200
  else:
1124
1201
  prefix = re.match(r"^[ \t]*", old_line).group()
1125
1202
  # }}}
1203
+ if message.startswith("trailing space:"):
1204
+ lines[index] = old_line[:-1] + ending
1205
+ return (
1206
+ "".join(lines),
1207
+ old_line,
1208
+ old_line[:-1],
1209
+ "A line ended with a single stray space. I removed it.",
1210
+ )
1126
1211
  if message.startswith("line too long:"):
1127
1212
  match = re.search(r"after '(.+?)' \(before '(.+?)'\); move", message)
1128
1213
  if match is None:
@@ -1164,22 +1249,13 @@ def apply_markdown_issue_fix(content, line_number, message):
1164
1249
  else:
1165
1250
  raise ValueError(f"No automatic fix is available for {message!r}")
1166
1251
 
1167
- if not first.strip():
1252
+ if not first.strip() or not second.strip():
1168
1253
  # a break here would insert a blank line, i.e. a paragraph break
1169
1254
  raise ValueError(
1170
1255
  f"Automatic fix would leave an empty line at {line_number}"
1171
1256
  )
1172
- if second.strip():
1173
- lines[index] = first + ending + prefix + second + ending
1174
- after = first + "\n" + prefix + second
1175
- else:
1176
- # only trailing whitespace followed the break, so drop it rather
1177
- # than adding a blank line
1178
- lines[index] = first + ending
1179
- after = first
1180
- reason = (
1181
- "A sentence ended with a trailing space. I removed the space."
1182
- )
1257
+ lines[index] = first + ending + prefix + second + ending
1258
+ after = first + "\n" + prefix + second
1183
1259
  return "".join(lines), old_line, after, reason
1184
1260
 
1185
1261
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyDiffTools"
7
- version = "0.1.39"
7
+ version = "0.1.40"
8
8
  authors = [{ name = "J M Franck" }]
9
9
  description = "Diff tools"
10
10
  readme = "README.rst"
@@ -1,3 +1,4 @@
1
+ import re
1
2
  import subprocess
2
3
 
3
4
  import pytest
@@ -150,15 +151,92 @@ def test_long_line_fix_splits_at_the_word_after_the_break():
150
151
  assert fixed.startswith("for $30\\;\\text{s}$ ($3\\times$\nfor AOT)")
151
152
 
152
153
 
153
- def test_trailing_space_after_sentence_is_removed_not_a_blank_line(tmp_path):
154
+ def test_single_trailing_space_is_flagged_and_removed(tmp_path):
155
+ # from RM_ESR: a sentence ending in a stray space
154
156
  path = tmp_path / "source.md"
155
157
  path.write_text("It melts near 238&nbsp;K. \nIn contrast, it stays.\n")
156
158
 
159
+ issues = markdown_lint_issues_from_text(path.read_text(), wrapnumber=55)
157
160
  autofix_markdown_file(path)
158
161
 
162
+ assert (1, "trailing space") in [
163
+ (line, message.split(":")[0]) for line, message in issues
164
+ ]
159
165
  fixed = path.read_text()
160
166
  assert fixed.startswith("It melts near 238&nbsp;K.\nIn contrast,")
161
- assert "\n\n" not in fixed
167
+ assert " \n" not in fixed
168
+
169
+
170
+ def test_double_trailing_space_hard_break_is_allowed():
171
+ source = "The first line ends here with a break \nand continues.\n"
172
+
173
+ assert markdown_lint_issues_from_text(source, wrapnumber=55) == []
174
+
175
+
176
+ @pytest.mark.parametrize(
177
+ "detached",
178
+ [
179
+ # equation markers must sit directly on the closing $$
180
+ "Intro.\n$$\nx = y\n$$ {#eq:x}\nwhere this holds.\n",
181
+ "Intro.\n$$\nx = y\n$$\n{#eq:x}\nwhere this holds.\n",
182
+ # figure markers must sit directly on the closing ) (from eigenmode)
183
+ "![A caption.](./media/BothProbes_Bfields.png)\n"
184
+ '{#fig:ProbeMagFieldsDrawing width="4in"}\n',
185
+ # and must not be split across lines (from RM_ESR)
186
+ "![A caption for\nspectroscopy.](Figures/simple_oned_CAT16.png)"
187
+ "{#fig:justSpectraCat16\nwidth=4in}\n",
188
+ ],
189
+ )
190
+ def test_detached_crossref_marker_is_flagged_and_joined(tmp_path, detached):
191
+ path = tmp_path / "source.md"
192
+ path.write_text(detached)
193
+
194
+ issues = markdown_lint_issues_from_text(detached, wrapnumber=55)
195
+ report = autofix_markdown_file(path)
196
+
197
+ fixed = path.read_text()
198
+ assert [message.split(":")[0] for _, message in issues] == [
199
+ "detached crossref marker"
200
+ ]
201
+ assert report["fixes"]
202
+ assert markdown_lint_issues_from_text(fixed, wrapnumber=55) == []
203
+ assert re.search(r"(\$\$|\))\{#(eq|fig):[^}\n]*\}", fixed)
204
+
205
+
206
+ def test_figure_crossref_marker_is_never_split_from_its_figure(tmp_path):
207
+ # from RM_ESR: a caption spanning lines, ending in a marker with
208
+ # attributes; wr and the linter must treat "...){#fig:... }" as one word
209
+ source = (
210
+ "![Room temperature ESR of 100 mM TEMPO-SO~4~ RMs with water "
211
+ "loading $w_0=30$ in isooctane dispersant (blue), single HE "
212
+ "simulation (orange), multi HE simulation "
213
+ "(green)](Figures/HE_Easyspin_Simul_100mM.png)\n"
214
+ "{#fig:HESimul width=5.5in}\n"
215
+ )
216
+ path = tmp_path / "source.md"
217
+ path.write_text(source)
218
+
219
+ wr(str(path), 55)
220
+
221
+ wrapped = path.read_text()
222
+ assert (
223
+ "(green)](Figures/HE_Easyspin_Simul_100mM.png)"
224
+ "{#fig:HESimul width=5.5in}\n"
225
+ ) in wrapped
226
+ assert markdown_lint_issues_from_text(wrapped, wrapnumber=55) == []
227
+
228
+
229
+ def test_wr_keeps_equation_marker_on_closing_dollars(tmp_path):
230
+ path = tmp_path / "source.md"
231
+ path.write_text(
232
+ "Intro.\n$$\nx = y\n$$ {#eq:tauFromLinewidth}\nwhere this holds.\n"
233
+ )
234
+
235
+ wr(str(path), 55)
236
+
237
+ wrapped = path.read_text()
238
+ assert "\n$${#eq:tauFromLinewidth}\n" in wrapped
239
+ assert markdown_lint_issues_from_text(wrapped, wrapnumber=55) == []
162
240
 
163
241
 
164
242
  def test_autofix_fixes_every_line_in_one_pass_from_the_end(tmp_path):
File without changes
File without changes
File without changes
File without changes