paperlint 4.0.0 → 4.2.0
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.
- package/.github/workflows/ci.yml +16 -23
- package/CLAUDE.md +55 -11
- package/CONTRIBUTING.md +30 -7
- package/README.md +11 -8
- package/dist/adapters/banal/invocation.d.ts +1 -1
- package/dist/adapters/latex/document-class.d.ts +23 -0
- package/dist/adapters/latex/document-class.d.ts.map +1 -0
- package/dist/adapters/latex/document-class.js +110 -0
- package/dist/adapters/latex/document-class.js.map +1 -0
- package/dist/adapters/latex/hidden.d.ts +16 -0
- package/dist/adapters/latex/hidden.d.ts.map +1 -0
- package/dist/adapters/latex/hidden.js +49 -0
- package/dist/adapters/latex/hidden.js.map +1 -0
- package/dist/adapters/latex/includes.d.ts +12 -0
- package/dist/adapters/latex/includes.d.ts.map +1 -0
- package/dist/adapters/latex/includes.js +42 -0
- package/dist/adapters/latex/includes.js.map +1 -0
- package/dist/adapters/latex/index.d.ts +33 -0
- package/dist/adapters/latex/index.d.ts.map +1 -0
- package/dist/adapters/latex/index.js +25 -0
- package/dist/adapters/latex/index.js.map +1 -0
- package/dist/adapters/latex/nodes.d.ts +58 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -0
- package/dist/adapters/latex/nodes.js +97 -0
- package/dist/adapters/latex/nodes.js.map +1 -0
- package/dist/adapters/latex/outline.d.ts +10 -0
- package/dist/adapters/latex/outline.d.ts.map +1 -0
- package/dist/adapters/latex/outline.js +39 -0
- package/dist/adapters/latex/outline.js.map +1 -0
- package/dist/adapters/latex/parse.d.ts +42 -0
- package/dist/adapters/latex/parse.d.ts.map +1 -0
- package/dist/adapters/latex/parse.js +95 -0
- package/dist/adapters/latex/parse.js.map +1 -0
- package/dist/adapters/latex/prose.d.ts +8 -0
- package/dist/adapters/latex/prose.d.ts.map +1 -0
- package/dist/adapters/latex/prose.js +353 -0
- package/dist/adapters/latex/prose.js.map +1 -0
- package/dist/adapters/latex/rendered.d.ts +10 -0
- package/dist/adapters/latex/rendered.d.ts.map +1 -0
- package/dist/adapters/latex/rendered.js +39 -0
- package/dist/adapters/latex/rendered.js.map +1 -0
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +12 -12
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/build.js +4 -4
- package/dist/build.js.map +1 -1
- package/dist/claim-provenance.d.ts +34 -0
- package/dist/claim-provenance.d.ts.map +1 -0
- package/dist/claim-provenance.js +146 -0
- package/dist/claim-provenance.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +49 -5
- package/dist/cli.js.map +1 -1
- package/dist/domain/paper-source.d.ts +58 -0
- package/dist/domain/paper-source.d.ts.map +1 -0
- package/dist/domain/paper-source.js +109 -0
- package/dist/domain/paper-source.js.map +1 -0
- package/dist/domain/sentences.d.ts +26 -0
- package/dist/domain/sentences.d.ts.map +1 -0
- package/dist/domain/sentences.js +48 -0
- package/dist/domain/sentences.js.map +1 -0
- package/dist/domain/tex-document.d.ts +119 -0
- package/dist/domain/tex-document.d.ts.map +1 -0
- package/dist/domain/tex-document.js +43 -0
- package/dist/domain/tex-document.js.map +1 -0
- package/dist/eslint-rules/paper-typography.d.ts +7 -0
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +53 -1
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +2 -2
- package/dist/facts-file.js.map +1 -1
- package/dist/new-paper.d.ts +5 -0
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +3 -0
- package/dist/new-paper.js.map +1 -1
- package/dist/package-dirs.d.ts +5 -0
- package/dist/package-dirs.d.ts.map +1 -0
- package/dist/package-dirs.js +14 -0
- package/dist/package-dirs.js.map +1 -0
- package/dist/ports/latex.d.ts +35 -0
- package/dist/ports/latex.d.ts.map +1 -0
- package/dist/ports/latex.js +2 -0
- package/dist/ports/latex.js.map +1 -0
- package/dist/presets.d.ts +15 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +19 -6
- package/dist/presets.js.map +1 -1
- package/dist/register.d.ts +40 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +134 -0
- package/dist/register.js.map +1 -0
- package/dist/tex-paper.d.ts +19 -0
- package/dist/tex-paper.d.ts.map +1 -0
- package/dist/tex-paper.js +105 -0
- package/dist/tex-paper.js.map +1 -0
- package/dist/tex-requirements.d.ts +11 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +14 -8
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +88 -0
- package/dist/tex-venue-rules.d.ts.map +1 -0
- package/dist/tex-venue-rules.js +268 -0
- package/dist/tex-venue-rules.js.map +1 -0
- package/dist/venue-rules.d.ts +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/docs/configuration.md +3 -2
- package/docs/e2e.md +69 -40
- package/docs/install.md +1 -1
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/package-location.md +5 -5
- package/docs/prior-art/repro/README.md +1 -1
- package/docs/prior-art/test-tooling.md +21 -10
- package/docs/rules/tex/claim-provenance.md +83 -0
- package/docs/rules/tex/missing-input.md +58 -0
- package/docs/rules/tex/register.md +77 -0
- package/docs/rules/tex/required-section.md +75 -0
- package/docs/rules/tex/template.md +74 -0
- package/docs/rules/tex/venue-leftover.md +73 -0
- package/docs/rules.md +73 -48
- package/docs/testing.md +17 -4
- package/docs/texlive-install-decision.md +1 -1
- package/docs/toolchain.md +2 -2
- package/eslint-rules/paper-typography.test.mjs +48 -1
- package/eslint-rules/paper-typography.ts +72 -1
- package/eslint.config.mjs +156 -15
- package/fixtures/build-e2e/acmart/expect.json +26 -0
- package/fixtures/build-e2e/aidc/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/aidc/expect.json +14 -0
- package/fixtures/build-e2e/aidc/paper.tex +46 -0
- package/fixtures/build-e2e/aidc/paperlint.json +1 -0
- package/fixtures/build-e2e/broken/expect.json +11 -0
- package/fixtures/build-e2e/cite/expect.json +27 -0
- package/fixtures/build-e2e/empty/expect.json +12 -0
- package/fixtures/build-e2e/fallback/expect.json +53 -0
- package/fixtures/build-e2e/guards/expect.json +6 -0
- package/fixtures/build-e2e/no-source/expect.json +10 -0
- package/fixtures/build-e2e/unbalanced/expect.json +60 -0
- package/fixtures/pdf-facts/README.md +1 -1
- package/fixtures/real-markdown-paper/baseline.ts +8 -7
- package/fixtures/render-paper/build-clean.sh +3 -5
- package/package.json +14 -2
- package/{skills/submit-paper/references/venues → presets}/acm-sigconf.jsonc +4 -1
- package/{skills/submit-paper/references/venues → presets}/agenticdev.jsonc +4 -0
- package/presets/aidc.jsonc +73 -0
- package/{skills/submit-paper/references/venues → presets}/aisec.jsonc +4 -0
- package/presets/ieee-conference.jsonc +66 -0
- package/{skills/submit-paper/references/venues → presets}/realm.jsonc +8 -1
- package/{skills/submit-paper/references/venues → presets/tex}/paper-guards.tex +1 -1
- package/{skills/submit-paper/references/venues → presets}/venue-profile.schema.json +30 -2
- package/scripts/check.harness.ts +64 -10
- package/scripts/check.test.ts +60 -13
- package/scripts/check.ts +83 -30
- package/scripts/test.test.ts +1 -1
- package/scripts/test.ts +1 -1
- package/skills/cold-read-diff/SKILL.md +31 -10
- package/skills/cold-read-diff/SKILL.md.spec.ts +30 -9
- package/skills/draft-paper/SKILL.md +2 -2
- package/skills/draft-paper/SKILL.md.spec.ts +1 -1
- package/skills/grade-paper-writing/SKILL.md +109 -44
- package/skills/grade-paper-writing/SKILL.md.spec.ts +108 -43
- package/skills/harden-paper/SKILL.md +35 -36
- package/skills/harden-paper/SKILL.md.spec.ts +34 -35
- package/skills/paper-adversarial-review/SKILL.md +2 -2
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/PIPELINE-MAP.md +2 -2
- package/skills/paper-pipeline/SKILL.md +4 -4
- package/skills/paper-pipeline/SKILL.md.spec.ts +3 -3
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +6 -6
- package/skills/paper-pipeline/references/writing-craft.md +53 -18
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -2
- package/skills/paper-pipeline/scripts/consumer.mjs +1 -15
- package/skills/pc-panel-review/SKILL.md +36 -11
- package/skills/pc-panel-review/SKILL.md.spec.ts +35 -10
- package/skills/render-paper/SKILL.md +2 -2
- package/skills/render-paper/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/check-render.sh +16 -63
- package/skills/render-paper/render-paper.harness.mjs +17 -59
- package/src/adapters/banal/index.test.ts +1 -1
- package/src/adapters/banal/invocation.ts +1 -1
- package/src/adapters/banal/output.test.ts +1 -1
- package/src/adapters/banal/run.test.ts +1 -1
- package/src/adapters/banal/xml.test.ts +1 -1
- package/src/adapters/latex/document-class.ts +155 -0
- package/src/adapters/latex/hidden.ts +70 -0
- package/src/adapters/latex/includes.ts +66 -0
- package/src/adapters/latex/index.test.ts +535 -0
- package/src/adapters/latex/index.ts +65 -0
- package/src/adapters/latex/nodes.ts +150 -0
- package/src/adapters/latex/outline.ts +64 -0
- package/src/adapters/latex/parse.ts +137 -0
- package/src/adapters/latex/prose.ts +484 -0
- package/src/adapters/latex/rendered.ts +59 -0
- package/src/adapters/pdfjs/fill.test.ts +1 -1
- package/src/adapters/pdfjs/fill.ts +18 -10
- package/src/build.harness.mjs +5 -7
- package/src/build.ts +4 -4
- package/src/claim-provenance.test.ts +352 -0
- package/src/claim-provenance.ts +217 -0
- package/src/cli.harness.mjs +1 -1
- package/src/cli.test.ts +2 -8
- package/src/cli.ts +53 -8
- package/src/domain/paper-source.test.ts +156 -0
- package/src/domain/paper-source.ts +233 -0
- package/src/domain/sentences.ts +67 -0
- package/src/domain/tex-document.test.ts +80 -0
- package/src/domain/tex-document.ts +143 -0
- package/src/engine.harness.mjs +1 -1
- package/src/facts-file.test.ts +4 -7
- package/src/facts-file.ts +2 -2
- package/src/latex-loop.harness.ts +1 -1
- package/src/new-paper.test.ts +28 -3
- package/src/new-paper.ts +8 -0
- package/src/package-dirs.ts +17 -0
- package/src/paper-settings-commands.test.ts +6 -1
- package/src/ports/latex.ts +42 -0
- package/src/presets.test.ts +65 -4
- package/src/presets.ts +38 -15
- package/src/register.test.ts +165 -0
- package/src/register.ts +177 -0
- package/src/rule-cases.test.ts +524 -0
- package/src/rule-docs.test.ts +159 -0
- package/src/tex-paper.test.ts +131 -0
- package/src/tex-paper.ts +152 -0
- package/src/tex-requirements.harness.ts +13 -23
- package/src/tex-requirements.ts +35 -8
- package/src/tex-venue-rules.test.ts +741 -0
- package/src/tex-venue-rules.ts +461 -0
- package/src/toolchain.harness.mjs +1 -1
- package/src/venue-rules.test.ts +96 -4
- package/src/venue-rules.ts +1 -1
- package/templates/paper/paper.tex +2 -1
- package/skills/harden-paper/fixtures/uncited-assertions-sample.md +0 -43
- package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +0 -77
- /package/{skills/submit-paper/references/venues → presets/tex}/agenticdev.tex +0 -0
- /package/{skills/submit-paper/references/venues → presets}/tex-base.jsonc +0 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# tex/template
|
|
2
|
+
|
|
3
|
+
**Level:** error · **Reads:** `paper.tex`, and the preset its `paperlint.json` extends
|
|
4
|
+
|
|
5
|
+
## What it catches
|
|
6
|
+
|
|
7
|
+
The paper is not set in the class its venue requires. The rule compares the `\documentclass` line
|
|
8
|
+
of `paper.tex` with the preset's `template`: the class must be the same, and every option the
|
|
9
|
+
template names must be there. The paper may add options of its own.
|
|
10
|
+
|
|
11
|
+
A source that keeps one file for several venues and picks the class behind a TeX switch has several
|
|
12
|
+
`\documentclass` lines. The switch is not evaluated: each line is a candidate, the paper passes when
|
|
13
|
+
one of them is the template's class with its options, and when none is, one finding at the first
|
|
14
|
+
candidate names every one:
|
|
15
|
+
|
|
16
|
+
```latex
|
|
17
|
+
\def\venue{2}
|
|
18
|
+
\if\venue1 \documentclass{article} \fi
|
|
19
|
+
\if\venue2 \documentclass[conference,compsoc]{IEEEtran} \fi % passes: the second candidate matches
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
It only knows the class a preset names. A paper whose `paperlint.json` names no preset, or a
|
|
23
|
+
preset with no `template`, gets no finding.
|
|
24
|
+
|
|
25
|
+
## Why
|
|
26
|
+
|
|
27
|
+
A venue checks the template before anyone reads the paper, and a paper in the wrong template can
|
|
28
|
+
be rejected with no review. Venues that name a class say so plainly — AIDC, for one: «LaTeX
|
|
29
|
+
submissions must use the IEEEtran.cls version 1.8b template with the
|
|
30
|
+
\documentclass[conference,compsoc]{IEEEtran} document class option», and «This requirement will be
|
|
31
|
+
strictly enforced» (https://aidcworkshop.github.io/).
|
|
32
|
+
|
|
33
|
+
The `pdf/*` rules see the consequences of a wrong class only after a build — fonts, page size,
|
|
34
|
+
columns — and some not at all: `compsoc` changes the text block and the headings, not the fonts.
|
|
35
|
+
This rule reads the cause, in the source, before any build.
|
|
36
|
+
|
|
37
|
+
## Examples
|
|
38
|
+
|
|
39
|
+
Under a preset with `"template": "\\documentclass[conference,compsoc]{IEEEtran}"`:
|
|
40
|
+
|
|
41
|
+
```latex
|
|
42
|
+
\documentclass[conference]{IEEEtran} % fails: compsoc is missing
|
|
43
|
+
\documentclass[sigconf]{acmart} % fails: another class
|
|
44
|
+
\documentclass[compsoc, conference]{IEEEtran} % passes: any order, spaces allowed
|
|
45
|
+
\documentclass[conference,compsoc,final]{IEEEtran} % passes: an extra option of the paper's own
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The first line reports:
|
|
49
|
+
|
|
50
|
+
> the class option `compsoc` is missing: aidc requires `\documentclass[conference,compsoc]{IEEEtran}`
|
|
51
|
+
|
|
52
|
+
## Preset field
|
|
53
|
+
|
|
54
|
+
No rule options. It reads `template`, merged along `extends` (a child's replaces its parent's):
|
|
55
|
+
|
|
56
|
+
```jsonc
|
|
57
|
+
"template": "\\documentclass[conference,compsoc]{IEEEtran}"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
A whole `\documentclass[…]{…}` line, or a bare class name (`"article"`), which requires the class
|
|
61
|
+
and no option. A value that is neither is reported as a finding that names the preset file.
|
|
62
|
+
|
|
63
|
+
## What it does not check
|
|
64
|
+
|
|
65
|
+
- Options a venue forbids (`review`, `anonymous` in a camera-ready): only that what the venue
|
|
66
|
+
requires is there.
|
|
67
|
+
- The version of the class file (#94).
|
|
68
|
+
- A `\documentclass` inside a comment: the rule reads the parse tree, so it does not count.
|
|
69
|
+
- Which branch of a TeX switch builds: with several class lines, one matching candidate is enough.
|
|
70
|
+
|
|
71
|
+
## How to fix
|
|
72
|
+
|
|
73
|
+
Set the `\documentclass` line to the one the message quotes, keeping your own extra options. A new
|
|
74
|
+
paper made with `paperlint new <name> --venue <preset>` already has it.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# tex/venue-leftover
|
|
2
|
+
|
|
3
|
+
**Level:** warn · **Reads:** `paper.tex`, and the shipped venue presets
|
|
4
|
+
|
|
5
|
+
## What it catches
|
|
6
|
+
|
|
7
|
+
The paper mentions a venue other than the one set in its `paperlint.json`. Usually that is text
|
|
8
|
+
left over from an earlier submission: "Submission to VenueA 2026" in the author block of a paper
|
|
9
|
+
now going to VenueB.
|
|
10
|
+
|
|
11
|
+
It only knows the venues paperlint ships a preset for, by each preset's `name` and `aliases`. A
|
|
12
|
+
venue with no preset is not recognized.
|
|
13
|
+
|
|
14
|
+
It reads the whole paper — `paper.tex` with the files it `\input`s, `\include`s or `\subfile`s
|
|
15
|
+
spliced in; a name found in an included file is reported at that include, with the file and line at
|
|
16
|
+
the front of the message. It looks at what a reader sees — prose, the author block, captions, footnotes, a citation's note in
|
|
17
|
+
square brackets, and front-matter macros such as `\acmConference` — and ignores comments, citation
|
|
18
|
+
keys, labels, URLs (a `\url`, and the address of an `\href` — its link text is read), math, code and
|
|
19
|
+
the bibliography. Names are matched as whole words, case-sensitive.
|
|
20
|
+
|
|
21
|
+
A macro definition that names another venue — `\newcommand{\oldvenue}{VenueA}` — is reported at the
|
|
22
|
+
definition, whether or not the macro is used: the rule does not expand macros, so a use such as
|
|
23
|
+
`Submitted to \oldvenue` has no text of its own, and the definition is where the old name lives.
|
|
24
|
+
|
|
25
|
+
## Why
|
|
26
|
+
|
|
27
|
+
A reviewer reads the author block before the abstract. A line naming the previous venue tells them
|
|
28
|
+
the paper was written for somewhere else, and at a double-blind venue it can say more than that.
|
|
29
|
+
Nothing else compares what the source says about the venue with what the settings say.
|
|
30
|
+
|
|
31
|
+
## Examples
|
|
32
|
+
|
|
33
|
+
Under `"extends": "paperlint:realm"`, where the `agenticdev` preset's alias is `AgenticDev`:
|
|
34
|
+
|
|
35
|
+
```latex
|
|
36
|
+
\acmConference[AgenticDev '26]{Workshop on Agentic Development} % warns: «AgenticDev»
|
|
37
|
+
As shown~\cite{agenticdev2025}. % silent: a citation key
|
|
38
|
+
% AgenticDev draft % silent: a comment
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The first line reports:
|
|
42
|
+
|
|
43
|
+
> «AgenticDev» names agenticdev, and this paper extends realm — a leftover from an earlier
|
|
44
|
+
> submission? A reviewer reads it before the abstract. Comments and citation keys are not
|
|
45
|
+
> reported; a sentence that names the other venue on purpose can keep it with a disable directive
|
|
46
|
+
|
|
47
|
+
The same line passes under `"extends": "paperlint:agenticdev"`: it names the paper's own venue.
|
|
48
|
+
|
|
49
|
+
## Preset fields
|
|
50
|
+
|
|
51
|
+
No rule options. It reads every shipped preset's `name` and `aliases`, collected along `extends`:
|
|
52
|
+
|
|
53
|
+
```jsonc
|
|
54
|
+
"aliases": ["AgenticDev"]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
A name the paper's own chain also declares (two workshops of one parent conference) is never
|
|
58
|
+
reported.
|
|
59
|
+
|
|
60
|
+
## What it does not check
|
|
61
|
+
|
|
62
|
+
- A venue no shipped preset describes.
|
|
63
|
+
- A name split by markup (`\textbf{AI}Sec`).
|
|
64
|
+
|
|
65
|
+
## How to fix
|
|
66
|
+
|
|
67
|
+
Remove or replace the old venue's name. A sentence that names another venue on purpose keeps it
|
|
68
|
+
with ESLint's directive and the reason:
|
|
69
|
+
|
|
70
|
+
```latex
|
|
71
|
+
% eslint-disable-next-line tex/venue-leftover -- comparing with the venue that published [3]
|
|
72
|
+
Unlike AgenticDev, this workshop …
|
|
73
|
+
```
|
package/docs/rules.md
CHANGED
|
@@ -3,30 +3,40 @@
|
|
|
3
3
|
The README lists what each check catches in one line. This page says exactly which file each one
|
|
4
4
|
reads and when it fails. Errors fail `paperlint lint`; warnings print and do not.
|
|
5
5
|
|
|
6
|
-
| Rule
|
|
7
|
-
|
|
|
8
|
-
| `paper/stages`
|
|
9
|
-
| `paper/source`
|
|
10
|
-
| `paper/author-list`
|
|
11
|
-
| `paper/research-question`
|
|
12
|
-
| `paper/section-word`
|
|
13
|
-
| `paper/leading-zero`
|
|
14
|
-
| `paper/figure-ref-style`
|
|
15
|
-
| `bib/reachable-entry`
|
|
16
|
-
| `paper/cite-exists`
|
|
17
|
-
| `paper/refs-fresh`
|
|
18
|
-
| `paper/refs-checked`
|
|
19
|
-
| `tex/future-promise`
|
|
20
|
-
| `tex/acm-frontmatter-override`
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `pdf/
|
|
6
|
+
| Rule | Level | Reads | Fails when |
|
|
7
|
+
| ------------------------------------------------------- | ----- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
8
|
+
| `paper/stages` | error | `PIPELINE-STATUS.md` | a declared stage's PDF is missing, or the `bytes:` it names does not match the file's real size, or a frozen PDF exists that no stage declares |
|
|
9
|
+
| `paper/source` | error | `PIPELINE-STATUS.md` | a declared stage has no frozen `.tex` beside its PDF (a commit hash does not count — squash and gc destroy it) |
|
|
10
|
+
| `paper/author-list` | error | `paper.tex` → `_build/references.json` | an entry's authors are not those of the version it cites — typically the arXiv preprint's list under a published venue. Checked online by `paperlint build` (DBLP), reported on the entry's own line |
|
|
11
|
+
| `paper/research-question` | warn | `paper.tex`, `paper.md`, `PIPELINE-STATUS.md` | a stage is declared and either the scorecard has no `researchQuestion` field, or it has one the paper does not contain. The declared sentence is compared against the source with whitespace collapsed — your words, not a pattern. Advisory, because nothing here can judge whether what you declared _is_ a research question |
|
|
12
|
+
| `paper/section-word` | warn | `paper.tex`, `paper.md` | `§` or `\S\ref` instead of the word Section. **Fixable:** `--fix` writes `Section~\ref{…}` / `Section 5` |
|
|
13
|
+
| `paper/leading-zero` | warn | `paper.tex`, `paper.md` | a decimal below 1 without its leading zero (`.05` instead of `0.05`, IEEE / ISO 80000-1 style), counted only where the reader sees it — prose, math and table cells, not macro options, column specs, comments, code or tikz. A number right after `¶` or `§` (`¶¶.42`) is a paragraph designator and is not reported; in a paragraph that cites numbers that way, another bare decimal is reported without a fix, with the zero as a suggestion. **Fixable** |
|
|
14
|
+
| `paper/figure-ref-style` | warn | `paper.tex` | `Fig.~\ref` and `Figure~\ref` mixed in one document; each minority occurrence is reported. **Fixable:** `--fix` writes the majority form |
|
|
15
|
+
| `bib/reachable-entry` | warn | `paper.tex` (its `filecontents` bibliography) | an entry has no doi, url or arXiv id — a reader has nothing to follow. Reported on the entry; no fix, the link has to be looked up |
|
|
16
|
+
| `paper/cite-exists` | error | `paper.tex` → `_build/references.json` | an entry's doi or arXiv id resolves to nothing, or to a different work. Checked online by `paperlint build`; a work that was merely not found is not a finding |
|
|
17
|
+
| `paper/refs-fresh` | error | `paper.tex` → `_build/references.json` | the bibliography changed since the build checked it (its SHA-256 differs) — run `npx paperlint build` |
|
|
18
|
+
| `paper/refs-checked` | warn | `paper.tex` → `_build/references.json` | the paper has a bibliography and no build has checked it, or the last build could not (no network) — so the two rules above did not run |
|
|
19
|
+
| `tex/future-promise` | warn | `paper.tex` | a camera-ready build still says "will be released" about something already handed over |
|
|
20
|
+
| `tex/acm-frontmatter-override` | error | `paper.tex` | an `acmart` build overrides ACM's front-matter commands and drops template elements from page 1 |
|
|
21
|
+
| [`tex/template`](rules/tex/template.md) | error | `paper.tex` → `paperlint.json` | the `\documentclass` is not the preset's `template` class, or lacks an option it names — [page](rules/tex/template.md) |
|
|
22
|
+
| [`tex/required-section`](rules/tex/required-section.md) | error | `paper.tex` → `paperlint.json` | a section the preset requires (AIDC: «LLM Usage Statement», last) is missing or not where it must be — [page](rules/tex/required-section.md) |
|
|
23
|
+
| [`tex/venue-leftover`](rules/tex/venue-leftover.md) | warn | `paper.tex` → `paperlint.json` | the text names another shipped venue than the one the paper extends («Submission to VenueA 2026» in a paper now set for VenueB) — [page](rules/tex/venue-leftover.md) |
|
|
24
|
+
| [`tex/claim-provenance`](rules/tex/claim-provenance.md) | warn | `paper.tex` | a sentence of the body states a number and does not say whose it is — no citation or link, no _we_/_our_, no `\ref` or "Table 2", no sample ("n = 134", "48 runs") — [page](rules/tex/claim-provenance.md) |
|
|
25
|
+
| [`tex/missing-input`](rules/tex/missing-input.md) | warn | `paper.tex` and the files it includes | an `\input`, `\include` or `\subfile` names a file that is not there, so no rule read its text — [page](rules/tex/missing-input.md) |
|
|
26
|
+
| [`tex/register`](rules/tex/register.md) | warn | `paper.tex` and the files it includes | the body runs to a post's register: more than 0.8 sentences per 1000 words open with _And_, _So_, _But_, _Nor_, _Or_ or _Yet_ — the limit is an option — [page](rules/tex/register.md) |
|
|
27
|
+
| `review/frontmatter` | error | `reviews/*.md` | the review's frontmatter fails paperlint's JSON Schema — a `findings` record without `id`/`status`, an unknown field, or an **open** finding with no `cause` (`skill-defect` · `missing-skill` · `hook` · `rule`). A review with no `findings` key is not checked for findings |
|
|
28
|
+
| `sibling/frontmatter` | warn | `siblings/*.md` (not `README.md`) | a sibling card's frontmatter has no `read:` saying how much of the competing paper was read (`full` · `abstract` · `none`) |
|
|
29
|
+
| `pdf/fresh` | error | `paper.tex` → `_build/paper.facts.json` | the paper names a venue and the facts cannot be judged: not JSON, a schema other than 2, or they describe a PDF that is gone or differs from the one on disk (its SHA-256) |
|
|
30
|
+
| `pdf/profile` | error | `paper.tex` → `paperlint.json` | `paperlint.json` is not JSON or has an unknown key, its `extends` does not resolve (not found, a cycle, a chain longer than four, a preset that fails the schema, an npm name), it names no `kind` while the preset has kinds, or names a kind the preset does not have |
|
|
31
|
+
| `pdf/fonts` | error | `paper.tex` → `_build/paper.facts.json` | a font the pages draw is Type 3 or not embedded, or no font starts with the family the venue preset names for body text (`fonts_text`) or headings (`fonts_title`) |
|
|
32
|
+
| `pdf/geometry` | error | `paper.tex` → `_build/paper.facts.json` | the page width or height is more than `dimTol` (default 0.05 in) off the preset's, or the column count differs |
|
|
33
|
+
| `pdf/limits` | error | `paper.tex` → `_build/paper.facts.json` | body or reference pages exceed the limit of the paper's kind, or the reference font size is outside the preset's range widened by `body_pt_tol` |
|
|
34
|
+
| `pdf/body-size` | warn | `paper.tex` → `_build/paper.facts.json` | the body font size is more than `body_pt_tol` off the preset's. A warning: banal measures the mode of the rendered text, not the declared size (9.30 pt measured at a declared 9) |
|
|
35
|
+
| `pdf/measured` | warn | `paper.tex` → `_build/paper.facts.json` | the paper's `paperlint.json` names no venue preset yet; or it names one and there are no facts (it was not built), or the facts carry no page geometry (banal was not found) — so the checks above did not run |
|
|
36
|
+
|
|
37
|
+
`tex/required-section` requires AIDC's «LLM Usage Statement» **always**. ACSAC asks for it «if
|
|
38
|
+
LLMs are used», which no rule can know; an author who used none says so in the section
|
|
39
|
+
([page](rules/tex/required-section.md)).
|
|
30
40
|
|
|
31
41
|
Optional rules — off unless you turn them on in the `rules` setting, because only some venues need
|
|
32
42
|
them — are on their own page: [`optional-rules.md`](optional-rules.md). Today there is one,
|
|
@@ -79,21 +89,34 @@ for papers, each with the quote it came from, the TeX packages its template need
|
|
|
79
89
|
the venue implies. A preset may itself extend another: the ACM venues extend the `acm-sigconf`
|
|
80
90
|
family, which holds everything the ACM template decides.
|
|
81
91
|
|
|
82
|
-
| preset
|
|
83
|
-
|
|
|
84
|
-
| `paperlint:acm-sigconf`
|
|
85
|
-
| `paperlint:agenticdev`
|
|
86
|
-
| `paperlint:aisec`
|
|
87
|
-
| `paperlint:realm`
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
| preset | extends | template | kinds | page limit checked | rules it turns on |
|
|
93
|
+
| --------------------------- | --------------------------- | ------------------------------------- | -------------------------------------------------------- | ------------------ | ----------------------- |
|
|
94
|
+
| `paperlint:acm-sigconf` | — | ACM `acmart` sigconf | none | no (no kinds) | — |
|
|
95
|
+
| `paperlint:agenticdev` | `paperlint:acm-sigconf` | ACM `acmart` sigconf | `short` (5 + 2 refs), `full` (10 + 2), `demo` (5 + 2) | yes | `pdf/last-page-balance` |
|
|
96
|
+
| `paperlint:aisec` | `paperlint:acm-sigconf` | ACM `acmart` sigconf | `research`, `benchmark`, `position`, `sok` (10 + 2 each) | yes | — |
|
|
97
|
+
| `paperlint:realm` | — | ACL | `long`, `short` | no — see below | — |
|
|
98
|
+
| `paperlint:ieee-conference` | — | IEEE `IEEEtran` conference | none | no (no kinds) | — |
|
|
99
|
+
| `paperlint:aidc` | `paperlint:ieee-conference` | IEEE `IEEEtran` conference, `compsoc` | `regular` (12), `short` (6) | no — see below | — |
|
|
100
|
+
|
|
101
|
+
That is all that ships today. There is no NeurIPS, USENIX or Springer preset. A paper for an ACM or
|
|
102
|
+
IEEE conference venue nobody has profiled can extend `paperlint:acm-sigconf` or
|
|
103
|
+
`paperlint:ieee-conference` directly: page size, columns and fonts are checked, and `pdf/profile`
|
|
104
|
+
says the page limit is not. For anything else, write your own preset
|
|
105
|
+
([below](#writing-your-own-venue-preset)). REALM's preset sets no page limit on purpose: banal
|
|
106
|
+
counts the Limitations and Ethics sections as body, ACL does not, and a limit on banal's number
|
|
107
|
+
would fail a correct paper. AIDC's sets none for the same reason, measured: banal counts a page as
|
|
108
|
+
body when an appendix or the LLM Usage Statement sits on it before the bibliography, and AIDC counts
|
|
109
|
+
neither, so a correct 12-page paper measured 13. AgenticDev's turns on `pdf/last-page-balance`
|
|
110
|
+
because its proceedings are produced by Conference Publishing Consulting, which sends back an
|
|
111
|
+
unbalanced last page; AISec's does not, because nothing in hand says who produces the AISec
|
|
112
|
+
proceedings.
|
|
113
|
+
|
|
114
|
+
**How presets are named.** A template family — the preset venues extend, owning the template,
|
|
115
|
+
the format and the TeX packages — is `<publisher>-<template variant>`: `acm-sigconf`, `ieee-conference`. A venue
|
|
116
|
+
is its own short name, lowercase, with no publisher and no parent-conference prefix: `aisec`,
|
|
117
|
+
`agenticdev`, `realm`, `aidc`. Which template a venue uses is said by its `extends`, not by its
|
|
118
|
+
name — a name repeating it would be wrong the day the venue changes template — and the parent
|
|
119
|
+
conference (CCS, ASE, ACSAC) goes into the preset's comment and its `aliases`.
|
|
97
120
|
|
|
98
121
|
**Build, then lint.** The rules judge what `paperlint build` measured and wrote to
|
|
99
122
|
`_build/paper.facts.json` — page count, fonts, and, through banal, page size, columns, font sizes
|
|
@@ -130,7 +153,7 @@ To skip a check for one paper — a finding you accept — set it to `"off"` in
|
|
|
130
153
|
|
|
131
154
|
A preset is a JSONC file (JSON with comments — keep the call-for-papers quote beside each number)
|
|
132
155
|
of the same shape as the shipped ones, validated by
|
|
133
|
-
[`venue-profile.schema.json`](../
|
|
156
|
+
[`venue-profile.schema.json`](../presets/venue-profile.schema.json).
|
|
134
157
|
Put it in your repository and extend it by a path relative to the file that names it:
|
|
135
158
|
|
|
136
159
|
```
|
|
@@ -170,14 +193,16 @@ papers/
|
|
|
170
193
|
|
|
171
194
|
The numbers and packages above illustrate the shape; take yours from the venue's own call for papers and template.
|
|
172
195
|
|
|
173
|
-
| key
|
|
174
|
-
|
|
|
175
|
-
| `extends`
|
|
176
|
-
| `name`
|
|
177
|
-
| `template`
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
196
|
+
| key | what it is |
|
|
197
|
+
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
198
|
+
| `extends` | the preset this one builds on: `paperlint:<name>` or `./path` / `../path`, relative to this file |
|
|
199
|
+
| `name` | how messages name the venue; the file name otherwise |
|
|
200
|
+
| `template` | the `\documentclass` the venue's template uses — a whole line (`\documentclass[conference,compsoc]{IEEEtran}`) or a bare class name; `tex/template` requires the paper's class and every option named here |
|
|
201
|
+
| `aliases` | other names the venue goes by in a paper's text — its acronym, its parent conference: `["AISec", "ACM CCS"]`. Collected along `extends`, with `name`. Read by `tex/venue-leftover` |
|
|
202
|
+
| `required_sections` | sections the venue requires, `[{ "title": "LLM Usage Statement", "position": "last" }]`: the exact title, and `last` when it must close the paper. Read by `tex/required-section`; a child's list replaces its parent's |
|
|
203
|
+
| `format` | page size, columns, fonts, font sizes, and `kinds` (page limits per kind of paper) |
|
|
204
|
+
| `tex` | TeX Live packages, each with the files that prove it is installed. Required unless `extends` is set |
|
|
205
|
+
| `rules` | rules the venue implies, rule id → severity or `[severity, options]` |
|
|
181
206
|
|
|
182
207
|
How a chain merges, from the root preset to the paper: `tex` is the union — a child never removes a
|
|
183
208
|
package; `format` keys are replaced one by one, and a child's kind replaces that kind whole;
|
package/docs/testing.md
CHANGED
|
@@ -40,6 +40,11 @@ When in doubt between unit and integration: if the function takes a port (`Files
|
|
|
40
40
|
- **Example:** `src/adapters/references/index.test.ts` — the reference checker over a fake
|
|
41
41
|
`fetch`: a cold run stores every answer, a warm run makes no request at all (the recorded list of
|
|
42
42
|
requests is empty), an edited entry asks only for itself.
|
|
43
|
+
- **A rule through real ESLint is integration, not e2e:** `src/rule-cases.test.ts` reads every rule
|
|
44
|
+
the registry holds (`rulePlugins`) and runs each through `paperlint lint --json` in-process on a
|
|
45
|
+
small paper tree — the CLI's own config, the `tex/latex` language, `loc`, message templating. Each
|
|
46
|
+
rule needs a case that reports (id, severity, line) and one that stays silent; a registered rule
|
|
47
|
+
without cases fails the test by name. A `_build/` file a rule reads is planted, not built.
|
|
43
48
|
|
|
44
49
|
## Harness — vigiles, the agent-facing surface only
|
|
45
50
|
|
|
@@ -56,14 +61,20 @@ When in doubt between unit and integration: if the function takes a port (`Files
|
|
|
56
61
|
- **Skills are tested through vigiles**, not a bespoke runner: a home-grown runner here once printed
|
|
57
62
|
byte-identical "clean" verdicts for three skills that had never loaded.
|
|
58
63
|
|
|
59
|
-
## End-to-end — `test/e2e
|
|
64
|
+
## End-to-end — `test/e2e/<area>/*.e2e.ts`
|
|
60
65
|
|
|
61
66
|
- **Answers:** does the package work once it is somewhere else? The tarball installed into another
|
|
62
67
|
tree, a real `pdflatex` build, the PDF's fonts measured.
|
|
63
68
|
- **May touch:** everything a user's machine has, except the network: a citation service is a
|
|
64
|
-
counting fake `fetch` even here (`test/e2e/build.ts`).
|
|
69
|
+
counting fake `fetch` even here (`test/e2e/tex/build.e2e.ts`).
|
|
65
70
|
- **Types:** the runs import the built modules from `dist/` (the shipped path is the point), so
|
|
66
71
|
their types are the declarations `tsc` emits — `npm run build` before type-checking them.
|
|
72
|
+
- **Where:** a directory per environment the tests need — `test/e2e/install/` (npm and pnpm),
|
|
73
|
+
`test/e2e/tex/` (TeX Live) — each a vitest project that finds `*.e2e.ts` by its glob. A new e2e
|
|
74
|
+
file needs no list anywhere. Helpers beside them are named without `.e2e.ts`.
|
|
75
|
+
- **A missing tool:** `missing(what, present)` from `test/e2e/need.ts` → `describe.skipIf`. Locally
|
|
76
|
+
the tests are reported skipped; with `CI` set they fail.
|
|
77
|
+
- **A build fixture:** a folder in `fixtures/build-e2e/` with its own `expect.json`; no test code.
|
|
67
78
|
- **What each run proves, and when a change owes one:** [`e2e.md`](e2e.md).
|
|
68
79
|
|
|
69
80
|
## Running
|
|
@@ -71,9 +82,11 @@ When in doubt between unit and integration: if the function takes a port (`Files
|
|
|
71
82
|
```bash
|
|
72
83
|
npx vitest run <file or folder> # unit and integration tests
|
|
73
84
|
npx vigiles test <file> # one harness
|
|
74
|
-
npm test #
|
|
85
|
+
npm test # unit and integration (vitest project `unit`), then every harness
|
|
75
86
|
npm run coverage # the same under c8, failing below the thresholds in .c8rc.json
|
|
76
|
-
npm run test:e2e #
|
|
87
|
+
npm run test:e2e # every end-to-end run
|
|
88
|
+
npm run test:e2e:install # the package packed and installed under npm and pnpm
|
|
89
|
+
npm run test:e2e:tex # real TeX Live: toolchain, builds of every fixture, banal
|
|
77
90
|
npm run check # every gate CI runs, and the list of what it does not reproduce
|
|
78
91
|
```
|
|
79
92
|
|
|
@@ -18,7 +18,7 @@ another"_.
|
|
|
18
18
|
> **Where this ended up (2026-09-24).** Method 5 below won, and it is no longer a CI script: it is
|
|
19
19
|
> `paperlint toolchain` (`src/toolchain.ts`), the command users run and the `build-e2e` job runs on Linux
|
|
20
20
|
> and macOS. The package list and the file contract (`REQUIRED_FILES` in the deleted
|
|
21
|
-
> `ensure-toolchain.sh`) moved into the venue profiles — each `
|
|
21
|
+
> `ensure-toolchain.sh`) moved into the venue profiles — each `presets/*.jsonc`
|
|
22
22
|
> declares a `tex` block of CTAN package → proof files, and `tex-base.jsonc` the set every paper
|
|
23
23
|
> gets. This file stays as the record of why tlmgr and not apt or a container; the scripts it names
|
|
24
24
|
> (`ensure-toolchain.sh`, `ci-install-texlive.sh`) were deleted in the same change.
|
package/docs/toolchain.md
CHANGED
|
@@ -58,7 +58,7 @@ that has every declared package, so a new-year install that was interrupted neve
|
|
|
58
58
|
complete older one.
|
|
59
59
|
|
|
60
60
|
**Where the package list lives:** in the venue profiles, not here and not in a script.
|
|
61
|
-
`
|
|
61
|
+
`presets/<venue>.jsonc` carries a `tex` block — CTAN package → the
|
|
62
62
|
files that prove it is installed — and `tex-base.jsonc` carries what every paper gets, and all a
|
|
63
63
|
paper with an unknown venue gets. The shape is `venue-profile.schema.json` in the same directory.
|
|
64
64
|
A venue that needs a new package gets one line there.
|
|
@@ -97,7 +97,7 @@ accepts as input. Measured on 50 PDFs / 598 pages (2026-09-25): banal on paperli
|
|
|
97
97
|
real `pdftohtml` agree on every field the facts file keeps, with no venue verdict changed. They
|
|
98
98
|
agree only because paperlint leaves out rotated and invisible text (as `pdftohtml` does), writes each
|
|
99
99
|
text's colour so banal drops light text by its own rule, and writes sizes and coordinates at the
|
|
100
|
-
zoom and precision banal expects. `test/e2e/banal.ts` checks each of those against the real banal
|
|
100
|
+
zoom and precision banal expects. `test/e2e/tex/banal.e2e.ts` checks each of those against the real banal
|
|
101
101
|
on the committed fixtures.
|
|
102
102
|
|
|
103
103
|
**How it is installed — and the licence boundary.** banal is **GPL-2.0-or-later**; paperlint is MIT. paperlint
|
|
@@ -16,7 +16,10 @@ import { readFileSync } from "node:fs";
|
|
|
16
16
|
import { dirname, join } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import { texLanguage } from "./latex-language.ts";
|
|
19
|
-
import typography, {
|
|
19
|
+
import typography, {
|
|
20
|
+
crefDefinitionRanges,
|
|
21
|
+
texVisibleRuns,
|
|
22
|
+
} from "./paper-typography.ts";
|
|
20
23
|
import bib from "./bib-reachable-entry.ts";
|
|
21
24
|
|
|
22
25
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -118,6 +121,50 @@ describe("paper/section-word", () => {
|
|
|
118
121
|
expect(ids(await lint(src), "paper/section-word")).toEqual([]);
|
|
119
122
|
});
|
|
120
123
|
|
|
124
|
+
it("🔴 a cleveref name definition is not prose: silent, and --fix leaves it as written", async () => {
|
|
125
|
+
const src =
|
|
126
|
+
"\\documentclass{article}\n\\usepackage{cleveref}\n\\crefname{section}{§}{§§}\n\\Crefname{section}{§}{§§}\n" +
|
|
127
|
+
"\\begin{document}\nText.\n\\end{document}\n";
|
|
128
|
+
expect(ids(await lint(src), "paper/section-word")).toEqual([]);
|
|
129
|
+
expect(await fixed(src)).toBe(src);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("a definition's range: its attached arguments, or the groups after a macro the parser did not sign", () => {
|
|
133
|
+
const signed = "\\crefname{section}{}{§§}";
|
|
134
|
+
expect(crefDefinitionRanges(signed)).toEqual([[0, signed.length]]);
|
|
135
|
+
const at = (s, e) => ({ start: { offset: s }, end: { offset: e } });
|
|
136
|
+
const tree = (macro, after) => ({ content: [macro, ...after] });
|
|
137
|
+
expect(
|
|
138
|
+
crefDefinitionRanges("", {
|
|
139
|
+
parse: () =>
|
|
140
|
+
tree({ type: "macro", content: "Crefname", position: at(0, 9) }, [
|
|
141
|
+
{ type: "group", content: [], position: at(9, 14) },
|
|
142
|
+
{ type: "string", content: "x", position: at(14, 15) },
|
|
143
|
+
]),
|
|
144
|
+
}),
|
|
145
|
+
).toEqual([[0, 14]]);
|
|
146
|
+
// A node with no position has no range; an argument with no content ends nothing.
|
|
147
|
+
expect(
|
|
148
|
+
crefDefinitionRanges("", {
|
|
149
|
+
parse: () =>
|
|
150
|
+
tree(
|
|
151
|
+
{
|
|
152
|
+
type: "macro",
|
|
153
|
+
content: "crefname",
|
|
154
|
+
args: [{ type: "argument" }],
|
|
155
|
+
},
|
|
156
|
+
[],
|
|
157
|
+
),
|
|
158
|
+
}),
|
|
159
|
+
).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("…while a § in prose next to such a definition is still reported", async () => {
|
|
163
|
+
const src =
|
|
164
|
+
"\\documentclass{article}\n\\crefname{section}{§}{§§}\n\\begin{document}\nSee §5.\n\\end{document}\n";
|
|
165
|
+
expect(ids(await lint(src), "paper/section-word")).toHaveLength(1);
|
|
166
|
+
});
|
|
167
|
+
|
|
121
168
|
it("markdown: `§5` → `Section 5`, code untouched", async () => {
|
|
122
169
|
const src = "See §5 and `§3` here.\n";
|
|
123
170
|
expect(ids(await lint(src, "paper.md"), "paper/section-word")).toHaveLength(
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
*/
|
|
62
62
|
import assert from "node:assert/strict";
|
|
63
63
|
import { getParser } from "@unified-latex/unified-latex-util-parse";
|
|
64
|
+
import { takeWhile } from "remeda";
|
|
64
65
|
import type { Nodes } from "mdast";
|
|
65
66
|
import type { TexArg, TexNode, TexRoot } from "./latex-language.ts";
|
|
66
67
|
import type {
|
|
@@ -121,7 +122,77 @@ function skippedRanges(
|
|
|
121
122
|
}
|
|
122
123
|
const bib = bibRange(raw);
|
|
123
124
|
if (bib) out.push([bib.start, bib.end]);
|
|
124
|
-
return out;
|
|
125
|
+
return [...out, ...crefDefinitionRanges(raw)];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* cleveref's name and format definitions (`\crefname{section}{§}{§§}`): the label a reference
|
|
130
|
+
* prints is set there, not written as prose, so a sign in them is not a sign in the text.
|
|
131
|
+
*/
|
|
132
|
+
const CREF_DEFINITIONS: ReadonlySet<string> = new Set([
|
|
133
|
+
"crefname",
|
|
134
|
+
"Crefname",
|
|
135
|
+
"crefformat",
|
|
136
|
+
"Crefformat",
|
|
137
|
+
"crefrangeformat",
|
|
138
|
+
"Crefrangeformat",
|
|
139
|
+
"crefmultiformat",
|
|
140
|
+
"Crefmultiformat",
|
|
141
|
+
"crefrangemultiformat",
|
|
142
|
+
"Crefrangemultiformat",
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
const isGroup = (n: TexNode): boolean => n.type === "group";
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A macro's range with its arguments: the ones the parser attached (cleveref's are signed) or the
|
|
149
|
+
* `{…}` groups standing right after it when it was given none — from the first to the last offset
|
|
150
|
+
* any of them carries. An argument's closing brace is one character past its last node.
|
|
151
|
+
*/
|
|
152
|
+
function withArguments(
|
|
153
|
+
m: TexNode & { readonly type: "macro" },
|
|
154
|
+
after: readonly TexNode[],
|
|
155
|
+
): readonly [number, number][] {
|
|
156
|
+
const offsets = [
|
|
157
|
+
m.position?.start.offset,
|
|
158
|
+
m.position?.end.offset,
|
|
159
|
+
...(m.args ?? []).map((a) => {
|
|
160
|
+
const last = a.content?.at(-1)?.position?.end.offset;
|
|
161
|
+
return last === undefined ? undefined : last + 1;
|
|
162
|
+
}),
|
|
163
|
+
takeWhile(after, isGroup).at(-1)?.position?.end.offset,
|
|
164
|
+
].filter((o): o is number => o !== undefined);
|
|
165
|
+
return offsets.length === 0
|
|
166
|
+
? []
|
|
167
|
+
: [[Math.min(...offsets), Math.max(...offsets)]];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The ranges of the cleveref definitions in the preamble and in the document body. `parse` is
|
|
172
|
+
* unified-latex's; a test hands in a tree with the fields the walk tolerates being absent.
|
|
173
|
+
*/
|
|
174
|
+
export function crefDefinitionRanges(
|
|
175
|
+
raw: string,
|
|
176
|
+
{
|
|
177
|
+
parse = (s: string): TexRoot => getParser().parse(s),
|
|
178
|
+
}: { readonly parse?: (s: string) => TexRoot } = {},
|
|
179
|
+
): readonly [number, number][] {
|
|
180
|
+
const root = parse(raw);
|
|
181
|
+
const doc = root.content.find(
|
|
182
|
+
(n): n is Extract<TexNode, { type: "environment" | "mathenv" }> =>
|
|
183
|
+
n.type === "environment" && n.env === "document",
|
|
184
|
+
);
|
|
185
|
+
const lists: readonly (readonly TexNode[])[] = [
|
|
186
|
+
root.content,
|
|
187
|
+
doc?.content ?? [],
|
|
188
|
+
];
|
|
189
|
+
return lists.flatMap((list) =>
|
|
190
|
+
list.flatMap((n, i) =>
|
|
191
|
+
n.type === "macro" && CREF_DEFINITIONS.has(n.content)
|
|
192
|
+
? withArguments(n, list.slice(i + 1))
|
|
193
|
+
: [],
|
|
194
|
+
),
|
|
195
|
+
);
|
|
125
196
|
}
|
|
126
197
|
const inside = (ranges: readonly [number, number][], i: number) =>
|
|
127
198
|
ranges.some(([s, e]) => i >= s && i < e);
|