pyDiffTools 0.1.38__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.
- {pydifftools-0.1.38/pyDiffTools.egg-info → pydifftools-0.1.40}/PKG-INFO +1 -1
- {pydifftools-0.1.38 → pydifftools-0.1.40/pyDiffTools.egg-info}/PKG-INFO +1 -1
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/wrap_sentences.py +140 -39
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pyproject.toml +1 -1
- {pydifftools-0.1.38 → pydifftools-0.1.40}/tests/test_wrap_check.py +133 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/LICENSE.md +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/MANIFEST.in +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/README.rst +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/_quarto.yml +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/example.qmd +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/example.tex +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/index.qmd +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/subproject1/independent.qmd +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/subproject1/index.qmd +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/subproject1/tasks.qmd +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/subproject1/test_include.qmd +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/test_include.qmd +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pyDiffTools.egg-info/SOURCES.txt +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pyDiffTools.egg-info/dependency_links.txt +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pyDiffTools.egg-info/entry_points.txt +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pyDiffTools.egg-info/requires.txt +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pyDiffTools.egg-info/top_level.txt +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/__init__.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/bibliography.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/browser_lifecycle.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/check_numbers.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/citation_dialog.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/command_line.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/command_registry.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/comment_functions.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/comment_migration.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/comment_tags.lua +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/comment_tags_margin.lua +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/comment_tags_no_comments.lua +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/comment_toggle.js +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/comments.css +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/continuous.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/copy_files.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/diff-doc.js +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/doc_contents.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/flowchart/__init__.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/flowchart/comparison.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/flowchart/dot_to_yaml.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/flowchart/graph.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/flowchart/watch_graph.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/flowchart/wgrph_view.js +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/forward_search.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/git_gd.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/git_gd_history.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/git_gd_image.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/git_gd_qt.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/html_comments.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/html_uncomments.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/log_example.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/match_spaces.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/notebook/__init__.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/notebook/display.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/notebook/fast_build.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/notebook/tex_to_qmd.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/onewordify.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/onewordify_undo.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/outline.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/rearrange_tex.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/searchacro.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/separate_comments.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/split_conflict.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/unseparate_comments.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/update_check.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/xml2xlsx.vbs +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/pydifftools/zotero.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/setup.cfg +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/tests/test_bibliography.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/tests/test_browser_lifecycle.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/tests/test_check_single_use.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/tests/test_command_line_help.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/tests/test_comment_migration.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/tests/test_continuous_shutdown.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/tests/test_forward_search.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/tests/test_git_gd_history.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/tests/test_git_gd_image.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/tests/test_git_gd_qt.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/tests/test_rrng.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/tests/test_tex_to_qmd.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/tests/test_update_check.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/tests/test_wrapping.py +0 -0
- {pydifftools-0.1.38 → pydifftools-0.1.40}/tests/test_zotero.py +0 -0
|
@@ -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 = [
|
|
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
|
|
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:
|
|
@@ -942,7 +968,9 @@ def _iter_classified_spans(text, filetype, line_start=1):
|
|
|
942
968
|
leaf prose blocks -- both just need the same protected-region
|
|
943
969
|
exclusions wr itself uses.
|
|
944
970
|
"""
|
|
945
|
-
|
|
971
|
+
# normalizing math can add lines (e.g. splitting "$${#eq:x}"), which
|
|
972
|
+
# would shift every reported line number after it
|
|
973
|
+
classified = classify_lines(text, filetype)
|
|
946
974
|
line = line_start
|
|
947
975
|
for wrappable, group in itertools.groupby(classified, key=lambda x: x[0]):
|
|
948
976
|
content = "".join(l for _, l in group)
|
|
@@ -985,6 +1013,9 @@ def markdown_lint_issues_from_text(
|
|
|
985
1013
|
prose_content = re.sub(
|
|
986
1014
|
r"<!--.*?(?:-->|\Z)", mask_comment, content, flags=re.DOTALL
|
|
987
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)
|
|
988
1019
|
issues = [
|
|
989
1020
|
issue
|
|
990
1021
|
for line, width, span in _iter_block_spans(
|
|
@@ -996,6 +1027,27 @@ def markdown_lint_issues_from_text(
|
|
|
996
1027
|
(issue["line"], issue["message"])
|
|
997
1028
|
for issue in unclosed_markdown_spans(content)
|
|
998
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
|
+
# }}}
|
|
999
1051
|
return sorted(issues)
|
|
1000
1052
|
|
|
1001
1053
|
|
|
@@ -1099,6 +1151,33 @@ def apply_markdown_issue_fix(content, line_number, message):
|
|
|
1099
1151
|
raise ValueError(
|
|
1100
1152
|
f"Markdown issue refers to missing line {line_number}"
|
|
1101
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
|
+
# }}}
|
|
1102
1181
|
old_line = lines[index]
|
|
1103
1182
|
ending = ""
|
|
1104
1183
|
if old_line.endswith("\r\n"):
|
|
@@ -1121,20 +1200,33 @@ def apply_markdown_issue_fix(content, line_number, message):
|
|
|
1121
1200
|
else:
|
|
1122
1201
|
prefix = re.match(r"^[ \t]*", old_line).group()
|
|
1123
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
|
+
)
|
|
1124
1211
|
if message.startswith("line too long:"):
|
|
1125
|
-
match = re.search(r"before '(
|
|
1212
|
+
match = re.search(r"after '(.+?)' \(before '(.+?)'\); move", message)
|
|
1126
1213
|
if match is None:
|
|
1127
1214
|
raise ValueError(
|
|
1128
1215
|
f"Cannot find the suggested word in {message!r}"
|
|
1129
1216
|
)
|
|
1130
|
-
word
|
|
1131
|
-
|
|
1217
|
+
# split at the suggested word that follows wr's break word, since the
|
|
1218
|
+
# same word can appear earlier on the line (e.g. "for ... ($3x$ for")
|
|
1219
|
+
position = re.search(
|
|
1220
|
+
r"(?<!\S)" + re.escape(match.group(1)) + r"[ \t]+("
|
|
1221
|
+
+ re.escape(match.group(2)) + r")(?!\S)",
|
|
1222
|
+
old_line,
|
|
1223
|
+
)
|
|
1132
1224
|
if position is None:
|
|
1133
1225
|
raise ValueError(
|
|
1134
|
-
f"Cannot find {match.group(1)!r} on
|
|
1135
|
-
f"{line_number}"
|
|
1226
|
+
f"Cannot find {match.group(2)!r} after {match.group(1)!r} on "
|
|
1227
|
+
f"Markdown line {line_number}"
|
|
1136
1228
|
)
|
|
1137
|
-
split_at = position.start()
|
|
1229
|
+
split_at = position.start(1)
|
|
1138
1230
|
first = old_line[:split_at].rstrip()
|
|
1139
1231
|
second = old_line[split_at:].lstrip(" \t")
|
|
1140
1232
|
reason = (
|
|
@@ -1157,8 +1249,12 @@ def apply_markdown_issue_fix(content, line_number, message):
|
|
|
1157
1249
|
else:
|
|
1158
1250
|
raise ValueError(f"No automatic fix is available for {message!r}")
|
|
1159
1251
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1252
|
+
if not first.strip() or not second.strip():
|
|
1253
|
+
# a break here would insert a blank line, i.e. a paragraph break
|
|
1254
|
+
raise ValueError(
|
|
1255
|
+
f"Automatic fix would leave an empty line at {line_number}"
|
|
1256
|
+
)
|
|
1257
|
+
lines[index] = first + ending + prefix + second + ending
|
|
1162
1258
|
after = first + "\n" + prefix + second
|
|
1163
1259
|
return "".join(lines), old_line, after, reason
|
|
1164
1260
|
|
|
@@ -1168,39 +1264,44 @@ def autofix_markdown_file(filename, wrapnumber=55, punctuation_slop=20):
|
|
|
1168
1264
|
with open(filename, encoding="utf-8", newline="") as fp:
|
|
1169
1265
|
content = fp.read()
|
|
1170
1266
|
fixes = []
|
|
1171
|
-
|
|
1267
|
+
# Lint once, fix every flagged line, then lint again, since fixing one
|
|
1268
|
+
# issue at a time re-lints the whole file for every single fix.
|
|
1269
|
+
for _ in range(100):
|
|
1172
1270
|
issues = markdown_lint_issues_from_text(
|
|
1173
1271
|
content, wrapnumber=wrapnumber, punctuation_slop=punctuation_slop
|
|
1174
1272
|
)
|
|
1175
|
-
fixable =
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
)
|
|
1185
|
-
if fixable is None:
|
|
1273
|
+
fixable = {}
|
|
1274
|
+
for line_number, message in issues:
|
|
1275
|
+
if not message.startswith(
|
|
1276
|
+
("unclosed math:", "unclosed HTML comment:")
|
|
1277
|
+
):
|
|
1278
|
+
# a fix only rewrites its own line, so fix one issue per line
|
|
1279
|
+
# per pass and let the next lint catch whatever remains
|
|
1280
|
+
fixable.setdefault(line_number, message)
|
|
1281
|
+
if not fixable:
|
|
1186
1282
|
break
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
)
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
f"Automatic Markdown fix made no change at line {line_number}"
|
|
1283
|
+
# work from the bottom up so each inserted line break leaves the
|
|
1284
|
+
# line numbers of the remaining issues unchanged
|
|
1285
|
+
pass_fixes = []
|
|
1286
|
+
for line_number in sorted(fixable, reverse=True):
|
|
1287
|
+
updated, before, after, reason = apply_markdown_issue_fix(
|
|
1288
|
+
content, line_number, fixable[line_number]
|
|
1194
1289
|
)
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1290
|
+
if updated == content:
|
|
1291
|
+
raise RuntimeError(
|
|
1292
|
+
"Automatic Markdown fix made no change at line "
|
|
1293
|
+
f"{line_number}"
|
|
1294
|
+
)
|
|
1295
|
+
pass_fixes.append(
|
|
1296
|
+
{
|
|
1297
|
+
"line": line_number,
|
|
1298
|
+
"reason": reason,
|
|
1299
|
+
"before": before,
|
|
1300
|
+
"after": after,
|
|
1301
|
+
}
|
|
1302
|
+
)
|
|
1303
|
+
content = updated
|
|
1304
|
+
fixes.extend(reversed(pass_fixes))
|
|
1204
1305
|
else:
|
|
1205
1306
|
raise RuntimeError("Too many automatic Markdown fixes were needed.")
|
|
1206
1307
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import re
|
|
1
2
|
import subprocess
|
|
2
3
|
|
|
3
4
|
import pytest
|
|
@@ -120,6 +121,138 @@ def test_one_reported_long_line_is_fixed_at_the_suggested_word():
|
|
|
120
121
|
assert "hard to read" in reason
|
|
121
122
|
|
|
122
123
|
|
|
124
|
+
def test_lint_line_numbers_survive_labeled_display_math():
|
|
125
|
+
# normalizing "$${#eq:x}" for wrapping splits it onto two lines, which
|
|
126
|
+
# must not shift the line numbers reported for later prose
|
|
127
|
+
source = (
|
|
128
|
+
"Intro.\n$$\nx = y\n$${#eq:x}\nwhere this sentence ends. "
|
|
129
|
+
"Another starts.\n"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
((line, message),) = markdown_lint_issues_from_text(source, 55)
|
|
133
|
+
|
|
134
|
+
assert line == 5
|
|
135
|
+
assert message.startswith("sentence ends mid-line")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_long_line_fix_splits_at_the_word_after_the_break():
|
|
139
|
+
# "for" also starts the line, but wr breaks before the second one
|
|
140
|
+
source = (
|
|
141
|
+
"for $30\\;\\text{s}$ ($3\\times$ for AOT) with some more words "
|
|
142
|
+
"added.\n"
|
|
143
|
+
)
|
|
144
|
+
message = (
|
|
145
|
+
"line too long: wr would break after '($3\\times$' (before 'for'); "
|
|
146
|
+
"move 'for' onward to the next line, or shorten this sentence"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
fixed, _, _, _ = apply_markdown_issue_fix(source, 1, message)
|
|
150
|
+
|
|
151
|
+
assert fixed.startswith("for $30\\;\\text{s}$ ($3\\times$\nfor AOT)")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_single_trailing_space_is_flagged_and_removed(tmp_path):
|
|
155
|
+
# from RM_ESR: a sentence ending in a stray space
|
|
156
|
+
path = tmp_path / "source.md"
|
|
157
|
+
path.write_text("It melts near 238 K. \nIn contrast, it stays.\n")
|
|
158
|
+
|
|
159
|
+
issues = markdown_lint_issues_from_text(path.read_text(), wrapnumber=55)
|
|
160
|
+
autofix_markdown_file(path)
|
|
161
|
+
|
|
162
|
+
assert (1, "trailing space") in [
|
|
163
|
+
(line, message.split(":")[0]) for line, message in issues
|
|
164
|
+
]
|
|
165
|
+
fixed = path.read_text()
|
|
166
|
+
assert fixed.startswith("It melts near 238 K.\nIn contrast,")
|
|
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
|
+
"\n"
|
|
184
|
+
'{#fig:ProbeMagFieldsDrawing width="4in"}\n',
|
|
185
|
+
# and must not be split across lines (from RM_ESR)
|
|
186
|
+
""
|
|
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
|
+
"\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) == []
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_autofix_fixes_every_line_in_one_pass_from_the_end(tmp_path):
|
|
243
|
+
path = tmp_path / "source.md"
|
|
244
|
+
paragraph = "First sentence ends here. A second sentence begins.\n"
|
|
245
|
+
path.write_text("\n".join([paragraph] * 30))
|
|
246
|
+
|
|
247
|
+
report = autofix_markdown_file(path)
|
|
248
|
+
|
|
249
|
+
fixed = path.read_text()
|
|
250
|
+
assert len(report["fixes"]) == 30
|
|
251
|
+
assert [fix["line"] for fix in report["fixes"]] == list(range(1, 60, 2))
|
|
252
|
+
assert fixed.count("\n\n") == 29
|
|
253
|
+
assert markdown_lint_issues_from_text(fixed, wrapnumber=55) == []
|
|
254
|
+
|
|
255
|
+
|
|
123
256
|
def test_unclosed_math_is_reported_after_fifty_lines():
|
|
124
257
|
short_math = "$$\n" + ("x = y\n" * 48)
|
|
125
258
|
long_math = "$$\n" + ("x = y\n" * 49)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/__version__.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/.jupyter_cache/global.db
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg
RENAMED
|
File without changes
|
{pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/subproject1/independent.qmd
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/subproject1/test_include.qmd
RENAMED
|
File without changes
|
{pydifftools-0.1.38 → pydifftools-0.1.40}/example_notebook/project1/subproject1/tryforerror.qmd
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|