mathfmt-tex 0.1.0__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,401 @@
1
+ Metadata-Version: 2.4
2
+ Name: mathfmt-tex
3
+ Version: 0.1.0
4
+ Classifier: Environment :: Console
5
+ Classifier: Intended Audience :: Science/Research
6
+ Classifier: Programming Language :: Rust
7
+ Classifier: Topic :: Text Processing :: Markup :: LaTeX
8
+ Summary: A filter that enforces math-notation conventions in LaTeX source
9
+ Keywords: latex,math,unicode,formatter,pre-commit
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
12
+ Project-URL: Repository, https://github.com/jackruder/mathfmt
13
+
14
+ # mathfmt
15
+
16
+ `mathfmt` rewrites the mathematical notation inside LaTeX source so that one
17
+ symbol is spelled one way, the way your profile says. `\R`, `\mathbb{R}`,
18
+ `\symbb{R}` and `ℝ` are four spellings of one symbol; a run makes them one. It
19
+ is a filter: LaTeX in, the same LaTeX out, with only the recognized math tokens
20
+ changed and every other byte untouched.
21
+
22
+ It is not a formatter. Whitespace, line breaks and indentation are left to
23
+ `latexindent`, `tex-fmt` or `badness`, and `mathfmt` runs beside them. It has no
24
+ opinion about when it runs (a pre-commit hook, a CI check, an editor's
25
+ format-on-save, a one-off switch of a whole repository to ASCII for a venue
26
+ that cannot take Unicode) or about what compiles the result.
27
+
28
+ Status: version zero. Atomic symbols (named symbols, Greek letters, styled
29
+ letters and digits, zero-argument macros that stand for one of them) and
30
+ notations with arguments (`\KL{p}{q}` and `D_{\mathrm{KL}}(p \| q)`) convert
31
+ in both directions; a conversion can be compiled before and after and the
32
+ pages compared. See `docs/superpowers/specs/2026-11-05-mathfmt-design.md`.
33
+
34
+ ## Install
35
+
36
+ ```sh
37
+ uv tool install mathfmt-tex # or pipx / pip: a prebuilt binary, no Rust
38
+ cargo install --path . # any platform with a Rust toolchain
39
+ nix build # Linux and macOS; ./result/bin/mathfmt
40
+ nix profile install . # or add the flake's package to your configuration
41
+ ```
42
+
43
+ The Python package is named `mathfmt-tex` because `mathfmt` on PyPI is an
44
+ unrelated project; the command it installs is `mathfmt`. Its wheels hold the
45
+ compiled binary and nothing else, one per platform (Linux x86-64 and ARM,
46
+ glibc and musl; macOS Intel and Apple; Windows x64), built by
47
+ `.github/workflows/wheels.yml` on the GitHub mirror and published to PyPI
48
+ from a `v*` tag. `maturin build --release` in the dev shell builds one for
49
+ the machine at hand.
50
+
51
+ ## Use
52
+
53
+ ```
54
+ mathfmt [OPTIONS] [FILE...]
55
+ no FILE: read stdin, write stdout
56
+ -p, --profile NAME profile from the config (default: config's `profile`)
57
+ -c, --config PATH config file (default: nearest mathfmt.toml)
58
+ --macros FILE an extra definition source (repeatable)
59
+ --project FILEs are root documents: take every file each one
60
+ inputs (no FILE: --root, else the config's `root`)
61
+ --root FILE the root document of the FILEs: its preamble's
62
+ packages and definitions apply to them
63
+ -w, --write rewrite FILEs in place (atomic: temp file + rename)
64
+ --check exit 1 if any file would change; print their names
65
+ --diff print a unified diff per changed file
66
+ --emit KIND regions | tokens | atoms | symbols, as JSON lines
67
+ --verify compile before and after, compare the rendered pages
68
+ --engine PROGRAM the TeX engine --verify uses (default: the config's
69
+ `engine`, else what the root's preamble says,
70
+ else pdflatex)
71
+ --declare under --verify, add the lines of `mathfmt
72
+ declarations` to a document's preamble, both sides
73
+ --preamble FILE preamble for snippets under --verify
74
+ --dpi N resolution of the page comparison (default 100)
75
+ --keep DIR keep the --verify build trees
76
+ --format text|json diagnostics format (default text)
77
+ --strict warnings make the exit status 1
78
+ -j, --jobs N parallel files (default: available cores)
79
+ -q, --quiet no diagnostics on stderr
80
+ -v, --verbose also show informational diagnostics
81
+
82
+ mathfmt rename --from SPELLING --to SPELLING [same options] [FILE...]
83
+ mathfmt declarations [--project] [--root FILE] [--with MECHANISM] [FILE...]
84
+ mathfmt project [ROOT]
85
+ mathfmt table [--profile NAME]
86
+ ```
87
+
88
+ ```sh
89
+ mathfmt < draft.tex > out.tex
90
+ mathfmt --profile unicode --write sections/*.tex
91
+ mathfmt --check --diff main.tex sections/*.tex
92
+ mathfmt --emit symbols main.tex | jq -r .atom | sort | uniq -c
93
+ mathfmt rename --from 𝔉 --to ℱ --write sections/*.tex
94
+ mathfmt --verify --macros ddssm.sty main.tex sections/*.tex
95
+ mathfmt --project main.tex --write
96
+ mathfmt --project main.tex --verify --declare
97
+ mathfmt declarations --project main.tex >> preamble.tex
98
+ ```
99
+
100
+ Exit status: 0 clean; 1 `--check` found changes, `--strict` found warnings or
101
+ `--verify` found a difference; 2 usage, config or I/O error, or an original
102
+ that `--verify` could not compile (every other verdict is still printed).
103
+ Silent on success. With no FILE it reads stdin, so guard a shell expansion that may come
104
+ back empty; a pre-commit hook passes the files itself.
105
+
106
+ ### A whole paper at once
107
+
108
+ A paper is a root file, the files it reads with `\input`, `\include`,
109
+ `\subfile` and `\import`, and a preamble that loads packages, some of them
110
+ local. Name the root and mathfmt reads that much itself, without running
111
+ TeX:
112
+
113
+ ```sh
114
+ mathfmt project main.tex # what it found: files, packages, engine
115
+ mathfmt --project main.tex --check # every file the root inputs
116
+ ```
117
+
118
+ or once, in `mathfmt.toml` beside the root:
119
+
120
+ ```toml
121
+ root = "main.tex"
122
+ ```
123
+
124
+ With a root, named either way, the definitions of its preamble and of the
125
+ local packages and classes it loads (`\usepackage{ddssm}` with a `ddssm.sty`
126
+ beside the root) are in force for every file, so a pre-commit hook that is
127
+ handed `sections/method.tex` alone still knows the paper's `\R`. `--project`
128
+ also takes the file list from the root: the root, then each file its body
129
+ inputs, in reading order. A package is read and never rewritten. The reader
130
+ does not evaluate conditionals, skips a file name built by a macro, and knows
131
+ a distributed package by its name only.
132
+
133
+ The preamble also says what the document is written for. Whether it loads
134
+ `unicode-math` decides the commands: a profile's commands for the math
135
+ alphabets become unicode-math's (`\symbf`, `\symbb`, `\symcal`) instead of
136
+ `\mathbf`, `\mathbb`, `\mathcal`, which that package typesets differently,
137
+ and a symbol whose amssymb name that package lacks or draws as another glyph
138
+ is written by unicode-math's own name (`\square` becomes `\mdlgwhtsquare`, ▸
139
+ becomes `\smallblacktriangleright`); read back, those names mean what
140
+ unicode-math draws. A lualatex document with fontspec and no unicode-math
141
+ keeps `\mathbf`. The root is `--root`, else the config's `root`, else, for a
142
+ file named on its own, the document itself, or the main document a subfile
143
+ names. `style_preset = "latex"` or `"unicode-math"` in a profile decides the
144
+ commands by hand; `--engine` or `engine` in the config names the program
145
+ `--verify` runs, which otherwise is lualatex for a preamble that loads
146
+ `unicode-math` or `fontspec`. Without a root, an engine other than pdflatex
147
+ is taken to mean unicode-math, which is what `--verify`'s own snippet
148
+ preamble for it loads.
149
+
150
+ When a root is known, a conversion that writes a command the preamble does
151
+ not provide (`\mathbbm` without bbm) gets a `package-not-loaded` warning.
152
+ With bbold or mathbbol loaded, lowercase and digit blackboard letters are
153
+ written with `\mathbb`, which those packages extend.
154
+
155
+ `mathfmt declarations` prints what the preamble must hold for the notation to
156
+ compile in either form: the packages the profile's commands come from that
157
+ the preamble does not load, and a meaning for the Unicode characters, each
158
+ declared as exactly what the profile spells it with commands.
159
+
160
+ ```
161
+ $ mathfmt declarations --project main.tex
162
+ % mathfmt declarations: main.tex, profile unicode, unicode-math-input
163
+ % packages the notation's commands need and the preamble does not load
164
+ \usepackage{unicode-math-input}
165
+ % styled letters, as the profile spells them (…)
166
+ \umiDeclareMathChar{𝐱}{\mathbf{x}}
167
+ \umiDeclareMathChar{𝛔}{\boldsymbol{\sigma}}
168
+ \umiDeclareMathChar{𝟙}{\mathbbm{1}}
169
+ …
170
+ ```
171
+
172
+ How a document reads Unicode is a choice (`--with`, or `unicode_input` in the
173
+ config):
174
+
175
+ | | |
176
+ |---|---|
177
+ | `unicode-math-input` | pdflatex with that package, which reads a character after `\big` or as a script where the kernel's mechanism does not. Every character is declared all the same: the package's own table lacks some symbols and reads others as something else (`√` as the radical). The default under pdflatex. |
178
+ | `kernel` | pdflatex with `\DeclareUnicodeCharacter` and no package. Every character the math uses is declared, inside `\ensuremath`, so it also works in text (mathfmt reads math only, so a character that appears only in text is not seen). `\big⟨` does not compile, and the primes cannot be declared at all: a declared `′` before a script is a double superscript. |
179
+ | `unicode-math` | lualatex or xelatex with unicode-math. The characters are native; nothing is declared. |
180
+
181
+ Without unicode-math, lualatex reads Unicode math through unicode-math-input
182
+ as pdflatex does.
183
+
184
+ ### Proving a conversion is safe
185
+
186
+ The promise is the same PDF from different source, and `--verify` checks it
187
+ rather than arguing it: the original and the converted source are compiled
188
+ with the same engine, both PDFs are rasterized with `pdftoppm`, and the pages
189
+ are compared pixel for pixel. Text extraction cannot tell a bold `x` from a
190
+ plain one; pixels can. A FILE with `\documentclass` is a document and is
191
+ compiled as it is, under its own preamble, with the converted copies of the
192
+ other FILEs in place:
193
+
194
+ ```
195
+ $ mathfmt --project main.tex --verify
196
+ broken: main.tex: the converted source does not compile
197
+ ! LaTeX Error: Unicode character 𝐱 (U+1D431)
198
+ not set up for use with LaTeX.
199
+ the preamble does not read this character: `mathfmt declarations` prints
200
+ the lines it needs, and --verify --declare compiles with them
201
+ ```
202
+
203
+ That is the honest answer for a pdflatex paper whose preamble does not read
204
+ Unicode math: it is not safe to convert yet. `--declare` answers the next
205
+ question without touching the paper: what would it render as with the lines
206
+ of `mathfmt declarations` in its preamble? They are added before
207
+ `\begin{document}` on both sides.
208
+
209
+ ```
210
+ $ mathfmt --project main.tex --verify --declare
211
+ differs: main.tex: 1 of 14 pages
212
+ page 2: 3225 pixels in the box x=95..401 y=212..241
213
+ the extracted text is the same; the difference is in glyph shapes or positions
214
+ with 28 declaration lines added to the preamble on both sides
215
+ ```
216
+
217
+ One page, and the reason is the paper's: it writes `\mathbf{\sigma_\theta}`,
218
+ which pdflatex draws plain, where its author meant all of it bold. mathfmt
219
+ reads it as meant: both profiles write it bold (`\boldsymbol{\sigma_\theta}`,
220
+ `𝛔_{𝛉}`), so the difference is a repair and the check has found a slip in
221
+ the paper. With those two occurrences written `\boldsymbol{\sigma_\theta}`
222
+ the verdict is `verified: main.tex (14 pages identical)`, in about eight
223
+ seconds for the seventeen files. A paper that wants pdflatex's plain reading
224
+ kept instead sets `bf_greek = "\\mathbf"` in its profile.
225
+
226
+ A FILE without `\documentclass` is a snippet and is wrapped in a preamble
227
+ (`--preamble FILE`, or a built-in one that loads the usual packages and
228
+ unicode-math-input and declares the styled letters the snippet uses to mean
229
+ what the profile spells them as). `--verify` needs the engine and
230
+ `pdftoppm` on PATH; the test suite runs it over every fixture and over the
231
+ small projects in `tests/projects/`, one of them under lualatex.
232
+
233
+ A project's `mathfmt.toml` names its root, the definition files whose macros
234
+ the tool reads beyond the root's, and the profiles it may emit; two profiles,
235
+ `unicode` and `ascii`, exist with defaults even when the file does not mention
236
+ them.
237
+
238
+ ### Notations with arguments
239
+
240
+ A symbol with parameters has spellings too. Declare the notation once, with
241
+ every form the project has ever written, and let each profile pick the one it
242
+ emits:
243
+
244
+ ```toml
245
+ [notations.KL]
246
+ macro = "\\KL{#1}{#2}"
247
+ expanded = "D_{\\mathrm{KL}}(#1 \\| #2)"
248
+ spaced = "D_{\\mathrm{KL}}(#1 \\,\\|\\, #2)"
249
+
250
+ [profiles.ascii.notations]
251
+ KL = "macro" # D_{\mathrm{KL}}(p \| q) → \KL{p}{q}
252
+ [profiles.unicode.notations]
253
+ KL = "expanded" # \KL{p}{q} → D_{\mathrm{KL}}(p ‖ q)
254
+ ```
255
+
256
+ Matching is on the math tree modulo symbols, so `\|`, `\Vert` and `‖` are one
257
+ thing, `\mathbb{E}` in a form is the character `𝔼` in the source,
258
+ `D_\mathrm{KL}` is `D_{\mathrm{KL}}`, and a hole binds the shortest stretch
259
+ between the literals that is balanced in its delimiters:
260
+ `D_{\mathrm{KL}}(q_\phi(z \mid x) ‖ p(z))` becomes
261
+ `\KL{q_\phi(z \mid x)}{p(z)}`, with the holes converted and nested notations
262
+ handled inside them. The form a profile writes goes through the profile's
263
+ symbol spellings too, so one form serves both; a macro in it (`\R^{#1}`) is
264
+ written as it stands. Among forms that match, the one with the most tokens
265
+ wins, whichever notation it belongs to. A profile that names no form leaves
266
+ the notation as written; `--emit symbols` still counts it, form by form.
267
+
268
+ The matcher prefers no match to a doubtful one, and a notation it does not
269
+ match is left exactly as written:
270
+
271
+ - A form must begin and end with something literal. `#1 \cdot #2` is refused
272
+ by the config: a hole at the edge has no boundary.
273
+ - A stretch that holds a `%` comment or a brace that is never closed is not a
274
+ match. A rewrite would swallow what follows the one and run to the end of
275
+ the file through the other.
276
+ - A group standing alone is not its content: `P(A {\mid} B)` is not
277
+ `P(#1 \mid #2)`, because the braces make the bar an ordinary symbol. A
278
+ braced script operand or command argument is its content.
279
+ - A notation that is itself a script operand (`e^\KL{p}{q}`) is kept: what
280
+ the script takes there is the macro's business.
281
+
282
+ ## What it does to a real paper
283
+
284
+ Run read-only over the eight `.tex` files of an ICML manuscript that defines
285
+ `\R`, `\E`, `\N` and friends in its style file:
286
+
287
+ ```sh
288
+ mathfmt --check --diff --macros ddssm.sty main.tex sections/*.tex
289
+ ```
290
+
291
+ | | |
292
+ |---|---|
293
+ | time for all eight files | about 25 ms |
294
+ | symbol occurrences rewritten | 679 |
295
+ | distinct symbols | 48 |
296
+ | warnings | 0 |
297
+ | style groups left as written | 0 |
298
+
299
+ The result taken through `--profile ascii` and back to `unicode` returns seven
300
+ of the eight files byte for byte. The eighth writes `\mathbf{\sigma_\theta}`,
301
+ which pdflatex renders as plain italic (its `\mathbf` ignores lowercase Greek);
302
+ mathfmt reads it literally as bold upright `𝛔_{𝛉}`, and the `ascii` profile
303
+ spells bold Greek with `\boldsymbol`, which reads back as bold italic `𝝈_{𝜽}`.
304
+ A profile with `bf_greek = "\\mathbf"` keeps pdflatex's reading and closes the
305
+ round trip; see "Known collapses" below. A line of the diff:
306
+
307
+ ```diff
308
+ -Let \( \mathbf{x}_{1:T} = (\mathbf{x}_1, \dots, \mathbf{x}_T) \) with \( \mathbf{x}_t \in \mathbb{R}^D \)
309
+ +Let \( 𝐱_{1:T} = (𝐱_1, \dots, 𝐱_T) \) with \( 𝐱_t ∈ ℝ^D \)
310
+ ```
311
+
312
+ The `ascii` profile on the same files makes 26 changes, every one a macro
313
+ inlined to its definition: `\N` to `\mathcal{N}`, `\E` to `\mathbb{E}`.
314
+ Collaborators who prefer the macros write a profile with
315
+ `spellings."ℝ" = "\\R"` and get them back.
316
+
317
+ ## As a pre-commit hook
318
+
319
+ For collaborators without Rust, a local hook that installs the wheel into
320
+ pre-commit's own environment:
321
+
322
+ ```yaml
323
+ repos:
324
+ - repo: local
325
+ hooks:
326
+ - id: mathfmt
327
+ name: mathfmt
328
+ entry: mathfmt
329
+ language: python
330
+ additional_dependencies: ["mathfmt-tex==0.1.0"]
331
+ args: ["--write"] # or ["--check"]: fail the commit, change nothing
332
+ files: \.(tex|sty)$
333
+ ```
334
+
335
+ Before the package is on PyPI, `additional_dependencies` takes a wheel's
336
+ path or URL instead. With a Rust toolchain, the repository is a hook repo
337
+ itself, and pre-commit builds the binary with cargo on first use:
338
+
339
+ ```yaml
340
+ repos:
341
+ - repo: ssh://forgejo@git.buttermilks.rocks/jack/mathfmt.git
342
+ rev: v0.1.0
343
+ hooks:
344
+ - id: mathfmt # rewrites staged .tex/.sty files to the profile
345
+ # - id: mathfmt-check # or: fail the commit, change nothing
346
+ ```
347
+
348
+ `tests/pre-commit-e2e.sh` builds the wheel and runs the first form in a
349
+ scratch repository, twice: the first run rewrites and fails, as pre-commit
350
+ does when a hook changes a file, and the second passes.
351
+
352
+ ## Known collapses
353
+
354
+ Places where the Unicode pivot loses a distinction, by design and
355
+ configurable:
356
+
357
+ - **cal and scr.** `\mathcal{F}` and `\mathscr{F}` both print as the one Unicode
358
+ script alphabet (`𝓕`); reading it back gives `\mathcal` unless the profile
359
+ sets `script_reads_as = "scr"`.
360
+ - **bf_greek.** The `ascii` profile prints a bold Greek letter as
361
+ `\boldsymbol{\alpha}` because pdflatex's `\mathbf` renders nothing bold for
362
+ Greek. Bold upright `𝛂` and bold italic `𝜶` therefore both become
363
+ `\boldsymbol{\alpha}` and come back as `𝜶`. Set `style_commands.bf_greek =
364
+ "\\mathbf"` for the literal reading.
365
+ - **ℑ and ℜ.** Unicode has one codepoint each for the fraktur I and R and for
366
+ the imaginary and real parts, so `\mathfrak{I}` and `\Im` are one atom. The
367
+ `ascii` profile spells it `\Im`; pdflatex draws `\mathfrak{I}` from eufm and
368
+ `\Im` from cmsy, and they look different. Set `spellings."frak:I" =
369
+ "\\mathfrak{I}"` (and `frak:R`) for a paper that means the letter.
370
+
371
+ - **The primes.** The characters `′`, `″` and `‴` are the superscript primes
372
+ that `'`, `''` and `'''` write, under unicode-math-input and unicode-math
373
+ alike, so every profile writes them that way; `\prime` is the unraised
374
+ glyph and is left alone, and `‵` has no command spelling.
375
+ - **`\emptyset` and `\varnothing`.** `\emptyset` (pdflatex's slashed zero) and `\varnothing`
376
+ (amssymb's slashed circle) are one codepoint, U+2205, whose glyph is the
377
+ circle. The `ascii` profile spells it `\varnothing`; set
378
+ `spellings."∅" = "\\emptyset"` for a paper that means the zero.
379
+ - **Shape under unicode-math.** With that package's default options the
380
+ shape of a bold or sans letter is the document's (`bold-style`,
381
+ `sans-style`), not the character's: a literal `𝛔`, a literal `𝝈` and
382
+ `\symbf{\sigma}` all draw the house bold sigma, and a literal `𝒙` draws
383
+ upright. The preset therefore spells `bf` as `\symbf` and `sf` as `\symsf`.
384
+ A letter whose shape contradicts the house style (`𝒙`, `𝘈`,
385
+ `\symbfup{\sigma}`, `\symbfit{x}`) changes when it crosses between a
386
+ character and a command. A document that sets `bold-style=literal` and
387
+ `sans-style=literal` wants `bf = "\\symbfup"`, `sf = "\\symsfup"` and
388
+ `bfsf = "\\symbfsfup"` in `style_commands`.
389
+
390
+ `\mathbb{1}` is not a collapse but a repair: amsfonts has no blackboard digits
391
+ and `\mathbb{1}` is the glyph ⊮, so the `bb_digit` key spells `𝟙` as
392
+ `\mathbbm{1}`. `--verify` reports each of these as a difference when a
393
+ conversion crosses one, which is how they were found.
394
+
395
+ ## Developing
396
+
397
+ `cargo test --workspace` on a laptop; `tests/dev.sh cargo test --workspace`
398
+ anywhere else (it enters the dev shell and runs cargo offline against the
399
+ crates nix vendored from `Cargo.lock`). `CLAUDE.md` has the rest: the fixture
400
+ discipline, the tables, and the rules.
401
+
@@ -0,0 +1,5 @@
1
+ mathfmt_tex-0.1.0.data/scripts/mathfmt.exe,sha256=PtpWZ_lYgeDcJBGVU_rzxRbECI9zXT-w0DaMdw3XEAE,2613760
2
+ mathfmt_tex-0.1.0.dist-info/METADATA,sha256=mudFUfunUrQ5B54z0Uh64U9-qDRfuzg3EY4OyDD70NA,20087
3
+ mathfmt_tex-0.1.0.dist-info/WHEEL,sha256=Uvw7WSmy7Kj2FBk8Va4L_dq4MDW8X5Ynby-pOX3ZNS0,94
4
+ mathfmt_tex-0.1.0.dist-info/sboms/mathfmt.cyclonedx.json,sha256=Ae8_5L3JQVGmJXvTTyb3BAlzM4hJt45-CQetxmQ0xD0,50665
5
+ mathfmt_tex-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.16.0)
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_amd64