paperlint 7.0.0 โ 7.1.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/README.md +18 -11
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/domain/body-pages.d.ts +29 -0
- package/dist/domain/body-pages.d.ts.map +1 -1
- package/dist/domain/body-pages.js +25 -0
- package/dist/domain/body-pages.js.map +1 -1
- package/dist/paper-includes.d.ts +3 -0
- package/dist/paper-includes.d.ts.map +1 -1
- package/dist/paper-includes.js +17 -2
- package/dist/paper-includes.js.map +1 -1
- package/dist/presets.d.ts +10 -3
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +8 -7
- package/dist/presets.js.map +1 -1
- package/dist/submission.d.ts +8 -0
- package/dist/submission.d.ts.map +1 -1
- package/dist/submission.js +19 -7
- package/dist/submission.js.map +1 -1
- package/dist/tex-requirements.d.ts +9 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +7 -3
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +5 -2
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +29 -10
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/venue-rules.d.ts +2 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +55 -7
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +1 -1
- package/docs/e2e.md +14 -12
- package/docs/rules/format/page-limit.md +18 -1
- package/docs/rules/tex/template.md +25 -5
- package/docs/rules/tex/venue-leftover.md +13 -0
- package/docs/rules.md +63 -55
- package/docs/submission.md +5 -1
- package/fixtures/build-e2e/msr/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/msr/expect.json +18 -0
- package/fixtures/build-e2e/msr/paper.tex +49 -0
- package/fixtures/build-e2e/msr/paperlint.json +1 -0
- package/fixtures/build-e2e/msr-appendix-after/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/msr-appendix-after/expect.json +20 -0
- package/fixtures/build-e2e/msr-appendix-after/paper.tex +48 -0
- package/fixtures/build-e2e/msr-appendix-after/paperlint.json +1 -0
- package/package.json +1 -1
- package/presets/ieee-conference.md +1 -1
- package/presets/msr.jsonc +71 -0
- package/presets/msr.md +94 -0
- package/presets/venue-profile.schema.json +25 -0
- package/skills/camera-ready/SKILL.md +8 -1
- package/skills/camera-ready/SKILL.md.spec.ts +7 -0
- package/skills/submission-portal/SKILL.md +2 -2
- package/skills/submission-portal/SKILL.md.spec.ts +1 -1
- package/skills/submit-paper/SKILL.md +9 -1
- package/skills/submit-paper/SKILL.md.spec.ts +8 -0
- package/src/cli.ts +7 -2
- package/src/domain/body-pages.test.ts +144 -0
- package/src/domain/body-pages.ts +61 -1
- package/src/new-paper.test.ts +27 -0
- package/src/paper-includes.test.ts +26 -0
- package/src/paper-includes.ts +20 -2
- package/src/paper-settings-commands.test.ts +1 -1
- package/src/preset-card-rule.test.ts +1 -0
- package/src/presets.test.ts +66 -1
- package/src/presets.ts +23 -6
- package/src/submission.test.ts +42 -3
- package/src/submission.ts +28 -7
- package/src/tex-requirements.ts +21 -4
- package/src/tex-venue-rules.test.ts +161 -0
- package/src/tex-venue-rules.ts +45 -15
- package/src/venue-rules.test.ts +173 -2
- package/src/venue-rules.ts +71 -10
package/src/presets.test.ts
CHANGED
|
@@ -63,6 +63,7 @@ describe("shipped presets", () => {
|
|
|
63
63
|
expect(r.value.template).toEqual({
|
|
64
64
|
text: "\\documentclass[sigconf]{acmart}",
|
|
65
65
|
file: join(VENUES, "acm-sigconf.jsonc"),
|
|
66
|
+
forbids: [],
|
|
66
67
|
});
|
|
67
68
|
expect("acmart" in r.value.tex.packages).toBe(true);
|
|
68
69
|
// The label is the venue's own name, from its file's `name`; the id stays the file name โ the
|
|
@@ -75,7 +76,40 @@ describe("shipped presets", () => {
|
|
|
75
76
|
url: "https://conf.researchr.org/home/ase-2026/agenticdev-2026",
|
|
76
77
|
});
|
|
77
78
|
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("shipped presets: paperlint:msr", () => {
|
|
82
|
+
it("paperlint:msr โ msr over ieee-conference: one kind, appendices counted as body, compsoc forbidden", () => {
|
|
83
|
+
const r = resolve("paperlint:msr");
|
|
84
|
+
expect(r.ok).toBe(true);
|
|
85
|
+
if (!r.ok) return;
|
|
86
|
+
expect(r.value.chain.map((f) => f.split("/").pop())).toEqual([
|
|
87
|
+
"ieee-conference.jsonc",
|
|
88
|
+
"msr.jsonc",
|
|
89
|
+
]);
|
|
90
|
+
expect([...r.value.format.kinds]).toEqual([
|
|
91
|
+
["technical", { bodyPagesMax: 10, refPagesMax: 2 }],
|
|
92
|
+
]);
|
|
93
|
+
expect(r.value.format.bodyEndsAt).toBe("references");
|
|
94
|
+
expect(r.value.format.appendixInBody).toBe(true);
|
|
95
|
+
expect(r.value.format.pageWidthIn).toBe(8.5);
|
|
96
|
+
expect(r.value.template).toEqual({
|
|
97
|
+
text: "\\documentclass[10pt,conference]{IEEEtran}",
|
|
98
|
+
file: join(VENUES, "msr.jsonc"),
|
|
99
|
+
forbids: ["compsoc", "compsocconf"],
|
|
100
|
+
});
|
|
101
|
+
expect(r.value.blind).toBe(true);
|
|
102
|
+
expect(r.value.portal).toEqual({
|
|
103
|
+
kind: "hotcrp",
|
|
104
|
+
url: "https://msr2027.hotcrp.com",
|
|
105
|
+
});
|
|
106
|
+
// No rule turned on: the IEEE capitalization rule has not been read and quoted.
|
|
107
|
+
expect(r.value.rules).toEqual({});
|
|
108
|
+
expect(r.value.registerAnchors).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
78
111
|
|
|
112
|
+
describe("shipped presets: a family, and a venue that stands alone", () => {
|
|
79
113
|
it("a paper may extend a family directly: format and fonts, and no kinds", () => {
|
|
80
114
|
const r = resolve("paperlint:acm-sigconf");
|
|
81
115
|
expect(r.ok && r.value.format.kinds.size).toBe(0);
|
|
@@ -139,6 +173,11 @@ describe("what each shipped preset declares itself to be", () => {
|
|
|
139
173
|
url: "https://aisec.cc/",
|
|
140
174
|
},
|
|
141
175
|
"ieee-conference.jsonc": { type: "family" },
|
|
176
|
+
"msr.jsonc": {
|
|
177
|
+
type: "venue",
|
|
178
|
+
name: "MSR",
|
|
179
|
+
url: "https://2027.msrconf.org/track/msr-2027-technical-papers",
|
|
180
|
+
},
|
|
142
181
|
"realm.jsonc": {
|
|
143
182
|
type: "venue",
|
|
144
183
|
name: "REALM",
|
|
@@ -300,7 +339,7 @@ describe("what does not resolve, and says why", () => {
|
|
|
300
339
|
"a shipped name with a typo",
|
|
301
340
|
"paperlint:agenticdve",
|
|
302
341
|
"not-found",
|
|
303
|
-
/acm-sigconf, agenticdev, aidc, aisec, ieee-conference, realm/,
|
|
342
|
+
/acm-sigconf, agenticdev, aidc, aisec, ieee-conference, msr, realm/,
|
|
304
343
|
],
|
|
305
344
|
["the base TeX set", "paperlint:tex-base", "not-found", /agenticdev/],
|
|
306
345
|
[
|
|
@@ -422,8 +461,32 @@ describe("a preset's template: kept as its file spells it, with that file", () =
|
|
|
422
461
|
expect(r.ok && r.value.template).toEqual({
|
|
423
462
|
text: "\\documentclass[conference]{IEEEtran}",
|
|
424
463
|
file: "/work/papers/p/own.jsonc",
|
|
464
|
+
forbids: [],
|
|
425
465
|
});
|
|
426
466
|
});
|
|
467
|
+
|
|
468
|
+
it("the options a file forbids travel with its template", () => {
|
|
469
|
+
const r = resolve("./own.jsonc", {
|
|
470
|
+
"/work/papers/p/own.jsonc": venue({
|
|
471
|
+
extends: "paperlint:ieee-conference",
|
|
472
|
+
template: "\\documentclass[conference]{IEEEtran}",
|
|
473
|
+
template_forbids: ["compsoc"],
|
|
474
|
+
}),
|
|
475
|
+
});
|
|
476
|
+
expect(r.ok && r.value.template?.forbids).toEqual(["compsoc"]);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("forbidden options without a template of the same file are refused by the schema", () => {
|
|
480
|
+
const r = resolve("./own.jsonc", {
|
|
481
|
+
"/work/papers/p/own.jsonc": venue({
|
|
482
|
+
extends: "paperlint:aidc",
|
|
483
|
+
template_forbids: ["compsoc"],
|
|
484
|
+
}),
|
|
485
|
+
});
|
|
486
|
+
expect(r.ok).toBe(false);
|
|
487
|
+
if (r.ok) return;
|
|
488
|
+
expect(presetProblemText(r.error)).toMatch(/template_forbids/);
|
|
489
|
+
});
|
|
427
490
|
});
|
|
428
491
|
|
|
429
492
|
describe("labelOf โ display only, never used to resolve", () => {
|
|
@@ -448,6 +511,7 @@ describe("shippedVenueNames โ what each shipped venue is called", () => {
|
|
|
448
511
|
"AIDC",
|
|
449
512
|
"AISec",
|
|
450
513
|
"ieee-conference",
|
|
514
|
+
"MSR",
|
|
451
515
|
"REALM",
|
|
452
516
|
]);
|
|
453
517
|
expect(names.find((n) => n.label === "AISec")?.aliases).toEqual(
|
|
@@ -469,6 +533,7 @@ describe("shippedVenueNames โ what each shipped venue is called", () => {
|
|
|
469
533
|
"AIDC",
|
|
470
534
|
"AISec",
|
|
471
535
|
"ieee-conference",
|
|
536
|
+
"MSR",
|
|
472
537
|
]);
|
|
473
538
|
});
|
|
474
539
|
});
|
package/src/presets.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* tex union โ a child never removes a package its parent needs
|
|
26
26
|
* format per key, the child wins; `kinds` by kind name, a child's kind replaces that kind
|
|
27
27
|
* rules per rule id, the child wins; `ruleOrigins` keeps which file set each
|
|
28
|
-
* template the child wins
|
|
28
|
+
* template the child wins, with its `template_forbids`; so does `name`, and `required_sections`, `register`, `talk` and
|
|
29
29
|
* `portal` (each whole)
|
|
30
30
|
* aliases union, with every `name` โ what the venue is called along the chain
|
|
31
31
|
*
|
|
@@ -75,10 +75,12 @@ export interface PresetDeps {
|
|
|
75
75
|
readonly venuesDir: string;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/** A preset's `template`, as its file spells it. */
|
|
78
|
+
/** A preset's `template`, as its file spells it, and the class options that file forbids. */
|
|
79
79
|
export interface PresetTemplate {
|
|
80
80
|
readonly text: string;
|
|
81
81
|
readonly file: string;
|
|
82
|
+
/** `template_forbids` of the same file: options the paper's class line must not carry. */
|
|
83
|
+
readonly forbids: readonly string[];
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
/** A resolved chain, merged. */
|
|
@@ -104,6 +106,11 @@ export interface Preset {
|
|
|
104
106
|
readonly aliases: readonly string[];
|
|
105
107
|
/** Whether the venue reviews double-blind (`anonymity/identity`); false when no file of the chain says. */
|
|
106
108
|
readonly blind: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* When the venue's names count as a mention in a paper's text (`tex/venue-leftover`): `any`
|
|
111
|
+
* whole-word use, or `with-year` beside it; `any` when no file of the chain says.
|
|
112
|
+
*/
|
|
113
|
+
readonly mentions: "any" | "with-year";
|
|
107
114
|
readonly requiredSections: readonly RequiredSection[];
|
|
108
115
|
readonly tex: TexRequirements;
|
|
109
116
|
readonly format: VenueFormat;
|
|
@@ -254,6 +261,7 @@ export function mergeFormat(
|
|
|
254
261
|
fontsText: pick("fontsText"),
|
|
255
262
|
fontsTitle: pick("fontsTitle"),
|
|
256
263
|
bodyEndsAt: pick("bodyEndsAt"),
|
|
264
|
+
appendixInBody: pick("appendixInBody"),
|
|
257
265
|
kinds: new Map([...parent.kinds, ...child.kinds]),
|
|
258
266
|
};
|
|
259
267
|
}
|
|
@@ -276,6 +284,7 @@ const NOTHING_MERGED: Merging = {
|
|
|
276
284
|
template: null,
|
|
277
285
|
aliases: [],
|
|
278
286
|
blind: false,
|
|
287
|
+
mentions: "any",
|
|
279
288
|
requiredSections: [],
|
|
280
289
|
tex: NO_REQUIREMENTS,
|
|
281
290
|
format: NO_FORMAT,
|
|
@@ -292,9 +301,13 @@ const mergeOne = (
|
|
|
292
301
|
{ preset: p, file }: { readonly preset: PresetFile; readonly file: string },
|
|
293
302
|
): Merging => ({
|
|
294
303
|
name: nameOf(p.identity) ?? acc.name,
|
|
295
|
-
template:
|
|
304
|
+
template:
|
|
305
|
+
p.template === null
|
|
306
|
+
? acc.template
|
|
307
|
+
: { text: p.template, file, forbids: p.templateForbids },
|
|
296
308
|
aliases: [...new Set([...acc.aliases, ...namesOf(p.identity), ...p.aliases])],
|
|
297
309
|
blind: p.blind ?? acc.blind,
|
|
310
|
+
mentions: p.mentions ?? acc.mentions,
|
|
298
311
|
requiredSections: p.requiredSections ?? acc.requiredSections,
|
|
299
312
|
tex: p.tex ? mergeRequirements(acc.tex, p.tex) : acc.tex,
|
|
300
313
|
format: mergeFormat(acc.format, p.format),
|
|
@@ -368,17 +381,21 @@ export function resolveShipped(deps: PresetDeps): readonly Preset[] {
|
|
|
368
381
|
}
|
|
369
382
|
|
|
370
383
|
/** A shipped venue as a name knows it, and the files of its chain: what tells it from a paper's own. */
|
|
371
|
-
export type ShippedVenue = Pick<
|
|
384
|
+
export type ShippedVenue = Pick<
|
|
385
|
+
Preset,
|
|
386
|
+
"label" | "aliases" | "chain" | "mentions"
|
|
387
|
+
>;
|
|
372
388
|
|
|
373
389
|
/**
|
|
374
390
|
* A resolved preset as a name knows it: its label, its aliases, its chain. Pure. (Not Remeda's
|
|
375
391
|
* `pick`: the app layer may not import it โ `boundaries/dependencies`, `APP_EXTERNALS`.)
|
|
376
392
|
*/
|
|
377
|
-
export const venueOf = ({
|
|
393
|
+
export const venueOf = ({
|
|
378
394
|
label,
|
|
379
395
|
aliases,
|
|
380
396
|
chain,
|
|
381
|
-
|
|
397
|
+
mentions,
|
|
398
|
+
}: Preset): ShippedVenue => ({ label, aliases, chain, mentions });
|
|
382
399
|
|
|
383
400
|
/**
|
|
384
401
|
* THE ONE SOURCE OF VENUE NAMES: every shipped venue's label and `aliases` (with each `name` along
|
package/src/submission.test.ts
CHANGED
|
@@ -117,6 +117,10 @@ const fakePortal = (answers: {
|
|
|
117
117
|
return { seen, portalFor };
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
+
/** The line `update` prints before it sends `pdf`, naming `call`. */
|
|
121
|
+
const CHECK = (pdf: string, call: string): string =>
|
|
122
|
+
`before you rely on it, open ${pdf} yourself and check it against ${call}: the page count and what counts toward the limit, the template and its class options, anonymity, and that every figure, table and reference renders โ paperlint passing is not the venue accepting the format`;
|
|
123
|
+
|
|
120
124
|
const ARGS: SubmissionArgs = {
|
|
121
125
|
sub: "show",
|
|
122
126
|
paperDir: "papers/p",
|
|
@@ -287,6 +291,7 @@ test("update: a dry run by default โ the paper's PDF sent, nothing saved, exit
|
|
|
287
291
|
],
|
|
288
292
|
]);
|
|
289
293
|
assert.deepEqual(r.out, [
|
|
294
|
+
CHECK("papers/p/paper.pdf", "the call for papers, https://example.org/cfp"),
|
|
290
295
|
"dry run โ the portal checked the change and kept nothing; --save sends it for real",
|
|
291
296
|
"HTTP 200",
|
|
292
297
|
"valid yes",
|
|
@@ -322,7 +327,41 @@ test("update --save --submit --abstract --pdf: all of it sent, and said to be sa
|
|
|
322
327
|
{ save: true },
|
|
323
328
|
],
|
|
324
329
|
]);
|
|
325
|
-
|
|
330
|
+
// Guards: the PDF that is sent is the one named, and --save is told to check it too.
|
|
331
|
+
assert.deepEqual(r.out.slice(0, 2), [
|
|
332
|
+
CHECK("other.pdf", "the call for papers, https://example.org/cfp"),
|
|
333
|
+
"saved",
|
|
334
|
+
]);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("๐ด update: the check line names the venue's call from the preset's `url`; a preset that names no venue gets the generic line", async () => {
|
|
338
|
+
const family = memoryFiles({
|
|
339
|
+
"/w/package.json": "{}",
|
|
340
|
+
"/w/house.jsonc": JSON.stringify({
|
|
341
|
+
type: "family",
|
|
342
|
+
extends: "paperlint:ieee-conference",
|
|
343
|
+
portal: { kind: "hotcrp", url: `${SITE}/` },
|
|
344
|
+
}),
|
|
345
|
+
[`${PAPER}/paperlint.json`]: JSON.stringify({
|
|
346
|
+
extends: "../../house.jsonc",
|
|
347
|
+
submission: { id: 7 },
|
|
348
|
+
}),
|
|
349
|
+
[`${PAPER}/paper.pdf`]: PDF,
|
|
350
|
+
});
|
|
351
|
+
const r = await run({ sub: "update" }, { files: family });
|
|
352
|
+
assert.equal(r.code, 0);
|
|
353
|
+
assert.equal(
|
|
354
|
+
r.out[0],
|
|
355
|
+
CHECK("papers/p/paper.pdf", "the venue's call for papers"),
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("show sends nothing, so it asks for no check", async () => {
|
|
360
|
+
const r = await run({});
|
|
361
|
+
assert.equal(
|
|
362
|
+
r.out.some((l) => l.includes("call for papers")),
|
|
363
|
+
false,
|
|
364
|
+
);
|
|
326
365
|
});
|
|
327
366
|
|
|
328
367
|
test("update: valid:false, or a non-2xx status, exits 1 with the portal's messages", async () => {
|
|
@@ -338,7 +377,7 @@ test("update: valid:false, or a non-2xx status, exits 1 with the portal's messag
|
|
|
338
377
|
{ portal: fakePortal({ update: invalid }) },
|
|
339
378
|
);
|
|
340
379
|
assert.equal(r1.code, 1);
|
|
341
|
-
assert.deepEqual(r1.out.slice(
|
|
380
|
+
assert.deepEqual(r1.out.slice(3), [
|
|
342
381
|
"valid NO",
|
|
343
382
|
"changes โ",
|
|
344
383
|
`local PDF papers/p/paper.pdf sha2-${HEX}`,
|
|
@@ -380,7 +419,7 @@ test("update --save that the portal withheld says so", async () => {
|
|
|
380
419
|
{ sub: "update", save: true },
|
|
381
420
|
{ portal: fakePortal({ update: withheld }) },
|
|
382
421
|
);
|
|
383
|
-
assert.equal(r.out[
|
|
422
|
+
assert.equal(r.out[1], "SAVE WITHHELD by the portal โ nothing changed");
|
|
384
423
|
});
|
|
385
424
|
|
|
386
425
|
test("update: no PDF to send, or no abstract file, is refused before the portal is asked", async () => {
|
package/src/submission.ts
CHANGED
|
@@ -80,6 +80,8 @@ export interface SubmissionTarget {
|
|
|
80
80
|
readonly id: number;
|
|
81
81
|
/** The paper's built PDF, absolute. */
|
|
82
82
|
readonly pdf: string;
|
|
83
|
+
/** The venue's call for papers (the preset's `url`); null when the preset names no venue. */
|
|
84
|
+
readonly call: string | null;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
const configOf = (dir: string): string => join(dir, CONFIG_FILE);
|
|
@@ -109,7 +111,9 @@ export function submissionTarget(
|
|
|
109
111
|
const portal = supportedPortal(declared);
|
|
110
112
|
if (!portal.ok) return portal;
|
|
111
113
|
const pdf = join(paperDir, p.settings.pdf ?? "paper.pdf");
|
|
112
|
-
|
|
114
|
+
const identity = p.preset.identity;
|
|
115
|
+
const call = identity.type === "venue" ? identity.url : null;
|
|
116
|
+
return ok({ portal: portal.value, id: sub.id, pdf, call });
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
/** The token from the environment, or the line naming the variable and where to get one. */
|
|
@@ -182,6 +186,14 @@ export function showLines(
|
|
|
182
186
|
];
|
|
183
187
|
}
|
|
184
188
|
|
|
189
|
+
/**
|
|
190
|
+
* What `update` says before it sends a PDF, dry run or not: the author opens that file and checks it
|
|
191
|
+
* against the venue's call. paperlint's checks are an aid; the venue's reading of the PDF is what
|
|
192
|
+
* counts. Pure.
|
|
193
|
+
*/
|
|
194
|
+
export const checkPdfLine = (shown: string, call: string | null): string =>
|
|
195
|
+
`before you rely on it, open ${shown} yourself and check it against ${call === null ? "the venue's call for papers" : `the call for papers, ${call}`}: the page count and what counts toward the limit, the template and its class options, anonymity, and that every figure, table and reference renders โ paperlint passing is not the venue accepting the format`;
|
|
196
|
+
|
|
185
197
|
/** `update`'s report. Pure. */
|
|
186
198
|
export function updateLines(
|
|
187
199
|
o: UpdateOutcome,
|
|
@@ -243,6 +255,17 @@ function abstractOf(
|
|
|
243
255
|
: ok(new TextDecoder().decode(bytes).trim());
|
|
244
256
|
}
|
|
245
257
|
|
|
258
|
+
/** What `update` sends: the PDF's bytes, the abstract's text, and whether to mark it submitted. */
|
|
259
|
+
const changeOf = (
|
|
260
|
+
a: SubmissionArgs & { readonly pdfPath: string },
|
|
261
|
+
bytes: Uint8Array,
|
|
262
|
+
abstract: string | null,
|
|
263
|
+
) => ({
|
|
264
|
+
pdf: { name: basename(a.pdfPath), bytes },
|
|
265
|
+
abstract,
|
|
266
|
+
submit: a.submit,
|
|
267
|
+
});
|
|
268
|
+
|
|
246
269
|
/** `update`: send the PDF (and the abstract, and "submitted" when asked); a dry run unless saving. */
|
|
247
270
|
async function runUpdate(
|
|
248
271
|
t: SubmissionTarget,
|
|
@@ -259,12 +282,10 @@ async function runUpdate(
|
|
|
259
282
|
);
|
|
260
283
|
const abstract = abstractOf(a, deps);
|
|
261
284
|
if (!abstract.ok) return (deps.err(abstract.error), 2);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
};
|
|
267
|
-
const r = await portal.update(t.id, change, { save: a.save });
|
|
285
|
+
deps.log(checkPdfLine(shown, t.call));
|
|
286
|
+
const r = await portal.update(t.id, changeOf(a, bytes, abstract.value), {
|
|
287
|
+
save: a.save,
|
|
288
|
+
});
|
|
268
289
|
if (!r.ok) return (deps.err(failureText(r.error)), 1);
|
|
269
290
|
const local = { shown, sha256: sha256Hex(bytes) };
|
|
270
291
|
updateLines(r.value, local, a.save).forEach((l) => {
|
package/src/tex-requirements.ts
CHANGED
|
@@ -140,6 +140,11 @@ export interface VenueFormat {
|
|
|
140
140
|
readonly fontsTitle: string | null;
|
|
141
141
|
/** How the body is counted: null is banal's count; `references`, the pages before the references. */
|
|
142
142
|
readonly bodyEndsAt: "references" | null;
|
|
143
|
+
/**
|
|
144
|
+
* With `bodyEndsAt`: appendices count as body wherever they sit, and the reference pages are the
|
|
145
|
+
* ones holding only references. Null: the appendix ends the body, as `references` alone counts it.
|
|
146
|
+
*/
|
|
147
|
+
readonly appendixInBody: boolean | null;
|
|
143
148
|
readonly kinds: ReadonlyMap<string, KindLimits>;
|
|
144
149
|
}
|
|
145
150
|
|
|
@@ -155,6 +160,7 @@ export const NO_FORMAT: VenueFormat = {
|
|
|
155
160
|
fontsText: null,
|
|
156
161
|
fontsTitle: null,
|
|
157
162
|
bodyEndsAt: null,
|
|
163
|
+
appendixInBody: null,
|
|
158
164
|
kinds: new Map(),
|
|
159
165
|
};
|
|
160
166
|
|
|
@@ -189,10 +195,14 @@ export interface PresetFile {
|
|
|
189
195
|
readonly extends: string | null;
|
|
190
196
|
/** The `\\documentclass` the venue's template uses, as the file spells it; null when it names none. */
|
|
191
197
|
readonly template: string | null;
|
|
198
|
+
/** Class options the venue forbids, read with `template`; empty when the file names none. */
|
|
199
|
+
readonly templateForbids: readonly string[];
|
|
192
200
|
/** Other names the venue goes by in a paper's text; empty when the file names none. */
|
|
193
201
|
readonly aliases: readonly string[];
|
|
194
202
|
/** Whether the venue reviews double-blind; null when the file does not say. */
|
|
195
203
|
readonly blind: boolean | null;
|
|
204
|
+
/** When the venue's names count as a mention in a paper's text; null when the file does not say. */
|
|
205
|
+
readonly mentions: "with-year" | null;
|
|
196
206
|
/** Sections the venue requires; null when the file names none (a child's list replaces its parent's). */
|
|
197
207
|
readonly requiredSections: readonly RequiredSection[] | null;
|
|
198
208
|
/** Null when the file declares no `tex` block (allowed only with `extends`). */
|
|
@@ -227,6 +237,7 @@ interface FormatJson {
|
|
|
227
237
|
readonly fonts_text?: string;
|
|
228
238
|
readonly fonts_title?: string;
|
|
229
239
|
readonly body_ends_at?: "references";
|
|
240
|
+
readonly appendix_in_body?: boolean;
|
|
230
241
|
readonly kinds?: KindsJson;
|
|
231
242
|
}
|
|
232
243
|
|
|
@@ -244,8 +255,10 @@ type PresetJson = PresetBodyJson &
|
|
|
244
255
|
interface PresetBodyJson {
|
|
245
256
|
readonly extends?: string;
|
|
246
257
|
readonly template?: string;
|
|
258
|
+
readonly template_forbids?: readonly string[];
|
|
247
259
|
readonly aliases?: readonly string[];
|
|
248
260
|
readonly blind?: boolean;
|
|
261
|
+
readonly mentions?: "with-year";
|
|
249
262
|
readonly required_sections?: readonly {
|
|
250
263
|
readonly title: string;
|
|
251
264
|
readonly position?: "last";
|
|
@@ -292,6 +305,7 @@ function formatOf(j: FormatJson = {}): VenueFormat {
|
|
|
292
305
|
fontsText: orNull(j.fonts_text),
|
|
293
306
|
fontsTitle: orNull(j.fonts_title),
|
|
294
307
|
bodyEndsAt: orNull(j.body_ends_at),
|
|
308
|
+
appendixInBody: orNull(j.appendix_in_body),
|
|
295
309
|
kinds: kindsOf(j.kinds),
|
|
296
310
|
};
|
|
297
311
|
}
|
|
@@ -376,6 +390,10 @@ const anchorsOf = (
|
|
|
376
390
|
},
|
|
377
391
|
}));
|
|
378
392
|
|
|
393
|
+
/** A preset's `portal`, or null when it names none. */
|
|
394
|
+
const portalOf = (p: PresetJson["portal"]): Readonly<VenuePortal> | null =>
|
|
395
|
+
p === undefined ? null : { kind: p.kind, url: p.url };
|
|
396
|
+
|
|
379
397
|
/**
|
|
380
398
|
* The text of one preset file โ the typed file, or an Error naming every problem. The ONE parser
|
|
381
399
|
* of a preset: the toolchain reads its `tex`, the venue rules its `format`, the config its `rules`.
|
|
@@ -400,18 +418,17 @@ export function parsePreset(
|
|
|
400
418
|
identity: identityOf(j),
|
|
401
419
|
extends: orNull(j.extends),
|
|
402
420
|
template: orNull(j.template),
|
|
421
|
+
templateForbids: j.template_forbids ?? [],
|
|
403
422
|
aliases: j.aliases ?? [],
|
|
404
423
|
blind: orNull(j.blind),
|
|
424
|
+
mentions: orNull(j.mentions),
|
|
405
425
|
requiredSections: sectionsOf(j.required_sections),
|
|
406
426
|
tex: texOf(j.tex),
|
|
407
427
|
format: formatOf(j.format),
|
|
408
428
|
rules: j.rules ?? {},
|
|
409
429
|
registerAnchors: anchorsOf(j.register),
|
|
410
430
|
talk: j.talk === undefined ? null : venueTalkOf(j.talk),
|
|
411
|
-
portal:
|
|
412
|
-
j.portal === undefined
|
|
413
|
-
? null
|
|
414
|
-
: { kind: j.portal.kind, url: j.portal.url },
|
|
431
|
+
portal: portalOf(j.portal),
|
|
415
432
|
};
|
|
416
433
|
}
|
|
417
434
|
|
|
@@ -322,6 +322,119 @@ describe("tex/template โ a project's own preset, and nothing to judge", () =>
|
|
|
322
322
|
});
|
|
323
323
|
});
|
|
324
324
|
|
|
325
|
+
/** A project's own venue on IEEEtran's conference class that forbids the two compsoc options. */
|
|
326
|
+
const forbidding = (body: Record<string, unknown> = {}) => ({
|
|
327
|
+
[`${PAPER}/own.jsonc`]: JSON.stringify(
|
|
328
|
+
venuePreset("Own", {
|
|
329
|
+
extends: "paperlint:ieee-conference",
|
|
330
|
+
template: "\\documentclass[10pt,conference]{IEEEtran}",
|
|
331
|
+
template_forbids: ["compsoc", "compsocconf"],
|
|
332
|
+
...body,
|
|
333
|
+
}),
|
|
334
|
+
),
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe("tex/template โ class options the preset forbids (`template_forbids`)", () => {
|
|
338
|
+
const judge = (cls: string, extra = forbidding()) =>
|
|
339
|
+
lint(paper(cls, "Text."), { extends: "./own.jsonc" }, { extra });
|
|
340
|
+
|
|
341
|
+
it("the template's own line passes, and so does an extra option the preset does not forbid", () => {
|
|
342
|
+
expect(judge("\\documentclass[10pt,conference]{IEEEtran}")).toEqual([]);
|
|
343
|
+
expect(
|
|
344
|
+
judge("\\documentclass[10pt,conference,letterpaper]{IEEEtran}"),
|
|
345
|
+
).toEqual([]);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("๐ด a forbidden option is reported at the class line, naming it and the venue's line", () => {
|
|
349
|
+
const fs = judge("\\documentclass[10pt,conference,compsoc]{IEEEtran}");
|
|
350
|
+
expect(ids(fs)).toEqual(["tex/template:forbiddenOption"]);
|
|
351
|
+
expect(fs[0]?.message).toBe(
|
|
352
|
+
"the class option `compsoc` is forbidden: Own requires `\\documentclass[10pt,conference]{IEEEtran}` without `compsoc`, `compsocconf`",
|
|
353
|
+
);
|
|
354
|
+
expect(fs[0]?.line).toBe(2);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("each forbidden option is one finding, beside a missing one", () => {
|
|
358
|
+
expect(
|
|
359
|
+
ids(judge("\\documentclass[compsoc,compsocconf,conference]{IEEEtran}")),
|
|
360
|
+
).toEqual([
|
|
361
|
+
"tex/template:forbiddenOption",
|
|
362
|
+
"tex/template:forbiddenOption",
|
|
363
|
+
"tex/template:missingOption",
|
|
364
|
+
]);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
describe("tex/template under paperlint:msr โ `[10pt,conference]`, without compsoc or compsocconf", () => {
|
|
369
|
+
const MSR = { extends: "paperlint:msr", kind: "technical" };
|
|
370
|
+
|
|
371
|
+
it("the class line MSR's call names passes", () => {
|
|
372
|
+
expect(
|
|
373
|
+
lint(paper("\\documentclass[10pt,conference]{IEEEtran}", "Text."), MSR),
|
|
374
|
+
).toEqual([]);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it.each([["compsoc"], ["compsocconf"]])(
|
|
378
|
+
"๐ด `%s` in the class options fails tex/template",
|
|
379
|
+
(option) => {
|
|
380
|
+
const fs = lint(
|
|
381
|
+
paper(`\\documentclass[10pt,conference,${option}]{IEEEtran}`, "Text."),
|
|
382
|
+
MSR,
|
|
383
|
+
);
|
|
384
|
+
expect(ids(fs)).toEqual(["tex/template:forbiddenOption"]);
|
|
385
|
+
expect(fs[0]?.message).toMatch(
|
|
386
|
+
`the class option \`${option}\` is forbidden: MSR requires`,
|
|
387
|
+
);
|
|
388
|
+
},
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
it("AIDC's class line fails MSR on both counts: compsoc forbidden, 10pt missing", () => {
|
|
392
|
+
expect(ids(lint(OFFICIAL, MSR))).toEqual([
|
|
393
|
+
"tex/template:forbiddenOption",
|
|
394
|
+
"tex/template:missingOption",
|
|
395
|
+
]);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe("tex/template โ forbidden options behind a switch, and down a chain", () => {
|
|
400
|
+
it("behind a TeX switch, a candidate with a forbidden option is not the venue's class", () => {
|
|
401
|
+
const two = (a: string, b: string) =>
|
|
402
|
+
`\\def\\v{1}\n\\if\\v1 ${a} \\fi\n\\if\\v2 ${b} \\fi\n\\begin{document}\nText.\n\\end{document}\n`;
|
|
403
|
+
const compsoc = "\\documentclass[10pt,conference,compsoc]{IEEEtran}";
|
|
404
|
+
const opts = { extra: forbidding() };
|
|
405
|
+
expect(
|
|
406
|
+
ids(lint(two(compsoc, compsoc), { extends: "./own.jsonc" }, opts)),
|
|
407
|
+
).toEqual(["tex/template:noCandidate"]);
|
|
408
|
+
expect(
|
|
409
|
+
lint(
|
|
410
|
+
two(compsoc, "\\documentclass[10pt,conference]{IEEEtran}"),
|
|
411
|
+
{ extends: "./own.jsonc" },
|
|
412
|
+
opts,
|
|
413
|
+
),
|
|
414
|
+
).toEqual([]);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("a child's template replaces its parent's, the forbidden options with it", () => {
|
|
418
|
+
const child = `${PAPER}/child.jsonc`;
|
|
419
|
+
const extra = {
|
|
420
|
+
...forbidding(),
|
|
421
|
+
[child]: JSON.stringify(
|
|
422
|
+
venuePreset("Child", {
|
|
423
|
+
extends: "./own.jsonc",
|
|
424
|
+
template: "\\documentclass[conference]{IEEEtran}",
|
|
425
|
+
}),
|
|
426
|
+
),
|
|
427
|
+
};
|
|
428
|
+
expect(
|
|
429
|
+
lint(
|
|
430
|
+
paper("\\documentclass[conference,compsoc]{IEEEtran}", "Text."),
|
|
431
|
+
{ extends: "./child.jsonc" },
|
|
432
|
+
{ extra },
|
|
433
|
+
),
|
|
434
|
+
).toEqual([]);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
325
438
|
describe("tex/template โ a template the reader cannot read, a bare class name, nothing to judge", () => {
|
|
326
439
|
it("a template the reader cannot read is said once, naming its file โ the class is not judged", () => {
|
|
327
440
|
const own = `${PAPER}/own.jsonc`;
|
|
@@ -580,6 +693,53 @@ describe("tex/venue-leftover โ another venue named in the text", () => {
|
|
|
580
693
|
});
|
|
581
694
|
});
|
|
582
695
|
|
|
696
|
+
describe("tex/venue-leftover โ MSR, a name that is also an acronym, counts only with a year (`mentions`)", () => {
|
|
697
|
+
const under = (text: string) =>
|
|
698
|
+
leftovers(lint(aidcPaper(`${text}\n${BODY}${STATEMENT}`), AIDC));
|
|
699
|
+
|
|
700
|
+
it.each([
|
|
701
|
+
[
|
|
702
|
+
"an acronym the paper defines",
|
|
703
|
+
"We score it by Manual Speech Recognition (MSR), and MSR scores rise.",
|
|
704
|
+
],
|
|
705
|
+
[
|
|
706
|
+
"Microsoft Research",
|
|
707
|
+
"Researchers at Microsoft Research (MSR) built it, and MSR released it.",
|
|
708
|
+
],
|
|
709
|
+
[
|
|
710
|
+
"a citation of an MSR paper: the key, and the entry in the bibliography",
|
|
711
|
+
"As shown~\\cite{msr2024}.\n\\begin{thebibliography}{1}\\bibitem{msr2024} A. Author, ``Mining,'' in \\emph{Proc. MSR 2024}, 2024.\\end{thebibliography}",
|
|
712
|
+
],
|
|
713
|
+
["a year that is not beside the name", "MSR, in 2027, scored best."],
|
|
714
|
+
])("silent: %s", (_, text) => {
|
|
715
|
+
expect(under(text)).toEqual([]);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it.each([
|
|
719
|
+
["First written for MSR 2027.", "MSR"],
|
|
720
|
+
["A version appeared as MSR'27 work.", "MSR"],
|
|
721
|
+
["Submitted to MSR~2027.", "MSR"],
|
|
722
|
+
])("๐ด reported: ยซ%sยป", (text, name) => {
|
|
723
|
+
const fs = under(text);
|
|
724
|
+
expect(fs.map((f) => f.message)).toEqual([
|
|
725
|
+
expect.stringMatching(
|
|
726
|
+
new RegExp(`^ยซ${name}ยป names MSR, and this paper extends AIDC`),
|
|
727
|
+
),
|
|
728
|
+
]);
|
|
729
|
+
expect(fs[0]?.line).toBe(4);
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it("under msr itself, its own name with a year is never a leftover", () => {
|
|
733
|
+
const tex = paper(
|
|
734
|
+
"\\documentclass[10pt,conference]{IEEEtran}",
|
|
735
|
+
"First written for MSR 2027.",
|
|
736
|
+
);
|
|
737
|
+
expect(
|
|
738
|
+
leftovers(lint(tex, { extends: "paperlint:msr", kind: "technical" })),
|
|
739
|
+
).toEqual([]);
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
|
|
583
743
|
describe("tex/venue-leftover โ a URL is not text, a definition body is", () => {
|
|
584
744
|
it("\\href: a name in the URL is not reported, a name in the link text is", () => {
|
|
585
745
|
const inUrl = aidcPaper(
|
|
@@ -736,6 +896,7 @@ describe("otherVenues", () => {
|
|
|
736
896
|
"acm-sigconf",
|
|
737
897
|
"AgenticDev",
|
|
738
898
|
"AISec",
|
|
899
|
+
"MSR",
|
|
739
900
|
]);
|
|
740
901
|
});
|
|
741
902
|
});
|