docconvert-local 2.0.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.
- docconvert/__init__.py +15 -0
- docconvert/chunkers/__init__.py +5 -0
- docconvert/chunkers/table_chunker.py +10 -0
- docconvert/cleaners/__init__.py +7 -0
- docconvert/cleaners/base.py +10 -0
- docconvert/cleaners/word_md.py +250 -0
- docconvert/cli.py +133 -0
- docconvert/config.py +23 -0
- docconvert/controller/__init__.py +5 -0
- docconvert/controller/conversion_controller.py +369 -0
- docconvert/converters/__init__.py +11 -0
- docconvert/converters/base.py +31 -0
- docconvert/converters/doc.py +84 -0
- docconvert/converters/excel.py +634 -0
- docconvert/converters/word.py +129 -0
- docconvert/exporters/__init__.py +22 -0
- docconvert/exporters/base.py +16 -0
- docconvert/exporters/html.py +15 -0
- docconvert/exporters/json_exporter.py +16 -0
- docconvert/exporters/markdown.py +15 -0
- docconvert/gui/__init__.py +5 -0
- docconvert/gui/app.py +997 -0
- docconvert/logger.py +36 -0
- docconvert/models/__init__.py +9 -0
- docconvert/models/models.py +24 -0
- docconvert/parsers/__init__.py +5 -0
- docconvert/parsers/semantic.py +10 -0
- docconvert/py.typed +0 -0
- docconvert/utils/__init__.py +21 -0
- docconvert/utils/utils.py +128 -0
- docconvert_local-2.0.0.dist-info/METADATA +253 -0
- docconvert_local-2.0.0.dist-info/RECORD +36 -0
- docconvert_local-2.0.0.dist-info/WHEEL +5 -0
- docconvert_local-2.0.0.dist-info/entry_points.txt +2 -0
- docconvert_local-2.0.0.dist-info/licenses/LICENSE +21 -0
- docconvert_local-2.0.0.dist-info/top_level.txt +1 -0
docconvert/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""DocConvert - 文档转换工具
|
|
2
|
+
|
|
3
|
+
The GUI is imported lazily so CLI / library use (``python main.py convert``
|
|
4
|
+
or ``from docconvert.controller import ConversionController``) does not
|
|
5
|
+
require the Tkinter package.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = ["DocConvertApp"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def __getattr__(name: str):
|
|
12
|
+
if name == "DocConvertApp":
|
|
13
|
+
from docconvert.gui.app import DocConvertApp
|
|
14
|
+
return DocConvertApp
|
|
15
|
+
raise AttributeError(f"module 'docconvert' has no attribute {name!r}")
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from docconvert.cleaners.base import BaseCleaner
|
|
7
|
+
from docconvert.config import AppConfig, DEFAULT_CONFIG
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_UNESCAPE_PATTERN = re.compile(r'\\([\\\[\]\-.\(\)])')
|
|
11
|
+
_HORIZ_WS_PATTERN = re.compile(r'[ \t]+')
|
|
12
|
+
_FULLWIDTH_SPACE = '\u3000'
|
|
13
|
+
# A line that opens or closes a fenced code block (``` or ~~~), possibly
|
|
14
|
+
# with a language tag: "```python". Used to keep code-block whitespace intact.
|
|
15
|
+
_FENCE_PATTERN = re.compile(r'^\s*(?:`{3,}|~{3,})')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WordMdCleaner(BaseCleaner):
|
|
19
|
+
"""
|
|
20
|
+
Defense-in-depth post-processor for Word -> Markdown output.
|
|
21
|
+
|
|
22
|
+
WordConverter already strips header/footer references in the DOCX XML
|
|
23
|
+
before conversion. This cleaner is the regex safety net for whatever
|
|
24
|
+
page-number artifacts leak through mammoth, plus general whitespace
|
|
25
|
+
cleanup that mammoth cannot infer from Word's XML.
|
|
26
|
+
|
|
27
|
+
Page-number rules (line-level, anchored to ``strip()`` so they only
|
|
28
|
+
match standalone markers, never page numbers embedded in body text):
|
|
29
|
+
|
|
30
|
+
- Inline refs: [1], [12]
|
|
31
|
+
- Chinese: 第3页, 第 5 页
|
|
32
|
+
- Bordered: - 1 -, -12-
|
|
33
|
+
- Western: Page 1, page 5, Pág. 3, P. 7
|
|
34
|
+
- Multi-form: Page 1 of 10, Page 3 / 10
|
|
35
|
+
|
|
36
|
+
Document-level rules applied after the line-level pass:
|
|
37
|
+
|
|
38
|
+
- ``remove_duplicate_headers`` — drop consecutive duplicate lines
|
|
39
|
+
(the typical leak when mammoth flattens page headers / footers
|
|
40
|
+
into the body of every page). Only consecutive duplicates are
|
|
41
|
+
removed; non-adjacent repeats are kept.
|
|
42
|
+
- ``remove_empty_lines`` — collapse runs of ≥2 empty lines down to
|
|
43
|
+
a single empty line. Whitespace-only lines count as empty.
|
|
44
|
+
- ``normalize_spaces`` — convert U+3000 (full-width space) and tabs
|
|
45
|
+
to a single space, collapse internal runs of horizontal whitespace,
|
|
46
|
+
and strip trailing whitespace. Leading whitespace is preserved so
|
|
47
|
+
indented markdown (code blocks, list items, tables) is not
|
|
48
|
+
destroyed.
|
|
49
|
+
|
|
50
|
+
All four rules skip lines inside fenced (````` ``` ```` / ``~~~``) or
|
|
51
|
+
indented (≥4 leading spaces) code blocks: page markers, blank lines,
|
|
52
|
+
repeated lines and internal spacing are all significant inside code,
|
|
53
|
+
and the cleaner must not corrupt it.
|
|
54
|
+
|
|
55
|
+
Rule activation is driven by ``AppConfig.cleaning_rules``. The four
|
|
56
|
+
documented keys all have working implementations now:
|
|
57
|
+
|
|
58
|
+
- ``remove_page_numbers`` (default True) → enables all 5 page rules
|
|
59
|
+
- ``remove_duplicate_headers`` (default True)
|
|
60
|
+
- ``remove_empty_lines`` (default True)
|
|
61
|
+
- ``normalize_spaces`` (default True)
|
|
62
|
+
|
|
63
|
+
When constructed without a config, ``DEFAULT_CONFIG`` is used and
|
|
64
|
+
every rule is active (backward-compatible default).
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
_RULES = (
|
|
68
|
+
re.compile(r'^\[\d+\]$'),
|
|
69
|
+
re.compile(r'^第\s*\d+\s*页$'),
|
|
70
|
+
re.compile(r'^-\s*\d+\s*-$'),
|
|
71
|
+
re.compile(r'^page\s*\d+\s*(?:of|/)\s*\d+\s*$', re.IGNORECASE),
|
|
72
|
+
re.compile(r'^(?:Page|Pág\.|P\.)\s*\d+\s*$', re.IGNORECASE),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
_LINE_RULE_GROUPS: dict[str, tuple[int, ...]] = {
|
|
76
|
+
"remove_page_numbers": (0, 1, 2, 3, 4),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def __init__(self, config: Optional[AppConfig] = None):
|
|
80
|
+
cfg = config or DEFAULT_CONFIG
|
|
81
|
+
rules = cfg.cleaning_rules
|
|
82
|
+
self._active_line_rules = self._resolve_line_rules(rules)
|
|
83
|
+
self._collapse_empty = bool(rules.get("remove_empty_lines", False))
|
|
84
|
+
self._normalize_spaces = bool(rules.get("normalize_spaces", False))
|
|
85
|
+
self._dedupe_consecutive = bool(rules.get("remove_duplicate_headers", False))
|
|
86
|
+
|
|
87
|
+
def _resolve_line_rules(self, cleaning_rules: dict) -> tuple:
|
|
88
|
+
active: list[int] = []
|
|
89
|
+
for key, indices in self._LINE_RULE_GROUPS.items():
|
|
90
|
+
if cleaning_rules.get(key, False):
|
|
91
|
+
active.extend(indices)
|
|
92
|
+
return tuple(self._RULES[i] for i in active)
|
|
93
|
+
|
|
94
|
+
def clean(self, content: str, **kwargs) -> str:
|
|
95
|
+
if not any((
|
|
96
|
+
self._active_line_rules,
|
|
97
|
+
self._collapse_empty,
|
|
98
|
+
self._normalize_spaces,
|
|
99
|
+
self._dedupe_consecutive,
|
|
100
|
+
)):
|
|
101
|
+
return content
|
|
102
|
+
|
|
103
|
+
lines = content.split('\n')
|
|
104
|
+
# Classify code-block lines ONCE so every rule below can leave code
|
|
105
|
+
# untouched: page markers, blank lines, repeated lines and internal
|
|
106
|
+
# spacing are all significant inside code.
|
|
107
|
+
code_mask = self._code_mask(lines)
|
|
108
|
+
|
|
109
|
+
# Stage 1: line-level rules (page numbers). Lines matching a
|
|
110
|
+
# rule are dropped; everything else keeps its original whitespace.
|
|
111
|
+
if self._active_line_rules:
|
|
112
|
+
lines, code_mask = self._apply_line_rules(lines, code_mask)
|
|
113
|
+
|
|
114
|
+
# Stage 2: collapse empty lines. Run before normalize so that
|
|
115
|
+
# runs of whitespace-only lines (which normalize would turn into
|
|
116
|
+
# empty strings) are deduped exactly once.
|
|
117
|
+
if self._collapse_empty:
|
|
118
|
+
lines, code_mask = self._collapse_empty_lines(lines, code_mask)
|
|
119
|
+
|
|
120
|
+
# Stage 3: normalize spaces within each line. Run before dedupe
|
|
121
|
+
# so that lines differing only in whitespace are treated as
|
|
122
|
+
# duplicates.
|
|
123
|
+
if self._normalize_spaces:
|
|
124
|
+
lines = self._normalize_lines_spaces(lines, code_mask)
|
|
125
|
+
|
|
126
|
+
# Stage 4: drop consecutive duplicate lines.
|
|
127
|
+
if self._dedupe_consecutive:
|
|
128
|
+
lines = self._dedupe_consecutive_lines(lines, code_mask)
|
|
129
|
+
|
|
130
|
+
return '\n'.join(lines)
|
|
131
|
+
|
|
132
|
+
def _code_mask(self, lines: list[str]) -> list[bool]:
|
|
133
|
+
"""Boolean per line: True when the line lives inside a code block.
|
|
134
|
+
|
|
135
|
+
Fenced blocks (````` ``` ```` / ``~~~``) are tracked by toggling on
|
|
136
|
+
their delimiter lines; indented blocks are any line with ≥4 leading
|
|
137
|
+
spaces/tabs. Delimiter lines themselves count as code so they are
|
|
138
|
+
never altered or removed by a later rule.
|
|
139
|
+
"""
|
|
140
|
+
in_fence = False
|
|
141
|
+
mask: list[bool] = []
|
|
142
|
+
for line in lines:
|
|
143
|
+
if _FENCE_PATTERN.match(line):
|
|
144
|
+
in_fence = not in_fence
|
|
145
|
+
mask.append(True)
|
|
146
|
+
continue
|
|
147
|
+
mask.append(in_fence or self._is_code_line(line))
|
|
148
|
+
return mask
|
|
149
|
+
|
|
150
|
+
def _apply_line_rules(self, lines: list[str], code_mask: list[bool]):
|
|
151
|
+
kept: list[str] = []
|
|
152
|
+
kept_mask: list[bool] = []
|
|
153
|
+
for line, code in zip(lines, code_mask):
|
|
154
|
+
stripped = line.strip()
|
|
155
|
+
if not stripped:
|
|
156
|
+
kept.append(line)
|
|
157
|
+
kept_mask.append(code)
|
|
158
|
+
continue
|
|
159
|
+
if code:
|
|
160
|
+
# Page-number markers inside code are content (e.g. a
|
|
161
|
+
# ``[1]`` array index or a ``Page 1`` literal); never strip.
|
|
162
|
+
kept.append(line)
|
|
163
|
+
kept_mask.append(True)
|
|
164
|
+
continue
|
|
165
|
+
# Normalize mammoth's backslash-escaped markdown punctuation
|
|
166
|
+
# (e.g. ``\[1\]`` → ``[1]``, ``\- 1 \-`` → ``- 1 -``,
|
|
167
|
+
# ``Pág\. 3`` → ``Pág. 3``) so the page-number rules match
|
|
168
|
+
# the real shape of the marker rather than its escaped form.
|
|
169
|
+
normalized = _UNESCAPE_PATTERN.sub(r'\1', stripped)
|
|
170
|
+
if any(rx.match(normalized) for rx in self._active_line_rules):
|
|
171
|
+
continue
|
|
172
|
+
kept.append(line)
|
|
173
|
+
kept_mask.append(code)
|
|
174
|
+
return kept, kept_mask
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _collapse_empty_lines(lines: list[str], code_mask: list[bool]):
|
|
178
|
+
result: list[str] = []
|
|
179
|
+
result_mask: list[bool] = []
|
|
180
|
+
prev_empty = False
|
|
181
|
+
prev_code = False
|
|
182
|
+
for line, code in zip(lines, code_mask):
|
|
183
|
+
is_empty = not line.strip()
|
|
184
|
+
# Blank lines inside a code block are significant and are not
|
|
185
|
+
# collapsed; blank runs outside code still become one line.
|
|
186
|
+
if is_empty and prev_empty and not (code and prev_code):
|
|
187
|
+
continue
|
|
188
|
+
# Normalize the kept empty line to a bare ``""`` so the
|
|
189
|
+
# output is canonical regardless of whether the input used
|
|
190
|
+
# ``" "`` or ``"\t"`` as its whitespace-only filler.
|
|
191
|
+
result.append("" if is_empty else line)
|
|
192
|
+
result_mask.append(code)
|
|
193
|
+
prev_empty = is_empty
|
|
194
|
+
prev_code = code
|
|
195
|
+
return result, result_mask
|
|
196
|
+
|
|
197
|
+
def _normalize_lines_spaces(self, lines: list[str], code_mask: list[bool]) -> list[str]:
|
|
198
|
+
result: list[str] = []
|
|
199
|
+
for line, code in zip(lines, code_mask):
|
|
200
|
+
result.append(self._normalize_line_spaces(line, code_line=code))
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def _is_code_line(line: str) -> bool:
|
|
205
|
+
"""True when ``line`` starts an indented markdown code block.
|
|
206
|
+
|
|
207
|
+
A line indented by ≥4 spaces/tabs is treated as code by CommonMark,
|
|
208
|
+
where internal spacing is significant. Non-code lines fall through
|
|
209
|
+
to the ordinary whitespace normalization.
|
|
210
|
+
"""
|
|
211
|
+
return len(line) - len(line.lstrip(' \t')) >= 4
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def _normalize_line_spaces(line: str, code_line: bool = False) -> str:
|
|
215
|
+
# Convert full-width space (U+3000) used in CJK typography to a
|
|
216
|
+
# regular space so downstream consumers can tokenize uniformly.
|
|
217
|
+
line = line.replace(_FULLWIDTH_SPACE, ' ')
|
|
218
|
+
if not line.strip():
|
|
219
|
+
return line
|
|
220
|
+
if code_line:
|
|
221
|
+
# Inside a fenced or indented code block, internal spacing is
|
|
222
|
+
# meaningful — collapsing it would corrupt the code. Only strip
|
|
223
|
+
# trailing whitespace.
|
|
224
|
+
return line.rstrip()
|
|
225
|
+
# Preserve leading whitespace (indentation matters in markdown
|
|
226
|
+
# for code blocks, list items, tables) while collapsing internal
|
|
227
|
+
# runs of horizontal whitespace and stripping trailing whitespace.
|
|
228
|
+
match = re.match(r'^([ \t]*)(.*?)([ \t]*)$', line, re.DOTALL)
|
|
229
|
+
if not match:
|
|
230
|
+
return line.rstrip()
|
|
231
|
+
leading, middle, _trailing = match.groups()
|
|
232
|
+
middle = _HORIZ_WS_PATTERN.sub(' ', middle)
|
|
233
|
+
return leading + middle
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def _dedupe_consecutive_lines(lines: list[str], code_mask: list[bool]) -> list[str]:
|
|
237
|
+
# Only collapse runs of identical non-empty lines. Empty lines
|
|
238
|
+
# were already handled by ``_collapse_empty_lines``. Two
|
|
239
|
+
# non-adjacent identical lines are kept as-is, and repeated lines
|
|
240
|
+
# inside code blocks are legitimate and kept too.
|
|
241
|
+
result: list[str] = []
|
|
242
|
+
prev: Optional[str] = None
|
|
243
|
+
prev_code = False
|
|
244
|
+
for line, code in zip(lines, code_mask):
|
|
245
|
+
if line.strip() and line == prev and not (code and prev_code):
|
|
246
|
+
continue
|
|
247
|
+
result.append(line)
|
|
248
|
+
prev = line
|
|
249
|
+
prev_code = code
|
|
250
|
+
return result
|
docconvert/cli.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from docconvert.config import DEFAULT_CONFIG
|
|
8
|
+
from docconvert.controller import ConversionController
|
|
9
|
+
from docconvert.logger import setup_logging
|
|
10
|
+
from docconvert.models import ProgressEvent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _cli_progress(event: ProgressEvent):
|
|
14
|
+
if event.message:
|
|
15
|
+
print(f'\r[{int(event.progress * 100):3d}%] {event.message:<50s}', file=sys.stderr, end='')
|
|
16
|
+
if event.done:
|
|
17
|
+
print(file=sys.stderr)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _HelpFormatter(argparse.HelpFormatter):
|
|
21
|
+
"""Preserves newlines in description, epilog, and all help strings."""
|
|
22
|
+
|
|
23
|
+
def _fill_text(self, text, width, indent):
|
|
24
|
+
if text:
|
|
25
|
+
return ''.join(indent + line + '\n' for line in text.splitlines())
|
|
26
|
+
return ''
|
|
27
|
+
|
|
28
|
+
def _split_lines(self, text, width):
|
|
29
|
+
return text.splitlines() if text else []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main_cli(argv: list[str] | None = None) -> int:
|
|
33
|
+
parser = argparse.ArgumentParser(
|
|
34
|
+
description='DocConvert — convert Excel & Word documents to clean Markdown, HTML, or JSON.',
|
|
35
|
+
formatter_class=_HelpFormatter,
|
|
36
|
+
epilog=(
|
|
37
|
+
'Examples:\n'
|
|
38
|
+
' python main.py convert input.xlsx --format md\n'
|
|
39
|
+
' python main.py convert input.docx --format html -o ./output\n'
|
|
40
|
+
' python main.py convert file1.xlsx file2.xlsx --format json\n'
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
subparsers = parser.add_subparsers(dest='command', help='Command to run')
|
|
45
|
+
|
|
46
|
+
convert_parser = subparsers.add_parser('convert', help='Convert one or more files')
|
|
47
|
+
convert_parser.add_argument('files', nargs='+', help='Input file paths')
|
|
48
|
+
convert_parser.add_argument(
|
|
49
|
+
'--format', '-f',
|
|
50
|
+
choices=['html', 'md', 'json'],
|
|
51
|
+
default='html',
|
|
52
|
+
help='Output format (default: html)',
|
|
53
|
+
)
|
|
54
|
+
convert_parser.add_argument(
|
|
55
|
+
'--output', '-o',
|
|
56
|
+
default=None,
|
|
57
|
+
help='Output directory (default: same as input file)',
|
|
58
|
+
)
|
|
59
|
+
convert_parser.add_argument(
|
|
60
|
+
'--enhanced', '-e',
|
|
61
|
+
action='store_true',
|
|
62
|
+
help='Enable enhanced Markdown cleaning (remove page numbers, duplicate headers, etc.)',
|
|
63
|
+
)
|
|
64
|
+
convert_parser.add_argument(
|
|
65
|
+
'--sheet', '-s',
|
|
66
|
+
action='append',
|
|
67
|
+
help='Select Excel sheet(s) (repeatable; default: all sheets)',
|
|
68
|
+
)
|
|
69
|
+
convert_parser.add_argument(
|
|
70
|
+
'--verbose', '-v',
|
|
71
|
+
action='store_true',
|
|
72
|
+
help='Enable verbose debug logging',
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
args = parser.parse_args(argv)
|
|
76
|
+
|
|
77
|
+
if args.command != 'convert':
|
|
78
|
+
parser.print_help()
|
|
79
|
+
return 1
|
|
80
|
+
|
|
81
|
+
setup_logging(level='DEBUG' if getattr(args, 'verbose', False) else 'INFO')
|
|
82
|
+
|
|
83
|
+
controller = ConversionController(DEFAULT_CONFIG)
|
|
84
|
+
|
|
85
|
+
all_results: list[tuple[str, str, str | None]] = []
|
|
86
|
+
failed = 0
|
|
87
|
+
|
|
88
|
+
for filepath in args.files:
|
|
89
|
+
p = Path(filepath)
|
|
90
|
+
if not p.exists():
|
|
91
|
+
print(f'Error: file not found — {filepath}', file=sys.stderr)
|
|
92
|
+
all_results.append((p.name, '', 'File not found'))
|
|
93
|
+
failed += 1
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
ext = p.suffix.lower()
|
|
97
|
+
if ext not in {'.xlsx', '.xls', '.docx', '.doc'}:
|
|
98
|
+
print(f'Error: unsupported format — {filepath}', file=sys.stderr)
|
|
99
|
+
all_results.append((p.name, '', 'Unsupported format'))
|
|
100
|
+
failed += 1
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
convert_results = controller.convert_files(
|
|
105
|
+
files=[filepath],
|
|
106
|
+
output_fmt=args.format,
|
|
107
|
+
output_dir=args.output,
|
|
108
|
+
enhanced_md=args.enhanced,
|
|
109
|
+
sheets=args.sheet,
|
|
110
|
+
progress_callback=_cli_progress,
|
|
111
|
+
)
|
|
112
|
+
for name, path, err in convert_results:
|
|
113
|
+
all_results.append((name, path, err))
|
|
114
|
+
if err:
|
|
115
|
+
failed += 1
|
|
116
|
+
print(f'Failed: {name} — {err}', file=sys.stderr)
|
|
117
|
+
else:
|
|
118
|
+
print(f'OK: {name} -> {path}')
|
|
119
|
+
except Exception as e:
|
|
120
|
+
print(f'Error: {filepath} — {e}', file=sys.stderr)
|
|
121
|
+
all_results.append((p.name, '', str(e)))
|
|
122
|
+
failed += 1
|
|
123
|
+
|
|
124
|
+
if failed:
|
|
125
|
+
print(f'\nDone: {len(all_results)} file(s), {failed} failed')
|
|
126
|
+
return 1
|
|
127
|
+
|
|
128
|
+
print(f'\nDone: {len(all_results)} file(s), all succeeded')
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == '__main__':
|
|
133
|
+
sys.exit(main_cli())
|
docconvert/config.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class AppConfig:
|
|
8
|
+
chunk_size: int = 1000
|
|
9
|
+
max_rows: int = 0
|
|
10
|
+
markdown_style: str = "github"
|
|
11
|
+
preview_chars: int = 5000
|
|
12
|
+
preview_lines: int = 150
|
|
13
|
+
large_file_size: int = 20 * 1024 * 1024
|
|
14
|
+
|
|
15
|
+
cleaning_rules: dict[str, bool] = field(default_factory=lambda: {
|
|
16
|
+
"remove_page_numbers": True,
|
|
17
|
+
"remove_duplicate_headers": True,
|
|
18
|
+
"remove_empty_lines": True,
|
|
19
|
+
"normalize_spaces": True,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
DEFAULT_CONFIG = AppConfig()
|