feat: add superderive stepwise derivation figures
A third sibling toolkit: rewrite steps, justification rails, cancel/substitute highlights, and lint-on-warning builds.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
build/
|
||||
*.aux
|
||||
*.log
|
||||
*.out
|
||||
*.fls
|
||||
*.fdb_latexmk
|
||||
*.synctex.gz
|
||||
@@ -0,0 +1,25 @@
|
||||
# superderive
|
||||
|
||||
Stepwise algebraic derivation figures. Third sibling of `superfig` /
|
||||
`supertensor`. Same muted palette and lint-on-warning build. The
|
||||
primitives are rewrite steps, not nodes or tensor faces.
|
||||
|
||||
This repository is a **child** of SuperPaper. Clone SuperPaper with
|
||||
`--recurse-submodules` for the family; use this directory alone when
|
||||
you only need a derivation figure.
|
||||
|
||||
```
|
||||
SKILL.md
|
||||
assets/superderive.sty
|
||||
scripts/{preflight,lint,build,test}
|
||||
examples/rewrite-cancel.tex
|
||||
```
|
||||
|
||||
```bash
|
||||
./scripts/preflight.sh
|
||||
./scripts/build.sh examples/rewrite-cancel.tex
|
||||
./scripts/test.sh
|
||||
```
|
||||
|
||||
Provenance: specified in SuperPaper `DESIGN.md` Layer 2. Not grown
|
||||
out of `superfig.sty`.
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: superderive
|
||||
description: >-
|
||||
Create or refine stepwise algebraic derivation figures — rewrite
|
||||
steps, justification rails, cancel and substitute highlights.
|
||||
Use when a rewrite must be seen (cancel-visual, subst-visual, or
|
||||
more than four steps), or the user asks for $superderive. Not for
|
||||
architecture (superfig), tensor faces (supertensor), or ordinary
|
||||
note-side align (superpaper v1).
|
||||
---
|
||||
|
||||
# superderive
|
||||
|
||||
Turn one rewrite sequence into one slide-ready figure with three zones:
|
||||
|
||||
1. **Top — claim.** The identity being justified.
|
||||
2. **Middle — steps.** `\sdstep` / `\sdcancel` / `\sdsubst` / `\sdreason`.
|
||||
3. **Bottom — meaning.** Idea, rewrite, caveat.
|
||||
|
||||
Draw with the package. Do not hand-roll TikZ unless the linter allows it.
|
||||
|
||||
## When to use
|
||||
|
||||
- A cancel or substitution that is the claim.
|
||||
- More than four rewrite steps that must stay aligned as a picture.
|
||||
- An illegal vs legal pair (`\sdcol`, one per figure).
|
||||
|
||||
If `expand: true` and steps ≤ 4 and there is no cancel/subst visual,
|
||||
superpaper still uses `align`. This package is for when the rewrite
|
||||
must be *seen*.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. `./scripts/preflight.sh`
|
||||
2. One rewrite path. Drop equivalent expansions.
|
||||
3. Roles: `keep` / `rewrite` / `cancel` / `intro` (already declared).
|
||||
4. Smallest grammar: `\sdstep` + `\sdreason`; add `\sdcancel` / `\sdsubst` only when they carry information.
|
||||
5. `./scripts/build.sh fig.tex` then audit `references/checklist.md`.
|
||||
|
||||
## Output
|
||||
|
||||
Editable TikZ. Build emits PDF, SVG, PNG, transparent PNG, thumbnail.
|
||||
|
||||
Chinese: `\usepackage[cjk]{superderive}`. English: `[en]`.
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "Superderive"
|
||||
short_description: "Turn a rewrite sequence into a step/justification figure"
|
||||
default_prompt: >-
|
||||
Use $superderive to turn a rewrite sequence into a step/justification
|
||||
figure. Not for architecture (superfig) or tensor faces (supertensor).
|
||||
@@ -0,0 +1,297 @@
|
||||
%% superderive.sty -- stepwise algebraic derivation figures
|
||||
%% Third sibling of superfig / supertensor. Same house style and
|
||||
%% role/cursor/warning discipline. Primitives are rewrite steps,
|
||||
%% not nodes or tensor faces.
|
||||
%%
|
||||
%% Options: cjk XeLaTeX + fandol
|
||||
%% en English meaning-box rails
|
||||
\NeedsTeXFormat{LaTeX2e}
|
||||
\ProvidesPackage{superderive}[2026/08/17 v1.0 stepwise derivation figures]
|
||||
|
||||
\newif\ifder@cjk\der@cjkfalse
|
||||
\newif\ifder@en\der@enfalse
|
||||
\DeclareOption{cjk}{\der@cjktrue}
|
||||
\DeclareOption{en}{\der@entrue}
|
||||
\DeclareOption{zh}{\der@enfalse}
|
||||
\ProcessOptions\relax
|
||||
|
||||
\RequirePackage{amsmath}
|
||||
\RequirePackage{amssymb}
|
||||
\RequirePackage{xcolor}
|
||||
\RequirePackage{etoolbox}
|
||||
\RequirePackage{array}
|
||||
\RequirePackage{tikz}
|
||||
\usetikzlibrary{calc,positioning,arrows.meta,fit}
|
||||
|
||||
\ifder@cjk
|
||||
\RequirePackage[UTF8,fontset=fandol]{ctex}
|
||||
\fi
|
||||
|
||||
\pgfdeclarelayer{sdbg}
|
||||
\pgfdeclarelayer{sdfg}
|
||||
\pgfsetlayers{sdbg,main,sdfg}
|
||||
|
||||
\definecolor{sdTeal}{HTML}{4F8FA5}
|
||||
\definecolor{sdOrange}{HTML}{EE995B}
|
||||
\definecolor{sdCoral}{HTML}{C95B5B}
|
||||
\definecolor{sdViolet}{HTML}{8A74B5}
|
||||
\definecolor{sdGray}{HTML}{85898F}
|
||||
\definecolor{sdInk}{HTML}{1A1A1A}
|
||||
|
||||
\newcommand{\sdsetrole}[2]{%
|
||||
\edef\sd@newrole{#2}%
|
||||
\ifcsname sd@role@#1\endcsname
|
||||
\edef\sd@oldrole{\csname sd@role@#1\endcsname}%
|
||||
\ifx\sd@oldrole\sd@newrole\else
|
||||
\PackageWarning{superderive}{Role `#1' was already mapped to
|
||||
`\sd@oldrole' and cannot be remapped to `\sd@newrole'}%
|
||||
\fi
|
||||
\else
|
||||
\expandafter\gdef\csname sd@role@#1\endcsname{#2}%
|
||||
\fi}
|
||||
\newcommand{\sdrole}[1]{%
|
||||
\ifcsname sd@role@#1\endcsname\csname sd@role@#1\endcsname\else sdGray\fi}
|
||||
\newcommand{\sdcheckrole}[1]{%
|
||||
\ifcsname sd@role@#1\endcsname\else
|
||||
\PackageWarning{superderive}{Undeclared role `#1' -- drawn in neutral gray.
|
||||
Declare it with \string\sdsetrole}%
|
||||
\fi}
|
||||
\sdsetrole{neutral}{sdGray}
|
||||
\sdsetrole{keep}{sdTeal}
|
||||
\sdsetrole{rewrite}{sdOrange}
|
||||
\sdsetrole{cancel}{sdCoral}
|
||||
\sdsetrole{intro}{sdViolet}
|
||||
\sdsetrole{warn}{sdCoral}
|
||||
|
||||
\newcommand{\sdlevelpct}[1]{\ifcase#1 0\or30\or55\or80\else55\fi}
|
||||
|
||||
\tikzset{
|
||||
sd stage/.style = {font=\small\bfseries, text=black!55, inner sep=2pt},
|
||||
sd note/.style = {font=\scriptsize, text=black!55, inner sep=2pt, align=left},
|
||||
sd arrow/.style = {-{Stealth[length=2.2mm,width=1.6mm]}, draw=black!45, line width=0.5pt},
|
||||
sd term/.style = {draw=black!60, line width=0.5pt, rounded corners=1.5pt,
|
||||
inner sep=6pt, font=\small, text=sdInk, align=center},
|
||||
sd reason/.style= {font=\scriptsize, text=black!55, align=left,
|
||||
draw=black!18, fill=black!3, rounded corners=1.5pt,
|
||||
inner xsep=6pt, inner ysep=5pt},
|
||||
}
|
||||
|
||||
\newlength{\sdgutter}\setlength{\sdgutter}{6mm}
|
||||
\newlength{\sdrowgap}\setlength{\sdrowgap}{5mm}
|
||||
\newlength{\sdblockgap}\setlength{\sdblockgap}{9mm}
|
||||
\newlength{\sd@railx}\newlength{\sd@cx}\newlength{\sd@cy}
|
||||
\newlength{\sd@ycur}\newlength{\sd@bandh}\newlength{\sd@tmpx}\newlength{\sd@tmpy}
|
||||
\newif\ifder@inrow
|
||||
\newif\ifder@first
|
||||
\newcount\sd@ncols
|
||||
|
||||
\newcommand{\sdlayoutreset}{%
|
||||
\global\sd@railx=0pt \global\sd@ycur=0pt
|
||||
\global\sd@cx=0pt \global\sd@cy=0pt \global\sd@bandh=0pt
|
||||
\global\der@inrowfalse
|
||||
\global\sd@ncols=0
|
||||
\gdef\sd@rowlist{}\gdef\sd@alllist{}\gdef\sd@rowname{}%
|
||||
\gdef\sd@lastnode{}}
|
||||
\sdlayoutreset
|
||||
\newcommand{\sdleftrail}[1]{\global\sd@railx=\dimexpr#1\relax}
|
||||
\newcommand{\sdvgap}[1]{\global\advance\sd@ycur by -\dimexpr#1\relax}
|
||||
\newcommand{\sdgap}[1]{\global\advance\sd@cx by \dimexpr#1\relax}
|
||||
|
||||
\newcommand{\sd@lower}[1]{%
|
||||
\pgfextracty{\sd@tmpy}{\pgfpointanchor{#1}{south}}%
|
||||
\ifdim\sd@tmpy<\sd@ycur \global\sd@ycur=\sd@tmpy \fi}
|
||||
\newcommand{\sd@regall}[1]{\xdef\sd@alllist{\sd@alllist(#1)}}
|
||||
\newcommand{\sd@regrow}[1]{%
|
||||
\xdef\sd@rowlist{\sd@rowlist(#1)}\sd@regall{#1}%
|
||||
\gdef\sd@lastnode{#1}}
|
||||
\newcommand{\sd@needrow}[1]{%
|
||||
\ifder@inrow\else
|
||||
\PackageError{superderive}{\string#1\space needs an open \string\sdrow}%
|
||||
{Close placement only works between \string\sdrow\space and \string\sdrowend.}%
|
||||
\fi}
|
||||
\newcommand{\sd@leadgap}[1]{%
|
||||
\ifder@first
|
||||
\global\der@firstfalse
|
||||
\else
|
||||
\global\advance\sd@cx by \dimexpr#1\relax
|
||||
\fi}
|
||||
|
||||
\newcommand{\sdstage}[2]{%
|
||||
\dimen0=\sd@ycur \advance\dimen0 by -\sdblockgap
|
||||
\node[sd stage, anchor=north west] (#1) at (\the\sd@railx,\the\dimen0) {#2};
|
||||
\sd@regall{#1}\sd@lower{#1}}
|
||||
|
||||
% \sdrow{name} or \sdrow[height]{name}
|
||||
\newcommand{\sdrow}[2][16mm]{%
|
||||
\gdef\sd@rowlist{}\gdef\sd@rowname{#2}\gdef\sd@lastnode{}%
|
||||
\global\der@inrowtrue\global\der@firsttrue
|
||||
\pgfmathsetlengthmacro{\sd@bh}{#1}%
|
||||
\global\sd@bandh=\sd@bh
|
||||
\global\sd@cx=\sd@railx
|
||||
\dimen0=\sd@ycur \advance\dimen0 by -\sdrowgap \advance\dimen0 by -0.5\sd@bandh
|
||||
\global\sd@cy=\dimen0}
|
||||
|
||||
\newcommand{\sdrowend}{%
|
||||
\ifdefempty{\sd@rowlist}%
|
||||
{\PackageWarning{superderive}{Empty \string\sdrow\space `\sd@rowname'}}%
|
||||
{\edef\sd@do{\noexpand\node[inner sep=0pt, outer sep=0pt,
|
||||
fit={\sd@rowlist}] (\sd@rowname) {};}\sd@do
|
||||
\sd@regall{\sd@rowname}\sd@lower{\sd@rowname}}%
|
||||
\global\der@inrowfalse}
|
||||
|
||||
\newcommand{\sdbbox}[1]{%
|
||||
\edef\sd@do{\noexpand\node[inner sep=0pt, outer sep=0pt,
|
||||
fit={\sd@alllist}] (#1) {};}\sd@do}
|
||||
\newcommand{\sdtrack}[1]{\sd@regall{#1}\sd@lower{#1}}
|
||||
\newcommand{\sdtopformula}[2]{%
|
||||
\sdbbox{sd@bbt}%
|
||||
\node[above=6mm of sd@bbt, anchor=south, font=\large] (#1) {#2};
|
||||
\sd@regall{#1}}
|
||||
|
||||
\pgfkeys{
|
||||
/sd/term/.cd,
|
||||
role/.store in=\sd@role,
|
||||
level/.store in=\sd@level,
|
||||
gap/.store in=\sd@gap,
|
||||
role=rewrite, level=2, gap=\sdgutter,
|
||||
}
|
||||
|
||||
% \sdstep[keys]{name}{lhs}{rhs}
|
||||
\newcommand{\sdstep}[4][]{%
|
||||
\sd@needrow{\sdstep}%
|
||||
\begingroup
|
||||
\pgfkeys{/sd/term/.cd,#1}%
|
||||
\sdcheckrole{\sd@role}%
|
||||
\edef\sd@col{\sdrole{\sd@role}}%
|
||||
\sd@leadgap{\sd@gap}%
|
||||
\node[anchor=west, inner sep=0pt] (#2) at (\the\sd@cx,\the\sd@cy) {%
|
||||
\begin{tikzpicture}[baseline=(sd@mid)]
|
||||
\node[sd term, fill=\sd@col!\sdlevelpct{\sd@level}] (sd@L) {$\displaystyle #3$};
|
||||
\node[sd term, fill=\sd@col!\sdlevelpct{\sd@level},
|
||||
right=\sdgutter of sd@L] (sd@R) {$\displaystyle #4$};
|
||||
\coordinate (sd@mid) at ($(sd@L.east)!0.5!(sd@R.west)$);
|
||||
\begin{pgfonlayer}{sdbg}
|
||||
\draw[sd arrow] (sd@L.east) -- (sd@R.west);
|
||||
\end{pgfonlayer}
|
||||
\end{tikzpicture}};%
|
||||
\pgfextractx{\sd@tmpx}{\pgfpointanchor{#2}{east}}%
|
||||
\global\sd@cx=\sd@tmpx
|
||||
\sd@regrow{#2}%
|
||||
\endgroup}
|
||||
|
||||
% \sdreason{step-name}{text} -- hangs off the named step, or the last step.
|
||||
\newcommand{\sdreason}[2]{%
|
||||
\sd@needrow{\sdreason}%
|
||||
\sd@leadgap{\sdgutter}%
|
||||
\node[sd reason, anchor=west, text width=38mm] (#1-why)
|
||||
at (\the\sd@cx,\the\sd@cy) {#2};%
|
||||
\pgfextractx{\sd@tmpx}{\pgfpointanchor{#1-why}{east}}%
|
||||
\global\sd@cx=\sd@tmpx
|
||||
\sd@regrow{#1-why}}
|
||||
|
||||
% \sdcancel[keys]{name}{math}
|
||||
\newcommand{\sdcancel}[3][]{%
|
||||
\sd@needrow{\sdcancel}%
|
||||
\begingroup
|
||||
\pgfkeys{/sd/term/.cd,role=cancel,#1}%
|
||||
\sdcheckrole{\sd@role}%
|
||||
\edef\sd@col{\sdrole{\sd@role}}%
|
||||
\sd@leadgap{\sd@gap}%
|
||||
\node[sd term, fill=\sd@col!\sdlevelpct{\sd@level}, anchor=west] (#2)
|
||||
at (\the\sd@cx,\the\sd@cy) {$\displaystyle #3$};%
|
||||
\draw[\sd@col, line width=0.8pt]
|
||||
($(#2.west)+(1.2mm,0)$) -- ($(#2.east)+(-1.2mm,0)$);
|
||||
\pgfextractx{\sd@tmpx}{\pgfpointanchor{#2}{east}}%
|
||||
\global\sd@cx=\sd@tmpx
|
||||
\sd@regrow{#2}%
|
||||
\endgroup}
|
||||
|
||||
% \sdsubst[keys]{name}{from}{to}
|
||||
\newcommand{\sdsubst}[4][]{%
|
||||
\sd@needrow{\sdsubst}%
|
||||
\begingroup
|
||||
\pgfkeys{/sd/term/.cd,role=rewrite,#1}%
|
||||
\sdcheckrole{\sd@role}%
|
||||
\edef\sd@col{\sdrole{\sd@role}}%
|
||||
\sd@leadgap{\sd@gap}%
|
||||
\node[anchor=west, inner sep=0pt] (#2) at (\the\sd@cx,\the\sd@cy) {%
|
||||
\begin{tikzpicture}[baseline=(sd@mid)]
|
||||
\node[sd term, fill=sdCoral!30] (sd@F) {$\displaystyle #3$};
|
||||
\draw[sdCoral, line width=0.7pt]
|
||||
($(sd@F.west)+(1mm,0)$) -- ($(sd@F.east)+(-1mm,0)$);
|
||||
\node[sd term, fill=\sd@col!\sdlevelpct{\sd@level},
|
||||
right=\sdgutter of sd@F] (sd@T) {$\displaystyle #4$};
|
||||
\coordinate (sd@mid) at ($(sd@F.east)!0.5!(sd@T.west)$);
|
||||
\draw[sd arrow] (sd@F.east) -- (sd@T.west);
|
||||
\end{tikzpicture}};%
|
||||
\pgfextractx{\sd@tmpx}{\pgfpointanchor{#2}{east}}%
|
||||
\global\sd@cx=\sd@tmpx
|
||||
\sd@regrow{#2}%
|
||||
\endgroup}
|
||||
|
||||
% \sdbox[keys]{name}{math}
|
||||
\newcommand{\sdbox}[3][]{%
|
||||
\sd@needrow{\sdbox}%
|
||||
\begingroup
|
||||
\pgfkeys{/sd/term/.cd,role=keep,#1}%
|
||||
\sdcheckrole{\sd@role}%
|
||||
\edef\sd@col{\sdrole{\sd@role}}%
|
||||
\sd@leadgap{\sd@gap}%
|
||||
\node[sd term, fill=\sd@col!\sdlevelpct{\sd@level}, anchor=west] (#2)
|
||||
at (\the\sd@cx,\the\sd@cy) {$\displaystyle #3$};%
|
||||
\pgfextractx{\sd@tmpx}{\pgfpointanchor{#2}{east}}%
|
||||
\global\sd@cx=\sd@tmpx
|
||||
\sd@regrow{#2}%
|
||||
\endgroup}
|
||||
|
||||
% \sdcol{name}{illegal}{legal} -- one contrast pair per figure.
|
||||
\newcommand{\sdcol}[3]{%
|
||||
\sd@needrow{\sdcol}%
|
||||
\global\advance\sd@ncols by 1
|
||||
\ifnum\sd@ncols>1
|
||||
\PackageWarning{superderive}{Contrast column `#1' is the second pair
|
||||
in the figure. The budget is one illegal/legal pair}%
|
||||
\fi
|
||||
\sd@leadgap{\sdgutter}%
|
||||
\node[anchor=west, inner sep=0pt] (#1) at (\the\sd@cx,\the\sd@cy) {%
|
||||
\begin{tikzpicture}[baseline=(sd@mid)]
|
||||
\node[sd term, fill=sdCoral!30] (sd@B) {$\displaystyle #2$};
|
||||
\node[font=\scriptsize, text=sdCoral, above=1pt of sd@B] {非法};
|
||||
\node[sd term, fill=sdTeal!30, right=8mm of sd@B] (sd@G) {$\displaystyle #3$};
|
||||
\node[font=\scriptsize, text=sdTeal, above=1pt of sd@G] {合法};
|
||||
\coordinate (sd@mid) at ($(sd@B.east)!0.5!(sd@G.west)$);
|
||||
\end{tikzpicture}};%
|
||||
\pgfextractx{\sd@tmpx}{\pgfpointanchor{#1}{east}}%
|
||||
\global\sd@cx=\sd@tmpx
|
||||
\sd@regrow{#1}}
|
||||
|
||||
\ifder@en
|
||||
\def\sd@lblidea{Idea}\def\sd@lblrw{Rewrite}\def\sd@lblcaveat{Caveat}
|
||||
\else
|
||||
\def\sd@lblidea{思路}\def\sd@lblrw{改写}\def\sd@lblcaveat{注意}
|
||||
\fi
|
||||
\newcommand{\sdsetlabels}[3]{\def\sd@lblidea{#1}\def\sd@lblrw{#2}\def\sd@lblcaveat{#3}}
|
||||
\newlength{\sd@raillen}
|
||||
\ifder@en
|
||||
\setlength{\sd@raillen}{5.4em}
|
||||
\else
|
||||
\setlength{\sd@raillen}{3.2em}
|
||||
\fi
|
||||
|
||||
\newcommand{\sdmeaningbox}[6]{%
|
||||
\node[below=4mm of #3, anchor=north,
|
||||
draw=black!18, fill=black!3, rounded corners=1.5pt,
|
||||
inner xsep=9pt, inner ysep=8pt, text width=#2] (#1) {%
|
||||
\small
|
||||
\setlength{\tabcolsep}{0pt}%
|
||||
\renewcommand{\arraystretch}{1.25}%
|
||||
\begin{tabular}{@{}p{\sd@raillen}@{\hspace{7pt}}p{\dimexpr#2-\sd@raillen-7pt\relax}@{}}
|
||||
\ifblank{#4}{}{\textbf{\sd@lblidea} & \raggedright\arraybackslash #4 \\}
|
||||
\ifblank{#5}{}{\textbf{\sd@lblrw} & \raggedright\arraybackslash #5 \\}
|
||||
\ifblank{#6}{}{\textbf{\sd@lblcaveat} & \raggedright\arraybackslash #6 \\}
|
||||
\end{tabular}};}
|
||||
|
||||
\newcommand{\sdsignature}[2]{%
|
||||
\node[below=2.2mm of #2, font=\scriptsize, text=black!45] (#2-sig) {#1};}
|
||||
|
||||
\endinput
|
||||
@@ -0,0 +1,20 @@
|
||||
% Golden example -- a scale rewrite that must be seen, not just aligned.
|
||||
% ../scripts/build.sh rewrite-cancel.tex
|
||||
\documentclass[border=10pt]{standalone}
|
||||
\usepackage[cjk]{superderive}
|
||||
|
||||
\begin{document}
|
||||
\begin{tikzpicture}
|
||||
\sdstage{D1}{缩放来自方差,不是装饰}
|
||||
\sdrow{R1}
|
||||
\sdstep[role=rewrite]{s1}{QK^{\top}}{QK^{\top}/\sqrt{d_k}}
|
||||
\sdreason{s1}{§3.2.1,避免 softmax 饱和}
|
||||
\sdrowend
|
||||
\sdbbox{all}
|
||||
\sdtopformula{F}{$\mathrm{Attention}(Q,K,V)=\mathrm{softmax}(QK^{\top}/\sqrt{d_k})V$}
|
||||
\sdmeaningbox{mb}{120mm}{all}
|
||||
{点积方差随 $d_k$ 线性涨}
|
||||
{缩放是改写,不是新算子}
|
||||
{没有缩放,softmax 进饱和区,梯度消失}
|
||||
\end{tikzpicture}
|
||||
\end{document}
|
||||
@@ -0,0 +1,6 @@
|
||||
# Antipatterns
|
||||
|
||||
- Drawing a rewrite as `\sfnode` boxes.
|
||||
- A new hue per step.
|
||||
- Two illegal/legal pairs in one figure.
|
||||
- Formula placed before the last row so it centers on an unknown width.
|
||||
@@ -0,0 +1,7 @@
|
||||
# API
|
||||
|
||||
`\usepackage[cjk]{superderive}` or `[en]`.
|
||||
|
||||
Layout: `\sdstage{n}{text}` `\sdrow[height]{name}` … `\sdrowend` `\sdbbox{all}` `\sdtopformula{F}{math}`.
|
||||
|
||||
Call `\sdtopformula` after the last `\sdrowend`.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Checklist
|
||||
|
||||
- Each step is one rewrite. Reasons sit on the rail, not on the shaft.
|
||||
- Cancel/subst encode the actual cancelled or replaced term.
|
||||
- At most one `\sdcol`. At most four active hues plus gray.
|
||||
- No raw `\draw`. Formula after the last row.
|
||||
- Clean `build.sh` (missing glyphs, overfull, package warnings).
|
||||
@@ -0,0 +1,3 @@
|
||||
# Fallback
|
||||
|
||||
`preflight.sh` 0/1/2 matches superfig. Exit 2: no XeLaTeX. Exit 1: no CJK or no pdftocairo.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Grammar
|
||||
|
||||
| Fact | Macro |
|
||||
|---|---|
|
||||
| one legal rewrite | `\sdstep[role=rewrite]{name}{lhs}{rhs}` |
|
||||
| why it is legal | `\sdreason{name}{text}` |
|
||||
| a cancelled term | `\sdcancel{name}{math}` |
|
||||
| substitution | `\sdsubst{name}{from}{to}` |
|
||||
| focus box | `\sdbox[role=keep]{name}{math}` |
|
||||
| illegal vs legal | `\sdcol{name}{bad}{good}` (one per figure) |
|
||||
| bottom | `\sdmeaningbox` (idea / rewrite / caveat) |
|
||||
|
||||
Built-in roles: `keep` `rewrite` `cancel` `intro` `warn` `neutral`.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Layout
|
||||
|
||||
Place by cursor inside `\sdrow` … `\sdrowend`. Never hand-tune a neighbour with a magic offset. If a reason does not fit, move the step to the next row.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Style
|
||||
|
||||
Palette `sdTeal #4F8FA5` `sdOrange #EE995B` `sdCoral #C95B5B` `sdViolet #8A74B5` `sdGray #85898F`. One hue per role. ≤4 active hues plus gray. Lightness `30/55/80`.
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# ./scripts/build.sh figure.tex [outdir]
|
||||
set -euo pipefail
|
||||
SRC="${1:?usage: build.sh figure.tex [outdir]}"
|
||||
[[ -f "$SRC" ]] || { echo "no such file: $SRC" >&2; exit 2; }
|
||||
SRCDIR="$(cd "$(dirname "$SRC")" && pwd)"
|
||||
BASE="$(basename "$SRC" .tex)"
|
||||
OUT="${2:-$SRCDIR/build}"
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
mkdir -p "$OUT"
|
||||
|
||||
if [[ "${SD_SKIP_LINT:-0}" != "1" ]]; then
|
||||
echo "==> lint $BASE"
|
||||
python3 "$ROOT/scripts/lint.py" "$SRC"
|
||||
fi
|
||||
|
||||
echo "==> xelatex $BASE"
|
||||
TEXINPUTS="$ROOT/assets:$SRCDIR:" \
|
||||
xelatex -halt-on-error -interaction=nonstopmode \
|
||||
-output-directory="$OUT" "$SRC" >/dev/null 2>&1 \
|
||||
|| { echo "!! xelatex failed; last errors:" >&2
|
||||
grep -n -A4 -m3 '^!' "$OUT/$BASE.log" >&2 || tail -30 "$OUT/$BASE.log" >&2
|
||||
exit 1; }
|
||||
|
||||
LOG="$OUT/$BASE.log"
|
||||
status=0
|
||||
if grep -q "Missing character" "$LOG"; then
|
||||
echo "!! missing glyphs:" >&2
|
||||
grep -m5 "Missing character" "$LOG" >&2
|
||||
status=1
|
||||
fi
|
||||
if grep -qE "^(Overfull|Underfull) \\\\[hv]box" "$LOG"; then
|
||||
echo "!! overfull/underfull boxes:" >&2
|
||||
grep -m5 -E "^(Overfull|Underfull) \\\\[hv]box" "$LOG" >&2
|
||||
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
|
||||
fi
|
||||
|
||||
if command -v pdftocairo >/dev/null 2>&1; then
|
||||
echo "==> exporting svg / png"
|
||||
pdftocairo -svg "$OUT/$BASE.pdf" "$OUT/$BASE.svg"
|
||||
pdftocairo -png -r 300 -singlefile "$OUT/$BASE.pdf" "$OUT/$BASE"
|
||||
pdftocairo -png -r 300 -singlefile -transp "$OUT/$BASE.pdf" "$OUT/$BASE-alpha"
|
||||
pdftocairo -png -scale-to-x 360 -scale-to-y -1 -singlefile \
|
||||
"$OUT/$BASE.pdf" "$OUT/$BASE-thumb"
|
||||
else
|
||||
echo "!! pdftocairo missing: PDF only" >&2
|
||||
status=1
|
||||
fi
|
||||
echo "==> artifacts in $OUT"
|
||||
ls -1 "$OUT/$BASE"*.{pdf,svg,png} 2>/dev/null | sed 's/^/ /'
|
||||
exit $status
|
||||
Executable
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Source lint for superderive figures."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROLE_DECL_RE = re.compile(r"\\sdsetrole\{([^{}]+)\}\{([^{}]+)\}")
|
||||
ROLE_KEY_RE = re.compile(r"role\s*=\s*([A-Za-z0-9_-]+)")
|
||||
RAW_TIKZ_RE = re.compile(r"\\(?:draw|fill|path)\b")
|
||||
|
||||
|
||||
def strip_comments(source: str) -> str:
|
||||
return re.sub(r"(?<!\\)%.*$", "", source, flags=re.MULTILINE)
|
||||
|
||||
|
||||
def line_of(source: str, offset: int) -> int:
|
||||
return source.count("\n", 0, offset) + 1
|
||||
|
||||
|
||||
def directives(source: str) -> set[str]:
|
||||
found: set[str] = set()
|
||||
for match in re.finditer(r"^\s*%\s*superderive-lint:\s*(.+)$", source, re.MULTILINE):
|
||||
found.update(item.strip() for item in match.group(1).split(","))
|
||||
return found
|
||||
|
||||
|
||||
def lint(path: Path) -> list[str]:
|
||||
raw = path.read_text(encoding="utf-8")
|
||||
allowed = directives(raw)
|
||||
source = strip_comments(raw)
|
||||
errors: list[str] = []
|
||||
|
||||
declared = {
|
||||
"neutral": "sdGray",
|
||||
"keep": "sdTeal",
|
||||
"rewrite": "sdOrange",
|
||||
"cancel": "sdCoral",
|
||||
"intro": "sdViolet",
|
||||
"warn": "sdCoral",
|
||||
}
|
||||
for match in ROLE_DECL_RE.finditer(source):
|
||||
name, color = (part.strip() for part in match.groups())
|
||||
previous = declared.get(name)
|
||||
if previous is not None and previous != color and name != "neutral":
|
||||
errors.append(
|
||||
f"line {line_of(source, match.start())}: role {name!r} changes "
|
||||
f"from {previous!r} to {color!r}"
|
||||
)
|
||||
declared[name] = color
|
||||
|
||||
used_colors = {"sdGray"}
|
||||
for match in ROLE_KEY_RE.finditer(source):
|
||||
role = match.group(1)
|
||||
color = declared.get(role)
|
||||
if color is None:
|
||||
errors.append(
|
||||
f"line {line_of(source, match.start())}: role {role!r} is used "
|
||||
f"before \\sdsetrole"
|
||||
)
|
||||
else:
|
||||
used_colors.add(color)
|
||||
|
||||
active = used_colors - {"sdGray"}
|
||||
if len(active) > 4:
|
||||
errors.append(
|
||||
f"figure uses {len(active)} active hue families "
|
||||
f"({', '.join(sorted(active))}); maximum is 4 plus gray"
|
||||
)
|
||||
|
||||
if "allow-raw-tikz" not in allowed:
|
||||
for match in RAW_TIKZ_RE.finditer(source):
|
||||
errors.append(
|
||||
f"line {line_of(source, match.start())}: hand-rolled TikZ "
|
||||
f"drawing; use \\sdstep/\\sdcancel/\\sdsubst"
|
||||
)
|
||||
|
||||
formula_positions = [m.start() for m in re.finditer(r"\\sdtopformula\b", source)]
|
||||
row_ends = [m.start() for m in re.finditer(r"\\sdrowend\b", source)]
|
||||
if formula_positions and row_ends and formula_positions[-1] < row_ends[-1]:
|
||||
errors.append(
|
||||
f"line {line_of(source, formula_positions[-1])}: \\sdtopformula "
|
||||
f"must follow the last \\sdrowend"
|
||||
)
|
||||
|
||||
cols = list(re.finditer(r"\\sdcol\b", source))
|
||||
if len(cols) > 1 and "allow-multiple-contrast" not in allowed:
|
||||
errors.append(
|
||||
f"line {line_of(source, cols[1].start())}: second \\sdcol; "
|
||||
f"budget is one illegal/legal pair per figure"
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="lint a superderive .tex source")
|
||||
parser.add_argument("sources", nargs="+", type=Path)
|
||||
args = parser.parse_args()
|
||||
failed = False
|
||||
for path in args.sources:
|
||||
if not path.is_file():
|
||||
print(f"superderive-lint: no such file: {path}", file=sys.stderr)
|
||||
failed = True
|
||||
continue
|
||||
errors = lint(path)
|
||||
if errors:
|
||||
failed = True
|
||||
print(f"!! {path}", file=sys.stderr)
|
||||
for error in errors:
|
||||
print(f" {error}", file=sys.stderr)
|
||||
else:
|
||||
print(f" lint {path}")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# Exit 0 full, 1 degraded, 2 no LaTeX.
|
||||
set -uo pipefail
|
||||
QUIET=0
|
||||
[[ "${1:-}" == "--quiet" ]] && QUIET=1
|
||||
say() { [[ $QUIET -eq 1 ]] || echo -e "$*"; }
|
||||
ok=0; warn=0; fail=0
|
||||
check() {
|
||||
local name="$1"; shift
|
||||
if "$@" >/dev/null 2>&1; then say " ok $name"; return 0
|
||||
else say " MISS $name"; return 1; fi
|
||||
}
|
||||
say "superderive preflight"
|
||||
say "--- required ---"
|
||||
check "xelatex" command -v xelatex || fail=$((fail+1))
|
||||
check "standalone.cls" kpsewhich standalone.cls || fail=$((fail+1))
|
||||
check "tikz.sty" kpsewhich tikz.sty || fail=$((fail+1))
|
||||
check "etoolbox.sty" kpsewhich etoolbox.sty || fail=$((fail+1))
|
||||
say "--- chinese figures ---"
|
||||
check "ctex.sty" kpsewhich ctex.sty || warn=$((warn+1))
|
||||
check "fandol" kpsewhich FandolSong-Regular.otf || warn=$((warn+1))
|
||||
say "--- export ---"
|
||||
check "pdftocairo" command -v pdftocairo || warn=$((warn+1))
|
||||
if [[ $fail -gt 0 ]]; then say "RESULT: no LaTeX."; exit 2; fi
|
||||
if [[ $warn -gt 0 ]]; then say "RESULT: degraded."; exit 1; fi
|
||||
say "RESULT: full path."
|
||||
exit 0
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
fail=0
|
||||
for f in "$ROOT"/tests/*.tex "$ROOT"/examples/*.tex; do
|
||||
[[ -e "$f" ]] || continue
|
||||
name="$(basename "$f")"
|
||||
if "$ROOT/scripts/build.sh" "$f" >/dev/null 2>&1; then
|
||||
echo " ok $name"
|
||||
else
|
||||
echo " FAIL $name"
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
done
|
||||
for f in "$ROOT"/tests/lint-invalid/*.tex; do
|
||||
[[ -e "$f" ]] || continue
|
||||
name="$(basename "$f")"
|
||||
if python3 "$ROOT/scripts/lint.py" "$f" >/dev/null 2>&1; then
|
||||
echo " FAIL $name (invalid fixture passed lint)"
|
||||
fail=$((fail+1))
|
||||
else
|
||||
echo " ok $name (rejected by lint)"
|
||||
fi
|
||||
done
|
||||
if [[ $fail -gt 0 ]]; then
|
||||
echo "$fail failing check(s)" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "all superderive checks passed"
|
||||
@@ -0,0 +1,7 @@
|
||||
\usepackage[en]{superderive}
|
||||
\begin{document}\begin{tikzpicture}
|
||||
\sdtopformula{F}{$x$}
|
||||
\sdrow{R1}
|
||||
\sdbox{a}{x}
|
||||
\sdrowend
|
||||
\end{tikzpicture}\end{document}
|
||||
@@ -0,0 +1,15 @@
|
||||
\usepackage[en]{superderive}
|
||||
\sdsetrole{a}{sdTeal}
|
||||
\sdsetrole{b}{sdOrange}
|
||||
\sdsetrole{c}{sdCoral}
|
||||
\sdsetrole{d}{sdViolet}
|
||||
\sdsetrole{e}{sdInk}
|
||||
\begin{document}\begin{tikzpicture}
|
||||
\sdrow{R1}
|
||||
\sdbox[role=a]{a}{1}
|
||||
\sdbox[role=b]{b}{2}
|
||||
\sdbox[role=c]{c}{3}
|
||||
\sdbox[role=d]{d}{4}
|
||||
\sdbox[role=e]{e}{5}
|
||||
\sdrowend
|
||||
\end{tikzpicture}\end{document}
|
||||
@@ -0,0 +1,7 @@
|
||||
\usepackage[en]{superderive}
|
||||
\begin{document}\begin{tikzpicture}
|
||||
\sdrow{R1}
|
||||
\sdbox{a}{x}
|
||||
\draw (0,0) -- (1,0);
|
||||
\sdrowend
|
||||
\end{tikzpicture}\end{document}
|
||||
@@ -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}
|
||||
@@ -0,0 +1,15 @@
|
||||
\documentclass[border=10pt]{standalone}
|
||||
\usepackage[en]{superderive}
|
||||
\begin{document}
|
||||
\begin{tikzpicture}
|
||||
\sdstage{S}{smoke}
|
||||
\sdrow{R1}
|
||||
\sdbox{a}{x}
|
||||
\sdsubst{b}{x}{y}
|
||||
\sdcancel{c}{z}
|
||||
\sdrowend
|
||||
\sdbbox{all}
|
||||
\sdtopformula{F}{$x\to y$}
|
||||
\sdmeaningbox{mb}{80mm}{all}{idea}{rewrite}{caveat}
|
||||
\end{tikzpicture}
|
||||
\end{document}
|
||||
Reference in New Issue
Block a user