paperlint 6.3.0 → 7.0.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/CONTRIBUTING.md +4 -2
- package/README.md +2 -2
- package/dist/adapters/latex/conditionals.d.ts +21 -0
- package/dist/adapters/latex/conditionals.d.ts.map +1 -0
- package/dist/adapters/latex/conditionals.js +75 -0
- package/dist/adapters/latex/conditionals.js.map +1 -0
- package/dist/adapters/latex/headings.d.ts +10 -0
- package/dist/adapters/latex/headings.d.ts.map +1 -0
- package/dist/adapters/latex/headings.js +129 -0
- package/dist/adapters/latex/headings.js.map +1 -0
- package/dist/adapters/latex/index.d.ts +1 -0
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +3 -0
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts +1 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +3 -2
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/build.d.ts +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +1 -1
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +17 -3
- package/dist/cli.js.map +1 -1
- package/dist/domain/heading-case.d.ts +100 -0
- package/dist/domain/heading-case.d.ts.map +1 -0
- package/dist/domain/heading-case.js +365 -0
- package/dist/domain/heading-case.js.map +1 -0
- package/dist/domain/tex-document.d.ts +32 -0
- package/dist/domain/tex-document.d.ts.map +1 -1
- package/dist/domain/tex-document.js +11 -0
- package/dist/domain/tex-document.js.map +1 -1
- package/dist/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +16 -3
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/facts-file.d.ts +3 -3
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +3 -3
- package/dist/facts-file.js.map +1 -1
- package/dist/folder-venue-rule.js +6 -6
- package/dist/folder-venue-rule.js.map +1 -1
- package/dist/heading-case.d.ts +48 -0
- package/dist/heading-case.d.ts.map +1 -0
- package/dist/heading-case.js +191 -0
- package/dist/heading-case.js.map +1 -0
- package/dist/ports/latex.d.ts +7 -1
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/preset-card-rule.d.ts +100 -0
- package/dist/preset-card-rule.d.ts.map +1 -0
- package/dist/preset-card-rule.js +221 -0
- package/dist/preset-card-rule.js.map +1 -0
- package/dist/presets.d.ts +19 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +97 -62
- package/dist/presets.js.map +1 -1
- package/dist/tex-requirements.d.ts +33 -3
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +27 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +1 -1
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js.map +1 -1
- package/docs/configuration.md +13 -1
- package/docs/optional-rules.md +4 -3
- package/docs/rules/format/page-limit.md +1 -1
- package/docs/rules/paper/folder-venue-leftover.md +1 -1
- package/docs/rules/talk/profile.md +1 -1
- package/docs/rules/tex/heading-case.md +424 -0
- package/docs/rules.md +62 -53
- package/eslint-rules/latex-language.test.mjs +12 -0
- package/eslint-rules/latex-language.ts +17 -4
- package/eslint-rules/paper-typography.test.mjs +12 -0
- package/eslint.config.mjs +59 -1
- package/fixtures/build-e2e/acmart/paper.tex +1 -1
- package/fixtures/build-e2e/fallback/expect.json +3 -3
- package/lib/skill-checks.mjs +31 -0
- package/package.json +7 -1
- package/presets/acm-sigconf.jsonc +5 -0
- package/presets/acm-sigconf.md +43 -0
- package/presets/agenticdev.jsonc +27 -6
- package/presets/agenticdev.md +151 -0
- package/presets/aidc.jsonc +6 -0
- package/presets/aidc.md +75 -0
- package/presets/aisec.jsonc +5 -1
- package/presets/aisec.md +117 -0
- package/presets/ieee-conference.jsonc +5 -0
- package/presets/ieee-conference.md +38 -0
- package/presets/realm.jsonc +7 -2
- package/presets/realm.md +166 -0
- package/presets/tex/agenticdev.tex +1 -1
- package/presets/tex-base.jsonc +6 -0
- package/presets/tex-base.md +30 -0
- package/presets/venue-profile.schema.json +40 -1
- package/skills/find-venue/SKILL.md +6 -5
- package/skills/find-venue/SKILL.md.spec.ts +5 -4
- package/skills/paper-pipeline/SKILL.md +5 -6
- package/skills/paper-pipeline/SKILL.md.spec.ts +4 -5
- package/skills/paper-pipeline/references/acl-venue-rules.md +1 -1
- package/skills/study-accepted-papers/SKILL.md +3 -3
- package/skills/study-accepted-papers/SKILL.md.spec.ts +2 -2
- package/skills/submit-paper/SKILL.md +26 -22
- package/skills/submit-paper/SKILL.md.spec.ts +25 -21
- package/skills/submit-paper/references/publishers/acm.md +1 -1
- package/src/adapters/latex/conditionals.ts +130 -0
- package/src/adapters/latex/headings.ts +163 -0
- package/src/adapters/latex/index.test.ts +65 -0
- package/src/adapters/latex/index.ts +3 -0
- package/src/adapters/latex/parse.ts +3 -2
- package/src/build.ts +2 -2
- package/src/cli.test.ts +50 -7
- package/src/cli.ts +21 -4
- package/src/domain/heading-case.test.ts +660 -0
- package/src/domain/heading-case.ts +538 -0
- package/src/domain/tex-document.ts +36 -0
- package/src/domain/venue-name.test.ts +13 -7
- package/src/facts-file.test.ts +4 -4
- package/src/facts-file.ts +6 -6
- package/src/folder-venue-rule.test.ts +10 -10
- package/src/folder-venue-rule.ts +6 -6
- package/src/heading-case.test.ts +500 -0
- package/src/heading-case.ts +275 -0
- package/src/init.test.ts +1 -1
- package/src/new-paper.test.ts +10 -6
- package/src/paper-settings-commands.test.ts +25 -10
- package/src/ports/latex.ts +7 -0
- package/src/preset-card-rule.test.ts +266 -0
- package/src/preset-card-rule.ts +384 -0
- package/src/presets.test.ts +193 -40
- package/src/presets.ts +139 -70
- package/src/rule-cases.test.ts +136 -50
- package/src/submission.test.ts +2 -1
- package/src/talk-rules.test.ts +6 -3
- package/src/tex-requirements.harness.ts +25 -11
- package/src/tex-requirements.test.ts +127 -1
- package/src/tex-requirements.ts +70 -8
- package/src/tex-venue-rules.test.ts +35 -21
- package/src/tex-venue-rules.ts +2 -1
- package/src/venue-rules.test.ts +7 -6
- package/templates/paper/paper.tex +1 -1
- package/skills/submit-paper/references/venues/agenticdev.md +0 -171
- package/skills/submit-paper/references/venues/aisec.md +0 -105
- package/skills/submit-paper/references/venues/realm.md +0 -155
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE RULE THAT JUDGES THE CASE OF TITLES — `tex/heading-case`, over the paper's `\title` and the
|
|
3
|
+
* titles of `\section`, `\subsection`, `\subsubsection`, `\paragraph` and `\subparagraph`
|
|
4
|
+
* (starred or not, and each optional short title).
|
|
5
|
+
*
|
|
6
|
+
* tex/heading-case off unless turned on every word of a title is in the case its scope asks:
|
|
7
|
+
* { title, headings, levels? }, each a CaseStyle
|
|
8
|
+
*
|
|
9
|
+
* ── WHY IT IS AN OPTION AND NOT A DEFAULT ────────────────────────────────────────
|
|
10
|
+
* Which style a venue asks is the venue's, and venues name different authorities: Conference
|
|
11
|
+
* Publishing (AgenticDev's proceedings) quotes the Chicago Manual of Style, ACL links APA title case,
|
|
12
|
+
* IEEE's manual has its own list. So the rule is optional, like `pdf/last-page-balance`: a venue
|
|
13
|
+
* preset whose author kit was read turns it on in its `rules` — today only `presets/agenticdev.jsonc`
|
|
14
|
+
* — and a paper may turn it on in its own `paperlint.json`.
|
|
15
|
+
*
|
|
16
|
+
* ── THE OPTIONS ARE PARSED ONCE, WHEN ESLINT LOADS THE RULE ──────────────────────
|
|
17
|
+
* ESLint checks the SHAPE against `meta.schema`: `title` and `headings` required, no other key, no
|
|
18
|
+
* unknown level. The VALUES are judged by `parseOptions`, because ajv's enum message («should be
|
|
19
|
+
* equal to one of the allowed values») names none of them: a misspelt style, the retired words
|
|
20
|
+
* `headline` and `off`, and `any` for both scopes each throw from `create`, which ESLint reports as
|
|
21
|
+
* «Error while loading rule» and stops the run. The judge then gets `HeadingCaseOptions` and never
|
|
22
|
+
* looks at the raw object again. A bare severity (`"error"`, no options) passes ESLint's schema —
|
|
23
|
+
* it validates the options it is given, not their count — so it is reported once, at the top of the
|
|
24
|
+
* file, rather than passing as a rule that checked nothing.
|
|
25
|
+
*
|
|
26
|
+
* ── ONE FILE AT A TIME ───────────────────────────────────────────────────────────
|
|
27
|
+
* The rule reads the file ESLint hands it, like `paper/section-word`: it runs on `paper.tex` and on
|
|
28
|
+
* every file `paper.tex` includes from its body (the `**` block of `src/cli.ts`), so a heading in
|
|
29
|
+
* `sections/results.tex` is reported at that file and line, and `--fix` edits that file. The
|
|
30
|
+
* preamble is read too: that is where `\title` stands.
|
|
31
|
+
*
|
|
32
|
+
* What a word is asked is decided in `src/domain/heading-case.ts`.
|
|
33
|
+
*/
|
|
34
|
+
import {
|
|
35
|
+
CASE_STYLES,
|
|
36
|
+
HEADING_LEVELS,
|
|
37
|
+
judgeHeadingCase,
|
|
38
|
+
type CaseFinding,
|
|
39
|
+
type CaseStyle,
|
|
40
|
+
type HeadingCaseOptions,
|
|
41
|
+
} from "./domain/heading-case.ts";
|
|
42
|
+
import { fieldOf, isRecord } from "./domain/record.ts";
|
|
43
|
+
import type { HeadingLevel } from "./domain/tex-document.ts";
|
|
44
|
+
import type { LatexReader } from "./ports/latex.ts";
|
|
45
|
+
import { ruleDocsUrl } from "./tex-venue-rules.ts";
|
|
46
|
+
|
|
47
|
+
/** What the rule reads with: the LaTeX reader. */
|
|
48
|
+
export interface HeadingCaseDeps {
|
|
49
|
+
readonly latex: LatexReader;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type Loc = { readonly line: number; readonly column: number };
|
|
53
|
+
|
|
54
|
+
/** The slice of ESLint's rule context this rule uses. `raw` exists on the `.tex` language's. */
|
|
55
|
+
export interface HeadingCaseContext {
|
|
56
|
+
readonly options: readonly unknown[];
|
|
57
|
+
readonly sourceCode: {
|
|
58
|
+
readonly text: string;
|
|
59
|
+
readonly raw?: string;
|
|
60
|
+
getLocFromIndex(index: number): Loc;
|
|
61
|
+
};
|
|
62
|
+
report(d: {
|
|
63
|
+
readonly loc: { readonly start: Loc; readonly end: Loc };
|
|
64
|
+
readonly messageId: string;
|
|
65
|
+
readonly data?: Readonly<Record<string, string>>;
|
|
66
|
+
readonly fix?: (fixer: {
|
|
67
|
+
replaceTextRange(range: readonly [number, number], text: string): unknown;
|
|
68
|
+
}) => unknown;
|
|
69
|
+
}): void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface HeadingCaseRule {
|
|
73
|
+
readonly meta: {
|
|
74
|
+
readonly type: "suggestion";
|
|
75
|
+
readonly fixable: "code";
|
|
76
|
+
readonly docs: { readonly description: string; readonly url: string };
|
|
77
|
+
readonly schema: readonly object[];
|
|
78
|
+
readonly messages: Readonly<Record<string, string>>;
|
|
79
|
+
};
|
|
80
|
+
create(context: HeadingCaseContext): { root?: () => void };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const DOCS_URL = ruleDocsUrl("heading-case");
|
|
84
|
+
|
|
85
|
+
/** The rule's options as ESLint gave them: a configuration, none at all, or one it refuses. */
|
|
86
|
+
type Asked =
|
|
87
|
+
| { readonly kind: "options"; readonly options: HeadingCaseOptions }
|
|
88
|
+
| { readonly kind: "none" }
|
|
89
|
+
| { readonly kind: "refused"; readonly reason: string };
|
|
90
|
+
|
|
91
|
+
const refused = (reason: string): Asked => ({
|
|
92
|
+
kind: "refused",
|
|
93
|
+
reason: `${reason} — ${DOCS_URL}`,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const quoted = (xs: readonly string[]): string =>
|
|
97
|
+
xs.map((x) => JSON.stringify(x)).join(", ");
|
|
98
|
+
|
|
99
|
+
/** One style value, or why it is none; `key` is how the message names where it stands. */
|
|
100
|
+
function styleOf(
|
|
101
|
+
key: string,
|
|
102
|
+
v: unknown,
|
|
103
|
+
): CaseStyle | { readonly why: string } {
|
|
104
|
+
const style = CASE_STYLES.find((s) => s === v);
|
|
105
|
+
if (style !== undefined) return style;
|
|
106
|
+
if (v === "headline")
|
|
107
|
+
return {
|
|
108
|
+
why: `"${key}" is "headline", which names no single style: write "chicago-headline" for the Chicago Manual of Style headline style. Other headline variants (APA title case, IEEE, Springer) are not implemented yet, and they disagree with Chicago on words such as With, Between and From`,
|
|
109
|
+
};
|
|
110
|
+
if (v === "off")
|
|
111
|
+
return {
|
|
112
|
+
why: `"${key}" is "off", which is not a style: write "any" to leave this scope unconstrained, or turn the whole rule off with "tex/heading-case": "off"`,
|
|
113
|
+
};
|
|
114
|
+
const shown = v === undefined ? "missing" : JSON.stringify(v);
|
|
115
|
+
return {
|
|
116
|
+
why: `"${key}" is ${shown}, which is not a style: one of ${quoted(CASE_STYLES)}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type Levels = Readonly<Partial<Record<HeadingLevel, CaseStyle>>>;
|
|
121
|
+
|
|
122
|
+
/** The `levels` object: the levels it names, each with its style, or why it is refused. */
|
|
123
|
+
function levelsOf(v: unknown): Levels | { readonly why: string } {
|
|
124
|
+
if (v === undefined) return {};
|
|
125
|
+
if (!isRecord(v))
|
|
126
|
+
return {
|
|
127
|
+
why: `"levels" is ${JSON.stringify(v)}, not an object such as {"paragraph": "sentence"}`,
|
|
128
|
+
};
|
|
129
|
+
return HEADING_LEVELS.reduce<Levels | { readonly why: string }>(
|
|
130
|
+
(acc, level) => {
|
|
131
|
+
if ("why" in acc || !(level in v)) return acc;
|
|
132
|
+
const style = styleOf(`levels.${level}`, v[level]);
|
|
133
|
+
return typeof style === "string" ? { ...acc, [level]: style } : style;
|
|
134
|
+
},
|
|
135
|
+
{},
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Whether every scope is `any`: the rule turned off, said in a second way. */
|
|
140
|
+
const asksNothing = (
|
|
141
|
+
o: HeadingCaseOptions & { readonly levels: Levels },
|
|
142
|
+
): boolean =>
|
|
143
|
+
o.title === "any" &&
|
|
144
|
+
o.headings === "any" &&
|
|
145
|
+
Object.values(o.levels).every((s) => s === "any");
|
|
146
|
+
|
|
147
|
+
/** ESLint's options for the rule → what it is asked, parsed once. */
|
|
148
|
+
function parseOptions(options: readonly unknown[]): Asked {
|
|
149
|
+
const [raw] = options;
|
|
150
|
+
if (raw === undefined) return { kind: "none" };
|
|
151
|
+
if (!isRecord(raw))
|
|
152
|
+
return refused(
|
|
153
|
+
`the options are ${JSON.stringify(raw)}, not an object such as {"title": "chicago-headline", "headings": "chicago-headline"}`,
|
|
154
|
+
);
|
|
155
|
+
const title = styleOf("title", fieldOf(raw, "title"));
|
|
156
|
+
if (typeof title !== "string") return refused(title.why);
|
|
157
|
+
const headings = styleOf("headings", fieldOf(raw, "headings"));
|
|
158
|
+
if (typeof headings !== "string") return refused(headings.why);
|
|
159
|
+
const levels = levelsOf(fieldOf(raw, "levels"));
|
|
160
|
+
if ("why" in levels) return refused(levels.why);
|
|
161
|
+
const parsed = { title, headings, levels };
|
|
162
|
+
return asksNothing(parsed)
|
|
163
|
+
? refused(
|
|
164
|
+
`"title" and "headings" are both "any" and no level asks for more: that is the rule turned off — remove it from "rules", or set it to "off"`,
|
|
165
|
+
)
|
|
166
|
+
: { kind: "options", options: parsed };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const MESSAGES: Readonly<Record<string, string>> = {
|
|
170
|
+
capitalize:
|
|
171
|
+
"{{where}}: capitalize «{{word}}» → «{{expected}}» — {{why}} (chicago-headline)",
|
|
172
|
+
lowercase:
|
|
173
|
+
"{{where}}: lowercase «{{word}}» → «{{expected}}» — {{why}} (chicago-headline)",
|
|
174
|
+
sentenceFirst:
|
|
175
|
+
"{{where}}: capitalize the first word, «{{word}}» → «{{expected}}» (sentence; not fixed: a product name written in lowercase looks the same)",
|
|
176
|
+
noOptions: `tex/heading-case is on with no options, so it judged nothing: give it the styles the venue asks, e.g. ["error", {"title": "chicago-headline", "headings": "chicago-headline"}] (styles: ${quoted(CASE_STYLES)})`,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** One finding reported where its word stands, with the fix when the judge is sure of it. */
|
|
180
|
+
function report(context: HeadingCaseContext, f: CaseFinding): void {
|
|
181
|
+
const sc = context.sourceCode;
|
|
182
|
+
const { fix } = f;
|
|
183
|
+
context.report({
|
|
184
|
+
loc: {
|
|
185
|
+
start: sc.getLocFromIndex(f.at.start),
|
|
186
|
+
end: sc.getLocFromIndex(f.at.end),
|
|
187
|
+
},
|
|
188
|
+
messageId: f.messageId,
|
|
189
|
+
data: f.data,
|
|
190
|
+
...(fix === null
|
|
191
|
+
? {}
|
|
192
|
+
: {
|
|
193
|
+
fix: (fixer) =>
|
|
194
|
+
fixer.replaceTextRange([fix.span.start, fix.span.end], fix.text),
|
|
195
|
+
}),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* A style value, as the schema checks it: a string. Which strings are styles is `parseOptions`'s to
|
|
201
|
+
* say, so that the refusal can name them (see the header).
|
|
202
|
+
*/
|
|
203
|
+
const STYLE = { type: "string" } as const;
|
|
204
|
+
|
|
205
|
+
const META: HeadingCaseRule["meta"] = {
|
|
206
|
+
type: "suggestion",
|
|
207
|
+
fixable: "code",
|
|
208
|
+
docs: {
|
|
209
|
+
description:
|
|
210
|
+
"the words of the paper's title and of its headings are in the capitalization the venue asks: chicago-headline or sentence",
|
|
211
|
+
url: DOCS_URL,
|
|
212
|
+
},
|
|
213
|
+
schema: [
|
|
214
|
+
{
|
|
215
|
+
type: "object",
|
|
216
|
+
properties: {
|
|
217
|
+
title: STYLE,
|
|
218
|
+
headings: STYLE,
|
|
219
|
+
levels: {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: Object.fromEntries(
|
|
222
|
+
HEADING_LEVELS.map((level) => [level, STYLE]),
|
|
223
|
+
),
|
|
224
|
+
additionalProperties: false,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
required: ["title", "headings"],
|
|
228
|
+
additionalProperties: false,
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
messages: MESSAGES,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/** The rule turned on with no options: said once, at the top of the file. */
|
|
235
|
+
function reportNoOptions(context: HeadingCaseContext): void {
|
|
236
|
+
const start = context.sourceCode.getLocFromIndex(0);
|
|
237
|
+
context.report({ loc: { start, end: start }, messageId: "noOptions" });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** One file against the options the rule was given. */
|
|
241
|
+
function judgeFile(
|
|
242
|
+
context: HeadingCaseContext,
|
|
243
|
+
deps: HeadingCaseDeps,
|
|
244
|
+
options: HeadingCaseOptions,
|
|
245
|
+
): void {
|
|
246
|
+
const sc = context.sourceCode;
|
|
247
|
+
const src = sc.raw ?? sc.text;
|
|
248
|
+
judgeHeadingCase(deps.latex.headings(src), options, src).forEach((f) => {
|
|
249
|
+
report(context, f);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The rule: its options are parsed when ESLint loads it, and a refused one stops the run. */
|
|
254
|
+
export const headingCaseRule = (deps: HeadingCaseDeps): HeadingCaseRule => ({
|
|
255
|
+
meta: META,
|
|
256
|
+
create: (context) => {
|
|
257
|
+
const asked = parseOptions(context.options);
|
|
258
|
+
switch (asked.kind) {
|
|
259
|
+
case "refused":
|
|
260
|
+
throw new Error(asked.reason);
|
|
261
|
+
case "none":
|
|
262
|
+
return {
|
|
263
|
+
root: () => {
|
|
264
|
+
reportNoOptions(context);
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
case "options":
|
|
268
|
+
return {
|
|
269
|
+
root: () => {
|
|
270
|
+
judgeFile(context, deps, asked.options);
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
});
|
package/src/init.test.ts
CHANGED
|
@@ -532,7 +532,7 @@ describe("paperlint init — a first paper named after a venue", () => {
|
|
|
532
532
|
expect(made).toEqual([]);
|
|
533
533
|
expect(asked.filter((q) => /format/.test(q))).toEqual([]);
|
|
534
534
|
expect(r.out[r.out.indexOf("first paper") + 1]).toBe(
|
|
535
|
-
' ✗ no paper created — `aisec-2026` — the paper folder names a venue ("
|
|
535
|
+
' ✗ no paper created — `aisec-2026` — the paper folder names a venue ("AISec"); venues change on resubmission, so name it after the work\n if the venue really belongs in the name, rerun `paperlint new` with --allow-venue-name',
|
|
536
536
|
);
|
|
537
537
|
});
|
|
538
538
|
});
|
package/src/new-paper.test.ts
CHANGED
|
@@ -175,6 +175,7 @@ async function newIn(
|
|
|
175
175
|
|
|
176
176
|
describe("paperlint new --venue / --kind", () => {
|
|
177
177
|
const PRESET = `{
|
|
178
|
+
"type": "venue", "name": "my-workshop", "url": "https://example.org/cfp",
|
|
178
179
|
"extends": "paperlint:acm-sigconf",
|
|
179
180
|
"format": { "kinds": { "short": { "body_pages_max": 4 } } },
|
|
180
181
|
}
|
|
@@ -205,6 +206,7 @@ describe("paperlint new --venue / --kind", () => {
|
|
|
205
206
|
for (const template of [undefined, "\\documentclass[a]{}"]) {
|
|
206
207
|
const r = await newIn(["--venue", "./venues/bare.jsonc"], {
|
|
207
208
|
"venues/bare.jsonc": JSON.stringify({
|
|
209
|
+
type: "family",
|
|
208
210
|
...(template === undefined ? {} : { template }),
|
|
209
211
|
tex: { packages: { x: ["x.sty"] } },
|
|
210
212
|
}),
|
|
@@ -230,7 +232,9 @@ describe("paperlint new --venue / --kind", () => {
|
|
|
230
232
|
expect(r.out).not.toMatch(/pdf\/profile/);
|
|
231
233
|
expect(r.code).toBe(0);
|
|
232
234
|
});
|
|
235
|
+
});
|
|
233
236
|
|
|
237
|
+
describe("paperlint new --venue — a venue it cannot use", () => {
|
|
234
238
|
it("an unknown venue exits 2, lists the shipped presets, and creates nothing", async () => {
|
|
235
239
|
const r = await newIn(["--venue", "icse"]);
|
|
236
240
|
expect(r.code).toBe(2);
|
|
@@ -246,7 +250,7 @@ describe("paperlint new --venue / --kind — refusals and defaults", () => {
|
|
|
246
250
|
[
|
|
247
251
|
"a kind the preset lacks",
|
|
248
252
|
["--venue", "agenticdev", "--kind", "long"],
|
|
249
|
-
/`
|
|
253
|
+
/`AgenticDev` has no kind `long`; its kinds: short, full, demo/,
|
|
250
254
|
],
|
|
251
255
|
["a kind without a venue", ["--kind", "short"], /--kind needs --venue/],
|
|
252
256
|
[
|
|
@@ -270,7 +274,7 @@ describe("paperlint new --venue / --kind — refusals and defaults", () => {
|
|
|
270
274
|
const r = await newIn(["--venue", "agenticdev"]);
|
|
271
275
|
expect(r.settings()).toEqual({ extends: "paperlint:agenticdev" });
|
|
272
276
|
expect(r.out).toMatch(
|
|
273
|
-
/kind: not set — `
|
|
277
|
+
/kind: not set — `AgenticDev` sets a page limit per kind/,
|
|
274
278
|
);
|
|
275
279
|
expect(r.out).toMatch(/pdf\/profile/);
|
|
276
280
|
});
|
|
@@ -298,19 +302,19 @@ describe("paperlint new — a name that names a venue is refused", () => {
|
|
|
298
302
|
"a venue's label, with the work's name left to suggest",
|
|
299
303
|
"aisec-agent-drift",
|
|
300
304
|
[],
|
|
301
|
-
'`aisec-agent-drift` — the paper folder names a venue ("
|
|
305
|
+
'`aisec-agent-drift` — the paper folder names a venue ("AISec"); venues change on resubmission, so name it after the work (e.g. "agent-drift")',
|
|
302
306
|
],
|
|
303
307
|
[
|
|
304
308
|
"nothing but a year left: no example",
|
|
305
309
|
"aisec-2026",
|
|
306
310
|
[],
|
|
307
|
-
'`aisec-2026` — the paper folder names a venue ("
|
|
311
|
+
'`aisec-2026` — the paper folder names a venue ("AISec"); venues change on resubmission, so name it after the work',
|
|
308
312
|
],
|
|
309
313
|
[
|
|
310
314
|
"its own venue too: right today, stale on the next resubmission",
|
|
311
315
|
"aidc-2026",
|
|
312
316
|
["--venue", "aidc"],
|
|
313
|
-
'`aidc-2026` — the paper folder names a venue ("
|
|
317
|
+
'`aidc-2026` — the paper folder names a venue ("AIDC"); venues change on resubmission, so name it after the work',
|
|
314
318
|
],
|
|
315
319
|
[
|
|
316
320
|
"by an alias, glued to a year",
|
|
@@ -322,7 +326,7 @@ describe("paperlint new — a name that names a venue is refused", () => {
|
|
|
322
326
|
"two venues",
|
|
323
327
|
"realm-aisec-rules",
|
|
324
328
|
[],
|
|
325
|
-
'`realm-aisec-rules` — the paper folder names a venue ("
|
|
329
|
+
'`realm-aisec-rules` — the paper folder names a venue ("AISec", "REALM"); venues change on resubmission, so name it after the work (e.g. "rules")',
|
|
326
330
|
],
|
|
327
331
|
])("%s: exit 2, nothing written", async (_, name, args, reason) => {
|
|
328
332
|
const r = await newIn(args, {}, name);
|
|
@@ -14,6 +14,7 @@ import { join } from "node:path";
|
|
|
14
14
|
import { afterEach, describe, expect, it } from "vitest";
|
|
15
15
|
import { run, toolchainTex } from "./cli.ts";
|
|
16
16
|
import { lintReport } from "../test/lint-report.ts";
|
|
17
|
+
import { venuePreset } from "../test/support.ts";
|
|
17
18
|
|
|
18
19
|
const dirs: string[] = [];
|
|
19
20
|
afterEach(() => {
|
|
@@ -203,7 +204,7 @@ describe("papersDir — optional, `papers` by default", () => {
|
|
|
203
204
|
});
|
|
204
205
|
|
|
205
206
|
describe("paperlint lint — the venue preset's rules", () => {
|
|
206
|
-
it("agenticdev's preset turns pdf/last-page-balance on for its paper alone", async () => {
|
|
207
|
+
it("agenticdev's preset turns pdf/last-page-balance and tex/heading-case on for its paper alone", async () => {
|
|
207
208
|
const root = project({
|
|
208
209
|
"papers/a/paperlint.json": JSON.stringify({
|
|
209
210
|
extends: "paperlint:agenticdev",
|
|
@@ -219,7 +220,13 @@ describe("paperlint lint — the venue preset's rules", () => {
|
|
|
219
220
|
// Scoped to the paper by `basePath` alone: which of its files the rule reaches is decided
|
|
220
221
|
// once, by narrowing to paperlint's owned scopes (#101), not by a copy of their globs.
|
|
221
222
|
basePath: join(root, "papers/a"),
|
|
222
|
-
rules: {
|
|
223
|
+
rules: {
|
|
224
|
+
"pdf/last-page-balance": ["error", { tolerancePt: 120 }],
|
|
225
|
+
"tex/heading-case": [
|
|
226
|
+
"error",
|
|
227
|
+
{ title: "chicago-headline", headings: "chicago-headline" },
|
|
228
|
+
],
|
|
229
|
+
},
|
|
223
230
|
},
|
|
224
231
|
],
|
|
225
232
|
own: [],
|
|
@@ -256,6 +263,10 @@ describe("paperlint lint — the paper over its preset, and the project's own pr
|
|
|
256
263
|
// The preset's block comes first and the paper's own after it: the later block wins.
|
|
257
264
|
expect(blocks.ok && blocks.value.preset[0]?.rules).toEqual({
|
|
258
265
|
"pdf/last-page-balance": ["error", { tolerancePt: 120 }],
|
|
266
|
+
"tex/heading-case": [
|
|
267
|
+
"error",
|
|
268
|
+
{ title: "chicago-headline", headings: "chicago-headline" },
|
|
269
|
+
],
|
|
259
270
|
});
|
|
260
271
|
expect(blocks.ok && blocks.value.own[0]?.rules).toEqual({
|
|
261
272
|
"pdf/last-page-balance": "off",
|
|
@@ -264,10 +275,12 @@ describe("paperlint lint — the paper over its preset, and the project's own pr
|
|
|
264
275
|
|
|
265
276
|
it("a project's own preset, by relative path: its rules apply, an unknown rule id is refused", async () => {
|
|
266
277
|
const root = project({
|
|
267
|
-
"venues/usenix-sec.jsonc": JSON.stringify(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
278
|
+
"venues/usenix-sec.jsonc": JSON.stringify(
|
|
279
|
+
venuePreset("usenix-sec", {
|
|
280
|
+
extends: "paperlint:acm-sigconf",
|
|
281
|
+
rules: { "pdf/no-such-rule": "error" },
|
|
282
|
+
}),
|
|
283
|
+
),
|
|
271
284
|
"papers/a/paperlint.json": JSON.stringify({
|
|
272
285
|
extends: "../../venues/usenix-sec.jsonc",
|
|
273
286
|
}),
|
|
@@ -294,10 +307,12 @@ describe("paperlint lint — the paper over its preset, and the project's own pr
|
|
|
294
307
|
describe("paperlint toolchain — installs what the project's own presets need", () => {
|
|
295
308
|
it("the shipped union plus a relative preset's tex packages", () => {
|
|
296
309
|
const root = project({
|
|
297
|
-
"venues/usenix-sec.jsonc": JSON.stringify(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
310
|
+
"venues/usenix-sec.jsonc": JSON.stringify(
|
|
311
|
+
venuePreset("usenix-sec", {
|
|
312
|
+
extends: "paperlint:acm-sigconf",
|
|
313
|
+
tex: { packages: { usenix: ["usenix.sty"] } },
|
|
314
|
+
}),
|
|
315
|
+
),
|
|
301
316
|
"papers/a/paperlint.json": JSON.stringify({
|
|
302
317
|
extends: "../../venues/usenix-sec.jsonc",
|
|
303
318
|
}),
|
package/src/ports/latex.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
Passage,
|
|
13
13
|
Span,
|
|
14
14
|
TextRun,
|
|
15
|
+
TitledHeading,
|
|
15
16
|
} from "../domain/tex-document.ts";
|
|
16
17
|
|
|
17
18
|
export interface LatexReader {
|
|
@@ -19,6 +20,12 @@ export interface LatexReader {
|
|
|
19
20
|
readonly documentClass: (src: string) => ClassLine;
|
|
20
21
|
/** Its sections, where the back matter begins, and where the document ends. */
|
|
21
22
|
readonly outline: (src: string) => Outline;
|
|
23
|
+
/**
|
|
24
|
+
* Its `\title` and the titles of its `\section`, `\subsection`, `\subsubsection`, `\paragraph`
|
|
25
|
+
* and `\subparagraph` (starred or not, and each optional short title), each as the text the case
|
|
26
|
+
* judge reads: in document order.
|
|
27
|
+
*/
|
|
28
|
+
readonly headings: (src: string) => readonly TitledHeading[];
|
|
22
29
|
/** The text a reader sees: prose, the author block, footnotes — not comments, keys, math, code. */
|
|
23
30
|
readonly renderedRuns: (src: string) => readonly TextRun[];
|
|
24
31
|
/**
|