paperlint 6.2.3 → 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 +10 -3
- package/dist/adapters/hotcrp/index.d.ts +7 -0
- package/dist/adapters/hotcrp/index.d.ts.map +1 -0
- package/dist/adapters/hotcrp/index.js +7 -0
- package/dist/adapters/hotcrp/index.js.map +1 -0
- package/dist/adapters/hotcrp/portal.io.d.ts +20 -0
- package/dist/adapters/hotcrp/portal.io.d.ts.map +1 -0
- package/dist/adapters/hotcrp/portal.io.js +90 -0
- package/dist/adapters/hotcrp/portal.io.js.map +1 -0
- package/dist/adapters/hotcrp/response.d.ts +13 -0
- package/dist/adapters/hotcrp/response.d.ts.map +1 -0
- package/dist/adapters/hotcrp/response.js +138 -0
- package/dist/adapters/hotcrp/response.js.map +1 -0
- 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 +4 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +126 -37
- 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/result.d.ts +4 -0
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +2 -0
- package/dist/domain/result.js.map +1 -1
- package/dist/domain/submission.d.ts +106 -0
- package/dist/domain/submission.d.ts.map +1 -0
- package/dist/domain/submission.js +39 -0
- package/dist/domain/submission.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/domain/venue-name.d.ts +38 -0
- package/dist/domain/venue-name.d.ts.map +1 -0
- package/dist/domain/venue-name.js +71 -0
- package/dist/domain/venue-name.js.map +1 -0
- 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.d.ts +51 -0
- package/dist/folder-venue-rule.d.ts.map +1 -0
- package/dist/folder-venue-rule.js +96 -0
- package/dist/folder-venue-rule.js.map +1 -0
- 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/init.d.ts +6 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -3
- package/dist/init.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +5 -1
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/new-paper.d.ts +19 -5
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +23 -4
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-settings.d.ts +3 -0
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +20 -4
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/latex.d.ts +7 -1
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/ports/submission-portal.d.ts +19 -0
- package/dist/ports/submission-portal.d.ts.map +1 -0
- package/dist/ports/submission-portal.js +2 -0
- package/dist/ports/submission-portal.js.map +1 -0
- 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 +43 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +130 -63
- package/dist/presets.js.map +1 -1
- package/dist/rules-config.js +1 -1
- package/dist/submission.d.ts +61 -0
- package/dist/submission.d.ts.map +1 -0
- package/dist/submission.js +211 -0
- package/dist/submission.js.map +1 -0
- package/dist/tex-requirements.d.ts +36 -3
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +30 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +7 -8
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +10 -14
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/configuration.md +30 -11
- package/docs/optional-rules.md +4 -3
- package/docs/rules/format/page-limit.md +1 -1
- package/docs/rules/paper/folder-venue-leftover.md +104 -0
- package/docs/rules/talk/captions-cover.md +1 -1
- package/docs/rules/talk/duration-floor.md +1 -1
- package/docs/rules/talk/profile.md +1 -1
- package/docs/rules/tex/heading-case.md +424 -0
- package/docs/rules.md +64 -53
- package/docs/skills.md +5 -4
- package/docs/submission.md +64 -0
- package/docs/talk.md +1 -1
- 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/paper-config.ts +6 -1
- package/lib/skill-checks.d.mts +7 -0
- package/lib/skill-checks.mjs +31 -0
- package/lib/skill-trigger-cases.mjs +11 -0
- package/package.json +8 -2
- 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 +12 -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 +57 -2
- 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/paper-pipeline.harness.mjs +4 -0
- 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/submission-portal/SKILL.md +69 -0
- package/skills/submission-portal/SKILL.md.spec.ts +71 -0
- package/skills/submission-portal/submission-portal.eval.mjs +15 -0
- package/skills/submission-portal/submission-portal.test.ts +81 -0
- package/skills/submit-paper/SKILL.md +27 -22
- package/skills/submit-paper/SKILL.md.spec.ts +26 -21
- package/skills/submit-paper/references/publishers/acm.md +1 -1
- package/src/adapters/hotcrp/cassette/README.md +24 -0
- package/src/adapters/hotcrp/cassette/get-noauth.json +10 -0
- package/src/adapters/hotcrp/cassette/get-paper.json +39 -0
- package/src/adapters/hotcrp/cassette/post-dot.json +21 -0
- package/src/adapters/hotcrp/cassette/post-ok.json +12 -0
- package/src/adapters/hotcrp/index.ts +11 -0
- package/src/adapters/hotcrp/portal.io.ts +130 -0
- package/src/adapters/hotcrp/portal.test.ts +503 -0
- package/src/adapters/hotcrp/response.ts +181 -0
- 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 +155 -3
- package/src/cli.ts +161 -25
- package/src/domain/heading-case.test.ts +660 -0
- package/src/domain/heading-case.ts +538 -0
- package/src/domain/result.test.ts +9 -1
- package/src/domain/result.ts +5 -0
- package/src/domain/submission.test.ts +45 -0
- package/src/domain/submission.ts +141 -0
- package/src/domain/tex-document.ts +36 -0
- package/src/domain/venue-name.test.ts +121 -0
- package/src/domain/venue-name.ts +129 -0
- package/src/facts-file.test.ts +4 -4
- package/src/facts-file.ts +6 -6
- package/src/folder-venue-rule.test.ts +182 -0
- package/src/folder-venue-rule.ts +141 -0
- package/src/heading-case.test.ts +500 -0
- package/src/heading-case.ts +275 -0
- package/src/init.test.ts +22 -0
- package/src/init.ts +9 -2
- package/src/new-paper.harness.mjs +11 -9
- package/src/new-paper.test.ts +135 -10
- package/src/new-paper.ts +38 -7
- package/src/paper-settings-commands.test.ts +25 -10
- package/src/paper-settings.test.ts +12 -1
- package/src/paper-settings.ts +25 -5
- package/src/parse-args.test.ts +19 -0
- package/src/ports/latex.ts +7 -0
- package/src/ports/submission-portal.ts +26 -0
- package/src/preset-card-rule.test.ts +266 -0
- package/src/preset-card-rule.ts +384 -0
- package/src/presets.test.ts +212 -37
- package/src/presets.ts +183 -71
- package/src/rule-cases.test.ts +147 -42
- package/src/rules-config.ts +1 -1
- package/src/submission.test.ts +497 -0
- package/src/submission.ts +311 -0
- 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 +78 -8
- package/src/tex-venue-rules.test.ts +36 -22
- package/src/tex-venue-rules.ts +14 -26
- package/src/types.ts +13 -0
- 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
package/src/new-paper.ts
CHANGED
|
@@ -27,6 +27,11 @@ import { join } from "node:path";
|
|
|
27
27
|
import { fileURLToPath } from "node:url";
|
|
28
28
|
import { CONFIG_FILE } from "#lib/paper-config";
|
|
29
29
|
import { isRecord } from "./domain/record.ts";
|
|
30
|
+
import {
|
|
31
|
+
venuesNamedBy,
|
|
32
|
+
withoutVenues,
|
|
33
|
+
type NamedVenue,
|
|
34
|
+
} from "./domain/venue-name.ts";
|
|
30
35
|
|
|
31
36
|
export type PaperFormat = "tex" | "md";
|
|
32
37
|
export const FORMATS: readonly PaperFormat[] = ["tex", "md"];
|
|
@@ -50,6 +55,9 @@ export const SOURCE_FILE: Readonly<Record<PaperFormat, string>> = {
|
|
|
50
55
|
md: "paper.md",
|
|
51
56
|
};
|
|
52
57
|
|
|
58
|
+
/** The flag that lets `paperlint new` create a folder whose name names a venue. */
|
|
59
|
+
export const ALLOW_VENUE_NAME = "--allow-venue-name";
|
|
60
|
+
|
|
53
61
|
export const isFormat = (v: unknown): v is PaperFormat =>
|
|
54
62
|
FORMATS.some((f) => f === v);
|
|
55
63
|
|
|
@@ -57,13 +65,30 @@ export const isFormat = (v: unknown): v is PaperFormat =>
|
|
|
57
65
|
* Why a name is refused, or null. `[a-z0-9._-]+` is the charset; a LEADING dot is refused on top
|
|
58
66
|
* of it, because discovery skips dot-directories — a paper named `.x` would be created and then
|
|
59
67
|
* never linted, which is worse than a refusal.
|
|
68
|
+
*
|
|
69
|
+
* A name that names a venue — one of `venues`' names as whole words (`aisec-2026`) — is refused
|
|
70
|
+
* too, the paper's own venue included: a paper that is rejected goes to another venue and keeps its
|
|
71
|
+
* folder, so the name goes stale, and nothing at creation time knows the paper will not move. A
|
|
72
|
+
* folder made by hand, and one made with `--allow-venue-name` (a whole word can be the work's own:
|
|
73
|
+
* a paper about realms), is what `paper/folder-venue-leftover` judges once the venue changes. The
|
|
74
|
+
* flag passes no `venues`, so it skips this refusal and no other.
|
|
60
75
|
*/
|
|
61
|
-
export function nameProblem(
|
|
76
|
+
export function nameProblem(
|
|
77
|
+
name: string,
|
|
78
|
+
venues: readonly NamedVenue[],
|
|
79
|
+
): string | null {
|
|
62
80
|
if (!/^[a-z0-9._-]+$/.test(name))
|
|
63
81
|
return `\`${name}\` — a paper name may hold only a-z, 0-9, dot, underscore and hyphen (it becomes a path and a shell argument)`;
|
|
64
82
|
if (name.startsWith("."))
|
|
65
83
|
return `\`${name}\` — a name starting with a dot is skipped by paper discovery, so it would never be linted`;
|
|
66
|
-
|
|
84
|
+
const named = venuesNamedBy(name, venues);
|
|
85
|
+
if (named.length === 0) return null;
|
|
86
|
+
const which = named.map((n) => `"${n.name}"`).join(", ");
|
|
87
|
+
const instead = withoutVenues(name, venues);
|
|
88
|
+
return [
|
|
89
|
+
`\`${name}\` — the paper folder names a venue (${which}); venues change on resubmission, so name it after the work${instead === null ? "" : ` (e.g. "${instead}")`}`,
|
|
90
|
+
` if the venue really belongs in the name, rerun \`paperlint new\` with ${ALLOW_VENUE_NAME}`,
|
|
91
|
+
].join("\n");
|
|
67
92
|
}
|
|
68
93
|
|
|
69
94
|
export interface FileOutcome {
|
|
@@ -180,17 +205,23 @@ function fromTemplate(
|
|
|
180
205
|
: edited;
|
|
181
206
|
}
|
|
182
207
|
|
|
208
|
+
/** What `newPaper` is told besides the name: the venues a name may not name, and what to write. */
|
|
209
|
+
export interface NewPaperOptions {
|
|
210
|
+
readonly packageTemplates?: string;
|
|
211
|
+
readonly venue?: VenueSetting | null;
|
|
212
|
+
/** Every venue paperlint knows (`shippedVenueNames`): a name naming one is refused. */
|
|
213
|
+
readonly venues: readonly NamedVenue[];
|
|
214
|
+
}
|
|
215
|
+
|
|
183
216
|
// Documented in README.md#getting-started — update it when this changes.
|
|
184
217
|
export function newPaper(
|
|
185
218
|
papersRoot: string,
|
|
186
219
|
name: string,
|
|
187
220
|
format: PaperFormat,
|
|
188
|
-
|
|
189
|
-
packageTemplates = PACKAGE_TEMPLATES,
|
|
190
|
-
venue = null,
|
|
191
|
-
}: { packageTemplates?: string; venue?: VenueSetting | null } = {},
|
|
221
|
+
opts: NewPaperOptions,
|
|
192
222
|
): NewPaperResult {
|
|
193
|
-
const
|
|
223
|
+
const { packageTemplates = PACKAGE_TEMPLATES, venue = null } = opts;
|
|
224
|
+
const problem = nameProblem(name, opts.venues);
|
|
194
225
|
if (problem) return { ok: false, reason: problem };
|
|
195
226
|
const dir = join(papersRoot, name);
|
|
196
227
|
const fresh = !existsSync(dir);
|
|
@@ -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
|
}),
|
|
@@ -32,6 +32,7 @@ describe("parsePaperSettings", () => {
|
|
|
32
32
|
rules: null,
|
|
33
33
|
identity: null,
|
|
34
34
|
talk: null,
|
|
35
|
+
submission: null,
|
|
35
36
|
},
|
|
36
37
|
});
|
|
37
38
|
expect(parsePaperSettings({})).toEqual({
|
|
@@ -43,6 +44,7 @@ describe("parsePaperSettings", () => {
|
|
|
43
44
|
rules: null,
|
|
44
45
|
identity: null,
|
|
45
46
|
talk: null,
|
|
47
|
+
submission: null,
|
|
46
48
|
},
|
|
47
49
|
});
|
|
48
50
|
});
|
|
@@ -57,6 +59,7 @@ describe("parsePaperSettings", () => {
|
|
|
57
59
|
rules: null,
|
|
58
60
|
identity: null,
|
|
59
61
|
talk: null,
|
|
62
|
+
submission: null,
|
|
60
63
|
},
|
|
61
64
|
});
|
|
62
65
|
});
|
|
@@ -155,7 +158,13 @@ describe("readPaperSettings — a root paperlint.json that cannot be used", () =
|
|
|
155
158
|
});
|
|
156
159
|
|
|
157
160
|
/** The fields a case does not set. */
|
|
158
|
-
const UNSET = {
|
|
161
|
+
const UNSET = {
|
|
162
|
+
pdf: null,
|
|
163
|
+
rules: null,
|
|
164
|
+
identity: null,
|
|
165
|
+
talk: null,
|
|
166
|
+
submission: null,
|
|
167
|
+
};
|
|
159
168
|
|
|
160
169
|
describe("readPaperSettings — the root paperlint.json's defaults, the paper's file over them", () => {
|
|
161
170
|
const ROOT = "/work";
|
|
@@ -210,6 +219,7 @@ describe("readPaperSettings — the root paperlint.json's defaults, the paper's
|
|
|
210
219
|
rules: { "pdf/fonts": "off" },
|
|
211
220
|
identity: null,
|
|
212
221
|
talk: null,
|
|
222
|
+
submission: null,
|
|
213
223
|
});
|
|
214
224
|
});
|
|
215
225
|
|
|
@@ -253,6 +263,7 @@ describe("paperRules — `rules` in paperlint.json", () => {
|
|
|
253
263
|
rules,
|
|
254
264
|
identity: null,
|
|
255
265
|
talk: null,
|
|
266
|
+
submission: null,
|
|
256
267
|
});
|
|
257
268
|
|
|
258
269
|
it("no rules: none", () => {
|
package/src/paper-settings.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* One file name at two levels, one schema (lib/paper-config.mjs → SETTINGS_KEYS):
|
|
5
5
|
*
|
|
6
6
|
* paperlint.json the project (optional): papersDir, rules, defaults
|
|
7
|
-
* <paper>/paperlint.json this paper: { extends, kind, pdf, rules }
|
|
7
|
+
* <paper>/paperlint.json this paper: { extends, kind, pdf, rules, identity, talk, submission }
|
|
8
8
|
* a venue preset `paperlint:<name>` (shipped) or `./x.jsonc` (src/presets.ts)
|
|
9
9
|
*
|
|
10
10
|
* The paper's `extends`, `kind` and `pdf` win over the root's; with no own value, the root's is
|
|
@@ -32,6 +32,10 @@ import {
|
|
|
32
32
|
} from "./rules-config.ts";
|
|
33
33
|
import { messageOf } from "./domain/text.ts";
|
|
34
34
|
import { parsePaperTalk, type PaperTalk } from "./domain/talk.ts";
|
|
35
|
+
import {
|
|
36
|
+
parsePaperSubmission,
|
|
37
|
+
type PaperSubmission,
|
|
38
|
+
} from "./domain/submission.ts";
|
|
35
39
|
|
|
36
40
|
/** What one paper declares. Every absent field is null. */
|
|
37
41
|
export interface PaperSettings {
|
|
@@ -54,6 +58,8 @@ export interface PaperSettings {
|
|
|
54
58
|
readonly identity: readonly string[] | null;
|
|
55
59
|
/** How this paper is presented (`talk/*`); null when it declares no talk. Never the root's. */
|
|
56
60
|
readonly talk: PaperTalk | null;
|
|
61
|
+
/** Which submission on the venue's portal is this paper's; null when it says none. Never the root's. */
|
|
62
|
+
readonly submission: PaperSubmission | null;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
/** Why a paper's settings cannot be read. */
|
|
@@ -112,17 +118,25 @@ export function identityField(
|
|
|
112
118
|
);
|
|
113
119
|
}
|
|
114
120
|
|
|
115
|
-
/** `rules` (checked for shape only) and `
|
|
121
|
+
/** `rules` (checked for shape only), `talk` and `submission`, each null when absent. */
|
|
116
122
|
function rulesAndTalk(
|
|
117
123
|
json: Readonly<Record<string, unknown>>,
|
|
118
|
-
): Result<Pick<PaperSettings, "rules" | "talk">, string> {
|
|
124
|
+
): Result<Pick<PaperSettings, "rules" | "talk" | "submission">, string> {
|
|
119
125
|
const rules = json["rules"];
|
|
120
126
|
if (rules !== undefined && !isObject(rules) && !Array.isArray(rules))
|
|
121
127
|
return err(
|
|
122
128
|
`"rules" must be an object of rule id → severity, or a list of ESLint blocks`,
|
|
123
129
|
);
|
|
124
130
|
const talk = parsePaperTalk(json["talk"]);
|
|
125
|
-
|
|
131
|
+
if (!talk.ok) return talk;
|
|
132
|
+
const submission = parsePaperSubmission(json["submission"]);
|
|
133
|
+
return submission.ok
|
|
134
|
+
? ok({
|
|
135
|
+
rules: rules ?? null,
|
|
136
|
+
talk: talk.value,
|
|
137
|
+
submission: submission.value,
|
|
138
|
+
})
|
|
139
|
+
: submission;
|
|
126
140
|
}
|
|
127
141
|
|
|
128
142
|
/** The parsed JSON of `paperlint.json` → the settings, or one line saying what is wrong. Pure. */
|
|
@@ -232,7 +246,12 @@ function merge(
|
|
|
232
246
|
paper: PaperSettings | null,
|
|
233
247
|
): PaperSettings | null {
|
|
234
248
|
const r = root ?? NO_DEFAULTS;
|
|
235
|
-
const p = paper ?? {
|
|
249
|
+
const p = paper ?? {
|
|
250
|
+
...NO_DEFAULTS,
|
|
251
|
+
rules: null,
|
|
252
|
+
talk: null,
|
|
253
|
+
submission: null,
|
|
254
|
+
};
|
|
236
255
|
const merged: PaperSettings = {
|
|
237
256
|
extends: p.extends ?? r.extends,
|
|
238
257
|
kind: p.kind ?? r.kind,
|
|
@@ -240,6 +259,7 @@ function merge(
|
|
|
240
259
|
rules: p.rules,
|
|
241
260
|
identity: joined(r.identity, p.identity),
|
|
242
261
|
talk: p.talk,
|
|
262
|
+
submission: p.submission,
|
|
243
263
|
};
|
|
244
264
|
return paper === null && Object.values(merged).every((v) => v === null)
|
|
245
265
|
? null
|
package/src/parse-args.test.ts
CHANGED
|
@@ -35,6 +35,25 @@ describe("parseArgs — `--flag=value` is the same as `--flag value`", () => {
|
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
describe("parseArgs — the switches, each turning one field on", () => {
|
|
39
|
+
it.each([
|
|
40
|
+
["--json", "json"],
|
|
41
|
+
["--fix", "fix"],
|
|
42
|
+
["--all", "all"],
|
|
43
|
+
["--dry-run", "dryRun"],
|
|
44
|
+
["--check", "check"],
|
|
45
|
+
["--yes", "yes"],
|
|
46
|
+
["-y", "yes"],
|
|
47
|
+
["--no-hooks", "noHooks"],
|
|
48
|
+
["--allow-venue-name", "allowVenueName"],
|
|
49
|
+
] as const)("%s → %s", (flag, key) => {
|
|
50
|
+
const off = parseArgs(["new", "x"]);
|
|
51
|
+
const on = parseArgs(["new", "x", flag]);
|
|
52
|
+
expect(off[key]).toBe(false);
|
|
53
|
+
expect(on).toEqual({ ...off, [key]: true });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
38
57
|
describe("an unknown flag is refused, not read as a path", () => {
|
|
39
58
|
it("exits 2 and names the flag", async () => {
|
|
40
59
|
const err: string[] = [];
|
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
|
/**
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A conference portal holding a paper's submission — the port `paperlint submission` talks through.
|
|
3
|
+
* Shaped by what the command needs (read one submission, send one change), not by any portal's API;
|
|
4
|
+
* one adapter per portal kind (`src/adapters/hotcrp/`).
|
|
5
|
+
*/
|
|
6
|
+
import type { Result } from "../domain/result.ts";
|
|
7
|
+
import type {
|
|
8
|
+
PortalFailure,
|
|
9
|
+
SubmissionChange,
|
|
10
|
+
SubmissionView,
|
|
11
|
+
UpdateOutcome,
|
|
12
|
+
} from "../domain/submission.ts";
|
|
13
|
+
|
|
14
|
+
export interface SubmissionPortal {
|
|
15
|
+
/** The submission as the portal holds it now. */
|
|
16
|
+
show(id: number): Promise<Result<SubmissionView, PortalFailure>>;
|
|
17
|
+
/**
|
|
18
|
+
* Send a change. Without `save` the portal only checks it (a dry run) and keeps nothing; an
|
|
19
|
+
* answer the portal gave — valid or not — is an outcome, not a failure.
|
|
20
|
+
*/
|
|
21
|
+
update(
|
|
22
|
+
id: number,
|
|
23
|
+
change: SubmissionChange,
|
|
24
|
+
o: { readonly save: boolean },
|
|
25
|
+
): Promise<Result<UpdateOutcome, PortalFailure>>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `preset/card-rules` through real ESLint, on a copy of the shipped presets and the rule pages: the
|
|
3
|
+
* section a card must hold, the findings for each way a card can be wrong, and what `--fix` writes.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
cpSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { ESLint } from "eslint";
|
|
14
|
+
import markdown from "@eslint/markdown";
|
|
15
|
+
import { describe, expect, it } from "vitest";
|
|
16
|
+
import { nodeFiles } from "./adapters/node/index.ts";
|
|
17
|
+
import { eslintConfig } from "./cli.ts";
|
|
18
|
+
import { presetsDir } from "./package-dirs.ts";
|
|
19
|
+
import {
|
|
20
|
+
entryParts,
|
|
21
|
+
presetCardRules,
|
|
22
|
+
SECTION_END,
|
|
23
|
+
SECTION_START,
|
|
24
|
+
} from "./preset-card-rule.ts";
|
|
25
|
+
import { useTempDir } from "../test/support.ts";
|
|
26
|
+
|
|
27
|
+
const REPO = join(presetsDir(), "..");
|
|
28
|
+
const root = useTempDir("paperlint-card-rule-");
|
|
29
|
+
cpSync(presetsDir(), join(root, "presets"), { recursive: true });
|
|
30
|
+
cpSync(join(REPO, "docs", "rules"), join(root, "docs", "rules"), {
|
|
31
|
+
recursive: true,
|
|
32
|
+
});
|
|
33
|
+
cpSync(join(REPO, "docs", "rules.md"), join(root, "docs", "rules.md"));
|
|
34
|
+
const P = join(root, "presets");
|
|
35
|
+
|
|
36
|
+
const eslint = (fix = false) =>
|
|
37
|
+
new ESLint({
|
|
38
|
+
cwd: root,
|
|
39
|
+
fix,
|
|
40
|
+
overrideConfigFile: true,
|
|
41
|
+
// Through cli.ts's one adapter from paperlint's blocks to ESLint's type (`eslintConfig`).
|
|
42
|
+
overrideConfig: eslintConfig([
|
|
43
|
+
{
|
|
44
|
+
files: ["presets/*.md"],
|
|
45
|
+
plugins: {
|
|
46
|
+
markdown,
|
|
47
|
+
preset: {
|
|
48
|
+
rules: presetCardRules({
|
|
49
|
+
files: nodeFiles,
|
|
50
|
+
venuesDir: P,
|
|
51
|
+
docsRoot: root,
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
language: "markdown/gfm",
|
|
56
|
+
rules: { "preset/card-rules": "error" },
|
|
57
|
+
},
|
|
58
|
+
]),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/** The findings for one card, as `messageId`-free text: rule id and message. */
|
|
62
|
+
async function findings(card: string): Promise<string[]> {
|
|
63
|
+
const [r] = await eslint().lintFiles([join(P, card)]);
|
|
64
|
+
return (r?.messages ?? []).map((m) => `${String(m.ruleId)}: ${m.message}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The card after `eslint --fix`, written to disk as ESLint would. */
|
|
68
|
+
async function fixed(card: string): Promise<string> {
|
|
69
|
+
const results = await eslint(true).lintFiles([join(P, card)]);
|
|
70
|
+
await ESLint.outputFixes(results);
|
|
71
|
+
return readFileSync(join(P, card), "utf8");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The generated section of a card's text, markers included. */
|
|
75
|
+
const sectionOf = (text: string): string =>
|
|
76
|
+
text.slice(
|
|
77
|
+
text.indexOf(SECTION_START),
|
|
78
|
+
text.indexOf(SECTION_END) + SECTION_END.length,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const STALE =
|
|
82
|
+
"preset/card-rules: the rules section does not match the preset beside this card — run `npx eslint --fix` on this file to rewrite it from the preset";
|
|
83
|
+
|
|
84
|
+
describe("the shipped cards", () => {
|
|
85
|
+
it("every shipped card holds the section its preset resolves to", async () => {
|
|
86
|
+
const results = await eslint().lintFiles([join(P, "*.md")]);
|
|
87
|
+
// Guards: a glob that matched nothing lints nothing and reports clean.
|
|
88
|
+
expect(results.map((r) => r.filePath.slice(P.length + 1)).sort()).toEqual([
|
|
89
|
+
"acm-sigconf.md",
|
|
90
|
+
"agenticdev.md",
|
|
91
|
+
"aidc.md",
|
|
92
|
+
"aisec.md",
|
|
93
|
+
"ieee-conference.md",
|
|
94
|
+
"realm.md",
|
|
95
|
+
"tex-base.md",
|
|
96
|
+
]);
|
|
97
|
+
expect(results.flatMap((r) => r.messages)).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("agenticdev: its own rules, linked to their page or to the index, and what it extends", async () => {
|
|
101
|
+
const text = readFileSync(join(P, "agenticdev.md"), "utf8");
|
|
102
|
+
writeFileSync(
|
|
103
|
+
join(P, "agenticdev.md"),
|
|
104
|
+
text.replace(sectionOf(text), `${SECTION_START}\n${SECTION_END}`),
|
|
105
|
+
);
|
|
106
|
+
expect(await findings("agenticdev.md")).toEqual([STALE]);
|
|
107
|
+
// Guards: --fix writes the resolved preset — the section is the generated output, compared whole.
|
|
108
|
+
expect(sectionOf(await fixed("agenticdev.md"))).toBe(
|
|
109
|
+
[
|
|
110
|
+
SECTION_START,
|
|
111
|
+
"<!-- prettier-ignore-start -->",
|
|
112
|
+
"<!-- Generated from agenticdev.jsonc by `npx eslint --fix` (rule preset/card-rules). Edit the preset, not this section. -->",
|
|
113
|
+
"",
|
|
114
|
+
"**`paperlint:agenticdev`** — venue preset for AgenticDev · call for papers: <https://conf.researchr.org/home/ase-2026/agenticdev-2026> · extends `paperlint:acm-sigconf`",
|
|
115
|
+
"",
|
|
116
|
+
"| rule | severity | options | set in |",
|
|
117
|
+
"| --- | --- | --- | --- |",
|
|
118
|
+
'| [`pdf/last-page-balance`](../docs/rules.md) | error | `{"tolerancePt":120}` | this preset |',
|
|
119
|
+
'| [`tex/heading-case`](../docs/rules/tex/heading-case.md) | error | `{"title":"chicago-headline","headings":"chicago-headline"}` | this preset |',
|
|
120
|
+
"",
|
|
121
|
+
"<!-- prettier-ignore-end -->",
|
|
122
|
+
SECTION_END,
|
|
123
|
+
].join("\n"),
|
|
124
|
+
);
|
|
125
|
+
expect(await findings("agenticdev.md")).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("the origin column: set here, or inherited from the file of the chain that set it", () => {
|
|
130
|
+
it("a venue over agenticdev: its own rule is its own, agenticdev's are inherited", async () => {
|
|
131
|
+
writeFileSync(
|
|
132
|
+
join(P, "fixture-venue.jsonc"),
|
|
133
|
+
JSON.stringify({
|
|
134
|
+
type: "venue",
|
|
135
|
+
name: "Fixture",
|
|
136
|
+
url: "https://example.org/cfp",
|
|
137
|
+
extends: "paperlint:agenticdev",
|
|
138
|
+
rules: { "pdf/body-size": "off", "tex/register": ["warn", "a|b"] },
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
writeFileSync(join(P, "fixture-venue.md"), "# Fixture\n");
|
|
142
|
+
expect(await findings("fixture-venue.md")).toEqual([
|
|
143
|
+
`preset/card-rules: this card has no generated rules section — run \`npx eslint --fix\` on this file to append one (between ${SECTION_START} and ${SECTION_END})`,
|
|
144
|
+
]);
|
|
145
|
+
const text = await fixed("fixture-venue.md");
|
|
146
|
+
// Guards: an inherited rule names its parent, an own rule says so; an entry without options
|
|
147
|
+
// shows a dash, and a `|` in the options is escaped so the table keeps its columns.
|
|
148
|
+
expect(text.split("\n").filter((l) => l.startsWith("| ["))).toEqual([
|
|
149
|
+
"| [`pdf/body-size`](../docs/rules/pdf/body-size.md) | off | — | this preset |",
|
|
150
|
+
'| [`pdf/last-page-balance`](../docs/rules.md) | error | `{"tolerancePt":120}` | inherited from `paperlint:agenticdev` |',
|
|
151
|
+
'| [`tex/heading-case`](../docs/rules/tex/heading-case.md) | error | `{"title":"chicago-headline","headings":"chicago-headline"}` | inherited from `paperlint:agenticdev` |',
|
|
152
|
+
'| [`tex/register`](../docs/rules/tex/register.md) | warn | `"a\\|b"` | this preset |',
|
|
153
|
+
]);
|
|
154
|
+
// Appended after one blank line to a card that ended with a newline.
|
|
155
|
+
expect(text.startsWith(`# Fixture\n\n${SECTION_START}\n`)).toBe(true);
|
|
156
|
+
expect(await findings("fixture-venue.md")).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("a card without a final newline gets one before the appended section", async () => {
|
|
160
|
+
writeFileSync(join(P, "fixture-venue.md"), "# Fixture");
|
|
161
|
+
expect(
|
|
162
|
+
(await fixed("fixture-venue.md")).startsWith(
|
|
163
|
+
`# Fixture\n\n${SECTION_START}\n`,
|
|
164
|
+
),
|
|
165
|
+
).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("a chain that leaves presets/", () => {
|
|
170
|
+
it("a rule set by a preset outside presets/ names that file by its path from the card", async () => {
|
|
171
|
+
mkdirSync(join(root, "shared"), { recursive: true });
|
|
172
|
+
writeFileSync(
|
|
173
|
+
join(root, "shared", "fixture-base.jsonc"),
|
|
174
|
+
JSON.stringify({
|
|
175
|
+
type: "family",
|
|
176
|
+
extends: "paperlint:acm-sigconf",
|
|
177
|
+
rules: { "pdf/body-size": "warn" },
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
writeFileSync(
|
|
181
|
+
join(P, "fixture-outside.jsonc"),
|
|
182
|
+
JSON.stringify({
|
|
183
|
+
type: "venue",
|
|
184
|
+
name: "Outside",
|
|
185
|
+
url: "https://example.org/cfp",
|
|
186
|
+
extends: "../shared/fixture-base.jsonc",
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
writeFileSync(join(P, "fixture-outside.md"), "# Outside\n");
|
|
190
|
+
// Guards: a file of the chain that is not a shipped preset is not given a `paperlint:` name
|
|
191
|
+
// that would resolve nowhere; it is the path a reader can open from the card.
|
|
192
|
+
expect(
|
|
193
|
+
(await fixed("fixture-outside.md"))
|
|
194
|
+
.split("\n")
|
|
195
|
+
.filter((l) => l.startsWith("| [")),
|
|
196
|
+
).toEqual([
|
|
197
|
+
"| [`pdf/body-size`](../docs/rules/pdf/body-size.md) | warn | — | inherited from `../shared/fixture-base.jsonc` |",
|
|
198
|
+
]);
|
|
199
|
+
rmSync(join(P, "fixture-outside.jsonc"));
|
|
200
|
+
rmSync(join(P, "fixture-outside.md"));
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("a family the shipped presets do not extend", () => {
|
|
205
|
+
it("says so, and that its chain sets no rule", async () => {
|
|
206
|
+
writeFileSync(
|
|
207
|
+
join(P, "fixture-family.jsonc"),
|
|
208
|
+
JSON.stringify({ type: "family", extends: "paperlint:acm-sigconf" }),
|
|
209
|
+
);
|
|
210
|
+
writeFileSync(join(P, "fixture-family.md"), "# Family\n");
|
|
211
|
+
expect(
|
|
212
|
+
sectionOf(await fixed("fixture-family.md"))
|
|
213
|
+
.split("\n")
|
|
214
|
+
.slice(4, 7),
|
|
215
|
+
).toEqual([
|
|
216
|
+
"**`paperlint:fixture-family`** — template family, no venue of its own · extends `paperlint:acm-sigconf` · extended by no shipped preset",
|
|
217
|
+
"",
|
|
218
|
+
"No preset of this chain sets a rule: under it, every rule keeps paperlint's default.",
|
|
219
|
+
]);
|
|
220
|
+
rmSync(join(P, "fixture-family.jsonc"));
|
|
221
|
+
rmSync(join(P, "fixture-family.md"));
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("a card that cannot be judged, or whose markers are not a pair", () => {
|
|
226
|
+
it("one marker without the other is reported, and not fixed", async () => {
|
|
227
|
+
for (const marker of [SECTION_START, SECTION_END]) {
|
|
228
|
+
writeFileSync(join(P, "fixture-venue.md"), `# Fixture\n\n${marker}\n`);
|
|
229
|
+
expect(await findings("fixture-venue.md")).toEqual([
|
|
230
|
+
`preset/card-rules: the generated rules section needs both markers, ${SECTION_START} and ${SECTION_END}, each on its own line`,
|
|
231
|
+
]);
|
|
232
|
+
expect(await fixed("fixture-venue.md")).toBe(`# Fixture\n\n${marker}\n`);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("a card with no preset beside it names the file it looked for", async () => {
|
|
237
|
+
writeFileSync(join(P, "orphan.md"), "# Orphan\n");
|
|
238
|
+
expect(await findings("orphan.md")).toEqual([
|
|
239
|
+
`preset/card-rules: a preset card describes the preset beside it, and there is none: ${join(P, "orphan.jsonc")}`,
|
|
240
|
+
]);
|
|
241
|
+
rmSync(join(P, "orphan.md"));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("a card beside a preset that does not resolve says why", async () => {
|
|
245
|
+
writeFileSync(
|
|
246
|
+
join(P, "broken.jsonc"),
|
|
247
|
+
JSON.stringify({ type: "family", extends: "x" }),
|
|
248
|
+
);
|
|
249
|
+
writeFileSync(join(P, "broken.md"), "# Broken\n");
|
|
250
|
+
expect(await findings("broken.md")).toEqual([
|
|
251
|
+
`preset/card-rules: the preset beside this card does not resolve, so its rules cannot be listed: "extends": "x" — a preset is \`paperlint:<name>\` (shipped) or a path starting with ./ or ../ (your own); npm presets: not yet supported. Did you mean "paperlint:x"?`,
|
|
252
|
+
]);
|
|
253
|
+
rmSync(join(P, "broken.jsonc"));
|
|
254
|
+
rmSync(join(P, "broken.md"));
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("entryParts", () => {
|
|
259
|
+
it.each<[unknown, { severity: string; options: string }]>([
|
|
260
|
+
["error", { severity: "error", options: "" }],
|
|
261
|
+
[2, { severity: "2", options: "" }],
|
|
262
|
+
[["warn", { a: 1 }, "b"], { severity: "warn", options: '{"a":1}, "b"' }],
|
|
263
|
+
])("%j", (entry, want) => {
|
|
264
|
+
expect(entryParts(entry)).toEqual(want);
|
|
265
|
+
});
|
|
266
|
+
});
|