答案卷抬头改为一行一项,修复长学院名重叠 #22
## 用户诉求
「评分标准最开始不是有个表格吗,里面有开课学院什么的,如果学院名字太长,
就和后面课程名称重合在一起了。看看怎么解决。我建议一行一个信息,
因为这个又不怕长,如果太长,一行都放不下,就直接换行。」
## 现象(已复现并量化)
`dept` 填超长值后,pymupdf 读出:
开课学院:(70.87~123.37) 值:(123.37~406.87)
而第二格「课程名称」本该从 216.0 开始 ⇒ **重叠 190.87bp**,
把「课程名称 / 试卷类型」整个压住。
⚠️ **编译完全不报错,也没有 Overfull**(rc=0 over=0)。
`\makebox` 内容溢出盒外时 TeX 不认为是坏行。这类 bug 只能靠
量 span 坐标发现,看日志发现不了。
## 根因
`\ouckey@inforow` 用三个 `\makebox[0.3333\linewidth][l]` 并排。
`\makebox` 是**不可断盒子**:
- 内容比盒宽短 → 左对齐(正常)
- 内容比盒宽长 → **直接溢出盒外,压到右边一格**(就是 bug)
- 盒内中文**不能换行**(LR 模式,不做段落断行)
⇒ 三格布局对短字段友好,对长字段必然重叠。
## 改动一:一行一项
\newcommand{\ouckey@inforow}[2]{%
\ouckey@ifempty{#2}{}{%
{\fontsize{10.5bp}{15bp}\selectfont
\par\noindent\textbf{#1:}#2\par}}}
调用点从「三参数一组」改成「两参数一行」,共 9 项:
课程名称 / 开课学院 / 学生学院 / 年级专业 / 试卷类型 / 考试方式 /
任课教师 / 命题人 / 满分。顺序按"读者关心程度"排。
行距收紧到 15bp(`\Fivhao` 基线距是 16bp)—— 抬头是查阅信息,
不需要正文行距;9 行省约 52bp,将近 3 行正文。
## 改动二:附带修掉一个旧版就有的判空 bug
**旧实现**:
\newcommand{\ouckey@ifempty}[1]{%
\if\relax\detokenize{#1}\relax ... }
调用 `\ouckey@ifempty{\ouckey@dept}{空}{非空}`。
`\detokenize` **不展开参数**,看到的是字符串 `\ouckey@dept`(宏名),
永远非空 ⇒ 判空永远失败。
后果:`dept = {}` 时抬头印出孤立的「开课学院:」,后面没有值。
这是旧版就有的 bug(三格布局下不明显),改成一行一项后暴露。
**修法**:在 `\define@key` 里就规范化空值,新增
\newcommand{\ouckey@setkey}[2]{%
\if\relax\detokenize{#2}\relax \def#1{}\else \def#1{#2}\fi}
13 个 key 全改走它。
⚠️ 试过「先 `\edef` 展开再 `\detokenize`」的写法,报
`! Incomplete \iffalse` —— `\if...\else...\fi` 被 `\expandafter`
链打散,TeX 匹配 `\fi` 时错位。这类宏层展开的修补太脆,放弃。
⚠️ `\ouckey@setkey` 必须定义在 `\define@key` **之前**,否则 undefined。
## 验证
### 三个场景
| 场景 | 结果 |
|---|---|
| 正常(学院名 12 字) | 9 项齐全,无重叠,无越界 |
| 长(含括号说明,约 40 字) | 单行排到 x=406.90,无重叠(下面没有格子可压) |
| 极长(约 70 字) | **自动换行成两行**,第二行缩进对齐,右缘正好 524.40 |
| 留空(dept/college/major 全空) | 三项**整项不印**,不留空行 |
### 编译
试卷题型示例 6 页 err=0 over=0
参考答案与评分标准 4 页 err=0 over=0
全库 `bash build.sh all` → **13/13 通过,0 失败**。
### 页数变化
答案卷 3 页 → 4 页。抬头从 3 行变 9 行,是「一行一项」的必然代价。
**已向用户确认,接受 4 页**(第 4 页只有说明文字末尾 2 行 + 签字行)。
## 示例文件未改
`参考答案与评分标准示例.tex` 测试后已还原,`git diff` 为空。
只改 `ouckey.sty` 一个文件。
This commit is contained in:
+108
-32
@@ -210,22 +210,65 @@
|
||||
\def\ouckey@major{}\def\ouckey@dept{}\def\ouckey@teacher{}
|
||||
\def\ouckey@examiner{}\def\ouckey@mode{}\def\ouckey@date{}
|
||||
\def\ouckey@total{}
|
||||
\define@key{ouckey}{year}{\def\ouckey@year{#1}}
|
||||
\define@key{ouckey}{term}{\def\ouckey@term{#1}}
|
||||
\define@key{ouckey}{course}{\def\ouckey@course{#1}}
|
||||
\define@key{ouckey}{type}{\def\ouckey@type{#1}}
|
||||
\define@key{ouckey}{college}{\def\ouckey@college{#1}}
|
||||
\define@key{ouckey}{grade}{\def\ouckey@grade{#1}}
|
||||
\define@key{ouckey}{major}{\def\ouckey@major{#1}}
|
||||
\define@key{ouckey}{dept}{\def\ouckey@dept{#1}}
|
||||
\define@key{ouckey}{teacher}{\def\ouckey@teacher{#1}}
|
||||
\define@key{ouckey}{examiner}{\def\ouckey@examiner{#1}}
|
||||
\define@key{ouckey}{mode}{\def\ouckey@mode{#1}}
|
||||
\define@key{ouckey}{date}{\def\ouckey@date{#1}}
|
||||
\define@key{ouckey}{total}{\def\ouckey@total{#1}}
|
||||
|
||||
%----------------------------------------------------------------------
|
||||
% key 赋值:空值规范成空串(#22)
|
||||
%
|
||||
% ⚠️ 所有 key 一律走这里,不要退回 `\def\ouckey@xxx{#1}`。
|
||||
% 原因见文件下方「判空」一节的完整说明:那样 dept={} 会存成
|
||||
% 「一个空组」,后续 \detokenize 展开后仍非空,判空失败,
|
||||
% 抬头会印出孤立的「开课学院:」而没有值。
|
||||
%
|
||||
% ⚠️ 必须定义在 \define@key 之**前**,否则展开时 undefined。
|
||||
%----------------------------------------------------------------------
|
||||
\newcommand{\ouckey@setkey}[2]{%
|
||||
\if\relax\detokenize{#2}\relax
|
||||
\def#1{}%
|
||||
\else
|
||||
\def#1{#2}%
|
||||
\fi}
|
||||
|
||||
% 所有 key 一律走 \ouckey@setkey
|
||||
\define@key{ouckey}{year}{\ouckey@setkey{\ouckey@year}{#1}}
|
||||
\define@key{ouckey}{term}{\ouckey@setkey{\ouckey@term}{#1}}
|
||||
\define@key{ouckey}{course}{\ouckey@setkey{\ouckey@course}{#1}}
|
||||
\define@key{ouckey}{type}{\ouckey@setkey{\ouckey@type}{#1}}
|
||||
\define@key{ouckey}{college}{\ouckey@setkey{\ouckey@college}{#1}}
|
||||
\define@key{ouckey}{grade}{\ouckey@setkey{\ouckey@grade}{#1}}
|
||||
\define@key{ouckey}{major}{\ouckey@setkey{\ouckey@major}{#1}}
|
||||
\define@key{ouckey}{dept}{\ouckey@setkey{\ouckey@dept}{#1}}
|
||||
\define@key{ouckey}{teacher}{\ouckey@setkey{\ouckey@teacher}{#1}}
|
||||
\define@key{ouckey}{examiner}{\ouckey@setkey{\ouckey@examiner}{#1}}
|
||||
\define@key{ouckey}{mode}{\ouckey@setkey{\ouckey@mode}{#1}}
|
||||
\define@key{ouckey}{date}{\ouckey@setkey{\ouckey@date}{#1}}
|
||||
\define@key{ouckey}{total}{\ouckey@setkey{\ouckey@total}{#1}}
|
||||
\newcommand{\keysetup}[1]{\setkeys{ouckey}{#1}}
|
||||
|
||||
% 判空:\ifx 比不了多 token 参数,用 \detokenize 展成字符串再比
|
||||
%----------------------------------------------------------------------
|
||||
% 判空(#22 修正:原实现有真 bug)
|
||||
%
|
||||
% ⚠️⚠️ 旧实现的坑
|
||||
% 旧代码是
|
||||
% \newcommand{\ouckey@ifempty}[1]{%
|
||||
% \if\relax\detokenize{#1}\relax ... }
|
||||
% 调用处写 `\ouckey@ifempty{\ouckey@dept}{空}{非空}`。
|
||||
% **\detokenize 不展开参数** —— 它看到的是字符串 `\ouckey@dept`
|
||||
% (宏名本身),永远非空 ⇒ 判空永远失败。
|
||||
% 后果:`dept = {}` 时抬头仍印出孤立的「开课学院:」,后面没有值。
|
||||
% 这是旧版就有的 bug(三格布局下不明显),#22 改成一行一项后暴露。
|
||||
%
|
||||
% 为什么不在宏里修
|
||||
% 试过「先 \edef 展开再 \detokenize」的版本,会报
|
||||
% ! Incomplete \iffalse; all text was ignored after line 49.
|
||||
% 因为 \if...\else...\fi 被 \expandafter 链打散,TeX 匹配 \fi 时错位。
|
||||
% 这类宏层展开的修补很脆,不值得。
|
||||
%
|
||||
% ✅ 正确做法:**在 \define@key 里就把空值规范化**(见文件上方
|
||||
% \ouckey@setkey)。空值存成真正的空串,判空就能工作。
|
||||
%
|
||||
% ⚠️ 这里只留判空宏本身。它拿到的是**已经规范化过的值**
|
||||
% (`\ouckey@dept` 展开后就是纯文本),所以 \detokenize 有效。
|
||||
%----------------------------------------------------------------------
|
||||
\newcommand{\ouckey@ifempty}[1]{%
|
||||
\if\relax\detokenize{#1}\relax
|
||||
\expandafter\@firstoftwo
|
||||
@@ -233,15 +276,45 @@
|
||||
\expandafter\@secondoftwo
|
||||
\fi}
|
||||
|
||||
% 一格信息:标签加粗,值用宋体;值为空则整格不出
|
||||
\newcommand{\ouckey@cell}[2]{%
|
||||
\ouckey@ifempty{#2}{}{\textbf{#1:}#2}}
|
||||
% 一行三格,等宽
|
||||
\newcommand{\ouckey@inforow}[6]{%
|
||||
\par\noindent
|
||||
\makebox[0.3333\linewidth][l]{\ouckey@cell{#1}{#2}}%
|
||||
\makebox[0.3333\linewidth][l]{\ouckey@cell{#3}{#4}}%
|
||||
\makebox[0.3333\linewidth][l]{\ouckey@cell{#5}{#6}}\par}
|
||||
%----------------------------------------------------------------------
|
||||
% 一项信息:标签加粗 + 值,**独占一行**(#22)
|
||||
%
|
||||
% ⚠️⚠️ 为什么从"一行三格"改成"一行一项"
|
||||
%
|
||||
% 旧实现是三个 \makebox[0.3333\linewidth][l] 并排:
|
||||
% \makebox[固定宽][l]{内容}
|
||||
% \makebox 是**不可断的盒子**,行为是:
|
||||
% · 内容比盒宽短 → 左对齐,右侧留白(正常)
|
||||
% · 内容比盒宽长 → **直接溢出盒外,压到右边一格上**(就是 bug)
|
||||
% · 盒内中文**不能换行**(LR 模式,不做段落断行)
|
||||
% 所以三格布局对短字段友好,对长字段必然重叠。
|
||||
%
|
||||
% 实测:把 dept 填成
|
||||
% 计算机科学与技术学院(人工智能与大数据技术方向实验班)
|
||||
% 后,pymupdf 读出
|
||||
% 开课学院:(70.87~123.37) 值:(123.37~406.87)
|
||||
% 而第二格「课程名称」本该从 216.0 开始 ⇒ **重叠 190.87bp**,
|
||||
% 把「课程名称 / 试卷类型」整个压住。
|
||||
%
|
||||
% ⚠️ **编译完全不报错,也没有 Overfull 警告**(rc=0 over=0)。
|
||||
% \makebox 内容溢出盒外时 TeX 不认为是"坏行"。
|
||||
% ⇒ 这类 bug 只能靠看渲染结果或量 span 坐标发现,看日志发现不了。
|
||||
%
|
||||
% 现在的做法:每项 `\par\noindent\textbf{标签:}值\par`,普通段落。
|
||||
% · 超长自动换行(中文可在任意汉字间断行)
|
||||
% · 不存在并排,也就不存在互相压住
|
||||
% · 抬头是"查阅"性质,一项一行比三格更容易扫读
|
||||
% 代价:抬头从 3 行变 9 行,多占约 6 行。答案卷 A4、内容不满页,
|
||||
% 多 6 行不影响版面。
|
||||
%
|
||||
% ⚠️ 留空的项仍**整项不印**(也不留空行),逻辑不变。
|
||||
%----------------------------------------------------------------------
|
||||
\newcommand{\ouckey@inforow}[2]{%
|
||||
\ouckey@ifempty{#2}{}{%
|
||||
% 行距收紧到 15bp(\Fivhao 的基线距是 16bp)。抬头是"查阅"信息,
|
||||
% 不需要正文那种行距;9 行 × 省 5.8bp ≈ 省 52bp,将近 3 行正文。
|
||||
{\fontsize{10.5bp}{15bp}\selectfont
|
||||
\par\noindent\textbf{#1:}#2\par}}}
|
||||
|
||||
%----------------------------------------------------------------------
|
||||
% 抬头:标题 + 信息栏 + 一条分隔线
|
||||
@@ -263,15 +336,18 @@
|
||||
\vspace*{10bp}
|
||||
\hrule height 1.2bp
|
||||
\vspace*{8bp}
|
||||
\ouckey@inforow{开课学院}{\ouckey@dept}%
|
||||
{课程名称}{\ouckey@course}%
|
||||
{试卷类型}{\ouckey@ifempty{\ouckey@type}{}{\ouckey@type~卷}}
|
||||
\ouckey@inforow{学生学院}{\ouckey@college}%
|
||||
{年级专业}{\ouckey@major}%
|
||||
{考试方式}{\ouckey@mode}
|
||||
\ouckey@inforow{任课教师}{\ouckey@teacher}%
|
||||
{命题人}{\ouckey@examiner}%
|
||||
{满分}{\ouckey@ifempty{\ouckey@total}{}{\TNR{\ouckey@total}~分}}
|
||||
% 每项一行(#22)。顺序按"读者关心程度"排:
|
||||
% 先课程、后学院、再人与考试形式,最后分值。
|
||||
% 留空的自动跳过,所以实际行数随填写项变化。
|
||||
\ouckey@inforow{课程名称}{\ouckey@course}
|
||||
\ouckey@inforow{开课学院}{\ouckey@dept}
|
||||
\ouckey@inforow{学生学院}{\ouckey@college}
|
||||
\ouckey@inforow{年级专业}{\ouckey@major}
|
||||
\ouckey@inforow{试卷类型}{\ouckey@ifempty{\ouckey@type}{}{\ouckey@type~卷}}
|
||||
\ouckey@inforow{考试方式}{\ouckey@mode}
|
||||
\ouckey@inforow{任课教师}{\ouckey@teacher}
|
||||
\ouckey@inforow{命题人}{\ouckey@examiner}
|
||||
\ouckey@inforow{满分}{\ouckey@ifempty{\ouckey@total}{}{\TNR{\ouckey@total}~分}}
|
||||
% ★ 抬头【不印考试日期】。
|
||||
% date 这个键仍保留(老文件写了 date= 不会报 unknown key),
|
||||
% 但不再渲染,对话框里也不显示,避免抬头行数随日期有无而浮动。
|
||||
|
||||
Reference in New Issue
Block a user