paperlint 4.2.0 → 6.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/CLAUDE.md +15 -2
- package/CONTRIBUTING.md +3 -0
- package/README.md +23 -12
- package/action.harness.mjs +54 -36
- package/action.yml +12 -27
- package/bin/paperlint.mjs +5 -1
- package/dist/adapters/latex/includes.d.ts +5 -1
- package/dist/adapters/latex/includes.d.ts.map +1 -1
- package/dist/adapters/latex/includes.js +8 -2
- package/dist/adapters/latex/includes.js.map +1 -1
- 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/layout.d.ts +27 -0
- package/dist/adapters/latex/layout.d.ts.map +1 -0
- package/dist/adapters/latex/layout.js +62 -0
- package/dist/adapters/latex/layout.js.map +1 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -1
- package/dist/adapters/latex/nodes.js +11 -1
- package/dist/adapters/latex/nodes.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +5 -1
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +7 -3
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +2 -8
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +92 -42
- package/dist/cli.js.map +1 -1
- package/dist/domain/anonymity.d.ts +68 -0
- package/dist/domain/anonymity.d.ts.map +1 -0
- package/dist/domain/anonymity.js +94 -0
- package/dist/domain/anonymity.js.map +1 -0
- package/dist/domain/body-pages.d.ts +69 -0
- package/dist/domain/body-pages.d.ts.map +1 -0
- package/dist/domain/body-pages.js +141 -0
- package/dist/domain/body-pages.js.map +1 -0
- package/dist/domain/paper-source.js +2 -2
- package/dist/domain/tex-document.d.ts +9 -0
- package/dist/domain/tex-document.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.d.ts +5 -1
- package/dist/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +6 -3
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/eslint-rules/paper-context.d.ts +20 -0
- package/dist/eslint-rules/paper-context.d.ts.map +1 -0
- package/dist/eslint-rules/paper-context.js +44 -0
- package/dist/eslint-rules/paper-context.js.map +1 -0
- package/dist/eslint-rules/paper-typography.d.ts +1 -1
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +32 -18
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.js +2 -2
- package/dist/eslint-rules/tex-build.d.ts.map +1 -1
- package/dist/eslint-rules/tex-build.js +7 -1
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/facts-file.d.ts +28 -2
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +54 -9
- package/dist/facts-file.js.map +1 -1
- package/dist/init.d.ts +4 -19
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +24 -29
- package/dist/init.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +2 -1
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/package-dirs.d.ts +2 -0
- package/dist/package-dirs.d.ts.map +1 -1
- package/dist/package-dirs.js +8 -0
- package/dist/package-dirs.js.map +1 -1
- package/dist/paper-files.d.ts +2 -2
- package/dist/paper-includes.d.ts +17 -0
- package/dist/paper-includes.d.ts.map +1 -0
- package/dist/paper-includes.js +51 -0
- package/dist/paper-includes.js.map +1 -0
- package/dist/paper-settings.d.ts +11 -0
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +38 -4
- package/dist/paper-settings.js.map +1 -1
- package/dist/pdf-content.d.ts +40 -0
- package/dist/pdf-content.d.ts.map +1 -0
- package/dist/pdf-content.js +97 -0
- package/dist/pdf-content.js.map +1 -0
- package/dist/pdf-facts.d.ts +30 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +37 -1
- package/dist/pdf-facts.js.map +1 -1
- package/dist/ports/latex.d.ts +3 -1
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +4 -0
- package/dist/presets.js.map +1 -1
- package/dist/rules-config.d.ts +1 -0
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.js +1 -1
- package/dist/structure.js.map +1 -1
- package/dist/tex-paper.d.ts +17 -3
- package/dist/tex-paper.d.ts.map +1 -1
- package/dist/tex-paper.js +36 -38
- package/dist/tex-paper.js.map +1 -1
- package/dist/tex-requirements.d.ts +4 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +3 -0
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +7 -1
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +40 -0
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts +38 -8
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +179 -41
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +23 -20
- package/docs/e2e.md +32 -11
- package/docs/install.md +8 -8
- package/docs/prior-art/paper-folder-scaffolding.md +4 -7
- package/docs/rules/anonymity/identity.md +111 -0
- package/docs/rules/format/layout-override.md +61 -0
- package/docs/rules/format/page-limit.md +108 -0
- package/docs/rules/pdf/body-size.md +87 -0
- package/docs/rules.md +55 -46
- package/eslint-rules/latex-language.ts +10 -4
- package/eslint-rules/paper-context.test.ts +47 -0
- package/eslint-rules/paper-context.ts +57 -0
- package/eslint-rules/paper-typography.ts +67 -23
- package/eslint-rules/pdf-last-page-balance.harness.mjs +6 -1
- package/eslint-rules/pdf-last-page-balance.ts +2 -2
- package/eslint-rules/tex-build.ts +8 -1
- package/fixtures/build-e2e/aidc/expect.json +1 -1
- package/fixtures/build-e2e/aidc/paperlint.json +1 -1
- package/fixtures/build-e2e/anon-ack/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-ack/expect.json +19 -0
- package/fixtures/build-e2e/anon-ack/paper.tex +31 -0
- package/fixtures/build-e2e/anon-ack/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-author/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-author/expect.json +19 -0
- package/fixtures/build-e2e/anon-author/paper.tex +29 -0
- package/fixtures/build-e2e/anon-author/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-bib/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-bib/expect.json +20 -0
- package/fixtures/build-e2e/anon-bib/paper.tex +35 -0
- package/fixtures/build-e2e/anon-bib/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-href/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-href/expect.json +19 -0
- package/fixtures/build-e2e/anon-href/paper.tex +30 -0
- package/fixtures/build-e2e/anon-href/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-macro/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-macro/expect.json +19 -0
- package/fixtures/build-e2e/anon-macro/paper.tex +30 -0
- package/fixtures/build-e2e/anon-macro/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-pdfauthor/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-pdfauthor/expect.json +19 -0
- package/fixtures/build-e2e/anon-pdfauthor/paper.tex +31 -0
- package/fixtures/build-e2e/anon-pdfauthor/paperlint.json +1 -0
- package/fixtures/build-e2e/unbalanced/expect.json +2 -2
- package/fixtures/real-markdown-paper/baseline.json +1 -2
- package/lib/paper-config.ts +3 -1
- package/package.json +1 -1
- package/presets/acm-sigconf.jsonc +3 -1
- package/presets/aidc.jsonc +16 -17
- package/presets/ieee-conference.jsonc +8 -0
- package/presets/venue-profile.schema.json +9 -0
- package/scripts/check.ts +2 -2
- package/scripts/eslint-report-guard.mjs +5 -2
- package/scripts/harness-api.frozen.json +0 -1
- package/skills/paper-pipeline/references/pipeline-status-template.md +2 -16
- package/skills/render-paper/extract-pdf-facts.harness.ts +3 -3
- package/src/adapters/latex/includes.ts +10 -2
- package/src/adapters/latex/index.test.ts +8 -0
- package/src/adapters/latex/index.ts +3 -0
- package/src/adapters/latex/layout.ts +106 -0
- package/src/adapters/latex/nodes.ts +11 -3
- package/src/adapters/latex/parse.ts +5 -1
- package/src/build.harness.mjs +6 -4
- package/src/build.test.ts +23 -1
- package/src/build.ts +7 -3
- package/src/cli.harness.mjs +22 -54
- package/src/cli.test.ts +15 -18
- package/src/cli.ts +114 -46
- package/src/domain/anonymity.test.ts +163 -0
- package/src/domain/anonymity.ts +129 -0
- package/src/domain/body-pages.test.ts +300 -0
- package/src/domain/body-pages.ts +223 -0
- package/src/domain/paper-source.ts +2 -2
- package/src/domain/tex-document.ts +10 -0
- package/src/facts-file.test.ts +100 -4
- package/src/facts-file.ts +91 -9
- package/src/init.test.ts +33 -13
- package/src/init.ts +25 -48
- package/src/linted-set.test.ts +11 -1
- package/src/new-paper.harness.mjs +1 -5
- package/src/new-paper.test.ts +7 -1
- package/src/package-dirs.ts +9 -0
- package/src/paper-files.ts +2 -2
- package/src/paper-includes.test.ts +161 -0
- package/src/paper-includes.ts +77 -0
- package/src/paper-settings.test.ts +63 -6
- package/src/paper-settings.ts +53 -5
- package/src/pdf-content.test.ts +71 -0
- package/src/pdf-content.ts +125 -0
- package/src/pdf-facts.test.ts +110 -2
- package/src/pdf-facts.ts +106 -8
- package/src/ports/latex.ts +3 -0
- package/src/presets.ts +8 -8
- package/src/rule-cases.test.ts +29 -33
- package/src/rule-docs.test.ts +0 -3
- package/src/rules-config.ts +1 -0
- package/src/structure.harness.ts +1 -3
- package/src/structure.ts +1 -1
- package/src/tex-paper.test.ts +78 -61
- package/src/tex-paper.ts +57 -48
- package/src/tex-requirements.ts +9 -0
- package/src/tex-venue-rules.test.ts +111 -6
- package/src/tex-venue-rules.ts +57 -2
- package/src/types.ts +2 -0
- package/src/venue-rules.test.ts +351 -39
- package/src/venue-rules.ts +247 -47
- package/templates/paper/PIPELINE-STATUS.md +0 -4
- package/dist/action-ref.d.ts +0 -12
- package/dist/action-ref.d.ts.map +0 -1
- package/dist/action-ref.js +0 -16
- package/dist/action-ref.js.map +0 -1
- package/dist/eslint-rules/paper-research-question.d.ts +0 -31
- package/dist/eslint-rules/paper-research-question.d.ts.map +0 -1
- package/dist/eslint-rules/paper-research-question.js +0 -188
- package/dist/eslint-rules/paper-research-question.js.map +0 -1
- package/docs/rules/tex/missing-input.md +0 -58
- package/eslint-rules/paper-research-question.harness.mjs +0 -212
- package/eslint-rules/paper-research-question.ts +0 -199
- package/fixtures/paper-research-question/comment-only/PIPELINE-STATUS.md +0 -9
- package/fixtures/paper-research-question/comment-only/paper.tex +0 -7
- package/fixtures/paper-research-question/declared-not-in-paper/PIPELINE-STATUS.md +0 -10
- package/fixtures/paper-research-question/declared-not-in-paper/paper.tex +0 -6
- package/fixtures/paper-research-question/draft/PIPELINE-STATUS.md +0 -6
- package/fixtures/paper-research-question/draft/paper.tex +0 -2
- package/fixtures/paper-research-question/markdown-no-rq/PIPELINE-STATUS.md +0 -9
- package/fixtures/paper-research-question/markdown-no-rq/paper.md +0 -4
- package/fixtures/paper-research-question/shipped-no-rq/PIPELINE-STATUS.md +0 -12
- package/fixtures/paper-research-question/shipped-no-rq/paper.tex +0 -3
- package/fixtures/paper-research-question/shipped-with-rq/PIPELINE-STATUS.md +0 -10
- package/fixtures/paper-research-question/shipped-with-rq/paper.tex +0 -2
- package/src/action-ref.test.ts +0 -26
- package/src/action-ref.ts +0 -15
|
@@ -31,6 +31,7 @@ import { outlineOf } from "./outline.ts";
|
|
|
31
31
|
import { parseLatex } from "./parse.ts";
|
|
32
32
|
import { bodyProse } from "./prose.ts";
|
|
33
33
|
import { renderedRuns } from "./rendered.ts";
|
|
34
|
+
import { layoutOverridesOf } from "./layout.ts";
|
|
34
35
|
|
|
35
36
|
export {
|
|
36
37
|
documentClassOf,
|
|
@@ -51,6 +52,7 @@ export { outlineOf } from "./outline.ts";
|
|
|
51
52
|
export { parseLatex, type ParsedTex } from "./parse.ts";
|
|
52
53
|
export { bodyProse } from "./prose.ts";
|
|
53
54
|
export { renderedRuns } from "./rendered.ts";
|
|
55
|
+
export { layoutOverridesOf } from "./layout.ts";
|
|
54
56
|
|
|
55
57
|
/** The `LatexReader` port over unified-latex: each call parses its source once. */
|
|
56
58
|
export const latexReader: LatexReader = {
|
|
@@ -62,4 +64,5 @@ export const latexReader: LatexReader = {
|
|
|
62
64
|
documentBody: (src) => documentBodyOf(parseLatex(src)),
|
|
63
65
|
template: parseTemplate,
|
|
64
66
|
withDocumentClass: (src, want) => replaceDocumentClass(parseLatex(src), want),
|
|
67
|
+
layoutOverrides: (src) => layoutOverridesOf(parseLatex(src)),
|
|
65
68
|
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a source changes the page layout its class sets for the whole document — the input of
|
|
3
|
+
* `format/layout-override`. Read from the parse tree, never the text: a command in a comment is not
|
|
4
|
+
* a command.
|
|
5
|
+
*
|
|
6
|
+
* What counts, each by its node:
|
|
7
|
+
*
|
|
8
|
+
* \usepackage{geometry} · \RequirePackage{…,geometry} the package that re-sets the margins
|
|
9
|
+
* \geometry{…} · \newgeometry{…} its commands
|
|
10
|
+
* \setlength / \addtolength on a layout length \textheight \textwidth \topmargin …
|
|
11
|
+
* \linespread{…} · \renewcommand{\baselinestretch} the line spacing
|
|
12
|
+
*
|
|
13
|
+
* Local spacing — a negative `\vspace` or `\vskip` around one float — is not a layout the template
|
|
14
|
+
* sets, and no venue's call names it; it is not read.
|
|
15
|
+
*
|
|
16
|
+
* A comment and a `comment` environment are not read — the parser hands both back as text, not
|
|
17
|
+
* macros. Everything else is — the bibliography, math, floats — and a macro definition too: a
|
|
18
|
+
* `\newcommand` whose body sets a layout length does so wherever it is used. Conditionals are not
|
|
19
|
+
* evaluated.
|
|
20
|
+
*/
|
|
21
|
+
import type { LayoutOverride } from "../../domain/tex-document.ts";
|
|
22
|
+
import {
|
|
23
|
+
collapse,
|
|
24
|
+
isList,
|
|
25
|
+
macroPlace,
|
|
26
|
+
mandatory,
|
|
27
|
+
textOf,
|
|
28
|
+
visited,
|
|
29
|
+
type Macro,
|
|
30
|
+
} from "./nodes.ts";
|
|
31
|
+
import type { ParsedTex } from "./parse.ts";
|
|
32
|
+
|
|
33
|
+
/** The lengths the class sets for the page and the line; changing one changes the venue's layout. */
|
|
34
|
+
export const LAYOUT_LENGTHS: ReadonlySet<string> = new Set([
|
|
35
|
+
"textheight",
|
|
36
|
+
"textwidth",
|
|
37
|
+
"topmargin",
|
|
38
|
+
"oddsidemargin",
|
|
39
|
+
"evensidemargin",
|
|
40
|
+
"columnsep",
|
|
41
|
+
"headsep",
|
|
42
|
+
"headheight",
|
|
43
|
+
"footskip",
|
|
44
|
+
"baselineskip",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/** The macro a mandatory argument holds first, or undefined: `{\textheight}` → `textheight`. */
|
|
48
|
+
function firstMacro(m: Macro, i: number): string | undefined {
|
|
49
|
+
const first = mandatory(m)[i]?.content.find((n) => n.type !== "whitespace");
|
|
50
|
+
return first?.type === "macro" ? first.content : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const argText = (m: Macro, i: number): string =>
|
|
54
|
+
collapse(textOf(mandatory(m)[i]?.content));
|
|
55
|
+
|
|
56
|
+
/** What names a macro that overrides the layout; null: this use of it does not. */
|
|
57
|
+
type Namer = (m: Macro) => string | null;
|
|
58
|
+
|
|
59
|
+
const usesGeometry: Namer = (m) =>
|
|
60
|
+
argText(m, 0)
|
|
61
|
+
.split(",")
|
|
62
|
+
.map((p) => p.trim())
|
|
63
|
+
.includes("geometry")
|
|
64
|
+
? `\\${m.content}{geometry}`
|
|
65
|
+
: null;
|
|
66
|
+
|
|
67
|
+
const setsLayoutLength: Namer = (m) => {
|
|
68
|
+
const length = firstMacro(m, 0);
|
|
69
|
+
return length !== undefined && LAYOUT_LENGTHS.has(length)
|
|
70
|
+
? `\\${m.content}{\\${length}}`
|
|
71
|
+
: null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Each macro that may override the layout, and how to tell whether this use of it does. */
|
|
75
|
+
const OVERRIDES: ReadonlyMap<string, Namer> = new Map<string, Namer>([
|
|
76
|
+
["usepackage", usesGeometry],
|
|
77
|
+
["RequirePackage", usesGeometry],
|
|
78
|
+
["geometry", (m) => `\\${m.content}`],
|
|
79
|
+
["newgeometry", (m) => `\\${m.content}`],
|
|
80
|
+
["linespread", (m) => `\\linespread{${argText(m, 0)}}`],
|
|
81
|
+
["setlength", setsLayoutLength],
|
|
82
|
+
["addtolength", setsLayoutLength],
|
|
83
|
+
[
|
|
84
|
+
"renewcommand",
|
|
85
|
+
(m) =>
|
|
86
|
+
firstMacro(m, 0) === "baselinestretch"
|
|
87
|
+
? "\\renewcommand{\\baselinestretch}"
|
|
88
|
+
: null,
|
|
89
|
+
],
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
/** How a message names a macro that overrides the layout, or null when it does not. */
|
|
93
|
+
const overrideOf: Namer = (m) => OVERRIDES.get(m.content)?.(m) ?? null;
|
|
94
|
+
|
|
95
|
+
/** Every layout override in the source, in document order. */
|
|
96
|
+
export function layoutOverridesOf(t: ParsedTex): readonly LayoutOverride[] {
|
|
97
|
+
// Nothing is pruned: a comment, and a `comment` environment, reach the tree as leaves of text.
|
|
98
|
+
const lists = visited(t.root, () => false).filter(isList);
|
|
99
|
+
return lists.flatMap((list) =>
|
|
100
|
+
list.flatMap((n) => {
|
|
101
|
+
if (n.type !== "macro") return [];
|
|
102
|
+
const command = overrideOf(n);
|
|
103
|
+
return command === null ? [] : [{ command, place: macroPlace(n) }];
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -63,9 +63,17 @@ const NONE: Shown = { kind: "none" };
|
|
|
63
63
|
*/
|
|
64
64
|
const SHOWN_ARGUMENT: ReadonlyMap<string, Shown> = new Map<string, Shown>([
|
|
65
65
|
["texorpdfstring", { kind: "argument", at: 0 }],
|
|
66
|
-
...[
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
...[
|
|
67
|
+
"color",
|
|
68
|
+
"fontsize",
|
|
69
|
+
"vspace",
|
|
70
|
+
"hspace",
|
|
71
|
+
"setlength",
|
|
72
|
+
"addtolength",
|
|
73
|
+
"linespread",
|
|
74
|
+
"geometry",
|
|
75
|
+
"newgeometry",
|
|
76
|
+
].map((m) => [m, NONE] as const),
|
|
69
77
|
]);
|
|
70
78
|
|
|
71
79
|
/** The text a (non-key) macro typesets: see `SHOWN_ARGUMENT`. */
|
|
@@ -82,7 +82,11 @@ const LINK_SIGNATURES = Object.fromEntries(signed(["href"], "m m"));
|
|
|
82
82
|
* Declarations unified-latex has no signature for: signed so their arguments (sizes, not text)
|
|
83
83
|
* attach to them instead of standing beside them as groups a reader would seem to see.
|
|
84
84
|
*/
|
|
85
|
-
const DECLARATION_SIGNATURES = Object.fromEntries(
|
|
85
|
+
const DECLARATION_SIGNATURES = Object.fromEntries([
|
|
86
|
+
...signed(["fontsize"], "m m"),
|
|
87
|
+
// The layout commands `format/layout-override` reads: a spacing factor, geometry's key list.
|
|
88
|
+
...signed(["linespread", "geometry", "newgeometry"], "m"),
|
|
89
|
+
]);
|
|
86
90
|
|
|
87
91
|
/**
|
|
88
92
|
* Macros that define other macros or environments. A heading in a body is no section where it is
|
package/src/build.harness.mjs
CHANGED
|
@@ -264,8 +264,10 @@ try {
|
|
|
264
264
|
tex.calls.every((c) => c.cwd === clean),
|
|
265
265
|
);
|
|
266
266
|
check(
|
|
267
|
-
"🔴 pdflatex got paperlint's venues directory on TEXINPUTS, trailing separator kept",
|
|
268
|
-
tex.calls.every(
|
|
267
|
+
"🔴 pdflatex got the paper's directory, then paperlint's venues directory, on TEXINPUTS, trailing separator kept",
|
|
268
|
+
tex.calls.every(
|
|
269
|
+
(c) => c.texinputs === `.${delimiter}${venues}${delimiter}`,
|
|
270
|
+
),
|
|
269
271
|
);
|
|
270
272
|
|
|
271
273
|
// ── the measure step: facts for the lint rules ────────────────────────────────────────
|
|
@@ -287,8 +289,8 @@ try {
|
|
|
287
289
|
if (existsSync(factsFile)) {
|
|
288
290
|
const f = JSON.parse(readFileSync(factsFile, "utf8"));
|
|
289
291
|
check(
|
|
290
|
-
"measure: the facts are schema
|
|
291
|
-
f.schema ===
|
|
292
|
+
"measure: the facts are schema 3, about paper.pdf, with the last page's heights",
|
|
293
|
+
f.schema === 3 &&
|
|
292
294
|
f.pdf === "paper.pdf" &&
|
|
293
295
|
JSON.stringify(f.last_page_cols_pt) === "[400,400]",
|
|
294
296
|
);
|
package/src/build.test.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import assert from "node:assert/strict";
|
|
8
8
|
import { existsSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { join } from "node:path";
|
|
9
|
+
import { delimiter, join } from "node:path";
|
|
10
|
+
import { texInputsDir } from "./package-dirs.ts";
|
|
10
11
|
import { test } from "vitest";
|
|
11
12
|
import { present, useTempDir, writeTree } from "../test/support.ts";
|
|
12
13
|
import {
|
|
@@ -114,6 +115,11 @@ const reader =
|
|
|
114
115
|
},
|
|
115
116
|
last,
|
|
116
117
|
layout: [],
|
|
118
|
+
pageTexts: [],
|
|
119
|
+
links: [],
|
|
120
|
+
bibAnchorPage: null,
|
|
121
|
+
appendixAnchorPage: null,
|
|
122
|
+
metadata: {},
|
|
117
123
|
},
|
|
118
124
|
});
|
|
119
125
|
const quiet = () => {};
|
|
@@ -344,6 +350,22 @@ test("paths: a paper that IS the working directory is named by its full path", a
|
|
|
344
350
|
assert.equal(existsSync(join(dir, "paper.pdf")), false);
|
|
345
351
|
});
|
|
346
352
|
|
|
353
|
+
test("🔴 pdflatex looks for an \\input in the paper's directory, then paperlint's inputs, then the system tree", async () => {
|
|
354
|
+
const dir = paper();
|
|
355
|
+
const f: Fake = {};
|
|
356
|
+
const inner = fakeRun(f);
|
|
357
|
+
const seen: (string | undefined)[] = [];
|
|
358
|
+
await build(dir, f, {
|
|
359
|
+
run: (bin, args, opts) => {
|
|
360
|
+
if (bin === "pdflatex") seen.push(opts.env?.["TEXINPUTS"]);
|
|
361
|
+
return inner(bin, args, opts);
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
// The same search path the lint rules read includes through (`texSearchPath`).
|
|
365
|
+
// A pass runs in the paper's directory, where `.` is that directory.
|
|
366
|
+
assert.equal(seen[0], [".", texInputsDir(), ""].join(delimiter));
|
|
367
|
+
});
|
|
368
|
+
|
|
347
369
|
test("facts that cannot be read fail the build at `facts`", async () => {
|
|
348
370
|
const { line } = await build(
|
|
349
371
|
paper({ "paper.tex": TEX, "paperlint.json": "{ nope" }),
|
package/src/build.ts
CHANGED
|
@@ -44,7 +44,7 @@ import { createHash } from "node:crypto";
|
|
|
44
44
|
import { delimiter, join, relative } from "node:path";
|
|
45
45
|
// eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
|
|
46
46
|
import { getParser } from "@unified-latex/unified-latex-util-parse";
|
|
47
|
-
import { texInputsDir } from "./package-dirs.ts";
|
|
47
|
+
import { texInputsDir, texSearchPath } from "./package-dirs.ts";
|
|
48
48
|
import { CONFIG_FILE } from "#lib/paper-config";
|
|
49
49
|
import {
|
|
50
50
|
declaredVenue,
|
|
@@ -268,11 +268,15 @@ export const inputsStep: BuildStep = {
|
|
|
268
268
|
required: false,
|
|
269
269
|
applies: () => ({
|
|
270
270
|
yes: true,
|
|
271
|
-
why: `TEXINPUTS += ${texInputsDir()}`,
|
|
271
|
+
why: `TEXINPUTS += the paper's directory, then ${texInputsDir()}`,
|
|
272
272
|
}),
|
|
273
|
+
// The same search path the lint rules read includes through: one answer to which file is meant.
|
|
274
|
+
// A pass runs IN the paper's directory, so that directory is `.` here. TeX names a file it finds
|
|
275
|
+
// through an absolute entry by that absolute path: with the directory itself on TEXINPUTS, the
|
|
276
|
+
// `broken` build fixture's log said `/tmp/…/paper.tex:6:` instead of `./paper.tex:6:` (measured).
|
|
273
277
|
run: (ctx) => ({
|
|
274
278
|
ok: true,
|
|
275
|
-
env: withTexInputs(ctx.env,
|
|
279
|
+
env: withTexInputs(ctx.env, texSearchPath(".")),
|
|
276
280
|
}),
|
|
277
281
|
};
|
|
278
282
|
|
package/src/cli.harness.mjs
CHANGED
|
@@ -132,8 +132,8 @@ check(
|
|
|
132
132
|
Array.isArray(cfg) && ruleBlocks(cfg).length === 4,
|
|
133
133
|
);
|
|
134
134
|
check(
|
|
135
|
-
"with a LaTeX language
|
|
136
|
-
ruleBlocks(buildConfig({}, {})).length ===
|
|
135
|
+
"with a LaTeX language two blocks are added: paper.tex, and the files it includes",
|
|
136
|
+
ruleBlocks(buildConfig({}, {})).length === 6,
|
|
137
137
|
);
|
|
138
138
|
check(
|
|
139
139
|
"🔴 the project's paper TEMPLATE directory is ignored — flat config does not skip dot-directories",
|
|
@@ -181,7 +181,7 @@ check(
|
|
|
181
181
|
writeFileSync(join(paper, "paper.md"), "# Intro\n\nRQ1: does it hold?\n");
|
|
182
182
|
writeFileSync(
|
|
183
183
|
join(paper, "PIPELINE-STATUS.md"),
|
|
184
|
-
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: 100\n source: versions/s.tex\n sourceBytes: 4\
|
|
184
|
+
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: 100\n source: versions/s.tex\n sourceBytes: 4\n---\n# S\n\n| id | note |\n|---|---|\n| cites | bib-authors run |\n`,
|
|
185
185
|
);
|
|
186
186
|
writeFileSync(
|
|
187
187
|
join(root, "package.json"),
|
|
@@ -441,32 +441,14 @@ check(
|
|
|
441
441
|
!existsSync(join(dir, WORKFLOW_PATH)),
|
|
442
442
|
);
|
|
443
443
|
check(
|
|
444
|
-
"and the CI
|
|
445
|
-
|
|
446
|
-
/paths: writing/.test(out.text()),
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
{
|
|
450
|
-
// Guards: the printed step carries the tag of the running release, not a placeholder.
|
|
451
|
-
const dir = project("ci-printed-pin", { papers: ["writing"] });
|
|
452
|
-
const out = say();
|
|
453
|
-
await init(dir, {
|
|
454
|
-
log: out.log,
|
|
455
|
-
err: out.log,
|
|
456
|
-
interactive: false,
|
|
457
|
-
run: haveAll,
|
|
458
|
-
version: "1.2.3",
|
|
459
|
-
});
|
|
460
|
-
check(
|
|
461
|
-
"🔴 a released version — the printed step is pinned to its tag, no <commit-sha>",
|
|
462
|
-
/uses: zernie\/paperlint@v1\.2\.3\b/.test(out.text()) &&
|
|
463
|
-
!/<commit-sha>/.test(out.text()),
|
|
444
|
+
"and the CI steps are still printed — they just get pasted in by hand",
|
|
445
|
+
out.text().includes("- uses: ./node_modules/paperlint"),
|
|
464
446
|
);
|
|
465
447
|
}
|
|
466
448
|
{
|
|
467
449
|
const dir = project("ci-yes", { papers: ["writing"] });
|
|
468
450
|
const asked = [];
|
|
469
|
-
const wf = await offerWorkflow(dir,
|
|
451
|
+
const wf = await offerWorkflow(dir, {
|
|
470
452
|
interactive: true,
|
|
471
453
|
ask: async (q) => {
|
|
472
454
|
asked.push(q);
|
|
@@ -477,16 +459,22 @@ check(
|
|
|
477
459
|
"saying yes WRITES the workflow",
|
|
478
460
|
wf === "written" && existsSync(join(dir, WORKFLOW_PATH)),
|
|
479
461
|
);
|
|
462
|
+
const steps =
|
|
463
|
+
yamlLoad(readFileSync(join(dir, WORKFLOW_PATH), "utf8"))?.jobs?.papers
|
|
464
|
+
?.steps ?? [];
|
|
480
465
|
check(
|
|
481
|
-
"
|
|
482
|
-
|
|
466
|
+
"🔴 the workflow runs the action from the installed package — one revision, set by the lockfile",
|
|
467
|
+
steps.some((st) => st.uses === "./node_modules/paperlint") &&
|
|
468
|
+
!steps.some((st) =>
|
|
469
|
+
String(st.uses ?? "").startsWith("zernie/paperlint"),
|
|
470
|
+
),
|
|
483
471
|
);
|
|
484
472
|
check(
|
|
485
|
-
"
|
|
486
|
-
|
|
473
|
+
"and it repeats no papers directory — the action reads papersDir from paperlint.json",
|
|
474
|
+
steps.every((st) => st.with?.paths === undefined),
|
|
487
475
|
);
|
|
488
476
|
check("the question is asked exactly once", asked.length === 1);
|
|
489
|
-
const again = await offerWorkflow(dir,
|
|
477
|
+
const again = await offerWorkflow(dir, {
|
|
490
478
|
interactive: true,
|
|
491
479
|
ask: async () => "y",
|
|
492
480
|
});
|
|
@@ -495,29 +483,9 @@ check(
|
|
|
495
483
|
again === "kept",
|
|
496
484
|
);
|
|
497
485
|
}
|
|
498
|
-
{
|
|
499
|
-
const dir = project("ci-pinned", { papers: ["writing"] });
|
|
500
|
-
const wf = await offerWorkflow(dir, "writing", {
|
|
501
|
-
interactive: true,
|
|
502
|
-
ask: async () => "y",
|
|
503
|
-
version: "1.2.3",
|
|
504
|
-
});
|
|
505
|
-
const yaml = existsSync(join(dir, WORKFLOW_PATH))
|
|
506
|
-
? readFileSync(join(dir, WORKFLOW_PATH), "utf8")
|
|
507
|
-
: "";
|
|
508
|
-
const uses = (yamlLoad(yaml)?.jobs?.papers?.steps ?? []).map(
|
|
509
|
-
(s) => s.uses,
|
|
510
|
-
);
|
|
511
|
-
check(
|
|
512
|
-
"🔴 a released version — the written workflow is pinned to its tag, no <commit-sha> left",
|
|
513
|
-
wf === "written" &&
|
|
514
|
-
uses.includes("zernie/paperlint@v1.2.3") &&
|
|
515
|
-
!yaml.includes("<commit-sha>"),
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
486
|
{
|
|
519
487
|
const dir = project("ci-no", { papers: ["writing"] });
|
|
520
|
-
const no = await offerWorkflow(dir,
|
|
488
|
+
const no = await offerWorkflow(dir, {
|
|
521
489
|
interactive: true,
|
|
522
490
|
ask: async () => "",
|
|
523
491
|
});
|
|
@@ -532,7 +500,7 @@ check(
|
|
|
532
500
|
// 🔴 THE `.catch` HERE IS LOAD-BEARING, NOT CAUTION. A leaked exception is a PROPERTY that
|
|
533
501
|
// gets asserted; without it, the harness crashes instead of failing a named assertion,
|
|
534
502
|
// i.e. the real defect would look like a hole in the test rather than a caught one.
|
|
535
|
-
const aborted = await offerWorkflow(dir,
|
|
503
|
+
const aborted = await offerWorkflow(dir, {
|
|
536
504
|
interactive: true,
|
|
537
505
|
ask: async () => {
|
|
538
506
|
throw new Error("Aborted with Ctrl+D");
|
|
@@ -733,7 +701,7 @@ check(
|
|
|
733
701
|
"x".repeat(100),
|
|
734
702
|
);
|
|
735
703
|
const status = (bytes) =>
|
|
736
|
-
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: ${bytes}\n source: versions/s.tex\n sourceBytes: 4\
|
|
704
|
+
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: ${bytes}\n source: versions/s.tex\n sourceBytes: 4\n---\n# S\n\n| id | note |\n|---|---|\n| cites | bib-authors run |\n`;
|
|
737
705
|
writeFileSync(join(paper, "versions", "s.tex"), "abcd");
|
|
738
706
|
writeFileSync(join(paper, "paper.md"), "# Intro\n\nRQ1: does it hold?\n");
|
|
739
707
|
|
|
@@ -822,7 +790,7 @@ check(
|
|
|
822
790
|
);
|
|
823
791
|
writeFileSync(join(paper, "paper.md"), "# Intro\n\nRQ1: does it hold?\n");
|
|
824
792
|
const status = (bytes) =>
|
|
825
|
-
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: ${bytes}\n source: versions/s.tex\n sourceBytes: 4\
|
|
793
|
+
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: ${bytes}\n source: versions/s.tex\n sourceBytes: 4\n---\n# S\n\n| id | note |\n|---|---|\n| cites | bib-authors run |\n`;
|
|
826
794
|
writeFileSync(join(paper, "PIPELINE-STATUS.md"), status(100));
|
|
827
795
|
|
|
828
796
|
// findProjectRoot — kept separate from the run so the failure is distinguishable
|
|
@@ -934,7 +902,7 @@ check(
|
|
|
934
902
|
);
|
|
935
903
|
writeFileSync(
|
|
936
904
|
join(paper, "PIPELINE-STATUS.md"),
|
|
937
|
-
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: 100\n source: versions/s.tex\n sourceBytes: 4\
|
|
905
|
+
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: 100\n source: versions/s.tex\n sourceBytes: 4\n---\n# S\n\n| id | note |\n|---|---|\n| cites | bib-authors run |\n`,
|
|
938
906
|
);
|
|
939
907
|
writeFileSync(
|
|
940
908
|
join(root, "paperlint.json"),
|
package/src/cli.test.ts
CHANGED
|
@@ -14,7 +14,6 @@ import type { runToolchain } from "./toolchain.ts";
|
|
|
14
14
|
import {
|
|
15
15
|
chooseVenue,
|
|
16
16
|
initTexLive,
|
|
17
|
-
ownVersion,
|
|
18
17
|
paperRuleBlocks,
|
|
19
18
|
parseArgs,
|
|
20
19
|
parseSettings,
|
|
@@ -259,7 +258,7 @@ test("build: a markdown-only paper is refused", async () => {
|
|
|
259
258
|
code: 1,
|
|
260
259
|
out: [
|
|
261
260
|
"papers/md",
|
|
262
|
-
` inputs: TEXINPUTS += ${venues}`,
|
|
261
|
+
` inputs: TEXINPUTS += the paper's directory, then ${venues}`,
|
|
263
262
|
" compile: refused — no paper.tex; paperlint compiles LaTeX, and this paper has none",
|
|
264
263
|
" measure: skipped — nothing is compiled",
|
|
265
264
|
" references: skipped — nothing is compiled",
|
|
@@ -288,7 +287,7 @@ test("build: with no TeX Live a dry run says where a real run stops, and a real
|
|
|
288
287
|
out: [
|
|
289
288
|
`engine: none — a real run would stop here: paperlint build: no TeX Live with every package these papers need — run \`npx paperlint toolchain\` (${missing})`,
|
|
290
289
|
"papers/tex",
|
|
291
|
-
` inputs: TEXINPUTS += ${venues}`,
|
|
290
|
+
` inputs: TEXINPUTS += the paper's directory, then ${venues}`,
|
|
292
291
|
" compile: paper.tex (\\documentclass{article})",
|
|
293
292
|
" measure: pdf.js → _build/paper.facts.json (facts for the lint rules; nothing is judged here)",
|
|
294
293
|
" references: online: citations exist, titles and authors match → _build/references.json (never fails the build)",
|
|
@@ -529,21 +528,6 @@ test("chooseVenue: a question that fails (the stream ended) takes the default, n
|
|
|
529
528
|
assert.deepEqual(r, { ok: true, value: null });
|
|
530
529
|
});
|
|
531
530
|
|
|
532
|
-
test("ownVersion: the manifest's version; an unreadable manifest or a non-string version is undefined", () => {
|
|
533
|
-
assert.deepEqual(
|
|
534
|
-
[
|
|
535
|
-
ownVersion(() => '{"version": "1.2.3"}'),
|
|
536
|
-
ownVersion(() => "{ nope"),
|
|
537
|
-
ownVersion(() => '{"version": 3}'),
|
|
538
|
-
ownVersion(() => {
|
|
539
|
-
throw new Error("ENOENT");
|
|
540
|
-
}),
|
|
541
|
-
typeof ownVersion(),
|
|
542
|
-
],
|
|
543
|
-
["1.2.3", undefined, undefined, undefined, "string"],
|
|
544
|
-
);
|
|
545
|
-
});
|
|
546
|
-
|
|
547
531
|
test("init's TeX Live install runs the toolchain over the project's papers, never a check", () => {
|
|
548
532
|
const dir = writeTree(join(root, "init-tex"), { "package.json": "{}" });
|
|
549
533
|
const calls: Parameters<typeof runToolchain>[0][] = [];
|
|
@@ -594,3 +578,16 @@ test("`paperlint hook` with no name: exit 2, and the usage names an example", as
|
|
|
594
578
|
err: "`hook` needs a name, e.g. `paperlint hook paper-edit-guard`",
|
|
595
579
|
});
|
|
596
580
|
});
|
|
581
|
+
|
|
582
|
+
test("parseSettings refuses a root `identity` that is not a list of words, and takes one that is", () => {
|
|
583
|
+
const bad = parseSettings({ identity: "Ada" }, "paperlint.json", "/r");
|
|
584
|
+
assert.deepEqual(bad, {
|
|
585
|
+
ok: false,
|
|
586
|
+
error:
|
|
587
|
+
'paperlint.json: "identity" must be a list of strings, each with a letter or digit, got "Ada"',
|
|
588
|
+
});
|
|
589
|
+
assert.equal(
|
|
590
|
+
parseSettings({ identity: ["Ada Example"] }, "paperlint.json", "/r").ok,
|
|
591
|
+
true,
|
|
592
|
+
);
|
|
593
|
+
});
|