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,660 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heading case, judged on real parses of the paper's `\title` and its `\section`-family headings:
|
|
3
|
+
* what each style asks of each word, and β as much β what it does not, because a finding at error
|
|
4
|
+
* level that is wrong is worse than a miss. Every title is parsed by the LaTeX adapter, judged, and
|
|
5
|
+
* the fixes are applied to the source, so what is asserted is the text an author would end up with.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { latexReader } from "../adapters/latex/index.ts";
|
|
9
|
+
import {
|
|
10
|
+
BOUND_PREFIXES,
|
|
11
|
+
judgeHeadingCase,
|
|
12
|
+
type CaseStyle,
|
|
13
|
+
type HeadingCaseOptions,
|
|
14
|
+
} from "./heading-case.ts";
|
|
15
|
+
|
|
16
|
+
/** One style for the title and every heading: the shape most venues state. */
|
|
17
|
+
const everywhere = (style: CaseStyle): HeadingCaseOptions => ({
|
|
18
|
+
title: style,
|
|
19
|
+
headings: style,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
type Asked = CaseStyle | HeadingCaseOptions;
|
|
23
|
+
const optionsOf = (asked: Asked): HeadingCaseOptions =>
|
|
24
|
+
typeof asked === "string" ? everywhere(asked) : asked;
|
|
25
|
+
|
|
26
|
+
const findingsOf = (src: string, asked: Asked = "chicago-headline") =>
|
|
27
|
+
judgeHeadingCase(latexReader.headings(src), optionsOf(asked), src);
|
|
28
|
+
|
|
29
|
+
/** The words a source's findings name, in order. */
|
|
30
|
+
const wordsOf = (src: string, asked: Asked = "chicago-headline"): string[] =>
|
|
31
|
+
findingsOf(src, asked).map((f) => String(f.data["word"]));
|
|
32
|
+
|
|
33
|
+
/** The source after every fix is applied, last first so offsets stay true. */
|
|
34
|
+
function fixed(src: string, asked: Asked = "chicago-headline"): string {
|
|
35
|
+
const fixes = findingsOf(src, asked)
|
|
36
|
+
.flatMap((f) => (f.fix === null ? [] : [f.fix]))
|
|
37
|
+
.toSorted((a, b) => b.span.start - a.span.start);
|
|
38
|
+
return fixes.reduce(
|
|
39
|
+
(text, f) => text.slice(0, f.span.start) + f.text + text.slice(f.span.end),
|
|
40
|
+
src,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const section = (title: string): string => `\\section{${title}}`;
|
|
45
|
+
|
|
46
|
+
describe("chicago-headline β sentence-case headings are reported and fixed", () => {
|
|
47
|
+
it.each([
|
|
48
|
+
["Related work", "Related Work"],
|
|
49
|
+
[
|
|
50
|
+
"The advertised savings don't show up",
|
|
51
|
+
"The Advertised Savings Don't Show Up",
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
"Method: a cost-aware, correctness-gated harness",
|
|
55
|
+
"Method: A Cost-Aware, Correctness-Gated Harness",
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
"A ceiling for any output-trimming tool",
|
|
59
|
+
"A Ceiling for Any Output-Trimming Tool",
|
|
60
|
+
],
|
|
61
|
+
[
|
|
62
|
+
"discussion and threats to validity",
|
|
63
|
+
"Discussion and Threats to Validity",
|
|
64
|
+
],
|
|
65
|
+
["The measurement pitfall", "The Measurement Pitfall"],
|
|
66
|
+
])("π΄ %s", (title, want) => {
|
|
67
|
+
const src = section(title);
|
|
68
|
+
expect(findingsOf(src).length).toBeGreaterThan(0);
|
|
69
|
+
expect(fixed(src)).toBe(section(want));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("names each word it reports, where it stands", () => {
|
|
73
|
+
const src = section("Related work");
|
|
74
|
+
const [f] = findingsOf(src);
|
|
75
|
+
expect(f?.data).toMatchObject({ word: "work", expected: "Work" });
|
|
76
|
+
expect(f?.at).toEqual({
|
|
77
|
+
start: src.indexOf("work"),
|
|
78
|
+
end: src.indexOf("}"),
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("chicago-headline β the corrected forms are silent", () => {
|
|
84
|
+
it.each([
|
|
85
|
+
"Related Work",
|
|
86
|
+
"The Advertised Savings Don't Show Up",
|
|
87
|
+
"Method: A Cost-Aware, Correctness-Gated Harness",
|
|
88
|
+
"A Ceiling for Any Output-Trimming Tool",
|
|
89
|
+
"Discussion and Threats to Validity",
|
|
90
|
+
"The Measurement Pitfall",
|
|
91
|
+
"Introduction",
|
|
92
|
+
"What Is It For",
|
|
93
|
+
"Is It Worth It?",
|
|
94
|
+
"Peer-to-Peer Verification of Claims",
|
|
95
|
+
"State-of-the-Art Baselines",
|
|
96
|
+
"Scaling Up the Pipeline",
|
|
97
|
+
])("π΄ %s", (title) => {
|
|
98
|
+
expect(findingsOf(section(title))).toEqual([]);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("chicago-headline β what the rule does not read", () => {
|
|
103
|
+
it.each([
|
|
104
|
+
["a bound prefix keeps its second element lowercase", "Multi-turn Agents"],
|
|
105
|
+
[
|
|
106
|
+
"β¦and may capitalise it: the rule cannot tell prefix from word",
|
|
107
|
+
"Multi-Turn Agents",
|
|
108
|
+
],
|
|
109
|
+
["non-", "Non-trivial Savings"],
|
|
110
|
+
["pre-", "Pre-trained Models for Review"],
|
|
111
|
+
["acronyms", "LLM Agents in CI"],
|
|
112
|
+
["an acronym with a plural", "LLMs and CIs"],
|
|
113
|
+
["mixed-case names", "GitHub and LaTeX in Practice"],
|
|
114
|
+
["a lowercase-first brand", "iOS Builds"],
|
|
115
|
+
["math", "Results for $k=3$ and $n$"],
|
|
116
|
+
["a word set in code", "Why \\texttt{grep} Fails"],
|
|
117
|
+
["numbers", "Top 10 Failures in 2026"],
|
|
118
|
+
["a numbered compound", "GPT-4 Judges"],
|
|
119
|
+
["a single-letter compound", "Choosing k for k-means"],
|
|
120
|
+
["a label after the title", "Related Work\\label{sec:rw}"],
|
|
121
|
+
["a footnote after the last word", "Related Work\\footnote{A note}"],
|
|
122
|
+
["a name particle", "Notes on von Neumann Machines"],
|
|
123
|
+
["a dotted abbreviation", "Pitfalls, e.g., Timeouts"],
|
|
124
|
+
["a slash compound", "Input/output Handling"],
|
|
125
|
+
["a title that is only math", "$O(n)$"],
|
|
126
|
+
["a capital letter that labels something", "Module A: Element Relation"],
|
|
127
|
+
["β¦and the same letter inside a compound", "Plan A-Prime Results"],
|
|
128
|
+
])("%s", (_why, title) => {
|
|
129
|
+
expect(findingsOf(section(title))).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("a group right after a macro the parser has no signature for is that macro's argument, not title words", () => {
|
|
133
|
+
expect(
|
|
134
|
+
findingsOf(section("Evaluating \\ours{some words} on Data")),
|
|
135
|
+
).toEqual([]);
|
|
136
|
+
expect(
|
|
137
|
+
findingsOf("\\subsubsection*{\\finding{label}{a sentence of words}}"),
|
|
138
|
+
).toEqual([]);
|
|
139
|
+
expect(wordsOf(section("related \\ours {work}"))).toEqual([
|
|
140
|
+
"related",
|
|
141
|
+
"work",
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("a comment inside a title says nothing, and a group may open it", () => {
|
|
146
|
+
expect(findingsOf("\\section{Related % note\nWork}")).toEqual([]);
|
|
147
|
+
expect(findingsOf(section("{GitHub} and Friends"))).toEqual([]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("emphasis commands are read as the words they wrap", () => {
|
|
151
|
+
expect(wordsOf(section("A \\emph{very} Good Idea"))).toEqual(["very"]);
|
|
152
|
+
expect(fixed(section("A \\emph{very} Good Idea"))).toBe(
|
|
153
|
+
section("A \\emph{Very} Good Idea"),
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("chicago-headline β words whose class is ambiguous are never reported", () => {
|
|
159
|
+
it.each([
|
|
160
|
+
"up",
|
|
161
|
+
"down",
|
|
162
|
+
"over",
|
|
163
|
+
"out",
|
|
164
|
+
"off",
|
|
165
|
+
"on",
|
|
166
|
+
"in",
|
|
167
|
+
"through",
|
|
168
|
+
"about",
|
|
169
|
+
"after",
|
|
170
|
+
"before",
|
|
171
|
+
"under",
|
|
172
|
+
"along",
|
|
173
|
+
"if",
|
|
174
|
+
"while",
|
|
175
|
+
"because",
|
|
176
|
+
"than",
|
|
177
|
+
"that",
|
|
178
|
+
"so",
|
|
179
|
+
"yet",
|
|
180
|
+
"how",
|
|
181
|
+
"using",
|
|
182
|
+
"following",
|
|
183
|
+
"including",
|
|
184
|
+
"given",
|
|
185
|
+
])("π΄ Β«%sΒ», mid-title, either case", (word) => {
|
|
186
|
+
const upper = word.charAt(0).toUpperCase() + word.slice(1);
|
|
187
|
+
expect(findingsOf(section(`Turning ${word} the Cache`))).toEqual([]);
|
|
188
|
+
expect(findingsOf(section(`Turning ${upper} the Cache`))).toEqual([]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("β¦but first, last and after a colon they are capitalised whatever they are", () => {
|
|
192
|
+
expect(fixed(section("in practice"))).toBe(section("In Practice"));
|
|
193
|
+
expect(fixed(section("Scaling up"))).toBe(section("Scaling Up"));
|
|
194
|
+
expect(fixed(section("Result: over the limit"))).toBe(
|
|
195
|
+
section("Result: Over the Limit"),
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("chicago-headline β what the publisher asks lowercase stays lowercase", () => {
|
|
201
|
+
it.each([
|
|
202
|
+
["The", "the"],
|
|
203
|
+
["And", "and"],
|
|
204
|
+
["Of", "of"],
|
|
205
|
+
["To", "to"],
|
|
206
|
+
["As", "as"],
|
|
207
|
+
["For", "for"],
|
|
208
|
+
["With", "with"],
|
|
209
|
+
["From", "from"],
|
|
210
|
+
["Between", "between"],
|
|
211
|
+
["Without", "without"],
|
|
212
|
+
["Nor", "nor"],
|
|
213
|
+
["But", "but"],
|
|
214
|
+
["An", "an"],
|
|
215
|
+
])("a capitalised Β«%sΒ» in the middle is reported", (word, lower) => {
|
|
216
|
+
const src = section(`Speed ${word} Safety`);
|
|
217
|
+
expect(findingsOf(src)).toHaveLength(1);
|
|
218
|
+
expect(fixed(src)).toBe(section(`Speed ${lower} Safety`));
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("β¦except where a capital is right or cannot be told from right", () => {
|
|
222
|
+
expect(findingsOf(section("To Measure and to Report"))).toEqual([]);
|
|
223
|
+
expect(findingsOf(section("What It Is For"))).toEqual([]);
|
|
224
|
+
expect(findingsOf(section("Method: The Harness"))).toEqual([]);
|
|
225
|
+
expect(findingsOf(section("Method --- The Harness"))).toEqual([]);
|
|
226
|
+
expect(findingsOf(section("Why? The Harness"))).toEqual([]);
|
|
227
|
+
expect(findingsOf(section("\\texttt{grep} And Friends"))).toEqual([]);
|
|
228
|
+
expect(findingsOf(section("Fast And $k$"))).toEqual([]);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("a lowercase minor word at the start, the end or after a colon is reported", () => {
|
|
232
|
+
expect(fixed(section("the Harness"))).toBe(section("The Harness"));
|
|
233
|
+
expect(fixed(section("Where to Look for"))).toBe(
|
|
234
|
+
section("Where to Look For"),
|
|
235
|
+
);
|
|
236
|
+
expect(fixed(section("Method: the Harness"))).toBe(
|
|
237
|
+
section("Method: The Harness"),
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("a minor word beside something the reader sees but the rule does not read is left alone", () => {
|
|
242
|
+
expect(findingsOf(section("\\texttt{x} for $y$"))).toEqual([]);
|
|
243
|
+
expect(findingsOf(section("Results for \\texttt{x}"))).toEqual([]);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("chicago-headline β hyphenated compounds", () => {
|
|
248
|
+
it.each([
|
|
249
|
+
["Cost-aware Gating", "Cost-Aware Gating"],
|
|
250
|
+
["cost-aware Gating", "Cost-Aware Gating"],
|
|
251
|
+
["Peer-to-peer Checks", "Peer-to-Peer Checks"],
|
|
252
|
+
["State-of-the-art Tools", "State-of-the-Art Tools"],
|
|
253
|
+
["Up-To-Date Checks", "Up-to-Date Checks"],
|
|
254
|
+
["Self-supervised Agents", "Self-Supervised Agents"],
|
|
255
|
+
["multi-turn Agents", "Multi-turn Agents"],
|
|
256
|
+
["Question-And-Answer Pairs", "Question-and-Answer Pairs"],
|
|
257
|
+
["GPU-accelerated Agents", "GPU-Accelerated Agents"],
|
|
258
|
+
])("π΄ %s", (title, want) => {
|
|
259
|
+
expect(fixed(section(title))).toBe(section(want));
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it.each([
|
|
263
|
+
"Built-in Checks",
|
|
264
|
+
"Built-In Checks",
|
|
265
|
+
"Re-use of Results",
|
|
266
|
+
"Co-design for Safety",
|
|
267
|
+
"Post-hoc Analysis",
|
|
268
|
+
"Anti-patterns in Review",
|
|
269
|
+
"Quasi-experiments in Review",
|
|
270
|
+
])("a bound prefix or an ambiguous second element: %s", (title) => {
|
|
271
|
+
expect(findingsOf(section(title))).toEqual([]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("the prefixes are the documented, small list", () => {
|
|
275
|
+
expect([...BOUND_PREFIXES]).toEqual([
|
|
276
|
+
"anti",
|
|
277
|
+
"co",
|
|
278
|
+
"de",
|
|
279
|
+
"inter",
|
|
280
|
+
"intra",
|
|
281
|
+
"multi",
|
|
282
|
+
"non",
|
|
283
|
+
"post",
|
|
284
|
+
"pre",
|
|
285
|
+
"pseudo",
|
|
286
|
+
"quasi",
|
|
287
|
+
"re",
|
|
288
|
+
"semi",
|
|
289
|
+
"sub",
|
|
290
|
+
"trans",
|
|
291
|
+
"ultra",
|
|
292
|
+
"un",
|
|
293
|
+
]);
|
|
294
|
+
expect(BOUND_PREFIXES.has("self")).toBe(false);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("chicago-headline β punctuation, dashes and quotes", () => {
|
|
299
|
+
it("a colon, a question mark and a full stop are read around, not into, the word", () => {
|
|
300
|
+
expect(fixed(section("Threats to validity."))).toBe(
|
|
301
|
+
section("Threats to Validity."),
|
|
302
|
+
);
|
|
303
|
+
expect(fixed(section("is it worth it?"))).toBe(section("Is It Worth It?"));
|
|
304
|
+
expect(fixed(section("(extended abstract)"))).toBe(
|
|
305
|
+
section("(Extended Abstract)"),
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("TeX quotes and apostrophes stay where they are", () => {
|
|
310
|
+
expect(fixed(section("The ``clean'' room's rules"))).toBe(
|
|
311
|
+
section("The ``Clean'' Room's Rules"),
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("dashes break the title: the next word is a start, either way for a minor word", () => {
|
|
316
|
+
expect(fixed(section("Fast --- the cost of safety"))).toBe(
|
|
317
|
+
section("Fast --- the Cost of Safety"),
|
|
318
|
+
);
|
|
319
|
+
expect(findingsOf(section("Fast--the Cost of Safety"))).toEqual([]);
|
|
320
|
+
expect(findingsOf(section("Fast - The Cost of Safety"))).toEqual([]);
|
|
321
|
+
expect(findingsOf(section("Fastβthe Cost of Safety"))).toEqual([]);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("a chunk the rule cannot read as a word is not judged, and is not first or last", () => {
|
|
325
|
+
expect(findingsOf(section("3 Ways to Fix It"))).toEqual([]);
|
|
326
|
+
expect(fixed(section("3 ways to Fix It"))).toBe(
|
|
327
|
+
section("3 Ways to Fix It"),
|
|
328
|
+
);
|
|
329
|
+
expect(findingsOf(section("Caf\\'e and Bar"))).toEqual([]);
|
|
330
|
+
expect(findingsOf(section("pre- and Post-processing"))).toEqual([]);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe("chicago-headline β which headings, which arguments", () => {
|
|
335
|
+
it.each([
|
|
336
|
+
"\\section",
|
|
337
|
+
"\\section*",
|
|
338
|
+
"\\subsection",
|
|
339
|
+
"\\subsection*",
|
|
340
|
+
"\\subsubsection",
|
|
341
|
+
"\\subsubsection*",
|
|
342
|
+
"\\paragraph",
|
|
343
|
+
"\\paragraph*",
|
|
344
|
+
"\\subparagraph",
|
|
345
|
+
"\\subparagraph*",
|
|
346
|
+
])("%s", (cmd) => {
|
|
347
|
+
expect(wordsOf(`${cmd}{related work}`)).toEqual(["related", "work"]);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("judges the short title as well as the title, and says which", () => {
|
|
351
|
+
const src = "\\subsection[short title]{Long Title}";
|
|
352
|
+
const found = findingsOf(src);
|
|
353
|
+
expect(found.map((f) => f.data["word"])).toEqual(["short", "title"]);
|
|
354
|
+
expect(found.map((f) => f.data["where"])).toEqual([
|
|
355
|
+
"\\subsection[β¦]",
|
|
356
|
+
"\\subsection[β¦]",
|
|
357
|
+
]);
|
|
358
|
+
expect(findingsOf("\\subsection{related}")[0]?.data["where"]).toBe(
|
|
359
|
+
"\\subsection",
|
|
360
|
+
);
|
|
361
|
+
expect(fixed(src)).toBe("\\subsection[Short Title]{Long Title}");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("does not read a heading in a comment, a definition or a comment environment", () => {
|
|
365
|
+
const src = [
|
|
366
|
+
"% \\section{related work}",
|
|
367
|
+
"\\newcommand{\\ours}{\\section{related work}}",
|
|
368
|
+
"\\begin{comment}\\section{related work}\\end{comment}",
|
|
369
|
+
"\\section{Related Work}",
|
|
370
|
+
].join("\n");
|
|
371
|
+
expect(findingsOf(src)).toEqual([]);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("does not read other commands: \\caption, \\textbf, \\subtitle", () => {
|
|
375
|
+
expect(findingsOf("\\caption{related work}")).toEqual([]);
|
|
376
|
+
expect(findingsOf("\\textbf{related work}")).toEqual([]);
|
|
377
|
+
expect(findingsOf("\\subtitle{related work}")).toEqual([]);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("an empty title, or one with only invisible parts, has nothing to judge", () => {
|
|
381
|
+
expect(findingsOf("\\section{}")).toEqual([]);
|
|
382
|
+
expect(findingsOf("\\section{\\label{x}}")).toEqual([]);
|
|
383
|
+
expect(findingsOf("\\section")).toEqual([]);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe("a fix is offered only where the source is the text", () => {
|
|
388
|
+
it("a word split by markup is reported, and not rewritten", () => {
|
|
389
|
+
const [f] = findingsOf(section("Fast \\emph{ca}che"));
|
|
390
|
+
expect(f?.data).toMatchObject({ word: "cache", expected: "Cache" });
|
|
391
|
+
expect(f?.fix).toBeNull();
|
|
392
|
+
expect(fixed(section("Fast \\emph{ca}che"))).toBe(
|
|
393
|
+
section("Fast \\emph{ca}che"),
|
|
394
|
+
);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("a minor word beside a command is not first or last, so it is left alone", () => {
|
|
398
|
+
expect(wordsOf(section("the \\texttt{x}"))).toEqual(["the"]);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe("sentence style", () => {
|
|
403
|
+
it("reports a lowercase first word and offers no fix: a brand cannot be told from a word", () => {
|
|
404
|
+
const [f] = findingsOf(section("related Work"), "sentence");
|
|
405
|
+
expect(f?.data).toMatchObject({ word: "related", expected: "Related" });
|
|
406
|
+
expect(f?.fix).toBeNull();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("is silent on everything else, because a proper noun cannot be told from a capital", () => {
|
|
410
|
+
expect(findingsOf(section("Related work"), "sentence")).toEqual([]);
|
|
411
|
+
expect(findingsOf(section("Related Work in Python"), "sentence")).toEqual(
|
|
412
|
+
[],
|
|
413
|
+
);
|
|
414
|
+
expect(
|
|
415
|
+
findingsOf(section("\\texttt{grep} and friends"), "sentence"),
|
|
416
|
+
).toEqual([]);
|
|
417
|
+
expect(findingsOf(section("3 ways to fix it"), "sentence")).toEqual([]);
|
|
418
|
+
expect(findingsOf(section("iOS builds"), "sentence")).toEqual([]);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe("any", () => {
|
|
423
|
+
it("judges nothing in the scope it is given", () => {
|
|
424
|
+
expect(findingsOf(section("related work and more"), "any")).toEqual([]);
|
|
425
|
+
expect(findingsOf("\\title{related work and more}", "any")).toEqual([]);
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
describe("the paper's title, \\title[short]{long}", () => {
|
|
430
|
+
const preamble = (title: string): string =>
|
|
431
|
+
`\\documentclass[sigconf]{acmart}\n${title}\n\\begin{document}\n\\maketitle\n\\end{document}\n`;
|
|
432
|
+
|
|
433
|
+
it("π΄ is judged in the preamble, at the place each word stands", () => {
|
|
434
|
+
const src = preamble("\\title{Measuring the wrong thing}");
|
|
435
|
+
const found = findingsOf(src);
|
|
436
|
+
expect(found.map((f) => f.data)).toEqual([
|
|
437
|
+
{
|
|
438
|
+
word: "wrong",
|
|
439
|
+
expected: "Wrong",
|
|
440
|
+
why: "nouns, pronouns, verbs, adjectives and adverbs are capitalized",
|
|
441
|
+
where: "\\title",
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
word: "thing",
|
|
445
|
+
expected: "Thing",
|
|
446
|
+
why: "the last word of a title is capitalized",
|
|
447
|
+
where: "\\title",
|
|
448
|
+
},
|
|
449
|
+
]);
|
|
450
|
+
// Guards: the span is the word in the preamble, not an offset into the body.
|
|
451
|
+
expect(found[0]?.at).toEqual({
|
|
452
|
+
start: src.indexOf("wrong"),
|
|
453
|
+
end: src.indexOf("wrong") + "wrong".length,
|
|
454
|
+
});
|
|
455
|
+
expect(fixed(src)).toBe(preamble("\\title{Measuring the Wrong Thing}"));
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("π΄ judges the short title and the long one, each saying which it is", () => {
|
|
459
|
+
const src = preamble("\\title[short form]{The Long Form}");
|
|
460
|
+
expect(
|
|
461
|
+
findingsOf(src).map((f) => [f.data["where"], f.data["word"]]),
|
|
462
|
+
).toEqual([
|
|
463
|
+
["\\title[β¦]", "short"],
|
|
464
|
+
["\\title[β¦]", "form"],
|
|
465
|
+
]);
|
|
466
|
+
expect(fixed(src)).toBe(preamble("\\title[Short Form]{The Long Form}"));
|
|
467
|
+
expect(wordsOf(preamble("\\title[Short Form]{the long form}"))).toEqual([
|
|
468
|
+
"the",
|
|
469
|
+
"long",
|
|
470
|
+
"form",
|
|
471
|
+
]);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("reads a title written over several lines, and fixes each word where it is", () => {
|
|
475
|
+
const src = preamble("\\title{Efficiency claims\n in agentic\n coding}");
|
|
476
|
+
expect(wordsOf(src)).toEqual(["claims", "agentic", "coding"]);
|
|
477
|
+
expect(fixed(src)).toBe(
|
|
478
|
+
preamble("\\title{Efficiency Claims\n in Agentic\n Coding}"),
|
|
479
|
+
);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
describe("the paper's title β what it does not read", () => {
|
|
484
|
+
const preamble = (title: string): string =>
|
|
485
|
+
`\\documentclass[sigconf]{acmart}\n${title}\n\\begin{document}\n\\maketitle\n\\end{document}\n`;
|
|
486
|
+
|
|
487
|
+
it("does not read code, math or a footnote in a title, as in a heading", () => {
|
|
488
|
+
expect(
|
|
489
|
+
findingsOf(preamble("\\title{Why \\texttt{grep} Fails for $k=3$}")),
|
|
490
|
+
).toEqual([]);
|
|
491
|
+
expect(
|
|
492
|
+
findingsOf(preamble("\\title{Measuring It Right\\thanks{a note}}")),
|
|
493
|
+
).toEqual([]);
|
|
494
|
+
expect(wordsOf(preamble("\\title{\\texttt{grep}: the tool}"))).toEqual([
|
|
495
|
+
"the",
|
|
496
|
+
"tool",
|
|
497
|
+
]);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("an empty title has nothing to judge", () => {
|
|
501
|
+
expect(findingsOf(preamble("\\title{}"))).toEqual([]);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it("a title in sentence style asks for its first word only, and is never fixed", () => {
|
|
505
|
+
const asked = { title: "sentence", headings: "any" } as const;
|
|
506
|
+
const [f] = findingsOf(preamble("\\title{measuring things}"), asked);
|
|
507
|
+
expect(f?.data).toEqual({
|
|
508
|
+
word: "measuring",
|
|
509
|
+
expected: "Measuring",
|
|
510
|
+
where: "\\title",
|
|
511
|
+
});
|
|
512
|
+
expect(f?.fix).toBeNull();
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
describe("which style each title and heading gets", () => {
|
|
517
|
+
const src = [
|
|
518
|
+
"\\title{measuring the thing}",
|
|
519
|
+
"\\section{related work}",
|
|
520
|
+
"\\subsection{related work}",
|
|
521
|
+
"\\paragraph{related work}",
|
|
522
|
+
"\\subparagraph{related work}",
|
|
523
|
+
].join("\n");
|
|
524
|
+
const judged = (asked: HeadingCaseOptions): string[] =>
|
|
525
|
+
findingsOf(src, asked).map(
|
|
526
|
+
(f) => `${String(f.data["where"])} ${String(f.data["word"])}`,
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
it("π΄ the title follows `title`, every heading follows `headings`", () => {
|
|
530
|
+
expect(judged({ title: "chicago-headline", headings: "any" })).toEqual([
|
|
531
|
+
"\\title measuring",
|
|
532
|
+
"\\title thing",
|
|
533
|
+
]);
|
|
534
|
+
expect(judged({ title: "any", headings: "sentence" })).toEqual([
|
|
535
|
+
"\\section related",
|
|
536
|
+
"\\subsection related",
|
|
537
|
+
"\\paragraph related",
|
|
538
|
+
"\\subparagraph related",
|
|
539
|
+
]);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("π΄ a level named in `levels` follows its own style instead of `headings`", () => {
|
|
543
|
+
expect(
|
|
544
|
+
judged({
|
|
545
|
+
title: "any",
|
|
546
|
+
headings: "chicago-headline",
|
|
547
|
+
levels: { paragraph: "sentence", subparagraph: "any" },
|
|
548
|
+
}),
|
|
549
|
+
).toEqual([
|
|
550
|
+
"\\section related",
|
|
551
|
+
"\\section work",
|
|
552
|
+
"\\subsection related",
|
|
553
|
+
"\\subsection work",
|
|
554
|
+
"\\paragraph related",
|
|
555
|
+
]);
|
|
556
|
+
expect(
|
|
557
|
+
judged({
|
|
558
|
+
title: "any",
|
|
559
|
+
headings: "any",
|
|
560
|
+
levels: { section: "chicago-headline" },
|
|
561
|
+
}),
|
|
562
|
+
).toEqual(["\\section related", "\\section work"]);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it("`levels` does not reach the title", () => {
|
|
566
|
+
expect(
|
|
567
|
+
judged({ title: "any", headings: "any", levels: { section: "any" } }),
|
|
568
|
+
).toEqual([]);
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
describe("chicago-headline β a title in parts: a line break, a colon, a question, a dash", () => {
|
|
573
|
+
it.each([
|
|
574
|
+
"Agents and Tools:\\\\ The Empirical Study",
|
|
575
|
+
"Agents and Tools: \\\\ An Empirical Study",
|
|
576
|
+
"Agents and Tools:\\newline The Study",
|
|
577
|
+
"People \\\\[1ex] For the People",
|
|
578
|
+
"Main Title\\\\ The Subtitle",
|
|
579
|
+
"Main Title \\linebreak[3] The Subtitle",
|
|
580
|
+
])(
|
|
581
|
+
"π΄ a minor word after a line break is a start, not reported: %s",
|
|
582
|
+
(title) => {
|
|
583
|
+
expect(findingsOf(section(title))).toEqual([]);
|
|
584
|
+
},
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
it("β¦and a lowercase major word after it still is", () => {
|
|
588
|
+
expect(
|
|
589
|
+
wordsOf(section("Agents and Tools:\\\\ the empirical Study")),
|
|
590
|
+
).toEqual(["empirical"]);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it.each([
|
|
594
|
+
"What Is It Good For: An Analysis",
|
|
595
|
+
"What Is It Good for: An Analysis",
|
|
596
|
+
"Something to Look Out For: Bugs in CI",
|
|
597
|
+
"Agents to Talk To --- A Study",
|
|
598
|
+
"Agents to Talk to --- A Study",
|
|
599
|
+
"Who Are We Talking To? A Study",
|
|
600
|
+
"Who Are We Talking to? A Study",
|
|
601
|
+
"Worth Fighting For! A Study",
|
|
602
|
+
])(
|
|
603
|
+
"π΄ the last word of a part (before : ? ! or a dash) is not asked either way: %s",
|
|
604
|
+
(title) => {
|
|
605
|
+
expect(findingsOf(section(title))).toEqual([]);
|
|
606
|
+
},
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
it("a minor word right beside something the rule does not read is left as it is", () => {
|
|
610
|
+
expect(findingsOf(section("Fast And \\cite{x} Safe"))).toEqual([]);
|
|
611
|
+
expect(findingsOf(section("Fast \\cite{x} And Safe"))).toEqual([]);
|
|
612
|
+
expect(findingsOf(section("Speed With Safety"))).toHaveLength(1);
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
describe("chicago-headline β abbreviations, comments, ligatures", () => {
|
|
617
|
+
it.each([
|
|
618
|
+
"Replicating Smith et al. on New Data",
|
|
619
|
+
"Replicating Smith et al on New Data",
|
|
620
|
+
"Tools, etc. and More",
|
|
621
|
+
"Results, cf. Prior Work",
|
|
622
|
+
"Results, i.e. Prior Work",
|
|
623
|
+
"Speed vs. Safety",
|
|
624
|
+
"An approx. Answer",
|
|
625
|
+
"Tools, Fonts, etc.",
|
|
626
|
+
])("π΄ never read, never fixed: %s", (title) => {
|
|
627
|
+
expect(findingsOf(section(title))).toEqual([]);
|
|
628
|
+
expect(fixed(section(title))).toBe(section(title));
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("a word followed by a full stop inside the title is not read; the last word still is", () => {
|
|
632
|
+
expect(findingsOf(section("Fast. Then Slow"))).toEqual([]);
|
|
633
|
+
expect(fixed(section("Threats to validity."))).toBe(
|
|
634
|
+
section("Threats to Validity."),
|
|
635
|
+
);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it("π΄ a % comment inside a title separates the words around it", () => {
|
|
639
|
+
const src = "\\title{Agents for % a comment\nthe Masses}";
|
|
640
|
+
expect(wordsOf(src)).toEqual([]);
|
|
641
|
+
expect(wordsOf("\\title{Agents for % a comment\nthe masses}")).toEqual([
|
|
642
|
+
"masses",
|
|
643
|
+
]);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it("π΄ a word starting with a ligature is not read: its capital is two letters", () => {
|
|
647
|
+
expect(findingsOf(section("The \uFB01nding"))).toEqual([]);
|
|
648
|
+
expect(fixed(section("The \uFB01nding"))).toBe(section("The \uFB01nding"));
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
describe("\\iffalse β¦ \\fi", () => {
|
|
653
|
+
it("π΄ a title or heading TeX never reads is not judged", () => {
|
|
654
|
+
expect(
|
|
655
|
+
findingsOf(
|
|
656
|
+
"\\iffalse\n\\title{old dead title}\n\\section{old dead}\n\\fi",
|
|
657
|
+
),
|
|
658
|
+
).toEqual([]);
|
|
659
|
+
});
|
|
660
|
+
});
|