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
package/src/pdf-facts.ts
CHANGED
|
@@ -34,6 +34,15 @@ import {
|
|
|
34
34
|
import type { PageLayout, TextBox } from "./domain/page-layout.ts";
|
|
35
35
|
// eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
|
|
36
36
|
import { fillsFor, isUpright } from "./adapters/pdfjs/fill.ts";
|
|
37
|
+
import {
|
|
38
|
+
appendixOutlineDests,
|
|
39
|
+
linkTargetsOf,
|
|
40
|
+
metadataOf,
|
|
41
|
+
namedRefs,
|
|
42
|
+
pageTextOf,
|
|
43
|
+
refOf,
|
|
44
|
+
xmpOf,
|
|
45
|
+
} from "./pdf-content.ts";
|
|
37
46
|
import { messageOf } from "./domain/text.ts";
|
|
38
47
|
import { fieldOf, isRecord, numbersOf } from "./domain/record.ts";
|
|
39
48
|
|
|
@@ -83,12 +92,27 @@ interface Page {
|
|
|
83
92
|
[Symbol.iterator](): Iterator<readonly unknown[]>;
|
|
84
93
|
};
|
|
85
94
|
getViewport(params: { scale: number }): Viewport;
|
|
95
|
+
/** pdf.js's annotations; the link ones carry `url`. */
|
|
96
|
+
getAnnotations(): Promise<readonly unknown[]>;
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
/** A document as this module reads it; pdf.js's `PDFDocumentProxy` is one. */
|
|
89
100
|
interface Doc {
|
|
90
101
|
readonly numPages: number;
|
|
91
102
|
getPage(pageNumber: number): Promise<Page>;
|
|
103
|
+
/** The Info dictionary, and the XMP packet when there is one. */
|
|
104
|
+
getMetadata(): Promise<{
|
|
105
|
+
readonly info: unknown;
|
|
106
|
+
readonly metadata: Iterable<unknown> | null;
|
|
107
|
+
}>;
|
|
108
|
+
/** Every named destination: name → explicit destination. */
|
|
109
|
+
getDestinations(): Promise<unknown>;
|
|
110
|
+
/** The 0-based index of the page a destination's reference names. */
|
|
111
|
+
getPageIndex(ref: object): Promise<number>;
|
|
112
|
+
/** One named destination, or null. */
|
|
113
|
+
getDestination(id: string): Promise<unknown>;
|
|
114
|
+
/** The bookmarks: `{ title, dest, items }`, or null. */
|
|
115
|
+
getOutline(): Promise<unknown>;
|
|
92
116
|
}
|
|
93
117
|
|
|
94
118
|
/** A viewport point, which pdf.js types as `any[]`: the two numbers it holds. */
|
|
@@ -106,6 +130,22 @@ export interface PdfFacts {
|
|
|
106
130
|
readonly fonts: Fonts;
|
|
107
131
|
readonly last: PageText;
|
|
108
132
|
readonly layout: readonly PageLayout[];
|
|
133
|
+
/** Each page's text, in content order, a line break after each line (`pageTextOf`). */
|
|
134
|
+
readonly pageTexts: readonly string[];
|
|
135
|
+
/** The target of every link, with its 1-based page. */
|
|
136
|
+
readonly links: readonly PdfLink[];
|
|
137
|
+
/** The 1-based page of the first bibliography entry hyperref anchored, or null without one. */
|
|
138
|
+
readonly bibAnchorPage: number | null;
|
|
139
|
+
/** The 1-based page the appendix starts on, from hyperref's destinations and bookmarks, or null. */
|
|
140
|
+
readonly appendixAnchorPage: number | null;
|
|
141
|
+
/** The Info dictionary and XMP packet, flat (`metadataOf`). */
|
|
142
|
+
readonly metadata: Readonly<Record<string, string>>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** A link annotation's target, and the page it is on. */
|
|
146
|
+
export interface PdfLink {
|
|
147
|
+
readonly page: number;
|
|
148
|
+
readonly uri: string;
|
|
109
149
|
}
|
|
110
150
|
|
|
111
151
|
export type PdfReadFailure =
|
|
@@ -198,10 +238,17 @@ async function readPage(
|
|
|
198
238
|
page: Page,
|
|
199
239
|
seen: ReadonlySet<string>,
|
|
200
240
|
lib: PdfJs,
|
|
201
|
-
): Promise<{
|
|
241
|
+
): Promise<{
|
|
242
|
+
raw: RawPage;
|
|
243
|
+
items: TextItem[];
|
|
244
|
+
layout: PageLayout;
|
|
245
|
+
text: string;
|
|
246
|
+
links: readonly string[];
|
|
247
|
+
}> {
|
|
202
248
|
// The operator list is what makes pdf.js load a page's fonts into `commonObjs`.
|
|
203
249
|
const ops = await page.getOperatorList();
|
|
204
|
-
const
|
|
250
|
+
const all = (await page.getTextContent()).items;
|
|
251
|
+
const items = all.filter(isText);
|
|
205
252
|
const loaded = loadedFonts(page);
|
|
206
253
|
const named = new Set(items.map((it) => it.fontName));
|
|
207
254
|
const fonts = [...loaded.values()].filter(
|
|
@@ -211,6 +258,8 @@ async function readPage(
|
|
|
211
258
|
raw: { hasText: items.length > 0, fonts },
|
|
212
259
|
items,
|
|
213
260
|
layout: layoutOf(page, items, ops, lib),
|
|
261
|
+
text: pageTextOf(all),
|
|
262
|
+
links: linkTargetsOf(await page.getAnnotations()),
|
|
214
263
|
};
|
|
215
264
|
}
|
|
216
265
|
|
|
@@ -310,12 +359,7 @@ function layoutOf(
|
|
|
310
359
|
* transform, no pages) — `readPdf` below is the one production caller.
|
|
311
360
|
*/
|
|
312
361
|
export async function factsOf(doc: Doc, lib: PdfJs): Promise<PdfRead> {
|
|
313
|
-
const read: {
|
|
314
|
-
page: Page;
|
|
315
|
-
raw: RawPage;
|
|
316
|
-
items: TextItem[];
|
|
317
|
-
layout: PageLayout;
|
|
318
|
-
}[] = [];
|
|
362
|
+
const read: (Awaited<ReturnType<typeof readPage>> & { page: Page })[] = [];
|
|
319
363
|
const seen = new Set<string>();
|
|
320
364
|
for (let i = 1; i <= doc.numPages; i++) {
|
|
321
365
|
const page = await doc.getPage(i);
|
|
@@ -340,10 +384,64 @@ export async function factsOf(doc: Doc, lib: PdfJs): Promise<PdfRead> {
|
|
|
340
384
|
fonts,
|
|
341
385
|
last: pageText(last.page, last.items),
|
|
342
386
|
layout: read.map((r) => r.layout),
|
|
387
|
+
...saidOf(read, await doc.getMetadata()),
|
|
388
|
+
...(await anchorsOf(doc)),
|
|
343
389
|
},
|
|
344
390
|
};
|
|
345
391
|
}
|
|
346
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Where hyperref anchored the bibliography's first entry (the lowest page of a `cite.<key>`
|
|
395
|
+
* destination) and the appendix (the lowest page of an `appendix.<letter>` destination or of a
|
|
396
|
+
* bookmark titled «Appendix…»). A reference pdf.js cannot place is skipped.
|
|
397
|
+
*/
|
|
398
|
+
async function anchorsOf(
|
|
399
|
+
doc: Doc,
|
|
400
|
+
): Promise<Pick<PdfFacts, "bibAnchorPage" | "appendixAnchorPage">> {
|
|
401
|
+
const destinations = await doc.getDestinations();
|
|
402
|
+
const bookmarked = await Promise.all(
|
|
403
|
+
appendixOutlineDests(await doc.getOutline()).map(async (d) =>
|
|
404
|
+
refOf(typeof d === "string" ? await doc.getDestination(d) : d),
|
|
405
|
+
),
|
|
406
|
+
);
|
|
407
|
+
return {
|
|
408
|
+
bibAnchorPage: await lowestPage(doc, namedRefs(destinations, "cite.")),
|
|
409
|
+
appendixAnchorPage: await lowestPage(doc, [
|
|
410
|
+
...namedRefs(destinations, "appendix."),
|
|
411
|
+
...bookmarked.filter((r): r is object => r !== null),
|
|
412
|
+
]),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** The lowest 1-based page any of `refs` names, or null when none can be placed. */
|
|
417
|
+
async function lowestPage(
|
|
418
|
+
doc: Doc,
|
|
419
|
+
refs: readonly object[],
|
|
420
|
+
): Promise<number | null> {
|
|
421
|
+
const pages = await Promise.all(
|
|
422
|
+
refs.map((r) =>
|
|
423
|
+
doc.getPageIndex(r).then(
|
|
424
|
+
(i) => [i + 1],
|
|
425
|
+
() => [],
|
|
426
|
+
),
|
|
427
|
+
),
|
|
428
|
+
);
|
|
429
|
+
const found = pages.flat();
|
|
430
|
+
return found.length === 0 ? null : Math.min(...found);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** What the pages say, from each page's read, and the document's metadata. */
|
|
434
|
+
function saidOf(
|
|
435
|
+
read: readonly Pick<Awaited<ReturnType<typeof readPage>>, "text" | "links">[],
|
|
436
|
+
meta: Awaited<ReturnType<Doc["getMetadata"]>>,
|
|
437
|
+
): Pick<PdfFacts, "pageTexts" | "links" | "metadata"> {
|
|
438
|
+
return {
|
|
439
|
+
pageTexts: read.map((r) => r.text),
|
|
440
|
+
links: read.flatMap((r, i) => r.links.map((uri) => ({ page: i + 1, uri }))),
|
|
441
|
+
metadata: metadataOf(meta.info, xmpOf(meta.metadata)),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
347
445
|
/** pdf.js's own exception names for the two conditions a caller can act on. */
|
|
348
446
|
export function failureOf(e: unknown): PdfRead {
|
|
349
447
|
const name = fieldOf(e, "name");
|
package/src/ports/latex.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Include } from "../domain/paper-source.ts";
|
|
|
6
6
|
import type {
|
|
7
7
|
ClassLine,
|
|
8
8
|
DocumentClass,
|
|
9
|
+
LayoutOverride,
|
|
9
10
|
Outline,
|
|
10
11
|
Passage,
|
|
11
12
|
Span,
|
|
@@ -39,4 +40,6 @@ export interface LatexReader {
|
|
|
39
40
|
* option `want` names (the author's own options stay), or it has none.
|
|
40
41
|
*/
|
|
41
42
|
readonly withDocumentClass: (src: string, want: DocumentClass) => string;
|
|
43
|
+
/** Every command that changes the page layout the class sets: margins, text block, line spacing. */
|
|
44
|
+
readonly layoutOverrides: (src: string) => readonly LayoutOverride[];
|
|
42
45
|
}
|
package/src/presets.ts
CHANGED
|
@@ -83,6 +83,8 @@ export interface Preset {
|
|
|
83
83
|
readonly template: PresetTemplate | null;
|
|
84
84
|
/** Every `name` and `aliases` along the chain: what this venue is called in a paper's text. */
|
|
85
85
|
readonly aliases: readonly string[];
|
|
86
|
+
/** Whether the venue reviews double-blind (`anonymity/identity`); false when no file of the chain says. */
|
|
87
|
+
readonly blind: boolean;
|
|
86
88
|
readonly requiredSections: readonly RequiredSection[];
|
|
87
89
|
readonly tex: TexRequirements;
|
|
88
90
|
readonly format: VenueFormat;
|
|
@@ -196,6 +198,7 @@ export function mergeFormat(
|
|
|
196
198
|
refPtMax: pick("refPtMax"),
|
|
197
199
|
fontsText: pick("fontsText"),
|
|
198
200
|
fontsTitle: pick("fontsTitle"),
|
|
201
|
+
bodyEndsAt: pick("bodyEndsAt"),
|
|
199
202
|
kinds: new Map([...parent.kinds, ...child.kinds]),
|
|
200
203
|
};
|
|
201
204
|
}
|
|
@@ -205,18 +208,13 @@ function merged(
|
|
|
205
208
|
spec: string,
|
|
206
209
|
rootFirst: readonly { readonly preset: PresetFile; readonly file: string }[],
|
|
207
210
|
): Preset {
|
|
208
|
-
const base: {
|
|
209
|
-
name: string | null;
|
|
210
|
-
template: PresetTemplate | null;
|
|
211
|
-
aliases: readonly string[];
|
|
212
|
-
requiredSections: readonly RequiredSection[];
|
|
213
|
-
tex: TexRequirements;
|
|
214
|
-
format: VenueFormat;
|
|
215
|
-
rules: Readonly<Record<string, unknown>>;
|
|
211
|
+
const base: Omit<Preset, "label" | "chain"> & {
|
|
212
|
+
readonly name: string | null;
|
|
216
213
|
} = {
|
|
217
214
|
name: null,
|
|
218
215
|
template: null,
|
|
219
216
|
aliases: [],
|
|
217
|
+
blind: false,
|
|
220
218
|
requiredSections: [],
|
|
221
219
|
tex: NO_REQUIREMENTS,
|
|
222
220
|
format: NO_FORMAT,
|
|
@@ -233,6 +231,7 @@ function merged(
|
|
|
233
231
|
...p.aliases,
|
|
234
232
|
]),
|
|
235
233
|
],
|
|
234
|
+
blind: p.blind ?? acc.blind,
|
|
236
235
|
requiredSections: p.requiredSections ?? acc.requiredSections,
|
|
237
236
|
tex: p.tex ? mergeRequirements(acc.tex, p.tex) : acc.tex,
|
|
238
237
|
format: mergeFormat(acc.format, p.format),
|
|
@@ -245,6 +244,7 @@ function merged(
|
|
|
245
244
|
chain: rootFirst.map((x) => x.file),
|
|
246
245
|
template: m.template,
|
|
247
246
|
aliases: m.aliases,
|
|
247
|
+
blind: m.blind,
|
|
248
248
|
requiredSections: m.requiredSections,
|
|
249
249
|
tex: m.tex,
|
|
250
250
|
format: m.format,
|
package/src/rule-cases.test.ts
CHANGED
|
@@ -43,11 +43,22 @@ const paper = (
|
|
|
43
43
|
|
|
44
44
|
const PDF = "%PDF-1.5 a stand-in for the built PDF";
|
|
45
45
|
const AGENTICDEV = { extends: "paperlint:agenticdev", kind: "short" };
|
|
46
|
+
/** A double-blind venue's paper that declares who wrote it (`anonymity/*`). */
|
|
47
|
+
const BLIND = {
|
|
48
|
+
extends: "paperlint:aidc",
|
|
49
|
+
kind: "regular",
|
|
50
|
+
identity: ["Ada Example", "adaexample"],
|
|
51
|
+
};
|
|
46
52
|
const ACM_TEX = tex("Text.", "\\documentclass[sigconf]{acmart}");
|
|
47
53
|
|
|
48
54
|
/** The facts of a short agenticdev paper that meets every number in its preset. */
|
|
49
55
|
const goodFacts = (): Record<string, unknown> => ({
|
|
50
|
-
schema:
|
|
56
|
+
schema: 3,
|
|
57
|
+
pages_text: [],
|
|
58
|
+
metadata: {},
|
|
59
|
+
links: [],
|
|
60
|
+
bib_anchor_page: null,
|
|
61
|
+
appendix_anchor_page: null,
|
|
51
62
|
pdf: "paper.pdf",
|
|
52
63
|
pdf_sha256: sha256Hex(new TextEncoder().encode(PDF)),
|
|
53
64
|
venue: "agenticdev",
|
|
@@ -186,25 +197,6 @@ const CASES: Readonly<Record<string, RuleCases>> = {
|
|
|
186
197
|
},
|
|
187
198
|
silent: paper(tex("x")),
|
|
188
199
|
},
|
|
189
|
-
"paper/research-question": {
|
|
190
|
-
reports: {
|
|
191
|
-
tree: {
|
|
192
|
-
[STATUS_FILE]: fixture(
|
|
193
|
-
"paper-research-question/shipped-no-rq/PIPELINE-STATUS.md",
|
|
194
|
-
),
|
|
195
|
-
[TEX_FILE]: fixture("paper-research-question/shipped-no-rq/paper.tex"),
|
|
196
|
-
},
|
|
197
|
-
file: TEX_FILE,
|
|
198
|
-
severity: 1,
|
|
199
|
-
line: 1,
|
|
200
|
-
},
|
|
201
|
-
silent: {
|
|
202
|
-
[STATUS_FILE]: fixture(
|
|
203
|
-
"paper-research-question/shipped-with-rq/PIPELINE-STATUS.md",
|
|
204
|
-
),
|
|
205
|
-
[TEX_FILE]: fixture("paper-research-question/shipped-with-rq/paper.tex"),
|
|
206
|
-
},
|
|
207
|
-
},
|
|
208
200
|
"paper/section-word": {
|
|
209
201
|
reports: {
|
|
210
202
|
tree: paper(tex("As in §2.")),
|
|
@@ -365,17 +357,6 @@ const CASES: Readonly<Record<string, RuleCases>> = {
|
|
|
365
357
|
},
|
|
366
358
|
silent: paper(tex(FORMAL)),
|
|
367
359
|
},
|
|
368
|
-
"tex/missing-input": {
|
|
369
|
-
reports: {
|
|
370
|
-
tree: paper(tex("Text.\n\\input{sections/gone}")),
|
|
371
|
-
file: TEX_FILE,
|
|
372
|
-
severity: 1,
|
|
373
|
-
line: 4,
|
|
374
|
-
},
|
|
375
|
-
silent: paper(tex("Text.\n\\input{sections/here}"), {
|
|
376
|
-
[`${P}/sections/here.tex`]: "More text.\n",
|
|
377
|
-
}),
|
|
378
|
-
},
|
|
379
360
|
"bib/reachable-entry": {
|
|
380
361
|
reports: {
|
|
381
362
|
tree: paper(inlineBib("@book{a,\n title = {A},\n year = {2026}\n}")),
|
|
@@ -411,16 +392,31 @@ const CASES: Readonly<Record<string, RuleCases>> = {
|
|
|
411
392
|
reports: onPdf(built({ page_w_in: 8.264 })),
|
|
412
393
|
silent: built(),
|
|
413
394
|
},
|
|
414
|
-
"
|
|
395
|
+
"format/page-limit": {
|
|
415
396
|
reports: onPdf(
|
|
416
397
|
built({ body_pages: 9, pages_by_type: { body: 9, bib: 2 } }),
|
|
417
398
|
),
|
|
418
399
|
silent: built(),
|
|
419
400
|
},
|
|
420
401
|
"pdf/body-size": {
|
|
421
|
-
reports:
|
|
402
|
+
reports: onPdf(built({ body_pt: 12 })),
|
|
422
403
|
silent: built(),
|
|
423
404
|
},
|
|
405
|
+
"anonymity/identity": {
|
|
406
|
+
reports: onPdf(
|
|
407
|
+
built({ pages_text: ["Ada Example\n", "References\n"] }, BLIND),
|
|
408
|
+
),
|
|
409
|
+
silent: built({ pages_text: ["Anonymous\n", "References\n"] }, BLIND),
|
|
410
|
+
},
|
|
411
|
+
"format/layout-override": {
|
|
412
|
+
reports: {
|
|
413
|
+
tree: aidc("\\linespread{0.9}Text."),
|
|
414
|
+
file: TEX_FILE,
|
|
415
|
+
severity: 2,
|
|
416
|
+
line: 4,
|
|
417
|
+
},
|
|
418
|
+
silent: aidc("\\vspace{-2mm}Text."),
|
|
419
|
+
},
|
|
424
420
|
"pdf/last-page-balance": {
|
|
425
421
|
reports: onPdf(
|
|
426
422
|
built(
|
package/src/rule-docs.test.ts
CHANGED
|
@@ -26,16 +26,13 @@ const AWAITING_PAGE: ReadonlySet<string> = new Set([
|
|
|
26
26
|
"paper/leading-zero",
|
|
27
27
|
"paper/refs-checked",
|
|
28
28
|
"paper/refs-fresh",
|
|
29
|
-
"paper/research-question",
|
|
30
29
|
"paper/section-word",
|
|
31
30
|
"paper/source",
|
|
32
31
|
"paper/stages",
|
|
33
|
-
"pdf/body-size",
|
|
34
32
|
"pdf/fonts",
|
|
35
33
|
"pdf/fresh",
|
|
36
34
|
"pdf/geometry",
|
|
37
35
|
"pdf/last-page-balance",
|
|
38
|
-
"pdf/limits",
|
|
39
36
|
"pdf/measured",
|
|
40
37
|
"pdf/profile",
|
|
41
38
|
"review/frontmatter",
|
package/src/rules-config.ts
CHANGED
|
@@ -64,6 +64,7 @@ export interface ConfigBlock {
|
|
|
64
64
|
readonly languageOptions?: Readonly<Record<string, unknown>>;
|
|
65
65
|
readonly linterOptions?: Readonly<Record<string, unknown>>;
|
|
66
66
|
readonly rules?: Readonly<Record<string, unknown>>;
|
|
67
|
+
readonly settings?: Readonly<Record<string, unknown>>;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
export type Parsed<T> =
|
package/src/structure.harness.ts
CHANGED
|
@@ -79,9 +79,7 @@ try {
|
|
|
79
79
|
check(
|
|
80
80
|
"and the message names the CONSEQUENCE, not a restatement of the condition",
|
|
81
81
|
/are skipped/.test(at("no-scorecard")[0]?.message ?? "") &&
|
|
82
|
-
/stage
|
|
83
|
-
at("no-scorecard")[0]?.message ?? "",
|
|
84
|
-
),
|
|
82
|
+
/stage and source checks/.test(at("no-scorecard")[0]?.message ?? ""),
|
|
85
83
|
);
|
|
86
84
|
check(
|
|
87
85
|
"and the consequence names THIS directory by name",
|
package/src/structure.ts
CHANGED
|
@@ -122,7 +122,7 @@ export function checkStructure(
|
|
|
122
122
|
*/
|
|
123
123
|
function whyMissingMatters(file: string, dirName: string): string {
|
|
124
124
|
if (file === "PIPELINE-STATUS.md")
|
|
125
|
-
return `the stage
|
|
125
|
+
return `the stage and source checks are skipped for \`${dirName}\``;
|
|
126
126
|
return `declared as required by your \`structure\` configuration`;
|
|
127
127
|
}
|
|
128
128
|
|
package/src/tex-paper.test.ts
CHANGED
|
@@ -1,81 +1,98 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The reading every parse-tree rule shares (`readPaper`), the files of the paper's body that lint
|
|
3
|
+
* reads on their own (`bodyFiles`), and where a finding of the assembled text is reported
|
|
4
|
+
* (`reportInPaper`).
|
|
5
5
|
*/
|
|
6
6
|
import { describe, expect, it } from "vitest";
|
|
7
7
|
import { latexReader } from "./adapters/latex/index.ts";
|
|
8
8
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { texInputsDir } from "./package-dirs.ts";
|
|
11
|
+
import { bodyFiles, readPaper, reportInPaper } from "./tex-paper.ts";
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
describe("readPaper — the paper as the rules read it", () => {
|
|
14
|
+
it("reads includes from the paper's own directory", () => {
|
|
15
|
+
const p = readPaper("/p/paper.tex", "x\\input{s/a}y", {
|
|
16
|
+
files: memoryFiles({ "/p/s/a.tex": "AAA" }),
|
|
17
|
+
latex: latexReader,
|
|
18
|
+
});
|
|
19
|
+
expect(p.text).toBe("xAAAy");
|
|
20
|
+
expect(p.main).toBe("paper.tex");
|
|
21
|
+
});
|
|
13
22
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
it("an include the paper's directory lacks is found in paperlint's own inputs, as the build finds it", () => {
|
|
24
|
+
const p = readPaper("/p/paper.tex", "x\\input{guards}y", {
|
|
25
|
+
files: memoryFiles({ [join(texInputsDir(), "guards.tex")]: "G" }),
|
|
26
|
+
latex: latexReader,
|
|
27
|
+
});
|
|
28
|
+
expect({ text: p.text, missing: p.missing }).toEqual({
|
|
29
|
+
text: "xGy",
|
|
30
|
+
missing: [],
|
|
31
|
+
});
|
|
20
32
|
});
|
|
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
33
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
34
|
+
it("the paper's directory comes first: its file wins over paperlint's of the same name", () => {
|
|
35
|
+
const p = readPaper("/p/paper.tex", "\\input{guards}", {
|
|
36
|
+
files: memoryFiles({
|
|
37
|
+
"/p/guards.tex": "mine",
|
|
38
|
+
[join(texInputsDir(), "guards.tex")]: "package",
|
|
39
|
+
}),
|
|
40
|
+
latex: latexReader,
|
|
41
|
+
});
|
|
42
|
+
expect(p.text).toBe("mine");
|
|
53
43
|
});
|
|
44
|
+
});
|
|
54
45
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
46
|
+
describe("bodyFiles — the files of the body lint reads on their own", () => {
|
|
47
|
+
const doc = (preamble: string, body: string): string =>
|
|
48
|
+
`\\documentclass{article}\n${preamble}\n\\begin{document}\n${body}\n\\end{document}\n`;
|
|
49
|
+
const files = (extra: Record<string, string> = {}) =>
|
|
50
|
+
memoryFiles({
|
|
51
|
+
"/p/macros.tex": "\\newcommand{\\x}{y}",
|
|
52
|
+
"/p/sections/a.tex": "A\n\\input{sections/b}\n",
|
|
53
|
+
"/p/sections/b.tex": "B",
|
|
54
|
+
[join(texInputsDir(), "guards.tex")]: "G",
|
|
55
|
+
...extra,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("every file an include in the document body brings in, nested ones too, by absolute path", () => {
|
|
59
|
+
expect(
|
|
60
|
+
bodyFiles("/p/paper.tex", doc("", "\\input{sections/a}"), {
|
|
61
|
+
files: files(),
|
|
62
|
+
latex: latexReader,
|
|
63
|
+
}),
|
|
64
|
+
).toEqual({
|
|
65
|
+
files: ["/p/sections/a.tex", "/p/sections/b.tex"],
|
|
66
|
+
missing: [],
|
|
67
|
+
});
|
|
59
68
|
});
|
|
60
69
|
|
|
61
|
-
it("
|
|
70
|
+
it("not a preamble include (macros), and not a file found only in paperlint's inputs", () => {
|
|
62
71
|
expect(
|
|
63
|
-
|
|
64
|
-
"/p/
|
|
65
|
-
"
|
|
72
|
+
bodyFiles(
|
|
73
|
+
"/p/paper.tex",
|
|
74
|
+
doc("\\input{macros}", "\\input{guards}\nText."),
|
|
75
|
+
{ files: files(), latex: latexReader },
|
|
76
|
+
),
|
|
77
|
+
).toEqual({ files: [], missing: [] });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("an include that resolves nowhere is named, with the file that wrote it", () => {
|
|
81
|
+
expect(
|
|
82
|
+
bodyFiles("/p/paper.tex", doc("", "\\input{gone}"), {
|
|
83
|
+
files: files(),
|
|
84
|
+
latex: latexReader,
|
|
66
85
|
}),
|
|
67
|
-
).toEqual([]);
|
|
86
|
+
).toEqual({ files: [], missing: [{ file: "paper.tex", target: "gone" }] });
|
|
68
87
|
});
|
|
69
|
-
});
|
|
70
88
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
expect(p.main).toBe("paper.tex");
|
|
89
|
+
it("a main file with no document environment: every include is the body", () => {
|
|
90
|
+
expect(
|
|
91
|
+
bodyFiles("/p/paper.tex", "\\input{sections/b}", {
|
|
92
|
+
files: files(),
|
|
93
|
+
latex: latexReader,
|
|
94
|
+
}),
|
|
95
|
+
).toEqual({ files: ["/p/sections/b.tex"], missing: [] });
|
|
79
96
|
});
|
|
80
97
|
});
|
|
81
98
|
|