diff --git a/试卷/ouckey.sty b/试卷/ouckey.sty index 81529da..5eae83c 100644 --- a/试卷/ouckey.sty +++ b/试卷/ouckey.sty @@ -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), % 但不再渲染,对话框里也不显示,避免抬头行数随日期有无而浮动。