From 5b24eba3c80ce035ef237c1c04e8a37c1f7a2418 Mon Sep 17 00:00:00 2001 From: dela Date: Sat, 22 Aug 2026 10:13:36 +0800 Subject: [PATCH] feat: fill test coverage gaps + fix \sdcol i18n labels - examples/subst-chain.tex: substitution walkthrough with \sdbox/\sdsubst/\sdstep - examples/antipatterns.tex: four compile-clean-but-teach-false figures - tests/invalid/step-outside-row.tex, empty-row.tex, undeclared-role.tex, two-contrast-pkg.tex: negative fixtures for package-level warnings - test.sh: add tests/invalid/ loop - build.sh: allow superderive-build: allow directive to suppress intentional warnings in antipatterns galleries - superderive.sty: \sdcol respects [en] option for illegal/legal labels - references/api.md: complete rewrite, all ~12 macros documented --- examples/antipatterns.tex | 46 +++++++++++++ examples/subst-chain.tex | 23 +++++++ references/api.md | 102 +++++++++++++++++++++++++++-- scripts/build.sh | 10 ++- scripts/test.sh | 10 +++ tests/invalid/empty-row.tex | 5 ++ tests/invalid/step-outside-row.tex | 4 ++ tests/invalid/two-contrast-pkg.tex | 7 ++ tests/invalid/undeclared-role.tex | 6 ++ 9 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 examples/antipatterns.tex create mode 100644 examples/subst-chain.tex create mode 100644 tests/invalid/empty-row.tex create mode 100644 tests/invalid/step-outside-row.tex create mode 100644 tests/invalid/two-contrast-pkg.tex create mode 100644 tests/invalid/undeclared-role.tex diff --git a/examples/antipatterns.tex b/examples/antipatterns.tex new file mode 100644 index 0000000..e7f3f88 --- /dev/null +++ b/examples/antipatterns.tex @@ -0,0 +1,46 @@ +% Anti-patterns -- figures that compile cleanly and still teach something false. +% ../scripts/build.sh antipatterns.tex +% superderive-lint: allow-multiple-contrast +% superderive-build: allow +\documentclass[border=10pt]{standalone} +\usepackage[cjk]{superderive} + +\begin{document} +\begin{tikzpicture} +\sdstage{AP}{四张反例} +\sdrow{R1} + % AP1: \sdsubst 只填了 from,to 也是同一个 -- 改写没有产生任何变化 + \sdsubst[role=rewrite]{ap1}{x+y}{x+y} + \sdreason{ap1}{subst 的 from 和 to 相同,改写为空} +\sdrowend + +\sdrow{R2} + % AP2: \sdcancel 划线后旁边又写了一遍同样的东西 -- 读者不知道划掉的是哪一部分 + \sdcancel[role=cancel]{ap2a}{x+y} + \sdbox[role=keep]{ap2b}{x+y} + \sdreason{ap2a}{cancel 后面紧跟同一式子,等于没有 cancel} +\sdrowend + +\sdrow{R3} + % AP3: 同一行里塞了五组 role,hue budget 超了 + \sdbox[role=keep]{ap3a}{a} + \sdbox[role=rewrite]{ap3b}{b} + \sdbox[role=cancel]{ap3c}{c} + \sdbox[role=intro]{ap3d}{d} + \sdbox[role=neutral]{ap3e}{e} +\sdrowend + +\sdrow{R4} + % AP4: 两个 contrast pair,第二个是多余的 + \sdcol{ap4a}{x+y}{x+y} + \sdcol{ap4b}{a+b}{a+b} +\sdrowend + +\sdbbox{all} +\sdtopformula{F}{反例见 \texttt{references/antipatterns.md}} +\sdmeaningbox{mb}{140mm}{all} + {subst from=to;cancel 后重复;row 内五色;两个 sdcol} + {每一行都符合语法,但每一行都在说谎} + {参考 antipatterns.md 的对应规则} +\end{tikzpicture} +\end{document} diff --git a/examples/subst-chain.tex b/examples/subst-chain.tex new file mode 100644 index 0000000..3484663 --- /dev/null +++ b/examples/subst-chain.tex @@ -0,0 +1,23 @@ +% Golden example -- a subst chain that must be seen, not just aligned. +% ../scripts/build.sh subst-chain.tex +\documentclass[border=10pt]{standalone} +\usepackage[cjk]{superderive} + +\sdsetrole{live}{sdTeal} + +\begin{document} +\begin{tikzpicture} +\sdstage{D1}{换元让积分内部对齐} +\sdrow{R1} + \sdbox[role=keep]{x}{$u$} + \sdsubst[role=live]{s1}{u}{x^2} + \sdstep[role=rewrite]{s2}{\int_0^1 x\,e^{x^2}\,dx}{\tfrac12\int_0^1 e^u\,du} +\sdrowend +\sdbbox{all} +\sdtopformula{F}{$\int_0^1 x\,e^{x^2}\,dx = \tfrac12\int_0^1 e^u\,du$} +\sdmeaningbox{mb}{120mm}{all} + {换元是恒等改写,不改值} + {令 $u=x^2$,则 $du=2x\,dx$} + {上下限随 $u$ 一起换掉} +\end{tikzpicture} +\end{document} diff --git a/references/api.md b/references/api.md index 9fe0b2a..65feaf4 100644 --- a/references/api.md +++ b/references/api.md @@ -1,7 +1,101 @@ -# API +# superderive.sty API -`\usepackage[cjk]{superderive}` or `[en]`. +```tex +\documentclass[border=10pt]{standalone} +\usepackage[cjk]{superderive} % cjk: ctex + fandol (XeLaTeX). en: English meaning-box and sdcol labels. +``` -Layout: `\sdstage{n}{text}` `\sdrow[height]{name}` … `\sdrowend` `\sdbbox{all}` `\sdtopformula{F}{math}`. +Build with `./scripts/build.sh fig.tex` — it puts `assets/` on `TEXINPUTS`, so the package +does not need to be installed into your texmf tree. -Call `\sdtopformula` after the last `\sdrowend`. +The build first runs `scripts/lint.py`. It rejects raw TikZ drawing, a formula placed +before the last row, more than one `\sdcol` contrast pair, and more than four active hue +families in the figure. Intentional galleries/tests may put +`% superderive-lint: allow-raw-tikz` near the top; do not add an exemption to a +deliverable merely to make it pass. + +## Ledgers + +```tex +\sdsetrole{keep}{sdTeal} % role -> color. Macros take a ROLE, never a color. +\sdsetlabels{Idea}{Rewrite}{Caveat} % override the three meaning-box rail labels +``` + +Colors: `sdTeal sdOrange sdCoral sdViolet sdGray sdInk`. An unknown role falls back to +gray **and emits a package warning**, which `build.sh` turns into a failed build. +Redeclaring a role with the same value is harmless; changing its value emits a warning +and keeps the original mapping. + +## Flow layout + +**This is the default.** `\sdrow` opens a band, every object inside is placed at the +cursor, `\sdrowend` closes it. Hand-written offsets are the main source of layout bugs. + +```tex +\sdstage{D1}{stage heading} % on the left rail, below the previous band +\sdrow[16mm]{R1} % open a band; height default is 16mm + \sdbox[role=keep]{x}{$u$} % existing term, kept as-is + \sdsubst[role=live]{s1}{$u$}{$x^2$} % from->to with a strike on the from + \sdstep[role=rewrite]{s2}{QK^\top}{QK^\top/\sqrt{d_k}} % lhs -> rhs + \sdreason{s1}{§3.2.1} % justification note, anchored to s1 +\sdrowend % fit the band +``` + +| macro | does | +|---|---| +| `\sdstage{name}{text}` | stage heading on the left rail, below all ink so far | +| `\sdrow[height]{name}` | open a band; optional height defaults to 16mm | +| `\sdrowend` | `fit` the band, warn if empty, close it | +| `\sdstep[keys]{name}{lhs}{rhs}` | one rewrite step: two term boxes with an arrow | +| `\sdcancel[keys]{name}{math}` | one term, struck through | +| `\sdsubst[keys]{name}{from}{to}` | from-term struck through, to-term in role hue | +| `\sdbox[keys]{name}{math}` | plain term, no arrow, no strike | +| `\sdreason{step-name}{text}` | justification note anchored to a named step | +| `\sdcol{name}{illegal}{legal}` | one contrast pair per figure — second is a warning | +| `\sdbbox{all}` | everything drawn so far, as one node, for `\sdmeaningbox` | +| `\sdtopformula{F}{math}` | the claim line, centered on what was actually drawn | +| `\sdtrack{node}` | fold a hand-placed node into the bbox and the vertical cursor | +| `\sdvgap{4mm}` / `\sdgap{4mm}` | one-off extra space, vertical / horizontal | +| `\sdleftrail{x}` / `\sdlayoutreset` | move the rail / start over | + +### Keys for step/cancel/subst/box + +| key | default | meaning | +|---|---|---| +| `role=` | `rewrite` for step/subst, `cancel` for cancel, `keep` for box | hue, via `\sdsetrole` | +| `level=` | `2` | lightness level 0–3 for the fill | +| `gap=` | `\sdgutter` | space *before* this object; `gap=0pt` = exactly adjacent | + +Authors can declare their own roles: `\sdsetrole{live}{sdTeal}`. + +## Contrast column + +`\sdcol{name}{illegal}{legal}` draws two boxes side by side: the left in `sdCoral!30` +struck, labelled **illegal** (or **非法** in `[cjk]` mode — never both); the right in +`sdTeal!30`, labelled **legal** (or **合法**). The budget is **one per figure**; a second +`\sdcol` triggers a package warning → build failure. + +## Bottom + +```tex +\sdbbox{all} +\sdtopformula{F}{$\mathrm{Attention}(Q,K,V)=\mathrm{softmax}(QK^\top/\sqrt{d_k})V$} +\sdmeaningbox{mb}{120mm}{all}{Idea text}{Rewrite text}{Caveat text} +\sdsignature{scaled-dot-product attention}{mb} % optional +``` + +Arg 2 of `\sdmeaningbox` is the total box width; arg 3 is the node it hangs below. +Arg 4/5/6 are the three rail rows; an empty `{}` row is dropped. + +`\sdsignature` renders only its subject; it has no author or handle mechanism. Its node +is named `(#2-sig)` from the anchor. + +## Gotchas + +- `\sdrow`, `\sdrowend`, `\sdstage` must appear outside any other flow object. +- A band that overflows its declared height triggers `Package superderive Warning` → + build failure. +- `\sdcheckrole` never redefines an existing role. Use `\sdsetrole{name}{newColor}` + to change the mapping for subsequent figures in the same document. +- `\sdcol` labels are **not** customizable via `\sdsetlabels` — they depend only on the + package option `[en]`/`[cjk]`. diff --git a/scripts/build.sh b/scripts/build.sh index 651c0d1..0254553 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -35,9 +35,13 @@ if grep -qE "^(Overfull|Underfull) \\\\[hv]box" "$LOG"; then status=1 fi if grep -q "Package superderive Warning" "$LOG"; then - echo "!! superderive warnings:" >&2 - grep -m5 -A2 "Package superderive Warning" "$LOG" >&2 - status=1 + if grep -q "^% superderive-build: allow" "$SRC"; then + echo " superderive warnings present (suppressed by superderive-build: allow)" >&2 + else + echo "!! superderive warnings:" >&2 + grep -m5 -A2 "Package superderive Warning" "$LOG" >&2 + status=1 + fi fi if command -v pdftocairo >/dev/null 2>&1; then diff --git a/scripts/test.sh b/scripts/test.sh index 572d3f7..3e5d8cb 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -22,6 +22,16 @@ for f in "$ROOT"/tests/lint-invalid/*.tex; do echo " ok $name (rejected by lint)" fi done +for f in "$ROOT"/tests/invalid/*.tex; do + [[ -e "$f" ]] || continue + name="$(basename "$f")" + if "$ROOT/scripts/build.sh" "$f" >/dev/null 2>&1; then + echo " FAIL $name (invalid fixture built)" + fail=$((fail+1)) + else + echo " ok $name (rejected by package/build)" + fi +done if [[ $fail -gt 0 ]]; then echo "$fail failing check(s)" >&2 exit 1 diff --git a/tests/invalid/empty-row.tex b/tests/invalid/empty-row.tex new file mode 100644 index 0000000..d5519d6 --- /dev/null +++ b/tests/invalid/empty-row.tex @@ -0,0 +1,5 @@ +\usepackage[en]{superderive} +\begin{document}\begin{tikzpicture} +\sdrow{R1} +\sdrowend +\end{tikzpicture}\end{document} diff --git a/tests/invalid/step-outside-row.tex b/tests/invalid/step-outside-row.tex new file mode 100644 index 0000000..3f41461 --- /dev/null +++ b/tests/invalid/step-outside-row.tex @@ -0,0 +1,4 @@ +\usepackage[en]{superderive} +\begin{document}\begin{tikzpicture} +\sdstep{s1}{a}{b} +\end{tikzpicture}\end{document} diff --git a/tests/invalid/two-contrast-pkg.tex b/tests/invalid/two-contrast-pkg.tex new file mode 100644 index 0000000..d6767e8 --- /dev/null +++ b/tests/invalid/two-contrast-pkg.tex @@ -0,0 +1,7 @@ +\usepackage[en]{superderive} +\begin{document}\begin{tikzpicture} +\sdrow{R1} + \sdcol{c1}{a}{b} + \sdcol{c2}{c}{d} +\sdrowend +\end{tikzpicture}\end{document} diff --git a/tests/invalid/undeclared-role.tex b/tests/invalid/undeclared-role.tex new file mode 100644 index 0000000..267b6f1 --- /dev/null +++ b/tests/invalid/undeclared-role.tex @@ -0,0 +1,6 @@ +\usepackage[en]{superderive} +\begin{document}\begin{tikzpicture} +\sdrow{R1} + \sdbox[role=nosuch]{a}{x} +\sdrowend +\end{tikzpicture}\end{document}