diff --git a/.claude/skills/naboj-authoring/SKILL.md b/.claude/skills/naboj-authoring/SKILL.md new file mode 100644 index 00000000..99eb6380 --- /dev/null +++ b/.claude/skills/naboj-authoring/SKILL.md @@ -0,0 +1,95 @@ +--- +name: naboj-authoring +description: Author, edit, and debug Náboj competition problem sources (source/naboj/**) using DGS's Markdown+Jinja+LaTeX pipeline. Use whenever the user is creating a new problem, editing problem.md / solution.md / answer.md / preamble.md / meta.yaml under source/naboj/phys/*/problems/** or source/naboj/chem/*/problems/**, debugging mdcheck violations, Jinja MissingVariablesError, pandoc/XeLaTeX errors, or extending core/ or modules/naboj (new filters, new physical constants in core/data/constants.yaml, new LaTeX macros in core/latex/*.tex, new templates in modules/naboj/templates). Applies to all Náboj volumes (phys/26, phys/27, phys/28, chem/*, ...) — the format is stable across volumes with only minor differences. +--- + +# Náboj problem authoring (DGS) + +DGS renders each problem through this pipeline: + +``` +source/naboj///problems///{problem,solution}.md (Markdown + Jinja) + + source/naboj///problems//{preamble,answer,answer-also,answer-interval}.md + + source/naboj///problems//meta.yaml + → render/naboj///problems///*.md (pure Markdown, no Jinja) + → build/naboj///problems///*.tex (pandoc → XeLaTeX) + → PDF (booklet, tearoff, answers, solutions, ...) +``` + +Preamble + meta.yaml are prepended into the Jinja context; the Jinja renderer runs **twice** so +substituted equations get their inner tags expanded on the second pass. Then the DGS Markdown +style linter (`core/mdcheck`) runs, and pandoc converts to TeX using DGS's custom class +(`core/latex/dgs.cls`, plus `math.tex`, `symbols.tex`, `siunitx.tex`, `hacks.tex`). + +## Where to start + +**Route by task:** + +- Authoring / editing a problem (`problem.md`, `solution.md`, `meta.yaml`, `preamble.md`, + `answer*.md`) → read `references/layout.md`, then `references/markdown-extensions.md`. +- Using `(§ … §)` templating, `@J set …`, math filters, `Q(…)`, `const.g`, etc. → + `references/jinja-templating.md`. +- Defining `values:` in meta.yaml, using `PhysicsQuantity`, `.eq`, `.approx`, `.widen`, ranges, + formatting filters (`|f2`, `|g3`, `|ef2`, `|af2`, `|w(0.05)`) → + `references/quantities-and-constants.md`. +- Using / adding custom LaTeX macros (`\Int`, `\Sum`, `\Ceil`, `\Nuclide`, `\Implies`, …) → + `references/latex-macros.md`. +- Style linter failures (missing spaces around `=`, `\SI` vs `\qty`, label conventions, + `\frac` in answers, …) → `references/markdown-extensions.md` §Style checker. + +## Ground rules + +1. **Do not invent macros.** Every math command not in vanilla amsmath comes from + `core/latex/*.tex`. Grep `core/latex/symbols.tex` and `core/latex/math.tex` before + introducing a new one. +2. **Use `\qty` / `\num` / `\ang` — never `\SI`.** `mdcheck` fails the build on `\SI`. +3. **Use `\Implies`, not `\implies` or `\Rightarrow`.** Same rule for `\Int`/`\Sum` over + `\int`/`\sum`, `\Ceil{…}` over `\lceil…\rceil`, `\ang{…}` over `^\circ`. +4. **Preserve spaces around binary operators** in math: `= \approx \doteq \geq \leq \gg \ll + + \cdot`. `mdcheck` flags each violation with a caret pointing at the column. +5. **Labels start with the problem id.** In `solution.md` you must add a + sublabel (`{#eq:archery:hd}`). In `problem.md` you can go bare + (`{#eq:archery}`) or sub-labelled (`{#fig:archery:diagram}`) — most + `problem.md` equations get no label at all. +6. **Answers use `\dfrac`, not `\frac`.** `mdcheck` enforces this on `answer.md` / + `answer-also.md` / `answer-interval.md`. +7. **Two-pass rendering matters.** If a value expands into a `\qty{…}` that itself contains + Jinja tags (e.g. from a MathObject), it still gets rendered on the second pass. Don't + try to escape or defer — just write the natural thing. + +## Rendering pipeline entry points + +- Convertor for problems: `modules.naboj.builder.renderer` (subclass of + `core.builder.renderer.CLIInterface`). Meta.yaml + preamble.md become the context. +- Jinja setup: `core/builder/jinja.py` — see `MarkdownJinjaRenderer` for the exact filter / + global table. +- Style linter: `core/markdown-check.py` runs `core/mdcheck/check.py` rules per line. +- Templates that assemble PDFs: `modules/naboj/templates/*.jtex` (these use `(* … *)` for + variables, unlike `.md` files which use `(§ … §)`). +- Build orchestration: root `Makefile` + `modules/naboj/module.mk` (rules + `NABOJ_TRANSLATABLE` for problem/solution, `NABOJ_NONTRANSLATABLE` for + answer/answer-also/answer-interval). + +## Volume differences to be aware of + +- `phys/28` uses `authors:` (plural, list). `phys/27` mostly uses `author:` (singular). + Both are tolerated; new problems should use `authors: [...]`. +- Older volumes often lack `values:` entirely; numbers are hard-coded in the Markdown. +- `chem/*` volumes use a flatter layout (`source/naboj/chem//problems//…`) + and use chemistry macros (`\ce{…}`, `\Nuclide{…}`, `\chemfig{…}`). Their `sk/` may be + the only language directory. + +## Environment / build + +The project uses **uv** (see recent commit "Switched to uv"). To build one problem: + +``` +uv run python -m modules.naboj.builder.renderer \ + -C source/naboj/phys/28/problems/archery/meta.yaml \ + -P source/naboj/phys/28/problems/archery/preamble.md \ + source/naboj/phys/28/problems/archery/en/solution.md \ + /tmp/out.md +``` + +Or via Make targets defined in `modules/naboj/module.mk`. Do not commit the `_minted-output` +directory (`.gitignore`d). diff --git a/.claude/skills/naboj-authoring/references/jinja-templating.md b/.claude/skills/naboj-authoring/references/jinja-templating.md new file mode 100644 index 00000000..3af7188d --- /dev/null +++ b/.claude/skills/naboj-authoring/references/jinja-templating.md @@ -0,0 +1,261 @@ +# Jinja2 templating in DGS + +DGS uses **two** Jinja environments with different delimiters. Get the file type +right and everything follows. + +## Delimiters by file type + +### Markdown files (`.md` under `source/`) + +Custom delimiters chosen so as not to clash with Markdown / TeX syntax: + +| Purpose | Delimiter | Example | +| ----------------------- | ---------------- | ----------------------------- | +| Variable expression | `(§ … §)` | `(§ v0|f2 §)` | +| Block statement | `(@ … @)` | `(@ if fig @)…(@ endif @)` | +| Comment | `(# … #)` | `(# note: dead code #)` | +| Line statement prefix | `@J ` (with space)| `@J set result = v0 + v1` | +| Line comment prefix | `%#` | `%# throwaway comment` | + +`trim_blocks=True`, `autoescape=False`. Missing variables are **collected and +raised together** as `MissingVariablesError` after render — you'll see them all at +once. Filters may still catch undefined via `|default(...)` without triggering the +error. + +### Static TeX templates (`.jtex` under `modules/naboj/templates/` and `core/templates/`) + +Different delimiters — variables use `(* … *)`: + +| Purpose | Delimiter | Example | +| ----------------------- | ---------------- | ----------------------------- | +| Variable expression | `(* … *)` | `(* problem.number *)` | +| Block statement | `(@ … @)` | `(@ if target=='booklet' @)…` | +| Comment | `(# … #)` | | +| Line statement prefix | `@J ` | | + +Everything else about the environments is identical. + +## Rendering flow for `.md` files + +`core/builder/renderer.py::JinjaConvertor.run` does **two passes**: + +1. Prepend `preamble.md` (if any). +2. First render pass: expands values, equations, `@J set …`. +3. Second render pass: re-renders the intermediate so that any Jinja tags that + appear **inside expanded content** (e.g. inside a MathObject) get expanded too. + +This is why you can write things like `(§ result|f0 §)` in `answer.md` and have +`result` come from `preamble.md` — the preamble is prepended before the first pass. + +The second pass matters most for `eq:` fragments: `(§ eq.foo|disp §)` expands +on pass 1 to a display equation whose body still contains raw `(§ … §)` tags +(the ones defined in the `eq:` YAML value); pass 2 evaluates those inner tags. +Without the second pass, `eq:` would be useless. + +## Context available in Markdown Jinja + +Provided by `core/builder/renderer.py::CLIInterface.build_context`: + +- `id` — the problem id (from directory name). +- **Every key under `values:` in `meta.yaml`** becomes a variable in the local + scope. If the value is a dict with `magnitude:` and `unit:`, it becomes a + `PhysicsQuantity`; if a bare string/number, it's used as-is. +- **Every key under `eq:` in `meta.yaml`** becomes a `MathObject` accessible as + `eq.`. The `MathObject`'s `.id` is set to `:` so + `|disp` and `|align` emit a label automatically. See + `source/naboj/chem/04/problems/maliari/` for the canonical example — the + content of each `eq:` entry can itself contain `(§ … §)` tags, which get + expanded on the **second** render pass (see below). +- `const` — the physics constants dict from `core/data/constants.yaml`. Access as + `const.g`, `const.G`, `const.c`, `const.gforce`, ... aliases are also mapped. + +## Filters (registered in `core/builder/jinja.py::MarkdownJinjaRenderer`) + +Number formatting — precision-parameterised versions are pre-generated for 0–9 +digits. Apply to `PhysicsQuantity`, `QuantityRange`, `QuantityList`, +`QuantityProduct`, or a raw number: + +| Filter | What it does | +| -------------------- | --------------------------------------------------------- | +| `|f`, `|f0`, ..., `|f9` | Fixed-decimal formatting. `q|f2` → `\qty{50.00}{...}` | +| `|g`, `|g0`, ..., `|g9` | General formatting (may use scientific). `q|g3` | +| `|n` | Wrap raw value in `\num{…}` (no unit). | +| `|nf`, `|nf0..9` | `\num{…}` with fixed precision. | +| `|ng`, `|ng0..9` | `\num{…}` with general precision. | +| `|ef`, `|ef0..9` | `symbol = \qty{…}` fixed. Uses `PhysicsQuantity.symbol`. | +| `|eg`, `|eg0..9` | Same, general precision. | +| `|af`, `|af0..9` | `symbol \approx \qty{…}` fixed — like `|ef` but `\approx`. | +| `|ag`, `|ag0..9` | Same, general precision. | +| `|mag` | Extract raw magnitude (pint magnitude, not a string). | +| `|unit` | Just the unit, formatted as `\unit{…}`. | +| `|sim` | `.simplify()` — convert to base SI units. | +| `|w(value)`, `|widen(value)` | Construct a tolerance range: `x|w(0.05)` → ±5%. | + +Every family exists both bare and suffixed `0`–`9`. Bare means "no precision in +the format spec", i.e. Python's default for that kind: `f` gives six decimals +(`\qty{96.700000}{…}`), `g` gives six significant digits with trailing zeros +dropped (`\qty{96.7}{…}`). In practice the bare `f` forms are rarely what you +want — reach for `|g`, `|eg`, `|ag` or an explicit precision. + +`|ef*` vs `|af*` is purely the relation symbol (`=` vs `\approx`); use `|af*` +for rounded numeric results. Both read `q.symbol`, so a quantity without a +symbol renders the literal `None` — set `symbol:` in `values:` or use +`.alias('x')`. + +For a `MathObject`: + +| Filter | What it does | +| ------------- | ---------------------------------------------------------------- | +| `|inline` | Renders `$…$`. Write sentence punctuation **outside** the tag. | +| `|disp` | Renders `$$\n …\n$$ {#eq:}`. | +| `|disp('.')` | Same with trailing punctuation inside the math. | +| `|align` | `$${\n …\n}$$ {#eq:}` (aligned environment). | +| `|align(',')` | Same with punctuation. | +| `|dispd`, `|dispc`, `|disps`, `|dispq`, `|dispe` | Shorthands for `|disp` with `.` `,` `;` `?` `!`. | +| `|alignd`, `|alignc`, `|aligns`, `|alignq`, `|aligne` | Ditto for `|align`. | + +Mnemonic: **d**ot, **c**omma, **s**emicolon, **q**uestion mark, **e**xclamation +mark — one suffix per member of `MathObject._INTERPUNCTION`, so the shorthands +cover every punctuation mark the formatter accepts. + +The suffixed forms are `functools.partial`s with the punctuation already bound, +so they take **no** argument: `(§ eq|dispd('.') §)` raises `TypeError`. +They exist because `|dispc` reads better than `|disp(',')` in the middle of a +derivation, where nearly every equation ends in a comma or a full stop. + +Punctuation rules, enforced in `MathObject.__format__`: + +- Accepted trailing punctuation is exactly `. , ; ? !`. +- A bad trailing character after a valid base spec raises `ValueError` naming + the character; an unknown base spec raises `NotImplementedError`. The two are + deliberately distinct — the first is the common author typo. +- `|inline` accepts no punctuation; write it after the closing `$`. +- `disp` and `align` indent each content line by four spaces and append + `{#eq:}` — this is why `eq:` keys double as label names. + +## Globals + +Math functions available in Jinja expressions: + +``` +sin cos tan asin acos atan atan2 +sqrt cbrt log log10 log2 exp pow +ceil floor +rad deg # radians ↔ degrees +gamma beta # Γ and B(x,y) = Γ(x)Γ(y)/Γ(x+y) +pi tau euler # constants +``` + +All of them work on raw numbers. On a `PhysicsQuantity` most are numpy ufuncs, +which dispatch to a same-named method on the object — so **only the subset that +`PhysicsQuantity` implements works**. Verified behaviour: + +| Works on a `PhysicsQuantity` | Raises `TypeError` / `AttributeError` | +| ------------------------------------------------ | ------------------------------------- | +| `sin cos tan asin acos atan log deg ceil floor` | `cbrt log10 log2 exp rad atan2` | +| `sqrt` (implemented as `x ** 0.5`), `pow` | | + +The failure message is opaque (`loop of ufunc does not support argument 0 of +type PhysicsQuantity which has no callable log10 method`). Fix by taking the +magnitude first: `log10(x|mag)` / `log10(x.mag)`, or add the missing method to +`core/builder/context/quantities/physics_quantity.py`. Note `deg(x)` works but +`rad(x)` does not — asymmetric because only `.degrees()` is implemented. + +`sqrt` on a quantity whose unit is not a perfect square yields a fractional +exponent: `sqrt(Q(4, 'metre'))` → `\qty{2}{\meter\tothe{0.500}}`. That is almost +always an authoring bug; take `.mag` or fix the expression's dimensions. + +Constructors (short aliases in parentheses): + +``` +Q(magnitude, unit) # ad-hoc PhysicsQuantity, e.g. Q(100, '%') +QuantityList(q1, q2, q3) # (QL) combine several commensurate quantities into a list +QuantityProduct(q1, q2, q3) # (QP) combine several commensurate quantities into a product (e.g. box dimensions) +QuantityRange(lo, hi) # (QR) build a range directly; equivalent to `lo % hi` +``` + +Both the long names and the short aliases are registered, so `QR(a, b)` and +`QuantityRange(a, b)` are the same global. + +`q1 % q2` and `q.widen(v)` remain the idiomatic ways to build a `QuantityRange` (see +quantities-and-constants.md); `QR(lo, hi)` is there for when the operands aren't +bare variables and `%` would need extra parens anyway. + +## Units beyond SI + +The pint registry lives in `core/builder/jinja.py` and is installed as pint's +*application* registry, so every module shares it. Two local extensions: + +- **Currency.** `eur` is defined as its own dimension `[currency]`, with `€` and + `EUR` as symbols; a preprocessor rewrites a literal `€` in unit strings to + `EUR`. So `Q(3, 'eur')|f2` → `\qty{3.00}{\eur}` (`\eur` is declared in + `core/latex/siunitx.tex`). Currency is not commensurate with anything else, as + intended. +- **Temperature.** `PhysicsQuantity.format_struct` rewrites pint's + `\degree_Celsius` → `\celsius` and `\delta_degree_Celsius` → `\dcelsius` + (both declared in `siunitx.tex`). Use `'degC'` for absolute temperatures and + `'delta_degC'` for differences. `absolute + delta` is fine + (`20 °C + 5 Δ°C = 25 °C`), but multiplying or dividing an absolute Celsius + value raises pint's `OffsetUnitCalculusError` — convert with `.to('kelvin')` + before doing arithmetic that scales it. + +## `@J set` — the everyday case + +Used almost exclusively inside `preamble.md`: + +``` +@J set result = v0**2 / const.g.approx - sqrt((v0**4 / const.g.approx**2) - D**2) +@J set result_exact = v0**2 / const.g - sqrt((v0**4 / const.g**2) - D**2) +``` + +The `@J` (line-statement prefix) form is preferred over `(@ set … @)` for +readability. Idiomatic pattern: compute a rounded-constant version for `answer.md` +and an exact-constant version for `solution.md`. + +## Control flow (`.jtex` templates) + +Standard Jinja `if / for` blocks with the DGS delimiters: + +``` +(@ for vid, venue in volume.venues.items() @)% + (* venue.head *) ((* i18n[language.id].venues[vid] *))% +(@ endfor @)% +``` + +Whitespace control follows Jinja: `(@- … -@)` trims surrounding whitespace. + +Path helpers available in static templates: + +- `path_exists('some/path')` — check whether a file exists (used to conditionally + include `answer-extra.md`, etc.). +- `file_size('some/path')` — file size in bytes. + +## MissingVariablesError + +If any Jinja variable used at render time is not found, the collector aggregates +them and raises `MissingVariablesError`. The message lists every missing name in +insertion order. Typical causes: + +- Typo in `(§ v_0 §)` when the value is defined as `v0`. +- Value defined in preamble but preamble file not prepended (check that + `preamble.md` exists at the problem level — the Makefile falls back to a + no-preamble rule if it's missing). +- Cross-problem reference. Values are scoped to a single problem. + +## Common pitfalls + +- **Space around `@J`.** It's a line statement *prefix*, not a delimiter. Write + `@J set …` (with a space). No leading whitespace either — must be column 0. +- **`(§ … §)` in `.jtex` won't work.** The `.jtex` environment uses `(* … *)` for + variables. Conversely, `(* … *)` in `.md` is a literal `(*` `*)`, not a + variable. +- **Numeric arithmetic on `PhysicsQuantity` returns `PhysicsQuantity`.** So + `(§ v0 * 2 §)` becomes `\qty{100}{\metre\per\second}`. If you want a bare number, + use `.mag` or `|mag`. +- **Ranges (`QuantityRange`) via `%`.** `q1 % q2` constructs a range with `q1` as + minimum and `q2` as maximum. If `q1 > q2`, pint raises. Prefer the + `.widen(fraction)` method when you want a symmetric tolerance band. +- **Constants inside math.** `const.g.approx` gives a rounded-magnitude + `PhysicsQuantity`; use it in expressions. `const.g.symbol` (or `const.g.sym`) + gives the TeX symbol string. `const.g.full` gives the printable full-precision + form. diff --git a/.claude/skills/naboj-authoring/references/latex-macros.md b/.claude/skills/naboj-authoring/references/latex-macros.md new file mode 100644 index 00000000..87668370 --- /dev/null +++ b/.claude/skills/naboj-authoring/references/latex-macros.md @@ -0,0 +1,245 @@ +# Custom LaTeX macros (DGS) + +Every non-vanilla-amsmath command you see in Náboj Markdown comes from +`core/latex/*.tex` and is loaded via `core/latex/dgs.cls`. Grep those files +before assuming a macro exists or defining a new one. + +Files: + +- `core/latex/dgs.cls` — class definition; package loads; document parameters. +- `core/latex/fonts.tex` — Minion Pro + math font setup. +- `core/latex/math.tex` — differentials, derivatives, integrals, gradient/div/rot, + vectors, sums/products, intervals, statistics. +- `core/latex/symbols.tex`— planets, nuclides. +- `core/latex/siunitx.tex`— siunitx global setup + custom units. +- `core/latex/hacks.tex` — pandoc/lists fixes, `\phi ↔ \varphi` swap, jigsaw, + `\st` for markdown strikeout. +- `core/latex/utilities.tex` — `\URL`, `\errorMessage`, `\protectedInput`, + `\exampleIO`. + +## Delimiters and brackets + +`\left(...\right)` is redefined via `mleftright` (`\mleft`, `\mright`) so spacing +around parentheses no longer requires manual `\!` corrections. + +Sized delimiters: + +- `\Paren{expr}` → `\left(expr\right)` +- `\Abs{expr}` → `\left| expr \right|` +- `\Floor{expr}` → `\lfloor…\rfloor` +- `\Ceil{expr}` → `\lceil…\rceil` (also enforced in answer files — see + `Adam_ISIC` result `\Ceil{t}`) +- `\ExpectedChevrons{X}[condition]` → `\langle X \mid condition \rangle` + +Tuples and coordinates (with a configurable inner delimiter, default `;`): + +- `\Tuple{a;b;c}` → `(a; b; c)` — round brackets +- `\Coord{x;y;z}` → `[x; y; z]` — square brackets + +## Differentials and derivatives + +Base differentials (each includes the standard math-space adjustment): + +- `\Diff x` → `\mathrm{d}\, x` +- `\PDiff x` → `\partial\, x` +- `\FDiff x` → `\Delta\, x` +- `\UDiff x` → `\delta\, x` +- With power: `\Diff[2] x` → `\mathrm{d}^{2}\, x` + +Derivatives (fraction style controlled by an optional ``): + +- `\Derivative[order]{f}{x}` (aliases `\Drv`) +- `\PDerivative[order]{f}{x}` (`\PDrv`) +- `\FDerivative`, `\UDerivative` — for Δ- and δ-based derivatives +- `\Derivative[2]{f}{x}` — with display-style fraction +- `\DerivativeParen[order]{f}{x}` — d/dx (f) form (`\DrvP`, `\PDrvP`, ...) +- `\DerivativeEmpty[order]{x}` — d/dx alone (`\DrvE`, `\PDrvE`, ...) +- `\DerivativeEval[order]{f}{x}{a}` — evaluated at x=a via `\Eval{…}{…}` + +## Integrals + +The naming scheme: `\Int` is the base 1-D form; the modifiers are +- `I` = integrand takes a differential-of-power operand +- `D` = dot product with `d…` +- `C` = cross product with `d…` +- `V` = auto-vectorise inputs (`\vec{}`) +- `O` = closed loop (single-integral) — `\oint` +- `II` = double integral — `\iint` +- `III`= triple integral — `\iiint` +- `OII`= closed surface (double) — `\oiint` + +1-D: +- `\Int[a][b]{f(x)}{x}` → `\int_a^b f(x) \diff x` +- `\IntP[a][b]{f(x)}{x}` → adds `\left(…\right)` around integrand +- `\IntX[a][b]{expr}` → no `\diff`, for algebraic manipulation +- `\IntE[a][b]{}{x}` → `\int_a^b \diff x` (empty integrand) +- `\IntD[a][b]{\vec E}{\vec r}` → dot product with `d\vec r` +- `\IntDV[a][b]{E}{r}` → auto-vec: same as IntD with `\vec{}` +- `\IntC`, `\IntCV` → cross product variants + +Loop: +- `\OInt[C]{f}{x}` → `\oint_C f \diff x` +- `\OIntD[C]{\vec E}{\vec r}`, `\OIntDV[C]{E}{r}` +- `\OIntC`, `\OIntCV` → cross product + +Surface (double): +- `\IIntI[S][…]{f}[power]{x}` → `\iint_S f \diff^{power} x` +- `\IIntD[S][…]{\vec E}{\vec S}`, `\IIntDV[S][…]{E}{S}` +- `\IIntC`, `\IIntCV` +- `\OIIntD`, `\OIIntDV`, `\OIIntC`, `\OIIntCV` — closed surface + +Volume (triple): +- `\IIInt[V][…]{f}{x}{y}{z}` → `\iiint_V f \diff x \diff y \diff z` +- `\IIIntV[V][…]{f}{r}` → `\iiint_V f \diff^{3} r` +- `\IIIntPV[V][…]{f}{r}` → with `\left(…\right)` around integrand + +## Vector calculus + +- `\Grad`, `\Div`, `\Rot`, `\Laplacian` — with `∇`. +- `\GradT`, `\DivT`, `\RotT` — text forms (`grad`, `div`, `rot`). +- `\GradV{X}`, `\DivV{X}`, `\RotV{X}` — auto-vectorised argument. + +## Vectors + +- `\ArrowVector{X}` / `\vv{X}` — small arrow (esvect). +- `\LongVector{X}` — long arrow (`\overrightarrow`). +- `\BoldVector{X}` — bold. +- `\UnitVector{X}` — `\hat{\vec{X}}`. +- `\UnitBoldVector`, `\UnitArrowVector`. + +`\vec{…}` is the default in DGS; the alternatives are for specific styles. + +## Aggregates (Σ, Π, ⋃, ⋂) + +Use these instead of raw `\sum`, `\prod`, `\bigcup`, `\bigcap`, `\bigtimes` +(`mdcheck` rules `sum` and `int` enforce it): + +- `\Sum[lo][hi]{elt}` — Σ with limits +- `\SumP[lo][hi]{elt}` — same with `\left(…\right)` around body +- `\Product[lo][hi]{elt}` — Π +- `\CartesianProduct[lo][hi]{elt}` — big × +- `\Union[lo][hi]{elt}` — ⋃ +- `\Intersection[lo][hi]{elt}` — ⋂ + +Non-aggregate: + +- `\Uni` (∪), `\Intersect` (∩), `\union`, `\intersection`. + +## Max/min operators + +- `\Max[cond]{expr}`, `\Min[cond]{expr}` — with `\underset`-style condition. + +## Sets, sequences, intervals + +- `\Set{a,b,c}[cond][power]` — `\{ … \}` with optional sub/superscript. +- `\Seq{a,b,c}[cond][power]` — parenthesised sequence. +- `\Natural`, `\NaturalZero`, `\Integer`, `\Rational`, `\Real`, `\RealPos`, + `\RealNeg`, `\RealNonneg`, `\RealNonpos`, `\Complex`, `\Quaternions`. +- `\IntervalCC{a}{b}` → `[a; b]` — closed–closed +- `\IntervalCO{a}{b}` → `[a; b)` — closed–open +- `\IntervalOC{a}{b}` → `(a; b]` +- `\IntervalOO{a}{b}` → `(a; b)` + +Note the semicolon separator inside intervals (Slovak convention). Consistent +with `\Tuple`, `\Coord`. + +## Text-in-math and phrase spacing + +- `\Text{…}` — one small space on each side. +- `\QText{…}` — `\quad` on each side. +- `\QQText{…}` — `\qquad` on each side. +- `\Operation{op}` — right-side annotation (`& \qquad / op`) for + aligned equations. + +Do **not** wrap punctuation in `\text{}` (linter rule `pun`). + +## Common relations + +- `\Implies` → `\quad\Rightarrow\quad` (use everywhere instead of `\implies` / + `\Rightarrow`). +- `\Iff` → `\quad\Leftrightarrow\quad`. +- `\ImpliedBy` → `\quad\Leftarrow\quad`. +- `\MustEq`, `\MustL`, `\MustLL`, `\MustG`, `\MustGG`, `\MustLeq`, `\MustGeq` + — over-stacked with `!` (must-equal etc.). +- `\DefEqual` → `\stackrel{\mathrm{def}}{=}`. +- `\Assign` → `\coloneqq`. +- `\Must{X}` — generic must-something (over-stacked `!`). + +## Statistics + +- `\Mean{X}` → `\overline{X}`. +- `\Var{X}`, `\MSE{X}`, `\Bias{X}`. +- `\Binomial{n}{k}` → `\binom{n}{k}`. +- `\Distribution{Name}[var]{params}` → `Name(var \mid params)`. +- `\Dimen{X}` → `[X]` (dimension brackets). + +## Number literals + +Fractions and units (safe to write in math or inline text): + +- `\OneHalf`, `\OneThird`, `\TwoThirds`, `\OneQuarter`, `\ThreeQuarters` — use + these instead of Unicode `½`, `⅓`, etc. + +Asymptotics: + +- `\SmallO{n^2}`, `\BigO{n^2}`, `\BigTheta{n^2}`. + +## Symbols + +Planets and celestial bodies (see `symbols.tex`): + +- `\Sun`, `\Mercury`, `\Venus`, `\Earth`, `\Moon`, `\Mars`, `\Jupiter`, `\Saturn`, + `\Uranus`, `\Neptune`, `\Pluto`. + +Nuclides (via mhchem): + +- `\Nuclide[A][Z]{sym}` — e.g. `\Nuclide[99m]{Tc}`, `\Nuclide[235][92]{U}`. + +## Chemistry (chem module) + +Loaded in `dgs.cls`: + +- `\ce{H2O}` — reactions and formulas (mhchem, version 4 with `formula=mhchem`). +- `\chemfig{…}` — structural formulas (chemfig package). +- `\chemsetup{formula=mhchem}` is set globally. + +## Font swap (`hacks.tex`) + +DGS **swaps** `\phi ↔ \varphi` and `\epsilon ↔ \varepsilon` at load time — Minion +Pro's default `\phi` and `\epsilon` glyphs are ugly. Consequences: + +- Write `\phi` when you mean φ (looks like `\varphi` in Computer Modern). +- Write `\epsilon` when you mean ε. +- The linter forbids `\varepsilon` outright (rule `vep`) — the swap makes it + unnecessary. + +## Utility macros for content + +- `\URL{https://…}` — `\href{…}{\texttt{…}}` combo. +- `\errorMessage{msg}` — bright red highlight (used for missing files etc.). +- `\todoMessage{msg}` — orange highlight. +- `\protectedInput{path}` — `\input` if it exists, else emit `\errorMessage`. +- `\tryInput{path}` — `\input` if it exists, else nothing. +- `\cutHere` — scissors line for tearoff sheets. +- `\exampleIO{input}{output}` — side-by-side verbatim boxes (programming problems). + +`\insertPicture` is **legacy** and forbidden by `mdcheck.lip` — use pandoc +image syntax instead. + +## Extending macros + +- **New physical constants**: `core/data/constants.yaml`. Add `magnitude`, `unit`, + `symbol`, `digits`, optional `aliases`, `exact`, `force_f`. No LaTeX changes + needed. +- **New math macro**: prefer `core/latex/symbols.tex` for symbols, `math.tex` for + operators / delimiters. Use `\NewDocumentCommand` (LaTeX3 syntax) for + consistency with the existing style. +- **New siunitx unit**: `core/latex/siunitx.tex`, `\DeclareSIUnit{…}{…}`. +- **New Jinja filter**: register in `core/builder/jinja.py::MarkdownJinjaRenderer` + under `self.env.filters` (define the function in `core/filters/`). +- **New Jinja global**: same file, `self.env.globals`. +- **New style-checker rule**: `core/mdcheck/check.py`, subclass `LineChecker` (or + add a `check.FailIfFound` entry with a short 3-letter key in + `core/markdown-check.py::StyleEnforcer.line_errors`). +- **New template block**: `modules/naboj/templates/blocks/`. Remember `.jtex` + uses `(* … *)` for variables, not `(§ … §)`. diff --git a/.claude/skills/naboj-authoring/references/layout.md b/.claude/skills/naboj-authoring/references/layout.md new file mode 100644 index 00000000..dfc2f16a --- /dev/null +++ b/.claude/skills/naboj-authoring/references/layout.md @@ -0,0 +1,208 @@ +# File layout of a Náboj volume + +## Volume directory + +``` +source/naboj/// +├── meta.yaml # volume-level metadata (see below) +├── languages/ +│ └── / +│ ├── meta.yaml # per-language booklet contents flags +│ ├── intro.jtex # editor's letter (uses (* … *) tags) +│ ├── instructions-inner.jtex +│ └── evaluators.jtex +├── venues/ +│ └── / +│ └── meta.yaml +└── problems/ + └── / + ├── meta.yaml # per-problem metadata (see below) + ├── preamble.md # (optional) Jinja preamble with @J set … + ├── answer.md # the answer expression (Jinja, non-translatable) + ├── answer-also.md # (optional) accepted alternative + ├── answer-interval.md # (optional) accepted interval + ├──
.svg / .tikz / .png # (optional) referenced by both problem and solution + └── / + ├── problem.md # the problem statement (Jinja + Markdown) + ├── problem-extra.md # (optional, rare) extra content appended after problem + └── solution.md # the worked solution +``` + +`` is `phys` or `chem`; `` is a two-digit numeric volume id. + +## Volume `meta.yaml` + +Fields observed in `source/naboj/phys/28/meta.yaml`: + +```yaml +date: 2025-11-07 +start: '11:30' +problems: [gravity-sudoku, train-mirror, balance-me, ...] # ordered! +workshop: 'chata Alexa, Oravská Lesná' +authors: + problems: [ "Martin ‚Kvík‘ Baláž", ... ] + pictures: [...] + editors: [...] + head: "Jaroslav Valovčan" +venues: + ba: { name: Bratislava, head: "Katarína Nedeľková" } + ke: { name: Košice, head: "Marián Kireš" } + # ... +table: 8 # answer table columns +constants: # constants that appear in the printed table + generic: [gforce, avogadro, universal_gas, boltzmann] + astronomy: [speed_light, gravity, radius_earth, ...] + elmag: [permittivity, mass_electron, elementary_charge] + misc: [stefan_boltzmann, density_water, ...] +``` + +`problems:` fixes the **order** in which problems appear (numbering follows this list). +Every id listed must exist as `problems//`. + +## Problem `meta.yaml` + +Minimal (no computed values, hard-coded numbers in the Markdown): + +```yaml +authors: ['Kvík'] +tags: ['kinematics'] +``` + +With values (used as Jinja variables inside `problem.md`, `solution.md`, `preamble.md`, +`answer.md`, ...): + +```yaml +authors: ['Kvík'] +tags: ['kinematics', 'oblique-throw'] +values: + v0: + magnitude: 50 + unit: 'metre / second' # pint syntax; see quantities-and-constants.md + symbol: 'v_0' # TeX symbol used by |eq / .eq + D: + magnitude: 70 + unit: 'metre' + symbol: 'd' +``` + +Dimensionless values: use `unit: ~` or `unit: '1'`. + +Percentages: `unit: '%'`. Angles: `unit: 'degree'` or `'radian'`. + +`siunitx` extras (rare, e.g. force `per-mode`): + +```yaml +values: + acc: + magnitude: 2.6 + unit: "kilometre / hour / second" + si_extra: + per-mode: "repeated-symbol" + symbol: a +``` + +A value can also be a bare string or number — no Quantity is constructed then. + +`author:` (singular) is accepted in older volumes; new problems use `authors: [...]`. + +### `eq:` — named math fragments (chem) + +Alongside `values:`, `meta.yaml` can carry an `eq:` top-level dict. Each entry +becomes a `MathObject` addressable in Jinja as `eq.`. Its content is a +LaTeX fragment that may itself contain `(§ … §)` tags — those are expanded on +the second render pass (see `jinja-templating.md` §Two-pass rendering). + +Canonical example: `source/naboj/chem/04/problems/maliari/meta.yaml`. + +```yaml +values: + m: { magnitude: 140, unit: 'g' } + w: { magnitude: 0.88, unit: '1' } +eq: + m: | + m(\ce{Zn}) = + m(\text{kov}) \cdot w(\ce{Zn}) = + (§ m|f0 §) \cdot (§ (w.to('1'))|f2 §) = + (§ (m * w)|f1 §). + Mr: | + M_r(\ce{ZnSO4}) + &= A_r(\ce{Zn}) + A_r(\ce{S}) + 4 A_r(\ce{O}) \\ + &= (§ const.M_Zn §) + (§ const.M_S §) + 4 \cdot (§ const.M_O §) = + (§ M_ZnSO4|f2 §). +``` + +Then in `solution.md`: + +``` +Hmotnosť rozpusteného zinku preto bude +(§ eq.m|disp §) + +Molekulová hmotnosť síranu zinočnatého je +(§ eq.Mr|align §) +``` + +`|disp` produces `$$\n …\n$$ {#eq:maliari:m}` and `|align` produces the +`aligned` variant (`&`-alignment supported). The label is generated +automatically as `{#eq::}` — do **not** add your own. + +Constraints: +- The name must match `^[a-z][a-zA-Z0-9_]+$` and cannot be `eq` or `const`. +- Names are validated by `StandaloneContext._schema` in `renderer.py`. +- More common in `chem` than `phys`; useful when the same computation is + displayed *and* re-used later as a labelled equation. + +## `preamble.md` + +Optional. Executes Jinja before problem/solution rendering; **prepended** to the file being +rendered. Almost always contains `@J set` lines: + +``` +@J set result = v0**2 / const.g.approx - sqrt((v0**4 / const.g.approx**2) - D**2) +@J set result_exact = v0**2 / const.g - sqrt((v0**4 / const.g**2) - D**2) +``` + +Convention: compute a rounded-`const.g` variant (`.approx`) alongside an exact-`const.g` +variant so both `answer.md` (rounded target for competitors) and `solution.md` (exact +expression) get sensible values. + +## `answer.md` / `answer-also.md` / `answer-interval.md` + +Single line (no trailing newline needed), typically referencing a preamble variable: + +``` +(§ result|f0 §) +``` + +- `answer.md` — the canonical answer displayed on the answer sheet. +- `answer-also.md` — an alternative accepted answer, typeset after "also". +- `answer-interval.md` — an interval, typeset after "interval". Usually `(§ (a % b)|f2 §)` + where `%` constructs a `QuantityRange`, or `(§ x|w(0.05)|f2 §)` for a ±5% tolerance. + +All three are **non-translatable** (same file across languages). If wrapped in `$…$` they +render as inline math on the answer sheet. + +The linter forbids `\frac` here — use `\dfrac` because answer cells are typeset small. + +## Language directory (`/`) + +- `problem.md` — problem statement in that language. +- `solution.md` — worked solution. +- `problem-extra.md` — rarely used, gets appended in the booklet after the problem. +- `answer-extra.md` — rarely used, gets appended after the answer. + +Supported language codes (see `Makefile`): `sk en cs hu pl es de fr ru fa uk pt`. + +## Figures + +- `.svg` — rendered via `rsvg-convert` / `dvisvgm`. +- `.tikz` — inline TikZ, wrapped by the pipeline. +- `.png`, `.jpg` — embedded directly. + +Figures live at the **problem level** (not per-language). Reference from Markdown: + +``` +![Caption text](my-figure.svg){#fig::