paperlint 4.0.0 → 4.2.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/.github/workflows/ci.yml +16 -23
- package/CLAUDE.md +55 -11
- package/CONTRIBUTING.md +30 -7
- package/README.md +11 -8
- package/dist/adapters/banal/invocation.d.ts +1 -1
- package/dist/adapters/latex/document-class.d.ts +23 -0
- package/dist/adapters/latex/document-class.d.ts.map +1 -0
- package/dist/adapters/latex/document-class.js +110 -0
- package/dist/adapters/latex/document-class.js.map +1 -0
- package/dist/adapters/latex/hidden.d.ts +16 -0
- package/dist/adapters/latex/hidden.d.ts.map +1 -0
- package/dist/adapters/latex/hidden.js +49 -0
- package/dist/adapters/latex/hidden.js.map +1 -0
- package/dist/adapters/latex/includes.d.ts +12 -0
- package/dist/adapters/latex/includes.d.ts.map +1 -0
- package/dist/adapters/latex/includes.js +42 -0
- package/dist/adapters/latex/includes.js.map +1 -0
- package/dist/adapters/latex/index.d.ts +33 -0
- package/dist/adapters/latex/index.d.ts.map +1 -0
- package/dist/adapters/latex/index.js +25 -0
- package/dist/adapters/latex/index.js.map +1 -0
- package/dist/adapters/latex/nodes.d.ts +58 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -0
- package/dist/adapters/latex/nodes.js +97 -0
- package/dist/adapters/latex/nodes.js.map +1 -0
- package/dist/adapters/latex/outline.d.ts +10 -0
- package/dist/adapters/latex/outline.d.ts.map +1 -0
- package/dist/adapters/latex/outline.js +39 -0
- package/dist/adapters/latex/outline.js.map +1 -0
- package/dist/adapters/latex/parse.d.ts +42 -0
- package/dist/adapters/latex/parse.d.ts.map +1 -0
- package/dist/adapters/latex/parse.js +95 -0
- package/dist/adapters/latex/parse.js.map +1 -0
- package/dist/adapters/latex/prose.d.ts +8 -0
- package/dist/adapters/latex/prose.d.ts.map +1 -0
- package/dist/adapters/latex/prose.js +353 -0
- package/dist/adapters/latex/prose.js.map +1 -0
- package/dist/adapters/latex/rendered.d.ts +10 -0
- package/dist/adapters/latex/rendered.d.ts.map +1 -0
- package/dist/adapters/latex/rendered.js +39 -0
- package/dist/adapters/latex/rendered.js.map +1 -0
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +12 -12
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/build.js +4 -4
- package/dist/build.js.map +1 -1
- package/dist/claim-provenance.d.ts +34 -0
- package/dist/claim-provenance.d.ts.map +1 -0
- package/dist/claim-provenance.js +146 -0
- package/dist/claim-provenance.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +49 -5
- package/dist/cli.js.map +1 -1
- package/dist/domain/paper-source.d.ts +58 -0
- package/dist/domain/paper-source.d.ts.map +1 -0
- package/dist/domain/paper-source.js +109 -0
- package/dist/domain/paper-source.js.map +1 -0
- package/dist/domain/sentences.d.ts +26 -0
- package/dist/domain/sentences.d.ts.map +1 -0
- package/dist/domain/sentences.js +48 -0
- package/dist/domain/sentences.js.map +1 -0
- package/dist/domain/tex-document.d.ts +119 -0
- package/dist/domain/tex-document.d.ts.map +1 -0
- package/dist/domain/tex-document.js +43 -0
- package/dist/domain/tex-document.js.map +1 -0
- package/dist/eslint-rules/paper-typography.d.ts +7 -0
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +53 -1
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +2 -2
- package/dist/facts-file.js.map +1 -1
- package/dist/new-paper.d.ts +5 -0
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +3 -0
- package/dist/new-paper.js.map +1 -1
- package/dist/package-dirs.d.ts +5 -0
- package/dist/package-dirs.d.ts.map +1 -0
- package/dist/package-dirs.js +14 -0
- package/dist/package-dirs.js.map +1 -0
- package/dist/ports/latex.d.ts +35 -0
- package/dist/ports/latex.d.ts.map +1 -0
- package/dist/ports/latex.js +2 -0
- package/dist/ports/latex.js.map +1 -0
- package/dist/presets.d.ts +15 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +19 -6
- package/dist/presets.js.map +1 -1
- package/dist/register.d.ts +40 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +134 -0
- package/dist/register.js.map +1 -0
- package/dist/tex-paper.d.ts +19 -0
- package/dist/tex-paper.d.ts.map +1 -0
- package/dist/tex-paper.js +105 -0
- package/dist/tex-paper.js.map +1 -0
- package/dist/tex-requirements.d.ts +11 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +14 -8
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +88 -0
- package/dist/tex-venue-rules.d.ts.map +1 -0
- package/dist/tex-venue-rules.js +268 -0
- package/dist/tex-venue-rules.js.map +1 -0
- package/dist/venue-rules.d.ts +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/docs/configuration.md +3 -2
- package/docs/e2e.md +69 -40
- package/docs/install.md +1 -1
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/package-location.md +5 -5
- package/docs/prior-art/repro/README.md +1 -1
- package/docs/prior-art/test-tooling.md +21 -10
- package/docs/rules/tex/claim-provenance.md +83 -0
- package/docs/rules/tex/missing-input.md +58 -0
- package/docs/rules/tex/register.md +77 -0
- package/docs/rules/tex/required-section.md +75 -0
- package/docs/rules/tex/template.md +74 -0
- package/docs/rules/tex/venue-leftover.md +73 -0
- package/docs/rules.md +73 -48
- package/docs/testing.md +17 -4
- package/docs/texlive-install-decision.md +1 -1
- package/docs/toolchain.md +2 -2
- package/eslint-rules/paper-typography.test.mjs +48 -1
- package/eslint-rules/paper-typography.ts +72 -1
- package/eslint.config.mjs +156 -15
- package/fixtures/build-e2e/acmart/expect.json +26 -0
- package/fixtures/build-e2e/aidc/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/aidc/expect.json +14 -0
- package/fixtures/build-e2e/aidc/paper.tex +46 -0
- package/fixtures/build-e2e/aidc/paperlint.json +1 -0
- package/fixtures/build-e2e/broken/expect.json +11 -0
- package/fixtures/build-e2e/cite/expect.json +27 -0
- package/fixtures/build-e2e/empty/expect.json +12 -0
- package/fixtures/build-e2e/fallback/expect.json +53 -0
- package/fixtures/build-e2e/guards/expect.json +6 -0
- package/fixtures/build-e2e/no-source/expect.json +10 -0
- package/fixtures/build-e2e/unbalanced/expect.json +60 -0
- package/fixtures/pdf-facts/README.md +1 -1
- package/fixtures/real-markdown-paper/baseline.ts +8 -7
- package/fixtures/render-paper/build-clean.sh +3 -5
- package/package.json +14 -2
- package/{skills/submit-paper/references/venues → presets}/acm-sigconf.jsonc +4 -1
- package/{skills/submit-paper/references/venues → presets}/agenticdev.jsonc +4 -0
- package/presets/aidc.jsonc +73 -0
- package/{skills/submit-paper/references/venues → presets}/aisec.jsonc +4 -0
- package/presets/ieee-conference.jsonc +66 -0
- package/{skills/submit-paper/references/venues → presets}/realm.jsonc +8 -1
- package/{skills/submit-paper/references/venues → presets/tex}/paper-guards.tex +1 -1
- package/{skills/submit-paper/references/venues → presets}/venue-profile.schema.json +30 -2
- package/scripts/check.harness.ts +64 -10
- package/scripts/check.test.ts +60 -13
- package/scripts/check.ts +83 -30
- package/scripts/test.test.ts +1 -1
- package/scripts/test.ts +1 -1
- package/skills/cold-read-diff/SKILL.md +31 -10
- package/skills/cold-read-diff/SKILL.md.spec.ts +30 -9
- package/skills/draft-paper/SKILL.md +2 -2
- package/skills/draft-paper/SKILL.md.spec.ts +1 -1
- package/skills/grade-paper-writing/SKILL.md +109 -44
- package/skills/grade-paper-writing/SKILL.md.spec.ts +108 -43
- package/skills/harden-paper/SKILL.md +35 -36
- package/skills/harden-paper/SKILL.md.spec.ts +34 -35
- package/skills/paper-adversarial-review/SKILL.md +2 -2
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/PIPELINE-MAP.md +2 -2
- package/skills/paper-pipeline/SKILL.md +4 -4
- package/skills/paper-pipeline/SKILL.md.spec.ts +3 -3
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +6 -6
- package/skills/paper-pipeline/references/writing-craft.md +53 -18
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -2
- package/skills/paper-pipeline/scripts/consumer.mjs +1 -15
- package/skills/pc-panel-review/SKILL.md +36 -11
- package/skills/pc-panel-review/SKILL.md.spec.ts +35 -10
- package/skills/render-paper/SKILL.md +2 -2
- package/skills/render-paper/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/check-render.sh +16 -63
- package/skills/render-paper/render-paper.harness.mjs +17 -59
- package/src/adapters/banal/index.test.ts +1 -1
- package/src/adapters/banal/invocation.ts +1 -1
- package/src/adapters/banal/output.test.ts +1 -1
- package/src/adapters/banal/run.test.ts +1 -1
- package/src/adapters/banal/xml.test.ts +1 -1
- package/src/adapters/latex/document-class.ts +155 -0
- package/src/adapters/latex/hidden.ts +70 -0
- package/src/adapters/latex/includes.ts +66 -0
- package/src/adapters/latex/index.test.ts +535 -0
- package/src/adapters/latex/index.ts +65 -0
- package/src/adapters/latex/nodes.ts +150 -0
- package/src/adapters/latex/outline.ts +64 -0
- package/src/adapters/latex/parse.ts +137 -0
- package/src/adapters/latex/prose.ts +484 -0
- package/src/adapters/latex/rendered.ts +59 -0
- package/src/adapters/pdfjs/fill.test.ts +1 -1
- package/src/adapters/pdfjs/fill.ts +18 -10
- package/src/build.harness.mjs +5 -7
- package/src/build.ts +4 -4
- package/src/claim-provenance.test.ts +352 -0
- package/src/claim-provenance.ts +217 -0
- package/src/cli.harness.mjs +1 -1
- package/src/cli.test.ts +2 -8
- package/src/cli.ts +53 -8
- package/src/domain/paper-source.test.ts +156 -0
- package/src/domain/paper-source.ts +233 -0
- package/src/domain/sentences.ts +67 -0
- package/src/domain/tex-document.test.ts +80 -0
- package/src/domain/tex-document.ts +143 -0
- package/src/engine.harness.mjs +1 -1
- package/src/facts-file.test.ts +4 -7
- package/src/facts-file.ts +2 -2
- package/src/latex-loop.harness.ts +1 -1
- package/src/new-paper.test.ts +28 -3
- package/src/new-paper.ts +8 -0
- package/src/package-dirs.ts +17 -0
- package/src/paper-settings-commands.test.ts +6 -1
- package/src/ports/latex.ts +42 -0
- package/src/presets.test.ts +65 -4
- package/src/presets.ts +38 -15
- package/src/register.test.ts +165 -0
- package/src/register.ts +177 -0
- package/src/rule-cases.test.ts +524 -0
- package/src/rule-docs.test.ts +159 -0
- package/src/tex-paper.test.ts +131 -0
- package/src/tex-paper.ts +152 -0
- package/src/tex-requirements.harness.ts +13 -23
- package/src/tex-requirements.ts +35 -8
- package/src/tex-venue-rules.test.ts +741 -0
- package/src/tex-venue-rules.ts +461 -0
- package/src/toolchain.harness.mjs +1 -1
- package/src/venue-rules.test.ts +96 -4
- package/src/venue-rules.ts +1 -1
- package/templates/paper/paper.tex +2 -1
- package/skills/harden-paper/fixtures/uncited-assertions-sample.md +0 -43
- package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +0 -77
- /package/{skills/submit-paper/references/venues → presets/tex}/agenticdev.tex +0 -0
- /package/{skills/submit-paper/references/venues → presets}/tex-base.jsonc +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tex/missing-input` and the reading every parse-tree rule shares (`readPaper`): an include that
|
|
3
|
+
* names no file is reported where it stands in `paper.tex`, once, whether `paper.tex` wrote it or a
|
|
4
|
+
* file it includes did; an include that resolves says nothing.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { latexReader } from "./adapters/latex/index.ts";
|
|
8
|
+
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
9
|
+
import { missingInputRule, readPaper, reportInPaper } from "./tex-paper.ts";
|
|
10
|
+
|
|
11
|
+
const doc = (body: string): string =>
|
|
12
|
+
`\\documentclass{article}\n\\begin{document}\n${body}\n\\end{document}\n`;
|
|
13
|
+
|
|
14
|
+
/** What the rule reports over `/p/paper.tex` holding `src`, with `files` on disk. */
|
|
15
|
+
function run(src: string, files: Record<string, string> = {}) {
|
|
16
|
+
const out: { line: number; messageId: string; data: unknown }[] = [];
|
|
17
|
+
const rule = missingInputRule({
|
|
18
|
+
files: memoryFiles(files),
|
|
19
|
+
latex: latexReader,
|
|
20
|
+
});
|
|
21
|
+
rule
|
|
22
|
+
.create({
|
|
23
|
+
filename: "/p/paper.tex",
|
|
24
|
+
sourceCode: {
|
|
25
|
+
text: src,
|
|
26
|
+
getLocFromIndex: (i) => ({
|
|
27
|
+
line: src.slice(0, i).split("\n").length,
|
|
28
|
+
column: 0,
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
report: (d) => {
|
|
32
|
+
if ("messageId" in d)
|
|
33
|
+
out.push({
|
|
34
|
+
line: d.loc.start.line,
|
|
35
|
+
messageId: d.messageId,
|
|
36
|
+
data: d.data,
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
.root?.();
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("tex/missing-input", () => {
|
|
45
|
+
it("an \\input naming no file is reported at its line, naming the target", () => {
|
|
46
|
+
expect(run(doc("Text.\n\\input{sections/gone}"))).toEqual([
|
|
47
|
+
{
|
|
48
|
+
line: 4,
|
|
49
|
+
messageId: "missing",
|
|
50
|
+
data: { target: "sections/gone", file: "paper.tex" },
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("one missing in an included file is reported at the include that brought that file in", () => {
|
|
56
|
+
expect(run(doc("\\input{a}"), { "/p/a.tex": "A\n\\input{b}\n" })).toEqual([
|
|
57
|
+
{ line: 3, messageId: "missingIn", data: { target: "b", file: "a.tex" } },
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("silent when every include resolves, with `.tex` added or not", () => {
|
|
62
|
+
expect(
|
|
63
|
+
run(doc("\\input{a}\\include{b.tex}"), {
|
|
64
|
+
"/p/a.tex": "A",
|
|
65
|
+
"/p/b.tex": "B",
|
|
66
|
+
}),
|
|
67
|
+
).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("readPaper — the paper as the rules read it", () => {
|
|
72
|
+
it("reads includes from the paper's own directory", () => {
|
|
73
|
+
const p = readPaper("/p/paper.tex", "x\\input{s/a}y", {
|
|
74
|
+
files: memoryFiles({ "/p/s/a.tex": "AAA" }),
|
|
75
|
+
latex: latexReader,
|
|
76
|
+
});
|
|
77
|
+
expect(p.text).toBe("xAAAy");
|
|
78
|
+
expect(p.main).toBe("paper.tex");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("reportInPaper — where a finding of the assembled text is reported", () => {
|
|
83
|
+
const paper = readPaper("/p/paper.tex", "x\n\\input{a}\ny", {
|
|
84
|
+
files: memoryFiles({ "/p/a.tex": "one\ntwo" }),
|
|
85
|
+
latex: latexReader,
|
|
86
|
+
});
|
|
87
|
+
const report = (
|
|
88
|
+
at: { start: number; end: number } | null,
|
|
89
|
+
messageId = "m",
|
|
90
|
+
) => {
|
|
91
|
+
const out: unknown[] = [];
|
|
92
|
+
reportInPaper(
|
|
93
|
+
{
|
|
94
|
+
filename: "/p/paper.tex",
|
|
95
|
+
sourceCode: {
|
|
96
|
+
text: "",
|
|
97
|
+
getLocFromIndex: (i) => ({ line: i, column: 0 }),
|
|
98
|
+
},
|
|
99
|
+
report: (d) =>
|
|
100
|
+
out.push(
|
|
101
|
+
"messageId" in d
|
|
102
|
+
? { at: d.loc.start.line, id: d.messageId }
|
|
103
|
+
: { at: d.loc.start.line, message: d.message },
|
|
104
|
+
),
|
|
105
|
+
},
|
|
106
|
+
{ m: "said {{w}}" },
|
|
107
|
+
paper,
|
|
108
|
+
[{ messageId, data: { w: "so" }, at }],
|
|
109
|
+
);
|
|
110
|
+
return out;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
it("in the main file: at its own place, by its id", () => {
|
|
114
|
+
expect(report({ start: 0, end: 1 })).toEqual([{ at: 0, id: "m" }]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("in an included file: at the include, the file, line and column in front of the filled message", () => {
|
|
118
|
+
// "x\n" then "one\ntwo": offset 6 is the `t` of `two`, line 2 column 1 of a.tex.
|
|
119
|
+
expect(report({ start: 6, end: 9 })).toEqual([
|
|
120
|
+
{ at: 2, message: "a.tex:2:1: said so" },
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("no place, or a place outside the text: the top of the file; an unknown id is printed as itself", () => {
|
|
125
|
+
expect(report(null)).toEqual([{ at: 0, id: "m" }]);
|
|
126
|
+
expect(report({ start: 99, end: 99 })).toEqual([{ at: 0, id: "m" }]);
|
|
127
|
+
expect(report({ start: 6, end: 9 }, "unknown")).toEqual([
|
|
128
|
+
{ at: 2, message: "a.tex:2:1: unknown" },
|
|
129
|
+
]);
|
|
130
|
+
});
|
|
131
|
+
});
|
package/src/tex-paper.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE PAPER, NOT ITS MAIN FILE — what every rule over the LaTeX parse tree reads, and how it reports.
|
|
3
|
+
*
|
|
4
|
+
* `readPaper` gives a rule `paper.tex` with every `\input`, `\include` and `\subfile` spliced in, the
|
|
5
|
+
* way TeX reads it (`assemblePaper`, src/domain/paper-source.ts). `reportInPaper` puts each finding
|
|
6
|
+
* back where its text came from: a finding in `paper.tex` at its own place; a finding in an included
|
|
7
|
+
* file at the include in `paper.tex` that brought it in — the one place ESLint can point to in this
|
|
8
|
+
* file — with the included file, line and column at the front of the message.
|
|
9
|
+
*
|
|
10
|
+
* tex/missing-input warn an `\input`, `\include` or `\subfile` names a file that is not there
|
|
11
|
+
*
|
|
12
|
+
* A missing file is a finding, not a silent skip: the rules would otherwise report a clean body for
|
|
13
|
+
* text they never read, and TeX stops on it too. Warn, not error: a file a build step writes (a
|
|
14
|
+
* table of generated numbers) is legitimately absent before that step has run.
|
|
15
|
+
*/
|
|
16
|
+
import { basename, dirname, join } from "node:path";
|
|
17
|
+
import {
|
|
18
|
+
assemblePaper,
|
|
19
|
+
originOf,
|
|
20
|
+
type PaperSource,
|
|
21
|
+
} from "./domain/paper-source.ts";
|
|
22
|
+
import type { Span } from "./domain/tex-document.ts";
|
|
23
|
+
import type { LatexReader } from "./ports/latex.ts";
|
|
24
|
+
import type { Files } from "./ports/files.ts";
|
|
25
|
+
import { callerPath } from "./caller-path.ts";
|
|
26
|
+
import type {
|
|
27
|
+
Located,
|
|
28
|
+
TexRuleContext,
|
|
29
|
+
TexRuleModule,
|
|
30
|
+
} from "./tex-venue-rules.ts";
|
|
31
|
+
|
|
32
|
+
/** What reading a paper needs: the disk, and the LaTeX reader. */
|
|
33
|
+
export interface PaperDeps {
|
|
34
|
+
readonly files: Files;
|
|
35
|
+
readonly latex: LatexReader;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The paper whose main file is `filename` with the text `src`, its includes spliced. */
|
|
39
|
+
export function readPaper(
|
|
40
|
+
filename: string,
|
|
41
|
+
src: string,
|
|
42
|
+
deps: PaperDeps,
|
|
43
|
+
): PaperSource {
|
|
44
|
+
const dir = dirname(filename);
|
|
45
|
+
return assemblePaper(basename(filename), src, {
|
|
46
|
+
includes: deps.latex.includes,
|
|
47
|
+
documentBody: deps.latex.documentBody,
|
|
48
|
+
read: (rel) => {
|
|
49
|
+
const b = deps.files.readBytes(callerPath(join(dir, rel)));
|
|
50
|
+
return b === null ? null : new TextDecoder().decode(b);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Line and column (1-based) of an offset in a text. */
|
|
56
|
+
function lineColumn(text: string, at: number): string {
|
|
57
|
+
const line = text.slice(0, at).split("\n").length;
|
|
58
|
+
const column = at - text.lastIndexOf("\n", at - 1);
|
|
59
|
+
return `${String(line)}:${String(column)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A message template with its data filled in, the way ESLint fills it. */
|
|
63
|
+
const filled = (
|
|
64
|
+
template: string,
|
|
65
|
+
data: Readonly<Record<string, string | number>> = {},
|
|
66
|
+
): string =>
|
|
67
|
+
Object.entries(data).reduce(
|
|
68
|
+
(m, [k, v]) => m.replaceAll(`{{${k}}}`, String(v)),
|
|
69
|
+
template,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
/** Where a finding stands in the main file, and — when its text came from another file — which. */
|
|
73
|
+
interface Placed {
|
|
74
|
+
readonly at: Span | null;
|
|
75
|
+
readonly from: string | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A finding's span of the assembled text, as a place in the main file. */
|
|
79
|
+
function placed(paper: PaperSource, at: Span | null): Placed {
|
|
80
|
+
const origin = at === null ? null : originOf(paper, at);
|
|
81
|
+
if (origin === null) return { at: null, from: null };
|
|
82
|
+
if (origin.file === paper.main) return { at: origin.span, from: null };
|
|
83
|
+
return {
|
|
84
|
+
at: origin.via,
|
|
85
|
+
from: `${origin.file}:${lineColumn(origin.source, origin.span.start)}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Each finding reported in the main file: at its own place, or at the include that brought its text
|
|
91
|
+
* in, named in the message (`sections/results.tex:12:3: …`). Offsets are the assembled text's.
|
|
92
|
+
*/
|
|
93
|
+
export function reportInPaper(
|
|
94
|
+
context: TexRuleContext,
|
|
95
|
+
messages: Readonly<Record<string, string>>,
|
|
96
|
+
paper: PaperSource,
|
|
97
|
+
findings: readonly Located[],
|
|
98
|
+
): void {
|
|
99
|
+
const loc = (i: number) => context.sourceCode.getLocFromIndex(i);
|
|
100
|
+
findings.forEach((f) => {
|
|
101
|
+
const p = placed(paper, f.at);
|
|
102
|
+
const where = {
|
|
103
|
+
start: loc(p.at?.start ?? 0),
|
|
104
|
+
end: loc(p.at?.end ?? 0),
|
|
105
|
+
};
|
|
106
|
+
if (p.from === null)
|
|
107
|
+
context.report({ loc: where, messageId: f.messageId, data: f.data });
|
|
108
|
+
else
|
|
109
|
+
context.report({
|
|
110
|
+
loc: where,
|
|
111
|
+
// A message id the rule does not define is printed as itself rather than dropped.
|
|
112
|
+
message: `${p.from}: ${filled(messages[f.messageId] ?? f.messageId, f.data)}`,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const MISSING_META: TexRuleModule["meta"] = {
|
|
118
|
+
type: "problem",
|
|
119
|
+
docs: {
|
|
120
|
+
description:
|
|
121
|
+
"an \\input, \\include or \\subfile names a file that is not there, so no rule read its text",
|
|
122
|
+
url: "https://github.com/zernie/paperlint/blob/main/docs/rules/tex/missing-input.md",
|
|
123
|
+
},
|
|
124
|
+
schema: [],
|
|
125
|
+
messages: {
|
|
126
|
+
missing:
|
|
127
|
+
"`{{target}}` is not there (tried `{{target}}.tex` and `{{target}}`, from the paper's directory): TeX stops on it, and no rule read what it should hold",
|
|
128
|
+
missingIn:
|
|
129
|
+
"`{{target}}`, which {{file}} includes, is not there (tried `{{target}}.tex` and `{{target}}`, from the paper's directory): TeX stops on it, and no rule read what it should hold",
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** The includes that name no file, each reported where it stands in the main file. */
|
|
134
|
+
export const missingInputRule = (deps: PaperDeps): TexRuleModule => ({
|
|
135
|
+
meta: MISSING_META,
|
|
136
|
+
create(context) {
|
|
137
|
+
return {
|
|
138
|
+
root() {
|
|
139
|
+
const sc = context.sourceCode;
|
|
140
|
+
const paper = readPaper(context.filename, sc.raw ?? sc.text, deps);
|
|
141
|
+
const loc = (i: number) => sc.getLocFromIndex(i);
|
|
142
|
+
paper.missing.forEach((m) => {
|
|
143
|
+
context.report({
|
|
144
|
+
loc: { start: loc(m.via.start), end: loc(m.via.end) },
|
|
145
|
+
messageId: m.file === paper.main ? "missing" : "missingIn",
|
|
146
|
+
data: { target: m.target, file: m.file },
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
});
|
|
@@ -16,22 +16,12 @@ import {
|
|
|
16
16
|
writeFileSync,
|
|
17
17
|
} from "node:fs";
|
|
18
18
|
import { tmpdir } from "node:os";
|
|
19
|
-
import {
|
|
20
|
-
import { fileURLToPath } from "node:url";
|
|
21
|
-
import ts from "typescript";
|
|
22
|
-
import { z } from "zod";
|
|
19
|
+
import { join } from "node:path";
|
|
23
20
|
import { createChecker } from "../lib/check.ts";
|
|
24
21
|
|
|
25
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
26
|
-
const VENUES = join(
|
|
27
|
-
HERE,
|
|
28
|
-
"..",
|
|
29
|
-
"skills",
|
|
30
|
-
"submit-paper",
|
|
31
|
-
"references",
|
|
32
|
-
"venues",
|
|
33
|
-
);
|
|
34
22
|
const R = await import("./tex-requirements.ts");
|
|
23
|
+
const { parseTemplate } = await import("./adapters/latex/index.ts");
|
|
24
|
+
const VENUES = (await import("./package-dirs.ts")).presetsDir();
|
|
35
25
|
|
|
36
26
|
const check = createChecker();
|
|
37
27
|
const throws = (fn: () => unknown): string => {
|
|
@@ -70,15 +60,15 @@ check(
|
|
|
70
60
|
"cm-super" in base.packages,
|
|
71
61
|
);
|
|
72
62
|
|
|
73
|
-
// The
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
).template;
|
|
63
|
+
// The class each profile's template names, as the parser reads it — never a line of text.
|
|
64
|
+
const presetOf = (v: string) =>
|
|
65
|
+
R.parsePreset(
|
|
66
|
+
readFileSync(join(VENUES, `${v}.jsonc`), "utf8"),
|
|
67
|
+
`${v}.jsonc`,
|
|
68
|
+
VENUES,
|
|
69
|
+
);
|
|
70
|
+
const templateOf = (v: string): string | undefined =>
|
|
71
|
+
parseTemplate(presetOf(v).template ?? "")?.cls;
|
|
82
72
|
const acm = venues.filter((v) => templateOf(v) === "acmart");
|
|
83
73
|
check(
|
|
84
74
|
"at least one acmart venue is shipped (else the next checks see nothing)",
|
|
@@ -108,7 +98,7 @@ for (const v of acm) {
|
|
|
108
98
|
// declares the template's packages, so this holds by construction; the check stays for any two
|
|
109
99
|
// standalone presets that name the same template.
|
|
110
100
|
const byTemplate = new Map<unknown, string[]>();
|
|
111
|
-
for (const v of venues) {
|
|
101
|
+
for (const v of venues.filter((x) => presetOf(x).extends === null)) {
|
|
112
102
|
const t = templateOf(v);
|
|
113
103
|
byTemplate.set(t, [...(byTemplate.get(t) ?? []), v]);
|
|
114
104
|
}
|
package/src/tex-requirements.ts
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* `TexRequirements` and never looks at the text again; a malformed profile throws with the path
|
|
17
17
|
* of every violation, it is never read as "no packages".
|
|
18
18
|
*
|
|
19
|
-
* Where the profiles live is answered by `
|
|
20
|
-
* allowed to know where this package is installed (rule 10).
|
|
19
|
+
* Where the profiles live is answered by `presetsDir()` in `package-dirs.ts` — the one module
|
|
20
|
+
* allowed to know where this package's data is installed (rule 10).
|
|
21
21
|
*/
|
|
22
22
|
// eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
|
|
23
23
|
import { readFileSync, readdirSync } from "node:fs";
|
|
@@ -25,7 +25,7 @@ import { createRequire } from "node:module";
|
|
|
25
25
|
import { join } from "node:path";
|
|
26
26
|
// eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
|
|
27
27
|
import Ajv from "ajv";
|
|
28
|
-
import {
|
|
28
|
+
import { presetsDir } from "./package-dirs.ts";
|
|
29
29
|
import { CONFIG_FILE } from "#lib/paper-config";
|
|
30
30
|
import { fieldOf } from "./domain/record.ts";
|
|
31
31
|
|
|
@@ -148,6 +148,13 @@ export const NO_FORMAT: VenueFormat = {
|
|
|
148
148
|
kinds: new Map(),
|
|
149
149
|
};
|
|
150
150
|
|
|
151
|
+
/** A section a venue requires: its exact title, and whether it must close the paper. */
|
|
152
|
+
export interface RequiredSection {
|
|
153
|
+
readonly title: string;
|
|
154
|
+
/** "last": after every section of the body; null: anywhere. */
|
|
155
|
+
readonly position: "last" | null;
|
|
156
|
+
}
|
|
157
|
+
|
|
151
158
|
/**
|
|
152
159
|
* One preset FILE, parsed — before its `extends` chain is resolved (`src/presets.ts` does that).
|
|
153
160
|
* A preset is a venue or a template family: `{ extends?, name?, template?, format?, tex?, rules? }`.
|
|
@@ -157,7 +164,12 @@ export interface PresetFile {
|
|
|
157
164
|
readonly extends: string | null;
|
|
158
165
|
/** A display name for messages; the file name otherwise. */
|
|
159
166
|
readonly name: string | null;
|
|
167
|
+
/** The `\\documentclass` the venue's template uses, as the file spells it; null when it names none. */
|
|
160
168
|
readonly template: string | null;
|
|
169
|
+
/** Other names the venue goes by in a paper's text; empty when the file names none. */
|
|
170
|
+
readonly aliases: readonly string[];
|
|
171
|
+
/** Sections the venue requires; null when the file names none (a child's list replaces its parent's). */
|
|
172
|
+
readonly requiredSections: readonly RequiredSection[] | null;
|
|
161
173
|
/** Null when the file declares no `tex` block (allowed only with `extends`). */
|
|
162
174
|
readonly tex: TexRequirements | null;
|
|
163
175
|
readonly format: VenueFormat;
|
|
@@ -188,6 +200,11 @@ interface PresetJson {
|
|
|
188
200
|
readonly extends?: string;
|
|
189
201
|
readonly name?: string;
|
|
190
202
|
readonly template?: string;
|
|
203
|
+
readonly aliases?: readonly string[];
|
|
204
|
+
readonly required_sections?: readonly {
|
|
205
|
+
readonly title: string;
|
|
206
|
+
readonly position?: "last";
|
|
207
|
+
}[];
|
|
191
208
|
/** The schema requires `packages` in a `tex` block; `tools` is optional. */
|
|
192
209
|
readonly tex?: Pick<TexRequirements, "packages"> &
|
|
193
210
|
Partial<Pick<TexRequirements, "tools">>;
|
|
@@ -225,6 +242,14 @@ function formatOf(j: FormatJson = {}): VenueFormat {
|
|
|
225
242
|
};
|
|
226
243
|
}
|
|
227
244
|
|
|
245
|
+
/** A preset's `required_sections`, or null when it names none. */
|
|
246
|
+
const sectionsOf = (
|
|
247
|
+
r: PresetJson["required_sections"],
|
|
248
|
+
): readonly RequiredSection[] | null =>
|
|
249
|
+
r === undefined
|
|
250
|
+
? null
|
|
251
|
+
: r.map((x) => ({ title: x.title, position: orNull(x.position) }));
|
|
252
|
+
|
|
228
253
|
/**
|
|
229
254
|
* The text of one preset file → the typed file, or an Error naming every problem. The ONE parser
|
|
230
255
|
* of a preset: the toolchain reads its `tex`, the venue rules its `format`, the config its `rules`.
|
|
@@ -236,7 +261,7 @@ function formatOf(j: FormatJson = {}): VenueFormat {
|
|
|
236
261
|
export function parsePreset(
|
|
237
262
|
text: string,
|
|
238
263
|
file: string,
|
|
239
|
-
dir: string =
|
|
264
|
+
dir: string = presetsDir(),
|
|
240
265
|
): PresetFile {
|
|
241
266
|
const parsed = typescript().parseConfigFileTextToJson(file, text);
|
|
242
267
|
const config: unknown = parsed.config;
|
|
@@ -255,6 +280,8 @@ export function parsePreset(
|
|
|
255
280
|
extends: orNull(j.extends),
|
|
256
281
|
name: orNull(j.name),
|
|
257
282
|
template: orNull(j.template),
|
|
283
|
+
aliases: j.aliases ?? [],
|
|
284
|
+
requiredSections: sectionsOf(j.required_sections),
|
|
258
285
|
tex: j.tex ? { packages: j.tex.packages, tools: j.tex.tools ?? {} } : null,
|
|
259
286
|
format: formatOf(j.format),
|
|
260
287
|
rules: j.rules ?? {},
|
|
@@ -265,7 +292,7 @@ export function parsePreset(
|
|
|
265
292
|
export function parseProfile(
|
|
266
293
|
text: string,
|
|
267
294
|
file: string,
|
|
268
|
-
dir: string =
|
|
295
|
+
dir: string = presetsDir(),
|
|
269
296
|
): TexRequirements {
|
|
270
297
|
return parsePreset(text, file, dir).tex ?? NO_REQUIREMENTS;
|
|
271
298
|
}
|
|
@@ -287,7 +314,7 @@ function readProfile(dir: string, file: string): TexRequirements {
|
|
|
287
314
|
}
|
|
288
315
|
|
|
289
316
|
/** The venues that have a profile — every `.jsonc` in the directory except the base set. */
|
|
290
|
-
export function venueNames(dir: string =
|
|
317
|
+
export function venueNames(dir: string = presetsDir()): string[] {
|
|
291
318
|
return readdirSync(dir)
|
|
292
319
|
.filter((f) => f.endsWith(PROFILE_EXT) && f !== BASE_PROFILE)
|
|
293
320
|
.map((f) => f.slice(0, -PROFILE_EXT.length))
|
|
@@ -325,7 +352,7 @@ export function packageNames(tex: TexRequirements): string[] {
|
|
|
325
352
|
*/
|
|
326
353
|
export function requirementsFor(
|
|
327
354
|
preset: { readonly label: string; readonly tex: TexRequirements } | null,
|
|
328
|
-
dir: string =
|
|
355
|
+
dir: string = presetsDir(),
|
|
329
356
|
): PaperRequirements {
|
|
330
357
|
const base = readProfile(dir, BASE_PROFILE);
|
|
331
358
|
return preset === null
|
|
@@ -345,7 +372,7 @@ export function requirementsFor(
|
|
|
345
372
|
* builds any paper.
|
|
346
373
|
*/
|
|
347
374
|
export function declaredUnion(
|
|
348
|
-
dir: string =
|
|
375
|
+
dir: string = presetsDir(),
|
|
349
376
|
extra: readonly TexRequirements[] = [],
|
|
350
377
|
): {
|
|
351
378
|
readonly tex: TexRequirements;
|