raw2md 0.1.0__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.
- raw2md/__init__.py +1 -0
- raw2md/__main__.py +5 -0
- raw2md/cleaner.py +389 -0
- raw2md/cleaning/__init__.py +169 -0
- raw2md/cleaning/findings.py +640 -0
- raw2md/cleaning/headings.py +2506 -0
- raw2md/cleaning/lists.py +348 -0
- raw2md/cleaning/math.py +2528 -0
- raw2md/cleaning/ocr.py +354 -0
- raw2md/cleaning/options.py +36 -0
- raw2md/cleaning/prose.py +1513 -0
- raw2md/cleaning/segments.py +357 -0
- raw2md/cleaning/tables.py +1661 -0
- raw2md/cli.py +240 -0
- raw2md/config.py +113 -0
- raw2md/doctor.py +222 -0
- raw2md/engines/__init__.py +40 -0
- raw2md/engines/base.py +179 -0
- raw2md/engines/djvu.py +309 -0
- raw2md/engines/marker.py +273 -0
- raw2md/engines/pandoc.py +206 -0
- raw2md/engines/pandoc_tables.py +187 -0
- raw2md/engines/preflight.py +56 -0
- raw2md/engines/pymupdf.py +153 -0
- raw2md/engines/signature.py +81 -0
- raw2md/exit_codes.py +16 -0
- raw2md/header.py +281 -0
- raw2md/init.py +147 -0
- raw2md/katex.min.js +1 -0
- raw2md/katex.py +93 -0
- raw2md/keywords.py +251 -0
- raw2md/keywords.yaml +111 -0
- raw2md/llm/__init__.py +58 -0
- raw2md/llm/acceptance.py +196 -0
- raw2md/llm/base.py +327 -0
- raw2md/llm/chunking.py +427 -0
- raw2md/llm/claude.py +189 -0
- raw2md/llm/edit_guard.py +816 -0
- raw2md/llm/factory.py +25 -0
- raw2md/llm/gemini.py +475 -0
- raw2md/llm/inspection/__init__.py +11 -0
- raw2md/llm/inspection/addressing.py +460 -0
- raw2md/llm/inspection/apply.py +369 -0
- raw2md/llm/inspection/common.py +73 -0
- raw2md/llm/inspection/coordinator.py +410 -0
- raw2md/llm/inspection/masking.py +272 -0
- raw2md/llm/inspection/numbering.py +216 -0
- raw2md/llm/inspection/reply.py +302 -0
- raw2md/llm/ocr.py +196 -0
- raw2md/llm/post/__init__.py +9 -0
- raw2md/llm/post/blocks.py +820 -0
- raw2md/llm/post/common.py +186 -0
- raw2md/llm/post/coordinator.py +341 -0
- raw2md/llm/post/ladder.py +609 -0
- raw2md/llm/post/spacing.py +341 -0
- raw2md/llm/post/tables.py +599 -0
- raw2md/llm/rpd.py +102 -0
- raw2md/llm/trace.py +98 -0
- raw2md/lock.py +178 -0
- raw2md/logging_setup.py +188 -0
- raw2md/mdtext/__init__.py +12 -0
- raw2md/mdtext/formulas.py +355 -0
- raw2md/mdtext/lines.py +93 -0
- raw2md/mdtext/links.py +67 -0
- raw2md/mdtext/loops.py +436 -0
- raw2md/mdtext/math_spans.py +118 -0
- raw2md/mdtext/pages.py +65 -0
- raw2md/mdtext/tables.py +490 -0
- raw2md/mdtext/zones.py +190 -0
- raw2md/media.py +379 -0
- raw2md/orchestrator.py +535 -0
- raw2md/output.py +229 -0
- raw2md/paths.py +47 -0
- raw2md/pipeline.py +1161 -0
- raw2md/progress.py +239 -0
- raw2md/prompts.py +336 -0
- raw2md/quality/__init__.py +5 -0
- raw2md/quality/evaluator.py +1155 -0
- raw2md/quality/headings.py +461 -0
- raw2md/queue.py +405 -0
- raw2md/service.py +62 -0
- raw2md/settings.py +473 -0
- raw2md/source_outline.py +141 -0
- raw2md/source_text.py +196 -0
- raw2md/temp.py +65 -0
- raw2md-0.1.0.dist-info/METADATA +322 -0
- raw2md-0.1.0.dist-info/RECORD +91 -0
- raw2md-0.1.0.dist-info/WHEEL +4 -0
- raw2md-0.1.0.dist-info/entry_points.txt +2 -0
- raw2md-0.1.0.dist-info/licenses/LICENSE +21 -0
- raw2md-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +87 -0
raw2md/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""raw2md: convert raw documents (PDF, DOCX, DjVu) to cleaned Markdown."""
|
raw2md/__main__.py
ADDED
raw2md/cleaner.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""Deterministic cleaning: in-place fixes, and a report of what stays broken.
|
|
2
|
+
|
|
3
|
+
Cleaning runs before any LLM step. An unambiguous defect is fixed in place
|
|
4
|
+
(`clean_in_place`); an ambiguous one is reported as a finding and left in the
|
|
5
|
+
body (`cleaning.findings`). `clean_with_report` returns both; `clean` returns
|
|
6
|
+
the body alone.
|
|
7
|
+
|
|
8
|
+
Protected zones pass through verbatim: front matter, fenced code, and HTML
|
|
9
|
+
tables. Three rules reach into an HTML table's cells but never its markup:
|
|
10
|
+
the alt-path strip, the LLM-OCR fabrication strip, and the math cell opening.
|
|
11
|
+
|
|
12
|
+
This module owns the order of the passes, the one thing about cleaning that
|
|
13
|
+
must be read as a whole. The rules live in the `cleaning` package.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, replace
|
|
19
|
+
|
|
20
|
+
from raw2md.cleaning import (
|
|
21
|
+
CleanOptions,
|
|
22
|
+
Finding,
|
|
23
|
+
apply_outline_levels,
|
|
24
|
+
classify_and_fix,
|
|
25
|
+
clear_display_span_tail,
|
|
26
|
+
close_section_level_gaps,
|
|
27
|
+
close_unpaired_delimiters,
|
|
28
|
+
collapse_blanks,
|
|
29
|
+
collapse_unused_heading_levels,
|
|
30
|
+
decompose_contents_table,
|
|
31
|
+
defuse_false_math,
|
|
32
|
+
drop_bodyless_table,
|
|
33
|
+
drop_content_free_math_spans,
|
|
34
|
+
drop_empty_table_columns,
|
|
35
|
+
drop_orphan_sizers,
|
|
36
|
+
drop_page_furniture,
|
|
37
|
+
drop_paragraph_emphasis,
|
|
38
|
+
drop_running_header_headings,
|
|
39
|
+
drop_stray_bullet_glyph,
|
|
40
|
+
fix_images_and_report,
|
|
41
|
+
fix_table_math,
|
|
42
|
+
fold_repeated_column_cycle,
|
|
43
|
+
group_settled_headings,
|
|
44
|
+
headings_settled_before_nesting,
|
|
45
|
+
join_contents_entry_breaks,
|
|
46
|
+
join_flattened_paragraphs,
|
|
47
|
+
join_letter_spaced_headings,
|
|
48
|
+
join_letter_spaced_runs,
|
|
49
|
+
join_wrapped_contents_entries,
|
|
50
|
+
keep_table_delimiter_under_header,
|
|
51
|
+
lay_out_table_blocks,
|
|
52
|
+
lift_prose_from_display_math,
|
|
53
|
+
mend_pipe_block_missing_delimiter,
|
|
54
|
+
normalize_heading_numbering,
|
|
55
|
+
normalize_lists,
|
|
56
|
+
normalize_math,
|
|
57
|
+
normalize_repeated_heading_levels,
|
|
58
|
+
normalize_unnumbered_heading_levels,
|
|
59
|
+
open_math_html_cells,
|
|
60
|
+
printed_outline,
|
|
61
|
+
promote_dingbat_bullet,
|
|
62
|
+
promote_paragraph_formula,
|
|
63
|
+
reclose_runaway_display_spans,
|
|
64
|
+
reclose_runaway_inline_spans,
|
|
65
|
+
recut_periodic_stack,
|
|
66
|
+
recut_swallowed_cells,
|
|
67
|
+
rejoin_hyphenation,
|
|
68
|
+
repair_attested_tokens,
|
|
69
|
+
reunite_split_formulas,
|
|
70
|
+
reunite_split_tables,
|
|
71
|
+
separate_bold_list_marker,
|
|
72
|
+
separate_glued_inline_spans,
|
|
73
|
+
settle_compound_hyphens,
|
|
74
|
+
split_glued_table_header,
|
|
75
|
+
split_run_in_headings,
|
|
76
|
+
spread_crushed_runs,
|
|
77
|
+
strip_alt_file_paths,
|
|
78
|
+
strip_heading_emphasis,
|
|
79
|
+
strip_heading_leading_glyphs,
|
|
80
|
+
strip_html_anchors,
|
|
81
|
+
strip_math_span_html,
|
|
82
|
+
strip_ocr_fabrications,
|
|
83
|
+
strip_oversize_heading_markers,
|
|
84
|
+
tag_equation_numbers,
|
|
85
|
+
trim_repetition_loops,
|
|
86
|
+
unwrap_single_glyph_math,
|
|
87
|
+
)
|
|
88
|
+
from raw2md.mdtext.zones import Segment, segments
|
|
89
|
+
from raw2md.source_outline import SourceOutline
|
|
90
|
+
|
|
91
|
+
# `CleanOptions` is re-exported: the options travel with the entry points.
|
|
92
|
+
__all__ = [
|
|
93
|
+
"CleanOptions",
|
|
94
|
+
"CleanResult",
|
|
95
|
+
"clean",
|
|
96
|
+
"clean_in_place",
|
|
97
|
+
"clean_with_report",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
# The two unnumbered-level rules can hand each other a fresh violation, so
|
|
101
|
+
# they alternate to a fixed point. The cap is defensive, not a proven bound:
|
|
102
|
+
# observed documents settle in one or two rounds. A missed fix beats a hang.
|
|
103
|
+
_HEADING_LEVEL_SETTLE_ROUNDS = 8
|
|
104
|
+
|
|
105
|
+
# The running-header drop reads levels that the later heading rules rewrite,
|
|
106
|
+
# and each drop changes their evidence, so the whole block repeats until
|
|
107
|
+
# nothing moves.
|
|
108
|
+
_HEADING_STRUCTURE_SETTLE_ROUNDS = 4
|
|
109
|
+
|
|
110
|
+
# A joined line carries the break of the line it absorbed, behind the walk.
|
|
111
|
+
_HYPHENATION_SETTLE_ROUNDS = 4
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def clean_in_place(text: str, options: CleanOptions | None = None) -> str:
|
|
115
|
+
"""Apply the in-place fixes to LF `text` and return the cleaned markdown.
|
|
116
|
+
|
|
117
|
+
The result ends with one newline, or is empty. Without `options` the
|
|
118
|
+
route is unknown, which disables heading recovery.
|
|
119
|
+
"""
|
|
120
|
+
if options is None:
|
|
121
|
+
options = CleanOptions()
|
|
122
|
+
text = strip_alt_file_paths(text)
|
|
123
|
+
if options.llm_ocr:
|
|
124
|
+
text = strip_ocr_fabrications(text, options.keywords)
|
|
125
|
+
seg_list = classify_and_fix(text)
|
|
126
|
+
seg_list = strip_html_anchors(seg_list)
|
|
127
|
+
seg_list = strip_heading_emphasis(seg_list)
|
|
128
|
+
# Before every rule that reads a heading's number: a stray leading glyph
|
|
129
|
+
# hides the number, as emphasis does.
|
|
130
|
+
seg_list = strip_heading_leading_glyphs(seg_list)
|
|
131
|
+
seg_list = split_run_in_headings(seg_list)
|
|
132
|
+
# Before every rule that compares titles: a letter-spaced title matches
|
|
133
|
+
# nothing but itself.
|
|
134
|
+
seg_list = join_letter_spaced_headings(seg_list)
|
|
135
|
+
# Before every rule that reads a heading's level: an oversize "heading" is
|
|
136
|
+
# mistagged prose or a crushed table and would feed a false section to
|
|
137
|
+
# the level votes.
|
|
138
|
+
seg_list = strip_oversize_heading_markers(seg_list)
|
|
139
|
+
seg_list = drop_running_header_headings(seg_list)
|
|
140
|
+
seg_list = drop_page_furniture(seg_list, options.keywords)
|
|
141
|
+
seg_list = _settle_heading_structure(seg_list, options)
|
|
142
|
+
# Before every span-scoped math rule: a glued-shut delimiter pairs with the
|
|
143
|
+
# next formula's opener, and every span between reads as one runaway block.
|
|
144
|
+
seg_list = reclose_runaway_display_spans(seg_list)
|
|
145
|
+
seg_list = reclose_runaway_inline_spans(seg_list)
|
|
146
|
+
seg_list = fix_table_math(seg_list)
|
|
147
|
+
seg_list = defuse_false_math(seg_list)
|
|
148
|
+
# Before the normalization: a piece of a cut formula balances only once
|
|
149
|
+
# the pieces are joined.
|
|
150
|
+
seg_list = reunite_split_formulas(seg_list)
|
|
151
|
+
# Before every rule that reads span content, after the reunion so that it
|
|
152
|
+
# reads whole spans.
|
|
153
|
+
seg_list = strip_math_span_html(seg_list)
|
|
154
|
+
seg_list = normalize_math(seg_list)
|
|
155
|
+
# After the normalization, which decides what renders; before the tag fold,
|
|
156
|
+
# which would read a legend entry's punctuation as a formula's.
|
|
157
|
+
seg_list = lift_prose_from_display_math(seg_list)
|
|
158
|
+
# Before the tag fold, so a number under a promoted formula folds in as
|
|
159
|
+
# under any display block.
|
|
160
|
+
seg_list = promote_paragraph_formula(seg_list)
|
|
161
|
+
# After the reunion, which moves a number to the span's tail, and after the
|
|
162
|
+
# normalization, which clears `\label{...}` and turns `\eqno` into a tag.
|
|
163
|
+
seg_list = tag_equation_numbers(seg_list)
|
|
164
|
+
seg_list = unwrap_single_glyph_math(seg_list)
|
|
165
|
+
seg_list = drop_content_free_math_spans(seg_list)
|
|
166
|
+
# Before the pairing fix: an orphan sizer would throw off its count.
|
|
167
|
+
seg_list = drop_orphan_sizers(seg_list)
|
|
168
|
+
seg_list = close_unpaired_delimiters(seg_list)
|
|
169
|
+
# Last of the span rules: it moves every span bound after the insert.
|
|
170
|
+
seg_list = separate_glued_inline_spans(seg_list)
|
|
171
|
+
# Before list de-nesting, so the promoted items form a block.
|
|
172
|
+
seg_list = promote_dingbat_bullet(seg_list)
|
|
173
|
+
seg_list = normalize_lists(seg_list)
|
|
174
|
+
seg_list = separate_bold_list_marker(seg_list)
|
|
175
|
+
seg_list = drop_stray_bullet_glyph(seg_list)
|
|
176
|
+
seg_list = split_glued_table_header(seg_list)
|
|
177
|
+
# Before every grid rule: until folded, one column index names several
|
|
178
|
+
# fields. After the header split, which leaves the title row to read.
|
|
179
|
+
seg_list = fold_repeated_column_cycle(seg_list)
|
|
180
|
+
# Before every rule that reads a table body whole: a half reads as a column
|
|
181
|
+
# that no row fills.
|
|
182
|
+
seg_list = reunite_split_tables(seg_list)
|
|
183
|
+
# Before the bodyless collapse, which would claim the same table.
|
|
184
|
+
seg_list = decompose_contents_table(seg_list)
|
|
185
|
+
# Before the bodyless collapse: a body stacked in one header cell leaves no
|
|
186
|
+
# data row, and the collapse would join it into prose.
|
|
187
|
+
seg_list = recut_periodic_stack(seg_list)
|
|
188
|
+
# Before the column rules: without rows, every header cell reads as a
|
|
189
|
+
# column that no row fills.
|
|
190
|
+
seg_list = drop_bodyless_table(seg_list)
|
|
191
|
+
seg_list = drop_empty_table_columns(seg_list)
|
|
192
|
+
# After the empty-column drop, which rewrites the column indices.
|
|
193
|
+
seg_list = recut_swallowed_cells(seg_list)
|
|
194
|
+
# After the recut: what is left is a run whose blanks leave a choice, which
|
|
195
|
+
# only the table's own columns decide.
|
|
196
|
+
seg_list = spread_crushed_runs(seg_list)
|
|
197
|
+
# After every grid rule, which reads an in-cell break as a lost cell; a
|
|
198
|
+
# break still standing is a wrapped contents entry.
|
|
199
|
+
seg_list = join_contents_entry_breaks(seg_list, options.keywords)
|
|
200
|
+
# After that join: a cell still stacking fragments defeats the column
|
|
201
|
+
# comparison.
|
|
202
|
+
seg_list = join_wrapped_contents_entries(seg_list, options.keywords)
|
|
203
|
+
# After the table rules: collapsing a carrier table leaves its number or
|
|
204
|
+
# picture behind the display closer.
|
|
205
|
+
seg_list = clear_display_span_tail(seg_list)
|
|
206
|
+
seg_list = _settle_hyphenation(seg_list, options)
|
|
207
|
+
# After heading recovery: a bold title is a heading by now, so a wrap
|
|
208
|
+
# still standing is noise around prose.
|
|
209
|
+
seg_list = drop_paragraph_emphasis(seg_list)
|
|
210
|
+
# After the rejoin: a fragment still ending in a break holds a split
|
|
211
|
+
# nothing attested, and this rule leaves it.
|
|
212
|
+
seg_list = join_flattened_paragraphs(seg_list)
|
|
213
|
+
seg_list = join_letter_spaced_runs(seg_list)
|
|
214
|
+
# After the rejoin, whose joined spellings the counts must weigh.
|
|
215
|
+
seg_list = settle_compound_hyphens(seg_list)
|
|
216
|
+
# Last of the body-count rules: every join above spells words it weighs.
|
|
217
|
+
seg_list = repair_attested_tokens(seg_list, options.witness_min)
|
|
218
|
+
# Postcondition on every table block: several passes drop a junk header
|
|
219
|
+
# row and leave the separator first, where GFM reads no table. Before the
|
|
220
|
+
# missing-delimiter mend, which would misread the rows under it.
|
|
221
|
+
seg_list = keep_table_delimiter_under_header(seg_list)
|
|
222
|
+
# Postcondition: a pipe block with no delimiter row renders as no table.
|
|
223
|
+
seg_list = mend_pipe_block_missing_delimiter(seg_list)
|
|
224
|
+
# Last segment pass, once no rule moves a cell: padding is billed, since
|
|
225
|
+
# the chunk planner sizes a request by text length.
|
|
226
|
+
seg_list = lay_out_table_blocks(seg_list)
|
|
227
|
+
# Last of all: it writes blank lines inside a protected zone, which every
|
|
228
|
+
# rule above reads as one line per table row.
|
|
229
|
+
return open_math_html_cells(collapse_blanks(seg_list))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _settle_heading_structure(
|
|
233
|
+
seg_list: list[Segment], options: CleanOptions
|
|
234
|
+
) -> list[Segment]:
|
|
235
|
+
"""Run the heading rules until the structure they decide stops moving.
|
|
236
|
+
|
|
237
|
+
A round drops running headers, then settles levels. A later round sees
|
|
238
|
+
the drop against settled levels instead of the converter's guess. The
|
|
239
|
+
level witness is read once: the heading that closes the contents zone
|
|
240
|
+
moves between rounds, and a re-read would state a new ladder each time.
|
|
241
|
+
"""
|
|
242
|
+
outline = _level_witness(seg_list, options)
|
|
243
|
+
for _ in range(_HEADING_STRUCTURE_SETTLE_ROUNDS):
|
|
244
|
+
fixed = _settle_heading_levels(
|
|
245
|
+
drop_running_header_headings(seg_list), options, outline
|
|
246
|
+
)
|
|
247
|
+
if fixed == seg_list:
|
|
248
|
+
break
|
|
249
|
+
seg_list = fixed
|
|
250
|
+
return seg_list
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _level_witness(seg_list: list[Segment], options: CleanOptions) -> SourceOutline:
|
|
254
|
+
"""The source outline when it has one, else the printed contents.
|
|
255
|
+
|
|
256
|
+
The two are never merged: each numbers levels on its own scale.
|
|
257
|
+
"""
|
|
258
|
+
if options.outline.has_outline:
|
|
259
|
+
return options.outline
|
|
260
|
+
return printed_outline(seg_list, options.keywords)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _settle_heading_levels(
|
|
264
|
+
seg_list: list[Segment], options: CleanOptions, outline: SourceOutline
|
|
265
|
+
) -> list[Segment]:
|
|
266
|
+
"""Decide every heading's level from the evidence the document itself gives."""
|
|
267
|
+
seg_list, stated = apply_outline_levels(seg_list, outline)
|
|
268
|
+
seg_list, stated = normalize_heading_numbering(seg_list, options.method, stated)
|
|
269
|
+
# A settled group applies from the next round, since its vote reads levels
|
|
270
|
+
# this round validates. A self-numbered series and repeated unnumbered
|
|
271
|
+
# titles settle up front: the nesting rule has nothing to tell them.
|
|
272
|
+
settled = headings_settled_before_nesting(seg_list, options.keywords, stated)
|
|
273
|
+
for _ in range(_HEADING_LEVEL_SETTLE_ROUNDS):
|
|
274
|
+
fixed = normalize_repeated_heading_levels(
|
|
275
|
+
normalize_unnumbered_heading_levels(
|
|
276
|
+
seg_list, options.keywords, stated, settled
|
|
277
|
+
),
|
|
278
|
+
options.keywords,
|
|
279
|
+
stated,
|
|
280
|
+
)
|
|
281
|
+
settled = group_settled_headings(fixed, options.keywords, stated)
|
|
282
|
+
if fixed == seg_list:
|
|
283
|
+
break
|
|
284
|
+
seg_list = fixed
|
|
285
|
+
# The whole-body collapse first: after a lift the unused rank is in use,
|
|
286
|
+
# and the sections the lift missed keep their hole. The collapse is
|
|
287
|
+
# monotonic, so the lift still sees the holes it would have seen.
|
|
288
|
+
return close_section_level_gaps(
|
|
289
|
+
collapse_unused_heading_levels(seg_list), stated, settled
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _settle_hyphenation(
|
|
294
|
+
seg_list: list[Segment], options: CleanOptions
|
|
295
|
+
) -> list[Segment]:
|
|
296
|
+
"""Rejoin hyphenated line splits until no further join is attested.
|
|
297
|
+
|
|
298
|
+
Rounds count a chain of breaks, not joins; a page does not produce a
|
|
299
|
+
chain longer than two. Each changing round shrinks the body.
|
|
300
|
+
"""
|
|
301
|
+
for _ in range(_HYPHENATION_SETTLE_ROUNDS):
|
|
302
|
+
joined = rejoin_hyphenation(seg_list, options.source_text)
|
|
303
|
+
if joined == seg_list:
|
|
304
|
+
break
|
|
305
|
+
seg_list = joined
|
|
306
|
+
return seg_list
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@dataclass(frozen=True)
|
|
310
|
+
class CleanResult:
|
|
311
|
+
"""What the cleaning stage produced: the body, and what it could not fix.
|
|
312
|
+
|
|
313
|
+
Each finding is addressed by its line in `body`.
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
body: str
|
|
317
|
+
findings: tuple[Finding, ...]
|
|
318
|
+
|
|
319
|
+
def counts(self) -> dict[str, int]:
|
|
320
|
+
"""How many findings of each kind, ordered by kind."""
|
|
321
|
+
return {kind: len(group) for kind, group in _by_kind(self.findings).items()}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _by_kind(findings: tuple[Finding, ...]) -> dict[str, list[Finding]]:
|
|
325
|
+
grouped: dict[str, list[Finding]] = {}
|
|
326
|
+
for finding in sorted(findings, key=lambda f: (f.kind, f.line)):
|
|
327
|
+
grouped.setdefault(finding.kind, []).append(finding)
|
|
328
|
+
return grouped
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def clean(text: str, options: CleanOptions | None = None) -> str:
|
|
332
|
+
"""Fully clean `text` and return the body, dropping the report."""
|
|
333
|
+
return clean_with_report(text, options).body
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def clean_with_report(text: str, options: CleanOptions | None = None) -> CleanResult:
|
|
337
|
+
"""Fully clean `text`: in-place fixes, then a report of what stays broken.
|
|
338
|
+
|
|
339
|
+
Idempotent in both halves. Repetition loops are trimmed after the
|
|
340
|
+
detectors, since the trim removes the evidence of a lost formula; only the
|
|
341
|
+
run that made the cut records it. Findings are renumbered onto the
|
|
342
|
+
returned body.
|
|
343
|
+
"""
|
|
344
|
+
if options is None:
|
|
345
|
+
options = CleanOptions()
|
|
346
|
+
fixed = clean_in_place(text, options)
|
|
347
|
+
seg_list = segments(fixed)
|
|
348
|
+
seg_list, findings = fix_images_and_report(seg_list, options)
|
|
349
|
+
seg_list, landed = trim_repetition_loops(seg_list)
|
|
350
|
+
numbers = _final_line_numbers(seg_list)
|
|
351
|
+
return CleanResult(
|
|
352
|
+
collapse_blanks(seg_list),
|
|
353
|
+
tuple(
|
|
354
|
+
replace(finding, line=numbers[landed[finding.line]]) for finding in findings
|
|
355
|
+
),
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _final_line_numbers(seg_list: list[Segment]) -> list[int]:
|
|
360
|
+
"""Line number in the collapsed body for each segment, 1-based.
|
|
361
|
+
|
|
362
|
+
Mirrors `collapse_blanks`. A dropped line takes the number of the next
|
|
363
|
+
line, else the last line, else 1 for an empty body.
|
|
364
|
+
"""
|
|
365
|
+
numbers: list[int | None] = []
|
|
366
|
+
written = 0
|
|
367
|
+
pending_blank = False
|
|
368
|
+
seen_content = False
|
|
369
|
+
for line, protected in seg_list:
|
|
370
|
+
if not protected and not line.strip():
|
|
371
|
+
pending_blank = seen_content
|
|
372
|
+
numbers.append(None)
|
|
373
|
+
continue
|
|
374
|
+
if pending_blank:
|
|
375
|
+
written += 1
|
|
376
|
+
pending_blank = False
|
|
377
|
+
written += 1
|
|
378
|
+
seen_content = True
|
|
379
|
+
numbers.append(written)
|
|
380
|
+
settled: list[int] = [0] * len(numbers)
|
|
381
|
+
follower = max(written, 1)
|
|
382
|
+
for idx in range(len(numbers) - 1, -1, -1):
|
|
383
|
+
number = numbers[idx]
|
|
384
|
+
if number is None:
|
|
385
|
+
settled[idx] = follower
|
|
386
|
+
else:
|
|
387
|
+
settled[idx] = number
|
|
388
|
+
follower = number
|
|
389
|
+
return settled
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Deterministic cleaning rules, one module per object they fix.
|
|
2
|
+
|
|
3
|
+
`cleaner` is the coordinator: it holds the order of the passes and the public
|
|
4
|
+
entry points. The rules here are grouped by what they repair, not by the
|
|
5
|
+
engine that produced the defect. `options` carries the context of the passes;
|
|
6
|
+
`segments` holds the predicates that several rule modules share.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from raw2md.cleaning.findings import (
|
|
10
|
+
Finding,
|
|
11
|
+
block_shows_defect,
|
|
12
|
+
drop_image_links,
|
|
13
|
+
fix_images_and_report,
|
|
14
|
+
trim_repetition_loops,
|
|
15
|
+
)
|
|
16
|
+
from raw2md.cleaning.headings import (
|
|
17
|
+
apply_outline_levels,
|
|
18
|
+
close_section_level_gaps,
|
|
19
|
+
collapse_unused_heading_levels,
|
|
20
|
+
detect_oversize_headings,
|
|
21
|
+
detect_run_in_headings,
|
|
22
|
+
drop_running_header_headings,
|
|
23
|
+
group_settled_headings,
|
|
24
|
+
headings_settled_before_nesting,
|
|
25
|
+
join_letter_spaced_headings,
|
|
26
|
+
normalize_heading_numbering,
|
|
27
|
+
normalize_repeated_heading_levels,
|
|
28
|
+
normalize_unnumbered_heading_levels,
|
|
29
|
+
printed_outline,
|
|
30
|
+
split_run_in_headings,
|
|
31
|
+
strip_heading_emphasis,
|
|
32
|
+
strip_heading_leading_glyphs,
|
|
33
|
+
strip_oversize_heading_markers,
|
|
34
|
+
)
|
|
35
|
+
from raw2md.cleaning.lists import (
|
|
36
|
+
drop_stray_bullet_glyph,
|
|
37
|
+
normalize_lists,
|
|
38
|
+
promote_dingbat_bullet,
|
|
39
|
+
separate_bold_list_marker,
|
|
40
|
+
)
|
|
41
|
+
from raw2md.cleaning.math import (
|
|
42
|
+
clear_display_span_tail,
|
|
43
|
+
close_unpaired_delimiters,
|
|
44
|
+
defuse_false_math,
|
|
45
|
+
drop_content_free_math_spans,
|
|
46
|
+
drop_orphan_sizers,
|
|
47
|
+
fix_table_math,
|
|
48
|
+
lift_prose_from_display_math,
|
|
49
|
+
normalize_math,
|
|
50
|
+
promote_paragraph_formula,
|
|
51
|
+
reclose_runaway_display_spans,
|
|
52
|
+
reclose_runaway_inline_spans,
|
|
53
|
+
reunite_split_formulas,
|
|
54
|
+
separate_glued_inline_spans,
|
|
55
|
+
strip_math_span_html,
|
|
56
|
+
tag_equation_numbers,
|
|
57
|
+
unwrap_single_glyph_math,
|
|
58
|
+
)
|
|
59
|
+
from raw2md.cleaning.ocr import (
|
|
60
|
+
DEFAULT_WITNESS_MIN,
|
|
61
|
+
repair_attested_tokens,
|
|
62
|
+
strip_ocr_fabrications,
|
|
63
|
+
)
|
|
64
|
+
from raw2md.cleaning.options import CleanOptions
|
|
65
|
+
from raw2md.cleaning.prose import (
|
|
66
|
+
classify_and_fix,
|
|
67
|
+
collapse_blanks,
|
|
68
|
+
drop_page_furniture,
|
|
69
|
+
drop_paragraph_emphasis,
|
|
70
|
+
join_flattened_paragraphs,
|
|
71
|
+
join_letter_spaced_runs,
|
|
72
|
+
rejoin_hyphenation,
|
|
73
|
+
settle_compound_hyphens,
|
|
74
|
+
strip_alt_file_paths,
|
|
75
|
+
strip_html_anchors,
|
|
76
|
+
strip_line_noise,
|
|
77
|
+
)
|
|
78
|
+
from raw2md.cleaning.tables import (
|
|
79
|
+
decompose_contents_table,
|
|
80
|
+
drop_bodyless_table,
|
|
81
|
+
drop_empty_table_columns,
|
|
82
|
+
fold_repeated_column_cycle,
|
|
83
|
+
join_contents_entry_breaks,
|
|
84
|
+
join_wrapped_contents_entries,
|
|
85
|
+
keep_table_delimiter_under_header,
|
|
86
|
+
lay_out_table_blocks,
|
|
87
|
+
mend_pipe_block_missing_delimiter,
|
|
88
|
+
open_math_html_cells,
|
|
89
|
+
recut_periodic_stack,
|
|
90
|
+
recut_swallowed_cells,
|
|
91
|
+
reunite_split_tables,
|
|
92
|
+
split_glued_table_header,
|
|
93
|
+
spread_crushed_runs,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
__all__ = [
|
|
97
|
+
"DEFAULT_WITNESS_MIN",
|
|
98
|
+
"CleanOptions",
|
|
99
|
+
"Finding",
|
|
100
|
+
"apply_outline_levels",
|
|
101
|
+
"block_shows_defect",
|
|
102
|
+
"classify_and_fix",
|
|
103
|
+
"clear_display_span_tail",
|
|
104
|
+
"close_section_level_gaps",
|
|
105
|
+
"close_unpaired_delimiters",
|
|
106
|
+
"collapse_blanks",
|
|
107
|
+
"collapse_unused_heading_levels",
|
|
108
|
+
"decompose_contents_table",
|
|
109
|
+
"defuse_false_math",
|
|
110
|
+
"detect_oversize_headings",
|
|
111
|
+
"detect_run_in_headings",
|
|
112
|
+
"drop_bodyless_table",
|
|
113
|
+
"drop_content_free_math_spans",
|
|
114
|
+
"drop_empty_table_columns",
|
|
115
|
+
"drop_image_links",
|
|
116
|
+
"drop_orphan_sizers",
|
|
117
|
+
"drop_page_furniture",
|
|
118
|
+
"drop_paragraph_emphasis",
|
|
119
|
+
"drop_running_header_headings",
|
|
120
|
+
"drop_stray_bullet_glyph",
|
|
121
|
+
"fix_images_and_report",
|
|
122
|
+
"fix_table_math",
|
|
123
|
+
"fold_repeated_column_cycle",
|
|
124
|
+
"group_settled_headings",
|
|
125
|
+
"headings_settled_before_nesting",
|
|
126
|
+
"join_contents_entry_breaks",
|
|
127
|
+
"join_flattened_paragraphs",
|
|
128
|
+
"join_letter_spaced_headings",
|
|
129
|
+
"join_letter_spaced_runs",
|
|
130
|
+
"join_wrapped_contents_entries",
|
|
131
|
+
"keep_table_delimiter_under_header",
|
|
132
|
+
"lay_out_table_blocks",
|
|
133
|
+
"lift_prose_from_display_math",
|
|
134
|
+
"mend_pipe_block_missing_delimiter",
|
|
135
|
+
"normalize_heading_numbering",
|
|
136
|
+
"normalize_lists",
|
|
137
|
+
"normalize_math",
|
|
138
|
+
"normalize_repeated_heading_levels",
|
|
139
|
+
"normalize_unnumbered_heading_levels",
|
|
140
|
+
"open_math_html_cells",
|
|
141
|
+
"printed_outline",
|
|
142
|
+
"promote_dingbat_bullet",
|
|
143
|
+
"promote_paragraph_formula",
|
|
144
|
+
"reclose_runaway_display_spans",
|
|
145
|
+
"reclose_runaway_inline_spans",
|
|
146
|
+
"recut_periodic_stack",
|
|
147
|
+
"recut_swallowed_cells",
|
|
148
|
+
"rejoin_hyphenation",
|
|
149
|
+
"repair_attested_tokens",
|
|
150
|
+
"reunite_split_formulas",
|
|
151
|
+
"reunite_split_tables",
|
|
152
|
+
"separate_bold_list_marker",
|
|
153
|
+
"separate_glued_inline_spans",
|
|
154
|
+
"settle_compound_hyphens",
|
|
155
|
+
"split_glued_table_header",
|
|
156
|
+
"split_run_in_headings",
|
|
157
|
+
"spread_crushed_runs",
|
|
158
|
+
"strip_alt_file_paths",
|
|
159
|
+
"strip_heading_emphasis",
|
|
160
|
+
"strip_heading_leading_glyphs",
|
|
161
|
+
"strip_html_anchors",
|
|
162
|
+
"strip_line_noise",
|
|
163
|
+
"strip_math_span_html",
|
|
164
|
+
"strip_ocr_fabrications",
|
|
165
|
+
"strip_oversize_heading_markers",
|
|
166
|
+
"tag_equation_numbers",
|
|
167
|
+
"trim_repetition_loops",
|
|
168
|
+
"unwrap_single_glyph_math",
|
|
169
|
+
]
|