From 90086b8bb78d217f8ad8f4f1a7360715728f1584 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Wed, 16 Sep 2026 04:29:16 +0800 Subject: [PATCH 01/27] fix(template): honor class layout options Forward standard document options to ctexbook, document reserved no-op options, remove conflicting geometry constraints, validate page presets, and keep inserted verso pages empty. --- LALUbook.cls | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/LALUbook.cls b/LALUbook.cls index 356ef8c..2256f92 100644 --- a/LALUbook.cls +++ b/LALUbook.cls @@ -19,14 +19,18 @@ % Declaration of options % ---------------------- -\DeclareBoolOption[false]{draft} % 是否打上未定稿标记 +\DeclareBoolOption[false]{draft} % 预留:目前尚未实现未定稿标记 \DeclareBoolOption[true]{colors} % 是否让链接带颜色 -\DeclareBoolOption[false]{CJKthechapter} % 是否让天眉的各章编号使用中文数字 -\DeclareStringOption{coverpage} % 封面档档名, 默认为空 -\DeclareStringOption{fontsetup} % 字体设定档档名 -\DeclareStringOption{titlesetup} % 章节标题设定档档名 +\DeclareBoolOption[false]{CJKthechapter} % 预留:目前尚未实现中文章编号天眉 +\DeclareStringOption{coverpage} % 预留:目前尚未使用的封面档档名 +\DeclareStringOption{fontsetup} % 预留:目前尚未使用的字体设定档档名 +\DeclareStringOption{titlesetup} % 预留:目前尚未使用的章节标题设定档档名 \DeclareStringOption[16k]{geometry} % 版面(默认 16k) +% 将 10pt/11pt/12pt、oneside/twoside、openany/openright 等标准选项 +% 交由底层 ctexbook 处理。 +\DeclareDefaultOption{\PassOptionsToClass{\CurrentOption}{ctexbook}} + \PassOptionsToPackage{dvipsnames}{xcolor} % 让 xcolor 带 dvipsnames 选项; tikz 随后会载入之. \ProcessKeyvalOptions* @@ -49,6 +53,7 @@ % 设置天眉所需 \RequirePackage{fancyhdr} +\RequirePackage{emptypage} \RequirePackage{graphicx} \RequirePackage{float} @@ -93,21 +98,16 @@ right = 20mm, headheight = 5ex, headsep = 5ex, - textwidth = 132mm, - textheight = 198mm, - twoside, - asymmetric, % 单双数页不分 % bindingoffset = 18pt, - }}{} - -\IfStrEq{\@LALU@geometry}{a4}{% 载入 a4 版面设置 - \geometry{ - a4paper, - twoside, - asymmetric, - }}{} - -% 使空页恒空 + }}{% 载入 A4 版面设置或报告不支持的值 + \IfStrEq{\@LALU@geometry}{a4}{ + \geometry{a4paper} + }{ + \ClassError{LALUbook}{Unknown geometry `\@LALU@geometry'}% + {Supported geometry values are `16k' and `a4'.} + }} + +% 章首页不显示页眉;由 emptypage 保证双面排版所插入的空页恒空 \fancypagestyle{plain}{ \fancyhead{} \renewcommand{\headrulewidth}{0pt} From f128a026726ac91674d1e31181dd2bffe6bcc62d Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Wed, 16 Sep 2026 04:30:59 +0800 Subject: [PATCH 02/27] fix(document): stabilize cover and exercise counters Scale the 16k cover to the paper width without multi-axis cropping, record the case-only filename correction, stop creating duplicate exercise-group anchors, and evaluate answer counters explicitly for both regular and unfinished chapters. --- .../figs/cover-16k.pdf" | Bin ...5\345\255\246\350\256\262\344\271\211.tex" | 18 ++++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) rename "\350\256\262\344\271\211/figs/cover-16K.pdf" => "\350\256\262\344\271\211/figs/cover-16k.pdf" (100%) diff --git "a/\350\256\262\344\271\211/figs/cover-16K.pdf" "b/\350\256\262\344\271\211/figs/cover-16k.pdf" similarity index 100% rename from "\350\256\262\344\271\211/figs/cover-16K.pdf" rename to "\350\256\262\344\271\211/figs/cover-16k.pdf" diff --git "a/\350\256\262\344\271\211/\347\272\277\346\200\247\344\273\243\346\225\260\350\215\243\350\252\211\350\257\276\350\276\205\345\255\246\350\256\262\344\271\211.tex" "b/\350\256\262\344\271\211/\347\272\277\346\200\247\344\273\243\346\225\260\350\215\243\350\252\211\350\257\276\350\276\205\345\255\246\350\256\262\344\271\211.tex" index 018c405..711a473 100644 --- "a/\350\256\262\344\271\211/\347\272\277\346\200\247\344\273\243\346\225\260\350\215\243\350\252\211\350\257\276\350\276\205\345\255\246\350\256\262\344\271\211.tex" +++ "b/\350\256\262\344\271\211/\347\272\277\346\200\247\344\273\243\346\225\260\350\215\243\350\252\211\350\257\276\350\276\205\345\255\246\350\256\262\344\271\211.tex" @@ -102,13 +102,24 @@ \newcounter{exgroupcounter} \ExplSyntaxOn \newenvironment{exercise}{ - \iow_now:Nx \g_lalu_ans_iow { \cs_if_exist:cTF { @exist@LUchapter@\arabic{chapter} } { \exp_not:N \setcounter{LUsection}{ \theLUchapter-1 } \exp_not:N \LUsection } { \exp_not:N \setcounter{section}{ \thechapter-1 } \exp_not:N \section } { \cs:w @LALU@chaptername \cs_end: } } + \iow_now:Nx \g_lalu_ans_iow { + \cs_if_exist:cTF { @exist@LUchapter@\arabic{chapter} } + { + \exp_not:N \setcounter{LUsection}{ \int_eval:n { \value{LUchapter} - 1 } } + \exp_not:N \LUsection + } + { + \exp_not:N \setcounter{section}{ \int_eval:n { \value{chapter} - 1 } } + \exp_not:N \section + } + { \cs:w @LALU@chaptername \cs_end: } + } \setcounter{exgroupcounter}{0} \NewDocumentEnvironment{exgroup}{o}{ \IfValueTF{##1}{ \setcounter{exgroupcounter}{##1} }{ - \refstepcounter{exgroupcounter} + \stepcounter{exgroupcounter} } \subsection*{\Alph{exgroupcounter}组} \begin{enumerate} @@ -189,8 +200,7 @@ \includepdf[ pages={1}, noautoscale=true, - trim=0 0 0 10mm, - clip, + width=\paperwidth, ]{./figs/cover-16k.pdf} \songti From 40c9bb381023c3c7d0f0cf6f6db9543e8fe9835b Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Wed, 16 Sep 2026 04:39:49 +0800 Subject: [PATCH 03/27] fix(build): propagate index generation failures Disable imakeidx automatic shell-escape execution and let latexmk own both xindy rules. This preserves the currently empty symbol index while ensuring a nonzero xindy status fails the overall build. --- "\350\256\262\344\271\211/.latexmkrc" | 28 +++++++++++++++++++ ...5\345\255\246\350\256\262\344\271\211.tex" | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 "\350\256\262\344\271\211/.latexmkrc" diff --git "a/\350\256\262\344\271\211/.latexmkrc" "b/\350\256\262\344\271\211/.latexmkrc" new file mode 100644 index 0000000..ca49cb9 --- /dev/null +++ "b/\350\256\262\344\271\211/.latexmkrc" @@ -0,0 +1,28 @@ +# imakeidx writes one .idx file per index. Let latexmk run xindy so +# failures become build failures instead of being hidden by TeX shell escape. +$makeindex = 'internal lalu_xindy %S %D %O'; + +sub lalu_xindy { + my ($source, $destination, @latexmk_options) = @_; + + my @modules = $source =~ m{(?:^|[\\/])sym[.]idx\z} + ? ('-M', 'numeric-sort', + '-M', 'latex', + '-M', 'latex-loc-fmts', + '-M', 'makeindex') + : ('-M', 'texindy'); + + my $status = system( + 'xindy', + @latexmk_options, + @modules, + '-I', 'xelatex', + '-C', 'utf8', + '-o', $destination, + $source, + ); + + return 127 if $status == -1; + return 128 + ($status & 127) if $status & 127; + return $status >> 8; +} diff --git "a/\350\256\262\344\271\211/\347\272\277\346\200\247\344\273\243\346\225\260\350\215\243\350\252\211\350\257\276\350\276\205\345\255\246\350\256\262\344\271\211.tex" "b/\350\256\262\344\271\211/\347\272\277\346\200\247\344\273\243\346\225\260\350\215\243\350\252\211\350\257\276\350\276\205\345\255\246\350\256\262\344\271\211.tex" index 711a473..802655c 100644 --- "a/\350\256\262\344\271\211/\347\272\277\346\200\247\344\273\243\346\225\260\350\215\243\350\252\211\350\257\276\350\276\205\345\255\246\350\256\262\344\271\211.tex" +++ "b/\350\256\262\344\271\211/\347\272\277\346\200\247\344\273\243\346\225\260\350\215\243\350\252\211\350\257\276\350\276\205\345\255\246\350\256\262\344\271\211.tex" @@ -22,7 +22,7 @@ \usetikzlibrary{cd, arrows, arrows.meta, calc, intersections, decorations.pathreplacing, patterns, decorations.markings,angles,quotes, graphs, positioning, shapes.geometric} \pgfplotsset{compat=newest} -\usepackage[xindy, splitindex]{imakeidx} +\usepackage[xindy, noautomatic, quiet]{imakeidx} \makeindex[ columns=1, program=truexindy, From c9f292693c1dab55c8d61fc60bd6dad3c835d179 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Wed, 16 Sep 2026 04:49:09 +0800 Subject: [PATCH 04/27] fix(template): reject conflicting paper options Do not silently accept ctexbook paper-size or landscape options that are later overridden by the custom geometry setting. Report a class error and direct A4 users to geometry=a4; also clarify the current draft option semantics. --- LALUbook.cls | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/LALUbook.cls b/LALUbook.cls index 2256f92..d23ffd3 100644 --- a/LALUbook.cls +++ b/LALUbook.cls @@ -19,7 +19,7 @@ % Declaration of options % ---------------------- -\DeclareBoolOption[false]{draft} % 预留:目前尚未实现未定稿标记 +\DeclareBoolOption[false]{draft} % 预留:尚无自定义未定稿标记;全局 draft 仍会影响支持它的宏包 \DeclareBoolOption[true]{colors} % 是否让链接带颜色 \DeclareBoolOption[false]{CJKthechapter} % 预留:目前尚未实现中文章编号天眉 \DeclareStringOption{coverpage} % 预留:目前尚未使用的封面档档名 @@ -29,6 +29,17 @@ % 将 10pt/11pt/12pt、oneside/twoside、openany/openright 等标准选项 % 交由底层 ctexbook 处理。 +\newcommand*{\LALU@rejectpaperoption}{% + \ClassError{LALUbook}{Class option `\CurrentOption' conflicts with `geometry'}% + {Use `geometry=a4' for A4 paper. Supported layouts are `geometry=16k' and `geometry=a4'.}% +} +\DeclareVoidOption{a4paper}{\LALU@rejectpaperoption} +\DeclareVoidOption{a5paper}{\LALU@rejectpaperoption} +\DeclareVoidOption{b5paper}{\LALU@rejectpaperoption} +\DeclareVoidOption{letterpaper}{\LALU@rejectpaperoption} +\DeclareVoidOption{legalpaper}{\LALU@rejectpaperoption} +\DeclareVoidOption{executivepaper}{\LALU@rejectpaperoption} +\DeclareVoidOption{landscape}{\LALU@rejectpaperoption} \DeclareDefaultOption{\PassOptionsToClass{\CurrentOption}{ctexbook}} \PassOptionsToPackage{dvipsnames}{xcolor} % 让 xcolor 带 dvipsnames 选项; tikz 随后会载入之. From 8f5b1d637636e57957bf1e71009bcaf543a8086f Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Wed, 16 Sep 2026 05:02:26 +0800 Subject: [PATCH 05/27] docs(template): outline structural refactors Record a staged design for replacing cross-chapter groups, hardening answer extraction, and consolidating theorem and proof semantics while preserving the regular-chapter and unfinished-topic numbering contract. --- docs/template-refactor-plan.md | 577 +++++++++++++++++++++++++++++++++ 1 file changed, 577 insertions(+) create mode 100644 docs/template-refactor-plan.md diff --git a/docs/template-refactor-plan.md b/docs/template-refactor-plan.md new file mode 100644 index 0000000..88eae2f --- /dev/null +++ b/docs/template-refactor-plan.md @@ -0,0 +1,577 @@ +# LALU 模板重构方案 + +> 状态:设计稿。本文只描述后续实现方案,不改变当前任何 TeX、索引或构建行为。 + +## 目标与不变量 + +本次重构的目标不是改变讲义的编排,而是把目前依赖跨章节分组、动态控制序列标记和隐式文件副作用的实现,替换成可检查、可测试的显式状态模型。 + +必须保持以下用户可见约定: + +- 普通正文继续使用 `\chapter`,显示为“第 N 讲”。 +- 未竟专题继续使用 `\LUchapter{标题}`;标题编号显示为“未竟专题 X”,其中 `X` 是未竟专题的全局中文序号。 +- 未竟专题的数字引用继续由“上一普通正文编号 + 希腊字母”组成。例如第一讲后的前两个未竟专题分别引用为 `1ε`、`1δ`。 +- 未竟专题内的节、公式、图表和定理继续沿用该数字引用,例如 `1ε.2`。 +- 正文和答案册中的既有标签键、目录层级及主要版式应保持兼容;确需改变的辅助文件格式必须通过干净重编译迁移。 +- 所有 Hyperref destination 必须唯一、稳定、只含适合机器处理的 ASCII 内容;显示编号不得直接充当锚点编号。 + +以下三个概念应始终分离: + +| 概念 | 示例 | 用途 | +| --- | --- | --- | +| 标题显示编号 | `未竟专题十四` | 章标题、答案册单元标题、目录章条目 | +| 数字引用编号 | `23ε` | `\ref`、节/公式/定理的父编号 | +| 机器锚点编号 | `chapter.LU.14` | Hyperref destination、外部 PDF 链接 | + +## 一、正文、未竟专题与答案册单元 + +### 1.1 统一的章节记录 + +正文和答案抽取器共享一个只读的“当前章节记录”。建议由一个内部模块维护下列字段,而不是通过 `\@exist@...` 控制序列是否存在来反推状态: + +| 字段 | 含义 | 示例 | +| --- | --- | --- | +| `phase` | `frontmatter`、`mainmatter`、`appendix` 或 `backmatter` | `mainmatter` | +| `kind` | `normal`、`unfinished`、`appendix` 或 `unnumbered` | `unfinished` | +| `structural-chapter` | 标准 `c@chapter` 的当前结构流水号 | `37` | +| `normal-display` | 普通正文的显示编号 | `23` | +| `normal-base` | 最近一个普通正文的阿拉伯数字编号 | `23` | +| `unfinished-serial` | 未竟专题的全局流水号 | `14` | +| `unfinished-slot` | 同一普通正文之后的未竟专题次序 | `1` | +| `unnumbered-serial` | 实际无编号顶层标题的内部流水号,仅用于必要的唯一锚点 | `3` | +| `title` | 完整标题,保持未展开 token | `范畴论视角下的线性代数` | +| `short-title` | 页眉、目录所用短标题;缺省时等于完整标题 | 同上 | +| `unit-id` | 稳定的逻辑单元标识 | `lu:14` | + +实现上可使用 expl3 的全局 token list、integer 和 boolean;对外只暴露查询命令,不允许正文直接改内部变量。至少提供以下语义接口: + +- 进入普通正文单元; +- 进入未竟专题单元; +- 进入无编号章节边界; +- 取得当前记录的不可变快照,供答案抽取器使用; +- 判断当前是否存在可承载习题的编号单元。 + +状态转换必须幂等。“离开未竟专题”调用两次不应产生额外 `\endgroup`、计数器变化或格式变化。未竟专题不得通过一个跨越整章的未闭合 TeX 分组维持状态。 + +### 1.2 正文计数器与转换规则 + +首选实现应把“结构上的顶层单元次序”和“普通正文显示编号”彻底分离,不再把标准 `chapter` 先减一再让 `\chapter` 加回。各计数器的职责为: + +| 计数器 | 职责 | 是否直接显示 | +| --- | --- | --- | +| 标准 `chapter`(即 `c@chapter`) | 所有**有编号顶层单元**的结构流水号;驱动标准 reset 链和 package hook | 否 | +| 新增 `LALUnormalchapter` | 只在普通正文开始时递增,表示“第 N 讲”中的 `N` | 是 | +| `LUchapter` | 只在未竟专题开始时递增,表示“未竟专题 X”中的全局序号 | 是 | +| `LUgreekchap` | 同一普通正文之后的未竟专题 slot | 通过希腊字母显示 | +| 新增 `LALUappendixchapter` | 附录显示编号 `A`、`B`…… | 仅 appendix 模式 | +| 内部 `LALUunnumberedunit` | 每个实际无编号顶层标题的锚点流水号;阶段命令本身不递增 | 否 | + +因此,普通编号章、未竟专题和编号附录都必须调用最终的标准 `\chapter`。每次这样的编号调用都会使标准 `c@chapter` 自然递增一次,LaTeX 注册在 chapter 下的 section、equation、figure、table、定理等 reset 以及 Hyperref/CTeX/其他 package hook 也自然执行一次。结构 counter 不再为了显示编号而回退、复用或伪造数值。 + +普通 `\chapter` 适配器必须先按 phase 和标准编号判定分派:mainmatter 的编号章走 normal 路径,appendix 的编号章走 appendix 路径,frontmatter/backmatter 中的章以及被 `secnumdepth` 抑制编号的章走 unnumbered 路径。星号形式始终走 unnumbered 路径;`\LUchapter` 则是只允许 mainmatter 编号路径的独立入口。这样同一个公共 `\chapter` 不会被“普通章流程只接受 mainmatter”的前置条件误伤。 + +普通章的编号路径为: + +1. 确认当前处于 `mainmatter`,并采用与标准 chapter 相同的“本次是否编号”判定。若受 `secnumdepth` 等设置影响而不编号,则不得改动任何显示计数器,而应按无编号边界更新状态后调用标准章命令。 +2. 仅在确定编号后设置 `kind=normal`,递增 `LALUnormalchapter`,把该值写入当前记录的 `normal-display` 和 `normal-base`,并把 LU slot 重置为 0。 +3. 调用最终标准 `\chapter`;它递增结构 `c@chapter` 并执行全部标准 reset/hook。 +4. `\thechapter` 在本次调用期间从 `LALUnormalchapter` 取显示值,而不是从 `\arabic{chapter}` 取值。 + +未竟专题转换流程为: + +1. 要求当前处于 `mainmatter`、已经存在普通正文,并且标准 chapter 判定本次会编号;任一条件不满足都必须在修改任何计数器或样式前给出明确的 package error,而不是生成 `0ε` 或一个半初始化单元。 +2. 将当前 `LALUnormalchapter` 快照为 `normal-base`,设置 `kind=unfinished`,并分别递增全局 `LUchapter` 与当前普通章的 `LUgreekchap`。普通章入口已经把 slot 归零,因此这里不再覆盖 `normal-base` 后做含义不清的“是否同章”比较。 +3. 调用同一个最终标准 `\chapter`;结构 `c@chapter` 再递增一次并执行全部标准 reset/hook。 +4. `\thechapter` 在本次调用及其正文期间返回保存的 `normal-base + Greek(slot)`;章标题的中文序号则来自 `LUchapter`。 + +`\chapter*` 结束当前编号单元但不清除最近普通章及其 LU slot。因而随后在 `mainmatter` 中再次调用 `\LUchapter` 是允许的,并继续使用下一个 slot;例如 `chapter A -> LU B -> chapter* -> LU C` 的两个 LU 引用仍依次为 `1ε`、`1δ`。如果希望星号章成为禁止后续 LU 的更强边界,应另设显式命令,不能通过重置 slot 得到可能重复的编号。 + +appendix 中的编号章由同一个 `\chapter` 适配器按 `phase` 分派: + +1. 确认当前处于 `appendix` 且标准 chapter 判定本次会编号。 +2. 设置 `kind=appendix`,递增 `LALUappendixchapter`,并记录本次附录显示字母。 +3. 调用同一份最终标准 `\chapter`,让结构 `c@chapter` 递增并执行标准 reset/hook。 +4. 本次调用及其正文中的 `\thechapter` 返回 `\Alph{LALUappendixchapter}`,锚点进入 `APP` 命名空间。该路径不得递增或重置普通正文、LU、slot 计数器。 + +unnumbered 路径在调用最终标准章命令前设置 `kind=unnumbered`、清空当前 `unit-id`,并为每个实际排出的无编号顶层标题递增一次内部 `LALUunnumberedunit`。它保留最近普通章、LU slot 和所有显示计数器,不递增结构 `chapter`,也不伪造编号章的 reset;仅 phase 命令本身不算一个无编号标题,不能递增该内部 serial。 + +以上路径都必须“先验证、后变更”;验证失败不得遗留已经递增的逻辑计数器、切换一半的 CTeX 样式或不完整的当前记录。 + +例如 `chapter A -> LU B -> LU C -> chapter D` 完成后,结构 `c@chapter` 依次为 1、2、3、4,普通显示计数器依次为 1、1、1、2;可见引用依次为 `1`、`1ε`、`1δ`、`2`。 + +现有希腊字母序列的前六项必须保持为: + +1. `ε` +2. `δ` +3. `λ` +4. `μ` +5. `φ`(正文仍使用当前 `\varphi` 字形) +6. `θ` + +该序列应由一张集中配置表生成正文形式和 PDF 字符串形式。超过已配置容量时应给出带有当前普通正文编号和所需 slot 的明确错误;如需扩展,只追加映射,不得改变既有六项顺序。 + +普通 `\chapter` 和 `\chapter*` 需要经过类在导言区末尾安装的完整签名适配器,或等价且经过验证的命令 hook。适配器必须: + +- 在 Hyperref、CTeX、fncychap 等完成补丁后安装,不能像当前实现一样提前保存一个可能过期的 `\chapter` 副本; +- 保留星号形式、短标题和长标题; +- 在普通章调用前恢复普通章状态和样式; +- 让 `\chapter*` 安全结束未竟专题显示状态,但不改变最近的普通正文编号;重复执行“离开 LU”的清理必须幂等,而每个实际排出的星号章只递增其内部 unnumbered anchor serial; +- 以未展开形式保存标题,不通过 `x` 型展开写入答案文件。 + +这里“最终标准 `\chapter`”指 Hyperref、CTeX、fncychap 等完成补丁后的实现。公共 chapter 适配器的 normal、appendix、unnumbered 分支和 `\LUchapter` 都只能调用这一份实现;不得各自复制 `\@chapter`、分页、写目录或 reset 代码。 + +#### 文档阶段和无编号边界 + +结构 counter 与状态在特殊文档阶段遵循以下规则: + +| 操作/阶段 | 结构 `c@chapter` | 显示计数器与状态 | +| --- | --- | --- | +| `\frontmatter` | 不重置为可见章号;无编号章不递增 | `phase=frontmatter`、`kind=unnumbered`;首次进入时普通/LU 显示计数器仍为 0;默认禁止 `\LUchapter` | +| 首次 `\mainmatter` | 初始化本书的结构与显示计数状态 | `phase=mainmatter`、`kind=unnumbered`、当前单元为空;首个普通章将 `LALUnormalchapter` 增至 1 | +| `\chapter*` | 不递增结构/显示 counter,也不额外触发编号章 reset | `kind=unnumbered`;结束 LU 的显示状态,但保留“最近普通正文”快照;清理可重复执行,每个实际标题取得新的内部 unnumbered serial | +| `\backmatter` | 后记、索引等无编号顶层单元不递增 | `phase=backmatter`、`kind=unnumbered`;清空当前单元,冻结普通/LU/slot 计数,默认禁止 `\LUchapter` | +| 首次 `\appendix` | **不把结构 `c@chapter` 清零** | `phase=appendix`、`kind=unnumbered`;清空当前单元,冻结普通正文和 LU 计数,将 appendix 显示计数器初始化为 0 | +| appendix 中的编号 `\chapter` | 每个附录递增一次 | `kind=appendix`,显示 `A`、`B`……;默认禁止 `\LUchapter`,因为“上一正文章节 + 希腊字母”不适用于字母附录 | + +支持的阶段顺序为 `frontmatter -> mainmatter -> appendix -> backmatter`,其中 frontmatter 和 appendix 均可省略。同一阶段命令的重复调用必须幂等:重复 `\mainmatter` 不得重置结构、普通正文、LU 或 slot 计数器,重复 `\appendix` 不得把附录显示计数器清零并重新产生 `A`;重复 front/mainmatter 也不得把页码重新切到 1 而制造重复 page destination。首次合法转换可以保留底层标准类原有的清页和页码模式切换,重复调用则应成为无状态变化的空操作或只发一次诊断。任何倒退到较早阶段的调用都应明确报错,除非未来另行定义并测试一套受支持的回退协议。 + +标准类的 `\appendix` 通常会把 `c@chapter` 清零;新适配层不能沿用这一计数器副作用。它只应用附录名称/显示格式,并在首次进入 appendix 时初始化 `LALUappendixchapter`,结构 `c@chapter` 继续单调递增。附录锚点使用独立 ASCII 命名空间,例如 `chapter.APP.A`。如果未来确需“附录后的未竟专题”,必须先单独定义其显示和引用契约,不能隐式复用正文规则。 + +在 `frontmatter`、首次 `\mainmatter` 与首个普通章之间、`backmatter`、`chapter*` 之后以及 `\appendix` 与首个附录章之间调用 `exercise` 时,章节状态 API 应报告“没有可映射的编号单元”,不得沿用上一个编号章的答案映射。第一阶段的答案 schema 只支持普通正文和 LU 单元,因此附录章中的 `exercise` 也应给出明确错误;若未来要支持附录习题,必须同时补齐 appendix 的 `unit-id`、答案标题、显示编号、锚点和回归测试,而不是把它伪装成普通章。 + +`\LUchapter` 的兼容签名仍接受现有的单个必选参数,同时可新增可选短标题: + +```tex +\LUchapter{完整标题} +\LUchapter[目录与页眉短标题]{完整标题} +``` + +### 1.3 显示编号、引用编号与锚点 + +`\thechapter` 应成为一个由当前状态分派的稳定命令: + +- 普通正文返回 `\arabic{LALUnormalchapter}`。 +- 未竟专题返回保存的 `normal-base` 加当前希腊字母的正文形式。 +- 附录返回 `\Alph{LALUappendixchapter}`。 +- 无编号状态返回空的、可展开 token list,并且语义查询 API 同时报告“没有当前编号单元”;绝不能悄悄返回上一个普通章或 LU 的显示编号。若 Hyperref 或其他包需要为实际的无编号顶层标题建立内部 destination,则另用单调递增、不可见的 unnumbered serial,使 `\theHchapter` 返回 `UNNUMBERED.`,而不占用结构或显示 counter。 + +CTeX 的章标题编号应使用另一条格式化路径: + +- 普通正文:前缀“第”,编号为普通正文编号,后缀“讲”。 +- 未竟专题:前缀“未竟专题”,编号为 `\zhnumber{unfinished-serial}`,无“讲”后缀。 +- 附录:沿用当前类/CTeX 的附录标题格式,但编号必须来自 `LALUappendixchapter`,不能来自结构 `chapter`。 +- 无编号状态:沿用标准星号章或 front/backmatter 的无编号标题路径,不读取任何旧单元编号。 + +优先把各套 CTeX 设置封装为命名样式,并在状态转换时显式、幂等地应用;如 CTeX 支持可靠的动态格式命令,也可由一个分派器生成。不得再以跨章分组恢复 CTeX 设置。 + +章标题/目录编号与引用编号必须有独立 provider。建议分别提供 `\LALUChapterTitleNumber`、`\LALUChapterTocNumber` 和 `\LALUCurrentReferenceNumber`:普通章三者都基于 `N`,未竟专题前两者生成“未竟专题十四”而引用 provider 生成 `23ε`,附录前两者生成类所约定的附录形式而引用 provider 生成 `A`。`\chapter` 适配器应通过 CTeX/标准章命令支持的格式接口,把 title/TOC provider 交给同一次标准章调用;如果所支持的 CTeX 版本没有独立 TOC 接口,只能在一个集中且有版本测试的窄适配层中替换该次标准目录条目的 `\numberline` 参数。不得为修正编号再写第二条 `\addcontentsline`,也不得把 `\thechapter` 临时改成中文标题编号后泄漏到标签或子计数器。 + +Hyperref 锚点规则如下: + +| 对象 | `\theH...` 约定 | 示例 | +| --- | --- | --- | +| 普通正文 | 使用普通显示计数器,保持既有 chapter 锚点外观 | `chapter.23` | +| 未竟专题 | `LU.` | `chapter.LU.14` | +| 未竟专题内 section | 从唯一章锚点派生 | `section.LU.14.2` | +| 附录 | `APP.` | `chapter.APP.A` | +| 实际无编号顶层标题 | `UNNUMBERED.`,仅作内部唯一性用途 | `chapter.UNNUMBERED.3` | +| 答案单元 | `answer.normal.` 或 `answer.LU.` | `LALUanswerunit.answer.LU.14` | +| 习题组 | 包含答案单元 ID 和组次序 | `LALUexgroup.lu.14.C` | + +锚点表示必须完全可展开并只含 ASCII。`\theHchapter` 同样按 `kind` 分派:普通章返回普通显示编号,未竟专题返回 `LU.`,附录返回 `APP.`,无编号状态在确需 chapter 型锚点时返回 `UNNUMBERED.`。它不能直接使用结构 `c@chapter`,否则在插入未竟专题后会改变普通章既有外链。`LUchapter` 只是标题流水号时使用 `\stepcounter`;真正承载紧随其后 `\label` 的标准 `chapter` 才使用 `\refstepcounter`,避免创建无用的第二锚点。 + +标准 `\chapter*` 若已由 Hyperref 使用自己的 `chapter*.` 命名空间,就继续沿用该机制;`UNNUMBERED.` 只是其他包确实展开 `\theHchapter` 时的确定性后备值,不能再额外创建一个与标准星号章重叠的 destination。 + +为了最大限度兼容已有 PDF 外链,新实现应优先保留当前未竟专题锚点 `chapter.LU.`。普通章节锚点保持标准形式。若任何锚点必须改变,应在发布说明中列出,并考虑在一个迁移版本中添加旧 destination 别名。 + +默认引用语义保持如下: + +- `\ref`:普通正文为 `23`,未竟专题为 `23ε`。 +- `\nameref`:返回章节标题。 +- `\autoref`:兼容现有 chapter 类型,输出“章 23ε”。若未来希望输出“未竟专题 23ε”,应作为单独的、有迁移说明的引用类型变更,而不在本次底层重构中夹带改变。 +- `\cref`:与 `\autoref` 保持相同对象类型,格式由类集中配置。 + +### 1.4 目录与 PDF 书签 + +目录和书签应复用标准章命令产生的单一条目,不同时手写第二个 `\addcontentsline`。期望结果为: + +- 普通正文目录条目显示“第 23 讲 标题”。 +- 未竟专题目录的章级条目显示“未竟专题十四 标题”,而不是 `23ε 标题`。 +- 未竟专题内 section 的目录编号显示 `23ε.1`。 +- PDF 书签使用 Unicode 希腊字符,例如 `23ε.1`,不得含 `$...$`、`\boldsymbol` 或其他 PDF string 不可用命令。 +- 目录条目和书签指向同一个唯一章锚点。 + +正文希腊字形与 PDF 字符串字形应分别定义,并通过 `\pdfstringdefDisableCommands` 或一个经过测试的 `\texorpdfstring` 边界连接。不能假定嵌入 `\thechapter` 的数学命令在写入 `.toc`、生成书签和二次读取时都以相同方式展开。 + +### 1.5 `LALU-answers` 中的答案单元 + +答案册不应再把普通章节和 `\LUsection` 分别实现为两套跨 section 分组逻辑。建议引入专用逻辑计数器 `LALUanswerunit` 和统一渲染接口: + +```tex +\LALUAnswerUnit{ + kind=unfinished, + normal-base=23, + unfinished-serial=14, + unfinished-slot=1, + title={范畴论视角下的线性代数}, + unit-id={lu:14} +} +``` + +它负责: + +- `\refstepcounter{LALUanswerunit}` 并重置属于答案单元的公式、定理和组计数器; +- 以 section 层级排版标题、写入目录和页眉,但不篡改全局 `section` 的意义; +- 普通单元显示“第 N 讲”,未竟单元显示“未竟专题 X”; +- `\theLALUanswerunit` 对普通单元返回 `N`,对未竟单元返回 `Nε`; +- 使用独立、唯一的 `\theHLALUanswerunit`; +- 在生成答案部分结束后无需把 `\thesection` 清空,因此不再破坏后续历年卷的公式编号。 + +可使用 `\section*` 加一次受控的目录/书签写入来保持现有视觉层级;公式和定理通过专用 `LALUanswerunit` 建立 reset 关系。这样答案册后半部分的普通 `\chapter`、`\section` 完全不受生成答案部分影响。 + +新答案清单必须携带完整章节记录,不能让答案册根据“上一个 section 是什么”猜测 `normal-base` 或希腊 slot。`\LUsection` 保留为过渡包装器,仅用于读取旧的已生成文件,并给出一次性弃用警告;干净构建生成的新文件不得再输出 `\setcounter{LUsection}{...}` 或 `\LUsection`。 + +### 1.6 兼容迁移 + +迁移期建议遵循以下策略: + +- `\LUchapter{...}` 原调用不需要修改;新增短标题是向后兼容扩展。 +- 暂时保留 `LUchapter`、`LUgreekchap`、`LUsection` 计数器名字,并由新状态模型同步其值;新增的 `LALUnormalchapter` 是普通正文显示编号的权威来源。 +- `\LUgroupsancheck` 在一个过渡版本中保留为空操作并发出弃用提示,使主文件尾部或缓存生成文件不会立即报未定义;确认仓库内调用移除后再删除。 +- 旧的 `\@exist@LUchapter@...`、`\@exist@LUsection@...` 不再作为真值来源。若外部文档确有探测,可在过渡版只生成兼容标记,但内部逻辑不得读取它们。 +- 保留已有标签键和 `chapter.LU.` destination;所有 `.aux`、`.toc`、`.out`、`.fdb_latexmk` 应在切换实现时统一清理并完整重编译。 +- 新旧答案生成器先并行输出到不同构建目录,比较单元顺序、标题、组号、题号和答案正文;一致后才切换 `LALU-answers.tex` 的输入。 +- 旧 `LALU-ans-contents.tex` 是可丢弃构建产物,不承诺由新 reader 永久支持。过渡包装器只用于平滑升级,不能成为新状态模型的长期分支。 + +这一首选模型会有意改变裸 `\value{chapter}`、`\arabic{chapter}` 和直接读取 `c@chapter` 的内部语义:它们返回的是“第几个有编号顶层单元”,不再等于普通正文的显示编号。迁移前必须全仓库扫描这些用法,并根据意图替换为语义 API: + +- 需要普通正文显示编号时使用 `\LALUNormalChapterNumber` 或对应 expl3 查询接口; +- 需要当前可见引用时使用 `\thechapter`/`\LALUCurrentReferenceNumber`; +- 需要判断普通章或未竟专题时读取 `kind`,不能比较计数器; +- 只有 reset、checkpoint 或底层诊断代码可以读取结构 `c@chapter`。 + +当前仓库扫描结果是:`\value{chapter}`、`\arabic{chapter}`、`\setcounter{chapter}`、`\addtocounter{chapter}` 或 `c@chapter` 这类 raw 模式只出现在主模板现有章节逻辑中;答案模板还会重定义 `\thechapter` 并根据章节状态组织答案,因此两份模板都必须迁移,而正文内容没有直接依赖这些 raw 模式。迁移后应对全部手写源码启用静态检查。检查只允许统一章节模块中为 reset、checkpoint 或底层断言而登记的少量结构用途白名单,并排除 `.aux` 等生成文件;不能仅检查正文而放过模板内部继续把 raw chapter 当显示号的代码。 + +如果实际外部使用者证明必须保持“裸 `chapter` 值等于普通正文显示编号”,才启用次选兼容方案:在一个严格封装且带断言的适配器中,未竟专题调用前把 `chapter` 临时减一,再交给标准 `\chapter` 加回。该方案仍优于跨章开放分组,但会复用结构 counter、增加异常恢复复杂度,也使 hook 观察到的计数值依赖调用阶段;它不应作为默认设计。 + +### 1.7 章节与链接测试矩阵 + +最小回归文档应覆盖下列序列: + +| 场景 | 预期标题/引用 | 重点断言 | +| --- | --- | --- | +| `chapter A` | 第 1 讲;`\ref=1` | 结构 counter=1,普通显示 counter=1,锚点 `chapter.1` | +| `chapter A -> LU B` | 未竟专题一;`\ref=1ε` | 结构 counter=2,普通显示 counter 仍为 1 | +| `chapter A -> LU B -> LU C` | 未竟专题一/二;`1ε`/`1δ` | 结构 counter=3;slot 与 LU 流水号连续 | +| 上述序列后 `chapter D` | 第 2 讲;`\ref=2` | 结构 counter=4,普通显示 counter=2,锚点 `chapter.2` | +| 新普通章后首个 LU | `2ε` | slot 正确重置为 1 | +| 同章后第六个 LU | `Nθ` | 既有六项映射不变 | +| 同章后第七个 LU | 明确错误 | 不产生空编号或重复锚点 | +| `LU -> chapter* -> chapter* -> chapter` | 最终普通章正常 | 两个星号章不递增结构/显示 counter,各自内部锚点唯一;无错误分组 | +| `chapter -> LU -> chapter* -> LU` | `1ε` 后为 `1δ` | 星号章不重置最近普通章或 slot;第二个 LU 建立新结构单元 | +| frontmatter 中的 `\chapter` 与 `\chapter*` | 无编号标题正常 | 结构、普通、LU counter 均不递增,current unit 为空,LU 调用被拒绝 | +| mainmatter 首个普通章之前调用 LU 或 `exercise` | 明确错误 | 当前单元为空,所有 counter 与样式保持初始状态 | +| mainmatter 中重复调用 `\mainmatter` | 章节编号和页码连续 | 不重置结构、普通、LU、slot 或页码,不产生重复 page destination | +| backmatter 中的 `\chapter`、`\chapter*` 与 index | 无编号标题正常 | current unit 为空,counter 全部冻结,不依赖手工 sanity check | +| 重复 `\appendix` 或从 appendix/backmatter 回到 mainmatter | 前者保持下一附录字母,后者明确报错 | 不重置 appendix/结构 counter,不产生重复 `A` | +| `appendix -> chapter A -> chapter B` | 附录 A/B | 结构 counter 各递增一次;标题、ref、TOC、outline 使用附录 provider;普通/LU counter 冻结;锚点在 APP 命名空间;chapter-dependent counter 正常 reset | +| appendix 入口空隙或附录章中的 `exercise` | 明确错误 | 不沿用最后一个正文单元;首阶段不生成不完整的 appendix 答案记录 | +| 降低 `secnumdepth` 后的普通章与 LU | 普通章走无编号路径,LU 原子失败 | 不预先递增任何逻辑 counter,不残留 LU 样式 | +| 带短标题和数学标题 | 正文、目录、书签各自正确 | 无 PDF string 警告 | +| LU 内 section/equation/theorem/figure | 均以 `Nε` 为父编号 | 对应 `\theH...` 唯一 | +| 正文 `\label` + `xr` 外部引用 | 数值和名称正确 | 冷启动与增量构建一致 | +| `\include`/`\includeonly` 跨普通章、LU、附录 | 全量与选择构建状态一致 | LaTeX checkpoint 恢复全部 counter;非 counter 状态由可重建记录恢复或明确禁止不安全的选择构建 | +| 答案普通单元 -> LU 单元 -> 普通单元 | 显示与正文映射一致 | 后续历年卷 section/公式不受污染 | +| 全仓手写源码静态扫描 | raw chapter 只剩登记的结构用途 | 模板和正文均无把 `value/arabic/setcounter/addtocounter{chapter}` 或 `c@chapter` 当显示号的调用;生成文件被排除 | + +测试至少在受支持的最旧 TeX Live 与当前 TeX Live 上使用 XeLaTeX 执行。若 LuaLaTeX 不在支持范围,应在类加载时明确拒绝,而不是让字体、索引或书签在中途失败。日志检查必须把以下内容视为失败: + +- `destination with the same identifier`; +- 未定义引用或多轮后仍要求 rerun; +- 由章节编号产生的 `Token not allowed in a PDF string`; +- 章节状态断言失败; +- 目录和 PDF outline 中出现重复章条目。 + +## 二、答案抽取器 + +### 2.1 单一源数据与记录模型 + +答案的权威源仍是作者编辑的源码;任何生成的 `.tex` 或 manifest 都是可删除、不可手工编辑的构建产物。每条答案记录至少包含: + +- schema 版本; +- 主文档身份和源文件哈希; +- 当前章节记录的完整快照; +- 习题组序号及显示标签; +- 题目在组内的逻辑序号、显示标签和稳定 ID; +- 答案状态:`answered`、`todo` 或 `omitted`; +- 正文载荷类型:`inline-token` 或 `file`; +- 可选源文件与行号,仅供诊断。 + +稳定 ID 的推荐缺省形式为 `//`,例如 `lu:14/C/2`。作者可显式指定更稳定的语义 ID;一旦发布,不应因插入前一题而改变已有 ID。 + +生成文件应是语义事件流,而不是预先写死 `\section`、`\setcounter`、`\subsection*` 等版式命令。例如 manifest 只允许调用: + +```tex +\LALUAnswerManifestBegin{...} +\LALUAnswerUnit{...} +\LALUAnswerGroup{...} +\LALUAnswerItem{...}{...} +\LALUAnswerManifestEnd{...} +``` + +答案册 reader 负责把记录渲染成标题和列表。这样正文编号策略改变时无需重新设计文本序列化格式。 + +### 2.2 编号与普通/未竟映射 + +进入 `exercise` 时,抽取器必须从章节状态 API 取得快照,禁止再通过某个动态控制序列是否存在来判断普通章或未竟专题。 + +编号规则如下: + +- `exgroup` 使用普通 `\stepcounter`;若组本身支持 `\label`,其 Hyperref ID 必须包含 unit ID 和组序号。 +- 可选显式组号先设置逻辑值,再通过同一条组初始化路径生成标题和锚点;不能让显式组号分支跳过锚点或 reset。 +- 答案题号来自与题目绑定的逻辑 exercise item,而不是在 `answer` 结束时临时读取固定的 `\theenumi`。 +- 嵌套 enumerate 只影响答案正文内部编号,不得改变所关联的外层题号。 +- 答案册使用 manifest 中的 `normal-base`、`unfinished-serial` 和 `unfinished-slot` 渲染单元,绝不根据前一个已输出答案单元推算。 + +迁移初期可自动生成稳定 ID 并记录告警;中长期建议提供 `\exitem[id=...]` 或等价接口,让题目和答案的关联显式化。 + +### 2.3 token 保真与 verbatim + +单一接口不能同时可靠地捕获任意 TeX token 和 verbatim 内容,因此应明确提供两种载荷: + +1. `inline-token`:兼容当前 `\begin{answer}...\end{answer}`。支持平衡花括号、段落和普通宏;以未展开 token 保存,禁止使用 `\tl_to_str:n`、`x` 型完全展开或 `\scantokens`。该模式明确不支持 verbatim、minted 及依赖特殊 catcode 的内容。 +2. `file`:答案正文保存在独立 `.tex` 片段中,manifest 只记录路径,答案册在正常输入阶段 `\input` 它。verbatim、listing、复杂 catcode 和很长答案必须使用该模式。 + +不建议编写一个外部正则表达式解析器去“理解”任意 TeX 并复制 `answer` 环境:嵌套环境、注释、条件分支和 verbatim 会使该方案不可可靠。迁移脚本可以机械地拆分已知格式,但拆分结果必须经过 TeX 编译和内容对比。 + +标题、题号等元数据与答案正文分开序列化。元数据需要 PDF 字符串时单独转换;不得为了生成元数据而展开正文。生成文件应保留合理换行,避免每条长答案变成单个超长输入行。 + +需要额外验证答案正文中的以下对象: + +- `\label`、`\ref`、`\nameref` 和从正文导入的 `xr` 标签; +- 浮动体与图片相对路径; +- 脚注、索引项、局部宏; +- display math 末尾的 QED; +- verbatim 文件载荷。 + +为避免答案册自身标签与 `\externaldocument` 导入的正文标签相撞,建议给外部标签添加固定前缀并提供 `\mainref`/`\mainnameref` 辅助命令;是否启用应先以标签清单验证兼容性。 + +### 2.4 空答案与占位语义 + +“没有 `answer` 环境”和“显式存在一个空 `answer` 环境”都不应默认制造答案册空白页。建议规则为: + +- 单元中没有 `answered` 或显式 `todo` 记录时,不输出该答案单元、目录条目或 `\clearpage`。 +- 组中没有可见记录时,不输出组标题。 +- 纯空白的兼容 `answer` 正文默认为 `omitted`,构建日志报告数量。 +- 作者希望保留占位时必须显式写 `status=todo`;reader 统一显示“暂无答案”或配置的占位文字。 +- 可提供仅供编辑审校的 `include-empty-units=true`,正式发布构建固定为 false。 + +每次生成结束应报告:章节单元数、习题组数、题目数、已答数、todo 数、空答案数和被省略单元数。异常下降可由 CI 阈值发现。 + +### 2.5 原子生成与失败恢复 + +TeX 主进程不应直接截断最后一个可用的正式答案文件。建议由构建辅助程序实施以下事务: + +1. 在目标构建目录创建同一文件系统上的唯一临时目录或临时文件。 +2. 主文档只向临时目标写 manifest 和 inline body。 +3. 写入 schema header、记录总数以及结束 sentinel;关闭所有 stream。 +4. 辅助程序验证退出状态、sentinel、记录计数、所有 file 载荷和依赖均存在,并可选择执行一次 reader 语法检查。 +5. 验证成功后,以原子 rename 替换当前 manifest;验证失败则保留上一份成功产物并删除或隔离临时文件。 + +纯 TeX 无法跨平台可靠地完成最终原子 rename;该动作应放在 latexmk rule 或小型构建脚本中,而不是依赖 `\write18`。manifest 应带 schema 版本和主源码指纹,答案册发现不完整、过期或版本不兼容时必须失败,不能静默排版旧答案。 + +### 2.6 并发、jobname 与构建目录 + +固定文件名 `LALU-ans-contents.tex` 不再作为内部协议。输出身份至少由以下内容组成: + +- 显式 ASCII jobname; +- 主文件规范化路径的短哈希,防止两个同名工程相撞; +- 会影响答案内容的构建变体; +- schema 版本。 + +推荐目录形如: + +```text +build/answers/LALU-/ + manifest.tex + bodies/ + generation.json +``` + +不同 jobname 或工程可并行写不同目录。同一身份的并发构建使用每进程唯一临时目录,并通过锁或 compare-and-swap 决定谁发布最终 manifest;绝不能共同写同一个 open stream。答案册由构建命令显式传入 manifest 路径,源码中不硬编码当前工作目录。 + +`make clean` 只删除已知构建目录,不删除作者维护的 file 载荷。生成内容不写回源码目录,也不参与版本控制。 + +### 2.7 答案抽取器测试 + +至少建立以下 fixture: + +- 普通章 A/B/C 三组,部分题有答案; +- 普通章后的两个连续未竟专题,验证 `Nε`、`Nδ` 与中文全局序号; +- 没有任何答案的 exercise、空组、空 answer、显式 todo; +- 自定义起始组号和超过 9 的题号; +- 答案含多段、嵌套 enumerate、宏参数符号、注释、Unicode、标签和浮动体; +- file 载荷含 verbatim/listing; +- 主编译在写到一半时故意失败,旧 manifest 仍可用; +- 两个不同 jobname 并行构建以及同一 jobname 的竞争构建; +- manifest 缺尾标、记录计数错误、源哈希过期、file 载荷缺失时 reader 明确失败; +- 新旧生成器的记录顺序和可见答案逐条对比。 + +## 三、定理、证明与引用框架 + +### 3.1 职责划分 + +移除 `ntheorem` 与自定义手写 QED 的并行语义层,改成清晰的两层职责: + +- `tcolorbox` theorem library 是 definition、example、lemma、theorem、corollary、axiom 的声明、计数器、标题栏和分页视觉层。 +- `amsthm` 是 proof 的语义与 QED 栈实现,保证 `\qedhere`、嵌套证明和显示公式末尾行为正确。 +- `tcolorbox` 只包装 proof/solution 的视觉外框,不自行在环境尾部附加 `$\square$`。 + +推荐加载顺序为:数学基础包与 `mathtools`,随后 `amsthm`,再加载 `tcolorbox` 及所需 library,最后加载 `hyperref`、`bookmark`、`cleveref`。`ntheorem` 完全退出,不保留同时生效的 theorem style 命令。 + +### 3.2 定理类环境 + +用一张集中声明表维护每个环境的属性: + +| 环境 | 中文名 | 标签前缀 | 颜色 | 缺省计数父级 | +| --- | --- | --- | --- | --- | +| `definition` | 定义 | `def:` | red | chapter | +| `example` | 例 | `ex:` | blue | chapter | +| `lemma` | 引理 | `lem:` | orange | chapter | +| `theorem` | 定理 | `thm:` | violet | chapter | +| `corollary` | 推论 | `cor:` | green | chapter | +| `axiom` | 公理 | `axm:` | olive | chapter | + +初次迁移应保持当前各环境分别计数的行为;是否共享 theorem 计数器属于独立的版式决策,不应在底层替换时顺便改变。正文中 `number within=chapter` 继续依赖统一章节模型,所以未竟专题内的显示编号和锚点都会自然使用唯一的 LU chapter 信息。 + +公共调用语法在兼容阶段保持不变: + +```tex +\begin{theorem}{显示标题}{标签键} + ... +\end{theorem} +``` + +第二参数非空时继续自动生成 `thm:<标签键>` 一类完整标签;空参数不生成标签。现有可选 tcolorbox 参数若实际有人使用,也必须由兼容层透传。未来可增加 key-value 新接口,但应先提供机械迁移工具和弃用周期,不能要求一次性人工修改全部现有环境。 + +视觉样式集中为一个基础 style,再按环境覆盖颜色。基础 style 负责 breakable、边框、标题字体、正文字体、首/中/末段虚线等当前特征。任何视觉调整都通过小型 golden PDF 检查,避免语义迁移意外改变分页。 + +### 3.3 proof、solution 与 QED + +`proof` 使用 amsthm 的 QED 栈;tcolorbox 可通过 `\tcolorboxenvironment` 装饰 amsthm proof,或使用一个严格转发到 amsthm begin/end 的包装器。无论采用哪种方式,都必须满足: + +- 普通文本结尾自动出现且只出现一个 QED; +- display math、`equation`、`align` 末尾的 `\qedhere` 位置正确,不再另加第二个方块; +- 跨页 breakable box 的 QED 位于证明内容结束处和盒子内部; +- 嵌套 proof 使用独立 QED 栈; +- 可选说明保持当前“证明 + 说明文字”的可见效果。 + +`solution` 复用同一视觉框架,但默认不显示 QED,以保持现有行为。若未来需要 QED,使用显式选项,不通过正文手写 `\square` 推测。 + +当前 `proof`/`solution` 的第二个可选参数可暂时作为原始 tcolorbox 选项透传;应扫描实际调用,若未使用则标记弃用,改由受控 key 列表代替任意样式注入。 + +### 3.4 `autoref` 与 `cleveref` + +所有定理环境创建后,由同一声明表生成引用名称,不能散落手写私有 counter 名: + +- 为 Hyperref 配置对应 `...autorefname`,保证 `\autoref` 输出“定理 3.2”等。 +- 为 cleveref 同时配置 `\crefname`、`\Crefname`、range 和 multiple 格式。 +- 现有标签前缀保持不变;`\ref`、`\autoref`、`\cref`、`\crefrange`、`\nameref` 都加入回归测试。 +- 如果 tcolorbox 的内部 counter 名随版本变化,类应在一个内部适配层解析,正文不得依赖 `tcb@cnt@...`。 +- 新正文优先推荐 `\cref`;`\autoref` 作为兼容接口继续受支持。 + +每个 theorem counter 的 `\theH...` 必须从统一的唯一 chapter anchor 派生。正常章和连续未竟专题中,同样显示为 `.1` 的定理也必须拥有不同 destination。 + +### 3.5 定理框架迁移兼容 + +迁移步骤如下: + +1. 统计所有环境调用形式、可选参数、空标题、空标签和手写 `\qedhere`/`\square`;建立代表性 fixture。 +2. 在测试分支中用 `amsthm` 替换 `ntheorem`,但保留现有 tcolorbox theorem 公共环境名、双参数语法、标签前缀和分别计数方式。 +3. 将 `\theoremheaderfont`、`\theorembodyfont` 的效果搬到集中 tcolorbox style;删除 ntheorem 命令前先验证字体快照。 +4. 用 amsthm proof + tcolorbox wrapper 替换手写尾部方块,修正真正需要 `\qedhere` 的少量正文。 +5. 集中生成 autoref/cleveref 配置并比较旧、新 `.aux` 中的标签值。 +6. 一个发布周期内保留旧 proof/solution 可选参数适配;弃用告警应包含源文件和行号。 + +迁移提交必须要求干净重编译辅助文件。除明确列出的 QED 位置修正外,既有标签键和可见定理编号不得变化。 + +### 3.6 定理测试矩阵 + +| 场景 | 验收点 | +| --- | --- | +| 六类环境各一个,标题/标签为空或非空 | 标题栏、字体、颜色、编号、标签前缀正确 | +| 同一普通章内各环境多个实例 | 分别计数规则与当前行为一致 | +| 两个连续未竟专题 | 显示父编号为 `Nε`/`Nδ`,锚点无重复 | +| 长定理跨两页 | 首/中/末边框正确,链接目标在标题处 | +| proof 以普通段落结束 | 恰有一个 QED | +| proof 以 equation/align 结束并使用 `\qedhere` | 方块位置正确且不重复 | +| 嵌套 proof | QED 栈正确 | +| solution | 缺省无 QED | +| `ref/autoref/cref/crefrange/nameref` | 中文名称、范围格式、链接目标正确 | +| 答案单元中的定理与证明 | 使用答案单元父编号,不污染后续历年卷 | + +## 四、分阶段落地顺序 + +### 阶段 0:冻结基线与建立检查工具 + +- 保存当前完整 PDF、答案 PDF、`.aux` 标签表、`.toc`、PDF outline 和归一化日志。 +- 建立章节、答案、定理三个最小回归文档。 +- 在 CI 中加入重复 destination、未定义引用、PDF string 警告和残留 rerun 的失败规则。 +- 统计当前答案记录数、空答案数、定理环境调用签名及标签前缀。 + +这一阶段只增加测试和基线,不改变产物。 + +### 阶段 1:引入统一章节状态,保持公共接口 + +- 在类中加入结构 `chapter`、普通显示计数器、LU 流水号、slot、appendix 显示计数器以及统一章节记录。 +- 提供语义查询 API,替换模板内部所有把裸 `chapter` 当普通显示编号的用法,并启用静态检查。 +- 在既有 `\chapter`、`\LUchapter` 外观后切换到新状态机。 +- 移除跨章节开放分组和历史 marker 的内部依赖;兼容命令暂留。 +- 同时改造答案册的统一 `LALUanswerunit` renderer,但暂不切换答案生成格式。 +- 通过章节测试矩阵和旧、新目录/outline 对比后合并。 + +### 阶段 2:答案 manifest 双写与原子发布 + +- 定义带版本的记录 schema、reader 和 file 载荷接口。 +- 构建系统同时生成 legacy 文件与新 manifest,分别编译答案册并逐记录比较。 +- 加入临时目录、结束 sentinel、原子 rename、过期检查和并发测试。 +- 将含 verbatim 或 catcode 风险的答案迁移到 file 载荷。 +- 对比通过后切换答案册读取新 manifest;保留一次发布周期的 legacy reader。 + +### 阶段 3:统一定理和证明框架 + +- 移除 ntheorem 语义层,引入 amsthm proof/QED。 +- 保持 tcolorbox theorem 的旧环境名、双参数语法、计数器行为和标签前缀。 +- 集中 theorem 声明、样式及 autoref/cleveref 配置。 +- 修正并测试 display math 的 `\qedhere`,进行分页与字体 golden PDF 对比。 + +### 阶段 4:清理兼容层 + +- 在确认仓库及外部维护文档已迁移后,删除 `\LUgroupsancheck`、动态 `@exist@...` marker、旧答案 reader 和原始样式透传。 +- 更新作者文档,说明 `\LUchapter`、稳定习题 ID、todo、file answer、proof 和引用的推荐写法。 +- 将旧生成文件从构建流程和清理规则中彻底移除。 + +每个阶段独立可回退,不把章节、答案和定理三类高风险改动塞进同一个不可审查提交。 + +## 五、总体验收标准 + +重构实现完成的最低标准如下: + +1. 从干净工作区运行正式构建,主讲义与答案册均成功;第二次运行不再要求 rerun。 +2. 章节测试序列精确得到“第 1 讲、未竟专题一 `1ε`、未竟专题二 `1δ`、第 2 讲”;四个有编号顶层单元使结构 `c@chapter` 最终等于 4,而普通显示计数器最终等于 2。 +3. 普通正文显示编号不因插入或删除未竟专题而改变;结构 counter 对每个有编号顶层单元恰递增一次,未竟专题全局中文序号和同章希腊 slot 各自按约定变化。 +4. 主 PDF 与答案 PDF 日志中不存在重复 destination;所有内部链接和 `xr` 链接指向正确页面。 +5. 目录与 PDF outline 中每个单元恰有一条记录,中文标题、Unicode 希腊编号和层级正确。 +6. `\ref`、`\nameref`、`\autoref`、`\cref` 以及范围引用覆盖普通章、未竟专题、附录、节、公式和六类定理,输出与兼容约定一致。 +7. 没有可见答案的单元不生成答案册标题页;显式 todo 按配置生成统一占位。 +8. 主文档失败、答案记录损坏或载荷缺失时,不覆盖上一份有效 manifest,并以非零状态失败。 +9. 不同 jobname 可并发构建;同一目标的竞争构建不会产生交叉或半截文件。 +10. 普通 inline 答案保持宏、段落和标签语义;file 答案可正常包含 verbatim/listing。 +11. 现有定理环境调用在兼容阶段无需批量人工修改,编号和标签键不变。 +12. proof 在普通文本、display math、跨页和嵌套场景中 QED 恰好出现一次;solution 缺省无 QED。 +13. 章节状态、答案 renderer 和 theorem 声明各有单一实现来源,不再存在两套通过历史 marker 或开放分组彼此模仿的逻辑。 +14. frontmatter、backmatter 和星号章不递增结构/显示 counter;重复阶段命令不重置任何章节状态,非法阶段回退明确失败;每个编号附录只递增结构与 appendix 显示 counter,且不会把结构 `c@chapter` 清零。 +15. 全部手写源码(包括类和模板)都不存在把裸 `\value{chapter}`、`\arabic{chapter}`、`\setcounter{chapter}`、`\addtocounter{chapter}` 或 `c@chapter` 当作普通显示编号的调用;确需读写结构值的统一模块代码均在静态检查白名单中。 +16. 受支持 TeX Live 版本的测试全部通过;不支持的引擎在加载早期给出明确错误。 +17. 无普通章、错误 phase、编号被抑制或希腊 slot 超界时,`\LUchapter` 都在改变任何 counter、当前记录或样式前失败;修正输入后可继续构建,不受半初始化状态污染。 +18. frontmatter、mainmatter 首章之前、backmatter、星号章、appendix 入口空隙及首阶段的附录章都不会把习题误挂到上一个正文/LU 单元;不支持的附录答案以明确错误结束。 From 34204126caf794f003385fbf7d87e8d3dff809c5 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Wed, 16 Sep 2026 06:13:10 +0800 Subject: [PATCH 06/27] docs(template): document breaking changes Map each incompatible refactor to its affected templates or chapter sources, record the intended replacement syntax, and add repository scans for migration work. --- docs/template-refactor-plan.md | 64 ++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/docs/template-refactor-plan.md b/docs/template-refactor-plan.md index 88eae2f..a267869 100644 --- a/docs/template-refactor-plan.md +++ b/docs/template-refactor-plan.md @@ -510,7 +510,67 @@ build/answers/LALU-/ | `ref/autoref/cref/crefrange/nameref` | 中文名称、范围格式、链接目标正确 | | 答案单元中的定理与证明 | 使用答案单元父编号,不污染后续历年卷 | -## 四、分阶段落地顺序 +## 四、Breaking changes 与源码迁移 + +这里把“breaking change”限定为:升级模板后,旧源码、构建命令、生成文件或外部链接即使重新编译,也不能自动保持原有语义,必须改写或显式迁移。单纯的内部实现替换、可接受的分页微调,以及清理辅助文件后即可恢复的变化不单独算作 breaking change。 + +### 4.1 明确会发生的兼容性中断 + +| 计划中的变化 | 中断点 | 当前仓库中受影响的位置与定位方法 | 新写法或迁移动作 | +| --- | --- | --- | --- | +| 标准 `chapter` 改作所有有编号顶层单元的结构流水号 | 裸 `\value{chapter}`、`\arabic{chapter}`、`c@chapter` 及手工 `\setcounter`/`\addtocounter` 不再表示普通“第 N 讲” | 当前只在 `讲义/线性代数荣誉课辅学讲义.tex` 的章节和答案生成逻辑中出现;`讲义/专题/` 与 `讲义/其它/` 的手写正文目前没有这类依赖 | 普通讲次改读 `\LALUNormalChapterNumber`,当前可见引用改读 `\LALUCurrentReferenceNumber` 或 `\thechapter`,单元类型改查 `kind`;正文不得再直接改 `chapter` | +| 无编号状态不再泄漏上一编号单元 | frontmatter、backmatter 或 `\chapter*` 后的 `\thechapter` 将为空;依赖其继续返回上一章号的宏会改变行为 | 当前手写正文没有这种读取;迁移时仍须把 `\thechapter`/`\theHchapter` 纳入模板和扩展宏扫描 | 需要最近普通讲次时显式查询 `\LALUNormalChapterNumber`,需要当前单元时先检查状态 API;不得把无编号边界后的 `\thechapter` 当历史快照 | +| 删除跨章分组和历史 marker | `\LUgroupsancheck`、`\LUsection` 以及 `\@exist@LUchapter@...`/`\@exist@LUsection@...` 最终会消失 | 定位于主模板、`讲义/LALU-answers.tex` 和生成的 `LALU-ans-contents.tex`;各个未竟专题文件开头的 `\LUchapter{...}` 不受影响 | 删除主文件末尾等处的手工 sanity check;答案册只消费 `\LALUAnswerUnit` 等 manifest 事件,不再手工设置 `LUsection` 或探测 marker | +| 对 phase 和单元位置执行严格校验 | 以前“碰巧可编译”的 `mainmatter` 首章之前的 LU、front/backmatter 或 appendix 中的 LU/习题、`secnumdepth` 抑制编号时的 LU,以及非法阶段回退将直接报错;重复 phase 命令不再重置页码或计数器 | 从主文件的 `\frontmatter`、`\mainmatter`、`\appendix`、`\backmatter` 和各文件的 `\LUchapter`、`\begin{exercise}` 定位;当前正式输入顺序没有发现违规项 | 在 `\mainmatter` 中先建立有编号的普通 `\chapter`,再写 `\LUchapter`;习题只放在普通章或 LU 单元内。`\chapter*` 后不能直接挂习题,但仍可继续写下一个 LU;首阶段不支持附录习题 | +| 答案交换格式和输出路径改为带版本的 manifest | `LALU-ans-contents.tex` 的文件名、内容语法和可从源码目录直接 `\input` 的约定不再保证 | 主要影响主模板、`讲义/LALU-answers.tex`、`讲义/Makefile`、latexmk 配置和 CI,不要求逐篇改普通 inline 答案 | 构建命令把 manifest 路径显式传给答案册;只通过 manifest reader 读取 `\LALUAnswerUnit`/`Group`/`Item` 事件,生成文件一律不可手工编辑 | +| 空答案和空答案单元获得明确状态语义 | 纯空白 `answer` 将变为 `omitted`;完全没有 `answered`/`todo` 的习题单元不再输出标题、目录项或空白分页 | 当前有 29 个纯空 `answer`,集中在第 2、3、4、7、8、9、10 讲;另有 15 个完全没有 `answer` 的单元,分布在第 6、12、16–25 讲及未竟专题 4、8、14 | 确实没有答案时删除空环境或接受省略;需要保留答案项或整个单元时,至少加入一个计划接口 `\begin{answer}[status=todo] ... \end{answer}` | +| 答案单元和附录采用新的机器锚点命名空间 | 答案单元切换到 `LALUanswerunit.answer.*`,附录使用 `chapter.APP.*`;硬编码到旧 href 的外部 PDF 深链接或工具会失效 | 当前正文没有编号附录,正文的 `\label`/`\ref` 键原则上不改;主要迁移面是答案 PDF 的外部链接和下游 PDF 工具 | 发布说明列出 href 映射并尽可能提供一个版本的旧锚点别名;同时清理辅助文件并重建主讲义、答案册和 `xr` 缓存 | +| `ntheorem` 退出依赖面 | 外部文档若依赖类“顺便加载” `ntheorem`,或直接调用它的样式/声明命令,将不再工作 | 当前正文没有直接调用;仓库内只有 `LALUbook.cls` 的 `\theoremheaderfont`、`\theorembodyfont` 等模板设置需要迁移 | 自定义定理接入新的集中声明表和 amsthm/tcolorbox 适配层;外部文档不得依赖类的传递包依赖 | + +上表中的“计划接口”只是目标写法,目前尚不是可执行命令;它们应在实现对应阶段时固定下来。一旦进入作者文档和正文,就必须遵循正常的弃用周期,不能在后续提交中无迁移说明地再次改名。 + +### 4.2 条件性 breaking changes 与迁移审计 + +下列事项要么只有在采用可选设计时才构成兼容性中断,要么必须人工审计,但不能预先假定所有命中处都需要改写: + +- 如果答案册为 `\externaldocument` 加固定前缀,答案正文中指向主讲义的 `\ref`/`\nameref`/`\autoref`/`\cref` 要改为 `\mainref`/`\mainnameref`/`\mainautoref`/`\maincref` 等成套 helper。当前入口位于 `讲义/LALU-answers.tex`;启用前应先扫描所有 `answer` 载荷中的引用并验证没有同名标签。 +- 如果稳定习题 ID 从“建议”升级为“必填”,`exercise` 中相应的普通 `\item` 要改为 `\exitem[id=...]`。迁移初期必须继续自动生成 ID 并告警,不能一次性破坏现有全部习题。 +- amsthm proof 的公共语法和普通自动 QED 行为应保持兼容,但 QED 的具体位置需要视觉审计。当前有 27 个 proof 直接以 display 结束;典型位置为 `讲义/专题/9 矩阵运算进阶.tex:443`、`讲义/专题/13 多项式.tex:542` 和 `讲义/专题/21 线性代数与几何.tex:49`。只有希望方块留在最后一行公式内的用例才需加入 `\qedhere`,不能把 27 处一律机械改写;当前正文没有手写 `\square`。 +- inline `answer` 本来就不保证支持 verbatim、minted、listing 或特殊 catcode,因此 file payload 是新增能力而非当前仓库的 breaking change。若外部答案依赖这些“碰巧工作”的未保证行为,则应把内容移到独立片段,并通过计划接口 `\answerfile{answers/.tex}` 引用;当前基线没有此类载荷。 +- 如果在兼容期后移除 proof/solution 的第二个原始 tcolorbox 选项,外部源码中的 `\begin{proof}[说明][任意样式]` 或同形 `solution` 要迁移到受控 key。当前仓库只有第一可选参数用例,没有发现第二可选参数。 +- 如果在更晚版本移除 `LUchapter`、`LUgreekchap`、`LUsection` counter alias,外部源码对这些 counter 的直接读取也会失效;当前仓库中的直接使用只在模板和生成文件内,应先由统一状态 API 取代。 +- 其他 section、公式、定理等后代 destination 只有在确认现值冲突时才改变;普通章和 LU 的既有 destination 仍以保留为默认。若不得不改变,必须按外部 PDF 深链接的 breaking change 处理,而不能把“清理 `.aux` 即可”当作充分迁移说明。 +- 支持引擎矩阵尚未最终确定;一旦决定不支持 LuaLaTeX 等引擎,这些非正式构建流程将改为早期失败。当前 Makefile 使用 XeLaTeX,不影响正文;若要把其他引擎列为受支持,须先补齐字体、索引、书签和回归测试。 +- 如果无法可靠恢复 `\includeonly` 所需的非 counter 状态,可以明确禁止跨普通章/LU 的选择构建。当前主讲义使用 `\input`,因此仓库正文不受影响;外部使用者若依赖 `\includeonly`,应改为完整构建或等待 checkpoint 协议落地。 + +### 4.3 明确保留兼容的作者接口 + +以下常用正文写法不应因本次重构而修改: + +- 普通章继续写 `\chapter{标题}`,未竟专题继续写 `\LUchapter{标题}`;新增的 `\LUchapter[短标题]{完整标题}` 只是可选扩展。 +- 普通 inline 答案继续写 `\begin{answer} ... \end{answer}`;只有空占位和特殊 catcode 载荷需要迁移。 +- definition、example、lemma、theorem、corollary、axiom 继续保留当前双参数形式和标签前缀。 +- `\begin{proof}[说明]` 与 `\begin{solution}[说明]` 的第一可选参数继续有效。 +- 既有 `\label` 键、普通章 `chapter.N` 和未竟专题 `chapter.LU.N` 锚点应保留;正常的 `\ref`、`\nameref`、`\autoref` 和 `\cref` 源码不要求批量改写。 + +### 4.4 迁移时的定位清单 + +实现各阶段前至少运行下列扫描;它们用于定位受影响的写法,不代表匹配项都必须修改: + +```zsh +rg -n --glob '*.tex' --glob '*.cls' '\\(value|arabic|setcounter|addtocounter)\{chapter\}|c@chapter|\\theH?chapter' . +rg -n --glob '*.tex' '\\LUgroupsancheck|\\LUsection|@exist@LU(chapter|section)' 讲义 +rg -n --glob '*.tex' '\\(frontmatter|mainmatter|appendix|backmatter|LUchapter)|\\begin\{exercise\}' 讲义 +rg -n -U --glob '*.tex' '\\begin\{answer\}(?:[[:space:]]|%[^\n]*\n)*\\end\{answer\}' 讲义 +comm -23 <(rg -l -F '\begin{exercise}' 讲义/专题 讲义/其它 | sort) <(rg -l -F '\begin{answer}' 讲义/专题 讲义/其它 | sort) +rg -n --glob '*.tex' --glob '*.cls' '\\(theoremheaderfont|theorembodyfont|newtheorem)' . +rg -n --glob '*.tex' '\\begin\{(proof|solution)\}|\\(qedhere|square)' 讲义 +rg -n -U --pcre2 --glob '*.tex' '(?:\\\]|\\end\{(?:equation\*?|align\*?|alignat\*?|gather\*?|multline\*?|displaymath)\})\s*(?:%[^\n]*\n\s*)*\\end\{proof\}' 讲义 +``` + +迁移顺序应是:先改类、主模板、答案模板和构建脚本,再处理扫描命中的少量正文特殊情况,最后删除全部生成文件做一次冷构建。普通章节与未竟专题文件不应为了适配内部状态机而插入新的 helper 命令。 + +## 五、分阶段落地顺序 ### 阶段 0:冻结基线与建立检查工具 @@ -553,7 +613,7 @@ build/answers/LALU-/ 每个阶段独立可回退,不把章节、答案和定理三类高风险改动塞进同一个不可审查提交。 -## 五、总体验收标准 +## 六、总体验收标准 重构实现完成的最低标准如下: From 0b31880f2805fff602b88df5aac1571e96e9da3c Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Wed, 16 Sep 2026 15:01:34 +0800 Subject: [PATCH 07/27] refactor(template): unify proof semantics Replace ntheorem with amsthm's QED stack while retaining the tcolorbox presentation and the existing author-facing theorem, proof, and solution forms. --- LALUbook.cls | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/LALUbook.cls b/LALUbook.cls index d23ffd3..4476430 100644 --- a/LALUbook.cls +++ b/LALUbook.cls @@ -58,6 +58,7 @@ \RequirePackage{mathrsfs} \RequirePackage{amssymb} \RequirePackage{mathtools} +\RequirePackage{amsthm} % 加入字串处理功能 \RequirePackage{xstring} @@ -72,7 +73,6 @@ \RequirePackage{multicol} \RequirePackage{pdfpages} \RequirePackage{indentfirst} -\RequirePackage{ntheorem} \RequirePackage{titlesec} \RequirePackage{mismath} % 包含 rank, span 等运算符 \RequirePackage{zhnumber} @@ -92,10 +92,6 @@ % 首行缩进 2 字符 \setlength{\parindent}{2em} -% 定理字体 -\theoremheaderfont{\bfseries\heiti} -\theorembodyfont{\fangsong} - \usetikzlibrary{shapes.symbols} % 设置页面尺寸 @@ -172,8 +168,22 @@ #3, } -\NewDocumentEnvironment{proof}{O{}O{}}{\begin{tcblaluthmbox}{{\heiti 证明}}{#1}{#2}\fangsong}{\hspace*{\fill}$\square$\end{tcblaluthmbox}} -\NewDocumentEnvironment{solution}{O{}O{}}{\begin{tcblaluthmbox}{{\heiti 解}}{#1}{#2}\fangsong}{\end{tcblaluthmbox}} +% amsthm 负责 QED 栈和 \qedhere,tcolorbox 只负责外观。 +\renewcommand*{\qedsymbol}{$\square$} +\RenewDocumentEnvironment{proof}{O{}}{% + \pushQED{\qed}% + \begin{tcblaluthmbox}{{\heiti 证明}}{#1}{}% + \fangsong\ignorespaces +}{% + \popQED + \end{tcblaluthmbox}% +} +\NewDocumentEnvironment{solution}{O{}}{% + \begin{tcblaluthmbox}{{\heiti 解}}{#1}{}% + \fangsong\ignorespaces +}{% + \end{tcblaluthmbox}% +} \NewDocumentCommand{\term}{m}{{\sffamily\heiti\bfseries{#1}}} From a6768a3275a4e5537c37d33fb41dd8644270ec92 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Wed, 16 Sep 2026 15:05:20 +0800 Subject: [PATCH 08/27] refactor(template): centralize theorem declarations Declare theorem styling, label prefixes, and autoref/cleveref names from one table so the six boxed environments keep a single source of truth. --- LALUbook.cls | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/LALUbook.cls b/LALUbook.cls index 4476430..9787528 100644 --- a/LALUbook.cls +++ b/LALUbook.cls @@ -185,6 +185,14 @@ \end{tcblaluthmbox}% } +% 定理环境的名称、颜色、标签前缀与引用名称由同一处声明。 +\newcommand*{\LALU@DeclareBoxedTheorem}[4]{% + \newtcbtheorem[number within=chapter]{#1}{#2}{laluthmstyle={#3}}{#4}% + \expandafter\def\csname tcb@cnt@#1autorefname\endcsname{#2}% + \crefname{tcb@cnt@#1}{#2}{#2}% + \Crefname{tcb@cnt@#1}{#2}{#2}% +} + \NewDocumentCommand{\term}{m}{{\sffamily\heiti\bfseries{#1}}} \DeclareMathOperator{\Ob}{Ob} @@ -205,12 +213,12 @@ \RequirePackage{bookmark} \RequirePackage{cleveref} - \newtcbtheorem[number within=chapter]{definition}{定义}{laluthmstyle={red}}{def} - \newtcbtheorem[number within=chapter]{example}{例}{laluthmstyle={blue}}{ex} - \newtcbtheorem[number within=chapter]{lemma}{引理}{laluthmstyle={orange}}{lem} - \newtcbtheorem[number within=chapter]{theorem}{定理}{laluthmstyle={violet}}{thm} - \newtcbtheorem[number within=chapter]{corollary}{推论}{laluthmstyle={green}}{cor} - \newtcbtheorem[number within=chapter]{axiom}{公理}{laluthmstyle={olive}}{axm} + \LALU@DeclareBoxedTheorem{definition}{定义}{red}{def} + \LALU@DeclareBoxedTheorem{example}{例}{blue}{ex} + \LALU@DeclareBoxedTheorem{lemma}{引理}{orange}{lem} + \LALU@DeclareBoxedTheorem{theorem}{定理}{violet}{thm} + \LALU@DeclareBoxedTheorem{corollary}{推论}{green}{cor} + \LALU@DeclareBoxedTheorem{axiom}{公理}{olive}{axm} \hypersetup{ unicode, @@ -235,15 +243,6 @@ \renewcommand{\equationautorefname}{式} \renewcommand{\chapterautorefname}{章} \renewcommand{\sectionautorefname}{节} - \makeatletter - \newcommand{\tcb@cnt@theoremautorefname}{定理} - \newcommand{\tcb@cnt@lemmaautorefname}{引理} - \newcommand{\tcb@cnt@corollaryautorefname}{推论} - \newcommand{\tcb@cnt@exampleautorefname}{例} - \newcommand{\tcb@cnt@definitionautorefname}{定义} - \newcommand{\tcb@cnt@axiomautorefname}{公理} - \makeatother - % https://tex.stackexchange.com/a/2244/316373 \makeatletter \renewcommand*\env@matrix[1][*\c@MaxMatrixCols c]{% From 68ee20314c12738fda76735537b6bec9c89c5f53 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Wed, 16 Sep 2026 16:22:27 +0800 Subject: [PATCH 09/27] fix(document): normalize misplaced limit controls Move \\limits before the summation scripts so current MacTeX parses the three formulas without a nonzero recovery error while preserving their mathematical layout. --- .../10 \350\241\214\345\210\227\345\274\217.tex" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/10 \350\241\214\345\210\227\345\274\217.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/10 \350\241\214\345\210\227\345\274\217.tex" index 9023c9a..372ae15 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/10 \350\241\214\345\210\227\345\274\217.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/10 \350\241\214\345\210\227\345\274\217.tex" @@ -4019,7 +4019,7 @@ \section{Cauchy-Binet 公式} 将上述行列式展开即有 \[ - \left( \sum_{i=1}^{n}\limits a_i^2 \right) \left( \sum_{i=1}^{n}\limits b_i^2 \right) - \left( \sum_{i=1}^{n}\limits a_i b_i \right)^2 \geqslant 0. + \left( \sum\limits_{i=1}^{n} a_i^2 \right) \left( \sum\limits_{i=1}^{n} b_i^2 \right) - \left( \sum\limits_{i=1}^{n} a_i b_i \right)^2 \geqslant 0. \] 故 Cauchy-Schwarz 不等式成立. From 5e75bf2ac68c6a3cb9ef33f22a2bcee11c6a5954 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Wed, 16 Sep 2026 16:55:21 +0800 Subject: [PATCH 10/27] fix(template): preserve legacy proof layout Keep amsthm's QED stack and math-mode placement while using the template's original fill glue for ordinary proof endings. This restores the existing page breaks and avoids changing the global amsthm \qed command. --- LALUbook.cls | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/LALUbook.cls b/LALUbook.cls index 9787528..bd28485 100644 --- a/LALUbook.cls +++ b/LALUbook.cls @@ -170,8 +170,17 @@ % amsthm 负责 QED 栈和 \qedhere,tcolorbox 只负责外观。 \renewcommand*{\qedsymbol}{$\square$} +% 普通段落结尾保留旧模板的精确胶水;数学模式仍转给 +% amsthm,使 \qedhere 可以在 equation/align 内正确放置方块。 +\newcommand*{\LALU@qed}{% + \ifmmode + \mathqed + \else + \hspace*{\fill}\qedsymbol + \fi +} \RenewDocumentEnvironment{proof}{O{}}{% - \pushQED{\qed}% + \pushQED{\LALU@qed}% \begin{tcblaluthmbox}{{\heiti 证明}}{#1}{}% \fangsong\ignorespaces }{% From 5d38c370679d70a4da41dde2b454d078b14bc2f2 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Wed, 16 Sep 2026 17:01:37 +0800 Subject: [PATCH 11/27] refactor(template): make chapter and answer state explicit Replace cross-chapter groups and counter aliases with an explicit phase/kind state machine while preserving normal and unfinished-topic semantics. Generate a versioned, strictly validated answer manifest, render it through an independent reader, and stage both PDFs until the complete build succeeds. --- "\350\256\262\344\271\211/LALU-answers.tex" | 353 ++++++- "\350\256\262\344\271\211/Makefile" | 36 +- .../scripts/publish-answer-manifest.pl" | 323 +++++++ ...5\345\255\246\350\256\262\344\271\211.tex" | 859 +++++++++++++++--- 4 files changed, 1422 insertions(+), 149 deletions(-) create mode 100644 "\350\256\262\344\271\211/scripts/publish-answer-manifest.pl" diff --git "a/\350\256\262\344\271\211/LALU-answers.tex" "b/\350\256\262\344\271\211/LALU-answers.tex" index 78b00cb..49174ef 100644 --- "a/\350\256\262\344\271\211/LALU-answers.tex" +++ "b/\350\256\262\344\271\211/LALU-answers.tex" @@ -22,34 +22,322 @@ pdftitle = {线性代数荣誉课辅学习题答案}, }} -\newcounter{LUsection} +% The main document publishes this generated manifest only after a successful +% build. Keeping the path overrideable makes out-of-tree builds possible. +\providecommand*{\LALUAnswerManifestPath}{build/answers/LALU.manifest.tex} +\providecommand*{\LALUAnswerManifestJob}{LALU} -\makeatletter -\newcommand*{\LUgroupsancheck}{% -\expandafter\@ifundefined{@exist@LUsection@\arabic{section}}% - {} - {\endgroup} -} -\let\@std@section\section -\renewcommand*{\section}{% - \LUgroupsancheck% - \@std@section -} -\makeatother +\newcounter{LALUanswerunit} -\NewDocumentCommand{\LUsection}{m}{% -\LUgroupsancheck -\begingroup -\addtocounter{section}{-1} -\refstepcounter{LUsection} -\renewcommand*{\thesection}{未竟专题\zhnumber{\arabic{LUsection}}} -\renewcommand*{\theHsection}{LU.\arabic{LUsection}} -\ctexset{ - section={format={\centering\Huge\bfseries},name={未竟专题,},number={\zhnumber{\arabic{LUsection}}}}, -} -\csname @std@section\endcsname{#1} -\expandafter\xdef\csname @exist@LUsection@\arabic{section}\endcsname{\null} -} +\ExplSyntaxOn +\msg_new:nnn { lalu-answer } { manifest-missing } + { Answer~manifest~'#1'~does~not~exist.~Build~the~main~document~first. } +\msg_new:nnn { lalu-answer } { manifest-version } + { Unsupported~answer~manifest~schema~'#1'. } +\msg_new:nnn { lalu-answer } { manifest-job } + { Answer~manifest~is~for~job~'#1',~not~'#2'. } +\msg_new:nnn { lalu-answer } { manifest-order } + { Invalid~answer~manifest~event~order~near~'#1'. } +\msg_new:nnn { lalu-answer } { manifest-status } + { Unsupported~answer~status~'#1'. } +\msg_new:nnn { lalu-answer } { manifest-count } + { Answer~manifest~count~'#1'~is~#2;~expected~#3. } +\msg_new:nnn { lalu-answer } { manifest-data } + { Invalid~answer~manifest~data~'#1'~near~'#2'. } + +\bool_new:N \g_lalu_answer_manifest_open_bool +\bool_new:N \g_lalu_answer_manifest_ended_bool +\bool_new:N \g_lalu_answer_first_unit_bool +\bool_new:N \g_lalu_answer_unit_seen_bool +\bool_new:N \g_lalu_answer_group_open_bool +\tl_new:N \l_lalu_answer_reference_tl +\tl_new:N \l_lalu_answer_anchor_tl +\tl_new:N \l_lalu_answer_heading_tl +\tl_new:N \l_lalu_answer_manifest_path_tl +\tl_new:N \l_lalu_answer_group_id_tl +\tl_new:N \l_lalu_answer_group_items_tl +\int_new:N \g_lalu_answer_unit_int +\int_new:N \g_lalu_answer_group_int +\int_new:N \g_lalu_answer_item_int +\int_new:N \g_lalu_answer_answered_int +\int_new:N \g_lalu_answer_todo_int +\int_new:N \g_lalu_answer_omitted_int +\int_new:N \l_lalu_answer_group_number_int +\int_new:N \l_lalu_answer_group_expected_int +\int_new:N \l_lalu_answer_group_item_int +\prop_new:N \g_lalu_answer_unit_id_prop +\prop_new:N \g_lalu_answer_group_id_prop +\prop_new:N \g_lalu_answer_item_id_prop +\cs_generate_variant:Nn \file_if_exist:nTF { V } +\cs_generate_variant:Nn \file_input:n { V } + +\cs_new:Npn \lalu_answer_greek:n #1 + { + \ifcase #1 \or + \texorpdfstring{$\boldsymbol{\varepsilon}$}{ε}\or + \texorpdfstring{$\boldsymbol{\delta}$}{δ}\or + \texorpdfstring{$\boldsymbol{\lambda}$}{λ}\or + \texorpdfstring{$\boldsymbol{\mu}$}{μ}\or + \texorpdfstring{$\boldsymbol{\varphi}$}{φ}\or + \texorpdfstring{$\boldsymbol{\theta}$}{θ}\else + \msg_expandable_error:nnn { lalu-answer } { manifest-order } + { unfinished-slot-#1 } + \fi + } + +\cs_set:Npn \theLALUanswerunit { \tl_use:N \l_lalu_answer_reference_tl } +\cs_if_exist:NTF \theHLALUanswerunit + { \cs_set:Npn \theHLALUanswerunit { \tl_use:N \l_lalu_answer_anchor_tl } } + { \cs_new:Npn \theHLALUanswerunit { \tl_use:N \l_lalu_answer_anchor_tl } } + +\cs_new_protected:Npn \lalu_answer_require_open:n #1 + { + \bool_if:NF \g_lalu_answer_manifest_open_bool + { \msg_fatal:nnn { lalu-answer } { manifest-order } {#1} } + \bool_if:NT \g_lalu_answer_manifest_ended_bool + { \msg_fatal:nnn { lalu-answer } { manifest-order } {#1-after-end} } + } + +\cs_new_protected:Npn \lalu_answer_assert_count:nnn #1#2#3 + { + \int_compare:nNnF {#2} = {#3} + { \msg_fatal:nnnnn { lalu-answer } { manifest-count } {#1}{#2}{#3} } + } + +\cs_new_protected:Npn \lalu_answer_claim_id:Nnn #1#2#3 + { + \prop_if_in:NnTF #1 {#2} + { \msg_fatal:nnnn { lalu-answer } { manifest-data } { duplicate-id } {#2} } + { \prop_gput:Nnn #1 {#2} {#3} } + } + +\cs_new_protected:Npn \LALUAnswerManifestBegin #1#2 + { + \bool_if:NT \g_lalu_answer_manifest_open_bool + { \msg_fatal:nnn { lalu-answer } { manifest-order } { duplicate-begin } } + \str_if_eq:nnF {#1} {2} + { \msg_fatal:nnn { lalu-answer } { manifest-version } {#1} } + \str_if_eq:eeF {#2} { \LALUAnswerManifestJob } + { + \msg_fatal:nnnn { lalu-answer } { manifest-job } + {#2} { \LALUAnswerManifestJob } + } + \bool_gset_true:N \g_lalu_answer_manifest_open_bool + \bool_gset_false:N \g_lalu_answer_manifest_ended_bool + \bool_gset_true:N \g_lalu_answer_first_unit_bool + \bool_gset_false:N \g_lalu_answer_unit_seen_bool + \bool_gset_false:N \g_lalu_answer_group_open_bool + \int_gzero:N \g_lalu_answer_unit_int + \int_gzero:N \g_lalu_answer_group_int + \int_gzero:N \g_lalu_answer_item_int + \int_gzero:N \g_lalu_answer_answered_int + \int_gzero:N \g_lalu_answer_todo_int + \int_gzero:N \g_lalu_answer_omitted_int + \prop_gclear:N \g_lalu_answer_unit_id_prop + \prop_gclear:N \g_lalu_answer_group_id_prop + \prop_gclear:N \g_lalu_answer_item_id_prop + \group_begin: + % The legacy answer PDF deliberately displayed chapter-based counters as + % ``.1'', ``.2'', ... in this generated section. Preserve that visible + % form, while giving Hyperref a unique answer-unit parent. + \cs_set:Npn \thechapter { } + \cs_set:Npn \theHchapter { \theHLALUanswerunit } + } + +\cs_new_protected:Npn \LALUAnswerUnit #1#2#3#4#5#6 + { + \lalu_answer_require_open:n { unit } + \bool_if:NT \g_lalu_answer_group_open_bool + { \msg_fatal:nnn { lalu-answer } { manifest-order } { unit-inside-group } } + \int_gincr:N \g_lalu_answer_unit_int + \str_if_eq:eeF + {#5} { answer-unit:\int_use:N \g_lalu_answer_unit_int } + { \msg_fatal:nnnn { lalu-answer } { manifest-data } { unit-id } {#5} } + \lalu_answer_claim_id:Nnn \g_lalu_answer_unit_id_prop {#5} { unit } + \bool_gset_true:N \g_lalu_answer_unit_seen_bool + \bool_if:NTF \g_lalu_answer_first_unit_bool + { \bool_gset_false:N \g_lalu_answer_first_unit_bool } + { \clearpage } + + \str_case:nnF {#1} + { + { normal } + { + \int_compare:nNnF {#2} > {0} + { \msg_fatal:nnnn { lalu-answer } { manifest-data } { normal-base } {#2} } + \int_compare:nNnF {#3} = {0} + { \msg_fatal:nnnn { lalu-answer } { manifest-data } { normal-serial } {#3} } + \int_compare:nNnF {#4} = {0} + { \msg_fatal:nnnn { lalu-answer } { manifest-data } { normal-slot } {#4} } + \tl_set:Nn \l_lalu_answer_reference_tl {#2} + \tl_set:Nn \l_lalu_answer_anchor_tl { answer.#5 } + \tl_set:Nn \l_lalu_answer_heading_tl { 第~#2~讲 } + } + { unfinished } + { + \int_compare:nNnF {#2} > {0} + { \msg_fatal:nnnn { lalu-answer } { manifest-data } { unfinished-base } {#2} } + \int_compare:nNnF {#3} > {0} + { \msg_fatal:nnnn { lalu-answer } { manifest-data } { unfinished-serial } {#3} } + \int_compare:nNnF {#4} > {0} + { \msg_fatal:nnnn { lalu-answer } { manifest-data } { unfinished-slot } {#4} } + \int_compare:nNnF {#4} < {7} + { \msg_fatal:nnnn { lalu-answer } { manifest-data } { unfinished-slot } {#4} } + \tl_set:Nn \l_lalu_answer_reference_tl + { #2\lalu_answer_greek:n {#4} } + \tl_set:Nn \l_lalu_answer_anchor_tl { answer.#5 } + \tl_set:Nn \l_lalu_answer_heading_tl + { 未竟专题\zhnumber{#3} } + } + } + { \msg_fatal:nnn { lalu-answer } { manifest-order } { unit-kind-#1 } } + + \refstepcounter{LALUanswerunit} + \setcounter{equation}{0} + \clist_map_inline:nn + { tcb@cnt@definition, tcb@cnt@example, tcb@cnt@lemma, + tcb@cnt@theorem, tcb@cnt@corollary, tcb@cnt@axiom } + { \cs_if_exist:cT { c@##1 } { \setcounter{##1}{0} } } + + % Write the TOC entry while the dedicated answer-unit href is current; + % \section* may create its own internal anchor afterwards. + \addcontentsline{toc}{section} + { \protect\numberline{\tl_use:N \l_lalu_answer_heading_tl}#6 } + \section*{\tl_use:N \l_lalu_answer_heading_tl \quad #6} + \markright{\tl_use:N \l_lalu_answer_heading_tl \quad #6} + } + +\cs_new_protected:Npn \LALUAnswerGroupBegin #1#2#3 + { + \lalu_answer_require_open:n { group-begin } + \bool_if:NF \g_lalu_answer_unit_seen_bool + { \msg_fatal:nnn { lalu-answer } { manifest-order } { group-before-unit } } + \bool_if:NT \g_lalu_answer_group_open_bool + { \msg_fatal:nnn { lalu-answer } { manifest-order } { nested-group } } + \int_gincr:N \g_lalu_answer_group_int + \str_if_eq:eeF + {#1} { answer-group:\int_use:N \g_lalu_answer_group_int } + { \msg_fatal:nnnn { lalu-answer } { manifest-data } { group-id } {#1} } + \lalu_answer_claim_id:Nnn \g_lalu_answer_group_id_prop {#1} { group } + \int_compare:nNnF {#2} > {0} + { \msg_fatal:nnnn { lalu-answer } { manifest-data } { group-number } {#2} } + \int_compare:nNnF {#3} > {0} + { \msg_fatal:nnnn { lalu-answer } { manifest-data } { group-size } {#3} } + \tl_set:Nn \l_lalu_answer_group_id_tl {#1} + \int_set:Nn \l_lalu_answer_group_number_int {#2} + \int_set:Nn \l_lalu_answer_group_expected_int {#3} + \int_zero:N \l_lalu_answer_group_item_int + \tl_clear:N \l_lalu_answer_group_items_tl + \bool_gset_true:N \g_lalu_answer_group_open_bool + } + +\cs_new_protected:Npn \LALUAnswerItem #1#2#3#4 + { + \lalu_answer_require_open:n { item } + \bool_if:NF \g_lalu_answer_group_open_bool + { \msg_fatal:nnn { lalu-answer } { manifest-order } { item-outside-group } } + \int_gincr:N \g_lalu_answer_item_int + \int_incr:N \l_lalu_answer_group_item_int + \str_if_eq:eeF + {#1} { answer-item:\int_use:N \g_lalu_answer_item_int } + { \msg_fatal:nnnn { lalu-answer } { manifest-data } { item-id } {#1} } + \lalu_answer_claim_id:Nnn \g_lalu_answer_item_id_prop {#1} { item } + \int_compare:nNnF {#2} > {0} + { \msg_fatal:nnnn { lalu-answer } { manifest-data } { item-number } {#2} } + \str_case:nnF {#3} + { + { answered } + { + \int_gincr:N \g_lalu_answer_answered_int + \tl_put_right:Nn \l_lalu_answer_group_items_tl + { \item[#2.] #4 } + } + { todo } + { + \int_gincr:N \g_lalu_answer_todo_int + \tl_put_right:Nn \l_lalu_answer_group_items_tl + { \item[#2.] \emph{暂无答案} } + } + % The legacy generator placed consecutive empty \item tokens next to + % one another. Accumulating raw item tokens and expanding them only + % at GroupEnd preserves that (slightly odd) visible layout exactly. + { omitted } + { + \int_gincr:N \g_lalu_answer_omitted_int + \tl_put_right:Nn \l_lalu_answer_group_items_tl + { \item[#2.] #4 } + } + } + { \msg_fatal:nnn { lalu-answer } { manifest-status } {#3} } + } + +\cs_new_protected:Npn \LALUAnswerGroupEnd #1#2 + { + \lalu_answer_require_open:n { group-end } + \bool_if:NF \g_lalu_answer_group_open_bool + { \msg_fatal:nnn { lalu-answer } { manifest-order } { group-end-without-begin } } + \str_if_eq:eeF {#1} { \tl_use:N \l_lalu_answer_group_id_tl } + { \msg_fatal:nnnn { lalu-answer } { manifest-data } { group-end-id } {#1} } + \lalu_answer_assert_count:nnn + { group-declared } {#2} { \l_lalu_answer_group_expected_int } + \lalu_answer_assert_count:nnn + { group-items } { \l_lalu_answer_group_item_int } + { \l_lalu_answer_group_expected_int } + \subsection*{\int_to_Alph:n { \l_lalu_answer_group_number_int }组} + \begin{enumerate} + \tl_use:N \l_lalu_answer_group_items_tl + \end{enumerate} + \tl_clear:N \l_lalu_answer_group_items_tl + \bool_gset_false:N \g_lalu_answer_group_open_bool + } + +\cs_new_protected:Npn \LALUAnswerManifestEnd #1#2#3#4#5#6#7 + { + \lalu_answer_require_open:n { end } + \bool_if:NT \g_lalu_answer_group_open_bool + { \msg_fatal:nnn { lalu-answer } { manifest-order } { end-inside-group } } + \lalu_answer_assert_count:nnn + { units } { \g_lalu_answer_unit_int } {#1} + \int_compare:nNnT {#3} > {#2} + { \msg_fatal:nnnn { lalu-answer } { manifest-data } { group-totals } {#3/#2} } + \lalu_answer_assert_count:nnn + { emitted-groups } { \g_lalu_answer_group_int } {#3} + \lalu_answer_assert_count:nnn + { items } { \g_lalu_answer_item_int } {#4} + \lalu_answer_assert_count:nnn + { answered } { \g_lalu_answer_answered_int } {#5} + \lalu_answer_assert_count:nnn + { todo } { \g_lalu_answer_todo_int } {#6} + \lalu_answer_assert_count:nnn + { omitted } { \g_lalu_answer_omitted_int } {#7} + \lalu_answer_assert_count:nnn + { item-status-sum } {#4} { \int_eval:n { #5 + #6 + #7 } } + \par + \group_end: + \bool_gset_true:N \g_lalu_answer_manifest_ended_bool + \iow_term:x + { + LALU~answers:~units~#1,~groups~#3/#2,~ + answered~#5,~todo~#6,~omitted~#7. + } + } + +\cs_new_protected:Npn \lalu_answer_input_manifest: + { + \tl_set:Ne \l_lalu_answer_manifest_path_tl + { \LALUAnswerManifestPath } + \file_if_exist:VTF \l_lalu_answer_manifest_path_tl + { \file_input:V \l_lalu_answer_manifest_path_tl } + { + \msg_fatal:nnn { lalu-answer } { manifest-missing } + { \LALUAnswerManifestPath } + } + \bool_if:NF \g_lalu_answer_manifest_ended_bool + { \msg_fatal:nnn { lalu-answer } { manifest-order } { missing-end } } + } +\cs_new_protected:Npn \LALUInputAnswerManifest + { \lalu_answer_input_manifest: } +\ExplSyntaxOff \begin{document} \frontmatter @@ -70,19 +358,14 @@ \chapter{《线性代数:未竟之美》习题参考答案} \titleformat{\subsection}[block]{\centering\heiti\bfseries}{}{1em}{} -\input{LALU-ans-contents.tex} - -\LUgroupsancheck - -\makeatletter -\let\section\@std@section -\let\@std@section\relax -\makeatother +% Read author tokens with ordinary LaTeX catcodes. In particular, spaces in +% label keys and prose must not be ignored as they would be under expl3 syntax. +\LALUInputAnswerManifest \ctexset{ section={format={\raggedright\Large\bfseries},name={,},number={}}, } -\renewcommand*{\thesection}{} % FIXME: this breaks equation numbering +\renewcommand*{\thesection}{} % 嵌套 enumerate 环境的 label \setlist[enumerate,1]{label=\chinese*、} diff --git "a/\350\256\262\344\271\211/Makefile" "b/\350\256\262\344\271\211/Makefile" index 8beee4f..a48b1e5 100644 --- "a/\350\256\262\344\271\211/Makefile" +++ "b/\350\256\262\344\271\211/Makefile" @@ -4,24 +4,46 @@ CPNAME := LALU LATEXMK := latexmk LATEXMK_FLAGS := -xelatex -shell-escape -interaction=nonstopmode -file-line-error +ANSWER_BUILD_DIR := build/answers +ANSWER_MANIFEST := $(ANSWER_BUILD_DIR)/$(CPNAME).manifest.tex +ANSWER_PUBLISHER := scripts/publish-answer-manifest.pl .PHONY: all clean all: - # Building $(FILENAME).pdf + @mkdir -p $(ANSWER_BUILD_DIR) ifdef CPNAME - @cp -p $(FILENAME).tex $(CPNAME).tex - $(LATEXMK) $(LATEXMK_FLAGS) $(CPNAME).tex - @rm -f $(CPNAME).tex - -@mv $(CPNAME).pdf $(FILENAME).pdf + @set -eu; \ + answer_tmp=$$(mktemp "$(ANSWER_BUILD_DIR)/$(CPNAME).manifest.XXXXXX"); \ + answer_stage=$$(mktemp -d "$(ANSWER_BUILD_DIR)/$(CPNAME).answers.XXXXXX"); \ + cleanup() { \ + rm -f "$$answer_tmp" "$(CPNAME).tex" "$(CPNAME).pdf"; \ + rm -rf -- "$$answer_stage"; \ + }; \ + trap cleanup EXIT HUP INT TERM; \ + cp -p $(FILENAME).tex $(CPNAME).tex; \ + $(LATEXMK) -g $(LATEXMK_FLAGS) \ + -usepretex="\\def\\LALUAnswerManifestTempPath{$$answer_tmp}" \ + $(CPNAME).tex; \ + perl $(ANSWER_PUBLISHER) --check "$$answer_tmp"; \ + $(LATEXMK) -g $(LATEXMK_FLAGS) \ + -outdir="$$answer_stage" \ + -usepretex="\\def\\LALUAnswerManifestPath{$$answer_tmp}\\def\\LALUAnswerManifestJob{$(CPNAME)}" \ + LALU-answers.tex; \ + test -s "$(CPNAME).pdf"; \ + test -s "$$answer_stage/LALU-answers.pdf"; \ + mv "$(CPNAME).pdf" "$(FILENAME).pdf"; \ + mv "$$answer_stage/LALU-answers.pdf" LALU-answers.pdf; \ + perl $(ANSWER_PUBLISHER) --publish "$$answer_tmp" "$(ANSWER_MANIFEST)" else $(LATEXMK) $(LATEXMK_FLAGS) $(FILENAME).tex -endif - # Building answers.pdf $(LATEXMK) $(LATEXMK_FLAGS) LALU-answers.tex +endif clean: rm -f *.aux *.log *.idx *.ind *.ilg *.thm *.toc *.blg *.bbl *.bcf *.out *.fls *.fdb_latexmk *.run.xml *.synctex.gz *.xdv *~ *.lof *.lot + rm -f LALU-ans-contents.tex *.answer-manifest.pending.tex + rm -rf -- $(ANSWER_BUILD_DIR) ifdef CPNAME rm -f $(CPNAME).pdf $(CPNAME).tex endif diff --git "a/\350\256\262\344\271\211/scripts/publish-answer-manifest.pl" "b/\350\256\262\344\271\211/scripts/publish-answer-manifest.pl" new file mode 100644 index 0000000..1b3ae19 --- /dev/null +++ "b/\350\256\262\344\271\211/scripts/publish-answer-manifest.pl" @@ -0,0 +1,323 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use File::Basename qw(dirname); +use File::Spec; + +sub usage { + die "usage: $0 --check TEMP_MANIFEST\n" + . " $0 --publish TEMP_MANIFEST PUBLISHED_MANIFEST\n"; +} + +sub read_manifest { + my ($path) = @_; + + -f $path or die "answer manifest was not generated: $path\n"; + open my $input, '<:raw', $path + or die "cannot read answer manifest '$path': $!\n"; + local $/; + my $contents = <$input>; + close $input or die "cannot close answer manifest '$path': $!\n"; + + defined $contents && length $contents + or die "answer manifest is empty: $path\n"; + $contents !~ /\0/ + or die "answer manifest contains a NUL byte: $path\n"; + return $contents; +} + +sub parse_reader_command { + my ($line, $name, $arity, $line_number) = @_; + my $prefix = "\\$name"; + + index($line, $prefix) == 0 + or die "expected $prefix at line $line_number\n"; + + my $length = length $line; + my $position = length $prefix; + if ($position < $length) { + my $next = substr($line, $position, 1); + $next =~ /[ \t{]/ + or die "invalid reader command name at line $line_number\n"; + } + + my @arguments; + for (1 .. $arity) { + ++$position while $position < $length + && substr($line, $position, 1) =~ /[ \t]/; + $position < $length && substr($line, $position, 1) eq '{' + or die "reader command '$name' has no argument $_ at line $line_number\n"; + + my $start = ++$position; + my $depth = 1; + while ($position < $length) { + my $character = substr($line, $position, 1); + + # A control symbol such as \{ does not contribute a grouping brace. + # Skipping the next byte is also sufficient for control words: the + # remaining letters cannot affect brace depth. + if ($character eq '\\') { + $position + 1 < $length + or die "reader command '$name' ends in a bare backslash at line $line_number\n"; + $position += 2; + next; + } + $character ne '%' + or die "reader command '$name' contains an unescaped comment at line $line_number\n"; + if ($character eq '{') { + ++$depth; + } + elsif ($character eq '}') { + --$depth; + if ($depth == 0) { + push @arguments, substr($line, $start, $position - $start); + ++$position; + last; + } + } + ++$position; + } + $depth == 0 + or die "reader command '$name' has an unclosed argument $_ at line $line_number\n"; + } + + ++$position while $position < $length + && substr($line, $position, 1) =~ /[ \t]/; + $position == $length + or die "reader command '$name' has trailing input at line $line_number\n"; + + return @arguments; +} + +sub is_uint { + my ($value) = @_; + return $value =~ /\A(?:0|[1-9]\d*)\z/; +} + +sub validate_manifest { + my ($path) = @_; + my $contents = read_manifest($path); + + # The manifest is read as raw UTF-8 bytes. Perl's \R also matches byte + # 0x85, which commonly occurs as a UTF-8 continuation byte in Chinese + # text, so only actual TeX input line endings may delimit records here. + my @lines = split /\r\n|\n|\r/, $contents, -1; + pop @lines if @lines && $lines[-1] eq ''; + @lines >= 3 + or die "answer manifest is incomplete: $path\n"; + + my $line_index = 0; + my $header = $lines[$line_index++]; + $header =~ /\A% LALU-ANSWER-MANIFEST schema=2 job=([A-Za-z0-9_.-]+)\z/ + or die "answer manifest has no valid schema header: $path\n"; + my $job = $1; + + my @reader_begin = parse_reader_command( + $lines[$line_index], 'LALUAnswerManifestBegin', 2, $line_index + 1 + ); + ++$line_index; + $reader_begin[0] eq '2' + or die "answer manifest reader has unsupported schema '$reader_begin[0]'\n"; + $reader_begin[1] =~ /\A[A-Za-z0-9_.-]+\z/ + or die "answer manifest reader has an invalid job name\n"; + $reader_begin[1] eq $job + or die "answer manifest header job '$job' does not match reader job '$reader_begin[1]'\n"; + + my (%unit_ids, %group_ids, %item_ids); + my ($current_unit, $current_group); + my ($expected_group_items, $seen_group_items) = (0, 0); + my ($unit_events, $group_events, $item_events) = (0, 0, 0); + my %status_events = (answered => 0, todo => 0, omitted => 0); + + my @reader_end; + while ($line_index < @lines) { + my $line = $lines[$line_index]; + if (index($line, '\\LALUAnswerManifestEnd') == 0) { + defined $current_group + and die "answer manifest ends inside group '$current_group'\n"; + @reader_end = parse_reader_command( + $line, 'LALUAnswerManifestEnd', 7, $line_index + 1 + ); + for my $value (@reader_end) { + is_uint($value) + or die "answer manifest reader end contains a non-canonical count at line " + . ($line_index + 1) . "\n"; + } + ++$line_index; + last; + } + + $line =~ /\A% LALU-MANIFEST-EVENT (.+)\z/ + or die "unexpected top-level input at line " . ($line_index + 1) . "\n"; + my $event = $1; + my $human_line = $line_index + 1; + ++$line_index; + $line_index < @lines + or die "event at line $human_line has no reader command\n"; + my $command = $lines[$line_index++]; + + if ($event =~ /\Aunit id=(answer-unit:([1-9]\d*))\z/) { + my ($id, $sequence) = ($1, $2); + defined $current_group + and die "unit '$id' begins before group '$current_group' ends\n"; + ++$unit_events; + $id eq "answer-unit:$unit_events" + or die "unit ID '$id' is out of sequence at line $human_line\n"; + !$unit_ids{$id}++ + or die "duplicate unit ID '$id' at line $human_line\n"; + my ($kind, $normal, $serial, $slot, $command_id) = + parse_reader_command($command, 'LALUAnswerUnit', 6, $human_line + 1); + $kind =~ /\A(?:normal|unfinished)\z/ + && is_uint($normal) && is_uint($serial) && is_uint($slot) + && $command_id eq $id + or die "unit marker at line $human_line does not match its reader command\n"; + if ($kind eq 'normal') { + $normal > 0 && $serial == 0 && $slot == 0 + or die "invalid normal-unit metadata at line $human_line\n"; + } + else { + $normal > 0 && $serial > 0 && $slot >= 1 && $slot <= 6 + or die "invalid unfinished-unit metadata at line $human_line\n"; + } + $current_unit = $id; + } + elsif ($event =~ /\Agroup-begin id=(answer-group:([1-9]\d*)) unit=(answer-unit:[1-9]\d*) number=([1-9]\d*) items=([1-9]\d*)\z/) { + my ($id, $sequence, $unit, $number, $items) = ($1, $2, $3, $4, $5); + defined $current_unit + or die "group '$id' appears before the first unit\n"; + !defined $current_group + or die "nested group '$id' at line $human_line\n"; + $unit eq $current_unit + or die "group '$id' belongs to '$unit', not current unit '$current_unit'\n"; + ++$group_events; + $id eq "answer-group:$group_events" + or die "group ID '$id' is out of sequence at line $human_line\n"; + !$group_ids{$id}++ + or die "duplicate group ID '$id' at line $human_line\n"; + my @arguments = parse_reader_command( + $command, 'LALUAnswerGroupBegin', 3, $human_line + 1 + ); + join("\0", @arguments) eq join("\0", $id, $number, $items) + or die "group-begin marker at line $human_line does not match its reader command\n"; + $current_group = $id; + $expected_group_items = $items; + $seen_group_items = 0; + } + elsif ($event =~ /\Aitem id=(answer-item:([1-9]\d*)) group=(answer-group:[1-9]\d*) number=([1-9]\d*) status=(answered|todo|omitted)\z/) { + my ($id, $sequence, $group, $number, $status) = ($1, $2, $3, $4, $5); + defined $current_group + or die "item '$id' appears outside a group\n"; + $group eq $current_group + or die "item '$id' belongs to '$group', not current group '$current_group'\n"; + ++$item_events; + ++$seen_group_items; + $id eq "answer-item:$item_events" + or die "item ID '$id' is out of sequence at line $human_line\n"; + !$item_ids{$id}++ + or die "duplicate item ID '$id' at line $human_line\n"; + ++$status_events{$status}; + my @arguments = parse_reader_command( + $command, 'LALUAnswerItem', 4, $human_line + 1 + ); + join("\0", @arguments[0 .. 2]) eq join("\0", $id, $number, $status) + or die "item marker at line $human_line does not match its reader command\n"; + } + elsif ($event =~ /\Agroup-end id=(answer-group:[1-9]\d*) items=([1-9]\d*)\z/) { + my ($id, $items) = ($1, $2); + defined $current_group + or die "group-end '$id' appears without a group-begin\n"; + $id eq $current_group + or die "group-end '$id' does not match current group '$current_group'\n"; + $items == $expected_group_items && $seen_group_items == $expected_group_items + or die "group '$id' item count mismatch\n"; + my @arguments = parse_reader_command( + $command, 'LALUAnswerGroupEnd', 2, $human_line + 1 + ); + join("\0", @arguments) eq join("\0", $id, $items) + or die "group-end marker at line $human_line does not match its reader command\n"; + undef $current_group; + ($expected_group_items, $seen_group_items) = (0, 0); + } + else { + die "unknown answer manifest event at line $human_line: $event\n"; + } + } + + @reader_end + or die "answer manifest has no reader end record: $path\n"; + $line_index < @lines + or die "answer manifest has no end sentinel: $path\n"; + my $footer = $lines[$line_index++]; + my ($total_units, $total_groups, $emitted_groups, $total_items, + $answered, $todo, $omitted) = + $footer =~ /\A% LALU-ANSWER-MANIFEST-END total-units=(0|[1-9]\d*) total-groups=(0|[1-9]\d*) emitted-groups=(0|[1-9]\d*) total-items=(0|[1-9]\d*) answered=(0|[1-9]\d*) todo=(0|[1-9]\d*) omitted=(0|[1-9]\d*)\z/; + defined $omitted + or die "answer manifest has no valid end sentinel at line $line_index: $path\n"; + $line_index == @lines + or die "unexpected input after answer manifest footer at line " + . ($line_index + 1) . "\n"; + + my @footer_counts = ($total_units, $total_groups, $emitted_groups, + $total_items, $answered, $todo, $omitted); + join("\0", @reader_end) eq join("\0", @footer_counts) + or die "answer manifest reader statistics do not match its end sentinel\n"; + + $unit_events == $total_units + or die "answer manifest unit count mismatch: found $unit_events, expected $total_units\n"; + $group_events == $emitted_groups + or die "answer manifest group count mismatch: found $group_events, expected $emitted_groups\n"; + $emitted_groups <= $total_groups + or die "answer manifest emitted more groups than it observed\n"; + $item_events == $total_items + or die "answer manifest item count mismatch: found $item_events, expected $total_items\n"; + $answered + $todo + $omitted == $total_items + or die "answer manifest status counts do not add up\n"; + $status_events{answered} == $answered + or die "answered item count mismatch\n"; + $status_events{todo} == $todo + or die "todo item count mismatch\n"; + $status_events{omitted} == $omitted + or die "omitted item count mismatch\n"; + + return { + job => $job, + units => $total_units, + total_groups => $total_groups, + emitted_groups => $emitted_groups, + answered => $answered, + todo => $todo, + omitted => $omitted, + }; +} + +my ($mode, $temporary, $published); +if (@ARGV == 2 && $ARGV[0] eq '--check') { + ($mode, $temporary) = @ARGV; +} +elsif (@ARGV == 3 && $ARGV[0] eq '--publish') { + ($mode, $temporary, $published) = @ARGV; +} +else { + usage(); +} + +my $stats = validate_manifest($temporary); + +if ($mode eq '--publish') { + my $temporary_directory = File::Spec->rel2abs(dirname($temporary)); + my $published_directory = File::Spec->rel2abs(dirname($published)); + -d $published_directory + or die "published answer manifest directory does not exist: $published_directory\n"; + $temporary_directory eq $published_directory + or die "temporary and published manifests must be in the same directory\n"; + rename $temporary, $published + or die "cannot atomically publish '$temporary' as '$published': $!\n"; + print "Published answer manifest: $published\n"; +} +else { + print "Validated answer manifest: $temporary\n"; +} + +print " units $stats->{units}, groups $stats->{emitted_groups}/$stats->{total_groups}, " + . "answers $stats->{answered}, todo $stats->{todo}, omitted $stats->{omitted}\n"; diff --git "a/\350\256\262\344\271\211/\347\272\277\346\200\247\344\273\243\346\225\260\350\215\243\350\252\211\350\257\276\350\276\205\345\255\246\350\256\262\344\271\211.tex" "b/\350\256\262\344\271\211/\347\272\277\346\200\247\344\273\243\346\225\260\350\215\243\350\252\211\350\257\276\350\276\205\345\255\246\350\256\262\344\271\211.tex" index 802655c..240e64d 100644 --- "a/\350\256\262\344\271\211/\347\272\277\346\200\247\344\273\243\346\225\260\350\215\243\350\252\211\350\257\276\350\276\205\345\255\246\350\256\262\344\271\211.tex" +++ "b/\350\256\262\344\271\211/\347\272\277\346\200\247\344\273\243\346\225\260\350\215\243\350\252\211\350\257\276\350\276\205\345\255\246\350\256\262\344\271\211.tex" @@ -43,41 +43,491 @@ % \chapter 正常专题 % \LUchapter 未竟专题 -\newcounter{LUchapter} -\newcounter{LUgreekchap} +% 结构 chapter 只记录所有有编号顶层单元的次序;各类可见编号独立维护。 +\newcounter{LALUnormalchapter} +\newcounter{LALUunfinishedchapter} +\newcounter{LALUunfinishedslot} +\newcounter{LALUappendixchapter} +\newcounter{LALUunnumberedunit} \makeatletter -% 此处可按需增改 -% \texorpdfstring 的两个参数分别显示在正文中与 PDF 书签中 -\newcommand*{\@LUgreek}[1]{% - \ifcase#1\or\texorpdfstring{$\boldsymbol{\varepsilon}$}{ε}% - \or\texorpdfstring{$\boldsymbol{\delta}$}{δ}% - \or\texorpdfstring{$\boldsymbol{\lambda}$}{λ}% - \or\texorpdfstring{$\boldsymbol{\mu}$}{μ}% - \or\texorpdfstring{$\boldsymbol{\varphi}$}{φ}% - \or\texorpdfstring{$\boldsymbol{\theta}$}{θ}% - \else\@ctrerr\fi% -} -\newcommand*{\LUgreek}[1]{% - \expandafter\@LUgreek\csname c@#1\endcsname -} -\newcommand*{\LUchapsancheck}{% -\expandafter\@ifundefined{@exist@LUchapter@\arabic{chapter}.\arabic{LUgreekchap}}% - {\setcounter{LUgreekchap}{1}} - {\stepcounter{LUgreekchap}} -} -\newcommand*{\LUgroupsancheck}{% -\expandafter\@ifundefined{@exist@LUchapter@\arabic{chapter}}% - {} - {\endgroup} -} -\let\@std@chapter\chapter -\renewcommand*{\chapter}{% - \LUgroupsancheck% - \@std@chapter -} -\let\@std@chaptermark\chaptermark -\def\chaptermark#1{\def\@LALU@chaptername{#1}\@std@chaptermark{#1}} +\ExplSyntaxOn +\tl_new:N \g_lalu_chapter_phase_tl +\tl_new:N \g_lalu_chapter_kind_tl +\tl_new:N \g_lalu_chapter_title_tl +\tl_new:N \g_lalu_chapter_short_title_tl +\tl_new:N \g_lalu_chapter_unit_id_tl +\tl_gset:Nn \g_lalu_chapter_phase_tl { initial } +\tl_gset:Nn \g_lalu_chapter_kind_tl { unnumbered } + +% 正文字形和 PDF 字符串字形在这一张表中同步维护。 +\cs_new:Npn \lalu_chapter_greek:n #1 + { + \int_case:nnF {#1} + { + {1}{\texorpdfstring{$\boldsymbol{\varepsilon}$}{ε}} + {2}{\texorpdfstring{$\boldsymbol{\delta}$}{δ}} + {3}{\texorpdfstring{$\boldsymbol{\lambda}$}{λ}} + {4}{\texorpdfstring{$\boldsymbol{\mu}$}{μ}} + {5}{\texorpdfstring{$\boldsymbol{\varphi}$}{φ}} + {6}{\texorpdfstring{$\boldsymbol{\theta}$}{θ}} + } + { } + } + +% 供答案生成器等 supporting structure 读取的语义接口。 +\NewExpandableDocumentCommand \LALUCurrentUnitKind { } + { \tl_use:N \g_lalu_chapter_kind_tl } +\NewExpandableDocumentCommand \LALUNormalChapterNumber { } + { \int_to_arabic:n { \value{LALUnormalchapter} } } +\NewExpandableDocumentCommand \LALUCurrentNormalBase { } + { \int_to_arabic:n { \value{LALUnormalchapter} } } +\NewExpandableDocumentCommand \LALUCurrentUnfinishedSerial { } + { + \str_if_eq:VnTF \g_lalu_chapter_kind_tl { unfinished } + { \int_to_arabic:n { \value{LALUunfinishedchapter} } } + { 0 } + } +\NewExpandableDocumentCommand \LALUCurrentUnfinishedSlot { } + { + \str_if_eq:VnTF \g_lalu_chapter_kind_tl { unfinished } + { \int_to_arabic:n { \value{LALUunfinishedslot} } } + { 0 } + } +\NewExpandableDocumentCommand \LALUCurrentReferenceNumber { } + { + \str_case:VnF \g_lalu_chapter_kind_tl + { + { normal } + { \int_to_arabic:n { \value{LALUnormalchapter} } } + { unfinished } + { + \int_to_arabic:n { \value{LALUnormalchapter} } + \lalu_chapter_greek:n { \value{LALUunfinishedslot} } + } + { appendix } + { \int_to_Alph:n { \value{LALUappendixchapter} } } + } + { } + } +\NewExpandableDocumentCommand \LALUCurrentUnitTitle { } + { \tl_use:N \g_lalu_chapter_title_tl } +\cs_new_protected:Npn \LALUGetCurrentUnitTitle:N #1 + { \tl_set:NV #1 \g_lalu_chapter_title_tl } +\NewExpandableDocumentCommand \LALUCurrentUnitID { } + { \tl_use:N \g_lalu_chapter_unit_id_tl } +\NewExpandableDocumentCommand \LALUIfCurrentAnswerUnitTF { m m } + { + \str_if_eq:VnTF \g_lalu_chapter_kind_tl { normal } + {#1} + { + \str_if_eq:VnTF \g_lalu_chapter_kind_tl { unfinished } + {#1} + {#2} + } + } + +\cs_new:Npn \lalu_chapter_anchor_number: + { + \str_case:VnF \g_lalu_chapter_kind_tl + { + { normal } + { \int_to_arabic:n { \value{LALUnormalchapter} } } + { unfinished } + { LU.\int_to_arabic:n { \value{LALUunfinishedchapter} } } + { appendix } + { APP.\int_to_Alph:n { \value{LALUappendixchapter} } } + { unnumbered } + { UNNUMBERED.\int_to_arabic:n { \value{LALUunnumberedunit} } } + } + { UNNUMBERED.0 } + } + +\cs_new_protected:Npn \lalu_chapter_install_number_commands: + { + \cs_gset:Npn \thechapter { \LALUCurrentReferenceNumber } + \cs_gset:Npn \theHchapter { \lalu_chapter_anchor_number: } + % CTeX makes \p@chapter discard \thechapter in favour of the Chinese + % title number. Current LaTeX/cleveref recomputes label data from this + % prefix inside every \label, so restore the standard empty prefix and + % keep the semantic reference number supplied by \thechapter. + \cs_gset:Npn \p@chapter { } + } + +% cleveref 自身会在 \appendix 中切换类型,但新 LaTeX 内核的 +% first-aid label hook 会从 counter alias 重建数据。在阶段切换时同步 +% alias,避免将附录章误记为 chapter。 +\cs_new_protected:Npn \lalu_chapter_set_main_cref_aliases: + { + \cs_if_exist:NT \crefalias + { + \crefalias{chapter}{chapter} + \crefalias{section}{section} + \crefalias{subsection}{subsection} + \crefalias{subsubsection}{subsubsection} + } + } +\cs_new_protected:Npn \lalu_chapter_set_appendix_cref_aliases: + { + \cs_if_exist:NT \crefalias + { + \crefalias{chapter}{appendix} + \crefalias{section}{subappendix} + \crefalias{subsection}{subsubappendix} + \crefalias{subsubsection}{subsubsubappendix} + } + } + +\cs_new_protected:Npn \lalu_chapter_apply_normal_style: + { + \ctexset + { + chapter = + { + format = { \centering \Huge \bfseries }, + name = { 第,讲 }, + number = { \arabic{LALUnormalchapter} } + } + } + } +\cs_new_protected:Npn \lalu_chapter_apply_unfinished_style: + { + \ctexset + { + chapter = + { + format = { \centering \Huge \bfseries }, + name = { 未竟专题, }, + number = { \zhnumber{\arabic{LALUunfinishedchapter}} } + } + } + } +\cs_new_protected:Npn \lalu_chapter_apply_appendix_style: + { + \ctexset + { + chapter = + { + format = { \centering \Huge \bfseries }, + name = { 附录, }, + number = { \Alph{LALUappendixchapter} } + } + } + } + +\cs_new_protected:Npn \lalu_chapter_clear_current_unit: + { + \tl_gset:Nn \g_lalu_chapter_kind_tl { unnumbered } + \tl_gclear:N \g_lalu_chapter_title_tl + \tl_gclear:N \g_lalu_chapter_short_title_tl + \tl_gclear:N \g_lalu_chapter_unit_id_tl + } +\cs_new_protected:Npn \lalu_chapter_set_titles:nn #1#2 + { + \tl_gset:Nn \g_lalu_chapter_title_tl {#2} + \tl_if_novalue:nTF {#1} + { \tl_gset:Nn \g_lalu_chapter_short_title_tl {#2} } + { \tl_gset:Nn \g_lalu_chapter_short_title_tl {#1} } + } +\cs_new_protected:Npn \lalu_chapter_set_normal:nn #1#2 + { + \stepcounter{LALUnormalchapter} + \setcounter{LALUunfinishedslot}{0} + \tl_gset:Nn \g_lalu_chapter_kind_tl { normal } + \tl_gset:Nx \g_lalu_chapter_unit_id_tl + { normal:\int_to_arabic:n { \value{LALUnormalchapter} } } + \lalu_chapter_set_titles:nn {#1}{#2} + \lalu_chapter_apply_normal_style: + } +\cs_new_protected:Npn \lalu_chapter_set_unfinished:nn #1#2 + { + \stepcounter{LALUunfinishedchapter} + \stepcounter{LALUunfinishedslot} + \tl_gset:Nn \g_lalu_chapter_kind_tl { unfinished } + \tl_gset:Nx \g_lalu_chapter_unit_id_tl + { lu:\int_to_arabic:n { \value{LALUunfinishedchapter} } } + \lalu_chapter_set_titles:nn {#1}{#2} + \lalu_chapter_apply_unfinished_style: + } +\cs_new_protected:Npn \lalu_chapter_set_appendix:nn #1#2 + { + \stepcounter{LALUappendixchapter} + \tl_gset:Nn \g_lalu_chapter_kind_tl { appendix } + \tl_gset:Nx \g_lalu_chapter_unit_id_tl + { appendix:\int_to_Alph:n { \value{LALUappendixchapter} } } + \lalu_chapter_set_titles:nn {#1}{#2} + \lalu_chapter_apply_appendix_style: + } +\cs_new_protected:Npn \lalu_chapter_set_unnumbered:nn #1#2 + { + \stepcounter{LALUunnumberedunit} + \lalu_chapter_clear_current_unit: + \lalu_chapter_set_titles:nn {#1}{#2} + } + +% 与 CTeX 的标准章编号判定保持一致。 +\cs_new_protected:Npn \lalu_chapter_if_numbered:TF #1#2 + { + \if@mainmatter + \ifnum \c@secnumdepth > \m@ne + \ifodd \CTEX@chapter@numbering + #1 + \else + #2 + \fi + \else + #2 + \fi + \else + #2 + \fi + } +\cs_new_protected:Npn \lalu_chapter_call_standard:nn #1#2 + { + \tl_if_novalue:nTF {#1} + { \LALU@standardchapter{#2} } + { \LALU@standardchapter[#1]{#2} } + } + +% CTeX 会把 chapter 的 label 值设成标题中文编号;这里恢复数字引用语义。 +\cs_new_protected:Npn \lalu_chapter_restore_current_label: + { + \protected@xdef \@currentlabel + { \LALUCurrentReferenceNumber } + \cs_if_exist:NT \cref@constructprefix + { + \cref@constructprefix{chapter}{\cref@result} + \str_if_eq:VnTF \g_lalu_chapter_kind_tl { appendix } + { \def\lalu@creftype{appendix} } + { \def\lalu@creftype{chapter} } + \protected@xdef \cref@currentlabel + { + [\lalu@creftype][\arabic{chapter}][\cref@result] + \LALUCurrentReferenceNumber + } + } + } +\cs_new_protected:Npn \lalu_chapter_clear_current_label: + { + \global\let\@currentlabel\@empty + % LaTeX 2025+ label hooks use this counter name to reconstruct package + % metadata. An unnumbered chapter must not inherit the previous chapter. + \global\let\@currentcounter\@empty + \cs_if_exist:NT \cref@currentlabel + { \global\let\cref@currentlabel\@empty } + } + +\cs_new_protected:Npn \lalu_chapter_numbered:nn #1#2 + { + \str_case:VnF \g_lalu_chapter_phase_tl + { + { initial } + { + \tl_gset:Nn \g_lalu_chapter_phase_tl { mainmatter } + \lalu_chapter_set_normal:nn {#1}{#2} + } + { mainmatter } + { \lalu_chapter_set_normal:nn {#1}{#2} } + { appendix } + { \lalu_chapter_set_appendix:nn {#1}{#2} } + } + { + \lalu_chapter_set_unnumbered:nn {#1}{#2} + } + \lalu_chapter_call_standard:nn {#1}{#2} + \str_if_eq:VnTF \g_lalu_chapter_kind_tl { unnumbered } + { \lalu_chapter_clear_current_label: } + { \lalu_chapter_restore_current_label: } + } +\cs_new_protected:Npn \lalu_chapter_unstarred:nn #1#2 + { + \lalu_chapter_if_numbered:TF + { \lalu_chapter_numbered:nn {#1}{#2} } + { + \lalu_chapter_set_unnumbered:nn {#1}{#2} + \lalu_chapter_call_standard:nn {#1}{#2} + \lalu_chapter_clear_current_label: + } + } +\cs_new_protected:Npn \lalu_chapter_starred:n #1 + { + \lalu_chapter_set_unnumbered:nn { \NoValue }{#1} + \LALU@standardchapter*{#1} + \lalu_chapter_clear_current_label: + } + +\cs_new_protected:Npn \lalu_unfinished_chapter:nn #1#2 + { + \str_if_eq:VnTF \g_lalu_chapter_phase_tl { mainmatter } + { + \int_compare:nNnTF { \value{LALUnormalchapter} } > { 0 } + { + \lalu_chapter_if_numbered:TF + { + \int_compare:nNnTF { \value{LALUunfinishedslot} } < { 6 } + { + \lalu_chapter_set_unfinished:nn {#1}{#2} + \lalu_chapter_call_standard:nn {#1}{#2} + \lalu_chapter_restore_current_label: + } + { + \PackageError{LALU} + {Too~many~unfinished~topics~after~chapter~\LALUNormalChapterNumber} + {Only~six~Greek~slots~are~configured.~Extend~the~central~mapping~before~adding~another~topic.} + } + } + { + \PackageError{LALU} + {An~unfinished~topic~cannot~be~unnumbered} + {Restore~chapter~numbering~before~using~\string\LUchapter.} + } + } + { + \PackageError{LALU} + {An~unfinished~topic~requires~a~preceding~normal~chapter} + {Start~a~numbered~normal~chapter~before~using~\string\LUchapter.} + } + } + { + \PackageError{LALU} + {\string\LUchapter~is~only~valid~in~mainmatter} + {Move~the~unfinished~topic~after~\string\mainmatter~and~a~numbered~normal~chapter.} + } + } + +\cs_new_protected:Npn \lalu_phase_error:n #1 + { + \PackageError{LALU} + {Invalid~document~phase~transition~from~\tl_use:N\g_lalu_chapter_phase_tl~to~#1} + {The~supported~order~is~frontmatter,~mainmatter,~appendix,~backmatter;~each~phase~command~is~idempotent.} + } +\cs_new_protected:Npn \lalu_frontmatter: + { + \str_if_eq:VnTF \g_lalu_chapter_phase_tl { frontmatter } + { } + { + \str_if_eq:VnTF \g_lalu_chapter_phase_tl { initial } + { + \LALU@standardfrontmatter + \tl_gset:Nn \g_lalu_chapter_phase_tl { frontmatter } + \lalu_chapter_clear_current_unit: + } + { \lalu_phase_error:n { frontmatter } } + } + } +\cs_new_protected:Npn \lalu_mainmatter: + { + \str_if_eq:VnTF \g_lalu_chapter_phase_tl { mainmatter } + { } + { + \str_case:VnF \g_lalu_chapter_phase_tl + { + { initial } + { + \LALU@standardmainmatter + \setcounter{chapter}{0} + \setcounter{LALUnormalchapter}{0} + \setcounter{LALUunfinishedchapter}{0} + \setcounter{LALUunfinishedslot}{0} + \setcounter{LALUappendixchapter}{0} + \tl_gset:Nn \g_lalu_chapter_phase_tl { mainmatter } + \lalu_chapter_clear_current_unit: + \lalu_chapter_set_main_cref_aliases: + } + { frontmatter } + { + \LALU@standardmainmatter + \setcounter{chapter}{0} + \setcounter{LALUnormalchapter}{0} + \setcounter{LALUunfinishedchapter}{0} + \setcounter{LALUunfinishedslot}{0} + \setcounter{LALUappendixchapter}{0} + \tl_gset:Nn \g_lalu_chapter_phase_tl { mainmatter } + \lalu_chapter_clear_current_unit: + \lalu_chapter_set_main_cref_aliases: + } + } + { \lalu_phase_error:n { mainmatter } } + } + } +\int_new:N \l_lalu_structural_chapter_int +\cs_new_protected:Npn \lalu_appendix: + { + \str_if_eq:VnTF \g_lalu_chapter_phase_tl { appendix } + { } + { + \str_if_eq:VnTF \g_lalu_chapter_phase_tl { mainmatter } + { + \int_set:Nn \l_lalu_structural_chapter_int { \value{chapter} } + \LALU@standardappendix + \setcounter{chapter}{\int_use:N \l_lalu_structural_chapter_int} + \setcounter{LALUappendixchapter}{0} + \tl_gset:Nn \g_lalu_chapter_phase_tl { appendix } + \lalu_chapter_clear_current_unit: + \lalu_chapter_install_number_commands: + \lalu_chapter_set_appendix_cref_aliases: + \lalu_chapter_apply_appendix_style: + } + { \lalu_phase_error:n { appendix } } + } + } +\cs_new_protected:Npn \lalu_backmatter: + { + \str_if_eq:VnTF \g_lalu_chapter_phase_tl { backmatter } + { } + { + \str_case:VnF \g_lalu_chapter_phase_tl + { + { frontmatter } + { + \LALU@standardbackmatter + \tl_gset:Nn \g_lalu_chapter_phase_tl { backmatter } + \lalu_chapter_clear_current_unit: + \lalu_chapter_set_main_cref_aliases: + } + { mainmatter } + { + \LALU@standardbackmatter + \tl_gset:Nn \g_lalu_chapter_phase_tl { backmatter } + \lalu_chapter_clear_current_unit: + \lalu_chapter_set_main_cref_aliases: + } + { appendix } + { + \LALU@standardbackmatter + \tl_gset:Nn \g_lalu_chapter_phase_tl { backmatter } + \lalu_chapter_clear_current_unit: + \lalu_chapter_set_main_cref_aliases: + } + } + { \lalu_phase_error:n { backmatter } } + } + } + +\NewDocumentCommand \LUchapter { o m } + { \lalu_unfinished_chapter:nn {#1}{#2} } + +% 所有宏包完成 chapter/phase 补丁后再保存最终标准实现。 +\AddToHook{begindocument/end} + { + \let\LALU@standardchapter\chapter + \let\LALU@standardfrontmatter\frontmatter + \let\LALU@standardmainmatter\mainmatter + \let\LALU@standardappendix\appendix + \let\LALU@standardbackmatter\backmatter + \RenewDocumentCommand \chapter { s o m } + { + \IfBooleanTF{#1} + { \lalu_chapter_starred:n {#3} } + { \lalu_chapter_unstarred:nn {#2}{#3} } + } + \RenewDocumentCommand \frontmatter { } { \lalu_frontmatter: } + \RenewDocumentCommand \mainmatter { } { \lalu_mainmatter: } + \RenewDocumentCommand \appendix { } { \lalu_appendix: } + \RenewDocumentCommand \backmatter { } { \lalu_backmatter: } + \lalu_chapter_install_number_commands: + } +\ExplSyntaxOff \makeatother % 内容总结 @@ -101,84 +551,286 @@ % \end{exercise} \newcounter{exgroupcounter} \ExplSyntaxOn -\newenvironment{exercise}{ - \iow_now:Nx \g_lalu_ans_iow { - \cs_if_exist:cTF { @exist@LUchapter@\arabic{chapter} } - { - \exp_not:N \setcounter{LUsection}{ \int_eval:n { \value{LUchapter} - 1 } } - \exp_not:N \LUsection - } - { - \exp_not:N \setcounter{section}{ \int_eval:n { \value{chapter} - 1 } } - \exp_not:N \section - } - { \cs:w @LALU@chaptername \cs_end: } - } +\msg_new:nnn { lalu-answer } { invalid-unit } + { The~exercise~environment~requires~a~numbered~normal~or~unfinished~chapter. } +\msg_new:nnn { lalu-answer } { invalid-nesting } + { Invalid~exercise~environment~nesting~near~'#1'. } +\msg_new:nnn { lalu-answer } { invalid-status } + { Unknown~answer~status~'#1'. } + +\bool_new:N \g_lalu_ans_in_exercise_bool +\bool_new:N \l_lalu_ans_in_group_bool +\seq_new:N \g_lalu_ans_group_seq +\tl_new:N \l_lalu_ans_status_tl +\tl_new:N \l_lalu_ans_body_probe_tl +\tl_new:N \l_lalu_ans_unit_kind_tl +\tl_new:N \l_lalu_ans_unit_normal_tl +\tl_new:N \l_lalu_ans_unit_serial_tl +\tl_new:N \l_lalu_ans_unit_slot_tl +\tl_new:N \l_lalu_ans_unit_id_tl +\tl_new:N \l_lalu_ans_unit_title_tl +\int_new:N \g_lalu_ans_total_unit_int +\int_new:N \g_lalu_ans_total_group_int +\int_new:N \g_lalu_ans_emitted_group_int +\int_new:N \g_lalu_ans_total_item_int +\int_new:N \g_lalu_ans_answered_int +\int_new:N \g_lalu_ans_todo_int +\int_new:N \g_lalu_ans_omitted_int +\int_new:N \g_lalu_ans_group_number_int +\tl_new:N \l_lalu_ans_group_id_tl + +\keys_define:nn { lalu / answer } + { status .tl_set:N = \l_lalu_ans_status_tl } + +\cs_new_protected:Npn \lalu_ans_write_marker:n #1 + { + \iow_now:Nx \g_lalu_ans_iow + { \c_percent_str \c_space_tl LALU-MANIFEST-EVENT \c_space_tl #1 } + } + +\cs_new_protected:Npn \lalu_ans_write_unit: + { + \lalu_ans_write_marker:n + { unit~id=\l_lalu_ans_unit_id_tl } + \iow_now:Nx \g_lalu_ans_iow + { + \exp_not:N \LALUAnswerUnit + { \exp_not:V \l_lalu_ans_unit_kind_tl } + { \exp_not:V \l_lalu_ans_unit_normal_tl } + { \exp_not:V \l_lalu_ans_unit_serial_tl } + { \exp_not:V \l_lalu_ans_unit_slot_tl } + { \exp_not:V \l_lalu_ans_unit_id_tl } + { \exp_not:V \l_lalu_ans_unit_title_tl } + } + } + +\cs_new_protected:Npn \lalu_ans_write_item:nnnn #1#2#3#4 + { + \lalu_ans_write_marker:n + { + item~id=#1~group=\l_lalu_ans_group_id_tl + \c_space_tl number=#2~status=#3 + } + \iow_now:Nx \g_lalu_ans_iow + { + \exp_not:N \LALUAnswerItem + {#1}{#2}{#3}{ \exp_not:n {#4} } + } + } + +\cs_new_protected:Npn \lalu_ans_begin_exercise: + { + \bool_if:NT \g_lalu_ans_in_exercise_bool + { \msg_fatal:nnn { lalu-answer } { invalid-nesting } { exercise } } + \LALUIfCurrentAnswerUnitTF + { + \bool_gset_true:N \g_lalu_ans_in_exercise_bool + \int_gincr:N \g_lalu_ans_total_unit_int + \tl_set:Ne \l_lalu_ans_unit_kind_tl { \LALUCurrentUnitKind } + \tl_set:Ne \l_lalu_ans_unit_normal_tl { \LALUCurrentNormalBase } + \tl_set:Ne \l_lalu_ans_unit_serial_tl + { \LALUCurrentUnfinishedSerial } + \tl_set:Ne \l_lalu_ans_unit_slot_tl { \LALUCurrentUnfinishedSlot } + % Record IDs deliberately follow manifest order rather than chapter + % identity. This keeps them unique even if a chapter contains more + % than one exercise environment; cross-version stability is outside + % the current schema contract. + \tl_set:Nx \l_lalu_ans_unit_id_tl + { answer-unit:\int_use:N \g_lalu_ans_total_unit_int } + \LALUGetCurrentUnitTitle:N \l_lalu_ans_unit_title_tl + \lalu_ans_write_unit: + } + { \msg_fatal:nn { lalu-answer } { invalid-unit } } \setcounter{exgroupcounter}{0} - \NewDocumentEnvironment{exgroup}{o}{ - \IfValueTF{##1}{ - \setcounter{exgroupcounter}{##1} - }{ - \stepcounter{exgroupcounter} - } - \subsection*{\Alph{exgroupcounter}组} - \begin{enumerate} - }{ - \end{enumerate} - \seq_if_empty:NF \g_lalu_ans_seq { - \iow_now:Nx \g_lalu_ans_iow { \exp_not:N \subsection*{ \Alph{exgroupcounter} 组 } \exp_not:N \begin{enumerate} } - \seq_map_tokens:Nn \g_lalu_ans_seq { \iow_now:Nn \g_lalu_ans_iow } - \iow_now:Nn \g_lalu_ans_iow { \end{enumerate} } - } - \seq_gclear:N \g_lalu_ans_seq - } - \NewDocumentEnvironment{answer}{+b}{}{ - \seq_gput_right:Nx \g_lalu_ans_seq { \exp_not:N \item [\theenumi.] \tl_to_str:n { ##1 } } - } - \NewDocumentCommand{\exquote}{om}{ - \vspace{2ex} - {\sffamily\kaishu ##2} - \IfValueT{##1}{ - \begin{flushright} - \sffamily\kaishu ——##1 - \end{flushright} - } - } \hypersetup{bookmarksnumbered=false} \titleformat{\section}[block]{\centering\heiti\Large}{}{1em}{} \titleformat{\subsection}[block]{\centering\heiti\bfseries}{}{1em}{} \phantomsection \section*{习题} \addcontentsline{toc}{section}{习题} -}{ - \iow_now:Nn \g_lalu_ans_iow { \clearpage } -} -\seq_new:N \g_lalu_ans_seq % stores the answers, shipped at the end of the exgroup + } + +\cs_new_protected:Npn \lalu_ans_end_exercise: + { + \bool_if:NF \g_lalu_ans_in_exercise_bool + { \msg_fatal:nnn { lalu-answer } { invalid-nesting } { exercise-end } } + \bool_gset_false:N \g_lalu_ans_in_exercise_bool + } + +\NewDocumentEnvironment{exercise} { } + { \lalu_ans_begin_exercise: } + { \lalu_ans_end_exercise: } + +\cs_new_protected:Npn \lalu_ans_begin_group:n #1 + { + \bool_if:NF \g_lalu_ans_in_exercise_bool + { \msg_fatal:nnn { lalu-answer } { invalid-nesting } { exgroup } } + \bool_if:NT \l_lalu_ans_in_group_bool + { \msg_fatal:nnn { lalu-answer } { invalid-nesting } { nested-exgroup } } + \bool_set_true:N \l_lalu_ans_in_group_bool + \int_gincr:N \g_lalu_ans_total_group_int + \seq_gclear:N \g_lalu_ans_group_seq + \tl_if_novalue:nTF {#1} + { \stepcounter{exgroupcounter} } + { \setcounter{exgroupcounter}{#1} } + \int_gset:Nn \g_lalu_ans_group_number_int { \value{exgroupcounter} } + \subsection*{\Alph{exgroupcounter}组} + \begin{enumerate} + } + +\cs_new_protected:Npn \lalu_ans_end_group: + { + \end{enumerate} + \seq_if_empty:NF \g_lalu_ans_group_seq + { + \int_gincr:N \g_lalu_ans_emitted_group_int + \tl_set:Nx \l_lalu_ans_group_id_tl + { answer-group:\int_use:N \g_lalu_ans_emitted_group_int } + \lalu_ans_write_marker:n + { + group-begin~id=\l_lalu_ans_group_id_tl + \c_space_tl unit=\l_lalu_ans_unit_id_tl + \c_space_tl number=\int_use:N \g_lalu_ans_group_number_int + \c_space_tl items=\int_eval:n { \seq_count:N \g_lalu_ans_group_seq } + } + \iow_now:Nx \g_lalu_ans_iow + { + \exp_not:N \LALUAnswerGroupBegin + { \exp_not:V \l_lalu_ans_group_id_tl } + { \int_use:N \g_lalu_ans_group_number_int } + { \int_eval:n { \seq_count:N \g_lalu_ans_group_seq } } + } + \seq_map_inline:Nn \g_lalu_ans_group_seq {##1} + \lalu_ans_write_marker:n + { + group-end~id=\l_lalu_ans_group_id_tl + \c_space_tl items=\int_eval:n { \seq_count:N \g_lalu_ans_group_seq } + } + \iow_now:Nx \g_lalu_ans_iow + { + \exp_not:N \LALUAnswerGroupEnd + { \exp_not:V \l_lalu_ans_group_id_tl } + { \int_eval:n { \seq_count:N \g_lalu_ans_group_seq } } + } + } + \seq_gclear:N \g_lalu_ans_group_seq + \bool_set_false:N \l_lalu_ans_in_group_bool + } + +\NewDocumentEnvironment{exgroup} { o } + { \lalu_ans_begin_group:n {#1} } + { \lalu_ans_end_group: } + +\cs_new_protected:Npn \lalu_ans_store_answer:nn #1#2 + { + \bool_if:NF \l_lalu_ans_in_group_bool + { \msg_fatal:nnn { lalu-answer } { invalid-nesting } { answer } } + \int_gincr:N \g_lalu_ans_total_item_int + \tl_set:Nn \l_lalu_ans_status_tl { answered } + \tl_if_blank:nF {#1} { \keys_set:nn { lalu / answer } {#1} } + % xparse represents a visually empty long-environment body as one or more + % \par tokens. Ignore those only for status classification; the original + % token list is still written unchanged for non-empty inline answers. + \tl_set:Nn \l_lalu_ans_body_probe_tl {#2} + \tl_remove_all:Nn \l_lalu_ans_body_probe_tl { \par } + \tl_if_blank:VT \l_lalu_ans_body_probe_tl + { + \tl_if_eq:NnT \l_lalu_ans_status_tl { answered } + { \tl_set:Nn \l_lalu_ans_status_tl { omitted } } + } + \str_case:VnF \l_lalu_ans_status_tl + { + { answered } { \int_gincr:N \g_lalu_ans_answered_int } + { todo } { \int_gincr:N \g_lalu_ans_todo_int } + { omitted } { \int_gincr:N \g_lalu_ans_omitted_int } + } + { + \msg_fatal:nnn { lalu-answer } { invalid-status } + { \l_lalu_ans_status_tl } + } + \seq_gput_right:Nx \g_lalu_ans_group_seq + { + \exp_not:N \lalu_ans_write_item:nnnn + { answer-item:\int_use:N \g_lalu_ans_total_item_int } + { \int_eval:n { \value{enumi} } } + { \exp_not:V \l_lalu_ans_status_tl } + { \exp_not:n {#2} } + } + } + +\NewDocumentEnvironment{answer} { O{} +b } + { \lalu_ans_store_answer:nn {#1}{#2} } + { } + +\NewDocumentCommand{\exquote}{om} + { + \vspace{2ex} + {\sffamily\kaishu #2} + \IfValueT{#1} + { + \begin{flushright} + \sffamily\kaishu ——#1 + \end{flushright} + } + } + \iow_new:N \g_lalu_ans_iow -\iow_open:Nn \g_lalu_ans_iow { LALU-ans-contents.tex } -\AtEndDocument{ -\iow_close:N \g_lalu_ans_iow -} +\tl_new:N \g_lalu_ans_manifest_path_tl +\cs_generate_variant:Nn \iow_open:Nn { NV } +\cs_if_exist:NTF \LALUAnswerManifestTempPath + { \tl_gset:Ne \g_lalu_ans_manifest_path_tl { \LALUAnswerManifestTempPath } } + { + \tl_gset:Nx \g_lalu_ans_manifest_path_tl + { \c_sys_jobname_str .answer-manifest.pending.tex } + } +\iow_open:NV \g_lalu_ans_iow \g_lalu_ans_manifest_path_tl +\iow_now:Nx \g_lalu_ans_iow + { + \c_percent_str \c_space_tl LALU-ANSWER-MANIFEST + \c_space_tl schema=2 \c_space_tl job=\c_sys_jobname_str + } +\iow_now:Nx \g_lalu_ans_iow + { \exp_not:N \LALUAnswerManifestBegin {2}{\c_sys_jobname_str} } +\AtEndDocument + { + \iow_now:Nx \g_lalu_ans_iow + { + \exp_not:N \LALUAnswerManifestEnd + { \int_use:N \g_lalu_ans_total_unit_int } + { \int_use:N \g_lalu_ans_total_group_int } + { \int_use:N \g_lalu_ans_emitted_group_int } + { \int_use:N \g_lalu_ans_total_item_int } + { \int_use:N \g_lalu_ans_answered_int } + { \int_use:N \g_lalu_ans_todo_int } + { \int_use:N \g_lalu_ans_omitted_int } + } + \iow_now:Nx \g_lalu_ans_iow + { + \c_percent_str \c_space_tl LALU-ANSWER-MANIFEST-END + \c_space_tl total-units=\int_use:N \g_lalu_ans_total_unit_int + \c_space_tl total-groups=\int_use:N \g_lalu_ans_total_group_int + \c_space_tl emitted-groups=\int_use:N \g_lalu_ans_emitted_group_int + \c_space_tl total-items=\int_use:N \g_lalu_ans_total_item_int + \c_space_tl answered=\int_use:N \g_lalu_ans_answered_int + \c_space_tl todo=\int_use:N \g_lalu_ans_todo_int + \c_space_tl omitted=\int_use:N \g_lalu_ans_omitted_int + } + \iow_close:N \g_lalu_ans_iow + \iow_term:x + { + LALU~answer~manifest:~units~ + \int_use:N \g_lalu_ans_total_unit_int,~groups~ + \int_use:N \g_lalu_ans_emitted_group_int / + \int_use:N \g_lalu_ans_total_group_int,~answered~ + \int_use:N \g_lalu_ans_answered_int,~todo~ + \int_use:N \g_lalu_ans_todo_int,~empty~ + \int_use:N \g_lalu_ans_omitted_int. + } + } \ExplSyntaxOff -\NewDocumentCommand{\LUchapter}{m}{% -\LUgroupsancheck -\begingroup -\LUchapsancheck -\addtocounter{chapter}{-1} -\refstepcounter{LUchapter} -\renewcommand*{\thechapter}{\arabic{chapter}\LUgreek{LUgreekchap}} -\renewcommand*{\theHchapter}{LU.\arabic{LUchapter}} \ctexset{ - chapter={format={\centering\Huge\bfseries},name={未竟专题,},number={\zhnumber{\arabic{LUchapter}}}}, -} -\csname @std@chapter\endcsname{#1} -\expandafter\xdef\csname @exist@LUchapter@\arabic{chapter}\endcsname{\null} -\expandafter\xdef\csname @exist@LUchapter@\arabic{chapter}.\arabic{LUgreekchap}\endcsname{\null} -} - -\ctexset{ - chapter={format={\centering\Huge\bfseries},name={第,讲},number=\arabic{chapter}}, + chapter={format={\centering\Huge\bfseries},name={第,讲},number=\arabic{LALUnormalchapter}}, section={format={\raggedright\Large\bfseries},name={,},number={\thechapter.\arabic{section}}}, subsection={format={\raggedright\large\bfseries},name={,},number={\thesection.\arabic{subsection}}}, subsubsection={format={\raggedright\normalsize\bfseries},name={,},number={\thesubsection.\arabic{subsubsection}}}, @@ -264,13 +916,6 @@ \input{./专题/25 线性代数与最优化问题.tex} \input{./其它/未竟专题14 范畴论视角下的线性代数.tex} -\LUgroupsancheck - -\makeatletter -\let\chapter\@std@chapter -\let\@std@chapter\relax -\makeatother - \backmatter {\small \printindex From d72db2318831f642e35425c036099acf5be902d7 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Wed, 16 Sep 2026 17:03:59 +0800 Subject: [PATCH 12/27] docs(template): finalize refactor compatibility record Record the accepted author-facing boundary, supporting-structure breaking changes, source migration locations, implemented chapter and answer designs, staged publication flow, and the completed MacTeX/PDF regression results. --- docs/template-refactor-plan.md | 279 +++++++++++++++++---------------- 1 file changed, 144 insertions(+), 135 deletions(-) diff --git a/docs/template-refactor-plan.md b/docs/template-refactor-plan.md index a267869..5cf45cf 100644 --- a/docs/template-refactor-plan.md +++ b/docs/template-refactor-plan.md @@ -1,6 +1,21 @@ # LALU 模板重构方案 -> 状态:设计稿。本文只描述后续实现方案,不改变当前任何 TeX、索引或构建行为。 +> 状态:本轮实施与迁移记录。本文同时记录已落地的内部重构和明确延后的提案;“后续提案”不构成本轮验收要求。 + +## 本轮范围与兼容边界 + +本轮以仓库正文(`讲义/专题/`、`讲义/历年卷/`、`讲义/其它/`)和最终 PDF 为兼容边界。`LALUbook.cls`、主文件 `讲义/线性代数荣誉课辅学讲义.tex`、答案模板 `讲义/LALU-answers.tex` 中仅用于支撑实现的 counter、helper、marker、生成文件协议和 href anchor **不是兼容接口**;只要正文无需改写、既有标签/引用语义和 PDF 可见行为不变,它们可以直接删除、改名或换成新 schema,不设过渡包装器。 + +本轮状态如下: + +| 子系统 | 本轮状态 | 兼容要求 | +| --- | --- | --- | +| 定理与证明 | 已切换到集中声明的 tcolorbox theorem + amsthm proof;已删除 proof/solution 未使用的第二个原始 tcolorbox 选项 | 六类定理的双参数接口、标签前缀、分别计数、proof/solution 第一可选说明及可见 QED/盒子行为不变 | +| 正文/未竟专题章节状态 | 已切换到显式状态机并完成 fixture 与整书回归 | `\chapter`、`\LUchapter`、中文标题编号、“上一正文章节 + 希腊字母”的引用编号、目录和 PDF 可见版式不变;旧 counter alias、开放分组和 marker 无需保留 | +| 答案生成与答案册 reader | 已切换到带版本的语义 manifest、独立 reader 和 staging 发布 | 正文继续使用普通 `answer` 环境;答案项、空答案、无答案单元及其既有分页在本轮保持可见行为不变;旧生成文件语法与路径无需兼容 | +| 稳定题目 ID、file payload、空单元省略策略及其余条件性变化 | 延后 review | 本轮不要求“在前面插题后已有 ID 不变”,不引入需要正文改写的新答案接口,也不启用其他条件性 breaking change | + +这里的“正文无需改写”只约束作者源文件的既有公共调用。干净重编译、删除旧辅助文件、更新 Makefile 以及同步修改上述三份 supporting structure 都属于本轮正常迁移,不算正文 breaking change。 ## 目标与不变量 @@ -12,8 +27,8 @@ - 未竟专题继续使用 `\LUchapter{标题}`;标题编号显示为“未竟专题 X”,其中 `X` 是未竟专题的全局中文序号。 - 未竟专题的数字引用继续由“上一普通正文编号 + 希腊字母”组成。例如第一讲后的前两个未竟专题分别引用为 `1ε`、`1δ`。 - 未竟专题内的节、公式、图表和定理继续沿用该数字引用,例如 `1ε.2`。 -- 正文和答案册中的既有标签键、目录层级及主要版式应保持兼容;确需改变的辅助文件格式必须通过干净重编译迁移。 -- 所有 Hyperref destination 必须唯一、稳定、只含适合机器处理的 ASCII 内容;显示编号不得直接充当锚点编号。 +- 正文和答案册中的既有标签键、引用结果、目录层级、内容、顺序及分页应保持;内部辅助文件格式可改变,但必须通过干净重编译得到一致产物。 +- 所有 Hyperref destination 必须在单次产物中唯一、确定且只含适合机器处理的 ASCII 内容;显示编号不得直接充当可能冲突的锚点编号。旧 PDF 的原始 href 名称不属于本轮兼容承诺。 以下三个概念应始终分离: @@ -21,7 +36,7 @@ | --- | --- | --- | | 标题显示编号 | `未竟专题十四` | 章标题、答案册单元标题、目录章条目 | | 数字引用编号 | `23ε` | `\ref`、节/公式/定理的父编号 | -| 机器锚点编号 | `chapter.LU.14` | Hyperref destination、外部 PDF 链接 | +| 机器锚点编号 | `chapter.LU.14` | Hyperref destination(内部实现,可改名,不承诺旧 PDF 深链接) | ## 一、正文、未竟专题与答案册单元 @@ -41,7 +56,7 @@ | `unnumbered-serial` | 实际无编号顶层标题的内部流水号,仅用于必要的唯一锚点 | `3` | | `title` | 完整标题,保持未展开 token | `范畴论视角下的线性代数` | | `short-title` | 页眉、目录所用短标题;缺省时等于完整标题 | 同上 | -| `unit-id` | 稳定的逻辑单元标识 | `lu:14` | +| `unit-id` | 当前 manifest 内唯一的逻辑单元标识 | `lu:14` | 实现上可使用 expl3 的全局 token list、integer 和 boolean;对外只暴露查询命令,不允许正文直接改内部变量。至少提供以下语义接口: @@ -61,8 +76,8 @@ | --- | --- | --- | | 标准 `chapter`(即 `c@chapter`) | 所有**有编号顶层单元**的结构流水号;驱动标准 reset 链和 package hook | 否 | | 新增 `LALUnormalchapter` | 只在普通正文开始时递增,表示“第 N 讲”中的 `N` | 是 | -| `LUchapter` | 只在未竟专题开始时递增,表示“未竟专题 X”中的全局序号 | 是 | -| `LUgreekchap` | 同一普通正文之后的未竟专题 slot | 通过希腊字母显示 | +| 内部 unfinished serial | 只在未竟专题开始时递增,表示“未竟专题 X”中的全局序号 | 是 | +| 内部 unfinished slot | 同一普通正文之后的未竟专题次序 | 通过希腊字母显示 | | 新增 `LALUappendixchapter` | 附录显示编号 `A`、`B`…… | 仅 appendix 模式 | | 内部 `LALUunnumberedunit` | 每个实际无编号顶层标题的锚点流水号;阶段命令本身不递增 | 否 | @@ -80,9 +95,9 @@ 未竟专题转换流程为: 1. 要求当前处于 `mainmatter`、已经存在普通正文,并且标准 chapter 判定本次会编号;任一条件不满足都必须在修改任何计数器或样式前给出明确的 package error,而不是生成 `0ε` 或一个半初始化单元。 -2. 将当前 `LALUnormalchapter` 快照为 `normal-base`,设置 `kind=unfinished`,并分别递增全局 `LUchapter` 与当前普通章的 `LUgreekchap`。普通章入口已经把 slot 归零,因此这里不再覆盖 `normal-base` 后做含义不清的“是否同章”比较。 +2. 将当前普通正文计数快照为 `normal-base`,设置 `kind=unfinished`,并分别递增内部的全局 unfinished serial 与当前普通章的 unfinished slot。普通章入口已经把 slot 归零,因此这里不再覆盖 `normal-base` 后做含义不清的“是否同章”比较。 3. 调用同一个最终标准 `\chapter`;结构 `c@chapter` 再递增一次并执行全部标准 reset/hook。 -4. `\thechapter` 在本次调用及其正文期间返回保存的 `normal-base + Greek(slot)`;章标题的中文序号则来自 `LUchapter`。 +4. `\thechapter` 在本次调用及其正文期间返回保存的 `normal-base + Greek(slot)`;章标题的中文序号则来自内部 unfinished serial。 `\chapter*` 结束当前编号单元但不清除最近普通章及其 LU slot。因而随后在 `mainmatter` 中再次调用 `\LUchapter` 是允许的,并继续使用下一个 slot;例如 `chapter A -> LU B -> chapter* -> LU C` 的两个 LU 引用仍依次为 `1ε`、`1δ`。如果希望星号章成为禁止后续 LU 的更强边界,应另设显式命令,不能通过重置 slot 得到可能重复的编号。 @@ -175,14 +190,13 @@ Hyperref 锚点规则如下: | 未竟专题内 section | 从唯一章锚点派生 | `section.LU.14.2` | | 附录 | `APP.` | `chapter.APP.A` | | 实际无编号顶层标题 | `UNNUMBERED.`,仅作内部唯一性用途 | `chapter.UNNUMBERED.3` | -| 答案单元 | `answer.normal.` 或 `answer.LU.` | `LALUanswerunit.answer.LU.14` | -| 习题组 | 包含答案单元 ID 和组次序 | `LALUexgroup.lu.14.C` | +| 答案单元 | 使用 manifest 内的顺序 unit ID | `LALUanswerunit.answer.answer-unit:29` | 锚点表示必须完全可展开并只含 ASCII。`\theHchapter` 同样按 `kind` 分派:普通章返回普通显示编号,未竟专题返回 `LU.`,附录返回 `APP.`,无编号状态在确需 chapter 型锚点时返回 `UNNUMBERED.`。它不能直接使用结构 `c@chapter`,否则在插入未竟专题后会改变普通章既有外链。`LUchapter` 只是标题流水号时使用 `\stepcounter`;真正承载紧随其后 `\label` 的标准 `chapter` 才使用 `\refstepcounter`,避免创建无用的第二锚点。 标准 `\chapter*` 若已由 Hyperref 使用自己的 `chapter*.` 命名空间,就继续沿用该机制;`UNNUMBERED.` 只是其他包确实展开 `\theHchapter` 时的确定性后备值,不能再额外创建一个与标准星号章重叠的 destination。 -为了最大限度兼容已有 PDF 外链,新实现应优先保留当前未竟专题锚点 `chapter.LU.`。普通章节锚点保持标准形式。若任何锚点必须改变,应在发布说明中列出,并考虑在一个迁移版本中添加旧 destination 别名。 +机器锚点属于 supporting structure,本轮不为旧 PDF 深链接保留 destination 别名。实现可以继续使用 `chapter.LU.`,也可以改用其他唯一 ASCII 命名;验收只检查正文标签键和 `\ref`/`\nameref`/`\autoref`/`\cref` 的语义、链接目标页面以及无重复 destination。引用锚点改名后必须干净重建 `.aux`、`.toc`、`.out` 和答案册的 `xr` 数据。 默认引用语义保持如下: @@ -229,19 +243,17 @@ Hyperref 锚点规则如下: 可使用 `\section*` 加一次受控的目录/书签写入来保持现有视觉层级;公式和定理通过专用 `LALUanswerunit` 建立 reset 关系。这样答案册后半部分的普通 `\chapter`、`\section` 完全不受生成答案部分影响。 -新答案清单必须携带完整章节记录,不能让答案册根据“上一个 section 是什么”猜测 `normal-base` 或希腊 slot。`\LUsection` 保留为过渡包装器,仅用于读取旧的已生成文件,并给出一次性弃用警告;干净构建生成的新文件不得再输出 `\setcounter{LUsection}{...}` 或 `\LUsection`。 +新答案清单必须携带渲染所需的章节语义快照,不能让答案册根据“上一个 section 是什么”猜测 `normal-base` 或希腊 slot。`\LUsection`、对应 counter 和旧 marker 只存在于 supporting structure,可随新 reader 一次删除;干净构建生成的新文件只输出 `\LALUAnswerUnit` 等当前 schema 事件。 -### 1.6 兼容迁移 +### 1.6 内部替换范围与源码影响 -迁移期建议遵循以下策略: +本轮不为 supporting structure 设置过渡期,迁移策略如下: -- `\LUchapter{...}` 原调用不需要修改;新增短标题是向后兼容扩展。 -- 暂时保留 `LUchapter`、`LUgreekchap`、`LUsection` 计数器名字,并由新状态模型同步其值;新增的 `LALUnormalchapter` 是普通正文显示编号的权威来源。 -- `\LUgroupsancheck` 在一个过渡版本中保留为空操作并发出弃用提示,使主文件尾部或缓存生成文件不会立即报未定义;确认仓库内调用移除后再删除。 -- 旧的 `\@exist@LUchapter@...`、`\@exist@LUsection@...` 不再作为真值来源。若外部文档确有探测,可在过渡版只生成兼容标记,但内部逻辑不得读取它们。 -- 保留已有标签键和 `chapter.LU.` destination;所有 `.aux`、`.toc`、`.out`、`.fdb_latexmk` 应在切换实现时统一清理并完整重编译。 -- 新旧答案生成器先并行输出到不同构建目录,比较单元顺序、标题、组号、题号和答案正文;一致后才切换 `LALU-answers.tex` 的输入。 -- 旧 `LALU-ans-contents.tex` 是可丢弃构建产物,不承诺由新 reader 永久支持。过渡包装器只用于平滑升级,不能成为新状态模型的长期分支。 +- `\chapter{...}`、`\LUchapter{...}` 以及现有可选短标题等作者接口不修改。 +- `LUchapter`、`LUgreekchap`、`LUsection` counter alias、`\LUgroupsancheck`、`\@exist@LUchapter@...`、`\@exist@LUsection@...` 和跨章开放分组可直接删除,不同步、不警告,也不保留空操作包装器。 +- 旧 `LALU-ans-contents.tex` 及其中的 `\LUsection`/手工 counter 协议是可丢弃构建产物;新 reader 不读取它。Makefile 负责先生成并验证当前 manifest,再编译答案册。 +- 标签键和引用可见结果必须保持;原始 href 名称、内部 counter 值和生成文件事件名可以变化。切换后统一清理 `.aux`、`.toc`、`.out`、`.fdb_latexmk` 与旧答案产物并完整重编译。 +- 用旧、新 PDF 的页面数、抽取文本、目录/outline、代表页面和链接目标作回归比较;这种对比是验证可见行为,不是要求同时维护两套生产 reader。 这一首选模型会有意改变裸 `\value{chapter}`、`\arabic{chapter}` 和直接读取 `c@chapter` 的内部语义:它们返回的是“第几个有编号顶层单元”,不再等于普通正文的显示编号。迁移前必须全仓库扫描这些用法,并根据意图替换为语义 API: @@ -252,7 +264,7 @@ Hyperref 锚点规则如下: 当前仓库扫描结果是:`\value{chapter}`、`\arabic{chapter}`、`\setcounter{chapter}`、`\addtocounter{chapter}` 或 `c@chapter` 这类 raw 模式只出现在主模板现有章节逻辑中;答案模板还会重定义 `\thechapter` 并根据章节状态组织答案,因此两份模板都必须迁移,而正文内容没有直接依赖这些 raw 模式。迁移后应对全部手写源码启用静态检查。检查只允许统一章节模块中为 reset、checkpoint 或底层断言而登记的少量结构用途白名单,并排除 `.aux` 等生成文件;不能仅检查正文而放过模板内部继续把 raw chapter 当显示号的代码。 -如果实际外部使用者证明必须保持“裸 `chapter` 值等于普通正文显示编号”,才启用次选兼容方案:在一个严格封装且带断言的适配器中,未竟专题调用前把 `chapter` 临时减一,再交给标准 `\chapter` 加回。该方案仍优于跨章开放分组,但会复用结构 counter、增加异常恢复复杂度,也使 hook 观察到的计数值依赖调用阶段;它不应作为默认设计。 +不再提供“裸 `chapter` 值等于普通正文显示编号”的次选兼容方案。仓库扫描表明这种读取只属于 supporting structure;继续临时回退 `chapter` 会重新引入重复锚点和 hook 状态歧义。若未来新增作者级查询需求,应暴露语义 API,而不是把结构 counter 重新定义成公共接口。 ### 1.7 章节与链接测试矩阵 @@ -284,7 +296,7 @@ Hyperref 锚点规则如下: | 答案普通单元 -> LU 单元 -> 普通单元 | 显示与正文映射一致 | 后续历年卷 section/公式不受污染 | | 全仓手写源码静态扫描 | raw chapter 只剩登记的结构用途 | 模板和正文均无把 `value/arabic/setcounter/addtocounter{chapter}` 或 `c@chapter` 当显示号的调用;生成文件被排除 | -测试至少在受支持的最旧 TeX Live 与当前 TeX Live 上使用 XeLaTeX 执行。若 LuaLaTeX 不在支持范围,应在类加载时明确拒绝,而不是让字体、索引或书签在中途失败。日志检查必须把以下内容视为失败: +本轮只要求使用本机 MacTeX 的 XeLaTeX 完成最小 fixture 和整书构建,不额外验证 Docker、其他 TeX Live 版本或 LuaLaTeX。日志检查必须把以下内容视为失败: - `destination with the same identifier`; - 未定义引用或多轮后仍要求 rerun; @@ -296,30 +308,30 @@ Hyperref 锚点规则如下: ### 2.1 单一源数据与记录模型 -答案的权威源仍是作者编辑的源码;任何生成的 `.tex` 或 manifest 都是可删除、不可手工编辑的构建产物。每条答案记录至少包含: +答案的权威源仍是作者编辑的源码;任何生成的 `.tex` 或 manifest 都是可删除、不可手工编辑的构建产物。本轮 manifest 包含: -- schema 版本; -- 主文档身份和源文件哈希; -- 当前章节记录的完整快照; +- schema 版本和构建 job 身份; +- 渲染所需的章节语义快照(kind、normal base、unfinished serial/slot、unit ID 和标题); - 习题组序号及显示标签; -- 题目在组内的逻辑序号、显示标签和稳定 ID; +- 题目在组内的逻辑序号、显示标签和本次生成中唯一的内部 ID; - 答案状态:`answered`、`todo` 或 `omitted`; -- 正文载荷类型:`inline-token` 或 `file`; -- 可选源文件与行号,仅供诊断。 +- 未展开的 inline-token 正文; +- 记录数和结束 sentinel,供发布脚本与 reader 校验。 -稳定 ID 的推荐缺省形式为 `//`,例如 `lu:14/C/2`。作者可显式指定更稳定的语义 ID;一旦发布,不应因插入前一题而改变已有 ID。 +本轮 schema 2 使用 manifest 内的连续顺序 ID:`answer-unit:N`、`answer-group:N`和 `answer-item:N`。它们只承诺在单份 manifest 中唯一且与事件顺序一致,因而同章多个 `exercise`、重复显示组号或同题多个 `answer` 也不会制造内部冲突。在前面插入题目后保持已有 ID 不变的“发布级稳定 ID”明确不在本轮范围,也不新增 `\exitem[id=...]` 等作者接口。该目标留待后续单独设计。 生成文件应是语义事件流,而不是预先写死 `\section`、`\setcounter`、`\subsection*` 等版式命令。例如 manifest 只允许调用: ```tex \LALUAnswerManifestBegin{...} \LALUAnswerUnit{...} -\LALUAnswerGroup{...} +\LALUAnswerGroupBegin{...} \LALUAnswerItem{...}{...} +\LALUAnswerGroupEnd{...} \LALUAnswerManifestEnd{...} ``` -答案册 reader 负责把记录渲染成标题和列表。这样正文编号策略改变时无需重新设计文本序列化格式。 +每个事件前还有一条受限 ASCII marker,供发布脚本在不解析任意 TeX 正文的前提下校验顺序、ID 和计数。答案册 reader 再使用独立状态机校验同一事件流并渲染标题和列表。每个 item 单独占一个物理行,答案 body 作为不透明、未展开 token 载荷传递。 ### 2.2 编号与普通/未竟映射 @@ -333,18 +345,17 @@ Hyperref 锚点规则如下: - 嵌套 enumerate 只影响答案正文内部编号,不得改变所关联的外层题号。 - 答案册使用 manifest 中的 `normal-base`、`unfinished-serial` 和 `unfinished-slot` 渲染单元,绝不根据前一个已输出答案单元推算。 -迁移初期可自动生成稳定 ID 并记录告警;中长期建议提供 `\exitem[id=...]` 或等价接口,让题目和答案的关联显式化。 +本轮按 manifest 事件顺序自动生成内部 ID,显示单元/组/题号另作元数据保存,不对正文发迁移告警。显式语义 ID 若将来确有跨版本引用需求,再作为会改变作者写法的独立提案 review。 -### 2.3 token 保真与 verbatim +### 2.3 inline token 边界 -单一接口不能同时可靠地捕获任意 TeX token 和 verbatim 内容,因此应明确提供两种载荷: +本轮只保留现有 `\begin{answer}...\end{answer}` 的 inline-token 写法。答案可包含用于普通文字和数学排版的 LaTeX token、平衡花括号、段落和常规宏;生成器以未展开 token 保存,禁止使用 `\tl_to_str:n`、`x` 型完全展开或 `\scantokens`。 -1. `inline-token`:兼容当前 `\begin{answer}...\end{answer}`。支持平衡花括号、段落和普通宏;以未展开 token 保存,禁止使用 `\tl_to_str:n`、`x` 型完全展开或 `\scantokens`。该模式明确不支持 verbatim、minted 及依赖特殊 catcode 的内容。 -2. `file`:答案正文保存在独立 `.tex` 片段中,manifest 只记录路径,答案册在正常输入阶段 `\input` 它。verbatim、listing、复杂 catcode 和很长答案必须使用该模式。 +`answer` 明确不支持 verbatim、minted、listing 或依赖特殊 catcode 的内容,本轮测试也不覆盖这些输入。这不是对当前正文的兼容性中断:仓库扫描没有此类答案,用户已确认可以把 inline answer 视为普通 LaTeX token。不要为不存在的特殊载荷编写外部正则解析器。 -不建议编写一个外部正则表达式解析器去“理解”任意 TeX 并复制 `answer` 环境:嵌套环境、注释、条件分支和 verbatim 会使该方案不可可靠。迁移脚本可以机械地拆分已知格式,但拆分结果必须经过 TeX 编译和内容对比。 +把答案人工拆到独立文件或新增 `\answerfile`/file payload 会改变作者写法,当前不实施。若以后确有需求,应先 review 是引入显式 file 接口,还是用其他方式从同一源代码生成两份 PDF;不能把特殊 catcode 支持悄悄塞进当前 inline 捕获器。 -标题、题号等元数据与答案正文分开序列化。元数据需要 PDF 字符串时单独转换;不得为了生成元数据而展开正文。生成文件应保留合理换行,避免每条长答案变成单个超长输入行。 +标题、题号等元数据与答案正文分开序列化。元数据需要 PDF 字符串时单独转换;不得为了生成元数据而展开正文。schema 2 刻意让每个 item 的命令及不透明 body 各占一个完整物理行,发布器只校验该行的固定外层结构,不展开或按行解析 body。 需要额外验证答案正文中的以下对象: @@ -352,21 +363,19 @@ Hyperref 锚点规则如下: - 浮动体与图片相对路径; - 脚注、索引项、局部宏; - display math 末尾的 QED; -- verbatim 文件载荷。 为避免答案册自身标签与 `\externaldocument` 导入的正文标签相撞,建议给外部标签添加固定前缀并提供 `\mainref`/`\mainnameref` 辅助命令;是否启用应先以标签清单验证兼容性。 ### 2.4 空答案与占位语义 -“没有 `answer` 环境”和“显式存在一个空 `answer` 环境”都不应默认制造答案册空白页。建议规则为: +本轮以 PDF 可见行为不变为先,继续区分“没有 `answer` 环境”和“显式存在一个空 `answer` 环境”,并保留旧答案册已经排出的空答案项、无答案单元、目录条目和单元间 `\clearpage`: -- 单元中没有 `answered` 或显式 `todo` 记录时,不输出该答案单元、目录条目或 `\clearpage`。 -- 组中没有可见记录时,不输出组标题。 -- 纯空白的兼容 `answer` 正文默认为 `omitted`,构建日志报告数量。 -- 作者希望保留占位时必须显式写 `status=todo`;reader 统一显示“暂无答案”或配置的占位文字。 -- 可提供仅供编辑审校的 `include-empty-units=true`,正式发布构建固定为 false。 +- 每个 `exercise` 都输出答案单元记录,即使其中没有答案;reader 仍按原顺序排标题和分页。 +- 含至少一个 `answer` 的组继续输出组标题;空的 `answer` 记为 `omitted`,但 reader 仍排出对应的空编号项。 +- 非空答案记为 `answered`;内部 schema 可识别显式 `todo`,但本轮不要求正文迁移到 todo 写法。 +- 日志报告总单元、组、题目、answered、todo 和 omitted 数量,以便和基线核对。 -每次生成结束应报告:章节单元数、习题组数、题目数、已答数、todo 数、空答案数和被省略单元数。异常下降可由 CI 阈值发现。 +“省略没有可见答案的单元”“删除空答案项”或引入正式 todo 政策都会改变现有答案 PDF,均延后为需要单独 review 的条件性变化。 ### 2.5 原子生成与失败恢复 @@ -375,32 +384,24 @@ TeX 主进程不应直接截断最后一个可用的正式答案文件。建议 1. 在目标构建目录创建同一文件系统上的唯一临时目录或临时文件。 2. 主文档只向临时目标写 manifest 和 inline body。 3. 写入 schema header、记录总数以及结束 sentinel;关闭所有 stream。 -4. 辅助程序验证退出状态、sentinel、记录计数、所有 file 载荷和依赖均存在,并可选择执行一次 reader 语法检查。 -5. 验证成功后,以原子 rename 替换当前 manifest;验证失败则保留上一份成功产物并删除或隔离临时文件。 +4. 发布脚本先校验 schema/job、Begin/End 唯一性、marker 与 reader 命令对应、事件顺序、ID 连续唯一性和 unit/group/item/status 汇总计数。 +5. 答案册直接读取这份临时 manifest,并在唯一 staging 目录中完整编译;reader 独立校验 schema/job、状态转移、ID 和实际计数,同时让 TeX 验证不透明答案 body 的可编译性。主 PDF 同样暂存在 ASCII jobname 输出中。 +6. 只有主 PDF、答案 PDF 和 manifest 全部生成并验证成功后,Makefile 才集中将三者发布到正式路径;任一编译或发布前校验失败都保留上一份成功产物。manifest 使用同目录原子 rename,PDF 只在所有高风险步骤成功后才移入正式文件名。 -纯 TeX 无法跨平台可靠地完成最终原子 rename;该动作应放在 latexmk rule 或小型构建脚本中,而不是依赖 `\write18`。manifest 应带 schema 版本和主源码指纹,答案册发现不完整、过期或版本不兼容时必须失败,不能静默排版旧答案。 +纯 TeX 无法跨平台可靠地完成最终发布;临时路径、集中发布和 manifest 的同目录原子 rename 都由 Makefile 与小型发布脚本负责,不依赖 `\write18`。本轮 manifest 带 schema 版本和 job 身份;答案册发现缺失、不完整、事件顺序错误或版本/job 不匹配时必须失败,不能静默排版错误答案。源文件指纹与更强的过期检测可后续增加,不作为本轮完成条件。 ### 2.6 并发、jobname 与构建目录 -固定文件名 `LALU-ans-contents.tex` 不再作为内部协议。输出身份至少由以下内容组成: - -- 显式 ASCII jobname; -- 主文件规范化路径的短哈希,防止两个同名工程相撞; -- 会影响答案内容的构建变体; -- schema 版本。 - -推荐目录形如: +固定文件名 `LALU-ans-contents.tex` 不再作为内部协议。本轮 Makefile 使用显式 ASCII jobname 和独立构建目录,形如: ```text -build/answers/LALU-/ - manifest.tex - bodies/ - generation.json +build/answers/ + LALU.manifest.tex ``` -不同 jobname 或工程可并行写不同目录。同一身份的并发构建使用每进程唯一临时目录,并通过锁或 compare-and-swap 决定谁发布最终 manifest;绝不能共同写同一个 open stream。答案册由构建命令显式传入 manifest 路径,源码中不硬编码当前工作目录。 +主文档写入与正式 manifest 同目录的唯一临时文件;构建命令把该临时路径和 job 身份显式传给答案册,并把答案 PDF 编译到唯一 staging 目录。不同 jobname/工程隔离、同一目标并发锁、source hash 和变体身份属于后续增强,本轮不宣称支持并行写同一个发布目标。 -`make clean` 只删除已知构建目录,不删除作者维护的 file 载荷。生成内容不写回源码目录,也不参与版本控制。 +`make clean` 删除已知答案构建目录和遗留生成文件;作者维护的正文不受影响。新生成内容不参与版本控制。 ### 2.7 答案抽取器测试 @@ -408,16 +409,14 @@ build/answers/LALU-/ - 普通章 A/B/C 三组,部分题有答案; - 普通章后的两个连续未竟专题,验证 `Nε`、`Nδ` 与中文全局序号; -- 没有任何答案的 exercise、空组、空 answer、显式 todo; +- 没有任何答案的 exercise、空组、空 answer、显式 todo;验证既有空项、单元和分页仍保留; - 自定义起始组号和超过 9 的题号; - 答案含多段、嵌套 enumerate、宏参数符号、注释、Unicode、标签和浮动体; -- file 载荷含 verbatim/listing; - 主编译在写到一半时故意失败,旧 manifest 仍可用; -- 两个不同 jobname 并行构建以及同一 jobname 的竞争构建; -- manifest 缺尾标、记录计数错误、源哈希过期、file 载荷缺失时 reader 明确失败; -- 新旧生成器的记录顺序和可见答案逐条对比。 +- manifest 缺尾标、记录计数错误、schema/job 不匹配时发布器或 reader 明确失败; +- 新旧 PDF 的单元顺序、标题、组号、题号、答案正文、空项和分页逐项对比。 -## 三、定理、证明与引用框架 +## 三、定理、证明与引用框架(本轮已实施) ### 3.1 职责划分 @@ -468,7 +467,7 @@ build/answers/LALU-/ `solution` 复用同一视觉框架,但默认不显示 QED,以保持现有行为。若未来需要 QED,使用显式选项,不通过正文手写 `\square` 推测。 -当前 `proof`/`solution` 的第二个可选参数可暂时作为原始 tcolorbox 选项透传;应扫描实际调用,若未使用则标记弃用,改由受控 key 列表代替任意样式注入。 +仓库扫描未发现 `proof`/`solution` 的第二个原始 tcolorbox 选项用法;它只属于模板 supporting structure,已直接删除,不保留弃用包装器。第一可选说明仍是作者接口,继续支持 `\begin{proof}[说明]` 和 `\begin{solution}[说明]`。 ### 3.4 `autoref` 与 `cleveref` @@ -482,18 +481,16 @@ build/answers/LALU-/ 每个 theorem counter 的 `\theH...` 必须从统一的唯一 chapter anchor 派生。正常章和连续未竟专题中,同样显示为 `.1` 的定理也必须拥有不同 destination。 -### 3.5 定理框架迁移兼容 +### 3.5 已实施的迁移 -迁移步骤如下: +本轮实施结果如下: -1. 统计所有环境调用形式、可选参数、空标题、空标签和手写 `\qedhere`/`\square`;建立代表性 fixture。 -2. 在测试分支中用 `amsthm` 替换 `ntheorem`,但保留现有 tcolorbox theorem 公共环境名、双参数语法、标签前缀和分别计数方式。 -3. 将 `\theoremheaderfont`、`\theorembodyfont` 的效果搬到集中 tcolorbox style;删除 ntheorem 命令前先验证字体快照。 -4. 用 amsthm proof + tcolorbox wrapper 替换手写尾部方块,修正真正需要 `\qedhere` 的少量正文。 -5. 集中生成 autoref/cleveref 配置并比较旧、新 `.aux` 中的标签值。 -6. 一个发布周期内保留旧 proof/solution 可选参数适配;弃用告警应包含源文件和行号。 +1. 用 `amsthm` 替换 `ntheorem`,保留现有 tcolorbox theorem 公共环境名、双参数语法、标签前缀和分别计数方式。 +2. 将六类 theorem 的名称、前缀、颜色、样式及 autoref/cleveref 名称集中声明。 +3. 用 amsthm proof/QED 栈配合 tcolorbox 外观替换手写尾部方块;普通 proof、嵌套 proof、display/align 中的 `\qedhere` 已由最小 fixture 覆盖,solution 仍无 QED。 +4. 删除无人使用的 proof/solution 第二可选原始样式参数;正文不需修改,第一可选说明保持。 -迁移提交必须要求干净重编译辅助文件。除明确列出的 QED 位置修正外,既有标签键和可见定理编号不得变化。 +整书审计已确认主讲义 892 页和答案册 272 页在 144 dpi 下逐页像素一致;主讲义 raw/layout 文本与基线一致,答案册 raw 文本一致。迁移要求干净重编译辅助文件;既有标签键、可见定理编号与默认 QED 版式均保持不变。 ### 3.6 定理测试矩阵 @@ -512,34 +509,36 @@ build/answers/LALU-/ ## 四、Breaking changes 与源码迁移 -这里把“breaking change”限定为:升级模板后,旧源码、构建命令、生成文件或外部链接即使重新编译,也不能自动保持原有语义,必须改写或显式迁移。单纯的内部实现替换、可接受的分页微调,以及清理辅助文件后即可恢复的变化不单独算作 breaking change。 +这里区分两类变化: -### 4.1 明确会发生的兼容性中断 +- **作者/PDF breaking change**:`讲义/专题/`、`讲义/历年卷/`、`讲义/其它/` 中的既有公共写法必须改写,或最终 PDF 的可见内容、编号、顺序、分页、标签/引用语义发生变化。本轮原则上不允许,除非下面明确列出并经 review。 +- **supporting-structure breaking change**:只影响类、主模板、答案模板、Makefile、生成文件协议、内部 counter/helper/marker 或原始 href 名称。用户已确认这些无需向后兼容;只要正文和 PDF 可见行为不变,可以直接发生。 -| 计划中的变化 | 中断点 | 当前仓库中受影响的位置与定位方法 | 新写法或迁移动作 | -| --- | --- | --- | --- | -| 标准 `chapter` 改作所有有编号顶层单元的结构流水号 | 裸 `\value{chapter}`、`\arabic{chapter}`、`c@chapter` 及手工 `\setcounter`/`\addtocounter` 不再表示普通“第 N 讲” | 当前只在 `讲义/线性代数荣誉课辅学讲义.tex` 的章节和答案生成逻辑中出现;`讲义/专题/` 与 `讲义/其它/` 的手写正文目前没有这类依赖 | 普通讲次改读 `\LALUNormalChapterNumber`,当前可见引用改读 `\LALUCurrentReferenceNumber` 或 `\thechapter`,单元类型改查 `kind`;正文不得再直接改 `chapter` | -| 无编号状态不再泄漏上一编号单元 | frontmatter、backmatter 或 `\chapter*` 后的 `\thechapter` 将为空;依赖其继续返回上一章号的宏会改变行为 | 当前手写正文没有这种读取;迁移时仍须把 `\thechapter`/`\theHchapter` 纳入模板和扩展宏扫描 | 需要最近普通讲次时显式查询 `\LALUNormalChapterNumber`,需要当前单元时先检查状态 API;不得把无编号边界后的 `\thechapter` 当历史快照 | -| 删除跨章分组和历史 marker | `\LUgroupsancheck`、`\LUsection` 以及 `\@exist@LUchapter@...`/`\@exist@LUsection@...` 最终会消失 | 定位于主模板、`讲义/LALU-answers.tex` 和生成的 `LALU-ans-contents.tex`;各个未竟专题文件开头的 `\LUchapter{...}` 不受影响 | 删除主文件末尾等处的手工 sanity check;答案册只消费 `\LALUAnswerUnit` 等 manifest 事件,不再手工设置 `LUsection` 或探测 marker | -| 对 phase 和单元位置执行严格校验 | 以前“碰巧可编译”的 `mainmatter` 首章之前的 LU、front/backmatter 或 appendix 中的 LU/习题、`secnumdepth` 抑制编号时的 LU,以及非法阶段回退将直接报错;重复 phase 命令不再重置页码或计数器 | 从主文件的 `\frontmatter`、`\mainmatter`、`\appendix`、`\backmatter` 和各文件的 `\LUchapter`、`\begin{exercise}` 定位;当前正式输入顺序没有发现违规项 | 在 `\mainmatter` 中先建立有编号的普通 `\chapter`,再写 `\LUchapter`;习题只放在普通章或 LU 单元内。`\chapter*` 后不能直接挂习题,但仍可继续写下一个 LU;首阶段不支持附录习题 | -| 答案交换格式和输出路径改为带版本的 manifest | `LALU-ans-contents.tex` 的文件名、内容语法和可从源码目录直接 `\input` 的约定不再保证 | 主要影响主模板、`讲义/LALU-answers.tex`、`讲义/Makefile`、latexmk 配置和 CI,不要求逐篇改普通 inline 答案 | 构建命令把 manifest 路径显式传给答案册;只通过 manifest reader 读取 `\LALUAnswerUnit`/`Group`/`Item` 事件,生成文件一律不可手工编辑 | -| 空答案和空答案单元获得明确状态语义 | 纯空白 `answer` 将变为 `omitted`;完全没有 `answered`/`todo` 的习题单元不再输出标题、目录项或空白分页 | 当前有 29 个纯空 `answer`,集中在第 2、3、4、7、8、9、10 讲;另有 15 个完全没有 `answer` 的单元,分布在第 6、12、16–25 讲及未竟专题 4、8、14 | 确实没有答案时删除空环境或接受省略;需要保留答案项或整个单元时,至少加入一个计划接口 `\begin{answer}[status=todo] ... \end{answer}` | -| 答案单元和附录采用新的机器锚点命名空间 | 答案单元切换到 `LALUanswerunit.answer.*`,附录使用 `chapter.APP.*`;硬编码到旧 href 的外部 PDF 深链接或工具会失效 | 当前正文没有编号附录,正文的 `\label`/`\ref` 键原则上不改;主要迁移面是答案 PDF 的外部链接和下游 PDF 工具 | 发布说明列出 href 映射并尽可能提供一个版本的旧锚点别名;同时清理辅助文件并重建主讲义、答案册和 `xr` 缓存 | -| `ntheorem` 退出依赖面 | 外部文档若依赖类“顺便加载” `ntheorem`,或直接调用它的样式/声明命令,将不再工作 | 当前正文没有直接调用;仓库内只有 `LALUbook.cls` 的 `\theoremheaderfont`、`\theorembodyfont` 等模板设置需要迁移 | 自定义定理接入新的集中声明表和 amsthm/tcolorbox 适配层;外部文档不得依赖类的传递包依赖 | +因此,清理辅助文件并冷构建是本轮迁移动作;它不意味着要为旧 manifest、旧 counter alias 或旧 PDF destination 增加兼容层。 -上表中的“计划接口”只是目标写法,目前尚不是可执行命令;它们应在实现对应阶段时固定下来。一旦进入作者文档和正文,就必须遵循正常的弃用周期,不能在后续提交中无迁移说明地再次改名。 +### 4.1 本轮明确发生的内部兼容性中断 + +| 本轮变化 | 被移除/改变的 supporting structure | 正文受影响位置与新写法 | +| --- | --- | --- | +| 章节状态改为显式记录 | 裸 `chapter` 变为结构流水号;`LUchapter`、`LUgreekchap` counter alias 可删除;无编号状态不再把上一单元伪装成当前单元 | 仓库扫描只在主模板内部命中 raw counter;`讲义/专题/`、`讲义/历年卷/`、`讲义/其它/` 无需改写,仍用 `\chapter`/`\LUchapter` 和正常 `\label`。模板内部改读语义状态 API | +| 删除跨章开放分组和历史 marker | `\LUgroupsancheck`、`\LUsection`、`\@exist@LUchapter@...`、`\@exist@LUsection@...` 及配套 counter/marker 直接消失 | 命中只在主模板、答案模板和旧生成文件;正文各未竟专题开头的 `\LUchapter{...}` 不变,无需加入替代 helper | +| 答案交换格式与路径改为带版本 manifest | `LALU-ans-contents.tex` 的文件名、命令语法和直接 `\input` 约定不再支持 | 只修改主模板、`讲义/LALU-answers.tex`、`讲义/Makefile` 和发布脚本;所有正文继续原样写 `exercise`/`exgroup`/`answer` | +| 内部题目/单元 ID 与 href 命名空间改变 | 旧 manifest ID、`chapter.LU.*` 等旧 PDF 原始 destination 不承诺保留别名 | 正文 `\label` 键及 `\ref`/`\nameref`/`\autoref`/`\cref` 写法不变;只需清理辅助文件并重建,不需要逐篇修改 | +| 定理语义层从 ntheorem 切换为 amsthm | 类不再传递加载 ntheorem,其私有 style 命令不可用 | 正文未直接调用 ntheorem;六类 theorem、proof、solution 的保留接口不变,因此正文无需改写 | +| 删除 proof/solution 第二个原始 tcolorbox 选项 | `\begin{proof}[说明][任意样式]` 和 solution 同形内部签名不再接受第二项 | 全仓正文没有第二项调用;第一可选说明仍原样使用,无正文修改。若将来需要受控样式,应另行设计作者接口 | + +这些变化无需弃用周期,因为删除对象没有出现在约定范围内的正文中。任何新命令一旦写入正文或作者文档,才转为需要稳定和迁移说明的作者接口。 ### 4.2 条件性 breaking changes 与迁移审计 -下列事项要么只有在采用可选设计时才构成兼容性中断,要么必须人工审计,但不能预先假定所有命中处都需要改写: +下列提案本轮**不启用**;若以后采用,必须另做正文扫描、PDF 基线和迁移 review: - 如果答案册为 `\externaldocument` 加固定前缀,答案正文中指向主讲义的 `\ref`/`\nameref`/`\autoref`/`\cref` 要改为 `\mainref`/`\mainnameref`/`\mainautoref`/`\maincref` 等成套 helper。当前入口位于 `讲义/LALU-answers.tex`;启用前应先扫描所有 `answer` 载荷中的引用并验证没有同名标签。 -- 如果稳定习题 ID 从“建议”升级为“必填”,`exercise` 中相应的普通 `\item` 要改为 `\exitem[id=...]`。迁移初期必须继续自动生成 ID 并告警,不能一次性破坏现有全部习题。 +- 如果以后要求“在前面插题后已有答案 ID 不变”,就需要语义 ID 或显式 `\exitem[id=...]` 一类设计;当前自动 ID 允许随位置变化,本轮不要求正文标注。 - amsthm proof 的公共语法和普通自动 QED 行为应保持兼容,但 QED 的具体位置需要视觉审计。当前有 27 个 proof 直接以 display 结束;典型位置为 `讲义/专题/9 矩阵运算进阶.tex:443`、`讲义/专题/13 多项式.tex:542` 和 `讲义/专题/21 线性代数与几何.tex:49`。只有希望方块留在最后一行公式内的用例才需加入 `\qedhere`,不能把 27 处一律机械改写;当前正文没有手写 `\square`。 -- inline `answer` 本来就不保证支持 verbatim、minted、listing 或特殊 catcode,因此 file payload 是新增能力而非当前仓库的 breaking change。若外部答案依赖这些“碰巧工作”的未保证行为,则应把内容移到独立片段,并通过计划接口 `\answerfile{answers/.tex}` 引用;当前基线没有此类载荷。 -- 如果在兼容期后移除 proof/solution 的第二个原始 tcolorbox 选项,外部源码中的 `\begin{proof}[说明][任意样式]` 或同形 `solution` 要迁移到受控 key。当前仓库只有第一可选参数用例,没有发现第二可选参数。 -- 如果在更晚版本移除 `LUchapter`、`LUgreekchap`、`LUsection` counter alias,外部源码对这些 counter 的直接读取也会失效;当前仓库中的直接使用只在模板和生成文件内,应先由统一状态 API 取代。 -- 其他 section、公式、定理等后代 destination 只有在确认现值冲突时才改变;普通章和 LU 的既有 destination 仍以保留为默认。若不得不改变,必须按外部 PDF 深链接的 breaking change 处理,而不能把“清理 `.aux` 即可”当作充分迁移说明。 +- file payload/`\answerfile` 仍只是未来提案;当前 inline `answer` 明确只支持普通 LaTeX token,不支持 verbatim、minted、listing 或特殊 catcode,仓库正文没有此类载荷。 +- 省略空答案项、完全无答案的单元、目录条目或分页,以及要求用显式 todo 保留它们,都会改变答案 PDF;本轮继续保留这些可见对象,不采用省略策略。 +- 改变六类 theorem 的双参数语法、标签前缀、分别计数方式,或改 proof/solution 第一可选说明,都仍是条件性 breaking change,本轮不动。 - 支持引擎矩阵尚未最终确定;一旦决定不支持 LuaLaTeX 等引擎,这些非正式构建流程将改为早期失败。当前 Makefile 使用 XeLaTeX,不影响正文;若要把其他引擎列为受支持,须先补齐字体、索引、书签和回归测试。 - 如果无法可靠恢复 `\includeonly` 所需的非 counter 状态,可以明确禁止跨普通章/LU 的选择构建。当前主讲义使用 `\input`,因此仓库正文不受影响;外部使用者若依赖 `\includeonly`,应改为完整构建或等待 checkpoint 协议落地。 @@ -548,10 +547,11 @@ build/answers/LALU-/ 以下常用正文写法不应因本次重构而修改: - 普通章继续写 `\chapter{标题}`,未竟专题继续写 `\LUchapter{标题}`;新增的 `\LUchapter[短标题]{完整标题}` 只是可选扩展。 -- 普通 inline 答案继续写 `\begin{answer} ... \end{answer}`;只有空占位和特殊 catcode 载荷需要迁移。 +- 普通 inline 答案继续写 `\begin{answer} ... \end{answer}`;既有空 `answer` 也不需迁移。特殊 catcode 载荷不在该接口的支持范围内,本轮不引入替代语法。 - definition、example、lemma、theorem、corollary、axiom 继续保留当前双参数形式和标签前缀。 - `\begin{proof}[说明]` 与 `\begin{solution}[说明]` 的第一可选参数继续有效。 -- 既有 `\label` 键、普通章 `chapter.N` 和未竟专题 `chapter.LU.N` 锚点应保留;正常的 `\ref`、`\nameref`、`\autoref` 和 `\cref` 源码不要求批量改写。 +- 既有 `\label` 键及正常的 `\ref`、`\nameref`、`\autoref` 和 `\cref` 源码不要求批量改写;这些引用的可见结果和目标页面应保持。原始 href destination 字符串是内部细节,不在此承诺内。 +- 空答案项、无答案单元、答案目录项及既有分页继续保留;正文不需要新增 `status=todo` 才能维持现有 PDF。 ### 4.4 迁移时的定位清单 @@ -572,46 +572,54 @@ rg -n -U --pcre2 --glob '*.tex' '(?:\\\]|\\end\{(?:equation\*?|align\*?|alignat\ ## 五、分阶段落地顺序 -### 阶段 0:冻结基线与建立检查工具 +### 阶段 0:冻结基线(已完成) - 保存当前完整 PDF、答案 PDF、`.aux` 标签表、`.toc`、PDF outline 和归一化日志。 -- 建立章节、答案、定理三个最小回归文档。 -- 在 CI 中加入重复 destination、未定义引用、PDF string 警告和残留 rerun 的失败规则。 -- 统计当前答案记录数、空答案数、定理环境调用签名及标签前缀。 +- 记录主讲义和答案册的页面数、页面尺寸和抽取文本哈希。 +- 扫描答案记录、空答案、定理环境调用签名、proof 可选参数及标签前缀。 + +基线只用于回归比较,不加入生产构建。 -这一阶段只增加测试和基线,不改变产物。 +### 阶段 1:统一定理和证明框架(已实施) -### 阶段 1:引入统一章节状态,保持公共接口 +- 移除 ntheorem 语义层,引入 amsthm proof/QED。 +- 保持 tcolorbox theorem 的旧环境名、双参数语法、计数器行为和标签前缀。 +- 集中 theorem 声明、样式及 autoref/cleveref 配置。 +- 删除无人使用的 proof/solution 第二可选原始样式参数;保留第一可选说明。 +- 用最小 fixture 验证普通、嵌套和 display/align `\qedhere`,再纳入整书 PDF 对比。 -- 在类中加入结构 `chapter`、普通显示计数器、LU 流水号、slot、appendix 显示计数器以及统一章节记录。 +### 阶段 2:引入统一章节状态(已实施) + +- 在主模板中加入结构 `chapter`、普通显示计数器、LU 流水号、slot、appendix 显示计数器以及统一章节记录。 - 提供语义查询 API,替换模板内部所有把裸 `chapter` 当普通显示编号的用法,并启用静态检查。 - 在既有 `\chapter`、`\LUchapter` 外观后切换到新状态机。 -- 移除跨章节开放分组和历史 marker 的内部依赖;兼容命令暂留。 -- 同时改造答案册的统一 `LALUanswerunit` renderer,但暂不切换答案生成格式。 -- 通过章节测试矩阵和旧、新目录/outline 对比后合并。 +- 直接移除跨章节开放分组、旧 counter alias 和历史 marker,不保留兼容命令。 +- 通过章节测试矩阵以及旧、新目录/outline/标签值/PDF 代表页面对比后完成。 -### 阶段 2:答案 manifest 双写与原子发布 +### 阶段 3:答案 manifest 与原子发布(已实施) -- 定义带版本的记录 schema、reader 和 file 载荷接口。 -- 构建系统同时生成 legacy 文件与新 manifest,分别编译答案册并逐记录比较。 -- 加入临时目录、结束 sentinel、原子 rename、过期检查和并发测试。 -- 将含 verbatim 或 catcode 风险的答案迁移到 file 载荷。 -- 对比通过后切换答案册读取新 manifest;保留一次发布周期的 legacy reader。 +- 定义带版本的 inline-token 事件 schema 和统一 `LALUanswerunit` renderer。 +- 主编译写同目录唯一临时文件;发布脚本校验 schema、sentinel、严格事件顺序、ID 和统计。 +- Makefile 将临时 manifest 路径和 job 身份传给答案册;reader 对缺失、版本/job 不匹配、事件顺序、ID 和计数错误明确失败。主 PDF 和答案 PDF 先留在 staging 路径,答案册成功后才与 manifest 集中发布,因此答案 body 的 TeX 错误不会提前覆盖任一份正式产物。 +- 发布器负向 fixture 已覆盖 Begin/End 错位、marker/command 不匹配、行尾额外 TeX、未闭合参数、重复 footer、非连续 ID 和统计不一致,这些输入均以非零状态拒绝。 +- 直接切换新 reader,不双写 legacy 文件,也不保留旧 `LALU-ans-contents.tex` reader。 +- 比较新旧答案 PDF 的单元、标题、组号、题号、正文、空答案、无答案单元和分页。 -### 阶段 3:统一定理和证明框架 +### 阶段 4:整书验证与收尾(已完成) -- 移除 ntheorem 语义层,引入 amsthm proof/QED。 -- 保持 tcolorbox theorem 的旧环境名、双参数语法、计数器行为和标签前缀。 -- 集中 theorem 声明、样式及 autoref/cleveref 配置。 -- 修正并测试 display math 的 `\qedhere`,进行分页与字体 golden PDF 对比。 +- 在本机 MacTeX 上从 `make clean` 开始完整构建主讲义和答案册,并确认构建返回值能传播索引/manifest 失败。 +- 检查 duplicate destination、undefined reference、PDF string warning 和残留 rerun 提示。 +- 对照基线检查页面数、页面尺寸、抽取文本、目录/outline、标签表和全书像素;最终主讲义 892/892 页、答案册 272/272 页在 144 dpi 下均无像素差异。 +- 清理构建产物,确认只保留本轮源码修改和用户原有的无关工作区改动。 -### 阶段 4:清理兼容层 +### 后续 review(不属于本轮) -- 在确认仓库及外部维护文档已迁移后,删除 `\LUgroupsancheck`、动态 `@exist@...` marker、旧答案 reader 和原始样式透传。 -- 更新作者文档,说明 `\LUchapter`、稳定习题 ID、todo、file answer、proof 和引用的推荐写法。 -- 将旧生成文件从构建流程和清理规则中彻底移除。 +- 设计插题后仍稳定的显式习题 ID。 +- 决定是否需要 file answer,以及它是否值得改变正文作者接口。 +- 评估空答案/todo/空单元省略政策;任何改变先审核答案 PDF 的可见差异。 +- 逐项 review 第 4.2 节的其余条件性 breaking changes。 -每个阶段独立可回退,不把章节、答案和定理三类高风险改动塞进同一个不可审查提交。 +定理/证明改动单独提交;章节记录与答案 manifest 共享同一语义 API,以一个始终可编译的原子提交落地,文档同步另作提交。 ## 六、总体验收标准 @@ -623,15 +631,16 @@ rg -n -U --pcre2 --glob '*.tex' '(?:\\\]|\\end\{(?:equation\*?|align\*?|alignat\ 4. 主 PDF 与答案 PDF 日志中不存在重复 destination;所有内部链接和 `xr` 链接指向正确页面。 5. 目录与 PDF outline 中每个单元恰有一条记录,中文标题、Unicode 希腊编号和层级正确。 6. `\ref`、`\nameref`、`\autoref`、`\cref` 以及范围引用覆盖普通章、未竟专题、附录、节、公式和六类定理,输出与兼容约定一致。 -7. 没有可见答案的单元不生成答案册标题页;显式 todo 按配置生成统一占位。 -8. 主文档失败、答案记录损坏或载荷缺失时,不覆盖上一份有效 manifest,并以非零状态失败。 -9. 不同 jobname 可并发构建;同一目标的竞争构建不会产生交叉或半截文件。 -10. 普通 inline 答案保持宏、段落和标签语义;file 答案可正常包含 verbatim/listing。 -11. 现有定理环境调用在兼容阶段无需批量人工修改,编号和标签键不变。 +7. 空 `answer` 仍生成原有空编号项;完全没有答案的单元仍保留原有标题、目录项和分页,答案 PDF 的可见顺序与基线一致。 +8. 主文档失败或临时 manifest 的 schema、sentinel、统计校验失败时,不覆盖上一份有效的主 PDF、答案 PDF 或 manifest,并以非零状态失败;reader 对缺失、损坏或 job/version 不匹配明确失败。 +9. 答案构建只读已结束且校验通过的临时 manifest,并将答案 PDF 写入 staging 目录;主编译、结构校验或答案 TeX 任一失败都不提前覆盖正式产物。全部成功后才集中移入两份 PDF 并以同目录原子 rename 替换 manifest;同一发布目标的并发构建不在本轮支持范围内。 +10. 普通 inline 答案保持宏、段落和标签语义;测试边界明确排除 verbatim、minted、listing 和特殊 catcode。 +11. 现有定理环境调用无需批量人工修改,编号、标签键、双参数接口和 proof/solution 第一可选说明不变。 12. proof 在普通文本、display math、跨页和嵌套场景中 QED 恰好出现一次;solution 缺省无 QED。 13. 章节状态、答案 renderer 和 theorem 声明各有单一实现来源,不再存在两套通过历史 marker 或开放分组彼此模仿的逻辑。 14. frontmatter、backmatter 和星号章不递增结构/显示 counter;重复阶段命令不重置任何章节状态,非法阶段回退明确失败;每个编号附录只递增结构与 appendix 显示 counter,且不会把结构 `c@chapter` 清零。 15. 全部手写源码(包括类和模板)都不存在把裸 `\value{chapter}`、`\arabic{chapter}`、`\setcounter{chapter}`、`\addtocounter{chapter}` 或 `c@chapter` 当作普通显示编号的调用;确需读写结构值的统一模块代码均在静态检查白名单中。 -16. 受支持 TeX Live 版本的测试全部通过;不支持的引擎在加载早期给出明确错误。 +16. 本机 MacTeX/XeLaTeX 的最小 fixture 和完整 `make clean && make` 均通过;Docker、其他 TeX Live 版本和其他引擎不作为本轮验收项。 17. 无普通章、错误 phase、编号被抑制或希腊 slot 超界时,`\LUchapter` 都在改变任何 counter、当前记录或样式前失败;修正输入后可继续构建,不受半初始化状态污染。 18. frontmatter、mainmatter 首章之前、backmatter、星号章、appendix 入口空隙及首阶段的附录章都不会把习题误挂到上一个正文/LU 单元;不支持的附录答案以明确错误结束。 +19. 主讲义和答案册的可见文本、章节/答案顺序、空答案项、无答案单元及分页与冻结基线一致;允许内部 counter、manifest 命令和 href destination 名称变化。 From ff1ac59afaa17bcdaeda7596e2d662c59614f5b5 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Fri, 18 Sep 2026 22:11:30 +0800 Subject: [PATCH 13/27] refactor(build): use ASCII document entrypoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将主讲义入口和正式产物统一为 LALU.tex/LALU.pdf,移除为 xindy 临时复制中文文件名的构建分支。同步收紧 CI 产物路径,并保留答案 manifest 的 LALU job 标识。 --- .github/workflows/tex.yml | 4 ++- .gitignore | 1 - .../LALU.tex" | 0 "\350\256\262\344\271\211/Makefile" | 28 ++++++------------- 4 files changed, 11 insertions(+), 22 deletions(-) rename "\350\256\262\344\271\211/\347\272\277\346\200\247\344\273\243\346\225\260\350\215\243\350\252\211\350\257\276\350\276\205\345\255\246\350\256\262\344\271\211.tex" => "\350\256\262\344\271\211/LALU.tex" (100%) diff --git a/.github/workflows/tex.yml b/.github/workflows/tex.yml index b0ad665..c323bb0 100644 --- a/.github/workflows/tex.yml +++ b/.github/workflows/tex.yml @@ -35,4 +35,6 @@ jobs: with: name: LALU-build-${{ github.run_number }}-${{ steps.build.outputs.sha }} if-no-files-found: error - path: ${{ github.workspace }}/讲义/*.pdf + path: | + ${{ github.workspace }}/讲义/LALU.pdf + ${{ github.workspace }}/讲义/LALU-answers.pdf diff --git a/.gitignore b/.gitignore index 13dd339..6c9f67b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ build gh-actions-build -讲义/LALU.tex 讲义/LALU-ans-contents.tex 习题参考答案/LALU-answers.tex diff --git "a/\350\256\262\344\271\211/\347\272\277\346\200\247\344\273\243\346\225\260\350\215\243\350\252\211\350\257\276\350\276\205\345\255\246\350\256\262\344\271\211.tex" "b/\350\256\262\344\271\211/LALU.tex" similarity index 100% rename from "\350\256\262\344\271\211/\347\272\277\346\200\247\344\273\243\346\225\260\350\215\243\350\252\211\350\257\276\350\276\205\345\255\246\350\256\262\344\271\211.tex" rename to "\350\256\262\344\271\211/LALU.tex" diff --git "a/\350\256\262\344\271\211/Makefile" "b/\350\256\262\344\271\211/Makefile" index a48b1e5..4fe340b 100644 --- "a/\350\256\262\344\271\211/Makefile" +++ "b/\350\256\262\344\271\211/Makefile" @@ -1,49 +1,37 @@ -# FIXME: xindy can't handle UTF-8 filenames -FILENAME := 线性代数荣誉课辅学讲义 -CPNAME := LALU +MAIN_JOB := LALU LATEXMK := latexmk LATEXMK_FLAGS := -xelatex -shell-escape -interaction=nonstopmode -file-line-error ANSWER_BUILD_DIR := build/answers -ANSWER_MANIFEST := $(ANSWER_BUILD_DIR)/$(CPNAME).manifest.tex +ANSWER_MANIFEST := $(ANSWER_BUILD_DIR)/$(MAIN_JOB).manifest.tex ANSWER_PUBLISHER := scripts/publish-answer-manifest.pl .PHONY: all clean all: @mkdir -p $(ANSWER_BUILD_DIR) -ifdef CPNAME @set -eu; \ - answer_tmp=$$(mktemp "$(ANSWER_BUILD_DIR)/$(CPNAME).manifest.XXXXXX"); \ - answer_stage=$$(mktemp -d "$(ANSWER_BUILD_DIR)/$(CPNAME).answers.XXXXXX"); \ + answer_tmp=$$(mktemp "$(ANSWER_BUILD_DIR)/$(MAIN_JOB).manifest.XXXXXX"); \ + answer_stage=$$(mktemp -d "$(ANSWER_BUILD_DIR)/$(MAIN_JOB).answers.XXXXXX"); \ cleanup() { \ - rm -f "$$answer_tmp" "$(CPNAME).tex" "$(CPNAME).pdf"; \ + rm -f "$$answer_tmp"; \ rm -rf -- "$$answer_stage"; \ }; \ trap cleanup EXIT HUP INT TERM; \ - cp -p $(FILENAME).tex $(CPNAME).tex; \ $(LATEXMK) -g $(LATEXMK_FLAGS) \ -usepretex="\\def\\LALUAnswerManifestTempPath{$$answer_tmp}" \ - $(CPNAME).tex; \ + $(MAIN_JOB).tex; \ perl $(ANSWER_PUBLISHER) --check "$$answer_tmp"; \ $(LATEXMK) -g $(LATEXMK_FLAGS) \ -outdir="$$answer_stage" \ - -usepretex="\\def\\LALUAnswerManifestPath{$$answer_tmp}\\def\\LALUAnswerManifestJob{$(CPNAME)}" \ + -usepretex="\\def\\LALUAnswerManifestPath{$$answer_tmp}\\def\\LALUAnswerManifestJob{$(MAIN_JOB)}" \ LALU-answers.tex; \ - test -s "$(CPNAME).pdf"; \ + test -s "$(MAIN_JOB).pdf"; \ test -s "$$answer_stage/LALU-answers.pdf"; \ - mv "$(CPNAME).pdf" "$(FILENAME).pdf"; \ mv "$$answer_stage/LALU-answers.pdf" LALU-answers.pdf; \ perl $(ANSWER_PUBLISHER) --publish "$$answer_tmp" "$(ANSWER_MANIFEST)" -else - $(LATEXMK) $(LATEXMK_FLAGS) $(FILENAME).tex - $(LATEXMK) $(LATEXMK_FLAGS) LALU-answers.tex -endif clean: rm -f *.aux *.log *.idx *.ind *.ilg *.thm *.toc *.blg *.bbl *.bcf *.out *.fls *.fdb_latexmk *.run.xml *.synctex.gz *.xdv *~ *.lof *.lot rm -f LALU-ans-contents.tex *.answer-manifest.pending.tex rm -rf -- $(ANSWER_BUILD_DIR) -ifdef CPNAME - rm -f $(CPNAME).pdf $(CPNAME).tex -endif From 9ba1192b8ecdf6f8ada65c67b15d1a25525472a5 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Fri, 18 Sep 2026 22:12:19 +0800 Subject: [PATCH 14/27] refactor(template): colocate the document class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 LALUbook.cls 移至两个入口文档所在目录,并删除依赖 LaTeX 内部 input@path 的全局搜索路径修改。标准 make、latexmk -cd 与编辑器从讲义目录构建时可直接解析文档类。 --- "\350\256\262\344\271\211/LALU-answers.tex" | 4 ---- "\350\256\262\344\271\211/LALU.tex" | 4 ---- LALUbook.cls => "\350\256\262\344\271\211/LALUbook.cls" | 0 3 files changed, 8 deletions(-) rename LALUbook.cls => "\350\256\262\344\271\211/LALUbook.cls" (100%) diff --git "a/\350\256\262\344\271\211/LALU-answers.tex" "b/\350\256\262\344\271\211/LALU-answers.tex" index 49174ef..ad479aa 100644 --- "a/\350\256\262\344\271\211/LALU-answers.tex" +++ "b/\350\256\262\344\271\211/LALU-answers.tex" @@ -1,7 +1,3 @@ -\makeatletter - \def\input@path{{..}} % 搜索上层目录的 LALUbook -\makeatother - \documentclass[ % colors = false, geometry = 16k, diff --git "a/\350\256\262\344\271\211/LALU.tex" "b/\350\256\262\344\271\211/LALU.tex" index 240e64d..3cf8196 100644 --- "a/\350\256\262\344\271\211/LALU.tex" +++ "b/\350\256\262\344\271\211/LALU.tex" @@ -1,7 +1,3 @@ -\makeatletter - \def\input@path{{..}} % 搜索上层目录的 LALUbook -\makeatother - \documentclass[ % colors = false, geometry = 16k, diff --git a/LALUbook.cls "b/\350\256\262\344\271\211/LALUbook.cls" similarity index 100% rename from LALUbook.cls rename to "\350\256\262\344\271\211/LALUbook.cls" From 24f5317594de5c1fd409323088defc44c8deb913 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Fri, 18 Sep 2026 22:18:05 +0800 Subject: [PATCH 15/27] fix(template): restore standard proof QED handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 让 proof 的 QED 栈直接使用 amsthm 的 \qed,恢复其断行、分页、最小间距和 \qedhere 定位语义。solution 继续不产生 QED marker。 --- "\350\256\262\344\271\211/LALUbook.cls" | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git "a/\350\256\262\344\271\211/LALUbook.cls" "b/\350\256\262\344\271\211/LALUbook.cls" index bd28485..9787528 100644 --- "a/\350\256\262\344\271\211/LALUbook.cls" +++ "b/\350\256\262\344\271\211/LALUbook.cls" @@ -170,17 +170,8 @@ % amsthm 负责 QED 栈和 \qedhere,tcolorbox 只负责外观。 \renewcommand*{\qedsymbol}{$\square$} -% 普通段落结尾保留旧模板的精确胶水;数学模式仍转给 -% amsthm,使 \qedhere 可以在 equation/align 内正确放置方块。 -\newcommand*{\LALU@qed}{% - \ifmmode - \mathqed - \else - \hspace*{\fill}\qedsymbol - \fi -} \RenewDocumentEnvironment{proof}{O{}}{% - \pushQED{\LALU@qed}% + \pushQED{\qed}% \begin{tcblaluthmbox}{{\heiti 证明}}{#1}{}% \fangsong\ignorespaces }{% From 9c816ae162141eebdde5d8e1f4125eaa7346e27c Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Fri, 18 Sep 2026 22:24:52 +0800 Subject: [PATCH 16/27] feat(template): add Chinese autoref spacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 hyperref 的作者接口外层保留普通与星号形式,并用 xspace 自动处理后继正文间距;中文及西文标点、括号保持紧贴。保留现有手写空格以减少正文改动,同时修正一处标点前的错误空格。 --- "\350\256\262\344\271\211/LALUbook.cls" | 19 +++++++++++++++++++ ...0\347\256\227\345\237\272\347\241\200.tex" | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git "a/\350\256\262\344\271\211/LALUbook.cls" "b/\350\256\262\344\271\211/LALUbook.cls" index 9787528..85414b7 100644 --- "a/\350\256\262\344\271\211/LALUbook.cls" +++ "b/\350\256\262\344\271\211/LALUbook.cls" @@ -62,6 +62,7 @@ % 加入字串处理功能 \RequirePackage{xstring} +\RequirePackage{xspace} % 设置天眉所需 \RequirePackage{fancyhdr} @@ -213,6 +214,24 @@ \RequirePackage{bookmark} \RequirePackage{cleveref} + % hyperref 在链接组内组装引用文字,XeCJK 因此无法总是从 + % 后继中文字符自动推断边界间距。xspace 在正文前补等价空格, + % 而在中英文标点和括号前保持紧贴。 + \xspaceaddexceptions{% + ,.;:!?()[]% + ,。.、;:?!% + ()[]〔〕【】〖〗〘〙〚〛% + 〈〉《》「」『』“”‘’% + …—·% + } + \NewCommandCopy{\LALU@hyperref@autoref}{\autoref} + \RenewDocumentCommand{\autoref}{s m}{% + \IfBooleanTF{#1} + {\LALU@hyperref@autoref*{#2}} + {\LALU@hyperref@autoref{#2}}% + \xspace + } + \LALU@DeclareBoxedTheorem{definition}{定义}{red}{def} \LALU@DeclareBoxedTheorem{example}{例}{blue}{ex} \LALU@DeclareBoxedTheorem{lemma}{引理}{orange}{lem} diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/7 \347\272\277\346\200\247\346\230\240\345\260\204\347\237\251\351\230\265\350\241\250\347\244\272\344\270\216\347\237\251\351\230\265\350\277\220\347\256\227\345\237\272\347\241\200.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/7 \347\272\277\346\200\247\346\230\240\345\260\204\347\237\251\351\230\265\350\241\250\347\244\272\344\270\216\347\237\251\351\230\265\350\277\220\347\256\227\345\237\272\347\241\200.tex" index 48e211a..8c7bde1 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/7 \347\272\277\346\200\247\346\230\240\345\260\204\347\237\251\351\230\265\350\241\250\347\244\272\344\270\216\347\237\251\351\230\265\350\277\220\347\256\227\345\237\272\347\241\200.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/7 \347\272\277\346\200\247\346\230\240\345\260\204\347\237\251\351\230\265\350\241\250\347\244\272\344\270\216\347\237\251\351\230\265\350\277\220\347\256\227\345\237\272\347\241\200.tex" @@ -1112,7 +1112,7 @@ \subsection{对偶映射的矩阵表示} a_1 x_1 + a_2 x_2 + \cdots + a_n x_n = 1 \] -我们记 $a = (a_1,a_2,\ldots,a_n)^\mathrm{T}, x = (x_1,x_2,\ldots,x_n)^\mathrm{T}$,于是上面的方程可以简化为 $a^\mathrm{T} x = 1$. 根据线性空间同构于其对偶,$a \in \mathbf{F}^n$ 与 $a^\mathrm{T} x = 1$(视为 $(\mathbf{F}^n)^*$ 中的元素,回忆\autoref{eq:6:线性泛函与超平面对应} )是一一对应的关系. 于是我们接下来可以给出\autoref{thm:对偶映射的矩阵表示} 从几何上的理解. 考虑超平面方程 $a^\mathrm{T} x = 1$,则与这个超平面对偶的点是 $a$. 设 $\sigma \in \mathcal{L}(\mathbf{F}^n,\mathbf{F}^m)$,根据\autoref{lem:向量空间线性映射的矩阵表示},$\sigma$ 与一个矩阵 $M$ 对应. +我们记 $a = (a_1,a_2,\ldots,a_n)^\mathrm{T}, x = (x_1,x_2,\ldots,x_n)^\mathrm{T}$,于是上面的方程可以简化为 $a^\mathrm{T} x = 1$. 根据线性空间同构于其对偶,$a \in \mathbf{F}^n$ 与 $a^\mathrm{T} x = 1$(视为 $(\mathbf{F}^n)^*$ 中的元素,回忆\autoref{eq:6:线性泛函与超平面对应})是一一对应的关系. 于是我们接下来可以给出\autoref{thm:对偶映射的矩阵表示} 从几何上的理解. 考虑超平面方程 $a^\mathrm{T} x = 1$,则与这个超平面对偶的点是 $a$. 设 $\sigma \in \mathcal{L}(\mathbf{F}^n,\mathbf{F}^m)$,根据\autoref{lem:向量空间线性映射的矩阵表示},$\sigma$ 与一个矩阵 $M$ 对应. 我们对 $a \in \mathbf{F}^n$ 作用上 $\sigma$,得到了 $\sigma(a) = Ma \in \mathbf{F}^m$. 而与 $\sigma(a)$ 对应的超平面方程是 $(Ma)^\mathrm{T} x' = 1$,即 $(a^\mathrm{T}M^\mathrm{T}) x' = 1$,即新的超平面和原来的超平面相差一个 $M^\mathrm{T}$,即 $(\mathbf{F}^m)^*$ 与 $(\mathbf{F}^n)^*$ 之间相差了 $M^\mathrm{T}$. 因此,从几何直观上,我们也验证了转置和对偶映射的对应关系. From c21ef6274346970feafb0ea1feb62825df704cf8 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Fri, 18 Sep 2026 22:27:21 +0800 Subject: [PATCH 17/27] fix(document): place QED markers in final displays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 27 个以展示公式结束的 proof 中使用 amsthm 的 \qedhere,使方块留在最后一行公式内;同时移除一处 proof 尾的空段落,避免自动 QED 被推到孤立的新行。列表结尾与空占位 proof 本轮不做机械改写。 --- .../10 \350\241\214\345\210\227\345\274\217.tex" | 8 ++++---- .../13 \345\244\232\351\241\271\345\274\217.tex" | 4 ++-- ...46\234\272\344\270\216\345\237\272\347\241\200.tex" | 2 +- ...45\206\205\347\247\257\347\251\272\351\227\264.tex" | 6 +++--- ...44\270\212\347\232\204\347\256\227\345\255\220.tex" | 2 +- ...46\225\260\344\270\216\345\207\240\344\275\225.tex" | 10 +++++----- .../22 \344\272\214\346\254\241\345\236\213.tex" | 7 +++---- ...44\270\216\345\276\256\347\247\257\345\210\206.tex" | 2 +- ...45\257\271\345\201\266\347\251\272\351\227\264.tex" | 4 ++-- ...50\277\220\347\256\227\345\237\272\347\241\200.tex" | 2 +- ...46\212\265\346\240\207\345\207\206\345\275\242.tex" | 4 ++-- ...50\277\220\347\256\227\350\277\233\351\230\266.tex" | 4 ++-- 12 files changed, 27 insertions(+), 28 deletions(-) diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/10 \350\241\214\345\210\227\345\274\217.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/10 \350\241\214\345\210\227\345\274\217.tex" index 372ae15..500c41b 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/10 \350\241\214\345\210\227\345\274\217.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/10 \350\241\214\345\210\227\345\274\217.tex" @@ -843,7 +843,7 @@ \subsection{打洞法} O & A \\ B & C \end{vmatrix} = (-1)^{kr} \begin{vmatrix} A & O \\ C & B - \end{vmatrix} = (-1)^{kr} |A||B|. + \end{vmatrix} = (-1)^{kr} |A||B|. \qedhere \] \end{proof} @@ -1054,7 +1054,7 @@ \subsection{行列式与函数} 与第一部分合并可得 \[ - \dfrac{\mathrm{d}}{\mathrm{d}t}F(t)=\sum\limits_{j=1}^nF_j(t). + \dfrac{\mathrm{d}}{\mathrm{d}t}F(t)=\sum\limits_{j=1}^nF_j(t). \qedhere \] \end{proof} @@ -2608,7 +2608,7 @@ \section{Cauchy-Binet 公式} \begin{pmatrix} j_1 & j_2 & \cdots & j_m \\ 1 & 2 & \cdots & m - \end{pmatrix}. + \end{pmatrix}. \qedhere \] \end{proof} @@ -2685,7 +2685,7 @@ \section{Cauchy-Binet 公式} \begin{pmatrix} k_1 & k_2 & \cdots & k_r \\ j_1 & j_2 & \cdots & j_r - \end{pmatrix}. \] + \end{pmatrix}. \qedhere \] \end{proof} 矩阵 $A$ 的子式 diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/13 \345\244\232\351\241\271\345\274\217.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/13 \345\244\232\351\241\271\345\274\217.tex" index c9ebe71..309733b 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/13 \345\244\232\351\241\271\345\274\217.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/13 \345\244\232\351\241\271\345\274\217.tex" @@ -158,7 +158,7 @@ \section{整除与互素} f(x)\in \mathbf{F}[x]$,证明:$g(x)$是$f^2(x)$的因式的充要条件是$g(x)$是$f(x)$的因式. \end{example} \begin{proof} - 根据带余除法可知存在唯一的$q(x),r(x)(r(x)\neq 0)\in\mathbf{F}[x]$使得$f(x)=g(x)q(x)+r(x)$,且$\deg r<\deg g=1$,故$\deg r=0$,即$r(x)$是一个常数多项式,记为$c$,则$f^2(x)=g^2(x)q^2(x)+2cg(x)q(x)+c^2=(g(x)q^2(x)+2cq(x))g(x)+c^2$. 从而\[g(x) \mid f^2(x) \iff c^2=0 \iff c=0 \iff g(x) \mid f(x).\] + 根据带余除法可知存在唯一的$q(x),r(x)(r(x)\neq 0)\in\mathbf{F}[x]$使得$f(x)=g(x)q(x)+r(x)$,且$\deg r<\deg g=1$,故$\deg r=0$,即$r(x)$是一个常数多项式,记为$c$,则$f^2(x)=g^2(x)q^2(x)+2cg(x)q(x)+c^2=(g(x)q^2(x)+2cq(x))g(x)+c^2$. 从而\[g(x) \mid f^2(x) \iff c^2=0 \iff c=0 \iff g(x) \mid f(x). \qedhere\] \end{proof} 关于带余除法的具体计算方法此处不具体展开,因为与我们在小学中学习的整数竖式除法是基本一致的. 接下来我们利用已经定义的整除、因式、带余除法等概念进一步引进公因式、公倍式、最大公因式和最小公倍式等概念: @@ -538,7 +538,7 @@ \section{实数域与有理数域上的多项式函数} 证明非常直接,我们直接验证 \begin{align*} p(\overline{\lambda}) & =a_n\overline{\lambda}^n+a_{n-1}\overline{\lambda}^{n-1}+\cdots+a_1\overline{\lambda}+a_0 \\ - & =\overline{a_n\lambda^n+a_{n-1}\lambda^{n-1}+\cdots+a_1\lambda+a_0}=0. + & =\overline{a_n\lambda^n+a_{n-1}\lambda^{n-1}+\cdots+a_1\lambda+a_0}=0. \qedhere \end{align*} \end{proof} 这一引理表明,实数域上的多项式的复根是成对出现的(实根的共轭是其本身,上面的定理不再具有意义). 下面我们来研究实数域上不可约多项式的形式,事实上中学阶段我们都学习过这一点: diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/14 \347\233\270\344\274\274\346\240\207\345\207\206\345\275\242\357\274\232\345\212\250\346\234\272\344\270\216\345\237\272\347\241\200.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/14 \347\233\270\344\274\274\346\240\207\345\207\206\345\275\242\357\274\232\345\212\250\346\234\272\344\270\216\345\237\272\347\241\200.tex" index 92aee98..a942f24 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/14 \347\233\270\344\274\274\346\240\207\345\207\206\345\275\242\357\274\232\345\212\250\346\234\272\344\270\216\345\237\272\347\241\200.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/14 \347\233\270\344\274\274\346\240\207\345\207\206\345\275\242\357\274\232\345\212\250\346\234\272\344\270\216\345\237\272\347\241\200.tex" @@ -76,7 +76,7 @@ \section{相似的定义与性质} P_1^{-1}A_1P_1 & O \\ O & P_2^{-1}A_2P_2 \end{pmatrix}=\begin{pmatrix} B_1 & O \\ O & B_2 - \end{pmatrix}.\] + \end{pmatrix}. \qedhere\] \end{proof} \item 与数量矩阵相似的为其自身,与幂等矩阵相似的仍幂等,与对合矩阵相似的仍对合,与幂零矩阵相似的仍幂零,与正交矩阵正交相似的仍正交(但与正交矩阵相似的不一定正交). diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/19 \345\206\205\347\247\257\347\251\272\351\227\264.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/19 \345\206\205\347\247\257\347\251\272\351\227\264.tex" index 1e5ea65..18f84f6 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/19 \345\206\205\347\247\257\347\251\272\351\227\264.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/19 \345\206\205\347\247\257\347\251\272\351\227\264.tex" @@ -190,7 +190,7 @@ \subsection{正交的定义 \quad 基于正交的性质} \begin{align*} \lVert u + v \rVert^2 & = \langle u + v, u + v \rangle \\ & = \langle u, u \rangle + \langle u, v \rangle + \langle v, u \rangle + \langle v, v \rangle \\ - & = \lVert u \rVert^2 + \lVert v \rVert^2 + & = \lVert u \rVert^2 + \lVert v \rVert^2 \qedhere \end{align*} \end{proof} @@ -275,7 +275,7 @@ \subsection{正交的定义 \quad 基于正交的性质} & = (\lVert u \rVert + \lVert v \rVert)^2, \end{align*} 因为 $\lVert u + v \rVert$ 和 $\lVert u \rVert + \lVert v \rVert$ 都是非负实数,两边开平方,即得: - \[\lVert u + v \rVert \le \lVert u \rVert + \lVert v \rVert.\] + \[\lVert u + v \rVert \le \lVert u \rVert + \lVert v \rVert. \qedhere\] \end{proof} 其几何解释就是俗称的三角形两边之和小于第三边. @@ -395,7 +395,7 @@ \section{标准正交基} \lVert u \rVert^2 = \sum_{i=1}^{m} a_i \overline{a_i} \langle e_i, e_i \rangle = \sum_{i=1}^{m} a_i \overline{a_i} \cdot 1 = \sum_{i=1}^{m} \lvert a_i \rvert^2, \] 即 - \[ \lVert a_1e_1 + \cdots + a_m e_m\rVert^2 = \lvert a_1 \rvert^2 + \cdots + \lvert a_m \rvert^2. \] + \[ \lVert a_1e_1 + \cdots + a_m e_m\rVert^2 = \lvert a_1 \rvert^2 + \cdots + \lvert a_m \rvert^2. \qedhere \] \end{proof} 其本质可以看作勾股定理的推广,该定理也有一个重要推论: diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/20 \345\206\205\347\247\257\347\251\272\351\227\264\344\270\212\347\232\204\347\256\227\345\255\220.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/20 \345\206\205\347\247\257\347\251\272\351\227\264\344\270\212\347\232\204\347\256\227\345\255\220.tex" index 155a970..62af7ac 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/20 \345\206\205\347\247\257\347\251\272\351\227\264\344\270\212\347\232\204\347\256\227\345\255\220.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/20 \345\206\205\347\247\257\347\251\272\351\227\264\344\270\212\347\232\204\347\256\227\345\255\220.tex" @@ -1898,7 +1898,7 @@ \subsubsection*{算子范数} \] 因而 $\lVert T \rVert \geqslant s_1$. 综上 \[ - \lVert T \rVert = s_1. + \lVert T \rVert = s_1. \qedhere \] \end{proof} diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/21 \347\272\277\346\200\247\344\273\243\346\225\260\344\270\216\345\207\240\344\275\225.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/21 \347\272\277\346\200\247\344\273\243\346\225\260\344\270\216\345\207\240\344\275\225.tex" index e50fb93..3c4e9ad 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/21 \347\272\277\346\200\247\344\273\243\346\225\260\344\270\216\345\207\240\344\275\225.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/21 \347\272\277\346\200\247\344\273\243\346\225\260\344\270\216\345\207\240\344\275\225.tex" @@ -45,7 +45,7 @@ \section{欧氏空间上的运算} \vec{a}\cdot\vec{b} & = (a_1 \vec{e}_1 + a_2 \vec{e}_2 + a_3 \vec{e}_3)\cdot(b_1 \vec{e}_1 + b_2 \vec{e}_2 + b_3 \vec{e}_3) \\ & = \sum_{i=1}^3 \sum_{j=1}^3 a_i b_j (\vec{e}_i \cdot \vec{e}_j) - = \sum_{i=1}^3 \sum_{j=1}^3 g_{ij} a_i b_j. + = \sum_{i=1}^3 \sum_{j=1}^3 g_{ij} a_i b_j. \qedhere \end{align*} \end{proof} @@ -119,7 +119,7 @@ \section{欧氏空间上的运算} + (a_2 b_3 - a_3 b_2)\,\vec{e}_2 \times \vec{e}_3 \\ & = (a_2 b_3 - a_3 b_2)\,\vec{e}_2 \times \vec{e}_3 + (a_3 b_1 - a_1 b_3)\,\vec{e}_3 \times \vec{e}_1 - + (a_1 b_2 - a_2 b_1)\,\vec{e}_1 \times \vec{e}_2. + + (a_1 b_2 - a_2 b_1)\,\vec{e}_1 \times \vec{e}_2. \qedhere \end{align*} \end{proof} @@ -245,7 +245,7 @@ \section{欧氏空间上的运算} b_1 & b_2 & b_3 \\ c_1 & c_2 & c_3 \end{vmatrix} - [\vec{e}_1, \vec{e}_2, \vec{e}_3]. + [\vec{e}_1, \vec{e}_2, \vec{e}_3]. \qedhere \end{align*} \end{proof} @@ -489,7 +489,7 @@ \subsection{点到直线和平面的距离} \[ d(P, \Pi) = \frac{|\langle \vec{n}, \vec{p} \rangle + D|}{|\vec{n}|} - = \frac{|A x_0 + B y_0 + C z_0 + D|}{\sqrt{A^2 + B^2 + C^2}}. + = \frac{|A x_0 + B y_0 + C z_0 + D|}{\sqrt{A^2 + B^2 + C^2}}. \qedhere \] \end{proof} @@ -817,7 +817,7 @@ \section{仿射变换} \] 因而 $T$ 是线性映射. 于是存在矩阵 $A$ 使得 $T(x) = A x$,从而 \[ - F(x) = A x + \vec{b}. + F(x) = A x + \vec{b}. \qedhere \] \end{proof} diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/22 \344\272\214\346\254\241\345\236\213.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/22 \344\272\214\346\254\241\345\236\213.tex" index 1d2d7da..fc407eb 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/22 \344\272\214\346\254\241\345\236\213.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/22 \344\272\214\346\254\241\345\236\213.tex" @@ -521,7 +521,6 @@ \section{相合规范形 \quad 惯性定理} \] 这两者显然矛盾,所以 $p > k$ 不成立,必然有 $p \leqslant k$,同理可证 $k \leqslant p$,所以 $p = k$,也就有实对称矩阵的相合规范形唯一. - \end{proof} \begin{example}{}{} @@ -988,7 +987,7 @@ \subsection{与特征值有关的进一步结论} \] 当 $X$ 取 $A$ 对应于 $\lambda_1$ 或 $\lambda_n$ 的特征向量时,上下界分别取到,因此 \[ - \lambda_1=\max_{X\neq 0}R_A(X), \qquad \lambda_n=\min_{X\neq 0}R_A(X). + \lambda_1=\max_{X\neq 0}R_A(X), \qquad \lambda_n=\min_{X\neq 0}R_A(X). \qedhere \] \end{proof} @@ -1044,7 +1043,7 @@ \subsection{与特征值有关的进一步结论} \] 再取 $X=\xi_k$,便知这里的最小值恰为 $\lambda_k$. 因而 \[ - \max_{\dim U=k}\ \min_{0\neq X\in U}R_A(X)=\lambda_k. + \max_{\dim U=k}\ \min_{0\neq X\in U}R_A(X)=\lambda_k. \qedhere \] \end{proof} @@ -1107,7 +1106,7 @@ \subsection{与特征值有关的进一步结论} \] 由\nameref{thm:Courant-Fischer}, \[ - \gamma_{i+j-1}=\min_{\dim W'=n-(i+j-1)+1}\ \max_{0\neq X\in W'}R_{A+B}(X)\leqslant \alpha_i+\beta_j. + \gamma_{i+j-1}=\min_{\dim W'=n-(i+j-1)+1}\ \max_{0\neq X\in W'}R_{A+B}(X)\leqslant \alpha_i+\beta_j. \qedhere \] \end{proof} diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/24 \347\272\277\346\200\247\344\273\243\346\225\260\344\270\216\345\276\256\347\247\257\345\210\206.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/24 \347\272\277\346\200\247\344\273\243\346\225\260\344\270\216\345\276\256\347\247\257\345\210\206.tex" index 0334684..43479ae 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/24 \347\272\277\346\200\247\344\273\243\346\225\260\344\270\216\345\276\256\347\247\257\345\210\206.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/24 \347\272\277\346\200\247\344\273\243\346\225\260\344\270\216\345\276\256\347\247\257\345\210\206.tex" @@ -335,7 +335,7 @@ \section{多元函数微分学} 由于欧氏空间的紧子集都是有界闭的,所以我们知道 $Jf$ 在 $C$ 上一致连续,所以任给 $\varepsilon>0$,存在 $\delta>0$ 使得只要 $u',u\in C$ 且 $\Vert u'-u\Vert < \delta$,就有\[\Vert Jf(u') - Jf(u)\Vert < \varepsilon.\] 暂时固定 $u\in C$,当 $x\in D$ 时,记 $F(x) = f(x) - f(u) - J(u)(x - u)$,则 $JF(x) = Jf(x) - Jf(u)$,当 $v\in C$ 时,使用拟微分中值定理,有 \[\Vert F(v)\Vert = \Vert F(v) - F(u)\Vert \leqslant \Vert JF(\xi)\Vert\cdot \Vert v - u\Vert = \Vert Jf(\xi) - Jf(u)\Vert\cdot \Vert v - u\Vert.\] - 其中 $\xi = \theta u + (1-\theta)v$,$\theta\in (0, 1)$,由于 $Jf$ 在 $C$ 上一致连续,所以只要 $\Vert v - u\Vert < \delta$,就有 $\Vert \xi - u\Vert = \theta \Vert v - u\Vert < \delta$,所以\[\Vert F(v)\Vert = \Vert f(v) - f(u) - Jf(u)(v - u)\Vert \leqslant \varepsilon\Vert v - u\Vert.\] + 其中 $\xi = \theta u + (1-\theta)v$,$\theta\in (0, 1)$,由于 $Jf$ 在 $C$ 上一致连续,所以只要 $\Vert v - u\Vert < \delta$,就有 $\Vert \xi - u\Vert = \theta \Vert v - u\Vert < \delta$,所以\[\Vert F(v)\Vert = \Vert f(v) - f(u) - Jf(u)(v - u)\Vert \leqslant \varepsilon\Vert v - u\Vert. \qedhere\] \end{proof} 既然我们可以对一个函数的反函数求微分,回想起对于一元函数,如果它可微并且导数处处非零,那么该函数存在反函数并且反函数可微. 那么一个自然的问题就是:在什么样的条件下,多元向量值函数存在反函数并且反函数可微呢?这就是\term{反函数定理}. 在考虑反函数定理之前,我们先看一个小小的比喻. diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/6 \345\257\271\345\201\266\347\251\272\351\227\264.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/6 \345\257\271\345\201\266\347\251\272\351\227\264.tex" index e2600fe..22f3e74 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/6 \345\257\271\345\201\266\347\251\272\351\227\264.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/6 \345\257\271\345\201\266\347\251\272\351\227\264.tex" @@ -183,7 +183,7 @@ \section{对偶空间与对偶映射} \begin{proof} 对于类似形式(在几何中称为推出和拉回)的映射,我们证明等式的思路都是直接代入定义展开证明: \[ - (f \circ g)^* (\varphi) = \varphi \circ (f \circ g) = (\varphi \circ f) \circ g = (f^*(\varphi)) \circ g = g^* (f^*(\varphi)) = (g^* \circ f^*)(\varphi). + (f \circ g)^* (\varphi) = \varphi \circ (f \circ g) = (\varphi \circ f) \circ g = (f^*(\varphi)) \circ g = g^* (f^*(\varphi)) = (g^* \circ f^*)(\varphi). \qedhere \] \end{proof} @@ -199,7 +199,7 @@ \section{对偶空间与对偶映射} 我们直接按照定义展开验证即可: \begin{gather*} \sigma^*(f_1+f_2)=(f_1+f_2)\circ\sigma=f_1\circ\sigma+f_2\circ\sigma=\sigma^*(f_1)+\sigma^*(f_2) \\ - \sigma^*(\lambda f)=(\lambda f)\circ\sigma=\lambda(f\circ\sigma)=\lambda\sigma^*(f) + \sigma^*(\lambda f)=(\lambda f)\circ\sigma=\lambda(f\circ\sigma)=\lambda\sigma^*(f) \qedhere \end{gather*} \end{proof} diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/7 \347\272\277\346\200\247\346\230\240\345\260\204\347\237\251\351\230\265\350\241\250\347\244\272\344\270\216\347\237\251\351\230\265\350\277\220\347\256\227\345\237\272\347\241\200.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/7 \347\272\277\346\200\247\346\230\240\345\260\204\347\237\251\351\230\265\350\241\250\347\244\272\344\270\216\347\237\251\351\230\265\350\277\220\347\256\227\345\237\272\347\241\200.tex" index 8c7bde1..ea54b70 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/7 \347\272\277\346\200\247\346\230\240\345\260\204\347\237\251\351\230\265\350\241\250\347\244\272\344\270\216\347\237\251\351\230\265\350\277\220\347\256\227\345\237\272\347\241\200.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/7 \347\272\277\346\200\247\346\230\240\345\260\204\347\237\251\351\230\265\350\241\250\347\244\272\344\270\216\347\237\251\351\230\265\350\277\220\347\256\227\345\237\272\347\241\200.tex" @@ -374,7 +374,7 @@ \subsection{同构的说明} \begin{proof} 这一结论可以直接从交换图中得到. 事实上 $X = \varphi_{B_1}(\alpha)$,而 - \[Y = \varphi_{B_2}(\sigma(\alpha)) = \varphi_{B_2}(\varphi_{B_2}^{-1}(\sigma_\mathbf{M}(\varphi_{B_1}(\alpha)))) = \sigma_\mathbf{M}(\varphi_{B_1}(\alpha)) = \sigma_\mathbf{M}(X) = AX\] + \[Y = \varphi_{B_2}(\sigma(\alpha)) = \varphi_{B_2}(\varphi_{B_2}^{-1}(\sigma_\mathbf{M}(\varphi_{B_1}(\alpha)))) = \sigma_\mathbf{M}(\varphi_{B_1}(\alpha)) = \sigma_\mathbf{M}(X) = AX \qedhere\] \end{proof} 其中第二个等号来源于交换图的要求,最后一个等号因为 $A$ 是 $\sigma$ 在 $B_1$ 和 $B_2$ 下的矩阵表示,回忆矩阵表示的流程即得. 总结一下,\autoref{thm:线性映射对向量坐标的影响} 表明,$\sigma(\alpha) = \beta$ 中 $\beta$ 和 $\alpha$ 坐标间的关联为 $Y = AX$,这就相当于在 $\mathbf{F}^n$ 和 $\mathbf{F}^m$ 中向量之间建立了一个与 $\sigma: V_1 \to V_2$同步的映射 $X \mapsto \mathbf{M}(\sigma)X$,每当$V$中向量经过 $\sigma$ 映射后,它的坐标也就经过了 $\mathbf{M}(\sigma)$ 的映射. diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/8 \347\233\270\346\212\265\346\240\207\345\207\206\345\275\242.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/8 \347\233\270\346\212\265\346\240\207\345\207\206\345\275\242.tex" index 9d823d3..70c26c5 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/8 \347\233\270\346\212\265\346\240\207\345\207\206\345\275\242.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/8 \347\233\270\346\212\265\346\240\207\345\207\206\345\275\242.tex" @@ -233,7 +233,7 @@ \section{过渡矩阵和基变换} \begin{align*} \mathbf{M}_{B_1',B_2'}(\sigma) &= \mathbf{M}_{B_2,B_2'}(I)\mathbf{M}_{B_1',B_2}(\sigma) \\ &= \mathbf{M}_{B_2,B_2'}(I)\mathbf{M}_{B_1,B_2}(\sigma)\mathbf{M}_{B_1',B_1}(I) \\ - &= Q^{-1} \mathbf{M}_{B_1,B_2}(\sigma)P + &= Q^{-1} \mathbf{M}_{B_1,B_2}(\sigma)P \qedhere \end{align*} \end{proof} @@ -912,7 +912,7 @@ \section{相抵标准形的应用} 从而 \[A^{2}=\widetilde{P} \widetilde{Q} \widetilde{P} \widetilde{Q}=\tr(A) \widetilde{P} \widetilde{Q}=\tr(A) A,\] 进一步地 - \[A^{k}=\widetilde{P} \widetilde{Q} \widetilde{P} \widetilde{Q} \cdots \widetilde{P} \widetilde{Q}=\tr(A)^{k-1} \widetilde{P} \widetilde{Q}=\tr(A)^{k-1} A.\] + \[A^{k}=\widetilde{P} \widetilde{Q} \widetilde{P} \widetilde{Q} \cdots \widetilde{P} \widetilde{Q}=\tr(A)^{k-1} \widetilde{P} \widetilde{Q}=\tr(A)^{k-1} A. \qedhere\] \end{proof} 事实上,本题的解答过程给我们了一个很重要的启示,那就是秩为1的矩阵一定可以分解为一个列向量和一个行向量的乘积. 之后我们将利用这一结论来解决一些问题,例如便于矩阵求幂等. 此外,若 $\tr(A) = 1$,则 $A^k = A$,这表明秩为 1 且迹为 1 的矩阵一定是幂等矩阵. diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/9 \347\237\251\351\230\265\350\277\220\347\256\227\350\277\233\351\230\266.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/9 \347\237\251\351\230\265\350\277\220\347\256\227\350\277\233\351\230\266.tex" index 53cb78d..9855783 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/9 \347\237\251\351\230\265\350\277\220\347\256\227\350\277\233\351\230\266.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/9 \347\237\251\351\230\265\350\277\220\347\256\227\350\277\233\351\230\266.tex" @@ -440,7 +440,7 @@ \subsection{提逆思想} \begin{proof} 因为没有关于矩阵减法的逆的公式,$(E - A^{-1})^{-1}$ 无法直接化简,我们可以考虑将其提逆,提出一个 $A^{-1}$:$(E-A^{-1})^{-1} = (A^{-1}(A-E))^{-1} = (A-E)^{-1}A$,从而 - \[(E-A^{-1})^{-1}+(E-A)^{-1}=(A-E)^{-1}A+(E-A)^{-1}=(E-A)^{-1}(E-A)=E.\] + \[(E-A^{-1})^{-1}+(E-A)^{-1}=(A-E)^{-1}A+(E-A)^{-1}=(E-A)^{-1}(E-A)=E. \qedhere\] \end{proof} 最后,除了上述介绍的方法之外,在接下来介绍了分块矩阵初等变换之后,我们还会介绍其它与逆矩阵相关的一些技巧. @@ -1258,7 +1258,7 @@ \section{秩等式与不等式} \end{pmatrix}=r\begin{pmatrix} O & -AB \\ BC & B \end{pmatrix} \\ - & \geqslant r(AB)+r(BC). + & \geqslant r(AB)+r(BC). \qedhere \end{align*} \end{proof} From 47df3be2e4c73c54cdc4a241299a0da353def686 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Fri, 18 Sep 2026 23:17:02 +0800 Subject: [PATCH 18/27] docs(document): update title and author MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新封面标题、作者说明及 PDF 标题元数据。 --- "\350\256\262\344\271\211/LALU.tex" | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git "a/\350\256\262\344\271\211/LALU.tex" "b/\350\256\262\344\271\211/LALU.tex" index 3cf8196..e9bd208 100644 --- "a/\350\256\262\344\271\211/LALU.tex" +++ "b/\350\256\262\344\271\211/LALU.tex" @@ -832,13 +832,13 @@ subsubsection={format={\raggedright\normalsize\bfseries},name={,},number={\thesubsection.\arabic{subsubsection}}}, } -\title{\heiti 浙江大学 2023--2024 学年 \\ 线性代数荣誉课辅学讲义} -\author{2023--2024 学年线性代数 I/II(H)辅学授课 \\ 吴一航 \quad \verb|yhwu_is@zju.edu.cn|} +\title{线性代数:未竟之美} +\author{线性代数荣誉课辅学讲义 \\ 吴一航 \quad \verb|yhwu_is@zju.edu.cn|} \AtEndPreamble{\hypersetup{ hypertexnames=true, pdfauthor={吴一航}, - pdftitle={线性代数荣誉课辅学讲义}, + pdftitle={线性代数:未竟之美}, }} \begin{document} From 787ce76b2193ba6a852699c1e446bcf109fa2b97 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Fri, 18 Sep 2026 23:17:53 +0800 Subject: [PATCH 19/27] ci: upgrade build toolchain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用 TeX Live 2025 historic 镜像,并将 checkout 与 artifact upload actions 更新至 v7;现有输入在新版本中保持兼容。 --- .github/workflows/tex.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tex.yml b/.github/workflows/tex.yml index c323bb0..a327b96 100644 --- a/.github/workflows/tex.yml +++ b/.github/workflows/tex.yml @@ -16,11 +16,11 @@ jobs: name: Build document runs-on: ubuntu-latest container: - image: registry.gitlab.com/islandoftex/images/texlive:TL2023-historic + image: registry.gitlab.com/islandoftex/images/texlive:TL2025-historic steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Build id: build @@ -31,7 +31,7 @@ jobs: make - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: LALU-build-${{ github.run_number }}-${{ steps.build.outputs.sha }} if-no-files-found: error From 6140b4377946e01113614f1dc8d7d46eadf768da Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Fri, 18 Sep 2026 23:21:11 +0800 Subject: [PATCH 20/27] fix(document): move Chinese punctuation out of math --- .../2011-2012-1final-answer.tex" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/\350\256\262\344\271\211/\345\216\206\345\271\264\345\215\267/2011-2012-1final-answer.tex" "b/\350\256\262\344\271\211/\345\216\206\345\271\264\345\215\267/2011-2012-1final-answer.tex" index 472c943..b034897 100644 --- "a/\350\256\262\344\271\211/\345\216\206\345\271\264\345\215\267/2011-2012-1final-answer.tex" +++ "b/\350\256\262\344\271\211/\345\216\206\345\271\264\345\215\267/2011-2012-1final-answer.tex" @@ -9,7 +9,7 @@ \section{2011-2012学年线性代数I(H)期末答案} $A(1,-1,0)^\mathrm{T}=(2, a-b, d-e)^\mathrm{T}=\lambda_3(1,-1,0)^\mathrm{T}\implies a-b=-2, d=e$. - 综合上述结果可知 $a=c=-\dfrac{1}{3}, b=\dfrac{5}{3}, d=e=f=\dfrac{1}{3}$. + 综合上述结果可知 $a=c=-\dfrac{1}{3}$,$b=\dfrac{5}{3}$,$d=e=f=\dfrac{1}{3}$. \item \begin{enumerate} From 4ceb42497f63af269424ed229d52ff2d8027f9f9 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Fri, 18 Sep 2026 23:29:54 +0800 Subject: [PATCH 21/27] refactor(template): make preamble finalization explicit --- "\350\256\262\344\271\211/LALU-answers.tex" | 11 +++++---- "\350\256\262\344\271\211/LALU.tex" | 5 ++-- "\350\256\262\344\271\211/LALUbook.cls" | 26 ++++++++++++++------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git "a/\350\256\262\344\271\211/LALU-answers.tex" "b/\350\256\262\344\271\211/LALU-answers.tex" index ad479aa..998e8cc 100644 --- "a/\350\256\262\344\271\211/LALU-answers.tex" +++ "b/\350\256\262\344\271\211/LALU-answers.tex" @@ -13,11 +13,6 @@ \title{\heiti 浙江大学 2023--2024 学年 \\ 线性代数荣誉课辅学习题答案} \author{2023--2024 学年线性代数 I/II(H)辅学授课 \\ 吴一航 \quad \verb|yhwu_is@zju.edu.cn|} -\AtEndPreamble{\hypersetup{ - pdfauthor = {吴一航}, - pdftitle = {线性代数荣誉课辅学习题答案}, -}} - % The main document publishes this generated manifest only after a successful % build. Keeping the path overrideable makes out-of-tree builds possible. \providecommand*{\LALUAnswerManifestPath}{build/answers/LALU.manifest.tex} @@ -335,6 +330,12 @@ { \lalu_answer_input_manifest: } \ExplSyntaxOff +\LALUFinalizePreamble +\hypersetup{ + pdfauthor = {吴一航}, + pdftitle = {线性代数荣誉课辅学习题答案}, +} + \begin{document} \frontmatter diff --git "a/\350\256\262\344\271\211/LALU.tex" "b/\350\256\262\344\271\211/LALU.tex" index e9bd208..821e400 100644 --- "a/\350\256\262\344\271\211/LALU.tex" +++ "b/\350\256\262\344\271\211/LALU.tex" @@ -835,11 +835,12 @@ \title{线性代数:未竟之美} \author{线性代数荣誉课辅学讲义 \\ 吴一航 \quad \verb|yhwu_is@zju.edu.cn|} -\AtEndPreamble{\hypersetup{ +\LALUFinalizePreamble +\hypersetup{ hypertexnames=true, pdfauthor={吴一航}, pdftitle={线性代数:未竟之美}, -}} +} \begin{document} \frontmatter diff --git "a/\350\256\262\344\271\211/LALUbook.cls" "b/\350\256\262\344\271\211/LALUbook.cls" index 85414b7..22f5791 100644 --- "a/\350\256\262\344\271\211/LALUbook.cls" +++ "b/\350\256\262\344\271\211/LALUbook.cls" @@ -206,7 +206,15 @@ \newcommand{\Set}{\mathsf{Set}} \newcommand{\FSet}{\mathsf{FSet}} -\AtEndPreamble{ +% 文档入口在自身的宏包和定义之后显式调用此命令,以保持 +% hyperref/cleveref 的载入次序,而不依赖隐式的 begin-document 钩子。 +\newif\ifLALU@preamble@finalized +\newcommand*{\LALUFinalizePreamble}{% + \ifLALU@preamble@finalized + \ClassError{LALUbook}{Preamble already finalized}% + {Call \string\LALUFinalizePreamble\space exactly once, after document-specific packages and definitions.}% + \else + \global\LALU@preamble@finalizedtrue \RequirePackage[Bjornstrup]{fncychap} % found on Internet % hyperref 与 cleveref 需要最后引入 @@ -226,9 +234,9 @@ } \NewCommandCopy{\LALU@hyperref@autoref}{\autoref} \RenewDocumentCommand{\autoref}{s m}{% - \IfBooleanTF{#1} - {\LALU@hyperref@autoref*{#2}} - {\LALU@hyperref@autoref{#2}}% + \IfBooleanTF{##1} + {\LALU@hyperref@autoref*{##2}} + {\LALU@hyperref@autoref{##2}}% \xspace } @@ -263,13 +271,13 @@ \renewcommand{\chapterautorefname}{章} \renewcommand{\sectionautorefname}{节} % https://tex.stackexchange.com/a/2244/316373 - \makeatletter \renewcommand*\env@matrix[1][*\c@MaxMatrixCols c]{% \hskip -\arraycolsep \let\@ifnextchar\new@ifnextchar - \array{#1}} - \makeatother + \array{##1}} - \crefrangeformat{equation}{式~#3#1#4--#5#2#6} - \crefrangeformat{example}{例~#3#1#4--#5#2#6} + \crefrangeformat{equation}{式~##3##1##4--##5##2##6} + \crefrangeformat{example}{例~##3##1##4--##5##2##6} + \fi } +\@onlypreamble\LALUFinalizePreamble From a157e8f611ceb593bf3925610206637d6886a077 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Fri, 18 Sep 2026 23:29:54 +0800 Subject: [PATCH 22/27] build: harden latexmk invocation --- "\350\256\262\344\271\211/Makefile" | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git "a/\350\256\262\344\271\211/Makefile" "b/\350\256\262\344\271\211/Makefile" index 4fe340b..632c3f4 100644 --- "a/\350\256\262\344\271\211/Makefile" +++ "b/\350\256\262\344\271\211/Makefile" @@ -1,7 +1,12 @@ MAIN_JOB := LALU LATEXMK := latexmk -LATEXMK_FLAGS := -xelatex -shell-escape -interaction=nonstopmode -file-line-error +LATEXMK_FLAGS := \ + -xelatex \ + -interaction=nonstopmode \ + -halt-on-error \ + -file-line-error \ + -no-shell-escape ANSWER_BUILD_DIR := build/answers ANSWER_MANIFEST := $(ANSWER_BUILD_DIR)/$(MAIN_JOB).manifest.tex ANSWER_PUBLISHER := scripts/publish-answer-manifest.pl From b31f0ca4d494c0b96f50a2ec280d62e6e8b19c77 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Fri, 18 Sep 2026 23:29:54 +0800 Subject: [PATCH 23/27] docs(template): audit source cleanup patterns --- docs/template-refactor-plan.md | 72 +++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/docs/template-refactor-plan.md b/docs/template-refactor-plan.md index 5cf45cf..15d793d 100644 --- a/docs/template-refactor-plan.md +++ b/docs/template-refactor-plan.md @@ -4,7 +4,7 @@ ## 本轮范围与兼容边界 -本轮以仓库正文(`讲义/专题/`、`讲义/历年卷/`、`讲义/其它/`)和最终 PDF 为兼容边界。`LALUbook.cls`、主文件 `讲义/线性代数荣誉课辅学讲义.tex`、答案模板 `讲义/LALU-answers.tex` 中仅用于支撑实现的 counter、helper、marker、生成文件协议和 href anchor **不是兼容接口**;只要正文无需改写、既有标签/引用语义和 PDF 可见行为不变,它们可以直接删除、改名或换成新 schema,不设过渡包装器。 +本轮以仓库正文(`讲义/专题/`、`讲义/历年卷/`、`讲义/其它/`)和最终 PDF 为兼容边界。`讲义/LALUbook.cls`、主文件 `讲义/LALU.tex`、答案模板 `讲义/LALU-answers.tex` 中仅用于支撑实现的 counter、helper、marker、生成文件协议和 href anchor **不是兼容接口**;只要正文无需改写、既有标签/引用语义和 PDF 可见行为不变,它们可以直接删除、改名或换成新 schema,不设过渡包装器。 本轮状态如下: @@ -644,3 +644,73 @@ rg -n -U --pcre2 --glob '*.tex' '(?:\\\]|\\end\{(?:equation\*?|align\*?|alignat\ 17. 无普通章、错误 phase、编号被抑制或希腊 slot 超界时,`\LUchapter` 都在改变任何 counter、当前记录或样式前失败;修正输入后可继续构建,不受半初始化状态污染。 18. frontmatter、mainmatter 首章之前、backmatter、星号章、appendix 入口空隙及首阶段的附录章都不会把习题误挂到上一个正文/LU 单元;不支持的附录答案以明确错误结束。 19. 主讲义和答案册的可见文本、章节/答案顺序、空答案项、无答案单元及分页与冻结基线一致;允许内部 counter、manifest 命令和 href destination 名称变化。 + +## 七、`patterns.re` 历史正则审计 + +`patterns.re` 是历次人工校对留下的搜索/替换便笺,不是可以按文件顺序无审查执行的 formatter。本节只记录各组规则的适用场景和后续处置;本轮不执行这些替换,不修改 `patterns.re`,也不据此批量修改正文。 + +这些规则有两项共同限制。第一,反复使用的 `[\u10ff-\ufeff]` 并不是中文字符类,而是覆盖 U+10FF--U+FEFF 间多种文字、数学符号、标点和私用区的宽范围。第二,正则不能解析 TeX 的嵌套花括号、注释、数学模式或 verbatim 类环境,而其中多处 `.+` 又是贪婪匹配。新增或删除空行还可能产生或移除 `\par`,从而影响列表间距、首段行为和答案正文。因此,后续即使采用某条规则,也应把它改成只报告候选位置的 lint,逐类 review、提交和编译验证。 + +### 7.1 `line wrap` + +- `([…])\n *([…]) -> $1$2` 用于删除中文文字、标点、`$`、`=` 等字符之间的硬换行;其范围也会命中对齐公式和 TikZ 坐标,甚至可能把相邻两行的 `$` 拼成 `$$`,不能全仓替换。 +- 引用命令后换行、句点后换行、数字后换行,以及正文换行后接非 `begin`/`end`/`item` 命令的几条无 replacement 规则,意在定位被折行的连续语句。这些查询的命令名和参数匹配都很宽,且会命中多行公式,应只作为人工搜索提示。 +- `\.\n *([…]) -> . $1` 只是把单换行规范成一个源码空格;TeX 原来的单换行通常已经等价于空格,而后继为标点或数学符号时,这一替换反而可能制造错误间距。 + +归类:源码 lint/人工排版检查,不进入模板层,也不作为自动修复。 + +### 7.2 `item wrap` + +- `\item` 单独占行后接正文的规则把首段移到同一源码行,通常只是风格调整,但不覆盖以命令、花括号或可选标签开始的 item。 +- 连续 `\item`、环境结束后接 `\item`、显示公式后接 `\item` 等规则会插入空行;`enumerate` 后首项以及空 `\item` 后接环境的规则又会删除换行或把两行合并。它们互相重叠并具有顺序依赖。 +- 在 list 中插入空行会产生段落边界,可能改变 `\parsep`;把空项与下一 `\item` 合并还会生成难以辨认的 `\item \item`,掩盖源文件错误。 + +归类:如需统一源码风格,优先配置 `latexindent`;涉及段落边界的候选仍须人工 review,不批量修改正文。 + +### 7.3 `dot spacing` + +- `\.([\u10ff-\ufeff]) -> . $1` 主要针对 `.故`、`.因此` 一类西文句点后紧接中文正文的情况,仓库中确有此类实例;但宽字符范围也包含中文标点和数学符号,可能在标点前或数学式内错误插空格。 +- `\.\n *(\d) -> . $1` 将句点、换行、数字整理为同一行;原换行通常已经提供空格,且该规则无法判断是否遇到被错误拆行的小数。 + +归类:只报告“句点后直接接 Han 字符”的窄 lint,并排除数学、注释、verbatim 和标点;不使用现有宽范围直接替换。 + +### 7.4 `ref spacing` + +搜索式试图覆盖 `\ref`、`\autoref`、`\eqref`、`\pageref`、`\nameref` 等 `*ref` 命令,并用 `[^h]` 避开 `\href`/`\hyperref`。其记录的 replacement 按文件内容实际只有 `$1`,没有尾随空格,因此当前是 no-op。即使补上空格也不正确:引用后是 `,`、`)` 等标点时不应插入;`\autoref` 常以数字结束而需要中西文间距,`\nameref` 却可能展开为中文标题并直接连接“一章”等中文文字。 + +归类:保留现有模板中针对 `\autoref` 的后继 token 分类。若以后扩展到 `\eqref`、`\pageref` 等,应按各命令的可见语义逐个设计并复用统一分类器,不能由这条正则推导所有 `*ref` 的行为。 + +### 7.5 `section spacing` + +- 第一条规则在 `section`、`subsection`、`subsubsection` 前后增加源码空行;它会把标题前的注释行也当作正文,且不支持星号形式、可选短标题或多行标题。 +- 第二条规则只删除标题后下一命令前的缩进;当前典型场景是 `\section` 后立即跟 `\subsection`,属于源码风格而非模板语义。 +- 标题后人为加入空段落可能干扰首段处理,不能用源码空行控制最终版式。 + +归类:formatter/人工风格检查,不进入模板,也不自动改正文。 + +### 7.6 `displaymath` + +- 第一条无 replacement 的跨行规则主要定位 `\[` 后第一行已经有公式内容的 display,供人工统一为开闭定界符独占一行;它仍可能跨过非预期内容,只能作为搜索器。 +- `$$...$$ -> \[...\]` 的迁移方向正确,因为前者是 plain TeX 写法;当前候选集中在旧历年卷答案。不过 `[\w\W]` 会跨任意内容,不能排除注释、verbatim 或转义美元符号。 + +归类:用 ChkTeX 等 lint 报告 `$$`,再逐处迁移和编译;delimiter 的换行风格交给 formatter。 + +### 7.7 `matrices` + +两条配套规则把 `\left|\begin{array}{...} ... \end{array}\right|` 改成 `vmatrix`,只在竖线确实表示行列式、列格式只是普通居中列时等价。开闭两侧独立替换可能产生不配对环境;opening 中的 `\{.+\}` 还可能吞掉同行后续花括号。`array` 的 `l`/`r` 对齐、列间竖线和 `@{...}` 等信息也会在转换后丢失。当前仓库没有精确命中,属于历史规则或已完成的清理。 + +归类:只对完整且已确认等价的一对 delimiter 做人工语义迁移,不保留为全局替换。 + +### 7.8 `dots` + +`(,\s*)\cdots(\s*,) -> $1\ldots$2` 用于把逗号分隔列表中的居中省略号改为低省略号,符合数学排版惯例;也可考虑使用由 amsmath 根据上下文选择的 `\dots`。规则不判断数学模式,`\s*` 又可以跨行,因此仍可能进入注释、宏定义或 verbatim。 + +归类:这是相对高置信度但仍需逐处 review 的语义 lint;若实施,应作为独立正文清理提交并检查公式输出。 + +### 7.9 proof 搜索 + +最后的 `(?!\n) *\\begin\{proof\}` 使用了负向前瞻,检查的是“当前位置的下一个字符不是换行”,而不是“前一个字符不是换行”。引擎可以直接从 `\begin` 的反斜杠开始并让空格匹配零次,所以它实际上等价于搜索全部 proof:当前两种搜索都得到 399 个字面 occurrence,其中 3 个位于注释,实际 active proof 为 396 个。 + +若原意是寻找没有独占一行的 proof,应改为按行锚定、要求 proof 前已有非空白内容并排除注释的查询,例如 `(?m)^(?![ \t]*%)[ \t]*\S.*?\\begin\{proof\}`。这只是源码布局检查,与 proof/QED 的模板语义无关。 + +总体上,`line wrap`、`item wrap`、`dot spacing`、`section spacing`、第一条 `displaymath` 和 proof 行布局属于 lint/格式化候选;`$$`、行列式 array 和逗号列表中的 `\cdots` 属于需单独审核的语义迁移;只有引用后的中文间距属于模板层问题。现有 `patterns.re` 不应成为构建步骤或一次性批处理脚本。 From 4ceac7cf4052963683e5a5d63cba61fdaf7e33c8 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Sat, 19 Sep 2026 00:05:39 +0800 Subject: [PATCH 24/27] ci: remove workspace path for artifact upload --- .github/workflows/tex.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tex.yml b/.github/workflows/tex.yml index a327b96..742f82e 100644 --- a/.github/workflows/tex.yml +++ b/.github/workflows/tex.yml @@ -36,5 +36,5 @@ jobs: name: LALU-build-${{ github.run_number }}-${{ steps.build.outputs.sha }} if-no-files-found: error path: | - ${{ github.workspace }}/讲义/LALU.pdf - ${{ github.workspace }}/讲义/LALU-answers.pdf + 讲义/LALU.pdf + 讲义/LALU-answers.pdf From 14ea5d615acc4f786cb616e7e26731e2521a3787 Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Sat, 19 Sep 2026 00:13:21 +0800 Subject: [PATCH 25/27] docs(document): fix index and remove factor plan from worktree --- .gitignore | 6 +- docs/template-refactor-plan.md | 716 ------------------ ...7\346\200\247\346\230\240\345\260\204.tex" | 2 +- ...0\347\256\227\345\237\272\347\241\200.tex" | 2 +- 4 files changed, 4 insertions(+), 722 deletions(-) delete mode 100644 docs/template-refactor-plan.md diff --git a/.gitignore b/.gitignore index 6c9f67b..375ac0b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,8 @@ 讲义/*.pdf 习题参考答案/*.pdf -build -gh-actions-build -讲义/LALU-ans-contents.tex -习题参考答案/LALU-answers.tex +build/ +docs/ ## latexindent.pl backup files *.bak* diff --git a/docs/template-refactor-plan.md b/docs/template-refactor-plan.md deleted file mode 100644 index 15d793d..0000000 --- a/docs/template-refactor-plan.md +++ /dev/null @@ -1,716 +0,0 @@ -# LALU 模板重构方案 - -> 状态:本轮实施与迁移记录。本文同时记录已落地的内部重构和明确延后的提案;“后续提案”不构成本轮验收要求。 - -## 本轮范围与兼容边界 - -本轮以仓库正文(`讲义/专题/`、`讲义/历年卷/`、`讲义/其它/`)和最终 PDF 为兼容边界。`讲义/LALUbook.cls`、主文件 `讲义/LALU.tex`、答案模板 `讲义/LALU-answers.tex` 中仅用于支撑实现的 counter、helper、marker、生成文件协议和 href anchor **不是兼容接口**;只要正文无需改写、既有标签/引用语义和 PDF 可见行为不变,它们可以直接删除、改名或换成新 schema,不设过渡包装器。 - -本轮状态如下: - -| 子系统 | 本轮状态 | 兼容要求 | -| --- | --- | --- | -| 定理与证明 | 已切换到集中声明的 tcolorbox theorem + amsthm proof;已删除 proof/solution 未使用的第二个原始 tcolorbox 选项 | 六类定理的双参数接口、标签前缀、分别计数、proof/solution 第一可选说明及可见 QED/盒子行为不变 | -| 正文/未竟专题章节状态 | 已切换到显式状态机并完成 fixture 与整书回归 | `\chapter`、`\LUchapter`、中文标题编号、“上一正文章节 + 希腊字母”的引用编号、目录和 PDF 可见版式不变;旧 counter alias、开放分组和 marker 无需保留 | -| 答案生成与答案册 reader | 已切换到带版本的语义 manifest、独立 reader 和 staging 发布 | 正文继续使用普通 `answer` 环境;答案项、空答案、无答案单元及其既有分页在本轮保持可见行为不变;旧生成文件语法与路径无需兼容 | -| 稳定题目 ID、file payload、空单元省略策略及其余条件性变化 | 延后 review | 本轮不要求“在前面插题后已有 ID 不变”,不引入需要正文改写的新答案接口,也不启用其他条件性 breaking change | - -这里的“正文无需改写”只约束作者源文件的既有公共调用。干净重编译、删除旧辅助文件、更新 Makefile 以及同步修改上述三份 supporting structure 都属于本轮正常迁移,不算正文 breaking change。 - -## 目标与不变量 - -本次重构的目标不是改变讲义的编排,而是把目前依赖跨章节分组、动态控制序列标记和隐式文件副作用的实现,替换成可检查、可测试的显式状态模型。 - -必须保持以下用户可见约定: - -- 普通正文继续使用 `\chapter`,显示为“第 N 讲”。 -- 未竟专题继续使用 `\LUchapter{标题}`;标题编号显示为“未竟专题 X”,其中 `X` 是未竟专题的全局中文序号。 -- 未竟专题的数字引用继续由“上一普通正文编号 + 希腊字母”组成。例如第一讲后的前两个未竟专题分别引用为 `1ε`、`1δ`。 -- 未竟专题内的节、公式、图表和定理继续沿用该数字引用,例如 `1ε.2`。 -- 正文和答案册中的既有标签键、引用结果、目录层级、内容、顺序及分页应保持;内部辅助文件格式可改变,但必须通过干净重编译得到一致产物。 -- 所有 Hyperref destination 必须在单次产物中唯一、确定且只含适合机器处理的 ASCII 内容;显示编号不得直接充当可能冲突的锚点编号。旧 PDF 的原始 href 名称不属于本轮兼容承诺。 - -以下三个概念应始终分离: - -| 概念 | 示例 | 用途 | -| --- | --- | --- | -| 标题显示编号 | `未竟专题十四` | 章标题、答案册单元标题、目录章条目 | -| 数字引用编号 | `23ε` | `\ref`、节/公式/定理的父编号 | -| 机器锚点编号 | `chapter.LU.14` | Hyperref destination(内部实现,可改名,不承诺旧 PDF 深链接) | - -## 一、正文、未竟专题与答案册单元 - -### 1.1 统一的章节记录 - -正文和答案抽取器共享一个只读的“当前章节记录”。建议由一个内部模块维护下列字段,而不是通过 `\@exist@...` 控制序列是否存在来反推状态: - -| 字段 | 含义 | 示例 | -| --- | --- | --- | -| `phase` | `frontmatter`、`mainmatter`、`appendix` 或 `backmatter` | `mainmatter` | -| `kind` | `normal`、`unfinished`、`appendix` 或 `unnumbered` | `unfinished` | -| `structural-chapter` | 标准 `c@chapter` 的当前结构流水号 | `37` | -| `normal-display` | 普通正文的显示编号 | `23` | -| `normal-base` | 最近一个普通正文的阿拉伯数字编号 | `23` | -| `unfinished-serial` | 未竟专题的全局流水号 | `14` | -| `unfinished-slot` | 同一普通正文之后的未竟专题次序 | `1` | -| `unnumbered-serial` | 实际无编号顶层标题的内部流水号,仅用于必要的唯一锚点 | `3` | -| `title` | 完整标题,保持未展开 token | `范畴论视角下的线性代数` | -| `short-title` | 页眉、目录所用短标题;缺省时等于完整标题 | 同上 | -| `unit-id` | 当前 manifest 内唯一的逻辑单元标识 | `lu:14` | - -实现上可使用 expl3 的全局 token list、integer 和 boolean;对外只暴露查询命令,不允许正文直接改内部变量。至少提供以下语义接口: - -- 进入普通正文单元; -- 进入未竟专题单元; -- 进入无编号章节边界; -- 取得当前记录的不可变快照,供答案抽取器使用; -- 判断当前是否存在可承载习题的编号单元。 - -状态转换必须幂等。“离开未竟专题”调用两次不应产生额外 `\endgroup`、计数器变化或格式变化。未竟专题不得通过一个跨越整章的未闭合 TeX 分组维持状态。 - -### 1.2 正文计数器与转换规则 - -首选实现应把“结构上的顶层单元次序”和“普通正文显示编号”彻底分离,不再把标准 `chapter` 先减一再让 `\chapter` 加回。各计数器的职责为: - -| 计数器 | 职责 | 是否直接显示 | -| --- | --- | --- | -| 标准 `chapter`(即 `c@chapter`) | 所有**有编号顶层单元**的结构流水号;驱动标准 reset 链和 package hook | 否 | -| 新增 `LALUnormalchapter` | 只在普通正文开始时递增,表示“第 N 讲”中的 `N` | 是 | -| 内部 unfinished serial | 只在未竟专题开始时递增,表示“未竟专题 X”中的全局序号 | 是 | -| 内部 unfinished slot | 同一普通正文之后的未竟专题次序 | 通过希腊字母显示 | -| 新增 `LALUappendixchapter` | 附录显示编号 `A`、`B`…… | 仅 appendix 模式 | -| 内部 `LALUunnumberedunit` | 每个实际无编号顶层标题的锚点流水号;阶段命令本身不递增 | 否 | - -因此,普通编号章、未竟专题和编号附录都必须调用最终的标准 `\chapter`。每次这样的编号调用都会使标准 `c@chapter` 自然递增一次,LaTeX 注册在 chapter 下的 section、equation、figure、table、定理等 reset 以及 Hyperref/CTeX/其他 package hook 也自然执行一次。结构 counter 不再为了显示编号而回退、复用或伪造数值。 - -普通 `\chapter` 适配器必须先按 phase 和标准编号判定分派:mainmatter 的编号章走 normal 路径,appendix 的编号章走 appendix 路径,frontmatter/backmatter 中的章以及被 `secnumdepth` 抑制编号的章走 unnumbered 路径。星号形式始终走 unnumbered 路径;`\LUchapter` 则是只允许 mainmatter 编号路径的独立入口。这样同一个公共 `\chapter` 不会被“普通章流程只接受 mainmatter”的前置条件误伤。 - -普通章的编号路径为: - -1. 确认当前处于 `mainmatter`,并采用与标准 chapter 相同的“本次是否编号”判定。若受 `secnumdepth` 等设置影响而不编号,则不得改动任何显示计数器,而应按无编号边界更新状态后调用标准章命令。 -2. 仅在确定编号后设置 `kind=normal`,递增 `LALUnormalchapter`,把该值写入当前记录的 `normal-display` 和 `normal-base`,并把 LU slot 重置为 0。 -3. 调用最终标准 `\chapter`;它递增结构 `c@chapter` 并执行全部标准 reset/hook。 -4. `\thechapter` 在本次调用期间从 `LALUnormalchapter` 取显示值,而不是从 `\arabic{chapter}` 取值。 - -未竟专题转换流程为: - -1. 要求当前处于 `mainmatter`、已经存在普通正文,并且标准 chapter 判定本次会编号;任一条件不满足都必须在修改任何计数器或样式前给出明确的 package error,而不是生成 `0ε` 或一个半初始化单元。 -2. 将当前普通正文计数快照为 `normal-base`,设置 `kind=unfinished`,并分别递增内部的全局 unfinished serial 与当前普通章的 unfinished slot。普通章入口已经把 slot 归零,因此这里不再覆盖 `normal-base` 后做含义不清的“是否同章”比较。 -3. 调用同一个最终标准 `\chapter`;结构 `c@chapter` 再递增一次并执行全部标准 reset/hook。 -4. `\thechapter` 在本次调用及其正文期间返回保存的 `normal-base + Greek(slot)`;章标题的中文序号则来自内部 unfinished serial。 - -`\chapter*` 结束当前编号单元但不清除最近普通章及其 LU slot。因而随后在 `mainmatter` 中再次调用 `\LUchapter` 是允许的,并继续使用下一个 slot;例如 `chapter A -> LU B -> chapter* -> LU C` 的两个 LU 引用仍依次为 `1ε`、`1δ`。如果希望星号章成为禁止后续 LU 的更强边界,应另设显式命令,不能通过重置 slot 得到可能重复的编号。 - -appendix 中的编号章由同一个 `\chapter` 适配器按 `phase` 分派: - -1. 确认当前处于 `appendix` 且标准 chapter 判定本次会编号。 -2. 设置 `kind=appendix`,递增 `LALUappendixchapter`,并记录本次附录显示字母。 -3. 调用同一份最终标准 `\chapter`,让结构 `c@chapter` 递增并执行标准 reset/hook。 -4. 本次调用及其正文中的 `\thechapter` 返回 `\Alph{LALUappendixchapter}`,锚点进入 `APP` 命名空间。该路径不得递增或重置普通正文、LU、slot 计数器。 - -unnumbered 路径在调用最终标准章命令前设置 `kind=unnumbered`、清空当前 `unit-id`,并为每个实际排出的无编号顶层标题递增一次内部 `LALUunnumberedunit`。它保留最近普通章、LU slot 和所有显示计数器,不递增结构 `chapter`,也不伪造编号章的 reset;仅 phase 命令本身不算一个无编号标题,不能递增该内部 serial。 - -以上路径都必须“先验证、后变更”;验证失败不得遗留已经递增的逻辑计数器、切换一半的 CTeX 样式或不完整的当前记录。 - -例如 `chapter A -> LU B -> LU C -> chapter D` 完成后,结构 `c@chapter` 依次为 1、2、3、4,普通显示计数器依次为 1、1、1、2;可见引用依次为 `1`、`1ε`、`1δ`、`2`。 - -现有希腊字母序列的前六项必须保持为: - -1. `ε` -2. `δ` -3. `λ` -4. `μ` -5. `φ`(正文仍使用当前 `\varphi` 字形) -6. `θ` - -该序列应由一张集中配置表生成正文形式和 PDF 字符串形式。超过已配置容量时应给出带有当前普通正文编号和所需 slot 的明确错误;如需扩展,只追加映射,不得改变既有六项顺序。 - -普通 `\chapter` 和 `\chapter*` 需要经过类在导言区末尾安装的完整签名适配器,或等价且经过验证的命令 hook。适配器必须: - -- 在 Hyperref、CTeX、fncychap 等完成补丁后安装,不能像当前实现一样提前保存一个可能过期的 `\chapter` 副本; -- 保留星号形式、短标题和长标题; -- 在普通章调用前恢复普通章状态和样式; -- 让 `\chapter*` 安全结束未竟专题显示状态,但不改变最近的普通正文编号;重复执行“离开 LU”的清理必须幂等,而每个实际排出的星号章只递增其内部 unnumbered anchor serial; -- 以未展开形式保存标题,不通过 `x` 型展开写入答案文件。 - -这里“最终标准 `\chapter`”指 Hyperref、CTeX、fncychap 等完成补丁后的实现。公共 chapter 适配器的 normal、appendix、unnumbered 分支和 `\LUchapter` 都只能调用这一份实现;不得各自复制 `\@chapter`、分页、写目录或 reset 代码。 - -#### 文档阶段和无编号边界 - -结构 counter 与状态在特殊文档阶段遵循以下规则: - -| 操作/阶段 | 结构 `c@chapter` | 显示计数器与状态 | -| --- | --- | --- | -| `\frontmatter` | 不重置为可见章号;无编号章不递增 | `phase=frontmatter`、`kind=unnumbered`;首次进入时普通/LU 显示计数器仍为 0;默认禁止 `\LUchapter` | -| 首次 `\mainmatter` | 初始化本书的结构与显示计数状态 | `phase=mainmatter`、`kind=unnumbered`、当前单元为空;首个普通章将 `LALUnormalchapter` 增至 1 | -| `\chapter*` | 不递增结构/显示 counter,也不额外触发编号章 reset | `kind=unnumbered`;结束 LU 的显示状态,但保留“最近普通正文”快照;清理可重复执行,每个实际标题取得新的内部 unnumbered serial | -| `\backmatter` | 后记、索引等无编号顶层单元不递增 | `phase=backmatter`、`kind=unnumbered`;清空当前单元,冻结普通/LU/slot 计数,默认禁止 `\LUchapter` | -| 首次 `\appendix` | **不把结构 `c@chapter` 清零** | `phase=appendix`、`kind=unnumbered`;清空当前单元,冻结普通正文和 LU 计数,将 appendix 显示计数器初始化为 0 | -| appendix 中的编号 `\chapter` | 每个附录递增一次 | `kind=appendix`,显示 `A`、`B`……;默认禁止 `\LUchapter`,因为“上一正文章节 + 希腊字母”不适用于字母附录 | - -支持的阶段顺序为 `frontmatter -> mainmatter -> appendix -> backmatter`,其中 frontmatter 和 appendix 均可省略。同一阶段命令的重复调用必须幂等:重复 `\mainmatter` 不得重置结构、普通正文、LU 或 slot 计数器,重复 `\appendix` 不得把附录显示计数器清零并重新产生 `A`;重复 front/mainmatter 也不得把页码重新切到 1 而制造重复 page destination。首次合法转换可以保留底层标准类原有的清页和页码模式切换,重复调用则应成为无状态变化的空操作或只发一次诊断。任何倒退到较早阶段的调用都应明确报错,除非未来另行定义并测试一套受支持的回退协议。 - -标准类的 `\appendix` 通常会把 `c@chapter` 清零;新适配层不能沿用这一计数器副作用。它只应用附录名称/显示格式,并在首次进入 appendix 时初始化 `LALUappendixchapter`,结构 `c@chapter` 继续单调递增。附录锚点使用独立 ASCII 命名空间,例如 `chapter.APP.A`。如果未来确需“附录后的未竟专题”,必须先单独定义其显示和引用契约,不能隐式复用正文规则。 - -在 `frontmatter`、首次 `\mainmatter` 与首个普通章之间、`backmatter`、`chapter*` 之后以及 `\appendix` 与首个附录章之间调用 `exercise` 时,章节状态 API 应报告“没有可映射的编号单元”,不得沿用上一个编号章的答案映射。第一阶段的答案 schema 只支持普通正文和 LU 单元,因此附录章中的 `exercise` 也应给出明确错误;若未来要支持附录习题,必须同时补齐 appendix 的 `unit-id`、答案标题、显示编号、锚点和回归测试,而不是把它伪装成普通章。 - -`\LUchapter` 的兼容签名仍接受现有的单个必选参数,同时可新增可选短标题: - -```tex -\LUchapter{完整标题} -\LUchapter[目录与页眉短标题]{完整标题} -``` - -### 1.3 显示编号、引用编号与锚点 - -`\thechapter` 应成为一个由当前状态分派的稳定命令: - -- 普通正文返回 `\arabic{LALUnormalchapter}`。 -- 未竟专题返回保存的 `normal-base` 加当前希腊字母的正文形式。 -- 附录返回 `\Alph{LALUappendixchapter}`。 -- 无编号状态返回空的、可展开 token list,并且语义查询 API 同时报告“没有当前编号单元”;绝不能悄悄返回上一个普通章或 LU 的显示编号。若 Hyperref 或其他包需要为实际的无编号顶层标题建立内部 destination,则另用单调递增、不可见的 unnumbered serial,使 `\theHchapter` 返回 `UNNUMBERED.`,而不占用结构或显示 counter。 - -CTeX 的章标题编号应使用另一条格式化路径: - -- 普通正文:前缀“第”,编号为普通正文编号,后缀“讲”。 -- 未竟专题:前缀“未竟专题”,编号为 `\zhnumber{unfinished-serial}`,无“讲”后缀。 -- 附录:沿用当前类/CTeX 的附录标题格式,但编号必须来自 `LALUappendixchapter`,不能来自结构 `chapter`。 -- 无编号状态:沿用标准星号章或 front/backmatter 的无编号标题路径,不读取任何旧单元编号。 - -优先把各套 CTeX 设置封装为命名样式,并在状态转换时显式、幂等地应用;如 CTeX 支持可靠的动态格式命令,也可由一个分派器生成。不得再以跨章分组恢复 CTeX 设置。 - -章标题/目录编号与引用编号必须有独立 provider。建议分别提供 `\LALUChapterTitleNumber`、`\LALUChapterTocNumber` 和 `\LALUCurrentReferenceNumber`:普通章三者都基于 `N`,未竟专题前两者生成“未竟专题十四”而引用 provider 生成 `23ε`,附录前两者生成类所约定的附录形式而引用 provider 生成 `A`。`\chapter` 适配器应通过 CTeX/标准章命令支持的格式接口,把 title/TOC provider 交给同一次标准章调用;如果所支持的 CTeX 版本没有独立 TOC 接口,只能在一个集中且有版本测试的窄适配层中替换该次标准目录条目的 `\numberline` 参数。不得为修正编号再写第二条 `\addcontentsline`,也不得把 `\thechapter` 临时改成中文标题编号后泄漏到标签或子计数器。 - -Hyperref 锚点规则如下: - -| 对象 | `\theH...` 约定 | 示例 | -| --- | --- | --- | -| 普通正文 | 使用普通显示计数器,保持既有 chapter 锚点外观 | `chapter.23` | -| 未竟专题 | `LU.` | `chapter.LU.14` | -| 未竟专题内 section | 从唯一章锚点派生 | `section.LU.14.2` | -| 附录 | `APP.` | `chapter.APP.A` | -| 实际无编号顶层标题 | `UNNUMBERED.`,仅作内部唯一性用途 | `chapter.UNNUMBERED.3` | -| 答案单元 | 使用 manifest 内的顺序 unit ID | `LALUanswerunit.answer.answer-unit:29` | - -锚点表示必须完全可展开并只含 ASCII。`\theHchapter` 同样按 `kind` 分派:普通章返回普通显示编号,未竟专题返回 `LU.`,附录返回 `APP.`,无编号状态在确需 chapter 型锚点时返回 `UNNUMBERED.`。它不能直接使用结构 `c@chapter`,否则在插入未竟专题后会改变普通章既有外链。`LUchapter` 只是标题流水号时使用 `\stepcounter`;真正承载紧随其后 `\label` 的标准 `chapter` 才使用 `\refstepcounter`,避免创建无用的第二锚点。 - -标准 `\chapter*` 若已由 Hyperref 使用自己的 `chapter*.` 命名空间,就继续沿用该机制;`UNNUMBERED.` 只是其他包确实展开 `\theHchapter` 时的确定性后备值,不能再额外创建一个与标准星号章重叠的 destination。 - -机器锚点属于 supporting structure,本轮不为旧 PDF 深链接保留 destination 别名。实现可以继续使用 `chapter.LU.`,也可以改用其他唯一 ASCII 命名;验收只检查正文标签键和 `\ref`/`\nameref`/`\autoref`/`\cref` 的语义、链接目标页面以及无重复 destination。引用锚点改名后必须干净重建 `.aux`、`.toc`、`.out` 和答案册的 `xr` 数据。 - -默认引用语义保持如下: - -- `\ref`:普通正文为 `23`,未竟专题为 `23ε`。 -- `\nameref`:返回章节标题。 -- `\autoref`:兼容现有 chapter 类型,输出“章 23ε”。若未来希望输出“未竟专题 23ε”,应作为单独的、有迁移说明的引用类型变更,而不在本次底层重构中夹带改变。 -- `\cref`:与 `\autoref` 保持相同对象类型,格式由类集中配置。 - -### 1.4 目录与 PDF 书签 - -目录和书签应复用标准章命令产生的单一条目,不同时手写第二个 `\addcontentsline`。期望结果为: - -- 普通正文目录条目显示“第 23 讲 标题”。 -- 未竟专题目录的章级条目显示“未竟专题十四 标题”,而不是 `23ε 标题`。 -- 未竟专题内 section 的目录编号显示 `23ε.1`。 -- PDF 书签使用 Unicode 希腊字符,例如 `23ε.1`,不得含 `$...$`、`\boldsymbol` 或其他 PDF string 不可用命令。 -- 目录条目和书签指向同一个唯一章锚点。 - -正文希腊字形与 PDF 字符串字形应分别定义,并通过 `\pdfstringdefDisableCommands` 或一个经过测试的 `\texorpdfstring` 边界连接。不能假定嵌入 `\thechapter` 的数学命令在写入 `.toc`、生成书签和二次读取时都以相同方式展开。 - -### 1.5 `LALU-answers` 中的答案单元 - -答案册不应再把普通章节和 `\LUsection` 分别实现为两套跨 section 分组逻辑。建议引入专用逻辑计数器 `LALUanswerunit` 和统一渲染接口: - -```tex -\LALUAnswerUnit{ - kind=unfinished, - normal-base=23, - unfinished-serial=14, - unfinished-slot=1, - title={范畴论视角下的线性代数}, - unit-id={lu:14} -} -``` - -它负责: - -- `\refstepcounter{LALUanswerunit}` 并重置属于答案单元的公式、定理和组计数器; -- 以 section 层级排版标题、写入目录和页眉,但不篡改全局 `section` 的意义; -- 普通单元显示“第 N 讲”,未竟单元显示“未竟专题 X”; -- `\theLALUanswerunit` 对普通单元返回 `N`,对未竟单元返回 `Nε`; -- 使用独立、唯一的 `\theHLALUanswerunit`; -- 在生成答案部分结束后无需把 `\thesection` 清空,因此不再破坏后续历年卷的公式编号。 - -可使用 `\section*` 加一次受控的目录/书签写入来保持现有视觉层级;公式和定理通过专用 `LALUanswerunit` 建立 reset 关系。这样答案册后半部分的普通 `\chapter`、`\section` 完全不受生成答案部分影响。 - -新答案清单必须携带渲染所需的章节语义快照,不能让答案册根据“上一个 section 是什么”猜测 `normal-base` 或希腊 slot。`\LUsection`、对应 counter 和旧 marker 只存在于 supporting structure,可随新 reader 一次删除;干净构建生成的新文件只输出 `\LALUAnswerUnit` 等当前 schema 事件。 - -### 1.6 内部替换范围与源码影响 - -本轮不为 supporting structure 设置过渡期,迁移策略如下: - -- `\chapter{...}`、`\LUchapter{...}` 以及现有可选短标题等作者接口不修改。 -- `LUchapter`、`LUgreekchap`、`LUsection` counter alias、`\LUgroupsancheck`、`\@exist@LUchapter@...`、`\@exist@LUsection@...` 和跨章开放分组可直接删除,不同步、不警告,也不保留空操作包装器。 -- 旧 `LALU-ans-contents.tex` 及其中的 `\LUsection`/手工 counter 协议是可丢弃构建产物;新 reader 不读取它。Makefile 负责先生成并验证当前 manifest,再编译答案册。 -- 标签键和引用可见结果必须保持;原始 href 名称、内部 counter 值和生成文件事件名可以变化。切换后统一清理 `.aux`、`.toc`、`.out`、`.fdb_latexmk` 与旧答案产物并完整重编译。 -- 用旧、新 PDF 的页面数、抽取文本、目录/outline、代表页面和链接目标作回归比较;这种对比是验证可见行为,不是要求同时维护两套生产 reader。 - -这一首选模型会有意改变裸 `\value{chapter}`、`\arabic{chapter}` 和直接读取 `c@chapter` 的内部语义:它们返回的是“第几个有编号顶层单元”,不再等于普通正文的显示编号。迁移前必须全仓库扫描这些用法,并根据意图替换为语义 API: - -- 需要普通正文显示编号时使用 `\LALUNormalChapterNumber` 或对应 expl3 查询接口; -- 需要当前可见引用时使用 `\thechapter`/`\LALUCurrentReferenceNumber`; -- 需要判断普通章或未竟专题时读取 `kind`,不能比较计数器; -- 只有 reset、checkpoint 或底层诊断代码可以读取结构 `c@chapter`。 - -当前仓库扫描结果是:`\value{chapter}`、`\arabic{chapter}`、`\setcounter{chapter}`、`\addtocounter{chapter}` 或 `c@chapter` 这类 raw 模式只出现在主模板现有章节逻辑中;答案模板还会重定义 `\thechapter` 并根据章节状态组织答案,因此两份模板都必须迁移,而正文内容没有直接依赖这些 raw 模式。迁移后应对全部手写源码启用静态检查。检查只允许统一章节模块中为 reset、checkpoint 或底层断言而登记的少量结构用途白名单,并排除 `.aux` 等生成文件;不能仅检查正文而放过模板内部继续把 raw chapter 当显示号的代码。 - -不再提供“裸 `chapter` 值等于普通正文显示编号”的次选兼容方案。仓库扫描表明这种读取只属于 supporting structure;继续临时回退 `chapter` 会重新引入重复锚点和 hook 状态歧义。若未来新增作者级查询需求,应暴露语义 API,而不是把结构 counter 重新定义成公共接口。 - -### 1.7 章节与链接测试矩阵 - -最小回归文档应覆盖下列序列: - -| 场景 | 预期标题/引用 | 重点断言 | -| --- | --- | --- | -| `chapter A` | 第 1 讲;`\ref=1` | 结构 counter=1,普通显示 counter=1,锚点 `chapter.1` | -| `chapter A -> LU B` | 未竟专题一;`\ref=1ε` | 结构 counter=2,普通显示 counter 仍为 1 | -| `chapter A -> LU B -> LU C` | 未竟专题一/二;`1ε`/`1δ` | 结构 counter=3;slot 与 LU 流水号连续 | -| 上述序列后 `chapter D` | 第 2 讲;`\ref=2` | 结构 counter=4,普通显示 counter=2,锚点 `chapter.2` | -| 新普通章后首个 LU | `2ε` | slot 正确重置为 1 | -| 同章后第六个 LU | `Nθ` | 既有六项映射不变 | -| 同章后第七个 LU | 明确错误 | 不产生空编号或重复锚点 | -| `LU -> chapter* -> chapter* -> chapter` | 最终普通章正常 | 两个星号章不递增结构/显示 counter,各自内部锚点唯一;无错误分组 | -| `chapter -> LU -> chapter* -> LU` | `1ε` 后为 `1δ` | 星号章不重置最近普通章或 slot;第二个 LU 建立新结构单元 | -| frontmatter 中的 `\chapter` 与 `\chapter*` | 无编号标题正常 | 结构、普通、LU counter 均不递增,current unit 为空,LU 调用被拒绝 | -| mainmatter 首个普通章之前调用 LU 或 `exercise` | 明确错误 | 当前单元为空,所有 counter 与样式保持初始状态 | -| mainmatter 中重复调用 `\mainmatter` | 章节编号和页码连续 | 不重置结构、普通、LU、slot 或页码,不产生重复 page destination | -| backmatter 中的 `\chapter`、`\chapter*` 与 index | 无编号标题正常 | current unit 为空,counter 全部冻结,不依赖手工 sanity check | -| 重复 `\appendix` 或从 appendix/backmatter 回到 mainmatter | 前者保持下一附录字母,后者明确报错 | 不重置 appendix/结构 counter,不产生重复 `A` | -| `appendix -> chapter A -> chapter B` | 附录 A/B | 结构 counter 各递增一次;标题、ref、TOC、outline 使用附录 provider;普通/LU counter 冻结;锚点在 APP 命名空间;chapter-dependent counter 正常 reset | -| appendix 入口空隙或附录章中的 `exercise` | 明确错误 | 不沿用最后一个正文单元;首阶段不生成不完整的 appendix 答案记录 | -| 降低 `secnumdepth` 后的普通章与 LU | 普通章走无编号路径,LU 原子失败 | 不预先递增任何逻辑 counter,不残留 LU 样式 | -| 带短标题和数学标题 | 正文、目录、书签各自正确 | 无 PDF string 警告 | -| LU 内 section/equation/theorem/figure | 均以 `Nε` 为父编号 | 对应 `\theH...` 唯一 | -| 正文 `\label` + `xr` 外部引用 | 数值和名称正确 | 冷启动与增量构建一致 | -| `\include`/`\includeonly` 跨普通章、LU、附录 | 全量与选择构建状态一致 | LaTeX checkpoint 恢复全部 counter;非 counter 状态由可重建记录恢复或明确禁止不安全的选择构建 | -| 答案普通单元 -> LU 单元 -> 普通单元 | 显示与正文映射一致 | 后续历年卷 section/公式不受污染 | -| 全仓手写源码静态扫描 | raw chapter 只剩登记的结构用途 | 模板和正文均无把 `value/arabic/setcounter/addtocounter{chapter}` 或 `c@chapter` 当显示号的调用;生成文件被排除 | - -本轮只要求使用本机 MacTeX 的 XeLaTeX 完成最小 fixture 和整书构建,不额外验证 Docker、其他 TeX Live 版本或 LuaLaTeX。日志检查必须把以下内容视为失败: - -- `destination with the same identifier`; -- 未定义引用或多轮后仍要求 rerun; -- 由章节编号产生的 `Token not allowed in a PDF string`; -- 章节状态断言失败; -- 目录和 PDF outline 中出现重复章条目。 - -## 二、答案抽取器 - -### 2.1 单一源数据与记录模型 - -答案的权威源仍是作者编辑的源码;任何生成的 `.tex` 或 manifest 都是可删除、不可手工编辑的构建产物。本轮 manifest 包含: - -- schema 版本和构建 job 身份; -- 渲染所需的章节语义快照(kind、normal base、unfinished serial/slot、unit ID 和标题); -- 习题组序号及显示标签; -- 题目在组内的逻辑序号、显示标签和本次生成中唯一的内部 ID; -- 答案状态:`answered`、`todo` 或 `omitted`; -- 未展开的 inline-token 正文; -- 记录数和结束 sentinel,供发布脚本与 reader 校验。 - -本轮 schema 2 使用 manifest 内的连续顺序 ID:`answer-unit:N`、`answer-group:N`和 `answer-item:N`。它们只承诺在单份 manifest 中唯一且与事件顺序一致,因而同章多个 `exercise`、重复显示组号或同题多个 `answer` 也不会制造内部冲突。在前面插入题目后保持已有 ID 不变的“发布级稳定 ID”明确不在本轮范围,也不新增 `\exitem[id=...]` 等作者接口。该目标留待后续单独设计。 - -生成文件应是语义事件流,而不是预先写死 `\section`、`\setcounter`、`\subsection*` 等版式命令。例如 manifest 只允许调用: - -```tex -\LALUAnswerManifestBegin{...} -\LALUAnswerUnit{...} -\LALUAnswerGroupBegin{...} -\LALUAnswerItem{...}{...} -\LALUAnswerGroupEnd{...} -\LALUAnswerManifestEnd{...} -``` - -每个事件前还有一条受限 ASCII marker,供发布脚本在不解析任意 TeX 正文的前提下校验顺序、ID 和计数。答案册 reader 再使用独立状态机校验同一事件流并渲染标题和列表。每个 item 单独占一个物理行,答案 body 作为不透明、未展开 token 载荷传递。 - -### 2.2 编号与普通/未竟映射 - -进入 `exercise` 时,抽取器必须从章节状态 API 取得快照,禁止再通过某个动态控制序列是否存在来判断普通章或未竟专题。 - -编号规则如下: - -- `exgroup` 使用普通 `\stepcounter`;若组本身支持 `\label`,其 Hyperref ID 必须包含 unit ID 和组序号。 -- 可选显式组号先设置逻辑值,再通过同一条组初始化路径生成标题和锚点;不能让显式组号分支跳过锚点或 reset。 -- 答案题号来自与题目绑定的逻辑 exercise item,而不是在 `answer` 结束时临时读取固定的 `\theenumi`。 -- 嵌套 enumerate 只影响答案正文内部编号,不得改变所关联的外层题号。 -- 答案册使用 manifest 中的 `normal-base`、`unfinished-serial` 和 `unfinished-slot` 渲染单元,绝不根据前一个已输出答案单元推算。 - -本轮按 manifest 事件顺序自动生成内部 ID,显示单元/组/题号另作元数据保存,不对正文发迁移告警。显式语义 ID 若将来确有跨版本引用需求,再作为会改变作者写法的独立提案 review。 - -### 2.3 inline token 边界 - -本轮只保留现有 `\begin{answer}...\end{answer}` 的 inline-token 写法。答案可包含用于普通文字和数学排版的 LaTeX token、平衡花括号、段落和常规宏;生成器以未展开 token 保存,禁止使用 `\tl_to_str:n`、`x` 型完全展开或 `\scantokens`。 - -`answer` 明确不支持 verbatim、minted、listing 或依赖特殊 catcode 的内容,本轮测试也不覆盖这些输入。这不是对当前正文的兼容性中断:仓库扫描没有此类答案,用户已确认可以把 inline answer 视为普通 LaTeX token。不要为不存在的特殊载荷编写外部正则解析器。 - -把答案人工拆到独立文件或新增 `\answerfile`/file payload 会改变作者写法,当前不实施。若以后确有需求,应先 review 是引入显式 file 接口,还是用其他方式从同一源代码生成两份 PDF;不能把特殊 catcode 支持悄悄塞进当前 inline 捕获器。 - -标题、题号等元数据与答案正文分开序列化。元数据需要 PDF 字符串时单独转换;不得为了生成元数据而展开正文。schema 2 刻意让每个 item 的命令及不透明 body 各占一个完整物理行,发布器只校验该行的固定外层结构,不展开或按行解析 body。 - -需要额外验证答案正文中的以下对象: - -- `\label`、`\ref`、`\nameref` 和从正文导入的 `xr` 标签; -- 浮动体与图片相对路径; -- 脚注、索引项、局部宏; -- display math 末尾的 QED; - -为避免答案册自身标签与 `\externaldocument` 导入的正文标签相撞,建议给外部标签添加固定前缀并提供 `\mainref`/`\mainnameref` 辅助命令;是否启用应先以标签清单验证兼容性。 - -### 2.4 空答案与占位语义 - -本轮以 PDF 可见行为不变为先,继续区分“没有 `answer` 环境”和“显式存在一个空 `answer` 环境”,并保留旧答案册已经排出的空答案项、无答案单元、目录条目和单元间 `\clearpage`: - -- 每个 `exercise` 都输出答案单元记录,即使其中没有答案;reader 仍按原顺序排标题和分页。 -- 含至少一个 `answer` 的组继续输出组标题;空的 `answer` 记为 `omitted`,但 reader 仍排出对应的空编号项。 -- 非空答案记为 `answered`;内部 schema 可识别显式 `todo`,但本轮不要求正文迁移到 todo 写法。 -- 日志报告总单元、组、题目、answered、todo 和 omitted 数量,以便和基线核对。 - -“省略没有可见答案的单元”“删除空答案项”或引入正式 todo 政策都会改变现有答案 PDF,均延后为需要单独 review 的条件性变化。 - -### 2.5 原子生成与失败恢复 - -TeX 主进程不应直接截断最后一个可用的正式答案文件。建议由构建辅助程序实施以下事务: - -1. 在目标构建目录创建同一文件系统上的唯一临时目录或临时文件。 -2. 主文档只向临时目标写 manifest 和 inline body。 -3. 写入 schema header、记录总数以及结束 sentinel;关闭所有 stream。 -4. 发布脚本先校验 schema/job、Begin/End 唯一性、marker 与 reader 命令对应、事件顺序、ID 连续唯一性和 unit/group/item/status 汇总计数。 -5. 答案册直接读取这份临时 manifest,并在唯一 staging 目录中完整编译;reader 独立校验 schema/job、状态转移、ID 和实际计数,同时让 TeX 验证不透明答案 body 的可编译性。主 PDF 同样暂存在 ASCII jobname 输出中。 -6. 只有主 PDF、答案 PDF 和 manifest 全部生成并验证成功后,Makefile 才集中将三者发布到正式路径;任一编译或发布前校验失败都保留上一份成功产物。manifest 使用同目录原子 rename,PDF 只在所有高风险步骤成功后才移入正式文件名。 - -纯 TeX 无法跨平台可靠地完成最终发布;临时路径、集中发布和 manifest 的同目录原子 rename 都由 Makefile 与小型发布脚本负责,不依赖 `\write18`。本轮 manifest 带 schema 版本和 job 身份;答案册发现缺失、不完整、事件顺序错误或版本/job 不匹配时必须失败,不能静默排版错误答案。源文件指纹与更强的过期检测可后续增加,不作为本轮完成条件。 - -### 2.6 并发、jobname 与构建目录 - -固定文件名 `LALU-ans-contents.tex` 不再作为内部协议。本轮 Makefile 使用显式 ASCII jobname 和独立构建目录,形如: - -```text -build/answers/ - LALU.manifest.tex -``` - -主文档写入与正式 manifest 同目录的唯一临时文件;构建命令把该临时路径和 job 身份显式传给答案册,并把答案 PDF 编译到唯一 staging 目录。不同 jobname/工程隔离、同一目标并发锁、source hash 和变体身份属于后续增强,本轮不宣称支持并行写同一个发布目标。 - -`make clean` 删除已知答案构建目录和遗留生成文件;作者维护的正文不受影响。新生成内容不参与版本控制。 - -### 2.7 答案抽取器测试 - -至少建立以下 fixture: - -- 普通章 A/B/C 三组,部分题有答案; -- 普通章后的两个连续未竟专题,验证 `Nε`、`Nδ` 与中文全局序号; -- 没有任何答案的 exercise、空组、空 answer、显式 todo;验证既有空项、单元和分页仍保留; -- 自定义起始组号和超过 9 的题号; -- 答案含多段、嵌套 enumerate、宏参数符号、注释、Unicode、标签和浮动体; -- 主编译在写到一半时故意失败,旧 manifest 仍可用; -- manifest 缺尾标、记录计数错误、schema/job 不匹配时发布器或 reader 明确失败; -- 新旧 PDF 的单元顺序、标题、组号、题号、答案正文、空项和分页逐项对比。 - -## 三、定理、证明与引用框架(本轮已实施) - -### 3.1 职责划分 - -移除 `ntheorem` 与自定义手写 QED 的并行语义层,改成清晰的两层职责: - -- `tcolorbox` theorem library 是 definition、example、lemma、theorem、corollary、axiom 的声明、计数器、标题栏和分页视觉层。 -- `amsthm` 是 proof 的语义与 QED 栈实现,保证 `\qedhere`、嵌套证明和显示公式末尾行为正确。 -- `tcolorbox` 只包装 proof/solution 的视觉外框,不自行在环境尾部附加 `$\square$`。 - -推荐加载顺序为:数学基础包与 `mathtools`,随后 `amsthm`,再加载 `tcolorbox` 及所需 library,最后加载 `hyperref`、`bookmark`、`cleveref`。`ntheorem` 完全退出,不保留同时生效的 theorem style 命令。 - -### 3.2 定理类环境 - -用一张集中声明表维护每个环境的属性: - -| 环境 | 中文名 | 标签前缀 | 颜色 | 缺省计数父级 | -| --- | --- | --- | --- | --- | -| `definition` | 定义 | `def:` | red | chapter | -| `example` | 例 | `ex:` | blue | chapter | -| `lemma` | 引理 | `lem:` | orange | chapter | -| `theorem` | 定理 | `thm:` | violet | chapter | -| `corollary` | 推论 | `cor:` | green | chapter | -| `axiom` | 公理 | `axm:` | olive | chapter | - -初次迁移应保持当前各环境分别计数的行为;是否共享 theorem 计数器属于独立的版式决策,不应在底层替换时顺便改变。正文中 `number within=chapter` 继续依赖统一章节模型,所以未竟专题内的显示编号和锚点都会自然使用唯一的 LU chapter 信息。 - -公共调用语法在兼容阶段保持不变: - -```tex -\begin{theorem}{显示标题}{标签键} - ... -\end{theorem} -``` - -第二参数非空时继续自动生成 `thm:<标签键>` 一类完整标签;空参数不生成标签。现有可选 tcolorbox 参数若实际有人使用,也必须由兼容层透传。未来可增加 key-value 新接口,但应先提供机械迁移工具和弃用周期,不能要求一次性人工修改全部现有环境。 - -视觉样式集中为一个基础 style,再按环境覆盖颜色。基础 style 负责 breakable、边框、标题字体、正文字体、首/中/末段虚线等当前特征。任何视觉调整都通过小型 golden PDF 检查,避免语义迁移意外改变分页。 - -### 3.3 proof、solution 与 QED - -`proof` 使用 amsthm 的 QED 栈;tcolorbox 可通过 `\tcolorboxenvironment` 装饰 amsthm proof,或使用一个严格转发到 amsthm begin/end 的包装器。无论采用哪种方式,都必须满足: - -- 普通文本结尾自动出现且只出现一个 QED; -- display math、`equation`、`align` 末尾的 `\qedhere` 位置正确,不再另加第二个方块; -- 跨页 breakable box 的 QED 位于证明内容结束处和盒子内部; -- 嵌套 proof 使用独立 QED 栈; -- 可选说明保持当前“证明 + 说明文字”的可见效果。 - -`solution` 复用同一视觉框架,但默认不显示 QED,以保持现有行为。若未来需要 QED,使用显式选项,不通过正文手写 `\square` 推测。 - -仓库扫描未发现 `proof`/`solution` 的第二个原始 tcolorbox 选项用法;它只属于模板 supporting structure,已直接删除,不保留弃用包装器。第一可选说明仍是作者接口,继续支持 `\begin{proof}[说明]` 和 `\begin{solution}[说明]`。 - -### 3.4 `autoref` 与 `cleveref` - -所有定理环境创建后,由同一声明表生成引用名称,不能散落手写私有 counter 名: - -- 为 Hyperref 配置对应 `...autorefname`,保证 `\autoref` 输出“定理 3.2”等。 -- 为 cleveref 同时配置 `\crefname`、`\Crefname`、range 和 multiple 格式。 -- 现有标签前缀保持不变;`\ref`、`\autoref`、`\cref`、`\crefrange`、`\nameref` 都加入回归测试。 -- 如果 tcolorbox 的内部 counter 名随版本变化,类应在一个内部适配层解析,正文不得依赖 `tcb@cnt@...`。 -- 新正文优先推荐 `\cref`;`\autoref` 作为兼容接口继续受支持。 - -每个 theorem counter 的 `\theH...` 必须从统一的唯一 chapter anchor 派生。正常章和连续未竟专题中,同样显示为 `.1` 的定理也必须拥有不同 destination。 - -### 3.5 已实施的迁移 - -本轮实施结果如下: - -1. 用 `amsthm` 替换 `ntheorem`,保留现有 tcolorbox theorem 公共环境名、双参数语法、标签前缀和分别计数方式。 -2. 将六类 theorem 的名称、前缀、颜色、样式及 autoref/cleveref 名称集中声明。 -3. 用 amsthm proof/QED 栈配合 tcolorbox 外观替换手写尾部方块;普通 proof、嵌套 proof、display/align 中的 `\qedhere` 已由最小 fixture 覆盖,solution 仍无 QED。 -4. 删除无人使用的 proof/solution 第二可选原始样式参数;正文不需修改,第一可选说明保持。 - -整书审计已确认主讲义 892 页和答案册 272 页在 144 dpi 下逐页像素一致;主讲义 raw/layout 文本与基线一致,答案册 raw 文本一致。迁移要求干净重编译辅助文件;既有标签键、可见定理编号与默认 QED 版式均保持不变。 - -### 3.6 定理测试矩阵 - -| 场景 | 验收点 | -| --- | --- | -| 六类环境各一个,标题/标签为空或非空 | 标题栏、字体、颜色、编号、标签前缀正确 | -| 同一普通章内各环境多个实例 | 分别计数规则与当前行为一致 | -| 两个连续未竟专题 | 显示父编号为 `Nε`/`Nδ`,锚点无重复 | -| 长定理跨两页 | 首/中/末边框正确,链接目标在标题处 | -| proof 以普通段落结束 | 恰有一个 QED | -| proof 以 equation/align 结束并使用 `\qedhere` | 方块位置正确且不重复 | -| 嵌套 proof | QED 栈正确 | -| solution | 缺省无 QED | -| `ref/autoref/cref/crefrange/nameref` | 中文名称、范围格式、链接目标正确 | -| 答案单元中的定理与证明 | 使用答案单元父编号,不污染后续历年卷 | - -## 四、Breaking changes 与源码迁移 - -这里区分两类变化: - -- **作者/PDF breaking change**:`讲义/专题/`、`讲义/历年卷/`、`讲义/其它/` 中的既有公共写法必须改写,或最终 PDF 的可见内容、编号、顺序、分页、标签/引用语义发生变化。本轮原则上不允许,除非下面明确列出并经 review。 -- **supporting-structure breaking change**:只影响类、主模板、答案模板、Makefile、生成文件协议、内部 counter/helper/marker 或原始 href 名称。用户已确认这些无需向后兼容;只要正文和 PDF 可见行为不变,可以直接发生。 - -因此,清理辅助文件并冷构建是本轮迁移动作;它不意味着要为旧 manifest、旧 counter alias 或旧 PDF destination 增加兼容层。 - -### 4.1 本轮明确发生的内部兼容性中断 - -| 本轮变化 | 被移除/改变的 supporting structure | 正文受影响位置与新写法 | -| --- | --- | --- | -| 章节状态改为显式记录 | 裸 `chapter` 变为结构流水号;`LUchapter`、`LUgreekchap` counter alias 可删除;无编号状态不再把上一单元伪装成当前单元 | 仓库扫描只在主模板内部命中 raw counter;`讲义/专题/`、`讲义/历年卷/`、`讲义/其它/` 无需改写,仍用 `\chapter`/`\LUchapter` 和正常 `\label`。模板内部改读语义状态 API | -| 删除跨章开放分组和历史 marker | `\LUgroupsancheck`、`\LUsection`、`\@exist@LUchapter@...`、`\@exist@LUsection@...` 及配套 counter/marker 直接消失 | 命中只在主模板、答案模板和旧生成文件;正文各未竟专题开头的 `\LUchapter{...}` 不变,无需加入替代 helper | -| 答案交换格式与路径改为带版本 manifest | `LALU-ans-contents.tex` 的文件名、命令语法和直接 `\input` 约定不再支持 | 只修改主模板、`讲义/LALU-answers.tex`、`讲义/Makefile` 和发布脚本;所有正文继续原样写 `exercise`/`exgroup`/`answer` | -| 内部题目/单元 ID 与 href 命名空间改变 | 旧 manifest ID、`chapter.LU.*` 等旧 PDF 原始 destination 不承诺保留别名 | 正文 `\label` 键及 `\ref`/`\nameref`/`\autoref`/`\cref` 写法不变;只需清理辅助文件并重建,不需要逐篇修改 | -| 定理语义层从 ntheorem 切换为 amsthm | 类不再传递加载 ntheorem,其私有 style 命令不可用 | 正文未直接调用 ntheorem;六类 theorem、proof、solution 的保留接口不变,因此正文无需改写 | -| 删除 proof/solution 第二个原始 tcolorbox 选项 | `\begin{proof}[说明][任意样式]` 和 solution 同形内部签名不再接受第二项 | 全仓正文没有第二项调用;第一可选说明仍原样使用,无正文修改。若将来需要受控样式,应另行设计作者接口 | - -这些变化无需弃用周期,因为删除对象没有出现在约定范围内的正文中。任何新命令一旦写入正文或作者文档,才转为需要稳定和迁移说明的作者接口。 - -### 4.2 条件性 breaking changes 与迁移审计 - -下列提案本轮**不启用**;若以后采用,必须另做正文扫描、PDF 基线和迁移 review: - -- 如果答案册为 `\externaldocument` 加固定前缀,答案正文中指向主讲义的 `\ref`/`\nameref`/`\autoref`/`\cref` 要改为 `\mainref`/`\mainnameref`/`\mainautoref`/`\maincref` 等成套 helper。当前入口位于 `讲义/LALU-answers.tex`;启用前应先扫描所有 `answer` 载荷中的引用并验证没有同名标签。 -- 如果以后要求“在前面插题后已有答案 ID 不变”,就需要语义 ID 或显式 `\exitem[id=...]` 一类设计;当前自动 ID 允许随位置变化,本轮不要求正文标注。 -- amsthm proof 的公共语法和普通自动 QED 行为应保持兼容,但 QED 的具体位置需要视觉审计。当前有 27 个 proof 直接以 display 结束;典型位置为 `讲义/专题/9 矩阵运算进阶.tex:443`、`讲义/专题/13 多项式.tex:542` 和 `讲义/专题/21 线性代数与几何.tex:49`。只有希望方块留在最后一行公式内的用例才需加入 `\qedhere`,不能把 27 处一律机械改写;当前正文没有手写 `\square`。 -- file payload/`\answerfile` 仍只是未来提案;当前 inline `answer` 明确只支持普通 LaTeX token,不支持 verbatim、minted、listing 或特殊 catcode,仓库正文没有此类载荷。 -- 省略空答案项、完全无答案的单元、目录条目或分页,以及要求用显式 todo 保留它们,都会改变答案 PDF;本轮继续保留这些可见对象,不采用省略策略。 -- 改变六类 theorem 的双参数语法、标签前缀、分别计数方式,或改 proof/solution 第一可选说明,都仍是条件性 breaking change,本轮不动。 -- 支持引擎矩阵尚未最终确定;一旦决定不支持 LuaLaTeX 等引擎,这些非正式构建流程将改为早期失败。当前 Makefile 使用 XeLaTeX,不影响正文;若要把其他引擎列为受支持,须先补齐字体、索引、书签和回归测试。 -- 如果无法可靠恢复 `\includeonly` 所需的非 counter 状态,可以明确禁止跨普通章/LU 的选择构建。当前主讲义使用 `\input`,因此仓库正文不受影响;外部使用者若依赖 `\includeonly`,应改为完整构建或等待 checkpoint 协议落地。 - -### 4.3 明确保留兼容的作者接口 - -以下常用正文写法不应因本次重构而修改: - -- 普通章继续写 `\chapter{标题}`,未竟专题继续写 `\LUchapter{标题}`;新增的 `\LUchapter[短标题]{完整标题}` 只是可选扩展。 -- 普通 inline 答案继续写 `\begin{answer} ... \end{answer}`;既有空 `answer` 也不需迁移。特殊 catcode 载荷不在该接口的支持范围内,本轮不引入替代语法。 -- definition、example、lemma、theorem、corollary、axiom 继续保留当前双参数形式和标签前缀。 -- `\begin{proof}[说明]` 与 `\begin{solution}[说明]` 的第一可选参数继续有效。 -- 既有 `\label` 键及正常的 `\ref`、`\nameref`、`\autoref` 和 `\cref` 源码不要求批量改写;这些引用的可见结果和目标页面应保持。原始 href destination 字符串是内部细节,不在此承诺内。 -- 空答案项、无答案单元、答案目录项及既有分页继续保留;正文不需要新增 `status=todo` 才能维持现有 PDF。 - -### 4.4 迁移时的定位清单 - -实现各阶段前至少运行下列扫描;它们用于定位受影响的写法,不代表匹配项都必须修改: - -```zsh -rg -n --glob '*.tex' --glob '*.cls' '\\(value|arabic|setcounter|addtocounter)\{chapter\}|c@chapter|\\theH?chapter' . -rg -n --glob '*.tex' '\\LUgroupsancheck|\\LUsection|@exist@LU(chapter|section)' 讲义 -rg -n --glob '*.tex' '\\(frontmatter|mainmatter|appendix|backmatter|LUchapter)|\\begin\{exercise\}' 讲义 -rg -n -U --glob '*.tex' '\\begin\{answer\}(?:[[:space:]]|%[^\n]*\n)*\\end\{answer\}' 讲义 -comm -23 <(rg -l -F '\begin{exercise}' 讲义/专题 讲义/其它 | sort) <(rg -l -F '\begin{answer}' 讲义/专题 讲义/其它 | sort) -rg -n --glob '*.tex' --glob '*.cls' '\\(theoremheaderfont|theorembodyfont|newtheorem)' . -rg -n --glob '*.tex' '\\begin\{(proof|solution)\}|\\(qedhere|square)' 讲义 -rg -n -U --pcre2 --glob '*.tex' '(?:\\\]|\\end\{(?:equation\*?|align\*?|alignat\*?|gather\*?|multline\*?|displaymath)\})\s*(?:%[^\n]*\n\s*)*\\end\{proof\}' 讲义 -``` - -迁移顺序应是:先改类、主模板、答案模板和构建脚本,再处理扫描命中的少量正文特殊情况,最后删除全部生成文件做一次冷构建。普通章节与未竟专题文件不应为了适配内部状态机而插入新的 helper 命令。 - -## 五、分阶段落地顺序 - -### 阶段 0:冻结基线(已完成) - -- 保存当前完整 PDF、答案 PDF、`.aux` 标签表、`.toc`、PDF outline 和归一化日志。 -- 记录主讲义和答案册的页面数、页面尺寸和抽取文本哈希。 -- 扫描答案记录、空答案、定理环境调用签名、proof 可选参数及标签前缀。 - -基线只用于回归比较,不加入生产构建。 - -### 阶段 1:统一定理和证明框架(已实施) - -- 移除 ntheorem 语义层,引入 amsthm proof/QED。 -- 保持 tcolorbox theorem 的旧环境名、双参数语法、计数器行为和标签前缀。 -- 集中 theorem 声明、样式及 autoref/cleveref 配置。 -- 删除无人使用的 proof/solution 第二可选原始样式参数;保留第一可选说明。 -- 用最小 fixture 验证普通、嵌套和 display/align `\qedhere`,再纳入整书 PDF 对比。 - -### 阶段 2:引入统一章节状态(已实施) - -- 在主模板中加入结构 `chapter`、普通显示计数器、LU 流水号、slot、appendix 显示计数器以及统一章节记录。 -- 提供语义查询 API,替换模板内部所有把裸 `chapter` 当普通显示编号的用法,并启用静态检查。 -- 在既有 `\chapter`、`\LUchapter` 外观后切换到新状态机。 -- 直接移除跨章节开放分组、旧 counter alias 和历史 marker,不保留兼容命令。 -- 通过章节测试矩阵以及旧、新目录/outline/标签值/PDF 代表页面对比后完成。 - -### 阶段 3:答案 manifest 与原子发布(已实施) - -- 定义带版本的 inline-token 事件 schema 和统一 `LALUanswerunit` renderer。 -- 主编译写同目录唯一临时文件;发布脚本校验 schema、sentinel、严格事件顺序、ID 和统计。 -- Makefile 将临时 manifest 路径和 job 身份传给答案册;reader 对缺失、版本/job 不匹配、事件顺序、ID 和计数错误明确失败。主 PDF 和答案 PDF 先留在 staging 路径,答案册成功后才与 manifest 集中发布,因此答案 body 的 TeX 错误不会提前覆盖任一份正式产物。 -- 发布器负向 fixture 已覆盖 Begin/End 错位、marker/command 不匹配、行尾额外 TeX、未闭合参数、重复 footer、非连续 ID 和统计不一致,这些输入均以非零状态拒绝。 -- 直接切换新 reader,不双写 legacy 文件,也不保留旧 `LALU-ans-contents.tex` reader。 -- 比较新旧答案 PDF 的单元、标题、组号、题号、正文、空答案、无答案单元和分页。 - -### 阶段 4:整书验证与收尾(已完成) - -- 在本机 MacTeX 上从 `make clean` 开始完整构建主讲义和答案册,并确认构建返回值能传播索引/manifest 失败。 -- 检查 duplicate destination、undefined reference、PDF string warning 和残留 rerun 提示。 -- 对照基线检查页面数、页面尺寸、抽取文本、目录/outline、标签表和全书像素;最终主讲义 892/892 页、答案册 272/272 页在 144 dpi 下均无像素差异。 -- 清理构建产物,确认只保留本轮源码修改和用户原有的无关工作区改动。 - -### 后续 review(不属于本轮) - -- 设计插题后仍稳定的显式习题 ID。 -- 决定是否需要 file answer,以及它是否值得改变正文作者接口。 -- 评估空答案/todo/空单元省略政策;任何改变先审核答案 PDF 的可见差异。 -- 逐项 review 第 4.2 节的其余条件性 breaking changes。 - -定理/证明改动单独提交;章节记录与答案 manifest 共享同一语义 API,以一个始终可编译的原子提交落地,文档同步另作提交。 - -## 六、总体验收标准 - -重构实现完成的最低标准如下: - -1. 从干净工作区运行正式构建,主讲义与答案册均成功;第二次运行不再要求 rerun。 -2. 章节测试序列精确得到“第 1 讲、未竟专题一 `1ε`、未竟专题二 `1δ`、第 2 讲”;四个有编号顶层单元使结构 `c@chapter` 最终等于 4,而普通显示计数器最终等于 2。 -3. 普通正文显示编号不因插入或删除未竟专题而改变;结构 counter 对每个有编号顶层单元恰递增一次,未竟专题全局中文序号和同章希腊 slot 各自按约定变化。 -4. 主 PDF 与答案 PDF 日志中不存在重复 destination;所有内部链接和 `xr` 链接指向正确页面。 -5. 目录与 PDF outline 中每个单元恰有一条记录,中文标题、Unicode 希腊编号和层级正确。 -6. `\ref`、`\nameref`、`\autoref`、`\cref` 以及范围引用覆盖普通章、未竟专题、附录、节、公式和六类定理,输出与兼容约定一致。 -7. 空 `answer` 仍生成原有空编号项;完全没有答案的单元仍保留原有标题、目录项和分页,答案 PDF 的可见顺序与基线一致。 -8. 主文档失败或临时 manifest 的 schema、sentinel、统计校验失败时,不覆盖上一份有效的主 PDF、答案 PDF 或 manifest,并以非零状态失败;reader 对缺失、损坏或 job/version 不匹配明确失败。 -9. 答案构建只读已结束且校验通过的临时 manifest,并将答案 PDF 写入 staging 目录;主编译、结构校验或答案 TeX 任一失败都不提前覆盖正式产物。全部成功后才集中移入两份 PDF 并以同目录原子 rename 替换 manifest;同一发布目标的并发构建不在本轮支持范围内。 -10. 普通 inline 答案保持宏、段落和标签语义;测试边界明确排除 verbatim、minted、listing 和特殊 catcode。 -11. 现有定理环境调用无需批量人工修改,编号、标签键、双参数接口和 proof/solution 第一可选说明不变。 -12. proof 在普通文本、display math、跨页和嵌套场景中 QED 恰好出现一次;solution 缺省无 QED。 -13. 章节状态、答案 renderer 和 theorem 声明各有单一实现来源,不再存在两套通过历史 marker 或开放分组彼此模仿的逻辑。 -14. frontmatter、backmatter 和星号章不递增结构/显示 counter;重复阶段命令不重置任何章节状态,非法阶段回退明确失败;每个编号附录只递增结构与 appendix 显示 counter,且不会把结构 `c@chapter` 清零。 -15. 全部手写源码(包括类和模板)都不存在把裸 `\value{chapter}`、`\arabic{chapter}`、`\setcounter{chapter}`、`\addtocounter{chapter}` 或 `c@chapter` 当作普通显示编号的调用;确需读写结构值的统一模块代码均在静态检查白名单中。 -16. 本机 MacTeX/XeLaTeX 的最小 fixture 和完整 `make clean && make` 均通过;Docker、其他 TeX Live 版本和其他引擎不作为本轮验收项。 -17. 无普通章、错误 phase、编号被抑制或希腊 slot 超界时,`\LUchapter` 都在改变任何 counter、当前记录或样式前失败;修正输入后可继续构建,不受半初始化状态污染。 -18. frontmatter、mainmatter 首章之前、backmatter、星号章、appendix 入口空隙及首阶段的附录章都不会把习题误挂到上一个正文/LU 单元;不支持的附录答案以明确错误结束。 -19. 主讲义和答案册的可见文本、章节/答案顺序、空答案项、无答案单元及分页与冻结基线一致;允许内部 counter、manifest 命令和 href destination 名称变化。 - -## 七、`patterns.re` 历史正则审计 - -`patterns.re` 是历次人工校对留下的搜索/替换便笺,不是可以按文件顺序无审查执行的 formatter。本节只记录各组规则的适用场景和后续处置;本轮不执行这些替换,不修改 `patterns.re`,也不据此批量修改正文。 - -这些规则有两项共同限制。第一,反复使用的 `[\u10ff-\ufeff]` 并不是中文字符类,而是覆盖 U+10FF--U+FEFF 间多种文字、数学符号、标点和私用区的宽范围。第二,正则不能解析 TeX 的嵌套花括号、注释、数学模式或 verbatim 类环境,而其中多处 `.+` 又是贪婪匹配。新增或删除空行还可能产生或移除 `\par`,从而影响列表间距、首段行为和答案正文。因此,后续即使采用某条规则,也应把它改成只报告候选位置的 lint,逐类 review、提交和编译验证。 - -### 7.1 `line wrap` - -- `([…])\n *([…]) -> $1$2` 用于删除中文文字、标点、`$`、`=` 等字符之间的硬换行;其范围也会命中对齐公式和 TikZ 坐标,甚至可能把相邻两行的 `$` 拼成 `$$`,不能全仓替换。 -- 引用命令后换行、句点后换行、数字后换行,以及正文换行后接非 `begin`/`end`/`item` 命令的几条无 replacement 规则,意在定位被折行的连续语句。这些查询的命令名和参数匹配都很宽,且会命中多行公式,应只作为人工搜索提示。 -- `\.\n *([…]) -> . $1` 只是把单换行规范成一个源码空格;TeX 原来的单换行通常已经等价于空格,而后继为标点或数学符号时,这一替换反而可能制造错误间距。 - -归类:源码 lint/人工排版检查,不进入模板层,也不作为自动修复。 - -### 7.2 `item wrap` - -- `\item` 单独占行后接正文的规则把首段移到同一源码行,通常只是风格调整,但不覆盖以命令、花括号或可选标签开始的 item。 -- 连续 `\item`、环境结束后接 `\item`、显示公式后接 `\item` 等规则会插入空行;`enumerate` 后首项以及空 `\item` 后接环境的规则又会删除换行或把两行合并。它们互相重叠并具有顺序依赖。 -- 在 list 中插入空行会产生段落边界,可能改变 `\parsep`;把空项与下一 `\item` 合并还会生成难以辨认的 `\item \item`,掩盖源文件错误。 - -归类:如需统一源码风格,优先配置 `latexindent`;涉及段落边界的候选仍须人工 review,不批量修改正文。 - -### 7.3 `dot spacing` - -- `\.([\u10ff-\ufeff]) -> . $1` 主要针对 `.故`、`.因此` 一类西文句点后紧接中文正文的情况,仓库中确有此类实例;但宽字符范围也包含中文标点和数学符号,可能在标点前或数学式内错误插空格。 -- `\.\n *(\d) -> . $1` 将句点、换行、数字整理为同一行;原换行通常已经提供空格,且该规则无法判断是否遇到被错误拆行的小数。 - -归类:只报告“句点后直接接 Han 字符”的窄 lint,并排除数学、注释、verbatim 和标点;不使用现有宽范围直接替换。 - -### 7.4 `ref spacing` - -搜索式试图覆盖 `\ref`、`\autoref`、`\eqref`、`\pageref`、`\nameref` 等 `*ref` 命令,并用 `[^h]` 避开 `\href`/`\hyperref`。其记录的 replacement 按文件内容实际只有 `$1`,没有尾随空格,因此当前是 no-op。即使补上空格也不正确:引用后是 `,`、`)` 等标点时不应插入;`\autoref` 常以数字结束而需要中西文间距,`\nameref` 却可能展开为中文标题并直接连接“一章”等中文文字。 - -归类:保留现有模板中针对 `\autoref` 的后继 token 分类。若以后扩展到 `\eqref`、`\pageref` 等,应按各命令的可见语义逐个设计并复用统一分类器,不能由这条正则推导所有 `*ref` 的行为。 - -### 7.5 `section spacing` - -- 第一条规则在 `section`、`subsection`、`subsubsection` 前后增加源码空行;它会把标题前的注释行也当作正文,且不支持星号形式、可选短标题或多行标题。 -- 第二条规则只删除标题后下一命令前的缩进;当前典型场景是 `\section` 后立即跟 `\subsection`,属于源码风格而非模板语义。 -- 标题后人为加入空段落可能干扰首段处理,不能用源码空行控制最终版式。 - -归类:formatter/人工风格检查,不进入模板,也不自动改正文。 - -### 7.6 `displaymath` - -- 第一条无 replacement 的跨行规则主要定位 `\[` 后第一行已经有公式内容的 display,供人工统一为开闭定界符独占一行;它仍可能跨过非预期内容,只能作为搜索器。 -- `$$...$$ -> \[...\]` 的迁移方向正确,因为前者是 plain TeX 写法;当前候选集中在旧历年卷答案。不过 `[\w\W]` 会跨任意内容,不能排除注释、verbatim 或转义美元符号。 - -归类:用 ChkTeX 等 lint 报告 `$$`,再逐处迁移和编译;delimiter 的换行风格交给 formatter。 - -### 7.7 `matrices` - -两条配套规则把 `\left|\begin{array}{...} ... \end{array}\right|` 改成 `vmatrix`,只在竖线确实表示行列式、列格式只是普通居中列时等价。开闭两侧独立替换可能产生不配对环境;opening 中的 `\{.+\}` 还可能吞掉同行后续花括号。`array` 的 `l`/`r` 对齐、列间竖线和 `@{...}` 等信息也会在转换后丢失。当前仓库没有精确命中,属于历史规则或已完成的清理。 - -归类:只对完整且已确认等价的一对 delimiter 做人工语义迁移,不保留为全局替换。 - -### 7.8 `dots` - -`(,\s*)\cdots(\s*,) -> $1\ldots$2` 用于把逗号分隔列表中的居中省略号改为低省略号,符合数学排版惯例;也可考虑使用由 amsmath 根据上下文选择的 `\dots`。规则不判断数学模式,`\s*` 又可以跨行,因此仍可能进入注释、宏定义或 verbatim。 - -归类:这是相对高置信度但仍需逐处 review 的语义 lint;若实施,应作为独立正文清理提交并检查公式输出。 - -### 7.9 proof 搜索 - -最后的 `(?!\n) *\\begin\{proof\}` 使用了负向前瞻,检查的是“当前位置的下一个字符不是换行”,而不是“前一个字符不是换行”。引擎可以直接从 `\begin` 的反斜杠开始并让空格匹配零次,所以它实际上等价于搜索全部 proof:当前两种搜索都得到 399 个字面 occurrence,其中 3 个位于注释,实际 active proof 为 396 个。 - -若原意是寻找没有独占一行的 proof,应改为按行锚定、要求 proof 前已有非空白内容并排除注释的查询,例如 `(?m)^(?![ \t]*%)[ \t]*\S.*?\\begin\{proof\}`。这只是源码布局检查,与 proof/QED 的模板语义无关。 - -总体上,`line wrap`、`item wrap`、`dot spacing`、`section spacing`、第一条 `displaymath` 和 proof 行布局属于 lint/格式化候选;`$$`、行列式 array 和逗号列表中的 `\cdots` 属于需单独审核的语义迁移;只有引用后的中文间距属于模板层问题。现有 `patterns.re` 不应成为构建步骤或一次性批处理脚本。 diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/5 \347\272\277\346\200\247\346\230\240\345\260\204.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/5 \347\272\277\346\200\247\346\230\240\345\260\204.tex" index 45cfc87..1ebcb8e 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/5 \347\272\277\346\200\247\346\230\240\345\260\204.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/5 \347\272\277\346\200\247\346\230\240\345\260\204.tex" @@ -335,7 +335,7 @@ \subsection{线性映射的基本运算} 注意:在上述定义中一定注意$\sigma$和$\tau$的顺序,我们需要先使用$\sigma$将$V_1$中的元素映射到$V_2$,然后再用外层的$\tau$将这个结果映射到$V_3$. 此外,有时也会将复合映射记作$\tau \circ \sigma$(一般是为了强调这是复合运算),这里为了简化记号直接使用$\tau\sigma$. -接下来定义线性映射的逆运算,也就是定义逆映射,事实上与反函数的定义类似. 设 $\sigma \in \mathcal{L}(V_1,V_2)$. 若存在 $\tau \in \mathcal{L}(V_2,V_1)$ 使得 $\sigma \tau = I_{V_2}$ 且 $\tau \sigma = I_{V_1}$,则称 $\sigma$\term{可逆}\index{ni!ke@可逆 (invertible)},并称 $\tau$ 为 $\sigma$ 的逆映射\index{ni!yingshe@映射 (inverse map)}. 其中 $I_{V_1}$ 和 $I_{V_2}$ 分别是 $V_1$ 和 $V_2$ 上的恒等映射,即 $I_{V_i}(\alpha)=\alpha,\enspace \forall \alpha \in V_i,\ i = 1, 2$. +接下来定义线性映射的逆运算,也就是定义逆映射,事实上与反函数的定义类似. 设 $\sigma \in \mathcal{L}(V_1,V_2)$. 若存在 $\tau \in \mathcal{L}(V_2,V_1)$ 使得 $\sigma \tau = I_{V_2}$ 且 $\tau \sigma = I_{V_1}$,则称 $\sigma$\term{可逆}\index{ni@逆!ke@可逆 (invertible)},并称 $\tau$ 为 $\sigma$ 的逆映射\index{ni@逆!yingshe@映射 (inverse map)}. 其中 $I_{V_1}$ 和 $I_{V_2}$ 分别是 $V_1$ 和 $V_2$ 上的恒等映射,即 $I_{V_i}(\alpha)=\alpha,\enspace \forall \alpha \in V_i,\ i = 1, 2$. \begin{theorem}{}{逆映射是线性映射} 上述定义的逆映射 $\sigma^{-1}$ 为线性映射. diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/7 \347\272\277\346\200\247\346\230\240\345\260\204\347\237\251\351\230\265\350\241\250\347\244\272\344\270\216\347\237\251\351\230\265\350\277\220\347\256\227\345\237\272\347\241\200.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/7 \347\272\277\346\200\247\346\230\240\345\260\204\347\237\251\351\230\265\350\241\250\347\244\272\344\270\216\347\237\251\351\230\265\350\277\220\347\256\227\345\237\272\347\241\200.tex" index ea54b70..5a6f83b 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/7 \347\272\277\346\200\247\346\230\240\345\260\204\347\237\251\351\230\265\350\241\250\347\244\272\344\270\216\347\237\251\351\230\265\350\277\220\347\256\227\345\237\272\347\241\200.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/7 \347\272\277\346\200\247\346\230\240\345\260\204\347\237\251\351\230\265\350\241\250\347\244\272\344\270\216\347\237\251\351\230\265\350\277\220\347\256\227\345\237\272\347\241\200.tex" @@ -861,7 +861,7 @@ \subsection{矩阵的逆的定义} \end{gather*} 不难验证恒等映射在出发空间和到达空间取同一组基下的矩阵表示一定为单位矩阵,故 $AB = \mathbf{M}_{B_1,B_1}(I_{V_1}) = E_n = \mathbf{M}_{B_2,B_2}(I_{V_2}) = BA$,由此我们从可逆映射的角度引入矩阵的逆的概念: \begin{definition}{矩阵的逆}{} - 设 $A \in \mathbf{M}_n(\mathbf{F})$. 若存在 $B \in \mathbf{M}_n(\mathbf{F})$ 使得 $AB = BA = E_n$(不刻意强调时可以省略 $n$),则称矩阵 $A$ 可逆,并把 $B$ 称为 $A$ 的\term{逆矩阵}\index{ni!juzhen@矩阵 (inverse matrix)},记作 $B = A^{-1}$. + 设 $A \in \mathbf{M}_n(\mathbf{F})$. 若存在 $B \in \mathbf{M}_n(\mathbf{F})$ 使得 $AB = BA = E_n$(不刻意强调时可以省略 $n$),则称矩阵 $A$ 可逆,并把 $B$ 称为 $A$ 的\term{逆矩阵}\index{ni@逆!juzhen@矩阵 (inverse matrix)},记作 $B = A^{-1}$. \end{definition} 在一些比较经典的教材中可逆矩阵也被称为非奇异矩阵,不可逆矩阵被称为\term{奇异矩阵}\index{qiyijuzhen@奇异矩阵 (singular matrix)}. From f89693bc8a5f39f3a74514f3b5196fd70dfba01f Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Sat, 19 Sep 2026 00:30:34 +0800 Subject: [PATCH 26/27] docs(document): fix spacing --- ...234\211\347\220\206\346\240\207\345\207\206\345\275\242.tex" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/18 \346\234\211\347\220\206\346\240\207\345\207\206\345\275\242.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/18 \346\234\211\347\220\206\346\240\207\345\207\206\345\275\242.tex" index 6e4c7ea..9e84f81 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/18 \346\234\211\347\220\206\346\240\207\345\207\206\345\275\242.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/18 \346\234\211\347\220\206\346\240\207\345\207\206\345\275\242.tex" @@ -16,7 +16,7 @@ \section{推广的广义特征子空间与第二有理标准形} \begin{definition}{}{推广广义特征子空间} 设$V$是一个有限维线性空间,$T\in\mathcal{L}(V)$. 设$T$的特征多项式为 \[f(\lambda)=p_1(\lambda)^{n_1}p_2(\lambda)^{n_2}\cdots p_k(\lambda)^{n_k},\] - 其中$p_i(\lambda)$是首一不可约多项式,$n_i$是正整数. 对于每一个不可约多项式$p_i(\lambda)$,我们定义\term{广义特征子空间}$G_{p_i}$为 + 其中$p_i(\lambda)$是首一不可约多项式,$n_i$是正整数. 对于每一个不可约多项式$p_i(\lambda)$,我们定义\term{广义特征子空间} $G_{p_i}$为 \[G_{p_i}=\{v\in V\mid\exists m\in\mathbf{N}^+,(p_i(T))^mv=0\},\] $G_{p_i}$中的向量则称为\term{广义特征向量}. \end{definition} From 609f354cc638b900c60adf460ed646c3376a2c4c Mon Sep 17 00:00:00 2001 From: 45gfg9 <45gfg9@45gfg9.net> Date: Mon, 5 Oct 2026 21:52:09 +0800 Subject: [PATCH 27/27] docs(document): symbol fix, add FIXME --- ...264\344\270\212\347\232\204\347\256\227\345\255\220.tex" | 6 +++--- .../5 \347\272\277\346\200\247\346\230\240\345\260\204.tex" | 2 +- ...01 \351\242\204\345\244\207\346\200\235\346\203\263.tex" | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/20 \345\206\205\347\247\257\347\251\272\351\227\264\344\270\212\347\232\204\347\256\227\345\255\220.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/20 \345\206\205\347\247\257\347\251\272\351\227\264\344\270\212\347\232\204\347\256\227\345\255\220.tex" index 62af7ac..b9eebe1 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/20 \345\206\205\347\247\257\347\251\272\351\227\264\344\270\212\347\232\204\347\256\227\345\255\220.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/20 \345\206\205\347\247\257\347\251\272\351\227\264\344\270\212\347\232\204\347\256\227\345\255\220.tex" @@ -52,7 +52,7 @@ \subsection{内积空间的同构} \end{theorem} \begin{proof} - \begin{itemize} + \begin{itemize} % FIXME: horizontal overflow \item[\ref*{item:20:保积同构条件:1}$\implies$\ref*{item:20:保积同构条件:2}] 由内积的保持可知范数的保持,从而对于任意 $v\in V$,$ Sv=\vec{0} \iff \|Sv\|=\|v\|=\vec{0} \iff v=\vec{0}$,所以 $ S $ 是单射. 而 $ V $ 和 $ U $ 维数相同,进而是双射. \item[\ref*{item:20:保积同构条件:2}$\implies$\ref*{item:20:保积同构条件:3}] 设 $ e_1, \ldots , e_n $ 是 $ V $ 的任意一组标准正交基,则 @@ -1239,7 +1239,7 @@ \subsection{正算子} % 从这里我们进一步加深类比. \begin{example}{镜像变换}{镜像变换} - 镜像变换(或镜面反射)--$\mathbf{R}^2$中每个向量关于过原点的直线$L$(看作镜面)相对称的变换 $\varphi$,即 + 镜像变换(或镜面反射)--$\mathbf{R}^2$中每个向量关于过原点的直线$L$(看作镜面)相对称的变换 $\varphi$,即 \[ \varphi: \forall \alpha = \vec{OA} \in \mathbf{R}^2,\\ \varphi(\alpha) = \alpha'= \vec{OB} @@ -1271,7 +1271,7 @@ \subsection{正算子} \end{example} \begin{proof} - 事实上,设直线 $L$ 的某个方向的单位向量为$\omega$,则$\vec{OC}=(\alpha,\omega)\omega$,其中$(\alpha,\omega)$为向量$\alpha$和$\omega$的内积,对于刚接触内积的读者来说,这可能并不是这么显然,但是稍加思考,我们发现这是很自然的,因为$\vec{OC}$是$\vec{OA}$在$\omega$方向上的投影,而求一个向量在另一个向量上的投影,我们高中时已经见了无数遍了,不妨设$\vec{OA}$与$\omega$的夹角为$\theta$,则$\vec{OC}=\vert\alpha \vert cos\theta\omega$,又 + 事实上,设直线 $L$ 的某个方向的单位向量为$\omega$,则$\vec{OC}=(\alpha,\omega)\omega$,其中$(\alpha,\omega)$为向量$\alpha$和$\omega$的内积,对于刚接触内积的读者来说,这可能并不是这么显然,但是稍加思考,我们发现这是很自然的,因为$\vec{OC}$是$\vec{OA}$在$\omega$方向上的投影,而求一个向量在另一个向量上的投影,我们高中时已经见了无数遍了,不妨设$\vec{OA}$与$\omega$的夹角为$\theta$,则$\vec{OC}=\vert\alpha \vert \cos\theta\omega$,又 \[ \vert\alpha \vert\cos\theta=\vert\alpha \vert\dfrac{\alpha\cdot\omega}{\vert \alpha\vert\cdot\vert\omega\vert}=\dfrac{(\alpha,\omega)}{\vert\omega\vert}. \] diff --git "a/\350\256\262\344\271\211/\344\270\223\351\242\230/5 \347\272\277\346\200\247\346\230\240\345\260\204.tex" "b/\350\256\262\344\271\211/\344\270\223\351\242\230/5 \347\272\277\346\200\247\346\230\240\345\260\204.tex" index 1ebcb8e..e1622dd 100644 --- "a/\350\256\262\344\271\211/\344\270\223\351\242\230/5 \347\272\277\346\200\247\346\230\240\345\260\204.tex" +++ "b/\350\256\262\344\271\211/\344\270\223\351\242\230/5 \347\272\277\346\200\247\346\230\240\345\260\204.tex" @@ -127,7 +127,7 @@ \subsection{线性映射举例} \draw node[below left] at (O) {$O$}; \end{tikzpicture} \end{center} - \item 镜像变换(或镜面反射)--$\mathbf{R}^2$中每个向量关于过原点的直线$L$(看作镜面)相对称的变换 $\varphi$,即 + \item 镜像变换(或镜面反射)--$\mathbf{R}^2$中每个向量关于过原点的直线$L$(看作镜面)相对称的变换 $\varphi$,即 \[ \varphi: \forall \alpha = \vec{OA} \in \mathbf{R}^2,\\ \varphi(\alpha) = \alpha'= \vec{OB} diff --git "a/\350\256\262\344\271\211/\345\205\266\345\256\203/\346\234\252\347\253\237\344\270\223\351\242\2301 \351\242\204\345\244\207\346\200\235\346\203\263.tex" "b/\350\256\262\344\271\211/\345\205\266\345\256\203/\346\234\252\347\253\237\344\270\223\351\242\2301 \351\242\204\345\244\207\346\200\235\346\203\263.tex" index f20f9ea..6dddf1d 100644 --- "a/\350\256\262\344\271\211/\345\205\266\345\256\203/\346\234\252\347\253\237\344\270\223\351\242\2301 \351\242\204\345\244\207\346\200\235\346\203\263.tex" +++ "b/\350\256\262\344\271\211/\345\205\266\345\256\203/\346\234\252\347\253\237\344\270\223\351\242\2301 \351\242\204\345\244\207\346\200\235\346\203\263.tex" @@ -46,7 +46,7 @@ \section{数学证明初探} 以高中可能未接触过的两个逻辑运算符$\to$和$\leftrightarrow$为例,其真值表如下: -\begin{center} +\begin{center} % FIXME \begin{minipage}[c]{0.45\textwidth} \centering \begin{tabular}{|c|c|c|}