paperfit-cli 1.0.0
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.
- package/.claude/commands/adjust-length.md +21 -0
- package/.claude/commands/check-visual.md +27 -0
- package/.claude/commands/fix-layout.md +31 -0
- package/.claude/commands/migrate-template.md +23 -0
- package/.claude/commands/repair-table.md +21 -0
- package/.claude/commands/show-status.md +32 -0
- package/.claude-plugin/README.md +77 -0
- package/.claude-plugin/marketplace.json +41 -0
- package/.claude-plugin/plugin.json +39 -0
- package/CLAUDE.md +266 -0
- package/CONTRIBUTING.md +131 -0
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/agents/code-surgeon-agent.md +214 -0
- package/agents/layout-detective-agent.md +229 -0
- package/agents/orchestrator-agent.md +254 -0
- package/agents/quality-gatekeeper-agent.md +270 -0
- package/agents/rule-engine-agent.md +224 -0
- package/agents/semantic-polish-agent.md +250 -0
- package/bin/paperfit.js +176 -0
- package/config/agent_roles.yaml +56 -0
- package/config/layout_rules.yaml +54 -0
- package/config/templates.yaml +241 -0
- package/config/vto_taxonomy.yaml +489 -0
- package/config/writing_rules.yaml +64 -0
- package/install.sh +30 -0
- package/package.json +52 -0
- package/requirements.txt +5 -0
- package/scripts/benchmark_runner.py +629 -0
- package/scripts/compile.sh +244 -0
- package/scripts/config_validator.py +339 -0
- package/scripts/cv_detector.py +600 -0
- package/scripts/evidence_collector.py +167 -0
- package/scripts/float_fixers.py +861 -0
- package/scripts/inject_defects.py +549 -0
- package/scripts/install-claude-global.js +148 -0
- package/scripts/install.js +66 -0
- package/scripts/install.sh +106 -0
- package/scripts/overflow_fixers.py +656 -0
- package/scripts/package-for-opensource.sh +138 -0
- package/scripts/parse_log.py +260 -0
- package/scripts/postinstall.js +38 -0
- package/scripts/pre_tool_use.py +265 -0
- package/scripts/render_pages.py +244 -0
- package/scripts/session_logger.py +329 -0
- package/scripts/space_util_fixers.py +773 -0
- package/scripts/state_manager.py +352 -0
- package/scripts/test_commands.py +187 -0
- package/scripts/test_cv_detector.py +214 -0
- package/scripts/test_integration.py +290 -0
- package/skills/consistency-polisher/SKILL.md +337 -0
- package/skills/float-optimizer/SKILL.md +284 -0
- package/skills/latex_fixers/__init__.py +82 -0
- package/skills/latex_fixers/float_fixers.py +392 -0
- package/skills/latex_fixers/fullwidth_fixers.py +375 -0
- package/skills/latex_fixers/overflow_fixers.py +250 -0
- package/skills/latex_fixers/semantic_micro_tuning.py +362 -0
- package/skills/latex_fixers/space_util_fixers.py +389 -0
- package/skills/latex_fixers/utils.py +55 -0
- package/skills/overflow-repair/SKILL.md +304 -0
- package/skills/space-util-fixer/SKILL.md +307 -0
- package/skills/taxonomy-vto/SKILL.md +486 -0
- package/skills/template-migrator/SKILL.md +251 -0
- package/skills/visual-inspector/SKILL.md +217 -0
- package/skills/writing-polish/SKILL.md +289 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Space Utilization Fixers - Category A 缺陷修复
|
|
3
|
+
|
|
4
|
+
处理孤行寡行、末页留白、页数预算、双栏不齐等问题。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any, Dict, List, Tuple
|
|
9
|
+
|
|
10
|
+
from .utils import add_package_to_preamble, add_to_preamble, find_paragraph_end, find_paragraph_start
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def fix_widow_orphan(
|
|
14
|
+
tex_content: str,
|
|
15
|
+
page_number: int | None = None,
|
|
16
|
+
paragraph_line: int | None = None,
|
|
17
|
+
short_line_threshold: float = 0.25,
|
|
18
|
+
) -> Tuple[str, Dict[str, Any]]:
|
|
19
|
+
"""
|
|
20
|
+
修复孤行/寡行问题(A1 缺陷)- 根除策略。
|
|
21
|
+
|
|
22
|
+
绝对禁止:
|
|
23
|
+
- 段落末尾出现仅占一行 1/4 宽度或单独跨页的"小尾巴"
|
|
24
|
+
|
|
25
|
+
执行逻辑(优先级从高到低):
|
|
26
|
+
1. 优先调用 \\looseness=-1(紧缩排版)或 \\looseness=1(扩展排版)
|
|
27
|
+
2. 若物理空间依然不兼容,触发自然语言层的"句法微缩/扩写"逻辑
|
|
28
|
+
3. 精准增删 3-5 个单词以平齐右边界
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
tex_content: .tex 文件内容
|
|
32
|
+
page_number: 问题所在页码
|
|
33
|
+
paragraph_line: 问题段落起始行号
|
|
34
|
+
short_line_threshold: 段末短行阈值(默认 0.25=1/4 栏宽)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
(modified_content, change_record)
|
|
38
|
+
"""
|
|
39
|
+
change_record = {
|
|
40
|
+
"defect_id": "A1-widow-orphan",
|
|
41
|
+
"action": "none",
|
|
42
|
+
"page": page_number,
|
|
43
|
+
"strategies_attempted": [],
|
|
44
|
+
"semantic_intervention_needed": False,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
lines = tex_content.split('\n')
|
|
48
|
+
|
|
49
|
+
# 策略 0: 首先添加全局防护(如果尚未存在)
|
|
50
|
+
if '\\widowpenalty' not in tex_content:
|
|
51
|
+
preamble_additions = [
|
|
52
|
+
"\\widowpenalty=10000",
|
|
53
|
+
"\\clubpenalty=10000",
|
|
54
|
+
"\\displaywidowpenalty=10000",
|
|
55
|
+
]
|
|
56
|
+
tex_content = add_to_preamble(tex_content, '\n'.join(preamble_additions))
|
|
57
|
+
change_record["strategies_attempted"].append("global_penalty")
|
|
58
|
+
|
|
59
|
+
# 策略 1: 优先使用 \\looseness=-1 紧缩段落(消除小尾巴)
|
|
60
|
+
if paragraph_line and 0 <= paragraph_line - 1 < len(lines):
|
|
61
|
+
target_idx = paragraph_line - 1
|
|
62
|
+
|
|
63
|
+
# 查找段落边界
|
|
64
|
+
para_start = find_paragraph_start(lines, target_idx)
|
|
65
|
+
para_end = find_paragraph_end(lines, para_start)
|
|
66
|
+
|
|
67
|
+
if para_start >= 0 and para_end >= 0:
|
|
68
|
+
para_content = '\n'.join(lines[para_start:para_end + 1])
|
|
69
|
+
|
|
70
|
+
# 检查是否已有\looseness 设置
|
|
71
|
+
if '\\looseness' not in para_content:
|
|
72
|
+
# 尝试 \\looseness=-1(紧缩,减少一行)
|
|
73
|
+
lines.insert(para_start, "{\\looseness=-1 ")
|
|
74
|
+
lines.insert(para_end + 2, "}")
|
|
75
|
+
change_record["action"] = f"added \\looseness=-1 to paragraph at line {para_start}"
|
|
76
|
+
change_record["strategies_attempted"].append("looseness=-1")
|
|
77
|
+
change_record["note"] = "紧缩排版以消除段末小尾巴"
|
|
78
|
+
return '\n'.join(lines), change_record
|
|
79
|
+
|
|
80
|
+
# 策略 2: 如果已有 \\looseness=-1 但仍然有问题,尝试 \\looseness=1(扩展)
|
|
81
|
+
if paragraph_line:
|
|
82
|
+
target_idx = paragraph_line - 1
|
|
83
|
+
para_start = find_paragraph_start(lines, target_idx)
|
|
84
|
+
para_end = find_paragraph_end(lines, para_start)
|
|
85
|
+
|
|
86
|
+
if para_start >= 0 and para_end >= 0:
|
|
87
|
+
para_content = '\n'.join(lines[para_start:para_end + 1])
|
|
88
|
+
|
|
89
|
+
if '\\looseness=-1' in para_content:
|
|
90
|
+
# 替换为 \\looseness=1
|
|
91
|
+
para_content = para_content.replace('\\looseness=-1', '\\looseness=1')
|
|
92
|
+
# 重新构建
|
|
93
|
+
new_lines = lines[:para_start] + para_content.split('\n') + lines[para_end + 1:]
|
|
94
|
+
change_record["action"] = f"changed to \\looseness=1 for paragraph at line {para_start}"
|
|
95
|
+
change_record["strategies_attempted"].append("looseness=1")
|
|
96
|
+
change_record["note"] = "扩展排版以填充空白"
|
|
97
|
+
return '\n'.join(new_lines), change_record
|
|
98
|
+
|
|
99
|
+
# 策略 3: 如果无法定位具体段落,标记需要语义干预
|
|
100
|
+
change_record["semantic_intervention_needed"] = True
|
|
101
|
+
change_record["action"] = "looseness_exhausted_requires_semantic"
|
|
102
|
+
change_record["note"] = "排版手段已用尽,需要语义级句法微缩/扩写(增删 3-5 词)"
|
|
103
|
+
|
|
104
|
+
return tex_content, change_record
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def detect_short_tail(
|
|
108
|
+
tex_content: str,
|
|
109
|
+
paragraph_line: int,
|
|
110
|
+
threshold_ratio: float = 0.25,
|
|
111
|
+
) -> Tuple[bool, float]:
|
|
112
|
+
"""
|
|
113
|
+
检测段落末尾是否存在"小尾巴"(长度小于栏宽 1/4 的行)。
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
tex_content: .tex 文件内容
|
|
117
|
+
paragraph_line: 段落起始行号
|
|
118
|
+
threshold_ratio: 短行阈值(默认 0.25)
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
(is_short_tail, line_ratio) - 是否为小尾巴及其相对长度
|
|
122
|
+
"""
|
|
123
|
+
lines = tex_content.split('\n')
|
|
124
|
+
|
|
125
|
+
if not (0 <= paragraph_line - 1 < len(lines)):
|
|
126
|
+
return False, 0.0
|
|
127
|
+
|
|
128
|
+
para_start = find_paragraph_start(lines, paragraph_line - 1)
|
|
129
|
+
para_end = find_paragraph_end(lines, para_start)
|
|
130
|
+
|
|
131
|
+
if para_start < 0 or para_end < 0:
|
|
132
|
+
return False, 0.0
|
|
133
|
+
|
|
134
|
+
# 获取段落最后一行
|
|
135
|
+
last_line = lines[para_end].strip()
|
|
136
|
+
|
|
137
|
+
# 计算行长(字符数近似)
|
|
138
|
+
if not last_line:
|
|
139
|
+
return True, 0.0 # 空行视为小尾巴
|
|
140
|
+
|
|
141
|
+
# 估算栏宽(典型 LaTeX 文档约 80-100 字符)
|
|
142
|
+
estimated_column_width = 80
|
|
143
|
+
line_ratio = len(last_line) / estimated_column_width
|
|
144
|
+
|
|
145
|
+
return line_ratio < threshold_ratio, line_ratio
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def fix_trailing_whitespace(
|
|
149
|
+
tex_content: str,
|
|
150
|
+
target_pages: int | None = None,
|
|
151
|
+
) -> Tuple[str, Dict[str, Any]]:
|
|
152
|
+
"""
|
|
153
|
+
修复末页大面积留白问题(A2 缺陷)。
|
|
154
|
+
|
|
155
|
+
策略:
|
|
156
|
+
1. 前移浮动体到末页
|
|
157
|
+
2. 调整垂直间距
|
|
158
|
+
3. 扩写结论部分
|
|
159
|
+
"""
|
|
160
|
+
change_record = {
|
|
161
|
+
"defect_id": "A2-trailing-whitespace",
|
|
162
|
+
"action": "none",
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# 策略 1: 查找可前移的浮动体
|
|
166
|
+
# 查找 figure/table 环境及其位置参数
|
|
167
|
+
float_pattern = r'\\begin\{(figure|table)\}(\[([^\]]*)\])?'
|
|
168
|
+
|
|
169
|
+
matches = list(re.finditer(float_pattern, tex_content))
|
|
170
|
+
if matches:
|
|
171
|
+
# 尝试将最后一个浮动体的位置参数改为 [h] 强制放在当前位置
|
|
172
|
+
last_float = matches[-1]
|
|
173
|
+
if last_float.group(2): # 有位置参数
|
|
174
|
+
old_param = last_float.group(3)
|
|
175
|
+
if 'h' not in old_param.lower():
|
|
176
|
+
# 替换为 [ht]
|
|
177
|
+
new_content = tex_content[:last_float.start()]
|
|
178
|
+
new_content += f"\\begin{{{last_float.group(1)}}}[ht]"
|
|
179
|
+
new_content += tex_content[last_float.end():]
|
|
180
|
+
tex_content = new_content
|
|
181
|
+
change_record["action"] = "adjusted float position to fill whitespace"
|
|
182
|
+
|
|
183
|
+
# 策略 2: 微调最后一节前的间距
|
|
184
|
+
if change_record["action"] == "none":
|
|
185
|
+
# 查找最后一个\section 或\section*
|
|
186
|
+
section_pattern = r'\\section(\*)?\{[^}]+\}'
|
|
187
|
+
sections = list(re.finditer(section_pattern, tex_content))
|
|
188
|
+
if len(sections) >= 2:
|
|
189
|
+
# 在倒数第二节后添加负间距
|
|
190
|
+
last_section = sections[-1]
|
|
191
|
+
# 查找节前的位置
|
|
192
|
+
insert_pos = last_section.start()
|
|
193
|
+
# 检查是否已有\vspace
|
|
194
|
+
context_before = tex_content[max(0, insert_pos - 100):insert_pos]
|
|
195
|
+
if '\\vspace' not in context_before:
|
|
196
|
+
tex_content = tex_content[:insert_pos] + "\\vspace{-0.3em}\n" + tex_content[insert_pos:]
|
|
197
|
+
change_record["action"] = "added negative vspace before last section"
|
|
198
|
+
|
|
199
|
+
return tex_content, change_record
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def fix_page_budget(
|
|
203
|
+
tex_content: str,
|
|
204
|
+
current_pages: int,
|
|
205
|
+
target_pages: int,
|
|
206
|
+
) -> Tuple[str, Dict[str, Any]]:
|
|
207
|
+
"""
|
|
208
|
+
修复页数预算问题(A3 缺陷)。
|
|
209
|
+
|
|
210
|
+
策略根据超页或缺页情况不同:
|
|
211
|
+
- 超页:压缩内容、精简文字
|
|
212
|
+
- 缺页:扩展内容、增加分页
|
|
213
|
+
"""
|
|
214
|
+
change_record = {
|
|
215
|
+
"defect_id": "A3-page-budget",
|
|
216
|
+
"action": "none",
|
|
217
|
+
"current_pages": current_pages,
|
|
218
|
+
"target_pages": target_pages,
|
|
219
|
+
"delta": current_pages - target_pages,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if current_pages > target_pages:
|
|
223
|
+
# 超页:需要压缩
|
|
224
|
+
change_record["strategy"] = "compress"
|
|
225
|
+
tex_content, compress_changes = compress_to_fewer_pages(tex_content)
|
|
226
|
+
if compress_changes["action"] != "none":
|
|
227
|
+
change_record["action"] = compress_changes["action"]
|
|
228
|
+
change_record["details"] = compress_changes.get("details", [])
|
|
229
|
+
|
|
230
|
+
elif current_pages < target_pages:
|
|
231
|
+
# 缺页:需要扩展
|
|
232
|
+
change_record["strategy"] = "expand"
|
|
233
|
+
tex_content, expand_changes = expand_to_more_pages(tex_content)
|
|
234
|
+
if expand_changes["action"] != "none":
|
|
235
|
+
change_record["action"] = expand_changes["action"]
|
|
236
|
+
change_record["details"] = expand_changes.get("details", [])
|
|
237
|
+
|
|
238
|
+
return tex_content, change_record
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def compress_to_fewer_pages(tex_content: str) -> Tuple[str, Dict[str, Any]]:
|
|
242
|
+
"""压缩到更少页数"""
|
|
243
|
+
changes = {"action": "none", "details": []}
|
|
244
|
+
|
|
245
|
+
# 策略 1: 缩小浮动体尺寸
|
|
246
|
+
includegraphics_pattern = r'\\includegraphics\[([^\]]*)\]\{([^}]+)\}'
|
|
247
|
+
|
|
248
|
+
def shrink_graphicx(match):
|
|
249
|
+
options = match.group(1).split(',')
|
|
250
|
+
filename = match.group(2)
|
|
251
|
+
new_options = []
|
|
252
|
+
width_changed = False
|
|
253
|
+
for opt in options:
|
|
254
|
+
opt = opt.strip()
|
|
255
|
+
if 'width' in opt and 'linewidth' in opt:
|
|
256
|
+
# 将\linewidth 改为 0.95\linewidth
|
|
257
|
+
new_options.append('width=0.95\\linewidth')
|
|
258
|
+
width_changed = True
|
|
259
|
+
else:
|
|
260
|
+
new_options.append(opt)
|
|
261
|
+
if width_changed:
|
|
262
|
+
changes["details"].append(f"shrank {filename} to 95% linewidth")
|
|
263
|
+
return f"\\includegraphics[{','.join(new_options)}]{{{filename}}}"
|
|
264
|
+
|
|
265
|
+
modified = re.sub(includegraphics_pattern, shrink_graphicx, tex_content)
|
|
266
|
+
|
|
267
|
+
if modified != tex_content:
|
|
268
|
+
changes["action"] = "compressed figure sizes"
|
|
269
|
+
return modified, changes
|
|
270
|
+
|
|
271
|
+
# 策略 2: 压缩垂直间距
|
|
272
|
+
if '\\vspace{' in modified:
|
|
273
|
+
# 查找并缩小\vspace
|
|
274
|
+
vspace_pattern = r'\\vspace\{([^}]+)\}'
|
|
275
|
+
|
|
276
|
+
def shrink_vspace(match):
|
|
277
|
+
space = match.group(1)
|
|
278
|
+
# 简单处理:如果是数字,减少 20%
|
|
279
|
+
try:
|
|
280
|
+
# 处理类似 1em, 2cm 等
|
|
281
|
+
num_match = re.match(r'([\d.]+)(\w+)', space)
|
|
282
|
+
if num_match:
|
|
283
|
+
num = float(num_match.group(1))
|
|
284
|
+
unit = num_match.group(2)
|
|
285
|
+
new_num = num * 0.8
|
|
286
|
+
changes["details"].append(f"compressed vspace from {space} to {new_num}{unit}")
|
|
287
|
+
return f"\\vspace{{{new_num}{unit}}}"
|
|
288
|
+
except (ValueError, TypeError):
|
|
289
|
+
pass
|
|
290
|
+
return match.group(0)
|
|
291
|
+
|
|
292
|
+
modified = re.sub(vspace_pattern, shrink_vspace, modified)
|
|
293
|
+
|
|
294
|
+
if modified != tex_content:
|
|
295
|
+
changes["action"] = "compressed vertical spaces"
|
|
296
|
+
|
|
297
|
+
return modified, changes
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def expand_to_more_pages(tex_content: str) -> Tuple[str, Dict[str, Any]]:
|
|
301
|
+
"""扩展到更多页数"""
|
|
302
|
+
changes = {"action": "none", "details": []}
|
|
303
|
+
|
|
304
|
+
# 策略 1: 增大浮动体尺寸
|
|
305
|
+
includegraphics_pattern = r'\\includegraphics\[([^\]]*)\]\{([^}]+)\}'
|
|
306
|
+
|
|
307
|
+
def expand_graphicx(match):
|
|
308
|
+
options = match.group(1).split(',')
|
|
309
|
+
filename = match.group(2)
|
|
310
|
+
new_options = []
|
|
311
|
+
width_changed = False
|
|
312
|
+
for opt in options:
|
|
313
|
+
opt = opt.strip()
|
|
314
|
+
if 'width' in opt:
|
|
315
|
+
if 'linewidth' in opt:
|
|
316
|
+
# 确保是完整\linewidth
|
|
317
|
+
new_options.append('width=\\linewidth')
|
|
318
|
+
else:
|
|
319
|
+
new_options.append(opt)
|
|
320
|
+
width_changed = True
|
|
321
|
+
else:
|
|
322
|
+
new_options.append(opt)
|
|
323
|
+
if width_changed and '0.95\\linewidth' not in str(new_options):
|
|
324
|
+
changes["details"].append(f"expanded {filename} to full linewidth")
|
|
325
|
+
return f"\\includegraphics[{','.join(new_options)}]{{{filename}}}"
|
|
326
|
+
|
|
327
|
+
modified = re.sub(includegraphics_pattern, expand_graphicx, tex_content)
|
|
328
|
+
|
|
329
|
+
if modified != tex_content:
|
|
330
|
+
changes["action"] = "expanded figure sizes"
|
|
331
|
+
return modified, changes
|
|
332
|
+
|
|
333
|
+
# 策略 2: 在合适位置增加分页
|
|
334
|
+
# 在\section 前添加\newpage(谨慎使用)
|
|
335
|
+
section_pattern = r'(\\section\{[^}]+\})'
|
|
336
|
+
sections = list(re.finditer(section_pattern, tex_content))
|
|
337
|
+
|
|
338
|
+
if len(sections) >= 2:
|
|
339
|
+
# 在最后一节前分页
|
|
340
|
+
last_section = sections[-1]
|
|
341
|
+
insert_pos = last_section.start()
|
|
342
|
+
# 检查是否已有\newpage
|
|
343
|
+
context_before = tex_content[max(0, insert_pos - 50):insert_pos]
|
|
344
|
+
if '\\newpage' not in context_before and '\\section' not in context_before:
|
|
345
|
+
tex_content = tex_content[:insert_pos] + "\\newpage\n" + tex_content[insert_pos:]
|
|
346
|
+
changes["action"] = "added page break before last section"
|
|
347
|
+
|
|
348
|
+
return tex_content, changes
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def fix_unbalanced_columns(
|
|
352
|
+
tex_content: str,
|
|
353
|
+
is_two_column: bool = True,
|
|
354
|
+
) -> Tuple[str, Dict[str, Any]]:
|
|
355
|
+
"""
|
|
356
|
+
修复双栏末页左右栏高度不齐问题(A4 缺陷)。
|
|
357
|
+
|
|
358
|
+
策略:
|
|
359
|
+
1. 使用 flushend 宏包自动平衡
|
|
360
|
+
2. 使用\balance 命令
|
|
361
|
+
3. 微调最后一段行数
|
|
362
|
+
"""
|
|
363
|
+
change_record = {
|
|
364
|
+
"defect_id": "A4-unbalanced-columns",
|
|
365
|
+
"action": "none",
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if not is_two_column:
|
|
369
|
+
change_record["note"] = "not a two-column layout, skipping"
|
|
370
|
+
return tex_content, change_record
|
|
371
|
+
|
|
372
|
+
# 策略 1: 添加 flushend 宏包
|
|
373
|
+
if '\\usepackage{flushend}' not in tex_content and '\\usepackage{balance}' not in tex_content:
|
|
374
|
+
tex_content = add_package_to_preamble(tex_content, "flushend")
|
|
375
|
+
change_record["action"] = "added flushend package for automatic column balancing"
|
|
376
|
+
return tex_content, change_record
|
|
377
|
+
|
|
378
|
+
# 策略 2: 在文末添加\balance 命令
|
|
379
|
+
if '\\usepackage{balance}' in tex_content:
|
|
380
|
+
# 查找\end{document}
|
|
381
|
+
doc_end = tex_content.find('\\end{document}')
|
|
382
|
+
if doc_end > 0:
|
|
383
|
+
# 向前查找最后的内容
|
|
384
|
+
content_before_end = tex_content[:doc_end].rstrip()
|
|
385
|
+
if '\\balance' not in content_before_end[-200:]: # 最后 200 字符内没有
|
|
386
|
+
tex_content = content_before_end + "\n\\balance\n" + tex_content[doc_end:]
|
|
387
|
+
change_record["action"] = "added \\balance command before \\end{document}"
|
|
388
|
+
|
|
389
|
+
return tex_content, change_record
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared utility functions for LaTeX fixers.
|
|
3
|
+
|
|
4
|
+
Common helper functions used across overflow_fixers, float_fixers, and space_util_fixers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_package_to_preamble(tex_content: str, package_name: str) -> str:
|
|
12
|
+
"""在导言区添加宏包引用"""
|
|
13
|
+
# 检查宏包是否已存在
|
|
14
|
+
if f'\\usepackage{{{package_name}}}' in tex_content:
|
|
15
|
+
return tex_content
|
|
16
|
+
|
|
17
|
+
# 查找最后一个\usepackage
|
|
18
|
+
usepackage_pattern = r'\\usepackage[^}]*\{[^}]*\}'
|
|
19
|
+
matches = list(re.finditer(usepackage_pattern, tex_content))
|
|
20
|
+
|
|
21
|
+
if matches:
|
|
22
|
+
last_match = matches[-1]
|
|
23
|
+
insert_pos = last_match.end()
|
|
24
|
+
return tex_content[:insert_pos] + f"\n\\usepackage{{{package_name}}}" + tex_content[insert_pos:]
|
|
25
|
+
else:
|
|
26
|
+
doc_begin = tex_content.find('\\begin{document}')
|
|
27
|
+
if doc_begin >= 0:
|
|
28
|
+
return tex_content[:doc_begin] + f"\\usepackage{{{package_name}}}\n" + tex_content[doc_begin:]
|
|
29
|
+
return tex_content
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def add_to_preamble(tex_content: str, content: str) -> str:
|
|
33
|
+
"""在导言区添加内容"""
|
|
34
|
+
doc_begin = tex_content.find('\\begin{document}')
|
|
35
|
+
if doc_begin >= 0:
|
|
36
|
+
return tex_content[:doc_begin] + content + "\n" + tex_content[doc_begin:]
|
|
37
|
+
return tex_content
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def find_paragraph_start(lines: List[str], target_idx: int) -> int:
|
|
41
|
+
"""向前查找段落开始"""
|
|
42
|
+
for i in range(target_idx, -1, -1):
|
|
43
|
+
line = lines[i].strip()
|
|
44
|
+
if not line or line.startswith('\\begin') or line.startswith('\\section'):
|
|
45
|
+
return i + 1 if i < target_idx else i
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def find_paragraph_end(lines: List[str], start_idx: int) -> int:
|
|
50
|
+
"""向后查找段落结束"""
|
|
51
|
+
for i in range(start_idx, len(lines)):
|
|
52
|
+
line = lines[i].strip()
|
|
53
|
+
if not line or line.startswith('\\end') or line.startswith('\\section'):
|
|
54
|
+
return i - 1 if i > start_idx else i
|
|
55
|
+
return len(lines) - 1
|