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/tex-venue-rules.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* requires of the LaTeX itself, before any build:
|
|
4
4
|
*
|
|
5
5
|
* tex/template error the `\documentclass` is the preset's class, with every option it names
|
|
6
|
+
* and none it forbids (`template_forbids`)
|
|
6
7
|
* tex/required-section error each section the preset requires is there, titled exactly, and
|
|
7
8
|
* where the preset says (`last`: after every section of the body)
|
|
8
9
|
* tex/venue-leftover warn the text a reader sees names ANOTHER shipped venue — its `name`
|
|
@@ -107,20 +108,38 @@ type TemplateData = {
|
|
|
107
108
|
readonly template: string;
|
|
108
109
|
};
|
|
109
110
|
|
|
110
|
-
/**
|
|
111
|
+
/** What the template requires of a class line: its class and options, and the options it forbids. */
|
|
112
|
+
interface Wanted {
|
|
113
|
+
readonly cls: DocumentClass;
|
|
114
|
+
readonly forbids: readonly string[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A class line that names a class, against the template's: the same class, with every option, and
|
|
119
|
+
* none of the options the preset forbids.
|
|
120
|
+
*/
|
|
111
121
|
function judgeClass(
|
|
112
122
|
line: Extract<ClassLine, { readonly kind: "class" }>,
|
|
113
|
-
want:
|
|
123
|
+
want: Wanted,
|
|
114
124
|
data: TemplateData,
|
|
115
125
|
): readonly Located[] {
|
|
116
126
|
const at = spanOf(line.place);
|
|
117
|
-
if (line.cls !== want.cls)
|
|
127
|
+
if (line.cls !== want.cls.cls)
|
|
118
128
|
return [{ messageId: "wrongClass", data: { ...data, got: line.cls }, at }];
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
const forbidden = want.forbids.filter((o) => line.options.includes(o));
|
|
130
|
+
const without = want.forbids.map((o) => `\`${o}\``).join(", ");
|
|
131
|
+
return [
|
|
132
|
+
...forbidden.map((option) => ({
|
|
133
|
+
messageId: "forbiddenOption",
|
|
134
|
+
data: { ...data, option, without },
|
|
135
|
+
at,
|
|
136
|
+
})),
|
|
137
|
+
...missingOptions(line, want.cls).map((option) => ({
|
|
138
|
+
messageId: "missingOption",
|
|
139
|
+
data: { ...data, option },
|
|
140
|
+
at,
|
|
141
|
+
})),
|
|
142
|
+
];
|
|
124
143
|
}
|
|
125
144
|
|
|
126
145
|
/** A candidate as the message names it: its line, or an empty `\documentclass{}`. */
|
|
@@ -133,7 +152,7 @@ const candidateLine = (c: ClassCandidate): string =>
|
|
|
133
152
|
*/
|
|
134
153
|
function judgeCandidates(
|
|
135
154
|
candidates: readonly [ClassCandidate, ...ClassCandidate[]],
|
|
136
|
-
want:
|
|
155
|
+
want: Wanted,
|
|
137
156
|
data: TemplateData,
|
|
138
157
|
): readonly Located[] {
|
|
139
158
|
const matches = candidates.some(
|
|
@@ -150,7 +169,7 @@ function judgeCandidates(
|
|
|
150
169
|
/** The paper's class line against a template that was read: present, naming a class, that class. */
|
|
151
170
|
function judgeClassLine(
|
|
152
171
|
line: ClassLine,
|
|
153
|
-
want:
|
|
172
|
+
want: Wanted,
|
|
154
173
|
data: TemplateData,
|
|
155
174
|
): readonly Located[] {
|
|
156
175
|
switch (line.kind) {
|
|
@@ -183,7 +202,7 @@ export function judgeTemplate(
|
|
|
183
202
|
return [{ messageId: "badTemplate", data, at: null }];
|
|
184
203
|
}
|
|
185
204
|
const data = { venue: preset.label, template: documentClassLine(want) };
|
|
186
|
-
return judgeClassLine(line, want, data);
|
|
205
|
+
return judgeClassLine(line, { cls: want, forbids: t.forbids }, data);
|
|
187
206
|
}
|
|
188
207
|
|
|
189
208
|
/** Where a heading starts, or null when the parser gave it no place. */
|
|
@@ -248,8 +267,17 @@ export function judgeRequiredSections(
|
|
|
248
267
|
);
|
|
249
268
|
}
|
|
250
269
|
|
|
251
|
-
/**
|
|
252
|
-
|
|
270
|
+
/**
|
|
271
|
+
* Another shipped venue: the word for it, what it is called in a paper's text, and when a name
|
|
272
|
+
* counts as naming it (`mentions`).
|
|
273
|
+
*/
|
|
274
|
+
export type OtherVenue = NamedVenue & Pick<Preset, "mentions">;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* What must follow a name for it to name a `with-year` venue: a year beside it — `2027`, `'27` or
|
|
278
|
+
* `’27`, after spaces or none (`~` renders as a space). A lookahead, so the finding is the name.
|
|
279
|
+
*/
|
|
280
|
+
const YEAR_BESIDE = "(?=[\\s\\u00a0]*(?:\\d{4}|['’]\\d{2})(?![\\p{L}\\p{N}]))";
|
|
253
281
|
|
|
254
282
|
/** `s` as a pattern that matches it literally. */
|
|
255
283
|
const literal = (s: string): string =>
|
|
@@ -271,7 +299,7 @@ export function judgeLeftover(
|
|
|
271
299
|
.map((alias) => ({
|
|
272
300
|
other: o.label,
|
|
273
301
|
re: new RegExp(
|
|
274
|
-
`(?<![\\p{L}\\p{N}])${literal(alias)}(?![\\p{L}\\p{N}])`,
|
|
302
|
+
`(?<![\\p{L}\\p{N}])${literal(alias)}(?![\\p{L}\\p{N}])${o.mentions === "with-year" ? YEAR_BESIDE : ""}`,
|
|
275
303
|
"gu",
|
|
276
304
|
),
|
|
277
305
|
})),
|
|
@@ -357,6 +385,8 @@ const RULES: Readonly<
|
|
|
357
385
|
"the class is `{{got}}`, and {{venue}} requires `{{template}}` — the page size, fonts and layout the venue checks come from the class",
|
|
358
386
|
missingOption:
|
|
359
387
|
"the class option `{{option}}` is missing: {{venue}} requires `{{template}}`",
|
|
388
|
+
forbiddenOption:
|
|
389
|
+
"the class option `{{option}}` is forbidden: {{venue}} requires `{{template}}` without {{without}}",
|
|
360
390
|
noCandidate:
|
|
361
391
|
"none of the {{count}} \\documentclass lines is `{{template}}`, which {{venue}} requires: {{candidates}}. The source picks one behind a TeX switch, which is not evaluated; make one of them the venue's",
|
|
362
392
|
},
|
|
@@ -442,7 +472,7 @@ export function otherVenues(
|
|
|
442
472
|
): readonly OtherVenue[] {
|
|
443
473
|
return shippedVenueNames(deps)
|
|
444
474
|
.filter((v) => !v.chain.every((f) => preset.chain.includes(f)))
|
|
445
|
-
.map(({ label, aliases }) => ({ label, aliases }));
|
|
475
|
+
.map(({ label, aliases, mentions }) => ({ label, aliases, mentions }));
|
|
446
476
|
}
|
|
447
477
|
|
|
448
478
|
/** What the venue-conformance rules are built with: the preset store, and the LaTeX reader. */
|
package/src/venue-rules.test.ts
CHANGED
|
@@ -239,13 +239,13 @@ describe("pdf/profile — the declaration must resolve, or nothing is judged", (
|
|
|
239
239
|
"an unknown venue (a typo would silently disable every check)",
|
|
240
240
|
{ extends: "paperlint:agentic-dev", kind: "short" },
|
|
241
241
|
"preset",
|
|
242
|
-
/agenticdev, aidc, aisec, ieee-conference, realm/,
|
|
242
|
+
/agenticdev, aidc, aisec, ieee-conference, msr, realm/,
|
|
243
243
|
],
|
|
244
244
|
[
|
|
245
245
|
"the base TeX set is not a venue",
|
|
246
246
|
{ extends: "paperlint:tex-base" },
|
|
247
247
|
"preset",
|
|
248
|
-
/agenticdev, aidc, aisec, ieee-conference, realm/,
|
|
248
|
+
/agenticdev, aidc, aisec, ieee-conference, msr, realm/,
|
|
249
249
|
],
|
|
250
250
|
[
|
|
251
251
|
"paperlint.json that is not JSON",
|
|
@@ -863,6 +863,177 @@ describe("format/page-limit on AIDC: the two signals of where the references sta
|
|
|
863
863
|
});
|
|
864
864
|
});
|
|
865
865
|
|
|
866
|
+
/**
|
|
867
|
+
* A project's own venue that counts its appendices in the body (`appendix_in_body`): a body of ten
|
|
868
|
+
* pages, appendices included, and two pages holding only references — MSR's rule.
|
|
869
|
+
*/
|
|
870
|
+
const OWN_APPENDIX_IN_BODY = {
|
|
871
|
+
[`${PAPER}/own.jsonc`]: JSON.stringify({
|
|
872
|
+
type: "venue",
|
|
873
|
+
name: "Own",
|
|
874
|
+
url: "https://example.org/cfp",
|
|
875
|
+
extends: "paperlint:ieee-conference",
|
|
876
|
+
format: {
|
|
877
|
+
body_ends_at: "references",
|
|
878
|
+
appendix_in_body: true,
|
|
879
|
+
kinds: {
|
|
880
|
+
technical: { body_pages_max: 10, ref_pages_max: 2 },
|
|
881
|
+
open: {},
|
|
882
|
+
refs: { ref_pages_max: 2 },
|
|
883
|
+
},
|
|
884
|
+
},
|
|
885
|
+
}),
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
/** `own.jsonc` papers of the kind named, over IEEE facts with `pages` and the two anchors. */
|
|
889
|
+
const ownSplit = (
|
|
890
|
+
pages: readonly string[],
|
|
891
|
+
bib: number | null,
|
|
892
|
+
appendix: number | null,
|
|
893
|
+
kind = "technical",
|
|
894
|
+
) =>
|
|
895
|
+
lint({
|
|
896
|
+
venue: { extends: "./own.jsonc", kind },
|
|
897
|
+
extra: OWN_APPENDIX_IN_BODY,
|
|
898
|
+
facts: ieeeFacts((f) => {
|
|
899
|
+
f.pages_text = [...pages];
|
|
900
|
+
f.bib_anchor_page = bib;
|
|
901
|
+
f.appendix_anchor_page = appendix;
|
|
902
|
+
// banal's counts, which this counting replaces: neither is judged.
|
|
903
|
+
f.body_pages = 99;
|
|
904
|
+
f.ref_pages = 99;
|
|
905
|
+
}),
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
describe("format/page-limit with `appendix_in_body`: the appendix is body, the reference pages hold only references", () => {
|
|
909
|
+
const REFS = ["the end\nReferences\n[1] A.\n", "[2] B.\n", "[3] C.\n"];
|
|
910
|
+
|
|
911
|
+
it("ten pages of body and two of references pass; banal's counts are not judged", () => {
|
|
912
|
+
expect(ownSplit([...bodyPages(9), ...REFS], 10, null)).toEqual([]);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
it("🔴 an appendix after the references that brings the body to eleven fails, naming where it starts", () => {
|
|
916
|
+
const fs = ownSplit(
|
|
917
|
+
[...bodyPages(9), ...REFS.slice(0, 2), "Appendix A.\nDetails\n"],
|
|
918
|
+
10,
|
|
919
|
+
12,
|
|
920
|
+
);
|
|
921
|
+
expect(fs.map((f) => f.message)).toEqual([
|
|
922
|
+
"body pages (up to the references on page 10, and the appendix from page 12): 11, over the limit 10 for Own/technical — a desk reject; cut the text",
|
|
923
|
+
]);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it("🔴 three pages holding only references fail", () => {
|
|
927
|
+
const fs = ownSplit([...bodyPages(9), ...REFS, "[4] D.\n"], 10, null);
|
|
928
|
+
expect(fs.map((f) => f.message)).toEqual([
|
|
929
|
+
"pages holding only references: 3, over the limit 2 for Own/technical — a desk reject; cut the text",
|
|
930
|
+
]);
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
it("an eleventh page of body before the references fails, appendices included", () => {
|
|
934
|
+
expect(ids(ownSplit([...bodyPages(10), ...REFS], 11, null))).toEqual([
|
|
935
|
+
"format/page-limit:pages",
|
|
936
|
+
]);
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
it.each<
|
|
940
|
+
[string, readonly string[], readonly [number | null, number | null], string]
|
|
941
|
+
>([
|
|
942
|
+
[
|
|
943
|
+
"no references heading",
|
|
944
|
+
bodyPages(3),
|
|
945
|
+
[null, null],
|
|
946
|
+
"format/page-limit:noReferences",
|
|
947
|
+
],
|
|
948
|
+
[
|
|
949
|
+
"an appendix anchor on a page with no «Appendix» line",
|
|
950
|
+
[...bodyPages(3), ...REFS],
|
|
951
|
+
[4, 6],
|
|
952
|
+
"format/page-limit:unclear",
|
|
953
|
+
],
|
|
954
|
+
])("%s: said, not counted", (_, pages, [bib, appendix], want) => {
|
|
955
|
+
expect(ids(ownSplit(pages, bib, appendix))).toEqual([want]);
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
it("a kind with no limits judges nothing; one with a reference limit only judges that", () => {
|
|
959
|
+
expect(ownSplit(bodyPages(3), null, null, "open")).toEqual([]);
|
|
960
|
+
expect(
|
|
961
|
+
ids(ownSplit([...bodyPages(12), ...REFS, "[4] D.\n"], 13, null, "refs")),
|
|
962
|
+
).toEqual(["format/page-limit:pages"]);
|
|
963
|
+
});
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
/** An MSR technical paper that declares who wrote it. */
|
|
967
|
+
const MSR = {
|
|
968
|
+
extends: "paperlint:msr",
|
|
969
|
+
kind: "technical",
|
|
970
|
+
identity: ["Ada Example"],
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
/** MSR facts: `pages` as the PDF's text, the two anchors where hyperref put them. */
|
|
974
|
+
const msrFacts = (
|
|
975
|
+
pages: readonly string[],
|
|
976
|
+
bib: number,
|
|
977
|
+
appendix: number | null = null,
|
|
978
|
+
) =>
|
|
979
|
+
ieeeFacts((f) => {
|
|
980
|
+
f.pages_text = [...pages];
|
|
981
|
+
f.bib_anchor_page = bib;
|
|
982
|
+
f.appendix_anchor_page = appendix;
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
/** References that start below the last body text on their page, then `more` pages of them. */
|
|
986
|
+
const refsFrom = (more: number): string[] => [
|
|
987
|
+
"the last lines of the conclusion\nR EFERENCES\n[1] D. E. Knuth.\n",
|
|
988
|
+
...Array.from({ length: more }, (_, i) => `[${String(i + 2)}] More.\n`),
|
|
989
|
+
];
|
|
990
|
+
|
|
991
|
+
describe("format/page-limit on MSR: ten pages of main text, appendices included, and two of only references", () => {
|
|
992
|
+
it("🔴 ten pages of body, the references starting on the tenth, and two pages of only references pass", () => {
|
|
993
|
+
expect(
|
|
994
|
+
lint({
|
|
995
|
+
venue: MSR,
|
|
996
|
+
facts: msrFacts([...bodyPages(9), ...refsFrom(2)], 10),
|
|
997
|
+
}),
|
|
998
|
+
).toEqual([]);
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
it("🔴 eleven pages of body fail", () => {
|
|
1002
|
+
const fs = lint({
|
|
1003
|
+
venue: MSR,
|
|
1004
|
+
facts: msrFacts([...bodyPages(10), ...refsFrom(2)], 11),
|
|
1005
|
+
});
|
|
1006
|
+
expect(fs.map((f) => f.message)).toEqual([
|
|
1007
|
+
"body pages (up to the references on page 11, appendices included): 11, over the limit 10 for MSR/technical — a desk reject; cut the text",
|
|
1008
|
+
]);
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
it("🔴 an appendix before the references that brings the body to eleven fails", () => {
|
|
1012
|
+
const pages = [...bodyPages(9), "Appendix A.\nDetails\n", ...refsFrom(2)];
|
|
1013
|
+
expect(ids(lint({ venue: MSR, facts: msrFacts(pages, 11, 10) }))).toEqual([
|
|
1014
|
+
"format/page-limit:pages",
|
|
1015
|
+
]);
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
it("🔴 an appendix after the references that brings the body to eleven fails", () => {
|
|
1019
|
+
const pages = [...bodyPages(9), ...refsFrom(1), "Appendix A.\nDetails\n"];
|
|
1020
|
+
const fs = lint({ venue: MSR, facts: msrFacts(pages, 10, 12) });
|
|
1021
|
+
expect(fs[0]?.message).toMatch(
|
|
1022
|
+
/^body pages \(up to the references on page 10, and the appendix from page 12\): 11, over the limit 10/,
|
|
1023
|
+
);
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
it("🔴 three pages of only references fail", () => {
|
|
1027
|
+
const fs = lint({
|
|
1028
|
+
venue: MSR,
|
|
1029
|
+
facts: msrFacts([...bodyPages(9), ...refsFrom(3)], 10),
|
|
1030
|
+
});
|
|
1031
|
+
expect(fs.map((f) => f.message)).toEqual([
|
|
1032
|
+
"pages holding only references: 3, over the limit 2 for MSR/technical — a desk reject; cut the text",
|
|
1033
|
+
]);
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
|
|
866
1037
|
/** An AIDC paper that declares who wrote it. */
|
|
867
1038
|
const BLIND = {
|
|
868
1039
|
extends: "paperlint:aidc",
|
package/src/venue-rules.ts
CHANGED
|
@@ -58,7 +58,12 @@ import { fieldOf } from "./domain/record.ts";
|
|
|
58
58
|
import type { Files } from "./ports/files.ts";
|
|
59
59
|
import { CONFIG_FILE } from "#lib/paper-config";
|
|
60
60
|
import { findLeaks, type Leak, type Searchable } from "./domain/anonymity.ts";
|
|
61
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
bodyEnd,
|
|
63
|
+
pageSplit,
|
|
64
|
+
type BodyEnd,
|
|
65
|
+
type PageSplit,
|
|
66
|
+
} from "./domain/body-pages.ts";
|
|
62
67
|
import type { TextFacts } from "./facts-file.ts";
|
|
63
68
|
|
|
64
69
|
// ── the verdict's vocabulary ─────────────────────────────────────────────────────────
|
|
@@ -261,9 +266,19 @@ export function judgeGeometry(
|
|
|
261
266
|
return out;
|
|
262
267
|
}
|
|
263
268
|
|
|
269
|
+
/**
|
|
270
|
+
* How many of banal's two counts — body, then references — a venue's counting replaces with its own
|
|
271
|
+
* from the text: none for banal's count, the body for `body_ends_at`, both with `appendix_in_body`.
|
|
272
|
+
*/
|
|
273
|
+
function countedFromText(format: VenueFormat): number {
|
|
274
|
+
if (format.bodyEndsAt !== "references") return 0;
|
|
275
|
+
return format.appendixInBody === true ? 2 : 1;
|
|
276
|
+
}
|
|
277
|
+
|
|
264
278
|
/**
|
|
265
279
|
* Body and reference pages, as banal classifies them, against the kind's limits. A venue that counts
|
|
266
|
-
* the body before the references is judged on its body by `judgeBodyEnd` instead
|
|
280
|
+
* the body before the references is judged on its body by `judgeBodyEnd` instead, and one that
|
|
281
|
+
* counts the appendix as body on both.
|
|
267
282
|
*/
|
|
268
283
|
export function judgePages(g: FlatGeometry, resolved: Resolved): Finding[] {
|
|
269
284
|
const kind = resolved.kind;
|
|
@@ -272,7 +287,7 @@ export function judgePages(g: FlatGeometry, resolved: Resolved): Finding[] {
|
|
|
272
287
|
[kind.limits.bodyPagesMax, g.body_pages, "body pages"],
|
|
273
288
|
[kind.limits.refPagesMax, g.ref_pages, "reference pages"],
|
|
274
289
|
];
|
|
275
|
-
const from = resolved.format
|
|
290
|
+
const from = countedFromText(resolved.format);
|
|
276
291
|
return pages.slice(from).flatMap(([max, got, what]) =>
|
|
277
292
|
max !== null && got > max
|
|
278
293
|
? [
|
|
@@ -300,13 +315,11 @@ export function judgeBodyEnd(
|
|
|
300
315
|
resolved: Resolved,
|
|
301
316
|
): readonly Finding[] {
|
|
302
317
|
const kind = resolved.kind;
|
|
303
|
-
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
)
|
|
309
|
-
return [];
|
|
318
|
+
if (resolved.format.bodyEndsAt !== "references" || kind === null) return [];
|
|
319
|
+
if (resolved.format.appendixInBody === true)
|
|
320
|
+
return judgeSplit(text, resolved, kind);
|
|
321
|
+
const max = kind.limits.bodyPagesMax;
|
|
322
|
+
if (max === null) return [];
|
|
310
323
|
const end = endOfBody(text);
|
|
311
324
|
const venue = resolved.venue;
|
|
312
325
|
switch (end.kind) {
|
|
@@ -324,6 +337,54 @@ export function judgeBodyEnd(
|
|
|
324
337
|
}
|
|
325
338
|
}
|
|
326
339
|
|
|
340
|
+
/** The findings of a split that was found: the body, and the pages holding only references. */
|
|
341
|
+
function splitFindings(
|
|
342
|
+
split: Extract<PageSplit, { readonly kind: "found" }>,
|
|
343
|
+
limits: KindLimits,
|
|
344
|
+
at: { readonly venue: string; readonly kind: string },
|
|
345
|
+
): readonly Finding[] {
|
|
346
|
+
const refs = `the references on page ${String(split.page)}`;
|
|
347
|
+
const body =
|
|
348
|
+
split.appendixFrom === null
|
|
349
|
+
? `body pages (up to ${refs}, appendices included)`
|
|
350
|
+
: `body pages (up to ${refs}, and the appendix from page ${String(split.appendixFrom)})`;
|
|
351
|
+
const counts: readonly [number | null, number, string][] = [
|
|
352
|
+
[limits.bodyPagesMax, split.bodyPages, body],
|
|
353
|
+
[limits.refPagesMax, split.refPages, "pages holding only references"],
|
|
354
|
+
];
|
|
355
|
+
return counts.flatMap(([max, got, what]) =>
|
|
356
|
+
max !== null && got > max
|
|
357
|
+
? [finding("pages", { what, got, max, ...at })]
|
|
358
|
+
: [],
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* The body, appendices included, and the pages holding only references (`appendix_in_body`),
|
|
364
|
+
* against the kind's two limits. Like `judgeBodyEnd`, an end it cannot find is said, not passed.
|
|
365
|
+
*/
|
|
366
|
+
function judgeSplit(
|
|
367
|
+
text: Pick<TextFacts, "pages" | "bibAnchorPage" | "appendixAnchorPage">,
|
|
368
|
+
resolved: Resolved,
|
|
369
|
+
kind: NonNullable<Resolved["kind"]>,
|
|
370
|
+
): readonly Finding[] {
|
|
371
|
+
const limits = kind.limits;
|
|
372
|
+
if (limits.bodyPagesMax === null && limits.refPagesMax === null) return [];
|
|
373
|
+
const split = pageSplit(text.pages, {
|
|
374
|
+
bib: text.bibAnchorPage,
|
|
375
|
+
appendix: text.appendixAnchorPage,
|
|
376
|
+
});
|
|
377
|
+
const venue = resolved.venue;
|
|
378
|
+
switch (split.kind) {
|
|
379
|
+
case "missing":
|
|
380
|
+
return [finding("noReferences", { venue })];
|
|
381
|
+
case "disagree":
|
|
382
|
+
return [finding("unclear", { venue, why: disagreement(split) })];
|
|
383
|
+
case "found":
|
|
384
|
+
return splitFindings(split, limits, { venue, kind: kind.name });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
327
388
|
/** Where the body ends in what the PDF says, and where hyperref anchored the two ends. */
|
|
328
389
|
const endOfBody = (
|
|
329
390
|
t: Pick<TextFacts, "pages" | "bibAnchorPage" | "appendixAnchorPage">,
|