markdowncleaner 0.1.1__py3-none-any.whl
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.
- markdowncleaner/__init__.py +3 -0
- markdowncleaner/cli.py +148 -0
- markdowncleaner/config/__init__.py +0 -0
- markdowncleaner/config/default_cleaning_patterns.yaml +296 -0
- markdowncleaner/config/loader.py +72 -0
- markdowncleaner/markdowncleaner.py +372 -0
- markdowncleaner-0.1.1.dist-info/LICENSE +21 -0
- markdowncleaner-0.1.1.dist-info/METADATA +132 -0
- markdowncleaner-0.1.1.dist-info/RECORD +12 -0
- markdowncleaner-0.1.1.dist-info/WHEEL +5 -0
- markdowncleaner-0.1.1.dist-info/entry_points.txt +2 -0
- markdowncleaner-0.1.1.dist-info/top_level.txt +1 -0
markdowncleaner/cli.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Command-line interface for the markdowncleaner package.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
from markdowncleaner.markdowncleaner import MarkdownCleaner, CleanerOptions
|
|
12
|
+
from markdowncleaner.config.loader import get_default_patterns, CleaningPatterns
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_args(args: Optional[List[str]] = None) -> argparse.Namespace:
|
|
16
|
+
"""Parse command line arguments."""
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
description="Clean and format markdown files by removing unwanted sections, lines, and patterns."
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"input_file",
|
|
23
|
+
type=Path,
|
|
24
|
+
help="Path to the markdown file to clean"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"-o", "--output",
|
|
29
|
+
type=Path,
|
|
30
|
+
help="Path to save the cleaned markdown file (default: input_file with '_cleaned' suffix)"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--output-dir",
|
|
35
|
+
type=Path,
|
|
36
|
+
help="Directory to save the cleaned file (default: same as input file)"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--config",
|
|
41
|
+
type=Path,
|
|
42
|
+
help="Path to custom YAML configuration file with cleaning patterns"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Options for customizing the cleaning process
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--keep-short-lines",
|
|
48
|
+
action="store_true",
|
|
49
|
+
help="Don't remove lines shorter than minimum length"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--min-line-length",
|
|
54
|
+
type=int,
|
|
55
|
+
default=70,
|
|
56
|
+
help="Minimum line length to keep (default: 70)"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--keep-bad-lines",
|
|
61
|
+
action="store_true",
|
|
62
|
+
help="Don't remove lines matching bad line patterns"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--keep-sections",
|
|
67
|
+
action="store_true",
|
|
68
|
+
help="Don't remove sections like References, Acknowledgements, etc."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--keep-footnotes",
|
|
73
|
+
action="store_true",
|
|
74
|
+
help="Don't remove footnote references"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--no-replacements",
|
|
79
|
+
action="store_true",
|
|
80
|
+
help="Don't perform text replacements"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"--keep-inline-patterns",
|
|
85
|
+
action="store_true",
|
|
86
|
+
help="Don't remove inline patterns like citations"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--keep-empty-lines",
|
|
91
|
+
action="store_true",
|
|
92
|
+
help="Don't contract consecutive empty lines"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--no-crimp-linebreaks",
|
|
97
|
+
action="store_true",
|
|
98
|
+
help="Don't crimp linebreaks"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return parser.parse_args(args)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main(args: Optional[List[str]] = None) -> int:
|
|
105
|
+
"""Main entry point for the CLI."""
|
|
106
|
+
parsed_args = parse_args(args)
|
|
107
|
+
|
|
108
|
+
# Configure cleaner options based on arguments
|
|
109
|
+
options = CleanerOptions()
|
|
110
|
+
options.remove_short_lines = not parsed_args.keep_short_lines
|
|
111
|
+
options.min_line_length = parsed_args.min_line_length
|
|
112
|
+
options.remove_whole_lines = not parsed_args.keep_bad_lines
|
|
113
|
+
options.remove_sections = not parsed_args.keep_sections
|
|
114
|
+
options.remove_footnotes_in_text = not parsed_args.keep_footnotes
|
|
115
|
+
options.replace_within_lines = not parsed_args.no_replacements
|
|
116
|
+
options.remove_within_lines = not parsed_args.keep_inline_patterns
|
|
117
|
+
options.contract_empty_lines = not parsed_args.keep_empty_lines
|
|
118
|
+
options.crimp_linebreaks = not parsed_args.no_crimp_linebreaks
|
|
119
|
+
|
|
120
|
+
# Load patterns from custom config or use defaults
|
|
121
|
+
if parsed_args.config:
|
|
122
|
+
try:
|
|
123
|
+
patterns = CleaningPatterns.from_yaml(parsed_args.config)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
print(f"Error loading custom configuration: {e}", file=sys.stderr)
|
|
126
|
+
return 1
|
|
127
|
+
else:
|
|
128
|
+
patterns = get_default_patterns()
|
|
129
|
+
|
|
130
|
+
# Initialize the cleaner with patterns and configured options
|
|
131
|
+
cleaner = MarkdownCleaner(patterns=patterns, options=options)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
# Clean the markdown file
|
|
135
|
+
output_file = cleaner.clean_markdown_file(
|
|
136
|
+
input_file=parsed_args.input_file,
|
|
137
|
+
output_path=parsed_args.output_dir,
|
|
138
|
+
output_file=parsed_args.output
|
|
139
|
+
)
|
|
140
|
+
print(f"Cleaned markdown saved to: {output_file}")
|
|
141
|
+
return 0
|
|
142
|
+
except Exception as e:
|
|
143
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
144
|
+
return 1
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
sys.exit(main())
|
|
File without changes
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
sections_to_remove:
|
|
2
|
+
- |-
|
|
3
|
+
Acknowledge?ments?\s?$
|
|
4
|
+
- Declaration of Competing Interest
|
|
5
|
+
- Declaration of Conflicting Interests
|
|
6
|
+
- Funding
|
|
7
|
+
- Endnotes
|
|
8
|
+
- Credit authorship contribution statement
|
|
9
|
+
- |-
|
|
10
|
+
ACM\s?Reference\?Format
|
|
11
|
+
- Contributors
|
|
12
|
+
- CCS CONCEPTS
|
|
13
|
+
- pdf version of the entry
|
|
14
|
+
- |-
|
|
15
|
+
Correspondence\s?$
|
|
16
|
+
- Funding information
|
|
17
|
+
- ORCID
|
|
18
|
+
- RESEARCH ARTICLE
|
|
19
|
+
- Compliance with Ethical Standards
|
|
20
|
+
- License
|
|
21
|
+
- |-
|
|
22
|
+
Index\s?$
|
|
23
|
+
- |-
|
|
24
|
+
References\s?\.?$
|
|
25
|
+
- |-
|
|
26
|
+
\s?\d{1,2}\.\s*References\s?$
|
|
27
|
+
- |-
|
|
28
|
+
Bibliography\s?
|
|
29
|
+
- |-
|
|
30
|
+
^\s?Notes\s?\:?\.?$
|
|
31
|
+
- Works Cited
|
|
32
|
+
- Author Biographies
|
|
33
|
+
- Author Biography
|
|
34
|
+
- About the author
|
|
35
|
+
- Oxford Scholarship Online
|
|
36
|
+
- University Press Scholarship Online
|
|
37
|
+
- Corresponding author
|
|
38
|
+
- |-
|
|
39
|
+
^Appendix\*
|
|
40
|
+
- Contents
|
|
41
|
+
- Table of Contents
|
|
42
|
+
- List of Figures
|
|
43
|
+
- List of Tables
|
|
44
|
+
- List of Illustrations
|
|
45
|
+
- |-
|
|
46
|
+
^\s?Keywords\s?\.?$
|
|
47
|
+
- Authors' Note
|
|
48
|
+
- |-
|
|
49
|
+
^[a-zA-Z0-9]+-[a-zA-Z0-9]+-?[a-zA-Z0-9]+[^\s]*$
|
|
50
|
+
- |-
|
|
51
|
+
Dedication\:?$
|
|
52
|
+
- Citing this paper
|
|
53
|
+
- General rights
|
|
54
|
+
- Disclosure statement
|
|
55
|
+
- PLEASE SCROLL DOWN FOR ARTICLE
|
|
56
|
+
- About the Contributors
|
|
57
|
+
- List of Key Terms
|
|
58
|
+
- |-
|
|
59
|
+
Citations\:
|
|
60
|
+
- |-
|
|
61
|
+
Provided by\:
|
|
62
|
+
- |-
|
|
63
|
+
Published by\:
|
|
64
|
+
- Preface
|
|
65
|
+
|
|
66
|
+
# Patterns for inline text that should be removed
|
|
67
|
+
bad_inline_patterns:
|
|
68
|
+
- |-
|
|
69
|
+
\[\d{1,3}(?:\s*,\s*\d{1,3})*\]
|
|
70
|
+
- |-
|
|
71
|
+
\(see.*?\)
|
|
72
|
+
- |-
|
|
73
|
+
\(Figure\s*.*?\)
|
|
74
|
+
- |-
|
|
75
|
+
\(Table\s*.*?\)
|
|
76
|
+
- |-
|
|
77
|
+
\(Section\s*.*?\)
|
|
78
|
+
- |-
|
|
79
|
+
\(Chapter\s*.*?\)
|
|
80
|
+
- |-
|
|
81
|
+
\* 1 ,
|
|
82
|
+
- |-
|
|
83
|
+
\(p\.?\d{1,3}\)
|
|
84
|
+
|
|
85
|
+
# Patterns for entire lines that should be removed
|
|
86
|
+
bad_lines_patterns:
|
|
87
|
+
# Copyright and legal
|
|
88
|
+
- |-
|
|
89
|
+
(?m)^.*?\b[Aa]ll [Rr]ights [Rr]eserved\..*?$|.*?\b[Aa]ll [Rr]ights [Rr]eserved\.$
|
|
90
|
+
- |-
|
|
91
|
+
Copyright \d{4} by
|
|
92
|
+
- |-
|
|
93
|
+
^#{1,3} ORIGINAL RESEARCH$
|
|
94
|
+
- |-
|
|
95
|
+
^#{1,3} ORIGINAL ARTICLE$
|
|
96
|
+
- |-
|
|
97
|
+
^[A-Z\s]+$
|
|
98
|
+
- |-
|
|
99
|
+
<!--.*?-->
|
|
100
|
+
- |-
|
|
101
|
+
^- (\d{1,3})?\.
|
|
102
|
+
- |-
|
|
103
|
+
^Received:
|
|
104
|
+
- |-
|
|
105
|
+
^\(Received
|
|
106
|
+
- |-
|
|
107
|
+
Received.*?(\d{4}).*?(accepted|Accepted).*
|
|
108
|
+
- |-
|
|
109
|
+
doi:
|
|
110
|
+
- |-
|
|
111
|
+
10\.\d{4,9}/[-._;()/:\w]+
|
|
112
|
+
- Digital Object Identifier
|
|
113
|
+
- |-
|
|
114
|
+
^[Kk]eywords
|
|
115
|
+
- |-
|
|
116
|
+
^Conflict of Interest
|
|
117
|
+
- ^Library of Congress
|
|
118
|
+
- ^Open Access
|
|
119
|
+
- ^OpenAccess
|
|
120
|
+
- ^Department of
|
|
121
|
+
- |-
|
|
122
|
+
^[A-Z][a-zA-Z]+ University Press, Inc
|
|
123
|
+
- |-
|
|
124
|
+
^Author\(s\):
|
|
125
|
+
- ^Acknowledgements
|
|
126
|
+
- ^Acknowledgments
|
|
127
|
+
- ^Funding Statement
|
|
128
|
+
- ^Table of Contents
|
|
129
|
+
- |-
|
|
130
|
+
^Source:
|
|
131
|
+
- |-
|
|
132
|
+
^Published by
|
|
133
|
+
- |-
|
|
134
|
+
^CCS Concepts
|
|
135
|
+
- ISSN
|
|
136
|
+
- ISBN
|
|
137
|
+
- |-
|
|
138
|
+
arxiv:
|
|
139
|
+
- JSTOR
|
|
140
|
+
- Contributed Paper
|
|
141
|
+
- |-
|
|
142
|
+
^Fig\.
|
|
143
|
+
- |-
|
|
144
|
+
^\s?Figure\s?\d{1,2}\s?[.:]
|
|
145
|
+
- |-
|
|
146
|
+
^\s?Table\s?\d{1,2}\s?[.:]
|
|
147
|
+
- ^First published
|
|
148
|
+
- |-
|
|
149
|
+
National Science Foundation\s*\(NSF\).*?[Gg]rant.*?\d{4,}
|
|
150
|
+
- |-
|
|
151
|
+
[Gg]rant\s*[Nn]umber\s*\d{4,}
|
|
152
|
+
- |-
|
|
153
|
+
[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}
|
|
154
|
+
- |-
|
|
155
|
+
\{\s*[\w,\s]+\s*\}\s*@\s*[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}
|
|
156
|
+
- |-
|
|
157
|
+
https?://?[^\s]+
|
|
158
|
+
- ©
|
|
159
|
+
- ®
|
|
160
|
+
- ™
|
|
161
|
+
- |-
|
|
162
|
+
c \d{4}
|
|
163
|
+
- |-
|
|
164
|
+
^\d{1,3}\s?.\s?[Ss]ee
|
|
165
|
+
- |-
|
|
166
|
+
^\-\s?\d{1,3}\s?.\s?[Ss]ee
|
|
167
|
+
- |-
|
|
168
|
+
^\d{1,2}[A-Za-z]+
|
|
169
|
+
- |-
|
|
170
|
+
^#{1,3}[\d\s]+$
|
|
171
|
+
- |-
|
|
172
|
+
^[^/]*(/[^/]*){5,}
|
|
173
|
+
- †
|
|
174
|
+
- ‡
|
|
175
|
+
- |-
|
|
176
|
+
^\d{1,3}.*supra note
|
|
177
|
+
- |-
|
|
178
|
+
^\d{1,3}.*\(\d{4}\)
|
|
179
|
+
- |-
|
|
180
|
+
^\d{1,3}.*cf
|
|
181
|
+
- |-
|
|
182
|
+
^\d{1,3}.\s?Id\.
|
|
183
|
+
- |-
|
|
184
|
+
(?:.*?GLYPH\<){3,}.*
|
|
185
|
+
- on previous versions of this
|
|
186
|
+
- |-
|
|
187
|
+
^We are grateful for
|
|
188
|
+
- |-
|
|
189
|
+
^I am grateful
|
|
190
|
+
- |-
|
|
191
|
+
^Subject:
|
|
192
|
+
- open access article under the terms of the Creative Commons
|
|
193
|
+
- |-
|
|
194
|
+
Key words:
|
|
195
|
+
- |-
|
|
196
|
+
Published online:
|
|
197
|
+
- |-
|
|
198
|
+
Correspondence:
|
|
199
|
+
- |-
|
|
200
|
+
Print Publication Date:
|
|
201
|
+
- |-
|
|
202
|
+
Accepted:
|
|
203
|
+
- |-
|
|
204
|
+
This article was downloaded by:
|
|
205
|
+
- This content downloaded from
|
|
206
|
+
- |-
|
|
207
|
+
Registered office:
|
|
208
|
+
- |-
|
|
209
|
+
DATE DOWNLOADED:
|
|
210
|
+
- NBER working papers are circulated
|
|
211
|
+
- FirstView Article
|
|
212
|
+
- Published in
|
|
213
|
+
- Published online
|
|
214
|
+
- Typeset by
|
|
215
|
+
- All rights reserved
|
|
216
|
+
- No portion of this publication may be reproduced
|
|
217
|
+
- authors have asserted their rights to be identified as the authors
|
|
218
|
+
- material in this work has been asserted in accordance
|
|
219
|
+
- imprint of Macmillan Publishers
|
|
220
|
+
- A catalogue record for this book is available
|
|
221
|
+
- Your use of this HeinOnline
|
|
222
|
+
- obtain permission to use this article
|
|
223
|
+
- This article is published with open access
|
|
224
|
+
- Links to third party websites
|
|
225
|
+
- Registered in England and Wales
|
|
226
|
+
- |-
|
|
227
|
+
\*I thank
|
|
228
|
+
- |-
|
|
229
|
+
\*We thank
|
|
230
|
+
- |-
|
|
231
|
+
\*For comments
|
|
232
|
+
- |-
|
|
233
|
+
\*I would like to thank
|
|
234
|
+
- |-
|
|
235
|
+
\*We would like to thank
|
|
236
|
+
- |-
|
|
237
|
+
\*Thanks to
|
|
238
|
+
- |-
|
|
239
|
+
\*\s?This article is
|
|
240
|
+
- I am grateful for
|
|
241
|
+
- Oxford University Press is a department of
|
|
242
|
+
- a registered trademark of
|
|
243
|
+
- Published in the United States
|
|
244
|
+
- Printed in the United States
|
|
245
|
+
- The paper used in this publication
|
|
246
|
+
- Trademark notice
|
|
247
|
+
- 1 Department of
|
|
248
|
+
- catalogue recordfor this publication
|
|
249
|
+
- An earlier version of this paper was
|
|
250
|
+
- Springer International Publishing AG
|
|
251
|
+
- part of Springer Nature
|
|
252
|
+
- This work is subject to copyright.
|
|
253
|
+
- permission to reuse the copyright
|
|
254
|
+
- distributed under the terms of a Creative Commons
|
|
255
|
+
- |-
|
|
256
|
+
, [A-Z]{2} \d{5}(?:-\d{4})?,
|
|
257
|
+
|
|
258
|
+
# Footnotes
|
|
259
|
+
# Pattern matches:
|
|
260
|
+
# 1. A period
|
|
261
|
+
# 2. Optional space
|
|
262
|
+
# 3. 1-3 digits
|
|
263
|
+
# 4. Optionally followed by comma and space and more 1-3 digit numbers
|
|
264
|
+
# 5. This pattern repeats for any number of comma-separated numbers
|
|
265
|
+
# 6. Followed by either end of string, whitespace, or newline
|
|
266
|
+
footnote_patterns:
|
|
267
|
+
- |-
|
|
268
|
+
\.\s?(\d{1,3}(?:,\s*\d{1,3})*?)(?=\s|$)
|
|
269
|
+
|
|
270
|
+
replacements:
|
|
271
|
+
'GLYPH<28>': 'fi'
|
|
272
|
+
'GLYPH<29>': 'fl'
|
|
273
|
+
'GLYPH<3>': '*'
|
|
274
|
+
'GLYPH<21>': '-'
|
|
275
|
+
'GLYPH<22>': '—'
|
|
276
|
+
'GLYPH<15>': '-'
|
|
277
|
+
'GLYPH<129>': '-'
|
|
278
|
+
'GLYPH<133>': 'fi'
|
|
279
|
+
'GLYPH<28>': 'fi'
|
|
280
|
+
'GLYPH<29>': 'fl'
|
|
281
|
+
'GLYPH<3>': '*'
|
|
282
|
+
'GLYPH<21>': '-'
|
|
283
|
+
'GLYPH<22>': '—'
|
|
284
|
+
'GLYPH<15>': '•'
|
|
285
|
+
'/uniFB01': 'fi'
|
|
286
|
+
'/uniFB03': 'ffi'
|
|
287
|
+
'/uniFB00': 'ff'
|
|
288
|
+
'/uniFB02': 'fl'
|
|
289
|
+
'GLYPH<146>': 'Š'
|
|
290
|
+
'GLYPH<228>': 'ä'
|
|
291
|
+
'GLYPH<252>': 'ü'
|
|
292
|
+
'GLYPH<237>': 'í'
|
|
293
|
+
'GLYPH<2>': 'Š'
|
|
294
|
+
'/ornm58.ornm': ''
|
|
295
|
+
'/ornm69.ornm': ''
|
|
296
|
+
'&': '&'
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import yaml
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Pattern, Optional, Dict
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
_logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class CleaningPatterns:
|
|
12
|
+
"""Container for document cleaning patterns."""
|
|
13
|
+
sections_to_remove: List[str]
|
|
14
|
+
bad_inline_patterns: List[Pattern]
|
|
15
|
+
bad_lines_patterns: List[Pattern]
|
|
16
|
+
footnote_patterns: List[Pattern]
|
|
17
|
+
replacements: Dict[Pattern | str, str]
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_yaml(cls, yaml_path: Path) -> 'CleaningPatterns':
|
|
21
|
+
"""
|
|
22
|
+
Create a CleaningPatterns instance from a YAML file.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
yaml_path: Path to the YAML configuration file
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
CleaningPatterns instance with compiled regex patterns
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
with open(yaml_path, 'r', encoding='utf-8') as f:
|
|
32
|
+
config = yaml.safe_load(f)
|
|
33
|
+
|
|
34
|
+
def compile_pattern(pattern: str) -> Pattern:
|
|
35
|
+
"""Safely compile a regex pattern."""
|
|
36
|
+
if not isinstance(pattern, str):
|
|
37
|
+
raise TypeError(f"Pattern must be a string, got {type(pattern)}: {pattern}")
|
|
38
|
+
try:
|
|
39
|
+
return re.compile(pattern, re.IGNORECASE)
|
|
40
|
+
except re.error as e:
|
|
41
|
+
_logger.error(f"Invalid regex pattern '{pattern}': {e}")
|
|
42
|
+
raise
|
|
43
|
+
|
|
44
|
+
# Ensure patterns are strings before compiling
|
|
45
|
+
sections = [str(p).strip() for p in config.get('sections_to_remove', [])]
|
|
46
|
+
inline_patterns = [str(p).strip() for p in config.get('bad_inline_patterns', [])]
|
|
47
|
+
line_patterns = [str(p).strip() for p in config.get('bad_lines_patterns', [])]
|
|
48
|
+
footnotes = [str(p).strip() for p in config.get('footnote_patterns', [])]
|
|
49
|
+
replace = {str(k).strip(): str(v).strip() for k, v in config.get('replacements', {}).items()}
|
|
50
|
+
|
|
51
|
+
return cls(
|
|
52
|
+
sections_to_remove=sections,
|
|
53
|
+
bad_inline_patterns=[compile_pattern(p) for p in inline_patterns],
|
|
54
|
+
bad_lines_patterns=[compile_pattern(p) for p in line_patterns],
|
|
55
|
+
footnote_patterns=[compile_pattern(p) for p in footnotes],
|
|
56
|
+
replacements=replace # just taking this a str dict, no patterns to compile
|
|
57
|
+
)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
_logger.error(f"Error loading cleaning patterns from {yaml_path}: {e}")
|
|
60
|
+
raise
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_cached_patterns: Optional[CleaningPatterns] = None
|
|
64
|
+
|
|
65
|
+
def get_default_patterns() -> CleaningPatterns:
|
|
66
|
+
"""Load the default cleaning patterns from the package's configuration file."""
|
|
67
|
+
global _cached_patterns
|
|
68
|
+
if _cached_patterns is None:
|
|
69
|
+
config_dir = Path(__file__).parent
|
|
70
|
+
yaml_path = config_dir / 'default_cleaning_patterns.yaml'
|
|
71
|
+
_cached_patterns = CleaningPatterns.from_yaml(yaml_path)
|
|
72
|
+
return _cached_patterns
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Pattern, Optional
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
import ftfy
|
|
7
|
+
from markdowncleaner.config.loader import get_default_patterns, CleaningPatterns
|
|
8
|
+
|
|
9
|
+
_logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class CleanerOptions:
|
|
13
|
+
"""Countainer for Cleaner options"""
|
|
14
|
+
remove_short_lines: bool = True
|
|
15
|
+
min_line_length: int = 70
|
|
16
|
+
remove_whole_lines: bool = True
|
|
17
|
+
remove_sections: bool = True
|
|
18
|
+
remove_footnotes_in_text: bool = True
|
|
19
|
+
replace_within_lines: bool = True
|
|
20
|
+
remove_within_lines: bool = True
|
|
21
|
+
contract_empty_lines: bool = True
|
|
22
|
+
crimp_linebreaks: bool = True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MarkdownCleaner:
|
|
26
|
+
"""Class to handle markdown document cleaning operations."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, patterns: CleaningPatterns = None, options: Optional[CleanerOptions] = None):
|
|
29
|
+
"""
|
|
30
|
+
Initialize the cleaner with patterns.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
patterns: CleaningPatterns instance, or None to use defaults
|
|
34
|
+
"""
|
|
35
|
+
self.patterns = patterns or get_default_patterns()
|
|
36
|
+
self.options = options or CleanerOptions()
|
|
37
|
+
|
|
38
|
+
def clean_markdown_file(self,
|
|
39
|
+
input_file: Path,
|
|
40
|
+
output_path: Optional[Path] = None,
|
|
41
|
+
output_file: Optional[Path | str] = None) -> Path:
|
|
42
|
+
"""
|
|
43
|
+
Clean a markdown file using the configured patterns.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
input_file: Path to the input markdown file
|
|
47
|
+
output_path: Optional directory to save the cleaned file
|
|
48
|
+
output_file: Optional path to save the cleaned file directly (overrides output_path)
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Path: The path to the cleaned output file
|
|
52
|
+
"""
|
|
53
|
+
# Read the content of the input file
|
|
54
|
+
with open(input_file, 'r', encoding='utf-8') as file:
|
|
55
|
+
cleaned_content = file.read()
|
|
56
|
+
|
|
57
|
+
# Apply cleaning operations
|
|
58
|
+
cleaned_content = self.clean_markdown_string(cleaned_content)
|
|
59
|
+
|
|
60
|
+
if output_file is not None:
|
|
61
|
+
cleaned_filepath = Path(output_file)
|
|
62
|
+
# Ensure parent directory exists
|
|
63
|
+
cleaned_filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
elif output_path is not None:
|
|
65
|
+
# Determine the output filepath
|
|
66
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
cleaned_filepath = output_path / input_file.name
|
|
68
|
+
else: # both output_path and output_file are None
|
|
69
|
+
# Generate a new filename with "_cleaned" suffix
|
|
70
|
+
cleaned_filename = f"{input_file.stem}_cleaned{input_file.suffix}"
|
|
71
|
+
cleaned_filepath = input_file.parent / cleaned_filename
|
|
72
|
+
# Ensure parent directory exists (might be unnecessary if same as input, but added for safety)
|
|
73
|
+
cleaned_filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
|
|
75
|
+
# Write the cleaned content
|
|
76
|
+
with open(cleaned_filepath, 'w', encoding='utf-8') as file:
|
|
77
|
+
file.write(cleaned_content)
|
|
78
|
+
|
|
79
|
+
_logger.info(f"Cleaned file saved to: {cleaned_filepath}")
|
|
80
|
+
|
|
81
|
+
return cleaned_filepath
|
|
82
|
+
|
|
83
|
+
def clean_markdown_string(self, content: str) -> str:
|
|
84
|
+
"""Apply all cleaning operations to the content."""
|
|
85
|
+
|
|
86
|
+
# Apply all default ftfy fixes if mojibake is detected
|
|
87
|
+
if ftfy.is_bad(content):
|
|
88
|
+
content = ftfy.fix_text(content)
|
|
89
|
+
|
|
90
|
+
# Reduce two or more subsequent spaces to a single space
|
|
91
|
+
content = re.sub(r' {2,}', ' ', content)
|
|
92
|
+
|
|
93
|
+
# Normalize quotes
|
|
94
|
+
content = self._normalize_quotation_symbols(content)
|
|
95
|
+
|
|
96
|
+
# Remove lines shorter than min_line_length (default: 70 characters)
|
|
97
|
+
if self.options.remove_short_lines:
|
|
98
|
+
content = self._remove_short_lines(content, self.options.min_line_length)
|
|
99
|
+
# Clean out "bad" lines
|
|
100
|
+
if self.options.remove_whole_lines:
|
|
101
|
+
content = self._remove_whole_lines(content, self.patterns.bad_lines_patterns)
|
|
102
|
+
|
|
103
|
+
# Remove unwanted sections
|
|
104
|
+
if self.options.remove_sections:
|
|
105
|
+
for title in self.patterns.sections_to_remove:
|
|
106
|
+
content = self._remove_sections(content, title)
|
|
107
|
+
|
|
108
|
+
# Replace strings for string
|
|
109
|
+
if self.options.replace_within_lines:
|
|
110
|
+
for k, v in self.patterns.replacements.items():
|
|
111
|
+
content = self._replace_within_lines(content, k, v)
|
|
112
|
+
|
|
113
|
+
# Replace footnote pattern (numbers at end of sentence) with '.'
|
|
114
|
+
if self.options.remove_footnotes_in_text:
|
|
115
|
+
content = self._replace_within_lines(content, self.patterns.footnote_patterns, '.')
|
|
116
|
+
|
|
117
|
+
# Remove remaining unwanted inline patterns (some may have been replaced by replacements)
|
|
118
|
+
if self.options.remove_within_lines:
|
|
119
|
+
content = self._replace_within_lines(content, self.patterns.bad_inline_patterns, '')
|
|
120
|
+
|
|
121
|
+
# Clean up formatting
|
|
122
|
+
if self.options.crimp_linebreaks:
|
|
123
|
+
content = self._crimp_linebreaks(content)
|
|
124
|
+
if self.options.contract_empty_lines:
|
|
125
|
+
content = self._contract_empty_lines(content)
|
|
126
|
+
|
|
127
|
+
return content
|
|
128
|
+
|
|
129
|
+
def _normalize_quotation_symbols(self, text: str) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Normalizes quotation symbols in the input text.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
text (str): Input text to clean
|
|
135
|
+
Returns:
|
|
136
|
+
str: Text with all single and double quotation symbols replaced with standard ones.
|
|
137
|
+
"""
|
|
138
|
+
double_quotes = [
|
|
139
|
+
"«",
|
|
140
|
+
"‹",
|
|
141
|
+
"»",
|
|
142
|
+
"›",
|
|
143
|
+
"„",
|
|
144
|
+
"“",
|
|
145
|
+
"‟",
|
|
146
|
+
"”",
|
|
147
|
+
"❝",
|
|
148
|
+
"❞",
|
|
149
|
+
"❮",
|
|
150
|
+
"❯",
|
|
151
|
+
"〝",
|
|
152
|
+
"〞",
|
|
153
|
+
"〟",
|
|
154
|
+
""",
|
|
155
|
+
]
|
|
156
|
+
single_quotes = ["‘", "‛", "’", "❛", "❜", "`", "´", "‘", "’"]
|
|
157
|
+
|
|
158
|
+
double_quotes_regex = re.compile("|".join(double_quotes))
|
|
159
|
+
single_quotes_regex = re.compile("|".join(single_quotes))
|
|
160
|
+
|
|
161
|
+
text = single_quotes_regex.sub("'", text)
|
|
162
|
+
text = double_quotes_regex.sub('"', text)
|
|
163
|
+
|
|
164
|
+
return text
|
|
165
|
+
|
|
166
|
+
def _replace_within_lines(self, text: str, patterns: str | Pattern | list[str | Pattern], replacement: str = '') -> str:
|
|
167
|
+
"""
|
|
168
|
+
Removes multiple patterns from text, applying them sequentially.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
text (str): Input text to clean
|
|
172
|
+
patterns (str | Pattern | list): Single regex pattern or list of regex patterns to remove
|
|
173
|
+
replacement (str, optional): String to replace the matched patterns with. Defaults to ''.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
str: Text with all patterns removed or replaced
|
|
177
|
+
"""
|
|
178
|
+
# Ensure patterns is a list, converting single pattern to a list if needed
|
|
179
|
+
if not isinstance(patterns, list):
|
|
180
|
+
patterns = [patterns]
|
|
181
|
+
|
|
182
|
+
cleaned_text = text
|
|
183
|
+
for pattern in patterns:
|
|
184
|
+
# Make sure each pattern is a compiled regex
|
|
185
|
+
if isinstance(pattern, str):
|
|
186
|
+
pattern = re.compile(pattern)
|
|
187
|
+
|
|
188
|
+
cleaned_text = pattern.sub(replacement, cleaned_text)
|
|
189
|
+
return cleaned_text
|
|
190
|
+
|
|
191
|
+
def _remove_short_lines(self, multiline_string: str, length: int = 70) -> str:
|
|
192
|
+
"""
|
|
193
|
+
Remove lines from a multiline string that are shorter than a specified length.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
multiline_string: The input string to clean
|
|
197
|
+
length: The minimum length of lines to keep
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Cleaned string with matching lines removed
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
# Split the content into lines
|
|
204
|
+
lines = multiline_string.splitlines()
|
|
205
|
+
|
|
206
|
+
# Filter out lines that are shorter than length but that are neither empty nor start with '#'
|
|
207
|
+
filtered_lines = []
|
|
208
|
+
for line in lines:
|
|
209
|
+
if not line.strip() == '' and not line.startswith('#') and len(line) < length:
|
|
210
|
+
continue
|
|
211
|
+
filtered_lines.append(line)
|
|
212
|
+
|
|
213
|
+
# Join the remaining lines back into a single string
|
|
214
|
+
return '\n'.join(filtered_lines)
|
|
215
|
+
|
|
216
|
+
def _remove_whole_lines(self, multiline_string: str, patterns: str | Pattern | list[str | Pattern]) -> str:
|
|
217
|
+
"""
|
|
218
|
+
Remove lines from a multiline string that match specified regex pattern(s).
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
multiline_string: The input string to clean
|
|
222
|
+
patterns: A single regex pattern (str or compiled Pattern) or a list of patterns
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Cleaned string with matching lines removed
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
# Split the content into lines
|
|
229
|
+
lines = multiline_string.splitlines()
|
|
230
|
+
|
|
231
|
+
# Make sure patterns is a list
|
|
232
|
+
if not isinstance(patterns, list):
|
|
233
|
+
patterns = [patterns]
|
|
234
|
+
# Filter out lines that match any of the patterns, except empty lines
|
|
235
|
+
filtered_lines = []
|
|
236
|
+
for line in lines:
|
|
237
|
+
if line.strip() == '': # Keep empty lines
|
|
238
|
+
filtered_lines.append(line)
|
|
239
|
+
continue
|
|
240
|
+
if any(pattern.search(line) for pattern in patterns):
|
|
241
|
+
continue
|
|
242
|
+
filtered_lines.append(line)
|
|
243
|
+
|
|
244
|
+
# Join the remaining lines back into a single string
|
|
245
|
+
return '\n'.join(filtered_lines)
|
|
246
|
+
|
|
247
|
+
def _contract_empty_lines(self, multiline_string: str) -> str:
|
|
248
|
+
"""Contract two or more consecutive empty lines from a multiline string."""
|
|
249
|
+
lines = multiline_string.splitlines()
|
|
250
|
+
result = []
|
|
251
|
+
prev_empty = False
|
|
252
|
+
|
|
253
|
+
for line in lines:
|
|
254
|
+
is_empty = not line.strip()
|
|
255
|
+
if not (is_empty and prev_empty):
|
|
256
|
+
result.append(line)
|
|
257
|
+
prev_empty = is_empty
|
|
258
|
+
|
|
259
|
+
return '\n'.join(result)
|
|
260
|
+
|
|
261
|
+
def _remove_sections(self, markdown_text: str, section_pattern: str) -> str:
|
|
262
|
+
"""
|
|
263
|
+
Removes a first or second level section from a markdown document if its title
|
|
264
|
+
matches the given regular expression pattern.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
markdown_text (str): The input markdown text
|
|
268
|
+
section_pattern (str): Regular expression pattern to match the section title
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
str: The markdown text with the matching section removed
|
|
272
|
+
"""
|
|
273
|
+
# Compile the pattern with IGNORECASE flag
|
|
274
|
+
pattern = re.compile(section_pattern, re.IGNORECASE)
|
|
275
|
+
|
|
276
|
+
# Split the markdown into lines
|
|
277
|
+
lines = markdown_text.splitlines()
|
|
278
|
+
result_lines = []
|
|
279
|
+
|
|
280
|
+
i = 0
|
|
281
|
+
while i < len(lines):
|
|
282
|
+
line = lines[i].strip()
|
|
283
|
+
|
|
284
|
+
# Check if line is a first or second level heading
|
|
285
|
+
if line.startswith(('#', '##')):
|
|
286
|
+
# Extract the heading text
|
|
287
|
+
heading_text = line.lstrip('#').strip() #.lower()
|
|
288
|
+
|
|
289
|
+
# Check if heading matches the pattern
|
|
290
|
+
if pattern.search(heading_text):
|
|
291
|
+
# Skip this heading and find the end of its section
|
|
292
|
+
i += 1
|
|
293
|
+
section_level = line.count('#')
|
|
294
|
+
|
|
295
|
+
# Continue until we find a heading of same or higher level, or end of document
|
|
296
|
+
while i < len(lines):
|
|
297
|
+
next_line = lines[i].strip()
|
|
298
|
+
if next_line.startswith('#'):
|
|
299
|
+
next_level = next_line.count('#')
|
|
300
|
+
if next_level <= section_level:
|
|
301
|
+
break
|
|
302
|
+
i += 1
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
result_lines.append(lines[i])
|
|
306
|
+
i += 1
|
|
307
|
+
|
|
308
|
+
# Return the modified markdown, preserving original line endings
|
|
309
|
+
return '\n'.join(result_lines)
|
|
310
|
+
|
|
311
|
+
def _crimp_linebreaks(self, markdown_text: str) -> str:
|
|
312
|
+
"""
|
|
313
|
+
Fix line break errors in markdown text converted from PDF.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
markdown_text (str): Input markdown text with potential line break errors
|
|
317
|
+
|
|
318
|
+
The function handles two cases:
|
|
319
|
+
1. Hyphenated words split across lines (even with one empty line in between)
|
|
320
|
+
2. Paragraphs incorrectly split by empty lines when a line ends with a letter
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
str: Text with all patterns removed
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
lines = markdown_text.splitlines()
|
|
327
|
+
result_lines = []
|
|
328
|
+
i = 0
|
|
329
|
+
|
|
330
|
+
while i < len(lines):
|
|
331
|
+
current_line = lines[i].strip()
|
|
332
|
+
|
|
333
|
+
# Try to join as many consecutive lines as possible
|
|
334
|
+
while True:
|
|
335
|
+
joined = False
|
|
336
|
+
|
|
337
|
+
# Case 1: Handle hyphenated words
|
|
338
|
+
if current_line.endswith('-'):
|
|
339
|
+
j = i + 1
|
|
340
|
+
while j < len(lines) and not lines[j].strip():
|
|
341
|
+
j += 1
|
|
342
|
+
|
|
343
|
+
if j < len(lines) and lines[j].strip() and lines[j].strip()[0].islower():
|
|
344
|
+
current_line = current_line[:-1] + lines[j].strip()
|
|
345
|
+
i = j # Update i to the last joined line
|
|
346
|
+
joined = True
|
|
347
|
+
continue # Skip to next join check
|
|
348
|
+
|
|
349
|
+
# Case 2: Handle paragraph merging
|
|
350
|
+
if not current_line.startswith('#') and current_line and (current_line[-1].isalpha() or current_line[-1] in ',;\'\"'):
|
|
351
|
+
_logger.debug(f'Crimping line:... {current_line[-50:]}')
|
|
352
|
+
j = i + 1
|
|
353
|
+
while j < len(lines) and not lines[j].strip():
|
|
354
|
+
j += 1
|
|
355
|
+
|
|
356
|
+
if j < len(lines) and lines[j].strip() and \
|
|
357
|
+
not lines[j].strip().startswith('#') and \
|
|
358
|
+
not lines[j].strip().startswith('*') and \
|
|
359
|
+
not lines[j].strip().startswith('-'):
|
|
360
|
+
current_line = current_line + ' ' + lines[j].strip()
|
|
361
|
+
i = j # Update i to the last joined line
|
|
362
|
+
joined = True # noqa: F841
|
|
363
|
+
continue # Skip to next join check
|
|
364
|
+
|
|
365
|
+
# If no joins were made, break the loop
|
|
366
|
+
break
|
|
367
|
+
|
|
368
|
+
# Add the fully processed line to results
|
|
369
|
+
result_lines.append(current_line)
|
|
370
|
+
i += 1 # Move to the next line
|
|
371
|
+
|
|
372
|
+
return '\n'.join(result_lines)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Johannes Himmelreich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: markdowncleaner
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A tool for cleaning and formatting markdown documents
|
|
5
|
+
Author-email: Johannes Himmelreich <jrhimmel@syr.edu>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/josk0/markdowncleaner
|
|
8
|
+
Project-URL: Issues, https://github.com/josk0/markdowncleaner/issues
|
|
9
|
+
Keywords: markdown,cleaning,formatting,text processing
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Classifier: Development Status :: 4 - Beta
|
|
22
|
+
Classifier: Intended Audience :: Science/Research
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: pyyaml>=6.0
|
|
27
|
+
Requires-Dist: ftfy>=6.0.3
|
|
28
|
+
|
|
29
|
+
# markdowncleaner
|
|
30
|
+
|
|
31
|
+
A simple Python tool for cleaning and formatting markdown documents. Default configuration with regex patterns for PDFs of academic papers that have been converted to markdown.
|
|
32
|
+
|
|
33
|
+
## Description
|
|
34
|
+
|
|
35
|
+
`markdowncleaner` helps you clean up markdown files by removing unwanted content such as:
|
|
36
|
+
- References, bibliographies, and citations
|
|
37
|
+
- Footnotes and endnotes
|
|
38
|
+
- Copyright notices and legal disclaimers
|
|
39
|
+
- Acknowledgements and funding information
|
|
40
|
+
- Author information and contact details
|
|
41
|
+
- Specific patterns like DOIs, URLs, and email addresses
|
|
42
|
+
- Short lines and excessive whitespace
|
|
43
|
+
|
|
44
|
+
This tool is particularly useful for processing academic papers, books, or any markdown document that needs formatting cleanup.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install markdowncleaner
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### Basic Usage
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from markdowncleaner import MarkdownCleaner
|
|
58
|
+
from pathlib import Path
|
|
59
|
+
|
|
60
|
+
# Create a cleaner with default patterns
|
|
61
|
+
cleaner = MarkdownCleaner()
|
|
62
|
+
|
|
63
|
+
# Clean a markdown file
|
|
64
|
+
result_path = cleaner.clean_markdown_file(Path("input.md"))
|
|
65
|
+
|
|
66
|
+
# Clean a markdown string
|
|
67
|
+
text = "# Title\nSome content here. [1]\n\nReferences\n1. Citation"
|
|
68
|
+
cleaned_text = cleaner.clean_markdown_string(text)
|
|
69
|
+
print(cleaned_text)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Customizing Cleaning Options
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from markdowncleaner import MarkdownCleaner, CleanerOptions
|
|
76
|
+
|
|
77
|
+
# Create custom options
|
|
78
|
+
options = CleanerOptions()
|
|
79
|
+
options.remove_short_lines = True
|
|
80
|
+
options.min_line_length = 50 # custom minimum line length
|
|
81
|
+
options.remove_footnotes_in_text = True
|
|
82
|
+
options.contract_empty_lines = True
|
|
83
|
+
|
|
84
|
+
# Initialize cleaner with custom options
|
|
85
|
+
cleaner = MarkdownCleaner(options=options)
|
|
86
|
+
|
|
87
|
+
# Use the cleaner as before
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Custom Cleaning Patterns
|
|
91
|
+
|
|
92
|
+
You can also provide custom cleaning patterns:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from markdowncleaner import MarkdownCleaner, CleaningPatterns
|
|
96
|
+
from pathlib import Path
|
|
97
|
+
|
|
98
|
+
# Load custom patterns from a YAML file
|
|
99
|
+
custom_patterns = CleaningPatterns.from_yaml(Path("my_patterns.yaml"))
|
|
100
|
+
|
|
101
|
+
# Initialize cleaner with custom patterns
|
|
102
|
+
cleaner = MarkdownCleaner(patterns=custom_patterns)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Configuration
|
|
106
|
+
|
|
107
|
+
The default cleaning patterns are defined in `default_cleaning_patterns.yaml` and include:
|
|
108
|
+
|
|
109
|
+
- **Sections to Remove**: Acknowledgements, References, Bibliography, etc.
|
|
110
|
+
- **Bad Inline Patterns**: Citations, figure references, etc.
|
|
111
|
+
- **Bad Lines Patterns**: Copyright notices, DOIs, URLs, etc.
|
|
112
|
+
- **Footnote Patterns**: Footnote references in text that fit the pattern '.1'
|
|
113
|
+
- **Replacements**: Various character replacements for PDF parsing errors
|
|
114
|
+
|
|
115
|
+
## Options
|
|
116
|
+
|
|
117
|
+
- `remove_short_lines`: Remove lines shorter than `min_line_length` (default: 70 characters)
|
|
118
|
+
- `remove_whole_lines`: Remove lines matching specific patterns
|
|
119
|
+
- `remove_sections`: Remove entire sections based on section headings
|
|
120
|
+
- `remove_footnotes_in_text`: Remove footnote references
|
|
121
|
+
- `replace_within_lines`: Replace specific patterns within lines
|
|
122
|
+
- `remove_within_lines`: Remove specific patterns within lines
|
|
123
|
+
- `contract_empty_lines`: Normalize whitespace
|
|
124
|
+
- `crimp_linebreaks`: Improve line break formatting
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT License
|
|
129
|
+
|
|
130
|
+
## Contributing
|
|
131
|
+
|
|
132
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
markdowncleaner/__init__.py,sha256=5P_nDL6T2WXoX2jVEtf-3fpGb-rEG1A_Bo-H0-upQFs,140
|
|
2
|
+
markdowncleaner/cli.py,sha256=qpqEl3AL0ekQe9JoMljuEZQQlMRnEH9kFz7np7wF_64,4356
|
|
3
|
+
markdowncleaner/markdowncleaner.py,sha256=wLWOBw_Oy1buw1Av7KvDDaN1V9w5gqvM27CMS9SzF3c,14150
|
|
4
|
+
markdowncleaner/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
markdowncleaner/config/default_cleaning_patterns.yaml,sha256=VI2tDtgHelJbEMUVR9dUUlXqagfsIpS86_Zss-I6Qsg,6005
|
|
6
|
+
markdowncleaner/config/loader.py,sha256=SX9ZgsGFV-25sFCeTy9XW5UQARz3EeaUhrVHu2Y-XeU,2927
|
|
7
|
+
markdowncleaner-0.1.1.dist-info/LICENSE,sha256=yH2dxfGn0hRK5hPAnNFarbaFq1jx8wrPdqy-nsMi2II,1076
|
|
8
|
+
markdowncleaner-0.1.1.dist-info/METADATA,sha256=8SXdYIt669JCe07MsNBG3zx8vr2-_zqt5yRthZO-duE,4296
|
|
9
|
+
markdowncleaner-0.1.1.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
|
10
|
+
markdowncleaner-0.1.1.dist-info/entry_points.txt,sha256=C_FJkZY4OeKfdTacBhqh9hzjZEk8gQJGzGSI8qLr-lU,61
|
|
11
|
+
markdowncleaner-0.1.1.dist-info/top_level.txt,sha256=dMBEsk9DuO55cKIbeM4Ih2e0Y5aP6AfcKEYo2SoyFs0,16
|
|
12
|
+
markdowncleaner-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
markdowncleaner
|