pyDiffTools 0.1.39__tar.gz → 0.1.41__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.41}/PKG-INFO +1 -1
  2. {pydifftools-0.1.39 → pydifftools-0.1.41/pyDiffTools.egg-info}/PKG-INFO +1 -1
  3. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/continuous.py +1 -1
  4. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/wrap_sentences.py +146 -26
  5. {pydifftools-0.1.39 → pydifftools-0.1.41}/pyproject.toml +1 -1
  6. {pydifftools-0.1.39 → pydifftools-0.1.41}/tests/test_wrap_check.py +125 -2
  7. {pydifftools-0.1.39 → pydifftools-0.1.41}/LICENSE.md +0 -0
  8. {pydifftools-0.1.39 → pydifftools-0.1.41}/MANIFEST.in +0 -0
  9. {pydifftools-0.1.39 → pydifftools-0.1.41}/README.rst +0 -0
  10. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/_quarto.yml +0 -0
  11. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  12. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  13. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  14. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  15. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  16. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  17. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  18. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  19. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  20. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  21. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  22. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  23. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  24. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  25. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  26. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  27. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  28. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  29. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  30. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  31. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  32. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  33. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/example.qmd +0 -0
  34. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/example.tex +0 -0
  35. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/index.qmd +0 -0
  36. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  37. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  38. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  39. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  40. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/subproject1/independent.qmd +0 -0
  41. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/subproject1/index.qmd +0 -0
  42. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  43. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  44. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  45. {pydifftools-0.1.39 → pydifftools-0.1.41}/example_notebook/project1/test_include.qmd +0 -0
  46. {pydifftools-0.1.39 → pydifftools-0.1.41}/pyDiffTools.egg-info/SOURCES.txt +0 -0
  47. {pydifftools-0.1.39 → pydifftools-0.1.41}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  48. {pydifftools-0.1.39 → pydifftools-0.1.41}/pyDiffTools.egg-info/entry_points.txt +0 -0
  49. {pydifftools-0.1.39 → pydifftools-0.1.41}/pyDiffTools.egg-info/requires.txt +0 -0
  50. {pydifftools-0.1.39 → pydifftools-0.1.41}/pyDiffTools.egg-info/top_level.txt +0 -0
  51. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/__init__.py +0 -0
  52. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/bibliography.py +0 -0
  53. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/browser_lifecycle.py +0 -0
  54. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/check_numbers.py +0 -0
  55. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/citation_dialog.py +0 -0
  56. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/command_line.py +0 -0
  57. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/command_registry.py +0 -0
  58. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/comment_functions.py +0 -0
  59. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/comment_migration.py +0 -0
  60. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/comment_tags.lua +0 -0
  61. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/comment_tags_margin.lua +0 -0
  62. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/comment_tags_no_comments.lua +0 -0
  63. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/comment_toggle.js +0 -0
  64. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/comments.css +0 -0
  65. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/copy_files.py +0 -0
  66. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/diff-doc.js +0 -0
  67. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/doc_contents.py +0 -0
  68. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/flowchart/__init__.py +0 -0
  69. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/flowchart/comparison.py +0 -0
  70. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  71. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/flowchart/graph.py +0 -0
  72. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  73. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/flowchart/watch_graph.py +0 -0
  74. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/flowchart/wgrph_view.js +0 -0
  75. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/forward_search.py +0 -0
  76. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/git_gd.py +0 -0
  77. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/git_gd_history.py +0 -0
  78. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/git_gd_image.py +0 -0
  79. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/git_gd_qt.py +0 -0
  80. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/html_comments.py +0 -0
  81. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/html_uncomments.py +0 -0
  82. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/log_example.py +0 -0
  83. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/match_spaces.py +0 -0
  84. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/notebook/__init__.py +0 -0
  85. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/notebook/display.py +0 -0
  86. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/notebook/fast_build.py +0 -0
  87. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/notebook/tex_to_qmd.py +0 -0
  88. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/onewordify.py +0 -0
  89. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/onewordify_undo.py +0 -0
  90. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/outline.py +0 -0
  91. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/rearrange_tex.py +0 -0
  92. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/searchacro.py +0 -0
  93. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/separate_comments.py +0 -0
  94. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/split_conflict.py +0 -0
  95. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/unseparate_comments.py +0 -0
  96. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/update_check.py +0 -0
  97. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/xml2xlsx.vbs +0 -0
  98. {pydifftools-0.1.39 → pydifftools-0.1.41}/pydifftools/zotero.py +0 -0
  99. {pydifftools-0.1.39 → pydifftools-0.1.41}/setup.cfg +0 -0
  100. {pydifftools-0.1.39 → pydifftools-0.1.41}/tests/test_bibliography.py +0 -0
  101. {pydifftools-0.1.39 → pydifftools-0.1.41}/tests/test_browser_lifecycle.py +0 -0
  102. {pydifftools-0.1.39 → pydifftools-0.1.41}/tests/test_check_single_use.py +0 -0
  103. {pydifftools-0.1.39 → pydifftools-0.1.41}/tests/test_command_line_help.py +0 -0
  104. {pydifftools-0.1.39 → pydifftools-0.1.41}/tests/test_comment_migration.py +0 -0
  105. {pydifftools-0.1.39 → pydifftools-0.1.41}/tests/test_continuous_shutdown.py +0 -0
  106. {pydifftools-0.1.39 → pydifftools-0.1.41}/tests/test_forward_search.py +0 -0
  107. {pydifftools-0.1.39 → pydifftools-0.1.41}/tests/test_git_gd_history.py +0 -0
  108. {pydifftools-0.1.39 → pydifftools-0.1.41}/tests/test_git_gd_image.py +0 -0
  109. {pydifftools-0.1.39 → pydifftools-0.1.41}/tests/test_git_gd_qt.py +0 -0
  110. {pydifftools-0.1.39 → pydifftools-0.1.41}/tests/test_rrng.py +0 -0
  111. {pydifftools-0.1.39 → pydifftools-0.1.41}/tests/test_tex_to_qmd.py +0 -0
  112. {pydifftools-0.1.39 → pydifftools-0.1.41}/tests/test_update_check.py +0 -0
  113. {pydifftools-0.1.39 → pydifftools-0.1.41}/tests/test_wrapping.py +0 -0
  114. {pydifftools-0.1.39 → pydifftools-0.1.41}/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.41
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.41
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -310,7 +310,7 @@ def run_pandoc(
310
310
 
311
311
  automatic_fixes_were_made = False
312
312
  while True:
313
- report = autofix_markdown_file(filename, wrapnumber=55)
313
+ report = autofix_markdown_file(filename, max_line_length=79)
314
314
  automatic_fixes_were_made |= bool(report["fixes"])
315
315
  if report["fixes"] or report["warnings"]:
316
316
  user_says_fixed = show_markdown_fix_dialog(report)
@@ -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:
@@ -978,26 +1004,71 @@ def _iter_block_spans(blocks, wrapnumber, line_start=1):
978
1004
 
979
1005
 
980
1006
  def markdown_lint_issues_from_text(
981
- content, wrapnumber=45, punctuation_slop=20
1007
+ content, wrapnumber=45, punctuation_slop=20, max_line_length=None
982
1008
  ):
983
- """Return Markdown writing issues, including unfinished source spans."""
1009
+ """Return Markdown writing issues, including unfinished source spans.
1010
+
1011
+ With `max_line_length`, a prose line is only too long when it holds
1012
+ more than that many characters, instead of wr's greedy line choice.
1013
+ """
984
1014
  def mask_comment(match):
985
1015
  return "".join("\n" if char == "\n" else " " for char in match[0])
986
1016
 
987
1017
  prose_content = re.sub(
988
1018
  r"<!--.*?(?:-->|\Z)", mask_comment, content, flags=re.DOTALL
989
1019
  )
990
- issues = [
991
- issue
992
- for line, width, span in _iter_block_spans(
993
- markdown_blocks(prose_content), wrapnumber
994
- )
995
- for issue in check_prose(span, width, punctuation_slop, line)
996
- ]
1020
+ # trailing spaces are reported separately below, and a double space
1021
+ # (a hard line break) must not look like a sentence ending mid-line
1022
+ prose_content = re.sub(r"[ \t]+$", "", prose_content, flags=re.M)
1023
+ issues = []
1024
+ prose_lines = prose_content.splitlines()
1025
+ for line, width, span in _iter_block_spans(
1026
+ markdown_blocks(prose_content), wrapnumber
1027
+ ):
1028
+ for issue in check_prose(span, width, punctuation_slop, line):
1029
+ if max_line_length is None or not issue[1].startswith(
1030
+ "line too long:"
1031
+ ):
1032
+ issues.append(issue)
1033
+ if max_line_length is not None:
1034
+ # {{{ flag prose lines longer than the character limit
1035
+ for number in range(line, line + len(span.splitlines())):
1036
+ text = prose_lines[number - 1]
1037
+ # a single unbreakable word cannot be shortened
1038
+ if len(text) > max_line_length and len(
1039
+ WORD.findall(text)
1040
+ ) > 1:
1041
+ issues.append((
1042
+ number,
1043
+ f"line over {max_line_length} characters: break "
1044
+ "it at a word boundary",
1045
+ ))
1046
+ # }}}
997
1047
  issues.extend(
998
1048
  (issue["line"], issue["message"])
999
1049
  for issue in unclosed_markdown_spans(content)
1000
1050
  )
1051
+ # {{{ report single trailing spaces and detached crossref markers
1052
+ fence = None
1053
+ for number, line in enumerate(content.splitlines(), start=1):
1054
+ opening = re.match(r"[ \t]*(\x60{3,}|~{3,})", line)
1055
+ if opening and (fence is None or opening[1].startswith(fence)):
1056
+ fence = None if fence else opening[1]
1057
+ elif fence is None and re.search(r"(?<! ) $", line):
1058
+ issues.append((
1059
+ number,
1060
+ "trailing space: remove the single space at the end of this "
1061
+ "line (two spaces are a deliberate hard line break)",
1062
+ ))
1063
+ for found in DETACHED_CROSSREF.finditer(prose_content):
1064
+ if found["gap"] == "":
1065
+ continue
1066
+ issues.append((
1067
+ prose_content.count("\n", 0, found.start()) + 1,
1068
+ "detached crossref marker: keep the {#eq:...} or {#fig:...} "
1069
+ "marker on one line, directly after the closing $$ or )",
1070
+ ))
1071
+ # }}}
1001
1072
  return sorted(issues)
1002
1073
 
1003
1074
 
@@ -1101,6 +1172,33 @@ def apply_markdown_issue_fix(content, line_number, message):
1101
1172
  raise ValueError(
1102
1173
  f"Markdown issue refers to missing line {line_number}"
1103
1174
  )
1175
+ if message.startswith("detached crossref marker"):
1176
+ # {{{ join the marker onto its "$$" or ")" (this can remove a line)
1177
+ start = sum(len(line) for line in lines[:index])
1178
+ found = next(
1179
+ found
1180
+ for found in DETACHED_CROSSREF.finditer(content, start)
1181
+ if found["gap"] != ""
1182
+ )
1183
+ if found["gap"] is None:
1184
+ span = found.span("marker")
1185
+ joined = re.sub(r"\s+", " ", found["marker"])
1186
+ else:
1187
+ span = found.span("gap")
1188
+ joined = ""
1189
+ before = content[found.start() : found.end()]
1190
+ updated = content[: span[0]] + joined + content[span[1] :]
1191
+ after = updated[
1192
+ found.start() : span[0] + len(joined) + found.end() - span[1]
1193
+ ]
1194
+ return (
1195
+ updated,
1196
+ before,
1197
+ after,
1198
+ "A crossref marker was separated from its equation or figure. "
1199
+ "I joined it back on.",
1200
+ )
1201
+ # }}}
1104
1202
  old_line = lines[index]
1105
1203
  ending = ""
1106
1204
  if old_line.endswith("\r\n"):
@@ -1123,7 +1221,33 @@ def apply_markdown_issue_fix(content, line_number, message):
1123
1221
  else:
1124
1222
  prefix = re.match(r"^[ \t]*", old_line).group()
1125
1223
  # }}}
1126
- if message.startswith("line too long:"):
1224
+ if message.startswith("trailing space:"):
1225
+ lines[index] = old_line[:-1] + ending
1226
+ return (
1227
+ "".join(lines),
1228
+ old_line,
1229
+ old_line[:-1],
1230
+ "A line ended with a single stray space. I removed it.",
1231
+ )
1232
+ if message.startswith("line over "):
1233
+ # {{{ break at the last word boundary that fits the limit
1234
+ limit = int(re.match(r"line over (\d+)", message)[1])
1235
+ gaps = [
1236
+ found.end()
1237
+ for found in WORD.finditer(old_line)
1238
+ if container.end() < found.end() < len(old_line.rstrip())
1239
+ ]
1240
+ fitting = [gap for gap in gaps if gap <= limit]
1241
+ # when even the first word is too long, break right after it
1242
+ split_at = fitting[-1] if fitting else gaps[0]
1243
+ first = old_line[:split_at].rstrip()
1244
+ second = old_line[split_at:].lstrip(" \t")
1245
+ reason = (
1246
+ f"This line was longer than {limit} characters. "
1247
+ "I moved the last words onto a new line."
1248
+ )
1249
+ # }}}
1250
+ elif message.startswith("line too long:"):
1127
1251
  match = re.search(r"after '(.+?)' \(before '(.+?)'\); move", message)
1128
1252
  if match is None:
1129
1253
  raise ValueError(
@@ -1164,26 +1288,19 @@ def apply_markdown_issue_fix(content, line_number, message):
1164
1288
  else:
1165
1289
  raise ValueError(f"No automatic fix is available for {message!r}")
1166
1290
 
1167
- if not first.strip():
1291
+ if not first.strip() or not second.strip():
1168
1292
  # a break here would insert a blank line, i.e. a paragraph break
1169
1293
  raise ValueError(
1170
1294
  f"Automatic fix would leave an empty line at {line_number}"
1171
1295
  )
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
- )
1296
+ lines[index] = first + ending + prefix + second + ending
1297
+ after = first + "\n" + prefix + second
1183
1298
  return "".join(lines), old_line, after, reason
1184
1299
 
1185
1300
 
1186
- def autofix_markdown_file(filename, wrapnumber=55, punctuation_slop=20):
1301
+ def autofix_markdown_file(
1302
+ filename, wrapnumber=55, punctuation_slop=20, max_line_length=None
1303
+ ):
1187
1304
  """Apply safe source line breaks and report unfinished source spans."""
1188
1305
  with open(filename, encoding="utf-8", newline="") as fp:
1189
1306
  content = fp.read()
@@ -1192,7 +1309,10 @@ def autofix_markdown_file(filename, wrapnumber=55, punctuation_slop=20):
1192
1309
  # issue at a time re-lints the whole file for every single fix.
1193
1310
  for _ in range(100):
1194
1311
  issues = markdown_lint_issues_from_text(
1195
- content, wrapnumber=wrapnumber, punctuation_slop=punctuation_slop
1312
+ content,
1313
+ wrapnumber=wrapnumber,
1314
+ punctuation_slop=punctuation_slop,
1315
+ max_line_length=max_line_length,
1196
1316
  )
1197
1317
  fixable = {}
1198
1318
  for line_number, message in issues:
@@ -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.41"
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):
@@ -175,6 +253,51 @@ def test_autofix_fixes_every_line_in_one_pass_from_the_end(tmp_path):
175
253
  assert markdown_lint_issues_from_text(fixed, wrapnumber=55) == []
176
254
 
177
255
 
256
+ def test_character_limit_ignores_lines_wr_would_break_sooner():
257
+ # from RM_ESR: 74 characters, which wr's width-55 rule would break
258
+ source = (
259
+ "temperatures not amenable to other methods, as in "
260
+ "[@Banerjee2009ESREviCoe].\n"
261
+ )
262
+
263
+ assert markdown_lint_issues_from_text(source, 55) != []
264
+ assert markdown_lint_issues_from_text(source, max_line_length=79) == []
265
+
266
+
267
+ @pytest.mark.parametrize("prefix", ["", "- ", "> "])
268
+ def test_character_limit_breaks_at_last_fitting_word(tmp_path, prefix):
269
+ # from RM_ESR: a sentence that grew past 79 characters
270
+ line = (
271
+ prefix + "This script determines the field positions and amplitudes "
272
+ "of the spectral lines for an entire variable-temperature "
273
+ "experiment.\n"
274
+ )
275
+ path = tmp_path / "source.md"
276
+ path.write_text(line)
277
+
278
+ issues = markdown_lint_issues_from_text(line, max_line_length=79)
279
+ report = autofix_markdown_file(path, max_line_length=79)
280
+
281
+ fixed = path.read_text()
282
+ assert [message.split(":")[0] for _, message in issues] == [
283
+ "line over 79 characters"
284
+ ]
285
+ assert "79 characters" in report["fixes"][0]["reason"]
286
+ assert all(len(text) <= 79 for text in fixed.splitlines())
287
+ assert fixed.replace("\n> ", "\n").split() == line.split()
288
+ # the first line is filled as far as the limit allows
289
+ first = fixed.splitlines()[0]
290
+ next_word = line.split()[len(first.split())]
291
+ assert len(first) + 1 + len(next_word) > 79
292
+ assert markdown_lint_issues_from_text(fixed, max_line_length=79) == []
293
+
294
+
295
+ def test_character_limit_skips_a_single_unbreakable_word():
296
+ source = "https://example.org/" + "x" * 90 + "\n"
297
+
298
+ assert markdown_lint_issues_from_text(source, max_line_length=79) == []
299
+
300
+
178
301
  def test_unclosed_math_is_reported_after_fifty_lines():
179
302
  short_math = "$$\n" + ("x = y\n" * 48)
180
303
  long_math = "$$\n" + ("x = y\n" * 49)
File without changes
File without changes
File without changes
File without changes