fix(render): per-level heading numbering via numbly; config-driven, offline (WU-C)

Fixes the numbering bug (single pattern "一、" reused across levels → level-2
rendered "二、一、…"). display now takes a `config` dict; heading numbering uses
numbly per level — framework default ("{1:一}、","{1:1}.{2:1}","{1:1}.{2:1}.{3:1}")
→ 一、 / 1.1 / 1.1.1 — overridable via config.numbering.heading from the
engineering file (ADR-0009's file-resident override path, the layer whose absence
caused the bug). Verified: grep "二、一" = 0 across all smoke PDFs.

Adds @preview/numbly:0.1.0 (pure typst, zero transitive deps), VENDORED in-repo
at render/vendor/typst-packages/preview/numbly/0.1.0/ for network-free CI;
resolve via --package-cache-path render/vendor/typst-packages. parts contract +
student/teacher field visibility unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 08:42:15 +08:00
parent b5bf9d60d1
commit 6f46aa708a
9 changed files with 187 additions and 10 deletions
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 梦飞翔
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,43 @@
# numbly
A package that helps you to specify different numbering formats for different levels of headings.
Suppose you want to specify the following numbering format for your document:
- Appendix A. Guide
- A.1. Installation
- Step 1. Download
- Step 2. Install
- A.2. Usage
You might use `if` to achieve this:
```typst
#set heading(numbering: (..nums) => {
nums = nums.pos()
if nums.len() == 1 {
return "Appendix " + numbering("A.", ..nums)
} else if nums.len() == 2 {
return numbering("A.1.", ..nums)
} else {
return "Step " + numbering("1.", nums.last())
}
})
= Guide
== Installation
=== Download
=== Install
== Usage
```
But with `numbly`, you can do this more easily:
```typst
#import "@preview/numbly:0.1.0": numbly
#set heading(numbering: numbly(
"Appendix {1:A}.", // use {level:format} to specify the format
"{1:A}.{2}.", // if format is not specified, arabic numbers will be used
"Step {3}.", // here, we only want the 3rd level
))
```
@@ -0,0 +1,31 @@
#let numbly(..arr, default: "1.") = (..nums) => {
let arr = arr.pos()
nums = nums.pos()
if nums.len() > arr.len() {
if default == none {
return none
}
if type(default) == function {
return default(..nums)
}
return numbering(default, ..nums)
}
let format = arr.at(nums.len() - 1)
if format == none {
return none
}
if type(format) == function {
return format(..nums)
}
format.replace(
regex("\{(\d)(:(.+?))?\}"),
m => {
let (a, b, c) = m.captures
if b != none {
numbering(c, nums.at(int(a) - 1))
} else {
str(nums.at(int(a) - 1))
}
},
)
}
@@ -0,0 +1,10 @@
[package]
name = "numbly"
version = "0.1.0"
entrypoint = "lib.typ"
authors = ["flaribbit <@flaribbit>"]
license = "MIT"
description = "A package that helps you to specify different numbering formats for different levels of headings."
categories = ["utility"]
keywords = ["numbering", "helper", "tool"]
repository = "https://github.com/flaribbit/numbly"